콘텐츠로 이동

오브젝트 실전 강의 교재

1장 — 객체, 설계

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


0. 이 장을 시작하기 전에

0.1 학습 목표

  • "절차지향 코드"가 왜 변경에 취약한지 티켓 판매 예제로 체감한다.
  • 객체에 자율성을 부여하는 것(캡슐화 + 책임의 이동)이 무엇인지 이해한다.
  • "좋은 설계 = 협력하는 객체 사이의 의존성을 잘 관리하는 것"이라는 이 책의 대전제를 받아들인다.

0.2 큰 그림 — 이 장의 한 문장

[ 절차지향 ]  한 객체(Theater)가 모든 것을 직접 주무른다 → 결합↑, 변경에 취약
      │  자율성을 높이자 (캡슐화 + 책임의 이동)
[ 객체지향 ]  각 객체가 자기 데이터를 스스로 책임진다 → 결합↓, 변경에 강함

비유 — "극장 주인이 관람객 주머니에 손을 넣는다?" 절차지향 코드의 Theater는, 극장 주인이 관람객의 가방을 직접 뒤져 표를 꺼내고, 판매원의 매표소를 직접 열어 돈을 세는 것과 같습니다. 무례하고 부자연스럽습니다. 객체지향은 "관람객은 스스로 표를 사고, 판매원은 스스로 표를 판다"로 바꿔, 각자 자기 일을 자기가 하게 만듭니다.

0.3 현업에서 왜 중요한가

  • "엔티티는 데이터, 로직은 전부 Service"인 빈약한 도메인 모델이 바로 이 장의 절차지향 코드입니다(리팩터링 3.22 데이터 클래스).
  • 도메인 모델을 먼저 잡는 설계의 출발점이 "객체에게 책임을 주는 것"입니다.

1. 티켓 판매 애플리케이션 — 절차지향 버전

소극장에서 이벤트를 합니다. 당첨자는 초대장으로, 미당첨자는 현금으로 표를 삽니다. 등장 객체: Theater, Audience(관람객), Bag(가방: 현금·초대장·티켓), TicketSeller(판매원), TicketOffice(매표소), Ticket.

// 절차지향: Theater가 모든 것을 직접 조작한다
public class Theater {
    private TicketSeller ticketSeller;

    public void enter(Audience audience) {
        if (audience.getBag().hasInvitation()) {                 // 가방 내부를 직접 들여다봄
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);                 // 가방을 직접 조작
        } else {
            Ticket ticket = ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());      // 관람객 돈을 직접 셈
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee()); // 매표소 돈을 직접 셈
            audience.getBag().setTicket(ticket);
        }
    }
}

2. 무엇이 문제인가

2.1 예상을 빗나가는 코드

  • TheaterAudience의 가방을 열고, TicketSeller의 매표소를 직접 조작합니다.
  • 현실의 직관(관람객/판매원이 스스로 행동)과 어긋납니다. AudienceTicketSeller는 그저 수동적인 데이터 덩어리로 전락했습니다.

2.2 변경에 취약한 코드

  • Theater는 너무 많은 것을 압니다: Audience에게 Bag이 있다는 것, Bag 안에 현금·초대장이 있다는 것, TicketSeller에게 TicketOffice가 있다는 것…
  • 세부 사항에 대한 의존(결합)이 문제입니다. 관람객이 가방 대신 지갑을 들거나, 현금 대신 신용카드를 쓰면 Theater까지 줄줄이 바뀝니다.

핵심 진단: 객체 사이의 의존성이 과하다. 한 객체가 다른 객체의 내부 구조를 너무 많이 알고 있다(디미터 법칙 위반 — 클린코드 6장).


3. 설계 개선하기 — 자율성을 높이자

비유 — "각자 자기 일을 한다"

극장은 판매원에게 "이 관람객을 받아주세요"라고 부탁(메시지)만 합니다. 표를 어떻게 꺼내고 돈을 어떻게 받는지는 판매원과 관람객이 알아서 합니다.

3.1 책임을 제자리로 옮긴다 (책임의 이동)

Theater에 몰려 있던 일을, 그 일을 가장 잘 아는 객체에게 넘깁니다.

// Theater: 더 이상 내부를 모른다. 그저 위임한다.
public class Theater {
    private TicketSeller ticketSeller;
    public void enter(Audience audience) {
        ticketSeller.sellTo(audience);     // "이 관람객에게 팔아주세요"
    }
}

// TicketSeller: 매표소는 자기만 다룬다(캡슐화)
public class TicketSeller {
    private TicketOffice ticketOffice;
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}

// Audience: 가방은 자기만 다룬다
public class Audience {
    private Bag bag;
    public Long buy(Ticket ticket) {
        return bag.hold(ticket);           // 가방에게 위임
    }
}

// Bag: 자기 데이터(현금·초대장·티켓)는 자기가 책임진다
public class Bag {
    private Long amount;
    private Invitation invitation;
    private Ticket ticket;

    public Long hold(Ticket ticket) {
        if (hasInvitation()) {             // 초대장 보유 → 무료
            setTicket(ticket);
            return 0L;
        } else {                           // 현금 결제
            setTicket(ticket);
            minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }
    private boolean hasInvitation() { return invitation != null; }
    private void setTicket(Ticket ticket) { this.ticket = ticket; }
    private void minusAmount(Long amount) { this.amount -= amount; }
}

3.2 무엇이 개선됐는가

  • Theater는 이제 Bag·TicketOffice의 내부를 전혀 모릅니다. 관람객이 지갑을 쓰든 카드를 쓰든, Theater는 바뀌지 않습니다.
  • 각 객체는 자신의 데이터를 스스로 처리합니다(자율성). 변경의 파급이 객체 내부에 갇힙니다.

3.3 캡슐화와 응집도

  • 캡슐화: 내부 구현을 숨기고, 외부에는 메시지(공개 인터페이스)만 노출. Bagamount·invitation은 외부가 모른다.
  • 자신의 데이터를 스스로 다루는 객체는 응집도가 높고, 외부와의 결합도가 낮습니다.

3.4 절차지향 vs 객체지향

  • 절차지향: 데이터(Audience/Bag)와 프로세스(Theater.enter)가 분리되어 있다. 프로세스가 데이터를 멀리서 조작한다.
  • 객체지향: 데이터와 그 데이터를 다루는 프로세스가 한 객체 안에 있다. 이 차이의 핵심이 바로 책임의 이동이다.

3.5 그래, 거짓말이다! (의인화)

현실에서 가방·매표소는 스스로 움직이지 못합니다. 하지만 객체지향에서는 무생물도 능동적인 존재로 의인화합니다. "가방이 스스로 표를 보관한다"는 거짓말이지만, 그 거짓말이 자율적이고 유연한 설계를 만듭니다. 현실의 수동성에 코드를 가두지 마세요.


4. 객체지향 설계

4.1 설계가 왜 필요한가

  • 설계란 코드를 어떻게 배치하느냐입니다. 동작하는 코드와 좋은 코드는 다릅니다.
  • 좋은 설계는 오늘의 기능을 구현하면서, 내일의 변경을 수용합니다. 요구사항은 반드시 바뀌기 때문입니다.

4.2 객체지향 설계란

이 책의 대전제: 훌륭한 객체지향 설계란, 서로 협력하는 객체들 사이의 의존성을 적절히 관리하는 설계다.

객체 자체가 아니라 객체 사이의 관계(의존성·협력)가 설계의 핵심이라는 선언입니다. 이후 모든 장이 이 주제를 변주합니다.


핵심 교훈

  1. 한 객체가 다른 객체의 내부를 너무 많이 알면(과한 의존) 변경에 취약하다.
  2. 해법은 자율성 — 자기 데이터는 자기가 책임지게 하라(캡슐화 + 책임의 이동).
  3. 절차지향과 객체지향의 본질적 차이는 책임이 어디에 있는가이다.
  4. 무생물도 의인화해 능동적 객체로 다뤄라. 현실의 수동성에 얽매이지 마라.
  5. 좋은 설계 = 협력하는 객체들의 의존성을 잘 관리하는 것.

현업 예제 — 빈약한 도메인 모델 탈출

// before: 빈약한 모델 — 엔티티는 getter/setter뿐, 로직은 전부 Service에
class Account { long balance; long getBalance(); void setBalance(long b); }
class TransferService {
    void transfer(Account from, Account to, long amount) {
        from.setBalance(from.getBalance() - amount);   // 남의 데이터를 직접 조작
        to.setBalance(to.getBalance() + amount);
    }
}

// after: 풍부한 모델 — 계좌가 자기 잔액을 스스로 책임진다(자율성)
class Account {
    private long balance;
    void withdraw(long amount) {
        if (balance < amount) throw new IllegalStateException("잔액 부족");
        balance -= amount;
    }
    void deposit(long amount) { balance += amount; }
}
class TransferService {
    void transfer(Account from, Account to, long amount) {
        from.withdraw(amount);   // 메시지를 보내고 위임 (묻지 말고 시켜라)
        to.deposit(amount);
    }
}

교차 연결: 리팩터링 3.22(데이터 클래스)·3.9(기능 편애), 클린코드 6장(디미터 법칙)·"묻지 말고 시켜라", 이펙티브 자바 15(접근 최소화)·18(컴포지션). 모두 "객체에게 책임을 주고 내부를 감춰라"는 한 방향을 가리킵니다.


함정 / 주의

  • 과한 의인화/위임도 비용이다. 모든 것을 객체로 쪼개면 협력이 복잡해질 수 있습니다. 변경 압력이 있는 곳부터 자율화하세요(9장 "유연성은 필요할 때만").
  • getter/setter를 습관적으로 열지 마라. 그 순간 객체는 데이터 덩어리로 퇴화합니다.
  • "동작하니 됐다"에 멈추지 마라. 변경 수용 가능성까지가 설계의 목표입니다.

체크리스트 (설계 리뷰용)

  • 한 객체가 다른 객체의 내부 구조(getBag().getXxx() 같은 연쇄)를 알고 있지 않은가
  • 데이터와 그 데이터를 다루는 로직이 같은 객체에 있는가
  • 객체에 묻지 말고(get) 시키고(메시지) 있는가
  • getter/setter를 무심코 전부 열어두지 않았는가
  • 요구사항이 바뀌면 변경이 한 객체 안에 갇히는가, 아니면 줄줄이 번지는가

퀴즈

  1. 절차지향 Theater.enter가 "변경에 취약"한 근본 원인은?
  2. "책임의 이동"이란 무엇이며, 절차지향과 객체지향의 차이에서 왜 핵심인가?
  3. "그래, 거짓말이다!(의인화)"가 설계에 주는 실천적 의미는?
  4. 이 책이 정의하는 "훌륭한 객체지향 설계"는?
  5. 빈약한 도메인 모델을 풍부한 모델로 바꿀 때의 한 줄 지침은?

정답·해설

  1. TheaterBag·TicketOffice내부 구조에 과도하게 의존(결합)하기 때문입니다. 관람객이 결제 수단을 바꾸는 등 작은 변경에도 Theater까지 연쇄적으로 바뀝니다.
  2. 한 객체에 몰려 있던 일을, 그 일을 가장 잘 아는(데이터를 가진) 객체에게 넘기는 것입니다. 데이터와 프로세스가 한 객체로 합쳐지는 이 이동이 절차지향과 객체지향을 가르는 핵심입니다.
  3. 무생물이라도 능동적인 객체로 의인화해, 자기 데이터를 스스로 처리하게 만들라는 뜻입니다. 현실의 수동성에 코드를 가두지 않을 때 더 자율적이고 유연한 설계가 나옵니다.
  4. 서로 협력하는 객체들 사이의 의존성을 적절히 관리하는 설계. 객체 자체보다 객체 간 관계가 설계의 핵심입니다.
  5. "묻지 말고 시켜라." 남의 데이터를 꺼내 직접 조작하지 말고, 그 객체에게 메시지를 보내 스스로 처리하게 하라.

다음 장 예고 — 2장: 객체지향 프로그래밍

영화 예매 시스템을 만들며 협력·객체·클래스, 도메인 구조를 따르는 프로그램 구조, 그리고 상속·다형성·추상화를 통한 유연한 설계를 다룹니다. 1장에서 잡은 "자율성·책임" 감각을 본격적인 객체지향 문법으로 구현하는 장입니다.