본문 바로가기

공유 참조와 소유권

하나의 가변 게시글을 여러 객체가 공유할 때 발생하는 부수 효과를 재현하고 복사·소유권·불변성 중 적합한 해결책을 선택합니다.

공유 참조는 두 코드 경로가 같은 객체 상태를 읽고 쓸 수 있게 합니다.

변경이 협력의 목적이라면 유용하지만, 한쪽의 지역적 수정이 다른 쪽의 결과까지 바꾸면 사이드 이펙트가 됩니다.

문제의 핵심은 참조를 복사했다는 사실보다 누가 변경 권한을 가지는지 드러나지 않는 데 있습니다.


미리보기 보정과 원본 상태 변경

다음 프로그램은 순위 미리보기용으로 조회 수에 5를 더합니다.

DailyReport와 PreviewService가 같은 MutableEntry를 공유하므로 보고서의 조회 수도 함께 바뀝니다.

lab/SharedEntrySideEffect.java
public final class SharedEntrySideEffect {
    public static void main(String[] args) {
        MutableEntry entry = new MutableEntry("object", 40);
        DailyReport report = new DailyReport(entry);
        PreviewService preview = new PreviewService(entry);

        preview.applyPreviewBoost(5);

        System.out.println("stored=" + report.totalViewCount());
        System.out.println("preview=" + preview.previewViewCount());
    }

    private static final class MutableEntry {
        private final String title;
        private int viewCount;

        MutableEntry(String title, int viewCount) {
            this.title = title;
            this.viewCount = viewCount;
        }

        void addViewCount(int extra) { viewCount += extra; }
        int viewCount() { return viewCount; }
    }

    private record DailyReport(MutableEntry entry) {
        int totalViewCount() { return entry.viewCount(); }
    }

    private record PreviewService(MutableEntry entry) {
        void applyPreviewBoost(int extra) { entry.addViewCount(extra); }
        int previewViewCount() { return entry.viewCount(); }
    }
}
의도하지 않은 결과
stored=45
preview=45

보고서는 조회 수 40의 원본을 보존해야 했지만 미리보기 보정이 저장 값까지 변경했습니다.

메서드 이름만 보면 PreviewService 내부 계산처럼 보이지만 실제로는 외부가 소유한 객체를 수정합니다.

이런 숨은 쓰기는 호출 순서에 따라 결과를 달라지게 하고 테스트를 서로 간섭시킵니다.


객체 복사와 영향 분리

한쪽이 독립적으로 수정해야 한다면 값이 같은 새 객체를 전달할 수 있습니다.

복사 위치를 조립점에 두면 소유권 전환이 코드에 보입니다.

src/SeparatedMutableEntries.java
public final class SeparatedMutableEntries {
    public static void main(String[] args) {
        MutableEntry stored = new MutableEntry("object", 40);
        MutableEntry previewEntry = stored.copy();

        DailyReport report = new DailyReport(stored);
        PreviewService preview = new PreviewService(previewEntry);
        preview.applyPreviewBoost(5);

        System.out.println("stored=" + report.totalViewCount());
        System.out.println("preview=" + preview.previewViewCount());
    }

    private static final class MutableEntry {
        private final String title;
        private int viewCount;

        MutableEntry(String title, int viewCount) {
            this.title = title;
            this.viewCount = viewCount;
        }

        MutableEntry copy() { return new MutableEntry(title, viewCount); }
        void addViewCount(int extra) { viewCount += extra; }
        int viewCount() { return viewCount; }
    }

    private record DailyReport(MutableEntry entry) {
        int totalViewCount() { return entry.viewCount(); }
    }

    private record PreviewService(MutableEntry entry) {
        void applyPreviewBoost(int extra) { entry.addViewCount(extra); }
        int previewViewCount() { return entry.viewCount(); }
    }
}
stored=40
preview=45

해결은 되었지만 모든 전달 지점에서 복사를 기억해야 합니다.

중첩된 가변 목록까지 독립이어야 한다면 얕은 복사로는 부족합니다.

복사 정책이 여러 곳에 흩어지면 새 필드가 추가됐을 때 일부 복사본이 누락될 수 있습니다.

같은 게시글을 공유할 때와 복사한 게시글을 수정할 때

공유 실행은 report/preview가 같은 Entry 40→45를 보고 copy 실행은 report 40을 보존하며 preview만 45가 됩니다. 두 독립 전후 상태를 고정 참조 역할로 비교할 수 있습니다.

같은 게시글을 공유할 때와 복사한 게시글을 수정할 때

SharedEntrySideEffect는 두 소비자가 같은 MutableEntry를 보므로 미리보기 +5가 보고서에도 45로 보인다. SeparatedMutableEntries는 previewEntry를 새 객체로 복사해 저장 값 40과 미리보기 45를 분리한다.

복사한 객체를 수정하면 보고서의 원본이 유지됩니다

두 프로그램 모두 제목 "object", 조회 수 40에서 시작해 preview.applyPreviewBoost(5)를 호출합니다. 아래 행은 서로 다른 실행입니다.

복사한 객체를 수정하면 보고서의 원본이 유지됩니다 공유예제에서report와preview는같은가변Entry40→45를본다. 별도copy()예제에서는report의40과preview의45가다른객체이며점선은생성시값복사이다. 공유 예제 copy() 예제 report 참조 preview 참조 같은 Entry 40 → 45 report의 Entry 조회 수 40 preview의 Entry 40 → 45 copy(): 별도 객체 왼쪽은 참조 공유 · 오른쪽은 생성 시 값 복사 · 독립된 두 실행
복사한 객체를 수정하면 보고서의 원본이 유지됩니다 공유예제에서report와preview는같은가변Entry40→45를본다. 별도copy()예제에서는report의40과preview의45가다른객체이며점선은생성시값복사이다. 공유 예제 · 별도 실행 report 참조 preview 참조 같은 Entry 40 → 45 복사 예제 · 별도 실행 report의 Entry 조회 수 40 preview의 Entry 40 → 45 copy(): 새 객체 외부 파일 저장의 뜻 아님
같은 미리보기 보정이 참조 공유 여부에 따라 달라지는 결과
원문 프로그램두 소비자가 보는 대상보정 뒤 실제 출력
SharedEntrySideEffectDailyReport와 PreviewService에 같은 entry를 전달합니다. 한 MutableEntry의 조회 수가 45가 됩니다.
stored=45
preview=45
SeparatedMutableEntriespreviewEntry = stored.copy()가 새 MutableEntry를 만듭니다. 보고서는 stored, 미리보기는 previewEntry를 봅니다.
stored=40
preview=45
SharedEntrySideEffect
두 소비자가 보는 대상: DailyReport와 PreviewService에 같은 entry를 전달합니다. 한 MutableEntry의 조회 수가 45가 됩니다.
보정 뒤 실제 출력:
stored=45
preview=45
SeparatedMutableEntries
두 소비자가 보는 대상: previewEntry = stored.copy()가 새 MutableEntry를 만듭니다. 보고서는 stored, 미리보기는 previewEntry를 봅니다.
보정 뒤 실제 출력:
stored=40
preview=45

보고서의 stored=는 report.totalViewCount()의 출력 라벨입니다. 이 예제에 파일 저장소나 별도 화면 객체는 없습니다. 복사본의 제목 문자열과 숫자 상태만 비교하며, 임의의 중첩 가변 객체까지 복제한다고 주장하지 않습니다.

이 예의 조건

  • 공유와 copy는 별도 실행입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2공유 예제에서 report와 preview는 같은 가변 Entry 40→45를 본다. 별도 copy() 예제에서는 report의 40과 preview의 45가 다른 객체이며 점선은 생성 시 값 복사이다.
원 도표의 설명과 주석
구분내용
설명공유 예제에서 report와 preview는 같은 가변 Entry 40→45를 본다. 별도 copy() 예제에서는 report의 40과 preview의 45가 다른 객체이며 점선은 생성 시 값 복사이다.
전체 읽기 정보같은 게시글을 공유할 때와 복사한 게시글을 수정할 때 같은 게시글을 공유할 때와 복사한 게시글을 수정할 때 SharedEntrySideEffect는 두 소비자가 같은 MutableEntry를 보므로 미리보기 +5가 보고서에도 45로 보인다. SeparatedMutableEntries는 previewEntry를 새 객체로 복사해 저장 값 40과 미리보기 45를 분리한다. 복사한 객체를 수정하면 보고서의 원본이 유지됩니다 두 프로그램 모두 제목 "object" , 조회 수 40 에서 시작해 preview.applyPreviewBoost(5) 를 호출합니다. 아래 행은 서로 다른 실행입니다. 같은 미리보기 보정이 참조 공유 여부에 따라 달라지는 결과 원문 프로그램 두 소비자가 보는 대상 보정 뒤 실제 출력 SharedEntrySideEffect DailyReport 와 PreviewService 에 같은 entry 를 전달합니다. 한 MutableEntry 의 조회 수가 45 가 됩니다. stored=45 preview=45 SeparatedMutableEntries previewEntry = stored.copy() 가 새 MutableEntry 를 만듭니다. 보고서는 stored , 미리보기는 previewEntry 를 봅니다. stored=40 preview=45 SharedEntrySideEffect 두 소비자가 보는 대상: DailyReport 와 PreviewService 에 같은 entry 를 전달합니다. 한 MutableEntry 의 조회 수가 45 가 됩니다. 보정 뒤 실제 출력: stored=45 preview=45 SeparatedMutableEntries 두 소비자가 보는 대상: previewEntry = stored.copy() 가 새 MutableEntry 를 만듭니다. 보고서는 stored , 미리보기는 previewEntry 를 봅니다. 보정 뒤 실제 출력: stored=40 preview=45 보고서의 stored= 는 report.totalViewCount() 의 출력 라벨입니다. 이 예제에 파일 저장소나 별도 화면 객체는 없습니다. 복사본의 제목 문자열과 숫자 상태만 비교하며, 임의의 중첩 가변 객체까지 복제한다고 주장하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초기 값

두 실행의 report는 Entry 40입니다.

  • 공유 preview: 같은 참조
  • copy preview: 새 객체 40
  • copy가 새 저장 객체를 만듭니다.
preview 변경

preview의 조회 수를 45로 바꿉니다.

  • 공유 객체: 40→45
  • copy 객체: 40→45
  • 쓰기 표현식은 같아도 대상 객체가 다릅니다.
report 관찰

공유 report는 45, copy report는 40입니다.

  • 공유: report/preview 45
  • copy: report 40/preview45
  • 생성 시 값 복사가 앞 객체의 상태를 보존합니다.

같은 게시글을 공유할 때와 복사한 게시글을 수정할 때

공유 실행은 report/preview가 같은 Entry 40→45를 보고 copy 실행은 report 40을 보존하며 preview만 45가 됩니다. 두 독립 전후 상태를 고정 참조 역할로 비교할 수 있습니다.

같은 게시글을 공유할 때와 복사한 게시글을 수정할 때

SharedEntrySideEffect는 두 소비자가 같은 MutableEntry를 보므로 미리보기 +5가 보고서에도 45로 보인다. SeparatedMutableEntries는 previewEntry를 새 객체로 복사해 저장 값 40과 미리보기 45를 분리한다.

복사한 객체를 수정하면 보고서의 원본이 유지됩니다

두 프로그램 모두 제목 "object", 조회 수 40에서 시작해 preview.applyPreviewBoost(5)를 호출합니다. 아래 행은 서로 다른 실행입니다.

복사한 객체를 수정하면 보고서의 원본이 유지됩니다 공유예제에서report와preview는같은가변Entry40→45를본다. 별도copy()예제에서는report의40과preview의45가다른객체이며점선은생성시값복사이다. 공유 예제 copy() 예제 report 참조 preview 참조 같은 Entry 40 → 45 report의 Entry 조회 수 40 preview의 Entry 40 → 45 copy(): 별도 객체 왼쪽은 참조 공유 · 오른쪽은 생성 시 값 복사 · 독립된 두 실행
복사한 객체를 수정하면 보고서의 원본이 유지됩니다 공유예제에서report와preview는같은가변Entry40→45를본다. 별도copy()예제에서는report의40과preview의45가다른객체이며점선은생성시값복사이다. 공유 예제 · 별도 실행 report 참조 preview 참조 같은 Entry 40 → 45 복사 예제 · 별도 실행 report의 Entry 조회 수 40 preview의 Entry 40 → 45 copy(): 새 객체 외부 파일 저장의 뜻 아님
같은 미리보기 보정이 참조 공유 여부에 따라 달라지는 결과
원문 프로그램두 소비자가 보는 대상보정 뒤 실제 출력
SharedEntrySideEffectDailyReport와 PreviewService에 같은 entry를 전달합니다. 한 MutableEntry의 조회 수가 45가 됩니다.
stored=45
preview=45
SeparatedMutableEntriespreviewEntry = stored.copy()가 새 MutableEntry를 만듭니다. 보고서는 stored, 미리보기는 previewEntry를 봅니다.
stored=40
preview=45
SharedEntrySideEffect
두 소비자가 보는 대상: DailyReport와 PreviewService에 같은 entry를 전달합니다. 한 MutableEntry의 조회 수가 45가 됩니다.
보정 뒤 실제 출력:
stored=45
preview=45
SeparatedMutableEntries
두 소비자가 보는 대상: previewEntry = stored.copy()가 새 MutableEntry를 만듭니다. 보고서는 stored, 미리보기는 previewEntry를 봅니다.
보정 뒤 실제 출력:
stored=40
preview=45

보고서의 stored=는 report.totalViewCount()의 출력 라벨입니다. 이 예제에 파일 저장소나 별도 화면 객체는 없습니다. 복사본의 제목 문자열과 숫자 상태만 비교하며, 임의의 중첩 가변 객체까지 복제한다고 주장하지 않습니다.

이 예의 조건

  • 공유와 copy는 별도 실행입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2공유 예제에서 report와 preview는 같은 가변 Entry 40→45를 본다. 별도 copy() 예제에서는 report의 40과 preview의 45가 다른 객체이며 점선은 생성 시 값 복사이다.
원 도표의 설명과 주석
구분내용
설명공유 예제에서 report와 preview는 같은 가변 Entry 40→45를 본다. 별도 copy() 예제에서는 report의 40과 preview의 45가 다른 객체이며 점선은 생성 시 값 복사이다.
전체 읽기 정보같은 게시글을 공유할 때와 복사한 게시글을 수정할 때 같은 게시글을 공유할 때와 복사한 게시글을 수정할 때 SharedEntrySideEffect는 두 소비자가 같은 MutableEntry를 보므로 미리보기 +5가 보고서에도 45로 보인다. SeparatedMutableEntries는 previewEntry를 새 객체로 복사해 저장 값 40과 미리보기 45를 분리한다. 복사한 객체를 수정하면 보고서의 원본이 유지됩니다 두 프로그램 모두 제목 "object" , 조회 수 40 에서 시작해 preview.applyPreviewBoost(5) 를 호출합니다. 아래 행은 서로 다른 실행입니다. 같은 미리보기 보정이 참조 공유 여부에 따라 달라지는 결과 원문 프로그램 두 소비자가 보는 대상 보정 뒤 실제 출력 SharedEntrySideEffect DailyReport 와 PreviewService 에 같은 entry 를 전달합니다. 한 MutableEntry 의 조회 수가 45 가 됩니다. stored=45 preview=45 SeparatedMutableEntries previewEntry = stored.copy() 가 새 MutableEntry 를 만듭니다. 보고서는 stored , 미리보기는 previewEntry 를 봅니다. stored=40 preview=45 SharedEntrySideEffect 두 소비자가 보는 대상: DailyReport 와 PreviewService 에 같은 entry 를 전달합니다. 한 MutableEntry 의 조회 수가 45 가 됩니다. 보정 뒤 실제 출력: stored=45 preview=45 SeparatedMutableEntries 두 소비자가 보는 대상: previewEntry = stored.copy() 가 새 MutableEntry 를 만듭니다. 보고서는 stored , 미리보기는 previewEntry 를 봅니다. 보정 뒤 실제 출력: stored=40 preview=45 보고서의 stored= 는 report.totalViewCount() 의 출력 라벨입니다. 이 예제에 파일 저장소나 별도 화면 객체는 없습니다. 복사본의 제목 문자열과 숫자 상태만 비교하며, 임의의 중첩 가변 객체까지 복제한다고 주장하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초기 값

두 실행의 report는 Entry 40입니다.

  • 공유 preview: 같은 참조
  • copy preview: 새 객체 40
  • copy가 새 저장 객체를 만듭니다.
preview 변경

preview의 조회 수를 45로 바꿉니다.

  • 공유 객체: 40→45
  • copy 객체: 40→45
  • 쓰기 표현식은 같아도 대상 객체가 다릅니다.
report 관찰

공유 report는 45, copy report는 40입니다.

  • 공유: report/preview 45
  • copy: report 40/preview45
  • 생성 시 값 복사가 앞 객체의 상태를 보존합니다.

참조 공유의 언어적 한계

자바는 객체 참조를 다른 변수에 대입하거나 여러 생성자에 전달하는 일을 허용합니다.

특정 클래스의 인스턴스가 공유되지 않도록 컴파일러에 일반 규칙을 선언할 수는 없습니다.

호출자가 같은 객체를 두 번 전달할 수도 있고 컬렉션이 동일 인스턴스를 여러 위치에 보관할 수도 있습니다.

따라서 방어 방향은 세 가지입니다.

  1. 공유해도 바뀌지 않도록 객체를 불변으로 만듭니다.
  2. 변경 가능한 객체의 소유자를 하나로 정하고 외부에는 읽기 전용 값만 노출합니다.
  3. 독립 변경이 필요한 경계에서 명시적으로 복사합니다.

무조건 복사하면 비용과 정체성 문제가 생길 수 있습니다.

회원, 주문처럼 한 실체의 최신 상태를 공유해야 하는 객체는 소유 서비스가 변경을 통제하는 편이 낫습니다.

날짜, 금액, 게시글 스냅숏처럼 값 의미가 강한 객체는 불변 설계가 자연스럽습니다.


Board의 변경 권한

가변 항목을 외부에 그대로 반환하지 않고, Board가 수정 명령을 받아 내부 상태를 관리할 수 있습니다.

조회 결과는 변경 메서드가 없는 불변 View로 변환합니다.

src/OwnedBoard.java
public final class OwnedBoard {
    public static void main(String[] args) {
        Board board = new Board();
        long id = board.add("references", 40);
        board.reviseViewCount(id, 55);

        EntryView[] snapshot = board.snapshot();
        for (EntryView entry : snapshot) System.out.println(entry);
        System.out.println("total=" + board.totalViewCount());
    }

    private record EntryView(long id, String title, int viewCount) { }

    private static final class MutableEntry {
        private final long id;
        private final String title;
        private int viewCount;

        MutableEntry(long id, String title, int viewCount) {
            this.id = id;
            this.title = title;
            this.viewCount = viewCount;
        }
    }

    private static final class Board {
        private final MutableEntry[] entries = new MutableEntry[10];
        private int size;
        private long nextId = 1;

        long add(String title, int viewCount) {
            validateViewCount(viewCount);
            long id = nextId++;
            if (size == entries.length) throw new IllegalStateException("full");
            entries[size++] = new MutableEntry(id, title, viewCount);
            return id;
        }

        void reviseViewCount(long id, int viewCount) {
            validateViewCount(viewCount);
            for (int index = 0; index < size; index++) {
                MutableEntry entry = entries[index];
                if (entry.id == id) {
                    entry.viewCount = viewCount;
                    return;
                }
            }
            throw new IllegalArgumentException("id=" + id);
        }

        EntryView[] snapshot() {
            EntryView[] result = new EntryView[size];
            for (int index = 0; index < size; index++) {
                MutableEntry entry = entries[index];
                result[index] = new EntryView(entry.id, entry.title, entry.viewCount);
            }
            return result;
        }

        int totalViewCount() {
            int total = 0;
            for (int index = 0; index < size; index++) {
                total += entries[index].viewCount;
            }
            return total;
        }

        private static void validateViewCount(int viewCount) {
            if (viewCount <= 0) throw new IllegalArgumentException("viewCount");
        }
    }
}
EntryView[id=1, title=references, viewCount=55]
total=55

Board의 조회 메서드는 내부 MutableEntry 참조 대신 새 EntryView를 반환합니다.

변경은 id와 새 값을 받는 Board 메서드에서 검증된 뒤 적용됩니다.

snapshot()은 현재 상태를 새 record 배열로 복제하므로 이후 Board 상태가 바뀌어도 이미 받은 조회 결과는 흔들리지 않습니다.

새 배열뿐 아니라 각 슬롯의 EntryView도 새로 만듭니다. id·조회 수는 숫자 값으로 복사하고, 제목은 불변 String을 공유합니다. 따라서 반환 배열의 슬롯에 다른 EntryView를 대입해도 Board의 entries 슬롯은 교체되지 않습니다.

스냅숏을 받는 시점 바꾸기

위 main에서 add 직후, reviseViewCount를 호출하기 전에 첫 snapshot을 받아 두세요. 같은 id를 55로 수정한 뒤 두 번째 snapshot을 받고, 두 번째 배열의 첫 슬롯을 조회 수 999인 다른 EntryView로 교체합니다. 첫 결과, 수정 직후 결과, 다시 조회한 Board의 조회 수를 차례로 예상한 뒤 실행해 보세요.

시점과 배열 슬롯을 나누어 확인하기

첫 스냅숏은 40, 수정 직후 두 번째 스냅숏은 55입니다. 외부 배열 슬롯을 교체해도 다시 조회한 내부 값은 55입니다. 40을 유지한 첫 결과는 실시간 연결이 아니라 그 시점의 필드를 복사한 조회 값입니다.

이 Board의 검증 범위도 코드대로 읽습니다. add는 제목을 검사하지 않으며, 용량 검사보다 앞에서 nextId를 증가시킵니다. 이미 10개를 채운 뒤 양수 조회 수로 add를 시도하면 full 예외가 나지만 다음 id 카운터는 진행합니다. 내부 객체를 밖으로 노출하지 않는 설계가 모든 실패에서 모든 필드를 되돌린다는 뜻은 아닙니다.

또한 totalViewCount의 int 합은 큰 양수 값에서도 넘칠 수 있습니다. 조회 수가 각각 Integer.MAX_VALUE인 두 항목이면 원래 코드의 합계는 -2가 됩니다. 그 입력도 지원하려면 Board 안에 다음과 같이 long으로 누적하는 별도 메서드를 두고 호출부도 그 결과 타입을 사용합니다.

Board 내부의 합계 범위 보완
long totalViewCountLong() {
    long total = 0;
    for (int index = 0; index < size; index++) {
        total += entries[index].viewCount;
    }
    return total;
}

같은 두 항목의 long 합계는 4294967294입니다. 현재 최대 항목 수 10과 각 int 양수 범위에서 이 합계는 long 범위 안에 있습니다.

Board의 변경 명령과 새 EntryView 조회값을 구별한다

Board.add 40 뒤 revise 55가 내부 MutableEntry를 바꾸고 snapshot은 새 배열/EntryView 값 55를 내보냅니다. 명령·내부 변경·조회 사본의 실제 전환이며 언어 차원의 소유권 보장으로 확대하지 않습니다.

Board의 변경 명령과 새 EntryView 조회값을 구별한다

OwnedBoard의 main은 references 게시글 40을 추가하고 id=1의 조회 수를 55로 수정한 뒤 새 EntryView 배열을 받는다. Board의 조회 메서드는 내부 MutableEntry를 반환하지 않으며 반환 배열과 각 조회값의 변경 가능성은 다르다.

변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다

OwnedBoard의 실제 main 호출입니다. Board의 조회 메서드는 내부 MutableEntry 대신 새 EntryView를 반환합니다.

변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다 main이add40뒤id1을55로수정한다. Board내부MutableEntry는references55이다. snapshot은새배열과EntryView로필드를복사해내보내고total은55이다. 실선은명령또는계산,점선은조회사본복사로서외부배열슬롯교체가내부entries를교체하지않는다. main의 명령 add: references / 40 revise: id1 / 55 Board 내부 MutableEntry id1 / references viewCount = 55 조회 사본 새 배열 + EntryView id1 / references / 55 합계 결과 total = 55 snapshot: 필드 값을 복사 영역은 이 코드의 내부 보관 · 언어가 보장하는 별도 소유권 기능 아님
변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다 main이add40뒤id1을55로수정한다. Board내부MutableEntry는references55이다. snapshot은새배열과EntryView로필드를복사해내보내고total은55이다. 실선은명령또는계산,점선은조회사본복사로서외부배열슬롯교체가내부entries를교체하지않는다. main의 명령 add: references / 40 revise: id1 / 55 Board의 내부 보관 MutableEntry id1 / references viewCount = 55 조회 사본 새 배열 + EntryView id1 / references / 55 합계 결과 total = 55 snapshot: 값 복사 영역은 이 코드의 내부 보관 외부 슬롯 교체 ≠ 내부 변경
OwnedBoard의 실제 명령과 조회 경계
main의 순서Board가 하는 일호출자에게 주는 값
add("references", 40)조회 수 검증 뒤 id=1인 MutableEntry를 보관합니다.long id 값 1
reviseViewCount(id, 55)조회 수 검증 뒤 같은 id의 내부 항목을 찾아 viewCount=55로 수정합니다.void · 반환값 없음
snapshot()새 EntryView[]와 새 EntryView를 만들어 현재 필드값을 담습니다.id=1, title="references", viewCount=55인 조회값
totalViewCount()현재 보관 항목의 조회 수를 합합니다.55
  1. add("references", 40)
    Board가 하는 일: 조회 수 검증 뒤 id=1인 MutableEntry를 보관합니다.
    호출자에게 주는 값: long id 값 1
  2. reviseViewCount(id, 55)
    Board가 하는 일: 조회 수 검증 뒤 같은 id의 내부 항목을 찾아 viewCount=55로 수정합니다.
    호출자에게 주는 값: void · 반환값 없음
  3. snapshot()
    Board가 하는 일: 새 EntryView[]와 새 EntryView를 만들어 현재 필드값을 담습니다.
    호출자에게 주는 값: id=1, title="references", viewCount=55인 조회값
  4. totalViewCount()
    Board가 하는 일: 현재 보관 항목의 조회 수를 합합니다.
    호출자에게 주는 값: 55

이후 Board를 수정해도 이미 만든 EntryView의 값은 바뀌지 않습니다. 그러나 반환 배열의 슬롯은 호출자가 바꿀 수 있습니다. 새 배열의 슬롯 변경은 Board의 보관 배열을 바꾸지 않습니다.

main의 출력은 EntryView[id=1, title=references, viewCount=55]와 total=55입니다. 여기서의 소유권은 Board가 제공하는 메서드 경계이며, 같은 최상위 클래스 안의 코드까지 접근을 금지하는 별도의 언어 기능을 뜻하지 않습니다.

이 예의 조건

  • Board의 명령·조회 규칙이며 언어 자체 별도 소유권 기능이 아닙니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2main이 add 40 뒤 id 1을 55로 수정한다. Board 내부 MutableEntry는 references 55이다. snapshot은 새 배열과 EntryView로 필드를 복사해 내보내고 total은 55이다. 실선은 명령 또는 계산이며 점선은 조회 사본 복사로서 외부 배열 슬롯 교체가 내부 entries를 교체하지 않는다.
4add: references
540
6revise: id 1
10id 1
11references
15id 1
16references
원 도표의 설명과 주석
구분내용
설명main이 add 40 뒤 id 1을 55로 수정한다. Board 내부 MutableEntry는 references 55이다. snapshot은 새 배열과 EntryView로 필드를 복사해 내보내고 total은 55이다. 실선은 명령 또는 계산이며 점선은 조회 사본 복사로서 외부 배열 슬롯 교체가 내부 entries를 교체하지 않는다.
전체 읽기 정보Board의 변경 명령과 새 EntryView 조회값을 구별한다 Board의 변경 명령과 새 EntryView 조회값을 구별한다 OwnedBoard의 main은 references 게시글 40을 추가하고 id=1의 조회 수를 55로 수정한 뒤 새 EntryView 배열을 받는다. Board의 조회 메서드는 내부 MutableEntry를 반환하지 않으며 반환 배열과 각 조회값의 변경 가능성은 다르다. 변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다 OwnedBoard 의 실제 main 호출입니다. Board 의 조회 메서드는 내부 MutableEntry 대신 새 EntryView 를 반환합니다. OwnedBoard의 실제 명령과 조회 경계 main의 순서 Board가 하는 일 호출자에게 주는 값 add("references", 40) 조회 수 검증 뒤 id=1 인 MutableEntry 를 보관합니다. long id 값 1 reviseViewCount(id, 55) 조회 수 검증 뒤 같은 id 의 내부 항목을 찾아 viewCount=55 로 수정합니다. void · 반환값 없음 snapshot() 새 EntryView[] 와 새 EntryView 를 만들어 현재 필드값을 담습니다. id=1 , title="references" , viewCount=55 인 조회값 totalViewCount() 현재 보관 항목의 조회 수를 합합니다. 55 add("references", 40) Board가 하는 일: 조회 수 검증 뒤 id=1 인 MutableEntry 를 보관합니다. 호출자에게 주는 값: long id 값 1 reviseViewCount(id, 55) Board가 하는 일: 조회 수 검증 뒤 같은 id 의 내부 항목을 찾아 viewCount=55 로 수정합니다. 호출자에게 주는 값: void · 반환값 없음 snapshot() Board가 하는 일: 새 EntryView[] 와 새 EntryView 를 만들어 현재 필드값을 담습니다. 호출자에게 주는 값: id=1 , title="references" , viewCount=55 인 조회값 totalViewCount() Board가 하는 일: 현재 보관 항목의 조회 수를 합합니다. 호출자에게 주는 값: 55 이후 Board 를 수정해도 이미 만든 EntryView 의 값은 바뀌지 않습니다. 그러나 반환 배열의 슬롯은 호출자가 바꿀 수 있습니다. 새 배열의 슬롯 변경은 Board 의 보관 배열을 바꾸지 않습니다. main 의 출력은 EntryView[id=1, title=references, viewCount=55] 와 total=55 입니다. 여기서의 소유권은 Board 가 제공하는 메서드 경계이며, 같은 최상위 클래스 안의 코드까지 접근을 금지하는 별도의 언어 기능을 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
명령으로 등록

main이 references 40을 Board에 add합니다.

  • 내부: MutableEntry id 1
  • viewCount: 40
  • 내부 가변 상태의 변경 입구는 Board 명령입니다.
승인된 변경

revise가 id 1을 55로 바꿉니다.

  • 내부 count: 55
  • 계산 total: 55
  • 조회 결과를 직접 변경하는 대신 명령으로 수정합니다.
조회 복사

snapshot이 새 배열과 EntryView로 필드를 복사합니다.

  • 사본: id 1,references 55
  • 외부 슬롯 교체: 내부 entries 교체 안 함
  • 조회 값이 내부 가변 참조를 노출하지 않습니다.

Board의 변경 명령과 새 EntryView 조회값을 구별한다

Board.add 40 뒤 revise 55가 내부 MutableEntry를 바꾸고 snapshot은 새 배열/EntryView 값 55를 내보냅니다. 명령·내부 변경·조회 사본의 실제 전환이며 언어 차원의 소유권 보장으로 확대하지 않습니다.

Board의 변경 명령과 새 EntryView 조회값을 구별한다

OwnedBoard의 main은 references 게시글 40을 추가하고 id=1의 조회 수를 55로 수정한 뒤 새 EntryView 배열을 받는다. Board의 조회 메서드는 내부 MutableEntry를 반환하지 않으며 반환 배열과 각 조회값의 변경 가능성은 다르다.

변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다

OwnedBoard의 실제 main 호출입니다. Board의 조회 메서드는 내부 MutableEntry 대신 새 EntryView를 반환합니다.

변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다 main이add40뒤id1을55로수정한다. Board내부MutableEntry는references55이다. snapshot은새배열과EntryView로필드를복사해내보내고total은55이다. 실선은명령또는계산,점선은조회사본복사로서외부배열슬롯교체가내부entries를교체하지않는다. main의 명령 add: references / 40 revise: id1 / 55 Board 내부 MutableEntry id1 / references viewCount = 55 조회 사본 새 배열 + EntryView id1 / references / 55 합계 결과 total = 55 snapshot: 필드 값을 복사 영역은 이 코드의 내부 보관 · 언어가 보장하는 별도 소유권 기능 아님
변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다 main이add40뒤id1을55로수정한다. Board내부MutableEntry는references55이다. snapshot은새배열과EntryView로필드를복사해내보내고total은55이다. 실선은명령또는계산,점선은조회사본복사로서외부배열슬롯교체가내부entries를교체하지않는다. main의 명령 add: references / 40 revise: id1 / 55 Board의 내부 보관 MutableEntry id1 / references viewCount = 55 조회 사본 새 배열 + EntryView id1 / references / 55 합계 결과 total = 55 snapshot: 값 복사 영역은 이 코드의 내부 보관 외부 슬롯 교체 ≠ 내부 변경
OwnedBoard의 실제 명령과 조회 경계
main의 순서Board가 하는 일호출자에게 주는 값
add("references", 40)조회 수 검증 뒤 id=1인 MutableEntry를 보관합니다.long id 값 1
reviseViewCount(id, 55)조회 수 검증 뒤 같은 id의 내부 항목을 찾아 viewCount=55로 수정합니다.void · 반환값 없음
snapshot()새 EntryView[]와 새 EntryView를 만들어 현재 필드값을 담습니다.id=1, title="references", viewCount=55인 조회값
totalViewCount()현재 보관 항목의 조회 수를 합합니다.55
  1. add("references", 40)
    Board가 하는 일: 조회 수 검증 뒤 id=1인 MutableEntry를 보관합니다.
    호출자에게 주는 값: long id 값 1
  2. reviseViewCount(id, 55)
    Board가 하는 일: 조회 수 검증 뒤 같은 id의 내부 항목을 찾아 viewCount=55로 수정합니다.
    호출자에게 주는 값: void · 반환값 없음
  3. snapshot()
    Board가 하는 일: 새 EntryView[]와 새 EntryView를 만들어 현재 필드값을 담습니다.
    호출자에게 주는 값: id=1, title="references", viewCount=55인 조회값
  4. totalViewCount()
    Board가 하는 일: 현재 보관 항목의 조회 수를 합합니다.
    호출자에게 주는 값: 55

이후 Board를 수정해도 이미 만든 EntryView의 값은 바뀌지 않습니다. 그러나 반환 배열의 슬롯은 호출자가 바꿀 수 있습니다. 새 배열의 슬롯 변경은 Board의 보관 배열을 바꾸지 않습니다.

main의 출력은 EntryView[id=1, title=references, viewCount=55]와 total=55입니다. 여기서의 소유권은 Board가 제공하는 메서드 경계이며, 같은 최상위 클래스 안의 코드까지 접근을 금지하는 별도의 언어 기능을 뜻하지 않습니다.

이 예의 조건

  • Board의 명령·조회 규칙이며 언어 자체 별도 소유권 기능이 아닙니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2main이 add 40 뒤 id 1을 55로 수정한다. Board 내부 MutableEntry는 references 55이다. snapshot은 새 배열과 EntryView로 필드를 복사해 내보내고 total은 55이다. 실선은 명령 또는 계산이며 점선은 조회 사본 복사로서 외부 배열 슬롯 교체가 내부 entries를 교체하지 않는다.
4add: references
540
6revise: id 1
10id 1
11references
15id 1
16references
원 도표의 설명과 주석
구분내용
설명main이 add 40 뒤 id 1을 55로 수정한다. Board 내부 MutableEntry는 references 55이다. snapshot은 새 배열과 EntryView로 필드를 복사해 내보내고 total은 55이다. 실선은 명령 또는 계산이며 점선은 조회 사본 복사로서 외부 배열 슬롯 교체가 내부 entries를 교체하지 않는다.
전체 읽기 정보Board의 변경 명령과 새 EntryView 조회값을 구별한다 Board의 변경 명령과 새 EntryView 조회값을 구별한다 OwnedBoard의 main은 references 게시글 40을 추가하고 id=1의 조회 수를 55로 수정한 뒤 새 EntryView 배열을 받는다. Board의 조회 메서드는 내부 MutableEntry를 반환하지 않으며 반환 배열과 각 조회값의 변경 가능성은 다르다. 변경은 Board가 승인하고 조회는 새 EntryView로 돌려줍니다 OwnedBoard 의 실제 main 호출입니다. Board 의 조회 메서드는 내부 MutableEntry 대신 새 EntryView 를 반환합니다. OwnedBoard의 실제 명령과 조회 경계 main의 순서 Board가 하는 일 호출자에게 주는 값 add("references", 40) 조회 수 검증 뒤 id=1 인 MutableEntry 를 보관합니다. long id 값 1 reviseViewCount(id, 55) 조회 수 검증 뒤 같은 id 의 내부 항목을 찾아 viewCount=55 로 수정합니다. void · 반환값 없음 snapshot() 새 EntryView[] 와 새 EntryView 를 만들어 현재 필드값을 담습니다. id=1 , title="references" , viewCount=55 인 조회값 totalViewCount() 현재 보관 항목의 조회 수를 합합니다. 55 add("references", 40) Board가 하는 일: 조회 수 검증 뒤 id=1 인 MutableEntry 를 보관합니다. 호출자에게 주는 값: long id 값 1 reviseViewCount(id, 55) Board가 하는 일: 조회 수 검증 뒤 같은 id 의 내부 항목을 찾아 viewCount=55 로 수정합니다. 호출자에게 주는 값: void · 반환값 없음 snapshot() Board가 하는 일: 새 EntryView[] 와 새 EntryView 를 만들어 현재 필드값을 담습니다. 호출자에게 주는 값: id=1 , title="references" , viewCount=55 인 조회값 totalViewCount() Board가 하는 일: 현재 보관 항목의 조회 수를 합합니다. 호출자에게 주는 값: 55 이후 Board 를 수정해도 이미 만든 EntryView 의 값은 바뀌지 않습니다. 그러나 반환 배열의 슬롯은 호출자가 바꿀 수 있습니다. 새 배열의 슬롯 변경은 Board 의 보관 배열을 바꾸지 않습니다. main 의 출력은 EntryView[id=1, title=references, viewCount=55] 와 total=55 입니다. 여기서의 소유권은 Board 가 제공하는 메서드 경계이며, 같은 최상위 클래스 안의 코드까지 접근을 금지하는 별도의 언어 기능을 뜻하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
명령으로 등록

main이 references 40을 Board에 add합니다.

  • 내부: MutableEntry id 1
  • viewCount: 40
  • 내부 가변 상태의 변경 입구는 Board 명령입니다.
승인된 변경

revise가 id 1을 55로 바꿉니다.

  • 내부 count: 55
  • 계산 total: 55
  • 조회 결과를 직접 변경하는 대신 명령으로 수정합니다.
조회 복사

snapshot이 새 배열과 EntryView로 필드를 복사합니다.

  • 사본: id 1,references 55
  • 외부 슬롯 교체: 내부 entries 교체 안 함
  • 조회 값이 내부 가변 참조를 노출하지 않습니다.

공유 참조 해결 기준

사이드 이펙트가 보인다고 모든 클래스를 불변으로 바꾸지는 않습니다.

변화의 목적과 관찰 범위를 먼저 적습니다.

  • 같은 실체의 최신 상태를 여러 기능이 봐야 하는가?
  • 변경 명령을 한 소유 객체로 모을 수 있는가?
  • 과거 상태 보존이 업무 요구인가?
  • 내부에 배열·목록 같은 가변 자원이 있는가?
  • 복사 비용보다 예측 가능성이 더 중요한가?

스냅숏과 설정 값은 불변 값으로, 수명 동안 계속 변하는 집계는 단일 소유자로, 임시 편집본은 명시적 복사로 다루는 식으로 조합할 수 있습니다.

공유 여부가 아니라 공유된 객체에 어떤 변경 권한이 열려 있는지가 판단의 중심입니다.


연습 문제

생성자에 전달된 String[]과 외부 반환값을 바꿔도 내부 태그가 바뀌지 않는 BoardNote를 만드세요.

생성 시점과 조회 시점 모두 배열을 복사해야 합니다.

해설 보기
src/DefensiveTagCopyExercise.java
public final class DefensiveTagCopyExercise {
    public static void main(String[] args) {
        String[] source = {"java", "object"};
        BoardNote note = new BoardNote("sharing", source);

        source[0] = "leak";
        String[] view = note.tags();
        view[1] = "changed";

        String[] safe = note.tags();
        System.out.println("source=" + source[0] + "," + source[1]);
        System.out.println("note=" + safe[0] + "," + safe[1]);
    }

        private static final class BoardNote {
        private final String text;
        private final String[] tags;

        BoardNote(String text, String[] tags) {
            this.text = text;
            this.tags = tags.clone();
        }

        String[] tags() {
            return tags.clone();
        }
    }
}
source=leak,object
note=java,object

생성자는 입력 배열을 복사하고 tags()도 새 배열을 반환합니다.

문자열 자체는 불변이므로 배열 구조를 양쪽 경계에서 복사하면 외부 대입이 내부 태그에 닿지 않습니다.

태그 배열은 생성과 조회 양쪽에서 복사한다

생성 시 source를 복사하고 조회 시 별도 view를 만들어 각각 외부 대입을 해도 내부 java/object와 다음 safe가 유지됩니다. 두 경계에서 복사본과 내부 값의 실제 상태를 추적합니다.

태그 배열은 생성과 조회 양쪽에서 복사한다

DefensiveTagCopyExercise에서 생성자의 clone은 source 변경이 내부 배열에 전파되는 것을 막고, tags의 clone은 반환 배열 변경을 내부와 분리한다. 최종 source는 leak,object지만 새 조회 safe는 java,object다.

입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다

DefensiveTagCopyExercise의 BoardNote는 생성자와 tags()에서 각각 배열을 clone()합니다. 문자열 원소는 공유하지만 배열의 슬롯은 분리됩니다.

입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다 생성자clone으로source와내부tags배열을분리하고각tags조회도새배열을만든다. 최종sourceleak/object,내부java/object,viewjava/changed,safejava/object이다. 점선은복사당시의경로,현재참조별칭이아니며String요소는불변으로공유한다. source [0] leak [1] object 내부 tags [0] java [1] object view [0] java [1] changed safe [0] java [1] object 생성 시 입력 복사 첫 조회: 배열 복사 둘째 조회: 배열 복사 점선은 생성·조회 시의 복사 · 현재 같은 배열을 공유한다는 뜻 아님
입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다 생성자clone으로source와내부tags배열을분리하고각tags조회도새배열을만든다. 최종sourceleak/object,내부java/object,viewjava/changed,safejava/object이다. 점선은복사당시의경로,현재참조별칭이아니며String요소는불변으로공유한다. 클래스 내부 source [0] leak [1] object 내부 tags [0] java [1] object view [0] java [1] changed safe [0] java [1] object 생성 시 입력 복사 첫 조회: 복사 둘째 조회: 복사 점선: 새 배열을 만든 복사 슬롯의 String은 불변 공유
입력 배열과 반환 배열의 수정이 서로 다른 복사 경계를 만나는 과정
원문의 시점바꾸거나 만드는 배열내부 태그와의 관계
new BoardNote 생성source의 "java", "object"를 새 내부 배열로 복사합니다.입력 배열과 내부 배열은 서로 다릅니다.
source[0] = "leak"호출자 입력 배열은 "leak", "object"가 됩니다.내부는 "java", "object" 그대로입니다.
view = note.tags()

view[1] = "changed"

새 반환 배열 view만 "java", "changed"가 됩니다.내부는 "java", "object" 그대로입니다.
safe = note.tags()내부 태그를 또 다른 새 배열 safe에 복사합니다.safe에서 "java", "object"를 읽습니다.
  1. new BoardNote 생성
    바꾸거나 만드는 배열: source의 "java", "object"를 새 내부 배열로 복사합니다.
    내부 태그와의 관계: 입력 배열과 내부 배열은 서로 다릅니다.
  2. source[0] = "leak"
    바꾸거나 만드는 배열: 호출자 입력 배열은 "leak", "object"가 됩니다.
    내부 태그와의 관계: 내부는 "java", "object" 그대로입니다.
  3. view = note.tags() 뒤 view[1] = "changed"
    바꾸거나 만드는 배열: 새 반환 배열 view만 "java", "changed"가 됩니다.
    내부 태그와의 관계: 내부는 "java", "object" 그대로입니다.
  4. safe = note.tags()
    바꾸거나 만드는 배열: 내부 태그를 또 다른 새 배열 safe에 복사합니다.
    내부 태그와의 관계: safe에서 "java", "object"를 읽습니다.

원문 main이 출력하는 두 줄입니다.

source=leak,object
note=java,object

view의 두 슬롯이 "java", "changed"인 상태는 대입문에서 읽은 설명이며 별도의 출력 줄이 아닙니다. 배열 원소인 문자열은 불변입니다. 이 clone()이 임의의 가변 원소까지 깊은 복사하는 것은 아닙니다.

이 예의 조건

  • String 요소는 불변이며 입력/출력 배열 구조를 복사한다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2생성자의 clone으로 source와 내부 tags 배열을 분리하고 각 tags 조회도 새 배열을 만든다. 최종 source는 leak/object, 내부는 java/object, view는 java/changed, safe는 java/object이다. 점선은 복사 당시의 경로이며 현재 참조 별칭이 아니고 String 요소는 불변으로 공유한다.
원 도표의 설명과 주석
구분내용
설명생성자의 clone으로 source와 내부 tags 배열을 분리하고 각 tags 조회도 새 배열을 만든다. 최종 source는 leak/object, 내부는 java/object, view는 java/changed, safe는 java/object이다. 점선은 복사 당시의 경로이며 현재 참조 별칭이 아니고 String 요소는 불변으로 공유한다.
전체 읽기 정보태그 배열은 생성과 조회 양쪽에서 복사한다 태그 배열은 생성과 조회 양쪽에서 복사한다 DefensiveTagCopyExercise에서 생성자의 clone은 source 변경이 내부 배열에 전파되는 것을 막고, tags의 clone은 반환 배열 변경을 내부와 분리한다. 최종 source는 leak,object지만 새 조회 safe는 java,object다. 입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다 DefensiveTagCopyExercise 의 BoardNote 는 생성자와 tags() 에서 각각 배열을 clone() 합니다. 문자열 원소는 공유하지만 배열의 슬롯은 분리됩니다. 입력 배열과 반환 배열의 수정이 서로 다른 복사 경계를 만나는 과정 원문의 시점 바꾸거나 만드는 배열 내부 태그와의 관계 new BoardNote 생성 source 의 "java", "object" 를 새 내부 배열로 복사합니다. 입력 배열과 내부 배열은 서로 다릅니다. source[0] = "leak" 호출자 입력 배열은 "leak", "object" 가 됩니다. 내부는 "java", "object" 그대로입니다. view = note.tags() view[1] = "changed" 새 반환 배열 view 만 "java", "changed" 가 됩니다. 내부는 "java", "object" 그대로입니다. safe = note.tags() 내부 태그를 또 다른 새 배열 safe 에 복사합니다. safe 에서 "java", "object" 를 읽습니다. new BoardNote 생성 바꾸거나 만드는 배열: source 의 "java", "object" 를 새 내부 배열로 복사합니다. 내부 태그와의 관계: 입력 배열과 내부 배열은 서로 다릅니다. source[0] = "leak" 바꾸거나 만드는 배열: 호출자 입력 배열은 "leak", "object" 가 됩니다. 내부 태그와의 관계: 내부는 "java", "object" 그대로입니다. view = note.tags() 뒤 view[1] = "changed" 바꾸거나 만드는 배열: 새 반환 배열 view 만 "java", "changed" 가 됩니다. 내부 태그와의 관계: 내부는 "java", "object" 그대로입니다. safe = note.tags() 바꾸거나 만드는 배열: 내부 태그를 또 다른 새 배열 safe 에 복사합니다. 내부 태그와의 관계: safe 에서 "java", "object" 를 읽습니다. 원문 main 이 출력하는 두 줄입니다. source=leak,object note=java,object view 의 두 슬롯이 "java", "changed" 인 상태는 대입문에서 읽은 설명이며 별도의 출력 줄이 아닙니다. 배열 원소인 문자열은 불변입니다. 이 clone() 이 임의의 가변 원소까지 깊은 복사하는 것은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 복사

java/object인 source를 내부 tags로 복사합니다.

  • source: [java,object]
  • tags: [java,object]
  • 외부 배열 대입이 내부 슬롯에 닿지 않는 경계를 만든다.
입력 변경

source[0]을 leak로 바꿉니다.

  • source: [leak,object]
  • tags: [java,object] 유지
  • 생성 이후 외부 입력 변경은 분리된다.
조회 복사본 변경

tags()가 반환한 view[1]을 changed로 바꿉니다.

  • view: [java,changed]
  • tags: [java,object] 유지
  • 반환 경계에서도 별도 배열을 제공한다.
다음 조회

safe가 내부 값의 새 배열을 받습니다.

  • safe: [java,object]
  • source: [leak,object]
  • 앞선 두 외부 대입이 내부 보관값을 바꾸지 않았다.

태그 배열은 생성과 조회 양쪽에서 복사한다

생성 시 source를 복사하고 조회 시 별도 view를 만들어 각각 외부 대입을 해도 내부 java/object와 다음 safe가 유지됩니다. 두 경계에서 복사본과 내부 값의 실제 상태를 추적합니다.

태그 배열은 생성과 조회 양쪽에서 복사한다

DefensiveTagCopyExercise에서 생성자의 clone은 source 변경이 내부 배열에 전파되는 것을 막고, tags의 clone은 반환 배열 변경을 내부와 분리한다. 최종 source는 leak,object지만 새 조회 safe는 java,object다.

입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다

DefensiveTagCopyExercise의 BoardNote는 생성자와 tags()에서 각각 배열을 clone()합니다. 문자열 원소는 공유하지만 배열의 슬롯은 분리됩니다.

입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다 생성자clone으로source와내부tags배열을분리하고각tags조회도새배열을만든다. 최종sourceleak/object,내부java/object,viewjava/changed,safejava/object이다. 점선은복사당시의경로,현재참조별칭이아니며String요소는불변으로공유한다. source [0] leak [1] object 내부 tags [0] java [1] object view [0] java [1] changed safe [0] java [1] object 생성 시 입력 복사 첫 조회: 배열 복사 둘째 조회: 배열 복사 점선은 생성·조회 시의 복사 · 현재 같은 배열을 공유한다는 뜻 아님
입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다 생성자clone으로source와내부tags배열을분리하고각tags조회도새배열을만든다. 최종sourceleak/object,내부java/object,viewjava/changed,safejava/object이다. 점선은복사당시의경로,현재참조별칭이아니며String요소는불변으로공유한다. 클래스 내부 source [0] leak [1] object 내부 tags [0] java [1] object view [0] java [1] changed safe [0] java [1] object 생성 시 입력 복사 첫 조회: 복사 둘째 조회: 복사 점선: 새 배열을 만든 복사 슬롯의 String은 불변 공유
입력 배열과 반환 배열의 수정이 서로 다른 복사 경계를 만나는 과정
원문의 시점바꾸거나 만드는 배열내부 태그와의 관계
new BoardNote 생성source의 "java", "object"를 새 내부 배열로 복사합니다.입력 배열과 내부 배열은 서로 다릅니다.
source[0] = "leak"호출자 입력 배열은 "leak", "object"가 됩니다.내부는 "java", "object" 그대로입니다.
view = note.tags()

view[1] = "changed"

새 반환 배열 view만 "java", "changed"가 됩니다.내부는 "java", "object" 그대로입니다.
safe = note.tags()내부 태그를 또 다른 새 배열 safe에 복사합니다.safe에서 "java", "object"를 읽습니다.
  1. new BoardNote 생성
    바꾸거나 만드는 배열: source의 "java", "object"를 새 내부 배열로 복사합니다.
    내부 태그와의 관계: 입력 배열과 내부 배열은 서로 다릅니다.
  2. source[0] = "leak"
    바꾸거나 만드는 배열: 호출자 입력 배열은 "leak", "object"가 됩니다.
    내부 태그와의 관계: 내부는 "java", "object" 그대로입니다.
  3. view = note.tags() 뒤 view[1] = "changed"
    바꾸거나 만드는 배열: 새 반환 배열 view만 "java", "changed"가 됩니다.
    내부 태그와의 관계: 내부는 "java", "object" 그대로입니다.
  4. safe = note.tags()
    바꾸거나 만드는 배열: 내부 태그를 또 다른 새 배열 safe에 복사합니다.
    내부 태그와의 관계: safe에서 "java", "object"를 읽습니다.

원문 main이 출력하는 두 줄입니다.

source=leak,object
note=java,object

view의 두 슬롯이 "java", "changed"인 상태는 대입문에서 읽은 설명이며 별도의 출력 줄이 아닙니다. 배열 원소인 문자열은 불변입니다. 이 clone()이 임의의 가변 원소까지 깊은 복사하는 것은 아닙니다.

이 예의 조건

  • String 요소는 불변이며 입력/출력 배열 구조를 복사한다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2생성자의 clone으로 source와 내부 tags 배열을 분리하고 각 tags 조회도 새 배열을 만든다. 최종 source는 leak/object, 내부는 java/object, view는 java/changed, safe는 java/object이다. 점선은 복사 당시의 경로이며 현재 참조 별칭이 아니고 String 요소는 불변으로 공유한다.
원 도표의 설명과 주석
구분내용
설명생성자의 clone으로 source와 내부 tags 배열을 분리하고 각 tags 조회도 새 배열을 만든다. 최종 source는 leak/object, 내부는 java/object, view는 java/changed, safe는 java/object이다. 점선은 복사 당시의 경로이며 현재 참조 별칭이 아니고 String 요소는 불변으로 공유한다.
전체 읽기 정보태그 배열은 생성과 조회 양쪽에서 복사한다 태그 배열은 생성과 조회 양쪽에서 복사한다 DefensiveTagCopyExercise에서 생성자의 clone은 source 변경이 내부 배열에 전파되는 것을 막고, tags의 clone은 반환 배열 변경을 내부와 분리한다. 최종 source는 leak,object지만 새 조회 safe는 java,object다. 입력과 반환 배열을 모두 복사해 내부 태그를 지킵니다 DefensiveTagCopyExercise 의 BoardNote 는 생성자와 tags() 에서 각각 배열을 clone() 합니다. 문자열 원소는 공유하지만 배열의 슬롯은 분리됩니다. 입력 배열과 반환 배열의 수정이 서로 다른 복사 경계를 만나는 과정 원문의 시점 바꾸거나 만드는 배열 내부 태그와의 관계 new BoardNote 생성 source 의 "java", "object" 를 새 내부 배열로 복사합니다. 입력 배열과 내부 배열은 서로 다릅니다. source[0] = "leak" 호출자 입력 배열은 "leak", "object" 가 됩니다. 내부는 "java", "object" 그대로입니다. view = note.tags() view[1] = "changed" 새 반환 배열 view 만 "java", "changed" 가 됩니다. 내부는 "java", "object" 그대로입니다. safe = note.tags() 내부 태그를 또 다른 새 배열 safe 에 복사합니다. safe 에서 "java", "object" 를 읽습니다. new BoardNote 생성 바꾸거나 만드는 배열: source 의 "java", "object" 를 새 내부 배열로 복사합니다. 내부 태그와의 관계: 입력 배열과 내부 배열은 서로 다릅니다. source[0] = "leak" 바꾸거나 만드는 배열: 호출자 입력 배열은 "leak", "object" 가 됩니다. 내부 태그와의 관계: 내부는 "java", "object" 그대로입니다. view = note.tags() 뒤 view[1] = "changed" 바꾸거나 만드는 배열: 새 반환 배열 view 만 "java", "changed" 가 됩니다. 내부 태그와의 관계: 내부는 "java", "object" 그대로입니다. safe = note.tags() 바꾸거나 만드는 배열: 내부 태그를 또 다른 새 배열 safe 에 복사합니다. 내부 태그와의 관계: safe 에서 "java", "object" 를 읽습니다. 원문 main 이 출력하는 두 줄입니다. source=leak,object note=java,object view 의 두 슬롯이 "java", "changed" 인 상태는 대입문에서 읽은 설명이며 별도의 출력 줄이 아닙니다. 배열 원소인 문자열은 불변입니다. 이 clone() 이 임의의 가변 원소까지 깊은 복사하는 것은 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
입력 복사

java/object인 source를 내부 tags로 복사합니다.

  • source: [java,object]
  • tags: [java,object]
  • 외부 배열 대입이 내부 슬롯에 닿지 않는 경계를 만든다.
입력 변경

source[0]을 leak로 바꿉니다.

  • source: [leak,object]
  • tags: [java,object] 유지
  • 생성 이후 외부 입력 변경은 분리된다.
조회 복사본 변경

tags()가 반환한 view[1]을 changed로 바꿉니다.

  • view: [java,changed]
  • tags: [java,object] 유지
  • 반환 경계에서도 별도 배열을 제공한다.
다음 조회

safe가 내부 값의 새 배열을 받습니다.

  • safe: [java,object]
  • source: [leak,object]
  • 앞선 두 외부 대입이 내부 보관값을 바꾸지 않았다.

요소가 가변 객체라면 요소도 별도로 복사해야 합니다.