객체지향 프로그래밍(2)
설계 품질과 트레이드오프
배경
객체지향 설계의 핵심은 역할, 책임, 협력이며, 그 중 책임
이 객체지향 애플케이션 전체의 품질을 결정하는 중요한 요소이다.
왜냐하면 책임이란 객체가 다른 객체와 협력하기 위해 수행하는 행동인데, 책임이 각 객체에 적절하게 할당되지 못한 상태에서 올바른 협력을 기대하기 어렵기 때문이다.
역할도 마찬가지로 책임의 집합이기 때문에 책임이 올바르게 되어있어야 한다.
책임
을 할당하는 작업은 응집도와 결합도 같은 설계 품질
과 깊이 연관되어 있다.
설계는 변경을 위해 존재하며, 변경에는 어떤 식으로든 비용이 발생한다.(애플리케이션의 변경은 피할수 없는 사항이며, 변경에 대한 비용을 합리적인 수순으로 조절해야된다.)
변경에 대한 비용을 조절하기 위해 각 객체들은 적절한 수준의 응집도와 결합도를 갖고 있어야한다. 이러한 것을 위해 객체를 상태가 아닌 책임(행동)에 초점을 맞춰야한다.
왜?? 객체의 상태를 중점으로 보면 내부 구현이 퍼블릭 인터페이스에 노출 되는것인가? => 추측에 의한 설계
로 이여지기 때문이다.
객체지향의 설계의 핵심은 각 객체에 적절한 책임을 부여하는 것이다.
객체에 책임을 할당하는 과정은 설계 품질에 관련이 있다.
좋은 품질의 설계는 합리적인 비용안에서 변경을 수용할 수 있는 구조이며 높은 응집도와 느슨한 결합으로 구성된다.
결합도와 응집도에 대한 원칙은 객체의 행동에 초점을 맞춰 생각하는것(객체의 상태가 아님)
상태 중심 설계와 책임 중심 설계 비교
하나의 큰 객체를 적절한 책임에 맡게 분할해야되는 상황에서 상태 중심
과 책임 중심
을 살펴보자
- 상태 중심
- 자신이 포함하고 있는 데이터를 조작하는데 필요한 오퍼레이션을 정의
- 객체가 독립된 데이터 덩어리
- 책임 중심
- 다른 객체가 요청할 수 있는 오퍼레이션을 위해 필요한 상태를 보관
- 객체를
협력
하는 공동체의 일원
상태 중심보단 책임 중심으로 객체를 분할하는것이 변경에 더 안정적인 설계를 얻을 수 있다.
왜?? 더 읽으면 정답이 나온다.
객체를 만들때 이 객체가 포함해야 하는 데이터는 무엇인가? 객체의 책임을 결정하기 전에 이런 질문의 반복에 휩쓸려 있다면 데이터 중심의 설계에 매몰돼 있을 확률이 높다.(그렇군 잘 못하고 있던 것이다.)
상태 중심의 설계
에서 상태는 구현에 속하고, 구현은 언제든지 변경될 가능성이 있다. 구현을 중점으로 객체를 분할 하였기 때문에 구현이 변경됨에 따라 인터페이스가 변경될 가능성이 크다. 따라서 변경에 취약하게 된다.(야근을 더 할 것이다.)
책임 중심의 설계
에서는 책임은 인터페이스에 속한다. 책임을 위한 구현은 캡슐화 되어 외부에 공개되지 않으므로 구현의 변경이 다른 객체에 영향이 없어 상대적으로 변경에 안정적인 설계를 얻을 수 있다. 즉 인터페이스로 객체간의 결합됨 이것의 의미는 결합도가 낮다. 그래서 다른 객체의 구현이 변경되어도 의존 관계에 있는 객체에 영향이 적다라는 말이다.
설계의 트레이드오프
설계의 좋고 나쁨을 판단하기 위해서 측정의 판단 기준을 명확하게 해야된다. 무작정 객체를 잘게 나눠서 작성하다보면 코드가 너무 어려워진다.( 나같이 머리 나쁜 사람은 코드를 따라가기 어렵다.. ) 여기서는 캡슐화, 응집도, 결합도의 기준을 갖고 판단하도록한다.
캡슐화
상태와 행동을 하나의 객체 안에 모으는 이유는 객체의 내부 구현을 외부로부터 감추기 위함이다. 객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 분리하여 의존성을 조절하는것이다. 객체지향 설계의 가장 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화하는 것이다. (그러니 인터페이스는 잘 만들어야된다. 인터페이스가 자주 변경되면 야근 각이다…)
캡슐화란 변경 가능성이 높은 부분을 객체 내부로 숨기는 추상화 기법이다.
캡슐화 대상은 변경될 수 있는 모든 것이다.
캡슐화는 결국 변경으로 부터 Side effect를 최소화 하기 위함이다.
응집도와 결합도
응집도는 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 갖고 있다라고 할 수 있다. 결합도는 의존성의 정도를 나타내며, 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지, 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만 유지하는지를 나타낸다.
좋은 설계란 변경이 발생하였을때 최소한의 비용으로 처리할 수 있어야하며, 좋은 설계를 위해서는 각 객체들이 높은 응집도
와 낮은 결합도
를 갖고 있어야한다.
즉 응집도와 결합도의 정도가 변경과 밀접한 관련이 있다라고 생각할 수 있다.
변경의 관점에서 보았을 때
응집도가 높으면 변경의 대상의 범위가 명확해지기 때문에 코드의 변경이 쉬워진다.
결합도가 낮으면 변경으로 영향을 받는 대상이 적어져 side effect를 최소화 할 수 있다.
즉 칼퇴를 할 수 있다…
응집도가 높고, 낮은 결합도는 좋은 설계라고 할 수 있다.
응집도는 변경이 발생할때 수정되어야할 모듈 수에 따라 응집도를 판단할 수 있다.(개수가 적어야 높은 응집도를 갖는다.)
결합도는 변경되기 위해 다른 모듈에서 수정되어야할 사항의 정도로 결합도를 판단할 수 있다.(개수가 적어야 낮은 결합도를 갖는다.)
캡슐화의 정도가 응집도와 결합도에 영향을 미친다. 캡슐화원칙을 지킴으로써 응집도를 높이고, 결합도를 낮출수 있기 때문에 먼저 캡슐화를 향상시키기 위해 노력해야된다. (캡슐화만 잘 해도 객체지향의 반은 먹고 들어가는거 같다.)
데이터 중심의 설계의 문제점
데이터 중심의 설계는 캡슐화 위반, 높은 결합도, 낮은 응집도의 문제를 갖고 있다.
캡슐화 위반
데이터 중심의 설계에서는 접근자와 수정자를 통해 내부 구현이 그대로 인터페이스에 노출되기 때문에 캡슐화에 위반된다.
왜 구현이 인터페이스에 노출되는가? 그것은 객체의 분리는 책임관점에서 보지 않아 협력 관계에 대한 상황을 추측을 통해서만 알 수 있기 때문이다.
객체에게 중요한것은 책임이며, 적절한 책임은 협력
이라는 문맥
을 고려할때 얻을 수 있다.
(객체간의 협력 관계를 고려하지 않고 객체의 책임을 분배할 수 없을 것이다.)
만약 협력 관계를 먼저 결정하지 않는다면 객체가 어떤 상황에서도 사용될 수 있도록 최대한 많은 인터페이스들이 생겨날 것이다. 그러면 캡슐화를 위반하게 된다.
이러한 설계방식을 추측에 의한 설계 전략
이라고 부르며 결과적으로는 내부 구현이 퍼블릭 인터페이스에 그대로 노출되는 상태가 될 가능성이 크다.
그 결과는 변경에 취약한 설계로 이여진다. 야근이 좋다면 이렇게 해라.
높은 결합도
캡슐화의 위반으로 인해 내부 구현이 인터페이스에 노출되었다 라는 것은 곧 객체를 사용하는 클라이언트가 내부 구현에 강한 결합도를 갖는 것을 의미한다. 따라서 내부 구현의 변경에 따라 클라이언트의 구현도 같이 변경될 가능성이 커진다.
낮은 응집도
데이터를 중점으로 객체를 만들었기 때문에 다른 책임을 갖는 코드가 하나의 객체 안에서 공존할 경우가 생긴다. 서로 다른 이유로 변경되는 코드가 하나의 모듈 안에 공존할 때 모듈의 응집도가 낮다고 한다. 이 말은 서로 다른 책임을 갖는 코드가 같은 모듈안에 있다라는 것이다. 높은 응집도를 갖기 위해서는 같은 책임을 갖는 코드가 하나의 모듈에 모여있어야한다. 어떤 요구사항 변경을 수용하기 위해 하나 이상의 클래스를 수정해야된다면 설계의 응집도가 낮다라는 증거이다.
단일 책임 원칙(SRP)이라는 설계 원칙에 따라 하나의 클래스는 하나의 책임만 갖도록 설계하면 높은 응집도를 갖을 수 있다.
자율적인 객체
객체는 스스로의 상태를 책임져야 하며 외부에서는 인터페이스에 정의된 메소드를 통해서만 상태에 접근할 수 있어야한다.
올바른 캡슐화
캡슐화는 설계의 제1원리이다. 객체는 자신이 어떤 데이터를 가지고 있는지를 내부에 캡슐화하고 외부에 공개해서는 안된다. 여기서 외부에 공개하는 인터페이스는 단순히 속성 하나의 값을 반환하는 접근자나 수정자를 의미하는것이 아니다. 객체의 속성을 private로 했다하더라고 접근자를 통해 접근할 수 있다면 캡슐화를 위반한것이다.
객체가
책임
져야하는 무언가를 수행하는 메소드를 인터페이스로 만들고, 이 인터페이스를 위한 구현은 모두 숨겨야한다.
무엇이던 구현(변경가능성)과 관련된 것이라면 감추는 것이 캡슐화이다.
스스로 자신의 데이터를 책임지는 객체
상태와 행동을 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리
할 수 있게 하기 위함이다.
객체가 갖는 데이터(상태)가 외부에 의해 변경된다면 스스로 자신의 데이터를 책임질 수 없는 객체이다.
따라서 객체가 갖는 데이터를 이용해서 수행할 수 있는 오퍼레이션을 인터페이스로 만들어 객체 스스로 자신의 데이터를 책임질 수 있는 상태로 만들어야된다.
객체의 책임이 무엇인가? => 객체가 책임 수행에 필요한 데이터는 무엇인가? => 이 데이터에 대해 수행해야되는 오퍼레이션은 무엇인가? 라는 흐름으로 객체를 설계해야된다.
인터페이스의 구현은 어떻게 테스트하는가?
인터페이스의 구현이 private 메소드여서 테스트가 어렵다. 이건 어떻게 하는가? 인터페이스를 호출하여 private 메소드를 테스트할 수 있도록한다. 왜냐하면 의존성을 같은 객체들이 인터페이스를 통해서 연결되어있기 때문에 의존성을 갖는 객체까지 검사할려면 이렇게 해야된다. 또한 구현이 변경될때마다 테스트 코드도 변경이 되기 때문에 테스트 코드에 구멍이 발생할 수 도 있다.