GRASP 패턴 - POLYMORIPHISM
GRASP 패턴 중 POLYMORIPHISM 패턴에 대해 알아보자
Polymoriphism 패턴
Polymoriphism(다형성)이란 한 문장으로 요약하자면
추상화 타입을 통해 객체의 다형성을 보장한다.
라고 할 수 있고, 이것만큼 명확한 정의는 없다고 생각한다. 하지만 다형성을 처음 접하는 입장에서는 뭘 말하는지 1도 모르는게 당연하다. 다형성을 가장 쉽게 이해할 수 있는 방법은 다형성이 필요한 상황을 이해하는것 부터 시작이다.
다형성 패턴이 필요한 상황
예시로 주문을 받아 수수료와 상품 가격을 합한 최종 가격을 계산하는 에플리케이션을 작성해보자. OrderService와 FeeCalculator 두개의 클래스를 작성해보겠다.
OrderService는 주문을 받아 최종 가격을 계산한다. 그리고 FeeCalculator를 의존하고 있다.
class OrderService {
/**
상품의 가격과 수수료를 합한 최종 가격을 반환한다.
*/
getTotalPrice(order: Order) {
const fee = new FeeCalculator().getFeesBy(order.payTYpe);
return order.price + fee;
}
}
FeeCalculator 클래스는 결제 방식에 따라 수수료를 계산하는 책임을 갖고 있다.
class FeeCalculator {
/**
지불 방법별 수수료를 계산한다.
*/
getFeesBy(payType: string): number {
if(payType === '토스') return 100줄넘는엄청복잡한계산1;
if(payType === '카카오') return 100줄넘는엄청복잡한계산2;
if(payType === '네이버') return 100줄넘는엄청복잡한계산3;
return 0;
}
}
불편러의 시점
먼저 FeeCalculator 클래스를 보고 보기 불편한 점은 무엇일까? 나는 payType에 따라 100줄이 넘는 복잡한 코드가 하나의 클래스에 있다라는게 보기 불편하다. 그럼 payType별로 클래스를 만들어 분리해보자
class TossFeeCalculator {
getFees(): number {
//수수료 계산 로직
}
}
class KakaoFeeCalculator {
getFees(): number {
//수수료 계산 로직
}
}
class NaverFeeCalculator {
getFees(): number {
//수수료 계산 로직
}
}
이제 3개의 클래스를 더 만들었고 FeeCalculator가 이 3개의 클래스를 의존하도록 만들어야 한다.
class FeeCalculator {
getFeesBy(payType: string): number {
if(payType === '토스')
return new TossFeeCalculator().getFees();
if(payType === '카카오')
return new KakaoFeeCalculator().getFees();
if(payType === '네이버')
return new NaverFeeCalculator().getFees();
return 0;
}
}
한결 마음이 편안해졌다. 그래도 여전히 어딘가 마음이 불편하다. 코드를 분리하고 보니 FeeCalculator에 3개의 강한 의존성이 생겨버렸다. 의존성이 생긴다 라는것이 무조건 나쁜것은 아니지만, 수수료를 계산하기 위해 FeeCalculator 외 3개의 클래스에 대한 의존성이 발생하여 OrderService 입장에서 결합도가 높아진 결과를 만들었다. (LOW COUPLING 패턴을 기억해보자)
젠장.. 클래스 분리의 결과가 객체간의 결합도를 높이는 결과를 가져왔다.
결합도를 낮추기 전에 FeeCalculator 클래스의 또 다른 문제점을 살펴보자. 그건 바로 변경에 취약한 클래스라는 것이다.
왜 변경에 취약한 클래스인가?
-
요구사항에 제로페이를 추가한다고 가정해보자, 그러면 FeeCalculator 클래스에 if 문을 하나를 추가하는 방법밖에 없다. 따라서 FeeCalculator의 변경이 발생한다.
-
FeeCalculator는 각 회사별 수수료 계산 클래스에 강한 의존성을 갖고 있으므로 각 회사의 수수료 계산 클래스의 변경에 직접적인 영향을 받는다.
코드의 변경은 항상 side effect의 가능성이 있다라는 점을 잊지 말자
다형성을 이용한 해결방법
앞서 다형성이란 추상화 타입을 이용한다고 말했다. 그럼 먼저 TossFeeCalculator, KakaoFeeCalculator, NaverFeeCalculator에 대한 추상화 타입을 만들어 보자.
위 3개의 클래스는 수수료 반환 이라는 같은 책임을 수행한다.
interface VendorFeeCalculator {
isMatch(payType: string): boolean;
getFees(): number;
}
각 클래스가 VendorFeeCalculator 추상화 타입을 구현하도록 만든다.
class TossFeeCalculator implements VendorFeeCalculator {
isMatch = (payType: string) => payType === '토스'
getFees(): number {
//수수료 계산 로직
}
}
class KakaoFeeCalculator implements VendorFeeCalculator {
isMatch = (payType: string) => payType === '카카오'
getFees(): number {
//수수료 계산 로직
}
}
class NaverFeeCalculator implements VendorFeeCalculator {
isMatch = (payType: string) => payType === '네이버'
getFees(): number {
//수수료 계산 로직
}
}
마지막으로 FeeCalculator클래스가 VendorFeeCalculator 인터페이스에 의존하도록 변경한다.
class FeeCalculator {
construct(private calculators: VendorFeeCalculator[]) {}
getFeesBy(payType: string) {
return this.calculators.find( calculator => calculator.isMatch(payType))?.getFees() ?? 0;
}
}
이제 FeeCalculator클래스는 VendorFeeCalculator 인터페이스에만 의존하므로 결합도가 낮아졌고, **FeeCalculator 클래스들에 대한 구체적인 구현에 대한 변경에 자유로워졌다.
객체가 추상화타입을 의존하도록 하여 결합도를 낮출 수 있다.
추상화 타입은 객체가 다형성을 갖을수 있도록 도와준다.
그래서 다형성이 뭔 말인데?
다형성이란 같은 책임을 다른 형태, 방법으로 수행할 수 있고 그것을 추상화 타입(인터페이스)을 통해서 사용할 수 있다 라고 생각하면 된다.
몇가지 예시를 더 들어보자.
전화를 수신하는 책임을 갖는다 => 아이폰객체, 갤럭시객체
글을 쓰는 책임을 갖는다 => 샤프객체, 볼펜객체
이제 느낌이 좀 오는가?
다형성을 언제 사용해야되는가?
다형성을 사용하면 가장 좋은 경우는 예시와 같다.
- 같은 책임을 수행하는 객체가 하나 이상의 형태(방법)으로 존재할 경우 Polymorphism 패턴으로 작성해보자
다른 객체이지만 동일한 책임을 수행한다면 두 객체는 같은 역할을 갖고 있다라고 볼 수 있다.
- 객체의 협력 관점에서 같은 역할을 갖고 있는 객체들은 대체(변경) 가능성이 있기 때문에 클라이언트 입장에서는 구체적인 구현체를 알지 못하게 하고 추상화 타입에만 의존하도록 작성해보자.
변경 가능성이 있는 부분은 추상화 타입을 통해 결합도를 낮춰 변경에 대한 사이드 이펙트를 줄이자.