예제 프로젝트 목표

1. 회원 도메인, 주문과 할인 도메인을 중심으로 예제를 만든다.
2. 역할과 구현을 나눈다.
3. 실제 요구사항이 변경되었을 때 다형성, OCP, DIP가 지켜지는지 확인한다. 
4. 예제를 만들고 나서 객체지향 원리를 적용해보자.

목차

1. 프로젝트 생성

2. 비즈니스 요구사항과 설계

3. 회원 도메인 설계

4. 회원 도메인 개발

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

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

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

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


1. 프로젝트 생성

1) 사전 준비 : java 11 설치, intelliJ 또는 Eclipse 설치 

2) 스프링부트 스타터 사이트에서 프로젝트 생성 

Project: Gradle Project

Spring Boot: 2.3.x 이상 (2021년 12월 기준, 2.6.1선택함)

Language: Java

Packaging: Jar

Java: 11

 

Project Metadata

groupId: hello, artifactId: core (core-basic으로 생성함)

 

Dependencies: 선택하지 않는다.

지금은 스프링 없는 순수한 자바로만 개발을 진행한다는 점을 꼭 기억하자! 스프링 관련은 한참 뒤에 등장한다.
스프링부트는 프로젝트 생성이 편하니까 이용하는 것이다. 

 

3) IntelliJ Gradle 대신에 자바 직접 실행 

최근 IntelliJ 버전은 Gradle을 통해서 실행 하는 것이 기본 설정이다. 이렇게 하면 실행속도가 느리다.

다음과 같이 변경하면, IntelliJ가 자바를 바로 실행해서 실행속도가 더 빠르다.

Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle 열고 아래 내용으로 선택.

Build and run using:  IntelliJ IDEA
Run tests using:    IntelliJ IDEA

4) Gradle Dependency에서 spring-boot-starter를 보면, 스프링 core 라이브러리를 확인할 수 있다. 


2. 비즈니스 요구사항과 설계

기획자로부터 아래의 요구사항을 듣게 되는 상황이다. 

요구사항은 크게 회원, 주문과 할인 2 가지로 구성된다. 

1) 회원

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

2) 주문과 할인 정책

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

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

도메인 설계 부터 시작해보자. 


3. 회원 도메인 설계

1) 회원 도메인 요구사항 

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

2) 도메인 협력 관계

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

  • 회원 서비스 : 회원 데이터에 접근할 수 있는 계층을 따로 만든다. 
  • 회원 저장소 : 저장소 인터페이스를 먼저 만든다. 자체 DB를 쓸 지, 외부 시스템 연동을 할지 미정이기 때문이다.
    일단 메모리 저장소를 사용하자.
  • (역할과 구현을 분리한다!)

3) 클래스 다이어그램

실제 구현 레벨로 내려오면 클래스, 인터페이스 명세를 작성한 클래스 다이어그램이 필요하다.

클래스 간의 의존 관계, 연관 관계, 제약 조건 등을 기반으로 개발한다. 백문이 불여일타! 곧 코드로 옮겨보자. 

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

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

저장소가 어떤 것이 생성되는지는 런타임에서 객체가 생성 됬을때 정해진다. 

특정 순간에 객체 간의 관계 및 흐름을 표현한다. 


4. 회원 도메인 개발

1) 회원 엔티티

class Member, enum Grade 

 

2) 회원 저장소

인터페이스: interface MemberRepository

구현체: class MemoryMemberRepository

 

인터페이스와 구현체는 다른 패키지에 두면 좋다. 지금은 작은 예제니까 member 패키지에 함께 뒀다.  

오류처리 같은 예외처리는 제쳐두고 가입, 조회 기능에 집중한다. 

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

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

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

1) JUnit Test Framework를 이용한다.  

main메서드에서 시험하는 것에는 한계가 있기 때문이다. 테스트 코드를 제대로 짜야 좋은 코드가 나온다. 

패키지 레벨을 맞추고 Test파일 만들기

Tip ) 아래와 같은 구조로 테스트 메서드를 작성하면 좋다. 

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

 

3) 주의 ! Assertions 는 assertj.core.api 를 쓰자. 헷갈리지 말기.

import org.assertj.core.api.Assertions;

4) 회원 서비스 구현체를 보면 구조적 문제가 있다? 

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

아래 코드를 보자.

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

-> DIP위반

public class MemberServiceImpl implements MemberService{

    // 구현 객체를 MemoryMemberRepository 로 선택해주자
    private final MemberRepository memberRepository = new MemoryMemberRepository();

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

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

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

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

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

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

앞서 배운대로 역할은 인터페이스, 구현은 구현클래스다. 

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

구현 객체가 점선 화살표로 인터페이스를 바라보고 있다. 

회원 저장소는 물론이고 할인 정책도 유연하게 변경할 수 있다. 

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

주문 서비스는 인터페이스와 구현체로 분리되어 있다. 

주문 서비스 구현체는 회원 저장소 인터페이스에 의존한다. 

주문 서비스 구현체는 할인 정책 인터페이스에 의존한다. 

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

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

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

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

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

객체 다이어그램1

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

이 경우에도 역할들의 협력관계를 그대로 재사용 할 수 있다.

객체 다이어그램2

이제 개발하자.


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

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

 

주문 생성 요청 받기 -> 주문 서비스 인터페이스에 의존

회원 정보를 조회 -> 멤버 리포지토리 인터페이스에 의존 

할인 정책 조회 -> 할인 정책 인터페이스에 의존 

 

1) 할인 정책 인터페이스 : interface DiscountPolicy

public interface DiscountPolicy {
    // @return 할인 대상 금액
    int discount(Member member, int price);
}

2) 고정 할인 정책 구현체 :  class FixDiscountPolicy

3) 주문 : class Order

4) 주문 서비스 인터페이스 : interface OrderService 

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

5) 주문 서비스 구현체 : class OrderServiceImpl


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

1) main() 메서드에서 테스트 코드

  JUnit 테스트에 익숙치 않은 사람을 위해 임시로.. 

2) JUnit 테스트 코드 "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);
    }
}

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

다음 시간에는 갑자기 악덕 기획자가 나타나서 요구사항을 바꿀 것이다.

할인 정책이 바뀌어도 큰 문제가 없을지 '객체 지향 원리'를 적용해서 해결하자. 


다음 강의에서는 '객체 지향 원리'를 적용하여 예제 프로젝트를 개선한다. 

공부 내용 출처 :  스프링 핵심 원리 기본편 

728x90

+ Recent posts