[Spring] 예제 만들기

jy9922·2022년 8월 16일
0

Spring

목록 보기
13/34
post-thumbnail

Project 시작 🚩

스프링 없는 순수한 자바로만 개발을 진행해보자

✔ 프로젝트 비즈니스 요구사항과 설계

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 10000원을 할인해주는 고정 금액 할인을 적용해달라.
      ( 나중에 변경 가능 )
    • 할인 정책은 변경 가능성이 높다.
      • 회사의 기본 할인 정책을 아직 정하지 못 했고, 오픈 직전까지 고민을 미루고 싶다.
      • 최악의 경우 할인을 적용하지 않을 수 있다. (미확정)
  • 요구사항을 보면 회원 데이터, 할인 정책 같은 부분은 지금 결정하기 어려운 부분임
  • 그렇다고 이런 정책이 결정될 때까지 개발을 무기한 기다릴 수도 없음

따라서, 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계하면 된다.

✔ 회원 도메인 설계

  • 회원 도메인 요구사항
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

회원 도메인 협력 관계

  • 클라이언트가 회원 서비스를 호출하고 두 가지 기능을 제공한다.
  • 회원 저장소를 인터페이스(역할)로 만든다.
    • 다양한 방법의 구현 객체를 만든다.

회원 클래스 다이어그램

회원 객체 다이어그램

  • 실제 객체 간의 메모리의 참조를 나타낸 그림이다.

✔ 회원 도메인 개발

  • grade enum
public enum Grade {
    BASIC,
    VIP
}
  • 회원 entity
package hello.core.member;

public class Member {
    private Long id;
    private String name;
    private Grade grade;

    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Grade getGrade() {
        return grade;
    }

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
}
  • MemberRepository 인터페이스
package hello.core.member;

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);
}
  • MemoryMemberRepository 구현체
package hello.core.member;

import java.util.HashMap;
import java.util.Map;

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>();

    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }
}
  • MemberService
package hello.core.member;

public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}
  • MemberServiceImp
    • 인터페이스의 구현체가 하나이면 뒤에 Imp 붙이는 것이 관례
package hello.core.member;

public class MemberServiceImp implements MemberService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    // 구현 객체를 선택해서 MemberRepository 선언

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }
    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

✔ 회원 도메인 실행과 테스트

  • 테스트를 위한 코드 ( MemberApp )
package hello.core;

import hello.core.member.*;

public class MemberApp {

    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImp();
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());
    }
}
  • junit을 이용한 테스트 코드
public class MemberServiceTest {

    MemberService memberService = new MemberServiceImp();

    @Test
    void join(){
        //given
        Member member = new Member(1L, "memberA", Grade.VIP);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}

회원 도메인 설계의 문제점

  • 다른 저장소를 변경할 때 OCP 원칙을 잘 준수하고 있는가?
    • OCP : 확장에는 열려 있으나 변경에는 닫혀 있어야 한다는 의미
  • DIP를 잘 지키고 있나?
    • 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음
    • DIP : 역할에 의존해야지 구현체에 의존하면 변경이 어려워진다.

✔ 주문화 할인 도메인 설계

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

  1. 주문 생성 : 클라이언트는 주문 서비스에 주문 생성을 요청
  2. 회원 조회 : 할인을 위해서는 회원 등급이 필요, 주문 서비스는 회원 저장소에서 회원을 조회
  3. 할인 적용 : 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임
  4. 주문 결과 반환 : 주문 서비스는 결과를 포함한 주문 결과를 반환

주문 도메인 전체

역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계한다.
회원 저장소와 할인 정책을 유연하게 변경할 수 있다.

주문 도메인 클래스 다이어그램

주문 도메인 객체 다이어그램

  • 회원 메모리에서 조회하고, 정액 할인 정책을 지원해도 주문 서비스를 변경하지 않아도 된다.
  • 역할들의 협력 관계를 그대로 재사용할 수 있다.

주문 도메인 객체 다이어그램2

  • MemberRepository가 Memory에서 DB로 바뀌고 DiscountPolicy가 Fix에서 Rate로 바뀌어도 주문 서비스 구현체를 변경할 필요가 없다.
    • 협력 관계를 그대로 재사용 할 수 있다.

✔ 주문화 할인 도메인 개발

DiscountPolicy

  • DiscountPolity 인터페이스
public interface DiscountPolicy {
    /*
    * @return 할인 대상 금액
    * */
    int discount(Member member, int price);
}
  • FixDiscountPolicy 구현체
public class FixDiscountPolicy implements DiscountPolicy {
    private int discountFixAmount = 1000; // 1000원 할인

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP){
            // enum 타입은 비교 타입을 ==으로 표현
            return discountFixAmount;
        }else{
            return 0;
        }
    }
}

Order 서비스

  • order entity
package hello.core.order;

import hello.core.member.Grade;

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 calculatorPrice(){
        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 +
                '}';
    }
}
  • OrderService 인터페이스
package hello.core.order;

public interface OrderService {
    Order createOrder(Long memberId, String itemName, int itemPrice);
}
  • OrderService 구현체 - OrderServiceImpl
package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;

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

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

  • 주문과 할인 도메인 실행 - OrderApp
public class OrderApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImp();
        OrderService orderService = new OrderServiceImp();

        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.calculator = "+ order.calculatorPrice());
    }
}

  • Junit 테스트 코드
public class OrderServiceTest {
    MemberService memberService = new MemberServiceImp();
    OrderService orderService = new OrderServiceImp();

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

0개의 댓글