카테고리 없음
TIL Interceptor와 AOP를 활용한 API 로깅
creator7087
2025. 4. 18. 21:21
로깅 구현 방법 (코드는 알아서 챙겨보기) :
- 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);
- 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; }
- 세부 구현 가이드
- 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 클래스를 활용하여 기록합니다.
- Interceptor:
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로 분리하면 더 효율적인 구조를 만들 수 있습니다.
🔄 함께 쓰는 방식
- Interceptor를 활용하여
- 요청 URI, 헤더, IP, 요청 시간 등을 로그로 기록
- AOP를 활용하여
- 비즈니스 메서드의 실행 시간, 파라미터, 리턴값, 예외 로그 등 기록
팁: 로깅 외에도 보안, 모니터링, 성능 분석 등 다양한 목적을 위해 Interceptor와 AOP를 조합할 수 있습니다.