01 데이터 중심의 영화 예매 시스템
객체지향 설계에서는 두 가지 방법을 이용해 시스템을 객체로 분할할 수 있다.
첫 번째 방법은 상태(데이터)를 분할의 중심축으로 삼는 방법이고, 두 번째 방법은 책임을 분할의 중심으로 삼는 방법이다.
데이터 중심의 관점에서 객체는 자신이 포함하고 있는 데이터를 조작하는 데 필요한 오퍼레이션을 정의한다.
책임 중심의 관점에서 객체는 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관한다.
데이터 중심의 관점은 객체의 상태에 초점을 맞추고,
책임 중심의 관점은 객체의 행동에 초점을 맞춘다.
훌륭한 객체지향 설계는 데이터가 아니라 책임에 초점을 맞춰야 한다. 이유는 변경과 관련이 있다.
객체의 상태는 구현에 속한다. 구현은 불안정하기 때문에 변하기 쉽다. 상태를 객체 분할의 중심축으로 삼으면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되어 캡슐화의 원칙이 무너진다.
그에 비해 객체의 책임은 인터페이스에 속한다. 객체는 책임을 드러내는 안정적인 인터페이스 뒤로 책임을 수행하는 데 필요한 상태를 캡슐화함으로써 구현 변경에 대한 파장이 외부로 퍼져나가는 것을 방지한다.
02 설계 트레이드오프
데이터 중심 설계와 책임 중심 설계의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도를 사용하겠다. 먼저 의미를 살펴보자.
캡슐화
상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위해서다. 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다.
변경될 가능성이 높은 부분을 구현이라고 부르고 상대적으로 안정적인 부분을 인터페이스라고 부른다. 객체를 설계하기 위한 가장 기본적인 방법은 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것이다.
정리하자면 캡슐화란 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다. 객체 내부에는 무엇을 캡슐화해야 하는가? 변경될 수 있는 어떤 것이라도 캡슐화해야 한다.
응집도와 결합도
응집도는 모듈에 포함된 내부 요소들이 연관돼 있는 정도를 나타낸다. 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.
결합도는 의존성의 정도를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고 있다면 두 모듈은 높은 결합도를 가진다.
하지만 위와 같은 애매한 설명만으로는 응집도와 결합도의 의미를 명확하게 이해하기 어렵다. 얼마나 강하게 연관돼 있어야 응집도가 높다고 말할 수 있는가? 어느 정도의 의존성만 남겨야 결합도가 낮다고 말할 수 있는가?
변경의 관점에서 응집도란 변경이 발생할 때 모듈 내부에서 발생하는 변경의 정도로 측정할 수 있다.
즉, 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높은 것이며 다수의 모듈이 함께 변경돼야 한다면 응집도가 낮은 것이다.
결합도 역시 변경의 관점에서 설명할 수 있다. 결합도는 한 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도로 측정할 수 있다.
즉, 결합도가 높으면 높을수록 함께 변경해야 하는 모듈의 수가 늘어나기 때문에 변경하기가 어려워진다.
결합도가 높아도 상관없는 경우도 있다. 일반적으로 변경될 확률이 매우 적은 안정적인 모듈에 의존하는 것은 아무런 문제도 되지 않는다. 표준 라이브러리에 포함된 모듈이나 성숙 단계에 접어든 프레임워크에 의존하는 경우가 여기에 속한다.
그러나 직접 작성한 코드는 항상 불안정하며 언제라도 변경될 가능성이 높다. 코드 안에 버그가 존재할 수도 있고 갑자기 요구사항이 변경될 수도 있다. 코드를 완성한 그 순간부터 수정할 준비를 해야 한다. 따라서 직접 작성한 코드의 경우에는 낮은 결합도를 유지하려고 노력해야 한다.
캡슐화의 정도가 응집도와 결합도에 영향을 미친다. 캡슐화를 지키면 모듈 안의 응집도는 높아지고 모듈 사이의 결합도는 낮아진다. 따라서 응집도와 결합도를 고려하기 전에 먼저 캡슐화를 향상시키기 위해 노력하라.
03 데이터 중심의 영화 예매 시스템의 문제점
데이터 중심의 설계는 캡슐화를 위반하고 객체의 내부 구현을 인터페이스의 일부로 만든다. 반면 책임 중심의 설계는 객체의 내부 구현을 안정적인 인터페이스 뒤로 캡슐화한다.
캡슐화의 정도가 객체의 응집도와 결합도를 결정하기 때문에, 캡슐화를 위반하기 쉬운 데이터 중심의 설계는 책임 중심의 설계에 비해 응집도가 낮고 결합도가 높은 객체들을 양산하게 될 가능성이 높다.
요약하자면 데이터 중심의 문제점은 다음과 같다.
- 캡슐화 위반
- 높은 결합도
- 낮은 응집도
단일 책임 원칙(Single Responsibility Principle, SRP)
로버트 마틴은 모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 단일 책임 원칙이라는 설계 원칙을 제시했다.
단일 책임 원칙을 한마디로 요약하자면 클래스는 단 한 가지의 변경 이유만 가져야 한다는 것이다.
주의할 점은 단일 책임 원칙이라는 맥락에서 '책임'이라는 말이 '변경의 이유'라는 의미로 사용된다는 점이다.
단일 책임 원칙에서의 책임은 지금까지 역할, 책임, 협력에서의 책임과는 다르며 변경과 관련된 더 큰 개념을 가리킨다.
04 자율적인 객체를 향해
캡슐화를 지켜라
캡슐화는 설계의 제1원리다. 데이터 중심의 설계가 낮은 응집도와 높은 결합도라는 문제로 몸살을 앓은 이유는 캡슐화를 위반했기 때문이다.
객체는 자신이 어떤 데이터를 가지고 있는지를 내부에 캡슐화하고 외부에 공개해서는 안된다. 객체는 스스로 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.
속성의 가시성을 private으로 설정했더라도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.
이 사각형의 너비와 높이를 증가시키는 코드가 필요하다면, 외부 어떤 클래스 안에 구현돼 있을 것이다.
위 코드에는 많은 문제점이 있다.
첫째, '코드 중복'이 발생할 확률이 높다. 다른 곳에서도 필요하다면 유사한 코드가 존재할 것이다.
둘째, 변경에 취약하다. right와 bottom대신 length와 height를 이용해 사각형을 표현하도록 수정이 필요하다면 어떨까?
이미 top, left, right, bottom이라는 내부 구현을 인터페이스를 통해 외부에 노출시키고 있다.
결과적으로 getRight, setRight, getBottom, setBottom 메서드를 getLength, setLength, getHeight, setHeight로 변경해야 하고, 이 변경은 기존의 접근자 메서드를 사용하던 모든 코드에 영향을 미친다.
해결 방법은 캡슐화를 강화시키는 것이다. Rectangle 내부에 너비와 높이를 조절하는 로직을 캡슐화하면 두 가지 문제를 해결할 수 있다.
이를 통해 Rectangle을 변경하는 주체를 외부의 객체에서 Rectangle로 이동시켰다. 즉, 자신의 크기를 스스로 증가시키도록 '책임을 이동'시킨 것이다. 이것이 바로 객체가 스스로를 책임진다는 말의 의미이다.
스스로 자신의 데이터를 책임지는 객체
상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서다.
객체는 단순한 데이터 제공자가 아니다. 객체 내부에 저장되는 데이터보다 객체가 협력에 참여하면서 수행할 책임을 정의하는 오퍼레이션이 더 중요하다.
따라서 객체를 설계할 때 "이 객체가 어떤 데이터를 포함해야 하는가?"라는 질문은 다음과 같은 두 개의 개별적인 질문으로 분리해야 한다.
- 이 객체가 어떤 데이터를 포함해야 하는가?
- 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?
두 번째 질문은 데이터를 처리하기 위해 어떤 오퍼레이션이 필요한지를 묻는 것이다. Movie가 포함하는 데이터를 살펴보면 영화 요금을 계산하는 오퍼레이션과 할인 여부를 판단하는 오퍼레이션이 필요할 것이다.
05 하지만 여전히 부족하다
캡슐화 위반
메서드의 파라미터 정보 즉, 인스턴스 변수가 포함돼 있다는 사실을 인터페이스 외부에 노출하고 있다.
만약 해당 속성을 수정해야 하면? 아마도 해당 메서드를 사용하는 모든 클라이언트도 함께 수정해야 할 것이다.
내부 구현의 변경이 외부로 퍼져나가는 파급 효과(ripple effect)는 캡슐화가 부족하다는 명백한 증거다.
캡슐화의 진정한 의미
캡슐화는 단순히 객체 내부의 데이터를 외부로부터 감추는 것 이상의 의미를 가진다.
사실 캡슐화는 변경될 수 있는 어떤 것이라도 감추는 것을 의미한다.
내부 속성을 외부로부터 감추는 것은 '데이터 캡슐화'라고 불리는 캡슐화의 한 종류일 뿐이다.
높은 결합도
캡슐화 위반으로 내부 구현이 외부로 노출됐기 때문에 결합도는 높을 수밖에 없다. 두 객체 사이에 결합도가 높을 경우 한 객체의 구현을 변경할 때 다른 객체에게 변경의 영향이 전파될 확률이 높아진다.
인터페이스가 아니라 '구현'을 변경하는 경우에도 변경해야 한다는 것은 두 객체 사이의 결합도가 높다는 것을 의미한다.
문제의 원인은 캡슐화 원칙을 지키지 않았다는 것이다. 유연한 설계를 창조하기 위해서는 캡슐화를 설계의 첫 번째 목표로 삼아야 한다.
낮은 응집도
캡슐화 위반으로 하나의 변경을 수용하기 위해 코드의 여러 곳을 동시에 변경해야 한다. 즉, 설계의 응집도가 낮다.
내부 구현이 인터페이스에 그대로 노출되고 있고, 노출된 구현에 직접적으로 의존한다.
이처럼 개선했지만 데이터 중심의 설계가 가지는 문제점으로 인해 몸살을 앓고 있다는 것에는 변함이 없다.
데이터 중심의 설계는 어떤 이유로 이러한 문제점을 유발하는 것일까?
06 데이터 중심 설계의 문제점
데이터 중심의 설계가 변경에 취약한 이유는 두 가지다.
- 데이터 중심의 설계는 본질적으로 너무 이른 시기에 데이터에 관해 결정하도록 강요한다.
- 데이터 중심의 설계에서는 협력이라는 문맥을 고려하지 않고 객체를 고립시킨 채 오퍼레이션을 결정한다.
데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다
데이터 중심의 설계를 시작할 때 첫 질문은 "이 객체가 포함해야 하는 데이터가 무엇인가?"다.
데이터는 구현이 일부이다. 데이터 주도 설계는 설계를 시작하는 단계부터 데이터에 관해 결정하도록 강요하기 때문에 너무 이른 시기에 내부 구현에 초점을 맞추게 한다. 객체의 내부 구현이 객체의 인터페이스를 어지럽히고 캡슐화 실패로 응징도와 결합도에 나쁜 영향을 미치기 때문에 변경에 취약한 코드를 낳게 된다.
데이터 중심 설계는 객체를 고립시킨 채 오퍼레이션을 정의하도록 만든다.
객체지향 애플리케이션을 구현한다는 것은 협력하는 객체들의 공동체를 구축한다는 것이다.
따라서 협력이라는 문맥 안에서 필요한 책임을 결정하고 이를 수행할 적절한 객체를 결정하는 것이 가장 중요하다.
올바른 객체지향 설계의 무게 중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 한다.
안타깝게도 데이터 중심 설계의 초점은 객체의 내부로 향한다. 객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워 맞출 수밖에 없다.
'도서 > Object' 카테고리의 다른 글
[Object] Chapter 05 - 책임 할당하기 (0) | 2021.08.26 |
---|---|
[Object] Chapter 03 - 역할, 책임, 협력 (0) | 2021.07.30 |
[Object] Chapter 02 - 객체지향 프로그래밍 (0) | 2021.07.03 |
[Object] Chapter 01 - 객체, 설계 (0) | 2021.07.03 |