Swift: @frozen

틀틀보·2025년 11월 30일

Swift

목록 보기
18/19

이 타입의 레이아웃이 앞으로 변하지 않을 거라는 걸 알려주어 성능을 최적화해주는 속성

@frozen

  • struct enum 에만 적용 가능

  • Library Evolution 모드가 켜진 상태에서 ABI 안정성과 컴파일러 최적화에 사용

  • Library Evolution 모드가 켜진 상태에서 의미가 있는 속성

  • 라이브러리 개발 시에 사용됨.

  • 해당 속성이 적용되면 향후 버전에서 프로퍼티나 케이스 추가 불가 (but 버전 업데이트 시 가능)

  • 컴파일러가 해당 타입의 메모리 레이아웃이 현재 모습으로 고정됨을 알고 있으므로, 해당 타입의 프로퍼티 또는 케이스 접근 시, 프로퍼티를 천천히 찾지 않고 크기에 맞춰 "이 프로퍼티는 바로 여기서 4번째에 있겠네" 추론하여 성능 최적화

// 라이브러리 측
import Foundation

protocol ShapeRenderable {
    func renderDescription(for shape: FixedShape) -> String
}

@frozen public enum FixedShape {
    case circle(radius: Double)
    case square(side: Double)
    case point
    
    // @frozen이므로 향후 버전에 'triangle' 같은 케이스 추가에 유의
}

// 클라이언트 측
final class DefaultShapeRenderer: ShapeRenderable {
    
    func renderDescription(for shape: FixedShape) -> String {
        // 컴파일러는 이 switch가 100% 완전함 보장
        switch shape {
        case .circle(let radius):
            return "Rendered Circle with radius: \(radius)"
        case .square(let side):
            return "Rendered Square with side: \(side)"
        case .point:
            return "Rendered Point at origin"
        }
    }
}

Library Evolution

Xcode 빌드 세팅에서 Build Libraries for Distribution 옵션

미적용 시

  • .swiftmodule 바이너리 파일 생성
  • 해당 파일은 컴파일러 버전에 종속
  • Swift 5.9로 빌드된 라이브러리는 Swift 6.0에서는 해석 불가

적용 시

  • .swiftinterface 텍스트 파일 생성
  • 컴파일러 버전에 종속 X
  • 구체적인 구현 내용은 숨기고 껍데기만 표시 (변수명, 함수명, 타입 정보 등)

동작

  • 컴파일러가 라이브러리의 .swiftmodule 파일 읽기
  • 해당 타입의 @frozen 속성 확인
  • "이거 고정이구나 최적화 해야지" 판단
  • 없으면 하던대로 동적 디스패치로 처리 (Witness Table 접근)
  • 최초의 컴파일로 최적화 후에 새로운 라이브러리 적용 후 다시 컴파일 하지 않으면 에러 발생 가능성

라이브러리가 업데이트 될 때는?

  • 흔히 앱스토어에 배포되는 앱에 들어가는 라이브러리는 APP Bundle에 빌드된 버전으로 포장되어 들어감.

  • 그러므로 Alamofire와 같은 오픈 소스 라이브러리가 업데이트되어도 상관 X

  • 단, 시스템 라이브러리에는 상관이 있음.

  • UIKit, SwiftUI, Foundation과 같이 Apple 자체에 OS 업데이트 때 같이 라이브러리가 업데이트되면?

  • 해당 앱에서 사용되던 UIKit이 ver1 쓰다가 ver2를 쓰게 됨!

해결 전략

  • 새로운 타입: @frozen 타입 안 건드리고 새로운 기능을 담은 새로운 타입을 정의

  • 라이브러리 버전 올리기: (1.2.0 -> 2.0.0) 변경하되면 Xcode가 이를 감지하고 전체 재컴파일

  • Extension 사용(struct 한정): 구조체에서 저장 프로퍼티만 수정 불가. 계산 프로퍼티나 메서드는 자유롭게 추가 가능

추가적인 속성

@unknown default

switch 문으로 각 case를 처리할 때, 특정 case를 처리하지 않으면 컴파일 경고로 알려주는 속성

final class ServerStatusManager: StatusHandlable {
    
    func handleStatus(_ status: ServerStatus) -> String {
        switch status {
        case .ok:
            return "Normal Operation"
            
        case .maintenance:
            return "Server is under maintenance"
            
        case .error(let code):
            return "Error occurred: \(code)"
            
        @unknown default:
            // 미래에 라이브러리가 업데이트되어 새로운 case가 생겼을 때 경고
            return "Unknown Status Detected - Please Update App"
        }
    }
}

switch 문에 사용되는 Enumcaseswitch 문에 사용되지 않은 case가 존재하면 컴파일 경고로 알려줌.

ABI 안정성

서로 다른 시점, 버전의 Swift 컴파일러로 빌드된 바이너리 파일들이 런타임에 서로 문제없이 소통할 수 있는 상태

  • ABI 안정성이 없던 시절에는 앱마다 각자의 Swift 컴파일러를 들고 다녀 코드를 실행했어야 했음.

  • 그 결과로 앱 용량, 메모리 사용 ⬆️

  • ABI 안정성이 생긴 후, 각 앱들은 어떤 버전의 컴파일러를 가지고 있든 동일한 바이너리 파일 생성

  • 아이폰에 내장된 공용 Swift 런타임을 모든 앱이 공유

API vs ABI

API (Application Programming Interface)

  • 레벨: 소스 코드 (Source Code) 단계의 약속

  • 상황: "내가 함수 이름을 func login()이라고 지었으니, 너도 코드 짤 때 login()이라고 적어야 해."

  • 확인 시점: 컴파일 타임 (오타 나면 빌드 에러)

ABI (Application Binary Interface)

  • 레벨: 기계어 (Binary) 단계의 약속

  • 상황: "내 메모리의 첫 8바이트는 Int이고, 그다음 레지스터 x0에 값을 넣어서 함수를 호출할 거야."

  • 확인 시점: 런타임 (안 맞으면 앱 크래시)

참고
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0260-library-evolution.md

https://github.com/swiftlang/swift-evolution/blob/main/proposals/0192-non-exhaustive-enums.md

https://www.swift.org/blog/abi-stability-and-more/

https://developer.apple.com/videos/play/wwdc2019/416/

profile
안녕하세요! iOS 개발자입니다!

0개의 댓글