init: 클린 기반 auth 서버 설계
This commit is contained in:
@@ -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` 계열 순서를 가정하지 않음
|
||||
@@ -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 우선 가능
|
||||
- 도구 경고는 신호일 뿐 자동 추출 근거가 아님
|
||||
@@ -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 기본 금지
|
||||
@@ -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 규칙을 예외 타입 설계에 반영
|
||||
- 구체 예외 사용
|
||||
@@ -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 금지
|
||||
- 맞출 수 없으면 수정하거나 삭제
|
||||
@@ -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으로 사용 금지
|
||||
@@ -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을 사용한다.
|
||||
@@ -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를 명시적으로 선택
|
||||
@@ -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 금지
|
||||
Reference in New Issue
Block a user