본문 바로가기

토이프로젝트/리뷰어(영화 리뷰 사이트)

비즈니스 로직 구현과 단위테스트

전체적인 구조를 잡기 위해서 간단한 api를 구현하면서 필요한 기능을 하나씩 추가했다.

- GolbalExceptionHandler

- dto

- 단위 테스트

 

API

api에서 밑으로 하나씩 구현했다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/")
public class MovieApi {

  private final MovieService movieService;

  @GetMapping("movies")
  public ResponseEntity<List<MovieResponse>> getMovies() {
    return ResponseEntity.ok(movieService.getMovieList());
  }

  @GetMapping("movies/{movieId}")
  public ResponseEntity<MovieResponse> getMovie(@PathVariable("movieId") Long movieId) {
    return ResponseEntity.ok(movieService.getMovieById(movieId));
  }
}

 

 

Service

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MovieService {

  private final MovieRepository movieRepository;

  public List<MovieResponse> getMovieList() {
    return movieRepository.findAll().stream().map(MovieConverter::toMovieResponse).toList();
  }

  public MovieResponse getMovieById(Long movieId) {
    return movieRepository.findMovieById(movieId).map(MovieConverter::toMovieResponse)
        .orElseThrow(() -> new NotFoundException(ErrorMessage.MOVIE_NOT_FOUNDED, movieId));
  }

}

jpa dao를 사용해서 정보를 받아온다.

이때, 단건 조회 메서드인 getMovieById()에서 id가 없을 경우 exception을 던지도록 설계했다.

그리고 MovieResponse dto를 만들어서 클라이언트가 필요한 정보만 담고있는 객체를 반환하게 했다.

 

 

GolbalExceptionHandler

getMovieById()에서 exception을 던졌다.

일반적인 경우 에러가 발생하면 프로그램이 멈추지만,

api에서의 에러는 멈추는 것이 아니라 에러 내용을 클라이언트에 전달해주어야 한다.

이때, 발생한 에러를 Advice를 통해 전달해 주도록 하는 것이 @RestControllerAdvice이다.

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

  @ExceptionHandler
  public ResponseEntity<String> handleInvalidReviewArgument(
      NotFoundException exception) {
    log.error(exception.getMessage());
    return ResponseEntity.badRequest().body(exception.getMessage());
  }
}

MovieApi에서 구현했던 것과 마찬가지로 ResponseEntity를 반환하지만,
.badRequest() 와 에러 메시지를 같이 반환한다는 차이점이 있다.

@ExceptionHandler 어노테이션을 메서드에 선언하고 특정 예외 클래스를 지정해주면
해당 예외가 발생했을 때 메서드에 정의한 로직으로 처리할 수 있다.

이렇게 예외처리 항목을 한 곳에 모아두면 관리하기 용이하다는 장점이 있다.

 

 

DTO

service에서 반환하는 타입을 보면 MovieResponse이다.

이 클래스는 response를 위해서 만든 dto로 도메인이 가지고 있는 값 중 필요한 요소만 담아 반환하도록 하는 클래스이다.

 

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieResponse {

  private Long id;

  private String title;

  private String genre;

  private String country;

  private Long runningTime;

  private String movieImage;

  private String director;

  private String summary;

}

 

또한, 서비스 단에서 도메인 -> DTO 작업을 수행하지 않도록 converter를 만들어 사용했다.

 

@Component
public class MovieConverter {

  public static MovieResponse toMovieResponse(Movie movie) {
    return MovieResponse.builder()
        .id(movie.getId())
        .title(movie.getTitle())
        .director(movie.getDirector())
        .genre(movie.getGenre())
        .country(movie.getCountry())
        .runningTime(movie.getRunningTime())
        .summary(movie.getSummary())
        .movieImage(movie.getMovieImage())
        .build();
  }
}

 

 

단위테스트

@ExtendWith(MockitoExtension.class)
class MovieServiceTest {

  @Spy
  @InjectMocks
  private MovieService movieService;

  @Mock
  private MovieRepository movieRepository;

...

  @Test
  void getMovieById() {
    //given
    given(movieRepository.findMovieById(movie1.getId())).willReturn(Optional.of(movie1));

    //when
    MovieResponse actual = movieService.getMovieById(movie1.getId());

    //then
    then(movieRepository).should().findMovieById(movie1.getId());

    assertThat(actual, samePropertyValuesAs(movieResponse1));

  }

  @Test
  void getMovieByIdNotExist() {
    //given
    given(movieRepository.findMovieById(3L))
        .willReturn(Optional.empty());

    //when
    assertThrows(NotFoundException.class, () -> movieService.getMovieById(3L));

    //then
    then(movieRepository).should().findMovieById(3L);


  }
}

 

영화 상세조회 부분을 테스트하는 코드이다.

성공하는 경우와 찾는 id가 없어 실패하는 경우 2가지를 확인했다.