Spring 입문 #3 - 회원 관리 예제: 회원 서비스

1. 회원 서비스 개발

 

 

public class MemberService {
    // "이 셰프는 요리할 때 이 창고(Repository)만 사용할 거야!"라고 선언합니다.
    // final이 붙어있으므로 한 번 정해지면 중간에 다른 창고로 바꿀 수 없습니다.
    private final MemberRepository memberRepository;

    // 셰프가 출근할 때(객체가 생성될 때), 외부에서 어떤 창고를 쓸지 지정해 줍니다.
    // 핵심 부분(생성자 주입)입니다.
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

 

먼저 의존성을 주입한다(DI).

이전 글에서 만들었던 Repository를 가져와쓴다.

private final로 선언하여 이 repository만 사용하고, 다른 repository로 바꿀 수 없도록 선언한다.

새 객체가 생성될 때, memberRepository를 사용하도록 객체 설정한다.

 

public Long join(Member member) {
        // 1. 가장 먼저 중복된 이름이 있는지 비즈니스 규칙을 검사합니다.
        validateDuplicateMember(member); 
        
        // 2. 검사를 무사히 통과했다면, 창고 관리자에게 저장을 지시합니다.
        memberRepository.save(member);
        
        // 3. 저장이 완료되면, 손님에게 영수증 번호처럼 회원 ID를 반환합니다.
        return member.getId();
    }

 

다음으로 회원가입 로직함수 join을 설계한다.

먼저 중복이름을 검사한다.

중복이름 검사를 통과했다면, memberRepository에 member 데이터를 저장한다.

저장이 완료되면, member id를 반환한다.

 

private void validateDuplicateMember(Member member) {
        // 창고에서 지금 가입하려는 사람의 이름으로 회원을 먼저 찾아봅니다.
        memberRepository.findByName(member.getName())
                // 만약 찾은 결과에 이미 회원(m)이 존재한다면 (ifPresent)
                .ifPresent(m -> {
                    // "이미 존재하는 회원입니다"라는 에러를 빵 터뜨립니다.
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

 

앞서 사용했던, 중복이름 검사 함수 validateDuplicateMember 함수를 선언한다.

repository에서 member의 이름을 받아 찾아본다.

만약 회원(m)이 존재한다면(ifPresent), 에러를 throw한다

.

앞서, findByName 함수의 결과는 Optional<Member>라는 껍데기로 감싸 반환되었다.

자바 8부터는 이 Optional 기능이 제공하는 ifPresent() 기능을 사용하여 

값이 있으면 안의 로직을 실행하는 코드를 간단하게 작성할 수 있다.

만약, Optional 기능을 사용하지 않는다면 if (result != null)과 같은 방식으로도 작성 가능하다.

 

// 식당의 모든 회원 명부를 리스트 형태로 달라고 창고에 요청합니다.
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    // 특정 회원 ID를 주면서, 그 번호에 해당하는 회원 딱 한 명만 찾아달라고 요청합니다.
    public Optional<Member> findOne(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

마지막으로 두 번째 핵심 서비스인 조회 함수를 설계한다.

전체 회원을 리스트 형태로 반환하는 findMembers 함수를 findAll() 함수를 사용하여 선언한다.

특정 id의 회원을 조회하는 findOne() 함수를 findById()함수를 사용하여 선언한다.

 

2. 회원 서비스 테스트

 

 

MemberService memberService;
    MemoryMemberRepository memberRepository;

    // @BeforeEach: 각 테스트 메서드(회원가입, 중복_회원_예외)가 실행되기 "직전"에 매번 호출됩니다.
    @BeforeEach
    public void beforeEach() {
        // 1. 새로운 빈 창고를 하나 만듭니다.
        memberRepository = new MemoryMemberRepository();
        // 2. 서비스(셰프)를 만들 때, 방금 만든 새 창고를 넣어줍니다. (이것이 의존성 주입, DI!)
        memberService = new MemberService(memberRepository);
    }

    // @AfterEach: 각 테스트 메서드가 끝날 때 "직후"에 매번 호출됩니다.
    @AfterEach
    public void afterEach() {
        // 창고(메모리)에 남아있는 데이터를 싹 지웁니다. 
        // 안 그러면 이전 테스트에서 가입한 "hello" 회원이 다음 테스트에 영향을 줄 수 있습니다.
        memberRepository.clearStore();
    }

 

먼저 테스트 환경을 초기화하는 beforeEach()와 afterEach()를 선언한다.

beforeEach()는 각 테스트 메서드가 실행되기 직전에 호출되며,

새로운 repository를 만들고, 그 repository를 넣어 새 service를 만든다.(의존성 주입)

afterEach()는 각 테스트 메서드가 실행된 직후에 호출되며,

repository 메모리에 남아있는 데이터를 지워 초기화한다.

 

@Test
    public void 회원가입() throws Exception {
        // Given (준비): 어떤 데이터가 주어졌을 때
        Member member = new Member();
        member.setName("hello"); // 이름이 "hello"인 회원을 만들 준비를 합니다.

        // When (실행): 무엇을 실행하면
        Long saveId = memberService.join(member); // 서비스의 join(회원가입) 기능을 실제로 실행합니다.

        // Then (검증): 어떤 결과가 나와야 하는가
        // 방금 가입해서 발급받은 ID(saveId)로 창고에서 회원을 다시 꺼내옵니다.
        Member findMember = memberRepository.findById(saveId).get();
        // 내가 처음에 만든 member의 이름("hello")과 DB에서 꺼낸 findMember의 이름이 똑같은지 확인합니다.
        assertEquals(member.getName(), findMember.getName()); 
    }

 

다음으로, 회원가입 기능을 테스트한다.

테스트 코드는 이전 글에서 말했듯 Given-When-Then(준비-실행-검증) 패턴을 적용한다.

새 member 객체를 생성하고 member의 name을 hello로 설정한다. (Given)

join 서비스 기능을 직접 실행하여 반환된 id를 saveId에 담는다. (When)

방금 저장한 saveId를 findById 함수를 사용하여 id로 회원 조회하여 얻은 멤버 객체를 findMember에 담고,

findMember의 이름과 처음에 만든 member의 이름(hello)을 assertEquals()를 사용하여 비교한다. (Then)

 

@Test
    public void 중복_회원_예외() throws Exception {
        // Given (준비)
        Member member1 = new Member();
        member1.setName("spring"); // 첫 번째 회원 이름 "spring"
        
        Member member2 = new Member();
        member2.setName("spring"); // 두 번째 회원 이름도 "spring" (일부러 중복되게 만듦)

        // When (실행)
        memberService.join(member1); // 첫 번째 회원은 정상적으로 가입됩니다.
        
        // Then (검증)
        // 두 번째 회원이 가입하려 할 때, IllegalStateException 에러가 "터져야만" 테스트가 성공합니다.
        // () -> memberService.join(member2) 이 부분이 실행될 때 앞의 예외가 발생하는지 잡는 문법(assertThrows)입니다.
        IllegalStateException e = assertThrows(IllegalStateException.class,
                () -> memberService.join(member2));
                
        // 터진 에러의 메시지가 우리가 서비스 코드에 적어둔 "이미 존재하는 회원입니다."와 정확히 일치하는지 한 번 더 확인합니다.
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
    }

 

마지막으로, 중복 회원 가입이 잘 차단되는지, 에러 예외 검증 테스트를 진행한다.

spring이라는 동일 이름을 가진 member1, member2 객체를 생성한다. (Given)

member1을 회원가입 시킨다. (When)

Exception을 선언하여  member2가 join할 때 예외를 throw하여 e에 담고,

에러 메시지가 앞서 예외 처리하여 나오는 출력 메시지와 동일한지 비교하여 검증한다. (Then)

 

 

초록색 체크표시가 모두 표시되어 있는 것을 확인할 수 있다.