Back-End/Spring Advance & Boot

스프링 고급편 - 실전예제

Meluu_ 2024. 11. 13. 14:46

✔️ 예제 만들기


유요한 스프링 AOP 만들기

  • @Trace : 애노테이션으로 로그 출력
  • @Retry : 애노테이션으로 예외 발생시 재시도

 

ExamRepository

@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";
    }
}

 

ExamService

@Service
@RequiredArgsConstructor
public class ExamService {

    private final ExamRepository examRepository;

    @Trace
    public void request(String itemId) {
        examRepository.save(itemId);
    }
}

 

ExamTest

@Slf4j
@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);
        }
    }
}

 

5번 요청하는 테스트이며, 5번째 요청시 리포지토리에서 예외 발생하여 실패한다.

 

✔️ 로그 출력 AOP


 

@Trace 애노테이션을 붙이면 호출 정보(log)가 출력되는 기능을 가진 AOP

 

@Trace

@Target(ElementType.METHOD) // 메서드타겟 
@Retention(RetentionPolicy.RUNTIME) // 애노테이션 유지 정책, 런타임(동적)에 사용 가능 
public @interface Trace {
}

 

TraceAspect

@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);

    }
}

  다시 말하지만 @Aspect 했다고 스프링 빈에 등록되는 것이 아니다. Import or Bean 수동 등록 or @Component로 등록해야한다.

 

@Slf4j
@Import(TraceAspect.class)
@SpringBootTest
public class ExamTest {...}

 

 

실행 결과

 hello.aop.exam.ExamTest                  : client request i=0
 hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data0]
 hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data0]
 hello.aop.exam.ExamTest                  : client request i=1
 hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data1]
 hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data1]
 hello.aop.exam.ExamTest                  : client request i=2
 hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data2]
 hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data2]
 hello.aop.exam.ExamTest                  : client request i=3
 hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data3]
 hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data3]
 hello.aop.exam.ExamTest                  : client request i=4
 hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data4]
 hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
 
 java.lang.IllegalStateException: 예외 발생

 

 

✔️ 재시도 AOP


@Retry

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
    int value() default 3;
}

 

RetryAspect

@Slf4j
@Aspect
public class RetryAspect {

    // 원래는 @annotation(Retry 패키지명)을 다 적었어야하는데 retry로 대체가능하다. retry에 타입정보가 있기에
    @Around("@annotation(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 (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
            try {
                log.info("[retry] try count={}/{}", retryCount, maxRetry);
                return joinPoint.proceed();
            } catch (Exception e) {
                exceptionHolder = e;
            }
        }

        throw exceptionHolder;
    }
}
  • 재시도 애스팩트
  • @annotation(retry), Retry retry 를 사용해서 어드바이스에 애노테이션을 파라미터로 전달
  • retry.value() 를 통해서 애노테이션에 지정한 값을 가져올 수 있음
  • 예외 발생하여 결과 정상 반환되지 않으면 retry.value() 만큼 재시도

ExamRepository

    @Trace
    @Retry(value = 4) // 중요한 점은 횟수 제한이 있어야한다., 파라미터로 값을 넣어 변경 가능 , value 생략 가능
    public String save(String itemId) {
     seq++;
        if (seq % 5 == 0) {
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }

 

@Slf4j
@Import({TraceAspect.class, RetryAspect.class})
@SpringBootTest
public class ExamTest {...}

 

실행결과

hello.aop.exam.ExamTest                  : client request i=0
hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data0]
hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data0]
hello.aop.exam.aop.RetryAspect           : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect           : [retry] try count=1/4
hello.aop.exam.ExamTest                  : client request i=1
hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data1]
hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data1]
hello.aop.exam.aop.RetryAspect           : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect           : [retry] try count=1/4
hello.aop.exam.ExamTest                  : client request i=2
hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data2]
hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data2]
hello.aop.exam.aop.RetryAspect           : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect           : [retry] try count=1/4
hello.aop.exam.ExamTest                  : client request i=3
hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data3]
hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data3]
hello.aop.exam.aop.RetryAspect           : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect           : [retry] try count=1/4
hello.aop.exam.ExamTest                  : client request i=4
hello.aop.exam.aop.TraceAspect           : [trace] void hello.aop.exam.ExamService.request(String) args=[data4]
hello.aop.exam.aop.TraceAspect           : [trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
hello.aop.exam.aop.RetryAspect           : [retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(4)
hello.aop.exam.aop.RetryAspect           : [retry] try count=1/4
hello.aop.exam.aop.RetryAspect           : [retry] try count=2/4

 

실행 로직 이해

재시도 애스펙트는 애노테이션 value 값만큼 재시도

test에서 5번 service를 호출

repository.save에서 seq가 5일때 예외 발생

@Retry가 붙은 save 메서드는 value만큼 재시도 

seq가 6이므로 정상적으로 결과 반환

try count는 2/4 로 변경됨 (1 -> 2번째 시도)

 

 

 

ps. 

AOP에 대해서 배우고 나니 @Transactional이 이렇게 만들어진거구나 싶다. 

항상 애노테이션을 어떻게 작동하게 하는지도 몰랐는데 애스펙트를 사용하면 된다는 것을 알게되었다. 

 

🔖 학습내용 출처


스프링 핵심 원리 - 고급편 / 김영한