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 등)를 깊이 있게 다룬다.
'Spring Framework > Spring' 카테고리의 다른 글
| [Spring] Spring AOP — Advice 종류와 실행 순서 (0) | 2026.06.01 |
|---|---|
| [Spring] Spring AOP - Pointcut 지시자 완전 정복 (0) | 2026.06.01 |
| [Spring] 트랜잭션 완전 정리 - @Transactional의 내부 동작과 함정들 (0) | 2026.05.31 |
| [Spring] Spring은 DB 예외를 어떻게 바꾸는가 (0) | 2026.05.27 |
| [Spring] Spring의 비동기 처리 — @Async부터 Virtual Thread 통합까지 (0) | 2026.03.16 |