본문 바로가기

책/오브젝트

01_객체 설계

영화관을 살펴보자

영화관에는 관객(Audience), 티켓 판매원(TicketSeller), 티켓 판매소(TicketOffice)가 존재한다.

관객은 가방을 가지고 있고, 가방에는 돈(amount), 티켓(ticket), 초대권(Invitation)이 존재한다.

티켓 판매소에는 티켓(ticket)과 돈(amount)가 존재한다.

 

우리는 아래와 같은 동작을 진행하는 프로그램을 만들어 볼 것이다.

 

1. 추첨을 통해 선정된 관람객에게 공연을 무료로 관람할 수 있는 초대장을 발송
    - 초대장에는 날짜가 적혀있다.
2. 이벤트에 당첨된 관람객은 초대장을 티켓으로 교환한 후에 입장.
    - 관람객은 티켓을 얻는다.
3. 이벤트에 당첨되지 않은 관람객은 티켓을 구매해야만 입장 가능.
    - 관람객의 돈은 줄어든다.
    - 관람객은 티켓을 얻는다.
    - ticket 판매소는 돈을 얻는다.
    - ticket 판매소에 존재하는 ticket의 수는 줄어든다.

 


1. 초대권

 

- 날짜가 적힌 초대권이다.

package ticket;

import java.time.LocalDateTime;

public class Invitation {
    private LocalDateTime when;
}

 


2. 티켓

 

- 티켓 요금이 적힌 티켓 클래스를 만들어보자.

package ticket;

public class Ticket {
    private Long fee;

    public Long getFee() {
        return fee;
    }
}

3. 가방

 

- 관객의 가방에는 돈, 티켓, 초대권이 들어있다.

- hasInvitation: 초대권을 가지고 있는지 확인하는 메소드

- getInvitation: 초대권이 있다면 초대권을 확인하는 메소드

- minusAmount: 티켓을 구매하면 관객의 가방에서 돈이 지출되는 메소드

- setTicket: 티켓을 구매하면 가방에 티켓을 집는 메소드

- hasTicket: 영화관에 입장할 때 관객의 가방에 티켓이 존재하는지 확인하는 메소드

 

package ticket;

public class Bag {
    private Long amount;
    private Ticket ticket;
    private Invitation invitation;

    public Bag(Long amount) {
        this.amount = amount;
    }

    public Bag(Long amount, Invitation invitation) {
        this.amount = amount;
        this.invitation = invitation;
    }

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

    public boolean hasTicket() {
        return ticket != null;
    }

    public boolean hasInvitation() {
        return invitation != null;
    }

    public Invitation getInvitation() {
        return invitation;
    }

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

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

4. 관객

 

관객은 가방을 소지하고 있다.

 

package ticket;

public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return bag;
    }
}

5. 티켓 판매소

 

getTicket: 관객에게 티켓을 판매하면 티켓 판매소에서는 티켓이 한개씩 줄어드는 메소드

plusAmount: 관객에게 티켓을 판매하면 티켓 판매소에 존재하는 돈은 늘어나는 메소드

 

package ticket;

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

 


6. 소극장을 구현하는 클래스

 

관객이 입장합니다.

관객에게 초대장이 존재하면

티켓 판매원은 티켓 판매소에서 티켓을 한장 가져옵니다.

가져온 티켓을 관객에게 줍니다.

 

관객에게 초대권이 존재하지 않으면

티켓 판매원은 티켓 판매소에서 티켓을 한장 가져옵니다.

관객은 가방에서 티켓 요금만큼 돈을 꺼냅니다.

티켓 판매원은 티켓 판매소에 가서 돈을 넣어 놓습니다.

관객은 티켓을 가방에 집어 넣습니다.

 

위와 같은 로직을 구현하는 것이다.

 

package ticket;

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. 지금의 코드는 우리의 예상을 벗어나는 코드이다. 그래서 이해하기가 어렵다.

    - 사실 위의 코드는 소극장이 모든 것을 제어하고 있는 형태이다.

    - 소극장이 관객의 가방을 뒤져 티켓을 확인하는 것이고,

    - 소극장이 티켓 판매원에게 티켓 판매소에 가서 티켓을 가져오라고 시키고 있는 형태이다.

 

2. 세부적인 내용까지 기억하고 있어야 한다.

    - Audience가 Bag을 가지고 있는 점을 기억하여야 하며

    - Bag에는 현금과 티켓이 있는 것을 기억하여야 하며

    - ticketSeller가 ticketOffice에서 티켓을 판매하고 

    - ticketOffice에는 돈과 티켓이 포관해 있다는 것을 모두 알아야 코드를 작성 가능하고, 또 이해할 수 있다.

 

3. 가장 중요한 문제는 변경에 취약하다.

    - 관객은 항상 가방을 들고 있어야 한다.

    - 티켓 판매원은 항상 티켓 판매소에서만 티켓을 판매한다고 가정한다.

    - 관객이 가방을 들고 있지 않으면? 관객이 현금이 아니라 신용카드를 쓴다면?

 

3-1 가방이 바뀌었다고 가정해보자

    - Audience 클래스에서 Bag을 제거만 하면 되는 것이 아니라

    - Audience의 Bag에 직접 접근하는 Theater의 enter 메서드 역시 수정해야 한다.

 

이것은 객체 사이의 의존성과 관련된 문제다.

객체 사이의 의존성을 완전히 없애는 것이 정답은 아니지만 최소한의 의존성만 유지하고 불필요한 의존성을 제거하도록 하자.

의존성이 높다 == 결합성이 높다.

 

아래에서는 의존성을 낮추는 작업을 진행해 볼 것이다.


해결 과정 1

 

1. 티켓을 판매하는 역할은 티켓 판매원에게 위임하자!

package ticket;

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

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

 

2. 판매원은 관람객에게 티켓만 전달하고, 관람객으로 부터 요금을 받고, 티켓 판매소에 돈을 전달한다.

 

package ticket;

public class TicketSeller {
    private TicketOffice ticketOffice;


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

    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }

    public void sellTo(Audience audience) {
        Ticket ticket = ticketOffice.getTicket();
        Long fee = audience.buy(ticket);
        ticketOffice.plusAmount(fee);
    }
}

 

3. 관람객은 초대권을 가지고 있으면 티켓 판매원에게 받은 초대권을 가방에 집어 넣고, 초대권이 없으면 티켓을 집어넣고 요금을 준다.

 

package ticket;

public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return bag;
    }

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

}

 

훨씬 코드가 직관적으로 변했다.

더 좋은점은 수정이 용이해졌다는 것이다.

Audience나 TicketSeller가 내부구현을 변경해도 Theater는 함께 변경할 필요가 없어졌다.

 

책임의 이동

    - theater에 몰려 있던 책임이 개별 객체로 이동하였다.

 

각 개체는 자신을 스스로 책임진다.

    - 독재자가 존재하지 않고 각 객체에 책임이 적절하게 분배되었다.

 

각 객체가 하는 역할이 명확해졌다.

    - 객체의 역할이 명확해 지면서 코드를 이해하기 쉬워졌다.

    - TicketSeller의 책임은 무엇인가? 티켓을 판매하는 것이다.

    - Audience의 책임은 무엇인가? 티켓을 사는 것이다.

    - Theater의 책임은 무엇인가? 관람객을 입장시키는 것이다.

 

설계를 어렵게 만드는 것은 의존성이라는 것을 기억하자.

불필요한 의존성을 제거해서 결합도를 낮추자.

 


조금 더 개선해보자.

 

이때까지는 Audience가 bag에 직접 접근하여 티켓을 넣고 돈을 꺼내는 동작을 진행하였다.

현실 세계에서는 위와 같이 동작을 진행하는 것이 맞지만, 객체지향의 세계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀐다.

이처럼 능ㅇ동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화라고 부른다.

Audience 클래스의 buy 메소드를 아래와 같이 수정하고 bag 클래스의 hold 메소드를 추가하여 bag 클래스를 자율적인 존재로 만들어보자.

package ticket;

public class Audience {
    private Bag bag;

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

    public Bag getBag() {
        return bag;
    }

    public Long buy(Ticket ticket) {
        return bag.hold();
    }
}

bag의 클래스를 아래와 같이 수정하게 됨으로써, 스스로 티켓을 집어넣고 돈을 꺼내는 역할을 진행한다.

package ticket;

public class Bag {
    private Long amount;
    private Ticket ticket;
    private Invitation invitation;

    public Bag(Long amount) {
        this.amount = amount;
    }

    public Bag(Long amount, Invitation invitation) {
        this.amount = amount;
        this.invitation = invitation;
    }

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

    public boolean hasTicket() {
        return ticket != null;
    }

    public boolean hasInvitation() {
        return invitation != null;
    }

    public Invitation getInvitation() {
        return invitation;
    }

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

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

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

 

현재 ticketSeller의 역할도 ticketOffice로 부터 티켓을 가져와서 관객에게 전달해 주고 있다.

ticketOffice의 자율성을 아래와 같이 코드를 변경해서 보장해 주도록 하자.

 

package ticket;

public class TicketSeller {
    private TicketOffice ticketOffice;


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

    public TicketOffice getTicketOffice() {
        return ticketOffice;
    }

    public void sellTo(Audience audience) {
//        Ticket ticket = ticketOffice.getTicket();
//        Long fee = audience.buy(ticket);
//        ticketOffice.plusAmount(fee);
        ticketOffice.sellTicketTo(audience);
    }
}
package ticket;

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

    public void sellTicketTo(Audience audience) {
        Ticket ticket = getTicket();
        Long fee = audience.buy(ticket);
        plusAmount(fee);
    }
}

 

모든 클래스가 자율성을 가지게 되었다.

허나 만족스럽지 않다. TicketOffice와 Audience 간의 새로운 의존성이 추가되었기 때문이다.

현재로서는 Ticketoffice의 자율성을 주기 보다는 Audience에 대한 결합도를 낮추는 것이 더 중요하다고 생각한다.

 

오늘 1장을 살펴봄으로써 두가지 사실을 알게 되었을 것이다.

첫째, 어떤 기능을 설계하는 방법은 한가지 이상일 수 있다.

둘째, 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 결국 설계는 트로에디오프의 산물이다. 모든 사람들을 만족 시킬 수 있는 설계는 만들 수 없다.

 

설계는 균형의 예술이다.