https://github.com/kyeongjun-dev/network
MSA 형태의 서비스들에서 best practice가 있다. client의 timeout과 server의 keep-alive 상태 등을 설정하는 좋은 예시들이 있다.
그러다가 문득 '그럼 어떤 상태에서 의도한 에러를 발생시킬 수 있을까?'라는 생각이 들었고, 이를 실천해보기 위해 이 시리즈를 작성하게 됐다.
주 목적은 미래의 내가 보기 위해서다.
시리즈 초반에는 실무에 도움이 될 수 있나 싶겠지만, 실제로 많은 도움이 됐고, 수많은 5xx에러를 없앴다.
server가 소켓을 열어두고, client가 서버로 접속한다. 실제 코드로 간단하게 테스트 해보자. (소스코드 : 레포 01 디렉토리)
먼저 client 코드다. 간단히 로컬호스트(127.0.0.1)의 30000 포트로 접속한다.
import socket
try:
# AF_INET : ipv4 사용, SOCK_STREAM : tcp 사용
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect(('127.0.0.1', 30000))
print("서버에 연결 시도...")
except ConnectionRefusedError:
print("서버에 연결할 수 없습니다. 서버가 이미 종료된 것 같습니다.")
except Exception as e:
print(f"오류가 발생했습니다: {e}")
finally:
client_socket.close()
다음은 server 코드다. 간단히 30000번 포트로 소켓을 열고, client의 연결을 기다린다.
import socket
# 서버 소켓 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('127.0.0.1', 30000))
server_socket.listen()
try:
# accept()는 클라이언트가 연결할 때까지 여기서 실행을 멈춤(blocking)
client_socket, addr = server_socket.accept()
print(f"{addr} 에서 접속했습니다.")
# 연결 성공 후 로직 (여기서는 간단히 연결 종료)
client_socket.close()
except Exception as e:
print(f"오류가 발생했습니다: {e}")
finally:
# 소켓 정리
server_socket.close()
print("서버 소켓을 닫았습니다.")
실제로 server.py, client.py 순서로 다른 터미널 창에서 실행해보면 아래와 같이 단순히 연결에 성공한 뒤, 바로 연결을 종료한다.
python3.13 client.py
서버에 연결 시도...
// 이걸 먼저 실행
python3.13 server.py
('127.0.0.1', 50460) 에서 접속했습니다.
서버 소켓을 닫았습니다.
위의 01번 상황에서는 여러 문제가 있다.
1. server가 기약없이 계속 client를 기다린다.
2. client는 연결에 실패하면 바로 연결을 종료한다.
// server.py를 실행하지 않고, 바로 client.py를 실행했을 때
python3.13 client.py
서버에 연결할 수 없습니다. 서버가 이미 종료된 것 같습니다.
이를 개선해보자.
client와 server에 타임아웃을 설정해보자. (소스코드 : 레포 02 디렉토리)
먼저 server 코드다. timeout_duration에 설정한 10초만큼 대기한다.
import socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('127.0.0.1', 30000))
server_socket.listen()
timeout_duration = 10 # 총 대기 시간 (초)
try:
# 서버 소켓이 클라이언트의 접속을 기다리는 시간을 timeout_duration으로 설정
server_socket.settimeout(timeout_duration)
client_socket, addr = server_socket.accept()
print(f"{addr} 에서 접속했습니다.")
client_socket.close()
except Exception as e:
print(f"오류가 발생했습니다: {e}")
finally:
server_socket.close()
print("서버 소켓을 닫았습니다.")
실제로 02/server.py를 실행한 뒤, 10초동안 아무것도 안하면 아래와 같이 timeout 에러를 출력하고 종료된다.
python3.13 server.py
오류가 발생했습니다: timed out
서버 소켓을 닫았습니다.
다음은 client 코드다. ConnectionRefusedError 에러가 발생하면, while 반복문으로 새롭게 소켓을 생성한 뒤, 1초마다 연결을 재시도한다.
import socket
import time
timeout_duration = 10 # 총 대기 시간 (초)
start_time = time.time() # 시작 시간 기록
# 반복문을 돌면서 소켓을 신규로 생성해서 연결 시도
while True:
try:
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 타임아웃 설정 (연결 과정 자체가 느린 경우를 대비)
client_socket.settimeout(5)
client_socket.connect(('127.0.0.1', 30000))
print("서버에 연결 시도...")
# 연결 성공 시 반복문 탈출
break
except ConnectionRefusedError:
print("서버에 연결할 수 없습니다. 1초 후 재시도 합니다.")
# 재시도 하는 로직 추가
time.sleep(1)
if time.time() - start_time > timeout_duration:
print(f"{timeout_duration}초를 초과해서 종료합니다.")
break
except Exception as e:
print(f"오류가 발생했습니다: {e}")
finally:
client_socket.close()
# 연결 성공 후 로직 (예: client_socket이 닫히지 않은 경우)
if client_socket and client_socket.fileno() != -1:
# 여기서 데이터 통신 로직 수행
try:
# 예시: 데이터 전송
# client_socket.sendall(b'Hello')
pass
finally:
client_socket.close()
02/client.py를 먼저 실행한 후, 02/server.py를 실행하면 아래와 같이 재시도 하다가 연결에 성공하고, 연결을 종료한다.

02/server.py는 10초 동안 소켓을 열어두고 대기한 뒤, 연결이 없으면 종료한다. 서버가 열었던 소켓을 닫고 종료했는데, client가 11초 뒤에 연결을 시도하면 당연히 에러가 발생할 것이다.

위 상황에서 실제로 Wireshark를 이용해 실제 패킷을 분석해본다.