299 lines
13 KiB
Markdown
299 lines
13 KiB
Markdown
# Exception Log 기준
|
|
|
|
## 1. 목적
|
|
|
|
이 문서는 예외를 로그로 남길 때의 기준을 정의한다.
|
|
|
|
이 문서의 목표는 다음과 같다.
|
|
|
|
- 같은 실패를 여러 레이어에서 중복 로그하는 일을 줄인다
|
|
- 대표 예외 로그가 운영에 필요한 맥락을 충분히 담게 한다
|
|
- 예외 메시지, stack trace, 민감정보 노출을 통제한다
|
|
- 예외 처리와 예외 로깅의 책임을 분리한다
|
|
|
|
## 2. 근거 수준
|
|
|
|
- Official: Spring Framework / Spring Boot / OWASP 문서에서 직접 확인되는 내용
|
|
- Official + Practice: 공식 기능 위에 일반적인 운영 관행을 결합한 내용
|
|
- Project Recommendation: 이 프로젝트 구조와 운영 방식에 맞춘 규칙
|
|
|
|
## 3. 기본 원칙
|
|
|
|
### 3.1 예외 처리와 예외 로깅은 같은 문제가 아니다
|
|
|
|
Spring은 @ExceptionHandler, @ControllerAdvice, @RestControllerAdvice, ResponseEntityExceptionHandler로 예외를 HTTP 응답으로 변환할 수 있게 합니다. 하지만 예외를 응답으로 바꾸는 위치가 곧 예외를 항상 거기서만 로그해야 한다는 뜻은 아닙니다. 이 프로젝트에서는 예외 변환 책임과 대표 예외 로그 책임을 구분합니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 예외를 HTTP/API 응답으로 만드는 책임과
|
|
- 예외를 운영 로그로 남기는 책임을
|
|
- 같은 지점에 둘 수도 있지만, 개념적으로는 구분한다
|
|
|
|
### 3.2 대표 예외 로그는 한 번만 남긴다
|
|
|
|
Spring은 예외를 전역 advice에서 일관되게 처리할 수 있게 해 주므로, 같은 예외가 controller, service, client, advice에서 모두 stack trace와 함께 반복 기록될 필요는 없습니다. 실무적으로도 한 실패에 대해 대표 ERROR 로그 한 번을 남기고, 나머지는 보조 맥락만 남기는 편이 검색·알림·분석 품질이 좋습니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 한 실패에 대해 대표 예외 로그 한 번을 원칙으로 한다
|
|
- 하위 계층은 필요하면 DEBUG 또는 WARN으로 맥락만 남긴다
|
|
- 같은 stack trace를 여러 레이어에서 반복 ERROR로 남기지 않는다
|
|
|
|
### 3.3 예외 로그는 “무슨 일이 왜 어디서 실패했는지”를 설명해야 한다
|
|
|
|
Spring Boot는 기본 로그에 level, thread, logger, correlation 정보를 담을 수 있고, structured logging도 지원합니다. 따라서 예외 로그도 단순 ex.getMessage()가 아니라, 요청/작업/외부 시스템/소요시간/에러 코드 같은 운영 키를 함께 남겨야 의미가 있습니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 예외 로그는 사건 설명을 먼저 쓴다
|
|
- 그 뒤에 운영 필드(key=value)를 붙인다
|
|
- 예외 객체(stack trace)는 마지막 인자로 넘긴다
|
|
|
|
권장 예:
|
|
|
|
```text
|
|
Failed external auth request. provider=keycloak actorId=u_001 requestPath=/api/v1/sessions errorCode=UPSTREAM_AUTH_SERVER_UNAVAILABLE
|
|
```
|
|
|
|
## 4. 대표 로그 위치 규칙
|
|
|
|
### 4.1 HTTP 요청 실패의 대표 로그는 공통 경계에서 남긴다
|
|
|
|
Spring MVC는 전역 @ControllerAdvice/@RestControllerAdvice에서 controller 예외를 공통 처리할 수 있습니다. 따라서 일반 HTTP 요청 실패의 대표 예외 로그는 보통 전역 예외 처리 경계 또는 요청 완료 공통 로깅 경계 중 한 곳에서 일관되게 남기는 것이 적절합니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 일반 요청 실패는 공통 advice 또는 공통 요청 로깅 경계에서 대표 로그를 남긴다
|
|
- controller 메서드마다 try-catch + log.error를 반복하지 않는다
|
|
- controller local @ExceptionHandler가 있어도 대표 예외 로그 위치는 프로젝트 단위로 일관되게 유지한다
|
|
|
|
### 4.2 외부 API 실패의 대표 로그는 “최종 실패가 확정된 경계”에서 남긴다
|
|
|
|
외부 연동은 client/adapter 계층에서 많은 중간 실패가 생길 수 있습니다. 이런 중간 실패를 모두 ERROR로 남기면 재시도 후 성공한 케이스도 장애처럼 보일 수 있습니다. 따라서 대표 로그는 최종적으로 호출 결과가 실패로 확정된 시점에 남기는 것이 좋습니다. 이 원칙은 앞서 정한 log level 기준과도 맞습니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 재시도 전 단일 실패는 기본적으로 DEBUG 또는 WARN
|
|
- 재시도 후 최종 실패가 되면 대표 ERROR
|
|
- fallback으로 정상 복구되면 WARN 또는 INFO로 남기고 ERROR로 과장하지 않는다
|
|
|
|
### 4.3 배치/스케줄/비동기 작업은 작업 경계에서 대표 로그를 남긴다
|
|
|
|
Spring의 예외 처리 문맥은 HTTP controller만을 위한 것이 아니므로, 스케줄/비동기/배치 작업에서는 작업 진입점이나 orchestration 경계에서 대표 예외 로그를 남겨야 합니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 스케줄 작업은 job/unit-of-work 경계에서 대표 로그를 남긴다
|
|
- 비동기 후속 작업도 작업 단위 식별자와 함께 실패를 기록한다
|
|
- 내부 helper 메서드들이 모두 각자 ERROR를 찍지 않는다
|
|
|
|
## 5. 로그 레벨 규칙
|
|
|
|
### 5.1 최종 실패 예외는 ERROR
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 요청/작업이 최종 실패로 끝났으면 ERROR
|
|
- 응답이 5xx이거나, 작업 결과가 실패로 종료되면 ERROR
|
|
- 복구되지 않은 예외는 ERROR
|
|
|
|
### 5.2 복구된 예외는 WARN 또는 DEBUG
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 재시도 후 성공
|
|
- fallback 후 성공
|
|
- 대체 경로로 정상 처리
|
|
|
|
이 경우 대표 로그는 WARN 또는 필요 시 INFO
|
|
|
|
stack trace가 꼭 필요하지 않으면 DEBUG/WARN 요약 로그만 남긴다
|
|
|
|
### 5.3 예상 가능한 클라이언트 오류는 무조건 ERROR로 남기지 않는다
|
|
|
|
Spring에서 validation 예외나 request parsing 예외도 전역 advice에서 처리할 수 있지만, 그것이 모두 서버 이상을 뜻하는 것은 아닙니다. OWASP도 보안상 가치 있는 실패는 남기라고 하지만, 민감정보 노출 없이 맥락 중심으로 남기라고 권고합니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- validation 실패
|
|
- 잘못된 요청 파라미터
|
|
- business rule rejection
|
|
- 권한 없음
|
|
|
|
같은 예상 가능한 4xx는 기본적으로 INFO 또는 WARN
|
|
|
|
대량 이상 징후가 아니면 ERROR로 과장하지 않는다
|
|
|
|
## 6. 메시지 구성 규칙
|
|
|
|
### 6.1 예외 로그 제목은 예외 메시지가 아니라 사건 설명이다
|
|
|
|
프로젝트 규칙:
|
|
|
|
- log.error(ex.getMessage(), ex)를 기본 금지
|
|
- 로그 제목은 애플리케이션이 통제하는 사건 설명으로 쓴다
|
|
- 예외 메시지는 보조 정보일 뿐, 로그 제목의 전부가 아니다
|
|
|
|
권장:
|
|
|
|
```java
|
|
log.error("Failed to create session. actorId={} requestPath={} errorCode={}",
|
|
actorId, requestPath, errorCode, ex);
|
|
```
|
|
|
|
### 6.2 대표 예외 로그의 권장 필드
|
|
|
|
권장 필드:
|
|
|
|
- traceId 또는 correlation ID 연결 가능 정보
|
|
- requestPath
|
|
- method
|
|
- actorId
|
|
- operation
|
|
- resourceId
|
|
- externalSystem
|
|
- errorCode
|
|
- status
|
|
- durationMs
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 모든 필드를 다 강제하지는 않는다
|
|
- 해당 실패를 운영에서 추적하는 데 필요한 최소 필드를 남긴다
|
|
- 같은 종류의 예외 로그는 같은 필드 이름을 유지한다
|
|
|
|
### 6.3 stack trace만으로 맥락을 대체하지 않는다
|
|
|
|
Spring Boot는 structured logging에서 stack trace 출력 방식도 조정할 수 있지만, stack trace는 어디까지나 원인 분석용입니다. 운영자가 “무슨 요청/작업이 왜 실패했는지”를 빠르게 이해하려면 메시지 맥락이 필요합니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- stack trace가 있으니 메시지를 대충 쓰지 않는다
|
|
- 메시지는 사건 설명과 운영 키를 담고
|
|
- stack trace는 원인 분석을 보조한다
|
|
|
|
## 7. stack trace 규칙
|
|
|
|
### 7.1 대표 ERROR 로그에는 기본적으로 stack trace를 포함한다
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 최종 실패를 나타내는 대표 ERROR 로그는 기본적으로 예외 객체를 함께 남긴다
|
|
- stack trace 없는 ERROR 로그는 원인 분석에 불리하므로 예외적 경우에만 허용한다
|
|
|
|
### 7.2 WARN/INFO에서는 stack trace를 신중하게 남긴다
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 재시도 후 성공, fallback 후 성공 같은 경우에는 stack trace 없이 요약 로그를 우선한다
|
|
- 같은 원인의 경고가 고빈도로 반복될 수 있으면 stack trace를 매번 남기지 않는다
|
|
- 필요하면 최초 1회만 stack trace, 이후는 요약만 남기는 전략을 검토한다
|
|
|
|
### 7.3 너무 큰 stack trace는 구조화 포맷과 수집 비용을 고려한다
|
|
|
|
Spring Boot는 structured logging에서 stack trace 포함과 길이, 출력 방식을 조정할 수 있습니다. 큰 예외가 자주 발생하는 시스템에서는 수집 비용과 검색성을 고려해야 합니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 대량 반복 예외의 stack trace 출력 정책은 운영 비용을 고려해 조정한다
|
|
- 하지만 비용을 이유로 대표 실패의 원인 정보가 완전히 사라지게 만들지는 않는다
|
|
|
|
## 8. 민감정보 규칙
|
|
|
|
### 8.1 예외 로그도 PII/sensitive 규칙을 그대로 따른다
|
|
|
|
OWASP는 세션 식별값, 토큰, 비밀번호, 민감 PII, 키/비밀값 등은 직접 로그에 남기지 말라고 권고합니다. 예외 로그도 예외가 아닙니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 예외 로그 제목에 민감정보 원문 금지
|
|
- 예외 메시지에 민감정보가 포함될 수 있으면 그대로 재사용 금지
|
|
- request/response body 전문을 예외 맥락으로 붙이지 않는다
|
|
- 외부 시스템 에러 본문도 원문 그대로 남기지 않는다
|
|
|
|
### 8.2 stack trace에도 비밀값이 섞일 수 있음을 전제한다
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 예외 생성 메시지에 비밀값을 넣지 않는 것이 우선
|
|
- 예외에 포함된 URL, 헤더, payload, connection string, token 등을 주의한다
|
|
- 비밀값이 exception message에 들어가도록 코드를 짜지 않는다
|
|
|
|
## 9. 번역(translation) 규칙
|
|
|
|
### 9.1 내부 예외와 외부 응답 메시지를 분리한다
|
|
|
|
Spring은 @ExceptionHandler와 ResponseEntityExceptionHandler로 응답 변환을 지원합니다. 이 프로젝트는 예외 로그와 API 응답 메시지도 분리합니다. 즉, 로그에는 운영에 필요한 안전한 맥락을 남기고, 응답은 ErrorCode와 외부 메시지 규약으로 보냅니다.
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 로그 메시지 ≠ API 응답 메시지
|
|
- ex.getMessage()를 API 응답에도, 로그 제목에도 그대로 재사용하지 않는다
|
|
- advice는 응답 변환을, 대표 로그는 운영 맥락 기록을 담당한다
|
|
|
|
### 9.2 예외 번역 계층이 있다면 원인 체인을 잃지 않는다
|
|
|
|
프로젝트 규칙:
|
|
|
|
- external/client 예외 → integration 예외 → application 예외로 번역할 수 있다
|
|
- 이 과정에서 root cause를 완전히 잃지 않는다
|
|
- 대표 로그는 번역된 비즈니스 의미와 원인 예외를 함께 남길 수 있어야 한다
|
|
|
|
## 10. 위치별 세부 규칙
|
|
|
|
### 10.1 controller
|
|
|
|
프로젝트 규칙:
|
|
|
|
- controller에서 try-catch + log.error를 기본 금지
|
|
- 공통 advice가 있는 구조에서는 controller는 예외를 그대로 위로 전파한다
|
|
- endpoint-local 특별 정책이 있어도 대표 예외 로그는 한 번만 남긴다
|
|
|
|
### 10.2 application service
|
|
|
|
프로젝트 규칙:
|
|
|
|
- application은 예외를 business/application 의미로 번역할 수 있다
|
|
- 하지만 같은 예외를 무조건 ERROR로 남기지는 않는다
|
|
- 최종 실패 책임이 상위 경계에 있으면 여기서는 DEBUG/WARN 맥락만 남길 수 있다
|
|
|
|
### 10.3 external client / integration adapter
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 외부 호출 1회 실패는 기본적으로 요약 로그
|
|
- 재시도/fallback/최종 실패 여부에 따라 상위에서 대표 로그를 결정한다
|
|
- 외부 payload/headers/token 원문은 남기지 않는다
|
|
|
|
### 10.4 advice / global exception handler
|
|
|
|
프로젝트 규칙:
|
|
|
|
- 요청 실패의 대표 로그 위치를 advice로 정했다면 거기서 일관되게 남긴다
|
|
- validation/4xx/5xx에 따라 레벨을 다르게 적용할 수 있다
|
|
- advice가 응답 생성만 하고 로그는 요청 공통 경계에서 남기는 구조도 허용하되, 프로젝트 전체로 하나를 택한다
|
|
|
|
## 11. 금지 규칙
|
|
|
|
다음은 기본 금지다.
|
|
|
|
- 같은 예외를 여러 레이어에서 모두 ERROR로 기록
|
|
- log.error(ex.getMessage(), ex) 남발
|
|
- request/response body 전문을 예외 로그에 포함
|
|
- 토큰, 세션 ID, 비밀번호, 키, PII를 예외 로그에 원문으로 기록
|
|
- validation/예상 가능한 4xx를 무조건 ERROR 처리
|
|
- controller마다 try-catch + log.error + ResponseEntity 반복
|
|
- stack trace 없이 맥락도 없는 ERROR 한 줄만 남김
|
|
|
|
## 12. 체크리스트
|
|
|
|
다음 질문에 “예”로 답할 수 있어야 한다.
|
|
|
|
- 이 실패에 대해 대표 예외 로그가 한 번만 남는가?
|
|
- 대표 로그는 사건 설명과 운영 키를 담는가?
|
|
- 최종 실패만 ERROR로 남기고 있는가?
|
|
- 복구된 예외를 과도하게 ERROR로 찍지 않는가?
|
|
- 예외 로그에도 민감정보 마스킹 규칙이 그대로 적용되는가?
|
|
- 로그 메시지와 API 응답 메시지를 분리하고 있는가?
|
|
- stack trace가 필요한 곳에는 남고, 불필요한 중복은 줄였는가?
|