스프링 핵심 원리 이해1 - 예제 만들기

고동현·2024년 3월 16일
0

Spring 기본

목록 보기
2/10

비즈니스 요구사항과 설계

  • 회원
    회원을 가입하고 조회할 수 있다.
    회원은 일반과 VIP 두가지 등급이 있다.
    회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동 할 수 있다.
  • 주문과 할인 정책
    회원은 상품을 주문할 수 있다.
    회원 등급에 따라 할인 정책을 적용 할 수 있다.
    할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라.
    할인정책은 변경 가능성이 높다.=>아직 미확정이다.

이렇게 미확정 된 부분은 객체지향 설계를 활용하여 인터페이스를 만들고 구현체를 갈아 끼우는 방식으로 설계해보겠다.

회원 도메인 설계


회원 저장소에는 여러가지 구현체들이 들어갈 수 있다.

MemberService 인터페이스를 만들고, 구현체로 MemberServiceImpl을 만든다.
나도 이 글을 적으면서, MemberRepository는 인터페이스를 쓰는건 이해하는데,
왜? memberService를 인터페이스로 구현했지? 싶은 생각이 들었다.
memberService가 변경이 아에 되지 않는다면 인터페이스보다는 class로 구현하는것이 올바르다.
이러한 질문에대한, 댓글을 남긴것을 보면

안녕하세요. ㅇㅇㅇ님
서비스에 인터페이스를 도입하는 것은 장단점이 있습니다.
인터페이스를 만드는 것도 좋지만, 인터페이스를 왜 도입하는지? 라는 질문이 더 중요합니다.
만약 구현체를 전혀 변경할 일이 없다면, 인터페이스를 도입하는 것도 추상화 관점에서 비용이라 생각합니다.
이런 관점에서 실제 실무에서 개발할 때도 서비스에 인터페이스를 도입하는 경우도 있고, 도입하지 않는 경우도 있습니다.
감사합니다.

고로 구현체 Service가 변경할 일이없다면, 인터페이스를 도입하지 않는것이 비용을 아끼는거라고 생각이 들었다.
이번시간은, 객체지향에 대해서 알아보는것이니까 일단 인터페이스로 구현해보았다.

회원 도메인 개발

  • 회원 등급

    회원등급을 enum클래스로 사용했습니다. Enum이란 열거라는 의미로 관련있는 상수들의 집합을 의미합니다. 지금은 일반과 vip두가지 등급밖에없지만 프로젝트가 확장되고, 클래스 내에서 선언하는 상수가 많아질 수록 네이밍이 겹치는것을 방지하고, 상숫값 관리를 용이하게 하기 위해서 Enum을 사용했습니다.
    Enum 을 사용하면
  1. 코드의 단순화로 가독성을 향상시킨다.
  2. 인스턴스 생성과 상속을 방지한다.
  3. enum키워드를 사용하므로서 구현의 의도를 분명히 나타낼수있다.

라는 장점이 있습니다.

  • 회원 엔티티


    Member에 id,name,grade를 필드로 설정하였습니다.
    getter,setter애노테이션을 붙여도되는데, 메소드를 만들었습니다.

  • 회원저장소

    앞에서 누차 설명했듯이, interface로 구현하였습니다.
    구현체는 여러가지로 바꿔끼울 수 있습니다.

  • 메모리 회원저장소 선택

    구현체를 만들었습니다.
    Member를 저장하기위해서 Map을 설정하였고, HashMap으로 구현하였습니다.
    동시성을 고려하면, concurrentHashmap을 사용해야되지만, 이 내용은 앞에서 스프링입문편 [회원 관리 예제-백엔드 개발] 에서 설명하였습니다.
    save,와 findById 메소드를 오버라이딩해서 구현해줬습니다.

  • 회원 서비스 인터페이스

    인터페이스로 구현하였습니다.
    앞에서 설명한것과 같이 구현체가 아에 변할것이 없다. 고정되어있다. 하면, 굳이 인터페이스로 만들 필요가없고, 비용을 절약하는것이 이득입니다.

  • 회원 서비스 구현체

    여기서는 구현체니까 우선 인터페이스를 implents를 해주고,
    MemoryMemberRepository를 사용할 것이므로, 객체지향의 다형성을 이용하여서 memberRepository에 new로 객체를 생성해서 넣어줬습니다.
    현재까지는 객체지향의 다형성을 잘 사용하였지만, OCP,DIP원칙을 지키지 못한 상태입니다.=>왜? 구현클래스에 의존적이므로, => 구현클래스인 MemboryMemberRepository에 의존하지않고 인터페이스에 의존해야합니다.

그러면 어떻게 해야되는지는 뒤에서 한번에 설명하도록 하겠습니다.

회원 도메인 실행과 테스트


join을 test하는 code를 작성하였다. 항상 test하고자하는 메서드에 @Test 에노테이션을 붙여주고,
MemberService를 사용하기위해서 new MemberService Imple를 생성해서 넣어주었다.
join test에서는 Member를 1L,memberA,VIP로 생성후에, join메서드를 불러 저장을한다.
=>그후에 findMember(1L)로 아이디 1 번의 Memeber를 가져온뒤에 동일한지 검사하였다.

주문과 할인 도메인 설계

  • 주문 도메인 협력,역할,책임

    여기서는 주문을 생성할때, id,상품명,상품가격을 order class를 통해 넘겨준다.
    원래 주문data는 DB에 저장하는게 맞는데 일단은, 예제니까 그냥 메모리에 올리겠다.
  • 주문도메인 전체

    역할과 구현을 잘 분리한것을 볼 수 있다. 역할로 표시된부분은, 전부 인터페이스로 만들것이고 원할때 갈아 끼워서 넣을수있게 구현체를 만들어줬다.

이렇게하면 좋은점이, 주문서비스를 변경하지 않아도, 역할들의 협력관계를 그대로 재사용할 수 있다.


위에서는 메모리에서 조회 아래에서는 DB에서 조회, 위에서는 정액 할인정책, 아래서는 정률할인정책, 구현체들이 전부 변경되었지만, 주문서비스는 변경하지 않아도 된다. 그저, 구현체들만 바꿔끼워주면 된다.

주문과 할인 도메인 개발

  • 할인 정책 인터페이스

    인터페이스로 설계하여서, 나중에 고정할인정책,정률할인정책으로 구현체를 바꿔 끼울 수 있게 설계했습니다. [다형성활용]

  • 정액 할인 정책 구현체

    VIP이면 1000을 할인해주게 return 1000원 아니면 0원할인

  • 주문 엔티티


    주문을 할때 id와, itemName, itemPrice,discountPrice를 받아서 orderclass를 만들고 주문서비스 구현체에 넘겨주었다.

  • 주문 서비스 인터페이스

    사실 주문서비스는 구현체를 바꿔 끼울게 없이 그냥 oder를 만드는 기능이 한가지 있어서 굳이 인터페이스로 안하고, 그냥 class로 만들어도 될꺼같은데, 뭐 다형성을 위해서 일단 예제에서는 이렇게 만들어보자.=>인터페이스도 비용이 드므로, 비용을 아끼기위해서 class로 만드는게 올바른거 같다.

  • 주문 서비스 구현체

    우선 member를 찾아야하니까 memberRepository를 만들고,
    DiscountPolicy에는 정액 할인정책을 적용할꺼니까 FixDiscountPolicy로 구현체를 넣는다. 나중에 이 Fix가 아니라 Rate비율 discountPolicy로 바꿀것이다.

createOrder메서드는 그냥, member를 찾아서 discountPolicy로 할인 가격을 받아서(vip면 1000 아니면 0) Order를 만들어서 return해준다.

뭐 로직같은것은 너무간단하고, 핵심적인 부분은 단일 책임 원칙을 잘 지켰다는것이다.
만약에 OrderService와 DiscountPolicy를 분리하지 않았다고 생각해보자.

뭔가 어? if 주문이 들어오면, member를 찾아서 vip면 order에 -1000원하고 return order
이거 될꺼같지 않나? 싶을수있다.
그러나 만약에 이제 앞으로 1000원이 아니라 2000원 할인으로 바꿔준다고 치자.

그러면 이 orderService에서 1000원이 아니라 2000으로 바꾸는코드를 전부 찾아서 바꿔줘야한다.

그러나 이 discount와 order를 분리하게되면 위에 있는 코드와 같이, 난 가격할인정책같은건 모르겠고, 그냥 객체 member만 넘겨주면 discount에서 처리해서 반환해줘, 하면
가격할인 정책이 아무리 변경되어도, service는 안고쳐줘도 되는것이다.

주문과 할인 도메인 실행과 테스트

  • 주문과 할인 정책 실행

    주문을 생성하기위해서 MemberService와 OrderService를 만들어주었고,
    그다음에는 join메서드를 통해서 가입하고, createOrder메서드로 order의 DiscountPrice가 Vip이므로 1000원이 맞는지 확인해보았다.
    여기서 왜 long이아니라 Long을 썻지? 고민을 좀해봤는데,

    long은 원시타입,Long은 참조타입이다.
    원시타입은, 실제 메모리에 데이터값을 직접 저장하는거고
    참조타입은, 객체의 메모리 주소값을 통해 객체를 참조하는 타입이다.
    long에는 그러므로 메모리에 올라가야하므로 null할당이 불가능하다.
    만약에 Service가 DB에 들어가고 이런식이라면, null이 들어가야하는 경우도 있을
    것 같아서 Long으로 만들어줬다.

그리고 이 Test는 단위Test이다 이게 뭔말이냐하면, SpringBootTest 애노테이션을 달지 않아서, 뭐 컨테이너에서 꺼내쓰거나 DI같은게 없다.
즉, 순수 JAVA코드로만 이루어져 있기 때문에, Test속도가 훨씬빠르다. 그래서 test코드는 단위test로 짜는게 좋다고한다.

단위테스트-Unit Test작성의필요성
실무에서 테스트 코드를 짠다고 하면, 거의 단위테스트를 의미한다.
통합테스트는 실제 여러 컴포넌트들의 상요작용들을 테스트하므로, 모든 컴포넌트들이
구동된상태에서 테스트를 하게 된다.
그러므로 캐시나 DB같은 다른 컴포넌트들과 실제 연결을 하고 이러한 과정에서 많은 비용이 소비된다.
반면에 단위 테스트는 해당 부분만 독립적으로 테스트 하기 때문에 어떤 코드를 리팩토링하여도 빠르게 문제여부를 확인할 수 있다.

profile
항상 Why?[왜썻는지] What?[이를 통해 무엇을 얻었는지 생각하겠습니다.]

0개의 댓글