구체 구현 의존 비용
구현별 필드와 분기가 늘어나는 게시글 내보내기를 재현하고 역할을 도입하기 전 변경 지점을 구조적으로 측정합니다.
역할과 구현을 분리하지 않은 코드는 처음 한 구현만 있을 때 단순합니다.
문제는 두 번째·세 번째 구현이 추가될 때 나타납니다.
클라이언트가 각 구체 타입을 필드로 보관하고 종류를 분기하면 새 구현마다 기존 핵심 코드를 수정해야 합니다.
구현 교체와 잔존 상태
public final class ConcreteExporterSwitchBug {
public static void main(String[] args) {
ExportService service = new ExportService();
service.setTextExporter(new TextExporter());
service.setCsvExporter(new CsvExporter());
service.export("가입 인사", 40);
}
private static final class ExportService {
private TextExporter textExporter;
private CsvExporter csvExporter;
void setTextExporter(TextExporter exporter) {
textExporter = exporter;
}
void setCsvExporter(CsvExporter exporter) {
csvExporter = exporter;
}
void export(String title, int viewCount) {
if (textExporter != null) {
textExporter.export(title, viewCount);
} else if (csvExporter != null) {
csvExporter.export(title, viewCount);
}
}
}
private static final class TextExporter {
void export(String title, int viewCount) { System.out.println(title + "=" + viewCount); }
}
private static final class CsvExporter {
void export(String title, int viewCount) { System.out.println(title + "," + viewCount); }
}
}가입 인사=40마지막에 CsvExporter를 설정했으므로 CSV를 기대했지만 TextExporter 필드도 null이 아니어서 첫 분기가 실행됐습니다.
두 exporter 필드의 조합과 먼저 선택되는 분기
두 필드가 모두 참조를 가지면 첫 text 조건이 참이 되어 Text를 호출하고 CSV 조건은 검사하지 않습니다. 설정 상태와 첫 분기의 우선순위를 실행 순서로 드러낼 수 있습니다.
ConcreteExporterSwitchBug의 export는 textExporter부터 검사한다. 두 필드가 모두 null이면 출력 없이 끝나고, 둘 다 참조를 담으면 CSV를 마지막에 설정했어도 TextExporter를 호출한다.
두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다
ConcreteExporterSwitchBug의 export는 textExporter를 먼저 검사합니다. 표는 두 필드의 null 여부에 따른 소스 분기를 비교합니다.
| 필드 조합 | textExporter | csvExporter | 선택하는 실행 |
|---|---|---|---|
| 설정 없음 | null | null | 두 조건 모두 거짓입니다. exporter를 호출하지 않고 출력 없이 끝납니다. |
| Text만 설정 | 참조 있음 | null | 첫 if에서 TextExporter를 호출합니다. |
| CSV만 설정 | null | 참조 있음 | 첫 조건은 거짓이고, else if에서 CsvExporter를 호출합니다. |
| 둘 다 설정 원문 main | 참조 있음 | 참조 있음 | 첫 if에서 TextExporter를 호출합니다. CSV 조건은 검사하지 않습니다. |
- 설정 없음
textExporter = null,csvExporter = null. 두 조건 모두 거짓이므로 exporter 호출이나 출력 없이 끝납니다.- Text만 설정
textExporter는 참조를 담고csvExporter = null입니다. 첫if에서TextExporter를 호출합니다.- CSV만 설정
textExporter = null이고csvExporter는 참조를 담습니다. 첫 조건은 거짓이며else if에서CsvExporter를 호출합니다.- 둘 다 설정 — 원문 main
- 두 필드 모두 참조를 담습니다. 첫
if에서TextExporter를 호출하므로 CSV 조건은 검사하지 않습니다.
main은 setTextExporter(...) 다음에 setCsvExporter(...)를 호출하지만, 첫 필드를 비우지 않습니다. 이어 export("가입 인사", 40)를 호출한 실제 출력은 다음 한 줄입니다.
가입 인사=40
다른 세 조합은 원문 main에서 실행한 단계가 아니라 같은 메서드의 조건으로 판별한 경우입니다. 둘 다 설정된 상태의 선택은 모호한 것이 아니라 코드 순서상 Text 우선입니다.
이 예의 조건
- 원 main은 text와 csv 둘 다 참조가 있습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | textExporter 참조가 있으면 Text를 호출하고 CSV 조건은 검사하지 않는다. 없을 때만 csvExporter를 검사한다. 두 참조가 모두 null이면 출력 없이 끝난다. 원본 main은 둘 다 참조가 있으므로 Text 출력이다. |
| 구분 | 내용 |
|---|---|
| 설명 | textExporter 참조가 있으면 Text를 호출하고 CSV 조건은 검사하지 않는다. 없을 때만 csvExporter를 검사한다. 두 참조가 모두 null이면 출력 없이 끝난다. 원본 main은 둘 다 참조가 있으므로 Text 출력이다. |
| 전체 읽기 정보 | 두 exporter 필드의 조합과 먼저 선택되는 분기 두 exporter 필드의 조합과 먼저 선택되는 분기 ConcreteExporterSwitchBug의 export는 textExporter부터 검사한다. 두 필드가 모두 null이면 출력 없이 끝나고, 둘 다 참조를 담으면 CSV를 마지막에 설정했어도 TextExporter를 호출한다. 두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다 ConcreteExporterSwitchBug 의 export 는 textExporter 를 먼저 검사합니다. 표는 두 필드의 null 여부에 따른 소스 분기를 비교합니다. ConcreteExporterSwitchBug의 두 필드 조합과 export 호출 결과 필드 조합 textExporter csvExporter 선택하는 실행 설정 없음 null null 두 조건 모두 거짓입니다. exporter를 호출하지 않고 출력 없이 끝납니다. Text만 설정 참조 있음 null 첫 if 에서 TextExporter 를 호출합니다. CSV만 설정 null 참조 있음 첫 조건은 거짓이고, else if 에서 CsvExporter 를 호출합니다. 둘 다 설정 원문 main 참조 있음 참조 있음 첫 if 에서 TextExporter 를 호출합니다. CSV 조건은 검사하지 않습니다. 설정 없음 textExporter = null , csvExporter = null . 두 조건 모두 거짓이므로 exporter 호출이나 출력 없이 끝납니다. Text만 설정 textExporter 는 참조를 담고 csvExporter = null 입니다. 첫 if 에서 TextExporter 를 호출합니다. CSV만 설정 textExporter = null 이고 csvExporter 는 참조를 담습니다. 첫 조건은 거짓이며 else if 에서 CsvExporter 를 호출합니다. 둘 다 설정 — 원문 main 두 필드 모두 참조를 담습니다. 첫 if 에서 TextExporter 를 호출하므로 CSV 조건은 검사하지 않습니다. main 은 setTextExporter(...) 다음에 setCsvExporter(...) 를 호출하지만, 첫 필드를 비우지 않습니다. 이어 export("가입 인사", 40) 를 호출한 실제 출력은 다음 한 줄입니다. 가입 인사=40 다른 세 조합은 원문 main 에서 실행한 단계가 아니라 같은 메서드의 조건으로 판별한 경우입니다. 둘 다 설정된 상태의 선택은 모호한 것이 아니라 코드 순서상 Text 우선입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 필드 검사 export가 textExporter 참조부터 확인합니다. |
|
|
| 원 출력 Text로 가입 인사를 출력합니다. |
|
|
| 별도 부재 분기 Text가 없을 때만 CSV를 검사합니다. |
|
|
두 exporter 필드의 조합과 먼저 선택되는 분기
두 필드가 모두 참조를 가지면 첫 text 조건이 참이 되어 Text를 호출하고 CSV 조건은 검사하지 않습니다. 설정 상태와 첫 분기의 우선순위를 실행 순서로 드러낼 수 있습니다.
ConcreteExporterSwitchBug의 export는 textExporter부터 검사한다. 두 필드가 모두 null이면 출력 없이 끝나고, 둘 다 참조를 담으면 CSV를 마지막에 설정했어도 TextExporter를 호출한다.
두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다
ConcreteExporterSwitchBug의 export는 textExporter를 먼저 검사합니다. 표는 두 필드의 null 여부에 따른 소스 분기를 비교합니다.
| 필드 조합 | textExporter | csvExporter | 선택하는 실행 |
|---|---|---|---|
| 설정 없음 | null | null | 두 조건 모두 거짓입니다. exporter를 호출하지 않고 출력 없이 끝납니다. |
| Text만 설정 | 참조 있음 | null | 첫 if에서 TextExporter를 호출합니다. |
| CSV만 설정 | null | 참조 있음 | 첫 조건은 거짓이고, else if에서 CsvExporter를 호출합니다. |
| 둘 다 설정 원문 main | 참조 있음 | 참조 있음 | 첫 if에서 TextExporter를 호출합니다. CSV 조건은 검사하지 않습니다. |
- 설정 없음
textExporter = null,csvExporter = null. 두 조건 모두 거짓이므로 exporter 호출이나 출력 없이 끝납니다.- Text만 설정
textExporter는 참조를 담고csvExporter = null입니다. 첫if에서TextExporter를 호출합니다.- CSV만 설정
textExporter = null이고csvExporter는 참조를 담습니다. 첫 조건은 거짓이며else if에서CsvExporter를 호출합니다.- 둘 다 설정 — 원문 main
- 두 필드 모두 참조를 담습니다. 첫
if에서TextExporter를 호출하므로 CSV 조건은 검사하지 않습니다.
main은 setTextExporter(...) 다음에 setCsvExporter(...)를 호출하지만, 첫 필드를 비우지 않습니다. 이어 export("가입 인사", 40)를 호출한 실제 출력은 다음 한 줄입니다.
가입 인사=40
다른 세 조합은 원문 main에서 실행한 단계가 아니라 같은 메서드의 조건으로 판별한 경우입니다. 둘 다 설정된 상태의 선택은 모호한 것이 아니라 코드 순서상 Text 우선입니다.
이 예의 조건
- 원 main은 text와 csv 둘 다 참조가 있습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | textExporter 참조가 있으면 Text를 호출하고 CSV 조건은 검사하지 않는다. 없을 때만 csvExporter를 검사한다. 두 참조가 모두 null이면 출력 없이 끝난다. 원본 main은 둘 다 참조가 있으므로 Text 출력이다. |
| 구분 | 내용 |
|---|---|
| 설명 | textExporter 참조가 있으면 Text를 호출하고 CSV 조건은 검사하지 않는다. 없을 때만 csvExporter를 검사한다. 두 참조가 모두 null이면 출력 없이 끝난다. 원본 main은 둘 다 참조가 있으므로 Text 출력이다. |
| 전체 읽기 정보 | 두 exporter 필드의 조합과 먼저 선택되는 분기 두 exporter 필드의 조합과 먼저 선택되는 분기 ConcreteExporterSwitchBug의 export는 textExporter부터 검사한다. 두 필드가 모두 null이면 출력 없이 끝나고, 둘 다 참조를 담으면 CSV를 마지막에 설정했어도 TextExporter를 호출한다. 두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다 ConcreteExporterSwitchBug 의 export 는 textExporter 를 먼저 검사합니다. 표는 두 필드의 null 여부에 따른 소스 분기를 비교합니다. ConcreteExporterSwitchBug의 두 필드 조합과 export 호출 결과 필드 조합 textExporter csvExporter 선택하는 실행 설정 없음 null null 두 조건 모두 거짓입니다. exporter를 호출하지 않고 출력 없이 끝납니다. Text만 설정 참조 있음 null 첫 if 에서 TextExporter 를 호출합니다. CSV만 설정 null 참조 있음 첫 조건은 거짓이고, else if 에서 CsvExporter 를 호출합니다. 둘 다 설정 원문 main 참조 있음 참조 있음 첫 if 에서 TextExporter 를 호출합니다. CSV 조건은 검사하지 않습니다. 설정 없음 textExporter = null , csvExporter = null . 두 조건 모두 거짓이므로 exporter 호출이나 출력 없이 끝납니다. Text만 설정 textExporter 는 참조를 담고 csvExporter = null 입니다. 첫 if 에서 TextExporter 를 호출합니다. CSV만 설정 textExporter = null 이고 csvExporter 는 참조를 담습니다. 첫 조건은 거짓이며 else if 에서 CsvExporter 를 호출합니다. 둘 다 설정 — 원문 main 두 필드 모두 참조를 담습니다. 첫 if 에서 TextExporter 를 호출하므로 CSV 조건은 검사하지 않습니다. main 은 setTextExporter(...) 다음에 setCsvExporter(...) 를 호출하지만, 첫 필드를 비우지 않습니다. 이어 export("가입 인사", 40) 를 호출한 실제 출력은 다음 한 줄입니다. 가입 인사=40 다른 세 조합은 원문 main 에서 실행한 단계가 아니라 같은 메서드의 조건으로 판별한 경우입니다. 둘 다 설정된 상태의 선택은 모호한 것이 아니라 코드 순서상 Text 우선입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 필드 검사 export가 textExporter 참조부터 확인합니다. |
|
|
| 원 출력 Text로 가입 인사를 출력합니다. |
|
|
| 별도 부재 분기 Text가 없을 때만 CSV를 검사합니다. |
|
|
구현마다 필드와 setter를 추가하고 “현재 하나만 선택됨”이라는 규칙을 클라이언트 분기로 관리한 결과입니다.
새 JsonExporter를 추가하면 필드, setter, export 분기, 초기화 순서를 모두 수정해야 합니다.
구현 수가 늘수록 유효하지 않은 조합도 늘어납니다.
단일 구현의 단순성
public final class SingleConcreteExporter {
public static void main(String[] args) {
ExportService service = new ExportService(new TextExporter());
service.export("가입 인사", 40);
}
private static final class ExportService {
private final TextExporter exporter;
ExportService(TextExporter exporter) {
this.exporter = exporter;
}
void export(String title, int viewCount) {
System.out.println("export:start");
exporter.export(title, viewCount);
System.out.println("export:end");
}
}
private static final class TextExporter {
void export(String title, int viewCount) {
System.out.println(title + "=" + viewCount);
}
}
}export:start
가입 인사=40
export:end현재 요구만 보면 읽기 쉽고 잘 동작합니다.
모든 구체 의존을 미리 인터페이스로 바꿔야 하는 것은 아닙니다.
변경 가능성이 실제로 있고 같은 역할의 구현이 늘어나는 지점에서 분리 가치가 커집니다.
다만 ExportService의 생성자와 필드가 TextExporter를 직접 가리키므로 CsvExporter로 바꾸려면 서비스 소스를 수정해야 합니다.
“새 구현 추가”가 “기존 클라이언트 수정”을 요구한다는 결합을 확인합니다.
다중 구현의 분기 비용
public final class BranchedExportService {
public static void main(String[] args) {
ExportService service = new ExportService();
service.export("text", "가입 인사", 40);
service.export("csv", "로그인 구현", 50);
service.export("bad", "input", 30);
}
private static final class ExportService {
private final TextExporter text = new TextExporter();
private final CsvExporter csv = new CsvExporter();
void export(String option, String title, int viewCount) {
System.out.println("export:start=" + option);
if (option.equals("text")) {
text.export(title, viewCount);
} else if (option.equals("csv")) {
csv.export(title, viewCount);
} else {
System.out.println("unknown");
}
}
}
private static final class TextExporter {
void export(String title, int viewCount) { System.out.println(title + "=" + viewCount); }
}
private static final class CsvExporter {
void export(String title, int viewCount) { System.out.println(title + "," + viewCount); }
}
}export:start=text
가입 인사=40
export:start=csv
로그인 구현,50
export:start=bad
unknownExportService가 옵션 해석, 구현 생성, 공통 흐름, 구체 호출을 모두 압니다.
새 형식 추가 때 기존 if 사슬을 수정해야 하고 각 구현의 생성 비용도 서비스 수명과 결합됩니다.
테스트에서 특정 구현만 교체하기도 어렵습니다.
변경 축 분리의 기준
구체 의존을 발견했다고 즉시 인터페이스를 만들지 않고 다음을 확인합니다.
- Text·CSV·JSON이 클라이언트 입장에서 같은 “내보내기” 역할인가?
- 공통 입력과 성공·실패 계약을 정의할 수 있는가?
- 새 형식 추가 빈도가 핵심 흐름 변경 빈도와 다른가?
- 실행 중 구현을 교체해야 하는가?
- 구현 선택 문자열을 해석하는 책임은 어디에 둘 것인가?
같은 역할이라면 ExportService는 공통 전후 흐름만 알고 포맷 동작은 인터페이스에 맡길 수 있습니다.
옵션 문자열에서 구현을 선택하는 공장이나 조립점은 여전히 새 구현을 알아야 할 수 있습니다.
다형성은 모든 변경을 없애는 것이 아니라 핵심 클라이언트에서 구현 변경을 격리합니다.
구성을 통한 구현 선택
public final class SingleSlotExporter {
public static void main(String[] args) {
ExportService service = new ExportService();
service.useText(new TextExporter());
service.export("가입 인사", 40);
service.useCsv(new CsvExporter());
service.export("로그인 구현", 50);
}
private static final class ExportService {
private TextExporter text;
private CsvExporter csv;
void useText(TextExporter value) {
text = value;
csv = null;
}
void useCsv(CsvExporter value) {
csv = value;
text = null;
}
void export(String title, int viewCount) {
if (text != null) text.export(title, viewCount);
else if (csv != null) csv.export(title, viewCount);
else throw new IllegalStateException("no exporter");
}
}
private static final class TextExporter {
void export(String title, int viewCount) { System.out.println(title + "=" + viewCount); }
}
private static final class CsvExporter {
void export(String title, int viewCount) { System.out.println(title + "," + viewCount); }
}
}가입 인사=40
로그인 구현,50유효하지 않은 두 구현 동시 선택은 막았지만 새 구현마다 필드·메서드·분기가 계속 늘어납니다. 여기서 보장한 것은 한 스레드가 선택 메서드를 끝까지 실행한 뒤 두 필드가 동시에 채워져 있지 않다는 조건입니다. 두 대입문을 다른 스레드가 관찰하는 상황까지 원자적으로 보호한 코드는 아닙니다.
또한 “둘 중 최대 하나”와 “반드시 하나”는 다릅니다. useText(null)을 실행하면 text와 csv가 모두 null이 됩니다. 뒤의 export는 정상 형식을 출력하지 않고 IllegalStateException("no exporter")를 던집니다.
직접 확인
원래의 Text → CSV 실행 뒤에 service.useText(null)과 한 번의 export를 추가해 보세요. 첫 두 출력은 유지되고 마지막 호출만 실패합니다. 반대로 앞의 두 필드를 따로 설정하는 예제에서는 두 필드가 모두 존재하면 마지막 설정 시점이 아니라 if의 Text 분기가 먼저 선택됩니다.
구현을 교체할 때 반대 필드를 비우는 효과
useText와 useCsv는 다른 필드를 비우며 순차 선택 뒤 각 형식으로 출력하고 useText(null)은 둘 다 null이 됩니다. 완료 시점의 상태 변화와 동시 원자성 미보장을 유지합니다.
SingleSlotExporter의 main은 useText 후 text만 남겨 가입 인사=40을 출력하고, useCsv 후 text를 null로 비워 로그인 구현,50을 출력한다. 여전히 구체 타입 필드 두 개이며 null 입력을 거부하지 않는다.
새 구현을 선택하면서 이전 필드를 null로 비웁니다
SingleSlotExporter의 main에서 각 선택 메서드가 끝난 뒤, 다음 export 호출이 읽는 상태입니다.
메서드 복귀 뒤의 순차 상태
| 원문의 선택 호출 | 복귀 뒤 두 필드 | 뒤따르는 export의 출력 |
|---|---|---|
useText(...) | text: TextExporter 참조
| 가입 인사=40 |
useCsv(...) | csv: CsvExporter 참조
| 로그인 구현,50 |
useText(...)복귀 뒤text는 새TextExporter참조이고csv = null입니다. 이어export("가입 인사", 40)를 호출하면가입 인사=40을 출력합니다.useCsv(...)복귀 뒤csv는 새CsvExporter참조이고 이전text는null로 비워집니다. 이어export("로그인 구현", 50)를 호출하면로그인 구현,50을 출력합니다.
main은 useText에 new TextExporter(), useCsv에 new CsvExporter()를 전달합니다. 각 메서드는 전달받은 참조를 저장하고 다른 필드에 null을 대입합니다.
여전히 구체 타입 필드 두 개입니다. 선택 메서드는 null 입력을 거부하지 않으므로 “항상 하나가 설정됨”을 보장하지 않습니다. 두 필드가 null인 채 export를 호출하면 IllegalStateException("no exporter")가 발생합니다. 이 경계는 원문 main의 두 호출에는 나타나지 않습니다.
이 예의 조건
- 선택 메서드 정상 복귀 뒤 상태이며 동시 원자성 보장이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 선택 메서드가 복귀한 순차 상태를 보여 준다. useText는 text 참조/csv null, useCsv는 text null/csv 참조로 바꾼다. 별도 반례 useText(null)은 둘 다 null이므로 뒤 export는 예외를 던진다. 동시 원자성을 보장하지 않는다. |
| 구분 | 내용 |
|---|---|
| 설명 | 선택 메서드가 복귀한 순차 상태를 보여 준다. useText는 text 참조/csv null, useCsv는 text null/csv 참조로 바꾼다. 별도 반례 useText(null)은 둘 다 null이므로 뒤 export는 예외를 던진다. 동시 원자성을 보장하지 않는다. |
| 전체 읽기 정보 | 구현을 교체할 때 반대 필드를 비우는 효과 구현을 교체할 때 반대 필드를 비우는 효과 SingleSlotExporter의 main은 useText 후 text만 남겨 가입 인사=40을 출력하고, useCsv 후 text를 null로 비워 로그인 구현,50을 출력한다. 여전히 구체 타입 필드 두 개이며 null 입력을 거부하지 않는다. 새 구현을 선택하면서 이전 필드를 null로 비웁니다 SingleSlotExporter 의 main 에서 각 선택 메서드가 끝난 뒤, 다음 export 호출이 읽는 상태입니다. SingleSlotExporter의 두 선택 호출 뒤 필드 상태와 실제 출력 원문의 선택 호출 복귀 뒤 두 필드 뒤따르는 export의 출력 useText(...) text : TextExporter 참조 csv = null 가입 인사=40 useCsv(...) csv : CsvExporter 참조 text = null 로그인 구현,50 useText(...) 복귀 뒤 text 는 새 TextExporter 참조이고 csv = null 입니다. 이어 export("가입 인사", 40) 를 호출하면 가입 인사=40 을 출력합니다. useCsv(...) 복귀 뒤 csv 는 새 CsvExporter 참조이고 이전 text 는 null 로 비워집니다. 이어 export("로그인 구현", 50) 를 호출하면 로그인 구현,50 을 출력합니다. main 은 useText 에 new TextExporter() , useCsv 에 new CsvExporter() 를 전달합니다. 각 메서드는 전달받은 참조를 저장하고 다른 필드에 null 을 대입합니다. 여전히 구체 타입 필드 두 개입니다. 선택 메서드는 null 입력을 거부하지 않으므로 “항상 하나가 설정됨”을 보장하지 않습니다. 두 필드가 null인 채 export 를 호출하면 IllegalStateException("no exporter") 가 발생합니다. 이 경계는 원문 main 의 두 호출에는 나타나지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| Text 선택 useText가 text 참조를 두고 csv를 비웁니다. |
|
|
| CSV 선택 useCsv가 text를 비우고 csv 참조를 둡니다. |
|
|
| null 반례 별도 useText(null)은 둘 다 null을 만듭니다. |
|
|
구현을 교체할 때 반대 필드를 비우는 효과
useText와 useCsv는 다른 필드를 비우며 순차 선택 뒤 각 형식으로 출력하고 useText(null)은 둘 다 null이 됩니다. 완료 시점의 상태 변화와 동시 원자성 미보장을 유지합니다.
SingleSlotExporter의 main은 useText 후 text만 남겨 가입 인사=40을 출력하고, useCsv 후 text를 null로 비워 로그인 구현,50을 출력한다. 여전히 구체 타입 필드 두 개이며 null 입력을 거부하지 않는다.
새 구현을 선택하면서 이전 필드를 null로 비웁니다
SingleSlotExporter의 main에서 각 선택 메서드가 끝난 뒤, 다음 export 호출이 읽는 상태입니다.
메서드 복귀 뒤의 순차 상태
| 원문의 선택 호출 | 복귀 뒤 두 필드 | 뒤따르는 export의 출력 |
|---|---|---|
useText(...) | text: TextExporter 참조
| 가입 인사=40 |
useCsv(...) | csv: CsvExporter 참조
| 로그인 구현,50 |
useText(...)복귀 뒤text는 새TextExporter참조이고csv = null입니다. 이어export("가입 인사", 40)를 호출하면가입 인사=40을 출력합니다.useCsv(...)복귀 뒤csv는 새CsvExporter참조이고 이전text는null로 비워집니다. 이어export("로그인 구현", 50)를 호출하면로그인 구현,50을 출력합니다.
main은 useText에 new TextExporter(), useCsv에 new CsvExporter()를 전달합니다. 각 메서드는 전달받은 참조를 저장하고 다른 필드에 null을 대입합니다.
여전히 구체 타입 필드 두 개입니다. 선택 메서드는 null 입력을 거부하지 않으므로 “항상 하나가 설정됨”을 보장하지 않습니다. 두 필드가 null인 채 export를 호출하면 IllegalStateException("no exporter")가 발생합니다. 이 경계는 원문 main의 두 호출에는 나타나지 않습니다.
이 예의 조건
- 선택 메서드 정상 복귀 뒤 상태이며 동시 원자성 보장이 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 선택 메서드가 복귀한 순차 상태를 보여 준다. useText는 text 참조/csv null, useCsv는 text null/csv 참조로 바꾼다. 별도 반례 useText(null)은 둘 다 null이므로 뒤 export는 예외를 던진다. 동시 원자성을 보장하지 않는다. |
| 구분 | 내용 |
|---|---|
| 설명 | 선택 메서드가 복귀한 순차 상태를 보여 준다. useText는 text 참조/csv null, useCsv는 text null/csv 참조로 바꾼다. 별도 반례 useText(null)은 둘 다 null이므로 뒤 export는 예외를 던진다. 동시 원자성을 보장하지 않는다. |
| 전체 읽기 정보 | 구현을 교체할 때 반대 필드를 비우는 효과 구현을 교체할 때 반대 필드를 비우는 효과 SingleSlotExporter의 main은 useText 후 text만 남겨 가입 인사=40을 출력하고, useCsv 후 text를 null로 비워 로그인 구현,50을 출력한다. 여전히 구체 타입 필드 두 개이며 null 입력을 거부하지 않는다. 새 구현을 선택하면서 이전 필드를 null로 비웁니다 SingleSlotExporter 의 main 에서 각 선택 메서드가 끝난 뒤, 다음 export 호출이 읽는 상태입니다. SingleSlotExporter의 두 선택 호출 뒤 필드 상태와 실제 출력 원문의 선택 호출 복귀 뒤 두 필드 뒤따르는 export의 출력 useText(...) text : TextExporter 참조 csv = null 가입 인사=40 useCsv(...) csv : CsvExporter 참조 text = null 로그인 구현,50 useText(...) 복귀 뒤 text 는 새 TextExporter 참조이고 csv = null 입니다. 이어 export("가입 인사", 40) 를 호출하면 가입 인사=40 을 출력합니다. useCsv(...) 복귀 뒤 csv 는 새 CsvExporter 참조이고 이전 text 는 null 로 비워집니다. 이어 export("로그인 구현", 50) 를 호출하면 로그인 구현,50 을 출력합니다. main 은 useText 에 new TextExporter() , useCsv 에 new CsvExporter() 를 전달합니다. 각 메서드는 전달받은 참조를 저장하고 다른 필드에 null 을 대입합니다. 여전히 구체 타입 필드 두 개입니다. 선택 메서드는 null 입력을 거부하지 않으므로 “항상 하나가 설정됨”을 보장하지 않습니다. 두 필드가 null인 채 export 를 호출하면 IllegalStateException("no exporter") 가 발생합니다. 이 경계는 원문 main 의 두 호출에는 나타나지 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| Text 선택 useText가 text 참조를 두고 csv를 비웁니다. |
|
|
| CSV 선택 useCsv가 text를 비우고 csv 참조를 둡니다. |
|
|
| null 반례 별도 useText(null)은 둘 다 null을 만듭니다. |
|
|
한 칸을 공통 인터페이스 타입으로 만들면 구조 자체가 하나의 구현만 보관하도록 표현할 수 있습니다.
연습 문제
ConsoleNotifier와 SilentNotifier를 문자열 option으로 고르는 NotificationService를 작성하고, 새 EmailNotifier 추가 시 수정할 위치를 주석으로 표시하세요.
아직 인터페이스로 개선하지 말고 결합을 관찰합니다.
해설 보기
public final class ConcreteNotificationBranches {
public static void main(String[] args) {
NotificationService service = new NotificationService();
service.notify("console", "가입 인사");
service.notify("silent", "로그인 구현");
}
private static final class NotificationService {
private final ConsoleNotifier console = new ConsoleNotifier();
private final SilentNotifier silent = new SilentNotifier();
void notify(String option, String title) {
if (option.equals("console")) console.send(title);
else if (option.equals("silent")) silent.send(title);
else System.out.println("unknown");
}
}
private static final class ConsoleNotifier {
void send(String title) { System.out.println("console=" + title); }
}
private static final class SilentNotifier {
void send(String title) { System.out.println("silent"); }
}
}console=가입 인사
silentEmailNotifier를 추가하려면 서비스 필드와 if 분기를 모두 수정해야 합니다.
다음 절에서는 역할 인터페이스로 핵심 서비스를 닫습니다.