본문 바로가기

study/CS

[CS/책] 「오브젝트」4장 - 설계 품질과 트레이드오프

책 「오브젝트」 4장 설계 품질과 트레이드오프를 읽고 작성한 내용입니다.

 


객체지향 설계란 올바른 객체에게 올바른 책임을 할당하면서 낮은 결합도와 높은 응집도를 가진 구조를 창조하는 활동이다[Evers09].
훌륭한 설계란 합리적인 비용 안에서 변경을 수용할 수 있는 구조를 만드는 것이다.

 

요즘 프로젝트를 진행하면서 설계는 매우 어렵다는 것을 깨닫고 있다.

4장을 읽으며 객체지향 설계를 데이터 중심으로 잘못하고 있다는 것을 한번 더 깨달았고, 의욕 넘치게 리팩토링을 시도해 보았으나.. 

생각처럼 쉽지 않았다. 그래도 이번 기회에 문제라는 것을 상기하고 갈 수 있어서 좋았다.

 

데이터 중심 관점

객체의 상태는 구현에 속한다. 구현은 불안정하기 때문에 변하기 쉽다. 상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다. 결과적으로 상태 변경은 인터페이스 변경을 초래하며 이 인터페이스에 의존하는 모든 객체에게 변경의 영향이 퍼지게 된다. 따라서 데이터에 초점을 맞추는 설계는 변경에 취약할 수밖에 없다.

 

객체지향 설계에서 시스템을 객체로 분할하는 두 가지 방법 중 하나는 데이터 중심 관점이다.

데이터 중심의 설계란 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할하는 방법이다.

 

현재 내가 구현 중인 Pong 게임에 빗대어 생각해 보면 공과 패들에 들어갈 수 있는 데이터를 행동보다 먼저 생각하면서 설계를 했다.

공은 좌표가 필요하고 공이 움직이려면 스피드, dx, dy, 기타 등등..

아 그러면 움직이는 함수가 필요하지. 이것도 필요하고 저것도 필요하고..

이렇게 공이라는 클래스 내부에 어떤 데이터가 필요한지를 먼저 생각하고 각 클래스를 구현했다.

 

객체의 책임을 결정하기 전에 이런 질문의 반복에 휩쓸려 있다면 데이터 중심의 설계에 매몰돼 있을 확률이 높다. 특히 객체의 종류를 저장하는 인스턴스 변수와 인스턴수 종류에 따라 배타적으로 사용될 인스턴스 변수를 하나의 클래스 안에 함께 포함시키는 방식은 데이터 중심의 설계 안에서 흔히 볼 수 있는 패턴이다.

 

행동을 먼저 생각한다는 것이 익숙지 않다. 그리고 어떤 행동이 결국 객체의 협력이기에 어느 기준으로 누구에게 책임을 맡겨야 할지 아직까지 판단이 쉽지 않다..

책임 중심 관점

책임 중심의 관점은 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관하는 방법이다.

객체를 협력하는 공동체의 일원으로 본다.

그에 비해 객체의 책임은 인터페이스에 속한다. 객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는 데 필요한 상태를 캡슐화함으로써 구현 변경에 대한 파장이 외부로 퍼져나가는 것을 방지한다. 따라서 책임에 초점을 맞추면 상대적으로 변경에 안정적인 설계를 할 수 있다.

 

아까 위의 프로젝트에서도 여러 객체에서 좌표를 많이 사용해 Point라는 새로운 객체를 사용하려고 보니 코드를 다 뜯어고쳐야 했다.

각자의 좌표를 이곳저곳에서 서로 가져다 쓰고 있는 부분이 너무 많아서 코드를 다 갈아엎어야 되더라..

인터페이스를 통해 서로 메시지를 전달하도록 했다면, 그 내부에서만 수정할 수 있었을 것이다.

인터페이스에 대해 프로그래밍하라[GOF94]

설계 품질 척도 3가지

캡슐화

캡슐화란 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화하는 추상화의 한 종류이다.

즉, 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다.

캡슐화가 중요한 이유는 불안정한 부분과 안정적인 부분을 분리해서 변경의 영향을 통제할 수 있기 때문이다.

  • 구현: 변경될 가능성이 높은 부분
  • 인터페이스: 상대적으로 안정적인 부분
객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급 효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다. 객체를 사용하면 변경 가능성이 높은 부분은 외부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.

응집도와 결합도

일반적으로 좋은 설계란 높은 응집도와 낮은 결합도를 가진 모듈로 구성된 설계를 의미한다.

➡️ 설계를 변경하기 쉽게 만든다.

 

응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 의미한다.

객체지향의 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는지를 나타낸다.

변경의 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도이다.

응집도가 높을수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.

 

만약 내가 공 객체의 좌표에 대한 타입을 변경하려고 했을 때 공 클래스 내부의 함수들이 함께 변경되는 것은 응집도가 높고, 클래스 외부에서 공 좌표를 사용하는 모듈도 함께 변경된다면 응집도가 낮다고 판단할 수 있을 것 같다.

 

결합도는 의존성의 정도, 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도이다.

객체지향의 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지하고 있는지를 나타낸다.

변경의 관점에서 결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도이다.

클래스의 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 얻을 수 있다.

캡슐화의 정도가 응집도와 결합도에 영향을 미친다. 먼저 캡슐화를 향상시키기 위해 노력하라.

데이터 중심 설계의 문제점

객체의 행동보다는 상태에 초점을 맞춘다.

데이터 주도 설계는 시작하는 처음부터 데이터에 관해 결정하도록 강요하기 때문에 너무 이른 시기에 내부 구현에 초점을 맞추게 한다.

추측에 의한 설계 전략(design-by-guessing-strategy)[Holub04]
: 객체가 사용될 협력을 고려하지 않고 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다.
설계할 때 협력에 관해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지게 되는 경향이 있다.

 

접근자와 수정자 메서드는 객체 내부의 상태에 대한 어떤 정보도 캡슐화하지 못한다.

getFee 메서드와 setFee 메서드만 보아도 fee라는 이름의 인스턴스 변수가 존재한다는 사실을 퍼블릭 인터페이스에 노골적으로 드러낸다.

또한 객체 내부에 포함된 인스턴스 변수에 대한 정보를 인터페이스를 통해 노출되는 것도 포함한다.

내부 구현의 변경이 외부로 퍼져나가는 파급 효과(ripple effect)는 캡슐화가 부족하다는 명백한 증거다.

 

캡슐화의 진정한 의미

이 예제는 캡슐화가 단순히 객체 내부의 데이터를 외부로부터 감추는 것 이상의 의미를 가진다는 것을 잘 보여준다. 사실 캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다. 내부 속성을 외부로부터 감추는 것은 ‘데이터 캡슐화’라고 불리는 캡슐화의 한 종류일 뿐이다.

캡슐화란 변하는 어떤 것이든 감추는 것이다[Bain08, Shalloway01]. 그것이 무엇이든 구현과 관련된 것이라면 말이다.

객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.

비록 데이터를 처리하는 작업과 데이터를 같은 객체안에 두더라도 데이터에 초점이 맞춰져 있다면 만족스러운 캡슐화를 얻기 어렵다. 데이터를 먼저 결정하고 데이터를 처리하는 데 필요한 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러나게 된다. 결과적으로 객체의 인터페이스는 구현을 캡슐화하는 데 실패하고 코드는 변경에 취약해진다.
객체의 인터페이스에 구현이 노출돼 있었기 때문에 협력이 구현 세부사항에 종속돼 있고 그에 따라 객체의 내부 구현이 변경됐을 때 협력하는 객체 모두가 영향을 받을 수밖에 없었던 것이다.

 

➡️ 이는 높은 결합도와 낮은 응집도를 가지도록 만든다.

 

객체 내부의 구현이 객체의 인터페이스에 드러난다는 것은 클라이언트가 구현에 강하게 결합된다는 것을 의미한다.

단지 객체의 내부 구현을 변경했음에도 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다.

 

하나의 변경을 수용하기 위해 코드의 여러 곳을 동시에 변경해야 한다는 것은 설계의 응집도가 낮다는 증거이다.

응집도가 낮은 이유는 캡슐화를 위반했기 때문이다.

변경의 이유가 서로 다른 코드들을 하나의 모듈 안에 뭉쳐두면 변경과 아무 상관이 없는 코드들이 영향을 받게 된다.

하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다.

자율적인 객체를 향해

캡슐화는 설계의 제1원리다.

데이터 중심의 설계가 낮은 응집도와 높은 결합도라는 문제로 몸살을 앓게 된 근본적인 원인은 바로 캡슐화의 원칙을 위반했기 때문이다.
객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다. 객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

 

  • 이 객체가 어떤 데이터를 포함해야 하는가?
  • 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

요약해 보자면

  • 데이터 중심 설계는 캡슐화를 위반하고 객체 내부 구현을 인터페이스의 일부로 만든다.
  • 따라서 높은 결합도와 낮은 응집도를 가지도록 만든다.
  • 캡슐화의 원칙을 지키자.
  • 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.