본문 바로가기

기술서적 정리/오브젝트 - 코드로 이해하는 객체지향 설계

05 CHAPTER 책임 할당하기

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를

wikibook.co.kr

 

 

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

 

GRASP 패턴을 이해하고 나면 응집도와 결합도, 캡슐화 같은 다양한 기준에 따라 책임을 할당하고 결과를 트레이드오프할 수 있는 기준을 알 수 있다.




◈ 책임 주도 설계를 향해

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

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

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




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

객체는 협력에 참여하기 위해 존재하며 협력 안에서 수행하는 책임이 객체의 존재가치를 증명한다.

 

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

 

우리에게 필요한 것은 객체의 데이터에서 행동으로 무게 중심을 옮기기 위한 기법이다.

 

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




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

객체에게 할당된 책임이 ‘협력에 어울리지 않는다면’ 그 책임은 나쁜 것이다.

 

협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다.

다시 말해서 메시지를 전송하는 클라이언트 의도에 적합한 책임을 할당해야 한다.

 

메시지를 결정한 후에 객체를 선택해야 한다.

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

 

“이 클래스가 필요하다는 점은 알겠는데 이 클래스는 무엇을 해야 하지? 라고 질문하지 않고 “메시지를 전송해야 하는데 누구에게 전송해야 하지?” 라고 질문하는 것.

설계의 핵심 질문을 이렇게 바꾸는 것이 메시지 기반 설계로 향하는 첫걸음이다.

 

메시지를 전송하기 때문에 객체를 갖게 된 것이다.

 

메시지가 클라이언트 의도를 표현한다는 사실에 주목하라.

클라이언트는 어떤 객체가 메시지를 수신할지 알지 못한다.

클라이언트는 단지 임의의 객체가 메시지를 수신할 것이라는 사실을 믿고 자신의 의도를 표현한 메시지를 전송할 뿐이다.

그리고 메시지를 수신하기로 결정된 객체는 메시지를 처리할 ‘책임’을 할당 받게 된다.

 

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

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

 

올바른 객체지향 설계는 클라이언트가 전송할 메시지를 결정한 후에야 비로소 객체의 상태를 저장하는 데 필요한 내부 데이터에 관해 고민하기 시작한다.




책임 주도 설계

책임 주도 설계의 핵심은 1)책임을 결정한 후에 2)책임을 수행할 객체를 결정하는 것이다.

그리고 협력에 참여하는 객체들의 책임이 어느 정도 정리될 때까지는 객체의 내부 상태에 대해 관심을 가지지 않는 것이다.







 책임 할당을 위한 GRASP 패턴

도메인 개념에서 출발하기.

도메인 개념들을 책임 할당의 대상에서 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 수월해진다.

따라서 어떤 책임을 할당해야 할 때 가장 먼저 고민해야 하는 유력한 후보는 바로 도메인 개념이다.

 

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

단지 우리에게 출발점이 필요할 뿐이다.

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

도메인 개념을 정리하는 데 너무 많은 시간을 들이지 말고 빠르게 설계와 구현을 진행하라.




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

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

책임을 애플리케이션에 대해 전송된 메시지로 간주하고 이 메시지를 책임질 첫 번째 객체를 선택하는 것으로 설계를 시작한다.

 

메시지는 메시지를 수신할 객체가 아니라 메시지를 전송할 객체의 의도를 반영해서 결정해야 한다.

따라서 첫 번째 질문은 다음과 같다.

 

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

 

메시지를 결정했으므로 메시지에 적합한 객체를 선택해야 한다. 두 번째 질문은 다음과 같다.

 

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

 

이 질문에 답하기 위해서는 객체가 상태와 행동을 통합한 캡슐화의 단위라는 사실에 집중해야 한다.

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

GRASP에서는 정보 전문가 패턴이라고 부른다.

 

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

여기서 이야기하는 정보는 데이터와 다르다는 사실에 주의하라.

책임을 수행하는 객체가 정보를 ‘알고’ 있다고 해서 그 정보를 ‘저장’하고 있을 필요는 없다.

객체는. 1) 해당 정보를 제공할 수 있는 다른 객체를 알고 있거나. 2) 필요한 정보를 계산해서 제공할 수도 있다.

어떤 방식이건 “정보 전문가가 데이터를 반드시 저장하고 있을 필요는 없다는 사실을 이해하는 것이 중요”하다.

 

개략적인 수준에서 객체들의 책임을 결정하는 단계에서는 너무 세세한 부분까지 고민할 필요는 없다.

책임을 수행하는 데 필요한 작업을 구상해보고 스스로 처리할 수 없는 작업이 무엇인지를 가릴 정도의 수준이면 된다.

 

만약 스스로 처리할 수 없는 작업이 있다면 외부에 도움을 요청해야 한다.

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

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

 

정보 전문가 패턴은 객체란 상태와 행동을 함께 가지는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의 관점에서 표현한다.

정보 전문가 패턴을 따르는 것만으로도 자율성이 높은 객체들로 구성된 협력 공동체를 구축할 가능성이 높아지는 것이다.




높은 응집도와 낮은 결합도.

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

 

이미 속성으로 포함하여 결합하고 있는 클래스와 협력하면 설계 전체적으로 결합도를 추가하지 않고도 협력을 완성할 수 있다.




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

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

 

창조자 패턴.

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

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

 

 

1) 잘 알고있거나, 2) 긴밀하게 사용하거나, 3) 초기화에 필요한 데이터를 가지고 있는 객체생성자로 선택하는 것이 적절하다.





 구현을 통한 검증

변경에 취약한 클래스란 코드를 수정해야 하는 이유를 하나 이상 가지는 클래스다.

 

클래스가 하나 이상의 변경 이유를 가진다면 응집도가 낮다.

응집도가 낮다는 것은 서로 연관성이 없는 기능이나 데이터가 하나의 클래스 안에 뭉쳐져 있다는 것을 의미한다.

따라서 낮은 응집도가 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다.

 

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



코드를 통해 변경의 이유를 파악하는 방법.

1) 인스턴스 변수가 초기화되는 시점을 살펴보는 것.

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

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

 

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

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

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

 

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

메서드의 크기가 너무 커서 긴 코드 라인 속에 문제가 숨겨져 명확하게 보이지 않을 수도 있다.

이 경우 긴 메서드를 응집도 높은 작은 메서드로 잘게 분해해 나가면 숨겨져 있던 문제점이 명확하게 드러나는 경우가 많다.

 

다형성을 통해 분리하기.

역할을 대체할 클래스들 사이에서 구현을 공유해야 할 필요가 있다면 추상 클래스를 사용하면 된다.
구현을 공유할 필요 없이 역할을 대체하는 객체들의 책임만 정의하고 싶다면 인터페이스를 사용하면 된다.

 

객체의 암시적인 타입에 따라 행동을 분리해야 한다면 암시적인 타입을 명시적인 클래스로 정의하고 행동을 나눔으로써 응집도 문제를 해결할 수 있다.

GRASP에서는 이를 다형성 패턴이라고 한다.

 

다형성 패턴.

객체의 타입에 따라 변하는 로직이 있을 때 타입을 명시적으로 정의하고 각 타입에 다형적으로 행동하는 책임을 할당한다.

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

다형성 패턴은 객체의 타입을 검사해서 타입에 따라 여러 대안들을 수행하는 조건적인 논리를 사용하지 말라고 경고한다.
대신 다형성을 이용해 새로운 변화를 다루기 쉽게 확장하라고 권고한다.




변경으로부터 보호하기.

인터페이스를 실체화하는 클래스를 추가하는 것으로 책임의 종류를 확장할 수 있다.

 

이처럼 변경을 캡슐화하도록 책임을 할당하는 것은 GRASP에서는 변경 보호 패턴이라고 부른다.

 

변경 보호 패턴.

변화가 예상되는 불안한 지점들을 식별하고 그 주위에 안정된 인터페이스를 형성하도록 책임을 할당하라.
우리가 캡슐화해야하는 것은 변경이다.
변경이 될 가능성이 높은가? 그렇다면 캡슐화하라.

 



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

하나의 클래스가 여러 타입의 행동을 구현하고 있는 것처럼 보인다면 클래스를 분해하고 다형성 패턴에 따라 책임을 분산시켜라.

 

예측 가능한 변경으로 인해 여러 클래스들이 불안정해진다면 변경보호 패턴에 따라 안정적인 인터페이스 뒤로 변경을 캡슐화하라.

 

적절한 상황에서 두 패턴을 조합하면 코드 수정의 파급 효과를 조정할 수 있고 변경과 확장에 유연하게 대처할 수 있는 설계를 얻을 수 있을 것이다.

 

 

책임을 중심으로 협력을 설계할 때 얻을 수 있는 혜택.

 

1. 모든 클래스의 내부 구현이 캡슐화.

2. 클래스 변경의 이유를 오직 하나씩만 가진다.

3. 각 클래스는 응집도가 높고 다른 클래스와 최대한 느슨하게 결합.

4. 클래스는 작고 오직 한 가지 일만 수행한다.

5. 책임은 적절하게 분배.



결론은 데이터가 아닌 책임을 중심으로 설계하라는 것이다.

객체에게 중요한 것은 상태가 아니라 행동이다.

객체지향 설계의 기본은 책임과 협력에 초점을 맞추는 것이다.




변경과 유연성.

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

1) 코드를 이해하고 수정하기 쉽도록 최대한 단순하게 설계.

2) 코드를 수정하지 않고도 변경을 수용할 수 있도록 코드를 더 유연하게 만드는 것.

대부분의 경우에 전자가 더 좋은 방법이지만 유사한 변경이 반복적으로 발생하고 있다면 복잡성이 상승하더라도 유연성을 추가하는 두 번째 방법이 더 좋다.

 

할인 정책을 구현하기 위해 상속을 이용하고 있다면 실행 중에 영화 할인 정책을 변경하기 위해서는 새로운 인스턴스를 생성한 후 필요한 정보를 복사해야 한다.

 

새로운 할인 정책이 추가될 때마다 인스턴스를 생성하고, 상태를 복사하고, 식별자를 관리하는 코드를 추가하는 일은 번거로울뿐만 아니라 오류가 발생하기도 쉽다.

이 경우 코드의 복잡성이 높아지더라도 할인 정책의 변경을 쉽게 수용할 수 있게 코드를 유연하게 만드는 것이 더 좋은 방법이다.

 

해결 방법은 상속 대신 합성을 사용하는 것이다.

 

합성을 사용한 예제의 경우 새로운 할인 정책이 추가되더라도 할인 정책을 변경하는 데 필요한 추가적인 코드를 작성할 필요가 없다.

새로운 클래스를 추가하고 클래스의 인스턴스를 멤버 인스턴스의 특정 메서드에 전달하기만 하면 된다.

 

만약 여전히 책임을 할당하는 데 어려움을 느끼고 있다면 일단 절차형 코드로 실행되는 프로그램을 빠르게 작성한 후 완성된 코드를 객체지향적인 코드로 변경해보자.








 책임 주도 설계의 대안

책임과 객체 사이에서 방황할 때 돌파구를 찾기 위해 선택하는 방법은 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하는 것이다.

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

 

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




메서드 응집도

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

1. 코드를 전체적으로 이해하는 데 너무 많은 시간이 걸린다.

2. 변경이 필요할 때 수정해야 할 부분을 찾기 어렵다.

3. 메서드 내부의 일부 로직만 수정하더라도 메서드의 나머지 부분에서 버그가 발생할 확률이 높다.

4. 로직의 일부만 재사용하는 것이 불가능하다.

5. 코드를 재사용하는 유일한 방법은 원하는 코드를 복사해서 붙여넣는 것뿐이므로 코드 중복을 초래하기 쉽다.

 

한마디로 말해서 긴 메서드는 응집도가 낮기 때문에 이해하기도 어렵고 재사용하기도 어려우며 변경하기도 어렵다.

 

메서드가 명령문들의 그룹으로 구성되고, 각 그룹에 주석을 달아야 할 필요가 있다면, 그 메서드의 응집도는 낮은 것이다.

이럴 경우 주석을 추가하는 대신 메서드를 작게 분해해서 각 메서드의 응집도를 높여야한다.



클래스가 작고, 목적이 명확한 메서드들로 구성돼 있다면. 

1) 변경을 처리하기 위해 어떤 메서드를 수정해야 하는지를 쉽게 판단할 수 있다.

2) 재사용하기도 쉽다.

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

 

객체로 책임을 분배할 때 가장 먼저 할 일은 메서드를 응집도 있는 수준으로 분해하는 것이다.

긴 메서드를 작고 응집도 높은 메서드로 분리하면 각 메서드를 적절한 클래스로 이동하기가 더 수월해지기 때이다.

 

클래스는 오직 하나의 작업만 수행하고, 하나의 변경 이유만 가지는 작고, 명확하고, 응집도가 높은 메서드들로 구성돼 있어야 한다.

비록 클래스 길이가 더 길어졌다하더라도 일반적으로 명확성의 가치가 클래스의 길이보다 더 중요하다.

 

작고, 명확하며, 한 가지 일에 집중하는 응집도 높은 메서드는 변경 가능한 설계를 이끌어 내는 기반이 된다.

이런 메서드들이 하나의 변경 이유를 가지도록 개선될 때 결과적으로 응집도 높은 클래스가 만들어진다.

 

응집도를 높이기 위해서는 변경의 이유가 다른 메서드들을 적절한 위치로 분배해야 한다.

적절한 위치란 바로 각 메서드가 사용하는 데이터를 정의하고 있는 클래스를 의미한다.





객체를 자율적으로 만들자.

어떤 메서드를 어떤 클래스로 이동시켜야 할까?

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

따라서 메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키면 된다.

 

어떤 데이터를 사용하는 지를 가장 쉽게 알 수 있는 방법은 메서드 안에서 어떤 클래스의 접근자 메서드를 사용하는지 파악하는 것이다.

 

메서드가 사용하는 데이터를 저장하고 있는 클래스로 메서드를 이동시키면 인스턴스를 인자로 받을 필요가 없어진다.

이처럼 메서드를 다른 클래스로 이동시킬 때는 인자에 정의된 클래스 중 하나로 이동하는 경우가 일반적이다.

 

데이터를 사용하는 메서드를 데이터를 가진 클래스로 이동시키고 나면 캡슐화의 높은 응집도, 낮은 결합도를 가지는 설계를 얻게 된다.

 

메서드를 이동할 때 캡슐화, 응집도, 결합도의 측면에서 이동시킨 메서드의 적절성을 판단해야 한다.

어떤 메서드가 어떤 클래스에 위치해야 하는지에 대한 감을 잡아가면서 다양한 기능을 책임 주도 설계 방식에 따라 설계하고 구현해보는 연습을 해봐야한다.

 

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

처음부터 책임 주도 설계 방법을 따르는 것보다 동작하는 코드를 작성한 후에 리팩터링하는 것이 더 훌륭한 결과물을 낳을 수도 있다.

캡슐화, 결합도, 응집도을 이해하고 훌륭한 객체지향 원칙을 적용하기 위해 노력한다면 책임 주도 설계 방법을 단계적으로 따르지 않더라도 유연하고 깔끔한 코드를 얻을 수 있을 것이다.