KwanIk Devlog
article thumbnail

01. 영화 예매 시스템

영화 예매 시스템을 구현하면서 객체지향 프로그래밍에 대한 개념을 잡아본다.

 

1) 요구사항 정의

  • 영화: 영화에 관련된 기본 정보(제목, 상영시간, 가격정보, ...)
  • 상영: 실제로 관객들이 영화를 관람하는 사건
  • 할인조건: 가격의 할인 여부(순서조건, 기간조건), 할인 정책에는 다수의 할인 조건을 설정할 수 있음 
    • 순서조건: 상영 순번을 이용해 할인 여부를 결정
    • 기간조건: 영화 상영 시작 시간을 이용해 할인 여부를 결정
  • 할인정책: 할인 요금을 결정, 영화당 하나의 할인 정책만 할당할 수 있음
    • 금액 할인 정책: 예매 요금에서 일정 금액을 할인해주는 방식
    • 비율 할인 정책: 정가에서 일정 비율의 요금을 할인해주는 방식

 


 

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

진정한 객체지향 프로그래밍으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다

 

1) 설계에서 무엇을 고려해야 하는가?

  1. 어떤 클래스가 필요한지를 고민하기 전에, 어떤 객체들이 필요한지 고민하라.
    • 클래스란 공통적인 상태와 행동을 공유하는 객체를 추상화한 것
    • 어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 함
  2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐라.
    • 객체를 협력하는 공동체의 일원으로 바라봐야 설계가 유연하고 확장 가능해진다.
  3. 도메인이란?
    • 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
    • 요구사항과 프로그램을 '객체'라는 동일한 개념에서 바라볼 수 있으므로, 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 잇음
    • 클래스는 이러한 도메인 개념들을 구현하기 위해 사용하는 것

 

2) 클래스 구현하기

  1. 적절한 클래스의 경계
    • 어떤 부분을 외부에 공개하고 어떤 부분을 감출지를 결정해야 함
    • 경계의 명확성이 객체의 자율성을 보장하기 때문
  2. 자율적인 객체란?
    • 객체는 상태(state)와 행동(behavior)을 함께 가지는 복합적인 존재
    • 객체는 스스로 판단하고 행동하는 자율적인 존재
    • 이와 같이 데이터와 기능을 내부로 묶는 것을 캡슐화(encapsulation)라고 함
    • 외부에서의 접근을 통제하기 위해 접근제어(access control) 개념을 탑재함
      • 접근을 통제하는 이유는 객체를 자율적인 존재로 만들기 위해서임 
      • 퍼블릭 인터페이스(public interface): 외부에서 접근 가능한 부분
      • 구현(implementation): 외부에서는 접근 불가능하고 오직 내부에서만 접근 가능한 부분
  3. 프로그래머의 자유
    • 클래스 작성자(class creator)
      • 새로운 데이터 타입을 프로그램에 추가
      • 클라이언트 프로그래머에게 필요한 부분만 공개(구현 은닉 - implementation hiding)
    • 클라이언트 프로그래머(client programmer)
      • 클래스 작성자가 추가한 데이터 타입을 사용
    • 객체의 내외부를 구분하면 클라이언트 프로그래머가 알아야 할 지식의 양이 줄어드록, 클래스 작성자가 자유롭게 구현을 변경할 수 있는 폭이 넓어짐

 

3) 협력하는 객체들의 공동체

  1. 협력(Collaboration)
    • 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용
      • 요청(request): 다른 객체의 인터페이스에 공개된 행동을 수행하도록 하는 것
      • 응답(response): 요청을 받은 객체가 자율적인 방법에 따라 요청을 처리한 후 응답하는 것
      • 메서드(method): 상호작용을 하는 방법은 '메시지를 주고 받는 것'으로, 수신된 메시지를 처리하기 위한 자신만의 방법
    • 메시지와 메서드의 구분에서부터 다형성(polymorphism)이 출발함
    • calculateMovieFee를 통해 할인 요금을 반환하지만, 어떤 할인 정책을 사용할 것인지 결정하는 코드는 없음
      • 그 어디에도 할인정책을 판단하는 코드는 존재하지 않음
      • 단지 discountPolicy에 메시지를 전송할 뿐!!!
public class Movie {
    private String title;
    private Duration duration;
    private Money fee;
    private DiscountPolicy discountPolicy;

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

    public Money getFee() {
        return fee;
    }

    public Money calculateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}

 

TEMPLATE METHOD PATTERN: 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고, 중간에 필요한 처리를 자식 클래스에 위임하는 패턴

 

요구사항에 기반한 현재까지의 코드 내용

 

4) 상속과 다형성

어떤 클래스가 다른 클래스에 접근할 수 있는 경로를 가지거나 해당 클래스 객체의 메서드를 호출할 경우 '의존성이 있다'라고 함

 

 

  • Movie가 'DiscountPolicy'와 연결되어 있지만, 실행시점에서는 'AmountDiscountPolicy'나 'PercentDiscountPolicy'의 인스턴스가 필요함
    • 컴파일 타임 의존성과 런타임 의존성이 다르다!!!!
    • 인스턴스 생성 시점에 의존성이 발생함
Movie avatar = new Movie(
    "아바타",
    Duration.ofMinutes(120),
    Money.wons(10000),
    new AmountDiscountPolicy(Money.wons(800), ...));

 

  • 객체지향 설계는 컴파일 타임 의존성과 런타임 의존성이 다를 수 있음!
    • 장점: 유연해지고 확장성이 좋아짐
    • 단점: 코드를 이해하기 어려워짐
  • 상속
    • 코드를 재사용하기 위해 가장 널리 사용되는 방법으로, 관계를 설정하는 것만으로 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있음
    • 차이에 의한 프로그래밍(programming by difference)
      • 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법
    • 인터페이스(interface)
      • 객체가 이해할 수 있는 메시지 목록을 정의하는 것
      • 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신할 수 있음
      • 상속이 단순히 메서드나 인스턴스 변수를 재사용하는 것이라고 생각해선 안됨
    • 업캐스팅(upcasting)
      • 자식 클래스는 부모 클래스를 대신해서 사용될 수 있음
    • 구현 상속(implementation inheritance) vs 인터페이스 상속(interface inheritance)
      • 구현 상속: 서브 클래싱, 코드를 재사용하기 위한 목적으로 상속을 사용하는 것
      • 인터페이스 상속: 서브 타이핑, 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용하는 것
      • 인터페이스 상속을 사용해야 함. 11장에 나오지만 단순 코드 재사용을 위해서 상속을 사용하는 것은 좋지 않음
  • 다형성
    • 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스에 따라 달라지는 것으로, 컴파일 타임 의존성과 런타임 의존성이 다를 수 있다는 사실에 기반함
    • 동일한 메시지를 수신했을 때, 객체의 타입에 따라 다르게 응답할 수 있는 것
    • 메시지에 응답하기 위해 실행될 메서드를 컴파일 타임이 아닌 런타임 시점에 결정
      • 지연 바인딩(lazy binding), 동적 바인딩(dynamic binding)이라고 함
      • 컴파일 타임에 결정되는 것을 '초기 바인딩(early binding), 정적 바인딩(static binding)' 이라고 함

 

5) 추상화와 유연성

할인정책은 구체적인 '금액할인정책'과 '비율할인정책'을 포괄하는 추상적인 개념. 이런 추상화는 어떤 이점을 가지고 있는가?

 

  1. 추상화의 계층만 따로 떼어 놓고 보면, 요구사항의 정책을 높은 수준에서 서술할 수 있음
    • 세부적인 내용을 무시한 채, 상위 정책을 쉽고 간단하게 표현할 수 있음
    • 기본적인 애플리케이션의 협력 흐름을 기술할 수 있음
    • 추상적인 개념으로 정의함으로써, 세부 문장을 포괄하는 종합적인 정책을 기술할 수 있기 때문임
      • ex) 영화 예매 요금은 최대 하나의 '할인 정책'과 다수의 '할인 조건'을 이용해 계산할 수 있다.
  2. 추상화를 이용하면 설계가 유연해짐
    • 유연한 설계를 가져갔을 경우, 예외 케이스에 대한 처리를 조건문을 통해 책임의 위치를 옮기기 보다, 일관성을 유지할 수 있는 방법을 선택해야 함
public class Movie {
    // ...
    
    // 조건문으로 처리할 경우, 요금 계산의 책임이 DiscountPolicy에서 Movie로 옮겨짐
    public Money calculateMovieFee(Screening screening) {
        if (discountPolicy == null) {
            return Money.ZERO;
        }
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}
    • 책임의 일관성을 유지할 수 있도록 하는 것을 컨텍스트 독립성(context independency)이라고 하며, 아래와 같이 구현하면 됨
// 할인정책이 없는 영화를 위해 NoneDiscountPolicy 추가
public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Screening screening) {
        return Money.ZERO;
    }
}

Movie avatar = new Movie(
    "스타워즈",
    Duration.ofMinutes(130),
    Money.wons(10000),
    new NoneDiscountPolicy());

 

추상화 수준에서의 결합도를 고려하여 이를 다시 인터페이스로 분리하는 작업 역시 진행할 수 있지만 늘 그랫듯이 트레이드오프의 대상이 될 것이며, 변화에는 적합하고 합당한 이유를 기반으로 작업해야 함.

 

6) 코드 재사용

  1. 합성(composition)
    • 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 법
    • 합성을 통해서 처리할 경우, 실행 시점에 인스턴스를 간단하게 변경할 수 있음
    • 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법
    • 상속보다 유연한 방식

 

인터페이스 상속과 합성이 일부 적용된 최종 소스코드


 

결국 특정 도메인을 구현함에 있어서 가장 중요한 것은 그 도메인, 객체를 추상화한 클래스를 구현하는 것에 집중해서는 안된다는 것이 내용의 핵심이라고 생각한다. 적절한 추상화 수준을 통해 추상화하여 각 객체들이 본인의 역할과 책임을 다 할 수 있도록 설계하는 것이 우선이 되야 한다. 이 과정에서 단순히 코드 재사용만을 위한 상속을 사용해서는 안된다. 왜냐하면 객체는 메시지를 통해서 상호작용하는 존재이므로, 각자의 객체들이 수신할 수 있는 메시지를 적절히 인터페이스로 정의하고, 이를 상속받아 객체의 행위를 결정지어야 하기 때문이다. 

 

또한 객체지향 설계의 핵심은 컴파일 타임 의존성과 런타임 의존성이 다르다는 점인데, 어느 정도로 추상화하여 설계할 것인지는 결국 트레이드오프 문제에 도달하게 된다. 유연한 설계는 곧 코드 가독성의 저하를 의미하는 바이기도 하기 때문이다.

 


Reference

1. 오브젝트, 조영호, 위키북스 , p37 ~ p72

반응형
profile

KwanIk Devlog

@갈닉

노력하는 개발자가 되고자 합니다. GitHub이나 블로그 모두 따끔한 피드백 부탁드립니다 :)