오브젝트


데이터 중심 관점의 폐해

앞선 챕터에서 역할, 책임, 협력 의 관점에서 객체지향 설계를 하는것의 중요성을 살펴보았다.

지금까지 우리는 객체는 자율적으로 판단하고 행동하며 스스로를 책임지는 존재 라고 배웠다. 즉, 자율적으로 행동 하는 객체에 대해 공부했다.

만일 객체를 단순히 데이터의 집합으로 본다면 어떻게 될까?

가장 먼저 캡슐화 가 저해된다. 왜일까? 그 이유를 살펴보자.

객체를 데이터 중심의 관점으로 바라보게 된다면 내부 구현이 퍼블릭 인터페이스에 노출 될 확률이 높아진다. 좀 더 구체적인 예시를 든다면 객체가 가진 데이터 에 대한 구체적인 명칭을 활용하게끔 퍼블릭 인터페이스를 설계할 가능성이 높아진다.

customer.getBag().setMoney() 와 같은 예시를 들 수 있겠다. customer 객체의 Bag 이라는 데이터의 Money 라는 데이터를 활용하려는 목적이 너무 상세한 수준으로 인터페이스에 들어나게 되면서 캡슐화가 저해된다.

또한 데이터는 구현에 속한다. 그리고 구현은 불안정 하기 때문에 변하기가 쉽다. 만일 데이터를 객체 분할의 중심축으로 삼게 된다면 구현에 관한 세부사항이 객체의 인터페이스에 스며들게 되고 캡슐화의 원칙이 무너지게 된다. 즉, 상태 변경으로 인터페이스의 변경이 초래될 것이고 인터페이스에 의존하는 다른 객체들에게 변경의 여파가 미치는 상황이 오게 된다.

이 외에도 여러가지 이유가 있지만 중요한 것은 객체의 책임에 초점 을 맞춰야 한다는 것이다. 객체와 객체의 상호작용인 책임 에 초점을 맞추게 된다면 자연스럽게 응집도는 높고 결합도는 낮은 설계가 탄생할 것이다.

왜그럴까?

객체의 책임, 즉 행동에 초점을 맞춤으로써 행동을 수행하기 위해 알아야 하는 데이터가 파생되고 결국 자신의 데이터를 스스로 책임지는 응집도 높은 객체가 탄생하기 때문이다. 그리고 데이터를 스스로 책임지고 처리하기 때문에 다른 객체들에게 불필요한 의존을 하지 않아 결합도는 낮게 유지된다.

이번에는 좀더 세부적으로 캡슐화, 응집도, 결합도 측면에서의 객체지향 설계를 살펴보자.


캡슐화

상태와 행동을 하나의 객체안에 모으는 이유는 객체의 내부 구현을 외부로 부터 감추기 위함 이다. 여기서 내부 구현이란 나중에 변겨오딜 가능성이 높은 어떤 것 을 가리킨다. 객체지향이 강력한 이유는 한 곳에서 일어난 변경이 전체 시스템에 영향을 끼치지 않도록 파급효과를 적절히 조절할 수 있는 장치를 제공하기 때문이다. 객체를 사용하면 변경 가능성이 높은 부분은 내부에 숨기고 외부에는 상대적으로 안정적인 부분만 공개함 으로써 변경의 여파를 통제할 수 있다.

변경될 가능성이 높은 부분을 구현 이라 부르고 상대적으로 안정적인 부분을 인터페이스 라고 부른다는 사실을 기억하자. 객체를 설계하기 위한 가장 기본적인 아이디어는 변경의 정도에 따라 구현과 인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절 하는 것이다.

지금까지 설명한 내용에서 알 수 있는 것처럼 객체지향에서 가장 중요한 원리는 캡슐화 이다.

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

객체지향 설계의 가장 중요한 원리는 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 캡슐화 하는 것 이다. 즉, 캡슐화가 중요한 이유는 불안정한 부분과 안정적인 부분을 분리해서 변경의 영향을 통제 할 수 있기 때문이다.

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


응집도와 결합도

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

결합도는 의존성의 정도 를 나타내며 다른 모듈에 대해 얼마나 많은 지식을 갖고있는지를 나타내는 척도다. 어떤 모듈이 다른 모듈에 대해 너무 자세한 부분까지 알고있다면 두 모듈은 높은 결합도를 가진다. 어떤 모듈이 다른 모듈에 대해 꼭 필요한 지식만 알고 있다면 두 모듈은 낮은 결합도를 가진다. 객체지향 관점에서 결합도는 객체 또는 클래스가 협력에 필요한 적절한 수준의 관계만을 유지 하고 있는지를 나타낸다.

응집도와 결합도의 의미를 이해하기 위한 첫 걸음은 두 개념 모두 설계/변경과 관련이 있다는 사실을 이해하는 것 이다. 일반적으로 좋은 설계란 높은 응집도와 낮은 ㅁ=결합도를 가진 모듈로 구성된 설계를 의미한다. 다시말해 어플리케이션을 구성하는 각 요소의 응집도가 높고 서로 느슨하게 결합돼 있다면 그 어플리케이션은 좋은 설계를 가졌다고 볼 수 있다.

좋은 설계란 오늘의 기능을 수행하면서 내일의 변경을 수용할 수 있는 설계다. 그리고 좋은 설계를 만들기 위해서는 높은 응집도와 낮은 결합도를 추구해야 한다. 따라서 응집도와 결합도는 변경과 관련된 것 이다.

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

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

  • 하나의 변경을 수용하기 위해 모듈 전체가 함께 변경된다면 응집도가 높은 것

  • 하나의 변경을 수용하기 위해 모듈의 일부만 변경된다면 응집도가 낮은 것

  • 하나의 변경에 대해 하나의 모듈만 변경된다면 응집도가 높은 것

  • 하나의 변경에 대해 다수의 모듈이 함께 변경되어야 한다면 응집도가 낮은 것

위 설명에서 알 수 있듯이 응집도가 높은 설계에서는 하나의 요구사항 변경을 반영하기 위해 오직 하나의 모듈만 수정 하면 된다. 반면 응집도가 낮은 설계에서는 하나의 원인에 의해 변경해야 하는 부분이 다수의 모듈에 걸쳐 분산 되어 있기 때문에 여러 모듈을 동시에 수정해야 한다. 응집도가 높을 수록 변경의 대상과 범위가 명확해지기 때문에 코드를 변경하기 쉬워진다.

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

영향을 받는 모듈의 수 이외에도 변경의 원인 을 이용해 결합도의 개념을 설명할 수도 있다. 내부 구현을 변경했을 때 다른 모듈에 영향을 미치는 경우 에도 결합도가 높다고 표현한다. 반면 퍼블릭 인터페이스를 변경했을 때만 다른 모듈에 영향을 미치는 경우에는 결합도가 낮다고 표현한다. 따라서 클래스의 내부 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 유지 할 수 있다.

다시한번 강조하지만 응집도와 결합도는 변경과 관련이 깊다. 어떤 설계를 쉽게 변경할 수 있다면 높은 응집도를 가진 요소들로 구성돼 있고 요소들 사이의 결합도가 낮을 확률이 높다. 응집도와 결합도를 변경의 관점 에서 바라보자


데이터 중심 설계의 문제점

책임 중심의 설계와 데이터 중심 설계의 근본적인 차이점은 캡슐화 를 다루는 방식이다. 데이터 중심의 설계에서는 캡슐화를 위반하고 객체의 내부 구현을 인터페이스의 일부로 만든다. 반면 책임 중심 설계는 객체 내부 구현을 안정적인 인터페이스 뒤로 캡슐화 한다.

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

데이터 중심의 설계는 캡슐화를 위반하기 쉽기 때문에 책임 중심 설계에 비해 응집도가 낮고 결합도가 높은 객체들을 양산하게 될 가능성이 높다.

  • 캡슐화 위반

  • 높은 결합도

  • 낮은 응집도


캡슐화 위반

데이터 중심 설계가 왜 캡슐화를 위반하기 쉬울까? 캡슐화는 객체의 변하기 쉬운(불안정한) 내부 구현 을 안정적인(쉽게 변하지 않는) 퍼블릭 인터페이스 뒤로 감추는 것이다. 여기서 쉽게 오해하기 쉬운 부분이 데이터(상태) 는 내부 구현이므로, private 접근자로 지정하고 getter/setter 를 통해 데이터에 접근함으로써 캡슐화를 유지한다고 생각할 수 있다.

결론부터 말하자면 단순 getter/setter는 캡슐화가 아니다. 오히려 캡슐화의 위반이다. 캡슐화가 의미하는 내부 구현 이란 단순히 데이터만을 의미하는 것이 아니다. 내부 프로세스나 로직이 상세한 수준으로 인터페이스에 드러난다면 이 역시도 내부 구현이 외부로 드러남을 의미한다. 돌아와서 getter/setter에 이 의미를 적용한다면 인터페이스만 보고도 내부에 어떤 데이터들이 존재하는지 상세한 수준으로 드러남을 알 수 있다.

위와 같은 캡슐화의 원칙을 위반하게 되는 경우가 생기는 근본적인 원인은 객체가 수행할 책임이 아니라 내부에 저장할 데이터에 초점을 맞췄기 때문이다. 객체에게 중요한 것은 책임이다. 그리고 구현을 캡슐화 할 수 있는 적절한 책임은 협력이라는 문맥을 고려할 때만 얻을 수 있다.

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


높은 결합도

변경의 관점에서 결합도는 어떤 객체의 내부 구현을 변경할 때 해당 객체에 의존하는 외부 객체들이 함께 변경되는 정도 라고 하였다. 그렇다면 데이터 중심 설계에서는 왜 높은 결합도를 갖게 될까? 이 역시도 캡슐화 와 관련이 있다.

데이터 중심의 설계에서는 내부 구현이 퍼블릭 인터페이스에 노출되어 내부 구현을 인터페이스의 일부로 만들기 때문에 캡슐화가 저해 된다라고도 배웠다. 이 때문에 객체의 내부 구현이 변경되면 퍼블릭 인터페이스 역시 변경될 가능성이 높아지고 이에 따라 인터페이스에 의존하는 외부 객체들도 같이 변경되는 상황이 발생한다.

Money fee = movie.getFee().minus(discountAmount).times(audienceCOunt);

위 코드를 살펴보면 fee의 값을 계산하기 위해 movie 객체의 getFee() 메서드를 호출하며 계산된 결과를 Money 타입의 fee에 저장한다. 여기서 만일 fee의 타입을 Money에서 Won으로 변경한다고 가정해보자. 이를 위해서는 movie 객체의 getFee() 메서드의 반환 타입도 함께 수정해야 할 것이다. 그리고 getFee() 메서드를 호출하는 외부 객체들의 구현도 변경된 Won 타입에 맞게 수정해야 할 것이다.

fee의 타입 변경으로 인해 협력하는 외부 객체들이 함께 변경되기 때문에 getFee() 메서드는 fee를 정상적으로 캡슐화 하지 못한다. 사실 getFee() 메서드를 사용하는 것은 인스턴스 변수 fee의 가시성을 private에서 public으로 변경하는 것과 거의 동일하다. 이처럼 데이터 중심 설계는 객체의 캡슐화를 약화시키기 때문에 클라이언트가 객체의 구현에 강하게 결합된다.

데이터 중심의 설계는 전체 시스템을 하나의 거대한 의존성 덩어리로 만들어 버리기 때문에 어떤 변경이라도 일단 발생하고 나면 시스템 전체가 요동칠 수 밖에 없다.


낮은 응집도

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

응집도를 살펴볼때 모듈의 내부 변경 에 초점을 맞추면 보다 쉽게 보인다. 만일 어떤 모듈 내부 변경의 원인이 한 가지라면 응집도는 높다.(밀접한 책임으로 연관되어있다.) 반면에 내부 변경의 원인이 하나가 아니라면 이는 해당 모듈의 책임이 밀집되어있지 않음을 의미한다.

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

  • 변경의 이유가 서로 다른 코드들을 하나의 모듈안에 뭉쳐놓았기 때문에 변경과 아무런 상관이 없는 코드들이 영향을 받게 된다. 예를들어 할인 정책을 선택하는 코드와 할인 조건을 판단하는 코드가 함께 존재하기 때문에 새로운 할인 정책을 추가하는 작업이 할인 조건에도 영향을 미칠 수 있다. 어떤 코드를 수정한 후에 아무런 상관도 없던 코드에 문제가 발생하는 것은 응집도가 낮을 때 발생하는 대표적인 증상이다.

  • 하나의 요구사항 변경을 반영하기 위해 동시에 여러 모듈을 수정해야 한다. 응집도가 낮을 경우 다른 모듈에 위치해야 할 책임의 일부가 엉뚱한 곳에 위치하게 되기 때문이다.


캡슐화를 지켜라

캡슐화는 설계의 제 1원리다. 데이터 중심의 설계가 낮은 응집도와 높은 결합도라는 문제로 몸살을 앓게 된 근본적인 원인은 바로 캡슐화의 원칙을 위배했기 때문이다.

객체는 자신이 어떤 데이터를 가지고 있는지를 내부에 캡슐화하고 외부에 공개해서는 안된다.

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

여기서 말하는 메서드는 단순히 속성 하나의 값을 반환하거나 변경하는 접근자나 수정자를 의미하는 것이 아니다.(getter/setter) 객체에게 의미있는 메서드는 객체가 책임져야 하는 무언가를 수행하는 메서드다. 속성의 가시성을 private으로 설정했다고 해도 접근자와 수정자를 통해 속성을 외부로 제공하고 있다면 캡슐화를 위반하는 것이다.


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

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

스스로 자신의 데이터를 책임지는 메서드가 존재한다고 해서 캡슐화가 완벽하게 되는 것은 아니다. 일례로 Moive 객체에 calculateAmountDiscountedFee(), calculatePercentDiscountedFee(), calculateNoneDiscountedFee() 메서드가 존재한다고 가정해보자. 각 메서드는 Movie 객체의 내부 데이터를 이용하여 각 할인 정책에 따라 할인 요금을 계산한다. 이렇게만 보면 스스로 데이터를 책임지기 때문에 캡슐화가 잘되었다고 생각할 수 있다.

하지만 위의 메서드들은 각각 금액 할인 정책, 비율 할인 정책, 미적용 할인 정책 이 존재한다는 구현 내용을 외부에 노출시키고 있다. 만일 할인 정책이 변경되거나 추가된다면 위 메서드들의 인터페이스는 변경될 것이고 그 파급효과로 Movie 객체에 의존하는 외부 객체들 역시 변경을 피할 수 없게된다.

또한 할인 여부를 판단하는 isDiscountable(DayOfWeek dayOfWeek, LocalTime time) // __isDiscountable(int sequence) 라는 메서드가 있다고 가정해보자. 위 메서드는 파라미터 타입으로 각 할인 여부 판단에 어떤 데이터가 필요한지 인터페이스에 고스란히 드러내고 있다. 만일 할인 정책이 달라진다면 위 메서드에 의존하는 외부 객체들도 변경되게 될 것이다.


캡슐화의 진정한 의미

위 예시는 캡슐화는 단순히 객체 내부의 데이터를 외부로 부터 감추는 것 이상의 의미를 갖는다 는 점을 잘 보여준다.

캡슐화는 변겨오딜 수 있는 어떤 것이라도 감추는 것을 의미한다. 내부 속성을 외부로부터 감추는 것은 데이터 캡슐화 라고 불리는 캡슐화의 한 종류일 뿐이다.

다시한번 강조하지만 캡슐화란 변할 수 있는 어떤 것이라도 감추는 것이다. 그것이 속성의 타입이건, 할인 정책의 종류건 상관 없이 내부 구현의 변경으로 인해 외부의 객체가 영향을 받는다면 캡슐화를 위반한 것이다.

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


데이터 중심 설계의 문제점

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

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

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

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


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

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

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

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


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

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

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

객체가 내부에 어떤 상태를 갖고 그 상태를 어떻게 관리하는지는 부가적인 문제다. 중요한 것은 객체가 다른 객체와 협력하는 방법이다.

데이터 중심 설계에서는 초점이 객체의 외부가 아니라 내부로 향한다. 실행 문맥에 대한 깊이있는 고민 없이 객체가 관리할 데이터의 세부 정보를 먼저 결정한다. 객체의 구현이 이미 결정된 상태에서 다른 객체와의 협력 방법을 고민하기 때문에 이미 구현된 객체의 인터페이스를 억지로 끼워맞출 수 밖에 없다.

참고 및 출처

  • 오브젝트