[Swift] 생성자는 어떻게 상속될까?

강대훈·2025년 4월 4일
post-thumbnail

UIKit을 사용할 때, ViewController를 초기화 할 때 ViewController() 를 하면서 생성자의 상속 기준을 잘 몰랐던 나는 내가 직접 생성자를 만들지 않았는데도 왜 이게 가능한건지 제대로 생각해 본 적이 없었던 것 같다.

이번 기회에 클래스의 생성자에 대해서 제대로 공부해보고 UIViewController의 생성자에 관해서도 한 번 얘기해보려고 한다.

먼저 생성자란 무엇일까?

클래스, 열거값, 구조체를 생성할 수 있는 도구

→ 모든 프로퍼티를 기본값으로 저장하는 행위이며, 실패하면 생성하지 못한다.

기본적인 생성자 예시

class Cat {
	var name: String
		
	init(name: String) {
		self.name = name
	}
}

저장 속성이 초기화되어 있지 않다면 반드시 지정 생성자를 통해서 저장 속성이 초기화가 되어야 한다.

class Color {
	var red: Double
	var blue: Double
	var green: Double

	init() {
			red = 0.0
			blue = 0.0
			green = 0.0
	}

	init(value: Double) {
			red = value
			blue = value
			green = value
	}

다음과 같이 저장 속성이 초기화만 될 수 있다면 지정 생성자는 많아도 상관없다.

편의 생성자

편의 생성자는 반드시 최종적으로 지정 생성자를 호출해야 한다.
(편리하게 사용될 수 있는 생성자)

→ 결국 최종적으로 지정 생성자를 호출하기 때문에, 편의 생성자에서는 모든 저장 속성이 초기화 되지 않아도 된다.

class Zoo {
    var catName: String
    var dogName: String
    var dolphinName: String
    
    init(catName: String, dogName: String, dolphinName: String) {
        self.catName = catName
        self.dogName = dogName
        self.dolphinName = dolphinName
    }
    
    convenience init() {
        self.init(name: "이름") // 또 다른 편의 생성자 호출
    }
    
    convenience init(name: String) {
        self.init(catName: name, dogName: name, dolphinName: name)
    }
}

편의 생성자는 또 다른 편의 생성자를 호출할 수 있지만, 최종적으로는 반드시 지정 생성자가 호출되어야 한다.

편의 생성자에서는 모든 저장 속성이 초기화 되기 전까지 저장 속성에 접근할 수 없다. 그 이유는 메모리가 초기화 되지 않았기 때문이다.

class Zoo {
    var catName: String
    var dogName: String
    var dolphinName: String
    
    init(catName: String, dogName: String, dolphinName: String) {
        self.catName = catName
        self.dogName = dogName
        self.dolphinName = dolphinName
    }
    
    convenience init() {
        self.init(name: "이름")
    }
    
    convenience init(name: String) {
		// self.catName = name (불가능) (1)
        self.init(catName: name, dogName: name, dolphinName: name)
        // self.catName = "나비" (가능) (2)
    }
}

지정 생성자가 호출된 뒤로부터는 저장 속성이 모두 초기화 됐기 때문에 저장 속성을 수정할 수 있는 기회를 받게 된다. 그렇기 때문에 (2)의 경우는 수정이 가능하다. 지정 생성자가 호출된 시점부터 메모리가 초기화됐기 때문이다.

상속에서의 편의 생성자

class Alpha {
    var a: Int
    var b: Int
    var c: Int
    
    init(a: Int, b: Int, c: Int) {
        self.a = a
        self.b = b
        self.c = c
    }
    
    convenience init(number: Int) { 
        self.init(a: number, b: number, c: 0)
    }
    
    convenience init() {
        self.init(number: 5)
    }
}

class Beta: Alpha {
    var d: Int
    
    init(a: Int, b: Int, c: Int, d: Int) {
        self.d = d
        super.init(a: a, b: b, c: c)
        self.b = 3 // 가능
    }
    
    convenience init() {
        self.init(a: 5, b: 5, c: 5, d: 5)
    }
}

편의 생성자는 슈퍼 클래스의 지정 생성자를 호출할 수 없다. 반드시 동일한 클래스의 지정 생성자가 호출되어야 한다.

또한 자식 클래스의 지정 생성자는 반드시 부모 클래스의 지정 생성자를 호출해야 한다. (편의 생성자 호출 X)

또한 부모 클래스 지정 생성자를 호출하기 전에 나 자신 클래스의 프로퍼티는 모두 초기화가 되어야 한다.

init(a: Int, b: Int, c: Int, d: Int) {
    self.d = d // 현재 인스턴스 프로퍼티
    super.init(a: a, b: b, c: c) // 부모 클래스 호출
    self.b = 3
}

생성자의 상속 조건

지정 생성자의 상속 조건

  1. 자식 클래스가 초기화 할 프로퍼티가 존재하지 않다면 지정 생성자는 자동으로 상속된다.
  2. 자식 클래스에서 지정 생성자를 만들어두지 않아야 한다.
class A {
    var a: Int
    
    init(a: Int) {
        self.a = a
    }
    
    init() {
		self.a = 5
	}
}

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

var bInstance = B(a: 10) // or B()

현재 자식 클래스인 B 클래스에서 프로퍼티가 모두 초기화가 되어 있으며, 지정 생성자를 구현해두지 않았다.

그렇기 때문에 부모 클래스인 A 클래스의 지정 생성자들을 자동으로 상속받게 된다.

편의 생성자의 상속 조건

  1. 위에 있는 2개의 조건을 만족했을 경우 자동으로 슈퍼 클래스의 편의 생성자는 상속된다.
  2. 부모 클래스의 지정 생성자를 모두 오버라이드 했을 경우
class A {
    var a: Int
    
    init(a: Int) {
        self.a = a
    }
    
    convenience init(num: Int) {
		super.init(a: num)
    }
}

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

var b = B(num: 5) // 편의 생성자 사용가능.

필수 생성자

부모 클래스에 구현되어 있다면 자식 클래스에서 필수 구현해야 하는 생성자

물론 예외는 존재한다. 위에서 봤듯이 지정 생성자를 상속받았다면 필수 생성자는 필수적으로 구현하지 않아도 된다.

결론적으로, 자식 클래스에서 지정 생성자를 직접 구현하거나 상위 지정 생성자를 오버라이드 했을 때 필수 생성자를 상속받아야 한다.

(1) - 지정 생성자를 직접 구현했을 때

class MyView: UIView {
    let subView: UIView
    
    init(subView: UIView) {
        self.subView = subView
        super.init(frame: .zero)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

(2) - 상위 클래스의 생성자를 오버라이드 했을 때

class MyView: UIView {
    let subView: UIView = {
        let view = UIView()
        view.translatesAutoresizingMaskIntoConstraints = false
        return view
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

(2) 의 경우는 상위 클래스의 모든 지정 생성자를 override한다면 편의 생성자를 상속받는다고 했다.

필수 생성자의 경우는 override 키워드가 붙지 않는 것일 뿐이지 실질적으로 override 가 맞기 때문에, 만약 부모 생성자에 편의 생성자가 존재했다면 편의 생성자까지 모두 상속받을 수 있다.

무슨 말인지 이해하기 쉽지 않다면 아래를 참고해보자.

class Aclass {
    var a: Int
    var b: Int
    
    init(a: Int, b: Int) {
        self.a = a
        self.b = b
    }
    
    required init(a: Int) {
        self.a = a
        self.b = 5
    }
    
    convenience init() {
        self.init(a: 5)
    }
}

class BClass: Aclass {
    var c: Int
    
    override init(a: Int, b: Int) {
        self.c = 5
        super.init(a: a, b: b)
    }
    
    required init(a: Int) {
        fatalError("init(a:) has not been implemented")
    }
}

UIViewController의 생성자

UIViewController를 상속받아서 사용하다 보면 초기화 할 프로퍼티가 존재하지 않다면 ViewController() 로 많이 초기화 했었을 것이다.

사실 이것 또한 UIViewController의 생성자를 상속받았기 때문에 이렇게 사용할 수 있는 것이다.

다음과 같이 두 개의 생성자를 상속 받는다. 실제로 UIViewController를 확인해보면 생성자를 확인할 수 있다.

만약 나의 ViewController가 저장 속성이 초기화되지 않거나 다른 값으로 초기화하고 싶어서 지정 생성자를 생성했다고 가정해보자.

다음과 같이 컴파일 에러가 발생하면서 필수 생성자를 만들라는 메세지가 나온다.

이것은 우리가 직접 지정 생성자를 만들었기 때문에, 부모 클래스에 있는 필수 생성자를 오버라이드 해야 하는 것이다.

이 필수 생성자를 호출해야 하는 이유는 NSCoding 과 연관이 있는데, 얘기가 길어질 수 있으니 다른 포스팅에서,,

아무튼 생성자가 언제 상속되고 편의 생성자가 왜 존재하는지 잘 몰랐었는데 직접 정리하고 나니까 후련한 마음이 든다.

이상으로 생성자에 대해 정리해보고 포스팅을 마치겠다.

0개의 댓글