본문 바로가기

Java List 선택 기준

ArrayList·LinkedList·List.of·Arrays.asList의 크기 변경 규칙을 구분하고 실전 게시글 저장소를 완성합니다.

직접 구현은 내부 원리를 이해하기 위한 장치이고 제품 코드에서는 검증된 표준 List를 우선합니다.

구현을 고를 때는 조회 패턴뿐 아니라 변경 가능성, null 정책, 원본 배열과의 공유까지 묻습니다.

특히 Arrays.asList는 set은 되지만 크기 변경은 안 되는 고정 크기 뷰입니다.


고정 크기 배열 뷰의 추가 실패

배열을 List로 보이게 만든 뒤 add를 호출합니다.

인터페이스 타입이 같아도 구현의 변경 가능성 규칙이 다를 수 있음을 보여 줍니다.

lab/ArraysAsListMutationFailure.java
import java.util.Arrays;
import java.util.List;

public final class ArraysAsListMutationFailure {
    public static void main(String[] args) {
        List<String> titles = Arrays.asList("array", "linked");
        titles.add("choice");
    }
}
목록 변경 권한과 원소 참조의 공유

배열 뷰·가변 복사·수정 불가 목록의 변경 권한과 원본 관계를 구분하며 원소 객체는 깊은 복사되지 않는다.

API 계약 · 목록과 원소를 구분

목록 변경 권한과 원소 참조의 공유

목록의 추가·삭제·교체 권한과 원소 객체의 공유는 다른 문제입니다. 수정 불가 목록도 가변 원소의 내부 상태까지 고정하지 않습니다.

배열 뷰의 연결과 복사 목록의 원소 참조 배열과Arrays.asList뷰는set에대한슬롯연결이양방향으로반영됩니다. 별도ArrayList복사와수정불가List.of/copyOf의원소는같은객체참조를보관할수있습니다. 점선은그참조관계이며각개념영역은계약비교이지순차실행이아닙니다. 복사목록을항상새인스턴스로만든다고약속하지않습니다. 배열과 뷰 원본 배열 set한 칸의 참조 공유 Arrays.asList 뷰 set 가능 / 크기 고정 목록 칸과 원소 객체 별도 목록 슬롯 ArrayList 복사 set/add/remove 가능 수정 불가 목록 슬롯 List.of / List.copyOf set/add/remove 불가 동일한 원소 객체 객체 내부는 변할 수 있음 왼쪽 실선: 원본 배열과 고정 크기 뷰 · 오른쪽 점선: 원소 객체 참조 각 계약을 비교하는 개념도 · 두 목록을 차례로 만들어야 한다는 실행 순서 아님
배열 뷰의 연결과 복사 목록의 원소 참조 배열과Arrays.asList뷰는set에대한슬롯연결이양방향으로반영됩니다. 별도ArrayList복사와수정불가List.of/copyOf의원소는같은객체참조를보관할수있습니다. 점선은그참조관계이며각개념영역은계약비교이지순차실행이아닙니다. 복사목록을항상새인스턴스로만든다고약속하지않습니다. 배열 슬롯을 보는 뷰 원본 배열 set한 칸의 참조 공유 Arrays.asList 뷰 set 가능 / 크기 고정 원소 객체 참조는 공유 별도 목록 슬롯 ArrayList 복사 set/add/remove 가능 수정 불가 목록 슬롯 List.of / List.copyOf set/add/remove 불가 동일한 원소 객체 객체 내부는 변할 수 있음 위 실선: 배열과 뷰의 연결 점선: 같은 원소 객체 참조 목록 슬롯 복사 ≠ 깊은 복사
목록 변경 권한과 원소 참조의 공유
만드는 방법목록의 변경 권한원본과의 관계
Arrays.asList(array)
set 가능
크기 변경 add/remove 불가
배열 슬롯과 목록 슬롯의 교체가 서로 반영됨
new ArrayList<>(view)
set/add/remove 가능
null 원소 허용
목록 슬롯은 별도이며 원소 객체의 참조는 공유
List.of(...)
set/add/remove 불가
null 원소 불가
전달한 원소 참조를 보관; 원소 객체를 복제하지 않음
List.copyOf(source)
set/add/remove 불가
null 원소 불가
이후 원본 목록의 추가·삭제·교체는 미반영; 원소 참조는 공유
Arrays.asList(array)
목록의 변경 권한
set 가능
크기 변경 add/remove 불가
원본과의 관계
배열 슬롯과 목록 슬롯의 교체가 서로 반영됨
new ArrayList<>(view)
목록의 변경 권한
set/add/remove 가능
null 원소 허용
원본과의 관계
목록 슬롯은 별도이며 원소 객체의 참조는 공유
List.of(...)
목록의 변경 권한
set/add/remove 불가
null 원소 불가
원본과의 관계
전달한 원소 참조를 보관; 원소 객체를 복제하지 않음
List.copyOf(source)
목록의 변경 권한
set/add/remove 불가
null 원소 불가
원본과의 관계
이후 원본 목록의 추가·삭제·교체는 미반영; 원소 참조는 공유

API 계약 비교입니다. 가변 원소의 내부 변경은 같은 객체를 보는 목록들에 보일 수 있습니다. 수정 불가 목록은 원소 객체까지 불변으로 만들지 않습니다.

관찰 결과
java.lang.UnsupportedOperationException

Arrays.asList는 배열 길이를 그대로 반영하는 고정 크기 뷰입니다.

크기를 바꾸려면 new ArrayList<>(view)로 복사해야 합니다.

목록의 add, set, remove를 막으려면 List.of 또는 List.copyOf를 사용합니다. 원소 객체 자체의 불변성은 별도입니다.


List 구현별 변경 규칙

  • 일반적인 조회·끝 추가 목록은 ArrayList를 기본 선택으로 두고 측정 결과로 반증합니다.
  • LinkedList는 양방향 Node와 first·last를 유지하며 Deque 연산이 목적일 때 더 자연스럽습니다.
  • List.of는 null과 구조 변경을 허용하지 않아 상수 데이터 의도를 명확히 합니다.
  • Arrays.asList는 원본 배열과 원소 변경을 공유하지만 add와 remove는 지원하지 않습니다.
  • List.copyOf는 이후 원본 목록의 추가·삭제·교체와 분리되지만 원소 객체의 참조는 공유합니다.
  • 변수 타입은 List<E> 역할로 두고 생성 지점에서 필요한 구현을 선택합니다.

ArrayList 내부 저장소와 불변 스냅숏

PracticalBoardList는 내부 저장소로 ArrayList를 선택하고 외부 조회에는 List.copyOf 스냅숏을 반환합니다.

첫 스냅숏을 만든 뒤 새 항목을 추가해도 이전 결과가 바뀌지 않습니다.

내부 구현과 외부 불변 규칙을 서로 다르게 선택할 수 있습니다.

src/PracticalBoardList.java
import java.util.ArrayList;
import java.util.List;

public final class PracticalBoardList {
    public static void main(String[] args) {
        PostRepository repository = new PostRepository();
        repository.add("array", 40);
        repository.add("linked", 50);
        List<Entry> snapshot = repository.entries();
        repository.add("choice", 30);
        System.out.println("snapshot=" + snapshot.size() + ", current=" + repository.entries().size());
        System.out.println("total=" + repository.total());
    }

    private static final class PostRepository {
        private final List<Entry> entries = new ArrayList<>();

        void add(String title, int viewCount) {
            if (title == null || title.isBlank() || viewCount <= 0)
                throw new IllegalArgumentException();
            entries.add(new Entry(title, viewCount));
        }

        List<Entry> entries() {
            return List.copyOf(entries);
        }

        int total() {
            return entries.stream().mapToInt(Entry::viewCount).sum();
        }
    }

    private record Entry(String title, int viewCount) {}
}

저장소에 추가해도 이전 스냅숏은 두 항목

이전 snapshot은 두 항목을 보존하고 저장소에 choice를 추가한 뒤 현재 항목 수 3개와 합계 120을 얻습니다. 복사 시점과 이후 변경의 실제 상태 차이입니다.

저장소에 추가해도 이전 스냅숏은 두 항목

snapshot 생성 뒤 choice:30을 추가하면 저장소는 3항목이 되며 이전 snapshot은 array:40과 linked:50을 유지한다.

저장소 상태 · 현재와 이전 스냅숏

저장소에 추가해도 이전 스냅숏은 두 항목

세 번째 항목을 저장소에 넣어도 이전 스냅숏에는 두 항목만 남습니다. 합계 120은 현재 저장소의 40 + 50 + 30입니다.

현재 저장소 추가와 이전 snapshot의 다른 크기 원문저장소array40/linked50뒤snapshot을받고choice30을추가합니다. 현재목록3항목합계120과이전snapshot2항목이분리됩니다. 실선은각목록상태의변화이며이전두Entry참조는공유합니다. 현재 저장소 두 Entry를 추가 array:40 / linked:50 현재 목록에 추가 choice:30 현재 크기 3 현재 저장소 합계 40 + 50 + 30 = 120 이전 snapshot 두 Entry 참조를 보관 array:40 / linked:50 현재 목록에 추가해도 새 choice 참조는 없음 이전 크기 유지 snapshot=2 실선: 각 목록 상태의 변화 snapshot은 기존 두 참조 공유 현재 목록 추가는 반영 안 됨
현재 저장소 추가와 이전 snapshot의 다른 크기 원문저장소array40/linked50뒤snapshot을받고choice30을추가합니다. 현재목록3항목합계120과이전snapshot2항목이분리됩니다. 실선은각목록상태의변화이며이전두Entry참조는공유합니다. 현재 저장소 두 Entry를 추가 array:40 / linked:50 현재 목록에 추가 choice:30 현재 크기 3 현재 저장소 합계 40 + 50 + 30 = 120 이전 snapshot 두 Entry 참조를 보관 array:40 / linked:50 현재 목록에 추가해도 새 choice 참조는 없음 이전 크기 유지 snapshot=2 실선: 각 목록 상태의 변화 snapshot은 기존 두 참조 공유 현재 목록 추가는 반영 안 됨

내부 ArrayList

두 Entry 뒤 snapshot = repository.entries()
array:40, linked:50
크기 2
repository.add("choice", 30)
array:40, linked:50, choice:30
현재 크기 3

이전 snapshot

두 Entry 뒤 snapshot = repository.entries()
같은 두 Entry 참조
크기 2
repository.add("choice", 30)
array:40, linked:50
이전 크기 2

소스 상태를 추적한 비교입니다. 원문의 출력은 snapshot=2, current=3과 total=120 두 줄입니다.

이 예의 조건

  • 복사는 멤버십이며 앞선 두 Entry 참조는 공유한다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원문 저장소의 array 40/linked 50 뒤 snapshot을 받고 choice 30을 추가합니다. 현재 목록 3항목·합계 120과 이전 snapshot 2항목이 분리됩니다. 실선은 각 목록 상태의 변화이며 이전 두 Entry 참조는 공유합니다.
5array:40
6linked:50
14array:40
15linked:50
원 도표의 설명과 주석
구분내용
설명원문 저장소의 array 40/linked 50 뒤 snapshot을 받고 choice 30을 추가합니다. 현재 목록 3항목·합계 120과 이전 snapshot 2항목이 분리됩니다. 실선은 각 목록 상태의 변화이며 이전 두 Entry 참조는 공유합니다.
전체 읽기 정보저장소에 추가해도 이전 스냅숏은 두 항목 저장소에 추가해도 이전 스냅숏은 두 항목 snapshot 생성 뒤 choice:30을 추가하면 저장소는 3항목이 되며 이전 snapshot은 array:40과 linked:50을 유지한다. 저장소 상태 · 현재와 이전 스냅숏 저장소에 추가해도 이전 스냅숏은 두 항목 세 번째 항목을 저장소에 넣어도 이전 스냅숏에는 두 항목만 남습니다. 합계 120은 현재 저장소의 40 + 50 + 30입니다. 내부 ArrayList 두 Entry 뒤 snapshot = repository.entries() array:40, linked:50 크기 2 repository.add("choice", 30) array:40, linked:50, choice:30 현재 크기 3 이전 snapshot 두 Entry 뒤 snapshot = repository.entries() 같은 두 Entry 참조 크기 2 repository.add("choice", 30) array:40, linked:50 이전 크기 2 소스 상태를 추적한 비교입니다. 원문의 출력은 snapshot=2, current=3과 total=120 두 줄입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
기존 두 항목

저장소에 array 40/linked 50을 보관합니다.

  • 크기: 2
  • 합: 90
  • snapshot의 기준 시점을 확보한다.
옛 snapshot

두 참조를 목록 사본으로 반환합니다.

  • snapshot: array40,linked50
  • 크기: 2
  • 이후 추가와 분리할 멤버십을 고정한다.
현재 저장소 추가

choice 30을 보관하고 현재/옛 목록을 읽습니다.

  • 현재: 3개 / total 120
  • snapshot: 2개 / choice 없음
  • 새 원소 참조가 옛 사본으로 자동 전달되지 않는다.

저장소에 추가해도 이전 스냅숏은 두 항목

이전 snapshot은 두 항목을 보존하고 저장소에 choice를 추가한 뒤 현재 항목 수 3개와 합계 120을 얻습니다. 복사 시점과 이후 변경의 실제 상태 차이입니다.

저장소에 추가해도 이전 스냅숏은 두 항목

snapshot 생성 뒤 choice:30을 추가하면 저장소는 3항목이 되며 이전 snapshot은 array:40과 linked:50을 유지한다.

저장소 상태 · 현재와 이전 스냅숏

저장소에 추가해도 이전 스냅숏은 두 항목

세 번째 항목을 저장소에 넣어도 이전 스냅숏에는 두 항목만 남습니다. 합계 120은 현재 저장소의 40 + 50 + 30입니다.

현재 저장소 추가와 이전 snapshot의 다른 크기 원문저장소array40/linked50뒤snapshot을받고choice30을추가합니다. 현재목록3항목합계120과이전snapshot2항목이분리됩니다. 실선은각목록상태의변화이며이전두Entry참조는공유합니다. 현재 저장소 두 Entry를 추가 array:40 / linked:50 현재 목록에 추가 choice:30 현재 크기 3 현재 저장소 합계 40 + 50 + 30 = 120 이전 snapshot 두 Entry 참조를 보관 array:40 / linked:50 현재 목록에 추가해도 새 choice 참조는 없음 이전 크기 유지 snapshot=2 실선: 각 목록 상태의 변화 snapshot은 기존 두 참조 공유 현재 목록 추가는 반영 안 됨
현재 저장소 추가와 이전 snapshot의 다른 크기 원문저장소array40/linked50뒤snapshot을받고choice30을추가합니다. 현재목록3항목합계120과이전snapshot2항목이분리됩니다. 실선은각목록상태의변화이며이전두Entry참조는공유합니다. 현재 저장소 두 Entry를 추가 array:40 / linked:50 현재 목록에 추가 choice:30 현재 크기 3 현재 저장소 합계 40 + 50 + 30 = 120 이전 snapshot 두 Entry 참조를 보관 array:40 / linked:50 현재 목록에 추가해도 새 choice 참조는 없음 이전 크기 유지 snapshot=2 실선: 각 목록 상태의 변화 snapshot은 기존 두 참조 공유 현재 목록 추가는 반영 안 됨

내부 ArrayList

두 Entry 뒤 snapshot = repository.entries()
array:40, linked:50
크기 2
repository.add("choice", 30)
array:40, linked:50, choice:30
현재 크기 3

이전 snapshot

두 Entry 뒤 snapshot = repository.entries()
같은 두 Entry 참조
크기 2
repository.add("choice", 30)
array:40, linked:50
이전 크기 2

소스 상태를 추적한 비교입니다. 원문의 출력은 snapshot=2, current=3과 total=120 두 줄입니다.

이 예의 조건

  • 복사는 멤버십이며 앞선 두 Entry 참조는 공유한다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원문 저장소의 array 40/linked 50 뒤 snapshot을 받고 choice 30을 추가합니다. 현재 목록 3항목·합계 120과 이전 snapshot 2항목이 분리됩니다. 실선은 각 목록 상태의 변화이며 이전 두 Entry 참조는 공유합니다.
5array:40
6linked:50
14array:40
15linked:50
원 도표의 설명과 주석
구분내용
설명원문 저장소의 array 40/linked 50 뒤 snapshot을 받고 choice 30을 추가합니다. 현재 목록 3항목·합계 120과 이전 snapshot 2항목이 분리됩니다. 실선은 각 목록 상태의 변화이며 이전 두 Entry 참조는 공유합니다.
전체 읽기 정보저장소에 추가해도 이전 스냅숏은 두 항목 저장소에 추가해도 이전 스냅숏은 두 항목 snapshot 생성 뒤 choice:30을 추가하면 저장소는 3항목이 되며 이전 snapshot은 array:40과 linked:50을 유지한다. 저장소 상태 · 현재와 이전 스냅숏 저장소에 추가해도 이전 스냅숏은 두 항목 세 번째 항목을 저장소에 넣어도 이전 스냅숏에는 두 항목만 남습니다. 합계 120은 현재 저장소의 40 + 50 + 30입니다. 내부 ArrayList 두 Entry 뒤 snapshot = repository.entries() array:40, linked:50 크기 2 repository.add("choice", 30) array:40, linked:50, choice:30 현재 크기 3 이전 snapshot 두 Entry 뒤 snapshot = repository.entries() 같은 두 Entry 참조 크기 2 repository.add("choice", 30) array:40, linked:50 이전 크기 2 소스 상태를 추적한 비교입니다. 원문의 출력은 snapshot=2, current=3과 total=120 두 줄입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
기존 두 항목

저장소에 array 40/linked 50을 보관합니다.

  • 크기: 2
  • 합: 90
  • snapshot의 기준 시점을 확보한다.
옛 snapshot

두 참조를 목록 사본으로 반환합니다.

  • snapshot: array40,linked50
  • 크기: 2
  • 이후 추가와 분리할 멤버십을 고정한다.
현재 저장소 추가

choice 30을 보관하고 현재/옛 목록을 읽습니다.

  • 현재: 3개 / total 120
  • snapshot: 2개 / choice 없음
  • 새 원소 참조가 옛 사본으로 자동 전달되지 않는다.

변경 가능성 결정

List.of는 생성 뒤 add, set, remove를 허용하지 않고 null도 받지 않습니다.

설정값이나 완성된 결과처럼 바뀌지 않아야 하는 데이터에 적합합니다.

List.copyOf는 이후 원본 목록의 추가·삭제·교체가 반영되지 않는 수정 불가 목록을 반환합니다. 원소 참조를 공유하므로 가변 원소의 내부 변경까지 막지는 않습니다.

목록의 칸과 그 칸이 가리키는 원소 객체를 따로 생각해야 합니다. 원문 Entry는 record여서 관찰값을 바꾸지 않지만, 원소가 가변 객체이면 목록의 add를 막아도 그 객체를 통해 값은 바뀔 수 있습니다. 아래는 그 차이를 확인하는 별도 예제이며 원래 저장소의 Entry 선언을 바꾸지 않습니다.

예시

src/SharedElementSnapshot.java
import java.util.ArrayList;
import java.util.List;

public final class SharedElementSnapshot {
    public static void main(String[] args) {
        MutableEntry first = new MutableEntry("array");
        List<MutableEntry> original = new ArrayList<>();
        original.add(first);
        List<MutableEntry> snapshot = List.copyOf(original);
        original.add(new MutableEntry("linked"));
        first.title = "updated";
        System.out.println("sizes=" + original.size() + "/" + snapshot.size());
        System.out.println("snapshot-title=" + snapshot.get(0).title);
        System.out.println("same-entry=" + (original.get(0) == snapshot.get(0)));
    }

    private static final class MutableEntry {
        private String title;
        MutableEntry(String title) { this.title = title; }
    }
}

출력은 sizes=2/1, snapshot-title=updated, same-entry=true입니다. 두 목록의 원소 수는 분리되지만 첫 칸이 같은 MutableEntry를 참조하므로 내부 제목 변경은 두 경로에서 보입니다. 이후 first.title 대입 대신 original.set(0, new MutableEntry("replacement"))만 실행하면 snapshot의 첫 참조는 이전 객체를 유지합니다. 그 조건에서 snapshot-title은 array이고 same-entry는 false입니다. 두 변경을 누적 실행한 결과와 혼동하지 않도록 각각 처음부터 별도로 실행해 확인하세요.

Arrays.asList는 배열을 고정 크기 List로 보는 뷰입니다.

set은 원본 배열에도 반영되지만 add와 remove는 실패합니다.

가변 목록이 필요하면 new ArrayList<>(Arrays.asList(values))처럼 명시적으로 복사합니다.


표준 가변 구현 선택

ArrayList는 임의 조회, 순차 순회, 끝 추가가 중심인 일반 목록의 기본값입니다.

내부 배열이 가득 찰 때 확장하지만 최적화된 복사와 캐시 지역성을 활용합니다.

초기 용량은 대량 입력 크기를 확실히 알 때만 힌트로 제공합니다.

LinkedList는 List와 Deque를 모두 구현하며 양방향 Node를 유지합니다.

index 기반 중간 접근이 빠른 구조는 아닙니다.

양끝 큐가 목적이라면 16장에서 다룰 ArrayDeque가 메모리와 순회 측면에서 더 나은 기본 선택인 경우가 많습니다.


게시글 저장소의 컬렉션 소유권

내부에는 ArrayList<Entry>를 두어 입력 순서와 끝 추가를 처리합니다.

조회 메서드는 내부 객체를 그대로 반환하지 않고 List.copyOf를 사용합니다.

호출자는 결과를 변경할 수 없고 나중에 게시글이 추가되어도 이전 스냅숏의 크기는 그대로입니다.

변수와 매개변수 타입은 가능하면 List 역할로 선언하되 생성식에서 구현 선택을 명확히 합니다.

반환 규칙이 불변인지 가변인지 메서드 이름과 문서로 드러내야 호출자가 UnsupportedOperationException을 시행착오로 배우지 않습니다.


표준 ArrayList 기반 게시글 목록

게시글 저장소는 검증된 ArrayList를 내부에 사용하고 합계를 제공합니다.

직접 만든 목록은 원리 확인용으로 남기되 실제 기능은 표준 구현의 예외·최적화·호환성을 활용합니다.

조회 결과는 복사본으로 경계를 닫습니다.

app/PostRepositoryCliCH149.java
import java.util.ArrayList;
import java.util.List;

public final class PostRepositoryCliCH149 {
    public static void main(String[] args) {
        PostRepository repository = new PostRepository();
        repository.add("standard-list", 46);
        repository.add("immutable-snapshot", 64);
        for (Entry entry : repository.findAll()) {
            System.out.println(entry.title() + "=" + entry.viewCount());
        }
        System.out.println("total=" + repository.totalViewCount());
    }

    private static final class PostRepository {
        private final List<Entry> entries = new ArrayList<>();

        void add(String title, int viewCount) {
            if (title == null || title.isBlank()) throw new IllegalArgumentException("title");
            if (viewCount <= 0) throw new IllegalArgumentException("viewCount");
            entries.add(new Entry(title, viewCount));
        }

        List<Entry> findAll() {
            return List.copyOf(entries);
        }

        int totalViewCount() {
            int total = 0;
            for (Entry entry : entries) {
                total += entry.viewCount();
            }
            return total;
        }
    }

    private record Entry(String title, int viewCount) {}
}

완성 CLI에서 내부 ArrayList는 끝 추가와 전체 출력이라는 실제 부하에 맞습니다.

findAll은 List.copyOf를 반환해 편집 권한을 넘기지 않습니다.

출력 total 110과 스냅숏 크기는 구현 교체보다 먼저 지켜야 하는 애플리케이션 규칙입니다.


표준 List 구현을 좁히는 순서

질문관찰할 값선택 또는 조치
기본 가변 목록인가조회·끝 추가 중심ArrayList
양끝 연산이 핵심인가Deque 사용 가능ArrayDeque 우선 검토
값을 고정할 것인가변경 허용 여부List.copyOf
배열과 공유해야 하는가원본 반영 의도Arrays.asList 제한 확인

표준 구현 이름보다 규칙을 먼저 고릅니다.

가변 여부, 순서, 중복, null, 스냅숏 여부를 정한 뒤 성능 특성으로 구현을 좁힙니다.

LinkedList를 단순히 중간 삽입 표만 보고 고르는 선택은 피합니다.


연습 문제

가변 ArrayList에 두 값을 넣고 List.copyOf 스냅숏을 만드세요.

원본에 세 번째 값을 추가한 뒤 두 목록 크기를 출력하고 스냅숏 add 실패도 관찰하세요.

정답과 해설

스냅숏은 생성 시점 원소를 보존합니다.

이후 원본 구조 변경은 반영되지 않으며 스냅숏 자체 수정도 거부됩니다.

exercise/ImmutableListSnapshotSolution.java
import java.util.ArrayList;
import java.util.List;

public final class ImmutableListSnapshotSolution {
    public static void main(String[] args) {
        ArrayList<String> mutable = new ArrayList<>(List.of("array", "list"));
        List<String> snapshot = List.copyOf(mutable);
        mutable.add("snapshot");
        System.out.println("mutable=" + mutable.size() + ", snapshot=" + snapshot.size());
        try {
            snapshot.add("blocked");
        } catch (UnsupportedOperationException exception) {
            System.out.println("immutable=" + exception.getClass().getSimpleName());
        }
    }
}

원본 추가와 스냅숏 추가의 다른 결과

snapshot을 만든 뒤 mutable에 추가하고 snapshot 추가는 거절되어 main이 계속됩니다. 두 목록의 고정 정체성과 크기/실패 전환을 추적할 수 있습니다.

원본 추가와 스냅숏 추가의 다른 결과

원본은 3항목으로 늘고 2항목 snapshot은 추가를 거부하며 main이 예외를 처리해 출력한다.

스냅숏 · 두 추가 호출의 결과

원본 추가와 스냅숏 추가의 다른 결과

원본에 추가하면 원본만 3개가 됩니다. 이전 스냅숏의 추가는 거부되고 catch에서 처리되어 프로그램은 정상 종료합니다.

원본 추가 성공과 snapshot 추가 거부 원본추가는크기를3으로만들지만기존snapshot은2입니다. snapshot.add blocked는UnsupportedOperationException으로거부되고catch가관찰하므로main은정상종료합니다. 실선은각호출과결과의관계입니다. 원본 mutable 첫 두 값을 보관 array / list 원본에 snapshot 추가 크기 3으로 변경 정상 관찰 mutable=3, snapshot=2 이전 snapshot 첫 두 참조를 보관 array / list / 크기 2 blocked 추가 시도 목록 변경은 거부 catch에서 관찰 UnsupportedOperation Exception / 크기 2 실선: 각각의 호출·상태 관계 나눈 예외명은 한 이름 예외 처리 뒤 main 정상 종료
원본 추가 성공과 snapshot 추가 거부 원본추가는크기를3으로만들지만기존snapshot은2입니다. snapshot.add blocked는UnsupportedOperationException으로거부되고catch가관찰하므로main은정상종료합니다. 실선은각호출과결과의관계입니다. 원본 mutable 첫 두 값을 보관 array / list 원본에 snapshot 추가 크기 3으로 변경 정상 관찰 mutable=3, snapshot=2 이전 snapshot 첫 두 참조를 보관 array / list / 크기 2 blocked 추가 시도 목록 변경은 거부 catch에서 관찰 UnsupportedOperation Exception / 크기 2 실선: 각각의 호출·상태 관계 나눈 예외명은 한 이름 예외 처리 뒤 main 정상 종료

mutable.add("snapshot")

호출 뒤 목록 상태
[array, list, snapshot]
원본 크기 3
관찰 결과
mutable=3, snapshot=2

snapshot.add("blocked")

호출 뒤 목록 상태
[array, list]
snapshot 크기 2 유지
관찰 결과

추가 거부; catch로 이동

immutable=UnsupportedOperationException

원본 추가가 이전 snapshot을 늘리지 않습니다. snapshot 추가 실패는 여기서 처리되므로 프로그램은 정상 종료합니다.

이 예의 조건

  • snapshot.add 실패를 catch하여 main은 정상 종료한다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원본 추가는 크기를 3으로 만들지만 기존 snapshot은 2입니다. snapshot.add의 blocked 추가는 UnsupportedOperationException으로 거부되고 catch가 관찰하므로 main은 정상 종료합니다. 실선은 각 호출과 결과의 관계입니다.
5array
6list
13array
14list
15크기 2
20Exception
21크기 2
원 도표의 설명과 주석
구분내용
설명원본 추가는 크기를 3으로 만들지만 기존 snapshot은 2입니다. snapshot.add의 blocked 추가는 UnsupportedOperationException으로 거부되고 catch가 관찰하므로 main은 정상 종료합니다. 실선은 각 호출과 결과의 관계입니다.
전체 읽기 정보원본 추가와 스냅숏 추가의 다른 결과 원본 추가와 스냅숏 추가의 다른 결과 원본은 3항목으로 늘고 2항목 snapshot은 추가를 거부하며 main이 예외를 처리해 출력한다. 스냅숏 · 두 추가 호출의 결과 원본 추가와 스냅숏 추가의 다른 결과 원본에 추가하면 원본만 3개가 됩니다. 이전 스냅숏의 추가는 거부되고 catch에서 처리되어 프로그램은 정상 종료합니다. mutable.add("snapshot") 호출 뒤 목록 상태 [array, list, snapshot] 원본 크기 3 관찰 결과 mutable=3, snapshot=2 snapshot.add("blocked") 호출 뒤 목록 상태 [array, list] snapshot 크기 2 유지 관찰 결과 추가 거부; catch로 이동 immutable=UnsupportedOperationException 원본 추가가 이전 snapshot을 늘리지 않습니다. snapshot 추가 실패는 여기서 처리되므로 프로그램은 정상 종료합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
복사 시점

mutable의 array/list를 snapshot으로 받습니다.

  • mutable: 2개
  • snapshot: 2개
  • 그 시점의 멤버십을 따로 보관한다.
원본 추가

mutable에 snapshot 값을 추가합니다.

  • mutable: 3개
  • snapshot: 2개 유지
  • 새 원본 멤버는 옛 snapshot에 반영되지 않는다.
snapshot 쓰기 거절

blocked를 추가하려 하지만 실패합니다.

  • 예외: UnsupportedOperationException
  • snapshot: 2개 유지
  • main: 계속
  • 구조 수정 계약이 반환된 목록의 쓰기를 막는다.

원본 추가와 스냅숏 추가의 다른 결과

snapshot을 만든 뒤 mutable에 추가하고 snapshot 추가는 거절되어 main이 계속됩니다. 두 목록의 고정 정체성과 크기/실패 전환을 추적할 수 있습니다.

원본 추가와 스냅숏 추가의 다른 결과

원본은 3항목으로 늘고 2항목 snapshot은 추가를 거부하며 main이 예외를 처리해 출력한다.

스냅숏 · 두 추가 호출의 결과

원본 추가와 스냅숏 추가의 다른 결과

원본에 추가하면 원본만 3개가 됩니다. 이전 스냅숏의 추가는 거부되고 catch에서 처리되어 프로그램은 정상 종료합니다.

원본 추가 성공과 snapshot 추가 거부 원본추가는크기를3으로만들지만기존snapshot은2입니다. snapshot.add blocked는UnsupportedOperationException으로거부되고catch가관찰하므로main은정상종료합니다. 실선은각호출과결과의관계입니다. 원본 mutable 첫 두 값을 보관 array / list 원본에 snapshot 추가 크기 3으로 변경 정상 관찰 mutable=3, snapshot=2 이전 snapshot 첫 두 참조를 보관 array / list / 크기 2 blocked 추가 시도 목록 변경은 거부 catch에서 관찰 UnsupportedOperation Exception / 크기 2 실선: 각각의 호출·상태 관계 나눈 예외명은 한 이름 예외 처리 뒤 main 정상 종료
원본 추가 성공과 snapshot 추가 거부 원본추가는크기를3으로만들지만기존snapshot은2입니다. snapshot.add blocked는UnsupportedOperationException으로거부되고catch가관찰하므로main은정상종료합니다. 실선은각호출과결과의관계입니다. 원본 mutable 첫 두 값을 보관 array / list 원본에 snapshot 추가 크기 3으로 변경 정상 관찰 mutable=3, snapshot=2 이전 snapshot 첫 두 참조를 보관 array / list / 크기 2 blocked 추가 시도 목록 변경은 거부 catch에서 관찰 UnsupportedOperation Exception / 크기 2 실선: 각각의 호출·상태 관계 나눈 예외명은 한 이름 예외 처리 뒤 main 정상 종료

mutable.add("snapshot")

호출 뒤 목록 상태
[array, list, snapshot]
원본 크기 3
관찰 결과
mutable=3, snapshot=2

snapshot.add("blocked")

호출 뒤 목록 상태
[array, list]
snapshot 크기 2 유지
관찰 결과

추가 거부; catch로 이동

immutable=UnsupportedOperationException

원본 추가가 이전 snapshot을 늘리지 않습니다. snapshot 추가 실패는 여기서 처리되므로 프로그램은 정상 종료합니다.

이 예의 조건

  • snapshot.add 실패를 catch하여 main은 정상 종료한다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원본 추가는 크기를 3으로 만들지만 기존 snapshot은 2입니다. snapshot.add의 blocked 추가는 UnsupportedOperationException으로 거부되고 catch가 관찰하므로 main은 정상 종료합니다. 실선은 각 호출과 결과의 관계입니다.
5array
6list
13array
14list
15크기 2
20Exception
21크기 2
원 도표의 설명과 주석
구분내용
설명원본 추가는 크기를 3으로 만들지만 기존 snapshot은 2입니다. snapshot.add의 blocked 추가는 UnsupportedOperationException으로 거부되고 catch가 관찰하므로 main은 정상 종료합니다. 실선은 각 호출과 결과의 관계입니다.
전체 읽기 정보원본 추가와 스냅숏 추가의 다른 결과 원본 추가와 스냅숏 추가의 다른 결과 원본은 3항목으로 늘고 2항목 snapshot은 추가를 거부하며 main이 예외를 처리해 출력한다. 스냅숏 · 두 추가 호출의 결과 원본 추가와 스냅숏 추가의 다른 결과 원본에 추가하면 원본만 3개가 됩니다. 이전 스냅숏의 추가는 거부되고 catch에서 처리되어 프로그램은 정상 종료합니다. mutable.add("snapshot") 호출 뒤 목록 상태 [array, list, snapshot] 원본 크기 3 관찰 결과 mutable=3, snapshot=2 snapshot.add("blocked") 호출 뒤 목록 상태 [array, list] snapshot 크기 2 유지 관찰 결과 추가 거부; catch로 이동 immutable=UnsupportedOperationException 원본 추가가 이전 snapshot을 늘리지 않습니다. snapshot 추가 실패는 여기서 처리되므로 프로그램은 정상 종료합니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
복사 시점

mutable의 array/list를 snapshot으로 받습니다.

  • mutable: 2개
  • snapshot: 2개
  • 그 시점의 멤버십을 따로 보관한다.
원본 추가

mutable에 snapshot 값을 추가합니다.

  • mutable: 3개
  • snapshot: 2개 유지
  • 새 원본 멤버는 옛 snapshot에 반영되지 않는다.
snapshot 쓰기 거절

blocked를 추가하려 하지만 실패합니다.

  • 예외: UnsupportedOperationException
  • snapshot: 2개 유지
  • main: 계속
  • 구조 수정 계약이 반환된 목록의 쓰기를 막는다.

첫 출력은 mutable=3, snapshot=2이고 두 번째는 불변 변경 차단을 보여 줍니다.

예외를 삼키기 위한 패턴이 아니라 변경 규칙을 관찰하는 예제 코드입니다.


표준 List 통합 판단

목록을 고르기 전에 순서·중복·null·변경 가능성을 결정합니다.

일반 가변 목록은 ArrayList에서 시작하고 실제 작업 부하가 반증할 때 바꿉니다.

외부에 돌려주는 값은 소유권 정책에 따라 불변 스냅숏 또는 명시적 가변 복사본으로 구분합니다.