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

wnajsldkf·2022년 9월 8일
0

김영한 선생님의 스프링 핵심 원리 - 기본편 강의를 듣고 정리하였습니다.

프로젝트를 생성하고 요구사항을 확인하고 설계한다. 회원, 주문, 할인 도메인을 설계하고 개발한 후, 실행 및 테스트한다.

프로젝트 생성

본 프로젝트는 순수한 자바 코드로 프로젝트를 작성한다. 다만 초기 환경 설정을 위해 spring initializer로 프로젝트를 설정하도록 하겠다.

Dependency 설정을 제외하고 설정한 후, GENERATE 버튼을 클릭한다.

  • Spring Boot 버전은 SNAPSHOT, M 접미사가 붙은 경우를 제외한다.
    • SNAPSHOT: 아직 개발 중인 버전으로, 언제든지 기능이 추가되고 삭제될 수 있으 불안정한 버전
    • M(Milestone build): 완전하지 않은 기능이 포함된 버전

비즈니스 요구사항과 설계

회원, 주문과 할인 정책에서 어떤 요구사항을 충족해야하는지 설계한다.
물론! 처음 설계한 내용이 후에 바뀔 수 있다. 그렇다고 무한정 기다릴 수 없으니 앞에서 배운 객체 지향 설계 방법으로 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계한다.

회원 도메인

비즈니스에서 주요 도메인은 크게 회원, 주문과 할인 두가지로 구분하였다. 첫번째로 회원 도메인이다.

요구사항
- 회원 가입 및 조회
- 회원 등급은 일반과 VIP 두 등급 존재
- 회원 데이터는 자체 DB 구축 가능 혹은 외부 시스템과 연동 가능(미확정)

회원 도메인 설계

도메인의 요구사항을 정했으면, 도메인의 협력 관계, 클래스 다이어그램, 객체 다이어그램을 결정한다.

  • 도메인 협력 관계: 기획자도 볼 수 있는 그림으로 요구사항 분석 과정에서 소통 도구로 사용된다.

  • 클래스 다이어그램: 도메인 다이어그램을 바탕으로 더 구체화하여 서버를 실제로 실행하지 않고 클래스의 의존 관계만 보고 그릴 수 있다.

  • 객체 다이어그램: 객체들의 연관관계를 표현한 그림으로, 실제 서버를 띄웠을 때 생성한 인스턴스끼리의 참조관계를 표현한다.

회원 도메인 개발

member 패키지를 생성한다.

hello.core
ㄴ member

member 패키지 아래에 다음의 내용들을 작성한다.

회원 등급을 갖는 Enum 타입의 Grade를 생성하고 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;
    }
    ...
}

MemberRepository 인터페이스를 생성한다.
repository 패키지는 DB에 접근하는 코드의 모음이라고 생각하면 된다.

package hello.core.member;

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);
}

인터페이스를 작성했으니 이번에는 구현체 클래스인 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);
    }
}

save, findById 메서드는 인터페이스를 오버라이드해서 정의한다.

다음은 MemberService를 생성한다.
service 패키지는 DB에 접근하는 코드는 repository에 위임하고, 비즈니스 로직과 관련된 모든 코드라고 생각하면 된다. service와 repository 차이가 잘 이해가지 않았는데 강의 답변을 통해 이해했다.

이렇게 하면 비즈니스 로직과 관련된 부분에 문제가 발생했을 때는 service 패키지를 DB 접근과 관련된 문제는 repository를 확인하면 된다.

package hello.core.member;

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

인터페이스를 작성했으니 이번에는 구현체 클래스인 MemoryServiceImpl를 생성한다.

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

회원 도메인 실행과 테스트

이제 회원 도메인 실행을 위한 코드들이 모두 작성되었다. 도메인 실행을 위한 코드를 작성한다.
MemberApp 파일을 생성한다.

hello.core
ㄴ member
  ㄴ Grade
  ㄴ ...
  ㄴ MemoryMemberRepository
CoreApplication
MemberApp

새로운 Member를 생성하고 가입 로직을 실행한다.
+) 코드를 확인하면 서비스 로직인 memberService을 실행하는 것을 확인할 수 있다.

public class MemberApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);
    }
}

멤버가 잘 등록되었는지 확인한다. 모두 memberA가 출력되는 것을 확인할 수 있다.

Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find Member = " + findMember.getName());

이렇게 System.out.prinitln으로 확인하는 것은 너무 비효율적이다. 메번 Application 코드를 실행해야하기 때문이다.
우리는 JUnit을 이용해서 테스트 코드를 작성해 memberService를 테스트해보겠다.

테스트 코드를 작성하는 이유는 기능을 개발할 때 잘 구현되었는지 작성하고 리펙토링에 용의하기 때문이다. 이때 의도한 대로 기능이 정확하게 작동하는지 검증한다.
테스트 코드 작성 방식으로 given, when, then 방식을 주로 사용한다.

  • given
    테스트를 위해 준비를 하는 과정이다. 테스트에 사용하는 변수, 입력 값 등을 정의하거나 Mock 객체를 정의하는 구문도 보한된다.
  • when
    실제로 액션을 하는 테스트를 실행한다.
  • then
    테스트를 검증한다. 예상한 값이랑 실제로 실행해서 나온 값을 검증한다.
    출처: Given-When-Then Pattern

실행할 서비스를 불러온다.

public class MemberServiceTest {
    MemberService memberService = new MemberServiceImpl();
}    

테스트를 준비한다.

// given
Member member = new Member(1L, "memberA", Grade.VIP);

실제로 액션을 하는 테스트를 실행한다.

// when
memberService.join(member);
Member findMember = memberService.findMember(1L);

테스트를 검증한다.

// then
Assertions.assertThat(member).isEqualTo(findMember);

✅ 초록색 체크 표시와 함께 테스트가 올바르게 실행됨을 확인할 수 있다.

🤔 그렇지만 여기서 생각해 볼 부분이 있다!
MemberServiceImpl를 보면 MemberRepository 인터페이스 뿐만 아니라 실제 구현체까지 의존하는 것을 확인할 수 있다. 즉 추상화와 구체화 모두 의존된다. 변경되었을 때 문제가 되며, DIP를 위반하게 된다.

public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

주문과 할인 도메인

다음은 주문과 할인 도메인이다.

요구사항
- 상품 주문
- 회원 등급에 따른 할인 정책
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인 적용(변경 가능성 있음)
- 할인을 적용하지 않을 수도 있음(미확정)

주문과 할인 도메인 설계

도메인의 요구사항을 결정했으면, 도메인의 협력 관계, 클래스 다이어그램, 객체 다이어그램을 결정한다.

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

    역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계한다. 회원 저장소와 할인 정책은 유연하게 변경될 수 있다.

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

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

    협력 관계는 수정될 수 있다.

주문과 할인 도메인 개발

할인 정책을 담을 discount 패키지를 생성한다.

hello.core
ㄴ discount
ㄴ member

DiscountPolicy 인터페이스를 생성한다.

public interface DiscountPolicy {

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

인터페이스를 생성했으니 역할을 수행할 FixDiscountPolicy 객체를 생성한다.

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 등급인 경우에만 적용하므로, 해당하는 메서드를 작성한다.

주문을 관리할 order 패키지를 생성한다.

hello.core
ㄴ discount
ㄴ member
ㄴ order

주문을 관리하는 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 +
                '}';
    }
}

toString()을 오버라이드한 이유는 객체를 출력하여 객체가 올바르게 생성되었는지 확인하기 위함이다.

orderService 인터페이스를 생성한다.

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

orderService 역할을 수행할 orderServiceImpl 클래스를 생성한다.

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

이렇게하면 주문과 할인 도메인을 실행할 수 있다.

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

이제 작성한 기능을 실행하고 올바르게 동작하는지 확인해본다.
orderApp 파일을 생성한다.

hello.core
ㄴ member
ㄴ order
ㄴ discount
CoreApplication
MemberApp
OrderApp

새로운 memberService 객체와 orderService 객체를 생성한다.

public class OrderApp {
    public static void main(String[] args) {
        MemberService memberService = new MemberServiceImpl();
        OrderService orderService = new OrderServiceImpl();
    }
}

member를 가입시킨 후, 주문시킨다.

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

생성된 toString메서드로 객체에 담긴 내용들이 모두 출력되기 때문에 다음과 같이 결과를 확인할 수 있다.

이번에도 JUnit을 이용해서 테스트코드를 작성한다.

테스트 클래스를 생성하고 memberServiceorderService를 생성한다.

public class OrderServiceTest {
    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();
}    

@Test 어노테이션을 추가하고 테스트할 메서드를 작성한다.

@Test
void createOrder() {
	// given
    Long memberId = 1L;
    Member member = new Member(memberId, "memberA", Grade.VIP);
    memberService.join(member);

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

다음 시간에는 개발한 것이 정말 클라이언트에게 영향을 주지 않는지 확인해보자!

profile
https://mywnajsldkf.tistory.com -> 이사 중

0개의 댓글