🍎 Spring/김영한 스프링 핵심원리

[김영한 스프링 핵심원리 - 기본편] 3. 예제 만들기

꾸씅이 2025. 1. 15. 19:05

이번 강의에서는 회원, 주문, 할인 도메인 설계 및 테스트까지 진행하였다.

인터페이스와 구현체에 관해 기초적인 부분을 이해할 수 있어서 좋았다.

평소에 프로젝트를 설계할 때 이부분에 관해서 깊게 생각해 본적이 없는데 앞으로 신경..써야함을 뼈저리게 느꼈다.


1. 회원 도메인 설계 

회원 도메인 요구 사항은 아래와 같다

  • 회원 가입 / 조회 가능
  • 회원은 일반/VIP 두가지 등급
  • 회원 데이터는 자체 DB 구축할 수 있고, 외부 시스템과 연동 가능성 있음(미확정)

협력 관계의 다이어그램을 보자면 위와 같다.

 

회원 클래스의 다이어그램을 보자면 위와 같은데, 이때 이 그림은 정적이라고 볼 수 있다. 

 

회원 객체 다이어그램은 위와 같은데 이 때 이 그림은 동적이다. 

 

 

2. 회원 도메인 개발

회원 등급 

public enum Grade {
    BASIC,
    VIP
}

 

등급은 BASIC(일반), VIP(vip) 등급 두가지로 나눠져 있고, 이넘으로 생성한다.

 

 

회원 엔티티

public class Member {
    private Long id;
    private String name;
    private Grade grade;

    //생성자
    public Member(Grade grade, String name, Long id) {
        this.grade = grade;
        this.name = name;
        this.id = id;
    }

    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;
    }
}

 

회원 엔티티를 생성한다. 

회원을 식별할 수 있는 id와 회원의 이름 그리고 등급을 나타내는 grade로 필드를 선언한다. 

이 후 생성자와 getter setter를 작성한다.

 

 

회원 저장소 인터페이스

public interface MemberRepository {

    //회원 저장
    void save(Member member);

    //회원 id로 찾는거
    Member findById(Long memberId);
}

 

회원 저장소 인터페이스를 선언한다.(역할)

 

 

 

메모리 회원 저장소 구현체

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);
    }
}

회원 저장소의 구현체 생성

데이터 베이스가 아직 확정이 되지 않았기 때문에 단순한 메모리 회원 저장소를 우선 구현한다.

 

 

 

회원 서비스 인터페이스

public interface MemberService {
    //회원 가입
    void join(Member member);

    //회원 조회
    Member findMember(Long memberId);
}

 

회원 서비스 인터페이스 (역할)을 생성한다.

 

 

 

회원 서비스 구현체

public class MemberServiceImpl implements MemberService {

    //DIP 위반
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    //comm shit enter

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

회원 서비스 구현체를 생성한다.

이때 서비스 구현체는 회원 저장소 인터페이스와 구현체 두개에 모두 의존하고 있기 때문에 DIP를 위반하게 된다. 

 

 

 

3. 회원 도메인  - 회원 가입 테스트

강의에서는 애플리케이션 로직으로 테스트를 먼저 하지만 나는 이 부분은 생략하겠다

바로 JUnit 테스트로 넘어간다.

 

public class MemberServiceTest {

    MemberService memberService = new MemberServiceImpl();

    @Test
    void join(){
        //given
        Member member = new Member(Grade.VIP, "memberA", 1L);

        //when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        //then
        assertThat(member).isEqualTo(findMember);


    }
}

 

실행 결과 잘 돌아간다는 것을 알 수 있다.


 

다음으로는 주문과 할인 정책에 대한 도메인 설계이다.

 

4. 주문과 할인 도메인 설계

주문과 할인 정책에 대한 요구 사항은 다음과 같다

  • 회원은 상품을 주문할 수 있음
  • 회원 등급에 따라 할인 정책 적용 가능
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인 적용(나중에 변경 가능성 있음)
  • 할인 정책은 변경 가능성 높음 -> 아직 정하지 못함. 오픈 직전까지 고민 중

 

역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다.

 

 

 

 

 

정적인 클래스 다이어그램이다.

이 다이어 그램을 통해 인터페이스에 여러 구현체가 매핑 될 수 있음을 알 수 있다.

 

 

 

 

 

 

회원을 메모리에서 조회하고, 정액 할인 정책을 지원해도 주문 서비스를 변경하지 않아도 된다.

역할들의 협력 관계를 그대로 재사용 할 수 있다.

 

 

 

회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책을 지원해도 주문 서비스를 변경하지 않아도 된다.

 

 

 

할인 정책 인터페이스

public interface DiscountPolicy {

    //@retrun 할인 대상 금액
    int discount(Member member, int price);
}

 

 

할인 정책 구현체

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;
        }
    }
}

 

회원 등급이 VIP인 경우 1000(할인 금액)을 반환하고 아닐 경우 0(할인 금액)을 반환한다.

 

 

 

 

주문 엔티티

public class Order {

    private Long memberId;
    private String itemName;
    private int imtemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int imtemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.imtemPrice = imtemPrice;
        this.discountPrice = discountPrice;
    }

    public int calculatePrice() {
        return imtemPrice - 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 getImtemPrice() {
        return imtemPrice;
    }

    public void setImtemPrice(int imtemPrice) {
        this.imtemPrice = imtemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    public void setDiscountPrice(int discountPrice) {
        this.discountPrice = discountPrice;
    }


    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", imtemPrice=" + imtemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

 

 

 

주문 서비스 인터페이스

public interface OrderService {

    Order createOrder(Long memberId, String itemName, int itemPrice);
}

 

 

 

주문 서비스 구현체

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);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

 

주문 생성이 요청 오면

회원 정보를 조회하고

할인 정책을 적용한 다음

주문 객체를 생성해서 반환한다.

 

 

5. 주문과 할인 도메인 테스트

public class OrderServiceTest {

    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder() {
        Long memberId = 1L;
        Member member = new Member(  Grade.VIP,"memberA",memberId);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "ItemA", 10000);

        assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }
}

 

잘 돌아감을 알 수 있다.

 


🙏 이 포스트는 김영한 개발자님의 <스프링 핵심원리 - 기본편> 를 듣고 공부한 내용을 바탕으로 작성되었습니다.