object와 companion object — 싱글턴, 팩토리, 그리고 static의 대안
코틀린에는
static키워드가 없다. 대신object와companion object가 그 역할을 한다. 하지만 단순히 "static 대체"라고만 이해하면 부족합니다. object는 싱글턴을 만들고, companion object는 팩토리 패턴을 구현하고, object expression은 익명 객체를 만든다. 각각이 어떻게 다른지 정리해보자.
object 선언 — 언어 차원의 싱글턴
자바에서 싱글턴을 만들려면 private 생성자, static 인스턴스, double-checked locking 같은 보일러플레이트가 필요했다. 코틀린은 object 키워드 하나로 해결한다.
object DatabaseConfig {
val url = "jdbc:mysql://localhost:3306/mydb"
val maxPool = 10
fun connect() {
println("데이터베이스 연결: $url")
}
}
// 사용 — 클래스 이름으로 직접 접근
DatabaseConfig.connect()
println(DatabaseConfig.url)
object 선언의 특징
object Logger {
init {
println("Logger 초기화") // 처음 접근할 때 실행
}
fun log(message: String) = println("[LOG] $message")
}
// Logger가 처음 사용될 때 init 블록이 실행됨 (lazy 초기화)
Logger.log("시작")
- 스레드 안전 — JVM이 클래스 로딩 메커니즘으로 보장
- lazy 초기화 — 처음 접근할 때 생성
- ** 생성자 없음** — 직접 인스턴스를 만들 수 없음
- ** 상속 가능** — 클래스나 인터페이스를 상속/구현할 수 있음
인터페이스 구현하는 object
interface EventListener {
fun onEvent(event: String)
}
// object가 인터페이스를 구현
object ConsoleLogger : EventListener {
override fun onEvent(event: String) {
println("[이벤트] $event")
}
}
fun registerListener(listener: EventListener) { /* ... */ }
registerListener(ConsoleLogger) // 싱글턴을 리스너로 등록
companion object — 클래스와 함께하는 동반 객체
companion object는 클래스 내부에 선언하는 특별한 object다. 자바의 static 멤버처럼 사용하지만, 실제로는 객체 인스턴스다.
class User private constructor(val name: String, val email: String) {
companion object {
// 팩토리 메서드 — private 생성자에 접근 가능
fun fromEmail(email: String): User {
val name = email.substringBefore("@")
return User(name, email)
}
fun fromName(name: String): User {
return User(name, "$name@default.com")
}
const val MAX_NAME_LENGTH = 50
}
}
// 클래스 이름으로 접근
val user = User.fromEmail("kotlin@example.com")
println(User.MAX_NAME_LENGTH)
companion object의 핵심 장점 — 팩토리 패턴
생성자 대신 팩토리 메서드를 사용하면 이런 장점이 있다.
- 이름이 있다 —
fromEmail(),fromName()처럼 의도를 표현 - ** 반환 타입이 유연하다** — 하위 타입을 반환할 수 있음
- ** 캐싱이 가능하다** — 매번 새 인스턴스를 만들지 않아도 됨
// 하위 타입 반환 예시
abstract class Notification {
companion object {
fun create(type: String): Notification = when (type) {
"email" -> EmailNotification()
"push" -> PushNotification()
else -> throw IllegalArgumentException("알 수 없는 타입: $type")
}
}
}
class EmailNotification : Notification()
class PushNotification : Notification()
val notification = Notification.create("email") // EmailNotification 인스턴스
companion object에 이름 붙이기
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}
// 두 가지 방법으로 호출 가능
MyClass.create() // 이름 생략
MyClass.Factory.create() // 이름 사용
이름을 생략하면 기본 이름은 Companion이 된다.
companion object의 인터페이스 구현
이것이 자바의 static과 결정적으로 다른 점이다. companion object는 실제 객체이므로 인터페이스를 구현할 수 있다.
interface JsonFactory<T> {
fun fromJson(json: String): T
}
class User(val name: String) {
companion object : JsonFactory<User> {
override fun fromJson(json: String): User {
// JSON 파싱 로직
return User(json)
}
}
}
// 인터페이스 타입으로 다룰 수 있다!
fun <T> loadFromJson(factory: JsonFactory<T>, json: String): T {
return factory.fromJson(json)
}
val user = loadFromJson(User, """{"name":"코틀린"}""")
// User가 companion object를 통해 JsonFactory를 구현하므로 가능
companion object의 확장 함수
class User(val name: String) {
companion object // 비어 있어도 선언해야 확장 가능
}
// companion object에 확장 함수 추가
fun User.Companion.fromCsv(csv: String): User {
val name = csv.split(",")[0]
return User(name)
}
val user = User.fromCsv("코틀린,kotlin@test.com")
object expression — 익명 객체
자바의 익명 내부 클래스에 해당한다. object 선언(싱글턴)과 달리 매번 새 인스턴스를 만든다.
// 인터페이스의 익명 구현
val listener = object : EventListener {
override fun onEvent(event: String) {
println("이벤트 발생: $event")
}
}
// 여러 인터페이스 동시 구현
val composite = object : Clickable, Focusable {
override fun click() = println("클릭")
override fun focus() = println("포커스")
}
object expression의 클로저
자바의 익명 내부 클래스와 달리, 코틀린의 object expression은 바깥 변수를 수정할 수 있다.
fun countClicks(button: Button): Int {
var clickCount = 0 // 바깥 변수
button.setOnClickListener(object : OnClickListener {
override fun onClick() {
clickCount++ // 바깥 변수 수정 가능! (자바에서는 불가)
}
})
return clickCount
}
SAM 변환과의 차이
자바 인터페이스가 메서드 하나만 가지면 람다로 대체할 수 있다(SAM 변환). 하지만 메서드가 여러 개이거나 코틀린 인터페이스이면 object expression을 써야 한다.
// SAM 변환 가능 (자바 인터페이스, 메서드 1개)
button.setOnClickListener { println("클릭!") }
// object expression 필요 (메서드 여러 개)
val watcher = object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {}
}
자바의 static과 다른 이유
companion object와 static의 핵심 차이는 세 가지입니다.
- companion object는 실제 객체다 — 인터페이스 구현, 확장 함수 정의 가능
- ** 상속 계층에 참여한다** — 인터페이스를 구현하고 다형적으로 사용 가능
- ** 하나의 인스턴스만 존재한다** — static 멤버의 모음이 아니라 싱글턴 객체
@JvmStatic과 @JvmField — 자바 호환
class Config {
companion object {
@JvmStatic
fun getDefault(): Config = Config() // 자바에서 Config.getDefault()
@JvmField
val VERSION = "1.0" // 자바에서 Config.VERSION
const val NAME = "MyApp" // 자바에서 Config.NAME (컴파일 타임 상수)
}
}
// 자바에서의 호출
Config.getDefault(); // @JvmStatic 덕분
Config.VERSION; // @JvmField 덕분
Config.NAME; // const val은 자동으로 static final
Config.Companion.getDefault(); // 어노테이션 없이도 이 방법은 가능
object 세 가지 비교 정리
| 구분 | object 선언 | companion object | object expression |
|---|---|---|---|
| 용도 | 싱글턴 | 팩토리, static 대체 | 익명 객체 |
| 인스턴스 수 | 1개 | 1개 (클래스당) | 매번 새로 생성 |
| 위치 | 최상위 또는 클래스 내부 | 클래스 내부만 | 어디서든 |
| 이름 | 있음 | 있거나 없음 | 없음 |
| 인터페이스 구현 | 가능 | 가능 | 가능 |
정리
- object 선언 — 스레드 안전한 싱글턴. lazy 초기화. 생성자 없음
- companion object — 클래스의 동반 객체. 팩토리 패턴에 적합. 인터페이스 구현 가능
- object expression — 익명 객체. 매번 새 인스턴스. 바깥 변수 수정 가능
- static과의 차이 — companion object는 실제 객체이므로 인터페이스 구현, 확장 함수 등이 가능
- ** 자바 호환** —
@JvmStatic,@JvmField,const val로 자바에서 static처럼 접근