- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
2. 주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)
요구사항을 보면 회원 데이터, 할인 정책 같은 부분은 지금 결정하기 어려운 부분이다.
하지만 우리는객체 지향 설계 방법에 따라인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계하면 된다.
프로젝트 환경설정을 편리하게 하려고 스프링 부트를 사용한 것이다.
지금은스프링 없는 순수한 자바로만 개발을 진행
2) 회원 도메인 설계
이제 요구사항을 바탕으로 회원 도메인 설계를 진행해보자.
회원 도메인 협력 관계
클라이언트는 회원 서비스를 호출한다.
회원 서비스는 두 가지 기능(회원가입, 회원조회)을 제공한다.
회원 저장소는 자체 DB를 사용할 수도 있고, 외부 시스템과 연동할수도 있기에 별도로 구축한다. (= 회원 데이터에 접근하는 계층을 따로 만들어서 역할과 구현을 분리한다. 나중에 저장소가 정해지면, 그 구현체만 개발해서 교체한다.)
회원 클래스 다이어그램
MemberService(회원 서비스)라는 역할을 인터페이스로 만들고, 그 구현체로 MemberServiceImpl를 생성한다.
MemberRepository(회원 저장소)라는 역할을 인터페이스로 만들고, 그 구현체로 MemoryMemberRepository 클래스와 DbMemberRepository 클래스를 생성한다.
회원 객체 다이어그램
클라이언트는 회원 서비스(MemberServiveImpl)를 바라보고,
회원 서비스(MemberServiveImpl)은 메모리 회원 저장소(MemoryMemberRepository)를 바라본다.
3) 회원 도메인 개발
이제 본격적으로 회원 도메인을 개발해보자.
(member 패키지 아래) 회원 등급으로, Grade enum을 생성한다.
(member 패키지 아래) 회원 엔티티 클래스를 생성한다.
회원의 속성은 id, name, grade로 하자.
생성자와 getter/setter를 생성하자.
(member 패키지 아래) 회원 저장소 인터페이스를 생성한다.
(member 패키지 아래) 회원 저장소에 대한 메모리 저장소 구현체 클래스를 생성한다.
(member 패키지 아래) 회원 서비스 인터페이스를 생성한다.
(member 패키지 아래) 회원 서비스에 대한 구현체 클래스를 생성한다.
(이렇게 하면) 회원 서비스의 join 메소드를 호출해서 save를 호출하면, 다형성에 의해서 MemberRepository 인터페이스가 아닌, MemoryMemberRepository에 있는 오버라이드한 save가 호출된다.
4) 회원 도메인 실행과 테스트
정상적으로 동작하는지 확인해보자.
core 패키지 하위에 MemberApp 클래스를 생성하고, 다음과 같이 입력 후 실행해보자.
정상적으로 실행된 것을 볼 수 있다.
✔그런데 이 회원 도메인 설계에는 문제점이 있다.
다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까?
DIP를 잘 지키고 있을까 ?
의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.
MemberServiceImpl은 MemberRepository 인터페이스를 의존하지만, 실제 할당하는 부분에서 구현체(MemoryMemberRepository)도 의존하고 있다. (=추상화에도 의존하고, 구현체에도 의존하고 있음.) 따라서 나중에 변경이 있을 때 문제가 될 수 있다.
5) 주문과 할인 도메인 설계
이번에는 요구사항을 바탕으로 주문과 할인 도메인을 설계해보자.
주문 도메인의 협력, 역할, 책임
1. 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다. (참고) 예제를 간단히 하기위해 상품에 대한 것은 별도로 만들지 않았다. 따라서 상품 정보인 상품명과 상품 가격은 데이터로 넘긴다.
2. 회원 조회: 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.
3. 할인 적용: 주문 서비스는 회원 등급에 따른 할인 가능 여부를 할인 정책에 위임한다. (할인 가능하면 할인해서 결과 알려달라.)
4. 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다. (참고) 실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해질 수 있어서 생략하고, 주문 서비스에서 단순히 주문 결과 객체를 만들어서 클라이언트에 보내는 것 까지만 해본다.
주문 도메인 전체 (역할과 구현)
역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다. 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.
주문 도메인 클래스 다이어그램
OrderService: (역할) 주문 인터페이스
OrderServiceImpl: (구현) 주문 인터페이스 구현체
MemberRepository: (역할) 회원 저장소 인터페이스
MemoryMemberRepository: (구현) 회원 저장소 구현체 (메모리 저장소)
DbMemberRepository: (구현) 회원 저장소 구현체 (DB 저장소)
DiscountPolicy: (역할) 할인 정책 인터페이스
FixDiscountPolicy: (구현) 할인 정책 인터페이스 구현체 (정액 할인 정책)
RateDiscountPolicy: (구현) 할인 정책 인터페이스 구현체 (정률 할인 정책)
주문 도메인 객체 다이어그램(1)
회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 된다.역할들의 협력 관계를 그대로 재사용 할 수 있다.
주문 도메인 객체 다이어그램(2)
회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 된다.협력 관계를 그대로 재사용 할 수 있다.
Order 클래스를 생성한다. (memberId, itemName, itemPrice, discountPrice를 속성으로 지정하고, getter/setter/constructor/toString을 생성한다.)
Order 클래스에 최종 금액을 계산하는 메소드를 추가
order 패키지 아래 OrderService 인터페이스를 생성한다.
order 패키지 아래 OrderService의 구현체인 OrderServiceImpl 클래스를 생성한다.
✔️ 참고
주문 서비스 입장에서 보면, 회원 조회나 할인 금액에 대해서는 알지 못한다. 할인에 대한 것은 discountPolicy에 역할을 위임하고, 회원에 대한 것은 memberRepository에 역할을 위임한다. 따라서 단일 책임의 원칙이 잘 지켜졌다고 볼 수 있다. (추후 할인에 대한 수정이 있을 때는, 할인쪽만 수정하면 된다. 주문쪽의 수정은 불필요하다. 만약, 단일 책임의 원칙을 잘 지키지 않아 discountPolicy라는게 없었다면, 할인과 관련된 변경을 해야할 때 주문 서비스의 변경이 필요하게 된다.)