Quick Comparison
List<String> roles = List.of("ADMIN", "EDITOR", "VIEWER");
Set<Integer> levels = Set.of(1, 2, 3);| 필요 | 먼저 고를 생성 방식 | 바로 확인할 경계 |
|---|---|---|
| 고정된 값 목록 선언 | List.of, Set.of, Map.of | null과 변경 연산을 허용하지 않음 |
| 원본을 계속 바꾸되 읽기만 노출 | Collections.unmodifiableList | 원본 변경이 view에 그대로 보임 |
| 원본과 끊어 낸 읽기 전용 값 | List.copyOf | 원소 객체까지 deep copy하지는 않음 |
| 이후 수정할 사본 | new ArrayList<>(source) | caller와 소유권을 분리해야 함 |
갈리는 기준
어떤 컬렉션 생성 방식을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 수정 불가능한 작은 컬렉션 선언 | List.of, Set.of, Map.of |
| 기존 컬렉션을 읽기 전용으로 노출 | Collections.unmodifiableList(...) |
| 이후 수정이 필요한 컬렉션 | new ArrayList<>(...) 등 가변 컬렉션 |
| 원소까지 불변이어야 함 | 컬렉션 불변성과 원소 불변성을 따로 설계 |
수정 가능 vs 수정 불가능 컬렉션
Java 컬렉션을 배울 때는 보통 ArrayList 같은 수정 가능한 컬렉션부터 익히지만, 실무에서는 "이 컬렉션이 바뀌어도 되는가"가 더 중요한 질문이 되는 경우가 많습니다. 코드 의도를 분명히 드러내고, 여러 곳에서 공유될 때 예상치 못한 변경을 줄이려면 처음부터 불변 컬렉션을 선택하는 편이 좋습니다.
List.of, Set.of, Map.of 특성
JDK 9부터 추가된 편의 생성 메서드로, 수정 불가능한 컬렉션을 간결하게 만들 수 있습니다. null 원소를 허용하지 않으며, add·remove·set 같은 변경 연산 호출 시 UnsupportedOperationException이 발생합니다.
List<String> roles = List.of("ADMIN", "EDITOR");
roles.add("VIEWER"); // UnsupportedOperationException!
roles.set(0, "USER"); // UnsupportedOperationException!
// 이후 수정이 필요하다면 수정 가능한 복사본으로
List<String> mutable = new ArrayList<>(roles);
mutable.add("VIEWER"); // OKCollections.unmodifiableList vs List.of
Collections.unmodifiableList(original)은 기존 리스트의 읽기 전용 뷰를 만듭니다. 뷰를 통한 수정은 막히지만 원본 리스트를 수정하면 뷰에도 반영됩니다. List.of는 처음부터 완전히 독립된 불변 리스트를 만들므로 원본 참조가 없습니다.
List<String> mutable = new ArrayList<>(List.of("a", "b", "c"));
List<String> view = Collections.unmodifiableList(mutable);
// view.add("d"); // UnsupportedOperationException — 뷰 수정 불가
mutable.add("d"); // ✅ 원본 수정
System.out.println(view); // [a, b, c, d] — 뷰에도 반영됨!
// List.of는 독립 복사본 — 원본 변경에 영향 없음
List<String> snapshot = List.of("a", "b", "c");List.of vs unmodifiableList vs copyOf
새 고정 데이터를 바로 만들면 List.of, 기존 컬렉션의 읽기 전용 뷰를 노출하면 unmodifiableList, 원본과 분리된 스냅샷이 필요하면 List.copyOf가 더 직접적입니다.
List<String> source = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(source);
List<String> snapshot = List.copyOf(source);
source.add("c");
System.out.println(view); // [a, b, c]
System.out.println(snapshot); // [a, b]List.copyOf는 source와 분리된 수정 불가 snapshot이 필요할 때 씁니다. source가 이미 적합한 수정 불가 collection이면 같은 instance를 돌려줄 수도 있지만, 호출자가 그 identity에 의존하면 안 됩니다. Set.of와 Map.of는 null과 duplicate를 허용하지 않고, List.of도 null을 허용하지 않습니다.
컬렉션 불변성과 원소 불변성의 차이
"컬렉션이 수정 불가능하다"와 "그 안의 원소까지 완전히 불변이다"는 다른 문제입니다. 원소 객체가 가변이면 컬렉션이 수정 불가능해도 내부 상태 변화는 여전히 일어날 수 있습니다.
List<User> users = List.of(new User("Kim"), new User("Lee"));
// users.add(...) — 불가 (UnsupportedOperationException)
users.get(0).setName("Park"); // 원소 자체는 변경 가능!선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 초기 데이터가 고정, 변경 없음 | List.of(...) / Set.of(...) |
| 이후 추가·제거 필요 | new ArrayList<>(...) |
| 수정 불가 맵 | Map.of(k1, v1, k2, v2) |
| 불변 컬렉션에서 수정 가능 복사본 | new ArrayList<>(immutableList) |
| 기존 리스트를 읽기 전용으로만 노출 | Collections.unmodifiableList(list) |
| 원본 변경에 영향 없는 완전 독립 스냅샷 | List.of(...) 또는 List.copyOf(list) |
collection을 수정할 수 없다는 뜻은 element까지 불변이라는 뜻이 아닙니다. mutable element를 외부에 넘기면 element 내부 상태는 계속 바뀔 수 있으므로, 필요한 경우 element도 immutable type 또는 defensive copy로 설계합니다.
주의할 점
List.of로 만든 컬렉션에 변경 연산을 호출하면 런타임 예외가 발생합니다.
// ❌ List.of 결과를 수정 가능한 것처럼 사용
List<String> list = List.of("a", "b", "c");
list.add("d"); // UnsupportedOperationException
list.remove("a"); // UnsupportedOperationException
// ✅ 이후 수정이 필요하면 처음부터 수정 가능한 컬렉션 사용
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.add("d"); // OKSet.of(...)와 Map.of(...)는 중복 key나 중복 원소를 허용하지 않습니다.
// ❌ 중복 원소
Set<String> roles = Set.of("ADMIN", "ADMIN"); // IllegalArgumentException
// ❌ 중복 key
Map<String, Integer> scores = Map.of("kim", 1, "kim", 2); // IllegalArgumentException참고 링크
2 sources