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` 계열 순서를 가정하지 않음