# 은행 창구 매니저 PR
은행 창구 매니저 [STEP 1] 써니쿠키, inho
@wonhee009
안녕하세요 라쟈냐! 🙇🏻♀️🙇
Step1 PR드립니다.
잘부탁드립니다
## 📝 STEP 진행 중 경험하고 배운 것
- 단방향 `Linked-list` 자료구조
☑️ 단방향 연결리스트를 코드로 구현해보기
☑️ `Generic Type` 개념이해와 적용하기
- `Queue` 자료구조
☑️ `Linked List`를 이용한 `Queue` 타입 코드로 구현해보기
☑️ `Generic Type` 개념이해와 적용하기
## ⚒️ 코드 구현내용
### 1️⃣ Node
- `Generics` 타입의 데이터를 담을 `data`프로퍼티와 다음 노드 정보를 나타내는 `next`프로퍼티를 생성하였습니다.
### 2️⃣ LinkedList
- 큐 타입 구현을 위한 `LinkedList`타입을 구현했습니다.
- 첫번째와 마지막 노드를 담는 `head`, `last`프로퍼티를 생성하였습니다.
- 큐에 필요한 기능을 구현하기 위한 메서드들을 가지고 있습니다.
- `append(datda:)`
- `removeFirst()`
- `removeAll()`
### 3️⃣ Queue
- 다양한 타입의 데이터를 담을 수 있도록 Generics를 이용하였습니다.
- 구현해야할 필수 기능들을 구현했습니다.
- `isEmpty`
- `claer()`
- `enqueue()`
- `peek()` : 삭제없이 첫번째 노드의 데이터를 확인합니다.
- `dequeue()`: 첫번째 노드를 삭제하고, 해당 데이터를 리턴합니다.
## 💭 고민한 부분
### `SwiftLint` 라이브러리 이용여부
- 프로젝트 요구사항에는 없지만 아카데미 학습활동 중 의존성 관리도구를 이용해 현재 프로젝트에 `SwiftLint` 라이브러리를 적용해보자는 미션이 있었습니다.
- 코코아팟을 이용해 `SwiftLint` 라이브러리를 `BankManagerUIApp` 파일에 적용해 보았고, 실제 프로젝트 진행에서도 이 라이브러리 사용을 계속 유지할까 고민했습니다. 상의 후 배운내용을 경험해보자하여 `BankManagerUIApp`파일에서 적용시키기로했습니다.
### `SwiftLint` 라이브러리 데이터를 `git ignore`에 담아야할지 여부
- 의존성 관리도구와 `git`을 사용할때, 소스코드를 담고있는 `Pods`폴더를 포함하여 푸쉬하게 되면 깃의 용량이 늘어나서 `git ignore`에 포함하는게 좋다고 생각하였습니다.
- 그런데 코코아팟에서 작성한 내용 중, `Pods` 폴더를 포함했을때 다른 사람이 `pod install`을 실행하지 않아도 해당 라이브러리를 사용수 있다는 장점을 확인할 수 있었습니다.
- 또한 라쟈냐가 사용하는 의존성관리도구의 종류에 관계없이 저희 코드를 확인할 수 있을 것 같다는 생각에 포함하였습니다.
---
---
# step 2 PR
은행 창구 매니저 [STEP 2] 써니쿠키, inho
@wonhee009
안녕하세요 라쟈냐! 🙇🏻♀️🙇
Step2 PR드립니다.
잘부탁드립니다
## 📝 STEP 진행 중 경험하고 배운 것
- Queue의 활용
☑️ 은행 고객(Customer)을 Queue에 담아 차례대로 처리
- Enum을 이용한 namespace
☑️ enum의 인스턴스생성이 불가능한 점을 이용해 `static let`으로 네임스페이스구현
## ⚒️ 코드 구현내용
### 1️⃣ `BankManager`
- `Bank`와 `Customer`를 생성하고, 프로그램 실행 및 사용자 입력을 받는 메서드를 가지고 있는 타입으로 전체 실행을 담당합니다.
- `startManagement()`: 메뉴를보여주고 사용자 입력에 따라 (1)은행문을열거나 (2)종료합니다.
- `setupBank()`: 은행 객체를 만들고, 은행업무를 진행하기위해 은행문을 엽니다.
- `receiveCustomer()`: 10~30명 사이의 랜덤한 수로 고객을 받습니다.
- [enum] `Constant` : BankManager의 네임스페이스입니다.
### 2️⃣ `Bank`
- 고객 큐와 은행 업무처리에 관련된 메서드들을 가지고 있는 타입입니다.
- `openBank()`: 은행의 문을 엽니다.
- `startBanking()`: 은행의 업무를 시작합니다.
- `serveCustomer(number: Int)`: 손님을 한명씩 응대합니다.
- `closeBanking()`: 은행 업무의 마감을 알립니다.
- [enum] `Constant` : Bank의 네임스페이스입니다.
### 3️⃣ `Customer`
- 고객을 나타내는 타입으로 프로그램에서 필요한 대기번호를 프로퍼티로 가지고있습니다.
## 💭 고민한 부분
### 프로그램 실행을 관리하는 `BankManager`타입
- 기존에는 앱을 실행하고 사용자 입력받는 기능을 `Bank`타입에서 수행하였습니다. 그런데 앱의 매뉴얼과 사용자 입력을 받는 기능은 `Bank`의 바깥 영역이라 생각되어 이를 관리하는 `BankManager`타입을 구현하였습니다.
```swift
struct BankManager {
mutating func startManagement() {
//실행 매뉴얼 출력
print(Constant.options, terminator: Constant.empty)
//사용자 입력
guard let input = readLine() else { return }
...
}
}
```
### `Bank`인스턴스와 `Customer Queue`생성 방법
- 위에서 언급한 `BankManager`와 연관된 고민이었습니다.
처음에는 `Bank`타입 내에서 `Queue<Customer>`를 생성하여 자신의 프로퍼티에 전달하는 방식을 이용했습니다. 그런데 이렇게 구현하면 `Customer`타입은 `Bank`인스턴스가 있을때만 존재한다는 문제가 있었습니다.
- 따라서 위에 구현한 `BankManager`에 10~30명으로 이루어진 손님 큐를 생성하는 `receiveCustomer`메서드를 구현하고, `Bank`의 인스턴스를 초기화할때 전달하도록 수정했습니다.
```swift
struct BankManager {
private mutating func setupBank() {
let customers = receiveCustomer() //손님 큐 생성
var bank = Bank(customers: customers) //인스턴스 생성과함께 초기화
bank.openBank()
}
private mutating func receiveCustomer() -> Queue<Customer> {
var customers: Queue<Customer> = Queue()
let customerCount = Int.random(in: Constant.customerRange)
for count in 1...customerCount {
customers.enqueue(Customer.init(waitingNumber: count))
}
return customers
}
}
```
### `bank`인스턴스에 대한 접근
- 기존에는 은행의 작업을 시작하고 끝내는 메서드인 `startBanking`과 `closeBanking`를 `BankManager`에서 `bank`를 생성한뒤 각각 호출했습니다. 그런데 타입의 접근제어에 대해 고민하면서 두 메서드를 타입 외부에서 호출하는게 좋지 않은 방향이라고 생각했습니다.
게다가 작업이 끝났음을 알려주는 `closeBanking`메서드를 직접 호출하면 코드가 누락될 여지도 있다고 생각되어, 프로퍼티와 연관지어서 작업이 끝나면 자동으로 메서드가 호출될수 있는 방법을 고민했습니다.
- 위 두가지 고민을 해결하기 위해 `Bank`타입에 `isOpen`프로퍼티와 `openBank`메서드를 생성하고, `isOpen`프로퍼티의 `didSet`값에 따라 은행이 문을 열면 `startBanking`, 마감하면 `closeBanking`이 호출되도록 수정하였습니다. 타입 외부에서는 `openBank`메서드만 호출합니다.
- 수정 전 (타입 외부에서 메서드 접근)
```swift
struct BankManager {
...
private mutating func setupBank() {
let customers = receiveCustomer()
var bank = Bank(customers: customers)
bank.startBanking()
bank.closeBanking()
}
}
```
- 수정 후 (`isOpen`프로퍼티를 이용)
```swift
struct BankManager {
...
private mutating func setupBank() {
let customers = receiveCustomer()
var bank = Bank(customers: customers)
bank.openBank()
}
}
struct Bank {
...
private var isOpen: Bool {
didSet {
isOpen ? startBanking() : closeBanking()
}
}
private mutating func serveCustomer(number: Int) { ... }
private func closeBanking() { ... }
}
```
### 각 파일의 `Constant` 네임스페이스
- `enum`을 이용하여 앱 내의 리터럴 표현을 모아둔 네임스페이스를 구현하였습니다.
그런데 리터럴 표현들이 각 파일 범위 내에서만 사용되고 있어서 하나의 파일로 분리하는 대신 각 타입의 `extension`을 통해 정리했습니다.
(`extension`에 `private`을 적용하면 내부 요소는 `fileprivate`의 접근수준으로 적용됩니다.)
```swift
private extension Bank {
enum Constant {
static let processingTime: Double = 0.7
...
}
}
private extension BankManager {
enum Constant {
static let options: String = "1 : 은행 개점 \n2 : 종료 \n입력 : "
...
}
}
```
## ❓ 조언을 얻고 싶은 부분
### 1️⃣ struct의 이니셜라이저
`Bank`의 `customers`프로퍼티에 접근제어자를 설정해주면서 조언을 얻고 싶은 부분이 생겼습니다.
저희가 알고 있기로는 `struct`를 이니셜라이징 할 때, 프로퍼티에 값이 담겨져 있지 않는다면 `struct`는 `Memberwise Initializers`의 특성으로 자동으로 이니셜라이저를 제공해 주는 것으로 알고 있습니다. 그래서 따로 `init`메서드를 만들어주지 않고 사용했었구요..!
근데 `Bank`의 `customers`프로퍼티에 `private`접근제어자를 추가하고 외부에서 인스턴스 생성하려고 `Memberwise Initializers`를 이용했을 때는 프로퍼티에 접근할 수 없다는 오류가 발생했습니다. 그리고 지정이니셜라이저(`init`메서드)를 만들어줬을 때는 오류가 사라졌습니다. 왜 이런 차이가 있는지 궁금합니다..🧐
`private`을 지정하고 명시적인 `init`을 생성했을때 작동 되는 것은 이해되지만, 선언하지 않았을때에는 왜 안되는지 궁금합니다..!
- 수정 전 (접근제어로 접근불가하다는 오류 발생)
```swift
struct Bank {
private var customers: Queue<Customer>
}
struct BankManager {
private mutating func setupBank() {
let customers = receiveCustomer()
var bank = Bank(customers: customers) // 'Bank' initializer is inaccessible due to 'private' protection level
}
```
- 수정 후 (이니셜라이저 생성)
```swift
struct Bank {
private var customers: Queue<Customer>
init(customers: Queue<Customer>) {
self.customers = customers
}
struct BankManager {
private mutating func setupBank() {
let customers = receiveCustomer()
var bank = Bank(customers: customers)
}
```
### 2️⃣ 네임스페이스에 클로저 사용
Bank 구조체의 네임스페이스를 구현하면서 궁금한점이 생겼습니다.
매개변수가 필요한 문자열을 네임스페이스에서 구현해주고싶어서 저희는 아래와같이 클로저를 이용했습니다. 혹시 이런 구현이 실제로도 많이 쓰이는지 아니면 클로저의 사용은 네임스페이스 구현으로는 맞지 않는것인지 궁금합니다.
(혹시 또 다르게 매개변수를 사용할 다른 방법이 있을까요??)
( 만약 매개변수를 받는 네임스페이스 자체가 어색한 부분이라면 두번째 예시처럼 매개변수를 사용은 제외한 네임스페이스로만 이루어져야하는걸까요? )
- 클로저를 이용한 네임스페이스
```swift
enum Constant {
static let startMessage = { ( number: Int ) -> String in
return "\(number)번 고객 업무 시작"
}
}
// 사용
func serveCustomer(number: Int) {
print(Constant.startMessage(number))
}
```
- 매개변수 사용없는 버전
```swift
enum Constant {
static let turn = "번"
static let startMessage = "고객 업무 시작"
}
//사용
func serveCustomer(number: Int) {
print(number + Constant.turn + Constant.startMessage)
}
```
### 3️⃣ POP 접목(프로토콜의 사용 방향성)
`Bank` 구조체를 구현하면서 `POP`를 적용할 수 있을까하며 아래 코드와 같이 생각했습니다.
그런데 현재 프로젝트에서 서로 다른 타입이 공통으로 갖는 기능들이 없었습니다.
모든 기능이 재사용할 일이 없다고 판단되어 `POP`를 내려놓고 구조체 안에서 구현해주었습니다.
혹시 이렇듯 추후에 재사용 여부는 모르고 현재는 재사용할 일이 없을 때에도 `POP`를 접목해 프로토콜로 기능을 구현해놓고 채택하는게 좋은 방향일까요?
아니면 추후에 재사용할 일이 생기면 그 때 `POP`를 적용하기 위해 프로토콜로 구현해야하는게 맞는걸까요..?🧐
- POP 미적용
```swift
struct Bank {
var customersNumber: Int
func serveCustomer(number: Int) { ... } //고객 업무 처리
}
```
- POP 적용 방향
```swift
protocol servable {
func serveCustomer(number: Int)
}
extension servable {
func serveCustomer(number: Int) { ... } //고객 업무 처리
}
struct Bank: servable {
var customersNumber: Int
}
```
---
---
# step 3 PR
은행 창구 매니저 [STEP 2] 써니쿠키, inho
@wonhee009
안녕하세요 라쟈냐! 🙇🏻♀️🙇
동시성으로 여러 실험을해보고, 고민이 많아 늦게서야 Step3 PR드립니다.😂
고민의 양만큼 PR이 무척이나 길어져버렸네요...!
Race Condition에서 정상작동을 위해 `DispatchSemaphore`와 `serialQueue`를 활용하는 방법을 각각 이용해보았고 결론적으로 저희 코드에서는 각 은행원이 SerialQueue를 갖고 동시성을 진행하는 방법으로 구현했습니다.
코드의 방향이 프로젝트 방향과는 다르게 빙빙 돌아왔나하는 의구심도 있어서 가감없는 리뷰 부탁드리겠습니다 🤣
## 📝 STEP 진행 중 경험하고 배운 것
- 동시성 프로그래밍 (Concurrency Programming)
☑️ GCD(Grand Central Dispatch)활용해 멀티 스레드 구현하기
☑️ 동기(Synchronous)와 비동기(Asynchronous) 동작의 구현 및 적용
☑️ Race Condition 해결하기 - `SerialQueue` 활용 / `DispatchSemaphore`활용
- POP (Protocol Object Programming)
☑️ Protocol의 Extension에 기본구현(Defalult Implementation)하기
☑️ Protocol 채택으로 특정 타입에 기능 부여하기
---
## 📱 코드 실행화면
|<img src="https://i.imgur.com/Qfi8qrR.gif" width=400>|
|---|
## ⚒️ 코드 구현내용
### 1️⃣ `BankManager`
- `Bank`와 `Customer`를 생성하고, 프로그램 실행 및 사용자 입력을 받는 메서드를 가지고 있는 타입으로 전체 실행을 담당합니다.
- `startManagement()`: 메뉴를보여주고 사용자 입력에 따라 (1)은행문을열거나 (2)종료합니다.
- `setupBank()`: 은행 객체를 만들고, 은행업무를 진행하기위해 은행문을 엽니다.
- `receiveCustomer()`: 10~30명 사이의 랜덤한 수로 고객을 받습니다.
- [enum] `Constant` : `BankManager`의 네임스페이스입니다.
### 2️⃣ `BankClerk`
- 은행원을 나타내는 타입으로, 업무 종류, 작업 처리 시간, 업무 창구를 가지고 있는 타입입니다.
- `bankingType`: 업무 중 예금 혹은 대출 업무를 담당합니다.
- `processingTime`: 업무 처리 시간을 나타냅니다.
- `counter: DispatchQueue`: 시리얼큐로 구현될 은행창구로, 은행원이 한번에 한명의 고객을 처리하기 위한 것입니다.
### 3️⃣ `Servable` protocol + extension
- 손님을 상대하는 기능을 포함하고 구현한 프로토콜입니다.
- `serve(customer: group:)`: 매개변수로 받은(자신에게 온) 손님의 업무를 담당하는 기능으로, 업무 처리시간만큼 손님의 업무를 처리하고, 진행과정을 출력해줍니다.
### 4️⃣ `BankingType`
- 은행 내 업무의 종류로 예금과 대출을 포함하고 있습니다.
- `deposit`
- `loan`
---
## 💭 고민한 부분
### 1️⃣ `RaceCondition`해결방법 -`DispatchSemaphore` / `serialQueue`
- `BankClerk` 3명이 `Queue`에 담긴 손님(업무)을 동시에 처리하기위해서 `GCD`를 이용해 멀티쓰레드환경을 구현해주었습니다. 이 과정 중에 하나의 데이터에 동시에 접근하여 생기는 `RaceCondition`을 해결하기 위해서 여러가지 시도를 해보았습니다.
#### **채택한 코드 : `BankClerk`프로퍼티에 `serialQueue`사용**
- 요구사항에 제시된 3명의 은행원들이 각자의 은행업무에 맞는 손님을 차례로 응대하기 위해 은행원의 프로퍼티에 `DispatchQueue`를 추가했습니다. 은행원은 한번에 한명의 손님을 처리할 수 있기 때문에 `serial`큐로 초기화합니다. 손님을 순서대로 `dequeue`하고 전달받은 후에는 자신의 시리얼큐에 비동기로 작업을 넘기도록 구현했습니다.
- 은행원을 나타내는 `BankClerk`의 인스턴스를 의도한대로 활용하기 위한 코드를 고민했습니다.
```swift
while !customers.isEmpty {
bankClerks.forEach { bankClerk in
guard let customer = customers.dequeue() else { return }
bankClerk.serve(customer: customer, group: group)
...
}
}
func serve(customer: Customer, group: DispatchGroup) {
counter.async(group: group) { //counter는 자신의 시리얼 큐입니다.
//업무 시작 및 종료 출력
}
}
```
- 👉 **첫번째 시도 : `DispatchSemaphore`사용**
<details>
<summary>details</summary>
- 동시에 처리할 수 있는 손님이 은행원 인원과 같기 때문에 `쓰레드 수 = 은행원 인원` 이라고 생각했습니다.
- `DispatchSemaphore`의 `value = 은행원 인원` 으로 지정하여 은행원 수만큼만 동시진행 될 수 있도록 구현했었습니다.
```swift
struct Bank {
let cumstomers: Queue<Customer> = Queue()
let depositBankeClerks: [DopositBankClerk]// 2명
let loanBankClerks: [LoanBankClerk] // 1명
private mutating func startBanking() {
let depositSemaphore = DispatchSemaphore(value: depositBankeClerks.count) // 2
let loanSemaphore = DispatchSemaphore(value: loanBankClerks.count) // 1
while !customers.isEmpty {
guard let customer = customers.dequeue() else {
return
}
DispatchQueue.global().async { [self] in
switch customer.bankingType {
case .deposit :
depositSemaphore.wait()
👉 DepositBankClerk.serveCustomer(number: customer.waitingNumber) //손님 업무 시작과 끝프린트 출력하는 함수
depositSemaphore.signal()
case .loan:
loanSemaphore.wait()
👉 LoanBankClerk.serveCustomer(number: customer.waitingNumber) //손님 업무 시작과 끝프린트 출력하는 함수
loanSemaphore.signal()
}
}
}
}
}
struct DepositBankClerk {
static func serveCustomer(number: Int) {
// 손님 업무 시작과 끝을 Print하는 코드
}
}
```
- 😵채택하지 않은 이유😵
- 1) BankClerk의 인스턴스를 활용하지 않기 때문이었습니다.
(=인스턴스 메서드 대신 타입메서드를 사용하는 문제였습니다)
BankClerk타입을 구현해놓고, `serveCustomer(number:)`로 손님을 처리하는 함수를 사용할 때, 은행원 각자의 인스턴스 메서드를 사용하지 못해서 의도한 방향과 다르다고 생각했습니다.(사용하더라도, 은행원이 두명인데 한명의 것만 사용하게됐습니다). 타입메서드가 아닌 은행원(인스턴스)들이 가지고있는 인스턴스 메서드를 사용해 손님을 처리하는게 맞는 방향이라고 생각했습니다.
- 2) 1)의 문제로 애초에 BankClerk 타입 구현을 하지않고 (인스턴스를 생성할 일이 없게 되고), Bank 내부에서 각 은행원의 인원수를 Int로 갖는 프로퍼티를 구현하는 방법도 생각했었습니다. 하지만 객체지향 프로그래밍과 맞지 않다고 생각했습니다. </br></br>
</details>
- 👉 **두번째 시도 : `serialQueue`사용 & `customers Queue` 분류없이 직접 처리하기**
<details>
<summary>details</summary>
- PR드린 코드는 손님이 담겨있는 `Queue`를 은행업무 종류에 따라 `depositQueue`와 `loanQueue`로 다시 분류해서 `Queue`에 담아준 후 타입이 같은 은행원이 손님처리를 하는 로직인데 손님분류 없이 손님의 은행업무타입이 섞여있는 원래의 `Queue`에서 바로 손님처리를 할 수 있는 코드를 구현하고 싶었습니다.
- 처음엔 `bankClerk` 이 담긴 `Array`에 `forEach`를 사용해 업무타입을 `Switch`문으로 구분해서 손님을 처리하도록 구현해주었었습니다. 예금은행원 두명이 공평하게 반씩 차례로 손님을 처리하지 않고 중간에 대출손님이 껴있으면 순서가 꼬이는 문제가 있었습니다. 그래서 순서에맞게 손님을 처리하기 위해 `bankClerk Array`에서 한 은행원씩 차례로 불러줘야했습니다. 0부터 +1씩 올라가는 변수에 은행직원 인원보다 커지면 다시 0으로 돌아가도록 설정한 변수를 선언하고 `Array index`로 사용해 은행원이 번갈아가며 손님을 처리할 수 있도록 구현했었습니다.
```swift
func serveCustomer() {
let depositMaximun = depositBankClerk.count - 1
let loanMaximum = loanBankClerk.count - 1
var depositPossible: Int = 0 {
didSet(oldValue) {
depositPossible = oldValue > depositMaximun ? 0 : oldValue
}
}
var loanPossible: Int = 0 {
didSet(oldValue) {
loanPossible = oldValue > loanMaximum ? 0 : oldValue
}
}
while !customers.isEmpty {
guard let customer = customers.dequeue() else {
return
}
switch customer.bakingType {
case .deposit:
depositBankClerk[depositPossible].call(customer: customer)
depositPossible += 1
case .loan:
loanBankClerk[loanPossible].call(customer: customer)
loanPossible += 1
}
}
}
struct banker {
func call(customer: Customer) {
counter.async(group: group) {
// 손님 업무 시작과 끝을 Print하는 코드
}
}
}
```
- 😵채택하지 않은 이유😵
- 이 시도는 `customers Queue`를 `BankingType`별로 분류해 다시 두개의 `Queue`(예금별, 대출별)를 생성하는 작업을 하지 않기 위해 시도해본 코드였는데, 작성 후에 비교해보니 `Queue`를 분류해서 처리하 코드가 더 깔끔하고 보기 좋은 것 같다고 판단해서 채택하지 않았습니다.
</details>
### 2️⃣ `Servable`프로토콜과 익스텐션의 활용
- 은행원의 업무를 `손님을 응대할 수 있는`기능이라 판단했고, 이 기능 구현이 코드를 작성하는 중간에 재사용되기도 하여 프로토콜로 분리하였고, `extension`을 통해 기본 구현을 추가했습니다. 이 프로토콜 내에는 은행원이 손님을 응대하는 과정에 필요하다고 생각한 요소들을 포함하였습니다.
- `counter: DispatchQueue`: 은행 창구를 나타내면서, 코드 실행중에는 비동기로 작업을 처리하기 위한 요소입니다. 은행원은 한번에 한명의 손님을 처리해야 하고, 순서가 중요하기 때문에 해당 큐를 `serial`로 초기화 하였습니다.
- `serve(customer:, group:)`: 매개변수로 받은 손님을 자신의 시리얼 큐로 비동기 처리한 후, 고객과 업무종류를 출력하고, 처리하는 기능을 합니다.
```swift
protocol Servable {
var processingTime: Double { get }
var counter: DispatchQueue { get }
func serve(customer: Customer, group: DispatchGroup)
}
extension Servable {
func serve(customer: Customer, group: DispatchGroup) {
counter.async(group: group) {
print("\(customer.waitingNumber)번 고객 \(customer.bankingType.name)업무 시작")
...
}
}
}
```
### 3️⃣ `Command Line Tool`에서 `main`스레드의 런루프가 종료되는 문제
- 프로젝트를 실행할때, `DispatchQueue.main.async`가 작동되지 않고, 다른 스레드의 비동기 작업도 끝까지 기다리지 않고 프로그램이 종료되는 문제가 있었습니다. 찾아보니 `iOS`에서는 `main run loop`를 자동으로 설정해 주지만, `macOS`의 `CLT`환경에서는 그렇지 않아서 코드가 끝나기 전에 종료되었습니다.
- 이를 해결하기 위해 처음에는 `Runloop.main.run(until:)`메서드를 활용했지만, 매개변수가 없으면 런루프가 아예 종료되지 않고, 매개변수에 필요한 값은 프로그램이 실행된 후에 생성된 손님의 수나 은행 업무에 따라 변경되어서 미리 계산할 수 없는 문제가 있었습니다.
- 최종적으로 사용한 코드는 기다려야 하는 작업들을 `DispatchGroup`으로 묶어서 해당 그룹을 기다리는 `group.wait()`메서드를 이용해서 의도하는 결과를 출력할 수 있었습니다.
```swift
let group = DispatchGroup()
matchClerk(to: &customers.deposit, of: .deposit, group: group)
matchClerk(to: &customers.loan, of: .loan, group: group)
group.wait()
```
### 4️⃣ 은행 업무시간을 계산하기 위한 `Date()`와 `timeIntervalSinceNow`
- 요구사항을 구현하기 위해 은행 업무시간을 계산해야 했습니다. 이때 고민한 부분은 "업무 시작 시간을 언제로 볼것인가" 였습니다. `Bank`의 인스턴스를 만드는 시점에 초기화할 수도 있지만, 저희가 구현한 기능 중 손님을 외부에서 받는 `receive(customer)`메서드와 손님을 업무종류에 따라 분류하는 `sortCustomer()`메서드 중간이 업무 시작시간으로 적합하다 판단하여 해당 시점을 기준으로 합니다.
- 총 업무 시간은 `DispatchGroup`으로 지정한 업무들이 종료되는 것을 기다리는 `group.wait()`이후에 `Date`의 `timeIntervalSinceNow`프로퍼티를 이용하여 계산하였습니다.
---
## ❓ 조언을 얻고 싶은 부분
### 1️⃣ runloop를 사용하여 종료를 방지하는 방법
- 위의 고민중 `runloop`를 적절히 사용하지 못해서 `DispatchGroup`을 이용한 부분이 있습니다.
그런데 해당 코드에서 비동기 작업이 실행되는 부분과 코드의 종료를 기다려야 하는 부분이 달라서 `group`을 여러번 매개변수로 전달해야 했습니다. `matchClerk(... group:)` -> `bankClerk.serve(... group:)` -> `...async(group: group) {}`
코드의 흐름상으로는 맞다고 판단하여 사용했는데, 이러한 방법이 맞을지, 혹은 `runloop`를 다르게 활용하는 방법이 있을지 궁금합니다..!🤔
### 2️⃣ protocol namespace
- 클래스 내부에서 namespace를 구현해 줄 때는 클래스의 extension에 구현해줄 수 있는데, Protocol을 구현해주면서 namespace를 만들어주고 싶을 때는 extension에서는 nested type이 안돼서 어떻게 하시는지 궁금합니다!
```
//오류내용
Type 'nameSpace' cannot be nested in protocol extension of 'Servable'
```
### 3️⃣ 동시성 구현 방법 및 컨셉 선택
- 저희는 객체지향의 관점에서 객체가 직접 손님을 처리할 수 있는 코드를 구현하자는 의도를 갖고 PR드린 동시성구현 방법을 고민했는데, 혹시 라쟈냐가 보시기에 프로젝트의 의도보다 더 저희가 복잡하게 생각한건지 궁금합니다. (그냥 `semaphore`를 사용했어도 됐을까요? )
- 위에 고민한 부분의 첫번째로 작성한 `RaceCondition해결방법 -DispatchSemaphore / serialQueue` 에서 저희 PR코드를 포함해 다른 두가지 방법도 코드컨셉이랑 예시를 작성해놓았는데 혹시 지금 코드보다 더 낫다고 생각되시는 구현방향이 있으시다면 가감없이 조언 부탁드립니다! (전면수정도 마다하지않습니다..)🧐
---
코멘트 답장
1. RunLoop 사용
> runloop를 어떻게 사용하셨는지 궁금합니다.
```swift
mutating func runBankingCycle() {
...
let group = DispatchGroup()
matchClerk(to: &customers.deposit, of: .deposit, group: group)
matchClerk(to: &customers.loan, of: .loan, group: group)
//group.wait()
RunLoop.current.run() // 혹은 RunLoop.main.run()
closeBanking()
}
```
- 위 코드에서 기존 코드인 `group.wait()`대신 `RunLoop.current.run()`를 적용했었는데요, `run`메서드를 사용하면 `matchClerk()`이후의 코드가 작동하지 않고, 프로그램도 종료되지 않았었습니다. `run()` 메서드를 공부해보니 그 쓰레드를 계속 사용한다는 메서드인 것을 알고 `run(until:)`에 매개변수를 전달하여 적절한 시간 후에 종료되게도 해보았습니다. 하지만 실행시 마다 바뀌는 실행 시간을 계산하는 방법을 찾지 못했습니다.
- runLoop로 해결하려는 방법이 잘못된 접근 방식인지, 아니면 저희가 아직 찾지못한 runLoop를 이용한 다른 해결방법이 있나 궁금합니다. 😂
2. Protocol Extension과 네임스페이스
> protocol에 네임스페이스를 어떤 경우에 추가하려고 하셨는지 궁금합니다
```swift
extension Servable {
func serve(customer: Customer, group: DispatchGroup) {
counter.async(group: group) {
print("\(customer.waitingNumber)번 고객 \(customer.bankingType.name)업무 시작")
Thread.sleep(forTimeInterval: processingTime)
print("\(customer.waitingNumber)번 고객 \(customer.bankingType.name)업무 종료")
}
}
}
//step2에서 사용했던 네임스페이스
enum Constant {
static let startMessage = "%d번 고객 업무 시작"
static let endMessage = "%d번 고객 업무 종료"
}
```
- step2에서는 고객업무 시작과 종료를 출력하는 구문을 상수로 만들어주기 위해서 `namespace`를 사용했었습니다. 그리고 이 네임스페이스는 출력구문이 사용되는 `클래스의 extension`에서 구현되어있었습니다.
- step3를 진행하면서 출력구문이 있는 메서드를 `프로토콜 extension`으로 옮겨주게되면서 `nameSpace`도 같이 옮겨주고 싶었습니다. 전역으로 구현하기에는 프로토콜 내부에서만 쓰이기 때문에 프로토콜이 갖고있으면 좋겠다고 생각했는데 방법을 찾지 못했습니다.