스프링 AOP (Aspect Oriented Programming)

4 분 소요

스프링 AOP (Aspect Oriented Programming)

스프링 AOP 란?

aop

  • AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.
  • 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하는 것 (모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.)
  • 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.
  • AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드를 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는데 이것을 흩어진 관심사(Crosscutting Concerns)라 부른다.

  • 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

AOP 주요 개념

  • Aspect: 흩어진 관심사를 모듈화 한 것.
  • Target: Aspect를 적용하는 곳 (클래스, 메서드)
  • Advice: 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
  • JointPoint: Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
  • PointCut: JointPoint의 상세한 스펙을 정의한 것

AOP 구현체

  • 자바
    • AspectJ
    • 스프링 AOP

AOP 적용 방법

  • 컴파일: 자바 파일을 클래스 파일로 만들 때 바이트코드를 조작하여 적용된 바이트코드를 생성
  • 로드 타임: 컴파일은 원래 클래스 그대로 하고, 클래스를 로딩하는 시점에 끼워서 넣는다.
  • 런타임: A라는 클래스를 빈으로 만들 때 A라는 타입의 프록시 빈을 감싸서 만든 후에, 프록시 빈이 클래스 중간에 코드를 추가해서 넣는다.

스프링 AOP 특징

  • 프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서다.
  • 스프링 빈에만 AOP를 적용할 수 있다.
  • 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가 … )에 대한 해결책을 지원하는 것이 목적

프록시 패턴

proxy

  • 프록시 패턴에는 interface가 존재하고 Client는 이 interface 타입으로 Proxy 객체를 사용하게 된다.
  • 프록시는 Real Subject를 감싸서 클라이언트의 요청을 처리하게 된다. 프록시 패턴의 목적은 기존 코드 변경 없이 접근 제어 또는 부가 기능을 추가하기 위해서이다.
  • Subject 인터페이스 역할 (EventService.java)
      public interface EventService{
          void createEvent();
          void publishEvent();
          void deleteEvent();
      }
    
  • Client 역할 (AppRunner.java)
      @Component
      public class AppRunner implements ApplicationRunner{
          @Autowired EventService eventService;
    
          @Override
          public void run(ApplicationArguments args) throws Exception{
              eventService.createEvent();
              eventService.publishEvent();
          }
      }
    
  • Real Subject 역할 (SimpleEventService.java)
      @Service
      public class SimpleEventService implements EventService {
          @Override
          public void createEvent() {
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("Created an event");
          }
          @Override
          public void publishEvent() {
              try {
                  Thread.sleep(2000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              System.out.println("Published an event");
          }
          @Override
          public void deleteEvent() {
                
          }
      }
    
  • 아래의 코드에서 단순히 메소드가 2개만 존재할 뿐이기에 괜찮을 수도 있지만 실제 프로젝트라면 많은 중복 코드가 필요할 것이다. 이러한 상황에서 AOP를 사용한다.
      @Service
      public class SimpleEventService implements EventService{
          @Override
          public void createEvent(){
              long begin= System.currentTimeMillis();
              try{
                  Thread.sleep(1000);
              }catch(InterruptedException e){
                  e.printStackTrace();
              }
              System.out.println("Created an event");
              System.out.println(System.currentTimeMillis()-begin);
          }
          @Override
          public void publishEvent(){
              long begin = System.currentTimeMillis();
              try{
                  Thread.sleep(2000);
              }catch(InterruptedException e){
                  e.printStackTrace();
              }
              System.out.println("Published an event");
              System.out.println(System.currentTimeMillis() - begin);
          }
          @Override
          public void deleteEvent(){
    
          }
      }
    
  • Proxy로 코드를 구현하였다. Proxy가 Real Subject에 해당하는 SimpleEventService를 가지고 있고 시간을 측정하는 기능도 대신 가지고 있다. 이렇게 하면 Real Subject와 Client의 코드를 수정하지 않아도 기능을 추가할 수 있다.
  • Proxy가 Real Subject를 가지고 있고, Real Subject에 일을 위임해서 대신 처리하고, 부가적인 기능들은 가지고 있다.
      @Primary  //같은 타입의 빈이 여러가지일 때 그 중 하나를 선택하여 사용하는 에너테이션
      @Service
      public class ProxySimpleEventService implements EventService{
    
          @Autowired
          SimpleEventService simpleEventService;
    
          @Override
          public void createEvent(){
              long begin = System.currentTimeMillis();
              simpleEventService.createEvent();
              System.out.println(System.currentMillis() - begin);
          }
          @Override
          public void publishEvent(){
              long begin = System.currentTimeMillis();
              simpleEventService.publishEvent();
              System.out.println(System.currentTimeMillis()- begin);
          }
          @Override
          public void deleteEvent(){
    
          }
      }
    
  • 하지만 Proxy 클래스에서도 중복코드가 발생한다는 점과 Proxy 클래스를 만들어야 하는 비용이 발생하는 문제가 있다.
  • 위의 예제에서는 Proxy를 클래스로 만들어서 사용했지만, 동적으로 Proxy 객체를 만드는 방법이 있다. 여기서 동적이란, 런타임, 즉 애플리케이션이 동작하는 중에 동적으로 어떤 객체의 Proxy 객체를 만드는 것을 말한다.
  • 스프링 IoC 컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용해서 여러 복잡한 문제 (중복 코드, Proxy 생성)를 해결할 수 있다. 이것이 바로 스프링 AOP 이다.

AOP

  • 먼저 @AOP를 사용하기 위해 pom.xml에 의존성을 추가해준다.
  • Aspect를 만들기 위해 해야할 일인 Advice, 어디에 적용할 것인가에 대한 Point Cut 두 가지를 정의해야 한다.
      @Component
      @Aspect
      public class PerfAspect {
    
          @Around("execution(* com.example..*.EventService.*(..))")
          public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
              long begin = System.currentTimeMillis();
              Object retVal = pjp.proceed();
              System.out.println(System.currentTimeMillis() - begin);
              return retVal;
      }
    
  • Aspect 에너테이션으로 이 클래스가 Aspect 클래스임을 알려준다. @Component 에너테이션을 사용해서 빈으로 등록하는데 이는 에너테이션 기반의 스프링 IoC를 사용하기 때문에 Component Scan을 통해서 빈 등록을 하기 때문이다.
  • ProceedingJoinPoint (PJP)는 Advice가 적용되는 대상이다. 즉, Advice가 적용되는 createEvent, publishEvenr와 같은 메서드 자체라고 보면 된다.
  • @Around()은 Aspect의 실행 시점을 지정할 수 있는 에너테이션을 이용해서 적용할 범위를 지정해준다.
  • exectuion은 Point Cut 표현식인데 이 표현식을 사용해서 어디에 적용할 지를 정의할 수 있다. 코드를 보면 “com.example 패키지 밑에 있는 모든 클래스에 적용을 하고, EventService 밑에 있는 모든 메소드에 정의해라” 라는 의미이다.
  • 하지만 이렇게 할 경우, 적용하고 싶지 않은 메소드에도 적용이 될 수 있다. execution을 사용하는 대신 에너테이션을 사용하면 원하는 곳에만 적용할 수 있다.
      @Documented
      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.CLASS)
      public @interface PerLogging{
    
      }
    
      @Component
      @Aspect
      public class PerfAspect {
    
          @Around("@annotation(PerLogging)")
          public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
              long begin = System.currentTimeMillis();
              Object reVal = pjp.proceed();
              System.out.println(System.currentTimeMillis() - begin);
              return reVal;
          }
      }
    
  • @Around 에너테이션에 위와 같이 수정을 하게 되면 위에서 만든 에너테이션 파일과 연결이 된다.
  • 성능을 측정하고자 하는 메소드에 @perLoggin 어노테이션을 추가해주면 해당 메소드가 실행될 때 성능 측정도 출력이 된다.
      @Service
      public class SimpleEventService implements EventService {
    
          @PerLogging
          @Override
          public void createEvent() {
              System.out.println("Created an event");
          }
    
          @PerLogging
          @Override
          public void publishEvent() {
              System.out.println("Published an event");
          }
    
          @Override
          public void deleteEvent() {
              System.out.println("deleteEvent");
          }
      }
    

    그 외

  • 이 밖에도 @Around 외에 타겟 메서드의 Aspect 실행 시점을 지정할 수 있는 에너테이션이 있다.
  • @Before (이전): 어드바이스 티켓 메소드가 호출되기 전에 어드바이스 기능을 수행
  • @After (이후): 타겟 메소드의 결과에 관계없이 (즉 성공, 예외 관계없이) 타겟 메소드가 완료되면 어드바이스 기능을 수행
  • @AfterReturning (정상적 반환 이후): 타겟 메소드가 성공적으로 결과값을 반환 후에 어드바이스 기능을 수행
  • @AfterThrowing (예외 발생 이후): 타겟 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
  • @Around (메소드 실행 전후): 어드바이스가 타겟 메소드를 감싸서 타겟 메소드 호출전과 후에 어드바이스 기능을 수행