[SpringBoot] Spring AOP란?, AOP 프록시

나른한 개발자·2026년 1월 7일

f-lab

목록 보기
17/44

AOP (Aspect Oriented Programming)

객체 지향 프로그래밍 패러다임을 보완하는 기술로 메소드나 객체의 기능을 핵심 관심사(Core Concern)와 공통 관심사(Cross-cutting Concern)로 나누어 프로그래밍하는 것을 말한다. 핵심 관심사는 각 객체가 가져야 할 본래의 기능이며, 공통 관심사는 여러 객체에서 공통적으로 사용되는 코드를 말한다.

여러 개의 클래스에서 반복해서 사용하는 코드가 있다면 해당 코드를 모듈화하여 공통 관심사로 분리한다. 이렇게 분리한 공통 관심사를 Aspect로 정의하고 Aspect를 적용할 메소드나 클래스에 Advice를 적용하여 공통 관심사와 핵심 관심사를 분리할 수 있다. 이렇게 AOP에서는 공통 관심사를 별도의 모듈로 분리하여 관리하며, 이를 통해 코드의 재사용성과 유지 보수성을 높일 수 있다.

Spring AOP

Spring AOP는 스프링 프레임워크에서 제공하는 기능 중 하나로 관점 지향 프로그래밍을 지원하는 기술이다. Spring AOP는 로깅, 보안, 트랜잭션 관리 등과 같은 공통적인 관심사를 모듈화 하여 코드 중복을 줄이고 유지 보수성을 향상하는데 도움을 준다.

활용 예시

  • @Transactional: 스프링의 @Transactional 은 내부적으로 AOP 활용한다. 메서드 실행 전에 트랜잭션 시작하고, 종료시 커밋, 예외 발생 시 롤백하는 AOP로 구현되어있다.
  • 성능 모니터링
  • API 호출 로깅
  • 권한 체크
  • 캐시
  • 재시도 로직

관련 용어 정리

  • Aspect: 공통적인 기능들을 모듈화 한 것
  • Target: Aspect가 적용될 대상. 메서드나 클래스
  • Join Point: Aspect가 실행되는 시점. 메서드 시작 전, 종료 후 등을 말한다.
  • Advice: Aspect의 기능을 정의한 것. 메서드의 실행 전, 후, 예외 처리 발생 시 실행되는 코드를 의미.
  • Point Cut: Aspect를 적용할 메소드의 범위를 지정하는 표현식이다.
  • Weaving: Aspect를 실제 메서드에 적용하는 과정이다. 스프링 AOP는 런타임 위빙을 사용한다.

주요 어노테이션

  • @Aspect: 해당 클래스를 Aspect로 사용하겠다는 것을 명시한다.
  • @Before: 대상 “메서드”가 실행되기 전에 Advice를 실행한다.
  • @After: 대상 “메서드”가 실행된 후에 Advice를 실행한다.
  • @AfterReturning: 대상 “메서드”가 정상적으로 실행되고 반환된 후에 Advice를 실행한다.
  • @AfterThrowing: 대상 “메서드에서 예외가 발생”했을 때 Advice를 실행한다.
  • @Around: 대상 “메서드” 실행 전, 후 또는 예외 발생 시에 Advice를 실행한다.

스프링 AOP 동작 원리

스프링 AOP는 프록시 기반으로 동작한다. 예를 들어, UserService라는 클래스가 있다면, 스프링은 실제 UserService를 직접 사용하는 게 아니라 UserService를 감싼 프록시를 만들어서 사용한다. 클라이언트가 메서드를 호출하면 프록시가 먼저 받아서 AOP 로직(로깅, 트랜잭션 등)을 실행한 후, 실제 메서드를 호출하는 것이다.

스프링에서 프록시는 주로 JDK 다이나믹 프록시와 CGLIB 두 가지 방식으로 구현되는데, JDK 동적 프록시와 CGLIB이다.

JDK 동적 프록시

public interface UserService {
    void saveUser(User user);
}

public class UserServiceImpl {
    void saveUser(User user){...}
}

// 스프링이 자동으로 만드는 프록시 (실제는 더 복잡)
public class UserServiceProxy extends UserService {
    
    private UserService realUserService;  // 실제 객체
    private LoggingAspect loggingAspect;  // Aspect
    
    @Override
    public void saveUser(User user) {
        // 1. Aspect 실행
        loggingAspect.before();
        
        // 2. 실제 메서드 실행
        realUserService.saveUser(user);
        
        // 3. Aspect 실행
        loggingAspect.after();
    }
}
  • 자바의 리플렉션 API를 사용하여 인터페이스 기반으로 프록시 객체를 생성한다.
  • 타겟 클래스가 반드시 하나 이상의 인터페이스를 구현하고 있어야한다.
  • 리플렉션을 사용하기 때문에 성능이 다소 떨어질 수 있다.
  • 구현 클래스 타입으로 의존성 주입(DI)을 받을 때 타입 오류가 발생할 수 있다: AOP가 적용된 클래스는 애플리케이션의 런타임 초기화 시에 빈으로 등록된다. 그 후 @Autowired를 통해 진짜 객체 대신 프록시 객체가 의존성으로 주입이 된다. 이때 인터페이스 타입으로 의존성을 주입받는다면 문제 없지만 구현체로 주입을 받는다면 (위 코드에선 UserServiceImpl) 의존성을 넣어줄 수 없어 에러가 발생한다. (프록시는 UserService 타입으로 되어있기 때문에)

CGLIB

  • 스프링 부트에서는 기본 설정(spring.aop.proxy-target-class=true)이 CGLIB를 사용하도록 되어있다.
  • 타겟 클래스를 상속받는 프록시 객체를 생성한다.
  • 인터페이스가 없는 구체 클래스에도 직접 프록시를 생성할 수 있다.
  • 상속을 이용하기 때문에 final 클래스나 final 메서드에는 AOP를 적용할 수 없다. (final이 붙은 클래스는 상속이 불가하고 final 메서드는 오버라이딩 할 수 없기 때문이다)

CGLIB 를 많이 사용하는 이유

개발자가 인터페이스를 만들지 않은 경우에도 AOP가 정상 작동해야 하며, 구체 클래스 타입으로 빈을 주입받는 상황에서도 안정적으로 동작하기 때문이다.

제약 사항 - 내부 호출 문제

@Service
public class MyService {
    @Transactional
    public void methodA() {
        methodB(); // 내부 호출
    }

    @Transactional
    public void methodB() {
        System.out.println("Method B");
    }
}

위 예시 코드를 보면 methodA()에서는 methodB()를 호출하고 있다. 그러나 methodB()에 적용된 트랜잭션은 동작하지 않는다. 내부 호출은 프록시 객체를 거치지 않기 때문에 AOP가 적용되지 않는다. 이 문제를 해결하려면, 내부 호출 대신 프록시 객체를 통해 호출해야 한다. 또는 설계를 변경하여 내부 호출을 피해야 한다.

스프링 AOP가 프록시 기반으로 동작하는 이유

AOP의 목표는 원본 코드를 건드리지 않고 부가 기능을 추가하는 것이다. 그러기 위해서는 클라이언트와 원본 객체 사이에서 가로채어 동작을 수행해야한다. 이때 프록시가 중간 다리 역할을 하여 메서드 호출을 가로채고 원본 메서드를 호출하는 것이다.

AOP 프레임워크 중 하나인 AspectJ에서는 프록시 방식이 아닌 다른 방식을 사용한다.

  • 컴파일 타임 위빙: 컴파일할 때 아예 부가 기능 코드를 클래스 파일에 집어넣는다. 프록시 없이 동작하여 제약이 없고 성능 좋다. 다만 컴파일 과정이 복잡하고 빌드 시간 증가하는 단점이 있다.
  • 로드 타임 위빙: 클래스를 메모리에 로드할 때 바이트코드를 수정한다. 배포 후에도 Aspect를 변경 할 수 있다. 다만 클래스 로더 설정 필요하다.

하지만 위 방식은 JVM 옵션을 추가해야하거나 AspectJ 컴파일러가 별도로 필요하다. AOP를 실행하기 위한 러닝커브가 존재한다.

스프링에서는 복잡한 것을 단순하게 제공하는 것이 목적이기 때문에 제한적이기만 단순하고 많은 설정없이 바로 사용가능한 프록시 패턴을 선택한 것이다.

상황에 따라 스프링에서도 스프링 AOP대신 AspectJ를 사용할 수도 있다. 그럼 프록시 패턴이 아닌 위의 방식처럼 스프링 컨테이너 밖에서 위빙이 일어난다.

profile
Start fast to fail fast

0개의 댓글