Swift의 클래스 초기화

공식 문서 일부를 번역한 글 입니다.

클래스 상속과 초기화

프로퍼티가 자동으로 0 혹은 nil로 초기화되는 Objective-C와는 달리 Swift에서는 모든 저장 프로퍼티에 대해서 초기화가 이루어져야 한다. (그렇지 않은 경우 컴파일러가 오류를 내놓는다.)

구조체의 경우, 모든 멤버의 값을 정의하는 기본 초기화 메소드가 자동으로 준비되고, 클래스 정의시에 모든 멤버의 초기값을 지정했다면 별도의 초기화 메소드를 만들어줄 필요가 없다.

클래스의 경우에도 모든 멤버의 초기값이 정해진 상태라면 디폴트 초기화 메소드가 자동으로 제공될 것이다. 하지만, 클래스의 경우 다른 클래스를 상속받는 경우가 있기 때문에 조금 주의가 필요하다. 특히 Swift의 경우 상속받는 부모 클래스의 초기화 메소드를 자동으로 상속받지 않으므로 (왜냐면, 부모의 초기화 메소드는 자식에게 추가된 새로운 멤버를 초기화하지 못하므로) 누락되는 멤버가 발생할 수 있기 때문이다.

대신에 디폴트 초기화 메소드와 마찬가지로, 현 클래스에서 새롭게 정의된 멤버가 정의와 동시에 초기값이 할당된다면 이는 특별한 케이스가 되어 초기화 메소드를 자동으로 상속한다.

“모든 멤버가 적절한 초기값을 가지고 완전히 초기화된다”는 목적을 만족시키면서 초기화 메소드 사이의 코드 중복을 최소화 하기 위해 Swift의 초기화메소드는 지정 초기화 메소드(Designated Initializer)와 편의 초기화 메소드(Convenience Initializer)로 구분된다.

Designated Initializer

지정 초기화 메소드는 클래스의 원시 초기화 메소드이다. 이 초기화 과정에서는 해당 클래스의 모든 멤버를 초기화해야 한다. 따라서 현 클래스에서 새롭게 정의한 모든 멤버의 초기값을 세팅하고, 다시 부모의 초기화 메소드를 호출하여 상속 받은 모든 멤버들의 초기값을 세팅한다.

클래스는 지정 초기화 메소드의 수를 최소화하려는 경향이 있으며 보통은 단 하나의 초기화 메소드만을 가진다. 이는 마치 깔대기처럼 수렴한다. (즉 다른 초기화메소드 과정에서 다시 지정 초기화 메소드를 호출한다는 의미)

모든 클래스는 최소 1개의 지정 초기화 메소드를 가진다. 경우에 따라서는 부모로부터 몇 개의 초기화 메소드를 상속받는 것으로 모든 조건을 충족하게 되는데 (현 단계에서 새롭게 추가한 멤버에 대해 추가적인 초기화가 필요없는 경우) 이를 자동 초기화 메소드 상속이라 한다.

Convenience Initializer

편의 초기화 메소드는 사용에 편의를 더하기 위해서 만든 보조적인 초기화 메소드이다. 이는 일부 혹은 전체 멤버에 대해서 별도의 디폴트 값을 주면서 지정 초기화 메소드를 다시 호출하게 된다. 반드시 만들어야 하는 것은 아니며, 구분을 위해 convenience 키워드를 init 앞에 붙여준다. 편의 초기화 메소드는 앞의 설명을 대로 그 내부에서 반드시 지정 메소드를 반드시 호출해야 한다.

연쇄 호출

모든 멤버를 초기화 하기 위해서는 몇 가지 규칙이 있다.

  1. 지정 초기화 메소드는 자신의 모든 멤버를 초기화 한 후, 반드시 바로 부모의 지정 초기화 메소드를 호출해야 한다.
  2. 편의 초기화 메소드는 다시 클래스 자신의 다른 초기화 메소드(주로)지정 초기화 메소드를 반드시 호출해야 한다.
  3. 2를 지키지 않는 경우가 있더라도 최종적으로는 지정 초기화 메소드를 호출해야 한다.

즉 클래스의 초기화 시에 편의 클래스 메소드는 내부에서 다른 편의 초기화 메소드를 호출하는 식으로 연쇄적 초기화를 시도하는데, 그 과정에서 반드시 한 번은 지정 초기화 메소드를 호출해야 한다. 왜냐하면 지정 초기화 메소드를 호출하지 않더라도 현 클래스의 모든 멤버를 초기화 할 수 있지만, 상속 받은 멤버는 부모 클래스의 초기화 메소드를 호출해야만 초기화할 수 있기 때문이다.

2단계 초기화

Swift의 클래스 초기화는 두 가지 단계를 통해 일어난다. 첫 단계는 클래스 자신이 소개한 새로운 멤버들을 초기화하고, 두 번째 단계에서는 상속받은 멤버들을 (부모의 지정 초기화 메소드를 호출함으로써) 모드 초기화하는 것이다.

의도한 대로 초기값이 만들어지기 위해서는 다음의 규칙을 따르는 것이 좋다. (컴파일러가 아마 강제하는 부분도 있을 것이다.)

  1. 부모의 초기화 메소드를 호출하기 이전에, 자신이 소개한 모든 신규 멤버는 초기화해야 한다.
  2. 상속받은 멤버를 초기화하려면, 그 이전에 부모 클래스의 초기화 메소드를 호출해야 한다. 그렇지 않으면 자신이 세팅한 값이 다시 부모 클래스가 초기화하는 값으로 덮어써질 위험이 있다.
  3. 편의(Convenience) 초기화 메소드는 반드시 다른 초기화 메소드를 호출(이 과정에서 최종적으로는 designated 초기화메소드가 호출되어야 한다).해준 다음에 다른 멤버를 초기화한다. 즉 모든 프로퍼티가 초기화된 후, 별도의 초기값을 다시 지정해야 한다. 역시 2번과 같은 이유에서이다.
  4. 초기화 중에는 인스턴스 메소드를 써서는 안되며, 인스턴스 프로퍼티를 읽어서도 안된다. 또한 초기화가 완전히 끝나지 않은 상태에서 self를 참조하는 것도 위험하다.

초기화 과정 추적

2단계 초기화 과정을 추적해보자. 앞서 잠깐 언급했지만, 첫 단계는 부모 방향으로 거슬러 올라가면서 모든 멤버의 초기값을 일단 만들어주고, 2단계에는 다시 초기화된 멤버 중 일부를 커스터마이징하는 과정이다.

  • 클래스의 초기화 메소드가 호출된다.
  • 클래스 인스턴스에 대한 메모리가 할당(allocate)된다. 아직 초기화되지 않은 상태이다.
  • 클래스의 지정 초기화메소드에 이르러, 자신이 소개한 모든 새로운 멤버가 초기화된다. 이 때는 아직 상속받은 멤버들은 초기화가 되지 않았다.
  • 지정 초기화메소드는 부모 클래스의 지정초기화 메소드를 호출한다. 이제 부모가 정의한 멤버가 초기화 된다.
  • 이 과정은 루트 클래스에 이를때까지 계속된다.
  • 꼭대기에 다다르게 되면 모든 멤버의 초기화가 일단 완료된 것이 보장된다.

2단계는 초기값 커스터마이징이다.

  • 꼭대기 클래스에서 다시 한 단계씩 아래로 내려간다.
  • 바로 다음 단계의 지정 초기화 메소드의 뒷부분에서 필요한 커스터마이징이 실행된다.
  • 이 과정이 계속해서 반복되고 다시 현단계의 클래스까지 이어져 내려온다.
  • 이제 흐름은 현단계의 편의 초기화 메소드쯤 오게되면 안전하게 self를 참조할 수 있게 된다.

즉, designated initalizer가 호출되고 나면 self를 사용할 수 있다고 보면 된다.

## 초기화 메소드 상속과 오버라이딩

  1. 앞서 제기한 문제, 완전한 초기화가 이루어지지 않는 문제로 Swift는 초기화메소드를 자동으로 상속하지 않는다.
  2. 만약 초기화 메소드를 오버라이딩한다면 override 키워드를 사용해줘야 한다. 오버라이드하는 메소드가 지정 메소드라면, 부모의 동일한 메소드를 호출해주도록 한다.
  3. 오버라이드 하는 메소드가 편의 메소드라면, 부모의 편의 메소드를 직접 호출해서는 안된다. 자신의 지정 초기화 메소드를 호출해야 한다. 따라서 이것은 사실 상 오버라이드가 아니다. 그래서 편의 메소드 앞에는 override를 붙이지 않는다.

자동 상속

  1. 모든 신규 멤버에는 초기값이 정의되어있어야 함
  2. 이 때 서브클래스가 지정 메소드를 하나도 정의하지 않았다면, 부모의 지정 메소드들이 자동 상속된다.
  3. 부모의 지정 메소드를 모두 제공한다면 (상속받거나, 작성하여) 편의 메소드들은 자동으로 상속된다.

부모 클래스에서 지정 메소드였던 것을 자식 클래스에서는 편의 메소드로 구현하는 것도 가능하다. 이 때에는 부모의 지정 메소드가 아닌 자신의 지정 메소드를 호출하도록 코딩해야 한다.

필수 메소드

required 키워드를 init 앞에 붙이면 이는 필수 구현 메소드가 되며, 모든 자손들이 해당 초기화 메소드를 구현해야 한다. required 메소드는 반드시 오버라이딩되는 것이므로 별도로 override 를 붙이지 않는다.