티스토리 뷰

티켓 판매 어플리케이션

  • 공연장의 공연을 보기 위해서 관람객은 티켓이 필요하다.
  • 이벤트를 진행하는데 관람객이 초대권을 가지고 있을경우 무료로 관람 가능하다.
  • 초대권이 있을경우 초대권을 교환하여 입장가능하며 없을경우 돈을 지불하고 입장 가능하다.

개선 전 소스

관람객의 경우 소유금액, 티켓, 초대권을 가지고있는 가방을 소유하고있다.

/**
 * 관람객 도메인 클래스
 * 소지품을 보호하기 위해 가방을 소지한다.
 */
@RequiredArgsConstructor
@Getter
public class Audience {
    private final Bag bag;
}
/**
 * 관람객의 가방을 나타내는 도메인 오브젝트
 */
public class Bag {

    /** * 보유 금액 */
    private Long amount = 0L;
    /** * 초대권 */
    private Invitation invitation;
    /** * 관람 티켓 */
    @Setter private Ticket ticket;

    /**
     * 초대권이 있는경우
     * @param invitation 초대장
     * @param amount 보유금액
     */
    public Bag(Invitation invitation, Long amount) {
        this.invitation = invitation;
        this.amount = amount;
    }

    /**
     * 초대권이 없고 돈만 있는경우
     * @param amount 보유금액
     */
    public Bag(Long amount) {
        this(null, amount);
    }

    /**
     * 초대장의 보유 여부를 판단한다.
     * @return 초대장 보유여부
     */
    public boolean hasInvitation() {
        return invitation != null;
    }

    /**
     * 티켓의 소유 여부를 판단한다.
     * @return 티켓의 소유 여부
     */
    public boolean hasTicket() {
        return ticket != null;
    }

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

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

초대권 정보

@Getter
@RequiredArgsConstructor
public class Invitation {
    // 공연을 관람할 수 있는 초대일자 (초대장)
    private final LocalDateTime when;
}

티켓 정보

@Getter
@ToString
public class Ticket {
    private final Long fee;

    /**
     * 공연을 관람하기 원햐는 모든 사굄들은 티켓을 소지해야한다.
     * @param fee 공연가격
     */
    public Ticket(Long fee) {
        this.fee = fee;
    }
}

매표소 정보

  • 매표소는 총 판매 금액과 판매 티켓 목록을 가지고있다.

/**
 * 매표소 도메인 오브젝트
 */
@ToString
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 this.tickets.remove(0);
    }

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

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

티켓 판매자 정보

  • 자신이 일하는 매표소의 정보를 가지고있다.

/**
 * 티켓 판매원 도메인 오브젝트
 */
@AllArgsConstructor
@ToString
public class TicketSeller {
    /** * 자신이 일하는 매표소 */
    @Getter private final TicketOffice ticketOffice;
}

소극장 정보

  • 관람객 입장시 초대권 소지여부를 파악한다.

  • 초대권을 가지고있을시 티켓을 배부한다.

  • 초대권이 없을시 티켓 가격만큼 관람객 금액에서 차감하고 티켓을 배부한다.

@RequiredArgsConstructor
@ToString
public class Theater {
    private final TicketSeller ticketSeller;

    public void enter(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            System.out.println("audience have an invitation!!");
            Ticket ticket = this.ticketSeller.getTicketOffice().getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            System.out.println("audience don`t have an invitation!!");
            Ticket ticket = this.ticketSeller.getTicketOffice().getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

실행

public class TicketSalesMain {
    public static void main(String[] args) {
        // 티켓 2장 생성
        Ticket ticket_1 = new Ticket(100L);
        Ticket ticket_2 = new Ticket(100L);
        // 매표소 생성
        TicketOffice ticketOffice = new TicketOffice(0L, ticket_1, ticket_2);
        // 판매원 생성
        TicketSeller ticketSeller = new TicketSeller(ticketOffice);

        // 초대권을 가진 관람객을 생성 후 극장 입장
        Bag hasInvitationBag = new Bag(new Invitation(LocalDateTime.now()), 0L);
        Audience hasInvitationAudience = new Audience(hasInvitationBag);
        Theater theater = new Theater(ticketSeller);
        theater.enter(hasInvitationAudience);
        System.out.println("hasInvitationAudience :: " + theater);

        // 초대권이 없는 관람객을 생성 후 극장 입장
        Bag dontHaveInvitationBag = new Bag(100L);
        Audience dontHaveInvitationAudience = new Audience(dontHaveInvitationBag);
        theater.enter(dontHaveInvitationAudience);

        System.out.println("dontHaveInvitationAudience :: " + theater);
    }
}

문제점

소프트웨어 모듈이 가져야 햐는 세 가지 기능
(클린 소프트웨어: 애자얼 원칙과 패던, 그러고 실천 방법)

1. 실행 중에 제매로 동작.
2. 변경을 위해 존재. (변경 용이)
3. 코드를 읽는사람과 의사소통. (읽기 쉬움)

 

위의 코드에서 관람객과 판매원은 극장(Theater)의 명령을 받는 수동적인 존재.

극장이 관람객의 가방에서 돈을 가져오고, 티켓을 넣어준다.
극장이 판매원의 매표소에서 티켓을 가져오고 돈을 넣어준다.

여러가지 세부사항을 한번에 기억해야한다.

극장 입장(enter)시에 관람객이 가방을 가지고있고 판매자가 매표소를 가지고있는 등의 세부사항을 모두 알고있다.
이는 이해하기 어려울 뿐만 아니라 관람객(Audience)나 판매자(TicketSeller)를 변경시 극장(Theater)도 변경해야함.

높은 의존성

한 클래스가 다른 클래스의 내부에대해 많이 알게되면(의존성) 클래스의 변경이 힘들어진다.
의존성은 어떤 객체가 번경될 때 그 객 체에게 의존히는 다른 객체도 험께 변경될 수 있다는 시실이 내포되어 있다.

우리의 목표는 애풀러케이션의 기능을 구현하는 뎨 필요한 최소한의 의존성만 유지하고 불필요한 의존셩을 제거히는 것이다.


개선점

극장(Theater)가 관람객(Audience)과 티켓 판매원(TicketSeller)에 관해 세세한 부분을 알지 못하도록 한다.(캡슐화)

관람객과 티켓 판매원을 자율적인 존재로 만든다.

개선 후 코드

Theater에서 TicketOffice로 접근했던 모든 코드를 TicketSeller 내부로 옮긴다.

@RequiredArgsConstructor
@ToString
public class Theater {

    /**
     * 티켓 판매원 정보
     */
    private final TicketSeller ticketSeller;

    /**
     * 관람객 극장 입장
     * @param audience 관람객 정보
     */
    public void enter(Audience audience) {
        this.ticketSeller.sellTo(audience);
    }
}
/**
 * 티켓 판매원 도메인 오브젝트
 */
@AllArgsConstructor
@ToString
public class TicketSeller {
    /** * 자신이 일하는 매표소 */
    private final TicketOffice ticketOffice;

    /**
     * 티켓 판매원이 관람객에게 티켓을 판매한다.
     * @param audience 관람객 정보
     */
    public void sellTo(Audience audience) {
        if (audience.getBag().hasInvitation()) {
            System.out.println("audience have an invitation!!");
            Ticket ticket = this.ticketOffice.getTicket();
            audience.getBag().setTicket(ticket);
        } else {
            System.out.println("audience don`t have an invitation!!");
            Ticket ticket = this.ticketOffice.getTicket();
            audience.getBag().minusAmount(ticket.getFee());
            this.ticketOffice.plusAmount(ticket.getFee());
            audience.getBag().setTicket(ticket);
        }
    }
}

TicketSeller에서 TicketOfiice의 @Getter가 사라짐으로써 TicketOfiice는 TicketSeller의 내부에서만 접근 가능해짐.
이처럼 세부사항을 안으로 감춤으로써 변경하기 쉬운코드를 만들고 객체사이의 결합도를 낮출수 있다.

이것이 바로 캡슐화!

현재 Bag인스턴스도 TicketSeller의 통제를 받고있으므로 TicketSeller와 같은 방법으로 Audience 내부로 Bag을 감춘다.

/**
 * 관람객 도메인 클래스
 * 소지품을 보호하기 위해 가방을 소지한다.
 */
@RequiredArgsConstructor
public class Audience {
    private final Bag bag;

    public Long buy(Ticket ticket) {
        if (this.bag.hasInvitation()) {
            this.bag.setTicket(ticket);
            return 0L;
        } else {
            this.bag.minusAmount(ticket.getFee());
            this.bag.setTicket(ticket);
            return ticket.getFee();
        }
    }
}
/**
 * 티켓 판매원 도메인 오브젝트
 */
@AllArgsConstructor
@ToString
public class TicketSeller {
    /** * 자신이 일하는 매표소 */
    private final TicketOffice ticketOffice;

    /**
     * 티켓 판매원이 관람객에게 티켓을 판매한다.
     * @param audience 관람객 정보
     */
    public void sellTo(Audience audience) {
        ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
    }
}

위와 같이 TicketSeller내부에서 Bag을 접근하는 소스는 사라졌으며 Audience의 Bag에 @Getter를 없애 캡슐화하였음.

이로써 Audience내부를 수정하더라도 TicketSeller에는 영향을 주지않는다.


바뀐점

  • Audience와 TicketSeller가 수동적인 존재에서 자신의 내부 데이터를 스스로 처리하는 능동적인 존재로 변함.
  • Audience와 TicketSeller의 내부를 변경하더라도 외부의 변경을 피요하지 않음.
  • 자신의 문제를 스스로 해결할수 있는 존재로 변경함.

캡슐화와 응집도

객체 내부는 캡슐화하고 객체간에 오직 메시지로만 상호작용 하는것이 핵심!

자신과 밀접하게 연관된 작업만 수행하고 연관이 없는 작업은 다른 객체에게 위임하는 객체를 응집도가 높다고 할수 있다.
응집도를 높이기 위해서는 자신의 데이터는 스스로 책임져야한다.

절자지향과 객체지향

개선전 코드에서는 Theater의 enter에서 모든 작업을 수행(Process)하며 나머지 오브젝트는 데이터이다. - 절차지향

개선후 코드에서는 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍 함 - 객체지향

책임의 이동

개선전에는 모든 처리의 책임이 Theater의 enter에 집중되어 있었음.

개선후에는 책임이 각자의 객체에 적절히 분배되어짐.


결론

코드 설계를 어렵게 하는것은 의존성이다.

불필요한 의존성을 없애므로써 결합도를 낮춰야한다.

위의 개선코드에서는 캡슐화를 통해 자율성을 높히고 결합도를 낮추었다.


느낀점

내가 지금까지 개발해왔던 방식은 절차지향적 개발이었다.

언어는 객체지향 언어를 사용했으면서 제대로 사용하지 못했던것이다.

면접이나 시험공부로나마 객체지향의 특징중 캡슐화를 못이박히도록 들었지만 그 개념만 알뿐 실제 어떻게 사용하는지는 알지 못했다.

객체 설계의 핵심은 각자가 맡은 책임만을 수행해야하며 객체끼리 세세한 부분은 알지 못하게 캡슐화 하여 의존도를 낮춰야한다.

자신과 관계없는 작업은 다른 객체와 메시지를 통해 의사소통 해야하며 각 객체의 접근 통로는 최대한 좁혀야한다. (내생각)

이제야 1장을 읽었고 아직도 각 개념이 애매하지만 계속해서 읽어가다보면 진짜 객체지향을 알아갈수 있지 않을까 생각한다.

 

GIT

위의 코드로 부족할거 같아서 git을 첨부한다.

commit별로 diff해서 보면 좀더 이해하기 편할거 같다.

https://github.com/oodmumc3/object-book-ticket-sales

댓글