[오브젝트] chap02을 읽고

이번 장은 이 책에서 이해하게 될 다양한 주제들을 얕은 수준으로 가볍게 살펴보는 장이다. 실제적인 코드는 책에서 살펴보도록 하고 핵심 내용만 정리할 예정이다.

객체 지향 설계 시 고려할 점들

가장 먼저 고려해야하는 점

객체지향 프로그램을 작성할 때 가장 먼저 고려하는 것이 무었인가? 대부분 어떤 클래스(class)가 필요한지 고민할 것이다. 하지만 이는 안타깝게도 객체지향의 본질과는 멀다. 객체지향은 말 그대로 객체를 지향하는 것이기 때문이다.

그렇다면 객체를 지향하는것은 무엇일까? 말 그대로이다. 클래스가 아닌 객체에 초점을 맞추는 것이다.

객체 지향적으로 생각하고 그 결과로 클래스라는 결과물의 윤곽을 잡아라.

클래스는 공통적인 상태와 행동을 공유하는 객체를 추상화한 것이다. 따라서 클래스의 윤곽을 잡기위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지 먼저 결정해야한다.

객체지향적인 사고는 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 보는 것이다. 객체들의 모양과 윤곽이 잡히면 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이 타입을 기반으로 클래스를 구현하자.

도메인의 구조를 따르는 프로그램 구조

문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다. 영화 예매 시스템을 만들고 싶다면 상영, 영화, 예매, 할인 정책(금액 할인 정책, 비율 할인 정책), 할인 조건(순번 조건, 기간 조건) 등이 있을 것이다.

도메인은 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야이다. 객체지향 패러다임은 요구사항을 분석하는 초기 단계부터 마지막까지 객체라는 추상화 기법을 사용한다.

문제 해결과 요구사항은 직결되기 때문에 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다. 따라서 객체지향 프로그램을 작성하는 데에 도메인이라는 개념이 도움이 될 것이다.

자율성

객체의 자율성

객체지향 이전의 패러다임과는 다르게 객체지향은 객체라는 단위에 데이터와 가능을 한 덩어리로 묶는다. 이를 캡슐화라고 한다. 여기서 한걸음 더 나아가 외부에서의 접근을 통제하는 접근 제어접근 수정자(public, protected, private 등과 같은)로 구현한다.

이렇게 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서다. 객체지향의 핵심은 스스로 상태를 관리하고, 판단하고, 행동하는 자율적인 객체들의 공동체를 구성하는 것이다. 이런 객체들의 공동체를 만들기 위해서는 외부의 간섭을 최소화 해야한다.

외부에서는 객체가 어떤 상태에 놓여 있는지, 어떤 생각을 하고 있는지 알아서는 안 되며, 결정에 직접적으로 개입하려고 해서도 안된다. 그저 객체에게 원하는 것을 요청하고 믿고 기다려야한다.

기본적으로 객체간의 소통방식이 위와같이 정립되면 자율적인 객체를 만들 수 있다. 인터페이스와 구현의 분리가 잘 이루어 진다면 자율적인 객체를 만들 수 있다.

프로그래머의 자유

클래스 작성자는 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 꽁꽁 숨겨야 한다. 이렇게 해야 작성자는 클라이언트 프로그래머에 대한 영향을 고려하지 않고도 숨겨놓은 내부 구현을 마음대로 변경할 수 있게 된다. 이를 구현 은닉이라고 한다.

또한 클라이언트 프로그래머의 입장에서도 내부의 구현은 무시한 채 인터페이스만 알고 있어도 클래스를 사용할 수 있기 때문에 머릿속에 담아둬야 하는 지식의 양을 줄일 수 있다.

설계가 필요한 이유는 변경을 관리하기 위해서이다. 변경 가능성이 있는 세부적인 구현 내용을 숨겨 변경으로인한 파급효과를 관리할 수 있게 된다.

협력의 방법

객체는 다른 객체의 인터페이스로 공개된 행동을 수행하도록 요청할 수 있다. 요청을 받은 객체는 자율적인 방법에 따라 응답한다. 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메세지를 전송하고 수신하는 것이다. 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.

수신된 메시지를 처리하기위한 자신만의 방법을 메서드라고 부른다.

메시지와 메서드를 구분하는 것은 중요하다. 어떤 객체에게 메시지를 전송하면 메시지를 전송받은 객체는 자신만의 메서드로 응답한다. 이로써 다형성의 개념이 출발한다.

상속과 다형성

컴파일 시간 의존성과 실행 시간 의존성

코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다. 이로써 유연하고 쉽게 재사용 가능하고, 확장 가능한 객체지향 설계를 만들 수 있다.

하지만 실행 시간 의존성과 컴파일 시간 의존성이 다를 수록 코드를 이해하기 어려워 진다. 코드를 이해하기 위해 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다. 즉 유연함과 이해가 가능한 코드 사이의 트레이드오프이다.

차이에 의한 프로그래밍

기존의 어떤 클래스와 매우 흡사한 클래스를 추가하고 싶을 때, 기존의 코드를 재사용 하는 일을 상속이라고한다. 상속을 이용하면 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다. 이처럼 부모 클래스와 다른 부분만들 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍이라고 한다.

상속과 인터페이스

상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려 받을 수 있기 때문이다. 즉, 상속의 목적을 구현의 재사용이라고 생각하는 것은 오해이다.

상속을 통해 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있게 된다. 즉 부모의 자리를 대체할 수 있다. 이처럼 자식 클래스가 부모 클래스를 대신하는 것을 업캐스팅이라고한다.

다형성

메시지와 메서드는 다른 개념이다. 동일한 메시지를 전송한다고 해도 실제로 어떤 객체가 메시지를 수신하냐에 따라서 어떤 메서드가 실행될 것인지 달라진다. 이를 다형성이라고한다.

컴파일 시간 의존성(정적 바인딩)과 실행 시간 의존성(지연, 동적 바인딩)을 다르게 만들 수 있는 객체지향의 특성을 이용해 서로 다른 메서드를 실행한다. 그러기 위해서는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다. 다시 말해 인터페이스가 동일해야한다. 두 클래스의 인터페이스를 동일하게 만드는 방법이 바로 상속인 것이다.

상속을 이용해 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다.

상속을 구현 상속이 아니라 인터페이스를 상속을 위해 사용해야한다. 대부분의 사람들은 코드 재사용을 상속의 주된 목적이라고 생각하지만 이것은 오해다. 인터페이스를 재사용할 목적이 아니라 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.

인터페이스와 상속의 차이

구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을 때가 있는데 이를 위해 인터페이스라는 요소를 제공한다. 말 그대로 구현에 대한 고려 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 외부 인터페이스를 정의한 것이다.

추상화의 힘

추상화의 계층만 따로 뗴어놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있게 된다. 추상화를 사용하면 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다. 즉 세부사항에 억눌리지 않고 도메인의 중요한 개념을 설명할 수 있게한다.

또한 추상화를 이용하면 좀 더 유연한 설계를 할 수 있다. 새로운 자식 클래스들은 추상화를 이용해서 정의한 상위의 협력 흐름을 그대로 따를 수 있다. 즉, 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있게된다.

예외 케이스

추상화에 의해 프로그램을 작성하다 보면 예외 케이스가 생기기 마련이다. 예외 케이스를 최소화 시키고 일관성을 유지할 수 있는 설계가 좋은 설게인데, 객체지향을 이용하면 기존의 구조는 따르면서도 예외케이스도 효과적으로 처리할 수 있다.

예를들어, 영화 예매 시스템에서 할인정책에 따라 요금을 할인한다고 추상화했다. 그런데 할인 정책이존재하지 않는 영화가 존재할 수 있다. 이러면 할인 정책이 null일 때 할인액을 0으로 하도록 기존의 코드를 수정해야한다. 그런데 기존의 코드를 변경하지 않고 기존의 코드를 상속하는 NoneDiscountPolicy를 구현해 예외 케이스를 기존의 흐름에서 처리할 수 있다.

이 예외 케이스를 기존의 흐름에 집어 넣는것은 작은 부작용을 낳는다. 기존의 추상화에 대한 인터페이스의 구현이 예의 케이스를 처리하는 클래스에서는 불필요하지만 상위의 인터페이스이다 보니 불필요하게 구현해야할 수 있다. 즉, 기존의 추상화 개념이 새로운 예외 케이스에 딱 들어맞지 않는 경우이다.

이런 경우에는 새로운 추상화 계층을 하나 더 추가하여 문제를 해결할 수 있다. 기존의 추상화 계층 위에 하나 더 추상화 계층을 만들어 예외 케이스를 새로운 추상화 계층에 상속시킨다.

이렇게 하면 기존의 추상화 개념에 예외 케이스를 껴넣다 발생하는 개념의 혼란과 결합을 해소할 수 있다.

하지만 사소한 개념의 혼란을 해소하기 위해 새로운 추상화 계층을 추가하는 것은 과하다고 생각할 수 있다. 인터페이스 생성에 대한 부담과 완벽한 개념적 분리에 대한 트레이드오프이다.

상속과 협력

일반적으로 상속보다는 합성이 더 좋은 방법이라고 이야기한다. 그 이유는 무엇일까?

상속

상속은 두 가지 관점에서 설게에 좋지 못한 영향을 끼친다.

첫째는 상속이 캡슐화를 위반한다는 것이다. 상속을 위해서는 부모클래스의 내부 구조를 잘 알고 있어야 한다. 즉, 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화가 약화된다. 캡슐화의 약화는 자식 클래스가 부모 클래스에 강하게 결합되게 만든다. 따라서 부모 클래스가 변경될 때, 자식 클래스도 함께 변경해야할 확률을 높인다.

두 번째 단점은 설계가 유연하지 않다는 것이다. 상속은 부모 클래스의 자식 클래스의 관계를 컴파일 시점에 결정한다. 따라서 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.

예를들어 같은 Movie클래스를 상속한 PercentDiscountMovieAmountDiscountMovie가 존재한다고 하면, 실행 시점에 PercentDiscountMovieAmountDiscountMovie로 변경하는 것은 어려운 일이다. 이것은 부모 클래스와 자식클래스가 강하게 결합되어 있기 때문이다. 하지만 상속 대신, 합성을 사용하면 이를 쉽게 해결할 수 있다.

합성

인터페이스에 정의된 메세지만들 통해 코드를 재사용하는 방법을 합성이라고 한다. 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화 할 수 있다. 또한 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다. 따라서 코드 재사용을 위해서는 상속보다는 합성이 더 좋은 방법이다.

이처럼 코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳다. 그렇다면 상속은 언제 사용해야하는가? 다형성을 위해 인터페이스를 재사용하는 경우에는 상속을 사용하자.

태그:

카테고리:

업데이트:

댓글남기기