Java/책읽기

[책읽기] 오브젝트(1) - 객체, 설계

무토(MUTO) 2020. 12. 10. 20:49

티켓 판매 애플리케이션 만들기

소극장은 관람객들을 모으기 위해 이벤트로 초대장을 배부하였고 관람객을 맞이한다.
관람객을 맞이할 때 초대장이 있는 사람과 없는 사람을 구분해야한다.
초대장이 있는 사람은 초대장을 티켓으로 교환한 후 입장을 할 수 있고
초대장이 없는 사람은 비용을 지불하여 티켓을 획득한 후 입장을 할 수 있다.

위의 내용을 감안하여 다음과 같은 구조의 클래스를 설계하여 1차적인 구성을 완료해보자.

1차 구현코드

Ticket.java

public class Ticket {
    private Long fee;

    public Long getFee() {
        return fee;
    }
}

TicketOffice.java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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

TicketSeller.java

public class TicketSeller {
    private TicketOffice ticketOffice;

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

    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }
}

Invitation.java

public class Invitation {
    private LocalDateTime when;
}

Bag.java

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

Audience.java

public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return bag;
    }
}

Theater.java

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

기본적으로 책에서 1차적으로 제공한 코드이다. 바로 위에 있는 Theater 클래스의 enter() 메소드를 보자.
핵심 비즈니스 로직이 담긴 코드이니 잠시 살펴보자. 이것을 읽으면 무슨 생각이 드는가?
대충 봐도 코드가 너무 길고 의도를 파악하기가 힘들다. 해당 객체가 무슨 일을 하는지도 전혀 알 수 없다.
뒤의 내용에서 책은 이를 개선한 코드를 제공하지만 그것을 그대로 보고 따라하면 학습하는 의미가 없게되기 때문에 책의 개선 사항을 보지 않고 우선적으로 내가 1차적으로 코드를 리팩토링 해보기로 한다.

내가 리팩토링 한 코드

Theater.java

public class Theater {
    private TicketSeller ticketSeller;

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

    public void enter(Audience audience) {
        ticketSeller.changeOrSellTicket(audience);
    }
}

가장 먼저 고치고 싶다는 생각이 든 곳이다.
비즈니스 로직은 결국 티켓 판매원이 해야하는 일이지 극장이 해야하는 일이 아니다.
티켓을 교환하는 과정을 티켓 셀러에게 넘기고 Theater 클래스에는 메소드 한개만 남겼다.
그렇다면 옮긴 코드를 보자.

public class TicketSeller {
    private TicketOffice ticketOffice;

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

    private void addFeeToOffice(Long amount) {
        ticketOffice.plusAmount(amount);
    }

    public void changeOrSellTicket(Audience audience) {
        Ticket ticket = ticketOffice.getTicket();

        if (!audience.hasInvitation()) {
            Long fee = audience.bill(ticket.getFee());
            addFeeToOffice(fee);
        }
        audience.setTicket(ticket);
    }
}

장황했던 코드가 조금 더 깔끔해졌다.
중요한 것은 의도대로 티켓 판매원이 관람객에게 티켓을 전달하게 되었다는 것이다.
그리고 티켓 셀러는 스스로 가지고있던 티켓 오피스와 소통하면서 내부의 의도대로 스스로 책임을 가진 작업만을 처리한다.

public class Audience {
    private Bag bag;

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

    public boolean hasInvitation() {
        return bag.hasInvitation();
    }

    public void setTicket(Ticket ticket) {
        bag.setTicket(ticket);
    }

    public Long bill(Long amount) {
        return bag.minusAmount(amount);
    }

}

관람객은 이제 관람객이 가진 역할만을 수행한다.
관람객은 돈이나 초대장이 가방에 있든 지갑에 있든 중요하지 않다.
그저 어디에선가 돈이나 초대장을 꺼내와서 지불하면 되는 것이다.
또한 외부에서 필요로 하던 bag객체의 로직을 전부 내부에서 처리했다.
따라서 외부에 제공하던 getBag() 메소드 또한 필요 없어졌다.

책에서 제공한 1차 개선 코드

Ticket.java

public class Ticket {
    private Long fee;

    public Long getFee() {
        return fee;
    }
}

TicketOffice.java

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

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

TicketSeller.java

public class TicketSeller {
    private TicketOffice ticketOffice;

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

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

Invitation.java

public class Invitation {
    private LocalDateTime when;
}

Bag.java

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

Audience.java

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

Theater.java

public class Theater {
    private TicketSeller ticketSeller;

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

    public void enter(Audience audience) {
        ticketSeller.sellTo(audience);
    }
}

나의 코드와 다른점을 살펴보면 책은 TicketSeller가 몰라도 되는 정보를 하나 제거했고
Audience가 지니는 책임을 하나 더 추가했다.
그 결과 코드의 의도가 더욱 분명하게 드러난다.
나의 코드는 티켓 셀러가 관람객의 가방을 뒤져 교환권이 있는지 없는지 확인을 한다.
사실 티켓 셀러는 관람객이 교환권을 가져오거나 돈을 가져오면 그저 티켓을 관람객에게 전달해주면 될 뿐이다.
굳이 관람객이 교환권이 있는지 확인을 할 필요가 없었다.
한번 더 생각을 해서 코드를 작성했어야 했던 것 같다.

이 이후에도 Bag이 자율성을 가지게 리팩토링 하는것도 가능하다.

하지만 그 경우에는 TicketOffice와 Audience 사이에 의존성이 생겨버리게 된다.

즉, 높은 결합도와 높은 자율성 사이에 트레이드 오프를 해야한다.

코드를 작성하며 항상 생각 해야 하는 것들

항상 객체가 어떤 책임을 지녀야 하는가를 생각해야한다.
코드의 의도가 드러나는가를 파악해야한다.

'Java > 책읽기' 카테고리의 다른 글

[책읽기] 오브젝트(2) - 객체지향 프로그래밍  (0) 2020.12.17