[Spring] 핵심원리 기본편 - 스프링 핵심 원리 이해1 (예제만들기)
Java & Spring

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

인프런 - 스프링 핵심 원리 (김영한) 강의를 듣고 간단하게 정리한 글입니다.

준비물

Java 11

Intellij

 


Project 생성

1. https://start.spring.io/ -> 아래와 같이 세팅하고 generate 클릭

 

2. 생성된 zip 파일을 압축해제해서 intellij로 띄우면 다음과 같은 폴더 구조가 생성되어 있음


설계부분은 생략

중요한건 역할과 구현을 분리하는 것!


회원 도메인 개발

member폴더에 아래와 같이 개발한다.

  • Grade: BASIC, VIP 정의
  • Member: 도메인
  • MemberRepository: 레포지토리 인터페이스
  • MemoryMemberRepository: MemberRepository를 구현한 것
  • MemberService: 서비스 인터페이스
  • MemberServiceImpl: MemberService를 구현한 것

 

 

Grade.java

- 회원 등급은 일반, VIP 등급 2가지로 나눠서 enum으로 정의해줬다.

package hello.core.member;

public enum Grade {
	BASIC,
	VIP
}

Member.java

- member는 id, name, grade 3가지 속성이 있다.

- 생성자와 getter, setter를 만들어줬다.

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

MemberRepository.java

- 레포지토리 인터페이스에 회원을 저장하는 save와 아이디로 회원을 찾는 findById를 추가해줬다.

package hello.core.member;

public interface MemberRepository {

	void save(Member member);

	Member findById(Long memberId);
}

MemberService.java

- 서비스 인터페이스에 회원 가입을 하는 join과 회원을 찾는 findMember 메서드를 선언해줬다.

package hello.core.member;

public interface MemberService {

	void join(Member member);

	Member findMember(Long memberId);
}

MemberServiceImpl.java

- 위에서 추가해준 MemberService를 상속받아서 메서드들을 오버라이드 해주고 구현해줬다.

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

MemoryMemberRepository.java

- 위에서 추가해준 MemberRepository를 상속받아서 메서드들을 오버라이드 해주고 구현해줬다.

- 아직 디비가 결정이 안됐다고 가정했기 때문에 메모리에 데이터를 저장하였고, 클래스 이름을 MemoryMemberRepository라고 정의해줬다.

- member는 hashMap으로 <memberID, member 객체> 형태로 저장하였다.

- 실무에서는 동기화의 문제로 concurrentHashMap을 사용한다고 한다.

package hello.core.member;

import java.util.HashMap;
import java.util.Map;

// 아직 디비가 결정이 안됐다고 가정 -> 우선 memory로 구현
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);
	}
}

회원 도메인 실행과 테스트

순수 JAVA 코드로 테스트

- member join을 통해 회원가입을 하고 findMember로 회원가입이 잘 되었는지 확인해줬다.

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

순수 JAVA 코드 테스트 결과

Junit으로 테스트

Junit?

  • 자바 프로그래밍 언어용 단위 테스트 도구
  • Assertions가 org.assertj.core.api.Assertions인지 import를 잘 체크해줘야 한다.
// main이 아닌 test폴더 아래에. test.java.hello.core.member.CoreApplicationTests.java

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 테스트 결과

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

  • 다른 저장소로 변경할 때 OCP 원칙을 잘 준수하는지? -> 내 생각: 코드를 변경해줘야 함. 변경에 닫혀있지 않음. 잘 준수하지 못함.
  • DIP를 잘 지키고 있는지? -> 내 생각: 구현클래스를 직접 선택함. 잘 못 지킴.
  • 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있음

주문과 할인 도메인 개발

설계할 때 역할과 구현을 분리해서 설계

discount 폴더에

  • DiscountPolicy.java: 할인정책 서비스 인터페이스
  • FixDiscountPolicy.java: 할인정책 서비스 구현체

order 폴더에

  • Order.java: 주문 도메인
  • OrderService.java: 주문 서비스 인터페이스
  • OrderServiceImpl.java: 주문 서비스 구현체

 

 

 

DiscountPolicy.java

- 할인 정책 인터페이스에 discount  메서드를 추가해줬다.

package hello.core.discount;

import hello.core.member.Member;

public interface DiscountPolicy {

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

}

FixDiscountPolicy.java

- 위의 DiscountPolicy 인터페이스를 상속받아 1000원 할인이라고 금액을 정해주고, discount를 오버라이드 해줬다.

- discount 메서드에서는 만약 고객이 VIP이면 할인 금액을 리턴하고, 아닌 경우 할인이 안되므로 0을 리턴한다.

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.java

- Order에는 memberId, itemName, itemPrice, discountPrice가 들어간다.

- 생성자, Getter, Setter, 가격 계산하는 메서드를 추가해줬다.

- toString 메서드는 모든 클래스들의 가장 최상위 클래스인 Object가 가지고 있는 메서드로 객체가 가지고 있는 값들을 문자열로 만들어 리턴하는 메서드이다.

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

OrderService.java

- 주문 서비스 인터페이스에 createOrder 메서드를 정의해주었다.

package hello.core.order;

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

OrderServiceImpl.java

- 위에서 정의한 createOrder 메서드를 오버라이드해서 주문을 생성하는 부분을 구현하고 생성한 주문을 리턴해주었다.

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

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

순수 JAVA 코드

// main/java/OrderApp.java

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

순수 JAVA 코드 실행 결과

Junit으로 테스트

// test/java/hello.core/order/OrderServiceTest.java

package hello.core.order;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

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

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

결과


결론

오늘 학습한 내용 폴더 구조

  • 회원, 주문, 할인 정책이 있는 간단한 서비스를 개발해봄
  • 인터페이스(역할)과 구현체(구현)를 분리 -> 다형성 잘 활용
  • 하지만 다른 구현체로 바꿀때? -> 다음시간에!
728x90
반응형