배경
서비스 전반에 반복되는 로깅 코드가 많아 가독성과 유지보수성이 떨어졌습니다. 공통 관심사인 로깅을 AOP로 분리해 DRY 원칙(Don't Repeat Yourself)을 지키는 것을 목표로 했습니다.
목표
- 서비스/컨트롤러/레포지토리 단의 공통 로깅을 AOP로 분리
- 메서드 실행 시간 로깅으로 병목 구간 식별
- 예외 로깅 표준화
- 기존 서비스 코드에서 불필요한 시작/완료 로그 제거
설계
- ServiceLoggingAspect: 서비스 레이어의 메서드 시작/완료 자동 로깅
- PerformanceLoggingAspect: 서비스/컨트롤러 실행 시간 로깅
- ExceptionLoggingAspect: 서비스/컨트롤러/레포지토리 예외 로깅
핵심 포인트컷
// 서비스 레이어
execution(* tryonu.api.service..*ServiceImpl.*(..))
// 컨트롤러 레이어
execution(* tryonu.api.controller..*Controller.*(..))
// 레포지토리 예외 로깅
execution(* tryonu.api.repository..*Repository*.*(..))

구현 포인트
1) ServiceLoggingAspect 인자 포매팅 성능 개선
인자 문자열 결합에서 reduce 대신 Collectors.joining을 사용해 효율 개선.
private String formatArguments(Object[] args) {
return Arrays.stream(args)
.map(arg -> arg == null ? "null" : {
String s = arg.toString();
String name = arg.getClass().getSimpleName();
if (name.contains("MultipartFile")) return "MultipartFile[...]";
if (s.length() > 100) return name + "[" + s.substring(0, 100) + "...]";
return s;
})
.collect(Collectors.joining(", "));
}
잠시 reduce와 Collectors.joining을 비교해보겠습니다.
기존 방식: reduce와 문자열 접합(+)
// 예시: reduce를 사용한 비효율적인 방식
.reduce((s1, s2) -> s1 + ", " + s2)
.orElse("");
이 방식의 가장 큰 문제는 Java에서 문자열(String) 객체가 불변(Immutable)하다는 특성 때문에 발생합니다.
- s1 + ", " + s2 연산이 실행될 때마다 기존의 s1, s2 문자열은 그대로 둔 채, 완전히 새로운 String 객체가 메모리에 생성됩니다.
- 예를 들어, 인자가 10개라면 이 결합 과정에서 최소 9개의 불필요한 중간 결과 문자열 객체가 생성되고 버려집니다.
- 인자의 개수가 많아질수록 이 비효율은 기하급수적으로 커져서 CPU와 메모리에 부담을 줄 수 있습니다. 로깅은 매우 자주 일어나는 작업이니까요!!
🚀 개선된 방식: Collectors.joining
.collect(Collectors.joining(", "));
Collectors.joining은 내부적으로 StringBuilder 또는 StringJoiner 와 같은 가변(Mutable) 객체를 사용합니다.
- StringBuilder는 단 하나의 객체만 생성한 뒤, 기존 객체에 계속해서 문자열을 **추가(append)**합니다.
- 스트림의 모든 요소를 다 순회한 후, 마지막에 딱 한 번 .toString()을 호출하여 최종 결과 String 객체를 생성합니다.
- 결과적으로, 결합 과정에서 불필요한 중간 객체가 전혀 생성되지 않아 매우 빠르고 메모리 효율적입니다.
2) 성능 로깅 기준 일원화
- 서비스: 100ms 이상만 경고 로그
- 컨트롤러: 응답시간 단계별 로깅 (상수로 관리)
3) 예외 로깅의 역할 명확화
- 클래스 설명을 실제 동작(각 레이어)과 일치하도록 정제
- 비즈니스 예외(CustomException)와 시스템 예외를 구분해서 로깅
적용 효과
- 서비스 코드에서 반복 로깅 제거 → 핵심 로직만 남아 가독성↑
- 실행 시간 자동 수집 → 느린 경로 즉시 파악
마무리
AOP로 로깅을 중앙화해 코드가 한층 가벼워졌습니다. 특히 인자 포매팅의 Collectors.joining 도입으로 작은 비용까지 줄였다는 점이 만족스럽네요. 다음은 임계치/로그 레벨을 환경별로 조정 가능한 설정화까지 확장할 계획입니다.
참고
[Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP
| 스프링 AOP ( Aspect Oriented Programming ) AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로
engkimbs.tistory.com
'프로덕트 문제 해결 기록' 카테고리의 다른 글
| WebClient와 CachingFilter 사용 시 InputStream 재사용 오류 해결 (0) | 2025.09.03 |
|---|---|
| [ThatzFit] Spring Data JPA 비관적 락 리팩토링: 문제 해결 기록 (1) | 2025.09.01 |
| [ThatzFit] 트랜잭션 최적화: 쓰기 경계 분리 (1) | 2025.09.01 |
| [ThatzFit] API 에러 모니터링: 체계적인 슬랙(Slack) 알림 시스템 구축기 (0) | 2025.08.14 |
| [WIMI] Spring에서 "No acceptable representation" 오류 (0) | 2025.03.28 |