---
# System prepended metadata

title: "\U0001F3E6 은행창구 매니저"
tags: [project]

---

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

## Ground Rules

### 규칙
- TIL, 일일 회고 작성 시간(매일 22시부터 1시간 작성 진행)
- 페어프로그래밍 시간 제한 최대 30분

### 스크럼
- 오전 11시 디스코드에서 진행
- 금일 진행 사항 공유하기(오늘의 할일)

### 프로젝트 규칙
- 네이밍 준수하기(가이드 라인)
- 커밋 메시지 규칙 진행
- 코드에 대한 기록 그때그때 하기


## 일일 스크럼
### 🙌 07/10
- 오늘의 컨디션
    - minsup: 좋았습니다
    - hoon: 주말에 푹 쉬었습니다😆
- 특이사항
    - minsup: 저녁에 운동을 다녀와보겠습니다🏋️‍♀️
    - hoon: 저녁에 운동을 다녀오겠습니다🏃🏻
- 오늘 할 일 
    - [ ] STEP 1 구현
    - [ ] STEP 1 PR 보내기

### 🙌 07/11
- 오늘의 컨디션
    - minsup: 
    - hoon: 좋습니다💪
- 특이사항
    - minsup: 늦잠을 잤습니다😪
    - hoon: 비가 옵니다🌧️
- 오늘 할 일 
    - [x] STEP 1 구현
    - [x] STEP 1 PR 보내기

### 🙌 07/12
- 오늘의 컨디션
    - minsup: 늦게자서 매우 졸린상태
    - hoon: 조금 피곤합니다😢
- 특이사항
    - minsup: 6시반쯤 가족외식
    - hoon: 오후에 가능하면 낮잠을🤣
- 오늘 할 일 
    - [x] STEP 2 구현

### 🙌 07/13
- 오늘의 컨디션
    - minsup: 무기력합니다...
    - hoon: 좋습니다😊
- 특이사항
    - minsup: 없습니다!
    - hoon: 활동학습 예습 및 동시성 공부. 너무 어렵습니다.😢
- 오늘 할 일 
    - [x] STEP 1 PR 답변
    - [ ] STEP 2 PR 보내기

### 🙌 07/14
- 오늘의 컨디션
    - minsup:좋아요
    - hoon: 꿉꿉해요😭
- 특이사항
    - minsup: 없습니다
    - hoon: STEP 3 예습
- 오늘 할 일 
    - [x] STEP 2 PR 보내기 

### 🙌 07/17
- 오늘의 컨디션
    - minsup:
    - hoon: 많이 피곤해요🥱
- 특이사항
    - minsup: 예비군
    - hoon: 활동 학습 예습
- 오늘 할 일 
    - [x] STEP 3 구현 
    - [x] STEP 3 PR 보내기

### 🙌 07/18
- 오늘의 컨디션
    - minsup: 매우 피곤
    - hoon:
- 특이사항
    - minsup: 예비군
    - hoon:
- 오늘 할 일 
    - [x] 
    - [x] 

### 🙌 07/19
- 오늘의 컨디션
    - minsup: 매우 피곤
    - hoon:
- 특이사항
    - minsup: 예비군
    - hoon:
- 오늘 할 일 
    - [x] 
    - [x] 

### 🙌 07/20
- 오늘의 컨디션
    - minsup: 매우 피곤
    - hoon:
- 특이사항
    - minsup: 예비군
    - hoon:
- 오늘 할 일 
    - [x] 
    - [x] 

### 🙌 07/21
- 오늘의 컨디션
    - minsup:매우 좋음
    - hoon:
- 특이사항
    - minsup:
    - hoon:
- 오늘 할 일 
    - [x] 
    - [x] 

# 🏦 은행창구 매니저

## 📖 목차
1. [소개](#-소개)
2. [팀원](#-팀원)
3. [타임라인](#-타임라인)
4. [시각화된 프로젝트 구조](#-시각화된-프로젝트-구조)
5. [실행 화면](#-실행-화면)
6. [고민한 점](#-고민한-점)
7. [트러블 슈팅](#-트러블-슈팅)
8. [참고 링크](#-참고-링크)
9. [팀 회고](#-팀-회고)

</br>

## 🍀 소개

minsup, hoon 팀이 만든 은행창구 매니저 콘솔 및 UI앱입니다. 업무를 수행하는 은행원의 수를 정하여 각 은행원이 고객의 업무를 수행합니다. 고객 추가 버튼을 누르면 10명의 고객이 생성되어 대기중 스택에 추가되고 해당 고객의 업무를 스레드가 처리하기 시작하면 업무중 스택으로 옮겨지게 됩니다. 스레드가 업무를 마치면 업무중 스택에서 제거됩니다. 총 업무시간을 기록하여 상단에 위치한 레이블에 표시합니다. 초기화 버튼을 누르면 업무 중이던 작업, 고객, 총 업무시간이 초기화 됩니다.

* 주요 개념: `Concurrent Programming`, `GCD`, `Operation`, `Queue`, `LinkedList`, `Unit Test`, `Timer`, `CustomView`

</br>

## 👨‍💻 팀원
|  minsup | hoon |
| :--------: | :--------: |
| <Img src = "https://avatars.githubusercontent.com/u/79740398?v=4" width="200"> |<Img src="https://i.imgur.com/zXoi5OC.jpg" width="200" height="200"> |
|[Github Profile](https://github.com/agilestarskim) |[Github Profile](https://github.com/Hoon94) |

</br>

## ⏰ 타임라인
|날짜|내용|
|:--:|--|
|2023.07.10.| Node, LinkedList, Queue 타입 생성 | 
|2023.07.11.| Node, LinkedList, Queue 테스트 작성 |
|2023.07.14.| Customer, BankManger, Bank 타입 생성 |
|2023.07.17.| Task 타입 생성, GCD를 통한 동시 업무 수행 |
|2023.07.18.| DispatchQueue에서 Operation으로 변경 |
|2023.07.19.| 스택뷰에 customer정보 삽입 삭제 구현 |
|2023.07.21.| Timer 구현 및 리팩토링, Delegate패턴을 통한 VC와 Model 통신|

</br>

## 👀 시각화된 프로젝트 구조

### ℹ️ File Tree
    BankManagerUIApp
    ├── Application
    │   ├── AppDelegate.swift
    │   └── SceneDelegate.swift
    ├── Controller
    │   └── BankManagerViewController.swift
    ├── Model
    │   ├── Bank
    │   │   ├── Bank.swift
    │   │   ├── Customer.swift
    │   │   └── TimerManager.swift
    │   └── Queue
    └── View
        ├── BankManagerView.swift
        └── CustomerCellView.swift
        
    BankMangerConsoleApp    
    ├── Bank
    │   ├── Bank.swift
    │   ├── BankManager.swift
    │   └── Customer.swift
    ├── Queue
    │   ├── LinkedList.swift
    │   ├── Node.swift
    │   └── Queue.swift
    └── main.swift



### 📐 Diagram
<p align="center">
<img width="500" src="https://hackmd.io/_uploads/rkeCWUCY3.jpg">
</p>


## 💻 실행 화면 

### 🖥️ ConsoleApp

| 은행 개점 |
|:--------:|
|<img src="https://hackmd.io/_uploads/HyEHINCYn.gif" width="480">|

### 📱 UIApp

| 은행 개점 | 고객 추가 |
|:--------:|:--------:|
|<img src="https://hackmd.io/_uploads/rkk8A4dc2.gif" width="250">|<img src="https://hackmd.io/_uploads/SJRLANuc3.gif" width="250">|

| 작업 종료 후 초기화 | 작업 진행 중 초기화 |
|:--------:|:--------:|
|<img src="https://hackmd.io/_uploads/rJTDC4O92.gif" width="250">|<img src="https://hackmd.io/_uploads/BkddR4O9n.gif" width="250">|

</br>

## 🤯 고민한 점

### 구현 방법 선택

### 1️⃣ `Dispatch`의 `semaphore value`로 은행원을 2명으로 제한하는 방법

<details>
<summary>내용</summary>
<div markdown="1">
    
- 코드

    ```swift
    private let depositSemaphore = DispatchSemaphore(value: 2)
    private let depositQueue = DispatchQueue(label: "deposit", attributes: .concurrent)

    //...

    while let client = clientWaitingLineQueue.dequeue() {
        switch client.banking {
        case .deposit:
            depositQueue.async(group: group) { [self] in
                depositSemaphore.wait()
                banker.work(client: client)
                depositSemaphore.signal()
            }
            
        //...생략
    }
    ```

- 코드 설명

    * 세마포어 `value`를 2로 설정해 줌으로서 아무리 많은 스레드가 생기더라도 2개의 스레드만 `work()`를 호출할 수 있습니다.
    * `depositQueue`는 비동기이므로 `task`의 순서를 보장하지 않았습니다. 
    * 순서를 보장하지 않는다는 뜻은 고객 간 새치기를 할 수 있다고 비유할 수 있었습니다. (번호표 의미 없음)
    * 또한 테스크를 비동기적으로 수행하기 위해 여러 스레드가 생성되므로 은행원의 수가 여러 명이라고 생각할 수 있습니다.
    * 스레드를 은행원이라고 비유한다면 요구사항을 100프로 충족했다고 보기는 힘들 것 같았습니다.
    * 마치 은행원이 여러 명 대기해있는 상황이고 2명의 은행원만 교대로 일한다고 생각했습니다.

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


- 결과

    ![](https://hackmd.io/_uploads/BkHt-fmc3.png)

    실제 lldb를 통해 확인해 본 결과 수많은 스레드가 생성되어 대기 중인 상태인 것을 확인할 수 있었습니다.
    
</div>
</details>

### 2️⃣ `DispatchQueue`의 `serial` 방식과 `semaphore`로 은행원을 2명으로 제한하는 방법

<details>
<summary>내용</summary>
<div markdown="1">

- 코드

    ```swift
    func start() {

        let semaphore = DispatchSemaphore(value: 1)
        var doneCustomers: Set<Int> = []

        let depositClerk1 = DispatchQueue(label: "depositClerk1")
        let depositClerk2 = DispatchQueue(label: "depositClerk2")


        func depositWork(customer: Customer) -> DispatchWorkItem {
            return DispatchWorkItem {
                semaphore.wait()
                if !doneCustomers.contains(customer.priority) {
                    doneCustomers.insert(customer.priority)
                    semaphore.signal()
                    print("업무 시작")
                    Thread.sleep(forTimeInterval: 생략)
                    print("업무 완료")
                }
                semaphore.signal()
            }
        }
        
        //...생략

        while let customer = self.customers.dequeue() {
            switch customer.task {
            case .deposit:
                depositClerk1.async(group: group, execute: depositWork(customer: customer))
                depositClerk2.async(group: group, execute: depositWork(customer: customer))
                
            //...생략
    ```

- 코드 설명

    * 첫 번째 방법에서 여러 스레드를 만드는 것을 방지하기 위해 은행원의 수에 맞게 시리얼 큐인 `depositClerk1`과 `depositClerk2` 두 개를 만들었습니다. 이후 네이밍을 `depositQueue`로 하는 것이 자연스럽다고 생각하여 수정하였습니다.
    * 그 후 두 개의 큐에 같은 `WorkItem`을 배정하였습니다.
    * 그럼 그 큐는 세마포어를 이용해 각각의 `task`를 부여받습니다.
    * 부여받는 알고리즘은 다음과 같습니다.
    * 임계 영역에 접근한 뒤 고객의 번호가 `doneCustomer` 집합에 포함되어 있는지 확인합니다.
    * 포함되어 있으면 이미 누군가(자신 포함) 작업한 고객이라는 의미이므로 세마포어를 풀고 리턴합니다.
    * 포함되어 있지 않으면 아무도 작업하지 않은 고객이라는 의미이므로 `doneCustomer`에 추가하고 작업을 시작합니다. 작업을 마치면 세마포어를 풀고 `task`를 종료합니다.
    * 이렇게 되면 각 큐마다 하나의 스레드만 생성되므로 스레드의 개수가 은행원이라고 비유해 봤을 때 요구사항을 올바르게 충족한다고 볼 수 있었습니다.
    * 하지만 예금 고객 줄이 두 줄인데 똑같은 고객이 두 개의 줄을 모두 차지하고 있는 그림이기에 100% 현실을 반영한다고 보기는 어려웠습니다.

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

- 결과

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

    스레드는 새로 생겨나거나 사라지지 않는 것을 확인할 수 있었습니다. 

</div>
</details>

### 3️⃣ `OperationQueue`의 상태를 통해 고객을 분배하는 방법
    
<details>
<summary>내용</summary>
<div markdown="1">
    
- 코드

    ```swift
    let depositClerk1 = OperationQueue()
    depositClerk1.maxConcurrentOperationCount = 1
    let depositClerk2 = OperationQueue()
    depositClerk2.maxConcurrentOperationCount = 1

    while let customer = self.customers.dequeue() {
        switch customer.task {
        case .deposit:
            if depositClerk1.operationCount < depositClerk2.operationCount {
                depositClerk1.addOperation(work(customer: customer))
            } else {
                 depositClerk2.addOperation(work(customer: customer))
            }
            
        //...생략
    ```

- 코드 설명

    * 이전 살펴본 두 번째 방법에서는 두 개의 큐에 똑같은 고객이 복사되었습니다.
    * 그 문제를 해결하기 위해 고객을 나눈 뒤 큐에 들어가는 방식을 고안했습니다.
    * 큐 안에 몇 개의 오퍼레이션이 있나 확인 후 더 적은 오퍼레이션을 가지고 있는 큐에 고객을 넣는 식으로 구현했습니다.
    * `DispatchQueue`는 상태를 알기 어렵기 때문에 `Operation`을 사용했습니다.
    * 이렇게 구현하면 똑같은 고객이 큐에 들어가는 것을 방지할 수 있었습니다.
    * 하지만 `Operation`을 이용하면 스레드가 고정되지 않고 변경되는 문제가 발생하였습니다.(이유는 찾지 못함😢)

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

- 결과

    ![](https://hackmd.io/_uploads/HJ2oGMQq3.png)
    
    5번 스레드가 사라지고 새로운 스레드가 생성된 것을 확인할 수 있었습니다.

</div>
</details>

### 4️⃣ 시스템이 자동으로 `task`를 분배하는 방법
    
<details>
<summary>내용</summary>
<div markdown="1">

- 코드

    ```swift
    let depositQueue = OperationQueue()
    depositQueue.maxConcurrentOperationCount = 2

    while let customer = customers.dequeue() {
        switch customer.task {
        case .deposit:
            depositQueue.addOperation(work(customer: customer))
            
        //...생략
    }
    ```

    * `OperationQueue`의 `maxConcurrentOperationCount`의 값을 2로 주면 `semaphore`의 `value`와 비슷한 효과를 줄 수 있습니다.
    * 따라서 3번째 방법인 상태 비교를 하지 않아도 시스템이 알아서 `task`를 분배하게 됩니다.
    * 또한 예금 고객 대기 줄을 한 개로 유지할 수 있다는 장점이 있습니다.

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

- 결과

    ![](https://hackmd.io/_uploads/rJNkmzm5h.png)
    
    이전 방법과 같이 새로운 스레드가 생기고 사라지는 것을 확인할 수 있었습니다.

</div>
</details>
    
</br>

## 🧨 트러블 슈팅

1️⃣ **`removeAll()` 할 때 `Node deinit` 시점** <br>
-
🔒 **문제점** <br>
- `LinkedList`의 `removeAll()` 메소드를 구현하던 중 의문이 들었습니다.

    > * 모든 노드를 순회하며 `nil`을 할당해서 메모리에서 해제시켜야 하는가?
    > * 첫 노드만 `nil`을 할당하면 연결된 모든 노드들이 메모리에서 자동으로 해제되는가?

🔑 **해결방법** <br>
- 질문의 해답을 찾기 위해 직접 실험을 해보았습니다. head에 nil을 할당하면 연쇄적으로 연결된 모든 노드들이 메모리에서 해제되는 것을 확인했습니다.

    ```swift
    mutating func removeAll() {
        head = nil
        tail = nil
    }
    ```

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

    `LinkedList`를 사용하여 새로운 `Node`가 추가되면 기존에 있던 `Node`의 `next` 프로퍼티를 통해 다음 `Node`를 참조합니다. 이렇게 각각의 `Node`는 다음에 오는 `Node`를 참조하고 있고 참조하는 `Node`가 없는 경우 `nil`을 가지고 있습니다. 첫 `Node`는 `head`가 참조하고 있습니다. `removeAll()` 메소드를 사용하여 각각 노드를 참조하고 있는 `head`와 `tail`에 `nil`을 할당하면 앞의 `Node`를 참조하고 있던 다음 `Node` 객체의 `reference count`는 0이 되어 메모리에서 해제됩니다. 그러므로 앞에 있던 `Node`부터 순차적으로 `deinit`이 실행됩니다.

<br>

2️⃣ **`commandline tool`에서 테스트하는 법** <br>
-
🔒 **문제점** <br>
- `commandline tool`에서 `Unit Test`를 진행하려고 했을 때 `iOS App` 과는 다른 문제점이 있었습니다. 아래의 설정처럼 `Target to be Tested`를 설정하는 것이 불가능했습니다.
 
    ![](https://hackmd.io/_uploads/SyxjXj9F3.png)

🔑 **해결방법** <br>
- 이를 해결하기 위해 몇 가지 추가적인 설정이 필요했습니다. 테스트하고자 하는 파일의 `Target MemberShip`에 Test 타겟을 설정해야 했습니다. Test 타겟을 설정하면 `compile source`에 포함시켜 실행시킬 때 함께 빌드 할 수 있도록 합니다.

   

<br>

3️⃣ **객체의 `private` 프로퍼티를 테스트 환경에서 접근하기** <br>
-
🔒 **문제점** <br>
- `Unit Test`를 하기 위해 아래와 같은 객체가 있을 때 테스트 코드에서 `head`와 `tail`을 어떻게 사용할지 고민을 했습니다. 프로퍼티에 대한 테스트와 노출된 메소드를 검증하기 위한 비교 대상으로 사용하기에는 `private`으로 은닉화가 되어 있어 외부에서 사용하기가 불가능했습니다. 

    ```swift
    struct LinkedList<Value> {
        private var head: Node<Value>?
        private var tail: Node<Value>?
        ...
    }
    ```

🔑 **해결방법** <br>
- 전처리문을 사용하여 테스트 코드에서만 적용되는 `extension`을 만들 수 있었습니다.

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

    ```swift
    //MARK: - Extension for Unit Test
    #if canImport(XCTest)
    extension LinkedList {
        var exposedHead: Node<Value>? {
            return head
        }

        var exposedTail: Node<Value>? {
            return tail
        }
        ...
    }
    #endif
    ```

<br>

4️⃣ **특정 셀을 지우는 방법** <br>
-
🔒 **문제점** <br>
- 대기 중에서 진행 중으로 변경된 특정 `customerCell`을 대기 중 셀에서 어떻게 지울지 고민하였습니다.

🔑 **해결방법** <br>
- 스택뷰에는 `subviews`라는 속성이 있었고 모든 스택뷰의 아이템을 순회할 수 있었습니다. 하지만 어떤 셀을 지워야 할지에 대한 정보가 아이템에 없었기 때문에 지울 방법이 없었습니다. 이전 프로젝트에서 사용한 `tag` 속성을 이용해서 문제를 해결하였습니다. 처음에 스택뷰에 셀을 넣어줄 때 `tag` 속성에 이번 프로젝트에서 유니크하게 사용되는 `numberTicket`값을 넣어주어서 뷰에 아이디를 부여하였습니다. 그 후 스택뷰의 `subviews`를 `forEach`로 돌며 `UIView`의 `tag`를 확인하는 조건문을 사용할 수 있게 되었습니다.
    
    ```swift
    //add
    label.tag = customer.numberTicket

    //move
    waitContentStackView.subviews.forEach { subview in
        if subview.tag == customer.numberTicket {
            waitContentStackView.removeArrangedSubview(subview)
            subview.removeFromSuperview()
            workContentStackView.addArrangedSubview(subview)
        }
    }
    ```

<br>

5️⃣ **`Custom View init`문제** <br> 
-
🔒 **문제점** <br>
- `UIView`를 상속받아 고객 정보를 담는 `CustomView`를 구현했습니다. `CustomView`에는 고객 정보를 담을 `label`과 작업에 따른 글자 색의 정보가 필요했습니다.

    해당 정보를 외부에서 주입받기 위해 `init`을 만드는 과정에서 많은 문제가 생겼습니다. `frame`을 `super`에게 전해줘야 하는데 `custom init`을 만들면 `frame`을 알지 못해 외부에서 `frame`을 주입받아야 한다는 단점이 있었습니다.

🔑 **해결방법** <br>
- `frame`을 전해주고 오토레이아웃을 잡아주면 `frame`이 무시된다는 것을 깨달았고 `frame`을 초기에 `.zero`로 준 뒤 오토레이아웃을 잡아 문제를 해결하였습니다.

<br>

6️⃣ **`UI`는 `main` 스레드에서 그리기** <br>
-
🔒 **문제점** <br>
- 은행원이 작업을 처리하는 과정을 `main` 스레드가 아닌 `global` 스레드에서 동작하도록 하였습니다. 은행원이 작업을 시작하며 화면의 `CustomerCellView`를 대기 중에서 업무 중으로 변경하도록 합니다. 이런 경우 `global` 스레드에서 화면을 그리는 작업을 실행하여 문제가 발생하였습니다.

🔑 **해결방법** <br>
- 이를 해결하기 위해 위와 같이 코드를 수정하였습니다.

    ```swift
    func addWaitingQueue(customer: Customer) {
        DispatchQueue.main.async {
            let message = "\(customer.numberTicket) - \(customer.task.information.title)"
            let color = customer.task == .deposit ? UIColor.black : UIColor.purple
            let customerCell = CustomerCellView(message: message, color: color, tag: customer.numberTicket)
            self.bankManagerView.waitContentStackView.addArrangedSubview(customerCell)
        }
    }
    
    func moveToWorkingQueue(customer: Customer) {
        DispatchQueue.main.async {
            self.bankManagerView.waitContentStackView.subviews.forEach { subview in
                if subview.tag == customer.numberTicket {
                    self.bankManagerView.waitContentStackView.removeArrangedSubview(subview)
                    subview.removeFromSuperview()
                    self.bankManagerView.workContentStackView.addArrangedSubview(subview)
                }
            }
        }
    }
    
    func removeWorkingQueue(customer: Customer) {
        DispatchQueue.main.async {
            self.bankManagerView.workContentStackView.subviews.forEach { subview in
                if subview.tag == customer.numberTicket {
                    self.bankManagerView.workContentStackView.removeArrangedSubview(subview)
                    subview.removeFromSuperview()
                }
            }
        }
    }
    ```

    `global` 스레드에서 각각의 메소드를 호출하면 메소드 내부에서 다시 `main` 스레드에서 실행할 수 있도록 `DispatchQueue.main`을 사용하여 `task`를 이동시킵니다.


<br>

7️⃣ **`Timer`가 계속 멈추는 문제** <br> 
-
🔒 **문제점** <br>
- `Timer`가 동작 중에 멈추는 경우가 발생하였습니다. 특정 동작에 따른 조건에서 멈추기보다는 정확히 어떤 경우에 타이머가 멈추는지 알기가 어려웠고 여러 실험을 하면서 스레드와 관련하여 문제가 있다고 유추했습니다. 이를 기준으로 다음의 코드를 살펴보았습니다.

    ```swift
    func start() {
        assignClerk()
        timer.resume()
        DispatchQueue.global().async {
            self.distributeCustomers()
        }
    }

    //...생략

    private func distributeCustomers() {
        while let customer = customers.dequeue() {
            
            self.timerDelegate?.addWaitingQueue(customer: customer)
            
            switch customer.task {
            case .deposit:
                depositQueue.addOperation(work(customer: customer))
            case .loan:
                loanQueue.addOperation(work(customer: customer))
            }
        }
        
        depositQueue.waitUntilAllOperationsAreFinished()
        loanQueue.waitUntilAllOperationsAreFinished()
        totalTaskTime = timer.suspend()
    }
    ```

    `timer`의 `suspend()` 메소드가 위와 같이 `global` 스레드에서 동작합니다. 즉, 고객 추가 버튼을 누를 경우 여러 번의 `start()` 메소드가 실행되고 그 수만큼의 `global` 스레드가 생성되어 `suspend()` 메소드를 호출하게 됩니다. `suspend` 메소드에서는 다음과 같이 `state`을 기준으로 동작합니다.
    
    ```swift
    func suspend() -> TimeInterval {
        if state == .suspended {
            return totalTaskTime
        }
        
        let currentTime = Date()
        state = .suspended
        timer.suspend()
        totalTaskTime += currentTime.timeIntervalSince(startTime)
        
        return totalTaskTime
    }
    ```
    
    여러 스레드에서 동작하던 중 공유 데이터인 `state`에 접근하여 값을 바꾸는 동작과 `if` 문에서 `state`을 기준으로 분기를 처리하는 과정이 동시에 일어나게 되면 `state`가 `suspended`로 변경되기 전에 다른 스레드에서 `if` 문의 조건이 `false`일 수 있습니다. 이런 경우 `timer`의 `suspend()` 메소드가 여러 번 호출이 될 수 있으며 타이머가 동작을 중지하는 문제를 일으킬 수 있습니다.

🔑 **해결방법** <br>
- 이러한 문제를 해결하기 위해 공유 데이터인 `state`에 접근하는 스레드의 수를 제한하기 위해 `DispatchSemaphore`를 사용하였습니다.
    
    ```swift
    private func distributeCustomers() {
        //...생략
        
        semaphore.wait()
        totalTaskTime = timer.suspend()
        semaphore.signal()
    }
    ```

</br>

## 📚 참고 링크
- [🍎Apple Docs: Access Control](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/accesscontrol)
- [🍎Apple Docs: Timer](https://developer.apple.com/documentation/foundation/timer)
- [🍎Apple Docs: UIStackView](https://developer.apple.com/documentation/uikit/uistackview)
- [🍎Apple Docs: Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)
- [🍎Apple Docs: Generics](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/)
- [🍎Apple Docs: cancelAllOperations()](https://developer.apple.com/documentation/foundation/operationqueue/1417849-cancelalloperations)
- [🍏Apple Archive: Concurrency Programming Guide](https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html)
- [📼Apple WWDC: Concurrent Programming With GCD in Swift 3](https://developer.apple.com/videos/play/wwdc2016/720/)
- [📙Swift forums: 테스트를 위한 전처리문](https://forums.swift.org/t/how-do-you-know-if-youre-running-unit-tests-when-calling-swift-test/49711/4)
- [📘stackOverflow: Swift extension for selected class instance](https://stackoverflow.com/questions/37682420/swift-extension-for-selected-class-instance)
- [📘blog: DispatchSourceTimer](https://medium.com/over-engineering/a-background-repeating-timer-in-swift-412cecfd2ef9)
- [📘blog: intrinsicContentSize](https://magi82.github.io/ios-intrinsicContentSize/)
- [📘blog: CommandLineTool](https://jwonylee.tistory.com/entry/XCode-Swift-Command-Line-Tool-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-%EC%9C%A0%EB%8B%9B-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EA%B8%B0)

</br>

## 👥 팀 회고
- [팀 회고 링크]()

# 팀 회고
## 우리팀이 잘한 점
- 

## 우리팀 개선할 점
- 

## 서로에게 좋았던 점 피드백
- to minsup:
- to hoon

## 서로에게 하고싶은 말
- to minsup:
- to hoon
    





# PR: STEP 1

## 고민되었던 점

### `removeAll()` 할 때 `Node deinit` 시점
- `LinkedList`의 `removeAll()` 메소드를 구현하던 중 의문이 들었습니다.
    > * 모든 노드를 순회하며 `nil`을 할당해서 메모리에서 해제시켜야 하는가?
    > * 첫 노드만 `nil`을 할당하면 연결된 모든 노드들이 메모리에서 자동으로 해제되는가?

    질문의 해답을 찾기 위해 직접 실험을 해보았습니다. head에 nil을 할당하면 연쇄적으로 연결된 모든 노드들이 메모리에서 해제되는 것을 확인했습니다.

    ```swift
    mutating func removeAll() {
        head = nil
        tail = nil
    }
    ```

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

    `LinkedList`를 사용하여 새로운 `Node`가 추가되면 기존에 있던 `Node`의 `next` 프로퍼티를 통해 다음 `Node`를 참조합니다. 이렇게 각각의 `Node`는 다음에 오는 `Node`를 참조하고 있고 참조하는 `Node`가 없는 경우 `nil`을 가지고 있습니다. 첫 `Node`는 `head`가 참조하고 있습니다. `removeAll()` 메소드를 사용하여 각각 노드를 참조하고 있는 `head`와 `tail`에 `nil`을 할당하면 앞의 `Node`를 참조하고 있던 다음 `Node` 객체의 `reference count`는 0이 되어 메모리에서 해제됩니다. 그러므로 앞에 있던 `Node`부터 순차적으로 `deinit`이 실행됩니다.

### `commandline tool`에서 테스트하는법
- `commandline tool`에서 `Unit Test`를 진행하려고 했을 때 `iOS App` 과는 다른 문제점이 있었습니다. 아래의 설정처럼 `Target to be Tested`를 설정하는 것이 불가능했습니다.
 
    ![](https://hackmd.io/_uploads/SyxjXj9F3.png)
    
    이를 해결하기 위해 몇 가지 추가적인 설정이 필요했습니다. 테스트하고자 하는 파일의 `Target MemberShip`에 Test 타겟을 설정해야 했습니다.

    - [참고 링크](https://jwonylee.tistory.com/entry/XCode-Swift-Command-Line-Tool-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EC%97%90%EC%84%9C-%EC%9C%A0%EB%8B%9B-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%95%98%EA%B8%B0)

### `xcode` 버전이 달라서 생기는 문제
- 저희는 서로 다른 xcode 버전을 사용 중입니다.(14.3, 14.2)
    버전이 서로 달라 테스트를 할 때 14.3은 `testplan`이 필요했고 14.2는 필요 없었습니다. 이런 문제 때문에 사용하는 환경을 한쪽에 맞춰서 작업을 진행해야 했습니다. 다행히 14.2를 기준으로 만들 `Unit Test scheme`을 14.3 환경에서 사용할 수 있었습니다.

## 조언을 얻고 싶은 부분

### 테스트 함수 이름 짓기
- 테스트 함수의 이름을 지을 때 많은 고민을 했습니다. 통일성, 논리성, 내용을 포함하는지 등등 다양한 상황에 대해 고민했습니다. 

    해당 테스트 코드에 대한 제목을 고민하는 과정에서 여러 후보들이 있었는데 어떤 것이 더 좋을까요? 더 좋은 후보가 있으면 추천해 주시면 감사하겠습니다.

    ```swift
    func test_????() {
        // given
        sut.append(1)
        sut.append(2)
        sut.append(3)
        sut.append(4)

        // when
        let expectation = [1, 2, 3, 4]

        // then
        XCTAssertEqual(sut.elements, expectation)
    }
    ```

    - func test_요소x를_추가하면_요소x가_저장된다()
    - func test_요소1234를_추가하면_요소1234가_저장된다()


### 객체의 `private` 프로퍼티를 테스트 환경에서 어떻게 접근해야 하는지 고민
- 아래와 같은 객체가 있을 때 테스트 코드에서 `head`와 `tail`을 어떻게 사용할지 고민을 했습니다.

    ```swift
    struct LinkedList<Value> {
        private var head: Node<Value>?
        private var tail: Node<Value>?
        ...
    }
    ```

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

    저희는 방법을 찾아보다가 전처리문을 사용하여 테스트 코드에서만 적용되는 `extension`을 만들 수 있었습니다.

    ```swift
    //MARK: - Extension for Unit Test
    #if canImport(XCTest)
    extension LinkedList {
        var exposedHead: Node<Value>? {
            return head
        }

        var exposedTail: Node<Value>? {
            return tail
        }
        ...
    }
    #endif
    ```
    
    이 방법은 괜찮은 방법일까요? 더 좋은 방법이 있다면 어떤 것이 있을까요?










안녕하세요, 델마()
STEP 2에서는 크게 고민한 점 없이 페어 프로그래밍을 진행한 것 같습니다.
부족한 부분이 있다면 언제든 편하게 코멘트 남겨주시면 감사하겠습니다.😆

# PR: STEP 2

## 고민되었던 점

### `OOP` 관점에서의 객체 분리
- 전체 프로그램이 실행하는 부분에 대해서 고민을 하였습니다. `Bank`에서 은행 업무를 시작하는 `start` 메소드를 선언하였고 실제로 은행이 실행하는지에 대한 판단은 `Bank` 자체에서 하는 것이 아닌 외부 즉 `main`에서 결정해 줘야 한다고 생각하였습니다. 이를 위해 사용자의 입력을 받는 부분을 `main`에 작성하였습니다.















안녕하세요, 델마()
STEP 3 PR 드립니다. 처음 비동기를 해보며 실제 동작을 예상해 보는 게 무척 어려웠습니다.🤣
이번 STEP도 잘 부탁드립니다. 감사합니다.🙇

# PR: STEP 3

## 고민되었던 점

### 구현 방법 선택

### 1️⃣ `Dispatch`의 `semaphore value`로 은행원을 2명으로 제한하는 방법

- 코드

    ```swift
    private let depositSemaphore = DispatchSemaphore(value: 2)
    private let depositQueue = DispatchQueue(label: "deposit", attributes: .concurrent)

    //...

    while let client = clientWaitingLineQueue.dequeue() {
        switch client.banking {
        case .deposit:
            depositQueue.async(group: group) { [self] in
                depositSemaphore.wait()
                banker.work(client: client)
                depositSemaphore.signal()
            }
            
        //...생략
    }
    ```

- 코드 설명

    * 세마포어 `value`를 2로 설정해 줌으로서 아무리 많은 스레드가 생기더라도 2개의 스레드만 `work()`를 호출할 수 있습니다.
    * `depositQueue`는 비동기이므로 `task`의 순서를 보장하지 않았습니다. 
    * 순서를 보장하지 않는다는 뜻은 고객 간 새치기를 할 수 있다고 비유할 수 있었습니다. (번호표 의미 없음)
    * 또한 테스크를 비동기적으로 수행하기 위해 여러 스레드가 생성되므로 은행원의 수가 여러 명이라고 생각할 수 있습니다.
    * 스레드를 은행원이라고 비유한다면 요구사항을 100프로 충족했다고 보기는 힘들 것 같았습니다.
    * 마치 은행원이 여러 명 대기해있는 상황이고 2명의 은행원만 교대로 일한다고 생각했습니다.

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


- 결과

    ![](https://hackmd.io/_uploads/BkHt-fmc3.png)

    실제 lldb를 통해 확인해 본 결과 수많은 스레드가 생성되어 대기 중인 상태인 것을 확인할 수 있었습니다.

### 2️⃣ `DispatchQueue`의 `serial` 방식과 `semaphore`로 은행원을 2명으로 제한하는 방법

- 코드

    ```swift
    func start() {

        let semaphore = DispatchSemaphore(value: 1)
        var doneCustomers: Set<Int> = []

        let depositClerk1 = DispatchQueue(label: "depositClerk1")
        let depositClerk2 = DispatchQueue(label: "depositClerk2")


        func depositWork(customer: Customer) -> DispatchWorkItem {
            return DispatchWorkItem {
                semaphore.wait()
                if !doneCustomers.contains(customer.priority) {
                    doneCustomers.insert(customer.priority)
                    semaphore.signal()
                    print("업무 시작")
                    Thread.sleep(forTimeInterval: 생략)
                    print("업무 완료")
                }
                semaphore.signal()
            }
        }
        
        //...생략

        while let customer = self.customers.dequeue() {
            switch customer.task {
            case .deposit:
                depositClerk1.async(group: group, execute: depositWork(customer: customer))
                depositClerk2.async(group: group, execute: depositWork(customer: customer))
                
            //...생략
    ```

- 코드 설명

    * 첫 번째 방법에서 여러 스레드를 만드는 것을 방지하기 위해 은행원의 수에 맞게 시리얼 큐인 `depositClerk1`과 `depositClerk2` 두 개를 만들었습니다. 이후 네이밍을 `depositQueue`로 하는 것이 자연스럽다고 생각하여 수정하였습니다.
    * 그 후 두 개의 큐에 같은 `WorkItem`을 배정하였습니다.
    * 그럼 그 큐는 세마포어를 이용해 각각의 `task`를 부여받습니다.
    * 부여받는 알고리즘은 다음과 같습니다.
    * 임계 영역에 접근한 뒤 고객의 번호가 `doneCustomer` 집합에 포함되어 있는지 확인합니다.
    * 포함되어 있으면 이미 누군가(자신 포함) 작업한 고객이라는 의미이므로 세마포어를 풀고 리턴합니다.
    * 포함되어 있지 않으면 아무도 작업하지 않은 고객이라는 의미이므로 `doneCustomer`에 추가하고 작업을 시작합니다. 작업을 마치면 세마포어를 풀고 `task`를 종료합니다.
    * 이렇게 되면 각 큐마다 하나의 스레드만 생성되므로 스레드의 개수가 은행원이라고 비유해 봤을 때 요구사항을 올바르게 충족한다고 볼 수 있었습니다.
    * 하지만 예금 고객 줄이 두 줄인데 똑같은 고객이 두 개의 줄을 모두 차지하고 있는 그림이기에 100% 현실을 반영한다고 보기는 어려웠습니다.

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

- 결과

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

    스레드는 새로 생겨나거나 사라지지 않는 것을 확인할 수 있었습니다. 


### 3️⃣ `OperationQueue`의 상태를 통해 고객을 분배하는 방법

- 코드

    ```swift
    let depositClerk1 = OperationQueue()
    depositClerk1.maxConcurrentOperationCount = 1
    let depositClerk2 = OperationQueue()
    depositClerk2.maxConcurrentOperationCount = 1

    while let customer = self.customers.dequeue() {
        switch customer.task {
        case .deposit:
            if depositClerk1.operationCount < depositClerk2.operationCount {
                depositClerk1.addOperation(work(customer: customer))
            } else {
                 depositClerk2.addOperation(work(customer: customer))
            }
            
        //...생략
    ```

- 코드 설명

    * 이전 살펴본 두 번째 방법에서는 두 개의 큐에 똑같은 고객이 복사되었습니다.
    * 그 문제를 해결하기 위해 고객을 나눈 뒤 큐에 들어가는 방식을 고안했습니다.
    * 큐 안에 몇 개의 오퍼레이션이 있나 확인 후 더 적은 오퍼레이션을 가지고 있는 큐에 고객을 넣는 식으로 구현했습니다.
    * `DispatchQueue`는 상태를 알기 어렵기 때문에 `Operation`을 사용했습니다.
    * 이렇게 구현하면 똑같은 고객이 큐에 들어가는 것을 방지할 수 있었습니다.
    * 하지만 `Operation`을 이용하면 스레드가 고정되지 않고 변경되는 문제가 발생하였습니다.(이유는 찾지 못함😢)

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

- 결과

    ![](https://hackmd.io/_uploads/HJ2oGMQq3.png)
    
    5번 스레드가 사라지고 새로운 스레드가 생성된 것을 확인할 수 있었습니다.

### 4️⃣ 시스템이 자동으로 `task`를 분배하는 방법

- 코드

    ```swift
    let depositQueue = OperationQueue()
    depositQueue.maxConcurrentOperationCount = 2

    while let customer = customers.dequeue() {
        switch customer.task {
        case .deposit:
            depositQueue.addOperation(work(customer: customer))
            
        //...생략
    }
    ```

    * `OperationQueue`의 `maxConcurrentOperationCount`의 값을 2로 주면 `semaphore`의 `value`와 비슷한 효과를 줄 수 있습니다.
    * 따라서 3번째 방법인 상태 비교를 하지 않아도 시스템이 알아서 `task`를 분배하게 됩니다.
    * 또한 예금 고객 대기 줄을 한 개로 유지할 수 있다는 장점이 있습니다.

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

- 결과

    ![](https://hackmd.io/_uploads/rJNkmzm5h.png)
    
    이전 방법과 같이 새로운 스레드가 생기고 사라지는 것을 확인할 수 있었습니다.

## 해결이 되지 않은 점

### `Overlapping accesses` 문제
- `Bank`의 `measureTime` 메소드는 비동기적으로 동작하는 은행원의 업무 수행 시간을 계산합니다.

    ```swift
    mutating func start() {
        assignClerk()
        totalTaskTime = measureTime {
            distributeCustomers()
        }
        announceResult()
    }

    //...생략

    private func measureTime(perform: () -> Void) -> CFAbsoluteTime {
        let start = CFAbsoluteTimeGetCurrent()
        perform()
        let end = CFAbsoluteTimeGetCurrent()
        
        return end - start
    }
    ```
    
    하지만 추상화 수준이 맞지 않는다는 문제가 있었고 이를 해결하기 위해 `measureTime` 메소드에서 수행 시간을 반환하는 것이 아닌 내부에서 직접 변경하는 방식으로 코드를 수정해 보았습니다.
    
    ```swift
    mutating func start() {
        assignClerk()
        measureTime {
            distributeCustomers()
        }
        announceResult()
    }

    //...생략

    private mutating func measureTime(perform: () -> Void) {
        let start = CFAbsoluteTimeGetCurrent()
        perform()
        let end = CFAbsoluteTimeGetCurrent()
        totalTaskTime = end - start
    }  
    ```
    
    위처럼 코드를 수정하는 경우 다음과 같은 에러가 발생하였습니다.
    
    ![](https://hackmd.io/_uploads/rJn9BxXq3.png)

    저희가 예상한 바로는 `measureTime` 메서드에서 `distributeCustomers` 메소드를 복사하는 과정에서 내부에서 사용하는 `self.customers`와 `distributeCustomers` 메소드의 `mutating` 키워드 간에 문제가 발생한 건가라는 의문이 들었습니다. 하지만 실제 코드에서는 이 부분과 관계없이 `totalTaskTime`에 값을 저장하는 부분만 변경 시 에러가 해결되었습니다. 아직까지 어떤 부분에 대해서 메모리 접근에 대한 충돌이 발생했는지에 대해 정확히 이해하지 못하고 있어 이 부분에 대해 질문드립니다.
    
    > [Overlapping accesses appear primarily in code that uses in-out parameters in functions and methods or mutating methods of a structure. The specific kinds of Swift code that use long-term accesses are discussed in the sections below.](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/memorysafety#Conflicting-Access-to-self-in-Methods)
    
















# PR: STEP 4

## 고민되었던 점

### 특정 셀을 지우는 방법 (`tag`)
- 대기 중에서 진행 중으로 변경된 특정 `customerCell`을 대기 중 셀에서 어떻게 지울지 고민하였습니다.
    스택뷰에는 `subviews`라는 속성이 있었고 모든 스택뷰의 아이템을 순회할 수 있었습니다.
    하지만 어떤 셀을 지워야 할지에 대한 정보가 아이템에 없었기 때문에 지울 방법이 없었습니다.
    이전 프로젝트에서 사용한 `tag` 속성을 이용해서 문제를 해결하였습니다.
    처음에 스택뷰에 셀을 넣어줄 때 `tag` 속성에 이번 프로젝트에서 유니크하게 사용되는 `numberTicket`값을 넣어주어서 뷰에 아이디를 부여하였습니다.
    그 후 스택뷰의 `subviews`를 `forEach`로 돌며 `UIView`의 `tag`를 확인하는 조건문을 사용할 수 있게 되었습니다.
    
    ```swift
    //add
    label.tag = customer.numberTicket

    //move
    waitContentStackView.subviews.forEach { subview in
        if subview.tag == customer.numberTicket {
            waitContentStackView.removeArrangedSubview(subview)
            subview.removeFromSuperview()
            workContentStackView.addArrangedSubview(subview)
        }
    }
    ```

### `Custom View init`문제

- `UIView`를 상속받아 고객 정보를 담는 `CustomView`를 구현했습니다.
    `CustomView`에는 고객 정보를 담을 `label`과 작업에 따른 글자 색의 정보가 필요했습니다.

    해당 정보를 외부에서 주입받기 위해 `init`을 만드는 과정에서 많은 문제가 생겼습니다.
    `frame`을 `super`에게 전해줘야 하는데 `custom init`을 만들면 `frame`을 알지 못해 외부에서 `frame`을 주입받아야 한다는 단점이 있었습니다.

    하지만 `frame`을 전해주고 오토레이아웃을 잡아주면 `frame`이 무시된다는 것을 깨달았고 `frame`을 초기에 `.zero`로 준 뒤 오토레이아웃을 잡아 문제를 해결하였습니다.


### `UI`는 `main` 스레드에서 그리기
- 은행원이 작업을 처리하는 과정을 `main` 스레드가 아닌 `global` 스레드에서 동작하도록 하였습니다. 은행원이 작업을 시작하며 화면의 `CustomerCellView`를 대기 중에서 업무 중으로 변경하도록 합니다. 이런 경우 `global` 스레드에서 화면을 그리는 작업을 실행하여 문제가 발생하였습니다.

    ```swift
    func addWaitingQueue(customer: Customer) {
        DispatchQueue.main.async {
            let message = "\(customer.numberTicket) - \(customer.task.information.title)"
            let color = customer.task == .deposit ? UIColor.black : UIColor.purple
            let customerCell = CustomerCellView(message: message, color: color, tag: customer.numberTicket)
            self.bankManagerView.waitContentStackView.addArrangedSubview(customerCell)
        }
    }
    
    func moveToWorkingQueue(customer: Customer) {
        DispatchQueue.main.async {
            self.bankManagerView.waitContentStackView.subviews.forEach { subview in
                if subview.tag == customer.numberTicket {
                    self.bankManagerView.waitContentStackView.removeArrangedSubview(subview)
                    subview.removeFromSuperview()
                    self.bankManagerView.workContentStackView.addArrangedSubview(subview)
                }
            }
        }
    }
    
    func removeWorkingQueue(customer: Customer) {
        DispatchQueue.main.async {
            self.bankManagerView.workContentStackView.subviews.forEach { subview in
                if subview.tag == customer.numberTicket {
                    self.bankManagerView.workContentStackView.removeArrangedSubview(subview)
                    subview.removeFromSuperview()
                }
            }
        }
    }
    ```

    이를 해결하기 위해 위와 같이 코드를 수정하였습니다. `global` 스레드에서 각각의 메소드를 호출하면 메소드 내부에서 다시 `main` 스레드에서 실행할 수 있도록 `DispatchQueue.main`을 사용하여 `task`를 이동시킵니다.

### `Timer`가 계속 멈추는 문제
- `Timer`가 동작 중에 멈추는 경우가 발생하였습니다. 특정 동작에 따른 조건에서 멈추기보다는 정확히 어떤 경우에 타이머가 멈추는지 알기가 어려웠고 여러 실험을 하면서 스레드와 관련하여 문제가 있다고 유추했습니다. 이를 기준으로 다음의 코드를 살펴보았습니다.

    ```swift
    func start() {
        assignClerk()
        timer.resume()
        DispatchQueue.global().async {
            self.distributeCustomers()
        }
    }

    //...생략

    private func distributeCustomers() {
        while let customer = customers.dequeue() {
            
            self.timerDelegate?.addWaitingQueue(customer: customer)
            
            switch customer.task {
            case .deposit:
                depositQueue.addOperation(work(customer: customer))
            case .loan:
                loanQueue.addOperation(work(customer: customer))
            }
        }
        
        depositQueue.waitUntilAllOperationsAreFinished()
        loanQueue.waitUntilAllOperationsAreFinished()
        totalTaskTime = timer.suspend()
    }
    ```

    `timer`의 `suspend()` 메소드가 위와 같이 `global` 스레드에서 동작합니다. 즉, 고객 추가 버튼을 누를 경우 여러 번의 `start()` 메소드가 실행되고 그 수만큼의 `global` 스레드가 생성되어 `suspend()` 메소드를 호출하게 됩니다. `suspend` 메소드에서는 다음과 같이 `state`을 기준으로 동작합니다.
    
    ```swift
    func suspend() -> TimeInterval {
        if state == .suspended {
            return totalTaskTime
        }
        
        let currentTime = Date()
        state = .suspended
        timer.suspend()
        totalTaskTime += currentTime.timeIntervalSince(startTime)
        
        return totalTaskTime
    }
    ```
    
    여러 스레드에서 동작하던 중 공유 데이터인 `state`에 접근하여 값을 바꾸는 동작과 `if` 문에서 `state`을 기준으로 분기를 처리하는 과정이 동시에 일어나게 되면 `state`가 `suspended`로 변경되기 전에 다른 스레드에서 `if` 문의 조건이 `false`일 수 있습니다. 이런 경우 `timer`의 `suspend()` 메소드가 여러 번 호출이 될 수 있으며 타이머가 동작을 중지하는 문제를 일으킬 수 있습니다.
    
    이러한 문제를 해결하기 위해 공유 데이터인 `state`에 접근하는 스레드의 수를 제한하기 위해 `DispatchSemaphore`를 사용하였습니다.
    
    ```swift
    private func distributeCustomers() {
        //...생략
        
        semaphore.wait()
        totalTaskTime = timer.suspend()
        semaphore.signal()
    }
    ```
 

## 조언을 얻고 싶은 부분

### 에니메이션 추가
- 셀이 사라질 때 뚝뚝 끊기는 느낌이 들어 animation을 사용하고 싶은데 어떻게 적용하는지 감이 오지 않습니다.어떻게 에니메이션을 추가할 수 있을까요?
