Swift의 클래스 초기화

공식 문서 일부를 번역한 글 입니다. (수정: 2019.06.18)

용어 번역 및 의미

  • initializer – 초기화 메소드. 객체 인스턴스가 생성될 때 호출되는 init(*)류의 이니셜라이저
  • designated initailizer – 지정 초기화메소드. 해당 클래스 레벨에서 새롭게 추가된 프로퍼티를 모두 초기화할 수 있는 메소드. 부모 클래스의 지정 초기화메소드를 호출해야 한다.
  • convenience initializer – 편의 초기화메소드. 프로퍼티에 적절한 기본값을 추가하여 호출을 간단하게 만든 초기화메소드. 반드시 해당 클래스의 지정 초기화 메소드를 호출해야 한다.
  • 초기화 – Swift에서는 객체를 생성할 때 객체가 요구하는 크기만큼의 메모리를 할당받은 후, 모든 프로퍼티는 해당 객체가 사용되기 전에 반드시 적절한 초기값을 가져야 한다. 프로퍼티에 초기값을 지정해 주는 행위
  • 멤버 – 프로퍼티. 일반적인 의미로 멤버라고 부른다.

클래스 상속과 초기화

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

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

구조체의 초기화메소드는 멤버의 디폴트 초기값에 대에서 ALL OR NONE이었다. 즉 멤버 중에서 단 하나라도 디폴트값이 있따면 자동으로 이니셜라이저를 생성하지 않았다. 이는 Swift 5.1에서 개선되었다.

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

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

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

Designated Initializer

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

클래스는 지정 이니셜라이저의 수를 최소화하려는 경향이 있으며 보통은 단 하나만을 가진다. 그리고 그 수행 과정은 마치 깔대기처럼 지정 이니셜라이저로 수렴한다. (즉 다른 초기화 메소드를 작성하더라도 보통은 그 실행 과정에서 다시 지정 초기화 메소드를 호출하기 때문에 결국 하나로 충분하다는 이야기이다.)

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

Convenience Initializer

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

연쇄 호출

Swift는 안정성 확보를 위해 새롭게 생성된 객체가 사용되기 전에 모든 멤버가 초기화된 상태를 강제하려는 정책을 가지고 있다. 클래스를 상속하는 경우, 여러 사소한 실수로 모든 멤버가 적절히 초기화되지 않는 상황을 만나기 쉬운데, 이런 문제를 컴파일 타임에 모두 캐치하기 위해서 멤버를 초기화하기 위한 규칙을 두고 있다.

  1. 지정 이니셜라이저는 자신의 모든 멤버를 초기화 한 직후, 다른 멤버를 액세스하기 전에 반드시 바로 부모의 지정 이니셜라이저를 호출해야 한다.
  2. 편의 이니셜라이저는 다시 클래스 자신의 다른 이니셜라이저를 반드시 호출해야 한다.
  3. 2를 지키지 않는 경우가 있더라도 최종적으로는 해당 클래서의 지정 이니셜라이저를 호출해야 한다. 즉 편의 이니셜라이저는 다른 편의 이니셜라이저를 호출하더라도, 그 호출 연쇄의 끝에는 지정 이니셜라이저가 있어야 하며 이를 통해 다시 부모의 지정 이니셜라이저를 호출하게 된다.

이 규칙이 목표하는 바는 명확하다. 지정 이니셜라이저는 반드시 자신이 도입한 멤버의 초기화를 책임진다. 그리고 그 내부에서 부모의 지정 이니셜라이저를 호출한다. 따라서 ‘지정 이니셜라이저를 호출하는 것으로 모든 멤버의 초기화 완료를 보장’하는 것이다. 그리고 이 모든 것의 기반이되는 제 1원칙. 모든 멤버의 초기화가 완료된 시점이후에 객체가 사용가능하다는 것은 이니셜라이저에서도 적용된다. 즉 지정 이니셜라이저를 호출하거나, 부모의 지정 이니셜라이저를 호출한 이후 시점부터 self를 사용할 수 있다.

이니셜라이저의 인자 이름과 멤버이름이 같은 경우 self.x = x 와 같이 사용하는 케이스는 예외로 둔다.

2단계 초기화

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

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

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

초기화 과정 추적

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

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

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

  • 꼭대기 클래스에서 다시 자식 클래스로 한 단계씩 아래로 내려간다.
  • 자식 클래스의 이니셜라이저로 돌아오게 되면, 부모의 지정 이니셜라이저를 호출한 시점으로 복귀한다. 이 시점에서 해당 단계에서의 상속받은 멤버에 대한 커스터마이징을 할 수 있다.
  • 이 과정이 계속해서 반복되고 다시 최종 단계의 자식 클래스까지 이어져 내려온다.

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

Swift의 독특한 이니셜라이저 정책은 타 언어와 다른 측면이 존재하며, 언뜻봐선 이상하게 돌아가는 거 같이 보이는 결과가 초래되기도 한다. 먼저, 클래스를 상속하면서 프로퍼티를 새롭게 추가하는 경우, 이 프로퍼티에 대한 초기화를 보장하는 것은 프로그래머의 책임이다. 따라서 Swift는 기본적으로 (안전하다고 판단되는 몇몇 상황을 제외하면) 초기화메소드를 자동으로 상속하지 않는다. 지정 이니셜라이저를 오버라이딩한다면 여기에 override 키워드를 써서 오버라이드임을 명시한다. 또한, 이 과정에서는 반드시 부모의 동일 이니셜라이저를 호출해서 초기화 체인을 완성해야 한다.

부모의 편의 이니셜라이저를 커스터마이징한다고치자. 현 클래스에서도 이는 여전히 편의 이니셜라이저이다. 따라서 규칙에 의해 부모의 것이 아닌 자신의 지정 이니셜라이저를 호출해야 한다. 따라서 편의 이니셜라이저는 실질적으로 오버라이드가 아니며, override 키워드도 쓰지 않는다.

자동 상속

앞서도 말했지만 Swift는 기본적으로 이니셜라이저를 자동으로 상속해주지 않는다. 이는 안정성을 확보하는 필수적인 수단이지만, 클래스를 상속하여 작성하려는 입장에서는 여간 불편한 것이 아니다. 대신 클래스를 자동상속 하지 않으려는 이유 -모든 멤버가 초기화되지 않는 상황을 막는 것-가 분명하기 때문에, 몇몇 조건에서는 이니셜라이저에 대한 자동 상속이 이루어진다.

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

먼저 기본적인 조건은 신규 멤버가 없거나, 있는 경우에는 선언 시에 디폴트값을 명시하는 것이다. 그러면 해당 클래스의 지정 이니셜라이저가 없더라도 ‘현단계의 새로운 멤버’가 없는 것으로 간주되어 부모의 지정 이니셜라이저들이 자동으로 상속될 수 있다.

서브 클래스에서 지정 이니셜라이저를 직접 작성하였다면, 부모의 지정 이니셜라이저는 상속되지 않는다. 지정 이니셜라이저를 직접 작성했다는 것은, 필요한 초기값을 설정하기 위해서 자신의 이니셜라이저를 호출해야 한다는 것이다. 이 시점에서 부모의 다른 지정 이니셜라이저를 상속받는다는 것은, 이를 통한 초기화 시 누락된 멤버가 발생할 수 있다는 것이므로 자동으로 상속될 수 없음을 암시한다.

모든 편의 이니셜라이저는 그 자신의 지정 이니셜라이저에 의존한다. 따라서 자동 상속에 의해서든, 오버라이드에 의해서든 부모가 가진 모든 지정 이니셜라이저를 갖추게 되었다면 편의 이니셜라이저는 자동으로 상속받게 된다.

필수 메소드

required 키워드를 init 앞에 붙이면 이는 필수 구현 메소드가 되며, 모든 자손들이 해당 초기화 메소드를 구현해야 한다. required 메소드는 반드시 오버라이딩되어야 하는 것이므로 별도로 override 를 붙이지 않는다. 보통 프로토콜에서 이니셜라이저를 지정하는 경우에 자동으로 required가 된다.