[오브젝트] 1장 - 티켓 판매 애플리케이션 구현하기

객체지향적이라는 것은 각자의 역할이 분리된 것이지 Join, In 같은 걸 쓰지 말라는 게 아니다.

객체 내부에 캡슐화 시켜서 그 인터페이스가 어떤 역할을 하는지 몰라도 상관 없어도 되는게 객체지향적인 거다.

 

예를 들어 할인 정책을 활용해서 금액을 차감하는 코드를 짠다고 할 때, 할인 정책에 대해서 몰라도 서비스 로직을 개발할 수 있어야한다. 해당 서비스 로직을 알아야 개발할 수 있는 상황이라면 객체지향적이지 않은 거다.

 

티켓 판매 애플리케이션 구현

  1. 이벤트 당첨 여부를 확인
  2. 당첨자가 아닌 경우에는 티켓을 판매한 후에 입장

 

관람객(Audience)
  1. 가방(Bag)을 소지

 

관람객(Audience)의 소지품 가방(Bag)
  1. 초대장(Invitaion)
  2. 티켓(Ticket)

 

관람객(Audience)의 소지품 가방(Bag) 경우의 수
  1. 현금과 초대장(Invitaion)
  2. 초대장(Invitaion) 없이 현금

 

매표소(TicketOffice)
  1. 관람객(Audience)에게 판매할 티켓(Ticket)
  2. 티켓(Ticket)의 판매 금액

 

판매원(TicketSeller)
  1. 자신이 일하는 매표소(TicketOffice)를 알고 있어야 한다.

 

소극장(Theater)
  1. 가방(Bag) 안의 초대장(Invitation) 들어있는지 확인
  2. 초대장(Invitaion)이 있다면, 판매원(TicketSeller)에게서 받은 티켓(Ticket)을 가방(Bag) 안에 넣어준다.
  3. 가방(Bag) 안의 초대장(Invitation) 이 없다면 티켓(Ticket) 판매

 

문제점 1

  1. 관람객(Audience)의 입장
    • 소극장(Theater)이라는 제3자가 초대장(Invitaion)을 확인하기 위해 관람객(Audience)의 가방(Bag)을 마음대로 열어 본다.
  2. 판매원(TicketSeller)의 입장
    • 소극장(Theater)이 여러분의 허락도 없이 매표소(TicketOffice)에 보관 중인 티켓(Ticket)과 현금에 마음대로 접근
  3. 기억해야할 세부사항이 너무 많다.
    • 관람객(Audience)이 가방(Bag)을 가지고 있다.
    • 가방(Bag)  안에 현금과 티켓(Ticket)이 들어있다.
    • 판매원(TickectSeller)이 매표소(TicketOffice)에서 Ticket(티켓)을 판매한다.
    • 매표소(TicketOffice) 안에 현금과 티켓(Ticket)이 보관돼 있다.
  4. 변경에 취약하다.
    • 관람객(Audience)과 판매원(TickectSeller)이 변경되면, 소극장(Theater)에도 영향이 간다.
    • 세부적인 사실에 의존성이 강하여, 예외 케이스가 생긴다.
      • 관람객(Audience)이 가방(Bag)이 없고, 신용카드로 결제할 때
      • 판매원(TickectSeller) 매표소(TicketOffice)  밖에서 티켓(Ticket)을 판매하게 될 때
public class Theater {
	private TicketSeller ticketSeller;
    
    public Theater(TicketSeller ticketSeller) {
    	this.ticketSeller = ticketSeller;
    }
    
    public void enter(Audience audience) {
    	// 1. 관람객(Audience)의 입장
        if (audience.getBag().hasInvitaion()) {
        	Ticket ticket = ticketSeller.getTicketOffice().getTicket();
        	audience.getBag().setTicket(ticket);
        // 2. 판매원(TicketSeller)의 입장
        } else {
        	Ticket ticket = ticketSeller.getTicketOffice().getTicket();
        	audience.getBag().minusAmout(ticket.getFee());
        	ticketeller.getTicketOffice().plusAmount(ticket.getFee());
        	audience.getBag().setTicket(ticket);
        }
    }
}

※ 의존성은 변경에 대한 영향을 암시한다. 그렇다고 의존성을 완전히 없애는 것이 아니라, 불필요한 의존성을 제거해야 한다.

 

Q. 단일 서버이고 도메인 주도 설계에 능숙하지 않은 사람이라는 전제일 경우, 비즈니스 로직을 객체에서 서비스 레이어로 분리하면, 조금 더 오류를 줄일 수 있는 코드가 되지 않았을까?

A. 소극장 객체가 생성될 때, 자동으로 문제점 1과 문제점 2가 발생하는 것으로 보이는데, 여전히 객체 설계는 수정이 필요해보이지만, 차선책으로 서비스 레이어에 해당 로직을 분리하면 오류를 줄일 수 있다고 보인다.

 

아래는 perplexity 의 답변이다. 물론, 장점과 단점이 각각 존재한다.

1. 현실적 개발 환경 고려
 ● 개발자 역량 분포: 2023년 GitHub 설문에 따르면, 자바 개발자 중 DDD(도메인 주도 설계)를 완전히 이해하는 개발자는 23%에 불과
 ●  유지보수 비용: 빈약한 도메인 모델 프로젝트에서 버그 수정 시간이 평균 2.3배 더 소요된다는 연구 결과(2024, IEEE)
 ●  프로젝트 규모 영향: 소규모 프로젝트에서는 서비스 레이어 접근이 40% 더 빠른 개발 속도 제공
 ●  장점 : 트랜잭션 경계 명확화, 크로스커팅 관심사(cross-cutting concerns) 집중 관리 용이
 ●  통계 : 500개 이상의 마이크로서비스 분석 결과, 서비스 레이어 사용 시 트랜잭션 오류 35% 감소

기준 도메인 모델 접근법 서비스 레이어 접근법
학습 곡선 6-12개월 숙련 기간 필요 2-4주 내 실무 적용 가능
코드 변경 영향 범위 평균 3.2개 파일 수정 평균 1.8개 파일 수정
테스트 작성 용이성
모킹(mocking) 70% 필요 모킹 30% 감소

2. 긍정적 요소
 ● 장점: 트랜잭션 관리 용이, 크로스커팅 관심사 처리 효율화
 ● 적용 사례: 복잡한 워크플로우 조정이 필요한 대규모 시스템

3. 부정적 요소
 ● 위험성: 서비스 레이어가 비대해지면서 도메인 객체의 의미 퇴색
 ● 통계: 68%의 개발자가 서비스 레이어 과도 사용 시 유지보수 비용 40% 증가 보고

 

 

해결방법 1

소극장(Theater)이 관람객(Audience)과 판매원(TicketSeller)에 대해 알아야 할 필요가 없다. 내부로 은닉, 캡슐화(encapsulation)를 한다.

  • 매표소(TicketOffice)에 접근하는 방법은 무조건 판매원(TickectSeller)를 거치도록 한다.
  • 가방(Bag)에 접근하는 방법은 무조건 관람객(Audience)을 거치도록한다.
[개선 결과]
관람객(Audience)의 소지품, 결제 방식이 바뀌거나 판매원(TicketSeller)이 티켓, 돈을 보관하는 방법을 변경할 경우에는 각각의 객체만 변경해주면 될 뿐, 소극장(Theater)에는 영향을 주지 않는다.

판매원(TicketSeller) 역시 관람객(Audience) 내부가 어떻게 동작하는지 알 필요가 없다. buy(ticket) 메소드를 통해서 얼마를 지불할지(Ticket 을 이미 소유하고 있으면 지불금액은 0원. 소유하고 있지 않으면 지불금액)를 return 받을 뿐이다.

[이유]
소극장(Theater)이 관람객(Audience)과 판매원(TicketSeller)의 내부에 직접 접근하지 않기 때문이다. 또한, 판매원(TicketSeller) 역시 관람객(Audience)의 가방(Bag)의 내부에 직접 접근하지 않는다.

 

문제점 2

  1. 판매원(TickectSeller)의 책임은 티켓 판매.
  2. 관람객(Audience)의 책임은 티켓 구매.
  3. 소극장(Theater)의 책임은 관람객을 입장 시키는 것.
  4. 가방(Bag)이 관람객(Audience)에게 의존적인 존재이다.

 

해결방법 2

관람객(Audience) 내부에 구현되어있던 가방(Bag)과 관련된 로직들을 캡슐화.

  • 가방(Bag)을 통해서만 접근 가능하도록 변경
  • hasInvitation(), minusAmount(), setTicket()

판매원(TickectSeller) 역시 매표소(TicketOffice)를 통해서만 티켓(Ticket) 판매가 가능하도록 캡슐화.

  • sellTo()


문제점 3

매표소(TicketOffice)와 관람객(Audience) 사이에 의존성 추가.
캡슐화 되어 내부 동작을 몰라도 사용할 수 있지만 결합도가 높아져서 변경에 다시 취약해진다.


결론

설계는 트레이드오프의 산물이다. 좋은 설계란 모든 객체들의 자율성을 높이고 결합도를 낮추도록 지향하는 것을 의미한다.