Tiny Star
본문 바로가기
Dev Log/Kotlin

[Kotlin] static은 잊어라, Companion Object(동반 객체)의 모든 것

by 별터 2026. 1. 4.

자바에서 코틀린으로 넘어온 개발자들이 가장 당황하는 순간. `"어? static 키워드가 없네? 그럼 전역 상수는 어디에 두지? 정적 메서드는?"`

 

코틀린은 `static`을 없애고 `companion object` (동반 객체)라는 개념을 도입했습니다. 이것은 단순히 이름만 바뀐 것이 아니라, 객체 지향 프로그래밍(OOP)을 더 엄격하게 지키기 위한 코틀린의 철학이 담겨 있습니다. 오늘은 이 동반 객체의 정체와 올바른 사용법을 파헤쳐 봅니다.


1. 정체가 무엇인가? : "이것도 객체다"

자바의 `static` 멤버는 클래스에 붙어있는 단순한 데이터나 함수였습니다. 객체가 아니었죠. 하지만 코틀린의 `companion object`는 이름 그대로 클래스와 동반되는 하나의 객체(Object)입니다.

 

클래스가 메모리에 로드될 때 딱 하나 생성되는 싱글톤(Singleton) 객체입니다. '객체'라는 말은 즉, 다음과 같은 일들이 가능하다는 뜻입니다.

  1. 인터페이스 구현: `companion object : MyInterface { ... }`
  2. 변수에 할당: `val obj = MyClass.Companion`
  3. 함수의 파라미터로 전달: 객체니까 넘길 수 있습니다.
interface Printer {
    fun print()
}

class User {
    // 인터페이스를 구현하는 동반 객체
    companion object : Printer {
        override fun print() = println("User Companion Printing")
    }
}

fun main() {
    // 동반 객체 자체가 Printer의 구현체이므로 바로 실행 가능
    User.print() 
}

2. 올바른 사용법 1: 상수 관리 (const val)

클래스 내부에서 사용할 상수나 전역 상수를 정의할 때 사용합니다. 여기서 중요한 것은 `val`과 `const val`의 차이입니다.

  • `val`: 런타임에 값이 결정될 수 있음. (Getter가 생성됨)
  • `const val`: 컴파일 타임에 값이 박제됨. (진정한 상수, Primitive 타입과 String만 가능)
class Lotto {
    companion object {
        // [추천] 컴파일 타임 상수 (성능 이점)
        private const val MIN_NUMBER = 1
        private const val MAX_NUMBER = 45
        
        // [비추천] 런타임 상수 (굳이 객체 내부에 멤버 변수처럼 존재)
        val DEFAULT_NAME = "Lotto" 
    }
}

3. 올바른 사용법 2: 정적 팩토리 메서드 (Static Factory Method)

생성자(Constructor)는 이름을 가질 수 없습니다. `Lotto(list)`라고만 쓰면 이게 자동 생성인지 수동 생성인지 알기 어렵습니다. `companion object`를 사용하면 이름을 가진 생성 함수를 만들 수 있습니다.

class Car private constructor(val name: String, val position: Int) {
    
    companion object {
        private const val DEFAULT_POSITION = 0

        // 기본 위치에서 시작하는 차 생성
        fun start(name: String): Car {
            return Car(name, DEFAULT_POSITION)
        }

        // 특정 위치에서 시작하는 차 생성 (테스트용 등)
        fun of(name: String, position: Int): Car {
            return Car(name, position)
        }
    }
}

// 사용
val car1 = Car.start("Starter")
val car2 = Car.of("Racer", 5)

 

이렇게 하면 주 생성자를 `private`으로 막아두고, 오직 팩토리 메서드를 통해서만 객체를 생성하도록 강제할 수 있어 객체 생성의 제어권을 확보할 수 있습니다.


4. Java와의 상호운용성 (@JvmStatic)

만약 자바 코드에서 코틀린의 동반 객체를 호출해야 한다면 주의해야 합니다. 기본적으로 자바에서는 `User.Companion.print()` 처럼 `Companion`이라는 내부 객체를 명시해야 접근 가능합니다.

이때 `@JvmStatic` 어노테이션을 붙여주면, 컴파일러가 자바의 진짜 `static` 메서드처럼 바이트코드를 생성해줍니다.

companion object {
    @JvmStatic
    fun doSomething() { ... }
}

이제 자바에서도 `User.doSomething()`으로 깔끔하게 호출할 수 있습니다. 안드로이드 개발이나 자바 라이브러리와 협업할 때 필수적인 팁입니다.


5. 정리

`companion object`를 단순히 `static` 변수 저장소로만 쓰지 마세요. 이 친구는 클래스의 분신과도 같은 객체입니다.

  • `const val`로 효율적인 상수 관리
  • 팩토리 메서드로 명확한 객체 생성 로직 캡슐화
  • 필요하다면 인터페이스 구현까지

이것이 코틀린이 지향하는 진정한 객체지향입니다.