본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 16. kotlin JVM annotation 알아보기 2편

by 사바라다 2022. 1. 26.

안녕하세요. 오늘은 이전 시간에 이어서 JVM annotation에 대해서 알아보는 시간을 가져보도록 하겠습니다. 오늘 알아볼 코틀린의 JVM annotation은 이전시간에 알아보지못한 @JvmOverloads, @JvmDefault, @Throws, @JvmWildcards 등입니다.

@JvmOverloads

코틀린과 자바는 100% 호환 된다고 합니다. 하지만 몇몇 부분에 있어서는 상호 호환이 되지 않는 부분이 있습니다. 그 중 대표적인 하나가 코틀린의 default 값에 대해서 자바에서 호출하지 못한다는 것입니다. 아래 예제 코드를 한번 보면서 좀 더 들여다 보도록 하겠습니다. 코틀린의 함수를 아래처럼 선언하면 이는 아래의 3가지 방법으로 모두 접근이 가능합니다. 코틀린에서 default 파라미터에 대해서 이러한 접근을 허용하고 있습니다.

  • findVersion("sender")
  • findVersion("sender", "type")
  • findVersion("sender", "type", 50)
object VersionService {

    fun findVersion(sender : String, type : String = "text", maxResults : Int = 10) : List<Long> {
        return emptyList()
    }
}

하지만 자바로 디컴파일 했을 경우에는 findVersion 하나의 메서드만 보이는 것을 확인할 수 있습니다. 이말인 즉슨 만약 자바에서 코틀린 함수를 접근하려고 하면 findVersion("sender", "type", 50) 로 밖에 접근할 수 없다는 뜻입니다.

@NotNull
public final List findVersion(@NotNull String sender, @NotNull String type, int maxResults) {
    Intrinsics.checkNotNullParameter(sender, "sender");
    Intrinsics.checkNotNullParameter(type, "type");
    return CollectionsKt.emptyList();
}

이러한 경우에 @JvmOverloads을 메서드의 어노테이션으로 아래의 코드처럼 넣어주면 오버로딩 함수들이 모두 만들어 지게 되어 자바에서도 무리없이 코틀린 함수를 모두 호출할 수 있게 됩니다.

object VersionService {

    @JvmOverloads
    fun findVersion(sender : String, type : String = "text", maxResults : Int = 10) : List<Long> {
        return emptyList()
    }
}

아래 코드가 컴파일시 추가적으로 생성된 자바 메서드입니다.

@JvmOverloads
@NotNull
public final List findVersion(@NotNull String sender, @NotNull String type) {
    return findVersion$default(this, sender, type, 0, 4, (Object)null);
}

@JvmOverloads
@NotNull
public final List findVersion(@NotNull String sender) {
    return findVersion$default(this, sender, (String)null, 0, 6, (Object)null);
}

@JvmDefault

JvmDefault 어노테이션은 코틀린을 사용하면서 JVM 7 버전이하를 사용할때도 interface에 default 메서드를 사용할 수 있게 해주는 어노테이션입니다. 이제는 자바 17이 나온 시점이고 대부분 Java 8을 사용하기 때문에 이 부분은 넘어가도록 하겠습니다. 구현하는 방식에 대해서 간단히 알려드리면 JVM 7 에서 코틀린을 사용할 때 interface의 default 메서드는 static inner class를 이용하여 구현합니다.

@Throws

자바와 코틀린의 차이점 중 또 다른 하나는 코틀린에서 모든 예외(Exception)들은 unChecked 라는 것이라는 부분입니다. 그렇기 때문에 try-catch 가 필수인 경우는 없습니다. 하지만 자바의 경우 IOException와 같이 checked exception은 필수적으로 try-catch가 필요한데요. 이러한 차이를 매꿔주는 JVM Annotation으로 @Throws가 있습니다. 아래의 코틀린 코드를 보도록 하겠습니다.

fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Message> {
    if(sender.isEmpty()) {
        throw IllegalArgumentException()
    }
    return ArrayList()
}

반드시 해당 함수를 사용하는 자바 코드에서 IllegalArgumentException를 예외처리하는 것을 의도할 수 있습니다. 이럴 경우에 @Throws를 아래와 같이 붙이면 됩니다.

@Throws(IllegalArgumentException::class)
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Long> {
    if(sender.isEmpty()) {
        throw IllegalArgumentException()
    }
    return ArrayList()
}

그러면 아래와 같은 java code가 나오게 됩니다. 코드를 잘 보시면 메서드 파라미터 뒤에 throws가 붙은 것을 확인하실 수 있습니다. 이렇게 되면 해당 메서드를 사용자는 반드시 try-catch를 사용해야하게 됩니다.

@NotNull
public static final List findMessages(@NotNull String sender, @NotNull String type, int maxResults) throws IllegalArgumentException {
    Intrinsics.checkNotNullParameter(sender, "sender");
    Intrinsics.checkNotNullParameter(type, "type");
    CharSequence var3 = (CharSequence)sender;
    boolean var4 = false;
    if (var3.length() == 0) {
        throw (Throwable)(new IllegalArgumentException());
    } else {
        return (List)(new ArrayList());
    }
}

안타깝게도 kotlin에서 해당 함수를 사용한다고 해서 반드시 try-catch를 쓰는 checked Exception 처럼 사용하게 할 수는 없습니다.

@JvmWildcard and @JvmSuppressWildcards

코틀린 제네릭과 자바의 제네릭은 일반적으로는 똑같아 보이지만 다른점이 있습니다. 바로 다형성에 관한내용인데요. 자바의 제네릭은 기본적으로 캐스팅을 허용하지 않습니다. 따라서 사용할 때 extend를 명시적으로 선언해야합니다. 와일드 카드와 함께 말이죠. 아래의 코드를 한번 보도록 하겠습니다.

List<Number> numberList = new ArrayList<Integer>(); // 컴파일 에러
List<? extends Number> numberList = new ArrayList<Integer>(); // OK

코틀린의 경우를 한번 보도록 하겠습니다. 코틀린은 아래와 같이 별도의 extends를 명시하지 않아도 컴파일 에러를 일으키지 않습니다.

val numberList : List<Number> = ArrayList<Int>() // OK

그 이유는 자바 바이트코드가 될 때 상속이 불가능한 final class가 아니면 필요한 경우에 따라서 자동으로 extends를 명시해주기 때문입니다.

제네릭을 사용한 함수를 보도록 하겠습니다. 아래의 코드는 Number 타입의 List를 받아서 또 다른 Number 타입의 List를 반환하는 함수입니다.

fun transformList(list : List<Number>) : List<Number>

코틀린의 이러한 함수는 자바로 보면 아래의 메서드와 동일합니다. 보시면 아시겠지만 파리미터의 경우 extends가 자동으로 붙은것을 확인할 수 있습니다. 그리고 return 타입의 경우는 extends를 포함하지 않습니다. 코틀린은 기본적으로 이러한 매커니즘을 가지고 있습니다.

public List<Number> transformList(List<? extends Number> list)

이러한 기본적인 매커니즘을 변경할 수 있는 방법이 JvmWildcard 과 JvmSuppressWildcards 어노테이션입니다. 해당 어노테이션을 이용하면 파라미터에 extends를 뺄 수 있고 return 타입에 extends를 추가할 수 있습니다. 아래 코드를 보도록 하겠습니다.

fun transformList(list : List<@JvmSuppressWildcards Number>) : List<@JvmWildcard Number>

위 코틀린 코드는 아래와 같은 자바 코드로 변환됩니다.

public List<? extends Number> transformListInverseWildcards(List<Number> list)

마무리

이번 포스팅까지해서 코틀린에서 사용하는 JVM annotation에 대해서 알아보는 시간을 가져보았습니다.

감사합니다.

참조

baeldung_jvm-annotations

댓글