춤추는 개발자

[Spring] 완전한 AOP 솔루션을 제공하는 AspectJ 본문

Developer's_til/스프링 프레임워크

[Spring] 완전한 AOP 솔루션을 제공하는 AspectJ

Heon_9u 2023. 10. 25. 20:06
728x90
반응형

✅ 배경

 실무에서는 로깅, 파일 처리 등 비즈니스 로직 외에 다양한 코드를 접할 수 있다. 현업에서 API에 녹아있는 코드를 보면 회사 정책에서 요구하는 로그를 객체에 담아 파일로 보관하는 로직을 접했는데 메서드마다 비슷한 코드가 반복됐다. 이러한 상황을 AspectJ 기반으로 리팩토링하여 유지보수성을 높이기로 계획했다.

 

✅ AspectJ

 AspectJ는 스프링 AOP와는 다르게 모든 도메인 객체에 적용할 수 있다는 장점이 있다. 다만, 둘은 다른 목표와 특징을 갖고 있다.

 

[ 기능 및 목표 ]

Simply put, Spring AOP and AspectJ have different goals.

Spring AOP는 프로그래머가 직면하는 가장 흔한 문제를 해결하기 위해 Spring IoC 전반에 걸쳐 간단한 AOP 구현을 제공하는 것이 목표, Spring 컨테이너에 의해 관리되는 빈에만 적용가능

AspectJ는 완전한 AOP 솔루션을 제공하는 것을 목표로 하는 독창적인 AOP 기술, Spring AOP보다 강력하지만 훨씬 더 복잡하다. AspectJ가 모든 도메인 객체에 적용가능

 

이외에도 공통 관심사에 해당하는 로직을 특정 객체에 연결 및 동작하는 유형 등이 다르다. 자세한 사항은 아래 URL의 표를 참고하길 바란다.

https://www.baeldung.com/spring-aop-vs-aspectj

 

Comparing Spring AOP and AspectJ | Baeldung

See advantages and disadvantages of Spring AOP and AspectJ.

www.baeldung.com

 

✅ 용어 정리

[ Pointcut의 표현식과 방법 ]

AOP 적용이 가능한 모든 비즈니스 메소드(JoinPoint) 중에서 조건을 통해 특정 부분에 공통 관심사(Advice)를 적용하기 위해 사용한다. @Pointcut을 적용하는 종류는 아래와 같으며, 실습으로 execution과 annotation을 활용할 것이다.

 

포인트컷 지시자의 종류

  • execution: 메소드 조인 포인트를 매칭. 부모 타입 지정 시, 자식 타입도 대상이 된다.
  • within: 특정 타입(클래스) 내의 조인 포인트를 매칭.
  • args: 파라미터가 주어진 타입의 인스턴스인 조인 포인트
  • this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target: Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target: 특정 어노테이션이 있는 클래스의 메서드와 그 클래스의 부모 클래스의 메서드까지 매칭
  • @within: 주어진 어노테이션이 있는 타입 내 조인 포인트
  • @annotation: 주어진 어노테이션을 가지고 있는 메서드와 조인 포인트를 매칭
  • @args: 메서드의 파라미터가 특정 어노테이션을 갖는 조인 포인트
  • bean: 빈의 이름을 기반으로 매칭

스프링 AOP에서 자주 사용하고, 기능도 복잡한 execution의 문법은 다음과 같다.

 여기서 *은 모든 값, 파라미터에서 .. 은 파라미터의 타입과 수와 상관없다는 뜻이 된다. 위 예시 문법을 해석하면 다음과 같다.

반환 타입 모두 허용
hello.aop.test 패키지의 모든 클래스의 모든 메서드
해당 메서드의 파라미터 타입과 수는 무관

 

파라미터 매칭 규칙

  • (String): 파라미터가 정확하게 String 타입의 파라미터.
  • (): 파라미터가 없는 메서드
  • (*): 파라미터 타입은 모든 타입을 허용하지만, 정확히 하나의 파라미터를 가진 메서드
  • (*, *): 파라미터 타입은 모든 타입을 허용하지만, 정확히 두 개의 파라미터를 가진 메서드
  • (..): 파라미터 모든 타입과 갯수를 허용. ( 파라미터가 없어도 된다. )
  • (String, ..) : 메서드의 첫 번째 파라미터는 String 타입으로 시작해야 하고, 나머지 파라미터 수와 무관하게 모든 파라미터, 모든 타입을 허용한다. ex: (String) , (String, Object ...)

 

[ 그 외 Annotation ]

@Around: advice가 타겟 메서드 호출 전/후 실행, 아래 4개의 Annotation을 모두 포함

@Before: 조인포인트 실행 이전에 실행

@After: 조인포인트에서 실행 결과와 무관하게 실행

@AfterReturning: 조인포인트 정상 동작 후 실행, 반환값 조작 불가능
@AfterThrowing: 메서드가 예외를 던지는 경우 실행, 예외 조작 불가능

 

✅ 실습

[ Setting ]

 기본적으로 Spring boot 프로젝트에 AspectJ를 적용하기 전, 의존성을 추가한다.

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.8</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.8</version>
</dependency>

기존에는 @EnableAspectJAutoProxy를 MainApplication에 추가했지만, 스프링부트2.0부터 기본값으로 설정된다. 의존성 추가 후, AOP를 적용하기위한 Controller와 Service를 다음과 같이 작성한다. Postman으로 호출 및 뒤에 작성할 AspectJ 적용 대상으로 Controller를 추가했다.

 

 validateAround로 어노테이션 기반의 Pointcut과 @Around를 검증하고,

 validateDetail로 execution기반의 Pointcut과 @Before, @AfterReturning을 검증할 것이다.

@Slf4j
@RequiredArgsConstructor
@RequestMapping("/api/aop")
@RestController
public class AopController {

    private final AopService aopService;

    @AopLog
    @GetMapping("/around")
    public ResponseEntity<?> validateAround() throws Exception {
        log.info("### AOP Controller - validateAround 진입 ###");

        AopResponseDto res1 = aopService.validateAround1();

        log.info("### AspectJ passed by proceed() ###");

        return new ResponseEntity<>(res1, HttpStatus.OK);
    }

    @GetMapping("/detail")
    public ResponseEntity<?> validateDetail() throws Exception {
        log.info("### AOP Controller - validateDetail 진입 ###");

        AopResponseDto res = aopService.validateDetail();
        return new ResponseEntity<>(res, HttpStatus.OK);
    }
}

 

아래 코드는 어노테이션 기반의 Pointcut을 검증하고자 추가했다. (@AopLog)

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopLog {
}

 

Service에서는 @Around기반의 advice에서 타겟 대상이 되는 메서드의 로직이 실행된 뒤에 응답값과 로그를 검증하고자 작성했다.

@Slf4j
@Service
public class AopService {

    public AopResponseDto validateAround1() throws Exception {
        log.info("### AOP Service - validateAround1 진입 ###");

        return AopResponseDto.builder()
                .message("AOP - Around1 검증")
                .code("200")
                .build();
    }

    public AopResponseDto validateDetail() throws Exception {
        log.info("### AOP Service - validateDetail 진입 ###");

        return AopResponseDto.builder()
                .message("AOP - Detail(Before/After) 검증")
                .code("200")
                .build();
    }
}

 

[ 공통 관심사 - Aspect ]

다음은 실제 공통 관심사를 작성한 Pointcut과 Advice로 둘을 합쳐 Advisor라고 부른다. 각 어노테이션과 메서드는 주석으로 설명을 더했다.

@Slf4j
@Component
@Aspect
public class Logging {

    // @AopLog가 붙은 메서드가 대상
    @Pointcut("@annotation(com.study.blog.AOP.domain.AopLog)")
    public void AopLogAnnotation() {}

    // 모든 패키지에서
    // com.study.blog.AOP.domain.AopController라는 클래스의
    // validateDetail이라는 메서드와 (..) 모든 파라미터의 타입/수를 대상
    @Pointcut("execution(* com.study.blog.AOP.domain.AopController.validateDetail(..))")
    public void AopLogExecution() {}

    @Around("AopLogAnnotation()")
    public Object validateAround(ProceedingJoinPoint pjp) throws Throwable {
        log.info("### AOP Around - joinPoint Annotation AopLog ###");

        Object res = pjp.proceed(); // 대상 메서드를 실행 및 응답값을 반환
        log.info("### AOP Around - joinPoint proceed ###");
        return res;
    }

    @Before("AopLogExecution()")
    public void validateDetailBefore() {
        log.info("### AOP validationDetail - Before ###");
    }

    @AfterReturning(value = "AopLogExecution()", returning = "res")
    public void validateDetail_AfterReturning(JoinPoint jp, ResponseEntity res) {
        log.info("### AOP validationDetail - AfterReturning ###");

        log.info("result: {}", res);
    }
}

 

[ 타겟 대상 호출 결과 ]

먼저, Postman으로 /around를 호출한 결과를 보며 Aspect가 어떻게 작동했는지 알아본다.

/around

 

로그가 출력된 순서를 보면, Around > Controller > Service > Controller > Around로 진행된다. 처음에 클라이언트로부터 Controller의 validateAround 메서드가 호출됐을 때, 해당 메서드에 @AopLog가 적용되어 Aspect가 적용된 것을 로그를 통해 확인할 수 있다.

0. 타겟 대상 호출

1. @Around advice 실행

2. proceed 메서드로 타겟 대상 실행

3. 타겟 대상의 응답값 저장(res)

4. Advice의 응답값이 클라이언트로 반환

 

이번에는 Postman으로 /detail을 호출한 로그 결과이다.

/detail

마찬가지로 로그가 출력된 순서를 보면, Before > Controller > Service > AfterReturning으로 진행된다. Aspect의 execution으로 정의한 Pointcut의 타겟 대상이 호출된 것을 확인할 수 있다.

0. 타겟 대상 호출

1. @Before advice 실행

2. 타겟 대상 실행 및 완료

3. @AfterReturning advice 실행, 완료된 타겟 대상의 응답값을 res라는 변수로 전달받음

4. 타겟 대상의 응답값이 클라이언트로 반환

 

이렇게 2가지 방식으로 AspectJ를 적용했다. 이외에도 타겟 대상의 파라미터 또는 어노테이션의 변수들을 Advice로 가져와서 핸들링할 수 있다.

 실제 필자가 현업에서 개발하고 있는 프로젝트에서는 Aspect의 타겟 대상 실행 전/후, 결과값에 따라 로그를 남겨야하기 때문에 @Around를 활용하고 있다. 또한, 타겟 대상의 파라미터, 실행 쿼리에 따른 분기 처리 등이 필요하기 때문에 인터페이스 기반으로 역할과 구현을 나눠 개발하고 있다.

 

✅ References

https://heejjeoyi.tistory.com/181

https://velog.io/@im_lily/AOP-%EB%B9%88-%EB%93%B1%EB%A1%9D

https://hstory0208.tistory.com/entry/Spring-%EC%8A%A4%ED%94%84%EB%A7%81-AOP-Pointcut-%ED%91%9C%ED%98%84%EC%8B%9D

https://jstobigdata.com/spring/after-returning-advice-in-spring-aop-afterreturning/

https://handr95.tistory.com/33

 

728x90
반응형