책 - 오브젝트 - Chapter 01 객체, 설계

By: Jun Heo
Posted: July 14, 2022

01 티켓 판매 애플리케이션 구현하기

소극장에 무료 이벤트 초대권을 가진 관객과 티켓을 사서 보는 관객이 공연을 보러 온다.

public class Invitation {
  private LocalDateTime when;
}

public class Ticket {
  private Long fee;
  
  public Long getFee() {
    return fee;
  }
}

public class Bag {
  private Long amount;
  private Invitation invitation;
  private Ticket ticket;
  
  public Bag(long amount) {
    this(null, amount);
  }
  
  public Bag(Invitation invitation, long amount) {
    this.invitation = invitation;
    this.amount = amount;
  }
  
  public boolean hasInvitation() {
    return invitation != null;
  }
  
  public boolean hasInvitation() {
    return invitation != null;
  }
  public boolean hasTicket() {
    return ticket != null;
  }
  public void setTicket(Ticket ticket) {
    this.ticket = ticket;
  }
  public void calculateAmount(Long amount) {
    this.amount += amount;
  }
}
public class Audience {
  private Bag bag;
  
  public Audience(Bab bag) {
    this.bag = bag;
  }
  
  public Bag getBag() {
    return bag;
  }
}
public class TicketOffice {
  private Long amount;
  private List<Ticket> tickets = new ArrayList<>();
  
  public TicketOffice(Long amount, Ticket ... tickets) {
    this.amount = amount;
    this.tickets.addAll(Arrays.asList(tickets));
  }
  
  public Ticket getTicket() {
    return tickets.remove(0);
  }

  public void calculateAmount(Long amount) {
    this.amount += amount;
  }
}
public class TicketSeller {
  private TicketOffice ticketOffice;

  public TicketSeller(TicketOffice ticketOffice) {
    this.ticketOffice = ticketOffice;
  }

  public TicketOffice getTicketOffice() {
    return ticketOffice;
  }
}
public class Theater {
  private TicketSeller ticketSeller;

  public Theater(TicketSeller ticketSeller) {
    this.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().calculateAmount(-ticket.getFee());
      ticketSeller.getTicketOffice().calculateAmount(ticket.getFee());
      audience.getBag().setTicket(ticket);
    }
  }
}

Audience -> Bag -> Invitation

→ Ticket

TicketSeller -> TicketOffice

02 무엇이 문제인가

예상을 빗나가는 코드

이해 가능한 코드란 그 동작이 우리의 예상에서 크게 벗어나지 않는 코드다. 현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건넨다. 하지만 코드 안의 관람객, 판매원은 그렇게 하지 않는다. 현재의 코드는 우리의 상식과는 너무나도 다르게 동작하기 때문에 코드를 읽는 사람과 제대로 의사소통하지 못한다.

코드를 이해하기 어렵게 만드는 또 다른 이유가 있다. 이 코드를 이해하기 위해서는 여러 가지 세부적인 내용들을 한꺼번에 기억하고 있어야 한다는 점이다.

03 설계 개선하기

자율성을 높이자

설계를 변경하기 어려운 이유는 Theater가 Audience와 TicketSeller뿐만 아니라 Audience 소유의 Bag과 TicketSeller가 근무하는 TicketOffice까지 마음대로 접근할 수 있기 때문이다. 해결 방법은 Audience와 TicketSeller가 직접 Bag과 TicketOffice를 처리하는 자율적인 존재가 되도록 설계를 변경하는 것이다.

첫 번째 단계는 Theater의 enter 메서드에서 TicketOffice에 접근하는 모든 코드를 Ticket Seller 내부로 숨기는 것이다.

public class TicketSeller {
  private TicketOffice ticketOffice;

  public TicketSeller(TicketOffice ticketOffice) {
    this.ticketOffice = ticketOffice;
  }

  // public TicketOffice getTicketOffice() {
  //   return ticketOffice;
  // }
  public void sellTo(Audience audience) {
    if (audience.getBag().hasInvitation()) {
      Ticket ticket = ticketOffice.getTicket();
      audience.getBag().setTicket(ticket);
    } else {
      Ticket ticket = ticketOffice.getTicket();
      audience.getBag().calculateAmount(-ticket.getFee());
      ticketOffice.calculateAmount(ticket.getFee());
      audience.getBag().setTicket(ticket);
    }
  }
}

public class Theater {
  private TicketSeller ticketSeller;

  public Theater(TicketSeller ticketSeller) {
    this.ticketSeller = ticketSeller;
  }

  public void enter(Audience audience) {
   // This logic move to TicketSeller.sellTo()
   ticketSeller.sellTo(audience);
  }
}

TicketSeller 다음으로 Audience의 캡슐화를 개선하자.

public class Audience {
  private Bag bag;

  public Audience(Bab bag) {
    this.bag = bag;
  }

  // public Bag getBag() {
  //   return bag;
  // }

  public Long buy(Ticket ticket) {
    if (bag.hasInvitation()) {
      bag.setTicket(ticket);
      return 0L;
    } else {
      bag.setTicket(ticket);
      bag.calculateAmount(-ticket.getFee());
      return ticket.getFee();
    }
  }
}

public class TicketSeller {
  private TicketOffice ticketOffice;

  public TicketSeller(TicketOffice ticketOffice) {
    this.ticketOffice = ticketOffice;
  }

  // public TicketOffice getTicketOffice() {
  //   return ticketOffice;
  // }
  public void sellTo(Audience audience) {
    ticketOffice.calculateAmount(audience.buy(ticketOffice.getTicket()));
  }
}

어떻게 한 것인가

다시 말해 자기 자신의 문제를 스스로 해결하도록 코드를 변경한 것이다.

그래, 거짓말이다!

레베카 워프스브록은 이처럼 능동적이고 자율적인 존재로 스프트웨어 객체를 설계하는원칙을 가리켜 의인화(anthropomorphism)라고 부른다.

따라서 이해하기 쉽고 변경하기 쉬운 코드를 작성하고 싶다면 차라리 한 편의 애니메이션을 만든다고 생각하라. 다른 사람의 코드를 읽고 이해하는 동안에는 애니메이션을 보고 있다고 여러분의 뇌를 속여라. 그렇게 하면 코드 안에서 웃고, 떠들고, 화내는 가방 객체를 만나더라도 당황하지 않을 것이다.