1. 예제 프로젝트 구성
1.1. 실전 예제 목표
이번 장에서는 지금까지 학습한 스프링 AOP 개념을 활용하여 실무에서 유용하게 사용할 수 있는 두 가지 AOP를 직접 구현해본다:
- 로그 출력 AOP: @Trace 애노테이션을 사용하여 메서드 호출 정보를 로그로 출력
- 재시도 AOP: @Retry 애노테이션을 사용하여 예외 발생 시 자동으로 재시도
1.2. 기본 예제 프로젝트 구성
1.2.1. ExamRepository - 저장소 클래스
간헐적으로 실패하는 저장소를 시뮬레이션하는 클래스이다.
package hello.aop.exam;
import org.springframework.stereotype.Repository;
@Repository
public class ExamRepository {
// 테스트를 위한 시퀀스 카운터
private static int seq = 0;
/**
* 5번에 1번 실패하는 저장 메서드
* @param itemId 저장할 아이템 ID
* @return 저장 결과
* @throws IllegalStateException 5번째 호출 시 예외 발생
*/
public String save(String itemId) {
seq++; // 호출 횟수 증가
// 5번에 1번 실패하도록 구현
if (seq % 5 == 0) {
throw new IllegalStateException("예외 발생");
}
return "ok";
}
}
동작 원리:
- 정적 변수 seq를 사용하여 호출 횟수를 카운트
- 5번에 한 번씩(seq % 5 == 0) IllegalStateException 발생
- 이렇게 간헐적으로 실패하는 시나리오는 재시도 AOP의 필요성을 보여줌
1.2.2. ExamService - 서비스 클래스
비즈니스 로직을 처리하는 서비스 클래스이다.
package hello.aop.exam;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor // Lombok: final 필드 생성자 자동 생성
public class ExamService {
private final ExamRepository examRepository;
/**
* 아이템 저장 요청 처리
* @param itemId 저장할 아이템 ID
*/
public void request(String itemId) {
examRepository.save(itemId);
}
}
설명:
- @RequiredArgsConstructor: Lombok 애노테이션으로 final 필드에 대한 생성자 자동 생성
- ExamRepository를 의존성으로 주입받아 사용
- 단순히 저장소의 save() 메서드를 호출하는 역할
1.2.3. ExamTest - 테스트 클래스
예제를 테스트하는 클래스이다.
package hello.aop.exam;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class ExamTest {
@Autowired
ExamService examService;
@Test
void test() {
// 5번 반복 실행
for (int i = 0; i < 5; i++) {
examService.request("data" + i);
}
}
}
초기 실행 결과 (AOP 적용 전):
// 1-4번: 정상 실행
// 5번째 실행 시 예외 발생
java.lang.IllegalStateException: 예외 발생
at hello.aop.exam.ExamRepository.save(ExamRepository.java:15)
...
문제점:
- 5번째 호출에서 항상 예외 발생
- 애플리케이션 비정상 종료
- 재시도 기능이 없어 실패한 작업 복구 불가
2. 로그 출력 AOP 구현
2.1. @Trace 애노테이션 정의
메서드에 로그 출력을 적용하기 위한 커스텀 애노테이션을 정의한다.
package hello.aop.exam.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 메서드에만 적용 가능
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
public @interface Trace {
// 추가 속성 없이 마커 애노테이션으로 사용
}
애노테이션 설정 설명:
- @Target(ElementType.METHOD): 이 애노테이션은 메서드에만 적용 가능
- @Retention(RetentionPolicy.RUNTIME): 런타임까지 애노테이션 정보 유지 (AOP에서 접근 가능)
- 마커 애노테이션(Marker Annotation): 속성이 없는 단순 표식 애노테이션
2.2. TraceAspect 구현
@Trace 애노테이션이 적용된 메서드의 호출 정보를 로그로 출력하는 Aspect를 구현한다.
package hello.aop.exam.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Slf4j // SLF4J 로거 자동 생성
@Aspect // 이 클래스가 Aspect임을 선언
public class TraceAspect {
/**
* @Trace 애노테이션이 있는 메서드 실행 전에 호출
* @param joinPoint 조인 포인트 정보
*/
@Before("@annotation(hello.aop.exam.annotation.Trace)")
public void doTrace(JoinPoint joinPoint) {
// 메서드 인자 정보 가져오기
Object[] args = joinPoint.getArgs();
// 로그 출력: 메서드 시그니처와 인자 값
log.info("[trace] {} args={}", joinPoint.getSignature(), args);
}
}
코드 분석:
- @Before 어드바이스:
- 메서드 실행 전에 실행되는 어드바이스
- @annotation(...) 포인트컷: 지정된 애노테이션이 있는 메서드만 대상
- JoinPoint 파라미터:
- joinPoint.getSignature(): 메서드 시그니처 정보 (메서드 이름, 반환 타입, 선언 클래스 등)
- joinPoint.getArgs(): 메서드에 전달된 인자 배열
- 로깅 포맷:
- [trace]: 로그 구분자
- 메서드 시그니처: 어떤 메서드가 호출되었는지 확인
- 인자 값: 어떤 값으로 메서드가 호출되었는지 확인
2.3. @Trace 애노테이션 적용
2.3.1. ExamService에 @Trace 적용
package hello.aop.exam;
import hello.aop.exam.annotation.Trace;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ExamService {
private final ExamRepository examRepository;
@Trace // 로그 출력 AOP 적용
public void request(String itemId) {
examRepository.save(itemId);
}
}
2.3.2. ExamRepository에 @Trace 적용
package hello.aop.exam;
import hello.aop.exam.annotation.Trace;
import org.springframework.stereotype.Repository;
@Repository
public class ExamRepository {
private static int seq = 0;
@Trace // 로그 출력 AOP 적용
public String save(String itemId) {
seq++;
if (seq % 5 == 0) {
throw new IllegalStateException("예외 발생");
}
return "ok";
}
}
2.4. TraceAspect 등록 및 테스트
2.4.1. 테스트 클래스에 Aspect 등록
package hello.aop.exam;
import hello.aop.exam.aop.TraceAspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
@Import(TraceAspect.class) // TraceAspect를 스프링 빈으로 등록
@SpringBootTest
public class ExamTest {
@Autowired
ExamService examService;
@Test
void test() {
for (int i = 0; i < 5; i++) {
examService.request("data" + i);
}
}
}
@Import의 역할:
- TraceAspect 클래스를 스프링 빈으로 등록
- @Aspect 클래스는 컴포넌트 스캔 대상이 아니므로 명시적 등록 필요
- 테스트 환경에서 쉽게 Aspect를 추가/제거할 수 있음
2.4.2. 실행 결과
[trace] void hello.aop.exam.ExamService.request(String) args=[data0]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data0]
[trace] void hello.aop.exam.ExamService.request(String) args=[data1]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data1]
[trace] void hello.aop.exam.ExamService.request(String) args=[data2]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data2]
[trace] void hello.aop.exam.ExamService.request(String) args=[data3]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data3]
[trace] void hello.aop.exam.ExamService.request(String) args=[data4]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
로그 분석:
- 메서드 호출 순서를 명확히 확인 가능
- 각 메서드에 전달된 인자 값 확인 가능
- 예외 발생 지점(5번째 save() 호출)도 로그로 기록됨
- 디버깅과 모니터링에 매우 유용한 정보 제공
2.5. 로그 출력 AOP의 실무 활용
2.5.1. 다양한 로깅 레벨 지원
@Slf4j
@Aspect
public class TraceAspect {
@Before("@annotation(hello.aop.exam.annotation.Trace)")
public void doTrace(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
// DEBUG 레벨: 상세 정보 (성능 영향 고려)
log.debug("[trace] {} args={}", joinPoint.getSignature(), args);
// INFO 레벨: 기본 정보
log.info("[trace] {}", joinPoint.getSignature());
// 성능 측정 시작
long startTime = System.currentTimeMillis();
}
@AfterReturning("@annotation(hello.aop.exam.annotation.Trace)")
public void doTraceAfter(JoinPoint joinPoint) {
// 성능 측정 종료 및 로깅
log.info("[trace] {} completed", joinPoint.getSignature());
}
@AfterThrowing(value = "@annotation(hello.aop.exam.annotation.Trace)",
throwing = "ex")
public void doTraceException(JoinPoint joinPoint, Exception ex) {
// 예외 발생 시 로깅
log.error("[trace] {} exception={}", joinPoint.getSignature(), ex.getMessage());
}
}
2.5.2. 조건부 로깅
@Slf4j
@Aspect
public class ConditionalTraceAspect {
@Around("@annotation(trace)")
public Object doConditionalTrace(ProceedingJoinPoint joinPoint, Trace trace)
throws Throwable {
// 특정 조건에서만 로깅 (예: 특정 파라미터 값일 때)
Object[] args = joinPoint.getArgs();
if (shouldLog(args)) {
log.info("[trace] {} args={}", joinPoint.getSignature(), args);
}
return joinPoint.proceed();
}
private boolean shouldLog(Object[] args) {
// 로깅 조건 로직 구현
// 예: 특정 값이 포함된 경우, 특정 시간대인 경우 등
return true;
}
}
3. 재시도 AOP 구현
3.1. @Retry 애노테이션 정의
재시도 횟수를 설정할 수 있는 커스텀 애노테이션을 정의한다.
package hello.aop.exam.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 메서드에만 적용 가능
@Retention(RetentionPolicy.RUNTIME) // 런타임까지 유지
public @interface Retry {
/**
* 재시도 횟수 (기본값: 3)
* @return 최대 재시도 횟수
*/
int value() default 3;
}
애노테이션 특징:
- value() 메서드: 재시도 횟수를 설정하는 속성
- default 3: 기본값으로 3회 재시도
- 애노테이션 값을 AOP에서 읽어 동적으로 재시도 횟수 제어 가능
3.2. RetryAspect 구현
@Retry 애노테이션이 적용된 메서드에서 예외 발생 시 지정된 횟수만큼 재시도하는 Aspect를 구현한다.
package hello.aop.exam.aop;
import hello.aop.exam.annotation.Retry;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Slf4j
@Aspect
public class RetryAspect {
/**
* @Retry 애노테이션이 있는 메서드에 재시도 로직 적용
* @param joinPoint 조인 포인트
* @param retry @Retry 애노테이션 인스턴스
* @return 메서드 실행 결과
* @throws Throwable 모든 재시도 실패 시 최종 예외
*/
@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) {
// 예외 발생 시 기록
log.info("[retry] exception={}", e.getMessage());
exceptionHolder = e;
// 마지막 시도가 아니면 계속 진행
if (retryCount < maxRetry) {
// 재시도 전 잠시 대기 (선택적)
// Thread.sleep(100);
}
}
}
// 모든 재시도 실패 시 최종 예외 발생
throw exceptionHolder;
}
}
코드 분석:
3.2.1. @Around 어드바이스
- @Around("@annotation(retry)"): @Retry 애노테이션이 있는 메서드 대상
- Retry retry: 애노테이션 인스턴스를 파라미터로 받음 (매개변수 바인딩)
3.2.2. 재시도 로직
- 최대 재시도 횟수 설정:
int maxRetry = retry.value(); // 애노테이션의 value 값 가져오기 - 재시도 루프:
for (int retryCount = 1; retryCount <= maxRetry; retryCount++) { try { // 메서드 실행 return joinPoint.proceed(); } catch (Exception e) { // 예외 처리 exceptionHolder = e; } } - 예외 보관:
마지막에 발생한 예외를 보관하여 모든 재시도 실패 시 던짐Exception exceptionHolder = null; // ... exceptionHolder = e;
3.2.3. 로깅
- 각 재시도 시도마다 로그 기록
- 예외 발생 시 예외 메시지 기록
- 디버깅과 모니터링에 유용
3.3. @Retry 애노테이션 적용
3.3.1. ExamRepository에 @Retry 적용
package hello.aop.exam;
import hello.aop.exam.annotation.Retry;
import hello.aop.exam.annotation.Trace;
import org.springframework.stereotype.Repository;
@Repository
public class ExamRepository {
private static int seq = 0;
@Trace // 로그 출력 적용
@Retry(value = 4) // 4번 재시도 적용
public String save(String itemId) {
seq++;
if (seq % 5 == 0) {
throw new IllegalStateException("예외 발생");
}
return "ok";
}
}
설정 설명:
- @Retry(value = 4): 최대 4번 재시도
- @Trace와 함께 사용 가능 (다중 AOP 적용)
- 5번에 한 번 실패하는 로직 + 4번 재시도 = 총 5번 시도 가능
3.3.2. 중첩 애노테이션 처리
스프링 AOP는 여러 애노테이션이 동시에 적용된 경우 자동으로 체인 형태로 어드바이스를 실행한다. 실행 순서는 @Order 애노테이션으로 제어할 수 있다.
3.4. RetryAspect 등록 및 테스트
3.4.1. 테스트 클래스에 Aspect 등록
package hello.aop.exam;
import hello.aop.exam.aop.RetryAspect;
import hello.aop.exam.aop.TraceAspect;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
// 두 Aspect 모두 등록
@Import({TraceAspect.class, RetryAspect.class})
@SpringBootTest
public class ExamTest {
@Autowired
ExamService examService;
@Test
void test() {
for (int i = 0; i < 5; i++) {
examService.request("data" + i);
}
}
}
3.4.2. 실행 결과
[trace] void hello.aop.exam.ExamService.request(String) args=[data0]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data0]
[trace] void hello.aop.exam.ExamService.request(String) args=[data1]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data1]
[trace] void hello.aop.exam.ExamService.request(String) args=[data2]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data2]
[trace] void hello.aop.exam.ExamService.request(String) args=[data3]
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data3]
[trace] void hello.aop.exam.ExamService.request(String) args=[data4]
[retry] String hello.aop.exam.ExamRepository.save(String) retry=@hello.aop.exam.annotation.Retry(value=4)
[retry] try count=1/4
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
[retry] exception=예외 발생
[retry] try count=2/4
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
[retry] exception=예외 발생
[retry] try count=3/4
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
[retry] exception=예외 발생
[retry] try count=4/4
[trace] String hello.aop.exam.ExamRepository.save(String) args=[data4]
결과 분석:
- 1-4번째 호출: 정상 실행, 로그만 출력
- 5번째 호출: 재시도 로직 발동
- 첫 시도 실패 → 재시도 시작
- 총 4번 재시도 설정
- 각 재시도마다 @Trace도 적용됨
- 5번째 시도(원본 1번 + 재시도 4번) 성공
- AOP 체인 실행 순서:
요청 → TraceAspect → RetryAspect → 실제 메서드
3.5. 고급 재시도 AOP 구현
3.5.1. 지수 백오프(Exponential Backoff) 추가
@Slf4j
@Aspect
public class RetryWithBackoffAspect {
@Around("@annotation(retry)")
public Object doRetryWithBackoff(ProceedingJoinPoint joinPoint, Retry retry)
throws Throwable {
int maxRetry = retry.value();
long baseDelay = 100; // 기본 대기 시간 (밀리초)
Exception lastException = null;
for (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
try {
log.info("[retry] attempt {}/{}", retryCount, maxRetry);
return joinPoint.proceed();
} catch (Exception e) {
lastException = e;
log.warn("[retry] attempt {}/{} failed: {}",
retryCount, maxRetry, e.getMessage());
if (retryCount < maxRetry) {
// 지수 백오프: 100ms, 200ms, 400ms, 800ms...
long delay = (long) (baseDelay * Math.pow(2, retryCount - 1));
log.info("[retry] waiting {}ms before next attempt", delay);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw ie;
}
}
}
}
throw lastException;
}
}
지수 백오프 장점:
- 서버 부하 분산
- 네트워크 복구 시간 확보
- 재시도 폭주(Retry Storm) 방지
3.5.2. 예외 타입별 재시도 처리
@Slf4j
@Aspect
public class SmartRetryAspect {
@Around("@annotation(retry)")
public Object doSmartRetry(ProceedingJoinPoint joinPoint, Retry retry)
throws Throwable {
int maxRetry = retry.value();
Exception lastException = null;
for (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
try {
return joinPoint.proceed();
} catch (IllegalStateException e) {
// 일시적 오류: 재시도
lastException = e;
log.info("[retry] temporary error: {}", e.getMessage());
} catch (IllegalArgumentException e) {
// 잘못된 인자: 재시도 의미 없음
log.error("[retry] invalid argument, no retry: {}", e.getMessage());
throw e;
} catch (Exception e) {
// 기타 예외: 로깅 후 재시도
lastException = e;
log.warn("[retry] unexpected error: {}", e.getMessage());
}
if (retryCount >= maxRetry) {
break;
}
// 대기 후 재시도
Thread.sleep(100);
}
throw lastException;
}
}
3.5.3. 회로 차단기(Circuit Breaker) 패턴 통합
@Slf4j
@Aspect
public class CircuitBreakerRetryAspect {
private static final Map<String, CircuitBreaker> circuitBreakers =
new ConcurrentHashMap<>();
@Around("@annotation(retry)")
public Object doCircuitBreakerRetry(ProceedingJoinPoint joinPoint, Retry retry)
throws Throwable {
String methodSignature = joinPoint.getSignature().toLongString();
CircuitBreaker circuitBreaker = circuitBreakers.computeIfAbsent(
methodSignature,
key -> new CircuitBreaker()
);
// 회로 차단기 상태 확인
if (!circuitBreaker.allowRequest()) {
log.warn("[circuit-breaker] circuit is OPEN for {}", methodSignature);
throw new CircuitBreakerOpenException("Circuit breaker is open");
}
try {
Object result = joinPoint.proceed();
circuitBreaker.recordSuccess();
return result;
} catch (Exception e) {
circuitBreaker.recordFailure();
throw e;
}
}
// 간단한 회로 차단기 구현
static class CircuitBreaker {
private static final int FAILURE_THRESHOLD = 5;
private static final long TIMEOUT = 5000; // 5초
private int failureCount = 0;
private long lastFailureTime = 0;
private State state = State.CLOSED;
enum State { CLOSED, OPEN, HALF_OPEN }
boolean allowRequest() {
if (state == State.OPEN) {
// 타임아웃 확인
if (System.currentTimeMillis() - lastFailureTime > TIMEOUT) {
state = State.HALF_OPEN;
return true;
}
return false;
}
return true;
}
void recordSuccess() {
if (state == State.HALF_OPEN) {
state = State.CLOSED;
failureCount = 0;
}
}
void recordFailure() {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= FAILURE_THRESHOLD) {
state = State.OPEN;
}
}
}
}
4. 실전 적용 고려사항
4.1. AOP 실행 순서 제어
여러 Aspect가 동시에 적용될 때 실행 순서가 중요한 경우가 있다.
@Slf4j
@Aspect
@Order(1) // 낮은 숫자가 먼저 실행
public class TraceAspect {
// 구현
}
@Slf4j
@Aspect
@Order(2) // 높은 숫자가 나중에 실행
public class RetryAspect {
// 구현
}
권장 실행 순서:
- 보안 검증 (가장 먼저)
- 트랜잭션 시작
- 로깅
- 재시도
- 캐싱
- 트랜잭션 커밋 (가장 나중)
4.2. 성능 고려사항
4.2.1. 불필요한 AOP 비활성화
@Configuration
public class AopConfig {
@Bean
@ConditionalOnProperty(name = "app.aop.trace.enabled", havingValue = "true")
public TraceAspect traceAspect() {
return new TraceAspect();
}
@Bean
@ConditionalOnProperty(name = "app.aop.retry.enabled", havingValue = "true")
public RetryAspect retryAspect() {
return new RetryAspect();
}
}
application.yml 설정:
app:
aop:
trace:
enabled: true
retry:
enabled: false # 프로덕션에서는 재시도 비활성화
4.2.2. 샘플링 로깅
@Slf4j
@Aspect
public class SamplingTraceAspect {
private final Random random = new Random();
@Before("@annotation(hello.aop.exam.annotation.Trace)")
public void doTrace(JoinPoint joinPoint) {
// 10% 확률로만 로깅 (성능 향상)
if (random.nextInt(100) < 10) {
Object[] args = joinPoint.getArgs();
log.info("[trace] {} args={}", joinPoint.getSignature(), args);
}
}
}
4.3. 예외 처리 전략
4.3.1. 재시도 가능 예외 vs 불가능 예외
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retry {
int value() default 3;
// 재시도할 예외 클래스들 (기본: 모든 예외)
Class<? extends Throwable>[] retryFor() default {Exception.class};
// 재시도하지 않을 예외 클래스들
Class<? extends Throwable>[] noRetryFor() default {};
}
4.3.2. Aspect 구현
@Slf4j
@Aspect
public class ConfigurableRetryAspect {
@Around("@annotation(retry)")
public Object doConfigurableRetry(ProceedingJoinPoint joinPoint, Retry retry)
throws Throwable {
int maxRetry = retry.value();
Class<? extends Throwable>[] retryFor = retry.retryFor();
Class<? extends Throwable>[] noRetryFor = retry.noRetryFor();
Exception lastException = null;
for (int retryCount = 1; retryCount <= maxRetry; retryCount++) {
try {
return joinPoint.proceed();
} catch (Exception e) {
// 재시도 여부 판단
if (shouldRetry(e, retryFor, noRetryFor)) {
lastException = e;
log.info("[retry] attempt {}/{} failed: {}",
retryCount, maxRetry, e.getMessage());
if (retryCount < maxRetry) {
Thread.sleep(100);
}
} else {
// 재시도하지 않는 예외
throw e;
}
}
}
throw lastException;
}
private boolean shouldRetry(Exception e,
Class<? extends Throwable>[] retryFor,
Class<? extends Throwable>[] noRetryFor) {
// noRetryFor 우선 체크
for (Class<? extends Throwable> noRetryClass : noRetryFor) {
if (noRetryClass.isInstance(e)) {
return false;
}
}
// retryFor 체크
for (Class<? extends Throwable> retryClass : retryFor) {
if (retryClass.isInstance(e)) {
return true;
}
}
return false;
}
}
5. 정리
5.1. 학습한 주요 개념
- 커스텀 애노테이션 생성: @Trace, @Retry 애노테이션 정의
- Aspect 구현: @Aspect, @Before, @Around 어드바이스 사용
- 포인트컷 표현식: @annotation()을 이용한 애노테이션 기반 매칭
- 매개변수 바인딩: 애노테이션 인스턴스를 어드바이스 파라미터로 전달
- 재시도 패턴: 예외 발생 시 지정된 횟수만큼 재시도
- AOP 체인: 여러 Aspect가 함께 동작하는 방식
5.2. 실무 적용 시 고려사항
| 항목 | 고려사항 | 해결 방안 |
| 성능 | AOP는 성능 오버헤드 있음 | 필요시만 적용, 샘플링 로깅 |
| 디버깅 | 실행 흐름 추적 어려움 | 상세 로깅, 트레이스 ID |
| 순서 | 여러 Aspect 간 실행 순서 | @Order 애노테이션 사용 |
| 예외 | 재시도 무한 루프 가능 | 최대 재시도 횟수 제한 |
| 테스트 | AOP 적용 여부 테스트 | 단위 테스트, 통합 테스트 분리 |
5.3. 확장 가능한 아이디어
- 실행 시간 측정 AOP:
@Around("@annotation(MeasureTime)") public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long endTime = System.currentTimeMillis(); log.info("Execution time: {}ms", endTime - startTime); } } - 캐싱 AOP:
@Around("@annotation(Cacheable)") public Object cache(ProceedingJoinPoint joinPoint) throws Throwable { String cacheKey = generateCacheKey(joinPoint); Object cachedValue = cache.get(cacheKey); if (cachedValue != null) { return cachedValue; } Object result = joinPoint.proceed(); cache.put(cacheKey, result); return result; } - 분산 트레이싱 AOP:
@Around("execution(* com.example..*(..))") public Object distributedTrace(ProceedingJoinPoint joinPoint) throws Throwable { String traceId = MDC.get("traceId"); if (traceId == null) { traceId = generateTraceId(); MDC.put("traceId", traceId); } // 분산 추적 시스템에 정보 전송 return joinPoint.proceed(); }
5.4. 스프링의 @Transactional과의 관계
스프링의 @Transactional 애노테이션은 가장 대표적인 AOP 적용 사례이다:
@Service
public class UserService {
@Transactional
public void createUser(User user) {
// 트랜잭션 내에서 실행
userRepository.save(user);
sendWelcomeEmail(user);
}
}
비교:
| 항목 | 커스텀 AOP | @Transactional |
| 구현 방식 | 직접 구현 | 스프링 제공 |
| 적용 범위 | 제한적 | 광범위 |
| 설정 | 애노테이션 값으로 제어 | 다양한 속성 지원 |
| 테스트 | 직접 테스트 필요 | 스프링 테스트 지원 |
5.5. 마무리
이번 장에서는 스프링 AOP를 활용하여 실무에서 바로 사용할 수 있는 두 가지 유용한 AOP를 구현해보았다:
- 로그 출력 AOP(@Trace): 메서드 호출 정보를 자동으로 로깅하여 디버깅과 모니터링을 지원
- 재시도 AOP(@Retry): 예외 발생 시 자동으로 재시도하여 시스템의 견고성 향상
이러한 AOP 패턴들은 실제 프로젝트에서 반복적으로 발생하는 횡단 관심사를 효과적으로 처리할 수 있는 강력한 도구이다. 적절히 활용하면 코드의 가독성, 유지보수성, 안정성을 크게 향상시킬 수 있다.
AOP의 진정한 가치는 비즈니스 로직과 횡단 관심사를 명확히 분리함으로써 단일 책임 원칙(Single Responsibility Principle)을 준수하고, 관심사의 분리(Separation of Concerns)를 실현하는 데 있다. 이를 통해 더 깔끔하고 관리하기 쉬운 코드베이스를 구축할 수 있다.
'Spring > Core' 카테고리의 다른 글
| [Advanced-13] 스프링 AOP - 실무 주의사항 (0) | 2026.01.03 |
|---|---|
| [Advanced-11] 스프링 AOP - 포인트 컷 (0) | 2026.01.02 |
| [Advanced-10] 스프링 AOP 구현 (0) | 2026.01.02 |
| [Advanced-9] 스프링 AOP 개념 (0) | 2026.01.02 |
| [Advanced-8] @Aspect AOP (0) | 2026.01.02 |
