메뉴 닫기

파이썬 ThreadPoolExecutor 활용법 submit과 map 그리고 Future 흐름 완벽 이해

파이썬 ThreadPoolExecutor 활용법 submit과 map 그리고 Future 흐름 완벽 이해

🚀 멀티스레딩으로 파이썬 성능을 극대화하는 핵심 비밀을 지금 확인하세요

복잡한 연산이나 대량의 데이터를 처리하다 보면 단일 스레드 방식만으로는 한계가 뚜렷하게 느껴집니다.
특히 웹 크롤링, 대규모 API 호출, 이미지 처리처럼 병렬 실행이 필요한 상황에서는 효율적인 멀티스레딩이 성능을 크게 좌우하죠.
이때 꼭 알아야 할 기능이 바로 ThreadPoolExecutor입니다.
단순히 여러 작업을 동시에 실행하는 것을 넘어, submit과 map을 통해 작업을 효율적으로 분배하고, Future 객체로 결과를 제어할 수 있어 더욱 안정적이고 직관적인 코드 작성이 가능합니다.
조금만 이해하면 업무 자동화나 데이터 처리 속도를 획기적으로 끌어올릴 수 있기 때문에, 파이썬을 중급 이상으로 다루고 싶다면 반드시 알아두어야 하는 핵심 개념이라 할 수 있습니다.

이번 글에서는 ThreadPoolExecutor의 기본 원리부터 submit, map 메서드의 차이, Future 객체를 통한 흐름 관리까지 실제 코드 예제와 함께 하나씩 풀어가겠습니다.
또한 단순히 사용법에 그치지 않고, 어떤 상황에서 어떤 방식이 더 적합한지 비교하며 실무에서 활용할 수 있는 팁까지 정리했으니 끝까지 읽어보시면 큰 도움이 될 것입니다.



🔗 ThreadPoolExecutor란 무엇인가

파이썬에서 멀티스레딩을 구현할 때 가장 많이 활용되는 도구 중 하나가 바로 concurrent.futures 모듈의 ThreadPoolExecutor입니다.
이 클래스는 여러 개의 스레드를 미리 풀(pool) 형태로 생성해 두고, 작업이 요청될 때마다 스레드를 재활용하여 실행 속도를 높여주는 방식으로 동작합니다.
즉, 매번 새로운 스레드를 생성하고 제거하는 오버헤드를 줄이고, 병렬 처리를 효율적으로 수행할 수 있도록 돕습니다.

ThreadPoolExecutor는 단순한 스레드 생성보다 훨씬 직관적이며, 자원 관리가 자동으로 이뤄지기 때문에 복잡한 동기화 코드를 작성할 필요가 줄어듭니다.
또한, Future 객체를 통해 비동기 작업의 결과를 관리할 수 있어, 코드 구조가 깔끔해지고 유지보수가 쉬워지는 장점도 있습니다.

⚡ 기본 동작 원리

ThreadPoolExecutor는 내부적으로 스레드 풀을 관리하며, 개발자는 풀의 크기(max_workers)를 지정할 수 있습니다.
풀에 있는 스레드가 모두 사용 중일 경우, 새로운 작업은 대기열에 저장되며, 스레드가 사용 가능해지면 순차적으로 실행됩니다.
이 구조 덕분에 CPU 자원을 효율적으로 활용할 수 있습니다.

CODE BLOCK
from concurrent.futures import ThreadPoolExecutor
import time

def task(name):
    print(f"작업 시작: {name}")
    time.sleep(2)
    print(f"작업 종료: {name}")
    return f"{name} 완료"

with ThreadPoolExecutor(max_workers=3) as executor:
    futures = [executor.submit(task, i) for i in range(5)]

위 예제에서는 3개의 스레드 풀을 만들고, 총 5개의 작업을 실행합니다.
동시에 3개까지 실행되며, 나머지는 대기 상태에 있다가 스레드가 비워지면 차례로 실행됩니다.

💡 TIP: ThreadPoolExecutor는 I/O 작업이 많은 경우에 특히 효과적입니다. CPU 연산이 많은 경우에는 ProcessPoolExecutor를 고려하는 것이 더 적합합니다.

🛠️ submit 메서드와 Future 객체

ThreadPoolExecutor에서 가장 많이 사용되는 기능 중 하나가 submit() 메서드입니다.
이 메서드를 사용하면 특정 함수를 비동기적으로 실행할 수 있으며, 그 결과는 Future 객체로 반환됩니다.
Future는 실행 중인 작업을 추적하고, 작업이 끝났을 때 결과를 받아오거나 예외를 처리할 수 있는 일종의 핸들 역할을 합니다.

🔍 submit 메서드의 특징

submit()은 단일 함수를 실행할 때 사용되며, 인자를 함께 전달할 수 있습니다.
호출 즉시 Future 객체를 반환하기 때문에, 작업이 끝나지 않아도 코드의 나머지 부분을 계속 실행할 수 있습니다.

CODE BLOCK
from concurrent.futures import ThreadPoolExecutor

def square(n):
    return n * n

with ThreadPoolExecutor(max_workers=2) as executor:
    future = executor.submit(square, 5)
    print(f"Future 상태: {future.done()}")
    result = future.result()
    print(f"결과: {result}")

위 예제에서 future.done()은 작업이 끝났는지 확인하는 메서드이며, future.result()는 실제 결과 값을 가져옵니다.
만약 작업이 완료되지 않았다면 result()는 완료될 때까지 대기하게 됩니다.

⚡ Future 객체의 주요 메서드

메서드 설명
done() 작업이 완료되었는지 여부 반환
result() 작업 결과 반환, 미완료 시 대기
exception() 작업 실행 중 발생한 예외 확인
cancel() 작업이 시작되지 않았다면 취소 가능

⚠️ 주의: Future.result()를 무분별하게 호출하면 프로그램이 블로킹될 수 있습니다. 반드시 적절한 시점에 사용해야 합니다.



⚙️ map 메서드로 여러 작업 처리하기

ThreadPoolExecutor에서 map() 메서드는 반복 가능한(iterable) 데이터를 받아, 각 요소를 병렬로 처리할 때 유용합니다.
여러 개의 작업을 동시에 실행하고 그 결과를 순서대로 반환해 주기 때문에, 반복문과 비슷하면서도 더 효율적인 처리를 가능하게 합니다.

특히 submit()과 달리 map()은 Future 객체를 직접 다루지 않고, 실행 결과만을 반환한다는 차이가 있습니다.
이 덕분에 단순 반복 처리에는 더 간단하고 직관적인 코드를 작성할 수 있습니다.

🌀 map 메서드 예제

CODE BLOCK
from concurrent.futures import ThreadPoolExecutor

def square(n):
    return n * n

with ThreadPoolExecutor(max_workers=3) as executor:
    results = executor.map(square, [1, 2, 3, 4, 5])
    for r in results:
        print(r)

위 코드에서는 [1, 2, 3, 4, 5] 리스트의 요소들이 각각 square 함수로 전달되어 동시에 실행됩니다.
결과는 원본 순서대로 반환되므로, 순차적인 출력이 보장됩니다.

📊 submit vs map 차이

특징 submit() map()
결과 반환 Future 객체 반환 실제 결과 반환
실행 제어 작업 상태 확인 및 예외 처리 가능 상태 제어 불가, 단순 반복 처리 적합
코드 복잡성 비교적 복잡 간단하고 직관적

💎 핵심 포인트:
반복적인 작업을 간단하게 처리할 때는 map(), 세밀한 제어가 필요할 때는 submit()을 활용하는 것이 좋습니다.

🔌 Future 흐름 제어와 예외 처리

ThreadPoolExecutor에서 실행된 작업은 Future 객체를 통해 제어할 수 있습니다.
이 객체는 단순히 결과를 가져오는 것을 넘어, 진행 상태를 추적하거나 예외 발생 시 처리할 수 있는 중요한 도구입니다.
비동기 작업이 많은 상황에서는 Future 흐름 제어를 잘 이해하는 것이 필수적입니다.

🕹️ Future 흐름 제어

Future는 작업이 완료되었는지를 확인하거나, 작업을 취소하는 기능을 제공합니다.
예를 들어 done() 메서드로 완료 여부를 확인할 수 있고, cancel() 메서드로 아직 실행되지 않은 작업을 취소할 수도 있습니다.

CODE BLOCK
from concurrent.futures import ThreadPoolExecutor
import time

def risky_task(n):
    if n == 3:
        raise ValueError("에러 발생!")
    time.sleep(1)
    return n * 10

with ThreadPoolExecutor(max_workers=2) as executor:
    futures = [executor.submit(risky_task, i) for i in range(5)]
    for f in futures:
        try:
            print(f.result())
        except Exception as e:
            print(f"예외 발생: {e}")

위 예제에서는 특정 작업에서 오류가 발생하면, Future.result() 호출 시 예외가 그대로 전파됩니다.
따라서 반드시 try-except로 감싸서 예외를 적절히 처리해야 안전한 프로그램을 만들 수 있습니다.

🚦 예외 처리 시 주의사항

  • ⚠️Future.result()는 예외를 그대로 전달하므로, try-except 구문이 필수입니다.
  • 🛠️exception() 메서드를 활용하면 예외 여부만 미리 확인할 수 있습니다.
  • 🔌cancel()은 작업이 실행되기 전에만 유효합니다.

💬 Future 객체의 흐름 제어는 안정적인 멀티스레딩 프로그램을 만들기 위한 핵심 요소입니다. 특히 예외 처리 로직을 놓치면 프로그램 전체가 중단될 수 있으므로 각별한 주의가 필요합니다.



💡 submit과 map 선택 기준과 활용 팁

ThreadPoolExecutor를 사용할 때 가장 많이 고민되는 부분 중 하나가 바로 submit()map() 중 무엇을 선택해야 하는가입니다.
두 메서드는 모두 비동기적으로 작업을 실행하지만, 성격과 용도가 다르기 때문에 상황에 맞는 선택이 중요합니다.

📝 선택 기준

상황 추천 메서드 이유
반복적인 데이터 처리 map() 코드가 간단하고 결과를 순서대로 반환
작업별 상태 확인 필요 submit() Future 객체를 통해 세밀한 제어 가능
예외 처리 필요 submit() 작업별 try-except 구문 작성 가능
단순 병렬 실행 map() 빠르고 직관적인 처리 방식

🔑 실무 활용 팁

  • 🚀네트워크 요청이나 파일 다운로드처럼 많은 수의 작업이 동시에 실행될 경우 map()이 효율적입니다.
  • ⚙️작업별 결과를 개별적으로 확인하고 싶다면 submit()을 활용하세요.
  • 🧩복잡한 프로그램에서는 map()과 submit()을 혼합하여 사용하면 더 유연한 구조를 만들 수 있습니다.

💎 핵심 포인트:
map()은 단순 반복에, submit()은 세밀한 제어에 강점을 가지므로 두 가지를 적절히 조합하면 훨씬 더 강력한 멀티스레딩 코드를 작성할 수 있습니다.

자주 묻는 질문 (FAQ)

ThreadPoolExecutor는 CPU 연산에도 유리한가요?
CPU 연산이 많은 경우에는 GIL(Global Interpreter Lock) 때문에 ThreadPoolExecutor보다는 ProcessPoolExecutor를 사용하는 것이 더 효과적입니다.
submit과 map을 동시에 사용할 수 있나요?
네, 가능합니다. 단순 반복 작업에는 map을 쓰고, 개별 상태 추적이 필요한 작업에는 submit을 섞어 사용하면 더 유연한 코드를 작성할 수 있습니다.
Future 객체 없이 결과만 얻고 싶을 때는 어떻게 하나요?
Future를 직접 다루고 싶지 않다면 map() 메서드를 사용하는 것이 좋습니다. 결과가 순서대로 반환되므로 간단한 반복 처리에 적합합니다.
Future.result()가 블로킹되는 문제를 피하려면 어떻게 해야 하나요?
as_completed() 같은 헬퍼 함수를 활용하면 완료된 작업부터 차례로 결과를 가져올 수 있어 불필요한 블로킹을 줄일 수 있습니다.
ThreadPoolExecutor에서 예외가 발생하면 프로그램이 중단되나요?
예외는 Future.result()를 호출할 때 전달됩니다. 따라서 반드시 try-except로 감싸야 안전하며, 그렇지 않으면 프로그램이 중단될 수 있습니다.
map()을 사용할 때 예외가 발생하면 어떻게 되나요?
map() 실행 중 예외가 발생하면 해당 위치에서 예외가 즉시 전파되며, 이후 결과를 가져오는 과정도 중단됩니다.
max_workers 값을 어떻게 정하는 게 좋을까요?
I/O 바운드 작업은 상대적으로 많은 스레드 수를 지정해도 되지만, CPU 바운드 작업에서는 코어 수에 맞추는 것이 효율적입니다.
비동기(asyncio)와 ThreadPoolExecutor는 어떻게 다른가요?
asyncio는 단일 스레드 내에서 이벤트 루프를 기반으로 동작하고, ThreadPoolExecutor는 여러 스레드를 활용하는 병렬 실행 모델입니다. 상황에 따라 적절히 선택해야 합니다.

📌 ThreadPoolExecutor 활용 정리

파이썬의 ThreadPoolExecutor는 멀티스레딩을 단순화하고 효율성을 높여주는 강력한 도구입니다.
submit()과 map() 메서드는 각각 다른 장점을 가지고 있으며, Future 객체를 통해 비동기 작업을 안전하게 제어할 수 있습니다.
특히 I/O 중심의 작업에서는 실행 속도를 크게 개선할 수 있고, 예외 처리까지 유연하게 다룰 수 있어 실무 환경에서도 매우 많이 사용됩니다.

submit()은 세밀한 제어와 상태 추적에, map()은 반복 처리와 간단한 실행에 적합하다는 점을 기억해 두시면 상황에 맞는 선택이 가능해집니다.
또한 Future의 흐름 제어와 예외 처리 방법을 숙지하면 안정적인 멀티스레딩 코드를 작성할 수 있습니다.
멀티스레딩은 복잡해 보일 수 있지만, ThreadPoolExecutor를 활용하면 훨씬 더 직관적이고 깔끔한 코드를 완성할 수 있습니다.


🏷️ 관련 태그 : 파이썬스레딩, ThreadPoolExecutor, 파이썬멀티스레딩, submit메서드, map메서드, Future객체, 비동기프로그래밍, 파이썬중급, 멀티스레드활용, concurrentfutures