[Spring] 기본편 02. 스프링 핵심 원리 이해1 - 예제 만들기

49145 단어 SpringSpring

이 글은 스프링 핵심 원리 - 기본편 을 듣고 정리한 내용입니다.

📌 프로젝트 세팅

  • https://start.spring.io/ 에서 다음과 같이 설정하고 GENERATE를 통해 다운받기.
  • Intellij에서 [Open or Import]-> 다운받은 [core] 폴더 안에서 -> build.gradle 누르고 [Open] -> [Open as Procject]
  • 이번 강의에서는 순수 java로만 진행해볼 거라서 Dependencies는 선택하지 않음.

  • Intellij IDEA -> preference에서 'gradle'입력하고 'Build and run using' 부분과 'Run tests using' 부분을 gradle->Intellij IDEA로 바꿔준다.
  • 이렇게 설정해야 Java를 실행할 때, Gradle을 거치지 않고, Intellij에서 바로 실행해서 더 빠름.

📌 회원 도메인 설계

[회원 도메인 요구사항]

  • 회원을 가입하고 조회할 수 있다.
  • 회원은 일반과 VIP 두 가지 등급이 있다.
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다.(미확정)
  • 도메인 협력관계는 기획자들도 볼 수 있음.
    이것을 바탕으로 개발자가 구체화에서 클래스 다이어그램을 만들어냄.

📌 회원 도메인 실행과 Test

  • Member.class
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;
    }
}
  • Grade.enum
package hello.core.member;

public enum Grade {
    BASIC,
    VIP
}
  • MemberRepository.interface
package hello.core.member;

public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId);
}
  • MemoryMemberRepository.class
    (메모리에 존재하는 MemberRepository)
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.interface
package hello.core.member;

public interface MemberService {

    void join(Member member);
    Member findMember(Long MemberId);

}
  • MemberServiceImpl.class
package hello.core.member;

public class MemberServiceImpl implements  MemberService{

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //memberRepository interface필요, but interface만 가지고 있으면 nullPointerException 터질것임.

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

    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}
  • MemberApp.class
package hello.core.member;

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("findMember = "+findMember.getName());
    }
}

Member TestCode 작성

  • 폴더가 main, test 폴더로 나뉘어져 있는데 나중에 빌드해서 나가면 main에 대한 코드만 나가고, test에 대한 코드는 빌드될 때 빠진다.

  • Test Code 작성은 이제 필수임!

  • Test 디렉토리에 member패키지 만들어서 그 안에 작성.
    - Test Code는 given, when, then으로 나눠서 작성하면 안헷갈림.

    • then의 Assertions.assertThat 부분에서 검증 이루어짐.
  • MemberServiceTest.java

package hello.core.member;

public class MemberServiceTest {
    
    MemberService memberService = new MemberServiceImpl();
    
    @Test // 이 어노테이션이 import 되어야 함.
    void join(){
        //givn - 멤버 생성
        Member member= new Member(1L, "memberA", Grade.VIP);
        
        //when - 멤버 회원가입하고, 그 멤버 찾음.
        memberService.join(member);
        Member findMember = memberService.findMember(2L);
        
        //then  //여기서 검증
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}

📌 주문과 할인 도메인 설계

  • 주문과 할인 목록
    - 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있음)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있음(미확정)
  • 주문 도메인 협력, 역할, 책임
  1. 주문 생성 : 클라이언트는 주문 생성을 요청한다.
  2. 회원 조회 : 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.
  3. 할인 적용 : 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.
  4. 주문 결과 반환 : 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.

  • 역할먼저 만들고, 그 다음에 구현을 만들었다.
  • 즉 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있다.
  • 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.
    ex) 미래에 할인 정책이 바뀌면, 할인 정책 부분만 바꿔 끼우면 됨.

  • Interface에 대한 구현체가 1개만 있으면 보통 InterfaceImpl 이라고 적는다. -> (Interface이름)+(Impl)

  • 할인 정책은 고정 할인정책과 정률 할인정책이 있음.

  • 객체 다이어그램은 클래스 다이어그램과 다르게, 실제 new로 생성해서, 동적으러 객체들의 연관관계가 맺어지는 그림이다.
  • 회원을 메모리에서 조회하고, 정액 할인 정책(고정금액)을 지원해도 주문 서비스를 변경하지 않아도 된다.
    -> 역할들의 협력 관계를 그대로 재사용 가능.
  • 회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 된다.
    -> 협력 관계를 그대로 재사용 할 수 있다.

📌 주문과 할인 도메인 실행 및 Test

  • 디렉토리 구조는 다음과 같다.

  • 먼저 Order 패키지를 생성하여 주문 관련된 파일들을 만든다.

  • OrderService의 주요 기능은 주문을 생성하는 것인데 회원ID, 상품명,상품 가격을 넘겨받아서 주문을 리턴한다.

  • Order.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 +
                '}';
    }
}
  • OrderService.interface
package hello.core.order;

public interface OrderService {
    Order createOrder(Long memberId, String itemName, int itemPrice);
    // 주문 생성할때, 회원id, 상품명, 상품 가격 넘김.
}
  • OrderServiceImpl.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 );
    }

}
  • 설계가 잘됐다고 할 수 있음.
    - 왜냐하면 orderservice 입장에서는 할인에 대해서는 모름.
    - 할인에 대해서는 discountPolicy에서 알아서 해주므로, 단일 책임 원칙을 잘 지킴.
    - 할인에 대한 변경이 필요하면 주문쪽까지 안가고, 할인쪽 부분만 수정하면 됨.

discount 패키지 생성

  • DiscountPolicy.interface
package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);
}
  • FixDiscountPolicy.class
    - 회원 등급이 VIP인 경우에만 1000원 할인
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;
        }

    }
}
  • OrderApp.class
    - 멤버 생성 후, join하고 주문 생성후 주문정보 출력 및 할인 적용된 금액 출력
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());
    }
}

Order TestCode 작성

TEST 폴더에 Order 패키지 생성하고, 그 안에 작성

  • OrderServiceTest.class
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);
    }
}
  • 테스트 여러개 한번에 실행시키려면?
    -> member 패키지와 order 패키지가 들어있는 hello.core패키지에서 실행시켜주면 됨.

좋은 웹페이지 즐겨찾기