---
# System prepended metadata

title: 은행창구 매니저PR
tags: [PR/답변]

---

# 은행창구 매니저 [STEP 1] - kyungmin, maxhyunm

안녕하세요, 하비@havilog! 첫 PR 올려드리는 kyungmin🐼, maxhyunm🤖입니다.
부족한 점이 많지만, 하비의 리뷰를 통해 열심히 배우겠습니다.🙇‍♀️
이번 프로젝트 리뷰 잘 부탁드립니다!

## 🤔 고민했던 점
### 🌟 Queue 구현
#### 1️⃣ Queue와 LinkedList의 분리
처음에는 `CustomerQueue`라는 단일 타입으로 `Linked-list` 방식의 `Queue`를 구현하였습니다.
```swift
struct CustomerQueue<Element>: QueueType {
    private var headNode: Node<Element>?
    private var tailNode: Node<Element>?
    
    mutating func enqueue(_ value: Element) { ... }
    
    mutating func dequeue() -> Element? { ... }
    
    mutating func clear() { ... }
    
    func peek() -> Element? { ... }
    
    func isEmpty() -> Bool { ... }
}
```

하지만 요구사항을 자세히 살펴보니 아래와 같이 정리되어 있었습니다.

> Queue 타입 구현을 위한 Linked-list 타입을 직접 구현합니다.

위의 내용으로 짐작해 보았을 때, `Queue`와 `Linked-list` 타입이 구분되어있는 것이 맞다고 판단되었습니다. 이에 따라 `CustomerQueue` 타입과 `LinkedList` 타입을 분리하였습니다. 기존 `CustomerQueue`에 정리되어 있던 `Node` 정보와 로직들은 `LinkedList`로 이동하였고, `CustomerQueue`에서는 각 메서드를 통해 필요한 로직을 호출할 수 있도록 하였습니다.

```swift
struct CustomerQueue<Element>: QueueType {
    private var linkedList = LinkedList<Element>()
    
    mutating func enqueue(_ value: Element) {
        linkedList.append(value)
    }
    
    mutating func dequeue() -> Element? {
        return linkedList.pop()
    }
    
    mutating func clear() {
        linkedList.removeAll()
    }
    
    func peek() -> Element? {
        return linkedList.peek()
    }
    
    func isEmpty() -> Bool {
        return linkedList.isEmpty()
    }
}
```
```swift
struct LinkedList<Element> {
    private var headNode: Node<Element>?
    private var tailNode: Node<Element>?

    mutating func append(_ value: Element) { ... }
    
    mutating func pop() -> Element? { ... }
    
    mutating func removeAll() { ... }
    
    func peek() -> Element? { ... }
    
    func isEmpty() -> Bool { ... }
}
```

#### 2️⃣ LinkedList 내부 프로퍼티의 접근제어
`LinkedList`에는 `headNode`와 `tailNode` 두 가지 프로퍼티가 포함됩니다.
해당 프로퍼티를 `private`으로 설정할 경우 테스트를 진행할 때 값에 접근할 수 없어 문제가 발생합니다. 하지만 어디서나 이 정보를 수정할 수 있도록 만들 수는 없었습니다. 때문에 저희는 `headNode`와 `tailNode` 를 `private(set)`으로 설정하였습니다.

```swift
private(set) var headNode: Node<Element>?
private(set) var tailNode: Node<Element>?
```

#### 3️⃣ CustomerQueue 내부 프로퍼티의 접근제어
`CustomerQueue` 타입에는 `private` 프로퍼티로 `LinkedList`가 포함됩니다. 이 부분 역시 테스트시 문제가 될 것으로 판단되어, `Queue`에 대한 테스트용 `Mock` 타입을 생성하는 쪽으로 방향을 잡았습니다. `Mock` 타입 생성을 위해서는 해당 `Queue`의 기본적인 내용이 추상화되어있는 편이 좋을 것이라는 결론을 내렸고, 이에 따라 `QueueType`이라는 프로토콜을 만들어 실제 프로젝트에 쓰일 `CustomerQueue`와 테스트용인 `MockCustomerQueue` 양쪽에서 모두 `QueueType`을 채택하도록 구현하였습니다.
`MockeCustomerQueue`에서는 추가로 `headNode`와 `tailNode`라는 연산 프로퍼티를 구현하여 `LinkedList`의 `firstNode`와 `lastNode`를 리턴하도록 했습니다.
```swift
var headNode: Node<Element>? {
        return linkedList.headNode
    }
    
    var tailNode: Node<Element>? {
        return linkedList.tailNode
    }
```

#### 4️⃣ Queue와 Node의 타입
`Node`의 경우 객체를 참조하는 방식으로 연결되기 때문에 `class`로 구현하였습니다. 하지만 `Queue`는 참조될 필요도, 상속될 필요도 없다고 생각되어 `struct`로 구현하였습니다.

### ✅ Test 진행

#### 1️⃣ 테스트용 Queue에 들어갈 데이터타입
`Queue`를 제네릭으로 생성하였기에 저희는 적어도 두 가지 이상의 타입으로 테스트를 진행해야 한다는 생각을 했습니다. 이에 따라 `String`용 테스트와 `Int`용 테스트 클래스를 각각 생성하였고, `sut`객체 타입을 각 용도에 맞게 지정해 주었습니다.

```swift
final class BankManagerConsoleStringTest: XCTestCase {
    ...
    var sut: MockCustomerQueue<String>!

    override func setUpWithError() throws {
        sut = MockCustomerQueue<String>()
    }
    ...
}
```
```swift
final class BankManagerConsoleIntTest: XCTestCase {
    ...
    var sut: MockCustomerQueue<Int>!

    override func setUpWithError() throws {
        sut = MockCustomerQueue<Int>()
    }
    ...
}
```

## 📌 해결하지 못한 점

### ✅ Test 빌드

#### 1️⃣ 테스트 빌드를 위한 Target 설정
`Test`를 생성 후 계속하여 빌드에 실패하였습니다. 원인을 찾아보니 `Command Line Tool` 프로젝트에서는 `TargetMembership` 설정을 따로 진행해주어야 한다는 내용을 발견하였습니다. 이에 따라 `File Inspector`에서 프로젝트 내부 파일들의 `TargetMembership` 설정 부분에 `App`, `Test` 두 가지를 모두 체크해주었고 그 후로 문제없이 빌드되었습니다. `Command Line Tool`에서는 이런 방식으로 테스트를 빌드하는 것이 옳은 방식 일까요⁉️
![](https://hackmd.io/_uploads/HJvXs_YYn.png)

## 📚 참고 링크
- [🍎Apple Docs: Generics](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/)
- [🍎Apple Docs: Protocols](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/protocols/)




# STEP 1 PR 답변
![](https://hackmd.io/_uploads/H19mgOsF3.png)
답변 x

![](https://hackmd.io/_uploads/rkgKJ_sF3.png)
[Choosing Between Structures and Classes](https://developer.apple.com/documentation/swift/choosing-between-structures-and-classes)를 참고했습니다. LinkedList를 상속하거나, 객체를 참조할 필요가 없다고 생각했기때문에 우선적으로 struct로 구현했습니다.

![](https://hackmd.io/_uploads/B1vKyujYn.png)
연산 프로퍼티와 메서드 중에 고민을 했었는데, 요구사항에 '기능'이라고 명시된 점에 집중해서 메서드로 구현을 했습니다.

![](https://hackmd.io/_uploads/SysYy_it3.png)
해당 내용 수정해보도록 하겠습니다!

![](https://hackmd.io/_uploads/HJAKJOoF3.png)
앗 훨씬 간편하게 쓸 수 있었는데 완전히 생각을 못 하고 있었습니다! 수정해보겠습니다!

![](https://hackmd.io/_uploads/SJzqJdiYh.png)
Node는 객체가 참조될때 같은 메모리 주소의 객체로 공유돼서 연결돼야 하기 때문에 클래스로 구현했습니다. Struct로 구현을 하게 되면 이미 만들어진 Node 인스턴스를 수정할 때마다 새로운 인스턴스가 생성되기 때문에 프로퍼티에 연결된 주소가 실제 필요한 인스턴스를 가리키지 않는 문제가 발생할 수 있다고 판단했습니다.

![](https://hackmd.io/_uploads/HJV5JOiFh.png)
네이밍 통일해보겠습니다!

![](https://hackmd.io/_uploads/Sy_cJ_iKh.png)
테스트를 위해 Node 자체에 Equatable을 넣을까 고민해본 적은 있는데, Element에 넣을 생각은 하지 못했습니다! 다양한 타입의 데이터를 취급할 수 있도록 하라는 요구사항을 생각하느라 Element의 타입을 제한할 생각은 하지 못한 것 같습니다. 테스트에서 XCTAssertEqual로 값을 비교하려면 Equatable이 있는 편이 좋을 것 같은데, 요구사항과 비교해보면 빠지는 게 맞을 것 같다는 생각도 들어 고민이 됩니다. 해당 부분이 필요할까요?

![](https://hackmd.io/_uploads/Syic1djKh.png)
이 프로토콜은 PR에 설명드린 대로 테스트용 Mock 클래스를 만들기 위해 구현하였습니다! (Queue 내부 LinkedList 프로퍼티가 private이기 때문에, 테스트시 접근이 불가하기 때문입니다.) 테스트에 쓰일 Mock 클래스와 실제 프로젝트에 쓰일 Queue 클래스가 모두 이 프로토콜을 채택하도록 추상화하였습니다.


---

# 은행창구 매니저 [STEP 2] - kyungmin, maxhyunm

안녕하세요, 하비@havilog! STEP 2 PR 올려드립니다!
OperationQueue를 처음으로 활용해 보아서 이래저래 헤맨 스텝이었습니다😭
이번 스텝도 잘 부탁드립니다...!🙇‍♀️

## 🤔 고민했던 점
### 🌟 **작업용 Queue 구현**
#### 1️⃣ DispatchQueue vs OperationQueue
저희는 요구사항의 은행직원이 작업용 `Queue`의 스레드 개수를 의미한다고 생각하였습니다. STEP 2에서는 은행직원이 1명이라고 되어 있지만, 이 부분은 얼마든지 늘어날 수 있는 개념이라고 생각했습니다. 이때 `OperationQueue`에서 `maxConcurrentOperationCount` 프로퍼티 하나로 스레드의 갯수 및 `Serial`, `Concurrent` 여부를 설정할 수 있기 때문에 변화에 좀더 유연하게 대응할 수 있을 것이라는 생각이 들었고, 결과적으로 `OperationQueue`를 선택하여 진행하게 되었습니다.

#### 2️⃣ waitUntilAllOperationsAreFinished()
`OperationQueue`가 진행되는 중에 다른 코드로 넘어갈 수 있도록 설정되면, 프로그램이 정상적으로 실행->종료의 루틴을 거치지 않고 은행업무 처리 중에 업무 종료 메시지가 먼저 출력이 되어 버립니다. 따라서 `OperationQueue`에 등록된 모든 `Task`를 종료하기 전까지 해당 스레드에 접근이 불가하도록(모든 `Task`가 종료된 후에 다음 코드를 진행할 수 있도록) 만들기 위하여 `waitUntilAllOperationsAreFinished()` 메서드를 사용하였습니다.

### 🛠️ **타입 설계**
`BankManager`와 `Bank`, `Customer` 타입을 어떻게 구분할지 고민이 많았습니다. 결과적으로 `main.swift`를 통해 처음 호출되는 타입은 프로젝트명과 동일한 `BankManager`가 되어야 한다고 생각하여 먼저 `BankManager`에서 메뉴 출력 및 `input`을 받고, `Bank`에서는 일일 업무를 처리하는 내용만 실행할 수 있도록 구분하였습니다. `Customer`에서는 각 고객이 대기번호와 작업 소요시간을 가질 수 있도록 프로퍼티로 생성하였습니다.
그 밖에 매직넘버로 이루어진 설정값이나 매직리터럴로 이루어진 출력메시지들은 모두 열거형으로 분리하였습니다.


#### 1️⃣ Bank 타입
- **property**
    - `banker`: 은행직원, 즉 해당 은행이 가진 OperationQueue 객체
    - `dailyCustomerQueue`: 일일 고객 대기열(CustomerQueue 객체)
    - `dailyTotalCustomer`: 일일 처리 고객수. 초기값 0
    - `dailyBusinessHour`: 일일 업무시간. 초기값 0.0
- **init**
    - `banker`의 스레드 갯수를 설정
- **method**
    - `dailyWork()`: 일일 업무 내용을 전반적으로 호출
    - `setDailyCustomerQueue()`: 일일 고객수를 랜덤하게 설정하고 고객 객체를 생성하여 대기열에 추가
    - `customerTask()`: `banker`에 고객별 작업 추가 후 일일 고객수, 업무시간 변경
    - `closeBank()`: 업무종료 메시지 출력
    - `resetBank()`: 일일 업무 관련 프로퍼티들 초기화



#### 2️⃣ Customer 타입
- **property**
    - `duration` : 업무에 필요한 시간
    - `waitingNumber` : 대기표

#### 3️⃣ BankManager 타입
- **property**
    - `bank` : Bank 인스턴스
- **method**
    - `workBankManager()` : 은행 개점 시 업무 진행. 종료를 선택하지 않을 경우 계속 반복
    - `selectMenu() -> Menu` : 메뉴를 입력 받음

#### 4️⃣ Menu 타입
사용자의 입력을 받을 메뉴에 대한 `case`를 정의

#### 5️⃣ BankConfiguration
은행 타입에 필요한 기본값 정리

#### 6️⃣ CustomerConfiguration
고객 타입에 필요한 기본값 정리

#### 7️⃣ BankNamespace
매직리터럴 정리를 위한 네임스페이스 구분

---

## 📚 참고 링크
- [🐻야곰 닷넷: 동시성 프로그래밍](https://yagom.net/courses/동시성-프로그래밍-concurrency-programming/)


---

# 은행창구 매니저 [STEP 3] - kyungmin, maxhyunm

안녕하세요, 하비@havilog!
Step 2를 빠르게 확인해주신 덕분에 Step 3도 바로 작업할 수 있었습니다! 감사합니다🙇‍♀️
이번 스텝도 잘 부탁드립니다!

## 🤔 고민했던 점
### 💵 Work 타입 구현
은행 업무가 `예금` / `대출` 두 가지로 나뉘고 이에 따라 업무 처리시간과 출력메시지도 달라지게 되어, 해당 내용을 `Work`라는 타입으로 분류하였습니다.
이 `Work`는 은행 내부의 업무이기 때문에 `Bank` 안에 `Nested` 처리를 해주었습니다.
```swift
enum Work: CaseIterable {
        case deposit
        case loan
        
        var duration: Decimal {
            switch self {
            case .deposit:
                return 0.7
            case .loan:
                return 1.1
            }
        }
        
        var name: String {
            switch self {
            case .deposit:
                return "예금"
            case .loan:
                return "대출"
            }
        }
    }
```

❓ 한 가지 여쭤보고 싶은 점은 `Customer` 인스턴스가 각자 진행할 업무를 프로퍼티로 소유하고 있어야 해서 해당 프로퍼티의 타입은 `Bank.Work` 형식이 되었는데, 이런 방식으로 접근하는게 문제가 될지 궁금합니다.
```swift
struct Customer: Equatable {
    let bankWork: Bank.Work
    let waitingNumber: Int
}
```

### ⚙️ 업무별 은행원 구현
`2명의 은행원은 예금업무를, 1명의 은행원은 대출업무를 처리`에서 업무별로 `OperationQueue`를 나눠서 생성하여 각각의 `Queue`의 `Thread`수를 조절해주면 된다고 생각했습니다.
```swift
private var depositBankerQueue = OperationQueue()
private var loanBankerQueue = OperationQueue()
```
각 업무에 맞게 `Queue`를 분리하고, `Thread`수는 `Configuration`에 정의해둔 내용을 토대로 설정하도록 구현했습니다.

```swift
enum Configuration {
        static let numberOfDepositBanker = 2
        static let numberOfLoanBanker = 1
    ...
}

init() {
    depositBankerQueue.maxConcurrentOperationCount = Configuration.numberOfDepositBanker
    loanBankerQueue.maxConcurrentOperationCount = Configuration.numberOfLoanBanker
}
```
마지막으로 `OperationQueue`에 `Task`를 전달하는 곳에서, 고객별 `bankWork`타입에 맞는 `Queue`에 추가할 수 있도록 `switch`문을 구현하였습니다.

```swift
switch customer.bankWork {
case .deposit:
    depositBankerQueue.addOperation(task)
case .loan:
    loanBankerQueue.addOperation(task)
}
```


---

## 📚 참고 링크
- [🐻야곰 닷넷: 동시성 프로그래밍](https://yagom.net/courses/동시성-프로그래밍-concurrency-programming/)



---

안녕하세요 하비@havilog!
Step 3 수정 완료하였습니다.
수정 작업을 하다보니 저희가 요구사항에서 놓친 부분이 있었던 것 같아서, 해당 내용도 함께 작업하였습니다!

1. 말씀주신 Customer의 work 프로퍼티를 purpose(String), duration(Double) 두 가지 rawValue로 변경하였습니다.

![](https://hackmd.io/_uploads/BkwJlzGq3.png)

2. 업무가 마감되었을때, 업무별로 업무시간을 소수점 연산 형식으로 더해주도록 구현했었는데 요구사항을 확인해보니 실제로 Thread를 업무별로 쉬어주는 것으로 판단하여 아래와 같이 수정했습니다.
    - Thread.sleep()을 통해 필요한 시간만큼 쉬어줄 수 있도록 Task 내용에 추가하였습니다.
    - CFAbsoluteTimeGetCurrent()를 활용하여 전체 업무 종료시간에서 시작시간을 빼는 방식으로 전체 업무시간을 구했습니다.
    - 실제 시간을 측정하는 방식으로 변경됨에 따라 손님의 duration이 Decimal 형식일 필요가 없어져서 Double 타입으로 변경하였습니다.


---
# 은행창구 매니저 [STEP 4] - kyungmin, maxhyunm

안녕하세요, 하비@havilog! STEP 4 PR 올려드립니다. 늦어져서 죄송합니다!
화면을 그리면서 동시에 로직 처리가 되도록 구현하는 부분에서 어려움을 느낀 스텝이었습니다.
마지막까지 잘 부탁드립니다!

## 🤔 고민했던 점
### 👥 **대기중인 고객과 처리중인 고객 분리**
처음에는 `OperationQueue` 자체에 접근하여 해당 내용을 처리하려고 생각했습니다. `OperationQueue`에는 다음과 같은 프로퍼티가 있고, 각각 `KVO`로 업데이트 알림을 받을 수 있습니다.
- `operationCount` : 큐에 추가된 모든 작업의 수
- `operations` : 큐에 추가된 작업 리스트(각 작업별로 상태값 등 확인 가능)

하지만 위의 두 항목은 모두 `deprecated` 예정으로 되어 있어, 이 내용을 활용하는 것은 그다지 좋은 방법이 아닌 것 같았습니다. 또한 각 작업의 상세 내용(고객번호와 작업내용)을 가져오도록 만드는 부분에서도 어려움이 있었습니다.
때문에 고민을 하다가, STEP 3 기준으로 `print`문이 출력되는 시점에 대신 `UI`단의 대기/작업중 상태를 변경해주면 되겠다는 결론을 내렸습니다.
해당 내용을 처리하기 위해서는 `UI`가 업데이트되어야 하는 타이밍에 `Notification`을 발생시키거나, `delegate`를 통해 적절한 처리를 진행해야 했습니다. 여기서는 발생하는 이벤트를 오로지 한 곳에서만 받아 처리하면 되기 때문에 전체 알림을 발송하는 `NotificationCenter`보다는 `delegate` 패턴이 알맞을 것 같다고 판단하여, 후자로 진행하였습니다.
```swift
private func addTask(_ customer: Customer) {
    let task = BlockOperation {
        self.delegate?.moveCustomerToProcessQueue(customer)
        Thread.sleep(forTimeInterval: customer.duration)
        self.delegate?.popProcessingQueue(customer)
    }
    ...
}
```
### 🌟 **UI 관련 코드 main 스레드에서 실행시키기**
`delegate`나 `NotificationCenter`를 활용하면 `UI`를 변경하는 시점은 정확히 정할 수 있지만 한가지 문제가 있었습니다. `UI` 관련 내용은 `main` 스레드에서만 실행이 되어야 하는데, `OperationQueue` 작업 중에 해당 부분이 호출되면서 `main` 스레드가 아닌 곳에서 `UI`에 접근하는 형태가 된 것입니다. 때문에 컴파일러가 빌드를 진행하지 못하였습니다.
이 문제를 해결하기 위해 `UI`를 그리는 작업을 `OperationQueue.main`에 따로 `addOperation`을 통해 추가해주는 방식으로 진행하여 문제를 해결하였습니다.
```swift
OperationQueue.main.addOperation {
    ...
    self.waitingStackView.addArrangedSubview(customerView)
    ...
}
```

### 🌟 **Queue 처리가 완료되기 전에 실시간으로 UI 그리기**
처음에는 `OperationQueue`의 처리가 완료되기 전에는 `CustomerView`에 담긴 내용이 보이지 않았습니다. 데이터는 제대로 전달되지만, UI에 그리는 작업 자체가 실시간으로 진행되지 않는 것으로 보였습니다.
<img src="https://hackmd.io/_uploads/Syo3LE852.png" width="200">

어째서 비동기 큐를 쓰는데 작업을 기다리는지, `Task/await`을 써야 하는지 다양한 고민을 하다가 아래 코드를 발견했습니다. STEP 3에서 모든 일과가 마무리된 후 마감 메시지를 출력하기 위해 추가했던 부분이었습니다.

```swift
depositBankerQueue.waitUntilAllOperationsAreFinished()
loanBankerQueue.waitUntilAllOperationsAreFinished()
```
`waitUntilAllOperationsAreFinished()` 메서드는 큐의 모든 작업이 끝날 때까지 다음 작업을 대기합니다. 해당 부분을 삭제하자 `UI`가 정상적으로 그려지는 것을 확인할 수 있었습니다.
<img src="https://hackmd.io/_uploads/Hk8DeOIq2.png" width="200">



### 🌟 **대기-업무 상태 변화에 따라 알맞은 CustomerView 이동하기**
`StackView`에 추가된 `CustomerView`를 상태값에 따라 이동/삭제해주는 과정에서, 각 `StackView`의 `subview`들을 하나씩 훑으며 작업을 진행하게 되면 반복문이 너무 많아 비효율적이라는 생각이 들었습니다. 각 `View`에 `identifier`나 `key`값을 부여할 수 있는 방법이 있을지 고민하다가, `Dictionary`를 활용하는 방식을 선택하였습니다.
```swift
private var waitingDictionary: Dictionary<Customer, CustomerView> = [:]
private var processingDictionary: Dictionary<Customer, CustomerView> = [:]
```
이렇게 하면 `customer` 정보만 있어도 고객 상태값을 정확히 이동시킬 수 있고, 또 각 스택뷰가 비어있는지 여부를 확인할 때도 유용하다고 생각했습니다.(스택뷰가 모두 비어있는지를 확인하는 내용은 타이머를 일시정지시킬 때 활용하였습니다. - 대기/업무 스택이 모두 비어있으면 정지)
```swift
if self.processingDictionary.isEmpty && self.waitingDictionary.isEmpty {
    self.pauseTimer()
}
```

### 🌟 **UI가 그려지기 전에 완료된 Task 처리**
간혹 대기에서 업무로 넘어가지 않는 고객들이 있어 확인해보니, 대기중 `StackView`에 해당 고객 정보를 추가하는 작업이 `main` 스레드에서 진행되고 있을 때 `Bank`의 `OperationQueue`(별도의 스레드에서 작업)에서 이미 `Task`가 실행/종료되어 대기->업무로 넘어가는 작업을 정상적으로 거치지 못하는 것으로 보였습니다.
|<img src="https://hackmd.io/_uploads/rJZol_Lc2.png" width="200">|<img src="https://hackmd.io/_uploads/r1WslOIq3.png" width="200">|
|-|-|

이를 해결하기 위해 대기중인 고객을 업무중으로 변경하는 `moveCustomerToProcessQueue()` 메서드에 아래와 같은 내용을 추가하였습니다.
```swift
if waitingDictionary[customer] == nil {
    OperationQueue.main.waitUntilAllOperationsAreFinished()
}
```
이를 통해 대기->업무로 넘기는 작업을 진행하라는 요청이 넘어왔을 때 해당 고객이 대기중 `Dictionary`에 존재하지 않는다면 일단 `main` 스레드의 작업이 모두 끝날 때까지 대기한 후 다시 찾아볼 수 있도록 하였습니다.

### 🌟 **타이머에 millisecond 표시**
처음에는 `TimeInterval`의 포맷을 조절할 수 있는 `DateComponentsFormatter`를 활용해보려고 했습니다. 하지만 해당 포매터에서 다룰 수 있는 내용에는 millisecond가 누락되어 있었습니다. 즉, 초 단위까지밖에 포맷이 적용되지 않았습니다. 결국 원하는 형태로 타이머를 구현하기 위해 `TimeInterval` 자체를 minute/second/millisecond 단위로 계산하여 스트링 포매터를 사용하는 방식을 선택하였습니다.
해당 내용은 확장성을 고려하여 `Double` 타입에 `extension`으로 구현하였습니다.
```swift
extension Double {
    func formatTimeIntervalToString() -> String {
        let minutes = String(format: "%02d", Int((self/60).truncatingRemainder(dividingBy: 60)))
        let seconds = String(format: "%02d", Int(self.truncatingRemainder(dividingBy: 60)))
        let milliseconds = String(format: "%003d", Int(self.truncatingRemainder(dividingBy: 1) * 1000))

        return "\(minutes):\(seconds):\(milliseconds)"
    }
}
```

### 🌟 **StackView의 정렬**
대기/업무중인 고객의 `CustomerView`가 제대로 상단에 정렬되지 못하고 세로 길이가 멋대로 늘어나는 등 문제가 발생하였습니다.
<img src="https://hackmd.io/_uploads/HJesW_L92.png" width="200">

처음에는 대기/업무 `StackView` 내부 요소에 `setContentHuggingPriority`를 낮게 설정한 `emptyLabel`을 추가하여 조절해보았습니다. 하지만 그렇게 하니 매번 `Task`가 진행될 때마다 `emptyLabel`을 `SuperView`에서 제거한 후 마지막에 다시 추가하는 작업을 거쳐야 해서 번거로웠습니다.
```swift
OperationQueue.main.addOperation {
    customerView.removeFromSuperview()
    self.waitingDictionary[customer] = nil
    self.emptyProcessingView.removeFromSuperview()
    self.processingStackView.addArrangedSubview(customerView)
    self.processingDictionary[customer] = customerView
    self.processingStackView.addArrangedSubview(self.emptyProcessingView)
}
```

해당 부분은 두 개의 `StackView`를 담는 `Horizontal StackView`에서 `alignment`를 `top`으로 조절하여 해결하였습니다.
```swift
stackView.alignment = .top
```

### 🌟 **Bank 타입 변경**
`OperationQueue`를 위한 `Task` 클로저에서 타입 내부의 메서드와 프로퍼티에 접근하려면 값 타입이 아닌 참조 타입이어야 했기에 `Bank` 타입을 `class`로 변경하였습니다.