본문 바로가기
스프링/스프링 핵심 원리 기본편

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

by 위대한초밥V 2023. 7. 8.

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

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

 

프로젝트 생성

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

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

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

비즈니스 요구사항과 설계

회원, 주문과 할인 정책에서 어떤 요구사항을 충족해야하는지 설계한다.

 

물론! 처음 설계한 내용이 후에 바뀔 수 있다. 그렇다고 무한정 기다릴 수 없으니 앞에서 배운 객체 지향 설계 방법으로 인터페이스를 만들고 구현체를 언제든지 갈아끼울 수 있도록 설계한다.

 

회원 도메인

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

요구사항
- 회원 가입 및 조회
- 회원 등급은 일반과 VIP 두 등급 존재
- 회원 데이터는 자체 DB 구축 가능 혹은 외부 시스템과 연동 가능(미확정) -> 회원 데이터에 접근하는 것을 별도로 만든다.

 

회원 도메인 설계

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

 

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

 

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

 

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

 


회원 도메인 개발

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

 

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

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

테스트 클래스를 생성하고 memberService와 orderService를 생성한다.

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

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

반응형