Skip to content

객체지향 프로그래밍

Published: at 오후 12:30

Table of contents

Open Table of contents

영화 예매 시스템

요구사항 살펴보기

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

협력, 객체, 클래스

객체지향 프로그램을 작성할 때 객체지향 언어에 익숙한 사람이라면 가장 먼저 어떤 클래스가 필요한지 고민할 것이다. 안타깝게도 이것은 객체지향의 본질과는 거리가 멀다. 진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때만 얻을 수 있다.

  1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
  2. 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.

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

클래스 구현하기

자율적인 객체

대부분의 객체지향 프로그래밍 언어들은 외부에서의 접근을 통제할 수 있는 접근 제어(access control) 메커니즘을 제공한다. 많은 프로그래밍 언어들은 접근 제어를 위해 public, protected, private 과 같은 접근 수정자(access modifier) 를 제공한다.

프로그래머의 자유 프로그래머의 역할을 클래스 작성자(class creator)클라이언트 프로그래머(client programmer) 로 구분하는 것이 유용하다.

협력하는 객체들의 공동체

협력에 관한 짧은 이야기

할인 요금 구하기

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

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 calculateMovieFee(Screening screening) {
    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
  }
}

할인 정책과 할인 조건

public abstract class DiscountPolicy {
  private List<DiscountCondition> conditions = new ArrayList<>();

  public DiscountPolicy(DiscountCondition ... conditions) {
    this.conditions = Arrays.asList(conditions);
  }

  public Money calculateDiscountAmount(Screening screening) {
    return conditions.stream()
      .filter(condition -> condition.isSatisfiedBy(screening))
      .findFirst()
      .map(this::getDiscountAmount)
      .orElse(Money.ZERO);
  }

  abstract protected Money getDiscountAmount(Screening screening);
}

이처럼 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 **템플릿 메서드 패턴(template method pattern)**이라고 부른다.

public interface DiscountCondition {
  boolean isSatisfiedBy(Screening screening);
}

public class SequenceCondition implements DiscountCondition {
  private int sequence;

  public SequenceCondition(int sequence) {
    this.sequence = sequence;
  }

  @Override
  public boolean isSatisfiedBy(Screening screening) {
    return screening.isSequence(sequence);
  }
}

public class PeriodCondition implements DiscountCondition {
  private DayOfWeek dayOfWeek;
  private LocalTime startTime;
  private LocalTime endTime;

  public PeriodCondition(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) {
    this.dayOfWeek = dayOfWeek;
    this.startTime = startTime;
    this.endTime = endTime;
  }

  @Override
  public boolean isSatisfiedBy(Screening screening) {
    return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
      startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
      endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
  }
}
public class AmountDiscountPolicy extends DiscountPolicy {
  private Money discountAmount;

  public AmountDiscountPolicy(Money discountAmount, DiscountCondition ... conditions) {
    super(conditions);
    this.discountAmount = discountAmount;
  }

  @Override
  protected Money getDiscountAmount(Screening screening) {
    return discountAmount;
  }
}

public class PercentDiscountPolicy extends DiscountPolicy {
  private double percent;

  public PercentDiscountPolicy(double percent, DiscountCondition ... conditions) {
    super(conditions);
    this.percent = percent;
  }

  @Override
  protected Money getDiscountAmount(Screening screening) {
    return screening.getMovieFee().times(percent);
  }
}

할인 정책 구성하기

하나에 영화에 대해 단 하나의 할인 정책만 설정할 수 있는 것과 할인 조건의 경우에는 여러 개를 적용할 수 있다는 것은 생성자를 통해 제약을 강제한다.

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

상속과 다형성

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

차이에 의한 프로그래밍

상속과 인터페이스

다형성

상속을 구현 상속과 인터페이스 상속으로 분류할 수 있다. 구현 상속을 **서브클래싱(subclassing)**이라고 부르고 인터페이스 상속을 **서브타이핑(subtyping)**이라고 부른다. 순수하게 코드를 재사용하기 위한 목적으로 상속을 사용하는 것을 구현 상속이라고 부른다. 상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다. 구현을 재사용할 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.

추상화와 유연성

추상화의 힘

유연한 설계

public class Movie {
  public Money calculateMovieFee(Screening screening) {
    if (discountPolicy == null) {
      return fee;
    }

    return fee.minus(discountPolicy.calculateDiscountAmount(screening));
  }
}
public class NoneDiscountPolicy extends DiscountPolicy {
  @Override
  protected Money getDiscountAmount(Screening screening) {
    return Money.ZERO;
  }
}

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

=> 유연성이 필요한 곳이 추상화를 사용하라.

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

코드 재사용

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

상속

합성

코드를 재사용하는 경우에는 상속보다 합성을 선호하는 것이 옳지만 다형성을 위해 인터페이스를 재사용하는 경우에는 상속과 합성을 함께 조합해서 사용할 수밖에 없다.