1. 소개
쉽게 말해, mutable 컬렉션은 생성 후에도 추가, 삭제, 수정이 가능한 컬렉션입니다. immutable(불변) 컬렉션은 생성 후 변경할 수 없는 컬렉션입니다. 굳어 버린 콘크리트처럼 볼 수도, 만질 수도 있지만 새 모양을 빚을 수는 없습니다.
mutable 컬렉션은 연필과 공책 같습니다: 쓰고, 지우고, 새 메모를 추가할 수 있죠. immutable 컬렉션은 라미네이팅한 페이지와 같습니다: 이제 누구도 추가하거나 지울 수 없습니다.
변경 가능한(mutable) 컬렉션 예시
Java의 표준 컬렉션은 대부분 기본적으로 변경 가능합니다. 예를 들면 다음과 같은 클래스들입니다:
- ArrayList
- LinkedList
- HashSet
- TreeSet
- HashMap
- LinkedHashMap
- 그 외 다수
예: ArrayList
import java.util.*;
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");
names.set(1, "Charlie"); // Bob을 Charlie로 교체
names.remove("Alice"); // Alice를 삭제
System.out.println(names); // [Charlie]
여기서는 컬렉션에 대해 무엇이든 할 수 있습니다: 요소를 추가하고, 삭제하고, 자리를 바꿀 수 있죠. 예를 들어 파일을 읽거나 사용자 입력을 처리하는 과정에서 컬렉션을 동적으로 구성할 때 유용합니다.
2. 변경 불가능한(immutable) 컬렉션 예시
이 친구들은 Java 9에서 도입되었고 이미 접해 보셨겠지만, 아직 익숙해질 필요가 있습니다:
- List.of(...)
- Set.of(...)
- Map.of(...)
- List.copyOf(collection)
- Set.copyOf(collection)
- Map.copyOf(map)
예: List.of
List<String> planets = List.of("Mercury", "Venus", "Earth", "Mars");
System.out.println(planets); // [Mercury, Venus, Earth, Mars]
planets.add("Jupiter"); // UnsupportedOperationException이 발생합니다!
컬렉션을 변경하려는 시도는 런타임 예외로 이어집니다.
예: Collections.unmodifiableList
List<String> modifiable = new ArrayList<>(List.of("a", "b"));
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
unmodifiable.add("c"); // UnsupportedOperationException!
하지만 여기에는 함정이 있습니다: 원본 컬렉션을 변경하면 래퍼도 함께 변합니다!
modifiable.add("c");
System.out.println(unmodifiable); // [a, b, c] — 요소가 추가되었습니다!
3. mutable vs immutable의 주요 차이
| 속성 | Mutable(변경 가능) | Immutable(변경 불가능) |
|---|---|---|
| 요소를 추가할 수 있나요? | 예 | 아니요 |
| 요소를 삭제할 수 있나요? | 예 | 아니요 |
| 요소를 수정할 수 있나요? | 예 (예: set) | 아니요 |
| 스레드 안전성 | 아니요(기본값) | 예(상태가 없어 바꿀 것이 없음) |
| null을 추가할 수 있나요? | 예(보통 가능) | 아니요(Java 9+ 팩토리 메서드에서는 불가) |
| 구현 | ArrayList, HashSet 등 | List.of, Set.of, Map.of, copyOf |
4. 왜 불변 컬렉션이 필요한가?
바로 이런 의문이 듭니다: 변경 가능한 컬렉션이 그렇게 유연한데, 왜 불변 컬렉션이 필요할까요? 이유는 많으며, 모두 코드의 안전성, 가독성, 예측 가능성과 관련됩니다.
안전성과 오류 예방
컬렉션을 외부(예: 메서드나 클래스의 반환값)로 내보낼 때는, 누군가가 실수로 그 내용을 바꾸지 못한다는 확신이 필요합니다. 특히 컬렉션이 초기화 이후 바뀌면 안 되는 "중요한" 데이터를 담고 있다면 더욱 그렇습니다.
예:
public class Team {
private final List<String> players;
public Team(List<String> players) {
// 누구도 멋대로 구성원을 바꾸지 못하도록 불변 복사본을 만듭니다
this.players = List.copyOf(players);
}
public List<String> getPlayers() {
return players;
}
}
이제 이 목록을 받은 어떤 코드도 자기 "친구"를 몰래 추가할 수 없습니다.
스레드 안전성
변경 가능한 컬렉션은 여러 스레드가 동시에 접근할 때 안전하지 않습니다. 반면 불변 컬렉션은 스레드 간에 자유롭게 전달해도 — 아무도 그것을 망가뜨릴 수 없습니다.
디버깅 단순화
컬렉션이 변하지 않으면, 그 안에 무엇이 있는지 항상 확실합니다. 코드의 다른 곳에서 누군가가 그것을 "몰래" 바꿨을까 걱정할 필요가 없습니다.
다른 컬렉션에서 키나 값으로 사용
불변 객체는 Map의 키나 Set의 요소로 쓰기에 이상적입니다. 객체가 추가된 후에 변경될 수 있다면, 그 객체에 대한 접근을 잃을 위험이 있습니다(hashCode와 equals 참고).
5. 변경 가능한 컬렉션을 언제 쓰면 좋은가?
변경 가능한 컬렉션은 다음과 같은 경우에 좋습니다:
- 컬렉션을 반복문이나 여러 소스로부터 단계적으로 구축할 때.
- 자주 변경이 필요할 때: 추가, 삭제, 정렬 등.
- 컬렉션이 내부 용도이며, "외부"에서 망가뜨릴 수 없을 때.
예: 리스트 구성
List<String> shoppingList = new ArrayList<>();
shoppingList.add("우유");
shoppingList.add("빵");
shoppingList.add("사과");
// 구성한 뒤에는 불변 버전으로 만들 수 있습니다
List<String> finalList = List.copyOf(shoppingList);
6. 불변 컬렉션을 언제 쓰면 좋은가?
- 상수 데이터를 보관할 때(예: 요일 목록).
- 애플리케이션 레이어 간 컬렉션을 전달할 때(예: DAO에서 서비스로).
- 메서드에서 컬렉션을 반환할 때, 변경으로부터 보호하려는 경우.
- 스레드 안전이 중요한 멀티스레드 시나리오에서.
예: 상수 데이터
public static final List<String> WEEKDAYS = List.of(
"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"
);
예: 외부로 전달
public List<String> getReadOnlyNames() {
return List.copyOf(names); // 누구도 이 리스트를 변경할 수 없습니다
}
7. 특징과 주의사항
불변 ≠ 스레드 안전
불변 컬렉션은 변경으로부터는 보호되지만, 멀티스레드 환경에서의 다른 유형의 문제(예: 컬렉션의 요소가 변경 가능한 객체인 경우)까지 자동으로 보호해 주는 것은 아닙니다.
List<List<String>> listOfLists = List.of(new ArrayList<>());
listOfLists.get(0).add("Oops!"); // 내부 리스트는 변경 가능합니다!
래퍼 vs 복사본
앞서 논의했듯이, Collections.unmodifiableList는 래퍼이므로 원본 컬렉션을 변경하면 래퍼도 함께 변합니다. 반면 List.copyOf는 진정한 독립 복사본을 만듭니다.
List<String> base = new ArrayList<>(List.of("a", "b"));
List<String> wrap = Collections.unmodifiableList(base);
List<String> copy = List.copyOf(base);
base.add("c");
System.out.println(wrap); // [a, b, c] — 변경되었습니다!
System.out.println(copy); // [a, b] — 그대로입니다!
NullPointerException
팩토리 메서드(List.of, Set.of, Map.of)는 null 추가를 허용하지 않습니다:
List<String> bad = List.of("a", null); // 생성 시점에 이미 NullPointerException이 발생합니다!
8. 접근 방식 비교: 장단점
변경 가능한 컬렉션
변경 가능한 컬렉션의 가장 큰 강점은 유연성입니다. 실행 중에도 요소를 추가·삭제하고, 프로그램 로직의 전개에 따라 컬렉션을 재구성할 수 있습니다. 임시 구조를 빠르게 만들거나 무언가를 동적으로 바꿔야 할 때 특히 편리합니다.
하지만 편의성에는 대가가 따릅니다. 변경 가능한 컬렉션에는 항상 우발적 개입의 위험이 있습니다. 코드 어딘가에서 누군가가 데이터를 실수로 바꾸면, 추적하기 어려운 버그로 이어질 수 있습니다. 멀티스레드 프로그램에서는 더 나쁩니다 — 병렬 변경은 경쟁 상태와 예기치 않은 실패를 쉽게 유발합니다. 이러한 데이터의 라이프사이클을 관리하는 것도 더 어렵습니다. 누가 언제 컬렉션을 바꿀 수 있는지 계속 의식해야 합니다.
불변 컬렉션
불변 컬렉션과 함께라면 더 마음이 놓입니다. 아무도 "등 뒤에서" 내용을 바꾸지 않는다는 걸 확신할 수 있죠. 코드는 더 안전해지고, 디버깅과 테스트가 쉬워지며, 추가적인 락 없이도 스레드 간 전달이 수월합니다.
단점은 때때로 성능을 양보해야 한다는 점입니다. 새 요소를 추가하려면 새로운 컬렉션 사본을 만들어야 해서 메모리와 시간이 더 듭니다. 또한 큰 구조를 처음부터 불변 형태로 바로 모으는 일은 불편할 수 있습니다. 보통은 임시로 변경 가능한 컬렉션에 모은 뒤, 모든 준비가 끝나면 "동결"합니다.
9. 흔한 실수
오류 № 1: 변경 가능한 컬렉션을 외부로 그대로 반환함. 메서드에서 일반 ArrayList를 반환하면, 외부 코드가 마음대로 요소를 추가하거나 삭제할 수 있습니다. 이는 추적하기 매우 어려운 버그로 이어질 수 있습니다.
오류 № 2: 복사본 대신 래퍼를 사용함. Collections.unmodifiableList를 사용하지만 원본 컬렉션이 다른 곳에서 변경된다면, 그 "불변"은 착각에 불과합니다.
오류 № 3: immutable 컬렉션 내부에 mutable 객체가 있음. 컬렉션 자체가 불변이어도, 그 요소는 변경 가능할 수 있습니다. 이는 상태가 예상치 못하게 바뀌는 문제를 야기할 수 있습니다.
오류 № 4: 팩토리 메서드로 만든 컬렉션에 null을 추가하려 함. 예전 컬렉션과 달리, 새로운 팩토리 메서드는 null을 허용하지 않으므로 NullPointerException이 발생합니다.
오류 № 5: 특정 구현을 기대함. List.of나 Set.of로 생성한 컬렉션은 구현 타입을 보장하지 않습니다(반드시 ArrayList나 HashSet일 필요가 없습니다). 이에 의존하지 마세요.
GO TO FULL VERSION