8 min read

<Spring> 스프링 핵심원리 이해 1 - 순수 자바코드로 예제 만들기

<Spring> 스프링 핵심원리 이해 1 - 순수 자바코드로 예제 만들기

프로젝트 생성

build.gradle 설정

plugins {
	id 'org.springframework.boot' version '2.5.2'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
	useJUnitPlatform()
}

비지니스 요구사항과 설계

회원

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

주문과 할인 정책

  • 회원을 상품을 주문 할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용 할 수 있다.
  • 할인 정책은 고정금액할인(1000원)과 비율할인정책으로 나누어 지고 결정되지 않았다.

회원 도메인 설계

회원 도메인 요구사항

  • 회원을 가입하고 조회할 수 있다.
  • 회원은 일반회원과 VIP회원이 있다.
  • 회원 데이터는 자체 DB를 가질 수도, 외부 시스템과 연동 할 수도 있다.
회원 도메인 협력 관계
회원 도메인 협력 관계
회원 클래스 다이어그램
회원 클래스 다이어그램
회원 객체 다이어그램
회원 객체 다이어그램
  • 회원 서비스 구현체 : MemberServiceImpl

회원 도메인 개발

회원 등급

package hello.core.member;

public enum Grade {
    BASIC
    ,VIP
}

회원 엔티티

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

회원 저장소 인터페이스

package hello.core.member;

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);

}

메모리 회원 저장소 구현체

package hello.core.member;

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

public class MemoryMemberRepository implements MemberRepository {

    //동시성 이슈 해결하려면? ConcurrentHashMap 사용!
    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);
    }
}

회원 서비스 인터페이스

package hello.core.member;

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

회원 서비스 구현체

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    //선언하는 곳은 추상화에 의존, 실제 할당하는 곳이 구현체에 의존하기 때문에 DIP를 위반
    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);
    }

}

회원 서비스 테스트 코드 작성

  • test 디렉토리에 아래 패키지에 MemberServiceTest 클래스 생성
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, "brido", Grade.VIP);
        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);
        //then
        Assertions.assertThat(member).isEqualTo(findMember);
    }
}

  • 현재까지의 코드는 의존관계가 인터페이스뿐만 아니라 구현체까지 의존하는 문제를 가지고 있다.private final MemberRepository memberRepository = new MemoryMemberRepository(); 에서 나타나는 문제점이다.

주문과 할인 도메인 설계

주문과 할인 정책

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책은 VIP회원은 1000원 할인을 해주는 고정할인정책 적용
  • 하지만 비율할인정책 또한 염두에 두고 있어서 할인 정책이 변경 가능성이 높다.
주문 도메인 협력,역할,책임
주문 도메인 협력,역할,책임
  1. 주문생성 : 클라이언트는 주문 서비스에 주문생성을 요청한다.
  2. 회원조회 : 할인을 위해서는 회원등급이 필요하다.그래서 주문 서비스는 회원저장소에서 회원등급을 조회한다.
  3. 할인적용 : 주문 서비스는 회원 등급에 따른 할인여부를 할인 정책에게 위임한다.
  4. 주문결과반환 : 주문 서비스는 할인 결과를 포함한 주문결과를 반환한다.
주문 도메인 전체 이미지
주문 도메인 전체 이미지
  • 역할과 구현을 분리해서 자유롭게 구현 객체를 조립 할 수 있게 설계했다.
  • 덕분에 회원 저장소는 물론, 할인 정책도 유연하게 변경할 수 있다.
주문 도메인 클래스 다이어그램
주문 도메인 클래스 다이어그램
주문 도메인 객체 다이어그램 1
주문 도메인 객체 다이어그램 1
주문 도메인 객체 다이어그램2
주문 도메인 객체 다이어그램2

주문과 할인 도메인 개발

할인 정책 인터페이스

  • discount 패키지 생성 후, DiscountPolicy interface를 생성
package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {
    /*
    * @return 할인 대상 금액
    *  */
    int discount(Member member, int price);
}

정액 할인 구현체(고정할인)

  • discount 패키지 아래에 FixDiscountPolicy 클래스를 생성
package hello.core.discount;

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

public class FixDiscountPolicy implements  DiscountPolicy{

    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {

        // enum 은 == 사용해야한다
        if (member.getGrade() == Grade.VIP) {
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}
  • VIP는 1000원 할인적용

주문 엔티티

  • order 패키지 생성 후, Order 클래스 생성
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 +
                '}';
    }
}

주문 서비스 인터페이스

  • order 패키지 아래에 OrderService 인터페이스 생성
package hello.core.order;

import hello.core.member.Member;

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

주문 서비스 구현체

  • order 패키지 아래에 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);
    }
}
  • 주문 생성 요청이 오면 회원정보를 조회하고, 할인 정책을 적용시킨 뒤 주문 객체를 생성해서 반환한다.
  • 메모리 회원 레포지토리와 고정 할인 금액 정책을 구현체로 생성한다.

주문과 할인 정책 테스트 코드 구현

  • test 디렉토리에 order패키지 생성후, OrderServiceTest 클래스 생성
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, "brdio", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "item1", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}
해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.