# 그라운드 룰 1. 만나는 시간: 10시 2. 점심 시간: 1시~2,3시 3. 저녁 시간: 6시~7시 4. 개인 공부 시간: 월, 목 2시전까지, PR 보낸 후 리뷰 오기 전까지 5. 프로젝트 마무리 시간: ~ 10시 6. step 나오면 같이 읽어보고 공부해보기 # 일일 스크럼 ### 🙌 07/10 월 - 오늘의 컨디션 - Redmango🥭: 문제 없습니다. - Yetti🦊: 좋습니다! - 특이사항 - Redmango🥭: 수업끝나고 1시간정도 볼일이 있습니다! - Yetti🦊: 저녁시간에 늦게 될 수도 있어서 곧 말씀드릴게요~ ### 🙌 07/11 화 - 오늘의 컨디션 - Redmango🥭: 좋습니다 - Yetti🦊: 오랜만에 많이 잔 것 같아 아주 상쾌합니다👍 - 특이사항 - Redmango🥭: 문제 없습니다. - Yetti🦊: 오늘은 큰 특이사항 없습니다 - 오늘 할 것 - [x] linkedlist의 메서드 명이 적합한지 확인(enqueue,dequeue) - [x] 유닛 테스트 진행 - [x] 연산프로퍼티 접근제어 및 반환메서드 관련 검색 - [ ] 링크드 리스트의 장단점 찾아보고 정리해서 PR에 추가 - [ ] step1 PR 정리해서 날리기 - [ ] step2 같이 읽어보기 ### 🙌 07/12 수 - 오늘의 컨디션 - Redmango🥭: 좋습니다 - Yetti🦊: - 특이사항 - Redmango🥭: 없습니다 - Yetti🦊: 개인 스케줄로 참석 못합니다ㅠ ### 🙌 07/13 목 - 오늘의 컨디션 - Redmango🥭: 좋습니다 - Yetti🦊: - 특이사항 - Redmango🥭: 없습니다 - Yetti🦊: ### 🙌 07/14 금 - 오늘의 컨디션 - Redmango🥭: 나쁘지 않습니다 - Yetti🦊: 좋아요! - 특이사항 - Redmango🥭: 문제 없습니다 - Yetti🦊: 점심먹고 카페서 공부할 예정입니다. ### 🙌 07/17 월 - 오늘의 컨디션 - Redmango🥭: 좋진 않습니다 헣ㅎ 어제 술을 먹어서... - Yetti🦊: 늦게 자서 살짝 피곤합니다 ㅠ - 특이사항 - Redmango🥭: 없습니다 - Yetti🦊: 없습니다! ### 🙌 07/18 화 - 오늘의 컨디션 - Redmango🥭: 좋습니다 - Yetti🦊: 좋습니다 - 특이사항 - Redmango🥭: KWDC23 참석 - Yetti🦊: KWDC23 참석 ### 🙌 07/19 수 - 오늘의 컨디션 - Redmango🥭: 나쁘지 않습니다 - Yetti🦊: 오랜만에 잠을 많이 자서 개운합니다 - 특이사항 - Redmango🥭: 저도.... - Yetti🦊: 배고픕니다.. ### 🙌 07/20 목 - 오늘의 컨디션 - Redmango🥭: 그저 그렇습니다 - Yetti🦊: - 특이사항 - Redmango🥭: 없습니다 - Yetti🦊: 개인일정으로 계속 왔다갔다예정입니다 ㅠㅠ ### 🙌 07/21 금 - 오늘의 컨디션 - Redmango🥭: - Yetti🦊: - 특이사항 - Redmango🥭: 저녁 8시 멘토링 - Yetti🦊: # Step1 PR ## 고민했던 점 ### 1. `LinkedList의 타입` - `LinkedList`의 타입을 `class`로 해야할지 `struct`로 해야할지 고민하였습니다. `Node`의 경우 다음 노드의 주소값을 가지고 있어야하니 `class`로 구현해야할 이유가 있었지만 `LinkedList`의 경우에는 현재 `head`와 `tail`의 주소값만 가지고 있으면 되니 특별히 `class`로 구현해야할 필요가 없을거라 판단되어 `struct`로 구현하였습니다. ### 2. `LinkedListable을 통한 추상화` - `LinkedList`를 구현할 때 `의존성 역전(DIP)`을 고려하여 `LinkedListable`라는 프로토콜을 만들어 `LinkedList`타입에서 채택하고 `CustomerQueue`타입에서는 해당 프로토콜의 인스턴스를 받아와 사용하려고 했었습니다. 하지만 `LinkedList` 타입에서 제네릭을 사용하면서 `protocol`엔 `associatedtype`을 사용해야 했고 그 이후 `CustomerQueue`타입에서 난항에 빠졌습니다. 결과적으로 서포터즈 `하모`가 아래와 같이 구현하는 데에 성공했습니다. ```swift= protocol LinkedListable<Element> { associatedtype Element var peek: Element? { get } var isEmpty: Bool { get } mutating func clear() mutating func addLast(_ data: Element) mutating func popFirst() -> Element? } struct LinkedList<Element>: LinkedListable { ... } struct CustomerQueue<Element> { var customerQueue: any LinkedListable<Element> = LinkedList() var peek: Element? { ... } var isEmpty: Bool { ... } mutating func clear() { ... } mutating func enqueue(_ newElement: Element) { ... } mutating func dequeue() -> Element? { ... } } ``` <br> 하지만 저희가 생각해 내어 구현한 것도 아닌 제대로 모르는 개념을 프로젝트 코드에 적용하는 것은 안된다고 생각하였고, 프로토콜과 제네릭을 함께 쓰게 되면 고려해야 할 점과 필요한 지식이 아직은 많이 부족하다고 느끼게 되어 최종적으로 LinkedList의 추상화 과정은 삭제하였습니다. 추후에 더 공부하고 추가할 수 있다면 추가해 볼 예정입니다. ## 링크드리스트의 장단점 ### 장점 - 다음 노드의 주소값을 가지고 있기 때문에 연속적으로 데이터를 저장할 필요가 없다. 즉 데이터의 추가, 삭제가 자유롭다.(단, 순차적으로 접근해야한다) - 배열에 비해 저장 공간의 활용이 자유롭다. ### 단점 - 데이터를 탐색할 때 인덱스로 접근하는 것이 아니라 첫번째 데이터부터 순차적으로 접근하기 때문에 접근 속도가 느리고 비효율적이다. - 노드에서 연결 정보를 저장하는 별도의 데이터 공간이 필요하기 때문에 저장 공간의 효율이 높지 않다. - 다음 노드를 가리키는 주소가 하나라도 잘못 된다면 이후의 모든 연결이 끊어진다. - 포인터를 위한 추가적인 메모리 공간이 필요하다. 위와 같은 점 때문에 기본적인 `LinkedList`는 값(노드)를 추가할 때마다 포인터가 마지막 노드로 순차적으로 이동하여 마지막 노드에 새로운 노드의 주소값을 추가해 주어야 합니다. 이때의 시간 복잡도는 노드의 갯수에 따라 달라지기 때문에 O(n)입니다. 저흰 시간 복잡도를 줄여보고자 항상 마지막 노드를 가리키고 있는 tail을 새로 추가하였습니다. 새로운 노드가 추가되면 포인터가 따로 움직일 필요 없이 현재 tail이 가리키는 노드에 새로운 노드의 주소값을 추가한 뒤 새로운 노드를 다시 tail이 가리키게 만들어 시간 복잡도를 줄여보았습니다. # Step2 PR ### 1. **sync 와 async 의 차이는?** - 동기(Synchronous) - 작업이 끝나기를 기다리는 것 ex) 1의 작업이 끝난뒤 2의 작업을 시작한다 - 즉 모든 작업이 순차적으로 일어난다. - 순차적으로 진행되기 때문에 설계가 간단하고 직관적이다 - 비동기(Asynchronous) - 작업이 끝나기를 기다리지 않는 것 ex) 1의 작업이 끝나지 않았는데 2의 작업을 시작한다 - 즉 모든 작업을 동시에 진행한다. - 동기에 비해 설계가 복잡하다.(아마 끝나는 타이밍이 제각각인것 때문인 것 같다.) ### 2. **[weak self]는 무엇인가?** - 메모리 누수의 해결책 중 하나로 순환참조를 방지하기 위해 사용되는 키워드입니다. 보통 클로저 내부에서의 약한 참조를 나타냅니다. swift에선 대부분의 참조 문제를 ARC로 해결이 가능하지만 2개 이상의 객체가 서로를 강한 참조하고 있는 경우에 메모리 누수가 발생할 위험성이 있고 그런 위험성이 예상될 때 [weak self]를 사용해줍니다. 보통 비동기 작업에서 [weak self]를 사용하여 클로저 내에서 self에 접근하는 경우가 많습니다. 이를 통해 메모리 누수를 방지하고 앱의 성능과 안정성을 향상시킬 수 있습니다. (여러 문서와 개념들을 찾아보긴 했지만 아직 사용해본 적이 없어 완벽히 이해는 하지 못했습니다. step 3에서 순환참조의 문제가 발생하면 사용해보도록 하겠습니다!) ### 3. **DispatchQueue 는 무엇인가?** - `DispatchQueue`는 `GCD`를 사용하기 위한 대기열로,`GCD`기술의 일부입니다. `DispatchQueue`는 크게 `Serial Queue`와 `Concurrent Queue` 두 가지로 구분할 수 있습니다. `Serial`은 단일 스레드에서만 작업을 처리하고, `Concurrent`는 다중 스레드에서 작업을 처리합니다. (`GCD`는 `Grand Central Dispatch`의 약자로 멀티 코어 환경과 멀티 스레드 환경에서 최적화된 프로그래밍을 할 수 있도록 애플에서 개발한 기술입니다.) - 우리는 이 Queue에 작업을 추가해주기만 하면 시스템은 알아서 스레드를 관리하여 작업을 처리하도록 도와주기 때문에 아주 편하고 강력한 기능입니다. - DispatchQueue에 작업을 넘길 때 `2가지`를 꼭 정해주어야 합니다. 1. `단일 스레드(Serial)`를 사용할 것인가, `다중 스레드(Concurrent)`를 사용할 것인가 2. `동기로 작업(sync)`을 처리할 것인가, `비동기로 작업(async)`을 처리할 것인가 ## 고민했던 점 ### 1. Bank와 BankManager의 역할분리 - 기존에 존재하던 파일인 `BankManager`와 새롭게 정의한 `Bank`파일의 역할에 대해 고민하였습니다. 실제 은행에서 필요한 기능들과 프로그램 내에서 그 기능들을 수행하기 위한 작업을 `BankManager`에서 해주기를 원했습니다. 하지만 실제로 총 고객을 세는 `finishedCustomerCount` 메서드나 총 업무시간을 세는 `check`메서드 등의 위치가 모호했습니다. 최종적으로 `Bank`파일 내부에 구현해주었습니다. ### 2. 오늘 업무처리한 고객의 수를 큐에서 가져와야 할지 카운트를 해줘야할지 - 요구사항에 `업무처리한 고객의 수`라고 써 있지만 또 다른 사항엔 `모든 고객의 업무가 끝나야 은행 업무를 마무리한다`는 조건 때문에 처음엔 `고객의 수 = 업무처리한 고객`으로 받아들였습니다. 그래서 `bankQueue`에서 `count`를 반환하는 메소드를 추가하려했지만 혹시 고객이 업무를 보지 못하고 순번이 지나가는 상황이 발생하면 `고객의 수 ≠ 업무처리한 고객`이기 때문에 별도로 카운트를 해주기로 했습니다. ## 조언이 필요한 점 ### 1. 접근성 관리 방식 - 타입의 프로퍼티에 접근성을 설정해줄 때 private으로 설정 후 이니셜라이저를 이용해 접근성을 제공하는 방식이 좋은 방식인지 또 어떤 방식이 있는지 궁금합니다. # Step3 PR ## 고민했던점 ### 1. 예금큐와 대출큐를 따로 만들어야할지 하나의 큐로만 관리해야할지 - 처음 큐를 구현했을 때 각 고객마다 업무타입에 대해서 지정을 해준 후 하나의 큐에 담아서 관리를 해주었습니다. 하지만 이렇게 큐를 구현하게되면 다음과 같은 상황에 문제가 생겼습니다. [예금 고객의 업무가 모두 종료되었지만 다음 대기하는 고객이 대출 고객일경우 그 고객이 빠지지 않으면 다음 예금 고객을 받을 수 없다.] 해당 문제점을 업무 타입마다의 큐를 만들어서 은행원의 업무가 종료되면 다시 새로운 고객을 바로 받을 수 있도록 해주었습니다. ### 2. 스레드를 작업시마다 생성,삭제가 반복되게 해야할지 유지해야할지 기존엔 dispatchQueue를 Banker타입의 work메서드 내부에서 사용하여 work메서드가 실행될때마다 새로운 스레드가 생성되는 방식이였습니다. 그로인해 불필요해보이는 스레드가 많이 생겼고, 작업을 끝내면 사라지는 스레드라고 하지만 애초에 은행원 수 만큼의 스레드만 생성되는게 맞다고 생각하여 수정해 보았습니다. #### 실행결과 | | 기존 | 수정후 | | -------- | -------- | -------- | | 실행화면 |<Img src = "https://hackmd.io/_uploads/rkkWcqPqn.gif" width="600" height="600">| <Img src = "https://hackmd.io/_uploads/H1Yqj9v53.gif" width="600" height="600">| |git 링크|[Before](https://github.com/iOS-Yetti/ios-bank-manager/commit/cf4cdf8a7666ab39920a6fc8c364636d79e4a67e) |[After](https://github.com/iOS-Yetti/ios-bank-manager/blob/daa1cfea50fef6e79e41a77128bf3f29dc66f74f/BankManagerConsoleApp/BankManagerConsoleApp/Bank.swift) | ## 조언이 필요한 점 ### 1. DispatchQueue.global().async과 mutating DispatchQueue.global().async내부에서 mutating이 붙은 메서드를 호출하면 에러가 발생하였습니다. 값타입인 구조체를 여러 스레드에서 동시에 접근하여 수정하면 스레드 안전성을 보장할 수 없기 때문에 에러를 발생시킨다는 것이였습니다. DispatchQueue.global().async를 호출하는 Bank타입의 구조체를 클래스로 변경하여 해결하였습니다만 1. 참조타입은 왜 문제가 되지 않는지 2. 비동기 내부에서 호출한 메서드에 self가 붙어야하는 이유가 약한 참조 때문인것 같은데 맞을까요 3. 또한 저번에 공부한 weak self가 관련이 있는 것인지 궁금합니다. # README # 🏦 은행창구 매니저 🏦 - Yetti☃️, Redmango🥭 ## 📖 목차 1. [프로젝트 소개](#-프로젝트-소개) <br> 2. [팀원](#-팀원) <br> 3. [타임 라인](#-타임-라인) <br> 4. [시각화 구조](#-시각화된-구조) <br> 5. [실행 화면](#-실행-화면) <br> 6. [트러블 슈팅](#-트러블-슈팅) <br> 7. [팀 회고](#-팀-회고) <br> 8. [참고 링크](#-참고-링크) <br> </br> ## 🏦 프로젝트 소개 은행에 고객이 왔을 때 고객들의 업무에 따라 은행원들이 각자의 업무를 처리해주는 앱 > 프로젝트 기간 23/07/10 ~ 23/07/21 </br> ## 👨‍💻 팀원 | ☃️Yetti☃️ | 🥭Redmango🥭 | | :--------: | :--------: | | <Img src = "https://hackmd.io/_uploads/rJj1EtKt2.png" width="200" height="200"> | <Img src = "https://hackmd.io/_uploads/HJ2D-DoNn.png" width="200" height="200"> | |[Yetti's Github](https://github.com/iOS-Yetti) |[Redmango's Github](https://github.com/redmango1447) | </br> ## ⏱️ 타임라인 |날짜|내용| |:--:|--| |2023.07.10.| - Linked List 기본 기능 구현 | |2023.07.11.| - 유닛테스트 코드추가 | |2023.07.12.| - Pull Request 작성 | |2023.07.13.| - Bank, Banker, Customer 타입 구현 | |2023.07.14.| - 타입 별로 기능 분리 | |2023.07.17.| - 네이밍 리팩토링, 네임스페이스 구현 | |2023.07.19.| - BankTask타입 생성, 업무 부여 기능 추가 |2023.07.20.| - DispatchQueue를 통한 비동기 구현 | </br> ## 👀 시각화 구조 ### 🌲 File Tree ``` BankManagerConsoleApp ├── main ├── Bank | ├── BankManager | ├── Bank | ├── Banker | ├── BankTask | ├── Customer ├── CustomerQueue | ├── CustomerQueue | ├── LinkedList └─────── Node BankManagerUIApp ├── BankManagerUIApp | ├── App | | ├── AppDelegate | | └── SceneDelegate | ├── Model | | ├── BankManager | | ├── CustomerQueue | | ├── LinkedList | | └── Node | ├── View | | ├── Main.storyboard | | └── LaunchScreen.storyboard | ├── Controller | | └── ViewController | └── Resoruce | └── Asset.xcassets ├── BankManagerUIAppTests | ├── CustomelQueueTests | └── BankManagerUIAppTests └── BankManagerUIAppUITests └── BankManagerUIAppUITests ``` ### 📐 Diagram ![](https://hackmd.io/_uploads/SkK9zVD53.png) </br> ## 📺 실행 화면 <img src="https://hackmd.io/_uploads/ryqXHcD53.gif" width="500"> ## 🧨 트러블 슈팅 ###### 핵심 트러블 슈팅위주로 작성하였습니다. 1️⃣ **파일 복사시 참조** <br> - 🔒 **문제점** <br> - 기존 step1의 Queue와 관련 타입을 모두 UIApp에 구현하였습니다만 step2는 ConsolApp에서 Queue를 이용해 구현을 시도해보라고 하였습니다. 이 경우 ConsolApp에서 Queue및 관련 타입을 찾지 못하는 문제가 발생했습니다. 🔑 **해결방법** <br> - 생각해낸 해결 방법은 2가지로 1. ConsolApp에서 UIApp과 똑같은 이름의 파일을 만들고 코드를 복사,붙여넣기 한다. 2. UIApp에서 ConsolApp으로 Xcode를 이용해 복사하되 Create Folder Referances를 선택하고 Copy items if needed를 체크해제한다. 즉 원본 파일을 참조하는 파일을 ConsolApp내부에 만들되 실제 폴더에서 파일이 생성되진 않게 한다. 저희는 둘 중 2번째 방법으로 진행하였습니다. 추후에 어떻게 될진 모르지만 복사한 대상인 Queue와 LinkedList, Node 타입은 자료구조의 일종으로 두 개의 App에서 별개의 작동원리를 가지고 있지 않을 것으로 예상되고 또한 추후 변형될 여지도 적다고 생각했기 때문입니다. <br> 2️⃣ **업무마다 큐 관리하기** <br> - 🔒 **문제점** <br> 처음 큐를 구현했을 때 각 고객마다 업무타입에 대해서 지정을 해준 후 하나의 큐에 담아서 관리를 해주었습니다. 하지만 이렇게 큐를 구현하게되면 문제가 생겼습니다. 예를 들어, 예금 고객의 업무가 모두 종료되었지만 다음 대기하는 고객이 대출 고객일경우 그 고객이 빠지지 않으면 다음 예금 고객을 받을 수 없어 딜레이가 생기는 문제가 발생하였습니다. 🔑 **해결방법** <br> 고객의 업무 타입마다 다른 큐에 넣어줌으로써 업무 진행에 효율성을 증가시켜주었습니다. ```swift! private var depositQueue: CustomerQueue<Customer> = CustomerQueue() private var loanQueue: CustomerQueue<Customer> = CustomerQueue() private func lineUp(_ customers: inout [Customer]) { for number in 0..<customers.count { customers[number].receiveQueueNumber(queueNumber: number + 1) switch customers[number].task { case .deposit: depositQueue.enqueue(customers[number]) case .loan: loanQueue.enqueue(customers[number]) } } } ``` <br> 3️⃣ **은행원이 늘어날 때의 확장성 고려** <br> - 🔒 **문제점** <br> DispatchGroup에 넣어 사용할 DispatchWorkItem을 만들어 주고 그 내부에 각각의 업무에 해당하는 로직을 작성하여 주었습니다.하지만 이렇게 생성하게 되면 은행원이 늘어날 때마다 새로운 DispatchWorkItem을 만들어줘야하고 또 해당 DispatchWorkItem을 group에 넣어줘야하는 번거로움이 있었습니다. ```swift! let firstDepositWindow = DispatchWorkItem { [self] in while let depositCustomer = depositQueue.dequeue() { bankers[0].work(for: depositCustomer) countFinishedCustomer() checkWorkTime(from: bankers[0]) } } let secondDepositWindow = DispatchWorkItem { [self] in while let depositCustomer = depositQueue.dequeue() { bankers[1].work(for: depositCustomer) countFinishedCustomer() checkWorkTime(from: bankers[1]) } } let firstLoanWindow = DispatchWorkItem { [self] in while let loanCustomer = loanQueue.dequeue() { bankers[2].work(for: loanCustomer) countFinishedCustomer() checkWorkTime(from: bankers[2]) } } DispatchQueue.global().async(group: group, execute: firstDepositWindow) DispatchQueue.global().async(group: group, execute: secondDepositWindow) DispatchQueue.global().async(group: group, execute: firstLoanWindow) ``` 🔑 **해결방법** <br> 은행원의 수를 세는 반복문을 통해 내부에서 은행뭔마다 각각의 큐를 구분해 비동기처리를 해주는 방식으로 수정해주었습니다. 하나의 업무마다 하나의 DispatchWorkItem을 생성할 필요가 없어지고 코드 역시 더 간결해졌습니다. ```swift! for i in 0..<bankers.count { var queue: CustomerQueue<Customer> switch bankers[i].task { case .deposit: queue = depositQueue case .loan: queue = loanQueue } DispatchQueue.global().async(group: group) { [self] in while let customer = queue.dequeue() { bankers[i].work(for: customer) countFinishedCustomer() checkWorkTime(from: bankers[i]) } } } ``` ## 👥 팀 회고 ### to. Redmango - 프로젝트에서 속도를 추구하지않고 이해와 정확성을 요하는 모습을 통해 학습에 어떻게 임해야하는지 많이 배우는 기간이었습니다! - 이번 주에 제가 개인적인 일로 자리를 비우는 일이 많았는데 그럼에도 학습에 의의를 두고 계속 공부하고 고민하시는 모습이 너무 좋았습니다! ### to. Yetti - 제가 생각하지 못한 관점으로 의견을 제시하시는게 새로웠습니다! - 바쁘신 와중에도 어떻게든 시간을 내시는 모습이 감동이였습니다! ## 📚 참고 링크 - [inout 파라미터의 활용](https://hyunsikwon.github.io/swift/Swift-Inout-01/) - [sleep(forTimeInterval:)](https://developer.apple.com/documentation/foundation/thread/1413673-sleep) - [DispatchGroup 공식문서](https://developer.apple.com/documentation/dispatch/dispatchgroup) - [차근차근 시작하는 GCD — 7](https://sujinnaljin.medium.com/ios-%EC%B0%A8%EA%B7%BC%EC%B0%A8%EA%B7%BC-%EC%8B%9C%EC%9E%91%ED%95%98%EB%8A%94-gcd-7-4d9dbe901835) - [야곰닷넷 - 동시성 프로그래밍](https://yagom.net/courses/%eb%8f%99%ec%8b%9c%ec%84%b1-%ed%94%84%eb%a1%9c%ea%b7%b8%eb%9e%98%eb%b0%8d-concurrency-programming/) # Step1 코드리뷰 린트를 사용하셨나요 ? 이유는 무엇인가요 ? - 활동학습에서 의존성 관리도구에 대해 배우다 현재 프로젝트에 SwiftLint를 적용해보라고 해서 적용해봤습니다. 하지만 규칙이라던지 여러가지로 모르는 부분이 많아 활용하진 않았습니다 --- 구조체의 이유는 무엇인가요 ? - LinkedList를 구조체로 설정해준 이유는 먼저 LinkedList를 어딘가에서 상속받고 있지 않고 다른 타입을 상속할 필요도 없기 때문입니다. 그리고 클래스와 구조체의 선택에 대한 공식문서에서도 클래스에 있는 기능을 사용하지 않는다면 기본적으로 구조체를 구현하기를 권장하고 있기에 현재 저희의 LinkedList에서는 구조체가 더 괜찮다고 판단하였습니다. - 초기에 헷갈렸던 부분이 저장 프로퍼티인 head와 tail이 class 타입인 `Node`를 저장하기 때문에 LinkedList 구조체가 복사될 때 레퍼런스 복사가 발생할지도 모른다고 생각됐었습니다. 고민해 본 결과 인스턴스가 생성될 때에 head와 tail 프로퍼티는 빈값이고 인스턴스가 생성된 이후 `Node`가 추가될 때는 별개에 인스턴스에 `Node`가 새로 생성되어 추가되는 것이기 때문에 같은 레퍼런스를 참조하지 않을 것이란 판단이 들어 구조체를 사용해도 문제없을 거란 판단이 들었습니다. 만약 이미 생성된 뒤 head와 tail에 값이 존재하는 인스턴스를 복사하여 새로운 인스턴스를 만든다면 레퍼런스 복사가 발생하겠지만, private으로 인해 외부에서 head와 tail에 접근할 수 없고 LinkedList 내부 메서드에서 `Node`타입 프로퍼티에 직접적으로 접근하여 변경하는 메서드도 없으니 이런 경우에도 문제없을 거란 생각이 들었습니다 --- 제네릭에 제약을 걸려면 어떻게 해야하나요 ? - 제네릭에 제약을 주는 것은 클래스 타입 또는 프로토콜을 이용해 가능합니다.클래스 상속을 받는 경우, 프로토콜을 준수하는 경우 등이 있으며 문법적으로는 아래와 같습니다. ```swift= final class Node<Element: 프로토콜> { ... } //여러 제약의 경우 where절 활용 final class Node<Element: 프로토콜> where 프로토콜 { ... } ``` --- ```swift= func test_isEmpty_1을_enqueue하고_dequeue하면_isEmpty의값은_true이다() { // given sut.enqueue(1) let _ = sut.dequeue() ``` 이렇게 사용해주신 이유가 무엇인가요 ? 다른 방법으로는 어떤게 있을까요 ? - dequeue메서드의 경우 반환 값이 존재하지만 해당 test는 `isEmpty`메서드를 대상으로 하기 때문에 dequeue메서드의 기능만 필요하고 반환값은 사용하지 않아서 `_`를 사용했습니다. 다른 방법으로는 `@discardableResult`가 있는데 메서드 앞에 붙여주면 반환값을 사용하지 않아도 경고창이 뜨지 않습니다. --- var result = sut.dequeue() result = sut.dequeue() 이해는 가는데 뭔가 읽히지는 않네요. 개선한다면 어떻게 할 수 있을까요 ? - 아래와 같이 가독성을 개선해 보았습니다. 더 좋은 방법이 있다면 가르쳐주시면 감사하겠습니다! ```swift var test: Int? test = sut.dequeue() test = sut.dequeue() ``` # Step2 코드리뷰 ```swift! mutating private func check(to taskTime: Double) { ``` - **July**: 이름이 모호한 것 같습니다. - **코멘트**: checkWorkTime이라는 네이밍으로 수정했습니다. 추가적으로 매개변수로 taskTime을 받지않고 Banker타입을 받아와서 Banker타입에서 정의한 notifyWorkTime메서드를 통해 총 업무시간을 계산할 수 있도록 수정해주었습니다. ```swift! mutating private func finishedCustomerCount() { ``` - **July**: 네이밍이 잘 된게 맞을까요 ? - **코멘트**: 메서드라는 점을 고려해 동사로 시작할 수 있도록 countFinishedCustomer라는 이름으로 수정해주었습니다. ```swift! while !bankQueue.isEmpty { guard let currentCustomer = bankQueue.dequeue(), let taskTime = bankers[0].task(currentCustomer) else { ``` - **July**: 같은 의미긴 하지만 bankers[0] 보다는 first 가 더 좋아보이네요. - **코멘트**: first를 추가해주었습니다! ```swift! mutating func task(_ customer: Customer) -> Double? { ``` - **July**: 이름과 리턴값이 전혀 매치가 안되네요. - **코멘트**: 기능 분리를 통해 workTime만 리턴해주는 메서드를 생성하고 task메서드에서는 반환값이 없도록 수정해주었습니다. 추가적으로 task라는 네이밍도 어떤 task인지 명시되있지 않아서 동사적인 느낌보다 명사적인 느낌으로 와닿을 수 있을 것 같아 work라는 동사표현으로 수정해주었고 프로그램 내에 전체적인 표현도 task에서 work로 수정해주었습니다. ```swift! struct Customer { private(set) var queueNumber: Int? mutating func receiveQueueNumber(queueNumber: Int) { ``` - **July**: init에서 넣을 수는 없었나요? - **코멘트**: 현실에 빗대어 생각해볼때 Customer가 queueNumber를 부여받는게 초기화될 때가 아니라 bank에서 lineUp메서드를 통해 enqueue될 때(줄을 설 때) 부여받는게 맞다고 생각해서 따로 메서드를 만들어 주었습니다. ```swift! let test = BankManager() ``` - **July**: 이름이 test인가요 ? - **코멘트**: test를 위해 썼던 네이밍을 깜빡하고 수정하지 못했습니다. bankManager로 수정했습니다! ```swift! var isBankOpen: Bool = true ``` - **July**: isBankOpened 가 맞지 않을까요 ? - **코멘트**: 컨벤션적인 부분에서 뭐가 맞는 표현인지 헷갈려서 자료를 찾아본 후 isBankOpened이 옳다는 걸 알게되었습니다! [Bool변수 네이밍에 참고한 자료 링크](https://soojin.ro/blog/naming-boolean-variables) ```swift! private func openBank() { let bankers = createBankers(number: 1) var customers = createCustomers() var bank: Bank = Bank(bankers: bankers) ``` - **July**: 여긴 왜 타입이 들어가있죠? - **코멘트**: 저희가 이해한 바로는 통일성 관련해서 말씀해주신 것 같아서 타입추론형식으로 수정했습니다. 아니라면 말씀해주시면 감사하겠습니다! ```swift! struct Bank { private var bankers: [Banker] ``` - **July**: bankers 는 변하나요? - **코멘트**: 지금의 요구조건으로 생각해본다면 변하지 않을것으로 예상되기에 let으로 수정해 주었습니다. # step3 코드리뷰 ![](https://hackmd.io/_uploads/HkK7sAOcn.png) - 답변: capture list입니다. capture list는 closure 안에서 사용할 객체를 정의하는 목록입니다. DispatchQueue.global().async메서드는 인자 excute로서 escaping 클로저가 전달됩니다. escaping 클로저가 인자로 전달되는 경우 함수가 끝나 함수의 인자가 없어져도 클로저가 작동될 수 있는데 이는 암묵적으로 주변환경을 capture했기 때문입니다. 이때 capture한 변수나 상수등을 사용할때 self.을 명시적으로 언급해야하는데 캡쳐리스트에 [self]를 명시함으로서 self.접두사를 반복사용하지 않아도 self를 참조하게 할수 있습니다. 다만 이때 self는 강한 참조임으로 순환 참조가 발생 할 수 있어서 [weak self]를 사용하는게 권장된다고하여서 기존의 [self]를 [weak self]로 수정하였습니다. 순환 참조가 발생할 수 있습니다. ![](https://hackmd.io/_uploads/SkRNsC_53.png) - 답변: 상속이 없어 final 키워드를 붙여주었습니다! ![](https://hackmd.io/_uploads/ryCriC_c2.png) - 답변: group은 꼭 밖에 있을 필요가 없어서 메서드 내부로 옮겼지만 Queue와 bankers는 은행타입이 가지고 있어야하는 속성이라고 생각되어 밖에 선언해주었습니다. - 답변: CustomerQueue를 class로 수정해주었기 때문에 상수로 설정해주면 값 변경은 불가능하지만 내부 속성은 변경 가능합니다. enqueue, dequeue를 통해 내부 속성만 변경해주기 때문에 let으로 수정해주었습니다. ![](https://hackmd.io/_uploads/SJu5sAO52.png) - 답변: `startBankService()`메서드와의 통일성을 고려해`notifyEndBankService()`라는 메서드명으로 수정해주었습니다! ![](https://hackmd.io/_uploads/rJQSwJFc2.png) - 답변: 매개변수로 각 업무의 은행원 수를 받을 수 있는 메서드로 수정해주었습니다! ![](https://hackmd.io/_uploads/Sy2LP1tc3.png) - 답변: 10...30이라는 범위를 타입으로 표현하는 방법에는 `ClosedRange<Int>`라는 타입으로 선언하는 방법이 있습니다. 그래서 해당 타입으로 수정해주었는데 `1...customerNumbers` 이렇게 범위 연산자를 사용해 반복문을 돌리려다보니 `Int`타입이아니라 `ClosedRange<Int>`타입이라 에러가 발생했습니다. 해당 문제를 for문이 아닌 forEach반복문을 이용하는 방법으로 해결해주었습니다 ![](https://hackmd.io/_uploads/HyPdwyt53.png) - 답변: CaseIterable은 열거형의 모든 case들을 배열처럼 순회(iteration)할 수 있도록 도와주는 프로토콜입니다. `BankTask.allCases.randomElement()`를 사용하여 고객의 업무를 랜덤하게 지정해주기 위해서 채택하였습니다. ![](https://hackmd.io/_uploads/HJ2FDytqn.png) - 답변: task는 변하지 않기 때문에 let으로 수정해주었습니다. ![](https://hackmd.io/_uploads/SkyjvkFq3.png) - 답변: 두 매개변수의 위치를 바꿔주었습니다