Spring 사용하기 (Coupon_check 백엔드(3) - 서비스)

김태훈·2022년 12월 27일
0

Spring_CouponCheck

목록 보기
6/14

1. 서비스 만들기

서비스가 무엇인지 잘 모른다면, 다시 한번 보고오자. 서비스 다시보기
당연히 해당 서비스를 구현하기 위해서 앞서 정의했던 repository 와 domain 객체들이 준비가 되어야 하겠다.

기본적인 구현할 사항들 중 회원가입 부터 해보자.

package Goat.CouponCheck.service;

import Goat.CouponCheck.repository.MemoryRepository;
import Goat.CouponCheck.repository.Repository;
import hello.hellospring.domain.Member;

public class MemberService {

    private final Repository repository = new MemoryRepository();

    /* 회원가입 */
    public Long join(Member member){
        //중복회원 방지
        validateDuplicateMember(member);
        repository.saveMember(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        repository.findByName(member.getName())
            .ifPresent(m -> {
                try {
                    throw new IllegalAccessException("이미 존재하는 회원입니다.");
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            });
    }
}

final
final 변수는 보통 자바에서는, 오직 한 번만 할당할 수 있는 entity를 정의할 때 사용된다. final 변수가 할당되면, 항상 같은 값을 가지게 된다. 만일 객체가 참조된다면, 객체의 상태만 바뀔 뿐, 매번 동일한 내용을 참조한다는 뜻이다. (객체가 immutable 하지는 않다는 뜻이다)

<코드 설명>

  1. 먼저 인터페이스 repository를 MemoryRepository로 객체를 생성한 부분이 조금 이해가 안될 수 있지만 받아들이기로 하자. 자바 내에서, repository를 implements한 class인 MemoryRepository를 new로 생성하면, 알아서 구현체가 해당 class의 메소드를 불러오는 양상인 것 같다.

  2. ifPresent문법은 앞서서 Optional을 다룰 때 이미 다룬 바 있다.

private void validateDuplicateMember(Member member) {
        repository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }
 원래는 이런 코드를, 통채로 validateDuplicateMember대신 넣었지만, 이런 경우에는 따로 메소드를 뽑아내는게
 낫다고 한다. 따라서, 해당 코드를 ctrl+alt+shift+t 단축키를 이용하여 method를 extract해주자.

2. 서비스 테스트 만들기

해당 서비스 클래스 내에서, ( ctrl + shift + t ) 를 누르면, test를 자동으로 생성해준다.

서비스 테스트는 기본적으로 다음의 절차로 짜는게 좋다.
1. given
2. when
3. then
상황이 주어졌을 때 (given),
해당 상황에 맞는 작업을 실행 하면 (when),
어떤 결과가 나올 것인가? (then)

이에 맞춰서 코드를 작성해보자.

test code는 최대한 꼼꼼해야한다. 무엇을 작성해야할까 생각해보면, 가장먼저 회원가입이 잘 되었는지를 확인하면 될 것 같지만, 더 중요한 것은, 특별한 케이스에 대한 test code를 작성하는 것이다. 우리가 앞서, 회원가입을 할 때, 중복된 name을 가진 회원의 회원가입을 방지했다. 따라서 해당 case에 대한 test code 또한 작성되어야 할 것이다.

  • 회원 가입 테스트 코드
@Test
    void join() {
        //given
        Member member  = new Member();
        member.setName("goat");
        //when
        Long saveId = memberService.join(member);
        //then
        Member findMember = memberService.findOne(member.getId()).get(); //Optional로 반환되는거 get으로 member를 꺼냄
        Assertions.assertThat(member.getName()).isEqualTo(findMember.getName());
}
  • 중복 회원 가입 테스트 코드
@Test
    void 중복_회원_예외(){
        //given
        Member member1 = new Member();
        Member member2 = new Member();

        member1.setName("goat1");
        member2.setName("goat1");
        //when
        memberService.join(member1);
        try{
            memberService.join(member2);
            fail();
        }catch(IllegalStateException e ){
            Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.1");
        }

        //then

    }

중복 회원 가입 테스트 코드를 작성할때, 위 코드와 같이 try catch를 넣어도 상관은 없다.
하지만 더 간단히 하려면,
assertThrows 메서드를 활용하면 된다.

  • assertThrows 메서드 활용
@Test
    void 중복_회원_예외(){
        //given
        Member member1 = new Member();
        Member member2 = new Member();

        member1.setName("goat1");
        member2.setName("goat1");
        //when
        memberService.join(member1);
        assertThrows(IllegalStateException.class,
        ()->memberService.join(member2));
    }

이렇게 작성한다면, assertThrows의 두번째 인자가 람다함수로써, 동일한 name을 가진 member2가 join되면, exception이 터지는데, 해당 exception을 체크하게 한다.
우리가 이미, memberService에서 exception을 정의할때, IllegalStateException으로 정의를 했으므로, 다른 exception이 나오면, 테스트를 실패할 수 밖에 없을 것이다.
추가적으로 메세지를 반환하고 싶다면,

  • 에러메세지 반환 후 체크
IllegalStateException msg = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
Assertions.assertThat(msg.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

3. 해당 Test가 가지고 있는 문제

같은 repository속에서 복수개의 test가 실행이 된다. 만약, 같은 name을 가진 member로 테스트가 계속해서 이루어 진다면, repository에 계속 해당 member 가 저장이 되므로, repository를 초기화 해주는 과정이 반드시 필요하겠다.

import한 클래스가 MemberSerivce 뿐이므로, repository를 불러와야한다.

MemoryRepository memoryRepository = new MemoryRepository();

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

다음과 같은 코드를 앞에 추가해주면 되겠다.

깊게 생각해 볼 만한 사항

현재 MemoryRepository 객체를 test code를 작성할 때 생성했다.
하지만, MemberService 클래스에서도 MemoryRepository를 불러오는데 거기서 불러오는 객체와
test code를 작성할 때 불러오는 객체가 달라질 수도 있는 것이 아닌가.
(물론 현재 상황은 MemoryRepository에서 memory로 다루고 있는 자료구조가 static으로 되어있기 때문에 인스턴스를 생성해도 같은 메모리를 쓰고 있는 상황인 것은 맞다.)

따라서, 조금 찜찜한 부분(다른 인스턴스 생성을 생기는 문제점)을 없애기 위해서, 다음과 같이 작성하는 것이 좋겠다.

  1. MemberService 클래스에서 외부에서 넣어주도록 injection 방식으로 설정
public class MemberService {

    private final Repository repository;

    public MemberService(Repository repository) {
        this.repository = repository;
    }
  1. MemberServiceTest 클래스 수정
MemberService memberService;
MemoryRepository memoryRepository;

@BeforeEach
public void beforeEach(){
	memoryRepository =  new MemoryRepository();
	memberService = new MemberService(memoryRepository);
}

이렇게 하면 각 테스트를 실행하기 전에, before each로
repository를 만들고,
해당 repository를 MemberService에서 만든 constructor 메서드로 memberService 객체를 만든다.
그러면 test에서 같은 repository로 memberService를 테스트할 수 있게 된다.
해당 과정에서 MemberService 클래스에서 new로 직접 생성하지 않고, 외부에서 repository를 넣어주는데 이러한 방식을 Dependency Injection (DI) 방식이라고 한다.

profile
기록하고, 공유합시다

0개의 댓글