본문 바로가기

Backend/Spring

[Spring] 회원관리 예제 - 3) 회원 서비스

728x90

회원 관리 예제 작성해보기 - 백엔드 개발

  • 비즈니스 요구사항 정리
  • 회원 도메인과 레포지토리 제작
  • 회원 레포지토리 테스트 케이스 작성
  • 회원 서비스 개발
  • 회원 서비스 테스트 - Junit이라는 테스트 프레임워크 사용

 

이제 회원 서비스를 개발하도록 한다. 

비즈니스 요구사항

  • 데이터 : 회원 ID, 이름
  • 기능 : 회원 등록, 조회

 

회원 등록기능(회원가입)을 만들어 보도록 한다.

 

 

회원 서비스 개발
-
1) 회원가입

 

마찬가지로 가장먼저 service라는 패키지를 먼저 만든 후 ,

그 밑에 MemberService라는 클래스를 만든다. 

 

 

다음과 같이 작성해줄 수 있다.

MemberService

public class MemberService {


    private final MemberRepository memberRepository = new MemoryMemberRepository();

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

        //같은 이름이 있는 중복 회원X
        Optional<Member> result = memberRepository.findByName(member.getName()); //입력받은 맴버의 이름으로 레포내에서 검색 -> result에 저장

        //result가 존재 : 이미 해당 이름을 가진 것이 store에 저장되어 있음 -> 가입불가능
       result.ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
        //result가 존재 x : 존재 하지 않음 -> 가입 가능


        memberRepository.save(member);
        return member.getId(); //ID반환
    }

}
더보기

 

여기서 Optional의 강점을 볼 수 있다. 

Optional은 앞서 설명했듯이 null이 반환되었을 때 여러가지 핸들링이 가능한 메소드(예  ifPresent)를 보유하고 있다.

과거에는 이를 다음과 같이 구성하였으나, Optional을 이용하면 다양한 메소드를 이용할 수 있다. 

if (result == 'null')
	throw new IllegalStateException("이미존재하는..");

Null일 가능성이 있으면 Optional로 감싸 주자. 

 

코드를 조금 정리해보자.

굳이 result를 사용할 필요 없이 바로 연결해줄 수 있다.

        memberRepository.findByName(member.getName())
        .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });

또한, 이렇게 길어지는 내용은 그냥 하나의 메소드로 따로 만들어 내는 것이 깔끔하다.

 

별개의 메소드로 분리 

원하는 코드를 드래그 한 뒤 ctrl + T 

or Just cmd + Option + M

 

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;

import java.util.Optional;

public class MemberService {


    private final MemberRepository memberRepository = new MemoryMemberRepository();

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

        //같은 이름이 있는 중복 회원X
        validateDuplicateMember(member);
        

        memberRepository.save(member);
        return member.getId(); //ID반환
    }

// 중복 회원 검사
    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
        .ifPresent(m -> {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        });
    }

}

 

 

회원 서비스 개발
-
2) 회원 조회

 

public class MemberService {


    private final MemberRepository memberRepository = new MemoryMemberRepository();

//생략


    //전체 회원 조회
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    // id를 이용한 1명의 회원 조회 
    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }

}

이는 repository에 사실상 거의 똑같은 기능이 있으므로 가져다 쓰면 된다.

 

참고로, service에 대한 개발은 기획자 개발자 등 많은 사람의 의사소통이 들어가야 하는 부분 이기 때문에 비즈니스 적인 용어를 선택(메소드 이름 등을 붙이는 것)하는 것이 확인할 때 용이하다.

그에 비해 repository는 개발적인 부분이므로 조금 자유롭게 할 수 있다.

예) 이 위에서도 회원 검색(조회)는 사실상 repository에도 같은 기능이 존재하나, service에서의 이름이 좀 더 직관적이다(회원찾기findMember ) 

그에 비해 repository에서의 이름은 조금 더 개발자의 자유도가 높다(전부 찾기 findAll)

 

 

 

 

회원 서비스 테스트


이 또한, 의도대로 기능이 구현되었는지에 대한 테스트가 필요하다.

이번에는 직접 패키지 작성이 아닌 제공되는 도구를 이용해 자동으로 생성해보도록 한다. 

 

테스트 틀 만들기 

테스트 케이스를 만들기를 원하는 클래스 이름에서 "shift + cmd + T"

 

테스트 케이스를 만들기를 원하는 메소드를 클릭해준다.

 

다음과 같이 기본적인 틀을 생성해준다. 

사실 테스트는 한글로 이름을 적어도 무방하다.

실제코드에 포함되는 내용도 아니기 때문에...

 

Store Clear
-
using SAME repository 

 

항상 잊지말기!

테스트 케이스 클래스를 작성할 때는 store을 비울 수 있는 AfterEach를 꼭 생성해 두어야 함!

store는 MemoryMemberRepository클래스에 존재하기 때문에, 이에 대한 객체를 하나 생성하여 만들어준다.

package hello.hellospring.service;

import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

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

class MemberServiceTest {

    //테스트하려는 클래스 객체를 생성
    MemberService memberService = new MemberService();
    //store를 비워주기 위한 MemoryMemberRepository 객체 생성 
    MemoryMemberRepository memberRepository = new MemoryMemberRepository();

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

}

 

 

그런데, 이 때 MemoryMemberRepository는 각 클래스에서 "새로운 객체"를 생성해주었다. 

새로운 객체이게 되면 안에 저장된 정보가 달라질 수 있기 때문에 동일한 객체를 사용하는 것이 맞다.

 

main에 있는 MemberService의 Repository와 

이의 Test용도 클래스인 MemberServiceTest의 Repository가 서로 다른 객체인 것! 

서로 다른 repository를 사용중인 것! 

테스트를 제대로 하기 위해서는 같은 repository를 사용하는 것이 맞다. 

 

이를 위해 다음과 같이 바꾸어 준다. 

MemberService

public class MemberService {

//MemberService클래스가 MemberRepository객체를 가진다.
    private final MemberRepository memberRepository;


//생성자 처럼 MemberService를 생성할 때, memberRepository를 매개변수로 받아 가져서 생성되도록함
    public MemberService(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }

}

MemberService입장에서 직접하지 않고, 외부에서 memberRepository를 넣어준다. 

이를 DI(Dependency Injection) 이라고 한다.

https://ict-nroo.tistory.com/38

 

[JAVA] DI(Dependency Injection)를 이용한 빈 의존성 관리

DI(Dependency Injection)를 이용한 빈 의존성 관리 '자바 웹 개발 워크북 - 엄진영'을 참고하여 학습한 내용입니다. MVC아키텍처에서 Controller가 작업을 수행하려면 데이터베이스로 부터 정보를 가져다

ict-nroo.tistory.com

 

MemberServiceTest

class MemberServiceTest {

    //테스트하려는 클래스 객체를 생성
    MemberService memberService;
    MemoryMemberRepository memberRepository;

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

}

MemberService 객체를 생성할 때, MemoryMemberRepository객체를 미리 생성하여, 생성자로 함께 생성한다.

그리고 이를 @BeforeEach라는 메소드가 실행되기전 실행되는 메소드로 만들어

동일한 객체(레포지토리)를 공유하도록 만든다

 

Given When Then 패턴

given : 이러한 데이터를 기반으로 검증함

when : 검증하려는 상황

then : 검증결과 

이러한 패턴을 따르면 한눈에 알아보기 쉽다. (물론 항상 이런 패턴을 따르게 되는 것은 아니다, 이런 방식으로 연습하고 변형해 나가는 것을 권장) 

이제 이와 같은 패턴으로 회원가입 / 조회 회원 서비스를 검증해보기로 한다. 

 

 

회원가입(join)검증 

 

1. 정상 flow에 대해서 

회원 가입이 정상적으로 되는 케이스 

    @Test
    void 회원가입() {

        //given
        //가입시키려는 멤버와 필수요소인 이름이 필요
        Member member = new Member();
        member.setName("hello");

        //when
        //회원 가입 상황(join 메소드이용)
        Long saveId = memberService.join(member);

        //then
        //회원 가입 한 멤버의 id(savdId)로 검색해서 나온 결과의 멤버(findMember)의 이름과 given에서 입력받은 name이 같아야함
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName());


    }

 

2. 예외 flow에 대해서

중복 회원이 입력되어 예외 메세지가 출력되는 경우 

 

이 또한 코드를 먼저 짠 후, 고쳐나가도록 한다.

try - catch이용 

    @Test
    public void 중복_회원_예외() { //name이 같은 회원이 가입을 원할 때 -> 가입 예외 메세지 떠야함
        //given
        Member member1 = new Member();
        member1.setName("spring");

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

        //when
        //name이 같은 회원이 가입을 원할 때
        memberService.join(member1);
        try{
            memberService.join(member2);
            fail(); //exception이 뜨지 않고 넘어가면 실패!
        }catch(IllegalStateException e){
            //예외가 발생하여 catch부분이 실행되었으면 성공
            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); //예외 발생시 출력시켰던 에러메세지와 같은 것이 출력되는지
        }

        //then


    }

try catch를 이용하여, 이미 "spring"이라는 사용자가 있는 상황에 

또 같은 이름의 사용자가 join메소드로 회원가입을 요청하는 경우 IllegalStateException이 발생하고, 에러 메세지가 정확한지 검증한다.

 

assertThrows

이(try-catch)와 비슷한 로직을 가지는 훨씬 편리한 메소드가 존재한다.

assertThrows를 사용한다. 

실행시키고 싶은 예외상황 코드를 2번째 매개변수에 넣고, 그 결과로 발생하는 예외의 종류를 1번째 매개변수로 준다. 

    @Test
    public void 중복_회원_예외() { //name이 같은 회원이 가입을 원할 때 -> 가입 예외 메세지 떠야함
        //given
        Member member1 = new Member();
        member1.setName("spring");

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

        //when
        //name이 같은 회원이 가입을 원할 때
        memberService.join(member1);

        //assertThrows(코드 실행결과로 기대되는 결과(실행되어야 하는 예외 ),실행할 코드 )
        assertThrows(IllegalStateException.class, () -> memberService.join(member2));


    }

 

에러 메세지까지 검증하는 것은 다음과 같다. ( assertThrows 에서 cmd + shift + V 하면 자동으로 변형됨 )

        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

 

 

 

728x90