Theme:

코틀린의 클래스는 자바보다 간결하다. 생성자, getter/setter, toString 같은 보일러플레이트가 대폭 줄어든다. 하지만 그 내부 동작을 제대로 이해하지 못하면 면접에서 당황할 수 있다. 주 생성자와 init 블록의 실행 순서, backing field, lateinit vs lazy 차이 — 하나씩 파헤쳐보자.

주 생성자(Primary Constructor)

코틀린의 클래스는 헤더에 주 생성자를 선언한다.

KOTLIN
// 주 생성자에서 프로퍼티 선언까지 한 번에
class User(val name: String, var age: Int)

// 자바로 치면 이만큼의 코드가 생략된 것
// - private 필드 2개
// - 생성자
// - getter 2개
// - setter 1개 (age만)

val/var 없이 선언하면?

KOTLIN
class User(name: String, age: Int) {
    // name과 age는 생성자 파라미터일 뿐, 프로퍼티가 아니다
    // init 블록이나 프로퍼티 초기화에서만 사용 가능
    val greeting = "안녕, $name"   // 여기서만 접근 가능

    // fun printName() = println(name)  // 컴파일 에러! 프로퍼티가 아님
}

val이나 var를 붙이면 프로퍼티가 되어 클래스 어디서든 접근할 수 있고, 안 붙이면 생성자 파라미터로만 사용된다. 면접에서 자주 물어보는 포인트다.

보조 생성자(Secondary Constructor)

KOTLIN
class User(val name: String, val age: Int) {
    var email: String = ""

    // 보조 생성자 — 반드시 주 생성자를 호출해야 함
    constructor(name: String, age: Int, email: String) : this(name, age) {
        this.email = email
    }
}

보조 생성자는 this()를 통해 직접 또는 간접적으로 주 생성자를 호출해야 한다. 실무에서는 기본값 파라미터로 대체하는 경우가 더 많다.

KOTLIN
// 기본값 파라미터로 보조 생성자 대체
class User(
    val name: String,
    val age: Int,
    var email: String = ""
)

init 블록의 실행 순서 — 면접 단골 주제

이 부분이 면접에서 자주 나온다. 실행 순서를 확실히 알아두자.

KOTLIN
class InitOrder(name: String) {
    val firstProperty = "첫 번째 프로퍼티: $name".also(::println)  // 1

    init {
        println("첫 번째 init 블록: $name")                       // 2
    }

    val secondProperty = "두 번째 프로퍼티: $name".also(::println) // 3

    init {
        println("두 번째 init 블록: $name")                       // 4
    }

    constructor(name: String, age: Int) : this(name) {
        println("보조 생성자: $name, $age")                       // 5
    }
}

InitOrder("코틀린", 5)
// 출력 순서:
// 첫 번째 프로퍼티: 코틀린
// 첫 번째 init 블록: 코틀린
// 두 번째 프로퍼티: 코틀린
// 두 번째 init 블록: 코틀린
// 보조 생성자: 코틀린, 5

핵심 규칙은 이렇다.

  1. 프로퍼티 초기화와 init 블록은 코드에 나타나는 순서대로 실행
  2. 보조 생성자 본문은 가장 마지막에 실행
  3. 주 생성자 → (프로퍼티 + init 순서대로) → 보조 생성자

프로퍼티와 Backing Field

코틀린의 프로퍼티는 자바의 필드 + getter + setter를 합친 개념이다.

KOTLIN
class Person {
    // var은 getter + setter 자동 생성
    var name: String = "이름 없음"

    // val은 getter만 자동 생성
    val isAdult: Boolean
        get() = age >= 19     // 커스텀 getter

    var age: Int = 0
        set(value) {          // 커스텀 setter
            require(value >= 0) { "나이는 음수일 수 없습니다" }
            field = value     // field — backing field에 접근
        }
}

Backing Field(field)란?

field는 프로퍼티의 실제 값을 저장하는 필드에 접근하는 키워드다. 커스텀 getter/setter 안에서만 사용할 수 있다.

KOTLIN
var counter: Int = 0
    set(value) {
        if (value >= 0) field = value  // field를 사용해 실제 값 저장
        // this.counter = value        // 이렇게 쓰면 setter 재귀 호출 → StackOverflow!
    }

Backing Field가 생성되지 않는 경우

KOTLIN
// backing field 없음 — 값을 저장하지 않고 매번 계산
val isEmpty: Boolean
    get() = size == 0

// backing field 있음 — field를 사용하거나 기본 getter/setter를 쓸 때
var name: String = "기본값"

lateinit vs lazy — 초기화를 미루는 두 가지 방법

lateinit — 나중에 반드시 초기화할게

KOTLIN
class UserService {
    // var만 가능, primitive 타입 불가
    lateinit var repository: UserRepository

    fun init() {
        repository = UserRepository()
    }

    fun findUser(id: Long): User {
        // 초기화 전 접근하면 UninitializedPropertyAccessException
        return repository.findById(id)
    }

    fun checkInit() {
        // 초기화 여부 확인
        if (::repository.isInitialized) {
            println("초기화됨")
        }
    }
}

by lazy — 처음 쓸 때 초기화해줘

KOTLIN
class Config {
    // val만 가능, 첫 접근 시 한 번만 초기화
    val dbConnection: Connection by lazy {
        println("DB 연결 생성")  // 첫 접근 때만 실행
        DriverManager.getConnection("jdbc:...")
    }
}

비교 정리

특징lateinitby lazy
키워드varval
초기화 시점개발자가 직접첫 접근 시 자동
primitive 타입불가가능
nullable불가가능
스레드 안전보장 안 됨기본 synchronized
대표 사용처DI, 테스트무거운 초기화 지연
KOTLIN
// lazy의 스레드 안전 모드
val data1: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    // 기본값 — 동기화됨
    "스레드 안전"
}

val data2: String by lazy(LazyThreadSafetyMode.NONE) {
    // 단일 스레드 환경에서 성능 향상
    "동기화 없음"
}

가시성 수정자(Visibility Modifiers)

코틀린의 가시성 수정자는 자바와 다르다.

KOTLIN
class Example {
    public val a = 1        // 어디서든 접근 (기본값)
    private val b = 2       // 이 클래스 내부에서만
    protected val c = 3     // 이 클래스 + 하위 클래스
    internal val d = 4      // 같은 모듈 내에서만
}

자바와의 차이점

수정자코틀린자바
기본값publicpackage-private
internal같은 모듈없음
package-private없음같은 패키지
protected하위 클래스만하위 클래스 + 같은 패키지

면접에서 자주 묻는 포인트가 두 가지 있다.

  1. 코틀린에는 package-private가 없다 — 대신 internal(모듈 단위)을 쓴다
  2. protected의 범위가 다르다 — 자바는 같은 패키지에서도 접근 가능하지만, 코틀린은 하위 클래스에서만 접근 가능

최상위 선언의 가시성

KOTLIN
// 파일 최상위에서도 가시성 수정자 사용 가능
public fun publicFunction() { }       // 어디서든
private fun privateFunction() { }     // 이 파일 내에서만
internal fun internalFunction() { }   // 같은 모듈 내에서만
// protected는 최상위에서 사용 불가

생성자의 가시성

KOTLIN
// 생성자를 private으로 — 외부에서 직접 생성 불가
class Singleton private constructor() {
    companion object {
        val INSTANCE = Singleton()
    }
}

// 주 생성자에 가시성 수정자를 붙일 때 constructor 키워드 필수
class User internal constructor(val name: String)

프로퍼티 접근자의 가시성

KOTLIN
class User(name: String) {
    var name: String = name
        private set          // setter만 private — 외부에서 변경 불가

    fun updateName(newName: String) {
        name = newName       // 클래스 내부에서는 변경 가능
    }
}

val user = User("코틀린")
println(user.name)           // OK — getter는 public
// user.name = "자바"        // 컴파일 에러 — setter는 private

정리 — 면접에서 기억할 포인트

  • 주 생성자에 val/var — 붙이면 프로퍼티, 안 붙이면 파라미터만
  • init 블록 실행 순서 — 프로퍼티 초기화와 init은 선언 순서대로, 보조 생성자는 마지막
  • backing field — 커스텀 setter에서 field 사용. this.property는 재귀 호출 위험
  • lateinit — var, non-primitive, 나중에 반드시 초기화
  • by lazy — val, 첫 접근 시 초기화, 기본 스레드 안전
  • internal — 모듈 단위 가시성. 자바의 package-private와 다름
댓글 로딩 중...