## 팀원 : 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)