AOP로 로그찍기 - woowa-turkey/miniprojects-2019 GitHub Wiki
아래 글은 제가 AOP 공부하면서 노션에 정리한 날 것 그대로입니다.
참고 : https://www.eclipse.org/aspectj/
참고 3: https://jojoldu.tistory.com/71
@AspectJ 스타일은 어노테이션으로 aspect
를 표현하는 자바 클래스를 의미한다.
Spring은 AspectJ 5를 기준으로 사용하고 AOP를 사용한다.
Spring boot에서 사용하려면 org.springframework.boot:spring-boot-starter-aop
를 의존성으로 추가해야한다. 의존성으로 추가하면 자동으로 @EnableAspectJAutoProxy
가 추가된다.
Spring에서 @AspectJ
를 지원하기 위해서는 설정이 필요하다.
@Configuration
빈 클래스에 @EnableAspectJAutoProxy
어노테이션을 매핑해주어야한다.
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// ...
}
@AspectJ
를 사용하도록 설정하면 @AspectJ 어노테이션이 매핑된 클래스는 Appication Context 내부에 정의된 모든 빈들에 대해 적용할 수 있다.
import org.aspectj.lang.annotation.Aspect;
@Aspect
public class NotVeryUsefulAspect {
// ...
}
위와 같이 Aspect
를 처리할 클래스를 @Aspect
어노테이션을 붙여 정의할 수 있다. Aspect는 다른 일반 클래스처럼 메서드와 필드를 가질 수 있다. 또한 pointcut
, advice
, introduction
을 가질 수 있다.
@Aspect
로는 자동으로 Spring 컨테이너가 찾아내기엔 부족하다. 이를 보충하기 위해서 @Component
를 추가해주거나 @Aspect
와 함께 @Component
를 사용한 커스텀 어노테이션을 만들어 사용할 수 있다.
Spring AOP에서는 다른 Aspect에서 Aspect의 타겟이 될 수 없다. 이는 @Aspect
어노테이션은 Auto-proxying
의 대상에서 제외하기 때문이다.
pointcut
은 일반 메서드로 정의할 수 있다. 메서드를 정의함과 동시에 @Pointcut
어노테이션으로 preticate
를 정의할 수 있다. 이때 pointcut
의 리턴 타입은 반드시 void여야한다.
@PointCut("excution(* transfer(..))")
private void anyOldTransfer {}
위와 같이 pointcut
을 정의할 수 있다. 위 pointcut
은 모든 접근자이면서 어떤 파라미터를 가지는 transfer 메서드에 대해 aspect
를 적용한다는 의미이다.
- execution
접근제한자, 리턴타입, 인자타입, 클래스와 인터페이스, 메서드명, 파라미터 타입, 예외타입 등을 전부 조합가능한 지정자.
메서드 시그니처대로 {접근제한자} {메서드 명} (파라미터 타입)
과 같은 형태로 @Pointcut
을 정의할 수 있다.
@Pointcut("execution(* edu.pkch.mvcedu.controller.*.* (..))")
위와 같이 pointcut
을 설정하면 edu.pkch.mvc.edu.controller
패키지에 있는 모든 클래스의 메서드에 해당 pointcut
이 설정된 Advice를 적용한다는 의미이다.
@Around("controllerPointcut()")
private Object requestAndResponseLogging(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("request to {} with {}", joinPoint.getSignature(), joinPoint.getArgs());
joinPoint.proceed();
log.info("response end");
return null;
}
위와 같이 Advice
어노테이션에 pointcut을 정의한 메서드 명()
으로 적용할 수 있다.
- within
특정 타입으로 한정하는 pointcut. 즉, 클래스와 인터페이스까지 지정하는 경우를 의미
@Pointcut("within(edu.pkch.mvcedu.controller.*)")
@Pointcut("within(edu.pkch.mvcedu.controller..)")
전자 pointcut은 controller 패키지 아래의 클래스와 인터페이스가 가진 모든 메서드를 매칭하는 반면 후자는 controller 아래의 모든 하위 패키지까지 범위를 확대한다.
- @within
주어진 어노테이션을 사용하는 타입으로 선언된 메서드를 지칭
- this
bean reference가 this에서 주어진 인스턴스인 join point
를 매칭
- target
target object가 this에서 주어진 인스턴스인 join point
를 매칭
여기서 this와 target의 차이는 this는 정의한 타입이 자기 자신일때의 의미이고 target은 정의한 타입을 부르는 object의 타입이 일치하는 경우를 의미한다.
this vs target 차이 : https://stackoverflow.com/questions/11924685/spring-aop-target-vs-this
- @target
실행한 object의 클래스가 어노테이션에서 정의한 타입과 일치한 메서드를 지칭
- args
인자가 주어진 타입의 인스턴스인 join point
를 지칭
@Around("loggingForException() && args(exception)")
public Object exceptionLogging(final ProceedingJoinPoint pjp, Exception exception) throws Throwable {
setLogger(exception.getClass());
Object result = pjp.proceed();
log.error("errorMessage: {}", exception.getMessage());
return result;
}
위와 같이 args 표현식을 사용하여 해당 이름의 인자를 파라미터로 받을 수 있다.
- @args
실제 런타임에서 넘겨진 인자가 주어준 타입의 어노테이션을 가지는 join point
를 지칭
- @annotation
주어진 어노테이션을 가지는 join point
를 지칭
위 지정자 중
execution
과@annotation
을 가장 많이 씀
위 지정자 이외에도 AspectJ에서 지원하는 지정자가 많지만 Spring에서 지원하지 않는다. Spring에서는 메서드를 대상으로만 AOP를 지원하기 때문이다. 다만 Spring AOP에서만 지원하는 pointcut도 있다.
- bean
지정한 빈 이름과 일치하는 join point
를 지칭
위와 같이 지정한 pointcut
은 &&
(AND), ||
(OR), !
(NOT) 과 같은 논리연산자와 조합하여 지정할 수 있다. 또한 pointcut
을 조합한 또다른 pointcut
을 지정할 수 있다.
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.xyz.someapp.trading..*)")
private void inTrading() {}
@Pointcut("anyPublicOperation() && inTrading()")
private void tradingOperation() {}
아래는 다양한 pointcut
과 해당 pointcut
의 적용을 받는 joinpoint
대상이다.
advice는 pointcut 표현식에 일치하는 join point
에 적용하는 로직이다. 즉, AOP에서 관리하는 공통 관심사를 의미한다. Advice에는 Before
, AfterReturning
, After
, AfterThrowing
, Around
가 있다.
- @Before
일치하는 join point
의 실행 전에 적용하는 Advice이다.
@Before("execution(* edu.pkch.mvcedu.controller.*.* (..))")
다음과 같이 pointcut
을 설정하여 정의할 수 있다. 이때 미리 정의한 pointcut
메서드가 있다면 그 메서드로 설정할 수도 있다.
-
@AfterReturning
메서드가
return
을 실행한 직후에 적용하는 Advice이다.
@AfterReturning("execution(* edu.pkch.mvcedu.controller.*.* (..))")
AfterReturning
의 경우는 메서드에서 리턴값이 나온 이후이므로 Advice에서 리턴값을 받아 추가적인 작업을 할 수 있다. @AfterReturning
의 attribute 중 returning을 설정하여 파라미터로 받아주면 된다.
@AfterReturning(
pointcut = "execution(* edu.pkch.mvcedu.service.UserService.create(..))",
returning = "user"
)
public void doCreateCheck(Object user) {
// ...
}
위와 같이 returning에 파라미터로 리턴값을 받을 변수명을 설정하여 받을 수 있다. 리턴값을 Object로 받고 있는데 pointcut에서 리턴 값을 *
로 어떤 리턴값이라도 받을 수 있도록 명시하였기 때문에 Object로 받는다.
- @AfterThrowing
pointcut
으로 설정한 join point
가 exception에 의해 종료하는 경우 적용하는 Advice이다. 이 경우도 throw된 Exception 클래스를 인자로 전달받아 사용할 수 있다. @AfterThrowing
어노테이션의 atturibute 중 throwing에 설정하여 파라미터로 받아주면 된다.
@AfterThrowing(
pointcut = "execution(* edu.pkch.mvcedu.service.UserService.create(..))",
throwing = "ex"
)
public void unauthorizedException(UnAuthorizedException ex) {
// ...
}
- @After
pointcut
으로 설정한 join point
의 실행이 모두 종료된 후에 적용하는 Advice이다. 주로 사용한 resource를 닫아주거나 이와 비슷한 목적으로 사용한다.
@After("execution(* edu.pkch.mvcedu.service.UserService.create(..))")
public void doReleaseLock() {
// ...
}
- @Around
사실 위 모든 어노테이션에서 할 수 있는 작업을 @Around
에서 할 수 있다. 이때 주의할 점은 반드시 첫번째 파라미터에는 ProceedingJoinPoint
타입이 있어야한다.
@Around("controllerPointcut()")
private Object requestAndResponseLogging(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("request to {} with {}", joinPoint.getSignature(), joinPoint.getArgs());
ResponseEntity<String> result = null;
try {
result = (ResponseEntity<String>) joinPoint.proceed();
log.info("response code {}", result.getStatusCode());
log.info("result {}", result);
} catch (Exception ex) {
ex.printStackTrace();
}
return result;
}
위와 같이 AOP를 사용하여 로그를 찍는다고 가정한다. 이때 joinpoint의 proceed()
메서드가 기준이 된다. proceed
는 대상이 되는 join point를 실행하는 메서드이다. proceed
이전의 로직은 @Before
와 동일하며 이후는 @After
와 동일하다.
또한 proceed
메서드로 리턴값을 받을 수 있으므로 이는 @AfterReturning
과 같다. 그리고 catch문 내부에서 로직에서 발생하는 Exception에 접근할 수 있으므로 @AfterThrowing
을 대체할 수 있다.
Around 어드바이스를 사용할 때 반드시 최종 결과를 리턴해주어야한다. AOP가 기존 요청의 결과를 하이재킹하는 형식을 사용하므로 최종 결과를 리턴하지 않는다면 최종 응답이 아무것도 없게된다.
참고 : https://www.mkyong.com/spring/spring-aop-examples-advice/
참고 : https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#aop-introductions
AOP Introduction이란 대상이 되는 클래스 / 인터페이스에 설정한 인터페이스의 구현체를 구현한다고 선언하는 것이다. Introduction은 @DeclareParents
로 구현할 수 있다.
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("com.xyz.myapp.SystemArchitecture.businessService() && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}
구현체의 인터페이스는 @DeclareParents
가 붙어있는 필드의 타입에 따라 결정된다. 이때 중요한 것은 위 @Before
Advice에서 Introduction으로 선언한 타입 UsageTracked
를 사용한다는 점이다.
추측 : Introduction이라는 것은 위와 같이 실제 코드에서는 선언한 필드를 사용하는 것이 아닌 AOP에서 사용한다는 의미라고 이해함
@Aspect
@Component
public class LoggingAspect {
private static Logger log;
@Pointcut("execution(* com.wootecobook.turkey.*.controller..*Controller.*(..))")
public void loggingForController() {
} // 1
@Pointcut("execution(* com.wootecobook.turkey..*Advice.*handleException(..))")
public void loggingForException() {
} // 2
@Around("loggingForController()")
public Object controllerLogging(final ProceedingJoinPoint pjp) throws Throwable {
setLogger(pjp.getSignature().getDeclaringType());
log.info("request by {}, args: {} ", pjp.getSignature(), pjp.getArgs());
Object requestResult = pjp.proceed();
log.info("response {}", requestResult);
return requestResult;
} // 3
@Around("loggingForException() && args(exception)")
public Object exceptionLogging(final ProceedingJoinPoint pjp, Exception exception) throws Throwable {
setLogger(exception.getClass());
Object result = pjp.proceed();
log.error("errorMessage: {}", exception.getMessage());
return result;
} // 4
private void setLogger(Class<?> clazz) {
log = LoggerFactory.getLogger(clazz);
}
}
아래 번호는 위 주석에 달린 번호에 대한 부연설명입니다.
execution(* com.wootecobook.turkey.*.controller..*Controller.*(..))
execution AOP 표현식으로 com.wootecobook.turkey 패키지 아래의 (한 뎁스 아래의 패키지 모두) 모든 패키지 에서 controller 패키지의 하위 모든 패키지에 있는 Controller
로 끝나는 클래스의 모든 메서드를 대상으로 지정하겠다는 의미이다. 이때 (..)
로 파라미터의 갯수, 종류는 상관없음을 표시하였다.
※ 참고
- aop 표현식에서 .과 ..의 차이 (패키지 표현에서)
.은 한 뎁스 아래의 패키지 만을 가리킨다.
반면 ..은 한 뎁스 아래 뿐만 아니라 그 하위 패키지 모두를 가리킨다. 즉, 해당 패키지 하위에 있는 모든 패키지를 의미한다.
execution(* com.wootecobook.turkey..*Advice.*handleException(..))
위와 비슷한 방식으로 이해
@Around("loggingForController()")
Around 방식으로 loggingForController pointcut을 사용한다는 의미이다.
public Object controllerLogging(final ProceedingJoinPoint pjp) throws Throwable {
around 방식은 반드시 ProceedingJoinPoint를 첫번째 파라미터로 받아야만한다.
setLogger(pjp.getSignature().getDeclaringType());
log.info("request by {}, args: {} ", pjp.getSignature(), pjp.getArgs());
Object requestResult = pjp.proceed();
log.info("response {}", requestResult);
setLogger는 해당 컨트롤러의 class를 Logger에서 표시하기 위해 사용한다.
ProceedingJoinPoint
에는 호출되는 메서드에 대한 시그니처 정보, 인자, 불리는 메서드의 소스코드 위치 등의 정보를 가지고 있다.
Around 방식에서 Aspect의 대상이 되는 메서드의 호출은 ProceedingJoinPoint
의 proceed
메서드 호출이 기준이된다.
proceed
메서드는 해당 메서드의 리턴값을 Object로 반환한다.
위 예시에서 요청과 응답에 대한 로그를 작성하기 위해 proceed 전후로 log.info
로 로그를 작성하고 있다.
return requestResult;
마지막으로 ProceedingJoinPoint.proceed
메서드의 결과를 반환해야 해당 메서드의 결과가 제대로 반환된다.
위 3번 예시와 대부분 비슷하다. 다만 args
를 사용한 점이 다르다.
@Around("loggingForException() && args(exception)")
마찬가지로 Around 방식을 사용했다. 2번의 loggingForException
포인트컷을 사용하면서 하나의 조건을 더 주었다. args로 exception
이라는 인자를 가지는 메서드를 대상으로 지정하였다.
public Object exceptionLogging(final ProceedingJoinPoint pjp, Exception exception) throws Throwable {
이렇게 args를 사용하면 인자로 해당 exception 인자를 Aspect에서도 사용할 수 있다.