안녕하세요! 구글 SEO와 사용자 경험에 정통한 기술 블로그 에디터입니다. 오늘은 파이썬(Python)의 http.server와 ThreadingMixIn을 사용하여 멀티스레드 웹 서버를 구축할 때 흔히 겪을 수 있는 오해와 그 해결 방법에 대해 다루겠습니다. 서버는 분명 멀티스레드로 동작하는데, 왜 요청이 순차적으로 처리되는 것처럼 보일까요? 이 글을 통해 그 원인을 명확히 이해하고, 실제 병렬 처리를 경험하는 방법을 알아보겠습니다.
1. 에러 발생 상황
개발자가 파이썬의 http.server 모듈과 socketserver.ThreadingMixIn을 결합하여 멀티스레드 웹 서버를 구현했습니다. 각 요청이 별도의 스레드에서 처리되어 동시에 여러 클라이언트 요청을 처리할 수 있을 것으로 기대했죠. 서버 코드에 time.sleep(2)를 추가하여 각 요청이 2초간의 작업을 수행하는 것을 시뮬레이션했습니다.
from http.server import HTTPServer, BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
import threading
import time
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
time.sleep(2) # <--- Simulate some work.
self.wfile.write(b'Hello world\t' +
threading.current_thread().name.encode() + b'\t' +
str(threading.active_count()).encode() + b'\n')
class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
pass
def run():
server = ThreadingSimpleServer(('myhost', 4444), Handler)
server.serve_forever()
if __name__ == '__main__':
run()
이 서버를 실행한 후, 아래와 같은 curl 명령어를 사용하여 100개의 요청을 보냈습니다.
for i in $(seq 1 100); do curl http://myhost:4444/$i --connect-timeout 3 --insecure; done
서버 출력 결과는 각 요청마다 다른 스레드 번호(Thread-1, Thread-2 등)를 보여주었지만, 실제 요청 처리 시간은 여전히 순차적이었습니다. 즉, 각 요청이 이전 요청이 2초간의 작업을 마칠 때까지 기다린 후에 처리되는 것처럼 보였습니다. 개발자는 모든 요청이 지연 없이 즉시 병렬로 처리되기를 기대했지만, 결과는 그렇지 않았습니다.
2. 명확한 발생 원인
이 문제의 핵심은 서버 코드가 올바르게 멀티스레드로 동작하고 있다는 것입니다. ThreadingMixIn 클래스는 각 클라이언트 요청을 받을 때마다 새로운 스레드를 생성하여 요청을 처리하도록 설계되어 있습니다. 서버 출력에 Thread-1 (process_request_thread), Thread-2 (process_request_thread)와 같이 다른 스레드 이름이 보이는 것이 바로 그 증거입니다.
그렇다면 왜 요청이 순차적으로 처리되는 것처럼 보였을까요? 문제는 서버가 아니라 클라이언트, 즉 curl 명령어를 실행한 방식에 있었습니다.
for i in $(seq 1 100); do curl ...; done이 쉘(shell) 루프는curl명령어를 순차적으로 실행합니다.- 각
curl명령은 이전curl명령이 완전히 종료된 후에야 실행됩니다. - 즉, 클라이언트는 첫 번째 요청을 보낸 후 2초를 기다려 응답을 받고, 그 다음에 두 번째 요청을 보내는 식으로 작동한 것입니다.
서버는 새로운 요청이 들어올 때마다 새 스레드를 생성할 준비가 되어 있지만, 클라이언트가 요청을 하나씩, 그것도 이전 요청의 응답을 기다린 후에 보냈기 때문에 서버 입장에서는 마치 순차적인 요청처럼 보였던 것입니다. time.sleep(2)는 각 스레드를 2초간 블로킹하지만, 만약 클라이언트가 동시에 여러 요청을 보냈다면, 서버는 그 요청들을 각각 다른 스레드에 할당하여 병렬로 처리했을 것입니다.
3. 해결 방법 및 코드 예시
이 문제를 해결하는 방법은 서버 코드를 수정하는 것이 아니라, 클라이언트에서 요청을 병렬로 보내는 것입니다. 클라이언트가 동시에 여러 요청을 보내야만 서버의 멀티스레드 기능이 제대로 활성화되는 것을 확인할 수 있습니다.
3.1. 쉘 스크립트에서 병렬 요청 보내기 (curl 사용)
curl 명령어를 백그라운드 프로세스로 실행하여 동시에 여러 요청을 보낼 수 있습니다. & 기호를 사용하여 명령어를 백그라운드로 보냅니다.
for i in $(seq 1 10); do curl http://myhost:4444/$i --connect-timeout 3 --insecure & done
wait # 모든 백그라운드 프로세스가 종료될 때까지 기다림
위 스크립트를 실행하면, for 루프가 10개의 curl 명령을 거의 동시에 백그라운드에서 실행합니다. 이때 서버 로그를 보면 여러 Thread-X가 동시에 활성화되어 요청을 처리하고, 전체 처리 시간이 2초에 가까워지는 것을 확인할 수 있습니다 (오버헤드 제외).
3.2. 파이썬 클라이언트로 병렬 요청 보내기 (requests 및 concurrent.futures 사용)
보다 정교하게 병렬 요청을 테스트하려면 파이썬의 requests 라이브러리와 concurrent.futures 모듈을 사용하는 것이 좋습니다. ThreadPoolExecutor를 사용하면 여러 스레드에서 동시에 HTTP 요청을 보낼 수 있습니다.
import concurrent.futures
import requests
import time
def send_request(url):
try:
start_time = time.time()
response = requests.get(url, timeout=5) # 타임아웃 넉넉히 설정
end_time = time.time()
print(f"[{start_time:.2f}s] {url} 요청 시작. 응답: {response.text.strip()} ({end_time - start_time:.2f}s 소요)")
return response.text
except requests.exceptions.RequestException as e:
print(f"Error requesting {url}: {e}")
return None
if __name__ == '__main__':
server_url = "http://myhost:4444"
num_requests = 10 # 동시에 보낼 요청 수
urls = [f"{server_url}/{i}" for i in range(num_requests)]
print(f"{num_requests}개의 요청을 병렬로 보내는 중...")
overall_start_time = time.time()
# 최대 5개의 워커(스레드)로 동시에 요청을 보냅니다.
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(send_request, url): url for url in urls}
# 모든 Future 객체가 완료될 때까지 기다리고 결과를 처리합니다.
for future in concurrent.futures.as_completed(futures):
url = futures[future]
try:
data = future.result()
# print(f"[{time.time():.2f}] {url} 처리 완료.")
except Exception as exc:
print(f"[{time.time():.2f}] {url} 요청 중 예외 발생: {exc}")
overall_end_time = time.time()
print(f"\n총 {num_requests}개의 요청이 {overall_end_time - overall_start_time:.2f}초 만에 완료되었습니다.")
이 파이썬 스크립트를 실행하면, 설정된 max_workers 수만큼의 요청이 동시에 서버로 전달되어 병렬로 처리되는 것을 확인할 수 있습니다. 총 실행 시간은 서버에서 가장 긴 응답 시간(여기서는 2초)과 네트워크 오버헤드, 그리고 max_workers 수에 따라 결정됩니다.
4. 향후 예방을 위한 팁
멀티스레딩 또는 병렬 처리와 관련된 문제를 디버깅하고 테스트할 때는 다음과 같은 점들을 고려하는 것이 중요합니다.
- 클라이언트와 서버의 역할 분리: 문제가 서버의 로직에 있는지, 아니면 클라이언트가 요청을 보내는 방식에 있는지 항상 명확히 구분하여 생각해야 합니다. 이 경우처럼 클라이언트가 순차적으로 요청을 보내면서 서버의 병렬 처리가 작동하지 않는다고 오해하는 경우가 많습니다.
- 실제 병렬 부하 테스트 도구 활용:
ApacheBench (ab),JMeter,locust또는 위에서 제시된 파이썬 스크립트와 같이, 여러 요청을 동시에 보낼 수 있는 전문적인 부하 테스트 도구를 사용하는 것이 좋습니다. - 로그 및 모니터링 강화: 서버 측에서 각 요청이 들어오고 처리되는 시점, 스레드 ID 등을 상세하게 로깅하여 실제 동작 방식을 추적하는 습관을 들이세요. 이는 문제 발생 시 디버깅에 큰 도움이 됩니다.
- 파이썬 GIL (Global Interpreter Lock) 이해: 파이썬의 스레딩은 I/O 바운드(네트워크 통신, 파일 I/O 등) 작업에는 효과적이지만, CPU 바운드 작업(복잡한 계산)에서는 GIL 때문에 진정한 병렬 처리가 어렵습니다. CPU 바운드 작업을 병렬화해야 한다면
multiprocessing모듈을 고려해야 합니다. 하지만 웹 서버처럼 I/O 대기가 많은 경우threading은 여전히 좋은 선택입니다.
이 글이 파이썬 멀티스레드 웹 서버의 동작 방식을 이해하고, 병렬 처리 관련 오해를 해소하는 데 도움이 되었기를 바랍니다. 성공적인 개발을 응원합니다!
![[에러 해결] [Selenium] Reddit 봇 탐지(401 Unauthorized) 우회 및 로그인 실패 원인과 해결 방법 [에러 해결] [Selenium] Reddit 봇 탐지(401 Unauthorized) 우회 및 로그인 실패 원인과 해결 방법](https://dev-error.com/wp-content/plugins/contextual-related-posts/default.png)