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가 붙은 모든 클래스와 그 하위 클래스의 메소드
- @target(org.springframework.stereotype.Repository)
@args
특정 어노테이션을 가지는 객체를 파라미터로 받는 메소드를 대상으로 지정
어노테이션은 Target ElementType이 Type이어야 함(클래스가 대상)
example
- @args(javax.persistence.Entity)
=> @Entity 가 붙은 객체를 파라미터로 받는 메소드
- @args(javax.persistence.Entity)
@within
특정 어노테이션을 가지는 클래스의 모든 메소드를 대상으로 지정
example
- @within(org.springframework.stereotype.Service)
=> @Service 가 붙은 모든 클래스의 메소드를 대상으로 지정 (하위 클래스는 대상 외)
- @within(org.springframework.stereotype.Service)
@annotation
특정 어노테이션을 가지는 메소드를 대상으로 지정
example
- @annotation(com.example.SampleProject.annotation.MyAnnotation)
=> @MyAnnotation이 붙은 모든 메소드를 대상으로 지정
- @annotation(com.example.SampleProject.annotation.MyAnnotation)
- @Pointcut("
- 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 의 실행 순서
@Around 실행 전 코드
@Before 코드
=== 메소드 실행 ===
@AfterReturning 코드
@After 코드
@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 |