Spring AOP

Spring에서 핵심 비즈니스 로직과 부가 기능(로깅, 트랜잭션, 보안 등)을 분리하여 모듈화하는 프로그래밍 기법입니다.

AOP가 필요한 이유

[AOP 적용 전 - 부가 기능이 핵심 로직에 섞임]
public void createOrder() {
    log.info("주문 시작");           // 로깅
    checkAuth();                     // 보안
    beginTransaction();              // 트랜잭션
    orderRepository.save(order);     // 핵심 로직
    commitTransaction();             // 트랜잭션
    log.info("주문 완료");           // 로깅
}

[AOP 적용 후 - 핵심 로직만 남음]
@Transactional
@Logging
public void createOrder() {
    orderRepository.save(order);     // 핵심 로직만 집중
}

핵심 용어

용어설명
Aspect부가 기능을 모듈화한 클래스 (로깅, 트랜잭션 등)
Advice부가 기능의 실제 구현 (언제 실행할지 정의)
JoinPointAdvice가 적용될 수 있는 지점 (메서드 실행 시점)
PointcutAdvice를 적용할 대상을 선별하는 표현식
TargetAdvice가 적용되는 실제 객체
ProxyTarget을 감싸서 부가 기능을 제공하는 대리 객체

동작 원리

클라이언트 → 프록시 객체 → Advice 실행 → Target 메서드 실행 → Advice 실행 → 반환
              (Spring이 자동 생성)

Spring은 프록시 패턴을 사용하여 AOP를 구현합니다.

방식설명
JDK 동적 프록시인터페이스 기반, 인터페이스를 구현한 경우 사용
CGLIB 프록시클래스 기반, 인터페이스가 없는 경우 사용 (Spring 기본값)

Advice 종류

종류실행 시점용도
@Before메서드 실행 전권한 체크, 파라미터 검증
@AfterReturning메서드 정상 완료 후결과 로깅, 후처리
@AfterThrowing예외 발생 후에러 로깅, 알림
@After메서드 완료 후 (성공/실패 무관)리소스 정리
@Around메서드 실행 전후 모두시간 측정, 트랜잭션, 가장 강력
@Before → Target 메서드 실행 → @AfterReturning (성공 시)
                              → @AfterThrowing  (예외 시)
                              → @After          (항상)

@Around: 위 전체를 감싸서 제어 가능

구현 예시

1. 실행 시간 측정

@Aspect
@Component
public class ExecutionTimeAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        Object result = joinPoint.proceed();  // 실제 메서드 실행

        long end = System.currentTimeMillis();
        log.info("{} 실행 시간: {}ms",
                joinPoint.getSignature().getName(), end - start);

        return result;
    }
}

2. 로깅 AOP

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        log.info("[호출] {} - args: {}",
                joinPoint.getSignature().getName(),
                Arrays.toString(joinPoint.getArgs()));
    }

    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))",
                     returning = "result")
    public void logAfter(JoinPoint joinPoint, Object result) {
        log.info("[완료] {} - result: {}",
                joinPoint.getSignature().getName(), result);
    }

    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))",
                    throwing = "ex")
    public void logError(JoinPoint joinPoint, Exception ex) {
        log.error("[에러] {} - {}",
                joinPoint.getSignature().getName(), ex.getMessage());
    }
}

3. 커스텀 어노테이션 기반 AOP

// 커스텀 어노테이션 정의
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleCheck {
    String value();  // 필요한 권한
}

// Aspect 구현
@Aspect
@Component
public class RoleCheckAspect {

    @Before("@annotation(roleCheck)")
    public void checkRole(JoinPoint joinPoint, RoleCheck roleCheck) {
        String requiredRole = roleCheck.value();
        String currentRole = SecurityContextHolder.getContext().getRole();

        if (!currentRole.equals(requiredRole)) {
            throw new AccessDeniedException("권한 없음: " + requiredRole + " 필요");
        }
    }
}

// 사용
@Service
public class AdminService {

    @RoleCheck("ADMIN")
    public void deleteUser(Long userId) {
        userRepository.deleteById(userId);
    }
}

Pointcut 표현식

// 패키지 내 모든 메서드
@Around("execution(* com.example.service.*.*(..))")

// 특정 어노테이션이 붙은 메서드
@Around("@annotation(com.example.annotation.Logging)")

// 특정 어노테이션이 붙은 클래스의 모든 메서드
@Around("@within(org.springframework.stereotype.Service)")

// 조합 (AND, OR)
@Around("execution(* com.example.service.*.*(..)) && @annotation(Logging)")

AOP가 적용된 Spring 기능들

기능설명
@Transactional트랜잭션 관리
@Cacheable캐시 처리
@Async비동기 실행
@Secured / @PreAuthorize보안/권한 체크
@Retryable재시도 처리

이들이 모두 프록시 기반 AOP로 동작하기 때문에, 내부 호출 시 미적용, public 메서드만 적용 등의 동일한 제약이 존재합니다.

→ 스프링 트랜잭션 AOP 동작 흐름에 대해서 설명해주세요

CSLOG에서 보기