티스토리 뷰

자바에서 제네릭을 이용할때 가장 불편한 부분은 타입 소거로 인해 타입 토큰을 전달해야하는 점이다.

// 런타임에 T 정보를 알 수 없으므로 이렇게 작성하면 컴파일 에러가 발생한다
public <T> T create() throws Exception {           
    return T.class.getConstructor().newInstance();
}                                                 

// 타입 파라미터와 별개로 타입 토큰을 전달해야한다
public <T> T create(Class<T> clazz) throws Exception { 
    return clazz.getConstructor().newInstance();       
}

 

코틀린은 inline + reified 키워드를 이용해 이런 부분을 개선했다.

// 두 코드 모두 정상적으로 컴파일되고, 실행된다
inline fun <reified T> create(): T {
    return T::class.java.getConstructor().newInstance()
}

inline fun <reified T> create(): T {
    return T::class.constructors.first { it.parameters.isEmpty() }.call()
}

 

그래서 코틀린으로 작성된 코드들은 제네릭을 사용하더라도 타입 토큰을 직접 전달하는 경우가 거의 없다. 다만 코드를 작성하다보면 이런 문제가 생길때가 있다.

class SomeClass(
    private val someProperty: String,
) {
    inline fun <reified T> create(): T {
        println(someProperty)
        return T::class.constructors.first { it.parameters.isEmpty() }.call()
    }
}

이런 형태로 inline 으로 공개하고 싶은 메서드가 내부에서 private 프로퍼티 혹은 private 메서드를 호출하는 경우다. inline 키워드는 해당 메서드를 호출하는 클라이언트에 직접 코드가 복사되게 되는데, 이렇게 되면 컴파일 된 결과물에선 외부에서 private 프로퍼티에 접근하게 되기 때문에 컴파일이 되지 않는다. 이럴땐 어쩔 수 없이 inline을 포기하고 자바처럼 타입 토큰을 전달하거나 inline을 유지하고 싶다면 프로퍼티의 접근 제어자를 변경해주는 방법이 있다.

class SomeClass(
    val someProperty: String,
) {
    inline fun <reified T> create(): T {
        println(someProperty)
        return T::class.constructors.first { it.parameters.isEmpty() }.call()
    }
}

class SomeClass(
    private val someProperty: String,
) {
    fun <T : Any> create(clazz: KClass<T>): T {
        println(someProperty)
        return clazz.constructors.first { it.parameters.isEmpty() }.call()
    }
}

물론 둘 다 썩 마음에 드는 방법은 아니다. 이럴때 권장하는 방법은 확장함수를 제공하는 것이다.

class SomeClass(
    private val someProperty: String,
) {
    fun <T : Any> create(clazz: KClass<T>): T {
        println(someProperty)
        return clazz.constructors.first { it.parameters.isEmpty() }.call()
    }
}

inline fun <reified T : Any> SomeClass.create(): T {
    return this.create(T::class)
}

이 방식은 실제로 많은 코틀린 라이브러리에서 사용하는 방식이고, 기존 자바 라이브러리들이 코틀린용 모듈을 제공할때 사용하는 방식이다. 두번째 방식은 @PublishedApi 애노테이션을 사용하는 방식이다.

class SomeClass(
    @PublishedApi
    internal val someProperty: String,
) {
    inline fun <reified T> create(): T {
        println(someProperty)
        return T::class.constructors.first { it.parameters.isEmpty() }.call()
    }
}

private 이었던 프로퍼티의 접근제어자를 internal 로 변경하고, @PublishedApi 를 붙여주면 된다. 이렇게하면 외부 모듈에서 create() 메서드를 호출할 수 있다. 다만 이 방식은 공개 라이브러리를 만드는 경우 바이너리 호환성 문제가 발생할 수 있다. 또한 처음 설계의도가 internal 이면 모르겠지만 여전히 타입때문에 접근제어자를 변경해야하는 점, 변경하고나면 모듈 내에선 public 과 마찬가지가 되는 부분 때문에 이 방식보다는 확장함수를 이용하는 방식이 더 낫다고 생각한다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/12   »
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 31
글 보관함