Code/OOP

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

noahkim_ 2025. 4. 2. 23:52

조영호 님의 "오브젝트" 책을 정리한 글입니다.


1. 영화 예매 시스템

요구사항 살펴보기

  • 사용자에게 예매 서비스 제공
    • 할인 조건을 만족할 경우 할인 정책에 따른 금액을 할인받을 수 있음
    • 예매 완료 시, 예매 정보 생성

 

정보
구분 설명
영화 정보 영화의 기본 정보
상영 정보 요일, 시작 시간, 종료 시간
예매 정보 제목, 상영 정보, 인원, 정가, 결제 금액

 

할인 적용 방식
구분 설명
할인 조건 (영화별로 여러 개 적용 가능)  
🔹 순서 조건 특정 상영 순번에 따라 할인 적용
🔹 기간 조건 특정 상영 시간에 따라 할인 적용
할인 정책 (영화별로 1개만 적용 가능)  
🔹 금액 할인 정책 일정 금액 할인
🔹 비율 할인 정책 정가에서 일정 비율 할인

 

 

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

협력, 객체, 클래스

  • 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때 일어남
  • 이를 위해 다음 2가지에 집중해야 함
  1. 어떤 객체들이 필요한 지 고민하기
    • 클래스는 객체들이 공통적으로 가져야 할 상태와 행동을 정의한 것
    • 클래스 정의 시 객체들이 가져야 할 할 상태와 행동을 먼저 결정해야 함
  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)

 

합성의 장점
  • 상속이 가지는 문제점 해결
    • 구현을 효과적으로 캡슐화 가능
    • 인스턴스 객체 변경이 쉬움