본문 바로가기

MOBILE/ios

[swift] 클래스 상속과 초기화 메소드: 지정 초기화 메소드 vs 편의 초기화 메소드

swift에서의 상속과 초기화 메소드의 기본

swift에서 클래스 선언과 상속은 다음과 같은 형태로 이루어진다.  하지만 다음 코드는 오류를 발생시킨다. 왜 일까?

바로 초기화를 하지 않았기 때문이다.

class A
{
    var a: Int
    var b: Int
}

class B: A
{
    var c: Int
}

 

클래스에서 가장 중요한 것은 초기화이다. 물론 옵셔널 타입은 굳이 nil을 대입하지 않아도 자동으로 nil로 초기화를 해준다.

하지만 옵셔널 타입을 제외한 모든 저장 프로퍼티는 인스턴스가 생성되기 전에 무조건 초기화가 완료되어야 한다.

즉 다음 형태와 같아야 한다는 뜻이다.

 

class A
{
    var a: Int = 0
    var b: Int = 0
}

class B: A
{
    var c: Int = 0
}

 

혹은 초기화 메소드에서 저장 프로퍼티의 값을 정해줘도 된다.

 

class A
{
    var a: Int
    var b: Int
    
    init()
    {
        a = 0
        b = 0
    }
}

 

이 둘의 차이점은 무엇일까?

바로 초기화 메소들의 자동 제공 유무이다.

 

즉 A와 같이 모든 저장 프로퍼티의 초기값이 있는 경우, 컴파일러는 init() 메소드를 자동으로 제공해준다.

따라서 A.init() 과 같이 코드를 작성할 수 있다.

하지만 초기화 메소드를 직접 쓰는 경우, 자동으로 제공해주는 init() 메소드는 더 이상 유효하지 않다.

 

상속은 부모의 메소드와 프로퍼티를 자식 클래스에 전해주는 것이다.

그렇다면 위와 같은 상황에서 초기화 메소드도 상속이 될까?

정답은 안된다는 것이다.

일반적으로 자식 클래스의 프로퍼티는 부모 클래스보다 같거나 많다. 따라서 부모 클래스의 초기화 메소드만으로는 자식 클래스의 프로퍼티를 완전하게 초기화하지 못할 수도 있다.

따라서 부모 클래스의 초기화 메소드는 상속되지 않는다.

 

위와 같은 이유로 A 클래스를 상속받는 B의 초기화 메소드는 다음과 같은 형태이다.

 

class A
{
    var a: Int = 0
    var b: Int = 0
    
    init()
    {
        a = 0
        b = 0
    }
}

class B: A
{
    var c: Int
    
    override init()
    {
        self.c = 0
        super.init()
    }
}

 

자식 클래스의 프로퍼티를 초기화 해준 다음에는 부모 클래스의 프로퍼티 또한 초기화 해야하기 때문에

super.init() 으로 부모 클래스의 초기화 메소드를 호출하는 것이다.

 

이런식으로 초기화 메소드가 연쇄적으로 불리는 것이 "초기화 메소드의 델리게이션"이다.


초기화 메소드를 여러개 작성한다고 하자. 그렇다면 분명 겹치는 코드가 발생할 것이다. 이렇게 초기화 메소드 사이에서의 코드 중복을 최소화하고 효율성을 높이기 위해 swift 설계자들은 초기화 메소드를 두 가지로 구분하였다.

바로 지정 초기화 메소드와 편의 초기화 메소드이다.

 

지정 초기화 메소드

지정 초기화 메소드(Designed initializer)는 클래스의 메인 초기화 메소드이다. 즉 모든 저장 프로퍼티가 완벽하게 초기화 되어있어야 하는 것을 책임진다는 것이다. 또한 앞의 이유 때문에 지정 초기화 메소드는 부모 클래스의 프로퍼티 또한 완벽하게 초기화를 해야하기 때문에, 부모 클래스의 지정 초기화 메소드를 호출한다.

 

편의 초기화 메소드

일부 초기화 메소드(Convenience initializer)는 말 그대로 일부 혹은 전체 프로퍼티의 초기화를 담당하는 것이다. 즉 꼭 모든 프로퍼티를 초기화 하지 않아도 된다. 그렇기 때문에 내부적으로 다른 초기화 메소드를 호출해야만 한다.

초기화 메소드 앞에 convenience 키워드를 붙임으로써 지정 초기화 메소드와 구분한다.

또한 편의 초기화 메소드의 마지막은 결국 지정 초기화 메소드를 가리키고 있어야 한다. 즉 연쇄적으로 편의 초기화 메소드가 불리우게 되더라도 무조건 마지막은 지정 초기화 메소드여야 한다는 것이다.

 

 

이러한 두 종류의 초기화 메소드 사이의 호출을 위해 몇가지 규칙이 존재한다.

1. 지정 초기화 메소드는 자신의 모든 저장 프로퍼티를 초기화하고, 부모 클래스의 지정 초기화 메소드를 호출한다.
2. 편의 초기화 메소드는 같은 클래스 내부의 초기화 메소드만 호출 가능하다.
3. 편의 초기화 메소드의 연쇄 호출은 결국 지정 초기화 메소드의 호출로 끝나야 한다.

 


초기화 메소드가 상속이 되는 특별한 경우

앞에서 일반적인 경우 초기화 메소드는 상속되지 않는다고 했다.

하지만 특별한 경우 초기화 메소드는 상속될 수 있다. 다음 조건이 있을 때 이다.

 

1. 새로운 저장 프로퍼티는 초기값이 선언과 동시에 있어야 한다.
2. 1번이 만족된 상황에서, 자식 클래스가 지정 초기화 메소드가 없을 경우, 자동으로 부모의 지정 초기화 메소드가 상속된다.
3. 1번이 만족된 상황에서, 부모의 지정 초기화 메소드를 모두 제공하는 경우, 편의 초기화 메소드 또한 자동으로 상속된다.

 

이렇게 조건만 나열해서는 사실 뭔 소리인지 알 수 없다. 다음 예시를 보면서 이해하자.


실제 호출 예시

 

실제 호출은 다음과 같이 이루어진다.

예를 들어 다음과 같은 코드는 B가 지정 초기화 메소드를 가지고 있고, 부모의 지정 초기화 메소드를 호출하는 형태이다.

 

class A
{
    var a: Int = 0
    var b: Int = 0
    
    init()
    {
        a = 0
        b = 0
    }
}

class B: A
{
    var c: Int
    
    init(C: Int)
    {
        c = C
        super.init()
    }
}

 

이 경우에 편의 호출 메소드가 추가된다면 다음과 같은 형태여야 한다.

 

class B: A
{
    var c: Int
    
    init(C: Int)
    {
        c = C
        super.init()
    }
    
    convenience init(value: Int)
    {
        self.init(C: value)
    }
}

 

즉 부모의 초기화 호출 메소드가 아닌 자신 클래스의 초기화 메소드를 호출해야 하는 것이다. 여기서는 다른 편의 메소드가 없으므로 지정 호출 메소드를 호출하는 것으로 끝나야 하기 때문에 init(C:)를 바로 호출하는 것이다.

 

그렇다면 이번에는 초기화 메소드가 상속될 조건을 만들어보자.

다음과 같은 경우는 모든 조건을 만족한다.

class A
{
    var a: Int = 0
    var b: Int = 0
    
    init(A: Int)
    {
        self.a = A
        self.b = 0
    }
    
    convenience init(a: Int)
    {
        self.init(A: a)
    }
}

class B: A
{
    var c: Int = 0
}

 

C 변수에 클래스 인스턴스를 생성한다고 하면 사용할 수 있는 초기화 메소드는 다음과 같다.

 

 

즉 A의 초기화 메소드를 상속받아서 B 인스턴스를 생성할 때도 사용할 수 있다는 것이다.