본문 바로가기
PJT

GPT랑 앱 만들기 - 4 (Youtube Data API v3과 suspend와 코루틴)

by 정고정 2025. 4. 20.
반응형

 

youtube api를 끌어 쓰는 건 사실 별 게 없다. 

 

https://console.cloud.google.com/apis/dashboard?project=tagypie&supportedpurview=project

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

구글 클라우드 콘솔에 들어가서 

 

 

프로젝트를 쌔 걸로 만들어 준다.

 

]

저러면 생성되는 API KEY를 복사해서 잘 둔다. 

사실 어디 secret manager 같은 곳에 올려놓는 게 속 편한데 벌써부터 배포 인프라 노가다 생각하고 싶지 않다. 

어디 잘 메모만 해 뒀다. 

 

 

API key를 만들었으면 무슨 api를 쓸 건지 구글에서 사용 권한을 열어둬야 한다. 

다행히 묶음으로 한 방에 열면 된다. 

 

원래는 관리가 아니라 사용이라고 뜬다. 사용 버튼만 눌러놓으면 아까 발급 받은 api key로 유튜브 정보를 마구 불러올 수 있다. 

이제 유튜브에서 뭘 갖다 쓸 수 있는지 좀 봐야 할 것 같다. 

예전 노래방에서 뭐 썼는지 하나도 기억이 안 나고, 코드를 까보자니 공식 docs 읽는 게 더 편할 것 같았다. 

 

 

https://developers.google.com/youtube/v3?hl=ko

 

YouTube Data API  |  Google for Developers

동영상 업로드, 재생목록 만들기 및 관리 등의 YouTube 기능을 애플리케이션에 추가합니다.

developers.google.com

 

그런데 내가 원하는 api가 어디 카테고리에 있는지 모르겠는 거임 

 

 

 

해 줘

 

공식 docs에서 video 카테고리 들어가 보니까 잘 맞는다. 

https://developers.google.com/youtube/v3/docs/videos?hl=ko#resource

 

 

 

요청 쏴 보니까 잘 받긴 하는데 문제가 하나 있다. 

 

일부러 메이저하지는 않은 일본 노래를 유튜브 뮤직 기준으로 따서 넣어봤다. 

빠른 템포 일본 노래 중에 베이스 잘 들리면서 슬랩보다 라인으로 승부하는 노래가 얼마 없어서 좋아한다. 

어쨌든 유튜브 뮤직/pc 유튜브/모바일 유튜브 url 형식이 달라서 id 파싱하는 방식이 조금씩 다르긴 한데 이건 나중에 생각할 문제 같고 

일단 문제는 이거다.

 

저 노래를 유튜브 뮤직 창에서 보면 이렇게 보인다. 

 

씹덕 같아서 한숨이 나. 하지만 여기서 이해가 안 가는 게, 내가 youtube data api v3를 통해 받은 응답을 아무리 살펴 봐도 제목에 있는 일본어가 없다.

유튜브 뮤직 ui에서 노출되는 타이틀과 가수는 내가 api를 통해 불러 온 정보와 다르다는 거다. 

 

https://www.music.youtube.com/watch?v=Zmnc6O-tm3Y

 

유튜브 뮤직 url에서 music을 떼면 해당 음악의 영상이 youtube로 열리는데 

 

내가 받아 온 데이터와 동일하다. 

그러니까 유튜브 뮤직이 ui에서 무슨 후가공을 하거나 추가 메타를 받아오는 거다. 

 

그럼 뭐... 위에서 내가 받아오는 데이터를 보면 description 세 번째 줄에 제목이랑 가수가 표시되긴 한다. 썸네일도 한 번에 온다. 

채널 정보는 Id만 넘어와서 재호출해야 하나 했는데 description 파싱해서 제목 가수 얻으면 될 것 같기도 하다. 

그런데 이건 음원 영상에만 먹히지 일반 영상에는 잘 안 먹히는 방법이다. 

 

 

일단 유튜브에서 틀었을 때는 내가 받아 온 데이터랑 일치를 하는데 유튜브 뮤직에서 틀면 조금 달라진다.

 

 

대체 가수명 유다빈밴드 어디서 가져왔냐

채널명 space로 파싱한 건가 싶어서 아예 고유명사부터 space가 들어간 채널을 찾아봤다. 

 

 

 

그치 이건 당연히 채널명이 다 나와야지.

또 지피티한테 물어봤다. 

 

 

솔직히 유튜브 뮤직이 무슨 배급을 하고, 20년 전 영상들에 대해 무슨 추가 데이터를 덧붙여서 유튜브 뮤직 서비스를 오픈했는지 나는 모른다. 

일정 이상 알고 싶지도 않다. 

일단 내가 알게 된 건, 안정적인 방식으로 유튜브 뮤직에서 보여주는 메타 데이터를 끌어 올 수는 없다는 사실뿐이다. 

그럼 타협을 조금 해야 한다. 

 

description에 "Provided to YouTube by"가 포함되어 있으면 공식 음원으로 간주 

* 공식 음원의 경우: title을 사용하고, 가수 이름은 description 세 번째 줄을 파싱하거나 channelTitle 사용 

* 영상의 경우: title을 사용하고, 가수 이름은 channelTitle 사용 

 

근데 적어놓고 보니까 그냥 title이랑 channelTitle을 쓰는 게 낫겠다. 

예쁘게 하는 데에 집착할 필요가 없는 것 같다. 

 

그래서 지피티의 도움을 받아 Youtube api 응답값을 받을 data class도 만들고

 

data class YouTubeVideosResponse(
    val kind: String,
    val etag: String,
    val items: List<YouTubeVideoItem>,
    val pageInfo: PageInfo
)

data class YouTubeVideoItem(
    val kind: String,
    val etag: String,
    val id: String,
    val snippet: Snippet
)

data class Snippet(
    val publishedAt: String,
    val channelId: String,
    val title: String,
    val description: String,
    val thumbnails: Thumbnails,
    val channelTitle: String,
    val tags: List<String>?,
    val categoryId: String,
    val liveBroadcastContent: String,
    val localized: Localized
)

data class Localized(
    val title: String,
    val description: String
)

data class Thumbnails(
    val default: ThumbnailInfo?,
    val medium: ThumbnailInfo?,
    val high: ThumbnailInfo?,
    val standard: ThumbnailInfo?,
    val maxres: ThumbnailInfo?
)

data class ThumbnailInfo(
    val url: String,
    val width: Int?,
    val height: Int?
)

data class PageInfo(
    val totalResults: Int,
    val resultsPerPage: Int
)

data class SimpleYouTubeVideoDto(
    val videoId: String,
    val title: String,
    val thumbnailUrl: String,
    val tags: List<String>?
)

/**
 * simpleDto converter
 */
fun YouTubeVideoItem.toSimpleDto(): SimpleYouTubeVideoDto {
    val thumbnailUrl = listOf(
        snippet.thumbnails.default?.url,
        snippet.thumbnails.medium?.url,
        snippet.thumbnails.high?.url,
        snippet.thumbnails.standard?.url,
        snippet.thumbnails.maxres?.url
    ).firstOrNull { !it.isNullOrBlank() } ?: ""

    val tags = snippet.tags?.takeIf { it.isNotEmpty() } ?: emptyList()

    return SimpleYouTubeVideoDto(
        videoId = id,
        title = snippet.title,
        thumbnailUrl = thumbnailUrl,
        tags = tags
    )
}

fun List<YouTubeVideoItem>?.toSimpleDtoList(): List<SimpleYouTubeVideoDto> {
    return this?.map { it.toSimpleDto() } ?: emptyList()
}

 

익숙한 webClient도 써 줬는데 지피티가 갑자기 suspend를 쓰랬다. 

나는 코틀린을 모른다. 

 

suspend fun getVideoById(videoId: String): SimpleYouTubeVideoDto? {
    return webClient.get()
        .uri { uriBuilder ->
            uriBuilder
                .path("/videos")
                .queryParam("part", partSnippet)
                .queryParam("id", videoId)
                .queryParam("key", apiKey)
                .build()
        }
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono<YouTubeVideosResponse>()
        .awaitSingle()
        .items
        .firstOrNull()?.toSimpleDto()
}

 

그냥 이 김에 webclient 비동기부터 싹 물어보기로 했다. 

 

 

✅ 비동기와 동기의 차이점

비동기동기의 핵심적인 차이는 실행 흐름대기 방식에 있어:

  • 동기(Synchronous):
    • 한 작업이 끝날 때까지 기다리고, 그 작업이 끝나야 다음 작업이 시작돼.
    • 즉, 결과를 기다리기 때문에 "차례차례 실행"되는 구조.
  • 비동기(Asynchronous):
    • 작업이 시작되면, 그 작업의 완료를 기다리지 않고 다음 작업을 바로 시작할 수 있어.
    • 작업이 끝났을 때만 결과를 받아오는 구조. 즉, "결과를 기다리는 동안 다른 일을 할 수 있음."

🌐 WebClient와 suspend 함수

1. WebClient와 awaitSingle()의 비동기 처리

val response = webClient.get() 
	.uri("https://some-api.com") 
    .retrieve() .bodyToMono<String>() 
    .awaitSingle() // 이 부분에서 비동기적으로 응답을 기다림

 

awaitSingle()을 호출하면 비동기적으로 응답을 받는다고 했지만, 사실 정확히 말하면:

  • awaitSingle()을 호출하기 전까지는 비동기적인 상태로 흐르고,
  • awaitSingle()에서 응답을 기다리게 되므로, 비동기 처리가 끝날 때까지 해당 suspend 함수는 대기하게 돼.

이렇게 보이지만 진짜 비동기 처리awaitSingle() 호출 전에 시작되는 WebClient의 네트워크 요청에서 이루어지는 거야. 네트워크 요청 자체가 비동기적으로 처리되므로, 응답을 기다리는 동안 다른 작업들이 실행될 수 있어.

2. suspend 함수로 만드는 이유는 무엇일까?

webClient.get()을 사용한 후 그 응답을 받기 위해 awaitSingle()을 호출하는 게 사실상 비동기적으로 네트워크 요청을 보내고 결과를 기다리는 작업이야.

하지만 이걸 suspend로 만들면,
코루틴의 특성상 이 함수가 호출된 이후 다른 코드들이 실행될 수 있게 되며, awaitSingle()이 끝날 때까지 기다리지 않고도 비동기적으로 백그라운드에서 다른 일을 할 수 있게 된다.

🧩 결론

  • WebClient는 비동기적으로 동작하기 때문에, awaitSingle()이 호출되기 전까지 다른 코드들이 계속 실행될 수 있지만,
  • 최종적으로 결과를 받아오기 위해 기다려야 하므로, 그 기다리는 과정은 동기적으로 처리되는 게 맞아.
  • 따라서, 비동기 처리가 필요한 코드라도 suspend 함수로 만들어주면, awaitSingle()에서 기다리는 동안 다른 코루틴들이 실행될 수 있는 구조로 만들어주는 거야.

🤔 그럼 비동기 처리가 진짜 비동기적이지 않냐는 의문에 대한 답

  • webClient.get()은 비동기적으로 요청을 보내고, 이 요청은 백그라운드에서 처리돼.
  • 결과를 기다리는 동안 다른 일들을 할 수 있게 되며, 결과를 받았을 때만 결과를 처리하고 종료되는 형태.

**비동기적 처리의 핵심은 "결과를 기다리는 동안 다른 일을 할 수 있다"**는 거야.
그래서 suspend 함수는 비동기적으로 다른 작업들을 실행할 수 있는 환경을 만들어준다고 볼 수 있어.


🚀 추가 설명: WebClient와 suspend 사용 예시

suspend fun getVideoById(videoId: String): SimpleYouTubeVideoDto? {
    return webClient.get()
        .uri { uriBuilder ->
            uriBuilder
                .path("/videos")
                .queryParam("part", "snippet")
                .queryParam("id", videoId)
                .queryParam("key", "YOUR_API_KEY")
                .build()
        }
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono<YouTubeVideosResponse>()
        .awaitSingle() // 비동기적으로 기다림, 결과를 받으면 진행
        .items
        .firstOrNull()
        ?.toSimpleDto() // 결과가 있으면 DTO로 변환
}

이 예시에서, 네트워크 요청은 비동기적으로 백그라운드에서 처리되고, awaitSingle()에서 결과를 받을 때까지 대기하되, 그 과정에서 다른 코루틴들이 실행될 수 있어.

따라서 전체적으로 비동기적인 흐름을 만들 수 있고, 서버의 성능도 효율적으로 높일 수 있게 된다는 점이 중요해!

 

 

 

 

 

맞아요! 다른 코루틴들이 실행된다는 말은, 실제로 쓰레드를 점유하지 않기 때문에 여러 작업을 동시에 실행할 수 있다는 뜻입니다. 좀 더 구체적으로 설명해볼게요.

🧵 쓰레드와 코루틴의 차이

1. 쓰레드(Thread)

쓰레드는 운영 체제에서 프로세스를 실행하는 기본적인 단위입니다. 각 쓰레드는 CPU에서 실행되는 독립적인 작업 흐름을 제공합니다.

  • 쓰레드를 여러 개 만들면, 각 쓰레드는 각각의 독립적인 작업을 병렬로 실행할 수 있습니다.
  • 하지만 쓰레드는 리소스를 많이 소모합니다. 너무 많은 쓰레드를 만들면 시스템 성능에 부담을 줄 수 있어요.
  • 쓰레드의 생성과 전환(Context Switch)은 비용이 드는 작업입니다. 예를 들어, 많은 쓰레드가 동시에 실행되면, CPU가 각 쓰레드의 상태를 계속 전환해야 하기 때문에 성능 저하가 발생할 수 있습니다.

2. 코루틴(Coroutine)

코루틴경량 스레드처럼 작동하는 비동기 프로그래밍 기법입니다. 코루틴은 전통적인 쓰레드와 비교했을 때 몇 가지 주요한 장점이 있어요.

  • 경량성: 코루틴은 실제로 쓰레드를 사용하지 않으며, CPU 자원을 점유하지 않효율적으로 작업을 처리합니다. 여러 개의 코루틴을 한 번에 실행할 수 있지만, 이를 위해 별도의 쓰레드를 소모하지 않죠.
  • Suspension: 코루틴은 작업을 "중단(suspend)"하고 나중에 다시 "재개(resume)"할 수 있습니다. 예를 들어, WebClient로 네트워크 요청을 보내는 코드는 대기 중일 때, 다른 코루틴이 CPU를 사용할 수 있게 하여 자원을 효율적으로 활용합니다.

🚀 비동기와 코루틴의 동작 방식

  • WebClient와 같은 비동기 작업을 수행할 때, 해당 요청은 비동기적으로 백그라운드에서 처리되며, suspend 함수가 호출되면 **코루틴은 그 지점에서 중단(suspend)**됩니다.
  • 중단된 코루틴은 쓰레드를 점유하지 않고, 다른 코루틴이 실행되도록 합니다. 즉, 대기 중인 코루틴은 아무런 리소스를 차지하지 않다른 작업들이 수행될 수 있도록 합니다.

🔄 코루틴의 Context Switching

  • 전통적인 쓰레드 전환(Context Switching)은 운영 체제에서 CPU 시간을 할당해주는 과정에서 발생합니다. 이때 각 쓰레드는 독립적인 작업을 하며, 전환 과정에서 비용이 발생합니다.
  • 하지만 코루틴은 그런 전환 없이 **자체적으로 중단(suspend)하고 재개(resume)**되므로, 비용이 거의 없고, 여러 코루틴을 동시에 효율적으로 실행할 수 있습니다.

💡 예시로 설명하기

suspend fun getVideoById(videoId: String): SimpleYouTubeVideoDto? {
    return webClient.get()
        .uri { uriBuilder ->
            uriBuilder
                .path("/videos")
                .queryParam("part", "snippet")
                .queryParam("id", videoId)
                .queryParam("key", "YOUR_API_KEY")
                .build()
        }
        .accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono<YouTubeVideosResponse>()
        .awaitSingle() // 비동기적으로 기다림, 결과를 받으면 진행
        .items
        .firstOrNull()
        ?.toSimpleDto() // 결과가 있으면 DTO로 변환
}

위 코드에서 awaitSingle()이 WebClient의 요청을 기다리는 동안 해당 코루틴은 중단(suspend) 상태가 되고, 이 동안 다른 코루틴들이 실행될 수 있습니다. 이 과정에서 쓰레드는 점유되지 않으며, 다른 작업이 CPU를 활용할 수 있게 됩니다.

👌 결과적으로

  • WebClient가 비동기적으로 데이터를 요청하는 동안 해당 코루틴은 대기 중에 있고, 쓰레드를 차지하지 않으며, 다른 코루틴들이 CPU를 사용할 수 있도록 합니다.
  • CPU 자원을 효율적으로 사용할 수 있기 때문에 더 많은 작업을 병렬로 처리할 수 있게 되고, 시스템 성능도 향상됩니다.

🏆 장점

  • 효율적인 자원 활용: 코루틴은 쓰레드보다 훨씬 더 적은 자원을 소모하므로, 수천 개의 코루틴을 실행할 수 있습니다. 반면, 쓰레드는 수천 개를 동시에 실행하는 데 한계가 있습니다.
  • 빠른 Context Switching: 코루틴은 전통적인 쓰레드 전환보다 훨씬 빠르게 상태를 변경하고 재개할 수 있습니다. 이는 시스템 성능에 큰 이점을 줍니다.
  • 비동기적 처리: 네트워크 요청, 파일 읽기 등과 같은 IO 작업을 비동기적으로 처리하면서 다른 작업들을 병렬로 실행할 수 있게 도와줍니다.

🚨 정리

  • 비동기 작업을 suspend로 처리하면, 작업이 끝날 때까지 다른 코루틴들이 실행될 수 있게 되어 자원을 효율적으로 사용할 수 있습니다.
  • 코루틴은 실제로는 쓰레드를 점유하지 않기 때문에, 여러 작업을 동시에 실행할 수 있고, 쓰레드 수가 많지 않아도 높은 성능을 유지할 수 있습니다.

 

 

솔직히 코루틴도 잘 모른다. 코루틴 잘 써야 한다는 말만 들었다.

그래서 쓰레드 물어봤는데 이건... 이건 좀 이해가 잘 안 돼서 더 물어봤다. 

어차피 코루틴은 쓰레드 개념이 아니다. 근데 내가 자꾸 쓰레드 개념을 끌어오려고 해서 혼동이 생겨서... 쓰레드풀 끌어와서 또 물어봤다. 

⚙️ 쓰레드풀(ThreadPool)과 코루틴

  • 쓰레드풀은 실제로 하드웨어 자원을 점유하는 실제 스레드 수를 제어하는 반면, 코루틴쓰레드를 점유하지 않고, 경량화된 작업 단위입니다.
  • 코루틴은 스레드가 아닌, 논리적인 실행 흐름에 불과하기 때문에 수천 개의 코루틴을 동시에 실행할 수 있어도, 쓰레드풀의 크기에 영향을 받지 않습니다.

그렇다고 해도, 코루틴을 실행할 때 어떤 스레드에서 실행될지는 **디스패처(Dispatcher)**가 결정합니다. 기본적으로 코루틴은 디스패처를 사용하여, 해당 코루틴을 실행할 스레드를 선택하게 되며, 이 디스패처는 한 번에 사용할 수 있는 스레드 수를 제한할 수 있습니다.

🎯 코루틴의 동시 실행 수 제어 방법

코루틴을 실행할 때 몇 개의 코루틴을 동시에 실행할지를 제어하는 방법은 여러 가지가 있습니다. 그 중에서 주로 사용하는 방법은 **디스패처(Dispatcher)**와 디스패처의 스레드 풀 크기를 제어하는 것입니다.

1. **디스패처(Dispatcher)**와 스레드풀(ThreadPool)

  • 디스패처어떤 스레드에서 코루틴을 실행할지를 결정하는 역할을 합니다. 코루틴을 실행하는 동안, 디스패처는 특정 스레드 풀을 사용하거나, 여러 코루틴을 동시에 실행할 수 있는 여러 스레드를 관리합니다.
  • **Dispatchers.IO**는 I/O 작업을 처리하는 데 최적화된 디스패처입니다. 이 디스패처는 기본적으로 **Dispatchers.Default**보다 많은 수의 스레드를 가질 수 있습니다.

 

 

쓰레드풀이랑 직접적인 상관이 없대...! 

아니 근데 내가 이해하기로 코루틴은 기본적으로 쓰레드가 아닌 건데 자꾸 경량 쓰레드라고 부르니까 헷갈리기 시작함 

그리고 코틀린 조금 재밌는 것 같다. 지피티한테 자아의탁해서 잉잉하고 끝내기에는 좀 아까워졌다.

 

나는 아날로그를 좋아하는 사람이니까 코틀린 책을 사보기로 했다. 

그런데 전자책을 사면 아날로그 인간이라고 부를 수 없는 걸까

하지만 디지털인간보다 아날로그 인간이 더 멋있으니까 나는 아날로그 하고 싶다. 

 

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=120267010&srsltid=AfmBOoo-MxNVlK9ZcCv2Wv04LK-kqUPM8k27aG9wBBkhWc_a0YPjBsER

 

Kotlin in Action : 알라딘

코틀린 기초를 소개하고, 고차함수, 객체지향, 제네릭스 등의 내용을 설명한다. 그리고 코틀린이 자바 언어를 어떻게 개선했고 기존 자바 프로젝트에서 코틀린을 함께 사용할 때 어떤 부분을 조

www.aladin.co.kr

 

전공 서적이 원래 이렇게 비싼 거였는지 까먹고 있었다. 

 

작년이었다면 회사 돈으로 샀을 텐데 올해부터는 비상경영이라 도서구매비가 안 나온다. 

에바다.

내일 저녁은 탕비실에서 때워야겠다.

반응형

댓글