SpringBoot AOP 의 아이러니

2025. 3. 11. 17:31·개발/SpringBoot

 

항상 서두에 글을 쓰지만 이글은 Spring AOP 를 정의하는 글이 아닌
그 동안의 경험을 바탕으로 Spring AOP를 이해하기 위해 실제 구동원리를 바탕으로 
이렇게도 생각하는구나 또는 이렇게 이해를 하는구나라고 생각을 확장하는 의미로 봐주시면 좋을 것 같습니다.

1. AOP의 본질: 횡단 관심사의 분리와 프록시 패턴

- Aspect Oriented Programming 으로 사전적 정의를 찾아보면 
  "횡단 관심사(cross-cutting concern)의 분리를 허용함으로써 모듈성을 증가시키는 것이 목적인 프로그래밍 패러다임"
   
관점형 프로그래밍이라고 설명을 하며 대표적으로 설명되는 그림이 프로그램 로직의 가로 flow 에 세로로 뭔가를 수행하는 이미지를 많이 참고하는데 

출처 : https://hoi5088.medium.com


관점형으로 실제 비지니스 수행과 관계없는 영역의 관심사를 분리하여 코드를 줄일 수 있는 강력한 방법이기에 Spring을 대표하는 기능중 하나로 AOP가 많이 언급이 됩니다만 위의 설명만으로는 사실상 제가 주니어일때 관점형 프로그래밍을 이해하는게 개념적으로 쉽지만은 않았습니다~  뭔말이야-_-

제 기준에서 좀 더 잘 이해했던 구동하는 원리(코드)를 가지고 개념을 역으로 설명 해 보겠습니다.

기능적인 부분을 기준으로 생각할때 Spring Bean으로 등록된 class를 wrapping을 해서 해당 클래스에 함수나
기능실행 직전이나 또는 끝날때 뭔가를 제어 할 수 있도록 해놓은 것으로 이해를 했고
이 과정에서 프록시 패턴(Proxy Pattern)이 사용됩니다. 

프록시 패턴을 직접 코드로 구현해 보면 AOP의 원리를 조금 더 쉽게 이해할 수 있습니다.

// User Service Interface
interface IUserService {
    void userSearch();
}

// 실제 UserService 를 구현한 Class
class UserService implements IUserService {
    public void userSearch() {
        System.out.println("유저 조회!!");
    }
}

// ProxyUserService: AOP 개념을 적용한 Proxy
class ProxyUserService implements IUserService {
    private final IUserService userService;

    ProxyUserService(IUserService userService) {
        this.userService = userService;
    }

    @Override
    public void userSearch() {
        // 실행 전 로직
        System.out.println("조회하기 전 시간 확인!! or 조회하기 전 Transaction 시작");

        userService.userSearch(); // 실제 유저 조회!!

        // 실행 후 로직
        System.out.println("조회 후 경과 시간 확인!! or 조회 후 Transaction 완료");
    }
}

public class ProxyPatternExample {
    public static void main(String[] args) {
    
        // 위에서 말한 Class를 Wrapping 한다는 의미
        IUserService userService = new ProxyUserService(new UserService());
        userService.userSearch();

        // 예상 결과
        // "조회하기 전 시간 확인!! or 조회하기 전 Transaction 시작"
        // "유저 조회!!"
        // "조회 후 경과 시간 확인!! or 조회 후 Transaction 완료"
    }
}


위의 코드가 일반적인 Proxy Pattern을 단순하게 구현한 것인데 

예제에서 userSearch Method를 수행하기전 과 후에 
해당 함수에서 무언가를 확인 하거나 제어한다는 기능적인 부분을 곰곰히 생각해보면
Spring에서의 Transaction Annotation ( begin , commit ) 이나 Method 수행 시간확인을 하기 위해서 구현하는
AOP 의 기능과 유사하다는 것을 알 수 있습니다.

2. Spring AOP의 내부 구현: JDK Dynamic Proxy vs CGLIB

실제로 Spring AOP는 위에서 설명한 프록시 패턴을 기반으로 동작합니다.
좀 더 세부적으로 들여다보면, Spring에서 제공하는 AOP 구현 방식은 크게 두 가지로 나뉩니다.

1> JDK Dynamic Proxy: 인터페이스 기반 프록시
JDK Dynamic Proxy는 대상 클래스가 인터페이스를 구현하고 있을 때 사용됩니다.
InvocationHandler 인터페이스를 구현하여 프록시 로직을 정의하고, 런타임에 동적으로 프록시 객체를 생성합니다.

// InvocationHandler 구현
class UserServiceInvocationHandler implements InvocationHandler {
    private final Object target;

    public UserServiceInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("AOP Before Logic");
        Object result = method.invoke(target, args);
        System.out.println("AOP After Logic");
        return result;
    }
}

public class JdkDynamicProxyExample {
    public static void main(String[] args) {
        IUserService realService = new UserService();

        IUserService proxyInstance = (IUserService) Proxy.newProxyInstance(
            IUserService.class.getClassLoader(),
            new Class[]{IUserService.class},
            new UserServiceInvocationHandler(realService)
        );

        proxyInstance.userSearch();

        // 예상 결과
        // "AOP Before Logic"
        // "유저 조회!!"
        // "AOP After Logic"
    }
}


위의 첫 번째 프록시 구현처럼 모든 인터페이스 구현마다 수동으로 프록시 클래스를 만들 필요 없이, JDK Dynamic Proxy는 이 과정을 자동화하여 코드 양과 복잡도를 줄여줍니다. Spring의 실제 코드는 훨씬 복잡하지만, 기본적인 메커니즘은 이와 크게 다르지 않습니다.
(더 자세한 내용은 Spring Framework Reference: Proxying Mechanisms 문서를 참고해주세요.)

2> CGLIB (Code Generation Library): 인터페이스 없는 클래스 프록시
CGLIB는 대상 클래스가 인터페이스를 구현하지 않았을 때 사용됩니다.
이 방식은 런타임에 대상 클래스의 바이트코드를 조작하여 새로운 서브클래스(자식 클래스)를 생성하고,
이 서브클래스가 프록시 역할을 수행하게 합니다.

// 인터페이스 없이 구현된 서비스 클래스
class UserServiceWithoutInterface {
    public void userSearch() {
        System.out.println("유저 조회!!");
    }
}

public class CglibProxyExample {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(UserServiceWithoutInterface.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
            System.out.println("AOP Before Logic");
            Object result = proxy.invokeSuper(obj, args);
            System.out.println("AOP After Logic");
            return result;
        });

        UserServiceWithoutInterface proxy = (UserServiceWithoutInterface) enhancer.create();
        proxy.userSearch();

        // 예상 결과
        // "AOP Before Logic"
        // "유저 조회!!"
        // "AOP After Logic"
    }
}


인터페이스를 구현하지 않은 클래스에 대해 프록시 객체를 만들기 위해서는 Enhancer 클래스를 사용합니다.
Spring의 내부 구현은 물론 더 복잡하겠지만,
CGLIB를 통한 프록시 생성 메커니즘은 이 예시와 크게 다르지 않습니다.

3.Spring Boot의 AOP 기본 동작과 숨겨진 우선순위

Spring Boot에서는 @EnableAspectJAutoProxy의 proxyTargetClass옵션 값(기본값은 false)을 기준으로
두 가지 프록시 생성 방법 중 하나를 선택합니다.

  • proxyTargetClass = false일 경우: JDK Dynamic Proxy (인터페이스 기반)
  • proxyTargetClass = true일 경우: CGLIB (클래스 상속 기반)

만약 proxyTargetClass = false 상태에서 인터페이스 구현체가 아닌 서비스 클래스에 AOP를 적용하려고 하면
런타임에 AopConfigException이 발생할 수 있습니다.
이는 위에서 설명한 JDK Dynamic Proxy의 동작 원리(인터페이스 필요) 때문입니다.

그런데 여기서 재미있는 점은,
SpringBoot의 AopAutoConfiguration이 proxyTargetClass값을 true로 설정하고 CGLIB를 우선으로 작동하며
이 설정은 Spring 프레임워크 자체의 @EnableAspectAutoProxy 기본값이 false 인 것과는 대조적이며
두 설정 중 AopAutoConfiguration의 설정이 우선순위를 가집니다

EnableAspectJAutoProxy 기본 값

즉, @EnableAspectJAutoProxy(proxyTargetClass = false)를 선언하더라도,
Spring Boot 환경에서는 기본적으로 CGLIB 방식의 프록시가 생성됩니다.

물론 application.properties 또는 application.yml 파일에
spring.aop.proxy-target-class= false로 셋팅을 하게되면 강제를 할 수 있긴 하나
이 부분은 쉽게 알기 힘든 숨겨진 동작 방식이기도 합니다.

그리고 Spring 공식 문서에서는 CGLIB보다 JDK Dynamic Proxy를 권장하고 있는데,
참고(https://docs.spring.io/spring-framework/reference/core/aop/introduction-proxies.html)
SpringBoot가 CGLIB를 우선으로 동작시킨다는 점이 다소 아이러니하게 느껴질 수 있습니다.


개인적으로 생각을 좀 해본다면 

Spring을 Base로 Application의 설계할때는
Interface를 구현하는 형태가 Spring의 사상에 더 부합하기 때문에 JDK Dynamic proxy 를 추천하지만
 
SpringBoot 는 설정에 관련된 일은 신경쓰지마세요~ 기본적인 설정은 내가 다 알아서 해줄께라는
설정을 최소화하는 사상이라 좀 더 넓게 포함할 수 있는 CGLIB의 형태가 기본값이 된 것으로 생각됩니다.
 
이는 Spring이 추구하는 근본적인 구현 사상(JDK Dynamic Proxy)을 유지하면서도,
Spring Boot가 지향하는 편의성과 자동화라는 현실적인 부분을 고민하여 균형점을 찾은
 즉 큰 방향성을 버리지 않되 현실적인 부분을 고민해둔 흔적이라고 생각합니다.

앞으로 Spring 진영에서 AOP 프록시 방식에 대한 개발 방향이 어떻게 변화할지 궁금해지는 부분이기도 합니다.
(뇌피셜이지만, Spring과 Spring Boot 진영에서 이 부분에 대해 많은 토론이 오가지 않았을까? 라고 생각해보기도 했습니다.)

무튼 AOP의 개념을 코드베이스와 동작원리를 통해 대략적으로 이해한걸 공유하였고 
다음 글에서는 실 서비스에서 사용 및 구현한 부분을 해 보도록 하겠습니다.

'개발 > SpringBoot' 카테고리의 다른 글

ApplicationEventPublisher의 @Async 사용 시 주의점  (0) 2025.05.13
SpringBoot의 ApplicationEventPublisher 를 활용한 비즈니스 분리  (0) 2025.04.25
Springboot Custom Annotation  (0) 2025.02.17
'개발/SpringBoot' 카테고리의 다른 글
  • ApplicationEventPublisher의 @Async 사용 시 주의점
  • SpringBoot의 ApplicationEventPublisher 를 활용한 비즈니스 분리
  • Springboot Custom Annotation
개발자아저씨
개발자아저씨
그냥 오랫동안 개발하고 싶은 아저씨...
  • 개발자아저씨
    코딩하는 아저씨
    개발자아저씨
  • 전체
    오늘
    어제
    • 분류 전체보기 (17)
      • 개발자 (5)
      • 개발 (11)
        • SpringBoot (4)
        • AI (5)
        • MSA (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    backend
    annotation 활용
    local llm 연동
    생각과 경험
    로그인 발전
    AOP
    비지니스 분리
    ai 서빙
    Claude.md
    ai서빙
    글쓰기
    aop 사용 기준
    simpleasynctaskexecutor
    경량llm
    ApplicationEventPublisher
    결재연동
    AI
    MSA
    Session Cluster
    은탄환은 없다
    이직
    ai coding
    claude desktop
    회고
    springboot
    백엔드
    AI컨벤션
    MCP
    springai
    claudecode
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
개발자아저씨
SpringBoot AOP 의 아이러니

티스토리툴바