중첩 타입
중첩 타입 (Nested Types)
타입 안에 다른 타입을 정의해요. 같은 목적을 위해 서로 얽힌 타입들을 한곳에 모아두면, 관련 있는 정의들이 한눈에 보여서 코드가 훨씬 정돈돼요.
보통 열거형(enumeration)은 특정 클래스나 구조체의 기능을 보조하려고 만들어지는 경우가 많아요. 마찬가지로, 더 복잡한 타입의 맥락 안에서만 쓰려고 만든 유틸리티 구조체나, 특정 타입과 함께 쓰이는 프로토콜을 정의하기 편하죠. Swift는 이런 목적을 위해 중첩 타입(nested types) 을 지원해요. 말 그대로 열거형, 구조체, 프로토콜 같은 보조 타입들을, 그 타입이 보조하는 대상의 정의 안에 중첩해서 넣는 방식이에요.
타입을 다른 타입 안에 중첩하려면, 보조 대상 타입의 바깥 중괄호 안에 그 정의를 쓰면 돼요. 타입은 필요한 만큼 몇 단계든 중첩할 수 있어요.
본문
작동하는 중첩 타입 (Nested Types in Action)
구체적으로 어떻게 쓰는지, 블랙잭 게임에서 쓰는 카드 한 장을 모델링한 BlackjackCard 구조체 예시로 볼게요. 이 구조체 안에는 Suit와 Rank라는 두 개의 중첩 열거형이 들어 있어요.
블랙잭에서 에이스(Ace) 카드는 값이 1 또는 11이 될 수 있어요. 이 성질은 Rank 열거형 안에 중첩된 Values라는 구조체로 표현해요.
struct BlackjackCard {
// nested Suit enumeration
enum Suit: Character {
case spades = "♠", hearts = "♡", diamonds = "♢", clubs = "♣"
}
// nested Rank enumeration
enum Rank: Int {
case two = 2, three, four, five, six, seven, eight, nine, ten
case jack, queen, king, ace
struct Values {
let first: Int, second: Int?
}
var values: Values {
switch self {
case .ace:
return Values(first: 1, second: 11)
case .jack, .queen, .king:
return Values(first: 10, second: nil)
default:
return Values(first: self.rawValue, second: nil)
}
}
}
// BlackjackCard properties and methods
let rank: Rank, suit: Suit
var description: String {
var output = "suit is \(suit.rawValue),"
output += " value is \(rank.values.first)"
if let second = rank.values.second {
output += " or \(second)"
}
return output
}
}
Suit 열거형은 네 가지 일반적인 카드 무늬를, 각 무늬를 나타내는 원시 Character 값과 함께 정의해요. Rank 열거형은 열세 가지 가능한 카드 등급을, 등급의 숫자 값을 나타내는 원시 Int 값과 함께 정의해요. (이 원시 Int 값은 Jack, Queen, King, Ace에는 쓰이지 않아요.)
앞서 말했듯 Rank 열거형은 자기 안에 다시 Values라는 중첩 구조체를 정의해요. 대부분의 카드는 값이 하나지만 에이스는 값이 두 개라는 사실을 이 구조체 하나로 깔끔하게 담아냈죠. 이 값 두 개를 나타내는 프로퍼티가 Values 구조체에 정의돼 있어요.
first— 타입은Intsecond— 타입은Int?, 또는 "옵셔널Int"
Rank는 또 Values 구조체의 인스턴스를 반환하는 계산 프로퍼티 values도 정의해요. 이 계산 프로퍼티는 카드의 등급을 보고 그에 맞는 값으로 새 Values 인스턴스를 만들어 초기화해요. jack, queen, king, ace에는 특별한 값을 쓰고, 숫자 카드에는 그 등급의 원시 Int 값을 써요.
BlackjackCard 구조체는 rank와 suit 두 프로퍼티를 가져요. 그리고 rank와 suit에 저장된 값을 이용해 카드의 이름과 값을 설명하는 문자열을 만드는 계산 프로퍼티 description도 정의해요. 이 프로퍼티는 옵셔널 바인딩으로 두 번째 값이 표시될 게 있는지 확인하고, 있으면 그 두 번째 값에 대한 설명을 추가로 붙여요.
BlackjackCard는 커스텀 이니셜라이저가 없는 구조체라서, 멤버와이즈 이니셜라이저(memberwise initializer)가 암시적으로 제공돼요. 이 이니셜라이저를 이용해 theAceOfSpades라는 새 상수를 만들 수 있어요.
let theAceOfSpades = BlackjackCard(rank: .ace, suit: .spades)
print("theAceOfSpades: \(theAceOfSpades.description)")
// Prints "theAceOfSpades: suit is ♠, value is 1 or 11".
Rank와 Suit가 BlackjackCard 안에 중첩돼 있더라도, 타입은 문맥에서 추론할 수 있어요. 그래서 이 인스턴스를 만들 때 열거형 케이스를 케이스 이름(.ace, .spades)만으로 가리킬 수 있죠. 위 예시에서 description 프로퍼티는 스페이드 에이스의 값이 1 또는 11이라고 정확히 알려줘요.
중첩 타입 참조하기 (Referring to Nested Types)
중첩 타입을 정의된 맥락 밖에서 쓰려면, 타입 이름 앞에 그 타입이 중첩돼 있는 타입의 이름을 붙여줘요.
let heartsSymbol = BlackjackCard.Suit.hearts.rawValue
// heartsSymbol is "♡"
이렇게 하면 Suit, Rank, Values의 이름을 의도적으로 짧게 유지할 수 있어요. 이름이 정의된 맥락에 의해 자연스럽게 구분되니까요.
더 알아보기
- 구조체 타입의 멤버와이즈 이니셜라이저에 대해서는 Initialization 챕터를 같이 보면 좋아요.
- 열거형이 어색하다면, 위 예시도 함께 보면서 중첩된 열거형이 실제 코드에서 어떻게 쓰이는지 다시 확인해 보세요.