어노테이션 기반 개발의 예제를 몇가지 만들어보자.
@Trace: 로그 출력
@Retry: 예외 발생해도 일정 횟수 재시도.
과 같은 유용한 AOP를 만들어보자.
우선, AOP를 적용할 예제를 만들어보자.
Repository
5번에 1번은 실패하는 Repository를 구성한다.
@Repository
public class ExamRepository {
private static int seq = 0;
/**
* 5번에 1번 실패하는 요청
*/
public String save(String itemId) {
seq++;
if (seq % 5 == 0) {
throw new IllegalStateException("예외 발생");
}
return "ok";
}
}
Service
단순히 Repository를 호출하는 서비스
@Service
@RequiredArgsConstructor
public class ExamService {
private final ExamRepository examRepository;
@Trace
public void request(String itemId) {
examRepository.save(itemId);
}
}
Test Code
@Slf4j
//@Import(TraceAspect.class)
@Import({TraceAspect.class, RetryAspect.class})
@SpringBootTest
public class ExamTest {
@Autowired
ExamService examService;
@Test
void test() {
for (int i = 0; i < 5; i++) {
log.info("client request i={}", i);
examService.request("data" + i);
}
}
}
예제를 구성했으니, 이제 어노테이션 기반 로직을 만들기 위해 Meta Annotation에 대해 알아보자.
Meta Annotation
다른 어노테이션에서도 사용되는 어노테이션을 말한다.
주로 커스텀 어노테이션을 만들 때 사용된다.
@Controller, @Service, @Repository등은 컴포넌트 스캔을 위해 @Component 를 내포하고 있는데, 여기서 @Component를
meta annotation이라고 한다.
ex
@Repository 어노테이션의 내부
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any (or empty String otherwise)
*/
@AliasFor(annotation = Component.class)
String value() default "";
}
@Target
자바 컴파일러에게 해당 어노테이션이 어디에 적용될지 알려준다.
위의 예시에서 ElementType.TYPE 은, 해당 어노테이션이 타입 선언에 사용된다는 의미이다.
ElementType.PACKAGE : 패키지 선언
ElementType.TYPE : 타입 선언
ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언
ElementType.CONSTRUCTOR : 생성자 선언
ElementType.FIELD : 멤버 변수 선언
ElementType.LOCAL_VARIABLE : 지역 변수 선언
ElementType.METHOD : 메서드 선언
ElementType.PARAMETER : 전달인자 선언
ElementType.TYPE_PARAMETER : 전달인자 타입 선언
ElementType.TYPE_USE : 타입 선언
@Retention
해당 어노테이션의 적용, 유지 범위를 나타낸다.
RetentionPolicy.RUNTIME
RetentionPolicy.CLASS
RetentionPolicy.SOURCE
RUNTIME: 컴파일 이후에도 JVM에 의해서 참조가 가능하다. 주로 리플랙션, 로깅에 많이 사용된다.
CLASS: 컴파일러가 클래스를 참조할 때까지 유효하다.
SOURCE: 컴파일 전까지만 유효하다.
이제 @Trace를 통해 로그를 출력하도록 커스텀 어노테이션을 만들어보자.
@Trace(로그 추적을 수행하는 어노테이션 만들기)
Trace 메타 어노테이션
@Target(ElementType.METHOD) //적용 대상은 메소드만
@Retention(RetentionPolicy.RUNTIME) //어노테이션 정보가 런타임에 읽고 처리될 수 있음을 의미한다.
public @interface Trace {
}
Aspect
로그를 적용하기 위한 어드바이저를 담은 Aspect를 만든다.
@Slf4j
@Aspect
public class TraceAspect {
@Before("@annotation(hello.aop.exam.annotation.Trace)")
public void doTrace(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
log.info("[trace] {} args={}", joinPoint.getSignature(), args);
}
}
포인트 컷에는 방금 만든 @Trace을 등록하여 해당 어노테이션에 대해 doTrace에 정의된 Advice를 적용하도록 한다.
@Before을 통해 메소드 실행전에, 넘어오는 파라미터 정보와 시그니처(메소드 정보)를 출력해보자.
**주의
PorceedingJoinPoint가 아닌 JoinPoint를 파라미터로 전달 받는다.
이제 방금 예제의 Repository의 save, Service의 request에 @Trace를 붙이고
@Import(TraceAspect.class)
등을 통해 aspect를 스프링 빈으로 등록하면(내부 모든 어드바이저를 스프링 빈에 등록) 아래와 같은 결과가 나온다.
@Retry(재실행 어노테이션 만들기)
외부 호출 API(ex: google 로그인 API) 등등은 간혹, 호출된 API쪽으 문제로 실패할 경우가 있다. 따라서, 외부 API를 호출하는 부분은
단순히 재실행하는 것 만으로도 성공하는 경우가 있다. 이럴 때 재실행 어노테이션을 사용하면 유용할 수 있다.
@Retry 메타 어노테이션 생성
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry { //외부 호출 API같은 경우, 호출쪽에서 문제일 경우가 간혹 있다. 이경우, 단순 재조회로 해결가능.
int value() default 3; //기본 3회
}
Aspect
마찬가지로 aspect를 구성해주자.
@Slf4j
@Aspect
public class RetryAspect {
@Around("@annotation(retry)") //아래의 "Retry retry"를 통한 파라미터 전달로 인해 포인트컷에 자동으로 어노테이션 타입이 "Retry"인 것만 들어오게 된다.
public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {
log.info("[retry] {} retry={}", joinPoint.getSignature(), retry);
int maxRetry = retry.value();
Exception exceptionHolder = null; //아래 for문에서 발생하는 예외를 담아둔다.
for (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
try {
log.info("[retry] try count={}/{}", retryCount, maxRetry);
return joinPoint.proceed();
} catch (Exception e) {
exceptionHolder = e; //exceptionHolder에 예외 담기 -> 몇번의 재시도 동안, 예외가 발생해도 계속 다시 시도하게
}
}
throw exceptionHolder;
}
}
@annotation을 통한 파라미터 전달 방식을 사용했기에 포인트 컷에서 자동으로 Retry타입만 통과되도록 해주기에 @annotation(xxx) 에서 xxx에 구체적인 패키지 경로까지 담지 않아도 되서 가독성과 편리성을 좋게 했다.
이후 retry.value()를 통해 앞에서 @interface Retry에 구성한 value(기본값은 3)를 받아오고, 그 횟수만큼 수행되도록 한다.
만약 지정된 3회 모두 예외가 발생 시, 마지막으로 발생한 예외를 throw하게 된다.
이제 Repository영역에 구성한 Annotation들을 적용해보자.
Repository
@Repository
public class ExamRepository {
private static int seq = 0;
/**
* 5번에 1번 실패하는 요청
*/
@Trace
@Retry(value = 4) //retry 횟수(value)를 기본값인 3에서 4로 변경
public String save(String itemId) {
seq++;
if (seq % 5 == 0) {
throw new IllegalStateException("예외 발생");
}
return "ok";
}
}
이제
@Import({TraceAspect.class, RetryAspect.class})
등을 톨해 Retry Aspect까지 빈으로 등록한 뒤 실행해보자.
구성한 Repository는 5회째에서 문제가 발생하게 된다. 만약 문제가 발생하면, 재시도를 수행하고, 그 결과 다음 시도에 성공하게 된다.
참고
이외에, 커스텀 어노테이션 말고 스프링이 제공하는 유용한 AOP 어노테이션들도 존재하는데, 대표적으로 @Transactional가 있다.
실무에서 만드는 방법
로그 출력 AOP: 현장마다 다르지만,
특정 시간 이상 실행되거나, 에러가 터지면 로그로 남기기
(1초걸리면 info로 남기고, 5초넘으면 log.error로 남기고...)
Retry: 어노테이션을 사용하는 메소드가 어노테이션의 파라미터로 재시도 횟수를 전달하면
그에 맞게 Advice가 재시도를 수행하도록 매개변수 전달을 사용했다.
'Spring boot' 카테고리의 다른 글
스프링 MVC (0) | 2023.10.21 |
---|---|
스프링 핵심원리 13(완) [스프링 AOP 실전 주의 사항] (0) | 2023.06.19 |
스프링 핵심원리 11 [포인트 컷] (0) | 2023.06.17 |
스프링 핵심원리 10 [스프링 AOP 사용법] (0) | 2023.06.17 |
스프링 핵심원리 9 [스프링 AOP] (0) | 2023.06.17 |