start.spring.io에서 프로젝트를 생성한다. Spring Boot 3.0.0 version부터는 java 17이 필요하다. java 11을 사용할 경우 2.대 version을 사용해야 한다.
그러나 2.대 프로젝트를 생성했음에도 온갖 오류가 다 떴다..
재시도 할 때마다 오류 메시지가 달라 검색을 반복했지만 원인을 찾지 못했다.
‘스트링 입문’ 강의를 들으면서 만든 프로젝트는 빌드에 문제가 없었기 때문에, 해당 프로젝트의 build파일과 차이점을 비교해보니 version이 달랐다. 2.7.10 version을 2.7.9로 수정한 것 만으로 빌드가 정상적으로 이루어졌다.
Settings - Gradle (검색 통해 접근 가능)
- Build and run using, Run tests using: intellij IDEA로 변경
회원
주문과 할인 정책
=> 회원 데이터, 할인 정책: 지금 결정이 어려움
따라서, 인터페이스로 구현체를 갈아끼울 수 있도록 설계한다!
+) 지금은 순수한 자바로만 개발을 진행한다!!
< 회원도메인 요구 사항 >
MemberServiceImpl: 구현체
MemberRepository: 회원 저장소.
MemoryMemberRepository, DbMemberRepository: 구현체
도메인 협력 관계 : 기획자와 공유
클래스 다이어그램: 개발자가 대략적으로 구현. 실제 서버 개발 전 클래스 확인.
객체 다이어그램: 동적으로 결정되는 요소들. 서버가 뜰 때 new해서 뭘 넣을지가 결정되는 것들. 프로그램 실행 후 클라이언트가 사용하는 실제 객체(인스턴스)끼리의 참조를 그린 것. 클래스 다이어그램만으로는 이것들을 알기 힘들다.
setup-keymap에서 단축키 찾을 수 있음.
인터페이스 구현체가 한개만 있으면, 인터페이스명 뒤에 Impl이라고 붙여서 관례상 사용하곤 한다.
회원 등급
package hello.core.member;
public enum Grade {
BASIC,
VIP
}
회원 엔티티
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;
}
}
회원 저장소 인터페이스
package hello.core.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
메모리 회원 저장소 구현체
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);
}
}
회원 서비스 인터페이스
package hello.core.member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
회원 서비스 구현체
package hello.core.member;
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository = new MemoryMemberRepository();
// ctrl+shift+spacebar
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
}
📢 회원 도메인 설계의 문제점
- OCP 개방-폐쇄 원칙. Open/ Closed Prnciple
: 소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀있어야 한다.- DIP 의존관계 역전 원칙 Dependency Inversion Principle
: 추상화에 의존해야지 구체화에 의존하면 안된다.
= 클래스에 의존하지 말고, 인터페이스에 의존하라.
✔️ private final MemberRepository memberRepository = new MemoryMemberRepository();
💬 (내 생각)
DIP: 클래스랑 인터페이스를 동시에 의존하고 있어 DIP를 준수하지 못함
OCP: 각 클래스가 개별 변수를 private final로 설정하여 외부의 변경은 예방된다. 하지만 확장에 열려있지는 못한것 같다. 솔직히 확장에 열려있다는 말이 잘 이해가 안된다.. 수업을 더 들어봐야 할 것 같다.
-> 주문까지 만들고나서 문제점과 해결 방안을 설명
package hello.core.member;
import org.assertj.core.api.Assert;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
class MemberServiceImplTest {
MemberService memberService = new MemberServiceImpl();
// 인터페이스와 클래스 모두에게 의존하고 있음.
// 추성화, 구체화 모두에 의존하고 있음.
@Test
void join() {
// given
Member member = new Member(1l, "memberA", Grade.BASIC);
// when
memberService.join(member);
Member findMember = memberService.findMember(1L);
// then (검증 부분만 위치하도록 해야)
Assertions.assertThat(findMember).isEqualTo(member);
}
}
클라이언트: main코드, Controller
1. 주문 생성: 클라이언트는 주문 서비스에 주문 생성을 요청한다.
2. 회원 조회: 할인을 위해서는 회원 등급이 필요하다. 그래서 주문 서비스는 회원 저장소에서 회원을 조회한다.
3. 할인 적용: 주문 서비스는 회원 등급에 따른 할인 여부를 할인 정책에 위임한다.
4. 주문 결과 반환: 주문 서비스는 할인 결과를 포함한 주문 결과를 반환한다.
역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다. 덕분에 회원 저장소는 물론이고, 할인 정책도 유연하게 변경할 수 있다.
1번
회원을 메모리에서 조회하고, 정액 할인 정책(고정 금액)을 지원해도 주문 서비스를 변경하지 않아도 된다. 역할들의 협력관계를 그대로 재사용할 수 있다.
2번
회원을 메모리가 아닌 실제 DB에서 조회하고, 정률 할인 정책(주문 금액에 따라 % 할인)을 지원해도 주분 서비스를 변경하지 않아도 된다. 협력 관계를 그대로 재사용할 수 있다.
package hello.core.discount;
import hello.core.member.Member;
public interface DiscountPolicy {
/*
* @return 할인 대상 금액
*/
int discount(Member member, int price);
}
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) {
// ENUM은 ==으로 비교
return discountFixAmount;
} else {
return 0;
}
}
}
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 calculaterPrice() {
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 +
'}';
}
}
package hello.core.order;
public interface OrderService {
Order createOrder(Long memberId, String itemName, int itemPrice);
}
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);
}
}
주문생성요청이 오면, 회원정보를조회하고, 할인정책을 적용한 다음 주문 객체를 생성해서 반환한다.
메모리회원 리포지토리와, 고정금액할인 정책을 구현체로 생성한다.
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 hello.core.order.Order;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
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 crateOrder() {
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);
}
}