콘텐츠로 이동

오브젝트 실전 강의 교재

6장 — 메시지와 인터페이스

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


0. 이 장을 시작하기 전에

0.1 학습 목표

  • 객체 협력의 언어 — 메시지·인터페이스 의 의미.
  • 묻지 말고 시켜라 (Tell, Don't Ask) 가 왜 핵심인가.
  • 의도를 드러내는 인터페이스 의 설계 기준.
  • 디미터 법칙 — 한 점만 허용 도그마 vs 본질.
  • 명령-쿼리 분리 (CQS) — 부작용과 조회의 분리.

0.2 큰 그림 — 메시지가 첫 시민

[ 클래스 ]                            [ 메시지 ]
 객체의 정적 정의 (필드·메서드)         객체 사이 요청·응답
 구현 디테일                           협력의 단위 ★

→ OO 의 본질은 클래스가 아니라 메시지
  ("메시지가 객체를 결정한다" — 3장)

비유 — "회의 vs 사람"

회사에 큰 프로젝트가 잡혔다고 해 봅시다. 가장 먼저 정해지는 것은 "어떤 회의를 열 것인가" 입니다. 회의가 잡히면 그 자리에는 "PM 자리·개발자 자리·디자이너 자리" 가 정해지고, 마지막에야 각 자리에 앉을 사람이 결정됩니다. 같은 'PM 자리' 라도 김 PM 이 앉을 수도, 이 PM 이 앉을 수도 있고, 일정만 맞으면 누가 앉아도 회의는 진행됩니다.

객체지향도 같은 순서입니다. 객체들 사이에 오갈 메시지가 가장 먼저 정해지고, 그 메시지를 받을 수 있는 역할이 그 다음에 정의되며, 그 역할을 수행하는 객체는 마지막에 결정됩니다. 같은 역할이라도 다양한 객체가 그 자리에 들어올 수 있고, 호출자는 누가 들어왔는지 알 필요가 없습니다.

0.3 현업에서 왜 중요한가

  • API 설계의 모든 결정 (시그니처·반환 타입·예외) 이 이 장에 모임.
  • Spring Controller·Repository·Service 의 메서드 시그니처 결정.
  • Effective Java Item 51 (시그니처)·56 (Javadoc) 과 같은 결.

1. 협력과 메시지

1.1 클라이언트-서버 모델

객체 협력 = 요청 (메시지) 보내는 클라이언트 + 응답 하는 서버.

  • 누가 클라이언트·누가 서버는 상대적 (한 객체가 한 협력에선 서버, 다른 협력에선 클라이언트).
  • 모든 객체가 양쪽 역할 모두 가능.

1.2 메시지와 메시지 전송

sender.send(message)
[ message ]
receiver.handleMessage()
  • 메시지: 의도 표현 (예: screening.reserve(customer, 2)).
  • 메시지 전송: 호출 행위.
  • 수신자가 메서드를 결정 (다형성).

1.3 메시지와 메서드의 차이

  • 메시지: 무엇을 요청 (의도).
  • 메서드: 어떻게 처리 (구현).

같은 메시지에 객체마다 다른 메서드 → 다형성.

DiscountPolicy policy = ...;   // 컴파일타임: 추상
policy.calculateDiscountAmount(screening);   // 메시지 전송
// 런타임: AmountDiscountPolicy.calculateDiscountAmount() 또는 PercentDiscountPolicy.calculateDiscountAmount() (다형성)

1.4 퍼블릭 인터페이스와 오퍼레이션

  • 퍼블릭 인터페이스: 객체가 외부에 노출한 메시지의 집합.
  • 오퍼레이션: 인터페이스에 선언된 메시지 (추상).
  • 메서드: 그 오퍼레이션의 구체 구현.

1.5 시그니처

오퍼레이션의 이름 + 매개변수 목록 + 반환 타입 + 예외. 객체 협력의 계약.


2. 인터페이스와 설계 품질

2.1 묻지 말고 시켜라 (Tell, Don't Ask)

데이터를 꺼내 와 호출자가 처리하지 말고, 데이터를 가진 객체에게 일을 시켜라.

// ❌ Ask — 데이터 꺼내서 외부가 결정
if (account.getBalance() >= amount) {
    account.setBalance(account.getBalance() - amount);
}

// ✅ Tell — 객체에게 시킴
account.withdraw(amount);   // 내부 검증·차감 모두 캡슐화

→ Account 가 자기 데이터로 자기 결정. 정보 전문가 (5장) 의 실천.

2.2 의도를 드러내는 인터페이스

메서드 이름·시그니처가 무엇을 하는지 (의도) 드러내야지, 어떻게 하는지 (구현) 노출 X.

// ❌ 구현 노출
boolean hasMoreThanFiveLateDeliveries(Customer c);

// ✅ 의도
boolean isPriorityCustomer(Customer c);

2.3 함께 모으기

관련 메시지를 한 인터페이스에. 응집도 ↑.


3. 원칙의 함정

3.1 디미터 법칙은 점 하나 강제 X

디미터 법칙: "객체는 자신이 직접 알고 있는 객체에게만 메시지를 보낸다".

// ❌ 위반 — 친구의 친구
order.getCustomer().getAddress().getStreet();

// ✅ — 친구에게만
order.getCustomerStreet();

→ 도그마 "점은 1개만" 이 아니라 객체 내부 구조 노출 회피 가 본질.

3.2 결합도와 응집도의 충돌

  • 결합도 낮추려고 위임을 너무 많이 = 중개자 (악취).
  • 응집도 높이려고 너무 묶으면 = 거대 클래스.

→ 균형. 한 원칙을 절대값으로 끌고 가지 마라.


4. 명령-쿼리 분리 원칙 (CQS)

4.1 핵심

한 메서드는 상태를 변경하거나 (명령) 값을 반환하거나 (조회) — 둘 중 하나만.

// ❌ — 둘 다 하는 함수
public Money withdraw(int amount) {
    balance -= amount;
    return new Money(balance);
}

// ✅ — 분리
public void withdraw(int amount) {       // 명령 — void
    balance -= amount;
}
public Money balance() { return balance; }   // 조회 — 부작용 X

4.2 명령-쿼리 분리와 참조 투명성

조회 메서드는 여러 번 호출해도 같은 결과 (참조 투명). 부작용 있는 함수는 호출 순서·횟수가 결과 영향 → 추론 어려움.

4.3 책임에 초점을 맞춰라

CQS 가 본질적으로 가리키는 것: 한 메서드 = 한 책임. Clean Code 3장 "함수는 한 가지만" 과 같은 결.


5. 인터페이스 설계 절차

1. 메시지를 먼저 결정

협력에 필요한 요청·응답을 시나리오로 적는다.

2. 메시지가 객체를 결정

그 메시지를 받을 수 있는 객체 (정보 전문가) 가 인터페이스의 주인.

3. 의도가 드러나는 이름

표준 명명 규칙 (findBy*, is*, to*).

4. 매개변수·반환 정련

  • 매개변수 4개 이하 (객체화 검토).
  • 반환은 컬렉션/Optional (null 회피).
  • 예외는 호출자 관점에서 분류.

핵심 교훈

  1. 메시지가 객체보다 먼저 — OO 의 본질.
  2. Tell, Don't Ask — 데이터 꺼내지 말고 객체에게 시켜라.
  3. 의도 드러내는 인터페이스 — 무엇을 (의도) > 어떻게 (구현).
  4. 디미터 법칙은 도그마 X — 내부 구조 노출 회피가 본질.
  5. CQS — 명령과 조회 분리, 추론·테스트 자유.
  6. 시그니처가 계약 — 한 번 공개되면 영원.

현업 예제 — Spring 컨트롤러의 시그니처

안티패턴

// ❌ — Ask + boolean 플래그 + 다중 책임
@PostMapping
public Map<String, Object> handle(@RequestBody Map<String, Object> raw, boolean isVip) {
    // 파싱·검증·저장·이메일 발송 다
}

권장

@PostMapping("/orders")
public OrderResponse create(@Valid @RequestBody CreateOrderCommand cmd) {   // 의도 + 검증
    Order order = orderService.create(cmd);
    return OrderResponse.from(order);
}

@GetMapping("/orders/{id}")
public OrderResponse get(@PathVariable Long id) {   // 명령-쿼리 분리
    return orderService.findById(id)
        .map(OrderResponse::from)
        .orElseThrow(() -> new OrderNotFoundException(id));
}

→ 의도가 메서드 이름·시그니처에 명시. CQS (create 명령, get 조회).


함정 / 주의

  • Tell, Don't Ask 도그마 — getter 가 무조건 나쁜 건 아님. 디스플레이용·DTO 등 조회 자체가 책임인 경우는 OK.
  • 디미터 "점 1개" 강제 = 함수 폭증. 본질은 내부 구조 노출 회피.
  • CQS 가 깨지는 경우Iterator.next() 처럼 표준 라이브러리가 이미 깸. 도메인 코드만 엄격히.
  • 공개 API 는 한 번 결정되면 영원Effective Java Item 56·Clean Code 11장과 같은 결.

체크리스트 (인터페이스 리뷰용)

  • 메서드 이름이 의도 (무엇을) 를 드러내는가 — 구현 (어떻게) 노출 X
  • a.getB().getC().d() 같은 체인이 없는가
  • 한 메서드가 명령 + 조회를 동시에 하지 않는가 (CQS)
  • boolean 플래그 매개변수 대신 enum 또는 분리된 메서드인가
  • 매개변수 4개 이상 → 객체화 검토

퀴즈

  1. 메시지메서드 의 차이는?
  2. Tell, Don't Ask 의 본질을 한 문장으로?
  3. 디미터 법칙 을 "한 점만 허용" 으로 해석하면 안 되는 이유?
  4. CQS 의 실용적 효과 두 가지?
  5. Effective Java Item 51 (시그니처 신중) 과 6장의 연결은?

정답·해설

  1. 메시지 = 무엇을 요청 (의도). 메서드 = 어떻게 처리 (구현). 같은 메시지에 객체마다 다른 메서드 = 다형성. OO 의 본질은 메시지 (요청) 이고 메서드는 그 응답 방식.
  2. 데이터를 꺼내지 말고 객체에게 일을 시켜라 — 데이터 가진 객체가 자기 결정. 외부가 데이터로 분기·계산하면 캡슐화 위반·결합도 폭증·빈혈 도메인.
  3. 본질이 점 개수가 아니라 객체 내부 구조 노출 회피. 도그마로 점 1개 강제하면 의미 없는 위임 메서드 폭증 (중개자 악취). a.b().c() 가 안전한 경우 (예: Optional.of(x).map(...).get()) 도 많음.
  4. (1) 추론 쉬움 — 조회는 부작용 없으므로 여러 번 호출해도 같은 결과 (참조 투명). (2) 테스트 쉬움 — 명령과 조회 분리하면 mock·verify 가 명확.
  5. Item 51 = "한 번 공개된 시그니처는 영원" + "매개변수 신중" + "boolean 플래그 회피". 6장의 "의도 드러내는 인터페이스" + "CQS" + "매개변수 객체화" 와 같은 결. Effective Java 가 메서드 단위, 오브젝트 가 객체 협력 관점.

다음 장 예고 — 7장: 객체 분해

객체로 분해하기 전에는 프로시저 추상화데이터 추상화 가 있었다. 7장은 두 추상화의 역사·차이·한계를 다루고, 추상 데이터 타입 → 클래스 로 발전한 흐름. 객체지향이 단순한 데이터 묶음을 넘어서는 이유.