본문 바로가기

고급 Python Skill/비동기함수

Python에서 비동기 함수가 필요한 이유?

파이썬을 통해서 어느정도 개발을 하다보면 처리 속도에 대한 고민을 하게 됩니다. 이런 처리속도 측면에서 좋은 스킬로 사용할 수 있는 비동기 함수 사용방법에 대해 소개하고 실제로 얼마나 감소 효과가 있는지 비교하여 설명해보겠습니다.

 


 

0. Coroutine(코루틴)

 

들어가기에 앞서, 비동기 처리를 이해하기 위하여 Coroutine 이라는 개념을 잠깐 소개하도록 하겠습니다.  Coroutine 은 비동기 프로그래밍을 위한 중요한 도구 중 하나로, '동시에 일어나는 것처럼 보이는 것'을 가능하게 합니다. Coroutine은 특별한 종류의 함수로, 실행을 일시 중단하고 필요할 때 다시 시작할 수 있습니다. 이를 통해 우리는 I/O 작업이나 사용자 입력과 같이 시간이 오래 걸리는 작업을 기다리는 동안 다른 코드를 실행할 수 있습니다. 본문에서는 asyncio와 async를 이용하여 Coroutine을 적용하였습니다.

 


 

1. 속도 비교

 

누구든 쉽게 실험해 보실 수 있도록 구글 코랩(https://colab.research.google.com/)을 이용한 샘플코드 예시를 보여드리겠습니다. 아래 코드는 웹 페이지를 가져오는 내용으로 6개의 페이지를 읽어오는데 소요되는 시간을 비교할 수 있도록 설정하였습니다.

 

[동기 함수 - 일반적인 사용법]

from time import time
from urllib.request import Request, urlopen
 
urls = ['https://www.google.co.kr/search?q=' + i
        for i in ['apple', 'pear', 'grape', 'pineapple', 'orange', 'strawberry']]
 
begin = time()
result = []
for url in urls:
    request = Request(url, headers={'User-Agent': 'Mozilla/5.0'})    # UA가 없으면 403 에러 발생
    response = urlopen(request)
    page = response.read()
    result.append(len(page))
 
print(result)
end = time()
print('실행 시간: {0:.3f}초'.format(end - begin))

# [298659, 148540, 96133, 85428, 89785, 183269]
# 실행 시간: 3.302초

 

 

[비동기 함수]

from time import time
from urllib.request import Request, urlopen
import asyncio
import nest_asyncio

nest_asyncio.apply() # 코랩이나 jupyter notebook에서 실행 시 필요한 부분
 
urls = ['https://www.google.co.kr/search?q=' + i
        for i in ['apple', 'pear', 'grape', 'pineapple', 'orange', 'strawberry']]
 
async def fetch(url):
    request = Request(url, headers={'User-Agent': 'Mozilla/5.0'})    # UA가 없으면 403 에러 발생
    response = await loop.run_in_executor(None, urlopen, request)    # run_in_executor 사용
    page = await loop.run_in_executor(None, response.read)           # run in executor 사용
    return len(page)
 
async def main():
    futures = [asyncio.ensure_future(fetch(url)) for url in urls]
                                                           # 태스크(퓨처) 객체를 리스트로 만듦
    result = await asyncio.gather(*futures)                # 결과를 한꺼번에 가져옴
    print(result)
 
begin = time()
loop = asyncio.get_event_loop()          # 이벤트 루프를 얻음
loop.run_until_complete(main())          # main이 끝날 때까지 기다림
end = time()
print('실행 시간: {0:.3f}초'.format(end - begin))

# [298656, 148579, 110084, 85428, 89785, 183124]
# 실행 시간: 0.718초

 

 

코드에 대해서 첨언하자면, Request와 urlopen은 웹 페이지 요청을 위해 필요한 모듈입니다. asyncio와 nest_asyncio 모듈은 비동기 프로그래밍을 위한 모듈로, nest_asyncio.apply()는 코랩이나 주피터 노트북에서 실행 시 필요한 부분입니다.

다음으로, 요청할 웹 페이지의 URL을 리스트로 정의합니다. 이 예제에서는 구글 검색 결과 페이지를 대상으로 합니다. urlopen 함수를 loop.run_in_executor를 통해 비동기적으로 실행하며, 응답을 받은 후에는 해당 페이지의 길이를 반환합니다. main 함수는 비동기적으로 실행될 주요 함수입니다. ensure_future 함수를 사용하여 여러 개의 fetch 태스크(퓨처) 객체를 생성하고, asyncio.gather 함수를 사용하여 모든 태스크의 결과를 한꺼번에 가져옵니다. 마지막으로, 실행 시간을 측정하여 출력합니다.

마지막으로, loop 객체를 얻고 main 함수를 실행시키고, 실행 시간을 측정하여 출력합니다.

 


 

2. 결론

 

실험 결과로부터, 해당 작업에서는 for 루프를 사용하여 처리하는 속도를 약 5배 빠르게 개선할 수 있다는 사실을 확인할 수 있었습니다. 이는 이전 루프의 결과를 기다리는 시간을 줄일 수 있음을 의미합니다. 이러한 기술은 웹 플랫폼 개발 시 Backend 코드의 처리 속도를 향상시키는 데 매우 유용하다고 생각됩니다. 따라서 이를 참고하여 좋은 플랫폼 개발에 적용하시면 좋을 것입니다.