CodeGym /행동 /JAVA 25 SELF /List.of, Set.of, Map.of — 불변 컬렉션

List.of, Set.of, Map.of — 불변 컬렉션

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

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 가능? 중복? 스레드 안전성 예
new ArrayList<>()
예 예 예 아니요
new ArrayList<>()
List.of(...)
아니요 아니요 예 예*
List.of("a", "b")
Set.of(...)
아니요 아니요 아니요 예*
Set.of("a", "b")
Map.of(...)
아니요 아니요 아니요 예*
Map.of("a", 1, "b", 2)
Collections.unmodifiableList(...)
아니요 원본에 따라 다름 예 아니요
Collections.unmodifiableList(list)

* — 불변성의 관점에서만 스레드 안전합니다: 컬렉션이 변경되지 않는다면 여러 스레드에서 안전하게 읽을 수 있습니다.

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) 같은 메서드도 예외를 던집니다. 팩토리 메서드로 생성한 컬렉션은 불변임을 기억하세요.

1
과제
JAVA 25 SELF, 레벨 34, 레슨 0
잠금
엄격한 색상 팔레트 🎨
엄격한 색상 팔레트 🎨
1
과제
JAVA 25 SELF, 레벨 34, 레슨 0
잠금
데이터 보호: 빈 항목과 중복 금지! 🛡️
데이터 보호: 빈 항목과 중복 금지! 🛡️
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION