[Spring] #2. 스프링 핵심 원리 이해1-예제만들기 (김영한_인프런_스프링핵심원리)

bien·2023년 4월 9일
0

Spring_basic

목록 보기
2/9

프로젝트 생성

start.spring.io에서 프로젝트를 생성한다. Spring Boot 3.0.0 version부터는 java 17이 필요하다. java 11을 사용할 경우 2.대 version을 사용해야 한다.

그러나 2.대 프로젝트를 생성했음에도 온갖 오류가 다 떴다..
재시도 할 때마다 오류 메시지가 달라 검색을 반복했지만 원인을 찾지 못했다.

‘스트링 입문’ 강의를 들으면서 만든 프로젝트는 빌드에 문제가 없었기 때문에, 해당 프로젝트의 build파일과 차이점을 비교해보니 version이 달랐다. 2.7.10 version을 2.7.9로 수정한 것 만으로 빌드가 정상적으로 이루어졌다.

Settings - Gradle (검색 통해 접근 가능)

  • Build and run using, Run tests using: intellij IDEA로 변경

비즈니스 요구사항과 설계

회원

  • 회원가입, 조회가 가능.
  • 두가지 등급: 일반 & vip
  • 자체 DB를 구축할 수도 있고, 외부 시스템과 연동할 수도 있다.(미확정)

주문과 할인 정책

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책: vip는 1000원 할인하는 고정 금액 할인 (변경 가능)
  • 할인 정책 변경 가능성 높음. 최악의 경우 할인 적용 안 할수도.

=> 회원 데이터, 할인 정책: 지금 결정이 어려움
따라서, 인터페이스로 구현체를 갈아끼울 수 있도록 설계한다!

+) 지금은 순수한 자바로만 개발을 진행한다!!


회원 도메인 설계

< 회원도메인 요구 사항 >

  • 회원을 가입하고 조회할 수 있다.
  • 회원: 2가지 등급= 일반, VIP
  • 자체 DB를 구축 하거나 외부 시스템과 연동 예정(미확정)

회원 도메인 협력 관계

회원 클래스 다이어그램


MemberServiceImpl: 구현체
MemberRepository: 회원 저장소.
MemoryMemberRepository, DbMemberRepository: 구현체

회원 객체 다이어그램

  • 실제 객체간의 참조. 클라이언트 > 회원 서비스 > 메모리 회원 저장소

도메인 협력 관계 : 기획자와 공유
클래스 다이어그램: 개발자가 대략적으로 구현. 실제 서버 개발 전 클래스 확인.
객체 다이어그램: 동적으로 결정되는 요소들. 서버가 뜰 때 new해서 뭘 넣을지가 결정되는 것들. 프로그램 실행 후 클라이언트가 사용하는 실제 객체(인스턴스)끼리의 참조를 그린 것. 클래스 다이어그램만으로는 이것들을 알기 힘들다.


회원 도메인 개발

setup-keymap에서 단축키 찾을 수 있음.

인터페이스 구현체가 한개만 있으면, 인터페이스명 뒤에 Impl이라고 붙여서 관례상 사용하곤 한다.

회원 엔티티

회원 등급

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{

    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을 사용하자.

회원 서비스

회원 서비스 인터페이스

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();
    // ctrl+shift+spacebar

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

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

회원 도메인 실행과 테스트

📢 회원 도메인 설계의 문제점

  • 이 코드의 설계상 문제점은 무엇일까요?
  • 다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까요?
  • DIP를 잘 지키고 있을까요?
    • OCP 개방-폐쇄 원칙. Open/ Closed Prnciple
      : 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.
    • DIP 의존관계 역전 원칙 Dependency Inversion Principle
      : 추상화에 의존해야지 구체화에 의존하면 안된다.
      = 클래스에 의존하지 말고, 인터페이스에 의존하라.

✔️ private final MemberRepository memberRepository = new MemoryMemberRepository();
💬 (내 생각)
DIP: 클래스랑 인터페이스를 동시에 의존하고 있어 DIP를 준수하지 못함
OCP: 각 클래스가 개별 변수를 private final로 설정하여 외부의 변경은 예방된다. 하지만 확장에 열려있지는 못한것 같다. 솔직히 확장에 열려있다는 말이 잘 이해가 안된다.. 수업을 더 들어봐야 할 것 같다.

-> 주문까지 만들고나서 문제점과 해결 방안을 설명

package hello.core.member;

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

class MemberServiceImplTest {
    MemberService memberService = new MemberServiceImpl();
    // 인터페이스와 클래스 모두에게 의존하고 있음.
    // 추성화, 구체화 모두에 의존하고 있음.

    @Test
    void join() {
        // given
        Member member = new Member(1l, "memberA", Grade.BASIC);

        // when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        // then (검증 부분만 위치하도록 해야)
        Assertions.assertThat(findMember).isEqualTo(member);

    }

}

주문과 할인 도메인 설계

주문과 할인 정책

  • 회원은 상품 주문 가능
  • 회원 등급에 따른 할인 정책
  • 할인 정책 = 고정금액할인: 모든 VIP는 1000원 할인 (변경 가능)
  • 할인 정책 변경 가능. 적용 안할수도.

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

클라이언트: main코드, Controller

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

  • 참고: 실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해질 수 있어 생략하고, 단순히 주문결과를 반환한다.

주문 도메인 전체


역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다. 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.

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

객체 다이어그램

1번

회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 된다. 역할들의 협력관계를 그대로 재사용할 수 있다.

2번

회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주분 서비스를 변경하지 않아도 된다. 협력 관계를 그대로 재사용할 수 있다.

주문과 할인 도메인 개발

할인정책 인터페이스

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) {
            // ENUM은 ==으로 비교
            return discountFixAmount;
        } else {
            return 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 calculaterPrice() {
        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 +
                '}';
    }
}

주문 서비스 인터페이스

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

주문생성요청이 오면, 회원정보를조회하고, 할인정책을 적용한 다음 주문 객체를 생성해서 반환한다.
메모리회원 리포지토리와, 고정금액할인 정책을 구현체로 생성한다.


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

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 hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
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 crateOrder() {
        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);
    }

}
profile
Good Luck!

0개의 댓글