디이니셜라이제이션
디이니셜라이제이션 (Deinitialization)
커스텀 정리가 필요한 리소스를 해제하는 방법을 다룹니다.
*디이니셜라이저(deinitializer)*는 클래스 인스턴스가 해제(deallocate)되기 직전에 자동으로 호출되는 특별한 메서드예요. 이니셜라이저를 init 키워드로 작성하는 것과 비슷하게, 디이니셜라이저는 deinit 키워드로 작성해요. 디이니셜라이저는 오직 클래스 타입에서만 사용할 수 있습니다.
본문
디이니셜라이제이션의 동작 방식 (How Deinitialization Works)
Swift는 인스턴스가 더 이상 필요 없을 때가 되면 자동으로 해제해서 리소스를 확보해요. Swift는 인스턴스의 메모리 관리를 *자동 참조 카운팅(ARC, Automatic Reference Counting)*으로 처리하는데, 이 내용은 doc:AutomaticReferenceCounting에서 자세히 다뤄요. 보통은 인스턴스가 해제될 때 직접 정리를 해줄 필요가 없습니다. 다만 여러분이 직접 관리하는 리소스를 다룰 때는 추가적인 정리를 직접 해야 하는 경우가 있어요. 예를 들어 파일을 열고 데이터를 쓰는 커스텀 클래스를 만들었다면, 클래스 인스턴스가 해제되기 전에 파일을 닫아야 할 거예요.
클래스 정의에는 클래스당 디이니셜라이저를 최대 하나만 넣을 수 있어요. 디이니셜라이저는 어떤 파라미터도 받지 않고, 괄호 없이 작성합니다:
deinit {
// perform the deinitialization
}
디이니셜라이저는 인스턴스가 해제되기 직전에 자동으로 호출돼요. 디이니셜라이저를 여러분이 직접 호출하는 것은 허용되지 않아요. 슈퍼클래스의 디이니셜라이저는 서브클래스가 상속받는데, 서브클래스 디이니셜라이저 구현의 마지막에 슈퍼클래스 디이니셜라이저가 자동으로 호출됩니다. 슈퍼클래스의 디이니셜라이저는 항상 호출돼요 — 서브클래스가 자신만의 디이니셜라이저를 제공하지 않더라도 마찬가지입니다.
인스턴스는 디이니셜라이저가 호출된 이후에야 해제되기 때문에, 디이니셜라이저는 호출되는 시점의 인스턴스가 가진 모든 프로퍼티에 접근할 수 있고, 그 프로퍼티들을 바탕으로 동작을 바꿀 수도 있어요(가령 닫아야 할 파일의 이름을 조회하는 것 같은 작업이죠).
동작하는 디이니셜라이저 (Deinitializers in Action)
디이니셜라이저가 실제로 동작하는 예시를 하나 볼게요. 이 예시는 간단한 게임을 위한 두 가지 타입, Bank와 Player를 정의합니다. Bank 클래스는 실제로 유통될 수 있는 코인이 10,000개를 넘지 않는 가상의 통화를 관리해요. 게임 안에는 Bank가 하나만 존재할 수 있기 때문에, Bank는 현재 상태를 저장하고 관리하는 타입 프로퍼티와 타입 메서드를 가진 클래스로 구현됩니다:
class Bank {
static var coinsInBank = 10_000
static func distribute(coins numberOfCoinsRequested: Int) -> Int {
let numberOfCoinsToVend = min(numberOfCoinsRequested, coinsInBank)
coinsInBank -= numberOfCoinsToVend
return numberOfCoinsToVend
}
static func receive(coins: Int) {
coinsInBank += coins
}
}
Bank는 자신이 보유한 현재 코인 수를 coinsInBank 프로퍼티로 추적합니다. 그리고 코인의 분배와 회수를 처리하는 두 메서드 — distribute(coins:)와 receive(coins:) — 도 제공해요.
distribute(coins:) 메서드는 코인을 분배하기 전에 은행에 충분한 코인이 있는지 확인합니다. 코인이 충분하지 않으면, Bank는 요청된 수보다 작은 수를 돌려줘요(그리고 은행에 남은 코인이 없다면 0을 돌려줍니다). 반환하는 정수 값은 실제로 지급된 코인 수를 나타내죠.
receive(coins:) 메서드는 단순히 받은 코인 수를 은행의 코인 저장소에 다시 더해줍니다.
Player 클래스는 게임 안의 플레이어를 묘사해요. 각 플레이어는 언제든 일정 수의 코인을 지갑에 보관합니다. 이는 플레이어의 coinsInPurse 프로퍼티로 표현됩니다:
class Player {
var coinsInPurse: Int
init(coins: Int) {
coinsInPurse = Bank.distribute(coins: coins)
}
func win(coins: Int) {
coinsInPurse += Bank.distribute(coins: coins)
}
deinit {
Bank.receive(coins: coinsInPurse)
}
}
각 Player 인스턴스는 초기화 과정에서 은행으로부터 지정된 수의 코인을 시작 자금으로 받아요. 다만 코인이 충분하지 않으면 그 수보다 적게 받을 수도 있습니다.
Player 클래스는 은행에서 일정 수의 코인을 가져와 플레이어의 지갑에 더하는 win(coins:) 메서드를 정의합니다. Player 클래스는 또 디이니셜라이저도 구현하는데, 이 디이니셜라이저는 Player 인스턴스가 해제되기 직전에 호출됩니다. 여기서 디이니셜라이저는 단순히 플레이어의 모든 코인을 은행에 돌려주는 일을 해요:
var playerOne: Player? = Player(coins: 100)
print("A new player has joined the game with \(playerOne!.coinsInPurse) coins")
// Prints "A new player has joined the game with 100 coins".
print("There are now \(Bank.coinsInBank) coins left in the bank")
// Prints "There are now 9900 coins left in the bank".
새 Player 인스턴스가 생성되는데, 가능하면 100코인을 요청합니다. 이 Player 인스턴스는 playerOne이라는 옵셔널 Player 변수에 저장됩니다. 플레이어는 언제든 게임을 떠날 수 있기 때문에 여기서는 옵셔널 변수를 사용해요. 이 옵셔널 덕분에 현재 게임 안에 플레이어가 있는지 여부를 추적할 수 있습니다.
playerOne이 옵셔널이기 때문에, 기본 코인 수를 출력하기 위해 coinsInPurse 프로퍼티에 접근할 때도, win(coins:) 메서드를 호출할 때도 느낌표(!)를 붙여 강제로 언랩해요:
playerOne!.win(coins: 2_000)
print("PlayerOne won 2000 coins & now has \(playerOne!.coinsInPurse) coins")
// Prints "PlayerOne won 2000 coins & now has 2100 coins".
print("The bank now only has \(Bank.coinsInBank) coins left")
// Prints "The bank now only has 7900 coins left".
여기서 플레이어는 2,000코인을 따냈어요. 플레이어의 지갑에는 이제 2,100코인이 들어 있고, 은행에는 7,900코인만 남았습니다.
playerOne = nil
print("PlayerOne has left the game")
// Prints "PlayerOne has left the game".
print("The bank now has \(Bank.coinsInBank) coins")
// Prints "The bank now has 10000 coins".
이제 플레이어가 게임을 떠났습니다. 이는 옵셔널인 playerOne 변수를 nil로 설정해서 나타내는데, “Player 인스턴스가 없다”는 뜻이에요. 이 일이 일어나는 시점에 playerOne 변수의 Player 인스턴스에 대한 참조가 끊어집니다. 다른 프로퍼티나 변수가 더 이상 Player 인스턴스를 참조하지 않기 때문에, 메모리를 확보하기 위해 인스턴스가 해제됩니다. 그 직전에 디이니셜라이저가 자동으로 호출되고, 플레이어의 코인이 은행에 돌아가게 돼요.
더 알아보기
- ARCC가 어떻게 인스턴스의 메모리 관리를 처리하는지 자세히 알고 싶다면, 자동 참조 카운팅(Automatic Reference Counting) 챕터를 참고하세요.
- 클래스가 초기화되는 시점에 호출되는 이니셜라이저의 모든 규칙은 초기화(Initialization) 챕터를 참고하세요.
- 이 문서의 코드를 Swift 플레이그라운드에 순서대로 붙여 넣어 직접 실행해 보면, 디이니셜라이저가 호출되는 흐름을 눈으로 확인할 수 있어요.