코틀린에는 static 키워드가 없다. 대신 objectcompanion object가 그 역할을 한다. 하지만 단순히 "static 대체"라고만 이해하면 부족합니다. object는 싱글턴을 만들고, companion object는 팩토리 패턴을 구현하고, object expression은 익명 객체를 만든다. 각각이 어떻게 다른지 정리해보자.

object 선언 — 언어 차원의 싱글턴

자바에서 싱글턴을 만들려면 private 생성자, static 인스턴스, double-checked locking 같은 보일러플레이트가 필요했다. 코틀린은 object 키워드 하나로 해결한다.

KOTLIN
object DatabaseConfig {
    val url = "jdbc:mysql://localhost:3306/mydb"
    val maxPool = 10

    fun connect() {
        println("데이터베이스 연결: $url")
    }
}

// 사용 — 클래스 이름으로 직접 접근
DatabaseConfig.connect()
println(DatabaseConfig.url)

object 선언의 특징

KOTLIN
object Logger {
    init {
        println("Logger 초기화")  // 처음 접근할 때 실행
    }

    fun log(message: String) = println("[LOG] $message")
}

// Logger가 처음 사용될 때 init 블록이 실행됨 (lazy 초기화)
Logger.log("시작")
  • 스레드 안전 — JVM이 클래스 로딩 메커니즘으로 보장
  • lazy 초기화 — 처음 접근할 때 생성
  • ** 생성자 없음** — 직접 인스턴스를 만들 수 없음
  • ** 상속 가능** — 클래스나 인터페이스를 상속/구현할 수 있음

인터페이스 구현하는 object

KOTLIN
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 멤버처럼 사용하지만, 실제로는 객체 인스턴스다.

KOTLIN
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의 핵심 장점 — 팩토리 패턴

생성자 대신 팩토리 메서드를 사용하면 이런 장점이 있다.

  1. 이름이 있다fromEmail(), fromName()처럼 의도를 표현
  2. ** 반환 타입이 유연하다** — 하위 타입을 반환할 수 있음
  3. ** 캐싱이 가능하다** — 매번 새 인스턴스를 만들지 않아도 됨
KOTLIN
// 하위 타입 반환 예시
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에 이름 붙이기

KOTLIN
class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

// 두 가지 방법으로 호출 가능
MyClass.create()           // 이름 생략
MyClass.Factory.create()   // 이름 사용

이름을 생략하면 기본 이름은 Companion이 된다.

companion object의 인터페이스 구현

이것이 자바의 static과 결정적으로 다른 점이다. companion object는 실제 객체이므로 인터페이스를 구현할 수 있다.

KOTLIN
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의 확장 함수

KOTLIN
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 선언(싱글턴)과 달리 매번 새 인스턴스를 만든다.

KOTLIN
// 인터페이스의 익명 구현
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은 바깥 변수를 수정할 수 있다.

KOTLIN
fun countClicks(button: Button): Int {
    var clickCount = 0   // 바깥 변수

    button.setOnClickListener(object : OnClickListener {
        override fun onClick() {
            clickCount++  // 바깥 변수 수정 가능! (자바에서는 불가)
        }
    })

    return clickCount
}

SAM 변환과의 차이

자바 인터페이스가 메서드 하나만 가지면 람다로 대체할 수 있다(SAM 변환). 하지만 메서드가 여러 개이거나 코틀린 인터페이스이면 object expression을 써야 한다.

KOTLIN
// 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의 핵심 차이는 세 가지입니다.

  1. companion object는 실제 객체다 — 인터페이스 구현, 확장 함수 정의 가능
  2. ** 상속 계층에 참여한다** — 인터페이스를 구현하고 다형적으로 사용 가능
  3. ** 하나의 인스턴스만 존재한다** — static 멤버의 모음이 아니라 싱글턴 객체

@JvmStatic과 @JvmField — 자바 호환

KOTLIN
class Config {
    companion object {
        @JvmStatic
        fun getDefault(): Config = Config()  // 자바에서 Config.getDefault()

        @JvmField
        val VERSION = "1.0"                  // 자바에서 Config.VERSION

        const val NAME = "MyApp"             // 자바에서 Config.NAME (컴파일 타임 상수)
    }
}
JAVA
// 자바에서의 호출
Config.getDefault();          // @JvmStatic 덕분
Config.VERSION;               // @JvmField 덕분
Config.NAME;                  // const val은 자동으로 static final
Config.Companion.getDefault(); // 어노테이션 없이도 이 방법은 가능

object 세 가지 비교 정리

구분object 선언companion objectobject expression
용도싱글턴팩토리, static 대체익명 객체
인스턴스 수1개1개 (클래스당)매번 새로 생성
위치최상위 또는 클래스 내부클래스 내부만어디서든
이름있음있거나 없음없음
인터페이스 구현가능가능가능

정리

  • object 선언 — 스레드 안전한 싱글턴. lazy 초기화. 생성자 없음
  • companion object — 클래스의 동반 객체. 팩토리 패턴에 적합. 인터페이스 구현 가능
  • object expression — 익명 객체. 매번 새 인스턴스. 바깥 변수 수정 가능
  • static과의 차이 — companion object는 실제 객체이므로 인터페이스 구현, 확장 함수 등이 가능
  • ** 자바 호환** — @JvmStatic, @JvmField, const val로 자바에서 static처럼 접근
댓글 로딩 중...