start.spring.io에서 설정을 다음과 같이 한다. Spring Boot버전은 최신 버전 중 snapshot이나 rc1이 적혀있지 않는 버전을 선택합니다. 필자는 java 버전이 안맞아서 2.7.17로 다시 선택하였다.
Dependencies는 아무것도 선택하지 않는다. 이러면 가장 기본적인 Spring을 선택하게 된다.
dependencies에 spring starter와 테스트 관련 라이브러리만 있는 것을 확인할 수 있다.
CoreApplication을 실행하면 다음과 같이 웹서버가 정상적으로 동작한다.
비즈니스 요구사항과 설계
비지니스 요구사항은 다음과 같다.
회원
- 회원을 가입하고 조회할 수 있다.
- 회원은 일반과 VIP 두 가지 등급이 있다.
- 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)
주문과 할인 정책
- 회원은 상품을 주문할 수 있다.
- 회원 등급에 따라 할인 정책을 적용할 수 있다.
- 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
- 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. 최악의 경우 할인을 적용하지 않을 수 도 있다. (미확정)
해당 강의에서는 스프링이 아닌 JAVA
만을 사용하여 이를 구현한다.
이를 그림으로 나타내자.
아래와 같은 도메인 협력관계를 바탕으로 개발자들이 클래스 다이어그램을 만들어낸다.
요구 사항이 자체 DB
와 외부 시스템 연동
이 아직 미정이기 때문에 일단 메모리 회원 저장소
를 구현하고 나중에 확정되면 추후에 저장한다. 이를 위해 MemberService
와 MemberRepository
를 인터페이스로 구현한다.
인터페이스와 구현체들이 모두 포함된다. 어떠한 구현체가 사용되는지(MemoryMemberRepository가 사용될지 DbMemberRepository가 사용될지)는 서버 실행시 동적으로 결정이 된다.
대략적으로 위와 같이 나올것이다.
객체 다이어그램으로 표현하면 다음과 같다.이는 실제 인스턴스 끼리의 참조를 나타낸다.
회원 별 등급을 설정하기 위해 다음 코드를 작성한다.
hello/core/member/grade.java
package hello.core.member;
public enum Grade {
BASIC,
VIP
}
회원은 BASIC과 VIP 2종류로 나뉜다.
hello/core/member/Member.java
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;
}
}
윈도우 기준 alt
+ ins
키를 사용하여 생성자와 getter, setter등을 쉽게 만들 수 있다.
다음은 회원 인터페이스를 구성하자
hello/core/member/MemberRepository.java
package hello.core.member;
public interface MemberRepository {
void save(Member member);
Member findById(Long memberId);
}
해당 인터페이스의 구현체를 구현하자
hello/core/member/MemoryMemberRepository.java
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);
}
}
NULL과 같은 오류처리는 하지 않고 간단하게 구성하였다. 원래 HashMap은 동시성 이슈가 발생할 수 있다. 이런 경우 ConcurrentHashMap을 사용해야 한다.
다음은 MemberService 인터페이스를 만들자
hello/core/member/MemberService.java
package hello.core.member;
public interface MemberService {
void join(Member member);
Member findMember(Long memberId);
}
해당 인터페이스의 구현체를 구현하자
hello/core/member/MemberServiceImpl.java
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);
}
}
참고로 해당 인터페이스에 대한 구현체가 하나밖에 없으면 뒤에 Impl을 붙이는게 관례라고 한다.
다음과 같은 코드를 작성한다.
hello/core/member/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, "mamberA", Grade.VIP);
memberService.join(member);
Member findMember = memberService.findMember(1L);
System.out.println("new member = " + member.getName());
System.out.println("find member = " + findMember.getName());
}
}
결과는 위와 같이 나온다.
하지만 매번 위와 같이 매서드를 테스트할 수는 없을 것이다. 그래서 Junit을 사용하여 테스트를 할 것이다.
test/java/hello/core/member/MemberServiceTest
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);
}
}
테스트코드가 잘 통과하는 것을 보면 잘 구현이 된 것 같다.
하지만 위 코드들은 의존관계가 인터페이스 뿐 아니라 구현체에 의존하는 문제점이 있습니다.
다음 코드 부분을 보면 알 수 있습니다.
이 부분에서 구현체를 직접 생성하는 것을 확인할 수 있습니다.