2025/07/19

10. Swift 초기화(Initialization): 인스턴스를 안전하게 생성하는 법

10. Swift 초기화 썸네일
안녕하세요! 지난 블로그에는 코드 재사용성을 높이는 상속에 대해 알아보았죠. 오늘은 우리가 만든 클래스, 구조체, 열거형 같은 타입의 인스턴스를 생성하는 방법, 즉 초기화(Initialization)에 대해 깊이 파고들 것입니다.

초기화는 단순히 객체를 '만드는' 과정을 넘어, 인스턴스가 사용되기 전에 모든 저장 프로퍼티가 적절한 초기 값을 가지도록 보장하는 중요한 역할을 합니다. 이는 Swift의 강력한 타입 안전성 원칙의 핵심이며, 여러분의 앱에서 발생할 수 있는 잠재적인 오류를 미리 방지하는 데 필수적입니다.


1. 초기화(Initialization)란 무엇인가요?

초기화는 클래스, 구조체, 또는 열거형의 새로운 인스턴스를 생성할 준비를 하는 과정입니다. 이 과정에서 새로운 인스턴스의 모든 저장 프로퍼티는 반드시 초깃값을 할당받아야 합니다.

초기화를 수행하는 특별한 메서드를 초기화 메서드(Initializer)라고 하며, init 키워드를 사용합니다. Swift는 모든 저장 프로퍼티에 초깃값이 할당되기 전까지는 인스턴스를 사용할 수 없도록 강제하여 잠재적인 오류를 방지합니다.

💡 왜 초기화가 중요한가요?

만약 인스턴스의 특정 프로퍼티가 초기화되지 않은 상태로 사용된다면, 이는 예측 불가능한 동작이나 앱 크래시로 이어질 수 있습니다. 초기화는 이러한 불안정한 상태를 원천 봉쇄하여 코드의 안정성을 극대화합니다.

1.1. 초기화 메서드 (Initializers)

init 키워드를 사용하여 초기화 메서드를 정의합니다. 함수와 유사하지만, 반환 타입이 없으며 인스턴스 자신을 반환합니다.

//Swift 예제
struct Fahrenheit {
    var temperature: Double

    // 초기화 메서드 정의
    init() {
        temperature = 32.0 // 기본값으로 초기화
    }

    init(celsius: Double) { // 외부에서 섭씨 온도를 받아 초기화
        temperature = celsius * 1.8 + 32.0
    }

    init(fahrenheit: Double) { // 외부에서 화씨 온도를 받아 초기화
        temperature = fahrenheit
    }
}

// 초기화 메서드 호출
let f1 = Fahrenheit() // init() 호출
print("기본 온도: \(f1.temperature)°F") // 출력: 기본 온도: 32.0°F

let f2 = Fahrenheit(celsius: 25.0) // init(celsius:) 호출
print("섭씨 25도: \(f2.temperature)°F") // 출력: 섭씨 25도: 77.0°F

let f3 = Fahrenheit(fahrenheit: 98.6) // init(fahrenheit:) 호출
print("화씨 98.6도: \(f3.temperature)°F") // 출력: 화씨 98.6도: 98.6°F

2. 매개변수 이름과 인자 레이블

초기화 메서드의 매개변수도 함수처럼 외부 매개변수 이름(인자 레이블)을 가질 수 있습니다. 기본적으로 첫 번째 매개변수를 제외하고는 매개변수 이름이 인자 레이블로 사용됩니다.

//Swift 예제
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) { // 외부에서 white라는 레이블 없이 호출 가능
        red = white
        green = white
        blue = white
    }
}

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0)
print("마젠타 색상: R=\(magenta.red), G=\(magenta.green), B=\(magenta.blue)")
// 출력: 마젠타 색상: R=1.0, G=0.0, B=1.0

let halfGray = Color(0.5) // _white 초기화 호출
print("회색 색상: R=\(halfGray.red), G=\(halfGray.green), B=\(halfGray.blue)")
// 출력: 회색 색상: R=0.5, G=0.5, B=0.5

3. 초기화 위임 (Initializer Delegation)

초기화 위임은 초기화 과정의 중복을 피하고 코드 재사용성을 높이는 강력한 방법입니다.
값 타입(구조체, 열거형): self.init()을 사용하여 동일 타입의 다른 초기화 메서드를 호출할 수 있습니다.
클래스: 더 복잡한 규칙이 적용됩니다. 지정 초기화(Designated Initializer)와 편의 초기화(Convenience Initializer)로 나뉘며, 클래스 상속과 깊은 관련이 있습니다.

3.1. 구조체의 초기화 위임

구조체는 초기화 위임을 통해 다른 init을 호출할 수 있습니다.
//Swift 예제
struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}

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) // 다른 init 호출 (위임)
    }
}

let basicRect = Rect() // init() 호출
print("기본 사각형: \(basicRect.origin), \(basicRect.size)") // 출력: 기본 사각형: Point(x: 0.0, y: 0.0), Size(width: 0.0, height: 0.0)

let customRect = Rect(origin: Point(x: 10.0, y: 10.0), size: Size(width: 50.0, height: 50.0)) // init(origin:size:) 호출
print("커스텀 사각형: \(customRect.origin), \(customRect.size)")

let centerRect = Rect(center: Point(x: 50.0, y: 50.0), size: Size(width: 100.0, height: 100.0)) // init(center:size:) 호출 -> init(origin:size:)으로 위임
print("중심으로 만든 사각형: \(centerRect.origin), \(centerRect.size)")

3.2. 클래스의 초기화 위임 (지정 초기화와 편의 초기화)

클래스 초기화는 구조체보다 복잡합니다. 지정 초기화(Designated Initializer)는 클래스의 모든 프로퍼티를 완전히 초기화하는 주된 초기화 메서드입니다. 편의 초기화(Convenience Initializer)는 지정 초기화 메서드를 호출하여 초기화 과정을 보조하는 보조 초기화 메서드입니다.

두 가지 초기화 위임 규칙:

  • 지정 초기화는 반드시 직계 수퍼클래스의 지정 초기화를 호출해야 합니다.
  • 편의 초기화는 반드시 동일 클래스의 다른 초기화 메서드를 호출해야 합니다. (결국 지정 초기화로 이어져야 함)

//Swift 예제
class Vehicle {
    var numberOfWheels: Int
    var maxPassengers: Int

    // 지정 초기화
    init(numberOfWheels: Int, maxPassengers: Int) {
        self0.numberOfWheels = numberOfWheels
        self.maxPassengers = maxPassengers
    }

    // 편의 초기화 (동일 클래스의 다른 초기화 호출)
    convenience init(singleWheelVehicle: Bool) {
        if singleWheelVehicle {
            self.init(numberOfWheels: 1, maxPassengers: 1)
        } else {
            self.init(numberOfWheels: 4, maxPassengers: 4)
        }
    }
}

class Bicycle: Vehicle {
    var hasBasket: Bool

    // 지정 초기화 (수퍼클래스의 지정 초기화 호출)
    init(hasBasket: Bool) {
        self.hasBasket = hasBasket
        super.init(numberOfWheels: 2, maxPassengers: 1) // Vehicle의 지정 초기화 호출
    }

    // 편의 초기화 (동일 클래스의 다른 초기화 호출)
    convenience init() {
        self.init(hasBasket: false) // Bicycle의 지정 초기화 호출
    }
}

let unicycle = Vehicle(singleWheelVehicle: true) // 편의 초기화 호출
print("외발 자전거 바퀴: \(unicycle.numberOfWheels), 승객: \(unicycle.maxPassengers)") // 1, 1

let myBicycle = Bicycle(hasBasket: true) // Bicycle의 지정 초기화 호출
print("내 자전거 바구니: \(myBicycle.hasBasket), 바퀴: \(myBicycle.numberOfWheels)") // true, 2

let normalBicycle = Bicycle() // Bicycle의 편의 초기화 호출
print("일반 자전거 바구니: \(normalBicycle.hasBasket), 바퀴: \(normalBicycle.numberOfWheels)") // false, 2

4. 실패 가능한 초기화 (Failable Initializers): nil을 반환할 수 있는 초기화

특정 조건이 충족되지 않으면 인스턴스 생성이 실패할 수 있는 경우가 있습니다. 예를 들어, 숫자를 Int로 변환하려는데 입력이 숫자가 아니거나, 유효하지 않은 데이터를 사용하여 객체를 만들려고 할 때입니다. 이때는 실패 가능한 초기화를 사용하며, init?로 정의합니다. 성공하면 옵셔널 인스턴스를 반환하고, 실패하면 nil을 반환합니다.
//Swift 예제
struct MenuItem {
    let name: String
    // 실패 가능한 초기화 (빈 문자열이거나 유효하지 않은 항목이면 실패)
    init?(name: String) {
        if name.isEmpty { // 이름이 비어있으면 초기화 실패
            return nil
        }
        self.name = name
    }
}

if let pizza = MenuItem(name: "피자") {
    print("메뉴 항목 생성 성공: \(pizza.name)") // 출력: 메뉴 항목 생성 성공: 피자
} else {
    print("메뉴 항목 생성 실패.")
}

if let emptyItem = MenuItem(name: "") { // 이름이 비어있으므로 nil 반환
    print("메뉴 항목 생성 성공: \(emptyItem.name)")
} else {
    print("메뉴 항목 생성 실패.") // 출력: 메뉴 항목 생성 실패.
}

5. 디이니셜라이저 (Deinitializers): 인스턴스 해제 시 정리 작업

클래스 인스턴스가 더 이상 필요 없어져 메모리에서 해제될 때, 디이니셜라이저(Deinitializer)가 호출됩니다. deinit 키워드를 사용하여 정의하며, 클래스에만 존재하고 구조체와 열거형에는 없습니다. 주로 리소스 해제(파일 닫기, 네트워크 연결 해제 등)와 같은 정리 작업을 수행할 때 사용됩니다.
//Swift 예제
class Bank {
    static var coinsInBank = 10_000 // 은행이 보유한 코인 총량
    static func distribute(coins numberOfCoinsRequested: Int) -> Int {
        let numberOfCoinsToDistribute = min(numberOfCoinsRequested, coinsInBank)
        coinsInBank -= numberOfCoinsToDistribute
        return numberOfCoinsToDistribute
    }
    static func receive(coins: Int) {
        coinsInBank += coins
    }
}

class Player {
    var coinsInPurse: Int
    init(coins: Int) {
        coinsInPurse = Bank.distribute(coins: coins)
    }
    deinit { // 플레이어 인스턴스가 해제될 때 호출
        Bank.receive(coins: coinsInPurse) // 보유 코인을 은행으로 반환
    }
}

var playerOne: Player? = Player(coins: 100) // 플레이어 생성, 100코인 받음
print("은행에 남은 코인: \(Bank.coinsInBank)") // 9900
print("플레이어1이 가진 코인: \(playerOne!.coinsInPurse)") // 100

playerOne = nil // playerOne이 nil이 되면서 인스턴스 해제, deinit 호출
print("은행에 남은 코인: \(Bank.coinsInBank)") // 10000 (코인이 다시 은행으로 돌아옴)
deinit은 수동으로 호출할 수 없으며, 인스턴스가 메모리에서 해제될 때 자동으로 호출됩니다.

정리하며

오늘은 Swift에서 인스턴스를 안전하고 효과적으로 생성하고 관리하는 초기화(Initialization)에 대해 자세히 알아보았습니다. 
  •  init 메서드: 인스턴스의 모든 저장 프로퍼티를 초깃값으로 설정하는 필수적인 과정입니다.
  • 초기화 위임: 구조체는 self.init(), 클래스는 지정 초기화와 편의 초기화를 통해 코드 중복을 줄입니다.
  • 실패 가능한 초기화(init?): 특정 조건에서 인스턴스 생성이 실패할 경우 nil을 반환합니다.
  • 디이니셜라이저(deinit): 클래스 인스턴스가 메모리에서 해제될 때 정리 작업을 수행합니다. 
초기화는 Swift의 타입 안전성을 보장하는 중요한 메커니즘입니다. 복잡한 클래스나 구조체를 만들 때는 초기화 규칙을 면밀히 검토하여 모든 프로퍼티가 유효하게 초기화되도록 주의해야 합니다. 

다음 시간에는 객체 지향 프로그래밍의 또 다른 핵심인 프로토콜(Protocol)에 대해 알아보겠습니다.

궁금한 점이 있다면 언제든지 댓글로 남겨주세요!

0 comments:

댓글 쓰기