요구사항(문제점)
기존에 제공하던 쿠폰은 상품 가격에 대한 할인가 적용만 가능했으며 VOC(Voice of Customer, 고객이 비즈니스, 제품 또는 서비스에 대해 말하는 것을 포착한 것) 중 가장 많은 요구사항으로 뽑히는 배송비 무료 쿠폰을 구현해야 하는 업무를 진행하게 되었습니다. 문제는 기존 코드의 구현 방식은 온전히 상품 가격에 대해서만 할인가 및 할인 금액이 적용 가능하도록 구현되어 있었고 코드의 양 또한 방대해 기존 코드 내에서의 수정을 통한 구현은 힘든 상태로 판단했습니다. 또한 코드의 수정을 통한 기능 구현은 OCP를 위반하는 방식이라 생각해 확장이 가능한 구조로 변경해야 하는 요구사항이 있다고 판단했습니다.
그래서 기존 코드의 결합도를 낮추고 로직의 흐름을 변경하지 않는 선에서 분기 처리가 가능하도록 수정해야 하는 필요가 있었습니다.
해결 방법
팩토리 메소드 패턴
팩토리 패턴은 객체를 생성하기 위한 인터페이스를 정의할 때, 어떤 클래스의 인스턴스를 만들지는 서브 클래스에서 결정하게 만드는 패턴을 의미합니다. 이를 통해 객체 간의 결합도를 낮추고 확장에 열려있는 구조로 코드를 구성할 수 있습니다.
쿠폰 적용 로직의 경우에도 사용자가 선택한 쿠폰의 적용 대상(상품 가격, 배송비)에 따라 쿠폰 할인가 계산을 각기 다른 객체가 처리할 수 있도록 구현한다면 추후에 다른 성격의 쿠폰 기능을 추가해야 하는 경우에도 기존 로직의 수정 소요 없이 기능 추가가 가능할 것이라는 장점이 있다고 판단했고 이를 적용해보기로 했습니다.
추상 팩토리 패턴(Abstract Factory Pattern)
팩토리 메소드 패턴 말고 추상 팩토리 패턴도 존재합니다. 추상 팩토리 패턴은 팩토리 객체를 생성하는 상위 팩토리 클래스가 존재하며 연관성이 높은 객체 클래스를 하나의 그룹으로 묶어 관리할 수 있다는 차이점이 있습니다.
팩토리 메소드 패턴 :
팩토리 객체
>하위 객체
추상 팩토리 패턴 :
상위 팩토리 객체(팩토리 객체의 팩토리 객체 개념)
>팩토리 객체
>하위 객체
템플릿 메소드 패턴
템플릿 메소드 패턴은 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방법으로 변하지 않는 기능은 슈퍼클래스에 만들어두고 자주 변경되며 확장할 기능은 서브클래스에서 만들도록 하는 것을 의미합니다. 즉, 공통으로 사용되는 메소드는 상위 클래스에 선언해 하위 클래스에서 그대로 사용하도록 하고, 하위 클래스 별 달라지는 메소드는 각 하위 클래스에서 오버라이딩해 구현하는 것을 의미합니다.
이러한 템플릿 메소드 패턴을 가지고 두가지 종류의 쿠폰에 대한 할인가 적용 로직을 각기 다른 하위 클래스에서 선언해 다른 로직으로 계산이 가능하도록 구현하고자 했습니다.
적용된 코드
해당 코드는 예시를 위해 간단히 구성한 코드입니다.
클래스 다이어그램
팩토리 메소드 패턴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class CouponServiceFactory {
// CouponService 하위 클래스를 CouponTarget에 따라 map 형식으로 저장
private static final Map<CouponTarget, CouponService> map = new HashedMap();
// 팩토리 클래스 생성 시, CouponService 리스트를 조회해 map에 저장
public CouponServiceFactory(List<CouponService> couponServices) {
couponServices.forEach(couponService -> map.put(couponService.getCouponTarget(), couponService));
}
// CouponTarget 값에 따라 해당하는 CouponService를 리턴
public CouponService getCouponService(CouponTarget couponTarget) {
return map.get(couponTarget);
}
}
템플릿 / 메소드 패턴
1
2
3
4
5
6
7
8
9
10
11
public abstract class CouponService {
// 하위 클래스에서 특성과 목적에 맞게 구현해서 사용
public abstract CouponTarget getCouponTarget();
public abstract JSONObject getCouponDiscountAmount();
// 하위 클래스에서 공통으로 사용
public JSONObject getErrorJsonResult() {
// ErrorResult 리턴 로직
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
@RequiredArgsConstructor
public class CouponOrderPriceService extends CouponService {
@Override
public CouponTarget getCouponTarget() {
return CouponTarget.ORDER_PRICE;
}
@Override
public JSONObject getCouponDiscountAmount() {
// 상품가격 쿠폰 할인가 계산 로직
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Service
@RequiredArgsConstructor
public class CouponDeliveryPriceService extends CouponService {
@Override
public CouponTarget getCouponTarget() {
return CouponTarget.DELIVERY_PRICE;
}
@Override
public JSONObject getCouponDiscountAmount() {
// 배송비 무료 쿠폰 할인가 계산 로직
}
}
실제 호출 시
1
2
3
4
5
6
7
// (상품 결제 로직 진행..)
// 쿠폰 종류에 따른 할인 가격 계산
CouponService couponService = couponServiceFactory.getCouponService(couponTarget);
couponService.getCouponDiscountAmount();
// (상품 결제 로직 진행..)
마치며
이처럼 두 패턴을 적용해 두 쿠폰적용대상에 따라 달라지는 로직을 분기처리할 수 있도록 구성했고 이후 로직 상 수정사항이나 새로운 적용대상을 가진 쿠폰 기능을 구현할 때 기존 로직의 변경을 하지 않고 확장할 수 있는 구조를 만들 수 있었습니다. 말 그대로 SOLID 원칙의 OCP를 지키는 구조로 로직을 구현했다는 점에서 객체 지향적인 고민과 패턴 적용을 해볼 수 있는 기회였습니다.