1장 ‘객체, 설계’의 핵심 부분 정리해보기
로버트 마틴 ‘클린 소프트웨어: 애자일 원칙과 패턴, 그리고 실천 방법’ 에서 소프트웨어 모듈이 가져야 하는 세 가지 기능은 다음과 같다.
- 실행 중에 제대로 동작한다.
- 변경을 위해 존재한다.
- 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단하게 변경할 수 있어야 한다.
- 변경하기 어렵다면, 개선해야 한다.
- 코드를 읽는 사람과 의사소통한다.
- 개발자가 쉽게 읽고 이해할 수 있어야 한다.
- 읽는 사람과 의사소통할 수 없는 모듈은 개선되어야 한다.
객체 사이의 의존성을 완전히 없애는 것이 정답은 아니다. OOP는 객체끼리 서로 의존(협력)해야 한다. 다만, 중요한 포인트는 불필요한 의존성(결합도, coupling)을 제거하는 것이다. 객체들이 합당하게 의존한다면 결합도가 낮은 것으로 본다.
결합도가 낮아야 하는 이유는 무엇일까? 결합도(의존성)은 변경에 취약하다. 변경에 취약하다는 것은 한 객체(부분)가 변경될 때 다른 부분도 변경됨을 의미한다.
객체간의 불필요한 의존을 제거하기 위해 아래와 같이 개선해볼 수 있다.
// 개선 전의 코드
class Theater {
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);
}
}
}
// 개선 후의 코드
class Theater {
TicketSeller ticketSeller;
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
class TicketSeller {
TicketOffice ticketOffice;
public void sellTo(Audience audience) {
ticketOffice.sellTicketTo(audience);
}
}
class TicketOffice {
private Long amount;
private List<Ticket> tickets;
public void sellTicketTo(Audience audience) {
plusAmount(audience.buy(getTicket()));
}
private Ticket getTicket() {
return tickets.remove(0);
}
private void plusAmount(Long amount) {
this.amount += amount;
}
}
class Audience {
Bag bag;
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
}
class Bag {
private Long amount;
private Ticket ticket;
private Invitation invitation;
public Long hold(Ticket ticket) {
setTicket(ticket);
if(hasInvitation()) {
return 0L;
}
minusAmount(ticket.getFee());
return ticket.getFee();
}
private void setTicket(Ticket ticket) { ... }
private boolean hasInviation() { ... }
private void minusAmount(Long amount) { ... }
}
여기서 주목해볼 점은, ‘캡슐화/객체의 독립성’이다. 객체는 각자 자신의 데이터(속성)에 대한 작업만 한다. 그 데이터가 다른 객체라면, 그 객체에 메시지를 던진다. 즉 그 객체의 인터페이스만 이용한다. 메시지를 받은 객체는 자신의 데이터(속성)에 대한 작업만 한다. 다른 객체의 데이터(속성)를 다루지 않는다.
한 객체가 자신의 데이터만 다루게 되면 자연스럽게 응집도가 향상되고 결합도가 떨어진다. 그 결과 각각의 객체는 다른 객체와의 불필요한 협력을 제거하게 되고 자기 자신에게만 의존하는, 변경에 용이한 객체가 된다.
이 행위는 ‘책임’과도 연관이 있다. A라는 객체가 다른 객체의 데이터를 사용한다는 것은 A라는 객체의 책임(결합도)가 높아짐을 의미한다. 객체는 자기 자신만을 책임질 수 있어야 한다.(이하 ‘객체의 독립성’, 책에서는 ‘자율성’이라고 표현함)
위와 같이 개선을 하다보면 (TicketOffice 객체와 같이) TradeOff 가 발생하는 순간이 온다. 객체의 독립성 vs 객체의 의존성에 대한 TradeOff이다.
예를 들어, 위의 TicketOffice 는 아래 코드 처럼 TicketSeller 에서 처리될 수 있었던 코드이다. 객체의 독립성을 위해 개선하면서 위와 같은 코드가 된 것이다. 위와 같은 코드가 되면서 TicketOffice 는 응집도가 향상되었지만 Audience 에 의존을 갖게 되었다. (책에서는 결국 아래 코드처럼 작성하기를 선택했다고 한다.) 포인트는 객체의 독립성(응집도)도 좋지만 적절하게 타협할 수도 있어야 한다는 것이다.
class TicketSeller {
...
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
또, 눈여겨볼 점은 ‘의인화’이다. 실생활에서는 Bag, TicketOffice 의 경우 수동적인 객체이다. 실생활을 생각한다면 Bag, TicketOffice는 로직을 가져서는 안된다.
하지만 이러한 객체가 코드로써 표현될 때에는 능동적으로 동작해야 한다고 한다.
" 비록 현실에서는 수동적인 존재라고 하더라도 일단 객체지향의 세계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀌다. 레베카 워프스브록은 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 의인화라고 부른다. "
즉, 사물이나 사람과 같이 보지말고 하나의 객체로써 보면 될 것 같다.
1장에서의 핵심 키워드(문장)를 정리해보면 다음과 같을 것 같다.
- 변경에 용이해야 한다.
- 객체의 자율성(독립성) : 객체를 독립적으로 만든다. (객체를 자율적인 존재로 만들어라.)
- 의인화 : 모든 객체를 능동적인 존재로 생각한다.