package hello.hellospring.domain;
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;
}
}
도메인 설정을 해줍니다
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository
{
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
추후에 구현체가 jdbctemplate 이나 mybatis등등으로 바뀔수 있기에 인터페이스 설정을 해줍니다. findbyid, findbyName 할떄 null이 올수도 있음으로 Optional 로 처리해줍니다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;
public class MemoryMeberRepository implements MemberRepository
{
private static Map<Long, Member> store = new HashMap<>(); // ConcurrentHashmap 을 써야한다 공유되는 자원이기에 동시성 문제가 생길 수 있다.
private static long sequence = 0L; // AtomicLong을 써줘야한다 실무에서는 동시성 문제
@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));
}
@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();
}
}
동시성 문제를 해결하기위한 방법은 주석으로 달아놓았다. findbyId는 앞에 언급한것처럼 null 이올수있기에 Optional.ofNullable로 처리해줍니다.
findByName 은 hashmap에 있는 value 들을 iterate 하면서 그중 이름이 같은것을 찾는 즉시 그 member 객체를 return 해줍니다.
Test case 작성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.*;
class MemoryMemberRepositoryTest
{
MemoryMeberRepository repository = new MemoryMeberRepository();
@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("spring1");
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("spring1");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
실무에서는 build할떄 test를 통과하지 못하면 다음 단계를 못넘어가게 막아버린다.
rename을 전체적으로 하고싶으면 해당 variable 에서 shift F6을 누르면 된다.
전체 test를 돌릴떄 순서가 보장이안되기에 에러가 생길 수 있다. 예를들면 findAll이 먼저 실행이 되고 findByName이 실행이되면 findAll에서 저장된 객체는 findByName에서 생성한 객체와 다르므로 에러를 가져온다. 항상 test가 끝난 메소드는 클리어를 해주어야한다.
@AfterEach 는 매번 테스트 메소드가 실행이 끝나면 불려지는 콜백함수를 정의 할 수 있다 이안에 공용저장소를 clear해주는 코드를 넣어주면 된다.
테스트 먼저 작성하고 실제 코드를 작성하는것!! 틀을 먼저 만드는 것!! Test-driven development. Test는 정말 중요하다 하셨다
Business Logic 1 (Service)
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMeberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService
{
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository)
{
this.memberRepository = memberRepository;
}
/**
* 회원 가입
*/
public Long join(Member member)
{
// 같은 이름이 있는 중복회훤은 안된다
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);
}
}
Repository같은 경우는 단순한 데이터 베이스와 CRUD 를 담당해주지만 Service 클래스는 이름만 봐도 약간 비즈니스 로직같은 다른 것을 담당하는 것을 알 수 있다.
findByName이 어차피 Optional 클래스로 감싸서 리턴 하기떄문에 ifPresent를 통해 람다 형식으로 이름이 같으면 Exception을 발생시키게 해줌으로 이름이 같은 사람들은 가입 못하게 하였다.
memberRepository 를 주입받음 으로서 추후에 테스트할때 같은 memberRepository 객체를 쓸 수 있다. 이런 방식을 Dependency Injection 이라 한다.
Service Test
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMeberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest
{
MemberService memberService;
MemoryMeberRepository memoryMeberRepository;
@BeforeEach
public void beforeEach()
{
memoryMeberRepository = new MemoryMeberRepository();
memberService = new MemberService(memoryMeberRepository);
}
@AfterEach
public void afterEach()
{
memoryMeberRepository.clearStore();
}
@Test
void 회원가입()
{
//given
Member member = new Member();
member.setName("spring");
//when
Long saveId = memberService.join(member);
//then
Member findMemeber = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMemeber.getName());
}
@Test
public 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));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// try
// {
// memberService.join(member2);
// fail();
// }
// catch(IllegalStateException e)
// {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// }
//then
}
@Test
void findMembers()
{
}
@Test
void findOne()
{
}
}
BeforeEach 로 저렇게 한 이유는 같은 MemoryMemberRepository 를 쓰기 위함이다.
로직상 Repository를 넣어주고 클리어하고를 반복한다. 또한 테스트 할때는 한국말로 메소드를 명시해도 좋다.
'WEB > Spring' 카테고리의 다른 글
김영한 (스프링 부트 5) (0) | 2021.02.08 |
---|---|
김영한 (스프링부트 입문) 4 (0) | 2021.02.08 |
김영한 (스프링부트 입문) 3 (1) | 2021.01.31 |
김영한 (스프링부트 입문) 1 (0) | 2021.01.24 |
Spring Ehcache example (0) | 2021.01.23 |