"객체지향의 본질은 협력하는 객체들의 공동체를 창조하는 것이다"
- 협력(Collaboration): 객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용
- 책임(Responsibility): 객체가 협력에 참여하기 위해 수행하는 행동
- 역할(Role): 협력 안에서 객체가 수행하는 책임의 집합
- 메시지(Message): 협력을 위한 유일한 커뮤니케이션 수단
- 정보 전문가(Information Expert): 책임을 수행하는 데 필요한 정보를 가장 잘 알고 있는 객체
- 협력이 객체 설계의 문맥(context)이 됨을 이해하기
- 책임 주도 설계(RDD)의 과정 익히기
- 메시지가 객체를 결정하는 이유 이해하기
- 행동이 상태를 결정해야 하는 이유 파악하기
- 역할을 통한 유연하고 재사용 가능한 협력 설계하기
이 챕터는 **구현(implementation)**이 아닌 **개념(concept)**과 **설계 원칙(design principle)**에 집중합니다.
❌ 이 챕터에서 다루지 않는 것:
- 구체적인 코드 예제
- 클래스 구현 방법
- 상속과 다형성 구현 기법
✅ 이 챕터에서 다루는 것:
- 왜 협력이 중요한가?
- 어떻게 책임을 할당하는가?
- 역할은 무엇이며 왜 필요한가?
- 객체지향 설계의 사고 과정
구현 메커니즘 (2차적):
├─ 클래스
├─ 상속
├─ 다형성
└─ 인터페이스
객체지향의 본질 (1차적):
├─ 협력
├─ 책임
└─ 역할
핵심:
클래스와 상속은 객체들의 책임과 협력이 어느 정도 자리를 잡은 후에 사용할 수 있는 구현 메커니즘일 뿐이다.
협력 = 객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용
┌─────────┐ 메시지 ┌───────────┐ 메시지 ┌─────────┐
│ 객체 A │ ────────→ │ 객체 B │ ────────→ │ 객체 C │
└─────────┘ └───────────┘ └─────────┘
↑ │
│ 메시지 │
└──────────────────────────────────────────────┘
협력의 특징:
- 메시지 전송을 통해서만 협력 가능
- **요청(request)**과 **응답(response)**의 흐름
- 객체들 사이의 수평적 관계 (계층 구조 X)
Chapter 02에서 본 영화 예매 시스템을 협력의 관점에서 다시 봅시다.
사용자
│
│ "2명 예매해줘"
↓
┌─────────┐
│Screening│
└─────────┘
│
│ "1인당 요금 계산해줘"
↓
┌─────────┐
│ Movie │
└─────────┘
│
│ "할인 금액 계산해줘"
↓
┌───────────────┐
│DiscountPolicy │
└───────────────┘
│
│ "조건 만족해?"
↓
┌───────────────────┐
│DiscountCondition │
└───────────────────┘
│
│ true/false
↓
┌───────────────┐
│DiscountPolicy │
└───────────────┘
│
│ 할인 금액 (Money)
↓
┌─────────┐
│ Movie │
└─────────┘
│
│ 1인당 요금 (Money)
↓
┌─────────┐
│Screening│
└─────────┘
│
│ Reservation 객체
↓
사용자
// 1. Screening이 협력 시작
public class Screening {
public Reservation reserve(Customer customer, int audienceCount) {
// 2. Movie에게 협력 요청 (메시지 전송)
return new Reservation(
customer,
this,
calculateFee(audienceCount),
audienceCount
);
}
private Money calculateFee(int audienceCount) {
// Movie에게 "요금 계산해줘" 메시지 전송
return movie.calculateMovieFee(this).times(audienceCount);
}
}
// 3. Movie가 응답하기 위해 DiscountPolicy에게 협력 요청
public class Movie {
public Money calculateMovieFee(Screening screening) {
// DiscountPolicy에게 "할인 금액 계산해줘" 메시지 전송
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
// 4. DiscountPolicy가 응답하기 위해 DiscountCondition에게 협력 요청
public class DiscountPolicy {
public Money calculateDiscountAmount(Screening screening) {
for(DiscountCondition each : conditions) {
// DiscountCondition에게 "조건 만족해?" 메시지 전송
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
}public class Movie {
// ❓ 왜 play() 메서드가 없을까?
// public void play() { ... }
// ✅ 대신 이런 메서드가 있다
public Money calculateMovieFee(Screening screening) { ... }
}답변: Movie는 **"영화 예매"**라는 협력에 참여하기 때문
영화 재생 협력에 참여하는 Movie:
class Movie {
void play(); // ✅
void pause(); // ✅
void stop(); // ✅
}
영화 예매 협력에 참여하는 Movie:
class Movie {
Money calculateFee(); // ✅
Duration getRuntime(); // ✅
}
핵심:
협력이 객체가 어떤 행동을 가져야 하는지 결정한다. 객체를 단독으로 보지 말고, **"이 객체가 어떤 협력에 참여하는가?"**를 봐야 한다.
1. 협력이 결정된다
"영화를 예매한다"
2. 협력에 필요한 행동이 결정된다
Movie: "요금을 계산한다"
3. 행동에 필요한 상태가 결정된다
Movie: fee(기본 요금), discountPolicy(할인 정책)
잘못된 순서:
// ❌ 상태부터 정의
class Movie {
String title;
int runningTime;
// 이제 이걸로 뭘 하지?
}올바른 순서:
// ✅ 협력 → 행동 → 상태
// 1. 협력: "영화 예매"
// 2. 행동: "요금 계산"
// 3. 상태: 요금 계산에 필요한 것들
class Movie {
private Money fee; // 행동을 위해 필요
private DiscountPolicy policy; // 행동을 위해 필요
public Money calculateMovieFee() {
// 행동을 정의
}
}// ❌ 자율적이지 않은 객체
public class Screening {
public Reservation reserve(Customer customer, int audienceCount) {
// Screening이 Movie의 일까지 다 함
Money fee = movie.getFee();
Money discount = movie.getDiscountPolicy()
.calculateDiscountAmount(this);
Money finalFee = fee.minus(discount).times(audienceCount);
return new Reservation(customer, this, finalFee, audienceCount);
}
}
// ✅ 자율적인 객체
public class Screening {
public Reservation reserve(Customer customer, int audienceCount) {
// Movie에게 "알아서 계산해줘" (위임)
return new Reservation(
customer,
this,
calculateFee(audienceCount),
audienceCount
);
}
private Money calculateFee(int audienceCount) {
// Movie가 어떻게 계산하는지 모름!
return movie.calculateMovieFee(this).times(audienceCount);
}
}자율적인 객체의 조건:
- 자신의 일은 스스로 처리
- 외부에서 내부 구현을 알 수 없음
- 메시지만 전송하고 방법은 객체가 선택
책임 = 객체가 협력에 참여하기 위해 수행하는 행동
책임은 추상적인 개념
메시지는 구체적인 구현
1️⃣ 하는 것 (Doing)
- 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
예시:
Screening의 "하는 것" 책임:
✓ 예매를 생성한다
✓ Movie에게 요금 계산을 시작시킨다
Movie의 "하는 것" 책임:
✓ 요금을 계산한다
✓ DiscountPolicy에게 할인 계산을 시작시킨다
2️⃣ 아는 것 (Knowing)
- 사적인 정보에 관해 아는 것
- 관련된 객체에 관해 아는 것
- 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
예시:
Screening의 "아는 것" 책임:
✓ 상영할 영화를 안다 (movie)
✓ 상영 순번을 안다 (sequence)
✓ 상영 시간을 안다 (whenScreened)
Movie의 "아는 것" 책임:
✓ 기본 요금을 안다 (fee)
✓ 할인 정책을 안다 (discountPolicy)
책임 (추상적, 개념적):
"예매를 생성한다"
"요금을 계산한다"
"할인 금액을 계산한다"
메시지 (구체적, 구현):
reserve(Customer, int)
calculateMovieFee(Screening)
calculateDiscountAmount(Screening)
중요:
책임 ≠ 메시지
하나의 책임이 여러 메시지로 구현될 수 있고,
하나의 메시지가 여러 책임을 포함할 수도 있다.
책임을 수행하는 데 필요한 정보를 가장 잘 알고 있는 전문가에게 그 책임을 할당하라
🎯 Step 1: 시스템 책임 파악
시스템 책임: "영화를 예매한다"
┌────────────────────────────┐
│ 영화 예매 시스템 │
│ │
│ 책임: 영화를 예매한다 │
└────────────────────────────┘
🎯 Step 2: 첫 번째 메시지 선택
질문: "영화를 예매하려면 어떤 메시지가 필요한가?"
답변: "예매하라" (reserve)
┌─────────────┐
│ 메시지: │
│ "예매하라" │
└─────────────┘
🎯 Step 3: 정보 전문가 찾기
질문: "예매를 생성하려면 무엇을 알아야 하는가?"
답변:
- 상영 시간
- 상영할 영화
- 상영 순번
질문: "이 정보를 가장 잘 알고 있는 객체는?"
답변: Screening!
┌─────────────┐
│ Screening │
│ │
│ ✓ movie │
│ ✓ sequence │
│ ✓ whenScre │
└─────────────┘
🎯 Step 4: 책임 할당
Screening에게 "예매하라" 책임 할당!
┌─────────────────────────────┐
│ Screening │
│ │
│ 책임: 예매를 생성한다 │
│ 메시지: reserve() │
└─────────────────────────────┘
🎯 Step 5: 새로운 책임 발견
Screening이 예매를 생성하려면...
→ 예매 가격을 알아야 함
→ Screening은 가격 계산 정보가 부족
새로운 메시지 필요: "가격을 계산하라"
🎯 Step 6: 다시 정보 전문가 찾기
질문: "가격을 계산하려면 무엇을 알아야 하는가?"
답변:
- 기본 요금
- 할인 정책
질문: "이 정보를 가장 잘 알고 있는 객체는?"
답변: Movie!
┌─────────────┐
│ Movie │
│ │
│ ✓ fee │
│ ✓ discount │
│ Policy │
└─────────────┘
🎯 Step 7: 책임 할당 (계속)
Movie에게 "가격을 계산하라" 책임 할당!
┌─────────────────────────────┐
│ Movie │
│ │
│ 책임: 요금을 계산한다 │
│ 메시지: calculateMovieFee() │
└─────────────────────────────┘
🎯 Step 8: 협력 완성
이런 식으로 계속...
1. 필요한 메시지 식별
2. 정보 전문가 찾기
3. 책임 할당
4. 새로운 필요 발견
5. 반복...
최종 결과:
Screening → Movie → DiscountPolicy → DiscountCondition
┌─────────────────────────────────────────────────────────┐
│ 1. 시스템 책임 │
│ "영화를 예매한다" │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. 메시지: "예매하라" │
│ 정보 전문가: Screening │
│ (상영 정보를 가장 잘 알고 있음) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. Screening의 새로운 필요 │
│ "가격을 계산해야 해!" │
│ → 메시지: "가격을 계산하라" │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. 메시지: "가격을 계산하라" │
│ 정보 전문가: Movie │
│ (요금과 할인 정책을 가장 잘 알고 있음) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. Movie의 새로운 필요 │
│ "할인 금액을 알아야 해!" │
│ → 메시지: "할인 금액을 계산하라" │
└─────────────────────────────────────────────────────────┘
↓
...
책임을 찾고, 책임을 수행할 적절한 객체를 찾아 할당하는 방식으로 협력을 설계하는 방법
1단계: 시스템이 사용자에게 제공해야 하는 기능(시스템 책임)을 파악한다
↓
2단계: 시스템 책임을 더 작은 책임으로 분할한다
↓
3단계: 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아
책임을 할당한다
↓
4단계: 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우
이를 책임질 적절한 객체 또는 역할을 찾는다
↓
5단계: 해당 객체 또는 역할에게 책임을 할당함으로써
두 객체가 협력하게 한다
↓
(2단계로 돌아가 반복)
시나리오: 사용자가 상품을 주문한다
"상품을 주문한다"
메시지: "주문하라" (placeOrder)
정보 전문가는?
- 주문하려면 사용자, 상품, 수량을 알아야 함
- 이 정보를 가장 잘 아는 것은? → Order
┌─────────────┐
│ Order │
│ │
│ 책임: │
│ 주문을 생성 │
└─────────────┘
Order가 주문을 생성하려면...
→ 재고를 확인해야 함
→ Order는 재고 정보가 없음
새로운 메시지: "재고를 확인하라"
정보 전문가는?
- 재고를 가장 잘 아는 것은? → Product
┌─────────────┐
│ Product │
│ │
│ 책임: │
│ 재고 확인 │
└─────────────┘
Order → Product: "재고 확인"
Product → Stock: "재고 차감"
Order → Payment: "결제 처리"
...
최종 협력:
User → Order → Product → Stock
↓
Payment
// 1. 먼저 객체를 만듦
class OrderProcessor {
void process() { ... }
void validate() { ... }
void save() { ... }
void notify() { ... }
// ... 계속 메서드 추가
}
// 2. 나중에 필요한 메시지를 찾음
// → 불필요한 메서드가 많아짐
// → 인터페이스가 비대해짐1. 필요한 메시지 식별
"주문을 처리하라"
2. 이 메시지를 처리할 객체 선택
OrderProcessor
3. 다음 메시지 식별
"재고를 확인하라"
4. 이 메시지를 처리할 객체 선택
InventoryChecker
→ 꼭 필요한 메시지만 가진 객체
→ 최소한의 인터페이스
1️⃣ 최소한의 인터페이스
// 메시지를 먼저 선택하면
class Movie {
// 정말 필요한 것만
Money calculateMovieFee(Screening screening);
}
// 객체를 먼저 만들면
class Movie {
// 혹시 몰라서 다 만듦
Money calculateMovieFee(Screening screening);
Money getOriginalFee();
Money getDiscountedFee();
Money calculateFeeWithTax();
Money calculateFeeWithoutTax();
// ...
}2️⃣ 추상적인 인터페이스
// 메시지 중심 (What)
class Movie {
Money calculateMovieFee(Screening screening); // ✅ 무엇을
}
// 구현 중심 (How)
class Movie {
Money getFeeAfterSubtractingDiscountAmount(Screening screening); // ❌ 어떻게
}// 1단계: 상태부터 정의
class Movie {
private String title;
private Duration runningTime;
private Money fee;
private String genre;
private String director;
// ... 더 많은 상태
}
// 2단계: getter/setter 기계적으로 생성
class Movie {
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public Duration getRunningTime() { return runningTime; }
public void setRunningTime(Duration rt) { this.runningTime = rt; }
// ...
}
// 3단계: 이제 뭘 하지?
// → 내부 구현이 모두 노출됨
// → 캡슐화 위반문제점:
1. 상태가 노출되어 캡슐화 깨짐
2. 객체가 수동적이 됨 (다른 객체가 조작)
3. 객체의 존재 이유가 불명확
// 1단계: 협력 정의
// "영화 예매를 위한 협력"
// 2단계: 필요한 행동 정의
// Movie는 "요금을 계산한다"
// 3단계: 행동 구현
class Movie {
public Money calculateMovieFee(Screening screening) {
// 행동을 구현하기 위해 필요한 상태만 추가
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
// 4단계: 필요한 상태 추가 (행동을 위해)
private Money fee;
private DiscountPolicy discountPolicy;
}장점:
1. 캡슐화 유지 (상태 숨김)
2. 객체가 능동적 (스스로 처리)
3. 객체의 존재 이유 명확 (협력을 위해)
┌──────────────────────────────────────┐
│ 협력을 식별한다 │
│ "영화를 예매한다" │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 필요한 메시지를 결정한다 │
│ "예매하라" │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 정보 전문가를 찾는다 │
│ Screening이 상영 정보 전문가 │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 책임을 할당한다 │
│ Screening.reserve() │
└──────────────────────────────────────┘
↓
┌──────────────────────────────────────┐
│ 필요한 상태를 결정한다 │
│ movie, sequence, whenScreened │
└──────────────────────────────────────┘
↓
(반복...)
역할 = 협력 안에서 객체가 수행하는 책임의 집합
┌─────────────────────────────┐
│ 역할 │
│ │
│ 책임 1: ... │
│ 책임 2: ... │
│ 책임 3: ... │
│ │
│ ↑ 여러 객체가 교대로 │
│ 이 역할을 수행 가능 │
└─────────────────────────────┘
↑ ↑ ↑
│ │ │
객체 A 객체 B 객체 C
질문: "할인 금액을 계산하라" 메시지에 응답할 객체는?
답변 1: AmountDiscountPolicy (금액 할인)
답변 2: PercentDiscountPolicy (비율 할인)
❓ 두 가지 협력을 따로 만들어야 하나?
협력 1: 금액 할인
Movie ────→ AmountDiscountPolicy ────→ SequenceCondition
└───→ PeriodCondition
협력 2: 비율 할인
Movie ────→ PercentDiscountPolicy ────→ SequenceCondition
└───→ PeriodCondition
문제점:
1. 협력이 중복됨
2. Movie 코드가 중복됨
3. 새로운 할인 정책마다 새로운 협력 필요
4. 유연하지 않음
핵심 통찰:
AmountDiscountPolicy와 PercentDiscountPolicy 모두
"할인 금액을 계산한다"는 동일한 책임을 수행
→ 이 책임을 역할로 추상화!
역할 이름: DiscountPolicy
┌──────────────────┐
│ DiscountPolicy │ ← 역할 (추상화)
│ (역할) │
│ │
│ 책임: │
│ 할인 금액 계산 │
└──────────────────┘
△
│
┌───────┴───────┐
│ │
┌────────────────┐ ┌────────────────┐
│ Amount │ │ Percent │ ← 객체들
│ DiscountPolicy │ │ DiscountPolicy │
└────────────────┘ └────────────────┘
통합된 협력:
Movie ────→ DiscountPolicy ────→ DiscountCondition
(역할)
실행 시점에:
- AmountDiscountPolicy가 DiscountPolicy 역할 수행
- PercentDiscountPolicy가 DiscountPolicy 역할 수행
- NoneDiscountPolicy가 DiscountPolicy 역할 수행
장점:
1. ✅ 협력 하나로 통합
2. ✅ 중복 코드 제거
3. ✅ 새로운 할인 정책 추가 쉬움
4. ✅ 유연한 설계
// 1. 인터페이스로 역할 정의
public interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
// 2. 구체적인 객체들이 역할을 구현
public class AmountDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
// 금액 할인 구현
}
}
public class PercentDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
// 비율 할인 구현
}
}
// 3. 역할에 의존하는 협력
public class Movie {
private DiscountPolicy discountPolicy; // 역할에 의존!
public Money calculateMovieFee(Screening screening) {
// 어떤 객체든 DiscountPolicy 역할을 수행하면 OK
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}┌─────────────────────────────────────┐
│ 이 책임을 수행하는 대상이 │
│ 몇 종류인가? │
└─────────────────────────────────────┘
│
├─── 1종류 → 객체로 시작
│ (나중에 필요하면 역할로)
│
└─── 2종류 이상 → 역할로 시작
// Screening은 한 종류만 있음
public class Screening {
public Reservation reserve(Customer customer, int audienceCount) {
// ...
}
}
// 역할로 만들 필요 없음!// DiscountPolicy는 여러 종류가 있음
// → 역할로 정의!
interface DiscountPolicy { }
class AmountDiscountPolicy implements DiscountPolicy { }
class PercentDiscountPolicy implements DiscountPolicy { }
class NoneDiscountPolicy implements DiscountPolicy { }1단계: 구체적인 객체로 시작
Movie ────→ AmountDiscountPolicy
2단계: 또 다른 객체 필요 발견
Movie ────→ PercentDiscountPolicy
3단계: 중복 발견
"두 객체가 같은 책임을 수행하네?"
4단계: 공통 책임 추출 → 역할 탄생!
┌──────────────┐
│DiscountPolicy│ ← 역할
└──────────────┘
△
┌──────┴──────┐
Amount Percent
5단계: 역할 기반으로 협력 재설계
Movie ────→ DiscountPolicy (역할)
1️⃣ 상위 수준 정책 표현
구체적 (추상화 전):
"Movie는 AmountDiscountPolicy에게 할인 금액 계산을 요청하고,
그 결과를 기본 요금에서 뺀다"
추상적 (추상화 후):
"Movie는 DiscountPolicy에게 할인 금액 계산을 요청하고,
그 결과를 기본 요금에서 뺀다"
→ 세부사항(금액 할인인지, 비율 할인인지)을 감춤
→ 본질(할인 정책 적용)에 집중
2️⃣ 유연한 설계
// 새로운 할인 정책 추가
public class SeasonDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
// 계절별 할인 구현
}
}
// Movie 코드 변경 필요 없음!
Movie movie = new Movie(
"아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new SeasonDiscountPolicy(...) // ✅ 그냥 사용 가능
);연극: "햄릿"
배역: 햄릿
- 배역은 연극이 상영되는 동안만 존재
- 서로 다른 배우가 같은 배역 연기 가능
- 한 배우가 여러 연극에서 다른 배역 연기 가능
배우 A ────→ 햄릿 (연극 1)
└───→ 오델로 (연극 2)
배우 B ────→ 햄릿 (연극 1)
└───→ 맥베스 (연극 3)
협력: "영화 예매"
역할: DiscountPolicy
- 역할은 협력이 진행되는 동안만 존재
- 서로 다른 객체가 같은 역할 수행 가능
- 한 객체가 여러 협력에서 다른 역할 수행 가능
AmountDiscountPolicy ────→ DiscountPolicy (영화 예매 협력)
PercentDiscountPolicy ───→ DiscountPolicy (영화 예매 협력)
핵심:
배우는 여러 배역을 연기할 수 있지만,
한 연극 안에서는 하나의 배역만 연기
객체는 여러 역할을 수행할 수 있지만,
한 협력 안에서는 하나의 역할만 수행
1. 협력을 설계한다
"사용자가 영화를 예매한다"
↓
2. 협력에 필요한 메시지를 식별한다
"예매하라"
↓
3. 메시지를 처리할 객체를 선택한다 (정보 전문가)
Screening (상영 정보 전문가)
↓
4. 객체에게 책임을 할당한다
Screening.reserve()
↓
5. 책임을 수행하는 데 필요한 상태를 결정한다
movie, sequence, whenScreened
↓
6. 객체가 다른 객체의 도움이 필요한가?
Yes → 2번으로 (새로운 메시지 식별)
No → 완료
↓
7. 여러 객체가 같은 책임을 수행하는가?
Yes → 역할로 추상화
No → 객체 그대로 유지
설계 시작 전:
□ 어떤 협력이 필요한가?
□ 사용자에게 어떤 기능을 제공하는가?
책임 할당 시:
□ 이 책임을 수행하려면 무엇을 알아야 하는가?
□ 그 정보를 가장 잘 알고 있는 객체는 무엇인가?
□ 메시지를 먼저 선택했는가? (객체가 아닌)
설계 검증 시:
□ 객체가 상태가 아닌 행동으로 정의되었는가?
□ 여러 객체가 같은 책임을 수행하는가? (역할 필요?)
□ 협력이 유연하고 확장 가능한가?
// 모든 책임을 혼자 다 함
public class MovieSystem {
public Reservation reserve(...) {
// 예매 로직
// 가격 계산 로직
// 할인 계산 로직
// 조건 확인 로직
// 모든 것을 여기서!
}
}해결: 책임을 분산 → 협력하는 객체들로
public class Movie {
private String title;
private Money fee;
// getter/setter만 있음
public String getTitle() { ... }
public void setTitle(String title) { ... }
public Money getFee() { ... }
public void setFee(Money fee) { ... }
}해결: 행동을 추가 → 자율적인 객체로
// 금액 할인용 Movie
public class AmountMovie { ... }
// 비율 할인용 Movie
public class PercentMovie { ... }
// 거의 같은 코드가 중복!해결: 역할로 통합 → 하나의 협력으로
A: 객체를 먼저 만들지 말고, **"누가 누구에게 무엇을 요청하는가?"**를 먼저 생각하라는 의미
❌ 잘못된 순서:
1. Movie 클래스 만들기
2. DiscountPolicy 클래스 만들기
3. 이제 이걸로 뭘 하지?
✅ 올바른 순서:
1. "영화를 예매한다" (협력)
2. "예매하려면 누구에게 뭘 요청해야 하지?" (메시지)
3. "Screening에게 '예매하라' 요청" (객체 선택)
4. "Screening은 누구에게 뭘 요청해야 하지?" (다음 메시지)
5. 반복...
A: **"이 책임을 수행하려면 무엇을 알아야 하는가?"**를 질문
예시: "예매를 생성하라"
질문: 예매를 생성하려면 무엇을 알아야 하는가?
- 어떤 영화를 상영하는지
- 언제 상영하는지
- 몇 번째 상영인지
질문: 이 정보를 누가 가장 잘 알고 있는가?
→ Screening!
결론: Screening이 정보 전문가
A: 설계 초반에는 구분하지 마세요!
초반:
- 일단 구체적인 객체로 시작
- 협력을 설계하며 책임 할당
중반:
- "어? 이 객체랑 저 객체가 같은 책임을 수행하네?"
- 공통점 발견
후반:
- 공통 책임을 역할로 추출
- 역할 기반으로 재설계
결론:
필요할 때 객체에서 역할을 분리하는 것이 가장 좋은 방법!
A: 메시지를 먼저 정하고, 그 메시지를 처리할 객체를 나중에 선택
❌ 객체가 메시지를 선택:
1. Movie 객체가 있다
2. Movie가 뭘 할 수 있을까?
3. calculate(), get(), set(), ... (메서드 나열)
→ 불필요한 메서드 많음
✅ 메시지가 객체를 선택:
1. "요금을 계산하라" 메시지가 필요하다
2. 이 메시지를 누가 처리할까?
3. Movie가 적절하다
4. Movie.calculateMovieFee() 메서드 추가
→ 꼭 필요한 메서드만
A: 행동을 결정한 후에 상태를 정의
순서:
1. 협력: "영화 예매"
2. 행동: Movie는 "요금을 계산한다"
3. 구현: calculateMovieFee() 메서드 작성
4. 상태: "어? 계산하려면 fee랑 discountPolicy가 필요하네"
5. 추가: private Money fee, private DiscountPolicy policy
핵심:
"이 상태가 필요해서 추가한다"가 아니라
"이 행동을 하려면 이 상태가 필요하네" 순서!
1. 협력이 객체의 행동을 결정하고, 행동이 상태를 결정한다.
2. 메시지를 먼저 선택하고, 정보 전문가에게 책임을 할당한다.
3. 여러 객체가 같은 책임을 수행하면 역할로 추상화한다.
📌 협력 중심
- 객체를 협력의 관점에서 바라봐라
- "이 객체가 어떤 협력에 참여하는가?"
📌 책임 할당
- 정보 전문가에게 책임을 할당하라
- 메시지를 먼저 선택하라
📌 행동 우선
- 상태가 아닌 행동으로 객체를 정의하라
- 협력 → 행동 → 상태 순서
📌 역할 활용
- 같은 책임은 역할로 추상화하라
- 필요할 때 객체에서 역할을 분리하라
// ❌ 상태부터 정의
public class Order {
private Long userId;
private Long productId;
private Integer quantity;
private Long totalPrice;
// getter/setter만 가득
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }
// ...
}
public class Product {
private Long id;
private String name;
private Long price;
private Integer stock;
// getter/setter만 가득
public Long getPrice() { return price; }
public void setPrice(Long price) { this.price = price; }
// ...
}
// Service가 모든 로직 처리
public class OrderService {
public Order createOrder(Long userId, Long productId, Integer quantity) {
Product product = productRepository.findById(productId);
// ❌ 재고 확인을 Service가 함
if (product.getStock() < quantity) {
throw new OutOfStockException();
}
// ❌ 재고 차감을 Service가 함
product.setStock(product.getStock() - quantity);
// ❌ 가격 계산을 Service가 함
Long totalPrice = product.getPrice() * quantity;
// ❌ Order 생성을 Service가 함
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(totalPrice);
orderRepository.save(order);
productRepository.save(product);
return order;
}
}문제점:
- Order, Product는 데이터만 보관 (수동적)
- OrderService가 모든 로직 처리 (God Object)
- 캡슐화 위반 (getter/setter 남발)
- Product, Order의 책임 불명확
"사용자가 상품을 주문한다"
필요한 협력:
- 재고 확인
- 재고 차감
- 가격 계산
- 주문 생성
1. "주문을 생성하라"
→ 정보 전문가: Order
→ Order가 주문 정보를 가장 잘 알고 있음
2. "재고를 확인하라"
→ 정보 전문가: Product
→ Product가 재고 정보를 가장 잘 알고 있음
3. "재고를 차감하라"
→ 정보 전문가: Product
→ Product가 재고를 가장 잘 알고 있음
4. "가격을 계산하라"
→ 정보 전문가: Product
→ Product가 가격 정보를 가장 잘 알고 있음
// ✅ 자율적인 객체
public class Product {
private Long id;
private String name;
private Long price;
private Integer stock;
// ✅ 책임: 재고를 확인하고 차감한다
public void decreaseStock(Integer quantity) {
if (!hasEnoughStock(quantity)) {
throw new OutOfStockException(
"재고 부족: 현재 " + stock + "개, 요청 " + quantity + "개"
);
}
this.stock -= quantity;
}
// ✅ 책임: 재고가 충분한지 확인한다
private boolean hasEnoughStock(Integer quantity) {
return this.stock >= quantity;
}
// ✅ 책임: 가격을 계산한다
public Money calculatePrice(Integer quantity) {
return Money.wons(this.price * quantity);
}
// getter만 제공 (setter 없음)
public Long getId() { return id; }
public Money getPrice() { return Money.wons(price); }
}
// ✅ 자율적인 객체
public class Order {
private Long userId;
private Long productId;
private Integer quantity;
private Money totalPrice;
private OrderStatus status;
// ✅ 책임: 주문을 생성한다
public static Order create(
Long userId,
Product product,
Integer quantity
) {
// 1. Product에게 재고 차감 요청 (협력)
product.decreaseStock(quantity);
// 2. Product에게 가격 계산 요청 (협력)
Money totalPrice = product.calculatePrice(quantity);
// 3. 주문 생성
return new Order(
userId,
product.getId(),
quantity,
totalPrice
);
}
// ✅ private 생성자: 외부에서 직접 생성 불가
private Order(
Long userId,
Long productId,
Integer quantity,
Money totalPrice
) {
this.userId = userId;
this.productId = productId;
this.quantity = quantity;
this.totalPrice = totalPrice;
this.status = OrderStatus.PENDING;
}
// ✅ 책임: 주문을 취소한다
public void cancel() {
if (this.status != OrderStatus.PENDING) {
throw new IllegalStateException("취소 불가능한 상태");
}
this.status = OrderStatus.CANCELLED;
}
// getter만 제공
public OrderStatus getStatus() { return status; }
}
// ✅ Service는 조율만 담당
public class OrderService {
public Order createOrder(Long userId, Long productId, Integer quantity) {
// 1. 조회
Product product = productRepository.findById(productId);
// 2. 도메인 객체에게 위임 (협력 요청)
Order order = Order.create(userId, product, quantity);
// 3. 저장
orderRepository.save(order);
productRepository.save(product);
return order;
}
}OrderService (조율자)
│
│ 1. "주문을 생성하라"
↓
Order.create()
│
│ 2. "재고를 차감하라"
↓
Product.decreaseStock()
│
│ 2-1. "재고가 충분한가?"
│
├─→ hasEnoughStock() [private]
│ └─→ true/false
│
│ 3. "가격을 계산하라"
↓
Product.calculatePrice()
│
│ Money 반환
↓
Order 생성 완료
| 측면 | 데이터 중심 설계 | 책임 주도 설계 |
|---|---|---|
| 캡슐화 | getter/setter 남발 | 행동만 노출 |
| 응집도 | Service에 모든 로직 집중 | 각 객체가 자신의 로직 처리 |
| 결합도 | Service가 모든 객체에 의존 | 인터페이스를 통한 협력 |
| 변경 영향 | Service 변경 시 전체 영향 | 해당 객체만 변경 |
| 테스트 | Service만 테스트 가능 | 각 객체 독립적 테스트 |
| 재사용성 | 낮음 (Service에 종속) | 높음 (객체 단위 재사용) |
책임 주도 설계를 위한 도구로, 실제 카드를 사용해서 설계합니다.
┌─────────────────────────────────┐
│ Screening │
├─────────────────────────────────┤
│ 책임 (Responsibilities): │
│ - 예매를 생성한다 │
│ - 예매 가격을 계산한다 │
├─────────────────────────────────┤
│ 협력자 (Collaborators): │
│ - Movie │
│ - Reservation │
└─────────────────────────────────┘
┌─────────────────────────────────┐
│ Movie │
├─────────────────────────────────┤
│ 책임: │
│ - 영화 요금을 계산한다 │
├─────────────────────────────────┤
│ 협력자: │
│ - DiscountPolicy │
└─────────────────────────────────┘
사용 방법:
- 각 객체마다 카드 작성
- 책임을 카드에 기록
- 협력자를 파악하여 기록
- 카드를 배치하며 협력 설계
- 책임 재배치 및 정제
때로는 정보 전문가가 아닌 다른 객체에게 책임을 할당하는 것이 나을 수 있습니다.
// 방법 1: 정보 전문가 (Order)
public class Order {
private List<OrderItem> items;
public Money calculateTotal() {
Money total = Money.ZERO;
for (OrderItem item : items) {
// ❌ Order가 OrderItem의 내부를 알아야 함
total = total.plus(
item.getProduct().getPrice()
.times(item.getQuantity())
);
}
return total;
}
}
// 방법 2: 낮은 결합도 (OrderItem에게 위임)
public class Order {
private List<OrderItem> items;
public Money calculateTotal() {
Money total = Money.ZERO;
for (OrderItem item : items) {
// ✅ OrderItem에게 위임 (캡슐화 유지)
total = total.plus(item.calculatePrice());
}
return total;
}
}
public class OrderItem {
private Product product;
private Integer quantity;
// ✅ OrderItem이 자신의 가격 계산
public Money calculatePrice() {
return product.getPrice().times(quantity);
}
}트레이드오프:
- 정보 전문가: Order가 총액 정보를 가장 잘 알지만, OrderItem 내부를 알아야 함
- 낮은 결합도: OrderItem에게 위임하여 캡슐화 유지
결정: 낮은 결합도를 선택 (캡슐화가 더 중요)
A: 아니요! 정보 전문가 패턴은 **"어떤 객체가 필요한지"**를 찾는 과정입니다.
❌ 잘못된 이해:
"이미 만들어진 객체들 중에서 정보를 많이 아는 객체를 선택한다"
✅ 올바른 이해:
"이 책임을 수행하려면 무엇을 알아야 하는가?
→ 그 정보를 알고 있어야 할 객체는 무엇인가?
→ 그 객체가 존재하지 않으면 만든다"
실제 과정:
1. 협력 시나리오: "상품을 주문한다"
2. 메시지: "주문을 생성하라"
3. 질문: "주문을 생성하려면 무엇을 알아야 하는가?"
답변: 사용자, 상품, 수량
4. 질문: "이 정보를 알고 있어야 할 객체는?"
답변: Order (문맥상 주문 정보를 담당)
5. 결론: Order 객체가 필요하다!
→ Order 객체를 만든다
→ Order에게 "주문을 생성하라" 책임 할당
A: 정보 전문가도 행동 중심입니다. 출발점은 항상 행동입니다.
순서 정리:
1. 협력 필요 발견
"영화를 예매한다"
2. 필요한 행동(책임) 파악
"예매를 생성한다"
"요금을 계산한다"
3. 행동을 수행하려면 무엇을 알아야 하는가?
"상영 시간, 영화, 순번을 알아야 한다" ← 여기서 상태 등장
4. 그 정보를 알고 있어야 할 객체는?
"Screening"
5. Screening 설계
- 행동: reserve(), calculateFee()
- 상태: movie, sequence, whenScreened ← 행동 때문에 필요
핵심:
상태를 먼저 정의하는 것이 아니라, **"이 행동을 하려면 무엇을 알아야 하는가?"**를 질문하는 것
A: 설계 초반에는 객체로 시작하고, 중복이 발견되면 역할로 추출합니다.
Phase 1: 객체로 시작
// 협력: "결제를 처리한다"
// 메시지: "결제하라"
// 정보 전문가: CreditCardPayment
public class Order {
public void pay(CreditCardPayment payment) {
payment.process(this.totalPrice);
}
}
public class CreditCardPayment {
public void process(Money amount) {
// 신용카드 결제 처리
}
}Phase 2: 새로운 결제 수단 추가
// 카카오페이 추가됨
public class KakaoPayPayment {
public void process(Money amount) {
// 카카오페이 결제 처리
}
}
// ❌ 문제: Order를 두 개 만들어야 하나?
public class Order {
public void pay(CreditCardPayment payment) { ... }
public void pay(KakaoPayPayment payment) { ... } // 중복!
}Phase 3: 역할 추출
// ✅ 공통 책임 발견: "결제를 처리한다"
// → 역할로 추출!
public interface Payment { // 역할
void process(Money amount);
}
public class CreditCardPayment implements Payment {
@Override
public void process(Money amount) {
// 신용카드 결제
}
}
public class KakaoPayPayment implements Payment {
@Override
public void process(Money amount) {
// 카카오페이 결제
}
}
// ✅ 하나의 협력으로 통합
public class Order {
public void pay(Payment payment) { // 역할에 의존
payment.process(this.totalPrice);
}
}정리:
1. 처음: 구체적인 객체로 시작
2. 추가 요구사항: 비슷한 객체가 또 필요
3. 중복 발견: "어? 둘이 같은 책임을 수행하네?"
4. 역할 추출: 공통 책임을 역할로 만들기
A: 시나리오를 먼저 작성하는 것입니다.
❌ 잘못된 방법 (클래스부터)
1. Board 클래스 만들기
- 제목, 내용, 작성자, 날짜...
2. Comment 클래스 만들기
- 내용, 작성자, 날짜...
3. 이제 이걸로 뭘 하지? 🤔
✅ 올바른 방법 (협력부터)
1. 사용자 스토리 작성
"사용자가 게시글을 작성한다"
"사용자가 게시글에 댓글을 단다"
"사용자가 게시글을 수정한다"
2. 첫 번째 협력 시나리오
"사용자가 게시글을 작성한다"
a. 누가 이 요청을 받는가?
→ BoardService? Board?
b. "게시글을 작성하라" 메시지
→ 정보 전문가는?
→ Board (제목, 내용 등을 알아야 함)
c. Board가 작성되려면 뭐가 필요한가?
→ 작성자 정보
→ 작성자는 누가 아는가?
→ User
3. 협력 설계
User ────→ Board.create()
"게시글을 작성하라"
4. 두 번째 협력 시나리오
"사용자가 댓글을 단다"
Board ────→ Comment.create()
"댓글을 추가하라"
5. 이제 클래스 구현
(협력이 명확하므로 구현이 쉬움)
예제: 쇼핑몰 장바구니
사용자 스토리:
- 사용자가 상품을 장바구니에 담는다
- 사용자가 장바구니의 상품을 삭제한다
- 사용자가 장바구니의 총액을 확인한다
- 사용자가 장바구니의 상품을 주문한다
시나리오 1: "상품을 장바구니에 담는다"
1. 사용자가 "담기" 버튼 클릭
2. 시스템이 상품 정보 확인
3. 장바구니에 상품 추가
4. 장바구니 개수 업데이트
필요한 메시지:
- "장바구니에 담아라" (addToCart)
- "상품 정보를 가져와라" (getProduct)
- "수량을 증가시켜라" (increaseQuantity)
"장바구니에 담아라"
→ 정보 전문가: Cart
(장바구니 항목들을 알고 있음)
"상품 정보를 가져와라"
→ 정보 전문가: ProductRepository
(상품 정보를 알고 있음)
"수량을 증가시켜라"
→ 정보 전문가: CartItem
(항목별 수량을 알고 있음)
CartService (조율)
│
│ "상품을 가져와라"
↓
ProductRepository
│
│ Product 반환
↓
CartService
│
│ "장바구니에 담아라"
↓
Cart.addItem(product, quantity)
│
│ "같은 상품이 있나?"
│
├─→ YES: CartItem.increaseQuantity()
│
└─→ NO: new CartItem(product, quantity)
public class Cart {
private List<CartItem> items;
public void addItem(Product product, Integer quantity) {
CartItem existing = findItem(product.getId());
if (existing != null) {
existing.increaseQuantity(quantity);
} else {
items.add(new CartItem(product, quantity));
}
}
private CartItem findItem(Long productId) {
return items.stream()
.filter(item -> item.hasProduct(productId))
.findFirst()
.orElse(null);
}
public Money calculateTotal() {
return items.stream()
.map(CartItem::calculatePrice)
.reduce(Money.ZERO, Money::plus);
}
}
public class CartItem {
private Product product;
private Integer quantity;
public void increaseQuantity(Integer amount) {
this.quantity += amount;
}
public Money calculatePrice() {
return product.getPrice().times(quantity);
}
public boolean hasProduct(Long productId) {
return this.product.getId().equals(productId);
}
}□ 각 객체가 명확한 책임을 가지고 있다
□ 협력이 명확하게 보인다
□ 객체가 자신의 데이터를 스스로 처리한다
□ getter/setter가 최소화되어 있다
□ Service 레이어가 조율만 한다
□ 도메인 객체에 비즈니스 로직이 있다
□ 새로운 기능 추가 시 기존 코드 수정이 최소화된다
□ Service가 모든 로직을 처리한다 (God Object)
□ 도메인 객체가 getter/setter만 있다 (Anemic Domain)
□ 한 객체가 다른 객체 내부를 마음대로 조작한다
□ 협력 없이 모든 것을 한 곳에서 처리한다
□ 객체의 존재 이유가 불명확하다
□ 비슷한 코드가 여러 곳에 중복된다
1. 협력 시나리오 작성
↓
2. 필요한 메시지 식별
↓
3. 정보 전문가 찾기
↓
4. 책임 할당
↓
5. 협력 완성
↓
6. 역할 추출 (필요시)
1. 협력이 객체를 만든다
(객체부터 만들지 마라)
2. 메시지가 인터페이스를 결정한다
(객체가 메시지를 선택하지 않는다)
3. 행동이 상태를 결정한다
(상태부터 정의하지 마라)