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

비즈니스 요구사항과 설계

(1) 회원

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

(2) 주문과 할인 정책

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라.
    (나중에 변경 될 수 있다.)
  • 할인 정책은 변경 가능성이 높다.
  • 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다.
    최악의 경우
    • 할인을 적용하지 않을 수도 있다. (미확정)

회원 도메인 설계

회원 도메인 협력관계

회원 도메인 협력관계

회원 클래스 다이어그램

회원클레스 다이어그램

회원 객체 다이어그램

회원 객체 다이어그램



회원 도메인 개발

회원 요구사항에 따른 필요 클래스및 인터페이스

Member.class : DB 에 저장할 회원 정보 (회원 엔티티)

grade.enum : 일반회원,VIP회원 을 나눌 enum

MemberService.interface : 회원 서비스의 기능(역할)을 정한 인터페이스

MeberServiceImpl.class : MemberService인터페이스의 구현체 회원가입과 회원조회의 기능을 구현

MemberMemory.interface : DB의 기능(역할)을 정한 인터페이스

MemoryMemberRepository : MemberMemory인터페이스의 구현체 (아직 어떤 DB를 사용할 지 정해지지않음)

package hello.core.member;
//회원등급
public enum grade {
    BASIC,VIP
}
package hello.core.member;

//회원 엔티티
public class Member {

    private Long id;
    private String email;
    private String grade;

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

    public Long getId() {
        return id;
    }

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

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getGrade() {
        return grade;
    }

    public void setGrade(String grade) {
        this.grade = grade;
    }
}
package hello.core.member;

//회원 저장소 인터페이스
public interface MemberRepository {
    void save(Member member);

    Member findId(Long MemberId);

}
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<>();
    //회원 요구사항에 아직 DB구축에대헤 미확정이여서 일단  단순한 회원 메모리인 Map의 구현체인 HashMap을 사용
    //Map이라는 인터페이스로 자바의 다형성을 이용해 언제든지 구현체를 바꿀수있도록 했다.

    @Override
    public void save(Member member){

    }

    @Override
    public Member findId(Long MemberId) {

        return null;
    }
}
package hello.core.member;

// 회원 (기능)서비스 인터페이스
public interface MemberService {
  void 회원가입();
  void 회원조회();
}
package hello.core.member;

//회원서비스 구현체
public class MemberServiceImpl implements MemberService{

    @Override
    public void 회원가입() {

    }

    @Override
    public void 회원조회() {

    }
}


회원 도메인 실행과 테스트



순수 자바코드 테스트

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImp;

public class MemberApp {
    //psvm 단축키
    public static void main(String[] args) {
        //테스트할 객체 MemberService 의 구현채 생성
        MemberService memberService = new MemberServiceImp();

        //가입시킬 멤버 정보 입력
        Member member = new Member(1L, "승빈", Grade.VIP);
        //변수 추출 단축키 control+alt+V
        memberService.회원가입(member);

        Member fMember = memberService.회원조회(1L);
// 단축키 soutv 변수 바로 출력
        System.out.println("member = " + member.getName());
        System.out.println("fMember = " + fMember.getName());

//지금코드는 스프링의 도움없이 순수 자바코드로만 만들어진 테스트 코드이며
        //main함수로 테스트하는거는 한게가 있기 때문에 JUnit 이라는 프레임워크를 사용한다.

    }
}

단축키 정리

public static void main(String[] args) {} = psvm

변수 추출 단축키 = control+alt+V

System.out.println("member = " + member.getName()); 변수 추출 = soutv

실행(테스트)결과

테스트결과1

  • 회원가입 정보와 회원조회의 결과가 같은지 테스트하는결과 이고 테스트결과 같게나와 테스트는 통과하였으나,

    이 테스트는 순수 자바코드로만 만들어진 테스트 코드이여서 main함소로 일일이 콘솔창을 보며

    다 테스트하기에는 한계가 있기 때문에 JUnit 이라는 프레임워크를 사용하여 테스트 하자!


JUnit Test

package hello.core.member;


 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;

 public class MemberServiceTest {

    MemberService memberService = new MemberServiceImp();

    @Test
        void join(){
        //given = 이런게 주어진다.
        Member member = new Member(1L, "승빈", Grade.VIP);

        //when = 이럴때 
        memberService.회원가입(member);
        Member member1 = memberService.회원조회(1L);

        //then = 이렇게 된다.(검증)
         Assertions.assertThat(member).isEqualTo(member1);

    }
}


junit테스트 결과

junit테스트 결과1

  • Assertions이라는 검증 API를 이용해서 member와 member1이 같은지 확인한후 같으면 위와
    같이 초록체크가되면서 테스트가 통과된다.


회원 도메인 설계의 문제점

public class MemberServiceImp implements MemberService {

    MemberRepository memberRepository = new MemberMemoryRepository();
  • 위 코드를 보면 MemberServiceImp클래스는 MemberRepository 인터페이스를 의존하고 MemberMemoryRepository 구현체도 의존한다.

    MemberServiceImp클래스가 인터페이스를 의존하는건 상관없지만, MemberMemoryRepository라는 구현체를 의존한다는게
    문제가 된다.(추상화도의존하고 구체화에도 의존한다는 문제) 따라서 나중에 변경이있을 때 문제가된다.(DIP위반)

    == 의존관계가 인터페이스 뿐만아니라 구현까지 모두 의존하는 문제점이 있다.

    주문까지 만들고나서 문제점과 해결방안을 찾아보자!


주문과 할인 도메인 설계

  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수
      있다.
      )
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을
    • 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)

위 도메인 설계의 핵심은 어떤 할인 정책을 사용할지 미확정 이기 때문에 할인정책을 역할과 구현으로 구분해서 언제든지 할인정책을 바꿀수있도록 설계하는 것이 핵심이다.




주문 도메인 전체 설계

주문 도메인 전체 설계

  • 역활과 구현을 분리해서 저장소와 할인 정책을 유연하게 변경할 수 있도록 설계 했다.


주문과 할인 도메인 개발

할인 정책 인터페이스

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

    int discount(Member member ,int price);
}

할인정책을 유연하게 변경하기위해 인터페이스로 생성
리턴값은 int형 discount 변수에 member,price를 받아서 리턴

할인 정책 구현체

package hello.core.discount;


import hello.core.member.Grade;
import hello.core.member.Member;

public class FixDiscountPolicy implements DiscountPolicy{

private int DiscounFixAmount = 1000;

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

할인정책의 구현체(고정할인)
member를 파라미터로 받았을때 객체의 등급이 VIP일때 DiscounFixAmount 리턴
아니면 기본등급이기때문에 0원을 리턴합니다.

주문 엔티티

package hello.core.order;

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 finalPrice(){
        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 +
                '}';
    }
}

주문생성시 필요한 필드값으로 회원id,상품명,상품가격,주문결과를 리턴할때 할인가를 리턴해야함으로

private Long memberId; //회원id

private String itemName; //상품명

private int itemPrice; //상품가격

private int discountPrice; //할인가

이라는 필드 생성한다.

그리고 파라미터를받아와 객체를 초기화하기위한 생성자 생성,

정보은닉화와 객체의 다향성을 위해 게터와 세터 생성

주문 서비스 인터페이스

package hello.core.order;


public interface OrderService {

    Order orderCreate(Long memberId, String itemName, int itemPrice);
}

주문 서비스 구현체

package hello.core.order;

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

public class OrderServiceImpl implements OrderService{

    MemberRepository memberRepository = new MemberMemoryRepository();
    DiscountPolicy discountPolicy = new FixDiscountPolicy();

    @Override
    public Order orderCreate(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.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.*;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class OrderApp {
    public static void main(String[] args) {

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


        Long memberId = 1L;
        Member member = new Member(memberId, "승빈", Grade.VIP);
        memberService.회원가입(member);

        Order order = orderService.orderCreate(1L, "몰통", 10000);

        System.out.println("order = " + order);


    }

}

결과

주문테스트 실해

할인 금액이 1000원으로 잘 실행되는걸 확인 할 수 있지만, 애플리케이션 로직으로 이렇게 테스트 하는거는 좋은 방법이 아니기때문에
JUnit을 이용해서 테스트 해자!.



주문과 할인 정책 테스트(JUnit 테스트)

package hello.core.order;

import hello.core.member.*;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

    @Test
   void join(){

        OrderService orderService = new OrderServiceImpl();
        MemberService memberService = new MemberServiceImp();
        MemberRepository memberRepository = new MemberMemoryRepository();


        //given
        Long memberId = 1L;
        Member member = new Member(memberId, "승빈", Grade.VIP);
        memberService.회원가입(member);

        //when

        Order order = orderService.orderCreate(memberId, "물통", 10000);

        //then
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

결과

오더서비스 테스트

할인금액을 확인할 회원을 가입시켜 주문객체를 만들어 Assertions API를 사용해서 회원의 할인금액이 1000이 맞는지 확인해 테스트를 통과하였다.