백/spring boot

스프링 입문-3

남승현 2023. 10. 18. 20:54

<비즈니스 요구사항 정리>

 

데이터: 회원 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();