스프링 AOP(Aspect Oriented Programming)

AOP

AOP는 Asepect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다.

관점 지향은 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고

부가적인 관점을 각각 따로 분리해서 모듈화하겠다는 것이다.

예로들어 핵심적인 관점은 우리가 적용하고자 하는 핵심 비지니스 로직이 된다.

또한 부가적인 관점은 핵심 로직외에 행해지는 데이터베이스 연결, 로깅, 파일 입출력등이 있다.

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드를 부분적으로 나누어서 모듈화하겠다는 의미다.

이때, 소스 코드상에서 핵심 비지니스 로직(Core Concerns)외에 계속 반복해서 쓰는 코드들을 발견할 수 있는데 이것을 흩어진 관심사(Crosscutting Concerns)라 부른다.

스크린샷 2020-11-27 오후 12 59 42

위와 같이 흩어진 관심사(Crosscutting Concerns)를 Aspect로 모듈화하고 핵심적인 비지니스 로직(Core concerns)에서 분리하여 재사용하겠다는 것이 AOP이다.

AOP 관련 용어

  • 조인 포인트(JoinPoint) : 클라이언트가 호출하는 모든 비지니스 메소드, 조인포인트 중에서 포인트컷이 되기때문에 포인트 컷의 후보로 생각할 수 있다.
  • 포인트 컷(Pointcut) : 특정 조건에 의해 필터링된 조인 포인트, 수 많은 조인 포인트 중에 특정 메소드에서만 Advice를 수행시키기 위해서 사용된다
  • 어드바이스(Advice) : Crosscutting Concern에 해당하는 공통 기능의 코드
  • 애스펙트(Aspect) : 포인트 컷과 어드바이스의 결합이다. 어떤 포인트 컷 메소드에 대해 어떤 어드바이스 메소드가 실행할 지 결정한다.
  • 위빙(Weaving) : 핵심 비지니스 로직과 Aspect를 연결하여 Advised Object를 만드는 과정
    1. complie time
    2. load time
    3. runtime

AOP 구현 방법

  1. 컴파일

    • A.java —— (AOP) —–> A.class
    • 컴파일을 할 때 중간에 공통 로직을 끼워 넣는다
    • 컴파일하기 전 코드에는 해당 로직이 없지만, 컴파일이 완료되면 해당 로직을 가지고
      있는 상태이다
    • AspectJ를 이용해서 만들어진다
  2. 바이트코드 조작

    • A.java —-> A.class —-> (AOP) —-> 메모리
    • load time에서 ClassLoader가 A.class를 읽어온다
    • 읽어와서 메모리에 올릴 때 공통 로직이 들어가도록 조작한다
    • AspectJ를 이용해서 만들어진다
  3. 프록시 패턴

    • Runtime에서 구현하는 방식
    • 프록시 패턴을 적용하여 실제 객체가 실행되는 것이 아닌 실제 객체를 감싸고 있는 프록시 객체를 Runtime에 생성
    • 프록시 객체는 Aspect가 적용되어있고 Core Concern이 들어있는 실제 객체를 reference하고 있다
    • AOP가 적용된 객체를 요청할 때 실제 객체가 아닌 프록시 객체가 실행된다.
    • AspectJ로 만들어져 있으며 SPRING AOP는 이 방식으로 구현되어있다.

Spring AOP

스프링 AOP 특징

  • 프록시 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용할 수 있다
  • 모든 AOP 기능을 제공하는 것이 목적이 아니라, Spring IoC와 연동하여 어플리케이션의 문제에 대한 해결책을 제공하는 것이 목적

프록시 패턴

  • 접근 제어 또는 부가기능 추가

    image

    • 인터페이스에 대해서 핵심 비지니스 로직만으로 구성되어진 실제 객체를 만든다
    • 인터페이스에 대해서 또 하나의 Proxy 구현체를 만든다
    • 이 구현체에는 부가기능이 추가있으며 실제 객체를 참조하여 실제 객체의 비지니스 로직을 실행 시킬 수 있다
    • 클라이언트는 객체를 인터페이스를 통해서 접근하게 되며, 실제객체가 직접 실행되는 것이 아닌 Proxy객체를 통해서 부가기능을 수행하고 간접적으로 실행된다.

프록시 패턴을 적용 (Spring AOP를 사용하지 않음)

  • EventService Interface

    1
    2
    3
    4
    5
    6
    7
    public interface EventService {
    void createEvent();

    void publishEvent();

    void deleteEvent();
    }
  • EventService 구현체 (핵심 비지니스 로직 수행)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    @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(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Publish an event");
    }

    public void deleteEvent() {
    System.out.println("Delete an event");
    }
    }
  • EventService의 Proxy 구현체 (부가기능 + 핵심 로직 객체 참조)

    @Primary로 SimpleEventService가 아닌 ProxySimpleEventService이 실행됨

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @Primary
    @Service
    public class ProxySimpleEventService implements EventService{

    @Autowired
    SimpleEventService simpleEventService;

    @Override
    public void createEvent() {
    long begin = System.currentTimeMillis();
    simpleEventService.createEvent();
    System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void publishEvent() {
    long begin = System.currentTimeMillis();
    simpleEventService.publishEvent();
    System.out.println(System.currentTimeMillis() - begin);
    }

    @Override
    public void deleteEvent() {
    simpleEventService.deleteEvent();
    }
    }

문제점

  • 매번 프록시 패턴을 적용해서 프록시 클래스를 작성해야한다
  • 여러 클래스 여러 메소드들에 적용할려면 반복적인 작업이 증가한다
  • 객체들관의 관계도 복잡하다

스프링 AOP 등장

  • 스프링 IoC컨테이너가 제공하는 기반 시설과 Dynamic 프록시를 사용하여 여러 복잡한 문제 해결
  • 동적 프록시 : 동적으로 프록시 객체를 생성하는 방법
    • 자바가 제공하는 방법은 인터페이스 기반 프록시 생성
    • CGlib은 클래스 기반 프록시도 지원
  • 스프링 IoC: 기존 빈을 대체하는 동적 프록시 빈을 만들어 등록해준다
    • 클라이언트 코드에 변경이 없다

스프링 AOP 적용해서 개선

스프링 AOP는 Spring Container에 등록된 Bean들만 적용된다

  • Pointcut을 Annotation 기반으로 작성

    1
    2
    3
    4
    5
    @Documented
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.METHOD)
    public @interface PerfLogging {
    }
  • Aspect 작성 (Around 방식을 적용한 Advice 메소드 작성)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component
    @Aspect
    public class PerfAspect {

    @Around("@annotation(PerfLogging)")
    public Object logPerf(ProceedingJoinPoint pjp) throws Throwable {
    long begin = System.currentTimeMillis();
    Object retVal = pjp.proceed();
    System.out.println(System.currentTimeMillis() - begin);
    return retVal;
    }

    @Before("bean(simpleEventService)")
    public void hello(){
    System.out.println("hello");
    }
    }
    • Around방식은 핵심 비지니스로직 실행 권한을 ProceedingJoinPoint로 받아온다 (Spring Container가 받아옴)
    • 받아온 객체를 실행하기 전 후에 작업을 진행한다
    • 받아온 객체가 return값을 가지고 있으면 Advice메소드에서 그 값을 return해 주어야한다
  • AOP를 적용할 Pointcut메소드를 지정한다(메소드 위에 어노테이션 설정)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Service
    public class SimpleEventService implements EventService{
    @PerfLogging
    @Override
    public void createEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Created an event");
    }

    @PerfLogging
    @Override
    public void publishEvent() {
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println("Publish an event");
    }

    @Override
    public void deleteEvent() {
    System.out.println("Delete an event");
    }
    }

Pointcut 정의 방식

  1. execution 설정 방식

    • 사용 예

      1
      @Around("execution(* com.example..*.EventService.*(..))")
    • 적용 규칙

      스크린샷 2020-11-27 오후 2 51 06
  2. bean 설정 방식

    • 사용 예

      적용할 빈 이름을 직접 입력

      1
      @Around("bean(simpleEventService)")
  3. annotation 설정 방식 (위에서 설명)

Advice 실행 시점

  1. Before

    • 핵심 비지니스 로직 실행 전에 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      @Before("execution(* com.rubypaper.biz..*Impl.*(..))")
      public void printLog(JoinPoint jp) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름
      Object[] args = jp.getArgs(); // 클라이언트가 전달한 인자 정보

      System.out.println("<사전 처리> " + method + "비지니스 로직 수행 전 동작" + "() 메소드 ARGS 정보 : " + args[0].toString());
      }
  2. After

    • 핵심 비지니스 로직 실행 후에 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      @After("execution(* com.rubypaper.biz..*Impl.*(..))")
      public void printLog(JoinPoint jp) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름
      Object[] args = jp.getArgs(); // 클라이언트가 전달한 인자 정보

      System.out.println("<사후 처리> " + method + "비지니스 로직 수행 후 동작" + "() 메소드 ARGS 정보 : " + args[0].toString());
      }
  3. After Returning

    • 핵심 비지니스 로직 실행한 후 반환된 객체를 Advice에서 받아와서 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @AfterReturning(pointcut = "!void com.rubypaper.biz..*Impl.*(..))", returning = "returnObj")
      public void afterLog(Object returnObj) {
      System.out.println("<사후 처리> 비지니스 로직 리턴 값 : " + returnObj.toString());

      if (returnObj instanceof UserVO) {
      UserVO user = (UserVO) returnObj;
      if(user.getRole().equals("ADMIN")) {
      System.out.println(user.getName() + "님은 관리자 화면으로 바로 이동........");
      }
      }
      }
    • 핵심 비지니스 로직에서 반환된 객체를 returnObj로 받아온다
  4. After Throwing

    • 핵심 비지니스 로직 실행헌 후 예외가 발생하면 예외객체를 받아와서 실행
    • 실행 예
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
       @AfterThrowing(pointcut = "execution(* com.rubypaper.biz..*Impl.*(..))", throwing = "exceptionObj")
      public void exceptionLog(JoinPoint jp, Exception exceptionObj) {
      String method = jp.getSignature().getName(); // 클라이언트가 호출한 메소드 이름

      System.out.println("[ 예외처리 ]"+ method +" 메소드 수행 중 예외 발생");

      if(exceptionObj instanceof IllegalArgumentException){
      System.out.println("0 번 게시 글을 등록할 수 없습니다");
      } else if (exceptionObj instanceof ArithmeticException) {
      System.out.println("0 으로 숫자를 나눌 수 없습니다");
      } else if (exceptionObj instanceof SQLException) {
      System.out.println("SQL 구문에 오류가 있습니다");
      } else {
      System.out.println("문제 발생 !! 시스템을 잠시 종료합니다");
      }
      }
      • 예외객체를 exceptionObj로 받아온다
  5. Around

    • 위에서 설명

Advice에서 Pointcut에 해당되는 메소드에 대한 정보를 얻기 위해 JoinPoint라는 객체를 받아올 수 있다. Around만 예외적으로 ProceedJoinPoint라는 객체를 통해서 받아온다