framework/spring

스프링 핵심 원리 이해 1 - 예제 만들기(회원)

wooweee 2023. 4. 8. 02:13
728x90

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

 

예제. 김영한의 스프링 핵심 기본 원리 pdf

 

 

2. 설계 - 회원 도메인

 

  • 2가지의 도메인이 존재
    1. 회원
    2. 주문
  • 우선 회원 도메인 먼저 설계

  • 회원 가입 목록 -> 회원서비스(가입, 조회) -> 회원저장소(메모리저장소, DB저장소, 외부 시스템 연동 회원 저장소)
      Member              service                            repository

 

비개발자도 알아볼 수 있도록 한 설계도

 

class 그자체 코드를 돌려보지 않아도 알 수 있는 정적인 설계도

 

실제 구현체를 다 넣어서 확인하는 동적인 설계도

 

2.1. 회원 도메인 개발

 

1. 회원 등급

public enum Grade {
    BASIC,
    VIP
}


2. 회원

public class Member {

    private  Long id;
    private String name;
    private 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; }

    public Member(Long id, String name, Grade grade) {
        this.id = id;
        this.name = name;
        this.grade = grade;
    }
}

 

3. 회원 저장소 (인터페이스)

public interface MemberRepository {
    void save(Member member);
    Member findById(Long memberId); }

 

3-1. 회원저장소 (구현체)

public class MemoryMemberRepository implements MemberRepository {
    
    // 임시 저장소 (db와 동일한 역할): store
    private static Map<Long, Member> store = new HashMap<>(); // 동시성 이슈 때문에 concurrentHashMap을 실무에서는 사용
    
    @Override
    public void save(Member member) {
    store.put(member.getId(), member);
    }
    
    @Override
    public Member findById(Long memberId) {
    return store.get(memberId);
    }
}

 

4. 회원 서비스(인터페이스)

public interface MemberService {
    void join(Member member);
    Member findMember(Long memberId); }
}

 

4-1. 회원 서비스(구현체)

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    // private final: 다른 MemberReposotory의 객체로 바뀌는 것을 방지
    // 변수는 interface에 의존, 객체는 구현체에 의존. DIP와 OCP에 위반
    
    public void join(Member member) {
    	memberRepository.save(member);
    }
    public Member findMember(Long memberId) {
    	return memberRepository.findById(memberId);
    }
}

 

2.2. 회원 도메인 TEST

 

1. java로만 test

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); // HashMap에서 valuer인 Member object를 반환
    System.out.println("new member = " + member.getName());
    System.out.println("find Member = " + findMember.getName()); // findMember는 Member 객체를 반환
    }
}

 

2. JUnit5 test

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); // 객체 주소로 비교
    }
}

 

3.설계 - 주문과 할인 도메인

 

예제. 김영한의 스프링 핵심 기본 원리 pdf

 

  • 2가지 도메인 존재
    1. 회원
    2. 주문
  • 두번째인 주문 도메인을 설계
    - 회원의 등급에 따른 할인 정책을 생각

  • 주문     ->   주문 서비스(등급 할인)  ->   1) 회원저장소(등급 찾기)  ->  2) 할인 적용(할인 정책)   ->   할인된 주문 반환
     domain              service                                repository                            repository                        return

 

  • 원래는 주문 데이터를 DB에 저장해야되지만 복잡하므로 주문 결과를 반환한다.

 

비개발자도 알아볼 수 있도록 한 설계도

 

class 그자체 코드를 돌려보지 않아도 알 수 있는 정적인 설계도

 

 

class 그자체 코드를 돌려보지 않아도 알 수 있는 정적인 설계도

 

실제 구현처를 다 넣어서 확인하는 동적인 설계도

 

  • 목표하는 설계
    • 회원 조회 방식, 할인 정책이 변경이 되어도 주문 서비스내부 코드 변경 필요없이 repository, discount interface 구현 객체만 변경하면 된다. 
    • 역할들의 협력 관계를 그대로 재사용 할 수 있다.
    •    1. 할인 정책(인터페이스)

 

 

 

3.1. 주문 도메인 개발 with 할인

 

1. 할인 정책(인터페이스) 

public interface DiscountPolicy{
    int discount(Member memeber, int price);
}

 

1-1. 정액 할인 정책(구현체)

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

 

2. 주문 엔티티(주문 틀)

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

 

3. 주문 서비스 (인터페이스)

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

 

3.1. 주문 서비스 (구현체)

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 = memberRepostotory.findById(memberId);
        int discountPrice = discountPolicy.discount(memeber, itemPrice);
        
        return new Order(memeberId, itemName, discountPrice);
    }
}

 

3.2. 주문 도메인 TEST

 

1. java로만 test

public class OrderApp {
	public static void main(String[] args ) {
        MemberService memberSrvice = new MeberServiceImpl();
        OrderService orderService = new OrderServiceImpl();

        Long memberId = 1L; 
        // 객체 생성 단계에서 null이 들어갈 수 있어서 long이 아니라 Long 사용
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);

        System.out.println("order = " + order); 
    }
}

 

2. JUnit5 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);
        asserThat(order.getDiscountPrice()).isEqualTo(1000);
    }
    
    @Test
    @DisplayName("VIP가 아니면 1000원 할인 못받음") // test code 이름 설정
    void createOrder_BASIC(){
    	//given
        Member member = new Member(2L, "memberBASIC", Grade.BASIC);
        //when
        int discount = discountPlicy.discount(member, 10000);
        //then
        asserThat(discount).isEqualTo(0);
    }
}

 

  • 이런 식으로 단위 test를 잘 만드는 것이 중요 - spring에 의존하지 않고 java로만 구현한 Test. 속도가 엄청 빠르다.

 

 

4. 현재 개발의 문제점

 

  • 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점
  • 정률 할인 정책 코드를 만들어 보면서 어떻게 의존 되는지 확인 

 

1-2. 정액 할인 정책(구현체)

public class RateDiscountPolicy implements DiscountPolicy{
	
    private int discountPercent = 10;
    
    @Override
    public int discount(Member member, int price){
    
    	if(member.getGrade() == Grade.VIP) {
        	return price * discountPercent / 100;
        } else{
        	return 0;
        }

	}
}
더보기

1-1. 정액 할인 정책(구현체)

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

 

 

3-1. 주문 서비스(구현체) - 변경

다형성을 최대한 구현 했지만 코드의 의존성 문제로 인해서 source code를 건들이는 문제 발생 - DIP, OCP 위반

public class OrderServiceImpl implements OrderService {
	
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    // 변경 부분
    // private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    
    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice){
    
    	Member member = memberRepostotory.findById(memberId);
        int discountPrice = discountPolicy.discount(memeber, itemPrice);
        
        return new Order(memeberId, itemName, discountPrice);
더보기

3-1. 주문 서비스(구현체)

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 = memberRepostotory.findById(memberId);
        int discountPrice = discountPolicy.discount(memeber, itemPrice);
        
        return new Order(memeberId, itemName, discountPrice);
    }
}

 

4.1. 위반 이유 - 설계도로 보기

OCP 위반

 

4.2. 해결 방안

  • 인터페이스에만 의존하도록 설계코드 변경

 

 

5. 추가 사항

  • domain
    • 객체 의미
    • service, repository, service에 주입되는 다른 추가 class 들의 작업에 핵심 data를 가진 객체
  • domain같은 경우는 client마다 새로운 객체를 사용하는 것 같다.
    • springContainer에 등록을 하지 않고 사용
    • 근거
      1. domain 객체는 상황에 따라서 공유하는 것도 있고 계속 생성해야하는 것도 존재하는 것 같다.
      2. member 같은 경우 사람마다 정보가 다르니깐 당연히 따로 써야한다.
      3. Order 같은 경우도 어떻게 보면 주문 후 없애버릴 객체지만 주문 동안에는 각각의 주문객체가 존재 해야한다.
      4. 뭐 상황에 따라서 각각의 객체가 필요없는 도메인도 있을 수 있겠지 않을까 싶다.

  •  왜 service 다음에는 무조건 repository이고 method도 비슷한데 둘을 분리하지?
    • repository에 저장되기전 필터링하는 역할도 수행, 잘못된 정보가 오면 거절
    • 항상 repository가 오는 것이 아니다.
      • controller로 부터 domain 객체를 받아서 해당 서비스가 db에 회원 정보 저장이다 하면 해당 도메인 객체를 repository로 이동 시키고 해당 서비스가 할인 이다 하면은 할인 정책에 관련된 작업을 수행
    • 결론
      • service에 따라서 service 다음 수행되는  무언가의 class는 repository가 될수도 있고 discount(repository가 아닌 작업 수행 class)가 될수 도 있는 것이다.
      • repository는 거의 필수로 사용되니깐 spring에서 @Repository 라는 어노테이션을 만들어준거고 할인 정책 같은 경우는 @Component로 spring Container에 등록해서 사용
  • 역할 구분
    • 역할이 다른 service, controller는 각자의 역할에 맞게 class를 나누어 줘야한다.
    • repository, discount는 도메인을 활용한 최종 작업이라 보면되는데 상식적으로 분리된 class를 만드는 것을 안다.

 

 

 

이전 발행글 : 스프링 핵심 원리 이해 0 - 객체지향 설계와 스프링

 

다음 발행글 : 스프링 핵심 원리 이해 2 - 객체 지향 원리 적용

 

 

 

 


출처: 인프런 스프링 핵심 원리 - 기본편