본문 바로가기
iOS/SWIFT

[Swift] Chapter 10. 프로퍼티와 메서드

by 원만사 2021. 10. 8.
반응형

프로퍼티는 클래스, 구조체 또는 열거형 등에 관련된 값을 뜻한다. 메서드는 특정 타입에 관련된 함수를 뜻한다.

 

10.1 프로퍼티


프로퍼티는 저장 프로퍼티(Stored Properties)와 연산 프로퍼티(Computed Properties), 타입 프로퍼티(Type Properties)로 나눌 수 있다. 저장 프로퍼티는 인스턴스의 변수 또는 상수를 의미한다. 연산 프로퍼티는 값을 저장한 것이 아니라 특정 연산을 실행한 결괏값을 의미한다. 연산 프로퍼티는 클래스, 구조체, 열거형에 쓰일 수 있다. 저장 프로퍼티는 구조체와 클래스에서만 사용할 수 있다. 저장 프로퍼티와 연산 프로퍼티는 특정 타입의 인스턴스에 사용되는 것을 뜻하지만 특정 타입에 사용되는 프로퍼티도 있는데 이를 타입 프로퍼티라고 한다.

프로퍼티의 값이 변하는 것을 감시하는 프로퍼티 감시자(Property Observers)도 있다. 프로퍼티 감시자는 프로퍼티의 값이 변할 때 값의 변화에 따른 특정 작업을 실행한다. 저장 프로퍼티에 적용할 수 있으며 부모클래스로부터 상속받을 수 있다.

 

10.1.1 저장 프로퍼티

인스턴스와 연관된 값을 저장하는 가장 단순한 개념의 프로퍼티이다. 저장 프로퍼티를 정의할 때 프로퍼티 기본값과 초깃값을 지정해줄 수 있다.

NOTE_구조체와 클래스의 저장 프로퍼티 구조체의 저장 프로퍼티가 옵셔널이 아니더라도, 구조체는 저장 프로퍼티를 모두 포함하는 이니셜라이저를 자동으로 생상한다. 하지만 클래스의 저장 프로퍼티는 옵셔널이 아니라면 프로퍼티 기본값을 지정해주거나 사용자 정의 이니셜라이저를 통해 반드시 초기화해주어야 한다. 또, 클래스 인스턴스의 상수 프로퍼티는 인스턴스가 초기화(이니셜라이즈)될 때 한 번만 값을 할당할 수 있으며, 자식클래스에서 이 초기화를 변경(재정의)할 수 없다.

 

[코드 10-1]은 아주 기본적인 저장 프로퍼티의 선언과 인스턴스 초기화 방법이다.

 

/*
	코드 10-1. 저장 프로퍼티의 선언 및 인스턴스 생성
*/

// 좌표
struct CoordinatePoint {
    var x: Int // 저장 프로퍼티
    var y: Int // 저장 프로퍼티
}

// 구조체에는 기본적으로 저장 프로퍼티를 매개변수로 갖는 이니셜라이저가 있다.
let yagomPoint: CoordinatePoint = CoordinatePoint(x: 10, y: 5)

// 사람의 위치 정보
class Position {
    var point: CoordinatePoint
    // 저장 프로퍼티(변수) - 위치(Point)는 변경될 수 있음을 뜻한다.
    let name: String // 저장 프로퍼티(상수)
    
    // 프로퍼티 기본값을 지정해주지 않는다면 이니셜라이저를 따로 정의해주어야 한다.
    init(name: String, currentPoint: CoordinatePoint) {
        self.name = name
        self.point = currentPoint
    }
}


// 사용자 정의 이니셜라이저를 호출해야만 한다.
// 그렇지 않으면 프로퍼티 초깃값을 할당할 수 없기 때문에 인스턴스 생성이 불가능하다.
let yagoomPosition: Position = Position(name: "yagom", currentPoint: yagomPoint)

구조체는 프로퍼티에 맞는 이니셜라이저를 자동으로 제공하지만 클래스는 그렇지 않아서 클래스 인스턴스의 저장 프로퍼티를 사용하는 일은 좀 번거롭다. 하지만 클래스의 저장 프로퍼티에 초깃값을 지정해주면 따로 사용자 정의 이니셜라이저를 구현해줄 필요가 없다.

 

/*
	코드 10-2. 저장 프로퍼티의 초깃값 지정
*/

// 좌표
struct CoordinatePoint {
    var x: Int = 0// 저장 프로퍼티
    var y: Int = 0 // 저장 프로퍼티
}

// 프로퍼티의 초깃값을 할당했다면 굳이 전달인자로 초깃값을 넘길 필요가 없다.
let yagomPoint: CoordinatePoint = CoordinatePoint()

// 물론 기존에 초깃값을 할당할 수 있는 이니셜라이저도 사용 가능하다.
let wizplanPoint: CoordinatePoint = CoordinatePoint(x: 10, y: 5)

// 사람의 위치 정보
class Position {
    var point: CoordinatePoint = CoordinatePoint() // 저장 프로퍼티
    var name: String = "Unknown" // 저장 프로퍼티
}

// 초깃값을 지정해줬다면 사용자 정의 이니셜라이저를 사용하지 않아도 된다.
let yagomPosition: Position = Position()

yagomPosition.point = yagomPoint
yagomPosition.name = "yagom"

 

인스턴스를 생성할 때 이니셜라이저를 통해 초깃값을 보내야 하는 이유는 프로퍼티가 옵셔널이 아닌 값으로 선언되었기 때문이다. 그러므로 인스턴스는 생성할 때 프로퍼티에 값이 꼭 있는 상태여야 한다. 하지만 옵셔널이라면 굳이 초깃값을 넣어주지 않아도 된다. 즉, 이니셜라이저에서 옵셔널 프로퍼티에 꼭 값을 할당해주지 않아도 된다.

 

/*
	코드 10-3. 옵셔널 저장 프로퍼티
*/

// 좌표
struct CoordinatePoint {
    // 위치는 x, y 값이 모두 있어야 하므로 옵셔널이면 안된다.
    var x: Int
    var y: Int
}

// 사람의 위치 정보
class Position {
    // 현재 사람의 위치를 모를 수도 있다. - 옵셔널
    var point: CoordinatePoint?
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

// 이름은 필수지만 위치는 모를 수도 있다.
let yagomPosition: Position = Position(name: "yagom")

// 위치를 알게되면 그 때 위치 값을 할당해준다.
yagomPosition.point = CoordinatePoint(x: 20, y: 10)

 

이렇게 옵셔널과 이니셜라이저를 적절히 사용하면 다른 프로그래머가 사용할 때, 내가 처음 의도했던 대로 구조체와 클래스를 사용할 수 있도록 유도할 수 있다.

10.1.2 지연 저장 프로퍼티

필요할 때 값이 할당되는 지연 저장 프로퍼티(Lazy Stored Properties)가 있다. 지연 저장 프로퍼티는 호출이 있어야 값을 초기화하며, 이때 lazy 키워드를 사용한다.

상수는 인스턴스가 완전히 생성되기 전에 초기화해야 하므로 필요할 때 값을 할당하는 지연 저장 프로퍼티와는 맞지 않다. 따라서 지연 저장 프로퍼티는 var 키워드를 사용하여 변수로 정의한다.

지연 저장 프로퍼티는 주로 복잡한 클래스가 구조체를 구현할 때 많이 사용된다. 인스턴스를 초기화하면서 저장 프로퍼티로 쓰이는 인스턴스들이 한 번에 생성되어야 한다면? 굳이 모든 저장 프로퍼티를 사용할 필요가 없다면? 이럴때 지연 저장 프로퍼티를 사용한다. 지연 저장 프로퍼티를 사용하면 불필요한 성능저하나 공간 낭비를 줄일 수 있다.

 

/*
	코드 10-4. 지연 저장 프로퍼티
*/

struct CoordinatePoint {
    var x: Int = 0
    var y: Int = 0
}

class Position {
    lazy var point: CoordinatePoint = CoordinatePoint()
    let name: String
    
    init(name: String) {
        self.name = name
    }
}

let yagomPosition: Position = Position(name: "yagom")

// 이 코드를 통해 point 프로퍼티로 처음 접근할 때
// point 프로퍼티의 CoordinatePoint가 생성된다.
print(yagomPosition.point) // x: 0, y: 0
NOTE_다중 스레드와 지연 저장 프로퍼티 다중 스레드 환경에서 지연 저장 프로퍼티에 동시다발적으로 접근할 때는 한 번만 초기화된다는 보장이 없다. 생성되지 않은 지연 저장 프로퍼티에 많은 스레드가 비슷한 시점에 접근한다면, 여러 번 초기화될 수 있다.

 

10.1.3 연산 프로퍼티

연산 프로퍼티는 실제 값을 저장하는 프로퍼티가 아니라, 특정 상태에 따른 값을 연산하는 프로퍼티이다. 인스턴스 내/외부의 값을 연산하여 적절한 값을 돌려주는 접근자(getter)의 역할이나 은닉화된 내부의 프로퍼티 값을 간적접으로 설정하는 설정자(setter)의 역할을 할 수도 있다.

클래스, 구조체, 열거형에 연산 프로퍼티를 정의할 수 있다.

메서드룰 두고 연산 프로퍼티를 쓰는 이유가 무엇일까? 인스턴스 외부에서 메서드를 통해 인스턴스 내부 값에 접근하려면 메서드를 두 개(접근자, 설정자) 구현해야 한다. 또한 이를 감수하고 메서드로 구현한다 해도 두 메서드가 분산 구현되어 코드의 가독성이 나빠질 수 있다. 프로퍼티를 이용하면 메서드 형식보다 훨씬 더 간편하고 직관적일 수 있다.

연산 프로퍼티는 접근자인 get 메서드만 구현해둔 것처럼 읽기 전용 상태로 구현하기 쉽지만, 쓰기 전용 상태로 구현할 수 없다는 단점이 있다. 메서드로는 설정자 메서드만 구현하여 쓰기 전용 상태로 구현할 수 있지만 연산 프로퍼티는 그것이 불가능하다.

먼저 [코드 10-5]에서 연산 프로퍼티를 적용하지 않고 메서드로 접근자와 설정자를 구현한 코드를 살펴보자.

 

/*
	코드 10-5. 메서드로 구현된 접근자와 설정자
*/

struct CoordinatePoint {
    var x: Int // 저장 프로퍼티
    var y: Int // 저장 프로퍼티
    
    // 대칭점을 구하는 메서드 - 접근자
    // Self는 타입 자기 자신을 뜻한다
    // Self 대신 CoordinatePoint를 사용해도 된다.
    func oppositePoint() -> Self {
        return CoordinatePoint(x: -x, y: -y)
    }
    
    // 대칭점을 설정하는 메서드 - 설정자
    // mutating 키워드에 관한 내용은 10.2.1절 참고
    mutating func setOppositePoint(_ opposite: CoordinatePoint) {
        x = -opposite.x
        y = -opposite.y
    }
}


var yagomPosition: CoordinatePoint = CoordinatePoint(x: 10, y: 20)

// 현재 좌표
print(yagomPosition)  // 10, 20

// 대칭 좌표
print(yagomPosition.oppositePoint()) // -10, -20

// 대칭 좌표를 (15, 10)으로 설정하면
yagomPosition.setOppositePoint(CoordinatePoint(x: 15, y: 10))

// 현재 좌표는 -15, -10으로 설정된다.
print(yagomPosition) // -15, -10

[코드 10-5]에서는 접근자와 설정자 이름의 일관성을 유지하기 힘들며, 해당 포인트에 접근할 때와 설정할 때 사용되는 코드를 한 번에 읽기도 쉽지 않다.

[코드 10-6]은 연산 프로퍼티를 사용한 코드다. 연산 프로퍼티를 사용하면 위의 두 메서드를 좀 더 간결하고 확실하게 표현할 수 있다.

 

/*
	코드 10-6. 연산 프로퍼티의 정의와 사용
*/

struct CoordinatePoint {
    var x: Int // 저장 프로퍼티
    var y: Int // 저장 프로퍼티
    
    // 대칭 좌표
    var oppositePoint: CoordinatePoint { // 연산 프로퍼티
        // 접근자
        get {
            return CoordinatePoint(x: -x, y: -y)
        }
        
        // 설정자
        set(opposite) {
            x = -opposite.x
            y = -opposite.y
        }
    }
}


var yagomPosition: CoordinatePoint = CoordinatePoint(x: 10, y: 20)

// 현재 좌표
print(yagomPosition)  // 10, 20

// 대칭 좌표
print(yagomPosition.oppositePoint) // -10, -20

// 대칭 좌표를 (15, 10)으로 설정하면
yagomPosition.oppositePoint = CoordinatePoint(x: 15, y: 10)

// 현재 좌표는 -15, -10으로 설정된다.
print(yagomPosition) // -15, -10

이런 식으로 연산 프로퍼티를 사용하면 하나의 프로퍼티에 접근자와 설정자가 모두 모여있고, 해당 프로퍼티가 어떤 역할을 하는지 좀 더 명확하게 표현 가능하다.

설정자의 매개변수로 원하는 이름을 소괄호 안에 명시해주면 set 메서드 내부에서 전달받은 전달인자를 사용할 수 있다. 관용적인 표현으로 newValue로 매개변수 이름을 대신할 수 있다. 그럴 경우에는 매개변수를 따로 표기하지 말아야 한다. 또 접근자 내부의 코드가 단 한 줄이고, 그 결괏값의 타입이 프로퍼티의 타입과 같다면 return 키워드를 생략해도 그 결괏값이 접근자의 반환값이 된다.

 

/*
	코드 10-7. 매개변수 이름을 생략한 설정자
*/

struct CoordinatePoint {
    var x: Int // 저장 프로퍼티
    var y: Int // 저장 프로퍼티
    
    // 대칭 좌표
    var oppositePoint: CoordinatePoint { // 연산 프로퍼티
        // 접근자
        get {
            // 이곳에서 return 키워들르 생략할 수 있다.
            CoordinatePoint(x: -x, y: -y)
        }
        
        // 설정자
        set {
            x = -newValue.x
            y = -newValue.y
        }
    }
}

 

읽기 전용으로 연산 프로퍼티를 사용하려면 get 메서드만 사용하면 된다.

 

10.1.4 프로퍼티 감시자

프로퍼티 감시자를 사용하면 프로퍼티의 값이 변경됨에 따라 적절한 작업을 취할 수 있다. 프로퍼티 감시자는 프로퍼티의 값이 새로 할당될 때마다 호출한다. 이때 변경되는 값이 현재의 값과 같더라고 호출한다.

저장 프로퍼티뿐만 아니라 프로퍼티를 재정의해 상속받은 저장 프로퍼티 또는 연산 프로퍼티에도 적용할 수 있다. 물론 상속받지 않은 연산 프로퍼티에는 프로퍼티 감시자를 사용할 필요가 없으며 할 수도 없다. 연산 프로퍼티의 접근자와 설정자를 통해 프로퍼티 감시자를 구현할 수 있기 때문이다. 연산 프로퍼티는 상속받았을 때만 프로퍼티 재정의를 통해 프로퍼티 감시자를 사용한다.

프로퍼티 감시자에는 프로퍼티의 값이 변경되기 직전에 호출하는 willSet 메서드와 프로퍼티의 값이 변경된 직후에 호출하는 didSet 메서드가 있다.

두 메서드에는 매개변수가 하나씩 있는데 willSet 메서드의 전달인자는 프로퍼티가 변경될 값이고, didSet 메서드의 전달인자는 프로퍼티가 변경되기 전의 값이다. 매개변수의 이름을 따로 지정하지 않으면 willSet 메서드에는 newValue가, didSet 메서드에는 oldValue라는 매개변수 이름이 자동 지정된다.

 

NOTE_oldValue와 didSet didSet 감시자 코드 블록 내부에서 oldValue 값을 참조하지 않거나 매개변수 목록에 명시적으로 매개변수를 적어(예: didSet(oldValueName))주지 않으면 didSet 코드 블록이 실행되지 않는다.

 

/*
	코드 10-9. 프로퍼티 감시자
*/

class Account {
    var credit: Int = 0 {
        willSet {
            print("잔액이 \(credit)원에서 \(newValue)원으로 변경될 예정이다.")
        }
        
        didSet {
            print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었다.")
        }
    }
}


let myAccount: Account = Account()
// 잔액이 0원에서 1000원으로 변경될 예정이다.
myAccount.credit = 1000
// 잔액이 0원에서 1000원으로 변경되었다.

 

클래스를 상속받았다면 기존의 연산 프로퍼티를 재정의하여 프로퍼티 감시자를 구현할 수도 있다. 연산 프로퍼티를 재정의해도 기존의 연산 프로퍼티 기능(접근자와 설정자, get과 set 메서드)은 동작한다.

 

/*
	코드 10-10. 상속받은 연산 프로퍼티의 프로퍼티 감시자 구현
*/

class Account {
    var credit: Int = 0 {
        willSet {
            print("잔액이 \(credit)원에서 \(newValue)원으로 변경될 예정입니다.")
        }
        
        didSet {
            print("잔액이 \(oldValue)원에서 \(credit)원으로 변경되었습니다.")
        }
    }
    
    var dollarValue: Double { // 연산 프로퍼티
        get {
            return Double(credit) / 1000.0
        }
        
        set {
            credit = Int(newValue * 1000)
            print("잔액을 \(newValue)달러로 변경 중입니다.")
        }
    }
}

class ForeignAccount: Account {
    override var dollarValue: Double {
        willSet {
            print("잔액이 \(dollarValue)달러에서 \(newValue)달러로 변경될 예정입니다.")
        }
        
        didSet {
            print("잔액이 \(oldValue)달러에서 \(dollarValue)달러로 변경되었습니다.")
        }
    }
}

let myAccount: ForeignAccount = ForeignAccount()
// 잔액이 0원에서 1000원으로 변경될 예정입니다.
myAccount.credit = 1000
// 잔액이 0원에서 1000원으로 변경되었습니다.

// 잔액이 1.0달러에서 2.0달러로 변경될 예정입니다.
// 잔액이 1000원에서 2000원으로 변경될 예정입니다.
// 잔액이 1000원에서 2000원으로 변경되었습니다.

myAccount.dollarValue = 2 // 잔액을 2.0달러로 변경 중입니다.
// 잔액이 1.0달러에서 2.0달러로 변경되었습니다.
NOTE_입출력 매개변수와 프로퍼티 감시자 만약 프로퍼티 감시자가 있는 프로퍼티를 함수의 입출력 매개변수의 전달인자로 전달한다면 항상 willSet과 didSet 감시자를 호출한다. 함수 내부에서 값이 변경되든 되지 않든 간에 함수가 종료되는 시점에 값을 다시 쓰기 때문이다.

 

10.1.5 전역변수와 지역변수

연산 프로퍼티와 프로퍼티 감시자는 전역변수와 지역변수 모두에 사용할 수 있다. 따라서 프로퍼티에 한정하지 않고, 전역에서 쓰일 수 있는 변수와 상수에도 두 기능을 사용할 수 있다.

전역변수 또는 전역상수는 지연 저장 프로퍼티처럼 처음 접근할 때 최.,초로 연산이 이루어진다. lazy 키워드를 사용하여 연산을 늦출 필요가 없다. 반대로 지역변수 및 지역상수는 절대로 지연 연산되지 않는다.

 

/*
	코드 10-11. 저장변수의 감시자와 연산변수
*/

var wonInPocket: Int = 2000 {
    willSet {
        print("주머니의 돈이 \(wonInPocket)원에서 \(newValue)원으로 변경될 예정입니다.")
    }
    
    didSet {
        print("주머니의 돈이 \(oldValue)원에서 \(wonInPocket)원으로 변경되었습니다.")
    }
}


var dollarInPocket: Double {
    get {
        return Double(wonInPocket) / 1000.0
    }
    
    set {
        wonInPocket = Int(newValue * 1000.0)
        print("주머니의 달러를 \(newValue)달러로 변경 중입니다.")
    }
}

// 주머니의 돈이 2000원에서 3500원으로 변경될 예정입니다.
// 주머니의 돈이 2000원에서 3500원으로 변경되었습니다.
dollarInPocket = 3.5  // 주머니의 달러를 3.5달러로 변경 중입니다.

 

10.1.6 타입 프로퍼티

인스턴스 프로퍼티는 인스턴스를 새로 생성할 때마다 초깃값에 해당하는 값이 프로퍼티의 값이 되고, 인스턴스마다 다른 값을 지닐 수 있다.

각각의 인스턴스가 아닌 타입 자체에 속하는 프로퍼티를 타입 프로퍼티라고 한다. 인스턴스의 생성 여부와 상관없이 타입 프로퍼티의 값은 하나며, 그 타입의 모든 인스턴스가 공통으로 사용하는 값, 모든 인스턴스에서 공용으로 접근하고 값을 변경할 수 있는 변수등을 정의할 때 유용하다.

타입 프로퍼티에는 두 가지가 있다.

[저장 타입 프로퍼티]

  • 변수 또는 상수로 선언할 수 있음
  • 반드시 초깃값을 설정해야 하며 지연 연산된다(lazy 키워드로 표시해주지는 않는다).
  • 지연 저장 프로퍼티와는 조금 다르게 다중 스레드 환경이라고 하더라도 단 한 번만 초기화된다는 보장을 받는다.

 

[연산 타입 프로퍼티]

  • 변수로만 선언 가능

 

/*
	코드 10-12. 타입 프로퍼티와 인스턴스 프로퍼티
*/

class AClass {
    // 저장 타입 프로퍼티
    static var typeProperty: Int = 0
    
    // 저장 인스턴스 프로퍼티
    var instanceProperty: Int = 0 {
        didSet {
            // Self.typeProperty는
            // AClass.typeProperty와 같은 표현이다.
            Self.typeProperty = instanceProperty + 100
        }
    }
    
    // 연산 타입 프로퍼티
    static var typeComputedProperty: Int {
        get {
            return typeProperty
        }
        
        set {
            typeProperty = newValue
        }
    }
}


AClass.typeProperty = 123

let classInstance: AClass = AClass()
classInstance.instanceProperty = 100

print(AClass.typeProperty) // 200
print(AClass.typeComputedProperty) // 200

 

10.1.7 키 경로

프로퍼티는 값을 바로 꺼내오는 것이 아니라 어떤 프로퍼티의 위치만 참조하도록 할 수 있다. 바로 키 경로(key path)를 활용하는 방법이다. 키 경로를 사용하여 간접적으로 특정 타입의 어떤 프로퍼티 값을 가리켜야 할지 미리 지정해두고 사용할 수 있다.

키 경로 타입은 AnyKeyPath라는 클래스로부터 파생된다. 키 경로는 역슬래시(\)와 타입, 마침표(.) 경로로 구성된다.

\타입이름.경로.경로.경로
// 여기서 경로는 프로퍼티 이름이라고 생각하면 된다.

 

/*
	코드 10-14. 키 경로 타입의 타입 확인
*/

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

struct Stuff {
    var name: String
    var owner: Person
}

print(type(of: \Person.name)) // ReferenceWritableKeyPath<Person, String>
print(type(of: \Stuff.name)) // WritableKeyPath<Stuff, String>

 

각 인스턴스의 keyPath 서브스크립트 메서드에 키 경로를 전달하여 프로퍼티에 접근할 수 있다.

/*
	코드 10-16. keyPath 서브스크립트와 키 경로 활용
*/

class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

struct Stuff {
    var name: String
    var owner: Person
}

let yagom = Person(name: "yagom")
let hana = Person(name: "hana")
let macbook = Stuff(name: "MacBook Pro", owner: yagom)
var iMac = Stuff(name: "iMac", owner: yagom)
let iPhone = Stuff(name: "iPhone", owner: hana)

let stuffNameKeyPath = \Stuff.name
let ownerKeyPath = \Stuff.owner

// \Stuff.owner.name과 같은 표현이 된다.
let ownerNameKeyPath = ownerKeyPath.appending(path: \.name)

// 키 경로와 서브스크립트를 이용해 프로퍼티에 접근하여 값을 가져온다.
print(macbook[keyPath: stuffNameKeyPath]) // MacBook Pro
print(iMac[keyPath: stuffNameKeyPath]) // iMac
print(iPhone[keyPath: stuffNameKeyPath]) // iPhone

print(macbook[keyPath: ownerNameKeyPath]) // yagom
print(iMac[keyPath: ownerNameKeyPath]) // yagom
print(iPhone[keyPath: ownerNameKeyPath]) // hana

// 키 경로와 서브스크립트를 이용해 프로퍼티에 접근하여 값을 변경한다.
iMac[keyPath: stuffNameKeyPath] = "iMac Pro"
iMac[keyPath: ownerKeyPath] = hana

print(iMac[keyPath: stuffNameKeyPath]) // iMac Pro
print(iMac[keyPath: ownerNameKeyPath]) // hana

// 상수로 지정한 값 타입과 읽기 전용 프로퍼티는 키 경로 서브스크립트로도 값을 바꿔줄 수 없다.

 

키 경로를 잘 활용하면 프로토콜과 마찬가지로 타입 간의 의존성을 낮추는 데 많은 도움을 준다.

NOTE_접근수준과 키 경로 키 경로는 타입 외부로 공개된 인스턴스 프로퍼티 혹은 서브스크립트에 한하여 표현할 수 있다.

 

자신을 나타내는 키 경로인 \.self를 사용하면 인스턴스 그 자체를 표현하는 키 경로가 된다. 또, 컴파일러가 타입을 유추할 수 있는 경우에는 키 경로에서 타입 이름을 생략할 수도 있다.

스위프트 5.2 버전부터 (SomeType) → Value 타입의 클로저를 키 경로 표현으로 대체하여 사용할 수 있다.

 

/*
	코드 10-17. 클로저를 대체할 수 있는 키 경로 표현
*/

struct Person {
    let name: String
    let nickname: String?
    let age: Int
    
    var isAdult: Bool {
        return age > 18
    }
}

let yagom: Person = Person(name: "yagom", nickname: "bear", age: 100)
let hana: Person = Person(name: "hana", nickname: "na", age: 100)
let happy: Person = Person(name: "happy", nickname: nil, age: 3)

let family: [Person] = [yagom, hana, happy]
let names: [String] = family.map(\.name) // ["yagom", "hana", "happy"]
let nicknames: [String] = family.compactMap(\.nickname) // ["bear", "na"]
let adults: [String] = family.filter(\.isAdult).map(\.name) // ["yagom", "hana"]

 

10.2 메서드


메서드는 특정 타입에 관련된 함수를 뜻한다. 클래스, 구조체, 열거형 등은 실행하는 기능을 캡슐화한 인스턴스 메서드를 정의할 수 있다. 또한, 타입 자체와 관련된 기능을 실행하는 타입 메서드를 정의할 수도 있다.

 

10.2.1 인스턴스 메서드

인스턴스 메서드는 특정 타입의 인스턴스에 속한 함수를 뜻한다. 인스턴스 내부의 프로퍼티 값을 변경하거나 특정 연산 결과를 반환하는 등 인스턴스와 관련된 기능을 실행한다.

인스턴스 메서드는 함수와 달리 특정 타입 내부에 구현한다. 따라서 인스턴스가 존재할 때만 사용할 수 있다.

 

/*
	코드 10-18. 클래스의 인스턴스 메서드
*/

class LevelClass {
    // 현재 레벨을 저장하는 저장 프로퍼티
    var level: Int = 0 {
        // 프로퍼티 값이 변경되면 호출하는 프로퍼티 감시자
        didSet {
            print("Level \(level)")
        }
    }
    
    // 레벨이 올랐을 때 호출할 메서드
    func levelUp() {
        print("Level Up!")
        level += 1
    }
    
    // 레벨이 감소했을 때 호출할 메서드
    func levelDown() {
        print("Level Down")
        level -= 1
        if level < 0 {
            reset()
        }
    }
    
    // 특정 레벨로 이동할 때 호출할 메서드
    func jumpLevel(to: Int) {
        print("Jump to \(to)")
        level = to
    }
    
    // 레벨을 초기화할 때 호출할 메서드
    func reset() {
        print("Reset!")
        level = 0
    }
}


var levelClassInstance: LevelClass = LevelClass()
levelClassInstance.levelUp() // Level Up!
//Level 1

levelClassInstance.levelDown() // Level Down
// Level 0

levelClassInstance.levelDown() // Level Down
// Level -1
// Reset!
// Level 0

levelClassInstance.jumpLevel(to: 3) // Jump to 3
// Level3

 

자신의 프로퍼티 값을 수정할 때 클래스의 인스턴스 메서드는 크게 신경 쓸 필요가 없지만, 구조체나 열거형 등은 값 타입이므로 메서드 앞에 mutating 키워드를 붙여서 해당 메서드가 인스턴스 내부의 값을 변경한다는 것을 명시해야 한다.

 

/*
	코드 10-19. mutating 키워드의 사용
*/

struct LevelStruct {
    // 현재 레벨을 저장하는 저장 프로퍼티
    var level: Int = 0 {
        // 프로퍼티 값이 변경되면 호출하는 프로퍼티 감시자
        didSet {
            print("Level \(level)")
        }
    }
    
    // 레벨이 올랐을 때 호출할 메서드
    mutating func levelUp() {
        print("Level Up!")
        level += 1
    }
    
    // 레벨이 감소했을 때 호출할 메서드
    mutating func levelDown() {
        print("Level Down")
        level -= 1
        if level < 0 {
            reset()
        }
    }
    
    // 특정 레벨로 이동할 때 호출할 메서드
    mutating func jumpLevel(to: Int) {
        print("Jump to \(to)")
        level = to
    }
    
    // 레벨을 초기화할 때 호출할 메서드
    mutating func reset() {
        print("Reset!")
        level = 0
    }
}


var levelClassInstance: LevelStruct = LevelStruct()
levelClassInstance.levelUp() // Level Up!
//Level 1

levelClassInstance.levelDown() // Level Down
// Level 0

levelClassInstance.levelDown() // Level Down
// Level -1
// Reset!
// Level 0

levelClassInstance.jumpLevel(to: 3) // Jump to 3
// Level3

 

self 프로퍼티

모든 인스턴스는 암시적으로 생성된 self 프로퍼티를 갖는다. 자기 자신을 가리키는 프로퍼티이다. self 프로퍼티는 인스턴스를 더 명확히 지칭하고 싶을 때 사용한다.

또, self 프로퍼티의 다른 용도는 값 타입 인스턴스 자체의 값을 치환할 수 있다. 클래스의 인스턴스는 참조 타입이라서 self 프로퍼티에 다른 참조를 할당할 수 없는데, 구조체나 열거형 등은 self 프로퍼티를 사용하여 자신 자체를 치환할 수도 있다.

 

/*
	코드 10-21. self 프로퍼티와 mutating 키워드
*/

class LevelClass {
    var level: Int = 0
    
    func reset() {
        // 오류!! self 프로퍼티 참조 변경 불가!
//        self = LevelClass()
    }
}

struct LevelStruct {
    var level: Int = 0
    
    mutating func levelUp() {
        print("Level Up!")
        level += 1
    }
    
    mutating func reset() {
        print("Reset!")
        self = LevelStruct()
    }
}

var levelStructInstance: LevelStruct = LevelStruct()
levelStructInstance.levelUp() // Level Up!
print(levelStructInstance.level) // 1

levelStructInstance.reset() // Reset!
print(levelStructInstance.level) // 0

enum OnOffSwitch {
    case on, off
    
    mutating func nextState() {
        self = self == .on ? .off : .on
    }
}

var toggle: OnOffSwitch = OnOffSwitch.off
toggle.nextState()
print(toggle) // on

 

 

인스턴스를 함수처럼 호출하도록 하는 메서드

특정 타입의 인스턴스를 문법적으로 함수를 사용하는 것처럼 보이게 할 수 있다. 인스턴스를 함수처럼 호출할 수 있도록 하려면 callAsFunction 이라는 이름의 메서드를 구현하면 된다. 이 메서드는 매개변수와 반환 타입만 다르다면 개수에 제한 없이 원하는만큼 만들 수 있다. mutating 키워드도 사용할 수 있고, throws와 rethrows도 함께 사용할 수 있다.

 

/*
	코드 10-22. Puppy 구조체에 callAsFunction 메서드 구현
*/

struct Puppy {
    var name: String = "멍멍이"
    
    func callAsFunction() {
        print("멍멍")
    }
    
    func callAsFunction(destination: String) {
        print("\(destination)(으)로 달려갑니다")
    }
    
    func callAsFunction(something: String, times: Int) {
        print("\(something)(을)를 \(times)번 반복합니다.")
    }
    
    func callAsFunction(color: String) -> String {
        return "\(color) 응가"
    }
    
    mutating func callAsFunction(name: String) {
        self.name = name
    }
}


var doggy: Puppy = Puppy()
doggy.callAsFunction() // 멍멍
doggy() // 멍멍
doggy.callAsFunction(destination: "집") // 집(으)로 달려갑니다
doggy(destination: "뒷동산") // 뒷동산(으)로 달려갑니다
doggy(something: "재주넘기", times: 3) // 재주넘기(을)를 3번 반복합니다
print(doggy(color: "무지개색")) // 무지개색 응가
doggy(name: "댕댕이")
print(doggy.name) // 댕댕이

callAsFunction은 메서드를 호출하는 것 외에 함수 표현으로는 사용할 수 없다. 즉,

let function: (String) -> Void = doggy(destination:)

위와 같이 사용할 수는 없다.

대신 다음과 같이 표현해야 한다.

let function: (String) -> Void = doggy.callAsFunction(destination:)

 

10.2.2 타입 메서드

타입 자체에 호출이 가능한 메서드를 타입 메서드라고 부른다. 메서드 앞에 static 키워드를 사용하여 타입 메서드임을 나타내준다. 클래스의 타입 메서드는 static 키워드와 class 키워드를 사용할 수 있는데 static으로 정의하면 상속 후 메서드 재정의가 불가능하고 class로 정의하면 상속 후 메서드 재정의가 가능하다.

/*
	코드 10-23. 클래스의 타입 메서드
*/

class AClass {
    static func staticTypeMethod() {
        print("AClass staticTypeMethod")
    }
    
    class func classTypeMethod() {
        print("Aclass classtypeMethod")
    }
}

class BClass: AClass {
    /*
    // 오류 발생!! 재정의 불가!
    override static func staticTypeMethod() {
        
    }
    */
    
    override class func classTypeMethod() {
        print("BClass classTypeMethod")
    }
}

AClass.staticTypeMethod() // AClass staticTypeMethod
AClass.classTypeMethod()  // AClass classTypeMethod
BClass.classTypeMethod()  // BClass classTypeMethod

 

또, 타입 메서드는 인스턴스 메서드와는 달리 self 프로퍼티가 타입 그 자체를 가리킨다는 점이 다르다. 인스턴스 메서드에서는 self가 인스턴스를 가리킨다면 타입 메서드의 self는 타입을 가리킨다. 그래서 타입 메서드 내부에서 타입 이름과 self는 같은 뜻이라고 볼 수 있다.

 

/*
	코드 10-24. 타입 프로퍼티와 타입 메서드의 사용
*/

// 시스템 음량은 한 기기에서 유일한 값이어야 한다.
struct SystemVolume {
    // 타입 프로퍼티를 사용하면 언제나 유일한 값이 된다.
    static var volume: Int = 5
    
    // 타입 프로퍼티를 제어하기 위해 타입 메서드를 사용한다.
    static func mute() {
        // SystemVolume.volume = 0과 같은 표현이다.
        // Self.volume = 0과도 같은 표현이다.
        self.volume = 0
    }
}


// 내비게이션 역할은 여러 인스턴스가 수행할 수 있다.
class Navigation {
    // 내비게이션 인스턴스마다 음량을 따로 설정할 수 있다.
    var volume: Int = 5
    
    // 길 안내 음성 재생
    func guideWay() {
        // 내비게이션 외 다른 재생원 음소거
        SystemVolume.mute()
    }
    
    // 길 안내 음성 종료
    func finishGuideWay() {
        // 기존 재생원 음량 복구
        SystemVolume.volume = self.volume
    }
}

SystemVolume.volume = 10

let myNavi: Navigation = Navigation()

myNavi.guideWay()
print(SystemVolume.volume) // 0

myNavi.finishGuideWay()
print(SystemVolume.volume) // 5

 

 

스위프트 프로그래밍: Swift 5 - 교보문고

객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번에! | 스위프트를 제대로 이해하고 싶은 개발자를 위한 책스위프트는 iOS와 macOS용 애플리케이션 개발에 주로 사용하는 프로그래밍 언어입니

www.kyobobook.co.kr

 

반응형

'iOS > SWIFT' 카테고리의 다른 글

[Swift] Chapter 12. 접근제어  (0) 2021.10.08
[Swift] Chapter 11. 인스턴스 생성 및 소멸  (0) 2021.10.08
[Swift] Chapter 09. 구조체와 클래스  (0) 2021.10.08
[Swift] Chapter 08. 옵셔널  (0) 2021.10.08
[Swift] Chapter 07. 함수  (0) 2021.10.08

댓글