먼저 Spring으로 회원관리를 설계하기 위해서는 요구사항을 먼저 정리해야한다.
위 조건을 기반으로 웹 애플리케이션 구조도 분석해보자.
이렇게 폴더 안에 package
를 만들어서 먼저 domain
을 생성합니다.
domain 안에 Member라는 파일을 만들고, 그 안에 다음과 같이 입력합니다.
package Inflearn_spring.studyspring.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;
}
}
이렇게 입력하는 이유는 Member라는 객체 안에 우리가 아까 요구사항 분석할 때 회원ID, 이름으로 만든다고 하였기 때문에 id
, name
을 세팅해 준 것이고 getter와 setter로 기본적인 기능을 구현해놓은 것이다.
그리고 respository
폴더를 만들어서
위와 같이 MemberRepository
라는 파일을 만들고 Interface에 체크해줍니다.
package Inflearn_spring.studyspring.repository;
import Inflearn_spring.studyspring.domain.Member;
import java.util.Optional;
import java.util.List;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
인터페이스에 이렇게 파일을 작성할 수 있습니다.
findById
나 findByName
으로 정보를 찾아왔을 때 null일경우 Optional
로 감싸서 반환합니다.repository
라는 파일 안에 MemoryMemberRepository
를 만들고 안에 다음과 같은 코드를 넣는다.
package Inflearn_spring.studyspring.repository;
import Inflearn_spring.studyspring.domain.Member;
import java.util.*;
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);
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());
}
}
inteface에서 만든 것들이 어떤 역할을 하는지 여기서 기술합니다.
일단 임시 저장소를 Map
을 이용하여 store
라고 이름을 짓고, 아이디를 하나씩 순차적으로 세기 위하여 sequence
라는 변수를 만들었습니다.
save
함수에서는 유저의 아이디를 ++sequence
를 통해 늘려가면서 저장합니다. Optional<Member> findById
는 id를 찾아오는데, null일 경우에는 Optional
로 감싸서 내보냅니다.Optional<Member> findByName
은 이름을 찾는데 하나라도 일치하는 것이 있으면 데려와서 리턴합니다.findAll
함수는 모든 리스트를 찾아옵니다. 이제 이것이 잘 돌아가는지 테스트케이스를 한번 작성해 보아야겠지요? 개발한 기능을 실행해서 테스트 할 때 자바의 main
메소드를 통하여 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당기능을 실행할 수 있습니다. 이러한 방법은 준비하고 실행하는 데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit
이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
테스트를 하기 위하여 test
폴더 안에 respository
라는 패키지를 하나 생성한다.
그리고 MemoryMemberRepositoryTest
라는 파일을 만들고 아래와 같이 코드를 작성한다.
package Inflearn_spring.studyspring.repository;
import org.junit.jupiter.api.Test;
class MemoryMemberRepositroyTest {
MemoryMemberRepository respository = new MemoryMemberRepository();
@Test
public void save() {
}
}
이렇게 되면 save
가 정상 작동하는지 확인할 수 있는데 save 부분만 실행시켜서 코드를 한번 돌려보자.
위와 같이 정상적으로 작동하고 있음을 알 수 있다.
테스트기능을 완전히 구현해서 잘 되는지 확인해보자.
package Inflearn_spring.studyspring.repository;
import Inflearn_spring.studyspring.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class MemoryMemberRepositroyTest {
MemoryMemberRepository respository = new MemoryMemberRepository();
@Test
public void save() {
Member member = new Member();
member.setName("spring");
respository.save(member);
Member result = respository.findById(member.getId()).get();
Assertions.assertEquals(member,result);
}
}
spring
이라는 이름을 가진 유저를 저장하는것이다.
그리고 저장한 후에 findById
를 통하여 그 저장한 값을 result에 저장하고, Assertions
를 이용하여 내가 구한 값이랑 동일한지 확인해볼 수 있는 것이다.
확인해보면 저장이 잘 된다. 그런데 null
값으로 한번 바꾸어 볼까낭?
기대하는 값과 다르기 때문에 test에서 오류가 발생하는 것이다.
다음으로는 findByName
도 Test 를 작성해보자.
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("spring1");
respository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
respository.save(member2);
Member result = respository.findByName("spring1").get();
assertThat(result).isEqualTo(member1);
}
findByName
에 대한 테스트도 문제 없이 진행가능하다!
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("spring1");
respository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
respository.save(member2);
List<Member> result = respository.findAll();
assertThat(result.size()).isEqualTo(2);
}
findAll()
도 테스트 무난하게 통과!
잘 먹던 findByName
의 테스트를 실패한다..
Spring에서는 Test의 실행 순서를 보장해주지 않는다.
그런데 지금 findAll()
이 먼저 실행되었다. findAll()
에서 spring1과 spring2를 이미 저장을 해버려서 findByName
할 때 이전에 저장한 다른 객체가 자꾸 출력되어서 fail하는 것이다.
그래서 test를 하나 끝내고 나면 깔끔하게 clear를 해주어야 한다!
먼저 이전에 만들었던 repository로 돌아가서 MemoryMemberRepository
에 다음 코드를 넣자.
public void clearStore(){
store.clear();
}
이렇게 되고 test파일로 돌아가서 다음 코드를 작성하자.
@AfterEach
public void afterEach() {
respository.clearStore();
}
이거 되게 중요하다. AfterEach
가 무엇을 하는 거냐면 콜백함수처럼 어떠한 동작하나가 끝나면 아래 afterEach
함수를 수행해 주는 것이다.
이제 확인해보면 Test가 별 문제없이 끝나는 것을 알 수 있다.
테스트는 순서 없이 실행된다는 점 다시한번 기억해두자! 그래서 공용 데이터를 깔끔히 지워주고~ 관계된 관계가 없도록 하자!
이렇게 service
프로젝트를 하나 만들어 MemberService
라는 class 파일을 하나 만들었다.
실제로 서비스될 아이들이니까 이름도 신중하게 정해줍시다.
그리고 아래와 같이 입력해넣습니다.
import java.util.Optional; public class MemberService {
private final MemberRepository memberRepository = new MemoryMemberRepository();
이렇게 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("이미 존재하는 회원입니다.");
});
}
validateDuplicateMember
라는 메소드를 만들어서 회원가입은 시켜주되, 같은 Name
을 가지고 있을시에는 회원가입을 거절시켜준다. 그 이외에는 memberRepositroy
의 save
함수를 통하여 정보를 저장하고 id를 리턴해준다.
/**
* 전체 회원 조회 */
public List<Member> findMembers() {
return memberRepository.findAll(); }
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId); }
}
전체 회원 조회하는 함수와 하의 멤버를 찾아내는 함수이다. 기존에 있던 함수만 이용하면 되므로 매우 간단하다!
회원 서비스 함수를 직접 치지 않아도 된다. 만들고자 하는 테스트 관련된 함수 위에다가 alt
+ enter
를 한다.
그럼 이렇게 이렇게 바로 테스트를 만들 수 있는 창이 뜬다. 단축키라서 매우매우 간단하다!
그럼 아래와 같이 껍데기가 만들어진다.
이제 회원가입 기능에 대한 테스트를 만들어보자.
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
테스트는 같이 배포되는 것이 아니기 때문에 함수의 이름을 한글로 적어도 무방하다.
여기서 아래 문법을 이용하면 편리하다
주어졌을 때, 이것이 주어졌을 때 어떤 것이 실행되는구나~~ 를 한번에 확인할 수 있도록 주석을 달아놓는것이다.
주석을 깔고 가면 되게 편하다.
일단 given
에 멤버가 저장되게 하고,
when
에 Id를 가져오게 한다.
마지막에 then
은 검증을 해주는데, findOne
함수를 통하여 Id를 가져오고, findMemeber
로 지정을 해둔다. 이것의 이름이 같은지 안같은지 이제 검증해주면 된다.
@Test
void 회원가입() {
//given
Member member = new Member();
member.setName("hello");
//when
Long saveId = memberService.join(member);
//then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.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("이미 존재하는 회원입니다.");
//then
}
위는 중복 회원 잡는 코드이다. 만약 이름이 같은 멤버 두명을 가입시키려고 하면 당연히 예외처리가 되어야 하고, 이 코드는 그 예외처리를 수행해주는 것이다. 만약 join
을 시켰는데, 다음 코드로 넘어가면 실패하는 것이니까 fail
을 해준 것이다. 그리고 오류를 잡아냈다면 asserThat
으로 넘어가서 예외처리가 확실히 되었다는 것을 알려주는 것이다.
여기서 IllegalStateException
을 통하여 예외가 터지면 그것을 e
라는 객체에 저장을 해 둡니다. 그리고 에러메세지도 검증을 해주는데, "이미 존재하는 회원입니다."라는 메세지가 일치한다면 에러가 터지지 않는데 에러가 터진다면 에러코드도 같지 않은 거겠지요.
자, 그런데 join할때 테스트를 spring
이라고 똑같은 애를 자꾸 가입시키면 테스트 할때도 에러가 발생하니까 clear
를 따로 진행시켜줘야 한다.
그러면 이용하고 있는 저장소를 같이 써야 하는데, test라는 저장소에
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
달랑 이 코드만 작성해두면 내가 service에서 이용하고 있는 저장소와 아예 다른 새 저장소를 불러들인 것이니까 조금 찝찝하다.. 테스트는 같은 저장소를 대상으로 해야 하는데!
그래서 Service
로 돌아가서 코드를 다음과 같이 갱신한다.
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
외부에서 불러와서 저장소를 생성해주는 것이다.
이제 test에 관련된 코드를 바꾸어 보도록 하자.
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
service 입장에서는 외부 레포지토리를 가져와서 사용하여서 쓰는 것이니까 dependency injection
이라고 한다.
이렇게 해서 Test case까지 완벽하게 구현을 했다.