김영한 | 스프링 핵심 원리 - 기본편을 참조한 글입니다.
1. 프로젝트 생성
아래의 스프링 부트 스타터 사이트를 이용하면 프로젝트를 편리하게 생성할 수 있다.
https://start.spring.io/
사진과 동일하게 설정하고 generate한다.
2. 비지니스 요구사항과 설계
2-1. 회원
- 회원 가입과 조회가 가능해야 한다.
- 회원 등급은 일반 / VIP 로 나뉜다.
- 회원 데이터는 자체 DB 를 구축할 수도 있고, 외부 시스템과 연동될 수도 있다. (미확정)
2-2. 주문
- 회원은 상품을 주문할 수 있다.
2-3. 할인 정책
- 회원 등급에 따라 할인 정책이 다르게 적용된다.
- 모든 VIP 는 1000원을 할인해주는 고정 금액 할인이 적용된다. (나중에 변경 가능)
- 할인 정책은 변경 가능하다. 최악의 경우 할인을 적용하지 않을 수 있다. (미확정)
=> Then How?
미확정인 부분이 있으나 일단 개발은 해야하는 상황이므로, 인터페이스와 구현체를 이용하는 객체 지향 설계를 통해 프로그래밍해보자!
3. 회원 도메인 설계 / 4. 회원 도메인 개발
전체 파일 구성은 다음과 같다.
4-1. Grade
'main - java - hello.core'에 member패키지를 생성하고, 그 하위에 Grade.enum 파일을 생성한 뒤,
다음의 코드를 입력한다.
package hello.core.member;
public enum Grade {
BASIC,
VIP
}
<코드 설명>
회원 등급을 BASIC, VIP의 2가지로 나눴다.
4-2. Member
member패키지 하위에 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;
}
}
<코드 설명>
- 회원 속성 3가지 : id, name, grade
- Member 생성자와 getter, setter
4-3. MemberRepository
package hello.core.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
<코드 설명>
- save : 회원 저장
- findByID : ID로 회원 찾기
4-4. MemberService
package hello.core.member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
<코드 설명>
- join : 회원 가입
- findMember : ID로 회원 찾기
*findByID 메서드는 데이터베이스 대신 사용하는 메모리 저장소인 store에서 파라미터로 받은 memberId로 Member객체를 가져와서 반환함
*findMember 메서드는 파라미터로 받은 memberId를 통해 저장소에서 Member를 찾아서 반환함
강의 QnA참고
4-5. 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);
}
}
<코드 설명>
위의 MemberService를 상속(implments)받아서 메서드들을 Overide함
4-6. MemoryMemberRepository
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);
}
}
<코드 설명>
- 위에서 추가해준 MemberRepository를 상속받아서 메서드들을 오버라이드 해주고 구현해줬다.
- 아직 디비가 결정이 안됐다고 가정했기 때문에 메모리에 데이터를 저장하였고, 클래스 이름을 MemoryMemberRepository라고 정의해줬다.
- member는 hashMap으로 <memberID, member 객체> 형태로 저장하였다.
- 실무에서는 동기화의 문제로 concurrentHashMap을 사용한다고 한다.
5. 회원 도메인 실행과 테스트
5-1. 순수 JAVA 코드로 테스트
member join을 통해 회원가입을 하고 findMember로 회원가입이 잘 되었는지 확인해보자.
main - java - hello.core 에 order 패키지를 생성하고, 그 하위에 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());
}
}
5-2. Junit으로 테스트
지난 강의에서 우리는 웹 어플리케이션의 컨트롤러를 이용해 테스트하는 것보다 Junit으로 테스트하는 것이 더 좋은 방법이라고 배웠다. Junit으로도 테스트를 진행해보자.
main - java - hello.core 에 member 패키지를 생성하고, 그 하위에 MemServiceTest 클래스를 생성한다.
다음의 코드를 입력한다.
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);
}
}
5-2. 회원 도메인 설계의 문제점
- 다른 저장소로 변경할 때 OCP 원칙을 잘 준수하는지? -> ㄴㄴ. 코드를 변경해줘야 함. 변경에 닫혀있지 않음.
- DIP를 잘 지키고 있는지? -> ㄴㄴ. 구현클래스를 직접 선택함.
- 의존관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제점이 있으므로 이를 해결하기 위한 추가적인 코드 필요!!!!
6. 주문과 할인 도메인 설계 / 7. 주문과 할인 도메인 개발
앞서 살펴봤듯이, 주문과 할인 요구사항은 다음과 같다.
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책이 다르게 적용된다.
- 모든 VIP 는 1000원을 할인해주는 고정 금액 할인이 적용된다. (나중에 변경 가능)
- 할인 정책은 변경 가능하다. 최악의 경우 할인을 적용하지 않을 수 있다. (미확정)
전체 파일 구성은 다음과 같다.
7-2. DiscountPolicy
package hello.core.discount;
import hello.core.member.Member;
public interface DiscountPolicy {
/**
*
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
<코드 설명>
DiscountPolicy 인터페이스 내부에 멤버 객체와 가격을 넘겨주는 discount메서드를 추가함
7-2. 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;
}
}
}
<코드 설명>
- 위의 DiscountPolicy 인터페이스를 상속받아 1000원 할인이라고 금액을 정해주고, discount를 오버라이드해줬다.
- discount 메서드에서는 만약 고객이 VIP이면 할인 금액을 리턴하고, 아닌 경우 할인이 안되므로 0을 리턴한다.
7-3. 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 +
'}';
}
}
<코드 설명>
- Order에는 memberId, itemName, itemPrice, discountPrice가 들어간다.
- 생성자, Getter, Setter, 가격 계산하는 메서드를 추가해줬다.
- toString 메서드는 모든 클래스들의 가장 최상위 클래스인 Object가 가지고 있는 메서드로 객체가 가지고 있는 값들을 문자열로 만들어 리턴하는 메서드이다.
7-4. OrderService
package hello.core.order;
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
<코드 설명>
OrderService 인터페이스에 createOrder 메서드 추가
7-5. 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);
}
}
위에서 정의한 createOrder 메서드를 오버라이드해서 주문을 생성하는 부분을 구현하고 생성한 주문을 리턴해주었다.
8. 주문과 할인 도메인 실행과 테스트
8-1. 순수 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);
}
}
8-2. Junit으로 테스트
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);
}
}
오늘은 회원, 주문, 할인 정책이 있는 간단한 프로그램을 개발해보았다.
인터페이스와 구현체를 분리함으로써 다형성을 잘 활용했다.
그런데 만약 다른 구현체로 바뀔 때는 상황이 어떻게 될까? 다음 시간에 알아보자...

'back-end > 김영한 | 스프링 기본' 카테고리의 다른 글
1. 객체 지향과 스프링 [김영한 | 스프링 핵심 원리 - 기본편] (0) | 2023.01.31 |
---|