Spring

[Spring] 주문과 할인 도메인 설계

songsua 2025. 3. 16. 17:11

1. OrderService 인터페이스를 생성하여 Impl에서 받게 할 것이다

2. Impl 에서 Member 정보를 저장한 interface 와 할인 정책을 가지고 있는 Discount interface에 따를 것이다.

 

 

주문과 할인 도메인 개발

 

discount 개발

package hello.springbasic.discount;

import hello.springbasic.member.Member;

public interface DiscountPolicy {
    //return은 할인 대상 금액
    int discount(Member member, int price);

}

 

할인을 하기 위해서는 price와 member가 필요하다.

package hello.springbasic.discount;

import hello.springbasic.member.Grade;
import hello.springbasic.member.Member;

public class FixdiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        }

        else {
            return 0;
        }
    }

}

 

고정할인은 1000원으로 고정하고,

만약 맴버의 등급이 vip라면 할인율은 1000원으로 내보낸다

그렇지 않을 경우에는 할인율은 없다.

 

 

package hello.springbasic.order;

import hello.springbasic.member.Member;

public class Order {
    private Long memberId;
    private  String ItemName;
    private  int ItemPrice;
    private  int discountPrice;


    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.ItemName = itemName;
        this.ItemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }
    
    //계산된 결과
    public int calculatePrice(int discountPrice) {
        this.discountPrice = discountPrice;
        return discountPrice;
    }
    //toString을 사용하여 보기 편하게 반환한다
    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", ItemName='" + ItemName + '\'' +
                ", ItemPrice=" + ItemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }

    public Long getMemberId() {
        return memberId;
    }

    public void setMemberId(Long memberId) {
        this.memberId = memberId;
    }

    public String getItemName() {
        return ItemName;
    }

    public void setItemName(String itemName) {
        ItemName = itemName;
    }

    public int getItemPrice() {
        return ItemPrice;
    }

    public void setItemPrice(int itemPrice) {
        ItemPrice = itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }
}

주문 페이지에서는 고객의 멤버아이디, 고객이름, 주문한 아이템, 아이템의 가격을 가지고 온다.

 

 

OrderService interface

package hello.springbasic.order;

public interface OrderService {
    //클라이언트는 주문을 생성할 때, 회원id,상품명,상품가격을 넘겨야한다.
    //주문서비스에서는 2. 회원 조회, 3. 할인적용 의 역할을 위임할 것이다.
    Order createOrder(Long memberId, String itemName, int itemPrice);


}


OrderImpl

package hello.springbasic.order;

import hello.springbasic.discount.DiscountPolicy;
import hello.springbasic.discount.FixdiscountPolicy;
import hello.springbasic.member.Member;
import hello.springbasic.member.MemberRepository;
import hello.springbasic.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    //회원을 찾기 위해 repository에서 정보를 가져와야 한다.
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixdiscountPolicy();


    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
       int discountPrice = discountPolicy.discount(member, itemPrice);
       return new Order(memberId, itemName, itemPrice, discountPrice);

    }
}

id을 찾고, discountPolicy을 이용하여 할인가격을 받아온다.

 

 

그렇다면 요구사항이 변경되어서 할인율을 정률 할인정책으로 분리해볼 것이다.

 

RatediscountPolicy 을 생성하여 vip일 경우 10% 할인이 적용되도록 구현할 것이다.

package hello.springbasic.discount;

import hello.springbasic.member.Grade;
import hello.springbasic.member.Member;

public class RatediscountPolicy implements DiscountPolicy {

    private int discountPercent = 10;
    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent/ 100;
        } else {
            return 0;
        }
    }
}

 

 

새로운 할인정책 적용해보기

package hello.springbasic.order;

import hello.springbasic.discount.DiscountPolicy;
import hello.springbasic.discount.FixdiscountPolicy;
import hello.springbasic.discount.RatediscountPolicy;
import hello.springbasic.member.Member;
import hello.springbasic.member.MemberRepository;
import hello.springbasic.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    //회원을 찾기 위해 repository에서 정보를 가져와야 한다.
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new RatediscountPolicy();


    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
       int discountPrice = discountPolicy.discount(member, itemPrice);
       return new Order(memberId, itemName, itemPrice, discountPrice);

    }
}

 

OrderServiceimpl 에서 객체 fixdiscountPolicy 을 RatediscountPolicy로 고치면된다.

하지만 이는 Orderserviceimpl는 fix와 rate에 의존된 것으로 보인다.

 

원했던 의존관계



실제 의존관계

이는 DIP 을 위반한 것이다.
DIP(의존성 역전 원칙, Dependency Inversion Principle)은 SOLID 원칙 중 하나로,
상위 모듈(추상적인 것)이 하위 모듈(구체적인 것)에 의존하면 안 된다는 원칙

즉, "구체적인 구현(Concrete Class)에 의존하지 말고, 추상적인 인터페이스(Interface, 추상 클래스)에 의존해라!"
💡 쉽게 말해, **"유연한 코드"**를 만들기 위한 원칙이다.

 

결국은 인터페이스에만 의존하게 해야하는 것이다!

 

해결하는 방법:

package hello.springbasic.order;

import hello.springbasic.discount.DiscountPolicy;
import hello.springbasic.discount.FixdiscountPolicy;
import hello.springbasic.discount.RatediscountPolicy;
import hello.springbasic.member.Member;
import hello.springbasic.member.MemberRepository;
import hello.springbasic.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    //회원을 찾기 위해 repository에서 정보를 가져와야 한다.
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
       int discountPrice = discountPolicy.discount(member, itemPrice);
       return new Order(memberId, itemName, itemPrice, discountPrice);

    }
}

 

new 을 없애버리고 인터페이스만 의존하게 한다.

그러면 nullpointexception발생한다.
하지만 인터페이스에는 값이 할당이 안되어있다. 인터페이스부분을 어떻게 변경해야할까?
인터페이스의 구현객체를 대신 생성하고 주입해줘야한다.

 

 

AppConfing 등장

공연  기획자같이 FixDiscount, RateDiscount 중에 무엇을 선택할지를 OrderService 가 선택하는 것은 안된다.

AppConfig을 사용하여 이 클래스가 여기서 FixDiscount, RateDiscount 중 어느걸 사용할지 선택 후에
Orderservice는 그저 실행해주는 역할을 하도록 해야한다.

 

package hello.springbasic;

import hello.springbasic.discount.FixdiscountPolicy;
import hello.springbasic.member.MemberService;
import hello.springbasic.member.MemberServiceImpl;
import hello.springbasic.member.MemoryMemberRepository;
import hello.springbasic.order.OrderService;
import hello.springbasic.order.OrderServiceImpl;

public class AppConfig {


    //생성자를 주입한다
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        //orderserviceImpl은 두개를 받고 있는데,
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixdiscountPolicy());
    }
}
package hello.springbasic.order;

import hello.springbasic.discount.DiscountPolicy;
import hello.springbasic.discount.FixdiscountPolicy;
import hello.springbasic.discount.RatediscountPolicy;
import hello.springbasic.member.Member;
import hello.springbasic.member.MemberRepository;
import hello.springbasic.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    //회원을 찾기 위해 repository에서 정보를 가져와야 한다.
    private final MemberRepository memberRepository ;
    private final DiscountPolicy discountPolicy;
    //private final DiscountPolicy discountPolicy = new RatediscountPolicy();
    //이는 추상클래스만을 의존하는 것이 아니라 구체(구현클래스) 에도 의존하는 것

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
       int discountPrice = discountPolicy.discount(member, itemPrice);
       return new Order(memberId, itemName, itemPrice, discountPrice);

    }
}
package hello.springbasic;

import hello.springbasic.member.Grade;
import hello.springbasic.member.Member;
import hello.springbasic.member.MemberService;
import hello.springbasic.order.Order;
import hello.springbasic.order.OrderService;
import hello.springbasic.order.OrderServiceImpl;
import hello.springbasic.AppConfig;


public class OrderApp {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();


        Member member = new Member("맴버a" , Grade.VIP, 1L);
        memberService.join(member);
        Order order = orderService.createOrder(1L,"음료" , 5000);
        System.out.println(order);


    }
}
package hello.springbasic;

import hello.springbasic.discount.DiscountPolicy;
import hello.springbasic.discount.FixdiscountPolicy;
import hello.springbasic.member.MemberService;
import hello.springbasic.member.MemberServiceImpl;
import hello.springbasic.member.MemoryMemberRepository;
import hello.springbasic.order.OrderService;
import hello.springbasic.order.OrderServiceImpl;

public class AppConfig {


    //생성자를 주입한다
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        //orderserviceImpl은 두개를 받고 있는데,
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixdiscountPolicy());
    }

    public DiscountPolicy discountPolicy() {
        return new FixdiscountPolicy();
    }
}

 

 

AppConfig를 보면 역할과 구현 클래스가 한눈에 들어온. 그래서 애플리케이션 전체 구성이 어떻게 되어있는지 빠르게 파악할 수 있다.

 

 

그러면 이제 FixCountPolicy 에서 RatecountPolicy 로 바꿀 것이다. 
이제 어느부분만 바꾸면 될까?

이제 AppConfig만 수정하면 된다.

package hello.springbasic;

import hello.springbasic.discount.DiscountPolicy;
import hello.springbasic.discount.FixdiscountPolicy;
import hello.springbasic.discount.RatediscountPolicy;
import hello.springbasic.member.MemberService;
import hello.springbasic.member.MemberServiceImpl;
import hello.springbasic.member.MemoryMemberRepository;
import hello.springbasic.order.OrderService;
import hello.springbasic.order.OrderServiceImpl;

public class AppConfig {


    //생성자를 주입한다
    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        //orderserviceImpl은 두개를 받고 있는데,
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixdiscountPolicy());
    }

    public DiscountPolicy discountPolicy() {
       // return new FixdiscountPolicy();  이전 할인 정책
        return new RatediscountPolicy();
    }
}

return부분만 수정하여 클라이언트 코드를 안바꿔도 된다. 사용영역은 변경할 필요가 없다.