지역 클래스와 변수 캡처
캡처된 지역 변수 변경의 컴파일 실패를 재현하고 스택·힙 수명 차이, 사실상 final 규칙, 바깥 필드 접근을 게시글 작업으로 이해합니다.
지역 클래스는 메서드 블록 안에 선언되며 그 범위의 매개변수와 지역 값을 사용할 수 있습니다.
하지만 메서드가 끝나면 스택 프레임의 지역 변수는 사라지고 지역 객체는 힙에서 더 오래 살아남을 수 있습니다.
자바는 필요한 지역 값을 객체에 복사해 캡처하고, 원본과 복사본이 어긋나지 않도록 값 변경을 제한합니다.
캡처 변수의 재대입 제한
prefix를 지역 클래스가 사용한 뒤 다른 문자열로 바꾸려 합니다.
캡처 대상은 final이거나 사실상 final이어야 하므로 실패합니다.
public final class CapturedLocalMutationFailure {
public static void main(String[] args) {
String prefix = "board";
class Printer {
void print() {
System.out.println(prefix);
}
}
prefix = "changed";
new Printer().print();
}
}error: local variables referenced from an inner class must be final or effectively finalfinal을 명시하지 않아도 초기화 뒤 한 번도 재대입하지 않으면 사실상 final입니다.
객체 참조를 캡처한 경우 참조 재대입은 금지되지만 그 객체의 가변 메서드 호출까지 자동으로 막지는 않습니다.
지역 객체와 캡처 값의 수명
팩터리 메서드는 Printer를 반환하고 종료됩니다.
나중에 print를 호출해도 prefix와 viewCount가 출력됩니다.
실행 중 원래 스택 변수를 다시 찾는 것이 아니라 생성 시 객체에 저장한 값을 사용합니다.
public final class CapturedPostPrinter {
public static void main(String[] args) {
Printer printer = createPrinter("daily", 40);
System.out.println("factory-finished");
printer.print("local class");
}
private static Printer createPrinter(String prefix, int viewCount) {
String separator = ":";
class LocalPrinter implements Printer {
@Override
public void print(String title) {
System.out.println(prefix + separator + title + "=" + viewCount);
}
}
return new LocalPrinter();
}
private interface Printer {
void print(String title);
}
}factory-finished
daily:local class=40prefix와 viewCount의 생명 주기가 LocalPrinter와 함께 연장된 것처럼 보입니다.
실제 구현 세부 필드 이름에 의존하지 않고 “필요한 값을 캡처한다”는 언어 계약으로 이해합니다.
팩터리 반환 뒤에도 사용하는 캡처 값
팩터리 생성→반환→factory-finished→나중 print의 호출 순서가 실제 예제입니다. 메서드 종료 뒤 캡처한 값이 사용되는 시간 차이를 장면으로 따라가며 캡처 값의 사용 시점을 확인할 수 있습니다.
CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다.
지역 클래스 · 생성 뒤 호출
팩터리 반환 뒤에도 사용하는 캡처 값
팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다.
팩터리 생성·반환
- 사용하는 값과 반환
createPrinter("daily", 40)에서LocalPrinter를 생성하고 참조를 반환합니다.prefix는"daily",viewCount는40입니다.- 실제 출력
- 이 단계의 출력 없음
반환 후 main
- 사용하는 값과 반환
- 반환된 참조를
printer에 보관한 뒤 다음 문장을 실행합니다. - 실제 출력
factory-finished
나중 print 호출
- 사용하는 값과 반환
printer.print("local class")는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다.- 실제 출력
daily:local class=40
원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":"도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다.
이 예의 조건
- 논리적 캡처값이며 물리 필드 배치를 가정하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. |
| 구분 | 내용 |
|---|---|
| 설명 | 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. |
| 전체 읽기 정보 | 팩터리 반환 뒤에도 사용하는 캡처 값 팩터리 반환 뒤에도 사용하는 캡처 값 CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다. 지역 클래스 · 생성 뒤 호출 팩터리 반환 뒤에도 사용하는 캡처 값 팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다. 팩터리 생성·반환 사용하는 값과 반환 createPrinter("daily", 40) 에서 LocalPrinter 를 생성하고 참조를 반환합니다. prefix 는 "daily" , viewCount 는 40 입니다. 실제 출력 이 단계의 출력 없음 반환 후 main 사용하는 값과 반환 반환된 참조를 printer 에 보관한 뒤 다음 문장을 실행합니다. 실제 출력 factory-finished 나중 print 호출 사용하는 값과 반환 printer.print("local class") 는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다. 실제 출력 daily:local class=40 원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":" 도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 팩터리 호출 daily/40을 받은 Printer를 만듭니다. |
|
|
| 팩터리 반환 main이 Printer 참조를 받고 다음 문장을 실행합니다. |
|
|
| 나중 행동 print(local class)가 앞선 값을 사용합니다. |
|
|
팩터리 반환 뒤에도 사용하는 캡처 값
팩터리 생성→반환→factory-finished→나중 print의 호출 순서가 실제 예제입니다. 메서드 종료 뒤 캡처한 값이 사용되는 시간 차이를 장면으로 따라가며 캡처 값의 사용 시점을 확인할 수 있습니다.
CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다.
지역 클래스 · 생성 뒤 호출
팩터리 반환 뒤에도 사용하는 캡처 값
팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다.
팩터리 생성·반환
- 사용하는 값과 반환
createPrinter("daily", 40)에서LocalPrinter를 생성하고 참조를 반환합니다.prefix는"daily",viewCount는40입니다.- 실제 출력
- 이 단계의 출력 없음
반환 후 main
- 사용하는 값과 반환
- 반환된 참조를
printer에 보관한 뒤 다음 문장을 실행합니다. - 실제 출력
factory-finished
나중 print 호출
- 사용하는 값과 반환
printer.print("local class")는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다.- 실제 출력
daily:local class=40
원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":"도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다.
이 예의 조건
- 논리적 캡처값이며 물리 필드 배치를 가정하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. |
| 구분 | 내용 |
|---|---|
| 설명 | 팩터리 호출은 Printer 참조를 반환하고 끝납니다. main이 factory-finished를 출력한 뒤 그 참조를 호출하면 앞서 받은 daily와 40 및 이번 제목 local class를 사용해 결과를 만듭니다. 논리적 값과 실행 순서를 나타내며 물리 캡처 필드 배치를 가정하지 않습니다. |
| 전체 읽기 정보 | 팩터리 반환 뒤에도 사용하는 캡처 값 팩터리 반환 뒤에도 사용하는 캡처 값 CapturedPostPrinter는 daily와 40으로 지역 Printer를 만들어 반환한다. main이 factory-finished를 출력한 뒤 반환 객체를 호출하면 daily:local class=40이 출력된다. 지역 클래스 · 생성 뒤 호출 팩터리 반환 뒤에도 사용하는 캡처 값 팩터리는 Printer 객체를 반환합니다. 팩터리 종료 후 print에 전달한 제목과 저장된 prefix·views를 함께 사용합니다. 팩터리 생성·반환 사용하는 값과 반환 createPrinter("daily", 40) 에서 LocalPrinter 를 생성하고 참조를 반환합니다. prefix 는 "daily" , viewCount 는 40 입니다. 실제 출력 이 단계의 출력 없음 반환 후 main 사용하는 값과 반환 반환된 참조를 printer 에 보관한 뒤 다음 문장을 실행합니다. 실제 출력 factory-finished 나중 print 호출 사용하는 값과 반환 printer.print("local class") 는 앞서 캡처한 값과 이번 제목 인수를 함께 사용합니다. 실제 출력 daily:local class=40 원래 팩터리 호출이 다시 실행되는 것은 아닙니다. separator = ":" 도 문장 구성에 사용하지만, 이 자료는 컴파일러가 만든 물리 필드의 이름·배치를 가정하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 팩터리 호출 daily/40을 받은 Printer를 만듭니다. |
|
|
| 팩터리 반환 main이 Printer 참조를 받고 다음 문장을 실행합니다. |
|
|
| 나중 행동 print(local class)가 앞선 값을 사용합니다. |
|
|
스택과 힙의 수명 차이
메서드 매개변수와 지역 변수는 호출 스택 프레임에 있고 메서드 종료와 함께 사라집니다.
new로 만든 LocalPrinter는 힙에 있으며 참조가 남아 있으면 계속 살아 있습니다.
지역 객체가 사라진 스택 값에 직접 접근할 수 없으므로 생성 시 값을 자기 상태로 복사합니다.
만약 캡처 뒤 원본 지역 변수를 바꿀 수 있다면 어느 값을 출력해야 하는지 문제가 생깁니다.
스택 값과 객체 안 복사본을 자동 동기화하려면 별도 공유 상자가 필요하고 동시성·수명 규칙이 복잡해집니다.
사실상 final 제약은 값 스냅숏 의미를 단순하게 보존합니다.
import java.util.ArrayList;
import java.util.List;
public final class CaptureSnapshotFactory {
public static void main(String[] args) {
List<Labeler> labelers = new ArrayList<>();
for (int index = 0; index < 3; index++) {
int capturedIndex = index;
class IndexedLabeler implements Labeler {
public String label(String title) {
return capturedIndex + "=" + title;
}
}
labelers.add(new IndexedLabeler());
}
for (Labeler labeler : labelers) {
System.out.println(labeler.label("capture"));
}
}
private interface Labeler {
String label(String title);
}
}0=capture
1=capture
2=capture루프의 index는 매 반복 증가하므로 캡처할 수 없습니다.
반복마다 새로 초기화되는 capturedIndex는 그 반복 안에서 사실상 final이고 각 객체가 다른 값 스냅숏을 가집니다.
바깥 인스턴스 필드의 접근
이 예제처럼 인스턴스 메서드에 선언한 지역 클래스는 바깥 인스턴스와 연결됩니다. 반면 static 메서드 등 정적 문맥에 선언한 지역·익명 클래스에는 그 바깥 인스턴스 연결이 없습니다.
지역 값 복사본이 아니라 힙에 있는 같은 바깥 객체 필드를 읽으므로 필드가 바뀌면 최신 값이 보입니다.
public final class OuterFieldAndLocalCapture {
private int publishedCount = 10;
public static void main(String[] args) {
OuterFieldAndLocalCapture tracker = new OuterFieldAndLocalCapture();
Reporter reporter = tracker.createReporter(100);
tracker.publishedCount = 40;
reporter.print();
}
private Reporter createReporter(int targetCount) {
class ProgressReporter implements Reporter {
public void print() {
System.out.println("progress=" + publishedCount + "/" + targetCount);
}
}
return new ProgressReporter();
}
private interface Reporter { void print(); }
}progress=40/100targetCount는 생성 시 캡처한 100이고 publishedCount는 바깥 객체의 현재 40입니다.
한 출력에서 스냅숏과 최신 상태를 섞는 것이 요구에 맞는지 확인해야 합니다.
모두 한 시점이어야 하면 불변 Progress 값을 먼저 만들어 전달합니다.
반복별 캡처 값과 바깥 객체의 현재 필드
반복별 캡처값과 별도 Reporter의 생성 시 targetCount/호출 시 publishedCount를 추적합니다. 각 독립 프로그램을 유지한 채 생성→바깥변경→나중호출 시점을 분리해 값의 출처를 확인할 수 있습니다.
CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다.
캡처와 필드 · 읽는 시점
반복별 캡처 값과 바깥 객체의 현재 필드
반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다.
| 읽는 값 | 시간 기준 | 원문 값·결과 |
|---|---|---|
반복별 capturedIndex | 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. | |
진행률의 targetCount | createReporter(100) 호출 때 캡처한 매개변수 값입니다. | 100 |
진행률의 publishedCount | 같은 tracker의 필드입니다. 객체 생성 때의 10이 아니라 reporter.print() 때의 현재 값을 읽습니다. | 40 |
- 반복별
capturedIndex - 시간 기준
- 반복마다 새 지역 값을 초기화하고 그 값으로 만든
IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. - 원문 값·결과
0=capture 1=capture 2=capture
- 진행률의
targetCount - 시간 기준
createReporter(100)호출 때 캡처한 매개변수 값입니다.- 원문 값·결과
100
- 진행률의
publishedCount - 시간 기준
- 같은
tracker의 필드입니다. 객체 생성 때의10이 아니라reporter.print()때의 현재 값을 읽습니다. - 원문 값·결과
40
진행률 예제는 두 값을 합쳐 progress=40/100을 출력합니다. 바뀌는 루프 index는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다.
이 예의 조건
- 반복 라벨러와 Reporter는 독립 프로그램이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. |
| 25 | print 때 현재 40 읽음 |
| 28 | 현재 40 + 캡처 100 |
| 구분 | 내용 |
|---|---|
| 설명 | 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. |
| 전체 읽기 정보 | 반복별 캡처 값과 바깥 객체의 현재 필드 반복별 캡처 값과 바깥 객체의 현재 필드 CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다. 캡처와 필드 · 읽는 시점 반복별 캡처 값과 바깥 객체의 현재 필드 반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다. 반복별 캡처 값과 바깥 객체의 현재 필드 읽는 값 시간 기준 원문 값·결과 반복별 capturedIndex 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 0=capture 1=capture 2=capture 진행률의 targetCount createReporter(100) 호출 때 캡처한 매개변수 값입니다. 100 진행률의 publishedCount 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 40 반복별 capturedIndex 시간 기준 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 원문 값·결과 0=capture 1=capture 2=capture 진행률의 targetCount 시간 기준 createReporter(100) 호출 때 캡처한 매개변수 값입니다. 원문 값·결과 100 진행률의 publishedCount 시간 기준 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 원문 값·결과 40 진행률 예제는 두 값을 합쳐 progress=40/100 을 출력합니다. 바뀌는 루프 index 는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex 는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 생성 시 값 반복마다 index를 캡처하고 Reporter는 목표 100을 받습니다. |
|
|
| 생성 후 변경 tracker 필드만 10→40으로 바꿉니다. |
|
|
| 나중 호출 각 객체가 자신의 캡처값 또는 현재 필드를 사용합니다. |
|
|
반복별 캡처 값과 바깥 객체의 현재 필드
반복별 캡처값과 별도 Reporter의 생성 시 targetCount/호출 시 publishedCount를 추적합니다. 각 독립 프로그램을 유지한 채 생성→바깥변경→나중호출 시점을 분리해 값의 출처를 확인할 수 있습니다.
CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다.
캡처와 필드 · 읽는 시점
반복별 캡처 값과 바깥 객체의 현재 필드
반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다.
| 읽는 값 | 시간 기준 | 원문 값·결과 |
|---|---|---|
반복별 capturedIndex | 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. | |
진행률의 targetCount | createReporter(100) 호출 때 캡처한 매개변수 값입니다. | 100 |
진행률의 publishedCount | 같은 tracker의 필드입니다. 객체 생성 때의 10이 아니라 reporter.print() 때의 현재 값을 읽습니다. | 40 |
- 반복별
capturedIndex - 시간 기준
- 반복마다 새 지역 값을 초기화하고 그 값으로 만든
IndexedLabeler를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. - 원문 값·결과
0=capture 1=capture 2=capture
- 진행률의
targetCount - 시간 기준
createReporter(100)호출 때 캡처한 매개변수 값입니다.- 원문 값·결과
100
- 진행률의
publishedCount - 시간 기준
- 같은
tracker의 필드입니다. 객체 생성 때의10이 아니라reporter.print()때의 현재 값을 읽습니다. - 원문 값·결과
40
진행률 예제는 두 값을 합쳐 progress=40/100을 출력합니다. 바뀌는 루프 index는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다.
이 예의 조건
- 반복 라벨러와 Reporter는 독립 프로그램이다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. |
| 25 | print 때 현재 40 읽음 |
| 28 | 현재 40 + 캡처 100 |
| 구분 | 내용 |
|---|---|
| 설명 | 위 영역의 세 라벨러는 반복마다 초기화한 capturedIndex 0,1,2를 각자 사용합니다. 아래 별도 프로그램의 Reporter는 생성 때 캡처한 targetCount 100과 호출 때 읽는 같은 tracker의 publishedCount 40을 함께 사용합니다. 점선은 출력에 사용되는 값입니다. |
| 전체 읽기 정보 | 반복별 캡처 값과 바깥 객체의 현재 필드 반복별 캡처 값과 바깥 객체의 현재 필드 CaptureSnapshotFactory의 세 객체는 각 반복의 capturedIndex 0,1,2를 사용한다. 별도 OuterFieldAndLocalCapture 예제는 캡처한 targetCount 100과 현재 publishedCount 40을 함께 출력한다. 캡처와 필드 · 읽는 시점 반복별 캡처 값과 바깥 객체의 현재 필드 반복 캡처 예제와 진행률 예제는 별도 프로그램입니다. 생성 때 고정한 지역 값과 호출 때 읽는 바깥 필드를 구별합니다. 반복별 캡처 값과 바깥 객체의 현재 필드 읽는 값 시간 기준 원문 값·결과 반복별 capturedIndex 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 0=capture 1=capture 2=capture 진행률의 targetCount createReporter(100) 호출 때 캡처한 매개변수 값입니다. 100 진행률의 publishedCount 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 40 반복별 capturedIndex 시간 기준 반복마다 새 지역 값을 초기화하고 그 값으로 만든 IndexedLabeler 를 보관합니다. 루프 뒤에 호출해도 각 값이 다릅니다. 원문 값·결과 0=capture 1=capture 2=capture 진행률의 targetCount 시간 기준 createReporter(100) 호출 때 캡처한 매개변수 값입니다. 원문 값·결과 100 진행률의 publishedCount 시간 기준 같은 tracker 의 필드입니다. 객체 생성 때의 10 이 아니라 reporter.print() 때의 현재 값을 읽습니다. 원문 값·결과 40 진행률 예제는 두 값을 합쳐 progress=40/100 을 출력합니다. 바뀌는 루프 index 는 직접 캡처할 수 없지만, 반복마다 초기화하는 capturedIndex 는 사실상 final입니다. 이것은 객체 내부 전체 상태를 동결한다는 뜻이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 생성 시 값 반복마다 index를 캡처하고 Reporter는 목표 100을 받습니다. |
|
|
| 생성 후 변경 tracker 필드만 10→40으로 바꿉니다. |
|
|
| 나중 호출 각 객체가 자신의 캡처값 또는 현재 필드를 사용합니다. |
|
|
가변 객체 참조 캡처의 위험
List<String> tags 참조를 재대입하지 않으면 사실상 final이지만 tags.add(...)는 가능합니다.
지역 객체가 나중에 목록을 읽으면 생성 이후 추가된 값까지 봅니다.
캡처 시점 스냅숏이 필요하면 List.copyOf(tags)를 별도 지역 값으로 만들어 캡처합니다.
목록을 복사했다는 말이 요소의 내용까지 고정했다는 뜻은 아닙니다. List.copyOf의 공식 계약은 목록의 추가·삭제·교체를 막지만 가변 요소까지 불변으로 만들지는 않습니다.
예시
다음 독립 파일에서 StringBuilder는 문자열 내용을 바꿀 수 있는 객체입니다. snapshot은 원소 구성을 고정한 목록이고, fixedText는 생성 시점의 불변 String 값입니다.
import java.util.ArrayList;
import java.util.List;
public final class MutableTagCaptureBoundary {
public static void main(String[] args) {
StringBuilder tag = new StringBuilder("java");
List<StringBuilder> source = new ArrayList<>(List.of(tag));
Formatter formatter = createFormatter(source);
tag.append("-changed");
source.add(new StringBuilder("nested"));
System.out.println(formatter.format());
System.out.println("source-size=" + source.size());
}
private static Formatter createFormatter(List<StringBuilder> tags) {
List<StringBuilder> snapshot = List.copyOf(tags);
String fixedText = snapshot.getFirst().toString();
class TagFormatter implements Formatter {
public String format() {
return "members=" + snapshot.size()
+ ", live=" + snapshot.getFirst()
+ ", fixed=" + fixedText;
}
}
return new TagFormatter();
}
private interface Formatter { String format(); }
}members=1, live=java-changed, fixed=java
source-size=2추가한 nested는 snapshot에 들어오지 않지만, 첫 요소는 같은 StringBuilder 참조라 변경된 내용을 읽습니다. fixedText는 toString을 호출한 시점의 String 값이므로 java를 유지합니다. 캡처한 지역 참조를 재대입하지 않은 것과 그 참조가 가리키는 객체를 바꾸지 않은 것은 별개의 조건입니다.
지역 클래스를 사용할 때 다음을 결정합니다.
- 캡처 값은 생성 시점 스냅숏인가, 공유 가변 상태인가?
- 객체가 메서드 범위를 벗어나 반환되는가?
- 큰 객체를 캡처해 불필요하게 수명을 늘리지 않는가?
- 같은 구현을 여러 메서드가 필요로 하지 않는가?
- 단일 메서드 함수라면 람다가 더 간단하지 않은가?
이름 있는 지역 클래스는 한 메서드 안에서 여러 메서드나 상태를 가진 작은 구현이 필요할 때 유용합니다.
연습 문제
가변 태그 목록을 받은 팩터리에서 불변 복사본을 만들고, 지역 클래스 Formatter가 그 복사본으로 문장을 만들게 하세요.
팩터리 반환 후 원본 목록을 바꿔도 출력은 유지되어야 합니다.
원본 초깃값은 ["java", "nested"]이고, 팩터리에서 Formatter를 만든 뒤 원본에 "changed"를 추가합니다. format의 title 입력은 "capture"이며 출력 형식은 title=쉼표로 이은 태그입니다. 태그 목록과 각 요소는 null이 아닌 것으로 정합니다.
독립 연습에서는 초기 목록을 ["java"]로 바꾼 뒤 같은 추가를 합니다. Formatter가 보는 요소 수와 format("capture")의 결과를 예상하세요.
해설 보기
import java.util.ArrayList;
import java.util.List;
public final class LocalTagSnapshotExercise {
public static void main(String[] args) {
List<String> source = new ArrayList<>(List.of("java", "nested"));
Formatter formatter = createFormatter(source);
source.add("changed");
System.out.println(formatter.format("capture"));
System.out.println("source=" + source);
}
private static Formatter createFormatter(List<String> tags) {
List<String> snapshot = List.copyOf(tags);
class TagFormatter implements Formatter {
public String format(String title) {
return title + "=" + String.join(",", snapshot);
}
}
return new TagFormatter();
}
private interface Formatter { String format(String title); }
}capture=java,nested
source=[java, nested, changed]List.copyOf로 만든 snapshot은 원소의 추가·삭제·교체를 허용하지 않고 원본 목록의 이후 변경을 반영하지 않습니다. 다만 요소 자체를 깊게 복사하지 않으며, 이 예제의 요소는 불변 String입니다.
독립 입력의 Formatter는 한 요소만 보며 capture=java입니다. 원본 목록은 [java, changed]가 됩니다. 요소는 String이므로 목록 구성뿐 아니라 각 태그의 문자 값도 생성 시점 값을 유지합니다.
캡처 대상의 시점 의미를 코드로 확정했습니다.
목록 멤버십의 복사와 요소 참조의 한계
List.copyOf 생성 뒤 source에 changed를 추가해 snapshot 멤버십은 유지되고 String 요소 참조는 공유됩니다. 복사 시점과 이후 추가를 분리하면 얕은 복사의 경계가 명확합니다.
LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다.
목록 복사 · 원소 참조
목록 멤버십의 복사와 요소 참조의 한계
List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다.
| 대상 | 나중 변경과의 관계 | 원문 결과·한계 |
|---|---|---|
원본 목록 source | source.add("changed")로 원본의 요소 수가 2에서 3이 됩니다. | |
캡처한 목록 snapshot | 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. | |
| 목록 안 요소 | 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. | 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. |
- 원본 목록
source - 나중 변경과의 관계
source.add("changed")로 원본의 요소 수가2에서3이 됩니다.- 원문 결과·한계
source=[java, nested, changed]
- 캡처한 목록
snapshot - 나중 변경과의 관계
- 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다.
- 원문 결과·한계
capture=java,nested
- 목록 안 요소
- 나중 변경과의 관계
- 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의
String요소는 불변입니다. - 원문 결과·한계
- 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다.
참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다.
이 예의 조건
- String 요소는 불변이며 가변 요소의 깊은 복사를 보장하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 4 | 나중 추가 뒤 요소 3개 |
| 8 | 추가 전 요소 2개 유지 |
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 목록 멤버십의 복사와 요소 참조의 한계 목록 멤버십의 복사와 요소 참조의 한계 LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다. 목록 복사 · 원소 참조 목록 멤버십의 복사와 요소 참조의 한계 List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다. 목록 멤버십의 복사와 요소 참조의 한계 대상 나중 변경과의 관계 원문 결과·한계 원본 목록 source source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. source=[java, nested, changed] 캡처한 목록 snapshot 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. capture=java,nested 목록 안 요소 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 원본 목록 source 나중 변경과의 관계 source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. 원문 결과·한계 source=[java, nested, changed] 캡처한 목록 snapshot 나중 변경과의 관계 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. 원문 결과·한계 capture=java,nested 목록 안 요소 나중 변경과의 관계 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 원문 결과·한계 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 복사 전 source가 두 String 요소를 보유합니다. |
|
|
| snapshot 생성 List.copyOf(tags)로 두 요소를 보관합니다. |
|
|
| source 추가 changed를 외부 목록에 넣습니다. |
|
|
목록 멤버십의 복사와 요소 참조의 한계
List.copyOf 생성 뒤 source에 changed를 추가해 snapshot 멤버십은 유지되고 String 요소 참조는 공유됩니다. 복사 시점과 이후 추가를 분리하면 얕은 복사의 경계가 명확합니다.
LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다.
목록 복사 · 원소 참조
목록 멤버십의 복사와 요소 참조의 한계
List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다.
| 대상 | 나중 변경과의 관계 | 원문 결과·한계 |
|---|---|---|
원본 목록 source | source.add("changed")로 원본의 요소 수가 2에서 3이 됩니다. | |
캡처한 목록 snapshot | 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. | |
| 목록 안 요소 | 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. | 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. |
- 원본 목록
source - 나중 변경과의 관계
source.add("changed")로 원본의 요소 수가2에서3이 됩니다.- 원문 결과·한계
source=[java, nested, changed]
- 캡처한 목록
snapshot - 나중 변경과의 관계
- 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다.
- 원문 결과·한계
capture=java,nested
- 목록 안 요소
- 나중 변경과의 관계
- 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의
String요소는 불변입니다. - 원문 결과·한계
- 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다.
참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다.
이 예의 조건
- String 요소는 불변이며 가변 요소의 깊은 복사를 보장하지 않는다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 4 | 나중 추가 뒤 요소 3개 |
| 8 | 추가 전 요소 2개 유지 |
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 목록 멤버십의 복사와 요소 참조의 한계 목록 멤버십의 복사와 요소 참조의 한계 LocalTagSnapshotExercise는 java,nested의 목록 스냅숏을 캡처한다. 원본에 changed를 추가해도 formatter의 결과는 유지되지만 List.copyOf는 요소 자체를 깊게 복사하지 않는다. 목록 복사 · 원소 참조 목록 멤버십의 복사와 요소 참조의 한계 List.copyOf로 얻은 목록은 source의 나중 추가를 반영하지 않습니다. 원소 객체 자체를 깊게 복사하는 것은 아닙니다. 목록 멤버십의 복사와 요소 참조의 한계 대상 나중 변경과의 관계 원문 결과·한계 원본 목록 source source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. source=[java, nested, changed] 캡처한 목록 snapshot 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. capture=java,nested 목록 안 요소 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 원본 목록 source 나중 변경과의 관계 source.add("changed") 로 원본의 요소 수가 2 에서 3 이 됩니다. 원문 결과·한계 source=[java, nested, changed] 캡처한 목록 snapshot 나중 변경과의 관계 추가·삭제·교체할 수 없는 목록이며 원본에 나중에 추가한 요소를 반영하지 않습니다. 원문 결과·한계 capture=java,nested 목록 안 요소 나중 변경과의 관계 복사는 요소 자체의 깊은 복사가 아닙니다. 이 예제의 String 요소는 불변입니다. 원문 결과·한계 가변 요소라면 그 요소의 상태 변경까지 막아 주지는 않습니다. 참조를 다시 대입하지 않는다는 규칙과 참조 대상의 불변성은 다릅니다. 실제 main은 formatter 결과를 먼저, 원본 목록을 다음에 출력합니다. 표는 두 저장 대상을 비교한 것이며 물리 메모리 배치나 항상 새 인스턴스를 만드는 계약을 뜻하지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 복사 전 source가 두 String 요소를 보유합니다. |
|
|
| snapshot 생성 List.copyOf(tags)로 두 요소를 보관합니다. |
|
|
| source 추가 changed를 외부 목록에 넣습니다. |
|
|