jhhan의 블로그

자바와 스프링 그 사이(2) 본문

JAVA

자바와 스프링 그 사이(2)

jhhan000 2020. 12. 26. 18:03

1편에 이어서 진행하겠습니다...

 

전에는 회원을 만들 수 있는 파트를 진행했습니다.

이제는 회원과 관련된 할인을 적용해보겠습니다.

일단 할인의 경우 VIP에 한해서 1000원 할인만 해보겠습니다.

 

프로젝트 구조입니다.

할인에 대한 도메인을 만들기 때문에 discout 라는 패키지를 추가했습니다.

그리고 인터페이스를 하나 만듭니다.

public interface DiscountPolicy {

    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);
}

현재는 1000원 할인만 진행하지만, 나중에 다른 방식으로 할인을 진행할 수 도 있기 때문에

인터페이스를 이렇게 만듭니다.
(할인 금액만 반환하는 아주 단순한 인터페이스... ㅎ)

다음으로 인터페이스를 구현하는 구현체(클래스)를 만들어보겠습니다.

public class FixDiscountPolicy implements DiscountPolicy{

    private final int discountFixAmount = 1000; // 1000원 할인

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

이렇게 설정하면 VIP 회원에 한해서 1000원을 할인해주는 방식이 적용될 것입니다.

 

회원과 할인에 대해 만들어 봤으니 이제 주문을 할 수 있어야 합니다.

order 패키지를 만들어서 진행합니다.

order 패키지 구성입니다.

먼저 Order Class를 만듭니다.

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() {
        return itemPrice - discountPrice;
    }

    public Long getMemberId() {
        return memberId;
    }

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

    public String getItemName() {
        return itemName;
    }

    public void setItemName(String itemName) {
        this.itemName = itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public void setItemPrice(int itemPrice) {
        this.itemPrice = itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

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

    @Override
    public String toString() {
        return "Order { " +
                "memberId = " + memberId +
                ", itemName = '" + itemName + '\'' +
                ", itemPrice = " + itemPrice +
                ", discountPrice = " + discountPrice +
                " }";
    }
}

Order에는 memberId, itemName, itemPrice, discountPrice 4개로 구성했습니다.
(주문자의 아이디, 상품 이름, 가격, 할인 - 4개를 뜻하는 것을 알 수 있습니다)

calculatePrice() 메서드를 통해 할인이 적용된 최종가격을 알 수 있습니다.

그리고 그에 따라 getter & setter를 만들었고, 추가로 toString()도 만들어서 제가 원하는 방식으로 출력을 할 수 있도록 했습니다.

 

다음으로 OrderService 인터페이스를 만듭니다.

public interface OrderService {
    Order createOrder(Long memberId, String itemName, int itemPrice);
}

주문을 할 수 있게 합니다.
(주문의 경우 이것보다 더 현실적으로 만들어서 진행할 수도 있지만 지금은 굳이 그럴 필요가 없습니다..)

이제 인터페이스를 구현한 구현체 클래스를 만듭니다.

public class OrderServiceImpl implements OrderService{

    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);
    }
}

이 구현 클래스를 통해 이제 주문을 할 수 있게 됩니다.

  1. 주문이 들어오면
  2. 아이디를 통해 회원의 정보를 불러옵니다.
  3. 그리고 회원 정보를 통해 할인가격을 알아냅니다.

아주아주 간단하게 주문을 할 수 있게 했습니다.
(즉, 중간에 주문 취소 & 수정 은 할 수 가 없습니다. 복잡한 건 배제했으니까요...)

 

이렇게 만들었으니 이제 잘 돌아가는지 테스트를 해봅니다.

먼저 자바 방식대로 진행해보겠습니다.

OrderApp 클래스를 만듭니다.

public class OrderApp {

    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        OrderService orderService = new OrderServiceImpl();

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);

        System.out.println("order = " + order);
//        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}

다음과 같이 적어줍니다.

주석 처리한 부분의 경우 -> 할인가격이 적용되어 최종 가격이 찍히게 됩니다.

그리고 실행을 해보면 정상적으로 작동하는 것을 알 수 있습니다.

 

이제 스프링의 방식을 이용해서 테스트 해봅니다.

test 폴더 밑에 order 패키지를 만들고 그 안에 OrderServiceTest 클래스를 만듭니다.

public class OrderServiceTest {

    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder() {
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

Assertions의 경우 org.assertj.core.api.Assertions를 사용하셔야 한다는 건 저번에도 말씀드린 것 같습니다.

잘 진행이 되면 초록색으로 성공된 표시를 볼 수 있고, 만약 어딘가가 문제가 있다면 빨간색으로 오류가 난 것을 확인할 수 있을 것입니다.

 

 

지금까지 만든 것을 약간 변형을 해보겠습니다.

할인을 하는 경우 -> 1000원 할인이 아니라 10% 할인을 하는 것으로 변경을 해보겠습니다.

제가 만든 것은 굉장히 부족한 부분이 많기 때문에... ㅋㅋ

코드를 추가하더라도 변경될 부분이 적습니다.

(근데 실제 코드라 할지라도, 추가는 되도 변경은 없어야 하는게 맞군요 ㅎㅎㅎㅎ)

어쨌든 이제 10% 할인을 하는 것으로 바꿔보겠습니다.

기존의 인터페이스를 이용해 새로운 구현 클래스를 만들어보죠

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;
        }
    }
}

이 클래스를 통해 이제는 10% 할인이 적용될 것입니다.

 

그럼 이것이 잘 적용되는지 테스트해봅니다.

test 폴더 밑에 discount 패키지를 만들고 테스트 클래스를 하나 만듭니다.

class RateDiscountPolicyTest {

    RateDiscountPolicy rateDiscountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용!")
    void vip() {
        // given
        Member member = new Member(1L, "memberVIP", Grade.VIP);

        // when
        int discount = rateDiscountPolicy.discount(member, 10000);

        // then
        assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되면 안됨!")
     void notVIP() {
        // given
        Member member = new Member(2L, "memberBASIC", Grade.BASIC);

        // when
        int discount = rateDiscountPolicy.discount(member, 10000);

        // then
        assertThat(discount).isEqualTo(0);
    }
}

@DisplayName의 경우 테스트를 돌렸을 때 그 안에 있는 내용이 보이게 합니다.

그럼 한번 테스트를 돌려보면...

DisplayName에 적은 대로 잘 나옵니다.

참고로 테스트는 성공 케이스 뿐만 아니라 실패 케이스도 만들어 봐야 테스트가 더 정확해집니다.

저기 있는 테스트 코드에서 일부를 변경해서 진행해보면 빨간색으로 실패가 된다는 것을 볼 수 있습니다.

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되면 안됨!")
     void notVIP() {
        // given
        Member member = new Member(2L, "memberBASIC", Grade.BASIC);

        // when
        int discount = rateDiscountPolicy.discount(member, 10000);

        // then
        assertThat(discount).isEqualTo(1000);
    }

assertThat(discount).isEqualTo(1000); 이라고 하면 어떻게 될까요?

빨간색으로 표시되면서 위의 에러가 등장하는 것을 알 수 있습니다...
(1000이 나오는 것을 예상했는데 0이 나와서 테스트가 실패했다고 하네요.)

 

이렇게 테스트 케이스는 잘 작성된 것으로 보입니다.

 

 

그럼 이제 1000원 할인에서 10% 할인으로 바꿔줘야 합니다.

그럴려면 OrderServiceImpl에 가면 됩니다.

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    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);
    }
}

FixDiscountPolicy를 주석으로 처리한 후 RateDiscountPolicy를 추가하면 됩니다.

이러면 변경이 완료됩니다.

 

근데 여기서 문제점이 나옵니다.

OrderServiceImpl는 클라이언트에 해당합니다. 

즉, Discount하고는 아무런 관련이 없는 도메인이기 때문에 Fix -> Rate으로 바꾸는데

OrderServiceImpl을 바꾸면 OCP, DIP를 제대로 지키지 않은 것입니다.

-> 처음부터 코드를 잘못 짰다는 것입니다.

 

의존관계를 살펴본다면 DiscountPolicy(인터페이스)에만 의존하는 것이 아니라 FixDiscountPolicy(구현체)에도 의존을 하고 있습니다.(DIP 위반)

그리고 Fix -> Rate으로 바뀌면 OrderServiceImpl을 같이 바꿔야 하는 문제점이 생깁니다.(OCP 위반)
(기능 확장 시, 변경이 일어남)

-> 이래서 코드가 잘못 짜여진 것입니다..

 

 

그러면 한번 위반된 내용을 쉽게 수정할 수 있는지 확인해 보겠습니다.

먼저 DIP 위반을 수정해보면..

DiscountPolicy(인터페이스)에만 의존하도록 해야하기 때문에 다음과 같이 변경해봅니다.

    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
//    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private DiscountPolicy discountPolicy;

OrderServiceImpl에 있는 코드에서 주석 처리를 한 다음에

DiscountPolicy만 의존하도록 했습니다. -> 와우 ~! 문제가 해결됐을까요?

실제 구현체가 없기 때문에 이제는 에러가 발생합니다....

기존에 짰던 OrderServiceTest를 한번 실행해본다면

널포인트익셉션이 나타나는 것을 알 수 있습니다.

 

DIP를 지켰더니.. 실제 코드가 돌아가지 않는 더 나쁜 상황이 발생했습니다.

그럼 이제 실제 구현체가 있어야 이 코드가 다시 잘 돌아갈 것입니다.

 

 

그래서 다음 포스트에는 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성해서 주입할 수 있도록 코드를 짜보겠습니다.

 

출처: 인프런 - 스프링 핵심원리(기본편) by 김영한

'JAVA' 카테고리의 다른 글

자바와 스프링 그 사이(마지막)  (0) 2021.01.01
자바와 스프링 그 사이(3)  (0) 2020.12.29
자바와 스프링 그 사이(1)  (0) 2020.12.16
BufferedReader & BufferedWriter  (0) 2020.06.18
Java의 정석을 시작하자(7)(2/11)  (0) 2020.02.14