스프링 입문-3
<비즈니스 요구사항 정리>
데이터: 회원 ID, 이름
기능: 회원 등록, 조회
아직 데이터 저장소가 선정되지 않은 상태
-> 서비스: 비즈니스 도메인 객체를 가지고 핵심 비즈니스 로직이 동작하도록 구현한 계층
<구현>
member.java
package com.example.demo.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 setId(String name) {
this.name = name;
}
}
MemberRepository.java -> member 저장할 공간
package com.example.demo.repository;
import com.example.demo.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();
}
MemoryMemberRepository.java -> 기능 구현 파트
package com.example.demo.repository;
import com.example.demo.domain.Member;
import java.util.*;
public class MemoryMemberRepository implements MemberRepository{
//밑에 있는 함수에서 member save할 때 저장을 어디가에 해야 돼서 만듦
private static Map<Long, Member> store =new HashMap<>();
//sequence:0,1,2 key 값 생성해주기 위한 애
private static long sequence =0L;
//회원 저장기능
@Override
public Member save(Member member) {
member.setId(++sequence); //store하기전에 id값 세팅해줌
store.put(member.getId(), member); //id값 세팅한 후 멤버를 store에 저장해줌
return member; //member에 id와 name이 저장됨
}
//회원 아이디로 찾기
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable(store.get(id)); //결과가 없어 null이 반환될 가능성있으면 optional로 감싸준다.
}
//이름으로 찾기
@Override
public Optional<Member> findByName(String name) {
return store.values().stream() //loop 돌린다
.filter(member -> member.getName().equals(name)) //parameter로 넘어온 name 과 getName값이 같은지 비교. 같은 경우에만 필터링 됨
.findAny(); // Map에서 루프를 다 돌면서 하나 찾으면 찾은 값 반환. 없으면 optional에 null이 포함돼서 반영됨
}
//전체 회원 리스트 보기
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values()); //values=members 멤버들을 반환
}
}
<테스트 케이스 작성>
이제 회원 repository와 class가 내가 원하는대로 정상적으로 작동하는지 검사해봐야한다
-> 이때 테스트 케이스를 작성해 검증한다!
테스트 케이스 작성하지 않고 main 메소드 돌리거나 웹 어플리케이션의 컨트롤러를 이용해서 실행시켜 테스트 하는 방법도 존재한다.but, 실행시키는 데 오래 걸리고 여러 테스트를 한번에 실행시키기 어려움
-> 따라서 위에서 말한대로 테스트 케이스를 작성해 검증한다고 한다. (JUnit이라는 프레임워크로 테스트 진행)
1. test>java>com.example.demo에 repository package생성
2. 우리는 MemoryMemberRepository class를 테스트 할 거니까 1에서 만든 repository package 에 MemoryMemberRepositoryTest라는 class 만들기 ( 이때 이걸 딴 데서 갖다쓸 거 아니니까 public이라고 할 필요 없다)
3. repository 객체 생성해준 후 @Test import 앞에서 구현했던 save 함수 실행
package com.example.demo.repository;
import com.example.demo.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class MemoryMemberRepositoryTest {
MemberRepository repository=new MemoryMemberRepository(); //MemberRepository 객체 repository가 MemoryMemberRepository객체 가리킴으로써 얘의 메소드와 데이터에 접근가능해짐
@Test
public void save(){
Member member=new Member();
member.setName("spring");
repository.save(member);//repository에 저장. Id도 저절로 할당되어 저장되게 앞에서 구현했었음
Member result=repository.findById(member.getId()).get();//optional에서 값을 꺼낼 때는 get을 이용해서 바로 꺼낼 수 있다
Assertions.assertEquals(member, result); // member랑 result랑 같은지 확인하는 기능(기대하는 member find 했을 때 튀어나와야한다)
}
}
여기에 있는 초록색 실행버튼을 눌러주면( 여러 test 다 한번에 돌리고 싶다면 class 옆에 있는 초록버튼 눌러주면 됨)
실행시키게 되면 출력되는 건 없지만 녹색불 뜬다
만약 member와 result가 달라 오류가 난다 이런식의 빨간 문구가 뜬다
아까는 맨위에 있는 org.junit 썼지만 이번엔 assertj를 써서 구현해볼 것이다. 얘가 더 편하게 구현할 수 있게 해줘서 요즘은 얘를 많이 쓴다고 한다.
@Test
public void save(){
Member member=new Member();
member.setName("spring");
repository.save(member);//repository에 저장. Id도 저절로 할당되어 저장되게 앞에서 구현했었음
Member result=repository.findById(member.getId()).get();//optional에서 값을 꺼낼 때는 get을 이용해서 바로 꺼낼 수 있다
Assertions.assertThat(member).isEqualTo(result);// member랑 result랑 같은지 확인하는 기능( member가 result랑 똑같은지)
}
맨 밑줄을 아까와 다르게 구현해줬다
4. 이번엔 findByName()을 검증
@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();
Assertions.assertThat(result).isEqualTo(member1);
}
5. 이번엔 findAll() 검증
@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();
Assertions.assertThat(result.size()).isEqualTo(2);
}
6. 여태까지 만든 테스트 한번에 돌리기 위해 class 실행시켜줌 -> but error 뜸!
test 할 때 실행되는 순서는 우리가 정할 수 없이 임의로 진행됨. 여기서는 findAll()부터 실행된 것.
findAll()에서 member1, member2 객체 만들고 저장해두어서 findByName()할때 이전에 저장했던(findAll에서) 객체가 나와버린 것이다
-> test 끝날때마다 repository clear 시켜주어야 함
->How?
MemoryMemberRepository에 맨 밑에 있는 함수 추가해주고 MemoryMemberRepositoryTest 코드 수성 및 추가
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository=new MemoryMemberRepository(); //MemberRepository 객체 repository가 MemoryMemberRepository객체 가리킴으로써 얘의 메소드와 데이터에 접근가능해짐
@AfterEach
public void afterEach(){
repository.clearStore();
}
-> @AfterEach부분을 통해 Test 가 실행되고 끝날 때마다 한번씩 repository(저장소) 지운다
cf) test class먼저 만든 후 MemberMemoryMepository 작성하는 경우 (검증 틀을 먼저 만드는 경우)
-> TDD (테스트 주도 개발)
<회원 서비스 class 만들기>
-> 회원 서비스 : 회원 레포지토리와 도메인을 이용하여 실제 비즈니스 로직 작성
src>main>java>com.example.demo>service package 생성 후 MemberService class 생성
package com.example.demo.service;
import com.example.demo.domain.Member;
import com.example.demo.repository.MemberRepository;
import com.example.demo.repository.MemoryMemberRepository;
import java.util.Optional;
public class MemberService {
//member service를 만드려면 member repository가 필요함. 따라서 얘부터 만들기
private final MemberRepository memberRepository=new MemoryMemberRepository();
//회원 가입 만들기 (member repository에 save 호출해주기, id 반환해주기)
public Long join(Member member){
//같은 이름이 있는 중복 회원 있으면 안된다
Optional<Member> result=memberRepository.findByName(member.getName());
//ifPresent : result가 null이 아니라 값이 존재하면 동작한다
result.ifPresent(m ->{
throw new IllegalStateException("이미 존재하는 회원입니다.")
} );
memberRepository.save(member);
return member.getId(); //회원가입 하면 id 반환해줌
}
}
근데 여기서 optional 을 바로 반환하는 건 좋지 않다.
//같은 이름이 있는 중복 회원 있으면 안된다
memberRepository.findByName(member.getName()) //일단 이름 찾아본다. 이 결과는 optional member
// 이미 그 이름이 존재한다면 ifPresent :null이 아니라 값이 존재하면 동작한다
.ifPresent(m ->{
throw new IllegalStateException("이미 존재하는 회원입니다.")
} );
이렇게 코드 수정해준다. 근데 같은 이름인 회원을 찾는 과정이 기니까 얘를 하나의 함수로 빼주는 게 좋다
-> ctrl + alt + shift+ t 눌러서 extract method 해주고 이름 지어주기
public Long join(Member member){
//같은 이름이 있는 중복 회원 있으면 안된다
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId(); //회원가입 하면 id 반환해줌
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName()) //일단 이름 찾아본다. 이 결과는 optional member
// 이미 그 이름이 존재한다면 ifPresent :null이 아니라 값이 존재하면 동작한다
.ifPresent(m ->{
throw new IllegalStateException("이미 존재하는 회원입니다.")
} );
}
}
전체회원 조회와 아이디로 회원 조회 기능까지 만든 최종 MemberService class는아래와 같다.
package com.example.demo.service;
import com.example.demo.domain.Member;
import com.example.demo.repository.MemberRepository;
import com.example.demo.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
//member service를 만드려면 member repository가 필요함. 따라서 얘부터 만들기
private final MemberRepository memberRepository=new MemoryMemberRepository();
//회원 가입 만들기 (member repository에 save 호출해주기, id 반환해주기)
public Long join(Member member){
//같은 이름이 있는 중복 회원 있으면 안된다
validateDuplicateMember(member); //중복 회원 있는지 검증
memberRepository.save(member);
return member.getId(); //회원가입 하면 id 반환해줌
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName()) //일단 이름 찾아본다. 이 결과는 optional member
// 이미 그 이름이 존재한다면 ifPresent :null이 아니라 값이 존재하면 동작한다
.ifPresent(m ->{
throw new IllegalStateException("이미 존재하는 회원입니다.")
} );
}
//전체 회원 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
//id로 회원 조회
public Optional<Member> findOne(Long memberId){
return memberRepository.findById(memberId);
}
}
<회원 서비스 테스트>
test 만들고 싶은 대상 class 이름 더블 클릭 후 ctrl + shift+ t 눌러 create Test 하면 보다 쉽게 test 만들 수 있다.
package com.example.demo.service;
import com.example.demo.domain.Member;
import com.example.demo.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
//첫 줄의 memberService에 있는 memberRepository와 두 번째 줄에 있는 memberRepository는 서로 다른 객체라서 서로 다른 db를 쓰게 되는 문제가 발생한다. 따라서 바꾸어야 함
// MemberService memberService=new MemberService();
// MemoryMemberRepository memberRepository=new MemoryMemberRepository();
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach //각 테스트 실행 전에 호출된다. 테스트에 영향 없도록 새로운 객체 생성
public void beforeEach(){
memberRepository=new MemoryMemberRepository();
memberService= new MemberService(memberRepository); //MemoryMemberRepository를 만들어 넣어주므로써 같은 메모리 리포지토리를 사용하게 됨
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@Test
void 회원가입() { //저장이 잘됐냐
//given(이러한 상황이 주어졌을 때)
Member member= new Member();
member.setName("hello");
//when(이걸로 실행했을 때)
Long saveId=memberService.join(member); //join의 반환값이 id라서
//then(이게 나와야 해)
Member findMember=memberService.findOne(saveId).get();
Assertions.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)); // () -> 뒤의 로직을 따르면 IllegalStateException이라는 예외가 터져야 된다는 뜻
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
/*
// memberService.join(member2); 호출 시 IllegalStateException 예외가 발생하면 그 예외 메시지가 "이미 존재하는 회원입니다."인지를 확인하여 예외 처리가 올바르게 동작하는지 테스트하는 것입니다. 만약 예외가 발생하지 않거나 예외 메시지가 다르다면 fail()을 호출하여 테스트를 실패로 표시합니다.
try{
memberService.join(member2);
fail(); //여기까지 오면 실패한 것
}catch (IllegalStateException e){
//예외터져서 정상적으로 성공
assertThat(e.getMessage().isEqualTo("이미 존재하는 회원입니다."))
}
*/
//then
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
public class MemberService {
//member service를 만드려면 member repository가 필요함. 따라서 얘부터 만들기
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository=memberRepository;
} //외부에서 repository 넣어주게 바꿈
위의 test에서 아래 부분을 수정하게 되었는데 이에 따라 MemberService class 코드를 위와 같이 수정함
//첫 줄의 memberService에 있는 memberRepository와 두 번째 줄에 있는 memberRepository는 서로 다른 객체라서 서로 다른 db를 쓰게 되는 문제가 발생한다. 따라서 바꾸어야 함
// MemberService memberService=new MemberService();
// MemoryMemberRepository memberRepository=new MemoryMemberRepository();