-
[Spring] DI 컨테이너 예제 만들기 ( 1 ), JUnit 사용하기Back-end/Spring 2022. 3. 3. 14:45
안녕하세요 이번 포스팅은 DI 컨테이너를 활용해서 OCP와 DIP를 준수하면 개발하는 예제와 JUnit 사용에 대해서 알아보겠습니다!!
회원에 관한 인터페이스와 클래스는 위와 같이 생성해보겠습니다.
우선 MemberRepository는 회원가입과, 조회의 기능을 갖고 있는 인터페이스이며 인터페이스를 구현하는 클래스는 MemoryMemberRepository와 DBMemberRepository 이렇게 2개가 있다고 가정합니다.
MemoryMemberRepo는 Member 객체를 생성해서 HashMap에다가 저장하고 찾는 방식이고 DBMemberRepo는 말 그대로 DB에 저장하고 찾는 방식입니다.
우선 다음의 enum과 클래스들을 생성해보겠습니다.
//회원등급 public enum Grade { BASIC, VIP }
//회원 엔티티 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; } }
//회원 저장소 인터페이스 public interface MemberRepository { void save(Member member); Member findById(Long memberId); }
//메모리 회원 저장소 구현체 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); } }
//회원서비스 인터페이스 public interface MemberService { void join(Member member); Member findMember(Long memberId); }
//회원 서비스 인터페이스 구현체 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); } }
위의 클래스와 인터페이스는 아주 단순한 구조로 이뤄져 있어서 이해하는데 어렵지 않습니다.
테스트를 해보기 위해서는 보통 main 메서드를 만들어서 주로 실행을 많이 해보는데요 좋은 방법이 아니라고 합니다. 그래서 JUnit 테스트를 한번 사용해보겠습니다.
Intelli J 기준으로 Ctrl + Shift + T를 누르면 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); } }
메서드에 @Test를 붙이면 테스트를 실행할 수 있고 Assertions.assertThat()이라는 메서드를 통해서 두 개의 인스턴스 혹은 값을 비교할 수 있습니다.
isEqualTo는 자바의 equal 메서드와 비슷하고 sameAs라는 메서드는 '==' 연산자와 비슷합니다.
위의 코드에서는 member와 findMember가 동일 인스턴스인지 비교하는데 실행을 시키면 초록불이 뜨면서 Test 성공으로 표시됩니다.
하지만, 위의 코드는 DIP를 지키고 있지 않기 때문에 OCP를 위반할 확률이 높습니다. 의존관계가 인터페이스뿐만 아니라 구현체까지 모두 의존하는 문제점을 가지고 있죠.
왜냐면 MemberServiceImpl을 보면 인터페이스뿐만 아니라 MemoryMemberRepository 클래스까지 의존하고 있기 때문입니다.
전체적인 클래스 다이어그램을 보면 다음과 같습니다.
DiscountPolicy는 할인 정책 인터페이스이고 구현체는 정액 할인인 FixDiscountPolicy, 정률 할인 정책인 RateDiscountPolicy가 있습니다.
정액은 회원이 VIP 등급이면 1000원을 할인해 주고 정률은 10000원당 10%를 할인해주는 정책입니다.
그리고 OrderServiceImpl은 DiscountPolicy와 MemberRepository를 의존하고 있습니다.
그럼 할인 정책 인터페이스와 구현체, 주문 엔티티를 만들어보겠습니다.
public interface DiscountPolicy { /** * @return 할인 대상 금액 */ int discount(Member member, int price); }
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; } } }
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 + '}'; } }
//주문 서비스 인터페이스 public interface OrderService { Order createOrder(Long memberId, String itemName, int itemPrice); }
//주문 서비스 구현체 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); } }
주문 생성 요청이 오면, 회원 정보를 조회하고, 할인 정책을 적용한 다음 주문 객체를 생성해서 반환합니다. 메모리 회원 리포지토리와, 고정 금액 할인 정책을 구현체로 생성합니다.
그럼 이제 원하는 로직대로 실행이 되는지 JUnit 테스트를 만들어서 실행해봅시다.
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); } }
위의 테스트를 실행시키면 테스트가 성공한 것을 확인할 수 있습니다.
다음 포스팅에서는 현재 정액 할인 정책으로 되어있는 시스템을 정률 할인 정책으로 바꿔보는 것을 해보면서 현재 코드가 왜 개방 폐쇄 원칙과 의존성 주입 원칙에 위배되는지 확인해보겠습니다.
'Back-end > Spring' 카테고리의 다른 글
[Spring]IoC, DI, 컨테이너 그리고 AppConfig 스프링으로 변환하기, 스프링 빈 조회하기 (0) 2022.03.04 [Spring] DI 컨테이너를 예제 만들기 ( 2 ) (0) 2022.03.04 [Spring] 좋은 객체 지향 설계의 5가지 원칙(SOLID) (0) 2022.03.02 [Spring] Spring의 핵심과 객체 지향 프로그래밍 (0) 2022.03.02 [Spring] Spring의 역사에 대해서 알아보자 (1) 2022.03.02