AOP를 추상화하여 표현하자면, 부가기능 X, Y, Z를 핵심 로직을 담당하는 클래스 A, B, C에 적용할 것만 적용하는 위 그림처럼 나타낼 수 있다.
마치 자신이 실제 호출되려는 대상(핵심기능)인 것처럼 위장해서 호출자의 요청을 Intercept하는 것이다.
여기서 프록시는 부가 기능만 실행하고, 실제 핵심 기능에 대한 실행은 Target에 위임하여 계속 실행하도록 한다.
Aspect가 적용된 Bean을 생성하고 사용할 때, 겉으로 보기에 우리는 우리가 만들고 등록한 Bean을 사용하는 것처럼 보인다. 하지만 실제로는 그렇지 않다. Spring AOP는 Bean에 대한 프록시를 동적으로 생성해 빈으로 등록하고, 실제 우리가 Bean으로 쓰고자 했던 Target을 프록시 빈에 연결한다. 그래서 우리가 Bean의 메소드를 호출하는 것은 프록시의 메소드를 호출하고, 프록시의 메소드가 타겟의 메소드를 호출하는 방식으로 작동한다.
Spring Framework는 Spring Boot를 통해 매우 간단하고 쉽게 AOP를 사용할 수 있는 방법을 제공한다.
build.gradle
에 다음과 같이 dependency를 추가한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
SpringBoot 어플리케이션 파일에서 어노테이션을 통해 AspectJ를 사용할 것을 명시한다.
@SpringBootApplication
@EnableAspectJAutoProxy
public class ExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ExampleApplication.class, args);
}
}
Aspect로 사용할 클래스를 다음과 같이 만든다.
@Component
@Aspect
public class ExampleAspect {
}
Aspect는 아래에서 설명할 포인트컷과 어드바이스의 결합이라고 보면 된다.
Aspect = Pointcut + Advice
부가기능을 적용할 대상을 지정할 때 포인트컷을 사용한다. 포인트컷의 예시는 다음과 같다.
@Pointcut("within(grimuri.backend.domain.diary.controller..*)")
public void onRequest() {}
@Pointcut("execution(* grimuri.backend.domain.user.controller.UserControllerImpl.*(..))"
public void onSignupAndLogin() {}
포인트컷 어노테이션 내부 지시자와 표현식으로 어떤 메소드에 부가기능을 적용할 지 정확히 지정할 수 있다.
지시자는 다음의 세 가지를 이용할 수 있다.
execution
.within
.bean
. Spring Bean의 이름을 통해 해당 Bean에 부가기능을 적용한다.표현식은 다음의 요소들을 포함할 수 있다. 자세한 표현식 작성법은 생략한다.
부가기능으로 작동할 메소드이다. 다음의 어노테이션들을 통해 동작 시점을 지정할 수 있다.
Before
. 대상 메서드 실행 이전에 동작한다.After
. 대상 메서드 실행 이후에 동작한다. 예외 발생 여부와 무관하게 항상 실행된다.AfterReturning
. 대상 메서드가 정상적으로 반환된 후에 동작한다.AfterThrowing
. 대상 메서드에서 예외가 발생한 후 동작한다.Around
. 대상 메서드를 감싸는 형태로, 메서드 실행 이전과 이후 모두 동작한다.예시는 다음과 같다. 필자는 bucket4j
라이브러리를 사용해 API의 처리율 제한 로직을 어드바이스로 작성하였다.
@Around("onSignupAndLogin()")
public Object signupAndLoginLimit(ProceedingJoinPoint pjp) throws Throwable {
log.debug("\tAround: signupAndLoginLimit,\tMethod: {}", pjp.getSignature().getName());
Bucket bucket = rateLimiter.resolveBucket(pjp.getSignature(), 3L, 1L, Duration.ofMinutes(1));
if (bucket.tryConsume(1L)) {
log.debug("\t>>> Remain bucket Count : {}", bucket.getAvailableTokens());
return pjp.proceed(pjp.getArgs());
} else {
log.debug("\t>>> Exhausted Limit in Simple Bucket");
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("too many requests");
}
}
@Around("onRequest()")
public Object onRequestLimit(ProceedingJoinPoint pjp) throws Throwable {
log.debug("\tAround: onRequestLimit,\tMethod: {}", pjp.getSignature().getName());
log.debug("\targs: {}", Arrays.stream(pjp.getArgs()));
User loginUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String email = loginUser.getEmail();
Bucket bucket = rateLimiter.resolveBucket(email, pjp.getSignature(), 3L, 1L, Duration.ofMinutes(1));
if (bucket.tryConsume(1L)) {
log.debug("\t>>> Remain bucket Count : {}", bucket.getAvailableTokens());
return pjp.proceed(pjp.getArgs());
} else {
log.debug("\t>>> Exhausted Limit in Simple Bucket");
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("too many requests");
}
}
어플리케이션에서 호출하고자 하는 비즈니스 로직이다. 부가기능(Aspect)이 적용되는 대상이라고 볼 수 있다.
코드가 참 익숙하네요 ^^