훌륭한 개발자가 되기 위하여
동기와 비동기, Thread와 Coroutine 그리고 여러 가지 패턴 본문
동기와 비동기
동기 : 어떤 요청을 보낸 뒤, 그 요청의 결과값을 얻기까지 작업을 멈추는 것
특징
- 순차적 처리: 요청을 보내면 결과가 반환될 때까지 기다립니다.
- 작업 차단: 요청이 완료될 때까지 다음 작업을 수행하지 않습니다.
- 단순성: 코드의 흐름이 직관적이며, 이해하기 쉽습니다.
- 응답 시간: 결과를 기다리는 동안 아무것도 하지 않기 때문에 응답 시간이 길어질 수 있습니다.
비동기 : 어떤 요청을 보낸 뒤, 그 요청의 결과값이 오기까지 멈추지 않고 또 다른 일을 수행하는 것
특징
- 병렬적 처리: 요청을 보내고, 결과를 기다리지 않으며, 다른 작업을 계속 수행합니다.
- 작업 비차단: 결과가 준비되면 알림을 받거나 콜백 함수가 호출됩니다.
- 복잡성: 코드의 흐름이 복잡해질 수 있으며, 콜백이나 비동기 제어 구조를 사용해야 합니다.
- 효율성: CPU 자원을 효율적으로 사용할 수 있으며, 긴 작업이 진행되는 동안 다른 작업을 병렬로 수행할 수 있습니다.
Process : 보조기억장치의 프로그램이 메모리 상으로 적재되어 실행되면 프로세스가 된다.
Thread : 같은 Process 내에서 실행되는 여러 작업 (흐름)의 단위
Thread와 Coroutine의 공통점
동시성 (Concurrency)
동시에 여러 작업을 수행하는 것이다.
-> 시분할 기법을 활용하여 그림과 같이 여러 작업을 조금씩 나누어서 번갈아가며 실행하는 것이다.

병렬성 (Parallelism)
시분할 기법없이 여러 작업을 한 번에 동시에 수행하는 것인데 자원(CPU 코어)의 입장에선 자기는 자기 할 일 1개를 하는 것이다. -> 병렬성은 자원(CPU 코어)이 여러 개 일 때 가능하다.

Thread와 Coroutine의 차이점
Thread
- 작업의 단위 : Thread
- 각 Thread가 독립적인 Stack 메모리 영역을 가짐
- 동시성 보장 수단 : Context Switching
- 운영체제 커널에 의한 Context Switching을 통해 동시성 보장
- 블로킹 (Blocking) : Thread A가 Thread B의 결과가 나오기까지 기다려야 한다면, Thread A은 Thread B의 결과가 나올 때 까지 해당 자원을 못 씀

Thread A 에서 Task 1 을 수행하다가 Task 2 의 결과가 필요할 때, 비동기적으로 Thread B 를 호출을 하게 된다. 이 때 Thread A 는 블로킹되고, Thread B 로 Context Switching 이 일어나 Task 2 를 수행한다. Task 2 가 완료되면, 다시 Thread A 로 Context Switching 이 일어나 결과값을 Task 1 에 반환한다.
Thread의 구성요소
- ThreadGroup : 여러 Thread를 하나의 그룹으로 묶어서 관리할 수 있는 클래스
- Runnable : 실행될 코드 블록을 정의하는 인터페이스 / run 메서드를 구현하여 실행될 작업을 정의한다.
- ExecutorService : ThreadPool을 관리하여 Thread 생성을 최적화하는 클래스
- Future : 비동기 작업의 결과를 나타내는 객체로 작업이 완료되었는지 여부를 확인하고 결과를 가져올 수 있다.
- Synchronization : Thread 간의 자원 접근을 조율하여 동기화 문제를 방지하는 메커니즘
- Synchronized 키워드 또는 Lock 클래스를 사용하여 구현
Coroutine
- 작업 하나하나의 단위 : Coroutine Object
- 여러 작업 각각에 Object 를 할당함
- Coroutine Object 도 엄연한 객체이기 때문에 JVM Heap 에 적재 됨 (Kotlin 기준)
- 동시성 보장 수단 : Programmer Switching (No-Context Switching)
- 프로그래머의 코드를 통해 Switching 시점을 마음대로 정함 (OS 관여 X)
- Suspend (Non-Blocking) : Object 1 이 Object 2 의 결과가 나오기까지 기다려야 한다면, Object 1 은 Suspend 되지만, Object 1 을 수행하던 Thread 는 그대로 유효하기 때문에 Object 2 도 Object 1 과 동일한 Thread 에서 실행될 수 있음

작업 단위는 Coroutine Object 이므로, Task 1 을 수행하다가 비동기 작업 Task 2 가 발생하더라도, Context Switching 없이 같은 Thread 에서 Task 2 를 수행할 수 있고, 맨 오른쪽 경우처럼 하나의 Thread 에서 여러 Coroutine Object 들을 같이 수행할 수도 있다. 한 쓰레드에서 다수의 Coroutine 을 수행할 수 있고, Context Switching 이 필요없는 특성에 따라 Coroutine 을 Light-weight Thread 라고 부르는 것이다.
코루틴의 구성요소
- CoroutineScope: 하나 이상의 관련 코루틴을 관리, 모든 코루틴은 해당 범위 내에서 실행해야 한다.
- 종류: CoroutineScope, LifecycleScope, ViewModelScope
- launch: 코루틴을 만들고 함수 본문의 실행을 해당하는 디스패처에 전달하는 함수
- Dispatcher: 코루틴이 어느 스레드에서 실행될지 결정하는 역할
- Dispatchers.Main : UI와 상호작용하고 빠른 작업을 실행하기 위해서만 사용해야 한다.
- Dispatchers.IO : 메인 스레드 외부에서 디스크 또는 네트워크 I/O를 실행하도록 최적화되어 있다.
- Dispatchers.Default : CPU를 많이 사용하는 작업을 실행하는데 최적화되어 있다. (list sort, Json parsing..)
Coroutine은 Thread의 대안이 아니라, Thread를 더 잘게 쪼개어 사용하기 위한 개념이다.
작업의 단위를 Object로 축소하면서 하나의 Thread가 다수의 코루틴을 다룰 수 있기 때문에, 작업 하나하나에 Thread를 할당하여 메모리 낭비, Context Switching 비용 낭비를 할 필요가 없음
싱글톤 패턴
객체의 인스턴스를 1개만 생성하여 계속 재사용하는 패턴
장점
- 정적 메모리 영역을 사용하기 때문에 추후 해당 객체에 접근할 때 메모리 낭비를 방지할 수 있다.
- 전역 데이터처럼 사용할 수 있어 쉽게 접근하고 공유하기 쉽다.
- 공통된 여러 객체를 생성하여 사용하는 경우 편하다.
단점
- 동시성 문제가 발생할 수도 있다. -> 싱글톤 객체에 대한 동시접근 문제
- 테스트 어려움 : 전역 상태를 가지기 때문에 단위 테스트가 어려울 수 있다.
class SingleTon private constructor() {
companion object {
private val instance = SingleTon()
fun sharedInstance(): SingleTon = instance
}
}
Publisher-Subscriber 패턴
발행자와 구독자가 있고, 그 사이에 브로커(=메시지 큐)가 존재하는 형태이다.
특징
- 발행자 메시지의 수진자가 정해져 있지 않다.
- 메시지는 정해진 범주에 따라서 구독을 신청한 수신자에게 전달이 된다.
- 수신자는 발행자에 대한 정보 없이, 원하는 메시지를 수신할 수 있다.
발행자(=pub)와 구독자(=sub)
1. pub이 sub의 선언 위치나 존재를 알 필요가 없다.
- 메시지 큐와 같은 브로커 역할을 하는 중간 지점에 메시지를 던져 두기만 하면, 브로커가 알아서 처리한다.
2. sub 역시 pub의 선언 위치나 존재를 알 필요가 없다.
- 브로커에 할당된 작업만을 모니터링하고, 원하는 작업이 발생하면 할당받아 작업을 수행하면 된다.
브로커
브로커는 발행자와 구독자 사이에 위치하고 있다.
메시지 큐
- 주로 메시지큐가 브로커로서의 역할을 수행하며, 두 객체 사이에서 구독과 발행 이후의 메시지를 처리해준다.
- 브로커가 모든 들어오는 메시지를 필터링하며, 타겟들(=구독자)에게 메시지를 배포하는 역할을 한다.
- 다른 곳의 API로부터 데이터 송수신이 가능하다.
- 다양한 앱과 비동기 통신이 가능하다.
- 이메일 발송, 문서 업로드가 가능하다.
- 많은 양의 프로세스를 처리 가능하다.
장점
- 비동기(Asynchronous) : 큐에 넣어서 나중에 처리 가능
- 비동조(Decoupling) : 앱과 분리 가능
- 탄력성(Resilience) : 일부 실패가 전체에 영향x
- 과잉(Redunadancy) : 실패할 경우 재실행 가능
- 보증(Guarantees) : 작업 처리 확인 가능
- 확장성(Scalable) : 다수 프로세스들이 큐에 메시지 보내기 가능
단점
- '큐'이기 때문에 사용자가 많아지거나, 데이터가 많아지면 요청에 대한 응답이 늦어지게 된다.
- 즉 과도한 트래픽이 몰리게 되면, 대기 시간 지연으로 인해 서비스가 망가질 위험이 있다.
Observer 패턴과 Pub-Sub 패턴의 차이점

1. Observer 패턴은 Observer와 Subject 간 서로를 알고 있으나, Pub-Sub 패턴은 서로를 몰라도 상관없다.
Pub-Sub 패턴은 발행 객체와 구독 객체 사이에 직접적인 소통을 하지 않아도 된다.
소통의 역할을 브로커라고 하는 제 3의 구성요소가 수행하기 때문이다.
브로커는 옵저버와 옵저버블 사이에서 메시지를 필터링하여 다시 배포하는 역할을 한다.
그러므로 옵저버와 옵저버블은 서로를 알지 못해도 브로커에 의해 소통 할 수 있다.
2. Pub-Sub 패턴이 Observer 패턴보다 더 낮은 결합도를 가진다.
3. Observer 패턴은 주로 동기적으로 동작하며, Pub-Sub 패턴은 비동기적으로 동작한다.
4. Observer 패턴은 단일 도메인에서 동작해야 하며 Pub-Sub 패턴은 크로스 도메인 상황에서도 동작이 가능하다.
크로스 도메인이란, 네이버나 카카오 같은 서로 다른 도메인을 의미한다.
또는 API 서버, 파일 서버, 소켓 서버 등 동일한 사이트에서 다른 목적으로 서버를 나눈 경우에도 크로스 도메인에 속한다.
발행구독 패턴에서 pub, sub은 서로를 몰라도 된다는 점을 이용하는 것이다.
pub, sub이 각각 브로커와 소통할 수 있다면, 앱의 도메인이 다르더라도 처리가 가능하기 때문이다.
'Kotlin' 카테고리의 다른 글
| 파일데이터 시스템 (0) | 2024.08.05 |
|---|---|
| 비동기 처리 (0) | 2024.07.29 |
| 객체 지향 프로그래밍(OOP)에 대하여 (0) | 2024.07.28 |
| 함수형 프로그래밍과 Sealed Class (2) | 2024.07.25 |
| 예외 처리란? (0) | 2024.07.15 |