요새 하고 있는것중에 직접 커스텀해서 어노테이션을 만들어서 써야할 일이 생겼습니다.
필수는 아니지만 깔끔한 코드를 위해 있으면 좋을것같았어요. 오늘은 이를 위해 공부한 어노테이션의 개념과 동작원리, 그리고 간단한 커스텀 어노테이션을 만들어보겠습니다.
1. 어노테이션(Annotation) 개념
1) 어노테이션 개념
먼저 어노테이션은 주석이라는 뜻입니다. 실제로 Java에서의 Annotation도 같은 역할을 합니다.
일반 // 주석과 @Annotation 의 다른점은 컴파일러가 주석 내용을 추출해 .class 파일 안에 별도의 메타데이터로 새겨둔다는 것 입니다.
이렇게 새겨진 정보는 두 군데에서 쓰입니다.
- 컴파일 단계
자바 컴파일러가 소스 코드를 바이트코드로 바꿀 때, 소스 코드를 생성하거나 규칙 위반을 잡아냄
ex) Lombok - 실행 단계
프로그램이 JVM 위에서 돌아갈 때, 리플렉션 API를 통해 해당 메타데이터를 다시 꺼낼 수 있음.
ex) @Transactional, @Controller
리플렉션(reflection): 런타임의 구조를 조사하고 조작할 수 있는 기능
소스코드가 컴파일될 때, 컴파일러는 클래스 파일의 속성(attribute)영역에 두가지 형태로 저장이됩니다.
RuntimeVisibleAnnotations은 런타임시에도 접근 가능하도록 유지됩니다.
RuntimeInvisibleAnnotations은 실행중에는 보이지 않지만 컴파일러 단계에는 남아있습니다.
이 둘은 @Retention 설정으로 결정됩니다.
2) 어노테이션 정의, @Target과 @Retention
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
String value() default "";
}
위의 코드는 어노테이션의 간단한 예시를 보여줍니다.
먼저 어노테이션은 @interface 로 정의합니다. 사실상 특수한 형태의 인터페이스를 만드는 셈 입니다.
요소의 자료형은 원시형, String, Class, 다른 어노테이션, 그리고 이들의 배열이 허용됩니다.
List<String> 같은 제네릭이나 컬렉션 타입은 허용되지 않습니다.
@Target은 어노테이션을 붙일 수 있는 위치를 지정해줍니다.
종류는 다음과 같습니다.
- TYPE: 클래스, 인터페이스, enum
- FIELD: 인스턴스/정적 변수
- METHOD: 메서드
- PARAMETER: 메서드 파라미터
- CONSTRUCTOR: 생성자
- LOCAL_VARIABLE: 지역변수
- PACKAGE: package-info.java 파일
- TYPE_PARAMETER: 제네릭 타입 파라미터
- TYPE_USE: 타입을 쓰는 모든곳
여러 위치에 붙이려면 @Target({Type, FIELD, METHOD}) 처럼 배열로 지정할 수 있습니다.
@Retention은 어노테이션의 유효기간을 설정합니다.
- SOURCE: 자바 소스까지만 존재. 컴파일이 끝나면 흔적 없음.
-> 코드 생성이나 검사 같은 컴파일-타입 용도, 코드 스타일 체크 등 - CLASS: .class 파일에는 박히지만 JVM이 클래스 데이터를 메모리에 올릴 떄는 버림
-> 바이트코드 조작 도구나 일부 정적 분석용 어노테이션이 사용 - RUNTIME: .class 파일에 남고, JVM에 올라간 뒤 리플렉션으로도 조회 가능
2. 어노테이션 동작
1) 런타임 리플렉션 방식(@Retention(RUNTIME))
런타임 리플렉션은 프로그램이 작동하면서 어노테이션을 읽어 프록시 검증, 매핑 등 동적인 행동을 수행할 수 있는 기능입니다. JVM이 클래스를 로드할 때 어노테이션 정보도 메타-테이블에 같이 적재 합니다. 실행중에 코드가 리플렉션 API를 호출하면 그 정보를 꺼낼 수 있습니다.
런타임 기반 어노테이션을 활용하는 예시는 다음과 같습니다.
- JPA - @Entity, @Id
하이버 네이트가 클래스 로드 시 리플렉션으로 읽고 테이블 매핑을 만듦 - Spring - @Controller, @Service, @Transactional
스프링 컨테이너가 리플렉션으로 판별해 빈 등록, 프록시 생성 - 검증 프레임워크 - @NotNull, @Size
런타임에 객체를 검사할 떄, 어노테이션으로 제약을 읽어 규칙을 적용
2) 컴파일-타임 프로세싱 방식(@Retention(SOURCE 또는 CLASS))
컴파일-타임 프로세싱 방식은 JVM까지 가지 않고 javac 단계에서 행동합니다.
META-INF/services/javax.annotation.processiong.Processor 파일을 만들고, 자신의 프로세서 클래스 이름을 적습니다.
javac가 어노테이션이 붙은 요소를 탐색하고 processor()를 호출합니다. 프로세서는 새 .java 파일 혹은 새 .class 파일을 생성하거나 제약을 읽어 규칙을 적용합니다.
예시는 다음과 같습니다.
- Lombok - @Getter, @Builder
메서드를 자동으로 생성 - MapStruct - @Mapper
컴파일 시점에 매핑 코드 클래스를 자동 생성
아래 화면은 Lombokdml javax.annotation.processing.Processor를 나타냅니다.
3. 커스텀 어노테이션
직접 간단한 어노테이션을 만들어보고 테스트를 진행해보겠습니다.
1) 구현
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
CrudType value();
String message();
}
먼저 제가 만들 어노테이션 이름은 TestAnnotation 입니다.
어노테이션은 컨트롤러 실행 시 CRUD를 체크하고, 해당 컨트롤러의 정보와 간단한 메세지를 띄울 수 있도록 하였습니다.
@Aspect
@Component
public class AnnotationTest {
@Around("@annotation(test)")
public Object printInfo(ProceedingJoinPoint joinPoint, TestAnnotation test) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
CrudType crudType = test.value();
System.out.println("Test Annotation!!" + "\n" +
"message = " + test.message() + "\n" +
"className = " + className + ", methodName = " + methodName + ", crudType = " + crudType);
return joinPoint.proceed();
}
}
어떤 역할을 수행할지 작성한 AnnotationTest 클래스입니다.
@TestAnnotation이 붙은 메서드가 실행될때 가로채서, 메서드 이름, 클래스 이름, CRUD 종류, 메세지를 콘솔에 print하도록 하였습니다. 이는 Spring AOP를 이용하였는데 나중에 관련 포스팅을 진행할 예정입니다.
2) 테스트
@TestAnnotation(value = CrudType.READ, message = "테스트 메세지!")
@Operation(summary = "서버가동 체크용", description = "서버가동체크")
@GetMapping("/api/alive")
public String alive() {
return "OK";
}
마지막으로 실제 컨트롤러에 어노테이션을 적용 후, 테스트 해보았습니다.
정상적으로 작동하는 것을 확인할 수 있었습니다.
어노테이션을 공부하면서 잘 사용하면 코드를 좀더 깔끔하게 유지할 수 있겠다는 생각이 들었습니다.
반대로 무분별하게 남발하다가는 협업 시 팀원이 이해할 수 없는 코드를 작성할 수도 있겠다는 생각이 들었습니다.
뭐든지..적당히가 중요하죠..