멈재

[JAVA] 자바 애너테이션과 활용 예시 (Annotation) 본문

JAVA & Spring & JPA

[JAVA] 자바 애너테이션과 활용 예시 (Annotation)

멈재 2023. 3. 26. 23:42
728x90

 

모든 예시 코드는 깃허브에 있으니 참고해주세요.

 

 


 

 

애너테이션이란

애너테이션은 Java 5부터 등장한 기능으로 Java 컴파일러 또는 JVM에게 추가 정보를 제공하는 메타데이터의 일종이다.

@Override
@Getter @Setter @Data
@Controller
...
메타 데이터란??
메타데이터란 데이터를 설명해주는 데이터라는 의미를 갖는데 예를 들어, 사진이라는 데이터는 사진 그 자체와 직접 관련이 없는 사진 크기, 해상도, 촬영 일시 등의 추가 정보를 담고 있다.
이러한 사진에 추가적인 정보를 메타데이터라고 한다.

 

1. 컴파일러에게 코드 문법이나 경고를 나타내지 말라는 정보를 줄 수 있다.

 

@Override

메서드 앞에만 붙일 수 있는 애너테이션으로 부모 클래스의 메서드를 오버라이딩한다는 것을 컴파일러에게 알려주는 역할을 한다. 만약, 오버라이딩 시에 부모 클래스의 메서드를 잘못 쓰게 되면 컴파일러가 잘못된 것임을 알려준다.

대문자 I가 아닌 소문자 l로 표시

method does not override or implement a method from a supertype

 

 

2. 소프트웨어 툴(Lombok, Mapstruct,...)은 애너테이션 정보로 컴파일 타임이나 배치 시에 코드를 생성할 수 있도록 정보를 제공한다.

 

@Data

Lombok의 애너테이션으로 모든 필드를 대상으로 접근자와 변경자 그리고 생성자 등의 메서드를 자동으로 만들어준다.

그래서 해당 애너테이션이 붙어있는 경우 클래스 내부에 접근자와 변경자(getter/setter)가 존재하지 않아도 사용이 가능해진다.

 

 

이처럼 애너테이션을 잘 활용하면 보일러 플레이트 코드(Boilerplate code)처럼 반복되는 코드를 줄일 수 있으므로 코드가 간결해지고, 읽어야 할 코드의 수가 줄어들게 된다.

그러나 애너테이션이 활용은 무궁무진하지만 의미를 함축하고 있기 때문에 어떤 동작을 하는지 명확히 알 수 없다. 또한, 특정 애너테이션을 리팩터링 해야 한다면 동작과 의미를 명확하게 알아야 하는 어려움도 존재한다.

 

이처럼 당장에는 이점이 많아 보이나 멀리 보았을 때 사용하는 것이 적절한 지를 따져보고 적용하는 것이 바람직하다.

 

 

애너테이션 정의

애너테이션은 인터페이스를 만들듯이 정의할 수 있는데 interface 앞에 '@' 기호를 붙여 정의할 수 있다.

애너테이션내에 선언된 메서드를 애너테이션의 요소라고 하고, 매개변수는 없고 반환 타입은 있는 형태로 되어있다.

public @interface 애너테이션이름 {
    타입 요소이름() // 애너테이션의 요소를 선언
}
---
public @interface CustomAnnotation {
    String name() default "기본값";
}

기본적으로 애너테이션을 적용할 때 애너테이션의 요소 값들을 설정해주어야 하는데 위와 같이 기본값을 설정해둔 경우라면 생략이 가능하고 기본값으로 설정된다.

(기본값이 설정되지 않은 요소가 존재할 경우 값을 지정해주지 않으면 컴파일 에러가 발생한다.)

 

 

애너테이션의 규칙

  1. 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용한다.
  2. 애너테이션의 요소에는 매개변수를 지정할 수 없다.
  3. 예외를 선언할 수 없다. (예외를 던지는 throws를 사용할 수 없음을 의미)
  4. 애너테이션 요소의 타입에 타입 매개변수를 정의할 수 없다.

 

 

메타 애너테이션

커스텀 애너테이션을 정의할 때 사용하는 메타 애너테이션에 대해 알아보자.

이미지 출처: 자바의 정석 3판 2권 P703

 

@Target

애너테이션을 적용할 수 있는 범위를 지정하는 애너테이션으로 여러 개의 대상 타입을 지정하는 경우에는 배열처럼 괄호({ })를 사용해야 한다.

 

https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/ElementType.html

 

ElementType (Java Platform SE 8 )

The constants of this enumerated type provide a simple classification of the syntactic locations where annotations may appear in a Java program. These constants are used in java.lang.annotation.Target meta-annotations to specify where it is legal to write

docs.oracle.com

@Target({ElementType.FIELD, ElementType.TYPE, ElementType.TYPE_USE})
public @interface MyAnnotation {}

---

@MyAnnotation     // 적용 대상이 TYPE
public class MyClass {
    @MyAnnotation // 적용 대상이 FIELD
    int i;
    
    @MyAnnotation
    MyClass mc;   // 적용 대상이 TYPE_USE
}

 

 

@Retention

애너테이션이 유지되는 기간을 정하는 애너테이션으로 유지 정책이 아무것도 지정되지 않을 경우 CLASS로 지정된다.

 

https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/RetentionPolicy.html

 

RetentionPolicy (Java Platform SE 8 )

 

docs.oracle.com

유지 정책(RetentionPolicy) 의미
SOURCE - 애너테이션이 소스 파일에만 존재한다. 이 말은 사실상 주석으로 사용한다는 것을 의미한다.
- 컴파일러가 컴파일할때 해당 애너테이션의 메모리를 버리게 된다.
CLASS - 애너테이션이 클래스 파일에 존재하지만 런타임시에 사용불가하다. (기본값)
- 컴파일러가 컴파일 타임에는 애너테이션의 메모리를 가져가지만 런타임시에는 사라지게 된다. “런타임시에 사라진다.”라는 말은 리플렉션으로 선언된 애너테이션의 데이터를 가져올 수 없음을 의미한다.
RUNTIME - 클래스(*.class) 파일에 존재하고 실행시에 사용이 가능하다.
- JVM이 자바 바이트코드가 담긴 클래스 파일을 런타임환경으로 구성하고, 런타임 종료시까지 메모리에 살아있다.

 

각 유지 정책이 가지는 정의들을 적긴 했으나 CLASS처럼 모호한 부분이 있어서 정책별 컴파일된 클래스를 뜯어보며 알아보겠다.

 

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.{SOURCE/CLASS/RUNTIME})
@Target(ElementType.METHOD)
public @interface RetentionAnnotation {}

커스텀 애너테이션인 RetentionAnnotation은 다음과 같이 정의될 것이고, 이후의 예시에서는 유지 정책만 변경되게 된다.

그리고 이 애너테이션은 RetentionTest 클래스의 printClass 메서드에 적용할 것이다.

public class RetentionTest {
    @RetentionAnnotation
    void printClass() {}
}

 

아래 코드블록 예시들은 소스 파일(xx.java)의 코드가 아닌 컴파일된 클래스 파일(xx.class)을 디컴파일한 코드이다.

 

 

SOURCE

// RetentionTest.class
package annotation;

public class RetentionTest {
    public RetentionTest() {
    }

    void printClass() {
    }
}

유지정책이 SOURCE인 경우 주석처럼 사용되기 때문에 소스 파일에만 존재하고 클래스 파일에는 존재하지 않게 된다.

 

 

CLASS

package annotation;

public class RetentionTest {
    public RetentionTest() {
    }

    @RetentionAnnotation
    void printClass() {
    }
}

유지정책이 CLASS인 경우에 클래스 파일에도 애너테이션이 존재한다고 정의되어있다. 따라서 해당 클래스 파일에도 애너테이션이 적용되어 있는 것을 알 수 있다.

 

그렇다면

런타임에 애너테이션이 사라진다는 정보는 어디서 알 수 있을까??

 

디컴파일되지 않은 클래스 파일을 직접 열어보면 바이트 코드로 작성된 부분 중에 RunTimeInVisibleAnnotation이라는 것이 존재하는데 이 부분으로 구분 지어놨다.

RetentionTest.class

CLASS 유지 정책은 단지 바이트 코드에서 확인 가능한 수준으로만 정의해 놓았을 뿐 런타임시에 애너테이션의 정보가 사라지게 된다.

 

애너테이션이 클래스 파일도 유지되고 런타임에도 유지되는 RUNTIME 정책은 어떨까

 

RUNTIME

package annotation;

public class RetentionTest {
    public RetentionTest() {
    }

    @RetentionAnnotation
    void printClass() {
    }
}

당연하게도 디컴파일된 클래스 파일에 애너테이션이 유지되어 있는 것을 알 수 있고, 해당 클래스 파일을 직접 열게되면 CLASS와 달리 RuntimeVisibleAnnotations으로 정의되어 있는 것을 알 수 있다.

 

 

@Inherited

부모 클래스에 정의한 애너테이션이 자식 클래스에도 상속이 되도록 한다.

import java.lang.annotation.*;

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InheritedAnnotation {
    String value() default "inheritance";
}
public class InheritedTest {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child = new Child();

        InheritedAnnotation parentAnnotation = parent.getClass().getAnnotation(InheritedAnnotation.class);
        InheritedAnnotation childAnnotation = child.getClass().getAnnotation(InheritedAnnotation.class);

        String parentVal = parentAnnotation.value();
        String childVal = childAnnotation.value();

        System.out.println("parentVal = " + parentVal); // parentVal = parent
        System.out.println("childVal = " + childVal);   // childVal = parent

    }
}

@InheritedAnnotation(value = "parent")
class Parent {}
class Child extends Parent {}

만약 InheritedAnnotation에 Inherited 애너테이션이 존재하지 않을 경우 런타임시에 NPE(NullPointerException)가 발생하게 된다.

 

 

@Repeatable

기본적으로 하나의 대상에 한 종류의 애너테이션만 붙일 수 있는데 해당 애너테이션을 지원하기 전에는 여러 속성을 정의하고 싶을 때 다음과 같이 정의했다.

@Chrome
@Firefox
@Edge
public class WebBrowser { ... }

 

JDK 1.8부터는 같은 애너테이션을 중복 정의가 가능하도록 하는 @Repeatable을 이용한다면 하나의 클래스 또는 메서드에 애너테이션을 여러 번 정의할 수 있다.

@Browser(webBrowser = "Chrome")
@Browser(webBrowser = "Firefox")
@Browser(webBrowser = "Edge")
class WebBrowser {}

 

그러나 이 애너테이션의 경우 일반적인 애너테이션과 달리 같은 이름의 애너테이션이 여러 개가 하나의 대상에 적용되기 때문에 애너테이션을 묶음으로 관리하는 컨테이너 애너테이션을 추가로 정의해주어야 한다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Browsers { // 컨테이너 애너테이션
    Browser[] value();       // 이름이 반드시 value여야 함
}
@Repeatable(value = Browsers.class)
public @interface Browser {
    String webBrowser();
}
public class RepeatableTest {
    public static void main(String[] args) {

        WebBrowser webBrowser = new WebBrowser();
        Browsers browsers = webBrowser.getClass().getAnnotation(Browsers.class);

        for (Browser browser : browsers.value()) {
            System.out.println("browser = " + browser);
        }
        // [실행결과]
        // browser = @annotation.Browser(webBrowser="Chrome")
        // browser = @annotation.Browser(webBrowser="Firefox")
        // browser = @annotation.Browser(webBrowser="Edge")
    }
}

@Browser(webBrowser = "Chrome")
@Browser(webBrowser = "Firefox")
@Browser(webBrowser = "Edge")
class WebBrowser {}

 

 

커스텀 애너테이션 예시

지금까지의 내용들을 바탕으로 간단한 커스텀 애너테이션을 만들어보고 적용해보는 예시를 들어보겠다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface CustomAnnotation {
    String name() default "기본값";
}

 

애너테이션은 위와 같이 정의가 되어있고, 이 애너테이션을 리플랙션으로 애너테이션의 정보를 가져와서 확인하는 예시이다.

public class AnnotationTest {

    @CustomAnnotation(name = "스터디")
    static class MyClass {}

    @CustomAnnotation
    static class DefaultClass {}

    @Test
    @DisplayName("애너테이션 예제")
    void annotationTest() throws Exception {
        AnnotationExample.DefaultClass defaultClass = new AnnotationExample.DefaultClass();
        CustomAnnotation customAnnotation = defaultClass.getClass().getAnnotation(CustomAnnotation.class);
        String 기본값 = customAnnotation.name();
        System.out.println("[Default] customAnnotation.name() = " + 기본값);

        AnnotationExample.MyClass myClass = new AnnotationExample.MyClass();
        customAnnotation = myClass.getClass().getAnnotation(CustomAnnotation.class);
        String 스터디 = customAnnotation.name();
        System.out.println("[Assign]  customAnnotation.name() = " + 스터디);

        assertThat("기본값").isEqualTo(기본값);
        assertThat("스터디").isEqualTo(스터디);
    }
}
// [실행 결과]
// [Default] customAnnotation.name() = 기본값
// [Assign]  customAnnotation.name() = 스터디

 

 


 

 

이전부터 애너테이션과 AOP로 인증 처리를 해보는 것이 투두 목록 중 하나였는데 약간 응용하여 요청값으로 한국 이름만 가능하도록 구현을 해보았습니다.

 

https://github.com/ahn-sj/spring-box/blob/master/annotation-sample/src/main/java/springbox/annotationsample/annotation/aspect/KoreanNameCheckerAspect.java

 

GitHub - ahn-sj/spring-box: I want to handle the spring better. This repository is for me to apply what I've learned

I want to handle the spring better. This repository is for me to apply what I've learned - GitHub - ahn-sj/spring-box: I want to handle the spring better. This repository is for me to apply wha...

github.com

 

 

 

참고

 


 

혹여 잘못된 정보나 내용이 있다면 댓글로 남겨주시면 감사하겠습니다.