이 책을 관통하는 첫 번째 예제, 영화 예매 시스템이 여기서 등장한다. 상영(Screening)을 예매하면 예매 정보(Reservation)가 나온다. 요금은 영화의 기본 가격에서 할인을 뺀 값인데, 할인 방식이 여럿이다 — 특정 금액을 깎거나(금액 할인), 비율로 깎거나(비율 할인), 아예 할인이 없거나. 이 “여러 방식 중 하나”를 어떻게 코드에 담느냐가 이 장의 주제다. 조영호는 추상화와 다형성이 왜 절차적 분기보다 나은지를 코드가 진화하는 과정으로 보여준다.

할인 로직을 어디에 둘 것인가

가장 곧은 길은 Movie가 자기 요금을 계산하면서 할인까지 직접 처리하는 것이다. 하지만 그러면 할인 종류가 늘 때마다 Movie 안의 if가 늘고, 금액 할인과 비율 할인의 계산이 한 클래스에 뒤섞인다. Movie는 “영화”인데 “할인 정책”의 세부까지 떠안는다.

핵심 통찰은 할인 정책이 영화와 독립적으로 변한다는 것이다. 변하는 축이 다르면 클래스도 나눈다. 할인 정책을 DiscountPolicy라는 추상 클래스로 뽑아내고, Movie는 구체적인 정책이 아니라 그 추상 타입에만 의존하게 만든다. 그러면 Movie는 “어떤” 할인인지 몰라도 “할인을 계산하라”고 요청할 수 있다.

판단 기준: 함께 바뀌는 것은 모으고, 다른 이유로 바뀌는 것은 분리한다. 영화의 정보와 할인 계산 방식은 변경 이유가 다르므로 갈라야 한다. 함정: if (type == AMOUNT) ... else if (type == PERCENT) ... 식 분기는 당장은 짧지만, 정책이 하나 늘 때마다 모든 분기 지점을 찾아 고쳐야 하는 산탄총 수술로 자란다.

추상화에 기대고, 구체는 갈아 끼운다

아래 스텝은 Movie에 직접 박힌 할인 로직을 DiscountPolicy 추상 클래스로 끌어내고, 금액/비율 두 구현을 다형성으로 꽂는 과정이다.

Refactoring Step Movie에 할인 로직을 직접 박는다
1/3
public class Movie {    private Money fee;    private MovieType movieType;    private Money discountAmount;    private double discountPercent;    public Money calculateMovieFee(Screening screening) {        if (movieType == MovieType.AMOUNT_DISCOUNT) {            return fee.minus(discountAmount);        } else if (movieType == MovieType.PERCENT_DISCOUNT) {            return fee.minus(fee.times(discountPercent));        }        return fee;    }}// 할인 종류가 늘 때마다 이 if-else가 늘고,// 금액 할인과 비율 할인의 계산 방식이 Movie 안에 뒤섞인다.

MoviecalculateMovieFee는 2단계 이후로 한 글자도 바뀌지 않는다. 금액 할인을 쓰든 비율 할인을 쓰든, 새 정책이 추가되든 Movie는 그대로다. 새 할인은 DiscountPolicy를 상속한 클래스를 하나 더 만들어 생성 시점에 꽂아주면 끝이다 — 기존 코드를 열지 않고 기능을 넓히는 개방-폐쇄 원칙이 여기서 나온다.

컴파일 타임 의존성과 런타임 의존성

이 설계의 묘미는 의존성이 두 겹이라는 데 있다. Movie의 코드는 DiscountPolicy라는 추상 타입에만 의존한다 — 이것이 컴파일 타임 의존성이다. 하지만 실제로 실행될 때 Movie 안의 discountPolicy에는 AmountDiscountPolicyPercentDiscountPolicy 같은 구체 인스턴스가 들어 있다 — 이것이 런타임 의존성이다.

classDiagram
    class Movie {
        -Money fee
        -DiscountPolicy discountPolicy
        +calculateMovieFee(Screening)
    }
    class DiscountPolicy {
        <<abstract>>
        +calculateDiscountAmount(Screening)
        #getDiscountAmount(Screening)*
    }
    class AmountDiscountPolicy {
        -Money discountAmount
        #getDiscountAmount(Screening)
    }
    class PercentDiscountPolicy {
        -double percent
        #getDiscountAmount(Screening)
    }
    Movie --> DiscountPolicy : 컴파일 타임 의존
    DiscountPolicy <|-- AmountDiscountPolicy
    DiscountPolicy <|-- PercentDiscountPolicy
    Movie ..> AmountDiscountPolicy : 런타임 의존
    Movie ..> PercentDiscountPolicy : 런타임 의존

두 의존성이 다를수록 설계는 유연해진다. 코드는 추상에만 묶여 있으니 읽는 사람은 구체를 몰라도 Movie를 이해할 수 있고, 실행은 상황에 맞는 구체를 갈아 끼운다. 대신 그 대가로 코드만 봐서는 실제 어떤 정책이 도는지 알 수 없다 — 실행 흐름을 좇으려면 생성 지점까지 따라가야 한다.

판단 기준: 유연함이 필요한 지점(변경이 잦고 방식이 여럿인 곳)에만 이 분리를 적용한다. 함정: 유연성과 가독성은 반비례한다. 변할 일 없는 곳까지 추상 타입으로 감싸면, 이해하려 클래스 사이를 헤매는 비용만 늘고 얻는 유연함은 쓰이지 않는다.

상속과 다형성, 그리고 그 너머

이 장은 상속으로 추상화를 구현했다. DiscountPolicy를 상속한 자식들이 getDiscountAmount를 저마다 재정의(오버라이딩)하고, Movie는 어느 자식이 왔는지 모른 채 같은 메시지를 보낸다 — 이것이 다형성이다. 다만 상속은 부모-자식을 강하게 묶는 도구이기도 하다. 조영호는 뒤 장(10, 11장)에서 상속의 한계를 짚고 합성으로 갈아타는데, 그 복선이 여기서 시작된다.

  • 변하는 것(할인 정책)을 추상 타입 뒤로 숨기고, 변하지 않는 협력 구조(MovieDiscountPolicy)만 코드에 남긴다.
  • 코드의 의존성(컴파일 타임)과 실행의 의존성(런타임)을 일부러 어긋나게 만들면 유연해진다. 그 유연함이 필요한 곳에만 쓴다.
  • 다형성은 조건 분기를 객체 선택으로 대체한다. if가 사라진 자리를 “어떤 객체를 꽂았는가”가 대신한다.

다음장으로 3장