본문 바로가기

구체 구현 의존 비용

구현별 필드와 분기가 늘어나는 게시글 내보내기를 재현하고 역할을 도입하기 전 변경 지점을 구조적으로 측정합니다.

역할과 구현을 분리하지 않은 코드는 처음 한 구현만 있을 때 단순합니다.

문제는 두 번째·세 번째 구현이 추가될 때 나타납니다.

클라이언트가 각 구체 타입을 필드로 보관하고 종류를 분기하면 새 구현마다 기존 핵심 코드를 수정해야 합니다.


구현 교체와 잔존 상태

lab/ConcreteExporterSwitchBug.java
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 조건은 검사하지 않습니다. 설정 상태와 첫 분기의 우선순위를 실행 순서로 드러낼 수 있습니다.

두 exporter 필드의 조합과 먼저 선택되는 분기

ConcreteExporterSwitchBug의 export는 textExporter부터 검사한다. 두 필드가 모두 null이면 출력 없이 끝나고, 둘 다 참조를 담으면 CSV를 마지막에 설정했어도 TextExporter를 호출한다.

두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다

ConcreteExporterSwitchBug의 export는 textExporter를 먼저 검사합니다. 표는 두 필드의 null 여부에 따른 소스 분기를 비교합니다.

두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다 textExporter 참조가 있으면 Text를 호출하고 CSV조건은검사하지않는다. 없을때만csvExporter를검사한다. 두참조가모두null이면출력없이끝난다. 원본main은둘다참조가있으므로Text출력이다. 있음 없음 있음 없음 export 호출 원문은 두 필드 모두 참조 textExporter 참조 있음? Text 호출 가입 인사=40 CSV 조건은 검사 안 함 csvExporter 참조 있음? CSV 호출 가입 인사,40 둘 다 null 출력 없이 종료
두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다 textExporter 참조가 있으면 Text를 호출하고 CSV조건은검사하지않는다. 없을때만csvExporter를검사한다. 두참조가모두null이면출력없이끝난다. 원본main은둘다참조가있으므로Text출력이다. 있음 없음 있음 없음 export 호출 원문은 둘 다 참조 textExporter 참조 있음? Text 호출 가입 인사=40 CSV 조건은 검사 안 함 csvExporter 참조 있음? CSV 호출 가입 인사,40 둘 다 null 출력 없이 종료
ConcreteExporterSwitchBug의 두 필드 조합과 export 호출 결과
필드 조합textExportercsvExporter선택하는 실행
설정 없음nullnull두 조건 모두 거짓입니다. 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 둘 다 참조가 있습니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2textExporter 참조가 있으면 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
  • 첫 조건의 true가 뒤 조건 검사를 막습니다.
원 출력

Text로 가입 인사를 출력합니다.

  • 출력: 가입 인사=40
  • CSV 조건: 미검사
  • 두 필드가 있다는 것이 두 출력을 뜻하지 않습니다.
별도 부재 분기

Text가 없을 때만 CSV를 검사합니다.

  • CSV 있음: 가입 인사,40
  • 둘 다 null: 출력 없이 종료
  • 순서가 우선순위와 동작이 없는 경계를 만듭니다.

두 exporter 필드의 조합과 먼저 선택되는 분기

두 필드가 모두 참조를 가지면 첫 text 조건이 참이 되어 Text를 호출하고 CSV 조건은 검사하지 않습니다. 설정 상태와 첫 분기의 우선순위를 실행 순서로 드러낼 수 있습니다.

두 exporter 필드의 조합과 먼저 선택되는 분기

ConcreteExporterSwitchBug의 export는 textExporter부터 검사한다. 두 필드가 모두 null이면 출력 없이 끝나고, 둘 다 참조를 담으면 CSV를 마지막에 설정했어도 TextExporter를 호출한다.

두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다

ConcreteExporterSwitchBug의 export는 textExporter를 먼저 검사합니다. 표는 두 필드의 null 여부에 따른 소스 분기를 비교합니다.

두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다 textExporter 참조가 있으면 Text를 호출하고 CSV조건은검사하지않는다. 없을때만csvExporter를검사한다. 두참조가모두null이면출력없이끝난다. 원본main은둘다참조가있으므로Text출력이다. 있음 없음 있음 없음 export 호출 원문은 두 필드 모두 참조 textExporter 참조 있음? Text 호출 가입 인사=40 CSV 조건은 검사 안 함 csvExporter 참조 있음? CSV 호출 가입 인사,40 둘 다 null 출력 없이 종료
두 필드가 남아 있으면 먼저 검사하는 Text가 실행됩니다 textExporter 참조가 있으면 Text를 호출하고 CSV조건은검사하지않는다. 없을때만csvExporter를검사한다. 두참조가모두null이면출력없이끝난다. 원본main은둘다참조가있으므로Text출력이다. 있음 없음 있음 없음 export 호출 원문은 둘 다 참조 textExporter 참조 있음? Text 호출 가입 인사=40 CSV 조건은 검사 안 함 csvExporter 참조 있음? CSV 호출 가입 인사,40 둘 다 null 출력 없이 종료
ConcreteExporterSwitchBug의 두 필드 조합과 export 호출 결과
필드 조합textExportercsvExporter선택하는 실행
설정 없음nullnull두 조건 모두 거짓입니다. 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 둘 다 참조가 있습니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2textExporter 참조가 있으면 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
  • 첫 조건의 true가 뒤 조건 검사를 막습니다.
원 출력

Text로 가입 인사를 출력합니다.

  • 출력: 가입 인사=40
  • CSV 조건: 미검사
  • 두 필드가 있다는 것이 두 출력을 뜻하지 않습니다.
별도 부재 분기

Text가 없을 때만 CSV를 검사합니다.

  • CSV 있음: 가입 인사,40
  • 둘 다 null: 출력 없이 종료
  • 순서가 우선순위와 동작이 없는 경계를 만듭니다.

구현마다 필드와 setter를 추가하고 “현재 하나만 선택됨”이라는 규칙을 클라이언트 분기로 관리한 결과입니다.

새 JsonExporter를 추가하면 필드, setter, export 분기, 초기화 순서를 모두 수정해야 합니다.

구현 수가 늘수록 유효하지 않은 조합도 늘어납니다.


단일 구현의 단순성

src/SingleConcreteExporter.java
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로 바꾸려면 서비스 소스를 수정해야 합니다.

“새 구현 추가”가 “기존 클라이언트 수정”을 요구한다는 결합을 확인합니다.


다중 구현의 분기 비용

src/BranchedExportService.java
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
unknown

ExportService가 옵션 해석, 구현 생성, 공통 흐름, 구체 호출을 모두 압니다.

새 형식 추가 때 기존 if 사슬을 수정해야 하고 각 구현의 생성 비용도 서비스 수명과 결합됩니다.

테스트에서 특정 구현만 교체하기도 어렵습니다.


변경 축 분리의 기준

구체 의존을 발견했다고 즉시 인터페이스를 만들지 않고 다음을 확인합니다.

  • Text·CSV·JSON이 클라이언트 입장에서 같은 “내보내기” 역할인가?
  • 공통 입력과 성공·실패 계약을 정의할 수 있는가?
  • 새 형식 추가 빈도가 핵심 흐름 변경 빈도와 다른가?
  • 실행 중 구현을 교체해야 하는가?
  • 구현 선택 문자열을 해석하는 책임은 어디에 둘 것인가?

같은 역할이라면 ExportService는 공통 전후 흐름만 알고 포맷 동작은 인터페이스에 맡길 수 있습니다.

옵션 문자열에서 구현을 선택하는 공장이나 조립점은 여전히 새 구현을 알아야 할 수 있습니다.

다형성은 모든 변경을 없애는 것이 아니라 핵심 클라이언트에서 구현 변경을 격리합니다.


구성을 통한 구현 선택

src/SingleSlotExporter.java
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 호출이 읽는 상태입니다.

새 구현을 선택하면서 이전 필드를 null로 비웁니다 선택메서드가복귀한순차상태를보여준다. useText는text참조/csvnull,useCsv는textnull/csv참조로바꾼다. 별도반례useText(null)는둘다null이므로뒤export는예외다. 동시원자성보장이아니다. useText useCsv useText(null) 시작 상태 text = null / csv = null Text 선택 완료 text = Text 참조 csv = null 뒤의 export 가입 인사=40 CSV 선택 완료 text = null csv = CSV 참조 뒤의 export 로그인 구현,50 별도 반례 둘 다 null export → no exporter 예외 각 메서드가 끝난 뒤를 읽음 · 동시 원자성 보장 아님
새 구현을 선택하면서 이전 필드를 null로 비웁니다선택메서드가복귀한순차상태를보여준다. useText는text참조/csvnull,useCsv는textnull/csv참조로바꾼다. 별도반례useText(null)는둘다null이므로뒤export는예외다. 동시원자성보장이아니다.
시작 상태text = nullcsv = null
useText(...)useText(...)시작 상태 · text = null · csv = null → Text 선택 완료 · text = Text 참조 · csv = null
Text 선택 완료text = Text 참조csv = null
연결 관계Text 선택 완료 · text = Text 참조 · csv = null → 뒤의 export · 가입 인사=40
뒤의 export가입 인사=40
useCsv(...)useCsv(...)뒤의 export · 가입 인사=40 → CSV 선택 완료 · text = null · csv = CSV 참조
CSV 선택 완료text = nullcsv = CSV 참조
연결 관계CSV 선택 완료 · text = null · csv = CSV 참조 → 뒤의 export · 로그인 구현,50
뒤의 export로그인 구현,50
연결 관계뒤의 export · 로그인 구현,50 → 별도 반례 · useText(null) · 두 필드 null → 예외
별도 반례useText(null)두 필드 null → 예외

메서드 복귀 뒤의 순차 상태

SingleSlotExporter의 두 선택 호출 뒤 필드 상태와 실제 출력
원문의 선택 호출복귀 뒤 두 필드뒤따르는 export의 출력
useText(...)text: TextExporter 참조

csv = null

가입 인사=40
useCsv(...)csv: CsvExporter 참조

text = null

로그인 구현,50
  1. useText(...) 복귀 뒤

    text는 새 TextExporter 참조이고 csv = null입니다. 이어 export("가입 인사", 40)를 호출하면 가입 인사=40을 출력합니다.

  2. 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를 비웁니다.

  • text: Text 참조
  • csv: null, 뒤 출력 가입 인사=40
  • 이전 다른 전략을 남기지 않도록 선택이 두 필드를 갱신합니다.
CSV 선택

useCsv가 text를 비우고 csv 참조를 둡니다.

  • text: null
  • csv: CSV 참조, 로그인 구현,50
  • 새 선택이 이전 Text 우선 조건을 제거합니다.
null 반례

별도 useText(null)은 둘 다 null을 만듭니다.

  • 필드: null/null
  • export: no exporter 예외
  • 선택 API의 null 허용이 정상 내보내기를 보장하지 않습니다.

구현을 교체할 때 반대 필드를 비우는 효과

useText와 useCsv는 다른 필드를 비우며 순차 선택 뒤 각 형식으로 출력하고 useText(null)은 둘 다 null이 됩니다. 완료 시점의 상태 변화와 동시 원자성 미보장을 유지합니다.

구현을 교체할 때 반대 필드를 비우는 효과

SingleSlotExporter의 main은 useText 후 text만 남겨 가입 인사=40을 출력하고, useCsv 후 text를 null로 비워 로그인 구현,50을 출력한다. 여전히 구체 타입 필드 두 개이며 null 입력을 거부하지 않는다.

새 구현을 선택하면서 이전 필드를 null로 비웁니다

SingleSlotExporter의 main에서 각 선택 메서드가 끝난 뒤, 다음 export 호출이 읽는 상태입니다.

새 구현을 선택하면서 이전 필드를 null로 비웁니다 선택메서드가복귀한순차상태를보여준다. useText는text참조/csvnull,useCsv는textnull/csv참조로바꾼다. 별도반례useText(null)는둘다null이므로뒤export는예외다. 동시원자성보장이아니다. useText useCsv useText(null) 시작 상태 text = null / csv = null Text 선택 완료 text = Text 참조 csv = null 뒤의 export 가입 인사=40 CSV 선택 완료 text = null csv = CSV 참조 뒤의 export 로그인 구현,50 별도 반례 둘 다 null export → no exporter 예외 각 메서드가 끝난 뒤를 읽음 · 동시 원자성 보장 아님
새 구현을 선택하면서 이전 필드를 null로 비웁니다선택메서드가복귀한순차상태를보여준다. useText는text참조/csvnull,useCsv는textnull/csv참조로바꾼다. 별도반례useText(null)는둘다null이므로뒤export는예외다. 동시원자성보장이아니다.
시작 상태text = nullcsv = null
useText(...)useText(...)시작 상태 · text = null · csv = null → Text 선택 완료 · text = Text 참조 · csv = null
Text 선택 완료text = Text 참조csv = null
연결 관계Text 선택 완료 · text = Text 참조 · csv = null → 뒤의 export · 가입 인사=40
뒤의 export가입 인사=40
useCsv(...)useCsv(...)뒤의 export · 가입 인사=40 → CSV 선택 완료 · text = null · csv = CSV 참조
CSV 선택 완료text = nullcsv = CSV 참조
연결 관계CSV 선택 완료 · text = null · csv = CSV 참조 → 뒤의 export · 로그인 구현,50
뒤의 export로그인 구현,50
연결 관계뒤의 export · 로그인 구현,50 → 별도 반례 · useText(null) · 두 필드 null → 예외
별도 반례useText(null)두 필드 null → 예외

메서드 복귀 뒤의 순차 상태

SingleSlotExporter의 두 선택 호출 뒤 필드 상태와 실제 출력
원문의 선택 호출복귀 뒤 두 필드뒤따르는 export의 출력
useText(...)text: TextExporter 참조

csv = null

가입 인사=40
useCsv(...)csv: CsvExporter 참조

text = null

로그인 구현,50
  1. useText(...) 복귀 뒤

    text는 새 TextExporter 참조이고 csv = null입니다. 이어 export("가입 인사", 40)를 호출하면 가입 인사=40을 출력합니다.

  2. 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를 비웁니다.

  • text: Text 참조
  • csv: null, 뒤 출력 가입 인사=40
  • 이전 다른 전략을 남기지 않도록 선택이 두 필드를 갱신합니다.
CSV 선택

useCsv가 text를 비우고 csv 참조를 둡니다.

  • text: null
  • csv: CSV 참조, 로그인 구현,50
  • 새 선택이 이전 Text 우선 조건을 제거합니다.
null 반례

별도 useText(null)은 둘 다 null을 만듭니다.

  • 필드: null/null
  • export: no exporter 예외
  • 선택 API의 null 허용이 정상 내보내기를 보장하지 않습니다.

한 칸을 공통 인터페이스 타입으로 만들면 구조 자체가 하나의 구현만 보관하도록 표현할 수 있습니다.


연습 문제

ConsoleNotifier와 SilentNotifier를 문자열 option으로 고르는 NotificationService를 작성하고, 새 EmailNotifier 추가 시 수정할 위치를 주석으로 표시하세요.

아직 인터페이스로 개선하지 말고 결합을 관찰합니다.

해설 보기
src/ConcreteNotificationBranches.java
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=가입 인사
silent

EmailNotifier를 추가하려면 서비스 필드와 if 분기를 모두 수정해야 합니다.

다음 절에서는 역할 인터페이스로 핵심 서비스를 닫습니다.