'관점 지향 프로그래밍'이란 OOP로 독립적으로 분리하기 어려운 부가 기능을 모듈화하는 방식이다.
전체 로직 사이에서 핵심적인 관점(비지니스 로직), 부가적인 관점(Aspect)으로 나누어서 보고 그 관점을 기준으로 각각 모듈화(분리)함으로써 OOP를 보완하는 역할을 한다.
Object
기반으로 모듈화 (OOP)Aspect
기반으로 모듈화 (AOP)AOP는 관점 지향 프로그래밍이란 개념 그대로, Spring 프레임워크뿐만 아니라 다른 프로그래밍 환경에서도 사용될 수 있다.
Aspect (부가 기능 모듈)
ex. 핵심 비즈니스 로직 사이에서 반복되는 트랜잭션, 로깅같은 부가 기능
Aspect 의 구성은 부가될 기능을 정의한 Advice와, 해당 Advice를 어디에 적용할 지를 결정하는 Pointcut 정보를 가지고 있다.
@AspectJ
(어노테이션 기반) 두 가지 방식을 통해 Aspect를 구현할 수 있다.@Before
, @After
, @Around
(전+후) 등의 어드바이스가 존재그렇다면 AOP는 어느 시점에서 부가 기능이 실제 로직에 추가 될 수 있을까?
크게 3가지 방법이 있다.
컴파일 시점
AspectJ가 제공하는 컴파일러로 컴파일(.java
→ .class
)을 진행하며, 실제 대상 코드에 부가 기능 호출 코드가 포함된다. 런타임에는 부하가 없지만, 단, Aspect가 제공하는 컴파일러를 적용해야한다는 불편함이 있다.
로드 타임
.class
파일을 JVM의 클래스 로더에 보관하려는 시점에 조작을 한 후 후 JVM에 올리는 것이다. 컴파일 시점과 동일하게 실제 대상 코드에 부가 기능 코드가 붙게 된다. 단, 이를 위해 Java Agent 설정, Load Time Weaver 등 별도의 설정이 필요하다.
런 타임 ( ⇢ Spring 방식)
이미 실행되고 있는 시점에서 Spring 컨테이너, Proxy Bean 개념을 적용하여 실제 대상 코드는 그대로 유지되고 항상 Proxy를 통해서 부가 기능이 적용된다. 때문에 실행 시점에만 AOP를 적용할 수 있으며, 현재 우리가 사용하는 Spring AOP 방식이다.
Spring AOP 핵심은 다음과 같이 크게 세가지로 요약할 수 있다.
- Spring AOP는 Proxy 기반의 AOP 프레임워크다.
- Spring AOP는 Spring IoC 컨테이너와 함께 사용된다.
- Proxy 기반의 Weaving은 Runtime weaving이다.
Spring AOP는 기존 실제 객체 대신 Proxy 기반의 객체를 통해 동작하게 된다.
Proxy 객체는 Aspect를 대신 수행하기 위해 생성된 객체로, 내부에 Advisor와 실제 호출해야할 대상 객체(target)을 알고 있다.
스프링은 프록시를 통해 횡단 관심 객체와 핵심 관심 객체의 느슨한 결합 구조를 만들고, 부가 기능 로직 구현에 대한 유연성 및 확장성 제공한다.
이러한 프록시 객체를 사용하기 위해선, Spring IoC 컨테이너에서 원본 객체 대신 프록시 객체가 Bean으로 등록되어야한다.
이때 스프링 부트는 자동으로 빈 후처리기(AnnotationAwareAspectJAutoProxyCreator
)를 통해서 빈 저장소에 실제 객체
대신 ⇢ 프록시 객체
를 스프링 빈(Bean) 으로 등록해준다.
정확히는, 빈 후처리기는 자동으로 Advisor, @Aspect를 인식해서 필요한 곳(Pointcut)에 프록시를 만들고 AOP를 적용해준다. 프록시 적용 대상이 아니라면 원본 객체가 스프링 빈으로 등록되며 무분별한 프록시 생성 비용 낭비를 막는다.
때문에 우리는 이제 Advisor (PointCut + Advice) 만 스프링 빈으로 등록하면 되는데, 이를 @Aspect
어노테이션을 통해 자동으로 등록할 수 있다.
(만약 빈 후처리기가 없다면 우리는 매번 프록시 객체를 생성하거나, 컴포넌트 스캔에 의해서 프록시 객체가 아닌 원본 객체가 스프링 빈으로 자동으로 등록되어 프록시 적용이 불가능했을 것이다)
AOP proxy
Proxy 객체에 대한 자세한 설명은 이전에 작성한 포스팅을 참고하면 된다.
Proxy 의 이유, 원리에 대해 알아야 Spring AOP 원리를 이해할 수 있다.
Spring AOP에선 1. XML(스키마 기반), 2. @AspectJ
(어노테이션 기반) 두 가지 방법으로 Aspect를 구현할 수 있다.
하지만 XML 방식은 표현의 제한, 하나의 설정으로 관리 등이 어려우므로 먼저 @AspectJ
방식으로 살펴보겠다.
@Aspect
: Advisor (Advice + PointCut) 생성 기능 지원@Aspect
어노테이션을 사용한 클래스는 Advisor로 인식되어 자동으로 빈 후처리기를 통해 Proxy 객체를 생성하고 AOP를 적용한다.@Aspect
어노테이션을 통해 Advisor가 스프링 빈으로 등록되는데, 그럼 스프링에서 해당 Advisor를 인식하여 자동으로 빈 후처리기를 통해 실제 객체 대신 Proxy 객체를 생성하고 알아서 AOP를 적용해준다.원리를 이해하기 전에, Spring AOP를 어떻게 사용할 수 있는지 살펴보자.
다음은 서비스의 모든 Service 메소드 시작 전 print 로그를 남겨보는 부가 기능에 대한 예제이다.
우리는 이전에 학습했던 핸들러나 인터셉터 그리고 Proxy 등을 직접 구현할 필요 없이 Aspect 하나만 정의해주면 된다.
@RestController
@RequestMapping("/pay")
public class PayController {
@Autowired
KakaoPayService kakaoPayService;
@GetMapping("/kakao")
public void kakaoPay(){
kakaoPayService.pay();
}
}
@Service
public class KakaoPayService implements PayService {
@Override
public void pay(){
System.out.println("카카오페이");
}
}
@Aspect // Advisor가 Bean으로 등록되며, 알아서 Proxy 생성 및 AOP 적용
@Component
public class PayLogAspect {
@Pointcut("execution(* com.tempspring.test.pay.service.*.*(..))")
private void services() {}
@Before("services()")
public void serviceLog(JoinPoint joinPoint) {
String className = joinPoint.getTarget().getClass().getName();
System.out.println(className + " 실행");
}
}
Spring @Transactional
은 말그대로 AOP를 적용하여 트랜잭션 프록시를 통해 트랜잭션을 구현한 방법이다.
스프링이 트랜잭션 AOP를 처리하기 위한 모든 기능을 제공한다.
스프링 부트를 사용 면 트랜잭션 AOP를 처리하기 위해 필요한 스프링 빈들도 자동으로 등록해준다.
public class TransactionProxy {
private PayService target;
public void logic() {
TransactionStatus status = transactionManager.getTransaction(..); //TM을 통한 트랜잭션 시작
try {
target.pay(); //실제 대상 호출 (= 비즈니스로직)
transactionManager.commit(status); //성공시 커밋 (= 부가로직)
} catch (Exception e) {
transactionManager.rollback(status); //실패시 롤백 (= 부가로직)
throw new IllegalStateException(e);
}
}
}
위 코드처럼 트랜잭션 프록시는 말 그대로 트랜잭션 시작,커밋,롤백 등의 로직이 부가로직으로 작성되어있고 target을 통해 실제 비즈니스 로직을 호출한다.
이제 Spring @Transactional
에서 AOP가 어떻게 적용되어있는지 내부를 간단하게 살펴보자.
https://mangkyu.tistory.com/312
@Cacheable
, @CacheEvict
, @CachePut
등의 어노테이션을 사용하여 대상 메서드에 캐싱 동작을 적용한다.Spring @Cacheable 에서 AOP가 어떻게 적용되어있는지 내부를 간단하게 살펴보자.
https://alwayspr.tistory.com/42
Spring은 AOP를 적용할 때, ProxyFactory를 통해 하나의 프록시에 여러 어드바이저를 적용할 수 있다.
하나의 target에 여러 AOP를 동시에 적용한다고해도, 최적화를 통해 target마다 하나의 proxy만 생성하고 하나의 proxy에 여러 어드바이저를 적용한다.
프록시 팩토리에 각각의 target과 advisor를 등록해서 프록시를 생성한다. 그리고 생성된 프록시를 스프링 빈으로 등록한다.
이때 프록시 팩토리에서는 자동으로 인터페이스가 존재하면 JDK 동적 프록시를 적용하고, 구현체가 있으면 CGLIB를 적용한다.
다음과 같이 포인트컷은 크게 ClassFilter, MethodMatcher 둘로 이뤄져있다. 이때 둘다 true를 반환해야 Advice(부가기능)를 적용할 수 있다.
ClassFilter
에서는 클래스가 맞는지 확인한다MethodMatcher
에서는 메서드가 맞는지 확인한다1. 만약 AOP 기능을 제공하는 프레임워크/라이브러리가 없다면?
2. AOP 기능을 제공하는 프레임워크/라이브러리를 사용한다면?
번거로운 프록시 클래스 작성없이 핵심 비즈니스 로직에서 부가 기능 관심사(ex.트랜잭션)를 간편하게 분리할 수 있다.
더불어 다양한 클래스가 Aspect를 재활용하며 공통 사용할 수 있다.
이제 위에서 학습했던 내용을 바탕으로 직접 AOP를 적용해보자
목표는 @Slack
어노테이션을 통해 Exception 발생시 slack으로 알람을 가게 하는 것이다.
- 이를 통해 Exception이 발생하면 개발자가 서버 접속 없이도 바로 로그를 확인할 수 있고 장애 대응이 가능하다.
- AOP를 적용함으로써 매번 Slack 알람을 불필요하게 핵심 비즈니스 로직 사이에서 붙이지 않아도 된다.
아래 코드는 실제 실무에서 적용했던 코드를 local에서 다시 작성하였다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Slack {
}
[참고]
https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/
https://seongmun-hong.github.io/spring/Spring-Aspect-Oriented-Programming(AOP)(1)
https://seongmun-hong.github.io/spring/Spring-Aspect-Oriented-Programming(AOP)(2)
https://seongmun-hong.github.io/spring/Spring-Aspect-Oriented-Programming(AOP)(3)![업로드중..](blob:https://velog.io/01072c74-df60-490c-be6d-2c886a03ded4)
https://gmoon92.github.io/spring/aop/2019/03/01/spring-aop-choosing.html