[Spring] AOP 적용
애플리케이션 로직은 핵심 기능과 부가 기능으로 나뉜다.
트랜잭션 관리, 보안, 로깅 등 부가기능은 애플리케이션 전반에 걸쳐 사용되는 횡단 관심사이고, 횡단 관심사를 효과적으로 처리하기 위해 AOP 기술이 도입됐다.
AOP는 부가 기능을 핵심 기능에서 분리하고 한 곳에서 통합해서 관리한다.
부가 기능과 부가 기능을 어디에 적용할 지 선택하는 기능을 Aspect 라고 부르고, @Aspect 애너테이션으로 사용한다.
AOP는 Aspect Oriented Programming 의 약자로 이름 그대로 애플리케이션을 바라보는 관점을 기능에서 횡단 관심사로 옮겨서 바라보는 패러다임이고, OOP의 부족한 부분을 보조하는 목적으로 도입됐다.
AOP의 구현 중 AspectJ 프레임워크가 있다.
스프링도 AspectJ의 문법을 사용하니 AOP를 적용할 때는 AspectJ를 사용하자.
이전까지 부가 기능을 추가할 때는 프록시를 사용해 런타임 시점에 부가 기능을 넣어 주는 방식을 사용했는데, 이 방법 외에도 컴파일 시점과 클래스 로딩 시점에도 부가 기능을 넣어 줄 수 있다.
자바로 작성한 소스코드를 컴파일러가 해석해 .class 확장자인 바이트코드를 만들어 내는 시점에 부가 기능 로직을 추가할 수 있다.
이 때 AspectJ가 제공하는 특별한 컴파일러를 사용한다.
AspectJ 컴파일러는 Aspect를 확인해 해당 클래스가 부가 기능 적용 대상인지 확인하고 부가 기능 로직을 적용한다.
이렇게 원본 로직에 부가 기능 로직이 추가하는 작업을 위빙이라고 부른다.
가능하긴 하지만 AspectJ가 제공하는 컴파일러를 사용해야 하고 사용이 복잡해서 잘 사용하지 않는다.
바이트코드인 .class 파일은 JVM의 클래스 로더에 보관된다.
(클래스 로더는 바이트코드를 JVM 메모리에 로드하고 링킹 및 초기화 작업을 수행한다)
자바 언어는 바이트코드를 JVM에 저장하기 전 조작할 수 있는 기능을 제공하고, 이 기능을 사용해 JVM 클래스 로더에 바이트코드를 올리는 시점에 바이트코드를 조작해 부가 기능을 수행하도록 할 수 있다. (모니터링 툴이 이 방법을 사용한다)
역시 사용하기 번거롭기 운영하기 어려워 잘 사용하지 않는다.
스프링 AOP는 프록시를 통해 부가 기능을 적용하는 방식을 사용한다.
프록시는 메서드 오버라이딩 개념으로 동작하기에 static 메서드와 생성자 접근 등 프록시가 접근하기 힘든 부분에서는 AOP를 적용할 수 없어 AOP의 조인 포인트는 스프링 컨테이너가 관리하는 스프링 빈에 대한 메서드 실행으로 제한된다.
스프링은 AspectJ 문법을 차용하고 프록시 방식의 AOP를 사용하는거지 AspectJ 프레임워크를 그대로 사용하는건 아니다.
AspectJ 프레임워크를 직접 사용하면 다양한 기능을 사용할 수 있지만, 설정이 복잡하고 사용하기 힘들기에 편하게 사용할 수 있는 스프링 AOP를 사용하자.
@Around("execution(* start.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around 애너테이션의 속성값은 포인트컷이고, doLog 메서드는 어드바이스이다.
@Around 애너테이션으로는 포인트컷과 어드바이스를 모두 지정할 수 있다.
AspectJ 가 제공하는 애너테이션으로, 스프링이 해당 부분을 가져와서 사용하는거지 실제로 AspectJ 프레임워크를 사용하지는 않는다.
해당 메서드를 스프링 빈으로 등록해야 함을 기억하자.
@Pointcut("execution(* start.aop.order..*(..))")
private void allOrder(){}
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@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());
}
}
@Pointcut 애너테이션으로 포인트컷과 어드바이스를 분리해서 사용할 수 있다.
allOrder를 포인트컷 시그니처라고 부른다.
하나의 포인트컷 표현식을 여러 어드바이스에서 같이 사용할 수 있다.
(public으로 설정 시 다른 클래스에서도 사용할 수 있고, 이 경우 패키지명을 포함해서 작성해 줘야 한다)
예시처럼 하나의 어드바이스에 여러 포인트컷을 적용할 수 있지만, 이 경우 순서를 보장할 수 없다.
@Slf4j
public class Aspects {
@Aspect
@Order(2)
public static class LogAspect {
@Around("start.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());
}
}
}
}
@Order 애너테이션으로 순서를 지정하는건 @Aspect 단위로만 가능하다. (숫자가 낮을 수록 우선된다)
@Aspect 애너테이션은 클래스 단위로 사용되니 순서 적용을 위해 포인트컷을 클래스 단위로 모듈화했다.
어드바이스를 지정하는 방법으로는 @Around 말고 다른 방법도 있다.
@Before, @AfterReturning, @AfterThrowing, @After 애너테이션으로도 어드바이스를 지정할 수 있지만..
@Around 애너테이션이 가장 강력하고 이 애너테이션으로 충분하다.
@Aspect
public class AspectV6Advice {
@Around("start.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
//@Before
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
//@AfterReturning
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
//@AfterThrowing
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
//@After
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
@Before("start.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "start.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "start.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", ex);
}
@After(value = "start.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
@Around 를 제외한 나머지 어드바이스들은 @Around가 할 수 있는 일부만 제공한다.
따라서 @Around 어드바이스만으로 모든 기능을 구현할 수는 있지만...
@Around 애너테이션으로 어드바이스를 작성하고 어드바이스 내부에서 타겟을 호출하지 않으면 부가기능만 실행하고 주요기능을 실행하지 않는 오류가 발생한다.
다른 애너테이션들은 jointPoint.proceed() 메서드로 타겟을 호출하는 작업을 고려하지 않아도 알아서 실행된다.
제약을 통해 실수를 방지하기 위해 @Around 외의 어드바이스 애너테이션이 도입됐다.
'Spring > Spring' 카테고리의 다른 글
[Spring] AOP 주의사항 (0) | 2023.07.29 |
---|---|
[Spring] 포인트컷 지시자 (0) | 2023.07.28 |
[Spring] 빈 후처리기 (0) | 2023.07.26 |
[Spring] 프록시 팩토리 (0) | 2023.07.24 |
[Spring] 동적 프록시 기술 (0) | 2023.07.23 |
댓글
이 글 공유하기
다른 글
-
[Spring] AOP 주의사항
[Spring] AOP 주의사항
2023.07.29 -
[Spring] 포인트컷 지시자
[Spring] 포인트컷 지시자
2023.07.28 -
[Spring] 빈 후처리기
[Spring] 빈 후처리기
2023.07.26 -
[Spring] 프록시 팩토리
[Spring] 프록시 팩토리
2023.07.24