💡 여러 곳에 흩어지는 공통 관심사(cross-cutting concern)를 핵심 비즈니스 로직과 분리해서 관리하는 프로그래밍 방식. Aspect-Oriented Programming
로깅, 트랜잭션, 인증/권한 체크, 성능 측정, 예외 처리 공통으로 처리
유지 보수성 ↑
Client -> Proxy (AOP 적용) -> Target 식으로 적용
| 용어 | 의미 |
|---|---|
| Aspect | 공통 관심사 모음 (클래스) |
| Advice | 언제 무엇을 할지 (메서드) |
| Join Point | AOP가 끼어들 수 있는 지점 (메서드 실행 등) |
| Pointcut | 어떤 Join Point에 적용할지 조건 |
| Target | 실제 비즈니스 객체 |
| Proxy | Target을 감싸는 객체 |
// @Around는 가장 강력한 Advice
// 메서드 실행 전 / 후 / 예외 발생 시점까지 모두 제어 가능
@Around("pointcut()")
// ProceedingJoinPoint: 실제 타겟 메서드를 대리 실행하는 객체
// proceed()를 호출해야만 진짜 비즈니스 로직이 실행됨
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// before
Object result = pjp.proceed(); // 실제 메서드 실행
// after
return result;
}
@Aspect // 공통관심사 선언, 프록시 생성 대상으로 인식
@Component // spring Bean으로 등록
public class LogAspect {
// com.example.service 패키지 이하의 모든 메서드 실행을 가로챔
@Around("execution(* com.example.service..*(..))")
// joinPoint = 지금 실행될 메서드 정보
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
// 실행시간 측정 및 실제 메서드 실행
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
// getSignature() → 메서드 시그니처 정보
System.out.println(joinPoint.getSignature() + " 실행 시간: " + (end - start));
return result;
}
}
// @Service → Spring Bean. com.example.service 패키지 → Pointcut 조건 만족
@Service
public class OrderService {
public void order() {
// 비즈니스 로직
}
}
// pointCut 문법
// execution(접근제어자 반환타입 패키지.클래스.메서드(파라미터))
// execution(* com.example..*Service.*(..))
// * 아무거나, .. 하위 패키지 전체
프록시 기반으로 Spring Bean만 AOP 적용 가능(new로 직접 만든 객체 x)
메서드 내부 호출은 적용 x(프록시 안 거치기 때문)
인터페이스, 클래스 기준. 인터페이스 있으면 JDK Dynamic Proxy, 없으면 CGLIB
| 구분 | AOP | Interceptor | Filter |
|---|---|---|---|
| 레벨 | 메서드 | 컨트롤러 | 서블릿 |
| 대상 | Bean | HTTP 요청 | 요청/응답 |
| 용도 | 로직 공통화 | 인증/로깅 | 인코딩 |
💡 실제 객체(Target) 앞에 대리 객체(Proxy)를 두고, 호출을 가로채서 부가 기능을 수행하는 구조
Spring AOP = Proxy 기반 AOP
@Autowired
OrderService orderService;
// 실제로는 OrderServiceProxy 가 주입
interface Service {
void run();
}
class RealService implements Service {
public void run() {
System.out.println("비즈니스 로직 실행");
}
}
class ServiceProxy implements Service {
private final Service target;
public ServiceProxy(Service target) {
this.target = target;
}
@Override
public void run() {
System.out.println("[AOP] before");
target.run();
System.out.println("[AOP] after");
}
}
public class Main {
public static void main(String[] args) {
Service service = new ServiceProxy(new RealService());
service.run();
}
}
대상 클래스가 인터페이스 구현한 경우 사용
Java 표준(java.lang.reflect.Proxy)
인터페이스 기반
실제 구현체 상속 x
public interface OrderService {
void order();
}
public class OrderServiceImpl implements OrderService { }
Code Generation Library
Spring이 인터페이스가 없는 클래스에 쓰는 방식.
메서드 오버라이딩 방식
final / private 메서드 프록시 불가
대상 클래스를 상속받아 프록시를 만듦
Spring은 내부적으로 CGLIB 사용
private 메서드가 AOP 에 걸리지 않음
class Service {
public void a() {
b(); // private
}
private void b() {}
}
CGLIB은 Service를 상속해서 프록시 생성
private 메서드는 자식 클래스에서 접근 불가 → 프록시가 끼어들 수 없음
JDK Dynamic Proxy도 마찬가지
인터페이스 메서드만 대상
private은 애초에 대상 아님
| 상황 | 이유 |
|---|---|
| private 메서드 | 프록시가 오버라이딩 불가 |
| final 메서드 | CGLIB 오버라이딩 불가 |
| static 메서드 | 객체 메서드 호출이 아님 |
| self-invocation | 프록시를 거치지 않음 |
| new로 생성한 객체 | Spring Bean 아님 |
Spring AOP는 프록시 기반, 같은 클래스 내부에서 메서드를 호출(self-invocation)하면 프록시를 거치지 않아 AOP가 적용되지 않는다.
Client -> Proxy -> target.outer() vs target.outer() -> this.inner()
@Service
public class MyService {
@Transactional
public void outer() {
inner();
}
@Transactional
public void inner() {
// 트랜잭션 대상
}
}
해결방법_메서드 분리
@Service
class InnerService {
@Transactional
public void inner() {}
}
@Service
class OuterService {
private final InnerService innerService;
public void outer() {
innerService.inner();
}
}