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
의 내부 구현이 외부로 노출됐기 때문에 Movie
와 DiscountCondition
사이의 결합도
는 높을 수 밖에 없다. 이에 따라 DiscountCondition
의 내부 구현을 변경했음에도 Movie
까지 변경의 영향이 미치게 된다. 에를들면 DiscountCondition
의 기간 할인 조건이 PERIOD -> WHEN
으로 변경된다면 Movie
도 수정되어야 한다. 할인 조건의 구체적인 타입이 캡슐화되지 않고 Movie에 노출되었기 때문에 Movie는 DiscountCondition의 구체적인 세부 사항에 대한 지식까지 알아야 함으로써 결합도가 높아졌다.
결국 할인 조건의 구체적인 타입을 캡슐화
하지 못했기 때문에 결합도가 높아졌다.
낮은 응집도
이번에는 Screening
을 살펴보자.
위의 높은 결합도
에서의 예시와 마찬가지로 DiscountCondition
의 할인 조건 타입
이 변경되거나 추가되는 경우 Movie
뿐만 아니라 Screening
에서 Movie
의 isDiscountable
메서드를 호출하는 부분도 함께 변경되어야 한다. 이는 할인 가능 여부
를 판단해야할 책임이 DiscountCondition
을 넘어 Movie
와 Screening
에도 분산되었기 때문이다.
DiscountCondition
과 Movie
의 내부 구현이 인터페이스에 고스란히 노출되고 있고 Screening
은 노출된 구현에 직접적으로 의존하고 있다. 이것은 원래 DiscountCondition
이나 Movie
에 위치해야할 로직(책임)이 Screening
으로 새어나왔기 때문이다.
이에 따라 하나의 변경
을 수용하기 위해 분산된 책임을 맡고있는 여러 곳을 함께 수정해야 한다. 캡슐화 위반
이 낮은 응집도
를 초래한 결과이다.
데이터 중심 설계의 문제점
데이터 중심 설계2
가 변경에 유연하지 못한 이유는 캡슐화를 위반
했기 때문이다. 캡슐화를 위반한 설계를 구성하는 요소들이 높은 응집도와 낮은 결합도를 가질 확률은 극히 낮다. 따라서 캡슐화를 위반한 설계는 변경에 취약할 수 밖에 없다.
데이터 중심 설계
는 본질적으로너무 이른시기에 데이터에 관해 결정
하도록 강요한다.데이터 중심 설계
에서는협력
이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.
비록 데이터를 처리하는 작업과 데이터를 같은 객체 안에 두더라도 데이터
에 초점이 맞춰져 있따면 만족스러운 캡슐화를 얻기 어렵다. 데이터를 먼저 결정하고 데이터를 처리하는데 필요한 오퍼레이션을 나중에 결정하는 방식(데이터 중심 설계2
)은 데이터에 관한 지식이 인터페이스에 고스란히 드러난다. 결과적으로 객체의 인터페이스는 구현을 캡슐화하는데 실패하고 코드는 변경에 취약해진다.
결론적으로 데이터 중심 설계는 너무 이른시기에 데이터에 관해 고민하기 때문에 캡슐화에 실패하게 된다. 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 끼치게 된다.
데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.
객체지향 어플리케이션을 구축한다는 것은 협력하는 객체들의 공동체
를 구축하는 것이다. 따라서 협력
이라는 문맥 안에서 필요한 책임
을 결정하고 이를 수행할 적절한 객체를 결정
하는 것이 중요하다.
올바른 객체지향 설계의 무게 중심은 항상 객체 내부가 아니라 외부에 맞춰져 있어야 한다. 객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다. 중요한 것은 객체가 다른 객체와 협력하는 방식이다.
데이터 중심 설계
는 초점이 객체의 외부가 아니라 내부
로 향한다. 협력(실행 문맥)
에 대한 고민 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정한다.
객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없다. 이것이 데이터 중심 설계2
가 변경에 취약한 이유이다.
객체의 인터페이스에 구현이 노출
되어 있기 때문에 협력이 구현 세부사항에 종속
되어 있고 그에 따라 객체의 내부 구현이 변경될 때 협력하는 객체 모두가 영향을 받을 수 밖에 없었던 것이다.
참고 및 출처
- 오브젝트