Spring

[스프링] 스프링 핵심 원리 기본편 정리: 2. 스프링 핵심 원리 이해1 - 예제 만들기

아윤_ 2023. 8. 7. 00:18

 

 

스프링 핵심 원리 이해 1 - 예제 만들기

 

 

프로젝트 생성

 

사전 준비물

 

프로젝트를 생성하기 전 다음과 같은 사전 준비물이 필요하다.

 

  • Java 11 설치
  • IDE: IntelliJ 또는 Eclipse 설치

 

본 강의에서는 Java 11 버전을 기반으로 강의가 진행되기 때문에 다른 버전을 설치하여 사용할 경우, 각종 오류가 발생할 가능성이 높다. 따라서 꼭 오라클의 Java 11 버전을 설치하여 사용하길 바란다.

 

Java 11 버전 설치 및 환경변수 설정과 관련된 내용은 해당 링크를 참조하면 된다. 

https://mimah.tistory.com/entry/Java-JDK-11-%EB%B2%84%EC%A0%84-%EC%84%A4%EC%B9%98-%EB%B0%8F-%ED%99%98%EA%B2%BD-%EB%B3%80%EC%88%98-%EC%84%A4%EC%A0%95

 

Java JDK 11 버전 설치 및 환경 변수 설정 (Open jdk, Oracle jdk)

벌써 JDK 17 버전까지 나왔네요.. Open JDK (+ 추가) 오라클 JDK 는 로그인이 필요하기 때문에 open jdk를 다운로드 받아도 됩니다. Java Platform, Standard Edition 11 Reference Implementations Java Platform, Standard Edition 1

mimah.tistory.com

 

 

다음으로, IDE 설치와 관련된 내용인데, 해당 강의에서는 IntelliJ를 사용하여 강의가 진행되기 때문에 IntelliJ를 설치하는 것을 권장한다. 

 

IntelliJ는 해당 사이트에서 설치가 가능하다.

https://www.jetbrains.com/ko-kr/idea/download/?section=windows 

 

최고의 Java 및 Kotlin IDE인 IntelliJ IDEA를 다운로드하세요

 

www.jetbrains.com

 

 

스프링 프로젝트 생성

 

사전 준비가 모두 끝났다면, 스프링 부트 스타터 사이트로 이동하여 스프링 프로젝트를 생성해야 한다.

 

[스프링 부트 스타터 사이트]

https://start.spring.io/

 

해당 사이트에 들어가면 다음과 같은 화면이 나올 것이다.

 

 

 

화면에 보이는 설정을 다음과 같이 변경해 준다.

 

  • 프로젝트 선택
    • Project: Gradle - Groovy Project
    • Spring Boot: 2.7.14 (2023.08.06 기준)
    • Language: Java
    • Packaging: Jar
    • Java: 11

 

  • Project Metadata
    • groupId: hello
    • artifactId: core

 

  • Dependencies: 선택하지 않는다.

 

 

다음과 같이 설정이 완료되었다면, 하단의 generate 버튼을 클릭하면 core.zip 파일이 생성되고, 해당 파일을 원하는 위치에 압축을 풀어주면 된다.

 

 

 

압축이 다 풀렸다면, IntelliJ를 실행한다.

압축을 푼 core 파일을 클릭한 다음 build.gradle 파일을 클릭하고 OK 버튼을 누른다.

 

 

 

Open as Project 파일을 클릭하면 프로젝트가 실행된다.

 

 

 

Gradle 전체 설정

 

build.gradle

 

 

프로젝트 내부의 build.gradle 파일에서 전체 설정 정보에 대한 내용을 확인할 수 있다.

 

이에 대한 자세한 내용은 해당 사이트를 참조하면 된다.

https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/

 

Spring Boot Gradle Plugin Reference Guide

To manage dependencies in your Spring Boot application, you can either apply the io.spring.dependency-management plugin or use Gradle’s native bom support. The primary benefit of the former is that it offers property-based customization of managed versio

docs.spring.io

 

 

프로젝트 실행

 

src -> main -> java -> hello.core 아래에 있는 CoreApplication 클래스를 실행한다.

  

CoreApplication

 

 

 

실행 결과

 

 

정상적으로 실행이 완료되었다면, 다음과 같은 화면이 뜨게 된다.

 

 

IntelliJ Gradle 대신에 자바 직접 실행

 

최근 IntelliJ 버전은 Gradle을 통해서 실행하는 것이 기본 설정으로 되어있다. 하지만, 이렇게 하면 실행속도가 느리기 때문에 자바로 바로 실행해서 실행속도를 빠르게 해 주도록 하자.

 

먼저, 좌측 상단에 있는 File -> Setting을 클릭한다.

 

 

 

검색창에 'gradle'을 검색하면 우측에 다음과 같은 창이 뜬다.

 

 

 

Build and run using과 Run tests using을 모두 IntelliJ IDEA로 변경한 후, OK를 클릭하여 저장한다.

 

 

 

해당 과정까지 모두 마치면, gradle 환경 설정이 완료된다.

 

 

 

비즈니스 요구사항과 설계

 

비즈니스 요구사항

 

회원

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

 

 

주문과 할인 정책

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

 

 

다음과 같은 요구사항이 있을 때, 할인 정책과 같은 부분은 지금 결정하기 어려운 부분이다. 하지만, 이러한 정책이 결정될 때까지 개발을 무기한 기다릴 수 없다.

 

이러한 상황이 주어졌을 때, 어떻게 설계를 해야 할까?

 

우리는 이전에 배웠던 객체 지향 설계 방법을 이용하여 인터페이스를 만들고, 구현체를 갈아 끼울 수 있도록 설계하면 된다. 객체 지향 설계와 관련된 내용은 이전 글을 참고하면 된다.

 

2023.08.04 - [Spring] - [스프링] 스프링 핵심 원리 기본편 정리: 1. 객체 지향 설계와 스프링

 

[스프링] 스프링 핵심 원리 기본편 정리: 1. 객체 지향 설계와 스프링

해당 강의는 인프런에서 '김영한' 님의 "스프링 핵심 원리 기본편" 강의를 보고 정리한 내용이다. 무료 강의인 "스프링 입문" 강의를 듣고 스프링에 대해 자세히 공부하고 싶다는 마음이 생겨 로

chinkl.tistory.com

 

※ 참고

프로젝트 환경설정을 편리하게 하려고 스프링 부트를 사용한 것이다. 지금은 스프링 없는 순수한 자바로만 개발을 진행한다는 점을 꼭 기억하자! 스프링 관련은 한참 뒤에 등장한다.

 

 

회원 도메인 설계

 

회원 도메인 요구사항

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

 

해당 요구사항을 통해 우리는 먼저 회원 도메인을 다음과 같이 설계할 수 있다.

 

 

회원 도메인 협력 관계

 

회원 도메인 협력 관계

 

 

도메인 협력 관계는 기획자들도 볼 수 있는 그림을 나타낸다.

 

클라이언트는 회원 서비스를 호출할 수 있고, 회원 서비스는 요구사항에 따라 회원가입과 회원조회라는 두 가지 기능을 제공한다.

 

회원 데이터를 저장하기 위해 회원 저장소라는 인터페이스를 만들었으며, 저장소의 구현체로 '메모리 회원 저장소'와 'DB 회원 저장소', '외부 시스템 연동 회원 저장소'를 가진다.

 

아직 저장소와 관련된 부분이 제대로 정해지지 않았기 때문에 메모리 회원 저장소를 만들어서 일단 개발을 진행할 것이다.

 

 

회원 클래스 다이어그램

 

회원 클래스 다이어그램

 

 

도메인 협력 관계를 바탕으로 개발자가 구체화하여 나타낸 그림이 클래스 다이어그램이다. 클래스 다이어그램을 통해 인터페이스와 구현체를 확인할 수 있다.

 

MemberService는 역할, 즉 회원 서비스 인터페이스로 이를 구현한 클래스는 MemberServiceImpl이다.

MemberRepository는 인터페이스로, 이를 구현한 클래스로 MemoryMemberRepository와 DbMemberRepository가 있다.

 

앞에서 배웠던 객체 지향 설계 방법을 이용해 역할인 인터페이스와 구현이 분리되어 있는 것을 알 수 있다.

 

 

회원 객체 다이어그램

 

회원 객체 다이어그램

 

 

객체 다이어그램의 경우, 실제 서버에 올라갔을 때, 실제 객체들의 메모리 간 참조 관계가 어떻게 되는지를 그림으로 나타낸 것이다. (동적으로 결정)

 

클라이언트는 회원 서비스를 바라보고, 회원 서비스는 메모리 회원 저장소를 바라보게 된다. 참고로, 객체 다이어그램에서의 회원 서비스는 클래스 다이어그램에서의 MemberServiceImpl을 나타낸다.

 

 

 

회원 도메인 개발

 

위에서 설계한 '회원 클래스 다이어그램'을 이용해 본격적으로 회원 도메인 개발을 진행해 볼 것이다.

 

 

회원 엔티티

 

먼저, main -> hello.core 패키지 밑에 hello.core.member 패키지를 생성한 다음, 생성된 패키지 아래에 회원 등급을 나타내기 위한 'Grade'를 'Enum'으로 생성한다.

 

 

 

다음과 같이 코드를 작성한다.

 

 

Grade (회원 등급)

package hello.core.member;

public enum Grade {
    //회원 등급
    BASIC,
    VIP
}

 

해당 코드는 회원에 대한 등급이 BASIC과 VIP 두 가지가 있음을 나타낸다.

 

 

다음으로는 회원 엔티티를 만들기 위해 member 패키지 아래에 'Member' 클래스를 생성한다.

 

 

 

회원은 id, name, grade라는 세 가지 속성을 지니므로, 다음과 같이 코드를 입력한다.

 

 

Member (회원 엔티티)

package hello.core.member;

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

 

참고로, Member 클래스에서 Member의 필드 값을 입력한 다음, 인텔리제이에서 alt + insert 단축키(윈도우 기준)를 통해 생성자와 각 필드에 대한 get, set 함수를 자동으로 생성할 수 있다.  

 

 

회원 저장소

 

이번에는 회원 저장소에 대한 코드를 작성해 보도록 하자.

 

먼저, member 패키지 밑에 'MemberRepository'라는 이름의 회원 저장소 인터페이스를 생성하고, 다음과 같이 코드를 작성한다.

 

 

MemberRepository (회원 저장소 인터페이스)

package hello.core.member;

public interface MemberRepository {
    void save(Member member);   //회원 저장

    Member findById(Long memberId); //회원 탐색
}

 

회원 저장소에는 회원 정보를 저장하는 기능과 회원의 아이디를 통해 회원을 찾는 기능 두 가지가 존재한다.

 

이제, 인터페이스인 역할을 만들었으니 구현체로 member 패키지 밑에 'MemoryMemberRepository' 클래스를 생성하여 코드를 작성하도록 하자.

 

 

MemoryMemberRepository (메모리 회원 저장소 구현체)

package hello.core.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); //회원의 아이디를 통해 회원을 찾아 반환한다.
    }
}

 

현재의 경우 데이터베이스가 아직 확정이 되지 않은 상태이다. 하지만, 개발은 진행해야 하기 때문에 위에서 말했던 것처럼 가장 단순한 메모리 회원 저장소를 구현해서 우선 개발을 진행하도록 하였다.

 

※ 참고 

HashMap 은 동시성 이슈가 발생할 수 있다. 이런 경우 ConcurrentHashMap을 사용하면 된다.

 

 

다음으로 회원 서비스에 대한 부분을 구현하기 위해 마찬가지로 member 패키지 아래에 'MemberService' 인터페이스를 먼저 생성한 다음, MemberService에 대한 구현체를 작성하도록 하자.

 

 

회원 서비스

 

MemberService (회원 서비스)

package hello.core.member;

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

 

 

MemberServiceImpl (회원 서비스 구현체)

package hello.core.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);
    }
}

 

 

 

회원 도메인 실행과 테스트

 

회원 도메인 설계를 바탕으로 코드 작성을 통해 회원 도메인 개발을 모두 완료하였다. 이제는 회원 도메인을 실행하고 직접 테스트를 진행할 것이다.

 

 

회원 도메인 -  회원 가입 main

 

회원 도메인 실행을 위해 member 클래스 밑에 'MemberApp'이라는 클래스를 만들고, 메인 메서드를 통해 실행해 보도록 하자.

 

 

MemberApp

package hello.core;
import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;

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(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());
    }
}

 

 

MemberApp 실행 결과

 

 

 

실행 결과를 통해 새로 가입한 멤버와 회원의 아이디를 통해 찾은 멤버가 동일한 것을 확인할 수 있다.

 

현재는 스프링에 관련된 코드는 아무것도 없이 순수한 자바 코드로만 개발을 진행한 상태이다. 하지만, 애플리케이션 로직으로 테스트하는 것은 한계가 있기에 좋은 방법이 아니다.

 

따라서, Junit이라는 테스트 프레임워크를 통해 테스트를 진행해 볼 것이다.

 

 

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

 

테스트를 위해서는 main 패키지가 아닌, test 패키지에 테스트 코드를 작성해야 한다는 것에 유의하자.

 

 

 

다음과 같이 test 패키지 아래의 hello.core 패키지에 'MemberServiceTest'라는 이름의 클래스를 만들어 테스트 코드를 작성하면 된다.

 

테스트 코드 작성 시, 메서드 명 위에 @Test 어노테이션을 선언해주어야 한다.

 

추가로, 테스트 코드를 작성할 때, 다음과 같은 형식으로 작성하는 것이 좋다.

  • given
    • 이러한 환경이 주어졌을 때

 

  • when
    • 이렇게 했을 때

 

  • then
    • 이렇게 된다. (검증)

 

 

코드를 통해 좀 더 자세히 살펴보도록 하자.

 

 

MemberServiceTest

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
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);
    }
}

 

해당 코드는 회원 가입 시에 회원 가입한 member와 회원의 아이디를 통해 찾은 findMember가 서로 같다면, 테스트에 성공하는 코드이다. Assertions를 통해 이에 대한 검증을 진행할 수 있다.

 

 

MemberServiceTest 실행 결과

 

 

다음과 같이 녹색 불이 들어온다면, 테스트에 성공했다는 것을 의미한다.

 

Member findMember = memberService.findMember(2L);

 

만약, findMember에서 찾고자 하는 멤버의 id를 1L 에서 2L로 변경할 경우,

 

 

 

테스트에 실패하게 된다.

 

요즘, 현대적인 애플리케이션 개발을 하기 위해서는 테스트 코드의 작성 방법은 선택이 아닌, 필수로 알아야 한다.

마찬가지로, 실무에서 개발자들의 화면을 보면, 대다수의 개발자가 테스트 코드를 개발하고 있다고 한다. 그만큼 테스트 코드의 작성은 중요하니 잘 알아두도록 하자!  

 

 

회원 도메인 설계의 문제점 

 

여태까지 우리는 회원 도메인을 설계하고, 설계한 내용을 바탕으로 개발을 해본 다음, 실제 실행과 테스트를 해보았다. 하지만, 회원 도메인 설계에는 다음과 같은 문제점이 존재한다.

 

  • 이 코드의 설계상 문제점은 무엇일까?

 

  • 다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까?

 

  • DIP를 잘 지키고 있을까?

 

  • 의존관계가 인터페이스뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.
    • 주문까지 만들고 나서 문제점과 해결 방안에 대해 알아보도록 하자.

 

 

 

주문과 할인 도메인 설계

 

주문과 할인 도메인을 설계하기 위해 먼저 주문과 할인 정책에 대해 살펴보자.

 

 

주문과 할인 정책

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

 

 

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

 

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

 

 

  1. 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다.
  2. 회원 조회: 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.
  3. 할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.
  4. 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.

 

※ 참고

실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해질 수 있어서 생략하고, 단순히 주문 결과를 반환한다.

 

 

주문 도메인 전체

 

주문 도메인 전체

 

역할과 구현을 분리해서 주문 도메인 전체를 설계한 모습은 다음과 같다. 자유롭게 구현 객체를 조립할 수 있게 설계함에 따라 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.

 

 

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

 

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

 

이를 클래스 다이어그램으로 나타내면 OrderService라는 인터페이스가 있고, 이에 대한 구현체로 OrderServiceImpl을 가진다. OrderServiceImpl은 MemberRespository와 DiscountPolicy 인터페이스에 의존하며, DiscountPolicy의 구현으로 FixDiscountPolicy와 RateDiscountPolicy로 나뉘게 된다.

 

 

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

 

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

 

객체 다이어그램을 살펴보면, 회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 된다.

 

 

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

 

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

 

다음과 같이 메모리 회원 저장소에서 회원을 실제 DB에서 조회하고, 정액 할인 정책 서비스를 정률 할인 정책으로 변경하더라도 주문 서비스 구현체를 변경할 필요가 없다.

 

 

 

주문과 할인 도메인 개발

 

할인 정책 

 

할인 정책에 대한 인터페이스를 만들기 위해 main 패키지의 hello.core 패키지 아래에 'discount' 패키지를 생성하고, 그 아래에 'DiscountPolicy' 인터페이스를 생성하자.

 

 

DiscountPolicy (할인 정책 인터페이스)

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

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

}

 

회원과 가격에 대한 정보를 넘겼을 때, 할인된 금액을 반환한다. 할인 정책에 대한 내용을 살펴보면, 할인 정책은 모든 VIP는 1000원을 할인해 주는 고정 금액 할인을 적용하는 정액 할인 정책이므로, 이에 대한 구현체로 'FixDiscountPolicy' 클래스를 생성하여 코드를 작성할 것이다.

 

 

FixDiscountPolicy (정액 할인 정책 구현체)

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

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원을 반환하고, 그렇지 않으면(vip가 아니라면) 할인 금액으로 0원을 반환하는 코드이다. 

 

 

주문 엔티티 

 

다음으로 주문 엔티티에 대한 개발을 위해 hello.core 패키지 아래에 order 패키지를 만들고, 주문 엔티티인 'Order' 클래스를 생성하여 코드를 작성하자.

 

 

Order (주문 엔티티)

package hello.core.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 +
                '}';
    }
}

 

주문 엔티티는 회원의 아이디, 주문할 상품명과 가격, 할인 가격이라는 총 네 가지의 필드를 갖는다. 추가적으로, 할인된 금액을 적용하여 최종적인 주문 가격을 반환하는 calculatePrice 메서드를 갖는다. 

 

 

주문 서비스

 

먼저, 주문 서비스에 대한 인터페이스를 작성하기 위해 'OrderService' 인터페이스를 생성하여 코드를 작성한다.

 

 

OrderService (주문 서비스 인터페이스)

package hello.core.order;

public interface OrderService {
    Order createOrder(Long memberId, String itemName, int itemPrice);
}

 

해당 코드를 통해 클라이언트가 주문을 생성할 때, 회원의 아이디와 상품명, 상품 가격을 파라미터로 넘기게 되고, 주문 서비스는 이에 대한 주문 결과를 반환한다.

역할을 만들었으니, 이번엔 이에 대한 구현체로 'OrderServiceImpl' 구현체 코드를 작성하자.

 

 

OrderServiceImpl (주문 서비스 구현체)

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;
import hello.core.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);
        int discountPrice = discountPolicy.discount(member, itemPrice);

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

 

주문 생성 요청이 오면, 회원 정보를 먼저 조회한 다음 할인 정책에 회원을 넘겨 최종적으로 할인된 가격을 반환받아 최종적으로 생성된 주문을 반환하게 된다.

 

 

 

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

 

회원 도메인을 개발하고 나서 실행과 테스트를 진행했던 방식과 동일하게 이번에도 주문과 할인 도메인에 대한 실행과 테스트를 진행해 볼 것이다.

 

 

주문과 할인 정책 실행

 

먼저 실행을 위해 hello.core 패키지 밑에 'OrderApp'이라는 클래스를 만들어 코드를 작성하자.

 

 

OrderApp

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.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);
        System.out.println("order.calculatePrice = " + order.calculatePrice());
    }
}

 

VIP 등급의 회원을 만들고, 가격이 만 원인 itemA를 주문하였을 때, 실행 결과가 어떻게 되는지 콘솔 창에 출력하여 확인하여 보자.

 

 

실행 결과

 

 

실행 결과를 통해 우리가 의도한 대로 주문과 할인이 제대로 된 걸 볼 수 있다. 하지만, 메인 메서드를 통해 확인하는 건 바람직하지 않기 때문에 테스트 코드를 작성하여 테스트해보자.

 

 

주문과 할인 정책 테스트

 

테스트 코드를 작성하기 위해 test의 hello.core 아래에 'order' 패키지를 생성하고, 그 아래에 'OrderServiceTest' 클래스를 생성하여 테스트 코드를 작성하자.  

 

 

OrderServiceTest

package hello.core.order;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.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() {
        //given
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);

        //when
        memberService.join(member);
        Order order = orderService.createOrder(memberId, "itemA", 10000);

        //then
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

 

등급이 vip인 멤버가 가격이 만 원인 itemA를 주문했을 때, 할인 가격이 1000원이 맞다면, 테스트에 성공하게 된다.

 

 

 

다음과 같이 정상적으로 테스트에 성공하는 모습을 확인할 수 있다.

 

 

 

마찬가지로, 전체 테스트 코드를 실행했을 때도 테스트에 성공하게 된다.

 

 

 


 

지금까지 다형성을 활용해 역할과 구현을 철저히 분리하여 순수한 자바 코드로 설계 및 개발을 진행하였다.

하지만, 정액 할인 정책에서 정률 할인 정책으로 변경하게 될 경우, 깔끔하게 변경할 수 있을까?

정답부터 말하자면, 그렇지 않다. 

 

다음에는 새로운 할인 정책을 개발하고, 이를 적용했을 때 생기는 문제점과 해결 방법에 대해 살펴볼 것이다.