
소개
Kotlin은 간결성, 안전성, 상호운용성(interoperability)을 목표로 설계된 언어입니다. 이 글에서는 시간에 크게 변하지 않는 핵심 개념들을 정리하고, 설계 선택에서 발생하는 트레이드오프를 설명한 뒤, 실무에서 자주 쓰이는 코루틴 기반 동시성 패턴의 예제를 통해 적용 방법을 보여줍니다.
핵심 개념 요약
아래 항목들은 Kotlin을 이해할 때 반드시 알고 있어야 하는 안정적인 개념들입니다.
널(null) 안전성
타입 시스템에 명시적으로 nullable 타입(예: String?)을 도입하여 널 참조를 컴파일 타임에 검출합니다. 안전 호출(?.), 엘비스 연산자(?:), !! 연산자 등을 통해 런타임 널 NPE를 줄입니다.
불변성과 가변성
val(읽기 전용)과 var(가변)를 명시적으로 구분합니다. 컬렉션도 읽기 전용 인터페이스와 가변 구현을 구분해 설계의 명확성을 높입니다.
표현적 타입 시스템
데이터 클래스, sealed 클래스/인터페이스, inline/value classes(원시값 래핑) 등으로 도메인 모델을 표현하기 쉽습니다. 스마트 캐스트와 when 분기에서의 exhaustive 체크는 안전성을 더해줍니다.
코루틴(비동기/동시성)
Kotlin의 코루틴은 경량 스레드와 같은 비동기 작업을 구조적 동시성(structured concurrency) 원칙으로 처리합니다. suspend 함수, Flow(reactive 스트림), Dispatchers, Job/Scope 개념이 핵심입니다.
멀티플랫폼(Kotlin Multiplatform)
Kotlin Multiplatform은 공통 로직을 여러 플랫폼(JVM, JS, Native, iOS 등)에서 공유할 수 있게 합니다. 플랫폼 간 API 차이와 런타임 제약을 고려해야 합니다.
설계 선택의 트레이드오프
언어 선택과 Kotlin 내부의 기능 사용에는 늘 장단점이 존재합니다. 주요 트레이드오프를 정리합니다.
Java와의 상호운용성 vs 언어적 표현력
Kotlin은 Java와 높은 호환성을 제공합니다. 기존 Java 코드를 그대로 활용할 수 있지만, 완벽한 상호운용성 유지를 위해 nullability 주석, SAM 변환, JVM 시그니처 차이 등을 신경 써야 합니다. Kotlin 고유 기능(예: inline 클래스, coroutines)을 남용하면 Java와 협업할 때 복잡도가 늘어납니다.
컴파일 속도와 빌드 복잡도
Kotlin은 점점 개선되었지만 대형 프로젝트에서 증분 컴파일이나 KAPT(애노테이션 프로세서) 사용 시 빌드 시간이 문제될 수 있습니다. KSP나 Gradle 설정 최적화가 필요할 수 있습니다.
바이너리 크기 및 런타임 종속성
특히 네이티브나 모바일 앱 환경에서는 라이브러리 종속성과 런타임 코드가 앱 크기에 미치는 영향을 고려해야 합니다. 멀티플랫폼 공유를 위해 공통 라이브러리를 추가하면 초기 오버헤드가 생길 수 있습니다.
코루틴의 편의성 vs 디버깅 난이도
코루틴은 비동기 코드를 동기 스타일로 작성할 수 있게 해 생산성을 올려줍니다. 반면 스택 트레이스가 복잡해지거나 디버깅이 까다로울 수 있고, 적절한 Dispatchers 및 구조적 동시성 설계가 없으면 리소스 누수(취소되지 않는 작업 등)가 발생할 수 있습니다.
실전 예제: 안전한 동시성으로 외부 API 병렬 호출하기
다음 예제는 코루틴을 사용해 여러 외부 API를 병렬로 호출하고, 타임아웃/취소/오류 처리 정책을 적용하는 패턴을 보여줍니다. 이 코드는 플랫폼에 독립적인 비즈니스 로직 형태로 작성되어 JVM과 멀티플랫폼 환경 모두에서 아이디어를 적용할 수 있습니다.
// Gradle (Kotlin DSL)에서 코루틴 의존성을 추가할 때 예시
// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:<version>")
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// suspend 함수로 외부 호출을 추상화
suspend fun fetchFromServiceA(id: String): String = withContext(Dispatchers.IO) {
// 네트워크 호출 또는 블로킹 I/O를 시뮬레이트
delay(300) // 실제 코드에서는 HTTP 클라이언트 호출
"A-result-for-$id"
}
suspend fun fetchFromServiceB(id: String): String = withContext(Dispatchers.IO) {
delay(500)
"B-result-for-$id"
}
// 병렬로 여러 서비스를 호출하고 결합하는 함수
suspend fun fetchCombined(id: String): Result = try {
// 구조적 동시성: coroutineScope 안에서 launch/async로 파생된 작업은 scope가 끝날 때까지 취소/대기된다
coroutineScope {
val a = async { fetchFromServiceA(id) }
val b = async { fetchFromServiceB(id) }
// 전체 작업에 타임아웃을 적용
withTimeout(1000) {
val combined = "${a.await()} | ${b.await()}"
Result.success(combined)
}
}
} catch (e: TimeoutCancellationException) {
Result.failure(Exception("timeout"))
} catch (e: CancellationException) {
// 상위 취소가 들어온 경우라면 그대로 재전파
throw e
} catch (e: Exception) {
Result.failure(e)
}
// Flow를 사용해 여러 ID를 처리하고 결과를 스트리밍
fun fetchFlow(ids: List): Flow>> = flow {
for (id in ids) {
emit(id to fetchCombined(id))
}
}.flowOn(Dispatchers.Default)
// 사용 예시 (JVM의 main 함수 등에서)
suspend fun runExample() = coroutineScope {
val ids = listOf("1", "2", "3")
fetchFlow(ids)
.buffer() // 생산자/소비자 속도 차이를 완화
.collect { (id, result) ->
when {
result.isSuccess -> println("$id -> success: ${result.getOrNull()}")
else -> println("$id -> error: ${result.exceptionOrNull()}")
}
}
}
fun main() {
runBlocking {
// SupervisorScope를 사용하면 일부 작업 실패가 다른 작업에 영향을 주지 않게 할 수 있다
supervisorScope {
launch { runExample() }
}
}
}
이 예제에서 주목할 점:
- coroutineScope/withTimeout/withContext를 조합해 명확한 취소와 타임아웃 정책을 세운다.
- async/await를 사용하면 병렬 호출 결과를 결합할 수 있지만, 실패 정책을 설계(예: supervisorScope)하지 않으면 한 작업 실패가 다른 작업을 취소할 수 있다.
- Flow는 스트리밍 처리와 backpressure(간단한 형태)를 처리하는 데 유용하다. buffer 등 연산으로 성능 특성을 튜닝할 수 있다.
- Dispatchers 관리는 플랫폼 특성(예: Android의 Main dispatcher, KMP에서의 제한)에 민감하다. 멀티플랫폼 공유 코드에서는 'Dispatchers' 접근을 추상화하거나 플랫폼별 actual 구현을 제공하는 방식을 고려해야 한다.
마무리: 언제 Kotlin이 적합한가?
Kotlin은 표현력과 생산성을 중요시하는 프로젝트, Java 생태계와의 높은 상호운용성이 필요한 경우, 또는 비즈니스 로직을 여러 플랫폼에서 재사용하려는 멀티플랫폼 시나리오에 적합합니다. 반면 빌드/런타임 오버헤드, 디버깅 난이도, 플랫폼별 제약 등은 도입 전 고려해야 할 현실적인 트레이드오프입니다. 코루틴 같은 강력한 도구는 올바른 설계(구조적 동시성, 명확한 취소/에러 정책)와 함께 사용될 때 큰 가치를 발휘합니다.
더 깊은 주제(컴파일러 최적화, KSP/KAPT 차이, Compose 활용 등)는 별도의 글로 다루기 좋습니다. 위 개념과 예제는 안정적 기반을 제공하므로 실제 코드에 적용해 보시기 바랍니다.