1. 객체, 설계
티켓 판매 어플리케이션 구현하기
티켓 판매라는 도메인을 활용해서 객체, 설계에 대한 이야기를 해본다.
이 도메인에는 초대장 개념이 존재한다. 초대장 개념을 구현한 Invitation 클래스는 초대일자를 의미하는 when 인스턴스 변수를 포함한다.
1 | public class Invitation { |
공연을 관람하기 위해 필요한 티켓이라는 개념도 존재한다.
1 | public class Ticket { |
초대장이 없는 관람객은 이 티켓을 현금으로 구매가 가능하다. 관람객이 초대장, 티켓, 현금을 들고 다닐 수 있는 가방 Bag 클래스가 있다.
1 | public class Bag { |
이 도메인에서 실제로 생성되는 Bag 인스턴스는 현금과 초대장을 함께 보관하거나, 초대장 없이 현금만 보관하는 두 가지 중 하나이다. 이런 제약을 강제할 수 있도록 생성자를 추가하자.
1 | public class Bag { |
이번에는 관람객 개념을 구현하는 Audience 클래스를 만들어보자. 이 관람객들은 소지품을 보관하기 위해 가방을 소지할 수 있다.
1 | public class Audience { |
관람객이 소극장에 입장하기 위해서는 매표소에서 초대장을 티켓으로 교환하거나 구매해야 한다. 따라서 매표소에는 관람객에게 판매할 티켓과 티켓의 판매 금액이 보관돼 있어야 한다.
1 | public class TicketOffice { |
매표소에는 판매원이 있다. 판매우언은 매표소에서 초대장을 티켓으로 교환해 주거나 티켓을 판매하는 역할을 수행한다. 판매원을 구현한 TicketSeller 클래스는 자신이 일하는 매표소(ticketOffice)를 알고 있어야 한다.
1 | public class ticketSeller { |
마지막으로 소극장 클래스에서 관람객을 맞이할 수 있도록 enter 메서드를 구현하자.
1 | public class Theater { |
무엇이 문제인가
로버트 마틴이 언급한 소프트웨어 모듈이 가져야 하는 세가지 기능은 아래와 같다.
- 첫번째 목적은 실행 중에 제대로 동작하는 것이다.
- 두번째 목적은 변경을 위해 존재하는 것이다.
- 세번째 목적은 코드를 읽는 사람과 의사소통 하는 것이다.
즉 모든 모듈은 제대로 실행되야 하고, 변경이 용이해야 하며, 이해하기 쉬워야 한다.
위 코드는 첫번째 목적은 만족하지만 두번째와 세번째 즉 변경 용이성과 가독성의 목적은 만족시키지 못한다. 위 코드를 읽어보면 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재라는 점을 알 수 있다. 소극장이 관람객의 가방을 마음대로 열어보기도 하고 판매원의 매표소도 소극장이 마음대로 접근하고 있다. 이러한 동작 방식은 우리의 예상을 벗어난다 즉 이해하기가 어렵다.
또 위 코드에서 관람객이 가방을 들고 있다는 가정이 바뀌었다고 상상해보자. Audience 클래스에서 Bag을 제거해야 할 뿐만 아니라 Audience의 Bag에 직접 접근하는 Theater의 enter 메서드 역시 수정해야 한다. 즉 이것은 객체 사이의 의존성과 관련된 문제다. 좋은 코드로 가기 위해서 우리의 목표는 애플리케이션의 기능을 구현하는 데 필요한 최소한의 의존성만 유지하고 불필요한 의존성을 제거하는 것이다.
설계 개선하기
다시말해서 관람객과 판매원이 자신의 일을 스스로 처리해야 한다는 우리의 직관을 벗어난다. 해결 방법은 간단하다. Theater가 Audience와 TicketSeller에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하면 된다. 다시 말해서 관람객과 판매원을 자율적인 존재로 만들면 되는 것이다.
1 | public class TicketSeller { |
TicketSeller에서 getTicketOffice 메서드가 제거됐다는 사실에 주목하라. ticketOffice의 가시성이 private이고 접근 가능한 퍼블릭 메서드가 더 이상 존재하지 않기 때문에 외부에서는 ticketOffice에 직접 접근할 수 없다. 이처럼 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것은 캡슐화(encapsulation)이라고 부른다.
1 | public class Theater { |
수정된 Theater 클래스 어디서도 ticketOffice에 접근하지 않는다는 사실에 주목하라. Theater는 ticketOffice가 TicketSeller 내부에 존재한다는 사실을 알지 못한다. 위 구조에서 Theater는 오직 TicketSeller의 인터페이스(interface)에만 의존한다. TicketSeller가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실은 구현(implementation)의 영역에 속한다.
그 다음으로 Audience의 캡슐화를 개선해보자.
1 | public class Audience { |
Audience가 Bag을 직접 처리하기 때문에 외부에서는 더 이상 Audience가 Bag을 소유하고 있다는 사실을 알 필요가 없다. TicketSeller도 Audience의 인터페이스에만 의존하도록 수정하자.
1 | public class TicketSeller { |
코드를 수정한 결과, TicketSeller와 Audience 사이의 결합도가 낮아졌다. 수정된 Audience와 TicketSeller는 자신이 가지고 있는 소지품을 스스로 관리한다. 더 중요한 점은 Audience나 TicketSeller의 내부 구현을 변경하더라도 Theater를 함께 변경할 필요가 없어졌다.
캡슐화와 응집도
핵심은 객체 내부의 상태를 캡슐화하고 객체 간에 오직 메시지를 통해서만 상호작용하도록 만드는 것이다. 밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도(cohesion)가 높다고 말한다. 객체의 응집도를 높이기 위해서는 객체 스스로 자신의 데이터를 책임져야 한다.
절차지향과 객체지향
수정하기 전 코드를 살펴보자. 이 관점에서 Theater의 enter 메서드는 프로세스(Process)이며 Audience, TicketSeller, Bag, TicketOffice는 데이터(Data)다. 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍(Procedural Programming)이라고 부른다.
이러한 절차적 프로그래밍의 세상에서는 데이터의 변경으로 인한 영향을 지역적으로 고립시키기가 어렵다. 즉 Audience와 TicketSeller의 내부 구현을 변경하려면 Theater의 enter 메서드를 함께 변경해야 한다. 변경하기 쉬운 설계는 한 번에 하나의 클래스만 변경할 수 있는 설계다. 절차적 프로그래밍 방식과 다르게 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍 하는 방식을 객체지향 프로그래밍(Object-Oriented Programming)이라고 부른다.