[spring 입문] 회원 관리 - 회원 도메인, 레포지토리 생성, 테스트 케이스 작성

채원·2024년 1월 4일

스프링

목록 보기
4/18
post-thumbnail

출처) 인프런 스프링 입문 강의

비즈니스 요구사항 정리

단순한 회원 시나리오를 가정함

  • 데이터: 회원 ID, 이름
  • 기능: 회원 등록 조회
  • 아직 데이터 저장소가 선정되지 않음

일반적인 웹 애플리케이션 구조

  • 컨트롤러: MVC의 컨트롤러
  • 서비스: 핵심 비즈니스 로직 구현
    ex) 회원은 중복 가입이 안된다
  • 레포지토리: DB 접근, 도메인 객체를 DB에 저장하고 관리
  • 도메인: 비즈니스 도메인 객체
    ex) 회원, 주문, 쿠폰 등 DB에 저장하고 관리

클래스 의존 관계

회원 서비스가 회원 레포지토리 (인터페이스)에 의존
아직 저장소가 지정되지 않았기 때문에 인터페이스로 생성
나중에 구현 클래스를 변경할 수 있게 하기 위함
초기 개발 단계에서는 개발을 위해 메모리 기반 데이터 저장소 사용

회원 도메인과 레포지토리 생성

  • 도메인
    java/hello.hellospring/domain 패키지 생성 후
    Member 클래스 생성
    요구사항 대로 id (식별용), name 필드 생성
    getter setter 해놓기
package hello.hellospring.domain;

public class Member {

    private Long id; // 시스템에 저장하는 아이디 (회원 구분용)
    private String name; // 회원 이름


    // getter setter
    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;
    }
}
  • 레포지토리
    회원 정보를 저장하는 레포지토리 만들기
    java/hello.hellospring/repository 패키지 생성 후
    MemberRepository 인터페이스 생성
    4가지 기능
    save를 하면 이후에 findBById, findByName, findAll로 회원 정보 조회가 가능

Optional: iD, name으로 조회시 반환 값이 없는 경우 null이 아니라 Optional로 감싼 것을 반환함, Java 8의 기능

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); // id로 회원 조회
    Optional<Member> findByName(String name); // name으로 조회
    List<Member> findAll(); // 지금까지 저장된 모든 회원을 조회
}

구현체 생성하기
repository 패키지에 MemoryMemberRepository 클래스 생성
implements MemberRepository 하고
Option + enter 키 누르면

implements methods가 나옴 클릭 후 모든 메소드 선택하기

package hello.hellospring.repository;

import hello.hellospring.domain.Member;

import java.util.List;
import java.util.Optional;

public class MemoryMemberRepository implements MemberRepository{
    @Override
    public Member save(Member member) {
        return null;
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.empty();
    }

    @Override
    public Optional<Member> findByName(String name) {
        return Optional.empty();
    }

    @Override
    public List<Member> findAll() {
        return null;
    }
}

이제 구현 해봅시다 ~

  • Map을 사용하여서 저장 Key = 회원 아이디, Value = 회원
    ⭐️ 단, 실무에서는 공유 변수는 ConcurrentHashMap을 사용해야한다! 예제이므로 간단히

  • sequence는 id 값을 생성해주는 역할

    private static Map<Long, Member> store = new HashMap<>();
    private static long sequence = 0L;

save(Member member)
setId로 sequence값 +1하고 id 셋팅 (id는 유저한테 입력 받는게 아니라, 시스템에서 결정)
store에 (id, member) 맵 저장
저장된 결과 반환

    public Member save(Member member) {  // id 시스템에 저장
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

findByID(Long id)
id 입력 받으면 store에 저장된 정보를 조회
null을 받을 수 있으므로, Optional.ofNullable()로 감싸기
이렇게 감싸면 클라이언트 측에서 처리를 할 수 있음

    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id));
    }

findByName(String name)
store에 있는 값을 돌면서 member의 getName 반환 값이 요청 받은 name과
동일한 경우 나오는 결과 값을 반환
만약 끝까지 돌았는데 아무것도 없다면 Optional에 null이 포함되어 반환됨

    public Optional<Member> findByName(String name) {
       return  store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

findAll()
실무에서 리스트 형태 많이 사용함
store에 저장된 값 (member 값)을 list 형태로 만들어서 리턴하기

    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }

전체 코드

package hello.hellospring.repository;

import hello.hellospring.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) {  // id 시스템에 저장
        member.setId(++sequence);
        store.put(member.getId(), member);
        return member;
    }

    @Override
    public Optional<Member> findById(Long id) { // id로 member 조회
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) { // name으로 member 조회
       return store.values().stream() // store에 저장된 값 돌면서, name이 일치하는 값 리턴
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
        return new ArrayList<>(store.values());
    }
}

회원 레포지토리 테스트 케이스 작성

위에서 구현한 회원 기능이 잘 되는지 테스트 해보기 위해서 테스트 케이스를 작성

자바에서 개발 기능을 테스트할 때 main 메소드, 웹 애플리케이션 컨틀롤러를 통해 실행하는데 이렇게 하는 경우는 반복 실행 및 여러 테스트를 한 번에 하기 번거로움

➡️ JUnit 프레임 워크로 테스트 실행

src/test/hello.hellospring 폴더 아래에
MemoryMemberRepositoryTest 클래스 생성

package hello.hellospring.repository;

import hello.hellospring.domain.Member;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();
    }
}

save 테스트

    @Test
    public void save(){
        Member member = new Member();
        member.setName("채원");

        repository.save(member);

        Member result = repository.findById(member.getId()).get();
        // 출력해서 확인하기
        // System.out.println("result = " + (result == member));

        //  Junit 사용해서 확인하기
        assertThat(member).isEqualTo(result);
    }

새 회원정보를 저장했을 때
findById를 통해 찾은 회원 정보가 저장한 정보 (member)와 같은지
확인함

Test 파일을 실행시키면
System.out.println() 을 통해서 결과 값을 확인할 수 있음

그러나 이렇게 출력하면서 확인하는 건 번거롭기 때문에 ~
Assertions를 사용함, assertEquals 보다 assertThat().isEqualTo()가
더 직관적임

import static org.assertj.core.api.Assertions.*;

이걸 import 해주면 앞에 Assertions 빼고 사용해도 됨

assertThat(member).isEqualTo(result);

아래에서
member가 result과 같은지 확인

println 사용할 때랑 달리 별 다른 출력은 없지만 초록불 잘 들어옴

만약 isEqualTo에 null을 넣어보면 오류가 발생하는 것을 확인할 수 있음

findByName 테스트
정확한 테스트를 위해 회원 정보를 2개 저장하고, 테스트해보겠음
채원, 채이 회원이 있을 때 채원 회원의 이름으로 멤버 정보를 조회하면 잘 되는지 확인

     @Test
    public void findByName(){
    	// 회원 정보 저장
        Member member1 = new Member();
        member1.setName("채원");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("채이");
        repository.save(member2);
		
        // 이름으로 회원 정보 조회한 결과
        Member result = repository.findByName("채원").get();
		
        System.out.println(result.getName());
        assertThat(result).isEqualTo(member1);
    }

assertThat을 확인도 문제 없고, 실제로 result (이름으로 찾은 회원 정보)에서 getName()을 했을 때 채원 이름이 잘 나온 것을 확인할 수 있음

여기서 만약 "채이" 즉 회원2번의 이름으로 정보를 조회했을 때, member1 "채원"과 일치하는지 확인하면 당연히 불일치해서 오류가 발생함

이렇게 Test 하는 방식의 장점 = 한 번에 여러 테스트 돌릴 수 있음
hello.helloSpring 즉 클래스 레벨에서 실행 시키면 여러 테스트가 한 번에 돌아감

findAll 테스트
위처럼 2개의 계정을 만들어 놓고, findAll 했을 때 찾은게 2개인지 확인하기

    @Test
    public void findAll(){
        Member member1 = new Member();
        member1.setName("채원");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("채이");
        repository.save(member2);

        List<Member> result = repository.findAll();
    
        assertThat(result.size()).isEqualTo(2);
    }

회원 생성할 때 같은 이름으로 만들면
findAll이 먼저 실행되면 findByName에서는 findAll에서 만든 값을 찾아와버림
그래서 오류가 발생할 수 있음 !!

⭐️ Test를 짤 때는 순서관계 (의존) 없이 돌아갈 수 있어야함
➡️ 테스트가 끝나면 정보를 삭제해주는 것이 필요

/main/java/hello.hellospring/repository/MemberMemoryRepository
구현체에다가 store에 저장된 걸 다 지우는 메서드를 추가함

public void clearStore(){
        store.clear();
    }

그리고 test 파일로 다시 돌아와서 아래 코드 추가

    @AfterEach
    public void afterEach(){
        repository.clearStore();
    }

한 테스트 수행이 끝날 때마다 호출되는 일종의 콜백 개념
AfterEach를 이용해서 store에 저장된 걸 지워줌

3가지 테스트 모두 잘 작동하는 것을 확인

0개의 댓글