Spring Framework/[인프런] Spring 핵심 원리

Chapter 02. 스프링 핵심 원리 이해1 - 예제 만들기

계란💕 2022. 8. 11. 15:18

2.1 프로젝트 생성

  - Java 11설치

  - https://start.spring.io/

    -> 다음과 같이 설정한다. generate 한 다음에  압축을 풀고 open

 

  - 기본 설정인 Gradle을 아래와 같이 인텔리제이로 바꿔야 속도가 빨라진다.

 

 

 

2.2 비즈니스 요구사항과 설계

 

  회원

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

 

  주문과 할인정책

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

 

 

 

2.3 회원 도메인 설계

 

회원 도메인 협력 관계

 

 

회원 클래스 다이어그램 - 정적이다

 

 

회원 객체 다이어그램 - 동적이다.

  - 회원 서비스(MemberServiceImpl)은 메모리 회원 저장소를 바라본다.

 

 

 

 

 

2.4 회원 도메인 개발

 

  - 회원 등급을 나타내는 Enum을 만든다.

<hide/>
package hello.core.member;
public enum Grade {
    BASIC, VIP
}

 

  - 회원 entity를 만든다.

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

 

  - Repository 클래스를 만든다.

<hide/>
package hello.core.member;
public interface MemberRepository {

    void save(Member member);
    Member findById(Long memberId);
}

 

  - MemoryMemberRepository 클래스 만들기

    -> 실무에서는 동시성 이슈 때문에 HashMap이 아닌 ConcurrentHashMap을 쓴다.

<hide/>
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<>();   // <id, member> 의 형태로 매핑된다.

    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }
}

  cf) ConcurrentHashMap를 쓰는 이유와 HashMap과의 차이점   

    - ConcurrentHashMap는 내부적 동기화 때문에 Thread safe, 수정 작업만 동기화 된다. 

    - HashMap은 내부적으로 동기화되지 않고 스레드로부터 안전하지 않다. 단일 스레드 프로그램에 적합하다. 

 

  - MemberService 인터페이스 만들기

<hide/>
package hello.core.member;
public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId);
}

 

  - MemberServiceImpl클래스

    -> long으로 만드는 이유는 회원 마다 고유번호를 부여해서 관리하기 위해서이다.

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

 

 

 

2.5 회원 도메인 실행과 테스트

 

   Ex) 순수 자바로 이뤄진 프로젝트

<hide/>
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());
        
    }
}

    - MemberApp 클래스 만든다.

 

  Note) 실행 결과

    - 처음에 강사님과 다르게 getter, setter를 @lombok으로 줬더니 오류가 났다. 다시 순수 자바 모드로 getter, setter를 주니까 해결완료

 

 

  cf) JUnit(제이유닛)

    - 자바 프로그래밍 언어용 유닛 테스트 프레임워크를 말한다.

 

 

  Ex)

    - MemberServiceTest 클래스를 만든다.

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

 

 

  회원 도메인 설계의 문제점은?

  • 의존 관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.
  • 주문 까지 만들고나서 문제점과 해결 방안을 설명한다.
  • MemberServiceImpl 클래스를 보면 MemoryRepository와 MemoryMemberRepository 두 군데에 의존한다는 특징이있다. 추상화에 의존하고 구체화에도 의존한다. => 'DIP-의존성 역전 원칙'을 위반한다.

 

 

2.6 주문과 할인 도메인 설계

주문 도메인 전체

 

 

 

2.7 주문과 할인 도메인 개발

 

  Ex) 

    - DiscountPolicy 클래스 만든다.

<hide/>
package hello.core.discount;
import hello.core.member.Member;
public interface DiscountPolicy {
    /**
     *
     * @return 할인 대상 금액
     */

    public int discount(Member member , int price);
}

 

    - 정액 할인 정책 클래스 FixDiscountPolicy  

      -> Enum 타입은 "==" 사용 가능하다.

<hide/>
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) {

        if(member.getGrade() == Grade.VIP){
            return discountFixAmount;
        }
        return 0;
    }
}

 

    - Order 클래스 만든다.

<hide/>
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 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;
    }
    
    public int calculatePrice(){    // 계산된 결과
        return  itemPrice - discountPrice;
    }
    
    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

 

    - OrderServiceImpl 클래스 만든다.

      -> 주문 만들어주는 메서드 createOrder()

      -> 할인과 주문에 대한 책임이 구분되므로 SRP(단일 책임 원칙)을 잘 지켰다는 것을 알 수 있다.

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

 

 

 

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

 

  Ex) main 메서드로 테스트

<hide/>
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, "itemA", 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());
    }
}

  Note) 실행 결과

 

 

  Ex) JUnit으로 자동화된 테스트를 위해 (OrderServiceTest 클래스)를 만든다.

<hide/>
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(){
        // given
        Long memberId = null;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

  Note) 실행 결과

 

 

 

 

출처 https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com