[오브젝트] 2장 객체지향 프로그래밍 (1)

ppparkta·2025년 1월 17일
0

오브젝트

목록 보기
3/6
post-thumbnail

객체지향 설계

경계의 명확성이 객체의 자율성을 보장한다

영화 예매 시스템

영화의 상영을 예매하는 영화 예매 시스템 개발

요구사항

  • 한 영화에는 여러 개의 상영 시간이 존재한다.
  • 한 영화에는 할인 정책과 할인 조건이 존재한다.
  • 한 영화에 대해 할인 정책은 하나만 지정할 수 있지만 할인 조건은 여러 개 지정할 수 있다.
  • 할인 정책에는 금액 할인(ex) 800원 할인)과 비율 할인(ex) 10% 할인)이 있다.
  • 할인 조건에는 순번 할인과 기간 할인이 있다.

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

도메인
문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야

일반적으로 클래스 이름은 대응되는 도메인 개념의 이름과 동일하거나 유사하게 지어야 한다.

도메인 개념 사이에 맺어진 관계와 유사하게 만들어서 프로그램 구조를 이해하고 예상하기 쉽게 만들어야 한다.

도메인 구조 클래스 구조

인스턴스 변수의 접근제거자는 private, 메서드는 public이다. 클래스의 경계를 구분짓는 것은 중요하다. 그 이유는 무엇일까?

경계의 명확성이 객체의 자율성을 보장하기 때문이다. 또한 같은 이유로 프로그래머에게 구현의 자유를 제공하기 때문이다.

이러한 개념을 캡슐화라고 부른다. 캡슐화와 접근제어자는 객체를 두 부분으로 나눈다.

  1. 외부에 공개할 수 있는 public interface
  2. 내부에서만 동작해야 하는 implementation

인터페이스와 구현의 분리(SOLID 중 ISP)은 훌륭한 객체지향 프로그램을 만들기 위한 핵심 원리이다.

이제 객체들 간 협력을 구현해보기 전, 모르는 타입이 나와서 짤막하게 정리했다.

BigDecimal

  • java에서 숫자를 가장 정밀하게 표현할 수 있는 방법
  • double도 소수점 정밀도에 한계가 있기 때문에 사실 상 BigDecimal이 정밀도 측면에서 유일한 선택지이다.
  • 아래 예제에서 valueOf로 값을 변환하는 이유는 double 값을 그대로 넣었을 때 손실이 발생할 수 있기 때문임.
    public static Money wons(double amount) {
        return new Money(BigDecimal.valueOf(amount));
    }
    아래와 같이 BigDecimal 내부 valueOf는 double을 문자열로 변환한 후에 값을 변환하므로 가장 정확하다.
    public static BigDecimal valueOf(double val) {
        // Reminder: a zero double returns '0.0', so we cannot fastpath
        // to use the constant ZERO.  This might be important enough to
        // justify a factory approach, a cache, or a few private
        // constants, later.
        return new BigDecimal(Double.toString(val));
    }
    Java, BigDecimal 사용법 정리

협력하는 객체들의 공동체

앞에서 등장했던 BigDecimal은 Money 객체를 구현하기 위해서 사용했다.

1장에서 money를 long으로 구현했던 것은 의미를 곧바로 전달하기도 어렵고 중복되는 연산을 피할 수 없었다. 따라서 하나의 멤버를 갖더라도 의미를 명확히 전달하기 위해서 객체를 만드는 것이 좋다.

Money 클래스는 지금 내 수준에 비해 난이도가 높은 코드인 것 같다. 일단 BigDecimal을 접해봤다는 점에서 굿굿. 자주 쓰는 0, 1, 100은 상수처리 되어 있다고 한다.

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 = 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(Money amount) {
        return new Money(this.amount.multiply(amount.amount));
    }

    public boolean isLessThan(Money other) {
        return amount.compareTo(other.amount) < 0;
    }

    public boolean isGreaterThanOrEqual(Money other) {
        return amount.compareTo(other.amount) >= 0;
    }
}

위에서 Screening 클래스 안에 reserve 메서드를 추가했는데, 그를 통해 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;
    }
}

지금까지 만들어본 Screening 클래스, Movie 클래스, Reservation 클래스 사이의 협력 관계를 그림으로 나타내면 아래와 같다.

할인 요금 구하기

영화 예매 시 할인을 제공하기 위해서 DiscountPolicy와 DiscountCondition을 구현한다.

그에 앞서, 코드를 다루지 않고 넘어갔던 Movie 클래스를 보면 calculateMovieFee 메서드를 통해 할인율을 계산한다.

위의 그림에서 일련의 과정을 확인할 수 있다. 실제로 낼 금액을 각각 calculateFee, calculateMovieFee 메서드를 거쳐서 계산할 수 있다.

public class Movie {
    private String title;
    private Duration runningTime;
    private DiscountPolicy discountPolicy;
    private Money fee;

    public Money getFee() {
        return fee;
    }

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

앞서 살펴본 할인 정책과 할인 조건은 여러 종류가 존재했다. 그러나 Movie 클래스의 필드에는 하나의 DiscountPolicy만 존재한다. 이는 다형성의 개념을 통해 구현할 수 있다.

이를 위해서 DiscountPolicy 클래스는 추상클래스로 구현했다. 구현체만 필요하므로 추상클래스를 사용했다.

할인을 적용하는 일련의 과정은 동일하지만, 할인율을 계산하는 방법에서 약간의 차이가 있기 때문에 추상메서드를 통해 이를 계산했다.

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 condition : conditions) {
            if (condition.isSatisfiedBy(screening)) {
                return getDiscountAmount(screening);
            }
        }
        return Money.ZERO;
    }

    abstract protected Money getDiscountAmount(Screening screening);
}

template method 패턴
위의 DiscountPolicy 클래스와 같이 공통된 흐름은 부모 클래스에서 처리하고 중간 연산만 자식 클래스에 위임하는 것을 template method 패턴이라고 한다.

DiscountPolicy에 대한 구현체는 간단하게 추가할 수 있다. 할인되는 금액의 양만을 계산하면 되기 때문에 각 정책에 맞게 구현체를 추가했다.

public class AmountDiscountPolicy extends DiscountPolicy {
    private Money 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);
    }
}

DiscountCondition은 인터페이스로 선언했다.

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

이에 대한 각각의 구현체를 추가했다.

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 SequenceCondition implements DiscountCondition {
    private int sequence;

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

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

이렇게 추상화된 객체를 포함하여 할인 과정을 그리면 아래와 같다.

다음 파트에서는 상속과 다형성의 개념에 대해서 추가로 학습한다.

profile
겉촉속촉

0개의 댓글