04-04 18:37
Notice
Recent Posts
관리 메뉴

독산구너

Youtube Data API 사용기 - 추상클래스 사용해서 url 생성 본문

프로젝트/[SW마에스트로] SROOM

Youtube Data API 사용기 - 추상클래스 사용해서 url 생성

독산구너 2023. 9. 11. 15:16

SROOM 프로젝트에서는 다음 화면과 같이 youtube 에 있는 영상, 재생목록을 키워드로 검색하고, 상세정보를 불러와 학습 코스에 등록하는 기능이 존재합니다.

스룸 프로젝트 키워드 검색결과

이때 정보를 불러오기 위해 Youtube Data API 를 사용하는데, 이곳에서 제공하는 api는 다음과 같습니다.

출처 : https://developers.google.com/youtube/v3/docs

 

 

[search] -> 키워드 검색결과를 가져오는 api 입니다.

GET https://www.googleapis.com/youtube/v3/search

이때, 쿼리 파라미터로 여러 값이 입력될 수 있습니다. (이외에도 여러 파라미터가 존재합니다)

- q : 검색된 키워드

- part : 필요한 정보 (snippet, id, kind ... )

- order : 검색결과 연관성 (date, rating, title ...)

- pageToken : 다음 페이지 또는 이전 페이지를 불러오기 위해 제공된 토큰

- type : 검색 결과 타입 (channel, playlist, video)

- maxResults : 검색 결과 리스트의 최대 개수

 

 

[video]  -> 영상에 대한 상세정보를 가져오는 api 입니다.

GET https://www.googleapis.com/youtube/v3/videos

- id : 영상 id

- part : 필요한 정보 (snippet, id, kind, contentDetails, statistics ... )

- field : part에서 중복된 정보를 피하기 위해 상세 정보 지정 ex) id,snippet(publishedAt,title,description,thumbnails,channelTitle,defaultAudioLanguage),contentDetails(duration,dimension)

 

 

[playlist] -> 재생목록에 대한 상세정보를 가져오는 api 입니다.

GET https://www.googleapis.com/youtube/v3/playlists

- id : 재생목록 id

- part : 필요한 정보 (snippet, id, kind, contentDetails, status ... )

- channelId : 채널 id

- field

 

 

[playlistItem] -> 재생목록 안의 영상들에 대한 정보를 리스트로 받아오는 api 입니다.

GET https://www.googleapis.com/youtube/v3/playlistItems

- id : 재생록록 id

- part

- maxResults : 리스트로 받아올 영상 최대개수. 최대 50개

- pageToken : 다음 리스트를 받아오기 위한 토큰

 

 

다른 여러 api 들이 존재하지만, 스룸 프로젝트에서는 위 4개의 api 만을 사용합니다.

자바 스프링 서버에서는 사용자의 요청에 맞게 파라미터를 조합시켜 api를 호출하고, 결과를 받아와야 합니다.

 

다음과 같은 코드로 url을 생성할 수 있습니다.

public String createSearchUrl(String keyword, int limit, String nextPageToken, String prevPageToken) {
    String url = "https://www.googleapis.com/youtube/v3/search?";
    String pageTokenOrNull = chooseTokenOrNull(nextPageToken, prevPageToken);

    String partQuery = "part=id,snippet";
    String fieldsQuery = "&fields=nextPageToken,prevPageToken,pageInfo,items(id,snippet(title,channelTitle,thumbnails,description,publishTime))";
    String maxResultsQuery = "&maxResults=".concat(String.valueOf(limit));
    String apikeyQuery = "&key=".concat(Secret.getGoogleCloudApiKey());
    String typeQuery = "&type=playlist,video";
    String qQuery = "&q=".concat(keyword);

    url = url.concat(partQuery).concat(fieldsQuery).concat(maxResultsQuery).concat(typeQuery).concat(apikeyQuery).concat(qQuery);
    if (pageTokenOrNull != null) {
        String pageTokenQuery = "&pageToken=".concat(pageTokenOrNull);
        url = url.concat(pageTokenQuery);
    }
    log.info("youtube request uri:" + url);
    return url;
}

private String createVideoDetailURl(String lectureId) {
    String url = "https://www.googleapis.com/youtube/v3/videos?";

    String partQuery = "part=snippet,contentDetails,statistics,status";
    String fieldsQuery = "&fields=pageInfo(totalResults),items(id,snippet(publishedAt,title,description,thumbnails,channelTitle,defaultAudioLanguage),contentDetails(duration,dimension),status(uploadStatus,embeddable),statistics(viewCount))";
    String lectureIdQuery = "&id=".concat(lectureId);
    String keyQuery = "&key=".concat(Secret.getGoogleCloudApiKey());

    url = url.concat(partQuery).concat(fieldsQuery).concat(lectureIdQuery).concat(keyQuery);
    return url;
}
    
private String createPlaylistDetailUrl(String lectureId) {
    String url = "https://www.googleapis.com/youtube/v3/playlists?";

    String partQuery = "part=id,snippet,status,contentDetails";
    String fieldsQuery = "&fields=pageInfo,items(id,snippet(publishedAt,title,description,thumbnails,channelTitle),status,contentDetails)";
    String lectureIdQuery = "&id=".concat(lectureId);
    String keyQuery = "&key=".concat(Secret.getGoogleCloudApiKey());

    url = url.concat(partQuery).concat(fieldsQuery).concat(lectureIdQuery).concat(keyQuery);
    return url;
}

private String createPlaylistItems(String lectureId, int limit) {
    String url = "https://www.googleapis.com/youtube/v3/playlistItems?";

    String partQuery = "part=snippet,status";
    String fieldsQuery = "&fields=pageInfo,nextPageToken,prevPageToken,items(snippet(title,position,resourceId,thumbnails),status)";
    String maxResultsQuery = "&maxResults=".concat(String.valueOf(limit));
    String playlistIdQuery = "&playlistId=".concat(lectureId);
    String keyQuery = "&key=".concat(Secret.getGoogleCloudApiKey());

    url = url.concat(partQuery).concat(fieldsQuery).concat(maxResultsQuery).concat(playlistIdQuery).concat(keyQuery);
    return url;
}

 

하지만 이 메서드에는 다음과 같은 문제점이 있습니다.

1. video, search, playlist, playlistItem에서 동일한 코드가 반복된다.

2. limit, pageToken 등 파라미터를 요청하지 않을 때도 메서드에 null 등으로 전달해야 한다.

3. 모두 같은 형태를 띄고있는 같은 api이고,  endpoint만 다를 뿐인데 서로 분리되어 있다.

 

이 문제점들을 해결하기 위해 파라미터를 전달해 요청 객체를 만들 수 있도록 추상클래스를 만들고, builder를 사용하기로 하였습니다.

또한 "https://www.googleapis.com/youtube/v3/"는 yml 파일로 빼놓고, api별 고정되어 있는 part, field 파라미터등은 상수화 하였습니다.

 

변화된 코드는 다음과 같습니다.

 

 

추상클래스 YoutubeReq에서는 api별 endPoint와 파라미터를 받아 Map 타입으로 리턴하는 함수를 정의하였습니다.

public abstract class YoutubeReq {

    protected String endPoint;

    public abstract Map<String, String> getParameters();

    public String getEndPoint() {
        return endPoint;
    }
}

YoutubeReq를 상속받는 클래스는 다음과 같습니다.

@Builder
public class SearchReq extends YoutubeReq {

    private final String keyword;
    private final int limit;
    private final String filter;
    private final String pageToken;
    private final String type;

    {
        endPoint = "/search";
    }

    @Override
    public Map<String, String> getParameters() {
        Map<String, String> params = new HashMap<>(YoutubeUtil.LECTURE_LIST_PARAMETERS);
        params.put("maxResults", String.valueOf(limit));
        if (filter.equals("all")) {
            params.put("type", "playlist,video");
        } else {
            params.put("type", filter);
        }
        params.put("q", keyword);

        if (pageToken != null) {
            params.put("pageToken", pageToken);
        }

        return params;
    }
}


@Builder
public class VideoReq extends YoutubeReq {

    private final String videoCode;

    {
        endPoint = "/videos";
    }

    @Override
    public Map<String, String> getParameters() {
        Map<String, String> params = new HashMap<>(YoutubeUtil.VIDEO_PARAMETERS);
        params.put("id", videoCode);
        return params;
    }
}


@Builder
public class PlaylistReq extends YoutubeReq {

    private final String playlistCode;

    {
        endPoint = "/playlists";
    }

    @Override
    public Map<String, String> getParameters() {
        Map<String, String> params = new HashMap<>(YoutubeUtil.PLAYLIST_PARAMETERS);
        params.put("id", playlistCode);
        return params;
    }
}


@Builder
public class PlaylistItemReq extends YoutubeReq {

    private final String playlistCode;
    private final String nextPageToken;
    private final int limit;

    {
        endPoint = "/playlistItems";
    }

    @Override
    public Map<String, String> getParameters() {
        Map<String, String> params = new HashMap<>(YoutubeUtil.PLAYLIST_ITEMS_PARAMETERS);
        params.put("playlistId", playlistCode);
        params.put("maxResults", String.valueOf(limit));
        if (nextPageToken != null) {
            params.put("pageToken", nextPageToken);
        }
        return params;
    }
}

 

이때 endPoint를 각 클래스별로 재정의하기 위해 인스턴스 초기화 블록을 사용하였습니다.

또한 어떠한 값을 Youtube 에서 가져올지 결정하는 part, field 파라미터는 YoutubeUtil 클래스에 상수로 정의하여 사용합니다.

 

이렇게 코드를 깔끔하게 바꿀 수 있었습니다.

 

다음은 리팩토링 된 메서드를 사용해 youtube data api를 요청하는 예시입니다

이때, 자바의 여러 http 요청 library 중 web client를 사용해 요청하였습니다.

httpUrlConnection, Okhttp 등을 사용할 수 있는데, 이에 대한 내용은 resultClass 사용 등 자세한 요청 로직과 함께 다음 글에서 작성하도록 하겠습니다.

public <T> Mono<T> getYoutubeVo(YoutubeReq req, Class<T> resultClass) {
    return this.webClient
            .get()
            .uri(uriBuilder -> {
                UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromHttpUrl(baseUrl)
                        .path(req.getEndPoint())
                        .queryParam("key", googleCloudApiKey);

                req.getParameters().forEach(uriComponentsBuilder::queryParam);

                return uriComponentsBuilder.build().toUri();
            })
            .retrieve()
            .bodyToMono(resultClass);
}

여기서 baseUrl은 yml 파일에 정의되어 있는 "https://www.googleapis.com/youtube/v3" 이고, api 요청을 위한 googleCloudApiKey도 secure.properties에 숨겨져 있습니다.