1장 객체 설계
01. 티켓 판매 시스템
이벤트 추첨을 통해 선정된 관람객에게 공연을 무료로 관람할 수 있는 초대장을 발송한다.
이벤트 당첨자에게 발송되는 초대장을 구현하자
public class Invitation {
private LocalDataTime when;//초대 일자(when) 인스턴스 변수
}
공연을 관람하기 원하는 모든 사람들은 티켓을 소지하고 있어야만 한다. Ticket 클래스를 추가하자
public class Ticket {
private Long fee;
public Long getFee() {
return fee;
}
}
이벤트에 당첨되지 않은 관람객은 티켓을 구매할 수 있는 현금 보유
초대장, 현금, 티켓
관람객은 소지품을 보관할 용도로 가방을 들고 올 수 있다.
public class Bag {
private Long amount;
private Invitation invitation;
private Ticket ticket;
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;
}
}
이벤트에 당첨된 관람객: 현금, 초대장
이벤트에 당첨되지 않은 관람객: 현금
public class Bag {
public Bag(long amount) {
this(null, amount); //Bag의 인스턴스를 생성하는 시전에 제약을 강제
}
public Bag(Invitation invitation, long amount) {
this.invitation = inviation;
this.amount = amount;
}
}
관람객이라는 개념을 구현하는 Audience클래스
관람객은 소지품을 보관하기 위해 가방을 소지할 수 있다.
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;
}
}
판매원은 1. 초대장을 티켓으로 교환 2. 티켓을 판매
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) { //TicketSeller클래수는 자신이 일하는 매표소를 알고 있어야 한다.
this.ticketOffice = ticketOffice;
}
}
소극장을 구현하는 클래수는 Theater다. Theater클래스가 관람객을 맞이할 수 있도록 enter메서드를 구현하자.
public class Theater { //소극장
private TicketSeller ticketSeller;
public Theater(TickerSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
public void enter(Audience audience) {
if(audience.getBag().hasInvitation()) { //초대장O(당첨)
Ticket ticket = ticketSeller.getTicketOffice().getTicket(): //판매원에게 받은 티켓을
audience.getBag().setTicket(ticket);//관람객의 가방 안에 넣어줌
} else { //초대장X
Ticket ticket = ticketSeller.getTicketOffice().getTicket();//티켓 판매
audience.getBag().minusAmount(ticket.getFee());//관람객 가방에서 티켓 금액만큼 차감
ticketSeller.getTicketOffice().plusAmount(ticket.getFee());//매표소 금액 증가
audience.getBag().setTicket(ticket);////관람객의 가방 안에 넣어줌
}
}
}
02. 무엇이 문제인가
로버트 마틴은 소프트웨어 모듈이 가져야 하는 세 가지 기능에 관해 설명한다.
모듈: 크기와 상관 없이 클래스나 패키지, 라이브러리와 같은 프로그램을 구성하는 임의의 요소
- 목적
- 실행 중에 제대로 동작(모듈의 존재 이유)
- 변경을 위해 존재(모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경 가능/변경하기 어려운 모듈은 제대로 동작)
- 코드를 읽는 사람과 의사소통(개발자가 쉽게 읽고 이해)
앞서 작업한 코드는 제대로 동작해야 한다는 제약은 만족시킨다. 하지만 불행하게도 2(변경 용이성)과 3(의사소통)의 목적은 만족X
예상을 빗나가는 코드
1. Theater 클래스의 enter메소드가 문제다!
관람객과 판매원이 소극장의 통제를 받는 수동적인 존재
관람객의 입장: 문제는 소극장이라는 제3자가 초대장을 확인하기 위해 관람객의 가방을 마음대로 열어 본다는 데 있다.
판매원원 입장: 허락도 없이 매표소에 보관 중인 티켓과 현금에 마음대로 접근
제일 큰 문제: 티켓을 꺼내 관람객의 가방에 집어넣고 관람객에서 받은 돈을 매표소에 적립하는 일은 여러분이 아닌 소극장이 수행
2.하나의 클래스나 메서도에서 너무 많은 세부사항을 다루기 때문에 코드를 작성하는 사람뿐만 아니라 코드를 읽고 이해하는 사람 모두에게 큰 부담
Audience가 Bag을 가지고, Bag안에는 현금과 티켓이 들어가 있으며 TicketSeller가 TicketOffice에서 티켓을 판매하고, TicketOffice에서 돈과 티켓이 보관
3.Audience와 TicketSeller를 변경할 경우 Theater도 함께 변경
변경에 취약한 코드
관람객이 현금과 초대장을 보관하기 위해 항상 가방을 들고 다닌다고 가정한다
판매원이 매표소에서만 티켓을 판매한다고 가정한다.
관람객이 가방을 들고 있지 않다면?
관람객이 현금이 아니라 신용카드를 이용한다면?
판매원이 매표소 밖에서 티켓을 판매한다면?
객체 사이의 의존성(dependency)과 관련된 문제이다.
의존성: 어떤 객체가 변경될 때 그 객체에게 의존하는 다른 객체도 함께 변경
세부적인 사실 한 가지라도 바뀌면 해당 클래스 뿐만 아니라 이 클래스에 의존하는 Theater도 함께 변경
목표: 애플리케이션의 기능을 구현하는 데 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거
의존성이 과한 경우= 결합도(coupling)이 높다.
객체들이 합리적인 수준으로 의존 = 결합도가 낮다.
03. 설계 개선하기
코드가 이해하기 어려운 이유는?
Theater가 관람객의 가방과 판매원의 매표소에 직접 전급하기 때문이다.
해결 방법: Theater가 Audience와 TicketSeller에 관해 세세한 부분까지 알지 못하도록 정보를 차단
관람객이 스스로 가방 안의 현금과 초대장을 처리하고, 판매원이 스스로 매표소의 티켓과 판매 요금을 다룬다.
- 자율성
Audience와 TicketSeller가 직접 Bag과 TicketSeller를 처리하는 자율적인 존재가 되도록 설계를 변경
TicketSeller에 sellTo메서드를 추가하고 Theater에 있던 로직을 옮기자.
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public void sellTo(Audience audience) {
if(audience.getBag().hasInvitaion()) {
Ticket ticket = ticketOffice.getTicket();
audience.getBag().setTicket(ticket);
} else {
Ticket ticket = ticketOffice.getTicket();
audience.getBag().minusAmount(ticket.getFee());
ticketOffice.plusAmount(ticket.getFee());
audience.getBag().setTicket(ticket());
}
}
}
- 캡슐화와 응집도
TicketSeller에서 getTicketOffice메서드가 제거
ticketOffice의 가시성이 private이고 퍼블릭 메서드가 더 이상 존재하지 않기 때문에 외부에서는 ticketOffice에 접근할 수가 없다.
ticketOffice에 대한 접근은 오직 TicketSeller만 가능
캡슐화(encapsulation): 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감춘다.
목적: 변경하기 쉬운 객체
캡슐화를 통해 객체 내부롭의 접근을 제한하면 객체와 객체 사이의 결합도를 낮출 수 있기 때문에 쉽게 변경
04. 객체지향 설계
- 설계가 왜 필요한가
- 요구사항이 항상 변경되기 때문이다.
- 코드를 변경할 때 버그가 추가될 가능성이 높기 때문이다.
첫 번째 코드는 데이터와 프로세스를 나누어 별도의 클래스 배치
두 번째 코드는 필요한 데이터를 보유한 클래스 안에 프로세스를 함께 배치
- 좋은 설계
- 오늘 완성해야 하는 기능을 구현하는 코드를 짜야 하는 동시에 내일 쉽게 변경
- 오늘 요구하는 기능을 온전히 수행하면서 내일의 변경을 매끄럽게 수용
'IT서적 > 오브젝트' 카테고리의 다른 글
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]6장: 메시지와 인터페이스 (0) | 2025.11.14 |
|---|---|
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]5장: 책임 할당하기 (0) | 2025.11.14 |
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]4장: 설계 품질과 트레이드오프 (0) | 2025.11.14 |
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]3장: 역할, 책임, 협력 (0) | 2025.11.14 |
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]2장: 객체지향 프로그래밍 (0) | 2025.11.14 |