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

Yongjun OH·2025년 8월 27일

Spring

목록 보기
2/4
post-thumbnail

프로젝트 생성

사전 준비물

  • Java 11 설치
  • IntelliJ 또는 Eclipse 설치
  • Spring 프레임워크는 사용하지 않고 순수 Java로만 설정

프로젝트 생성

  • start.spring.io에서 프로젝트 생성 가능
  • Dependencies는 추가하지 않고 위와 같이 설정한 후 생성

Gradle 설정

  • 설정 -> gradle 검색 후 빌드 및 실행, 테스트 실행을 IntelliJ IDEA로 변경
  • gradle보다 IntelliJ IDEA가 더 빠름

비즈니스 요구사항과 설계

  • 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  • 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경될 수 있음)
    • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수도 있다 (미확정)

회원 도메인 설계

회원 도메인 협력 관계

기획

  • 클라이언트 : 회원 서비스 호출
  • 회원 서비스 : 회원가입, 회원 조회 기능 제공
  • 회원 저장소 : 회원 데이터에 접근하는 계층

회원 클래스 다이어그램

구체화(정적)

  • MemberService : 회원 서비스 역할을 인터페이스로 만듦
    • MemberServiceImpl : 회원 서비스 인터페이스를 구현
  • MemberRepository : 회원 저장소 역할을 인터페이스로 만듦

회원 객체 다이어그램

클라이언트가 실제 사용하는 인스턴스끼리의 참조(동적)

  • 메모리 간의 참조를 나타내는 다이어그램

--

회원 도메인 개발

열거형

Grade 열거

package hello.core.member;

public enum Grade {
    BASIC,
    VIP,
}

인터페이스

MemberService 인터페이스

package hello.core.member;

public interface MemberService {
    void join(Member member);

    Member findMember(Long memberId);
}

MemberRepository 인터페이스

package hello.core.member;

public interface MemberRepository {
    void save(Member member);

    Member findById(Long memberId);
}

클래스

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

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

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

회원 도메인 실행과 테스트

테스트 코드 생성

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());
    }
}
  • 눈으로 하는 테스트

MemberServiceTest 클래스

package hello.core.member;

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);
    }
}
  • JUnit을 이용한 테스트
  • main 폴더 안에 만드는 것이 아닌 test폴더 안에 만들어야 함
  • main 폴더에서 검증을 하면 출력된 결과를 보면서 검증하는 게 아님
  • test 폴더에서 돌리면 실패했을 때 문제점을 빠르게 캐치할 수 있음
  • 테스트 작성 방법은 필수로 알고 있어야 함

코드의 설계상 문제

  • 다른 저장소로 변경할 때 OCP 원칙을 잘 준수할까?
  • DIP를 잘 지키고 있을까?
  • 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음

주문과 할인 도메인 설계

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

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

주문 도메인 전체

  • 역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계함

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

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

  • 회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 됨
  • 역할들의 협력 관계를 그대로 재사용할 수 있음

  • 회원을 DB에서 조회하고, 정률 할인 정책(주문 금액에 따른 %)을 지원해도 주문 서비스를 변경하지 않아도 됨
  • 역할들의 협력 관계를 그대로 재사용할 수 있음

주문과 할인 도메인 개발

열거형

인터페이스

DiscountPolicy 인터페이스

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {
    // @return 할인 대상 금액
    int discount(Member member, int price);
}

OrderService 인터페이스

package hello.core.order;

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

클래스

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

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

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

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

테스트 코드 생성

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

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

}
profile
이것저것 하는걸 좋아합니다

0개의 댓글