회원 관리 예제 - 도메인 및 리포지토리의 개발 및 테스트

guswls·2023년 1월 9일
1

스프링 입문

목록 보기
4/13
post-thumbnail

이 포스트는 김영한 이사님의 스프링 입문 강의를 듣고 작성하였습니다.

1. 설계

이번 포스트부터는 간단한 회원 관리 예제를 구현해 볼 예정이다.

들어가기 앞서 일반적인 웹 어플리케이션의 계층구조를 간략하게 살펴보면 다음과 같다.

각각의 기능들을 살펴보면 다음과 같다.

  • 컨트롤러 : 웹 MVC의 컨트롤러 역할
  • 서비스 : 핵심 비지니스 로직 구현 (ex. 회원 중복 가입 불가 로직 등등)
  • 도메인 : 비지니스 도메인 객체 (ex. 회원, 주문, 쿠폰 등등 주로 데이터 베이스에 저장되고 관리)
  • 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리

이러한 구조를 사용하여 우리도 실습을 진행할 예정이며 구현할 인터페이스 및 클래스는 다음과 같다.

Member : 회원 도메인
MemberService : 회원 관리에 대한 비지니스 로직 담당(회원 가입, 회원 조회)
MemberRepository : 데이터 베이스 접근에 관한 동작들을 인터페이스로 정의
MemoryMemberRepository : MemberRepository에 관한 구현체

2. Member 및 MemberRepository 구현

2-1. Member

먼저 다른 클래스와의 구분을 위해 domain 패키지를 생성한 후에 Member클래스를 만들어준다. Member에는 pk의 역할을 하는 id와 이름을 나타내는 name이 존재한다.

//Member.java
package memberpractice.memberpractice.domain;

public class Member {

    private Long id;
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }
}

2-2. MemberRepository

MemberRepositoryMember와 마찬가지로 repository 패키지를 만들 후에 인터페이스로 생성하여 준다.

//MemberRepository.java
package memberpractice.memberpractice.repository;

import memberpractice.memberpractice.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(); //모든 회원 조회
}

여기서 Optional이라는 반환 타입이 조금 생소한데 간략하게 얘기해서 NullPointerException을 방지하기 위해 한번 감싸주는 Wrapper클래스라고 볼 수 있으며 Java8부터 지원되는 기능이다.

이렇게 Interface를 통해 4가지 기능에 대해서 정의를 해주었다.

3. MemoryMemberRepository 구현

앞서 정의한 MemberRepository인터페이스에 대한 구현체가 MemoryMemberRepository이다. 이 클래스에서는 저장공간으로 Java의 메모리를 사용하도록 만들 예정이다.

//MemoryMemberRepository.java
package memberpractice.memberpractice.repository;

import memberpractice.memberpractice.domain.Member;

import java.util.*;

public class MemoryMemberRepository implements MemberRepository{

    private static Map<Long, Member> store = new HashMap<>(); //저장공간
    private static long sequence = 0L; //pk

    @Override
    public Member save(Member member) {
        member.setId(++sequence); //id를 지정하고
        store.put(member.getId(), member); //지정된 id로 해당 member를 넣고
        return member; //결과 반환
    }

    @Override
    public Optional<Member> findById(Long id) {
        return Optional.ofNullable(store.get(id)); //null도 반환될 수 있게 감싸주고 반환
    }

    @Override
    public Optional<Member> findByName(String name) {
        return store.values().stream()
                .filter(member -> member.getName().equals(name)) //member에 인자로 전달받은 name과 같은 이름의 데이터가 있는지 확인
                .findAny(); //있으면 해당 member반환
    }

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

중요한 부분은 주석으로 설명을 하였다. 반환값으로 null을 허용해주는 ofNullable이나 stream을 통해 값을 조회하는 방법들은 이번 실습을 통해 처음 알게 된 내용이다. 따라서 이에 대해선 나중에 다뤄볼 예정이다.

4. MemoryMemberRepository test

우리가 구현한 MemoryMemverRepository에 대한 검증은 test code를 작성하여 진행할 것이다.
우선 test폴더에서 src에서 했던 것과 같이 Repository패키지를 만든 후 그 안에 MemoryMemberRepositoryTest를 만들어 진행한다.

//MemoryMemberRepositoryTest.java
package memberpractice.memberpractice.repository;

import memberpractice.memberpractice.domain.Member;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.List;

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

class MemoryMemberRepositoryTest {
    MemoryMemberRepository repository = new MemoryMemberRepository();

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

        repository.save(member);

        Member result = repository.findById(member.getId()).get();
        //Assertions.assertEquals(member, result); Junit
        assertThat(member).isEqualTo(result);
    }

    @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();

        assertThat(result).isEqualTo(member1);
    }

    @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();

        assertThat(result.size()).isEqualTo(2);
    }
}

Test는 save, findbyId, findAll 3가지 메소드에 대해서 진행을 하였다. 각각의 메소드에 대해서 독립적으로 테스트를 진행할 수 있고 assertThat().isEqualto()를 통해 실제 값기대 값들을 비교하는 방식으로 테스트를 진행하였다.

findByName()findAll()에는 1) 객체를 생성하고 2) 이름을 지정한 후에 3) 해당 객체를 save하는 과정이 공통적으로 들어가있다.

이렇게 Member에 값을 저장한 다음에 MemoryMemberRepository에 존재하는 findByName() 혹은 findById()와 같은 메소드를 수행하여 얻어온 결과값과 기대값을 비교하여 테스트를 진행한다.

하나의 메소드만 실행하는 것이 아닌 전체의 메소드를 실행할 수도 있다. 메소드 단위가 아닌 클래스 단위에서 테스트를 진행하여보자.
3가지의 테스트 케이스를 동시에 수행하자 findByName() 메소드에서 오류가 발생하였다.

오류를 간단히 분석하면 findByName()결과값으로 반환된 객체와 테스트에서 생성된 객체가 서로 다르다는 것을 확인할 수 있다.

이러한 오류가 발생하는 이유는 각각의 테스트 케이스에서 객체를 생성된것이 그대로 유지되면서 전에 작성한 테스트 케이스의 영향을 받게 되는 것이다.

테스트 코드 작성 시 주의할 점

테스트 코드를 작성할 때는 테스트의 순서에 의존적으로 짜면 절대 안된다. 따라서 하나의 테스트가 끝날 때마다 생성된 데이터를 모두 지워주어야 한다.

이제 주의사항을 반영하여 MemoryMemberRepositorystore를 비우는 메소드를 추가하여주자.

//MemoryMemberRepository.java
package memberpractice.memberpractice.repository;

import memberpractice.memberpractice.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); //id를 지정하고
        store.put(member.getId(), member); //지정된 id로 해당 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)) //member에 인자로 전달받은 name과 같은 이름의 데이터가 있는지 확인
                .findAny(); //있으면 해당 member반환
    }

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

	//추가
    public void clearStore(){
        store.clear();
    }
}

우리가 작성한 테스트 코드에서도 @afterEach 어노테이션을 통하여 각 테스트가 끝날 때마다 수행할 동작을 만들어준다. 즉, 각각의 테스트가 끌날 때마다 저장공간을 모두 비우도록 한다.

//MemoryMemberRepositoryTest.java
package memberpractice.memberpractice.repository;

import memberpractice.memberpractice.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 {
    MemoryMemberRepository repository = new MemoryMemberRepository();
	
    //추가
    @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();
        //Assertions.assertEquals(member, result); Junit
        assertThat(member).isEqualTo(result);
    }

    @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();

        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);
    }
}

이렇게 코드를 추가하자 테스트가 올바르게 동작하는 것을 확인할 수 있다.

5. 총정리

이번 챕터는 스프링을 다루는 것이 아닌 순수하게 자바로 기본적인 웹 어플리케이션의 구조를 구현하는 실습을 진행하였다.

가장 머리에 많이 남았던 부분은 테스트 케이스에 관련된 부분이었다. Junit에 대해선 대충 어떤건지는 알고 있었고 간단하게 학교 과제로도 진행했던 적이 있었다. 하지만 각각의 테스트는 독립적으로 동작해야된다는 것을 잘 모르고 테스트 코드를 짰던 것 같다.

이번 실습은 스프링 자체를 배운 것은 아니었지만 기본적인 웹 어플리케이션 구조를 구성하고 구현하는 과정, 그에 따라 테스트 코드를 작성하는 것 까지 스프링을 본격적으로 배우기 전에 아주 중요한 내용 다뤘다.

다음 포스트에서는 뒤에 이어 Service에 대해서도 실습을 진행할 예정이다.

profile
안녕하세요

0개의 댓글