본문 바로가기

Playlist/Open API

Open API : TheMovie

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