-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
은행 창구 관리 앱 [Step3] 이지, hong #92
base: d_Hong
Are you sure you want to change the base?
Conversation
This reverts commit c35f050.
private var loanBankerQueue = Queue<Banker>() | ||
private var depositCustomerQueue = Queue<Customer>() | ||
private var loanCustomerQueue = Queue<Customer>() | ||
private let group = DispatchGroup() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DispatchGroup을 Bank 타입 내부 변수로 선언해주신 이유는 무언인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
작업별로 나뉘어진 메서드에서 접근하기 위해 Bank타입에 DispatchGroup를 선언했습니다.
|
||
let formattedTotalWorkingTime = max(totalLoanWorkingTime, totalDepositWorkingTime).formattedDecimal | ||
Messages.closeBank(customerCount: numberOfCustomers, totalTime: formattedTotalWorkingTime).printMessage() | ||
depositBankerQueue.clear() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clear 메서드가 필요한 이유는 Banker를 setUp하는 부분을 계속해서 반복해야하는 open이 호출되면서 중복으로 호출되는 것이 문제로 보여요! 이 부분을 clear를 사용하지 않는다면 어떻게 해결할 수 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if문을 활용한 방법
private func setUpBankerQueue(depositBankerCount: Int, loanBankerCount: Int) {
if depositBankerQueue.isEmpty() && loanBankerQueue.isEmpty() {
for _ in 1...depositBankerCount {
depositBankerQueue.enqueue(Banker(taskType: .deposit))
}
for _ in 1...loanBankerCount {
loanBankerQueue.enqueue(Banker(taskType: .loan))
}
}
}
init에서 생성하는 방법
init(depositBankerCount: Int, loanBankerCount: Int) {
for _ in 1...depositBankerCount {
depositBankerQueue.enqueue(Banker(taskType: .deposit))
}
for _ in 1...loanBankerCount {
loanBankerQueue.enqueue(Banker(taskType: .loan))
}
self.depositBankerCount = depositBankerCount
self.loanBankerCount = loanBankerCount
self.totalDepositWorkingTime = 0.0
self.totalLoanWorkingTime = 0.0
}
두가지 방법을 더 작성해 보았습니다. 저희는 생성자를 활용한 방법이 깔끔하고 일을 줄이는 것 같습니다! ㅎㅎ
} | ||
group.wait() | ||
|
||
let formattedTotalWorkingTime = max(totalLoanWorkingTime, totalDepositWorkingTime).formattedDecimal |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Time을 계산하는 로직이 실질적으로 업무시간을 체크하는 것과 조금 상이해보이는 것 같아요. 각각의 업무 Time을 요구사항에서 요구하진 않으니 Time을 1개로 관리하고 출력하는 것은 어떨꺄요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
시간 측정은 관점에 따라 달라 질 수 있는데, 저희는 은행의 모든 업무가 끝날때 까지의 시간을 각각의 은행원들의 업무시간을 더해 측정하는 방식으로 만들었습니다. 예금과 대출의 Time을 따로 둔 것은 각자 따로 더해서 더 오래걸린 업무가 실제 총 은행 업무시간이라 생각했기 때문입니다.
하지만 다시 생각해보니 총 은행업무시간과는 다를 수 있다는 걸 깨달았습니다. 심지어 비동기적으로 업무를 처리하는 예금 은행원의 경우엔 저희의 의도와 정반대로 측정하고 있기 때문에 Date 메서드를 활용해 실제 코드의 실행시간을 측정하는 방향으로 다시 만들어 보았습니다!
let startTime = Date()
Messages.openBank.printMessage()
let numberOfCustomers = Int.random(in: 10...30)
print("고객 수: \(numberOfCustomers)")
setUpBankerQueue(depositBankerCount: depositBankerCount, loanBankerCount: loanBankerCount)
setUpCustomerQueue(count: numberOfCustomers)
while !depositCustomerQueue.isEmpty() || !loanCustomerQueue.isEmpty() {
serveCustomer(bankerQueue: depositBankerQueue, customerQueue: depositCustomerQueue)
serveCustomer(bankerQueue: loanBankerQueue, customerQueue: loanCustomerQueue)
}
group.wait()
let endTime = Date()
let elapsedTime = endTime.timeIntervalSince(startTime)
let formattedTotalWorkingTime = elapsedTime.formattedDecimal
Messages.closeBank(customerCount: numberOfCustomers, totalTime: formattedTotalWorkingTime).printMessage()
serveCustomer(bankerQueue: depositBankerQueue, customerQueue: depositCustomerQueue) | ||
serveCustomer(bankerQueue: loanBankerQueue, customerQueue: loanCustomerQueue) | ||
} | ||
group.wait() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Dispatchgroup에서의 enter, leave, enotify, wait에 대해서 각각 설명해주시고 이번 프로젝트에서는 왜 wait을 사용하셔서 구현했는지 이유를 설명해주세요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
enter()
:Dispatchgroup
에서 추적하려는 작업이나 코드 블록의 시작을 나타냅니다. 내부 task reference count 를 1 증가시켜 작업이 아직 끝나지 않았음을 알려줍니다. -
leave()
:Dispatchgroup
에서 추적하는 작업의 끝을 나타내며 task reference count 를 1 감소시켜 0이 될 시 작업이 끝났음을 알려줍니다.enter
와leave
는 서로 짝지어 쓰이면서 주로 디스패치 그룹에 비동기 작업이 포함된 task 를 보낼 때 해당 작업의 끝나는 지점을 알려주기 위해 사용합니다. -
notify(queue:)
:Dispatchgroup
에 추가된 모든 작업이 완료될 때 실행될 클로저를 예약할 때 사용됩니다. 또한 실행될 클로저가 실행되는 큐를 지정할 수 있습니다. -
wait()
:Dispatchgroup
이 추적하는 모든 작업이 완료때까지 현재 스레드를 block 시킬 수 있습니다. 저희는 해당 메서드를 통해 은행원의 모든 비동기 업무가 종료되는 시점까지 코드를 진행하지 않게 만들었습니다.
} | ||
|
||
private func processTask(banker: Banker, customer: Customer, bankerQueue: Queue<Banker>) { | ||
DispatchQueue.global().async(group: group) { [self] in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- DispatchQueue란 무엇인가요?
- global() 이란 어떤 키워드이고 어떨 때 사용되나요?
- sync와 async의 차이점은 무엇인가요?
- 여기서의 group은 왜 사용해주신 건가요?
- CaptureList를 사용해주셨네요? 사용해주신 CaptureList는 무엇이고 왜 사용해주셨나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
DispatchQueue:
◦DispatchQueue
는 Grand Central Dispatch(GCD)에서 제공하는 기능 중 하나로, 비동기적으로 작업을 실행하는 데 사용됩니다.
◦ 큐(Queue)는 작업을 순서대로 처리하거나, 동시에 여러 작업을 실행할 수 있도록 도와줍니다. -
global():
◦global()
은 DispatchQueue의 static 메서드로, 글로벌(dispatch global) 큐를 반환합니다.
◦ 글로벌 큐는 시스템 전체에서 사용할 수 있는 큐로, 다양한 QoS(Quality of Service) 레벨을 제공하여 작업을 효과적으로 스케줄링할 수 있습니다. -
sync와 async의 차이:
◦sync
: 현재 스레드에서 작업을 동기적으로 실행합니다. 해당 작업이 완료될 때까지 대기하며, 다음 코드는 해당 작업이 끝날 때까지 실행되지 않습니다.
◦async
: 작업을 비동기적으로 실행합니다. 현재 스레드에서 대기하지 않고, 작업을 큐에 추가한 후 즉시 다음 코드를 실행합니다. -
group 사용 이유:
◦DispatchGroup
은 여러 비동기 작업을 그룹화하고, 해당 그룹 내에서 모든 작업이 완료될 때까지 대기하도록 하는 데 사용됩니다.
◦ 여러 큐에서 실행되는 작업들을 조율하여, 모든 작업이 완료될 때까지 기다릴 수 있게 해줍니다.
◦ deposit 큐와 loan 큐가 끝나는 시점이 다르기 때문에 둘이 모두 끝나는 시점에 group.wait()시킨 후 업무 마감 정보를 출력하기 위해서입니다. -
CaptureList:
◦ CaptureList는 클로저 내에서 외부 변수를 참조할 때 발생하는 strong reference cycle(강한 참조 순환)을 방지하는 데 사용됩니다.
◦ [weak self]나 [unowned self]와 같이 사용하여 클로저가 self를 강한 참조하지 않도록 하고, 메모리 누수를 방지합니다.
◦ 코드에서 [self]를 사용하여 클로저 내에서 self를 캡처하면서도 strong reference cycle을 방지할 수 있습니다.
} | ||
|
||
private func serveCustomer(bankerQueue: Queue<Banker>, customerQueue: Queue<Customer>) { | ||
if bankerQueue.isEmpty() == false { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Queue에서 Dequeue하는 로직이 조금 이해하기 힘든 것 같아요. 사용하는 Queue의 갯수를 줄인다면 조금 더 이해하기 쉬운 로직으로 개선이 가능할 것 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여러가지 시도를 해보다가 결국 은행원 큐를 없애고 손님 큐 하나로 만들어 보았습니다.
Banker
는 업무를 진행하는 메서드만 소유하고 있고 손님의 업무 타입에 따라 비동기 메서드 processCustomer
를 실행합니다.
은행원의 수는 DateSemaphore
를 활용해 해당 메서드에 접근할 수 있는 스레드를 제한 했습니다.
결과적으로 손님을 데려다 일하는 은행원을 스레드로 보는 관점으로 수정해 좀 더 간결한 코드가된 것 같습니다!
class Bank {
...
private let depositSemaphore: DispatchSemaphore
private let loanSemaphore: DispatchSemaphore
...
init(depositBankerCount: Int, loanBankerCount: Int) {
self.depositSemaphore = DispatchSemaphore(value: depositBankerCount)
self.loanSemaphore = DispatchSemaphore(value: loanBankerCount)
}
func open() {
...
while let customer = customerQueue.dequeue() {
switch customer.taskType {
case .deposit:
serveCustomer(semaphore: depositSemaphore, customer: customer)
case .loan:
serveCustomer(semaphore: loanSemaphore, customer: customer)
}
}
...
}
...
private func serveCustomer(semaphore: DispatchSemaphore, customer: Customer) {
DispatchQueue.global().async(group: group) {
semaphore.wait()
self.banker.processCustomer(customer)
semaphore.signal()
}
}
}
|
||
init?(number: Int) { | ||
self.number = number | ||
guard let randomTaskType = TaskType.allCases.randomElement() else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Customer의 생성이 실패하는 경우가 존재해야하나요?
위의 코드는 allCases를 사용하시려다보니 Optional Binding으로 인해 발생한 불필요한 init?으로 보여요 ㅎㅎ
다르게 해결해 볼 수 있지 않을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- TaskType에 random메서드를 만들어 allCases를 사용하지않고 loan과 deposit 두 경우에 대해서만 랜덤하게 선택할 수 있도록 했습니다.
final class Customer {
let number: Int
let taskType: TaskType
init(number: Int) {
self.number = number
self.taskType = TaskType.random()
}
}
enum TaskType: CaseIterable, CustomStringConvertible {
case loan
case deposit
}
enum TaskType: CaseIterable, CustomStringConvertible {
case loan
case deposit
}
extension TaskType {
var description: String {
switch self {
case .loan:
return "대출"
case .deposit:
return "예금"
}
}
static func random() -> TaskType {
return Bool.random() ? .loan : .deposit
}
var taskTime: Double {
switch self {
case .loan:
return 1.1
case .deposit:
return 0.7
}
}
}
or
- fatalError를 사용해서 예외처리를 했습니다.. 그런데 fatalError는 프로그램을 중단시키기 때문에 애플리케이션이 예기치 않은 상황에 직면할 때 적절한 조치를 취할 수 없게 되기때문에 별로 좋지 않은 방법 같은데 어떻게 생각하시나요?..
init(number: Int) {
self.number = number
if let randomTaskType = TaskType.allCases.randomElement() {
self.taskType = randomTaskType
} else {
// allCases가 비어있거나 nil을 반환할 경우에 대한 예외 처리
fatalError("TaskType.allCases is empty or returned nil")
}
}
// | ||
// Created by dopamint on 2/1/24. | ||
// | ||
enum TaskType: CaseIterable, CustomStringConvertible { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CustomStringConvertible 은 어떤 역할을 하고 있나요?
그리고 왜 사용되어야 하나요? description을 그냥 추가해주기만 하면 안되는 걸까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CustomStringConvertible
은 특정 type을 textual한 표현으로 커스텀해주는 protocol입니다.
struct Sample: CustomStringConvertible {
var description: String {
return "description"
}
}
let a = Sample()
print(a) // description
print(a.description) //description
예를 들어 위처럼 CustomStringConvertible
채택 시 인스턴스를 출력하면 description
의 string이 출력되는것을 확인 할 수 있습니다.
저희의 코드에서는 굳이 CustomStringConvertible
을 채택하지 않아도 상관 없을 것 같습니다!
|
||
private func setUpCustomerQueue(count: Int) { | ||
for number in 1...count { | ||
guard let customer = Customer(number: number) else { return } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Customer의 생성이 실패하면 Queue의 설정자체가 return 되는 로직으로 보여요. 의도한 부분이 맞으실까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Customer 클래스 생성자가 옵셔널로 만들어져서 바인딩을 위해서 작성했습니다.. 생성자 수정해 바인딩이 필요없어져 삭제했습니다.
질문 해주신 부분 정리해 봤습니다~
|
@LeeZion94
안녕하세요 시온~~! 오래 기다리셨습니다..
STEP 3 PR 보내드립니다~
📝구현 사항
프로퍼티
예금 은행원과 대출 은행원의 수를 나타내는 프로퍼티와 각각의 업무 시간을 측정하는 프로퍼티를 가지고 있습니다.
Queue타입으로 구현한 대기열들
큐를 이용하여 예금 은행원과 대출 은행원, 예금 고객, 대출 고객을 관리합니다.
업무가 진행되기 위해서는 손님 뿐만 아니라 현재 업무가 가능한 은행원 (일하고 있지 않은 은행원)도 동시에 필요하기 때문에 업무에 맞는 은행원 큐를 따로 만들어
현재 업무가 가능한 은행원의 대기열
을 구현했습니다.각각의 큐는
setUpBankerQueue
와setUpCustomerQueue
를 통해 은행 개점과 함께 반복문으로 enqueue되며 채워집니다.업무구분을 위한 TaskType
업무 종류에 따라 각각의 enum 케이스를 만들고, 필요한 계산속성을 구현했습니다.
업무시간 측정을 위한 속성
예금업무와 대출업무 소요시간을 각각 다른 변수에 저장해 두 시간중 더 오래걸린 시간을 총 업무시간으로 표현했습니다.
Double
타입의 확장을 통해 두자리수로 잘라 출력되도록 했습니다.업무 진행을 위한 메서드와 비동기 처리
serveCustomer
:customerQueue
와bankerQueue
에서 dequeue 된 각각의customer
와banker
를processTask
메서드로 전달 해 업무를 진행합니다. (손님과 은행원이 만났습니다.)processTask
:serveCustomer
에서전달받은banker
가customer
를 가지고processCustomer
를 통해 업무를 진행합니다. (은행원이 손님의 업무를 처리해줍니다.) 업무가 마무리되면 다시bankerQueue
에banker
가 enqueue 되며 다음 손님을 기다립니다.이때, 업무의 종료 여부와 상관없이 Bank 내의 다른 업무들이 진행될 수 있도록
DispatchQueue.global().async
를 통해 다른 스레드에서 비동기 작업을 하도록 구현했습니다.🤔고민했던 점들
비동기 처리의 시행착오들
처음 비동기 처리를 위한 코드를 작성할 때 어려웠던 점은 오류예측과 오류의 원인을 파악하는 것이 힘들다는 것 이었습니다. 그래서 여러가지 오류를 만나고 비동기 처리의 범위를 다양하게 바꿔보면서 시도해 보았습니다. 그중 가장 해결하기 힘들었던 것은 다음과 같은 오류입니다.
반복문을 빠져나가 업무 종료메세지를 띄운 후에도 비동기 처리 작업이 남아있어 뒤늦게 고객업무 종료 메세지를 띄우게 됩니다.
(위와같은 상황입니다.)
출처 : 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-8-37146743787f
그래서 저희는
DispatchGroup
을 활용해 비동기 업무까지 모두 종료된 시점까지 코드가 진행되지 않도록 하여 문제를 해결했습니다.Queue를 전역적으로 사용했을 때 문제점..?
첫번째 실행 후 큐가 초기화 되지 않아 두번째 실행부터 은행원이 남아있게 되어 다음과 같은 문제가 발생합니다.
은행 업무가 종료된 후에도 은행원이 큐에 남아있기 때문에, 다음 은행 업무를 수행할 때 이미 사용된 은행원이 재사용됩니다.
초기화를 통해 은행 업무가 종료된 후에 은행원 큐가 비워지고 다음 은행 업무를 수행할 때 새로운 은행원이 생성되어 큐에 들어갑니다.
큐를 전역적으로 사용하지 않고 어떻게 활용할 수 있을까요..?
은행원 타입이 꼭 필요할까?
처음부터 현실세계를 반영한다는 느낌으로 각각의 타입을 만들었습니다.(업무 가능한 은행원 대기열을 위해) 그러나 현재 코드의 banker는 customer가 dequeue 될 수있는 조건으로서 쓰이는 것 같기도 하고 함수로 대체될수 있지않을까.. 란 생각을 하기도 했습니다.
이런 생각으로 코드를 짜다보니 쓸데없이 코드가 많아지거나 성능상 불이익이 있는 방법으로 만들고 있는게 아닐까란 생각이 들었습니다.
상황에 따라 많은 정답들이 있겠지만, 시온은 설계단계에서 어떤 것을 더 중점으로 하시는지, 그 기준이 궁금합니다!
개발자가 이해하기 쉬운코드 VS 간결한 or 조금더 나은 성능의 코드
TaskType vs Task
�'업무 종류' 를
TaskType
으로 네이밍 했는데,TaskType
자체가 타입 이기 때문에 개발자 입장에서 'TaskType
타입' 으로 불러야 할 것 같아Task
로 바꾸는것이 맞을까? 란 생각을 해보았습니다..