[파이썬 100강] 39강. concurrent.futures로 동시성 코드 단순화하기
concurrent.futures은 문법 몇 줄만 아는 것으로 끝나지 않습니다. 실제 업무에서는 입력 데이터가 매번 다르고, 실패 상황이 반드시 생기며, 코드가 처음 작성될 때보다 유지보수 단계에서 더 많은 비용이 듭니다. 그래서 이번 강의에서는 단순 사용법 나열 대신, 초보가 막히는 지점을 먼저 짚고 안전한 기본 패턴을 충분히 반복해 보겠습니다. 특히 이번 주제에서 자주 나오는 오해는 “예제가 돌아갔으니 실제 운영에서도 문제없다”는 착각입니다. 실제 환경에서는 네트워크 지연, 데이터 누락, 타입 불일치, 파일 경로 차이처럼 작은 변수들이 동시에 겹치므로 방어적인 사고가 중요합니다. 이번 강의 목표는 개념을 이해하고, 즉시 재현 가능한 코드로 감각을 익히고, 실수를 예방하는 체크리스트를 손에 넣는 것입니다.
핵심 개념
- concurrent.futures의 목적은 코드 길이를 줄이는 것이 아니라, 불확실한 입력과 실행 환경에서도 결과를 예측 가능하게 만드는 데 있습니다.
- 표면 문법보다 중요한 것은 데이터 흐름(입력→가공→검증→출력)과 실패 흐름(예외→복구→로그→재시도)을 분리하는 습관입니다.
- 초보 단계에서 구조를 조금만 의식해도, 기능 추가와 디버깅 속도는 몇 배 차이 납니다.
concurrent.futures을 공부할 때 흔히 “어떤 함수 이름을 외워야 하지?”에만 집중하게 됩니다. 그런데 실무에서는 API 이름 자체보다도, 어떤 시점에 어떤 데이터를 신뢰할 수 있는지 판단하는 능력이 더 중요합니다. 예를 들어 테스트 데이터에서는 항상 값이 있던 필드가 운영 데이터에서는 비어 있을 수 있고, 작은 응답 지연이 누적되어 전체 처리 시간이 폭증할 수도 있습니다. 따라서 이번 강의의 핵심은 단일 기능 사용법이 아니라, 동작 조건과 실패 조건을 함께 이해하는 것입니다. 이 관점이 잡히면 새로운 라이브러리를 만나도 빠르게 적응할 수 있습니다.
왜 중요한가
현업에서 concurrent.futures이 중요한 이유는 단순합니다. 데이터 처리, 자동화, 서비스 운영의 대부분이 “외부 입력을 받아서 가공한 뒤 안정적으로 내보내는 작업”으로 이루어지기 때문입니다. 이때 가장 비싼 장애는 화려한 알고리즘 부족이 아니라, 사소한 예외를 놓쳐 전체 배치가 중단되거나 잘못된 결과가 조용히 저장되는 경우입니다. concurrent.futures을 제대로 다루면 코드가 길어지기보다 오히려 의도가 분명해지고, 문제 발생 시 복구 지점이 명확해집니다.
또 하나 중요한 포인트는 협업입니다. 혼자 작성한 스크립트는 당장 돌아가면 만족할 수 있지만, 팀 단위에서는 재현 가능성과 설명 가능성이 필수입니다. 왜 이 분기에서 실패를 무시했는지, 어떤 조건에서 재시도하는지, 어떤 입력을 허용하는지 문서화되어 있어야 운영 부담이 줄어듭니다. 그래서 이번 강의는 “실행된다”를 넘어서 “다른 사람이 읽고 수정해도 안전하다”를 목표로 잡습니다.
기본 사용
예제 1) 가장 기본 패턴
>>> source = {'name': 'python100', 'status': 'active'}
>>> source.get('name'), source.get('status')
('python100', 'active')
>>> source.get('missing', 'N/A')
'N/A'
해설:
- 기본 패턴은 데이터를 꺼낼 때 실패 기본값을 동시에 설계하는 것입니다.
- 키가 없을 가능성을 처음부터 코드에 반영하면 이후 예외 처리가 단순해집니다.
예제 2) 조건/반복/조합 확장
>>> rows = [
... {'id': 1, 'ok': True, 'value': 10},
... {'id': 2, 'ok': False, 'value': 99},
... {'id': 3, 'ok': True, 'value': 30},
... ]
>>> valid = [r['value'] for r in rows if r['ok']]
>>> sum(valid)
40
해설:
- 입력을 바로 쓰지 말고 조건으로 먼저 걸러서 “신뢰 가능한 데이터 집합”을 만듭니다.
- 이런 전처리 단계를 분리하면 문제 발생 지점을 로그로 남기기 쉬워집니다.
예제 3) 실전형 미니 케이스
>>> def parse_limit(raw, default=100):
... try:
... value = int(raw)
... except (TypeError, ValueError):
... return default
... return min(max(value, 1), 1000)
>>> parse_limit('250'), parse_limit('oops'), parse_limit('-3')
(250, 100, 1)
해설:
- 실전에서는 입력이 문자열로 들어오는 경우가 많으므로 변환 실패를 기본 흐름으로 취급해야 합니다.
- 범위 보정(clamp)을 추가하면 예상치 못한 과부하 요청을 줄일 수 있습니다.
자주 하는 실수
실수는 실력 부족의 증거가 아니라, 방어 설계가 아직 코드에 반영되지 않았다는 신호입니다. 아래 두 실수는 거의 모든 초보가 한 번씩 겪는 패턴이므로, 원인과 해결을 묶어서 익혀 두면 디버깅 시간이 크게 줄어듭니다.
실수 1) 정상 입력만 가정하고 바로 인덱싱하기
>>> payload = {'items': []}
>>> payload['items'][0]
Traceback (most recent call last):
... IndexError: list index out of range
원인:
- 테스트 데이터에는 값이 있었지만, 실제 운영 데이터에서는 빈 목록이 들어올 수 있다는 점을 놓쳤습니다.
해결:
>>> payload = {'items': []}
>>> first = payload.get('items', [None])[0] if payload.get('items') else None
>>> first is None
True
실수 2) 예외를 무시하고 진행해서 원인 추적이 어려워짐
- 증상: 중간 단계 실패가 누락되어 마지막 단계에서만 이상 결과가 보입니다.
- 원인:
except: pass처럼 모든 오류를 삼켜 버리면 어떤 입력에서 실패했는지 알 수 없습니다. - 해결: 예상 가능한 예외만 좁게 잡고, 실패 데이터와 사유를 로그로 남긴 뒤 기본값/재시도 정책을 명시합니다.
실무 패턴
실무에서는 기능이 맞는지 못지않게 운영성이 중요합니다. 첫째, 입력 검증 규칙을 함수 경계에서 명확히 하세요. 함수 안쪽 깊은 곳에서 터지는 오류는 복구 비용이 큽니다. 둘째, 로그는 “언제/무엇을/왜”를 남겨야 합니다. 단순히 실패 메시지 한 줄만 찍으면 원인 재현이 어렵습니다. 셋째, 재사용 가능한 전처리 함수(예: 타입 변환, 범위 제한, 필드 검증)를 별도 모듈로 분리하면 팀 내 코드 품질이 일정해집니다. 넷째, 성능은 마지막에만 보는 항목이 아니라 초기부터 데이터 크기 가정을 숫자로 적어두는 습관이 필요합니다.
concurrent.futures 관점에서도 같은 원칙이 적용됩니다. “지금은 작은 데이터니까 괜찮다”는 판단이 누적되면, 트래픽이 늘었을 때 병목이 한꺼번에 터집니다. 따라서 처리 단계를 분리하고, 각 단계의 입력·출력 형태를 문서화하고, 실패 시 대체 경로를 준비하는 것이 장기적으로 가장 빠른 길입니다.
오늘의 결론
한 줄 요약: concurrent.futures은 기능 암기 과목이 아니라, 실패를 예측하고 제어하는 설계 습관입니다.
기억할 것:
- 정상 경로와 실패 경로를 같은 비중으로 코드에 표현해야 합니다.
- 입력 검증과 기본값 전략을 초기에 넣으면 디버깅 시간이 크게 줄어듭니다.
- 예제 한 번 성공에 만족하지 말고, 빈 값/오타/지연 같은 나쁜 입력으로 반드시 재검증하세요.
연습문제
- 문자열로 들어오는 설정값(
"10","-3","abc")을 안전하게 정수로 변환하고 1~100 범위로 보정하는 함수를 작성하세요. - 딕셔너리 리스트에서
active=True인 항목만 골라 특정 필드를 합산하는 코드를 작성하고, 필드 누락 시 기본값 처리까지 포함하세요. - 실패한 입력을 별도 리스트에 수집한 뒤 마지막에 요약 보고(총 건수, 실패 사유별 개수)를 출력하는 미니 루틴을 작성하세요.
이전 강의 정답 (38강)
- 연습문제 1 정답 예시
>>> items = ['api', 'db', 'cache']
>>> [name.upper() for name in items]
['API', 'DB', 'CACHE']
- 연습문제 2 정답 예시
>>> values = [3, 7, 11]
>>> sum(v for v in values if v % 2 == 1)
21
- 연습문제 3 정답 예시
>>> def normalize(text):
... return text.strip().lower().replace(' ', '_')
>>> normalize(' Python 100 Lesson ')
'python_100_lesson'
실습 환경/재현 정보
- 실행 환경:
condaenvpython100(Python 3.11.14) - 가정한 OS: macOS/Linux 공통
- 권장 준비: 가상환경 활성화 후
python -qREPL에서 예제 먼저 재현