@junbangg
안녕하세요!
반갑습니다 알라딘:) 이번 스탭에는 동시성 프로그래밍에 대해서 고민을 많이 했습니다..!
리뷰 잘 부탁드리겠습니다! 💪🏽
# 은행 창구 매니저 [STEP 3] 토털이, Aaron
## 개발한 내용
- 예금과 대출 업무 큐 분리
- 대출과 예금 큐 각각 동시적으로 work 메서드 처리 구현
- 확장성과 다형성을 고려하여 workable 프로토콜 및 예금, 대출 은행원 구조체 구현
- 은행원 타입 두가지(DepositClerk, LoanClerk)로 분리, Workable protocol 생성
- serial Queue를 활용하여 Race Condition을 방지해 줌.
- Workable 프로토콜 내부의 scheduleWork() 함수를 정의하여 DispatchWorkItem을 생성해주도록 함.
- Bank init()에서 은행원 수와, 예금인원수를 입력하면 대출 및 예금 은행원이 자동 생성되도록 해줌.
## 고민한 점
- 같은 업무를 처리하는 은행원 사이에서 같은 고객 대기열에 접근할 때 발생하는 race condtion 해결
- 은행원 타입의 다형성을 위해 프로토콜 분리 및 구조체 구현
### 1. 은행원의 역할
이전 코드에서는 은행원의 역할이 무엇이던 상관없이 은행원은 print만을 담당하였습니다. 그 코드는 아래와 같습니다.
```swift
struct BankClerk {
func work(for customer: Customer) {
print("\(customer.number)번 고객 업무 시작")
}
}
```
하지만, 객체지향적인 설계가 아니라는 생각이 들었고, 은행원을 enum 타입으로 대출 및 예금 형태로 구분해주는 것이 아닌 Workable 프로토콜을 따르는 대출 은행원, 예금 은행원으로 만들어주는 것이 더 낫다고 판단하였습니다.
```swift
protocol Workable {
var service: Service { get }
var processingTime: Double { get }
func work(for customer: Customer)
func scheduleWork(from customerQueue: Queue<Customer>) -> DispatchWorkItem
static var serviceQueue: DispatchQueue { get }
}
```
위와 같은 형태로 프로토콜을 생성해 은행원이 일을 스케쥴링 하여 일을 할 수 있도록 하고 각각의 타입 프로퍼티로 serviceQueue를 설정하여 race condition을 방지할 수 있는 serial queue를 만들어 주었습니다.
### 2. 같은 큐 접근에 대한 race condtion 방지
- `DispatchQueue.gloabl().async`를 사용해 동시적으로 처리할 경우 같은 고객 대기열을 접근하게 되어 race condition이 발생할 수 있습니다. <br>
이를 방지하기 위해서 `Dispatch Gruop`을 사용해 해당 그룹의 task가 끝나기 전까지 동기적으로 고객을 받아 race condition을 방지했습니다.
```swift
func serve() {
let group = DispatchGroup()
clerks.forEach { clerk in
switch clerk.service {
case .loan:
DispatchQueue.global().async(group: group, execute: clerk.scheduleWork(from: loanQueue))
case .deposit:
DispatchQueue.global().async(group: group, execute: clerk.scheduleWork(from: depositQueue))
}
}
group.wait()
}
```
```swift
func scheduleWork(from customerQueue: Queue<Customer>) -> DispatchWorkItem {
let depositWorkItem = DispatchWorkItem {
while customerQueue.isEmpty == false {
var customer: Customer?
Self.serviceQueue.sync {
customer = customerQueue.dequeue()
}
guard let customer = customer else {
return
}
self.work(for: customer)
}
}
return depositWorkItem
}
```
### 3. randomElement()
- customer를 customerQueue에 할당 할 때 임의로 `serviceType`을 어떻게 할지 고민했습니다.
서로 고민한 결과 `Service` 열겨형에 `CaseIterable`프로토콜을 채택해 배열처럼 다루며 `randomElement()`를 사용해 구현했습니다.
```swif
enum Service: CaseIterable {
case deposit
case loan
var message: String {
switch self {
case .deposit:
return "예금"
case .loan:
return "대출"
}
}
}
```
### 4. manageCustomer에서 고객 큐 분리
- 처음 `Service` enum 타입을 고민할 때 미리 고객 대기열을
- 업무별로 분리하여 각각의 큐를 만들어 할당해줄지
- 전체 큐를 Clerk이 할당받아 자기 업무에 맞는 고객을 찾아갈지
고민해 주었습니다. 저희는 결국 업무별로 분리하여 각각의 큐를 만들어 할당해주기로 하고 코드를 작성해 주었습니다.
## 조언을 구하고 싶은 부분
- async가 제대로 작동하는지 확인할 방법이 없어 정확히 잘 작동하는지 감을 잡기가 어려웠습니다. 저희가 알 수 있는 방법이 있을까요? 테스트 코드를 작성하면 될까요?🙏
- race condition을 해결하기 위해 serial 큐를 작성해 주었는데, 아래 코드에서
```swift
func scheduleWork(from customerQueue: Queue<Customer>) -> DispatchWorkItem {
let depositWorkItem = DispatchWorkItem {
while customerQueue.isEmpty == false {
var customer: Customer?
Self.serviceQueue.sync {
customer = customerQueue.dequeue()
}
guard let customer = customer else {
return
}
self.work(for: customer)
}
}
return depositWorkItem
}
```
`while customerQueue.isEmpty == false`의 경우도 customerQueue에 대한 접근인데 race condition이 일어나지 않을까 걱정이 되었습니다. 이 경우에도 race condition을 막아줄 수 있는 방법 이 있을까요?