티스토리 뷰
로버트 마틴은 클린 소프트웨어라는 책에서 소프트웨어 모듈이 가져야 하는 세 가지 기능에 관해 설명한다. 여기서 모듈이란 크기와 상관없이 클래스나 패키지, 라이브러리와 같이 프로그램을 구성하는 임의의 요소를 의미한다.
모든 소프트웨어 모듈에는 세 가지 목적이 있다. 첫 번째 목적은 실행 중에 제대로 동작하는 것이다. 이것은 모듈의 존재 이유라고 할 수 있다. 두 번째 목적은 변경을 위해 존재하는 것이다. 대부분의 모듈은 생명주기 동안 변경되기 때문에 간단한 작업만으로도 변경이 가능해야 한다. 변경하기 어려운 모듈은 제대로 동작하더라도 개선해야 한다. 모듈의 세 번째 목적은 코드를 읽는 사람과 의사소통하는 것이다. 모듈은 특별한 훈련 없이도 개발자가 쉽고 읽고 이해할 수 있어야 한다. 읽는 삶과 의사소통할 수 없는 모듈은 개선해야 한다.
요약하자면 모든 모듈은 제대로 실행돼야 하고, 변경이 용이해야 하며, 이해하기 쉬워야 한다. 제대로 실행돼야 하는 것은 애플리케이션을 만들면서 당연한 말이고, 변경 용이성과 읽는 사람과의 의사소통이라는 목적을 지키지 않는 것은 어떤 경우를 말하는지, 어떻게 해결하는지에 대해서 적어본다. 대부분의 내용은 오브젝트 책에서 나오고, 짧은 나의 생각을 추가했다.
예상을 빗나가는 코드
읽는 사람과의 의사소통이 안된다는 말이 뭘까? 이 말은 결국 읽는 사람이 코드를 쉽게 이해하지 못하는 것을 말한다. 반대로 이해가 쉽게 가능한 코드는 그 동작이 읽는 사람의 예상에서 크게 벗어나지 않는 코드다.
예상을 빗나가는 코드가 뭔지, 예를 들어 설명한다.
public class Theater {
private 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);
}
}
}
위 예제에서 enter 메서드가 수행하는 일을 말로 풀어보면 다음과 같다.
소극장은 관람객의 가방을 열어 그 안에 초대장이 있는지 살펴본다. 가방 안에 초대장이 들어 있으면 판매원은 매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다. 가방 안에 초대장이 들어 있지 않다면 관람객의 가방에서 티켓 금액만큼의 현금을 꺼내 매표소에 적립한 후에 매표소에 보관돼 있는 티켓을 관람객의 가방 안으로 옮긴다.
뭔가 이상하다. 우리가 살아가고 있는 현실에서 일어나는 일과는 다른 것 같다. 현실에서는 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건넨다. 티켓을 구매하는 관람객은 가방 안에서 돈을 직접 꺼내 판매원에게 지불한다. 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고 관람객에게서 직접 돈을 받아 매표소에 보관한다. 하지만 예제의 코드는 우리의 상식과 다르게 동작하기 때문에 코드를 읽는 사람과 제대로 의사소통하지 못한다.
코드를 이해하기 어렵게 만드는 또 다른 이유가 있는데, 개인적으로 이 이유가 더 와닿을 수 있다고 생각한다.
바로 이 코드를 이해하기 위해서는 여러 가지 세부적인 내용들을 한꺼번에 기억하고 있어야 한다는 점이다. 예제를 보면, enter 메서드를 이해하기 위해서는 Audience가 Bag을 가지고 있고, Bag 안에는 현금과 티켓이 들어 있으며 TicketSeller가 TicketOffice에서 티켓을 판매하고, TicketOffice안에 돈과 티켓이 보관돼 있다는 모든 사실을 동시에 기억하고 있어야 한다.
위 예제는 하나의 클래스나 메서드에 너무 많은 세부사항을 다루고 있어 읽고 이해해야 하는 사람한테 큰 부담을 준다.
변경에 취약한 코드
하지만 더 큰 문제는 변경에 취약하다는 점이다. 이 코드는 관람객(Audience)이 항상 가방을 들고 다닌다고 가정하고 있고, 판매원이 매표소에서만 티켓을 판매한다고 가정하고 있다. 또한 만약 관람객이 현금이 아닌 신용카드로 티켓을 사고 싶다면 어떻게 해야 할까? 아니면 현금을 가방이 아니라 주머니에 가지고 다닌다면? 관람객의 가방에 직접적으로 접근하는 enter 메서드 또한 변경되어야 한다.
여기서 중요한 점은 지나치게 세부적인 사실에 의존해서 동작한다. 다른 클래스가 Audience의 내부에 대해 더 많이 알면 알수록 Audience를 변경하기 어려워진다. 이것은 객체 사이의 의존성과 관련된 문제다. 의존성은 변경과 관련돼 있다. 한 객체를 변경할 때 해당 객체를 의존하는 다른 객체들도 변경될 수 있다는 것을 내포하고 있다.
객체 사이의 의존성이 과한 경우를 결합도가 높다고 말한다. 반대로 객체들이 합리적인 수준으로 의존할 경우에는 결합도가 낮다고 말한다. 두 객체 사이의 결합도가 높으면 높을수록 함께 변경될 확률도 높아지기 때문에 변경하기 어려워진다.
해결
변경과 의사소통이라는 문제가 서로 엮여있다는 걸 생각해보면, 이해하기 어려운 이유가 Theater가 Audience와 TicketSeller에 대해 너무 세세하게 알고 있기 때문이다. 따라서 세세한 부분까지 알지 못하도록 정보를 차단하고, 관람객과 판매원이 스스로 맡은 일을 처리하면 문제를 해결할 수 있다.
간단하게 말하면 관람객과 판매원을 자율적인 존재로 만들면 되는 것이다.
먼저, 극장은 판매원이 매표소에서 티켓을 판매한다는 사실을 알 필요가 없다. 그래서 이러한 내용을 판매원 내부로 숨겨보자.
public class TicketSeller {
private TicketOffice ticketOffice;
public void sellTo(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);
}
}
}
이렇게 객체가 가지고 있는 필드를 외부에서 접근하는 것이 아니고 은닉하여 객체 내부에서만 사용하는 것을 캡슐화라고 한다. Theater은 아래와 같이 간단하게 바뀐다.
public class Theater {
private TicketSeller ticketSeller;
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
여기서 중요한 점은 Theater가 TicketSeller안에 TicketOffice가 있다는 사실을 알지 못한다. 그냥 TicketSeller가 티켓 판매 로직을 처리하는구나를 알고있을 뿐이다. Theater은 오직 TicketSeller의 인터페이스에만 의존을 하고, TicketSeller 내부의 구현은 알지 못한다. 이렇게 인터페이스와 구현을 나누고 인터페이스만을 공개하는 것은 객체 사이의 결합도를 낮추고 변경하기 쉬운 코드를 작성하기 위해 따라야 하는 가장 기본적인 설계 원칙이다.
아직 고쳐야 할 것이 남아있다. Audience는 여전히 객체로 사용되는 것이 아닌 데이터로 사용되는 수동적인 존재다. TicketSeller가 Audience의 Bag에 직접적으로 접근하는 것을 Audience의 내부로 감춰보자.
package theater;
public class Audience {
private Bag bag;
public Long buy(Ticket ticket) {
if(bag.hasInvitation()) {
bag.setTicket(ticket);
return 0L;
}
bag.setTicket(ticket);
bag.minusAmount(ticket.getFee());
return ticket.getFee();
}
}
TicketSeller에서는 다음과 같이 변경된다.
package theater;
public class TicketSeller {
private TicketOffice ticketOffice;
public void sellTo(Audience audience) {
Long paidAmount = audience.buy(ticketOffice.getTicket());
ticketOffice.plusAmount(paidAmount);
}
}
객체를 데이터로서 사용하지 말고 객체스럽게 사용한다라는 말은 단일 책임 원칙이 떠오른다. 객체들이 각자의 책임을 맡고, 책임이 아닌 행위는 메세지를 통해 책임이 맞는 객체로 보낸다. 이 것을 책임의 이동이라고 하는데, 변경하기 전 코드는 책임이 Theater에 집중되어 있는 반면, 변경한 코드는 각 객체가 맡은 일을 스스로 처리하면서 Theater에 몰려 있던 책임이 개별 객체로 이동한 것이다.
Bag 객체도 자유롭지 않다. 자기 자신을 책임지지 않고 외부에서 수동적으로 사용되고 있다. Audience의 buy로직을 Bag 내부로 숨겨보자.
public class Bag {
private Long amount;
private Invitation invitation;
private Ticket ticket;
public Long hold(Ticket ticket) {
if(hasInvitation()) {
setTicket(ticket);
return 0L;
}
setTicket(ticket);
minusAmount(ticket.getFee());
return ticket.getFee();
}
private boolean hasInvitation() {
return invitation != null;
}
private void setTicket(Ticket ticket) {
this.ticket = ticket;
}
private void minusAmount(Long amount) {
this.amount -= amount;
}
}
Audience는 다음과 같이 변경된다.
public class Audience {
private Bag bag;
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
}
이렇게 객체 간의 의존성을 느슨함으로써 결합도를 낮추고, 각 객체한테 책임을 부여하면서 응집도를 높였다. 단순히 OOP의 특징, SOLID 개념을 달달 외우는 것보다 이렇게 예제를 리팩토링하면서 OOP의 특징들을 어떤 상황에서 어떻게 사용해야 하는지 알면 객체지향적으로 설계하는게 재밌어질 수 있다.
오브젝트 책에서는 이런 말이 나온다.
이 책을 읽다 보면 유연한 객체지향 설계에 이르는 길이 생각보다 어렵지 않다는 사실을 알게 될 것이다.
뭔가 오랜만에 책으로 공부하는데, 앞으로 읽으면서 또 어떤 내용이 즐겁게 할지 기대가 된다. 오브젝트 책 강추!
'Java' 카테고리의 다른 글
유용한 Stream API의 연산자들을 알아보자 (0) | 2022.10.29 |
---|---|
[자바 객체지향의 원리와 이해 책] 3장 1-2챕터 요약 (0) | 2022.08.18 |
[자바 객체지향의 원리와 이해 책] 2장 챕터 1-2 요약 (0) | 2022.08.17 |
스트림으로 짠 게시글 전체조회 분석해보기 (0) | 2022.07.26 |
디폴트 메서드를 사용하기 전과 후 비교 (0) | 2022.07.10 |
- Total
- Today
- Yesterday