대부분의 사람들은 이론이 정립된 후 실무가 그에 따라 발전된다고 생각하지만,
분야를 막론하고 이론을 정립할 수 없는 초기에는 실무가 먼저 급속한 발전을
이룬다. 한마디로 이론보다 실무가 먼저라는 것 이다. 그런 맥락에서 짧은 시간내에 발전한
소프트웨어 분야의 경우 실무에 초점을 맞추는 것이 설계와 유지보수에 있어 효과적이다.
이벤트에 당첨된 관광객과 그렇지 못한 관광객을 구분하여 이벤트 당첨 여부에 따라
초대장과 티켓을 교환 혹은 티켓을 판매하는 상황을 가정해보자.
초대장은 초대 일자를 포함하여 아래와 같이 정의할 수 있다.
package chap1;
import java.time.LocalDateTime;
public class Invitation {
private LocalDateTime when;
}
티켓의 경우는 아래와 같이 표현할 수 있다.
package chap1;
public class Ticket {
private Long fee;
public Long getFee() {
return fee;
}
}
관람객은 초대장, 현금, 티켓 세 가지만을 가질 수 있고 이를 보관하기 위해 가방을
가지고 있다고 생각하자.
package chap1;
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;
}
}
조건에 따라 Bag 인스턴스의 상태는 현금과 초대장을 함께 보관하거나 초대장
없이 현금만 보관하는 상태중 하나일 것이다. 인스턴스 생성 시점에 이 제약을
강제하도록 생성자를 추가하자.
public class Bag {
private Long amount;
private Invitation invitation;
private Ticket ticket;
public Bag(Long amount, Invitation invitation) {
this.amount = amount;
this.invitation = invitation;
}
//...
}
관람객은 아래와 같이 구현할 수 있다.
package chap1;
public class Audience {
private Bag bag;
public Audience(Bag bag) {
this.bag = bag;
}
public Bag getBag() {
return bag;
}
}
이제 매표소를 구현해보자.
package chap1;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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 getTicket() {
return tickets.remove(0);
}
public void minusAmount(Long amount) {
this.amount -= amount;
}
public void plusAmount(Long amount) {
this.amount += amount;
}
}
판매원은 자신이 일하는 매표소를 알고 있어야 한다.
package chap1;
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public TicketOffice getTicketOffice() {
return ticketOffice;
}
}
현재까지 구현한 상황은 다음과 같다.

소극장은 다음과 관람객을 맞이할 수 있게 다음과 같이 구현하자.
package chap1;
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);
}
}
}
위 과정을 통해 작성된 코드는 정상적으로 동작하는 듯 보이나 문제를 가지고 있다.
로버트 마틴의 클린 소프트웨어 에선 소프트웨어 모듈이 가져야 하는 세 가지 기능을
다음과 같이 설명한다.
- 실행 중에 제대로 동작해야 한다.
- 간단한 작업만으로 변경이 가능해야 한다.
- 다른 개발자가 쉽게 읽고 이해할 수 있어야 한다.
앞선 코드는 변경 용이성과 읽는 사람과의 의사소통 목적을 만족하지 못한다.
Theater 클래스에서 관람객의 가방은 소극장이 확인하고, 매표소의 자원에도
판매원의 허락 없이 소극장이 접근한다. 이는 실제로 주어진 요구사항 상황과
다른 흐름이다. 또한 Theater의 enter 메서드 내에서 초대장을 확인하고
구매하는 과정을 일괄적으로 소화하다 보니 코드 작성자와 코드를 읽고 이해하는
사람 모두에게 복잡하다는 부담을 준다.
Audience 클래스에서 Bag을 제거해야 할 경우 Audience의 Bag에
접근하는 Theater의 enter 메서드 역시 수정해야 한다. 이처럼 다른
클래스가 Audience에 대해 너무 세부적으로 알 수록 Audience를
변경하기 어렵다.
이는 어떤 객체가 변경될 때 그 객체에 의존하는 다른 객체도 변경될 수 있다는
의존성 을 내포하고 있다. 우리의 목적은 기능을 구현하는데 필요한 최소한의
의존성만 유지하는 것이다.
Theater와 같이 객체 사이의 의존성이 너무 많은 경우를 가리켜 결합도 가
높다고 한다. 결합도 높을 수록 변경이 어렵다. 따라서 결합도를 낮추는 방향으로
설계해야 한다.
Theater가 관람객이 소극장에 입장하는 것만 처리하고, 관람객과 판매원을
자율적인 존재 로 만들면 설계를 개선할 수 있다.
Theater의 enter 메서드에서 TicketOffice에 접근하는 모든 코드를
TicketSeller 내부로 숨기자.
package chap1;
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
// Theater.enter -> TicketSeller.sellTo
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);
}
}
}
ticketOffice에 대한 접근을 오롯히 TicketSeller만 할 수 있게 되어
캡슐화 를 수행할 수 있다. 캡슐화를 통해 객체 내부로의 접근을 제한해
객체간 결합도를 낮출 수 있다.
package chap1;
public class Theater {
private TicketSeller ticketSeller;
public Theater(TicketSeller ticketSeller) {
this.ticketSeller = ticketSeller;
}
// 변경
public void enter(Audience audience) {
ticketSeller.sellTo(audience);
}
}
Theater는 오직 TicketSeller의 인터페이스 에만 의존한다.
TicketSeller가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실은
구현 의 영역에 속한다. 객체를 인터페이스와 구현으로 나누고 인터페이스만을
공개하는 것은 결합도를 낮추기 위한 기본 설계 원칙이다.
다음으로 Bag 에 접근하는 모든 로직을 Audience 내부로 감추어 로직을
더 개선할 수 있다.
package chap1;
public class Audience {
private Bag bag;
public Audience(Bag bag) {
this.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();
}
}
}
TicketSeller가 Audience의 인터페이스에만 의존하도록 수정하자.
package chap1;
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public void sellTo(Audience audience) {
ticketOffice.plusAmount(audience.buy(ticketOffice.getTicket()));
}
}
수정된 Audience와 TicketSeller는 자신의 소지품을 스스로 관리한다. 이는
예상했던 동작과 정확히 일치하여 읽는 사람과의 의사소통 측면에서 확실히 개선되었다.
또한 Audience와 TicketSeller의 내부 구현을 변경하여도 Theater를 함께
변경할 필요가 없다.
TicketOffice를 사용하는 모든 부분을 TickSeller 내부로 옮기고 Bag을
사용하는 모든 부분을 Audience 내부로 옮겼다. 한마디로 객체의 자율성을 높이는
방향으로 설계해 이해하고 유연하게 되었다.
핵심은 객체 내부의 상태를 캡슐화하고 객체 간에 오직 메시지를 통해서만
상호작용하도록 만드는 것이다. 밀접하게 연관 작업만을 수행하고 연관성 없는 작업은
다른 객체에게 위임하는 객체를 가리켜 응집도 가 높다고 말한다.
객체의 응집도를 높이기 위해선 객체 스스로 자신의 데이터를 책임져야 한다.
Theater의 enter 메서드는 프로세스 이며 Audience, TicketSeller, Bag, TickerOffice는
데이터 다. 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을
절차적 프로그래밍 이라 부른다.
하지만 절차적 프로그래밍은 우리의 직관에 위배된다. 또한 데이터 변경으로 인한
영향을 지역적으로 고립시키기 어렵게 만든다.
변경하기 쉬운 설계는 하나의 클래스만 변경할 수 있는 설계다.
해결 방법은 자신의 데이터를 스스로 처리하도록 프로세스의 적절한 단계를
Audience와 TicketSeller 내부로 이동시키는 것이다. 이와 같은 데이터와
프로세스를 하나의 모듈에 위치하는 방식을 OOP 라고 부른다.
훌륭한 객체 지향 설계의 핵심은 캡슐화를 이용해 의존성을 적절히 관리해 객체
사이의 의존도를 낮추는 것이다.
'책임'은 기능을 가리키는 OOP의 용어로 볼 수 있다. 코드의 변경에 따라 Theater에
몰려있던 책임이 적절한 개별 객체로 이동하였다. 이것이 책임의 이동 이다.
객체 지향 설계에서는 책임이 각 객체에 분배되기 때문에 객체가 스스로 책임진다.
따라서 객체가 어떤 데이터를 가지냐 보다 객체에 어떤 책임을 할당한 것인지를 초점을
맞춰야 한다.
Bag 역시도 자율성을 가지도록 개선할 수 있다.
package chap1;
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 boolean hasInvitation() {
return invitation != null;
}
private void minusAmount(Long amount) {
this.amount -= amount;
}
}
코드의 중복을 제거하고 표현력을 높이려 기존의 메서드들을 삭제하지 않은 채 활용했다.
다만, private 으로 제어자를 변경했다. 이제 Audience를 수정해보자.
package chap1;
public class Audience {
private Bag bag;
public Audience(Bag bag) {
this.bag = bag;
}
public Long buy(Ticket ticket) {
return bag.hold(ticket);
}
}
TicketSeller 역시 TicketOffice의 자율권을 침해한다. 이것을 개선하자.
package chap1;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
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 void sellTicketTo(Audience audience){
plusAmount(audience.buy(getTicket()));
}
private Ticket getTicket() {
return tickets.remove(0);
}
private void plusAmount(Long amount) {
this.amount += amount;
}
}
package chap1;
public class TicketSeller {
private TicketOffice ticketOffice;
public TicketSeller(TicketOffice ticketOffice) {
this.ticketOffice = ticketOffice;
}
public void sellTo(Audience audience) {
ticketOffice.sellTicketTo(audience);
}
}
이 변경을 통해 TicketSeller가 TicketOffice의 구현이 아닌 인터페이스에
의존하게 되었다. 하지만 TicketOffice가 Audience에 관해 알고 있어야 한다는
새로운 의존성 추가, 결합도 증가가 발생했다.
이를 통해 두 가지 사실을 알 수 있다.
직관에 따르는 코드는 이해하기 쉬운 경향이 있다. 하지만 Theater, Bag과 같은
경우는 현실에선 자율적인 존재가 아니다. 코드에서는 이들 역시 자율적인 존재로
취급하였다. 이처럼 능동적이고 자율적인 존재로 객체를 설계하는 원칙을
의인화(anthropomorphism) 라고 부른다.
현실에선 수동적인 존재라 하여도 객체지향적인 설계에선 자율적인 존재로 설정되어야
한다.