본문 바로가기
Project/ThreeMovie(영화리뷰및예약도우미)

2. 비동기, 코루틴, 크론 탭 스케줄러 [kotlin / jsoup]

by HDobby 2023. 6. 23.

스케줄러를 만들때 사용한 async와 scheduled 어노테이션, 그리고 코루틴에 대해 알아봅시다.

 

목차

     


    사용된 코드

    @EnableScheduling
    @EnableAsync
    class ThreemovieapiApplication
    @Async
    @Scheduled(cron = "0 0/5 * * * ?")
    fun chkMovieShowingTime() {
    	runBlocking {
            for (mbTheater in mbTheaters) {
                CoroutineScope(Dispatchers.IO).async { updateMBShowtimes(mbTheater) }
                    .also { showTimeAsync.add(it) }
            }
            for (lcTheater in lcTheaters) {
                CoroutineScope(Dispatchers.IO).async { updateLCShowtimes(lcTheater) }
                    .also { showTimeAsync.add(it) }
            }
            for (cgvTheater in cgvTheaters) {
                CoroutineScope(Dispatchers.IO).async { updateCGVShowtimes(cgvTheater) }
                    .also { showTimeAsync.add(it) }
            }
            for (showTimeDeferred in showTimeAsync) {
                showTimeList.addAll(showTimeDeferred.await())
            }
        }
    }
    @Configuration
    class ScheduleConfig : AsyncConfigurer, SchedulingConfigurer {
    	fun threadPoolTaskScheduler(): ThreadPoolTaskScheduler {
    		val scheduler = ThreadPoolTaskScheduler()
    		scheduler.poolSize = Runtime.getRuntime().availableProcessors() * 2
    		scheduler.setThreadNamePrefix("MY-SCHEDULER-")
    		scheduler.initialize()
    		
    		return scheduler
    	}
    	
    	override fun configureTasks(taskRegistrar: ScheduledTaskRegistrar) {
    		taskRegistrar.setTaskScheduler(this.threadPoolTaskScheduler())
    	}
    }

    비동기

    가장 대표적인 예시인 은행 업무 처리로 봅시다.

     

    동기 방식의 경우 과정이 동시에 진행이 되어야 합니다. 돈을 차감함과 동시에 현금을 지급해야 합니다.

     

     

    비동기 방식의 경우 동시에 진행이 되지 않아도 됩니다. 한쪽의 업무를 계속 진행하다 끝이 나면 결과를 알려줍니다.

     

    이를 사용하기 위해서는

    @EnableAsync 어노테이션을 달아줘야 하고, 사용할 public 함수 위에 @Async 어노테이션을 달아줘야 합니다.

     

    원래는 설정에서 ThreadPoolTaskExecutor를 사용합니다. 하지만 저는 scheduled와 함께 사용하여 ThreadPoolTaskScheduler를 사용하게 됩니다.

    @Configuration
    class ScheduleConfig : AsyncConfigurer, SchedulingConfigurer {
    	fun threadPoolTaskScheduler(): ThreadPoolTaskScheduler {
    		val scheduler = ThreadPoolTaskScheduler()
    		scheduler.poolSize = Runtime.getRuntime().availableProcessors() * 2
    		scheduler.setThreadNamePrefix("MY-SCHEDULER-")
    		scheduler.initialize()
    		
    		return scheduler
    	}
    	
    	override fun configureTasks(taskRegistrar: ScheduledTaskRegistrar) {
    		taskRegistrar.setTaskScheduler(this.threadPoolTaskScheduler())
    	}
    }

    제 서버가 nas이기에 pool size를 크게 잡게된다면 문제가 생길 여지가 있습니다. 그렇기에 availableProcessors의 2배 정도 만큼만 할당을 해주었습니다.


     

    스케줄러

    크론 스케줄러를 사용 했습니다.

    @EnableScheduling 어노테이션을 메인이나 설정파일에 달아 주시면 됩니다.

    @Scheduled(cron = "0 0/5 * * * ?") 어노테이션을 실행할 함수에 달아 주시면 됩니다.

     

    cron은 초 분 시 일 월 요일 순 입니다.

    *는 모든 경우를 의미합니다.

    /5는 나누었을 때 0이되는 5 10 15 20 25 30...분 마다 실행하겠다는 의미입니다.

    모든 시각의 5분 단위로 실행하게 됩니다.

     

    cron이 아니더라도 실행이 가능하지만 서버가 껏다 켜지거나 업데이트 이슈로 인해 웹사이트 크롤러가 디도스로 오인받아 막히는 경우를 막기위해 5분의 제한을 걸어 두었습니다. 

    var chkTime = lastUpdateTimeRepositorySupport.getLastTime(code)
    		if (chkTime == null) {
    			lastUpdateTimeRepository.save(LastUpdateTime(code, 202302110107))
    			chkTime = 202302110107
    		}
    		if (ChkNeedUpdate.chkUpdateFiveMinute(chkTime))

     

    그리고 따로 위와 같이 마지막 실행 시간을 확인하여 현재 시간으로부터 5분이 경과했는지 확인한 뒤 실행합니다.

    fun chkUpdateTime(time: Long, gap: Long): Boolean {
    			val current = LocalDateTime.now()
    			val formatted = LocalDateTime.parse(time.toString(), formatter)
    			
    			return between(formatted, current).seconds >= gap
    		}

     

    코루틴

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")

    spring boot에서는 해당 implementation을 추가 해주셔야 합니다.

     

    코루틴은 쓰레드보다 더 작은 단위로 이해하고 있습니다. 

    만약 프로세스와 스레드를 배우신 분이라면 아시겠지만 한 프로세스에서 여러 스레드가 돌아갈 수 있습니다.

    하지만 스레드는 os의 스케줄러에 의해 연산 순서가 정해집니다.

     

    스레드는 멈춰도 진행하던 프로세스에서 작업을 진행하지만, 코루틴은 그런게 없이 중간에 suspend 되더라도 자유롭게 스레드를 오가며 하던 작업을 이어서 작업합니다.

     

    코루틴은 2가지의 scope가 있습니다.

    • GlobalScope 부모가 죽더라도 해당 Scope로 선언된 코루틴은 죽지 않습니다. 부모가 죽는 경우 fork로 선언된 좀비 프로세스와 비슷해집니다.
    • CoroutineScope 부모가 죽으면 같이 사라지게 됩니다. 데이터를 반환할 수 있습니다.

     

    데이터를 크롤링해서 처리하는데 평균 6~8분 정도가 소요됩니다. 단순히 크롤링도 오래걸리지만 데이터를 매핑하여 저희가 원하는대로 가공하는 과정 또한 2분이 걸리게 됩니다. 그 시간을 극단적으로 줄여준게 코루틴 입니다.

     

    runBlocking {
            for (mbTheater in mbTheaters) {
                CoroutineScope(Dispatchers.IO).async { updateMBShowtimes(mbTheater) }
                    .also { showTimeAsync.add(it) }
            }
            for (lcTheater in lcTheaters) {
                CoroutineScope(Dispatchers.IO).async { updateLCShowtimes(lcTheater) }
                    .also { showTimeAsync.add(it) }
            }
            for (cgvTheater in cgvTheaters) {
                CoroutineScope(Dispatchers.IO).async { updateCGVShowtimes(cgvTheater) }
                    .also { showTimeAsync.add(it) }
            }
            for (showTimeDeferred in showTimeAsync) {
                showTimeList.addAll(showTimeDeferred.await())
            }
        }

    runBlocking의 경우 함수의 진행을 막고 내부에 CoroutineScope를 선언할 수 있습니다.

    이를 비동기 적으로 처리하기 위해 async로 각 영화사 영화관 별 데이터를 넘겨줘 코루틴으로 실행을 시킨 뒤, await으로 기다린 데이터를 모아 따로 저장을 해줍니다.

     

    전체적인 과정은 아래와 같습니다.

    1. 비동기적으로 스케줄러를 실행
    2. 함수 실행 시간이 5분이 넘었는지 확인
    3. 코루틴으로 크롤러를 실행
    4. 전체 데이터를 batch insert

     

    더 자세한 코루틴의 내용을 알고 싶으시다면 참고에 적어놓은 블로그 들을 봐주시면 감사하겠습니다.


    참고

     

    728x90

    댓글