Backend
[코틀린 마스터하기] 4편: 코틀린 고급 - 코루틴으로 비동기 마스터
관리자
6일 전
12600
#백엔드#Kotlin#Coroutines#Async#Flow#WebFlux
[코틀린 마스터하기] 4편: 코틀린 고급 - 코루틴으로 비동기 마스터
📚 이 글은 "코틀린 마스터하기" 시리즈의 마지막 편입니다.
- 1편: 자바 개발자가 코틀린 30분만에 정복하기
- 2편: 코틀린의 꽃 - 이것만 알면 생산성 2배!
- 3편: 실전 코틀린 - Spring Boot와 함께!
- 4편: 코틀린 고급 - 코루틴으로 비동기 마스터 (현재 글)
🎯 한 줄 요약
코틀린의 킬러 기능! 코루틴으로 비동기 프로그래밍을 동기처럼 쉽게!
🤔 이런 경험 있으신가요?
- "Callback Hell에서 벗어나고 싶어요"
- "CompletableFuture 너무 복잡해요"
- "비동기 코드 디버깅이 지옥같아요"
- "동시에 여러 API 호출하는데 코드가 난장판이에요"
코루틴이 이 모든 걸 해결해줍니다! 🚀
💡 코루틴? 그냥 가벼운 스레드입니다!
"스레드 1개에 코루틴 100,000개 실행 가능!"
자바의 스레드는 생성 비용이 큽니다 (1MB+ 메모리).
하지만 코루틴은? 단 몇 KB!
🚀 1. 코루틴 시작하기 - 너무 쉬워!
기본 설정 (build.gradle.kts)
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.0") // Spring WebFlux용
}
첫 번째 코루틴
자바의 CompletableFuture 방식:
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(1000);
return "Hello";
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
future.thenAccept(result -> System.out.println(result));
코틀린 코루틴 방식:
fun main() = runBlocking {
val result = async {
delay(1000) // Thread.sleep 대신!
"Hello"
}
println(result.await())
}
10배는 간단하죠? 😎
💎 2. async/await - 병렬 처리의 정석!
동시에 여러 API 호출하기
class UserService {
suspend fun getUserDetails(userId: String): UserDetails {
// 3개 API를 동시에 호출!
val profileDeferred = async { fetchProfile(userId) }
val postsDeferred = async { fetchPosts(userId) }
val friendsDeferred = async { fetchFriends(userId) }
// 모두 기다리기
return UserDetails(
profile = profileDeferred.await(),
posts = postsDeferred.await(),
friends = friendsDeferred.await()
)
}
// 각 API 호출은 suspend 함수
private suspend fun fetchProfile(userId: String): Profile {
delay(1000) // 네트워크 지연 시뮬레이션
return Profile(name = "홍길동", age = 25)
}
private suspend fun fetchPosts(userId: String): List<Post> {
delay(1500)
return listOf(Post("첫 게시글"), Post("두 번째 게시글"))
}
private suspend fun fetchFriends(userId: String): List<Friend> {
delay(800)
return listOf(Friend("김철수"), Friend("이영희"))
}
}
순차 실행이면 3.3초, 병렬이면 1.5초! ⚡
에러 처리도 깔끔하게
suspend fun fetchDataWithErrorHandling(): Result<Data> {
return try {
supervisorScope {
val data1 = async { api1() }
val data2 = async { api2() }
val data3 = async { api3() }
// 하나가 실패해도 나머지는 계속!
val result1 = data1.awaitOrNull()
val result2 = data2.awaitOrNull()
val result3 = data3.awaitOrNull()
Result.success(
Data(result1, result2, result3)
)
}
} catch (e: Exception) {
Result.failure(e)
}
}
// 확장 함수로 편리하게
suspend fun <T> Deferred<T>.awaitOrNull(): T? = try {
await()
} catch (e: Exception) {
null
}
🌊 3. Flow - 리액티브 스트림의 진화!
RxJava vs Flow
RxJava 방식:
Observable.interval(1, TimeUnit.SECONDS)
.take(5)
.map(i -> "Item " + i)
.subscribe(System.out::println);
Flow 방식:
fun countdownFlow() = flow {
for (i in 5 downTo 1) {
emit("카운트다운: $i")
delay(1000)
}
emit("🚀 발사!")
}
// 사용
fun main() = runBlocking {
countdownFlow().collect { value ->
println(value)
}
}
실시간 데이터 스트림
class StockPriceService {
fun getStockPrices(symbol: String): Flow<StockPrice> = flow {
while (currentCoroutineContext().isActive) {
val price = fetchLatestPrice(symbol)
emit(price)
delay(1000) // 1초마다 갱신
}
}.flowOn(Dispatchers.IO)
// 변환과 필터링
fun getHighPrices(symbol: String): Flow<StockPrice> =
getStockPrices(symbol)
.filter { it.price > 100000 }
.map { it.copy(alert = "고가 알림!") }
}
StateFlow와 SharedFlow - UI 상태 관리
class ViewModel {
// 상태 관리 (항상 최신 값 유지)
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 이벤트 전달 (일회성)
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = repository.getData()
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
_events.emit(Event.ShowToast("오류 발생!"))
}
}
}
}
📡 4. Channel - 코루틴 간 통신!
Producer-Consumer 패턴
fun CoroutineScope.produceNumbers() = produce<Int> {
var x = 1
while (true) {
send(x++) // 무한히 숫자 생성
delay(100)
}
}
fun CoroutineScope.square(numbers: ReceiveChannel<Int>) = produce<Int> {
for (x in numbers) {
send(x * x) // 제곱 계산
}
}
fun main() = runBlocking {
val numbers = produceNumbers()
val squares = square(numbers)
// 5개만 출력
repeat(5) {
println(squares.receive())
}
coroutineContext.cancelChildren() // 자원 정리
}
Fan-out / Fan-in 패턴
// Fan-out: 하나의 생산자, 여러 소비자
suspend fun fanOut() {
val producer = produce {
repeat(10) {
send(it)
}
}
// 5개의 워커가 동시에 처리
repeat(5) { workerId ->
launch {
for (msg in producer) {
println("Worker $workerId 처리: $msg")
}
}
}
}
// Fan-in: 여러 생산자, 하나의 소비자
suspend fun fanIn() {
val channel = Channel<String>()
// 여러 소스에서 데이터 수집
launch {
repeat(5) { channel.send("Source1: $it") }
}
launch {
repeat(5) { channel.send("Source2: $it") }
}
// 하나의 소비자가 모두 처리
repeat(10) {
println(channel.receive())
}
}
🚀 5. Spring WebFlux와 코루틴 조합!
Reactive REST API
@RestController
@RequestMapping("/api/reactive")
class ReactiveController(
private val service: ReactiveService
) {
// Flow 반환 - 스트리밍 응답
@GetMapping("/stream")
fun streamData(): Flow<DataEvent> = flow {
repeat(10) {
emit(DataEvent("Event $it"))
delay(1000)
}
}
// suspend 함수 - 단일 응답
@GetMapping("/user/{id}")
suspend fun getUser(@PathVariable id: String): User {
return service.findUser(id)
}
// 병렬 처리
@GetMapping("/dashboard")
suspend fun getDashboard(): Dashboard {
return coroutineScope {
val stats = async { service.getStats() }
val activities = async { service.getActivities() }
val notifications = async { service.getNotifications() }
Dashboard(
stats = stats.await(),
activities = activities.await(),
notifications = notifications.await()
)
}
}
}
WebClient와 코루틴
@Service
class ApiClient {
private val webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.build()
suspend fun fetchData(id: String): ApiResponse {
return webClient
.get()
.uri("/data/{id}", id)
.retrieve()
.awaitBody() // suspend 함수로 변환!
}
// 여러 API 동시 호출
suspend fun aggregateData(ids: List<String>): List<ApiResponse> {
return ids.map { id ->
async {
try {
fetchData(id)
} catch (e: Exception) {
ApiResponse.empty() // 실패 시 기본값
}
}
}.awaitAll() // 모두 완료 대기
}
}
⚡ 6. 실전 최적화 팁!
Dispatcher 선택 가이드
class OptimizedService {
// CPU 집약적 작업
suspend fun processData(data: List<Item>) = withContext(Dispatchers.Default) {
data.map { complexCalculation(it) }
}
// I/O 작업
suspend fun readFile(path: String) = withContext(Dispatchers.IO) {
File(path).readText()
}
// UI 업데이트 (Android)
suspend fun updateUI(text: String) = withContext(Dispatchers.Main) {
textView.text = text
}
}
성능 최적화 패턴
// 1. Flow 캐싱
val expensiveFlow = flow {
// 비싼 연산
emit(calculateExpensive())
}.shareIn(
scope = GlobalScope,
started = SharingStarted.Lazily,
replay = 1
)
// 2. 타임아웃 설정
suspend fun fetchWithTimeout(): String? {
return withTimeoutOrNull(5000) { // 5초 타임아웃
slowApiCall()
}
}
// 3. 재시도 로직
suspend fun <T> retryWithBackoff(
times: Int = 3,
initialDelay: Long = 100,
block: suspend () -> T
): T {
repeat(times - 1) { attempt ->
try {
return block()
} catch (e: Exception) {
delay(initialDelay * (attempt + 1))
}
}
return block() // 마지막 시도
}
🎯 7. 테스트 작성하기
class CoroutineTest {
@Test
fun `코루틴 테스트`() = runTest {
// runTest는 delay를 스킵!
val service = UserService()
val result = service.fetchUser("123")
assertEquals("홍길동", result.name)
}
@Test
fun `Flow 테스트`() = runTest {
val flow = flowOf(1, 2, 3)
.map { it * 2 }
.filter { it > 2 }
val results = flow.toList()
assertEquals(listOf(4, 6), results)
}
}
📊 실제 성능 비교
작업 | 스레드 (Java) | 코루틴 (Kotlin) |
---|---|---|
10,000개 동시 실행 | 10GB 메모리 | 50MB 메모리 |
컨텍스트 스위칭 | 느림 (μs) | 빠름 (ns) |
생성 비용 | 높음 | 거의 없음 |
취소/타임아웃 | 복잡 | 간단 |
💭 마무리
"코루틴 없이 어떻게 코딩했나 싶어요"
이제 여러분도 이런 말을 하게 될 겁니다!
코루틴은 단순히 비동기를 쉽게 만드는 것이 아니라,
코드를 읽기 쉽고, 유지보수하기 쉽게 만들어줍니다.
자바의 CompletableFuture, RxJava를 쓰셨다면,
코루틴의 간결함에 놀라실 거예요!
🎓 시리즈를 마치며
4편에 걸쳐 코틀린의 A to Z를 다뤘습니다:
- 기초: 자바와의 차이점, 기본 문법
- 생산성: 데이터 클래스, 확장 함수, 컬렉션 API
- 실전: Spring Boot 통합, REST API 구축
- 고급: 코루틴으로 비동기 마스터
이제 여러분은 진정한 코틀린 개발자입니다! 🎉
📚 전체 시리즈 다시보기
- 📘 1편: 자바 개발자가 코틀린 30분만에 정복하기
- 📗 2편: 코틀린의 꽃 - 이것만 알면 생산성 2배!
- 📙 3편: 실전 코틀린 - Spring Boot와 함께!
- 📕 4편: 코틀린 고급 - 코루틴으로 비동기 마스터 (현재 글)
시리즈를 끝까지 읽어주셔서 감사합니다! 🙏
코틀린 개발 중 막히는 부분이 있다면 언제든 댓글로 질문해주세요! 💬
Happy Kotlin Coding! 🚀
댓글 0개
아직 댓글이 없습니다
첫 번째 댓글을 작성해보세요!