Open API 구축
패키지 구조
playlist/domain/content/api/
- config/WebClientConfig.java
- controller/TheMovieApiController.java - > 삭제됨
- mapper/TheMovieMapper.java
- response/TheMovieListResponse.java
- response/TheMovieResponse.java
- scheduler/TheMovieScheduler.java
- service/TheMovieApiService
config
WebClientConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient() {
return WebClient.builder().build();
}
}
controller : 스케줄러 적용으로 삭제됨
TheMovieApiController.java
import com.codeit.playlist.domain.content.api.service.TheMovieApiService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class TheMovieApiController {
private final TheMovieApiService theMovieApiService;
@GetMapping(value = "/movie", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<String> getMovie(@RequestParam String query) {
return theMovieApiService.getApiMovie(query);
}
@GetMapping(value = "/tv", produces = MediaType.APPLICATION_JSON_VALUE)
public Mono<String> getTv(@RequestParam String query) {
return theMovieApiService.getApiTv(query);
}
}
Mapper
TheMovieMapper.java
import com.codeit.playlist.domain.content.api.response.TheMovieResponse;
import com.codeit.playlist.domain.content.entity.Content;
import com.codeit.playlist.domain.content.entity.Type;
import org.mapstruct.Mapper;
@Mapper(componentModel = "spring")
public interface TheMovieMapper {
Content toContent(TheMovieResponse theMovieResponse, Type type);
}
Response
TheMovieListResponse.java
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record TheMovieListResponse(
@JsonProperty("results")
List<TheMovieResponse> results
) {
}
TheMovieResponse.java
import com.fasterxml.jackson.annotation.JsonProperty;
public record TheMovieResponse(
@JsonProperty("title") String title,
@JsonProperty("overview") String description,
@JsonProperty("poster_path") String thumbnailUrl,
@JsonProperty("vote_average") Double averageRating
) {
}
Scheduler
TheMovieScheduler.java
@Slf4j
@Component
@EnableScheduling
@RequiredArgsConstructor
public class TheMovieScheduler {
private final TheMovieApiService theMovieApiService;
private final ContentRepository contentRepository;
private final TheMovieMapper theMovieMapper;
// 초, 분, 시, 일, 월, 요일
// 요일 상관없이 매월 1일, 01시에 스케쥴링을 시작함
// 테스트는 1분마다 실행함
@Scheduled(cron = "*/30 * * * * *", zone = "Asia/Seoul")
public void startTheMovieScheduler() {
log.info("The Movie 스케쥴러 시작, API 데이터 수집");
String query = "Japan";
theMovieApiService.getApiMovie(query)
.map(resp -> theMovieMapper.toContent(resp, Type.MOVIE))
.doOnNext(content -> contentRepository.save(content))
.doOnError(e -> log.error(e.getMessage(), e))
.subscribe();
log.info("The Movie 스케쥴러 동작 완료, API 데이터 수집");
}
}
수정사항

수정된 Scheduler
import com.codeit.playlist.domain.content.api.mapper.TheMovieMapper;
import com.codeit.playlist.domain.content.api.service.TheMovieApiService;
import com.codeit.playlist.domain.content.entity.Type;
import com.codeit.playlist.domain.content.repository.ContentRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import reactor.core.Disposable;
@Slf4j
@Component
@EnableScheduling
@RequiredArgsConstructor
public class TheMovieScheduler {
private final TheMovieApiService theMovieApiService;
private final ContentRepository contentRepository;
private final TheMovieMapper theMovieMapper;
private Disposable movieSubscription;
// 초, 분, 시, 일, 월, 요일
// 요일 상관없이 매월 1일, 01시에 스케쥴링을 시작함
// 테스트는 1분마다 실행함
@Scheduled(cron = "*/30 * * * * *", zone = "Asia/Seoul")
public void startTheMovieScheduler() {
// 이전 구독이 있으면 정리하는 조건문
if(movieSubscription != null && !movieSubscription.isDisposed()) {
movieSubscription.dispose();
}
log.info("The Movie 스케쥴러 시작, API 데이터 수집");
String query = "Japan";
movieSubscription = theMovieApiService.getApiMovie(query)
.map(response -> theMovieMapper.toContent(response, Type.MOVIE))
.doOnNext(content -> contentRepository.save(content))
.doOnComplete(() -> log.info("The Movie 스케줄러 동작 완료, API 데이터 수집"))
.doOnError(e -> log.error("The Movie 스케줄러 에러 발생 : {}", e.getMessage(), e))
.subscribe();
}
}
Service
TheMovieApiService.java
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import java.time.Duration;
@Slf4j
@Service
@RequiredArgsConstructor
public class TheMovieApiService {
private final WebClient webClient;
@Value("${TMDB_API_KEY}")
private String apikey; // tmdb API key
public Mono<String> getApiMovie(String query) {
return searchTheMovieApi(query, "Movie", "/3/search/movie");
}
public Mono<String> getApiTv(String query) {
return searchTheMovieApi(query, "Tv", "/3/search/tv");
}
private Mono<String> searchTheMovieApi(String query, String type, String path) {
log.info("TheMovie API {} 수집 시작, query = {}", type, query); // type 매개변수, Movie or TV, debug로 수정 필요 - 확인용 info
return webClient.get()
.uri(uriBuilder -> uriBuilder
.scheme("https")
.host("api.themoviedb.org")
.path(path) // path 매개변수, /3/search/movie or /3/search/tv
.queryParam("query", query)
.queryParam("api_key", apikey)
.build())
.retrieve()
.onStatus(s -> s.value() == 429, clientResponse -> clientResponse.createException().flatMap(Mono::error))
.bodyToMono(String.class)
.retryWhen(
Retry.backoff(3, Duration.ofSeconds(2))
.filter(exception -> exception instanceof WebClientResponseException webException && webException.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS)
.transientErrors(true)
)
.doOnError(WebClientResponseException.class,
e -> log.error("TheMovie API {} 수집 오류 : status = {}, body = {}", query, e.getStatusCode(), e.getResponseBodyAsString()));
}
}
수정된 TheMovieApiService.java
import com.codeit.playlist.domain.content.api.response.TheMovieListResponse;
import com.codeit.playlist.domain.content.api.response.TheMovieResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import java.time.Duration;
@Slf4j
@Service
@RequiredArgsConstructor
public class TheMovieApiService {
private final WebClient webClient;
@Value("${TMDB_API_KEY}")
private String apikey; // tmdb API key
private Mono<TheMovieListResponse> callTheMovieApi(String query, String path) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.scheme("https")
.host("api.themoviedb.org")
.path(path)
.queryParam("query", query)
.queryParam("api_key", apikey)
.build())
.retrieve()
.onStatus(s -> s.value() == 429, clientResponse -> clientResponse.createException().flatMap(Mono::error))
.bodyToMono(TheMovieListResponse.class)
.retryWhen(
Retry.backoff(3, Duration.ofSeconds(2))
.filter(exception -> exception instanceof WebClientResponseException webException && webException.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS)
.transientErrors(true)
)
.doOnError(WebClientResponseException.class,
e -> log.error("TheMovie API {} 수집 오류 : status = {}, body = {}", query, e.getStatusCode(), e.getResponseBodyAsString()));
}
private Flux<TheMovieResponse> fluxingTheMovieApi(String query, String path) {
return callTheMovieApi(query, path)
.flatMapMany(res -> {
if(res.results() == null || res.results().isEmpty()) {
return Flux.empty();
}
return Flux.fromIterable(res.results());
});
}
@Transactional
public Flux<TheMovieResponse> getApiMovie(String query) {
return fluxingTheMovieApi(query, "/3/discover/movie");
}
}
'Playlist > Open API' 카테고리의 다른 글
| TvSeriesApiService (0) | 2025.12.17 |
|---|---|
| TheMovieApiService (0) | 2025.12.17 |
| Open API 사용하기 : The Movie (진행중) (0) | 2025.11.20 |
| Open API : The Sports (1) | 2025.11.20 |
| Open API 설정하기 (진행중) (0) | 2025.11.16 |