5. 책임 할당하기
책임에 초점을 맞춰야 한다!!! 하지만 이때 우리가 마주하는 문제점은 어떤 객체에게 어떤 책임을 할당할 지 결정하기가 쉽지 않다는 것이다. 이번 장에선 GRASP패턴을 통해 응집도와 결합도, 캡슐화와 같은 다양한 기준에 따라 책임을 분배하고 결과를 트레이드오프할 수 있는 기준을 배운다.
00. 선요약
01. 책임 주도 설계를 향해
책임주도 설계를 위해 지켜야 할 두 가지 원칙
- 데이터보다 행동을 먼저 결정하라
- 협력이라는 문맥 안에서 책임을 결정하라
데이터보다 행동을 먼저 결정하라
앞서 데이터 중심 설걔를 했을 때 했던 질문의 순서를 바꿔야한다
- 이 객체가 수행해야 하는 책임은 무엇인가?
- 이 책임을 수행하는 데 필요한 데이터는 무엇인가?
즉 책임 중심의 설계에서는 객체의 행동(책임)을 먼저 결정한 후 객체의 상태를 결정한다. 그렇다면 대체 어떤 객체에게 어떤 책임을 할당해야 하는가?
협력이라는 문맥 안에서 책임을 결정하라
객체에게 적절한 책임을 할당하기 위해선 문맥을 고려해야 한다. 메시지를 전송하는 클라이언트의 의도에 적합한 책임을 할당해야 한다. 객체를 결정한 후 메시지를 선택하는 것이 아니라 메시지를 선택한 후 객체를 선택해야 한다. 클라이언트는 단지 임의의 객체가 메시지를 수신할 것을 믿고 자신의 의도를 표현한 메시지를 전송하고, 메시지를 수신하기로 결정된 객체는 메시지를 처리할 책임을 할당받게 된다.
책임 주도 설계
책임주도 설계 과정을 다시 알아보면 아래와 같다.
- 시스템이 사용자에게 제공해야 하는 기능인 시스템 책임을 파악한다.
- 시스템 책임을 더 작은 책임으로 분할한다.
- 분할된 책임을 수행할 수 있는 적절한 객체 또는 역할을 찾아 책임을 할당한다.
- 객체가 책임을 수행하는 도중 다른 객체의 도움이 필요할 경우 이를 책임 질 적절한 객체 또는 역할을 찾는다.
- 해당 객체 또는 역할에 책임을 할당함으로써 두 객체가 협력하게 한다.
02. 책임 할당을 위한 GRASP패턴
GRASP패턴은 "General Responsibility Asignment Software Pattern"의 약자로 객체에 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리해 놓은 것이다.
도메인 개념에서 출발하기
설계를 하기 전 도메인에 대한 개략적인 모습을 그려보는 것은 유용하다. 도메인 안에는 무수한 개념이 있고 이 도메인들을 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 쉬워진다.
설계 시작단계에선 개념들의 의미나 관계가 정확할 필요는 없다.
정보 전문가에게 책임을 할당하라
GRASP 패턴에선 이를 INFORMATION EXPERT 패턴이라 부른다.
지금까지 우리는 '객체는 자신의 상태를 스스로 처리하는 자율적인 존재'여야 한다고 배웠다. 책임을 할당하기 위해선 그 책임에 대해 가장 잘 알고 있는 객체를 선택하고 메시지를 처리할 책임을 할당하여야 한다.
다음 두 질문을 반복적으로 하며 메시지를 처리 할 객체를 찾는다.
- 메시지를 전송할 객체는 무엇을 원하는가? : 메시지 정하기
- 메시지를 수신할 적절한 객체는 무엇인가? : 메시지를 수신할 객체 정하기
높은 응집도와 낮은 결합도
설계를 하다 보면 몇가지 설계 중에서 한가지를 선택해야 하는 경우가 생긴다. 이떄 고려할 수 있는 원칙 중 LOW COUPLING(낮은 결합도) 패턴과 HIGH COHESION(높은 응집도) 패턴이 있다.
LOW COUPLING
설계의 전체적인 결합도를 낮게 유지하도록 책임을 할당하면 의존성을 낮추고 변화의 영향을 줄이며 재사용성을 증가시킬 수 있다.
현재의 책임 할당을 검토하거나 여러 설계 대안이 있을 때 낮은 결합도를 유지할 수 있는 설계를 선택해야 한다.
HIGH COHESION
높은 응집도를 유지할 수 있게 책임을 할당하면 복잡성을 관리할 수 있는 수준으로 유지할 수 있다.
낮은 결합도와 마찬가지로 높은 응집도는 모든 설계 과정에서 염두에 둬야 할 원리다. 현재의 책임 할당을 검토하거나 여러 설계 대안이 있을 때 높은 응집도를 유지할 수 있는 설계를 선택해야 한다.
창조자에게 객체 생성 책임을 할당하라
GRASP에는 CREATOR 패턴이 있는데 이는 객체를 생성할 책임을 어떤 객체에 할당할지에 관한 규칙이다.
03. 구현을 통한 검증
지금까지 설계한 것들을 토대로 구현을 해보고 설계를 검증, 부족한 부분에 대한 리팩토링을 진행한다.
변경에 취약한 클래스 개선하기
변경에 취약한 클래스란 코드를 수정해야 하는 이유를 하나 이상 가지는 클래스다. 클래스가 변경에 취약한지 여부를 알려주는 몇 가지 패턴이 있다.
클래스가 하나 이상의 변경 이유를 가진다
클래스가 하나 이상의 변경 이유를 가진다는 것은 응집도가 낮아 서로 연관없는 기능이다 데이터가 클래스 안에 뭉쳐져 있다는 것을 의미한다. 따라서 변경의 이유에 따라 클래스를 분리해줘서 응집도를 낮춰줘야 한다.
인스턴스 변수가 초기화 되는 시점
클래스의 속성이 서로 다른 시점에 초기화 되거나 일부만 초기화 된다는 것은 응집도가 낮다는 증거이므로 함께 초기화되는 속성을 기준으로 코드를 분리해야 한다.
메서드가 인스턴스를 사용 하는 방식
메서드들이 사용하는 속성에 따라 그룹이 나뉜다면 응집도가 낮다고 볼 수 있다. 이땐 속성 그룹과 해당 그룹에 접근하는 메서드를 기준으로 코드를 분리해야 한다.
타입 분리하기
두 개의 독립적인 타입이 하나의 클래스에 있으면 이땐 각 타입을 개별 클래스로 분리시켜 줘야 한다.
이렇게 하면 수정한 객체들의 응집도가 높아졌다는 장점이 있지만 전체적인 결합도가 높아지거나 변경과 캡슐화라는 관점에서 전체적인 설계의 품질이 나빠지는 문제점을 야기한다.
다형성을 통해 분리하기
결합도가 높아지는 문제점이 있다면 역할 개념을 적용해 볼만 하다. 역할을 사용해 객체의 구체적 타입을 추상화 할 수 있다. 자바는 인터페이스나 추상클래스를 사용한다.
변경으로부터 보호하기
Protected Variation 패턴 이라고 한다. 변화가 예상되는 불안한 지점들을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당하는 방식이다. 이 패턴은 책임할당의 관점에서 캡술화를 설명한 것이다.
클래스 개선하기
클래스를 변경에 따라 분리하고 인터페이스를 이용해 변경을 캡슐화 하는 것은 설계의 결합도와 응집도를 향상시키는 매우 강력한 방법이다.
일단, 하나의 클래스가 여러 타입의 행동을 구현하는 것처럼 보이면 클래스를 분리하고 다형성 패턴에 따라 책임을 분산시킨다. 그리고 예측가능한 변경 때문에 여러 클래스들이 불안해진다면 Protected Variation 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화 하라.
변경과 유연성
4장에서도 본 것 같은데... 설계는 변경을 위해 존재한다! 우리가 변경에 대비할 수 있는 두 가지 방법이 있다. 하나는 코드를 이해하고 수정하기 쉽게 최대한 단순하게 설계하는 것이다. 다른 하나는 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것이다. 대부분의 경우 전자가 더 좋은 방법이지만 유사한 변경이 잦다면 복잡성이 상승하더라도 유연성을 추가하는 두 번째 방법이 더 좋다.
예를 들어 어떤 정책을 구현할 때 상속을 사용했다면 그 기능을 합성을 사용하도록 변경하는 것이다. 상속 대신 합성을 사용함으로서 새로운 정책이 추가되더라도 변경을 위해 추가적인 코드를 작성할 필요가 없다.
04. 책임 주도 설계의 대안
특히 초보자의 경우 책임 주도 설계는 어렵다. 책임과 객체 사이에서 방황하고 있을 때 취할 수 있는 방법 중 하나는 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하는 것이다. 일단 실행되는 코드를 작성 후에 코드상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키면 된다.
주의할 점은 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌면 안된다. 캡슐화를 향상시키고 응집도를 높이고 결합도를 낮추는 동시에 동작은 그대로 유지시켜야 한다는 것이다. 이러한 행위를 리팩터링이라 한다.
메서드 응집도
메서드의 길이가 너무 길면 이해하기가 어렵고 다양한 측면에서 코드 유지 보수에 부정적인 영향을 미친다.
- 어떤 일을 수행하는 지 한 눈에 파악하기 어렵기 때문에 코드를 전체적으로 이해하는 데 너무 많은 시간이 걸린다.
- 하나의 메서드 안에서 너무 많은 작업을 처리하기 때문에 변경이 필요할 때 수정할 부분을 찾기 어렵다.
- 메서드 내부의 일부 로직만 수정하더라도 매서드의 나머지 부분에서 버그가 발생할 확률이 높다.
- 로직의 일부분만 재사용하는 것이 불가능하다.
- 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는 것뿐이므로 중복을 초래하기 쉽다.
한마디로 말해서 긴 메서드는 응집도가 낮기 때문에 이해하기도 어렵고 재사용하기도 어려우며 변경하기도 어렵다.
이런 경우 메서드를 작게 분리하여 응집도를 높여야 한다. 클래스의 응집도와 마찬가지로 메서드의 응집도를 높이는 이유도 변경과 관련이 높다. 응집도가 높은 메소드는 변경 이유가 하나여야 한다. 또한 메서드의 크기가 작고 목적이 분명하기 때문에 재사용하기 쉽다. 작은 메서드들로 열거된 메서드는 마치 주석들을 나열한 것 처럼 보이기 때문에 코드를 이해하기도 쉽다.
메서드를 작게 분리하고 난 뒤 변경이 다른 메서드들을 적절한 위치(메서드가 사용하는 데이터를 가지고 있는 곳)로 이동시켜 준다.
객체를 자율적으로 만들자
자신이 소유하고 있는 데이터를 자기 스스로 처리하게 만드는 것이 자율적인 객체를 만드는 지름길이다.