class A {
method a() {
pre();
aaa();
post();
}
}
class B {
method b() {
pre();
bbb();
post();
}
}
class C {
method c() {
pre();
bbb();
post();
}
}
모든 클래스는 pre()와 post()를 공통으로 가지고 있다. 이때 이 두 메서드에 수정이 필요하다면 세 클래스 모두 건드려야 한다.
class A {
method a() {
aaa();
}
}
class B {
method b() {
bbb();
}
}
class C {
method c() {
bbb();
}
}
class PrePro {
method prePro(JoinPoint point) {
pre();
point.execute();
post();
}
}
그래서 공통된 메서드는 별도의 클래스로 빼낸다. 이게 바로 AOP다.
AOP 구현 방법
컴파일
A.java ----> (AOP) ----> A.class
A.java 파일을 컴파일 하면 A.class 파일이 생긴다. 컴파일을 하는 과정에서 AOP를 끼워넣는 것이다. AspectJ가 이 방법을 지원한다.
바이트코드 조작
A.java ----> A.class ----> (AOP) ----> 메모리
클래스 로더가 A.class 파일을 읽어오면서 메모리에 올릴 때 조작하는 방법이다.
클래스 파일의 코드 상엔 아무것도 없는데 클래스를 로딩하는 시점에 메모리 상에서 추가하고 싶은 코드가 들어가는 것이다. 역시 AspectJ가 제공한다.
프록시 패턴
스프링 AOP가 사용하는 방법이다. 빈이 등록될 때 자동으로 프록시가 만들어진다.
public class toreTest {
@Test
public void testPay() {
// 프록시를 넣어준다.
Payment cashPerf = new CashPerf();
// 프록시를 쓰지 않았다면 아래처럼 Cash를 그대로 썼을 것이다.
// Payment cash = new Cash();
Store store = new Store(cashPerf);
store.buySomething(100);
}
}
public class Store {
Payment payment;
public Store(Payment payment) {
this.payment = payment;
}
public void buySomething(int amount) {
payment.pay(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();
System.out.println("start");
cash.pay(amount);
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
}
start
100 현금 결제
StopWatch '': running time = 138590 ns
---------------------------------------------
ns % Task name
---------------------------------------------
000138590 100%
우리는 Store나 Cash를 건드리지 않고도 Stopwatch를 이용한 부가 기능을 사이에 끼워넣었다. 스프링에 빗대자면, Cash를 빈으로 등록해놨지만 내가 만들고 싶은 프록시가 자동으로 생성되어 클라이언트는 Cash 대신에 프록시를 사용한다.
@Controller
class OwnerController {
private final OwnerRepository owners;
private final PetRepository petRepository;
public OwnerController(OwnerRepository clinicService, PetRepository petRepository) {
this.owners = clinicService;
this.petRepository = petRepository;
}
@GetMapping("/owners/new")
@LogExecutionTime
public String initCreationForm(Map<String, Object> model) {
...
}
@PostMapping("/owners/new")
@LogExecutionTime
public String processCreationForm(@Valid Owner owner, BindingResult result) {
...
}
@GetMapping("/owners/find")
@LogExecutionTime
public String initFindForm(Map<String, Object> model) {
...
}
@GetMapping("/owners")
@LogExecutionTime
public String processFindForm(Owner owner, BindingResult result, Map<String, Object> model) {
...
}
}
// 어디에 쓸 수 있는지 설정한다. 여기서는 메서드에서 쓸 수 있도록 설정했다.
@Target(ElementType.METHOD)
// 애너테이션 정보를 런타임까지 유지한다고 설정한다.
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
@LogExecutionTime은 그저 어디에 적용할지 표시하는 용도일 뿐이다. 실제 이 애너테이션을 읽어서 처리하는 일은 Aspect가 담당한다.
// 빈으로 등록되어야 하므로 컴포넌트 애너테이션을 붙인다.
@Component
// Aspect 라는 것을 알려준다.
@Aspect
public class LogAspect {
Logger logger = LoggerFactory.getLogger(LogAspect.class);
// Around 애너테이션을 사용하면 joinPoint를 사용할 수 있다.
// joinPoint란 우리가 커스텀해서 만든 애너테이션을 적용할 타겟을 의미한다.
// 즉, OwnerController에서 @LogExecutionTime을 붙인 메서드들이다.
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 타겟인 메서드가 JoinPoint 타입의 인터페이스로 들어오면 실행한다.
Object proceed = joinPoint.proceed();
// 메서드 앞/뒤로 스탑워치를 실행한다.
stopWatch.stop();
logger.info(stopWatch.prettyPrint());
return proceed;
}
}