ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Nonexhaustive enums - Swift 6.2.3
    Swift 2025. 12. 20. 10:08

    안녕하세요! 그린입니다 🍏

    이번 포스팅에서는 Nonexhaustive enums에 대해 알아보겠습니다 🙋🏻

     

    Swift 6.2.3에 정말 중요한 기능이 추가됩니다.

     

    SE-0487: Extensible Enums!

     

    지금까지 Swift 패키지를 만들 때 enum에 case를 추가하면 breaking change가 발생하죠?

    이제 그 문제가 해결될 수 있습니다.

     


    AS-IS Problem

    // Version 1.0
    public enum HTTPStatus {
        case ok, notFound, serverError
    }
    
    // Version 1.1 - 새 case 추가
    public enum HTTPStatus {
        case ok, notFound, serverError
        case earlyHints  // 🆕 Breaking change!
    }

     

    이럴 경우 사용자 코드의 switch문이 깨지는 현상이 발생하죠.

    // 사용자 코드
    switch status {
    case .ok: print("OK")
    case .notFound: print("Not found")
    case .serverError: print("Error")
    // earlyHints 없음 → 💥 크래시!
    }

    요렇게 사용 시 크래시가 발생하는거 너무 잘 아실거에요.

     
    // Struct로 변경
    public struct HTTPStatus {
        public let rawValue: Int
        public static let ok = HTTPStatus(rawValue: 200)
        public static let notFound = HTTPStatus(rawValue: 404)
    }
    
    // 문제점:
    // - Switch의 exhaustive 체크 불가
    // - Associated values 불가
    // - Enum의 장점 상실

     

    그래서 이런게 싫다면 이렇게 우회해서 사용하지만 좋은 패턴은 아니였어요.

     

    그래서 이게 나왔습니다!

    @nonexhaustive

    @nonexhaustive
    public enum HTTPStatus {
        case ok, notFound, serverError
    }
    
    // 사용자 코드
    switch status {
    case .ok: print("OK")
    case .notFound: print("Not found")
    case .serverError: print("Error")
    @unknown default:  // 필수!
        print("Unknown")
    }
    
    // 나중에 안전하게 추가 가능!
    @nonexhaustive
    public enum HTTPStatus {
        case ok, notFound, serverError
        case earlyHints  // ✅ Breaking change 아님!
    }

     

    이제는 @nonexhaustive를 사용한다면 나이스하게 해결하고 확장 할 수 있습니다 😃


    @unknown default

    @nonexhaustive
    public enum Animal {
        case cat, dog
    }
    
    // ❌ 일반 default
    switch animal {
    case .cat: print("Cat")
    default: print("Other")  // ⚠️ dog를 빠뜨려도 경고 없음
    }
    
    // ✅ @unknown default
    switch animal {
    case .cat: print("Cat")
    @unknown default: print("Other")  
    // ⚠️ 컴파일러 경고: 'dog' case가 처리되지 않았습니다!
    }

    @unknown default는 현재 알려진 case + 미래의 알려지지 않은 case를 명시할 수 있도록 해줍니다.

     


    Same Module/Package Rule

    핵심은 같은 패키지 안에서는 exhaustive switch가 가능하다는점이에요.

    // MyLibrary 패키지
    @nonexhaustive
    public enum Animal {
        case cat, dog
    }
    
    // 같은 패키지 안
    func describe(_ animal: Animal) -> String {
        switch animal {
        case .cat: return "Meow"
        case .dog: return "Woof"
        // ✅ @unknown default 필요 없음!
        }
    }
    
    // 외부 패키지
    import MyLibrary
    
    func handle(_ animal: Animal) {
        switch animal {
        case .cat: print("Cat")
        case .dog: print("Dog")
        // ❌ 에러: @unknown default 필요!
        }
    }

    같은 패키지는 함께 개발되므로 모든 case를 알고 있다고 볼 수 있어서가 이유가 될 수 있습니다.

     

    그럼 우리는 어떻게 이걸 마이그레이션 해볼 수 있을까요?

     


    How do we Migrate?

    // Version 2.0 - 경고로 시작
    @nonexhaustive(warn)
    public enum FileError: Error {
        case notFound, permissionDenied
    }
    
    // 사용자 코드
    switch error {
    case .notFound: handle()
    case .permissionDenied: handle()
    // ⚠️ 경고: 곧 @unknown default가 필요합니다
    }
    
    // Version 3.0 - 에러로 전환
    @nonexhaustive  // (warn) 제거
    public enum FileError: Error {
        case notFound, permissionDenied
        case diskFull  // 🆕 안전하게 추가
    }

     

    이런 스텝으로 점진적으로 마이그레이션을 해보면 좋을것 같습니다.

    애플에서도 warn을 사용하라고 권고하고 있구요.

    1. Version N: @nonexhaustive(warn) 추가 → 사용자에게 경고
    2. Version N+1: @nonexhaustive 전환 → 에러 발생
    3. 이후: 자유롭게 case 추가

     

    그럼 이거 언제 사용하는게 적절할까요?

    When we use this?

    // 1. 외부 스펙 기반
    @nonexhaustive
    enum HTTPMethod {
        case get, post, put, delete
    }
    
    // 2. 라이브러리 에러
    @nonexhaustive
    enum NetworkError: Error {
        case timeout, noConnection
    }
    
    // 3. 확장 가능한 비즈니스 로직
    @nonexhaustive
    enum PaymentStatus {
        case pending, completed, failed
    }

    이런 경우 사용하기 좋을것 같아요.

     
    // 1. 고정된 값
    enum Bool {
        case true, false
    }
    
    // 2. 앱 내부 전용
    enum ViewState {
        case loading, loaded, error
    }
    
    // 3. 수학적으로 완결된 집합
    enum Comparison {
        case less, equal, greater
    }

    반면 이런 케이스에서는 사용하는것이 적절하지 않아요.


    @extensible → @nonexhaustive

    @extensible로 하지 않고 왜 해당 네이밍으로 하지 않았을까요?

     

    1. Extension과 혼동

    @extensible enum MyEnum { }
    extension MyEnum { }  // 🤔 관련 있나?

     

    2. 미래의 진짜 "extensible enum"과 충돌

    // 미래의 가능성?
    enum Foo { case bar }
    extension Foo { case baz }  // 다른 모듈에서 case 추가

     

    3. Objective-C NS_TYPED_EXTENSIBLE_ENUM과 혼동

    @nonexhaustive가 더 나은 이유

    • 목적이 명확: "exhaustive switch 불가"
    • 기존 용어와 일관성: "exhaustive switch", "@unknown default"
    • Rust의 #[non_exhaustive]와 유사

    Performance

    해당 사용은 좋은점이 성능의 영향이 없습니다.
    @nonexhaustive
    enum MyEnum { case a, b, c }

     

    Non-Resilient 패키지에서

    • 사용자 코드와 함께 컴파일
    • 모든 case가 컴파일 타임에 알려짐
    • 완전히 최적화됨
    • 성능 차이 없음

    Resilient 패키지 (Foundation 등)와의 차이

    • 런타임 case 체크 필요
    • Indirect call
    • 최적화 제한

    Future Directions

    1. Access Control 기반

    @nonexhaustive(beyond: fileprivate)
    enum Animal { case cat, dog, bird }
    
    // 같은 파일: exhaustive 가능
    // 다른 파일: @unknown default 필요

     

    2. Typed Throws 통합

    @nonexhaustive
    enum FileError: Error { case notFound, permissionDenied }
    
    do {
        try readFile()
    } catch .notFound {
        // handle
    } @unknown catch {
        // 미래의 에러
    }

     


    Conclusion

    SE-0487 Extensible Enums는 Swift Package 생태계의 체인저가 될 수 있을것 같습니다.

    핵심 요약을 해볼까요?

    1️⃣ @nonexhaustive: 확장 가능한 enum 선언
    2️⃣ @unknown default: 안전한 처리
    3️⃣ 같은 모듈/패키지: exhaustive 유지
    4️⃣ @nonexhaustive(warn): 점진적 전환
    5️⃣ 성능 영향 없음

     

    이럴 때 사용을 권장합니다.

    ✅ 외부 스펙, 에러 타입, 확장 가능한 로직
    ❌ 고정값, 앱 내부 전용

     


    References

     

    swift-evolution/proposals/0487-extensible-enums.md at main · swiftlang/swift-evolution

    This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution

    github.com

Designed by Tistory.