아래와 같은 코드가 있다고 생각해보자.
Copy class A {
method a () {
pre() ;
aaa() ;
post() ;
}
}
class B {
method b () {
pre() ;
bbb() ;
post() ;
}
}
class C {
method c () {
pre() ;
bbb() ;
post() ;
}
}
모든 클래스는 pre()
와 post()
를 공통으로 가지고 있다. 이때 이 두 메서드에 수정이 필요하다면 세 클래스 모두 건드려야 한다.
Copy 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가 사용하는 방법이다. 빈이 등록될 때 자동으로 프록시가 만들어진다.
Copy 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 ());
}
}
Copy start
100 현금 결제
StopWatch '': running time = 138590 ns
---------------------------------------------
ns % Task name
---------------------------------------------
000138590 100%
우리는 Store
나 Cash
를 건드리지 않고도 Stopwatch
를 이용한 부가 기능을 사이에 끼워넣었다. 스프링에 빗대자면, Cash
를 빈으로 등록해놨지만 내가 만들고 싶은 프록시가 자동으로 생성되어 클라이언트는 Cash
대신에 프록시를 사용한다.
Copy public interface OwnerRepository extends Repository < Owner , Integer > {
@ Transactional (readOnly = true )
Collection < Owner > findByLastName (@ Param ( "lastName" ) String lastName);
}
원래 JDBC를 쓸 때면 코드 앞 뒤로 setAutoCommit()
과 커밋, 롤백 코드가 들어갔다. 하지만 @Transactional
이 붙으면 OwnerRepository
객체 타입의 프록시가 새로 생성되어 그 코드를 생략할 수 있다.
Reference
프록시 패턴
AOP 실습
성능을 측정하는 @LogExecutionTime
을 만들어 적용해보자.
Copy @ 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) {
...
}
}
Copy // 어디에 쓸 수 있는지 설정한다. 여기서는 메서드에서 쓸 수 있도록 설정했다.
@ Target ( ElementType . METHOD )
// 애너테이션 정보를 런타임까지 유지한다고 설정한다.
@ Retention ( RetentionPolicy . RUNTIME )
public @ interface LogExecutionTime {
}
@LogExecutionTime
은 그저 어디에 적용할지 표시하는 용도일 뿐이다. 실제 이 애너테이션을 읽어서 처리하는 일은 Aspect
가 담당한다.
Copy // 빈으로 등록되어야 하므로 컴포넌트 애너테이션을 붙인다.
@ 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;
}
}
이 코드가 Aspect
이며 프록시 패턴
으로 동작하는 것이다.