[되새김질] 스프링 기본 - 스프링 핵심 원래 이해#1

jeyong·2023년 7월 29일
0

- 해당 게시물은 인프런 "스프링 핵심 원리 - 기본편" 강의를 참고하여 작성한 글 입니다.

스프링 핵심 원리 - 기본편

1. 비지니스 요구 사항 설계

기획자로부터 아래의 요구사항을 듣게 된다.
1) 회원

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

2) 주문과 할인 정책

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
  • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. (미확정)

확정되지 않은 부분이 있더라도, 인터페이스를 만들어 두면 구현체는 언제든 갈아 끼울 수 있도록 설계하면 된다!

2. 회원 도메인 설계

1) 회원

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

2) 도메인 협력 관계
기획자도 보는 그림이다. 도메인 협력 관계를 기반으로 클래스 다이어그램을 그린다.

  • 회원 서비스 : 회원 데이터에 접근할 수 있는 계층을 따로 만들어서 회원 저장소에 접근한다.
  • 회원 저장소 : 저장소 인터페이스를 먼저 만든다. 자체 DB를 쓸 지, 외부 시스템 연동을 할지 미정이기 때문이다.

3) 클래스 다이어그램
실제 구현 레벨로 내려오면 클래스, 인터페이스 명세를 작성한 클래스 다이어그램이 필요하다.
클래스 간의 의존 관계, 연관 관계, 제약 조건 등을 기반으로 개발한다.

  • 회원 서비스 인터페이스 : MemberService
  • 회원 서비스 구현체 : MemberServiceImpl
  • 저장소 인터페이스 : MemberRepository
  • 저장소 구현체 : MemoryMemberRepository

4) 객체 다이어그램 : 런타임에서 객체가 생성됬을 때의 시나리오를 다이어그램으로 표현

  • 저장소가 어떤 것이 생성되는지는 런타임에서 객체가 생성 됬을때 정해진다.
  • 특정 순간에 객체 간의 관계 및 흐름을 표현한다.

간단하게 이야기하자면 동적으로 정해진다고 이해하면 된다.

3. 회원 엔티티 개발

1) 회원 엔티티

  • class Member
  • enum Grade

2) 회원 저장소

  • 인터페이스: interface MemberRepository
  • 구현체: class MemoryMemberRepository

메모리 회원 저장소는 동시성 이슈 때문에 실무에서는 ConcurrentHashMap을 써야 하지만, 간단한 하게 HashMap을 쓴다. 아래 링크를 참고하자

https://tecoble.techcourse.co.kr/post/2021-11-26-hashmap-hashtable-concurrenthashmap/

private static Map<Long, Member> store = new HashMap<>(); // 메모리 저장소

3) 회원 서비스

  • 인터페이스 interface MemberService
  • 구현체 class MemberServiceImpl
public class MemberServiceImpl implements MemberService{
    // 구현 객체를 MemoryMemberRepository 로 선택해주자
    private final MemberRepository memberRepository = new MemoryMemberRepository();

4. 회원 도메인 실행과 테스트

1) JUnit Test Framework를 이용한다.

단축키로 간편하게 만들 수 있다. 아래 링크를 참고하자
https://theamabile.tistory.com/21

2) 회원 서비스에서 저장 및 조회 JUnit 테스트 코드

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

문제 발생) 회원 서비스 구현체를 보면 구조적 문제가 발생한다.

의존 관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제가 있다.

public class MemberServiceImpl implements MemberService{
    // 구현 객체를 MemoryMemberRepository 로 선택해주자
    private final MemberRepository memberRepository = new MemoryMemberRepository();

MemberServiceImpl 구현체가 MemberRepository 인터페이스 뿐만 아니라, MemoryMemberRepository 구현체 까지 모두 의존하고 있다.
-> DIP위반

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

1) 주문과 할인 정책 요구사항

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
  • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. (미확정)

2) 주문 도메인 협력, 역할, 책임 : 기획자와 개발자가 구상하는 설계도

  • 주문 생성 : 주문. 회원id, 상품명, 상품 가격 -> 주문 서비스 인터페이스
  • 회원 조회 : 할인 받을 수 있는 등급인지 회원id로 저장소에 조회 -> 레포지토리 인터페이스
  • 할인 적용 : 할인 정책은 회원 등급에 따라 할인을 적용해줌 -> 할인 정책 인터페이스
  • 주문 결과 반환 : 실무에서는 데이터를 DB에 저장하지만, 예제를 단순화 하기 위해 주문 결과만 반환. -> 주문 서비스 인터페이스

3) 주문 도메인 전체 : 역할과 구현의 분리

  • 앞서 배운대로 역할은 인터페이스, 구현은 구현클래스다.
  • 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다.

구현 객체가 점선 화살표로 인터페이스를 바라보고 있다.
-> 회원 저장소는 물론이고 할인 정책도 유연하게 변경할 수 있다.

4) 클래스 다이어그램 : 정적 정보. 클래스, 인터페이스의 명세

  • 주문 서비스는 인터페이스와 구현체로 분리되어 있다.
  • 주문 서비스 구현체는 회원 저장소 인터페이스에 의존한다.
  • 주문 서비스 구현체는 할인 정책 인터페이스에 의존한다.

    -> 구현체가 인터페이스(역할)에만 의존한다

5) 객체 다이어그램 : 동적 정보. 런타임에서 특정 순간에 객체 간의 관계 및 상황을 표현

회원을 메모리에서 조회하고, 정액 할인 정책을 지원하는 경우.

회원을 DB에서 조회하고, 정률 할인 정책을 지원하는 경우.

-> 즉, 저장소의 구현체가 바뀌어도, 주문 서비스 구현체를 변경할 필요가 없다.

5. 주문과 할인 도메인 개발

구현내용 : 주문 생성 요청이 오면, 회원 정보를 조회하고, 할인 정책을 조회 한 다음 주문 객체를 생성해서 반환한다.

  • 주문 생성 요청 받기 -> 주문 서비스 인터페이스에 의존
  • 회원 정보를 조회 -> 멤버 리포지토리 인터페이스에 의존
  • 할인 정책 조회 -> 할인 정책 인터페이스에 의존

1) 할인 정책 인터페이스 : interface DiscountPolicy
2) 고정 할인 정책 구현체 : class FixDiscountPolicy

  • VIP면 1000원 할인, 아니면 할인 없음

3) 주문 엔터티 : class Order

엔터티 클래스에 toString 메소드를 재정의하면 객체의 이름이나 주소값이 아닌 객체의 고유 정보를 출력할 수 있다.
https://inpa.tistory.com/entry/JAVA-%E2%98%95-toString-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%9E%AC%EC%A0%95%EC%9D%98-%EC%99%84%EB%B2%BD-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B8%B0

4) 주문 서비스 인터페이스 : interface OrderService
5) 주문 서비스 구현체 : class OrderServiceImpl

  • 주문 생성 요청이 오면, 회원 정보를 조회하고, 할인 정책을 적용한 다음 주문 객체를 생성해서 반환한다.
  • 메모리 회원 리포지토리와, 고정 금액 할인 정책을 구현체로 생성한다.

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

JUnit 테스트 코드

public class OrderServiceTest {

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

    @Test
    void createOrder(){
        Long memberId = 1L; // null 이 들어갈 수도 없어서 wrapper type 썼음
        Member member = new Member(memberId, "itemA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

지금까지 주문 도메인에서 최대한 다형성을 활용하여 인터페이스를 의존하도록 설계했다.

다형성만을 이용해서 중간에 DIP위반을 한 것을 제외하고는 성공적이었다.
다음 시간에는 갑자기 악덕 기획자가 나타나서 요구사항을 바꿀 것이다.
할인 정책이 바뀌어도 큰 문제가 없을지 '객체 지향 원리'를 적용해서 해결하자.

profile
천천히 잊어가기

0개의 댓글