CodeGym /행동 /JAVA 25 SELF /불변 컬렉션: Collections.unmodifiable

불변 컬렉션: Collections.unmodifiable

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

1. 컬렉션 가변성의 문제

Java의 컬렉션은 누구나 와서 무언가를 추가하고, 제거하고, 바꿀 수 있는 창고와 비슷합니다. 때로는 편리하지만, 대규모 프로그램에서는 두통거리가 됩니다. 자신의 클래스에서 바깥으로 상품 목록을 넘겼는데 누군가가 항목의 절반을 삭제해 버렸다고 상상해 보세요. 또는 — 더 골치 아프게 — 멀티스레드 프로그램에서 한 스레드는 요소를 추가하고 다른 스레드는 읽고 있다면 결과가 예상 밖이 될 수 있고, 오류는 잡기 어렵습니다(예: ConcurrentModificationException).

다음은 왜 컬렉션의 가변성이 버그의 근원이 되는지 보여 주는 예입니다:

import java.util.*;

public class Inventory {
    private List<String> products = new ArrayList<>();

    public Inventory() {
        products.add("차");
        products.add("커피");
    }

    public List<String> getProducts() {
        // 위험! 내부 리스트에 대한 참조를 반환함
        return products;
    }
}
public class Main {
    public static void main(String[] args) {
        Inventory inv = new Inventory();
        List<String> external = inv.getProducts();
        external.remove("차"); // 앗! 이제 인벤토리에 차가 없다
        System.out.println(inv.getProducts()); // [커피]
    }
}

함정이 보이나요? 한 메서드는 내부 컬렉션을 그대로 돌려주고, 다른 곳에서 그 컬렉션을 바꿉니다. 이렇게 하면 보호되어야 할 데이터를 우연히 망가뜨릴 수 있습니다.

2. 불변 컬렉션 만들기: Collections.unmodifiable*

이런 문제를 피하기 위해 Java는 효과적인 보호 수단을 제공합니다. 컬렉션을 클래스 Collections의 특수 래퍼로 “불변”으로 만들 수 있습니다:

  • Collections.unmodifiableList(list)
  • Collections.unmodifiableSet(set)
  • Collections.unmodifiableMap(map)

어떻게 작동할까요? 먼저 일반 컬렉션을 만든 뒤, 그 위를 “불변” 껍질로 감쌉니다:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> drinks = new ArrayList<>();
        drinks.add("차");
        drinks.add("커피");

        List<String> immutableDrinks = Collections.unmodifiableList(drinks);

        System.out.println(immutableDrinks); // [차, 커피]

        // 요소를 추가해 보자
        immutableDrinks.add("코코아"); // 쾅! UnsupportedOperationException
    }
}

이런 컬렉션을 변경하려 하면 UnsupportedOperationException이 던져집니다. 마치 상자에 거대한 스티커 "건들지 마!"를 붙여 둔 것과 같아서, 누가 추가/삭제를 시도하든 혼쭐(혹은 호출 스택)나게 됩니다.

예시: 내부 상태 보호

앞선 예시의 Inventory 클래스를 고쳐 봅시다:

import java.util.*;

public class Inventory {
    private List<String> products = new ArrayList<>();

    public Inventory() {
        products.add("차");
        products.add("커피");
    }

    public List<String> getProducts() {
        // 이제 래퍼를 반환합니다
        return Collections.unmodifiableList(products);
    }
}

이제 누군가 반환된 리스트를 변경하려 하면 예외가 발생합니다.

3. 불변 컬렉션의 동작: 얕은 보호

중요한 점: unmodifiableList와 그 ‘형제들’은 원본 컬렉션을 싸는 껍질일 뿐입니다. 복사본을 만들지 않습니다 — ‘안쪽’의 원본 컬렉션을 변경하면 그 변경은 래퍼에서도 보입니다!

데모

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> drinks = new ArrayList<>();
        drinks.add("차");
        List<String> immutableDrinks = Collections.unmodifiableList(drinks);

        drinks.add("커피"); // 원본 컬렉션을 변경
        System.out.println(immutableDrinks); // [차, 커피] — 요소가 나타났다!
    }
}

결론: 래퍼는 래퍼 자체를 통한 변경만 막아 줍니다. 누군가 원본 컬렉션에 대한 참조를 들고 있다면, 여전히 그것을 변경할 수 있습니다.

4. 깊은 불변성: 신화와 현실

unmodifiable* 래퍼는 바깥에서만 컬렉션을 불변으로 만듭니다. 하지만 컬렉션이 가변 객체를 담고 있다면, 그 객체는 여전히 바꿀 수 있습니다!

예시

import java.util.*;

class Product {
    String name;
    Product(String name) {
        this.name = name;
    }
    public String toString() {
        return name;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Product("차"));
        List<Product> immutableProducts = Collections.unmodifiableList(products);

        // 컬렉션 내부의 객체를 변경
        immutableProducts.get(0).name = "커피";
        System.out.println(immutableProducts); // [커피]
    }
}

결론:

  • 컬렉션 자체는 “불변”이지만, 내부 객체는 아니다.
  • 완전한(깊은) 불변성을 원한다면 불변 객체를 사용하세요(예: String, Integer, record 클래스, 또는 자신의 클래스를 immutable로 만들기).

5. 언제 불변 컬렉션을 사용할까

내부 상태 보호용

컬렉션을 보관하는 클래스를 작성하고 그것을 외부에 반환한다면, 항상 래퍼를 반환하여 누군가가 당신의 데이터를 실수로(또는 의도적으로) 바꾸지 못하게 하세요:

public List<String> getProducts() {
    return Collections.unmodifiableList(products);
}

멀티스레드 프로그램에서

멀티스레드 애플리케이션에서 가변 컬렉션은 문제의 근원입니다(race condition, ConcurrentModificationException 등). 생성 이후 컬렉션을 바꿀 필요가 없다면 — 불변으로 만드세요.

레이어 간 데이터 전달

프로그램의 한 레이어에서 다른 레이어(예: DAO → 서비스)로 컬렉션을 전달한다면, 불변 복사본이나 래퍼를 전달하세요 — 우발적 변경으로부터 보호합니다.

6. 실전 예제

예시 1: 학생 목록 보호

import java.util.*;

public class Group {
    private final List<String> students = new ArrayList<>();

    public void addStudent(String name) {
        students.add(name);
    }

    public List<String> getStudents() {
        return Collections.unmodifiableList(students);
    }
}

이제 누구도 getStudents()로 학생을 직접 추가하거나 삭제할 수 없습니다.

예시 2: 불변 맵(Map)

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> grades = new HashMap<>();
        grades.put("Vasya", 5);
        grades.put("Masha", 4);

        Map<String, Integer> immutableGrades = Collections.unmodifiableMap(grades);

        // immutableGrades.put("Petya", 3); // UnsupportedOperationException
    }
}

7. 유용한 포인트

현대적 대안: List.of, Set.of, Map.of

Java 9에서 더 편리한 불변 컬렉션 생성 방법이 추가되었습니다:

List<String> drinks = List.of("차", "커피");
Set<String> fruits = Set.of("사과", "바나나");
Map<String, Integer> ages = Map.of("Vasya", 20, "Masha", 21);
  • 이 컬렉션들은 불변입니다(변경을 시도하면 예외).
  • 원본 가변 컬렉션을 갖지 않습니다( Collections.unmodifiable* 와 달리).
  • null을 허용하지 않습니다.

또한 Java 10부터는 List.copyOf, Set.copyOf, Map.copyOf가 제공되며 — 전달한 컬렉션의 불변 복사본을 만듭니다.

불변 컬렉션 생성 방식 비교

방법 깊은 불변성 원본 컬렉션 변경 가능? null 허용? Java 버전
Collections.unmodifiableList(list)
아니요 예 예 1.2
List.of(...), Set.of(...), Map.of(...)
아니요 아니요(원본 컬렉션 없음) 아니요 9+

8. 불변 컬렉션 사용 시 흔한 실수

오류 №1: 래퍼 생성 후 원본 컬렉션을 변경함. unmodifiableList를 만들었지만 누군가 원본 리스트를 바꿉니다. 래퍼는 이것을 막지 못합니다 — 변경은 래퍼를 쓰는 모든 곳에서 그대로 보입니다.

오류 №2: 깊은 불변성을 기대함. 많은 사람이 컬렉션이 불변이면 내부 객체도 바꿀 수 없다고 생각합니다. 실제로 보호되는 것은 구조(컬렉션을 통한 추가/삭제/변경)일 뿐, 객체의 내용물은 아닙니다.

오류 №3: 최신 팩토리에서 null을 사용하는 것. List.of, Set.of, Map.of로 생성한 컬렉션은 null을 허용하지 않습니다. null을 넣거나 얻으려 하면 예외가 발생합니다.

오류 №4: 가변 컬렉션의 참조를 외부로 내보냄. 내부 컬렉션에 대한 참조(래퍼 없이)를 외부로 반환하면 데이터에 대한 통제력을 잃게 됩니다 — 버그와 불변식(invariant) 파괴로 직행하는 지름길입니다.

오류 №5: 가변성을 기대하는 코드에서 불변 컬렉션을 사용함. 외부 코드가 컬렉션을 변경하려고 하면(예: 요소 추가) UnsupportedOperationException이 발생합니다. 소비자들이 데이터의 불변성을 알고 있도록 하세요.

1
과제
JAVA 25 SELF, 레벨 33, 레슨 2
잠금
도서관의 희귀 도서 보호 목록 만들기 📚
도서관의 희귀 도서 보호 목록 만들기 📚
1
과제
JAVA 25 SELF, 레벨 33, 레슨 2
잠금
레스토랑의 동적 메뉴: 레시피 변경이 게스트 뷰에 미치는 영향 🍽️
레스토랑의 동적 메뉴: 레시피 변경이 게스트 뷰에 미치는 영향 🍽️
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION