[오브젝트] #2. 객체지향 프로그래밍

bien·2024년 9월 19일
0

오브젝트

목록 보기
2/13

01. 영화 예매 시스템

요구사항

  • 온라인 영화 예매 시스템
    • 영화; 영화에 대한 기본 정보
      • 제목, 상영시간, 가격 정보
    • 상영; 실제 관객들이 영화를 관람하는 사건
      • 상영 일자, 시간, 순번
    • 사용자가 실제로 예매하는 대상; 상영
  • 할인
    • 할인액 결정 조건
      • 할인 조건(discount condition); 가격의 할인 여부를 결정
        • 순서 조건(sequence condition); 상영 순번으로 할인 여부를 결정
          • 순번이 10번인 경우, 매일 10번째 상영 영화 예매 사용자에게 할인 혜택 제공
        • 기간 조건(period condition); 영화 상영 시간 시작을 이용해 할인 여부를 결정
          • 요일, 시작시간, 종료 시간이 기간 안에 포함될 경우 할인
          • ex) 월요일, 오전 10시, 오후 1시
      • 할인 정책(discount policy); 할인 요금 결정
        • 금액 할인 정책 (amount discount policy)
          • 예매 요금에서 일정 금액을 할인
        • 비율 할인 정책 (percent discount policy)
          • 정가에서 일정 비율의 요금을 할인해주는 방식가에서 일정 비율의 요금을 할인해주는 방식
    • 영화에 할인 정책을 지정하지 않는 것이 가능하다.
    • 하나의 영화에 하나의 할인 정책만 할당할 수 있다.
    • 하나의 영화에 다수의 할인 조건을 적용할 수 있다.

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

협력, 객체, 클래스

  • 객체지향; 객체를 지향하는 것.
    • 방법1. 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지를 고민한다.
      • 클래스; 공통적인 상태와 행동을 공유하는 객체들을 추상화한 것.
    • 방법2. 객체는 (독립적인 존재가 아니라) 기능 구현을 위해 협력하는 공동체의 일원.

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

  • 도메인(domain)
    • 소프트웨어; 사용자가 원하는 특정 문제를 해결하기 위한 것
    • 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야를 도메인이라고 부른다.
      • 영화 예매 시스템; 영화를 좀 더 쉽고 빠르게 예매하려는 사용자의 문제를 해결하는 것.
  • 객체지향 패러다임의 강점
    • 요구사항 분석 초기 단계부터 프로그램 구현이라는 마지막 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있다는 점.
      • 도메인을 구성하는 개념들이 프로그램의 객체와 클래스로 매끄럽게 연결될 수 있다.
      • 클래스 사이의 관계도 최대한 도메인 개념 사이의 관계와 유사하게 만들어 프로그램의 구조를 이해하고 예상하기 쉽게 만들어야 한다.

클래스 구현하기

예시에서 주목해야 할 점은, 인스턴스 변수의 가시성은 private으로, 메서드의 가시성은 public으로 표현된 점이다.

public class Screening {
    
    private Movie movie;
    private int sequence;
    private LocalDateTime whenScreened;
    
    public Screening(Movie movie, int sequence, LocalDateTime whenScreened) {
        this.movie = movie;
        this.sequence = sequence;
        this.whenScreened = whenScreened;
    }
    
    public LocalDateTime getStartTime() {
        return whenScreened;
    }
    
    public boolean isSequence(int sequence) {
        return this.sequence == sequence;
    }
    
    public Money getMovieFee() {
        return movie.getFee();
    }
    
            
}
  • 좋은 클래스 설계의 핵심
    • 명확한 경계를 갖는 것.
      • 즉, 클래스의 내부와 외부를 명확하게 구분하는 것.
        • 어떤 부분을 외부에 공개하고, 내부에 감출 것인가?
      • 경계의 명확성은 객체의 자율성을 보장한다.
      • 경계의 명확성은 프로그래머에게 구현의 자유를 제공한다.

자율적인 객체

  • 객체
    1. 상태(state)행동(behavior)을 함께 가지는 복합적인 존재
    2. 객체는 스스로 판단하고 행동하는 자율적인 존재
  • 객체지향
    • 객체라는 단위 안에 데이터와 기능을 한 덩어리로 묶어 문제 영역의 아이디어를 표현
      • 캡슐화
    • 외부에서의 접근 통제
      • 접근 제어(access control) 매커니즘
        • 접근 수정자(access modifier); public, protected, private
      • 객체 내부에 대한 접근을 통제; 객체를 자율적인 존재로 만들기 위함
  • 캡술화 + 접근제어 => 두 종료의 객체
    • 객체1: 퍼블릭 인터페이스 (public interface)
      • 외부에서 접근 가능한 부분
    • 객체2: 구현(implementation)
      • 외부에서 접근 불가능하고, 오직 내부에서만 접근 가능한 부분
        • => 인터페이스와 구현의 분리 (separation of interface and implementation)

프로그래머의 자유

  • 프로그래머의 역할 2가지
    1. 클래스 작성자(class creator)
      • 새로운 데이터 타입을 프로그램에 추가
      • 목표; 필요한 부분만 공개하고 나머지는 숨겨, 클라이언트 프로그래머가 마음대로 접근 및 변경하는 것을 막는 것.
        • => 구현 은닉(implementation hiding)
    2. 클라이언트 프로그래머 (client programmer)
      • 클래스 작성자가 추가한 데이터 타입을 사용
      • 목표; 필요한 클래스들을 이용해 빠르고 안정적으로 애플리케이션을 구축하는 것
  • 구현 은닉(implementation hiding)의 장점
    • 클라이언트 프로그래머; 내부의 구현은 무시한 채 인터페이스만 알고 있어도 클래스 사용 가능
      • 머릿속에 담아둬야 하는 지식의 양 축소
    • 클래스 작성자
      • 인터페이스를 변경하지 않는 한, 외부에 미치는 영향을 걱정하지 않고 내부 구현 수정 가능
  • 클래스 개발 시, 인터페이스와 구현을 깔끔하게 분리하기 위해 노력해야 한다.
  • 설계; 변경 관리를 위함
    • 객체지향 언어는 객체 사이의 의존성을 관리하여 변경에 대한 파급효과를 제어할 수 있는 다양한 방법을 제공함.
      • 대표적인 기법이 접근 제어
      • 변경 가능한 세부 구현내용을 private 영역 안에 감추어, 변경으로 인한 혼란을 최소화 할 수 있음.

협력하는 객체들의 공동체

  • 협력(Collaboration)
    • 시스템의 기능을 구현하기 위해 객체들 사이에 이뤄지는 상호작용
  • 객체지향 프로그램 작성
    1. 먼저 협력의 관점에서 어떤 객체가 필요한지 결정한다.
    2. 객체들의 공통 상태와 행위를 구현하기 위해 클래스를 작성한다.
  • 하위 코드에서 확인 가능한 객체들 사이의 협력
    • Screen, Reservation, Movie

Screening

public class Screening {
	// ...
    
    public Reservation reserve(Customer customer, int audienceCount) {
        return new Reservation(customer, this, calculateFee(audienceCount), audienceCount);
    }

    private Money calculateFee(int audienceCount) {
        return movie.calculateMovieFee(this).times(audienceCount);
}
  • calculateFee라는 private 메서드를 통해 요금을 계산한 후, 그 결과를 Reservation에 전달한다.
  • 전체 예매 요금을 구하기 위해 1인당 예매 요금인 calculateMovieFee에 관람객 숫자인 audienceCount를 곱한다.

Money

public class Money {
	public static final Money ZERO = Money.wons(0);
    
    private final BigDecimal amount;
    
    public static Money wons(long amount) {
    	return new Money(BigDecimal.valueOf(amount));
    }
    
    public static Money wons(double amount) {
    	return new Money(BigDecimal.valueOf(amount));
    }
    
    Money(BigDecimal amount) {
    	this.amount = amoun;
    }
    
    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;
    }
}
  • 금액 구현을 위해 Long 대신 Money를 사용하는 경우 얻을 수 있는 장점
    • 저장하는 값이 금액과 관련되어 있다는 의미를 전달할 수 있음.
    • 금액과 관련된 로직이 중복되어 구현되는 것을 막을 수 있음.
  • 객체지향의 장점;
    • 객체를 이용해 도메인의 의미를 풍부하게 표현할 수 있다.
  • 의미를 더 명시적이고 분명하게 표현할 수 있다면 객체를 사용해 해당 개념을 구현해라.
    • 하나의 인스턴스 변수만 포함하더라도, 개념의 명시적 표현은 전체적인 설계의 명확성과 유연성을 높여준다.

Reservation

public class Reservation {
    private Customer customer;
    private Screening screening;
    private Money fee;
    private int audienceCount;
    
    public Reservation(Customer customer, Screening screening, Money fee, int audienceCount) {
        this.customer = customer;
        this.screening = screening;
        this.fee = fee;
        this.audienceCount = audienceCount;
    }
}

협력에 관한 짧은 이야기

  • 객체는 다른 객체의 인터페이스에 공개된 행동을 수행하도록 요청(request)한다.
  • 요청받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답(response)한다.
  • 객체간의 상호작용; 메시지를 전송(send a message)하는 것이 유일한 방법
    • 다른 객체에 요청이 도착할 때, 해당 객체가 메시지를 수신(receive a message)했다고 이야기 한다.
    • 메시지를 수신한 객체는 스스로의 결정에 따라 자율적으로 메시지를 처리할 방법을 결정한다.
    • 수신된 메시지를 처리하기 위한 자신만의 방법을 메서드(method)라고 한다.
  • 메시지와 메서드의 구분에서부터 다형성(polymorphism)의 개념이 출발한다.

03. 할인 요금 구하기

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

Movie

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));
    }
}
  • 어떤 할인 정책을 사용할 것인지 결정하는 코드가 존재하지 않는다.
    • 영화 예매 시스템에 두 가지 종류의 할인 정책이 존재함에도 불구하고, 적용된 할인 정책의 종류를 파악하는 코드가 없다.
      • 단지 discountPolicy에게 메시지를 전송할 뿐이다.
  • 이 코드에는 객체지향에서 중요하다고 여겨지는 두 가지 개념이 숨겨져 있다.
    1. 상속 (inheritance)
    2. 다형성
      • 그 기반에는, 추상화(abstraction)라는 원리가 숨겨져 있다.

할인 정책과 할인 조건

  • 할인 정책
    • 금액 할인 정책; AmountDiscountPolicy
    • 비율 할인 정책; PercentDiscountPolicy
      • 대부분의 코드가 유사 & 할인 요금 계산 방식만 다름
      • 중복 코드 제거를위해, 공통 코드를 보관할 장소가 필요하다.
  • 탬플릿 메서드(Template Method) 패턴
    • 부모 클래스에 기본적인 알고리즘을 구현하고, 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴

DiscoutPolicy

  • DiscountPolicy
    • 부모클래스에 중복 코드위치
    • DiscountCondition의 리스트를 가지고 있으므로, 여러개의 할인 조건을 포함할 수 있다.
  • calculateDiscountAmount
    • 할인 정책들에 대해 isSatisfiedBy 메서드를 호출한다.
    • 전달된 Screening의 할인 조건을 점검해 만족 여부를 true/false로 반환한다.
package screen;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public abstract class DiscountPolicy {

    private List<DiscountCondition> conditions = new ArrayList<>();

    // ... ; 가변인자
    //      ; 메서드 인수의 갯수가 가변적일 때 인자의 갯수를 동적으로 변경할 수 있다.
    public DiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = Arrays.asList(conditions);
    }

    public Money calculateDiscountAmount(Screening screening) {
        for(DiscountCondition each : conditions) {
            if (each.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }

        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening screening);
}

DiscountCondition

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

SequenceCondition

package fintate.latefee;

import screen.Screening;

public class SequenceCondition implements DiscountCondition {

    private int sequence;

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

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

}

PeriodCondition

package fintate.latefee;

import screen.Screening;

import java.time.DayOfWeek;
import java.time.LocalTime;

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().getDayOfMonth().equals(dayOfWeek) &&
                startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
                endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
    }
}

AmountDiscountPolicy

package fintate.latefee;

import screen.Money;
import screen.Screening;

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;
    }
}

PercentDiscountPolicy

package fintate.latefee;

import screen.Money;
import screen.Screening;

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);
    }
}

📌 오버라이딩과 오버로딩

  • 오버라이딩(overriding)
    • 부모 클래스에 정의된 같은 이름, 같은 파라미터 목록을 가진 메서드를 자식 클래스에서 재정의하는 것
    • 자식 클래스의 메서드가 부모 클래스의 메서드를 가리므로, 외부에서 부모 클래스의 메서드가 보이지 않는다.
  • 오버로딩 (overloading)
    • 메서드의 이름이 동일하지만, 제공되는 파라미터의 목록이 다르다.
public class Money {
	public Money plus(Money money) {
    	return new Money(this.amount.add(amount.amount));
    }
    
    public Money plus(long amount) {
    	reutnr new Money(this.amount.add(BiDecimal.valueOf(amount)));
    }
}

할인 정책 구성하기

  • 요구사항
    • 하나의 영화에 하나의 할인 정책만 설정할 수 있다.
    • 하나의 영화에 여러 개의 할인 조건을 적용할 수 있다.
  • 위의 요구사항을 생성자를 통해 표현할 수 있다.
    • 생성자의 파라미터 목록을 이용해 초기화에 필요한 정보를 전달하도록 강제하면, 올바른 상태를 가진 객체의 생성을 보장할 수 있다.
    public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
        ...
    }

    public DiscountPolicy(DiscountCondition ... conditions) {
        this.conditions = Arrays.asList(conditions);
    }
  • 호출되는 생성자를 통해 적용되는 정책을 파악할 수 있다.
    Movie avatar = new Movie("아바타",
            Duration.ofMinus(120),
            Money.wons(10000),
            new AmountDiscountPolicy(Money.wons(800),
                    new SequenceCondition(1),
                    new SequenceCondition(10),
                    new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(11, 59)),
                    new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(20, 59))
            )
    );

04. 상속과 다형성

Movie 내부에서 할인 정책금액인지, 비율인지를 판단하지 않는다.
내부에 정책을 결정하는 조건문이 없음에도, 어떻게 영화 요금 계산 시 해당 정책을 선택할 수 있을까?

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

  • Movie의 의존성
    • 코드상에서 MovieDiscountPolicy에 의존한다.
    • 실행 시점의 MovieAmountDiscountPolicyPercentDiscountPolicy에 의존한다.
  • 코드의 의존성과 실행 시점의 의존성은 다를 수 있다.
    • 즉, 클래스 사이의 의존성과 객체 사이의 의존성은 다를 수 있다.
  • 유연하고, 쉽게 재사용할 수 있으며, 확장 가능한 객체지향 설계의 특징;
    • 코드의 의존성과 실행시점의 의존성이 다르다.
      • 코드를 이해하기 어려워진다.
        • 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다.
  • 설계가 유연해질 수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다.
    • 반대로, 유연성을 억제하면 코드 이해와 디버깅은 쉬어지지만, 재사용성과 확장가능성은 낮아진다.
      • 가독성과 유연성은 트레이드 오프되는 개념이다.
  • 항상 유연성과 가독성 사이에서 고민해야 한다. 어느 쪽도 명확한 정답은 아니다.

차이에 의한 프로그래밍

  • 상황
    • 기존의 클래스와 매우 유사한 클래스를 추가하고 싶은 경우
  • 해결방법
    • 기존의 코드에서 약간만 수정해서 새로운 클래스를 만드는게 좋을 것.
    • 가장 좋은 방법; 기존 클래스를 수정하지 않고 재사용하는 것.
      • = 상속
  • 핵심개념
    • 상속; 기존 클래스를 수정하지 않고 재사용하는 프로그래밍 기법

📚 상속

  • 객체지향에서 코드를 재사용하기 위해 가장 널리 사용되는 방법
  • 클래스 사이에 관계를 설정하는 것만으로, 기존 클래스가 가지고 있는 모든 속성과 행동을 새로운 클래스에 포함시킬 수 있다.
  • 기존 클래스를 기반으로 새로운 클래스를 쉽고 빠르게 추가할 수 있음
    • 부모 클래스의 구현은 공유, 해동이 다른 자식 클래스를 쉽게 추가할 수 있다.
  • 차이에 의한 프로그래밍 (programming by diference)
    • 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법

상속과 인터페이스

  • 상속의 가치
    • 자식 클래스가 부모가 제공하는 인터페이스를 물려받을 수 있다는 점
  • 업케스팅(upcasting)
    • 자식 클래스가 부모 클래스를 대신하는 것
      • 자식 클래스는 상속을 통해 부모 클래스의 인터페이스를 물려받기 때문에, 부모클래스 대신 사용될 수 있다.
      • 컴파일러는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용한다.

다형성

  • 메시지와 메서드는 다른 것이다.
    • 메시지; 코드상 다른 객체에게 전달되는 요구
    • 메서드; 실제 해당 요구를 수행하는 인스턴스의 구현
  • 다형성
    • 동일한 메시지를 수신했을 때, 객체의 타입에 따라 다르게 응답할 수 있는 능력.
      • 다형적인 협럭에 참여하는 객체들은 모두 같은 메시지를 이해할 수 있어야 한다.
      • 즉, 인터페이스가 동일해야 한다.
        • 예시에서는 인터페이스 통일을 위해 상속을 사용했다.
    • 객체지향 프로그램에서 컴파일 시간 의존성실행시간 의존성이 다를 수 있다는 사실을 기반으로 한다.
    • 다형성을 구현하는 방법은 다양하지만, 메시지에 응답할 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
      • 이를 지연 바인딩(lazy binding) 또는 동적 바인딩(dynamic binding)이라고 부른다.
      • cf. 초기 바인딩(early binding), 정적 바인딩(static binding)
        • 전통적인 함수 호출처럼 컴파일 시점에 실행될 함수나 프로시저를 결정.
      • 지연로딩 이라는 매커니즘을 이용해 객체지향이 컴파일 시점과 실행 시점의 의존성을 분리하고, 하나의 메시지를 여러개의 메서드와 연결할 수 있다.

구현 상속 vs 인터페이스 상속

상속에는 2종류의 상속이 있다.

  1. 구현 상속 (implementation inheritance)
    • 서브클래싱(subclassing)
    • 순수하게 코드를 재사용하려는 목적으로 상속
  2. 인터페이스 상속 (interface inheritance)
    • 서브타이핑(subtyping)
    • 다형적인 협럭을 위해 부모 클래스와 자식 클래스가 인터페이스를 공유할 수 있도록 상속을 이용

상속은 구현 상속이 아니라 인터페이스 상속을 위해 사용해야 한다.
인터페이스 재사용이 아니라 코드 재사용을 목적으로 상속을 사용하면 변경에 취약한 코드를 낳게 될 확률이 높다.

인터페이스와 다형성

  • 인터페이스
    • 자바; 말 그대로 구현에 대한 고려 없이 다형적인 협력에 참옇나느 클래스들이 공유 가능한 외부 인터페이스를 정의
    • C++; 추상 파생 클래스(Abstract Base Class, ABC)를 통해 자바의 인터페이스 개념 구현 가능

05. 추상화와 유연성

추상화의 힘

  • 추상화
    • 같은 계층에 속하는 클래스들이 공통으로 가질 수 있는 인터페이스를 정의하고, 구현의 일부(추상 클래스인 경우) 또는 전체(자바 인터페이스인 경우)를 자식 클래스가 결정하도록 결정권을 위임.
    • 세부적인 내용을 무시한 채 상위 정책을 더 쉽고 가단하게 표현할 수 있다.
  • 추상화의 장점
    1. 추상화의 계층만 따로 떼어놓고 살펴보면, 요구사항의 정책을 더 높은 수준에서 서술할 수 있다.
    2. 설계가 더 유연해진다.

유연한 설계

  • 책임의 위치를 결정하기 위해 조건문을 사용하는 것은 협력의 설계 측면에서 좋지 않은 선택이다.
    • 항상 예외 케이스를 최소화하고 일관성을 유지할 수 있는 방법을 선택해라.

예시

  • 아래 방식은 할인 정책이 없는 경우를 예외 케이스로 취급하기 때문에 일관성이 깨진다.
public class Movie {
	public Money calculateMovieFee(Screening screening) {
    	if (discountPolicy == null) {
        	return fee;
        }
        
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}
  • 일관성을 유지하기 위해서는, 할인 요금을 계산할 책임을 그대로 DiscountPolicy 계층에 유지시키는 것이다.
public class NoneDiscountPolicy extends DiscountPolicy {
	@Override
    protected Money getDiscountAmount(Screening screening) {
    	return Money.ZERO;
    }
}
  • 유연성이 필요한 곳에 추상화를 사용해라.
    • 추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다.

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

  • 부모 클래스인 DiscountPolicy에서 할인 조건이 없을 경우 getDiscountAMount를 호출하지 않는다.
    • 이는 부모 클래스인 DiscountPolicyNoneDiscountPolicy를 개념적으로 결합시킨다.
      • 개발자가 getDiscountAmount()가 호출되지 않을 경우 DiscountPolicy가 0원을 반환할 것이라는 사실을 가정하고 있기 때문이다.
  • 해결 방법
    • DiscountPolicy를 인터페이스로 변경한다.
    • NoneDiscountPolicycalculateDiscountAmount()를 오버라이딩 하도록 변경한다.

DiscountPolicy.java

public interface DiscountPolicy {
	Money calculateDiscountAmount(Screening screening);
}

DefaultDiscountPolicy.java

  • 원래의 DiscountPolicy의 이름을 DefaultDiscountPolicy로 변경하고, 인터페이스를 구현하도록 수정한다.
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
	...
}

NoneDiscountPolicy.java

public class NoneDiscountPolicy implements DiscountPolicy {
	@Override
    public Money calculateDiscountAmount(Screening screening) {
    	return Money.ZERO;
    }
}

최종 결과물 계층도

💭 트레이드 오프

어떤 설계가 더 좋은 설계일까?

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

구현과 관련된 모든 것들이 트레이드오프의 대상이 될 수 있다. 우리가 작성하는 모든 코드에는 합당한 이유가 있어야 한다. 비록 아주 사소한 결정이더라도, 트레이드 오프를 통해 얻어진 결론과 그렇지 않은 결론 사이의 차이는 크다. 고민하고 트레이드 오프하라.

코드 재사용

  • 코드를 재사용할 방법
    1. 상속
    2. 합성(composition)
      • 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법
      • 객체지향에서 흔히 상속보다 더 선호되는 방식.

상속

  • 상속; 객체지향에서 코드를 재사용하기 위해 흔히 사용되는 방법
    • 문제점1. 캡슐화 위반
      • 상속을 위해서는 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
      • 그림에서, MoviecalculateFee메서드 안에 추상 메서드인 getDiscountAmout()를 호출한다는 사실을 알고 있어야 한다.
        • 부모 클래스의 구현이 자식에게 노출되므로, 캡슐화가 약화된다.
        • 자식 클래스가 부모 클래스에 강하게 결합되므로, 부모 클래스 변경 시 자식 클래스도 함께 변경될 확률이 높다.
    • 문제점2. 설계를 유연하지 못하게 만든다.
      • 상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정한다.
      • 실행 시점에 금액 할인 정책인 영화를 비율 할인 정책으로 변경할 수 없다.
        • 상속은 코드 레벨에서 정해지기 때문.
  • 상속보다 인스턴스 변수로 관계를 연결하는 것이 더 유연하다.
public class Movie {
	private DiscountPolicy discountPolicy;
    
    public void changeDiscountPolicy(DisocuntPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
}

합성

  • 합성; 인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법.
    • 장점1. 구현을 효과적으로 캡슐화
      • 인터페이스에 정의된 메시지를 통해서만 재사용이 가능하므로, 구현을 효과적으로 캡슐화한다.
    • 장점2. 설계를 유연하게 만든다.
      • 의존하는 인스턴스를 교체하는 것이 비교적 쉬움.

결론

  • 코드 재사용의 경우, 상속보다 합성이 권장됨.
    • 단, 다형성을 위해 인터페이스를 재사용하는 경우, 상속과 합성을 함께 조합해서 사용.
  • 객체지향이란 객체를 지향하는 것이다.
    • 패러다임의 중심에 객체가 위치. 각 객체를 떼어놓고 이야기하는 것은 무의미하다.
    • 중요한 부분; 애플리케이션의 기능을 구현하기 위해 협력에 참여하는 객체들 사이의 상호작용.
  • 객체지향 설계의 핵심
    • 적절한 협력을 식별하고, 협력에 필요한 역할을 정의한 후에, 역할을 수행할 수 있는 적절한 객체에게 적절한 책임을 할당하는 것.

Reference

  • 오브젝트 | 조영호
profile
Good Luck!

0개의 댓글