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

유진·2024년 6월 29일
0

스프링 기본편

목록 보기
2/9
post-thumbnail

출처 : 인프런 > 스프링 핵심 원리 - 기본편 강의를 듣고 작성한 글입니다.
강의 링크 : 스프링 핵심 원리 - 기본편

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

📘 프로젝트 생성

아래와 같이 프로젝트 초기 설정을 해주었다.

순수한 자바 프로젝트로 실행하기 위해 dependencies는 넣지 않았다.

📘 비즈니스 요구사항과 설계

다음은 순수한 자바 코드만 이용해 역할과 구현을 나누고 실제 요구상황이 나올때 얼마나 자연스럽게 바꿀수 있는지를 확인하기 위해 시작하는 예제이다.

📘 회원 도메인 설계

회원 도메인 요구사항

📖 회원 도메인 협력 관계

📖 회원 클래스 다이어그램

📖 회원 객체 다이어그램

그림은 위와 같이 개념적으로 크게 회원 도메인 협력 관계, 회원 클래스 다이어그램, 회원 객체 다이어그램 3가지로 그려진다. 도메인 협력 관계는 기획자들도 볼 수 있는 그림이다. 이를 바탕으로 개발자가 구체화해서 인터페이스랑 구현체를 볼 수 있는 클래스 다이어그램을 만들어낸다. 객체 다이어그램은 실제 서버를 실행하지 않고 클래스들만 분석해서 볼 수 있는 그림이다.

MemoryMemberRepository를 넣을지 DbMemberRepository를 넣을 지는 서버가 뜰 때 new를 해서 동적으로 결정되기 때문에 클래스 다이어그램으로만으로는 판단이 어렵다. 그래서 객체 다이어그램을 따로 두어 실제 클라이언트가 사용하는 MemberServiceImpl, MemberRepository를 나타내고 실제 new한 인스턴스끼리의 참조를 볼 수 있다.

📘 회원 도메인 개발

📖 회원 엔터티

  • 회원 등급
    src\main\java\hello\core\member\Grade.java
package hello.core.member;

public enum Grade {
    BASIC,
    VIP
}
  • 회원 엔터티
    src\main\java\hello\core\member\Member.java
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;
    }
}

가장 먼저 설정해둔 private 변수를 이용해서 Member Constructor, GetterSetter를 만들 수 있다.
Constructor, GetterSetter 단축키 : Alt + Insert

📖 회원 저장소

  • 회원 저장소 인터페이스
    src\main\java\hello\core\member\MemberRepository.java
package hello.core.member;

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);
}
  • 메모리 회원 저장소 구현체
    src\main\java\hello\core\member\MemoryMemberRepository.java
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);
    }
}

인터페이스를 implements 한 구현체이다. Override 해서 인터페이스에 method를 전부 import 해주었고 DB에서 다르게 쓰일 수 있게 Map, save, findById 방법을 각각 맞게 써주었다.
데이터베이스가 아직 확정이 안되었다. 그래도 개발은 진행해야 하니 가장 단순한, 메모리 회원 저장소를 구현해서 우선 개발을 진행하자.


📖 회원 서비스

  • 회원 서비스 인터페이스

src\main\java\hello\core\member\MemberService.java

package hello.core.member;

public interface MemberService {

    void join(Member member);

    Member findMember(Long memberId);
}

회원 서비스는 회원 가입과 회원 조회 기능을 제공한다.

  • 회원 서비스 구현체
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);
    }
}

구현체를 만들어주었고 서비스 구현체는 MemberRepository에 접근을 하여 멤버를 조인해주는 역할을 수행한다. 따라서 구현체 안에 MemberRepository interface 오브젝트를 만들어준 다음 new MemoryMemberRepository()로 설정하여 만들었던 MemberRepository에 구현체를 설정했다. MemoryMemberRepository에 있는 @Override한 save, findMember가 호출이 된다.

📘 회원 도메인 실행과 테스트

📖 회원 도메인 - 회원 가입 main

hello.core package에 MemberApp을 만들어준다.

src\main\java\hello\core\MemberApp.java

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();
        // id는 long type이라 1L 그냥 1이라 쓰면 error
        Member member = new Member(1L, "memberA", Grade.VIP); 
        memberService.join(member);

        // 위에서 join한 Member와 findMember가 같은 지 확인
        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member.getName());
        System.out.println("find Member = " + findMember.getName());
    }
}

위 코드는 순수한 자바코드로 스프링 관련 코드가 하나도 없다. 위에서는 애플리케이션 로직으로 MemberApp을 만들어서 테스트 하는걸 보여줬지만 JUnit 테스트를 활용하는게 더 올바른 방법이다.


+) soutv하면 쉽게 프린트문을 찍을 수 있다.


실행을 해보면 위와 같이 새롭게 가입한 멤버와 조회한 멤버가 memberA로 동일한 것을 확인할 수 있다.

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

src\test\java\hello\core\member\MemberServiceTest.java

package hello.core.member;

import org.assertj.core.api.Assertions;
import org.hamcrest.core.IsEqual;
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);
    }
}

JUnit 테스트 프레임을 사용해 위와 같이 테스트 코드를 만들었다. 이는 콘솔을 보면서 출력된 결과를 검증하는게 아니라 테스트를 실패하면 오류가 발생해 빨리 캐치가 가능하다. 애플리케이션을 개발하려면 테스트 코드는 선택이 아니라 필수이다.


실행을 해보면 위와 같이 정상적으로 작동된다.

📖 회원 도메인 설계의 문제점

  • 이 코드의 설계상 문제점은 무엇일까요?
  • 다른 저장소로 변경할 때 OCP(Open-Closed Principle) 원칙을 잘 준수할까요?
  • DIP(Dependency Inversion Principle)를 잘 지키고 있을까요?
  • 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음
    -> 주문까지 만들고나서 문제점과 해결 방안을 설명

src\main\java\hello\core\member\MemberServiceImpl.java

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();

    ... 
}

MemberServiceImpl을 확인해보자. 왼쪽에 있는 MemberRepository는 인터페이스를 의존하지만 오른쪽에 있는 실제 할당하는 부분인 MemoryMemberRepository가 구현체를 의존한다. 따라서 둘다 의존 즉, 추상화에도 의존하고 구체화에도 의존하기 때문에 나중에 변경했을 때 문제가 발생한다. -> DIP를 위반

📘 주문과 할인 도메인 설계

📖 주문과 할인 정책

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

클라이언트는 시작과 동시에 주문 서비스에 주문 생성(회원 id, 상품명, 상품 가격)을 요구한다. 그리고 주문 서비스는 회원 등급을 확인하기 위해 먼저 회원 저장소에서 회원을 조회하고, 등급에 따라 할인 정책을 적용한다. 마지막으로 DB에 주문을 저장하는 것이 아니라 주문 결과를 반환한다.

📖 주문 도메인 전체

위에는 이제 역할과 구현까지 그린 그림이다. 역할을 먼저 만들고 구현을 그 다음에 만들었다. 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다. 역할 밑에 점선으로 이어진게 구현체인데 각 역할에는 두개의 구현체가 있다. 이는 구현체가 확정 된 것이 아니라 변경 가능하기 때문이다.

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


interface에 대한 구현체가 딱 하나만 있으면 보통 뒤에 Impl이라고 적는다. MemberRepository interface에에 접근하여 회원 정보를 확인한 후 DiscountPolicy interface에 접근하여 해당 구현체에 맞게 할인 정책을 따를 것이다.

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

클래스 다이어그램과 다르게 객체 다이어그램에서는 실제 new 생성을 하여 application을 띠워 동적으로 객체들의 연관 관계가 맺어지는 그림이다. 추후에 바뀔 수 있는 요구사항에 따라 객체 다이어그램도 바뀔 수도 있다는 것을 표현한 것이다.

📘 주문과 할인 도메인 개발

📖 할인 정책 인터페이스

src\main\java\hello\core\discount\DiscountPolicy.java

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

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


}

📖 정액 할인 정책 구현체

src\main\java\hello\core\discount\FixDiscountPolicy.java

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원 할인, 아니면 할인 없음

📖 주문 엔티티

src\main\java\hello\core\order\Order.java

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 String getItemName() {
        return itemName;
    }

    public int getItemPrice() {
        return itemPrice;
    }

    public int getDiscountPrice() {
        return discountPrice;
    }

    @Override
    public String toString() {
        return "Order{" +
                "memberId=" + memberId +
                ", itemName='" + itemName + '\'' +
                ", itemPrice=" + itemPrice +
                ", discountPrice=" + discountPrice +
                '}';
    }
}

📖 주문 서비스 인터페이스

src\main\java\hello\core\order\OrderService.java

package hello.core.order;

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

📖 주문 서비스 구현체

src\main\java\hello\core\order\OrderServiceImpl.java

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);
        
        // 그 다음 할인 정책에 member 넘기기
        int discountPrice = discountPolicy.discount(member, itemPrice);
        
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

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

📖 주문과 할인 정책 실행

src\main\java\hello\core\OrderApp.java

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

MemberApp과 동일하게 OrderApp도 만들어준다.


위와 같이 실행을 해보면 잘 출력되는 것을 확인할 수 있다. 하지만 앞서 말한 것처럼 애플리케이션 로직으로 MemberApp을 만들어서 테스트 하는 것보다 JUnit 테스트를 활용하는게 더 올바른 방법이다.

📖 주문과 할인 정책 테스트

src\test\java\hello\core\order\OrderServiceTest.java

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

JUnit 프레임을 이용해서 테스트를 주문 서비스 테스트를 해보았다.

위와 같이 테스트가 정상적으로 실행되었다.


마지막으로 전체 테스트도 실행 해보았다.

profile
유진진입니덩

0개의 댓글