TL;DR
개발자마다 제각각이던 860개 로그를 AOP 하나로 정리했다. 비밀번호는 자동으로 가려지고, 실행 시간도 알아서 찍힌다.
문제: 개발자마다 다른 로깅 스타일
우리 프로젝트 코드베이스를 분석해보니 충격적인 결과가 나왔다:
// AuthController - 과도하게 자세한 로그
log.debug("로그인 요청 수신: username={}", request.getUsername());
log.debug("로그인 성공: username={}, userId={}", ...);
log.info("로그인 요청 Origin 헤더: {}", ...);
log.info("모든 헤더: {}", ...);
// UserController - 로그가 아예 없음
public ResponseEntity<...> getUser(...) {
// 침묵...
}
// StudentController - 중복되는 패턴
log.info("학생 목록 조회 요청 - classId: {}, grade: {}, ...");
log.info("학생 목록 조회 성공 - 조회된 학생 수: {}", students.size());
86개 파일, 860개 로그 호출, 0개의 일관성.
디버깅할 때마다 “이 API는 로그가 있나?” 확인하는 게 일상이었다.
해결: AOP로 모든 Controller 자동 로깅
로깅 정책 수립
먼저 뭘 자동화하고 뭘 수동으로 남길지 정했다:
✅ AOP 자동 처리 (DEBUG 레벨)
- 모든 Controller 메서드 호출
- 요청 파라미터 (민감 정보 제외)
- 실행 시간
- 성공/실패 여부
✅ 개발자 직접 작성 (INFO 레벨)
- 로그인, 회원가입 등 중요 비즈니스 이벤트
- 데이터 생성/수정/삭제
ControllerLoggingAspect 구현
@Aspect
@Component
@Slf4j
public class ControllerLoggingAspect {
@Around("@within(org.springframework.web.bind.annotation.RestController)")
public Object logControllerMethods(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = method.getName();
// HTTP 메서드 추출 (GET, POST, PUT, DELETE)
String httpMethod = extractHttpMethod(method);
// 안전한 파라미터만 추출 (민감 정보 필터링)
Map<String, Object> params = extractSafeParameters(method, joinPoint.getArgs());
String paramsStr = formatParameters(params);
// 요청 로그
log.debug("[{}] {} {}() called with [{}]",
className, httpMethod, methodName, paramsStr);
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
// 성공 로그 + 실행 시간
long duration = System.currentTimeMillis() - startTime;
log.debug("[{}] {} {}() completed successfully in {}ms",
className, httpMethod, methodName, duration);
return result;
} catch (Exception e) {
// 실패 로그
long duration = System.currentTimeMillis() - startTime;
log.debug("[{}] {} {}() failed in {}ms with exception: {}",
className, httpMethod, methodName, duration, e.getClass().getSimpleName());
throw e;
}
}
}
핵심 기능: 민감 정보 자동 필터링
로그에 비밀번호가 찍히면 보안 사고다. 자동 필터링을 구현했다:
private static final String[] SENSITIVE_PARAM_NAMES = {
"password", "pwd", "token", "accessToken", "refreshToken",
"secret", "apiKey", "authorization", "cookie"
};
private Map<String, Object> extractSafeParameters(Method method, Object[] args) {
Map<String, Object> params = new HashMap<>();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
String paramName = parameters[i].getName();
// 민감 정보는 필터링
if (isSensitiveParameter(paramName)) {
params.put(paramName, "***FILTERED***");
continue;
}
// Spring 내부 객체는 제외
if (isSpringFrameworkParameter(parameters[i].getType())) {
continue;
}
// 안전한 파라미터만 추가
Object arg = args[i];
if (arg == null) {
params.put(paramName, "null");
} else if (isPrimitiveOrWrapper(arg)) {
params.put(paramName, arg);
} else {
// 복잡한 객체는 클래스명만 (toString() 오버헤드 방지)
params.put(paramName, arg.getClass().getSimpleName());
}
}
return params;
}
실제 로그 출력
// Before: 비밀번호 노출
[AuthController] POST login() called with [username=testuser, password=secret123!]
// After: 자동 필터링
[AuthController] POST login() called with [username=testuser, password=***FILTERED***]
HTTP 메서드 자동 추출
디버깅 시 HTTP 메서드를 보면 API를 바로 알 수 있다:
private String extractHttpMethod(Method method) {
if (method.isAnnotationPresent(GetMapping.class)) {
return "GET";
} else if (method.isAnnotationPresent(PostMapping.class)) {
return "POST";
} else if (method.isAnnotationPresent(PutMapping.class)) {
return "PUT";
} else if (method.isAnnotationPresent(DeleteMapping.class)) {
return "DELETE";
}
// RequestMapping은 method 배열 확인
else if (method.isAnnotationPresent(RequestMapping.class)) {
RequestMapping mapping = method.getAnnotation(RequestMapping.class);
if (mapping.method().length > 0) {
return mapping.method()[0].name();
}
}
return "HTTP";
}
성능 최적화
1. toString() 오버헤드 방지
// ❌ Bad: 큰 객체의 toString() 호출
log.debug("Request: {}", request); // 느림
// ✅ Good: 클래스명만 출력
params.put(paramName, arg.getClass().getSimpleName()); // "StudentRequest"
2. 프로덕션에서 DEBUG 끄기
# application-prod.yml
logging:
level:
saomath.checkusserver: INFO # DEBUG 로그 비활성화
saomath.checkusserver.common.aspect: INFO # AOP 로그도 비활성화
로그 레벨이 INFO면 log.debug()는 실행조차 안 된다. 성능 영향 제로.
3. Spring 내부 객체 제외
private boolean isSpringFrameworkParameter(Class<?> paramType) {
String packageName = paramType.getPackage() != null ?
paramType.getPackage().getName() : "";
return packageName.startsWith("jakarta.servlet") ||
packageName.startsWith("org.springframework.security") ||
paramType.getName().contains("Authentication") ||
paramType.getName().contains("Principal");
}
HttpServletRequest, Authentication 같은 건 로그에서 제외. 의미도 없고 로그만 길어진다.
적용 결과
실제 로그 예시
[StudentController] GET getStudents() called with [classId=1, grade=5, status=ENROLLED]
[StudentController] GET getStudents() completed successfully in 42ms
[AuthController] POST login() called with [username=testuser, password=***FILTERED***]
[AuthController] POST login() completed successfully in 156ms
[TaskController] POST createTask() called with [request=TaskInstanceRequest]
[TaskController] POST createTask() failed in 23ms with exception: BusinessException
깔끔하다. 언제 호출됐는지, 파라미터가 뭔지, 얼마나 걸렸는지 한눈에 보인다.
개발자는 이제 중요한 것만
@PostMapping
public ResponseEntity<...> createStudent(@RequestBody StudentRequest request) {
StudentDetailResponse response = studentService.createStudentByTeacher(request);
// 중요한 비즈니스 이벤트만 INFO로
log.info("학생 계정 생성: studentId={}, username={}",
response.getId(), response.getUsername());
return ResponseEntity.status(HttpStatus.CREATED).body(...);
}
메서드 호출, 파라미터, 실행 시간은 AOP가 자동으로 처리한다.
숫자로 보는 개선 효과
Before
- 86개 파일에 860개 로그 호출
- 개발자마다 다른 로깅 스타일
- 민감 정보 노출 위험
- 디버깅 시 로그 유무 확인 필요
After
- 1개의 AOP Aspect로 모든 Controller 커버
- 100% 일관된 로그 포맷
- 민감 정보 자동 필터링
- 실행 시간 자동 측정
- 30분만에 구현 완료 (예상 4-6시간)
배운 점
1. 로깅 정책이 코드보다 중요하다
코드 짜기 전에 “뭘 로그로 남길지” 정하는 게 먼저다.
2. 민감 정보 필터링은 자동화하라
사람은 실수한다. 자동 필터링으로 보안 사고를 방지하자.
3. 프로덕션과 개발 환경을 구분하라
개발: DEBUG 로그로 디버깅 편하게 프로덕션: INFO 이상만 남겨서 성능 확보
4. AOP의 한계를 알아두자
내부 메서드 호출은 프록시를 안 거쳐서 로그가 안 남는다:
public void outerMethod() {
innerMethod(); // AOP 적용 안 됨
}
private void innerMethod() {
// 로그 안 남음
}
프로덕션 체크리스트
배포 전 확인사항:
- 로그 레벨 INFO로 설정
- 민감 정보 필터링 동작 확인
- 새로운 민감 파라미터 추가 시
SENSITIVE_PARAM_NAMES업데이트 - 성능 모니터링
결론: 860개 로그를 하나의 AOP로 대체했다. 일관성, 보안, 성능 모두 잡았다. 🎯
Comments