[Project] Spring AOP로 로그 / 알림 Aspect 구현하기 !

현주·2023년 6월 23일
3
post-custom-banner

📌 Spring AOP(Aspect Oriented Programming)에 대한 자세한 개념들은 아래 포스팅을 참고해주세요.

우리 프로젝트에서는 Spring AOP를 활용하여 로그 기능과 알림 기능을 구현하였다.

어떻게 구현했는지에 대해 자세히 살펴보려고 한다 !


✏️ logging

우리 프로젝트에서는 실행 결과가 나오는 모든 시점, Controller/Service 코드의 실행 전/후에 로그를 남겨 줄 것이므로

아래와 같이 LogAspect를 구현하였다.

@Aspect // (1)
@Slf4j
@Profile("local") // (2)
@Component // (3)
public class LogAspect {

    @Pointcut("execution(* com.yata.backend..*(..))") // (4)
    public void all() {
    }
    
    @Pointcut("execution(* com.yata.backend..*Controller.*(..))") // (5)
    public void controller() {
    }
    
    @Pointcut("execution(* com.yata.backend..*Service.*(..))") // (6)
    public void service() {
    }
    
    @Around("all()") // (7)
    public Object logging(ProceedingJoinPoint joinPoint) throws Throwable { // (7-1)
        long start = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            log.info("log = {}" , joinPoint.getSignature());
            log.info("timeMs = {}", timeMs);
        }
    }
    
    @Before("controller() || service()") // (8)
    public void beforeLogic(JoinPoint joinPoint) throws Throwable { // (8-1)
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        log.info("method = {}", method.getName());

        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if(arg != null) {
                log.info("type = {}", arg.getClass().getSimpleName());
                log.info("value = {}", arg);
            }

        }
    }
    
    @After("controller() || service()") // (9)
    public void afterLogic(JoinPoint joinPoint) throws Throwable { // (9-1)
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        log.info("method = {}", method.getName());

        Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if(arg != null) {
                log.info("type = {}", arg.getClass().getSimpleName());
                log.info("value = {}", arg);
            }

        }
    }
}

위에 번호 순서대로 살펴보면,

(1) @Aspect
➜ 이 클래스가 Aspect를 나타내는 클래스임을 명시

(2) @Profile("local")
➜ local 환경에서만 나타나도록 정의

(3) @Component
➜ 스프링 빈으로 등록

(4) @Pointcut("execution(* com.yata.backend..*(..))")
➜ 부가기능을 주입할 포인트컷을 com.yata.backend 패키지의 하위 패키지에 있는 모든 메서드를 대상으로 설정
( 유지보수/확장성을 위해 포인트컷을 메서드로 만들어 사용 )

(5) @Pointcut("execution(* com.yata.backend..*Controller.*(..))")
➜ 부가기능을 주입할 포인트컷을 com.yata.backend 패키지의 하위 패키지에 있는 모든 Controller를 대상으로 설정
( 유지보수/확장성을 위해 포인트컷을 메서드로 만들어 사용 )

(6) @Pointcut("execution(* com.yata.backend..*Service.*(..))")
➜ 부가기능을 주입할 포인트컷을 com.yata.backend 패키지의 하위 패키지에 있는 모든 Service를 대상으로 설정
( 유지보수/확장성을 위해 포인트컷을 메서드로 만들어 사용 )

(7) @Around("all()")
➜ 메서드 호출 전/후, 예외 발생 시점에 해당 부가기능(Advice) 실행
➜ 인자로 위 (4)에서 설정했던 포인트 컷을 넣어줌

(7-1) Advice 정의

@Around("all()")
public Object logging(ProceedingJoinPoint joinPoint) throws Throwable { // (7-1)
        long start = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            log.info("log = {}" , joinPoint.getSignature());
            log.info("timeMs = {}", timeMs);
        }
    }
  • @Around() 애너테이션의 사용으로, 해당 Advice의 첫번째 파라미터는 ProceedingJoinPoint 사용

    ✔️ ProceedingJoinPoint
    JoinPoint를 확장한 것
    ➜ 실제 메소드 실행을 직접 제어하는 기능을 제공하는 객체

    ✔️ JoinPoint
    ➜ Advice가 적용되는 시점에서의 메소드 실행 정보를 제공하는 인터페이스

  • System.currentTimeMillis() 로 현재 시간을 측정 == 메서드 실행 시작 시간

  • joinPoint.proceed() 로 Advice가 적용된 메서드 실행 후 그 결과를 반환하고,
    반환된 결과를 Object result 변수에 저장
    ( 원래 메서드의 반환값이기 때문에 Object 타입 )

  • finally 블록 - 위의 메서드 실행이 완료된 후에 실행되는 코드
    ➜ Advice 이후의 추가 로직 수행
    ( 주로 알림 로깅과 같은 작업이 이루어짐 )

  • System.currentTimeMillis() 를 다시 호출하여 메서드 실행 완료 시간 측정

  • long timeMs = finish - start; 로 시작 시간과 완료 시간의 차이를 계산하여 메서드 실행에 걸린 시간 계산

  • log.info() 로 메서드 시그니처와 메서드 실행 시간 출력

    • joinPoint.getSignature()
      ➜ 현재 Advice가 적용된 메서드의 시그니처(메서드명, 매개변수 등)를 가져옴
    • result
      ➜ Advice 이후의 메서드 실행 결과

(8) @Before("controller() || service()")
➜ Controller 또는 Service 가 실행되는 시점 에 해당 부가기능(Advice) 실행
➜ 인자로 위 (5),(6)에서 설정했던 포인트 컷을 넣어줌

(8-1) Advice 정의

@Before("controller() || service()")
    public void beforeLogic(JoinPoint joinPoint) throws Throwable { // (8-1)
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        log.info("method = {}", method.getName());Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if(arg != null) {
                log.info("type = {}", arg.getClass().getSimpleName());
                log.info("value = {}", arg);
            }
        }
    }

➜ Advice가 적용된 메서드의 실행 전에, 해당 메서드의 이름과 인자값들을 로그로 출력하는 역할
( 이를 통해 메서드 호출 시 전달되는 인자값들을 확인 / 모니터링 가능 )

  • Advice의 첫번째 파라미터로 JoinPoint 사용

    ✔️ JoinPoint
    ➜ Advice가 적용되는 시점에서의 메소드 실행 정보를 제공하는 인터페이스

  • joinPoint.getSignature() 로 현재 Advice가 적용된 메서드의 시그니처를 가져옴

  • MethodSignature 를 이용하여 메서드 시그니처로부터 Method 객체를 얻음
    ( 이를 통해 메서드의 이름을 알 수 있음 )

  • log.info() 로 메서드의 이름 출력

  • joinPoint.getArgs() 로 메서드의 인자값들을 가져옴

  • for loop 로 각 인자값에 대해 아래 작업 수행

    • arg.getClass().getSimpleName() 로 인자값의 타입 가져옴
    • arg 가 null이 아닌 경우, 타입과 값을 로그에 출력

(9) @After("controller() || service()")
➜ 실행 결과 / 예외에 상관없이,
Controller 또는 Service 가 실행되는 시점 에 해당 부가기능(Advice) 실행
➜ 인자로 위 (5),(6)에서 설정했던 포인트 컷을 넣어줌

(9-1) Advice 정의

@After("controller() || service()")
    public void afterLogic(JoinPoint joinPoint) throws Throwable { // (9-1)
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        log.info("method = {}", method.getName());Object[] args = joinPoint.getArgs();
        for (Object arg : args) {
            if(arg != null) {
                log.info("type = {}", arg.getClass().getSimpleName());
                log.info("value = {}", arg);
            }
        }

➜ Advice가 적용된 메서드의 실행 후에, 해당 메서드의 이름과 인자값들을 로그로 출력하는 역할
( 이를 통해 메서드 호출 이후에 전달되는 인자값들을 확인 / 모니터링 가능 )

  • beforeLogic() 메서드와 상세 내용이 같음

✏️ Notify

우리 프로젝트에서는 Custom Annotation에 Advice를 붙여줄 것이므로 아래와 같이 NotifyAspect를 구현하였다.

📌 Custom Annotation을 생성하여 Spring AOP를 적용하는 방법에 대해서는 아래 포스팅을 참고해주세요.
<[Project] Custom Annotaiton을 이용하여 Spring AOP 프로젝트에 적용하기 !>

  • @Aspect
    ➜ 이 애너테이션으로 이 클래스가 Aspect를 나타내는 클래스임을 명시

  • @Component
    @Aspect 가 붙은 클래스를 Aspect로 쓰기 위해선 이 애너테이션으로 스프링 빈으로 등록해야함

  • @EnableAsync
    @Async 애너테이션으로 비동기적 처리를 할 수 있게 함
    ➜ 클래스 레벨에 붙여줌

  • @Pointcut("@annotation(com.yata.backend.domain.notify.annotation.NeedNotify)")
    ➜ 해당 애너테이션을 가지고 있는 메서드의 조인포인트에 Advice(부가기능)을 주입해라 ~

    위 포인트컷의 타겟 명세 (com.yata.backend.domain.notify.annotation.NeedNotify)는
    위에서 만든 Custom Annotation의 위치임 !

  • @Async
    ➜ Spring에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 Annotation
    ➜ 메서드 레벨에 붙여줌

    ✔️ @Async 애너테이션의 제약조건

    1. public 메서드 일 것
    2. 동일 클래스에서 호출하는 Self-invocation 이어서는 안됨
      ( 즉, inner method에서는 사용 불가 )

      👉 @Async가 적용된 method의 경우, 프록시가 메서드를 가로채 다른 Thread에서 실행 시켜주는 동작 방식이기 때문에 메서드는 public이어야 하고,
      Self-invocation를 사용하게되면 프록시를 무시하고 바로 메서드를 호출하기 때문에 사용이 불가하다.
      [ 참고 Effective Advice on Spring Async: Part 1 ]

    💡 만약 Spring Boot에서 간단히 사용하고 싶다면,
    단순히 Application Class에 @EnableAsync Annotation을 추가하고,
    비동기로 작동하길 원하는 메서드에 @Async Annotation을 붙여주면 사용이 가능하다.
    [ 참고 ]

✔️ 동기적 방식 vs 비동기적 방식

  • 동기적 방식
    ➜ 요청을 보낸 후, 해당 응답을 받아야만 다음으로 넘어갈 수 있는 실행 방식
  • 비동기적 방식
    ➜ 요청을 보낸 후, 응답과 관계없이 바로 다음 동작을 실행할 수 있는 방식
  • @AfterReturning(pointcut = "annotationPointcut()", returning = "result")
    ➜ Target에 대한 내용이 예외없이 성공적으로 수행되면, 결과값 반환 후 해당 내용(Advice)이 주입됨

    ➜ 첫번째 인자로 포인트컷 / 두번째 인자로 Target 메서드의 반환값을 받음
    ( 바로 포인트컷을 넣어주어도 되는데, 유지보수성/확장성을 위해 따로 메서드를 만들어 넣어줌 )

  • NotifyInfo

  • notifyService.send
    send() 메서드를 통해 알림을 생성하고 클라이언트에게 전달

    📌 notifyService.send()에 대한 자세한 내용은 아래 포스팅을 참고해주세요.


🌼 결과

여기까지 log 기능과 notify 기능을 Aspect로 분리한 후 프로젝트를 실행시켜보면

위와 같이 LogAspect와 NotifyAspect가 정상적으로 실행되는 것을 알 수 있다 !!

( 오른 쪽 형광펜으로 칠 한 값들은 NotifyAspect에서 send()한 값들이다. )


📌 다음 포스팅은 실시간 알림 구현에 대한 내용입니다.

👉 [Project] SSE(Servcer-Sent-Events)로 실시간 알림 기능 구현하기 !

post-custom-banner

0개의 댓글