오늘은 어드민 전용 API에 접근할 때마다
어드민 사용자만 접근 가능한 특정 API에 접근할 때마다 로그 기록
구현 코드 (최종)
package org.example.expert.common.aop;
import com.fasterxml.jackson.databind.ObjectMapper;
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.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Parameter;
import java.time.LocalDateTime;
@Aspect
@Component
@RequiredArgsConstructor
public class ApiAuditLoggingAspect {
private static final Logger log = LoggerFactory.getLogger(ApiAuditLoggingAspect.class);
private final ObjectMapper objectMapper;
@Around(
"execution(* org.example.expert.domain.comment.controller.CommentAdminController.deleteComment(..))"
+ " || execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))"
)
public Object logAdminApiAccess(ProceedingJoinPoint joinPoint) throws Throwable {
// 메서드 실행 전
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes == null ? null : attributes.getRequest();
if (request == null) {
log.warn("HttpServletRequest가 null 입니다.");
return joinPoint.proceed();
}
String methodName = joinPoint.getSignature().getName();
Long userId = (Long) request.getAttribute("userId");
LocalDateTime requestTime = LocalDateTime.now();
String requestURI = request.getRequestURI();
Object requestBody = extractRequestBody(joinPoint);
String requestBodyJson = safeToJson(requestBody);
log.info("Request 정보: Method: {}, URI: {}, time: {}, userId: {}, RequestBody: {}",
methodName, requestURI, requestTime, userId, requestBodyJson);
// 메서드 실행
Object result = joinPoint.proceed();
// 메서드 실행 후
LocalDateTime responseTime = LocalDateTime.now();
Object responseBody = extractResponseBody(result);
String responseBodyJson = safeToJson(responseBody);
log.info("Response 정보: method: {}, time: {}, ResponseBody: {}",
methodName, responseTime, responseBodyJson);
return result;
}
private Object extractRequestBody(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Parameter[] parameters = signature.getMethod().getParameters();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(RequestBody.class)) {
return args[i];
}
}
return null;
}
private Object extractResponseBody(Object result) {
if (result == null) return null; // void
if (result instanceof ResponseEntity<?> re) return re.getBody();
return result;
}
private String safeToJson(Object obj) {
if (obj == null) return "null";
try {
return objectMapper.writeValueAsString(obj);
} catch (Exception e) {
return "\"<json-error:" + e.getClass().getSimpleName() + ">\"";
}
}
}
@Around는 메서드 실행 전/후를 모두 감쌀 수 있어서 요청 전 로그 + 응답 후 로그를 한 곳에서 처리하기 딱 좋다.
Object result = joinPoint.proceed();
이 코드가 실제 컨트롤러 메서드를 실행시키고, 실행 결과(응답 값)를 그대로 반환한다.
HttpServletRequest.getInputStream() 같은 걸로 Body를 다시 읽으려고 하면 이미 Spring MVC가 읽어서 DTO로 변환한 뒤라 비어있거나 예외가 날 수 있다.
그래서 joinPoint.getArgs()에서 직접 꺼내는 게 안정적이다.
다만 @PathVariable 같은 값도 args에 들어가기 때문에 정확하게 RequestBody만 추출하려면 어노테이션 기반으로 찾는다.
if (parameters[i].isAnnotationPresent(RequestBody.class)) {
return args[i];
}
ResponseBody는 joinPoint.proceed()의 결과 자체다.
그래서 아래처럼 처리했다.
if (result instanceof ResponseEntity<?> re) return re.getBody();
return result;
로그를 찍다가 ObjectMapper.writeValueAsString()에서 예외가 터지면 AOP가 예외를 던져서 API가 실패할 수도 있다.
그래서 safeToJson()으로 감싸서 로그는 실패해도 요청 흐름은 유지되도록 했다.