Kotlin Object 키워드를 다루는 방법

코틀린을 배우기 위해서 인프런에서 강의를 구매하고 코틀린과 친해지고 기본기를 다지기 위해서 공부하는 중이다. 글 내용은 Object 키워드를 다루는 방법이고 최태현님의 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) 강의에 소금을 조금 친 내용이다

코틀린에서 object 키워드는 자바에는 없는 독특한 기능을 제공하며, static 멤버, 싱글톤 패턴, 익명 클래스 구현에 활용된다.

static 함수화 변수 (Companion object)

자바와 달리 코틀린에는 static 키워드가 직접 존재하지 않는다. 대신 companion object를 사용하여 클래스의 인스턴스에 종속되지 않는 멤버를 정의한다. companion object는 해당 클래스와 동행하는 유일한 객체이다

  • 유일한 객체: 각 클래스 최대 하나만 존재할 수 있으며, 클래스가 로드될 때 생성된다
  • 이름: 명시적으로 이름을 지정할 수 있으며 (예: companion object Factory), 이름을 지정하지 않으면 기본적으로 Companion 이라는 이름이 부여된다
  • 인터페이스 구현: 다른 객체와 마찬가지로 인터페이스를 구현할 수 있다
  • @JvmStatic: 자바 코드에서 코틀린의 companion object 멤버를 마치 자바의 static 멤버처럼 접근하려면 @JvmStatic 어노테이션을 붙여야 한다
자바 (Person.java)
public class Person {
    private static final int MIN_AGE = 1;

    public static Person newBaby(String name) {
        return new Person(name, MIN_AGE);
    }

    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
코틀린( Person.kt)
// 인터페이스 정의
interface Log {
    fun log()
}

// @JvmStatic 사용 예시 (자바에서 static처럼 접근 가능)
class Person private constructor(
    val name: String,
    val age: Int
) {
    companion object Factory { // companion object에 Factory라는 이름 부여
        private const val MIN_AGE = 1; // 컴파일 시 할당되는 상수

        @JvmStatic // 자바에서 Person.newBaby("이름")으로 직접 호출 가능
        fun newBaby(name: String): Person {
            return Person(name, MIN_AGE)
        }
    }
}

// 인터페이스 구현 예시
class Person private constructor(
    var name: String,
    var age
) {
    companion object Factory: Log { // companion object가 Log 인터페이스 구현
        private const val MIN_AGE = 1;

        fun newBaby(name: String): Person {
            return Person(name, MIN_AGE)
        }
        
        override fun log() {
            println("companion object of Person class")
        }
    }
}
  • const 키워드: const는 컴파일 시점에 값이 할당되는 ‘진짜 상수’를 만들 때 사용한다. 주로 기본 타입(primitive type)과 String에 사용할 수 있으며, companion object 또는 최상위 레벨에서 선언해야 한다. val은 런타임에 값이 할당되는 읽기 전용 변수이다
const와 val의 차이
const (Compile-Time constant)

const는 컴파일 시점에 이미 값을 알고 할당할 수 있는 상수를 선언할 때 사용한다. ‘진짜 상수’라고 불리는 이유가 여기에 있다

  • 컴파일 시점 초기화: 컴파일러가 코드를 기계어로 변환하는 시점에 const 변수의 값이 최종적으로 할당된다. 따라사 런타임에 이 값을 변경할 수 없다
  • 불변성: const로 선언된 변수는 값을 변경할 수 없다 (당연히 상수는 불변)
  • 허용되는 타입: 오직 기본 자료형(Boolean, Char, Byte, Int, Long, Float, Double)과 String 타입에만 사용할 수 있다. 복잡한 객체(클래스의 인스턴스)에는 사용할 수 없다
  • 선언 위치
    • companion object 내부 (클래스 수준의 상수)
    • 최상위 레벨 (파일 수준의 상수)
    • 일반 클래스 내에서는 사용할 수 없다. 일반 클래스 내에서 상수를 만들고 싶다면 val과 lateinit등을 고려해야 한다
  • 성능: 컴파일 시점에 값이 결정되므로, 런타임에 값을 계산하거나 메모리를 할당하는 오버헤드가 없다. 코드가 더 효율적으로 실행될 수 있다
  • 주요 사용처
    • 애플리케이션 전반에서 고정적으로 사용되는 설정 값(API 키, 기본 URL, 버전 정보 등)
    • 수학 상수 (파이 값 등)
    • 특정 상태를 나타내는 문자열 상수
// 최상위 레벨 상수
const val APP_NAME = "My Kotlin App"
const val MAX_ITEMS = 100

class MyClass {
    compainon object {
        // compainon object 내부 상수
        const val DEFAULT_TIMEOUT = 5000 // 밀리초
        const val API_KEY = "api_key"
    }

    // 컴파일 에러 발생: 'const' val is only allowed in top-level or declarations
    // const val ERROR_CODE = 404
}

fun main() {
    println(APP_NAME) // My Kotlin App
    println(MyClass.DEFAULT_TIMEOUT) // 5000
}
val (Read-Only Variable, Run-Time Constant)

val은 런타임에 값이 할당되는 읽기 전용 변수를 선언할 때 사용한다. 한 번 할당되면 변경할 수 없다

  • 런타임 시점 초기화: 프로그램이 실행되는 도중에 값이 결정되고 할당된다. 즉, 코드가 실행되어 해당 val 선언문을 만나는 시점에 초기화된다
  • 불변성: val로 선언된 변수는 한 번 초기화되면 값을 재할당할 수 없다. 하지만 val이 참조하는 객체 내부의 속성은 변경될 수 있다(mutable 객체의 경우)
  • 허용되는 타입: 모든 타입에 사용할 수 있다. 기본 자료형, String, 사용자 정의 클래스 인스턴스 등 제한이 없다
  • 선언 위치: 최상위 레벨, 클래스 속성, 함수 내의 지역 변수 등 모든 곳에서 선언할 수 있다
  • 유연성: 초기화 시점에 복잡한 로직을 통해 값을 계산하거나, 함수 호출의 결과로 값을 할당하는 등 유연하게 사용할 수 있다
  • 주요 사용처
    • 한 번만 초기화되고 그 이후로는 변경되지 않는 모든 종류의 변수
    • 함수의 반환 값, 객체 생성 결과 등 런타임에 결정되는 값
    • 불변성을 유지하여 사이드 이펙트를 줄이고 코드를 안전하게 만들 때
val message: String = "Hello Kotlin" // 런타임에 할당
val currentTimeMillis = System.currentTimeMillis() // 런타임에 함수 호출 결과 할당

class User(val name: String, val age: Int): // 클래스 속성으로 val 사용

fun calculateResult(): Int {
    return (1..100).random() // 런타임에 랜덤 값 할당
}

fun main() {
    println(message) // Hello Kotlin
    println(currentTimeMillis) // 현재 시간 (매번 다르다)


    val user = User("Hyeok", 100)
    println(user.name) // Hyeok
    // user.name = "Kim" // 컴파일 에러: Val cannot be reassigned


    val result = calculateResult()
    println(result) // 1부터 100 사이의 랜덤 값 (매번 다르다)


    val mutableList = mutableListOf("Apple")
    // mutableList = mutableListOf("Banana") // 컴파일 에러: Val cannot be reassigned
    mutableList.add("Banana") // 리스트 내부의 내용은 변경 가능
    println(mutableList) // [Apple, Banana]
}
특징const valval
초기화 시점컴파일 시점 (미리 값을 알아야 함)런타임 시점 (프로그램 실행 중 결정)
값 변경불가능 (항상 상수)불가능 (한 번 할당 후 변경 불가)
허용 타입기본 타입 (Int, String 등)모든 타입 (기본 타입, 객체 등)
선언 위치companion object 또는 최상위 레벨어디든지 (최상위, 클래스 속성, 지역 변수 등)
주요 용도고정적인 상수 값 (API 키, 설정 값 등)한 번 할당 후 변하느 않는 모든 읽기 전용 변수
  • 정말로 변하지 않는 고정된 값(예: 설정 값, 고유 식별자 문자열)이라면 const val을 사용하여 컴파일 시점에 최적화하고 명확성을 높이는 것이 좋다
  • 런타임에 결정되지만 한 번 할당되면 변하지 않는 값(예: 객체 인스턴스, 함수 결과)이라면 val을 사용하는 것이 좋다

싱글톤 패턴

  • 싱글톤 패턴은 애플리케이션 내에서 특정 클래스의 인스턴스를 하나만 생성하고, 어디서든 그 인스턴스에 접근할 수 있도록 하는 디자인 패턴이다. 데이터베이스 커넥션 풀과 같이 자원 관리나 전역 상태 관리에 유용하게 사용된다
  • 코틀린에서는 object 키워드를 사용하여 매우 간결하게 싱글톤을 구현할 수 있다
자바 (Singleton.java)
public class Singleton {
    // 클래스 로딩 시 즉시 인스턴스 생성 (Eager Initialization)
    // Static final로 불변성과 단일성 보장
    private static final Singleton INSTANCE = new Singleton();

    // private 생성자로 외부에서 new 키워드를 통한 인스턴스 생성 차단
    private Singleton() {}

    // 유인한 인스턴스에 접근할 수 있는 전역 접근점 제공
    public static Singleton getInstance() {
        return INSTANCE;
    }
}
코틀린 (Singleton.kt)
// 싱글톤 객체 선언 (인스턴스 및 초기화 코드 포함)
object Singleton {
    var a: Int = 0 // 멤버 변수를 가질 수 있다
}

fun main() {
    println(Singleton.a) // 인스턴스 생성 없이 직접 접근
    Singleton.a += 10
    println(Singleton.a)
}
  • 코틀린의 object 선언은 자바의 싱글톤 구현에 필요한 복잡한 과정을 (private 생성자, static 인스턴스, static getter 등) 한 번에 처리한다. object 선언된 객체는 프로그램 전체에서 단 하나의 인스턴스만 존재하며, 해당 객체의 멤버로 바로 접근할 수 있다

익명 클래스 (Object Expression)

  • 익명 클래스는 이름 없이 한 번만 사용되는 클래스이다. 특정 인터페이스나 클래스를 상속받아 일회성으로 구현해야 할 때 유용하다
자바 (Movable.java, Main.java)
public interface Movable {
    void move();
    void fly();
}

public class Main {
    public static void main(String[] args) {
        moveSomething(new Movable() { // new Movable()로 익명 클래스 생성
            @Override
            public void move() {
                System.out.println("move");
            }

            @Override
            public void fly() {
                System.out.println("fly");
            }
        }
    }

    private static void moveSomething(Movable movable) {
        movable.move();
        movable.fly();
    }
}
코틀린 (main.kt)
fun main() {
    moveSomething(object: Movable { // object: Movable로 익명 클래스 생성
        override fun move() {
            println("move")
        }

        override fun fly() {
            println("fly")
        }
    }
}

private fun moveSomething(movable: Movable) {
    movable.move();
    movable.fly();
}
  • 코틀린에서는 object: 인터페이스 명 { … } 또는 object 클래스명() { … } 형태로 익명 클래스를 생성한다. 이 방식을 객체 표현식 (Object Expression)이라고 부른다. 자바의 익명 클래스보다 더 간결하고 직관적인 문법을 제공한다

출처 – 인프런 강의 중 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)