예제 만들기

hoonie·2023년 2월 6일
0

인프런 김영한님의 강의 '스프링 핵심 원리 - 기본편'의 강의 내용을 참고하여 작성한 글입니다.

비즈니스 요구사항과 설계

  • 회원

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

    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP 고객에 대해서는 1000원을 할인해주는 고정 금액 할인을 적용한다.(나중에 변경 될 수 있음)
    • 할인 정책은 변경가능성이 높다.

이처럼 하나의 서비스를 개발하는 과정에서는 추후에 변경에 대한 요구가 있을 수 있다. 하지만 변경이 확정되기 전까지 개발을 미루고 있을 수 는 없기 때문에 인터페이스를 만들고 구현체를 갈아끼울 수 있도록 설계를 진행해야한다.

우선은 스프링의 도움없이 순수하게 자바로만 개발을 하고 추후에 스프링으로 코드를 수정하는 방향으로 진행한다. 이를 통해 스프링의 동작방식을 직접적으로 이해할 수 있다.

회원 도메인 설계

회원 도메인 협력관계

회원 클래스 다이어그램

회원 객체 다이어그램

회원 도메인 개발

회원 엔티티

  • 회원 등급
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 findeById(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{

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

회원 도메인 실행과 테스트

회원 도메인 - 회원 가입 main

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("findMember = " + findMember.getName());
    }
}

애플리케이션 로직으로 이렇게 테스트 하는 것은 권장되는 방법이 아니므로 JUnit 테스트를 사용하자. 아래는 회원가입 테스트를 하는 코드이다. IntelliJ의 test 폴더 아래 작성하면 테스트 코드를 돌릴 수 있다.
(window 기준으로 ctrl + shift + T 단축키로 생성할 수 있음)

참고: 테스트 코드가 실행되지 않고 다음과 같은 오류가 발생한다면 이 페이지를 참고해보자.
오류메시지: Execution failed for task ':test'.
There were failing tests. See the report at: file:///{프로젝트 경로}/index.html

https://velog.io/@be_have98/IntelliJ-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-%EC%8B%A4%ED%96%89-%EC%8B%9C-Execution-failed-for-task-test.-%EC%98%A4%EB%A5%98

회원 도메인 - 회원 가입 test

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

Assertion는 org.junit.jupiter.api.Assertions에 있는 것을 사용한다.

지금까지 작성한 회원 도메인 설계의 문제점

  • 현재 사용하고 있는 MemoryMemberRepository를 다른 저장소로 변경할 때 OCP원칙을 잘 준수할 수 있을지?
  • DIP를 잘 지키고 있는지?

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

발견된 문제점과 해결방안은 주문 기능까지 완성한 후 다음 포스트에서 다룰 예정.

주문과 할인 도메인 설계

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


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; // 고정적으로 할인 해주는 금액


    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){ // enum형식은 equals가 아닌 ==로 비교한다.
            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 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 +
                '}';
    }
}

주문서비스 인터페이스

package hello.core.order;

public interface OrderService {
    Order createOrder(Long memberId, String itemName, String 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);
    }
}
  • 주문 생성 요청이 들어오면, 회원정보(등급)를 조회해서 할인정책을 적용 후 주문 객체를 생성해서 반환. 이때 MemoryMemberRepository와 FixDiscountPolicy를 구현체로 활용하여 객체를 생성한다.

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

주문과 할인 정책 실행 - main

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, "memberA", Grade.VIP);
        memberService.join(member);

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

        System.out.println("order = " + order);
    }
}
  • 회원 도메인 실행과 마찬가지로 애플리케이션 로직으로 테스트하는 것은 좋은 방법이 아니므로 JUnit테스트를 활용해보자.

주문과 할인 정책 test

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, "itmeA", 10000);

        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }

}

정리

  • member 패키지엔 Member 엔티티, MemberService 인터페이스, MemberServiceImpl 구현체를 만들었고 MemberServiceImpl은 MemoryMemberRepository를 활용하여 Member정보를 저장한다.
  • discount 패키지엔 DiscountPolicy 인터페이스(역할)와 이를 구현한 FixDiscountPolicy 구현체(구현)를 만들었다.
  • order 패키지엔 Order 엔티티, OrderService 인터페이스(역할)와 이를 구현한 OrderServiceImpl(구현)을 만들었다. OrderServiceImpl은 memberRepository와 discountPolicy를 이용하여 비즈니스 로직을 실행한다.

다음 포스트

  • 정액 할인 정책이 아닌 정률 할인 정책을 개발하고 이를 지금까지 작성한 코드에 적용할 때 발생하는 문제점과 이런 문제를 어떻게 효율적으로 해결할 수 있는지에 대해 알아볼 예정이다.(중요)
profile
사우루스 팡팡!

0개의 댓글