KOTLIN · 심층 가이드
Kotlin 성능 완전 정리
value class와 inline으로 할당을 줄이고, Sequence·Dispatcher 선택을 근거 있게 판단한 뒤 JMH로 확인하는 성능 작업 13개 주제입니다.
Kotlin의 간결한 문법에는 대부분 대응하는 바이트코드 비용이 있습니다. 인라인되지 않은 람다는 객체 하나를 만들고, 제네릭과 널 가능 타입은 원시 값을 박싱하며, 플랫폼 타입 경계에는 널 검사 호출이 삽입됩니다. 그렇다고 이걸 전부 피해 다니면 코드만 나빠집니다. 실제로 문제가 되는 건 뜨거운 루프와 대량 할당 지점뿐이라, 성능 작업은 언제나 어디가 뜨거운지 측정으로 확인하는 데서 시작합니다.
value class (인라인 클래스)는 타입 안전을 유지하면서 래퍼 할당을 없애는 가장 값싼 개선이라 먼저 볼 만합니다. 이어서 인라인 함수 성능 (Inline Functions)으로 고차 함수의 비용 구조를 확인하고, 시퀀스 vs 리스트 성능에서 지연 평가가 이득이 되는 조건을 데이터 크기와 체인 길이로 정리하세요. 추측을 끊는 단계가 벤치마크 (JMH)이고, 컴파일 단계의 변화는 K2 컴파일러 변경사항에서 따로 다룹니다.
value class가 항상 인라인되는 것은 아닙니다. 널 가능 타입으로 쓰거나, 제네릭 타입 인자로 넘기거나, 인터페이스 타입으로 취급하는 순간 실제 객체로 박싱됩니다. 최적화를 의도했다면 그 값이 컬렉션이나 널 가능 자리로 흘러가지 않는지 확인해야 합니다. 측정 쪽 함정도 같습니다. System.nanoTime()으로 루프를 감싸는 방식은 JIT 워밍업과 죽은 코드 제거 때문에 실제와 다른 숫자를 냅니다. JVM에서 유효한 비교를 하려면 JMH처럼 이를 방어하는 도구가 필요합니다.
01value class (인라인 클래스)
런타임 오버헤드 없이 타입 안전한 래퍼를 만드는 value class.
Kotlin code
// 문제: 타입 혼동 (UserId와 ProductId가 둘 다 Int)
fun findOrder(userId: Int, productId: Int) { /* ... */ }
// findOrder(productId, userId) // 컴파일은 되지만 버그!
// 해결: value class — 컴파일 시 Int로 언박싱됨 (오버헤드 없음)
@JvmInline
value class UserId(val value: Int)
@JvmInline
value class ProductId(val value: Int)
@JvmInline
value class Email(val value: String) {
init {
require(value.contains("@")) { "이메일 형식 오류" }
}
}
fun findOrder(userId: UserId, productId: ProductId) {
println("주문 조회: user=${userId.value}, product=${productId.value}")
}
fun main() {
findOrder(UserId(1), ProductId(42))
// findOrder(ProductId(42), UserId(1)) // ❌ 컴파일 에러!
val email = Email("dev@kotlin.io")
println(email.value)
// runningFold — 중간 합계 등 누적 연산
val cumSum = listOf(1, 2, 3, 4, 5)
.runningFold(0) { acc, n -> acc + n }
println(cumSum) // [0, 1, 3, 6, 10, 15]
}@JvmInline은 Kotlin 1.5+에서 필수입니다. value class는 프로퍼티 하나만 가질 수 있고, 인터페이스 구현은 가능합니다.
value class를 nullable로 사용하거나(UserId?), 컬렉션에 담거나, as 캐스팅하면 박싱이 발생해 성능 이점이 사라집니다.
02Kotlin Coroutine 성능 — Dispatcher 선택
올바른 Dispatcher 선택이 코루틴 성능의 핵심입니다. 작업 유형에 맞는 Dispatcher를 사용하세요.
Kotlin code
<span class="kw">import</span> <span class="pk">kotlinx.coroutines.*</span>
<span class="kw">import</span> <span class="pk">kotlin.system.measureTimeMillis</span>
<span class="kw">suspend fun</span> <span class="fn">cpuTask</span>(n: <span class="ty">Int</span>): <span class="ty">Long</span> =
withContext(Dispatchers.Default) {
(<span class="num">1</span>..<span class="num">1_000_000</span>).fold(<span class="num">0L</span>) { acc, i -> acc + i * n }
}
<span class="kw">suspend fun</span> <span class="fn">ioTask</span>(): <span class="ty">String</span> =
withContext(Dispatchers.IO) {
delay(<span class="num">10</span>) <span class="cm">// I/O 시뮬레이션</span>
<span class="str">"IO 완료"</span>
}
<span class="kw">fun</span> <span class="fn">main</span>() = runBlocking {
<span class="cm">// CPU: Default (코어 수만큼 스레드)</span>
<span class="kw">val</span> t1 = measureTimeMillis {
(1..8).map { async { cpuTask(it) } }.awaitAll()
}
<span class="fn">println</span>(<span class="str">"CPU 병렬 8개: ${t1}ms"</span>)
<span class="cm">// IO: IO (최대 64 스레드, 블로킹 IO 허용)</span>
<span class="kw">val</span> t2 = measureTimeMillis {
(1..20).map { async { ioTask() } }.awaitAll()
}
<span class="fn">println</span>(<span class="str">"IO 병렬 20개: ${t2}ms"</span>)
<span class="cm">// limitedParallelism — 동시 실행 수 제한</span>
<span class="kw">val</span> limited = Dispatchers.IO.limitedParallelism(<span class="num">4</span>)
withContext(limited) { ioTask() }
}CPU 바운드 → Dispatchers.Default, I/O 바운드 → Dispatchers.IO, UI → Dispatchers.Main. limitedParallelism()으로 외부 API 요청 수를 제한하세요.
I/O 작업을 Dispatchers.Default에서 실행하면 CPU 코어를 I/O 대기로 낭비합니다. 반대로 CPU 작업을 Dispatchers.IO에서 실행하면 스레드 수가 너무 많아집니다.
03K2 컴파일러 변경사항
Kotlin 2.0 K2 컴파일러 성능과 주요 변경점
Kotlin code
<span class="cm">// K2 컴파일러 변경사항 예제
// data/prompts.js의 생성 프롬프트로 상세 코드 생성 가능</span>
fun main() { println("K2 컴파일러 변경사항") }KOTLIN 공식 문서를 함께 참고하세요.
자주 발생하는 실수에 주의하세요.
04인라인 함수 성능 (Inline Functions)
inline 키워드로 고차 함수의 람다 오버헤드를 제거합니다. 객체 할당과 가상 호출을 방지합니다.
Kotlin code
// inline: 호출 지점에 함수 본문 삽입
inline fun <T> measureExecution(label: String, block: () -> T): T {
val start = System.nanoTime()
val result = block()
val elapsed = (System.nanoTime() - start) / 1_000_000.0
println("$label: ${"%.3f".format(elapsed)}ms")
return result
}
// noinline: 특정 람다만 인라인 제외
inline fun transaction(
noinline onError: (Exception) -> Unit = {},
block: () -> Unit
) {
try { block() }
catch (e: Exception) { onError(e) }
}
// crossinline: 비지역 반환 방지
inline fun runSafely(crossinline block: () -> Unit) {
val thread = Thread { block() }
thread.start()
thread.join()
}
fun main() {
val sum = measureExecution("리스트 합계") {
(1..1_000_000).sum()
}
println("결과: $sum")
transaction(onError = { println("오류: ${it.message}") }) {
println("트랜잭션 실행")
}
runSafely { println("안전 실행 완료") }
}표준 라이브러리의 let, apply, also 등은 모두 inline이라 런타임 오버헤드가 없습니다.
큰 함수를 inline으로 선언하면 호출 지점마다 코드가 복사되어 바이트코드가 비대해집니다. 짧은 래퍼 함수에만 사용하세요.
05시퀀스 vs 리스트 성능
시퀀스(지연 평가)와 리스트(즉시 평가)의 성능 차이를 이해합니다. 데이터 크기와 연산 체인에 따른 선택 기준입니다.
Kotlin code
import kotlin.system.measureTimeMillis
fun main() {
val data = (1..5_000_000).toList()
// 리스트: 매 연산마다 중간 컬렉션 생성
val listTime = measureTimeMillis {
data.filter { it % 2 == 0 }
.map { it.toLong() * it }
.take(100)
.sum()
}
// 시퀀스: 원소별 처리, 중간 컬렉션 없음
val seqTime = measureTimeMillis {
data.asSequence()
.filter { it % 2 == 0 }
.map { it.toLong() * it }
.take(100)
.sum()
}
println("리스트: ${listTime}ms")
println("시퀀스: ${seqTime}ms")
println("시퀀스가 ${"%.1f".format(listTime.toDouble() / seqTime)}배 빠름")
// 작은 컬렉션에서는 리스트가 빠를 수 있음
val small = (1..100).toList()
val smallList = measureTimeMillis {
repeat(100000) { small.filter { it > 50 }.map { it * 2 } }
}
val smallSeq = measureTimeMillis {
repeat(100000) { small.asSequence().filter { it > 50 }.map { it * 2 }.toList() }
}
println("
소규모 - 리스트: ${smallList}ms, 시퀀스: ${smallSeq}ms")
}일반 규칙: 원소 수 10,000개 이상이거나 take로 일부만 필요한 경우 시퀀스가 유리합니다.
시퀀스를 sorted()와 함께 사용하면 전체 원소를 수집해야 하므로 지연 평가의 이점이 사라집니다.
06메모리 최적화
객체 할당을 줄이고 메모리 사용을 최적화하는 기법입니다. 원시 타입 배열, 객체 풀, WeakReference를 다룹니다.
Kotlin code
import java.lang.ref.WeakReference
fun main() {
// 1. 원시 타입 배열 (박싱 방지)
val intArray = IntArray(1000) { it } // int[] (박싱 없음)
val intList = List(1000) { it } // List<Integer> (박싱 발생)
println("IntArray sum: ${intArray.sum()}")
// 2. StringBuilder (문자열 연결 최적화)
val slow = buildString { repeat(10000) { append("item ") } } // 효율적
// val slow2 = (1..10000).fold("") { acc, _ -> acc + "item " } // 비효율!
// 3. 객체 재사용 (풀)
class ObjectPool<T>(private val factory: () -> T, poolSize: Int = 10) {
private val available = ArrayDeque<T>()
init { repeat(poolSize) { available.add(factory()) } }
fun acquire(): T = available.removeFirstOrNull() ?: factory()
fun release(obj: T) { available.addLast(obj) }
}
val bufferPool = ObjectPool({ StringBuilder(256) }, 5)
val buf = bufferPool.acquire()
buf.append("재사용 가능!")
println(buf.toString())
buf.clear()
bufferPool.release(buf)
// 4. WeakReference (캐시)
var largeData: ByteArray? = ByteArray(1024 * 1024)
val weakRef = WeakReference(largeData)
largeData = null // 강한 참조 제거
System.gc()
println("WeakRef 유효: ${weakRef.get() != null}")
}IntArray, LongArray 등 원시 타입 배열은 박싱 오버헤드가 없어 대량 수치 연산에 훨씬 빠릅니다.
문자열 연결에 +를 반복 사용하면 매번 새 String 객체가 생성됩니다. buildString이나 StringBuilder를 사용하세요.
07프로파일링 기법
코드의 성능 병목을 찾는 프로파일링 기법입니다. 시간 측정, 메모리 추적, 핫스팟 분석을 다룹니다.
Kotlin code
import kotlin.system.measureTimeMillis
class SimpleProfiler {
private val timings = mutableMapOf<String, MutableList<Long>>()
inline fun <T> profile(name: String, block: () -> T): T {
val start = System.nanoTime()
val result = block()
val elapsed = System.nanoTime() - start
timings.getOrPut(name) { mutableListOf() }.add(elapsed)
return result
}
fun report() {
println("=== 프로파일링 결과 ===")
timings.entries.sortedByDescending { it.value.sum() }.forEach { (name, times) ->
val total = times.sum() / 1_000_000.0
val avg = total / times.size
val max = times.max() / 1_000_000.0
println(" $name: 총=${"%.2f".format(total)}ms, 평균=${"%.3f".format(avg)}ms, 최대=${"%.3f".format(max)}ms, 호출=${times.size}회")
}
}
}
fun main() {
val profiler = SimpleProfiler()
val data = (1..100_000).toList()
repeat(5) {
profiler.profile("filter+map") {
data.filter { it % 3 == 0 }.map { it * 2 }
}
profiler.profile("sequence") {
data.asSequence().filter { it % 3 == 0 }.map { it * 2 }.toList()
}
profiler.profile("manual loop") {
buildList {
for (i in data) if (i % 3 == 0) add(i * 2)
}
}
}
profiler.report()
// JVM 메모리 상태
val rt = Runtime.getRuntime()
println("
메모리: 사용=${(rt.totalMemory() - rt.freeMemory()) / 1024 / 1024}MB, 최대=${rt.maxMemory() / 1024 / 1024}MB")
}IntelliJ의 내장 프로파일러나 VisualVM을 사용하면 CPU/메모리 핫스팟을 시각적으로 분석할 수 있습니다.
프로덕션 코드에 프로파일링 코드를 남겨두면 성능에 영향을 줍니다. AOP나 디버그 빌드 전용으로 분리하세요.
08벤치마크 (JMH)
JMH(Java Microbenchmark Harness)로 정밀한 마이크로벤치마크를 작성합니다. JVM 최적화를 고려한 신뢰할 수 있는 측정입니다.
Kotlin code
// build.gradle.kts:
// plugins { id("me.champeau.jmh") version "0.7.2" }
// 실제 JMH 벤치마크 예시 (개념 코드)
// @State(Scope.Benchmark)
// @BenchmarkMode(Mode.AverageTime)
// @OutputTimeUnit(TimeUnit.NANOSECONDS)
// @Warmup(iterations = 3, time = 1)
// @Measurement(iterations = 5, time = 1)
open class CollectionBenchmark {
private val data = (1..10000).toList()
// @Benchmark
fun listFilterMap(): List<Int> =
data.filter { it % 2 == 0 }.map { it * 2 }
// @Benchmark
fun sequenceFilterMap(): List<Int> =
data.asSequence().filter { it % 2 == 0 }.map { it * 2 }.toList()
// @Benchmark
fun manualLoop(): List<Int> = buildList {
for (i in data) if (i % 2 == 0) add(i * 2)
}
}
// 간이 벤치마크 (JMH 없이)
fun simpleBench(name: String, iterations: Int = 1000, block: () -> Any?) {
// 워밍업
repeat(500) { block() }
// 측정
val start = System.nanoTime()
repeat(iterations) { block() }
val elapsed = (System.nanoTime() - start) / iterations
println("$name: ${elapsed}ns/op (${elapsed/1000}μs/op)")
}
fun main() {
val data = (1..10000).toList()
simpleBench("List chain") { data.filter { it % 2 == 0 }.map { it * 2 } }
simpleBench("Sequence chain") { data.asSequence().filter { it % 2 == 0 }.map { it * 2 }.toList() }
simpleBench("Manual loop") { buildList { for (i in data) if (i % 2 == 0) add(i * 2) } }
}JMH는 dead code elimination, constant folding 등 JVM 최적화를 방지하여 정확한 벤치마크를 보장합니다.
간이 벤치마크는 JVM 최적화로 인해 실제보다 빠르게 측정될 수 있습니다. 정밀한 결과가 필요하면 반드시 JMH를 사용하세요.
09GC 튜닝과 메모리 관리
JVM 가비지 컬렉션의 동작을 이해하고 Kotlin 코드에서 GC 압력을 줄이는 전략을 다룹니다.
Kotlin code
fun main() {
// GC 모니터링
val rt = Runtime.getRuntime()
fun memStats() = buildString {
val used = (rt.totalMemory() - rt.freeMemory()) / 1024 / 1024
val total = rt.totalMemory() / 1024 / 1024
append("사용: ${used}MB / 전체: ${total}MB")
}
println("시작: ${memStats()}")
// 1. 임시 객체 최소화
// 나쁜 예: 매번 새 리스트 생성
// val bad = (1..100000).map { it.toString() }.filter { it.length > 3 }
// 좋은 예: 시퀀스로 중간 객체 제거
val good = (1..100000).asSequence()
.map { it.toString() }
.filter { it.length > 3 }
.toList()
println("리스트 생성 후: ${memStats()}")
// 2. 큰 객체 명시적 해제
var largeBuffer: ByteArray? = ByteArray(50 * 1024 * 1024) // 50MB
println("할당 후: ${memStats()}")
largeBuffer = null
System.gc()
Thread.sleep(100)
println("해제 후: ${memStats()}")
// 3. 객체 재사용
val reusableBuffer = StringBuilder(1024)
repeat(1000) { i ->
reusableBuffer.clear()
reusableBuffer.append("항목-").append(i)
// reusableBuffer.toString() 사용
}
println("완료: ${memStats()}")
println("리스트 크기: ${good.size}")
}JVM 옵션 -XX:+UseG1GC -Xmx512m으로 GC 알고리즘과 힙 크기를 조절합니다. -verbose:gc로 GC 로그를 확인하세요.
System.gc()를 직접 호출하는 것은 권장되지 않습니다. JVM이 GC 시점을 최적으로 결정하므로 수동 호출은 오히려 성능을 저하시킬 수 있습니다.
10스레드 풀 최적화
코루틴 디스패처와 스레드 풀 설정을 최적화합니다. CPU 바운드와 I/O 바운드 작업의 구분이 핵심입니다.
Kotlin code
import kotlinx.coroutines.*
import java.util.concurrent.Executors
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
// CPU 바운드: Dispatchers.Default (코어 수 만큼)
println("Default 스레드: ${Runtime.getRuntime().availableProcessors()}")
// I/O 바운드: Dispatchers.IO (최대 64 스레드)
val ioTime = measureTimeMillis {
val jobs = List(100) {
async(Dispatchers.IO) {
Thread.sleep(100) // I/O 시뮬레이션
"결과-$it"
}
}
jobs.awaitAll()
}
println("IO 100개 병렬: ${ioTime}ms")
// 커스텀 디스패처
val customPool = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
val customTime = measureTimeMillis {
val jobs = List(20) {
async(customPool) {
Thread.sleep(50)
it
}
}
jobs.awaitAll()
}
println("커스텀(4스레드) 20개: ${customTime}ms")
customPool.close()
// limitedParallelism: 기존 디스패처에서 동시성 제한
val limited = Dispatchers.IO.limitedParallelism(2)
val limitedTime = measureTimeMillis {
val jobs = List(10) {
async(limited) {
Thread.sleep(100)
it
}
}
jobs.awaitAll()
}
println("제한(2스레드) 10개: ${limitedTime}ms")
}Dispatchers.IO.limitedParallelism(n)은 전체 IO 풀과 스레드를 공유하면서 동시 실행 수만 제한합니다. DB 연결 풀 크기에 맞출 때 유용합니다.
CPU 바운드 작업에 Dispatchers.IO를 사용하면 스레드가 과다 생성되어 컨텍스트 스위칭 오버헤드가 발생합니다. CPU 작업에는 Default를 사용하세요.
11코루틴 성능 최적화
코루틴의 성능을 최적화하는 기법입니다. 불필요한 중단 방지, 적절한 디스패처 선택, 배치 처리를 다룹니다.
Kotlin code
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.measureTimeMillis
suspend fun fetchItem(id: Int): String {
delay(100) // 네트워크 지연 시뮬레이션
return "item-$id"
}
fun main() = runBlocking {
// 나쁜 예: 순차 실행
val seqTime = measureTimeMillis {
val results = (1..10).map { fetchItem(it) }
}
println("순차: ${seqTime}ms")
// 좋은 예: 병렬 실행
val parTime = measureTimeMillis {
val results = coroutineScope {
(1..10).map { async { fetchItem(it) } }.awaitAll()
}
}
println("병렬: ${parTime}ms")
// Flow 배치 처리
val batchTime = measureTimeMillis {
(1..100).asFlow()
.buffer(10) // 버퍼링
.map { fetchItem(it) }
.collect {}
}
println("Flow 버퍼: ${batchTime}ms")
// 최적: chunked + 병렬
val optTime = measureTimeMillis {
(1..100).chunked(10).map { batch ->
coroutineScope {
batch.map { async { fetchItem(it) } }.awaitAll()
}
}
}
println("청크 병렬: ${optTime}ms")
}flow.buffer()를 사용하면 방출과 수집이 별도 코루틴에서 실행되어 파이프라인 병렬성이 향상됩니다.
무제한 병렬 요청은 서버에 과부하를 줄 수 있습니다. Semaphore나 limitedParallelism으로 동시 요청 수를 제한하세요.
12캐싱 전략 (Caching)
다양한 캐싱 전략을 구현합니다. TTL, LRU, Write-through 캐시로 성능과 일관성을 확보합니다.
Kotlin code
class LRUCache<K, V>(private val maxSize: Int) {
private val cache = LinkedHashMap<K, V>(maxSize, 0.75f, true)
fun get(key: K): V? = cache[key]
fun put(key: K, value: V) {
cache[key] = value
if (cache.size > maxSize) {
val eldest = cache.keys.first()
cache.remove(eldest)
}
}
val size get() = cache.size
fun stats() = "크기: $size/$maxSize"
}
class TTLCache<K, V>(private val ttlMs: Long) {
private data class Entry<V>(val value: V, val expiry: Long)
private val cache = mutableMapOf<K, Entry<V>>()
fun get(key: K): V? {
val entry = cache[key] ?: return null
if (System.currentTimeMillis() > entry.expiry) {
cache.remove(key)
return null
}
return entry.value
}
fun put(key: K, value: V) {
cache[key] = Entry(value, System.currentTimeMillis() + ttlMs)
}
}
fun main() {
// LRU 캐시
val lru = LRUCache<String, Int>(3)
lru.put("a", 1); lru.put("b", 2); lru.put("c", 3)
lru.get("a") // a를 최근 사용으로 이동
lru.put("d", 4) // b가 제거됨 (가장 오래 미사용)
println("LRU a: ${lru.get("a")}") // 1
println("LRU b: ${lru.get("b")}") // null (제거됨)
println(lru.stats())
// TTL 캐시
val ttl = TTLCache<String, String>(500) // 500ms TTL
ttl.put("token", "abc123")
println("즉시: ${ttl.get("token")}") // abc123
Thread.sleep(600)
println("만료 후: ${ttl.get("token")}") // null
}읽기가 빈번하고 데이터 변경이 적은 경우 LRU 캐시가 적합합니다. 실시간성이 중요하면 TTL을 짧게 설정하세요.
캐시 무효화는 어려운 문제입니다. 데이터 변경 시 캐시를 즉시 갱신하는 write-through 전략이나 이벤트 기반 무효화를 고려하세요.
13컴파일 최적화 (Kotlin Compiler)
Kotlin 컴파일러 옵션으로 빌드 속도와 런타임 성능을 최적화합니다.
Kotlin code
// build.gradle.kts 컴파일러 옵션
// kotlin {
// compilerOptions {
// jvmTarget.set(JvmTarget.JVM_17)
// freeCompilerArgs.addAll(
// "-Xjsr305=strict", // null 안전성 강화
// "-opt-in=kotlin.RequiresOptIn",
// "-Xjvm-default=all", // 인터페이스 기본 메서드
// )
// }
// }
// 컴파일 최적화 확인 코드
fun main() {
// 1. const val → 컴파일 타임 상수 (인라인)
val config = "API_URL: 상수 인라인됨"
// 2. when → tableswitch/lookupswitch
fun optimize(code: Int) = when (code) {
200 -> "OK"
404 -> "Not Found"
500 -> "Server Error"
else -> "Unknown"
}
// 3. 원시 타입 유지
val nums = intArrayOf(1, 2, 3) // int[] (박싱 없음)
val sum = nums.sum()
// 4. 인라인 클래스 → 래퍼 제거
@JvmInline
value class Meter(val value: Double)
val distance = Meter(100.0) // 런타임에 Double
// 5. 빌드 속도 최적화
// gradle.properties:
// kotlin.incremental=true
// kotlin.daemon.jvmargs=-Xmx2g
// org.gradle.parallel=true
// org.gradle.caching=true
println("JVM: ${System.getProperty("java.version")}")
println("Kotlin: ${KotlinVersion.CURRENT}")
println("최적화 결과: $sum, ${optimize(200)}")
println("인라인 클래스: ${distance.value}m")
}kotlin.incremental=true와 org.gradle.caching=true를 활성화하면 빌드 시간이 크게 단축됩니다.
-Xjvm-default=all을 기존 라이브러리에 적용하면 바이너리 호환성이 깨질 수 있습니다. 새 프로젝트에서만 설정하세요.
정리하며
- 최적화 대상은 프로파일러로 확인한 뜨거운 경로에 한정하고 나머지는 가독성을 우선합니다
- value class는 널 가능·제네릭·인터페이스 자리에서 박싱되어 이점이 사라집니다
- Sequence는 원소가 많고 체인이 길 때만 이득이며 짧은 체인에서는 오버헤드입니다
- 직접 만든 나노초 측정 대신 JMH를 써야 JIT 왜곡 없는 비교가 가능합니다
더 깊이 들어가고 싶다면 Kotlin 학습 라이브러리에서 다른 주제 가이드를 이어서 보거나, 언어 비교에서 같은 개념이 다른 언어에서 어떻게 표현되는지 확인해 보세요.