Skip to content

Commit 7c3deae

Browse files
authored
�Refactor/252 Logging 설정 추가및 리팩토링 (#270)
* chore: 로그 파일 디렉토리를 gitignore 추가 -root/logs 디렉토리의 ignore 설정 * feat: loki 의존성 추가 - 로그 저장, 시각화를 위한 loki 의존성 추가 * refactor: logback-spring.xml 리팩토링 - loki 관련 설정 추가 - 로그 패턴의 단일화 적용 - 파일 저장 appender의 설정 변경 * chore: jacoco 예외 클래스 추가 - Extractor 클래스의 테스트 커버리지 예외 조건 추가 * refactor: logging AOP 리팩토링 - 로그 기록 세분화 - 민감한 데이터 마스킹 처리 - 책임에 따른 클래스 분리 - 마스킹 테스트및 검증 완 * feat: log 공통 응답 객체 생성 - 로그 응답에 대한 Response 객체 생성 * refactor: 명시적인 생성자 제거및 어노테이션 대체 - ParameterExtractor의 생성자 코드 제거및 RequiredArgs 어노테이션 추가 - ResponseExtractor의 생성자 코드 제거및 RequiredArgs 어노테이션 추가 - SensitiveDataMasker의 불필요한 어노테이션 제거 * chore: 잠재적 에러 예방을 위한 개행 추가 - logback-spring.xml 파일의 마지막 라인에 개행 추가
1 parent d7ad70b commit 7c3deae

File tree

11 files changed

+490
-107
lines changed

11 files changed

+490
-107
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,6 @@ out/
4141

4242
### env file ###
4343
*.env
44+
45+
### log file ###
46+
/logs/

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ dependencies {
6969
// Monitoring
7070
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '3.3.3'
7171
implementation 'io.micrometer:micrometer-registry-prometheus'
72+
implementation 'com.github.loki4j:loki-logback-appender:1.5.1'
7273

7374
//elastic-search
7475
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
@@ -116,6 +117,7 @@ def jacocoExcludePatterns = [
116117
'**/*Application.class',
117118
'**/*Config*',
118119
'**/*Exception*',
120+
'**/*Extractor*',
119121
'**/*Request*',
120122
'**/*Response*',
121123
'**/*Entity*',
@@ -135,6 +137,7 @@ def jacocoExcludePatternsForVerify = [
135137
'*.*Application*',
136138
'*.*Config*',
137139
'*.*Exception*',
140+
'*.*Extractor*',
138141
'*.*Request*',
139142
'*.*Response*',
140143
'*.*Entity*',

src/main/java/com/somemore/global/aspect/LoggingAspect.java

Lines changed: 0 additions & 70 deletions
This file was deleted.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package com.somemore.global.aspect.log;
2+
3+
import com.somemore.global.aspect.log.extractor.ParameterExtractor;
4+
import com.somemore.global.aspect.log.extractor.RequestExtractor;
5+
import com.somemore.global.aspect.log.extractor.ResponseExtractor;
6+
import com.somemore.global.common.response.LoggedResponse;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.aspectj.lang.ProceedingJoinPoint;
11+
import org.aspectj.lang.annotation.Around;
12+
import org.aspectj.lang.annotation.Aspect;
13+
import org.aspectj.lang.annotation.Pointcut;
14+
import org.slf4j.MDC;
15+
import org.springframework.http.HttpStatus;
16+
import org.springframework.stereotype.Component;
17+
18+
19+
import java.util.UUID;
20+
21+
@RequiredArgsConstructor
22+
@Slf4j
23+
@Aspect
24+
@Component
25+
public class LoggingAspect {
26+
27+
private final RequestExtractor requestExtractor;
28+
private final ResponseExtractor responseExtractor;
29+
private final ParameterExtractor parameterExtractor;
30+
31+
@Pointcut("execution(* com.somemore.domains.*.controller..*.*(..))")
32+
private void controllerPointCut() {}
33+
34+
@Around("controllerPointCut()")
35+
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
36+
String requestId = UUID.randomUUID().toString();
37+
MDC.put("requestId", requestId);
38+
39+
try {
40+
return doLogAround(joinPoint);
41+
} finally {
42+
MDC.remove("requestId");
43+
}
44+
}
45+
46+
private Object doLogAround(ProceedingJoinPoint joinPoint) throws Throwable {
47+
String methodName = joinPoint.getSignature().toShortString();
48+
HttpServletRequest request = requestExtractor.getCurrentRequest();
49+
50+
MDC.put("method", request.getMethod());
51+
MDC.put("uri", request.getRequestURI());
52+
53+
String params = parameterExtractor.extractParameters(joinPoint);
54+
log.info("엔드포인트 호출: {} \n- URI: {} \n- Method: {} \n- 파라미터: {}",
55+
methodName,
56+
request.getRequestURI(),
57+
request.getMethod(),
58+
params);
59+
60+
long startTime = System.currentTimeMillis();
61+
62+
try {
63+
Object result = joinPoint.proceed();
64+
long elapsedTime = System.currentTimeMillis() - startTime;
65+
66+
LoggedResponse loggedResponse = responseExtractor.extractResponse(result);
67+
log.info("호출 성공: {} \n- 응답 코드: {} \n- 응답 값: {} \n- 실행 시간: {}ms",
68+
methodName,
69+
loggedResponse.getStatusCode(),
70+
loggedResponse.getBody(),
71+
elapsedTime);
72+
73+
return result;
74+
} catch (Exception e) {
75+
long elapsedTime = System.currentTimeMillis() - startTime;
76+
HttpStatus status = responseExtractor.extractExceptionStatus(e);
77+
78+
log.warn("예외 발생: {} \n- 예외 코드: {} \n- 예외 타입: {} \n- 예외 메세지: {} \n- 실행 시간: {}ms",
79+
methodName,
80+
status,
81+
e.getClass().getSimpleName(),
82+
e.getMessage(),
83+
elapsedTime);
84+
85+
throw e;
86+
} finally {
87+
MDC.clear();
88+
}
89+
}
90+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.somemore.global.aspect.log.extractor;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.somemore.global.aspect.log.utils.SensitiveDataMasker;
6+
import lombok.RequiredArgsConstructor;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.aspectj.lang.ProceedingJoinPoint;
9+
import org.aspectj.lang.reflect.MethodSignature;
10+
import org.springframework.stereotype.Component;
11+
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.RequestParam;
13+
14+
import java.lang.reflect.Parameter;
15+
import java.util.LinkedHashMap;
16+
import java.util.Map;
17+
18+
@RequiredArgsConstructor
19+
@Slf4j
20+
@Component
21+
public class ParameterExtractor {
22+
23+
private final ObjectMapper objectMapper;
24+
private final SensitiveDataMasker sensitiveDataMasker;
25+
26+
public String extractParameters(ProceedingJoinPoint joinPoint) {
27+
try {
28+
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
29+
Parameter[] parameters = signature.getMethod().getParameters();
30+
Object[] args = joinPoint.getArgs();
31+
32+
if (parameters.length == 0) {
33+
return "{}";
34+
}
35+
36+
Map<String, Object> paramMap = new LinkedHashMap<>();
37+
for (int i = 0; i < parameters.length; i++) {
38+
if (args[i] != null) {
39+
addParameter(paramMap, parameters[i], args[i]);
40+
}
41+
}
42+
43+
return objectMapper.writeValueAsString(paramMap);
44+
} catch (Exception e) {
45+
log.warn("파라미터 변환 실패: {}", e.getMessage());
46+
return "{}";
47+
}
48+
}
49+
50+
private void addParameter(Map<String, Object> paramMap, Parameter parameter, Object value) throws JsonProcessingException {
51+
String paramName = extractParamName(parameter);
52+
paramMap.put(paramName, sensitiveDataMasker.maskSensitiveData(paramName, value, objectMapper));
53+
}
54+
55+
private String extractParamName(Parameter parameter) {
56+
PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);
57+
if (pathVariable != null && !pathVariable.value().isEmpty()) {
58+
return pathVariable.value();
59+
}
60+
61+
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
62+
if (requestParam != null && !requestParam.value().isEmpty()) {
63+
return requestParam.value();
64+
}
65+
66+
return parameter.getName();
67+
}
68+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.somemore.global.aspect.log.extractor;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import org.springframework.stereotype.Component;
5+
import org.springframework.web.context.request.RequestContextHolder;
6+
import org.springframework.web.context.request.ServletRequestAttributes;
7+
8+
@Component
9+
public class RequestExtractor {
10+
11+
public HttpServletRequest getCurrentRequest() {
12+
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
13+
if (attributes == null) {
14+
throw new IllegalStateException("요청을 찾을수 없습니다.");
15+
}
16+
return attributes.getRequest();
17+
}
18+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.somemore.global.aspect.log.extractor;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.somemore.global.common.response.LoggedResponse;
5+
import com.somemore.global.exception.DuplicateException;
6+
import com.somemore.global.exception.ImageUploadException;
7+
import com.somemore.global.exception.BadRequestException;
8+
import com.somemore.global.exception.NoSuchElementException;
9+
import lombok.RequiredArgsConstructor;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.http.HttpStatus;
12+
import org.springframework.http.ResponseEntity;
13+
import org.springframework.stereotype.Component;
14+
import org.springframework.web.bind.MethodArgumentNotValidException;
15+
16+
@RequiredArgsConstructor
17+
@Slf4j
18+
@Component
19+
public class ResponseExtractor {
20+
21+
private final ObjectMapper objectMapper;
22+
23+
public LoggedResponse extractResponse(Object result) {
24+
try {
25+
if (result == null) {
26+
return new LoggedResponse(HttpStatus.OK, "null");
27+
}
28+
29+
if (result instanceof ResponseEntity<?> responseEntity) {
30+
String body = objectMapper.writeValueAsString(responseEntity.getBody());
31+
return new LoggedResponse(responseEntity.getStatusCode(), body);
32+
}
33+
34+
return new LoggedResponse(HttpStatus.OK, objectMapper.writeValueAsString(result));
35+
} catch (Exception e) {
36+
log.warn("응답 변환 실패: {}", e.getMessage());
37+
return new LoggedResponse(HttpStatus.OK, "[응답 변환 실패]");
38+
}
39+
}
40+
41+
public HttpStatus extractExceptionStatus(Exception e) {
42+
if (e instanceof BadRequestException ||
43+
e instanceof ImageUploadException ||
44+
e instanceof DuplicateException ||
45+
e instanceof MethodArgumentNotValidException) {
46+
return HttpStatus.BAD_REQUEST;
47+
} else if (e instanceof NoSuchElementException) {
48+
return HttpStatus.NOT_FOUND;
49+
}
50+
return HttpStatus.INTERNAL_SERVER_ERROR;
51+
}
52+
}

0 commit comments

Comments
 (0)