코틀린으로 개발을 하다 보면 클래스 내부에 프로퍼티를 선언하고,init 블록에서 초기화 로직을 수행하는 경우가 많습니다. 하지만 코드가 복잡해질수록 "도대체 이 코드는 언제 실행되는 거지?"라는 의문이 들 때가 있습니다.
특히 프로퍼티 선언, init 블록, 생성자 로직이 뒤섞여 있을 때 초기화 순서를 잘못 이해하면 예기치 않은 NullPointerException이나 의도치 않은 값 할당 문제를 겪을 수 있습니다.
오늘은 코틀린 클래스의 초기화 순서와 init 블록의 올바른 위치, 그리고 가독성을 높이는 코드 배치 규칙에 대해 깊이 있게 알아보겠습니다.
1. 코틀린 클래스 초기화의 대원칙
코틀린 클래스가 인스턴스화될 때 실행되는 순서는 명확한 규칙을 따릅니다. 이 규칙만 기억해도 대부분의 초기화 버그를 잡을 수 있습니다.
Rule: 위에서 아래로 (Top-down)
클래스 본문(Body)에 있는 프로퍼티 초기화와 초기화 블록(init) 은 작성된 순서대로 실행됩니다.
2. 예제 코드로 보는 실행 순서
아래 코드를 보며 실행 결과를 예측해 볼까요?
class InitializationOrder(name: String) {
// 1. 주 생성자의 매개변수는 이미 사용 가능함
val firstProperty = "1번 프로퍼티: $name".also { println(it) }
// 2. 첫 번째 init 블록
init {
println("2번: 첫 번째 init 블록 실행")
}
// 3. 두 번째 프로퍼티
val secondProperty = "3번 프로퍼티".also { println(it) }
// 4. 두 번째 init 블록
init {
println("4번: 두 번째 init 블록 실행")
}
// 5. 보조 생성자 (Constructor)
constructor() : this("기본값") {
println("5번: 보조 생성자 실행")
}
}
fun main() {
InitializationOrder
}
[실행 결과]
1번 프로퍼티: 기본값
2번: 첫 번째 init 블록 실행
3번 프로퍼티
4번: 두 번째 init 블록 실행
5번: 보조 생성자 실행
해설:
- 객체 생성이 시작되면 가장 먼저 주 생성자(Primary Constructor)가 호출됩니다.
- 클래스 내부로 들어와서 위에서부터 순서대로 프로퍼티 할당과
init블록이 실행됩니다. - 모든 초기화(
init및 프로퍼티)가 끝난 후에야 보조 생성자(constructor)의 본문이 실행됩니다.
3. 흔히 하는 실수: 선언 순서의 중요성
init 블록은 주 생성자의 일부로 간주됩니다. 따라서 init 블록 안에서 클래스의 프로퍼티를 사용할 때는 반드시 그 프로퍼티가 init 블록보다 위에 선언되어 있어야 안전합니다.
class BadExample {
init {
// 컴파일 에러 혹은 런타임 에러(null 접근) 가능성 있음
// println(name.length)
}
val name = "Kotlin"
}
위 코드는 name이 초기화되기 전에 init 블록이 먼저 실행되려 하므로 문제가 발생합니다. 코틀린 컴파일러는 이를 'Illegal Forward Reference'로 막아주지만, 복잡한 로직에서는 이 순서를 놓치기 쉽습니다.
4. Clean Code: 권장하는 코드 배치 순서
우아한테크코스나 실무 프로젝트 등에서 협업을 위해 권장되는 클래스 내부의 배치 순서는 다음과 같습니다. 읽는 사람이 코드의 흐름을 위에서 아래로 자연스럽게 읽을 수 있도록 하는 것이 핵심입니다.
- 상수 (const val): 클래스 수준의 상수가 있다면 최상단 혹은
companion object의 최상단 - 프로퍼티 (Property): 상태를 나타내는 값들
init블록: 초기화 및 유효성 검증 로직- 보조 생성자 (Secondary Constructor): 필요한 경우
- 메서드 (Method):
- 공개 함수 (
public) - 비공개 함수 (
private) - public 함수 바로 아래나 파일 하단에 배치
- 공개 함수 (
class Lotto(private val numbers: List<Int>) {
// 1. 프로퍼티 (주 생성자 포함)
// 2. init 블록 (유효성 검사)
init {
require(numbers.size == 6) { "로또 번호는 6개여야 합니다." }
}
// 3. 공개 메서드
fun contains(number: Int): Boolean {
return numbers.contains(number)
}
// 4. 비공개 메서드 (내부 로직)
private fun validateRange() { ... }
}
5. 마무리
init 블록은 단순한 코드 블록이 아니라, 객체가 태어나는 순간 가장 먼저 수행해야 할 의무(유효성 검사, 초기 세팅)를 담당하는 중요한 공간입니다.
"프로퍼티 -> Init -> 생성자" 순서를 기억하고, 항상 코드를 위에서 아래로 흐르듯이 작성하는 습관을 들인다면 훨씬 더 예측 가능하고 견고한 코틀린 코드를 작성할 수 있을 것입니다.
'Dev Log > Kotlin' 카테고리의 다른 글
| [Kotlin] 함수를 변수처럼? 고차함수와 더블 콜론(::) 완벽 이해하기 (1) | 2026.01.04 |
|---|---|
| [Kotlin] 견고한 코드 만들기: require와 check를 활용한 방어적 프로그래밍 (0) | 2026.01.04 |
| [Kotlin] 반복문 리팩토링: for 루프 대신 'repeat'를 써야 하는 이유 (0) | 2026.01.03 |
| [Kotlin] 코틀린 컬렉션 함수 정복하기: map, filter, forEach의 모든 것 (0) | 2026.01.03 |
| [Kotlin] 예외 처리 패턴: 사용자 입력을 '우아하게' 다시 받는 법 (ft. 재귀 함수) (0) | 2026.01.02 |