스프링부트에서 AOP 구현은 프록시를 활용하며 @Aspect 어노테이션을 제공하여 구현 편의성을 제공해준다. 스프링부트에서 AOP 를 구현하기 위해선 @Aspect 에 대해 알아야 하며 이와 관련된 용어에 대해 숙지하고 있어야 한다.
AOP 관련 용어
- 조인 포인트(Join point)
- 어드바이스가 적용될 수 있는 위치, 메소드 실행, 생성자 호출, 필드 값 접근, static 메서드 접근 같은 프로그램 실행 중 지점
- 조인 포인트는 추상적인 개념이다. AOP를 적용할 수 있는 모든 지점이라 생각하면 된다.
- 스프링 AOP는 프록시 방식을 사용하므로 조인 포인트는 항상 메소드 실행 지점으로 제한된다.
- 포인트컷(Pointcut)
- 조인 포인트 중에서 어드바이스가 적용될 위치를 선별하는 기능
- 주로 AspectJ 표현식을 사용해서 지정
- 프록시를 사용하는 스프링 AOP는 메서드 실행 지점만 포인트컷으로 선별 가능
- 타켓(Target)
- 어드바이스를 받는 객체, 포인트컷으로 결정
- 어드바이스(Advice)
- 부가 기능
- 특정 조인 포인트에서 Aspect에 의해 취해지는 조치 Around(주변), Before(전), After(후)와 같은 다양한 종류의 어드바이스가 있음
- 어드바이저(Advisor)
- 하나의 어드바이스와 하나의 포인트 컷으로 구성
- 스프링 AOP에서만 사용되는 특별한 용어
- 위빙(Weaving)
- 포인트컷으로 결정한 타켓의 조인 포인트에 어드바이스를 적용하는 것
- 위빙을 통해 핵심 기능 코드에 영향을 주지 않고 부가 기능을 추가 할 수 있음
- AOP 적용을 위해 애스펙트를 객체에 연결한 상태
- 컴파일 타임(AspectJ compiler)
- 로드 타임
- 런타임, 스프링 AOP는 런타임, 프록시 방식
- AOP 프록시
- AOP 기능을 구현하기 위해 만든 프록시 객체, 스프링에서 AOP 프록시는 JDK 동적 프록시 또는 CGLIB 프록시이다. (인터페이스를 활용한 경우 JDK 동적 프록시, 아닌 경우 CGLIB 프록시)
AOP 예제 코드
특정 메소드 호출을 할때 로그를 남기는 기능을 AOP 를 활용하여 구현해보자.
스프링에선 프록시를 이용하여 AOP 를 구현하는데 프록시 객체 생성을 편리하기 위해 @Aspect 어노테이션을 이용하며 Pointcut, Adivce 로 이루어진 Advisor 라는 개념이 사용된다.
package com.example.demo.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
@Component
public class AspectExample {
// com.example.demo.aop.order 패키지와 하위 패키지
@Around("execution(* com.example.demo.aop.order..*.*(..))")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
execution(* com.example.demo.aop.order..*.*(..).)) : Advisor 의 Pointcut
@Around : Adivsor 의 Advice
위 코드는 아래와 같이 Pointcut 을 메소드로 만들어 사용할수도 있다.
package com.example.demo.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
@Slf4j
@Component
public class AspectExample {
// com.example.demo.aop.order 패키지와 하위 패키지
@Pointcut("execution(* com.example.demo.aop.order..*(..))") //pointcut expression
private void allOrder(){}
@Around("allOrder()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
그리고 여러 Aspect 를 순서대로 적용하고 싶다면 org.springframework.core.annotation.@Order 를 적용해야 한다. 하지만 이 경우 클래스 단위로 적용해야 하므로 별도의 클래스로 분리하여 다음과 같이 사용한다.
package com.example.demo.aop.order.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
@Slf4j
public class AspectExample {
@Aspect
@Order(2)
@Component
public static class secondAspect {
@Around("com.example.demo.aop.order.aop.Pointcuts.allOrder()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
@Component
public static class firstAspect {
@Around("com.example.demo.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());
}
}
}
}
이때까지의 예제코드에선 Advice 적용시 @Around 어노테이션만 사용하였는데 @Around 이외에도 다른 종류의 어노테이션이 더 존재한다.
어드바이스 종류
@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Around 만으로 다른 종류의 어드바이스를 모두 구현가능할것 같은데 왜 이렇게 따로 세분화 되어 있을까??
우선 @Around 와 나머지에 대한 차이는 joinPoint.proceed() 실행을 따로 호출해주거나 호출해주지 않아도 되는것이다.(@Around 의 경우 따로 호출해주어야 한다.)
그래서 @Around 에선 joinPoint.proceed() 를 호출하지 않는 실수가 발생할수도 있고 다른 어노테이션은 호출 시점이 명확하고 네이밍또한 직관적이기 때문에 유지보수와 코드 가독성에도 좋다. 이런식으로 세분화하고 제약을 두어 다른 개발자가 고민해야하는 범위와 코드 파악을 좀더 용이하게 해주는것이 좋다.
간단한 AOP 예제코드와 어드바이스의 종류에 대해서 알아보았는데 실제 실무에선 AOP 가 어떤식으로 적용되는지 코드를 통해 알아보자.
실무 예제
AOP 를 적용하기 위한 어노테이션을 작성하고 Pointcut 의 조건으로 활용한다.
@Trace 어노테이션
package com.example.demo.aop.exam.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Trace {
}
포인트컷에 @Trace 조건을 활용한 @Aspect
package com.example.demo.aop.exam.aop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Slf4j
@Aspect
public class TraceAspect {
@Before("@annotation(com.example.demo.aop.exam.annotation.Trace)")
public void doTrace(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
log.info("[trace] {} args={}", joinPoint.getSignature(), args);
}
}
AOP 적용을 위한 @Trace 활용법
package com.example.demo.aop.exam;
import com.example.demo.aop.exam.annotation.Trace;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class ExamService {
private final ExamRepository examRepository;
@Trace
public void request(String itemId) {
examRepository.save(itemId);
}
}
'IT > 스프링' 카테고리의 다른 글
[Springboot] 스프링 AOP (Aspect Oriented Programming) 에 대해서 (0) | 2023.10.10 |
---|---|
[spring] 스프링부트 어노테이션 @Bean, @Component 차이 (1) | 2023.10.02 |
[java] JPA (Java Persistence API) 란? (0) | 2023.02.06 |