반응형

AOP 용어

  • Target Object
    • 부가 기능을 부여할 대상 객체를 말함
    • 스프링에서는 주로 Service 객체가 대상이 됨
  • Aspect
    • AOP 기능을 갖는 모듈
    • 여러 객체에 공통적으로 적용되는 관심사항을 말함
    • 스프링에서는 @Aspect 어노테이션을 달아 명시
  • Advice
    • 어느 target method에 Aspect 로직을 적용할지를 명시하는 것
    • 스프링에서는 @Around 어노테이션을 달아 명시
  • Pointcut
    • 어느 target method에 Aspect 로직을 적용할지를 정의하는 표현식
    • 스프링에서는 @Around 어노테이션 값으로 들어갈 내용
  • Joint Point
    • target method의 이름, 객체 정보, 파라미터 정보 등을 가져올 수 있는 인터페이스
    • 스프링에서는 Advice 메소드의 파라미터 객체(ProceedingJoinPoint)에 해당

Proxy

  • target object와 Aspect를 하나로 합친 객체를 말한다.
  • Weaving 과정을 통해 생성됨
  • JDK Dynamic Proxy
    • Java Reflection을 이용하여 비교적 느림
    • 인터페이스를 구현한 객체를 대상으로 함
  • CGLIB Proxy
    • 스프링 부트에서는 디폴트로 사용되는 프록시 객체
    • 상속을 통해 Proxy를 구현
    • final 클래스일 경우 Proxy로 생성 불가
    • 인터페이스를 통하지 않은 일반 객체를 대상으로 함

Weaving

  • Proxy 객체를 생성하는 과정을 말한다.
  • Compile-time Weaving
    • Compile 시점에 Proxy 객체 생성
    • Load-time에 대한 절차가 없어 퍼포먼스 하락 없음
    • lombok 처럼 compile시 수행되는 plugin과 충돌 발생할 수 있음
    • AspectJ 라이브러리를 추가해서 사용하는 방식
  • Load-time Weaving
    • Load 시점에 AspectJ에 의해 Proxy 객체를 생성
    • applicationContext에 로드될 객체들이 모두 로드된 이후에 weaving을 하므로 퍼포먼스 하락이 있음
    • AspectJ 라이브러리를 추가해서 사용하는 방식
  • Run-time Weaving
    • 스프링에서는 AspectJ를 사용하지 않고 Spring AOP를 사용
    • Spring AOP는 Run-time Weaving
    • 소스코드나 클래스 정보 자체를 변경하지 않음
    • 실행 속도가 AspectJ에 비해 느림
    • 구현하기가 쉬움

AOP 작동 과정

  • Bean 생성시 @Aspect 클래스의 @Around에 매칭되는 메소드가 존재할 경우 Aspect 로직과 원본 코드를 합친 Proxy 객체를 생성.
  • 해당 Proxy 객체를 의존하는 객체에 주입.
  • Proxy 객체가 호출될 때 Aspect 로직과 함께 원본 코드를 실행.

AOP 사용시 주의사항

  • self-invocation
    • 프록시 객체의 target 메소드 안에서 또 다른 target 메소드를 실행시 AOP가 동작하지 않음
    • 자기 자신의 target 메소드를 호출하는 것은 proxy 객체를 거치는 것이 아니기 때문에 발생하는 현상이다.
    • 해당 이슈 해결 방법
      • AspectJ(non-proxy based) 방식을 사용
      • 프록시 객체 내부에서 자기 자신을 다시 getBean()해서 사용
      • target 메소드를 담는 클래스를 별도로 분리해서 Bean 등록하여 주입받아 호출

Spring AOP

  • 스프링에서 사용하는 AOP
  • CGLIB Proxy를 사용함
  • 상속을 통해 Proxy를 구현
  • final 클래스일 경우 Proxy로 생성 불가
  • private, final 메소드는 AOP 적용 불가
  • public 메소드를 대상으로만 AOP 적용 가능
  • 타겟 클래스 내에서 호출하는 타겟 메소드는 AOP 적용 불가 (self invocation)
    • 프록시 객체를 주입받아 사용해야 AOP 적용됨

Spring AOP 예제

  • dependency
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    
    dependencies {
        ...
        implementation 'org.springframework.boot:spring-boot-starter-aop'        
    }
    
  • Main
    @EnableAspectJAutoProxy
    @SpringBootApplication
    public class SpringAopApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringAopApplication.class, args);
        }
    
    }
    
  • ProcessTimeLogging
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ProcessTimeLogging {
        String value() default "unknown";
    }
    
  • ProcessTimeAspect
    @Slf4j
    @Aspect
    @Component
    public class ProcessTimeAspect {
        @Around("@annotation(ProcessTimeLogging)")
        public Object loggingProcessTime(ProceedingJoinPoint joinPoint) throws Throwable {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
    
            ProcessTimeLogging processTimeLogging = method.getAnnotation(ProcessTimeLogging.class);
            String name = processTimeLogging.value(); // 어노테이션 값
    
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
    
            Object result = joinPoint.proceed();
    
            stopWatch.stop();
    
            log.info("process time : {}s", stopWatch.getTotalTimeSeconds());
    
            return result;
        }
    }
    
  • ExampleService
    @Service
    public class ExampleService {
        @ProcessTimeLogging
        public String getMessage() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello World";
        }
    }
    
  • AopTest
    @Slf4j
    @SpringBootTest
    public class AopTest {
        @Autowired
        private ExampleService exampleService;
    
        @Test
        public void test() {
            log.info("message : {}", exampleService.getMessage());
        }
    }
    

궁금증 1 - AOP 적용된 객체는 타입도 다를까?

  • 객체명 출력 코드 추가
    @Slf4j
    @SpringBootTest
    public class AopTest {
        @Autowired
        private ExampleService exampleService;
    
        @Test
        public void test() {
            log.info("message : {}", exampleService.getMessage());
            log.info("class name : {}", exampleService.getClass().getSimpleName());
        }
    }
    
  • AOP 미적용시
    @Service
    public class ExampleService {
        // @ProcessTimeLogging
        public String getMessage() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello World";
        }
    }
    
    message : Hello World
    class name : ExampleService
    
  • AOP 적용시
    @Service
    public class ExampleService {
        @ProcessTimeLogging
        public String getMessage() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello World";
        }
    }
    
    process time : 1s
    message : Hello World
    class name : ExampleService$$EnhancerBySpringCGLIB$$cf9a38ac
    
  • 결론
    • 다르다
    • AOP 미적용 객체는 자기자신 클래스 타입을 갖음
    • AOP 적용 객체는 CGLIB Proxy 처리된 타입을 갖음

궁금증 2 - final 클래스에 AOP를 적용하려고 하면 어떻게될까?

  • AOP 대상 클래스를 final로 수정
    @Service
    public final class ExampleService {
        @ProcessTimeLogging
        public String getMessage() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello World";
        }
    }
    
  • 스프링 실행
    Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class com.example.springexample.ExampleService: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class com.example.springexample.ExampleService
    	at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:208) ~[spring-aop-5.3.1.jar:5.3.1]
    	at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110) ~[spring-aop-5.3.1.jar:5.3.1]
    
  • 결론
    • 실행 단계에서 AOP 설정 오류가 발생함

궁금증 3 - final 메소드에 적용하려고 하면?

  • final 메소드에 AOP 적용
    @Service
    public class ExampleService {
        @ProcessTimeLogging
        public final String getMessage() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "Hello World";
        }
    }
    
    message : Hello World
    class name : ExampleService$$EnhancerBySpringCGLIB$$333c0352
    
  • 결론
    • 스프링은 잘 실행됨
    • 대상 객체는 Proxy 처리됨
    • 대상 메소드는 AOP 로직이 적용 안됨

궁금증 4 - private 메소드에 적용하려고 하면?

  • private 메소드 AOP 적용
    @Service
    public class ExampleService {
        public String getMessage() {
            sleep();
            return "Hello World";
        }
    
        @ProcessTimeLogging
        private void sleep() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    message : Hello World
    class name : ExampleService$$EnhancerBySpringCGLIB$$204f16bb
    
  • 결론
    • 스프링은 잘 실행됨
    • 대상 객체는 Proxy 처리됨
    • 대상 메소드는 AOP 로직이 적용 안됨

궁금증 5 - AOP 대상 메소드 내부에서 호출되는 private 메소드는 AOP 로직에 영향을 끼칠까?

  • public 메소드에 적용, 내부에서 private 메소드 호출
    @Service
    public class ExampleService {
        @ProcessTimeLogging
        public String getMessage() {
            sleep();
            return "Hello World";
        }
    
        private void sleep() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    process time : 1s
    message : Hello World
    class name : ExampleService$$EnhancerBySpringCGLIB$$3532a423
    
  • 결론
    • AOP 로직 잘 작동하고, private 메소드는 AOP 로직에 영향을 끼침

궁금증 6 - 타겟 클래스 내에서 호출하는 타겟 메소드는 AOP 적용이 정말 안될까?

  • 타겟 클래스 내에서 타겟 메소드 호출
    @Service
    public class ExampleService {
        @ProcessTimeLogging
        public String getMessage() {
            sleep(1000);
            sleepMoreWithAop();
            return "Hello World";
        }
    
        @ProcessTimeLogging
        public void sleepMoreWithAop() {
            sleep(2000);
        }
    
        private void sleep(int millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    process time : 3s
    message : Hello World
    class name : ExampleService$$EnhancerBySpringCGLIB$$e7b3c1b3
    
  • 두 타겟 메소드를 다른 클래스로 분리하여 한 객체에 주입받아 사용
    @AllArgsConstructor
    @Service
    public class ExampleService {
        private final ExampleService2 exampleService2;
    
        @ProcessTimeLogging
        public String getMessage() {
            sleep(1000);
            exampleService2.sleepMoreWithAop();
            return "Hello World";
        }
    
        public static void sleep(int millis) {
            try {
                Thread.sleep(millis);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    @Service
    public class ExampleService2 {
        @ProcessTimeLogging
        public void sleepMoreWithAop() {
            ExampleService.sleep(2000);
        }
    }
    
    process time : 2s
    process time : 3s
    message : Hello World
    class name : ExampleService$$EnhancerBySpringCGLIB$$5c96befa
    
  • 결론
    • 타겟 클래스의 메소드 안에서 호출하는 또 다른 타겟 메소드는 AOP 적용이 되지 않음
    • Spring AOP는 프록시 객체를 주입받은 객체에서만 AOP 적용이 됨

pointcut 표현식

  • execution(public * *(..)) : public 메소드 실행
  • execution(* set*(..)) : 이름이 set으로 시작하는 모든 메소드명 실행
  • execution(* com.xyz.service.AccountService.*(..)) : AccountService 인터페이스의 모든 메소드 실행
  • execution(* com.xyz.service..(..)) : service 패키지의 모든 메소드 실행
  • execution(* com.xyz.service...(..)) : service 패키지와 하위 패키지의 모든 메소드 실행
  • within(com.xyz.service.*) : service 패키지 내의 모든 결합점 (클래스 포함)
  • within(com.xyz.service..*) : service 패키지 및 하위 패키지의 모든 결합점 (클래스 포함)
  • bean(*Repository) : 이름이 “Repository”로 끝나는 모든 빈
  • bean(*) : 모든 빈
  • bean(account*) : 이름이 'account'로 시작되는 모든 빈
  • bean(*dataSource) || bean(*DataSource) : 이름이 “dataSource” 나 “DataSource” 으로 끝나는 모든 빈

참고

반응형

'Development > Spring' 카테고리의 다른 글

[Spring] Distributed Lock (with MySQL, Redis)  (4) 2020.12.27
[Spring] Transactional  (0) 2020.12.27
[Spring] Cookie & Session  (0) 2020.12.27
[Spring] Cache (with Redis)  (0) 2020.12.27
[Spring] Swagger  (0) 2020.12.27

+ Recent posts