개인적으로 스위프트가 문법 구조가 맛깔나서 매력적인 언어임에는 틀림 없지만 쉬운 언어는 절대 아니라고 생각한다. 그 이유는 여러가지 지켜야 할 사항이 많기 때문이다. 어렵다는건 두가지 측면이 있는데, 논리적으로 복잡해서 어려운 것이 있고 양이 많아서 어려운 것이 있다.
스위프트는 특정한 부분에서 외워야 할 것이 많은데, 특히 생성자에 관한 규칙이 그러하다.
핵심 키워드 : Two-Phase Initialization, Designated Initializer, Convenience Initializer, Inheritance, Safety Check
상속
클래스는 다른 타입과 달리 상속을 할 수 있다는 것이 가장 큰 차이점이다. 서브 클래스는 슈퍼 클래스의 모든 메소드, 프로퍼티, 특징을 이어 받는다. 또한, 프로퍼티와 메소드의 추가, 변경이 가능하다.
기반 클래스(Base class = Root class)
다른 특정한 클래스를 상속하지 않은 클래스를 기반 클래스라고 한다. Objective-C 는 NSObject 와 NSProxy 라는 2개의 기반 클래스를 이용하지만, 스위프트는 전역적인 기반 클래스가 없다. 따라서 아무것도 상속하지 않고 클래스를 정의하면 그것이 바로 기반 클래스가 되는 것이다. 하지만 실질적으로 NSObject 를 상속하는게 foundation framework를 이용 할 수 있으니 상속하는게 일반적이다.
Cocoa Foundation Framework 의 일반적인 기능을 이용하려면 NSObject 를 상속하면 된다.
클래스 상속
클래스 상속은 정의한 클래스 이름 뒤에 “ : 수퍼 클래스”를 적으면 된다.
[code language=”swift”]
class Shape {
func draw() { … }
}
class CircleShape : Shape {
var radius:Float
…
}
[/code]
생성(Initialization)
생성은 클래스, 구조체, 열거형은 인스턴스 준비하는 과정이다. 이 메소드를 생성자(Initializer)라고 한다.
생성시 꼭 수행되야 하는 일
- 모든 저장형 프로퍼티에 값이 정해져야 한다.
물론 옵셔널은 예외다. 하지만, 옵셔널이 아니면 강제사항이다. 생성자에서 값이 정해지던지, 언언부에서 기본값이 정해 지던지 해야한다.
- 기본 값 정의
* ex)
struct circle {
var radius = 10.0
}
- 생성자
* init() 메소드 정의를 통해 수행
* ex)
struc circle {
var radius
init () {
radius = 10.0
}
}
TIP) 기본 값 를 사용하는 것과, 생성자에서 값을 지정하는 것의 차이는 무엇일까? 어떤 방식을 왜 선택해야 할까?
값 타입(Value Type =Structure, Enumeration) 생성자 규칙
만약 값 타입에 새로운 생성자(Custom Initializer)를 만들면, 기본 생성자는 더 이상 사용 할 수 없게 된다.
또한, self.init 은 생성자 안에서만 호출 할 수 있다.
팁 : 만약 값 타입에서 커스텀 생성자와 기본 생성자를 같이 쓰고 싶다면, 자신의 선언에서 사용하지 말고 Extentions 를 활용하는게 좋다.
클래스 상속
이 부분이 현재 글에서 가장 중요한 부분이라 생각한다. 스위프트는 생성자가 논리적으로 무결하게 동작하도록 하기 위해 몇 가지 규칙을 만들었다.
지정 생성자(Designated Initializer)와 편의 생성자(Convenience Initializer)
지정 생성자는 생성자 중에서 가장 근원적인 생성자이다(꼭 한번은 호출된다). 클래스의 모든 프로퍼티가 초기화 되며, 정확한 생성자 프로세스를 수행하기 위해 가장 적합한 수퍼 클래스의 생성자를 호출하기도 한다.
여러개의 지정 생성자가 있을 수도 있지만, 일반적으로 1개만 만드는 것이 일반적이다. 지정 생성자는 상속 상하위 관계에서 초기화 작업의 종착지이기도 하다.(funnel point )
편의 생성자는 인스턴스를 생성 할 때 몇가지 편리한 방식을 제공하기 위한 것이다. 편의 생성자는 일반적으로 파라미터가 없거나 적고( init() ), 지정 생성자는 생성자 파라미터에 프로퍼티를 상세하게 지정하는 경우가 일반적이다. ( init(_ name:String, ……… ) )
지정 생성자
[code language=”swift”]
init(parameters) {
statements
}
[/code]
편의 생성자
[code language=”swift”]
convenience init(parameters) {
statements
}
[/code]
위 문법만 보면 init 앞에 convenience 한정자를 붙인 것 외에 다른 것을 못느꼈을 것이다.
생성자 규칙
지정 생성자는 직전 수퍼 클래스의 지정 생성자를 호출 해야 한다.
편의 생성자는 같은 클래스의 다른 생성자를 반드시 호출 해야 한다.
편의 생성자는 최종적으로 지정 생성자를 호출 해야 한다.
다시 말하면
지정 생성자는 하위 클래스에서 상위 클래스를 호출하며 (super.init 호출)
편의 생성자는 ‘같은 클래스’ 내에서 호출이 이루어 진다. (self.init 호출)
도식화하면 아래와 같다.
생성자 2단계
인스턴스가 생성 될 때는 2 단계를 거친다.
현재 클래스의 모든 저장형 프로퍼티 초기화(선언부에서 초기값 지정)
1번이 끝난 후, 필요한 작업을 수행(customize)하고 인스턴스를 사용 할 수 있게 한다.
※ Objective-C 와 다른점은 Objective-C 는 기본적으로 0 이나 nil 로 초기화 하지만, 스위프트는 0이나 nil 이 아닌 적절한 초기화가 이루어 지도록 한다.
생성자 규칙 체크 (Safety Check)
컴파일러는 위 규칙을 정확히 적용 했는지 알려준다. 아래 규칙을 위반하면 컴파일러 에러가 발생한다. 이 규칙들을 살펴보면 한 프로퍼티가 최종적으로 내가 의도한 값이 아닌 다른 값을 가지게 되는 것을 방지하기 위함이다.
수퍼클래스의 지정 생성자를 호출하기 전에(super.init) 해당 클래스의 모든 프로퍼티가 초기화 되어야 한다.
= 지정 생성자에서 해당 클래스의 모든 프로퍼티가 초기화 되어야 한다.수퍼 클래스에서 상속받은 프로퍼티의 값을 서브 클래스(의 지정 생성자)에서 변경하기 전에, 이미 수퍼 클래스의 지정 생성자가 호출 되었어야 한다. 왜냐하면 서브 클래스의 지정 생성자에서 상속받은 프로퍼티의 값을 변경한 후 1번 규칙에 따라 수퍼 클래스의 생성자를 호출하면 서브 클래스에서 호출한 부분이 덮어 씌여질 수 있기 때문이다.
올바른 순서 : super.init() …. -> 상속받은 프로퍼티 수정
잘못된 순서 : 상속받은 프로퍼티 수정 -> super.init()편의 생성자의 가장 처음 동작은, 다른 생성자를 호출하는 것이다.
생성자 2단계 중 1단계가 지나기 전에는 다음과 같은 일을 하지 못한다. (그 후에 가능하다는 이야기)
1. 인스턴스의 다른 메소드 호출
2. 인스턴스의 다른 프로퍼티 읽기
3. self 이용하기
이 부분이 어찌나 중요한지 또 설명이 있다.
Phase 1
지정 생성자나 편의 생성자가 클래스에서 호출 됨
새로운 인스턴스를 위한 메모리가 할당 된다. 아직 초기화는 안된 상태.
지정 생성자는 클래스에서 선언된 모든 저장형 프로퍼티가 값을 갖도록 한다.
지정 생성자는 수퍼 클래스에 대해서도 바로 위의 동작을 수행하게 한다. 즉, 수퍼 클래스에서 정의한 저장형 프로퍼티도 초기 값을 갖게 한다.
이와 같은 동작이 상속 연결 고리에 따라 쭉 이어서 발생하게 한다.
최종적으로 가장 최상의 수퍼 클래스의 초기화가 끝나면 모든 저장형 프로퍼티가 초기값을 갖게 된다. 이로써 1단계가 완결된다.
Phase 2
1단계가 완료되면 드디어 다른 작업을 생성자에서 할 수 있다. 프로퍼티의 값을 바꿀 수 있다. self 를 이용 할 수 있고, 다른 메소드도 호출 가능해진다.
최종적으로 편의 생성자에서 self 를 이용 할 수 있게 되고, 필요한 작업 또한 수행 할 수 있다.
요약하면 수퍼 클래스에서 서브 클래스 방향으로 지정 생성자가 실행되어야 하고, 그 다음에 편의 생성자에서 자신만의 기능을 수행 할 수 있게 된다.
생성자 상속과 오버라이드
Objective-C 와는 다르게 스위프트는 자동으로 생성자를 상속하지 않는다. 앞서 설명했듯이 스위프트에서는 상속 고리에서 발생 할 수 있는 의도치 않은 초기값이 정해지는 것을 방지하기 위해서이다.
하지만, 자동 상속이 되는 규칙을 지정해 두었다. 특히, Objective-C 와 연동해서 사용 할 때 이규칙을 모르면 혼동이 올 수 있으니 꼭 숙지해야 한다.
Swift 에서 프로퍼티, 메소드, subscript 를 서브 클래스에서 재정의 할 때는 꼭 override 한정자로 수식해야 한다. 그러면 위에 적은 규칙들에 의해 정확히 구현되었는지 컴파일러가 체크해준다.
또 하나의 규칙은 지정 생성자를 재정의 할 때는 꼭 override 키워드를 붙여야 한다. 서브 클래스에서 지정 생성자를 편의 생성자로 변경 할 수도 있지만 이때도 꼭 override 를 붙여야 한다.
만약 수퍼 클래스의 편의 생성자를 서브 클래스에서 지정 생성자로 오버라이드 하면, 수퍼 클래스의 편의 생성자는 서브 클래스에서 바로 호출 할 수 없을 것이다. 왜냐면 규칙에 의해 편의 생성자는 해당 클래스의 생성자 내부에서만 호출되어야 한다. 그러므로 편의 생성자와 지정 생성자가 서브 클래스에서 바뀌게 되면 위 규칙에 맞춰서 적절하게 모든 생성자들을 재구성 해주어야 한다.
생성자 자동 상속 규칙
위에 언급했듯이 생성자는 자동 상속되지 않는다. 하지만 특정 조건을 만족하면 자동 상속한다. (이 부분이 Objective-C 와 다른 부분이다. Objective-C 는 모두 자동 상속 한다.)
자동 상속 규칙 1
- 서브 클래스에서 지정 생성자를 새로이 만들지 않았다면, 수퍼 클래스의 모든 지정 생성자가 상속 된다.
자동 상속 규칙 2
- 서브 클래스에서 수퍼 클래스의 모든 지정 생성자를 상속했다면 서브 클래스는 수퍼 클래스의 모든 편의 생성자를 상속하게 된다. 이것은 지정 생성자는 자동 상속 규칙 1에 의해 상속 되는 경우와, 직접 모든 지정 생성자를 구현 했을 경우 둘 다 포함한다.
이 규칙은 서브 클래스에 편의 생성자를 추가 했을 때도 유효하다.
2번 규칙에 의해서 수퍼 클래스의 지정 생성자를 서브 클래스에서는 편의 생성자로 구현 하는 것이 가능하다.
실패 가능한 생성자
여러 사정에 의해, 가령 파라미터가 잘 못 됐다던지 리소스가 없다던지 어딘가에 문제가 발생 해서 인스턴스를 생성 할 수 없는 경우가 있을 것이다. 이 때 유용하게 쓰이는 것이 실패 가능한 생성자이다. 클래스, 구조체, 열거형 모두 적용 가능하다.
init 대신 init? 을 사용한다.
실패 시 nil 을 반환한다.
실패 시 “ return nil “ 을 한다고 해서, 성공 했을 때 “return” 을 하면 안된다. Swift 생성자는 인스턴스를 반환하는 형식이 아니기 때문이다.
optional 변수와 같이 사용하는게 좋을 것이다.
실패 가능한 생성자는 생성 프로세스 전체에 영향을 주게 된다. 그래서 생성자 체인의 한 부분에서 실패가 일어나면 결과적으로 실패로 끝나게 된다.
실패 가능한 생성자는 실패 없는 생성자를 대리 할 수 있다. 실패하지 않는 기존 초기화 프로세스에 잠재적 장애 상태를 추가하기 위해서 이 방법을 이용 할 수 있다.
실패 가능한 생성자 오버라이드
실패 가능한 생성자를 서브 클래스에서 실패 없는 생성자로 오버라이드 가능하다. 반대는 안된다.
다른 규칙으로는, 실패 없는 생성자에서 실패 가능한 생성자를 사용하려면 ‘Force unwrapping’ 을 해야 한다. ( super.init()! )
이 때, Force Unwrapping 에 실패하면 런타임 오류가 발생한다.
init!
실패 가능한 생성자인데, implicitly unwrapped optional 인스턴스를 만드는 생성자이다.
init! -> init? 호출 가능
init? -> init! 호출 가능
init -> init! 호출 가능. 다만 실패하면 런타임 에러 발생
필수 생성자 (Required Initializer)
required 키워드를 사용하면 서브 클래스에서 해당 생성자를 꼭 구현해야 한다. 또한 서브 클래스의 생성자마다 다시 required 한정자를 적어야 한다.
필수 지정 생성자를 오버라이드 할 때는 override 키워드를 붙이지 않는다.
예외적으로, 자동 상속 조건을 만족한다면 필수 생성자를 꼭 구현해야 할 필요는 없다.
클로저와 함수를 이용한 기본값 지정
아래와 같은 방식으로 기본값을 클로저나 글로벌 함수를 이용해 생성 할 수 있다.
class SomeClass {
let someProperty: SomeType = {
return someValue
}()
let otherProperty:OtherType = createOtherFunction()
}
이 때 유의할 점은 이것은 초기화가 끝난 상태가 아니다. 즉 self 를 호출해도 안되고, 다른 멤버를 이용하는 것도 안된다.
규칙 다시 보기
생성자 규칙
지정 생성자는 직전 수퍼 클래스의 지정 생성자를 호출 해야 한다.
편의 생성자는 같은 클래스의 다른 생성자를 반드시 호출 해야 한다.
편의 생성자는 최종적으로 지정 생성자를 호출 해야 한다.
생성자 자동 상속 규칙
- 자동 상속 규칙 1
* 서브 클래스에서 지정 생성자를 새로이 만들지 않았다면, 수퍼 클래스의 모든 지정 생성자가 상속 된다.
- 자동 상속 규칙 2
* 서브 클래스에서 수퍼 클래스의 모든 지정 생성자를 상속했다면 서브 클래스는 수퍼 클래스의 모든 편의 생성자를 상속하게 된다. 이것은 지정 생성자는 자동 상속 규칙 1에 의해 상속 되는 경우와, 직접 모든 지정 생성자를 구현 했을 경우 둘 다 포함한다.