Files
project-auth-server/docs/standards/language/collections-immutability.md
T

5.3 KiB

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