티스토리 뷰
코루틴의 등장 배경
코루틴(Coroutine)은 비동기 프로그래밍을 쉽게 할 수 있게 도와주는 개념으로, 특히 Kotlin에서 매우 강력하게 활용되는 기능 중 하나이다. 쓰레드를 블로킹하지 않으면서도 동기식 코드처럼 깔끔하게 비동기 로직을 작성할 수 있도록 해준다.
비동기 작업을 동기식 코드처럼 읽기 쉽게 작성하기 위해 등장한 것이다. 이전에는 콜백 지옥(callback hell), Future 등으로 복잡했던 비동기 흐름을 코루틴의 등장으로 간단히 표현 가능해졌다.
코루틴이란?
코루틴은 **중단(suspend)**과 **재개(resume)**가 가능한 비동기 실행 단위이다. 쓰레드가 아니라, 쓰레드 위에서 작동하는 논리적 작업 단위이다. 중단과 재개의 개념이 익숙하지 않을 것이다.
코루틴은 진입하고 빠져나가는 부분이 여러개인 것이다. 루틴 작업에 들어가면 바로 나왔다가 작업(DB I/O 등)이 끝나면 다시 들어가서 이어서 진행한다. 따라서 중단과 재개가 가능한 비동기 실행 단위라는 말은 말 그대로 작업을 중단했다가 다른 일을 인행하고 다시 재개하는 것이 가능하다는 말이다.
중단한다면 작업을 안하는 것일까?? 그렇지는 않다. 정확히는 쓰레드를 더이상 점유하지 않는다는 것이다.
아래 예제를 보자.
suspend fun `코루틴으로 실행`() {
println("함수 실행 직후 쓰레드 = ${Thread.currentThread().name}") // main
withContext(Dispatchers.IO) {
println("코루틴 실행 직후 쓰레드 = ${Thread.currentThread().name}") // worker-1
launch { routineA() }
println("지금은? = ${Thread.currentThread().name}") // worker-1
launch { routineB() }
}
}
suspend fun routineA() {
println("작업 A 진행: 현재 쓰레드 = ${Thread.currentThread().name}") // worker-2
오래 걸리는 외부 API 호출()
println("작업 A 완료: 현재 쓰레드 = ${Thread.currentThread().name}")
}
suspend fun routineB() {
println("작업 B 진행: 현재 쓰레드 = ${Thread.currentThread().name}") // worker-3
오래 걸리는 외부 API 호출()
println("작업 B 완료: 현재 쓰레드 = ${Thread.currentThread().name}")
}
함수 실행 직후 쓰레드 = main
코루틴 실행 직후 쓰레드 = DefaultDispatcher-worker-1
지금은? = DefaultDispatcher-worker-1
작업 A 진행: 현재 쓰레드 = DefaultDispatcher-worker-2
작업 B 진행: 현재 쓰레드 = DefaultDispatcher-worker-3
작업 B 완료: 현재 쓰레드 = DefaultDispatcher-worker-2
작업 A 완료: 현재 쓰레드 = DefaultDispatcher-worker-3
함수가 처음 실행될 때는 애플리케이션 main 쓰레드에서 진행되고 있다. 그리고 withContext(Dispatchers.IO)를 호출하면, 해당 블록 전체가 Dispatchers.IO 컨텍스트에서 실행된다. 이제 스코프 내에서는 main이 아닌 Dispatchers.IO로 부터 받은 쓰레드인 DefaultDispatcher-worker-1 이 진행하고 있는 것을 확인할 수 있다.
launch는 컨텍스트(Dispatchers.IO)를 계승한 새 코루틴을 생성하며, 스레드 풀에서 가용한 워커 스레드가 작업을 실행하게 된다.
routineA와 routineB는 각기 스레드 풀에서 사용 가능한 워커 스레드를 배정받아 작업을 수행하게 된다. 그러다가 외부 API를 호출하게 되면 suspend가 일어나게 되고 코루틴은 해당 작업이 오래 걸리니 쓰레드를 반납하게 된다. 즉, 중단(suspend)이 일어나는 것이다.
그러다 외부 API 작업이 끝나면 다시 재개(resume)이 되는데 이때 이전에 사용했던 쓰레드는 반납했기 때문에 Dispatchers.IO로부터 다시 쓰레드를 할당받아 기존과 다른 쓰레드로 작업이 진행되는 것을 볼 수 있다.
코루틴의 핵심 개념은 쓰레드를 점유하지 않는다는 것이다.
애플리케이션이 진행되는 도중에 DB 조회 작업이나 외부 API 호출 같이 작업이 오래 걸리는 경우가 발생한다. 이때 애플리케이션 쓰레드가 해당 작업을 기다리지 않고 다른 작업을 진행할 수 있도록 해주는 것이 코루틴이다.
DB 조회 작업을 예시로 설명하면 DB I/O 작업 자체는 JVM 쓰레드가 처리하지 않는다. JVM은 TCP 소켓을 열고 요청을 던진다. 그리고 그 이후에는 OS 커널, DB 서버가 그 요청을 처리하게 된다. 이 동안 우리 JVM은 그냥 기다리게 된다. 기다리는 동안은 아무 코드도 실행하지 않는다. 마냥 기다리는 것보다는 다른 작업을 처리하고 있다가 요청에 응답이 오면 그때 다시 작업을 진행하는 것이 효율적일 것이다 그리고 이걸 가능하게 해주는 것이 코루틴이다.
코루틴은 병렬 처리를 보장하거나 강제하지 않는다
비동기 작업을 처리하기 위해 코루틴을 접하다 보니 자연스럽게 코루틴을 사용하게 되면 비동기 작업을 빠르게 수행할 수 있다고 생각하게 된다. 그러면서 코루틴을 사용하게 되면 무조건 비동기 작업을 병렬처리하게 된다고 오해하기 쉽다. 하지만 이는 오해이다. 코루틴 자체가 병렬성을 “목적으로 만들어지진 않았지만”, 상황에 따라 병렬 실행이 가능한 것이다.
우선 병렬 처리가 무엇인지 병행 개념과 구분해서 이해해보자.
개념 정의 예시
병행 (Concurrency) | 작업 여러 개가 동시에 실행되는 것처럼 보이지만, 실제로는 시간을 나눠 번갈아 실행됨 | 하나의 주방에서 요리사 한 명이 A요리를 하다가 B요리를 하다가 왔다 갔다 하며 함 |
병렬 (Parallelism) | 작업 여러 개가 물리적으로 동시에 실행됨, 즉 여러 스레드에서 동시에 돌아감 | 요리사 A는 1번 주방에서 A 요리를, 요리사 B는 2번 주방에서 B 요리를 동시에 함 |
쓰레드 개념에 대해서 학습할 때 자연스럽게 학습하게 되는 컨텍스트 스위칭은 병행 개념인 것이다.
코루틴은 병렬 처리될지, 병행 처리될지는 코루틴이 실행되는 컨텍스트나 내부 로직에 따라 달라진다. 따라서 코루틴 = 병렬처리는 오해이다.
[병행 처리]
fun main() = runBlocking {
measureTimeMillis {
**coroutineScope** {
async { routineA() }
async { routineB() }
}
}
}
suspend fun routineA() {
println("작업 A 진행: 현재 쓰레드 = ${Thread.currentThread().name}")
delay(1000L)
println("작업 A 완료: 현재 쓰레드 = ${Thread.currentThread().name}")
}
suspend fun routineB() {
println("작업 B 진행: 현재 쓰레드 = ${Thread.currentThread().name}")
delay(1000L)
println("작업 B 완료: 현재 쓰레드 = ${Thread.currentThread().name}")
}
작업 A 진행: 현재 쓰레드 = main
작업 B 진행: 현재 쓰레드 = main
작업 A 완료: 현재 쓰레드 = main
작업 B 완료: 현재 쓰레드 = main
corutineScope를 통해 현재 컨텍스트를 가져다가 코루틴을 실행했다. 두 작업 모두 main으로 진행된 것을 볼 수 있다. 즉, 하나의 쓰레드로 컨텍스트 스위칭을 하면서 병행 작업 처리한 것이다.
[병렬 처리]
fun main() = runBlocking {
measureTimeMillis {
**withContext(Dispatchers.IO)** {
async { routineA() }
async { routineB() }
}
}
}
suspend fun routineA() {
println("작업 A 진행: 현재 쓰레드 = ${Thread.currentThread().name}")
delay(1000L)
println("작업 A 완료: 현재 쓰레드 = ${Thread.currentThread().name}")
}
suspend fun routineB() {
println("작업 B 진행: 현재 쓰레드 = ${Thread.currentThread().name}")
delay(1000L)
println("작업 B 완료: 현재 쓰레드 = ${Thread.currentThread().name}")
}
작업 B 진행: 현재 쓰레드 = DefaultDispatcher-worker-1
작업 A 진행: 현재 쓰레드 = DefaultDispatcher-worker-3
작업 B 완료: 현재 쓰레드 = DefaultDispatcher-worker-3
작업 A 완료: 현재 쓰레드 = DefaultDispatcher-worker-1
이번에는 withContext로 Dispatchers.IO 컨텍스트로 코루틴 작업을 수행한다. 이때 코루틴 작업은 Dispatchers.IO로 부터 쓰레드를 받아 진행하기에 서로 다른 쓰레드에서 작업이 수행되며 병렬처리된 것을 볼 수 있다.
⚠️ 사실 위 작업은 병렬 처리를 보장하지 않는다.
Dispatchers.IO 컨텍스트 안에서 async {} 두 개를 실행하면, 같은 IO 디스패처 하에서 실행되므로 병렬 실행될 수 있다. 다만 Dispatcher가 상황에 따라 동일한 워커 스레드를 재사용할 수도 있기 때문에 병렬은 보장되지 않는다.
다만 이해를 돕고자 위와 같은 예시를 들었던 것이다.
만약 병렬 처리를 보장하려면 다음과 같이 호출해야 한다.
fun main() = runBlocking {
measureTimeMillis {
withContext(Dispatchers.IO) ****{
async**(Dispatchers.IO)** { routineA() }
async**(Dispatchers.IO)** { routineB() }
}
}
}
coroutineScope vs withContext
항목 | coroutineScope {} | withContext(Dispatcher) {} |
목적 | 코루틴 묶음 실행 & 구조화 | 컨텍스트(스레드/디스패처) 전환 |
컨텍스트 변경 | ❌ 현재 컨텍스트 그대로 사용 | ✅ 전달된 컨텍스트로 변경 |
새로운 코루틴 생성 | ❌ 아님. 현재 코루틴 안에서 블록 실행 | ❌ 아님. 코루틴 전환만 함 |
블록 내 suspend 지원 | ✅ 가능 | ✅ 가능 |
예외 전파 | 부모 코루틴으로 전파 | 부모 코루틴으로 전파 |
사용 예 | 여러 코루틴을 launch로 병렬 실행하고 기다릴 때 | 특정 작업을 IO/CPU 작업으로 전환하고 싶을 때 |
'개발 스토리' 카테고리의 다른 글
레디스 분산 락을 활용한 멀티 서버 환경 푸시 알림 중복 문제 해결 (2/ 2) (0) | 2025.04.11 |
---|---|
레디스 분산 락을 활용한 멀티 서버 환경 푸시 알림 중복 문제 해결 (1 / 2) (2) | 2025.04.10 |
토큰 Blacklist를 Redis로 관리해도 될까? (0) | 2025.03.28 |
Swap memory 설정법 및 장단점 (4) | 2025.03.18 |
MySQL 원격 연결 에러 및 해결 (feat. GCP 인바운드) (0) | 2025.03.09 |
- Total
- Today
- Yesterday
- 자바
- 토큰 블랙리스트
- 알고리즘
- JWT
- 6기
- setnx
- 우아한테크코스
- 우아한테크코스 자소서
- 스왑 메모리 설정
- 우테코 프리코스
- 스프링 api 테스트
- 코루틴
- 게임개발
- 레디스 분산락
- 우테코 6기
- redis 메모리 사용량
- 우아한테크코스 후기
- 스왑 메모리 장단점
- 레디스
- 우테코 준비
- 파이썬
- gcp 인바운드
- 토큰
- contextwith
- redis
- 우아한테크코스 6기
- Assertions
- 우테코
- sh 문법 오류
- 환경변수 관리
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |