
이 글은 '야구보구' 프로젝트를 진행하며 수립했던 서버 로깅 전략에 대해 공유하기 위함입니다.
저희 팀이 어떤 고민을 통해 로깅 시스템을 설계하고 구현했는지 상세하게 기록해 보았습니다.
1. 로깅은 왜 중요한가요?
본격적인 설명에 앞서, 저희가 정의한 로깅의 핵심 목표는 다음과 같습니다.
- 서버 동작 상태 파악
- 요청 흐름, 주요 도메인 이벤트 추적
- traceId 기반으로 문제 요청 흐름을 파악할 수 있음
- 장애 파악 & 알림 트리거
- 예기치 못한 예외, 시스템 오류를 빠르게 감지
- Sentry, CloudWatch 등 외부 시스템과 연동 가능
- 로그 분석을 통한 서비스 지표 확인
- API 사용량, 특정 기능의 사용 빈도, 오류 발생율, 응답 시간 등
- 운영 환경에서 의미 있는 서비스 통계 도출 가능
2. Log4j2 선택과 로그 레벨 정의
저희 프로젝트에서는 Logback 대신 Log4j2를 로깅 프레임워크로 선택했습니다.
로그 레벨은 다음과 같은 기준으로 정의하여 사용했습니다.
FATAL > ERROR > WARN > INFO > DEBUG > TRACE
1) Error
런타임 예외(runtime exception)나 NullPointerException 같은 치명적 오류를 여기서 잡자
개발자가 반드시 보고 고쳐야 함.
ex) YaguBoguException 애플리케이션 내부에서 예상하지 못한 심각한 서버 오류를 의미하는 커스텀 예외
ex) RuntimeException 널포인터, 인덱스 초과 등 코드 실행 중 발생하는 비예측 예외
2) Warn
예측 가능한 예외.
비정상적이긴 하지만 서버가 계속 정상 동작할 수 있는 경고성 문제임
경고 수준 문제임. 서비스 계속 가능하지만 주의 필요한 상황
ex) UnAuthorizedException, ForbiddenException, BadGatewayException
3) Info
찾는 데이터가 없다는 것은 예외라기보다 정상적으로 발생할 수 있는 상황이므로 NotFound에러는 INFO 레벨로 기록
정상 처리 중 발생하는 예측 가능한 상태임. 참고용 로그
보통은 공격이나 비정상 행위가 아니면 info로 처리한대. 즉, 로그인 실패도 info임.
ex) BadRequestException, NotFoundException, UnprocessableEntityException
3. 로그 레벨별 모니터링 및 알림 기준
저희는 각 로그 레벨에 따라 다음과 같은 모니터링 및 알림 기준을 설정했습니다.
| 레벨 | 정의 | 예시 | 알림 기준 |
| ERROR | 예측하지 못한 치명적인 시스템 오류 | 시스템 코드 오류, 인프라 문제, 예상치 못한 예외 | 즉시 |
| WARN | 악의적 사용자로 의심 가는 상황이거나, 시스템의 이상 상태가 될 가능성이 있을 때 | 주의 필요한 입력, 외부 문제, 반복적 흐름 이상 징후 | 5분 내 3건 이상 발생 시 |
| INFO | 일반 사용자라면 충분히 할 수 있는 행동 | 사용자 잘못된 요청, 유효성 실패, 존재하지 않는 ID | 동일 사용자가 같은 유형의 실패 요청을 1분 내 100회 이상 반복할 경우 |
| DEBUG | 개발자 디버깅을 위한 상세한 내부 정보 | 내부 파라미터, 조건문 결과 등 | 없음 (개발 환경에서만 사용) |
4. API 로깅 전략: Interceptor vs AOP
API 요청/응답을 효율적으로 로깅하기 위해 야구보구는 Interceptor와 AOP를 목적에 맞게 조합하여 사용했습니다.
1) Interceptor 특징과 용도
Interceptor는 디스패처 서블릿과 컨트롤러 사이에서 동작하며, 주로 HTTP 요청 자체의 메타데이터를 로깅하는 데 사용했습니다.
특징
- 컨트롤러에 진입하는 모든 요청을 가로챌 수 있습니다.
- 컨트롤러 메서드의 시그니처(이름, 파라미터 등) 정보에는 접근하기 어렵습니다.
- Request Body를 직접 파싱하지 않는 한, DTO 객체의 내용에 접근하기 번거롭습니다.
- HttpServletRequest를 통해 IP, URI, Header 등 HTTP 프로토콜 정보를 쉽게 얻을 수 있습니다.
용도
| 용도 | 구체적인 예시 | 로깅 이유 |
| IP 검증 | - /api/admin/* 요청인데, 외부 IP에서 호출됨 → 접근 차단 - 톡방에 인증 안 된 기기에서 접근 시 차단 |
공격/불법 접근 탐지 및 차단을 위한 보안 로깅 |
| traceId 생성 | - 요청마다 UUID 하나 생성해서 MDC.put("traceId", uuid)로 넣어두기 - 이후 AOP/Logger 등에서 해당 ID로 로그를 연결 |
하나의 요청에 대해 모든 로그를 연결지어 추적 가능 |
| 요청 URI, 헤더 로깅 | - /api/users/123 요청이 Postman에서 왔는지, 어떤 토큰인지 확인 (Header에 있는 User-Agent에 포함됨) - User-Agent, Authorization 헤더 확인 | 이상 요청/클라이언트 디버깅에 유용 |
| 요청 시간 기록 (선택) | - 요청 시작 시간 기록 → AOP에서 끝나는 시간과 비교 | 전체 요청 처리 시간 측정 가능 |
2) AOP 특징과 용도
AOP는 Service Layer를 중심으로 비즈니스 로직의 흐름과 성능을 상세하게 로깅하는 데 사용했습니다.
특징
- Service, Controller 등 특정 계층의 메서드 실행 전후에 원하는 로직을 주입할 수 있습니다.
- 메서드의 파라미터, 반환값에 직접 접근이 가능하여 비즈니스 로직 중심의 상세 로깅에 유리합니다.
- HTTP 관련 정보(IP, 헤더 등)에 직접 접근하기는 어렵습니다.
- 주로 트랜잭션 처리와 같이 여러 비즈니스 로직에 공통으로 적용되는 부가 기능 구현에 적합합니다.
용도
| 용도 | 구체적인 예시 | 로깅 이유 |
| 서비스 트랜잭션 시간 측정 | - OrderService.createOrder() 호출 → DB 처리까지 2.4초 - 2초 넘는 요청은 WARN으로 기록 |
느린 API를 자동으로 추적하고 성능 개선 대상 선별 |
| 입력 파라미터, 리턴값 확인 | - createUser(UserRequestDto)가 어떤 값으로 호출됐는지 확인 - 응답으로 어떤 데이터가 반환됐는지 확인 |
잘못된 요청이나 이상 동작을 빠르게 분석 가능 |
| 예외 발생 감지 및 로깅 | - ReservationService.reserve() 중 DB 오류 발생 시 예외 로깅 - 어떤 요청에서 어떤 파라미터로 문제가 발생했는지 확인 |
에러 발생 위치, 요청 흐름 파악 가능 |
| 비즈니스 로직 로깅 | - 유저가 어떤 상품을 예약했는지, 결제했는지 등의 상태 추적 - createReservation() 성공 시 유저 ID, 상품 ID 로깅 |
감사/기록용 로그, 나중에 문제 생겼을 때 근거 자료 확보 |
5. '야구보구' 프로젝트의 실제 로깅 구현
이러한 전략을 바탕으로 프로젝트의 로깅 관련 코드는 아래와 같이 global 패키지 하위에 구성했습니다.
.../src/main/java/com/yagubogu/global
.
.
.
├── GlobalExceptionHandler.java
├── LoggingInterceptor.java
└── TransactionalLoggingAspect.java
.../src/main/resources
.
.
.
└── log4j2-spring.xml // 모든 로깅 정책을 정의하는 설정 파일
- GlobalExceptionHandler
- 애플리케이션 전역에서 발생한 에러 로깅
- LoggingInterceptor
- traceId UUID로 랜덤 생성
- MDC 넣기
- 요청시간 로깅(사용자 측면에서 얼마나 걸리는지)
- TransactionalLoggingAspect
- 처음엔 트랜잭션 readOnly=false 걸린 서비스들만 로깅
- 그러니까 데이터 조회 부분은 로깅 안 돼서 서비스 전체로 로깅하도록 수정
- log4j2-spring.xml
- 로그 찍힐 형식, 로그 레벨, 출력 위치, 패키지별 설정 등 로깅 전략 전체를 컨트롤하는 파일
6. 환경별 log4j2-spring.xml 상세 설정
로깅 전략의 핵심은 log4j2-spring.xml 설정 파일에 있습니다.
저희는 prod(운영), dev(개발), local(로컬) 환경별로 로깅 정책을 다르게 적용했습니다.
1) prod
<RollingFile name="ProdFile" fileName="/var/log/yagubogu/prod.log"
filePattern="/var/log/yagubogu/prod-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout
disableAnsi="false"
pattern="%d{yyyy-MM-dd HH:mm:ss} [%X{method} %X{uri}] [%X{controller}.%X{action}] [%highlight{%level}{FATAL=red blink, ERROR=red, WARN=yellow, INFO=green}] [%X{traceId}] [%X{params}] [%X{clientIp}] - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="7"/>
</RollingFile>
- [%X{method} %X{uri}]: [GET /api/users/1] 형태로 HTTP 메서드와 URI를 기록하여 어떤 API에서 문제가 발생했는지 직관적으로 파악할 수 있습니다.
- [%X{controller}.%X{action}]: [UserController.getUserProfile] 형태로 실제 호출된 컨트롤러와 메서드명을 기록합니다. 과거 이 정보가 누락되었을 때, 오류가 발생한 정확한 코드 위치를 추적하는 데 어려움을 겪은 경험이 있어 추가했습니다.
- [%highlight{%level}]: 로그 레벨(ERROR, WARN 등)에 따라 색상을 다르게 표시하여 가독성을 높입니다.
- [%X{traceId}]: Interceptor에서 생성한 요청별 고유 ID를 기록하여 분산 환경에서도 하나의 요청 흐름을 쉽게 추적할 수 있습니다.
- [%X{params}]: [userId=1&status=ACTIVE] 형태로 쿼리 파라미터를 기록합니다.
- 이게 만약 있었다면 우리가 겪은 문제 — score 값이 null인데 responseDto에서 int로 받으면서 NullPointerException이 터진 상황 — 을 훨씬 빨리 파악할 수 있었을 것입니다. 왜냐하면 해당 파라미터로 상황을 재현해 볼 수 있었기 때문입니다.
- [%X{clientIp}]: 요청 클라이언트의 IP를 기록하여 비정상적인 접근(공격, 스팸 등)을 탐지하고 특정 사용자의 요청을 추적하는 데 사용합니다.
2) dev
<RollingFile name="DevFile" fileName="/var/log/yagubogu/dev.log"
filePattern="/var/log/yagubogu/dev-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout
disableAnsi="false"
pattern="%d{yyyy-MM-dd HH:mm:ss} [%X{method} %X{uri}] [%X{controller}.%X{action}] [%highlight{%level}{FATAL=red blink, ERROR=red, WARN=yellow, INFO=green, DEBUG=blue, TRACE=cyan}] [%X{traceId}] [%X{params}] [%X{clientIp}] - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="5"/>
</RollingFile>
prod 환경과의 차이점:
- <Logger name="com.yagubogu" level="debug"/>: 저희가 직접 개발하는 com.yagubogu 패키지에 한해서는 DEBUG 레벨까지 로그를 기록합니다. 이를 통해 서비스 메서드 호출, 내부 파라미터 값 등 상세한 정보를 확인할 수 있습니다.
- 패턴 자체는 prod와 동일하게 유지하여 로그 형식의 일관성을 지켰습니다.
3) local
<RollingFile name="LocalFile" fileName="log/local.log"
filePattern="log/local-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout
disableAnsi="false"
pattern="%d{yyyy-MM-dd HH:mm:ss} [%X{method} %X{uri}] [%X{controller}.%X{action}] [%highlight{%level}{FATAL=red blink, ERROR=red, WARN=yellow, INFO=green, DEBUG=blue, TRACE=cyan}] [%X{params}] - %msg%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<DefaultRolloverStrategy max="5"/>
</RollingFile>
dev 환경과의 차이점:
- fileName="log/local.log": 로그 파일이 서버의 /var/log가 아닌, 프로젝트 내부의 log 폴더에 생성되도록 했습니다.
- 로그 패턴에서 traceId, clientIp 제외: 로컬 환경에서는 모든 요청이 개발자 본인에게서 발생하므로, 요청 추적이나 IP 식별의 필요성이 없어서 패턴을 없앴습니다.
7. dev 서버에서 찍힌 dev.log 관찰

의도한 대로 로깅이 잘 된 것을 볼 수 있었습니다.
'공뷰 > Spring' 카테고리의 다른 글
| [Spring] 야구보구의 실시간 기능, 배포 후 발생한 SSE 문제 정리(nginx 설정) (2) | 2025.10.05 |
|---|---|
| [Spring] Soft Delete와 영속성 컨텍스트 (flush, clear, JPA 엔티티 상태) (0) | 2025.09.29 |
| [Spring] 야구보구 홈 화면 SSE(Server-Sent Events)로 실시간 팬 현황 갱신하기 (5) | 2025.09.15 |
| [Spring] 스프링부트 개발 환경에 따른 application.yml 환경 분리(local, dev, prod) (1) | 2025.07.27 |
| [Spring] Insomnia로 API 테스트하기, JPA 설정, 테이블 생성(Entity) (0) | 2025.01.22 |