초기화
초기화 (Initialization)
타입의 저장 프로퍼티에 초기 값을 세팅하고, 인스턴스가 사용되기 전에 해둬야 할 준비 작업을 한 번 수행하는 절차를 알아볼게요. 바로 **초기화(initialization)**예요. 클래스, 구조체, 열거형의 인스턴스를 실제로 사용할 수 있게 만드는 과정이죠.
초기화는 **이니셜라이저(initializer)**를 정의해서 구현해요. 이니셜라이저는 특정 타입의 새 인스턴스를 만드는 데 호출되는 특별한 메서드 같은 존재예요. Objective-C의 이니셜라이저와는 달리 Swift의 이니셜라이저는 값을 반환하지 않아요. 핵심 역할은 '타입의 새 인스턴스가 처음 사용되기 전에 올바르게 초기화되도록 보장'하는 거예요.
클래스 타입의 인스턴스는 **디이니셜라이저(deinitializer)**도 구현할 수 있어요. 인스턴스가 해제되기 직전에 정리 작업을 수행하는 역할이죠. 디이니셜라이저에 대한 자세한 내용은 doc:Deinitialization에서 다룰게요.
본문
저장 프로퍼티의 초기 값 설정하기
클래스와 구조체는 인스턴스가 생성되는 시점까지 모든 저장 프로퍼티에 적절한 초기 값을 반드시 세팅해야 해요. 저장 프로퍼티를 '값이 정해지지 않은 불확정 상태'로 그냥 두면 안 되거든요.
저장 프로퍼티의 초기 값을 이니셜라이저 안에서 세팅하거나, 프로퍼티를 정의할 때 **기본 프로퍼티 값(default property value)**을 할당하는 두 가지 방법으로 처리할 수 있어요. 각각 아래 절에서 자세히 볼게요.
참고: 저장 프로퍼티에 기본 값을 할당하거나 이니셜라이저 안에서 값을 세팅하면, 그 값은 프로퍼티 옵저버를 호출하지 않고 직접 할당돼요.
이니셜라이저
이니셜라이저는 특정 타입의 새 인스턴스를 만들기 위해 호출돼요. 가장 단순한 형태는 파라미터가 없는 인스턴스 메서드처럼 생겼는데, init 키워드로 작성해요:
init() {
// perform some initialization here
}
아래 예시는 화씨(Fahrenheit) 눈금의 온도를 저장하는 Fahrenheit라는 구조체를 새로 정의해요. Fahrenheit 구조체는 Double 타입의 저장 프로퍼티 temperature 하나를 가져요:
struct Fahrenheit {
var temperature: Double
init() {
temperature = 32.0
}
}
var f = Fahrenheit()
print("The default temperature is \(f.temperature)° Fahrenheit")
// Prints "The default temperature is 32.0° Fahrenheit".
이 구조체는 파라미터가 없는 이니셜라이저 init을 하나 정의하는데, 저장 프로퍼티 temperature를 32.0(화씨 눈금에서 물의 어는점이에요)으로 초기화해요.
기본 프로퍼티 값
윗부분에서 봤듯이 저장 프로퍼티의 초기 값은 이니셜라이저 안에서 세팅할 수 있어요. 대신 프로퍼티를 선언할 때 기본 프로퍼티 값으로 지정할 수도 있죠. 기본 프로퍼티 값은 프로퍼티를 정의할 때 초기 값을 할당하는 방식으로 지정해요.
참고: 프로퍼티가 항상 같은 초기 값을 가진다면, 이니셜라이저 안에서 값을 세팅하는 대신 기본 값을 제공하는 편이 좋아요. 결과는 같지만 기본 값은 프로퍼티의 초기화를 선언과 더 가깝게 묶어주거든요. 이니셜라이저를 더 짧고 명확하게 만들어 주고, 프로퍼티의 타입을 기본 값에서 추론할 수 있게 해요. 기본 값이 있으면 이 장에서 나중에 설명할 기본 이니셜라이저와 이니셜라이저 상속도 더 쉽게 활용할 수 있어요.
위에서 본 Fahrenheit 구조체는 프로퍼티를 선언하는 지점에서 temperature에 기본 값을 제공하는 더 간단한 형태로 쓸 수 있어요:
struct Fahrenheit {
var temperature = 32.0
}
초기화 커스터마이징하기
초기화 과정은 입력 파라미터와 옵셔널 프로퍼티 타입으로 커스터마이징할 수 있고, 초기화 중에 상수 프로퍼티를 할당하는 방식으로도 바꿀 수 있어요. 아래 절에서 차례로 봐요.
초기화 파라미터
이니셜라이저 정의의 일부로 **초기화 파라미터(initialization parameters)**를 제공할 수 있어요. 초기화 과정을 커스터마이징하는 값의 타입과 이름을 정하는 역할이죠. 초기화 파라미터는 함수나 메서드의 파라미터와 같은 능력과 문법을 가져요.
아래 예시는 섭씨(Celsius) 눈금의 온도를 저장하는 Celsius 구조체를 정의해요. Celsius 구조체는 init(fromFahrenheit:)와 init(fromKelvin:)이라는 두 커스텀 이니셜라이저를 구현하는데, 각각 다른 온도 눈금의 값을 받아 구조체의 새 인스턴스를 초기화해요:
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
}
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
// boilingPointOfWater.temperatureInCelsius is 100.0
let freezingPointOfWater = Celsius(fromKelvin: 273.15)
// freezingPointOfWater.temperatureInCelsius is 0.0
첫 번째 이니셜라이저는 인자 레이블(argument label)이 fromFahrenheit, 파라미터 이름이 fahrenheit인 초기화 파라미터 하나를 가져요. 두 번째 이니셜라이저는 인자 레이블이 fromKelvin, 파라미터 이름이 kelvin인 파라미터 하나를 갖고요. 두 이니셜라이저 모두 자신의 인자 하나를 그에 해당하는 섭씨 값으로 변환한 뒤, temperatureInCelsius라는 프로퍼티에 저장해요.
파라미터 이름과 인자 레이블
함수·메서드 파라미터처럼, 초기화 파라미터도 이니셜라이저 본문 안에서 쓰는 파라미터 이름과 호출할 때 쓰는 인자 레이블을 모두 가질 수 있어요.
하지만 이니셜라이저는 함수나 메서드처럼 괄호 앞에 식별용 함수 이름이 없어요. 그래서 이니셜라이저의 파라미터 이름과 타입은 '어느 이니셜라이저를 호출할지'를 식별하는 데 특히 중요한 역할을 해요. 이런 이유 때문에 Swift는 이니셜라이저의 모든 파라미터에, 직접 제공하지 않으면 자동으로 인자 레이블을 만들어 줘요.
아래 예시는 red, green, blue라는 세 상수 프로퍼티를 가진 Color 구조체를 정의해요. 이 프로퍼티들은 색상 안의 빨강·초록·파랑 양을 나타내는 0.0에서 1.0 사이의 값을 저장해요.
Color는 Double 타입의 파라미터 세 개를 갖는 이니셜라이저를 제공해요. 각각 빨강, 초록, 파랑 성분을 받죠. Color는 또 white 파라미터 하나를 받는 두 번째 이니셜라이저도 제공하는데, 세 색 성분 모두에 같은 값을 주는 용도예요.
struct Color {
let red, green, blue: Double
init(red: Double, green: Double, blue: Double) {
self.red = red
self.green = green
self.blue = blue
}
init(white: Double) {
red = white
green = white
blue = white
}
}
두 이니셜라이저 모두 각 초기화 파라미터에 이름 붙은 값을 제공해 새 Color 인스턴스를 만드는 데 쓸 수 있어요:
let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
let halfGray = Color(white: 0.5)
이니셜라이저를 인자 레이블 없이 호출하는 건 불가능해요. 인자 레이블이 정의돼 있으면 이니셜라이저에서 항상 사용해야 하고, 생략하면 컴파일 타임 오류가 나요:
let veryGreen = Color(0.0, 1.0, 0.0)
// this reports a compile-time error - argument labels are required
인자 레이블이 없는 초기화 파라미터
초기화 파라미터에 인자 레이블을 쓰고 싶지 않다면, 그 파라미터의 명시적 인자 레이블 자리에 밑줄(_)을 써서 기본 동작을 덮어쓰면 돼요.
아래는 위의 doc:Initialization#Initialization-Parameters에서 본 Celsius 예시를 확장한 버전이에요. 이미 섭씨 눈금에 있는 Double 값으로 새 Celsius 인스턴스를 만드는 이니셜라이저를 하나 더 추가했어요:
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
init(_ celsius: Double) {
temperatureInCelsius = celsius
}
}
let bodyTemperature = Celsius(37.0)
// bodyTemperature.temperatureInCelsius is 37.0
Celsius(37.0)이라는 이니셜라이저 호출은 인자 레이블이 없어도 의도가 명확해요. 그래서 이 이니셜라이저를 init(_ celsius: Double)로 작성해 이름 없는 Double 값을 주고 호출할 수 있게 하는 게 적절해요.
옵셔널 프로퍼티 타입
커스텀 타입에 '값이 없다'가 논리적으로 허용되는 저장 프로퍼티가 있다면 — 초기화 중에 값을 세팅할 수 없거나, 이후 어떤 시점에는 '값이 없다'가 허용되는 경우라면 — 프로퍼티를 옵셔널 타입으로 선언해요. 옵셔널 타입의 프로퍼티는 자동으로 nil 값으로 초기화돼요. 초기화 중에 '아직 값이 없다'를 의도적으로 나타내는 신호인 셈이죠.
아래 예시는 옵셔널 String 프로퍼티 response를 가진 SurveyQuestion이라는 클래스를 정의해요:
class SurveyQuestion {
var text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let cheeseQuestion = SurveyQuestion(text: "Do you like cheese?")
cheeseQuestion.ask()
// Prints "Do you like cheese?"
cheeseQuestion.response = "Yes, I do like cheese."
설문 질문에 대한 응답은 실제로 질문하기 전까지 알 수 없어요. 그래서 response 프로퍼티는 String?, 즉 "옵셔널 String" 타입으로 선언해요. 새 SurveyQuestion 인스턴스가 초기화되면 "아직 문자열 없음"을 뜻하는 기본 값 nil이 자동으로 할당되죠.
초기화 중 상수 프로퍼티 할당하기
상수 프로퍼티에는 초기화 과정의 어떤 시점에서든 값을 할당할 수 있어요. 초기화가 끝나는 시점까지 확실한 값이 세팅되기만 하면 되죠. 일단 상수 프로퍼티에 값이 할당되면 그다음부터는 더 이상 수정할 수 없어요.
참고: 클래스 인스턴스의 경우, 상수 프로퍼티는 초기화 중에 그것을 도입한 클래스만 수정할 수 있어요. 서브클래스는 수정할 수 없어요.
위에서 본 SurveyQuestion 예시를, 질문의 text 프로퍼티를 변수 프로퍼티 대신 상수 프로퍼티로 바꿔서 다시 쓸 수 있어요. SurveyQuestion 인스턴스가 만들어진 뒤에는 질문이 바뀌지 않는다는 뜻이죠. text 프로퍼티가 이제 상수여도, 클래스의 이니셜라이저 안에서 값을 세팅하는 건 여전히 가능해요:
class SurveyQuestion {
let text: String
var response: String?
init(text: String) {
self.text = text
}
func ask() {
print(text)
}
}
let beetsQuestion = SurveyQuestion(text: "How about beets?")
beetsQuestion.ask()
// Prints "How about beets?"
beetsQuestion.response = "I also like beets. (But not with cheese.)"
기본 이니셜라이저
Swift는 모든 프로퍼티에 기본 값을 제공하면서 이니셜라이저를 하나도 직접 정의하지 않는 구조체나 클래스에 **기본 이니셜라이저(default initializer)**를 제공해요. 기본 이니셜라이저는 모든 프로퍼티를 기본 값으로 세팅한 새 인스턴스를 그냥 만들어 줘요.
아래 예시는 쇼핑 목록 항목의 이름·수량·구매 여부를 담는 ShoppingListItem이라는 클래스를 정의해요:
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
ShoppingListItem 클래스의 모든 프로퍼티가 기본 값을 갖고 있고, 수퍼클래스가 없는 베이스 클래스이기 때문에, 이 클래스는 모든 프로퍼티를 기본 값으로 세팅한 새 인스턴스를 만드는 기본 이니셜라이저 구현을 자동으로 얻어요. (name 프로퍼티는 옵셔널 String이라 코드에 쓰지 않아도 자동으로 nil 기본 값을 받아요.) 위 예시에서는 이 클래스의 기본 이니셜라이저를 ShoppingListItem()이라는 이니셜라이저 문법으로 호출해 새 인스턴스를 만들고, item이라는 변수에 할당하고 있어요.
구조체 타입의 멤버와이즈 이니셜라이저
구조체 타입은 자신만의 커스텀 이니셜라이저를 하나도 정의하지 않으면 **멤버와이즈 이니셜라이저(memberwise initializer)**를 자동으로 받아요. 기본 이니셜라이저와 달리, 구조체는 기본 값이 없는 저장 프로퍼티를 갖고 있어도 멤버와이즈 이니셜라이저를 받아요.
멤버와이즈 이니셜라이저는 새 구조체 인스턴스의 멤버 프로퍼티를 초기화하는 간단한 방법이에요. 새 인스턴스의 프로퍼티 초기 값들을 이름 붙여서 멤버와이즈 이니셜라이저에 넘길 수 있죠.
아래 예시는 width와 height라는 두 프로퍼티를 가진 Size 구조체를 정의해요. 두 프로퍼티 모두 0.0 기본 값을 할당해 Double 타입으로 추론돼요.
Size 구조체는 init(width:height:) 멤버와이즈 이니셜라이저를 자동으로 받아서, 새 Size 인스턴스를 초기화할 때 쓸 수 있어요:
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
멤버와이즈 이니셜라이저를 호출할 때, 기본 값이 있는 프로퍼티의 값은 생략할 수 있어요. 위 예시에서 Size 구조체는 height와 width 프로퍼티 둘 다 기본 값을 갖고 있죠. 어느 한쪽만, 또는 둘 다 생략할 수 있고, 이니셜라이저는 생략된 부분에 기본 값을 사용해요. 가령:
let zeroByTwo = Size(height: 2.0)
print(zeroByTwo.width, zeroByTwo.height)
// Prints "0.0 2.0".
let zeroByZero = Size()
print(zeroByZero.width, zeroByZero.height)
// Prints "0.0 0.0".
값 타입의 이니셜라이저 위임
이니셜라이저는 다른 이니셜라이저를 호출해서 인스턴스 초기화의 일부를 수행할 수 있어요. 이 과정을 **이니셜라이저 위임(initializer delegation)**이라고 하고, 여러 이니셜라이저 사이에서 코드가 중복되는 걸 막아줘요.
이니셜라이저 위임이 어떻게 동작하는지, 어떤 형태의 위임이 허용되는지는 값 타입과 클래스 타입이 서로 달라요. 값 타입(구조체·열거형)은 상속을 지원하지 않기 때문에 초기화 위임 과정이 비교적 단순해요. 자기 자신이 제공한 다른 이니셜라이저에게만 위임할 수 있거든요. 반면 클래스는 doc:Inheritance에서 설명하듯 다른 클래스로부터 상속받을 수 있어요. 이 말은 클래스가 초기화 중에 상속받은 모든 저장 프로퍼티에 적절한 값을 할당해야 한다는 추가 책임을 진다는 뜻이에요. 이 책임들은 아래 doc:Initialization#Class-Inheritance-and-Initialization에서 다룰게요.
값 타입의 경우, 커스텀 이니셜라이저를 쓸 때 같은 값 타입의 다른 이니셜라이저를 가리키는 데 self.init을 사용해요. self.init은 이니셜라이저 안에서만 호출할 수 있어요.
값 타입에 커스텀 이니셜라이저를 정의하면, 그 타입의 기본 이니셜라이저(구조체라면 멤버와이즈 이니셜라이저)에는 더 이상 접근할 수 없다는 점을 알아두세요. 이 제약은 '더 복잡한 이니셜라이저가 제공하는 필수 설정을, 누군가 자동 이니셜라이저 중 하나를 써서 실수로 우회하는' 상황을 막아줘요.
참고: 커스텀 값 타입을 기본 이니셜라이저·멤버와이즈 이니셜라이저로도, 자기 커스텀 이니셜라이저로도 초기화할 수 있게 하고 싶다면, 커스텀 이니셜라이저를 값 타입의 원래 구현의 일부로 쓰지 말고 확장(extension)에 작성하세요. 자세한 내용은 doc:Extensions를 봐요.
아래 예시는 기하 도형인 사각형을 나타내는 커스텀 Rect 구조체를 정의해요. 이 예시는 Size와 Point라는 두 보조 구조체가 필요한데, 둘 다 모든 프로퍼티에 0.0 기본 값을 제공해요:
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
아래의 Rect 구조체는 세 가지 방법 중 하나로 초기화할 수 있어요. 기본으로 0으로 초기화된 origin과 size 프로퍼티 값을 쓰거나, 특정 origin 점과 size를 주거나, 특정 center 점과 size를 주는 방법이죠. 이 초기화 옵션들은 Rect 구조체 정의에 포함된 세 커스텀 이니셜라이저로 표현돼요:
struct Rect {
var origin = Point()
var size = Size()
init() {}
init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
첫 번째 Rect 이니셜라이저 init()은 기능적으로, 구조체가 자기 커스텀 이니셜라이저를 갖지 않았다면 받았을 기본 이니셜라이저와 같아요. 이 이니셜라이저는 빈 중괄호 쌍 {}로 표현된 빈 본문을 가져요. 이 이니셜라이저를 호출하면 origin과 size 프로퍼티가 프로퍼티 정의의 기본 값 Point(x: 0.0, y: 0.0)과 Size(width: 0.0, height: 0.0)으로 초기화된 Rect 인스턴스를 돌려줘요:
let basicRect = Rect()
// basicRect's origin is (0.0, 0.0) and its size is (0.0, 0.0)
두 번째 Rect 이니셜라이저 init(origin:size:)은 기능적으로, 구조체가 자기 커스텀 이니셜라이저를 갖지 않았다면 받았을 멤버와이즈 이니셜라이저와 같아요. 이 이니셜라이저는 origin과 size 인자 값을 그냥 해당 저장 프로퍼티에 할당해요:
let originRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
// originRect's origin is (2.0, 2.0) and its size is (5.0, 5.0)
세 번째 Rect 이니셜라이저 init(center:size:)은 조금 더 복잡해요. center 점과 size 값을 바탕으로 적절한 origin 점을 계산하는 것으로 시작해요. 그런 다음 init(origin:size:) 이니셜라이저를 호출(즉, 위임)해서 새 origin과 size 값을 해당 프로퍼티에 저장하게 하죠:
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)
init(center:size:) 이니셜라이저는 새 origin과 size 값을 직접 해당 프로퍼티에 할당할 수도 있었어요. 하지만 정확히 그 기능을 이미 제공하는 기존 이니셜라이저를 활용하는 편이 더 편하고, 의도도 더 명확해요.
참고:
init()과init(origin:size:)이니셜라이저를 직접 정의하지 않고 이 예시를 쓰는 대안이 있는데, doc:Extensions에서 볼 수 있어요.
클래스 상속과 초기화
클래스의 모든 저장 프로퍼티 — 수퍼클래스로부터 상속받은 프로퍼티까지 포함해서 — 는 초기화 중에 반드시 초기 값을 할당받아야 해요.
Swift는 모든 저장 프로퍼티가 초기 값을 받도록 보장하기 위해 클래스 타입에 두 종류의 이니셜라이저를 정의해요. 바로 **지정 이니셜라이저(designated initializer)**와 **편의 이니셜라이저(convenience initializer)**예요.
지정 이니셜라이저와 편의 이니셜라이저
지정 이니셜라이저는 클래스의 주요 이니셜라이저예요. 지정 이니셜라이저는 해당 클래스가 도입한 모든 프로퍼티를 완전히 초기화하고, 적절한 수퍼클래스 이니셜라이저를 호출해 초기화 과정을 수퍼클래스 체인 위쪽으로 계속 진행시켜요.
클래스는 지정 이니셜라이저를 아주 적게 갖는 경향이 있고, 딱 하나만 갖는 것도 아주 흔해요. 지정 이니셜라이저는 초기화가 일어나는 '깔때기(funnel)' 지점이면서, 초기화 과정이 수퍼클래스 체인 위로 계속 이어지는 통로예요.
모든 클래스는 지정 이니셜라이저를 최소 하나 가져야 해요. 어떤 경우에는 이 요구사항이 수퍼클래스로부터 지정 이니셜라이저를 하나 이상 상속받는 방식으로 충족되기도 하는데, 아래 doc:Initialization#Automatic-Initializer-Inheritance에서 설명할게요.
편의 이니셜라이저는 클래스의 보조적인, 지원 역할을 하는 이니셜라이저예요. 편의 이니셜라이저를 정의하면 같은 클래스의 지정 이니셜라이저를, 지정 이니셜라이저 파라미터 중 일부에 기본 값을 세팅해서 호출할 수 있어요. 특정 사용 사례나 입력 값 타입에 맞춰 그 클래스의 인스턴스를 만드는 편의 이니셜라이저를 정의할 수도 있죠.
클래스에 편의 이니셜라이저가 필요 없다면 제공하지 않아도 돼요. 공통 초기화 패턴에 대한 지름길(shortcut)이 시간을 아껴 주거나 클래스 초기화의 의도를 더 명확히 해줄 때마다 편의 이니셜라이저를 만들면 돼요.
지정·편의 이니셜라이저 문법
클래스의 지정 이니셜라이저는 값 타입의 단순 이니셜라이저와 같은 방식으로 작성해요:
init(<#parameters#>) {
<#statements#>
}
편의 이니셜라이저도 같은 스타일이지만, init 키워드 앞에 convenience 수식어를 공백으로 구분해 붙여요:
convenience init(<#parameters#>) {
<#statements#>
}
클래스 타입의 이니셜라이저 위임
지정 이니셜라이저와 편의 이니셜라이저 사이의 관계를 단순화하기 위해, Swift는 이니셜라이저 사이의 위임 호출에 다음 세 가지 규칙을 적용해요:
-
규칙 1: 지정 이니셜라이저는 바로 위 수퍼클래스의 지정 이니셜라이저를 호출해야 해요.
-
규칙 2: 편의 이니셜라이저는 같은 클래스의 다른 이니셜라이저를 호출해야 해요.
-
규칙 3: 편의 이니셜라이저는 궁극적으로 지정 이니셜라이저를 호출해야 해요.
이걸 쉽게 기억하는 방법이 있어요:
- 지정 이니셜라이저는 항상 위로(up) 위임해요.
- 편의 이니셜라이저는 항상 가로질러(across) 위임해요.
이 규칙들은 아래 그림으로 설명돼요:
여기서 수퍼클래스는 지정 이니셜라이저 하나와 편의 이니셜라이저 둘을 가져요. 편의 이니셜라이저 하나가 다른 편의 이니셜라이저를 호출하고, 그게 다시 단 하나뿐인 지정 이니셜라이저를 호출해요. 이는 위의 규칙 2와 규칙 3을 만족해요. 수퍼클래스 자신은 더 위의 수퍼클래스가 없으므로 규칙 1은 적용되지 않아요.
이 그림의 서브클래스는 지정 이니셜라이저 둘과 편의 이니셜라이저 하나를 가져요. 편의 이니셜라이저는 같은 클래스의 다른 이니셜라이저만 호출할 수 있으므로, 두 지정 이니셜라이저 중 하나를 호출해야 해요. 이는 규칙 2와 규칙 3을 만족해요. 두 지정 이니셜라이저는 모두 수퍼클래스의 단 하나뿐인 지정 이니셜라이저를 호출해서 규칙 1을 만족해야 해요.
참고: 이 규칙들은 사용자가 각 클래스의 인스턴스를 만드는 방식에는 영향을 주지 않아요. 위 그림의 어떤 이니셜라이저든, 소속 클래스의 완전히 초기화된 인스턴스를 만드는 데 쓸 수 있어요. 이 규칙들은 클래스 이니셜라이저의 구현을 어떻게 작성하는지에만 영향을 미쳐요.
아래 그림은 네 클래스로 이루어진 더 복잡한 클래스 계층을 보여줘요. 이 계층에서 지정 이니셜라이저들이 클래스 초기화의 '깔때기' 지점으로 작용해, 체인에 있는 클래스들 사이의 상호관계를 단순화하는 방식을 보여주죠:
2단계 초기화
Swift의 클래스 초기화는 2단계(phase) 과정이에요. 첫 번째 단계에서 각 저장 프로퍼티는 그 프로퍼티를 도입한 클래스에 의해 초기 값이 할당돼요. 모든 저장 프로퍼티의 초기 상태가 정해지면 두 번째 단계가 시작되고, 각 클래스는 새 인스턴스가 사용 준비가 된 것으로 간주되기 전에 자신의 저장 프로퍼티를 더 커스터마이징할 기회를 받아요.
2단계 초기화 과정을 쓰면 초기화가 안전해지면서도, 클래스 계층의 각 클래스에게 완전한 유연성을 부여해요. 2단계 초기화는 프로퍼티 값이 초기화되기 전에 접근되는 것을 막고, 프로퍼티 값이 다른 이니셜라이저에 의해 뜻밖에 다른 값으로 세팅되는 것도 막아줘요.
참고: Swift의 2단계 초기화 과정은 Objective-C의 초기화와 비슷해요. 가장 큰 차이는 1단계 동안 Objective-C는 모든 프로퍼티에 0이나 null 값(예:
0,nil)을 할당한다는 점이에요. Swift의 초기화 흐름은 커스텀 초기 값을 설정할 수 있고,0이나nil이 유효한 기본 값이 아닌 타입도 처리할 수 있다는 점에서 더 유연해요.
Swift의 컴파일러는 2단계 초기화가 오류 없이 완료되도록 네 가지 유용한 안전 검사를 수행해요:
- 안전 검사 1: 지정 이니셜라이저는 수퍼클래스 이니셜라이저에 위임하기 전에, 자기 클래스가 도입한 모든 프로퍼티가 초기화되도록 보장해야 해요.
위에서 언급했듯이, 객체의 메모리는 모든 저장 프로퍼티의 초기 상태가 알려진 뒤에야 완전히 초기화된 것으로 간주돼요. 이 규칙을 만족하려면 지정 이니셜라이저가 체인 위로 넘겨주기 전에 자기 프로퍼티를 전부 초기화해야 해요.
-
안전 검사 2: 지정 이니셜라이저는 상속받은 프로퍼티에 값을 할당하기 전에 수퍼클래스 이니셜라이저로 위임해야 해요. 그렇게 하지 않으면 지정 이니셜라이저가 할당한 새 값이 수퍼클래스 자기 초기화 과정에서 덮어써져 버려요.
-
안전 검사 3: 편의 이니셜라이저는 어떤 프로퍼티(자기 클래스가 정의한 프로퍼티 포함)에든 값을 할당하기 전에 다른 이니셜라이저로 위임해야 해요. 그렇게 하지 않으면 편의 이니셜라이저가 할당한 새 값이 자기 클래스의 지정 이니셜라이저에 의해 덮어써져요.
-
안전 검사 4: 이니셜라이저는 초기화의 첫 번째 단계가 완료될 때까지 인스턴스 메서드를 호출하거나, 인스턴스 프로퍼티의 값을 읽거나,
self를 값으로 참조할 수 없어요.
클래스 인스턴스는 첫 번째 단계가 끝날 때까지 완전히 유효하지 않아요. 프로퍼티에 접근하고 메서드를 호출하는 건 첫 번째 단계 끝에 클래스 인스턴스가 유효하다고 알려진 뒤에만 가능해요.
위 네 가지 안전 검사를 바탕으로 2단계 초기화가 어떻게 진행되는지 볼게요:
1단계
- 클래스에서 지정 또는 편의 이니셜라이저가 호출돼요.
- 그 클래스의 새 인스턴스를 위한 메모리가 할당돼요. 이 메모리는 아직 초기화되지 않은 상태예요.
- 그 클래스의 지정 이니셜라이저가, 그 클래스가 도입한 모든 저장 프로퍼티가 값을 갖는지 확인해요. 이 저장 프로퍼티들의 메모리는 이제 초기화된 상태예요.
- 지정 이니셜라이저는 수퍼클래스 이니셜라이저에게 자신의 저장 프로퍼티에 대해 같은 작업을 하도록 넘겨줘요.
- 이 과정이 클래스 상속 체인을 따라 맨 위에 도달할 때까지 계속돼요.
- 체인 맨 위에 도달해 체인의 마지막 클래스가 자기 저장 프로퍼티 전부에 값이 있음을 보장하면, 인스턴스의 메모리는 완전히 초기화된 것으로 간주되고 1단계가 완료돼요.
2단계
- 체인 맨 위에서부터 역방향으로 내려가며, 체인에 있는 각 지정 이니셜라이저는 인스턴스를 더 커스터마이징할 선택권을 가져요. 이제 이니셜라이저는
self에 접근할 수 있고, 프로퍼티를 수정하고 인스턴스 메서드를 호출하는 등의 일을 할 수 있어요. - 마지막으로, 체인에 있는 편의 이니셜라이저들은 인스턴스를 커스터마이징하고
self로 작업할 선택권을 가져요.
가상의 서브클래스·수퍼클래스에 대한 초기화 호출에서 1단계가 어떻게 생겼는지 보여줄게요:
이 예시에서 초기화는 서브클래스의 편의 이니셜라이저 호출로 시작해요. 이 편의 이니셜라이저는 아직 어떤 프로퍼티도 수정할 수 없어요. 같은 클래스의 지정 이니셜라이저로 가로질러 위임하죠.
지정 이니셜라이저는 안전 검사 1에 따라 서브클래스의 모든 프로퍼티에 값이 있는지 확인해요. 그다음 수퍼클래스의 지정 이니셜라이저를 호출해 초기화를 체인 위로 계속 진행해요.
수퍼클래스의 지정 이니셜라이저는 수퍼클래스의 모든 프로퍼티에 값이 있는지 확인해요. 더 초기화할 수퍼클래스가 없으므로 추가 위임은 필요 없어요.
수퍼클래스의 모든 프로퍼티가 초기 값을 갖는 즉시 그 메모리는 완전히 초기화된 것으로 간주되고, 1단계가 완료돼요.
같은 초기화 호출에서 2단계가 어떻게 생겼는지 보여줄게요:
수퍼클래스의 지정 이니셜라이저는 이제 인스턴스를 더 커스터마이징할 기회를 가져요(꼭 그럴 필요는 없어요).
수퍼클래스의 지정 이니셜라이저가 끝나면, 서브클래스의 지정 이니셜라이저가 추가 커스터마이징을 수행할 수 있어요(이것도 꼭 그럴 필요는 없어요).
마지막으로, 서브클래스의 지정 이니셜라이저가 끝나면, 처음에 호출됐던 편의 이니셜라이저가 추가 커스터마이징을 수행할 수 있어요.
이니셜라이저 상속과 오버라이딩
Objective-C의 서브클래스와 달리, Swift의 서브클래스는 기본적으로 수퍼클래스의 이니셜라이저를 상속받지 않아요. Swift의 이런 방식은 '수퍼클래스의 단순한 이니셜라이저가 더 특화된 서브클래스에 상속되어, 완전하거나 올바르게 초기화되지 않은 서브클래스 인스턴스를 만드는 데 사용되는' 상황을 막아줘요.
참고: 수퍼클래스 이니셜라이저는 특정 상황에서는 상속되지만, 안전하고 적절한 때에만 그래요. 자세한 내용은 아래 doc:Initialization#Automatic-Initializer-Inheritance를 봐요.
커스텀 서브클래스가 수퍼클래스와 같은 이니셜라이저를 하나 이상 노출하고 싶다면, 서브클래스 안에 그 이니셜라이저들의 커스텀 구현을 제공하면 돼요.
수퍼클래스 지정 이니셜라이저와 일치하는 서브클래스 이니셜라이저를 작성하면, 사실상 그 지정 이니셜라이저의 오버라이드를 제공하는 거예요. 따라서 서브클래스 이니셜라이저 정의 앞에 override 수식어를 반드시 써야 해요. 자동으로 제공되는 기본 이니셜라이저를 오버라이드하는 경우에도 마찬가지예요(doc:Initialization#Default-Initializers 참고).
오버라이드된 프로퍼티·메서드·서브스크립트와 마찬가지로, override 수식어가 있으면 Swift가 수퍼클래스에 일치하는 지정 이니셜라이저가 있는지 확인하고, 오버라이드하는 이니셜라이저의 파라미터가 의도대로 지정됐는지 검증해요.
참고: 수퍼클래스 지정 이니셜라이저를 오버라이드할 때는 항상
override수식어를 써요. 서브클래스의 이니셜라이저 구현이 편의 이니셜라이저여도 마찬가지예요.
반대로, 수퍼클래스 편의 이니셜라이저와 일치하는 서브클래스 이니셜라이저를 작성하면, doc:Initialization#Initializer-Delegation-for-Class-Types의 규칙에 따라 그 수퍼클래스 편의 이니셜라이저는 서브클래스에 의해 직접 호출될 수 없어요. 그래서 서브클래스는 (엄밀히 말하면) 수퍼클래스 이니셜라이저의 오버라이드를 제공하는 게 아니에요. 결과적으로, 수퍼클래스 편의 이니셜라이저와 일치하는 구현을 제공할 때는 override 수식어를 쓰지 않아요.
아래 예시는 Vehicle이라는 베이스 클래스를 정의해요. 이 베이스 클래스는 기본 Int 값 0을 가진 numberOfWheels이라는 저장 프로퍼티를 선언해요. numberOfWheels 프로퍼티는 description이라는 계산 프로퍼티에서 사용되어 탈것의 특성을 설명하는 String을 만들어요:
class Vehicle {
var numberOfWheels = 0
var description: String {
return "\(numberOfWheels) wheel(s)"
}
}
Vehicle 클래스는 유일한 저장 프로퍼티에 기본 값을 제공하고, 자기 자신의 커스텀 이니셜라이저는 제공하지 않아요. 그 결과 doc:Initialization#Default-Initializers에서 설명한 대로 기본 이니셜라이저를 자동으로 받아요. 기본 이니셜라이저(사용 가능할 때)는 항상 클래스의 지정 이니셜라이저이며, numberOfWheels가 0인 새 Vehicle 인스턴스를 만드는 데 쓸 수 있어요:
let vehicle = Vehicle()
print("Vehicle: \(vehicle.description)")
// Vehicle: 0 wheel(s)
다음 예시는 Vehicle의 서브클래스인 Bicycle을 정의해요:
class Bicycle: Vehicle {
override init() {
super.init()
numberOfWheels = 2
}
}
Bicycle 서브클래스는 커스텀 지정 이니셜라이저 init()을 정의해요. 이 지정 이니셜라이저는 Bicycle의 수퍼클래스인 Vehicle의 지정 이니셜라이저와 일치하므로, Bicycle 버전의 이니셜라이저는 override 수식어로 표시돼요.
Bicycle의 init() 이니셜라이저는 super.init() 호출로 시작해요. 이 호출은 Bicycle 클래스의 수퍼클래스인 Vehicle의 기본 이니셜라이저를 호출하죠. 이렇게 하면 Bicycle이 프로퍼티를 수정할 기회를 갖기 전에 상속받은 numberOfWheels 프로퍼티가 Vehicle에 의해 초기화되도록 보장돼요. super.init() 호출 후에는 numberOfWheels의 원래 값이 새 값 2로 교체돼요.
Bicycle 인스턴스를 만들면, 상속받은 description 계산 프로퍼티를 호출해 numberOfWheels 프로퍼티가 어떻게 갱신됐는지 볼 수 있어요:
let bicycle = Bicycle()
print("Bicycle: \(bicycle.description)")
// Bicycle: 2 wheel(s)
서브클래스 이니셜라이저가 초기화 과정의 2단계에서 아무 커스터마이징도 수행하지 않고, 수퍼클래스가 동기적(synchronous), 인자 없는 지정 이니셜라이저를 갖고 있다면, 서브클래스의 모든 저장 프로퍼티에 값을 할당한 뒤 super.init() 호출을 생략할 수 있어요. 수퍼클래스의 이니셜라이저가 비동기(asynchronous)라면 명시적으로 await super.init()을 써야 해요.
이 예시는 Vehicle의 또 다른 서브클래스인 Hoverboard를 정의해요. Hoverboard 클래스는 이니셜라이저에서 color 프로퍼티만 세팅해요. 명시적으로 super.init()을 호출하는 대신, 이 이니셜라이저는 수퍼클래스 이니셜라이저에 대한 암시적 호출에 의존해 초기화를 완료해요.
class Hoverboard: Vehicle {
var color: String
init(color: String) {
self.color = color
// super.init() implicitly called here
}
override var description: String {
return "\(super.description) in a beautiful \(color)"
}
}
Hoverboard 인스턴스는 Vehicle 이니셜라이저가 제공하는 기본 바퀴 수를 사용해요.
let hoverboard = Hoverboard(color: "silver")
print("Hoverboard: \(hoverboard.description)")
// Hoverboard: 0 wheel(s) in a beautiful silver
참고: 서브클래스는 초기화 중에 상속받은 변수 프로퍼티를 수정할 수 있지만, 상속받은 상수 프로퍼티는 수정할 수 없어요.
자동 이니셜라이저 상속
위에서 언급했듯이 서브클래스는 기본적으로 수퍼클래스의 이니셜라이저를 상속받지 않아요. 하지만 특정 조건이 충족되면 수퍼클래스 이니셜라이저가 자동으로 상속되기도 해요. 실제로 이 말은 많은 흔한 상황에서 이니셜라이저 오버라이드를 작성할 필요가 없고, 안전할 때마다 최소한의 노력으로 수퍼클래스 이니셜라이저를 상속받을 수 있다는 뜻이에요.
서브클래스에서 도입하는 새 프로퍼티들에 기본 값을 제공한다고 가정하면, 다음 두 규칙이 적용돼요:
-
규칙 1: 서브클래스가 지정 이니셜라이저를 하나도 정의하지 않으면, 수퍼클래스의 지정 이니셜라이저를 전부 자동으로 상속해요.
-
규칙 2: 서브클래스가 수퍼클래스의 지정 이니셜라이저 전부에 구현을 제공하면 — 규칙 1에 따라 상속받거나, 정의의 일부로 커스텀 구현을 제공해서 — 수퍼클래스의 편의 이니셜라이저를 전부 자동으로 상속해요.
이 규칙들은 서브클래스가 편의 이니셜라이저를 추가로 더하더라도 적용돼요.
참고: 서브클래스는 규칙 2를 충족하는 방법으로 수퍼클래스 지정 이니셜라이저를 서브클래스 편의 이니셜라이저로 구현할 수 있어요.
지정·편의 이니셜라이저 실제로 쓰기
아래 예시는 지정 이니셜라이저, 편의 이니셜라이저, 자동 이니셜라이저 상속이 실제로 어떻게 쓰이는지 보여줘요. 이 예시는 Food, RecipeIngredient, ShoppingListItem이라는 세 클래스로 이루어진 계층을 정의하고, 이들의 이니셜라이저가 어떻게 상호작용하는지 보여줘요.
계층의 베이스 클래스는 Food예요. 식품의 이름을 담는 단순한 클래스죠. Food 클래스는 name이라는 String 프로퍼티 하나를 도입하고, Food 인스턴스를 만드는 두 이니셜라이저를 제공해요:
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
아래 그림은 Food 클래스의 이니셜라이저 체인을 보여줘요:
클래스에는 기본 멤버와이즈 이니셜라이저가 없어요. 그래서 Food 클래스는 name이라는 인자 하나를 받는 지정 이니셜라이저를 제공해요. 이 이니셜라이저로 특정 이름을 가진 새 Food 인스턴스를 만들 수 있어요:
let namedMeat = Food(name: "Bacon")
// namedMeat's name is "Bacon"
Food 클래스의 init(name: String) 이니셜라이저는 지정 이니셜라이저로 제공돼요. 새 Food 인스턴스의 모든 저장 프로퍼티가 완전히 초기화되도록 보장하니까요. Food 클래스에는 수퍼클래스가 없어서, init(name: String) 이니셜라이저는 초기화를 완료하기 위해 super.init()을 호출할 필요가 없어요.
Food 클래스는 인자 없는 편의 이니셜라이저 init()도 제공해요. init() 이니셜라이저는 이름 [Unnamed]을 넣어 Food 클래스의 init(name: String)으로 가로질러 위임함으로써, 새 식품에 기본 자리표시자 이름을 제공해요:
let mysteryMeat = Food()
// mysteryMeat's name is "[Unnamed]"
계층의 두 번째 클래스는 Food의 서브클래스인 RecipeIngredient예요. RecipeIngredient 클래스는 요리 레시피의 재료를 모델링해요. Food에서 상속받은 name 프로퍼티에 더해 Int 프로퍼티 quantity를 도입하고, RecipeIngredient 인스턴스를 만드는 두 이니셜라이저를 정의해요:
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
override convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}
아래 그림은 RecipeIngredient 클래스의 이니셜라이저 체인을 보여줘요:
RecipeIngredient 클래스는 지정 이니셜라이저 init(name: String, quantity: Int) 하나를 가져요. 새 RecipeIngredient 인스턴스의 모든 프로퍼티를 채우는 데 쓸 수 있죠. 이 이니셜라이저는 RecipeIngredient가 도입한 유일한 새 프로퍼티인 quantity 프로퍼티에 넘겨받은 quantity 인자를 할당하는 것으로 시작해요. 그런 다음 Food 클래스의 init(name: String) 이니셜라이저로 위로 위임해요. 이 과정은 위 doc:Initialization#Two-Phase-Initialization의 안전 검사 1을 만족해요.
RecipeIngredient는 또 편의 이니셜라이저 init(name: String)을 정의해요. 이름만으로 RecipeIngredient 인스턴스를 만드는 데 쓰이죠. 이 편의 이니셜라이저는 명시적 수량 없이 만들어지는 RecipeIngredient 인스턴스에 대해 수량 1을 가정해요. 이 편의 이니셜라이저의 정의 덕에 RecipeIngredient 인스턴스를 더 빠르고 편리하게 만들 수 있고, 수량이 1인 RecipeIngredient 인스턴스를 여러 개 만들 때 코드 중복도 피할 수 있어요. 이 편의 이니셜라이저는 quantity 값 1을 넘겨 클래스의 지정 이니셜라이저로 가로질러 위임하기만 해요.
RecipeIngredient가 제공하는 init(name: String) 편의 이니셜라이저는 Food의 지정 이니셜라이저 init(name: String)과 같은 파라미터를 가져요. 이 편의 이니셜라이저가 수퍼클래스의 지정 이니셜라이저를 오버라이드하기 때문에, doc:Initialization#Initializer-Inheritance-and-Overriding에서 설명한 대로 override 수식어로 표시해야 해요.
RecipeIngredient는 init(name: String) 이니셜라이저를 편의 이니셜라이저로 제공하면서도, 수퍼클래스의 지정 이니셜라이저 전부에 구현을 제공했어요. 따라서 RecipeIngredient는 수퍼클래스의 편의 이니셜라이저도 전부 자동으로 상속해요.
이 예시에서 RecipeIngredient의 수퍼클래스는 Food이고, 이 클래스는 init()이라는 편의 이니셜라이저 하나를 가져요. 그래서 RecipeIngredient가 이 이니셜라이저를 상속해요. 상속된 init() 버전은 Food 버전과 정확히 같은 방식으로 동작하는데, 다만 Food 버전의 init(name: String)이 아니라 RecipeIngredient 버전의 init(name: String)으로 위임한다는 점만 달라요.
이 세 이니셜라이저 모두 새 RecipeIngredient 인스턴스를 만드는 데 쓸 수 있어요:
let oneMysteryItem = RecipeIngredient()
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)
계층의 세 번째이자 마지막 클래스는 RecipeIngredient의 서브클래스인 ShoppingListItem이에요. ShoppingListItem 클래스는 쇼핑 목록에 나타나는 모습의 레시피 재료를 모델링해요.
쇼핑 목록의 모든 항목은 "구매 안 함" 상태로 시작해요. 이 사실을 나타내기 위해 ShoppingListItem은 기본 값 false를 가진 Boolean 프로퍼티 purchased를 도입해요. ShoppingListItem은 또 ShoppingListItem 인스턴스에 대한 텍스트 설명을 제공하는 계산 프로퍼티 description도 추가해요:
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ✔" : " ✘"
return output
}
}
참고:
ShoppingListItem은purchased의 초기 값을 제공하는 이니셜라이저를 정의하지 않아요. 쇼핑 목록의 항목이(여기서 모델링한 것처럼) 항상 구매 안 함 상태로 시작하기 때문이에요.
도입한 모든 프로퍼티에 기본 값을 제공하고 이니셜라이저를 직접 정의하지 않기 때문에, ShoppingListItem은 수퍼클래스의 지정·편의 이니셜라이저를 모두 자동으로 상속해요.
아래 그림은 세 클래스 전체의 이니셜라이저 체인을 보여줘요:
상속받은 세 이니셜라이저를 모두 써서 새 ShoppingListItem 인스턴스를 만들 수 있어요:
var breakfastList = [
ShoppingListItem(),
ShoppingListItem(name: "Bacon"),
ShoppingListItem(name: "Eggs", quantity: 6),
]
breakfastList[0].name = "Orange juice"
breakfastList[0].purchased = true
for item in breakfastList {
print(item.description)
}
// 1 x Orange juice ✔
// 1 x Bacon ✘
// 6 x Eggs ✘
여기서 breakfastList라는 새 배열이, 세 개의 새 ShoppingListItem 인스턴스를 담은 배열 리터럴로 만들어져요. 배열의 타입은 [ShoppingListItem]으로 추론되죠. 배열이 만들어진 뒤에는 배열 맨 앞 ShoppingListItem의 이름이 "[Unnamed]"에서 "Orange juice"로 바뀌고, 구매됨으로 표시돼요. 배열에 있는 각 항목의 description을 출력하면 기본 상태가 기대한 대로 설정됐음을 보여줘요.
실패 가능한 이니셜라이저
초기화가 실패할 수 있는 클래스, 구조체, 열거형을 정의해야 할 때가 있어요. 이 실패는 잘못된 초기화 파라미터 값, 필요한 외부 리소스의 부재, 또는 초기화 성공을 막는 다른 어떤 조건에 의해 촉발될 수 있어요.
실패할 수 있는 초기화 조건을 다루기 위해, 클래스·구조체·열거형 정의의 일부로 **실패 가능한 이니셜라이저(failable initializer)**를 하나 이상 정의해요. 실패 가능한 이니셜라이저는 init 키워드 뒤에 물음표를 붙여(init?) 작성해요.
참고: 같은 파라미터 타입과 이름을 가진, 실패 가능한 이니셜라이저와 실패 가능하지 않은 이니셜라이저를 둘 다 정의할 수는 없어요.
실패 가능한 이니셜라이저는 초기화하는 타입의 옵셔널 값을 만들어요. 실패 가능한 이니셜라이저 안에 return nil을 작성해 초기화 실패가 촉발될 수 있는 지점을 표시해요.
참고: 엄밀히 말하면 이니셜라이저는 값을 반환하지 않아요. 이니셜라이저의 역할은 초기화가 끝나는 시점까지
self가 완전하고 올바르게 초기화되도록 보장하는 거예요. 초기화 실패를 촉발하려고return nil을 쓰긴 하지만, 초기화 성공을 나타내는 데return키워드를 쓰지는 않아요.
가령 숫자 타입 변환에 실패 가능한 이니셜라이저가 구현돼 있어요. 숫자 타입 간의 변환이 값을 정확히 유지하도록 보장하려면 init(exactly:) 이니셜라이저를 사용해요. 타입 변환이 값을 유지할 수 없으면 이니셜라이저가 실패해요.
let wholeNumber: Double = 12345.0
let pi = 3.14159
if let valueMaintained = Int(exactly: wholeNumber) {
print("\(wholeNumber) conversion to Int maintains value of \(valueMaintained)")
}
// Prints "12345.0 conversion to Int maintains value of 12345".
let valueChanged = Int(exactly: pi)
// valueChanged is of type Int?, not Int
if valueChanged == nil {
print("\(pi) conversion to Int doesn't maintain value")
}
// Prints "3.14159 conversion to Int doesn't maintain value".
아래 예시는 상수 String 프로퍼티 species를 가진 Animal 구조체를 정의해요. Animal 구조체는 species라는 파라미터 하나를 받는 실패 가능한 이니셜라이저도 정의해요. 이 이니셜라이저는 넘겨받은 species 값이 빈 문자열인지 확인해요. 빈 문자열이면 초기화 실패가 촉발되고, 그렇지 않으면 species 프로퍼티의 값이 세팅되며 초기화가 성공해요:
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
이 실패 가능한 이니셜라이저로 새 Animal 인스턴스 초기화를 시도하고, 초기화가 성공했는지 확인할 수 있어요:
let someCreature = Animal(species: "Giraffe")
// someCreature is of type Animal?, not Animal
if let giraffe = someCreature {
print("An animal was initialized with a species of \(giraffe.species)")
}
// Prints "An animal was initialized with a species of Giraffe".
실패 가능한 이니셜라이저의 species 파라미터에 빈 문자열 값을 넘기면 이니셜라이저가 초기화 실패를 촉발해요:
let anonymousCreature = Animal(species: "")
// anonymousCreature is of type Animal?, not Animal
if anonymousCreature == nil {
print("The anonymous creature couldn't be initialized")
}
// Prints "The anonymous creature couldn't be initialized".
참고: 빈 문자열 값(예:
"Giraffe"가 아닌"")을 확인하는 것은, 옵셔널String값의 부재를 나타내는nil을 확인하는 것과는 달라요. 위 예시에서 빈 문자열("")은 유효한, 옵셔널이 아닌String이에요. 하지만 동물의species프로퍼티 값으로 빈 문자열을 갖는 건 적절하지 않죠. 이 제약을 모델링하기 위해, 실패 가능한 이니셜라이저는 빈 문자열이 발견되면 초기화 실패를 촉발해요.
열거형의 실패 가능한 이니셜라이저
실패 가능한 이니셜라이저를 사용해 하나 이상의 파라미터를 바탕으로 적절한 열거형 케이스를 선택할 수 있어요. 제공된 파라미터가 적절한 열거형 케이스와 일치하지 않으면 이니셜라이저가 실패할 수 있죠.
아래 예시는 세 가지 가능한 상태(kelvin, celsius, fahrenheit)를 가진 TemperatureUnit 열거형을 정의해요. 실패 가능한 이니셜라이저는 온도 기호를 나타내는 Character 값에 대한 적절한 열거형 케이스를 찾는 데 사용돼요:
enum TemperatureUnit {
case kelvin, celsius, fahrenheit
init?(symbol: Character) {
switch symbol {
case "K":
self = .kelvin
case "C":
self = .celsius
case "F":
self = .fahrenheit
default:
return nil
}
}
}
이 실패 가능한 이니셜라이저로 세 가지 가능한 상태에 대한 적절한 열거형 케이스를 고르고, 파라미터가 그중 하나와 일치하지 않으면 초기화가 실패하도록 할 수 있어요:
let fahrenheitUnit = TemperatureUnit(symbol: "F")
if fahrenheitUnit != nil {
print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."
let unknownUnit = TemperatureUnit(symbol: "X")
if unknownUnit == nil {
print("This isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."
원시 값이 있는 열거형의 실패 가능한 이니셜라이저
원시 값이 있는 열거형은 실패 가능한 이니셜라이저 init?(rawValue:)를 자동으로 받아요. 이 이니셜라이저는 적절한 원시 값 타입의 rawValue 파라미터를 받아, 일치하는 값이 있으면 그 열거형 케이스를 선택하고 일치하는 값이 없으면 초기화 실패를 촉발해요.
위의 TemperatureUnit 예시를 Character 타입의 원시 값을 쓰고 init?(rawValue:) 이니셜라이저를 활용하도록 다시 쓸 수 있어요:
enum TemperatureUnit: Character {
case kelvin = "K", celsius = "C", fahrenheit = "F"
}
let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
print("This is a defined temperature unit, so initialization succeeded.")
}
// Prints "This is a defined temperature unit, so initialization succeeded."
let unknownUnit = TemperatureUnit(rawValue: "X")
if unknownUnit == nil {
print("This isn't a defined temperature unit, so initialization failed.")
}
// Prints "This isn't a defined temperature unit, so initialization failed."
초기화 실패의 전파
클래스·구조체·열거형의 실패 가능한 이니셜라이저는 같은 클래스·구조체·열거형의 다른 실패 가능한 이니셜라이저로 가로질러 위임할 수 있어요. 마찬가지로, 서브클래스의 실패 가능한 이니셜라이저는 수퍼클래스의 실패 가능한 이니셜라이저로 위로 위임할 수 있어요.
두 경우 모두, 초기화 실패를 일으키는 다른 이니셜라이저로 위임하면 전체 초기화 과정이 즉시 실패하고, 그 이후의 초기화 코드는 전혀 실행되지 않아요.
참고: 실패 가능한 이니셜라이저는 실패 가능하지 않은 이니셜라이저로도 위임할 수 있어요. 다른 방식으로는 실패하지 않는 기존 초기화 과정에 잠재적 실패 상태를 추가해야 할 때 이 방식을 사용해요.
아래 예시는 Product의 서브클래스인 CartItem을 정의해요. CartItem 클래스는 온라인 쇼핑 카트의 항목을 모델링해요. CartItem은 저장 상수 프로퍼티 quantity를 도입하고, 이 프로퍼티가 항상 최소 1의 값을 갖도록 보장해요:
class Product {
let name: String
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class CartItem: Product {
let quantity: Int
init?(name: String, quantity: Int) {
if quantity < 1 { return nil }
self.quantity = quantity
super.init(name: name)
}
}
CartItem의 실패 가능한 이니셜라이저는 1 이상의 quantity 값을 받았는지 검증하는 것으로 시작해요. quantity가 유효하지 않으면 전체 초기화 과정이 즉시 실패하고 그 이후의 초기화 코드는 실행되지 않아요. 마찬가지로 Product의 실패 가능한 이니셜라이저는 name 값을 확인하고, name이 빈 문자열이면 초기화 과정이 즉시 실패해요.
비어 있지 않은 이름과 1 이상의 수량으로 CartItem 인스턴스를 만들면 초기화가 성공해요:
if let twoSocks = CartItem(name: "sock", quantity: 2) {
print("Item: \(twoSocks.name), quantity: \(twoSocks.quantity)")
}
// Prints "Item: sock, quantity: 2".
quantity 값이 0인 CartItem 인스턴스를 만들려고 하면, CartItem 이니셜라이저가 초기화 실패를 일으켜요:
if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
print("Unable to initialize zero shirts")
}
// Prints "Unable to initialize zero shirts".
마찬가지로, 빈 name 값으로 CartItem 인스턴스를 만들려고 하면 수퍼클래스 Product 이니셜라이저가 초기화 실패를 일으켜요:
if let oneUnnamed = CartItem(name: "", quantity: 1) {
print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
print("Unable to initialize one unnamed product")
}
// Prints "Unable to initialize one unnamed product".
실패 가능한 이니셜라이저 오버라이드하기
실패 가능한 이니셜라이저는 다른 이니셜라이저처럼 서브클래스에서 오버라이드할 수 있어요. 대신 수퍼클래스의 실패 가능한 이니셜라이저를 서브클래스의 실패 가능하지 않은 이니셜라이저로 오버라이드할 수도 있어요. 이렇게 하면 수퍼클래스의 초기화는 실패가 허용되더라도, 초기화가 실패할 수 없는 서브클래스를 정의할 수 있어요.
실패 가능한 수퍼클래스 이니셜라이저를 실패 가능하지 않은 서브클래스 이니셜라이저로 오버라이드하면, 수퍼클래스 이니셜라이저로 위임하는 유일한 방법은 실패 가능한 수퍼클래스 이니셜라이저의 결과를 강제 언래핑(force-unwrap)하는 거라는 점을 알아두세요.
참고: 실패 가능한 이니셜라이저를 실패 가능하지 않은 이니셜라이저로 오버라이드는 할 수 있지만, 그 반대는 안 돼요.
아래 예시는 Document 클래스를 정의해요. 이 클래스는 비어 있지 않은 문자열 값 또는 nil 중 하나인 name 프로퍼티로 초기화할 수 있지만, 빈 문자열이 될 수는 없는 문서를 모델링해요:
class Document {
var name: String?
// this initializer creates a document with a nil name value
init() {}
// this initializer creates a document with a nonempty name value
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
다음 예시는 Document의 서브클래스인 AutomaticallyNamedDocument를 정의해요. AutomaticallyNamedDocument 서브클래스는 Document가 도입한 지정 이니셜라이저 둘 다를 오버라이드해요. 이 오버라이드들은, 이름 없이 초기화되거나 init(name:) 이니셜라이저에 빈 문자열이 넘겨지면 AutomaticallyNamedDocument 인스턴스가 초기 name 값 "[Untitled]"을 갖도록 보장해요:
class AutomaticallyNamedDocument: Document {
override init() {
super.init()
self.name = "[Untitled]"
}
override init(name: String) {
super.init()
if name.isEmpty {
self.name = "[Untitled]"
} else {
self.name = name
}
}
}
AutomaticallyNamedDocument는 수퍼클래스의 실패 가능한 init?(name:) 이니셜라이저를 실패 가능하지 않은 init(name:) 이니셜라이저로 오버라이드해요. AutomaticallyNamedDocument는 빈 문자열 케이스를 수퍼클래스와 다른 방식으로 처리하기 때문에 이니셜라이저가 실패할 필요가 없고, 그래서 실패 가능하지 않은 버전의 이니셜라이저를 제공해요.
초기화에서 강제 언래핑을 사용해, 서브클래스의 실패 가능하지 않은 이니셜라이저 구현의 일부로 수퍼클래스의 실패 가능한 이니셜라이저를 호출할 수 있어요. 예를 들어 아래 UntitledDocument 서브클래스는 항상 "[Untitled]"로 이름 지어지고, 초기화 중에 수퍼클래스의 실패 가능한 init(name:) 이니셜라이저를 사용해요.
class UntitledDocument: Document {
override init() {
super.init(name: "[Untitled]")!
}
}
이 경우 수퍼클래스의 init(name:) 이니셜라이저가 빈 문자열을 이름으로 호출된다면, 강제 언래핑 연산은 런타임 오류를 일으켜요. 하지만 문자열 리터럴로 호출되기 때문에 이니셜라이저가 실패하지 않을 걸 알 수 있어서, 이 경우에는 런타임 오류가 발생할 수 없어요.
init! 실패 가능한 이니셜라이저
보통 실패 가능한 이니셜라이저는 init 키워드 뒤에 물음표를 붙여(init?) 해당 타입의 옵셔널 인스턴스를 만드는 것으로 정의해요. 대신, 해당 타입의 암시적 언래핑 옵셔널(implicitly unwrapped optional) 인스턴스를 만드는 실패 가능한 이니셜라이저를 정의할 수도 있어요. 물음표 대신 init 키워드 뒤에 느낌표를 붙이면(init!) 돼요.
init?에서 init!로, 그리고 그 반대로 위임할 수 있고, init?를 init!로, 그리고 그 반대로 오버라이드할 수도 있어요. init에서 init!로 위임할 수도 있는데, 다만 init! 이니셜라이저가 초기화 실패를 일으키면 어서션(assertion)이 촉발돼요.
필수 이니셜라이저
클래스 이니셜라이저 정의 앞에 required 수식어를 쓰면, 그 클래스의 모든 서브클래스가 그 이니셜라이저를 구현해야 한다는 뜻이 돼요:
class SomeClass {
required init() {
// initializer implementation goes here
}
}
필수 이니셜라이저의 모든 서브클래스 구현 앞에도 required 수식어를 써야 해요. 이니셜라이저 요구사항이 체인에 있는 그다음 서브클래스들에게도 적용된다는 뜻이죠. 필수 지정 이니셜라이저를 오버라이드할 때는 override 수식어를 쓰지 않아요:
class SomeSubclass: SomeClass {
required init() {
// subclass implementation of the required initializer goes here
}
}
참고: 상속받은 이니셜라이저로 요구사항을 충족할 수 있다면 필수 이니셜라이저의 명시적 구현을 제공하지 않아도 돼요.
클로저나 함수로 기본 프로퍼티 값 설정하기
저장 프로퍼티의 기본 값에 커스터마이징이나 설정 단계가 필요하다면, 클로저나 전역 함수로 그 프로퍼티의 커스터마이즈된 기본 값을 제공할 수 있어요. 프로퍼티가 속한 타입의 새 인스턴스가 초기화될 때마다 클로저나 함수가 호출되고, 그 반환 값이 프로퍼티의 기본 값으로 할당돼요.
이런 종류의 클로저나 함수는 보통 프로퍼티와 같은 타입의 임시 값을 만들고, 그 값을 원하는 초기 상태를 나타내도록 다듬은 다음, 프로퍼티의 기본 값으로 쓸 수 있도록 그 임시 값을 반환해요.
클로저로 기본 프로퍼티 값을 제공하는 골격을 대략 그려볼게요:
class SomeClass {
let someProperty: SomeType = {
// create a default value for someProperty inside this closure
// someValue must be of the same type as SomeType
return someValue
}()
}
클로저의 끝 중괄호 뒤에 빈 괄호 쌍이 따라온다는 걸 눈여겨보세요. 이 괄호는 Swift에게 클로저를 즉시 실행하라고 알려줘요. 이 괄호를 생략하면 클로저의 반환 값이 아니라 클로저 자체를 프로퍼티에 할당하려는 게 돼요.
참고: 클로저로 프로퍼티를 초기화할 때는, 클로저가 실행되는 시점에 인스턴스의 나머지 부분은 아직 초기화되지 않았다는 걸 기억하세요. 즉 클로저 안에서 다른 프로퍼티 값에 접근할 수 없어요. 그 프로퍼티들이 기본 값을 갖고 있더라도 마찬가지예요. 암시적
self프로퍼티를 사용하거나 인스턴스의 메서드를 호출하는 것도 할 수 없어요.
아래 예시는 체스 게임의 판을 모델링하는 Chessboard 구조체를 정의해요. 체스는 흑·백 사각형이 번갈아 놓인 8 x 8 판에서 플레이해요.
이 게임 판을 나타내기 위해 Chessboard 구조체는 boardColors라는 단일 프로퍼티를 가져요. 64개의 Bool 값으로 이루어진 배열이죠. 배열에서 true 값은 검은 사각형을, false 값은 흰 사각형을 나타내요. 배열의 첫 번째 항목은 판의 왼쪽 위 사각형을, 마지막 항목은 판의 오른쪽 아래 사각형을 나타내요.
boardColors 배열은 색 값들을 설정하는 클로저로 초기화돼요:
struct Chessboard {
let boardColors: [Bool] = {
var temporaryBoard: [Bool] = []
var isBlack = false
for i in 1...8 {
for j in 1...8 {
temporaryBoard.append(isBlack)
isBlack = !isBlack
}
isBlack = !isBlack
}
return temporaryBoard
}()
func squareIsBlackAt(row: Int, column: Int) -> Bool {
return boardColors[(row * 8) + column]
}
}
새 Chessboard 인스턴스가 만들어질 때마다 클로저가 실행되고, boardColors의 기본 값이 계산되어 반환돼요. 위 예시의 클로저는 temporaryBoard라는 임시 배열에서 판의 각 사각형에 적절한 색을 계산해 설정하고, 설정이 끝나면 이 임시 배열을 클로저의 반환 값으로 돌려줘요. 반환된 배열 값은 boardColors에 저장되고, squareIsBlackAt(row:column:) 유틸리티 함수로 조회할 수 있어요:
let board = Chessboard()
print(board.squareIsBlackAt(row: 0, column: 1))
// Prints "true".
print(board.squareIsBlackAt(row: 7, column: 7))
// Prints "false".
더 알아보기
- 이니셜라이저 위임과 상속: 값 타입·클래스 타입의 위임 규칙, 지정·편의 이니셜라이저, 2단계 초기화, 자동 이니셜라이저 상속은 이 문서 본문에서 다뤘어요. 상속 일반에 대한 내용은 doc:Inheritance 챕터를 봐요.
- 디이니셜라이저: 인스턴스가 해제되기 직전에 수행하는 정리 작업에 관한 내용은 doc:Deinitialization 챕터에 있어요.
- 엄격한 초기화 검사: 속성 초기화 순서와 관련된 언어의 안전성 설계에 관심이 있다면 Swift의 수동 메모리 관리와 프로퍼티 초기화 규칙을 함께 보면 이해가 깊어져요.
- 실패 가능한 이니셜라이저와 옵셔널:
init?·init!과 옵셔널 바인딩·강제 언래핑의 상호작용은 옵셔널을 다루는 챕터(doc:Optional-Chaining 등)와 같이 읽으면 좋아요. - 확장을 통한 초기화: 커스텀 이니셜라이저를 타입의 원래 구현이 아니라 확장에 작성하는 패턴은 doc:Extensions 챕터에 있어요.