[스프링 핵심원리 기본편] 예제 만들기

흑수·2022년 1월 21일
0

김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.

앞으로 다룰 예제를 만드는데 집중하는 포스팅이 될 것 같아요.

비즈니스 요구사항과 설계

회원

  • 회원 가입, 조회
  • 일반과 VIP 회원 등급
  • 회원 데이터는 자체 DB를 구축하거나 외부 시스템과 연동 (현재 미정)

주문과 할인 정책

  • 상품 주문
  • 회원 등급에 따른 할인 정책
  • 현재는 VIP 회원에게 1,000원 고정 금액 할인. 하지만 할인 정책은 현재 고정이 아니라 추후 변경 가능성 있음

위의 요구사항을 보면 DB를 선택하는 것이나 할인 정책 같은 경우, 확실하지 않습니다.
그렇다면 개발을 계속 미루어야 하는가,,?
객체 지향 설계 방법을 이용해서 추후에 바뀌어도 쉽게 바꿀 수 있게 개발을 하는것이 목표!!

인터페이스를 만들고, 추후에 정해지면 구현체를 만들어 갈아끼우면 됨!

회원 도메인 설계

회원 도메인 협력 관계

클라이언트 -> 회원 서비스(가입, 조회) -> 회원 저장소

메모리 회원 저장소 ---> 회원 저장소
DB 회원 저장소 ---> 회원 저장소
외부 시스템 연동 회원 저장소 ---> 회원 저장소

회원 클래스 다이어 그램

  • 인터페이스 : interface
  • 구현 객체 : object

MemberServiceImpl ---> MemberService
MemberServiceImpl -> MemberRepository
MemoryMemberRepository ---> MemberRepository
DbMemberRepository ---> MemberRepository

회원 객체 다이어 그램

클라이언트 -> 회원 서비스(MemberServiceImpl) -> 회원 저장소

회원 도메인 개발

member

member 패키지 아래에서 파일을 생성합니다.

단축키 (인텔리제이)

  • command + n : 새로운 패키지나 파일을 만들 때
  • control + enter : 생성자, getter, setter 만들 때
  • command + shift + enter : 세미콜론 등, 작업을 완성 시켜줌
  • option + enter : import등을 빠르게 해줌
  • psvm : public static main void~~ 자동 생성
  • f2 : 문제가 있는곳으로 커서 이동
  • soutv : 금방 만들어 놓은 value 값 확인할 수 있게 프린트

회원 등급

public enum Grade {
    BASIC,
    VIP
}

enum으로 만들었기 때문에 모두 대문자로 작성합니다.


회원 엔티티

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

회원 아이디와, 이름, 등급을 필드로 넣어주고 생성자와 getter, setter를 만들어 줍니다.


회원 저장소 인터페이스

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);
}

데이터 베이스 내에서 회원 가입, 조회하는 interface를 담당합니다.


메모리 회원 저장소 구현체

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

어떤 저장소를 이용할지 정해지지 않았지만, 우선 메모리 회원 저장소를 구현해서 이를 이용하겠습니다.
Interface를 상속하기 위해 implements를 이용합니다.


회원 서비스 인터페이스

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

회원 서비스와 관련해서 가입, 조회를 가진 interface입니다.


회원 서비스 구현체

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

회원의 비즈니스 로직을 담당하는 부분입니다.

회원 도메인 실행과 테스트

회원 가입 main

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(member.getId());
        System.out.println("member = " + member);
        System.out.println("findMember = " + findMember);
    }
}

어플리케이션 로직으로 이런식으로 테스트 하는 방법은 옳지 않습니다.
테스트 파일을 따로 만들어 거기에서 테스트하도록 해요.


회원 가입 테스트

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(member.getId());

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

테스트 코드를 작성할 때에는 @Test 어노테이션을 붙여줍니다.
Junit에서 제공하는 Assertions을 이용했는데 단정문이라고 하네요. 성공하지 않는다면 테스트는 실패입니다.
즉, member와 findMember가 isEqualTo(객체가 같아야 함)해야 테스트가 성공하게 됩니다.

memberService의 findMember를 이용해서 찾았으니 이 둘은 같아야겠죠?

문제점은 무엇일까요?!

저장소를 바꾸게 된다면, OCP의 원칙을 준수하지 못합니다. 왜냐하면 OCP는 변경에 있어 닫혀있어야 하는데 구현체를 직접 변경하게 되니까요!
또한, 의존관계를 보면 인터페이스 뿐만 아니라 구현체에도 의존하게 된다는 걸 알 수 있어요.

예시) MemberService(인터페이스)뿐만 아니라 MemberServiceImpl(구현체)

MemberService memberService = new MemberServiceImpl();

주문과 할인 도메인 설계

주문 도메인 협력, 역할, 책임

  1. 주문 생성 : 클라이언트 -> 주문 서비스 역할
  • 회원 Id, 상품명, 상품 가격을 이용해 클라이언트는 주문 생성을 요청
  1. 회원 조회 : 주문 서비스 역할 -> 회원 저장소 역할
  • 할인을 위해 회원 등급이 필요, 주문 서비스는 회원 조회
  1. 할인 적용 : 주문 서비스 -> 할인 정책 역할
  • 회원 등급에 따라 할인 적용
  1. 주문 결과 반환 : 주문 서비스 역할 -> 클라이언트
  • 주문 서비스는 할인 결과를 포함한 주문 결과 반환

주문 도메인 전체

주문 도메인 클래스 다이어그램

주문 도메인 객체 다이어 그램

  • 클라이언트 -> 주문 서비스 구현체 -> 메모리 회원 저장소, 정액 할인 정책
  • 클라이언트 -> 주문 서비스 구현체 -> DB 회원 저장소, 정률 할인 정책

이렇게 구현체만 갈아끼우고 협력 관계를 그대로 사용할 수 있다!
다형성의 힘~!

주문과 할인 도메인 개발

order, discount

order과 discount 패키지 아래에서 파일을 생성합니다.

할인 정책 인터페이스

public interface DiscountPolicy {
    int discount(Member member, int price);
}

할인 가격을 반환하는 할인 정책입니다.


정액 할인 정책 구현체

public class FixDiscountPolicy implements DiscountPolicy{
    private int discountFixAmount = 1000;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return discountFixAmount;
        } else {
            return 0;
        }
    }
}

회원 등급이 VIP라면, 1000원을 할인해줍니다.


주문 엔티티

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

주문자의 id, 아이템 이름, 가격, 할인 가격을 필드로 갖고, 생성자, getter와 setter를 갖습니다.


주문 서비스 인터페이스

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

주문 비즈니스 로직을 담당하고, 메모리 회원 리포지토리와, 고정 금액 할인 정책을 구현체로 생성합니다.

주문과 할인 도메인 실행과 테스트

주문과 할인 정책 실행

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, "memberA", 10000);
        System.out.println("order = " + order);
    }
}

테스트 파일을 따로 만들어서 테스트하도록 해요.


주문과 할인 정책 테스트

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

}

회원 등급이 VIP일 때, 할인 가격이 1000원이 맞으면 테스트를 통과한답니다. memberA는 Grade.VIP가 맞기 때문에 테스트에 통과하지요,,

이번에는 앞으로 다룰 예제를 만드는데 집중했지만, 다음번엔 객체 지향적으로 다시 설계하는 포스팅을 가져오겠습니다!

profile
기록용

0개의 댓글