조영호 님의 "오브젝트" 책을 정리한 글입니다.
1. 티켓 판매 애플리케이션 구현하기
관람객
구분 | 입장 조건 | 필요한 절차 |
이벤트 당첨자 | 초대장을 티켓으로 교환한 후 입장 |
초대장 → 티켓 교환 → 입장
|
이벤트 미당첨자 | 티켓을 구매한 후 입장 | 티켓 구매 → 입장 |
극장
- 관람객 입장 담당 (이벤트 당첨자/미당첨자에 따른 입장)
더보기
코드
public class Invitation {
private LocalDateTime when;
}
public class Ticket {
private Long fee;
public Long getFee() { return fee; }
}
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 hasTicket() { return ticket != null; }
public void setTicket(Ticket ticket) { this.ticket = ticket; }
public void minusAmount(Long fee) { this.amount -= amount; }
public void plusAmount(Long fee) { this.amount += amount; }
}
public class Audience {
// 관람객: 가방 소지
private Bag bag;
public Audience(Bag bag) { this.bag = bag; }
public Audience getBag() { return bag; }
}
public class TicketOffice {
// 매표소: 티켓을 초대장으로 교환하거나 구매해야 함
private Long amount;
private List<Ticket> tickets = new ArrayList<>();
public TicketOffice(Long amount, List<Ticket> tickets) {
this.amount = amount;
this.tickets = 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 class TicketSeller {
// 판매원: 매표소에서 티켓을 교환 및 판매 담당
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; }
public TicketOffice getTicketOffice() { return ticketOffice; }
}
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);
}
}
}
2. 무엇이 문제인가
모듈이 가져야 할 세 가지 기능
- 로버트 마틴은 <클린 소프트웨어>에서 소프트웨어 모듈이 가져야 할 세 가지 기능에 대해 설명함
- 실행중에 제대로 동작할 것
- 변경에 용이해야 함
- 이해하기 쉬워야 함
위 코드는 첫번째만 만족함
즉, 두번째, 세번째 기능은 만족시키지 못함
예상을 빗나가는 코드
Theater의 enter()
- 소극장이 초대장 여부를 파악 (관람객의 가방을 직접 열음)
- 판매원이 티켓 제공 (관람객의 가방에 직접 넣음)
- ⚠️ 관람객과 판매원이 모두 수동적으로 동작함
단점
개념 | 설명 |
이해하기 어려움 | 직관적으로 이해하기 어려움 - 상식과 다르게 동작함 - 관람객이 스스로 자신의 일을 처리함 ↔️ 소극장이 관람객의 가방을 직접 엶 |
세부 내용 필요 |
구체적인 정보를 알아야 이해 가능
- TicketOffice에 돈과 티켓이 보관되어 있음 - Audience는 Bag을 가지고 있음 |
변경에 취약한 코드
Audience와 TicketSeller를 변경할 경우, TicketOffice도 변경해야 함
- 특정 객체와의 협력을 가정하기 때문
- Audience는 Money와 Invitation을 보관하기 위해 항상 Bag을 가지고 다닌다 가정
- TicketSeller는 TicketOffice에서만 Ticket을 판매한다 가정
의존성과 결합도
개념 | 설명 | 예시 |
의존성 | 어떤 객체가 다른 객체에 기능적으로 의존하는 관계 |
A 객체가 B 객체의 메서드를 호출함
|
결합도 | 객체들 간 의존성의 정도 |
강한 결합: 직접 인스턴스 생성
약한 결합: 인터페이스 사용 및 주입 |
3. 설계 개선하기
- Theater가 Audience의 Bag과 TicketSeller에 직접 접근한다는 것은 결합됨을 의미
- Theater가 Audience와 TicketSeller에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하기
- Audience가 스스로 Bag안의 Money와 Invitation을 처리하기
- TicketSeller가 스스로 TicketOffice 작업을 처리하기
자율성을 높이자
- 설계를 변경하기 어려운 이유는 Theater가 Audience와 TicketSeller뿐만 아니라 각각의 의존성까지 접근할 수 있기 때문
더보기
코드
public class Audience {
// 관람객: 가방 소지
private Bag bag;
public Audience(Bag bag) { this.bag = bag; }
// getBag() 삭제
// buy() 추가: 스스로 티켓을 관리
public Long buy(Ticket ticket) {
if (bag.hasInvitation()) {
bag.setTicket(ticket);
return 0L;
} else {
bag.setTicket(ticket);
bag.setMinusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
public class TicketSeller {
// 판매원: 매표소에서 티켓을 교환 및 판매 담당
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; }
// getTicketOffice() 삭제
// audience에서 티켓 구매 요청
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) { this.ticketSeller = ticketSeller; }
public void enter(Audience audience) { ticketSeller.sellTo(audience); }
}
객체 | 캡슐화 | 인터페이스 분리 |
TicketSeller | getTicketOffice() 삭제 | Theater는 오직 TicketSeller의 인터페이스에만 의존 |
Audience | getBag() 삭제 | TicketSeller는 Audience의 인터페이스에만 의존 |
무엇이 개선됐는가
개념 | 설명 | 목적 | 효과 |
캡슐화 | 내부 구현을 감추기 | 결합도 감소 가독성 향상 |
유지보수 용이 (내부 변경이 외부에 영향 X) |
인터페이스 분리 | 인터페이스만 공개하고, 구현은 감춤 | 결합도 감소 변경 용이성 증가 |
구현체 변경 시 외부 영향 최소화
|
어떻게 한 것인가
- 캡슐화: 자기 자신이 할일을 스스로 하도록 변경
캡슐화와 응집도
응집도
- 밀접하게 연관된 작업을 하나의 모듈에서 수행하도록 하는 정도
응집도를 높이는 방법 | 설명 |
자신의 데이터를 스스로 책임짐 | 객체 내부의 상태와 행동이 일관되도록 유지 |
외부 데이터 의존 최소화 | 외부 객체나 시스템의 변화에 영향을 받지 않도록 설계 |
메시지를 통한 협력 | 직접 접근이 아닌 메시지를 주고받으며 협력 |
절차지향과 객체지향
구분 | 절차지향 | 객체지향 |
개념 | 프로세스와 데이터를 별도의 모듈에 위치 | 프로세스와 데이터를 동일한 모듈 내에 위치 |
특징 | 순차적인 흐름에 따라 실행됨 | 객체 간 메시지를 주고받으며 동작 |
캡슐화 | 없음 (데이터와 로직이 분리됨) | 있음 (데이터를 객체 내부에 숨김) |
재사용성 | 낮음 (코드를 복사하여 사용) | 높음 (객체 단위로 재사용 가능) |
유지보수성 | 변경 시 전체 코드 수정 가능성이 큼 | 변경이 용이 (객체 단위 수정 가능) |
확장성 | 낮음 (새로운 기능 추가가 어렵고 복잡) | 높음 (객체를 추가하거나 확장하기 용이) |
장점 | 직관적이고 실행 속도가 빠름 | 유지보수성과 확장성이 뛰어남 |
단점 | 코드 중복이 많고 유지보수가 어려움 | 상대적으로 실행 속도가 느릴 수 있음 |
책임의 이동
- 두 방식의 근본적인 차이를 만드는 것은 책임의 이동임
- 절차 지향은 책임이 한쪽에 몰려있음 ↔️ 객체 지향은 각 객체에 적절히 분산되어 있음
더 개선할 수 있다
Bag를 자율적인 존재로 변경하기
- 현재 Bag은 과거의 Audience처럼 수동적으로 동작함
더보기
public class Audience {
private Bag bag;
public Audience(Bag bag) { this.bag = bag; }
public Long buy(Ticket ticket) { return bag.hold(ticket); }
}
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 void setTicket(Ticket ticket) { this.ticket = ticket; }
public void minusAmount(Long fee) { this.amount -= amount; }
public Long hold(Ticket ticket) {
setTicket(ticket);
if (hasInvitation()) {
return 0L;
} else {
minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
TicketOffice가 자율적으로 처리하도록 하기
- TicketOffice는 수동적으로 사용됨 (TicketSeller에 의해 내부 작업이 이루어짐)
더보기
public class TicketOffice {
// 매표소: 티켓을 초대장으로 교환하거나 구매해야 함
private Long amount;
private List<Ticket> tickets = new ArrayList<>();
public TicketOffice(Long amount, List<Ticket> tickets) {
this.amount = amount;
this.tickets = 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) { plusAmount(audience.buy(getTicket())); }
}
public class TicketSeller {
// 판매원: 매표소에서 티켓을 교환 및 판매 담당
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) { this.ticketOffice = ticketOffice; }
// ticketOffice에서 티켓 구매 요청
public void sellTo(Audience audience) { ticketOffice.sellTicketTo(audience); }
}
그래, 거짓말이다!
- 실세계의 생물처럼 스스로 생각하고 행동하도록 이해하기 쉽게 짜는 것
- 더 나아가 그 대상이 실세계에서 생명이 없는 수동적인 존재라 하더라도 객체지향의 세계에서는 생명을 가진 존재로 다시 태어남
4. 객체지향 설계
설계가 왜 필요한가
- 설계는 코드를 배치하는 것
좋은 설계가 필요한 이유
- 변경 용이성을 확보할 수 있음
- 항상 요구사항이 변경되기 때문에 코드 변경은 필연적임
- 변경 용이성을 확보하면 버그 발생 가능성을 줄일 수 있음 (코드 변경 시 버그가 추가될 가능성이 높기 때문)
'Code > OOP' 카테고리의 다른 글
[오브젝트] 4. 설계 품질과 트레이드오프 (0) | 2025.04.03 |
---|---|
[오브젝트] 3. 역할, 책임, 협력 (0) | 2025.04.03 |
[오브젝트] 2. 객체지향 프로그래밍 (1) | 2025.04.02 |
[객체지향의 사실과 오해] 2. 이상한 나라의 객체 (1) | 2025.04.02 |
[객체지향의 사실과 오해] 1. 협력하는 객체들의 공동체 (0) | 2025.04.02 |