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
@@ -0,0 +1,127 @@
# collections / immutability 기준
## 목적
컬렉션은 기본적으로 **immutable-first**로 다룬다.
변경이 꼭 필요한 로컬 조립 단계에서만 mutable 컬렉션을 허용하고, 경계를 넘길 때는 수정 불가 snapshot 또는 명시적 불변 구조로 바꾼다.
## 공식 의미
- JDK의 `List.of`, `Set.of`, `Map.of`, `copyOf`, `toUnmodifiable*`는 수정 불가 컬렉션을 만든다.
- `Collections.unmodifiableXXX`는 원본 컬렉션을 감싼 view일 뿐이다. 원본이 바뀌면 view도 바뀐다.
- 컬렉션이 수정 불가여도 원소가 mutable이면 내용이 바뀐 것처럼 보일 수 있다.
- `List.of` / `List.copyOf` 계열은 null을 허용하지 않는다.
- `Set.of``Map.of` 계열은 null을 허용하지 않으며, 중복 원소/중복 키를 허용하지 않는다.
- `Set.of`, `Map.of`, `Map.ofEntries`, `toUnmodifiableSet`, `toUnmodifiableMap`는 iteration order가 JVM 실행마다 달라질 수 있다.
## 기본 규칙
### 1. 기본은 immutable-first
다음 경우에는 mutable보다 immutable/unmodifiable 결과를 기본값으로 사용한다.
- 상수 컬렉션
- 설정/정책/권한 목록
- 외부에 반환하는 결과
- 공유되는 데이터
- 생성 후 더 이상 바뀌지 않아야 하는 상태
### 2. mutable 컬렉션은 로컬 조립 단계에서만 허용
다음 경우에는 mutable 컬렉션을 허용한다.
- 여러 source를 모아 결과를 만드는 임시 버퍼
- 반복적으로 add/remove가 필요한 내부 알고리즘
- aggregate/entity 내부에서 실제 상태 변경이 본질인 경우
단, 경계를 넘기기 전에 immutable/unmodifiable로 바꾼다.
### 3. 경계를 넘길 때는 defensive copy를 우선
다음 상황에서는 원본 컬렉션을 그대로 넘기지 않는다.
- 생성자에 받아서 필드로 저장할 때
- getter/응답 DTO/결과 객체로 반환할 때
- 다른 레이어로 넘길 때
기본 선택:
- snapshot이 목적 -> `List.copyOf`, `Set.copyOf`, `Map.copyOf`
- 작은 상수 컬렉션 -> `List.of`, `Set.of`, `Map.of`
- stream 수집 결과를 수정 불가로 고정 -> `Collectors.toUnmodifiableList/Set/Map`
### 4. `Collections.unmodifiableXXX`는 “view”가 필요할 때만
이 API는 기본 선택이 아니다.
허용되는 경우:
- 원본과 동기화되는 read-only view가 정말 필요할 때
- legacy API와의 호환 때문에 wrapper view가 필요한 경우
기본 금지 이유:
- 원본이 바뀌면 view도 바뀐다
- defensive copy나 true immutable 의도와 다르다
### 5. `copyOf`를 snapshot 기본값으로 사용
이미 가지고 있는 mutable collection을 안전하게 보관/반환해야 하면 `copyOf`를 우선 검토한다.
예:
- 생성자에서 받은 list를 필드에 저장
- service 결과를 외부에 반환
- mapper 결과를 response/domain에 전달
### 6. 컬렉션 null 금지, empty 우선
컬렉션 필드/반환값/파라미터는 가능하면 null을 금지한다.
기본값:
- 없음 -> empty list / set / map
- null collection 금지
### 7. 원소의 immutability를 따로 본다
컬렉션만 수정 불가여도 원소가 mutable이면 완전한 불변이 아니다.
기본 원칙:
- 공유되는 컬렉션은 가능하면 immutable element를 담는다
- mutable element를 담는 경우 “shallow immutable only”라는 점을 의식한다
- 외부에서 element mutation이 가능한 구조를 장기 공유 상태로 두지 않는다
### 8. 결과 컬렉션의 의미를 명시적으로 선택
- 순서가 중요하면 `List`
- 중복 제거가 목적이면 `Set`
- key lookup이 목적이면 `Map`
불변이 목적이라면:
- `List.copyOf`
- `Set.copyOf`
- `Map.copyOf`
- `Collectors.toUnmodifiable*`
를 우선 검토한다.
### 9. iteration order를 가정하지 않는다
`Set.of`, `Map.of`, `Map.ofEntries`, `toUnmodifiableSet`, `toUnmodifiableMap`는 iteration order가 랜덤화될 수 있다.
순서가 중요하면 `List`나 순서를 보장하는 별도 컬렉션 타입을 명시적으로 사용한다.
### 10. `Collectors.toList()` 결과를 mutable이라고 가정하지 않는다
수집 결과의 mutability가 중요하면 명시적으로 선택한다.
- 수정 불가 결과 필요 -> `stream.toList()` 또는 `Collectors.toUnmodifiableList()`
- 구체 mutable 타입 필요 -> `Collectors.toCollection(ArrayList::new)`
### 11. getter는 내부 mutable collection을 노출하지 않는다
다음 패턴을 금지한다.
- 내부 `ArrayList` 참조를 그대로 반환
- 생성자에서 받은 collection 참조를 그대로 필드에 저장
- 외부에서 수정 가능한 map/set/list를 그대로 보관
### 12. domain/application에서 컬렉션은 가능한 한 non-null + stable
도메인/유스케이스 내부에서는:
- null collection 금지
- mutable shared state 최소화
- 변경 가능성이 없다면 불변 구조로 고정
## 프로젝트 기준 요약
- 기본은 immutable-first
- mutable은 로컬 조립 단계에서만 허용
- 경계에서는 `copyOf`/`of`/`toUnmodifiable*`를 기본값으로 사용
- `Collections.unmodifiableXXX`는 view가 필요할 때만 제한적으로 사용
- null collection 대신 empty collection
- 원소가 mutable이면 컬렉션만 막아도 완전한 불변이 아님
- iteration order가 중요한 곳에서 `Set.of`/`Map.of` 계열 순서를 가정하지 않음
+154
View File
@@ -0,0 +1,154 @@
# duplication 기준
## 목적
중복은 무조건 제거하지 않는다.
반복 수정 비용이 큰 **진짜 중복**은 제거하고, 서로 다른 이유로 바뀔 가능성이 있는 **우연히 비슷한 코드**는 성급히 합치지 않는다.
## 공식/실무 기준 요약
- 중복 코드는 수정/리팩터링 시 같은 변경을 여러 곳에 반복하게 만들고, 누락 위험을 높인다.
- 정적 분석 도구는 일정 크기 이상의 구조적 중복을 duplication으로 탐지한다.
- 하지만 DRY를 너무 빨리 적용하면 premature abstraction이 생겨 이후 변경을 더 어렵게 만들 수 있다.
- 특히 테스트 코드는 production code보다 DRY보다 가독성을 더 우선할 수 있다.
## 기본 규칙
### 1. 기본 원칙: “같아 보인다”와 “같은 이유로 바뀐다”를 구분
다음 둘을 구분한다.
- **진짜 중복**
- 같은 의미
- 같은 정책
- 같은 이유로 바뀜
- 한 군데 고치면 나머지도 같이 바뀌어야 함
- **우연한 유사성**
- 지금은 비슷해 보여도
- 맥락/책임/변화 이유가 다름
- 나중에 독립적으로 진화할 가능성이 큼
기본적으로 **같은 이유로 바뀌는 경우만 추출**한다.
### 2. Rule of Three를 기본값으로 사용
추상화는 보통 아래 순서를 따른다.
- 1회: 그냥 작성
- 2회: 비슷한 패턴을 인식하되 아직 참음
- 3회 이상: 변화 이유가 같다면 추출을 적극 검토
단, 보안/예외 번역/외부 API 호출처럼 실수 비용이 큰 중복은 2회부터도 추출 가능하다.
### 3. 반복 수정 비용이 크면 추출
다음 조건이 크면 중복 제거를 우선 검토한다.
- 같은 정책 변경을 여러 파일에 반복해야 함
- 누락 시 장애/보안/데이터 불일치 위험이 큼
- 테스트도 같이 여러 군데 바뀌어야 함
- 팀원이 쉽게 한쪽만 수정할 수 있음
### 4. 추상화 비용이 더 크면 중복 허용
다음 경우는 중복을 허용한다.
- 서로 다른 레이어 책임을 억지로 합쳐야 하는 경우
- 공통화하면 이름이 모호해지는 경우
- 분기 옵션이 계속 늘어나 helper가 더 복잡해지는 경우
- 미래 변화 방향이 아직 불확실한 경우
- 테스트 가독성이 helper 때문에 더 나빠지는 경우
### 5. 레이어를 넘는 중복 제거는 특히 신중
중복 제거를 위해 레이어 경계를 깨지 않는다.
금지 예:
- presentation과 infrastructure의 비슷한 코드라는 이유로 공통 유틸로 합치기
- domain 규칙과 controller 검증을 한 helper로 합치기
- 외부 API 포맷과 내부 도메인 규칙을 같은 mapper로 합치기
### 6. 복붙보다 작은 추출부터
중복 제거는 아래 순서로 작게 시작한다.
1. local variable 추출
2. private method 추출
3. mapper/helper 추출
4. 같은 클래스 계층이면 pull up / template method 검토
5. 그래도 명확할 때만 더 큰 추상화
처음부터 범용 util/service/common으로 키우지 않는다.
### 7. helper는 “짧아진 코드”보다 “명확해진 이름”이 있을 때만
다음 중 하나가 아니면 helper 추출을 보류한다.
- 반복되는 의미를 정확히 설명하는 이름이 있음
- 공통 정책/계약을 하나로 모아야 함
- 테스트/검증/예외 처리 중복을 안정적으로 줄임
“줄 수 있으니까 줄인다”는 이유만으로 추출하지 않는다.
### 8. 테스트는 DRY보다 가독성 우선 가능
테스트는 production code와 기준이 다를 수 있다.
기본:
- 테스트는 사람이 바로 읽어 이해할 수 있어야 함
- 과한 helper, loop, setup 공유로 의미가 숨겨지면 중복을 허용
- DAMP를 우선하고, 진짜 반복 보일러플레이트만 줄인다
### 9. 생성/변환/정책 중복은 추출 우선
다음은 실수 비용이 높아 추출을 우선 검토한다.
- 에러 응답 조립
- 외부 API request/response 변환
- 시간 생성/포맷 정책
- 권한/역할 판별 정책
- 공통 validation 규칙
- persistence <-> domain 매핑 규칙
- exception translation 규칙
### 10. 우연한 한두 줄 중복은 허용
다음은 무리해서 추출하지 않는다.
- 간단한 guard clause
- 명확한 builder/setter 호출 몇 줄
- 테스트의 준비/검증 코드
- 각 레이어에서 맥락상 당연한 짧은 변환 코드
### 11. 중복 제거는 behavior-preserving으로 작게
중복 제거는 리팩터링이다.
행동 보존을 전제로 작은 단계로 진행한다.
기본:
- 테스트가 있으면 먼저 보호
- 한 번에 큰 범용 추상화로 가지 않음
- 단계적으로 추출 후 검증
### 12. common/util 모듈은 중복 제거 수단으로 남용 금지
중복을 본다고 바로 `common`, `util`, `helper`로 보내지 않는다.
먼저 묻는다:
- 이 중복의 소유 레이어는 어디인가?
- 정말 여러 모듈이 같은 이유로 바뀌는가?
- 경계를 안 깨고도 추출 가능한가?
### 13. 도구 경고는 “신호”이지 자동 수정 명령은 아님
Sonar/PMD가 duplication을 잡았다고 무조건 추출하지 않는다.
다음 둘을 함께 본다.
- 구조적 중복 크기
- 변화 이유의 동일성
### 14. 문서화 기준
중복을 의도적으로 남겼다면 이유를 짧게 남길 수 있다.
특히 아래 경우:
- 테스트 가독성
- 레이어 분리 유지
- 조기 추상화 방지
- 외부 계약의 독립 진화 가능성
## 프로젝트 기준 요약
- 진짜 중복만 제거
- 같은 이유로 바뀌는 경우만 추출
- Rule of Three 기본
- 레이어 경계를 깨는 공통화 금지
- 작은 추출부터 시작
- 테스트는 DAMP 우선 가능
- 도구 경고는 신호일 뿐 자동 추출 근거가 아님
+140
View File
@@ -0,0 +1,140 @@
# enum / constants 기준
## 목적
고정된 의미 집합은 문자열/정수 상수 남발 대신 enum으로 표현한다.
상수는 단순히 `static final`인 값이 아니라, 실제로 변경 불가능하고 의미가 분명한 값으로 관리한다.
## 공식 의미
- enum은 미리 정의된 상수 집합을 표현하는 특별한 클래스다.
- 각 enum constant는 해당 enum 타입의 고유 인스턴스다.
- enum은 필드, 메서드, 생성자를 가질 수 있다.
- `ordinal()`은 대부분의 개발자가 사용할 용도가 아니며 `EnumSet`, `EnumMap` 같은 저수준 구조를 위한 값이다.
- `name()`은 선언된 정확한 이름을 반환한다.
- `toString()`은 더 사람이 읽기 좋은 문자열이 필요할 때만 재정의한다.
- enum key/set에는 `EnumMap`, `EnumSet` 같은 특화 구현을 우선 검토한다.
## 기본 규칙
### 1. 고정된 값 집합이면 enum 우선
다음과 같은 경우는 enum을 우선 검토한다.
- 상태
- provider 종류
- 역할 종류
- 모드
- 정책 선택지
- 결과 코드의 제한된 집합
금지 예:
- `"LOCAL"`, `"GOOGLE"`, `"KAKAO"` 같은 문자열 분기
- `1`, `2`, `3` 같은 매직 넘버 상태값 분기
### 2. 값 집합이 열려 있으면 enum을 쓰지 않는다
외부 시스템이 임의 값을 추가할 수 있거나, 런타임에 확장 가능한 값이면 enum보다 다른 모델을 검토한다.
예:
- 자유 입력 태그
- 외부 설정으로 계속 늘어나는 코드값
- tenant별 커스텀 상태
### 3. `ordinal()` 의존 금지
다음 용도로 `ordinal()`을 사용하지 않는다.
- DB 저장값
- 외부 API/JSON 값
- 비즈니스 분기
- 배열 인덱스 계약
- stable code/wire number
안정적인 숫자/문자 코드가 필요하면 enum 필드로 명시한다.
### 4. `name()`은 내부 안정 식별자, 사용자 표시값은 별도 필드/메서드
- 내부 고정 식별이 필요하면 `name()`
- 사용자에게 보이는 문자열은 별도 필드나 메서드
- API/DB/wire 값도 별도 필드로 분리
즉:
- internal name
- display label
- external code
를 섞지 않는다.
### 5. `toString()`은 신중하게
공식 문서상 `toString()`은 필요하면 더 programmer-friendly 한 문자열로 재정의할 수 있다.
하지만 프로젝트에서는 다음 기준을 따른다.
- 로깅/디버깅/개발자용 표현만 바꿀 때 제한적으로 허용
- 외부 계약, JSON, DB 저장값을 `toString()`에 의존하지 않는다
- 외부 계약값은 명시적 getter/field를 사용한다
### 6. enum 비교는 enum끼리 직접 비교
동일 enum 타입 비교는 enum 값 자체로 직접 비교한다.
비즈니스 의미 비교를 위해 문자열로 변환해서 비교하지 않는다.
금지 예:
- `status.name().equals("ACTIVE")`
- `provider.toString().equals(input)`
### 7. enum set/map은 `EnumSet` / `EnumMap` 우선 검토
enum을 key/set element로 사용할 때는 다음을 우선 검토한다.
- 집합 -> `EnumSet`
- 맵 -> `EnumMap`
비트 플래그용 `int`나 일반 `HashSet`/`HashMap`을 습관적으로 쓰지 않는다.
### 8. switch/분기는 enum 의미 중심으로 작성
enum 분기는 문자열/정수 코드가 아니라 enum 값 자체를 기준으로 작성한다.
또한:
- default로 조용히 삼키지 않는다
- 새로운 enum 값 추가 시 분기 누락이 드러나도록 작성한다
### 9. 상수는 진짜 immutable일 때만 상수로 취급
상수는 다음을 만족해야 한다.
- `static final`
- 값 자체가 immutable
- 내부 원소도 변경으로 의미가 흔들리지 않음
즉:
- mutable collection
- mutable object reference
- 변경 가능한 배열
은 상수처럼 다루지 않는다.
### 10. 관련 상수는 “상수 클래스”보다 소유 타입 근처에 둔다
다음 순서를 우선한다.
1. enum으로 승격 가능한 값이면 enum
2. 특정 클래스 책임이면 해당 클래스 내부 상수
3. 여러 모듈에 걸친 진짜 전역 상수만 별도 위치
의미 없는 `Constants`, `CommonConstants`, `AppConstants` 집합소는 기본 금지다.
### 11. null enum 기본 금지
enum 값은 가능한 한 non-null로 다룬다.
부재 표현이 필요하면:
- Optional 반환
- nullable boundary 입력
- 명시적 `UNKNOWN` / `UNSPECIFIED` 도입 여부를 신중히 검토
단, `UNKNOWN`이 실제 비즈니스 상태가 아닐 경우 null 회피용으로 남용하지 않는다.
### 12. persistence / serialization 계약은 별도 기준을 따른다
enum을 DB나 JSON에 노출할 때는 language 차원 기본 동작에 기대지 않고,
- 어떤 값을 저장/전송할지
- 호환성을 어떻게 유지할지
를 명시적 기준으로 정의한다.
## 프로젝트 기준 요약
- 고정된 의미 집합은 enum 우선
- 열린 값 집합은 enum 지양
- `ordinal()` 의존 금지
- internal name / display label / external code 분리
- enum set/map은 `EnumSet` / `EnumMap` 우선
- mutable 값은 상수처럼 다루지 않음
- 잡다한 constants class 기본 금지
+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 규칙을 예외 타입 설계에 반영
- 구체 예외 사용
+153
View File
@@ -0,0 +1,153 @@
# Javadoc 작성 / 수정 / 삭제 기준
## 목적
Javadoc은 코드 설명서가 아니라 **API 계약과 오해 가능성이 있는 의미를 문서화하는 도구**로 사용한다.
“코드를 읽으면 바로 아는 내용”을 반복하지 않는다.
## 공식 의미
- Javadoc doc comment는 선언 바로 앞에 있어야 인식된다.
- 한 doc comment는 설명(description)과 block tags로 구성된다.
- 첫 요약 문장은 summary/index에 재사용되므로 특히 중요하다.
- override/implement 메서드는 자체 문서가 없으면 상위 문서를 상속할 수 있다.
- visible한 class/member에는 Javadoc이 있는 것이 기본이지만, 정말 단순하고 obvious한 경우나 override는 예외가 될 수 있다.
## 기본 규칙
### 1. Javadoc은 계약을 문서화할 때만 쓴다
다음 중 하나가 있으면 Javadoc을 작성한다.
- public/protected API
- 외부 모듈이 호출하는 계약
- null 허용 여부가 중요함
- 예외 조건/전제조건/후조건이 중요함
- thread-safety, side effect, state change가 중요함
- 경계 조건이나 corner case를 안 쓰면 오해할 수 있음
- 이름만으로는 의미가 충분하지 않음
### 2. “코드를 읽으면 아는 내용”은 쓰지 않는다
다음은 기본 금지한다.
- getter/setter를 그대로 풀어쓴 설명
- 필드명/파라미터명을 문장으로 반복
- 구현을 한 줄씩 설명하는 Javadoc
- 리팩터링 후 쉽게 stale 해질 정보
금지 예:
- `Returns the user name.` 만 있는 자명한 `getUserName()`
- `Sets the value.` 같은 설명
### 3. 첫 줄은 summary로 쓴다
Javadoc 첫 줄은 짧고 독립적으로 읽히는 요약이어야 한다.
기본:
- 요약 1줄
- 필요한 경우 상세 설명
- 그 후 block tags
### 4. 구현 세부보다 호출 계약을 우선
Javadoc은 아래를 더 우선해서 쓴다.
- 무엇을 보장하는가
- 어떤 입력이 허용되는가
- 어떤 경우 실패하는가
- 호출자가 믿어도 되는 동작은 무엇인가
- 반환값의 의미는 무엇인가
다음은 기본 지양한다.
- 내부 알고리즘 설명
- 현재 구현 방식
- 성능 미세 최적화 세부
### 5. `@param`, `@return`, `@throws`는 의미가 있을 때만 정확히 쓴다
태그를 채우기 위한 태그를 금지한다.
기본:
- `@param`: 파라미터 의미/제약/허용 범위/nullable 여부
- `@return`: 반환값 의미, empty/optional/null/ordering/ownership
- `@throws`: 실제 계약상 중요한 예외 조건
### 6. 예외 문서는 “언제 왜 던지는가”를 쓴다
단순히 예외 타입만 나열하지 않는다.
좋은 방향:
- 어떤 입력/상태에서
- 어떤 이유로
- 호출자가 무엇을 기대해야 하는지
### 7. self-explanatory 멤버는 Javadoc 생략 가능
정말 단순하고 obvious한 멤버는 Javadoc을 생략할 수 있다.
예:
- 의미가 완전히 자명한 getter
- record component 중 이름만으로 충분한 경우
단, typical reader가 모를 수 있는 의미가 있으면 생략하지 않는다.
### 8. override는 상속 문서를 우선 활용
override/implement 메서드에서 상위 문서가 충분하면 Javadoc을 반복하지 않는다.
다만 아래 경우에는 다시 쓴다.
- 하위 타입에서 계약이 추가됨
- 예외/부작용/동시성 보장이 달라짐
- 더 좁은 의미가 생김
### 9. Javadoc과 코드가 어긋나면 Javadoc이 잘못된 것
코드가 바뀌면 Javadoc도 같이 수정한다.
맞출 수 없으면 지운다.
기본 규칙:
- stale Javadoc 금지
- 애매한 Javadoc보다 없는 편이 낫다
- 거짓 문서 금지
### 10. Javadoc은 boundary/API 중심으로 우선 배치
프로젝트에서는 아래 우선순위로 작성한다.
1. public/protected API
2. 외부 호출되는 application/presentation boundary
3. 의미가 어려운 domain type / value object
4. 예외/정책/동시성 규약이 중요한 infrastructure API
5. package-level overview가 필요한 package
### 11. implementation comment로 계약을 설명하지 않는다
class/member의 전체 목적이나 호출 계약을 설명하는 내용이면 `//` 주석 대신 Javadoc으로 쓴다.
### 12. package/class level Javadoc은 구조 설명에 사용
package/class 수준에서는:
- 목적
- 포함 내용
- 관계
- 사용 시 주의점
- 외부 문서 링크
를 설명할 수 있다.
긴 설명은 외부 architecture/spec 문서로 분리하고 링크한다.
### 13. 한 줄 Javadoc은 정말 짧을 때만
한 줄로 끝나는 Javadoc은:
- 매우 짧고
- block tag가 없고
- 요약만으로 충분할 때만 사용한다
### 14. 포맷보다 의미를 우선하되 형식은 일관되게
기본 형식:
- summary
- 빈 줄
- 상세 설명 (필요 시)
- block tags (`@param`, `@return`, `@throws`, `@deprecated` 순)
### 15. Javadoc은 examples보다 계약 우선
examples/tutorial 성격의 설명은 docs/examples나 외부 문서가 더 적합할 수 있다.
Javadoc은 먼저 API contract를 충실히 담는다.
## 프로젝트 기준 요약
- Javadoc은 계약/제약/의미를 문서화할 때만 작성
- 자명한 설명, 구현 반복 설명 금지
- 첫 줄 summary 필수
- `@param/@return/@throws`는 의미 있을 때만 정확히 작성
- self-explanatory 멤버와 override는 생략 가능
- stale Javadoc 금지
- 맞출 수 없으면 수정하거나 삭제
+134
View File
@@ -0,0 +1,134 @@
# null 처리 기준
## 목적
`null`은 “값이 없음”을 표현하는 기본 도구가 아니라, **명시적으로 허용된 경계에서만 제한적으로 다루는 값**으로 취급한다.
기본값은:
- non-null
- 명시적으로 nullable인 경우만 `null` 허용
- 내부 로직에서는 가능한 빨리 null을 제거하고 더 명확한 표현으로 바꾼다
## 공식 의미
- Java는 null-safety를 타입 시스템으로 직접 표현하지 못한다.
- 따라서 null 허용 여부는 API 계약, annotation, 검증 코드로 명시해야 한다.
- `Objects.requireNonNull(...)`은 메서드/생성자 파라미터 검증 용도로 우선 사용한다.
- Spring 계열에서는 package/type 수준의 nullability 기본값과 `@Nullable` 명시를 통해 API 계약을 드러내는 방식을 권장한다.
## 기본 규칙
### 1. 기본값은 non-null
명시적으로 nullable이라고 선언되지 않은 값은 non-null로 간주한다.
프로젝트 기본 태도:
- 파라미터: 기본 non-null
- 반환값: 기본 non-null
- 필드: 기본 non-null
- nullable은 예외적 상황만 명시
### 2. nullable 여부는 계약으로 드러낸다
다음 중 하나로 null 허용 여부를 명시한다.
- nullability annotation
- Optional 반환
- 빈 컬렉션/빈 문자열이 아닌 명시적 상태 타입
- API/문서/Javadoc 계약
“읽어보면 알 수 있음” 상태를 금지한다.
### 3. 경계에서만 null을 받는다
다음 경계에서는 null이 들어올 수 있다고 가정하고 방어한다.
- 외부 요청 입력
- DB/JPA 결과
- 외부 API 응답
- 설정값/환경변수
- legacy library API
하지만 경계를 지나 내부 로직으로 들어오면:
- 즉시 검증하거나
- Optional/value object/default object/명시적 상태로 변환한다
### 4. 내부 로직에서 null 전파 금지
application/domain/business 로직에서는 nullable 값을 계속 흘려보내지 않는다.
금지:
- 여러 계층을 거치며 nullable String/DTO field를 계속 전달
- null 여부를 business 의미처럼 암묵적으로 해석
- “일단 null로 두고 나중에 확인” 패턴
### 5. 파라미터 검증은 가능한 한 즉시
public/protected/package boundary 또는 생성자에서는 필요한 경우 초기에 검증한다.
기본 방식:
- `Objects.requireNonNull(...)`
- 명시적 argument validation
- request binding / validation annotation
- value object 생성 시 검증
### 6. Optional과 null을 섞지 않는다
- `Optional` 자체를 null로 두지 않는다
- Optional을 반환하면 null 반환 금지
- nullable field를 억지로 Optional field로 바꾸지 않는다
### 7. 컬렉션/배열/스트림은 null 대신 빈 값 우선
다건 결과는 가능한 한 null 대신 다음을 사용한다.
- empty list
- empty set
- empty map
- empty stream
- empty array
`null` 컬렉션은 기본 금지다.
### 8. DTO/직렬화 경계는 nullable을 명시적으로 관리
Request/Response DTO에서는 nullable field가 필요할 수 있다.
이 경우:
- nullable 여부를 명시하고
- 내부 로직 진입 전에 변환/검증한다.
DTO의 nullable 상태를 domain/application 전체로 전파하지 않는다.
### 9. Entity와 Domain은 구분해서 본다
- JPA entity는 DB nullable 제약을 반영할 수 있다
- domain model은 비즈니스 invariant 기준으로 더 엄격해야 한다
즉:
- DB가 nullable이어도 domain은 non-null일 수 있다
- persistence mapper에서 변환/검증 책임을 진다
### 10. null-check는 가능한 한 한 곳에서 끝낸다
같은 값에 대해 여러 레이어에서 반복 null-check 하지 않는다.
기본 방향:
- 경계에서 검증
- 값 객체로 승격
- 이후는 non-null 가정
### 11. null을 business state로 쓰지 않는다
다음 패턴을 금지한다.
- `status == null`이면 임시 상태
- `provider == null`이면 로컬 로그인
- `deletedAt == null` 같은 인프라 관례를 business 의미로 직접 사용
business state는 enum, value object, explicit flag, 상태 타입으로 표현한다.
### 12. annotation 기반 nullability는 일관되게 사용
Spring/JSpecify 스타일을 도입하면:
- package/type default를 먼저 정하고
- 예외만 `@Nullable`로 표시한다
- 무의미하게 nullable/non-null annotation을 섞지 않는다
## 프로젝트 기준 요약
- 기본은 non-null
- nullable은 계약으로 명시
- 경계에서 null을 받고 내부에서 제거
- public/constructor 경계는 `requireNonNull` 등으로 빠르게 검증
- 컬렉션/스트림은 null 대신 empty
- Optional과 null 혼용 금지
- DTO/JPA nullable을 domain/application에 그대로 전파 금지
- null을 business meaning으로 사용 금지
+161
View File
@@ -0,0 +1,161 @@
# Optional 사용 기준
## 목적
`Optional`은 값의 부재를 **메서드 반환 타입에서 명시적으로 표현**할 때 사용한다.
`null`을 감추기 위한 만능 래퍼로 쓰지 않는다.
## 공식 의미
- `Optional`은 값이 있을 수도 없을 수도 있는 non-null 컨테이너다.
- `Optional`은 **주로 메서드 반환 타입**으로 사용하도록 의도되었다.
- `Optional` 변수 자체는 `null`이면 안 된다.
- `Optional`은 value-based class이므로 동기화나 `==` 같은 identity-sensitive 사용을 하지 않는다.
## 기본 규칙
### 1. 기본값은 반환 타입으로만 사용
다음 경우에만 `Optional<T>`를 우선 검토한다.
- 조회 결과가 없을 수 있는 단건 반환
- 파싱/탐색/검색 결과가 없을 수 있는 반환
- 호출자가 "없음"을 명시적으로 처리해야 하는 API
예:
- `findById`
- `findUserByEmail`
- `parseXxx`
- `resolveCurrentTenant`
### 2. 필드 타입으로는 기본 금지
`Optional`을 엔티티 필드, DTO 필드, 설정 필드, 상태 저장 필드로 두지 않는다.
이유:
- 공식적으로 `Optional`은 주로 반환 타입 용도다.
- 상태 저장 필드에서는 nullable field, value object, 별도 타입, 또는 명시적 상태 모델이 더 적합하다.
- nullable `Optional`, empty `Optional`, present `Optional`의 혼합은 의미를 더 복잡하게 만든다.
### 3. 파라미터 타입으로는 기본 금지
메서드/생성자 파라미터에 `Optional<T>`를 직접 받지 않는다.
대신 아래를 우선 검토한다.
- 메서드 오버로드
- 별도 명시적 파라미터 타입
- nullable + 명확한 nullability annotation
- 요청 객체/커맨드 객체로 캡슐화
예외:
- 외부 라이브러리 API와의 일관성을 맞춰야 하는 경우
- 함수형 조합 API에서 정말 표현력이 좋아지는 경우
### 4. 절대 금지: `Optional`을 null로 사용
`Optional<T>` 타입 변수/반환값/파라미터에 `null`을 쓰지 않는다.
금지 예:
```java
return null; // Optional<T> 반환 메서드
Optional<User> user = null;
someMethod(null); // Optional<User> 파라미터
```
없음은 반드시 `Optional.empty()`로 표현한다.
### 5. `get()`은 기본 금지
`get()`은 값이 없으면 예외를 던진다.
공식 문서도 preferred alternative로 `orElseThrow()`를 제시한다.
기본 규칙:
- `get()`은 테스트 코드나 명백한 증명 직후의 제한된 상황이 아니면 사용하지 않는다.
- 업무 코드에서는 `map`, `flatMap`, `filter`, `ifPresent`, `orElse`, `orElseGet`, `orElseThrow`를 사용한다.
### 6. `isPresent()` + `get()` 조합은 기본 금지
다음 패턴은 기본 금지한다.
```java
if (opt.isPresent()) {
return opt.get();
}
```
대신 아래를 우선 검토한다.
- `map`
- `ifPresent`
- `orElse`
- `orElseGet`
- `orElseThrow`
### 7. 기본값 계산 비용이 있으면 `orElseGet`
단순 상수/기존 값이면 `orElse`를 검토한다.
계산/조회/생성 비용이 있거나 side-effect 가능성이 있으면 `orElseGet`을 사용한다.
기본 원칙:
- expensive default는 `orElseGet`
- 단순 literal/default object는 `orElse` 검토
### 8. nested Optional 금지
`Optional<Optional<T>>` 형태를 만들지 않는다.
- mapper가 다시 `Optional`을 반환하면 `map` 대신 `flatMap`을 사용한다.
- 계층 탐색은 `map(...).flatMap(...)` 또는 `flatMap(...)` 조합으로 평탄화한다.
### 9. `map`은 값 변환, `ifPresent`는 최종 부작용
- 변환/계산/연결은 `map`, `flatMap`, `filter`를 사용한다.
- 최종 경계 side-effect는 `ifPresent`, `ifPresentOrElse`를 사용한다.
반환값을 쓰지 않을 때 `map`을 사용하지 않는다.
### 10. 컬렉션/스트림은 Optional보다 빈 표현 우선
컬렉션/배열/스트림 결과는 `Optional<List<T>>`보다 빈 컬렉션/빈 스트림을 우선한다.
기본 규칙:
- 단건 부재 -> `Optional<T>`
- 다건 부재 -> empty collection / empty stream
### 11. Optional은 직렬화/DTO 경계 기본 금지
Request/Response DTO, JPA Entity, 설정 프로퍼티 객체에 `Optional` 필드를 두지 않는다.
이유:
- 경계 타입은 직렬화/바인딩/스키마 명확성이 우선이다.
- `Optional`은 표현 모델보다 API 결과 표현 쪽에 더 적합하다.
### 12. primitive optional은 정말 필요할 때만
`OptionalInt`, `OptionalLong`, `OptionalDouble`은 primitive 부재 표현이 정말 중요한 경우에만 사용한다.
일반 업무 도메인에서는 보통 `Optional<T>` 또는 명시적 값 객체가 더 읽기 쉽다.
## Spring / Repository 기준
- 단건 조회 결과 없음은 `Optional<T>`를 우선 검토한다.
- 컬렉션 반환은 `null` 대신 empty collection을 반환한다.
- repository package의 nullability는 `@NonNullApi`, `@NonNull`, `@Nullable` 같은 도구와 함께 설계한다.
## 프로젝트 기준 요약
- `Optional`은 기본적으로 반환 타입에만 사용한다.
- 필드/파라미터는 기본 금지한다.
- `Optional` 자체를 `null`로 두지 않는다.
- `get()``isPresent() + get()`은 기본 금지한다.
- 값 변환은 `map`, `flatMap`, `filter`를 사용한다.
- 최종 부작용은 `ifPresent`, `ifPresentOrElse`를 사용한다.
- 다건 결과는 `Optional<List<T>>` 대신 empty collection을 사용한다.
+97
View File
@@ -0,0 +1,97 @@
# Stream 사용 기준
## 목적
`Stream`은 컬렉션을 직접 조작하는 도구가 아니라, 데이터 소스에 대한 집계/변환/검색 파이프라인을 선언적으로 표현하는 도구로 사용한다.
## 사용할 때
다음 중 하나에 해당하면 `Stream`을 우선 검토한다.
- filter / map / flatMap / grouping / reduction처럼 **집계 파이프라인**이 중심일 때
- “무엇을 만들 것인가”가 분명하고, 루프보다 **의도가 더 잘 드러날 때**
- `anyMatch`, `findFirst`, `max`, `min`, `sum`, `groupingBy`, `partitioningBy` 같은 **집계 연산**이 핵심일 때
- 결과를 새로운 컬렉션이나 요약값으로 만드는 작업일 때
## 사용하지 않을 때
다음 중 하나에 해당하면 일반 `for` / `for-each`를 기본값으로 사용한다.
- 핵심이 **부작용(side-effect)** 인 경우
- 인덱스 기반 접근이 본질인 경우
- 중간 분기/예외 처리/조기 탈출이 많아져 파이프라인이 오히려 읽기 어려운 경우
- 디버깅이나 상태 변경이 중심인 경우
- 성능 민감 경로인데 스트림이 더 낫다는 측정 근거가 없는 경우
## 핵심 규칙
### 1. source를 수정하지 않는다
스트림 실행 중 source collection/data source를 수정하지 않는다.
- 스트림 내부 람다에서 source를 변경하지 않는다
- 외부에서 동시에 source를 수정하지 않는다
- source 변경이 필요하면 스트림 밖에서 먼저 끝내고, 새 스트림을 만든다
### 2. 람다는 non-interfering, stateless를 기본값으로 한다
`filter`, `map`, `sorted`, `reduce`, `collect` 등에 넘기는 람다는 기본적으로 상태를 들고 있지 않아야 하며, source나 외부 공유 상태를 변경하지 않아야 한다.
금지 예:
- 외부 `List``forEach`로 accumulate
- `AtomicInteger` 같은 외부 상태를 카운터처럼 밀어넣기
- 람다 안에서 source collection 변경
### 3. side-effect에 의존하지 않는다
`forEach` / `forEachOrdered`를 제외하면, 스트림 구현은 최적화를 위해 일부 연산이나 람다 호출을 생략할 수 있다.
따라서 비즈니스 로직은 side-effect가 아니라 **reduction / collection** 으로 표현한다.
### 4. 한 번 사용한 Stream은 재사용하지 않는다
terminal operation 이후 스트림은 소모된다.
같은 데이터를 다시 순회하려면 source에서 새 stream을 만든다.
### 5. stateful intermediate operation은 비용을 의식한다
`sorted`, `distinct`, 일부 `limit/skip` 조합은 비용이 크거나 버퍼링이 필요할 수 있다.
정렬/중복 제거는 정말 필요할 때만 넣는다.
### 6. 기본은 sequential stream이다
기본값은 `stream()`이다.
`parallel()` / `parallelStream()`은 아래를 모두 만족할 때만 검토한다.
- 병렬화 이득이 측정됨
- 공유 상태/side-effect 없음
- 순서 보장이 중요하지 않거나 비용을 감수 가능
- collector/연산이 병렬 친화적임
### 7. `forEach`는 결과 생성이 아니라 최종 경계 부작용에만 쓴다
`forEach`는 보통 아래와 같은 최종 경계에서만 허용한다.
- 로그 출력
- 이벤트 발행
- 외부 시스템 호출
- 이미 계산된 결과를 최종 전달
컬렉션 생성/집계는 `collect`, `reduce`, `toList` 등으로 표현한다.
### 8. `peek`는 디버깅용으로만 제한한다
`peek`는 비즈니스 로직, 상태 변경, 필수 검증 로직에 사용하지 않는다.
임시 디버깅 후 제거를 기본으로 한다.
### 9. 컬렉션 결과 규칙을 명확히 한다
- 수정 불가능한 결과가 목적이면 `toList()` 또는 `Collectors.toUnmodifiableList()`를 우선 검토한다
- 구체 컬렉션 타입이 필요하면 `Collectors.toCollection(...)`을 사용한다
- `Collectors.toList()` 결과를 mutable이라고 가정하지 않는다
### 10. 숫자 집계는 primitive stream을 우선 검토한다
합계/평균/최댓값/최솟값 중심이면 `mapToInt`, `mapToLong`, `mapToDouble`을 우선 검토해 boxing 비용과 표현 복잡도를 줄인다.
### 11. I/O 기반 stream은 닫는다
`Files.lines(...)` 같은 I/O 기반 stream은 `try-with-resources`로 닫는다.
## 프로젝트 기준 요약
- 집계/변환/검색 파이프라인이면 Stream
- 부작용/복잡한 제어 흐름이면 loop
- source 변경 금지
- 외부 mutable state 의존 금지
- `forEach`로 수집 금지
- `parallel()`은 측정 기반으로만 허용
- 결과 컬렉션의 mutability를 명시적으로 선택
+160
View File
@@ -0,0 +1,160 @@
# time 타입 / 포맷 기준
## 목적
시간은 문자열이나 숫자 조합이 아니라 **의미에 맞는 타입**으로 표현한다.
“시점”, “날짜”, “시간”, “벽시계 기준 날짜-시간”, “시간대 포함 날짜-시간”, “기간”을 서로 다른 개념으로 취급한다.
## 공식 의미
- `java.time`는 날짜/시간/instant/duration의 기본 API다.
- `java.time` 타입은 immutable, thread-safe 다.
- 시스템 경계를 넘는 저장/네트워크에는 ISO-8601 기반 `java.time` 타입을 우선 사용한다.
- `Instant`는 timestamp에 해당한다.
- `LocalDate`는 날짜만 표현한다.
- `LocalTime`은 시간만 표현한다.
- `LocalDateTime`은 날짜+시간이지만 offset/time-zone이 없다.
- `ZonedDateTime`은 time-zone까지 포함한 “full” date-time이다.
- `OffsetDateTime`은 UTC offset을 포함하며 네트워크 프로토콜/DB 접근에서 주로 쓰인다.
- 현재 시각은 `Clock`에서 얻을 수 있고, 이는 테스트를 쉽게 만든다.
- `now()` 계열은 시스템 clock과 기본 time-zone을 사용한다.
## 기본 규칙
### 1. 문자열 대신 타입으로 표현
시간/날짜를 내부 로직에서 `String`으로 들고 다니지 않는다.
금지:
- `"2026-04-12T10:15:30Z"`를 business field로 보관
- `"2026-04-12"`를 domain/application 로직에서 날짜 대신 사용
- `"09:00"`를 opening time 의미로 직접 비교
기본:
- 날짜 -> `LocalDate`
- 시각만 -> `LocalTime`
- 시점 -> `Instant`
- 날짜+시간(시간대 없음) -> `LocalDateTime`
- 시간대 포함 wall-clock -> `ZonedDateTime`
- offset 포함 경계 표현 -> `OffsetDateTime`
- 시간 간격 -> `Duration`
- 사람 기준 기간(일/월/년) -> `Period`
### 2. 시점(point in time)은 `Instant`를 기본값으로
로그 시각, 생성 시각, 만료 시각, 이벤트 발생 시각처럼 timeline 위 한 점이면 `Instant`를 우선한다.
예:
- `createdAt`
- `updatedAt`
- `expiresAt`
- `issuedAt`
### 3. 사람 기준 날짜/시간은 Local 타입 우선
시간대와 무관한 도메인 의미에는 Local 타입을 쓴다.
예:
- 생일 -> `LocalDate`
- 영업 시작 시각 -> `LocalTime`
- UI에서 입력한 예약 시각(아직 zone 미확정) -> `LocalDateTime`
### 4. `LocalDateTime`은 시점이 아니다
`LocalDateTime`은 offset/time-zone이 없으므로 절대적인 시점으로 저장/비교/교환할 때 기본값으로 쓰지 않는다.
금지 예:
- `createdAt``LocalDateTime`으로 저장
- 외부 시스템과 절대 시각을 `LocalDateTime`으로 주고받기
### 5. `ZonedDateTime`은 정말 시간대 계산이 필요할 때만
실제 지역 시간대 규칙(DST 포함)을 고려해야 하는 계산만 `ZonedDateTime`을 쓴다.
예:
- 특정 도시/지역 wall-clock 기준 예약
- 사용자 time-zone 기준 만료/알림 계산
기본값은 아니다. 시간대는 복잡도를 크게 높인다.
### 6. 경계/프로토콜/DB에서는 `Instant` 또는 `OffsetDateTime`을 우선 검토
시스템 경계를 넘는 시간 값은 ISO-8601 기반 `java.time` 타입을 사용한다.
기본 선택:
- “절대 시점” 저장/전송 -> `Instant`
- offset이 포함된 wire/db 표현 필요 -> `OffsetDateTime`
### 7. 현재 시각은 `Clock` 기반으로 다룰 수 있게 설계
테스트 가능성이 중요한 코드에서는 `Instant.now()` / `LocalDate.now()` / `LocalDateTime.now()`를 직접 박지 않는다.
기본 방향:
- 현재 시각을 얻는 위치를 경계/서비스로 모은다
- 필요 시 `Clock`을 주입한다
- 테스트에서는 고정 clock을 사용한다
### 8. 기본 시스템 time-zone 의존 최소화
기본 시스템 time-zone을 암묵적으로 쓰는 호출은 신중히 제한한다.
기본 원칙:
- zone이 중요하면 `ZoneId`를 명시한다
- `now()` 계열의 기본 zone 의존은 casual code에서만 허용
- business logic, persistence, cross-system contract에서는 zone을 명시적으로 다룬다
### 9. 포맷/파싱은 경계에서만
날짜/시간 포맷팅과 파싱은 주로 boundary에서 수행한다.
예:
- controller/request binding
- response serialization
- external API adapter
- logging formatter
domain/application 내부에서는 typed value를 유지한다.
### 10. 기본 포맷은 ISO-8601 우선
새로운 커스텀 날짜 포맷을 기본값으로 만들지 않는다.
시스템 간 교환은 ISO-8601을 우선한다.
### 11. `Duration`과 `Period`를 구분
- 기계적 시간 간격 -> `Duration`
- 사람 기준 달력 기간 -> `Period`
예:
- access token TTL -> `Duration`
- “30일 후”, “1개월 후” 같은 달력 의미 -> `Period`
### 12. legacy 시간 API는 새 코드에서 기본 금지
새 코드에서는 다음을 기본 금지한다.
- `java.util.Date`
- `java.util.Calendar`
- `java.sql.Timestamp`
- `TimeZone` 중심 설계
- `System.currentTimeMillis()`를 직접 흩뿌리는 패턴
기본은 `java.time` 사용이다.
### 13. 저장/조회/직렬화 기준을 명시
시간 값을 DB/JSON/API에 노출할 때는 다음을 명확히 한다.
- 어떤 타입을 저장하는가
- 어떤 zone/offset 가정을 하는가
- 어떤 포맷으로 직렬화하는가
- 정밀도(초/밀리초/나노초)를 어느 수준까지 쓸 것인가
### 14. 문자열 비교/부분 파싱으로 시간 로직 작성 금지
시간 판단을 문자열 조작으로 하지 않는다.
금지:
- `timestamp.startsWith(...)`
- `"09:00".compareTo(...)`
- substring으로 연/월/일 추출
## 프로젝트 기준 요약
- 시간은 타입으로 표현한다
- 시점은 `Instant`
- 날짜는 `LocalDate`
- 시각은 `LocalTime`
- `LocalDateTime`은 절대 시점 용도 금지
- 실제 time-zone 계산만 `ZonedDateTime`
- 경계/DB/네트워크는 `Instant` 또는 `OffsetDateTime`
- 현재 시각은 가능하면 `Clock` 기반
- 기본 시스템 time-zone 의존 최소화
- 포맷/파싱은 경계에서만
- 새 코드에서 legacy 시간 API 금지