카테고리 없음

TIL Interceptor와 AOP를 활용한 API 로깅

creator7087 2025. 4. 18. 21:21

로깅 구현 방법 (코드는 알아서 챙겨보기) :

  1. Interceptor를 사용하여 구현하기
    • 요청 정보(HttpServletRequest)를 사전 처리합니다.
    • 인증 성공 시, 요청 시각과 URL을 로깅하도록 구현하세요.
      @Slf4j
      @RequiredArgsConstructor
      @Component
      public class AdminApiInterceptor implements HandlerInterceptor {
      
          private static final Logger logger = LoggerFactory.getLogger(AdminApiInterceptor.class);
      
          private final JwtUtil jwtUtil;  // JWT 유틸리티 클래스 (필드 주입 또는 생성자 주입 가능)
      
          @Override
          public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
              String requestURI = request.getRequestURI();
              if (!requestURI.startsWith("/admin")) {
                  return true; // 어드민 API가 아니면 통과
              }
              String bearerToken = request.getHeader("Authorization");
              String jwt = jwtUtil.substringToken(bearerToken);
              Claims claims = jwtUtil.extractClaims(jwt);
      
              String role = claims.get("userRole", String.class);
              Long userId = Long.parseLong(claims.getSubject());
              // ✅ 사전 인증 통과 후, 로깅
              logger.info("[어드민 접근] 사용자 ID: {}, 시간: {}, URL: {}",
                      userId, LocalDateTime.now(), requestURI);
  2. AOP를 사용하여 구현하기(세부구현의 json같이 구현됨)
    • 어드민 API 메서드 실행 전후에 요청/응답 데이터를 로깅합니다.
    • 로깅 내용에는 다음이 포함되어야 합니다:
      • 요청한 사용자의 ID
      • API 요청 시각
      • API 요청 URL
      • 요청 본문(RequestBody)
      • 응답 본문(ResponseBody)
        @Aspect
        @Component
        @RequiredArgsConstructor
        public class AdminApiLoggingAspect {
        
            private final JwtUtil jwtUtil;
        
            private static final Logger logger = LoggerFactory.getLogger(AdminApiLoggingAspect.class);
        
        
            public Object logAdminApi(ProceedingJoinPoint joinPoint) throws Throwable {
                HttpServletRequest request =
                        ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        
                String bearerToken=request.getHeader("Authorization");
                String jwt = jwtUtil.substringToken(bearerToken);
                Claims claims = jwtUtil.extractClaims(jwt);
                Long userId = Long.parseLong(claims.getSubject());
                String url = request.getRequestURI();
                LocalDateTime timestamp = LocalDateTime.now();
        
                Object[] args = joinPoint.getArgs();
                String requestBody = Arrays.stream(args)
                        .map(arg -> new Gson().toJson(arg))
                        .collect(Collectors.joining(", "));
        
                logger.info("[어드민 API 요청] 사용자ID: {}, 시간: {}, URL: {}, 요청본문: {}",
                        userId, timestamp, url, requestBody);
        
                Object response = joinPoint.proceed();
        
                String responseBody = new Gson().toJson(response);
                logger.info("[어드민 API 응답] 사용자ID: {}, 응답본문: {}", userId, responseBody);
        
                return response;
            }
            
  3. 세부 구현 가이드
    • Interceptor:
      • 어드민 인증 여부를 확인합니다.
      • 인증되지 않은 경우 예외를 발생시킵니다.
        public class AdminApiInterceptor implements HandlerInterceptor {
        
            private static final Logger logger = LoggerFactory.getLogger(AdminApiInterceptor.class);
        
            private final JwtUtil jwtUtil;  // JWT 유틸리티 클래스 (필드 주입 또는 생성자 주입 가능)
        
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
                String requestURI = request.getRequestURI();
        
                // 어드민 API만 감지
                if (!requestURI.startsWith("/admin")) {
                    return true; // 어드민 API가 아니면 통과
                }
        
                String bearerToken = request.getHeader("Authorization");
        
                if (bearerToken == null) {
                    response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
                    return false;
                }
        
                try {
                    String jwt = jwtUtil.substringToken(bearerToken);
                    Claims claims = jwtUtil.extractClaims(jwt);
        
                    if (claims == null) {
                        response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 유효하지 않습니다.");
                        return false;
                    }
        
                    String role = claims.get("userRole", String.class);
                    Long userId = Long.parseLong(claims.getSubject());
        
                    if (!"ADMIN".equals(role)) {
                        response.sendError(HttpServletResponse.SC_FORBIDDEN, "어드민 권한이 없습니다.");
                        return false;
                    }
        
                    // ✅ 사전 인증 통과 후, 로깅
                    logger.info("[어드민 접근] 사용자 ID: {}, 시간: {}, URL: {}",
                            userId, LocalDateTime.now(), requestURI);
        
                    return true;
                } catch (Exception e) {
                    logger.error("JWT 검증 중 오류 발생", e);
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 인증 실패");
                    return false;
                }
            }
        }
    • AOP:
      • @Around 어노테이션을 사용하여 어드민 API 메서드 실행 전후에 요청/응답 데이터를 로깅합니다.
      • 요청 본문과 응답 본문은 JSON 형식으로 기록하세요.
        package org.example.expert.config;
        
        import com.google.gson.Gson;
        import io.jsonwebtoken.Claims;
        import jakarta.servlet.http.HttpServletRequest;
        import lombok.RequiredArgsConstructor;
        import org.aspectj.lang.ProceedingJoinPoint;
        import org.aspectj.lang.annotation.Around;
        import org.aspectj.lang.annotation.Aspect;
        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import org.springframework.stereotype.Component;
        import org.springframework.web.context.request.RequestContextHolder;
        import org.springframework.web.context.request.ServletRequestAttributes;
        
        import java.time.LocalDateTime;
        import java.util.Arrays;
        import java.util.stream.Collectors;
        
        @Aspect
        @Component
        @RequiredArgsConstructor
        public class AdminApiLoggingAspect {
        
            private final JwtUtil jwtUtil;
        
            private static final Logger logger = LoggerFactory.getLogger(AdminApiLoggingAspect.class);
        
            @Around("execution(* org.example.expert.domain.comment.controller.CommentAdminController.deleteComment(..)) || " +
                    "execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")
        
            public Object logAdminApi(ProceedingJoinPoint joinPoint) throws Throwable {
                HttpServletRequest request =
                        ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
        
                String bearerToken=request.getHeader("Authorization");
                String jwt = jwtUtil.substringToken(bearerToken);
                Claims claims = jwtUtil.extractClaims(jwt);
                Long userId = Long.parseLong(claims.getSubject());
                String url = request.getRequestURI();
                LocalDateTime timestamp = LocalDateTime.now();
        
                Object[] args = joinPoint.getArgs();
                String requestBody = Arrays.stream(args)
                        .map(arg -> new Gson().toJson(arg))
                        .collect(Collectors.joining(", "));
        
                logger.info("[어드민 API 요청] 사용자ID: {}, 시간: {}, URL: {}, 요청본문: {}",
                        userId, timestamp, url, requestBody);
        
                Object response = joinPoint.proceed();
        
                String responseBody = new Gson().toJson(response);
                logger.info("[어드민 API 응답] 사용자ID: {}, 응답본문: {}", userId, responseBody);
        
                return response;
            }
        }
        
    • 로깅은 Logger 클래스를 활용하여 기록합니다.

postman+ 로그

 

1. 로그인

2. 포스트맨

3. 로그(접근 -> API요청 순)

내용 :ID, 시간 ,URL, Requse랑 Response 속해져 있습니다. 

 

전체코드

더보기
package org.example.expert.config;



import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.IOException;
import java.time.LocalDateTime;

@Slf4j
@RequiredArgsConstructor
@Component
public class AdminApiInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(AdminApiInterceptor.class);

    private final JwtUtil jwtUtil;  // JWT 유틸리티 클래스 (필드 주입 또는 생성자 주입 가능)

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        String requestURI = request.getRequestURI();

        // 어드민 API만 감지
        if (!requestURI.startsWith("/admin")) {
            return true; // 어드민 API가 아니면 통과
        }

        String bearerToken = request.getHeader("Authorization");

        if (bearerToken == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
            return false;
        }

        try {
            String jwt = jwtUtil.substringToken(bearerToken);
            Claims claims = jwtUtil.extractClaims(jwt);

            if (claims == null) {
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 유효하지 않습니다.");
                return false;
            }

            String role = claims.get("userRole", String.class);
            Long userId = Long.parseLong(claims.getSubject());

            if (!"ADMIN".equals(role)) {
                response.sendError(HttpServletResponse.SC_FORBIDDEN, "어드민 권한이 없습니다.");
                return false;
            }

            // ✅ 사전 인증 통과 후, 로깅
            logger.info("[어드민 접근] 사용자 ID: {}, 시간: {}, URL: {}",
                    userId, LocalDateTime.now(), requestURI);

            return true;
        } catch (Exception e) {
            logger.error("JWT 검증 중 오류 발생", e);
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "JWT 인증 실패");
            return false;
        }
    }
}

package org.example.expert.config;

import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.stream.Collectors;

@Aspect
@Component
@RequiredArgsConstructor
public class AdminApiLoggingAspect {

    private final JwtUtil jwtUtil;

    private static final Logger logger = LoggerFactory.getLogger(AdminApiLoggingAspect.class);

    @Around("execution(* org.example.expert.domain.comment.controller.CommentAdminController.deleteComment(..)) || " +
            "execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))")

    public Object logAdminApi(ProceedingJoinPoint joinPoint) throws Throwable {
        HttpServletRequest request =
                ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

        String bearerToken=request.getHeader("Authorization");
        String jwt = jwtUtil.substringToken(bearerToken);
        Claims claims = jwtUtil.extractClaims(jwt);
        Long userId = Long.parseLong(claims.getSubject());
        String url = request.getRequestURI();
        LocalDateTime timestamp = LocalDateTime.now();

        Object[] args = joinPoint.getArgs();
        String requestBody = Arrays.stream(args)
                .map(arg -> new Gson().toJson(arg))
                .collect(Collectors.joining(", "));

        logger.info("[어드민 API 요청] 사용자ID: {}, 시간: {}, URL: {}, 요청본문: {}",
                userId, timestamp, url, requestBody);

        Object response = joinPoint.proceed();

        String responseBody = new Gson().toJson(response);
        logger.info("[어드민 API 응답] 사용자ID: {}, 응답본문: {}", userId, responseBody);

        return response;
    }
}​

-하나 더 수정했지만, 그건 비밀 궁금하면 질문하세요.

 

✅ Interceptor와 AOP를 활용한 API 로깅 정리

 

더보기

🔹 Interceptor란?

Spring MVC에서 제공하는 기능으로, DispatcherServlet과 Controller 사이에서 요청/응답을 가로채는 역할을 합니다.

주요 메서드

메서드설명
preHandle() Controller 진입 전에 실행 (주로 요청 정보 로그)
postHandle() Controller 실행 후, View 렌더링 전에 실행
afterCompletion() View까지 렌더링이 끝난 후 실행 (예외 처리 포함)

사용 목적

  • 요청 URL, HTTP Method, Header 정보, 인증 토큰 등의 로깅
  • 응답 시간 측정
  • 인증/인가 체크 등

: Interceptor는 HTTP Request/Response 기반의 로깅에 유리하며, URL, Header, 인증 토큰 등의 처리에 강합니다.

🔹 AOP(Aspect Oriented Programming)란?

공통 기능(Aspect)을 핵심 비즈니스 로직과 분리하여 모듈화하는 프로그래밍 기법입니다. 주로 메서드 실행 전/후에 적용되며, 다양한 관심사(Logging, 트랜잭션, 보안 등)를 분리하여 처리합니다.

주요 어노테이션

어노테이션설명
@Aspect AOP 클래스 지정
@Before 메서드 실행 전 동작
@AfterReturning 메서드 정상 실행 후 동작
@AfterThrowing 예외 발생 시 동작
@Around 메서드 실행 전후 모두 처리 가능

 

: AOP는 비즈니스 로직과 독립적인 공통 기능(로깅, 트랜잭션 등)을 모듈화할 때 강력하며, 특정 패키지 전체에 적용 가능하여 확장성이 좋습니다.

✅ Interceptor vs AOP 비교

항목InterceptorAOP
작동 위치 컨트롤러 진입 전/후 메서드 호출 전/후
적용 대상 Spring MVC Controller 모든 Spring Bean
주요 용도 요청/응답 정보 확인 비즈니스 로직 전후 처리
장점 HTTP 레벨 접근 가능 세부적인 메서드 제어 가능

: 두 기능을 동시에 사용할 수 있으며, 요청/응답 로깅은 Interceptor, 메서드 실행 로깅은 AOP로 분리하면 더 효율적인 구조를 만들 수 있습니다.

🔄 함께 쓰는 방식

  1. Interceptor를 활용하여
    • 요청 URI, 헤더, IP, 요청 시간 등을 로그로 기록
  2. AOP를 활용하여
    • 비즈니스 메서드의 실행 시간, 파라미터, 리턴값, 예외 로그 등 기록

: 로깅 외에도 보안, 모니터링, 성능 분석 등 다양한 목적을 위해 Interceptor와 AOP를 조합할 수 있습니다.