Swift 정리 - 10. 프로퍼티와 메서드


Swift에서 인스턴스를 생성하는 방법과 인스턴스가 소멸할 때 무슨 일이 일어나는지 알아보자.


Initialization

초기화는 클래스, 구조체, 열거형의 인스턴스를 사용하기 위해 준비하는 과정이다. 초기화를 할 때 아래 과정이 포함된다.

  • 인스턴스의 저장 프로퍼티에 초깃값을 설정한다.
  • 새로운 인스턴스가 사용되기 전에 설정되어야 하는 부분을 설정하거나 다른 초기화를 진행한다.

초기화는 initializer를 통해 진행하는데, 특정 타입의 인스턴스를 생성하기 위해 호출되는 특별한 메서드다. Swift의 이니셜라이저는 값을 리턴하지 않는다. 이니셜라이저의 주 역할은 처음 사용되기 전에 인스턴스가 잘 초기화되는 것을 보장하는 것이다.

클래스의 인스턴스는 이니셜라이저에 더불어 deinitializer를 구현할 수 있는데, 디이니셜라이저는 해당 클래스 인스턴스가 해제되기 전에 할 작업들을 수행한다.

저장 프로퍼티의 초깃값 설정

클래스와 구조체는 꼭 인스턴스가 생성된 시점에 저장 프로퍼티에 적절한 초깃값이 설정되어 있어야 한다. 저장 프로퍼티들은 값이 결정되지 않은 상태로 남아있을 수 없다.

저장 프로퍼티의 초깃값을 설정하는 방법은 아래와 같다.

  1. 이니셜라이저
  2. 프로퍼티 정의에서 기본값을 설정

만약 이니셜라이저를 통해서 초깃값을 설정하거나 프로퍼티의 정의에서 기본값을 정의하면 프로퍼티 감시자는 호출되지 않는다.

Initializer

이니셜라이저는 특정 타입의 인스턴스를 생성하기 위해 호출된다. 가장 간단한 형태는 파라미터 없이 init 키워드로 생성하는 아래의 형태다.

init() {
    // perform some initialization here
}

Default Property Value

이니셜라이저에서 저장 프로퍼티의 초깃값(initial value)을 설정할 수 있지만, 프로퍼티를 정의할 때 프로퍼티의 기본 값(default value)을 설정할 수 있다.

만약 프로퍼티가 항상 같은 초깃값을 가진다면, 이니셜라이저를 통해 초깃값을 설정하기보다는 기본값을 지정해주는 것이 좋다. 이니셜라이저를 더 간단하게 만들 수도 있고, 기본 이니셜라이저와 이니셜라이저 상속에서 얻는 이점도 있는데, 이는 뒤에서 더 볼 것이다.

struct Fahrenheit {
    var temperature = 32.0
}

var f = Fahrenheit()

Customizing Initialization

이니셜라이저도 매개변수를 가질 수 있다.

Initialization Parameter

이니셜라이저 정의에서 initialization parameter를 제공해서 이니셜라이저를 커스터마이징 할 수 있다.

struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0

Parameter Names and Argument Labels

함수와 메서드 파라미터와 같이, 이니셜라이저 파라미터도 전달 인자 레이블과 파라미터 이름을 가질 수 있다. 이니셜라이저는 호출할 때 따로 이름이 없기 때문에, 이니셜라이저가 호출될 때 이니셜라이저의 파라미터의 파라미터의 이름과 타입이 매우 중요하다. 이때문에 Swift는 내가 전달인자 레이블을 제공하지 않은 모든 파라미터에 대해 자동으로 생성된 전달인자 레이블을 제공한다.

struct Color {
    let red, green, blue: Double
    
    init(red: Double, green: Double, blue: Double) {
        self.red   = red
        self.green = green
        self.blue  = blue
    }
    
    init(white: Double) {
        red   = white
        green = white
        blue  = white
    }
}

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)

// 위에서 정의된 이니셜라이저중에서 전달인자 레이블 없이 호출할 수 있는 이니셜라이저는 없다. 컴파일 에러가 발생한다.
let veryGreen = Color(0.0, 1.0, 0.0)

Initializer Parameters Without Argument Labels

만약 이니셜라이저 파라미터에 전달인자 레이블을 사용하고 싶지 않으면 와일드카드 식별자(_)를 사용해서 전달인자 레이블을 사용하지 않게 할 수 있다.

struct Celsius {
    var temperatureInCelsius: Double
    
    init(_ celsius: Double) {
        temperatureInCelsius = celsius
    }
}

let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius is 37.0

Optional Property Types

만약 저장 프로퍼틱가 옵셔널이라면, 프로퍼티를 optional 타입으로 지정해주면 된다. 옵셔널 타입 프로퍼티는 자동으로 nil로 초기화된다.

class SurveyQuestion {
    var text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// Prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."

Assigning Constant Properties During Initialization

초기화 과정에서 상수 프로퍼티에 값을 할당할 수 있다. 상수 프로퍼티에 값이 할당되면 이후에 수정할 수 없다.

클래스 인스턴스의 경우 상수 프로피티는 프로퍼티가 정의된 클래스에서만 초기화할 수 있고, 해당 클래스를 상속받은 자식클래스의 이니셜라이저에서는 부모 클래스의 상수 프로퍼티 값을 초기화할 수 없다.

class SurveyQuestion {
    let text: String
    var response: String?
    init(text: String) {
        self.text = text
    }
    func ask() {
        print(text)
    }
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"

Default Initializers

Swift는 내부에서 이니셜라이저를 제공하지 않거나 모든 프로퍼티에 대해 기본 값을 지정하지 않은 클래스나 구조체에 default initializer를 제공한다. 기본 이니셜라이저는 모든 프로퍼티를 초기화해서 인스턴스를 생성한다.

class ShoppingListItem {
    var name: String?
    var quantity = 1
    var purchased = false
}

// 모든 프로퍼티의 기본값이 지정되어 있고, 부모 클래스가 없기 때문에 자동으로 기본 이니셜라이저를 생성한다.
var item = ShoppingListItem()

Memberwise Initializers for Structure Types

구조체는 커스텀 이니셜라이저를 정의하지 않은 경우에 자동으로 memberwise initializer를 사용할 수 있다. 기본 이니셜라이저와는 다르게, 기본 값이 없는 저장 프로퍼티가 없는 경우에도 memberwise initializer를 사용할 수 있다.

멤버와이즈 이니셜라이저는 구조체 인스턴스의 멤버 프로퍼티를 초기화하는 간편한 방법이다. 인스턴스의 프로퍼티들에 할당될 초깃값들이 멤버와이즈 이니셜라이저에 이름으로 전달될 수 있다.

struct Size {
    var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)

let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0"

let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// Prints "0.0 0.0"

Initializer Delegation for Value Types

이니셜라이저는 인스턴스 초기화 과정중에 다른 이니셜라이저를 호출할 수 있다. 이를 initializer delegation이라 하고, 이는 여러 이니셜라이저에서 중복되는 코드를 줄일 수 있다.

초기화 위임이 일어나는 규칙이나 어떤 형태의 위임이 허용되는지는 값타입과 클래스 타입에서 다르다. 구조체, 열거형 이런 값 타입은 상속이 안되기 때문에 그들 스스로가 제공하는 다른 이니셜라이저에만 위임할 수 있다. 반면 클래스는 이에 비해 복잡한데, 다른 클래스를 상속할 수 있기 때문이다. 이는 클래스는 초기화 도중 상속받은 모든 저장 프로퍼티에 적절한 값이 할당된 것을 보장해야 하는 추가적인 작업이 필요한 것이다. 이런 클래스의 책임에 대해서는 밑에서 더 볼 것이다.

값 타입에서는 커스텀 이니셜라이저가 다른 이니셜라이저를 호출하려고 하면 self.init을 사용한다. 이 self.init은 이니셜라이저 내에서만 호출할 수 있다.

값 타입에 커스텀 이니셜라이저를 정의하면 기본 이니셜라이저나 구조체의 경우 memberwise 이니셜라이저를 사용할 수 없다. 이 제약은 복잡한 이니셜라이저에서 꼭 필요한 추가적인 셋업이 자동으로 생성된 이니셜라이저를 사용함으로써 실수로 피하게 되는 상황을 예방한다.

만약 기본 이니셜라이저와 멤버와이즈 이니셜라이저를 모두 사용해서 값 타입을 초기화하고 싶다면, 커스텀 이니셜라이저를 extension을 사용해서 구현하면 된다.

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}

struct Rect {
    var origin = Point()
    var size = Size()
    
    // 기능적으로 커스텀 이니셜라이저가 없었을 경우에 받았을 기본 이니셜라이저와 같은 기능을 한다. 
    init() {}
    
    // 만약 커스텀 이니셜라이저가 없었을 경우에 받았을 멤버와이즈 이니셜라이저와 같은 기능을 한다. 
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

// 첫 번째 이니셜라이저로 호출
let basicRect = Rect()
// 두 번째 이니셜라이저로 호출
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
                      size: Size(width: 5.0, height: 5.0))
// 세 번째 이니셜라이저로 호출
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))

Failable Initializers

클래스, 구조체, 열거형에서 초기화가 실패하는 경우를 정의하는게 유용할 때가 있다. 이런 실패는 유효하지 않은 초기화 파라미터 값이나, 요구되는 외부 리소스가 존재하지 않다거나, 다른 초기화를 실패하게 하는 여러 조건 때문일 수 있다.

초기화가 실패할 수 있다는 걸 다루기 위해 하나 이상의 실패 가능한 이니셜라이저를 정의한다. init? 키워드로 실패 가능 이니셜라이저를 정의한다.

같은 파라미터 타입과 이름으로 실패 가능한 이니셜라이저와 실패하지 않는 이니셜라이저를 정의할 수 없다.

실패가능한 이니셜라이저는 초기화하는 타입의 옵셔널 값을 생성한다. 초기화가 실패할 수 있는 부분에서 return nil을 해줘야 한다.

엄밀히 말하자면 이니셜라이저는 값을 리턴하지 않고, 그들의 역할은 초기화가 끝났을 때 self가 완전히, 올바르게 초기화되게 하는 것이다. return nil을 써서 초기화가 실패한다고 해도, 초기화가 성공하는 경우에 return은 쓰지 않는다.

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

let someCreature = Animal(species: "Giraffe")
// someCreature is of type Animal?, not Animal

if let giraffe = someCreature {
    print("An animal was initialized with a species of \(giraffe.species)")
}
// Prints "An animal was initialized with a species of Giraffe"

let anonymousCreature = Animal(species: "")
// anonymousCreature is of type Animal?, not Animal

if anonymousCreature == nil {
    print("The anonymous creature couldn't be initialized")
}
// Prints "The anonymous creature couldn't be initialized"

Failable Initializers for Enumerations

실패 가능한 이니셜라이저를 사용해서 하나 이상의 파라미터를 바탕으로 적절한 열거형 케이스를 선택하게 할 수 있다.

enum TemperatureUnit {
    case kelvin, celsius, fahrenheit
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = .kelvin
        case "C":
            self = .celsius
        case "F":
            self = .fahrenheit
        default:
            return nil
        }
    }
}

let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
    print("This isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."

Failable Initializers for Enumerations with Raw Values

Raw value를 가진 열거형은 자동으로 실패 가능한 이니셜라이저를 사용할 수 있다.

init?(rawValue:)는 만약 받은 rawValue와 잂치하는 원시 값을 갖는 열거형의 케이스가 있는 경우 해당 열거형 케이스를 선택하지만, 일치하는 케이스가 없는 경우 실패하게 된다.

enum TemperatureUnit: Character {
    case kelvin = "K", celsius = "C", fahrenheit = "F"
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
    print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."

let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
    print("This isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."

Propagation of Initialization Failure

클래스, 구조체, 열거형의 실패 가능한 이니셜라이저는 같은 클래스, 구조체, 열거형의 다른 실패 가능한 이니셜라이저에서 사용도리 수 있다. 비슷하게 하위 클래스의 실패 가능한 이니셜라이저는 부모 클래스의 실패 가능한 이니셜라이저에서 실패를 위임받을 수 있다.

만약 초기화 위임에서 한 초기화가 실패한다면 전체 초기화 과정은 즉시 실패하게 되고, 그 이후의 초기화 코드는 실행되지 않는다.

class Product {
    let name: String
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class CartItem: Product {
    let quantity: Int
    init?(name: String, quantity: Int) {
        if quantity < 1 { return nil }
        self.quantity = quantity
        super.init(name: name)
    }
}

if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
    print("Unable to initialize one unnamed product")
}
// Prints "Unable to initialize one unnamed product"

Overriding a Failable Initializer

부모 클래스의 실패 가능한 이니셜라이저를 자식 클래스에서 오버라이딩할 수 있다. 대신, 부모 클래스의 실패 가능한 이니셜라이저를 자식 클래스의 실패하지 않는 이니셜라이저로 오버라이딩 할 수 있다.

class Document {
    var name: String?
    // this initializer creates a document with a nil name value
    init() {}
    // this initializer creates a document with a nonempty name value
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[Untitled]"
    }
    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[Untitled]"
        } else {
            self.name = name
        }
    }
}

class UntitledDocument: Document {
    override init() {
        // forced unwrapping을 사용해서 실패 가능한 이니셜라이저를 사용할 수 있다.
        super.init(name: "[Untitled]")!
    }
}

init! Failable Initializer

일반적으로 실패 가능한 이니셜라이저를 정의할 때 init?을 사용한다. 대신, unwrapped 된 옵셔널 인스턴스를 생성하는 실패 가능한 이니셜라이저를 정의할 수 있다. 이는 init! 키워드를 사용해서 정의한다.

Required Initializers

모든 자식 클래스가 해당 이니셜라이저를 구현해야 하는 이니셜라이저가 있다면 required를 붙인다.

class SomeClass {
    required init() {
        // initializer implementation goes here
    }
}

class SomeSubclass: SomeClass {
    // required를 붙이고, override는 붙이지 않는다.
    required init() {
        // subclass implementation of the required initializer goes here
    }
}

Setting a Default Property Value with a Closure or Function

만약 저장 프로퍼티의 기본 값이 어떤 커스터마이징을 하거나 설정이 필요하다면, 해당 프로퍼티의 커스터마이징된 기본 값을 제공하기 위해 클로저나 전역 함수를 사용할 수 있다. 인스턴스가 생성될 때 클로저나 함수가 호출되고, 리턴된 값이 프로퍼티의 기본값으로 할당된다.

class SomeClass {
    let someProperty: SomeType = {
        // create a default value for someProperty inside this closure
        // someValue must be of the same type as SomeType
        return someValue
    }() // 빈 괄호를 클로저 뒤에 붙이면 Swift가 클로저를 즉시 실행하게 끔한다. 괄호가 없다면 클로저 자체를 프로퍼티에 할당하는 것이다.
}

클로저를 사용해서 초기화할 때, 클로저가 실행되는 순간 인스턴스의 나머지 부분이 아직 초기화가 안됐을 수 있다는 점에 주의하자. 이는 클로저 내에서 기본 값이 있다해도 다른 프로퍼티 값에 접근할 수 없다는 걸 의미한다. self 프로퍼티를 사용하거나 인스턴스 메서드를 호출할 수도 없다.

struct Chessboard {
    let boardColors: [Bool] = {
        var temporaryBoard: [Bool] = []
        var isBlack = false
        for i in 1...8 {
            for j in 1...8 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAt(row: Int, column: Int) -> Bool {
        return boardColors[(row * 8) + column]
    }
}

let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true"
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false"

Deinitialization

디이니셜라이저는 클래스 인스턴스가 해제되기 직전에 호출된다. deinit 키워드를 작성해서 디이니셜라이저를 작성한다. 디이니셜라이저는 클래스 타입에서만 사용할 수 있다.

How Deinitialization Works

Swift는 인스턴스가 더 이상 필요가 없을 때 리소스를 확보하기 위해 인스턴스를 해제한다. Swift는 ARC(Automatic reference counting)를 사용해서 인스턴스들의 메모리를 관리한다. 일반적으로 인스턴스가 해제될 때 따로 무언가를 작업할 필요는 없다. 하지만 따로 무언가를 할 필요가 있을 때가 있다. 예를 들어 파일을 열고 데이터를 쓰는 커스텀 클래스를 작성했다면, 클래스 인스턴스가 해제되기 전에 파일을 닫아야 할 것이다.

클래스 하나당 하나의 디이니셜라이저만 가질 수 있다.

deinit {
    // perform the deinitialization
}

디이니셜라이저는 해제가 일어나기 전에 자동으로 호출된다. 디이니셜라이저를 직접 호출하는 건 불가능하다. 부모 클래스의 디이니셜라이저는 자식 클래스로 상속되고, 자식 클래스의 디이니셜라이저 구현 끝에서 자동으로 호출된다. 자식 클래스가 자신만의 디이니셜라이저를 제공하지 않는다 해도 부모 클래스의 디이니셜라이저는 항상 호출된다.

디이니셜라이저가 호출되고 종료되기까지 인스턴스가 해제되지 않기 때문에, 디이니셜라이저는 인스턴스의 프로퍼티에 접근할 수 있다.