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 |