
소개
Kotlin은 JVM 기반 언어로 출발했지만, 현재는 멀티플랫폼 지원과 현대적인 언어 특성을 갖춘 범용 언어입니다. 이 글에서는 안정적으로 적용 가능한 개념과 실무 관점의 장단점, 그리고 실제로 바로 사용할 수 있는 코루틴 기반 예제를 다룹니다.
핵심 개념 요약
아래는 Kotlin을 이해할 때 반드시 알고 있어야 할 핵심 개념들입니다.
널 안전성 (Null-safety) — 타입 시스템 차원에서 nullable(!) 타입과 non-null 타입을 구분합니다. 컴파일 타임에 널 가능성을 추적해 NPE를 줄입니다.
데이터 클래스와 불변성 — data class는 값 객체를 쉽게 정의하게 해주며, 불변성을 권장하는 디자인과 잘 어울립니다.
확장 함수 / 프로퍼티 — 기존 타입에 함수를 추가할 수 있어, API 설계와 도메인 표현력이 좋아집니다. 단 확장 함수는 정적 호출에 가깝다는 점을 이해해야 합니다.
봉인(Sealed) 타입 / when 검사 — 계층화된 타입 표현과 exhaustiveness 체크로 안전한 분기 처리를 도와줍니다.
코루틴 (Coroutines) — 비동기/동시성 모델로, 경량 스레드처럼 동작합니다. structured concurrency(코루틴 스코프 기반 생명주기 관리)를 중심으로 설계하는 것이 권장됩니다.
멀티플랫폼 — Kotlin Multiplatform(KMP)은 코드 공유를 목표로 합니다. 플랫폼별 API는 expect/actual 패턴 등으로 분리합니다.
실무 트레이드오프
Kotlin을 도입할 때 고려할 주요 장단점과 트레이드오프입니다.
장점
- 짧고 표현력 높은 문법으로 생산성이 향상됩니다.
- Null-safety와 타입 시스템으로 런타임 오류를 줄일 수 있습니다.
- 코루틴을 통한 비동기 모델은 복잡한 비동기 로직을 간결하게 만듭니다.
- Java와 상호운용성이 좋아 기존 JVM 코드와 혼용 가능.
단점 / 고려사항
- 런타임 라이브러리(특히 코루틴, 표준 라이브러리 확장)로 인해 바이너리 크기가 커질 수 있습니다. 임베디드나 제약이 있는 환경에서는 주의가 필요합니다.
- Java와의 완벽한 1:1 매핑은 아니므로(예: 널성, SAM 변환, 람다 캡처 방식 등) 상호작용 시 주의가 필요합니다.
- 플랫폼별 구현(특히 Native나 JS)은 JVM과 다른 성능/동작 특성을 가질 수 있어 테스트가 필요합니다.
- 코루틴의 잘못된 사용(전역 스코프 남용, 취소 미처리 등)은 리소스 누수로 이어질 수 있습니다.
실용 예제: 코루틴 + Flow로 비동기 데이터 파이프라인 만들기
아래 예제는 kotlinx.coroutines와 Flow를 이용해 비동기적으로 데이터를 생산하고, 변환하고, 소비하는 간단한 파이프라인입니다. 구조화된 동시성과 취소/예외 처리를 보여줍니다.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// suspend 함수(외부 I/O를 시뮬레이션)
suspend fun fetchData(id: Int): String {
delay(100L) // 실제 환경에서는 네트워크/디스크 호출
return "item-$id"
}
// Flow 생성: producer는 suspend 함수를 안전하게 호출할 수 있다.
fun produceFlow(): Flow = flow {
for (i in 1..5) {
emit(fetchData(i))
}
}.flowOn(Dispatchers.IO) // I/O 바운드 작업을 분리
fun main() = runBlocking {
// structured concurrency: this runBlocking 범위 안에서 파이프라인 실행
val job = launch {
produceFlow()
.map { it.uppercase() } // 변환 단계
.buffer() // 생산/소비 decoupling
.catch { e -> // 예외 처리
println("stream error: ${'$'}{e.message}")
}
.collect { value -> // 소비 단계
println("received: ${'$'}value")
// 간단한 취소 조건 (예시)
if (value.contains("3")) cancel("stop at 3")
}
}
job.join()
println("pipeline finished")
}
핵심 포인트:
- flowOn으로 생산자(emit) 쪽 스레드를 분리해 I/O 작업을 블로킹하지 않도록 합니다.
- buffer()는 생산자와 소비자를 디커플링해 처리량을 높이지만, 메모리 사용량이 늘어날 수 있으므로 적절한 크기를 설정해야 합니다.
- catch는 upstream 예외만 잡습니다. collect 내부에서 발생하는 예외는 별도 처리가 필요합니다.
- 취소는 cooperative합니다. suspend 지점에서만 즉시 반영되므로, 긴 계산에서는 주기적으로 취소 가능 지점을 두어야 합니다.
실무 권장사항
간단한 체크리스트:
- 코루틴을 사용하면 structured concurrency 원칙(스코프 단위로 생명주기 관리)을 따르세요.
- 플랫폼 간 공유 코드를 작성할 때는 expect/actual과 플랫폼별 API 차이를 명확히 하세요.
- Java와 상호운용할 때는 널성 차이를 꼼꼼히 주석/어노테이션으로 문서화하세요(@JvmOverloads, @JvmStatic 등 필요 시 사용).
- 성능/바이너리 크기가 중요하다면 필요 라이브러리만 포함하고, ProGuard/R8 같은 도구로 최적화하세요.
맺음말
Kotlin은 표현력과 안전성을 제공하는 현대적인 언어입니다. 그러나 도입 시에는 런타임 특성(라이브러리 종속성, 바이너리 크기), 플랫폼별 차이, 코루틴 사용 규범에 대한 합의가 필요합니다. 위 예제와 권장사항을 바탕으로 안전하고 유지보수 가능한 Kotlin 코드를 작성해 보세요.