glenn-syj / more-effective-java

이펙티브 자바를 읽으며 자바를 더 효율적으로 공부합니다
4 stars 5 forks source link

[MEJ-009] 커스텀 어노테이션의 예시들에 대해 #174

Closed undeadtimo closed 1 month ago

undeadtimo commented 1 month ago

based on item41 by @glenn-syj

평소 어노테이션을 자주 접하면서도, 어노테이션을 직접 만든다는 생각은 전혀 하지 못하였기에.

glenn님의 심화 탐구 부분에 대하여 특히 흥미롭게 읽었습니다!

@Target 어노테이션을 통해서 선언하는 어노테이션의 대상을 정할 수 있는데, 그렇다면 @Target 어노테이션은 어떻게 구성할 수 있을까? 하는 부분 또한 생각지 못한 부분을 짚어주셔서 재밌게 읽었습니다.


저는 추가적으로 어떠한 커스텀 어노테이션을 만들 수 있는지, 실제로 어떻게 사용할 수 있는지를 찾아보았습니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface JsonSerializable {
}

해당 커스텀 어노테이션(JAVA 내장 라이브러리에 존재하지 않는 어노테이션이다) JsonSerializable은 직렬화 가능한 클래스에 붙임으로써 직렬화 가능한 클래스임을 나타내는 어노테이션입니다.

저는 이 어노테이션을 보고, 어떻게 직렬화 가능한 클래스임을 나타내는지 의문을 가졌으나, Retention과 Target에 대해 파악하여 그 원리를 알 수 있게 되었습니다.

@Retention의 RuntimePolicy.RUNTIME 을 통해서 어노테이션이 붙은 것이 런타임에 실행이 되어야 함을 나타내고 있으며, @Target의 ElementType.TYPE을 통해서 어노테이션이 붙은 것이 클래스, 인터페이스, 열거 타입임을 나타내고 있습니다.

즉, '런타임에 실행되고 있을 클래스'라는 것 자체가, '직렬화 가능한 클래스'임을 나타내기 때문에, 해당 JsonSerializable 어노테이션을 통해 '직렬화 가능한 클래스'를 표시할 수 있게 되는 것입니다.

다음은 field에 적용해볼 수 있는 커스텀 어노테이션입니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonElement {
    public String key() default "";
}

참고로, 커스텀 어노테이션 내부의 메서드는 여러 제약을 갖고 있습니다.

  1. 매개변수를 가질 수 없다.
  2. 타입 매개변수를 가질 수 없다.
  3. throws 절을 가질 수 없다.

위에서 JsonElement가 갖는 내부 메서드는 특정 field에 대해 "key" 이름의 String 변수를 Json에서의 '키'값으로 설정하여 값을 매핑할 수 있도록 하는 것입니다.

예를 들어,

@JsonElement(key = "personAge")
    private String age;

이런 식으로 JsonElement 값을 적용하고 age 값으로 30이 온다면,

{personAge : 30}

으로 Json이 표시될 것입니다.

메서드 타입 어노테이션

이 부분을 이해하는 것이 특히 신기하고 어려웠습니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface undeadAnnotation {
}

이런 식으로 메서드를 대상으로하는 커스텀 어노테이션을 만들어서, 클래스 내부 메서드에 적용할 경우, 특수한 역할을 하게 됩니다.

@JsonSerializable
public class Person {

    @JsonElement
    private String firstName;

    @JsonElement
    private String lastName;

    @JsonElement(key = "personAge")
    private String age;

    private String address;

    @undeadAnnotation
    private void initNames() {
        this.firstName = this.firstName.substring(0, 1).toUpperCase() 
          + this.firstName.substring(1);
        this.lastName = this.lastName.substring(0, 1).toUpperCase() 
          + this.lastName.substring(1);
    }

}

위처럼 클래스 내부의 메서드에 제가 임의로 생성한 어노테이션을 적용하면,

객체가 생성되어 초기화될 경우, 자동으로 커스텀 어노테이션이 붙은 메서드를 호출하여 실행하도록 만듭니다.

이처럼, 커스텀 어노테이션을 사용하면 단순히 메타 데이터의 정보를 표시하는 것을 넘어서 리플렉션을 통해 메타 데이터가 있는 작업을 처리하는 기능을 구현할 수 있습니다.


References : https://www.baeldung.com/java-custom-annotation

glenn-syj commented 1 month ago

@JsonElement를 통해서 추가적으로 상세히 설명해주셔서 감사합니다! 사실 어노테이션을 따로 만드는 게 쉽지 않은 일 같지만, 내부적으로 유용한 경우가 많을 것도 같아서 꼭 짚고 넘어가면 좋은 부분이네요!