BottleH Blog

Object - 2장 객체지향 프로그래밍

    Tags

  • java
Object - 2장 객체지향 프로그래밍 thumbnail

📖 2.1 영화 예매 시스템

🔖 2.1.1 요구사항 살펴보기

  • 영화

    • 영화에 대한 기본적인 정보 표현
    • 제목, 상영시간, 가격 정보
  • 상영

    • 실제로 관객들이 영화를 관람하는 사건을 표현
    • 상영일자, 시간, 순번
    • 실제로 예매하는 대상은 영화가 아니라 상영임
  • 할인조건

    • 가격의 할인 여부 결정
    • 순서 조건은 상영 순번을 이용해 할인 여부를 결정
    • 기간 조건은 영화 상영 시작 시간을 이용해 할인 여부를 결정
    • 영화에 다수의 할인 조건을 지정할 수 있다.
  • 할인 정책

    • 할인 요금 결정
    • 금액 할인 정책은 예매 요금에서 일정 금액을 할인
    • 비율 할인 정책은 정가에서 일정 비율의 요금을 할인
    • 영화별로 하나 이하의 할인 정책만 할당할 수 있다.

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

🔖 2.2.1 협력, 객체, 클래스

  • 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
    • 클래스는 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것
    • 클래스의 윤곽을 잡기 위해서는 어떤 객체들이 어떤 상태와 행동을 가지는지**를 먼저 결정
  • 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 보자.
    • 객체는 홀로 존재하지 않으며, 다른 객체에게 도움을 주거나 의존하면서 살아가는 협력적인 존재
    • 공통된 특성과 상태를 가진 객체들을 타입으로 분류하고 이를 기반으로 클래스를 구현

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

  • 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 한다.

❗️ 객체지향 패러다임이 강력한 이유

  • 요구사항을 분석하는 초기 단계부터 프로그램을 구현하는 마지막 단계까지 동일한 추상화 기법을 사용할 수 있음
  • 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있음

🔖 2.2.3 클래스 구현하기

/** * 상영 Class */ @AllArgsConstructor public class Screening { /** * 영화 */ private Movie movie; /** * 순번 */ private int sequence; /** * 상영 시작 시간 */ @Getter private LocalDateTime startTime; /** * 순번 일치 여부 검사 */ public boolean isSequence(int sequence) { return this.sequence == sequence; } /** * 기본 요금 반환 */ public Money getMovieFee() { return movie.getFee(); } }
  • 클래스는 내부와 외부로 구분
  • 어떤 부분을 외부에 공개하고 어떤 부분을 감출지 결정해야 함
    • 경계의 명확성이 객체의 자율성을 보장하기 때문
    • 프로그래머에게 구현의 자유를 제공하기 때문

🎈 2.2.3.1 자율적인 객체

  • 객체는 **상태(state) 와 행동(behavior) 을 함께 가지는 복합적인 존재
  • 객체는 스스로 판단하고 행동하는 자율적인 존재
  • 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화 라고 한다.
    • 접근 제어 메커니즘 과 접근 수정자
    • 객체가 자율적인 존재로 우뚝 서기 위해서는 외부의 간섭을 최소화해야 한다.
  • 캡슐화와 접근 제어는 객체를 두 부분으로 나눈다. (인터페이스와 구현의 분리 원칙)
    • public interface
      • 외부에서 접근 가능한 부분
    • implimentation
      • 내부에서만 접근 가능한 부분

🎈 2.2.3.2 프로그래머의 자유

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

  • 클래스 작성자
    • 새로운 데이터 타입을 프로그램에 추가
    • 구현 은닉(implementation hiding): 클라이언트 프로그래머에게 필요한 부분만 공개하고 나머지는 숨기고 접근을 방지함으로써 내부 구현을 마음대로 변경할 수 있다.
  • 클라이언트 프로그래머
    • 클래스 작성자가 추가한 데이터 타입을 사용
    • 내부의 구현은 무시한 채 인터페이스만 알고 있어도 클래스를 사용할 수 있기 때문에 머릿속에 담아둬야 하는 지식의 양을 줄일 수 있다.

🔖 2.2.4 협력하는 객체들의 공동체

public class Screening { /** * 영화 예매 기능 * * @param customer 예매자 정보 * @param audienceCount 인원수 * @return 예매 정보 */ public Reservation reserve(Customer customer, int audienceCount) { return new Reservation(customer, this, calculateFee(audienceCount), audienceCount); } /** * 전체 예매 요금 계산 * * @param audienceCount 인원수 * @return 전체 예매 요금 */ private Money calculateFee(int audienceCount) { return movie.calculateMovieFee(this).times(audienceCount); } } /** * 예매 요금 Class */ @RequiredArgsConstructor public class Money { private final BigDecimal amount; public static final Money ZERO = Money.wons(0); public static Money wons(long amount) { return new Money(BigDecimal.valueOf(amount)); } public static Money wons(double amount) { return new Money(BigDecimal.valueOf(amount)); } public Money plus(Money amount) { return new Money(this.amount.add(amount.amount)); } public Money minus(Money amount) { return new Money(this.amount.subtract(amount.amount)); } public Money times(double percent) { return new Money(this.amount.multiply(BigDecimal.valueOf(percent))); } public boolean isLessThan(Money other) { return amount.compareTo(other.amount) < 0; } public boolean isGreaterThanOrEqual(Money other) { return amount.compareTo(other.amount) >= 0; } }
  • 객체지향의 장점은 객체를 이용해 도메인의 의미를 풍부하게 표현할 수 있다.
  • 의미를 좀 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해서 해당 개념을 구현
    • 전체적인 설계의 명확성과 유연성을 높이는 첫걸음
/** * 예매 정보 Class */ @AllArgsConstructor public class Reservation { /** * 고객 */ private Customer customer; /** * 상영 */ private Screening screening; /** * 예매 요금 */ private Money fee; /** * 인원 수 */ private int audienceCount; }
  • 시스템의 어떤 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용을 협력(Collaboration) 이라 한다.

🔖 2.2.5 협력에 관한 짧은 이야기

  • 객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request) 할 수 있다.
    • 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response) 한다.
  • 객체가 다른 객체와 상호작용할 수 있는 유일한 방법은 메시지를 전송 하는 것뿐이다.
    • 다른 객체에게 요청이 도찰할 때 해당 객체가 메시지를 수신 했다고 이야기한다.
    • 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드 라고 부른다.
    • 메시지와 메서드의 구분에서부터 다형성(polymorphism) 의 개념이 나온다.

📖 2.3 할인 요금 구하기

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

/** * 영화 Class */ @AllArgsConstructor public class Movie { /** * 제목 */ private String title; /** * 상영시간 */ private Duration runningTime; /** * 기본요금 */ @Getter private Money fee; /** * 할인 정책 */ private DiscountPolicy discountPolicy; public Money calculateMovieFee(Screening screening) { return fee.minus(discountPolicy.calculateDiscountAmount(screening)); } }
  • calculateMovieFee 에는 상속다형성 의 개념이 숨겨져 있다.

🔖 2.3.2 할인 정책과 할인 조건

/** * 할인 정책 class */ public abstract class DiscountPolicy { private final List<DiscountCondition> conditions; protected DiscountPolicy(DiscountCondition... conditions) { this.conditions = Arrays.asList(conditions); } public Money calculateDiscountAmount(Screening screening) { for (DiscountCondition condition : conditions) { if (condition.isSatisfiedBy(screening)) { return getDiscountAmount(screening); } } return Money.ZERO; } protected abstract Money getDiscountAmount(Screening screening); }
  • 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴을 Template Method 패턴 이라고 한다.
/** * 할인 조건 interface */ public interface DiscountCondition { boolean isSatisfiedBy(Screening screening); } @AllArgsConstructor public class SequenceCondition implements DiscountCondition { /** * 순번 */ private int sequence; /** * 할인 여부 판단 Method * <pre> * - 상영 순번과 일치할 경우 할인 가능 * </pre> * * @param screening 상영 * @return 할인 여부 */ @Override public boolean isSatisfiedBy(Screening screening) { return screening.isSequence(sequence); } } @AllArgsConstructor public class PeriodCondition implements DiscountCondition { /** * 조건에 사용할 요일 */ private DayOfWeek dayOfWeek; /** * 시작 시간 */ private LocalTime startTime; /** * 종료 시간 */ private LocalTime endTime; /** * 할인 여부 판단 Method * <pre> * - 상영 요일이 같고, 상영 시작 시간이 startTime과 endTime 사이에 있을 경우 할인 * </pre> * @param screening 상영 * @return 할인 여부 */ @Override public boolean isSatisfiedBy(Screening screening) { return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) && !startTime.isAfter(screening.getStartTime().toLocalTime()) && !endTime.isBefore(screening.getStartTime().toLocalTime()); } } /** * 금액 할인 정책 class */ public class AmountDiscountPolicy extends DiscountPolicy { /** * 할인 요금 */ private final Money discountAmount; public AmountDiscountPolicy(Money discountAmount, DiscountCondition... conditions) { super(conditions); this.discountAmount = discountAmount; } @Override protected Money getDiscountAmount(Screening screening) { return discountAmount; } } /** * 비율 할인 정책 class */ public class PercentDiscountPolicy extends DiscountPolicy { /** * 할인 비율 */ private final double percent; public PercentDiscountPolicy(double percent, DiscountCondition... conditions) { super(conditions); this.percent = percent; } @Override protected Money getDiscountAmount(Screening screening) { return screening.getMovieFee().times(percent); } }

🔖 2.3.3 할인 정책 구성하기

  • 하나의 영화에 대한 단 하나의 할인 정책만 설정할 수 있지만 여러 개의 할인 조건을 적용할 수 있다.
    • Movie와 DiscountPolicy의 생성자는 이런 제약을 강제한다.

📖 2.4 상속과 다형성

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

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

  • Movie의 인스턴스는 실행 시에 AmountDiscountPolicyPercentDiscountPolicy의 인스턴스에 의존해야 한다.
  • 하지만 코드 수준에서 Movie 클래스는 오직 추상 클래스인 DiscountPolicy에만 의존한다.

❗️ 코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다

  • 확장 가능한 객체지향 설계가 가지는 특징은 코드의 의존성과 실행 시점의 의존성이 다르다는 것
    • 코드를 이해하기 어려워짐
    • 코드 유연성 ⬆️
    • 코드 확장성 ⬆️
  • 결국 trade-off의 산물이다.

🔖 2.4.2 차이에 의한 프로그래밍

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

  • 부모 클래스의 구현은 공유하면서도 행동이 다른 자식 클래스를 쉽게 추가할 수 있다.
  • 차이에 의한 프로그래밍(programming by difference)

🔖 2.4.3 상속과 인터페이스

상속이 가치 있는 이유

  • 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있음.
  • 상속을 통해 자식 클래스는 자신의 인터페이스에 부모 클래스의 인터페이스를 포함하게 된다.
  • 외부 객체는 자식 클래스를 부모 클래스와 동일한 타입으로 간주할 수 있다.
    • 업캐스팅(upcasting)
  • 컴파일로는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용

🔖 2.4.4 다형성

  • 동일한 메시지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.
  • 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력
  • 다형적인 협력에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다.
  • 메시지와 메서드를 실행 시점에 바인딩한다.
    • 지연 바인딩(lazy binding), 동적 바인딩(dynamic binding)
  • 클래스를 상속받는 것만이 다형성을 구현할 수 있는 유일한 방법은 아니다.

🔖 2.4.5 인터페이스와 다형성

  • 구현은 공유할 필요가 없고 순수하게 인터페이스만 공유하고 싶을 때 인터페이스라는 프로그래밍 요소를 쓰면 된다.

📖 2.5 추상화와 유연성

🔖 2.5.1 추상화의 힘

  • 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
    • 상위 개념만으로 도메인의 중요한 개념을 설명할 수 있다.
  • 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것
    • 디자인 패턴이나 프레임워크 모두 추상화를 이용해 상위 정책을 정의
  • 기존 구조를 수정하지 않고도 새로운 기능을 쉽게 추가하고 확장할 수 있다.

🔖 2.5.2 유연한 설계

public Money calculateMovieFee(Screening screening) { if (discountPolicy == null) { return fee; } return fee.minus(discountPolicy.calculateDiscountAmount(screening)); }
  • 이 방식은 일관성 있던 협력 방식이 무너지게 된다.
  • 할인 금액이 0원이라는 사실을 결정하는 책임이 DiscountPolicy가 아닌 Movie에 있음.
/** * 0원 할인 정책 class */ public class NoneDiscountPolicy extends DiscountPolicy { @Override protected Money getDiscountAmount(Screening screening) { return Money.ZERO; } }
  • 새로운 클래스를 추가하는 것만으로 애플리케이션의 기능을 확장함

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

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

  • 구현과 관련된 모든 것들은 trade-off의 대상이 될 수 있다.
    • DiscountPolicy를 interface로 변환

🔖 2.5.4 코드 재사용

  • 코드 재사용을 위해서는 상속보다는 **합성(composition)**이 더 좋은 방법
    • 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용

🔖 2.5.5 상속

상속은 두 가지 관점에서 설계에 안 좋은 영향을 미친다.

  1. 캡슐화 위반

    • 부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화 약화
    • 자식 클래스가 부모 클래스에 강하게 결합되도록 만들기 때문에 부모 클래스를 변경할 때 자식 클래스도 함께 변경될 확률을 높인다.
  2. 설계를 유연하지 못하게 만듦

    • 상속은 부모 클래스와 자식 클래스 사이의 관게를 컴파일 시점에 결정
    • 실행 시점에 객체의 종류를 변경하는 것이 불가능
public void changeDiscountPolicy(DiscountPolicy discountPolicy) { this.discountPolicy = discountPolicy; }
  • 위와 같이 인스턴스 변수로 연결한 방법을 사용하면 실행 시점에 할인 정책을 간단하게 변경할 수 있다.

🔖 2.5.6 합성

  • 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 한다.
  • 구현을 효과적으로 캡슐화할 수 있다.
  • 의존하는 인스턴스를 교체하는 것이 비교적 쉽기 때문에 설계를 유연하게 만든다.
  • 대부분의 설계에서는 상속과 합성을 함께 사용해야 한다.
Written by@BottleH
Back-End Developer

GitHub