개발자는 기록이 답이다
오브젝트 1장 - 객체, 설계(1) 본문
이론이 먼저일까, 실무가 먼저일까?
from 로버트 L.글래스
로버트 L.글래스는 어떤 분야를 막론하고 이론을 정립할 수 없는 초기에는 실무가 먼저 급속한 발전을 이룬다고 한다.
실무가 어느 정도 발전하고 난 다음에야 비로소 실무의 실용성을 입증할 수 있는 이론이 서서히 모습을 갖춰가고, 해당 분야가 충분히 성숙해지는 시점에 이르러서야 이론이 실무를 추월하게 된다고 했다.
따라서 객체 지향 프로그래밍을 설계하고 유지보수하는데 필요한 원칙과 기법을 배우기 위해 추상적인 개념이나 이론보다 코드를 만지고 손을 더럽혀야 한다.
1. 티켓 판매 애플리케이션 구현하기
여러분은 연극이나 음악회를 공연할 수 있는 소극장을 경영하고 있다. 홍보를 위해 추첨을 통해 선정된 관람객에게 무료 공연 초대장을 발송했다. 이벤트가 마감되고 공연 당일날 이벤트 당첨자들과 표를 구매하려는 관람객이 오랜 시간 기다리고 있다.
여기서 주의사항은 이벤트에 당첨된 관람객과 일반 관람객은 다른 방식으로 입장시켜야 한다.
- 이벤트에 당첨된 관람객은 초대장을 티겟으로 교환한 후에 입장할 수 있다.
- 이벤트에 당첨되지 않은 관람객은 티켓을 구매해야만 입장할 수 있다.
따라서 입장 전에 이벤트 당첨 여부를 확인해야 한다.
초대장이라는 개념을 구현한 Intevation은 공연을 관람할 수 있는 초대일자(when)을 인스턴스 변수로 포함하고 있다.
public class Invitation {
private LocalDateTime when;
}
공연을 관람하기 원하는 모든 사람들은 티켓을 소지하고 있어야 한다.
public class Ticket {
private Long fee;
public Long getFee() {
return fee;
}
}
이벤트 당첨자 : 초대장 -> 티켓
이벤트에 당첨되지 않은 관람객 : 현금 -> 티켓
따라서, 관람객이 갖고 있을 소지품은 초대장, 현금, 티켓 3가지 뿐이다.
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 Bag(Long amount) {
this.amount = amount;
}
// 이벤트에 당첨된 관람객
public Bag(Long amount, Invitation invitation) {
this.amount = amount;
this.invitation = invitation;
}
관람객은 소지품을 보관하기 위해 가방을 소지할 수 있다.
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 getTickets() {
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;
}
}
소극장은 관람객을 맞이할 수 있도록 enter를 구현한다.
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().getTickets();
audience.getBag().setTicket(ticket);
// 초대장이 없다면 -> 이벤트 당첨되지 않은 관람객
} else {
// 판매원이 티켓을 판매한다.
Ticket ticket = ticketSeller.getTicketOffice().getTickets();
// 소극장이 관람객의 가방에서 티켓 금액 만큼 차감
audience.getBag().minusAmount(ticket.getFee());
// 매표소의 금액을 판매금액 만큼 증가시킨다
ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
// 가방안에 넣어준다.
audience.getBag().setTicket(ticket);
}
}
}
2. 무엇이 문제일까?
💡 모든 소프트웨어 모듈에는 3가지 목적이 있다.
1. 실행 중에 제대로 동작해야 한다
2. 변경을 위해 존재해야 한다.
3. 코드를 읽는 사람과 의사소통하는 것이다.
from 로버트 마틴
위에서 작성한 프로그램은 관람객을 입장시키는데 필요한 기능을 오류 없이 정확하게 수쟁하고 있다.
하지만 불행하게도 변경의 용이성과 읽는 사람과의 의사소통이라는 목적은 만족시키지 못한다.
예상을 빗나가는 코드
문제는 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재이다.
관람객 입장에서 문제는 소극장이라는 제 3자가 초대장을 확인하기 위해 관람객의 가방을 마음대로 열어보는 것이다.
만약 누군가가 허락 없이 가방의 내용물을 마음대로 뒤적이고 돈을 가져가면 기분이 나쁠 것이다.
판매원의 입장에서 문제는 소극장이 허락도 없이 매표소에 보관 중인 티켓을 꺼내 관람객의 가방에 집어넣고 관람객에게서 받은 돈을 매표소에 적립하는일을 판매원이 아닌 소극장이 한다는 것이다.
이해 가능한 코드란 그 동작이 우리의 예상에서 크게 벗어나지 않는 코드다.
현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건낸다. 티켓을 구매하는 관람객은 가방 안에서 돈을 직접 꺼내 판매원에게 지불한다. 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고 관람객에게서 직접 돈을 받아 매표소에 보관한다.
위의 코드는 우리 상식과는 너무도 다르게 동작하기 때문에 코드를 읽는 사람과 제대로 의사소통을 하지 못한다.
또다른 문제는, 이 코드를 이해하기 위해 여러 가지 세부적인 내용을 전부 다 기억하고 있어야 한다는 점이다.
Theater의 enter 메서드를 이해하기 위해서 Audiene는 Bag을 가지고 있고, Bag안에는 현금과 티켓이 들어 있으며 TicketSeller가TicketOffice에서 티켓을 판매하고 있고, TicketOffice안에 돈과 티켓이 보관돼 있다는 모든 사실을 기억해야 한다.
하나의 클래스나 메서드에서 너무 많은 세부사항을 다루기 때문에 코드를 작성하는 사람뿐만 아니라 코드를 읽는 사람에게 큰 부담을 준다.
변경에 취약한 코드
위의 코드는 관람객이 현금과 초대장을 보관하기 위해 항상 가방을 들고 다닌다고 가정하고, 판매원이 매표소에서만 티켓을 판매한다고 가정하고 있다.
하지만 관람객이 가방을 들고 있지 않다면 어떻게 할것인가? 현금이 아니라 신용카드를이용한다면? 판매원이 매표소 밖에서 티켓을 판매해야 한다면? 모든 가정이 깨지는 순간 모든 코드를 바꿔야 한다.
관람객이 가방을 들고 있다는 가정이 바뀌었다고 해보자. Audience 클래스에스Bag을 제거할 뿐만 아니라 Audience의 Bag에 직접 접근하는 Theater의 enter메서드 역시 수정해야 한다.
이러한 세부적인 사실 중 한 가지라도 바뀌면 해당 클래스 뿐만 아니라 이 클래스에 의존하는 Theater 클래스도 함께 변경해야 한다.
이것은 객체 사이의 의존성(Dependency)와 관련된 문제다. 문제는 의존성이 변경과 관련돼 있다는점이다.
의존성은 변경에 대한 영향을 암시한다. 의존성이라는 말 속에는 어떤 객체가 변경될 때그 객체에게 의존하는 다른 객체도 함께 변경될 수 있다는 사실이 내포돼있다.
그렇다고 객체 사이의 의존성을 완전히 없애는 것이 정답은 아니다. 객체 지향 설계는 서로 의존하면서 협력하는 객체들의 공동체를 구축하는 것이다. 따라서 우리의 목표는 최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것이다.
객체 사이의 의존성이 과한 경우를 가리켜 결합도(coupling)이 높다고 말한다. 반대로 객체들이 합리적인 수준으로 의존할 경우에는 결합도가 낮다고 말한다. 결합도는 의존성과 관련돼 있기 때문에 결합도 역시 변경과 관련이 있다. 따라서 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만들어야 한다.
GitHub - codesejin/OOP-Object: [오브젝트] - 코드로 이해하는 객체지향 설계
[오브젝트] - 코드로 이해하는 객체지향 설계. Contribute to codesejin/OOP-Object development by creating an account on GitHub.
github.com
'기술 서적 > OOP' 카테고리의 다른 글
오브젝트 3장 - 역할, 책임, 협력(2) (0) | 2023.11.28 |
---|---|
오브젝트 3장 - 역할, 책임, 협력(1) (0) | 2023.11.28 |
오브젝트 2장 - 객체지향 프로그래밍(2) (1) | 2023.11.27 |
오브젝트 2장 - 객체지향 프로그래밍(1) (1) | 2023.11.25 |
오브젝트 1장 - 객체, 설계(2) (0) | 2023.11.21 |