1. Unmodifiable wrappers: 컬렉션에 대한 래퍼
때때로 코드에는 누군가가 (실수로 또는 의도적으로) 변경할 수 있는 컬렉션이 이미 존재합니다. 예를 들어, 외부에 반환하고는 싶지만 누군가가 수정하길 원하지 않는 사용자 목록이 있다고 해봅시다:
List<String> users = new ArrayList<>();
users.add("Alice");
users.add("Bob");
이 목록을 메서드에서 반환했는데, 누군가가 users.add("Hacker"); — 를 호출해버리면 시스템에 승인되지 않은 사용자가 생깁니다! 어떻게 보호할 수 있을까요?
Collections의 래퍼
Java에는 이미 오래전부터 Collections 클래스에 특수 래퍼 메서드가 있습니다:
- Collections.unmodifiableList(list)
- Collections.unmodifiableSet(set)
- Collections.unmodifiableMap(map)
이 메서드들은 여러분의 컬렉션 위에 변경을 막는 래퍼를 반환합니다. 래퍼를 통해 요소를 추가, 삭제 또는 변경하려고 하면 UnsupportedOperationException이 발생합니다.
예:
import java.util.*;
public class Demo {
public static void main(String[] args) {
List<String> modifiable = new ArrayList<>();
modifiable.add("Alice");
modifiable.add("Bob");
// 변경 불가 래퍼 생성
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
System.out.println(unmodifiable); // [Alice, Bob]
// 래퍼를 통해 요소를 추가해 보자
try {
unmodifiable.add("Charlie"); // 쾅! UnsupportedOperationException
} catch (UnsupportedOperationException e) {
System.out.println("컬렉션을 변경할 수 없습니다: " + e);
}
}
}
중요!
- 래퍼는 원본 컬렉션을 불변으로 만들지 않습니다. 누군가가 원본 컬렉션에 대한 참조를 가지고 있다면 여전히 변경할 수 있습니다.
- 원본 컬렉션의 모든 변경 사항은 래퍼를 통해서도 보입니다.
modifiable.add("Charlie");
System.out.println(unmodifiable); // [Alice, Bob, Charlie]
즉, 코드 어딘가에서 원본 리스트에 요소를 추가/삭제하면 래퍼에도 반영되어 보입니다. 이것은 “동결”이 아니라, 래퍼를 통해서만 변경을 금지하는 것입니다.
다른 컬렉션을 위한 래퍼
같은 방식으로 Set, Map, 심지어 더 특이한 구조에도 래퍼를 씌울 수 있습니다:
Set<Integer> numbers = new HashSet<>(Set.of(1, 2, 3));
Set<Integer> unmodSet = Collections.unmodifiableSet(numbers);
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 30);
ages.put("Bob", 25);
Map<String, Integer> unmodMap = Collections.unmodifiableMap(ages);
팩토리 메서드(List.of 등)와의 비교
- List.of(...)는 새로운 불변 컬렉션을 생성하며, 애초에 요소를 추가할 수 없습니다.
- Collections.unmodifiableList(list) — 기존 컬렉션 위의 래퍼입니다. 원본 리스트가 바뀌면 래퍼도 바뀝니다.
표: 접근 방식 비교
|
|
|
|---|---|---|
| 추가 가능? | 아니요 | 아니요(래퍼 경유) |
| 원본에 추가 가능? | 해당 없음 | 예 |
| 변경 사항이 보이나? | 아니요 | 예 |
| null 저장 가능? | 아니요 (NPE) | 예 (원본 컬렉션이 허용하면) |
| 구현 | 자체 구현 | 컬렉션 위의 래퍼 |
2. CopyOnWrite 컬렉션
멀티스레드 프로그램에서는 한(또는 여러) 스레드가 컬렉션을 읽고, 다른 스레드가 때때로 이를 변경하는 상황이 자주 발생합니다. 일반 컬렉션은 여기에 적합하지 않습니다. 경쟁 상태, 오류, ConcurrentModificationException 등 멀티스레드 세계의 “즐거움”이 생길 수 있기 때문입니다.
이런 경우를 위해 CopyOnWrite 컬렉션이 고안되었습니다 — 읽기는 잦고 변경은 드문 시나리오에 특화되어 있습니다.
어떻게 동작하나요?
- 변경(추가, 삭제, 대체)이 발생할 때마다 컬렉션은 내부 배열의 새 복사본을 생성합니다.
- 모든 읽기 스레드는 읽는 동안 바뀌지 않는 “자신만의” 배열 버전을 받습니다.
- 따라서 읽기는 완전히 안전하며 동기화가 필요 없습니다.
주요 클래스
- CopyOnWriteArrayList<E>
- CopyOnWriteArraySet<E>
이들은 java.util.concurrent 패키지에 있습니다.
사용 예
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteDemo {
public static void main(String[] args) {
CopyOnWriteArrayList<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("Alpha");
cowList.add("Beta");
// 누군가가 동시에 요소를 추가하더라도 안전하게 반복할 수 있습니다
for (String s : cowList) {
System.out.println(s);
cowList.add("Gamma"); // ConcurrentModificationException이 발생하지 않습니다!
}
System.out.println(cowList); // [Alpha, Beta, Gamma, Gamma]
}
}
특징:
- CopyOnWrite 컬렉션의 이터레이터는 항상 이터레이터 생성 시점의 스냅샷만을 “봅니다”.
- 이터레이터가 만들어진 이후에 누군가가 요소를 추가해도, 이터레이터는 이를 보지 못합니다.
- 순회 중에도 안전하게 요소를 추가/삭제할 수 있으며 — ConcurrentModificationException이 발생하지 않습니다!
CopyOnWrite 컬렉션을 언제 사용하나요?
많은 스레드가 주로 컬렉션을 읽고, 변경 작업은 매우 드문 상황에 적합합니다. 고전적인 예는 이벤트 리스너 목록(event listeners)입니다. 새로운 리스너의 추가/삭제는 자주 일어나지 않지만, 이 리스너들을 통지하는 작업은 계속해서 수행됩니다.
예 — 이벤트 구독자
import java.util.concurrent.CopyOnWriteArrayList;
public class EventBus {
private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>();
public void subscribe(Runnable listener) {
listeners.add(listener);
}
public void publishEvent() {
for (Runnable listener : listeners) {
listener.run(); // 방금 누군가가 구독/구독 해지했더라도 안전합니다!
}
}
}
CopyOnWrite 컬렉션의 단점
- 잦은 변경에는 느림: 변경이 있을 때마다 새 배열 복사본을 만들기 때문에 시간과 메모리 비용이 큽니다.
- 대규모 컬렉션에는 비효율적: 컬렉션이 큰 경우 배열 복사는 비용이 많이 듭니다.
3. 비교: 언제 무엇을 사용할까?
Unmodifiable 래퍼 (Collections.unmodifiable...)
언제 사용하나: 이미 있는 컬렉션을 외부 코드로부터의 변경으로부터 보호하고 싶지만, 내부(컬렉션 소유자)에서의 변경은 허용하고 싶을 때.
스레드 안전성: 보장되지 않습니다! 원본 컬렉션이 다른 스레드에서 변경되면 경쟁과 오류가 발생할 수 있습니다.
팩토리 메서드 (List.of, Set.of, Map.of)
언제 사용하나: 어디에서도 변경할 수 없는 상수 불변 컬렉션을 처음부터 만들고 싶을 때.
스레드 안전성: 보장됨(컬렉션이 전혀 변경되지 않음).
CopyOnWrite 컬렉션
언제 사용하나: 읽기는 많고 변경은 적은 멀티스레드 시나리오. 예: 구독자(리스너) 리스트.
스레드 안전성: 예, 완전히 스레드 안전합니다.
불변성: 아니요, 컬렉션을 변경할 수는 있지만 매번 새 복사본이 만들어져서 읽는 쪽은 영향을 받지 않습니다.
4. 흔한 실수와 구현상의 특징
오류 №1: 래퍼로 원본 컬렉션이 “동결”될 것이라 기대함. 많은 사람이 Collections.unmodifiableList(list)가 컬렉션을 완전히 불변으로 만든다고 생각합니다. 실제로는 누군가가 원본 리스트에 대한 참조를 가지고 있으면 여전히 변경할 수 있고, 그 변경은 래퍼를 통해서도 보입니다. 해결: 진짜 불변이 필요하다면 List.copyOf(list) (Java 10+) 또는 List.of(...)를 사용하세요.
오류 №2: 자주 변경되는 컬렉션에 CopyOnWrite를 사용함. CopyOnWriteArrayList에 요소를 지속적으로 추가하거나 삭제하면 성능과 메모리 문제가 발생합니다. CopyOnWrite는 “읽기 많고 쓰기 적은” 시나리오에만 적합합니다.
오류 №3: 래퍼가 곧 스레드 안전하다고 기대함. Collections.unmodifiableList는 컬렉션을 스레드 안전하게 만들지 않습니다! 원본 리스트가 여러 스레드에서 변경되면 오류가 발생할 수 있습니다.
오류 №4: List.of 또는 Set.of의 컬렉션에 null 사용. 일반 컬렉션과 달리, 팩토리 메서드는 null을 허용하지 않습니다 — null을 넣거나 심지어 null로 컬렉션을 생성하려 하면 NullPointerException이 발생합니다.
GO TO FULL VERSION