Spring

[Spring] AOP & Proxy 란?

tudamoa 2025. 8. 1. 18:18

✅1. AOP와 Proxy를 왜 공부해야 하는가?

스프링 프레임워크를 제대로 이해하고 활용하려면 반드시 AOP(Aspect-Oriented Programming)와 Proxy(프록시)에 대한 개념을 짚고 넘어가야 한다. AOP는 스프링의 내부 동작 원리와 실무 로직 구현에 깊이 관여한다.

 

📌 1-1. 관심사의 분리 (Separation of Concerns)

실제 서비스를 개발하다 보면 모든 클래스나 메서드가 자신의 작업 외에 공통적인 작업을 반복적으로 수행하게 된다.
예를 들어:

  • 메서드 실행 전후에 로그를 남긴다.
  • 실행 시간 측정을 위해 시간을 기록한다.
  • 트랜잭션을 시작하고 커밋하거나 롤백한다.
  • 인증/인가 처리를 수행한다.

공통적인 작업을 하는 이런 코드들을 모든 클래스에 직접 반복적으로 작성하게 된다면?
➡ 유지보수가 어렵고, 로직이 꼬이기 쉽고, 코드 가독성도 떨어진다.
이 때 AOP가 등장한다.

 

AOP는 이런 공통적인 관심사(Cross-Cutting Concern)를 분리해서 핵심 로직(비즈니스 로직)에 영향을 주지 않고도 관리할 수 있게 도와준다.

 

📌 1-2. 실무에서 AOP는 어디에 쓰일까?

Spring AOP는 실무에서 다음과 같은 용도로 자주 사용된다:

  • 트랜잭션 관리: @Transactional을 통해 메서드 단위 트랜잭션 적용
  • 로깅 및 감사: 사용자의 행동 추적 기록
  • 보안: 권한 검사
  • 성능 측정: 실행 시간 측정 및 슬로우 쿼리 탐지
  • 예외 처리 로직 일괄 적용

이처럼 AOP는 실무에서 없어서는 안 될 기능이며 코드의 품질과 유지보수성을 높이는 데 크게 기여한다.

 

📌 1-3. 그럼 왜 Proxy까지 알아야 할까?

스프링 AOP는 프록시(Proxy)를 기반으로 동작한다.
AOP가 동작하기 위해 프록시 객체를 만들고 그 객체가 실제 메서드를 호출하기 전에 Advice*(공통 기능을 언제 실행할지)를 실행하도록 설계되어 있다.

따라서 AOP를 이해하려면 프록시가 무엇인지 스프링은 어떤 방식으로 프록시를 생성하는지(JDK 동적 프록시 / CGLIB)를 반드시 함께 알아야 한다.

 


✅2. AOP 핵심 개념 정리

AOP를 구성하는 핵심 요소 6가지와 예시

 

📌 2-1. Aspect (관점)

  • AOP가 분리한다고 하는 공통 관심사(Cross-Cutting Concern)를 모듈화한 것
  • 쉽게 말해 공통 관심사 - 로깅, 보안, 트랜잭션 관리 같은 부가 기능 덩어리를 담고 있는 클래스
  • 예: @Aspect가 붙은 클래스
@Aspect
@Component
public class LoggingAspect {
    // Advice 정의
}

 

📌 2-2. Advice (실행 시점 정의)

  • 공통 기능(Aspect)을 언제 실행할 것인가를 정의한 코드
  • 예: 메서드 실행 , , 예외 발생 시, 전후 모두
  • 종류:
    • @Before: 실행 전
    • @After: 실행 후 (성공/실패 무관)
    • @AfterReturning: 정상 종료 후
    • @AfterThrowing: 예외 발생 시
    • @Around: 전/후 모두 (가장 강력)
@Before("execution(* com.example.service.*.*(..))")
public void beforeLog() {
    System.out.println("메서드 실행 전 로그");
}

 

📌 2-3. JoinPoint (적용 가능한 지점)

  • Advice가 적용될 수 있는 실행 지점
  • 대부분의 Spring AOP에서는 메서드 실행 시점만 지원 (그 외에 생성자 호출, 필드 접근 시점 등이 있다.)
  • 예: 어떤 클래스의 getUser() 메서드 호출 전후
public void log(JoinPoint joinPoint) {
    String methodName = joinPoint.getSignature().getName();
    System.out.println("현재 실행 중인 메서드: " + methodName);
}

 

📌 2-4. Pointcut (JoinPoint 필터링)

  • Advice를 적용할 JoinPoint를 선별하는 규칙
  • execution() 등으로 표현식을 작성
  • 예:
    • execution(* com.example.service.*.*(..))
      → com.example.service 패키지 아래의 모든 메서드
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}

 

📌 2-5. Weaving (엮기)

  • Advice를 실제 JoinPoint에 결합(적용) 하는 과정
  • Spring AOP는 이 과정을 런타임 시점의 프록시 기반으로 수행
  • AspectJ는 컴파일 타임이나 로딩 타임에도 위빙 가능

 

📌 2-6. Target & Proxy

  • Target: AOP가 적용될 원본 객체 (실제 서비스 클래스)
  • Proxy: Target을 감싸는 가짜 객체. Advice 실행 후 실제 Target 호출
💡 Spring AOP는 바로 이 Proxy 객체를 통해 메서드 호출을 가로채서 Advice를 실행한다.

 


✅3. Proxy란? (Spring AOP의 기반)

AOP의 모든 실행은 프록시라는 기술적 기반 위에서 이루어진다.
스프링 AOP는 프록시 객체를 생성하여 공통 로직(Advice)을 실행하고, 그 뒤에 실제 메서드를 호출한다.
따라서 프록시를 이해하지 못하면 AOP의 동작 원리도 이해할 수 없다.

 

📌 3-1. 프록시란?

  • 프록시(Proxy) 는 "대리인" 이라는 뜻을 가지고 있다. ('가짜객체' 라고도 불린다.)
  • 실제 객체의 앞단에 위치해서 요청을 대신 받아 처리하거나 가로채는 객체이다.
즉, 클라이언트는 프록시에게 요청하고 프록시는 내부적으로 실제 객체를 호출하면서 중간에 부가 로직을 삽입할 수 있다.

 

📌 3-2. 프록시를 왜 써야 할까?

왜 굳이 새로운 객체까지 만드는 방식을 사용해야 하는걸까?

프록시는 단순한 새로운 객체가 아니다.

✅ 1. 핵심 로직을 건드리지 않고 부가 기능을 삽입할 수 있음

예를 들어 모든 서비스 메서드에 로그를 남기고 싶다고 해보자.

public void createOrder() { 
	System.out.println("LOG: createOrder called"); 
    	// 실제 로직... 
    }

 

이렇게 매 메서드에 로그 코드를 삽입하면 중복 + 유지보수가 어렵게 될 것이다.
반면, 프록시를 사용하면 메서드를 감싸면서도 핵심 로직은 전혀 수정하지 않고 로그, 트랜잭션, 보안 등을 삽입할 수 있다.

 

프록시를 쓰지 않는다면 같은 메서드 안에 코드에 추가하는 것이 아니면 메서드 실행 도중 부가 기능을 먼저 실행하는 것은 안된다.

그렇다고 부가 기능을 담은 코드를 매번 위에 써버리면 부가 기능을 따로 분리 하는 것이 목표이기 때문에 의미가 없다.

또한 자바에서 어떤 메서드를 호출하면, 그 메서드가 곧장 실행되며 그 호출 자체를 가로채거나 중간에 끼어들 방법은 없다.

 

그래서 프록시를 사용하면 클라이언트가 메서드를 호출하면 코드가 분리 되어있어도 프록시가 그 호출을 가로채서 자신(로그 남기기, 인증/인가 검사, 트랜잭션 시작 등)을 먼저 수행시킬 수 있다.

 

✅ 2. 컴파일 시점이 아닌 런타임에 유연하게 적용 가능

프록시 기반 AOP는 런타임에 Advice를 동적으로 주입한다.
덕분에 설정만 바꾸면 어떤 메서드에든 유연하게 부가기능을 붙일 수 있다.

 

예시: 설정만 바꾸면 로그가 자동으로 붙는 경우

🧩 1. 기존 서비스 코드 (로직에는 아무것도 없음)

// 핵심 비즈니스 로직 - 수정 X
@Service
public class PaymentService {
    public void pay(String method) {
        System.out.println("결제 처리: " + method);
    }
}
  • @Aspect도, 로그 코드도, 트랜잭션도 전혀 없음

🧩 2. 별도로 정의한 Aspect 클래스 (Advice만 정의)

@Aspect // 이 클래스가 AOP에서 사용할 공통 로직(Advice) 이라는 표시
@Component // 이 클래스를 스프링 빈으로 등록하겠다는 의미
public class LoggingAspect {

    @Before("execution(* com.example.service..*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("[LOG] 실행 전: " + joinPoint.getSignature().getName());
    }
}
  • service에만 Advice가 적용되며 포인트컷 조건만 바꾸면 어느 서비스 클래스든 적용 가능
  • 스프링은 시작 시 @Aspect가 붙은 Bean을 찾아 AOP 처리를 준비
  • 스프링은 execition 조건에 부합하는 클래스를 감싸는 프록시를 자동 생성

🧩 3. 결과

[LOG] 실행 전: pay
결제 처리: CARD

→ PaymentService 코드는 전혀 건드리지 않았는데 Advice가 런타임에 동적으로 붙은 것처럼 동작한다.

 

🧩 4. 만약 조건을 변경하면?

@Before("execution(* com.example.service.PaymentService.*(..))")

→ 이제는 PaymentService에만 Advice가 적용되고 다른 서비스에는 전혀 영향을 주지 않는다.

 

✅ 3. OOP의 한계를 극복하는 관점 지향적 구조 구현

객체지향(OOP)은 모듈화에 강하지만 공통 기능을 여러 객체에 일관되게 적용하는 데는 약하다.
프록시는 이 공통 기능을 객체와 분리해 외부에서 적용시킬 수 있도록 해준다.
즉, 관심사 분리(Separation of Concerns)를 실현하는 핵심 도구이다.

 


 

✅ 4. Spring AOP의 실행 흐름과 구조

이제까지는 개념과 구성 요소을 알아보았다면 이제는 실제로 어떻게 Spring AOP가 실행되고 있고

내부 구조는 어떻게 이루어져 있는지 알아보자. 또한 같은 AOP를 적용하는 기술인 AspectJ와의 차이까지 짚어보자.

📌 4-1. Spring AOP의 실행 흐름

스프링에서 AOP는 다음과 같은 구조로 동작한다:

[클라이언트 코드]
      ↓
[프록시 객체 (Advice 포함)]
      ↓
[실제 Target 객체 (핵심 로직)]
  1. 클라이언트가 Bean 메서드를 호출한다.
  2. 스프링이 생성한 프록시 객체가 그 요청을 먼저 받는다.
  3. 프록시는 Advice (Before, Around 등) 를 실행한다.
  4. 그 다음에 실제 Target 객체의 메서드를 호출한다.
  5. 결과값 또는 예외를 받아서 필요한 후처리(After, AfterReturning, AfterThrowing)를 수행한 뒤 반환한다.

 

📌 4-2. Spring AOP의 프록시 타입

Spring AOP는 클래스나 바이트코드를 조작하지 않는다.
대신 프록시 객체를 만들어서 원래 객체처럼 동작하도록 위장한다.

이때, 사용되는 프록시의 타입은 JDK와 CGLIB로 구분할 수 있다.

프록시 타입 조건 사용 방법 특징
JDK Dynamic Proxy 클래스가 인터페이스를 구현한 경우 java.lang.reflect.Proxy 사용 인터페이스 기반 프록시 생성
CGLIB Proxy 클래스에 인터페이스가 없는 경우 클래스를 상속하여 프록시 생성 실제 클래스를 확장한 프록시 생성
❗ @EnableAspectJAutoProxy(proxyTargetClass = true) 설정 시 강제로 CGLIB 사용 가능하다.
✅ JDK보다 CGLIB이 더 자주 사용된다.

 

🔹 1. JDK 프록시가 생성되는 경우

public interface UserService { void join(); }

@Service
public class UserServiceImpl implements UserService {
    public void join() { ... }
}
 

➡ UserServiceImpl은 인터페이스를 구현하므로 JDK 프록시 사용됨
➡ 프록시 클래스 이름: com.sun.proxy.$ProxyXX

 

🔹2. CGLIB 프록시가 생성되는 경우

@Service
public class UserService {
    public void join() { ... }
}

➡ 인터페이스 없이 클래스만 존재
➡ 프록시 클래스 이름: UserService$$EnhancerBySpringCGLIB$$...

 

📌 4-3. AOP 적용 방법: @Aspect 애너테이션 방식 vs XML 방식

방식 설명 장점 단점
@Aspect 애너테이션 자바 클래스에 직접 애너테이션으로 정의 간결하고 직관적 코드에 설정이 섞임
XML 설정 방식 <aop:config>로 AOP 설정 분리 설정 분리 가능, 코드 침투 없음 복잡하고 가독성 낮음
실무에서는 거의 대부분 애너테이션 방식을 사용한다.

 

🔹 1. @Aspect 애너테이션 방식

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("[LOG] 실행 전: " + joinPoint.getSignature().getName());
    }
}
  • 설정 없이도 Spring Boot에서는 자동으로 AOP 처리됨
  • 클래스 하나 + 의존성만 있으면 적용 가능

 

🔹 2. XML 설정 방식

(1) Java 클래스: Advice만 구현

public class LoggingAdvice {
    public void logBefore() {
        System.out.println("[LOG] 실행 전: 메서드 호출");
    }
}

 

(2) XML 설정 파일

<aop:config>
    <aop:aspect id="loggingAspect" ref="loggingAdvice">
        <aop:pointcut id="serviceMethods" expression="execution(* com.example.service.*.*(..))"/>
        <aop:before pointcut-ref="serviceMethods" method="logBefore"/>
    </aop:aspect>
</aop:config>

<bean id="loggingAdvice" class="com.example.LoggingAdvice"/>
  • Advice 클래스에는 @Aspect, @Before 같은 애너테이션 없음
  • AOP 설정을 모두 XML에서 분리해 구성함

 

📌 4-4. AOP 적용 기술: Spring AOP vs AspectJ

항목 Spring AOP AspectJ
위빙 시점 런타임 컴파일/로딩 타임
구현 방식 프록시 기반 바이트코드 조작
JoinPoint 범위 메서드 실행 시점 메서드 진입, 생성자 호출, 필드 접근 시점 등 포함
적용 대상 Spring Bean만 일반 객체도 가능
진입 장벽 낮음 (간단 설정) 높음 (Weaver/컴파일러 필요)
Spring AOP는 가볍고 제한적이지만 사용이 쉽고 Bean 생명주기와 잘 맞으며
AspectJ는 더 강력하지만 무겁고 복잡하다.

 

📌 4-5. Spring AOP의 적용 대상과 한계

  • ✅ 적용 가능:
    • Spring 컨테이너에 의해 관리되는 Bean 객체
    • 해당 객체의 public 메서드 실행 시점
    • 포인트컷 조건에 일치하는 경우
  • ❌ 적용 불가:
    • private 또는 final 메서드
    • 내부 호출(self-invocation): 클래스 내에서 자기 자신의 메서드 호출은 프록시를 거치지 않음
    • 일반 POJO 객체 (Spring Bean이 아닌 경우)

 


✅ 5. Spring AOP 예시

 

  • 핵심 비즈니스 로직을 수정하지 않고
  • 로깅, 실행 시간 측정 등의 공통 기능(Advice)을 AOP로 외부에서 주입하는 구조를 직접 구현해보기

 

📌 파일 구성

역할 클래스명 설명
핵심 비즈니스 로직 OrderService 주문을 처리하는 서비스
Aspect LoggingAspect 메서드 실행 전후에 로그 출력
실행 DemoApplication 스프링 부트 실행 메인 클래스

 

🔹 1. 핵심 서비스 클래스

// OrderService.java
package com.example.service;

import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public void placeOrder(String product) {
        System.out.println("product: " + product);
    }
}
  • 핵심 로직에는 AOP 관련 코드가 전혀 없음
  • AOP가 잘 작동하면, placeOrder 실행 전에 로그가 찍혀야 함

🔹 2. Aspect 클래스 (@Before, @After, @Around)

// LoggingAspect.java
package com.example.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service..*(..))")
    public void logBefore() {
        System.out.println("[Before] method before");
    }

    @After("execution(* com.example.service..*(..))")
    public void logAfter() {
        System.out.println("[After] method after");
    }

    @Around("execution(* com.example.service..*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        System.out.println("[Around] start: " + joinPoint.getSignature().getName());
        Object result = joinPoint.proceed(); // 이 시점에서 실제 메서드 실행
        long end = System.currentTimeMillis();
        System.out.println("[Around] close: " + joinPoint.getSignature().getName() + ", run-time: " + (end - start) + "ms");
        return result;
    }
}
  • @Before: 메서드 실행 직전에 호출
  • @After: 예외 여부 관계없이 메서드 종료 후 호출
  • @Around: 메서드 실행 전후 전체를 감싸고 실행 시간 측정 가능

 

🔹 3. 실행 클래스

// Application.java
package com.example;

import com.example.service.OrderService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = "com.example")
public class Application implements CommandLineRunner {

    private final OrderService orderService;

    public Application(OrderService orderService) {
        this.orderService = orderService;
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) {
        orderService.placeOrder("ipad pro");
        System.out.println(orderService.getClass());
    }
}

 

✅ 실행 결과

 

  • 핵심 클래스에는 로깅 코드가 전혀 없는데 로그가 출력되었는가? → O
  • 메서드 실행 순서가 Around → Before → After → Around로 맞는가? → O

 


 

✅ 6. Spring AOP의 한계와 주의점

문제 원인 해결방법
self-invocation 프록시를 거치지 않음 구조 분리, 프록시 Bean 호출
private/final/static 프록시로 감쌀 수 없음 접근제어자 변경, 구조 리팩터링
일반 객체 Bean이 아님 @Component로 Bean 등록
포인트컷 오타 표현식 미일치 표현식 명확히 검증
디버깅 어려움 프록시 클래스명 AopUtils 활용 또는 프록시 제거 테스트

 

📌 self-invocation 문제 예시 (내부 호출 안 먹힘)

@Service
public class UserService {

    public void outer() {
        inner();  // AOP 적용 안 됨
    }

    @Transactional
    public void inner() {
        // 트랜잭션 시작 안 됨
    }
}

📌 디버깅이 어려움

  • 프록시가 생성되면 객체 이름이 밑에 예시처럼 달라지게 된다.
com.example.service.UserService$$EnhancerBySpringCGLIB$$123abc
  • → 실제 클래스가 아닌 프록시 객체이므로 디버깅, instanceof 비교, 테스트 시 혼란이 생길 수 있음
  • 해결 방법:
    • AopUtils.isAopProxy(bean) 등으로 프록시 여부 확인
    • 테스트에서는 AOP 제거하거나 ProxyTargetClass 강제 설정 고려

'Spring' 카테고리의 다른 글

[Spring] 트랜잭션 전파(Propagation) 정리  (3) 2025.07.12
[Spring] JDBC Template 정리  (0) 2025.05.31
[Spring] MVC 정리  (0) 2025.04.12
[Spring] IoC / DI 정리  (0) 2025.03.22
[Spring] 빈 / 빈 스코프(Bean / Bean Scope) 정리  (0) 2025.03.19