- 공유 링크 만들기
- X
- 이메일
- 기타 앱
WebSocket 연결이 끊기는 이유와 운영 환경에서의 영향
업비트와 바이낸스 같은 거래소 API를 사용한 자동매매 시스템에서 WebSocket Connection Closed 에러는 예고 없이 발생합니다. 네트워크 불안정, 거래소 서버 점검, 또는 장시간 유휴 상태로 인한 타임아웃이 주요 원인입니다. 문제는 연결이 끊긴 순간부터 실시간 시세 데이터를 받지 못해 매매 로직이 멈추거나 잘못된 판단을 내릴 수 있다는 점입니다.
운영 환경에서는 자동 재연결(Auto Reconnect) 로직이 필수입니다. 단순히 연결을 맺는 것을 넘어, 비정상 종료를 감지하고 재시도 간격을 조절하며, 재연결 실패 시 알림을 보내는 전체 흐름을 asyncio 기반으로 구현해야 합니다. 이 글에서는 Python asyncio와 websockets 라이브러리를 활용한 실전 구현 방법과 예외 처리 전략을 다룹니다.
| Python자동매매시스템의WebSocket안정성확보재연결과예외처리실전가이드 |
WebSocket 비정상 종료 감지 및 asyncio 예외 처리 전략
WebSocket 연결이 끊기면 websockets.exceptions.ConnectionClosed 예외가 발생합니다. 이를 감지하고 재연결하려면 asyncio의 예외 처리 메커니즘을 정확히 이해해야 합니다.
- 기본 동작: asyncio.gather()로 여러 WebSocket 연결을 동시에 관리할 때, 하나의 코루틴에서 예외가 발생하면 기본적으로 나머지 태스크가 즉시 취소됩니다. 이는 업비트와 바이낸스를 동시에 연결한 상황에서 하나가 끊기면 다른 정상 연결까지 끊길 수 있다는 의미입니다.
- 해결책 1 - return_exceptions=True: gather에 이 옵션을 추가하면 예외를 결과 리스트로 받아 개별 처리할 수 있습니다. 각 거래소 연결 상태를 독립적으로 관리할 수 있어 운영 환경에서 권장합니다.
- 해결책 2 - 개별 try/except: 각 WebSocket 수신 코루틴 내부에서 ConnectionClosed를 잡아 재연결 로직을 즉시 실행하는 방식입니다. 코드가 분산되지만 응답 속도가 빠릅니다.
실무에서는 두 가지 방식을 혼합합니다. 개별 코루틴에서 ConnectionClosed를 잡아 로그를 남기고, 바깥 gather에서 return_exceptions=True로 전체 상태를 모니터링하는 구조입니다.
예시 코드 구조:
async def connect_upbit():
while True:
try:
async with websockets.connect(UPBIT_URL) as ws:
async for msg in ws:
process_data(msg)
except websockets.ConnectionClosed:
logger.warning("업비트 연결 종료, 5초 후 재연결")
await asyncio.sleep(5)
except Exception as e:
logger.error(f"업비트 예외: {e}")
await asyncio.sleep(10)
async def connect_binance():
# 바이낸스도 동일한 구조
async def main():
results = await asyncio.gather(
connect_upbit(),
connect_binance(),
return_exceptions=True
)
for r in results:
if isinstance(r, Exception):
logger.critical(f"치명적 오류: {r}")
재연결 로직 구현 시 핵심 고려사항
단순히 while True로 재연결을 반복하면 거래소 서버에 부담을 주거나 IP 차단을 당할 수 있습니다. 다음 요소들을 반드시 구현해야 합니다.
- Exponential Backoff: 재연결 실패 시 대기 시간을 지수적으로 증가시킵니다(5초 → 10초 → 20초 → 최대 60초). 네트워크 장애나 서버 점검 시 무한 재시도로 인한 리소스 낭비를 방지합니다.
- 최대 재시도 횟수: 연속 실패 횟수를 카운트하여 일정 횟수(예: 10회) 초과 시 알림을 보내고 프로세스를 종료하거나 대기 상태로 전환합니다. 설정 오류나 API 키 만료 같은 복구 불가능한 상황을 조기에 감지할 수 있습니다.
- Heartbeat/Ping 체크: 거래소 대부분은 일정 시간 동안 메시지가 없으면 연결을 끊습니다. websockets 라이브러리의 ping_interval, ping_timeout 파라미터를 설정하거나, 수동으로 ping 프레임을 보내 연결을 유지합니다.
- 연결 상태 모니터링: 마지막 수신 시각을 기록하고, 일정 시간(예: 60초) 동안 데이터가 없으면 연결이 끊긴 것으로 간주하여 능동적으로 재연결을 시작합니다. asyncio.wait_for()로 타임아웃을 걸어 구현할 수 있습니다.
주의사항: task.cancel()로 기존 연결을 강제 종료할 때 CancelledError는 즉시 발생하지 않습니다. 해당 태스크가 다음 await 지점에 도달했을 때 예외가 발생하므로, cancel() 호출 후 반드시 try/except asyncio.CancelledError로 감싸서 await해야 리소스 누수를 방지할 수 있습니다.
디버그 모드 활성화와 실전 트러블슈팅
개발 단계에서 asyncio.run(main(), debug=True)를 사용하면 다음과 같은 진단 정보를 얻을 수 있습니다.
- 'coroutine was never awaited' 경고: WebSocket 연결 함수를 호출만 하고 await하지 않았을 때 발생합니다. 이 경고가 뜨면 해당 코루틴이 실행조차 되지 않은 상태이므로 즉시 수정해야 합니다.
- 오래 걸리는 콜백 감지: 이벤트 루프를 블로킹하는 동기 함수 호출(예: requests.get, time.sleep)을 찾아냅니다. 이런 함수들은 asyncio 환경에서 전체 시스템을 멈추게 하므로 비동기 버전(aiohttp, asyncio.sleep)으로 교체해야 합니다.
- tracemalloc 연동: 코드 상단에
import tracemalloc; tracemalloc.start()를 추가하면 객체 할당 추적 정보가 함께 출력되어 메모리 누수 원인을 파악할 수 있습니다.
운영 환경에서는 디버그 모드 대신 구조화된 로깅(structlog, loguru)을 사용합니다. 연결 시도, 성공, 실패, 재연결 간격 등을 타임스탬프와 함께 기록하여 장애 발생 시 시계열 분석이 가능하도록 합니다. 특히 재연결 성공률과 평균 복구 시간(MTTR) 지표를 모니터링하면 시스템 안정성을 정량적으로 평가할 수 있습니다.
실전 코드 템플릿 및 배포 전 체크리스트
아래는 업비트/바이낸스 WebSocket 재연결 로직의 프로덕션 레벨 템플릿입니다.
import asyncio
import websockets
import logging
from typing import Optional
logger = logging.getLogger(__name__)
class WebSocketClient:
def __init__(self, url: str, name: str):
self.url = url
self.name = name
self.ws: Optional[websockets.WebSocketClientProtocol] = None
self.retry_count = 0
self.max_retries = 10
async def connect(self):
backoff = 5
while self.retry_count < self.max_retries:
try:
self.ws = await websockets.connect(
self.url,
ping_interval=20,
ping_timeout=10
)
logger.info(f"{self.name} 연결 성공")
self.retry_count = 0 # 성공 시 카운터 초기화
await self.receive_messages()
except websockets.ConnectionClosed as e:
logger.warning(f"{self.name} 연결 종료: {e.code} {e.reason}")
self.retry_count += 1
await asyncio.sleep(min(backoff * (2 ** self.retry_count), 60))
except Exception as e:
logger.error(f"{self.name} 예외: {e}", exc_info=True)
self.retry_count += 1
await asyncio.sleep(10)
logger.critical(f"{self.name} 최대 재시도 횟수 초과")
async def receive_messages(self):
async for message in self.ws:
try:
# 메시지 처리 로직
await self.process(message)
except Exception as e:
logger.error(f"메시지 처리 오류: {e}")
async def process(self, message):
# 실제 데이터 처리
pass
async def main():
upbit = WebSocketClient("wss://api.upbit.com/websocket/v1", "업비트")
binance = WebSocketClient("wss://stream.binance.com:9443/ws", "바이낸스")
await asyncio.gather(
upbit.connect(),
binance.connect(),
return_exceptions=True
)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main(), debug=False)
배포 전 체크리스트:
- ping_interval과 ping_timeout이 거래소별 권장값으로 설정되어 있는가?
- 재연결 실패 시 Slack/Discord 알림이 발송되는가?
- 로그 파일이 로테이션되어 디스크 풀을 방지하는가?
- systemd나 supervisor로 프로세스 자동 재시작이 설정되어 있는가?
- 연결 상태를 모니터링하는 헬스체크 엔드포인트가 있는가?
핵심 요약 및 Action Item
WebSocket 자동 재연결 구현의 핵심은 예외를 결과로 수집하는 gather 패턴, 지수 백오프를 적용한 재시도 로직, 능동적인 연결 상태 모니터링 세 가지입니다. asyncio의 CancelledError는 await 시점에 발생한다는 점을 기억하고, 디버그 모드와 tracemalloc을 활용하여 개발 단계에서 잠재적 문제를 사전에 제거해야 합니다.
당장 실행할 작업: 위 템플릿 코드를 기반으로 현재 운영 중인 WebSocket 클라이언트에 재연결 카운터와 알림 로직을 추가하십시오. 최대 재시도 횟수 초과 시 관리자에게 알림을 보내도록 설정하면, 새벽 시간대 장애 발생 시에도 즉시 대응할 수 있습니다.
# 함께 보면 좋은 글
댓글
댓글 쓰기