객체와 객체 지향
진정한 객체지향 패러다임으로의 전환은 클래스가 아닌 객체에 초점을 맞출 때에만 얻을 수 있다.
첫째, 클래스 이전에 어떤 객체들이 필요한지 고민하라. 클래스의 윤곽을 잡기 위해선 어떤 객체들이 어떤 상태와 행동을 가지는지 먼저 결정해야 한다.
둘째, 객체를 독립적인 존재가 아니라 기능을 구현하기 위해 협력하는 공동체의 일원으로 봐야 한다. …이것은 설계를 유연하고 확장 가능하게 만든다.
프로그램이 사용되는 분야를 도메인이라고 부른다.
객체지향 패러다임이 강력한 이유는 요구사항 분석 단계부터 프로그램 구현 단계까지 객체라는 동일한 추상화 기법을 사용할 수 있기 때문이다.
인터페이스와 구현의 분리
클래스의 내부와 외부를 구분해야 하는 이유는 무엇일까?
경계의 명확성이 객체의 자율성을 보장하기 때문이다.
더 중요한 이유는, 프로그래머에게 구현의 자유를 제공하기 때문이다.
인터페이스와 구현의 분리.
캡슐화와 접근 제어는 객체를 두 부분으로 나눈다. 외부(public)와 내부(private, protected).
객체가 수신된 메세지를 처리하기 위한 자신만의 방법을 메서드라고 부른다.
메시지와 메서드를 구분하는건 매우 중요하다. 메시지와 메서드의 구분에서부터 다형성의 개념이 출발한다.
상속과 다형성
컴파일 시간 의존성과 실행 시간 의존성
Movie 객체 인스턴스는 실행시에 할인 정책이 결정되어 있어야 한다. 즉, AmountDiscountPolicy 혹은 PercentDiscountPolicy 에 의존해야 한다.
그러나 코드 수준에서(컴파일 시간에) 두 클래스 중 어떤 것에도 의존하지 않는다. 오직 추상 클래스인 DiscountPolicy에만 의존하고 있다.
실행시에, Movie 생성자를 통해 인스턴스를 전달하면 된다.
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = discountPolicy;
}
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); // TEMPLATE METHOD 패턴
}
}
return Money.ZERO;
}
// 추상 메서드
abstract protected Money getDiscountAmount(Screening Screening);
}
public class Movie {
// ...
private DiscountPolicy discountPolicy;
// ...
public Money calculateMovieFee(Screening screening) {
// DiscountPolicy 인터페이스 의존
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
Movie는 동일한 메세지를 전송하지만 실제로 어떤 메서드가 실행될 것인지는 메시지를 수신하는 객체의 클래스가 무엇이냐에 따라 달라진다.
이를 다형성이라고 부른다.
public class AmountDiscountPolicy extends DiscountPolicy {
private Money discountAmount;
// ...
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
}
public class PercentDiscountPolicy extends DiscountPolicy {
private double percent;
// ...
@Override
protected Money getDiscountAmount(Screening screening) {
return screening.getMovieFee().times(percent);
}
}
상속이 가치 있는 이유는 부모 클래스가 제공하는 모든 인터페이스를 자식 클래스가 물려받을 수 있기 때문이다.
…결과적으로 자식 클래스는 부모 클래스가 수신할 수 있는 모든 메세지를 수신할 수 있기 때문에 외부 객체(Movie)는 자식 클래스를 부모 클래스(DiscountPolicy)와 동일한 타입으로 간주할 수 있다.
다형성이란 동일한 메시지를 수신했을 때 객체의 타입에 따라 다르게 응답할 수 있는 능력을 의미한다.
AmountDiscountPolicy와 PercentDiscountPolicy가 다형적인 협력에 참여할 수 있는 이유는 이들이 DiscountPolicy로부터 동일한 인터페이스를 물려받았기 때문이고, 이 두 클래스의 인터페이스를 통일하기 위해 사용한 방법이 바로 상속인 것이다.
다형성을 구현하는 방법은 매우 다양하지만 메시지에 응답하기 위해 실행될 메서드를 컴파일 시점이 아닌 실행 시점에 결정한다는 공통점이 있다.
다시 말해 메시지와 메서드를 실행 시점에 바인딩한다. 이를 동적 바인딩이라고 부른다.
한 가지 간과해서는 안 되는 사실은 코드의 의존성과 실행 시점의 의존성이 다르면 다를수록 코드를 이해하기 어려워진다는 것이다. 코드를 이해하기 위해서는 코드뿐만 아니라 객체를 생성하고 연결하는 부분을 찾아야 하기 때문이다. (컴파일 시점 의존성과 실행 시점의 의존성이 다른 부분)
설계가 유연해질수록 코드를 이해하고 디버깅하기는 점점 더 어려워진다는 사실을 기억하라. 반면 유연성을 억제하면 코드를 이해하고 디버깅하기는 쉬워지지만 재사용성과 확장 가능성은 낮아진다는 사실도 기억하라. 훌륭한 객체지향 설계자로 성장하기 위해서는 항상 유연성과 가독성 사이에서 고민해야 한다.
추상화의 힘
추상화의 장점은 추상화 계층만 따로 떼어놓고 봤을 때 요구사항을 높은 수준에서 서술할 수 있다는 것이다. 세부사항에 억눌리지 않고 상위 개념만으로도 도메인의 중요한 개념을 설명할 수 있게 된다.
추상화를 이용해 상위 정책을 기술한다는 것은 기본적인 애플리케이션의 협력 흐름을 기술한다는 것을 의미한다.
Movie는 특정한 할인 정책에 묶이지 않는다.
추상화가 유연한 설계를 가능하게 하는 이유는 설계가 구체적인 상황에 결합되는 것을 방지하기 때문이다.
추상화를 이용하면 기존 코드를 수정하지 않고도 기능을 확장할 수 있다.
❗OCP 원칙
개방 폐쇄 원칙은 "확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다."를 의미합니다. 조금 더 쉽게 풀어 쓰자면, "기능을 변경하거나 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않는다."를 뜻합니다.
각 DB들과 Driver들은 변경될 수 있습니다. 확장에는 열려있는 것입니다. 반면에 DB가 변경되거나 특정 DB에 변화가 발생해도, 그것을 사용하는 Java 애플리케이션에는 영향을 미치지 않습니다. JDBC Driver Manager가 제공하는 인터페이스만 그대로 사용하면 되기 때문입니다.
합성과 상속
상속은 두 가지 관점에서 설계에 안 좋은 영향을 미친다.
- 캡슐화 위반
- 상속을 이용하기 위해선 자식 클래스가 부모 클래스의 내부 구조를 잘 알고 있어야 한다. 부모 클래스의 구현이 자식에게 노출되기 때문에 캡슐화가 약화된다.
- 설계를 유연하지 않게 만듬
- 부모 클래스와 자식 클래스가 강하게 결합되어 있다면, 결국 둘중 하나를 변경할 때 서로 변경시킬 확률을 높인다. 상속을 과도하게 사용한 코드는 변경하기도 어려워진다.
합성은 상속이 가지는 두 가지 문제점을 모두 해결한다.
- 인터페이스에 정의된 메세지를 통해서만 재사용이 가능하기 때문에 구현을 효과적으로 캡슐화
- 인터페이스를 교체하는 것이 비교적 쉽기 때문에 설계가 유연
상속은 클래스를 통해 강하게 결합되는 데 비해 합성은 메시지를 통해 느슨하게 결합된다.
배운 점
- 클래스보다 객체를 먼저 생각하자. 추상적으로 생각하면 핵심적인 애플리케이션의 협력 흐름에 집중할 수 있다.
- 인터페이스와 구현의 분리. 캡슐화와 접근 제어자를 적절하게 사용하면 객체의 자율성과 재사용성을 모두 높일 수 있다.
- 상속(인터페이스와 추상 클래스)은 컴파일 시점 의존성과 실행 시점 의존성을 다르게 만든다. 이는 다형성을 이룰 수 있게 해주고 설계를 유연하게 해준다. 그러나 코드 파악과 디버깅을 어렵게 해준다는 단점이 있다. 또한 부모 자식 클래스간의 결합도가 높으면 코드 변경이 어려워진다. 합성으로 이 문제를 해결할 수 있다.
- 상속은 다형성이라는 목적을 이루는 수단이다.
'CS > OOP' 카테고리의 다른 글
[오브젝트] 6장 - 메시지와 인터페이스 (0) | 2024.01.22 |
---|---|
[오브젝트] 5장 : 책임 할당과 책임 중심 설계 (0) | 2024.01.18 |
[오브젝트] 4장 - 데이터 중심 설계의 문제점과 캡슐화 (0) | 2024.01.15 |
[오브젝트] 3장 - 역할, 책임, 협력 (0) | 2024.01.11 |
[오브젝트 스터디] 1장 - 응집도는 높게 결합도는 낮게 (0) | 2024.01.01 |