All Articles

[Book] 오브젝트 정리 Chapter 1 - 3

변경은 언제든 찾아올 수 있다. 변경은 피할 수 없다. 이러한 사실을 깨달은 사람들은 변경에 유연하게 대응할 수 있는 방법을 고민해왔고 이에 다영한 해결법이 제시되었다. 이번에 다루게 될 객체지향 프로그래밍도 그 중 하나라고 할 수 있다.

객체지향 프로그래밍. 익숙하지만 제대로 이해하고 활용하고 있다고 얘기하기는 어려운 그런 녀석이었다. Java 를 사용하면 객체지향일까? 클래스 개념을 활용하면 객체지향일까? 안타깝게도 Java 내에서 class 를 사용해도 얼마든지 절차지향적인 코드가 나올 수 있고, 실제로도 그런 코드를 많이 양산해왔다. 객체지향적인 설계에 대한 고민이 깊어질때 즘 ‘객체지향의 사실과 오해’ 책으로 알게된 조영호님의 신간이 나왔음을 들었고 ‘오브젝트’ 책을 읽기 시작했다. 이번 포스팅은 챕터 1 ~ 3 을 읽고 정리하는 내용으로 채웠다.

객체간의 협력과 책임 (설계 레벨)

OOP 설계의 핵심은 협력을 구성하기 위해 적절한 객체를 찾고 적절한 책임을 할당하는 과정에서 드러난다. 클래스와 상속은 객체들의 책임과 협력이 어느 정도 자리를 잡은 후에 사용할 수 있는 구현 메커니즘일 뿐이다.

잘못된 방식

OOP 를 막 입문하면 가장 흔하게 하는 실수가 객체의 행동이 아닌 상태에 초점을 맞추는 것이다. 객체의 상태를 먼저 결정하고, 상태에 필요한 행동을 결정한다. 이런 방식은 객체의 내부 구현이 객체의 퍼블릭 인터페이스에 노출되도록 만들기 때문에 캡슐화를 저해한다. 또한 객체의 내부 구현은 언제든 바뀔 수 있기 때문에 결국 외부에 변화가 전파된다.

객체지향 관점에서 객체의 구성 방식

→ 객체는 어떤 협력을 참여하는가

→ 협력을 위해서 어떤 행동을 해야하는가

→ 행동하기 위해서 어떤 상태를 가져야하는가

1. 객체간의 협력과 수행해야할 책임을 파악할 것

어플리케이션안에 어떤 객체가 필요하다면 그 이유는 단 하나여야 한다. 객체가 어떤 협력에 참여하고 있기 때문이다. 그리고 객체가 협력에 참여할 수 있는 이유는 협력에 필요한 적절한 행동을 보유하고 있기 때문이다.

객체가 책임을 할당하는 데 필요한 메세지를 먼저 식별하고, 메세지를 처리할 객체를 나중에 선택하는 것이 중요하다. 이런 방식으로 객체를 구성할 경우 다음과 같은 이점이 있다.

  1. 객체가 최소한의 퍼블릭 인터페이스를 가질 수 있게 된다.
  2. 객체의 퍼블릭 인터페이스는 무엇을 하는지만 나타낼 뿐, 어떻게 수행하는지가 노출되지 않는다.

2. 책임을 수행하기 위해 필요한 상태 및 내부 구현 정의

객체의 퍼블릭 인터페이스가 정해졌다면, 다음으로는 퍼블릭 인터페이스로 들어온 요청을 객체 스스로 처리할 수 있도록 구성해야한다. 즉, 내부 구현 및 내부 구현에 필요한 상태의 정의가 필요하다. 객체의 상태는 그 객체가 행동을 수행하는 데 필요한 정보가 무엇인지로 결정된다. 객체는 자신의 상태를 스스로 결정하고 관리하는 자율적인 존재이기 때문이다.

협력관계속에서 다른 객체에게 무엇을 제공해야 하고 다른 객체로부터 무엇을 얻어야 하는지를 고민해야만 훌륭한 책임을 수확할 수 있다.

3. 책임을 조합하여 역할을 정의

어느정도 객체의 책임이 정의되었다면 객체의 역할을 정의할 수 있다. 코드레벨에서는 Java 기준으로 ‘interface, abstract class 를 구현한다.’ 라고도 얘기할 수 있다. 객체의 역할을 정의해주는 것이 중요한 이유는 동일한 협력을 수행하는 객체들을 추상화할 수 있기 때문이다. 협력관계가 역할이라는 추상화된 형태로 묶일 경우 당연히 변화에도 대응하기 쉬워진다. 책에서는 객체의 역할에 대해 다음과 같이 비유했다.

  • 서로 다른 배우들(객체)이 동일한 배역(역할)을 연기할 수 있다.
  • 하나의 배우(객체)가 서로 다른 배역(역할)을 연기할 수 있다.

협력이라는 문맥 안에서 역할은 특정한 협력에 참여해서 책임을 수행하는 객체의 일부다. 역할은 객체의 구조나 상태에 의해 정의될 수 없으며, 시스템의 문맥 안에서 무엇을 하는지에 의해서만 정의될 수 있음을 주의해야한다.

번외로, 오직 한 종류의 객체만 협력에 참여하는 상황에서 역할이라는 개념을 고려하는 것이 유용할까? 역할이라는 개념을 생략하고 직접 객체를 이용해 협력을 설계하는 것이 더 좋지 않을까? 라는 궁금증이 생길 수 있다. 책에서 내린 결론은 다음과 같다.

“협력에 적합한 책임을 수행하는 대상이 한 종류라면, 간단하게 객체를 이용하면 된다. 만약 여러 종류의 객체가 협력에 이용될 수 있다면 협력의 대상은 역할이 될 것이다.

하지만 설계 초반에는 적절한 책임과 협력의 큰 그림을 탐색하는 것이 가장 중요한 목표여야 하고 역할과 객체를 명확하게 구분하는 것은 그렇게 중요하지는 않다는 것이다.

즉, 상황에 맞게 역할(추상화된 형태) 혹은 객체 (구체적인 형태)를 잘 선택해서 사용하되, 어차피 바뀔 수 있는 내용이므로 설계 초기에는 크게 신경쓰지 않아도 된다고 이야기 하고 있다.

객체 지향 구현 기법 (구현 레벨)

캡슐화

클래스를 구현하거나 개발된 클래스를 사용할 때 가장 중요한 것은 클래스의 경계를 구분 짓는 것이다. 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 한다.캡슐화 된 객체는 상태는 숨기고 행동만 public 인터페이스로 공개한다.

객체의 외부와 내부를 구분하면 클래스를 사용하는 입장에서 알아야할 지식의 양이 줄어들고, 클래스 구현자는 내부 구현을 변경할 수 있는 폭이 넓어진다.

다형성

객체를 추상화된 형태로 제공할 수 있다. 추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다. (구체적인 상황은 항상 변경된다.)

주의할 점은 다형성을 이용하여 설계를 유연하게 가져갈수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다. 무조건 유연한 설계도, 무조건 읽기 쉬운 코드도 정답이 아니다.

상속

상속이 가치있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다. (다형성 - 업캐스팅의 활용)

주의할 점으로 상속은 객체지향 프로그래밍에서 코드를 재사용하기 위해 널리 사용되는 되지만 두가지 관점에서 설계에 안좋은 영향을 미친다. (이펙티브 자바에서도 동일한 내용의 챕터가 존재한다.) ****

구현의 재사용성으로 이용된 상속은 변경에 취약하고, 부모클래스의 캡슐화를 깨기때문에 지양해야한다. 따라서 상속의 사용 목적은 구현의 재사용보다 인터페이스를 재사용하는 것에 초점을 맞춰야 한다.

하지만 실제 개발시에는 상속을 통해 내부 구현을 재사용해야할 일이 생기기 마련이다. 따라서 상황에 맞게 적절히 활용하는 지혜가 요구된다.

상황별로 권장되는 방법은 다음과 같다.

  • 구현내용 재사용 X, 다형성 활용 O : interface (Java)
  • 구현내용 재사용 O, 다형성 활용 O : abstract class (Java)
  • 구현내용 재사용 O, 다형성 활용 X : composition (디자인 패턴)

설계의 트레이드 오프

  • 어떤 기능을 설계하는 방법은 한가지 이상일 수 있다.
  • 동일한 기능을 한 가지 이상의 방법으로 설계할 수 있기 때문에 결국 설계는 트레이트 오프의 산물이다. 어떤 경우에도 모든 상황을 만족시킬 수 있는 설계를 만들 수는 없다.

구현과 관련된 모든 것들이 트레이드 오프의 대상이 될 수 있다. 작성하는 모든 코드에는 합당한 이유가 있어야 한다. 비록 아주 사소한 결정이더라도 트레이드 오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다.