Ch4 설계 품질과 트레이드오프

본문


들어가며

앞선 챕터를 통해서 우리는 객체지향 설계에 있어 협력, 책임, 역할을 가장 먼저 고려해야 하고, 그 중 책임이 가장 중요하다는 것을 알았다.

설계를 진행함에 있어 협력이라는 문맥을 고려하여 필요한 책임을 식별한 후 적절한 역할(또는 객체)에게 할당함으로써 책임 주도 설계가 가능하다.

이번 장에서는 객체의 책임(행동)이 아닌 데이터(상태)에 중점을 둔 첫 번째 설계를 살펴보고 이를 개선하기 위해 자신을 스스로 책임지는 객체로 변모시키는 두 번째 설계를 살펴본다.

결론부터 이야기 하자면 두 번째 설계가 조금 더 개선되었지만 결국 데이터에 중점을 맞춘 설계가 갖는 한계를 벗어나기 어렵다.

왜 그런지 캡슐화, 결합도, 응집도 측면에서 자세히 살펴보도록 하자.

캡슐화, 응집도, 결합도 간단 정리 start

캡슐화객체지향 설계의 가장 기본적인 원칙이자 핵심이다. 결론부터 이야기 하자면 변경의 여파를 통제하기 위해 캡슐화가 반드시 필요하다.

캡슐화는 간단히 설명하면 변경 가능성이 높은 불안정한 부분인 구현상대적으로 안정적인 부분인 인터페이스분리하여 외부에서는 안정적인 인터페이스에만 의존하게 함으로써 변경의 여파를 통제한다.

변경 가능한 어떤 것이라도 캡슐화의 대상(객체 내부로 숨김)이 된다. 이것이 바로 캡슐화는 변경의 여파를 통제하기 위해 필요하다는 의미이다.

응집도결합도캡슐화와 마찬가지로 변경과 관련이 깊다.

결론부터 이야기 하자면 응집도/결합도를 고려해야 하는 이유는 캡슐화와 마찬가지로 변경의 여파를 통제하기 위해서이다.

즉, 변경하기 쉬운 설계를 만들기 위해서이다.

또한 어떤 변경이 일어났다고 가정했을 때 객체에게 또는 객체 주변에 일어나는 현상으로 응집도/결합도의 정도를 판단할 수 있다. 여기서 객체 주변에 일어나는 현상이란 앞서 언급한 변경의 여파(Side Effect)라고 할 수 있다.

이것이 바로 응집도/결합도변경의 관점에서 바라보아야 하는 이유이다.


핵심.

  • 훌륭한 객체지향 설계가 객체의 데이터가 아닌 책임에 초점을 맞추는 이유는 변경과 관련이 깊다.

  • 객체의 상태구현에 속한다. 구현은 불안정하기 때문에 변하기가 쉽다.

  • 객체의 상태에 초점을 맞추게 되면 구현에 관한 세부사항이 인터페이스에 노출되게 되어 캡슐화의 원칙이 무너지게 되고 상태 변경이 인터페이스의 변경으로 이어지게 되어 이 인터페이스에 의존하는 모든 객체들에도 영향을 끼치게 된다.

  • 객체의 책임인터페이스에 속한다.

  • 객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는데 필요한 상태를 캡슐화함으로써 구현 변경에 대한 파급효과를 제한한다.

  • 데이터 중심 설계책임이 결정되기 전에 객체의 데이터가 무엇인지 먼저 고려하게 되고 인터페이스가 정의되기 전에 상태들이 정의된다. 그리고 자연스럽게 이 상태에 접근하고 수정하는 getter/setter가 정의된다.

  • 객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.

  • 변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적인 부분을 인터페이스라고 부른다.

  • 객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.

  • 객체지향에서 가장 중요한 원리는 캡슐화이다. 캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화 하는 추상화의 일종이다.

  • 객체지향 설계의 가장 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다.

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

  • 캡슐화란 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다. 객체 내부에 무엇을 숨겨야 하는가? 변경될 수 있는 어떤 것이라도 캡슐화해야 한다.

  • 응집도는 모듈에 포함된 내부 요소들이 연관된 정도를 나타낸다.

  • 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 응집도가 높은것이고, 모듈 내의 요소들이 서로 다른 목적을 추구한다면 응집도가 낮은 것이다.

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

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

  • 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가지고 어떤 모듈이 다른 모듈에 대해 필요한 지식만을 알고있다면 두 모듈은 낮은 결합도를 가진다.

  • 응집도결합도의 의미를 이해하기 위한 첫 걸음은 두 개념 모두 설계와 관련이 있다는 사실을 이해하는 것이다. 더 구체적으로는 응집도결합도변경과 관련된 것임을 명심해야 한다.

  • 변경의 관점에서 응집도요구사항의 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.

  • 응집도가 높은 설계에서는 하나의 요구사항 변경을 반영하기 위해 오직 하나의 모듈만 수정하면 된다.

  • 응집도가 낮은 설계에서는 하나의 원인에 의해 변경해야 하는 부분이 다수의 모듈에 분산되어 있기 때문에 여러 모듈을 동시에 수정해야 한다.

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

  • 결합도한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.

  • 영향을 받는 모듈의 수 외에도 변경의 원인을 이용해 결합도를 설명할 수도 있다. 내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우에는 두 모듈 사이의 결합도가 높다고 표현한다.

  • 결합도가 높으면 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기가 어려워진다.

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

  • 응집도결합도변경과 관련이 깊다는 점을 명심하라

  • 캡슐화의 정도가 응집도결합도에 영향을 미친다는 점을 명심하라.

  • 캡슐화를 지키면 모듈 내의 응집도가 높아지고 모듈 사이의 결합도는 낮아진다.

  • 응집도결합도를 고려하기 전에 먼저 캡슐화를 향상시키기 위해 노력하라.

  • 데이터 중심 설계책임 주도 설계의 근본적인 차이점은 바로 캡슐화다.

  • 데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현이 인터페이스에 스며들도록(getter/setter) 만든다. 반면 책임 주도 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화 한다.

  • 데이터 중심 설계캡슐화를 위반하기 때문에 책임 중심의 설계에 비해 낮은 응집도높은 결합도를 가지게 될 확률이 높다.

  • getter 메서드와 setter 메서드는 객체 내부에 어떤 인스턴스 변수가 존재하는지를 인터페이스에 노골적으로 드러냄으로써 캡슐화를 위반한다. 이는 근본적으로 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞추었기 때문에 나타나는 문제점이다.

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

  • 데이터 중심 설계캡슐화를 약화시킴으로써 내부 구현이 인터페이스에 고스란히 드러나게 되고, 내부 구현을 변경했을 뿐인데도 인터페이스도 함께 변경되어 이 인터페이스에 의존하는 클라이언트들 역시 함께 변경된다.

  • 결합도 측면에서 데이터 중심 설계가 가지는 또 다른 단점은 여러 제어 로직을 담당하는 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이다. 이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수 밖에 없다.

  • 서로 다른 이유로 변경되는 코드가 하나의 모듈안에 공존할 때 모듈의 응집도가 낮다고 말한다. 따라서 각 모듈의 응집도를 살펴보기 위해서는 코드를 수정하는 이유가 무엇인지 살펴봐야 한다.

  • 어떤 요구사항 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거이다.

  • 단일 책임 원칙(SRP)모듈의 응집도가 '변경'과 연관이 있다는 사실을 강조하기 위한 설계 원칙이다. 단일 책임 원칙을 한마디로 요약하면 클래스는 단 한가지의 변경 이유만 가져야 한다. 는 것이다.

  • 객체에게 의미있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다.

  • 우리가 상태행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다. 객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

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

  • 캡슐화는 단순히 객체 내부의 데이터를 감추는(private 사용)것 이상의 의미를 가진다.

  • 캡슐화는 변경될 수 있는 어떤 개념이라도 감추는 것을 의미한다. 내부 속성을 외부로 부터 감추는 것은 ‘데이터 캡슐화’라고 불리는 캡슐화의 일부분일 뿐이다.

  • 다시한번 강조하지만 “캡슐화란 변할 수 있는 어떤 개념이라도 감추는 것”을 의미한다. 그것이 객체의 구체적인 타입(할인 정책의 종류)이든, 데이터든, “변경되는 개념”이라면 캡슐화 함으로써 감추어야 할 대상이 된다.

  • 어떤 개념이든 내부 구현의 변경으로 외부의 객체가 영향을 받는다면 캡슐화를 위반한 것이다.

  • 설계에서 변하는 것이 무엇인지 고려하고 ‘변하는 개념’을 캡슐화 해야 한다.

  • 캡슐화를 위반한 설계를 구성하는 요소들이 높은 응집도와 낮은 결합도를 가질 확률은 극히 낮다. 따라서 캡슐화를 위반한 설계는 변경에 취약할 수 밖에 없다.

  • 데이터 중심 설계는 본질적으로 너무 이른시기에 데이터에 관해 결정하도록 강요한다.

  • 데이터 중심 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

  • 결론적으로 데이터 중심 설계너무 이른시기에 데이터에 관해 고민하기 때문에 캡슐화에 실패하게 된다. 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 끼치게 된다.

  • 올바른 객체지향 설계의 무게 중심은 항상 객체 내부가 아니라 외부에 맞춰져 있어야 한다. 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다. 중요한 것은 객체가 다른 객체와 협력하는 방식이다.

  • 데이터 중심 설계객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없다. 이것이 데이터 중심 설계2가 변경에 취약한 이유이다. 객체의 인터페이스에 구현이 노출되어 있기 때문에 협력이 구현 세부사항에 종속되어 있고 그에 따라 객체의 내부 구현이 변경될 때 협력하는 객체 모두가 영향을 받을 수 밖에 없었던 것이다.


데이터 중심 설계 1

데이터 중심 설계는 객체 내부에 저장되는 데이터를 기반으로 시스템을 분할한다. 책임 중심 설계‘책임이 무엇인가’ 를 묻는 것으로 시작한다면 데이터 중심의 설계는 ‘객체가 내부에 저장해야 하는 데이터가 무엇인가’ 로 시작한다.

이에 따라 책임이 결정되기 전에 객체의 데이터가 무엇인지 먼저 고려하게 되고 인터페이스가 정의되기 전에 상태들이 정의된다.

그리고 자연스럽게 이 상태에 접근하고 수정하는 getter/setter를 추가하게 된다.

getter/setter를 갖는 데이터 객체가 정의되면 이 데이터 객체에 접근하여 제어 로직을 수행하는 제어 객체가 등장한다. 이 하나의 제어 객체에서 다른 데이터 객체getter/setter를 이용하여 필요한 기능을 구현한다.

아래는 데이터 객체를 사용하는 제어 객체의 모습을 보여주는 데이터 중심 설계의 결과물이다.

코드와 주석을 참고하면 알 수 있지만 제어 객체(ReservationAgency)에서 데이터 객체(Screening, Movie, DiscountCondition)의 데이터를 가져와서 할인 조건 판단, 할인 정책 판단, 영화 예매 요금 계산 로직을 수행한다.

이제 이 설계를 책임 중심 설계 방법과 비교해 보면서 두 방법의 장단점을 비교해보도록 하자. 먼저 두 설계 방법을 비교하기 위해 사용할 수 있는 기준으로 캡슐화, 응집도, 결합도를 사용할 것이다.

설계 트레이드오프

캡슐화

상태행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서이다. 여기서 구현이란 변경될 가능성이 높은 어떤 것을 의미한다. 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 미치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다.

객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함으로써 변경의 여파를 통제할 수 있다.

변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적인 부분을 인터페이스라고 부른다.

객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.

지금까지 설명한 것에서 알 수 있는 것 처럼 객체지향에서 가장 중요한 원리는 캡슐화이다. 캡슐화는 외부에서 알 필요가 없는 부분을 감춤으로써 대상을 단순화 하는 추상화의 일종이다.

객체지향 설계의 가장 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다.

설계가 필요한 이유는 요구사항이 변경되기 때문이고 캡슐화가 중요한 이유는 불안정한 부분과 안정적인 부분을 분리해서 변경의 영향을 통제할 수 있기 때문이다. 따라서 변경의 관점에서 설계의 품질을 판단하기 위해 캡슐화를 기준으로 삼을 수 있다.

정리하면 캡슐화란 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다. 객체 내부에 무엇을 숨겨야 하는가? 변경될 수 있는 어떤 것이라도 캡슐화해야 한다.

응집도와 결합도

응집도는 모듈에 포함된 내부 요소들이 연관된 정도를 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 응집도가 높은것이고, 모듈 내의 요소들이 서로 다른 목적을 추구한다면 응집도가 낮은 것이다. 객체지향 관점에서 응집도는 객체 또는 클래스에 얼마나 관련 높은 책임들을 할당했는가를 나타낸다.

결합도의존성의 정도를 나타내며 다른 모듈에 얼마나 많은 지식을 갖고있는가를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가지고 어떤 모듈이 다른 모듈에 대해 필요한 지식만을 알고있다면 두 모듈은 낮은 결합도를 가진다.

응집도결합도의 의미를 이해하기 위한 첫 걸음은 두 개념 모두 설계와 관련이 있다는 사실을 이해하는 것이다. 더 구체적으로는 응집도결합도변경과 관련된 것임을 명심해야 한다.

높은 응집도낮은 결합도를 추구해야 하는 이유는 그것이 설계를 변경하기 쉽게 만들기 때문이다.

변경의 관점에서 응집도요구사항의 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.

하나의 변경을 수용하기 위해 모듈 전체가 함께 변경된다면 응집도가 높은 것이고 하나의 변경에 대해 모듈의 일부만 변경된다면 응집도가 낮은 것이다. 또한 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높은 것이고 하나의 변경에 대해 다수의 모듈이 변경된다면 응집도가 낮은 것이다.

응집도에 대한 개인적인 사색 start

변경의 관점에서 응집도를 판단하는 범위는 모듈 내부의 변경협력하는 모듈들의 변경으로 구분할 수 있을 것 같다.

위에서도 응집도 판단으로 어떤 변경에 대해서 모듈 내부의 요소가 전체적으로 함께 변경되는 경우어떤 변경에 대해 하나의 모듈 뿐 아니라 다른 모듈도 함께 변경 으로 나누어 제시하였다.

응집도는 말 그대로 책임이 응집된 정도를 의미한다. 먼저 모듈 내부의 변경에 초점을 맞춘 응집도 판단은 하나의 모듈이 갖는 책임들이 얼마나 서로 연관되어 있는가로 측정할 수 있다. 만일 책임이 서로 연관되어 있다면 어떤 변경사항으로 인해 해당 모듈이 수정될 때는 연관된 책임들이 함께 변경되기 마련이다. 반면 책임 사이가 서로 연관되어 있지 않다면 서로 다른 이유로 변경됨으로써 낮은 응집도를 갖게된다.

협력하는 모듈들의 변경에 초점을 맞춘 응집도 판단은 하나의 책임이 얼마나 많은 모듈에 분산되어 있는가로 측정할 수 있다. 만일 하나의 모듈에 응집되어 있다면 자연스럽게 해당 모듈만 수정될 테니 높은 응집도를 가진다. 반면 여러 모듈에 분산되어 있다면 분산된 책임을 맡은 모든 모듈을 함께 변경해야 하기 때문에 낮은 응집도를 갖는다.

여기서 협력하는 모듈들의 변경결합도와도 관련이 있다.

응집도에 대한 개인적인 사색 end

응집도가 높은 설계에서는 하나의 요구사항 변경을 반영하기 위해 오직 하나의 모듈만 수정하면 된다. 반면 응집도가 낮은 설계에서는 하나의 원인에 의해 변경해야 하는 부분이 다수의 모듈에 분산되어 있기 때문에 여러 모듈을 동시에 수정해야 한다.

결합도 역시 변경의 관점에서 설명할 수 있다.

결합도한 모듈이 변경되기 위해 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다. 다시말해 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함께 수정해야 하는지를 나타낸다. 따라서 결합도가 높으면 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기가 어려워진다.

영향을 받는 모듈의 수 외에도 변경의 원인을 이용해 결합도를 설명할 수도 있다. 내부 구현을 변경했을 때 이것이 다른 모듈에 영향을 미치는 경우에는 두 모듈 사이의 결합도가 높다고 표현한다.

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

다시한번 강조하지만 응집도결합도변경과 관련이 깊다.

또한 캡슐화의 정도가 응집도결합도에 영향을 미친다. 캡슐화를 지키면 모듈 안의 응집도는 높아지고 모듈 사이의 결합도는 낮아진다 따라서 응집도결합도를 고려하기 전에 먼저 캡슐화를 향상시키기 위해 노력하라.

데이터 중심의 설계의 문제점(영화 예매 시스템 예시)

위에서 보여준 데이터 중심 설계의 영화 예매 시스템 코드를 확인하면 알 수 있듯이 사실상 기능적으로는 2장에서 설계한 책임 주도 설계와 동일하다. 하지만 설계 관점에서는 완전히 다르다. 근본적인 차이점은 캡슐화를 다루는 방식이다. 데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현이 인터페이스에 스며들도록(getter/setter) 만든다. 반면 책임 주도 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화 한다.

캡슐화의 정도가 객체의 응집도와 결합도를 결정한다는 사실을 기억하라.

데이터 중심 설계캡슐화를 위반하기 때문에 책임 중심의 설계에 비해 낮은 응집도높은 결합도를 가지게 될 확률이 높다.

요약하자면 데이터 중심 설계가 가진 대표적인 문제점은 다음과 같다.

  • 캡슐화 위반

  • 높은 결합도

  • 낮은 응집도

캡슐화 위반

getter 메서드와 setter 메서드는 객체 내부에 어떤 인스턴스 변수가 존재하는지를 인터페이스에 노골적으로 드러냄으로써 캡슐화를 위반한다. 이는 근본적으로 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞추었기 때문에 나타나는 문제점이다. 객체에게 가장 중요한 것은 책임이다. 그리고 구현을 캡슐화 할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때만 얻을 수 있다.

높은 결합도

지금까지 살펴본 것 처럼 데이터 중심 설계getter/setter를 통해 내부 구현을 인터페이스에 드러내기 때문에 캡슐화를 위반한다.

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

그리고 더 나쁜 소식은 단지 객체의 내부를 변경했음에도 불구하고 이 인터페이스에 의존하는 모든 클라이언트들도 함께 변경해야 한다는 것이다.

이처럼 데이터 중심 설계캡슐화를 약화시킴으로써 내부 구현이 인터페이스에 고스란히 드러나게 되고, 내부 구현을 변경했을 뿐인데도 인터페이스도 함께 변경되어 이 인터페이스에 의존하는 클라이언트들 역시 함께 변경된다. 즉, 클라이언트가 객체의 구현에 강하게 결합된다.

결합도 측면에서 데이터 중심 설계가 가지는 또 다른 단점은 여러 제어 로직을 담당하는 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이다. 이 결합도로 인해 어떤 데이터 객체를 변경하더라도 제어 객체를 함께 변경할 수 밖에 없다.

낮은 응집도

서로 다른 이유로 변경되는 코드가 하나의 모듈안에 공존할 때 모듈의 응집도가 낮다고 말한다. 따라서 각 모듈의 응집도를 살펴보기 위해서는 코드를 수정하는 이유가 무엇인지 살펴봐야 한다.

위의 영화 예매 시스템 코드의 경우 다양한 이유로 모듈 내부를 변경해야 한다.

  • 할인 정책이 추가될 경우
  • 할인 정책별로 할인 요금을 계산하는 방법이 변경될 경우
  • 할인 조건이 추가되는 경우
  • 할인 조건별로 할인 여부를 판단하는 방법이 변경될 경우
  • 예매 요금을 계산하는 방법이 변경될 경우

벌써 다섯 가지의 서로 다른 이유로 하나의 모듈 내부를 변경해야 한다.

낮은 응집도두 가지 측면에서 설계에 문제를 일으킨다.

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

예를 들어 위의 영화 예매 시스템 코드 안에 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 함께 존재하기 때문에 새로운 할인 정책을 추가

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

현재의 데이터 중심 설계1은 새로운 할인 정책을 추가하거나 새로운 할인 조건을 추가하기 위해 하나 이상의 클래스를 동시에 수정해야 한다. 어떤 요구사항 변경을 수용하기 위해 하나 이상의 클래스를 수정해야 하는 것은 설계의 응집도가 낮다는 증거이다.

단일 책임 원칙(SRP)란 모듈의 응집도가 ‘변경’과 연관이 있다는 사실을 강조하기 위한 설계 원칙이다. 단일 책임 원칙을 한마디로 요약하면 “클래스는 단 한가지의 변경 이유만 가져야 한다”는 것이다. 단익 책임 원칙에서의 ‘책임’은 협력, 책임, 역할에서의 ‘책임’과 조금 다르며 ‘변경’과 관련된 ‘더 큰 개념’을 가리킨다.

자율적인 객체를 향해

캡슐화를 지켜라.

캡슐화는 설계의 제 1원리다. 데이터 중심 설계가 낮은 응집도높은 결합도 문제로 몸살을 앓고있는 근본적인 원인은 바로 캡슐화 원칙을 위반했기 때문이다. 객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 접근할 수 있어야 한다.

여기서 말하는 메서드는 getter/setter를 의미하는 것이 아니다. 객체에게 의미있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다.

스스로 자신의 데이터를 책임지는 객체

우리가 상태행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다. 객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.

데이터 중심 설계1의 코드는 계속 언급했듯이 캡슐화 원칙을 위배함으로써 구현(상태)를 외부로 드러내는 다수의 데이터 객체제어 객체가 의존하기 때문에 낮은 응집도높은 결합도로 작은 변경에도 큰 파급 효과가 생긴다.

이제 각 데이터 객체들을 자신의 데이터를 스스로 책임지는 객체로 수정해보자. 즉, 하나의 제어 객체에서 제어 로직을 모두 수행하는 것이 아닌 각 객체들이 자신의 데이터를 스스로 책임지도록 수정하자.

수정된 데이터 중심 설계2 코드는 아래와 같다.

위 코드는 데이터 중심 설계1에 비해서 객체들이 스스로 자신의 데이터를 책임지게끔 함으로써 하나의 제어 객체에 모든 로직을 담는 것이 아닌 각 객체가 자신의 데이터를 스스로 책임진다.

하지만 여전히 부족하다.

분명히 캡슐화 관점에서 데이터 중심 설계1보다 데이터 중심 설계2가 향ㅇ상된것은 사실이지만 그렇다고 만족스러울 정도는 아니다. 사실 별칭에서도 알 수 있듯이 본질적으로는 데이터 중심 설계2 역시 데이터 중심 설계 방식에 속한다.

고통이 조금 경감되기는 했지만 데이터 중심 설계1에서 발생했던 변경과 관련된 문제들은 데이터 중심 설계2에서도 여전히 발생한다. 그 이유를 살펴보자.

캡슐화 위반

분명 수정된 데이터 중심 설계2자기 자신의 데이터를 스스로 처리한다. 예를들어 DiscountCondition은 자기 자신의 데이터를 이용해 할인 가능 여부를 스스로 판단한다.

하지만 주석에서도 알 수 있듯이 DiscountCondition에 구현된 두 개의 isDiscountable 메서드를 자세히 살펴보면 이상한 점이 몇 군데 눈에 띈다.

isDiscountable 메서드들은 DiscountCondition이 내부에 순번 데이터기간 데이터를 포함하고 있다는 사실을 노출한다.

또한 getType()을 통해 외부에 DiscountConditionType을 포함하고 있다는 정보가 고스란히 노출된다.

만일 위와 같은 데이터를 변경하게 된다면 어떻게 될까? 위 메서드를 사용하는 모든 클라이언트들 역시 함께 수정해야 할 것이다.

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

Movie도 마찬가지로 내부 구현을 인터페이스에 노출시키고 있다. Movie할인 정책의 종류를 인터페이스에 고스란히 노출함으로써 어떤 할인 정책 타입이 존재하는지를 클라이언트가 알아야하고, 할인 정책 타입이 변경되거나 추가되면 Movie에 의존하는 모든 클라이언트 코드 역시 변경되어야 한다.

따라서 Movie는 세 가지 할인 정책을 포함하고 있다는 세부적인 내부 구현을 성공적으로 캡슐화하지 못한다.

이 예제는 캡슐화가 단순히 객체 내부의 데이터를 감추는(private 사용)것 이상의 의미를 가진다는 것을 잘 보여준다.

캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다. 내부 속성을 외부로 부터 감추는 것은 ‘데이터 캡슐화’라고 불리는 캡슐화의 일부분일 뿐이다.

다시한번 강조하지만 “캡슐화란 변할 수 있는 어떤 개념이라도 감추는 것”을 의미한다. 그것이 객체의 구체적인 타입(할인 정책의 종류)이든, 데이터든, “변경되는 개념”이라면 캡슐화 함으로써 감추어야 할 대상이 된다.

어떤 개념이든 내부 구현의 변경으로 외부의 객체가 영향을 받는다면 캡슐화를 위반한 것이다.

설계에서 변하는 것이 무엇인지 고려하고 ‘변하는 개념’을 캡슐화 해야 한다.

정리하자면 “캡슐화란 변하는 어떤 개념이든 감추는 것이다.” 그것이 무엇이든 구현과 관련된 것이라면 말이다.

높은 결합도

캡슐화 위반으로 DiscountCondition의 내부 구현이 외부로 노출됐기 때문에 MovieDiscountCondition 사이의 결합도는 높을 수 밖에 없다. 이에 따라 DiscountCondition의 내부 구현을 변경했음에도 Movie까지 변경의 영향이 미치게 된다. 에를들면 DiscountCondition의 기간 할인 조건이 PERIOD -> WHEN으로 변경된다면 Movie도 수정되어야 한다. 할인 조건의 구체적인 타입이 캡슐화되지 않고 Movie에 노출되었기 때문에 Movie는 DiscountCondition의 구체적인 세부 사항에 대한 지식까지 알아야 함으로써 결합도가 높아졌다.

결국 할인 조건의 구체적인 타입을 캡슐화하지 못했기 때문에 결합도가 높아졌다.

낮은 응집도

이번에는 Screening을 살펴보자.

위의 높은 결합도에서의 예시와 마찬가지로 DiscountCondition할인 조건 타입이 변경되거나 추가되는 경우 Movie뿐만 아니라 Screening에서 MovieisDiscountable 메서드를 호출하는 부분도 함께 변경되어야 한다. 이는 할인 가능 여부를 판단해야할 책임이 DiscountCondition을 넘어 MovieScreening에도 분산되었기 때문이다.

DiscountConditionMovie의 내부 구현이 인터페이스에 고스란히 노출되고 있고 Screening은 노출된 구현에 직접적으로 의존하고 있다. 이것은 원래 DiscountCondition이나 Movie에 위치해야할 로직(책임)이 Screening으로 새어나왔기 때문이다.

이에 따라 하나의 변경을 수용하기 위해 분산된 책임을 맡고있는 여러 곳을 함께 수정해야 한다. 캡슐화 위반낮은 응집도를 초래한 결과이다.

데이터 중심 설계의 문제점

데이터 중심 설계2가 변경에 유연하지 못한 이유는 캡슐화를 위반했기 때문이다. 캡슐화를 위반한 설계를 구성하는 요소들이 높은 응집도와 낮은 결합도를 가질 확률은 극히 낮다. 따라서 캡슐화를 위반한 설계는 변경에 취약할 수 밖에 없다.

  • 데이터 중심 설계는 본질적으로 너무 이른시기에 데이터에 관해 결정하도록 강요한다.
  • 데이터 중심 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.

비록 데이터를 처리하는 작업과 데이터를 같은 객체 안에 두더라도 데이터에 초점이 맞춰져 있따면 만족스러운 캡슐화를 얻기 어렵다. 데이터를 먼저 결정하고 데이터를 처리하는데 필요한 오퍼레이션을 나중에 결정하는 방식(데이터 중심 설계2)은 데이터에 관한 지식이 인터페이스에 고스란히 드러난다. 결과적으로 객체의 인터페이스는 구현을 캡슐화하는데 실패하고 코드는 변경에 취약해진다.

결론적으로 데이터 중심 설계는 너무 이른시기에 데이터에 관해 고민하기 때문에 캡슐화에 실패하게 된다. 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 끼치게 된다.

데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.

객체지향 어플리케이션을 구축한다는 것은 협력하는 객체들의 공동체를 구축하는 것이다. 따라서 협력이라는 문맥 안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 중요하다.

올바른 객체지향 설계의 무게 중심은 항상 객체 내부가 아니라 외부에 맞춰져 있어야 한다. 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다. 중요한 것은 객체가 다른 객체와 협력하는 방식이다.

데이터 중심 설계는 초점이 객체의 외부가 아니라 내부로 향한다. 협력(실행 문맥)에 대한 고민 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정한다.

객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없다. 이것이 데이터 중심 설계2가 변경에 취약한 이유이다.

객체의 인터페이스에 구현이 노출되어 있기 때문에 협력이 구현 세부사항에 종속되어 있고 그에 따라 객체의 내부 구현이 변경될 때 협력하는 객체 모두가 영향을 받을 수 밖에 없었던 것이다.

참고 및 출처

  • 오브젝트