1. 비즈니스 요구사항과 설계
-회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
-주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루 고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)
-> 미확정 부분은 객체지향 설계 방법을 이용해서, 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계
2. 회원 도메인 설계
-회원 도메인 요구사항
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
-회원 도메인 협력 관계
메모리 회원 저장소를 사용하여 개발. 외부 시스템을 결정한 후 구현체를 교체
-회원 클래스 다이어그램
MemberService(회원 서비스) 인터페이스에 대한 구현체 : MemberServicelmpl
MemberRepository(회원 저장소) 인터페이스에 대한 구현체 : MemoryMemberRepository, DbMemberRepository
인터페이스에 대한 구현체가 하나이면 구현체 이름 뒤에 lmpl을 붙인다
-회원 객체 다이어그램
클라이언트 -> Memberservicelmpl -> MemoryMemberRepository
3. 회원 도메인 개발
main/java에 Member 패키지 생성 후, 그 안에 Enum 타입의 Grade를 생성한다.
public enum Grade {
//회원 등급을 BASIC, VIP로 나눈다
BASIC,
VIP
}
main/java/Member 패키지에 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;
}
}
cf) generate 윈도우 단축키 : Alt + Insert (단축키 settings -> keymap에서 확인)
main/java/member 패키지에 MemberRepository 인터페이스를 생성한다.
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
main/java/member 패키지에 MemoryMemberRepository 클래스를 생성한다.
import java.util.HashMap;
import java.util.Map;
public class MemoryMemberRepository implements MemberRepository{
//저장소이므로 map 생성
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);
}
}
cf) 위에서 HashMap으로 생성한 store의 경우 일반적으로는 동시성 이슈를 방지하기 위해서 ConcurrentHashMap으로 생성하는 것이 바람직하나 여기서는 예제를 최대한 단순화하기 위해서 HashMap을 사용하였다.
main/java/member 패키지에 MemberService 인터페이스를 생성한다.
public interface MemberService {
void join(Member member); // 회원가입
Member findMember(Long memberId); // 회원 조회
}
main/java/member 패키지에 위 인터페이스를 구현하는 구현 클래스로 MemberServiceImpl 클래스를 생성한다.
public class MemberServiceImpl implements MemberService{
//MemberRepository와 연결 필요. 이때 구현 객체(MemoryMemberRepository)도 선택해야 한다.
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);
}
}
4. 회원 도메인 실행과 테스트
-회원가입 테스트
main/java/member 패키지에 MemberApp 클래스를 생성한다.
public class MemberApp {
// psvm+Enter 치면 아래 코드 생성
public static void main(String[] args) {
MemberService memberService=new MemberServiceImpl();
//Member 객체 생성 후, 회원 가입 처리
//Long id, String name, Grade grade
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
//아이디에 해당하는 회원 정보를 조회하고, 그 결과를 findMember 변수에 저장
Member findMember = memberService.findMember(1L);
//새로 가입한 회원과 조회한 회원의 이름을 콘솔에 출력
//soutv+Enter 치면 아래 코드 생성
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());
}
}
위 코드는 회원 관리 시스템을 가정하고, 회원 정보를 추가하고 조회하는 기능을 한다.
이는 순수한 자바 코드로 설계한 것이다. 그러나 이렇게 메인 메서드로 로직을 테스트하는 데에는 한계가 있으므로 Junit이라는 테스트 프레임워크를 사용할 수 있다.
cf) 변수 조회 : Ctrl+Alt+V 키
-JUnit 사용 회원가입 테스트
src/test에 member 패키지 생성 후 그 아래에 MemberServiceTest 클래스를 생성한다.
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
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);
}
}
그러나 아래 코드에서 클라이언트(MemberServiceImpl)는 인터페이스인 memberRepository를 의존하고 있다. 또한 이 인터페이스의 구현체를 클라이언트에서 생성하고 있다. 추후 변경 발생시 클라이언트 코드가 변경된다. 즉, OCP원칙을 위배하게 된다. 또한 추상화에 의존해야 한다는 DIP원칙도 위배하게 된다.
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository=new MemoryMemberRepository();
}
5. 주문과 할인 도메인 설계
-주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루 고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다. (미확정)
-주문 도메인 설계
역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있도록 설계했다.
덕분에 회원 저장소와 할인 정책을 유연하게 변경 가능하다.
-주문 도메인 클래스 다이어그램
-주문 도메인 객체 다이어그램
위의 어떠한 다이어그램을 선택하든지 상관없이 상관없이 주문 서비스를 변경하지 않아도 된다.
즉, 역할들의 협력 관계를 그대로 재사용 가능하다.
6. 주문과 할인 도메인 개발
src/main/java에 discount 패키지를 생성하고, 그 아래에 DiscountPolicy 인터페이스를 생성한다.
public interface DiscountPolicy {
/**
* @return 할인 대상 금액
*/
// member: 할인 받을 회원, prince: 할인 금액
int discount(Member member, int price);
}
src/main/java/discount에 위 인터페이스를 구현하는 구현 클래스인 FixDiscountPolicy 클래스를 생성한다.
import hello.core1.member.Grade;
import hello.core1.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;
}
}
}
주문 정보를 담기 위한 주문 객체 클래스를 생성한다.
src/main/java에 order 패키지를 생성하고, 그 아래에 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;
}
//toString()은 객체의 정보를 문자열로 반환하는 역할
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId + // memberId=값
", itemName='" + itemName + '\'' + // itemName='값'
", itemPrice=" + itemPrice + // itemPrice=값
", discountPrice=" + discountPrice + // discoutPrice=값
'}';
}
}
cf) '\'' : Java에서 문자열을 나타낼 때, 따옴표(' ')로 묶인 문자열 리터럴에서는 특수 문자들을 escape하기 위해
역슬래시( \ )를 사용한다. 따라서 '\''는 문자열 내에서 작은 따옴표(')를 표현한다.
다음으로 주문 서비스의 역할을 정의하는 인터페이스를 생성한다.
src/main/java/order에 OrderService 인터페이스를 생성한다.
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
위 인터페이스를 구현하는 구현 클래스를 생성한다.
src/main/java/order에 OrderServiceImpl 클래스를 생성한다.
import hello.core1.discount.DiscountPolicy;
import hello.core1.discount.FixDiscountPolicy;
import hello.core1.member.Member;
import hello.core1.member.MemberRepository;
import hello.core1.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); // 회원 정보를 조회하고, member에 조회된 회원 정보 할당
int discountPrice=discountPolicy.discount(member,itemPrice); // 할인 정책을 적용하고, 할인된 가격 반환
return new Order(memberId,itemName,itemPrice,discountPrice);
}
}
주문 생성 요청이 오면, 회원 정보를 조회하고, 할인 정책을 적용한 다음 주문 객체를 생성해서 반환한다.
7. 주문과 할인 도메인 실행과 테스트
src/main/java/order에 OrderApp 클래스를 생성한다.
import hello.core1.member.Grade;
import hello.core1.member.Member;
import hello.core1.member.MemberService;
import hello.core1.member.MemberServiceImpl;
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 프레임워크를 사용하여 테스트할 수 있다.
-JUnit 사용 테스트
src/test/java에 order 패키지를 생성하고, 그 아래 OrderServiceTest 클래스를 생성한다.
import hello.core1.member.Grade;
import hello.core1.member.Member;
import hello.core1.member.MemberService;
import hello.core1.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); // 할인된 가격이 1000인지 테스트
}
}
cf) Assertions.assertThat() : AssertJ 라이브러리에서 제공하는 메서드로, 테스트 결과를 검증하기 위해 사용한다.
'Spring > 스프링 핵심 원리' 카테고리의 다른 글
인프런 스프링 기본 강의 정리 #6 (2) | 2023.11.25 |
---|---|
인프런 스프링 기본 강의 정리 #5 (0) | 2023.11.25 |
인프런 스프링 기본 강의 정리 #4 (1) | 2023.11.23 |
인프런 스프링 기본 강의 정리 #3 (0) | 2023.11.20 |
인프런 스프링 기본 강의 정리 #1 (1) | 2023.11.19 |