02.객체지향 프로그래밍
01 온라인 영화 예매 시스템
영화 = 영화에 대한 기본적인 정보(제목, 상영시간, 가격 정보)
상영 = 실제로 관객들이 영화를 관람하는 사건(상영 일자, 시간, 순번)
사용자가 실제로 예매하는 대상은 영화가 아니라 상영이다.
특정한 조건을 만족하는 예매자는 요금을 할인받는다.(할인액)
할인조건, 할인정책
할인조건: 가격의 할인 여부 결정
- 순서 조건: 상영 순번을 이용해 할인 여부를 결정 ex)10번째로 상영
- 기간 조건: 영화 상영 시작 시간을 이용해 할인 여부를 결정 ex) 오전10~오후1시 상영되는 모든 영화에 대해 할인
요일, 시작 시간, 종료 시간의 세 부분으로 구성
영화 시작 시간이 해당 기간 안에 포함될 경우 요금 할인
할인 정책: 할인 요금을 결정
- 금액 할인 정책: 예매 요금에서 일정 금액을 할인해주는 방식 ex)800원 할인
- 비율 할인 정책: 정가에서 일정 비율의 요금을 할인 ex) 10%할인
02 객체지향 프로그래밍을 향해
협력, 객체 클래스
- 어떤 클래스가 필요한지를 고민하기 전에 어떤 객체들이 필요한지 고민하라.
클래스: 공통적인 상태와 행동을 공유하는 객체들을 추상화
어떤 객체들이 어떤 상태와 행동을 가지는지를 먼저 결정해야 한다. - 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다.
도메인의 구조를 따르는 프로그램 구조
도메인: 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
🔺(의문점)p.42 할인 조건이 반드시 1개 이상 있어야 한다"는 제약(1..)??0..아닌가
p.40 할인 정책은 적용돼 있지만 할인 조건을 만족하지 못하는 경우나 아예 할인 정책이 적용돼 있지 않은 경우에는 요금을 할인하지 않는다.
클래스 구현
public class Screening {//상영(사용자들이 예매하는 대상)
private Movie movie;//영화(인스턴스 변수)
private int sequence;//순번
private LocalDataTime whenScreened;//상영 시작 시간
public Screening(Movie movie, int sequence, LocalDataTime whenScreened) {
this.movie = movie;
this.sequence = sequence;
this.whenScreened = whenScreened;
}
public LocalDataTime getStartTime() {//상영 시작 시간
return whenScreened;
}
public boolean isSequence(int sequence) { //순번의 일치 여부 검사
return this.sequence == sequence;
}
public Money getMovieFee() { //기본 요금 반환
return movie.getFee();
}
}
인스턴스 변수의 가시성은 private
메소드의 가시성은 public
클래수는 내부와 외부로 구분
경계의 명확성이 객체의 자율성을 보장/프로그래머에게 구현의 자유 제공
자율적인 객체
- 객체가 상태(state) 와 행동(behavior) 가진다.
- 객체가 스스로 판단하고 행동하는 자율적인 존재
객체지향은 객체라는 단위 안에 데이터와 가능을 한 덩이리로 묶음으로써 문제 영역의 아이디어를 적절하게 표현
캡슐화 = 데이터와 기능을 객체 내부로 함께 묶음
접근제어 = 외부에서의 접근을 통제
접근 수정자 = 접근 제어를 위해 public, protected, private
캡슐화와 접근 제어는 두 부분으로 나눈다
퍼블릭 인터페이스: 외부에서 접근 가능한 부분
구현: 내부에서만 접근 가능한 부분
인터페이스와 구현의 분리!!
객체 상태는 숨기고 행동만 외부에 공개
클래스 속성: private
일부 메서드: public
어떤 메서드: 서브클래스나 내부에서 접근 가능해야 한다면 가시성을 protected나 private
퍼블릭 인터페이스: public, private 메서드나 protected메서드, 속성은 구현에 포함
프로그래머의 자유
프로그래머의 역할
클래스 작성자: 새로운 데이터 타입을 프로그램에 추가
클라이언트 프로그래머: 클래스 작성자가 추가한 데이터 타입을 사용
클라이언트 프로그래머 목표: 필요한 클래스들을 엮어서 애플리케이션을 빠르고 안정적으로 구축하는 것
구현은닉? 클라이언트 프로그래머가 숨겨 놓은 부분에 마음대로 접근할 수 없도록 방지함으로써 클라이언트 프로그래머에 대한 영향을 걱정하지 않고도 내부 구현을 마음대로 변경할 수 있다.(접근 제어 메커니즘)
협력
Money타입: 저장하는 값이 금액과 관련돼 있다는 의미를 전달, 금액과 관련된 로직이 서로 다른 곳에 중복 구현 막음 vs Long
객체지향 장점은 객체를 이용해 도메인의 의미를 풍부하게 표현
🔺 p.47 Money 타입을 만들 때 내부를 long 으로 할지, BigDecimal 로 할지는?
단순한 정수 단위 금액/ ₩ 단위만 다루는 쇼핑몰: private final long amount;
소수점 단위까지 필요한 실제 화폐 계산/ 결제 서비스: private final BigDecimal amount;
요청(request): 객체는 다른 객체의 인터페이스에 공개된 행동을 수행
응답(response): 요청을 받은 객체는 자율적인 방법에 따라 요청을 처리한 후 응답
메서드(method): 수신된 메시지를 처리하기 위한 방법
메시지(요청)vs메서드(구현)
다형성(polymorphism): 같은 메시지에 다른 구현
🔺 p.49 메서드를 호출한다(call)가 아니라 메시지를 전송한다(send a message)가 더 적절하다?
Screening이 Movie의 calculateMovieFee() 메서드를 호출한다: 절차적 사고(함수)
Screening이 Movie에게 calculateMovieFee라는 메시지를 전송한다: 객체지향 사고(협력)
03 할인 요금 구하기
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discoutPolicy;
public Move(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.mius(discountPolicy.calculateDiscountAmout(screening));
}
}
할인 정책을 판단하는 코드가 존재하지 않는다.
이 코드에는 상속(inheriatnace)와 다형성이 존재한다. 그리고 그 기반에는 추상화(abstraction)의 원리가 숨겨져 있다.
할인
public abstarct class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondion ... conditions) {
this.coundtions = Arrays.asList(conditions);
}
public Money calculateDiscountAmount(Screening screnning) {
for(DiscountCondition each : conditions) {
if(each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening Screening);
}
🔺p.52 DiscountPolicy의 인스턴스를 생성할 필요없기 때문에 추상 클래스(abstract class)로 구현했다.
할인 계산 구조만 제공하고 얼마를 할인할지는 정하지 않았기 때문에 getDiscountAmount 메서드를 호출해 할인 요금을 계산한다. 만족하는 할인 조건이 하나도 존재하지 않는다면 할인 요금으로 0을 반환한다.
인스턴스화 = new: 설계도(Class)를 가지고 실제 객체(Object)를 만든다.
DiscountPolicy는 DiscountCondition의 리스트인 conditions를 인스턴스 변수로 가지기 때문에 하나의 할인 정책은 여러 개의 할인 조건을 포함할 수 있다.
TEMPLATE METHOD패턴: 부모 클래스에 기본적인 알고리즘의 흐름을 구현하고 중간에 필요한 처리를 자식 클래스에게 위임하는 디자인 패턴
할인 정책
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;
}
}
...는 가변 인자(varargs) 문법
0개 이상의 객체를 받을 수 있다.
DiscountPolicy의 getDiscountAmount메서드를 오버라이딩한다. 할인 요금은 인스턴스 변수인 discountAmount에 저장한다.
| 접근 수준 | 기호 | 설명 |
|---|---|---|
| public | + | 어디서나 접근 가능 |
| protected | # | 클래스 내, 서브클래스에서 접근 가능 |
| private | - |
오버라이딩과 오버로딩
오버라이딩: 부모 클래스에 정의된 같은 이름, 같은 파라미터 목록을 가진 메서드를 자신 클래스에서 재정의
자식 클래스의 메서드는 오버라이딩한 부모 클래스의 메서드를 가리기 때문에 외부에서는 부모 클래스의 메서드가 보이지 않는다.
오버로딩: 메서드의 으름은 같지만 제공되는 파라미터의 목록이 다르다.
오버로딩한 메서드는 원래의 메서드를 가리지 않기 때문에 메서드들은 사이 좋게 지낸다.
04 상속과 다형성
컴파일 시간 의존성과 실행 시간 의존성
Movie 클래스는 DiscountPolicy 클래스와 연결되어 있다.
구현체와 연결되어있지 않아도 되었던 이유는, Movie의 인스턴스를 생성할 때 인자로 AmountDiscountPolicy 인스턴스를 전달하기 때문이다. (물론 PercentDiscountPolicy를 전달해도 된다.)
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(1000),
new AmountDiscountPolicy(Money.wons(800)...));
Movie의 인스턴스는 AmountDiscountPolicy에 의존하게 될 것이다.
코드의 의존성과 실행 시점의 의존성이 서로 다를 수 있다는 것이다. = 클래스 사이의 의존성과 객체 사이의 의존성은 동일하지 않을 수 있다.
코드의 의존성과 실행 시점의 의존성이 다를수록 코드를 이해하기 어려워진다
훌륭한 객체지향 설계자가 되기 위해선 항상 유연성과 가독성 사이에서 고민해야 한다
🔺 p.59 핵심 코드 = 유연성, 나머지 코드 = 가독성
차이에 의한 프로그래밍
클래스를 추가하고 싶은데 기존 클래스와 매우 흡사하다고 가정해보자. 그때 사용할 수 있는 방법이 상속이다. 부모 클래스와 다른 부분만을 추가해서 새로운 클래스를 쉽고 빠르게 만드는 방법을 차이에 의한 프로그래밍(programming by difference)라고 부른다.
상속(Inheritance)
목적: 부모 클래스가 제공하는 인터페이스(메시지 수신 능력)를 자식이 물려받음
외부는 자식 객체를 부모 타입처럼 다룰 수 있음 (업캐스팅, Upcasting)
구현 상속(Subclassing): 부모 구현을 재사용
인터페이스 상속(Subtyping): 메시지를 재사용, 외부에서 동일한 타입처럼 다루기 위해 사용
다형성(Polymorphism)
동일한 메시지를 보내도 실행되는 메서드는 메시지를 받은 객체 클래스에 따라 달라짐
실행 시점에 메서드 결정 → 지연 바인딩 / 동적 바인딩
컴파일 시점에 결정되면 초기 바인딩 / 정적 바인딩
05 추상화와 유연성
추상화의 힘
- 추상화의 계층만 따로 떼어 놓고 살펴보면 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미
- 추상화를 사용하면 설계가 좀 더 유연해진다.
유연한 설계
- 추상화를 중심으로 코드의 구조를 설계하면 유연하고 확장 가능한 설계를 만들 수 있다.
추상 클래스와 인터페이스 트레이드 오프
- DiscountPolicy에서 할인 조건이 없을 경우에는 getDiscountAmount() 메서드를 호출하지 않는데
- 할인 조건이 없는 NoneDiscountPolicy 클래스의 getDiscountAmount() 메서드는 쓰지 않는 메서드가 된다.
- 이것은 부모 클래스인 DiscountPolicy와 NoneDiscountPolicy를 개념적으로 결합시킨다.
- 기존 추상 클래스였던 DiscountPolicy를 인터페이스로 바꾸고
public interface DiscountPolicy {
Money calculateDiscountAmount(Screening screening);
}
원래 DiscountPolicy를 DefaultDiscountPolicy로 변경하고 인터페이스를 구현하도록 수정
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
// .... 생략
}
DiscountPolicy 인터페이스를 NoneDiscountPolicy를 구현하도록 하여 개념적인 혼란과 결합을 제거할 수 있다.
public class NoneDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
변경된 설계에 대한 두 가지 생각 - 이상적으로는 인터페이스를 사용하도록 변경한 설계가 더 좋다. - 현실적으로는 NoneDiscountPolicy만을 위해 인터페이스를 추가하는 것이 과하다. 구현과 관련된 모든 것들이 트레이드오프의 대상이며, 작성하는 모든 코드에는 합당한 이유가 있어야 한다.
코드 재사용
객체지향 설계에서는 코드 재사용을 위해서는 상속보다는 합성(composition)이 더 좋은 방법
합성은 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 재사용하는 방법을 말한다.
상속
코드를 재사용하기 위해 널리 사용해온 상속은
부모 클래스의 구현이 자식 클래스에게 노출되기 때문에 캡슐화를 위반한다.
상속은 부모 클래스와 자식 클래스 사이의 관계를 컴파일 시점에 결정하기 때문에 실행 시점에 객체의 종류를 변경할 수 없어 설계를 유연하지 못하게 만든다.
합성
인터페이스에 정의된 메시지를 통해서만 코드를 재사용하는 방법을 합성이라고 부른다.
합성은 상속이 가지는 두 가지 문제점(캡슐화 위반, 유연하지 못한 설계)을 모두 해결한다.
- 인터페이스에 정의된 메시지를 통해서만 사용이 가능하기 대문에 구현을 효과적으로 캡슐화
- 의존하는 인스턴스를 교체하는 것이 쉬어 설계를 유연하게 만든다.
'IT서적 > 오브젝트' 카테고리의 다른 글
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]6장: 메시지와 인터페이스 (0) | 2025.11.14 |
|---|---|
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]5장: 책임 할당하기 (0) | 2025.11.14 |
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]4장: 설계 품질과 트레이드오프 (0) | 2025.11.14 |
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]3장: 역할, 책임, 협력 (0) | 2025.11.14 |
| [오브젝트: 코드로 이해하는 객체지향 설계/조영호]1장: 객체, 설계 (1) | 2025.11.14 |