본문 바로가기

Swift

Initialization - 구조체/클래스 초기화

728x90

Initialization

class, structure, enumeration과 같은 named type의 stored property의 값을 초기화하는 것입니다.

 

Structure Initialization

예를 들어 아래와 같은 구조체 Pet을 정의하겠습니다.

struct Pet {

}

 

Default Initializer

let myPet = Pet()

Pet에는 아직 이니셜라이저가 정의되어 있지 않지만, 자동으로 default initializer가 사용됩니다. 만약 사용하고자 하는 타입이 저장 프로퍼티가 없거나 모든 저장 프로퍼티의 기본 값이 있다면 기본 이니셜라이저를 사용할 수 있습니다.

 

구조체 Pet에 몇가지 저장 프로퍼티를 만들어보겠습니다. ⬇️

struct Pet {
    let name: String = "HuHu"
    let isDog: Bool = true
}

위와 같이 두 개의 저장 프로퍼티가 추가되었지만 여전히 기본 이니셜라이저는 작동을 합니다.

그 이유는 name, isDog가 기본 값을 갖고 있기 때문입니다.

 

🤔 이 경우에, 옵셔널 값이 추가되면 어떻게 될까요?

struct Pet {
    let name: String = "Joy"
    let isDog: Bool = true
    var age: Int?
}

age의 값이 nil로 초기화 되면서 기본 이니셜라이저가 작동하는 것을 알 수 있습니다. 하지만 만약, age가 상수라면 nil로 초기화 되지 않기 때문에 nil이라는 초기 값을 지정해줘야 합니다. 

 

Memberwise Initializer 

타입의 모든 저장 프로퍼티의 값을 초기화하거나 상수로 사용하는 경우는 많지 않기 때문에 각 멤버별로 초기화하는 방법을 사용할 수 있습니다.

 

struct Pet {
    let name: String
    let isDog: Bool
    let age: Int
}

앞서 작성한 Pet과 비슷하지만 각 프로퍼티에 대해서 기본 값이 없고 정의되어 있는 이니셜라이저 역시 없습니다.

 

이 경우에 아래와 같이 인스턴스를 생성하면 어떻게 될까요? ⬇️

let myPet = Pet(name: "Joy", isDog: true, age: 10)

Swift의 구조체는 자동으로 memberwise initializer를 만들어주기 때문에 각 멤버별로 초기 값을 지정할 수 있습니다.

 

그런데 만약 아래와 같이 기본 값이 있는 멤버가 있다면, 어떻게 될까요? ⬇️

struct Pet {
    let name: String
    let isDog: Bool = true
    let age: Int
}

이 경우에는 기본 값이 설정되어 있는 멤버의 값을 파라미터로 받지 않습니다. 

 

Custom Initializer 

struct Pet {
    let name: String
    let isDog: Bool
    let age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.isDog = true
    }
}

위와 같이 구조체의 정의 안에 이니셜라이저를 정의하게 되면, 더 이상 멤버별 이니셜라이저는 자동으로 생성되지 않습니다.

 

그런데 만약 커스텀 이니셜라이저와 멤버별 이니셜라이저를 둘 다 사용하고 싶다면?

struct Pet {
    let name: String
    let isDog: Bool
    let age: Int
}

extension Pet {
    init(name: String, age: Int) {
        self.name = name
        self.age = age
        self.isDog = true
    }
}

위와 같이 커스텀 이니셜라이저를 extension을 사용해서 따로 작성하면 됩니다.

 

✅ 구조체는 자동으로 기본 값이 없는 멤버를 초기화할 수 있는 Memberwise Initializer를 만들어줍니다.
✅ Custom Initializer가 있다면 Memberwise를 사용할 수 없지만, extension으로 분리하면 사용할 수 있습니다.

 

Custom Initializer - Custom init 구현

struct Pet {
    let name: String
    let isDog: Bool
    let age: Int

    init(name: String = "Joy", isDog: Bool = true, age: Int = 10) {
        self.name = name
        self.isDog = isDog
        self.age = age
    }
}

named type의 이니셜라이저는 기본 값이 없는 모든 저장 프로퍼티를 초기화할 수 있어야 합니다. 위 코드에서 이니셜라이저는 값을 받아서 멤버를 초기화하는 동시에 기본 값을 갖고 있기 때문에 let myPet = Pet()으로 초기화를 해도 Default Initializer가 동작하듯이 동작할 수 있습니다. 

 

Initializer Delegation

저장 프로퍼티가 단순히 파라미터의 값을 저장하는 것이 아니라, 연산이 필요한 경우 커스텀 이니셜라이저가 계속 늘어나게 됩니다.

 

위의 코드를 예제로 사용하자면, isDog를 Bool 타입이 아니라, String 타입으로 "Dog", "Cat"등의 파라미터가 들어오는 것으로 초기화하고 싶다면, 아래와 같이 작성할 수 있습니다.

struct Pet {
    let name: String
    let isDog: Bool
    let age: Int

    init(name: String = "Joy", isDog: Bool = true, age: Int = 10) {
        self.name = name
        self.isDog = isDog
        self.age = age
    }

    init(name: String = "Joy", isDog: String = "Dog", age: Int = 10) {
        self.name = name
        self.age = age
        self.isDog = (isDog == "Dog")
    }
}

하지만 위의 코드는 중복되는 코드가 많다는 것을 확인할 수 있습니다.

따라서 아래와 같이 코드를 수정할 수 있습니다. 

struct Pet {
    let name: String
    let isDog: Bool
    let age: Int

    init(name: String = "Joy", isDog: Bool = true, age: Int = 10) {
        self.name = name
        self.isDog = isDog
        self.age = age
    }

    // delegating initializer
    init(name: String = "Joy", isDog: String = "Dog", age: Int = 10) {
        self.init(name: name, isDog: (isDog == "Dog"), age: age)
    }
}

이처럼 다른 이니셜라이저에게 초기화를 위임하기 때문에 delegation initializer가 됩니다. 

 

Initialization Phase

이미지에서 볼 수 있는 것처럼 단계 1은 초기화의 시작 부분에서 시작해서 모든 저장 프로퍼티가 초기화된 후에 종료되고, 단계 2에서 나머지 초기화 작업이 진행됩니다.

 

단계 1에서는 지금 초기화하는 대상 인스턴스를 사용할 수 없지만, 단계 2에서는 사용가능하다는 차이가 있습니다.

 

Phase의 진행 단계를 코드와 함께 보면 아래와 같습니다. ⬇️

struct Pet {
    let name: String
    let isDog: Bool
    let age: Int

    init(name: String = "Joy", isDog: Bool = true, age: Int = 10) {
        // Phase 1 init
        self.name = name
        self.isDog = isDog
        self.age = age
        // Phase 2 init
    }

    // delegating initializer
    init(name: String = "Joy", isDog: String = "Dog", age: Int = 10) {
        // Phase 1 Delegating init
        self.init(name: name, isDog: (isDog == "Dog"), age: age)
        // Phase 2 Delegating init
    }
}

 

Failure Handling

위의 예시는 Pet의 이름, 개의 여부, 나이를 초기화하고  있습니다. 그러나, 파라미터에 대한 검사가 없기 때문에 나이 값에 음수를 넘겨도 오류 처리가 되지 않습니다.

 

Failable(Optional) Initializer

문맥에 맞지 않는 이상한 값이 들어왔을 때 인스턴스를 생성하지 않고 nil을 반환한다면, 보다 안정적인 프로그램을 만들 수 있고 불필요한 에러를 제거할 수 있습니다. 

 

init?(name: String, isDog: Bool, age: Int) {
    if age < 0 {
        return nil
    }
    self.name = name
    self.isDog = isDog
    self.age = age
}

guard let myPet = Pet(name: "Joy", isDog: true, age: -1) {
    print("Success")
} else {
    print("Error!")
}

위와 같이 init에 ?를 붙이고 맞지 않는 값이 들어왔을 때 nil을 리턴하도록 합니다.

이렇게 Failable(Optional) Initializer를 만들면 인스턴스를 생성하는 단계에서 optional binding을 통해 에러를 방지할 수 있습니다. 

 

Error Throwing

물론 Failable(Optional) Initializer를 통해 에러를 감지할 수 있지만, 검사해야 하는 property가 여러 개이고 어디서 어떤 에러가 발생했는지 알고 싶다면 Error Throwing이 효과적입니다.

 

위의 코드에 Pet의 이름이 비어있으면 안되고 나이가 음수면 안된다는 조건을 추가하겠습니다.

enum InvalidPetDataError: Error {
    case EmptyName
    case InvalidAge
}

init(name: String, isDog: Bool, age: Int) throws {
    if name.isEmpty {
        throw InvalidPetDataError.EmptyName
    }
    if age < 0 {
        throw InvalidPetDataError.InvalidAge
    }

    self.name = name
    self.isDog = isDog
    self.age = age
}

위와 같이 enum으로 에러를 정의하고 이니셜라이저에서 throw를 정의하면 됩니다.

 

그리고 이 이니셜라이저를 사용해서 인스턴스를 생성하고 싶다면,

let myPet = try? Pet(name: "Joy", isDog: true, age: -1) // nil
let myPet = try? Pet(name: "Joy", isDog: true, age: 10)

위와 같이 생성하거나, do-catch문을 사용하면 됩니다.

Class Initialization

Designated Initializers

기본 값이 없는 모든 non-optional stored property를 초기화하는 이니셜라이저

 

class Member {
    let name: String
    let id: String
    let isPaid: Bool
    
    // Designated Initializer
    init(name: String, id: String, isPaid: Bool) {
        self.name = name
        self.id = id
        self.isPaid = isPaid
    }
}

let member1 = Member(name: "Kim", id: "123", isPaid: true)

 

Convenience Initializer

저장 프로퍼티 중에서 몇 개만 값을 받아서 초기화를 하고 나머지는 기본 값을 이용해서 초기화합니다.

 

내부에서 다른 이니셜라이저를 호출하여 동작합니다.

convenience init(name: String, id: String) {
    // Designated init 호출
    self.init(name: name, id: id, isPaid: true)
}

let member2 = Member(name: "Park", id: "456")

 

 

Failing and Throwing

초기화 과정에서 프로퍼티 값에 특정한 조건이 있어 조건에 맞지 않는 값이 파라미터로 들어오면 nil을 리턴합니다.

// Designated init
init?(name: String, id: String, isPaid: Bool) {
    if id.count != 3 {
        return nil
    }
    self.name = name
    self.id = id
    self.isPaid = isPaid
}

let member = Member(name: "Lee", id: "111", isPaid: true)
let nonmember = Member(name: "Jay", id: "1212", isPaid: true) // nil

 

🤔 만약 아래와 같이 일단 값을 받고 후에 처리하는 경우에는 어떻게 하면 좋을까요?

 

init을 위한 파라미터가 가공이 필요하다면, convenience init에서 처리를 다 하고 designated init에서는 세팅만 하는 것이 좋습니다.

// Designated Initializer
init(name: String, id: String, isPaid: Bool) {
    self.name = name
    self.id = id
    self.isPaid = isPaid
}

convenience init?(info: String) {
    let components = info.components(separatedBy: "-")

    if components[1].count != 3 {
        return nil
    }
    self.init(name: components[0], id: components[1], isPaid: Bool(components[2])!)
}

let member1 = Member(info: "Kim-123-true")

 

클래스 상속과 이니셜라이저

만약 위에서 생성한 클래스 Member를 상속하는 새로운 클래스가 있다면, 해당 subclass는 init을 다시 정의해야 합니다.

 

class Worker: Member {
    let isWorking: Bool
}

Member를 상속받는 클래스 Worker에서 위와 같이 정의를 하지 않으면 컴파일 에러가 발생합니다.

 

그러므로 기본 값을 설정하거나 designated init을 사용해서 재정의를 해야 합니다.

// 해결1: isWorking의 기본 값 세팅
class Worker: Member {
    let isWorking = false
}

// 해결2: Designated init 
class Worker: Member {
    let isWorking: Bool
    
    init(isWorking: Bool, name: String, id: String, isPaid: Bool) {
        self.isWorking = isWorking
        // super class의 property는 반드시 super.init()로 delegating
        super.init(name: name, id: id, isPaid: isPaid)
    }
}

 

subclass init에서 고려해야 할 점

✔️ subclass의 프로퍼티를 모두 init 하기 전에 super.init()을 호출하면 에러가 발생합니다.

// #1
class Worker: Member {
    let isWorking: Bool
    
    // Compile Error!
    init(isWorking: Bool, name: String, id: String, isPaid: Bool) {
        super.init(name: name, id: id, isPaid: isPaid)
        self.isWorking = isWorking
    }
}

✔️ super.init()을 수행하기 전에 superclass의 property에 접근하면 에러가 발생합니다.

// #2
class Worker: Member {
    let isWorking: Bool
    
    // Compile Error!
    init(isWorking: Bool, name: String, id: String, isPaid: Bool) {
        self.isWorking = self.isPaid
        super.init(name: name, id: id, isPaid: isPaid)
    }
}

 

✔️ subclass에서 superclass에 delegateing을 하기 위해서, 반드시 subclass의 designated init에서 superclass의 designated init을 호출해야 합니다. 

 

Deinitialization 

De를 통해 유추할 수 있는 것처럼, 인스턴스가 메모리에서 해제될 때 호출됩니다. deinit 키워드를 사용해서 호출할 수 있습니다.

❌ init과 다르게 deinit는 클래스에서만 사용할 수 있습니다.

 

🤔 왜 deinit을 해야 하는가?

Swift에서는 인스턴스 메모리 관리를 ARC를 체크해서 하므로 인스턴스가 더 이상 필요하지 않다고 판단이 되면 자동으로 deallocate 됩니다. 이렇게 자동으로 해결되는데 왜 deinit을 해야 할까요?

✅ 인스턴스를 소멸하기 전에 처리할 것들이 있다면, deinit에서 하면 됩니다.

(ex. 파일을 open 한 상태였다면, 소멸하기 전에 파일을 close 하면 됩니다.)

 

🤔 몇 개 생성할 수 있는가?

init은 여러 개를 만들 수 있지만, deinit은 하나만 만들 수 있습니다.

또한, 파라미터를 따로 받지 않고 아래의 코드처럼 작성하여 클래스 내부에서 사용하면 됩니다.

deinit {

}

 

 

'Swift' 카테고리의 다른 글

Generic  (0) 2022.04.29
atomic VS nonatomic  (0) 2022.04.26
Initialization - 그 외  (0) 2022.04.20
Initialization - 상속과 초기화  (0) 2022.04.20
Initialization - 무엇인가  (0) 2022.04.20