본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 3. · 15 Views
병렬 처리 개요 완벽 가이드
파이썬에서 병렬 처리와 비동기 프로그래밍의 핵심 개념을 초급자도 이해할 수 있도록 쉽게 설명합니다. 순차 처리와 병렬 처리의 차이부터 asyncio와 concurrent.futures 활용법까지 실무 예제와 함께 알아봅니다.
목차
- 병렬_처리란_무엇인가
- 순차_처리_vs_병렬_처리_비교
- 병렬_처리의_장점
- 파이썬_비동기_프로그래밍_기초
- asyncio와_concurrent_futures
- 주요_활용_사례_분석
- 병렬_처리_설계_시_고려사항
1. 병렬 처리란 무엇인가
어느 날 김개발 씨가 데이터 수집 프로그램을 만들고 있었습니다. 100개의 웹사이트에서 정보를 가져와야 하는데, 하나씩 순서대로 처리하니 무려 10분이나 걸렸습니다.
"이걸 더 빠르게 할 수 없을까?" 고민하던 중, 선배 박시니어 씨가 다가와 말했습니다. "병렬 처리를 써보는 건 어때요?"
병렬 처리는 한마디로 여러 작업을 동시에 처리하는 것입니다. 마치 식당에서 요리사 한 명이 모든 요리를 순서대로 만드는 것과 여러 요리사가 각자 다른 요리를 동시에 만드는 것의 차이와 같습니다.
이것을 제대로 이해하면 프로그램의 실행 속도를 획기적으로 개선할 수 있습니다.
다음 코드를 살펴봅시다.
# 순차 처리 vs 병렬 처리 개념 비교
import time
# 순차 처리: 한 번에 하나씩
def sequential_work():
for i in range(3):
print(f"작업 {i+1} 처리 중...")
time.sleep(1) # 각 작업에 1초 소요
# 병렬 처리 개념: 여러 작업을 동시에
# (실제 구현은 다음 섹션에서 다룹니다)
def parallel_concept():
# 작업 1, 2, 3이 동시에 시작됨
# 총 소요 시간: 1초 (3초가 아닌!)
pass
print("순차 처리 시작")
sequential_work() # 총 3초 소요
print("완료!")
김개발 씨는 입사 6개월 차 주니어 개발자입니다. 오늘 팀장님으로부터 특별한 미션을 받았습니다.
회사에서 운영하는 100개 쇼핑몰의 상품 가격을 매일 수집해서 비교 분석하는 프로그램을 만들어야 합니다. 처음에는 단순하게 생각했습니다.
첫 번째 사이트에 접속해서 가격을 가져오고, 두 번째 사이트에 접속해서 가격을 가져오고, 이런 식으로 100번 반복하면 되겠지요. 그런데 막상 실행해보니 문제가 생겼습니다.
각 사이트에서 데이터를 가져오는 데 평균 6초가 걸렸습니다. 100개 사이트를 순서대로 처리하니 무려 10분이나 소요되었습니다.
팀장님은 이 작업을 1분 안에 끝내달라고 하셨는데, 이대로는 불가능해 보였습니다. 이때 선배 박시니어 씨가 조언을 해주었습니다.
"김개발 씨, 병렬 처리라는 걸 들어봤어요?" 그렇다면 병렬 처리란 정확히 무엇일까요? 쉽게 비유하자면, 병렬 처리는 마치 은행 창구와 같습니다.
창구가 하나뿐인 은행에서는 고객들이 긴 줄을 서서 기다려야 합니다. 앞 사람의 업무가 끝나야 다음 사람 차례가 됩니다.
하지만 창구가 10개라면 어떨까요? 10명의 고객이 동시에 업무를 볼 수 있어서 대기 시간이 크게 줄어듭니다.
프로그래밍에서도 마찬가지입니다. 한 번에 하나의 작업만 처리하는 것을 순차 처리라고 합니다.
반면에 여러 작업을 동시에 처리하는 것을 병렬 처리라고 합니다. 병렬 처리를 활용하면 전체 작업 시간을 획기적으로 줄일 수 있습니다.
김개발 씨의 상황으로 돌아가 봅시다. 100개 사이트를 순서대로 처리하면 10분이 걸렸습니다.
하지만 10개씩 동시에 처리한다면 어떨까요? 이론적으로는 1분이면 충분합니다.
바로 이것이 병렬 처리의 힘입니다. 물론 병렬 처리가 만능은 아닙니다.
모든 상황에서 효과적인 것은 아니며, 적절한 상황에서 올바르게 사용해야 합니다. 다음 섹션에서 순차 처리와 병렬 처리를 구체적으로 비교해보겠습니다.
박시니어 씨의 조언을 들은 김개발 씨는 눈이 반짝였습니다. "그렇군요!
병렬 처리를 배워서 적용해봐야겠어요." 이제 본격적으로 병렬 처리의 세계로 들어가 봅시다.
실전 팁
💡 - 병렬 처리는 여러 독립적인 작업을 동시에 수행할 때 효과적입니다
- 모든 작업이 병렬 처리에 적합한 것은 아니므로 작업의 특성을 먼저 파악하세요
- 처음에는 개념을 이해하고, 실제 구현은 단계적으로 학습하세요
2. 순차 처리 vs 병렬 처리 비교
김개발 씨는 병렬 처리의 개념을 이해했지만, 아직 확신이 서지 않았습니다. "정말 그렇게 차이가 클까요?" 박시니어 씨는 미소를 지으며 말했습니다.
"직접 비교해보면 확실히 알 수 있어요. 같은 작업을 두 가지 방식으로 실행해볼까요?"
순차 처리는 작업을 하나씩 차례대로 수행하는 방식입니다. 반면 병렬 처리는 여러 작업을 동시에 수행합니다.
마치 한 줄로 서서 놀이기구를 타는 것과 여러 줄로 나뉘어 동시에 입장하는 것의 차이와 같습니다. 처리할 작업이 많고 각 작업이 독립적일수록 병렬 처리의 효과가 극대화됩니다.
다음 코드를 살펴봅시다.
import time
import concurrent.futures
def fetch_data(site_id):
"""웹사이트에서 데이터를 가져오는 시뮬레이션"""
time.sleep(1) # 네트워크 지연 시뮬레이션
return f"사이트 {site_id} 데이터"
# 순차 처리
def sequential_fetch(sites):
start = time.time()
results = [fetch_data(site) for site in sites]
print(f"순차 처리: {time.time() - start:.2f}초")
return results
# 병렬 처리
def parallel_fetch(sites):
start = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(fetch_data, sites))
print(f"병렬 처리: {time.time() - start:.2f}초")
return results
sites = [1, 2, 3, 4, 5]
sequential_fetch(sites) # 약 5초 소요
parallel_fetch(sites) # 약 1초 소요
박시니어 씨는 화이트보드에 간단한 그림을 그리기 시작했습니다. "자, 여기 5개의 작업이 있다고 가정해볼게요.
각 작업은 1초씩 걸립니다." 먼저 순차 처리를 설명했습니다. 첫 번째 작업이 끝나야 두 번째 작업이 시작됩니다.
두 번째가 끝나야 세 번째가 시작됩니다. 이런 식으로 5개 작업을 모두 완료하려면 5초가 필요합니다.
이번에는 병렬 처리입니다. 5개의 작업이 동시에 시작됩니다.
모든 작업이 1초 동안 실행되고, 1초 후에 5개 작업이 모두 완료됩니다. 총 소요 시간은 단 1초입니다.
김개발 씨는 고개를 끄덕였지만 의문이 생겼습니다. "그런데 컴퓨터가 정말로 여러 작업을 동시에 할 수 있나요?
CPU는 한 번에 하나의 명령만 처리하는 거 아닌가요?" 좋은 질문이었습니다. 박시니어 씨가 설명을 이어갔습니다.
"사실 여기에는 두 가지 개념이 있어요. 동시성과 병렬성이에요." **동시성(Concurrency)**은 여러 작업이 번갈아가며 실행되는 것입니다.
마치 요리사가 파스타 물을 끓이는 동안 샐러드를 준비하는 것과 같습니다. 실제로 동시에 하는 것은 아니지만, 대기 시간을 활용해서 다른 작업을 처리합니다.
**병렬성(Parallelism)**은 진짜로 동시에 실행되는 것입니다. 여러 명의 요리사가 각자 다른 요리를 동시에 만드는 것과 같습니다.
이것은 여러 개의 CPU 코어가 있어야 가능합니다. 위의 코드 예제를 살펴봅시다.
ThreadPoolExecutor를 사용해서 5개의 작업을 병렬로 처리하고 있습니다. max_workers=5는 최대 5개의 작업을 동시에 처리하겠다는 의미입니다.
fetch_data 함수는 각 사이트에서 데이터를 가져오는 것을 시뮬레이션합니다. time.sleep(1)로 네트워크 지연을 표현했습니다.
순차 처리에서는 5개 사이트를 하나씩 처리하므로 5초가 걸립니다. 하지만 병렬 처리에서는 5개를 동시에 처리하므로 1초면 충분합니다.
김개발 씨는 감탄했습니다. "5배나 빨라지네요!" 박시니어 씨가 덧붙였습니다.
"맞아요. 하지만 항상 그런 건 아니에요.
병렬 처리에도 오버헤드가 있고, 작업의 특성에 따라 효과가 달라져요." 실제로 모든 작업이 병렬 처리에 적합한 것은 아닙니다. I/O 바운드 작업, 즉 파일 읽기, 네트워크 요청, 데이터베이스 쿼리처럼 대기 시간이 많은 작업에서 병렬 처리 효과가 큽니다.
반면 CPU 바운드 작업, 즉 복잡한 계산처럼 CPU를 많이 사용하는 작업에서는 다른 접근이 필요합니다. 김개발 씨의 상품 가격 수집 프로그램은 전형적인 I/O 바운드 작업입니다.
웹사이트에 요청을 보내고 응답을 기다리는 시간이 대부분이기 때문입니다. 이런 경우 병렬 처리가 매우 효과적입니다.
실전 팁
💡 - I/O 바운드 작업에서 병렬 처리 효과가 가장 큽니다
- max_workers 값은 작업 특성과 시스템 자원을 고려해서 설정하세요
- 병렬 처리 전후의 실행 시간을 측정해서 효과를 확인하세요
3. 병렬 처리의 장점
김개발 씨는 이제 병렬 처리의 개념을 확실히 이해했습니다. 하지만 단순히 "빨라진다"는 것 외에 어떤 장점이 더 있을까요?
팀장님께 보고할 때 병렬 처리를 도입해야 하는 이유를 명확하게 설명하고 싶었습니다. 박시니어 씨는 화이트보드에 세 가지 키워드를 적었습니다.
병렬 처리의 핵심 장점은 세 가지입니다. 첫째, 처리량 향상으로 같은 시간에 더 많은 작업을 수행할 수 있습니다.
둘째, 응답 시간 단축으로 사용자가 결과를 더 빨리 받을 수 있습니다. 셋째, 자원 활용 최적화로 CPU와 네트워크 대기 시간을 효율적으로 사용할 수 있습니다.
다음 코드를 살펴봅시다.
import time
import concurrent.futures
import requests
def check_website(url):
"""웹사이트 응답 시간 측정"""
try:
start = time.time()
response = requests.get(url, timeout=5)
elapsed = time.time() - start
return {"url": url, "status": response.status_code, "time": elapsed}
except Exception as e:
return {"url": url, "status": "error", "time": 0}
urls = [
"https://www.google.com",
"https://www.github.com",
"https://www.python.org",
]
# 병렬로 여러 사이트 상태 확인 - 처리량 향상
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(check_website, urls))
for result in results:
print(f"{result['url']}: {result['status']} ({result['time']:.2f}초)")
박시니어 씨가 화이트보드에 적은 세 가지 키워드를 하나씩 설명하기 시작했습니다. 첫 번째는 처리량(Throughput) 향상입니다.
처리량이란 일정 시간 동안 처리할 수 있는 작업의 양을 말합니다. 마치 고속도로에 차선이 하나일 때와 다섯 개일 때의 차이와 같습니다.
차선이 많을수록 같은 시간에 더 많은 차량이 통과할 수 있습니다. 김개발 씨의 상품 가격 수집 프로그램을 예로 들어봅시다.
순차 처리로 1시간에 360개 사이트를 처리할 수 있었다면, 10개씩 병렬 처리하면 1시간에 3,600개 사이트를 처리할 수 있습니다. 처리량이 10배 늘어난 것입니다.
두 번째는 응답 시간(Response Time) 단축입니다. 사용자가 요청을 보내고 결과를 받기까지 걸리는 시간입니다.
웹 서비스에서 이것은 매우 중요합니다. 예를 들어 사용자가 상품 비교 페이지를 요청했다고 가정해봅시다.
이 페이지를 보여주려면 5개 쇼핑몰의 가격 정보가 필요합니다. 순차 처리로 각 쇼핑몰에서 2초씩 걸린다면 사용자는 10초를 기다려야 합니다.
하지만 병렬 처리를 사용하면 5개 요청이 동시에 나가므로 2초면 충분합니다. 사용자 경험이 크게 개선됩니다.
세 번째는 자원 활용 최적화입니다. 이것은 조금 더 기술적인 내용입니다.
순차 처리에서 네트워크 요청을 보내고 응답을 기다리는 동안 CPU는 놀고 있습니다. 이 대기 시간에 다른 작업을 처리하면 자원을 더 효율적으로 사용할 수 있습니다.
박시니어 씨가 비유를 들었습니다. "카페에서 바리스타가 커피를 내리는 동안 가만히 기다리기만 하면 비효율적이잖아요?
커피가 내려지는 동안 다음 손님 주문을 받거나 컵을 준비할 수 있죠. 이게 바로 자원 활용 최적화예요." 위의 코드 예제는 여러 웹사이트의 상태를 동시에 확인합니다.
check_website 함수가 각 URL에 요청을 보내고 응답 시간을 측정합니다. ThreadPoolExecutor를 사용해서 3개의 요청을 동시에 보내므로 전체 소요 시간이 크게 줄어듭니다.
실제 서비스에서 이런 패턴은 매우 흔합니다. 모니터링 시스템에서 여러 서버의 상태를 동시에 확인하거나, 검색 엔진에서 여러 데이터 소스를 동시에 조회하거나, 결제 시스템에서 여러 결제 수단의 가용성을 동시에 체크하는 경우가 모두 이에 해당합니다.
김개발 씨는 메모장에 세 가지 장점을 적었습니다. "이 정도면 팀장님께 충분히 설득력 있게 설명할 수 있겠어요!" 하지만 박시니어 씨가 한 가지를 덧붙였습니다.
"장점만 있는 건 아니에요. 복잡성이 증가하고, 동시성 버그가 발생할 수 있어요.
이건 나중에 더 자세히 다룰게요."
실전 팁
💡 - 병렬 처리 도입 전후의 처리량과 응답 시간을 측정해서 효과를 수치화하세요
- I/O 대기 시간이 긴 작업일수록 병렬 처리 효과가 큽니다
- 사용자 경험 개선을 위해 응답 시간 단축에 집중하세요
4. 파이썬 비동기 프로그래밍 기초
이제 실제 파이썬 코드로 병렬 처리를 구현할 차례입니다. 박시니어 씨는 "파이썬에서는 비동기 프로그래밍으로 병렬 처리를 구현할 수 있어요"라고 말했습니다.
김개발 씨는 async와 await 키워드를 본 적은 있지만, 정확히 어떻게 동작하는지는 몰랐습니다.
파이썬의 비동기 프로그래밍은 async와 await 키워드를 사용해서 동시성을 구현하는 방식입니다. 마치 식당에서 주문을 받은 후 요리가 되는 동안 다른 테이블 주문을 받는 것과 같습니다.
특히 I/O 작업이 많은 프로그램에서 효율적이며, 단일 스레드에서도 동시에 여러 작업을 처리하는 것처럼 보이게 할 수 있습니다.
다음 코드를 살펴봅시다.
import asyncio
async def fetch_data(name, delay):
"""비동기로 데이터를 가져오는 시뮬레이션"""
print(f"{name}: 데이터 요청 시작")
await asyncio.sleep(delay) # 네트워크 대기 시뮬레이션
print(f"{name}: 데이터 수신 완료")
return f"{name} 결과"
async def main():
# 세 작업을 동시에 실행
tasks = [
fetch_data("사이트A", 2),
fetch_data("사이트B", 1),
fetch_data("사이트C", 3),
]
# 모든 작업이 완료될 때까지 대기
results = await asyncio.gather(*tasks)
print(f"결과: {results}")
# 비동기 함수 실행
asyncio.run(main()) # 총 3초 소요 (6초가 아닌!)
박시니어 씨가 키보드 앞에 앉았습니다. "자, 이제 실제 코드를 보면서 설명할게요.
파이썬에서 비동기 프로그래밍의 핵심은 async와 await 두 키워드예요." async def로 정의된 함수를 **코루틴(coroutine)**이라고 부릅니다. 일반 함수와 다르게, 코루틴은 실행 중간에 일시 정지했다가 나중에 다시 재개할 수 있습니다.
마치 책갈피를 꽂아두고 다른 책을 읽다가 돌아오는 것과 같습니다. await는 "여기서 잠시 기다릴게요"라는 의미입니다.
await를 만나면 해당 코루틴은 일시 정지하고, 그 사이에 다른 코루틴이 실행될 수 있습니다. 이것이 바로 동시성의 핵심입니다.
코드를 한 줄씩 살펴봅시다. fetch_data 함수는 async def로 정의되어 있습니다.
이 함수 안에서 await asyncio.sleep(delay)를 호출합니다. asyncio.sleep은 time.sleep과 비슷하지만, 비동기적으로 대기합니다.
즉, 대기하는 동안 다른 코루틴이 실행될 수 있습니다. main 함수에서는 세 개의 fetch_data 코루틴을 생성합니다.
각각 2초, 1초, 3초의 지연 시간을 가집니다. 순차적으로 실행하면 총 6초가 걸리겠지만, asyncio.gather를 사용하면 세 작업이 동시에 시작됩니다.
asyncio.gather는 여러 코루틴을 동시에 실행하고, 모든 코루틴이 완료될 때까지 기다립니다. 세 작업 중 가장 오래 걸리는 것이 3초이므로, 전체 소요 시간도 3초입니다.
김개발 씨가 질문했습니다. "그런데 이게 정말 동시에 실행되는 건가요?
스레드를 여러 개 사용하는 건가요?" 박시니어 씨가 고개를 저었습니다. "아니요, 비동기 프로그래밍은 기본적으로 단일 스레드에서 동작해요.
진짜 동시에 실행되는 게 아니라, 한 작업이 대기하는 동안 다른 작업을 처리하는 거예요. 그래서 CPU를 많이 쓰는 작업에는 적합하지 않아요." 중요한 포인트입니다.
비동기 프로그래밍은 I/O 대기 시간을 효율적으로 활용하는 것이지, 여러 CPU 코어를 동시에 사용하는 것이 아닙니다. 네트워크 요청, 파일 읽기, 데이터베이스 쿼리처럼 대기 시간이 긴 작업에 효과적입니다.
마지막으로 asyncio.run(main())은 비동기 프로그램의 진입점입니다. 이 함수가 이벤트 루프를 생성하고 main 코루틴을 실행합니다.
이벤트 루프는 여러 코루틴 사이를 오가며 실행을 관리하는 역할을 합니다. 김개발 씨는 코드를 직접 실행해보았습니다.
정말로 3초 만에 세 개의 결과가 모두 출력되었습니다. "신기하네요!
스레드 없이도 이렇게 동시에 처리할 수 있다니."
실전 팁
💡 - async def로 정의한 함수는 반드시 await로 호출하거나 asyncio.run으로 실행해야 합니다
- 일반 time.sleep 대신 asyncio.sleep을 사용해야 비동기 효과를 얻을 수 있습니다
- 비동기 코드에서는 비동기를 지원하는 라이브러리(aiohttp 등)를 사용하세요
5. asyncio와 concurrent futures
김개발 씨는 async/await를 배웠지만, 아까 코드에서 본 ThreadPoolExecutor와는 어떻게 다른지 궁금했습니다. "둘 다 병렬 처리를 위한 거 아닌가요?
언제 뭘 써야 하는 거예요?" 박시니어 씨는 이것이 많은 개발자가 헷갈려하는 부분이라며 차이점을 설명하기 시작했습니다.
asyncio는 비동기 I/O에 최적화된 단일 스레드 동시성 라이브러리입니다. concurrent.futures는 스레드 또는 프로세스를 사용한 진정한 병렬 처리 라이브러리입니다.
마치 한 사람이 멀티태스킹하는 것(asyncio)과 여러 사람이 분담하는 것(concurrent.futures)의 차이와 같습니다. 상황에 따라 적절한 도구를 선택해야 합니다.
다음 코드를 살펴봅시다.
import asyncio
import concurrent.futures
import time
# CPU 집약적 작업 (계산)
def cpu_intensive(n):
total = sum(i * i for i in range(n))
return total
# I/O 집약적 작업 (대기)
async def io_intensive(name):
await asyncio.sleep(1)
return f"{name} 완료"
# CPU 작업은 ProcessPoolExecutor 사용
def parallel_cpu():
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(cpu_intensive, 10**6) for _ in range(4)]
results = [f.result() for f in futures]
return results
# I/O 작업은 asyncio 사용
async def parallel_io():
tasks = [io_intensive(f"작업{i}") for i in range(4)]
return await asyncio.gather(*tasks)
# 혼합: asyncio + ThreadPoolExecutor
async def mixed_approach():
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as executor:
result = await loop.run_in_executor(executor, cpu_intensive, 10**6)
return result
박시니어 씨가 화이트보드를 두 부분으로 나눴습니다. 왼쪽에는 "asyncio", 오른쪽에는 "concurrent.futures"라고 적었습니다.
"먼저 asyncio부터 볼게요. asyncio는 단일 스레드에서 동작해요.
하나의 스레드가 여러 작업을 번갈아가며 처리하죠. 마치 한 명의 직원이 여러 전화를 받으면서 통화 대기 중에는 다른 전화를 받는 것과 같아요." asyncio의 장점은 가볍다는 것입니다.
스레드를 여러 개 만들지 않아도 되므로 메모리를 적게 사용합니다. 또한 스레드 간 데이터 공유 문제도 없습니다.
네트워크 요청, 파일 읽기 등 I/O 바운드 작업에 최적입니다. "이제 concurrent.futures를 볼게요.
이 라이브러리는 두 가지 Executor를 제공해요. ThreadPoolExecutor와 ProcessPoolExecutor예요." ThreadPoolExecutor는 여러 스레드를 사용합니다.
파이썬의 GIL(Global Interpreter Lock) 때문에 CPU 작업에서는 진정한 병렬 처리가 어렵지만, I/O 작업에서는 효과적입니다. 기존의 동기 코드를 크게 수정하지 않고도 병렬 처리를 적용할 수 있다는 장점이 있습니다.
ProcessPoolExecutor는 여러 프로세스를 사용합니다. 각 프로세스가 독립적인 파이썬 인터프리터를 가지므로 GIL의 영향을 받지 않습니다.
CPU 바운드 작업, 즉 복잡한 계산이 필요한 작업에 적합합니다. 김개발 씨가 정리했습니다.
"그러니까 I/O 작업이 많으면 asyncio, CPU 계산이 많으면 ProcessPoolExecutor를 쓰면 되는 거네요?" 박시니어 씨가 고개를 끄덕였습니다. "맞아요!
하지만 실제로는 두 가지가 섞여 있는 경우도 많아요. 그럴 때는 run_in_executor를 사용해서 asyncio와 Executor를 결합할 수 있어요." 코드의 mixed_approach 함수를 보면, asyncio 이벤트 루프 안에서 ThreadPoolExecutor를 사용하고 있습니다.
loop.run_in_executor를 통해 동기 함수를 비동기적으로 실행할 수 있습니다. 이렇게 하면 CPU 작업이 진행되는 동안에도 다른 비동기 작업을 처리할 수 있습니다.
실제 프로젝트에서는 상황에 맞게 선택해야 합니다. 웹 크롤링처럼 네트워크 요청이 대부분인 경우 asyncio가 좋습니다.
이미지 처리나 데이터 분석처럼 CPU를 많이 쓰는 경우 ProcessPoolExecutor가 좋습니다. 두 가지가 섞여 있다면 run_in_executor로 조합할 수 있습니다.
김개발 씨는 자신의 상품 가격 수집 프로그램을 떠올렸습니다. 네트워크 요청이 대부분이니 asyncio가 적합해 보였습니다.
"저는 asyncio를 써야겠네요!"
실전 팁
💡 - GIL 때문에 파이썬에서 CPU 작업의 스레드 병렬화는 효과가 제한적입니다
- ProcessPoolExecutor는 프로세스 생성 오버헤드가 있으므로 작은 작업에는 비효율적입니다
- 기존 동기 코드와 통합할 때는 ThreadPoolExecutor가 더 편리할 수 있습니다
6. 주요 활용 사례 분석
이론을 충분히 배운 김개발 씨는 이제 실제로 어떤 상황에서 병렬 처리가 사용되는지 궁금해졌습니다. 박시니어 씨는 회사에서 실제로 병렬 처리를 적용한 세 가지 사례를 공유해주었습니다.
"이 사례들을 보면 언제 병렬 처리를 적용해야 할지 감이 올 거예요."
병렬 처리의 대표적인 활용 사례로는 웹 크롤링, API 병렬 호출, 파일 일괄 처리가 있습니다. 웹 크롤링에서는 여러 페이지를 동시에 수집하고, API 호출에서는 여러 서비스에 동시에 요청을 보내며, 파일 처리에서는 여러 파일을 동시에 변환합니다.
각 사례는 대기 시간이 많은 I/O 작업이라는 공통점이 있습니다.
다음 코드를 살펴봅시다.
import asyncio
import aiohttp
async def fetch_page(session, url):
"""웹 페이지 비동기 수집"""
async with session.get(url) as response:
return await response.text()
async def crawl_websites(urls):
"""여러 웹사이트 동시 크롤링"""
async with aiohttp.ClientSession() as session:
tasks = [fetch_page(session, url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
async def call_multiple_apis(endpoints):
"""여러 API 동시 호출"""
async with aiohttp.ClientSession() as session:
tasks = []
for endpoint in endpoints:
tasks.append(fetch_page(session, endpoint))
responses = await asyncio.gather(*tasks)
return responses
# 사용 예시
urls = ["https://api.example.com/users", "https://api.example.com/products"]
# asyncio.run(crawl_websites(urls))
박시니어 씨가 첫 번째 사례를 꺼냈습니다. "우리 회사에서 경쟁사 가격을 모니터링하는 시스템을 만들었어요.
매일 100개 쇼핑몰의 가격을 수집하는데, 처음에는 순차 처리로 만들었더니 2시간이 걸렸어요." 2시간이면 업무 시간 중 상당 부분을 차지합니다. 게다가 가격은 실시간으로 변하기 때문에 수집이 오래 걸리면 데이터의 정확성도 떨어집니다.
"asyncio와 aiohttp를 도입해서 20개씩 동시에 수집하도록 바꿨어요. 결과는 놀라웠죠.
2시간이 6분으로 줄었어요!" 20배 가까이 빨라진 것입니다. 위 코드의 crawl_websites 함수가 바로 이런 패턴입니다.
aiohttp는 비동기 HTTP 클라이언트 라이브러리입니다. asyncio.gather로 여러 페이지를 동시에 요청하고, 모든 응답이 도착하면 결과를 반환합니다.
두 번째 사례는 마이크로서비스 아키텍처에서의 API 병렬 호출입니다. 박시니어 씨가 설명했습니다.
"요즘은 하나의 기능이 여러 마이크로서비스에 분산되어 있는 경우가 많아요." 예를 들어 상품 상세 페이지를 보여주려면 상품 서비스, 재고 서비스, 리뷰 서비스, 추천 서비스 등 여러 곳에서 데이터를 가져와야 합니다. 순차적으로 호출하면 각 서비스 응답 시간이 누적되어 사용자 경험이 나빠집니다.
"네 개 서비스를 순차 호출하면 평균 800ms가 걸렸어요. 병렬 호출로 바꾸니 200ms로 줄었죠." 사용자 입장에서는 페이지가 훨씬 빠르게 로딩됩니다.
세 번째 사례는 파일 일괄 처리입니다. 회사에서 고객이 업로드한 이미지를 여러 크기로 리사이징하는 기능이 있었습니다.
수백 장의 이미지를 처리해야 할 때, 순차 처리로는 시간이 너무 오래 걸렸습니다. "이 경우는 CPU 작업이 포함되어 있어서 ProcessPoolExecutor를 사용했어요.
여러 프로세스가 각자 다른 이미지를 처리하니 코어 수만큼 빨라졌죠." 김개발 씨가 정리했습니다. "정리하면, 네트워크 요청처럼 기다리는 시간이 많으면 asyncio, CPU 계산이 많으면 ProcessPoolExecutor를 쓰면 되는 거네요?" 박시니어 씨가 엄지를 치켜세웠습니다.
"정확해요! 중요한 건 작업의 특성을 파악하는 거예요.
어떤 부분에서 시간이 걸리는지를 분석하면 어떤 방식을 쓸지 결정할 수 있어요." 코드에서 return_exceptions=True 옵션에 주목하세요. 이 옵션을 사용하면 일부 요청이 실패해도 전체가 멈추지 않습니다.
실패한 요청은 예외 객체로 반환되어 나중에 처리할 수 있습니다.
실전 팁
💡 - aiohttp는 asyncio와 함께 사용하는 비동기 HTTP 라이브러리입니다
- return_exceptions=True로 일부 실패를 허용하면 전체 작업이 중단되지 않습니다
- 동시 요청 수를 적절히 제한해서 서버에 과부하를 주지 마세요
7. 병렬 처리 설계 시 고려사항
김개발 씨는 배운 내용을 바로 적용하려고 했습니다. 하지만 박시니어 씨가 손을 들어 잠시 멈추게 했습니다.
"잠깐, 병렬 처리를 적용하기 전에 알아야 할 게 있어요. 잘못 사용하면 오히려 문제가 생길 수 있거든요." 이어서 주의사항들을 설명하기 시작했습니다.
병렬 처리 설계 시 반드시 고려해야 할 사항들이 있습니다. 동시 요청 수 제한으로 서버 과부하를 방지하고, 에러 처리로 일부 실패에도 전체가 멈추지 않게 하며, 공유 자원 접근에서 경쟁 상태를 피해야 합니다.
또한 디버깅 어려움과 복잡성 증가도 감안해야 합니다.
다음 코드를 살펴봅시다.
import asyncio
import aiohttp
async def fetch_with_limit(session, url, semaphore):
"""동시 요청 수를 제한하는 패턴"""
async with semaphore: # 세마포어로 동시 실행 수 제한
try:
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
return await response.text()
except Exception as e:
return f"Error: {e}" # 에러 시 None 대신 에러 정보 반환
async def safe_parallel_fetch(urls, max_concurrent=10):
"""안전한 병렬 처리 패턴"""
semaphore = asyncio.Semaphore(max_concurrent) # 최대 10개 동시 실행
async with aiohttp.ClientSession() as session:
tasks = [fetch_with_limit(session, url, semaphore) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return results
# 사용 예시
urls = [f"https://api.example.com/item/{i}" for i in range(100)]
# asyncio.run(safe_parallel_fetch(urls, max_concurrent=10))
박시니어 씨가 심각한 표정으로 말했습니다. "예전에 신입 개발자가 병렬 처리를 잘못 적용해서 큰일 났던 적이 있어요.
100개 요청을 동시에 보내서 상대 서버가 우리 IP를 차단해버렸죠." 첫 번째 고려사항은 동시 요청 수 제한입니다. 아무리 빨리 처리하고 싶어도 한계가 있습니다.
상대 서버의 처리 능력, 네트워크 대역폭, 우리 시스템의 메모리 등을 고려해야 합니다. 코드에서 asyncio.Semaphore를 사용한 부분을 주목하세요.
세마포어는 동시에 실행할 수 있는 작업 수를 제한합니다. Semaphore(10)은 최대 10개까지만 동시 실행을 허용합니다.
11번째 작업은 앞선 작업 중 하나가 끝날 때까지 대기합니다. 두 번째는 에러 처리입니다.
100개 요청 중 99개가 성공했는데 1개가 실패했다고 전체가 멈추면 안 됩니다. try-except로 개별 요청의 에러를 처리하고, return_exceptions=True로 실패한 작업도 결과에 포함시켜야 합니다.
세 번째는 타임아웃 설정입니다. 네트워크 요청은 언제든 느려지거나 응답이 없을 수 있습니다.
타임아웃 없이 무한정 기다리면 전체 시스템이 멈출 수 있습니다. aiohttp.ClientTimeout으로 적절한 타임아웃을 설정하세요.
네 번째는 공유 자원 접근 문제입니다. 여러 작업이 동시에 같은 변수나 파일에 접근하면 예상치 못한 결과가 발생할 수 있습니다.
이를 **경쟁 상태(Race Condition)**라고 합니다. 박시니어 씨가 예를 들었습니다.
"만약 100개 작업이 동시에 같은 파일에 결과를 쓰면 어떻게 될까요? 내용이 섞이거나 덮어씌워질 수 있어요.
이럴 때는 Lock을 사용하거나, 결과를 모아서 마지막에 한 번에 쓰는 게 좋아요." 다섯 번째는 디버깅 어려움입니다. 순차 처리는 문제가 생기면 어디서 발생했는지 찾기 쉽습니다.
하지만 병렬 처리는 여러 작업이 동시에 실행되므로 로그가 섞이고, 버그 재현이 어렵습니다. "그래서 처음에는 순차 처리로 개발하고, 완성된 후에 병렬 처리로 전환하는 게 좋아요.
버그가 병렬 처리 때문인지, 원래 로직 문제인지 구분하기 쉽거든요." 마지막으로 복잡성 증가입니다. 병렬 처리 코드는 순차 처리보다 이해하기 어렵습니다.
팀원들이 유지보수할 수 있는 수준으로 작성해야 합니다. 때로는 조금 느리더라도 이해하기 쉬운 코드가 더 나은 선택일 수 있습니다.
김개발 씨는 메모장에 체크리스트를 만들었습니다. 동시 요청 수 제한, 에러 처리, 타임아웃, 공유 자원 주의, 순차 개발 후 병렬 전환.
"이걸 확인하면서 구현해야겠어요!" 박시니어 씨가 마지막으로 덧붙였습니다. "그리고 꼭 성능을 측정해보세요.
병렬 처리를 도입했는데 오히려 느려지는 경우도 있거든요. 오버헤드 때문이에요.
작업 수가 적거나 각 작업이 너무 짧으면 오히려 손해일 수 있어요."
실전 팁
💡 - 동시 요청 수는 10-20개부터 시작해서 서버 상태를 보며 조절하세요
- 모든 네트워크 요청에는 타임아웃을 설정하는 습관을 들이세요
- 병렬 처리 도입 전후의 성능을 반드시 측정하고 비교하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.