쌩로그
스프링 핵심원리 기본편 - Ch02. 스프링 핵심 원리 이해1 - 예제 만들기 본문
목록
- 포스팅 개요
- 본론
2-1. 비즈니스 요구사항과 설계
2-2. 회원 도메인 설계
2-3. 회원 도메인 개발
2-4. 회원 도메인 실행과 테스트
2-5. 주문과 할인 도메인 설계
2-6. 주문과 할인 도메인 개발
2-7. 주문과 할인 도메인 실행과 테스트 - 요약
1. 포스팅 개요
인프런에서 영한님의 스프링 핵심 원리 기본편 Section02 스프링 핵심 원리 이해1 - 예제 만들기를 학습하며 정리한 포스팅이다.
| 참고 이전 포스팅
2. 본론
2-1. 비즈니스 요구사항과 설계
요구 사항은 이와 같다.
미확정 혹은 나중에 변경되는 부분은 인터페이스로 역할만 정해놔야 한다는 느낌을 받을 수 있다.
2-2. 회원 도메인 설계
회원 요구사항에 따라 다음과 같이 설계할 수 있다.
도메인 협력 관계는 기획자들도 볼 수 있는 그림이다.
이를 이용해서 개발자가 구체화해서 클래스 다이어그램을 만들어낸다.
클래스 다이어그램은 서버를 실행시키지 않고 클래스만 분석해서 볼 수 있는 것이다.
저장을 메모리에 할지 DB에 할지는 동적으로 결정된다.
즉 서버가 실행될 때 결정하기 때문에 클래스 다이어그램만으론 판단하기 어렵다. 이 때 객체 다이어그램을 활용한다.
객체 다이어그램은 클라이언트가 실제 사용하는 인스턴스끼리의 참조를 보여준다.
2-3. 회원 도메인 개발
member 패키지
Member
public class Member {
private Long id;
private String name;
private Grade grade;
// 생성자...
// getter, setter 등
}
public enum Grade {
BASIC,
VIP
}
MemberRepository(인터페이스) + 구현체
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
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);
}
}
MemberService(인터페이스) + 구현체
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
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-4. 회원 도메인 실행과 테스트
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("mew member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
위의 코드를 실행하면, 결과는 다음과 같다.
눈으로 직접 확인해야하는 단점이 있다.
따라서 테스트 디렉터리를 적극 활용하자.
다음과 같이 작성해서 확인하면 된다.
다음은 실행결과다.
만약 MemberId에 1을 줬지만, 2L로 찾은 member 와 비교를 한다면,
코드는 다음과 같다.
// given
Member member = new Member(1L, "memberA", Grade.VIP);
// when
memberService.join(member);
Member findMember = memberService.findMember(2L);
테스트 결과는 다음과 같다.
눈으로 확인할 필요없이 IDE가 알아서 검증해준다.
사실 중요한 것은 회원 도메인 설계의 문제점이다.
다른 저장소로 변경할 때 OCP 원칙을 잘 준수할수 있을까?
DIP를 잘 지키고 있을까?
의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.
주문까지 만들고 문제점과 해결 방안을 설명한다.
2-5. 주문과 할인 도메인 설계
주문과 할인 정책은 다음과 같다.
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수
있다.) - 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
설계는 다음과 같다.
- 클라이언트가 주문 서비스에 주문 생성을 요청한다.
- 그러면 할인을 위해서 주문 서비스는 회원 저장소에서 회원을 조회한다.
- 등급에 따라 할인 여부를 할인정책에 위임한다.
- 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.
참고: 실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해 질 수 있어서 생략하고, 단순히 주문
결과를 반환한다고 하신다.
위 그림은 단순히 역할에 대한 그림이지만,
다음 그림은 역할의 구현까지 그린 그림이다.
역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다.
덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다
다음은 클래스 다이어그림이다.
객체 다이어그램으로는 다음과 같이 그릴 수 있다.
회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원하거나,
회원을메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문
서비스를 변경하지 않아도 된다.
협력 관계를 그대로 재사용 할 수 있다.
2-6. 주문과 할인 도메인 개발
할인 정책 인터페이스이다.
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
할인 정책 인터페이스를 구현한 정액 할인 클래스이다.
public class FixDicountPolicy 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;
// .. 생성자
// .. Getter. Sett
public int calculatePrice() {
return itemPrice - discountPrice;
}
주문 서비스 인터페이스이다.
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
주문 서비스 인터페이스를 구현한 구현체다.
public class OrderServiceImple implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDicountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member findMember = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(findMember, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
참고로 잘 설계된 구조이다.
만약 할인정책을 주문에서 같이 관리하면, 할인 정잭이 변경될 때 주문에서도 변경해야한다.
2-7. 주문과 할인 도메인 실행과 테스트
public class OrderApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImple();
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);
System.out.println("order.calculatePrice() = " + order.calculatePrice());
}
}
- Member를 생성해서 회원가입을 시킨다.
- orderService의 createOrder 메서드를 이용해서 내부적으로 할인 정책까지 적용되어 주문이 생성된다.
결과는 다음과 같다.
order = Order{memberId=1, itemName='itemA', itemPrice=10000, discountPrice=1000}
order.calculatePrice() = 9000
이를 테스트로 옮겨보면, 다음과 같이 작성할 수 있다.
결과는 다음과 같다.
참고로 테스트에 사용되는 Assertions는 junit이 아닌 assertj의 라이브러리에서 import 해야한다.
다형성을 잘 활용했는데, 과연 정액 할인이 아니라, 정률 할인으로 잘 바꿀 수 있는지, 다음 Section을 통해서 확인할 수 있다.
3. 요약
비즈니스 요구사항에 따라 순수 자바 코드만으로 요구사항에 대한 설계와 구현을 해보았다.
테스트는 덤이다.
다음 섹션은 문제를 발견하고 해결하면서 리팩터링하실 거 같다.