본문 바로가기

Spring Framework/Spring

[Spring] Spring AOP — 개념과 구조

1. AOP가 왜 필요한가

서비스 레이어를 작성하다 보면 이런 패턴이 반복된다.

public class UserService {
    public void save(User user) {
        // 로깅
        log.info("save 호출: {}", user);
        // 트랜잭션 시작
        transaction.begin();
        // 인증 확인
        checkAuth();

        // ─── 핵심 비즈니스 로직 ───
        userRepository.save(user);
        // ──────────────────────────

        // 트랜잭션 커밋
        transaction.commit();
        log.info("save 완료");
    }
}

문제는 이 코드가 OrderService, PaymentService에도 똑같이 반복된다는 점이다.

 

 

로깅, 트랜잭션, 인증처럼 여러 클래스에 걸쳐 공통으로 필요한 기능횡단 관심사(Cross-Cutting Concerns) 라고 부른다.

이 문제를 해결하기 위해 등장한 것이 AOP다.

 


2. AOP란 무엇인가

AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)는 핵심 비즈니스 로직과 횡단 관심사를 분리하는 프로그래밍 패러다임이다.

기존 방식 (OOP만으로)
UserService    = 핵심 로직 + 로깅 + 트랜잭션 + 인증
OrderService   = 핵심 로직 + 로깅 + 트랜잭션 + 인증
PaymentService = 핵심 로직 + 로깅 + 트랜잭션 + 인증

AOP 적용 후
UserService    = 핵심 로직만
OrderService   = 핵심 로직만
PaymentService = 핵심 로직만
LoggingAspect  = 로깅 (한 곳에서 관리)
TxAspect       = 트랜잭션 (한 곳에서 관리)
AuthAspect     = 인증 (한 곳에서 관리)

Spring AOP는 프록시(Proxy) 패턴을 기반으로 동작한다. 실제 객체를 직접 호출하는 대신, 프록시 객체를 통해 호출하면서 그 과정에서 부가 기능을 끼워 넣는 방식이다.

 


3. 핵심 개념 3가지

Pointcut — 어디에 적용할 것인가

적용 대상 메서드를 선별하는 필터다. 내부적으로 두 가지 작업을 한다.

  • ClassFilter: 이 클래스에 AOP를 적용할지 판단
  • MethodMatcher: 이 메서드에 AOP를 적용할지 판단
// AspectJ 표현식으로 지정 (현대 방식)
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceLayer() {}

 

Advice — 무엇을 할 것인가

실제로 끼워 넣을 부가 기능 로직이다. 적용 시점에 따라 종류가 나뉜다.

종류 시점
@Before 메서드 실행 전
@After 메서드 실행 후 (예외 무관)
@AfterReturning 정상 반환 후
@AfterThrowing 예외 발생 후
@Around 전후 모두 감쌈 (가장 강력)
@Aspect
@Component
public class LoggingAspect {

    @Around("serviceLayer()")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        log.info("실행 전: {}", pjp.getSignature());
        Object result = pjp.proceed(); // 실제 메서드 호출
        log.info("실행 후: {}", pjp.getSignature());
        return result;
    }
}

 

Advisor — Pointcut + Advice 묶음

"어디에(Pointcut) + 무엇을(Advice)"를 하나로 묶은 단위다. Spring AOP 내부에서 실제로 동작하는 단위이기도 하다.

현대에는 @Aspect 클래스를 작성하면 Spring이 내부적으로 Advisor를 자동 생성한다. 직접 조립할 일은 없지만, 토비의 스프링처럼 내부 구조를 다루는 자료에서는 저수준 API로 등장한다.

// 저수준 API — 직접 조립 (토비의 스프링 스타일)
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
advisor.setPointcut(new NameMatchMethodPointcut()); // 어디에
advisor.setAdvice(new TransactionAdvice());          // 무엇을

 


4. @Aspect — 현대 AOP의 진입점

@Aspect"이 클래스는 횡단 관심사를 정의하는 클래스다" 라고 Spring에게 알려주는 어노테이션이다. AspectJ의 어노테이션 방식을 Spring이 차용한 것으로, 클래스 안에 Pointcut과 Advice를 함께 정의하면 Spring이 이를 파싱해 Advisor를 자동으로 생성하고 프록시에 등록한다.

 

실무에서는 @Aspect가 붙은 클래스를 Aspect 클래스라고 부르며, 네이밍은 LoggingAspect, SecurityAspect처럼 ~Aspect suffix를 사용하는 것이 관례다.

@Aspect      // ← "이 클래스는 Aspect다" 선언
@Component   // ← Spring Bean으로 등록 (필수 — @Aspect만으로는 Bean 등록이 안 됨)
public class LoggingAspect {

    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}   // Pointcut 정의

    @Around("serviceLayer()")
    public Object log(ProceedingJoinPoint pjp) throws Throwable { ... }  // Advice 정의
}

@Aspect만으로는 Bean으로 등록되지 않는다. @Component를 함께 붙이는 것이 일반적이며, @Bean으로 직접 등록하는 방법도 있다.

 

// 방법 2 — @Bean 직접 등록
@Configuration
public class AopConfig {
    @Bean
    public LoggingAspect loggingAspect() {
        return new LoggingAspect();
    }
}

또한 @Aspect 클래스 자체는 프록시 대상이 되지 않는다. Aspect가 Aspect에 적용되는 무한 루프를 방지하기 위해 Spring이 의도적으로 제외한다.

 

@EnableAspectJAutoProxy

@Aspect를 인식하려면 Spring 컨테이너에 AspectJ 자동 프록시 기능이 활성화되어 있어야 한다.

// 순수 Spring 환경에서는 직접 선언 필요
@Configuration
@EnableAspectJAutoProxy
public class AppConfig { ... }

Spring Boot에서는 spring-boot-starter-aop 의존성을 추가하면 자동으로 활성화되므로 별도 선언이 필요 없다.

 


5. 프록시 동작 방식

Spring AOP는 두 가지 프록시 방식을 사용한다.

 

 

JDK Dynamic Proxy

인터페이스 기반으로 프록시를 생성한다. java.lang.reflect.Proxy를 사용하며, 인터페이스가 있어야만 동작한다.

public interface OrderService { void order(); }

// 구체 클래스 타입으로 주입하면 에러
@Autowired OrderServiceImpl orderService; // ClassCastException 발생!

// 인터페이스 타입으로만 주입 가능
@Autowired OrderService orderService; // OK

 

CGLIB Proxy

클래스 상속으로 프록시를 생성한다. 원본 클래스의 서브클래스를 바이트코드 조작으로 만들어낸다.

// 인터페이스 없는 클래스도 프록시 가능
@Service
public class OrderService {
    public void order() { ... }
}

// 구체 클래스 타입으로도 주입 가능 (서브클래스이므로)
@Autowired OrderService orderService; // OK

 

단, 다음 경우는 프록시 생성이 불가능하다.

// final 클래스 — 상속 불가
public final class OrderService { ... } // 프록시 생성 실패

// final 메서드 — 오버라이드 불가, AOP 적용 안 됨
public class OrderService {
    public final void order() { ... }
}

 

Spring Boot 기본값

Spring Boot 1.4부터 CGLIB가 기본값이다. 인터페이스 유무와 관계없이 CGLIB를 사용한다.

# 기본값 true (CGLIB), false로 바꾸면 JDK Proxy 사용
spring.aop.proxy-target-class=true

참고 — 토비의 스프링 시대(Spring 2~3)에는 JDK Dynamic Proxy가 기본값이었다. 그래서 당시에는 인터페이스를 항상 만드는 것이 관례였고, 구체 클래스 타입으로 의존하면 문제가 생겼다. 현재는 CGLIB가 기본이라 인터페이스 없이도 잘 동작하지만, final 제약은 주의해야 한다.

 


6. 현대 방식으로 작성하기

Spring Boot에서 AOP를 사용하려면 의존성을 추가한다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

 

앞서 설명한 Pointcut, Advice, Advisor, @Aspect가 실제 코드에서 어떻게 조립되는지 전체 예시로 확인한다.

@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    // ① Pointcut — 어디에 적용할지
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void serviceLayer() {}

    // ② Advice — 무엇을 할지 (@Around = 전후 모두 감쌈)
    // ③ Advisor — Spring이 ①+②를 묶어 내부적으로 생성
    @Around("serviceLayer()")
    public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
        String method = pjp.getSignature().toShortString();
        log.info("[AOP] {} 시작", method);

        long start = System.currentTimeMillis();
        Object result = pjp.proceed(); // 실제 메서드 호출
        long elapsed = System.currentTimeMillis() - start;

        log.info("[AOP] {} 완료 ({}ms)", method, elapsed);
        return result;
    }
}

 


7. 정리

개념 역할 현대 방식
Pointcut 어디에 적용할지 선별 @Pointcut + AspectJ 표현식
Advice 무엇을 할지 정의 @Before, @Around
Advisor Pointcut + Advice 묶음 Spring이 @Aspect 파싱 후 자동 생성
Aspect Advisor를 선언하는 단위 @Aspect 클래스 (~Aspect 네이밍 관례)

 

핵심 흐름

클라이언트
  ↓ 호출
[프록시] ← Spring이 자동 생성 (CGLIB 기본)
  ↓ Pointcut 조건 확인
  ↓ 조건 일치 → Advice 실행 (전처리)
  ↓ 실제 메서드 호출 (pjp.proceed())
  ↓ Advice 후처리
결과 반환

다음 포스트에서는 Pointcut 지시자(execution, within, this, target, @annotation 등)를 깊이 있게 다룬다.