[오브젝트] chap05을 읽고

4장에서 데이터 중심의 접근법을 취할 경우 직면하는 다양한 문제점들을 살펴봤다. 데이터 중심 설계로 인해 발생하는 문제점을 해결할 수 있는 가장 기본적인 방법은 데이터가 아닌 책임에 초점을 맞추는 것이다.

책임에 초점을 맞춰 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것이다. 책임 할당 과정은 일종의 트레이드오프 활동이다. 동일한 문제를 해결할 수 있는 다양한 책임 할당 방법이 존해하며, 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다. 따라서 올바른 책임을 할당하기 위해서는 다양한 관점에서 설계를 평가할 수 있어야 한다.

GRASP 패턴은 책임 할당의 어려움을 해결하기 위한 답을 제시해 준다. 응집도와 결합도, 캡슐화와 같은 다양한 기준에 따라 책임을 할당하고 결과를 트레이드오프할 수 있는 기준을 배울 수 있다.

책임 주도 설계를 향해

책임 중심의 설계로 나아가기 위해서는 다음의 두 가지 원칙을 따라야 한다.

  • 데이터보다 행동을 먼저 결정하라
  • 협력이라는 문맥 안에서 책임을 결정하라

위의 내용을 좀 더 자세히 알아보자.

데이터보다 행동을 먼저 결정하라

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다. 클라이언트 객체의 관점에서 객체가 수행하는 행동이란 곧 객체의 책임을 의미한다. 객체는 협력에 참여하기 위해 존재하며 협력 안에서 수행하는 책임이 객체의 존재가치를 결정한다. 데이터는 행동을 수행하기 위한 재료일 뿐이다.

4장에서도 살펴 보았듯이 데이터 중심 설계에서는 데이터를 먼저 고민하기 때문에 인터페이스에 구현, 즉 데이터가 새어나가게 되고 캡슐화가 위반된다. 따라서 시스템 관점에서의 결합도는 상승하고 응집도는 떨어지게 된다. 결국 변화에 유연한 시스템을 설계하기 위해서는 책임 주도 설계로 나아가야 한다.

데이터 중심 설계에서는 객체의 데이터를 먼저 고민하고 이 데이터를 처리하기 위한 오퍼레이션이 무엇인가에 대한 고민을 하기 시작한다. 하지만 책임 중심 설게에서는 객체가 수행하는 책임을 먼저 고민하고 후에 이를 위해 필요한 데이터가 무엇인지 고민한다.

결국 책임 중심 설계에서 중요한 것은 어떤 객체에게 어떤 책임을 할당할 것인가 이다. 이는 협력에서 답의 실마리를 찾을 수 있다.

협력이라는 문맥 안에서 책임을 결정하라

객체에게 할당된 책임이 협력에 어울리지 않는다면 그 책임은 나쁜 것이다. 객체의 입장에서는 책3임이 조금 어색해 보이더라고 협력에 적합하면 그 책임은 좋은 것이다. 즉, 책임은 객체의 입장이 아니라 협력에 적합해야 한다.

결국 협력을 시작하는 것은 메시지의 전송자이기 때문에 협력에 적합한 책임이란 메시지 전송자에게 적합한 책임을 의미한다. 즉, 클라이언트 객체의 의도에 적합한 책임을 할당해야 한다.

클라이언트는 임의의 객체가 메시지를 수신할 것이라고 믿고 자신의 의도를 표현한 메시지를 전송할 뿐이다. 그리고 메시지를 수신하기로 결정된 객체는 메시지를 처리할 책임을 할당받는다.

메시지를 먼저 결정하기 때문에 메시지 송신자는 메시지 수신자에 대해 어떤 가정도 할 수 없다. 메시지 전송자의 입장에서 메시지 수신자가 깔끔하게 캡슐화되는 것이다.

책임 할당을 위한 GRASP 패턴 1

GRASP 패턴은 크레이그 라만이 제안한 객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것이다.

설계 과정을 따라가보며 GRASP 패턴을 익혀보자.

도메인 개념에서 출발하기

설계 과정은 도메인 안에 존재하는 개념들을 정리하는 것으로 시작된다. 도메인 안에는 무수히 많은 개념들이 존재하며 이 도메인 개념들을 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 수월해진다. 따라서 어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인이다.

설계를 시작할 때, 개념들의 의미과 관계에 대해 정확하거나 완벽하게 알 필효가 없다. 단지 출발점으로서 존재할 뿐이다. 중요한 것은 설계를 시작하는 것이지 도메인 개념들을 완벽하게 정리하는 것이 아니다. 도메인 개념을 정리하는데 너무 많은 시간을 쏟지 말고 빠르게 설계와 구현을 진행하자.

올바른 도메인 모델은 존재하지 않는다.

다르게 표현되는 도메인 모델이 있다고 해서 그 도메인 모델이 틀린 것이 아니다. 하지만 염두해 둘 것은 도메인 모델이 구현을 결정한다는 것이다. 따라서 구현을 염두해 두고 도메인 모델을 짜야한다. 그리고 도메인 모델을 짯다고 해서 그 도메인 모델이 변하지 않는 것은 아니다. 구현에서 얻게 되는 통찰이 도메인을 바라보는 관점을 바꾸기 때문이다. 필요한 것은 도메인을 그대로 투영한 모델이 아니라 구현에 도움이 되는 모델이다. 다시 말해서 구현에 실용적이면서도 유용한 모델이 답이다.

정보 전문가에게 책임을 할당하라

책임 주도 설계 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각하는 것이다. 이 책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.

두 가지 질문으로 시작해야한다.

  • 메시지를 전송할 객체는 무엇을 원하는가?
  • 메시지를 수신할 적합한 객체는 누구인가?

첫 질문은 시스템의 책임으로 명확하므로 두번째 질문에 대한 답을 내려보자.

이 질문에 답하기 위해서는 객체가 상태와 행동을 통합한 캡슐화의 단위라는 사실에 집중해야한다. 즉, 객체의 책임과 책임을 수행하기 위한 데이터는 동일한 객체 안에 존재해야한다. 따라서 우리는 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것이다. 이를 정보 전문가(INFORMATION EXPERT) 패턴 이라고 부른다.

하지만 여기서 이야기 하는 정보는 데이터와는 다르다. 책임을 수행하는 객체가 정보를 ‘알고’있다고 해서 정보를 ‘저장’할 필요는 없다. 객체는 해당 정보를 제공할 수 있는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수 도 있다. 어떤 방식이건 정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다는 사실을 이해해야 한다.

먼저 늘어놓은 도메인 개념에서 메시지를 수신할 객체를 선택한다. 필요한 정보를 가장 많이 알고 있는 객체에게 책임을 할당해야 한다. 영화 예매 시스템을 예로 들면 상영, 예매, 영화, 할인 조건상영이 영화에 대한 정보와 상영 시간, 상영 순번처럼 영화 예매에 필요한 다양한 정보를 알고 있다. 따라서 상영이 ‘예매하라’라는 책임에 적합한 객체일 것이다.

지금은 개략적인 수준에서 객체들의 책임을 결정하는 단계이기 때문에 너무 세세한 부분까지 고민할 필요는 없다. 단지 책임을 수행하는데 필요한 작업을 구상해 보고 스스로 처리할 수 없는 작업이 무엇인지를 가릴 정도의 수준이면 된다.

만약 스스로 처리할 수 없는 작업이 있다면 외부에 도움을 요청해야 한다. 이 요청이 외부로 전송해야하는 새로문 메세지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당된다. 이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다.

이제 객체가 스스로 처리할 수 없는 작업에 대한 책임을 다른 객체에게 연쇄적으로 할당한다. 해당 책임을 수행할 수 있는 정보를 가진 정보 전문가 객체를 선택한다. 이 정보 전문가 패턴이 연쇄적으로 잘 일어난다면 자율성이 높은 객체들로 구성된 협력 공동체를 구축할 가능성이 높아진다.

높은 응집도와 낮은 결합도 패턴

설계는 트레이드오프 활동이다. 동일한 기능을 구현할 수 있는 무수히 많은 설계가 존재한다. 즉, 정보 전문가 패턴을 적용했다고 하더라도, 다른 패턴의 관점에서 고려할 필요가 있다.

GRASP에서는 낮은 결합도(LOW COUPLING) 패턴과 높은 응집도(HIGH COHESION) 패턴이 있다. 여러 협력 패턴이 있다면 높은 응집도와 낮은 결합도를 얻을 수 있는 설계를 선택해야 한다는 것이다.

위와 같은 두 협력 패턴이 후보로 있을 때 어떤 패턴을 사용해야 할까? 낮은 결합도 패턴 관점에서 보면 MovieDiscountCondition은 이미 결합되어 있기 때문에 MovieDiscountCondition을 협력하게 하면 설계 전체적으로 결합도늘 추가하지 않고 렵력을 완성할 수 있다. 하지만 두번째 패턴을 선택하면 ScreeningDiscountCondition 사이의 결합도가 추가되게 된다. 따라서 첫 번째 협력 패턴을 선택하는 것이 낮은 결합도 패턴 관점에서 더 낫다.

이번엔 높은 응집도 패턴 관점에서 위의 협력 패턴들을 살펴보자. Screening의 가장 중요한 책임은 예매를 생성하는 것이다. 만약 ScreeningDiscoundCondition과 협력해야 한다면 Screening영화 요금 계산과 관련된 책임 일부를 떠안아야 할 것이다. 이 경우 ScreeningDiscountCondition이 할인 여부를 판단할 수 있고 Movie가 이 할인 여부를 필요로 한다는 사실 역시 알고 있어야 한다.

즉, 요금 계산 방식이 변경될 경우 Screening도 함께 변경해야 할 필요성이 생길 수 있다. 결과적으로 ScreeningDiscountCondition과 협력하게 되면 Screening은 다른 이유로 변경되는 책임을 짊어지게 되므로 응집도가 낮아지게 된다.

결론은 이렇다. 정보 전문가 패턴으로 책임을 할당하되 높은 응집도 패턴과 낮은 결합도 패턴의 관점에서 전체적인 설계 품질을 검토하면 단순하면서도 재사용 가능하고 유연할 설계를 얻을 수 있다.

창조자에게 객체 생성 책임을 할당하라

GRASP의 창조자(CREATOR) 패턴은 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공한다.

객체 A를 생성해햐 할 때 아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임을 할당하라.

  • B가 A 객체를 포함하거나 참조한다.
  • B가 A 객체를 기록한다.
  • B가 A 객체를 긴밀하게 사용한다.
  • B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다.(이 경우 B는 A에 대한 정보 전문가다)

창조자 패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것이다. 생성될 객체에 대해 잘 알고 있어야 하거나 그 객체를 사용해야 하는 객체는 어떤 방식으로든 생성될 객체와 연결될 것이다. 다리 말해서 두 객체는 서로 결합된다.

이미 결합되어 있는 객체에게 생성 책임을 할당하는 것은 설계의 전체적인 결합도에 영향을 미치지 않는다. 결과적으로 창조자 패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 설계가 낮은 결합도를 유지할 수 있게 한다.

구현하기

책임 분배는 설계를 시작하기 위한 대략적인 스케치에 불과하다. 실제 설계는 코드를 작성하는 동안 이뤄진다. 그리고 협력과 책임이 재대로 동작하는지 확인할 수 있는 유일한 방법은 코드를 작성하고 실행해 보는 것뿐이다. 올바르게 설계하고 있는지 궁금하다면 코드를 작성하라.

이제 맡은 책임에 따라 책임을 수행하는 데 필요한 인스턴스 변수를 결정한다. 인스턴스 변수에는 책임을 수행하는 데에 필요한 데이터와 협력을 요청할 객체도 포함된다.

메시지 수신자가 메시지를 결정하라

다른 객체에 협력을 요청할 때, 메시지를 보낼 객체의 내부 구현을 고려하지 않고 메시지를 전송하라. 즉, 메시지를 전송할 객체에서 메시지의 시그니처를 먼저 결정하는 것이 유리하다. 그렇게 되면 메시지를 수신받는 객체의 내부 구현을 깔끔하게 캡슐화 할 수 있다.

메시지를 기반으로 협력을 구성하면 객체 사이의 결합도를 느슨하게 유지할 수 있다. 이처럼 메시지가 객체를 선택하도록 책임 주도 설계 방식을 따르면 캡슐화와 낮은 결합도라는 목표를 비교적 손쉽게 달성할 수 있다.

클래스 분리하기

코드를 작성하다 보면 변경에 취약한 클래스를 작성한다. 코드를 수정해야 하는 이유가 하나 이상일 때, 이를 변경에 취약한 클래스라고 한다.

하나 이상의 변경 이유를 가지면 응집도가 낮다는 의미이다. 즉, 서로 연관성이 없는 기능이거나 기능이나 데이터가 하나의 클래스 안에 뭉쳐져 있다는 것을 의미한다. 따라서 낮은 응집도가 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다.

코드를 짜면서 변경 이유를 분석해 보고 그것이 하나 이상이라면 설계를 개선해 나갈 수 있다. 문제는 변경의 이유를 찾는 것이 생각보다 어렵다는 것이다. 하지만 희망적인 것은 변경의 이유가 하나 이상인 큰래스에는 위험 징후를 또렷하게 드러내는 패턴이 존재한다는 점이다.

  • 인스턴수 변수가 초기화 되는 시점을 살펴보라
    • 해당 객체를 생성할 때, 어떤 인스턴스 변수는 초기화 하고 어떤 인스턴스 변수는 초기화 하고 있지 않다면 함께 초기화되는 속성을 기준으로 코드를 분리해야한다.
  • 메서드가 인스턴스 변수를 사용하는 방식을 살펴보라
    • 모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다.
    • 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있다.

패턴에 해당하지 않는다고 해서 변경의 이유가 하나인 것은 아니다. 따라서 다방면으로 코드를 살펴보고 코드를 분리할 수 있어야 응집도 높은 설계를 할 수 있다.

책임 할당을 위한 GRASP 패턴 2

다형성을 통해 분리하기

그렇다면 클래스 분리는 어떻게 적용하고 있을까? GRASP의 다형성(POLYMORPHISM) 패턴은 어느정도 답을 제공한다.

인스턴스 변수에 따라 클래스를 먼저 분리하자. type, sequence, dayOfWeek, startTime, endTime을 가지고 있던 DiscountCondition이라는 클래스를 상상해 보자.

  • 새로운 할인 조건 추가
  • 순번 조건을 판단하는 로직 변경
  • 기간 조건을 판단하는 로직 변경

위의 세가지 변경 이유를 가지고 있다.

이를 sequence를 가지고 있는 SequenceConditiondayOfWeek, startTime, endTime을 가지고 있는 PeriodCondition으로 분리할 수 있다.

하지만 여기서 DiscountCondition을 알고 있던 Movie클래스는 SequenceConditionPeriodCondition 둘을 모두 알아야 하는 결합도의 상승 문제가 발생한다. 하지만 이를 다형성을 통해 해결할 수 있다.

SequenceConditionDiscountCondition 모두를 자식으로 두는 DiscountCondition 인터페이스를 만드는 것이다. 그렇게 되면 Movie 클래스는 DiscountCondition 인터페이스만을 알고 DiscountCondition에 메시지를 전송하면 수신할 객체에 따라 적절한 메시지를 결정하게 될 것이다.

프로그램을 if ~ else 또는 switch ~ case 등의 조건 논리를 사용해 설계한다면 새로운 변화가 일어난 경우 이 조건 논리를 수정해야 한다. 이것은 프로그램을 수정하기 어렵고 변경에 취약하게 만든다.

객체의 타입에 따라 변하는 행동이 있다면 타입을 분리하고 변화하는 행동을 각 타입의 책임으로 할당한다. 이를 다형성(POLYMORPHISM) 패턴이라고 부른다.

하지만 타입을 분리하는것만이 능사는 아니다. 만약 다형성 패턴에 따라 타입을 분리했지만 프로그램 실행 중에 해당 타입을 다른 타입으로 변경해야 하는 요구사항이 추가되었다고 가정해보자. 현재의 설계에서는 상속을 사용하고 있기 때문에 실행중에 타입을 변경하려면 새로운 인스턴스를 생성하고 필요한 정보를 복사해야한다.

새로운 타입이 추가될 때 마다 상태를 복사하는 코드를 작성하는 것은 여간 번거로울 뿐만 아니라 오류가 발생하기도 쉽다. 이 경우에는 코드의 복잡성이 높아 지더라도 타입 변경을 쉽게 수용할 수 있게 코드를 유연하게 만드는 것이 더 좋은 방법이다.

해결 방법은 상속 대신 합성을 사용하는 것이다. 상속 계층 안에 구현된 하위 클래스의 속성을 독립적인 클래스로 분리하고 기존 클래스에 합성시키면 유연한 설계가 완성된다.

합성을 이용하면서 기존 도메인에 없던 새로운 객체가 생겨났다. 유연성을 위한 코드 개선이 도메인 구조에 영향을 미치고 새로운 통찰력을 제공한다.

변경으로부터 보호하기

위의 경우에 DiscountCondition의 두 서블래스는 서로 다른 이유로 변경된다는 사실을 알 수 있다. 두 개의 서로 다른 변경이 두 개의 서로 다른 클래스 안으로 캡슐화 된다.

새로운 할인 조건을 추가하는 경우에도 오직 DiscountCondition을 구체화하는 클래스를 추가함으로써 할인 조건의 종류를 확장할 수 있다. 이처럼 변경을 캡슐화하도록 책임을 할당하는 것을 GRASP에서는 변경 보호(PROTECTED VARIATIONS) 패턴이라고 부른다.

변경 보호 패턴은 책임 할당의 관점에서 캡슐화를 설명한 것이다. 설계에서 변하는 것이 무엇인지 고려하고 변하는 개녑을 캡슐화 하라.

결론은 이렇다. 하나의 클래스가 여러 타입의 행동을 구현하고 있는 것처럼 보인다면 클래스를 분해하고 다형성 패턴에 따라 책임을 분산시켜라. 예측 가능한 변경으로 인해 여러 클래스들이 불안정해진다면 변경 보호 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화하라.

책임 주도 설계로 시작하기 어렵다면

책임 주도 설계에 익숙해 지기 위해서는 부단한 노력과 시간이 필요하다. 저자가 추천하는 방법은 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하고, 코드 상에 명확하게 드러나는 책임들을 올바를 위치로 이동시키는 것이다.

주의할 점은 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌어서는 안 된다는 것이다. 캡슐화를 향상시키고, 응집도를 높이고, 결합도를 낮춰야 하지만 동작은 그대로 유지해야 한다. 이처럼 이해하기 쉽고 수정하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보이는 동작은 바꾸지 않은 채 내부 구조를 변경하는 것을 리팩터링(Refactoring)이라고 부른다.

리팩터링 과정을 알아보면서 이 과정의 장점을 알아보자.

너무 긴 메서드를 분해해 각 메서드의 응집도를 높이자. 메서드가 명령문의 그룹으로 구성되고 각 그룹에 주석을 달아야 할 필요가 있다면 그 메서드의 응집도는 낮을 것이다. 주석을 추가하는 대신 메스드를 작게 분해해서 각 메서드의 응집도를 높이자. 작은 메서드들로 조합된 메서스는 마치 주석들을 나열한 것처럼 보이기 때문에 코드를 이해하기도 쉽다.

하지만 메서드를 분리했다고 해서 해당 객체의 응집도가 높아지는 것은 아니다. 해당 메서드들을 적절한 위치로 분배해야 한다.

어떤 메서드를 어떤 클래스로 이동시킬지에 대한 힘트는 객체가 자율적인 존재여야 한다는 사실을 떠올리면 쉽게 답할 수 있다. 자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는 것이 자율적인 객체를 만드는 지름길이다. 따라서 메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키면 된다.

여기서 하고 싶은 말은 책임 주도 설계 방법에 익숙하지 않다면 일단 데이터 중심으로 구현하 후 이를 리팩터링하더라고 유사한 결과를 얻울 수 있다는 것이다. 캡슐화, 결합도, 응집도를 이해하고 훌륭한 객체지향 원칙을 적용하기 위해 노력한다면 책임 주도 설계 방법을 단게적으로 따르지 않더라도 유연하고 깔끔한 코드를 얻을 수 있다.

태그:

카테고리:

업데이트:

댓글남기기