Skip to content

Commit bed7f69

Browse files
committed
refactor: logging AOP 리팩토링
- 로그 기록 세분화 - 민감한 데이터 마스킹 처리 - 책임에 따른 클래스 분리 - 마스킹 테스트및 검증 완
1 parent 265a3e9 commit bed7f69

File tree

6 files changed

+396
-0
lines changed

6 files changed

+396
-0
lines changed
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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.extern.slf4j.Slf4j;
7+
import org.aspectj.lang.ProceedingJoinPoint;
8+
import org.aspectj.lang.reflect.MethodSignature;
9+
import org.springframework.stereotype.Component;
10+
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.RequestParam;
12+
13+
import java.lang.reflect.Parameter;
14+
import java.util.LinkedHashMap;
15+
import java.util.Map;
16+
17+
@Slf4j
18+
@Component
19+
public class ParameterExtractor {
20+
21+
private final ObjectMapper objectMapper;
22+
private final SensitiveDataMasker sensitiveDataMasker;
23+
24+
public ParameterExtractor(ObjectMapper objectMapper) {
25+
this.objectMapper = objectMapper;
26+
this.sensitiveDataMasker = new SensitiveDataMasker();
27+
}
28+
29+
public String extractParameters(ProceedingJoinPoint joinPoint) {
30+
try {
31+
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
32+
Parameter[] parameters = signature.getMethod().getParameters();
33+
Object[] args = joinPoint.getArgs();
34+
35+
if (parameters.length == 0) {
36+
return "{}";
37+
}
38+
39+
Map<String, Object> paramMap = new LinkedHashMap<>();
40+
for (int i = 0; i < parameters.length; i++) {
41+
if (args[i] != null) {
42+
addParameter(paramMap, parameters[i], args[i]);
43+
}
44+
}
45+
46+
return objectMapper.writeValueAsString(paramMap);
47+
} catch (Exception e) {
48+
log.warn("파라미터 변환 실패: {}", e.getMessage());
49+
return "{}";
50+
}
51+
}
52+
53+
private void addParameter(Map<String, Object> paramMap, Parameter parameter, Object value) throws JsonProcessingException {
54+
String paramName = extractParamName(parameter);
55+
paramMap.put(paramName, sensitiveDataMasker.maskSensitiveData(paramName, value, objectMapper));
56+
}
57+
58+
private String extractParamName(Parameter parameter) {
59+
PathVariable pathVariable = parameter.getAnnotation(PathVariable.class);
60+
if (pathVariable != null && !pathVariable.value().isEmpty()) {
61+
return pathVariable.value();
62+
}
63+
64+
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
65+
if (requestParam != null && !requestParam.value().isEmpty()) {
66+
return requestParam.value();
67+
}
68+
69+
return parameter.getName();
70+
}
71+
}
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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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.extern.slf4j.Slf4j;
10+
import org.springframework.http.HttpStatus;
11+
import org.springframework.http.ResponseEntity;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.web.bind.MethodArgumentNotValidException;
14+
15+
@Slf4j
16+
@Component
17+
public class ResponseExtractor {
18+
19+
private final ObjectMapper objectMapper;
20+
21+
public ResponseExtractor(ObjectMapper objectMapper) {
22+
this.objectMapper = objectMapper;
23+
}
24+
25+
public LoggedResponse extractResponse(Object result) {
26+
try {
27+
if (result == null) {
28+
return new LoggedResponse(HttpStatus.OK, "null");
29+
}
30+
31+
if (result instanceof ResponseEntity<?> responseEntity) {
32+
String body = objectMapper.writeValueAsString(responseEntity.getBody());
33+
return new LoggedResponse(responseEntity.getStatusCode(), body);
34+
}
35+
36+
return new LoggedResponse(HttpStatus.OK, objectMapper.writeValueAsString(result));
37+
} catch (Exception e) {
38+
log.warn("응답 변환 실패: {}", e.getMessage());
39+
return new LoggedResponse(HttpStatus.OK, "[응답 변환 실패]");
40+
}
41+
}
42+
43+
public HttpStatus extractExceptionStatus(Exception e) {
44+
if (e instanceof BadRequestException ||
45+
e instanceof ImageUploadException ||
46+
e instanceof DuplicateException ||
47+
e instanceof MethodArgumentNotValidException) {
48+
return HttpStatus.BAD_REQUEST;
49+
} else if (e instanceof NoSuchElementException) {
50+
return HttpStatus.NOT_FOUND;
51+
}
52+
return HttpStatus.INTERNAL_SERVER_ERROR;
53+
}
54+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package com.somemore.global.aspect.log.utils;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.util.*;
10+
11+
@RequiredArgsConstructor
12+
@Slf4j
13+
@Component
14+
public class SensitiveDataMasker {
15+
16+
private final Set<String> sensitiveFields = new HashSet<>(Arrays.asList(
17+
"password", "token", "secret", "credential", "authorization",
18+
"accessToken", "refreshToken"
19+
));
20+
21+
public Object maskSensitiveData(String fieldName, Object value, ObjectMapper objectMapper) throws JsonProcessingException {
22+
if (isSensitiveField(fieldName)) {
23+
return "********";
24+
} else if (value instanceof Map) {
25+
return maskSensitiveDataInMap((Map<?, ?>) value);
26+
} else if (isComplexObject(value)) {
27+
String json = objectMapper.writeValueAsString(value);
28+
json = maskSensitiveDataInJson(json, objectMapper);
29+
return objectMapper.readValue(json, Object.class);
30+
}
31+
return value;
32+
}
33+
34+
private boolean isSensitiveField(String fieldName) {
35+
String lowercaseFieldName = fieldName.toLowerCase();
36+
return sensitiveFields.stream()
37+
.anyMatch(sensitive -> lowercaseFieldName.contains(sensitive.toLowerCase()));
38+
}
39+
40+
private Map<String, Object> maskSensitiveDataInMap(Map<?, ?> map) {
41+
Map<String, Object> maskedMap = new LinkedHashMap<>();
42+
43+
map.forEach((key, value) -> {
44+
String keyStr = String.valueOf(key);
45+
if (isSensitiveField(keyStr)) {
46+
maskedMap.put(keyStr, "********");
47+
} else if (value instanceof Map) {
48+
maskedMap.put(keyStr, maskSensitiveDataInMap((Map<?, ?>) value));
49+
} else {
50+
maskedMap.put(keyStr, value);
51+
}
52+
});
53+
54+
return maskedMap;
55+
}
56+
57+
private String maskSensitiveDataInJson(String json, ObjectMapper objectMapper) {
58+
try {
59+
Map<String, Object> jsonMap = objectMapper.readValue(json, Map.class);
60+
Map<String, Object> maskedMap = maskSensitiveDataInMap(jsonMap);
61+
return objectMapper.writeValueAsString(maskedMap);
62+
} catch (Exception e) {
63+
log.warn("JSON 마스킹 처리 실패: {}", e.getMessage());
64+
return json;
65+
}
66+
}
67+
68+
private boolean isComplexObject(Object value) {
69+
return !(value instanceof String || value instanceof Number ||
70+
value instanceof Boolean || value instanceof Date);
71+
}
72+
}

0 commit comments

Comments
 (0)