https://wikibook.co.kr/object/
2장에서는 객체지향 프로그래밍의 관점에서 설계를 개선해본다.
영화 예매 시스템
사용자는 영화 예매 시스템을 이용해 영화를 예매한다.
보통 App의 usecase는 화면을 기반으로 나오기에 화면을 와이어프레임 해놓고 기능을 설계하는 경우가 많다.
사람들은 실제로 영화(Movie)를 예매하는게 아니라 상영(Screening)을 예매한다.
예매자는 할인조건(discountCondition)과 할인 정책(discountPolicy)을 만족하면 요금을 할인받을 수 있다.
할인 조건은 가격의 할인 여부를 결정한다.
- 순서 조건은 상영 순번을 이용해 할인 여부를 결정한다. (10이면 10번째 할인)
- 기간 조건은 요일, 시작시간, 종료시간 세 부분으로 구성되어 "영화 시작 시간"이 기간에 포함되면 요금을 할인한다.
- 주 단위로 모든 영화에 적용된다.
할인 정책은 할인 요금을 결정한다
- 금액 할인 정책 : 일정 금액을 할인해준다.
- 비율 할인 정책 : 금액의 일정 비율을 할인해준다.
할인 정책은 0,1개만 가능하다.
할인 조건은 여러개가 가능하다.
할인 조건이 만족되면 정책을 적용한다.
만족되지 않으면 할인은 없다.
할인정책이 없으면 할인은 없다.
객체지향 프로그래밍을 향해
협력 ,객체, 클래스
객체는 클래스가 아니다.
객체지향은 말 그대로 객체를 지향하는 것이다.
책임(메세지에 대해 자신만의 메소드로 자율적으로 응답.) 관점에서 접근해야 한다.
- 어떤 객체들이 필요한지 고민하라. 클래스는 객체의 추상화에 불과하다. 행동과 상태에 집중하자(행동우선).
- 객체들의 모양과 윤곽이 잡히면 타입으로 분류하고, 타입 기반으로 클래스를 구현하라.
- 객체는 협력하는 공동체의 일원이다. 즉 협력자다.
도메인의 구조를 따르는 프로그램 구조
도메인 : 문제를 해결하기 위해 사용자가 프로그램을 사용하는 분야
- 객체지향 패러다임이 강력한 이유 : 요구사항 분석 초기 단계부터 프로그램 구현 마지막 단계까지 객체라는 동일한 추상화 기법 적용
- 요구사항과 프로그램을 객체라는 동일한 관점에서 바라볼 수 있다
- 도메인 구성 개념들이 객체와 클래스로 매끄럽게 연결된다
자율적인 객체
- 객체는 상태와 행동을 함께 가지는 복합적인 존재이다.
- 객체는 스스로 판단하고 행동하는 자율적인 존재이다.
- 데이터와 기능을 객체 내부로 함께 묶는 것을 캡슐화라 한다.
- 필드의 타입이 인터페이스 파라미터, 다른 클래스의 지역변수로 노출되지 않게 하는 것
- 접근제어를 위한 접근 수정자
- 캡슐화와 접근 제어는 객체를 두 부분으로 나눈다
- 퍼블릭 인터페이스 - 인터페이스와 구현의 분리 원칙
- 구현(private, protected 메서드, 모든 속성)
- 속성 public 절대 금디
구현 은닉(캡슐화)을 통한 프로그래머의 자유
- 클래스 작성자(class creator)
- 내부 구현을 마음껏 변경할 수 있음
- 클라이언트 프로그래머(client programmer)
- 사용법만 알면 됨.
- stable한 인터페이스 덕택에 내부 구현이 바뀌어도 수정할 필요가 없음
협력하는 객체들의 공동체
- 객체지향 프로그램을 작성할 때
- 협력의 관점에서 어떠한 객체가 필요한지를 결정하고
- 객체들의 공통 데이터와 행위를 구현하기 위해 클래스를 작성한다.
메서드와 메서드를 구분하기.
- 객체의 내부 상태는 외부에서 접근하지 못하도록 감춘다.
- 공개 외부 인터페이스 통해 내부 상태에 접근할 수 있도록 허용한다. (request & response)
- 다른 객체와 상호작용하는 방법은 메세지를 전송하는 것이다.
- 퍼블릭 인터페이스 호출은, 메서드를 호출한다는 표현보단 다른 객체에게 메세지를 전송한다고 하자.
- 다른 객체에 요청이 도착하면 메세지를 수신했다고 이야기한다.
- 메세지를 수신한 객체는 자신만의 방법(method)로 자율적으로 메세지를 처리한다.
- 메서드와 메서드를 구분함에서부터 다형성의 개념이 출발한다.
할인 요금 구하기
탬플릿 메소드 패턴 : 공통 사용 기본적인 알고리즘의 흐름을 구현하고 구체적인 구현을 자식 클래스에 위임함
- 공통 알고리즘이 있으면 추상 클래스 아니면 인터페이스 사용.
- 추상화에 기반한 상속과 다형성 이용
- getDiscountAmount의 구현을 위임함.
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 each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening Screening);
}
- 오버로딩 : 한 클래스 내에서 이름 같고 다른 파라미터 메서드 정의. 부모 메소드 안가림.
- 오버라이딩 : 부모 메소드 자식 메소드에서 재정의. 부모 메서드를 가려 외부에서 안보임.
상속과 다형성
Movie Class 어디에도 할인 정책의 구체적인 클래스와 조건문이 보이지 않는다.
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));
}
}
할인 정책에게 단지 할인금액을 계산하라고 시킨다. 그리고 Money 객체에게 요금을 계산하라고 시킨다.
할인정책 결정 조건문이 없는데 어떻게? 상속과 다형성을 알아보자
컴파일 시간 의존성과 실행 시간 의존성
Movie는 추상 클래스인 DiscountPolicy와 연결되어 있지만, 필요한건 구체 클래스의 인스턴스다.
Movie 인스턴스를 생성할 때 인자로 AmountDiscountPolicy의 인스턴스를 전달하면 된다.
코드상에서는 Movie는 오직 DiscountPolicy만 알고 있다. (의존한다)
실행 시점은 인스턴스에 의존한다.
즉, 코드의 의존성과 실행 시점의 의존성은 다를 수 있다
이는 코드를 이해하기 어렵게 할 수 있다.
객체를 생성하고 연결하는 부분을 살펴봐야 한다.
하지만 코드는 더욱 유연해지고 확장 가능해진다.
의존성의 양면성은 설계가 트레이드 오프의 산물이라는 것을 보여준다.
재사용성, 확장 가능성과 디버깅 용이성의 트레이드 오프다.
차이에 의한 프로그래밍
상속을 이용하면 기존 클래스의 모든 필드와 메서드의 재사용이 가능하다.
또한 자손 클래스에서 자신만의 메소드를 추가할 수 있다.
이를 차이에 의한 프로그래밍이라 한다.
상속과 인터페이스
상속의 목적은 공통 알고리즘, 공통 인터페이스 사용이지, 메서드나 인스턴스 변수 재사용이 아니다
인터페이스는 객체가 이해할 수 있는 메세지의 목록을 정의한다.
상속을 통해 자식은 부모가 수신할 수 있는 모든 메세지를 수신할 수 있다. 즉 책임을 이어받는다.
다형성
다형성 : 실행 시점에 동일한 메세지를 전송해도 어떤 메세지가 실행될 지는 메세지를 수신하는 객체의 클래스에 의해 달라진다.
메세지와 메서드는 다르다 (public vs private, protected)
컴파일 시간 의존성(코드-추상클래스/인터페이스)와 실행시간 의존성(인스턴스-구체클래스)이 다를수 있다는 것을 기반으로 한다.
메시지와 메서드를 실행 시점에 바인딩하므로 lazy, dynamic binding이라 한다.
컴파일 시점에 실행될 함수와 프로시저를 결정하는것을 early, static binding이라 한다
동일한 인터페이스를 공유하는 클래스들을 하나의 타입 계층으로 묶는다.
상속은 인터페이스 재사용(서브타이핑)을 목적으로 사용해야 한다. 구현을 재사용할 목적으로 상속하면 변경에 취약한 코드를 낳게 될 확률이 높다.
인터페이스와 다형성
구현을 공유할 필요가 없고, 순수하게 인터페이스만 공유하고 싶을 때
인터페이스 : 구현에 대한 고려 없이 다형적인 협력에 참여하는 클래스들이 공유 가능한 외부 인터페이스
(C++나 파이썬은 추상 기반 클래스를 통해 자바 인터페이스 개념 구현)
클라이언트 입장에서 추상 클래스와 구체적인 클래스는 아무 차이 없다. 동일한 메세지를 이해할 수 있기 때문이다.
실제 사용하는 클래스는 구체클래스이다.
이 경우도 업캐스팅이 적용되며, 협력은 다형적이다.
추상화와 유연성
추상클래스와 인터페이스는 구체클래스보다 추상적이다.
하위 타입의 모든 클래스가 수신할수 있는 메세지를 정의하기 때문이다.
따라서 세부적인 내용을 무시한 채 상위 정책을 쉽고 간단하게 표현할 수 있다.
세부사항에 억눌리지 않고 상위 개념만으로 도메인의 중요한 개념을 설명할 수 있다.
- 추상화의 첫번째 장점 : 요구사항의 정책을 높은 수준에서 서술할 수 있다.
- 아래 그림은 "영화 예매 요금은 최대 하나의 할인 정책과 다수의 할인 조건을 이용해 계산할 수 있다"를 나타냄
- 협력 다이어그램을 보면 좀 더 명확해짐
- 추상화의 두번째 장점 : 설계가 유연해짐
- 기존 구조를 수정하지 않고 하위타입을 통해 기능 추가, 변경.
- 하위타입을 추가하거나
- 해당 하위타입을 수정하거나
- 다른곳은 안건드려도 된다.
- 기존 구조를 수정하지 않고 하위타입을 통해 기능 추가, 변경.
public class NoneDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
Movie starWars = new Movie("스타워즈",...,new NoneDiscountPolicy());
기존 클래스 수정 없이 하위타입 하나 선언만으로 할인정책이 없는 영화를 만들 수 있음
추상화가 유연한 설계를 가능하게 하는 이유는 설계(필드)가 구현에 결합되지 않기 때문이다.
Movie는 특정한 할인 정책에 묶이지 않는다. 필드 클래스의 하위 타입이라면 새로운 클래스는 언제나 Movie와 협업이 가능하다.
이를 컨텍스트 독립성이라 한다 (8장)
추상 클래스와 인터페이스 트레이드오프
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 each : conditions) {
if (each.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening Screening);
}
위를 보면, NoneDiscountPolicy는 할인조건이 없어 calculateDiscountAmount 메세지가 호출되지 않음.
이는 DiscountPolicy와 NoneDiscountPolicy를 개념적으로 결합시킴.
조건이 없으면 0원을 반환할 것이라는 사실을 가정하기 때문.
이는 아래와 같이 구조만 변경해준다. (원래 DiscountPolicy => DefaultDiscountPolicy)
현실적으로 위 설계는 과하다는 생각이 들 수 있다. 즉 응집도와 결합도 / 코드 가독성의 트레이드 오프다.
코드 재사용
다시 한번 말하지만, 상속은 코드 재사용을 위한 방법이 아니다. 부모 객체 인터페이스의 재사용을 위한 방법이다.
Money는 합성을 통해 DiscountPolicy를 재사용한다. 아래와 같이 하면 합성을 이용한 방법과 동일한데, 왜 상속보다 합성인가?
상속
상속의 단점 :
- 캡슐화를 위반. 부모 클래스의 내부 구조를 잘 알고 있어야 한다.
- 부모 클래스의 calculateMovieFee 안에서 getDiscountAmount를 호출한다는 사실을 알아야 함.
- 런타임시 변경이 불가능하여, 설계를 유연하지 못하게 만듬
- 캡슐화의 약화는 강결합을 만든다.
- 부모가 변경되면 자식이 변경되는 결과를 만든다.
- 부모와 자식 클래스 사이의 관계를 컴파일 시점에 결정하여, 런타임 시 바꿀 수 없다.
- 실행 시점에 금액 할인 정책인 영화를 비율 할인 정책으로 바꿀 수 없다.
- 상속은 코드 레벨에서 정해지기 때문이다.
public void changeDiscountPolicy(DiscountPolicy discountPolicy){
this.discountPolicy=discountPolicy;
}
위와 같은 메소드를 사용이 불가능하다.
즉 상속보다 인스턴스 변수로 관계를 연결한 원래의 설계가 더 유연하다.
합성
합성의 장점은 상속은 컴파일 시점에 하나의 단위로 강하게 결합하는데 (부모, 자식 코드)
또한 합성은 클래스 간 인터페이스를 통해 약하게 결합된다.
즉, 내부 구현에 대해서는 전혀 알지 못한다.
인터페이스에 정의된 메세지를 통해서만 코드를 재사용하는 방법
구현의 효과적 캡슐화
메세지를 통한 느슨한 결합 => 의존 인스턴스 교체가 쉬움
다형성을 위해 인터페이스를 재사용 하는 경우는 상속과 합성을 조합해 사용할 수밖에 없음
객체지향이란 객체를 지향하는 것이다. 객체지향 패러다임의 중심에는 객체가 위치한다.
객체지향에서 가장 중요한 것은 앱의 기능을 구현하는 객체들의 상호작용이다.
'BackEnd' 카테고리의 다른 글
오브젝트 5장. 책임 할당하기 (0) | 2021.12.17 |
---|---|
오브젝트 4장 설계 품질과 트레이드오프 정리 (0) | 2021.12.17 |
오브젝트 3장 역할, 책임, 협력 정리 (0) | 2021.12.17 |
오브젝트 1장 객체, 설계 내용 정리 (0) | 2021.12.15 |
[컨버런스 리뷰] TDD는 오브젝트 디자인(객체 설계)에 도움이 되는가? (0) | 2021.12.14 |