..
[Spring Basic] 3. Member Management Example - Backend
Table of contents
- Business Requirement
- Requirement
- Web Application Layer Structure
- Class Dependency
- Member Domain and Repository
- Package setting
- Domain
- Repository
- Package setting
- Member Repository Test Case
- Junit Test
- Test시 주의점
- Member Service
- Package setting
- Service
- 회원가입
- 전체 회원 조회
- 단일 회원 조회 (id)
- Service
- Package setting
- Member Repository and Implementation 구현체 test
- Test Convention
- 회원가입 테스트
- 중복 회원 예외 테스트
- 주의 사항
- 문제점
- 문제 해결 방법
- Dependency Injection
Business Requirement
Requirement
- Data : userId, name
- Functions : Member Registration, Search
- Data storage is not decided yet..
Web Application Layer Structure

- Controller : Web MVC Controller
- Service : Business logic
- Repository : Store and manage Domain object in DB
- Domain : Business domain object.
- e.g., Member, Order, Coupon, etc..
- they are mainly stored and managed in DB
Class Dependency

MemberRepositoryis designed as interfece since DB is not selected yet.- We can easily change concrete class(구현 클래스)
- DB로는 RDB, JDBC, NoSQL 등등 고민중
- 초기 개발 단계에서는 구현체(e.g., MemoryMemberRepository)로 가벼운 메모리 기반의 데이터 저장소 사용한다
Member Domain and Repository
Package Setting
Domain
- Member 클래스 (회원 객체)
domain/Member.java
public class Member {
private Long id;
private String name;
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;
}
}
Repository
- Member(회원 객체)를 저장하는 저장소
repository/MemberRepository.java (interface)
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
repository/MemoryMemberRepository.java (구현체)
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence); // id는 시스템이 결정 한다
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
// return store.get(id)
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore(){
store.clear();
}
}
- Null이 발생할 가능성이 있으면 Optional로 감싸준다
Optional.ofNullable(store.get(id))
- 람다를 사용
- member의 이름이 파라미터로 넘어온 이름과 같은지 비교
findAny()는 하나라도 찾는것을 반환, 없으면 Null을 반환
Java8 Optional
- Optional class (java.util.Optional< T >)
- Java 8부터 지원되는 클래스
- null이 될 수도 있는 객체를 감싸는 Wrapper 클래스
- 없으면 NULL로 반환, Optional 로 감싸서 반환하는걸 선호
- Optional.ofNullable
- Optional의 장점
- NPE을 유발할 수 있는 null을 직접 다루지 않아도 된다
- 매번 null을 체크할 필요 없다
- 명시적으로 해당 변수가 null일 수도 있다는 가능성을 표현 가능
실무에서는 Map 대신 ConcurrentHashMap을 사용 실무에서는 Long 대신 AutomicLong을 사용
Member Repository Test Case
Junit Test
- Class레벨에서 각 메소드 테스트를 한번에 돌릴 수 있다
- 각 메소드의 테스트 순서는 보장이 안된다
-
테스트는 서로(method간에) 순서 및 의존관계가 없이 될 수 있도록 설계가 되어야 한다
- Test libraries
- org.junit.jupiter.api.Assertions
- e.g., Assertions.assertEquals(member, result);
- org.assertj.core.api.Assertions
- e.g., assertThat(member).isEqualTo(result);
- org.junit.jupiter.api.Assertions
repository/MemoryMemberRepositoryTest.java
import org.junit.jupiter.api.Assertions;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach(){
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
// System.out.println("result = " + (result == member));
Assertions.assertEquals(member, result); // way1, (expected, actual)
assertThat(member).isEqualTo(result); // way2
}
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
// Optional<Member> result = repository.findByName("spring1");
Member result = repository.findByName("spring1").get();
assertThat(member1).isEqualTo(result);
}
@Test
public void findAll(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
Test시 주의점
- 모든 테스트들은 순서가 보장이 안된다
- Repository에 메모리를 지워주는 코드를 넣어야 한다
- 이전에 저장했던 객체를 초기화 안해주면 다른 메소드에서 같은 이름의 객체 테스트시 에러 발생
- 그래서 테스트가 끝나면 data를 clear해주어야 한다
@AfterEach
public void afterEach(){
repository.clearStore();
}
- @AfterEach
- => 테스트가 끝날때마다 clear함수 (e.g., clearStore)를 수행해서 data를 지운다
Member Service
Package Setting
Service
- MemberService 클래스
service/MemberService.java
회원가입
회원가입시 같은 이름 중복을 막는 로직 추가
public Long join(Member member){
// 이름 중복 제거, 이름을 찾아봐서 이름이 있으면
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
Optional<Member> result = memberRepository.findByName(member.getName());
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다");
});
}
아래와 같이 편의성을 위해 변경 가능
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다");
})
전체 회원 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
단일 회원 조회 (id)
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
Member Repository and Implementation 구현체 test
Test Convention
- Given (무언가가 주어졌을때)
- 해당 데이터를 기반으로 테스트
- When (이걸 실행 했을때)
- memberService에서 join할때 검증
- Then (결과가 이렇게 나와야해)
- 검증부
회원가입 테스트
MemberService memberService = new MemberService();
@Test
void join() {
// given
Member member = new Member();
member.setName("hello");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
- join테스트시 Service에서 findOne으로 찾아도 되지만 우리가 저장을 한것이 Repository에 있는게 맞음을 확인하고 싶은것
중복회원 예외 테스트
- 테스트는 정상 flow도 중요하지만 예외flow도 중요하다
- 중복_회원_예외 호출시 예외가 터져야되
@Test
public void 중복_회원_예외(){
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
try {
memberService.join(member2);
fail();
} catch (IllegalStateException e){
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");
}
// 위의 try-catch문과 똑같이 동작한다
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다");
}
- Try Catch도 좋지만 juniit에서 문법을 제공해준다
- assertThrows()
- 오른쪽이 실행되면 왼쪽의 예외가 발생해야 한다
- assertThrows()
주의 사항
- 회원가입 태스트와 중복회원예외 테스트시 같은 이름을 member에 지정하면 테스트가 실패한다. 왜냐하면 중복이름을 허용하지 않기 때문이다.
문제점
- memberService에서의 memoryMemberRepository와 MemberServiceTest에서의 memoryMemeberRepository가 다른 instance이다! 이렇게 따로 2개를 쓸 이유가 없고 같은 걸 사용해야 한다.
MemberService repository
public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
...
}
MemberServiceTest repository
class MemberServiceTest {
MemberService memberService = new MemberService();
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
...
}
MemberService 의 데이터의 경우 현재는 static이라 괜찮지만, static이 아닌 경우에는 다른 DB가 되면서 문제가 생길 수 있다.
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
...
}
문제 해결 방법
Dependency Injection
기존 MemberService Constructor를 통해 외부에서 MemberRepository를 넣어준다 (DI)
public class MemberService {
// private final MemoryMemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
- MemberService의 입장에서 memberRepository를 외부에서 넣어준다. 이걸 Dependency Injection (DI)이라고 한다
MemberServiceTest 에서는 동작하기 전마다 넣어주면 된다
class MemberServiceTest {
// MemberService memberService = new MemberService();
MemberService memberService;
MemoryMemberRepository memoryMemberRepository;
@BeforeEach
public void beforeEach(){
memoryMemberRepository = new MemoryMemberRepository();
memberService = new MemberService(memoryMemberRepository);
}
- 이렇게 하면 매번 같은 MemberRepository가 사용이 된다
위의 테스트 코드들은 Java JVM안에서 끝나게 된다