안녕하세요 여러분! 제가 은행창구 매니저 프로젝트를 진행했을 때 해결하지 못했던 문제를 이번에 해결하게 되어서 여러분들께 도움이 될까 하여 공유해봅니다. 야곰닷넷의 Concurrency Programing 코스 GCD 예제를 보면 비동기 코드 테스트는 `플레이그라운드`를 활용할 것을 권장하고 있습니다. CLT 환경에서는 마지막 코드가 끝나면 프로그램이 종료되기 때문이라고 하고, CLT 환경에서 비동기 코드를 테스트하려면 어떤 코드들이 추가로 필요할 것이라고도 나와있죠. 직접 Xcode의 CLT project에서 비동기 코드를 테스트해보면 실제로 잘 작동하지 않는 경험을 할 수 있습니다. 아래 사진처럼 말이죠...... ![](https://hackmd.io/_uploads/rkGlIoS9n.png) 이런 현상은 왜 일어나는 걸까요? CLT는 한 번 실행되고 나면 프로그램이 종료되는 Top-down approach를 사용하고 있다고 합니다. 따라서 main.swift 파일에 실행할 코드가 더 이상 없다면 프로그램이 종료가 되어버립니다. 비동기로 넘긴 코드들은 main 스레드에서 실행되지 않기 때문에 실행할 코드가 없어지겠죠?! 그러면 CLT 환경에서 비동기 코드를 실행하려면 어떻게 해야할까요? 비동기 코드 실행을 위한 방법은 여러가지가 있겠지만 이번에는 RunLoop를 제어해 프로그램이 종료되지 않도록 하는 방법을 공유하려 합니다. </br> ## RunLoop 우선 개발자 문서 Threading Programming Guide의 Run Loop에 대한 설명을 보겠습니다. ``` Run Loop는 작업을 예약하고 들어오는 이벤트 수신을 조정하는 데 사용하는 이벤트 처리 loop이다. ``` 즉, 프로그램이 실행되는 동안 발생하는 이벤트를 처리하기 위해 스레드를 유지해주는 객체라고 볼 수 있겠네요. > ❗️ CLT는 콘솔에 실행 결과를 순서대로 보여주며 GUI가 없기 때문에 이벤트 처리를 위한 객체인 Run Loop를 실행하는 것이 좋은 방법은 아닌 것 같습니다. 다른 좋은 방법이 있을 것 같아요.. 🥲 > 다른 방법을 알게 되시면 공유 부탁드립니다 🙏 Swift 앱은 기본적으로 run loop를 자동으로 생성하고 관리합니다. 대부분의 경우 이 기본 run loop를 사용하는데, CLT 프로그램은 main run loop가 자동으로 실행되지 않기 때문에 직접 제어해주어야 합니다. </br> ## 문제 해결하기 간략히 RunLoop에 대한 설명이 되었으니 문제가 생겼던 코드를 보시죠! ```swift final class BankManager { // ... @objc func start() { printMenu() guard let userInput = readInput() else { return } handleMenuInput(userInput) } private func handleMenuInput(_ userInput: String) { switch userInput { case "1": let totalCustomer = customerReceiver.receiveCustomer() DispatchQueue.global().async { [weak self] in guard let self else { return } bank.open(totalCustomer: totalCustomer) NotificationCenter.default.post(name: NSNotification.Name.bankClosed, object: nil) } case "2": return default: let message = "잘못 입력하셨습니다." print(message) NotificationCenter.default.post(name: Notification.Name.bankClosed, object: nil) } } } ``` 문제의 원인은 은행 업무를 처리할 때 `sleep` 메서드를 통해 잠깐 스레드를 멈추게되는 부분입니다. main 스레드가 sleep되는 것을 피하기 위해 `bank.open()`을 `global().async`를 통해 넘겨주고 있습니다. (반복문 없이 메서드를 반복 호출하는 것은 Notification을 활용했습니다.) 이때, main.swift 에서 `bankManager.start()`를 호출하게 되면 반복문이 없기 때문에 위 사진에서 볼 수 있듯이 비동기 코드가 실행되기 전에 start 메서드가 끝나고 프로그램이 종료됩니다. 이를 해결하기 위해 RunLoop 객체를 활용해 보겠습니다. RunLoop 객체를 얻는 방법은 여러가지가 있지만 그 중에서`stop`이 제공되는 CFRunLoop 객체를 선택했습니다. * 사용할 메서드 * `CFRunLoopGetCurrent()` - 현재 스레드의 CFRunLoop 객체 반환 * `CFRunLoopRun()` - 현재 스레드의 CFRunLoop 객체를 default 모드로 즉시 실행 * `CFRunLoopStop(CFRunLoop!)` - 전달된 CFRunLoop 객체가 실행을 중지하도록 강제 그럼 main run loop를 실행해봅시다! ```swift import Foundation bankManager.start() CFRunLoopRun() ``` ![](https://hackmd.io/_uploads/H1bYg6rq3.png) 정상적으로 실행되는 것이 확인됐습니다!!! 하지만 2를 입력했는데 프로그램이 종료되지 않는 문제가 발생했습니다. RunLoop가 실행되고 있기 때문에 프로그램이 종료되지 않고 계속 대기중인 것으로 생각됩니다. 그럼 RunLoop를 종료해야겠네요! 2가 입력되었을 때, RunLoop를 종료하기 위해 escaping closure를 활용합니다. ```swift // main.swift let runLoop = CFRunLoopGetCurrent() func stopRunLoop() { CFRunLoopStop(runLoop) } bankManager.addObserver() bankManager.start(completionHandler: stopRunLoop) CFRunLoopRun() ``` ```swift final class BankManager { //... @objc func start(completionHandler: @escaping () -> Void) { printMenu() guard let userInput = readInput() else { return } handleMenuInput(userInput, onCompleted: completionHandler) } private func handleMenuInput(_ userInput: String, onCompleted: @escaping () -> Void) { switch userInput { case "1": let totalCustomer = customerReceiver.receiveCustomer() DispatchQueue.global().async { [weak self] in guard let self else { return } bank.open(totalCustomer: totalCustomer) NotificationCenter.default.post(name: NSNotification.Name.bankClosed, object: nil) } case "2": DispatchQueue.main.async { onCompleted() } return default: let message = "잘못 입력하셨습니다." print(message) NotificationCenter.default.post(name: Notification.Name.bankClosed, object: nil) } } } ``` ![](https://hackmd.io/_uploads/Hk2UdaB53.png) 이렇게 발생했던 문제들을 모두 해결해봤습니다. RunLoop 설명 인용문에서도 말씀드렸듯이 이 방법은 그렇게 좋은 방법이 아닌 것 같습니다🤔 추후 main run loop라는 단어를 만났을 때 생소하지 않도록 '아 RunLoop라는게 있구나' 정도로 넘어가주시고 좋은 방법을 직접 고민해보시는 것이 더 좋은 경험이 될 거라고 생각합니다. (해결하시면 꼭 공유 부탁드림...) Notification과 escaping closure도 꼭 정리해보시길 💪 이번 프로젝트도 고생 많으셨습니다 👏 전체 프로젝트 코드는 [여기](https://github.com/Tediousday93/ios-bank-manager/tree/RunLoop/BankManagerConsoleApp/BankManagerConsoleApp)에서 확인하실 수 있습니다👀