반응형
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 |