Spring/Spring-Basic

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

y-seo 2023. 9. 5. 02:38

프로젝트 생성

  • 추가 설정 for 속도

 

비즈니스 요구사항과 설계

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 등급이 있다.
    • 회원 데이터는 자체 DB 구축하거나 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원은 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (추후 변동 가능)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다.(미확정)
  • 요구 사항을 보면 결정하기 어려운 부분이 있는데 이를 위해 우리는 역할과 구현을 구분하면 된다. (객체 지향 설계 방법, 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계)
  • 스프링 프레임워크 미사용

 

회원 도메인 설계

  • 회원 도메인 협력 관계
    • 회원 저장소 역할의 구현을 그 아래 세가지로 나눌 것이다 → 나중에 구현체 선택

  • 회원 클래스 다이어그램
    • 실제 구현 레벨
    • MemberService에 대한 구현체로 MemberServiceImpl을 만듦
    • MemoryMemberRepository인지 DbMemberRepository인지는 서버를 띄울 때 결정되기 때문에 클래스 다이어그램만으로는 결정하기 어려움
    • 정적이다

  • 회원 객체 다이어그램
    • 객체 간의 참조가 어떻게 되는지
    • 동적이다

 

회원 도메인 개발

1.  hello.core에 member 패키지 생성

2. Grade라는 Enum Java Class 생성 후 아래 코드 작성

package hello.core.member;

public enum Grade {
    BASIC,
    VIP
}
  • 회원 저장소 만들기

3. Member라는 Java Class 생성 후 아래 코드 작성

package hello.core.member;

public class Member { //Member의 속성 3가지
    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() { //Getter
        return id;
    }

    public String getName() {
        return name;
    }

    public Grade getGrade() {
        return grade;
    }

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

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

    public void setGrade(Grade grade) {
        this.grade = grade;
    }
    
    
}
  • Member의 속성은 3가지

생성자 단축키 : Alt + Insert

4. MemberRepository라는 Interface Java Class 생성 후 아래 코드 작성

package hello.core.member;

public interface MemberRepository {
    
    void save(Member member); //회원 저장
    
    Member findById(Long memberId); //회원 찾기
}
  • 회원 저장소(인터페이스)를 만든 것 → 구현체를 만들어야 함
  • 인터페이스와 구현체는 다른 package에 두는 것이 설계상 좋음

5. MemmoryMemberRepository라는 Java Class 생성 후 아래 코드 작성

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); //ID 찾기
    }
}
  • 회원 저장소 만들기 끝
  • 회원 서비스 만들기

6. MemberSerivce라는 Interface Java Class 생성 후 아래 코드 작성

package hello.core.member;

public interface MemberService {

    void join(Member member); //회원 가입
    Member findMember(Long memberId); //회원 조회
    
}

7. MemberServiceImpl 라는 Java Class 생성 후 아래 코드 작성

package hello.core.member;

public class MemberServiceImpl implements MemberService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
  • 구현체 생성
  • 회원 서비스 만들기 끝

 

회원 도메인 실행과 테스트

1. java/hello/core/MemberApp.java 생성 후 아래 코드 작성

package hello.core;

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

public class MemberApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        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());
    }
}

2. 테스트 : 순수 자바로만 작성한 결과

3. JUnit으로 테스트 하기 위해 아래와 같은 package 생성

4. MemberServiceTest 라는 Java Class 생성 후 아래 코드 작성

package hello.core.member;

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

public class MemberServiceTest {

    MemberService memberService = new MemberServiceImpl();

    @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); //똑같냐
    }
}

5. 테스트 하기

  • 1,2번의 검증에서는 나의 눈으로 검증해야 함
  • 3번 이후의 검증에서는 오류가 뜸

 

  • 회원 도메인 설계의 문제점
    • OCP 원칙을 지킬지
    • DIP를 잘 지킬지
    • 의존관계가 구현까지 모두 의존하기에 문제가 있음

 

주문과 할인 도메인 설계

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

  • 주문 도메인 전체
    • 역할을 먼저, 구현을 나중에
    • 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 함

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

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

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

  • 무얼 지원해도 주문 서비스를 변경하지 않아도 된다. 협력 관계를 그대로 재사용 할 수 있다.

 

주문과 할인 도메인 개발

1. hello.core.discount 패키지 생성

2. DiscountPolicy 라는 Interface 생성 후 아래 코드 작성

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

    /**
     * @return 할인 대상 금액
     */

    int discount(Member member, int price);
}

3. 구현체 만들기 → FixDiscountPolicy 라는 Java Class 생성 후 아래 코드 작성

package hello.core.discount;

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

public class FixDiscountPolicy implements DiscountPolicy{

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

3. hello.core.order 패키지 생성

4. Order 이라는 Java Class 생성 후 아래 코드 작성

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

5. OrderService 라는 인터페이스 생성 후 아래 코드 작성

package hello.core.order;

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

6. OrderServiceImpl 라는 구현체 Java Class 생성 후 아래 코드 작성

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

 

객체 만들기 → 인터페이스 만들기 → 구현체 만들기 → 테스트 코드 짜기

 

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

1. hello.core 아래 OrderApp 이라는 Java Class 생성

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
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 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.toString());
        System.out.println("order.calculatePrice = " + order.calculatePrice());

    }
}

2. 실행하기

3. JUnit으로 옮기기

4. order 패키지 생성

5. 패키지 아래 OrderServicTest 라는 Java Class 생성 후 아래 코드 작성

package hello.core.order;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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

6. 실행하기