아래의 코드를 보면 AOP가 무엇을 나타내는 지 쉽게 이해할 수 있다.
소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는데 이것을 흩어진 관심사(Crosscutting Concerns)라 부른다.
이런 코드는 AAAA - > aaa로 고치거나, BBBB -> BB로 고쳐야하는 작업이 생긴다면 흩어진 관심사들을 모두 찾아서 일일이 하나하나 고쳐줘야하기 때문에 번거로운 작업이 될 수 있다.
class A {
method a() {
AAAA
오늘은 7월 4일 미국 독립 기념일이래요.
BBBB
}
method b() {
AAAA
저는 아침에 운동을 다녀와서 밥먹고 빨래를 했습니다.
BBBB
}
}
class B {
method c() {
AAAA
점심은 이거 찍느라 못먹었는데 저녁엔 제육볶음을 먹고 싶네요.
BBBB
}
}
하지만 아래 코드처럼, 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP이다.
class A {
method a() {
오늘은 7월 4일 미국 독립 기념일이래요.
}
method b() {
저는 아침에 운동을 다녀와서 밥먹고 빨래를 했습니다.
}
}
class B {
method c(){
점심은 이거 찍느라 못먹었는데 저녁엔 제육볶음을 먹고 싶네요.
}
}
class AAAABBBB {
method aaaabbb(JoinPoint point){
AAAA
point.excute()
BBBB
}
}
컴파일 (A.java ---(AOP)--> A.class(AspectJ))
자바 파일에는 Aspect 코드가 없지만, 컴파일 후에는 A.class에는 Aspect코드가 있게끔 해주는 컴파일러(AspectJ)를 사용한다.
바이트코드 조작(A.java -> A.class --(AOP) -->메모리(AspectJ))
JVM의 클래스 로더가 A.class를 읽어서 JVM 메모리에 올릴때 바이트코드 조작.
프록시 패턴 (스프링 AOP가 사용하는 방법)
기존 코드를 고치지 않고, 기촌 객체를 다른 객체로 바꾸는 방법
대표적인 예로, @Transaction 어노테이션이 스프링 AOP 기반으로 만들어져있다.
: 실제 기능을 수행하는 객체 대신 가상의 객체를 사용해 로직의 흐름을 제어하는 디자인 패턴.
public interface Payment{
void pay(int amount);
}
public class Cash implements Payment{
@Override
public void pay(int amount){
System.out.println(amount + "현금 결제");
}
}
//프록시 클래스
public class CashPerf implements Payment {
Payment cash = new Cash();
@Override
public void pay(int amount){
StopWatch stopWatch = new StopWatch();
stopWatch.start();
cash.pay(amount);
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
}
//인터페이스를 사용하는 클래스
public class Store {
Payment payment;
public Store(Payment payment){
this.payment = payment;
}
public void buySomething(int amount){
payment.pay(amount);
}
}
//테스트 코드
public class StoreTest{
@Test
public void testPay(){
Payment cashPerf = new CashPerf();
Store store = new Store(cashPerf);
store.buySomething(100);
}
}
Store라는 클래스에 buySomething()의 성능 측정 코드를 추가하려고 한다면 이 메소드 안에서의 시작점과 끝지점에 성능 측정 코드를 넣어줘야한다.
하지만 프록시 패턴을 이용하여 CashPerf라는 프록시 클래스를 생성하였고 성능 측정 코드를 추가 하였다. 이러한 패턴이 프록시 패턴이다.
Store 클래스에서는 전혀 바뀌지 않은 상태에서 프록시 코드를 사용할 수 있는 것이다. 즉 새로운 코드를 추가하였지만 기존 코드를 바뀌지 않았다는 게
AOP를 프록시 패턴으로 구현하는 방법이다.
스프링 내부에서는 프록시가 자동으로 빈이 등록될때 만들어진다.
즉, 위의 코드로 보면 사용자가 Cash라는 클래스를 빈으로 등록하였다고 가정하면 Cash가 빈으로 등록이 되는 것이 아닌 사용자가 만들고 싶은 프록시(CashPerf)가 자동으로 빈 등록이 되어 Cash 대신에 사용자가 만든 프록시가 등록이 된다.
클라이언트 입장에서는(Store) Cash를 사용하는 게 아닌 CashPerf를 대신 사용하는 일이 스프링 내부에서 발생하게 된다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
@Component
@Aspect
/*
* 각 메소드의 성능평가를 위한 콘솔 시간 출력을 위한 @LogExecutionTime 애노테이션 생성 부분
* 조인 타겟 메소드의 앞 뒤로, StopWatch를 활용하여 시간을 출력한다.
*/
public class LogAspect {
Logger logger = LoggerFactory.getLogger(LogAspect.class);
//Around라는 애노테이션을 사용한 메소드 안에서 joinPoint(애노테이션이 붙어있는 타겟 메소드) 파라미터 사용 가능
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
//조인 타겟 메소드 실행하는 부분
Object proceed = joinPoint.proceed();
stopWatch.stop();
logger.info(stopWatch.prettyPrint());
return proceed;
}
}
//OwnerController.java
@PostMapping("/owners/new")
@LogExecutionTime
public String processCreationForm(@Valid Owner owner, BindingResult result) {
....
....
}
위의 코드에서 OwnerController는 Aspect이 적용할 Target 즉 Cash라고 보면 되고 LogAspect는 CashPerf라고 볼 수 있다.
스프링 AOP 매커니즘에 의해서 OwnerController가 빈으로 주입될 때 프록시 객체를 생성하여 자동으로 이루어진다.
Cash가 Store클래스에 주입되는 것이 아닌 CashPerf가 주입되는 과정이라고 볼 수 있는 것이다.
참고자료
예제로 배우는 스프링 입문(개정판) ,
https://jdm.kr/blog/235,
https://engkimbs.tistory.com/746,
https://dailyworker.github.io/spring-triangle/