Swift/개념 & 응용

[Swift] OptionSet 알아보기

유정주 2023. 9. 11. 20:23
반응형

OptionSet

OptionSet은 비트 마스크를 이용해 여러 옵션을 다룰 때 채택하는 프로토콜입니다.

 

프로토콜이지만 이름에 Set이 붙은 거처럼 분류도 Collections로 되어 있고,

intersection, union 등 집합 메서드도 사용할 수 있습니다. (이건 아래에서 더 자세히 알아볼게요.)

 

OptionSet이 낯설 수 있는데요.

생각보다 많은 곳에서 사용됩니다.

대표적으로 UIView.AnimationOptions, UIView.AutoresizingMask가 있습니다.

 

 

OptionSet 사용법

struct ShippingOptions: OptionSet {
    let rawValue: Int


    static let nextDay    = ShippingOptions(rawValue: 1 << 0)
    static let secondDay  = ShippingOptions(rawValue: 1 << 1)
    static let priority   = ShippingOptions(rawValue: 1 << 2)
    static let standard   = ShippingOptions(rawValue: 1 << 3)


    static let express: ShippingOptions = [.nextDay, .secondDay]
    static let all: ShippingOptions = [.express, .priority, .standard]
}

공식 문서에 있는 OptionSet 예시입니다.

각 옵션은 shift 연산으로 구분합니다.

1비트 당 1개의 옵션이라고 생각하면 돼요.

 

OptionSet을 준수하기 위해서는 rawValue를 구현해야 합니다.

이때 rawValue 타입은 FixedWidthInterger 프로토콜을 준수하는 타입이어야 해요.

비트마스킹을 하기 위해 고정된 비트 너비를 가지고 있는 정수이고, 비트 연산(AND, OR, XOR 등)이 가능해야 합니다.

FixedWidthInterger 타입의 예시로는 Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64이 있습니다.

당연히 일반 Int 타입도 준수하고요 ㅎㅎ 

 

 

SetAlgebra

OptionSet 프로토콜은 SetAlgebra 프로토콜을 채택하고 있습니다.

SetAlgebra 프로토콜을 채택하면 집합 연산 메서드를 사용할 수 있어요.

비트마스크 원리인데 집합 연산을 지원하는 게 정말 신기하지 않나요?

 

OptionSet에서 비트마스크를 생성할 수 있다고 따로 적혀 있는 걸 보면 저만 신기했던 건 아닌가 봅니다 ㅋㅋ

OptionSet도 적용시키기 위해 설계되었다고 Note로 적혀 있습니다.

 

 

Collections

분류가 Collections인 만큼 Collection 처럼 생성할 수 있어요.

let singleOption: ShippingOptions = .priority
let multipleOptions: ShippingOptions = [.nextDay, .secondDay, .priority]
let noOptions: ShippingOptions = []

1개의 옵션만 사용할 수도 있고, 대괄호로 묶어서 여러 옵션으로 생성할 수 있습니다.

심지어 [ ]로 생성하면 빈 옵션으로 생성이 됩니다.

 

insert, remove, contains 메서드도 사용할 수 있습니다.

let purchasePrice = 87.55

var freeOptions: ShippingOptions = []
if purchasePrice > 50 {
    freeOptions.insert(.priority)
}

if freeOptions.contains(.priority) {
    print("You've earned free priority shipping!")
} else {
    print("Add more to your cart for free priority shipping!")
}

insert와 contains 예시입니다.

빈 옵션을 생성하고 조건을 검사하여 옵션을 insert 합니다.

그리고 contains로 들어 있는지 검사하고 있습니다.

비트 마스크를 활용하기 때문에 Set과 마찬가지로 O(1)로 동작합니다.

 

 

Stack or Heap?

Swift의 Collections는 동적 크기를 가지기 때문에 대부분 Heap에 저장됩니다.

Array, Set, Dictionary 모두요.

 

하지만 OptionSet은 다릅니다.

OptionSet은 구조체로 정의되고, 비트마스크로 표현하기 때문에 인스턴스 크기가 고정입니다.

따라서 Heap이 아니라 Stack에 저장됩니다.

 

(이제 면접에서 모든 Collections가 Heap에 저장된다고 말하면 안 되겠어요...!!)

 

 

Enum과 비교

"옵션"하면 떠오르는 자료구조인 Enum과는 무슨 차이점이 있을까요?

 

먼저 Enum은 1개의 옵션 밖에 설정하지 못합니다.

하지만 OptionSet은 여러 옵션을 저장할 수 있어요.

 

그리고 Enum은 연관값을 활용할 수 있는데

OptionSet은 0, 1 상태만 표현할 수 있다는 차이점이 있습니다.

 

의미적인 관점에서 Enum은 명확한 상태 또는 값의 유형을 표현하지만

OptionSet은 다중 선택이 필요한 옵션을 표현한다고 하네요!

 

 

Set과 비교

Set<Enum타입>과의 차이점도 궁금했어요.

Enum 타입을 Set으로 관리하면 OptionSet 아니야!? 라는 생각이 있었습니다.

 

먼저 위에서 말했듯 메모리 저장 위치가 다릅니다.

Set은 동적 인스턴스 크기라서 Heap에 저장되고, OptionSet은 스택에 저장돼요.

 

중복 제거의 원리도 다른데, Set은 Hashable의 특성을 이용하고,

OptionSet은 비트마스크를 사용해 중복을 제거합니다.

 

의미적인 관점에서 Set은 중복 제거에 더 초점이 있고,

OptionSet은 다중 옵션에 초점이 있다고 하네요.

 

 

OptionSet 옵션 개수 구하는 법

OptionSet은 Collection 이지만 count 프로퍼티가 없습니다.

비트마스크를 활용하기 때문에 count 개념이 아니기 때문입니다.

그러면 옵션의 개수를 어떻게 구할까요?

 

이것도 비트마스크 원리를 사용하면 됩니다 ㅎㅎ

0이 아닌 비트는 옵션이 있는 것이므로 이 개수를 세주면 돼요.

var userPermissions: Permissions = [.read, .read, .write, .execute]
print(userPermissions.rawValue.nonzeroBitCount)

// 3

FixedWidthInterger 프로토콜을 준수하는 타입은 nonzeroBitCount 프로퍼티를 지원합니다.

이를 이용해 0이 아닌 비트 개수를 세주면 옵션의 개수를 알 수 있습니다.

 

 

OptionSet의 all 쉽게 구하는 법

OptionSet은 allCases를 하나하나 넣어야 합니다.

예시 코드에서도 all 프로퍼티를 정의할 때 하나하나 넣고 있죠? ㅠㅠ

 

이것도 비트마스크의 원리를 이용해 쉽게 개선할 수 있습니다.

extension OptionSet where RawValue == Int {
    static var all: Self {
        Self.init(rawValue: Int.max)
    }
    
    init(_ shift: Int) {
        self.init(rawValue: 1 << shift)
    }
}

비트를  모두 1로 채우면 모든 옵션이 선택되었음을 표현할 수 있습니다.

 

 

OptionSet Macros

OptionSet 구조체를 정의할 때 중복 코드가 많아 번거롭습니다.

Swift 5.9부터 사용 가능한 매크로를 이용하면 OptionSet을 편하게 정의할 수 있습니다.

 

먼저 OptionSet을 사용하지 않을 때의 코드입니다.

struct SundaeToppings: OptionSet {
    let rawValue: Int
    
    static let nuts = SundaeToppings(rawValue: 1 << 0)
    static let cherry = SundaeToppings(rawValue: 1 << 1)
    static let fudge = SundaeToppings(rawValue: 1 << 2)
}

옵션마다 shift 연산을 수행해야 해서 번거롭습니다.

 

@OptionSet<Int>
struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }
}

매크로 기능을 사용하니 굉장히 깔끔하게 정의가 되었습니다.

 

이 매크로는 아래 코드를 표현하고 있습니다.

struct SundaeToppings {
    private enum Options: Int {
        case nuts
        case cherry
        case fudge
    }

    typealias RawValue = Int
    var rawValue: RawValue
    init() { self.rawValue = 0 }
    init(rawValue: RawValue) { self.rawValue = rawValue }
    static let nuts: Self = Self(rawValue: 1 << Options.nuts.rawValue)
    static let cherry: Self = Self(rawValue: 1 << Options.cherry.rawValue)
    static let fudge: Self = Self(rawValue: 1 << Options.fudge.rawValue)
}
extension SundaeToppings: OptionSet { }

매크로... 정말 편리한 기능이네요!

곧 Swift 5.9가 정식 릴리즈될텐데 벌써 기대됩니다.

 

 

감사합니다.

 

참고

https://developer.apple.com/documentation/swift/optionset

https://developer.apple.com/documentation/swift/setalgebra

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/ https://www.avanderlee.com/swift/optionset-swift/

https://swiftdoc.org/v3.0/protocol/optionset/


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

반응형