AOP
이 포스트는 김영한님의 ‘스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술’을 수강하고 작성하였습니다.
AOP
- [Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP
출처: https://engkimbs.tistory.com/entry/스프링AOP 새로비:티스토리- AOP(Aspect-Oriented Programming)의 이해와 스프링에서의 적용
출처: https://f-lab.kr/insight/understanding-aop-in-spring
AOP는 Aspect Oriented Programming의 약자로, 관점 지향 프로그래밍을 말한다.
어떤 로직을 기준으로 핵심 관심 사항(core concern)과 공통 관심 사항(cross-cutting concern)을 나누어 공통 관심 사항을 모듈화하는 것이 핵심이다.
AOP를 언제 쓰는가?
회원 관리 예제를 진행하는 입장에서 한 가지 가정을 해보자. 만약 내가 지금까지 만들었던 컨트롤러, 서비스, 리포지토리 함수의 실행 시간을 측정해야 한다고 했을 때, 나는 각각의 함수마다 시작 시간과 종료 시간을 구해서 그 차이를 계산할 수 있다.
그런데 그 작업을 일일이 모든 함수에 적용해야 한다면 생각만 해도 머리 아프고 유지보수성 떨어지는 게 안 봐도 뻔하다.
이때 컨트롤러, 서비스, 리포지토리에서 하는 일(= 비즈니스 로직)은 핵심 관심 사항이고, 시간을 측정하는 일은 공통 관심 사항이다. 때문에 시간을 측정하는 함수를 구현할 때 AOP 방식을 사용할 것이다.
AOP 예시
수동 Bean 등록
간단하게 컴포넌트 스캔을 이용하는 방법도 있지만, 수동으로 Bean에 등록하면 명시적으로 내가 어떤 AOP를 사용하고 있는지를 볼 수 있기 때문에 이 방법을 권장한다고 한다.
따라서 나는 이 방법으로 예제를 구현했다.
MyTimeTraceAOP
생성
package hello.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect // 공통 관심 사항을 모듈화한 것임을 의미
public class MyTimeTraceAOP {
// @Around("execution()")로 hello.hellospring 패키지 내 SpringConfig을 제외한 모든 곳에서 실행
@Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.SpringConfig)")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try {
// 원래 실행하려는 함수(= 핵심 관심 사항, 비즈니스 로직) 실행
return joinPoint.proceed();
} finally {
long end = System.currentTimeMillis();
long ms = end - start;
System.out.println("END: " + joinPoint.toString() + " => " + ms + "ms");
}
}
}
SpringConfig
에 AOP 등록
package hello.hellospring;
// import 생략
@Configuration
public class SpringConfig {
// 생략
@Bean
public MyTimeTraceAOP myTimeTraceAOP() {
return new MyTimeTraceAOP();
}
}
컴포넌트 스캔 사용
MyTimeTraceAOP
생성
package hello.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Aspect
@Component
public class MyTimeTraceAOP {
@Around("execution(* hello.hellospring..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try {
return joinPoint.proceed();
} finally {
long end = System.currentTimeMillis();
long ms = end - start;
System.out.println("END: " + joinPoint.toString() + " => " + ms + "ms");
}
}
}
AOP 동작 방식
예시로 서비스의 한 함수에 MyTimeTraceAOP
가 적용된다고 가정해보면 다음과 같은 순서로 동작한다.
helloController
에서helloService
를 호출한다.- 스프링 컨테이너에 등록된 실제 스프링
helloService
대신, 프록시helloService
를 호출한다. - 프록시
helloService
내에서joinPoint.proceed()
를 실행하면 그때 스프링helloService
를 실행한다.
여담
TimeTraceAOP
였던 것
MyTimeTraceAOP
클래스를 만들 때 처음에는 TimeTraceAOP
로 했다가 Bean을 만들 때 이미 같은 이름이 있다고 빌드를 실패했다…
순환 참조 발생
참고: AOP(TimeTraceAop)를 @Component 로 선언 vs SpringConfig에 @Bean으로 등록
수동으로 Bean에 등록했을 때의 MyTimeTraceAOP
와 컴포넌트 스캔을 이용한 MyTimeTraceAOP
의 @Around
내 execution
표현식을 보면 차이가 있다.
- 수동으로 Bean 등록 시
MyTimeTraceAOP
@Around("execution(* hello.hellospring..*(..)) && !target(hello.hellospring.SpringConfig)")
- 컴포넌트 스캔
MyTimeTraceAOP
@Around("execution(* hello.hellospring..*(..))")
수동으로 Bean을 등록했을 때 execution 범위에서 SpringConfig
을 제외하지 않으면, SpringConfig
내 myTimeTraceAOP()
를 호출하여 스프링 컨테이너에 빈을 등록할 때 순환 참조가 발생한다.
그러나 컴포넌트 스캔 방식을 이용하면 SpringConfig
내 myTimeTraceAOP()
함수 호출이 필요가 없어 해당 부분이 없기 때문에 순환 참조가 발생하지 않는다.
댓글남기기