## 팀원 : Erick🦦, Serena🐷
@Vivi
안녕하세요.
은행창구 매니저 프로젝트 Step1 PR 보냅니다.
2주동안 리뷰 잘 부탁드립니다~ 븨븨!😆
-----
## 구현한 내용
은행에 도착한 고객이 임시로 대기할 대기열을 구현했습니다.
> 주요 내용 : WaitingLineQueue / LinkedList / Node, UnitTest
-----
## 고민한 부분
### 🔥 UNIT Test
- 테스트 작성 시 각 테스트 항목의 기준을 잡는 것에 있어서 고민을 했습니다. 예를들어 `dequeue`를 테스트하고자 할 때, 결과를 `isEmpty`로 확인해야한다는 점에서 한 테스트 안에 2개의 함수를 테스트를 하는 것이 아닌가하는 고민이 있었습니다.
- 각 함수 별로 테스트를 진행하기 위해서 빈 `queue`를 `dequeue`시켜 `nil`값을 반환하는 것을 체크하는 테스트와 빈 `queue`에 `isEmpty`를 호출하여 `true`값을 반환하는 테스트로 나누어 테스트를 진행하였습니다.
### 🔥 private(set)
- `LinkedList`에서 `head`는 `Queue`의 `peak` 기능이나 `isEmpty` 기능에서 쓰일 수 있다고 생각했기 때문에 `LinkedList`에 `first`나 `isEmpty`와 같은 프로퍼티를 따로 만들지 않고 `private(set)`을 사용하여 `head`로 접근만 가능하도록 하여 기능을 구현할 수 있도록 했습니다.
-----
## 조언을 얻고 싶은 부분
### ⭐️ Generic Type Naming
- `Queue`와 `LinkedList`를 생성할 때 제네릭 타입의 이름을 어떻게 지을지 고민했습니다.
- 아래 내용과 같이 `Queue`와 `Linked List`는 자료구조이기 때문에 개별 데이터를 `Element`라고 불러도 어색하지 않다고 생각했지만 `Element`라고 네이밍하는 것이 맞는지 확신이 없었기 때문에 `T`라고 네이밍하였습니다.
- 븨븨라면 자료구조의 제네릭 타입 네이밍을 할 때 어떤 이름을 사용했을지 조언을 얻고 싶습니다.
> T는 제네릭 타입을 선언할 때 일반적으로 사용되는 관례적인 이름이고, Element는 컬렉션 또는 자료구조에서 개별 요소를 나타내는 데에 사용되는 관례적인 이름입니다.
### ⭐️ Queue와 LinkedList 파일 구분과 네이밍
- `queue`와 `linkedList`는 별도의 자료구조라 생각했습니다. 그래서 `linkedList`를 별도의 class로 구분하여 `queue`와 `node`사이에서 구분되도록 분리하였습니다.
- 이때 `linkeList`와 `node`는 다른 `queue`에도 확장이 가능하지기 때문에, 네이밍부분에서 고민이 생겼습니다. 현재는 하나의 `queue`만 존재하기 때문에 `queue`의 이름을 따 `WaitingLineLinkedList`, `WaitingLineNode`라고 네이밍을 했습니다. 하지만 확장성을 고려하여 `LinkedList`, `Node`로 네이밍을 하는 것이 적절한지 븨븨의 조언을 듣고 싶습니다.
-----
## 참고자료
- [🍎 Apple Developer: Generic](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/generics/)
- [📒 Blog: Swift Command Line Tool 프로젝트에서 Unit Test 하기](https://jwonylee.tistory.com/entry/XCode-Swift-Command-Line-Tool-프로젝트에서-유닛-테스트-하기)
--------
@YebinKim
안녕하세요 븨븨!😆
Erick & Serena 조입니다⭐️
은행창구 매니저 프로젝트 Step2 PR 보냅니다.
리뷰 잘 부탁드립니다~!
-----
## 구현한 내용
은행 개점을 하여 고객 업무 처리를 은행원이 진행합니다. 이 모든 것을 은행매니저가 총괄합니다.
> 주요 내용 : Command Line, sleep(), Date()
-----
## 고민한 부분
### 🔥 객체 지향 관점에서 파일 및 역할 분리
- 실제 은행을 생각하며 은행 `Bank`, 은행원 `BankClerk`, 고객 `Customer`, 그리고 이를 모두 총괄하는 은행매니저`BankManager`로 나누어 구현하였습니다.
- `Customer`은 자신의 대기 번호와 업무 시간을 가지고 있고, `BankClerk`은 이를 이용하여 은행 업무 시작, 완료를 하도록 했습니다.
- `Bank`은 은행원과 고객의 숫자 그리고 대기줄을 가지고 있으며 고객을 줄 세우고, 은행원을 이용한 전체 은행 서비스를 수행하도록 했습니다.
- 마지막으로 `BankManager`는 `Bank`를 가지고 있고 관리하는 프로그램 영역의 객체라고 생각하여 메뉴의 출력, 선택과 `Bank`를 초기화하는 로직을 담당하도록 했습니다.
### 🔥 DispatchQueue, Sleep
- 스텝2를 구현함에 있어서 고객의 업무를 처리하는 데 걸리는 시간 0.7초를 딜레이를 시켜야했습니다. 이를 구현하는데 `DispatchQueue`를 활용한 방법과, `sleep`메소드를 활용한 방법 두가지 중 어떤 방식을 사용할지 고민했습니다.
- `DispatchQueue`의 `global` 메소드를 사용하면 업무의 시작 함수(`startTask`)와 업무 종료 알림 함수(`endTask`)가 서로 다른 스레드에서 생성될 수 있다는 생각을 했습니다. 이와 더불어 비록 이번 스텝은 은행원이 한명이지만, 이후 스텝에 n명의 은행원이 생길 수 있다는 점을 고려했을 때, 은행원 한명의 Task가 다른 스레드에서 처리될 수 있다는 것은 어색하게 느껴졌습니다.
- 하여 `sleep` 메소드를 활용하여 하나의 `main`스레드에서 업무 처리가 될 수 있도록 코드를 구현했습니다.
- `Dispatch Queue`를 사용한 코드
```swift
let group = DispatchGroup()
group.enter()
DispatchQueue.global().async {
startTask(turn)
group.leave()
}
group.enter()
DispatchQueue.global().asyncAfter(deadline: .now() + 0.7) {
endTask(turn)
group.leave()
}
group.wait()
```
-----
## 조언을 얻고 싶은 부분
### ⭐️ 업무시간 출력
- 처음 스텝을 진행하며 생각했을 때는 단순히 `고객의 수 * 0.7`로 추정업무시간을 계산하려고 했지만, 메소들들의 구현 실제 구현 시간과 추정업무시간과 오차가 발생할 수 있다고 생각하였습니다. 저희는 추정시간이 아닌 실제 실행 시간을 측정하기 위해 `Date().timeIntervalSince`를 사용했습니다.
- 하지만 스텝의 `실행 예`에서는 실제 구현 시간이 아닌 추정계산된 값을 출력하여 실제 실행 시간을 출력해주는 것이 맞는지 고민이 생겼습니다.
- 이런 경우 실제 코드의 실제 실행 시간을 출력해주는 것이 좋을까요? 아니면 오차가 없는 추정계산값을 출력해주는 것이 좋을까요? 븨븨의 생각을 듣고 싶습니다!
**관련 추가 질문**
- 추가적으로 실행 시간을 측정하는 방법으로 `CFAbsoluteTimeGetCurrent()`를 사용하는 방법과 `Date().timeIntervalSince`를 사용하는 방법 중 `timeIntervalSince` 메서드가 시간 사이 간격을 구하는 메서드라 실행 시간을 측정하기에 더 알맞은 메서드라고 생각하여 사용했습니다.
- `CFAbsoluteTimeGetCurrent()`와 `Date().timeIntervalSince` 중 어떤 방법으로 실행 시간을 측정하는 것이 좋은 방법일까요?
-----
## 참고자료
- [🍎 Apple Developer: sleep(forTimeInterval:)](https://developer.apple.com/documentation/foundation/thread/1413673-sleep)
- [🍎 Apple Developer: CFAbsoluteTimeGetCurrent()](https://developer.apple.com/documentation/corefoundation/1543542-cfabsolutetimegetcurrent)
- [🍎 Apple Developer: init(format)](https://developer.apple.com/documentation/swift/string/init(format:_:) )
- [🍎 Apple Developer: Date](https://developer.apple.com/documentation/foundation/date)
- [📒 Blog: Swift 코드 실행 시간 측정 방법](https://hongssup.tistory.com/571)
-------
# STEP 3
@YebinKim
안녕하세요.
Erick & Serena 조입니다⭐️
은행창구 매니저 프로젝트 Step3 PR 보냅니다.
리뷰 잘 부탁드립니다~!
-----
## 구현한 내용
은행에 대출업무 팀에 은행원 1명과 예금업무 팀에 은행원 2명을 배치하였습니다.
대기하던 고객들의 업무 목적에 따라 대출, 예금 은행원에게 배정하였습니다.
> 주요 내용 : GCD / Opertion
-----
## 고민한 부분
### 🔥 DispatchSemaphore
- `DispatchQueue`의 `global().async`는 여러 스레드를 생성 가능하게 합니다. 이렇게 생성된 여러 스레드가 하나의 `Customer Queue`에 접근하게 되면 `Race Condition`이 발생할 수 있습니다. 이를 방지하면서 이전 step에서 은행원이 1명일 때와는 달리 은행원이 여러명일 때는 비동기 작업을 어떤식으로 처리할지 고민했습니다.
- `DispatchSemaphore`는 카운팅 `Semaphore`를 이용하여 리소스(스레드)의 접근을 제어할 수 있습니다. 이를 사용하면 멀티 스레드가 하나의 Queue에 접근하면서 생길 수 있는 `Race Condition`을 방지할 수 있었습니다.
- 또한 `DispatchSemaphore`의 값을 업무 종류에 따른 은행원의 수로 설정하고 `Wait()`과 `Signal()`을 사용하여 은행원(스레드)이 업무 중에는 동시에 다른 업무를 할 수 없도록 구현하였습니다.
```swift
self.semaphore = DispatchSemaphore(value: workType.numberOfBankClerk)
// ...
DispatchQueue.global().async(group: group) {
semaphore.wait()
startTask(customer.waitingNumber)
Thread.sleep(forTimeInterval: workType.taskTime)
endTask(customer.waitingNumber)
semaphore.signal()
}
```
### 🔥 비동기 처리 로직
- 대기줄의 고객을 각 업무에 배치하는 비동기 처리 로직을 어떤 객체에서 진행해야할지에 대한 고민을 했습니다.
- `Bank`에서는 `BankClerk` 인스턴스를 부서별로 `Deposit`과 `Loan`으로 나누어 생성하였습니다. 이로써 `Bank`에서는 `BankClerk`별 부서를 나누어, `clockIntoWork` 메소드에서 각 부서별 `BankClerk`에게 업무를 지시하였습니다.
- 지시를 받은 `BankClerk`는 `carryOutBankService`에서 부서별 업무 인원에 맞추어 업무를 처리하였습니다.
- 이때 부서별 업무 인원의 수를 `workType.numberOfBankClerk`라고 네이밍하여, 각 부서에 맞추어 `DispatchSemaphore`의 값을 지정하였습니다.
- 결론적으로 비동기 처리 로직(업무 인원에 맞춰 동시에 일을 시키는 작업)은 `BankClerk`에서 하도록 구현하였습니다.
-----
## 조언을 얻고 싶은 부분
### ⭐️ GCD VS Operation
- 비동기 로직을 구현함에 있어 `GCD`를 사용할지 `Operation`을 사용할지 고민이 많았습니다. 왜냐하면 `Operation`을 이용하여 구현도 해보았지만 `GCD`와의 차이점을 느끼지 못했기 때문입니다. 저희는 `GCD`보다 `Operation`이 객체 지향적으로 개편된 API라고 공부하여, 두 방법을 적용할 때 구조적 차이가 생길 것이라고 예상했습니다. 하지만 `GCD`로 구현한 구조와 동일한 형태로 `Operation`을 구현하게 되어 두가지 방법의 차이를 느끼지 못했습니다.
```swift
// BankClerk의 Operation 비동기 처리 로직
struct BankClerk {
// ...
func carryOutBankService(for customer: Customer, of operation: OperationQueue) {
let bankClerkTask = BlockOperation{
startTask(customer.waitingNumber)
Thread.sleep(forTimeInterval: workType.taskTime)
endTask(customer.waitingNumber)
}
operation.addOperation(bankClerkTask)
}
// ...
}
```
- 또한 `GCD`와 `Operation`의 차이로 `Operation`의 상태추적 프로퍼티를 생각했습니다. 이것 또한 현재 프로젝트에서는 사용하지 않아 `GCD`와 `Operation`의 차이가 더욱 없어진 느낌이었습니다.
- 현재는 저희가 `GCD`로 코드를 완성한 상황입니다. 하지만 이러한 경우 `Operation`과 `GCD`를 선택하는 기준이 있을까요? 븨븨의 생각이 궁금합니다!
-----
## 참고자료
- [🍎 Apple Developer: Concurrency Programming Guide](https://developer.apple.com/library/archive/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html)
- [🍎 Apple Developer: DispatchSemaphore](https://developer.apple.com/documentation/dispatch/dispatchsemaphore)
- [🐻 야곰닷넷: 동시성 프로그래밍](https://yagom.net/courses/동시성-프로그래밍-concurrency-programming/)
-------
# STEP 4
@YebinKim
안녕하세요.
Erick & Serena 조입니다⭐️
은행창구 매니저 프로젝트 마지막 PR 보냅니다.
항상 꼼꼼히 챙겨주셔서 감사합니다!
리뷰 잘 부탁드립니다~!
-----
## 구현한 내용
`Console App`에서 구현하였던 은행을 `UI App`에서도 구동될 수 있도록 하였습니다.
은행의 업무가 진행된 시간을 알 수 있는 타이머를 구현하였습니다.
> 주요 내용 : UI 코드 구현, MVC, Timer, Swift Package Manager
-----
## 고민한 부분
### 🔥 화면 회전 / scrollview
- 화면 회전을 해도 `UI`가 기기의 가자장자리에 잘리지 않고 출력될 수 있도록 `scrollView`를 뷰의 안전구역 내에 위치하게 하였습니다. 이를 위해 `view.safeAreaLayoutGuide`와 `constraint`를 설정해주었습니다.
```swift
private func setUpScrollViewConstraints() {
NSLayoutConstraint.activate([
scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor),
scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor),
scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: safeAreaLayoutGuide.topAnchor),
scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: safeAreaLayoutGuide.bottomAnchor)
])
}
```
- 화면 스크롤을 `대기중/업무중` `stackView`만 스크롤이 되도록 할지 전체 화면을 모두 스크롤을 할지 고민했습니다.
- 앱을 이용할 때 화면 내 뷰객체가 스크롤되는 것보단 전체 화면을 스크롤 하는 쪽이 접근성이 더 좋다고 생각하여 전체화면이 스크롤 되도록 설정하였습니다.
- 특히 야곰 닷넷을 통해 배운 내용 또한 활용하였습니다. 스크롤뷰 설정 시 frame과 multiply을 1로 맞춘 후 constant 1값을 주면 contentview의 크기가 작아도 드래그 및 스크롤의 효과를 내었습니다
### 🔥 Operation
- 직전 스텝과 달리 UI에 맞추어 프로젝트를 구성하다보니 어떠한 방식으로 비동기를 구현할지 고민하였습니다. 첫번째로 DispatchQueue는 Operation과 달리 스레드에 작업을 전달 시 변경 및 취소가 되지 않았습니다. 이에 UI는 main스레드에서만 진행기 때문에 작동 시 충돌의 우려가 있었습니다. 두번째로 콘솔앱과 UI를 구현할 때 뷰컨트롤이라는 가장 큰 차이가 생겼습니다. 이에 구조를 이전과 동일하게 진행하기엔 어색한 부분이 생길 수 있다고 생각했습니다
- 이에 `GCD`로 구현되어 있던 비동기 로직을 `Operation`으로 변경하였습니다.
- `BankClerk`을 업무별로 나눠 인스턴스를 생성하는 것이 아닌 업무별로 `OperationQueue`를 나눠 생성하여 스레드 관리를 하였습니다.
- 그리고 `BankClerk.carryOutBankService`를 `BlockOperation`으로 타입화하여 업무별 `Queue`에 넣어주는 것으로 비동기 처리를 하였습니다.
- `초기화` 버튼을 눌렀을 때 이미 큐에 들어간 작업을 `cancelAllOperations`를 이용하여 취소하였습니다.
</br>
- `Operation`으로 작업을 타입화 시켜줄 수 있고, `Queue`를 이용한 작업관리가 더 쉬워 코드가 간결하며 가독성이 좋아져 `Operation`으로 변경하였습니다.
```swift
class Bank {
// ...
private let loanOperationQueue: OperationQueue = {
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = WorkType.loan.numberOfBankClerk
return operationQueue
}()
private let depositOperationQueue: OperationQueue = {
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = WorkType.deposit.numberOfBankClerk
return operationQueue
}()
// ...
private func startBankService() {
while !waitingLine.isEmpty {
guard let currentCustomer = waitingLine.dequeue() else { return }
let operation = BlockOperation { BankClerk.carryOutBankService(for: currentCustomer) }
switch currentCustomer.workType {
case .deposit:
depositOperationQueue.addOperation(operation)
case .loan:
loanOperationQueue.addOperation(operation)
default:
print("workType이 nil입니다.")
}
}
}
func stopBankService() {
// ...
depositOperationQueue.cancelAllOperations()
loanOperationQueue.cancelAllOperations()
}
}
```
### 🔥 MVC 패턴에 따른 구조 고민
- `Model`, `View`, `Controller`의 역할에 대해 고민했습니다.
- 또한 객체가 서로의 프로퍼티에 직접 접근하는 것이 아닌 메서드를 통한 의사소통을 해야한다고 생각했습니다.
</br>
- **View**: `View`는 `View`객체를 프로퍼티로 가지고 `Constraints` 코드와 `View`객체에 데이터를 세팅할 수 있는 메서드를 가지고 있습니다.
- **Model**: 비즈니스 로직을 실행시킬 수 있는 코드와 `Controller`에서 이를 실행 시킬 수 있는 메서드를 가지고 있습니다.
- **Controller**: `View`와 `Model`을 객체로 가지고 있고 사용자의 입력이나 특정 이벤트가 발생했을때 뷰를 업데이트 시키거나 `Model`의 로직을 실행시킵니다.
### 🔥 Swift Package Manager를 활용하여 콘솔앱과 UI
- 기존에 구현한 콘솔앱 프로젝트를 활용하여 새롭게 UI프로젝트를 진행함에 있어 어떻게 파일을 합칠지 고민을 했습니다. 콘솔앱 프로젝트에 구현한 파일의 경로를 추적하여 파일을 공유하는 방법도 가능하지만 이 방법을 사용할 경우 UI프로젝트를 위해 파일을 변경할 경우 콘솔앱의 프로젝트도 함께 수정된다는 단점이 존재했습니다.
- 콘솔앱과 UI 프로젝트 모두 `Queue`, `Linked-List`, `Customer` 등 큐 구조와 `Customer` 내용을 로컬 `framework`로 만들었습니다. 프레임워크를 활용하는데 `CocoaPods`, `Carthage`, `SPM` 등 여러 방법이 있지만 로컬로 바로 연동이 가능한 `SPM`을 사용하여 콘솔앱 프로젝트의 공통 내용을 UI 프로젝트에 적용하였습니다
### 🔥 Public 키워드
- `swift packmanager`를 사용하여 `customer` 로컬 패키지를 생성하여 활용하는 과정에서 직접 만든 패지를 활용하다보니 새로운 문제에 직면했습니다. 기본적으로 설정되어있는 `internal`을 모듈 외부에서도 사용가능하게 수정해야하는 문제였습니다. 모듈 외부에서 사용 가능하게 하는 접근제어자에는 `public`과 `open`이 있습니다. 이 둘의 차이점을 고려하여 적용하고자 하였습니다.
- `open`과 `public`은 모두 외부에 개방이 되어 있어 모듈 밖에서 모두 접근이 가능하다는 공통점이 있지만, `open`의 경우 오버라이드, 서브클래싱이 가능하지만 `public`의 경우 오버라이드, 서브클래싱이 불가능합니다. 이러한 차이를 고려을했을 때 정형화된 큐와 `Customer` 객체를 공유하고자 프레임워크를 생성한 저희의 의도에는 `public`이 더 적절하다 생각하여 `public`을 을각 메소드, 프로퍼티별로 추가하였습니다.
```swift
public struct Customer {
public let waitingNumber: Int
public let workType: WorkType? = WorkType.allCases.randomElement()
public init(waitingNumber: Int) {
self.waitingNumber = waitingNumber
}
}
```
-----
## 조언을 얻고 싶은 부분
### ⭐️ Timer vs. DispatchSourceTime의 차이
**Timer**
- 특정 시간 간격이 경과한 후 대상 개체에 지정된 메시지를 보냅니다.
- 주로 메인 스레드에서 실행되며 단순한 반복작업을 수행할 때 사용한다고 이해했습니다.
**DispatchSourceTimer**
- 타이머를 기반으로 이벤트 핸들러 블록을 제출하는 디스패치 소스입니다.
- GCD에서 제공하는 기술로 메인 스레드가 멀티 스레드에서 실행될 수 있어 백그라운드 환경에서도 실행될 수 있습니다.
타이머를 구현할 수 있는 두가지 방법 중 현재 앱은 백그라운드에서는 타이머가 실행이 안되고 업무 중일때만 실행이 되며 단순한 반복작업(타이머 카운트에 대한 UI 업데이트)만 수행하면 된다고 생각하여 `Timer`를 이용하여 구현하였습니다.
저희가 선택한 방법이 적절한지 `Timer`와 `DispatchSourceTimer` 선택에 대한 븨븨의 기준이 있는지 궁금합니다!
또한 DispatchSourceTimer에 대한 자료가 많이 없어서 활용 시 어려움이 많았습니다. 특히 cancel, suspend 등 메소드들을 적극적으로 활용해보고자 노력해보았는데 이를 효율적으로 사용모하지 못해서 아쉬운 점이 많았습니다. 이런 부분에서 븨븨께서 조언을 주신다면 적극적으로 배워보고 싶습니다!
-----
## 참고자료
- [🍎 Apple Developer: Operation](https://developer.apple.com/documentation/foundation/operation)
- [🍎 Apple Developer: Accesscontrol](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/accesscontrol/)
- [🍎 Apple Developer: Timer](https://developer.apple.com/documentation/foundation/timer)
- [🍎 Apple Developer: Dispatchsourcetimer](https://developer.apple.com/documentation/dispatch/dispatchsourcetimer)