본문 바로가기
[inflearn] 스프링 입문

스프링 입문) 7. 회원 관리 예제 - 백엔드 개발(2)

by 슬픈 야옹이 2023. 4. 3.

(회원 서비스 개발 ~ 회원 서비스 테스트)

 

지난 포스트에 이어 회원 관리 서비스 예제를 개발한다.

 

- 비즈니스 요구사항 정리

- 회원 도메인, 리포지토리 작성

- 리포지토리 테스트 케이스 작성

- 회원 서비스 개발 <-

- 회원 서비스 테스트

 

 

회원 서비스 개발

회원 서비스를 작성한다.

리포지토리와의 차이점은 리포지토리는 회원 데이터 저장 및 검색 등 서버 내부 로직에 관한 것이라면,

서비스는 회원가입 등 실제 사용자에게 제공되는 비즈니스 로직에 관한 내용이 작성된다.

 

프로젝트 경로 하위에 service 패키지를 생성하고, MemberService 클래스를 추가한다.

 

 

첫 번째로 회원가입 기능을 작성해본다.

비즈니스 로직 상 같은 이름으로는 가입하지 못하도록 한다.

// 회원 가입 기능
public Long join(Member member) {

    // 중복된 이름으로는 가입 불가.
    checkNameDuplication(member);

    repository.save(member);
    return member.getId();
}

private void checkNameDuplication(Member member) {
    repository.findByName(member.getName()) // findbyName()의 리턴 타입은 Optional<Member>
            .ifPresent(m -> {
                throw new IllegalStateException("이미 존재하는 회원입니다.");
            });
}

checkNameDuplication() 메서드 내용은 join() 내부에 작성할 수도 있으나, 따로 메서드로 만들어 기능을 분리시켰다.

 

Optional<T>.ifPresent() : Optional<>이 감싼 객체가 존재할 때 (null이 아닐 때) 지정한 코드를 실행한다.

 

 

같은 방식으로 전체 회원 조회 기능, id로 회원을 찾는 기능 등을 추가한다.

전체 MemberService 소스코드는 다음과 같다.

package hdxian.hdxianspring.service;

import hdxian.hdxianspring.domain.Member;
import hdxian.hdxianspring.repository.MemberRepository;
import hdxian.hdxianspring.repository.MemoryMemberRepository;

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

public class MemberService {

    private final MemberRepository repository = new MemoryMemberRepository();


    // 회원 가입 기능
    public Long join(Member member) {

        // 중복된 이름으로는 가입 불가.
        checkNameDuplication(member);

        repository.save(member);
        return member.getId();
    }

    private void checkNameDuplication(Member member) {
        repository.findByName(member.getName()) // findbyName()의 리턴 타입은 Optional<Member>
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    public List<Member> findMembers() {
        return repository.findAll();
    }

    public Optional<Member> findOne(Long memberId) {
        return repository.findById(memberId);
    }


}

MemberService에서 사용할 MemberRepository 참조 변수를 하나 선언하고,

MemoryMemberRepository 객체를 가리키도록 하였다. (후에 이 참조변수가 가리키는 구현체를 바꾸면 저장소 변경 작업이 쉽게 완료된다.)

 

여기에 선언된 MemberRepository는 이후 서버를 시작해 실제로 서비스할 때 사용될 리포지토리다.

 

 

 

 

회원 서비스 테스트

작성된 서비스 기능을 테스트한다.

 

지난 포트스에서는 test 폴더에 직접 패키지를 생성하여 테스트 코드를 만들었지만,

사실 인텔리제이는 해당 클래스의 테스트 코드를 만들어주는 기능이 있다.

 

내용이 없는 껍데기 코드를 만들어주는 기능이고, 경로는 원본 클래스 기준으로 설정된다.

Create Test 기능

 

라이브러리는 JUnit5를 선택하고, 테스트할 메서드들을 선택한 뒤 [OK]를 누른다.

Create Test

 

그러면 이렇게 원본 파일 경로와 같은 패키지 경로로 테스트 클래스가 생성된다.

 

 

테스트 코드를 작성하기 전에, 테스트에 사용할 MemberService를 선언하고 afterEach() 메서드를 작성한다.

 

+) 중요!

afterEach() 메서드에는 테스트 실행 직후 리포지토리에 대한 초기화 작업을 작성하려 하는데,

MemberService 인스턴스로는 그런 기능을 직접 이용할 수가 없다.

MemberService 인스턴스로는 초기화 작업을 수행할 수 없다.

 

하는 수 없이 MemoryMemberRepository 인스턴스를 하나 더 생성하는 수밖에 없을 듯 하다.

MemoryMemberRepository 인스턴스 추가(?)

 

지금까지 작성한 MemberServiceTest 클래스를 보면 다음과 같다.

package hdxian.hdxianspring.service;

import hdxian.hdxianspring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {

    MemberService service = new MemberService();
    MemoryMemberRepository repository = new MemoryMemberRepository();

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

    @Test
    void join() {
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

근데 뭔가 이상하다. 

코드를 이렇게 작성하면 MemberService가 동작하는데 쓰는 repository와

Test 클래스에서 사용하는 repository가 서로 다른 객체다.

 

하나의 기능이 동작하는데 서로 다른 두 개의 데이터 저장소를 이용하는 셈이다.

 

물론 현재 예제 상으로는 MemoryMemberRepository가 실제로 데이터를 저장하는 자료구조인 store가 static으로 선언되어 있기 때문에, 두 객체가 서로 다르더라도 같은 데이터 저장소를 이용하기 때문에 동작에는 문제가 없다.

 

하지만 여기에서나 문제가 없지, 실제로 모든 repository가 지금처럼 자료구조를 static으로 선언할 리 없기 때문에,

이런 식으로 하나의 기능에 두 개의 객체를 사용해서는 안 된다.

 

이 문제(하나의 기능 구현에 두 개의 리포지토리 객체를 사용하는 것)를 해결하기 위해

MemberService 클래스를 변경한다.

MemberService 객체가 생성될 때 자신이 사용할 repository를 새로 생성하지 않고,

생성자를 통해 외부로부터 전달받도록 한다.

 

이렇듯 클래스가 의존하는 요소를 직접 생성하지 않고 생성자 등을 통해 외부로부터 전달받도록 하는 기법

DI (Dependency Injection, 의존성 주입) 라 한다.

 

어찌 되었든, 클래스가 자신이 사용할 요소를 그때그때 새로 만들어 사용하면 같은 요소를 사용하는 클래스끼리 요소가 통합되지 않아 여러 문제가 발생할 수 있는데, DI기법을 사용하면 이러한 요소를 통합할 수 있어 문제가 발생하지 않는다.

 

 

돌아와서, MemberService를 이렇게 변경하면 MemberServiceTest 클래스에서도 리포지토리 초기화 작업이 가능하다.

 

테스트 메서드 작성 전의 MemberServiceTest 클래스 전체 소스 코드는 다음과 같다.

package hdxian.hdxianspring.service;

import hdxian.hdxianspring.repository.MemoryMemberRepository;
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 memberService;
    MemoryMemberRepository memberRepository;

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

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

    @Test
    void join() {
    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

먼저 join() (회원가입 기능)을 작성해본다.

테스트 케이스를 작성할 때, given-when-then 문법을 따르면 좀 더 체계적으로 작성할 수 있다.

테스트 케이스를 어떤 상황이 주어졌을 때(given), 어떤 작업을 수행하면(when), 어떠한 결과가 발생한다(then) 의

3가지 순서로 나누어 작성하는 것이다.

 

join()을 given-when-then으로 작성하면 다음과 같다.

@Test
void join() {
    // given
    Member member = new Member();
    member.setName("spring");

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

    // then
    Member findMember = memberService.findOne(savedId).get();
    Assertions.assertThat(findMember.getName()).isEqualTo(member.getName());
}

given : "spring"이라는 이름의 member가 있다.

when : 이 member가 회원가입을 한다. 회원가입을 하면 저장된 member의 id값을 얻을 수 있다.

then : member와 같은 id을 가진 findmember의 이름이 member의 이름과 같다.(spring이다.)

 

 

테스트는 정상 실행 케이스도 중요하지만, 오류 발생 등의 상황에도 처리 로직이 잘 작동하는지 확인해야 한다.

 

MemberService에서, 중복된 이름의 회원이 가입을 시도하면 예외가 발생하도록 작성했었다.

이에 대한 테스트 케이스를 작성한다.

@Test
void 중복_회원_예외() {
    // given
    Member member1 = new Member();
    member1.setName("hello");

    Member member2 = new Member();
    member2.setName("hello");

    // when
    memberService.join(member1);
    IllegalStateException e = org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class, () -> memberService.join(member2));

    // then
    Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}

테스트 코드는 실제 빌드에 포함되지 않아서,

비영어권 업무환경에서는 한글로도 테스트 메서드를 작성하는 경우도 있다고 한다.

 

 

findMembers, findOne() 메서드는 같은 방식의 반복이어서 생략하였다.

MemberServiceTest 전체 소스 코드는 다음과 같다.

package hdxian.hdxianspring.service;

import hdxian.hdxianspring.domain.Member;
import hdxian.hdxianspring.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;

class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

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

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

    @Test
    void join() {
        // given
        Member member = new Member();
        member.setName("spring");

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

        // then
        Member findMember = memberService.findOne(savedId).get();
        Assertions.assertThat(findMember.getName()).isEqualTo(member.getName());
    }

    @Test
    void 중복_회원_예외() {
        // given
        Member member1 = new Member();
        member1.setName("hello");

        Member member2 = new Member();
        member2.setName("hello");

        // when
        memberService.join(member1);
        IllegalStateException e = org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class, () -> memberService.join(member2));

        // then
        Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

    @Test
    void findMembers() {
        
    }

    @Test
    void findOne() {
    }
}