1. 소개
전통적인 컬렉션: 유연성과 함정
new ArrayList<>()로 컬렉션을 만들면 요소를 자유롭게 추가·삭제·변경할 수 있는 구조를 얻게 됩니다. 프로그램을 작성하면서 데이터를 즉석에서 구성할 때는 편리하죠. 하지만 이 컬렉션을 다른 클래스나 메서드로 넘기는데, 거기서는 변경되면 안 된다면 어떨까요? 실수로 컬렉션을 외부로 노출해서 누군가가 바꿔 버리면요? 바로 이런 지점에서 문제가 시작됩니다.
전형적인 실수 예
import java.util.*;
public class Example {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.add("Charlie");
// 컬렉션을 '외부'로 전달
processNames(names);
// 리스트가 변하지 않았다고 기대...
System.out.println(names);
}
public static void processNames(List<String> list) {
// 그런데 누군가가 요소를 삭제해 버림!
list.remove("Bob");
}
}
출력:
[Alice, Charlie]
컬렉션이 본의 아니게 변경되었습니다. 대규모 프로젝트에서는 이런 “깜짝 변화”가 아주 불쾌하고 잡기 어려운 버그로 쉽게 이어집니다.
문제는 컬렉션을 사용하는 코드가 데이터의 예기치 않은 변경과 맞닥뜨릴 수 있다는 데 있습니다. 게다가 정보 손실 위험도 늘 존재합니다 — 누군가가 요소를 실수로 삭제했거나 덮어썼을 수 있죠. 동시에 여러 스레드가 컬렉션을 변경하면 ConcurrentModificationException은 물론이고, 더 교묘한 문제 — 일관성이 깨진 데이터 — 도 만나게 됩니다.
컬렉션 보호: 기존 접근
Java 9 이전에는 지난 단계에서 다뤘던 Collections.unmodifiableList(...) 같은 래퍼 메서드를 사용해야 했습니다. 이들은 “얼려진” 컬렉션을 반환하는 데 도움이 됩니다. 하지만 이 방법은 늘 편리한 것도 아니고 모든 문제를 해결하지도 않습니다(자세한 내용은 다음 강의에서 다룹니다).
2. 최신 해법: 팩토리 메서드 List.of, Set.of, Map.of
Java 9에서는 컬렉션 인터페이스에 새로운 정적 메서드 List.of, Set.of, Map.of가 추가되었습니다. 이 메서드들은 변경할 수 없는 컬렉션을 빠르고 편리하게 만들 수 있게 해 줍니다. 마치 컬렉션을 만들자마자 콘크리트로 굳혀 버린 것과 같습니다 — 누구도 요소를 추가·삭제·변경할 수 없습니다.
불변 컬렉션 생성 예
import java.util.*;
public class ImmutableDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
Set<Integer> numbers = Set.of(1, 2, 3);
Map<String, Integer> ages = Map.of("Alice", 30, "Bob", 25, "Charlie", 28);
System.out.println(names);
System.out.println(numbers);
System.out.println(ages);
}
}
출력:
[Alice, Bob, Charlie]
[1, 2, 3]
{Alice=30, Bob=25, Charlie=28}
어떻게 동작하나요?
- List.of(...) — 불변 리스트를 생성합니다.
- Set.of(...) — 불변 집합을 생성합니다.
- Map.of(...) — 불변 맵을 생성합니다(키-값 쌍을 10개까지 지원; 그 이상은 Map.ofEntries(...) 사용).
주의! 이 메서드들로 생성된 컬렉션은 변경이 허용되지 않습니다. 요소를 추가·삭제·교체하려는 모든 시도는 예외를 던집니다.
3. 사용 예와 주의할 점
예: 컬렉션을 변경하려는 시도
import java.util.*;
public class ImmutableFail {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob");
// names.add("Charlie"); // 런타임에서 오류 발생!
try {
names.add("Charlie");
} catch (UnsupportedOperationException ex) {
System.out.println("요소를 추가할 수 없습니다: " + ex.getClass().getSimpleName());
}
}
}
출력:
요소를 추가할 수 없습니다: UnsupportedOperationException
예: null 추가 시도
import java.util.*;
public class NullFail {
public static void main(String[] args) {
try {
List<String> badList = List.of("Alice", null, "Bob");
} catch (NullPointerException ex) {
System.out.println("null은 허용되지 않습니다: " + ex.getClass().getSimpleName());
}
}
}
출력:
null은 허용되지 않습니다: NullPointerException
예: Set.of에서 중복
import java.util.*;
public class DuplicatesFail {
public static void main(String[] args) {
try {
Set<String> badSet = Set.of("one", "two", "one");
} catch (IllegalArgumentException ex) {
System.out.println("중복은 허용되지 않습니다: " + ex.getClass().getSimpleName());
}
}
}
출력:
중복은 허용되지 않습니다: IllegalArgumentException
예: 많은 쌍을 가진 Map.of
import java.util.*;
public class MapOfLarge {
public static void main(String[] args) {
// Map.of는 최대 10개의 키-값 쌍을 지원합니다
Map<String, Integer> map = Map.of(
"one", 1, "two", 2, "three", 3, "four", 4, "five", 5,
"six", 6, "seven", 7, "eight", 8, "nine", 9, "ten", 10
);
System.out.println(map);
// 더 많은 경우에는 Map.ofEntries를 사용하세요
Map<String, Integer> bigMap = Map.ofEntries(
Map.entry("eleven", 11),
Map.entry("twelve", 12),
Map.entry("thirteen", 13)
// ...등등
);
System.out.println(bigMap);
}
}
4. 불변 컬렉션의 특징과 제약
변경 불가.
요소를 추가·삭제·변경하려는 모든 시도는 UnsupportedOperationException을 발생시킵니다. 보통 가능하던 메서드(add, remove, set)도 동작하지 않습니다.
null 사용 불가.
null을 리스트나 집합의 요소, 또는 맵의 키/값으로 추가하려 하면 NullPointerException이 발생합니다. 이는 안전을 위한 조치입니다: null 요소는 컬렉션에서 자주 오류의 원인이 됩니다.
구체 구현은 보장되지 않습니다.
List.of 등으로 생성된 컬렉션의 내부에 어떤 클래스가 쓰였는지는 알 수 없습니다. instanceof로 ArrayList인지 확인하거나 특정 타입으로 캐스팅하려 해서는 안 됩니다.
요소 순서.
— List.of는 요소 순서를 보장합니다(일반 리스트와 동일).
— Set.of는 순서를 보장하지 않습니다(실제로는 전달한 인수의 순서와 같을 수 있으나, 의존하지 마세요).
— Map.of는 쌍의 순서를 보장하지 않습니다.
성능.
팩토리 메서드로 생성된 컬렉션은 보통 변경 가능한 컬렉션에 대한 래퍼보다 빠르게 동작합니다. 불필요한 기능을 위해 메모리를 쓰지 않기 때문입니다.
5. 언제 그리고 왜 불변 컬렉션을 사용할까
상수 데이터 집합
프로그램 동작 중 변경되면 안 되는 리스트·집합·맵이 있다면 List.of, Set.of, Map.of를 사용하세요. 예:
private static final List<String> ROLES = List.of("USER", "ADMIN", "MODERATOR");
이제 아무도 이 리스트에 불필요한 역할을 추가할 수 없습니다.
메서드에서 컬렉션 반환
메서드에서 컬렉션을 반환할 때, 외부에서 변경되면 안 된다면:
public List<String> getDefaultNames() {
return List.of("Alice", "Bob", "Charlie");
}
호출자는 당신의 데이터를 망가뜨릴 수 없습니다.
애플리케이션 계층 간 전달
프로그램의 서로 다른 부분(예: 웹 애플리케이션의 Controller와 Service 계층) 사이에 컬렉션을 전달할 때, 아무도 몰래 변경하지 못하도록 불변 컬렉션을 사용하는 것이 좋습니다.
안전성과 스레드 안전성
불변 컬렉션은 정의상 읽기 측면에서 스레드 안전합니다. 누가 변경할 수 없다면, 여러 스레드에서 동기화 없이 안심하고 사용할 수 있습니다.
6. 일반 애플리케이션용 실전 예시
예를 들어, 우리 학습용 애플리케이션에는 지원되는 명령 목록이 있습니다:
public class Commands {
public static final List<String> SUPPORTED_COMMANDS = List.of(
"help", "exit", "list", "add", "remove"
);
}
이렇게 시도하면:
Commands.SUPPORTED_COMMANDS.add("hack_the_system");
예외가 발생하며 애플리케이션에 해를 끼칠 수 없습니다.
또는 오류 코드를 담은 맵이 있다면:
public class ErrorCodes {
public static final Map<Integer, String> CODES = Map.of(
404, "Not Found",
500, "Internal Server Error",
403, "Forbidden"
);
}
새 코드를 추가하려는 모든 시도는 예외를 던집니다.
7. 컬렉션 생성 접근 방식 비교
| 생성 방법 | 변경 가능? | null 가능? | 중복? | 스레드 안전성 | 예 |
|---|---|---|---|---|---|
|
예 | 예 | 예 | 아니요 | |
|
아니요 | 아니요 | 예 | 예* | |
|
아니요 | 아니요 | 아니요 | 예* | |
|
아니요 | 아니요 | 아니요 | 예* | |
|
아니요 | 원본에 따라 다름 | 예 | 아니요 | |
* — 불변성의 관점에서만 스레드 안전합니다: 컬렉션이 변경되지 않는다면 여러 스레드에서 안전하게 읽을 수 있습니다.
8. List.of, Set.of, Map.of 사용 시 자주 하는 실수
오류 1: 컬렉션을 변경하려는 시도.
아주 흔한 실수는 List.of, Set.of, Map.of로 생성한 컬렉션에 요소를 추가하거나 삭제하려는 것입니다. 예를 들어 names.add("Dmitry") 또는 ages.remove("Bob") 같은 코드입니다. 이는 항상 런타임에 UnsupportedOperationException을 발생시킵니다.
오류 2: null을 추가하려는 시도.
실수로 null을 어떤 메서드에 전달하더라도(예: List.of("Alice", null)) NullPointerException이 발생합니다. Java 9+의 불변 컬렉션은 null을 싫어합니다 — 사실 좋은 일입니다.
오류 3: Set.of 또는 Map.of에 중복 요소.
Set.of("a", "b", "a") 또는 Map.of("x", 1, "x", 2)는 IllegalArgumentException을 유발합니다. 집합과 맵은 정의상 중복을 포함할 수 없습니다.
오류 4: 특정 구현을 기대함.
이렇게 하면 안 됩니다:
List<String> list = List.of("a", "b");
if (list instanceof ArrayList) {
// ...
} // 이것은 항상 false입니다!
내부 구현은 숨겨져 있으므로, 구현 세부사항에 의존하지 마세요.
오류 5: 변경 메서드를 사용하려는 시도.
clear(), 리스트의 set(index, value) 같은 메서드도 예외를 던집니다. 팩토리 메서드로 생성한 컬렉션은 불변임을 기억하세요.
GO TO FULL VERSION