Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

Mini

객체지향으로 할인요구사항 구현하기 본문

기술블로그

객체지향으로 할인요구사항 구현하기

Mini_96 2025. 7. 5. 19:58

객체지향으로 할인요구사항 구현하기

jpashop에 아이템을 할인할수있게 해달라는 요구사항이 들어왔습니다.

이를 객체지향을 이용하여 확장에는 열려있고 수정에는 닫혀있는 코드를 구현해 보겠습니다.

먼저 메시지를 결정하고, 이를 처리할 객체를 결정합니다. 이때, 메시지를 처리하기 위해 정보를 많이 알고있는 객체를 선택합니다.

 

상속을 이용한 방법

할인정책 세부 클래스를 구현할때 어떤 방법이 있는지 보겠습니다.

단점 : 할인정책 변경을 위해, 새로운 Item 인스턴스를 만들고 필요한 정보 복사가 필요합니다.

 

상속대신 합성을 사용하라

장점 : 할인정책이 추가 되더라도 새로운 할인정책 클래스를 추가하고,이를 Item 생성시 전달하면 됩니다. 

즉 Item에 연결된 DiscountPolicy 인스턴스만 바꾸면 되므로, 이 방법을 선택하겠습니다.

 

할인정책을 누가 가지고 있을것인가

order vs item vs orderItem

order : 주문 전체에 대해 동일한 할인정책을 적용할때

item : 아이템 별로 할인정책을 적용할때

orderItem : 주문상품별로 각각 다른 할인정책을 적용할때

-> 네이버 페이의 경우 item 별로 할인정책이 적용된것을 볼 수 있음. ->

정보전문가에게 책임을 할당하라. 가격 정보를 알고있는 item 객체를 선택!

 

new는 해롭지만, 가끔은 생성해도 무방하다

Item이 대부분의 경우에 NoneDiscountPolicy와 협력하고, 가끔씩만 AmountDiscountPolicy 또는 PercentDiscountPolicy와 협력하는 경우, 클라이언트에게 생성하는 책임을 모두 넘긴다면, 중복되는 new NoneDiscountPolicy 코드가 많아지게 됩니다.

이럴 때 생성자 체이닝을 이용하면 좋습니다.

protected Item(String name , int price, int stockQuantity, DiscountPolicy discountPolicy) {
    this.name=name;
    this.price=price;
    this.stockQuantity=stockQuantity;
    this.discountPolicy= discountPolicy;
}

protected Item(String name, int price, int stockQuantity) {
    this(name, price, stockQuantity, new NoneDiscountPolicy());
}

 

할인정책, 할인조건을 어떻게 영속화 할 것인가

한테이블 전략 vs Joined 전략

기본적으로 Joined를 선택하고, 조인에 성능상 이슈가 있거나 요구사항이 매우간단한 경우에만 한테이블 전략을 선택하면 좋습니다.

참고 : 영속화를 위해 interface에서 abstract class로 변경해야 하는 단점이 존재합니다. (jpa가 interface를 지원하지않음)

@Entity
@Table(name = "discount_policy")
@Inheritance(strategy = JOINED)
public abstract class DiscountPolicy {

    @OneToMany(
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    @JoinColumn(name = "policy_id", nullable = false)
    private List<DiscountCondition> conditions = new ArrayList<>();

양방향 vs 단방향 연관관계

기본적으로 단방향 연관관계로 설정하고, 할인조건에서 할인정책을 참고해야하는 상황이 생기는 경우에만 양방향 연관관계를 추가합니다.

 

이 설계가 유연하고 재사용 가능 한가? (검증)

할인혜택을 제공하지 않아도 된다는 요구사항 추가

먼저 문제가 있었던 구현방식을 먼저 보겠습니다.

class Item{
...

    public Money calculateItemFee() {
        if(discountpolicy==null) return price;
        return Money.of(price).minus(discountPolicy.calculateDiscountAmount(this));
    }

위 코드의 문제점은 할인정책의 변경으로 인해 Item 클래스를 수정해야 한다는 점입니다.

이는 버그의 발생 가능성을 높이게 됩니다.

해결하는 방법은 할인정책이 없다는 사실을 할인정책으로 추가하는 것입니다.

@Entity
@DiscriminatorValue("NONE")
public class NoneDiscountPolicy extends DiscountPolicy {
    @Override
    protected Money getDiscountAmount(Item item) {
        return Money.ZERO;
    }
}

클래스를 단1개만 추가 하였습니다. OCP 원칙을 준수하는 설계라고 할수 있습니다.

 

중복할인 정책 요구사항 추가

아이템 1개에 대해 비율할인, 정률할인을 모두 할 수 있어야 한다는 요구사항이 추가되었습니다.

public class OverlappedDiscountPolicy extends DiscountPolicy {

    private List<DiscountPolicy> discountPolicies = new ArrayList<>();

    public OverlappedDiscountPolicy(DiscountPolicy... policies) {
        super(new NoneCondition());
        this.discountPolicies = Arrays.asList( policies);
    }

    @Override
    protected Money getDiscountAmount(Item item) {
        Money result = Money.ZERO;
        for (DiscountPolicy each : discountPolicies) {
            result = result.plus(each.calculateDiscountAmount(item));
        }
        return result;
    }
}

클래스 1개를 추가하는것만으로 요구사항 구현이 완료되었습니다!

@Test
@DisplayName("중복할인 정책이 적용되어 상품의 가격에서 각각 1000원이 할인되고, 원가의 30%가 추가 할인된다.")
void createItem5() {
    //given
    Book book = new Book("Test Item", 10000, 10, "조영호", "오브젝트",
            new OverlappedDiscountPolicy(
                    new AmountDiscountPolicy(Money.of(1000),
                            new NoneCondition()
                    ),
                    new PercentDiscountPolicy(0.3,
                            new NoneCondition()
                    )
            ));

    ...

    //when
    Money discountMoney = book.calculateItemFee();

    //then
    assertEquals(Money.of(6000), discountMoney);
    assertEquals(Money.of(18000), order.getTotalPrice());
}

결과 (30000의 30%인 9000원할인, 1000원 할인)

 

결론

변하는 부분과 변하지 않는 부분을 분리하고, 변하는 부분(할인정책, 할인조건)을 추상화 하였습니다.

이로 인해 새로운 요구사항이 추가되더라도 클래스 1개를 추가하는것만으로 개발이 완료되는 코드를 만들었습니다.

이때까지 과제테스트를 제출할때 위와 같은 할인, 쿠폰등과 같은 요구사항이 있는경우, DiscountType같은 필드변수와 if else 문으로 문제를 해결하려고 했습니다. 하지만 이를 객체지향적으로 해결하는 방법을 배웠고, 코드를 짤때 객체지향적인지 변화에 대응이 되는지 생각하면서 코드를 짜야겠습니다.

 

레퍼런스

https://product.kyobobook.co.kr/detail/S000001766367

 

오브젝트 | 조영호 - 교보문고

오브젝트 | 역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음

product.kyobobook.co.kr