Topic (오늘의 주제)
AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍) 는 핵심 비즈니스 로직과 애플리케이션 전반에 걸쳐 반복되는 횡단 관심사(부가 기능) 를 분리하여 모듈화하는 프로그래밍 기법이다. Spring AOP는 프록시(Proxy) 패턴을 활용해 런타임에 부가 기능을 적용하며, @Transactional, 로깅, 권한 검사 등이 이 방식으로 동작한다.
Why (왜 사용하는가? 왜 중요한가?)
- 문제점: 로깅, 권한 검사, 성능 측정(시간 측정) 같은 코드가 여러 서비스·컨트롤러에 반복되면 비즈니스 로직과 섞여 가독성과 유지보수가 어려워진다.
- 해결책: 핵심 로직과 부가 기능(횡단 관심사)을 분리해, 한 곳에서 부가 기능을 정의하고 필요한 메서드에만 적용할 수 있게 한다.
- 면접 포인트:
@Transactional이 어떻게 동작하는지, 왜 같은 클래스 안에서 호출하면 동작하지 않는지(내부 호출 문제) 를 설명하려면 AOP와 프록시 동작 원리 이해가 필수다.
"AOP(관점 지향 프로그래밍)에 대해 설명해 주시겠어요?"
"AOP는 핵심 비즈니스 로직과 애플리케이션 전반에 걸쳐 나타나는 횡단 관심사(부가 기능)를 분리하여 모듈화하는 프로그래밍 기법입니다. 로깅, 시간 측정, 권한 검사 같은 코드가 비즈니스 로직에 섞여 중복되는 것을 막아줍니다. Spring AOP는 주로 프록시(Proxy) 패턴을 활용해 런타임 시점에 동작하며, @Transactional이나 인터셉터 등도 이 AOP 개념을 기반으로 동작합니다."
1. AOP가 필요한 상황 (등장 배경)
횡단 관심사(Cross-cutting Concern)란?
여러 클래스·메서드에 공통으로 들어가는 부가 기능을 말한다. 비즈니스 로직과는 별개지만, 여러 곳에 반복해서 작성해야 하는 코드다.
흔한 예:
- 로깅 (메서드 진입/종료, 파라미터·반환값 기록)
- 트랜잭션 관리 (
@Transactional) - 권한/인증 검사
- 실행 시간 측정 (성능 모니터링)
- 예외 변환·로깅
AOP 없이 작성하면 생기는 문제
@Service
public class OrderService {
public Order createOrder(OrderRequest request) {
// 매번 로깅 코드 반복
log.info("createOrder 호출, request={}", request);
long start = System.currentTimeMillis();
try {
Order order = orderRepository.save(...); // 핵심 로직
log.info("createOrder 완료, orderId={}", order.getId());
return order;
} finally {
log.info("실행 시간: {}ms", System.currentTimeMillis() - start);
}
}
public Order getOrder(Long id) {
log.info("getOrder 호출, id={}", id);
long start = System.currentTimeMillis();
try {
Order order = orderRepository.findById(id); // 핵심 로직
return order;
} finally {
log.info("실행 시간: {}ms", System.currentTimeMillis() - start);
}
}
}
문제점:
- 중복: 로깅·시간 측정 코드가 메서드마다 반복된다.
- 가독성 저하: 핵심 로직과 부가 기능이 섞여 있어 의도가 불명확하다.
- 유지보수 어려움: 로깅 형식을 바꾸려면 모든 메서드를 수정해야 한다.
AOP로 분리한 뒤
@Service
public class OrderService {
public Order createOrder(OrderRequest request) {
return orderRepository.save(...); // 핵심 로직만
}
public Order getOrder(Long id) {
return orderRepository.findById(id); // 핵심 로직만
}
}
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logAndMeasure(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("{} 호출, args={}", joinPoint.getSignature(), joinPoint.getArgs());
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
log.info("실행 시간: {}ms", System.currentTimeMillis() - start);
return result;
} catch (Throwable e) {
log.error("예외 발생", e);
throw e;
}
}
}
효과:
- 서비스 클래스는 비즈니스 로직만 담당한다.
- 로깅·시간 측정은 한 곳(Aspect) 에만 정의하고, 적용 대상을 Pointcut 으로 지정한다.
2. AOP 핵심 용어 정리
부가 기능을 어디에, 무엇을, 언제 적용할지 정의하는 데 쓰는 용어다.
| 용어 | 설명 (쉽게) |
|---|---|
| Target | 부가 기능이 적용되는 대상 객체 (실제 비즈니스 로직을 가진 클래스) |
| Advice | 어떤 부가 기능을 넣을지에 대한 구체적인 로직 (로깅, 트랜잭션 시작/커밋 등) |
| Join Point | Advice가 끼어들 수 있는 실행 시점 (Spring AOP는 메서드 실행 시점만 지원) |
| Pointcut | 어느 메서드에 Advice를 적용할지 정하는 조건 (예: 특정 패키지, 특정 어노테이션) |
| Aspect | Advice + Pointcut 을 묶어서 하나의 관심사(부가 기능 모듈)로 정의한 것 |
한 줄로:
Pointcut으로 "어디에" 적용할지 정하고, Advice로 "무엇을(어떤 로직을)" 넣을지 작성한다. 이걸 Aspect로 묶어서 관리한다.
3. Advice의 5가지 종류
메서드 실행을 기준으로, 언제 부가 기능이 실행될지에 따라 다섯 가지로 나뉜다.
| 종류 | 실행 시점 | 사용 예 |
|---|---|---|
| @Before | 타겟 메서드 호출 전 | 파라미터 검증, 로깅 |
| @AfterReturning | 타겟 메서드가 정상 반환한 후 | 반환값 로깅 |
| @AfterThrowing | 예외가 발생했을 때 | 예외 로깅, 알림 |
| @After | 정상/예외 관계없이 메서드 종료 후 | 리소스 정리 |
| @Around | 메서드 실행 전·후 모두 제어 | 트랜잭션, 로깅, 시간 측정 (가장 많이 사용) |
간단한 코드 예시
@Aspect
@Component
public class ExampleAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
log.info("메서드 실행 전: {}", joinPoint.getSignature());
}
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
log.info("정상 반환 후, result={}", result);
}
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
log.error("예외 발생: {}", ex.getMessage());
}
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("전처리");
Object result = joinPoint.proceed(); // 실제 메서드 실행
log.info("후처리");
return result;
}
}
[!note] Around를 가장 많이 쓰는 이유
@Around는joinPoint.proceed()호출 전후로 자유롭게 로직을 넣을 수 있어, 트랜잭션 시작/커밋, 실행 시간 측정, 예외 래핑 등을 한 번에 처리하기 좋다.@Transactional도 내부적으로 Around 형태의 Advice로 동작한다.
4. Spring AOP의 동작 원리: 프록시(Proxy)
Spring AOP는 런타임에 타겟 객체를 프록시 객체로 감싼 뒤, 클라이언트가 그 프록시를 통해 호출하도록 한다. 그래서 호출이 들어오면 프록시가 먼저 Advice(부가 기능)를 실행하고, 그다음 실제 타겟 메서드를 호출한다.
호출 흐름
[클라이언트] ──호출──▶ [AOP 프록시 객체] ──위임──▶ [실제 Target 객체]
│
├─ Advice 실행 (로깅, 트랜잭션 등)
└─ target.메서드() 호출- 클라이언트는 타겟을 직접 부르는 것이 아니라 프록시를 부른다.
- 프록시가 부가 기능을 수행한 뒤, 내부적으로 실제 객체의 메서드를 호출한다.
- 따라서 Spring AOP는 메서드 호출 시점에만 부가 기능을 넣을 수 있고, 이를 런타임 위빙이라고 부른다.
프록시가 적용된 코드 (개념)
// 스프링이 만드는 프록시 (개념적 코드)
public class OrderService$$SpringProxy extends OrderService {
private final OrderService target; // 실제 대상 객체
@Override
public Order createOrder(OrderRequest request) {
// 1. 부가 기능 실행 (트랜잭션 시작, 로깅 등)
transactionManager.begin();
log.info("createOrder 호출");
// 2. 실제 타겟 메서드 호출
Order result = target.createOrder(request);
// 3. 부가 기능 마무리 (커밋, 로깅 등)
transactionManager.commit();
return result;
}
}
5. 내부 호출(Self-Invocation) 문제 — 반드시 이해할 것
[!important] 주의: 내부 호출 시 AOP가 적용되지 않음
같은 클래스 안에서 자기 자신의 다른 메서드를 호출하면, 그 호출은 프록시를 거치지 않고 타겟 객체의 메서드를 직접 호출하게 된다.
그래서 그 내부 메서드에 걸린 @Transactional 이나 다른 AOP가 전혀 동작하지 않는다.
왜 그런가?
- AOP가 적용되려면 반드시 프록시를 통한 호출이어야 한다.
- 클라이언트 → 프록시 → 타겟 순서로만 호출이 들어올 때 Advice가 실행된다.
- 같은 객체 안에서
this.다른메서드()를 호출하면 this는 프록시가 아니라 실제 객체이기 때문에, 프록시를 타지 않는다.
문제가 되는 코드 예시
@Service
public class OrderService {
public void methodA() {
// methodB()를 내부에서 호출 → 프록시를 거치지 않음!
this.methodB(); // @Transactional 적용 안 됨
}
@Transactional
public void methodB() {
// DB 작업
}
}
- 외부에서
orderService.methodB()를 호출하면 → 프록시를 타므로@Transactional동작 ✅ - 같은 클래스의
methodA()에서this.methodB()를 호출하면 → 프록시를 타지 않으므로@Transactional동작 ❌
해결 방법
1) 부가 기능이 필요한 메서드를 다른 빈으로 분리 (권장)
@Service
public class OrderService {
private final OrderTxHelper orderTxHelper;
public OrderService(OrderTxHelper orderTxHelper) {
this.orderTxHelper = orderTxHelper;
}
public void methodA() {
orderTxHelper.methodB(); // 다른 빈 호출 → 프록시를 타므로 @Transactional 동작
}
}
@Service
public class OrderTxHelper {
@Transactional
public void methodB() {
// DB 작업
}
}
2) 자기 자신의 프록시를 주입받아 호출 (비권장, 구조가 복잡해짐)
@Service
public class OrderService {
@Autowired
private ApplicationContext context; // 또는 self-injection
public void methodA() {
OrderService self = context.getBean(OrderService.class);
self.methodB(); // 프록시를 통해 호출
}
@Transactional
public void methodB() { ... }
}
실무에서는 1) 별도 서비스/헬퍼로 분리하는 방식을 추천한다.
6. Spring AOP vs AspectJ
| 구분 | Spring AOP | AspectJ |
|---|---|---|
| 기반 | 프록시 패턴 | 자바용 AOP 구현체 (위빙) |
| 위빙 시점 | 런타임 (프록시 생성) | 컴파일 타임 / 로드 타임 |
| Join Point | 메서드 실행만 | 메서드 호출·실행, 생성자, 필드 접근 등 |
| 적용 대상 | 스프링 빈만 | 모든 자바 객체 |
| 설정 | 스프링 설정만으로 사용 가능 | 별도 도구/플러그인 필요 |
정리:
- 일상적인 로깅, 트랜잭션, 권한 체크는 Spring AOP로 충분하다.
- 생성자 호출, 필드 접근 등 메서드 외부에 부가 기능을 붙여야 하면 AspectJ를 고려한다.
7. 흔한 실수와 오해
"트랜잭션이 왜 안 먹지?"
- 원인: 같은 클래스 안에서
@Transactional이 없는 메서드가,@Transactional이 붙은 메서드를 내부에서 호출하고 있는 경우다. - 이유: 내부 호출은 프록시를 거치지 않아 트랜잭션 Advice가 실행되지 않는다.
- 대응: 트랜잭션이 필요한 부분을 별도 빈(서비스/헬퍼) 으로 분리하고, 그 빈의 메서드를 호출하도록 변경한다.
8. 예상 꼬리 질문 정리
Q1. Spring AOP는 어떤 방식으로 동작하나요?
런타임에 프록시 패턴으로 동작합니다. 스프링은 AOP가 적용된 빈에 대해 프록시 객체를 만들고, 클라이언트는 타겟 대신 이 프록시를 호출하게 됩니다. 프록시가 먼저 Advice(부가 기능) 를 실행한 뒤, 실제 타겟 메서드를 호출합니다.
Q2. 내부 호출 시 @Transactional이 동작하지 않는 이유와 해결 방법은?
Spring AOP는 프록시를 통한 호출에만 적용됩니다. 같은 클래스 안에서 this.메서드()를 호출하면 프록시를 거치지 않고 타겟을 직접 호출하기 때문에 AOP가 적용되지 않습니다.
해결: 트랜잭션이 필요한 로직을 별도 빈(클래스) 으로 분리하고, 그 빈을 주입받아 호출하면 프록시를 타므로 @Transactional이 정상 동작합니다.
Q3. Spring AOP와 AspectJ의 차이는?
Spring AOP는 스프링 빈에 대해 런타임에 프록시를 만들어 메서드 실행 시점에만 부가 기능을 적용합니다.
AspectJ는 컴파일/로드 타임 위빙을 사용하며, 메서드 호출·생성자·필드 접근 등 다양한 Join Point를 지원하고, 스프링 빈이 아닌 일반 자바 객체에도 적용할 수 있는 AOP 구현체입니다.
요약
- AOP는 핵심 비즈니스 로직과 횡단 관심사(로깅, 트랜잭션, 권한 등) 를 분리해 모듈화하는 기법이다.
- Pointcut으로 적용할 메서드를 정하고, Advice로 부가 기능 로직을 작성하며, Aspect로 묶어 관리한다.
- Advice 종류: Before, AfterReturning, AfterThrowing, After, Around(가장 활용도 높음).
- Spring AOP는 프록시를 이용한 런타임 위빙이며, 메서드 실행에만 적용된다.
- 내부 호출(같은 클래스에서
this.메서드())은 프록시를 타지 않아 @Transactional 등 AOP가 적용되지 않는다. 해결은 트랜잭션 로직을 별도 빈으로 분리하는 것이다. - Spring AOP는 빈 + 메서드 실행 중심, AspectJ는 다양한 Join Point와 컴파일/로드 타임 위빙을 지원한다.
참고 자료
'Spring' 카테고리의 다른 글
| 인 메모리가 빠른데 어떻게 활용해야하나요 (1) | 2026.03.14 |
|---|---|
| Spring_29) Bean 그냥 다 해줬잖아 (0) | 2026.01.20 |
| Spring_27) 쿼리가... 무한히 증식(N+1)하고 있어! (1) | 2026.01.14 |
| Spring_26) 한 계단씩 올라가야 이해하는 Spring 동작 및 구조 (0) | 2026.01.13 |
| Spring_25) 미안하다 이거 보여주려고 JDBC만 쓴다고 어그로 끌었다 (0) | 2026.01.12 |