Spring 스터디2 - 2
프로젝트 생성
스프링 프레임워크의 기능을 사용하지는 않을 거지만, 빠르게 프로젝트를 생성하기 위해 spring initializr를 통해 프로젝트를 생성한다.
조금 더 빠른 실행을 위해 IntelliJ 설정 > Gradle에서 빌드/테스트 실행 환경을 IntelliJ IDEA로 수정한다.
github remote repository 생성 후 해당 프로젝트를 연결했다.
# 프로젝트 디렉토리 루트 위치에서 실행
git init
# remote repository 설정(origin)
git remote add origin https://github.com/hongbre/spring-basic-1.git
# 사용 브랜치 master → main 변경
git branch -b main
# 변경 된 모든 파일 커밋 대상으로 추가
git add .
# 커밋 메시지와 함께 커밋
git commit -m "Project 생성"
# remote repository(github)에 push
git push origin main
비즈니스 요구사항
- 회원
- 회원을 가입하고 조회할 수 있다.
- 회원을 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
- 주문과 할인 정책
- 회원은 상품을 주문할 수 있다
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용한다. (미확정)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고자 한다. 최악의 경우 할인을 적용하지 않을 수 있다. (미확정)
미확정인 회원 데이터 저장 방법, 할인 정책은 언제든지 변경 가능하도록 인터페이스로 구현할 필요가 있다.
회원 도메인 설계 및 개발
회원 도메인 클래스 다이어그램
회원 도메인 개발
회원 등급을 가진 enum 타입의 Grade와 회원 엔티티를 가진 Member 클래스를 생성한다.
public enum Grade {
BASIC,
VIP
}
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 인터페이스를 생성하고, MemoryMemberRepository 구현 클래스를 생성한다.
public class MemoryMemberRepository implements MemberRepository {
private static Map<Long, Member> store = new HashMap<>();
...
}
MemorymemberRepository에서 HashMap을 사용하고 있는데, 동시성 문제가 발생할 수 있으니 ConcurrentHashMap 사용을 권장한다고 한다. 이때 HashMap을 사용 할 때 동시성 문제가 발생하는 이유는 주로 여러 스레드가 동시에 맵을 수정하거나 접근할 때 발생하며, 데이터 무결성이 깨지는 문제이다. ConcurrentHashMap은 세그먼트 잠금(내부를 16개 세그먼트로 분할하여 부분적 잠금 사용), 원자적 메소드를 제공하여 복합 연상을 스레드 세이프하게 처리하는 등 스레드의 안전한 연산을 보장하고, Fail-Safe 반복자 사용으로 수정 중에도 안전하게 순회할 수 있다.
MemberService 인터페이스를 생성하고, MemberServiceImpl 구현 클래스를 생성, MemberApp를 생성해서 main 클래스를 추가해주는데 여기서 테스트를 진행하는 것은 바람직하지 못하니 JUnit 테스트 클래스를 생성해서 진행한다.
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);
}
}
가입한 member와 findMember로 찾은 멤버가 같으면 테스트는 성공한다.
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
...
}
여기서 문제가 있다면 Service 구현 클래스에서 인터페이스인 MemberRepository에도 의존하고, 구현 클래스인 MemorymemberRepository에도 의존하고 있다.
주문과 할인 정책 도메인 설계 및 개발
주문과 할인 정책 도메인 클래스 다이어그램
주문과 할인 정책 도메인 개발
memberId, itemName, itemPrice, discountPrice 4개의 속성을 가진 Order 클래스를 생성하고, OrderService 인터페이스와 OrderServiceImpl 구현 클래스 생성 및 DiscountPolicy 인터페이스와 FixDiscountPolicy 구현 클래스를 생성하게 된다. 동일하게 OrderApp 이라는 main 클래스를 추가해주었고, JUnit 테스트 클래스를 생성하였다.
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);
}
}
memberA를 VIP 등급으로 회원 가입 시키고, 10000원짜리 itemA를 주문했을 때 할인 금액이 1000원이면 테스트는 성공한다.