장애가 터졌는데 로그를 열어보니 단서가 없었습니다. 에러는 났지만 어떤 요청이, 어떤 값으로, 어디서 실패했는지 알 수 없었습니다. 로그가 너무 적어서가 아니라, 필요한 정보 없이 쓸모없는 로그만 잔뜩 쌓여 있었기 때문입니다. 반대로 어떤 서비스는 로그가 디스크를 가득 채워 서버가 멈추기도 합니다. 운영 로깅은 “많이 남기는” 문제가 아니라 “무엇을 어떻게 남기느냐”의 문제입니다.
이 글은 운영 환경에서 로그를 쓸모 있게 만드는 실전 체크리스트입니다. 로그 레벨을 규칙으로 정하는 법, 민감정보를 가리는 법, 검색 가능한 구조적 로깅, 예외를 남기는 올바른 방법, 그리고 로그 파일 관리와 성능까지 순서대로 짚습니다.
로그 레벨을 규칙으로 정한다
가장 먼저 팀이 로그 레벨의 의미를 합의해야 합니다. 레벨 기준이 사람마다 다르면, ERROR가 남발돼 진짜 장애가 묻히거나 중요한 이벤트가 DEBUG에 숨어버립니다.
| 레벨 | 의미 | 예시 |
|---|---|---|
| ERROR | 즉시 대응이 필요한 실패 | 결제 처리 실패, 외부 연동 불가 |
| WARN | 비정상이지만 처리는 됨/잠재 위험 | 재시도 성공, 임계치 근접 |
| INFO | 주요 비즈니스 이벤트·상태 변화 | 주문 생성, 배치 시작/종료 |
| DEBUG | 문제 분석용 상세 정보 | 분기 조건, 중간 계산값 |
| TRACE | 매우 상세한 추적 | 외부 요청/응답 원문 |
운영 환경은 보통 INFO 이상만 출력하고, DEBUG·TRACE는 문제 분석이 필요할 때만 일시적으로 엽니다. 핵심 기준은 “이 로그를 보고 대응할 사람이 있는가”입니다. 아무도 조치하지 않을 내용을 ERROR로 남기면, 알림 피로가 쌓여 정작 중요한 ERROR를 놓치게 됩니다. 예외를 읽고 원인을 좁히는 기본기는 신입 개발자 디버깅 가이드에서도 정리한 바 있습니다.
무엇을 남기고, 무엇을 절대 남기지 않나
로그의 가치는 문제를 재현할 수 있게 하는 맥락에서 나옵니다. 그래서 어떤 요청이었는지 식별할 수 있는 정보를 함께 남겨야 합니다. 요청 ID, 사용자 식별자(가명 처리된), 주요 파라미터, 처리 결과 같은 것들입니다. 이런 맥락 없이 “처리 실패”만 찍힌 로그는 사후 추적에 거의 도움이 되지 않습니다.
반대로 절대 로그에 남기면 안 되는 것이 있습니다. 비밀번호, 주민등록번호, 카드번호, 인증 토큰, 그리고 마스킹하지 않은 개인정보입니다. 이런 값이 평문으로 로그에 남으면 그 자체가 심각한 보안·법적 리스크가 됩니다.
- 요청/응답 본문을 통째로 찍지 않는다. 꼭 필요하면 민감 필드를 마스킹한다.
- 카드번호는 앞 6자리·뒤 4자리만, 이메일·전화번호는 일부만 노출한다.
- 마스킹은 개별 로그 호출에 맡기지 말고, 공통 마스킹 유틸이나 로깅 필터로 강제한다.
구조적 로깅(JSON)으로 검색 가능하게 만든다
로그가 사람이 눈으로 읽는 텍스트에 머물면, 수천만 줄 속에서 원하는 것을 찾기 어렵습니다. 운영 규모에서는 구조적 로깅, 즉 로그를 key=value나 JSON 형태로 남겨 기계가 검색·집계할 수 있게 하는 것이 큰 차이를 만듭니다.
핵심은 요청 하나를 끝까지 추적할 수 있는 식별자를 모든 로그에 붙이는 것입니다. 요청이 들어올 때 traceId를 발급해 로그마다 포함시키면, 한 요청이 여러 계층을 거치며 남긴 로그를 한 번에 모아볼 수 있습니다. 자바·스프링에서는 MDC(Mapped Diagnostic Context)에 이 값을 넣어두면 이후의 모든 로그에 자동으로 따라붙습니다.
여기서 주의할 점이 하나 있습니다. MDC는 스레드 로컬에 저장되므로, 스레드풀에서 스레드가 재사용될 때 이전 요청의 값이 남아 오염될 수 있습니다. 요청 처리가 끝나면 반드시 MDC를 비워야 합니다. 비동기 작업으로 넘어갈 때 이 맥락이 자동 전파되지 않는 점도 RestTemplate 타임아웃 누락, 전 서버가 멈춘 날 같은 문제를 추적할 때 함께 챙겨야 할 부분입니다.
예외는 스택트레이스와 함께, 한 번만 남긴다
예외 로깅에는 흔한 실수가 두 가지 있습니다. 첫째, 스택트레이스를 버리는 것입니다. 예외 메시지만 문자열로 이어 붙이면 어디서 터졌는지 알 수 없습니다. 로거에 예외 객체를 마지막 인자로 넘겨야 스택트레이스가 함께 남습니다.
// 나쁜 예: 스택트레이스가 사라짐
log.error("결제 실패: " + e.getMessage());
// 좋은 예: 예외 객체를 인자로 넘겨 스택트레이스까지 기록
log.error("결제 실패. orderId={}", orderId, e);
둘째는 로그 앤 스로우(log-and-throw)입니다. 예외를 잡아 로그를 남기고 다시 던지면, 상위에서 또 로그를 남겨 같은 예외가 여러 번 찍힙니다. 로그가 중복되면 실제 발생 건수를 파악하기 어려워집니다. 예외는 처리하는 곳에서 한 번만 로깅하고, 중간 계층은 잡지 않고 흘려보내거나 맥락만 덧붙여 다시 던지는 것이 깔끔합니다. 슬로우 쿼리 같은 문제를 로그로 추적한 사례는 MySQL 쿼리 지연 해결: 3개월 클라우드 DB 분투기에서 확인할 수 있습니다.
로그 파일 관리: 롤링과 보관 정책
로그를 잘 남겨도 파일 관리를 소홀히 하면, 로그가 디스크를 가득 채워 서비스가 멈추는 사고로 이어집니다. 실제로 흔한 장애 원인입니다.
- 롤링 정책을 설정한다. 파일 크기나 날짜 기준으로 로그를 분할해, 한 파일이 무한히 커지지 않게 한다.
- 보관 기간과 총량 상한을 둔다. 오래된 로그는 자동 삭제하고, 전체 로그 용량의 상한을 정해 디스크 고갈을 원천 차단한다.
- 압축을 켠다. 지난 로그는 gzip 등으로 압축해 보관 비용을 줄인다.
logback을 쓴다면 크기·시간 기반 롤링에 maxHistory(보관 일수)와 totalSizeCap(총 용량 상한)을 함께 지정하는 것이 안전합니다. 이 두 값이 없으면, 트래픽이 몰리는 날 로그가 순식간에 디스크를 채울 수 있습니다.
로깅이 서비스를 느리게 하지 않게 한다
로깅도 비용입니다. 잘못 쓰면 응답 속도를 떨어뜨립니다. 특히 요청마다 대량의 로그를 남기거나, 로그를 파일에 동기로 기록하는 경우 부하가 커집니다.
// 나쁜 예: 레벨이 꺼져 있어도 문자열을 항상 조립한다
log.debug("응답 본문: " + heavyToString(response));
// 좋은 예: 파라미터 방식 — DEBUG가 꺼져 있으면 조립조차 하지 않는다
log.debug("응답 본문: {}", response);
로그 메시지는 문자열을 직접 이어 붙이지 말고 파라미터 방식({})을 쓰는 것이 기본입니다. 이렇게 하면 해당 레벨이 비활성일 때 불필요한 문자열 생성 자체를 건너뜁니다. 무거운 연산이 들어가는 로그는 isDebugEnabled()로 감싸고, 처리량이 높은 서비스라면 비동기 appender로 파일 기록을 분리하는 것도 방법입니다. 반복문 안에서 매 반복마다 로그를 남기는 습관은 특히 조심해야 합니다.
운영 투입 전 체크리스트
마지막으로, 서비스를 운영에 올리기 전 로깅을 점검하는 체크리스트입니다. 하나씩 확인하면 “장애는 났는데 단서는 없는” 상황을 대부분 예방할 수 있습니다.
- [ ] 로그 레벨 기준을 팀이 합의했고, 운영은 INFO 이상으로 설정했는가
- [ ] 모든 로그에 요청을 식별할
traceId가 붙는가 (MDC 설정 + 요청 종료 시 clear) - [ ] 비밀번호·주민번호·카드번호·토큰이 로그에 평문으로 남지 않는가 (마스킹 강제)
- [ ] 예외는 스택트레이스와 함께, 처리 지점에서 한 번만 남기는가
- [ ] 롤링·보관 기간·총 용량 상한을 설정해 디스크 고갈을 막았는가
- [ ] 로그 메시지에 파라미터 방식(
{})을 쓰고, 반복문·고빈도 경로의 과도한 로그를 걷어냈는가 - [ ] 구조적 로깅(JSON 등)으로 검색·집계가 가능한 형태인가