묻고 답해요
156만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
해결됨코틀린 코루틴 완전 정복
CoroutineDispatcher에 대한 질문
안녕하세요.CoroutineDispatcher강의를 들다보니 제가 이해한게 맞는지 궁금하여 질문드립니다. 말씀하시기론 CoroutineDispatcher가 코루틴을 스레드로 보내 실행시키는 객체라고 하셨습니다. 하지만 예제에서 singleThreadDispatcher나 multiThreadDispatcher로 선언했던 객체들이 실제로는 Dispatcher라기보단 Dispatcher에 의해 작업이 수행될 스레드인것 같더라고요. 저는 다른 스레드풀에 그저 Dispatch만 하는줄 알았는데 아닌것 같아 뭐가 맞는지 궁금하여 질문남깁니다.
-
해결됨코틀린 코루틴 완전 정복
코루틴과 JVM이 끝나는 조건에 대해 문의 드립니다.
- 학습 관련 질문을 남겨주세요. 질문을 상세히 작성하면 더 좋습니다.- 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요. 어렵게 느껴졌던 Structured Concurrency에 대해 강의를 듣고 많이 이해하고 배워 갑니다. 자바에서는 데몬이 아닌 자식 쓰레드가 끝나지 않으면 메인 쓰레드가 끝났더라도 JVM이 종료가 되지 않는 걸로 알고 있습니다. 따라서 아래 자바 코드는 th01을 join()으로 기다리지 않더라도 JVM은 끝나지 않습니다.public class scratch02 { public static void main(String[] args) throws Exception { Thread th01 = new Thread(()-> { while(true) { System.out.println("I'm working"); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); th01.start(); System.out.println("End of the main thread"); } } 그런데 코루틴의 경우 Main Job과 별도의 Root Job으로 수행되는 코루틴의 경우에 아무리 동작 중이더라도 Main Job이 끝나 버리면 JVM이 종료 되어버리죠. 자바나 코틀린이나 같은 JVM에서 동작할 텐데 이런 차이는 왜 발생하는 걸까요? 즉, 같은 질문이긴 한데 코루틴은 어떻게 Main Thread가 종료 될 때 다른 코루틴이나 쓰레드의 종료를 기다리지 않고 JVM을 종료 할수 있게 되는 것인가요?
-
해결됨코틀린 코루틴 완전 정복
Job() 생성후 complete을 불러 주어야 하는 경우가 헷깔립니다.
- 학습 관련 질문을 남겨주세요. 질문을 상세히 작성하면 더 좋습니다.- 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요.안녕하세요.Job() 생성시에 어떤 경우는 compete을 호출해 주어야 하고 어떤 경우는 아닌 것 같은데 그 상황이 헷깔려서 문의 드립니다. 강의에서는 다음과 같이 설명 합니다.launch나 async 함수를 통해 생성한 job객체는 모든 코드가 실행 되면 자동 완료 되지만 job 생성 함수를 통해 생성한 job 객체는 자동으로 실행 완료 되지 않습니다. 아래 코드에서는 launch 안에서 생성함수를 통해 생성한 job 객체는 자동 완료 되지 않는 것으로 보입니다.fun main() = runBlocking<Unit> { launch(CoroutineName("Coroutine1")) { launch(CoroutineName("Coroutine2") + Job(parent = this.coroutineContext[Job])) { delay(100L) println("[${Thread.currentThread().name}] 코루틴 실행") } } } 하지만 아래 코드에서는 똑같이 생성함수를 통해 생성된 job 객체인데도 자동 완료 되는 이유는 무엇인가요? fun main() = runBlocking<Unit> { launch(CoroutineName("Coroutine1")) { val newJob = Job() launch(CoroutineName("Coroutine2") + newJob) { delay(100L) println("[${Thread.currentThread().name}] 코루틴 실행") } } }
-
미해결2시간으로 끝내는 코루틴
코루틴과 가상 스레드의 차이가 궁금합니다.
안녕하세요. 좋은 강의 감사드립니다.코루틴에 대한 설명을 듣고 보니,자바 21에 등장한 Virtual Thread와 그 개념이 매우 비슷하다고 생각이 들었습니다.코루틴을 실행시키는 스레드는 어찌보면 Virtual Thread 의 carrier thread로 생각할 수 있고,코루틴 자체를 virtual thread 라고 생각해보면,결국 두 개념이 용어만 다를 뿐 결과적으로는 동일한 기능(?)을 수행하는 것으로 보입니다.제가 궁금한 것은 자바에서 Virtual Thread로 작성된 코드가 100퍼센트 코틀린 - 코루틴으로 변환 가능한지,그리고 그 역도 성립하는 것인지 궁금합니다.감사합니다.
-
해결됨코틀린 코루틴 완전 정복
왜 runBlocking 종료로 메인스레드가 종료될까요?
새로운 루트Job이 dispatchers.IO 스레드에 의해 실행되면, 메인스레드가 기다려야하지 않나요?dispatchers.IO 에 의해 할당되는 실행 스레드는 데몬스레드가 아닐거같은데 말이죠..
-
해결됨코틀린 코루틴 완전 정복
cancel 질문드립니다.
안녕하세요 강의 잘 듣고 있습니다.Code4-17에서 dispatchers를 사용하지 않고 코드를 그대로 실행하면 while문 코루틴이 cancel이 되지 않는데 왜 그런걸까요??메인스레드 1개의 스레드에서만 동작하여 상태를 바꿀 수 없기 떄문일까요??
-
미해결2시간으로 끝내는 코루틴
Coroutine과 ThreadLocal관련 질문
안녕하세요 선생님, 강의 정말 잘 듣고 있습니다. 한가지 강의를 들으면서 궁금한 점이 생겨 질문 드립니다.설명해주신 내용을 들었을 때, 코루틴은 특정 스레드와 종속적인 관계가 아니기 때문에 여러 스레드에 걸쳐 실행될 수 있다고 말씀해주셨습니다."코루틴1의 코드1은 스레드1에서 실행되었지만, 코루틴1의 코드2는 스레드2에서 실행되었다!"그런데 아래의 예시의 경우에는 문제가 존재할지 궁금합니다.스레드1에서 코루틴1의 코드1은 ThreadLocal을 만들어 사용하다가 중단 지점에 걸려 멈췄습니다.그 후에 코루틴1의 코드2가 실행될 때, 스레드2에서 실행되었습니다.코드2에는 코드1에서 만든 ThreadLocal을 가져다 쓰는 경우스레드1에 종속적인 ThreadLocal을 스레드2에서 가져다 쓸 경우 문제가 발생할 것 같은데.. (1)코루틴에서 ThreadLocal을 사용하는 것은 안티패턴으로 취급될까요?이보다 좀 더 근본적인 질문으로 돌아가서 (2)코루틴을 사용할 때 ThreadLocal을 사용하는 게 자주 있는 일일까요?
-
해결됨코틀린 코루틴 완전 정복
delay 함수 사용 시 스레드 양보
스레드를 양보하며 일시 중단하는 방식은 yield 함수라고 이해를 했는데, delay 함수도 스레드를 양보하며 일시 중단을 하는 것인가요?그러면 delay 와 yield 의 차이점은 "지연된 만큼 기다렸다가 재개가 되냐" vs "지연 없이 바로 재개되냐" 일까요?
-
해결됨코틀린 코루틴 완전 정복
강사님께서 번역해주신 코틀린 코루틴 공식 가이드 문서 문의
강사님께서 코틀린 코루틴 공식 가이드 문서 한국어 번역 및 배포 작업을 해주신 것으로 알고 있습니다.직접 번역하신 관련 문서를 보고 공부하고 싶은데, 어느 사이트에 있는지 링크를 공유해주실 수 있으실까요?
-
미해결2시간으로 끝내는 코루틴
선생님 강의를 듣고 크롤링에 코루틴을 적용해보고 있습니다. 그런데 코루틴이 하나만 나와서 동시처리가 안되는데 혹시 봐주실 수 있나요??
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객체를 받지못해 컴파일 오류가 뜹니다이 상황을 어떻게 돌파해야하는지 감이 잘 안오는데 힌트 주실 수 있을까요? ㅠㅠ
-
미해결2시간으로 끝내는 코루틴
7강에 대해서 궁금증이 있는데요
안녕하세요! 강의를 잘보고 있습니다.강의를 보다가 질문이 있는데요,.같은 디스패쳐를 썼을때, 자식 코루틴이 코루틴 스코프를 새로 만드는게 어떤 효과를 기대할 수 있나요? 스코프를 새로 만드는게 어떤 이점이 있는지 잘이해가 안되는 것 같습니다..또 부모코루틴안에서 새로운 코루틴 스코프를 만든 자식 코루틴과 그냥 자식 코루틴 이 2개가 있을 때 동작하는 방식에서도 차이가 있는지 궁금합니다!
-
해결됨2시간으로 끝내는 코루틴
Spring MVC에서 corountine 활용 방안
안녕하세요. 저는 오랜기간 동안 Java, Spring을 기반으로 웹 프로그래밍 해왔고 이번에 일부 프로젝트를 코틀린 + Spring을 기반으로 구현을 검토하게 되어 해당 강의를 듣게 되었습니다.사실 Java, Spring 기반이다 보니 동기방식의 프로그램에 익숙해져 있고 러닝커브나 디버깅의 어려움, DB등의 관련 라이브러리들이 아직 안정화 수준이 아니라 판단하여 WebFlux 도입을 꺼려왔고,일부 RestTemplate 호출등의 병렬 처리가 필요할 경우 CompletableFuture에 별도 ThreadExecutor를 사용해 처리하는 방법을 주로 사용해 왔습니다.이번 신규 프로젝트도 우선은 Spring 프레임워크를 사용하기 때문에 Webflux가 아닌 Spring MVC 기반의 동작이 될 예정인데요,그러다 보니 강의를 다 들었지만 coroutine을 어떻게 활용할 수 있을지 감을 잘 못잡은 상태입니다.가령 RestTemplate이나 RestClient 등의 동기식 block 기반의 통신 시 호출 부를 CoroutineScope(Dispatchers.Default).async { } 으로 감싸서 호출을 하게 되면 두개의 API가 각각 1초가 걸린다면 1.05 정도로 가능하겠지만 이런 패턴은 기존 자바에서 CompletableFuture로 충분히 가능했던더라 coroutine을 잘 활용했다고 보는게 맞는지 궁금한데요,coroutine의 장점을 활용하려면 결국 API 통신 같은 경우 가능하면 webclient나 ktorClient 등을 통해 non-block으로 변경해서 처리를 해야 하는건지 궁금하고, 코틀린으로 웹애플리케이션을 구현한다고 했을때 spring을 사용하면 webflux 도입은 약간 필수? 같은 개념으로 봐야 하는건지 궁금합니다.(강의 마지막 Continuation 예제에서 repository call을 두번 하는 부분도 non-block으로 DB처리가 가능해야 의미있는 coroutine 동작이 가능한거겠죠??) 질문을 요약하자면,Spring MVC 구조에서 block 기반의 로직 처리 시 CoroutineScope(Dispatchers.Default).async { } 으로 감싸서 호출하는 구조가 coroutine의 장점을 활용한 방식이 맞을까요?1번이 장점이 아니라면 non-blocking으로 처리 가능한 webclient나 ktorClient을 사용해야 해야 할까요?보통 Spring에서 coroutine을 활용하려면 webflux를 사용하는게 기본일지? 혹시 다른 활용 방안은 없을지?입니다.감사합니다.
-
미해결2시간으로 끝내는 코루틴
9강 코루틴 중단과 재개관련 문의 드립니다.
강의 정말 재미있게 잘 듣고 있습니다. 이제 막바지네요 :)제공해주신 샘플코드를 보면서도 코틀린이 코루틴을 어떻게 중단하는지는 이해가 잘 안되었습니다. 코드상(예제나 디컴파일된 코드)에 딱히 코루틴을 중단 시키는 부분이 보이지 않습니다. 재개의 경우 Continuation의 resumeWith 를 사용할것 같은데, 중단의 경우에는 내부적으로 CoroutineContext 가 활용되는 걸까요 ?
-
해결됨코틀린 코루틴 완전 정복
Flow와 Channel
강의에서 Flow나 Channel에 대해 추가로 다루실 예정이 있는지 궁금합니다.
-
해결됨코틀린 코루틴 완전 정복
[코틀린 코루틴의 정석 책 추첨 이벤트] 강의 구매 기간 관련 문의
안녕하세요 강사님! 책 추첨 이벤트 관련 내용을 보고 강의 구매 기간에 관련하여 여쭤보고 싶은 사항이 있어서 글을 남기게 됐습니다. 여기에 말씀해주신 기간(25.12.19 ~ 26.1.18)에 강의를 구매한 사람만 해당 이벤트에 참여할 수 있는 걸까요 ?? (기간 이전에 강의를 구매한 사람은 해당되지 않는 지 궁금합니다) 감사합니다.
-
해결됨코틀린 코루틴 완전 정복
[코루틴 테스트 심화] runTest의 스레드 관련 문의
안녕하세요 강사님!강의 복습 중에 runTest 관련 문의가 있어서 다음과 같이 질문을 남기게 됐습니다. 코루틴 테스트 심화강의의 4:07초에 시작되는 부분을 보면 runTest를 호출해 실행되는 코루틴은 메인 스레드를 사용한다라고 해주셨습니다. 제가 실제로 runTest를 이용해 코드를 실행시켜 보니 Test worker스레드를 사용하고 있다고 나왔습니다. 실행 코드 @Test fun `메인 스레드만 사용하는 runTest`() = runTest { println("[${Thread.currentThread().name}] 메인 시작") delay(100) println("[${Thread.currentThread().name}] 메인 종료") }결과[Test worker @kotlinx.coroutines.test runner#2] 메인 시작 [Test worker @kotlinx.coroutines.test runner#2] 메인 종료 실제 사용되는 스레드는 Test worker 스레드 이지만, 테스트 코드에서 실행될 때 Test worker 스레드가 메인 스레드 처럼 동작하기 때문에 메인이라고 말씀해주신 걸까요?? 감사합니다!
-
미해결2시간으로 끝내는 코루틴
suspend 함수에 관해 추가적인 질문 있습니다! (runcatching, Result)
9강을 듣고 이해한 바로는, suspend 함수는 Continuation으로 resumeWith의 Result를 통해 내부적으로 콜백을 진행한다고 이해하였습니다!그렇다면 suspend 함수를 사용할때에는 runCatching을 사용하거나, 반환값을 Result로 묶는것은 불필요한 행위라는 결론을 내렸는데, 맞는 생각인지가 궁금합니다!왜냐면 runCatching과 Result 반환값을 활용하게 된다면, 애초에 suspend로 한번 예외처리를 진행했기에 중복된 작업이라고 생각하였기 때문입니다!팀원들과 부트캠프를 통해 진행한 프로젝트를 개선하고자 하는데, suspend 함수에 runCatching을 사용 후 Result로 반환을 하였어서 불필요한 작업이라고 생각되어 개선하고자 하기에 질문드립니다!긴 글 읽어주셔서 감사합니다!
-
해결됨코틀린 코루틴 완전 정복
job과 코루틴의 관계?
- 학습 관련 질문을 남겨주세요. 질문을 상세히 작성하면 더 좋습니다.- 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요. 먼저 좋은 강의 감사합니다! 혼자서 다음과 같이 마구마구 찍어보고있었습니다.이때 저희 예상과 다르게 돌아가서 질문을 드립니다.저의 가설로는 "withContext는 코루틴을 생성하지 않고 Context만 바꾸니 코루틴을 제어하고 추적하는데 사용되는 Job은 새로 만들어지지 않을 것이다. 따라서 job1과 job5는 같을 것이다." 이었습니다.하지만 결과는 새로운 job을 만들고 job의 부모에 job1을 연결시키더군요.동일한 코루틴에서 동작하는데 왜 새로운 Job을 만드는 것인가요?fun main() = runBlocking { val job1 = coroutineContext[Job] // 1 coroutineScope { val job2 = coroutineContext[Job.Key] val job3 = launch {} val job4 = launch {} println("job2 = ${job2}") // 2 println("job2 parent = ${job2?.parent}") println("job3 parent = ${job3.parent}") println("job4 parent = ${job4.parent}") } withContext(Dispatchers.Default) { val job5 = coroutineContext[Job.Key] println("job5 = ${job5}") println("job5 parent = ${job5?.parent}") } println("job1 = ${job1}") } // // job5 = DispatchedCoroutine{Active}@4311e223 // job5 parent = BlockingCoroutine{Active}@1c2c22f3 // job1 = BlockingCoroutine{Active}@1c2c22f3coroutineScope도 주석 1, 2 부분이 같은 코루틴이라 job1이랑 job2가 동일하게 나올 거라 생각했는데 다르군요.그렇다면 현재 상황에서 "하나의 코루틴에 여러 개의 job(?)을 가진 것 아닌가? 이러면 job으로 코루틴을 제어할 수 있나?" 라는 의문이 듭니다. job과 코루틴의 관계가 헷갈립니다😱
-
해결됨코틀린 코루틴 완전 정복
코루틴의 blocking I/O작업 처리
- 학습 관련 질문을 남겨주세요. 질문을 상세히 작성하면 더 좋습니다.- 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요.궁금한점JDBC, File I/O와 같은 블로킹 I/O작업들은 코루틴에서 수행시 스레드를 반납하지 않는걸로 알고있습니다!그렇다면 대부분의 작업이 JDBC를 통한 RDB조회라면 코루틴을 통해서 크게 가져갈 수 있는 이점은코드의 간결함구조화에서 나오는 장점예외처리작업 제어이정도 되고, 이러한 환경에서는 스레드 반납을 통한 이점은 아쉽지만 크게 못챙겨가는 걸까요?
-
해결됨코틀린 코루틴 완전 정복
KTOR Server 에서 delay
- 학습 관련 질문을 남겨주세요. 질문을 상세히 작성하면 더 좋습니다.- 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요. 안녕하세요, coroutine 강의를 듣고 ktor server 공부를 하는 중에 delay 관련 질의가 생겨 글 남깁니다. delay 의 경우 coroutine 이 스레드를 양보하고, 일정 시간 후에 다시 스레드가 비어있으면 점유하는 형식으로 진행될텐데, 아래와 같은 경우 delay 이후 코드가 실행되지 않습니다. 사용자 → KTOR Server → suspend function call → loop { logic → 너무 많은 동작을 제한하기위한 delay } ktor 는 기본적으로 요청이 IO Dispatcher 를 사용하는 것 같은데, coroutine 에서 delay 이후 기능이 동작하지 않는 것에 대한 이유가 있을까요..? (강의랑 무관한 내용이라 죄송합니다 ㅠ.ㅠ) /* Routing.kt */ fun Application.configureRouting(searcher: BaseSearcher) { routing { post("/search") { val params = call.receive<Map<String, String>>() searcher.logging("Received POST request with params: $params") val result: List<BaseDTO> = searcher.search(params) searcher.logging("POST Result: $result") call.respond(HttpStatusCode.OK, mapOf("result" to result)) } get("/") { call.respondText("Hello World!") } } } /* Searcher.kt */ /** * 함수 이름 : search * 내용 설명 : 입력된 map<string, string> param 을 바탕으로 요청 전송 및 응답 반환 */ override suspend fun search(searchParam: Map<String, String>): List<SimpleSearchLandResultDTO> { // 별도의 Job 으로 분리해서 오류시 상위 전파 제한 return HttpClient(CIO).use { client -> val parseResultLst: MutableList<SimpleSearchLandResultDTO> = mutableListOf() try { val initRequestResultDto: SimpleSearchResultDTO = sendFormedRequest(client, SearcherConst.URL_MAIN_PAGE, false) // 기본 페이지 요청이 성공했을 때 다음 요청을 진행 if (initRequestResultDto.isSuccess) { val mutableSearchParam = searchParam.toMutableMap() delay(300) while (true) { val eachPageSearchResultDto: SimpleSearchResultDTO = sendFormedRequest(client, makeGetUrl(mutableSearchParam), true) // 실패한 경우 종료 if (!eachPageSearchResultDto.isSuccess) { logging("[search] 조회 중 오류가 발생하였습니다.") break } // 결과가 빈 경우도 종료 else if (eachPageSearchResultDto.landSearchResultLst.isEmpty()) { break } // 결과가 있는 경우엔 전부 더해줌 parseResultLst.addAll(eachPageSearchResultDto.landSearchResultLst) logging("[search] result(${eachPageSearchResultDto.landSearchResultLst[0].currentPage} : ${eachPageSearchResultDto.landSearchResultLst}") // 마지막 페이지인 경우 종료 if (eachPageSearchResultDto.landSearchResultLst[0].currentPage == eachPageSearchResultDto.landSearchResultLst[0].maxPage ) { break } else { // 다음 페이지 수집 진행 mutableSearchParam["currentPage"] = (eachPageSearchResultDto.landSearchResultLst[0].currentPage + 1).toString() delay(300) } } } } catch (e: Exception) { logging("[search] search 함수 중 오류가 발생하였습니다. param: $searchParam, exception: $e") } logging("[search] Result : $parseResultLst") // parseResultLst 반환 parseResultLst } } private suspend fun sendFormedRequest(client: HttpClient, url: String, isResultNeed: Boolean): SimpleSearchResultDTO { return try { val response = client.get(url) { headers { append(HttpHeaders.Cookie, cookie.toCookieString()) append(HttpHeaders.Accept, SearcherConst.DEFAULT_HTTP_ACCEPT) append(HttpHeaders.AcceptEncoding, SearcherConst.DEFAULT_HTTP_ACCEPT_ENCODING) append(HttpHeaders.Connection, SearcherConst.DEFAULT_HTTP_CONNECTION) append(HttpHeaders.AcceptLanguage, SearcherConst.DEFAULT_HTTP_ACCEPT_LANGUAGE) append(HttpHeaders.UserAgent, SearcherConst.DEFAULT_HTTP_USER_AGENT) } } cookie.putAll(response.headers[HttpHeaders.SetCookie]?.split(";")?.map { it.split("=") }?. filter { it.size == 2 }?.filter { it[1] != "/" && it[1].isNotEmpty() }?.associate { it[0] to it[1] } ?: mapOf()) if (response.status != HttpStatusCode.OK) { SimpleSearchResultDTO(false) } else { SimpleSearchResultDTO(true, if(isResultNeed) parseData(response.bodyAsText()) else mutableListOf()) } } catch(e: Exception) { logging("[sendFormedRequest] Error : $e", Level.ERROR) SimpleSearchResultDTO(false) } }