728x90
1. 비즈니스 요구사항과 설계
2. 설계 - 회원 도메인
- 2가지의 도메인이 존재
- 회원
- 주문
- 우선 회원 도메인 먼저 설계
- 회원 가입 목록 -> 회원서비스(가입, 조회) -> 회원저장소(메모리저장소, DB저장소, 외부 시스템 연동 회원 저장소)
Member service repository
2.1. 회원 도메인 개발
1. 회원 등급
public enum Grade {
BASIC,
VIP
}
2. 회원
public class Member {
private Long id;
private String name;
private 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 Member(Long id, String name, Grade grade) {
this.id = id;
this.name = name;
this.grade = grade;
}
}
3. 회원 저장소 (인터페이스)
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId); }
3-1. 회원저장소 (구현체)
public class MemoryMemberRepository implements MemberRepository {
// 임시 저장소 (db와 동일한 역할): store
private static Map<Long, Member> store = new HashMap<>(); // 동시성 이슈 때문에 concurrentHashMap을 실무에서는 사용
@Override
public void save(Member member) {
store.put(member.getId(), member);
}
@Override
public Member findById(Long memberId) {
return store.get(memberId);
}
}
4. 회원 서비스(인터페이스)
public interface MemberService {
void join(Member member);
Member findMember(Long memberId); }
}
4-1. 회원 서비스(구현체)
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final: 다른 MemberReposotory의 객체로 바뀌는 것을 방지
// 변수는 interface에 의존, 객체는 구현체에 의존. DIP와 OCP에 위반
public void join(Member member) {
memberRepository.save(member);
}
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
2.2. 회원 도메인 TEST
1. java로만 test
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); // HashMap에서 valuer인 Member object를 반환
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName()); // findMember는 Member 객체를 반환
}
}
2. 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); // 객체 주소로 비교
}
}
3.설계 - 주문과 할인 도메인
- 2가지 도메인 존재
- 회원
- 주문
- 두번째인 주문 도메인을 설계
- 회원의 등급에 따른 할인 정책을 생각 - 주문 -> 주문 서비스(등급 할인) -> 1) 회원저장소(등급 찾기) -> 2) 할인 적용(할인 정책) -> 할인된 주문 반환
domain service repository repository return
- 원래는 주문 데이터를 DB에 저장해야되지만 복잡하므로 주문 결과를 반환한다.
- 목표하는 설계
- 회원 조회 방식, 할인 정책이 변경이 되어도 주문 서비스내부 코드 변경 필요없이 repository, discount interface 구현 객체만 변경하면 된다.
- 역할들의 협력 관계를 그대로 재사용 할 수 있다.
- 1. 할인 정책(인터페이스)
3.1. 주문 도메인 개발 with 할인
1. 할인 정책(인터페이스)
public interface DiscountPolicy{
int discount(Member memeber, int price);
}
1-1. 정액 할인 정책(구현체)
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;
}
}
}
2. 주문 엔티티(주문 틀)
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 +'}';
}
}
3. 주문 서비스 (인터페이스)
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice)
}
3.1. 주문 서비스 (구현체)
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 = memberRepostotory.findById(memberId);
int discountPrice = discountPolicy.discount(memeber, itemPrice);
return new Order(memeberId, itemName, discountPrice);
}
}
3.2. 주문 도메인 TEST
1. java로만 test
public class OrderApp {
public static void main(String[] args ) {
MemberService memberSrvice = new MeberServiceImpl();
OrderService orderService = new OrderServiceImpl();
Long memberId = 1L;
// 객체 생성 단계에서 null이 들어갈 수 있어서 long이 아니라 Long 사용
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);
Order order = orderService.createOrder(memberId, "itemA", 10000);
System.out.println("order = " + order);
}
}
2. JUnit5 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);
asserThat(order.getDiscountPrice()).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 1000원 할인 못받음") // test code 이름 설정
void createOrder_BASIC(){
//given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
//when
int discount = discountPlicy.discount(member, 10000);
//then
asserThat(discount).isEqualTo(0);
}
}
- 이런 식으로 단위 test를 잘 만드는 것이 중요 - spring에 의존하지 않고 java로만 구현한 Test. 속도가 엄청 빠르다.
4. 현재 개발의 문제점
- 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점
- 정률 할인 정책 코드를 만들어 보면서 어떻게 의존 되는지 확인
1-2. 정액 할인 정책(구현체)
public class RateDiscountPolicy implements DiscountPolicy{
private int discountPercent = 10;
@Override
public int discount(Member member, int price){
if(member.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else{
return 0;
}
}
}
더보기
1-1. 정액 할인 정책(구현체)
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;
}
}
}
3-1. 주문 서비스(구현체) - 변경
다형성을 최대한 구현 했지만 코드의 의존성 문제로 인해서 source code를 건들이는 문제 발생 - DIP, OCP 위반
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 변경 부분
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice){
Member member = memberRepostotory.findById(memberId);
int discountPrice = discountPolicy.discount(memeber, itemPrice);
return new Order(memeberId, itemName, discountPrice);
더보기
3-1. 주문 서비스(구현체)
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 = memberRepostotory.findById(memberId);
int discountPrice = discountPolicy.discount(memeber, itemPrice);
return new Order(memeberId, itemName, discountPrice);
}
}
4.1. 위반 이유 - 설계도로 보기
4.2. 해결 방안
- 인터페이스에만 의존하도록 설계코드 변경
5. 추가 사항
- domain
- 객체 의미
- service, repository, service에 주입되는 다른 추가 class 들의 작업에 핵심 data를 가진 객체
- domain같은 경우는 client마다 새로운 객체를 사용하는 것 같다.
- springContainer에 등록을 하지 않고 사용
- 근거
- domain 객체는 상황에 따라서 공유하는 것도 있고 계속 생성해야하는 것도 존재하는 것 같다.
- member 같은 경우 사람마다 정보가 다르니깐 당연히 따로 써야한다.
- Order 같은 경우도 어떻게 보면 주문 후 없애버릴 객체지만 주문 동안에는 각각의 주문객체가 존재 해야한다.
- 뭐 상황에 따라서 각각의 객체가 필요없는 도메인도 있을 수 있겠지 않을까 싶다.
- 왜 service 다음에는 무조건 repository이고 method도 비슷한데 둘을 분리하지?
- repository에 저장되기전 필터링하는 역할도 수행, 잘못된 정보가 오면 거절
- 항상 repository가 오는 것이 아니다.
- controller로 부터 domain 객체를 받아서 해당 서비스가 db에 회원 정보 저장이다 하면 해당 도메인 객체를 repository로 이동 시키고 해당 서비스가 할인 이다 하면은 할인 정책에 관련된 작업을 수행
- 결론
- service에 따라서 service 다음 수행되는 무언가의 class는 repository가 될수도 있고 discount(repository가 아닌 작업 수행 class)가 될수 도 있는 것이다.
- repository는 거의 필수로 사용되니깐 spring에서 @Repository 라는 어노테이션을 만들어준거고 할인 정책 같은 경우는 @Component로 spring Container에 등록해서 사용
- 역할 구분
- 역할이 다른 service, controller는 각자의 역할에 맞게 class를 나누어 줘야한다.
- repository, discount는 도메인을 활용한 최종 작업이라 보면되는데 상식적으로 분리된 class를 만드는 것을 안다.
이전 발행글 : 스프링 핵심 원리 이해 0 - 객체지향 설계와 스프링
다음 발행글 : 스프링 핵심 원리 이해 2 - 객체 지향 원리 적용
출처: 인프런 스프링 핵심 원리 - 기본편