로그 레벨 설정 (INFO, ERROR)
로그 파일 저장 (application.xml 설정)
어떤 것을 로그로 남겨야 하나?
AOP로 적용하고싶음. 한꺼번에 적용하는게 유지보수도 좋음.
user_id
, request
, response
를 Service
에서 찍고싶음.
근데 User
는 중요 정보가 들어있음. Exchange
와 MyExchange
도 전화번호가 있음. 얘네를 빼고 찍을까?
→ 어 근데 찾아보니 특정 정보만 필터링 가능!
날짜 기준(00시 00분)으로 로그 파일을 남기는게 좋아보임. 저장 기한은 1년.
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@Aspect
@Component
@Slf4j
public class ServiceLoggingAspect {
// Service 계층의 모든 메서드 적용
@Pointcut("execution(* com.example.codePicasso.domain..service.*.*(..))")
public void serviceMethods() {}
// 민감한 데이터 필터링을 위한 필드 목록
private static final Set<String> SENSITIVE_FIELDS = new HashSet<>(Arrays.asList("password", "contact"));
@Around("serviceMethods()")
public Object logServiceExecution(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs(); // 메서드 파라미터 가져오기
// 요청 데이터 필터링
String filteredRequest = filterSensitiveData(args);
log.info("Entering Service: {} | Request: {}", methodName, filteredRequest);
try {
Object result = joinPoint.proceed(); // 실제 메서드 실행
// 응답 데이터 필터링
String filteredResponse = filterSensitiveData(result);
log.info("Exiting Service: {} | Response: {}", methodName, filteredResponse);
return result;
} catch (Exception ex) {
log.error("Exception in Service: {} - {}", methodName, ex.getMessage(), ex);
throw ex; // 예외를 다시 던짐
}
}
// 객체 내부의 민감한 필드 제거 (password, contact 등)
private String filterSensitiveData(Object data) {
if (data == null) return "null";
String jsonString = data.toString();
for (String field : SENSITIVE_FIELDS) {
jsonString = jsonString.replaceAll("\"" + field + "\":\"[^\"]+\"", "\"" + field + "\":\"****\"");
}
return jsonString;
}
}
logging:
level:
root: info # 기본 로그 레벨
com.example.codePicasso: info # 프로젝트 패키지 로그 레벨
file:
name: logs/application.log # 기본 로그 파일 위치
logback:
rollingpolicy:
file-name-pattern: logs/application-%d{yyyy-MM-dd}.log # 날짜별 로그 파일
max-history: 365 # 1년 동안 로그 유지
max-file-size: 100MB # 100MB 이상이면 새로운 파일 생성
→ request가 [Ljava.lang.Object;@1205a22f
로 이상하게 나옴.
package com.example.codePicasso.global.common;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Aspect
@Component
@Slf4j
public class ServiceLoggingAspect {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Pointcut("execution(* com.example.codePicasso.domain..service.*.*(..))")
public void serviceMethods() {}
private static final Set<String> SENSITIVE_FIELDS = new HashSet<>(Arrays.asList("password", "contact"));
@Around("serviceMethods()")
public Object logServiceExecution(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
Object[] args = joinPoint.getArgs();
// 요청 데이터 필터링 (배열을 JSON으로 변환)
String filteredRequest = filterSensitiveData(args);
log.info("🔥 Entering Service: {} | Request: {}", methodName, filteredRequest);
try {
Object result = joinPoint.proceed();
// 응답 데이터 필터링
String filteredResponse = filterSensitiveData(result);
log.info("✅ Exiting Service: {} | Response: {}", methodName, filteredResponse);
return result;
} catch (Exception ex) {
log.error("❌ Exception in Service: {} - {}", methodName, ex.getMessage(), ex);
throw ex;
}
}
/**
* 요청/응답 객체의 민감한 필드(`password`, `contact`)를 마스킹하여 필터링
*/
private String filterSensitiveData(Object data) {
try {
if (data == null) return "null";
// 배열이면 개별 요소 변환 후 리스트로 출력
if (data.getClass().isArray()) {
Object[] arrayData = (Object[]) data;
return Arrays.stream(arrayData)
.map(this::filterSensitiveData)
.collect(Collectors.toList())
.toString();
}
// JSON 변환 후 필터링
String jsonString = objectMapper.writeValueAsString(data);
for (String field : SENSITIVE_FIELDS) {
jsonString = jsonString.replaceAll("\"" + field + "\":\"[^\"]+\"", "\"" + field + "\":\"****\"");
}
return jsonString;
} catch (Exception e) {
log.warn("⚠️ Failed to parse JSON for logging: {}", e.getMessage());
return data.toString(); // JSON 변환 실패 시 기본 `toString()` 반환
}
}
}
→ request는 해결
⚠️LocalDateTime을 Json으로 변환 실패
2025-03-05 18:03:16 [http-nio-*8080*-exec-*2*] WARN *c.e.c.g.common.ServiceLoggingAspect* - ⚠️ Failed to parse JSON for logging: Java *8* date/time type
java.time.LocalDateTimenot supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: *com.example.codePicasso.domain.user.entity.User*["createdAt"])
⚠️Page를 Json으로 바꾸는건 안정하지 않음
2025-03-05 18:03:22 [http-nio-*8080*-exec-*3*] WARN *o.s.d.w.c.SpringDataJacksonConfiguration*$PageModule$WarningLoggingModifier - Serializing PageImpl instances as-is is not supported, meaning that there is no guarantee about the stability of the resulting JSON structure! For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)) or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in *https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables*.
→ response는 로그에 필요 없다고 판단, 삭제. 하지만 Page를 바꾸는 문제가 남아있음.
@Getter
@AllArgsConstructor
public class PageResponse<T> {
private List<T> content;
private int page;
private int totalPages;
private long totalElements;
}
@Configuration
@EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO)
public class WebConfig {
}
→ 생각해보니 PageSerializationMode.VIA_DTO
가 있으면 PageResponse
필요없음 ㅋㅋ
PageResponse
에서 발생한 log
를 보면 아래와 같다.
For a stable JSON structure, please use Spring Data's PagedModel (globally via @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)) or Spring HATEOAS and Spring Data's PagedResourcesAssembler as documented in https://docs.spring.io/spring-data/commons/reference/repositories/core-extensions.html#core.web.pageables.`
난 그냥 stable을 보고 안정하지 않구나 했는데, 뒤에 어떤거를 써야하는지와 공식 문서에 친절하게 나와있다. 그냥 "예전 페이지네이션은 쓸데없는 정보도 많고 안좋으니 이걸 써라" 이다.