3 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

예제 만들기

지금까지 학습한 내용을 활용해서 유용한 스프링 AOP를 만들어보자.

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

먼저 AOP를 적용할 예제를 만들자.

해당 위치에 예제 리포지토리를 하나 만든다.

ExamRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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";  
    }  
}

5번에 1번 정도 실패하는 저장소이다. 이렇게 간헐적으로 실패할 경우 재시도 하는 AOP가 있으면 편리하다.

ExamService

1
2
3
4
5
6
7
8
9
10
@RequiredArgsConstructor  
@Service  
public class ExamService {  
  
    private final ExamRepository examRepository;  
  
    public void request(String itemId) {  
        examRepository.save(itemId);  
    }  
}

실행시키는 서비스를 만들었다.

이제 테스트 코드를 작성하자.

ExamTest

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootTest  
public class ExamTest {  
  
    @Autowired  
    ExamService examService;  
  
    @Test  
    void test() {  
        for (int i = 0; i < 5; i++) {  
            examService.request("data" + i);  
        }  
    }  
}

실행해보면 테스트가 5번째 루프를 실행할 때 리포지토리 위치에서 예외가 발생하면서 실패하는 것을 확인할 수 있다.

로그 출력 AOP

먼저 로그 출력용 AOP를 만들어보자.

@Trace가 메서드에 붙어 있으면 호출 정보가 출력되는 편리한 기능이다.

Trace

1
2
3
4
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
public @interface Trace {  
}

이 애노테이션을 감지할 TraceAspect를 만들자.

hello.aop.exam.aop.TraceAspect

1
2
3
4
5
6
7
8
9
10
@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);  
    }  
}

@annotation(hello.aop.exam.annotation.Trace)포인트컷을 사용해서 @Trace가 붙은 메서드에 어드 바이스를 적용한다.

이제 각 메서드에 애노테이션을 붙혀보자.

1
2
3
4
5
6
7
8
9
10
11
@RequiredArgsConstructor  
@Service  
public class ExamService {  
  
    private final ExamRepository examRepository;  
  
    @Trace  
    public void request(String itemId) {  
        examRepository.save(itemId);  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Repository  
public class ExamRepository {  
  
    private static int seq = 0;  
  
    /**  
     * 5번에 1번 실패하는 요청  
     * */  
    @Trace  
    public String save(String itemId) {  
        seq++;  
        if (seq % 5 == 0) {  
            throw new IllegalStateException("예외 발생!");  
        }  
        return "ok";  
    }  
}

자 마지막으로 테스트 코드에 애스펙트를 임포트 하면 끝이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest  
@Import(TraceAspect.class)  
public class ExamTest {  
  
    @Autowired  
    ExamService examService;  
  
    @Test  
    void test() {  
        for (int i = 0; i < 5; i++) {  
            examService.request("data" + i);  
        }  
    }  
}

실행해보면 @Trace가 붙은 request(), save()호출 시 로그가 잘 남는 것을 확인할 수 있다.

재시도 AOP

이번에는 좀 더 의미 있는 재시도 AOP를 만들어보자.

@Retry 애노테이션이 있으면 예외가 발생했을 때 다시 시도해서 문제를 복구한다.

hello.aop.exam.annotation.Retry

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

이 애노테이션에는 재시도 횟수로 사용할 값이 있다. 기본값으로 3을 사용한다.

애스펙트를 만들자.

hello.aop.exam.aop.RetryAspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Slf4j  
@Aspect  
public class RetryAspect {  
    @Around("@annotation(retry)")  
    public Object doRetry(ProceedingJoinPoint joinPoint, Retry retry) throws Throwable {  
        log.info("[retry] {} retry={}", joinPoint.getSignature(), retry);  
        Exception exceptionHolder = null;  
        int maxRetry = retry.value();  
  
        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에 적용해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Repository  
public class ExamRepository {  
  
    private static int seq = 0;  
  
    /**  
     * 5번에 1번 실패하는 요청  
     * */  
    @Trace  
    @Retry(value = 4)  
    public String save(String itemId) {  
        seq++;  
        if (seq % 5 == 0) {  
            throw new IllegalStateException("예외 발생!");  
        }  
        return "ok";  
    }  
}

ExamRepository.save()메서드에 @Retry(value = 4)를 적용했다. 이 메서드에서 문제가 발생하면 4번 재시도 한다.

이제 마지막으로 테스트코드에 임포트 해서 확인해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest  
@Import({TraceAspect.class, RetryAspect.class})  
public class ExamTest {  
  
    @Autowired  
    ExamService examService;  
  
    @Test  
    void test() {  
        for (int i = 0; i < 5; i++) {  
            examService.request("data" + i);  
        }  
    }  
}

실행 결과를 보면 5번째 문제가 발생했을 때 재시도 덕분에 문제가 복구 되고, 정상 응답 되는 것을 확인할 수 있다.

참고 : 스프링이 제공하는 @Transactional은 가장 대표적인 AOP이다.

댓글남기기