콘텐츠로 이동

오브젝트 실전 강의 교재

3장 — 역할, 책임, 협력

원서: 조영호 『오브젝트: 코드로 이해하는 객체지향 설계』 대상: Java/Spring 백엔드 입문~중급 수강생 형식: 개념 → 비유 → 예시 → 핵심 교훈 → 현업 예제 → 함정 → 체크리스트 → 퀴즈(정답 분리)


0. 이 장을 시작하기 전에

0.1 학습 목표

  • 객체지향의 본질이 클래스가 아니라 협력·책임·역할임을 이해한다.
  • 책임 주도 설계(RDD)의 사고 순서(협력 → 책임 → 객체)를 익힌다.
  • "메시지가 객체를 결정한다", "행동이 상태를 결정한다"라는 설계 격언을 체득한다.

0.2 큰 그림 — 설계의 사고 순서

[ 협력 ]  어떤 요청-응답으로 목표를 이룰까
   │  (협력이 필요한 행동을 끌어낸다)
[ 책임 ]  그 행동(의무)을 누가 맡을까
   │  (책임을 객체에게 할당한다)
[ 역할 ]  대체 가능한 책임 묶음 → 여러 객체가 채울 수 있는 '자리'

비유 — "연극 무대": 협력은 한 편의 공연(목표 달성을 위한 상호작용), 책임은 각 인물이 무대에서 해야 할 일(대사·동작), 역할은 그 인물의 배역입니다. 배역(역할)은 정해져 있고, 그 배역을 연기하는 배우(객체)는 공연마다 바뀔 수 있습니다.

0.3 현업에서 왜 중요한가

  • 좋은 클래스를 먼저 떠올리는 게 아니라, 어떤 협력이 필요한가 → 어떤 책임이 필요한가 → 그 책임을 누구에게의 순서로 사고하면 자연스럽게 응집도 높은 설계가 나옵니다.
  • 인터페이스 기반 설계(역할)는 그대로 다형성·교체 가능성·테스트 용이성으로 이어집니다.

1. 협력

비유 — "식당의 주문 흐름"

손님이 웨이터에게 "파스타 주세요"라고 요청하고, 웨이터는 주방에 전달하고, 주방은 요리해 응답합니다. 아무도 남의 일을 대신하지 않습니다. 협력은 이런 요청과 응답의 연쇄입니다.

핵심

  • 협력: 객체들이 메시지(요청)를 주고받으며 함께 목표를 달성하는 것.
  • 한 객체가 혼자 처리할 수 없는 일을, 다른 객체에게 요청함으로써 협력이 생긴다.
  • 협력이 설계의 문맥을 결정한다: 객체의 행동(책임)은 "어떤 협력에 참여하는가"에 따라 정해진다. 협력 없이 객체의 책임을 정할 수 없다.

2장 복습: Screening이 혼자 요금을 못 구하니 Movie에게 요청하고, MovieDiscountPolicy에게 요청했습니다. 이 협력의 사슬이 각 객체의 책임을 만들어냈습니다.


2. 책임

2.1 책임이란 무엇인가 — "하는 것"과 "아는 것"

책임은 객체가 협력에서 맡는 의무입니다. 두 범주로 나뉩니다.

  • 하는 것(doing): 객체를 생성/계산하거나, 다른 객체의 행동을 시작/제어/조정한다.
  • 아는 것(knowing): 사적 정보를 알고, 관련된 객체를 알고, 자신이 유도/계산할 수 있는 것을 안다.

책임은 곧 객체의 공개 인터페이스 후보입니다. "이 객체는 무엇을 할 줄 알고, 무엇을 아는가"가 메시지가 됩니다.

2.2 책임 주도 설계 (Responsibility-Driven Design)

사고 순서: 시스템이 해야 할 일(협력)을 → 더 작은 책임으로 나누고 → 그 책임을 수행하기에 적절한 객체에게 할당한다.

데이터(필드)부터 정하지 않고, 책임부터 정합니다. 데이터 중심 설계의 함정은 4장에서 다룹니다.

2.3 메시지가 객체를 결정한다 (순서를 뒤집어라)

비유 — "채용"

회사를 만들 때, 사람부터 뽑아놓고 "이제 무슨 일 시키지?"라고 하지 않습니다. 필요한 일(메시지)을 먼저 정의하고, 그 일을 가장 잘할 사람(객체)을 찾습니다.

// 나쁜 순서: 객체(클래스)를 먼저 만들고 → 거기 맞춰 메시지를 끼워넣음
// 좋은 순서: 협력에 필요한 '메시지'를 먼저 정하고 → 그 메시지를 처리할 객체를 찾음

// 예: "이 상영의 요금을 계산하라"는 메시지가 먼저 필요했다
//     → 그 메시지를 가장 잘 처리할 객체로 Movie를 선택
movie.calculateMovieFee(screening);   // 메시지가 먼저, Movie는 그 뒤

메시지를 먼저 정하면, 객체의 인터페이스가 클라이언트가 원하는 것으로 채워집니다(꼭 필요한 public 메서드만 생김 → 최소 인터페이스).

2.4 행동이 상태를 결정한다

비유 — "이력서를 거꾸로 쓰지 마라"

스펙(상태)부터 나열한 이력서가 아니라, "무슨 일을 할 수 있는가(행동)"부터 정의하고 그 일에 필요한 스펙(상태)을 채웁니다.

  • 상태 먼저(나쁨): 필드를 먼저 정하면, 그 데이터를 노출하는 getter/setter가 생기고 캡슐화가 약해진다. 협력은 뒷전이 된다.
  • 행동 먼저(좋음): "협력에서 이 객체가 무엇을 하는가(책임)"를 먼저 정하고, 그 행동에 꼭 필요한 상태만 추가한다.
// 행동 먼저: Account가 협력에서 '출금/입금'을 한다 → 그 행동에 필요한 balance만 둔다
class Account {
    private long balance;            // 행동(withdraw/deposit)에 필요해서 생긴 상태
    void withdraw(long amount) { ... }
    void deposit(long amount)  { ... }
}

연결: 1장의 "자율적 객체", 클린코드 "묻지 말고 시켜라"와 한 줄기입니다.


3. 역할

3.1 역할이란 — "대체 가능한 책임의 집합"

같은 협력에서, 책임의 묶음을 여러 객체가 번갈아 수행할 수 있을 때, 그 자리를 역할(role)이라 부릅니다.

비유 — "배우와 배역"

연극의 "주인공"이라는 배역(역할)은 정해져 있고, 그것을 연기하는 배우(객체)는 공연마다 다릅니다. 한 배우가 여러 배역을, 한 배역을 여러 배우가 맡을 수 있습니다.

// '할인 정책'이라는 역할(배역) = DiscountPolicy
// 그 배역을 연기하는 배우(객체) = AmountDiscountPolicy / PercentDiscountPolicy / NoneDiscountPolicy
DiscountPolicy policy = new PercentDiscountPolicy(...);   // 오늘 공연의 배우

3.2 역할 = 협력의 추상화

  • 협력을 "구체 객체들의 상호작용"이 아니라 "역할들의 상호작용"으로 보면, 협력이 추상적이고 재사용 가능해집니다.
  • Movie는 "할인 정책이라는 역할"과 협력합니다. 어떤 구체 정책이 오든 협력 구조는 그대로입니다.
  • 역할은 곧 인터페이스/추상 클래스로 구현되고, 역할을 채우는 다양한 객체가 다형성을 만듭니다.

3.3 객체 대 역할

  • 협력에 등장하는 자리가 여러 객체로 대체 가능하면 → 역할(추상 타입).
  • 항상 한 종류 객체만 온다면 → 굳이 역할로 추상화하지 않고 구체 객체로 둬도 됩니다(과한 추상화 경계 — 9장).

핵심 교훈

  1. 객체지향의 본질은 클래스가 아니라 협력·책임·역할이다.
  2. 설계 순서는 협력 → 책임 → 객체(책임 주도 설계).
  3. 메시지가 객체를 결정한다: 필요한 메시지를 먼저 정하고, 처리할 객체를 찾아라.
  4. 행동이 상태를 결정한다: 책임(행동)을 먼저 정하고, 필요한 상태만 채워라.
  5. 역할은 대체 가능한 책임의 집합(배역) — 협력을 추상화하고 다형성을 낳는다.

현업 예제 — 인터페이스(역할)부터 설계하기

// 1) 협력에 필요한 '메시지'를 역할(인터페이스)로 먼저 정의
public interface PaymentGateway {        // '결제 처리'라는 역할
    PaymentResult pay(Order order, Money amount);
}
// 2) 그 역할을 여러 객체(배우)가 연기
class TossPaymentGateway  implements PaymentGateway { ... }
class KakaoPaymentGateway implements PaymentGateway { ... }
class FakePaymentGateway  implements PaymentGateway { ... }   // 테스트용 배우

// 3) 클라이언트는 역할에만 의존(누가 연기하든 무관)
@Service
class CheckoutService {
    private final PaymentGateway gateway;   // 역할에 의존
    CheckoutService(PaymentGateway gateway) { this.gateway = gateway; }
}

역할(인터페이스)을 먼저, 배우(구현)는 나중에 — 이것이 책임 주도 설계가 Spring DI·테스트 용이성으로 이어지는 통로입니다(이펙티브 자바 5·20, 2장의 전략 구조와 동일).


함정 / 주의

  • 상태부터 설계하는 습관을 경계하라. ERD/필드부터 그리면 데이터 중심 설계로 빠져 캡슐화가 약해집니다(4장에서 그 폐해를 본격적으로 봅니다).
  • 모든 자리를 역할로 추상화하지 마라. 대체 가능성이 없는데 인터페이스부터 만들면 추측성 일반화(리팩터링 3.15)입니다.
  • 책임을 엉뚱한 객체에 두면 기능 편애(리팩터링 3.9)가 생깁니다. "정보를 가진 객체에게 책임을"(5장 정보 전문가).

체크리스트 (설계 사고 점검)

  • 클래스가 아니라 협력(요청-응답)부터 떠올렸는가
  • 필요한 메시지를 먼저 정하고 객체를 찾았는가
  • 데이터(상태)가 아니라 행동(책임)을 먼저 정했는가
  • 대체 가능한 자리를 역할(인터페이스/추상)로 표현했는가
  • 책임이 그 일을 가장 잘 아는 객체에 있는가(엉뚱한 곳에 있지 않은가)

퀴즈

  1. "협력이 설계의 문맥을 결정한다"는 무슨 뜻인가?
  2. 책임의 두 범주는 무엇인가?
  3. "메시지가 객체를 결정한다"가 권하는 설계 순서는?
  4. "행동이 상태를 결정한다"를 어기면(상태 먼저) 어떤 문제가 생기나?
  5. 역할과 객체의 관계를 "배우와 배역"으로 설명하라.

정답·해설

  1. 객체의 책임(행동)은 혼자 정해지지 않고, 어떤 협력에 참여하느냐에 따라 결정된다는 뜻입니다. 협력이라는 문맥이 있어야 "이 객체가 무엇을 해야 하는지"가 정해집니다.
  2. 하는 것(doing) — 계산/생성/제어·조정, 아는 것(knowing) — 사적 정보·관련 객체·유도 가능한 값을 아는 것.
  3. 객체(클래스)를 먼저 만들지 말고, 협력에 필요한 메시지를 먼저 정의한 뒤 그 메시지를 가장 잘 처리할 객체를 찾는 순서. 그래야 인터페이스가 클라이언트가 원하는 최소한으로 채워집니다.
  4. 필드(상태)부터 정하면 그것을 노출하는 getter/setter가 생겨 캡슐화가 약해지고, 협력보다 데이터에 초점이 쏠려 데이터 중심 설계로 빠집니다.
  5. 역할 = 배역(정해진 자리/책임 묶음), 객체 = 배우(그 배역을 연기). 한 배역을 여러 배우가 맡을 수 있어(다형성), 협력을 추상적·재사용 가능하게 만듭니다.

다음 장 예고 — 4장: 설계 품질과 트레이드오프

일부러 데이터 중심으로 영화 예매 시스템을 다시 설계해, 그것이 왜 캡슐화 위반·높은 결합도·낮은 응집도로 이어지는지 해부합니다. 3장에서 배운 "책임/행동 우선"과 대비되는 반면교사 장입니다.