TIL: 중첩 타입(Nested Type)

Royce·2025년 3월 29일

Swift 문법

목록 보기
56/63

중첩 타입(Nested Type)

  • Swift에서는 타입 내부에 다른 타입을 선언하는 것이 가능하다
  • 이러한 방식을 중첩 타입(Nested Types) 이라고 하며, 특정 타입 내에서만 사용되는 타입을 정의할 때 유용하다

중첩 타입의 사용 목적

  • 특정 타입과 밀접하게 관련된 타입을 내부에서 정의하여 범위를 명확히 할 수 있다
  • 타입 간의 연관성을 명확히 표현하고, 내부 구조를 더욱 디테일하게 설계할 수 있다
  • 코드의 가독성을 높이고, 구조를 더 잘 이해할 수 있도록 도와준다
  • 외부에서 사용할 때 중첩된 경로를 사용해야 하므로, 의미가 명확해진다
  • 문자열 실수 방지 및 데이터 관리 용이성을 제공한다

중첩 타입을 사용하지 않은 경우(타입의 관계가 명확하지 않다)

// 중첩 타입을 사용하지 않은 경우 (전역 선언)
enum Style {
    case full
    case long
    case medium
    case none
}

struct DateFormatters {
    var style: Style
}

var dateStyle = DateFormatters(style: .full)
dateStyle.style = .medium

중첩 타입을 사용한 경우(타입의 관계가 명확해진다)

// 중첩 타입을 사용하여 정의한 구조체
struct DateFormatters {
    enum Style {
        case full
        case long
        case medium
        case none
    }
    
    var style: Style
}

// 인스턴스 생성 (전체 경로를 사용하여 스타일을 지정)
var dateStyle = DateFormatters(style: DateFormatters.Style.full)
dateStyle.style = .medium   // 내부에서 사용할 때는 .medium으로 사용 가능
  1. 중첩 타입을 사용하지 않은 경우 (전역 선언)
    • Style 열거형이 전역 선언되었기 때문에, 사용자가 어느 타입과 관련된 스타일인지 정확히 알기 어렵다
    • 예를 들어, Style.full 이라고 한다면, 어떤 용도로 사용되는 스타일인지 알기 어렵다
  2. 중첩 타입을 사용한 경우
    • DateFormatters.Style 로 사용하게 되면, 이 스타일이 DateFormatters 와 관련된 것임을 명확히 알 수 있다
    • 외부에서 사용하려면 반드시 전체 경로(DateFormatters.Style.full)를 지정해야 하기 때문에 의미가 명확해진다
    • 타입 간의 관계를 명확히 표현할 수 있고, 코드의 의미도 더 분명해진다

중첩 타입을 배우는 목적

  1. 중첩 타입으로 선언된 API들을 볼 줄 알아야 한다
    • 예시: DateFormatter.Style.full
    • 중간에 대문자가 나오면, 중첩 타입임을 인지해야 한다
  2. 실제 앱을 만들 때 중첩 선언을 잘 활용해야 한다
    • 관련된 타입을 내부에 선언함으로써 타입 간의 관계를 명확히 표현한다
    • 코드를 더욱 깔끔하고 이해하기 쉽게 작성할 수 있다
  3. 하나의 타입의 내부 구조 (계층 관계 등)를 디테일하게 설계할 수 있다
    • 내부의 타입은 외부로부터의 접근을 제한하면서도 의미 있는 관계를 유지할 수 있다

간단한 중첩 타입 예제

// 동물원 클래스를 정의
class Zoo {
    
    // 동물 구조체를 내부에 정의 (Zoo에 속하는 동물 타입)
    struct Animal {
        
        // 동물의 종류를 열거형으로 정의
        enum AnimalType {
            case mammal, bird, reptile, amphibian, fish, insect
        }
        
        // 동물의 속성 정의
        var name: String
        var type: AnimalType
    }
}

// Zoo 클래스의 인스턴스를 생성
let myZoo = Zoo()

// Zoo 내부의 Animal 타입을 사용하여 동물 객체 생성
let lion = Zoo.Animal(name: "Lion", type: .mammal)
let parrot = Zoo.Animal(name: "Parrot", type: .bird)

// 출력 예제
print("동물 이름: \(lion.name), 종류: \(lion.type)")
print("동물 이름: \(parrot.name), 종류: \(parrot.type)")
  • Zoo 클래스 내부에 Animal 구조체가 정의되었다
  • Animal 구조체 내부에는 AnimalType 이라는 열거형이 정의되었다
  • 외부에서 Animal 구조체와 AnimalType 열거형을 사용하기 위해서는 전체 경로를 사용해야 한다 (Zoo.Animal, Zoo.Animal.AnimalType)
  • 중첩 타입을 사용하면, 타입 간의 관계를 더욱 명확히 표현할 수 있다

복잡한 중첩 타입 예제

// 게임 캐릭터 클래스 정의
class GameCharacter {
    
    // 무기 타입을 정의하는 열거형 (WeaponType)
    enum WeaponType {
        case sword, bow, staff
        
        // 무기 타입별 설명을 제공하는 계산 속성
        var description: String {
            switch self {
            case .sword: return "검 (근접 공격)"
            case .bow: return "활 (원거리 공격)"
            case .staff: return "지팡이 (마법 공격)"
            }
        }
    }
    
    // 무기 구조체 정의 (Weapon)
    struct Weapon {
        
        // 무기 레벨을 정의하는 구조체 (Level)
        struct Level {
            let value: Int
            
            // 레벨에 따른 공격력 계산 (계산 속성)
            var attackPower: Int {
                return value * 10  // 레벨당 공격력 10씩 증가
            }
        }
        
        // 무기의 속성
        var type: WeaponType
        var level: Level
        
        // 무기의 정보를 출력하는 메서드
        func weaponInfo() -> String {
            return "무기 타입: \(type.description), 레벨: \(level.value), 공격력: \(level.attackPower)"
        }
    }
    
    // 캐릭터의 속성
    var name: String
    var equippedWeapon: Weapon?
    
    // 생성자
    init(name: String) {
        self.name = name
    }
    
    // 무기 장착 메서드
    func equip(weapon: Weapon) {
        self.equippedWeapon = weapon
        print("\(name)이(가) \(weapon.type.description)을(를) 장착했습니다.")
    }
    
    // 공격 메서드
    func attack() {
        if let weapon = equippedWeapon {
            print("\(name)이(가) 공격합니다! 공격력: \(weapon.level.attackPower)")
        } else {
            print("\(name)은(는) 무기가 없습니다.")
        }
    }
}
  1. GameCharacter 클래스(최상위 타입)
    • 게임 캐릭터를 표현하며, 이름과 장착된 무기를 속성으로 가진다
    • equip(weapon:) 메서드를 통해 무기를 장착하고, attack() 메서드를 통해 공격을 수행한다
  2. WeaponType 열거형(중첩 타입)
    • 무기의 종류를 정의하며, 각 타입별 설명을 제공하는 계산 속성(description)이 포함되어 있다
  3. Weapon 구조체(중첩 타입)
    • 무기의 타입(type)과 레벨(level)을 속성으로 가진다
    • weaponInfo() 메서드를 통해 무기의 정보를 출력한다
  4. Level 구조체(중첩 타입)
    • 무기의 레벨을 표현하며, 레벨에 따른 공격력을 계산한다(attackPower)
  5. 경로 사용의 중요성
    • 외부에서 사용하려면 전체 경로를 지정해야 한다(GameCharacter.Weapon, GameCharacter.Weapon.Level)
    • 경로를 명확히 지정함으로써 의미를 분명히 할 수 있다

사용 예제 (코드 실행 예제)

// 새로운 캐릭터 생성
let hero = GameCharacter(name: "Knight")

// 무기 생성 (검, 레벨 5)
let sword = GameCharacter.Weapon(type: .sword, level: GameCharacter.Weapon.Level(value: 5))

// 무기 장착
hero.equip(weapon: sword)  // Knight이(가) 검 (근접 공격)을(를) 장착했습니다.

// 캐릭터 공격
hero.attack()  // Knight이(가) 공격합니다! 공격력: 50

실제 API의 사용 예시(중첩 타입 활용 - DateFormatter)

  • Swift의 표준 라이브러리와 Apple의 프레임워크에서는 중첩 타입을 광범위하게 사용한다
  • 특히 DateFormatter 클래스는 Style 이라는 중첩 타입을 사용하여 날짜와 시간을 관리한다
import Foundation

// DateFormatter 인스턴스 생성
let formatter = DateFormatter()

// DateFormatter에서 중첩 타입 사용하기 (Style)
formatter.dateStyle = .full         // .full은 DateFormatter.Style.full을 의미한다
formatter.dateStyle = DateFormatter.Style.medium  // 명확하게 전체 경로를 사용하여 타입을 지정

// 타입 확인을 위한 예제
let setting1: DateFormatter.Style = DateFormatter.Style.full
let setting2: DateFormatter.Style = DateFormatter.Style.short

// 출력하기 (DateFormatter의 중첩 타입 확인)
print(setting1)  // 출력: full
print(setting2)  // 출력: short
  1. DateFormatter 클래스
    • 날짜와 시간을 문자열로 변환하거나, 문자열을 날짜와 시간으로 변환할 때 사용되는 클래스이다
  2. 중첩 타입(Style 열거형)
    • DateFormatter 클래스 내부에 정의된 중첩 타입으로, 날짜와 시간을 표시하는 스타일을 정의한다
    • 사용법: DateFormatter.Style(하지만 DateFormatter 내부에서 사용하는 경우 .full, .medium 등으로 줄여서 사용 가능)
  3. 사용법의 차이점
    • .full 처럼 사용하면: DateFormatter 클래스 내부의 스타일을 의미(경로를 생략할 수 있다)
    • DateFormatter.Style.full 처럼 사용하면: 타입을 정확하게 지정하는 방식(더 명확한 표현)
  4. 이점
    • 외부에서 사용하기 위해서는 DateFormatter.Style 이라는 경로를 명확히 지정해야 한다
    • 중첩 타입을 사용함으로써, 관련 타입 간의 관계를 명확히 표현할 수 있다

사용자 정의 중첩 타입 예제(문자열 관리)

  • 앱에서 자주 사용하는 문자열들을 별도의 파일에서 관리하는 대신, 중첩 타입으로 관리하면 더욱 안전하게 사용할 수 있다
// 앱 전역에서 사용되는 상수들을 모아 관리하는 구조체
struct AppConstants {
    
    // 앱 정보 (앱 이름, 버전 등)
    struct Info {
        static let appName = "MyAmazingApp"
        static let version = "1.0.0"
    }
    
    // 색상 정보 (브랜드 컬러)
    struct Colors {
        static let red = "ThemeRed"
        static let green = "ThemeGreen"
        static let blue = "ThemeBlue"
    }
    
    // Firebase 데이터베이스 관련 상수
    struct Database {
        static let collectionName = "users"
        static let userField = "username"
        static let messageField = "message"
        static let timestampField = "timestamp"
    }
    
    // 화면 전환 식별자 (Segue Identifiers)
    struct Segues {
        static let loginSegue = "LoginToMain"
        static let registerSegue = "RegisterToMain"
    }
}

// 예제 코드 (문자열 사용하기)
let appName = AppConstants.Info.appName  // "MyAmazingApp"
let themeColor = AppConstants.Colors.blue  // "ThemeBlue"
let collection = AppConstants.Database.collectionName  // "users"
let loginSegue = AppConstants.Segues.loginSegue  // "LoginToMain"

// 출력 예제
print(appName)
print(themeColor)
print(collection)
print(loginSegue)
  • AppConstants 구조체는 앱의 전역 상수들을 관리하기 위해 사용되는 상위 타입이다
  • Info, Colors, Database, Segues 와 같은 중첩 타입으로 구성되어 있다
  • 문자열을 상수로 관리하여 오타로 인한 오류를 방지하고, 코드의 의미를 명확히 표현할 수 있다
  • 중첩 타입을 사용하여 관련 있는 데이터를 그룹화함으로써 코드의 유지보수성을 높일 수 있다

메시지 모델 설계(메신저 앱의 메시지 관리)

  • 메신저 앱에서 메시지를 관리할 때, 상태 정보와 데이터를 중첩 타입으로 관리할 수 있다
import UIKit

// 메세지를 관리하는 클래스
class ChatMessage {
    
    // 메시지 상태를 나타내는 열거형 (읽기 상태 관리)
    private enum Status {
        case sent
        case delivered
        case read
    }
    
    // 메시지의 정보 (보낸 사람, 내용, 시간)
    let sender: String
    let content: String
    let timestamp: Date
    private var status: Status = .sent  // 기본 상태는 'sent'
    
    init(sender: String, content: String) {
        self.sender = sender
        self.content = content
        self.timestamp = Date()
    }
    
    // 메세지 정보 표시
    func displayMessageInfo() -> String {
        return "보낸 사람: \(sender), 내용: \(content), 시간: \(timestamp)"
    }
    
    // 상태에 따른 색상 반환
    func statusColor() -> UIColor {
        switch status {
        case .sent: return .gray
        case .delivered: return .blue
        case .read: return .green
        }
    }
    
    // 상태 업데이트 메서드
    func updateStatus(to newStatus: Status) {
        self.status = newStatus
    }
}

// 새로운 메시지 생성
let message = ChatMessage(sender: "Alice", content: "안녕하세요?")
print(message.displayMessageInfo())
  • ChatMessage 클래스는 메시지 정보를 관리하기 위한 타입이다
  • Status 라는 중첩 열거형을 사용하여 메시지의 상태를 관리한다(sent, delivered, read)
  • displayMessageInfo() 메서드는 메시지의 기본 정보를 반환한다
  • statusColor() 메서드는 메시지의 상태에 따라 다른 색상을 반환한다(UIColor)
  • updateStatus() 메서드를 사용하여 메시지의 상태를 업데이트할 수 있다

중첩 타입 사용의 이유

  • 타입 간의 관계를 명확히 표현할 수 있다(캡슐화)
  • 코드의 의미가 분명해진다(경로를 사용하여 타입을 구분)
  • 확장성 (Extensibility) - 새로운 타입을 추가하거나 수정할 때 쉽게 변경 가능
  • 코드의 가독성과 유지보수성이 향상된다(특정 타입 내부에서만 사용하는 타입 관리)
  • Swift 표준 라이브러리에서도 널리 사용된다(DateFormatter.Style, URLSession.Task 등)

요약

  • 특정 타입 내에서만 사용되는 타입을 정의하여 범위를 명확히 지정할 수 있다
  • 타입 간의 관계를 더욱 명확하게 표현할 수 있다
  • 외부에서 사용할 때 경로를 모두 표시해야 하므로 의미가 명확해진다
  • 코드의 가독성과 유지보수성이 향상된다
profile
iOS 개발자 지망생

0개의 댓글