인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

정재익님의 프로필 이미지

작성한 질문수

2시간으로 끝내는 코루틴

선생님 강의를 듣고 크롤링에 코루틴을 적용해보고 있습니다. 그런데 코루틴이 하나만 나와서 동시처리가 안되는데 혹시 봐주실 수 있나요??

작성

·

37

·

수정됨

1

private fun scrapeBookData(browser: Browser, bookLinks: List<String>): List<BookDTO?> {

        val bestsellers = mutableListOf<BookDTO?>()

        runBlocking {
            bookLinks.mapIndexed { i, link ->
                printWithThread("${i} 시작")
                val page = browser.newPage()
                page.navigate(link)
                printWithThread("${link}에 접속 완료")

                launch {
                    delay(3000)
                    page.waitForLoadState(LoadState.DOMCONTENTLOADED)
                    val data = page.evaluate(
                        """ () => JSON.stringify({
        title: document.querySelector('.prod_title')?.innerText?.trim() || '',
        author: document.querySelector('.author')?.innerText?.trim() || '',
        isbn: document.querySelector('#scrollSpyProdInfo .product_detail_area.basic_info table tbody tr:nth-child(1) td')?.innerText?.trim() || '',
        description: document.querySelector('.intro_bottom')?.innerText?.trim() || '',
        image: document.querySelector('.portrait_img_box img')?.getAttribute('src') || ''}) """
                    ).toString()

                    val type = object : TypeToken<Map<String, String>>() {}.type
                    val json: Map<String, String> = Gson().fromJson(data, type)

                    page.close()
                    printWithThread("${link}의 데이터 파싱 완료")

                    var bestseller: BookDTO? = null

                    if (!json.values.all { it.isBlank() }) {
                        bestseller = BookDTO(
                            id = 0L,
                            title = json["title"] ?: "",
                            author = json["author"] ?: "",
                            description = json["description"] ?: "",
                            image = json["image"] ?: "",
                            isbn = json["isbn"] ?: "",
                            ranking = i + 1,
                            favoriteCount = 0
                        )
                    }
                    bestsellers.add(bestseller)
                }
            }
        }
        return bestsellers
    }
[http-nio-8080-exec-1 @coroutine#1] 0 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000215819502에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 1 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000215150862에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 2 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000215150863에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 3 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000215150882에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 4 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000215150895에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 5 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000215150892에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 6 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000000610612에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 7 시작
[http-nio-8080-exec-1 @coroutine#1] https://product.kyobobook.co.kr/detail/S000001632467에 접속 완료
[http-nio-8080-exec-1 @coroutine#1] 8 시작

DOM객체가 로드되는데까지 시간이 오래 걸려서 페이지들을 한번에 호출하고 DOM객체가 로드되는 시간을 공유하고 객체가 로드되는대로 데이터를 가져오려고 했습니다

접속하는건 메인 코루틴이 하게하고 반복문만큼 launch로 코루틴을 만들어서 각 DOM객체가 만들어지면 데이터를 파싱하려고 했으나 delay(3000)때문에 3초마다 메인코루틴이 접속하는 것 같습니다.

 

하지만 delay(3000)을 없애도 같은 결과가 나옵니다 메인 코루틴이 거의 3초느낌으로 웹페이지에 접속을 합니다 launch를 하나 더 만들어서 페이지를 호출하는것도 코루틴 처리를 하면 launch 바깥부분은 page객체를 받지못해 컴파일 오류가 뜹니다

이 상황을 어떻게 돌파해야하는지 감이 잘 안오는데 힌트 주실 수 있을까요? ㅠㅠ

답변 1

0

최태현님의 프로필 이미지
최태현
지식공유자

안녕하세요 재익님! 🙂 질문 주셔서 감사합니다.

 

결론부터 말씀드리면, 현재 blocking I/O가 single thread를 점유하고 있기 때문에 그런 것으로 보입니다.

bookLinks.mapIndexed { i, link ->
  businessLogic()
}

에서 businessLogic() 은 크게

  1. 외부 Page를 불러오는 과정

  2. 불러온 Page에 대한 데이터를 가공하는 과정

으로 나눠지는 것 같은데요!

 

이 중 1번은 blocking I/O로 코루틴 안에서 실행한다고 하더라도, 코루틴도 결국 특정한 스레드에 매핑되어 실행 (1강 내용입니다!) 되기 때문에 스레드가 하나인 경우 로딩 -> 로직 실행 -> 로딩 -> 로직 실행 처럼 순차적인 결과로 보일 수 밖에 없습니다.

 

가장 간단한 해결책은 해당 로직을 여러 스레드에 매핑될 수 있게끔 해주는건데요!

예를 들어 스레드가 5개라면, 동시에 5명이

  • 로딩 -> 로직 실행

  • 로딩 -> 로직 실행

  • 로딩 -> 로직 실행

  • 로딩 -> 로직 실행

  • 로딩 -> 로직 실행

을 하기 때문에 로딩이 blocking 하게 3초 정도 걸린다 하더라도 3초에 5번씩 businessLogic을 돌릴 수 있게 됩니다.

runBlocking {
  val jobs = bookLinks.mapIndexed { i, link ->
    async(Dispatchers.IO) {
      businessLogic()
    }
  }
  jobs.awaitAll()
}

처럼 runBlocking 만 사용해 '하나의 스레드'에 코루틴을 배정하지 않고 Dispatchers.IO 를 통해 여러개의 스레드에 코루틴을 배정하면 원하시는 결과를 얻으실 수 있을거에요! 🙂

 

또 다른 해결책으로는 blocking I/O를 non-blocking I/O로 교체하는건데, 이는 저희가 100% 제어할 수는 없고 non-blocking I/O가 지원되는 라이브러리가 있는지 확인이 필요하기에 애매할 수 있습니다.

 

학습하신 내용을 바로 적용해 보려 하시다니 대단하시네요~! 👍

답변이 도움이 되었으면 좋겠습니다. 감사합니다. 🙇

정재익님의 프로필 이미지
정재익
질문자

감사합니다! 알림이안떠서 이제봤네요 ㅠ

알고보니 제가 사용한 playwright 라는 라이브러리가 싱글스레드기반에서만 작동했던거여서 스레드를 늘려서 각각 playwright인스턴스를 넣고 Dispatchers.io로 해결했습니다! 감사합니다!!!

 

강의 복습 한번 더 해야겠네용ㅎㅎ