애플리케이션 전반에 걸쳐 적용되는 공통 기능을 비즈니스 로직으로부터 분리해내는 것을 AOP(Aspect Oriented Programming, 관심 지향 프로그래밍)라고 합니다.
스프링 어플리케이션은 특별한 경우를 제외하고는 MVC 웹 어플리케이션으로 만들어진다. 그리고 크게 세가지 Layer로 정의한다.
aop는 메소드들 또는 특정구역에 반복되는 로직들을 한 곳에 몰아서 코딩을 할 수 있게 해준다.
어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.
예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 횡단관심(흩어진 관심사, Crosscutting Concerns)라 부른다.
출처: https://engkimbs.tistory.com/entry/스프링AOP [새로비:티스토리]
public vodi Aclass() {
System.out.println("start");
//TODO : 각 메소드의 로직
System.out.println("stop");
}
public vodi Bclass() {
System.out.println("start");
//TODO : 각 메소드의 로직
System.out.println("stop");
}
위와 같이 서로 다른 메소드들의 실행시작과 끝을 알아야 하는경우 각 메소드마다 넣어주면 하나라도 변경 될 시 일일히 다 수정해야하는 문제를 받아들여야할 것이다. 이렇게 반복되는 코드들을 횡단관심이라 할 수 있겠다.
AOP를 사용하기 위해서는 Gradle의 경우 다음과 같은 의존성을 추가해야한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
주요 Annotation
@Aspect사용시 Bean등록을 해줘야한다.
@Aspect // aop로 동작하기 위해서
@Component // 스프링에서 관리하기위해
public class ParameterAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
// controller하위의 모든 메소드에 적용
private void cut(){}
@Before("cut()") // Pointcut이 실행될떄 (cut())
public void before(JoinPoint joinPoint){
MethodSignature methodSignature =
(MethodSignature) joinPoint.getSignature();
System.out.println(methodSignature.getName());
Object[] args = joinPoint.getArgs();
for(Object obj : args) {
System.out.println("type : " + obj.getClass().getSimpleName());
System.out.println("value : " + obj);
}
}
@AfterReturning(value = "cut()", returning = "returnObj")
// 들어가는 지점에 대한 정보가 있는 JoinPoint 객체
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
System.out.println("return obj");
System.out.println(returnObj);
}
}
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
return id + " " + name;
}
@PostMapping("/post")
public User post(@RequestBody User user) {
return user;
}
}
실행결과
Custom Annotation
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
@Aspect
@Component
public class TimerAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
private void cut(){}
@Pointcut("@annotation(com.example.aop.annotation.Timer)")
// Timer annotation이 설정된 메소드만
public void enableTimer(){}
@Around("cut() && enableTimer()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = joinPoint.proceed();
//proceed를 호출하면 실질적인 메소드가 실행된다.
//특정한 값이나 객체가 return 되면 Object에 들어간다.
stopWatch.stop();
System.out.println("total time : " + stopWatch.getTotalTimeSeconds());
}
}
cut()은 controller 패키지 하위의 모든 메소드들이 실행되면,
enableTimer()는 @Timer annotation가 실행되면 작업을 수행한다.
cut() 메소드와 enableTimer() 메소드가 실행되면 around()메소드가 실행된다.
@Around의 경우 ProceedingJoinPoint를 인자로 받아 타겟 메서드를 실행하는 proceed 코드를 반드시 적어야 target 메서드를 호출하지만 나머지 4개의 애노테이션의 경우, 타겟 메서드를 호출하는 proceed를 명시하지 않아도 알아서 호출됩니다.
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@Around와 달리 proceed 코드가 없이 정의한 로직이 수행된 후 자동으로 target 메서드를 호출합니다.
@AfterReturning(value = "execution(* com.example.mvc.order..*(..))", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
다른 애노테이션과 다르게 속성값으로 returning이 추가되었습니다.
이 부분에는 Target 메서드가 반환하는 변수명을 적어주고, advice 메서드의 인자로 변수명을 일치시켜준다면 해당 값을 가져와서 사용할 수 있습니다.
여기서 주의할점은 returning 값을 받는 인자의 타입이 해당 리턴 값의 부모타입 혹은 같은 타입이어야만 해당 Advice가 동작합니다.
타입이 부모 혹은 동일 타입이 아니라면 해당 Advice 자체가 동작하지 않으니 주의해야 합니다.
@AfterThrowing(value = "execution(* com.example.mvc.order..*(..))", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
@AfterReturning과 비슷하게 throwing 속성이 추가되고 advice 메서드 인자에 변수명을 일치시키면 받아서 사용할 수 있습니다.
@Around
@Around는 타겟 메서드의 실행이 종료되면 무조건 실행됩니다.(try catch의 finally문과 같습니다.)
동일한 @Aspect 안에서는 위와 같은 우선순위로 동작합니다.
즉, 동일한 @Aspect 안에서 여러 개의 Advice가 존재하는데 타겟 메서드가 여러 Advice의 대상이 될 경우 다음과 같이 동작합니다.
Around -> Before -> AfterThrowing -> AfterReturning -> After -> Around
https://velog.io/@backtony/Spring-AOP-%EC%B4%9D%EC%A0%95%EB%A6%AC