클래스·객체 — data·sealed·enum·object·생성자
코틀린의 클래스는 자바보다 훨씬 적은 코드로 같은 일을 한다 — 생성자·프로퍼티를 한 줄에 선언하고, data class 하나로 equals·hashCode·toString·copy를 공짜로 얻는다. 또 sealed·object·value class 같은 자바엔 없거나 번거로운 도구로 상태와 싱글톤을 우아하게 모델링한다. 이 장은 코틀린의 클래스 도구 상자를 코드로 훑는다. (출처: Kotlin Docs — Classes / Kotlin in Action 2e Ch 4.)
클래스와 생성자 — 선언과 초기화를 한 줄에
코틀린은 *주 생성자(primary constructor)*를 클래스 헤더에 둔다. val/var를 붙이면 그 자리에서 프로퍼티 선언 + 할당까지 끝난다.
class User(val name: String, var age: Int) { // 프로퍼티 선언+생성자가 한 줄
init { // 초기화 블록
require(age >= 0) { "나이는 음수 불가" }
}
constructor(name: String) : this(name, 0) // 부 생성자 — 주 생성자에 위임
}
val u = User("Kim", 30) // new 키워드 없음
println(u.name) // 프로퍼티 직접 접근 (내부적으론 getter)
자바라면 필드 선언 + 생성자 + getter/setter로 수십 줄이 코틀린에선 한 줄이다.
프로퍼티 — 필드가 아니라 접근자
코틀린엔 필드가 아니라 프로퍼티가 있다. u.age는 getter/setter 호출이고, 필요하면 커스텀 접근자나 계산 프로퍼티를 둔다.
class Rect(val w: Int, val h: Int) {
val area: Int get() = w * h // 계산 프로퍼티 — 저장 안 하고 매번 계산
var name: String = ""
set(value) { field = value.trim() } // 커스텀 setter, field = 백킹 필드
}
data class — 값 객체를 공짜로
DTO·모델처럼 데이터를 담는 클래스엔 **data class**를 쓴다. 컴파일러가 *equals·hashCode·toString·copy·componentN*을 자동 생성한다.
data class Member(val id: Long, val name: String, val age: Int)
val a = Member(1, "Kim", 30)
val b = a.copy(age = 31) // 일부만 바꿔 새 객체 (불변 갱신)
println(a == Member(1,"Kim",30)) // true — 내용 비교(equals 자동)
val (id, name) = a // 구조 분해 (componentN)
println("$id $name") // 1 Kim
copy는 불변 객체를 다루는 핵심 — 안드로이드의 *단방향 데이터 흐름(UDF)*에서 상태를 일부만 바꿔 새 상태를 만들 때 끝없이 쓴다([andarch] UDF).
enum과 sealed — 제한된 타입으로 상태를 모델링
**enum class**는 고정된 상수 집합이다(메서드·프로퍼티도 가능).
enum class Status(val label: String) {
LOADING("로딩"), SUCCESS("성공"), ERROR("오류");
fun isDone() = this != LOADING
}
**sealed class/sealed interface**는 더 강력하다 — *하위 타입이 컴파일 시점에 고정(같은 모듈)*된 제한된 계층이다. 서로 다른 데이터를 가진 상태를 표현하고, when이 모든 경우를 망라하면 else 없이도 컴파일러가 빠진 분기를 잡는다.
sealed interface UiState {
data object Loading : UiState
data class Success(val data: List<Member>) : UiState
data class Error(val message: String) : UiState
}
fun render(state: UiState) = when (state) { // else 불필요 — 망라성 보장
UiState.Loading -> showSpinner()
is UiState.Success -> showList(state.data) // 스마트 캐스트로 data 접근
is UiState.Error -> showError(state.message)
}
이 sealed + when 조합이 안드로이드 화면 상태 모델링의 교과서 패턴이다 — enum은 상수만, sealed는 각자 다른 데이터를 담을 때.
object와 companion — 싱글톤과 정적 멤버
코틀린엔 static이 없다. 대신 object(싱글톤)와 companion object(클래스당 하나의 동반 객체)를 쓴다.
object AppConfig { // 싱글톤 — 인스턴스 하나, 스레드 안전 지연 초기화
var baseUrl = "https://api.app.com"
}
AppConfig.baseUrl // 클래스명으로 접근
class User private constructor(val name: String) {
companion object { // 정적 멤버 자리
fun create(name: String) = User(name) // 팩터리 (private 생성자 우회)
}
}
User.create("Kim") // 정적 호출처럼
*익명 객체(object expression)*는 자바의 익명 클래스 대체다 — val listener = object : OnClickListener { ... }.
상속과 인터페이스 — 기본은 final
코틀린 클래스는 기본이 final(상속 불가)이다 — 상속을 의도한 클래스만 open. 이는 "상속을 위한 설계가 아니면 상속을 금지하라"(Effective Java)를 언어 기본값으로 강제한 것이다.
open class Animal(val name: String) { // open 이라야 상속 가능
open fun sound() = "..." // open 이라야 오버라이드 가능
}
class Dog(name: String) : Animal(name) {
override fun sound() = "멍멍" // override 키워드 필수
}
interface Clickable {
fun click()
fun showOff() = println("clickable") // 인터페이스 기본 구현
}
value class — 래퍼 비용 0
타입 안전을 위해 원시값을 감싸되 런타임 오버헤드는 없애고 싶을 때 value class(inline class)를 쓴다 — 컴파일 시 내부 값으로 인라인되어 객체 할당이 없다.
@JvmInline
value class UserId(val value: Long) // Long을 감싸지만 런타임엔 Long 그대로
fun load(id: UserId) { /* ... */ }
// load(42L) ❌ — 그냥 Long 못 넣음. 타입으로 UserId/OrderId 혼동 방지
load(UserId(42))
정리 — 적은 코드로 정확한 모델
코틀린 클래스의 핵심 — 주 생성자로 선언·초기화를 한 줄에, data class로 값 객체를 공짜로, sealed로 상태를 망라적으로, object/companion으로 싱글톤·정적을, open으로 상속을 의도적으로, value class로 비용 없는 타입 안전을. 이 도구들이 안드로이드의 상태·모델·DI 코드를 간결하고 안전하게 만든다.
한 줄 요약 — 생성자·프로퍼티는 헤더 한 줄.
data class=equals/hashCode/copy/구조분해 자동.enum=상수,sealed=각자 데이터 가진 제한 계층(when 망라).object=싱글톤,companion=정적 대체. 클래스는 기본final(상속은open).value class=비용 0 래퍼.
(출처: Kotlin Docs — Classes / Data Classes / Sealed / Objects / Kotlin in Action 2e Ch 4 / Effective Kotlin Item 36.)