[python] API 비동기 처리를 위한 python의 동시성 이해

MS·2025년 4월 6일
0

개요

  • 현재 우리 회사 제품의 훈련 기록은 전부 LRS 서버에 저장되고 있으며, 이 훈련 기록은 API호출을 통해 조회함
  • 하지만, 파이썬에서 일반적으로 사용하는 requests 라이브러리를 이용한 API 호출은 I/O bound를 유발하여 API 호출을 여러 번 할 수록 소요 시간이 계속해서 늘어나는 문제가 생김
  • 따라서, I/O bound로 발생하는 시간 소요를 줄이기 위한 파이썬의 비동기 처리 방법을 정리함

동시성(concurrency)이란?

  • 일반적으로 여러 API 호출을 하는 경우 task1 - task2 - task3 - task4 순서대로 반드시 이전 task의 수행이 끝내야만 다음 task가 실행되어 동기 처리 된다.

  • 하지만, 비동기 처리는 이전 단계가 끝나지 않더라도 다음 단계를 실행할 수 있는데, 크게 동시성(concurrency) 또는 병렬성(parallelism)을 가지고 여러 작업을 동시에 수행할 수 있다.

  • 여기서 병렬성은 한 순간에 여러 작업의 수행이 가능한 것을 의미하고, 동시성은 비록 한번에 여러 작업을 수행할 수는 없지만, 여러 작업을 스위칭해가며 동시에 다룰 수 있다.

  • 병렬성은 CPU core를 단위로 멀티 프로세싱 또는 스레드를 단위로 멀티 스레드를 통해 물리적으로 구현되고, 동시성은 물리 장치에 구애받지 않아 논리적으로 구현된다.

I/O bound란?


I/O bound는 프로세스가 진행될 때, 파일 쓰기, 디스크 작업, 네트워크 통신와 같은 I/O 작업의 대기 시간이 많은 경우에 발생한다.

API 호출을 하는 경우 LRS서버로부터 응답 받을 때까지의 대기 시간 동안 어떤 작업도 하지 않기 때문에 I/O wating이 발생하고, 이는 실행 시간의 지연으로 이어지는 것이다.

따라서, 파이썬에서는 동시성 개념을 차용한 비동기 처리를 이용하여 대기 시간에도 다른 작업을 처리하는 방법으로 병목 현상을 해결함으로써 I/O bound를 해결하게 된다.

예시

위 코드는 GET API 요청 100개를 순차적으로 동기 처리하는 코드이다.

수행 결과 16.21초가 소요된 것을 확인할 수 있다.
그렇다면, 비동기 처리를 통해 API 요청을 수행하면 어떨까?

동시성 개념을 이용한 비동기처리는 asyncio와 aiohttp를 활용해서 구현가능하다.
asyncio에서 핵심적인 개념은 바로 코루틴이다.

코루틴은 어떤 작업이 완료되지 않은 채로 작업을 일시 정지시켰다가 다시 정지된 작업을 재개할 수 있는 기능을 가진 객체로 async def를 통해 만들어진다.

이때, async def로 정의된 비동기 함수는 해당 함수를 실행하는 것이 아닌, 해당 함수에 대한 코루틴 객체를 반환할 뿐이다. 때문에 이 코루틴 객체를 처리할 수 있는 이벤트 루프가 필요하다.

위 코드의 실행 흐름을 정리해보면 아래와 같다.

  1. 코루틴을 사용하기 위해선 asyncio.run()로 이벤트 루프를 만든 다음 test_async_request() 코루틴 객체가 등록

  2. 이벤트 루프는 test_async_request()가 만든 코루틴을 실행하다가 await를 만나면 일시 정지하고, 제어권을 이벤트 루프에 반환

  3. asyncio.gather에 리스트 형태로 담긴 복수의 코루틴 객체를 이벤트 루프에 전부 등록하고 비동기 처리가 끝나면 responses를 반환

  4. 반환된 응답 코루틴 객체를 읽을 수 있는 text 형태로 변환

실행 시간이 엄청나게 줄어들었음을 알 수 있다.
추가적으로 async with, async for를 이용해서 기존 컨텍스트 매니저나 중첩문에서 병목이 발생하는 경우에도 사용 가능하다.

결론

  • 네트워크 연결이나 입출력 과정에서 발생하는 I/O bound 문제는 asyncio를 이용하여 해결하는 것이 효율적이다.

  • 동시성 개념을 이용한 비동기처리를 하기 때문에 싱글 스레드, 싱글 프로세스 환경에서 동작하며, 병목 현상이 발생하는 지점에서 task를 swith하는 방식으로 구현된다.

  • 개발자가 병목 발생 구간을 await로 명확하게 정의해줘야만 비동기 처리로 시간 절감을 할 수 있다.

profile
한 걸음씩 꾸준하게

0개의 댓글