본문 바로가기
Dev/Spring

[스프링 프레임워크 입문] AOP

by dev_jsk 2020. 11. 18.
728x90
반응형

AOP (Aspect Oriented Programming)

관점 지향 프로그래밍으로 애플리케이션에서의 공통 기능(관심사)를 핵심적인 기능에서 분리하고 분리한 기능을 Aspect라는 모듈을 이용하여 설계 및 개발하는 방법

※ 분리된 기능은 핵심 기능이 존재하는 클래스에 코드로 존재하지 않는다.

AOP 주요 용어

  • Target : 핵심 기능을 담고 있는 모듈. 부가기능을 부여할 대상
  • Advice : 타겟에 제공할 부가기능을 담고 있는 모듈
  • JoinPoint : 어드바이스가 적용될 수 있는 위치. 타겟 객체가 구현한 인터페이스의 모든 메서드는 조인포인트
  • PointCut : 어드바이스를 적용할 타겟의 메서드를 선별하는 정규표현식
  • Aspect : AOP의 기본 모듈
  • Weaving : 포인트컷에 의해 결정된 타겟의 조인포인트에 어드바이스를 삽입하는 과정

AOP 구현 방법

  • 컴파일 이용 : AspectJ를 이용하여 컴파일 시 분리된 공통 코드를 포함하여 컴파일
  • 바이트코드 조작 : ClassLoader가 클래스파일을 읽고 메모리에 올릴 때 분리된 공통 코드를 포함 (AspectJ 제공)
  • 프록시 패턴 : 디자인 패턴 중 하나를 사용해서 AOP와 같은 효과를 내는 방법으로 스프링 AOP가 사용하는 방법

프록시 패턴 예제

기존 클라이언트 코드를 건드리지 않고 새 기능(성능 측정) 추가하기

 

클라이언트 (Store.java)

public class Store {

  Payment payment;

  public Store(Payment payment) {
    this.payment = payment;
  }

  public void buySomething(int amount) {
    payment.pay(amount);
  }
  
}

프록시 (CashPerf.java)

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());
  }
  
}

인터페이스 (Payment.java)

// Payment.java
public interface Payment {
  void pay(int amount);
}

타겟 (Cash.java)

public class Cash implements Payment {

  @Override
  public void pay(int amount) {
    System.out.println(amount + " 현금 결제");
  }
  
}

테스트 (StoreTest.java)

public class StoreTest {
  
  @Test
  public void testPay() {
    Payment cashPerf = new CashPerf();  // 프록시 클래스를 사용하여 성능 측정 기능 사용
    Store store = new Store(cashPerf);
    store.buySomething(100);
  }
  
}

결과

클라이언트 클래스(Store.java)를 수정하지 않고, 프록시 클래스(CashPerf.java)를 사용하여 기존 기능에 성능 측정 기능을 추가하여 동작한다.

만약 프록시 클래스를 사용하지 않고 기존 기능 클래스(Cash.java)를 사용하면 성능 측정 기능은 동작하지 않는다.

 

따라서 위의 예제는 프록시 패턴을 사용하여 AOP를 구현한 것이고, 스프링은 이것을 자동으로 빈이 생성, 등록될 때 프록시 패턴이 생성된다.

@AOP 실습

성능 측정하는 어노테이션을 만들어 구현해보자

 

타겟 (OwnerController.java)

...

@GetMapping("/owners/{ownerId}/edit")
@LogExecutionTime
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) {
  Owner owner = this.owners.findById(ownerId);
  model.addAttribute(owner);
  return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}

@PostMapping("/owners/{ownerId}/edit")
@LogExecutionTime
public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId) {
    if (result.hasErrors()) {
      return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
    }
    else {
      owner.setId(ownerId);
      this.owners.save(owner);
      return "redirect:/owners/{ownerId}";
    }
}

...

어노테이션 선언 클래스 (LogExecutionTime.java)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}

Aspect 구현 클래스 (LogAspect.java)

@Component  // Bean 등록
@Aspect  // Aspect 선언
public class LogAspect {
  Logger logger = LoggerFactory.getLogger(LogAspect.class);

  @Around("@annotation(LogExecutionTime)")  // 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;
  }
}

결과

어노테이션을 적용한 타겟 메소드를 실행 했을 때 성능 측정이 되는 것을 볼 수 있다.

 

위 코드에서 보면 LogAspect 클래스 내 logExecutionTime 메소드는 이전 예제의 CashPerf 클래스와 동일한 역할을 한다.

 

따라서 스프링 AOP는 이런방식으로 동작하며 위 예제는 프록시 패턴을 바탕으로 동작한다.

728x90
반응형

댓글