[강의] 김영한님의 스프링 핵심 원리 - 고급편 (스프링 AOP)

크리링·2023년 5월 14일
0
post-thumbnail

강의 : 스프링 핵심 원리 - 고급편
코드 : 스프링 고급편 - AOP 코드

AOP

소개

핵심 기능과 부가 기능

애플리케이션 = 핵심 기능 + 부가 기능



부가 기능

  • 중복 코드
  • 변경시 많은 수정
  • 적용 대상 변경시 많은 수정

비효율적






Aspect

관점이라는 뜻
애플리케이션을 바라보는 관점을 하나하나의 기능에서 횡단 관심사로 보는 것
관점 지향 프로그래밍
OOP 대체가 아닌 보조 목적







적용 방식

  • 컴파일 시점

    • 컴파일 시점에 코드 추가
    • 특별한 컴파일러 필요 + 복잡
  • 클래스 로딩 시점

    • .class 파일을 JVM 클래스 로더에 보관
    • 번거롭고 운영이 어려움
  • 런타임 시점(프록시)

    • 자바의 메인 메서드가 실행된 다음 부가 기능 적용






용어 정리

Join Point

추상적 개념, AOP 적용할 수 있는 모든 지점

Pointcut

조인 포인트 중 어드바이스가 적용될 위치 선별 기능

Aspect

@Aspect
어드바이스 + 포인트 컷

Advice

부가 기능

Advisor

하나의 어드바이스와 포인트 컷 (스프링 AOP에서만 사용)

Weaving

포인트 컷으로 결정한 타겟의 조인 포인트에 어드바이스를 적용하는 것
핵심 기능 코드에 영향을 주지 않고 부가 기능 추가 가능






구현

V1 - AspectV1

@Slf4j
@Aspect
public class AspectV1 {
 	//hello.aop.order 패키지와 하위 패키지
 	@Around("execution(* hello.aop.order..*(..))")
 	public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
 		log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
 		return joinPoint.proceed();
 	}
}
  • "execution(* hello.aop.order..*(..))" : 포인트컷
  • doLog : 어드바이스


Test

@Slf4j
@Import(AspectV1.class) //추가
@SpringBootTest
public class AopTest {
 	@Autowired
 	OrderService orderService;
 	@Autowired
 	OrderRepository orderRepository;
    
 	@Test
 	void aopInfo() {
 		log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService));
	 	log.info("isAopProxy, orderRepository={}", AopUtils.isAopProxy(orderRepository));
 	}
    
 	@Test
 	void success() {
 		orderService.orderItem("itemA");
 	}
    
 	@Test
 	void exception() {
 		assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
 	}
}

@Aspect 는 애스펙트라는 표식이지 컴포넌트 스캔이 되는 것은 아니다. 따라서 AspectV1 를 AOP로 사용하려면 스프링 빈으로 등록해야 한다.



실행

[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행






V2 - 포인트컷 분리

@Slf4j
@Aspect
public class AspectV2 {
 	//hello.aop.order 패키지와 하위 패키지
 	@Pointcut("execution(* hello.aop.order..*(..))") //pointcut expression
 	private void allOrder(){} //pointcut signature
    
 	@Around("allOrder()")
 	public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
 		log.info("[log] {}", joinPoint.getSignature());
 		return joinPoint.proceed();
 	}
}

@Pointcut

  • 포인트컷 표현식 사용
  • 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처
  • 메서드의 반환 타입은 void
  • 코드 내용은 비워둔다.
  • 포인트컷 시그니처는 allOrder()
  • @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다. 여기서는 @Around("allOrder()") 를 사용한다.
  • private , public 같은 접근 제어자는 내부에서만 사용하면 private 을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public 을 사용해야 한다



AopTest

//@Import(AspectV1.class)
@Import(AspectV2.class)
@SpringBootTest
public class AopTest {
}



실행

[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행






V3 - 어드바이스 추가

트랜잭션 기능이 동작한 것 처럼 로그를 남긴다.

  • 핵심 로직 실행 직전에 트랜잭션을 시작
  • 핵심 로직 실행
  • 핵심 로직 실행에 문제가 없으면 커밋
  • 핵심 로직 실행에 예외가 발생하면 롤백
@Slf4j
@Aspect
public class AspectV3 {
 	//hello.aop.order 패키지와 하위 패키지
 	@Pointcut("execution(* hello.aop.order..*(..))")
 	public void allOrder(){}
    
 	//클래스 이름 패턴이 *Service
 	@Pointcut("execution(* *..*Service.*(..))")
 	private void allService(){}
    
 	@Around("allOrder()")
 	public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
 		log.info("[log] {}", joinPoint.getSignature());
 		return joinPoint.proceed();
 	}
    
 	//hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
 	@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("allOrder() && allService()") : 포인트컷 조합 가능

orderService : doLog() , doTransaction() 어드바이스 적용
orderRepository : doLog() 어드바이스 적용



AopTest

//@Import(AspectV1.class)
//@Import(AspectV2.class)
@Import(AspectV3.class)
@SpringBootTest
public class AopTest {
}



실행

[log] void hello.aop.order.OrderService.orderItem(String)
[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
[log] void hello.aop.order.OrderService.orderItem(String)
[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 롤백] void hello.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)

AOP 적용전

클라이언트 -> orderService.orderItem() -> orderRepository.save()

AOP 적용후
클라이언트 -> doLog() -> doTransaction() -> orderService.orderItem() -> doLog() -> orderRepository.save()






V4 - Pointcuts 참조

public class Pointcuts {
 	//hello.springaop.app 패키지와 하위 패키지
 	@Pointcut("execution(* hello.aop.order..*(..))")
 	public void allOrder(){}
    
 	//타입 패턴이 *Service
 	@Pointcut("execution(* *..*Service.*(..))")
 	public void allService(){}
    
 	//allOrder && allService
 	@Pointcut("allOrder() && allService()")
 	public void orderAndService(){}
}

AspectV4Pointcut

@Slf4j
@Aspect
public class AspectV4Pointcut {
 	@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());
 		}
 	}
}



AopTest

//@Import(AspectV1.class)
//@Import(AspectV2.class)
//@Import(AspectV3.class)
@Import(AspectV4Pointcut.class)
@SpringBootTest
public class AopTest {
}






V5 - 어드바이스 순서

어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다. 문제는 이것을 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다는 점이다. 그래서 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다. 따라서 애스펙트를 별도의 클래스로 분리해야 한다.

로그를 남기는 순서를 바꾸어서 doTransaction() -> doLog() 트랜잭션이 먼저 처리되고, 이후에 로그가 남도록 변경해보자.

AspectV5Order

@Slf4j
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());
 			}
 		}
 	}
}



AopTest

//@Import(AspectV4Pointcut.class)
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@SpringBootTest
public class AopTest {
}



실행

[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)






V6 - 어드바이스 종류

  • @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
  • @Before : 조인 포인트 실행 이전에 실행
  • @AfterReturning : 조인 포인트가 정상 완료후 실행
  • @AfterThrowing : 메서드가 예외를 던지는 경우 실행
  • @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

@Slf4j
@Aspect
public class AspectV6Advice {
 	@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
 	public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
 		try {
 			//@Before
 			log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
 			Object result = joinPoint.proceed();
 			//@AfterReturning
 			log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
 			return result;
 		} catch (Exception e) {
 			//@AfterThrowing
 			log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
 			throw e;
 		} finally {
 			//@After
 			log.info("[around][리소스 릴리즈] {}", 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("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
 	}
    
 	@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
 	public void doAfter(JoinPoint joinPoint) {
 		log.info("[after] {}", joinPoint.getSignature());
 	}
}



JoinPoint 인터페이스 주요 기능

  • getArgs() : 메서드 인수를 반환합니다.
  • getThis() : 프록시 객체를 반환합니다.
  • getTarget() : 대상 객체를 반환합니다.
  • getSignature() : 조언되는 메서드에 대한 설명을 반환합니다.
  • toString() : 조언되는 방법에 대한 유용한 설명을 인쇄합니다.

ProceedingJoinPoint 인터페이스의 주요 기능

  • proceed() : 다음 어드바이스나 타겟을 호출



어드바이스 종류

@Before : 조인 포인트 실행 전
@AfterReturning : 메서드 실행이 정상적으로 반환될 때 실행
@AfterThrowing : 메서드 실행이 예외를 던져서 종료될 때 실행
@After : 메서드 실행이 종료되면 실행 (finally와 유사)
@Around : 메서드의 실행의 주변에서 실행. 메서드 실행 전후에 작업 수행



AopTest

//@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@Import(AspectV6Advice.class)
@SpringBootTest
public class AopTest {
}



실행

[around][트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[before] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[orderRepository] 실행
[return] void hello.aop.order.OrderService.orderItem(String) return=null
[after] void hello.aop.order.OrderService.orderItem(String)
[around][트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[around][리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)



좋은 설계는 제약이 있는 것

@Around 만 있으면 되는데 왜? 이렇게 제약을 두는가? 제약은 실수를
미연에 방지한다. 일종의 가이드 역할을 한다. 만약 @Around 를 사용했는데, 중간에 다른 개발자가 해당 코드를 수정해서 호출하지 않았다면? 큰 장애가 발생했을 것이다. 처음부터 @Before 를 사용했다면 이런 문제 자체가 발생하지 않는다. 제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.



스프링 AOP - 포인트컷

포인트컷 지시자

AspectJ는 포인트컷을 편리하게 표현히가 위한 특별한 표현식을 제공한다.
예) @Pointcut("execution(* hello.aop.order..*(..))")


포인트컷 지시자 종류

  • execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다. (가장 많이 쓰임)
  • within : 특정 타입 내의 조인 포인트를 매칭한다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.



예제

ClassAop

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}



MethodAop

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
 	String value();
}



MemeberService

public interface MemberService {
 	String hello(String param);
}

MemberServiceImpl

@ClassAop
@Component
public class MemberServiceImpl implements MemberService {
 	@Override
 	@MethodAop("test value")
 	public String hello(String param) {
 		return "ok";
 	}
 	public String internal(String param) {
 		return "ok";
 	}
}



ExecutionTest

@Slf4j
public class ExecutionTest {

 	AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
 	Method helloMethod;
    
 	@BeforeEach
 	public void init() throws NoSuchMethodException {
 		helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
 	}
    
 	@Test
 	void printMethod() {
 		//public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
 		log.info("helloMethod={}", helloMethod);
 	}
}

AspectJExpressionPointcut 포인트컷 표현식을 처리해주는 클래스
AspectJExpressionPointcut는 상위에 Pointcut 인터페이스를 가진다.



실행 결과

helloMethod = public java.lang.String
hello.aop.member.MemberServiceImpl.hello(java.lang.String)



Execution1 - 문법

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
 	throws-pattern?)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
  • 메소드 실행 조인 포인트를 매칭한다.
  • ?는 생략할 수 있다.
  • * 같은 패턴을 지정할 수 있다.



pointcut.setExpression("execution(public String 
hello.aop.member.MemberServiceImpl.hello(String))");

매칭 조건

  • 접근제어자?: public
  • 반환타입: String
  • 선언타입?: hello.aop.member.MemberServiceImpl
  • 메서드이름: hello
  • 파라미터: (String)
  • 예외?: 생략



@target, @within

  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트

설명

@target, @within은 다음과 같이 타입에 있는 애노테이션으로 AOP 적용 여부를 판단한다.

  • @target(hello.aop.member.annotation.ClassAop)
  • @within(hello.aop.member.annotation.ClassAop)
@ClassAop
class Target{}

@target vs @within

  • @target은 인스턴스의 모든 메서드를 조인 포인트로 저용
  • @within은 해당 타입 내에 있는 메서드만 조인 포인트로 적용

@target은 부모 클래스의 메서드까지 어드바이스를 다 적용, @within은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용




AtTargetAtWithinTest

@Slf4j
@Import({AtTargetAtWithinTest.Config.class})
@SpringBootTest
public class AtTargetAtWithinTest {
 	@Autowired
 	Child child;
    
 	@Test
 	void success() {
 		log.info("child Proxy={}", child.getClass());
 		child.childMethod(); //부모, 자식 모두 있는 메서드
 		child.parentMethod(); //부모 클래스만 있는 메서드
 	}
    
 	static class Config {
 		@Bean
 		public Parent parent() {
 			return new Parent();
 		}
        
 		@Bean
 		public Child child() {
 			return new Child();
 		}
        
 		@Bean
 		public AtTargetAtWithinAspect atTargetAtWithinAspect() {
 			return new AtTargetAtWithinAspect();
 		}
 	}
    
 	static class Parent {
 		public void parentMethod(){} //부모에만 있는 메서드
 	}
 	@ClassAop
 	static class Child extends Parent {
 		public void childMethod(){}
 	}
    
 	@Slf4j
 	@Aspect
 	static class AtTargetAtWithinAspect {
 		//@target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용
 		@Around("execution(* hello.aop..*(..)) &&
		@target(hello.aop.member.annotation.ClassAop)")
 		public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
	 		log.info("[@target] {}", joinPoint.getSignature());
	 		return joinPoint.proceed();
	 	}
        
	 	//@within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용되지 않음
	 	@Around("execution(* hello.aop..*(..)) &&
		@within(hello.aop.member.annotation.ClassAop)")
		public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
	 		log.info("[@within] {}", joinPoint.getSignature());
	 		return joinPoint.proceed();
	 	}
	}
}



실행 결과

[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()

parentMethod()Parent 클래스에만 정의되어 있고, Child 클래스에 정의되어 있지 않기 때문에 @within 에서 AOP 적용 대상이 되지 않는다.
실행결과를 보면 child.parentMethod() 를 호출 했을 때 [@within] 이 호출되지 않은 것을 확인할 수 있다.



참고

@target , @within 지시자는 뒤에서 설명할 파라미터 바인딩에서 함께 사용된다.



@annotation

@annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭

@annotation(hello.aop.member.annotation.MethodAop)



예제 - AtAnnotationTest


...
	@Slf4j
	@Aspect
	static class AtAnnotationAspect {
		@Around("@annotation(hello.aop.member.annotation.MethodAop)")
		public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
			log.info("[@annotation] {}", joinPoint.getSignature());
			return joinPoint.proceed();
		}
 	}



@args

@args: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트

전달된 인수의 런타임 타입에 @Check 애노테이션이 있는 경우에 매칭한다.
@args(test.Check)



bean

bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 지정

  • 스프링 빈의 이름으로 AOP 적용 여부를 지정. 스프링에서만 사용할 수 있는 특별한 지시자
  • bean(orderService) || bean(*Repository)
  • * 과 같은 패턴을 사용할 수 있다.


예제 - BeanTest

...

	@Aspect
 	static class BeanAspect {
 		@Around("bean(orderService) || bean(*Repository)")
 		public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
 			log.info("[bean] {}", joinPoint.getSignature());
 			return joinPoint.proceed();
 		}
 	}

OrderService , *Repository(OrderRepository) 의 메서드에 AOP가 적용된다

실행 결과

[bean] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[bean] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행



매개변수 전달

포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있다.

this, target, args,@target, @within, @annotation, @args

@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
 	log.info("[logArgs3] arg={}", arg);
}



this, target

this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트

this, target은 다음과 같이 적용 타입 하나를 정확하게 지정해야 한다.

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
  • * 같은 패턴 사용 불가
  • 부모 타입 허용



this vs target

스프링에서 AOP를 적용하면 실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다.

  • this 는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭한다.
  • target 은 실제 target 객체를 대상으로 포인트컷을 매칭한다.


프록시 생성 방식에 따른 차이

  • JDK 동적 프록시: 인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체를 생성한다.
  • CGLIB: 인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성한다.




JDK 동적 프록시

MemberService 인터페이스 지정

  • this(hello.aop.member.MemberService) : proxy 객체를 보고 판단한다. this 는 부모 타입을 허용하기 때문에 AOP가 적용된다.
  • target(hello.aop.member.MemberService) : target 객체를 보고 판단한다. target 은 부모 타입을 허용하기 때문에 AOP가 적용된다.

MemberServiceImpl 구체 클래스 지정

  • this(hello.aop.member.MemberServiceImpl) : proxy 객체를 보고 판단한다. JDK 동적 프록시로 만들어진 proxy 객체는 MemberService 인터페이스를 기반으로 구현된 새로운 클래스다. 따라서 MemberServiceImpl 를 전혀 알지 못하므로 AOP 적용 대상이 아니다.
  • target(hello.aop.member.MemberServiceImpl) : target 객체를 보고 판단한다. target 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다.



CGLIB 프록시

업로드중..

MemberService 인터페이스 지정

  • this(hello.aop.member.MemberService) : proxy 객체를 보고 판단한다. this 는 부모 타입을 허용하기 때문에 AOP가 적용된다.
  • target(hello.aop.member.MemberService) : target 객체를 보고 판단한다. target 은 부모 타입을 허용하기 때문에 AOP가 적용된다.

MemberServiceImpl 구체 클래스 지정

  • this(hello.aop.member.MemberServiceImpl) : proxy 객체를 보고 판단한다. CGLIB로 만들어진 proxy 객체는 MemberServiceImpl 를 상속 받아서 만들었기 때문에 AOP가 적용된다. this 가 부모 타입을 허용하기 때문에 포인트컷의 대상이 된다.
  • target(hello.aop.member.MemberServiceImpl) : target 객체를 보고 판단한다. target 객체가 MemberServiceImpl 타입이므로 AOP 적용 대상이다



예제

/**
 * application.properties
 * spring.aop.proxy-target-class=true CGLIB
 * spring.aop.proxy-target-class=false JDK 동적 프록시
 */
@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
@SpringBootTest(properties = "spring.aop.proxy-target-class=false") //JDK 동적
프록시
//@SpringBootTest(properties = "spring.aop.proxy-target-class=true") //CGLIB
public class ThisTargetTest {

 	@Autowired
 	MemberService memberService;
    
 	@Test
 	void success() {
 		log.info("memberService Proxy={}", memberService.getClass());
 		memberService.hello("helloA");
 	}
    
 	@Slf4j
 	@Aspect
 	static class ThisTargetAspect {
 		//부모 타입 허용
 		@Around("this(hello.aop.member.MemberService)")
 		public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
 			log.info("[this-interface] {}", joinPoint.getSignature());
 			return joinPoint.proceed();
 		}
        
 		//부모 타입 허용
 		@Around("target(hello.aop.member.MemberService)")
 		public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
 			log.info("[target-interface] {}", joinPoint.getSignature());
 			return joinPoint.proceed();
 		}
        
 		//this: 스프링 AOP 프록시 객체 대상
 		//JDK 동적 프록시는 인터페이스를 기반으로 생성되므로 구현 클래스를 알 수 없음
 		//CGLIB 프록시는 구현 클래스를 기반으로 생성되므로 구현 클래스를 알 수 있음
 		@Around("this(hello.aop.member.MemberServiceImpl)")
 		public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
 			log.info("[this-impl] {}", joinPoint.getSignature());
 			return joinPoint.proceed();
 		}
        
 		//target: 실제 target 객체 대상
 		@Around("target(hello.aop.member.MemberServiceImpl)")
 		public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
 			log.info("[target-impl] {}", joinPoint.getSignature());
 			return joinPoint.proceed();
 		}
 	}
}

spring.aop.proxy-target-class=false
JDK 동적 프록시 사용

memberService Proxy=class com.sun.proxy.$Proxy53
[target-impl] String hello.aop.member.MemberService.hello(String)
[target-interface] String hello.aop.member.MemberService.hello(String)
[this-interface] String hello.aop.member.MemberService.hello(String)

spring.aop.proxy-target-class=true, 또는 생략(스프링 부트 기본 옵션)
CGLIB 사용

memberService Proxy=class hello.aop.member.MemberServiceImpl$
$EnhancerBySpringCGLIB$$7df96bd3
[target-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[target-interface] String hello.aop.member.MemberServiceImpl.hello(String)
[this-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[this-interface] String hello.aop.member.MemberServiceImpl.hello(String)

0개의 댓글