[ 김영한 스프링 핵심 원리 - 기본편 #2 ] 스프링 핵심 원리 이해1 - 예제 만들기

김수호·2023년 8월 9일
1
post-thumbnail

이번에는 지금까지 앞에서 이야기했던 "역할"과 "구현"을 나눠서 순수한 자바로 개발을 해보자.

목차는 아래와 같다.

1) 프로젝트 생성
2) 비즈니스 요구사항과 설계
3) 회원 도메인 설계
4) 회원 도메인 개발
5) 회원 도메인 실행과 테스트
6) 주문과 할인 도메인 설계
7) 주문과 할인 도메인 개발
8) 주문과 할인 도메인 실행과 테스트

위와 같은 과정으로 스프링의 도움없이 역할과 구현을 나눠서 순수한 자바로 개발해보자. 그렇게 해서 요구사항이 추후에 변경되었을 때, 정말 유연하게 대처가 가능한지(=앞서 이야기한 다형성 + OCP, DIP가 잘 지켜지는지)를 보고, 다음 섹션인 [스프링 핵심 원리 이해2]에서 객체지향 원리를 적용해가면서 문제들을 해결해보자.


1) 프로젝트 생성

(참고) 스프링을 사용하지 않고 순수 자바로만 예제 실습을 진행하지만, 프로젝트 세팅할 때는 (추후 실습에 용이하도록) 스프링 부트 프로젝트를 생성한다.

  • 스프링 부트 스타터 사이트로 이동해서 스프링 프로젝트를 생성하자.

    • Project는 Gradle로 빌드를 할것이고,
    • Language는 Java를 사용할 것이다.
    • Spring Boot 버전은 2.7.14 버전을 사용한다. (강의에서는 2.3.3 버전을 사용했다.)
    • Project Metadata로는 아래와 같이 설정한다.
      • Group: hello
      • Artifact: core
      • Packaging: Jar
      • Java: 11
    • Dependencies는 아무것도 선택하지 않는다. (별도의 의존관계를 설정하지 않음.)
      • 아무것도 선택하지 않으면 스프링 부트는 spring-core 라이브러리와 몇가지 가장 간단한 구성만 세팅해준다.
    • Generate 버튼을 클릭하여 다운로드 받는다.
  • 다운로드 받은 파일을 압축 해제 후 intelliJ로 열어보자.

    • 프로젝트를 열고 build.gradle을 보면, 위에서 정의한 대로 설정되어 있는 것을 볼 수 있고, 의존관계는 테스트 관련 라이브러리와 spring-boot-starter만 사용되는 것을 볼 수있다.
    • 실행해보면 정상적으로 실행되는 것을 볼 수 있다.
      (스프링 웹 프로젝트 등을 넣은게 아니기 때문에, 실행하고 끝나야 정상이다.)

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

이번에는 비즈니스 요구사항과 설계를 진행해보자.

먼저 (회원, 주문, 할인정책)에 대한 요구사항이 있다.

👉 회원

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

👉 주문할인정책

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

요구사항을 보면 회원 데이터, 할인정책 같은 부분은 지금 결정하기 어려운 부분이다.
그렇다고 이런 정책이 결정될 때 까지 개발을 무기한 기다릴수도 없다.

앞에서 이야기했던 내용을 바탕으로, 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계해보자.

 

✔️ 참고

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

3) 회원 도메인 설계

이제 요구사항을 바탕으로 회원 도메인 설계를 진행해보자.

  • 회원 도메인 협력 관계
    • 클라이언트는 회원 서비스를 호출한다.
    • 회원 서비스는 두 가지 기능(회원가입, 회원조회)을 제공한다.
    • 회원 저장소는 자체 DB를 사용할 수도 있고, 외부 시스템과 연동할수도 있기에 별도로 구축한다. (= 회원 데이터에 접근하는 계층을 따로 만들어서 역할과 구현을 분리한다. 나중에 저장소가 정해지면, 그 구현체만 개발해서 교체한다.)
  • 회원 클래스 다이어그램
    • MemberService(회원 서비스)라는 역할을 인터페이스로 만들고, 그 구현체로 MemberServiceImpl를 생성한다.
    • MemberRepository(회원 저장소)라는 역할을 인터페이스로 만들고, 그 구현체로 MemoryMemberRepository 클래스와 DbMemberRepository 클래스를 생성한다.
  • 회원 객체 다이어그램
    • 클라이언트는 회원 서비스(MemberServiveImpl)를 바라보고,
    • 회원 서비스(MemberServiveImpl)은 메모리 회원 저장소(MemoryMemberRepository)를 바라본다.

✔️ 참고

  • 항상 위 그림은 개념적으로 크게 세가지로 그려진다.
    회원 도메인 협력 관계(=기획자들도 볼 수 있는 그림)를 바탕으로 개발자가 구체화 해서 클래스 다이어그램을 만든다.(=클래스 다이어그램에는 인터페이스나 구현체들이 다 보이게 된다.)
    • 클래스 다이어그램: 실제 서버를 실행하지 않고, 클래스들만 분석해서 볼수있는 그림.
    • 객체 다이어그램: 애플리케이션이 띄워질 때 동적으로 객체들의 연관관계가 맺어지는 그림.
      • (new 키워드로) 회원 저장소의 구현체로 MemoryMemberRepository를 넣을지, DbMemberRepository를 넣을지 하는 것은 서버가 뜰때 동적으로 결정되기 때문에, 클래스 다이어그램만으로 판단하기 어렵다. 그래서 객체 다이어그램이라는 것이 따로있다. (실제 new한 인스턴스끼리의 참조)

4) 회원 도메인 개발

이제 본격적으로 회원 도메인을 개발해보자.
(앞서 작성한 회원 클래스 다이어그램을 참고하여 만들어보자.)

  • 먼저 core 패키지 아래 member 패키지를 생성한다.
  • (member 패키지 아래) 회원 등급으로, Grade enum을 생성한다.
  • (member 패키지 아래) 회원 엔티티 클래스를 생성한다.
    • 회원의 속성은 id, name, grade로 하자.
    • 생성자와 getter/setter를 생성하자.
  • (member 패키지 아래) 회원 저장소 인터페이스를 생성한다.
  • (member 패키지 아래) 회원 저장소에 대한 메모리 저장소 구현체 클래스를 생성한다.
  • (member 패키지 아래) 회원 서비스 인터페이스를 생성한다.
  • (member 패키지 아래) 회원 서비스에 대한 구현체 클래스를 생성한다.
    • (이렇게 하면) 회원 서비스의 join 메소드를 호출해서 save를 호출하면, 다형성에 의해서 MemberRepository 인터페이스가 아닌, MemoryMemberRepository에 있는 오버라이드한 save가 호출된다.

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

정상적으로 동작하는지 확인해보자.

  • core 패키지 하위에 MemberApp 클래스를 생성하고, 다음과 같이 입력 후 실행해보자.
    • 정상적으로 실행된 것을 볼 수 있다.

☝️ 위 코드를 보면 스프링 관련 코드없이 순수하게 자바 코드로 작성해서 확인해보았다. 그런데 위와 같이 애플리케이션 로직으로 테스트 하는 것은 좋은 방법이 아니다. JUnit 테스트를 사용하자.

  • test > java > hello > core 아래 member 패키지를 생성하고, 내부에 MemberServiceTest 클래스를 생성하자. 그리고 다음과 같이 입력하고 실행해보자.
    • 정상적으로 수행된 것을 볼 수 있다.

 

✔️ 그런데 이 회원 도메인 설계에는 문제점이 있다.

  • 다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까?
  • DIP를 잘 지키고 있을까 ?
  • 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있다.
    • MemberServiceImpl은 MemberRepository 인터페이스를 의존하지만, 실제 할당하는 부분에서 구현체(MemoryMemberRepository)도 의존하고 있다. (=추상화에도 의존하고, 구현체에도 의존하고 있음.) 따라서 나중에 변경이 있을 때 문제가 될 수 있다.

👉 주문 도메인까지 만들고나서 이러한 문제점과 해결방안을 설명한다.


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

이번에는 요구사항을 바탕으로 주문과 할인 도메인을 설계해보자.

  • 주문 도메인의 협력, 역할, 책임
    • 1. 주문 생성 : 클라이언트는 주문 서비스에 주문 생성을 요청한다.
      (참고) 예제를 간단히 하기위해 상품에 대한 것은 별도로 만들지 않았다. 따라서 상품 정보인 상품명과 상품 가격은 데이터로 넘긴다.
    • 2. 회원 조회: 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.
    • 3. 할인 적용: 주문 서비스는 회원 등급에 따른 할인 가능 여부를 할인 정책에 위임한다. (할인 가능하면 할인해서 결과 알려달라.)
    • 4. 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.
      (참고) 실제로는 주문 데이터를 DB에 저장하겠지만, 예제가 너무 복잡해질 수 있어서 생략하고, 주문 서비스에서 단순히 주문 결과 객체를 만들어서 클라이언트에 보내는 것 까지만 해본다.
  • 주문 도메인 전체 (역할과 구현)
    • 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다. 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.
  • 주문 도메인 클래스 다이어그램
    • OrderService: (역할) 주문 인터페이스
    • OrderServiceImpl: (구현) 주문 인터페이스 구현체
    • MemberRepository: (역할) 회원 저장소 인터페이스
    • MemoryMemberRepository: (구현) 회원 저장소 구현체 (메모리 저장소)
    • DbMemberRepository: (구현) 회원 저장소 구현체 (DB 저장소)
    • DiscountPolicy: (역할) 할인 정책 인터페이스
    • FixDiscountPolicy: (구현) 할인 정책 인터페이스 구현체 (정액 할인 정책)
    • RateDiscountPolicy: (구현) 할인 정책 인터페이스 구현체 (정률 할인 정책)
  • 주문 도메인 객체 다이어그램(1)
    • 회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 된다. 역할들의 협력 관계를 그대로 재사용 할 수 있다.
  • 주문 도메인 객체 다이어그램(2)
    • 회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주문 서비스를 변경하지 않아도 된다. 협력 관계를 그대로 재사용 할 수 있다.

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

  • 이번에는 주문과 할인 도메인을 개발해보자.
  • 할인 도메인 개발
    • core 아래 discount 패키지를 생성한다.
    • discount 패키지 아래 DiscountPolicy 인터페이스를 생성한다.
    • discount 패키지 아래 DiscountPolicy의 구현체인 FixDiscountPolicy 클래스를 생성한다. (정액 할인 정책)
  • 주문 도메인 개발
    • core 아래 order 패키지를 생성한다.
    • Order 클래스를 생성한다. (memberId, itemName, itemPrice, discountPrice를 속성으로 지정하고, getter/setter/constructor/toString을 생성한다.)
      • Order 클래스에 최종 금액을 계산하는 메소드를 추가하자.
    • order 패키지 아래 OrderService 인터페이스를 생성한다.
    • order 패키지 아래 OrderService의 구현체인 OrderServiceImpl 클래스를 생성한다.

✔️ 참고

  • 주문 서비스 입장에서 보면, 회원 조회나 할인 금액에 대해서는 알지 못한다. 할인에 대한 것은 discountPolicy에 역할을 위임하고, 회원에 대한 것은 memberRepository에 역할을 위임한다. 따라서 단일 책임의 원칙이 잘 지켜졌다고 볼 수 있다.
    (추후 할인에 대한 수정이 있을 때는, 할인쪽만 수정하면 된다. 주문쪽의 수정은 불필요하다. 만약, 단일 책임의 원칙을 잘 지키지 않아 discountPolicy라는게 없었다면, 할인과 관련된 변경을 해야할 때 주문 서비스의 변경이 필요하게 된다.)

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

이제 주문과 할인 도메인을 실행하고 테스트해보자.

먼저, 주문이 잘 동작하는지 메인 메서드를 만들어보자.

  • core 아래 OrderApp 클래스를 생성후 실행해보자.
    • 정상적으로 출력됨을 확인할 수 있다.

☝️ 애플리케이션 로직으로 위와 같이 테스트 하는 것은 좋은 방법이 아니다. JUnit 테스트를 사용하자.

  • test > java > hello > core > order 패키지를 생성하고, 내부에 OrderServiceTest 클래스를 생성하자.
    • 정상적으로 실행됨을 확인할 수 있다.

다음에는 할인 정책의 구현체를 바꾸거나 했을 때, 우리가 지금 개발한 것이 정말 객체지향적으로 개발되었는지 확인해보자.


강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글