스프링 핵심 원리 이해 1

유병익·2022년 10월 17일
0

스프링 핵심 원리

목록 보기
2/9
post-thumbnail

0. 프로젝트 생성


  • 사전 준비물
    • Java 11
    • Intelli J or Eclipse
  • **https://start.spring.io 에서 스프링 프로젝트 생성**
    • Project: Gradle Project
    • Spring Boot: 최신 버전 선택
    • Language: Java
    • Packaging: Jar
    • Java: 11
    • Project Metadata
      • groupId: hello
      • artifactId: core
    • Dependencies: 선택 X
  • build.gradle 설정
plugins {
 id 'org.springframework.boot' version '2.3.3.RELEASE'
 id 'io.spring.dependency-management' version '1.0.9.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') {
 exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
 }
}
test {
 useJUnitPlatform()
}

1. 비즈니스 요구 사항과 설계


  • 회원
    • 기능 : 가입, 조회
    • 등급 : VIP, 일반
    • DB : 미정
  • 주문 & 할인 정책
    • 상품 주문
    • 회원 등급에 따른 할인 정책 적용 → 모든 VIP는 고정 할인 적용(추후 변경 가능)
💡 정책이 결정될 때 까지 기다릴 수 없으므로, 인터페이스를 만들어 구현체를 언제든 바꿀 수 있도록 설계하자.

2. 회원 도메인 설계


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

💡 DB가 미확정인 상황이기 때문에, `인터페이스`를 통해 필요에 따라`구현체`를 바꿀 수 있도록 설계한다.

3. 회원 도메인 개발 (Codes)


3.1 회원 등급

package hello.core.member;

public enum Grade {
		 BASIC,
		 VIP
}

3.2 회원 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;
		 }
}

3.3 회원 저장소

  • 회원 저장소 Interface
    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 {
      
      		 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);
      		 }
      }
💡 데이터베이스가 아직 확정이 안되었지만, 개발은 진행해야 하니 가장 단순한, 메모리 회원 저장소를 구현해서 우선 개발을 진행하자. 💡 `HashMap` 은 `동시성 이슈`가 발생할 수 있다. 이런 경우 `ConcurrentHashMap`을 사용하자

3.4 회원 서비스

  • 회원 서비스
    • 회원 서비스 Interface

      package hello.core.member;
      
      public interface MemberService {
      	 void join(Member member);
      	 Member findMember(Long memberId);
      }
    • 회원 서비스 구현체

      package hello.core.member;
      
      public class MemberServiceImpl implements MemberService {
       
      	private final MemberRepository memberRepository = new MemoryMemberRepository();
       
      	public void join(Member member) {
      		 memberRepository.save(member);
      	}
      	
      	public Member findMember(Long memberId) {
      		 return memberRepository.findById(memberId);
      	 }
      }

3.5 회원 도메인 Test Code

  • 회원 도메인 Test Code
package hello.core.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

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

+) 회원 도메인 설계의 문제점

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

4. 주문 & 할인 도메인 설계


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

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

<aside>
💡 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다.

</aside>

<aside>
💡 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.

</aside>

5. 주문 & 할인 도메인 개발 (Codes)


5.1 할인 정책

  • 할인 정책 Interface
package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {
		 /**
		 * @return 할인 대상 금액
		 */
		 int discount(Member member, int price);
}
  • 정액 할인 구현체
    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;
    				 }
    		 }
    }

5.2 주문 Entity

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 String getItemName() {
				 return itemName;
		 }
		 public int getItemPrice() {
				 return itemPrice;
		 }
		 public int getDiscountPrice() {
				 return discountPrice;
		 }

		 @Override
		 public String toString() {
				 return "Order{" + "memberId=" + memberId +
				 ", itemName='" + itemName + '\'' +
				 ", itemPrice=" + itemPrice +
				 ", discountPrice=" + discountPrice +
				 '}';
		 }
}

5.3 주문 서비스

  • 주문 서비스 Interface
    package hello.core.order;
    
    public interface OrderService {
    		 Order createOrder(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.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);
      		 }

5.4 주문과 할인 도메인 Test Code

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;
import static org.junit.jupiter.api.Assertions.*;

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);
		 }
}
💡 주문 생성 요청이 오면, 회원 정보를 조회하고, 할인 정책을 적용한 다음 주문 객체를 생성해서 반환한다. 💡 메모리 회원 Repository 와, 고정 금액 할인 정책을 구현체로 생성한다.
profile
Backend 개발자가 되고 싶은

0개의 댓글