역할과 구현의 분리
협력에서 역할과 구현을 구분하고 구체 클래스 의존이 대체 가능성과 변경 범위를 어떻게 제한하는지 게시글 알림으로 판단합니다.
좋은 객체 지향 설계는 클래스를 많이 만드는 것이 아니라 객체들이 책임을 나누고 메시지로 협력하게 하는 것입니다.
역할은 클라이언트가 기대하는 계약이고 구현은 그 계약을 실제로 수행하는 구체 객체입니다.
역할과 구현을 분리하면 클라이언트가 세부 구현을 몰라도 대체 가능한 객체와 협력할 수 있습니다.
전략은 같은 목적을 수행하는 여러 구현 가운데 실행할 하나를 객체로 전달하는 방식입니다.
다음 최소 예제에서 Notifier가 역할이고 ConsoleNotifier가 전략 구현입니다.
interface Notifier {
void send(String message);
}
class ConsoleNotifier implements Notifier {
public void send(String message) {
System.out.println(message);
}
}
Notifier notifier = new ConsoleNotifier();
notifier.send("게시글 공개");위 코드는 역할을 읽는 최소 조각입니다. 마지막 두 문장은 클래스 밖에 있으므로 일반 .java 파일에 그대로 저장하기보다 JShell에서 순서대로 입력해 확인합니다. 아래의 public final class 예제들은 파일 이름을 클래스 이름과 맞추면 독립 실행할 수 있습니다.
사용하는 쪽은 Notifier의 send만 알고 실제 출력 방식은 구현 객체가 결정합니다.
전략 패턴은 이 교체 지점을 의도적으로 설계한 형태이며 ch8-3에서 생성자 주입과 함께 완성합니다.
구체 타입과 구현 제한
public final class ConcreteNotifierDependency {
public static void main(String[] args) {
BoardService service = new BoardService(new SilentNotifier());
service.publish("가입 인사");
}
private static final class BoardService {
private final ConsoleNotifier notifier;
BoardService(ConsoleNotifier notifier) {
this.notifier = notifier;
}
void publish(String title) {
notifier.notify(title);
}
}
private static final class ConsoleNotifier {
void notify(String title) {
System.out.println("console=" + title);
}
}
private static final class SilentNotifier {
void notify(String title) {
System.out.println("silent");
}
}
}error: incompatible types: SilentNotifier cannot be converted to ConsoleNotifier두 알림 클래스에 우연히 같은 notify 메서드가 있어도 자바 타입 관계는 없습니다.
BoardService 생성자가 ConsoleNotifier라는 구현 타입을 요구하므로 SilentNotifier로 교체할 수 없습니다.
새 알림을 추가할 때 서비스 필드·생성자·호출 코드를 바꾸거나 타입별 분기를 넣게 됩니다.
공통 역할을 인터페이스로 정의하면 클라이언트가 구현 대신 계약에 의존합니다.
interface PostNotifier {
void notify(String title);
}ConsoleNotifier와 SilentNotifier가 역할을 구현하고 BoardService가 PostNotifier를 받으면 둘을 교체할 수 있습니다.
역할의 메시지 계약
알림 역할을 만드는 기준은 구현 클래스의 공통 메서드를 기계적으로 추출하는 것이 아닙니다.
BoardService가 공개 사실을 전달하기 위해 어떤 메시지를 필요로 하는지에서 시작합니다.
public final class RoleBasedNotification {
public static void main(String[] args) {
BoardService console = new BoardService(new ConsoleNotifier());
BoardService silent = new BoardService(new SilentNotifier());
console.publish("가입 인사");
silent.publish("로그인 구현");
}
private interface PostNotifier {
void notifyPublished(String title);
}
private static final class BoardService {
private final PostNotifier notifier;
BoardService(PostNotifier notifier) {
this.notifier = notifier;
}
void publish(String title) {
System.out.println("published=" + title);
notifier.notifyPublished(title);
}
}
private static final class ConsoleNotifier implements PostNotifier {
@Override public void notifyPublished(String title) {
System.out.println("console=" + title);
}
}
private static final class SilentNotifier implements PostNotifier {
@Override public void notifyPublished(String title) {
System.out.println("silent");
}
}
}published=가입 인사
console=가입 인사
published=로그인 구현
silentBoardService는 알림이 콘솔인지 무음인지 알지 못합니다.
notifyPublished 역할만 호출합니다.
구현 선택은 main 같은 조립점이 담당합니다.
생성자 계약이 바꾸는 알림 구현의 대체 가능성
구체 타입 예제의 컴파일 실패와 별도 Console/Silent 서비스의 published 출력→notify 호출을 비교합니다. 실행되지 않은 경로를 유지하면서 역할 계약의 실제 호출 결과를 대응 단계로 볼 수 있습니다.
구체 타입을 요구하는 ConcreteNotifierDependency는 SilentNotifier 인수를 컴파일할 수 없다. 별도 RoleBasedNotification은 PostNotifier를 요구하여 두 구현을 받으며, console과 silent 서비스가 각기 원문 메시지를 출력한다.
역할 계약을 받으면 두 알림 구현을 교체할 수 있습니다
ConcreteNotifierDependency와 RoleBasedNotification은 서로 다른 프로그램입니다.
| 비교 대상 | 생성자가 요구하는 타입 | 호출과 결과 |
|---|---|---|
| 구체 타입 예제
| ConsoleNotifier | 컴파일 실패
|
| 역할 예제
| PostNotifier
| console.publish("가입 인사") |
| 역할 예제
| PostNotifier
| silent.publish("로그인 구현") |
- 구체 타입 예제 ·
SilentNotifier전달생성자는
ConsoleNotifier를 요구합니다. 컴파일 실패로publish는 실행되지 않습니다. - 역할 예제 ·
ConsoleNotifier전달생성자는
PostNotifier를 받으며,notifyPublished(String title)계약을 사용합니다.console.publish("가입 인사")published=가입 인사 console=가입 인사 - 역할 예제 ·
SilentNotifier전달생성자는
PostNotifier를 받으며,notifyPublished(String title)계약을 사용합니다.silent.publish("로그인 구현")published=로그인 구현 silent
역할 예제의 main이 두 서비스에 구현을 각각 전달하고 console, silent 순서로 호출합니다. 각 서비스는 먼저 published=를 출력한 뒤 역할에 요청합니다. 이 예제에는 게시글 저장 코드가 없습니다.
이 예의 조건
- 구체 타입 컴파일 실패와 역할 예제는 별도 프로그램이며 저장 코드가 없습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 구체 타입 예제는 ConsoleNotifier를 요구하는 자리에 SilentNotifier를 전달하므로 실행 전에 컴파일이 실패한다. 별도의 역할 예제에서 console과 silent 서비스는 각각 published 출력 뒤 주입된 PostNotifier에 요청한다. 두 서비스 사이 화살표는 없으며 저장 코드도 없다. |
| 구분 | 내용 |
|---|---|
| 설명 | 구체 타입 예제는 ConsoleNotifier를 요구하는 자리에 SilentNotifier를 전달하므로 실행 전에 컴파일이 실패한다. 별도의 역할 예제에서 console과 silent 서비스는 각각 published 출력 뒤 주입된 PostNotifier에 요청한다. 두 서비스 사이 화살표는 없으며 저장 코드도 없다. |
| 전체 읽기 정보 | 생성자 계약이 바꾸는 알림 구현의 대체 가능성 생성자 계약이 바꾸는 알림 구현의 대체 가능성 구체 타입을 요구하는 ConcreteNotifierDependency는 SilentNotifier 인수를 컴파일할 수 없다. 별도 RoleBasedNotification은 PostNotifier를 요구하여 두 구현을 받으며, console과 silent 서비스가 각기 원문 메시지를 출력한다. 역할 계약을 받으면 두 알림 구현을 교체할 수 있습니다 ConcreteNotifierDependency 와 RoleBasedNotification 은 서로 다른 프로그램입니다. 두 프로그램의 BoardService 생성자 계약과 원문 호출 결과 비교 대상 생성자가 요구하는 타입 호출과 결과 구체 타입 예제 SilentNotifier 전달 ConsoleNotifier 컴파일 실패 SilentNotifier 는 요구 타입으로 변환할 수 없습니다. publish 는 실행되지 않습니다. 역할 예제 ConsoleNotifier 전달 PostNotifier notifyPublished(String title) 계약 console.publish("가입 인사") published=가입 인사 console=가입 인사 역할 예제 SilentNotifier 전달 PostNotifier notifyPublished(String title) 계약 silent.publish("로그인 구현") published=로그인 구현 silent 구체 타입 예제 · SilentNotifier 전달 생성자는 ConsoleNotifier 를 요구합니다. 컴파일 실패로 publish 는 실행되지 않습니다. 역할 예제 · ConsoleNotifier 전달 생성자는 PostNotifier 를 받으며, notifyPublished(String title) 계약을 사용합니다. console.publish("가입 인사") published=가입 인사 console=가입 인사 역할 예제 · SilentNotifier 전달 생성자는 PostNotifier 를 받으며, notifyPublished(String title) 계약을 사용합니다. silent.publish("로그인 구현") published=로그인 구현 silent 역할 예제의 main 이 두 서비스에 구현을 각각 전달하고 console , silent 순서로 호출합니다. 각 서비스는 먼저 published= 를 출력한 뒤 역할에 요청합니다. 이 예제에는 게시글 저장 코드가 없습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 구체 타입 거절 ConsoleNotifier 요구에 SilentNotifier를 전달합니다. |
|
|
| Console 역할 서비스 PostNotifier 계약 서비스가 published 뒤 요청을 위임합니다. |
|
|
| Silent 역할 서비스 별도 서비스가 로그인 구현을 같은 역할에 요청합니다. |
|
|
생성자 계약이 바꾸는 알림 구현의 대체 가능성
구체 타입 예제의 컴파일 실패와 별도 Console/Silent 서비스의 published 출력→notify 호출을 비교합니다. 실행되지 않은 경로를 유지하면서 역할 계약의 실제 호출 결과를 대응 단계로 볼 수 있습니다.
구체 타입을 요구하는 ConcreteNotifierDependency는 SilentNotifier 인수를 컴파일할 수 없다. 별도 RoleBasedNotification은 PostNotifier를 요구하여 두 구현을 받으며, console과 silent 서비스가 각기 원문 메시지를 출력한다.
역할 계약을 받으면 두 알림 구현을 교체할 수 있습니다
ConcreteNotifierDependency와 RoleBasedNotification은 서로 다른 프로그램입니다.
| 비교 대상 | 생성자가 요구하는 타입 | 호출과 결과 |
|---|---|---|
| 구체 타입 예제
| ConsoleNotifier | 컴파일 실패
|
| 역할 예제
| PostNotifier
| console.publish("가입 인사") |
| 역할 예제
| PostNotifier
| silent.publish("로그인 구현") |
- 구체 타입 예제 ·
SilentNotifier전달생성자는
ConsoleNotifier를 요구합니다. 컴파일 실패로publish는 실행되지 않습니다. - 역할 예제 ·
ConsoleNotifier전달생성자는
PostNotifier를 받으며,notifyPublished(String title)계약을 사용합니다.console.publish("가입 인사")published=가입 인사 console=가입 인사 - 역할 예제 ·
SilentNotifier전달생성자는
PostNotifier를 받으며,notifyPublished(String title)계약을 사용합니다.silent.publish("로그인 구현")published=로그인 구현 silent
역할 예제의 main이 두 서비스에 구현을 각각 전달하고 console, silent 순서로 호출합니다. 각 서비스는 먼저 published=를 출력한 뒤 역할에 요청합니다. 이 예제에는 게시글 저장 코드가 없습니다.
이 예의 조건
- 구체 타입 컴파일 실패와 역할 예제는 별도 프로그램이며 저장 코드가 없습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 구체 타입 예제는 ConsoleNotifier를 요구하는 자리에 SilentNotifier를 전달하므로 실행 전에 컴파일이 실패한다. 별도의 역할 예제에서 console과 silent 서비스는 각각 published 출력 뒤 주입된 PostNotifier에 요청한다. 두 서비스 사이 화살표는 없으며 저장 코드도 없다. |
| 구분 | 내용 |
|---|---|
| 설명 | 구체 타입 예제는 ConsoleNotifier를 요구하는 자리에 SilentNotifier를 전달하므로 실행 전에 컴파일이 실패한다. 별도의 역할 예제에서 console과 silent 서비스는 각각 published 출력 뒤 주입된 PostNotifier에 요청한다. 두 서비스 사이 화살표는 없으며 저장 코드도 없다. |
| 전체 읽기 정보 | 생성자 계약이 바꾸는 알림 구현의 대체 가능성 생성자 계약이 바꾸는 알림 구현의 대체 가능성 구체 타입을 요구하는 ConcreteNotifierDependency는 SilentNotifier 인수를 컴파일할 수 없다. 별도 RoleBasedNotification은 PostNotifier를 요구하여 두 구현을 받으며, console과 silent 서비스가 각기 원문 메시지를 출력한다. 역할 계약을 받으면 두 알림 구현을 교체할 수 있습니다 ConcreteNotifierDependency 와 RoleBasedNotification 은 서로 다른 프로그램입니다. 두 프로그램의 BoardService 생성자 계약과 원문 호출 결과 비교 대상 생성자가 요구하는 타입 호출과 결과 구체 타입 예제 SilentNotifier 전달 ConsoleNotifier 컴파일 실패 SilentNotifier 는 요구 타입으로 변환할 수 없습니다. publish 는 실행되지 않습니다. 역할 예제 ConsoleNotifier 전달 PostNotifier notifyPublished(String title) 계약 console.publish("가입 인사") published=가입 인사 console=가입 인사 역할 예제 SilentNotifier 전달 PostNotifier notifyPublished(String title) 계약 silent.publish("로그인 구현") published=로그인 구현 silent 구체 타입 예제 · SilentNotifier 전달 생성자는 ConsoleNotifier 를 요구합니다. 컴파일 실패로 publish 는 실행되지 않습니다. 역할 예제 · ConsoleNotifier 전달 생성자는 PostNotifier 를 받으며, notifyPublished(String title) 계약을 사용합니다. console.publish("가입 인사") published=가입 인사 console=가입 인사 역할 예제 · SilentNotifier 전달 생성자는 PostNotifier 를 받으며, notifyPublished(String title) 계약을 사용합니다. silent.publish("로그인 구현") published=로그인 구현 silent 역할 예제의 main 이 두 서비스에 구현을 각각 전달하고 console , silent 순서로 호출합니다. 각 서비스는 먼저 published= 를 출력한 뒤 역할에 요청합니다. 이 예제에는 게시글 저장 코드가 없습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 구체 타입 거절 ConsoleNotifier 요구에 SilentNotifier를 전달합니다. |
|
|
| Console 역할 서비스 PostNotifier 계약 서비스가 published 뒤 요청을 위임합니다. |
|
|
| Silent 역할 서비스 별도 서비스가 로그인 구현을 같은 역할에 요청합니다. |
|
|
서비스의 공개 메시지 출력 순서는 알림 구현과 분리됩니다. 이 예제의 published=는 콘솔 메시지이며 게시글을 배열이나 파일에 저장하지는 않습니다. 실제 저장과 알림의 순서는 다음 예제에서 따로 확인합니다.
역할과 구현의 협력
운전자는 자동차 역할의 시동·가속·정지 방법을 알고, K3나 Model3 같은 구현 내부 엔진 구조를 알 필요가 없습니다.
게시글에서도 클라이언트는 저장소의 add, 내보내기의 export, 알림의 notify 역할을 사용하고 구체 형식이나 전송 방식을 구현에 맡깁니다.
클라이언트 → 역할 계약 ← 구현 A / 구현 B / 구현 C역할이 안정적이면 구현을 추가하거나 교체해도 클라이언트의 핵심 흐름을 유지할 수 있습니다.
역할 자체가 자주 바뀌면 모든 구현과 클라이언트가 함께 영향을 받으므로 인터페이스를 만드는 것만으로 안정성이 생기지는 않습니다.
의미를 지키는 대체 가능성
PostNotifier 구현은 다음 의미를 지켜야 합니다.
- 공개 알림 요청을 받아 처리합니다.
- 알림 방식이 달라도 BoardService의 저장 상태를 임의로 바꾸지 않습니다.
- 한 구현으로 교체해도 서비스가 기대하는 성공·실패 계약을 유지합니다.
메서드 서명만 같아도 의미가 다르면 대체할 수 없습니다.
예를 들어 notifyPublished가 어떤 구현에서는 프로그램을 종료하고 다른 구현에서는 단순 출력한다면 클라이언트의 기대가 깨집니다.
인터페이스 이름, 매개변수, 반환값, 예외 정책이 역할 의미를 함께 표현해야 합니다.
다형성은 구현을 숨기는 기술이지만 모든 차이를 지울 수는 없습니다.
특정 알림만 지원하는 예약 시간이나 재시도 정책이 필요하면 공통 역할을 비대하게 만들지 말고 별도 능력 인터페이스나 구성 객체로 분리합니다.
객체 생성과 사용의 분리
public final class BoardApplicationComposition {
public static void main(String[] args) {
Board board = new Board(2);
PostNotifier notifier = new ConsoleNotifier();
BoardApplication app = new BoardApplication(board, notifier);
app.publish("가입 인사", 40);
app.publish("로그인 구현", 50);
System.out.println(board.summary());
}
private static final class BoardApplication {
private final Board board;
private final PostNotifier notifier;
BoardApplication(Board board, PostNotifier notifier) {
this.board = board;
this.notifier = notifier;
}
void publish(String title, int viewCount) {
if (board.add(title, viewCount)) notifier.notifyPublished(title);
}
}
private interface PostNotifier { void notifyPublished(String title); }
private static final class ConsoleNotifier implements PostNotifier {
@Override public void notifyPublished(String title) {
System.out.println("published=" + title);
}
}
private static final class Board {
private final Post[] entries;
private int size;
Board(int capacity) { entries = new Post[capacity]; }
boolean add(String title, int viewCount) {
if (size == entries.length || title.isBlank() || viewCount <= 0) return false;
entries[size++] = new Post(title, viewCount);
return true;
}
String summary() {
int total = 0;
for (int index = 0; index < size; index++) {
total += entries[index].viewCount();
}
return "count=" + size + ", total=" + total;
}
}
private record Post(String title, int viewCount) {}
}published=가입 인사
published=로그인 구현
count=2, total=90main이 구체 ConsoleNotifier를 생성하지만 BoardApplication은 PostNotifier 역할만 압니다.
구현 생성 책임과 사용 책임을 분리하면 조립점에서 정책을 교체할 수 있습니다.
예시
첫 호출은 배열의 0번 칸에 조회 수 40인 게시글을 넣고 size를 1로 늘린 뒤 알립니다. 두 번째 호출도 같은 순서로 처리하여 count=2, total=90이 됩니다. 그 뒤 app.publish("추가 인사", 30)을 호출하면 배열이 가득 차서 add가 false를 반환합니다. 알림 호출을 건너뛰므로 추가 출력은 없고 요약도 그대로입니다.
순서를 뒤집어 생각해 봅시다. 알림 구현이 예외를 던지더라도 add는 이미 성공했으므로 배열의 게시글은 남습니다. 이 예제에는 되돌리기나 재시도 코드가 없습니다. 저장과 알림의 두 작업을 하나의 성공으로 보장하는 설계는 별도의 실패 정책이 필요합니다.
직접 확인
세 번째 게시글을 추가하는 호출을 요약 출력 앞에 넣고, 알림 줄의 개수와 total을 먼저 예상한 뒤 실행해 보세요. false를 반환하는 분기에서 배열에 대입하는 문장까지 도달하는지로 설명하면 됩니다.
조립된 앱은 저장 성공 뒤에만 알림을 요청한다
Board.add 성공은 저장·size 증가 뒤 알림을 호출하고 거절은 알림을 건너뛰며 알림 예외도 앞선 저장을 되돌리지 않습니다. 실제 저장과 부수 효과의 전후 경계가 중요합니다.
BoardApplicationComposition의 main이 Board와 ConsoleNotifier를 생성하여 앱에 전달한다. 앱은 Board와 PostNotifier 필드로 협력하며, 실제 두 게시글을 저장한 뒤 각각 알림을 요청하고 최종 count=2, total=90을 출력한다.
배열 저장에 성공한 뒤 알림을 요청합니다
BoardApplicationComposition에서 main은 new Board(2)와 new ConsoleNotifier()를 만든 뒤 new BoardApplication(board, notifier)에 전달합니다.
앱의 필드 타입은 각각 Board와 PostNotifier입니다. publish는 board.add(title, viewCount)가 true를 반환한 뒤에만 notifier.notifyPublished(title)을 호출합니다.
| main의 호출 순서 | Board의 저장 | 알림 출력 |
|---|---|---|
app.publish("가입 인사", 40) | entries[0]에 제목 "가입 인사", 조회 수 40 저장 후 true 반환 | published=가입 인사 |
app.publish("로그인 구현", 50) | entries[1]에 제목 "로그인 구현", 조회 수 50 저장 후 true 반환 | published=로그인 구현 |
app.publish("가입 인사", 40)Board가entries[0]에 제목"가입 인사", 조회 수40을 저장하고true를 반환합니다.그 뒤
ConsoleNotifier가published=가입 인사를 출력합니다.app.publish("로그인 구현", 50)Board가entries[1]에 제목"로그인 구현", 조회 수50을 저장하고true를 반환합니다.그 뒤
ConsoleNotifier가published=로그인 구현을 출력합니다.
두 호출 뒤 main이 board.summary()의 결과 count=2, total=90을 출력합니다.
위 두 입력은 모두 저장에 성공합니다. add가 false이면 알림을 호출하지 않는다는 것은 소스의 if 조건이며, 이 main에서 거절 입력을 실행한 결과가 아닙니다.
이 예의 조건
- 알림 예외가 저장을 되돌리는 코드는 없다.
- notifyPublished는 저장 성공 뒤에만 호출된다.
- 원 main은 두 추가 호출로 count=2, total=90까지입니다. 마지막 장면은 본문의 확장 실습에서 같은 가득 찬 저장소에 추가 인사,30을 세 번째 요청하는 조건입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 조건 통과 시 배열에 저장하고 true 반환 뒤 알림을 요청한다. 거절이면 대입과 알림 호출을 건너뛴다. 두 정상 호출의 count는 2, total은 90. 알림 예외는 이미 저장한 상태를 되돌리지 않는다. |
| 13 | 2회 저장: count=2 |
| 14 | total=90 |
| 16 | 가득 참 · 빈 제목 · 조회≤0 |
| 구분 | 내용 |
|---|---|
| 설명 | 조건 통과 시 배열에 저장하고 true 반환 뒤 알림을 요청한다. 거절이면 대입과 알림 호출을 건너뛴다. 두 정상 호출의 count는 2, total은 90. 알림 예외는 이미 저장한 상태를 되돌리지 않는다. |
| 전체 읽기 정보 | 조립된 앱은 저장 성공 뒤에만 알림을 요청한다 조립된 앱은 저장 성공 뒤에만 알림을 요청한다 BoardApplicationComposition의 main이 Board와 ConsoleNotifier를 생성하여 앱에 전달한다. 앱은 Board와 PostNotifier 필드로 협력하며, 실제 두 게시글을 저장한 뒤 각각 알림을 요청하고 최종 count=2, total=90을 출력한다. 배열 저장에 성공한 뒤 알림을 요청합니다 BoardApplicationComposition 에서 main 은 new Board(2) 와 new ConsoleNotifier() 를 만든 뒤 new BoardApplication(board, notifier) 에 전달합니다. 앱의 필드 타입은 각각 Board 와 PostNotifier 입니다. publish 는 board.add(title, viewCount) 가 true 를 반환한 뒤에만 notifier.notifyPublished(title) 을 호출합니다. 원문의 두 publish 호출에서 저장이 먼저 완료되고 알림이 이어지는 관계 main의 호출 순서 Board의 저장 알림 출력 app.publish("가입 인사", 40) entries[0] 에 제목 "가입 인사" , 조회 수 40 저장 후 true 반환 published=가입 인사 app.publish("로그인 구현", 50) entries[1] 에 제목 "로그인 구현" , 조회 수 50 저장 후 true 반환 published=로그인 구현 app.publish("가입 인사", 40) Board 가 entries[0] 에 제목 "가입 인사" , 조회 수 40 을 저장하고 true 를 반환합니다. 그 뒤 ConsoleNotifier 가 published=가입 인사 를 출력합니다. app.publish("로그인 구현", 50) Board 가 entries[1] 에 제목 "로그인 구현" , 조회 수 50 을 저장하고 true 를 반환합니다. 그 뒤 ConsoleNotifier 가 published=로그인 구현 을 출력합니다. 두 호출 뒤 main 이 board.summary() 의 결과 count=2, total=90 을 출력합니다. 위 두 입력은 모두 저장에 성공합니다. add 가 false 이면 알림을 호출하지 않는다는 것은 소스의 if 조건이며, 이 main 에서 거절 입력을 실행한 결과가 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 저장 Board.add가 첫 슬롯에 조회 수가 40인 게시글을 저장합니다. |
|
|
| 첫 알림 성공을 확인한 application이 알림 역할을 호출합니다. |
|
|
| 둘째 성공 두 번째 저장과 알림이 같은 순서로 이어집니다. |
|
|
| 확장 실습의 세 번째 요청 본문 확장 실습에서 추가 인사,30을 같은 가득 찬 저장소에 세 번째로 요청합니다. |
|
|
조립된 앱은 저장 성공 뒤에만 알림을 요청한다
Board.add 성공은 저장·size 증가 뒤 알림을 호출하고 거절은 알림을 건너뛰며 알림 예외도 앞선 저장을 되돌리지 않습니다. 실제 저장과 부수 효과의 전후 경계가 중요합니다.
BoardApplicationComposition의 main이 Board와 ConsoleNotifier를 생성하여 앱에 전달한다. 앱은 Board와 PostNotifier 필드로 협력하며, 실제 두 게시글을 저장한 뒤 각각 알림을 요청하고 최종 count=2, total=90을 출력한다.
배열 저장에 성공한 뒤 알림을 요청합니다
BoardApplicationComposition에서 main은 new Board(2)와 new ConsoleNotifier()를 만든 뒤 new BoardApplication(board, notifier)에 전달합니다.
앱의 필드 타입은 각각 Board와 PostNotifier입니다. publish는 board.add(title, viewCount)가 true를 반환한 뒤에만 notifier.notifyPublished(title)을 호출합니다.
| main의 호출 순서 | Board의 저장 | 알림 출력 |
|---|---|---|
app.publish("가입 인사", 40) | entries[0]에 제목 "가입 인사", 조회 수 40 저장 후 true 반환 | published=가입 인사 |
app.publish("로그인 구현", 50) | entries[1]에 제목 "로그인 구현", 조회 수 50 저장 후 true 반환 | published=로그인 구현 |
app.publish("가입 인사", 40)Board가entries[0]에 제목"가입 인사", 조회 수40을 저장하고true를 반환합니다.그 뒤
ConsoleNotifier가published=가입 인사를 출력합니다.app.publish("로그인 구현", 50)Board가entries[1]에 제목"로그인 구현", 조회 수50을 저장하고true를 반환합니다.그 뒤
ConsoleNotifier가published=로그인 구현을 출력합니다.
두 호출 뒤 main이 board.summary()의 결과 count=2, total=90을 출력합니다.
위 두 입력은 모두 저장에 성공합니다. add가 false이면 알림을 호출하지 않는다는 것은 소스의 if 조건이며, 이 main에서 거절 입력을 실행한 결과가 아닙니다.
이 예의 조건
- 알림 예외가 저장을 되돌리는 코드는 없다.
- notifyPublished는 저장 성공 뒤에만 호출된다.
- 원 main은 두 추가 호출로 count=2, total=90까지입니다. 마지막 장면은 본문의 확장 실습에서 같은 가득 찬 저장소에 추가 인사,30을 세 번째 요청하는 조건입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 조건 통과 시 배열에 저장하고 true 반환 뒤 알림을 요청한다. 거절이면 대입과 알림 호출을 건너뛴다. 두 정상 호출의 count는 2, total은 90. 알림 예외는 이미 저장한 상태를 되돌리지 않는다. |
| 13 | 2회 저장: count=2 |
| 14 | total=90 |
| 16 | 가득 참 · 빈 제목 · 조회≤0 |
| 구분 | 내용 |
|---|---|
| 설명 | 조건 통과 시 배열에 저장하고 true 반환 뒤 알림을 요청한다. 거절이면 대입과 알림 호출을 건너뛴다. 두 정상 호출의 count는 2, total은 90. 알림 예외는 이미 저장한 상태를 되돌리지 않는다. |
| 전체 읽기 정보 | 조립된 앱은 저장 성공 뒤에만 알림을 요청한다 조립된 앱은 저장 성공 뒤에만 알림을 요청한다 BoardApplicationComposition의 main이 Board와 ConsoleNotifier를 생성하여 앱에 전달한다. 앱은 Board와 PostNotifier 필드로 협력하며, 실제 두 게시글을 저장한 뒤 각각 알림을 요청하고 최종 count=2, total=90을 출력한다. 배열 저장에 성공한 뒤 알림을 요청합니다 BoardApplicationComposition 에서 main 은 new Board(2) 와 new ConsoleNotifier() 를 만든 뒤 new BoardApplication(board, notifier) 에 전달합니다. 앱의 필드 타입은 각각 Board 와 PostNotifier 입니다. publish 는 board.add(title, viewCount) 가 true 를 반환한 뒤에만 notifier.notifyPublished(title) 을 호출합니다. 원문의 두 publish 호출에서 저장이 먼저 완료되고 알림이 이어지는 관계 main의 호출 순서 Board의 저장 알림 출력 app.publish("가입 인사", 40) entries[0] 에 제목 "가입 인사" , 조회 수 40 저장 후 true 반환 published=가입 인사 app.publish("로그인 구현", 50) entries[1] 에 제목 "로그인 구현" , 조회 수 50 저장 후 true 반환 published=로그인 구현 app.publish("가입 인사", 40) Board 가 entries[0] 에 제목 "가입 인사" , 조회 수 40 을 저장하고 true 를 반환합니다. 그 뒤 ConsoleNotifier 가 published=가입 인사 를 출력합니다. app.publish("로그인 구현", 50) Board 가 entries[1] 에 제목 "로그인 구현" , 조회 수 50 을 저장하고 true 를 반환합니다. 그 뒤 ConsoleNotifier 가 published=로그인 구현 을 출력합니다. 두 호출 뒤 main 이 board.summary() 의 결과 count=2, total=90 을 출력합니다. 위 두 입력은 모두 저장에 성공합니다. add 가 false 이면 알림을 호출하지 않는다는 것은 소스의 if 조건이며, 이 main 에서 거절 입력을 실행한 결과가 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 첫 저장 Board.add가 첫 슬롯에 조회 수가 40인 게시글을 저장합니다. |
|
|
| 첫 알림 성공을 확인한 application이 알림 역할을 호출합니다. |
|
|
| 둘째 성공 두 번째 저장과 알림이 같은 순서로 이어집니다. |
|
|
| 확장 실습의 세 번째 요청 본문 확장 실습에서 추가 인사,30을 같은 가득 찬 저장소에 세 번째로 요청합니다. |
|
|
연습 문제
PostFormatter 역할과 TextFormatter, CsvFormatter 구현을 만들고 같은 print(PostFormatter) 함수에 전달하세요.
제목이 가입 인사이고 조회 수가 40인 게시글을 서로 다른 문자열로 표현합니다.
해설 보기
public final class FormatterRole {
public static void main(String[] args) {
Post entry = new Post("가입 인사", 40);
print(new TextFormatter(), entry);
print(new CsvFormatter(), entry);
}
private static void print(PostFormatter formatter, Post entry) {
System.out.println(formatter.format(entry));
}
private interface PostFormatter { String format(Post entry); }
private record Post(String title, int viewCount) {}
private static final class TextFormatter implements PostFormatter {
@Override public String format(Post entry) {
return entry.title() + "=" + entry.viewCount();
}
}
private static final class CsvFormatter implements PostFormatter {
@Override public String format(Post entry) {
return entry.title() + "," + entry.viewCount();
}
}
}가입 인사=40
가입 인사,40print는 역할만 사용하고 형식 차이는 구현이 책임집니다.
새 형식도 같은 계약으로 전달할 수 있습니다.