[Spring] TDD로 멤버십 등록 API 구현 예제 -(3/5)_Service계층

윤재열·2022년 5월 2일
0

Spring

목록 보기
57/72
post-custom-banner

Service 계층 개발

이번에는 멤버십을 등록하는 서비스 계층을 개발해야 합니다. 앞서 Repository 계층을 개발했던 것처럼 이번에도 다음과 같이 서비스 계층에 대한 테스트 클래스부터 작성해야 합니다.
서비스 계층은 데이터베이스에 데이터를 처리하는 Repository 계층을 Mocking하기 위해 MockitoExtension에서 실행되도록 합니다.

  • JUniit5와 Mockito를 연동하기 위해서는 @ExtendWith(MockitoExtension.class)를 사용해야 한다.
@ExtendWith(MockitoExtension.class)
public class MembershipServiceTest {
}

그리고 이제 멤버십 등록에 대한 테스트 코드를 작성해야 하는데, 사용자 Id와 멤버십 타입으로 이미 멤버십이 존재하여 실패하는 테스트 코드부터 작성하도록 해줍니다.
즉, membershipRepository의 findByUserIdAndMembershipType을 호출했을 때 결과가 null이 아니여야 하는것입니다.
그리고 이에 대한 결과로 Exception을 던지도록 구현하고자 할때, 이에 대한 테스트 코드를 작성하면 다음과 같습니다.

@ExtendWith(MockitoExtension.class)
public class MembershipServiceTest {

    private final String userId = "userId";
    private final MembershipType membershipType = MembershipType.NAVER;
    private final Integer point =10000;

//    doReturn(): Mock 객체가 특정한 값을 반환해야 하는 경우
//    doNothing(): Mock 객체가 아무 것도 반환하지 않는 경우(void)
//    doThrow(): Mock 객체가 예외를 발생시키는 경우


    @Test
    @DisplayName("멤버십등록실패_이미존재합니다.")
    void failRegister(){
        //given
        doReturn(Membership.builder().when(membershipRepository).findByUserIdAndMembershipType(userId,membershipType));

        //when
        final assertThrows(MembershipException.class, () -> target.addMembership(userId,membershipType,point));
        //then
        assertThat(result.getErrorResult()).isEqalTo(MembershipErrorResult.DUPLICATED_MEMBERSHIP_REGISTER);
    }
}
  • 위와 같이 테스트 코드를 작성하면 많은 부분에서 컴파일 에러가 발생합니다.
    하나 하나씩 차근차근 구현해보도록 합니다.

여기서 userId,membershipType,point 값들은 다른 테스트 메서드에서도 사용할것 같으므로 클래스 변수로 빼두도록하고, 테스트 대상인 클래스와 의존성이 있는 클래스를 추가해주도록 합니다.

  • 먼저 테스트 대상인 MembershipService에는 가짜 객체 주입을 위한 @InjectMocks를 붙여주어야 한다. 그리고 MembershipRepository에는 가짜 Mock 객체 생성을 위해 @Mock 어노테이션을 붙여주면 된다.

컴파일 에러를 해결하기 위해 다음과 같은 클래스들을 작성했습니다.

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum MembershipErrorResult {

    DUPLICATED_MEMBERSHIP_REGISTER(HttpStatus.BAD_REQUEST,"Duplicated Membership Register Request"),
    ;
    
    private final HttpStatus httpStatus;
    private final String message;
}
@Getter
@RequiredArgsConstructor
public class MembershipException extends RuntimeException{

    private final MembershipErrorResult errorResult;
}
@Service
public class MembershipService {
    
    public Membership addMembership(final String userId, final MembershipType membershipType,final Integer point){
        return null;
    }
}
  • 위와 같이 MembershipErrorResult를 작성한 이유는 추후에 MembershipException이 throw 되었을 때 RestControllerAdvice를 통해 membershipErrorResult의 HttpStatus와 message를 반환하기 위함입니다.

  • MembershipException이 RuntimeException(언체크 예외)로 된 이유는 예외 복구 가능성이 없으므로 예외 처리를 개발자가 강제할 필요가 없으며, 트랜잭션 내에서 언체크 예외 만이 자동으로 롤백되기 때문이되고 체크 예외는 롤백되지 않기 때문이다. (물론 옵션으로 체크 예외도 롤백되도록 변경할 수 있다.)

위와 같은 클래스들을 추가하였으면 컴파일 에러는 해결이 되었을 것이다. 그리고 테스트 클래스를 실행하면 다음과 같이 예외가 throw되지 않아서 테스트가 실패하게 된다. (테스트를 작성하고 프로덕션 코드를 작성하고 테스트를 빠르게 실행하는게 TDD의 흐름이다.)

  • 위의 에러를 해결하기 위해 MembershipRepository에서 Membership을 조회하여 있으면 throw하도록 기존의 코드를 수정합니다.

그러면 이제 중복되어 실패하는 멤버십 등록 테스트는 통과하게 된다. 실패 테스트에 대한 작성이 마무리 되었으니 멤버십 등록에 성공하는 테스트 코드를 추가로 작성하도록 하자. 멤버십 등록의 응답값으로는 멤버십 id와 멤버십 타입을 반환해주어야 하므로 두 값이 잘 반환되는지를 통해 검증하도록 하자.

이에 대한 테스트 코드를 작성하면 다음과 같다.

이번에는 추가로 membershipRepository를 통해 메서드가 호출되었는지를 검증하는 verify 단계까지 추가로 작성해보았습니다.
그리고 테스트를 실행하면 result가 null이므로 NPE가 발생합니다.
해당 테스트를 통과하기 위해서는 MemberService를 다음과 같이 수정해야 합니다.

@Service
@RequiredArgsConstructor
public class MembershipService {

    private final MembershipRepository membershipRepository;

    public Membership addMembership(final String userId, final MembershipType membershipType,final Integer point){

        final Membership result = membershipRepository.findByUserIdAndMembershipType(userId, membershipType);
        if( result != null ){
            throw new MembershipException(MembershipErrorResult.DUPLICATED_MEMBERSHIP_REGISTER);
        }
        final Membership membership = Membership.builder()
                .userId(userId)
                .point(point)
                .membershipType(membershipType)
                .build();

        return membershipRepository.save(membership);
    }
}
  • 위와 같이 addMembership 함수를 구현하면 테스트를 통과하고, 초록막대를 볼 수 있다.

끝났다고 착각할 수 있지만, 아직 끝나지 않았다. 왜냐하면 리팩토링의 단계가 진행되지 않았기 때문이다.

위와 같은 코드를 보니 MembershipService에서 컨트롤러로 반환하는 객체는 Membership 엔티티이다. 엔티티를 반환하는 것 보다는 DTO를 반환하는 것이 바람직하므로 DTO를 반환하도록 리팩토링하도록 하자.

우리는 TDD로 개발하고 있으므로 프로덕션 코드(MembershipService)가 아닌 테스트 코드부터 수정하도록 하자.

  @Test
    @DisplayName("멤버십등록성공")
    public void successRegister(){
        //given
        doReturn(null).when(membershipRepository).findByUserIdAndMembershipType(userId, membershipType);
        doReturn(membership()).when(membershipRepository).save(any(Membership.class));

        //when
        final MembershipResponse result = target.addMembership(userId, membershipType, point);

        //then
        assertThat(result.getId()).isNotNull();
        assertThat(result.getMembershipType()).isEqualTo(MembershipType.NAVER);

        //verify
        verify(membershipRepository, times(1)).findByUserIdAndMembershipType(userId, membershipType);
        verify(membershipRepository, times(1)).save(any(Membership.class));

    }

그리고 컴파일 오류를 해결하기 위해 MembershipResponse 클래스를 추가하고, MembershipService를 수정해야 합니다.

@Getter
@Builder
@RequiredArgsConstructor
public class MembershipResponse {

    private final Long id;
    private final MembershipType membershipType;
}
@Service
@RequiredArgsConstructor
public class MembershipService {

    private final MembershipRepository membershipRepository;

    public MembershipResponse addMembership(final String userId, final MembershipType membershipType,final Integer point){

        final Membership result = membershipRepository.findByUserIdAndMembershipType(userId, membershipType);
        if( result != null ){
            throw new MembershipException(MembershipErrorResult.DUPLICATED_MEMBERSHIP_REGISTER);
        }
        final Membership membership = Membership.builder()
                .userId(userId)
                .point(point)
                .membershipType(membershipType)
                .build();

        final Membership savedMembership = membershipRepository.save(membership);

        return MembershipResponse.builder()
                .id(savedMembership.getId())
                .membershipType(savedMembership.getMembershipType())
                .build();
    }
}
  • 이렇게 되면 서비스 계층에 대한 개발이 완료된 것입니다.
profile
블로그 이전합니다! https://jyyoun1022.tistory.com/
post-custom-banner

0개의 댓글