22.10.20
스프링 AOP 구현4 - 포인트컷 참조
포인트컷을 공용으로 사용하기 위해 외부 클래스에 모아두기
외부에서 호출하기 위해 포인트컷의 접근 제어자는 public 으로 해두어야 한다.
Pointcuts 클래스
public class Pointcuts {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){} //pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService(){}
//allOrder() && allService()
@Pointcut("allOrder() && allService()")
public void orderAndService(){}
}
orderAndService() : allOrder() 포인트컷와 allService() 포인트컷을 조합해서 새로운 포인트컷을 만들었다.
접근 제어자 -> public
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()); //join point 시그니처
return joinPoint.proceed();
}
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
//hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
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 값에 모두 지정
포인트컷을 여러 어드바이스에서 함께 사용할 때 이 방법을 사용하면 효과적이다.
스프링 AOP 구현5 - 어드바이스 순서
어드바이스는 기본적으로 순서를 보장하지 않는다.
순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.@Order 애노테이션을 적용해야 한다. 문제는 이것을 어드바이스(메서드) 단위가 아니라 클래스 단위로 적용할 수 있다는 점이다. 그래서 지금처럼 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다. 따라서 애스펙트를 별도의 클래스로 분리해야 한다.
로그를 doTransaction() -> doLog() 순으로 남겨보자.
@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()); //join point 시그니처
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
//hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
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());
}
}
}
}
하나의 애스펙트 안에 있던 어드바이스를 LogAspect , TxAspect 애스펙트로 각각 분리했다.
그리고 각 애스펙트에 @Order 애노테이션을 통해 실행 순서를 적용했다.
@Order: @Aspect + 클래스 단위에 적용한다. 숫자가 작을 수록 먼저 실행된다.

호출: doTx() -> doLog()
반환: doLog() -> doTx()
Aspect 스프링 빈 등록
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
스프링 AOP 구현6 - 어드바이스 종류
- @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스
- @Before : 조인 포인트 실행 이전에 실행
- @AfterReturning : 조인 포인트가 정상 완료후 실행
- @AfterThrowing : 메서드가 예외를 던지는 경우 실행
- @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
다른 어드바이스는 @Around 의 부분적 기능을 수행한다. 즉, @Around 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있다.
AspectV6Advice 클래스
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
try{
//@Before
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch(Exception e){
//@AfterThrowing
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally{
//@After
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] {} result={}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex){
log.info("[ex] {} message={}", ex, ex.getMessage());
}
@After("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint){
log.info("[after] {}", joinPoint.getSignature());
}
}
참고 정보 획득
모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다. (생략 가능)
단, @Around 는 ProceedingJoinPoint 을 사용해야 한다.
JoinPoint 와 ProceedingJoinPoint
JoinPoint 인터페이스의 주요 기능
- getArgs() : 메서드 인수를 반환
- getThis() : 프록시 객체를 반환
- getTarget() : 대상 객체를 반환
- getSignature() : 조언되는 메서드에 대한 설명을 반환
- toString() : 조언되는 방법에 대한 유용한 설명을 인쇄
ProceedingJoinPoint
org.aspectj.lang.JoinPoint 의 하위 타입. 주요 기능으로 다음 어드바이스나 타켓을 호출하는 proceed() 가 있다.
어드바이스 종류
@Before
조인 포인트 실행 전
- @Around 와 다르게 작업 흐름을 변경할 수는 없다.
- @Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음 대상이 호출되지 않는다.
- 반면에 @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출된다. 물론 예외가 발생하면 다음 코드가 호출되지는 않는다.
@AfterReturning
메서드 실행이 정상적으로 반환될 때 실행
- returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
- 타겟 메서드 반환 타입이 returning 절에 지정된 타입과 다를 경우 @AfterReturning 어드바이스 자체가 실행되지 않는다. (부모 타입을 지정하면 모든 자식 타입은 인정된다. void 도 Object는 받을 수 있다.)
- @Around 와 다르게 반환되는 객체를 변경할 수는 없다. 반환 객체를 변경하려면 @Around 를 사용해야 한다. 참고로 반환 객체를 조작할 수 는 있다. (ex. 객체의 setter를 통해)
@AfterThrowing
메서드 실행이 예외를 던져서 종료될 때 실행
- throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
- @AfterReturning 처럼 throwing 절에 지정된 타입과 맞은 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)
@After
메서드 실행이 종료될 때 실행 (finally)
- 정상 및 예외 반환 조건을 모두 처리한다.
- 일반적으로 리소스를 해제하는 데 사용한다.
@Around
메서드 실행의 주변에서 실행 (메서드 실행 전후)
- 조인 포인트 실행 여부 선택 : joinPoint.proceed() 호출 여부 선택
- 전달 값 변환: joinPoint.proceed(args[])
- 반환 값 변환
- 예외 변환
- 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
- 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint 를 사용해야 한다. (규칙)
- proceed() 를 여러번 실행할 수도 있음 (재시도)

실행 순서
스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
- @Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing
- 어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대이다.
- 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다. 이 경우 @Aspect 를 분리하고 @Order 를 적용하자.
@Around 외에 다른 어드바이스가 존재하는 이유
@Before 는 joinPoint.proceed() 를 호출하는 고민을 하지 않아도 된다.
@Around 가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다.
반면에 @Before , @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다.
그리고 코드를 작성한 의도가 명확하게 드러난다. @Before 애노테이션을 보는 순간 이 코드는 타켓 실행 전에 한정해서 어떤 일을 하는 코드구나 라는 것을 알 수 있다.
좋은 설계의 조건: 제약
제약은 실수를 미연에 방지한다. 일종에 가이드 역할을 한다. 제약 덕분에 역할이 명확해진다. 다른 개발자도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도도 파악하기 쉽다.
11. 스프링 AOP - 포인트컷
포인트컷 지시자
AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공한다. -> AspectJ pointcut expression
예) @Pointcut("execution(* hello.aop.order..*(..))")
포인트컷 지시자
포인트컷 표현식은 execution 같은 포인트컷 지시자(Pointcut Designator)로 시작한다. 줄여서 PCD라 한다.
- execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.
- within : 특정 타입 내의 조인 포인트를 매칭한다.
- args : 인자가 주어진 타입의 인스턴스인 조인 포인트
- this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
- target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
- @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
- @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
- @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
- @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
- bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
execution 은 가장 많이 사용하고, 나머지는 자주 사용하지 않는다. 따라서 execution 을 중점적으로 이해하자.
예제 만들기
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
ClassAop: 클래스 단위 애노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
String value();
}
MethodAop: 메서드 단위 애노테이션
public interface MemberService {
String hello(String param);
}
@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";
}
}
예제 코드 테스트
@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)
//execution 으로 시작하는 포인트컷 표현식은 이 메서드 정보를 매칭해서 포인트컷 대상을 찾아낸다.
log.info("helloMethod={}", helloMethod);
}
}
- getMethod( 메서드 이름, 파라미터 타입 ) 을 통해 메서드 메타 정보를 받아온다. execution 으로 시작하는 포인트컷 표현식은 이 메서드 정보를 매칭해서 포인트컷 대상을 탐색한다.
- AspectJExpressionPointcut : 포인트컷 표현식을 처리해주는 클래스. 여기에 포인트컷 표현식을 지정한다. AspectJExpressionPointcut 는 상위에 Pointcut 인터페이스를 가진다.
- printMethod() 테스트는 MemberServiceImpl.hello(String) 메서드의 정보를 출력해준다.