어드민 사용자만 접근할 수 있는 특정 API에 접근할 때마다 접근 로그를 기록하려고 한다.
이를 위해 AOP를 사용해서 로깅 기능을 구현했다.
로깅 시 아래 내용들을 포함시키려고 한다.
RequestBody)ResponseBody)@LogAdmin이라는 커스텀 어노테이션이 붙어있는 컨트롤러의 메서드에서만 로그가 찍히도록 하기 위해서 @Around("@annotation(org.example.expert.aop.LogAdmin)")와 같이 사용했다.
유저 정보는 JwtFileter에서 인증 성공 시 httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));로 값을 넣어주고 있기 때문에 request.getAttributes("userId")로 가져올 수 있다.
AdminApiLoggingAspect.java
@Aspect
@Component
@RequiredArgsConstructor
public class AdminApiLoggingAspect {
private final ObjectMapper objectMapper;
private static final Logger logger = LogManager.getLogger(AdminApiLoggingAspect.class);
@Around("@annotation(org.example.expert.aop.LogAdmin)")
public Object logAdminApi(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest httpRequest = getHttpRequest();
if (httpRequest == null) {
// request 또는 response 값이 null 이면 중간에 종료
// @Around는 메서드 실행을 감싸는 래퍼이므로 proceed()를 호출해야 원래 메서드가 실행됨
logger.warn("HttpServletRequest 객체를 가져올 수 없음");
return joinPoint.proceed();
}
String methodName = joinPoint.getSignature().getName();
Long userId = (Long) httpRequest.getAttribute("userId"); // 요청한 사용자의 ID
LocalDateTime requestTime = LocalDateTime.now(); // API 요청 시각
String requestURI = httpRequest.getRequestURI(); // API 요청 URL
String requestBody = getRequestBody(joinPoint); // 요청 본문
logger.info("[Admin API Logging]");
logger.info("Request 정보: Method: {}, URI: {}, time: {}, userId: {}, RequestBody: {}",
methodName,
requestURI,
requestTime,
userId,
requestBody
);
Object result;
try {
result = joinPoint.proceed(); // 실제 API 실행
} catch (Exception e) {
logger.error("에러 발생!! : Method: {}, URI: {}, message: {}",
methodName,
requestURI,
e.getMessage()
);
throw e; // 처리는 ControllerAdvice 에게 위임
}
String responseString = convertObjectToJson(result);
String responseBody = extractBodyFromJson(responseString);
logger.info("Response 정보: Method: {}, URI: {}, ResponseBody: {}",
methodName,
requestURI,
responseBody
);
return result;
}
private HttpServletRequest getHttpRequest() {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attr != null ? attr.getRequest() : null;
}
private String getRequestBody(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
if (args.length > 0) {
try {
return Arrays.stream(args)
.map(this::convertObjectToJson)
.reduce((arg1, arg2) -> arg1 + ", " + arg2)
.orElse("");
} catch (Exception e) {
logger.error("Error serializing request body", e);
}
}
return "";
}
private String convertObjectToJson(Object object) {
if (object == null)
return "";
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object);
} catch (JsonProcessingException e) {
logger.error("Error serializing object to JSON", e);
return "Error serializing object to JSON";
}
}
private String extractBodyFromJson(String json) {
if (json == null || json.isBlank()) return "";
try {
JsonNode root = objectMapper.readTree(json);
JsonNode body = root.path("body");
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
} catch (JsonProcessingException e) {
logger.error("Error extracting body from JSON", e);
return "Error extracting body from JSON";
}
}
}
LogAdmin.java
/**
* Logging 작업을 수행할 Admin API 메서드에 붙여서 사용
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAdmin {
}
적용 예시
@LogAdmin
@DeleteMapping("/admin/comments/{commentId}")
public void deleteComment(@PathVariable long commentId) {
commentAdminService.deleteComment(commentId);
}