커스텀 어노테이션 적용기
📌 서론
SpringBoot로 개발하다 보면 @Controller, @Service, @Repository, @Transactional, @Component, @Bean 등등 정말 많은 어노테이션을 사용하여 편하게 개발을 한다.
근데 스프링이 제공하는 어노테이션 말고도 개발자가 직접 본인만의 어노테이션을 만들어서 프로젝트에 적용시킬 수 있다는 것을 아는가? 아마 대부분의 개발자들은 알고 있었겠지만 사용해 볼 일은 거의 없었을 것이다. 나는 사용자 커스텀 어노테이션이 어떻게 작성되고 프로젝트에서 사용되는지 내부의 동작원리가 궁금했고 직접 만들어보기로 했다.
지금부터 Spring에 직접 커스텀 어노테이션을 만들어보면서 이게 어떻게 동작하는지 알아보자.
완성된 코드는 아래의 Repo에 존재합니다!
1. 프로젝트 세팅하기
Spring initializer를 통해 프로젝트를 생성한다.
- Gradle-Kotlin, Java17, SpringBoot3.2.3을 선택했다.
- 의존성에는 Spring Web과 Lombok을 추가해 준다.
프로젝트를 인텔리제이에서 열고 gradle 세팅에서 선택한 java의 버전을 선택한다.
- 만약 이전 프로젝트에서 자바 11을 사용했다면 새로운 프로젝트 초기 build에서 gradle오류가 나올 것이다.
- 이것은 내 java버전은 11인데 SpringBoot3.x.x부터는 자바 버전을 17 이상으로 선택해야 하기 때문에 발생하는 오류이다. Gradle 또한 JVM(17 버전)을 선택해 줘야 빌드가 성공한다.
다음으로 AOP 의존성을 직접 추가해 준다.
- 이것을 추가해 줘야 추후 @Aspect 어노테이션을 사용할 수 있게 된다.
- 추가한 후에는 꼭 build를 다시 해주도록 하자 (우측의 Gradle에서 refresh 또는 코끼리 모양의 아이콘이 나오면 클릭하기)
// aop 추가
implementation("org.springframework.boot:spring-boot-starter-aop")
2. 커스텀 어노테이션 작성하기
내가 원하는 어노테이션 클래스를 작성한다.
- @Retention 어노테이션은 어노테이션 정보가 유지되는 범위를 설정한다.
- @Target 어노테이션은 어노테이션을 적용할 수 있는 대상을 설정한다.
package com.study.customannotation.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface JinanLogging {
String message() default "커스텀 annotation 적용";
}
코드 분석하기
- @Retention(RetentionPolicy.RUNTIME) 설정으로 JinanLogging 어노테이션 정보는 런타임까지 유지된다. 즉, 프로그램 실행 중에 Reflection API를 통해 어노테이션 정보를 읽고 활용할 수 있다.
- @Target(ElementType.METHOD) 설정으로 JinanLogging 어노테이션은 메서드에만 적용할 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
- String message() 속성에는 기본값으로 "커스텀 annotation 적용"이라는 문자열이 설정되어 있다.
String message() default "커스텀 annotation 적용";
실제 메서드 적용 예시
- 아래의 코드에서 myMethod() 메서드에는 message() 속성을 명시적으로 설정하지 않았다. 따라서 기본값인 "커스텀 annotation 적용" 문자열이 사용된다.
@JinanLogging
public void myMethod() {
// ...
}
커스텀 어노테이션을 만들었다면 이제 왜 커스텀 어노테이션을 사용하는지 이해해 보도록 하자
3. 커스텀 어노테이션을 사용하는 이유
1. 코드에 메타데이터를 추가할 수 있다.
- 어노테이션은 코드 자체에 직접 정보를 추가하는 방법이다.
- 이 정보는 프로그램 실행 과정에서 활용되거나 개발자가 코드를 이해하는 데 도움이 된다.
- 내가 만든 JinanLogging 어노테이션의 경우, 메서드에 로그 메시지를 추가하는 메타데이터 역할을 한다.
1-1. 메타데이터 역할이란? (JinanLogging 어노테이션 기준)
- 어떤 메서드에 로그 메시지를 추가해야 하는지: 어노테이션이 적용된 메서드
- 로그 메시지 내용: 어노테이션의 message 속성에 지정된 문자열
1-2. 코드 적용예시
@JinanLogging(message = "데이터 처리 시작")
public void processData() {
// ...
}
위 코드에서 processData() 메서드에는 JinanLogging 어노테이션이 적용되어 있다. 이는 다음과 같은 메타데이터를 제공한다.
- 어떤 메서드에 로그 메시지를 추가해야 하는지: processData() 메서드
- 로그 메시지 내용: "데이터 처리 시작"
2. 어노테이션에 Aspect-Oriented Programming (AOP)를 지원한다.
- AOP는 공통 관심사 (로깅, 보안, 트랜잭션 관리 등)를 분리하여 코드에 적용하는 프로그래밍 기법이다.
- 어노테이션은 AOP에서 어드바이스 (Advice)를 적용할 대상을 지정하는 데 사용된다.
- @Aspect 어노테이션과 함께 @JinanLogging 어노테이션을 사용하면, JinanLogging 어노테이션이 적용된 메서드에만 로깅 로직을 추가할 수 있다. (내가 원하는 메서드에 특정 기능을 적용시킬 수 있게 된다.)
지금까지 작성한 @JinanLogging 어노테이션 자체는 단순히 메서드에 로그 메시지를 추가할지 여부를 구분하는 역할(메타데이터)만 수행한다. 실제 로깅 로직은 @Aspect 어노테이션(AOP)을 통해 추가하게 될 것이다.
4. 커스텀 어노테이션에 AOP 적용하기
커스텀 어노테이션의 동작을 적용시킬 AOP 작성
- 아래의 코드는 @JinanLogging 어노테이션이 적용된 메서드의 동작을 AOP를 통해 적용시키는 코드다.
package com.study.customannotation.annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* CustomAnnotationAspect 클래스의 주요 역할
* @JinanLogging 어노테이션이 적용된 메서드의 동작을 가로채서 전후에 원하는 로직을 실행한다.
*/
@Aspect
@Component
public class CustomAnnotationAspect {
// @Around를 통해 @JinanLogging 어노테이션이 적용된 메서드가 실행될 때 logAround 메서드 내부의 코드가 작동하도록 지정
@Around("@annotation(com.study.customannotation.annotation.JinanLogging)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. MethodSignature로 형변환
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 2. Method 객체로부터 어노테이션 추출
JinanLogging annotation = method.getAnnotation(JinanLogging.class);
String message = annotation.message();
String methodName = methodSignature.getName();
System.out.println("[**" + methodName + "**] 실행 전: " + message);
Object result = joinPoint.proceed();
System.out.println("[**" + methodName + "**] 실행 후: " + message);
return result;
}
}
코드 분석
CustomAnnotationAspect 클래스
- @Aspect 어노테이션: AOP 어드바이스를 포함하는 클래스임을 표시
- @Component 어노테이션: Spring 빈으로 등록
@Aspect
@Component
public class CustomAnnotationAspect {
...
}
logAround 메서드
- @Around 어노테이션: @JinanLogging 어노테이션이 적용된 메서드 실행 전후에 실행
- 전후가 아니라 전, 후 중에 원하는 실행 순서가 있다면 @Around 대신 @Before나 @After를 적용하면 된다.
- ProceedingJoinPoint 객체: 어드바이스가 적용되는 메서드에 대한 정보 제공
- MethodSignature 객체: 어드바이스가 적용되는 메서드의 메타데이터 제공
- JinanLogging 어노테이션: 메서드에서 추출
- message: 어노테이션에서 설정된 로그 메시지(추출된 어노테이션의 message 속성 값)
- methodName: 어드바이스가 적용되는 메서드 이름
@Around("@annotation(com.study.customannotation.annotation.JinanLogging)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. MethodSignature로 형변환
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 2. Method 객체로부터 어노테이션 추출
JinanLogging annotation = method.getAnnotation(JinanLogging.class);
String message = annotation.message();
String methodName = methodSignature.getName();
System.out.println("[**" + methodName + "**] 실행 전: " + message);
Object result = joinPoint.proceed();
System.out.println("[**" + methodName + "**] 실행 후: " + message);
return result;
}
동작 이해하기
- @JinanLogging 어노테이션이 적용된 메서드가 실행될 때 logAround 메서드가 실행된다.
- logAround 메서드는 다음을 수행한다.
- 어노테이션에서 로그 메시지와 메서드 이름 추출
- 메서드 실행 전에 로그 메시지 출력
- 원본 메서드 실행
- 메서드 실행 후에 로그 메시지 출력
5. 컨트롤러 클래스 작성 및 테스트
테스트를 위해 컨트롤러를 생성한다. (3개의 메서드 작성)
- 커스텀 어노테이션의 default 값을 사용하는 메서드
- 커스텀 어노테이션의 message값을 내가 설정한 메서드
- 커스텀 어노테이션을 사용하지 않는 메서드
package com.study.customannotation.controller;
import com.study.customannotation.annotation.JinanLogging;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AnnotationController {
@JinanLogging
@GetMapping("/customAnnotation")
public void customAnnotationMethod() {
System.out.println("커스텀 어노테이션 적용 메소드 실행");
}
@JinanLogging(message = "커스텀 어노테이션 메시지 직접 작성")
@GetMapping("/customAnnotationMethodSetMessage")
public void customAnnotationMethodSetMessage() {
System.out.println("커스텀 어노테이션 적용 메소드 실행");
}
@GetMapping("/nonAnnotation")
public void nonAnnotationMethod() {
System.out.println("커스텀 어노테이션 미적용 메소드 실행");
}
}
다음으로 intelliJ에서 지원하는 http 파일을 만들어서 테스트를 한다.
- intellj에서는 파일을 .http로 만들면 postman처럼 http요청을 테스트할 수 있는 파일을 생성해 준다.
- 아래와 같이 내가 만든 Get매핑을 3개 만들어 준다.
- ### 이렇게 #을 3개 적어주는 것이 각 요청의 구분자이니 꼭 적어주도록 하자
### 커스텀 어노테이션 적용 (default 메시지)
GET http://localhost:8080/customAnnotation
### 커스텀 어노테이션 적용 (내가 설정한 메시지)
GET http://localhost:8080/customAnnotationMethodSetMessage
### 커스텀 어노테이션 미적용
GET http://localhost:8080/nonAnnotation
첫 번째 http요청
- 어노테이션에 default메시지가 적용되어 "커스텀 annotation 적용"이라는 메시지가 추가된다.
@JinanLogging
두 번째 http요청
- 어노테이션에 내가 작성한 message가 출력되는 것을 확인할 수 있다.
@JinanLogging(message = "커스텀 어노테이션 메시지 직접 작성")
세 번째 http요청
- 어노테이션을 적용시키지 않았으므로 아무런 추가 동작이 없는 것을 확인할 수 있다.
6. 결론
내가 만든 @JinanLogging 어노테이션과 스프링에서 제공하는 어노테이션은 동일한 원리로 작동한다.
그렇기에 어노테이션을 사용만 하기보단 직접 한번 만들어보는 것이 동작원리를 이해하는데 큰 도움이 된다. 가끔 프로젝트를 하다 보면 이렇게 커스텀 어노테이션을 통해 로직을 구현하는 방법이 사용되기도 하니 기회가 된다면 적용시켜 보도록 하자!
마지막으로 간단하게 Spring이 제공하는 어노테이션과 내가 만든 어노테이션을 비교하면서 마무리하도록 하겠다.
1. 어노테이션 처리
- 컴파일 시: 어노테이션 프로세서가 어노테이션을 검사하고 필요한 메타데이터를 생성한다.
- 런타임 시: 스프링 컨테이너는 어노테이션 정보를 기반으로 빈 생성, 의존성 주입, AOP 어드바이스 적용 등을 수행한다.
2. @JinanLogging 어노테이션
- @JinanLogging 어노테이션은 메서드 실행 전후에 로그 메시지를 출력하는 어노테이션이다.
- @Around 어노테이션을 기반으로 구현되었으며, 어노테이션 속성으로 메시지를 설정할 수 있다.
3. 스프링 어노테이션
- @Transactional, @ControllerAdvice, @Controller 등 다양한 어노테이션을 제공한다.
- 각 어노테이션은 특정 기능을 수행하도록 설계되었으며, 어노테이션 속성을 통해 추가적인 설정을 할 수 있다.
4. 결론
- @JinanLogging 어노테이션은 스프링에서 제공하는 어노테이션과 동일한 원리로 작동한다.
- 어노테이션은 코드에 직접 로직을 작성하는 대신 메타데이터를 통해 코드를 보다 명확하고 이해하기 쉽게 만들 수 있으며, 유지 관리 및 재사용성을 높일 수 있다.
사이드 팀원 "평양냉면"님의 블로그도 한번 들려주세요! 좋은 글이 많습니다.
'Spring 기초 > Spring 기초 지식' 카테고리의 다른 글
[Spring] 코틀린 스프링에서 Validation 적용 방법과 주의점 (3) | 2024.06.06 |
---|---|
[Spring] Spring Event 스레드의 동작원리 (동기/비동기) (5) | 2024.03.10 |
[Spring] @Transactional: 트랜잭션 전파 처리과정 (8) | 2024.02.17 |
[Spring] @ModelAttribute 바인딩 실패와 해결 (33) | 2024.02.01 |
[Spring] 톰캣과 스프링: 웹 요청의 라이프사이클 이해하기 (27) | 2023.12.31 |