[0. 프로그래밍 패러다임](# 0. 프로그래밍 패러다임)
[1. 객체, 설계](# 1. 객체, 설계)
[2. 객체지향 프로그래밍](# 2. 객체지향 프로그래밍)
[3. 역할, 책임, 협력](# 3. 역할, 책임, 협력)
[4. 설계 품질과 트레이드오프](# 4. 설계 품질과 트레이드오프)
[5. 책임 할당하기](# 5. 책임 할당하기)
[6. 메시지와 인터페이스](# 6. 메시지와 인터페이스)
[7. 객체 분해](# 7. 객체 분해)
프로그래밍 패러다임 = 규칙성? 통일성?
다중패러다임 언어(Multiparadigm Language)
- 절차형 패러다임 + 객체지향 패러다임 -> c++ 탄생 (사실 객체지향 패러다임에 가깝지 않나?)
- 함수형 패러다임 + 객체지향 패러다임 -> 스칼라(scala)
쿤의 패러다임 | 프로그래밍 패러다임 |
---|---|
상이한 두 가지 패러다임이 있을 때 두 패러다임은 함께 존재할 수 없다. | 서로 다른 패러다임이 하나의 언어 안에서 공존함으로써 서로의 장단점을 보완하는 경향이 있다. |
과거의 패러다임과 새로운 패러다임은 개념 자체가 다르기 때문에 비교할 수 없다. | 절차형 패러다임과 객체지향 패러다임을 비교하는 것이 가능하다. |
프로그래밍 패러다임은 혁명적이 아니라 발전적이다.
객체지향이 적합하지 않은 상황에서는 언제라도 다른 패러다임을 적용할 수 있는 시야를 기르고 지식을 갈고 닦아야 한다.
-> 그치만 자바를 쓰는 상황에서는 무조건 객체지향을 고집하는게 맞겠지?
[위로](# 목차)
어떤 분야를 막론하고 이론을 정립할 수 없는 초기에는 실무가 먼저 급속한 발전을 이룬다. - 글래스
당연한 말 같으면서도 좀 구차하게 조건을 붙혀가며 멋있으려고 한 느낌..
대부분의 사람들은 이론이 먼저 정립된 후에 실무가 그 뒤를 따라 발전한다고 생각한다는데 글쎄?
소프트웨어 설계와 유지보수에 중점을 두려면 이론이 아닌 실무에 초점을 맞추는 것이 효과적이다.
- Invitation - 당첨자에게 발송되는 초대장
- 공연을 관람할 수 있는 초대일자(when)
- Ticket - 공연을 관람하기 위한 티켓
- Bag - 관광객이 소지품을 보관할 가방
- Invitation
- Ticket
- Amount
- 이벤트에 당첨된 관람객의 가방 안에는 현금과 초대장이 들어있지만 이벤트에 당첨되지 않은 관람객의 가방 안에는 초대장이 들어있지 않을 것이다. -> Bag의 인스턴스를 생성하는 시점에 이 제약을 강제할 수 있도록 생성자를 추가
- Audience - 관람객이라는 개념을 구현. 소지품을 보관하기 위한 가방을 소지
- Bag
- TicketOffice - 초대장을 티켓으로 교환하거나 구매할 장소
- TicketSeller - 매표소에서 일할 판매원
로버트 마틴(Robert C. Martin)
소프트웨어 모듈이 가져야 하는 세가지 기능
- 실행 중에 제대로 동작하는 것
- 변경을 위해 존재하는 것
- 코드를 읽는 사람과 의사소통하는 것
1.1 까지의 코드는 2와 3을 만족시키지 못한다.
문제: 관람객과 판매원이 소극장의 통제를 받는 수동적인 존재라는 점
- 소극장이라는 제3자가 관람객의 가방을 마음대로 열어 본다.
- 소긍장이 허락도 없이 매표소에 보관 중인 티켓과 현금에 마음대로 접근한다.
- 티켓을 꺼내 관람객의 가방에 집어넣고 괌람객에게서 받은 돈을 매표소에 적립하는 일은 판매원이 아닌 소극장이 수행한다.
TO-BE
- 관람객이 직접 자신의 가방에서 초대장을 꺼내 판매원에게 건넨다.
- 티켓을 구매하는 관람객은 가방 안에서 돈을 직접 꺼내 판매원에게 지불한다.
- 판매원은 매표소에 있는 티켓을 직접 꺼내 관람객에게 건네고 관람객에게서 직접 돈을 받아 매표소에 보관한다.
+
코드를 이해하기 위해서는 여러 가지 세부족인 내용들을 한꺼번에 기억하고 있어야 한다.
+
가장 심각한 문제는 Audience와 TicketSeller를 변경할 경우 Theater도 함께 변경해야 한다는 사실이다.
- 관람객이 가방을 들고 있지 않다면?
- 관람객이 현금이 아니라 신용카드를 이용해서 결제를 한다면?
- 판매원이 매표소 밖에서 티켓을 판매해야 한다면?
객체 사이의 의존성(dependency) 과 관련된 문제.
객체 사이의 의존성이 과한 경우를 가리켜 결합도(coupling) 가 높다고 말한다. 설계의 목표는 객체 사이의 결합도를 낮춰 변경이 용이한 설계를 만드는 것이어야 한다.
Theater가 관람객의 가방과 판매원의 매표소에 직접 접근하기 때문에 코드를 이해하기 어렵다.
해결 방법: Theater가 Audience와 TicketSeller에 관해 너무 세세한 부분까지 알지 못하도록 정보를 차단하면 된다.
다시 말해서 관람객과 판매원을 자율적인 존재 로 만들면 되는 것이다.
- Theater의 enter 메서드에서 TicketOffice에 접근하는 모든 코드를 TicketSeller 내부로 숨긴다.
- 개념적이나 물리적으로 객체 내부의 세부적인 사항을 감추는 것을 캡슐화(encapsulation) 라고 부른다.
- Theater는 오직 TicketSeller의 인터페이스(interface) 에만 의존한다.
- TicketSeller가 내부에 TicketOffice 인스턴스를 포함하고 있다는 사실은 구현(implementation) 의 영역에 속한다.
- Audience의 캡슐화 개선
- Bag에 접근하는 모든 로직을 Audience 내부로 감춘다.
- TicketSeller가 Audience의 인터페이스에만 의존하도록 수정.
- 수정된 Audience와 TicketSeller는 자신이 가지고 있는 소지품을 스스로 관리한다.
- Audience나 TicketSeller의 내부 구현을 변경하더라도 Theater를 함께 변경할 필요가 없어졌다.
수정된 코드는 변경 용이성의 측면에서도 확실히 개선됐다고 말할 수 있다.
- 판매자가 티켓을 판매하기 위해 TicketOffice를 사용하는 모든 부분을 TicketSeller 내부로 옮기고,
- 관람객이 티켓을 구매하기 위해 Bag을 사용하는 모든 부분을 Audience 내부로 옮긴 것이다.
핵심은 객체 내부의 상태를 캡슐화하고 객체 간에 오직 메시지를 통해서만 상호작용하도록 만드는 것이다.
밀접하게 연관된 작업만을 수행하고 연관성 없는 작업은 다른 객체에게 위임하는 객체를 가리켜 응집도(cohesion) 가 높다고 말한다. 자신의 데이터를 스스로 처리하는 자율적인 객체를 만들면 결합도를 낮출 수 있을뿐더러 응집도를 높일 수 있다.
외부의 간섭을 최대한 배제하고 메시지를 통해서만 협력하는 자율적인 객체들의 공동체를 만드는 것이 훌륭한 객체지향 설계를 얻을 수 있는 지름길인 것이다.
수정하기 전의 코드에서 Theater의 enter 메서드는 프로세드(Process) 이며 Audience, TicketSeller, Bag, TicketOffice는 데이터(Data) 다. 이처럼 프로세스와 데이터를 별도의 모듈에 위치시키는 방식을 절차적 프로그래밍(Procedural Programming) 이라고 부른다.
만약에... 만약에 절차적 프로그래밍을 지향한다 할지라도... 수정하기 전의 코드를 좋은 코드라고 볼 수 있을까?? 객체지향 프로그래밍만 열심히 배워서 그래도 어떤 느낌인지 알겠는데... 앞서 언제라도 다른 패러다임을 적용할 수 있는 시야를 기르라고 했는데 좋은 절차적 프로그래밍은 뭘까?
절차적 프로그래밍의 세계에서는 관람객과 판매원이 수동적인 존재일 뿐이다. 타인이 자신의 가방을 마음대로 헤집어 놓아도 아무런 불만을 가지지 않는 소극적인 존재다.
더 큰 문제는 절차적 프로그래밍의 세상에서는 데이터의 변경으로 인한 영향을 지역적으로 고립시키기 어렵다는 것 이다. 절차적 프로그래밍의 세상은 변경하기 어려운 코드를 양산하는 경향이 있다.
해결 방법은 자신의 데이터를 스스로 처리하도록 프로세스의 적절한 단계를 Audience와 TicketSeller로 이동시키는 것이다. 이처럼 데이터와 프로세스가 동일한 모듈 내부에 위치하도록 프로그래밍하는 방식을 객체지향 프로그래밍(Object-Oriented Programming) 이라고 부른다.
두 방식 사이에 근본적인 차이를 만드는 것은 책임의 이동(Shift of responsibility) 이다.
작업 흐름이 주로 Theater에 의해 제어된다는 사실을 알 수 있다. 객체지향 세계의 용어를 사용해서 표현하면 책임이 Theater에 집중돼 있는 것 이다.
- 설계를 어렵게 만드는 것은 의존성 이라는 것을 기억하라.
- 해결 방법은 불필요한 의존성을 제거함으로써 객체 사이의 결합도 를 낮추는 것이다.
- 결합도를 낮추기 위해 사용하는 방법으로 캡슐화 를 이용.
- 캡슐화하는 것은 객체의 자율성 을 높이고 응집도 높은 객체들의 공동체를 창조할 수 있게 한다.
불필요한 세부사항을 캡슐화하는 자율적인 객체들이 낮은 결합도와 높은 응집도를 가지고 협력하도록 최소한의 의존성만을 남기는 것이 훌륭한 객체지향 설계다.
- Bag은 스스로 자기 자신을 책임지지 않고 Audience에 의해 끌려다니는 수동적인 존재다.
AS-IS | TO-BE | How |
---|---|---|
Bag은 스스로 자기 자신을 책임지지 않고 Audience에 의해 끌려다니는 수동적인 존재다. | Bag을 자율적인 존재로 바꿔보자. | Bag의 내부 상태에 접근하는 모든 로직을 Bag 안으로 캡슐화해서 결합도를 낮춘다. |
TicketSeller 역시 TicketOffice의 자율권을 침해한다. | 잃어버린 TicketOffice의 자율권을 찾아주자. | TicketSeller가 TicketOffice의 구현이 아닌 인터페이스에만 의존하게 하자. (but, TicketOffice와 Audience 사이에 의존성이 추가된다.) |
- 어떤 기능을 설계하는 방법은 한 가지 이상일 수 있다.
- 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 결국 설계는 트레이드오프의 산물이다.
비록 현실에서는 수동적인 존재라고 하더라도 일단 객체지향의 세계에 들어오면 모든 것이 능동적이고 자율적인 존재로 바뀐다. 이처럼 능동적이고 자율적인 존재로 소프트웨어 객체를 설계하는 원칙을 가리켜 의인화(anthropomorphism) 라고 부른다.
훌륭한 객체지향 설계란 소프트웨어를 구성하는 모든 객체들이 자율적으로 행동하는 설계를 가리킨다.
우리가 짜는 프로그램은 두 가지 요구사항을 만족시켜야 한다.
- 오늘 완성해야 하는 기능을 구현하는 코드를 짜야 하는 동시에
- 내일 쉽게 변경할 수 있는 코드를 짜야 한다.
훌륭한 객체지향 설계란 협력하는 객체 사이의 의존성을 적절하게 관리하는 설계다.
[위로](# 목차)
- 영화
- 제목
- 사영시간
- 가격 정보
- 상영
- 상영 일자
- 시간
- 순번
사용자가 셀지로 예매하는 대상은 영화가 아니라 상영이다.
할인액을 결정하는 두 가지 규칙
- 할인 조건(discount condition)
- 순서 조건(Sequence condition)
- 기간 조건(period condition)
- 할인 정책(discount policy)
- 금액 할인 정책(amount discount policy)
- 비율 할인 정책(percent discount policy)
클래스 기반의 객체지향 언어에 익숙한 사람이라면 가장 먼저 클래스를 결정한 후에 클래스에 어떤 속성과 메서드가 필요한지 고민한다.
안타깝게도 이것은 객체지향의 본질과는 거리가 멀다. 진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
- 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
영화 예메 시스템의 목적은 영화를 좀 더 쉽고 빠르게 예매하려는 사용자의 문제를 해결하는 것이다. 이처럼 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인 이라고 부른다.
책에서는 영화에 할인 정책을 할당하지 않거나 할당하더라도 오직 하나만 할당할 수 있도록 하였다.
그러나 현실에서는 보통 금액 할인 정책을 적용한 이후 비율 할인 정책이 중복 적용되는 사례를 심심찮게 볼 수 있다.
이 경우를 생각하고 개발한다면 차라리 할인 정책으로 묶지 않고 금액 할인 정책, 비율 할인 정책을 별개로 적용시키는 것이 옳은 방향이 아닐까?
- Movie - 영화
- title - 제목
- runningTime - 상영시간
- fee - 기본 요금
- discountPolicy - 할인 정책
- Screening - 상영
- sequence - 순번
- whenScreend - 상영 시작 시간
- DiscountPolicy - 할인 정책
- AmountDiscountPolicy - 금액 할인 정책
- PercentDiscountPolicy - 비율 할인 정책
- DiscountCondition - 할인 조건
- SequenceCondition - 순번 조건
- PeriodCondition - 기간 조건
- Reservation - 예매
- 객체는 상태(state) 와 행동(behavior) 을 함께 가지는 복합적인 존재이다.
- 객체는 스스로 판단하고 행동하는 자율적인 존재 이다.
데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 부르며, 대부분의 객체지향 프로그래밍 언어들은 더 나아가 외부에서의 접근을 통제할 수 있는 접근 제어(access control) 메커니즘도 함께 제공한다. 접근 제어를 위해 public, protected, private과 같은 접근 수정자(access modifier) 를 제공한다.
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다. 하나는 외부에서 접근 가능한 부분으로 이를 퍼블릭 인터페이스(public interface) 라고 부른다. 다른 하나는 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분으로 이를 구현(implementation) 이라고 부른다. 인터페이스와 구현의 분리(separation of interface and implementation) 원칙은 훌륭한 객체지향 프로그램을 만들기 위해 따라야 하는 핵심 원칙이다.
프로그래머의 역할을 클래스 작성자(class creator) 와 클라이언트 프로그래머(client programmer) 로 구분하는 것이 유용하다. 클래스 작성자는 새로운 데이터 타입을 프로그램에 추가하고, 클라이언트 프로그래머는 클래스 작성자가 추가하는 데이터 타입을 사용한다.
이런 개념은 처음 봐서 좀 새롭고 신선하다...
구분 | 프로그래머의 역할 | 프로그래머의 목표 |
---|---|---|
클래스 작성자 | 새로운 데이터 타입을 프로그램에 추가 | 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 꽁꽁 숨긴다 |
클라이언트 프로그래머 | 클래스 작성자가 추가한 데이터 타입을 사용 | 필요한 클래스들을 엮어서 애플리케이션을 빠르고 안정적으로 구축 |
클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다 - 구현 은닉(implementation hiding)
- Money 클래스에서 plus, minus 함수를 보면 Money 인스턴스 명으로 굳이 amount를 사용한다. 때문에 Money 객체의 인자명 amount와 겹쳐서 굳이 this.amount 혹은 amount.amount를 사용하게 되는데 직관성이 너무 떨어지는 것이 아닐까?
- Money 클래스에서 isLessThan 함수나 isGreaterThanOrEqual 함수를 보면 this.amount가 아닌 amount를 사용하였는데, this가 필요없는 것은 알겠지만 사용한 곳이 있다면 통일성을 유지하는 것이 좋지 않을까?
시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 협력(Collaboration) 이라고 부른다.
객체지향 프로그램을 작성할 때는 먼저 협력의 관점에서 어떤 객체가 필요한지를 결정하고, 객체들의 공통 상태와 행위를 구현하기 위해 클래스를 작성한다.
객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request) 할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response) 한다.
객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송(send a message) 하는 것뿐이다. 다른 객체에게 요청이 도착할 때 해당 객체가 메시지를 수신(receive a message) 했다고 이야기 한다. 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method) 라고 부른다.
지금까지는 Screening이 Movie의 calculateMovieFee '메서드를 호출한다'고 말했지만 사실은 Screening이 Movie에게 calculateMovieFee '메시지를 전송한다'라고 말하는 것이 더 적절한 표현이다. - 대박사건... 더 적절한 표현이라는데 너무 어색하게 느껴진다... why...?
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
이 코드에는 객체지향에서 중요하다고 여겨지는 두 가지 개념이 숨겨져 있다. 하나는 상속(ingeritance) 이고 다른 하나는 다형성 ㅇㅣ다. 그리고 그 기반에는 추상화(abstraction) 라는 원리가 숨겨져 있다.
여기서는 부모 클래스인 DiscountPolicy 안에 중복 코드를 두고 AmountDiscountPolicy와 PercentDiscountPolicy가 이 클래스를 상속받게 할 것이다. 실제 애플리케이션에서는 DiscountPolicy의 인스턴스를 생성할 필요가 없기 때문에 추상 클래스(abstract class) 로 구현했다.
직접적으로 중복되는 코드를 상속시켜주어야 하기 때문에 interface가 아닌 abstract class를 사용하는 것 같다.
또한 메소드만 포함된 것이 아니라 속성도 포함되있는 것이 다른 이유인 것 같다.
할인 조건을 만족하는 DiscountCondition이 하나라도 존재하는 경우에는 추상 메서도(abstract method) 인 getDiscountAmount 메서드를 호출해 할인 요금을 계산한다.
할인 조건이 여러 개가 존재하고 그 할인 조건을 모두 만족해야 할인을 해주는줄 알았는데, 여러 조건 중 단 한가지만 만족하면 된다. 나만 어색한가?
부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴 이라고 부른다.
Movie 클래스 어디에도 할인 정책이 금액 할인 정책인지, 비율 할인 정책인지를 판단하지 않는다. Movie 내부에 할인 정책을 결정하는 조건문이 없는데도 불구하고 어떻게 영화 요금을 계산할 때 금액 할인 정책과 비율 할인 정책을 선택할 수 있을까?
코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다. 다시 말해 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다. 그리고 유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다.
그러나 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다. 반면 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드는 더 유연해지고 확장 가능해진다. 이와 같은 의존성의 양면성은 설계가 트레이드오프의 산물이라는 사실을 잘 보여준다.
'코드의 의존성과 실행 시점의 의존성이 다르다' - 좀 더 자세한 설명...
부모 클래스와 다른 부분만을 추가해서 새로은 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference) 이라고 부른다.
대부분의 사람들은 상속의 목적이 메서드나 인스턴스 변수를 재사용하는 것이라고 생각한다. - me...
상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다. 결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있기 때문에 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
정리하면 자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에 부모 클래스 대신 사용될 수 있다.
이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅(upcastiog) 이라고 부른다.
Movie는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다. 이를 다형성 이라고 부른다.
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다. 이를 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding) 이라고 부른다. 그에 반해 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정하는 것을 초기 바인딩(early binding) 또는 정적 바인딩(static binding) 이라고 부른다.
종종 구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을 때가 있다. 이를 위해 자바에서는 인터페이스 라는 프로그래밍 요소를 제공한다.
추상 클래스를 이용해 다형성을 구현했던 할인 정책과 달리 할인 조건은 구현을 공유할 필요가 없기 때문에 자바의 인터페이스를 이용해 타입 계층을 구현했다.
- 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- "영화 예매 요금은 최대 하나의 '할인 정책'과 다수의 '할인 조건'을 이용해 계산할 수 있다."
- -> "영화의 예매 요금은 '금액 할인 정책'과 '두 개의 순서 조건, 한개의 기간 조건'을 이용해서 계산할 수 있다."
- 추상화를 이용하면 설계가 좀 더 유연해진다.
- 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.
할인 정책이 적용돼 있지 않은 경우, 할인 요금을 계산할 필요 없이 영화에 설정된 기본 금액을 그대로 사용할 수 있으나, 이 방식의 문제점은 할인 정책이 없는 경우를 예외 케이스로 취급하기 때문에 지금까지 일관성 있던 협력 방식이 무너지게 된다.
NoneDiscountPolicy 클래스의 getDiscountAmount() 메서드는 사용되지 않는다. 부모 클래스인 DiscountPolicy와 NoneDiscountPolicy를 개념적으로 결합시킨다. 이를 위해 원래의 DiscountPolicy 클래스의 이름을 DefaultDiscountPolicy로 바꾸고 DiscountPolicy 인터페이스를 만든다.
이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋으나, 현실적으로는 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다는 생각이 들 수도 있다. 이 책에서는 설명을 단순화하기 위해 인터페이스를 사용하지 않는 원래의 설계에 기반해서 설명을 이어갈 것이다.
구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다.
객체지향 설계와 관련된 자료를 조금이라도 본 사람들은 코드 재사용을 위해서는 상속보다는 합성(composition) 이 더 좋은 방법이라는 이야기를 많이 들었을 것이다. 합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.
상속 은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 기법이다. 하지만 두 가지 관점에서 설계에 안 좋은 영향을 미친다. 하나는 상속이 캡슐화를 위반한다는 것이고, 다른 하나는 설계를 유연하지 못하게 만든다는 것이다.
부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다.
이 부분 조금 더 이해하기...
상속을 과도하게 사용한 코드는 변경하기 어려워진다.
상속의 두 번째 단점은 설계가 유연하지 않다는 것이다. 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.
인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성 이라고 부른다.
합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.
- 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다.
- 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
그렇다고 해서 상속을 절대 사용하지 말라는 것은 아니다. 대부분의 설계에서는 상속과 합성을 함께 사용해야 한다. 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수 밖에 없다.
[위로](# 목차)
객체지향 패러다임의 관점에서 핵심은 역할(role), 책임(responsibility), 협력(collaboration) 이다. 클래스, 상속, 지연 바인딩이 주용하지 않은 것은 아니지만 다분히 구현 측면에 치우쳐 있기 때문에 객체지향 패러다임의 본질과는 거리가 멀다.
다양한 객체들이 영화 예매라는 기능을 구현하기 위해 메시지를 주고받으면서 상호작용한다. 이처럼 객체들이 애플리케이션의 기능을 구현하기 위해 수행하는 상호작용을 협력 이라고 한다. 객체가 협력에 참여하기 위해 수행하는 로직은 책임 이라고 부른다. 객체들이 협력 안에서 수행하는 책임들이 모여 객체가 수행하는 역할 을 구성한다.
메시지 전송(message sending) 은 객체 사이의 협력을 위해 사용할 수 있는 유일한 커뮤니케이션 수단이다. 객체는 다른 객체의 상세한 내부 구현에 직접 접근할 수 없기 때문에 오직 메시지 전송을 통해서만 자신의 요청을 전달할 수 있다.
메시지를 수신한 객체는 메서드 를 실행해 요청에 응답한다.
결과적으로 객체를 자율적으로 만드는 가장 기본적인 방법은 내부 구현을 캡슐화 하는 것이다.
책임 이란 객책에 의해 정의되는 응집도 있는 행위의 집합으로, 객체가 유지해야 하는 정보와 수행할 수 있는 행동에 대해 개략적으로 서술한 문장이다. 크레이그 라만(Craig Larman)은 이러한 분류 체계에 따라 객체의 책임을 크기 '하는 것(doing)' 과 '아는 것(knowing)' 의 두 가지 범주로 나누어 세분화하고 있다,
- 객체를 생성하거나 계산을 수행하는 등의 스스로 하는 것
- 다른 객체의 행동을 시작시키는 것
- 다른 객체의 활동을 제어하고 조절하는 것
- 사적인 정보에 관한 아는 것
- 관련된 객체에 관해 아는 것
- 자신이 유도하거나 계산할 수 있는 것에 관해 아는 것
"객체지향 개발에서 가장 중요한 능력은 책임을 능숙하게 소프트웨어 객체에 할당하는 것이다" - 크레이크 라만
객체의 구현 방법은 상대적으로 책임보다는 덜 중요하며 책임을 결정한 다음에 고민해도 늦지 않다.
자율적인 객체를 만드는 가장 기본적인 방법은 책임을 수행하는 데 필요한 정보를 가장 잘 알고 있는 전문가에게 그 책임을 할당하는 것이다. 이를 책임 할당을 위한 INFORMATION EXPERT(정보 전문가) 패턴이라고 부른다.
협력을 설계하기 위해서는 책임에 초점을 맞춰야 한다. 어떤 책임을 선택하느냐가 전체적인 설계의 방향과 흐름을 결정한다. 이처럼 책임을 찾고 책임을 수행할 적절한 객체를 찾아 책임을 할당하는 방식으로 협력을 설계하는 방법을 책임 주도 설계(Responsibility-Driven Design, RDD) 라고 부른다.
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 할당함으로써 두 객체가 협력하게 한다.
- 메시지가 객체를 결정한다는 것
- 행동이 상태를 결정한다는 것
나중에 다시 한 번 더 읽어보자
메시지가 객체를 선택하게 해야 하는 두 가지 이유
- 객체가 최소한의 인터페이스(minimal interface) 를 가질 수 있게 된다.
- 객체는 충분히 추상적인 인터페이스(abstract interface) 를 가질 수 있게 된다.
협력을 구성하는 객체들의 인터페이스는 충분히 추상적인 동시에 최소한의 크기를 유지할 수 있었다. 객체가 충분히 추상적이면서 미니멀리즘을 따르는 인터페이스를 가지게 하고 싶다면 메시지가 객체를 선택하게 해야 한다.
객체를 객체답게 만드는 것은 객체의 상태가 아니라 객체가 다른 객체에게 제공하는 행동이다.
객체지향 패러다임에 갓 입문한 사람들이 가장 쉽게 빠지는 실수는 객체의 행동이 아니라 상태에 초점을 맞추는 것이다. 초보자들은 먼저 객체에 필요한 상태가 무엇인지를 결정하고, 그 후에 상태에 필요한 행동을 결정한다. 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화 를 저해한다. 객체의 내부 구현에 초점을 맞춘 설계 방법을 데이터-주도 설계(Data-Driven Design) 라고 부른다.
객체가 가질 수 있는 상태는 행동을 결정하고 나서야 비로소 결정할 수 있다. 협력이 객체의 행동을 결정하고 행동이 상태를 결정한다. 그리고 그 행동이 바로 객체의 책임이 된다.
객체가 어떤 특정한 협력 안에서 수행하는 책임의 집합을 역할 이라고 부른다.
어떤 이유로 역할이라는 개념을 이용해서 설계 과정을 더 번거롭게 만드는 것일까? 어차피 역할이 없어도 객체만으로 충분히 협력을 설계할 수 있는 것 아닌가?
역할이 중요한 이유는 역할을 통해 유연하고 재사용 가능한 협력을 얻을 수 있기 때문이다.
문제를 해결하기 위해서는 객체가 아닌 책임에 초점을 맞춰야 한다. 요점은 동일한 책임을 수행하는 역할을 기반으로 두 개의 협력을 하나로 통합할 수 있다는 것이다. 따라서 역할을 이용하면 불필요한 중복 코드를 제거할 수 있다. 따라서 책임과 역할을 중심으로 협력을 바라보는 것이 바로 변경과 확장이 용이한 설계로 나아가는 첫걸음이다.
오직 한 종류의 객체만 협력에 참여하는 상황에서 역할이라는 개념을 고려하는 것이 유용할까? 역할이라는 개념을 생략하고 직접 객체를 이용해 협력을 설계하는 것이 더 좋지 않을까? 이런 경우에 역할을 사용하는 것은 상황을 오히려 더 복잡하게 만드는 것은 아닐까?
=> 확장이 용이하지 않은 설계를 만들 것 같다
대부분의 경우에 어떤 것이 역할이고 어떤 것이 객체인지가 또렷하게 드러나지는 않을 것이다. 특히나 명확한 기준을 세우기 어렵고 정보가 부족한 설계 초반에는 결정을 내리기가 더욱 어려울 것이다.
설계 초반에는 적절한 책임과 협력의 큰 그림을 탐색하는 것이 가장 중요한 목표여야 하고 역할과 객체를 명확하게 구분하는 것은 그렇게 중요하지는 않다는 것이다.
중요한 것은 협력을 구체적인 객체가 아니라 추상적인 역할의 관점에서 설계하면 협력이 유연하고 재사용 가능해진다는 것이다. 따라서 역할의 가장 큰 장점은 설계의 구성 요소를 추상화할 수 있다는 것이다.
[추상화를 사용한 설계가 가질 수 있는 두 가지 장점](# 추상화를 사용할 경우의 두 가지 장점)
추상화가 가지는 두 가지 장점은 협력의 관점에서 역할에도 동일하게 적용될 수 있다.
- 협력이라는 관점에서는 세부적인 사항을 무시하고 추상화에 집중하는 것이 유용하다.
- 역할은 다양한 환경에서 다양한 객체들을 수용할 수 있게 해주므로 협력을 유연하게 만든다.
결과적으로 앞으로 추가될 미지의 할인 정책과 할인 조건을 수용할 수 있는 유연한 설계를 얻을 수 있다. 프레임워크나 디자인 패턴과 같이 재사용 가능한 코드나 설계 아이디어를 구성하는 핵심적인 요소가 바로 역할이다.
역할은 모양이나 구조에 의해 정의될 수 없으며 오직 시스템의 문맥 안에서 무엇을 하는지에 의해서만 정의될 수 있다.
동일한 객체라고 하더라도 객체가 참여하는 협력에 따라 객체의 얼굴은 계속해서 바뀌게 된다.
[위로](# 목차)
객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다. 이 정의에는 객체지향 설계에 관한 두 가지 관점이 섞여 있다.
- 객체지향 설계의 핵심은 책임이다.
- 책임을 할당하는 작업이 응집도와 결합도 같은 설계 품질과 깊이 연관돼 있다.
상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다. - 98p
데이터 중심의 설계란 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할하는 방법이다. 데이터 중심의 설계는 객체가 내부에 저장해야 하는 '데이터가 무엇인가'를 묻는 것으로 시작한다. 때문에 Movie에 저장될 데이터를 결정하는 것으로 설계를 시작한다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private List<DiscountCondition> discountConditions;
private MovieType movieType;
private Money discountAmount;
private double discountPercent;
}
가장 두드러지는 차이점은 할인 조건의 목록(discountCondition)이 인스턴스 변수로 Movie 안에 직접 포함돼 있다는 것이다. 또한 할인 정책을 DiscountPolicy라는 별도의 클래스로 분리했던 이전 예제와 달리 금액 할인 정책에 사용되는 할인 금액(discountAmount)과 비율 할인 정책에 사용되는 할인 비율(discountPercent)을 Movie 안에서 직접 정의하고 있다.
영화에 사용된 할인 정책의 종류는 movieType으로 결정한다.
public enum MovieType {
AMOUNT_DISCOUNT, // 금액 할인 정책
PERCENT_DISCOUNT, // 비율 할인 정책
NONE_DISCOUNT // 미적용
}
2. 객체지향 프로그래밍 | 4. 설계 품질과 트레이드오프 |
---|---|
Movie - 영화 title - 제목 runningTime - 상영시간 fee - 기본 요금 discountPolicy - 할인 정책 Screening - 상영 sequence - 순번 whenScreend - 상영 시작 시간 DiscountPolicy - 할인 정책 AmountDiscountPolicy - 금액 할인 정책 PercentDiscountPolicy - 비율 할인 정책 DiscountCondition - 할인 조건 SequenceCondition - 순번 조건 PeriodCondition - 기간 조건 Reservation - 예매 | Movie title runningTime fee discountConditions movieType - 영화의 할인 정책 타입 discountAmount - 할인액 discountPercent - 할인비율 Screening movie sequence whenScreened DiscountCondition DiscountConditionType sequence dayOfWeek startTime endTime Reservation customer screening fee audienceCount Customer name id |
나중에 테이블 다시 정리...
ReservationAgency - 데이터 클래스들을 조합해서 영화 예매 절차를 구현하는 클래스
public class ReservationAgency {
public Reservation reserve(Screening screening, Customer customer, int audienceCount) {
Movie movie = screening.getMovie();
boolean discountable = false;
for(DiscountCondition condition : movie.getDiscountConditions()) {
if (condition.getType() == DiscountConditionType.PERIOD) {
discountable = screening.getWhenScreened().getDayOfWeek.equals(condition.getDayOfWeek()) &&
condition.getStartTime().compareTo(screening.getWhenScreened().toLocalTime()) <= 0 &&
condition,getEndTime().compareTo(screening.getWhenScreened().toLocalTime()) >= 0;
} else {
discountable = condition.getSequence() == screening.getSequence();
}
if discountable() {
break;
}
}
Money fee;
if (discountable) {
Money discountAmount = Money.ZERO;
switch(movie.getMovieType()) {
case AMOUNT_DISCOUNT:
discountAmount = movie.getDiscountAmount();
break;
case PERCENT_DISCOUNT:
discountAmount = movie.getFee().times(movie.getDiscountPercent());
break;
case NONE_DISCOUNT:
discountAmount = Money.ZERO;
break
}
fee = movie.getFee().minus(discountAmount);
} else {
fee = movie.getFee();
}
return new Reservation(customer, screening, fee, audienceCount)
}
}
데이터 중심 설계와 책임 중심 설계의 장단점을 비교해보기 전에 세 가지 품질 척도의 의미를 살펴보자.
객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다. 변경될 가능성이 높은 부분을 구현 이라고 부르고 상대적으로 안정적인 부분을 인터페이스 라고 부른다.
응집도 는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.
결합도 는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 객체지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.
일반적으로 좋은 설계란 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계를 의미한다.
변경의 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도 로 측정할 수 있다. 응집도가 높을수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.
결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도 로 측정할 수 있다. 결합도가 높으면 높을수록 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기가 어려워진다.
결합도가 높아도 상관 없는 경우도 있다. 일반적으로 변경될 확률이 매우 적은 안정적인 모듈에 의존하는 것은 아무런 문제가 되지 않는다. 표준 라이브러리에 포함된 모듈이냐 성숙 단계에 접어든 프레임워크에 의존하는 경우가 여기에 속한다.
데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현을 인터페이스의 일부로 만든다. 반면 책임 중심의 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화한다.
- 캡슐화 위반
- 높은 결합도
- 낮은 응집도
접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.
앨런 홀럽은 이처럼 접근자와 수정자에 과도하게 의존하는 설계 방식을 추측에 의한 설계 전략(design-by-guessing starategy) 이라고 부른다. 이 전략은 객체가 사용될 협력을 고려하지 않고 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다. 따라서 프로그래머는 내부 상태를 드러내는 메서드를 최대한 많이 추가해야 한다는 압박에 시달릴 수밖에 없으며 결과적으로 대부분의 내부 구현이 퍼블릭 인터페이스에 그대로 노출될 수밖에 없는 것이다. 그 결과, 캡슐화의 원칙을 위반하는 변경에 취약한 설계를 얻게 된다. - 114p
인터페이스라는 용어는 정말 interface 파일로 만들어진 것을 부르는게 아니라 클래스에 적혀있는 코드도 의미하는 것인가?
데이터 중심 설계는 객체의 캡슐화를 약화시키기 때문에 클라이언트가 객체의 구현에 강하게 결합된다.
또한 여러 데이터 객체들을 사용하는 제어 로직이 특정 객체 안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이다. 이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수 밖에 없다.
데이터 중심의 설계는 전체 시스템을 하나의 거대한 의존성 덩어리로 만들어 버리기 때문에 어떤 변경이라도 일단 발생하고 나면 시스템 전체가 요동칠 수밖에 없다.
- 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생할 수 있다.
- 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다.
어떤 요구사항 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거다.
단일 책임 원칙(Single Responsibility Principle, SRP)
클래스는 단 한 가지의 변경 이유만 가져야 한다.
- '코드 중복'이 발생할 확률이 높다
- '변경에 취약'하다.
'책임을 이동' 시킴으로써 문제를 해결한다. 객체가 자기 스스로를 책임지도록 설계.
객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.
TO-BE ) 첫 번째 설계보다 내부 구현을 더 면밀하게 캡슐화하고 있다. 또한 데이터를 처리하는 데 필요한 메서드를 데이터를 가지고 있는 객체 스스로 구현하고 있다.
두 번째 설계에서 발생하는 문제
접근자 메서드를 통해 내부 정보를 노출시키고 있다.
내부 구현의 변경이 외부로 퍼져나가는 파급 효과(ripple effect) 는 캡슐화가 부족하다는 명백한 증거다.
높은 결합도, 낮은 응집도 모두 캡슐화를 위반했기 때문에 발생한다. 데이터 중심의 설계가 가지는 문제점으로 인해 설계가 쉽게 개선되지 않는 모습을 확인할 수 있다.
- 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
- 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.
데이터는 구현의 일부이다. 데이터 주도 설계는 설계를 시작하는 처음부터 데이터에 관해 결정하도록 강요하기 때문에 너무 이른 시기에 내부 구현에 초점을 맞추게 한다.
데이터 중심 설계 방식에 익숙한 개발자들은 일반적으로 데이터와 가능을 분리하는 절차적 프로그래밍 방식을 따른다. 접근자와 수정자를 과도하게 추가하게 되는데 접근자와 수정자는 public 속성과 큰 차이가 없기 때문에 객체의 캡슐화는 완전히 무너질 수밖에 없다.
객체의 인터페이스는 구현을 캡슐화하는 데 실패하고 코드는 변경에 취약해진다.
결과적으로 데이터 중심의 설계는 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 된다.
객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수밖에 없다.
[위로](# 목차)
GRASP 패턴
- 2장 : 책임을 중심으로 설계된 객체지향 코드의 대략적인 모양
- 3장 : 역할, 책임, 협력이 객체지향적인 코드를 작성하기 위한 핵심이라는 사실
- 4장 : 역할, 책임, 협력이 아닌 데이터에 초점을 맞출 때 발생하는 문제점
데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해
- 데이터보다 행동을 먼저 결정하라
- 협력이라는 문맥 안에서 책임을 결정하라
객체지향에 갓 입문한 사람들이 가장 많이 저지르는 실수가 바로 객체의 행동이 아니라 데이터에 초점을 맞추는 것이다. 너무 이른 시기에 데이터에 초점을 맞추면 객체의 캡슐화가 약화되기 때문에 낮은 응집도와 높은 결합도를 가진 객체들로 넘쳐나게 된다.
책임 중심의 설계에서는 객체의 행동, 즉 책임을 먼저 결정한 후에 객체의 상태를 결정한다.
객체의 입장에서는 책임이 조금 어색해 보이더라도 협력에 적합하다면 그 책임은 좋은 것이다.
협력을 시작하는 주체는 메시지 전송자이기 때문에 협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다. 다시 말해서 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 하는 것이다.
메시지가 클라이언트의 의도를 표현한다는 사실에 주목하라. 그리고 메시지를 수신하기로 결정된 객체는 메시지를 처리할 '책임'을 할당 받게 된다.
결론적으로 책임 중심의 설계에서는 협력이라는 문맥 안에서 객체가 수행할 책임에 초점을 맞춘다.
저자가 살짝 눈치보였는지 3장에서의 내용을 다시 한번 말했다고 알려준다
책임 주도 설계의 흐름
- 시스템이 사용자아게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에게 책임을 할당함으로써 두 객체가 협력하게 한다.
대중적으로 가장 널리 알려진 책입 할당 기법은 크레이그 라만이 패턴 형식으로 제안한 GRASP 패턴 이다. GRASP은 "General Responsibility Assignment Software Pattern(일반적인 책임 할당을 위한 소프트웨어 패턴)"의 약자로 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것이다.
어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다.
설계를 시작하는 단계에서는 개념들의 의미와 관계가 정확하거나 완벽할 필요가 없다.
도메인 개념을 정리하는 데 너무 많은 시간을 들이지 말고 빠르게 설계와 구현을 진행하라.
첫 번째 질문, 메시지를 전송할 객체는 무엇을 원하는가?
=> 영화를 예매하는 것
두 번째 질문, 메시지를 수신할 적합한 객체는 누구인가?
=> 객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다. GRASP에서는 이를 INFORMATION EXPERT(정보 전문가) 패턴이라고 부른다.
INFORMATION EXPERT 패턴은 객체가 자신이 소유하고 있는 정보와 관련된 작업을 수행한다는 일반적인 직관을 표현한 것이다. => 정보와 데이터는 다르다. 객체가 정보를 알고 있다고 해서 그 정보를 저장할 필요는 없다.
- '예매하라' 메시지
- 예매할 객체, 'Screening'
- '가격을 계산하라' 메시지
- 계산할 객체, 'Movie'
- '할인 여부를 판단하라' 메시지
- 할인 여부를 판단할 'DiscountCondition' 인터페이스
Screening의 가장 중요한 책임은 예매를 생성하는 것이다. 만약 Screening이 DiscountCondition과 협력해야 한다면 Screening은 영화 요금 계산과 관련된 책임 일부를 떠안아야 한다. 다시 말해서 예매 요금을 계산하는 방식이 변경될 경우 Screening도 함께 변경해야 하는 것이다. 이는 응집도가 낮아지는 결과를 야기하게 된다.
영화 예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것이다.
CREATOR 패턴
객체 A를 생성해야 할 때 어떤 객체에게 객체 생성 책임을 할당해야 하는가? 아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임을 할당하라.
- B가 A 객체를 포함하거나 참조한다.
- B가 A 객체를 기록한다.
- B가 A 객체를 긴밀하게 사용한다.
- B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다(이 경우 B는 A에 대한 정보 전문가다)
- 새로운 할인 조건 추가
- 순번 조건을 판단하는 로직 변경
- 기간 조건을 판단하는 로직이 변경되는 경우
DiscountCondition의 가장 큰 문제는 순번 조건과 기간 조건이라는 두 개의 독립적인 타입이 하나의 클래스 안에 공존하고 있다는 점이다.
타입 분리하기: 두 타입을 두 개의 클래스로 분리하는 것 => but, Movie 클래스가 두 개의 클래스 모두에게 결합된다.
역할의 개념을 적용하면 Movie가 구체적인 클래스는 알지 못한 채 오직 역할에 대해서만 결합하도록 의존성을 제한할 수 있다.
객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당한다. GRASP에서는 이를 POLYMORPHISM(다형성) 패턴 이라고 부른다.
변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서는 PROTECTED VARIATIONS(변경 보호) 패턴이라고 부른다.
예측 가능한 변경으로 인해 클래스들이 불안정해진다면 PROTECTED VARIATIONS 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화하라.
금액 할인 정책 영화와 비율 할인 정책 영화라는 두 가지 타입을 하나의 클래스 안에 구현하고 있기 때문에 하나 이상의 이유로 변경될 수 있다.
설계를 주도하는 것은 변경이다. 개발자로서 변경에 대비할 수 있는 두 가지 방법이 있다.
- 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계하는 것
- 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것
요소들 사이의 의존성의 정도가 유연성의 정도를 결정한다. 유연성의 정도에 따라 결합도를 조절할 수 있는 능력은 객체지향 개발자가 갖춰야 하는 중요한 기술 중 하나다.
아무것도 없는 상태에서 책임과 협력에 관해 고민하기 보다는 일단 실행되는 코드를 얻고 난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시킨다.
긴 메서드는 다양한 측면에서 코드의 유지보수에 부정적인 영향을 미친다.
- 어떤 일을 수행하는지 한눈에 파악하기 어렵기 때문에 코드를 전체적으로 이해하는 데 너무 많은 시간이 걸린다.
- 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다.
- 메서드 내부의 일부 로직만 수정하더라도 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.
- 로직의 일부만 재사용하는 것이 불가능하다.
- 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는 것뿐이므로 코드 중복을 초래하기 쉽다.
책임 주도 설계 방법에 익숙하지 않다면 일단 데이터 중심으로 구현한 후 이를 리팩터링하더라도 유사한 결과를 얻을 수 있다. 처음부터 책임 주도 설계 방법을 따르는 것보다 동작하는 코드를 작성한 후에 리팩터링하는 것이 더 훌륭한 결과물을 낳을 수도 있다.
[위로](# 목차)
객체가 수신하는 메시지들이 객체의 퍼블릭 인터페이스를 구성한다. 훌륭한 퍼블릭 인터페이스를 얻기 위해서는 책임 주도 설계 방법을 따르는 것만으로는 부족하다. 유연하고 재사용 가능한 퍼블릭 인터페이스를 만드는 데 도움이 되는 설계 원칙과 기법을 익히고 적용해야 한다.
두 객체 사이의 협력 관계를 설명하기 위해 사용하는 전통적인 메타포는 클라이언트-서버(Client-Server) 모델 이다. 협력 안에서 메시지를 전송하는 객체를 클라이언트, 메시지를 수신하는 객체를 서버라고 부른다. 협력은 클라이언트가 서버의 서비스를 요청하는 단방향 상호작용이다.
대부분의 사람들은 객체가 수신하는 메시지의 집합에만 초점을 맞추지만 협력에 적합한 객체를 설계하기 위해서는 외부에 전송하는 메시지의 집합과 함께 고려하는 것이 바람직하다.
객체가 독립적으로 수행할 수 있는 것보다 더 큰 책임을 수행하기 위해서는 다른 객체와 협력해야 한다는 것이다.
메시지는 오퍼레이션명(operation name) 과 인자(argument) 로 구성되며 메시지 전송은 여기에 메시지 수진자 를 추가한 것이다.
기술적인 관점에서 객체 사이의 메시지 전송은 전통적인 방식의 함수 호출이나 프로시저 호출과는 다르다. 객체는 메시지와 메서드라는 두 가지 서로 다른 개념을 실행 시점에 연결해야 하기 때문에 컴파일 시점과 실행 시점의 의미가 달라질 수 있다.
메시지 수신자는 메세지를 처리하기 위해 필요한 메서드를 스스로 결정할 수 있는 자율권을 누린다.
실행 시점에 메시지와 메서드를 바인딩하는 메커니즘은 두 객체 사이의 결합도를 낮춤으로써 유연하고 확장 가능한 코드를 작성할 수 있게 만든다.
외부의 객체는 오직 객체가 공개하는 메시지를 통해서만 객체와 상호작용할 수 있다. 이처럼 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 퍼블릭 인터페이스 라고 부른다.
프로그래밍 언어의 관점에서 퍼블릭 인터페이스에 포함된 메시지를 오퍼레이션(operation) 이라고 부른다.
오퍼레이션(또는 메서드)의 이름과 파라미터 목록을 합쳐 시그니쳐(signature) 라고 부른다.
다형성의 축복을 받기 위해서는 하나의 오퍼레이션에 대해 다양한 메서드를 구현해야만 한다.
좋은 인터페이스는 최소한의 인터페이스 와 추상적인 인터페이스 라는 조건을 만족해야 한다.
- 디미터 법칙
- 묻지 말고 시켜라
- 의도를 드러내는 인터페이스
- 명령-쿼리 분리
협력하는 객체의 내부 구조에 대한 결합으로 인해 발생하는 설계 문제를 해결하기 위해 제안된 원칙이 디미터 법칙(Law of Demeter) 이다. 디미터 법칙을 간단하게 요약하면 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하라는 것이다.
[위로](# 목차)
실제로 문제를 해결하기 위해 사용하는 저장소는 장기 기억이 아니라 단기 기억이다. 문제를 해결하기 위해서는 필요한 정보들을 먼저 단기 기억 안으로 불러들여야 한다. 그러나 문제 해결에 필요한 요소의 수가 단기 기억의 용량을 초과하는 순간 문제 해결 능력은 급격하게 떨어지고 만다. 이런 현상을 인지 과부하(cognitive overload) 라고 부른다.
큰 문제를 해결 가능한 작은 문제로 나누는 작업을 분해(decomposition) 라고 부른다. 분해의 목적은 큰 문제를 인지 과부하의 부담 없이 단기 기억 안에서 한 번에 처리할 ㅅㅜ 있는 규모의 문제로 나누는 것이다.
급여 계산을 위한 모든 절차
직원의 급여를 계산한다
사용자로부터 소득세율을 입력받는다
"세율을 입력하세요: " 라는 문장을 화면에 출력한다
키보드를 통해 세율을 입력받는다
직원의 급여를 계산한다
전역 변수에 저장된 직원의 기본급 정보를 얻는다
급여를 계산한다
양식에 맞게 결과를 출력한다
"이름: {직원명}, 급여: {계산된 금액}" 형식에 따라 출력 문자열을 생성한다
기능 분해의 결과는 최상위 기능을 수행하는 데 필요한 절차들을 실행되는 시간 순서에 따라 나열한 것이다.
기능 분해 방법에서는 기능을 중심으로 필요한 데이터를 결정한다.
기능 분해를 위한 하향식 접근법은 먼저 필요한 기능을 생각하고 이 기능을 분해하고 정제하는 과정에서 필요한 데이터의 종류와 저장 방식을 식별한다.
하양식 기능 분해는 논리적이고 체계적인 시스템 개발 절차를 제시한다. 커다란 기능을 좀 더 작은 기능으로 단계적으로 정제해 가는 과정은 구조적이며 체계적인 동시에 이상적인 방법으로까지 보일 것이다.
실제로 발생하게 되는 다양한 문제들
- 시스템은 하나의 메인 함수로 구성돼 있지 않다.
- 기능 추가나 요구사항 변경으로 인해 메인 함수를 빈번하게 수정해야 한다.
- 비즈니스 로직이 사용자 인터페이스와 강하게 결합된다.
- 하향식 분해는 너무 이른 시기에 함수들의 실행 순서를 고정시키기 때문에 유연성과 재사용성이 저하된다.
- 데이터 형식이 변경될 경우 파급효과를 예측할 수 없다.
하양식 아이디어가 매력적인 이유는 설계가 어느 정도 안정화된 후에는 설계의 다양한 측면을 논리적으로 설명하고 문서화하기에 용이하기 때문이다. 그러나 설계를 문서화하는 데 적절한 방법이 좋은 구조를 설계할 수 있는 방법과 동일한 것은 아니다.
하양식 분해는 작은 프로그램과 개별 알고리즘을 위해서는 유용한 패러다임으로 남아 있다. 특히 프로그래밍 과정에서 이미 해결된 알고리즘을 문서화하고 서술하는 데는 훌륭한 기법이다.
시스템의 변경을 관리하는 기본적인 전략은 함께 변경되는 부분을 하나의 구현 단위로 묶고 퍼블릭 인터페이스를 통해서만 접근하도록 만드는 것이다. 즉, 기능을 기반으로 시스템을 분해하는 것이 아니라 변경의 방향에 맞춰 시스템을 분해하는 것이다.
- 복잡성: 모듈이 너무 복잡한 경우 이해하고 사용하기가 어렵다. 외부에 모듈을 추상화할 수 있는 간단한 인터페이스를 제공해서 모듈의 복잡도를 낮춘다.
- 변경 가능성: 변경 가능한 설계 결정이 외부에 노출될 경우 실제로 변경이 발생했을 때 파급효과가 커진다. 변경 발생 시 하나의 모듈만 수정하면 되도록 변경 가능한 설계 결정을 모듈 내부로 감추고 외부에는 쉽게 변경되지 않을 인터페이스를 제공한다.
자바에서 모듈의 개념은 패키지(package)를 이용해 구현 가능하다.
- 모듈 내부의 변수가 변경되더라도 모듈 내부에만 영향을 미친다
- 비즈니스 로직과 사용자 인터페이스에 대한 관심사를 분리한다
- 전역 변수와 전역 함수를 제거함으로써 네임스페이스 오염(namespace pollution)을 방지한다
모듈 내부는 높은 응집도를 유지하고, 모듈과 모듈 사이에는 낮은 결합도를 유지한다.
데이터를 중심으로 시스템을 분해하는 것이다. 모듈은 데이터와 함수가 통합된 한 차원 높은 추상화를 제공하는 설계 단위다.
프로그래밍 언어에서 타입(type) 이란 변수에 저장할 수 있는 내용물의 종류와 변수에 적용될 수 있는 연산의 가짓수를 의미한다.
추상 데이터 타입은 프로시저 추상화 대신 데이터를 추상화를 기반으로 소프트웨어를 개발하게 한 최초의 발걸음이다.
- 타입 정의를 선언할 수 있어야 한다.
- 타입의 인스턴스를 다루기 위해 사용할 수 있는 오퍼레이션의 집합을 정의할 수 있어야 한다.
- 제공된 오퍼레이션을 통해서만 조작할 수 있도록 데이터를 외부로부터 보호할 수 있어야 한다.
- 타입에 대해 여러 개의 인스턴스를 생성할 수 있어야 한다.
언어 차원에서 추상 데이터 타입을 지원하는 것과 관습과 약속, 기법을 통해 추상 데이터 타입을 모방하는 것은 완전히 다른 이야기다.
추상 데이터 타입과 클래스의 가장 핵심적인 차이는 클래스는 상속과 다형성을 지원하는 데 비해 추상 데이터 타입은 지원하지 못한다는 점이다. 상속과 다형성을 지원하는 객체지향 프로그래밍(Object-Oriented Programming) 과 구분하기 위해 상속과 다형성을 지원하지 않는 추상 데이터 타입 기반의 프로그래밍 패러다임을 객체기반 프로그래밍(Object-Based Programming) 이라고 부르기도 ㅎㅏㄴ다.
추상 데이터 타입은 타입을 추상화한 것이고 클래스는 절차를 추상화한 것이다.
추상 데이터 타입은 오퍼레이션을 기준으로 타입을 묶는 방법이고, 객체지향은 타입을 기준으로 오퍼레이션을 묶는다.
타입을 기준으로 절차를 추상화하지 않았다면 그것은 객체지향 분해가 아니다.
클래스가 추상 데이터 타입의 개념을 따르는지를 확인할 수 있는 가장 간단한 방법은 클래스 내부에 인스턴스의 타입을 표현하는 변수가 있는지를 살펴보는 것이다.
흔히 '객체지향이란 조건문을 제거하는 것' 이라는 다소 편협한 견해...
다시 한번 개방-폐쇄 원칙 의 중요성 상기...
새로운 타입을 빈번하게 추가해야 한다면 객체지향의 클래스 구조가 더 유용하다. 새로운 오퍼레이션을 빈번하게 추가해야 한다면 추상 데이터 타입을 선택하는 것이 현명한 판단이다.
객체지향에서 중요한 것은 역할, 책임, 협력이다.
7장은 객체에게 로직을 분해하는 방법에 있어서 추상 데이터 타입과 클래스의 차이를 보여주는 것이지 객체를 설계하는 방법을 설명한 것이 아니다.
[위로](# 목차)