본문 바로가기

Backend/SpringBoot

SpringBoot의 AOP을 이용해서 로그 남기기

안녕하세요!

이번 포스팅에서는 SpringBoot의 AOP를 이용해서

로그를 남기는 방법에 대해 알아보겠습니다. 👩🏻‍💻 

 

전체 코드는 Github에서 확인이 가능합니다


사전 준비

1. AOP

AOP는 관점 지향 프로그래밍이라는 뜻으로, 코드를 핵심 기능공통 기능으로 나눈 후에

핵심 기능에서 따로 빼놓은 공통 기능을 불러와 적용하는 방법입니다.

AOP

2. 프로젝트 설명

개발을 하면서 로그는 에러가 난 이유를 찾거나 값을 확인하는 데 중요한 역할을 합니다.

수동으로 찍는 로그

하지만 이런 식으로 로그를 수동으로 찍다 보면 아래와 같은 문제가 생깁니다.

  • 중복된 코드의 증가
  • 실수로 로그를 찍지 않으면 값 확인 불가능

따라서 AOP를 사용해 요청이 오면 요청 데이터, 응답 데이터, 요청까지 걸린 시간을 로그로 자동으로 찍어주는 코드를 작성해보도록 하겠습니다.

3. 개발 환경

  • IntelliJ Ultimate
  • Maven
  • Java 1.8
  • SpringBoot 2.2.5
  • Window

구현

1. 의존성 추가

AOP를 사용하기 위해 의존성을 추가해줍니다.

Maven

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

 

Gradle

dependencies {
  compile('org.springframework.boot:spring-boot-starter-aop')
  compile group: 'com.google.guava', name: 'guava', version: '18.0'
}

 

2. @EnableAspectJAutoProxy

Application.java

@SpringBootApplication
@EnableAspectJAutoProxy // 추가
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

MainApplication에 @EnableAspectJAutoProxy 어노테이션을 추가해줍니다.

위 어노테이션을 사용하면 AOP를 사용할 수 있습니다.

 

3. @Aspect 클래스 만들기

Log 관련 설정을 할 클래스를 만들어줍니다.

LogConfig.java

@Slf4j
@Component
@Aspect
public class LogConfig {

  @Around("within(패키지이름..*))") // ex. within(me.shinsunyoung.demo..*)) 1
  public Object logging(ProceedingJoinPoint pjp) throws Throwable { // 2

    String params = getRequestParams(); // request 값 가져오기

    long startAt = System.currentTimeMillis();

    log.info("-----------> REQUEST : {}({}) = {}", pjp.getSignature().getDeclaringTypeName(),
        pjp.getSignature().getName(), params);

    Object result = pjp.proceed(); // 4

    long endAt = System.currentTimeMillis();

    log.info("-----------> RESPONSE : {}({}) = {} ({}ms)", pjp.getSignature().getDeclaringTypeName(),
        pjp.getSignature().getName(), result, endAt - startAt);

    return result;
  }


  private String paramMapToString(Map<String, String[]> paramMap) {
    return paramMap.entrySet().stream()
        .map(entry -> String.format("%s -> (%s)",
            entry.getKey(), Joiner.on(",").join(entry.getValue())))
        .collect(Collectors.joining(", "));
  }

  // Get request values 
  private String getRequestParams() {

    String params = "없음";

    RequestAttributes requestAttributes = RequestContextHolder
        .getRequestAttributes(); // 3

    if (requestAttributes != null) {
      HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
          .getRequestAttributes()).getRequest(); 

      Map<String, String[]> paramMap = request.getParameterMap();
      if (!paramMap.isEmpty()) {
        params = " [" + paramMapToString(paramMap) + "]";
      }
    }

    return params;

  }
}

 

@Around

대상 객체(within으로 범위 설정 가능)의 메서드 실행 전, 후 시점에 메소드를 실행

 

ProceedingJoinPoint

호출되는 객체에 대한 정보, 실행될 메소드에 대한 정보가 존재

 

getRequestAttributes

호출된 값의 Request 값을 얻을 때 사용, 없으면 null 반환

currentRequestAttributes와 비슷하지만 currentRequestAttributes는 값이 없으면 예외 발생

 

proceed()

메소드 실행

 

개인적으로 중요하다고 생각한 부분이나 이해가 잘 안 됐던 부분만 정리를 해보았습니다!

전체 플로우를 설명하자면 아래와 같습니다.

  • 요청
  • 메소드 실행 전에 컨트롤러와 메소드 이름, Request 값의 정보를 담은 Request 로그 출력
  • procced()메소드를 이용해 원래 실행해야 하는 메소드 실행
  • 실행 후에 컨트롤러와 메소드 이름, 반환된 값의 정보를 담은 Response 로그 출력
  • 응답

그리고 실행해보면 콘솔에 잘 찍히는 것을 확인할 수 있습니다!


정리

위와 같은 방법으로 AOP를 사용해서 로그를 자동으로 찍을 수 있습니다.

아직 AOP에 대한 완벽한 이해가 없어서 깊게 설명하진 못했지만,

이해하게 된다면 AOP에 대한 포스팅도 해볼 계획입니다!

혹시 글을 읽으면서 잘못된 내용이 있으면 댓글로 알려주시면 감사하겠습니다!

읽어주셔서 감사합니다! 😊


참고링크

https://jeong-pro.tistory.com/171

https://shortstories.gitbook.io/studybook/spring_ad00_b828_c815_b9ac/aop/baa8-b4e0-c6f9-c694-ccad-c5d0-b300-d574-c11c-b85c-adf8-b85c-b0a8-ae30-ae30