일반적으로 어플리케이션 서비스들이 공통으로 처리해야 할 기능들은 그 성격과 범위에 따라 인터셉터AOP를 사용합니다.

 

  • 접근권한
  • 로깅
  • 예외처리(에러처리)
  • 트랜잭션

 

어플리케이션에서의 공통기능은 이런 기능들이 대표적일 것입니다.

AOP(Aspect Oriented Programing)에 대해서는 이 글에서 따로 다루지 않습니다. 

 

인터셉터는 어플리케이션의 요청과 처리 양 끝단에서 개입하는 개념이라 엔드포인트에 국한되어 처리가 가능한데 비해

AOP는 좀 더 어플리케이션내로 들어가서 영역(Controller, Service, Repository등)에 상관없이,

포인트컷과 애노테이션 등 활용하여 대상을 지정할 수 있습니다.

 

AOP는 어플리케이션의 어떤 영역이든 종단으로 잘라 관점(Aspect)에 따라 개입할 수 있기 때문에 좀 더 강력한 기능을 제공합니다. AOP가 개입할 수 있는 관점은 아래와 같습니다.

 

  • Before: 대상 메서드의 수행 전
  • After: 대상 메서드의 수행 후
  • After-returning: 대상 메서드의 정상적인 수행 후
  • After-throwing: 예외발생 후
  • Around: 대상 메서드의 수행 전/후

 

모두 다 어노테이션으로 지원하며,

관점에 따라 받을 수 있는 매개변수를 통해서 대상메서드에 개입이 가능합니다.

 

이런 차이로 인해 어플리케이션의 진입점에서 필요한 접근제어등의 공통처리는 인터셉터를 활용하고

트랜잭션 처리나 서비스 로그처리등 비즈니스 영역과 밀접하게 관련있는 공통기능는 AOP를 적용해서 처리하는게 일반적인 방식입니다.

 

AOP를 사용하다 보면, 공통처리에서 제외하고 싶은 메서드가 생기는데요. 보통은 포인트 컷의 표현식을 사용하여 처리합니다.

제외처리를 해야 할 메서드가 몇 개 되지 않는다면 상관없으나,

그 대상이 많을 경우, 특히 특정한 규칙없이 여러 영역에 퍼져 있는 대상일 경우는 어노테이션 표현식을 사용하게 되면

가독성에 영향을 줄 정도로 코드가 산만해질 수 있습니다.

 

지금 설명드리는 커스텀 어노테이션을 활용하면 이런 경우라 하더라도

좀 더 깔끔한 코드로 원하는 필터링 효과를 구현 할 수 있습니다.

 

어노테이션은 객체(클래스, 메서드, 필드)의 성격을 표현하기 위해 사용하는 일종의 약속된 메타데이타 표현식이라 할 수 있습니다.

그렇기 때문에 내가 작성한 소스에 목적에 맞는 성격을 임의로 부여할 수 있습니다.

 

우리가 많이 사용하는 Spring의 MVC와 관련된 어노테이션인 @Controller, @Service, @Repository등이 그 대표적인 예일 것입니다.

 

로깅처리가 필요없는 메서드에 사용할 커스텀어노테이션을 아래와 같이 간단히 작성할 수 있습니다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * AOP Logging 처리에 에 제외할 메서드에 적용할 어노테이션
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogging {
}

 

어노테이션의 이름은 그 성격을 잘 드러낼 수 있도록 부여하는게 중요합니다. 로그처리가 필요없는 메서드에 붙일 어노테이션이기 때문에

직관적으로 NoLogging 이라고 명명했습니다.

 

지정될 대상과 범위는 AOP에서 지원하는 @Target@Retention 어노테이션을 활용합니다.

@Taget : 어노테이션이 지정될 위치를 의미합니다. 여러 옵션들이 있는 데 일반적으로 많이 사용하는 옵션입니다.

 

  • ElementType.PACKAGE
  • ElementType.CLASS
  • ElementType.METHOD
  • 등등등

이 이노테이션은 메서드 레벨에서 사용할 목적이기 때문에 ElementType.METHOD으로 설정했습니다.

 

@Retention: 어노테이션이 실제로 적용되고 유지되는 범위를 의미합니다. 정책에 관련된 어노테이션으로,

컴파일 이후에도 런타임환경(JVM)에서 참조가 가능한 RUNTIME으로 지정합니다.

 

  • RetentionPolicy.RUNTIME
  • RetentionPolicy.CLASS
  • RetentionPolicy.SOURCE

 

RetentionPolicy.CLASS, RetentionPolicy.SOURCE 이 두 옵션은 Lombok과 같이 컴파일까지만 참조가 가능한 옵션이기 때문에 일반적으로는 거의 사용하지 않습니다.

 

이제 로깅이 필요없는 메서드에 이 어노테이션을 적용해줍니다.

로깅 AOP는 컨트롤러 영역의 클래스들이 대상이므로 컨트롤러 클래스내에서 로깅을 필요로 하지 않는 메서드에게  이 어노테이션을 추가해줍니다.

@Slf4j
@RestController
public class CommonController {

    // 로깅처리가 필요없는 Endpoint
    @NoLogging
    @GetMapping("/ping")
    public String healthCheck() {
        return "pong";
    }
  
    // 로깅처리를 해야하는 비즈니스 서비스 Endpoint
    @GetMapping("/members")
    public ResponseEntity<?> getMembers() {
      
      ApiResponseDto responseDto = initResponseDto(request);
      // Business logic
      ...
      ...
      return Response.ok(responseDto);
    }

}

 

이해를 돕기 위해 위와 같이 성격이 다른 메서드를 한 컨트롤러 클래스에 구현했습니다.

healthCheck 메서드는 전혀 로깅처리가 필요없는 메서드이고, getMembers는 로깅이 꼭 필요한 비즈니스 컨트롤러 메서드입니다.

이렇게 로깅이 필요없는 메서드는 구현할때 미리 선언적으로 @NoLogging 어노테이션을 붙여주면 됩니다.

 

 

이제 이 이노테이션을 활용해서 어플리케이션 엔드포인트 요청에 대한 로깅처리를 담당하는 AOP Advice클래스를 작성해 보겠습니다.

@Slf4j
@Aspect
@Component
public class ApiCommonAopAdvice {

    @Around("execution(* com.yoarth.api.controller.*Controller.*(..)) " +
            "&& !@annotation(com.yoarth.api.common.annotation.NoLogging) ")
    public Object aroundProcess(ProceedingJoinPoint pjp) throws Throwable {
      
      HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
      
      // 로깅 처리 
      long start = System.currentTimeMillis();

      try {
          return pjp.proceed(pjp.getArgs());
      } finally {
          long end = System.currentTimeMillis();
          log.debug("[Process-Result]: [{}] {} {} {} {} {} ms",
                  txid,
                  userIp,
                  request.getRequestURI(),
                  request.getMethod(),
                  response.getStatus(),
                  end - start);
        }
    }
}    

 

로깅처리에서 제외해야 할 메서드를 커스텀 어노테이션을 활용해서 포인트컷 표현식을 최소한으로 사용한 예입니다.

이 AOP 메서드는

  • com.yoarth.api.controller 패키지 하위의,
  • Controller라는 텍스트가 포함된 이름의 모든 클래스(모든 컨트롤러 클래스)의 모든 메서드가

실행될 때 적용하고, -> execution(* com.yoarth.api.controller.*Controller.*(..))

동시에

  • NoLogging 어노테이션이 없는 메서드에만 적용하되, !@annotation(com.yoarth.api.common.annotation.NoLogging)

메서드의 실행 전/후(@Around)에 관여해서 수행하게 됩니다.

 

어플리케이션내 모든 컨트롤러의 메서드에서 발생하는 요청응답상태, 처리시간을 로그로 기록하는 목적의 AOP 메서드인데,

heath check를 하거나 index에 접속하는 등의 비즈니스 서비스와는 상관없는 엔드포인트에 선언적으로 커스텀 어노테이션인 NoLogging을 적용하여 AOP에서 포인트컷 표현식으로 간단하게 해당 메서드들을 걸러낼 수 있게 됩니다.

 

물론 인터셉터를 사용하여 위의 작업들도 가능합니다만 그럴 경우,

제외해야 할 엔드포인트들을 preHandle 메서드내에서

직접 필터링하는 로직들을 구현해야 하는데. 만약 엔드포인트가 바뀌면 같이 수정해야 하는 불편함과 리스크가 같이 따라오게 됩니다.

 

커스텀 어노테이션의 다른 활용법들도 많을텐데,

이렇게 AOP에서도 포인트컷 표현식에 커스텀어노테이션을 활용한다면

어플리케이션 전역에서, 비즈니스 로직과 관계없는 관점에서의 공통작업들은 좀 더 우아(?)하게 적용할 수 있을거라 생각합니다.

 

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기