티스토리 뷰

 

이 글에서 조금 더 나아간 버전이다.

2024.02.01 - [개발/Kotlin] - 코틀린에서 static class 사용하기 : companion object - 자바와 다른 점 3

 

Utility 클래스 용으로 static을 사용하기 위해서 companion object를 쓰다가 확장함수까지 알게되서 정리한 글이었고, 이번엔 object를 걸 알게되서 다시 작성해본다.

 

아래에 작성한 내용들은 내가 사용하면서 느낀 점들이 조금씩 녹아있어서 오피셜한 내용을 파악하고 싶으면 코틀린 공식 문서를 보는 것을 추천한다. https://kotlinlang.org/docs/object-declarations.html#data-objects

 

목차를 따고 들어가면 다음 순서로 정리된다.

 

1. compaion object

2. object

3. 확장 함수

4. top-level 함수

 

하나씩 정리해보자

companion object

그동안 자바의 static에 가장 가깝다 생각해서 가장 유틸 클래스로 많이 사용했던 방식이다. 사실 코틀린에서 제공하는 기능들 덕분에 자바에서 static 썼듯이 쓴다면 차이를 느끼기 어렵다.

class CommonUtils {
    companion object {
        fun convertToUtc(...) = ... 
    }
}

CommonUtils.convertToUtc() // ✅ OK

// 내부 구현
public final class CommonUtils {
    public static final CommonUtils.Companion Companion = new CommonUtils.Companion();

    public static final class Companion {
        public LocalDateTime convertToUtc(...) { ... }
    }
}

하지만 위 코드를 보면 알 수 있는게, 객체 자체가 static으로 선언된건 맞다. 다만 CommonUtils 라는 클래스 안에 정적 멤버로 선언된 객체라는 것이다.

 

companion object의  함수를 쓰는 방식을 정리해보면 클래스 안에 static으로 선언된 멤버 객체를 호출해서 맴버 객체 내부의  함수를 쓰게 되는 것이다.

 

당연히 companion object 내부의  함수 동작은 자바의 static처럼 동작하지 않는다. 코틀린에서 선언한 companion object를 자바에서 사용해보면 이 차이를 더 잘 느낄 수 있다. 

class Config {
    companion object {
        const val VERSION = "1.0.0"
    }
}

val version = Config.VERSION // 코틀린에서 제공하는 단축 접근 방식 ✅ OK
val version = Config.Companion.VERSION // 실제 동작하는 방식을 표현 ✅ OK

String version = Config.VERSION; // 그래서 자바에서 단축 접근을 사용하면 ❌ 컴파일 오류

Config 클래스안에 Companion 이라는 객체가 생성되지만 코틀린이 코드 슈거링으로 Config.Version을 사용가능하게 만들어 준 것이다. 그런데 이 슈거링이 또 웃긴게  함수에선 적용되지 않는다.

class Config {
    companion object {
        fun sayHi() = println("hi")
    }
}

val x = Config.sayHi() // ❌ 오류 (함수는 자동 위임 X)
val x = Config.Companion.sayHi() // ✅ OK

@JvmStatic
fun sayHi() = println("hi") // ✅ OK

companion object 안에서 static과 동일한 동작으로 사용하고 싶다면 @JvmStatic 어노테이션을 사용하면 된다. 변수에도 똑같이 적용된다.

 

object

object의 경우 companion object와 비슷하지만, object는 선언한 객체가 싱글톤임을 보장한다. companion object도 싱글톤을 보장하긴 하지만, companion object가 선언된 객체가 싱글톤임을 보장하는게 아니라 그냥 자기 자신이 싱글톤(클래스 내의 정적 변수로 생성되니까..)이다. 객체 지향 개념이 익숙하지 않으면 어려울 수 있다. 그렇다면 object로 선언된 코틀린 코드를 자바로 보면 편하다.

// 이런 코틀린 코드가 있을 때
object Logger {
    const val TAG = "MyApp"

    fun log(message: String) {
        println("[$TAG] $message")
    }
}

// 이렇게 자바코드로 변환된다.
public final class Logger {
    // 싱글턴 인스턴스 (static final)
    public static final Logger INSTANCE;

    static {
        INSTANCE = new Logger();
    }

    public static final String TAG = "MyApp";

    // private 생성자 (외부에서 생성 불가)
    private Logger() {}

    public final void log(String message) {
        System.out.println("[" + TAG + "] " + message);
    }
}

엄밀히 말하면, object도 companion object처럼 클래스에 직접 static이 붙는 건 아니다. 다만 object는 클래스 자체가 static 객체처럼 동작하며, 완전한 싱글톤임이 언어 차원에서 보장된다. 사용법은 companion object와 유사하지만, 중간에 인스턴스명(예: Companion)을 넣는 방식은 불가능하다.

fun main() {
    println(Logger.TAG)          // ✅ OK - const val은 바로 접근 가능
    Logger.log("Hello!")         // ✅ OK - object의 멤버 함수 호출

    println(Logger.INSTANCE.TAG) // ❌ 오류 - Kotlin에선 INSTANCE 접근 불가
}

✅ Java에서 사용할 경우
System.out.println(Logger.TAG);               // ✅ const val → static final 필드
Logger.INSTANCE.log("Hello from Java");       // ✅ INSTANCE 통해 싱글턴 메서드 호출

정리하고보니 더 큰차이가 없는 것 같다. 코틀린에서 싱글톤 객체의 인스턴스에 접근이 가능하고 안하고의 차이 정도다. object에서도 마찬가지로 @JvmStatic 어노테이션을 사용하면 자바에서의 static 처럼 사용 가능하다.

 

확장함수

확장 함수는 기존 클래스에 클래스 외부에서 새로운 기능을 추가할 때 사용된다. 클래스 외부에서 선언해도 특정 클래스의 멤버처럼 사용할 수 있도록 해 준다.

fun String.toUtcFormat(): String {
    return "Converted $this to UTC"
}

fun main() {
    val date = "2024-03-28"
    println(date.toUtcFormat()) // ✅ "Converted 2024-03-28 to UTC"
}

이번 예시에는 약간 예외되는 상황 같은데... 글로벌 느낌으로 함수를 사용하기 위해 static이 언급되는데 확장함수를 안넣기에는 애매해서 넣어봤다. 위 코드를 자바로 변환하면 다음과 같다.

public final class ExtensionsKt {
    public static final String toUtcFormat(String $this$toUtcFormat) {
        return "Converted " + $this$toUtcFormat + " to UTC";
    }
}

변환시켜보면 확장 함수는 기존 클래스의 멤버가 아니라, 단순한 static 함수이다. 이건 좀 생각지 못했던 것 같다.

 

top-level  함수

마지막으로 top-level 함수이다. 그냥 모든 스코프 밖에다가 함수나 const가 붙은 변수를 선언하면 된다. 이러면, 자동으로 static으로 선언된다... 어쩌면 고민하지 않고 자바처럼 사용할만한 가장 쉬운 방법이긴 하다.

const val APP_NAME = "MyApp"

fun log(message: String) {
    println("[$APP_NAME] $message")
}

fun main() {
    log("Hello, World!") // ✅ OK
}

이 코드를 자바로 변환하면 다음과 같다.

public final class UtilsKt {
    public static final String APP_NAME = "MyApp";

    public static final void log(@NotNull String message) {
        System.out.println("[" + APP_NAME + "] " + message);
    }
}

늘 그렇지만 정답은 언제나 가까운 곳에 있을지도 모른다...는 생각이 들었다.

마치며

내 결론은 object를 사용한 방식이지만, 상황에 맞게 사용하면 될 것 같다.

 

그냥 쉬운거 쓸래 : 확장 함수  & top-level 함수

코틀린만 쓰는 게 아니라 Java에서도 같이 써야 해 : companion object

완전한 싱글톤으로 쓰고 싶어  : object

 

이렇게 정리하면 되지 싶다. 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함