1. 빈 후처리기
- 스프링에서 빈을 생성할때 빈 후처리기를 통해 빈 컨테이너로 등록되게 된다
- 그래서 빈 후처리기를 통해 빈 객체를 조작하거나 다른 객체로 바꿔치기 가능
1). 자바에서 쓰는 빈 후처리기
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B {
public void helloB() {
log.info("hello B");
}
}
@Slf4j
static class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={} bean={}", beanName, bean);
if (bean instanceof A) {
return new B();
}
return bean;
}
}
2). 스프링에서 제공하는 빈 후처리기
- aop 의존성만 추가하면 @Aspect도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다
- 자동 프록시 생성기가 등록이 된다
- 빈 후처리기가 스프링 빈에 자동으로 등록된다
- Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다

@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor advisor1(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
포인트컷은 2가지에 사용된다
- 프록시 적용 여부 판단 - 생성단계
- 해당 빈이 프록시를 생성할 필요가 있는지 체크한다
- 클래스 + 메서드 조건을 모두 비교해서 조건이 하나라도 맞으면 프록시 생성
- 어드바이스 적용 여부 판단 - 사용단계
- 어드바이스를 적용여부를 포인트컷을 보고 판단한다
2). @Aspect
- 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다
- 자동 프록시 생성기는
@Aspect를 Advisor로 변환해준다
자동 프록시 생성기
@Aspect를 Advisor로 변환해준다
- 내부에 AdviserBuilder를 통해 어드바이저를 생성한다
- 어드바이저를 기반으로 프록시를 생성한다

- Advisor 빈을 다 조회한다
- 컨테이너 생성시점에 생성된 @Aspect Advoser를 조회한다

@Slf4j
@Aspect
public class LogTraceAspect {
private final LogTrace logTrace;
public LogTraceAspect(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Around("execution(* hello.proxy.app..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
TraceStatus status = null;
try {
String message = joinPoint.getSignature().getName();
status = logTrace.begin(message);
Object result = joinPoint.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
3. AOP
- 애플리케이션 로직은 크게 핵심 로직과 부가 기능으로 나눌수 있다
- 핵심 기능
- 해당 객체가 제공하는 고유의 기능,
OrderService의 핵심기능은 주문 로직
- 부가 기능
- 핵심 기능을 보조하기 위해 제공되는 기능
- 로그 추적 로직, 트랜잭션 기능
- 부가 기능은 단독으로 사용되지 않고, 핵심 기능과 함께 사용된다
부가기능
- 횡단 관심사라고 한다
- 보통 부가 기능은 여러 클래스에 걸쳐서 함께 사용된다

1). 소개
- 부가기능을 핵심 기능에서 분리하고 한 곳에서 관리하도록 했다
Aspect: 부가기능과 부가기능을 어디에 적용할지 선택하는 기능을 합해서 하나의 모듈로 만들었다
- AOP: Aspect-oriendted Programming

용어정리
- 조인포인트
- 어드바이스가 적용될 수 있는 위치
- 조인포인트는 추상적인 개념
- AOP를 적용할 수 있는 모든 지점이라고 생각하면 된다
- 조인포인트는 항상 메소드 실행 지점으로 제한된다
- 포인트컷
- 조인포인트 중에서 어드바이스가 적용될 위치를 선별한다
- 주로 AspectJ표현식을 사용해서 지정한다
- 타겟
- 실질적으로 어드바이스를 받는 객체, 포인트컷으로 결정
- 어드바이스
- 부가기능
- Around, Before, After 같은 다양한 종류의 어드바이스가 있다
- 애스펙트
- 어드바이스 + 포인트컷을 모듈화 한것, @Aspect를 생각하면 된다
- 어드바이저
- 하나의 어드바이스와 하나의 포인트 컷으로 구성
- 위빙
- 포인트컷으로 결정한 타켓 조인 포인트에 어드바이스를 적용하는 것
- 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가할 수 있다
2) AOP 적용 방식
- AOP의 3가지 적용 방식
- 스프링 AOP는 런타임방식이고, AspectJ를 이용하면 나머지 두가지 방법을 사용할 수 있다
1. 컴파일 시점 (위빙)
- 컴파일러를 사용해서
.class를 만드는 시점에 기능 로직 추가
- 컴파일 시점에 부가기능을 적용하려면 특별한 컴파일러도 필요하고 복잡한다
2. 클래스 로딩 시점 (로드 타임 - 위빙)
.class 파일을 JVM에 저장하기 전에 조작할 수 있는 기능
- 자바를 실행할 때 특별한 옵션을 통해 클래스 로더 조작기를 지정해야하는데 어렵고 복잡하다
3. 런타임 시점 (런타임 - 위빙)
- 바이트코드를 실제 조작하기 때문에 해당 기능을 모든 지점에 다 적용할 수 있다
- 프록시를 사용하는 스프링 AOP는 메서드 실행 지점에만 AOP를 적용할 수 있다
- 오버라이딩 개념으로 동작하기 때문에 생성자, static, 필드 값 접근에는 프록시 적용이 안된다
- 스프링 컨테이너가 관리할 수 있는 스프링 빈에만 AOP를 적용할 수 있다
4. 포인트컷
public String hello.aop.order.aop.member.MemberServiceImpl.hello(String)
반환타입 주소.메서드명(파라미터)
execution(접근제어자? 반환타입 선언타입? 메서드이름(파라미터) 예외?)
1). 포인트컷 분리
- 포인트 컷 시그니처라고도 불린다
- 메서드 반환 타입은 void 여야한다
- public으로 하면 다른 패키지에서도 적용할 수 있다
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){}
@Pointcut("execution(* *..*Service.*(..)))")
private void allService(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
@Around("hello.aop.order.aop.PointCuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("hello.aop.order.aop.PointCuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
2). 포인트컷 지시자
- execution : 메소드 실행 조인 포인트를 실행
- within : 특정 타입 내의 조인 포인트를 매칭
- args : 인자가 주어진 타입의 인스턴스인 조인 포인트
- this: 스프링 빈 객체(프록시)를 대상으로 하는 조인 포인트
- target: Target 객체(실제 대상)를 대상으로 하는 조인 조인포인트
- @target: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인포인트
- @within: 주어진 애노테이션이 있는 타입 타입 내 조인포인트
- @anootation: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트
- @args: 전달된 실제 인수의 런타임 타입의 애노테이션을 갖고 조인 포인트
- bean: 스프링 전용 포인트컷 지시자, 빈 이름으로 포인트컷을 지정
(1). execution
public String hello.aop.order.aop.member.MemberServiceImpl.hello(String)
execution(접근제어자? 반환타입 선언타입? 메서드이름(파라미터) 예외?)
- 부모 타입을 선언해도 그 자식 타입은 매칭된다
- 부모 타입에 있는 메서드만 가능
- 오버라이딩 한 메서드만 가능하다
- 파라미터는 부모타입은 허용하지 않는다
매칭조건
- 접근제어자?:
public
- 반환타입:
String
- 선언타입?:
hello.aop.order.aop.member.MemberServiceImpl
- 메서드 이름:
hello
- 파라미터:
(String)
- 예외? : 생략
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
pointcut.setExpression("execution(* *(..))");
pointcut.setExpression("execution(* hello.aop.member.*(..))");
pointcut.setExpression("execution(* hello.aop..*.*(..))");
pointcut.setExpression("execution(* hello(..))");
pointcut.setExpression("execution(* hel*(..))");
pointcut.setExpression("execution(* hello..*.*(String, ..))");
(2). Within
- execution에서 타입부분만 사용한다
- 부모 타입을 지정하면 안된다. 정확하게 타입이 맞아야 한다
@Test
void withinExact(){
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
}
(3). args
- 기본문법은
execution의 args 부분과 같다
- args는 부모 타입을 허용
- 메서드들의 파라미터만 보고 판단한다
@Test
void args(){
pointcut.setExpression("args(Object, ..)");
pointcut.setExpression("args(String)");
pointcut.setExpression("args(*)");
}
(4). @target, @within
- 애노테이션를 보고 AOP를 건다
- @target
- 자식에 걸어도 부모의 메서드까지 전부 어드바이스 적용된다
- @within
- 자기 자신의 클래스에 정의된 메서드만 어드바이스 적용된다
- args, @args, @target 같은 포인트컷 지시자는 모든곳에 프록시를 걸기때문에 단독사용하지 말자
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop")
@Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop")
(5). @annotation, @args
@Around("@annotation(hello.aop.member.annotation.MethodAop)")
@args는 안씀
(6). bean
@Around("bean(orderSerivce) || bean(*Repoisitory)")
(7). 매개변수 전달
- 타입이 맞는 애들만 들어온다
- 포인트컷 이름과 매개변수 이름을 맞춰야 한다
@Around("execution(* hello.aop.member.**.*(..))")
private Object logArgs(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
@Around("hello.aop.order.aop.PointCuts.orderAndService() && args(arg,..)")
public Object doTransaction(ProceedingJoinPoint joinPoint, String arg) throws Throwable {
log.info("[트랜잭션 시작] {} args={}", joinPoint.getSignature(),arg);
return joinPoint.proceed();
}
@Before("allService() && target(obj)")
public void logArgs3(JoinPoint joinPoint, MemberService obj) {
log.info("[this], obg={}",joinPoint.getArgs(), obj.getClass());
}
@Before("allService() && this(obj)")
public void logArgs4(JoinPoint joinPoint, MemberService obj) {
log.info("[this], obg={}",joinPoint.getArgs(), obj.getClass());
}
@Around("execution(* hello.aop.member.**.*(..) && @witnin(annotation))")
private Object targetArgs(ProceedingJoinPoint joinPoint, ClassAop annotation) throws Throwable {
log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), annotation);
return joinPoint.proceed();
}
(8). this, target
- 적용 타입 하나만 정확하게 지정해야 한다.
- *같은 타입 사용 X, 부모 타입은 허용
- this
- This는 프록시 객체를 대상으로 한다
- JDK 동적 프록시를 적용했을 때
MemberServiceImpl라는 구체 클래스를 지정하면 MemberService인터페이스 기반으로 구현된 새로운 클래스가 프록시 객체로 생성되기 때문에 MemberServiceImpl은 AOP 적용대상이 안된다
- CGLIB는 구체클래스기반이기 때문에 구체 클래스를 지정해도 AOP 적용대상이 된다
- target
- Target은 객체(실제 대상)를 대상으로 한다
- target은 인터페이스랑 구체 클래스 둘다 사용가능하다
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
5). 어드바이스
- @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스
- @Before: 조인 포인트 실행 이전에 실행
- @After Returning: 조인 포인트가 정상 완료후 실행
- @After throwing: 메서드가 예외를 던지는 경우 실행
- @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

@Slf4j
@Aspect
public class AspectV6Advice {
@Around("hello.aop.order.aop.PointCuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
@Before("hello.aop.order.aop.PointCuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.PointCuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.PointCuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[return] {} message={}", joinPoint.getSignature(), ex);
}
@After(value = "hello.aop.order.aop.PointCuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {} ", joinPoint.getSignature());
}
}
1). 어드바이스 순서
- 메서드로는 순서를 보장하지 않는다
- 그래서 다른 클래스로 빼거나 내부 클래스를 이용
@Slf4j
@Aspect
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect{
@Around("hello.aop.order.aop.PointCuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect{
@Around("hello.aop.order.aop.PointCuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
}
6. 스프링 AOP 주의사항
(1). 클래스 내부에서 메서드가 메서드를 호출하면 프록시 적용이 안됨
@Slf4j
@Component
public class CallServiceV1 {
private CallServiceV1 callServiceV1;
@Autowired
public void setCallServiceV1(CallServiceV1 callServiceV1) {
this.callServiceV1 = callServiceV1;
}
public void external() {
log.info("call external");
callServiceV1.internal();
}
public void internal() {
log.info("call internal");
}
}
```java
@Slf4j
@Component
public class CallServiceV2 {
private final ObjectProvider<CallServiceV2> callServiceProvider;
public CallServiceV2(ObjectProvider<CallServiceV2> callServiceProvider) {
this.callServiceProvider = callServiceProvider;
}
public void external() {
log.info("call external");
CallServiceV2 callServiceV2 = callServiceProvider.getObject();
callServiceV2.internal();
}
public void internal() {
log.info("call internal");
}
}
2). 프록시 기술과 한계
- 인터페이스가 없으면 JDK 동적 프록시가 불가능하다
- 구체 클래스는 프록시를 못하기 때문에 캐스팅이 안된다
- CGLIB는 인터페이스, 구체클래스 둘다 사용가능하므로 스프링은 기본적으로 CGLIB를 사용한다
- 대상 클래스에 기본 생성자가 필수다
- 자식 클래스는 생성자 첫줄에 부모 클래스 기본 생성자를 호출이 자동이다