오브젝트(조영호) 책 정리 -1-
소프트웨어 분야는 역사가 다른 공학에 비해 짧다. 또한 이론들은 실무에서 나온 패턴들을 바탕으로 만들어진 것이다. 이 결과로 소프트웨어 설계와 유지보수에 중점을 두려면 이론이 아닌 실무에 초점을 맞추어야 한다. 추상적인 개념과 이론은 훌륭한 코드를 작성하는 데 필요한 도구일 뿐이다. 프로그래밍을 통해 개념과 이론을 배우는 것이 개념과 이론을 통해 프로그래밍을 배우는 것보다 더 훌륭한 학습 방법이아라고 생각한다.
티켓 판매 애플리케이션 구현하기
초기 코드
가방과 관람객
// 가방에는 초대장(invitation)과 티켓(ticket) 그리고 금액(amount)이 들어있다
// 관람객은 가방을 가지고 있다
// 초대장
import java.time.LocalDateTime;
class Invitation {
public LocalDateTime when;
}
// 티켓
class Ticket {
private Long fee;
public Long getFee() {
return fee;
}
}
class Bag {
private Long amount;
private Ticket ticket;
private Invitation invitation;
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 plusAmount(Long amount){
this.amount += amount;
}
public void minusAmount(Long amount){
this.amount -= amount;
}
}
// 관람객
class Audience {
private Bag bag;
public Audience(Bag bag){
this.bag = bag;
}
public Bag getBag(){
return bag;
}
}
판매원과 매표소
// 매표소
class TicketOffice {
private Long amount;
private List<Ticket> tickets = new ArrayList<>();
public TicketOffice(Long amount, Ticket ... tickets){
this.amount = amount;
this.tickets.addAll(List.of(tickets));
}
public Ticket getTicket(){
return tickets.remove(0);
}
public void plusAmount(long amount){
this.amount += amount;
}
public void minusAmount(long amount){
this.amount -= amount;
}
}
// 판매원
class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice){
this.ticketOffice = ticketOffice;
}
public TicketOffice getTicketOffice() {
return ticketOffice;
}
}
코드의 문제점
- 관람객과 판매원의 극장의 통재를 받는다. 이는 우리의 상식과는 너무 다르기 때문에 코드를 읽는 사람과 의사소통이 어렵다. 또한 코드를 이해하기 위해서 여러 가지 세부적인 내용들을 한꺼번에 기억하고 있어야 한다
- Audience와 TicketSeller를 변경하는 경우 Theater도 함께 변경해야 한다. 즉, 변경에 취약하다
객체을 합리적인 수준으로 의존하게 해 결합도를 낮추어야 한다. 결합도가 높을 수록 변경하기 어렵다.
개선된 코드 step1
// Theater와 결합도를 낮추자
class TicketSeller {
....
/* 제거
public TicketOffice getTicketOffice() {
return ticketOffice;
}
*/
public void toSell(Audience audience){
if(audience.getBag().hasInvitation()){
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);
}
}
}
class Theater {
...
public void enter(Audience audience){
ticketSeller.toSell(audience);
}
}
Theater가 원하는 것은 관람객이 극장에 입장하는 것이다. 따라서 판매원과 관람객이 티켓을 거래하는 것은 알 필요가 없다. 따라서 캡슐화를 통해 세부적인 사항을 감춘다.
개선된 코드 step2
// Ticketeller의 결합도를 낮추자
class Audience {
...
/* 삭제
public Bag getBag() {
return 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();
}
}
}
class TicketSeller {
...
public void toSell(Audience audience){
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
캡슐화를 개선한 후 달라진 점은 Audience와 TicketSeller가 내부 구현을 외부에 노출하지 않고 자신의 문제를 스스로 책임지고 해결하는 것이다. 이제는 Audience나 TicketSeller의 변경사항은 외부에 영향이 가지 않는다. 스스로 책임을 진다는 것은 "데이터와 데이터를 사용하는 프로세스가 동일한 객체 안에 위치 하는 것" 뿐만아니라 "적절한 객체에 적절한 책임을 할당하는 것"이다.
결합도를 낮추기 위한 핵심은 객체 내부의 상태를 캡슐화하고 객체 간의 오직 메시지를 통해서만 상호작용하로록 만드는 것이다. 자신과 밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 것이 객체의 응집도를 높일 수 있다. 우리는 결합도를 낮추고 응집도는 높여야 한다.
개선된 코드 step3
// Audience의 결합도를 낮추자
class Audience {
...
public Long buy(Ticket ticket){
return bag.Hold(ticket);
}
}
class Bag {
...
public Long Hold(Ticket ticket){
if(hasInvitation()){
setTicket(ticket);
return 0L;
}
else{
setTicket(ticket);
minusAmount(ticket.getFee());
return ticket.getFee();
}
}
}
// TicketSeller의 결합도를 낮추자
class TicketSeller {
...
public void toSell(Audience audience){
ticketOffice.sellTicketTo(audience);
}
}
class TicketOffice {
...
public void sellTicketTo(Audience audience){
plusAmount(audience.buy(getTicket()));
}
}
이 경우 audience와 beg 사이와 TicketOffice와 TicketSeller 사이에 결합도는 낮아졌다. 하지만 TicketOffice와 Audience 사이에 의존성이 추가 되었다. 이처럼 트레이드오프가 일어날 수 있다. 따라서 적절한 트레이드오프를 하면서 설계해야한다
우리는 오늘 완성해야 하는 기능을 구현하는 코드를 짜야 하는 동시에 내일 쉽게 변경할 수 있는 코드를 짜야한다. 변경을 수용할 수 있는 설계가 중요한 이유는 요구사항이 항상 변경되며 코드가 변경될 때 버그가 추가될 가능성이 높기 때문이다. 변경이 용이한 설계는 버그 발생 가능성을 낮추고 이는 코드를 개선하려는 의지를 높인다.