조금 더 객체 지향의 전문성을 기르고 싶어 조영호 저자의 '오브젝트' 서적을 읽으면서 진행하는 예제와 핵심 내용들을 정리한다.
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 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 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 ... ticket) {
this.amount = amount;
this.tickets.addAll(Arrays.asList(ticket));
}
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. 무엇이 문제인가
책에서 로버트 마틴(Robert C. Martin)은 소프트웨어 모듈이 가져야 하는 세 가지 기능에 대해 설명했다고 한다.
첫 번째, 모든 모듈을 실행 중 제대로 동작해야 한다.
두 번째, 변경을 위해 존재하므로 간단한 작업만으로도 변경이 가능해야 한다.
세 번째, 개발자가 쉽게 읽고 이해할 수 있어야 한다.
Theater 클래스의 enter 메서드는 제대로 동작해야 한다는 제약은 만족시킨다. 하지만 변경 용이성과 개발자가 쉽게 읽고 이해할 수 있어야 한다는 목적은 만족시키지 못한다.
예상을 빗나가는 코드
1. 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재가 되어있다.
현실에서는 초대장이 있는 관람객은 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건넬 것이고 티켓을 구매하는 관람객은 가방 안에서 돈을 직접 꺼내 판매원에게 지불할 것이다. 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고 관람객에게서 돈을 받아 매표소에 보관한다.
하지만 현재 코드는 소극장이 허락없이 매표소의 티켓과 현금에 마음대로 접근할 수 있고 관람객의 가방에도 접근할 수 있다. 소극장이 모든 역할을 다 해버리기 때문에 관람객과 판매원의 역할은 없어진 셈이다.
2. 코드를 이해하기 위해서 여러 가지 세부적인 내용을 한 번에 알고 있어야 한다.
Theater의 enter 메서드를 이해하기 위해 Audience가 Bag을 가지고 있고, Bag안에는 현금과 티켓을 가지고 있으며 TicketSeller가 TicketOffice에서 티켓을 판매하고 있는 등 모든 사실을 동시에 기억하고 있어야한다. 한 클래스에서 많은 세부사항을 다루기 때문에 코드를 작성하거나 읽는 사람에게 너무 많은 부담을 준다.
가장 심각한 문제는 Audience와 TicketSeller를 변경할 경우 Theater도 함께 변경해야 한다는 것이다.
변경에 취약한 코드
현재 코드는 변경사항이 발생하면 의존된 객체를 모두 수정해야해서 변경에 용이하지 못하다.
우리는 애플리케이션의 기능을 구현하데 필요한 최소한의 의존성만 유지하고 불필요한 의존성은 제거해야한다.
객체 사이의 의존성이 과한 경우를 결합도(coupling)이 높다고 하고 반대로 객체들이 합리적인 수준으로 의존할 경우 결합도가 낮다고 하는데 두 객체 사이의 결합도가 높을수록 의존성이 과하기 때문에 함께 변경될 확률도 높아지게 된다.
따라서 객체사이의 결합도를 낮춰 변경이 용이한 설계를 만들어야한다.
3. 설계 개선하기
자율성을 높히자
더이상 소극장이 관람객과 판매원의 역할을 하지 않고 관람객과 판매원을 자율적인 존재로 직접 자신들의 역할을 하도록 설계를 변경한다.
첫 번째 단계는 Theater의 enter 메서드의 TicketOffice에 접근하는 코드는 TicketSeller에게, Bag에 접근하는 코드는 Audience로 메서드를 옮기고 외부에서 접근하지 못하도록 수정하는 것이다.
판매원
public class TicketSeller {
private TicketOffice ticketOffice;
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
}
- TicketSeller에서 getTicketOffice가 제거되어 외부에서 ticketOffice에 직접 접근할 수 없으며 오직 TicketSeller만 ticketOffice에 접근할 수 있다. 결과적으로 매표소에서의 업무는 판매원만 스스로 수행할 수 밖에 없다.
이처럼 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 캡슐화(encapsulation)라고 부른다.
관람객
public class Audience {
private Bag bag; // 가방
public Long buy(Ticket ticket) {
if (bag.hasInvitation()) {
bag.setTicket(ticket);
return 0L;
} else {
bag.setTicket(ticket);
bag.minusAmount(ticket.getFee());
return ticket.getFee();
}
}
public Audience(Bag bag) {
this.bag = bag;
}
public Bag getBag() {
return bag;
}
}
- Audience가 Bag를 직접 처리하기 때문에 외부에서 더이상 Audience가 Bag를 소유하고 있다는 것을 알 필요가 없다.
- getBag 메서드가 제거 되었고 캡슐화할 수 있게 되었다.
소극장
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
public void enter(Audience audience){
ticketSeller.sellTo(audience);
}
}
- Theater는 오직 TicketSeller의 인터페이스에만 의존한다.
- 객체를 인터페이스와 구현으로 나누고 인터페이스만을 공개하는 것은 객체 사이의 결합도를 낮추고 변경에 용이한 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙이다.
무엇이 개선됐는가
객체간의 의존성이 줄고 결합도가 낮아졌다. 또한 내부 구현이 캡슐화됐으므로 Audience가 수정되도 TicketSeller는 영향을 받지 않는다. 캡슐화를 통해 가장 크게 달라진 점은 Audience와 TicketSeller가 내부 구현을 외부에 노출하지 않고 자신의 문제를 스스로 책임지고 해결한다.
어떻게 한 것인가
객체의 역할과 책임을 분리하여 자기 자신의 문제를 스스로 해결하도록 코드를 변경하였다.
우리는 우리의 직관을 따랐고 그 결과로 코드는 변경이 용이하고 이해 가능하도록 수정됐다.
캡슐화와 응집도
핵심은 객체 내부의 상태를 캡슐화하고 객체 간에 오직 메시지만 통해서 상호작용 하도록 하는 것이다.
Theater는 TicketSeller의 내부에 대해서는 전혀 알지 못한다. 단지 TicketSeller가 sellTo 메시지를 이해하고 응답할 수 있다는 사실만 알고 있을 뿐이다. TicketSeller에서의 Audience의 buy 메서드도 동일하다.
객체는 자신의 데이터를 스스로 처리하는 자율적인 존재여야하고 그것이 객체의 응집도를 높이는 첫걸음이다.
외부의 간섭을 최대한 배제하고 메시지를 통해서만 협력하는 자율적인 객체들의 공동체를 만드는 것이 훌륭한 객체지향 설계를 얻을 수 있는 지름길이다.
절차지향과 객체지향
수정하기 전의 Theater의 enter 메서드 안에서 모든 처리가 이루어졌다. 이 관점에서 enter 메서드는 프로세스(Process) 이며 Audience, TicketSeller, Bag, TicketOffice는 데이터(Data)이다. 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차지향적 프로그래밍 이라고 부른다.
수정한 코드는 자신의 데이터를 스스로 처리하도록 프로세스의 적절한 단계를 Audience와 TicketSeller로 옮겼다. 이처럼 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍 하는 방식을 객체지향 프로그래밍 이라고 한다.
훌륭한 객체지향 설계 핵심은 캡슐화를 이용해 의존성을 적절히 관리함으로써 객체 사이의 결합도를 낮추는 것이다. 이것이 절차지향보다 객체지향이 좀 더 유연하다고 말하는 이유다.
책임의 이동
변경 전의 절차적 설계에서는 Theater가 전체적인 작업을 도맡아 처리했고 변경 후의 객체지향 설계에서는 객체가 자신이 맡은 일을 스스로 처리했다. Theater에게 몰려있던 책임이 개별객체로 이동한 이것이 바로 책임의 이동이 의미하는 것이다.
객체 지향 애플리케이션은 스스로 책임을 수행하는 자율적인 객체들의 공동체를 구성함으로써 완성된다.
더 개선할 수 있다.
Bag와 TicketOffice도 자기 자신을 책임지도록 변경할 수 있다.
가방
public class Bag {
private Long amount; // 보유 현금
private Invitation invitation; // 초대장
private Ticket ticket; // 티켓
public Long hold(Ticket ticket) {
if (hasInvitation()) {
setTicket(ticket);
return 0L;
} else {
setTicket(ticket);
minusAmount(ticket.getFee());
return ticket.getFee();
}
}
private void setTicket(Ticket ticket) {
this.ticket = ticket;
}
private void minusAmount(Long amount) {
this.amount -= amount;
}
//...
}
관람객
public class Audience {
private Bag bag; // 가방
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
//...
}
매표소
public class TicketOffice {
private Long amount; // 보유 현금
private List<Ticket> tickets = new ArrayList<>();
public void sellTicketTo(Audience audience) {
plusAmount(audience.buy(getTicket()));
}
//...
}
매표원
public class TicketSeller {
private TicketOffice ticketOffice;
public void sellTo(Audience audience) {
ticketOffice.sellTicketTo(audience);
}
//...
}
가방과 매표소에게 자율성을 주어 스스로 처리할 수 있게 되었지만 TicketOffice와 Audience 사이에 의존성이 추가되었다. 변경 전에 존재하지 않았던 새로운 의존성이 추가되었고 Audience에 대한 결합도와 TicketOffice의 자율성 모두를 만족시키는 것 중 선택을 해야하는 트레이드오프 시점이 왔다.
이 작은 예제를 통해 2가지 사실을 알 수 있다.
1. 어떤 기능을 설계하는 방법은 한가지 이상일 수 있다.
2. 어떤 경우에도 모든 사람들을 만족시킬 수 있는 설계를 만들 수는 없다.
훌륭한 설계는 적절한 트레이드오프의 결과물이다.
그래 거짓말이다!
앞에서는 실생활의 관람객과 판매자가 스스로의 일을 처리하기 때문에 Audience, TicketOffice가 스스로의 일을 처리해야한다 했지만 Bag와 TicketOffice는 실생활에서 자율적인 존재가 아닌 사물일 뿐이다. 하지만 현실 세계에서 생명이 없는 수동적인 존재라고 해도 객체지향 세계에서는 능동적이고 자율적인 존재로 의인화해서 표현할 수 있었고 그 결과, 더욱 객체 지향적인 코드를 만들 수 있었다.
훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가르킨다.
4. 객체 지향 설계
앞의 예제를 통해 코드 변경이라는 측면에서 객체지향이 과거의 다른 방법보다 안정감을 주는 사실을 알 수 있었다.
우리는 변경에 유연하게 대응할 수 있는 코드를 원하고 변경 가능한 코드는 이해하기 쉬운코드이다.
또한, 애플리케이션의 기능을 구현하기 위해 객체들이 협력하는 과정 속에서 객체들은 서로 다른 객체들과 협력하여 의존하게 되고 이 의존성을 적절하게 조절하는 것이 변경에 용이한 설계이며 훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계이다.
객체의 의존성을 적절하게 조절하여 변경에 용이한 코드를 작성하도록 하자.
객체에 역할과 책임을 부여해서 객체 사이의 결합도를 낮추고 변경이 용이한 설계를 해야함을 느끼게 되었다.
객체지향적으로 코드를 작성하기 위해 항상 회고하고 생각하자..
'독서 > 오브젝트' 카테고리의 다른 글
오브젝트 : 객체지향 프로그래밍 (1) | 2024.01.06 |
---|
댓글