@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을 막아줄 수 있는 방법 이 있을까요?