전체적인 구조를 잡기 위해서 간단한 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가지를 확인했다.
'토이프로젝트 > 리뷰어(영화 리뷰 사이트)' 카테고리의 다른 글
16일차 - JPA N+1 문제 (0) | 2022.11.07 |
---|---|
14일차 - 영화 상세정보 조회 api 구현 (0) | 2022.11.04 |
13일차 - 테스트 DB 구성 및 domain단 구현 (0) | 2022.10.27 |
12일차 - api 명세 및 도메인 구현 (0) | 2022.10.27 |
5~12일차 - 프론트엔드 페이지 구현 (0) | 2022.10.26 |