Java Spring Core

Spring에 적용되는 핵심 원리들을 이해하기 위해 예제를 구현

처음에는 순수 Java 형태의 코드 작성으로 시작해서 OOP, SOLID 원칙을 만족하기 위한 코드로 개선해가다 보면 어느 순간 Spring 프레임워크가 하는 역할에 대해서 이해할 수 있다.

Installation

(start.spring.io) 사이트에서 project을 만들어서 시작한다

Build.gradle

plugins {
	id 'org.springframework.boot' version '2.7.0'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '18'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

Simple Business Requirements

  1. 회원
    • 회원을 가입하고 조회할 수 있다.
    • 회원은 일반과 VIP 두 가지 등급이 있다.
    • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
  2. 주문과 할인 정책
    • 회원은 상품을 주문할 수 있다.
    • 회원 등급에 따라 할인 정책을 적용할 수 있다.
    • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
    • 할인 정책은 변경 가능성이 높다. 변경 가능성이 높다는 의미는 먼저 인터페이스로 필요한 기능들을 만들어 놓고, 가벼운 구현 클래스를 생성해서 이 기능들을 개발 가능할정도로 구현해놓는다.

Design

Member

member_domain

비지니스 요구사항에 따라 멤버 도메인을 살펴보면 위의 다이어그램으로 표현할 수 있다.

도메인 협력 관계 다이어그램의 경우 business logic 단에서 표현한 것으로 주로 기획자를 통한 수정이 이루어진다.

주로 개발에서 쓰이는 다이어그램은 클래스 다이어그램으로, 역할(interface), 구현(class)가 분리된 형태의 다이어그램이다.

Domain

Grade

package hello.core.member;

public enum Grade {
    BASIC,VIP
}

Member Domain

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;
    }
    //Getter& Setter 생략
}

Member 관련 비지니스 로직 수행을 위한 도메인 생성

Service

Member Service Interface

package hello.core.member;

public interface MemberService {
    void join(Member member);

    Member fingById(long memberId);
}

Member Service Implement

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 fingById(long memberId) {
        return memberRepository.findById(memberId);
    }
}

Repository

Repository Interface

package hello.core.member;

public interface MemberRepository {
    void save(Member member);
    Member findById(long memberId);

}

Memory Based Repository

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

개발 상에 구현 편의를 위해 임시적으로 메모리에 저장하는 방식활용

메모리에 저장하기 HashMap을 활용했는데, 실무에서는 동시성 문제를 고려해서 ConcurrentHashMap을 이용한다.

junit Test

Member Service Test

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.fingById(1L);
        
        //then
        Assertions.assertThat(member).isEqualTo(findMember);

    }
}

실제로 멤버를 생성해서 이를 저장한 다음, 저장된 결과를 비교해서 올바르게 저장되었는지를 junit 기반으로 테스트를 진행한다.

Discount

order_domain1 order_domain2

주문과 할인 정책 관련된 도메인 다이어그램은 위와 같다.

Policy Interface

package hello.core.discount;

import hello.core.member.Member;

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

Policy Interface Implement

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;

public class FixDiscountPolicy implements DiscountPolicy{
    private int discountFixAmount=1000;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade()== Grade.VIP){
            return discountFixAmount;
        }
        else{
            return 0;
        }
    }
}

할인 정책의 경우, 비지니스 로직에서도 변경의 여지가 높다고 하였으므로, 역할과 구현을 서로 분리시켜서 OCP,DIP을 만족시키도록 한다.

Order

Domain

Order Domain

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

    //Getter& Setter 생략

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

Service

Order Service Interface

package hello.core.order;

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

Order Service Implement

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 MemberRepository memberRepository = new MemoryMemberRepository();
    private 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);
    }
}

junit test

Order Service Test

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
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);
        Assertions.assertThat(order.calculatePrice()).isEqualTo(9000);
    }

}

References

link: inflearn

link:springcore

link:spring boot

link:lsp

댓글남기기