1. AOP(Aspect Oriented Programming)

  • 횡단 관심사(Cross-cutting Concerns)

    • 어플리케이션 전반에 걸쳐 공통적으로 나타나는 로직
    • 동일한 기능을 하는 코드가 여러 모듈에서 반복적으로 나타나기에 흩어진 관심사라고도 함
    • 횡단 관심사를 통합하는 것으로 중복 코드를 제거하고 각 모듈이 핵심 로직에 집중 가능
      => 단일 책임 원칙(Single Responsibility Principle) 을 자연스럽게 적용 가능
  • 횡단 관심사를 상속 등의 기본적인 객체 지향 기법을 사용하여 분리하는 데는 한계가 있음

  • AOP는 보다 효율적으로 공통 로직을 여러 모듈에 적용하기 위한 프로그래밍 기법

 

 

2. Spring AOP의 특성

  • 대상 객체나 메소드를 감싸는 동적 프록시를 사용하여 구현
  • 프록시의 특성상 프록시 내부에서 메소드를 직접 호출할 경우 부가기능이 동작하지 않음
  • AOP는 Bean으로 등록된 객체에만 사용 가능
  • @Transactional, @Cacheable 등이 Spring이 지원하는 대표적인 AOP 서비스
  • 기본적으로 지원하는 AOP 서비스 외에도 사용자가 직접 AOP를 구현할 수도 있음

 

 

3. @Aspect

  • Spring에서 직접 AOP를 구현하기 위해 사용하는 어노테이션

  • 대상 클래스를 AOP 모듈로 사용할 것임을 명시

  • AOP 모듈로 사용하기 위해서는 Bean으로 등록되어야함(@Component 등 사용)

  • 공통 로직을 구현한 AOP 모듈 내의 메소드는 Advice라 함

     

  • Advice 실행 시점 지정

    • @Before : 대상 메소드의 실행 이전

    • @After : 대상 메소드의 실행 이후

    • @AfterReturning : 대상 메소드가 정상적으로 실행되어 반환된 이후

    • @AfterThrowing : 대상 메소드 실행중 Exception이 발생한 시점

    • @Around : 메소드 호출을 intercept하여 로직 실행 전후로 공통 로직을 수행 가능

       

  • AOP 적용 대상 지정

    • @Pointcut

      • @Pointcut("")의 형태로 AOP 적용 대상을 지정할 수 있음
        => Pointcut에서 사용되는 표현식을 PCD(Point Cut Designator)라 함

       

      • execution

        • 메소드의 반환 타입, 이름, 파라미터 등을 기준으로 대상을 지정

        • example

          • execution(pubilc * *(..)) : 모든 public 메소드
          • execution(int *(..)) : 반환 타입이 int인 모든 메소드
          • execution(* * (Long, int)) : 파라미터가 Long, int 인 모든 메소드

       

      • within

        • 특정 패키지, 클래스 내의 모든 메소드를 대상으로 지정

        • example

          • within(com.example.SampleProject.service.TestService)

       

      • this / target

        • 타입명을 사용하여 해당 클래스 또는 그 하위 클래스의 메소드를 대상으로 지정

        • this는 프록시 객체의 타입과, target은 실제 객체의 타입과 매칭

        • example

          • this(com.example.SampleProject.service.TestInterface)
          • target(com.example.SampleProject.service.TestInterfaceImpl)

       

      • args

        • 특정 이름의 매개변수를 받는 메소드를 대상으로 지정

        • Pointcut 메소드 또는 Advice 메소드에서 해당 변수를 매개변수로 받아야함

        • example

          • args(name)
          • public Object advice(ProceedingJoinPoint pj, String name) { ... }
          • String 타입의 name 변수를 가지는 메소드가 대상이 됨
          • 해당 변수의 값을 참조하는 것도 가능

       

      • @target

        • 특정 어노테이션을 가지는 모든 클래스와 그 하위 클래스의 메소드를 대상으로 지정

        • example

          • @target(org.springframework.stereotype.Repository)
            => @Repository가 붙은 모든 클래스와 그 하위 클래스의 메소드

       

      • @args

        • 특정 어노테이션을 가지는 객체를 파라미터로 받는 메소드를 대상으로 지정

        • 어노테이션은 Target ElementType이 Type이어야 함(클래스가 대상)

        • example

          • @args(javax.persistence.Entity)
            => @Entity 가 붙은 객체를 파라미터로 받는 메소드

       

      • @within

        • 특정 어노테이션을 가지는 클래스의 모든 메소드를 대상으로 지정

        • example

          • @within(org.springframework.stereotype.Service)
            => @Service 가 붙은 모든 클래스의 메소드를 대상으로 지정 (하위 클래스는 대상 외)

       

      • @annotation

        • 특정 어노테이션을 가지는 메소드를 대상으로 지정

        • example

          • @annotation(com.example.SampleProject.annotation.MyAnnotation)
            => @MyAnnotation이 붙은 모든 메소드를 대상으로 지정

     

    • PCD 는 @Pointcut 대신 Adivce 실행 시점 어노테이션(@Before, @After 등)에서도 사용 가능

     

  • 구현 절차

    • AOP 모듈로 사용할 클래스에 @Aspect, @Component 어노테이션 적용
    • Advice 메소드를 구현한 후 @Before, @After, @Around 등의 어노테이션을 사용하여
      실행 시점을 지정하고 PCD를 사용하여 Advice가 적용될 대상을 지정

     

  • 예시

    package com.example.SampleProject.service;
    
    import com.example.SampleProject.annotation.MyClassAnnotation;
    import com.example.SampleProject.annotation.MyMethodAnnotation;
    import com.example.SampleProject.domain.Category;
    import org.springframework.stereotype.Service;
    
    /* Test Service */
    @Service
    @MyClassAnnotation
    public class TestService implements TestInterface {
    
        // target 테스트
        public void doSomething(int i) {
            System.out.println("TargetTest: Do Something...");
        }
    
        // args 테스트
        public void doSomething(String name) {
            System.out.println("ArgsTest: Do Something...");
        }
    
        // @within 테스트
        public void doSomething(Long a, Long b) {
            System.out.println("@WithinTest: Do Something...");
        }
    
        // @args 테스트
        public void doSomething(Category category) {
            System.out.println("@ArgsTest: Do Something...");
        }
    
        // @annotation 테스트
        @MyMethodAnnotation
        public void doSomething(Boolean a) {
            System.out.println("@AnnotationTest: Do Something...");
        }
    }
    

     

    package com.example.SampleProject.aop;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    /* Test AOP */
    @Aspect
    @Component
    public class MyLogger {
        // Execution 테스트: 이름이 doSomething 인 모든 메소드가 각각 실행된 후에 실행
        @After("execution(* doSomething(..)) && within(com.example.SampleProject.service.TestService)")
        public void logAfterDoSomething() {
            System.out.println(">>> LogAfterDoSomething");
        }
    
        // Target 테스트:
        @Around("target(com.example.SampleProject.service.TestInterface)")
        public Object logging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            try {
                System.out.println("AOP 시작");
                return proceedingJoinPoint.proceed();
            } finally {
                System.out.println("AOP 끝\n");
            }
        }
    
        // Args 테스트: 인자로 String name 을 받는 메소드가 실행되기 전에 실행
        @Before("args(name) && within(com.example.SampleProject.service.TestService)")
        public void logBeforeArgs(String name) {
            System.out.println(">>> LogBeforeArgs: Parameter = " + name);
        }
    
        // @target 테스트: @MyClassAnnotation 이 붙은 클래스와 그 하위 클래스의 메소드가 실행된 후에 실행
        @After("@target(com.example.SampleProject.annotation.MyClassAnnotation) && within(com.example.SampleProject.service.TestService)")
        public void logAfterTargetAnnotation() {
            System.out.println(">>> LogAfterTargetAnnotation");
        }
    
        // @within 테스트: @MyClassAnnotation 이 붙은 클래스의 메소드 실행 전에 실행
        @Before("@within(com.example.SampleProject.annotation.MyClassAnnotation)")
        public void logBeforeWithinAnnotation() {
            System.out.println(">>> LogBeforeWithinAnnotation");
        }
    
        // @args 테스트: 인자로 @Entity 어노테이션이 붙은 클래스의 객체를 받는 메소드가 정상적으로 반환되는 시점에 실행
        @AfterReturning("@args(javax.persistence.Entity) && within(com.example.SampleProject.service.TestService)")
        public void logAfterReturingArgs() {
            System.out.println(">>> logAfterReturingArgs");
        }
    
        // @annotation 테스트: @MyMethodAnnotation 이 붙은 메소드가 실행되기 전에 실행
        @Before("@annotation(com.example.SampleProject.annotation.MyMethodAnnotation)")
        public void logBeforeAnnotation() {
            System.out.println(">>> LogBeforeAnnotation");
        }
    
    }
    
    

     

    package com.example.SampleProject;
    
    import com.example.SampleProject.domain.Category;
    import com.example.SampleProject.service.TestService;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    /* Test Main */
    @SpringBootTest
    class SampleProjectApplicationTests {
    	private final TestService testService;
    
    	@Autowired
    	public SampleProjectApplicationTests(
    			TestService testService
    	) {
    		this.testService = testService;
    	}
    
    	@Test
    	void contextLoads() {
    
    		// target 테스트
    		testService.doSomething(0);
    
    		// args 테스트
    		testService.doSomething("name");
    
    		// @args 테스트
    		testService.doSomething(new Category());
    
    		// @within 테스트
    		testService.doSomething(0L, 0L);
    
    		// @annotation 테스트
    		testService.doSomething(true);
    
    	}
    }
    
    

     

    === 실행결과 ===
    AOP 시작
    >>> LogBeforeWithinAnnotation
    TargetTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    >>> LogBeforeArgs: Parameter = name
    ArgsTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    @ArgsTest: Do Something...
    >>> logAfterReturingArgs
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    @WithinTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    AOP 시작
    >>> LogBeforeWithinAnnotation
    >>> LogBeforeAnnotation
    @AnnotationTest: Do Something...
    >>> LogAfterTargetAnnotation
    >>> LogAfterDoSomething
    AOP 끝
    
    • within(com.example.SampleProject.service.TestService) 와 결합하여 범위 한정
      => AOP의 대상을 너무 넓게 잡을 경우 의도치 않은 대상에게도 AOP를 적용하게됨

       

  • 하나의 대상에 적용되는 Advice 의 실행 순서

    1. @Around 실행 전 코드

    2. @Before 코드

    3. === 메소드 실행 ===

    4. @AfterReturning 코드

    5. @After 코드

    6. @Around 실행 후 코드

       

  • @Order를 사용한 실행순서 제어

    • 하나의 Advice 내에서의 실행 순서는 제어가 불가능(기본 우선순위를 따름)
    • 같은 대상에 적용될 Advice간에 임의의 순서를 지정해야한다면 다른 Aspect로 분리해야함
    • 다른 Aspect간에는 @Order를 사용하여 우선순위를 지정해줄 수 있음
    • Order안의 값이 높을수록 메소드 실행 이전에는 먼저 실행되고 실행 이후에는 나중에 실행됨
      => Order의 우선순위가 높을수록 보다 바깥의 Proxy가 되기 때문

 

  • AOP의 작동 방식

    • JDK Dynamic Proxy

      • 코드상에는 Proxy가 나타나지 않지만 Runtime에 동적으로 프록시를 생성

      • 대상과 같은 인터페이스를 구현한 Proxy 클래스를 사용

      • 인터페이스를 대상으로만 사용 가능한 방식

         

    • CGLib(Code Generation Library)

      • JDK Dynamic Proxy와 마찬가지로 Runtime에 동적으로 Advice를 적용

      • 대상을 상속하여 오버라이딩한 Proxy 클래스를 사용

      • 인터페이스가 아닌 클래스를 대상으로도 사용 가능

      • final, private 클래스는 상속이 불가능하기에 대상으로 할 수 없음

         

    • Spring에서는 기본적으로 타겟이 되는 클래스가 인터페이스를 구현하고있다면
      JDK Dynamic Proxy 방식을, 구현하고 있지 않다면 CGLib를 사용하도록 설정되어있음

       

    • aop config 파일을 작성하여 특정 방식을 강제할 수도 있음

 

'프레임워크 > Spring' 카테고리의 다른 글

#7 Spring 의 구조  (0) 2022.05.03
#6 PSA(Portable Service Abstraction)  (0) 2022.05.02
#4 Spring 의존성 주입 방법  (0) 2022.04.27
#3 Spring에 적용된 디자인 패턴  (0) 2022.04.24
#2 Spring Bean  (0) 2022.04.22

+ Recent posts