본문 바로가기

[Kotlin] Kotlin @annotation 정리

Kotlin 자주 사용하는 @annotation 정리

Annotations

  • @JvmName : JAVA에서 호출되는 Kotlin의 함수, 변수, 파일명을 Renamed(변경)
  • @JvmMultifileClass : 여러 파일, 클래스들이 같은 이름으로 Renamed 될 경우 선언해서 같은 이름으로 사용
  • @JvmStatic : static 변수의 Getter/Setter 함수를 자동으로 생성하라는 애노테이션, 즉 static Getter/Setter 
  • @JvmField : Getter/Setter를 자동으로 생성하지 말라는 애노테이션
  • @Throws : 해당 코틀린 함수가 예외를 던질 수 있다는 의미의 애노테이션
  • @JvmOverloads
     : 인자의 기본값(Default Value)이 없는 Java를 위해, 오버로딩 메서드를 자동으로 생성하라는 애노테이션

@JvmName

@JvmName은 코틀린을 바이트코드로 변환할 때 JVM 시그니쳐를 변경하는 용도로 사용합니다
즉, JAVA에서 호출되는
코틀린 함수 파일의 이름을 변경하는 용도의 의미입니다

아래코드는 같은 이름을 가진 함수의 다형성을 볼 수 있습니다.
(kotlin에선 파라미터 List의 제네릭타입이 다르므로, 다른 파라미터의 함수로 인식하고 다형성을 보여줍니다)

// foo.kt - Kotlin 파일명

// Compile Error
fun foo(a : List<String>) {
    println("foo(a : List<String>")
}

fun foo(a : List<Int>) {
    println("foo(a : List<Int>")
}


: 두
foo() 함수는 바이트코드로 변경될 때 인자가 List<>이기 때문에 시그니쳐(파라미터)가 동일합니다. 

때문에 컴파일하려고 하면 에러를 발생하게 됩니다.

Error:(7, 1) Kotlin: Platform declaration clash: The following declarations have the same JVM signature (foo(Ljava/util/List;)V):
    fun foo(a: List<Int>): Unit defined in foo.main.kotlin in file kotlin.kt
    fun foo(a: List<String>): Unit defined in foo.main.kotlin in file kotlin.kt

List의 Generic은 구별되지 않기 때문에 두 함수의 Signature는 동일하게 여겨집니다

@JvmName 애노테이션을 사용해서 두 함수의 Signature를 변경할 수 있습니다.

(애노테이션으로 두 함수의 이름을 서로 다르게 변경하는 뜻입니다)

// foo.kt - kotlin 파일명

@JvmName("fooString")
fun foo(a : List<String>) {
    println("foo(a : List<String>")
}

@JvmName("fooInt")
fun foo(a : List<Int>) {
    println("foo(a : List<Int>")
}

두 함수는 fooString(), fooInt() 함수로 이름이 변경되어 Compile되기 때문에 서로 다른 함수로 여겨집니다

위 함수를 Java로 변환해서 보면 아래의 형태로 구성됩니다

static(정적) 함수로 선언된 이유는 Kotlin에서 Top Level 함수로 정의되었기 때문입니다

// foo(a : List<String>) 함수
public static void fooString(@NotNull List a) {
    String var1 = "foo(a : List<String>)";
    System.out.println(var1);
}

// foo(a : List<Int>) 함수
public static void fooInt(@NotNull List a) {
    String var1 = "foo(a : List<Int>)";
    System.out.println(var1);
}

: 위 코드를 코틀린에서 사용할 때는 @JvmName으로 Renamed된 함수명이 아닌 원래의 함수이름을 사용해야 합니다

Java / Kotlin 각각 사용예시를 보겠습니다

  • JAVA

     : FooKt는 해당함수가 선언된 Kotlin 파일명입니다 (foo.kt)
List<String> listString = new ArrayList();
listString.add("foo");
listString.add("bar");
FooKt.fooString(listString);	// foo(a : List<String>) 함수
        
List<Integer> listInt = new ArrayList<>();
listInt.add(1);
listInt.add(2);
FooKt.fooInt(listInt);		// foo(a : List<Int>) 함수
  • Kotlin

      : foo()함수를 다형성의 형태로 바로 사용이 가능
val listString = listOf<String>("foo","bar")
foo(listString)			// foo(a : List<String>) 함수
        
val listInt = listOf<Int>(1,2)
foo(listInt)			// foo(a : List<Int>) 함수

: Java에서 사용하는 예시를 보면 FooKt.fooString() 형식으로 사용하고 있습니다.

FooKt ? 사용하려는 Kotlin 파일을 의미합니다 함수가 선언되어있는 Kotlin파일의 이름이 foo.Kt 입니다

Kotlin에서 함수를 사용하려면 파일명 생략하고 정적함수로 바로 사용할 수 있지만 Java에서 사용하려면 해당 함수가 선언되어있는 파일을 통해 접근해야하기 때문입니다.

이때 파일명도 @JvmName으로 Custom하게 변경해서 Java에서 사용 할 수 있습니다

// foo.kt -> bar 파일명 변경
@file:JvmName("bar")

@JvmName("fooString")
fun foo(a : List<String>) {
    println("foo(a : List<String>")
}

@JvmName("fooInt")
fun foo(a : List<Int>) {
    println("foo(a : List<Int>")
}

: Java에서 접근하기 위한 파일명을 FooKt -> bar로 변경하였습니다

아래처럼 변경된 Kotlin 파일명으로 접근해서 함수를 사용하게 됩니다

List<String> listString = new ArrayList();
listString.add("foo");
listString.add("bar");
bar.fooString(listString);	// foo(a : List<String>) 함수
        
List<Integer> listInt = new ArrayList<>();
listInt.add(1);
listInt.add(2);
bar.fooInt(listInt);		// foo(a : List<Int>) 함수

정리

  • @JvmName은 Kotlin의 함수 or 파일을 Java에서 사용할 때 이름을 Renamed 하는 역할
  • Kotlin에서 해당 함수를 사용할 때는 @JvmName의 이름이 아닌 원래의 이름으로 사용

    :
    Java는 변경된 이름으로 호출 / Kotlin은 원래의 이름으로 호출

@JvmMultifileClass

위의 예시처럼 @file:JvmName()으로 여러 Kotlin파일들이 동일한 이름으로 Renamed되었을 때 사용하는 애노테이션입니다

아래는 각각 하나의 Top-level class와 Top-level 메서드를 포함한 두 Kotlin파일입니다  

  • oldUtil.kt
@file:JvmName("Test")
@file:JvmMultifileClass

fun getTime() {
    ...
}
  • newUtil.kt
@file:JvmName("Test")
@file:JvmMultifileClass

fun getDate() {
    ...
}


: old와 new의 두 Kotlin들을 모두
Test의 이름으로 Renamed해서 Java에서는 같은 파일명으로
서로 다른 두 파일의 접근이 가능하게 됩니다

Test.getTime() // oldUtil.kt의 메서드
Test.getDate() // newUtil.kt의 메서드

@JvmStatic

@JvmStatic은 static 변수의 Getter Setter 함수를 자동으로 만들라는 의미입니다.

예시로 barName의 전역변수(클래스변수)를 갖는 Bar클래스를 만들겠습니다

(companion object에 선언함으로 전역변수로 선언)

class Bar {
    companion object {
        var barName : String = "bar"
    }
}


:
companion object는 Java의 static과 다른 개념입니다.

이해를 돕기 위해 Java로 변경된 구성을 보겠습니다

public final class Bar {
     private static String barName;
     
     public static final class Companion {
         public final int getBarName() {
             return Bar.barName;
         }
         public final void setBarName(String var1) {
             Bar.barName = var1;
         }
     }
}

: 코드를 보면 barName은 전역변수(클래스변수)로 선언되었지만, Getter/Setter 함수는 Companion 클래스에 내부에 선언되어 사용하려면 Companion을 통해서만 사용이 가능합니다

Bar클래스의 전역변수인 barName의 Getter/Setter를 Companion을 통해서만 접근이 가능합니다

Bar.Companion.getBarName();		// Getter
Bar.Companion.setBarName("foo");	// Setter

@JvmStatic은 barName 전역변수의 바로 밑에 Getter/Setter를 생성하게끔 만들어줍니다

class Bar {
    companion object {
        @JvmStatic var barName : String = "bar"
    }
}

이해를 돕기 위해 Java로 변경된 구조를 보겠습니다

barName 전역변수 밑에 Getter/Setter의 정적(static)함수가 생겼고, Companion의 구조는 이전과 동일합니다

public final class Bar {
     private static String barName;
     
     public static final int getBarName() {
         return barName;
     }
     
     public static final void setBarName(String var0) {
         barName = var0;
     }
     
     public static final class Companion {
         public final int getBarName() {
             return Bar.barName;
         }
         public final void setBarName(String var1) {
             Bar.barName = var1;
         }
     }
}


이제 barName의 전역변수에 접근하는 방법은 Companion을 통해서도 가능하고 정적함수로 바로 접근도 가능

Bar.getBarName();			// 정적 Getter 함수
Bar.setBarName("test");			// 정적 Setter 함수


Bar.Companion.getBarName();		// Getter
Bar.Companion.setBarName("foo");	// Setter

정리

  • @JvmStatic은 전역변수의 Getter Setter를 정적함수(static)로 설정 
  • 전역메서드에 사용할 경우 클래스의 정적메서드로 설정

@JvmField

@JvmFieldGetter  Setter를 생성하지 말라는 의미의 annotation입니다

아래 예시에서 Kotlin은 클래스의 프로퍼티(barName)에 Getter/Setter 함수를 자동으로 생성해줍니다

class Bar {
   var barName: String = "bar"
}


이해를 쉽게 하기위해 위 코드를 Java로 변환해보면 아래와 같습니다

public final class Bar {
    private String barName;
    
    public final String getBarName() {
        return this.barName;
    }
    
    public final void setBarName(String var1) {
        this.barName = var1;
    }
}

위와 동일한 코드에서 @JvmField annotation을 붙여보겠습니다

class Bar {
   @JvmField
   var barName: String = "bar"
}


이해를 위해 Java로 변환해보면 아래와 같습니다

public final class Bar {
    @JvmField
    private String barName;
}

@Throws

@Throws는 코틀린 함수가 예외를 던질 수 있다는 것을 의미하는 annotation입니다
Kotlin에는 자바의
throws와 같은 코드가 없습니다

아래는 Java에서 IOException 예외를 던질 수 있다고 명시된 함수입니다
이 함수를 사용하려면
try-catch로 IOException에 대한 예외처리를 구현해줘야 사용이 가능합니다

void baz(String test) throws IOException {
    ... // 메서드 Logic
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
// 위 함수 사용예시, try-catch로 예외처리 선언 필요
try {
   baz("테스트함수")
} catch (IOException e) {
   ... // 예외처리 Logic
}


Kotlin에서는 위와 같이 함수가 예외를 던질 수 있다고 명시하고 싶으면
@Thorws를 사용하면 됩니다

아래 코드처럼 @Throws의 파라미터로 IOException::class 형태의 예외를 명시해주면 됩니다

@Thorws(IOException::class)
fun baz(test : String) {
    ... // 메서드 Logic
}

위 Kotlin 함수를 java로 변환하면, 아래와 같은 형태로 변환되게 됩니다

public static final void baz(@NotNull String test) throws IOException {
    ... // 메서드 Logic
}

@JvmOverloads

@JvmOverloads는 Kotlin 함수의 오버로딩 메서드들을 생성해주는 annotation입니다

아래 코드는 생성자의 파라미터가 3개지만, 2개는 기본인자(dafault arguments)로 선언된 구조입니다 

class Bar(var name: String, var size: Int = 0, var max: Int = 0) {
    init {
        println("Bar init: $name, $size, $max")
    }
}


즉, 코틀린은 아래 예시처럼 여러 생성자 형태를 사용해서 인스턴스를 생성할 수 있습니다

Bar("foo")
Bar("foo", 15)
Bar("foo", 20, 21)

하지만 위 Kotlin 클래스를 Java로 변환하면 아래처럼 3개의 인자를 갖고있는 하나의 생성자만 생성됩니다

왜냐면, Java는 기본인자(dafault arguments)의 개념이 없기 때문입니다

public final class Bar {
   private String name;
   private int size;
   private int max;

   public Bar(String name, int size, int max) {
      String var4 = "Bar init: " + this.name + ", " + this.size + ", " + this.max;
      System.out.println(var4);
   }
}

ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ
new Bar("foo", 20, 21);

new Bar("foo") // Error, size/max 인자 필요

그렇기 때문에 Java에서 Bar클래스의 인스턴스를 생성하려면 3개의 인자를 모두 입력해줘야 합니다

Java에서도 위의 Kotlin처럼 3개의 다른 생성자 형태로 구현을 하고싶다면 Kotlin에서 constructor로 생성자의 다형성을 구현해줘야 합니다

기본생성자 1개 + 추가 생성자 2개로 선언하였습니다

class Bar (var name: String, var size: Int = 0, var max: Int = 0) {
    constructor(name: String, size: Int): this(name, size, 0)
    constructor(name: String): this(name, 0, 0)

    init {
        println("Bar init: $name, $size, $max")
    }
}

@JvmOverloads는 위의 오버로딩 생성자를 자동으로 생성해주는 annotation입니다

@JvmOverloads를 함수(생성자) 앞에 선언하면, constructor로 2개의 생성자를 추가로 선언한 것과 같이 자동으로 오버로딩 생성자를 생성해줍니다 

class Bar 
    @JvmOverloads constructor(var name: String, var size: Int = 0, var max: Int = 0) {

    init {
        println("Bar init: $name, $size, $max")
    }
}

위 Bar클래스를 Java로 변환하면 오버로딩 메서드들이 자동으로 생성된 것을 볼 수 있습니다

public final class Bar {
   @NotNull
   private String name;
   private int size;
   private int max;

   // 인자 3개의 생성자
   @JvmOverloads
   public Bar(@NotNull String name, int size, int max) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      super();
      this.name = name;
      this.size = size;
      this.max = max;
      String var4 = "Bar init: " + this.name + ", " + this.size + ", " + this.max;
      System.out.println(var4);
   }

   // 인자 2개의 생성자
   @JvmOverloads
   public Bar(@NotNull String name, int size) {
      this(name, size, 0, 4, (DefaultConstructorMarker)null);
   }

   // 인자 1개의 생성자
   @JvmOverloads
   public Bar(@NotNull String name) {
      this(name, 0, 0, 6, (DefaultConstructorMarker)null);
   }
}

정리

  • @JvmOverloads는 오버로딩 메서드(생성자 포함)를 Java에 자동으로 생성해주는 annotation
  • Kotlin 코드만 사용하는 프로젝트라면 사용할 필요가 없는 annotation
  • 즉, Java에서 Kotlin의 기본인자(default arguments)의 개념을 사용하기 위한 annotation