AOP
애스펙트를 사용한 프로그래밍 방식을 관점 지향 프로그래밍 AOP(Aspect-Oriented Programming)이라 한다.
참고로 AOP는 OOP를 대체하기 위한 것이 아니라 횡단 관심사를 깔끔하게 처리하기 어려운 OOP의 부족한 부분을 보조하는 목적으로 개발되었다.
횡단 관심사: 여러 기능들 사이에 걸쳐서 들어가는 관심사
AspectJ 프레임워크
AOP의 대표적인 구현으로 스프링도 AOP를 지원하지만 대부분 AspectJ의 문법을 차용하고 AspectJ가 제공하는 일부 기능만 제공한다.
기능
횡단 관심사의 깔끔한 모듈화
- 오류 검사 및 처리
- 동기화
- 성능 최적화(캐싱)
- 모니터링 및 로깅
AOP를 적용 방식
크게 3가지 (컴파일 시점, 클래스 로딩 시점, 런타임 시점)으로 나뉘는데, 런타임 시점이 프록시를 사용하는 방법이다.
스프링은 프록시를 사용하여 AOP를 적용하고 실무에서는 스프링이 제공하는 AOP 기능만으로 대부분의 문제를 해결할 수 있어 해당 AOP 기능에 대해 더 다뤄볼 예정이다.
AOP 용어 정리
- 조인 포인트(Join point)
- 조인 포인트는 추상적인 개념이다. AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.
- 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점으로 제한된다.
- 포인트컷(Pointcut)
- 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
- 주로 AspectJ 표현식을 사용해서 지정
- 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷으로 선별 가능
- 타켓(Target)
- 어드바이스를 받는 객체, 포인트컷으로 결정
- 어드바이스(Advice)
- 부가 기능
- Around(주변), Before(전), After(후)와 같은 다양한 종류의 어드바이스가 있음
- 애스펙트(Aspect)
- 어드바이스 + 포인트컷을 모듈화 한 것
- @Aspect 를 생각하면 됨
- 여러 어드바이스와 포인트 컷이 함께 존재
- 어드바이저(Advisor)
- 하나의 어드바이스와 하나의 포인트 컷으로 구성
- 스프링 AOP에서만 사용되는 특별한 용어
- 위빙(Weaving)
- 타켓의 조인 포인트에 어드바이스를 적용하는 것
- 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가 할 수 있음
- AOP 프록시
- AOP 기능을 구현하기 위해 만든 프록시 객체, 스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시이다.
@Aspect
편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저를 생성
이렇게 생성된 어드바이저는 자동 프록시 생성기를 통해 자동으로 스프링 빈으로 등록되어 포인트컷에 매칭되는 빈들에 자동으로 프록시를 적용해준다.
구현
@Around
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
- @Around 의 값에 포인트컷 표현식을 넣는다. 표현식은 AspectJ 표현식을 사용한다.
- @Around 의 메서드는 어드바이스( Advice )가 된다.
- ProceedingJoinPoint joinPoint: 내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와 어떤 메서드가 호출되었는지 정보가 포함
- joinPoint.proceed(): 실제 호출 대상( target )을 호출
@Pointcut 분리
@Aspect
public class AspectV3 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {
}
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService() {
}
//hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
어드바이스 적용 순서
어드바이스는 기본적으로 순서을 보장하지 않는데 지정해주기 위해 @Order 애노테이션을 적용해야 한다.
@Order는 클래스 단위로 적용할 수 있기 때문에 class를 여러 개 생성해줘야 한다.
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws
Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
}
어드바이스 종류
@Before
조인 포인트 실행 이전에 실행
- @Around 와 다르게 작업 흐름을 변경할 수는 없다.
- @Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출.
- @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출
@AfterReturning
메서드 실행이 정상적으로 반환될 때 실행
- returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
- returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행
- @Around 와 다르게 반환되는 객체를 변경할 수는 없다
@AfterThrowing
메서드 실행이 예외를 던져서 종료될 때 실행
- throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
- throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행
@After
메서드 실행이 종료되면 실행된다. (finally를 생각하면 된다.)
- 정상 및 예외 반환 조건을 모두 처리한다.
- 일반적으로 리소스를 해제하는 데 사용
@Around
메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행
모든 어드바이스는 JoinPoint를 파라미터로 사용할 수 있지만 @Around는 ProceedingJoinPoint를 사용해야 한다.
JoinPoint 인터페이스의 주요 기능
- getArgs() : 메서드 인수를 반환
- getThis() : 프록시 객체를 반환
- getTarget() : 대상 객체를 반환
- getSignature() : 조언되는 메서드에 대한 설명을 반환
- toString() : 조인되는 방법에 대한 유용한 설명을 인쇄
ProceedingJoinPoint 인터페이스의 주요 기능
- proceed() : 다음 어드바이스나 타켓을 호출
구현
@Aspect
public class AspectV6Advice {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//@Before
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
}
}
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(),
ex.getMessage());
}
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
@Around를 통해 모든 어드바이스를 처리할 수 있지만 여러 어드바이스를 두는 이유
@Around 가 가장 넓은 기능을 제공하는 것은 맞지만, 항상 joinPoint.proceed()를 호출해야 하기 때문에실수할 가능성이 있다. 반면에 @Before , @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다.
더욱 중요한 것은 의도가 명확하게 드러난다는 점이다. @Before에 경우 타겟을 실행하기 전에 실행한다는 것을 더욱 명확하게 확인할 수 있다. 즉 제약을 걸어주는 것이다
좋은 설계는 제약이 있는 것이다.
제약은 실수를 미연에 방지한다.
'Study > Spring' 카테고리의 다른 글
스프링 AOP의 문제점 및 한계 (0) | 2023.10.14 |
---|---|
빈 후처리기(Bean PostProcessor)와 어드바이저 (1) | 2023.10.11 |
리플렉션과 프록시 팩토리 (0) | 2023.10.11 |
PRG Post/Redirect/Get (0) | 2023.08.22 |
HTTP 헤더, 파라미터, 바디 조회하는 방법 (0) | 2023.08.21 |