Skip to content

Commit d96a79f

Browse files
committed
add post
1 parent 94e9319 commit d96a79f

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
---
2+
layout: post
3+
title: "[Swift 5.9+] Generic과 Noncopyable을 활용하여 보다 안전한 상태머신을 만들기"
4+
tags: [Swift, Generic, Noncopyable]
5+
---
6+
{% include JB/setup %}
7+
유한 상태 머신(Finite State Machine, FSM)은 소프트웨어 개발에서 자주 사용하는 패턴 중 하나입니다. 특정 사건(Event)에 의해 한 상태에서 다른 상태로 변할 수 있으며, 이를 전이(Transition)라고 합니다. 다양한 시스템의 동작을 모델링하는 데 유용합니다.
8+
9+
FSM을 작성하다 보면 다양한 상태와 이벤트 등을 다루게 되는데, 코드가 복잡해지는 경우가 있습니다.
10+
11+
예를 들어, 간단한 턴스타일(Turnstile)의 FSM을 생각해봅시다.
12+
13+
```swift
14+
struct Turnstile {
15+
enum State {
16+
case locked, unlocked
17+
}
18+
19+
enum Event {
20+
case insertCoin, push
21+
}
22+
23+
private(set) var state: State = .locked
24+
25+
mutating func handleEvent(_ event: Event) {
26+
switch (state, event) {
27+
case (.locked, .insertCoin):
28+
state = .unlocked
29+
print("Turnstile is now unlocked")
30+
case (.locked, .push):
31+
print("❌ Turnstile is locked. Please insert a coin.")
32+
case (.unlocked, .insertCoin):
33+
print("❌ Turnstile is already unlocked. You can push.")
34+
case (.unlocked, .push):
35+
state = .locked
36+
print("Turnstile is now locked")
37+
}
38+
}
39+
}
40+
41+
var turnstile = Turnstile()
42+
turnstile.handleEvent(.insertCoin) // Output: "Turnstile is now unlocked"
43+
turnstile.handleEvent(.insertCoin) // Output: "❌ Turnstile is already unlocked. You can push."
44+
turnstile.handleEvent(.push) // Output: "Turnstile is now locked"
45+
turnstile.handleEvent(.push) // Output: "❌ Turnstile is locked. Please insert a coin."
46+
```
47+
48+
위 코드에서는 상태와 이벤트를 Enum으로 정의하고, 상태 전이를 조건문(Switch-Case)으로 작성하였습니다. 이 방법에는 다음과 같은 문제점이 있습니다.
49+
50+
* 확장성 부족: 새로운 상태나 이벤트를 추가할 때마다 조건문을 수정해야 합니다.
51+
* 유지보수 어려움: 상태 전이 로직이 분산되어 있으면 코드의 가독성과 유지보수가 어려워집니다.
52+
* 안전성 부족: 상태 전이가 잘못 정의되거나 빠질 경우, 예기치 않은 동작이 발생할 수 있습니다.
53+
54+
## 제네릭을 활용한 상태 전이 정의
55+
56+
57+
상태 enum의 각 case를 가지지 않는 `Locked``Unlocked` Enum 타입으로 정의하고, Turnstile은 내부에서 가지고 있던 상태를 제네릭으로 받아 상태를 런타임에서 컴파일 타임 유형으로 정의할 수 있습니다.
58+
59+
```swift
60+
enum Locked {}
61+
enum Unlocked {}
62+
63+
struct Turnstile<State> {}
64+
65+
let locked = Turnstile<Locked>()
66+
let unlocked = Turnstile<Unlocked>()
67+
```
68+
69+
다음으로 각 상태에서 수행할 수 있는 이벤트를 Enum이 아닌, 함수를 호출하도록 하며, 각 이벤트는 해당 상태에서만 사용할 수 있게 제한을 둡니다.
70+
71+
```swift
72+
extension Turnstile where State == Locked {
73+
func insertCoin() -> Turnstile<Unlocked> {
74+
print("Turnstile is now unlocked")
75+
return .init()
76+
}
77+
}
78+
79+
extension Turnstile where State == Unlocked {
80+
func push() -> Turnstile<Locked> {
81+
print("Turnstile is now locked")
82+
return .init()
83+
}
84+
}
85+
86+
let locked = Turnstile<Locked>()
87+
let unlocked = locked.insertCoin()
88+
89+
locked.push() // ❌ Referencing instance method 'push()' on 'Turnstile' requires the types 'Locked' and 'Unlocked' be equivalent
90+
91+
unlocked.push() // Output: "Turnstile is now locked"
92+
unlocked.insertCoin() // ❌ Referencing instance method 'insertCoin()' on 'Turnstile' requires the types 'Unlocked' and 'Locked' be equivalent
93+
```
94+
95+
각 함수에서는 변경된 상태를 타입인 Turnstile을 반환하도록 하여, 각 이벤트는 해당 상태에서만 사용할 수 있게 제약을 두어 다른 이벤트를 사용할 수 없도록 만들었습니다.
96+
97+
하지만 `locked``unlocked`는 함수 호출 뒤에도 사용할 수 있는 문제가 있습니다. 상태의 수명을 제한하고, 임의의 재사용을 지양해야 문제를 방지할 수 있습니다.
98+
99+
Swift 5.9의 [SE-0377 - borrowing and consuming parameter ownership modifiers](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md)[SE-0390 - Noncopyable structs and enums](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md)의 consuming을 이용하여 사용한 상태를 해제하는 방식을 활용할 수 있습니다.
100+
101+
## Noncopyable를 활용한 상태 재사용 제한
102+
103+
Noncopyable 타입은 Struct, Enum에 추가할 수 있습니다. Noncopyable을 추가하여 자신을 복사 불가능하도록 선언하여 상태의 수명을 제한합니다.
104+
105+
```swift
106+
struct Turnstile<State>: ~Copyable {}
107+
108+
extension Turnstile where State == Locked {
109+
consuming func insertCoin() -> Turnstile<Unlocked> {
110+
print("Turnstile is now unlocked")
111+
return .init()
112+
}
113+
}
114+
115+
extension Turnstile where State == Unlocked {
116+
consuming func push() -> Turnstile<Locked> {
117+
print("Turnstile is now locked")
118+
return .init()
119+
}
120+
}
121+
122+
let locked = Turnstile<Locked>()
123+
let unlocked = locked.insertCoin()
124+
125+
_ = unlocked.push() // ❌ 'unlocked' consumed more than once
126+
_ = locked.insertCoin() // ❌ 'locked' consumed more than once
127+
```
128+
129+
`consume`을 사용하여 변수의 수명이 종료되어 재사용이 불가능해졌습니다. 위 코드와 같이 변수를 재사용하려고 하면 컴파일러가 에러를 발생시켜 안전한 코드를 작성할 수 있게 됩니다.
130+
131+
또한, `var로` 작성 시 기존 변수에 새로운 값을 다시 할당하는 것은 가능하지만, 기존 변수를 재사용하는 것은 여전히 불가능합니다.
132+
133+
```swift
134+
var locked = Turnstile<Locked>()
135+
var unlocked = locked.insertCoin()
136+
137+
locked = unlocked.push()
138+
unlocked = locked.insertCoin()
139+
locked = unlocked.push()
140+
unlocked = locked.insertCoin()
141+
142+
unlocked = locked.insertCoin() // ❌ 'locked' consumed more than once
143+
locked = unlocked.push() // ❌ 'locked' consumed more than once
144+
```
145+
146+
Noncopyable을 활용하여 FSM의 안전성을 더욱 강화할 수 있습니다.
147+
148+
## 정리
149+
150+
Swift의 제네릭을 활용하여 FSM을 구현하는 것은 코드의 확장성과 안전성을 높이고 버그를 줄이는 데 도움이 됩니다. Noncopyable을 통해 값의 수명을 제한하여 재사용을 막음으로써 상태의 안전성을 보장할 수 있습니다. 코드의 확장성과 안전성을 높이는 데 타입 시스템을 활용하는 것은 안정성과 확장성 있는 애플리케이션을 개발하는 데 중요한 요소입니다.
151+
152+
## 참고자료
153+
154+
* Swift Evolution
155+
* [SE-0377 - borrowing and consuming parameter ownership modifiers](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0377-parameter-ownership-modifiers.md)
156+
* [SE-0390 - Noncopyable structs and enums](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0390-noncopyable-structs-and-enums.md)
157+
158+
* GitHub
159+
* [Orion98MC/FSM.swift](https://github.com/Orion98MC/FSM.swift)
160+
161+
* [Wikipedia - 유한 상태 기계](https://ko.wikipedia.org/wiki/%EC%9C%A0%ED%95%9C_%EC%83%81%ED%83%9C_%EA%B8%B0%EA%B3%84)
162+
* [Typestate the new Design Pattern in Swift 5.9](https://swiftology.io/articles/typestate/)
163+
* [[iOS - swift] 1. noncopyable, ~Copyable - 개념 (Swift 5.9+, owner, ownership, 최적화)](https://ios-development.tistory.com/1683)
164+
* [[iOS - swift] 2. noncopyable, ~Copyable - 연산자 (borrowing, inout, consuming)](https://ios-development.tistory.com/1684)

0 commit comments

Comments
 (0)