[Swift] 초기화(Initialization)란?
Swift

[Swift] 초기화(Initialization)란?

이 글은 스위프트 공식 문서를 한국어로 번역한 Swift Language Guide의 초기화 챕터를 간단하게 정리한 글 입니다.

공식 문서를 번역해주신 분들에게 감사의 말씀을 전합니다.


초기화란?

 

초기화는 클래스, 구조체, 열거형 타입의 인스턴스를 사용하기 위해 준비하는 단계로서, 타입마다 초기자(initializer)를 정의하여 초기화할 수 있습니다. 초기자는 init 이라는 키워드를 사용하여 정의할 수 있고 이때 여러 파라미터를 갖도록 만들 수 있습니다.

 

인스턴스를 사용하기 위해서는 내부의 모든 저장 프로퍼티가 초기값을 갖고 있어야 합니다. 따라서 저장 프로퍼티의 선언과 동시에 값을 할당하여 기본값을 설정하거나 그렇게 하지 않고 초기자를 통해 특정 값을 갖도록 할 수도 있습니다.

1. 옵셔널 타입의 프로퍼티는 값이 설정되지 않았을 경우 nil을 기본값으로 갖습니다. 
2. let 키워드로 선언된 상수 프로퍼티는 한번 초기화 되면 값을 변경할 수 없습니다.

 

일반적인 메소드와 동일하게 파라미터를 갖는 초기자는 인자 레이블파라미터 이름을 갖는데 이때 인자 레이블을 설정하지 않으면 Swift에서 자동으로 할당해 사용할 수 있게 해줍니다. "_" 키워드를 사용해 인자 레이블을 생략할 수도 있습니다.

인자 레이블: 메소드 또는 초기자에게 값을 넘겨줄때 사용하는 이름
파라미터 이름: 메소드 또는 초기자 내부에서 매개변수를 사용하기 위한 이름

 

모든 저장 프로퍼티의 기본값이 설정돼있다면 초기자를 선언하지 않았더라도 Swift에서 파라미터가 없는 기본 초기자 init()를 제공해줍니다. 특별하게 구조체 타입에서는 기본값이 설정되지 않은 프로퍼티를 초기화할 수 있도록 멤버쪽(Memberwiser) 초기자를 제공해줍니다.

구조체의 모든 프로퍼티가 기본값을 가지더라도 멤버쪽 초기자를 사용할 수 있습니다.

 

초기자에서 다른 초기자를 호출하는 방법을 초기자 위임(Initializer Delegation)이라고 합니다. 구조체 타입은 상속 개념이 없어서 초기자를 자신의 다른 초기자에서만 사용할 수 있는데, 클래스 타입은 상속이 가능하여 하위 클래스의 초기자에서 상위 클래스의 초기자를 사용할 수 있습니다. 

 

아래 코드의 init(center: Point, size: Size) 초기자 내부에서 init(origin: Point, size: Size) 초기자가 호출됐음을 볼 수 있습니다. 초기자 위임을 위해 별도로 초기자를 선언하긴 했지만 사실 init(origin: Point, size: Size) 초기자는 멤버쪽 초기자와 동일한 기능을 수행합니다. 따라서 자동으로 생성되는 기본 초기자와 멤버쪽 초기자를 직접 선언하지 않고도 extension을 이용해 초기자 위임을 구현할 수 있습니다.

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)
    }
}
extension Rect {
    
    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)
    }
    
}

 

클래스 타입의 경우 상속받은 프로퍼티를 초기화 해야하는 의무가 있다는 점에서 값 타입(구조체, 열거형)과는 조금 다릅니다. 클래스 타입은 초기화 과정에서 자신의 프로퍼티 뿐만 아니라 상속받은 프로퍼티가 초기값 가질 수 있도록 보장해야 합니다.

 

클래스 타입에는 두 가지 종류의 초기자가 있습니다.

  • 지정 초기자: 클래스의 프로퍼티가 초기값을 갖게 만들어 주는 주 초기자이며 클래스 타입은 반드시 하나 이상의 지정 초기자를 가져야 합니다. 
  • 편의 초기자: 프로퍼티가 특정한 값으로 초기값이 설정되게 만들어주는 보조 초기자이며 내부에서 반드시 지정 초기자를 호출해야 합니다. 초기자 키워드인 init의 앞에 convenience 키워드를 붙여 선언할 수 있습니다.

 

클래스 타입의 초기화에서 초기자 위임을 위해 몇 가지 규칙이 존재합니다.

 

1. 지정 초기자는 반드시 상위 클래스의 지정 초기자를 호출해야 합니다.

클래스의 지정 초기자는 프로퍼티가 초기값을 갖도록 만들기 때문에 상위 클래스의 지정 초기자를 호출하여 상속받은 프로퍼티가 초기값을 갖도록 초기화를 위임하기 위함입니다.

2. 편의 초기자는 반드시 같은 클래스(레벨)의 다른 초기자를 호출하고 최종적으로 지정 초기자를 호출해야 합니다.

편의 초기자는 프로퍼티가 특정한 초기값으로 설정되게 만들기 위해 같은 클래스의 다른 초기자에게 위임하면서 결국은 지정 초기자가 모든 프로퍼티에 초기값을 할당할 수 있게 만들어야 합니다.

초기자 위임 규칙

 

Swift에서 클래스의 초기화는 총 2단계를 거칩니다.

  1. 상속받은 프로퍼티를 포함한 모든 저장 프로퍼티에 초기값을 할당합니다.
  2. 1번으로 초기화가 완료된 인스턴스를 사용하기 전에 프로퍼티의 값을 사용자화 합니다.

 

그래서 Swift 컴파일러는 클래스를 초기화 할때 항상 위 2단계를 거쳐 초기화가 이뤄지는 것을 보장하기 위해서 4가지 사항을 확인합니다.

  • 하위 클래스에서 상위 클래스로 초기자를 위임하기 전에 하위 클래스의 저장 프로퍼티를 초기화 해야합니다.
메모리 상에서 모든 프로퍼티에 초기값이 설정돼야 초기화가 완료된 것으로 간주하기 때문에 먼저 하위 클래스의 프로퍼티를 초기화하고 상위 클래스의 프로퍼티를 초기화하도록 초기자를 위임합니다.
  • 하위 클래스의 지정 초기자는 상속받은 프로퍼티에 값을 할당하기 전에 상위 클래스의 지정 초기자를 호출하여 초기자를 위임해야합니다.
그렇지 않으면 하위 클래스에서 설정한 값이 상위 클래스의 초기자에 의해 변경될 수 있기 때문입니다.
  • 편의 초기자는 프로퍼티에 값을 할당하기 전에 같은 클래스의 다른 초기자를 호출하여 초기자를 위임해야 합니다.
그렇지 않으면 프로퍼티에 할당한 값이 다른 초기자에 의해 변경될 수 있기 때문입니다.
  • 초기자 내부에서 1단계 초기화가 끝나기 전에는 인스턴스의 메소드 또는 프로퍼티를 읽을 수 없고 self 값을 참조할 수 없습니다.
모든 프로퍼티가 초기값을 가져야 초기화가 완료된 것으로 간주하기 때문에 그 전에는 인스턴스를 사용할 수 없습니다.

클래스 초기화 1단계
클래스 초기화 2단계

 

Swift에서는 기본적으로 무분별하고 잘못된 초기화를 막기 위해서 상위 클래스의 초기자가 하위 클래스로 상속되지 않습니다. 하지만 특별한 상황에 한해서 하위 클래스가 상위 클래스의 초기자를 상속받습니다. 만약 하위 클래스에서 추가된 모든 프로퍼티에 초기값이 할당돼있다면 자동으로 초기자를 상속받는 상황은 아래 2가지 입니다.

 

1. 하위 클래스에서 별도의 지정 초기자를 선언하지 않았을경우 상위 클래스의 모든 지정 초기자를 상속받습니다.

상위 클래스의 프로퍼티만 초기화하면 되기 때문에 상속받은 상위 클래스의 초기자를 사용해서 하위 클래스의 인스턴스를 만들 수 있습니다.

2. (1번에 의해 또는) 하위 클래스에서 상위 클래스의 지정 초기자를 모두 구현한 경우 상위 클래스의 편의 초기자를 상속받습니다.

하위 클래스에 상위 클래스의 지정 초기자가 모두 구현돼있기 때문에 그 지정 초기자들을 사용하는 상위 클래스의 편의 초기자 역시 사용할 수 있습니다. 이 조건을 만족시키기 위해 상위 클래스의 지정 초기자를 하위 클래스의 편의 클래스로 구현할 수도 있습니다.

 

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() {
        self.init(name: "[Unnamed]")
    }
}

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}

RecipeIngredient 클래스는 Food의 하위 클래스이며 Food의 지정 초기자인 init(name: String)을 구현했으므로 2번 조건에 의해 자동으로 상위 클래스인 Food의 편의 초기자를 상속받습니다. 따라서 RecipeIngredient 클래스의 인스턴스를 생성하는 방법은 아래 3가지 입니다.

let instance1 = RecipeIngredient()
let instance2 = RecipeIngredient(name: "Bacon")
let instance3 = RecipeIngredient(name: "Eggs", quantity: 6)

 

ShoppingListItem 클래스는 RecipeIngredient 클래스의 하위 클래스입니다. 이런 상속 관계에서 하위 클래스의 프로퍼티가 모두 초기값을 갖고 있으며 별도의 초기자를 정의하지 않았기 때문에 자동으로 상위 클래스의 지정 초기자를 모두 상속받습니다. 따라서 위와 마찬가지로 ShoppingListItem 클래스의 인스턴스를 생성하는 방법은 동일한 3가지 입니다.

 

아래는 이 3개의 클래스에 대한 초기자 체인을 나타낸 그림입니다.

초기자 체인


중요하고 헷갈린다고 생각되는 것들만 먼저 정리했고 나머지 부분은 추후 업데이트 하도록 하겠습니다.

'Swift' 카테고리의 다른 글

[Swift] COW(Copy-on-Write)에 대하여  (0) 2022.07.28
[Swift] JSON 객체를 구조체로 변환하기  (0) 2022.05.16
[Swift] Singleton과 Thread-Safe  (0) 2022.05.12