일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- publicapi
- 비정적중첩클래스
- 애너테이션
- github actions
- 스파르타코딩클럽
- refreshtoken
- 인수테스트
- 항해99 9기
- DI
- privateapi
- 변경감지
- 클라이언트사이드렌더링
- IoC
- SOLID
- 스프링컨테이너
- 서버사이드렌더링
- 항해99
- 다형성
- 9기
- 더티채킹
- Spring
- 인프콘
- 싱글톤패턴
- 정적중첩클래스
- bean
- java
- 지네릭스
- 자바의정석
- Velog
- 일급컬렉션
- Today
- Total
멈재
[JAVA] 자바 애너테이션과 활용 예시 (Annotation) 본문
모든 예시 코드는 깃허브에 있으니 참고해주세요.
애너테이션이란
애너테이션은 Java 5부터 등장한 기능으로 Java 컴파일러 또는 JVM에게 추가 정보를 제공하는 메타데이터의 일종이다.
@Override
@Getter @Setter @Data
@Controller
...
메타 데이터란??
메타데이터란 데이터를 설명해주는 데이터라는 의미를 갖는데 예를 들어, 사진이라는 데이터는 사진 그 자체와 직접 관련이 없는 사진 크기, 해상도, 촬영 일시 등의 추가 정보를 담고 있다.
이러한 사진에 추가적인 정보를 메타데이터라고 한다.
1. 컴파일러에게 코드 문법이나 경고를 나타내지 말라는 정보를 줄 수 있다.
@Override
메서드 앞에만 붙일 수 있는 애너테이션으로 부모 클래스의 메서드를 오버라이딩한다는 것을 컴파일러에게 알려주는 역할을 한다. 만약, 오버라이딩 시에 부모 클래스의 메서드를 잘못 쓰게 되면 컴파일러가 잘못된 것임을 알려준다.
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 "기본값";
}
기본적으로 애너테이션을 적용할 때 애너테이션의 요소 값들을 설정해주어야 하는데 위와 같이 기본값을 설정해둔 경우라면 생략이 가능하고 기본값으로 설정된다.
(기본값이 설정되지 않은 요소가 존재할 경우 값을 지정해주지 않으면 컴파일 에러가 발생한다.)
애너테이션의 규칙
- 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용한다.
- 애너테이션의 요소에는 매개변수를 지정할 수 없다.
- 예외를 선언할 수 없다. (예외를 던지는 throws를 사용할 수 없음을 의미)
- 애너테이션 요소의 타입에 타입 매개변수를 정의할 수 없다.
메타 애너테이션
커스텀 애너테이션을 정의할 때 사용하는 메타 애너테이션에 대해 알아보자.
@Target
애너테이션을 적용할 수 있는 범위를 지정하는 애너테이션으로 여러 개의 대상 타입을 지정하는 경우에는 배열처럼 괄호({ })를 사용해야 한다.
https://docs.oracle.com/javase/8/docs/api/java/lang/annotation/ElementType.html
@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) | 의미 |
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이라는 것이 존재하는데 이 부분으로 구분 지어놨다.
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로 인증 처리를 해보는 것이 투두 목록 중 하나였는데 약간 응용하여 요청값으로 한국 이름만 가능하도록 구현을 해보았습니다.
참고
혹여 잘못된 정보나 내용이 있다면 댓글로 남겨주시면 감사하겠습니다.
'JAVA & Spring & JPA' 카테고리의 다른 글
[JAVA] 중첩 클래스, 중첩 클래스의 문단속 (0) | 2023.04.29 |
---|---|
[디자인 패턴] 싱글턴 패턴, 내가 알던 싱글턴이 아니야! (0) | 2023.04.18 |
[Java] 정확한 답이 필요하다면 float와 double은 피하라 (1) | 2023.03.10 |
[JAVA] 20. 동일성(identity)과 동등성(equality) (0) | 2023.01.08 |
[JAVA] 18. 상속(IS-A) VS 구성(HAS-A) (0) | 2023.01.02 |