[골든리포트!] 2) TMDB API 가져와서 리뷰 작성기능 넣기

2026. 2. 5. 22:16·개인 프로젝트/[2026] 골든리포트!

https://dev-dx2d2y-log.tistory.com/195

 

[골든리포트!] 1. 영영 볼 수 없는 리뷰 작성 설계하기

최근에 넷플릭스를 보다가 되게 감명 깊은 작품을 몇 개 보았다. 아주 인상깊어서 감상문을 작성해보고 싶다는 생각이 들었다. 블로그에 올려도되지만 지금 컬러잇이랑 디디디만으로도 벅차기

dev-dx2d2y-log.tistory.com

골든리포트 프로젝트는 개인의 리뷰작성기록, 리뷰반응기록 등을 토대로 무작위 영화/드라마리뷰들을 추천해주는 기능입니다.

 

※원래는 영화리뷰만 목적으로 했는데 TMDB를 사용하면서 영화/드라마로 확장, 따라서 글 내에서 표현이 영화/드라마를 영화로 지칭하는 경우가 있음


우선 리뷰 작성기능을 만들어야한다. 리뷰들이 있어야 영화리뷰들을 추천해주기 때문...

그리고 그러기 전에 영화/드라마를 가져오는 기능을 만들어야한다.

 

원래 네이버 영화 API를 사용하려했는데 지원종료이슈 때문에 TMDB로 변경

https://www.themoviedb.org/

 

The Movie Database (TMDB)

찾으시는 영화나 TV 프로그램이 없나요? 로그인 하셔서 직접 만들어주세요.

www.themoviedb.org

개인적 사용은 무료로 사용할 수 있다고한다. 어차피 공부용도라 배포할 생각은 없긴해서 우선은 무료API 사용

ㅇ

간단히 회원가입과 API키 발급을 마치면

curl --request GET \
     --url 'https://api.themoviedb.org/3/search/movie?query=###&include_adult=false&language=ko-KR&page=1' \
     --header 'Authorization: Bearer #' \
     --header 'accept: application/json'

로 링크를 보낼 수 있는데, 각 쿼리파라미터를 보면

query - 검색어. 한국어로도 검색 가능. URL로 인코딩된 값이다.

include_adult - 19세 이상 관람가의 작품을 결과로 보이려면 true로 설정하면 되는듯?

language - 출력값의 언어. 기본값은 en-US(영어), 한국어로하려면 ko-KR로 하면 된다.

 

대충 이렇게 보내면 되고..

추가로 url 엔드포인트를 영화는 movie, 드라마는 tv, 둘 다는 multi로 설정하면 된다.

 

{
    "page": 1,
    "results": [
        {
            "adult": false,
            "backdrop_path": "/5N5dSOrysuquExvn8Gpp5jMEf6u.jpg",
            "id": 87739,
            "name": "퀸스 갬빗",
            "original_name": "The Queen's Gambit",
            "overview": "1950년대 한 보육원, 체스에 천재적인 재능을 보이는 소녀. 점점 더 넓은 세계로 향하며, 체스 스타의 여정을 이어간다. 하지만 더 이기고 싶다면 중독부터 극복해야 한다.",
            "poster_path": "/vcCx1ngs1FSbMCDgl4TwJViCKXy.jpg",
            "media_type": "tv",
            "original_language": "en",
            "genre_ids": [
                18
            ],
            "popularity": 17.5984,
            "first_air_date": "2020-10-23",
            "vote_average": 8.449,
            "vote_count": 5273,
            "origin_country": [
                "US"
            ]
        },
        {
            "adult": false,
            "backdrop_path": "/4aTonMQCSEgu5PV3n3xRaNMZmG1.jpg",
            "id": 784047,
            "title": "퀸스 갬빗 비하인드 스토리",
            "original_title": "Creating The Queen's Gambit",
            "overview": "매혹적인 캐릭터와 정교한 세트, 다양한 가발. 고통과 대결하는 체스 천재의 이야기. 출연진과 제작진이 놀라운 성공을 거둔 이 시리즈의 제작에 얽힌 이야기를 들려준다.",
            "poster_path": "/gKxPyeItCrOscP8On4y5sG3WY9A.jpg",
            "media_type": "movie",
            "original_language": "en",
            "genre_ids": [
                99
            ],
            "popularity": 0.9867,
            "release_date": "2021-01-10",
            "video": false,
            "vote_average": 7.87,
            "vote_count": 135
        }
    ],
    "total_pages": 1,
    "total_results": 2
}

넷플릭스 미니시리즈 <퀸스 갬빗>을 검색어로 입력하면 이렇게 반환값이 나온다.

이 값들에서 추천 알고리즘, 사용자에게 보여줄 값, DB에 정보저장용으로 저장할 값들을 알맞게 골라야한다.ㄷㄷ

 

우선 제일 먼저 고려해야할 추천사항은 사용자활동. 사용자가 좋아요를 누른 리뷰나, 사용자가 작성한 리뷰를 가장 먼저 추천 알고리즘에 넣어야한다. 만약 가중치를 둬야한다면 거기서 추가하겠지.

 

그러면 사용자 리뷰에 저장해야할 사항부터 정해야한다.

사용자 리뷰에는 먼저 가장 기본적인 작품의 메타데이터들이 들어가고, (작품 ID, 이름, 줄거리, 감독, 미디어타입, 장르, 첫 방영일 또는 개봉일, 원산지(?), 청불여부) 이거를 작품ID를 FK삼아서 사용자 리뷰에 묶어야할 듯 하다.

이렇게 가장 기본적인 DB테이블이 완성되었다. DB설계가 은근 어렵네.. 빨리 디비공부를 좀 해야지

암튼 이렇게하면 가장 기본적인 영화 정보를 담은 테이블이 완성되었다.

 

이제 사용자 리뷰에 저장해야할 사항들이 있는데,

어떤 영화인지 영화ID를 담아야하고, 작성자, 작성일, 별점들을 남겨야한다.

 

이정도...?

우선은 추천알고리즘을 위해서 DB만 우선적으로 이렇게 짠거고, 작품 리뷰 작성기능부터 짜보도록하자.


영화 검색하기

리뷰를 작성하기 전에 영화부터 검색해야한다. 영화를 검색하는 방법은 프론트에서 검색어를 백단으로 보내야한다. 검색어를 검색하면 작품명, 장르, 첫방영일 또는 개봉일, 청불여부를 담아서 내보내도록한다.

 

API명세서


요청 보내기

package com.example.GoldenReport.Config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Value("${tmdb.api.key}")
    private String apiKey;

    @Bean
    protected RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.getInterceptors().add(((request, body, execution) -> {
            request.getHeaders().add("Authorization", "Bearer " + apiKey);
            return execution.execute(request, body);
        }));
        
        return restTemplate;
    }
}

RestTemplate Config 설정하고

 

package com.example.GoldenReport.DTO.MovieSerchResultDTO;

import lombok.Data;

@Data
public class MovieSearchRequestDTO {
    private String query;
}

검색어를 받을 DTO

 

요청을 보내면 위와 같은 형태로 떨어지는 응답을 받아야하기 때문에 응답을 받을 DTO를 만들어야한다.

 

package com.example.GoldenReport.DTO.MovieInfo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TMDBGetMoviewInfo {
    int page;
    TMDBMovieInfo[] results;

    int total_pages;
    int total_results;
}

응답을 받을 껍데기 부분

 

package com.example.GoldenReport.DTO.MovieInfo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TMDBMovieInfo {
    String name;
    String overview;
    int[] genre_ids;
    String first_air_date;
    boolean adult;
}

실제로 응답을 받을 부분.

응답으로 떨어지는 영화의 메타데이터가 정말 많은데 그 중에서 필요한 정보 (이름, 줄거리, 장르, 첫방영일, 청불여부)만 뽑아쓰면 된다.

 

package com.example.GoldenReport.Service;

import com.example.GoldenReport.DTO.MovieInfo.TMDBGetMoviewInfo;
import com.example.GoldenReport.DTO.MovieInfo.TMDBMovieInfo;
import com.example.GoldenReport.DTO.MovieSerchResultDTO.MovieSearchRequestDTO;
import com.example.GoldenReport.DTO.MovieSerchResultDTO.MovieSearchResultDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

@Service
public class MovieSearchService {
    private final RestTemplate restTemplate;

    @Autowired
    public MovieSearchService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Value("${tmdb.url}")
    private String tmdbUrl;

    public ResponseEntity<MovieSearchResultDTO> SearchMovie(MovieSearchRequestDTO movieSearchRequestDTO) {
        try {
            String query = movieSearchRequestDTO.getQuery();
            String queryToURL = URLEncoder.encode(query, "UTF-8");

            TMDBGetMoviewInfo getMovieFromTMDB = restTemplate.getForObject(tmdbUrl + query,
                    TMDBGetMoviewInfo.class);

            TMDBMovieInfo[] result = getMovieFromTMDB.getResults();
            System.out.println(result[0]);

            return null;

        } catch (UnsupportedEncodingException e){
            e.printStackTrace();
            return null;
        }
    }
}

서비스 부분.

우선은 요청을 보내고 DTO에 담기만하면 되므로 디버깅용 출력문과 return null로만 처리해놨다.

 

출력문 양호


아무튼 이정도로하면 되고, 실제로 개발을 해보면..

 

리뷰작성을 위한 영화 검색 → 백엔드에서는 영화 메타데이터를 프론트엔드로 응답 → 영화를 고르고, 리뷰를 작성해서 프론트엔드는 백엔드로 리뷰정보를 넘김 → 백엔드는 디비에 저장

이정도 플로우로 진행될 수 있겠다.

 

package com.example.GoldenReport.DTO.MovieInfo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class TMDBMovieInfo {
    int id;
    String media_type;

    String name;            //for tv
    String title;           //for movie

    String overview;
    int[] genre_ids;

    String first_air_date;  //for tv
    String release_date;    //for movie

    boolean adult;
}

영화 정보를 담는 DTO를 좀 수정했다.

우선은 드라마(tv)와 영화에 따라서 이후 검색 URL이 달라지기도하고, DTO 내부에서도 속성값이 좀 달라지기 때문에 미디어타입을 담았고, 미디어타입과 함께 정보를 구분할 ID도 담았다.

 

만약에 영화라면 name과 first_air_date 부분이 null로 반환되고, 영화가 아닌 경우 title과 release_date 부분이 null로 반환된다. NPE의 우려가 있지만.. 우선은 TMDB에서 영화정보를 받는 DTO와 응답에 반환되는 DTO를 재사용하기 위해서 그대로 두기로

 

 

또한 TMDB 요청 URL에 page 속성이 있는데, 이걸 사용하면 다른 페이지의 값도 가져올 수 있다.

{
    "query": "검색어",
    "page": 1
}

따라서 파라미터 page에 사용할 속성 page도 프론트에서 받는다.

 

만약에 "퀸스 갬빗" 드라마를 검색하면 localhost:8080/movie로 요청을 보내고, Body로 query가 "퀸스 갬빗", page가 1인 요청을 보내면 되고,

"어벤져스" 영화를 검색하면 localhost:8080/movie로 요청을 보내고, Body로 query가 "어벤져스", page가 1인 요청을 보내면 된다. 만약 1페이지에 원하는 값이 없다면 page가 2가 되도록 요청을 보내면 된다. 이 부분은 프론트에서 담당할 듯?

 

백엔드 개발을 하다보면 백엔드개발이 아니라 프론트와 누가누가 기능 많이 담당하나가 되는 것 같은데...?

 

암튼 대충 이렇게 요청을 반환하면 된다. 밑부분이 잘렸는데 adult 속성만 있다.

예전에는 DTO를 왜 사용하는지 몰랐는데 지금은 DTO없으면 개발을 못함ㄷㄷ

 

트루먼 쇼 검색결과.

프론트나 백이나 저렇게 내려간 정보를 사용하려면 먼저 media_type에 있는 값을 보고 어느 속성을 사용할지 잘 판단해야한다.


리뷰 작성하기

DB는 우선은 스프링 JPA와 H2DB를 사용하고, 나중에 스프링 JDBC와 MySQL 또는 postgre로 바꿀 생각이다.

 

ERD에서 보면 알 수 있듯이 리뷰테이블의 컬럼에는 PK로 id, 리뷰내용, 작성일, 작품ID, 작성자 ID를 집어넣어야하고, 추가로 작품검색과정에서 media_type이라는 속성을 사용했으므로 테이블에 이를 반영해야한다.

ERD에 start 컬럼이 있는데 왜 있는지는 모름. 아마 잘못 넣은듯싶다.

 

응답예시와 반환예시

 

package com.example.GoldenReport.Service;

import com.example.GoldenReport.DTO.HTTPResponseDTO.HTTPResponseDTO;
import com.example.GoldenReport.DTO.ReviewDTO.ReviewRequestDTO;
import com.example.GoldenReport.Domain.Review;
import com.example.GoldenReport.Repository.ReviewRepository;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

@Service
public class WriteReviewService {
    ReviewRepository reviewRepository;

    public WriteReviewService(ReviewRepository reviewRepository) {
        this.reviewRepository = reviewRepository;
    }

    public ResponseEntity<HTTPResponseDTO> saveReview(ReviewRequestDTO reviewRequestDTO) {
        try {
            Review review = Review.builder()
                    .content(reviewRequestDTO.getContent())
                    .media_type(reviewRequestDTO.getMedia_type())
                    .movieId(reviewRequestDTO.getVideo_id())
                    .memberId(1010101010)       //로그인 기능 작성 후 추가
                    .build();
            reviewRepository.save(review);

            HTTPResponseDTO response = HTTPResponseDTO.builder()
                    .status(200)
                    .message("성공!!")
                    .build();
            return ResponseEntity.ok(response);

        } catch (Exception e) {

            HTTPResponseDTO response = HTTPResponseDTO.builder()
                    .status(500)
                    .message("Internal Server Error")
                    .build();
            return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body(response);
        }
    }
}

서비스 코드는 요청예시를 받아서 이를 Review 클래스 속성으로 이동시킨 후 이를 저장하는 간단한 방식이다.

 

package com.example.GoldenReport.Domain;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.util.Date;

@Entity
@EntityListeners(AuditingEntityListener.class)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Review {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    long id;

    private String content;

    @CreatedDate
    private Date writeDay;

    //ManyToOne 어노테이션 사용해보고 성능검사
    private long movieId;
    private String media_type;

    private long memberId;
}

Review 도메인

작성일은 DB에 값이 저장된 일자를 그대로 반영해야하므로 @CreatedDate 어노테이션을 사용했다. 그리고 이 어노테이션을 사용하기 위해서는 @EntityListeners(AuditingEntityListner.class)를 명시해줘야하고, 

 

package com.example.GoldenReport;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@SpringBootApplication 어노테이션 위에 @EnableJpaAudigin 어노테이션을 추가해주면 된다.

 

이렇게 저장되었다.


이후 해야할 것

영화 검색기능

- 페이지가 여러 개일 경우 여러 페이지를 돌아다니며 값을 받아오기

- 유효성 검사

- 에러반환

- 로그인 기능 후 리뷰작성기능과 연계

- 장르부분을 숫자가 아니라 문자로 표현하기

 

다른 기능

- 로그인 기능

- 메인화면에 도달한 경우 개인의 성향에 맞거나 혹은 무작위로 리뷰 추천

- 리뷰 클릭 시 리뷰 정보 응답

기타등등...

'개인 프로젝트 > [2026] 골든리포트!' 카테고리의 다른 글

[골든리포트!] 6) 필터체인에서 JWT필터 인증 처리하기  (0) 2026.02.13
[골든리포트!] 5) JWT 토큰 후속기능 처리하기  (0) 2026.02.12
[골든리포트!] 4) 스프링 세션 설정 및 JWT토큰 발급  (0) 2026.02.10
[골든리포트!] 3) Spring Security Config로 스프링 OAuth 다루기  (0) 2026.02.10
[골든리포트!] 1. 영영 볼 수 없는 리뷰 작성 설계하기  (0) 2026.01.24
'개인 프로젝트/[2026] 골든리포트!' 카테고리의 다른 글
  • [골든리포트!] 5) JWT 토큰 후속기능 처리하기
  • [골든리포트!] 4) 스프링 세션 설정 및 JWT토큰 발급
  • [골든리포트!] 3) Spring Security Config로 스프링 OAuth 다루기
  • [골든리포트!] 1. 영영 볼 수 없는 리뷰 작성 설계하기
Radiata
Radiata
개발을 합니다.
  • Radiata
    DDD
    Radiata
  • 전체
    오늘
    어제
    • 분류 전체보기 (211) N
      • 신년사 (3)
        • 2025년 (2)
        • 2026년 (1)
      • CS (59) N
        • JVM (12)
        • 백엔드 (20) N
        • 언어구현 (1)
        • 객체지향 (1)
        • 논리회로 (5)
        • 컴퓨터구조 (9)
        • 데이터베이스 (1)
        • 컴퓨터 네트워크 (10)
      • 언어공부 (64)
        • Java | Kotlin (48)
        • JavaScript | TypeScript (9)
        • C | C++ (6)
      • 개인 프로젝트 (11)
        • [2025] Happy2SendingMails (3)
        • [2026] 골든리포트! (8)
        • [2026] 순수자바로 개발하기 (0)
        • 기타 이것저것 (0)
      • 팀 프로젝트 (29)
        • [2025][GDG]홍대 맛집 아카이빙 프로젝트 (29)
      • 알고리즘 (13)
        • 백준풀이기록 (11)
      • 놀이터 (0)
      • 에러 수정일지 (2)
      • 고찰 (24)
        • CEOS 23기 회고록 (2)
  • 블로그 메뉴

    • CS
    • 언어공부
    • 개인 프로젝트
    • 팀 프로젝트
    • 알고리즘
    • 고찰
    • 신년사
    • 컬러잇 개발블로그
  • 링크

    • 컬러잇 개발블로그
  • 공지사항

  • 인기 글

  • 태그

    144
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
Radiata
[골든리포트!] 2) TMDB API 가져와서 리뷰 작성기능 넣기
상단으로

티스토리툴바