[Android] Custom Annotation - @MyAnnotation

양현진·2022년 12월 22일
1

Oh My Android

목록 보기
21/22
post-thumbnail
post-custom-banner

개발관련 포스팅을 보면 가끔 코드에 Annotation이 붙어있는걸 볼 수 있는데 뭔가 고수의 향기가 나는 것 같은 느낌을 받는다ㅎ
자주 사용하는 Annotation들은 Res Contraint을 걸어주는 타입을 주로 사용한다.

Custom을 시작하기 전, 몇가지 Annotation을 알아보자.

Embedded Annotation

1. @Deprecated

@Deprecated("Error Logic")
fun function1(a: Int, b: Int) : Int {
    return a + b
}

Android API 버전에 따라 취소선이 나오는 메서드들이 있다. 바로 Deprecated Annotation을 사용하면 개인이 직접 타 개발자에게 알려줄 수 있는 기능이다. 주로 라이브러리나 SDK를 개발할 때 버전업에 따라 사용할 수 있을 것 같다.

Annotation안에는 설명을 해주는 commit같은 JavaDoc 기능도 포함되어있다. 커서를 가져다 대면 메세지가 나온다.

또한 Deprecated되었으니 다른 메서드를 추천하는 기능 또한 제공한다.

@Deprecated("Error Logic", ReplaceWith("functionNew1(a, b)"))
fun function1(a: Int, b: Int) : Int {
    return a + b
}

2. @IntRange, @FloatRange

fun function2(@IntRange(from = 0, to = 10) a: Int, b: Int) : Int {
    return a + b + 1
}

fun function2(@FloatRange(from = 0.0, to = 10.0) a: Float, b: Float) : Float {
    return a + b
}

파라미터의 범위 제약을 걸어주는 Annotation이다.
@StringRes와 같이 코드의 제약이 어느정도 강제되면 좋은 코드라고 볼 수 있다. 이유는 개발단계에서 버그 발생 커버리지를 높일 수 있기 때문이다.

위 메서드의 파라미터에 from..to 범위를 벗어난 값을 넣게 되면 컴파일 단계에서 알려주게 된다.

3. @JvmOverloads

@JvmOverloads
fun function3 (a: Int = 0, b: Int = 0) : Int {
    return a + b
}

코틀린과 자바를 한 프로젝트에서 같이 사용할 때 유용할 수 있다.
Kotlin에서는 파라미터의 Default Value를 제공하는데 자바에서 Default Value가 들어있는 kotlin 메서드를 호출하려하면 무조건 파라미터를 넣어야하는 불편함이 생긴다.

JvmOverloads Annotation은 자바에서 이를 구현할 수 있게 도와준다. 디컴파일된 .java파일로 차이를 살펴보자.

fun function3 (a: Int = 0, b: Int = 0) : Int {
    return a + b
}

@JvmOverloads
fun function4 (a: Int = 0, b: Int = 0) : Int {
    return a + b
}



/* Convert to .java */


/**
@JvmOverloads 미 적용
*/
public final int function3(int a, int b) {
    return a + b;
}

// $FF: synthetic method
public static int function3$default(AnnotationType var0, int var1, int var2, int var3, Object var4) {
    if ((var3 & 1) != 0) {
        var1 = 0;
    }

    if ((var3 & 2) != 0) {
        var2 = 0;
    }

    return var0.function3(var1, var2);
}

/**
@JvmOverloads 적용
*/
@JvmOverloads
public final int function4(int a, int b) {
    return a + b;
}

// $FF: synthetic method
public static int function4$default(AnnotationType var0, int var1, int var2, int var3, Object var4) {
    if ((var3 & 1) != 0) {
        var1 = 0;
    }

    if ((var3 & 2) != 0) {
        var2 = 0;
    }

    return var0.function4(var1, var2);
}

@JvmOverloads
public final int function4(int a) {
    return function4$default(this, a, 0, 2, (Object)null);
}

@JvmOverloads
public final int function4() {
    return function4$default(this, 0, 0, 3, (Object)null);
}

@JvmOverloads를 붙이지 않은 function3의 경우 추가로 생긴 $default 메서드 말고는 Overload된 함수가 없다.
하지만 funtion4의 경우 총 3개의 Overload 메서드를 볼 수 있다. 이를 통해 자바에서도 디폴트 값을 가지는 코틀린 메서드를 호출 할 수 있게 된다.

다만 아쉬운 점은 코틀린은 파라미터를 선택해서 넣어줄 수 있는데, 자바에서는 빌드된 파일처럼 int b값만을 줄 순 없나 보다. 코틀린 짱

이제 직접 Annotation을 만들어보자.

Custom Annotation

AlertDialog Attribute 예제

전에 SDK를 만들 때 회사 내부에서만 사용할 다이얼로그를 만들었던 적이 있다.

AlertDialog중 Neutral Button은 자주 사용하진 않지만, 사용하지 않는다고 해서 막아버리면 추후 필요할 때 SDK를 변경하거나 따로 또 Dialog를 만들어야하니 일단 만들어두고 visibility로 컨트롤 할 예정이다.

여기서 View.VISIBILITY 필드에 단순 Int값이 오는것을 방지하기 위해 Custom Annotation을 달아 보았다.

@Target(AnnotationTarget.FIELD)
@IntDef(View.GONE, View.VISIBLE, View.INVISIBLE)
@Retention(AnnotationRetention.SOURCE)
annotation class Visibility

위와 같이 코드를 입력하면 @Visibility를 사용할 수 있게된다.

  • Target: Annotation을 어느 구간에만 적용할 지 제약을 두는 기능이다.
    위는 PROPERTY영역에서만 유효하게 정해두었다. 이유는 Data Class의 속성에서 사용하기 때문이다. 달지 않아도 기능은 정상작동하지만 이 또한 위에서 언급한 코드 제약을 높이는 방법의 일환이다.
    그 외에도 다양한 타입이 있다. PROPERTY와 같이 각자 맡은 영역만 제약을 둔다.
public enum class AnnotationTarget {
    /** Class, interface or object, annotation class is also included */
    CLASS,
    /** Annotation class only */
    ANNOTATION_CLASS,
    /** Generic type parameter */
    TYPE_PARAMETER,
    /** Property */
    PROPERTY,
    /** Field, including property's backing field */
    FIELD,
    /** Local variable */
    LOCAL_VARIABLE,
    /** Value parameter of a function or a constructor */
    VALUE_PARAMETER,
    /** Constructor only (primary or secondary) */
    CONSTRUCTOR,
    /** Function (constructors are not included) */
    FUNCTION,
    /** Property getter only */
    PROPERTY_GETTER,
    /** Property setter only */
    PROPERTY_SETTER,
    /** Type usage */
    TYPE,
    /** Any expression */
    EXPRESSION,
    /** File */
    FILE,
    /** Type alias */
    @SinceKotlin("1.1")
    TYPEALIAS
}
  • IntDef: 가장 필요한 기능이다. 여기서는 VISIBLE, GONE, INVISIBLE 3가지만 넣을 수 있게 해뒀다. IntDef인 경우는 View.VISIBILITY 변수가 Int 타입이기 때문이다. Annotation안에 값만이 올 수 있도록 제약을 준다.
  • Retention
    SOURCE: compile time 에만 유용하며 빌드된 binary 에는 포함되지 않는다. 이는 Hilt와 같이 Annotation으로 코드를 생성하는거와 같이 빌드 후에도 영향을 끼칠건지 아닌지를 정하는 기능 같다. 몇번을 읽어봤지만 확실치 않음 처음엔 컴파일 에러만 발생시키고 runtime에선 작동을 안하나 싶었는데 이건 아니었다. 이해를 바탕으로 결과를 내면 우선 위와 같이 단순 제약을 위한 Annotation에서는 SOURCE타입만으로도 충분해 보인다.
    BINARY: compile time 과 binary 에도 포함되지만 reflection 을 통해 접근할 수 없다.

리플렉션(Reflection)이란 런타임 때 프로그램의 구조(객체, 함수, 프로퍼티)를 분석해 내는 기법을 말한다. startActivity때 자주 사용하던 Activity::class.java또한 리플렉션에 포함된다. 리플렉션을 통해 클래스의 상태(final, sealed)등 다양한 요소들을 파악할 수 있다.

RUNTIME: compile time 과 binary 에도 포함되고, reflection 을 통해 접근이 가능하다. Custom Annotation 에 @Retention 을 표시해주지 않을경우, 디폴트로 값이기도 하다.

이렇게 설명을 마무리하고, 위 @Visibility를 적용한 Data Class를 보자

data class AlertDialogAttr(
    @FloatRange(from = 0.0, to = 48.0)
    val textSize: Float = ALERT_DIALOG_TITLE_DEFAULT_SIZE,
    @FontRes
    var fontId: Int = R.font.alert_dialog_title_default,
    @StringRes
    val textId: Int = R.string.alert_dialog_title_default,
    @ColorRes
    var colorId: Int = R.color.alert_dialog_title_default,
    @Visibility
    var visibility: Int = View.INVISIBLE
)

Custom외에도 다양한 Annotation으로 제약을 걸었다. 1~4번째는 위에서 설명했으니 과연 Custom한 것도 잘 동작할까?

정말로 단순 Int값을 넣으니 에러가 발생했다. 또한 설명까지 친절하게 알려준다.

여기서 더 나아가 만일 View.GONE의 Int값을 직접 입력해도 에러가 발생할까?

이 또한 컴파일 에러가 발생한다. 오로지 정의한 3가지의 요소만이 들어갈 수 있게 되어 버그 방지와 Dos로 사용자에게 어떤 속성인지 설명할 수 있게된다.

Custom Annotation 관련 포스팅을 하게 된 이유는 Hilt를 직접 만들어보고 싶어서였다. 그리하여 다음 포스팅으로 직접 Hilt를 만들기 위한 과정으로 Annotation을 통한 Code Generation을 해볼 예정이다. Hilt가 Annotation을 기반 DI를 제공하기 때문이다.

profile
Android Developer
post-custom-banner

0개의 댓글