본문 바로가기

기술서적 정리/오브젝트 - 코드로 이해하는 객체지향 설계

04 CHAPTER 설계 품질과 트레이드 오프

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라! 객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두 번째 걸음은 객체를

wikibook.co.kr

 

객체지향 설계의 핵심은 역할, 책임, 협력이다.

협력은 애플리케이션의 기능을 구현하기 위해 메시지를 주고받는 객체들 사이의 상호작용이다.

책임은 객체가 다른 객체와 협력하기 위해 수행하는 행동이다.

역할은 대체 가능한 책임의 집합이다.



훌륭한 설계란 합리적인 비용안에서 변경을 수용할 수 있는 구조를 만드는 것이다.



 

◈ 설계 트레이드 오프

이 장에서는 데이터 중심 설계와 책임 중심 설계의 장단점을 비교하기 위해 캡슐화, 응집도, 결합도를 사용한다.

캡슐화

객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절하게 조절할 수 있는 장치를 제공하기 때문이다.

 

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

 

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

 

객체지향에서 가장 중요한 원리는 캡슐화이다.

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

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

 

객체 내부에 무엇을 캡슐화해야 하는가?

변경될 수 있는 어떤 것이라도 캡슐화해야 한다.

 

응집도와 결합도

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

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

 

응집도와 결합도의 의미를 이해하기 위한 첫걸음은 두 개념 모두 설계와 관련 있다는 사실을 이해하는 것이다.

 

좋은 설계란 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계이다.

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



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

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

또한 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높지만 다수의 모듈이 함께 변경돼야 한다면 응집도가 낮은 것이다.

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



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

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

다시 말해 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함께 수정해야 하는지를 나타낸다.

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

직접 작성한 코드는 항상 불안정하며 언제라도 변경될 가능성이 높다.

직접 작성한 코드의 경우에는 낮은 결합도를 유지하려고 노력해야 한다.

 

캡슐화의 정도가 응집도와 결합도에 영향을 미친다는 사실을 강조.

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

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





 데이터 중심의 시스템의 문제점

데이터 중심의 설계가 가진 대표적인 문제점은 다음과 같이 요약할 수 있다.

캡슐화 위반, 높은 결합도, 낮은 응집도.

캡슐화 위반

오직 getFee 메서드와 setFee 메서드로만 객체의 내부 상태에 접근할 수 있게 구성하면 캡슐화의 원칙을 지키는 것처럼 보인다.

 

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

 

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



설계할 때 협력에 관해 고민하지 않으면 캡슐화를 위반하는 과도한 접근자와 수정자를 가지게 되는 경향이 있다.

객체가 사용될 문맥을 추측할 수밖에 없는 경우 개발자는 어떤 상황에서도 해당 객체가 사용될 수 있게 최대한 많은 접근자 메서드를 추가하게 된다.



추측에 의한 설계 전략(design-by-guessing strategy)은 객체가 사용될 협력을 고려하지 않고 객체가 다양한 상황에서 사용될 수 있을 것이라는 막연한 추측을 기반으로 설계를 진행한다.

결과적으로 대부분의 내부 구현이 퍼블릭 인터페이스에 그대로 노출될 수밖에 없다.




높은 결합도

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

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

 

getFee 메서드의 fee 타입을 변경한다고 가정해보자.

이를 위해서는 getFee 메서드의 반환 타입도 함께 수정해야 할 것이다.

그리고 getFee 메서드를 호출하는 클래스의 구현도 변경된 타입에 맞게 함께 수정해야할 것이다.



사실 getFee 메서드를 사용하는 것은 인스턴스 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다.



결합도 측면에서 데이터 중심 설계가 갖는 또 다른 단점은 여러 데이터 객체들을 사용하는 제어 로직이 특정 객체안에 집중되기 때문에 하나의 제어 객체가 다수의 데이터 객체에 강하게 결합된다는 것이다.





낮은 응집도

서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 말한다.

 

낮은 응집도가 설계에 문제를 일으키는 두 가지 측면.

 

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

코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것은 모듈의 응집도가 낮을 때 발생하는 대표적인 증상이다.

 

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

응집도가 낮을 경우 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.



단일 책임 원칙.

“클래스는 단 한 가지의 변경 이유만 가져야 한다.”

모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위한 원칙.



 

 자율적인 객체를 향해

캡슐화를 지켜라.

객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메서드를 통해서만 상태에 접근할 수 있어야 한다.

 

속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.

 

속성을 외부로 제공하면 ‘코드 중복’ 발생할 확률이 높고 ‘변경에 취약’하다는 문제점이 있다.

 

이러한 문제를 해결하기 위해서는 캡슐화를 강화시켜야 한다.

속성을 외부로 제공하여 변경을 다른 객체에서 수행하지 않고 객체 자신이 변경의 주체가 된다면 객체가 자기 스스로를 책임지게 된다.




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

우리가 상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게하기 위해서다.

 

객체를 설계할 때 “이 객체가 어떤 데이터를 포함해야 하는가?”라는 질문은 다음과 같은 두 개의 개별적인 질문으로 분리해야 한다.

1. 이 객체가 어떤 데이터를 포함해야 하는가?

2. 이 객체가 데이터에 대해 수행해야 하는 오퍼레이션은 무엇인가?

 

두 질문을 조합하면 1)객체의 내부 상태를 저장하는 방식과 저장된 상태에 대해 2)호출할 수 있는 오퍼레이션의 집합을 얻을 수 있다.

다시 말해 새로운 데이터 타입을 만들 수 있는 것이다.

 

위와같은 질문을 통해 내부 구현을 더 면밀하게 캡슐화한다면 데이터를 처리하는 데 필요한 메서드를 데이터를 가지고 있는 객체 스스로 구현하게 만들 수 있다.

이러한 객체들은 스스로를 책임진다고 말할 수 있다.

 

 

 하지만 여전히 부족하다

캡슐화 위반

파라미터로 특정 타입을 받게 되면 그 정보가 인스턴스 변수로 포함돼 있다는 사실을 인터페이스를 통해 외부에 노출하고 있는 것이다.

 

만약 이렇게 구성하고 속성을 변경해야 한다면 메서드의 파라미터를 수정하고 해당 메서드를 사용하는 모든 클라이언트도 함께 수정해야할 것이다.

 

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




캡슐화의 진정한 의미.

캡슐화란 변화는 어떤 것이든 감추는 것이다.

그것이 무엇이든 구현과 관련된 것이라면 말이다.

 

 

높은 결합도

클래스의 내부 구현을 제대로 캡슐화하지 못하면 그 클래스에 의존하는 다른 클래스와 결합도가 함께 높아진다.

 

 

낮은 응집도

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

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





 데이터 중심 설계의 문제점

데이터 중심의 설계가 변경에 취약한 이유는 두 가지다.

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

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





데이터 중심 설계는 객체의 행동보다는 상태에 초점을 맞춘다.

데이터는 구현의 일부라는 사실을 명심하라.

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

 

데이터 중심의 관점에서 객체는 그저 단순한 데이터의 집합체일 뿐이다.

이로 인해 접근자와 수정자를 과도하게 추가하게 되고 이 데이터 객체를 사용하는 절차를 분리된 별도의 객체 안에 구현하게 된다.

 

데이터를 먼저 결정하고 데이터를 처리하는 데 필요한 오퍼레이션을 나중에 결정하는 방식은 데이터에 관한 지식이 객체의 인터페이스에 고스란히 드러나게 된다.

 

결론적으로 데이터 중심의 설계는 너무 이른 시기에 데이터에 대해 고민하기 때문에 캡슐화에 실패하게 된다.

객체의 내부 구현이 객체의 인터페이스를 어지럽히고 객체의 응집도와 결합도에 나쁜 영향을 미치기 때문에 변경에 취약한 코드를 낳게 된다.




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

올바른 객체 지향 설계의 무게 중심은 항상 객체의 내부가 아니라 외부에 맞춰져 있어야 한다.

객체가 내부에 어떤 상태를 가지고 그 상태를 어떻게 관리하는가는 부가적인 문제다.

중요한 것은 객체가 다른 객체와 협력하는 방법이다.

 

안타깝게도 데이터 중심 설계에서 초점은 객체의 외부가 아니라 내부로 향한다.

객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출수밖에 없다.

 

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