본문 바로가기

study/CS

[CS/책] 「오브젝트」5장 - 책임 할당하기

책 「오브젝트」 5장 책임 할당하기를 읽고 작성한 내용입니다.

 


데이터 중심 설계로 인해 발생하는 문제점을 해결할 수 있는 가장 기본적인 방법은 데이터가 아닌 책임에 초점을 맞추는 것이다.
책임에 초점을 맞춰서 설계할 때 직면하는 가장 큰 어려움은 어떤 객체에게 어떤 책임을 할당할지를 결정하기가 쉽지 않다는 것이다. 책임 할당 과정은 일종의 트레이드오프 활동이다. 동일한 문제를 해결할 수 있는 다양한 책임 할당 방법이 존재하며, 어떤 방법이 최선인지는 상황과 문맥에 따라 달라진다. 따라서 올바른 책임을 할당하기 위해서는 다양한 관점에서 설계를 평가할 수 있어야 한다.
실제 설계는 코드를 작성하는 동안 이뤄진다.
그리고 협력과 책임이 제대로 동작하는지 확인할 수 있는 유일한 방법은
코드를 작성하고 실행해 보는 것뿐이다.
올바르게 설계하고 있는지 궁금한가? 코드를 작성하라.

책임 주도 설계를 향해

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

객체에게 중요한 것은 데이터가 아니라 외부에 제공하는 행동이다.
데이터는 객체가 책임을 수행하는 데 필요한 재료를 제공할 뿐이다.

 

객체의 데이터에서 행동으로 무게 중심을 옮기기!

 

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

“메시지를 전송해야 하는데 누구에게 전송해야 하지?”라고 질문하는 것. 설계의 핵심 질문을 이렇게 바꾸는 것이 메시지 기반 설계로 향하는 첫걸음이다. 객체를 가지고 있기 때문에 메시지를 보내는 것이 아니다. 메시지를 전송하기 때문에 객체를 갖게 된 것이다[Metz12].
협력이라는 문맥에서 적절한 책임이란 곧 클라이언트의 관점에서 적절한 책임을 의미한다. 올바른 객체지향 설계는 클라이언트가 전송할 메시지를 결정한 후에야 비로소 객체의 상태를 저장하는 데 필요한 내부 데이터에 관해 고민하기 시작한다.

 

협력에 적합한 책임이란 메시지 수신자가 아니라 메시지 전송자에게 적합한 책임을 의미한다.항상 메시지를 받는 수신자의 입장에서 생각했는데, 결국 메시지 수신자가 메시지를 받는 이유는 메시지 전송자가 있기 때문이라고 생각하니 이런 관점이 이해가 되었다. 누군가 그 메시지를 보내지 않으면 구현될 필요가 없다. 누가 어떤 메시지를 전송했는지에 초점을 맞춰 책임을 결정해야겠다.

 

책임 할당을 위한 GRASP 패턴

GRASP 패턴

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

객체에게 책임을 할당할 때 지침으로 삼을 수 있는 원칙들의 집합을 패턴 형식으로 정리한 것

설계는 트레이드오프 활동이다. 실제로 설계를 진행하다 보면 몇 가지 설계 중에서 한 가지를 선택해야 하는 경우가 빈번하게 발생한다. 이 경우에는 올바른 책임 할당을 위해 INFORMATION EXPERT 패턴 이외의 다른 책임 할당 패턴들을 함께 고려할 필요가 있다.

INFORMATION EXPERT(정보 전문가) 패턴

: 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하는 것

  • 객체에게 책임을 할당할 때 가장 기본이 되는 책임 할당 원칙
  • 객체란 상태와 행동을 함께 가지는 단위라는 객체지향의 가장 기본적인 원리를 책임 할당의 관점에서 표현한다.
여기서 이야기하는 정보는 데이터와 다르다는 사실에 주의하라.

책임을 수행하는 객체가 정보를 ‘알고’ 있다고 해서 그 정보를 ‘저장’하고 있을 필요는 없다.
객체는 해당 정보를 제공할 수 있는 다른 객체를 알고 있거나 필요한 정보를 계산해서 제공할 수도 있다.
만약 스스로 처리할 수 없는 작업이 있다면 외부에 요청해야 한다. 이 요청이 외부로 전송해야 하는 새로운 메시지가 되고, 최종적으로 이 메시지가 새로운 객체의 책임으로 할당된다. 이 같은 연쇄적인 메시지 전송과 수신을 통해 협력 공동체가 구성되는 것이다.

 

LOW COUPLING (낮은 결합도) 패턴과 HIGH COHESION (높은 응집도) 패턴

  • 설계를 진행하면서 책임과 협력의 품질을 검토하는 데 사용할 수 있는 중요한 평가 기준
  • 낮은 결합도 / 높은 응집도는 모든 설계 결정에서 염두에 둬야 하는 원리, 다시 말해 설계 결정을 평가할 때 적용할 수 있는 평가원리
  • 현재의 책임 할당을 검토하거나 여러 설계 대안들이 있을 때 낮은 결합도와 높은 결합도를 유지할 수 있는 설계를 선택하라

책에서는 특정 클래스의 개선을 통해 응집도를 높인다. 일단 특정 클래스가 응집도가 낮은지 높은지는 어떻게 판단할까?

하나 이상의 변경 이유를 가지기 때문에 응집도가 낮다. 응집도가 낮다는 것은 서로 연관성이 없는 기능이나 데이터가 하나의 클래스 안에 뭉쳐져 있다는 것을 의미한다. 따라서 낮은 응집도가 초래하는 문제를 해결하기 위해서는 변경의 이유에 따라 클래스를 분리해야 한다.
  • 클래스가 하나 이상의 이유로 변경돼야 한다면 응집도가 낮은 것이다. 변경의 이유를 기준으로 클래스를 분리하라.
  • 클래스의 인스턴스를 초기화하는 시점에 경우에 따라 서로 다른 속성들을 초기화하고 있다면 응집도가 낮은 것이 다. 초기화되는 속성의 그룹을 기준으로 클래스를 분리하라.
  • 메서드 그룹이 속성 그룹을 사용하는지 여부로 나뉜다면 응집도가 낮은 것이다. 이들 그룹을 기준으로 클래스를 분리하라.

 

이 부분을 읽고 현재 설계하고 있는 클래스의 코드를 분리했다.

내가 만든 클래스는 게임 세션을 관리하는 역할을 담당한다. 이때, 1vs1 대결과 토너먼트 대결을 모두 함께 관리하고 있었다. 

이렇게 구현되었더니, 대진표에 관련된 변수들은 토너먼트에서만 사용되고 1 vs1에서는 사용되지 않았다. 대진표와 관련된 메서드도 마찬가지였다.

따라서 이를 토너먼트 / 1 vs1을 관리하도록 분리하고, 대진표도 새로운 클래스로 만들어 관리할 수 있도록 하였다.

이 책에서 짚어준 부분을 바탕으로 코드를 개선하는 경험을 할 수 있었다.

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

CREATER (창조자) 패턴

  • 객체를 생성할 책임을 어떤 객체에게 할당할지에 대한 지침 제공
  • 객체 A를 생성해야 할 때, 아래 조건을 최대한 많이 만족하는 B에게 객체 생성 책임을 할당하라
    • B가 A 객체를 포함하거나 참조한다.
    • B가 A 객체를 기록한다.
    • B가 A 객체를 초기화하는 데 필요한 데이터를 가지고 있다(이 경우 B는 A에 대한 정보 전문가다)
이미 결합돼 있는 객체에게 생성 책임을 할당하는 것은 설계의 전체적인 결합도에 영향을 미치지 않는다. 결과적으로 CREATER 패턴은 이미 존재하는 객체 사이의 관계를 이용하기 때문에 설계가 낮은 결합도를 유지할 수 있게 한다.

 

POLYMORPHISM(다형성) 패턴

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

PROTECTED VARIATIONS(변경 보호) 패턴

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

도메인 개념에서 출발하기

설계를 시작하기 전에 도메인에 대한 개략적인 모습을 그려 보는 것이 유용하다. 도메인 안에는 무수히 많은 개념들이 존재하며 이 도메인 개념들은 책임 할당의 대상으로 사용하면 코드에 도메인의 모습을 투영하기가 좀 더 수월해진다.
도메인 모델은 단순히 설계에 필요한 용어를 제공하는 것을 넘어 코드의 구조에도 영향을 미친다.

도메인 모델에는 도메인 안에서 변하는 개념과 이들 사이의 관계가 투영돼 있어야 한다.

구현을 가이드할 수 있는 도메인 모델을 선택하라.

객체지향은 도메인의 개념과 구조를 반영한 코드를 가능하게 만들기 때문에 도메인의 구조가 코드의 구조를 이끌어 내는 것은 자연스러울뿐만 아니라 바람직한 것이다.
코드의 구조가 바뀌면 도메인에 대한 관점도 바뀐다.

도메인 모델은 단순히 도메인의 개념과 관계를 모아 놓은 것이 아니다. 도메인 모델은 구현과 밀접환 관계를 맺어야 한다.

 

올바른 도메인이란 존재하지 않지만, 설계를 위한 출발점으로 길잡이가 되어줄 수 있는 것 같다.

책임 주도 설계의 대안: 리팩터링

개인적으로 책임과 객체 사이에서 방황할 때 돌파구를 찾기 위해 선택하는 방법은 최대한 빠르게 목적한 기능을 수행하는 코드를 작성하는 것이다. 아무것도 없는 상태에서 책임과 협력에 관해 고민하기보다는 일단 실행되는 코드를 얻고 난 후에 코드 상에 명확하게 드러나는 책임들을 올바른 위치로 이동시키는 것이다.
주의할 점은 코드를 수정한 후에 겉으로 드러나는 동작이 바뀌어서는 안 된다는 것이다. 캡슐화를 향상시키고, 응집도를 높이고, 결합도를 낮춰야 하지만 동작은 그대로 유지해야 한다. 이처럼 이해하기 쉽고 수정하기 쉬운 소프트웨어로 개선하기 위해 겉으로 보이는 동직은 바꾸지 않은 채 내부 구조를 변경하는 것을 리팩터링(Refactoring)이라고 부른다[Fowler99a].

메서드 응집도를 높이자

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

한마디로 말해서 긴 메서드는 응집도가 낮기 때문에 이해하기도 어렵고 재사용하기도 어려우며 변경하기도 어렵다. 마이클 페더스(Michael Feathers)는 이런 메서드를 몬스터 메서드(monster method)[Feathers04]라고 부른다.
비록 클래스의 길이는 더 길어졌지만 일반적으로 명확성의 가치가 클래스의 길이보다 더 중요하다.

작고, 명확하며, 한 가지 일에 집중하는 응집도 높은 메서드는 변경 가능한 설계를 이끌어 내는 기반이 된다. 이런 메서드들이 하나의 변경 이유를 가지도록 개선될 때 결과적으로 응집도 높은 클래스가 만들어진다.

 

코드를 작성할 때 응집도 높은 메서드를 작성하기 위해 노력하는 편이다.

42서울에서는 C 코드를 작성할 때 lint와 같은 norminette이 있다. 항상 함수 내부 줄 길이는 25줄 이내, 한 줄에 80자를 넘으면 안 됐었다. 처음에 코드를 작성할 때는 왜 이런 규칙에 맞추면 작성해야 할까?라고 생각했는데, 점점 코드 양이 늘어나고 어쩔 수 없이 함수를 쪼개면서 알게 모르게 메서드 응집도를 높이는 코드를 작성하게 되었다. 확실히 함수명을 통해 의미하는 바가 보이면 가독성이 좋아지고 평가 때 설명이 조금 더 편해진다. 

객체를 자율적으로 만들자

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

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

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

요약해 보자면

  • 협력에 적합한 책임이란 메시지 전송자에게 적합한 책임을 의미한다.
  • 책임을 수행할 정보를 알고 있는 객체에게 책임을 할당하기
  • 설계를 진행하면서 책임과 협력의 품질을 검토할 때 결합도와 응집도를 확인해 보기
  • 이미 결합돼 있는 객체에게 생성 책임을 할당하도록 하기
  • 설계를 주도하는 것은 변경이다.
  • 변경이 필요한 코드가 있는지 확인하기
  • 책임 주도 설계에 익숙하지 않다면 리팩터링을 통해 유사한 결과를 얻도록 노력하기