본문 바로가기

Java/오브젝트 (교재)

2장. 객체지향 프로그래밍

아래는 조영호 저자의『오브젝트』 1장 “객체, 설계” 내용을 정리한 글입니다.


진정한 객체지향 패러다임으로의 전환은 클래스가 아니라 객체에 초점을 맞출 때에만 얻을 수 있다.

  • 어떤 클래스를 만들지 고민하기 전에, 어떤 객체들이 필요한지 먼저 고민하라.
  • 객체를 독립된 존재가 아니라, 기능을 구현하기 위해 협력하는 공동체의 일원으로 보아야 한다.

1. 도메인이란?

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

 

2. 예제 UML

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

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

변수는 private, 메서드는 public
=> 경계의 명확성이 객체의 자율성을 보장하고, 프로그래머에게 구현의 자유를 제공한다.


3. 자율적인 객체

  • 객체는 상태(state) 행동(behavior)을 함께 가지는 복합적인 존재
  • 객체는 스스로 판단하고 행동하는 자율적인 존재
  • 캡슐화를 위해 접근 제어자를 적극 활용하자.

4. 프로그래머의 자유

프로그래머 역할을 두 가지로 구분하자.

  1. 클래스 작성자: 새로운 데이터 타입을 프로그램에 추가
  2. 클라이언트 프로그래머: 클래스 작성자가 추가한 데이터 타입을 사용

접근 제어자로 객체의 외부와 내부를 구분하면,

클라이언트 프로그래머가 알아야 할 지식의 양이 줄어들고
클래스 작성자는 구현을 자유롭게 변경할 수 있다.


5. 협력하는 객체들의 공동체

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

    private Money(BigDecimal amount) {
        this.amount = amount;
    }

    public Money plus(Money other) {
        return new Money(this.amount.add(other.amount));
    }

    public Money minus(Money other) {
        return new Money(this.amount.subtract(other.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;
    }
}

(2) 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 인스턴스들이 서로의 메서드를 호출하며 상호작용


6. 협력에 관한 짧은 이야기

  • 객체는 다른 객체의 인터페이스에 공개된 행동을 요청(request) 할 수 있다.
  • 요청을 받은 객체는 자율적으로 처리한 후 응답(response) 한다.
  • 객체 간 상호작용의 유일한 방법은 메시지 전송(send a message) 이다.
  • 메시지를 받으면 수신(receive a message) 했다고 표현하며,
    이 메시지를 처리하기 위한 방법이 바로 메서드이다.

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

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

중요 포인트 !

  • Movie 클래스에는 할인 정책 자체가 구현되어 있지 않다.
  • 실제 할인 계산은 DiscountPolicy가 담당한다.

8. 할인 정책과 조건

  • 할인 정책은 금액 할인(AmountDiscountPolicy)비율 할인(PercentDiscountPolicy) 두 가지
  • 부모 클래스 DiscountPolicy에 중복 코드를 두고,
    자식 클래스가 실제 할인 금액을 제공하도록 설계
public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions;

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

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

    protected abstract Money getDiscountAmount(Screening screening);
}

 

(1) DiscountCondition 인터페이스

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

 

(2) SequenceCondition

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

 

(3) PeriodCondition

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

 

(4) AmountDiscountPolicy

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

 

(5) PercentDiscountPolicy

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

9. 할인 정책 구성하기

  • 하나의 영화에는 하나의 할인 정책만 설정 가능
  • 여러 개의 할인 조건을 적용할 수 있다
Movie avatar = new Movie(
    "아바타",
    Duration.ofMinutes(120),
    Money.wons(10000),
    new AmountDiscountPolicy(
        Money.wons(800),
        new SequenceCondition(1),
        new PeriodCondition(DayOfWeek.MONDAY, LocalTime.of(10, 0), LocalTime.of(11, 59)),
        new PeriodCondition(DayOfWeek.THURSDAY, LocalTime.of(10, 0), LocalTime.of(20, 59))
    )
);

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

  • 컴파일 시점MovieDiscountPolicy 인터페이스만 알고 있다.
  • 실행 시점에 구체 구현(AmountDiscountPolicy 등)이 주입된다.

 

11. 템플릿 메서드 패턴

  • DiscountPolicy비즈니스 로직 흐름을 제공하고,
  • 자식 클래스에 구체 처리를 위임
  • 이 패턴을 Template Method라고 부른다.

 

12. 프로그래밍 by Difference

  • 부모 클래스와 다른 부분만 추가해 새로운 클래스를 쉽고 빠르게 만드는 방법

 

13. 상속과 다형성

  • 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메시지를 수신 가능
  • 외부 객체는 자식 클래스를 부모 타입으로 간주할 수 있다
  • 이를 업캐스팅(upcasting)이라고 한다

 

14. 인터페이스와 다형성

  • 순수한 인터페이스만으로도 다형성을 구현할 수 있다
  • 예: DiscountPolicy 인터페이스를 직접 구현하는 NoneDiscountPolicy

 

15.추상화의 힘

  • DiscountPolicyAmountDiscountPolicy, PercentDiscountPolicy보다 추상적
  • 추상화를 통해 요구사항의 정책을 높은 수준에서 표현
  • 디자인 패턴과 프레임워크도 이 개념을 사용

 

16. 유연한 설계

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()
);
  • 예외 케이스(할인 없음)를 0원 정책으로 처리하여 일관성 유지

 

17. 추상 클래스 vs 인터페이스 트레이드오프

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

public abstract class DefaultDiscountPolicy implements DiscountPolicy {
    private List<DiscountCondition> conditions;

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

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

    protected abstract Money getDiscountAmount(Screening screening);
}

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

 

18. 코드 재사용: 상속 vs 합성

  • 상속은 캡슐화를 위반하고 설계를 경직되게 만든다.
  • 합성(Composition)을 통해 다른 객체의 인스턴스를 포함하여 재사용하라.
  • 예: MovieDiscountPolicy 인스턴스를 참조하여 할인 로직 재사용
public class Movie {
    private DiscountPolicy discountPolicy;

    public void changeDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}

 

중요!
유연성이 필요한 곳에 추상화를 사용하라.
(할인 정책, 할인 조건 등 다양한 종류가 존재하기 때문)

'Java > 오브젝트 (교재)' 카테고리의 다른 글

3장. 역할, 책임, 협력  (1) 2025.05.12
1장. 객체, 설계  (0) 2025.05.11