Getter Setter의 캡슐화 위반
getFee 메서드와 setFee 메서드는 Movie 내부에 Money 타입의 fee라는 인스턴스 변수가 존재하는 사실을 퍼블릭 인터페이스에 노골적으로 드러낸다.
- 추측에 의한 설계 전략 - 엘런 홀 업
단일 책임 원칙 (Single Responsiblility Principle)
로버트 마틴은 모듈의 응집도가 변경과 연관이 있다는 사실을 강조하기 위해 단일 책임 원칙이라는 설계 원칙을 제세했다.
단일 책임 원칙을 한마디로 요약하면 클래스는 단 한 가지의 변경 이유만 가져야 한다는 것이다. (클래스의 응집도를 높일 수 있는 설계 원칙)
책임이라는 말은 "변경의 이유"이다. 단일 책임 원칙에서의 책임은 역할, 책임, 협력에서 이야기하는 책임과는 다르며 변경과 관련된 더 큰 개념의 의미
public class Rectangle {
private int right;
private int left;
public int getRight() {
return right;
}
public void setRight(int right) {
this.right = right;
}
public int getLeft() {
return left;
}
public void setLeft(int left) {
this.left = left;
}
}
public class AnyClass {
public int calculateArea(Rectangle rectangle) {
return rectangle.getLeft()*rectangle.getRight();
}
}
이러한 식으로 설계가 되어있다면 만약 단순히 명칭 하나가 바뀌더라도 AnyClass를 수정해야 한다.
또한 이러한 방식의 코드가 어디든 산재할 것이므로 코드의 중복이 발생하기도 한다.
차라리 Rectangle 클래스 내부에 놓으면 이러한 문제가 발생하지 않는다.
public class Rectangle {
private int right;
private int left;
public Rectangle(int right, int left) {
this.right = right;
this.left = left;
}
public void Area(int addHeight) {
this.left += addHeight;
this.right += addHeight;
}
@Override
public String toString() {
return "Rectangle{" +
"right=" + right +
", left=" + left +
'}';
}
}
GRASP 패턴
INFORMATION EXPERT 패턴
정보는 데이터와 다르다. 정보를 알고 있다고 해서 그 정보를 저장할 필요는 없다.
상영이라는 도메인 개념이 영화의 상영 시간, 상영 순번과 같은 다양한 정보를 알고 있다.
가격을 계산하는데 필요한 정보를 알고 있는 전문가는 누구인가? 영화 Movie가 예매하라는 메시지를 접했을 때 일어나는 작업흐름에 대해서 생각 - 너무 세세하게 고민할 필요 X
Movie는 할인 가능한지 여부를 판단할 수 없다.
할인 가능한지 여부를 판단할 수 있는 객체는 누구인가? 할인조건 할인 조건은 자체적으로 할인 여부를 판단하는 데 필요한 모든 정보를 알고 있기 때문에 외부의 도움 없이도 스스로 판단 가능! |
LOW COUPLING(낮은 결합도) HIGH COHESION(높은 응집도) 패턴몇 가지 설계 중 한 가지를 선택해야 하는 경우가 빈번
LOW COUPLING 관점다시 도메인을 보면 영화가 할인 조건과 이미 결합되어 있기 때문에 영화를 할인 조건과 협력하게 되면 전체적으로 결합도를 추가하지 않고 협력을 완성할 수 있다.
HIGH COHESION 관점영화의 주된 책임은 요금을 계산하는 것이다. 영화 요금을 계산하기 위해서 할인 조건과 협력하는 것은 응집도에 아무런 문제가 없다.
|
CREATOR 패턴영화 예매 협력의 최종 결과물은 Reservation 인스턴스를 생성하는 것
|
시그니처
메시지를 전송할 때 수신자의 의도가 아닌 송신자의 의도를 표현해라!
public class Screening {
private Movie movie;
private int sequence;
private LocalDateTime whenScreened;
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 LocalDateTime getWhenScreened() {
return whenScreened;
}
public int getSequence() {
return sequence;
}
}
Screening은 Movie의 내부 구현에 대한 어떤 지식도 없이 전송할 메시지를 결정한다.
하나 이상의 변경 이유를 가지는 클래스 개선 (1)해당 클래스는 응집도가 낮다.
왜냐면 연관성 없는 기능이나 데이터가 하나의 클래스 안에 뭉쳐 저 있기 때문에 서로 다른 시점에 변경이 이루어진다. 코드를 통해 변경의 이유를 파악하는 방법1. 인스턴스 변수가 초기화되는 시점을 파악! 빨간색과 파란색으로 인스턴스 변수의 초기화 시점이 구별됨을 알 수 있다.
2. 메서드들이 인스턴스 변수를 사용하는 방식 파악! isSatisfiedByPeriod 메서드는 dayOfWeek, startTime, endTime을 사용하고 isSatisfiedBySequence 메서드는 sequence를 사용한다.
각각 사용하는 인스턴스 변수가 다르다. |
하나 이상의 변경 이유를 가지는 클래스 개선(2) - 다형성하나 이상의 이유를 가지는 메서드를 클래스 단위로 쪼갰을 때 부작용이 있는가?
있다! 왜냐면 이전에 Movie는 DiscountCoundition 만 협력관계가 있었지만 쪼갬으로써 PeriodCondition과 SequenceCondition 과의 추가적인 협력관계가 형성되었다.
개선 1. 목록을 따로 유지이러한 방식으로 구현하면 부작용은 만약 할인조건이 추가된다면 List 도 추가하고 해당 조건을 판단하는 메서드도 추가하는 작업이 나타난다. 또한 해당 메서드를 호출하도록 isDiscountable 메서드를 수정해야 한다.
개선 2. 다형성구현을 공유해야 할 필요가 있다면 추상 클래스 구현을 공유해야 할 필요가 없고 책임만을 정의하고 싶다면 인터페이스
위와 같이 암시적인 타입에 따라서 행동을 분기해야 한다면 암시적인 타입을 명시적인 클래스로 정의하고 행동을 나눔으로써 응집도를 해결할 수 있다. |
결합도 낮추기 : 디미터 법칙디미터 법칙이란 객체의 내부 구조에 강하게 결합되지 않도록 협력 경로를 제한하자 낯선 자에게 말을 걸지 말고 오직 인접한 이웃에게만 말을 걸자! 아래 다섯 가지 조건에 해당하는 인스턴스에게만 메시지를 전송하자
아래의 코드는 너무 강 결합이다. ReservationAgenct, Screening, Movie, DiscountCondition 중 한 개라도 변경된다면 다 같이 바뀌기 때문에 강 결합이라는 것! 디미터의 법칙에서 언급하였듯이 메서드의 인자로 전달된 Screening 인스턴스에게만 메시지를 전달하는 것으로 코드를 변경하였다. |
의존성을 관리하기1. 의존성 구분의존성은 객체 간의 협력을 위해 어쩔 수 없이 사용되어야 한다.
PeriodCondition 클래스의 의존성 2. 의존성 전이Screening이 가지고 있는 의존성 : Movie, LocalDateTime, Customer PeriodCondition이 가지고 있는 의존성 : DayOfWeek, LocalTime, Screening
Screening이 가지는 의존성이 PeriodCondition에게 전파된다는 말 (반대도 성립) 3. 런타임 의존성 & 컴파일 타임 의존성Movie 클래스의 코드 어디에도 AmountDiscountPolicy PercentDiscountPolicy를 찾아볼 수 없다. Movie클래스는 단지 DiscountPolicy와만 협력한다. 하지만 런타임 시점에서는 Movie는 AmountDiscountPolicy PercentDiscountPolicy와 협력하게 된다.
코드 작성 시점의 Movie 클래스는 할인 정책을 구현한 두 클래스에 대해서 알지 못하지만 실행 시점의 Movie 클래스는 두 클래스의 인스턴스와 협력할 수 있게 된다. (컴파일 시점의 의존성과 런타임 시점의 의존성이 다르다.)
이처럼 클래스의 인스턴스가 다양한 클래스의 인스턴스와 협력하기 위해서는 협력할 인스턴스의 구체적인 클래스를 알아서는 안된다. |
의존성을 해결하는 세 가지 방법
1번 방식생성자에 아래와 같이 추상 클래스 타입을 파라미터로 선언한다. 이러한 방식으로 선언하면 인스턴스 생성 시에 원하는 인스턴스를 타입으로 받을 수 있다. 2번 방식인스턴스 생성 시에 파라미터로 원하는 타입을 받지 않고 인스턴스 생성 이후에 setter 메서드를 통해 원하는 타입을 받는다. 해당 방식은 setter메서드로 원하는 인스턴스를 주입받기 이전에 해당 인스턴스 변수를 선언하면 NPE가 발생할 수 도 있다는 단점이 존재한다. 제일 좋은 건 1번 방식과 2번 방식을 병행해서 사용하는 것이다. 3번 방식메서드 인자를 사용하는 방식은 메서드를 실행될 때마다 의존 관계가 매번 달라져야 하는 경우에 유용하다.
|
클래스 내에서 NEW 연산자는 해롭다클래스 내에서 new 연산자를 사용해서 인스턴스를 선언하는 것은 결합도를 높이기 때문에 올바른 설계라고 할 수 없다.
아래 코드를 보면 Movie 클래스 생성자에 인스턴스를 직접 선언하도록 되어있다.
이에 대한 파급 효과는 아래와 같다. Movie 생성 시에 new 연산자를 사용하여 여러 가지 객체를 생성하기 때문에 결합도가 높아지는 것을 볼 수 있다. 그러면 객체의 생성을 누구에게 맡겨야 하는가? 클라이언트에게 맡기자! 인스턴스를 직접 생성하지 말고 클라이언트에게 맡겨버리면 결합도가 낮아진다. 하지만 협력하는 기본 객체를 설정하고 싶을 땐 클래스 내에 new를 선언해도 된다. (Feat. 오버 로딩)예를 들어서 Movie가 대부분의 경우에는 AmountDiscountPolicy 인스턴스와 협력하고 특별한 경우에만 PercentDiscountPolicy 인스턴스와 협력할 때를 예로 들 수 있다.
이때는 오버 로딩을 사용하면 된다.
|
개방 폐쇄 원칙 (OCP)개방 폐쇄 원칙이란 를 의미한다.
기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있다니 약간 이상하지 않은가?
컴파일 타임 의존성에서 Movie와 각각의 할인 정책이 직접적으로 의존하지 않기 때문에 할인 정책을 추가하더라도 기존의 코드의 수정 없이 확장이 가능한 것이다. 결국은 추상화가 핵심 |
상속의 문제점 1Vector와 StackVector는 임의의 위치에서 요소를 조회 추가 삭제 기능을 제공한다. stack은 맨 마지막 위치에서만 요소를 추가하거나 제거할 수 있다.
유감스럽게도 Stack은 Vector를 상속받고 있다. 어떤 문제가 일어나는지 코드를 통해서 살펴보자.
stack에서 add를 하면 마지막 요소가 아닌 특정 인덱스의 자리고 들어가게 된다. 그러고 나서 pop을 해버리면 맨 마지막 요소를 바라보기에 "4th" == stack.pop() 은 성립할 수 없다.
안 쓰면 그만이지 라고 생각할 수 도 있겠지만 Stack 개발자 한 사람의 일시적인 편의를 위해 인터페이스를 사용해야 하는 무수한 사람들이 가슴을 졸여야 하는 상황을 초래한 것은 어떠한 경우에도 정당화될 수 없다. |
상속의 문제점 2상속에서 부모 클래스와 자식 클래스 사이의 의존성은 컴파일 타임에 해결 (has - a) 합성에서 부모 클래스와 자식 클래스 사이의 의존성은 런타임 시점에 해결 (is -a)
기본정책 + 부가 정책기본정책은 필수적인 정책이고 부가 정책은 사용자가 선택적으로 선택할 수 있는 정책이다.
일단 상속을 함으로써 발생하는 일련의 문제점들을 살펴보자 일반 요금제와 세금 정책의 조합을 상속으로 구현가장 쉬운 방법은 일반 요금제를 세금 정책이 상속받는 것이다. 위의 코드에서 도드라져 보이는 것은 super이다. 즉 자식 클래스가 부모 클래스의 코드를 직접 참조한다는 것이다. 이로 인해서 부모 자식 간의 결합도가 높아져 버리는 문제점이 생긴다.
이전보다는 나아졌지만 굳이 afterCalculated을 구현할 필요가 없는 자식 클래스들을 구현을 해야 한다는 문제점이 발생하였다. (hook method) 이렇게 된다면 문제가 무엇일까? 일단은 afterCalculated라는 중복 코드가 발생하였다는 것이다. 또한 더 중요한 문제는 상속을 이용한 해결 방법은 모든 가능한 조합별로 자식 클래스를 하나씩 추가하는 것이기 때문에 아래와 같은 문제가 발생한다. 꽤 복잡해 보이지 않는가? 더 나아가 기본 정책이 새로 추가된다면? 위에 보이는 바와 같이 기본 정책 하나를 추가하기 위해서 다섯 개의 클래스가 추가된 점을 볼 수 있다. 얼마나 비효율 적인가..? 왜 이런 문제가 발생했을까?상속 관계는 컴파일 타임에 결정되고 고정되기 때문에 실행 도중에는 변경할 수 없다. 따라서 여러 기능을 조합해야 하는 설계의 경우에는 상속을 이용하면 모든 조합 가능한 경우의 수에 대한 클래스를 추가해야 하기 때문이다. (클래스 폭발) |
합성을 적용시켜보자
1. RatePolicy 인터페이스 (기본정책 + 부가 정책) 2. 중복 코드를 담기 위한 BasicRatePolicy 추상 클래스 생성 3. BasicRatePolicy를 구현한 일반 요금제 클래스 생성 (또 하나의 기본정책인 심야 요금제도 비슷한 방식으로 생성) 4. Phone 수정 (인터페이스 -> 클래스)
컴파일 시점에는 Phone이 RatePolicy 인터페이스와 협력하지만 런타임 시점에는 RegularPolicy와 협력한다. 부가 정책 적용
1. 부가 정책에 관한 추상 클래스인 AdditionalPolicy 생성 2. 세금정책 구현 (기본요금 할인 정책도 추가하는 것도 이와 동일) 상속과 합성 최종적으로 비교해보기상속 합성 상속에서 기본정책을 추가할 때에는 5개의 클래스를 추가했던 것을 기억하는가? 이와 다르게 합성을 사용하였을 때는 클래스만 추가하면 된다. |
다형성
|
기억에 남는 문장
- 행동과 상태를 객체라는 하나의 단위로 묶는 이유는 객체 스스로 자신의 상태를 처리할 수 있게 하기 위해서이다.
- 시그니처를 외부에 들어내지 말라, 내부 구현을 외부에 들어내지 말라 - 캡슐화란 변하는 어떤 것이든 감추는 것!
퍼블릭 인터페이스란 객체가 의사소통을 위해 외부에 공개하는 메시지의 집합을 말한다.
오퍼레이션이란 퍼블릭 인터페이스에 포함된 메시지를 말한다. 보통은 추상화된 시그니처를 의미한다.
201페이지 (원칙의 함정) : 가끔은 시키지 말고 물을 때도 있어야 한다. 소프트웨어 설계에 원칙이란 존재하지 않는다. 항상 상황에 맞게 써야 한다.
(257P) 컴파일 타임 vs 런타임
런타임은 단순히 애플리케이션이 실행되는 시점을 의미
컴파일 타임은 실제로 코드가 컴파일되는 시점일 수 도 있고 코드 그 자체의 시점이 될 수 도 있다. 하지만 동적 타입 언어의 경우에는 컴파일이 존재하지 않기 대문에 코드를 작성하는 시점으로 이해하는 것이 좋다.
Dry 원칙 : Don't Repeat Yourself
모든 지식은 시스템 내에서 단일하고, 애매하지 않고, 정말로 믿을 만한 표현 양식을 가져야 한다. - 즉 코드 안에 중복이 존재해서는 안된다.
상속의 진정한 목적은 코드의 재사용이 아닌 서브타입 계층을 구축하는 것!
References
|
'Java > 객체지향' 카테고리의 다른 글
(OOP) extends는 왜 죄악인가? (0) | 2020.04.05 |
---|---|
(OOP) 객체지향에 대해서(3) - 추상화 (0) | 2020.02.21 |
(OOP) 리스코프 치환 원칙 (0) | 2020.02.20 |
(OOP) 객체지향에 대해서(2) - 은유 (0) | 2020.02.20 |
(OOP) 객체지향에 대해서(1) - 객체, 행동, 상태 (0) | 2020.02.19 |