로그 적용

박재성·2025년 3월 7일
0

파이널 프로젝트

목록 보기
1/3

로그 레벨 설정 (INFO, ERROR)

로그 파일 저장 (application.xml 설정)

어떤 것을 로그로 남겨야 하나?

  • AOP 활용한 공통 로깅
  • 로그 롤링 (날짜 기준으로 회전)
  • 콘솔과 파일 로그 분리 (콘솔 - INFO, 파일 로그 - ERROR)

생각 Flow🌊

AOP로 적용하고싶음. 한꺼번에 적용하는게 유지보수도 좋음.

user_id, request, responseService에서 찍고싶음.

근데 User는 중요 정보가 들어있음. ExchangeMyExchange도 전화번호가 있음. 얘네를 빼고 찍을까?

→ 어 근데 찾아보니 특정 정보만 필터링 가능!

날짜 기준(00시 00분)으로 로그 파일을 남기는게 좋아보임. 저장 기한은 1년.

1️⃣ 로깅 V1

  • 로깅 V1 코드

    ServicLoggingAspect.java

    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;
        }
    }
  • 로깅 V1 설정

    application.yml

    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로 이상하게 나옴.

2️⃣ 로깅 V2

  • 로깅 V2 코드

    ServicLoggingAspect.java

    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.LocalDateTime not 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를 바꾸는 문제가 남아있음.

3️⃣ 로깅 V3

PageResponse와 WebConfig(Spring 3.2 이상) 설정을 통해 해결

@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에 WARN은 오류가 아니다?

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을 보고 안정하지 않구나 했는데, 뒤에 어떤거를 써야하는지와 공식 문서에 친절하게 나와있다. 그냥 "예전 페이지네이션은 쓸데없는 정보도 많고 안좋으니 이걸 써라" 이다.

0개의 댓글