### 규칙
- 월, 목: 오후 6시부터 시작
- 화, 수, 금: 오전 11시부터 시작
- 일정 있으면 사전에 연락하기
### 스크럼
- 특이사항, 오늘의 할 일 작성하기
### 프로젝트 규칙
- 네이밍 준수하기(가이드 라인)
- 커밋 메시지 규칙 준수하기
### 브랜치 이름 규칙
- 공동 작업 브랜치: step0, step1 ...
- 테스트 작업 브랜치: step0_test1, 2 ...
### 커밋 메시지 규칙
- 카르마 스타일 컨벤션 커밋메시지 사용
- TYPE: 설명
- TYPE = feat, fix, docs, style, refactor, test, chore
### 네이밍 컨벤션
- [API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/)" 참고
## 일일 스크럼
### 🙌 07/10
- 특이사항
- idinaloq: 7월 11일 화요일 오후 2시부터 진행 부탁드립니다. (치과 예약 있어요!!)
- EtialMoon: 없습니다~
- 오늘 할 일
- [ ] 활동학습 수업 참여하기
- [ ] Step 0 : 저장소 포크 및 복제
- [ ] Step 1 : 큐 타입 구현
### 🙌 07/12
- 특이사항
- idinaloq: 교정기 때문에 입을 크게 못벌려요 😢
- EtialMoon: 없습니다!
- 오늘 할 일
- [x] PR 답변 남기기
- [x] 코드 리팩토링
### 🙌 07/13
- 특이사항
- idinaloq: 없습니다
- EtialMoon: 없습니다!
- 오늘 할 일
- [x] STEP2 구현
- [x] PR 보내기
### 🙌 07/14
- 특이사항
- idinaloq: 없습니다
- EtialMoon: 없습니다
- 오늘 할 일
- [x] README 작성
### 🙌 07/15
- 특이사항
- idinaloq: 없습니다
- EtialMoon: 없습니다
- 오늘 할 일
- [x] PR 코멘트 달기
### 🙌 07/17
- 특이사항
- idinaloq: 조금 피곤하네요 😩
- EtialMoon: 없습니다
- 오늘 할 일
- [x] STEP3 PR보내기
### 🙌 07/19
- 특이사항
- idinaloq: 없습니다
- EtialMoon: 없습니다
- 오늘 할 일
- [x] STEP3 코멘트 답변달기
### 🙌 07/21
- 특이사항
- idinaloq:
- EtialMoon: 없습니다
- 오늘 할 일
- [ ] README작성
- [ ] STEP4마무리
### 활동학습 23.07.10
활동학습
iOS 환경에서 활용할 수 있는 의존성 관리도구의 종류에는 무엇무엇이 있을까?
- 코코아팟 (CocoaPods)
- 카르타고 (Carthage)
- 스위프트 패키지 매니저 (SPM: Swift Packge Manager)
각각의 의존성 관리도구의 장단점도 알아봅시다
- 코코아팟 (CocoaPods)
- 장점:
- 지원하는 라이브러리가 가장 많다.
- 단점:
- 라이브러리를 다운받아 설치(pod install or update)하는데 오랜 시간이 걸린다.
- 프로젝트를 빌드 할 때마다 모든 팟 라이브러리가 같이 빌드되므로 다른 도구를 사용할 때 보다 프로젝트 빌드 시간이 느리다.
- 카르타고 (Carthage)
- 장점:
- 라이브러리를 빌드한 후 프로젝트에 추가하기 때문에 프로젝트의 빌드와 별개이기 때문에 빌드 시간이 짧다
- 단점:
- 의존성이 추가될 때마다 해줘야 하는 번거로운 작업이 있다.
- 사용자나 인기가 많은 라이브러리가 아니라면 카르타고를 지원하지 않을 수도 있다.
- 스위프트 패키지 매니저 (SPM: Swift Packge Manager)
- 장점:
- 애플에서 지원한다.
- 단점:
- 아직은 지원하지 않는 라이브러리가 많다.
의존성 관리도구를 사용하는 이유는?
- 라이브러리들의 의존성을 직접 관리하며 발생할 수 있는 휴먼 에러를 줄일 수 있다.
- 의존성 관리를 위한 시간이 절약되고 안정성이 보장된다.
의존성 관리도구와 Git을 함께 사용할 때 주의할 점은?
- .gitignore 설정
- 의존성을 Git에 올리지 않고, 의존성의 버전만 명시하여 프로젝트를 관리하는 것이 좋다. 즉, 의존성 관리 도구에서 생성한 파일과 폴더를 Git 추적에서 제외시켜야 한다.
- 패키지(외부 라이브러리) 버전 관리
- 패키지 버전이 업데이트되면 문제가 있을 수 있기 때문에 패키지 버전을 커밋 메시지, 변경 로그와 함께 관리하고 특정 버전으로 고정하거나 업데이트를 하는것이 좋다.
이번 프로젝트에 SwiftLint 라이브러리를 적용해봅시다
Lint 규칙 정하기 등의 세부 설정은 나중에 하고, 이번 활동학습 시간 중에는 Lint 설치에만 의의를 둡니다
- cocoaPods으로 설치
- SPM으로는 힘듬(?)
### PR STEP1
은행창구 매니저 [STEP1] EtialMoon, idinaloq
안녕하세요 제임스(@inwoodev)!
은행창구 매니저 프로젝트에서 리뷰 받게 된 EtialMoon, idinaloq입니다!
2주간 잘 부탁드립니다~
### 고민한 점
- 유닛 테스트를 진행하는 과정에서 모듈에 접근 시 접근제어자 때문에 접근이 불가능 한 문제가 있었고, 해결 방법을 다음과 같이 고민해 보았습니다.
1. 접근제어자 수정: 유닛 테스트의 목적은 프로퍼티, 메서드, 클래스 등을 코드 단위로 테스트하기 위해서 진행하지만, 만약 테스트를위해 접근제어자가 변경된다면 코드가 테스트에 종속되고, 캡슐화에도 문제가 있다고 생각해서 다음 방법을 고민했습니다.
2. `extension` 사용: 접근제어자 수정을 대체할 방법으로 extension으로 테스트할 코드의 기능을 구현하는 것인데, 캡슐화에 문제가 없지만 테스트를 위해 코드를 작성한다는 부분에서 아쉬움이 있었기 때문에 선택하지 않고 다른 방법을 고민했습니다.
3. 테스트 더블 사용: 테스트를 위한 객체를 만들어 실제 코드에서 접근제어자 때문에 테스트가 불가능하던 부분을 대신하고, 실제로 동작되어야 하는 코드의 변경 없이 테스트를 할 수 있기 때문에 이 방법을 선택했습니다.
### 해결되지 않은 점, 조언을 얻고 싶은 점
- `BankManagerConsoleApp` 파일에서 Unit Test를 진행하려고 하면, `BankManagerConsoleApp` 모듈이 `testable import` 되지 않거나, 그 이외에 Linker와 관련된 오류가 발생했습니다.
- 이 방법을 해결하지 못해서 대안으로 `Node`에서 `Target Membership`의 `BankManagerConsoleAppTests`항목에 체크를 하여 테스트를 진행했습니다.
- (`LinkedList`를 테스트하기 위해 테스트 더블 객체를 사용하기 때문에 `LinkedList`에서 필요한 `Node`만 `Target Membership`을 체크했습니다.)
- 처음에는 저희만의 문제인 줄 알았으나, 다른 팀들도 비슷한 문제가 생긴다고 들었습니다. 다른 프로젝트에서는 정상적으로 유닛 테스트가 생성, 진행되는데 뱅크매니저 프로젝트만 진행이 안되는 것 같습니다...
- **No such module 'BankManagerConsoleApp'
xcode undefined symbol
linker failed with exit code 1**
- 위와 같은 오류가 왜 발생하는지 찾아 본 결과에서 답변으로
https://developer.apple.com/forums/thread/52211
**the machinery to load your test bundle within a process only works if the process is based on a GUI framework (like Cocoa or Cocoa Touch)**
Cocoa 또는 CoCoa Touch같은 GUI프레임워크에서만 동작한다고 하는 것 같습니다. 그렇다면 저희가 테스트 방법은 맞게 테스트를 진행한 것인지에 대해 궁금합니다.
***Xcode의 타겟 설정에서 General - Testing - Hosting Application 항목을 확인해보시면 UI앱과 Command Line Tools의 차이가 보이실 거에요. Unit Test를 추가 할때 마찬가지로 옵션에 Target to be Tested항목을 지정하는게 있죠. 결론은 CLT라서 안된다...입니다 ㅎㅎㅎ
https://developer.apple.com/forums/thread/52211 를 참조하세요!***
---
### 리뷰
LinkedList는 struct 타입인데 Node는 class타입으로 선언하신 특별한 이유가 있으실까요?
또한 final 키워드에 대해서 잘 알고 계신지 궁금해요~ 여기서 사용하신 이유도 궁금합니다~😀
***답변***
- `Node`는 다음 노드에 대한 참조가 필요하기 때문에 값 타입인 `struct`보다는 참조 타입인 `class`가 적절하다고 생각했습니다.
`final` 키워드는 클래스에서 상속을 막아주는 역할을 합니다. 저희는 `Node`를 상속해야할 이유가 없기 때문에 `final`키워드을 사용했습니다.
Stub란 무엇일까요?
LinkedList 대신에 해당 타입을 선언하신 뒤 테스트하신 이유도 궁금합니다. 이 stub의 동작 검증이 LinkedList의 동작 검증을 보장한다고 볼 수 있을까요~?
***답변***
- Stub는 실제로 동작하는 것처럼 만들어 실제 코드를 대신 동작해주는 객체로 알고 있습니다. 테스트가 곤란한 부분의 객체를 도려내어 그 역할을 최소한으로 대신해 줄 만큼만 간단하게 구현하여 메서드를 호출하고 그 결과값과 예상값을 비교하는 상태 기반 테스트(State Base Test)를 합니다.
- `stub`이 `LinkedList`와 완전히 똑같지 않기 때문에 검증을 보장할 수 없다고 생각했습니다. 그래서 `stub` 대신에 직접 `LinkedList`객체를 테스트하는 것으로 수정했습니다.
위 코멘트에서 이어지는 내용입니다. LinkedList의 head와 tail은 private 하기에 외부에서 접근할 수 있는데요 여기서는 internal 한데....이렇게 차이점이 존재하는데 이 stub을 테스트하는게 LinkedList를 테스트한다고 볼 수 있을지는 한 번 생각해볼만한 것 같아요!
-
LinkedList를 구현하셨군요!! 👏👏👏👏
LinkedList에 대해서 공부하신 부분이 있다면 공유 받을 수 있을까요?
그리고 해당 타입은 DoubleLinkedList인 것 같은데 네이밍에서 오는 혼란이 있을 수 있지는 않을지는 생각 해 볼 만한 포인트인듯 합니다!
***답변***
참고한 링크
https://supermemi.tistory.com/entry/Linked-list-연결-리스트-란-무엇인가
https://jud00.tistory.com/entry/자료구조-연결-리스트Linked-List
https://www.youtube.com/watch?v=R9PTBwOzceo
- 저희가 구현한 코드를 보면, `Node`에서 해당 노드의 데이터 값과 다음 노드 참조하고 있습니다. `LinkedList`에서 보면 `head`는 맨 처음 노드이고, 그 다음 `enqueue`를 하게되면, 먼저`tail.next`에 새로 만들어진 노드를 참조하고, 그 다음 현재 `tail`을 새로 만들어진 노드로 설정하게 됩니다.
결국 `head`는 맨 첫 번째 노드만 가리키고, `tail`은 계속해서 추가되는`node`만을 참조하고 있기 때문에 이전 노드를 알 수 없는 상태라고 생각합니다. `DoubleLinkedList`가 맞는걸까요?
sut란 무엇일까요~?
***답변***
- `sut`은 `system under test`의 줄임말로 테스트 중인 객체를 뜻합니다.
여러가지 검증을 시도하려는 노력은 매우 좋습니다! 👏👏👏👏
다만 해당 유닛테스트가 유효한지는 다른 코멘트들을 한 번 읽어 보시고 한 번 더 생각 해 보시면 좋을 것 같아요!
또한 성공하는 케이스들을 나열 해 주셨는데요, 시간이 되신다면 실패하는 테스트 케이스 추가하시는 것도 검증하시는 데에 도움이 될 거라고 생각해요 :)
***답변***
요 키워드는 무엇을 뜻하는 걸까요~?
***답변***
- 테스트 중에 `dequeue`한 값을 사용하지 않고 `LinkedList`의 `head`와 `tail`을 확인하는 테스트가 있습니다. `Result of call to 'dequeue()' is unused` 경고를 표시하지 않기 위해 `discardableResult`를 사용했습니다.
수정사항 확인 했습니다~
single과 double얘기가 나온 김에 그 차이점을 여러분께서 설명 해 주시면 다음 스텝으로 넘어가도 충분하다고 생각됩니다~ 머지전까지 추가적으로 수정하고 싶으신 부분 있으면 수정 해 주시고 준비되셨다 하면 알려주시면 머지할게요~!! 이번 스텝 고생 많으셨습니다!!🙌🙌🙌
***답변***
**single linked list**

</br>
**double linked list**

|차이점|single linked list|double linked list|
|:--:|:--:|:--:|
|이전 노드의 주소|알 수 없음|알 수 있음|
|데이터 탐색|처음부터 탐색 진행|처음 또는 마지막 둘 다 가능|
|안정성|좋지 않음(한 곳에서 끊기면 복구가 힘듬)|좋음|
|크기|double보다 작음(데이터,참조1개)|큼(데이터,참조2개)|
---
### PR STEP2
은행창구 매니저 [STEP2] EtialMoon, idinaloq
안녕하세요 제임스(@inwoodev)!
EtialMoon, idinaloq입니다!
스탭2 리뷰도 잘 부탁드립니다~
### 고민한 점
- `Bank`타입에서 고객들의 수 만큼 인스턴스를 생성해서 `enqueue`하는 `enqueueCustomers()`메서드를 구현했습니다. 하지만 3번 이상 `enqueue`를 하게 되면 `enqueue`가 되지 않는 현상이 있었습니다.
- `SingleLinkedList`에서 `tail`을 지우는 것으로 수정하며 문제가 있었습니다.
**이전코드**
```swift
mutating func enqueue(_ data: Element) {
let node: Node<Element> = Node(data: data)
guard !isEmpty else {
firstNode = node
return
}
var nextNode: Node<Element>? = firstNode
while nextNode?.next != nil {
nextNode = firstNode?.next
}
nextNode?.next = node
}
```
- 처음 코드를 보면, 만약 새로운 노드가 추가 되었을 때 항상 첫 번째와 같은 값을 가지는 노드를 추가하고 있습니다. 이 때문에 잘못된 값이 계속 들어갔기 때문에 다음과 같이 수정했고, 두 차이점을 표현하면 다음과 같습니다.
**수정된 코드**
```swift
mutating func enqueue(_ data: Element) {
...
while nextNode?.next != nil {
nextNode = nextNode?.next
}
...
}
```
**차이점**
|sequence|nextNode = firstNode?.next|nextNode = nextNode?.next|
|:--:|:--:|:--:|
|초기값|nil|nil|
|enqueue(1)|1 -> nil|1 -> nil|
|enqueue(2)|1 -> 2 -> nil|1 -> 2 -> nil|
|enqueue(3)|1 -> 2 -> 3 -> nil |1 -> 2 -> 3 -> nil|
|enqueue(4)|1 -> 2 -> 3 -> 3 -> ...|1 -> 2 -> 3 -> 4 -> nil|
### 조언을 얻고 싶은 점
- 현재 STEP2의 요구조건 중에 **각 고객의 업무를 처리하는 데 걸리는 시간은 0.7초입니다.** 라는 조건이 있습니다. 해당 부분은`BankManager`타입에서 `work()`메서드에 `Thread.sleep(forTimeInterval: 0.7)
`로 구현을 하였습니다. 이럴 경우 실제로 `Thread.sleep()`메서드가 동작하면서 0.7초의 딜레이가 걸리고, 추가적으로 `print`메서드도 처리하는데 시간이 걸릴 것 같다고 생각하고 있습니다.
- 고객 수에 0.7을 곱하는게 아니라 실제로 작업에 걸린 총 시간을 측정하는 방법이 더 나았을까요?
---
### 리뷰
오 tearDownWithError는 필요가 없을까요?
***답변***
- setUpWithError: 각각의 테스트 메서드가 실행되기 전에 실행되는 메서드로 초기화 작업을 수행하는 메서드입니다.
- tearDownWithError: 테스트 메서드가 종료되고 나서 실행되는 메서드로 정리 작업(테스트에서 생성한 리소스 해제)등의 작업을 수행하는 메서드입니다.
`setUpWithError`에서 항상 `sut`을 `SingleLinkedList`로 초기화하고 있기 때문에 `tearDownWithError`가 없어도 테스트가 정상적으로 실행된다고 생각합니다.
이 질문을 드린다는걸 깜빡했네요~ 제네릭에 대해서 어디까지 공부하셨는지 궁금합니다~ 두 분의 지식을 나눠주시면 감사하겠습니다! :)
***답변***
- 제네릭은 다양한 데이터 타입에 대해 재사용하고 확장할 수 있는 기능을 제공합니다.
- 제네릭 코드는 정의한 요구사항에 따라 모든 타입에서 동작할 수 있는 유연하고 재사용 가능한 함수와 타입을 작성할 수 있습니다.
- 제네릭을 사용하여 중복을 피하고 명확하고 추상적인 방식으로 의도를 표현하는 코드를 작성할 수 있습니다.
예를들어, 다음과 같은 메서드가 존재한다고 할 때
```swift
func swap(_ a: inout String, _ b: input String) {
let temporaryA = a
a = b
b = temporaryA
}
```
`swap` 메서드가 `String`이 아닌 다른 타입인 경우 다시 메서드를 생성해야 된다는 단점이 있습니다. 이를 해결하기 위해 제네릭을 사용하며, 선언은 다음과 같이 합니다.
```swift
func swap<T>(_ a: inout T, _ b: inout T) {
...
}
```
- `T`는 제네릭 타입 파라미터로, 제네릭을 메서드에 사용하는 경우를 **제네릭 함수**라고 합니다.
- 제네릭은 구조체, 클래스, 열거형 타입에도 선언할 수 있고, 이것을 **제네릭 타입** 이라고 합니다.
제네릭 타입은 다음과 같이 사용합니다.
```swift
struct MyStruct<T> { ... }
```
[🍎Apple Docs: Generics](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/#Naming-Type-Parameters)
- Any vs Generics
- [`Any`](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/types/#Any-Type)
- 모든 유형의 인스턴스를 나타낼 수 있는 타입입니다.
- 사용하려면 실제 타입으로 다시 캐스팅 해야 합니다.
- 구체적인 타입을 사용하는 것이 항상 더 좋습니다.
- 업/다운 캐스팅은 런타임에서 발생합니다.
- `Generics`
- 제네릭을 사용할 때는 캐스팅이 필요하지 않습니다.
- 컴파일 타임에 타입이 지정됩니다.
오 이 부분은 분명히 취향 차이일거에요~ 각 메서드들의 추상화를 현실단계로 맞춰 놓으셨잖아요! 이런 맥락이라면 저는 enqueue 동사 대신에 accept, receive와 같은 동사를 사용할것 같아요 :)
***답변***
### PR STEP 3
은행창구 매니저 [STEP3] EtialMoon, idinaloq
### 고민한 점
- 메서드의 매개변수에 메서드의 역할과 관련없는 전달인자가 있었습니다.
처음 작성한 코드는 다음과 같습니다.
```swift
struct BankManager {
func work(for customer: Customer, group: DispatchGroup, semaphore: DispatchSemaphore) { ... }
}
struct Bank {
private mutating func processBusiness() {
var depositBankManagerNumber = 0
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 3)
let startDate = Date()
}
```
`GCD`를 활용하기 위해 `processBusiness`메서드 내부에서 프로퍼티를 만들고 이를 `work`메서드에 전달인자로 전달했는데, 이렇게 되면 결국 `BankManager`가 `work`라는 동작을 할 때 필요없는 정보들을 넘겨주는 것 같다고 생각했습니다. 이 부분에 대해서는 다음과 같이 수정을 했고, 결과적으로 `Dispatch Queue`를 사용하기 위한 프로퍼티들이 `processBusiness`메서드를 실행할 때 마다 계속해서 생성되지 않게 되었고, `BankManager`가 `work(for: customer)`라는 동작을 할 때 어떤 기능인지에 대해 명확하게 나타낼 수 있게 되었다고 생각합니다.
```swift
struct BankManager {
private let group: DispatchGroup = DispatchGroup()
private let semaphore: DispatchSemaphore
init(semaphore: Int) {
self.semaphore = DispatchSemaphore(value: semaphore)
}
func work(for customer: Customer) { ... }
}
```
<br>
#### 은행원을 나누는 기준
은행원의 수가 3명이고 2명은 예금, 1명은 대출 업무를 처리해야 합니다. 은행원 1명당 하나의 `BankManager` 객체를 두는 것을 생각했지만 업무를 나누는 기준이 불명확했습니다.
```swift
struct Bank {
private let bankManagers: [BankManager]
...
private mutating func processBusiness() {
...
while let customer = customerQueue.dequeue() {
...
switch customer.getBankingType() {
case .deposit:
bankManagers[depositBankManagerNumber].work(for: customer, group: group, semaphore: semaphore)
depositBankManagerNumber += 1
case .loans:
bankManagers[2].work(for: customer, group: group, semaphore: semaphore)
case .none:
print("asdf")
}
}
...
}
...
}
```
업무별로 `BankManager` 객체를 나누고 각각의 `DispatchSemaphore`를 조절해 스레드 하나를 은행원 한 명으로 볼 수 있도록 했습니다.
```swift
struct Bank {
private let depositBankManagers: BankManager = BankManager(people: 2)
private let loansBankManagers: BankManager = BankManager(people: 1)
...
private mutating func processBusiness() {
measureStartTime()
while let customer = customerQueue.dequeue() {
switch customer.getBankingServiceType() {
case .deposit:
depositBankManagers.work(for: customer)
case .loans:
loansBankManagers.work(for: customer)
case .none:
print("BankingTypeIsNil")
}
}
depositBankManagers.finishWork()
loansBankManagers.finishWork()
measureTotalTime()
}
...
}
```
===================================================
startTime이 프로퍼티로 선언 되며 옵셔널인 특별한 이유가 있을까요?
***답변***
처음에 `processBusiness`메서드에서 `startTime`과 `totalTime`을 생성, 사용을 했습니다. 이렇게 되는 경우 `processBusiness`메서드에서 추상화 레벨이 맞지 않는다고 생각했기 때문에 이 둘을 각각`measureTotalTime`, `measureStartTime`메서드로 기능을 분리했고 그에 따라 프로퍼티도 생성하도록 했습니다.
옵셔널인 이유는 `startTime`이 `Date`타입을 사용하기 때문에 인스턴스 생성 과정에서 초기값을 지정하거나 `init`메서드를 사용하는 것 보다는 옵셔널을 이용해서 쓰는 것이 간단하다고 생각해서 옵셔널을 사용했습니다.
totalTime이 프로퍼티로 선언된 특별한 이유가 있을까요?
***답변***
업무 시간의 측정은 `processBusiness`메서드 안에서 이루어져야 한다고 생각했습니다. 메서드가 시작할 때 `measureStartTime`을 호출하고 메서드가 끝날 때 `measureTotalTime`으로 시간을 측정한 후 `finishBusiness` 메서드에서 `totalTime`을 출력하기 때문에 프로퍼티로 선언해서 사용했습니다.
클로저를 훌륭하게 사용하셨군요! 그런데 다만 Bank라는 객체가 numberFormatter를 내부에 가지고 있는게 적절한지는 한 번 생각 해 보면 좋을 것 같습니다!
***답변***
말씀하신대로 `numberFormatter`가 `Bank`에 존재하는것은 맞지 않다고 생각합니다. 따라서 `TotalTimeFormatter` 타입을 만들고 `Bank.finishBusiness`메서드에서
```swift
guard let totalProcessTime = numberFormatter.string(for: totalTime) else { return }
```
이 부분의 기능도 `TotalTimeFormatter`에 `string`메서드로 구현했습니다.
dispatchGroup에 대해서 알려주세요~~ 어떤 의도로 사용하셨는지 두 분의 생각이 궁금합니다~
***답변***
`dispatchGroup`은 여러 작업들을 그룹에 연결하고 같은 큐 또는 다른 큐에서 동기/비동기 식으로 실행하도록 만듭니다.
은행 업무의 모든 작업이 끝나기 전까지 프로그램이 종료되지 않도록 `wait`을 사용하기 위해 `dispatchGroup`을 사용했습니다.
DispatchGroup만으로 문제를 해결할 수 없었을까요? 세마포어를 사용하시게 된 이유가 궁금합니다~ 두 분의 생각의 흐름을 알려주세요!
***답변***
`BankManager`의 `work`메서드 안에서 그룹을 만들고 `wait`까지 하면 모든 작업이 순차적으로 진행됩니다.
`work`메서드 밖에서 그룹을 만들고 진행하면 모든 업무가 끝날 때까지 프로그램이 종료되지는 않지만 모든 업무가 한 번에 실행되는 문제가 있습니다.
세마포어를 사용하여 접근 가능한 스레드의 수를 제한하여 예금 업무에는 2개의 스레드, 대출 업무에는 1개의 스레드씩만 접근할 수 있도록 했습니다.
group의 wait을 사용하셨군요~ 혹시 해당 메서드의 위험성도 인지하고 계실까요~?
***답변***
[wait](https://developer.apple.com/documentation/dispatch/dispatchgroup/2016090-wait)메서드는 이전에 제출된 작업이 완료될 때 까지 동기적으로 기다리는 메서드 입니다. 따라서 `group`의 `wait`메서드는 해당 그룹에 포함된 비동기 작업들이 완료될 때 까지 기다리는 역할을 합니다.
`wait`메서드는 **데드락**의 위험성이 있습니다. 데드락이란 프로세스가 자원을 얻지 못해 다음 처리를 하지 못하는 상황을 뜻합니다.
만약 `depositBankManager`와 `loansBankManager`가 각각 스레드를 처리할 때 자원을 점유하고 있다고 가정할 때 만약 이 상태에서 서로의 자원에 접근하려고 하면 서로의 동작이 끝날 때 까지 멈춰있는 (두 개의 스레드가 하나의 자원을 놓고 경쟁하려는)데드락 현상이 발생할 수 있습니다.
해당 메서드가 호출되고 있는 쓰레드가 어딘지 혹시 알고 계실까요~? 특별히 문제될 상황은 없을까요?
***답변***
해당 메서드는 메인 스레드에서 호출되고 있습니다. 현재는 문제가 없지만 위의 답변과 같이 데드락의 가능성이 존재합니다.
혹시 CustomStringConvertible에 대해서 알고 계실까요~?
***답변***
[CustomStringConvertible](https://developer.apple.com/documentation/swift/customstringconvertible)은 사용자 정의에 따른 텍스트를 출력해주는 기능을 가지고 있습니다.
기존에 `CustomStringConvertible`을 상속해서 `description`을 사용하려고 했는데, 모르고 상속을 안해서 연산프로퍼티로 적용이 된 것 같습니다.
해당 부분 수정을 해서 `.description`을 안붙히고 사용할 수 있도록 수정했습니다
결국 measureStartTime 메서드에서 시작시간이 결정되는 것 같은데요. 그럼 결국 해당 메서드를 통해서 startTime이 지정되는 상황으로 보여요~ 그렇다면 해당 메서드가 시작시간을 반환하는 방향으로 수정하몀 추후 발생할 수 있는 사이드 이펙트를 방지할 수 있겠다 라는 생각이 들었어요~ 제 의견에 대한 두 분의 생각이 궁금합니다!
***답변***
추상화 레벨을 맞추는 것보다 사이드 이펙트를 방지하는 것이 중요한 것같아 `startTime` 프로퍼티를 없애고 `processBusiness`에서 시간 측정을 시작하도록 수정했습니다.
이건 제 개인적인 생각이에요~ 내부 프로퍼티로 선언하면 결국 해당 상태를 변경해야하는 근거가 충분해야 한더고 생각해요. 현 상황에서는 measureTotalTime이 startTime을 주입 받고 총시간을 반환하는식으로 구현하여 발생할 수 있는 사이드이펙트를 최소화할 수 있지 않을 까 하는 생각이 들었어요~ (이렇게 수정하라는 의미는 전혀 아닙니다. 고려 해 볼만한 부분이는 뜻 입니다 😃
***답변***
`startTime`을 수정한 것과 같이 `totalTime`도 수정하였습니다. 추상화 레벨을 어떻게하면 맞출 수 있을지 한 번 고민해보면 좋을 것 같다고 생각합니다.
훌륭합니다. 추가적으로 드는 생각은 extension을 활용해도 괜찮을 것 같아요!
***답변***
(STEP4때 바꾸기)
메인쓰레드인걸 인지하고 계시는 군요! 그럼 앱을 개발하는 과정에서 메인쓰레드를 블록시키면 발생할 수 있는 문제는 없을까요?~
***답변***
메인스레드를 블록시키면 화면이 멈춘 것 처럼 보이는 상황이 발생할 수 있습니다.
========README========
# 🏦 은행창구 매니저
## 📖 목차
1. [소개](#-소개)
2. [팀원](#-팀원)
3. [타임라인](#-타임라인)
4. [시각화된 프로젝트 구조](#-시각화된-프로젝트-구조)
5. [실행 화면](#-실행-화면)
6. [트러블 슈팅](#-트러블-슈팅)
7. [참고 링크](#-참고-링크)
8. [팀 회고](#-팀-회고)
</br>
## 🍀 소개
임의의 수의 고객이 방문하고, 은행창구 매니저가 고객들의 예금, 대출업무를 처리합니다.
* 주요 개념: `LinkedList`, `GCD`, `Queue`, `DispatchQueue`, `DispatchGroup`, `Thread`
</br>
## 👨💻 팀원
| idinaloq | EtialMoon |
| :--------: | :--------: |
| <Img src = "https://user-images.githubusercontent.com/109963294/235301015-b81055d2-8618-433c-b680-58b6a38047d9.png" width = "200" height="200"/> |<img src="https://i.imgur.com/hSdYobS.jpg" width="200"> |
|[Github Profile](https://github.com/idinaloq) |[Github Profile](https://github.com/hojun-jo) |
</br>
## ⏰ 타임라인
|날짜|내용|
|:--:|--|
|2023.07.10.| Node 타입 추가<br>SingleLinkedList 타입 추가<br> |
|2023.07.12.| SingleLinkedList 메서드 추가 |
|2023.07.13.| Bank 타입 추가<br>Customer 타입 추가<br>코드 리팩토링<br>BankManager 구현 |
|2023.07.15|enqueueCustomers메서드 네이밍 수정|
|2023.07.17|대출 업무 기능 구현|
|2023.07.17|파일분할<br>work메서드 매개변수 수정<br>processBusiness추상화레벨 수정<br>전반적인 네이밍 수정|
|2023.07.19|numberFormatter타입 파일분할<br>BankingService타입에 CustomStringConvertible 채택|
|2023.07.20|measureStartTime, measureTotalTime메서드가 각각 Date, TimeInterval을 반환하도록 수정|
</br>
## 👀 시각화된 프로젝트 구조
### ℹ️ File Tree
```
BankManagerConsoleApp
├── BankManagerConsoleApp
│ ├── main
│ ├── Node
│ ├── SingleLinkedList
│ ├── Bank
│ ├── BankManager
│ ├── Customer
│ ├── BankingService
│ └── TotalTimeFormatter
└── BankManagerConsoleAppTests
└── BankManagerConsoleAppTests
```
### 📐 Class Diagram
<p align="center">
<img width="1000" src="https://github.com/idinaloq/testRep/assets/124647187/3c8184ea-a1bd-475f-b833-9668d6a25b94">
</p>
</br>
## 💻 실행 화면
<img width="500"
src="https://github.com/idinaloq/testRep/assets/124647187/17197f90-e5a0-4893-b98f-77dc2740c5e3">
</br>
## 🧨 트러블 슈팅
1️⃣ **Test Double** <br>
🔒 **문제점** <br>
- 유닛 테스트를 진행하는 과정에서 모듈에 접근 시 접근제어자 때문에 접근이 불가능 한 문제가 있었고, 해결 방법을 다음과 같이 고민해 보았습니다.
1. 접근제어자 수정: 유닛 테스트의 목적은 프로퍼티, 메서드, 클래스 등을 코드 단위로 테스트하기 위해서 진행하지만, 만약 테스트를위해 접근제어자가 변경된다면 코드가 테스트에 종속되고, 캡슐화에도 문제가 있다고 생각해서 다음 방법을 고민했습니다.
2. `extension` 사용: 접근제어자 수정을 대체할 방법으로 extension으로 테스트할 코드의 기능을 구현하는 것인데, 캡슐화에 문제가 없지만 테스트를 위해 코드를 작성한다는 부분에서 아쉬움이 있었기 때문에 선택하지 않고 다른 방법을 고민했습니다.
3. 테스트 더블 사용: 테스트를 위한 객체를 만들어 실제 코드에서 접근제어자 때문에 테스트가 불가능하던 부분을 대신하고, 실제로 동작되어야 하는 코드의 변경 없이 테스트를 할 수 있기 때문에 이 방법을 선택했습니다.
- 하지만 `stub` 객체와 `real` 객체 간의 동일성을 보장할 수 없다는 문제가 있었습니다.
```swift
struct LinkedListStub<Element> {
var head: Node<Element>?
var tail: Node<Element>?
...
}
```
```swift
struct LinkedList<Element> {
private var head: Node<Element>?
private var tail: Node<Element>?
...
}
```
🔑 **해결방법**
- `real` 객체에서 `private`인 `firstNode` 프로퍼티를 사용하는 메서드를 통해 테스트를 진행할 수 있도록 했습니다.
```swift
struct SingleLinkedList<Element> {
...
mutating func currentFirstNode() -> Node<Element>? {
return firstNode
...
}
```
```swift
final class BankManagerConsoleAppTests: XCTestCase {
...
func test_리스트에Node가있는경우_enqueue하면_firstNode는유지된다() {
let firstInput: Int = 123
let secondInput: Int = 456
sut.enqueue(firstInput)
let previousFirstNode = sut.currentFirstNode()
sut.enqueue(secondInput)
let currentFirstNode = sut.currentFirstNode()
XCTAssert(previousFirstNode === currentFirstNode)
}
...
}
```
2️⃣ **Linked List** <br>
🔒 **문제점** <br>
- `Bank`타입에서 고객들의 수 만큼 인스턴스를 생성해서 `enqueue`하는 `enqueueCustomers()`메서드를 구현했습니다. 하지만 3번 이상 `enqueue`를 하게 되면 `enqueue`가 되지 않는 현상이 있었습니다.
- `SingleLinkedList`에서 `tail`을 지우는 것으로 수정하며 문제가 있었습니다.
**이전코드**
```swift
mutating func enqueue(_ data: Element) {
let node: Node<Element> = Node(data: data)
guard !isEmpty else {
firstNode = node
return
}
var nextNode: Node<Element>? = firstNode
while nextNode?.next != nil {
nextNode = firstNode?.next
}
nextNode?.next = node
}
```
🔑 **해결방법**
- 처음 코드를 보면, 만약 새로운 노드가 추가 되었을 때 항상 첫 번째와 같은 값을 가지는 노드를 추가하고 있습니다. 이 때문에 잘못된 값이 계속 들어갔기 때문에 다음과 같이 수정했고, 두 차이점을 표현하면 다음과 같습니다.
**수정된 코드**
```swift
mutating func enqueue(_ data: Element) {
...
while nextNode?.next != nil {
nextNode = nextNode?.next
}
...
}
```
**차이점**
|sequence|nextNode = firstNode?.next|nextNode = nextNode?.next|
|:--:|:--:|:--:|
|초기값|nil|nil|
|enqueue(1)|1 -> nil|1 -> nil|
|enqueue(2)|1 -> 2 -> nil|1 -> 2 -> nil|
|enqueue(3)|1 -> 2 -> 3 -> nil |1 -> 2 -> 3 -> nil|
|enqueue(4)|1 -> 2 -> 3 -> 3 -> ...|1 -> 2 -> 3 -> 4 -> nil|
3️⃣ **메서드의 역할과 매개변수** <br>
🔒 **문제점** <br>
- `GCD`를 활용하기 위해 `processBusiness`메서드 내부에서 프로퍼티를 만들고 이를 `work`메서드에 전달인자로 전달했는데, 이렇게 되면 결국 `BankManager`가 `work`라는 동작을 할 때 `group`과 `semaphore`와 같은 필요없는 정보들을 넘겨주는 문제가 있었습니다.
**이전코드**
```swift
struct BankManager {
func work(for customer: Customer, group: DispatchGroup, semaphore: DispatchSemaphore) { ... }
}
struct Bank {
private mutating func processBusiness() {
var depositBankManagerNumber = 0
let group = DispatchGroup()
let semaphore = DispatchSemaphore(value: 3)
let startDate = Date()
}
```
🔑 **해결방법**
- 다음과 같이 `Bank`타입이 아닌 `BankManager`타입에서 `group`과 `semaphore` 프로퍼티를 만들고 이를 `work`메서드에서 사용하도록 수정을 했습니다.
- 결과적으로 `Dispatch Queue`를 사용하기 위한 프로퍼티들이 `processBusiness`메서드를 실행할 때 마다 계속해서 생성되지 않게 되었기 때문에 `BankManager`가 `work(for: customer)`라는 동작을 할 때 어떤 기능인지에 대해 명확하게 나타낼 수 있게 되었다고 생각합니다.
**수정된 코드**
```swift
struct BankManager {
private let group: DispatchGroup = DispatchGroup()
private let semaphore: DispatchSemaphore
init(semaphore: Int) {
self.semaphore = DispatchSemaphore(value: semaphore)
}
func work(for customer: Customer) { ... }
}
```
4️⃣ **은행원을 나누는 기준** <br>
🔒 **문제점** <br>
- 은행원의 수가 3명이고 2명은 예금, 1명은 대출 업무를 처리해야 합니다. 은행원 1명당 하나의 `BankManager` 객체를 두는 것을 생각했지만 `BankManager`가 업무를 나누는 기준이 불명확했습니다.
**이전코드**
```swift
struct Bank {
private let bankManagers: [BankManager]
...
private mutating func processBusiness() {
...
while let customer = customerQueue.dequeue() {
...
switch customer.getBankingType() {
case .deposit:
bankManagers[depositBankManagerNumber].work(for: customer, group: group, semaphore: semaphore)
depositBankManagerNumber += 1
case .loans:
bankManagers[2].work(for: customer, group: group, semaphore: semaphore)
case .none:
print("BankingServiceTypeIsNil")
}
}
...
}
...
}
```
🔑 **해결방법**
- 업무별로 `BankManager` 객체를 나누고 각각의 `DispatchSemaphore`를 조절해 스레드 하나를 은행원 한 명으로 볼 수 있도록 했습니다.
**수정된 코드**
```swift
struct Bank {
private let depositBankManagers: BankManager = BankManager(people: 2)
private let loansBankManagers: BankManager = BankManager(people: 1)
...
private mutating func processBusiness() {
let startTime: Date = measureStartTime()
while let customer = customerQueue.dequeue() {
switch customer.getBankingServiceType() {
case .deposit:
depositBankManagers.work(for: customer)
case .loans:
loansBankManagers.work(for: customer)
case .none:
print("BankingTypeIsNil")
}
}
depositBankManagers.finishWork()
loansBankManagers.finishWork()
totalTime = measureTotalTime(startTime)
}
...
}
```
</br>
## 📚 참고 링크
<!-- - [🍎Apple Docs: ]()
- [📘stackOverflow: ]() -->
- [🍎Apple Docs: Generics](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/#Naming-Type-Parameters)
- [🍎Apple Docs: CustomStringConvertible](https://developer.apple.com/documentation/swift/customstringconvertible)
- [🍎Apple Docs: DispatchQueue](https://developer.apple.com/documentation/dispatch/dispatchqueue)
- [🍎Apple Docs: DispatchGroup](https://developer.apple.com/documentation/dispatch/dispatchgroup)
- [🍎Apple Docs: DispatchSemaphore](https://developer.apple.com/documentation/dispatch/dispatchsemaphore)
- [📘blog: Linked List](https://supermemi.tistory.com/entry/Linked-list-연결-리스트-란-무엇인가)
- [📘blog: Main Thread와 Background Thread의 이해](https://velog.io/@yongchul/iOSThread의-기본개념)
- [🖥️video: Single Linked List](https://www.youtube.com/watch?v=R9PTBwOzceo)
</br>
## 👥 팀 회고
### 우리팀 잘한 점
- 프로젝트 요구조건에 맞도록 구현을 했고, 메서드와 타입의 추상화를 잘 했다고 생각합니다.
- 깔끔한 코드를 만들기 위해 기능 분리와 추상화 레벨을 맞추는 등 노력했다고 생각합니다.
### 우리팀 개선할 점
- 이번 프로젝트와 우리 팀만의 문제가 아니라고 생각하는데, 짝 프로그래밍에서 드라이버와 네비게이터의 역할이 모호해져 가는 것 같다고 생각합니다.