코틀린으로 코드를 작성하다 보면 문법에는 금방 익숙해집니다.
하지만 막상 이 코드가 어떤 과정을 거쳐 실제로 실행되는지는 깊게 생각하지 않고 넘어가는 경우가 많습니다.
예를 들어 우리가 fun main()을 작성하고 프로그램을 실행할 때,
Kotlin 코드는 곧바로 실행되는 걸까요?
아니면 중간에 다른 형태로 바뀌는 과정을 거칠까요?
이 흐름을 한 번 이해해 두면,
단순히 문법을 아는 수준을 넘어 Kotlin/JVM이 어떤 구조 위에서 동작하는지 더 선명하게 볼 수 있습니다.
특히 백엔드처럼 JVM 환경에서 Kotlin을 자주 사용하는 개발자라면 한 번쯤 정리해둘 만한 주제입니다.
“Kotlin is a modern language that's concise, multiplatform, and interoperable with Java and other languages.”
— Kotlin Getting Started
공식 문서가 설명하듯 Kotlin은 간결한 문법, 멀티플랫폼 지원, 그리고 Java와의 상호운용성을 함께 내세우는 언어입니다.
다만 이 글에서는 Kotlin 전체를 다루기보다, 우리가 서버 개발에서 자주 사용하는 Kotlin/JVM에 범위를 좁혀 보겠습니다.
Kotlin은 JVM 전용 언어가 아닙니다.
JVM뿐 아니라 JavaScript, Native, Wasm 같은 여러 플랫폼을 대상으로 사용할 수 있습니다.
하지만 같은 Kotlin이라도 어느 플랫폼을 대상으로 컴파일하느냐에 따라 결과물과 실행 방식은 달라집니다.
그래서 Kotlin을 이해할 때는 “Kotlin 전체”와 “Kotlin/JVM”을 구분해서 보는 편이 더 좋습니다.
이 글에서 다루는 범위는 명확합니다.
.kt 파일즉, Kotlin/JVM은 Kotlin으로 작성한 코드를 JVM이 이해할 수 있는 형태로 바꿔 실행하는 구조라고 보면 됩니다.
Kotlin/JVM의 흐름은 생각보다 단순합니다.
소스 코드 작성 → Kotlin 컴파일러 분석 → 바이트코드 생성 → JVM 로드 및 실행
이 과정을 한 줄로 요약하면,
JVM은 Kotlin 문법을 직접 실행하지 않고, Kotlin 컴파일러가 만들어 준 결과물을 실행한다는 뜻입니다.
이 점이 중요한 이유는,
Kotlin 코드를 이해할 때 언어 자체의 특징과 JVM 런타임의 특징을 나눠서 볼 수 있기 때문입니다.
가장 단순한 예제로 시작해보겠습니다.
fun main() {
println("Hello Kotlin")
}
Kotlin에서는 이런 코드를 아주 자연스럽게 작성할 수 있습니다.
Java처럼 반드시 클래스를 먼저 만들지 않아도, 파일 단위에서 바로 main() 함수를 선언할 수 있기 때문입니다.
이런 문법적 간결함은 Kotlin의 장점으로 자주 이야기되지만,
오늘 글에서 더 중요한 건 이 코드가 이후 어떤 경로를 거쳐 실행되는가입니다.
우리가 작성한 이 코드는 아직 JVM이 바로 실행할 수 있는 상태는 아닙니다.
먼저 컴파일러의 처리를 거쳐야 합니다.
“The Kotlin compiler for JVM compiles Kotlin source files into Java class files.”
— Kotlin Compiler Reference
Kotlin/JVM에서 컴파일러의 역할은 분명합니다.
.kt 파일을 읽어 JVM이 이해할 수 있는 클래스 파일, 즉 .class 형태로 바꾸는 것입니다.
하지만 이 과정은 단순한 형식 변환만은 아닙니다.
컴파일러는 먼저 코드를 분석합니다.
예를 들어 다음과 같은 것들을 확인합니다.
즉 컴파일러는 단순히 “코드를 다른 파일로 저장하는 도구”가 아니라,
코드의 구조와 의미를 분석한 뒤 JVM용 결과물로 바꾸는 프로그램입니다.
그래서 Kotlin에서 자주 말하는 타입 안정성이나 타입 추론 같은 특징도,
대부분 이 컴파일 단계와 깊게 연결되어 있습니다.
컴파일이 끝나면 결과물은 보통 .class 파일 또는 .jar 파일 형태로 만들어집니다.
쉽게 말하면 이 시점부터는 더 이상 “Kotlin 소스 코드”가 아니라,
JVM이 읽을 수 있는 바이트코드 형태의 산출물이 준비된 상태라고 볼 수 있습니다.
예를 들어 Kotlin 공식 문서에서는 아래처럼 kotlinc 명령으로 jar 파일을 만드는 예시를 보여줍니다.
kotlinc Main.kt -include-runtime -d main.jar
이 명령은 Main.kt를 컴파일해서 실행 가능한 jar 파일로 만드는 예시입니다.
즉 우리가 작성한 Kotlin 소스가 실행 가능한 결과물로 바뀌는 과정을 직접 보여주는 셈입니다.
여기서 중요한 건 JVM이 읽는 대상이 .kt 파일이 아니라,
이렇게 컴파일된 .class나 .jar라는 점입니다.
컴파일이 끝났다고 해서 실행이 바로 시작되는 것은 아닙니다.
이제부터는 Kotlin 컴파일러의 영역이 아니라 JVM의 영역입니다.
“The Java Virtual Machine dynamically loads, links and initializes classes and interfaces.”
— Java Virtual Machine Specification
JVM은 컴파일된 클래스 파일을 가져와 다음 순서로 실행을 준비합니다.

— 이미지 출처
로드(Loading)
실행할 클래스 정보를 JVM 안으로 불러옵니다.
링크(Linking)
불러온 클래스가 실행 가능한 상태가 되도록 검증하고 연결합니다.
초기화(Initialization)
정적 필드나 클래스 초기화와 관련된 작업을 수행합니다.
그 뒤에야 실제 실행 흐름이 시작됩니다.
쉽게 말하면 JVM은
“파일을 그냥 읽고 바로 실행하는 프로그램”이 아니라,
실행할 클래스를 가져오고, 검증하고, 준비를 마친 뒤 실행하는 환경이라고 볼 수 있습니다.
여기서 한 가지 더 자주 헷갈리는 부분이 있습니다.
JVM은 바이트코드를 받았다고 해서 그것을 처음부터 전부 네이티브 코드로 바꿔 놓고 실행하지는 않습니다.
일반적으로 JVM은 바이트코드를 실행하면서,
자주 호출되는 부분은 더 효율적으로 실행할 수 있도록 최적화합니다.
이 과정에서 등장하는 개념이 바로 JIT(Just-In-Time) 컴파일입니다.
즉 실행 흐름을 아주 단순하게 표현하면 아래와 같습니다.
바이트코드 실행 → 반복적으로 많이 쓰이는 부분은 최적화
이 구조 덕분에 JVM은 플랫폼 독립성과 실행 최적화를 어느 정도 함께 가져갈 수 있습니다.
그래서 Kotlin/JVM의 성능을 이야기할 때도
단순히 Kotlin 언어만 볼 것이 아니라,
결국 JVM 위에서 어떻게 실행되는가까지 함께 봐야 합니다.
다시 처음 예제로 돌아가 보겠습니다.
fun main() {
println("Hello Kotlin")
}
우리가 보기에는 한 줄짜리 단순한 코드지만, 실제로는 다음 흐름을 거칩니다.
즉 Kotlin/JVM의 핵심은
Kotlin 코드가 바로 실행되는 것이 아니라, 컴파일러와 JVM이라는 두 단계를 거쳐 실행된다는 점입니다.
Kotlin을 공부하다 보면 여러 개념이 한꺼번에 섞여 보일 때가 많습니다.
특히 아래 두 범주는 구분해서 보는 편이 좋습니다.
다음은 Kotlin 언어와 컴파일 단계에 더 가까운 이야기입니다.
이런 것들은 대부분 Kotlin 언어가 제공하는 기능이거나,
Kotlin 컴파일러가 코드를 분석하고 변환하는 과정과 연결됩니다.
예를 들어 null safety는
실행 중 예외를 줄이기 위해 타입 차원에서 null 가능 여부를 더 엄격하게 다루는 방식입니다.
smart cast도 컴파일러가 타입 정보를 보고 더 구체적으로 판단해주는 기능에 가깝습니다.
즉 이런 기능은
“JVM이 알아서 해준다”기보다 Kotlin이 컴파일 단계에서 더 잘 처리해 준다고 이해하는 편이 자연스럽습니다.
반대로 아래는 JVM 실행 환경 쪽에 더 가까운 이야기입니다.
이런 개념은 Kotlin 고유 기능이라기보다,
Kotlin/JVM이 올라가는 JVM 런타임의 특징에 가깝습니다.
즉 Kotlin으로 작성했든 Java로 작성했든,
최종적으로 JVM 위에서 실행된다면 이런 런타임 특성의 영향을 함께 받습니다.
이 구분을 알고 있으면
공부할 때도 “이건 Kotlin 언어의 특징인가, 아니면 JVM의 특징인가?”를 더 쉽게 나눠서 볼 수 있습니다.
Kotlin/JVM의 흐름은 생각보다 명확합니다.
.kt 파일을 작성합니다.즉 Kotlin/JVM을 이해한다는 것은
문법 몇 개를 외우는 것보다,
컴파일러와 JVM이 각각 어떤 역할을 맡고 있는지 이해하는 것에 더 가깝습니다.
이 흐름을 한 번 잡아두면 이후에 null safety, inline, 성능, GC 같은 주제를 접할 때도
“이건 컴파일 단계의 이야기인지, 런타임 단계의 이야기인지”를 훨씬 더 분명하게 볼 수 있습니다.
Kotlin은 Java 생태계와 자연스럽게 함께 사용할 수 있으면서도,
더 간결한 문법과 더 나은 타입 안정성을 제공하는 언어입니다.
하지만 우리가 작성한 Kotlin/JVM 코드는 곧바로 실행되는 것이 아닙니다.
먼저 Kotlin 컴파일러가 JVM이 이해할 수 있는 바이트코드로 바꾸고,
그다음 JVM이 그 결과물을 로드하고 실행합니다.
결국 Kotlin/JVM의 핵심은
“Kotlin 문법을 JVM이 직접 실행한다”가 아니라, “Kotlin 컴파일러가 JVM 친화적인 형태로 바꿔 준다”는 데 있습니다.
이 흐름을 이해하면 Kotlin 코드를 조금 더 구조적으로 바라볼 수 있고,
앞으로 성능이나 언어 기능을 공부할 때도 훨씬 덜 헷갈리게 됩니다.