ch2. 객체지향 프로그래밍

조영호님의 오브젝트: 코드로 이해하는 객체지향 설계
를 읽고 정리한 내용입니다.


01. 영화 예매 시스템

영화상영이라는 용어를 구분하자.
두 용어의 차이가 중요한 이유는 사용자가 실제로 예매하는 대상은
영화가 아니라, 상영이기 때문이다.


02. 객체지향 프로그래밍을 향해

협력, 객체, 클래스

이제부터는 예매 시스템보다는 객체 지향에 대해 알아보자.
대부분의 사람들은 클래스를 결정한 후에 클래스에 어떤 속성과 메서드가 필요한지 고민한다.
(객체 = 클래스라는 관점) -> 객체지향의 본질과는 거리가 멀다.
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
객체들의 모양과 윤곽이 잡히면, 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고
이 타입을 기반으로 클래스를 구현한다.

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

문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
ex) 설계 순서의 예시
일반적으로 RDB를 사용하는 실무에서의 설계 순서는 대부분 테이블을 만들고 그 테이블을
기반으로 클래스를 만드는 설계하는 반면,
이 책에서는 도메인을 먼저 구성한 후, 그 도메인을 기반으로 클래스를 설계하는 것이 좋다고 말한다.)

클래스 구현하기

클래스는 내부와 외부로 구분되며 훌륭한 클래스를 설계하기 위한 핵심은
어떤 부분을 외부에 공개하고 어떤 부분을 감출지 결정하는 것이다.

자율적인 객체

두 가지 사실을 알아야 한다.

  • 객체가 상태(state)행동(behavior)을 함께 가지는 복합적인 존재라는 것
  • 객체가 스스로 판단하고 행동하는 자율적인 존재라는 것
객체지향 이전의 패러다임 VS 객체지향
  • 객체지향 이전의 패러다임: 데이터와 기능이라는 독립적인 존재를 서로 엮어 프로그램을 구성
  • 객체지향: 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현할 수 있게 함
    -> 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라고 한다.

객체가 자율적인 존재로 우뚝 서기 위해서는 외부 간섭을 최소화해야 한다.

캡슐화와 접근 제어는 객체를 두 부분으로 나눈다.

  • 퍼블릭 인터페이스(public interface): 외부에서 접근 가능한 부분
  • 구현(implementation): 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분

프로그래머의 자유

프로그래머의 역할을 클래스 작성자(class creator)
클라이언트 프로그래머(client programmer)로 구분하는 것이 유용하다.
ex) 공통 코드를 만들어내는 플랫폼 개발자 -> 클래스 작성자

객체 변경을 관리할 수 있는 기법 중에서 가장 대표적인 접근 제어는 경험과 연습이 중요하다.
ex) 모든 method를 처음에는 모두 private으로 만들고,
클라이언트 프로그래머에게 공개가 필요할 경우에 해당 method를 public으로 open하는 식으로 연습하는 경우도 있다.

클래스를 개발할 때마다 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다.
설계가 필요한 이유변경을 관리하기 위해서라는 것을 기억하라.

접근 제어 메커니즘
  1. 프로그래밍 언어 차원에서 클래스의 내부와 외부를 명확하게 경계 지을 수 있게 한다.
  2. 클래스 작성자가 내부 구현을 은닉할 수 있게 해준다.
  3. 클라이언트 프로그래머가 실수로 숨겨진 부분에 접근하는 것을 막아준다.

협력하는 객체들의 공동체

협력에 관한 짧은 이야기

메서드를 호출한다메시지를 전송한다로 말하는 것이 더 적절한 표현이다.
메시지를 수신한 객체는 스스로 적절한 메서드를 선택한다.


03. 할인 요금 구하기

할인 요금 계산을 위한 협력 시작하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;

public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}

public Money getFee() {
return fee;
}

public Money caculateMovieFee(Screening screening) {
// interface인 discountPolicy에 calculateDiscountAmount 메시지를 전송한다.
// -> 할인 요금을 반환 받는다.
// Movie는 기본 요금인 fee에서 반환된 할인 요금을 차감한다.
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}

caculateMovieFee 메서드에는 한 가지 이상한 점이 있다.
어떤 할인 정책을 사용할 것인지 결정하는 코드가 어디에도 존재하는 않는다는 것이다.
이 코드에는 객체지향에서 중요하다고 여겨지는 두 가지 개념인
상속(inheritance)다형성(Polymorphism)이 숨겨져 있다.
그리고 그 기반에는 추상화(abstraction)이라는 원리가 숨겨져 있다.

할인 정책과 할인 조건

부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를
자식 클래스에게 위임하는 디자인 패턴을 TEMPLATE METHOD 패턴[GOF94]라고 부른다.
cf) SpringTEMPLATE METHOD 패턴을 이용한 경우가 많다.(IoC, DI)

  • 헐리우드 원칙

    “Don’t call us, we’ll call you”.
    부모 클래스는 서브클래스에 정의된 연산을 호출할 수 있지만
    반대 방향의 호출은 안 된다는 의미

할인 정책 구성하기

외부에서 의존성을 주입하면 코드가 유연해진다.
ex) 스프링의 제어의 역전(Inversion of Control)
스프링에서는 프로그램의 흐름을 프레임워크가 주도한다.
제어권이 컨테이너로 넘어가게 되어 제어권의 흐름이 바뀌는 것이다.
따라서 개발자가 프로그램 코드로 객체 생성을 하지 않고, 컨테이너나 프레임워크가 만들어준 객체를 사용한다.


04. 상속과 다형성

컴파일 시간 의존성실행 시간(런타임) 의존성 어느 한 쪽에 치우쳐서는 안 된다.
한 가지 간과해서는 안 되는 사실은 코드의 의존성실행 시점의 의존성다르면 다를수록
코드를 이해하기 어려워진다는 것이다.

설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실을 기억하라.
반면, 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만,
재사용성과 확장 가능성은 낮아진다.

차이에 의한 프로그래밍

상속은 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법이다.
상속은 바라보는 관점에 따라 상대적이다.
ex) java.lang 패키지 내의 모든 클래스의 최상위 클래스는 Object이다.
자식 클래스로 Throwable이 있고, ThrowableErrorException의 부모 클래스가 된다.

상속과 인터페이스

상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를
자식 클래스가 물려받을 수 있기 때문이다.(public 메서드를 의미하는 것 같다.)

다형성

다시 한번 강조하지만, 메시지메서드는 다른 개념이다.
코드 상에서는 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는
메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.
이를 다형성이라고 한다.

메시지가 바뀌지 않아도 메서드는 바뀔 수 있다.

정적 바인딩 VS 동적 바인딩

  • 동적 바인딩(dynamic binding): 이미 코드가 돌아가고 있는 시점에서 바인딩하는 것
    (지연 바인딩(lazy binding))

    ex) AOP, Reflection, Generic

  • 정적 바인딩(static binding): 전통적인 함수 호출처럼 컴파일 시점에
    실행될 함수나 프로시저를 결정하는 것
    (초기 바인딩(early binding))

상속을 이용하면 동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶을 수 있다.
이런 이유로 대부분의 사람들은 다형성을 이야기할 때 상속을 함께 언급한다.
그러나 클래스를 상속받는 것만이 다형성을 구현할 수 있는 유일한 방법은 아니다.

인터페이스와 다형성

여기서의 다형성은 인터페이스, 추상 클래스만을 의미하지는 않는다.


05. 추상화와 유연성

추상화의 힘

할인 정책이나 할인 조건의 새로운 자식 클래스들은 추상화를 이용해서 정의한 상위의 협력 흐름을 그대로 따르게 된다.
재사용 가능한 설계의 기본을 이루는 디자인 패턴(design pattern)이나 프레임워크(framework) 모두 추상화를 이용해 상위 정책을 정의하는 객체지향의 메커니즘을 활용하고 있기 때문에
매우 중요하다.

추상 클래스와 인터페이스 트레이드오프

이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋을 것이다.
현실적으로는 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다는 생각이 들 수도 있을 것이다.

여기서 이야기하고 싶은 사실은 구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다는 사실이다.(ex) 인터페이스 -> 추상클래스, 추상클래스 -> 인터페이스)

코드 재사용

합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.

상속

상속은 객체지향에서 코드를 재사용하기 위해 널리 사용되는 기법이다.

  • 상속이 설계에 안 좋은 영향을 미치는 두 가지 관점

    • 캡슐화를 위반한다.
    • 설계를 유연하지 못하게 만든다.(추상화가 덜 됐다는 의미)
      (상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다.
      -> 실행 시점에 객체의 종류를 변경하는 것이 불가능하다.)

상속보다 인스턴스 변수로 관계를 연결한 설계가 더 유연하다.

합성

상속: 부모 클래스의 코드와 자식 클래스의 코드를 컴파일 시점에 하나의 단위로 강결합
합성: 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법(약결합)

합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.
인터페이스에 정의된 메시지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화할 수 있다.

그렇다고 해서 상속을 절대 사용하지 말라는 것은 아니다.
대부분의 설계에서는 상속과 합성을 함께 사용해야 한다.
이처럼 코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만
다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수 밖에 없다.


더 생각해 볼 것

  • 추상화란 무엇인가? 내가 생각하는 추상화란 무엇인가?
  • 추상화의 적정 depth는 어느 정도일까?