Daehoon Lee
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    ![](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을 사용하고 싶은데 어떻게 적용하는지 감이 오지 않습니다.어떻게 에니메이션을 추가할 수 있을까요?

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully