init: 클린 기반 auth 서버 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:30:18 +09:00
parent 471db0203d
commit 8a1ac1e769
3642 changed files with 275893 additions and 1 deletions
+151
View File
@@ -0,0 +1,151 @@
# exceptions 기준
## 목적
예외는 정상 흐름 제어 도구가 아니라, **예외 상황을 명시적으로 전달하고 경계에서 번역하는 수단**으로 사용한다.
## 공식 의미
- Java 예외는 `Throwable` 계층에 속한다.
- `RuntimeException``Error`가 아닌 예외는 checked exception으로 취급된다.
- 예외는 cause와 suppressed exception을 함께 가질 수 있다.
- `try-with-resources`는 자원 정리를 보장하며, close 중 발생한 예외는 suppressed로 보존될 수 있다.
- Spring transaction은 기본적으로 `RuntimeException``Error`에서 rollback한다. checked exception은 기본 rollback 대상이 아니다.
## 기본 규칙
### 1. 예외는 정상 분기 대신 예외 상황에만 사용
다음 용도로 예외를 쓰지 않는다.
- 일반적인 조건 분기
- 결과 없음 표현
- 루프 탈출
- 검증 실패를 무조건 예외로만 처리하는 패턴
기본:
- 정상적인 “없음”은 Optional/empty collection/명시적 결과 타입
- 예외는 정말 비정상적이거나 계약 위반인 경우
### 2. catch는 번역 / 문맥 추가 / 복구일 때만
예외를 catch하는 목적은 아래 셋 중 하나여야 한다.
- 계층 번역
- 의미 있는 문맥 추가
- 명시적 복구
그 외 목적 없는 catch는 금지한다.
### 3. broad catch 기본 금지
다음 catch는 기본 금지한다.
- `catch (Exception e)`
- `catch (Throwable t)`
예외:
- 최상위 boundary에서 마지막 방어선으로 처리할 때
- framework integration 때문에 정말 필요한 경우
- 테스트 코드에서 의도가 분명한 경우
기본값은 더 구체적인 예외를 잡는 것이다.
### 4. 빈 catch 금지
아무 일도 하지 않는 catch block은 금지한다.
정말 무시해야 하는 경우만:
- 왜 안전하게 무시 가능한지 주석으로 설명
- 가능하면 metrics/logging/상태 기록 중 하나 수행
### 5. `printStackTrace()` 금지
업무 코드에서는 `printStackTrace()`를 사용하지 않는다.
기본:
- logging framework로 기록
- 또는 적절한 예외로 rethrow
- 또는 상위 계층으로 전파
### 6. cause를 버리지 않는다
예외를 번역할 때 원인 예외를 cause로 보존한다.
좋은 예:
- `throw new InfrastructureException(code, message, cause)`
나쁜 예:
- `throw new InfrastructureException(code, "failed")` 만 하고 원인을 버림
### 7. 메시지는 문맥을 더하고, 중복은 줄인다
예외 메시지는 “무엇을 하다가 왜 실패했는지”를 추가한다.
단, 하위 예외 메시지를 그대로 복붙해 중복하지 않는다.
예:
- 좋음: `Failed to sign JWT with Vault transit key`
- 나쁨: `IOException occurred` / `Error happened`
### 8. checked / unchecked 선택은 복구 가능성 기준
기본 방향:
- 호출자가 의미 있게 복구/대응할 수 있는 경우 -> checked exception 검토
- 프로그래밍 오류, 불변식 위반, 계약 위반, 복구 불가 -> unchecked exception 우선
프로젝트 기본은:
- domain/application/infrastructure 내부의 의미 예외는 대체로 unchecked
- 외부 API/IO 경계에서는 원래 checked 예외를 잡고 계층 예외로 번역 가능
### 9. 계층별 번역 책임을 지킨다
- domain: 도메인 의미만 표현
- application: business outcome / error code로 번역
- infrastructure: 기술 실패를 infrastructure exception으로 번역
- presentation/bootstrap: HTTP/security/framework boundary로 번역
예외를 아무 계층에서나 최종 사용자 메시지로 확정하지 않는다.
### 10. try-with-resources 우선
닫아야 하는 자원은 `try-with-resources`를 기본값으로 사용한다.
금지:
- `finally`에서 close 하다가 기존 예외를 덮어쓰는 패턴
- 자원 해제를 수동으로 반복 구현하는 패턴
### 11. `finally`에서 return/throw 금지
`finally`에서 return/throw 하면 try/catch의 결과를 덮어쓸 수 있다.
`finally`는 정리 작업만 수행하고 정상 종료해야 한다.
### 12. InterruptedException은 별도 처리
`InterruptedException` 가능성이 있는 코드를 `Exception`으로 뭉뚱그려 잡지 않는다.
기본 방향:
- 가능하면 별도로 catch
- 현재 스레드 인터럽트 상태 복원 검토 (`Thread.currentThread().interrupt()`)
- interruption 의미를 상위로 전달
### 13. transaction rollback 규칙을 예외 설계에 반영
Spring transaction 경계 안에서는 예외 타입이 rollback에 영향을 준다.
기본:
- unchecked -> 기본 rollback
- checked -> 기본 no rollback
- checked도 rollback해야 하면 `rollbackFor` 등으로 명시
따라서 “checked냐 unchecked냐”를 스타일이 아니라 transaction 의미까지 보고 결정한다.
### 14. 테스트에서 try-catch 남용 금지
예외 기대 테스트는 `assertThrows` 류를 우선 사용한다.
예외를 catch하고 `fail()`만 호출하는 패턴은 지양한다.
### 15. throws 선언은 실제로 던질 수 있는 checked 예외만
checked exception을 실제로 던지지 않는데 시그니처에 선언하지 않는다.
호출자에게 불필요한 처리 부담을 준다.
### 16. 구체 예외를 던진다
`Exception`, `RuntimeException` 같은 너무 넓은 기반 예외를 직접 던지지 않는다.
의미에 맞는 더 구체적인 예외를 사용한다.
## 프로젝트 기준 요약
- 예외는 정상 흐름 제어 수단이 아니다
- catch는 번역 / 문맥 추가 / 복구일 때만
- broad catch, empty catch, printStackTrace 금지
- cause 보존
- try-with-resources 우선
- `finally`에서 return/throw 금지
- `InterruptedException` 별도 처리
- Spring rollback 규칙을 예외 타입 설계에 반영
- 구체 예외 사용