** 본 글은 우아한 형제들 김영한 개발자님 강의를 기반으로 직접 재구성한 글 입니다. **
🎯 비즈니스 요구 사항과 설계
- 회원
회원을 가입하고 조회할 수 있다.
회원은 일반과 VIP 두 가지 등급이 있다.
회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
- 주문과 할인 정책
회원은 상품을 주문할 수 있다.
회원 등급에 따라 할인 정책을 적용할 수 있다.
할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
💡 미확정인 부분이 있지만 객체 지향 설계 방법을 통해 결정 전에도 개발 가능! ( 인터페이스를 만들고 구현체를 언제든지 바꿔 낄 수 있도록 설계 )
🎯 도메인 설계 & 개발
1. 회원 도메인 설계
- 회원 객체 다이어그램은 실제 실행시 사용되는 객체를 의미한다. 본 회원 객체 다이어그램에 있는 회원 서비스는 MemberServiceImpl을 의미한다.
- 회원 클래스 다이어그램만으로는 실제 실행 시 어떤 클래스가 이용되는지 명확히 알 수 없어 회원 객체 다이어그램을 두어 이를 확인한다.
- 클래스 다이어그램은 정적, 객체 다이어그램은 동적인 것으로 볼 수 있음
2. 회원 도메인 개발
package hello.hellospring.member;
# 회원 등급 표기를 위한 enum 클래스
public enum Grade {
BASIC,
VIP
}
package hello.hellospring.member;
# 회원 객체 클래스 생성
# cmd + n : constructor, getter,setter 자동 생성 단축키
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;
}
}
회원 저장소에 해당하는 인터페이스
package hello.hellospring.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
회원 저장소 구현체에 해당하는 클래스 - MemoryMemberRepository
package hello.hellospring.member;
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);
}
}
회원 서비스 인터페이스
package hello.hellospring.member;
public interface MemberService {
void join(Member member); // 회원가입
Member findMember(Long memberId); // 회원 조회
}
회원 서비스 구현체
package hello.hellospring.member;
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);
}
}
3. 회원 도메인 실행과 테스트
3-1. 1번 예제는 순수 java 만을 이용하여 구현한 기본 예시 ( 비효율적 )
package hello.hellospring;
import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberService;
import hello.hellospring.member.MemberServiceImpl;
public class MemberApp {
public static void main(String[] args) { // psvm 치면 자동완성
MemberService memberService = new MemberServiceImpl();
Member member = new Member(1L, "memberA", Grade.VIP); // cmt+opt+ v : 자동완성
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("findMember = " + findMember.getName()); //soutv 치면 자동완성
}
}
3-2. Spring의 junit을 이용한 테스트 코드
- 상위 test/hellospring 폴더 아래 member package를 만들고 아래 테스트 코드 파일 생성
- 상위 java 아래 있던 동일한 구조를 test 아래에 똑같이 만들어 줬다고 보면 됨.
package hello.hellospring.member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public 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); // 회원가입한 멤버와 찾는 멤버가 동일하면 ok
}
}
- Test 코드임을 명시하는 어노테이션 @Test를 써줘야한다. ( junit.jupiter.api.Test )
- given -> when -> then 구조로 테스트 코드를 로직화하는 것이 좋다
- 비교 및 검증을 위해 Assertions 라이브러리를 사용한다. ( assertj.core.api.Assertions )
❓ 위 회원 도메인 설계의 문제점
Q. 다른 저장소로 변경할 때 OCP의 원칙을 잘 준수하는가?
A. 잘 준수하지 못한다. 새로운 저장소로 변경할 경우 new 해서 저장소 객체를 넣어주었던 부분 코드들을 수정해야하는 문제가 발생한다.
Q. DIP를 잘 지키고 있는가?
A. 추상화에 의존해야지, 구체화에 의존하면 안된다 라는 DIP원칙에 위배되는 부분이 있다. 의존관계가 인터페이스 뿐 아니라 구현까지 모두 의존하는 문제가 있다. 위 예로 MemberServiceImpl은 MemberService 인터페이스에도 의존하고 MemoryMemberRepository 구현체에도 의존한다.
4. 주문과 할인 도메인 설계
주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)

- 할인 정책의 경우 변동가능성이 높아 인터페이스와 구현체를 분리해서 구현한다!!!!
- 데이터베이스, 할인 정책 등이 추후 변경 되어도 주문 서비스를 변경하지 않아도 된다.
- 회원 저장소 뿐 아니라 할인 정책까지 유연하게 변경할 수 있는 구조!
5. 주문과 할인 도메인 개발
할인 정책 인터페이스
package hello.hellospring.discount;
import hello.hellospring.member.Member;
public interface DiscountPolicy {
/**
*
* @param member
* @param price
* @return 할인 대상 금액
*/
int discount(Member member, int price); // f2 누르면 오류 난 곳으로 이동
}
할인 정책 구현체
package hello.hellospring.discount;
import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;
public class FixDiscountPolicy implements DiscountPolicy{
private int discountFixAmount = 1000;
@Override
public int discount(Member member, int price) {
if (member.getGrade() == Grade.VIP){ //멤버 등급이 vip 일 때
return discountFixAmount;
}else{
return 0;
}
}
}
주문 객체 생성
package hello.hellospring.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 void setMemberId(Long memberId) {
this.memberId = memberId;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
public int getItemPrice() {
return itemPrice;
}
public void setItemPrice(int itemPrice) {
this.itemPrice = itemPrice;
}
public int getDiscountPrice() {
return discountPrice;
}
public void setDiscountPrice(int discountPrice) {
this.discountPrice = discountPrice;
}
@Override
public String toString() {
return "Order{" +
"memberId=" + memberId +
", itemName='" + itemName + '\'' +
", itemPrice=" + itemPrice +
", discountPrice=" + discountPrice +
'}';
}
}
주문 서비스 인터페이스
package hello.hellospring.order;
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice );
}
주문 서비스 구현체
package hello.hellospring.order;
import hello.hellospring.discount.DiscountPolicy;
import hello.hellospring.discount.FixDiscountPolicy;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberRepository;
import hello.hellospring.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); //cmd+opt+v
// 할인 금액 알아오기 ( 할인정책 서비스가 처리해줌, order서비스는 그냥 받아서 쓰면됨)
int discountPrice = discountPolicy.discount(member, itemPrice); // 할인은 할인 정책에서 알아서 처리 , 주문 서비스는 관여 x
return new Order(memberId, itemName, itemPrice, discountPrice);
}
}
6. 주문과 할인 도메인 실행과 테스트
6-1. 1번 예제는 순수 java 만을 이용하여 구현한 기본 예시 ( 비효율적 )
package hello.hellospring;
import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberService;
import hello.hellospring.member.MemberServiceImpl;
import hello.hellospring.order.Order;
import hello.hellospring.order.OrderService;
import hello.hellospring.order.OrderServiceImpl;
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.toString());
}
}
6-2. Spring의 junit을 이용한 테스트 코드
package hello.hellospring.order;
import hello.hellospring.member.Grade;
import hello.hellospring.member.Member;
import hello.hellospring.member.MemberService;
import hello.hellospring.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
public 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);
}
}
🎯 최종 정리
- 다형성을 잘 활용하여 예시를 만들었지만 이후 할인 정책 또는 저장소가 변경 되었을 때도 잘 동작하는가? 에 대한 고민 필요!!
- 인터페이스와 구현을 좀 더 분리할 수 있는 방안을 고려할 필요가 있음
- 객체 지향적 설계 구조에 대해 학습할 수 있었음!!
** 다음 글에서 위 문제점을 개선하는 내용을 담을 예정입니다 **
'스터디IT🌼 > SpringBoot' 카테고리의 다른 글
스프링이란 ?! (1) | 2022.10.10 |
---|---|
[SpringBoot] 프로젝트 생성 _라이브러리 살펴보기 (0) | 2022.10.08 |
[ Intellij ] mac에서 마우스 먹통 & 씹힘 문제 해결 (0) | 2022.10.05 |
[SpringBoot] Lombok Annotation Processer 에러 ( Cannot find Symbol ) (0) | 2022.01.14 |
[SpringBoot] Annotation 종류 & 역할 (0) | 2022.01.14 |
댓글