데이터 검증과 예외처리2

hanana·2023년 9월 18일
post-thumbnail

이전의 테스트케이스에는 한가지 중요한점이 빠져있다.

실패상황에 대한 테스트가 부족하다

테스트코드 작성에 있어서 성공하는 케이스 만큼이나, 실패하는 테스트케이스 작성이 중요하다고 한다.

존재하지 않는 Id로 조회시 예외상황에 대한 테스트 코드를 함께 작성해 보려고 한다.

CourseService

/**
 * Id로 조회해서 없으면 EntityNotFoundException
 * 있으면 조회된 엔티티를 이용해서 Dto객체를 생성 후 반환
 */
public CourseResponse findById(Long id) {
    return CourseResponse.from(courseRepository.findById(id).orElseThrow(()-> new EntityNotFoundException("강의를 찾을 수 없습니다!")));
}

위 코드를 검증하는 테스트코드를 작성해본다.

* 필자주
실제 SpringBootApplication을 띄우는 통합 테스트가 아닌
가벼운 단위테스트를 위해 테스트로 Mockito를 사용했다.

CourseServiceTest

@Test
@DisplayName("없는 id로 조회하면, 예외를 던진다.")
void findByNotExistId() {
    //given
    Long courseId = 0L;
    given(courseRepository.findById(courseId)).willReturn(Optional.empty());
    
    //when
    Throwable t = catchThrowable(() -> sut.findById(courseId));
    
    //then
    assertThat(t)
            .isInstanceOf(EntityNotFoundException.class)
            .hasMessage("강의를 찾을 수 없습니다!");
    then(courseRepository).should().findById(courseId);
}

위 테스트 코드는 나쁘지 않은것 같으나 한가지 문제가 존재한다.

만약 메세지가 "강의를 찾을 수 없습니다!" 에서 "강의가 없습니다!" 등으로 변경하고 싶은 경우
테스트 코드를 포함하여 관련된 모든 코드의 메세지를 변경해야한다.

또한 EntityNotFoundException은 나름 직관적인 예외작명이지만,
기본적으로 hibernate에서 제공해주는 예외이기 때문에, 후처리를 비롯해서 우리 Business에 최적화된 예외를 표현할 수는 없다.

특히나 나는 예외 발생시 어떤 예외가 발생했는지, json형태로 response의 body에 담아서 돌려주고 싶다.

* 주의
Custom예외를 만들때는 tradeOff를 충분히 고려하여야 한다.
이펙티브자바[72번 아이템] 에서는 자바의 표준예외를 사용하기를 권장한다.

가장 큰 이유는 많은 프로그래머에게 이미 익숙한 예외이기 때문에 다른사람이 익히고 사용하기 쉽기 때문이라고 설명한다.


커스텀 예외 만들기

CourseNotFound

public class CourseNotFound extends RuntimeException {
    private static final String MESSAGE = "강의를 찾을 수 없습니다!";
    
    public CourseNotFound() {
        super(MESSAGE);
    }
}

예외 메세지를 여기서 선언해 주었고, 기본생성자로 예외를 처리할 경우,
위 클래스에서 생성된 메세지를 사용하게 하였다.

CourseService

/**
 * Id로 조회해서 없으면 EntityNotFoundException
 * 있으면 조회된 엔티티를 이용해서 Dto객체를 생성 후 반환
 */
public CourseResponse findById(Long id) {
    return CourseResponse.from(courseRepository.findById(id).orElseThrow(CourseNotFound::new));
}

EntityNotFoundException 대신 CourseNotFound로 변경
더이상 별도로 메세지를 전달해줄 필요가 없어져서, 람다표현식을 사용할 수 있게 변경하였다.

마지막으로 이전에 만들었던 @ExceptionHandler에 CourseNotFound 예외처리를 추가한다.

ExceptionController

@ResponseBody
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(CourseNotFound.class)
public ErrorResponse courseNotFoundExceptionHandler(CourseNotFound e) {
    return ErrorResponse.of("404",e.getMessage(), new HashMap<>());
}

* Map대신 ErrorResponse라는 객체를 생성, 정적팩토리 메소드로 구성하였다.

CourseServiceTest

@Test
@DisplayName("없는 id로 조회하면, 예외를 던진다.")
void findByNotExistId() {
    //given
    Long courseId = 0L;
    given(courseRepository.findById(courseId)).willReturn(Optional.empty());
    //when
    Throwable t = catchThrowable(() -> sut.findById(courseId));
    //then
    assertThat(t)
            .isInstanceOf(CourseNotFound.class)
            .hasMessage("강의를 찾을 수 없습니다!");
    then(courseRepository).should().findById(courseId);
}

성공적으로 테스트가 진행된다.

그러나, 이렇게 하나하나 모든 예외를 하나하나씩 다 추가하게 된다면
ExceptionController가 너무 길어지게 된다.


커스텀 예외를 처리하는 최상위 클래스를 만들기.

BusinessException

/**
 * 비지니스 로직예외 최상위 클래스.
 * 공통적인 커스텀예외는 BusinessException를 상속받아
 * 공통적으로 처리가 가능
 */
public abstract class BusinessException extends RuntimeException {
    public BusinessException(String message) {
        super(message);
    }
    public abstract String getStatusCode();
}

CourseNotFound

public class CourseNotFound extends BusinessException {

    private static final String MESSAGE = "강의를 찾을 수 없습니다!";

    public CourseNotFound() {
        super(MESSAGE);
    }

    @Override
    public String getStatusCode() {
        return "404";
    }
}

기존의 RuntimeException을 상속받는 BusinessException 클래스를 abstact으로 생성한 뒤,
CourseNotFoundException은 BusinessException을 상속받는다.

ExceptionController

/**
 * 공통적으로 Custom한 Exception은 BusinessException을 상속 받아 여기서 공통적으로 처리
 */
@ResponseBody
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ErrorResponse> courseNotFoundExceptionHandler(BusinessException e) {

    ErrorResponse errorResponse = ErrorResponse.of(e.getStatusCode(), e.getMessage(), new HashMap<>());
    // 응답값의 HttpStatus에는 발생한 에러코드를,
    // 응답값의 body에 ErrorResponse를 담고 return
    return ResponseEntity.status(Integer.parseInt(e.getStatusCode()))
            .body(errorResponse);
}

BusinessException을 처리하는 @ExceptionHandler를 만들었다.
Spring에서 제공하는 Response 객체를 이용하여,

body에는 생성한 ErrorResponse 객체를,
status에는 각각의 상황에 맞는 예외코드를 담아주고 return 해준다.

의도한대로 예외메세지를 body에 Json 형태로 담기는 모습을 홗인 할 수 있다.

profile
성숙해지려고 노력하지 않으면 성숙하기까지 매우 많은 시간이 걸린다.

0개의 댓글