Post

책임 할당하기

5. 책임 할당하기

책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 점

책임 할당 과정은 일종의 트레이드오프 활동

GRASP 패턴은 책임 할당의 어려움을 해결하기 위한 답을 제시해 줌

5.1 책임 주도 설계를 향해

데이터 중심의 설계에서 책임 중심의 설계로 전환하기 위해서는 다음의 두 가지 원칙을 따라야 한다.

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

두 원칙의 핵심은 설계를 진행하는 동안 데이터가 아니라 객체의 책임과 협력에 초점을 맞추라는 것

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

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동

데이터는 객체가 책임을 수행하는 데 필요한 재료를 제공할 뿐

너무 이른 시기에 데이터에 초점을 맞추면 객체의 캡슐화가 약화되기 때문에 낮은 응집도와 높은 결합도를 가진 객체들로 넘쳐나게 됨 -> 취약한 설계

“이 객체가 포함해야 하는 데이터가 무엇인가” -> “이 객체가 수행해야 하는 책임은 무엇인가”

“데이터를 처리하는 데 필요한 오퍼레이션은 무엇인가” -> “이 책임을 수행하는 데 필요한 데이터는 무엇인가”

객체지향 설계에서 가장 중요한 것은 적절한 객체에게 적절한 책임을 할당하는 능력

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

객체에게 할당된 책임의 품질은 협력에 적합한 정도로 결정됨

책임은 객체의 입장이 아니라 객체가 참여하는 협력에 적합해야 한다.

협력에 적합한 책임을 수확하기 위해서는 객체를 결정한 후에 메시지를 선택하는 것이 아니라 메시지를 결정한 후에 객체를 선택해야 한다.

메시지가 존재하기 때문에 그 메시지를 처리할 객체가 필요한 것이다.

메시지를 먼저 결정하기 때문에 메시지 송신자는 메시지 수신자에 대한 어떠한 가정도 할 수 없다.

메시지 전송자의 관점에서 메시지 수신자가 깔끔하게 캡슐화되는 것

협력이라는 문맥 안에서 메시지에 집중하는 책임 중심의 설계는 캡슐화의 원리를 지키기가 훨씬 쉬워진다.

책임 중심의 설계가 응집도가 높고 결합도가 낮으며 변경하기 쉽다고 말하는 이유

책임 중심 설계에서는 협력이라는 문맥 안에서 객체가 수행할 책임에 초점을 맞춘다.

책임 주도 설계

책임 주도 설계의 흐름

  • 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악
  • 시스템 책임을 더 작은 책임으로 분할
  • 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당
  • 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요한 경우 이를 책임질 적절한 객체 또는 역할을 찾음
  • 해당 객체 또는 역할에개 책임을 할당함으로써 두 객체가 협력하게 한다.

5.2 책임 할당을 위한 GRASP 패턴

책임 할당 기법 중 가장 널리 알려진 것 -> GRASP 패턴

GRASP(General Responsibility Assignment Software Pattern): 일반적인 책임 할당을 위한 소프트웨어 패턴

도메인 개념에서 출발하기

설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려보는 것이 유용함

도메인 안에는 무수히 많은 개념들이 존재하며 이 도메인 개념들을 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 수월해짐

설계를 시작하는 단계에서는 개념들의 의미와 관계가 정확하거나 완벽할 필요가 없다.

이 단계에서는 책임을 할당받을 객체들의 종류와 관계에 대한 유용한 정보를 얻을 수 있으면 충분

중요한 것은 설계를 시작하는 것이지 도메인 개념들을 완벽하게 정리하는 것이 아니다.

올바른 도메인 모델이란 존재하지 않는다

도메인 모델은 도메인을 개념적으로 표현한 것이지만 그 안에 포함된 개념과 관계는 구현의 기반이 되어야 한다.

도메인 모델이 구현을 염두에 두고 구조화되는 것이 바람직하다

필요한 것은 도메인을 그대로 투영한 모델이 아니라 구현에 도움이 되는 모델

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

책임 주도 설계 방식의 첫 단계는 애플리케이션이 제공해야 하는 기능을 애플리케이션의 책임으로 생각하는 것

  1. 메시지를 전송할 객체는 무엇을 원하는가?

    ex. 예매하라

  2. 메시지를 수신할 적합한 객체는 누구인가?

    객체에게 책임을 할당하는 첫 번째 원칙은 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것

    GRASP 에서는 이를 INFORMATION EXPERT(정보 전문가) 패턴

    INFORMATION EXPERT

    ​ 정보와 행동을 최대한 가까운 곳에 위치시키기 때문에 캡슐화를 유지할 수 있다.

    ​ 필요한 정보를 가진 객체들로 책임이 분산되기 때문에 더 응집력 있고, 이해하기 쉬워진다.

    ​ -> 높은 응집도, 낮은 결합도 -> 간결하고 유지보수하기 쉬운 시스템 구축

    INFORMATION EXPERT 패턴은 객체가 자신이 소유하고 있는 정보와 관련된 작업을 수행한다는 일반적인 직관을 표현

    여기서 정보는 데이터와 다름

    정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다

    ex. 상영

스스로 처리할 수 없는 작업이 있다면 다시 외부에 도움 요청

이 요청이 외부로 전송해야 하는 새로운 메시지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당됨

이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성됨

INFORMATION EXPERT 패턴은 객체에게 책임을 할당할 때 가장 기본이 되는 책임 할당의 원칙

높은 응집도와 낮은 결합도

높은 응집도와 낮은 결합도는 객체에 책임을 할당할 때 항상 고려해야 하는 기본 원리

책임을 할당할 수 있는 다양한 대안들이 존재한다면 응집도와 결합도의 측면에서 더 나은 대안을 선택하는 것이 좋다.

LOW COUPLING 패턴

설계의 전체적인 결합도가 낮게 유지되도록 책임을 할당하라

HIGH COHESION 패턴

높은 응집도를 유지할 수 있게 책임을 할당하라

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

협력에 참여하는 어떤 객체에게는 인스턴스를 생성할 책임을 항당해야 함

CREATOR(창조자) 패턴은 이 같은 경우에 사용할 수 있는 책임 할당 패턴으로서 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침을 제공

CREATOR 패턴

객체 A를 생성할 때, 어떤 객체에게 객체 생성 책임을 할당해야 하는가? 아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임 할당

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

CREATOR 패턴의 의도는 어떤 방식으로든 생성되는 객체와 연결되거나 관련될 필요가 있는 객체에 해당 객체를 생성할 책임을 맡기는 것

CREATOR 패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 설계가 낮은 결합도를 유지할 수 있게 함

협력과 책임이 제대로 동작하는지 확인할 수 있는 유일한 방법 -> 코드 작성/실행

5.3 구현을 통한 검증

구혀어어언~~ 했지만… 문제점 존재!!

DiscountCondition 개선하기

가장 큰 문제점은 변경에 취약한 클래스를 포함하고 있다는 것

변경의 이유가 3가지 -> 변경의 이유에 따라 클래스를 분리해야 함

일반적으로 설계를 개선하는 작업은 변경의 이유가 하나 이상인 클래스를 찾는 것으로부터 시작하는 것이 좋다.

코드를 통해 변경의 이유를 파악할 수 있는 방법

  1. 인스턴스 변수가 초기화되는 시점

    응집도가 높은 클래스는 인스턴스를 생성할 때 모든 속성을 함께 초기화

    응집도가 낮은 클래스는 객체의 속성 중 일부만 초기화하고 일부는 초기화되지 않은 상태로 남겨짐

    클래스의 속성이 서로 다른 시점에 초기화되거나 일부만 초기화된다는 것은 응집도가 낮다는 증거

    따러서 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.

  2. 메서드들이 인스턴스 변수를 사용하는 방식을 살펴보는 것

    모든 메서드가 객체의 모든 속성을 사용한다면 클래스의 응집도는 높다고 볼 수 있다.

    반면 메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 클래스의 응집도가 낮다고 볼 수 있다.

    클래스의 응집도를 높이기 위해서는 속성 그릅과 해당 그룹에 접근하는 메서드 그룹을 기준으로 코드를 분리

클래스 응집도 판단하기

  • 클래스가 하나 이상의 이유로 변경돼야 한다면 응집도가 낮은 것. 변경의 이유를 기준으로 클래스 분리 필요
  • 클래스의 인스턴스를 초기화하는 시점에 경우에 따라 서로 다른 속성들을 초기화하고 있다면 응집도가 낮은 것 -> 초기화되는 속성의 그룹을 기준으로 클래스를 분리
  • 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은 것 -> 이들 그룹을 기준으로 클래스를 분리

타입 분리하기

타입 분리를 통해 응집도 향상 -> 2개의 클래스와 결합되는 문제점 발생(결합도 상승) + 새로운 조건 추가하기 어려워짐

다형성을 통해 분리하기

역할을 사용하여 객체의 구체적인 타입 추상화

객체의 암시적인 타입에 따라 행동을 분기해야 한다면 암시적인 타입을 명시적인 클래스로 정의하고 행동을 나눔으로써 응집도 문제를 해결 가능 -> POLYMORPHISM(다형성) 패턴

POLYMORPHISM 패턴

타입을 명시적으로 정의하고 각 타입에 다형적으로 행동하는 책임을 할당하라

조건 대신 다형성을 이용하라고 권고!!

변경으로부터 보호하기

두 개의 서로 다른 변경이 두 개의 서로 다른 클래스 안으로 캡슐화

새로운 것이 추가되어도 보호됨 -> PROTECTED VARTATIONS 패턴

PROTECTED VARTATIONS 패턴

변화가 예상되는 불안정한 지점들을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당

설계에서 변하는 것이 무엇인지 고려하고 변하는 개념을 캡슐화하라

클래스를 변경에 따라 분리하고 인터페이스를 이용해 변경을 캡슐화하는 것은 설계의 결합도와 응집도를 향상시키는 매우 강력한 방법

Movie 클래스 개선

위의 방법과 동일

데이터가 아닌 책임을 중심으로 설계해야 한다.

도메인의 구조가 코드의 구조를 이끈다

변경과 유연성

설계를 주도하는 것은 변경

개발자로서 변경에 대비할 수 있는 두 가지 방법

  1. 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계
  2. 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것

상속 보다는 합성이 유연성 관점에서 좋다.

유연성은 의존성 관리의 문제 -> 요소들 사이의 의존성의 정도가 유연성의 정도를 결정

유연성의 정도에 따라 결합도를 조절할 수 있는 능력은 객체지향 개발자가 갖춰야 하는 중요한 기술 중 하나

코드의 구조가 도메인의 구조에 대한 새로운 통찰력을 제공

코드의 구조가 바뀌면 도메인에 대한 관점도 바뀐다.

도메인 모델은 코드에 대한 가이드를 제공할 수 있어야 하며 코드의 변화에 발맞춰 함께 변화해야 한다.

5.4 책임 주도 설계의 대안

아무것도 없는 상태에서 책임과 협력에 관해 고민하기 보다는 일단 실행되는 코드를 얻고 난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동 시키는 것

주의할 점은 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌어서는 안 된다는 것

캡슐화를 항상시키고, 응집도를 높이고, 결합도를 낮춰야 하지만 동작은 그대로 유지해야 함

-> 리팩터링(refactoring)

메서드 응집도

긴 메서드는 다양한 측면에서 코드의 유지보수에 부정적인 영향을 미친다.

  • 어떤 일을 수행하는지 한눈에 파악하기가 어렵기 때문에 코드를 전체적으로 이해하는데 너무 많은 시간이 걸린다.
  • 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다
  • 메서드 내부의 일부 로직만 수정하더라도 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.
  • 로직의 일부만 재사용하는 것이 불가능
  • 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는 것뿐이므로 코드 중복을 초래하기 쉽다.

-> 몬스터 메서드(monster method)

클래스의 응집도와 마찬가지로 메서드의 응집도를 높이는 이유도 변경과 관련이 깊다.

작은 메서드들로 조합된 메서드는 마치 주석들을 나열한 것처럼 보이기 때문에 코드를 이해하기 쉽다.

짧은 메서드가 좋은 이유

  • 메서드가 잘게 나눠져 있을 때 다른 메서드에서 사용될 확률이 높아짐
  • 고수준의 메서드를 볼 때 일련의 주석을 읽는 것 같은 느낌이 들게 할 수 있음
  • 오버라이딩하기 쉬움

코드를 작은 메서드들로 분해하면 전체적인 흐름을 이해하기도 쉬워진다.

변경하기도 쉬움

객체를 자율적으로 만들자

자신이 소유하고 있는 데이터를 자기 스스로 처리하도록 만드는 것이 자율적인 객체를 만드는 지름길

책임 주도 설계 방법에 익숙하지 않다면 일단 데이터를 중심으로 구현한 후 이를 리팩터링하고 유사한 결과를 얻을 수 있다.

참조

  1. 오브젝트
This post is licensed under CC BY 4.0 by the author.