[스프링 입문] Section 3. 회원 관리 예제

 

본 포스팅의 내용은

인프런 김영한님의 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의 기반으로 작성했습니다.

 
 

🌱 비즈니스 요구사항

데이터 : 회원ID, 이름

기능 : 회원 등록, 조회

DB : 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

 

컨트롤러 : 웹 MVC의 컨트롤러

서비스 : 핵심 비즈니스 로직

리포지토리 : 데이터베이스에 접근하고 도메인 객체를 DB에 저장하고 관리

도메인 : 비즈니스 도메인 객체, 가령 회원, 주문, 쿠폰 등등 주로 DB에 저장되고 관리됨

 

 

아직 DB가 선정되지 않아서, 우선 인터페이스로 구현 클래스를 변경할 수 있도록 설계

DB는 RDB, NoSQL 등등 다양한 저장소를 고민 중인 상황으로 가정

개발을 진행하기 위해 초기 단계에선 구현체로 메모리 기반의 DB 사용

 

🍃 회원 리포지토리 코드

 

MemberRepository 인터페이스

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

 

MemoryMemberRepository, 메모리 기반 저장소

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(m -> m.getName().equals(name))
                .findAny();
    }

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

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

 

🖥️ 회원 리포지토리 테스트 케이스

개발한 기능을 테스트할 때 main 메서드를 통해서 실행하거나, 웹 어플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는 데 오랜 시간이 걸리고 반복 실행과 여러 테스트를 한 번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.

 

테스트 코드의 핵심은 Given 어떤 상황이 주어지고, When 이걸 실행했을 때, Then 예측된 결과가 나와야 한다.

 

테스트 코드는 src/test/java 하위 폴더에 생성한다.

한 번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 이전 테스트가 다음 테스트에 영향을 미치게 된다. @AfterEach 어노테이션을 사용하면 각 테스트가 종료될 때마다 @AfterEach이 붙은 메서드를 실행한다.

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

        Optional<Member> optionalMember = repository.findById(member.getId());
        Member result = optionalMember.get();

        assertEquals(member, result);
        assertThat(member).isEqualTo(result);
    }
    
    ```
}

 

테스트는 각각 독립적으로 실행돼야 한다. 테스트 순서에 의존관계가 있는 것은 좋은 테스트가 아닐 가능성이 높다.

 

📜 회원 서비스 개발

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    /**
     * 회원가입
     * 같은 이름이 있는 중복 회원 허가 X
     */
    public Long join(Member member) {
        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }
    
    ```
}

 

🍕 회원 서비스 테스트

기존엔 MemberService가 MemoryMemberRepository를 직접 생성했다.

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    
    ```
}

 

회원 서비스를 테스트할 때, afterEach 메서드에서 MemoryMemberRepository의 clearStore를 호출해야 하는데 아래와 같은 테스트는 잘못된 테스트다. MemberService가 들고 있는 MemoryMemberRepository와 실제 clearStore를 호출하는 MemoryMemberRepository 객체는 전혀 다른 객체이기 때문. 단지 static 멤버 변수를 공유하고 있어서 동작은 하지만 올바른 테스트 방법은 아니다.

 

MemberServiceTest

class MemberServiceTest {
    private MemberService memberService = new MemberService();
    private MemoryMemberRepository memberRepository = new MemoryMemberRepository();

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

    @Test
    void join() {
        // given
        Member member = new Member();

        member.setName("kangworld1");

        // when
        Long saveId = memberService.join(member);

        // then
        Member findMember = memberService.findOne(saveId).get();

        assertThat(member.getName()).isEqualTo(findMember.getName());
    }

 

실제 MemberService가 들고 있는 MemoryMemberRepository 객체로 clearStore를 호출해야 하는 상황으로 해결법은 간단하다. MemberService의 코드를 DI 가능하게 변경해 주면 된다.

 

MemberService

public class MemberService {
    private final MemberRepository memberRepository;

    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

	```
}

 

MemberServiceTest

class MemberServiceTest {
    private MemoryMemberRepository memberRepository;
    private MemberService memberService;

    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

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

 

참고로 예외 검증으로 try-catchassertThrows 방식이 존재한다.

try {
    memberService.join(member1);
    fail();
} catch (IllegalArgumentException e) {
    assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}

 

IllegalArgumentException illegalArgumentException = assertThrows(IllegalArgumentException.class, () -> {
    memberService.join(member1);
});