공부기록

오브젝트 1장 본문

일단써

오브젝트 1장

코타쿠 2021. 12. 6. 17:35

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

티켓 판매 어플리케이션을 구현하기 위한 다음과 같은 코드들이 있다.


# 영화관 초대장 클래스이다. 초대장이 있으면 티켓이 무료이다.
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 hasTicket(){
        return ticekt != 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 void plusAmount(Long amount){
        this.amount += amount;
    }
}
# 관객이다. 가방을 들 수 있다.
public class Audience{
    private Bag bag;

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

    public void plusAmount(Long amount){
        this.amount += amount;
    }
}
# 티켓 판매원이다. 매표소를 가진다.
public class TicketSeller{
    private TicketOffice ticketOffice;

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

    public TicketOffice getTicketOffice(){
        return ticketOffice;
    }
}
# 영화관이다. enter 메서드를 주목하라.

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);
        }
    }
}

Theater클래스에 큰 문제가 있다. 이렇게 짜면 영화관 클래스는 TicketSeller, TicketOffice, Ticket, Audience, Bag 클래스에 모두 의존하게된다(의존성 은 한 객체의 변화가 다른 객체의 변화를 일으키는 성질을 말한다.). Theater 클래스가 나열한 각 클래스의 메서드들을 직접 호출하기 때문이다. 이렇게 짜여진 코드를 결합도가 높다고 말한다.

왜 의존성을 가지게 된것일까? 각 객체가 자신의 데이터에 대한 프로세스를 가지지 않기 때문이다. 각 객체가 자신의 데이터에 대한 책임을 맡지 않고 Theater에 전가했기 때문이다. 이 때문에 다음과 같은 문제가 벌어진다.

  1. 예상가능한 코드가 아니게 된다.
    현실세계에서는 영화관의 판매원이 매표소에서 표를 판매한다. 하지만 위의 코드는 영화관이 판매원의 매표소에서 표를 판매하게 된다. 이처럼 책임의 주객전도가 일어나게 되면서 읽기 어려운, 현실세계와의 개념과 거리가 먼 코드가 짜여지게 된다.

  2. 변경에 취약한 코드가 된다.
    위 코드는 관람객의 가방을 직접 꺼내서 쓰고 있다. 만약에 상황이 바뀌어 가방을 쓰지않고 카드를 쓴다면? 이 코드 또한 수정이 되어야 한다. 즉 관람객의 변화에 의해 영화관도 그에 맞춰 수정되어야 하는 변경에 취약한 상황이 만들어 진다.

이 때문에 우리는 의존성을 줄이는 코드를 만들어야 한다.

아래는 의존성을 줄이도록 바꾼 코드이다.

# 최적화된 영화관이다. enter 메서드를 주목하라.

public class Theater{
    private TicketSeller ticketSeller;

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

    public void enter(Audience audience){
        ticketSeller.sellTo(audience);
    }
}
# 최적화된 티켓 판매원이다. 매표소를 가진다.
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){
        return bag.hold(ticket);
    }
}
# 돈, 초대장, 티켓을 담는 가방이다.
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 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 boolean hasTicket(){
        return ticekt != 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 void plusAmount(Long amount){
        this.amount += amount;
    }
}

위 코드에서 영화관은 영화관의 의존성이 확 낮아졌다. 영화관은 판매원을 관객에게 표를 팔으라고 메세지를 보내는 것에 그침으로써 판매원 이외의 클래스에 의존하지 않는다. 그리고 판매원은 매표소에서 표를 파는 자신의 책임을 스스로 다한다. 즉 객체가 스스로의 책임을 스스로 다하는 자율성을 강화하였다.


핵심은 객체 내부의 자율성을 강화하고 객체 간에는 오직 메세지를 통해서만 상호작용하도록 만드는 것이다. 객체가 자신에게 밀접하게 연관된 작업만 수행하고 연관성 없는 작업은 다른객체 에게 위임하는 것을 응집도가 높다고 한다. 객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 처리해야한다.


클래스의 필드는 데이터이고, 메서드는 프로세스라고 하자. 프로세스와 데이터를 서로 다른 모듈에 적재하는 방식을 절차적 프로그래밍이라고 한다. 절차적 프로그래밍에서는 프로세스를 구현하는 모듈이 다른 클래스에 의존하게된다. 절차적 프로그래밍은 우리의 직관에 벗어나고, 데이터의 구조변경으로 인한 영향을 고립시키기가 어렵다.


변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계이며, 절차적 프로그래밍은 프로세스가 모든 데이터에 의존하기에 변경에 취약할 수 밖에 없다.


해결 방법은 어떤 데이터를 사용하는 프로세스가 그 데이터의 내부로 이동시키는 방법인데 이렇게 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍 하는 방식을 객체지향 프로그래밍이라고 부른다. 객체지향의 핵심은 캡슐화를 통해 의존성 전파를 막아 결합도를 낮추는 것이다.


절차형 프로그래밍과 객체지향 프로그래밍의 차이는 책임의 이동에 있다. Theater에 몰려있던 책임, 프로세스를 각 데이터를 담는 클래스에 이양함으로써 자율성과 응집도를 강화했다.


사실 객체지향에서 모든 객체는 능동적이게 된다. 영화관, 티켓, 가방과 같은 사물도 실제에선 수동적인 대상이지만 코드에서는 능동적으로 책임을 다한다. 이를 의인화라고한다.


설계란 코드를 배치하는 것이다. 라고 하는데 이에 따라 절차형 설계와 객체지향 설계는 서로 다른 설계가 될 것이다. 그러면 좋은 설계는 무엇일까? 기능을 다하면서 변경을 수용하는 코드를 짜는 것이다. 디버깅을 하고 새로운 요구사항을 수렴하기 용이하게 하기 위함이다.


절차시향에 따라 코드를 설계하더라도, 한 객체가 다른 객체의 메서드를 호출하는 의존성이 존재하게 된다. 객체지향 설계는 이 의존성을 적절하게 관리하는 데에 핵심이 있다.

'일단써' 카테고리의 다른 글

logback log level 설정  (0) 2022.06.19
pandas 특수문자가 들어간 query가 있을 때  (0) 2022.02.18
LinkedHashMap, HashMap 차이점  (0) 2021.11.09
PL에서의 Orthogonality  (0) 2021.11.07
데이터 무결성  (0) 2021.11.07