오브젝트(코드로 이해하는 객체지향 설계)

1. 객체, 설계

01. 티켓 판매 어플리케이션

  • 메인은 소극장

  • 초대장을 가지고 있으면 티켓으로 교환입장

  • 초대장이 없으면 현금으로 구매후 입장

  1. 소극장은 관람객의 가방을 열어 초대장이 있는지 확인

  2. 초대장이 있으면 매표소에 보관되어 있는 티켓을 관람객의 가방으로 이동

  3. 초대장이 없으면 관람객의 가방에서 티켓 금액만큼의 돈을 꺼내 매표소에 적립

  4. 매표소에 보관되어 있는 티켓을 관람객의 가방으로 옮긴다

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().minusAmount(ticket.getFee());
ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket);
}
}
}

public class TicketSeller {
private TicketOffice ticketOffice;

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

public TicketOffice getTicketOffice() {
return ticketOffice;
}
}


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 minusAmount(Long amount) {
this.amount -= amount;
}

public void plusAmount(Long amount) {
this.amount += amount;
}
}


public class Ticket {
private Long fee;

public Long getFee() {
return fee;
}
}


public class Audience {
private Bag bag;

public Audience(Bag bag) {
this.bag = bag;
}
public Bag getBag() {
return bag;
}
}

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 hasTicket() {
return ticket != null;
}

public void setTicket(Ticket ticket) {
this.ticket = ticket;
}

public void minusAmount(Long amount) {
this.amount -= amount;
}

public void plusAmount(Long amount) {
this.amount += amount;
}
}


public class Invitation {
private LocalDateTime when;
}




02. 무엇이 문제인가

  • 소프트웨어 모듈의 세가지 목적

    1. 정상적인 동장

    2. 변경을 위해 존재

    3. 코드를 읽는 사람과 의사소통

    제대로 실행되고, 변경이 용이하며,이해하기 쉬워야 한다.

    변경하기 어려운 모듈은 제대로 동작하더라도 개선되야 한다

위 설계의 문제점

  • 예상을 빗나가는 코드

    • 이해 가능한 코드란 그 동작이 우리 예상에서 크게 벗어나지 않는 코드

      • 보통 소극장이 매표소에 티켓과 현금을 마음대로 접근하고 관람객의 가방을 뒤지지는 않는다

  • 변경에 취약하다

    • 현금결제 말고 카드로 결제하면 어떻게 할 것인가

    • 이것은 객체 사이의 의존성(dipendency) 문제

    • 객체사이의 의존성이 과한 경우 결합도(coupling)가 높다고 말한다



설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것


03. 설계 개선하기

  • 정상적으로 동작하지만, 변경에 취약하고, 의사소통에 문제가 있다

    • Theater가 관람객의 가방과 판매원의 매표소의 직접 접근하는게 문제

  • 관람객과 판매원이 자신의 일을 스스로 하지 못하는 문제

    • 관람객과 판매원을 자율적인 존재로 만들어야 한다

해결방법은 Theater가 Audience와 TicketSeller에 관해 너무 세세한 부분까지 알지 못하도록 수정

관람객이 가방을 가지고 있든, 판매원이 티켓을 판매하든 Theater가 알필요가 없다

Theater가 원하는 것은 관람객이 소극장에 입장하는 것 뿐.

따라서 관람객이 스스로 현금과 초대장을 처리하고, 판매원이 스스로 매표소의 티켓과 판매요금을 관리하면 해결된다.


자율성을 높이자

  1. TicketSeller의 캡슐화 개선

    • Theater.class > enter() 에서 TicketOffice.class에 접근하는 모든 코드를 TicketSeller.class 내부로 숨기는 것

    • Theater.class에서는 TicketOffice.class에 직접 접근하지 못하게 한다

      • Theater에서 TicketOffice로의 의존성이 제거

  1. Audience의 캡슐화 개선

    • Audience 의 Bag 인스턴스에 접근하는 객체가 Theater에서 TicketSeller 로 바꼈을 뿐, Audience는 여전히 자율적인 존재가 아니다.

    • TicketSeller에서 Bag객체에 접근하는 모든 로직을 Audience 내부로 감춘다

    • Audience는 자신의 가방에 초대장이 있는지를 스스로 확인하고, 제3자가 열어보도록 허용하지 않음

    public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public void sellTo(Audience audience) {
    ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
    }

    public class Audience {
    private Bag bag;

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

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

    /*
    * public Bag을 제거함으로
    * Audience에서만 접근 가능
    */
    // public Bag getBag() {
    // return bag;
    // }
    }

무엇이 개선됐고, 어떻게 한 것 인가

  • TickketSeller와 Audience 사이의 겨홈도가 낮아지고 내부 구현이 캡슐화 됐으므로 Audience의 구현을 수정해도 TicketSeller에는 영향을 미치지 않는다.

  • as-is는 Theater가 Audience와 TicketSeller의 상세한 구현까지 알고 있어야 해서, 강하게 결합되어 있었고, Audience와 TicketSeller의 사소한 수정에도 Theater가 영향을 받았다.

  • to-be는 Theater가 직접적으로 내부에 접근하지 않고 Audience는 Bag 작업으로 스스로 처리하며 외부에 자신의 가방을 열어보도록 허용하지 않는다. TicketSeller 역시 매표소에 보관된 티켓을 직접 판매하도록 함



캡슐화와 응집도

  • 객체 내부의 상태를 캡슐화 하고 객체 간에 오직 메시지를 통해서만 상호작용하도록 만들었다.

    • Theater는 TicketSeller 내부에 대해서는 모르고, TicketSeller역시 Audience 내부에 모른다

  • 밀접하게 연관된 작업만을 수행하고, 연관성 없는 작업은 다른 객체에게 위함하는 객체를 가리켜 응집도(cohesion)이 높다고 한다.

  • 자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 겹합도를 낮추고 응집도를 높일 수 있다


책임의 이동

  • 변경전의 코드는 모든 책임이 Theater에 몰려있었다. (안좋아)

    • TicketSeller의 책임은 티켓을 판매

    • Theater의 책임은 관람객을 입장시키는 것

    • Audience의 책임은 티켓을 사는 것

설계를 어렵게 만드는 것은 의존성이다

해결 방법은 불필요한 의존성을 제거하여 결합도록 낮추고

결합도를 낮추기 위해 선택한 방법은 세부사항을 캡슐화 하는 것이다

결과적으로 불필요한 세부사항을 객체 내부로 캡슐화 하는 것은

객체의 자율성을 높이고 응집도 높은 객체들의 공동체를 창조 할 수 있게 한다.



더 개선할 수 있다

  1. Bag을 자율적인 존재로

    • Bag내부에 접근하는 모든 로직을 Bag안으로 캡슐화

      • public 메서드들은 외부에서 사용하지 않고 내부에서만 사용하게 private으로 변경


    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();
    }
    }
    public boolean hasInvitation() {
    return invitation != null;
    }
    public void setTicket(Ticket ticket) {
    this.ticket = ticket;
    }
    public void minusAmount(Long amount) {
    this.amount -= amount;
    }
    }

    public class Audience {
    private Bag bag;

    public Audience(Bag bag) {
    this.bag = bag;
    }
    public Long buy(Ticket ticket) {
    return bag.hold(ticket);
    }
    }
  2. TicketOffice의 자율권


    public class TicketOffice {
    private Long amount;
    private List<Ticket> tickets = new ArrayList<>();

    public void sellTicketTo(Audience audience) {
    plusAmount(audience.buy(getTicket()));
    }
    private Ticket getTicket() {
    return tickets.remove(0);
    }
    private void plusAmount(Long amount) {
    this.amount += amount;
    }
    }

    public class TicketSeller {
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice){
    this.ticketOffice = ticketOffice;
    }
    public void sellTo(Audience audience) {
    ticketOffice.sellTicketTo(audience);
    }
    }



04. 객체지향 설계

설계란 코드를 배치하는 것이다.

좋은 설계란

  • 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용할 수 있는 설계

  • 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계



+ Recent posts