애플사이다의 iOS 개발 일지

[Swift] 이니셜라이저의 종류 (간단 요약) 본문

Swift/Swift 문법

[Swift] 이니셜라이저의 종류 (간단 요약)

Applecider 2021. 9. 30. 03:47

안녕하세요. 애플사이다 입니다.

 

이니셜라이저에 대한 용어가 가끔 헷갈릴 때가 있습니다.

기본 이니셜라이저, 멤버와이즈 이니셜라이저, 사용자 정의 이니셜라이저 등 이니셜라이저의 종류에 대해 간단히 요약해보겠습니다.

 


인스턴스 초기화란?

초기화는 새로운 인스턴스를 사용하기 전에 미리 준비하는 작업이다. 저장 프로퍼티의 초기값을 설정하는 등의 역할을 한다.

구조체, 클래스, 열거형의 인스턴스를 초기화할 수 있다.

 

초기화를 완료하면, 인스턴스의 모든 저장 프로퍼티의 값이 확정된다. (옵셔널 저장 프로퍼티는 제외)

저장 프로퍼티에 초기값을 지정하는 방법은 두 가지이다.

1) 프로퍼티를 정의할 때 프로퍼티 기본값 (default value)를 할당한다.

2) 이니셜라이저를 실행하여 초기값 (initial value)을 할당한다. 

이니셜라이저란?

이니셜라이저 (Initializer)를 직접 정의하면, 초기화 과정을 원하는 대로 구현할 수 있다.

이니셜라이저는 새로운 인스턴스를 생성 및 초기화하는 목적의 "메서드"이다.

(이니셜라이저를 다른 언어나 번역본에서는 초기자, 생성자라고도 한다.)

struct DrinkA {
    let name: String = "Coke" // 1) 저장 프로퍼티의 기본값을 할당하는 방법 - 선택
    let alcohol: Double = 0.0
}
let coke: DrinkA = DrinkA() // 인스턴스 생성 및 초기화
print(coke.name) // Coke 출력

class DrinkB {
    let name: String
    let alcohol: Double

    init() {
        self.name = "AppleCider" // 2) 이니셜라이저에서 초기값을 할당하는 방법 - 선택
        self.alcohol = 5.0
    }
}
let appleCider: DrinkB = DrinkB() // 인스턴스 생성 및 초기화
print(appleCider.name) // AppleCider 출력

enum Direction { 
    case left
    case right
    
    init() { // 열거형의 이니셜라이저
        self = .left
    }
}
let left: Direction = Direction()
print(left) // left 출력

이니셜라이저의 매개변수

이니셜라이저도 메서드이므로 매개변수를 가질 수 있다. (Swift의 이니셜라이저는 반환하지 않는다.)

class DrinkC {
    let name: String
    let alcohol: Double

    init(name: String, alcohol: Double) { // 매개변수가 있는 이니셜라이저
        self.name = name
        self.alcohol = alcohol
    }
}
let lemonade: DrinkC = DrinkC(name: "Lemonade", alcohol: 0.0) // 인스턴스 생성 및 초기화
print(lemonade.name) // Lemonade 출력

 

  • 사용자 정의 이니셜라이저를 생성하면, 기존의 기본 이니셜라이저 (init())는 사용할 수 없다. (별도로 구현하면 가능)

옵셔널 저장 프로퍼티

인스턴스를 사용하는 동안 필수적으로 값을 가지지 않아도 되는 프로퍼티가 있을 수 있다. 또는 초기값을 미리 지정하기 어려운 프로퍼티도 있을 수 있다. 이때는 지정 프로퍼티를 옵셔널로 선언한다.

옵셔널 프로퍼티는 초기화 과정에서 값을 할당하지 않으면, defalut로 nil이 할당된다.

class DrinkD {
    let name: String
    var alcohol: Double? // let으로 선언하면 컴파일 오류 - 인스턴스 생성 후 값을 지정할 수 있어야 하므로 변수로 선언
    
    init(name: String) {
        self.name = name
    }
}
let noneAlcohol: DrinkD = DrinkD(name: "Fanta")
print(noneAlcohol.name)   // Fanta 출력
print(noneAlcohol.alcohol) // nil 출력 - 옵셔널이므로 자동으로 nil이 할당된 상태
noneAlcohol.alcohol = 0.0  
print(noneAlcohol.alcohol) // Optional(0.0) 출력

class DrinkE {
    let name: String
    var alcohol: Double?
    
    init(name: String) {
        self.name = name
    }
    
    init(name: String, alcohol: Double) { // 옵셔널 프로퍼티를 포함한 이니셜라이저도 생성 가능
        self.name = name
        self.alcohol = alcohol
    }
}
let orangeSqueeze: DrinkE = DrinkE(name: "Orange Squeeze") // 두 가지 이니셜라이저를 모두 사용 가능
let tequilaSunrise: DrinkE = DrinkE(name: "Tequila Sunrise", alcohol: 12.0)
  • 음료 (Drink 타입)의 알코올 함량 (alcohol 프로퍼티)의 값은 있어도 되고, 없어도 된다. 따라서 옵셔널로 선언한다.
  • 이때 alcohol 프로퍼티는 상수가 아니라 변수로 선언한다. 인스턴스 생성 후에 값을 지정할 수 있어야 하기 때문이다.
    let으로 선언하면 컴파일 오류가 발생한다. (Return from initializer without initializing all stored properties)
  • 이니셜라이저는 옵셔널 프로퍼티를 포함해도 되고, 안해도 된다. 여러 종류의 이니셜라이저를 정의할 수 있다.
    인스턴스를 생성할 때, 원하는 이니셜라이저를 선택할 수 있다.

기본 이니셜라이저 및 멤버와이즈 이니셜라이저

사용자 정의 이니셜라이저를 정의하지 않으면, 모든 프로퍼티에 기본값이 지정되어 있다는 전제 하에 기본 이니셜라이저 (Default Initializer)를 사용한다. 즉, 기본 이니셜라이저는 프로퍼티를 정의할 때 할당한 프로퍼티 기본값으로 프로퍼티를 초기화한다.

따라서 기본 이니셜라이저를 사용하려면, 1) 저장 프로퍼티의 기본값이 모두 지정되어 있고, 2) 사용자 정의 이니셜라이저가 정의되어 있지 않은 상태여야 한다.

 

구조체는 사용자 정의 이니셜라이저를 정의하지 않으면, 멤버와이즈 이니셜라이저를 default로 제공한다. (클래스는 제공하지 않음)

멤버와이즈 이니셜라이저는 프로퍼티의 이름을 매개변수로 갖는 이니셜라이저이다.

struct InBody {
    var height: Double = 170.0
    var weight: Double = 70.0
}
let james: InBody = InBody(height: 180, weight: 80) // 멤버와이즈 이니셜라이저를 사용
let kevin: InBody = InBody() // 기본 이니셜라이저를 사용
let jane: InBody = InBody(height: 160) // 기본값이 있는 저장 프로퍼티는 필요한 매개변수만 사용하여 초기화 가능
let emily: InBody = InBody(weight: 80)

사용자 정의 이니셜라이저를 정의하면, 기본 이니셜라이저 및 멤버와이즈 이니셜라이저를 사용할 수 없다.

struct ComplexInBody {
    var height: Double = 170.0
    var weight: Double = 70.0
    var rate: Double = 1
    
    init(height: Double, weight: Double) {
        self.height = height
        self.weight = weight
        self.rate = (height / weight) * 0.8 + 100
    }
}
let james2: ComplexInBody = ComplexInBody(height: 180, weight: 80) // 사용자 정의 이니셜라이저만 사용 가능

// 아래 코드 모두 컴파일 에러 발생
let newOne: ComplexInBody = ComplexInBody(height: 180, weight: 60, rate: 1) // 멤버와이즈 이니셜라이저 사용 불가
let kevin2: ComplexInBody = ComplexInBody() // 기본 이니셜라이저 사용 불가
let jane2: ComplexInBody = ComplexInBody(height: 160) // 사용 불가
let emily2: ComplexInBody = ComplexInBody(weight: 80) // 사용 불가

이니셜라이저 위임

값 타입인 구조체, 열거형은 특정 이니셜라이저가 다른 이니셜라이저에게 일부 초기화를 위임할 수 있다.

이니셜라이저 위임 (Initializer Delegation)을 통해 코드 중복을 최소화한다.

*클래스의 이니셜라이저 위임은 복잡해서 (상속 때문) 별도로 포스팅하겠습니다.

 

특정 이니셜라이저가 다른 이니셜라이저를 호출할 때는 self.init을 사용한다. 이니셜라이저 위임을 하려면 최소 2개 이상의 사용자 정의 이니셜라이저를 정의해야 한다. (self.init을 사용하는 것 자체가 사용자 정의 이니셜라이저를 정의하고 있다는 뜻이기 때문이다.)

enum Student {
    case elementary, middle, high
    case none

    init(koreanAge: Int) { // 첫 번째 사용자 정의 이니셜라이저
        switch koreanAge {
        case 8...13:
            self = .elementary
        case 14...16:
            self = .middle
        case 17...19:
            self = .high
        default:
            self = .none
        }
    }
    
    init(bornAt: Int, currentYear: Int) { // 두 번째 사용자 정의 이니셜라이저
        self.init(koreanAge: currentYear - bornAt + 1) // 이니셜라이저 위임
    }

    init() { // 사용자 정의 이니셜라이저가 있는 경우, init() 메서드를 별도로 구현해야 기본 이니셜라이저를 사용 가능
        self = .none
    }
}
var liah: Student = Student() // 별도 구현한 기본 이니셜라이저를 사용
print(liah) // none 출력
liah = Student(koreanAge: 13) // 첫 번째 사용자 정의 이니셜라이저 사용
print(liah) // elementary 출력
liah = Student(bornAt: 2007, currentYear: 2021) // 두 번째 사용자 정의 이니셜라이저 사용
print(liah) // middle 출력
  • 두 가지 사용자 정의 이니셜라이저, 그리고 별도로 구현한 기본 이니셜라이저를 사용할 수 있다.
  • 이니셜라이저 위임을 통해 코드 중복을 줄였다.

실패 가능한 이니셜라이저 (init?)

이니셜라이저의 전달인자로 잘못된 값이 전달되면, 이니셜라이저는 초기화에 실패할 수 있다. 이외에도 다양한 실패 원인이 있다.

실패 가능성을 내포하는 이니셜라이저를 실패 가능한 이니셜라이저 (Failable Initializer)라고 한다.

초기화에 실패하면 nil을 반환하므로 옵셔널 (init?)이다. (실제로 값을 반환하지 않는다.)

열거형을 초기화할 때 유용하다.

 

Note: 실패 가능한 이니셜라이저 (init?)와 일반적인 이니셜라이저 (init)는 동일한 이름 및 동일한 타입의 매개변수를 가질 수 없다.

class NoneAlcohol {
    let name: String
    var alcohol: Double
    
    init?(name: String, alcohol: Double) { // 실패 가능한 이니셜라이저
        if alcohol > 1 {
            return nil
        }
        self.name = name
        self.alcohol = alcohol
    }
}
if let fakeBeer: NoneAlcohol = NoneAlcohol(name: "Fake Beer", alcohol: 0.5) {
    print(fakeBeer.name)
} else {
    print("초기화 실패")
}
// Fake Beer 출력

if let beer: NoneAlcohol = NoneAlcohol(name: "Beer", alcohol: 5.0) {
    print(beer.name)
} else {
    print("초기화 실패")
}
// 초기화 실패 - 출력
  • 알코올 함량 (alcohol 프로퍼티) 값이 1 이상이면, 초기화에 실패하는 조건이다.

프로토콜을 사용한 이니셜라이저 요구

프로토콜 (Protocol)을 통해 특정 타입이 구현해야 할 이니셜라이저를 요구할 수 있다.

실패 가능한 이니셜라이저도 요구할 수 있다. 단, 구현할 때는 일반적인 이니셜라이저 및 실패가능한 이니셜라이저 모두 가능하다.

protocol MainIngredient {
    var mainIngredient: String { get } // 프로퍼티 요구
    
    init(mainIngredient: String) // 이니셜라이저 요구
}

class DrinkC2: MainIngredient { // 프로토콜 채택
    var name: String?
    var alcohol: Double?
    var mainIngredient: String

    init(name: String, alcohol: Double, mainIngredient: String) {
        self.name = name
        self.alcohol = alcohol
        self.mainIngredient = mainIngredient
    }

    required init(mainIngredient: String) { // 프로토콜에서 요구한 인스턴스 구현
        self.mainIngredient = mainIngredient
    }

//    required init(name: String, alcohol: Double, mainIngredient: String) { // 프로토콜에서 요구한 인스턴스가 아님
//        self.name = name
//        self.alcohol = alcohol
//        self.mainIngredient = mainIngredient
//    }
}
var lemonade2: DrinkC2 = DrinkC2(mainIngredient: "Lemon")
print(lemonade2.mainIngredient) // Lemon 출력
lemonade2 = DrinkC2(name: "AppleCider", alcohol: 5.0, mainIngredient: "Apple")
print(lemonade2.mainIngredient) // Apple 출력
  • 프로토콜에서 요구한 인스턴스를 구현할 때, required 키워드를 명시해야 한다.
    DrinkC2 클래스를 상속받는 모든 자식 클래스는 MainIngredient 프로토콜을 준수해야 하고, 따라서 자식 클래스는 프로토콜에서 요구한 인스턴스를 항상 구현해야 하기 때문이다.
  • 상속이 불가한 final 클래스이면, required 키워드가 필요 없다.
  • 만약 특정 클래스가 프로토콜을 준수하지 않는데, 이미 프로토콜이 요구하는 이니셜라이저를 정의한 상태라면, 해당 클래스를 상속받은 자식 클래스는 required override 키워드를 명시해야 한다.

익스텐션을 사용한 이니셜라이저 추가

익스텐션 (Extension)을 통해 특정 타입의 이니셜라이저를 추가할 수 있다.

단, 클래스는 편의 이니셜라이저는 추가 가능하지만, 지정 이니셜라이저 및 디이니셜라이저는 추가 불가하다. 

struct DrinkF {
    var name: String
    var alcohol: Double?
    
    init(name: String) {
        self.name = name
    }
}

extension DrinkF { // 익스텐션을 통한 이니셜라이저 추가
    init() {
        self.name = "JackCoke"
    }
    
    init(alcohol: Double) { // convenience init은 컴파일 에러 
        self.init() // 이니셜라이저 위임
        self.alcohol = 8.0
    }
}
let cocaCola: DrinkF = DrinkF(name: "Coca-Cola")
print(cocaCola.name)        // Coca-Cola 출력
var drinkWithCoke: DrinkF = DrinkF()
print(drinkWithCoke.name)   // JackCoke 출력
print(drinkWithCoke.alcohol) // nil 출력
drinkWithCoke = DrinkF(alcohol: 8.0)
print(drinkWithCoke.name)   // JackCoke 출력
print(drinkWithCoke.alcohol) // Optional(8.0) 출력
  • 익스텐션을 통해 다양한 이니셜라이저를 추가 가능하다.
  • 구조체는 이니셜라이저 위임을 할 때, convenience 키워드 (편의 이니셜라이저)를 사용하지 않는다.
    클래스만 이니셜라이저를 지정/편의 이니셜라이저로 구분한다.
    convenience를 명시하면 컴파일 에러가 발생한다. (Delegating initializers in structs are not marked with 'convenience') 
class DrinkC3 {
    let name: String
    let alcohol: Double

    init(name: String, alcohol: Double) {
        self.name = name
        self.alcohol = alcohol
    }
}

extension DrinkC3 { 
    convenience init() { // 익스텐션을 통한 편의 이니셜라이저 추가
        self.init(name: "welcomeDrink", alcohol: 0.0)
    }
}
let appleCider2: DrinkC3 = DrinkC3(name: "AppleCider", alcohol: 5.0) // 사용자 정의 이니셜라이저 사용
print(appleCider2.name) // AppleCider 출력

let welcomeDrink: DrinkC3 = DrinkC3() // 익스텐션으로 추가한 이니셜라이저 사용
print(welcomeDrink.name) // welcomeDrink 출력
  • 클래스는 익스텐션으로 convenience 키워드를 붙인 편의 이니셜라이저만 추가 가능하다.

익스텐션으로 값 타입에 이니셜라이저를 추가했을 때, 아래 조건이 모두 성립하면 익스텐션으로 사용자 정의 이니셜라이저를 추가한 후에도 해당 타입의 기본 이니셜라이저 및 멤버와이즈 이니셜라이저를 호출할 수 있다.
(원래는 사용자 정의 이니셜라이저가 있으면, 기본/멤버와이드 이니셜라이저를 사용할 수 없다.)

조건 1) 모든 저장 프로퍼티에 기본값이 있다.

조건 2) 타입에 기본 이니셜라이저 및 멤버와이즈 이니셜라이저 외에 추가적인 사용자 정의 이니셜라이저가 없다.

struct Point {
    var x: Double = 0
    var y: Double = 0
}

struct Size {
    var height: Double = 0
    var width: Double = 0
}

struct Rectangle {
    var origin: Point = Point() // Point 타입의 모든 저장 프로퍼티가 기본값을 가지므로 기본 이니셜라이저를 사용 가능
    var size: Size = Size()
}

extension Rectangle {
    init(center: Point, size: Size) {
        let originX: Double = center.x - (size.width / 2)
        let originY: Double = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size) // 이니셜라이저 위임 (멤버와이즈 이니셜라이저에게)
    }
}
let defaultRectangle: Rectangle = Rectangle() // 기본 이니셜라이저 사용
let memberwiseRectangle: Rectangle = Rectangle(origin: Point(x: 3, y: 3),
                                               size: Size(height: 10, width: 10)) // 멤버와이즈 이니셜라이저 사용
let centerRectangle: Rectangle = Rectangle(center: Point(x: 100, y: 100),
                                           size: Size(height: 1, width: 1)) // 익스텐션으로 추가한 이니셜라이저 사용
let anotherRectangle: Rectangle = Rectangle(center: Point(), size: Size())  // 익스텐션으로 추가한 이니셜라이저 사용
  • Point 타입 및 Size 타입의 모든 저장 프로퍼티는 기본값을 가지며, 추가적인 사용자 정의 이니셜라이저를 정의하지 않았다.
    따라서 Point 타입 및 Size 타입의 기본/멤버와이즈 이니셜라이저를 사용할 수 있다.
  • 따라서 익스텐션으로 추가한 이니셜라이저는 Rectangle 타입의 멤버와이즈 이니셜라이저에게 이니셜라이저 위임을 할 수 있다.

참고 - 클래스는 익스텐션으로 지정 이니셜라이저를 추가할 수 없다. 편의 이니셜라이저만 추가할 수 있다.

또한 클래스는 멤버와이즈 이니셜라이저가 제공되지 않으므로 위 예시와 같이 익스텐션으로 init(center:size)를 정의할 수 없다.

class Point {
    var x: Double = 0 // 모든 저장 프로퍼티에 기본값을 할당하면, 사용자 정의 이니셜라이저를 정의하지 않아도 됨 (클래스)
    var y: Double = 0
}

class Size {
    var height: Double = 0
    var width: Double = 0
}

class Rectangle {
    var origin: Point = Point() // Point 타입의 모든 저장 프로퍼티가 기본값을 가지므로 기본 이니셜라이저를 사용 가능
    var size: Size = Size()
}

extension Rectangle {
    var color: String { // 연산 프로퍼티 추가
        return "black"
    }
    
    convenience init(origin: Point, size: Size) { // 익스텐션으로 편의 이니셜라이저 추가 가능
//        let originX: Double = center.x - (size.width / 2)
//        let originY: Double = center.y - (size.height / 2)
//        self.init(origin: Point(x: originX, y: originY), size: size) // 이니셜라이저 위임 (멤버와이즈 이니셜라이저에게) - 불가 (클래스는 멤버와이즈 이니셜라이저가 없음)
        self.init() // 이니셜라이자 위임 (기본 이니셜라이저에게) - 가능
    }
    
//    init(color: String) { // 익스텐션으로 (사용자 정의) 지정 이니셜라이저 추가 불가 - 컴파일 에러 발생
//        self.color = color
//    }
}
let defaultRectangle: Rectangle = Rectangle() // 기본 이니셜라이저 사용
let memberwiseRectangle2: Rectangle = Rectangle(origin: Point(), size: Size()) // 익스텐션으로 추가한 이니셜라이저 사용
  • Rectangle 타입의 모든 저장 프로퍼티에 기본값이 지정되어 있으므로 기본 이니셜라이저를 사용 가능하다.
  • 클래스는 멤버와이즈 이니셜라이저가 제공되지 않으므로 위 예시와 같이 익스텐션으로 init(center:size)를 정의할 수 없다.
  • 클래스는 익스텐션으로 지정 이니셜라이저를 추가할 수 없다. 추가 시 컴파일 에러가 발생한다. (Designated initializer cannot be declared in an extension of 'Rectangle'; did you mean this to be a convenience initializer?)
  • Rectangle 타입은 기본 이니셜라이저를 사용 가능하므로 인스텐션으로 추가한 편의 이니셜라이저는 기본 이니셜라이저에게 이니셜라이저 위임을 할 수 있다. 

 

- Reference

  • Swift Language Guide > Initialization
  • 책 <스위프트 프로그래밍>, 야곰

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments