Spring AOP

Kim, Beomgoo·2023년 5월 13일
1
post-thumbnail

AOP (Aspect Oriented Programming)

AOP란?

  • 비즈니스 로직과 같은 핵심기능들은, 객체로서 고유한 역할과 책임을 가지며 다른 객체들과 협력할 수 있으므로 객체지향(OOP) 기법을 통해 모듈화가 가능하다.
  • 하지만 로깅, 트랜잭션 등과 같은 부가기능은 핵심기능 실행 전이나 실행 후, 그리고 여러 핵심기능에 적용된다.
    • 어플리케이션 전반적으로 흩어져 있기 때문에 전통적인 객체지향 기법으로는 모듈화가 불가능하다.
  • 이러한 부가기능들을 객체(Object)가 아닌 Aspect라는 모듈로 분리하여 설계하고 개발하는 기법을 Aspect Oriented Programming(AOP)이라고 한다.

모식도

AOP를 추상화하여 표현하자면, 부가기능 X, Y, Z를 핵심 로직을 담당하는 클래스 A, B, C에 적용할 것만 적용하는 위 그림처럼 나타낼 수 있다.

Proxy Pattern

프록시(Proxy)

마치 자신이 실제 호출되려는 대상(핵심기능)인 것처럼 위장해서 호출자의 요청을 Intercept하는 것이다.
여기서 프록시는 부가 기능만 실행하고, 실제 핵심 기능에 대한 실행은 Target에 위임하여 계속 실행하도록 한다.

Spring AOP에서의 프록시 패턴


Aspect가 적용된 Bean을 생성하고 사용할 때, 겉으로 보기에 우리는 우리가 만들고 등록한 Bean을 사용하는 것처럼 보인다. 하지만 실제로는 그렇지 않다. Spring AOP는 Bean에 대한 프록시를 동적으로 생성해 빈으로 등록하고, 실제 우리가 Bean으로 쓰고자 했던 Target을 프록시 빈에 연결한다. 그래서 우리가 Bean의 메소드를 호출하는 것은 프록시의 메소드를 호출하고, 프록시의 메소드가 타겟의 메소드를 호출하는 방식으로 작동한다.

Spring AOP (AspectJ)

Spring Framework는 Spring Boot를 통해 매우 간단하고 쉽게 AOP를 사용할 수 있는 방법을 제공한다.

Dependency

build.gradle에 다음과 같이 dependency를 추가한다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-aop'
}

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)

부가기능을 적용할 대상을 지정할 때 포인트컷을 사용한다. 포인트컷의 예시는 다음과 같다.

@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에 부가기능을 적용한다.

표현식은 다음의 요소들을 포함할 수 있다. 자세한 표현식 작성법은 생략한다.

  • 메소드의 리턴 타입
  • 패키지
  • 클래스
  • 메소드 이름
  • 메소드의 매개변수

어드바이스(Advice)

부가기능으로 작동할 메소드이다. 다음의 어노테이션들을 통해 동작 시점을 지정할 수 있다.

  • 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");
        }
    }

조인포인트(JoinPoint)

어플리케이션에서 호출하고자 하는 비즈니스 로직이다. 부가기능(Aspect)이 적용되는 대상이라고 볼 수 있다.

profile
하나에 하나를 보탠다

1개의 댓글

comment-user-thumbnail
2023년 5월 14일

코드가 참 익숙하네요 ^^

답글 달기