CodeGym /행동 /JAVA 25 SELF /CopyOnWrite 컬렉션, unmodifiable 래퍼

CopyOnWrite 컬렉션, unmodifiable 래퍼

JAVA 25 SELF
레벨 34, 레슨 2
사용 가능

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) — 기존 컬렉션 위의 래퍼입니다. 원본 리스트가 바뀌면 래퍼도 바뀝니다.

표: 접근 방식 비교

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이 발생합니다.

1
과제
JAVA 25 SELF, 레벨 34, 레슨 2
잠금
금지 단어 목록 보호 🚫
금지 단어 목록 보호 🚫
1
과제
JAVA 25 SELF, 레벨 34, 레슨 2
잠금
실시간 이벤트 로그 📈
실시간 이벤트 로그 📈
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION