지난글에 이은 세번째 글입니다. 지난 글에서 파이썬에서 비동기를 구현하는 법에 대해 알아봤습니다. 이번 글에서는 어떻게 비동기를 활용할 수 있을지에 대해 알아봅시다.
비동기를 사용하면 네트워크 IO의 지연 때문에 낭비되는 시간을 줄일 수 있습니다. 온라인 사전사이트에서 단어들의 의미를 크롤링하는 코드를 작성한다고 가정해봅시다. 동기적인 방식을 사용한다면 아래와 같이 코드를 작성할 수 있습니다.
동기적 방식
# requests와 bs4 설치 필요
import requests
import time
from bs4 import BeautifulSoup
def get_text_from_url(url):
print(f'Send request to ... {url}')
res = requests.get(url, headers={'user-agent': 'Mozilla/5.0'})
print(f'Get response from ... {url}')
text = BeautifulSoup(res.text, 'html.parser').text
return text
if __name__ == "__main__":
start = time.time()
base_url = 'https://www.macmillandictionary.com/us/dictionary/american/{keyword}'
keywords = ['hi', 'apple', 'banana', 'call', 'feel',
'hello', 'bye', 'like', 'love', 'environmental',
'buzz', 'ambition', 'determine']
urls = [base_url.format(keyword=keyword) for keyword in keywords]
for url in urls:
text = get_text_from_url(url)
print(text[:100].strip())
end = time.time()
print(f'time taken: {end-start}')
>>
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/hi
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/hi
hi (interjection) American English definition and synonyms | Macmillan Dictionary
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/apple
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/apple
apple (noun) American English definition and synonyms | Macmillan Dictionary
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/banana
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/banana
...
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/determine
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/determine
determine (verb) American English definition and synonyms | Macmillan Dictionary
time taken: 12.112096309661865
약 12.11초가 걸렸습니다. 요청에 대한 응답을 받아야만 다음 요청을 할 수 있기 때문에 지연되는 시간이 발생합니다.
비동기적 방식 (Asyncio + requests)
이번에는 비동기적으로 코드를 작성해봅시다. requests는 비동기적으로 작성되지 않았기 때문에 loop.run_in_executor
를 통해 쓰레드를 만드는 방식을 사용합니다. (이전 글 참고)
import requests
import time
import asyncio
from functools import partial
from bs4 import BeautifulSoup
async def get_text_from_url(url): # 코루틴 정의
print(f'Send request to ... {url}')
loop = asyncio.get_event_loop()
# loop.run_in_executor는 kwargs(keyword arguments)를 사용할 수 없기 때문에 functools.partial을 활용
request = partial(requests.get, url, headers={
'user-agent': 'Mozilla/5.0'})
# ascyncio의 디폴트 쓰레드풀을 사용할 경우 첫번째 인자로 None
# 직접 쓰레드풀을 만들 경우 concurrent.futures.threadpoolexecutor 사용
res = await loop.run_in_executor(None, request)
print(f'Get response from ... {url}')
text = BeautifulSoup(res.text, 'html.parser').text
print(text[:100].strip())
async def main():
base_url = 'https://www.macmillandictionary.com/us/dictionary/american/{keyword}'
keywords = ['hi', 'apple', 'banana', 'call', 'feel',
'hello', 'bye', 'like', 'love', 'environmental',
'buzz', 'ambition', 'determine']
# 아직 실행된 것이 아니라, 실행할 것을 계획하는 단계
futures = [asyncio.ensure_future(get_text_from_url(
base_url.format(keyword=keyword))) for keyword in keywords]
await asyncio.gather(*futures)
if __name__ == "__main__":
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
end = time.time()
print(f'time taken: {end-start}')
>>
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/hi
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/apple
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/banana
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/call
...
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/ambition
ambition (noun) American English definition and synonyms | Macmillan Dictionary
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/apple
apple (noun) American English definition and synonyms | Macmillan Dictionary
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/love
love (verb) American English definition and synonyms | Macmillan Dictionary
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/determine
determine (verb) American English definition and synonyms | Macmillan Dictionary
...
time taken: 1.4359536170959473
이번에는 약 1.43초가 걸렸습니다. 비동기적으로 작성한 코드가 동기적으로 작성한 코드에 비해 약 8배 이상의 성능개선이 있습니다. 결과를 보면 요청에 대한 응답을 기다리지 않고 바로 다른 요청을 하는 것을 확인할 수 있습니다. 또한, "hi > apple > banana" 순으로 요청했으나 응답은 "ambition > apple > love" 순으로 요청순서와 일치하지 않는 것을 확인할 수 있습니다. 이는 요청순서가 아니라 응답 순서로 코드를 처리했기 때문입니다.
비동기적 방식 (asyncio + aiohttp)
하지만, requests
모듈은 코루틴으로 만들어진 모듈이 아니기 때문에 위의 코드는 내부적으로 쓰레드를 만들어 동작합니다. 따라서, 요청의 수가 많아질수록 컨텍스트 스위칭의 비용이 발생합니다. (이전글 참고)
비동기 HTTP통신 라이브러리인 aiohttp
를 이용하면 코루틴을 이용한 비동기 방식을 이용할 수 있습니다.
import time
import asyncio
# aiohttp 설치 필요
import aiohttp
from bs4 import BeautifulSoup
async def get_text_from_url(url): # 코루틴 정의
print(f'Send request to ... {url}')
async with aiohttp.ClientSession() as sess:
async with sess.get(url, headers={'user-agent': 'Mozilla/5.0'}) as res:
text = await res.text()
print(f'Get response from ... {url}')
text = BeautifulSoup(text, 'html.parser').text
print(text[:100].strip())
async def main():
base_url = 'https://www.macmillandictionary.com/us/dictionary/american/{keyword}'
keywords = ['hi', 'apple', 'banana', 'call', 'feel',
'hello', 'bye', 'like', 'love', 'environmental',
'buzz', 'ambition', 'determine']
# 아직 실행된 것이 아니라, 실행할 것을 계획하는 단계
futures = [asyncio.ensure_future(get_text_from_url(
base_url.format(keyword=keyword))) for keyword in keywords]
await asyncio.gather(*futures)
if __name__ == "__main__":
start = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
end = time.time()
print(f'time taken: {end-start}')
>>
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/hi
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/apple
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/banana
Send request to ... https://www.macmillandictionary.com/us/dictionary/american/call
...
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/hello
hello (interjection) American English definition and synonyms | Macmillan Dictionary
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/environmental
environmental (adjective) American English definition and synonyms | Macmillan Dictionary
Get response from ... https://www.macmillandictionary.com/us/dictionary/american/apple
apple (noun) American English definition and synonyms | Macmillan Dictionary
...
time taken: 1.4322197437286377
물론 위의 코드에서는 요청 수가 많지 않아 성능의 차이를 확인하기는 어렵습니다.
위와같이 하나의 웹사이트에서 크롤링할 때 비동기적인 방식을 사용하면 짧은 시간에 너무나도 많은 요청으로 인해 웹사이트에 나쁜 영향을 끼칠 수 있습니다. 아이피를 차단당할 수도 있으니 주의합시다. 위의 코드는 단지 하나의 예시일 뿐입니다. 만약 여러 웹사이트에서 크롤링한다면 비동기적인 방식을 고려해볼 수 있습니다.
Awesome Asyncio
asyncio
를 제대로 사용하기 위해서는 사용하는 모듈들이 모두 코루틴으로 작성되어 있어야 합니다. 아래의 페이지에서는 asyncio
기반 모듈들을 소개하고 있습니다.
비동기 기반 웹프레임워크
파이썬에는 훌륭한 웹프레임워크인 Django
와 Flask
가 존재합니다. 하지만, 비동기 방식으로 동작하는 웹프레임워크들도 활발히 개발중입니다. 대표적인 예가 sanic
과 vibora
입니다.
sanic은 카카오
를 비롯한 많은 곳에서 이미 생산단계에서 이용중인 웹프레임워크이며, vibora는 아직 생산단계에서 사용하기에는 무리가 따르지만, cython
과 asyncio
를 이용해 엄청난 퍼포먼스로 많은 관심을 받고있는 웹프레임워크입니다.
아래는 vibora
에서 소개하고 있는 벤치마크입니다. sanic
과 vibora
모두 Django
와 Flask
에 비해 좋은 성능을 보여줍니다. (물론 Instagram은 Django를 사용하지만 전 세계 유저들이 무리 없이 이용하고 있습니다.)
출처: https://github.com/vibora-io/benchmarks
두 프레임워크 모두 올 12월에 버전업이 예정되어있습니다. 파이썬에서 비동기 생태계가 좀 더 발전하길 바라는 마음에서 소개해봤습니다.
이상으로 파이썬과 비동기 프로그래밍 시리즈의 연재를 마칩니다. 프로젝트에 비동기를 도입하는 것이 어떨지 고민해보는 것도 좋을 거 같습니다. 읽어주셔서 감사합니다.
'개발 > 파이썬' 카테고리의 다른 글
파이썬으로 만드는 나만의 커맨드라인 프로그램 #2 - click (1) | 2019.03.10 |
---|---|
파이썬으로 만드는 나만의 커맨드라인 프로그램 #1 - argparse (0) | 2019.03.10 |
파이썬과 비동기 프로그래밍 #2, 파이썬에서 비동기 프로그래밍 시작하기 (0) | 2019.03.01 |
파이썬과 비동기 프로그래밍 #1, 비동기 프로그래밍이란 (0) | 2019.03.01 |
파이썬의 컨텍스트 매니저 (Context Manager) 에 대해 알아봅시다. (4) | 2019.03.01 |