회원 관리 예제
비즈니스 요구사항
- 데이터: 회원 ID, 이름
- 기능: 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않았다고 가정.
- 컨트롤러: 웹 MVC 컨트롤러 역할
- 서비스: 핵심 비즈니스 로직 구현 ( 중복 가입 방지 등 )
- 리포지토리: DB에 접근, 도메인 객체를 DB에 저장하고 관리
- 도메인: 비즈니스 도메인 객체 ( 회원, 주문, 쿠폰 등 DB에 주로 관리되는 ) (= Model)
아직 DB가 선정되지 않았음을 가정해, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계
추후 RDB, NoSQL 등등 다양한 저장소를 사용한다고 가정할 때 수정이 용이하도록 구현체로 가벼운 메모리 기반의 데이터 저장소 사용. (MemoryMemberRepository)
도메인 및 리포지토리
도메인은 MVC의 M에 해당된다.
- Domain.Member
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.MemberRepository (인터페이스)
1
2
3
4
5
6
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
- Repository.MemoryMemberRepository
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MemoryMemberRepository implements MemberRepository{
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L; // key값을 생성해주는.
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id));
//get(id)에서 null 값이 있을 수 있어 이럴 때 Optional.ofNullable()을 통해 null일때 활용가능.
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name)) // name과 같은애들 filter
.findAny(); //하나라도 찾는다. 없으면 null 반환
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
}
테스트케이스 작성
Main 메서드를 통해 실행하거나 웹 애플리케이션의 컨트롤러를 통해 기능을 실행할 수 있지만 실행하는데 오래걸리고, 반복 실행하기 어려워 여러 테스트를 한번에 실행하기 어렵다.
자바는 JUnit이라는 프레임워크를 통해 테스트를 실행할 수 있다.
- test.java.Repository.MemoryMemberRepositoryTest Package 명과 자바파일 이름은 이런식으로 동일하게 하고 뒤에 Test를 붙이는 방식이다.
@Test 어노테이션을 붙여주면 해당 함수를 바로 실행 가능하다. (왼쪽 줄 수 옆 재생버튼)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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();
assertThat(member).isEqualTo(result);
}
@Test
public void findByName(){
Member member1 = new Member();
member1.setName("spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
repository.save(member2);
Member result = repository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
@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);
}
}
** 테스트는 서로 순서상관없이 작동하도록 설계 되어야 한다. 모든 테스트 케이스를 작성했을 때 테스트에 이용되었던 데이터를 초기화해주지 않으면 오류가 발생한다. (@AfterEach 활용)
@AfterEach를 활용하면 class 단위로 Test를 한번에 실행했을 때, 각 함수 실행 후 AfterEach 함수도 같이 실행한다.
@AfterEach 어노테이션을 통해 repository에 clear 메서드를 만든 후 각 메서드가 끝날 때 마다 호출해주어 데이터를 초기화한다.
Assertions.assertThat() 을 이용해 테스트 결과를 확인 할 수 있다.
** Test Code를 먼저 짜고 구현하면 TDD.
회원 서비스
- MemberService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
//회원가입
public Long join(Member member){
//같은 이름이 있는 중복 회원 X라고 가정
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member){
memberRepository.findByName(member.getName())
.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
//전체 회원 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
** MemberService 생성자를 통해 Test에서 MemberService를 테스트 할 때 같은 MemberRepository를 사용할 수 있다.
** class 드래그 하고 Ctrl + Shift + T 하면 자동으로 Test package, method 다 만들어줌.
- Service.MemoryMemberRepositoryTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class MemberServiceTest {
//MemberService memberService = new MemberService();
//MemoryMemberRepository memberRepository = new MemoryMemberRepository();
//이렇게 했을 때 memberService에서 생성한 MemoryMemberRepository랑 이 MemoryMemberRepository는 다른 객체.
//현재 static으로 Map이 선언되어 있기 때문에 같은 저장소를 참조할 수 있어 오류발생 X
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach //실행전에 한번 실행.
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@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.assertEquals(member.getName(), findMember.getName());
}
@Test
void 중복_회원_예외(){
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
//memberService.join(member2)를 실행할 때 IllegalStateException 이 발생해야 한다를 검증.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}