조영호 님의 "오브젝트" 책을 정리한 글입니다.
1. 영화 예매 시스템
요구사항 살펴보기
- 사용자에게 예매 서비스 제공
- 할인 조건을 만족할 경우 할인 정책에 따른 금액을 할인받을 수 있음
- 예매 완료 시, 예매 정보 생성
정보
구분 | 설명 |
영화 정보 | 영화의 기본 정보 |
상영 정보 | 요일, 시작 시간, 종료 시간 |
예매 정보 | 제목, 상영 정보, 인원, 정가, 결제 금액 |
할인 적용 방식
구분 | 설명 |
할인 조건 (영화별로 여러 개 적용 가능) | |
🔹 순서 조건 | 특정 상영 순번에 따라 할인 적용 |
🔹 기간 조건 | 특정 상영 시간에 따라 할인 적용 |
할인 정책 (영화별로 1개만 적용 가능) | |
🔹 금액 할인 정책 | 일정 금액 할인 |
🔹 비율 할인 정책 | 정가에서 일정 비율 할인 |
2. 객체지향 프로그래밍을 향해
협력, 객체, 클래스
- 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때 일어남
- 이를 위해 다음 2가지에 집중해야 함
- 어떤 객체들이 필요한 지 고민하기
- 클래스는 객체들이 공통적으로 가져야 할 상태와 행동을 정의한 것
- 클래스 정의 시 객체들이 가져야 할 할 상태와 행동을 먼저 결정해야 함
- 객체를 독립적인 존재가 아닌 기능을 구현하기 위해 협력하는 공동체의 일원으로 보기
- 객체는 협력적인 존재
- 유연하고 확장가능한 설계를 가능케 함
도메인의 구조를 따르는 프로그램 구조
도메인
- 문제를 해결하기 위해 도입된 시스템에서 사용하는 분야
객체
개념 | 설명 |
객체 |
현실 세계 또는 개념을 프로그램에서 표현한 독립적인 존재
|
객체지향 패러다임 |
개발 주기 동안 객체라는 동일한 추상화 기법을 사용
|
도메인 연결 |
현실 도메인의 개념들이 프로그램 내에서 객체와 클래스로 매핑될 수 있음
|
클래스 다이어그램 |
객체 간의 관계를 표현하며, 시스템 및 도메인 구조를 시각적으로 표현하는 데 용이함
|
클래스 구현하기
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 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);
}
}
public class Money {
public static final Money ZERO = Money.wons(0);
private final BigDecimal amount;
public Money(BigDecimal amount) { this.amount = amount; }
public static Money wons(long 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; }
}
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;
}
}
협력에 관한 짧은 이야기
- 객체간 협력은 서로간의 요청과 응답을 통해 이루어짐
- 요청자는 응답자에게 메시지를 보내 요청하고, 응답자는 자신의 메서드를 호출하여 처리 및 응답함
- 메시지와 메서드의 구분에서부터 다형성이 출발함
3. 할인 요금 구하기
할인 요금 계산을 위한 협력 시작하기
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)); }
}
calculateMovieFee()
- 적용될 할인 정책을 결정하는 코드가 없음
- 상속, 다형성, 추상화 개념을 기반으로 할인 정책이 결정됨
할인 정책과 할인 조건
DiscountPolicy
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DefaultDiscountPolicy(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);
}
더보기
public class AmountDiscountPolicy extends DefaultDiscountPolicy {
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 DefaultDiscountPolicy {
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); }
}
구분 | 설명 |
할인 정책 (추상 클래스) |
- 하나의 할인 정책은 여러 개의 할인 조건을 포함할 수 있음
- 할인 조건을 만족하는 예매가 있는지 확인함 - 할인 조건을 만족하면 각 자식 클래스에서 구현된 메서드를 호출하여 할인 요금을 계산함 |
Template Pattern |
- 부모 클래스가 기본적인 알고리즘의 흐름을 구현
- 중간 과정은 자식 클래스에 위임하여 필요한 처리를 구현 - 코드 중복을 줄이고, 유연한 설계를 가능하게 함 |
<<DiscountCondition>>
public interface DiscountCondition {
boolean isSatisfiedBy(Screening 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); }
}
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;
}
public boolean isSatisfiedBy(Screening screening) {
return screening.getStartTime().getDayOfWeek().equals(dayOfWeek) &&
startTime.compareTo(screening.getStartTime().toLocalTime()) <= 0 &&
endTime.compareTo(screening.getStartTime().toLocalTime()) >= 0;
}
}
- 인자로 전달된 screening이 할인 가능한 지 판단
- 각 구현체에서 자신의 조건에 맞는 로직을 구현함
할인 정책 구성하기
- 생성자 선언을 통해 영화는 하나의 할인 정책만, 할인 정책은 여러 할인 조건을 받도록 보장할 수 있음
4. 상속과 다형성
컴파일 시간 의존성과 실행 시간 의존성
컴파일 시간 의존성 | 실행 시간 의존성 |
코드가 작성된 시점에서 의존하는 클래스 |
프로그램 실행 시점에서 실제로 어떤 객체가 사용되는지
|
Movie 클래스는 DiscountPolicy(추상 클래스)를 의존하지만, 특정 구현체는 의존하지 않음 |
Movie 객체가 생성될 때 DiscountPolicy의 구현체가 주입됨
|
실행 시간 의존성의 장단점
개념 | 설명 |
장점 | 확장성: 새로운 할인 정책을 추가해도 Movie 코드를 변경할 필요 없음 유연성: 런타임에 원하는 구현체를 주입할 수 있음 (코드 수정 X) |
단점 |
코드만 보고 이해하기 어려움
- 코드 상에서는 Movie가 어떤 할인 정책을 사용하는지 알 수 없음 실행 시점에 결정되므로 추적이 어려울 수 있음 |
차이에 의한 프로그래밍
- 상속을 사용하여 중복된 곳을 재사용할 수 있음
- 부모 클래스의 다른 부분만 추가해서 빠르게 만드는 방법을 차이에 의한 프로그래밍이라 함
상속과 인터페이스
- 상속은 인터페이스를 물려받을 수 있다는 측면에서 가치가 있음
- 여러 클래스의 인터페이스를 통일시킬 수 있음
- 자식 클래스는 부모클래스가 받을 메시지를 처리할 능력이 있음
- 즉, 부모클래스 대신 사용될 수 있음
업캐스팅
- 자식 클래스가 부모 클래스를 대신하는 것
- 컴파일러는 코드 상에서 부모 클래스가 나오는 모든 장소에서 자식 클래스를 사용하는 것을 허용함
다형성
- 요청에 대한 메시지에 대해 어떻게 처리될지는 수신하는 클래스가 무엇이냐에 따라 달라지는 성질
- 컴파일 타임 의존성과 런타임 의존성이 다를 수 있다는 객체지향의 특성을 이용한 것
바인딩 유형 | 설명 | 예시 |
동적 바인딩
|
런타임에 메서드가 결정됨 (다형성) | 오버라이딩된 메서드 호출 시 |
실행 시점에서 실제 객체의 타입에 따라 메서드 결정 | Parent p = new Child(); → p.someMethod(); (Child의 메서드 실행) |
|
정적 바인딩
|
컴파일 타임에 메서드가 결정됨 | 오버로딩된 메서드 호출 시 |
참조 변수의 타입을 기준으로 호출할 메서드 결정 | AClass obj = new AClass(); → obj.someMethod(int a); |
인터페이스와 다형성
- 구현은 공유할 필요가 없고, 인터페이스만 공유하고 싶을 때 '인터페이스'라는 요소를 제공함
- 이 경우에도 업캐스팅이 적용되며 다형성이 사용된다
5. 추상화와 유연성
추상화의 힘
개념 | 설명 |
상위 정책을 간단하게 표현 |
세부적인 내용을 무시하고 핵심 개념만 표현할 수 있음.
도메인의 중요한 개념을 설명하는 데 적합 디자인 패턴이나 프레임워크는 추상화를 이용해 상위 정책을 정의하여 일관된 구조를 제공 |
협력 흐름을 기술 |
자식 클래스들은 상위 클래스(또는 인터페이스)가 정의한 협력 흐름을 그대로 따름
|
유연한 설계
확장성
- 다양한 구현체를 쉽게 교체할 수 있음
- 새로운 클래스를 추가하는 것만으로 애플리케이션의 기능을 확장함
- 기존 코드를 수정하지 않고도 확장 가능
추상 클래스와 인터페이스 트레이드오프
- 정의된 추상클래스의 흐름을 따르지 않지만 비슷할 경우, 윗 레벨에 추상화 계층이 하나 더 추가될 수도 있음
- 이에 따른 시스템 복잡도가 높아짐
- 만약 추가된 상위레벨의 추상화 계층을 상속받는 추상 클래스의 구현체가 오직 하나일 경우, 작업량에 비해 효과가 미비할 수 있음
- 이러할 경우 합당한 이유로 트레이드 오프 원칙을 세워 도입 여부를 결정해야 함
상속 vs 합성
구분 | 상속(Inheritance) | 합성(Composition) |
개념 | 부모 클래스의 기능을 자식 클래스가 물려받아 확장 | 기존 객체를 포함하여 기능을 조합 |
캡슐화 | 부모 클래스의 구현이 자식 클래스에 노출됨 (캡슐화 위반 가능) | 내부 구현이 감춰지고 인터페이스를 통해 접근 (캡슐화 유지) |
결합도 | 강결합 (부모-자식 간 강한 의존) | 약결합 (객체 간 독립성이 높음) |
유연성 | 컴파일 타임에 클래스 간 관계가 결정됨 (변경이 어려움) | 런타임에 객체 변경 가능 (유연함) |
코드 재사용 | 코드 재사용 가능하지만 부모 클래스에 의존 | 인터페이스를 통해 코드 재사용 가능 |
변경 영향 | 부모 클래스 변경 시 자식 클래스도 영향받음 | 독립적 변경이 가능 |
다형성 활용 | 가능 (오버라이딩, 추상 클래스 활용) | 가능 (인터페이스를 활용) |
사용 예시 | is-a 관계 (ex. Bird extends Animal) | has-a 관계 (ex. Car has an Engine) |
합성의 장점
- 상속이 가지는 문제점 해결
- 구현을 효과적으로 캡슐화 가능
- 인스턴스 객체 변경이 쉬움
'Code > OOP' 카테고리의 다른 글
[오브젝트] 4. 설계 품질과 트레이드오프 (0) | 2025.04.03 |
---|---|
[오브젝트] 3. 역할, 책임, 협력 (0) | 2025.04.03 |
[객체지향의 사실과 오해] 2. 이상한 나라의 객체 (1) | 2025.04.02 |
[객체지향의 사실과 오해] 1. 협력하는 객체들의 공동체 (0) | 2025.04.02 |
[오브젝트] 1. 객체, 설계 (0) | 2025.04.02 |