클래스와 객체 — 생성자, 프로퍼티, 그리고 자바와 다른 점
코틀린의 클래스는 자바보다 간결하다. 생성자, getter/setter, toString 같은 보일러플레이트가 대폭 줄어든다. 하지만 그 내부 동작을 제대로 이해하지 못하면 면접에서 당황할 수 있다. 주 생성자와 init 블록의 실행 순서, backing field, lateinit vs lazy 차이 — 하나씩 파헤쳐보자.
주 생성자(Primary Constructor)
코틀린의 클래스는 헤더에 주 생성자를 선언한다.
// 주 생성자에서 프로퍼티 선언까지 한 번에
class User(val name: String, var age: Int)
// 자바로 치면 이만큼의 코드가 생략된 것
// - private 필드 2개
// - 생성자
// - getter 2개
// - setter 1개 (age만)
val/var 없이 선언하면?
class User(name: String, age: Int) {
// name과 age는 생성자 파라미터일 뿐, 프로퍼티가 아니다
// init 블록이나 프로퍼티 초기화에서만 사용 가능
val greeting = "안녕, $name" // 여기서만 접근 가능
// fun printName() = println(name) // 컴파일 에러! 프로퍼티가 아님
}
val이나 var를 붙이면 프로퍼티가 되어 클래스 어디서든 접근할 수 있고, 안 붙이면 생성자 파라미터로만 사용된다. 면접에서 자주 물어보는 포인트다.
보조 생성자(Secondary Constructor)
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()를 통해 직접 또는 간접적으로 주 생성자를 호출해야 한다. 실무에서는 기본값 파라미터로 대체하는 경우가 더 많다.
// 기본값 파라미터로 보조 생성자 대체
class User(
val name: String,
val age: Int,
var email: String = ""
)
init 블록의 실행 순서 — 면접 단골 주제
이 부분이 면접에서 자주 나온다. 실행 순서를 확실히 알아두자.
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
핵심 규칙은 이렇다.
- 프로퍼티 초기화와 init 블록은 코드에 나타나는 순서대로 실행
- 보조 생성자 본문은 가장 마지막에 실행
- 주 생성자 → (프로퍼티 + init 순서대로) → 보조 생성자
프로퍼티와 Backing Field
코틀린의 프로퍼티는 자바의 필드 + getter + setter를 합친 개념이다.
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 안에서만 사용할 수 있다.
var counter: Int = 0
set(value) {
if (value >= 0) field = value // field를 사용해 실제 값 저장
// this.counter = value // 이렇게 쓰면 setter 재귀 호출 → StackOverflow!
}
Backing Field가 생성되지 않는 경우
// backing field 없음 — 값을 저장하지 않고 매번 계산
val isEmpty: Boolean
get() = size == 0
// backing field 있음 — field를 사용하거나 기본 getter/setter를 쓸 때
var name: String = "기본값"
lateinit vs lazy — 초기화를 미루는 두 가지 방법
lateinit — 나중에 반드시 초기화할게
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 — 처음 쓸 때 초기화해줘
class Config {
// val만 가능, 첫 접근 시 한 번만 초기화
val dbConnection: Connection by lazy {
println("DB 연결 생성") // 첫 접근 때만 실행
DriverManager.getConnection("jdbc:...")
}
}
비교 정리
| 특징 | lateinit | by lazy |
|---|---|---|
| 키워드 | var | val |
| 초기화 시점 | 개발자가 직접 | 첫 접근 시 자동 |
| primitive 타입 | 불가 | 가능 |
| nullable | 불가 | 가능 |
| 스레드 안전 | 보장 안 됨 | 기본 synchronized |
| 대표 사용처 | DI, 테스트 | 무거운 초기화 지연 |
// lazy의 스레드 안전 모드
val data1: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
// 기본값 — 동기화됨
"스레드 안전"
}
val data2: String by lazy(LazyThreadSafetyMode.NONE) {
// 단일 스레드 환경에서 성능 향상
"동기화 없음"
}
가시성 수정자(Visibility Modifiers)
코틀린의 가시성 수정자는 자바와 다르다.
class Example {
public val a = 1 // 어디서든 접근 (기본값)
private val b = 2 // 이 클래스 내부에서만
protected val c = 3 // 이 클래스 + 하위 클래스
internal val d = 4 // 같은 모듈 내에서만
}
자바와의 차이점
| 수정자 | 코틀린 | 자바 |
|---|---|---|
| 기본값 | public | package-private |
| internal | 같은 모듈 | 없음 |
| package-private | 없음 | 같은 패키지 |
| protected | 하위 클래스만 | 하위 클래스 + 같은 패키지 |
면접에서 자주 묻는 포인트가 두 가지 있다.
- 코틀린에는 package-private가 없다 — 대신
internal(모듈 단위)을 쓴다 - protected의 범위가 다르다 — 자바는 같은 패키지에서도 접근 가능하지만, 코틀린은 하위 클래스에서만 접근 가능
최상위 선언의 가시성
// 파일 최상위에서도 가시성 수정자 사용 가능
public fun publicFunction() { } // 어디서든
private fun privateFunction() { } // 이 파일 내에서만
internal fun internalFunction() { } // 같은 모듈 내에서만
// protected는 최상위에서 사용 불가
생성자의 가시성
// 생성자를 private으로 — 외부에서 직접 생성 불가
class Singleton private constructor() {
companion object {
val INSTANCE = Singleton()
}
}
// 주 생성자에 가시성 수정자를 붙일 때 constructor 키워드 필수
class User internal constructor(val name: String)
프로퍼티 접근자의 가시성
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와 다름
댓글 로딩 중...