[Swift] class 인스턴스 초기화

이정훈·2023년 2월 25일
0

Swift 파헤치기

목록 보기
3/10
post-thumbnail

Swift에서 class의 이니셜라이저를 공부하다보면 다른 언어들과 다르게 Swift 나름(?)의 규칙을 가지고 있어 이번 포스트에서는 class의 이니셜라이저에 대하여 필자가 이해한 방식대로 정리를 해보려고 한다.

이니셜라이저의 종류


먼저 Swift class의 이니셜라이저 종류는 두 가지로

  • 지정 이니셜라이저(Designated initializer)
  • 편의 이니셜라이저(Convenience initializer)

이렇게 두 가지가 존재한다.

두 이니셜라이저는 개발자가 목적에 맞게 사용하면 되며, 각 이니셜라이저가 수행해야하는 목적은 다음과 같다.

지정 이니셜라이저


지정 이니셜라이저를 정의할때는 init 키워드를 사용하여 정의하며, 지금까지 아래 형태와 같이 써왔던 이니셜라이저는 모두 지정 이니셜라이저라고 할 수 있겠다.

//지정 이니셜라이저
init(매개변수) {
	...
}

그렇다면, 지정 이니셜라이저가 해야되는 역할은 무엇일까?

지정 이니셜라이저는 모든 클래스의 기본이 되는 이니셜라이저로 클래스의 모든 프로퍼티를 초기화하는 역할을 담당한다.

지정 이니셜라이저는 기본이 되는 이니셜라이저 이기 때문에 모든 클래스에서는 최소 한개 이상의 지정 이니셜라이저를 가져야 한다.

다만, 자식 클래스가 부모 클래스를 상속 받았을때, 부모 클래스의 지정 이니셜라이저로 자식 클래스의 프로퍼티를 모두 초기화할 수 있다면 자식 클래스에서 지정 이니셜라이저를 정의하지 않아도 된다. 이니셜라이저 자동 상속

편의 이니셜라이저


편의 이니셜라이저라고 정의하는 방법이 크게 다르지 않다.

init 앞에 convenience 키워드만 추가함으로서 편의 이니셜라이저를 정의할 수 있다.

//편의 이니셜라이저
convenience init {
	...
}

편의 이니셜라이저는 이름에서 알 수 있듯이 지정 이니셜라이저를 보조하여 초기화를 쉽게 도와주는 역할을 한다.

편의 이니셜라이저는 내부에서 지정 이니셜라이저를 호출하며 예를 들어 매개변수로 들어온 값을 변형하여 지정 이니셜라이저에 전달하는 등으로 초기화를 도울 수 있다.

지정 이니셜라이저와 편의 이니셜라이저의 관계


위에서 알아본 두 이니셜라이저 사이에는 특별한 규칙이 존재한다.

두 이니셜라이저의 규칙에 대해 알아보기 위해 한 클래스가 다른 클래스에 상속하여 부모 클래스와 자식 클래스의 관계를 가진다고 가정 하겠다.

  1. 편의 이니셜라이저는 자신이 속한 클래스의 다른 이니셜라이저를 호출해야 한다.

  2. 결과적으로 마지막에 편의 이니셜라이저는 자신이 속한 클래스의 지정 이니셜라이저를 호출해야 한다.

  3. 자식 클래스의 지정 이니셜라이저는 반드시 부모 클래스의 지정 이니셜라이저를 호출해야 한다.

이렇게 이니셜라이저가 다른 이니셜라이저를 호출하는 관계를 초기화를 위임한다라고 할 수 있으며, 최상단의 부모 클래스는 상속 받은 클래스가 없으므로 3번 조건은 무시할 수 있다.

다음은 위의 조건을 만족하는 부모 클래스와 자식 클래스를 구현한 코드이다.

class Country {
    var name: String
    var population: Int?
    
    //지정 이니셜라이저
    init(name: String, population: Int?) {
        self.name = name
        self.population = population
    }
    
    //편의 이니셜라이저
    convenience init(name: String) {
        self.init(name: name, population: nil)
    }
}

class Korea: Country {
    var greet: String
    
    //지정 이니셜라이저
    override init(name: String, population: Int?) {
        self.greet = "안녕하세요!"
        super.init(name: name, population: population)
    }
    
    //편의 이니셜라이저
    convenience init(name: String) {
        self.init(name: name, population: 50000000)
    }
}

두 클래스의 편의 이니셜라이저는 내부에서 자기 클래스에 정의 되어 있는 지정 이니셜라이저를 호출하므로 조건 1번과 2번을 동시에 만족한다.

자식 클래스의 지정 이니셜라이저는 부모 클래스의 지정 이니셜라이저를 호출하고 있으므로 조건 3번 또한 만족한다.

2단계 초기화


위의 코드에서 보면 각 이니셜라이저는 그냥 프로퍼티에 값을 대입하여 프로퍼티를 초기화하는것 처럼 보이지만 Swift의 클래스 초기화는 2단계를 거쳐 초기화 된다.

또한 Swift는 초기화를 진행하면서 다음과 같은 조건에 대하여 Safety check를 하면서 초기화를 진행한다.

Safety check(안전 확인)

  1. 자식 클래스의 지정 이니셜라이저는 부모 클래스의 이니셜라이저로 초기화를 위임하기 전 자신 클래스만이 가지는 프로퍼티가 모두 초기화 되었는지 확인 후 초기화를 위임한다.

  2. 자식 클래스의 지정 이니셜라이저는 상속 받은 프로퍼티를 초기화하기 전 부모 클래스의 지정 이니셜라이저로 초기화를 위임해야 한다.

  3. 편의 이니셜라이저는 모든 프로퍼티를 초기화하기 전에 먼저 지정 이니셜라이저에게 초기화를 위임해야 한다.

  4. 1단계 초기화를 마치기 전까지 이니셜라이저는 인스턴스 메서드, 인스턴스 프로퍼티, 상속 받은 프로퍼티에 대하여 self 키워드를 사용할 수 없다.(자기 자신 클래스가 원래 가지고 있던 프로퍼티는 self로 접근 가능)

2, 3번 조건은 의도하지 않은 값으로 프로퍼티가 초기화 되는것을 방지하기 위함이다.

1단계 초기화

  • 클래스가 지정 혹은 편의 이니셜라이저를 호출한다.

  • 인스턴스 생성을 위한 메모리가 할당되며, 해당 메모리는 아직 초기화 되지 않았다.

  • 지정 이니셜라이저는 자신 클래스가 가지는 모든 저장 프로퍼티를 초기화한다.

  • 지정 이니셜라이저는 상속 받은 프로퍼티도 초기화 할 수 있도록 부모 클래스의 이니셜라이저에게 초기화를 위임한다.

  • 최상위 부모 클래스에 도달할 때까지 위의 과정을 반복한다.

2단계 초기화

2단계 초기화는 최상위 부모 클래스부터 최하위 클래스까지 지정 이니셜라이저를 통해 chain처럼 내려 오면서 인스턴스를 사용자 정의 한다. 상속 받은 프로퍼티 사용자 정의

위의 안전확인 조건에 의해 1단계를 마친 경우 self 키워드, 인스턴스 프로퍼티, 인스턴스 메서드 등을 사용할 수 있으며 이것을 통해 인스턴스를 사용자 정의 할 수 있다.

편의 이니셜라이저도 마찬가지로 2단계부터 self를 통해 인스턴스를 사용자 정의를 할 수 있다.

위의 예시 코드를 일부 가져와서 보면

class Korea: Country {
    var greet: String
    
    //지정 이니셜라이저
    override init(name: String, population: Int?) {
        self.greet = "안녕하세요!"
        super.init(name: name, population: population)
    }
    
    //편의 이니셜라이저
    convenience init(name: String) {
        self.init(name: name, population: 50000000)
    }
}

Korea 클래스는 Country 클래스를 상속받은 상태이다.

지정 이니셜라이저는 super 클래스의 이니셜라이저를 호출하기 전에 self 키워드를 통해 자기 자신이 가지는 프로퍼티를 초기화한다.

자기 클래스가 가지는 프로퍼티가 모두 초기화된 것을 확인하여 안전 확인이 만족 되었을때 부모 클래스의 이니셜라이저를 호출하여 1단계 초기화를 이어 나간다.

편의 이니셜라이저의 경우 지정 이니셜라이저를 바로 호출하여 안전 확인을 만족한 것을 확인할 수 있다.

이니셜라이저 상속 및 재정의


자식 클래스에서 부모 클래스와 동일한 지정 이니셜라이저를 구현하고자 할때, 다시 말해 재정의 하고자 할때 override 키워드를 사용한다.

하지만 편의 이니셜라이저를 구현할때는 override 키워드를 사용할 수 없다. 편의 이니셜라이저는 재정의가 불가능하기 때문에 override 키워드 없이이 구현한다.

위의 예시를 다시 한번 가져오자면

class Korea: Country {
    var greet: String
    
    //지정 이니셜라이저
    override init(name: String, population: Int?) {
        self.greet = "안녕하세요!"
        super.init(name: name, population: population)
    }
    
    //편의 이니셜라이저
    convenience init(name: String) {
        self.init(name: name, population: 50000000)
    }
}

지정 이니셜라이저override 키워드를 사용한 반면, 편의 이니셜라이저override 키워드를 사용하지 않고 구현한 것을 확인할 수 있다.

이니셜라이저 자동 상속


자식 클래스가 특정 조건을 만족 한다면 부모 클래스로 부터 이니셜라이저를 자동으로 상속 받을 수 있다.

  • 자식 클래스의 모든 프로퍼티가 기본 값을 가질때, 자식 클래스에서 이니셜라이저를 재정의 하지 않는다면 부모 클래스로 부터 지정 이니셜라이저가 자동 상속된다.

  • 재정의 혹은 자동 상속으로 부모 클래스와 동일한 지정 이니셜라이저를 사용할 수 있다면, 부모 클래스의 편의 이니셜라이저 또한 자동 상속 된다.

그렇다면 위의 예시 코드를 다시 가져와서 다음과 같이 변경할 수 있다.

class Korea: Country {
    var greet: String = "안녕하세요!"
}

Korea 클래스는 Country 클래스를 상속 받고 Korea 클래스가 가지는 프로퍼티는 기본 값을 가지므로 부모 클래스로부터 지정 이니셜라이저를 상속 받고 이제 부모 클래스의 이니셜라이저를 사용할 수 있으므로 편의 이니셜라이저 또한 자동으로 상속 받는다.

위의 조건 중 단 하나라도 만족하지 않으면 재정의를 통해 반드시 지정 이니셜라이저를 구현해야 한다. 기본적으로 이니셜라이저는 상속되지 않기 때문

요구 이니셜라이저


이니셜라이저 앞에 required 키워드를 사용하면 해당 이니셜라이저는 자식 클래스에서 반드시 재정의 해야 한다.

class Country {
    var name: String
    var population: Int?
    
    //요구 이니셜라이저
    required init() {
        self.name = "Unkown"
        self.population = nil
    }
}

위의 Country 클래스는 매개변수가 없는 이니셜라이저를 required 키워드로 정의 하였다.

따라서 자식 클래스에서 해당 이니셜라이저를 반드시 구현해야하며 다음과 같이 구현할 수 있다.

class Korea: Country {
    var greet: String
    
    //요구 이니셜라이저 구현
    required init() {
        self.greet = ""
        super.init()
    }
}

요구 이니셜라이저를 구현하기 위해서 override 키워드 대신 required 키워드를 사용한다. required 키워드가 override 키워드를 내포한다

부모 클래스로부터 재정의한 이니셜라이저를 해당 클래스부터 요구 이니셜라이저로 지정하고 싶다면 required override 키워드를 사용하여 정의한다.

편의 이니셜라이저 또한 요구 이니셜라이저로 정의하고 싶다면 required convenience 키워드를 사용하여 정의한다.

class Country {
    var name: String
    var population: Int?
    
    //지정 이니셜라이저
    init(name: String, population: Int?) {
        self.name = name
        self.population = population
    }
    
    //편의 이니셜라이저
    convenience init(name: String) {
        self.init(name: name, population: nil)
    }
    
    init() {
        self.name = "Unkown"
        self.population = nil
    }
}

class Korea: Country {
    var greet: String
    
    //지정 이니셜라이저
    override init(name: String, population: Int?) {
        self.greet = "안녕하세요!"    //super 클래스의 이니셜라이저를 호출하기 전에 자신 클래스가 가지는 프로퍼티를 초기화
        super.init(name: name, population: population)    //지정 이니셜라이저는 super 클래스의 지정 이니셜라이저를 호출
    }
    
    //요구 편의 이니셜라이저
    required convenience init(name: String) {
        self.init(name: name, population: 50000000)    //편의 이니셜라이저는 해당 클래스의 지정 이니셜라이저를 반드시 호출
    }
    
    //재정의한 이니셜라이저를 요구 이니셜라이저로 구현
    required override init() {
        self.greet = ""
        super.init()
    }
}

class Seoul: Korea {
    var landmark: String
    
    //요구 이니셜라이저 구현
    required init() {
        self.landmark = "경복궁"
        super.init(name: "서울", population: 10000000)
    }
    
    //요구 편의 이니셜라이저 구현
    required convenience init(name: String) {
        self.init()
        self.name = name
    }
}

Korea클래스의 init() 지정 이니셜라이저Country 클래스로 부터 재정의 하였고, required 키워드를 사용하여 Korea클래스에서 요구 이니셜라이저로 정의하였다.

Korea클래스의 init(name: String) 편의 이니셜라이저 또한 required 키워드를 사용하여 요구 이니셜라이저로 정의하였다.

참고 링크

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글