7강. 반복 송수신과 종료 처리

0. 학습 목표
→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.
0.1 이번 글에서 다룰 내용
이번 글은 구현 중심 강의입니다.
6강에서는 메시지를 한 번 보내고 응답을 한 번 받은 뒤 프로그램이 종료되었습니다.
이번 강의에서는 사용자가 여러 번 메시지를 보낼 수 있고, 종료를 원할 때까지 연결을 유지하는 구조를 만듭니다.
클라이언트가 메시지를 입력한다
↓
서버로 메시지를 보낸다
↓
서버가 메시지를 받고 응답을 보낸다
↓
클라이언트가 응답을 출력한다
↓
이 과정을 반복한다 ← while True
↓
사용자가 exit을 입력하면 종료한다
핵심은 while 반복문과 종료 조건입니다. 반복문은 연결을 유지하기 위해, 종료 조건은 프로그램이 언제 멈춰야 하는지 알려 주기 위해 필요합니다. 이번 강의에서 만드는 반복 송수신 구조는 최종 채팅 프로젝트의 모든 메시지 처리 흐름의 기반입니다. GUI 채팅창에서 메시지를 주고받는 동작도 내부적으로는 이 while문 구조를 바탕으로 합니다.
| 구분 | 내용 |
| 이해할 것 | while 반복문으로 연결 상태를 유지하는 방법과 종료 조건의 필요성 |
| 만들 것 | 여러 번 메시지를 주고받고 exit으로 종료되는 server.py, client.py |
| 확인할 것 | 여러 메시지가 반복 송수신되고, exit 입력 시 서버와 클라이언트가 정상 종료되는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 6강에서 만든 두 파일을 모두 수정합니다.
chat_server/
└── server.py ← 이번 강의에서 수정
chat_client/
└── client.py ← 이번 강의에서 수정
| 파일 | 이번 강의 이후 역할 |
chat_server/server.py |
클라이언트가 보낸 메시지를 반복해서 받고, 응답을 보낸다 |
chat_client/client.py |
사용자가 입력한 메시지를 반복해서 서버로 보내고, 서버 응답을 출력한다 |
1. 실습 준비하기
→ 6강 코드의 한계를 확인하고 종료 규칙을 정합니다.
1.1 이전 6강 코드의 한계 확인하기
6강의 코드는 메시지를 한 번만 주고받고 종료되었습니다. 이 구조에는 다음과 같은 한계가 있습니다.
| 한계 | 이유 |
| 메시지를 한 번만 보낼 수 있다 | 송수신 코드가 반복문 안에 있지 않다 |
| 사용자가 직접 메시지를 입력하지 않는다 | 메시지가 코드 안에 고정되어 있다 |
| 종료 기준이 명확하지 않다 | 연결을 언제 끝낼지 정하는 조건이 없다 |
이번 강의에서는 이 한계를 해결하기 위해 while True문을 사용합니다.
1.2 종료 규칙 정하기
반복문에는 반드시 끝나는 조건이 있어야 합니다. 이번 강의에서는 사용자가 exit을 입력하면 다음 순서로 종료합니다.
exit 입력
↓
exit 메시지를 서버에 전송
↓
클라이언트 반복문 종료
↓
서버도 exit 메시지를 받고 종료
가장 중요한 점은 클라이언트가 exit을 서버에 먼저 보내고 나서 종료해야 한다는 것입니다. 만약 클라이언트가 exit을 서버에 보내지 않고 바로 종료하면, 서버는 왜 연결이 끝났는지 알기 어렵습니다.
✔ 확인 기준: exit을 먼저 서버에 보내고 그다음 클라이언트가 종료해야 한다고 설명할 수 있으면 완료입니다. send() 전에 break가 실행되도록 작성하려고 하지 않았는지 확인하세요.
2. 서버 - 반복 구조와 종료 조건 만들기
→ 서버와 클라이언트의 송수신 코드를 while True 안으로 옮기고 종료 조건을 추가합니다.
chat_server/server.py를 다음과 같이 수정합니다. accept() 이후 수신 코드를 while True 안으로 옮기고 종료 조건을 추가합니다.
import socket
HOST = "127.0.0.1"
PORT = 5000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((HOST, PORT))
server_socket.listen()
print("서버를 시작합니다.")
print("클라이언트 접속 대기 중...")
client_socket, client_address = server_socket.accept()
with client_socket:
print(f"클라이언트가 접속했습니다: {client_address}")
while True:
data = client_socket.recv(1024)
# 클라이언트 연결이 갑자기 끊겼을 때 종료하는 안전장치
if not data:
print("클라이언트 연결이 종료되었습니다.")
break
message = data.decode("utf-8")
print(f"클라이언트 메시지: {message}")
# 클라이언트가 exit을 보내면 종료
if message == "exit":
print("클라이언트가 종료를 요청했습니다.")
break
response = f"서버 응답: {message}"
client_socket.send(response.encode("utf-8"))
print("응답 메시지를 보냈습니다.")
print("서버를 종료합니다.")
서버의 두 가지 종료 흐름은 다음과 같습니다.
| 코드 | 의미 |
| if not data: | 클라이언트 연결이 갑자기 끊겼을 때 종료하는 안전장치 |
| if message == "exit": | 클라이언트가 정상 종료를 요청했을 때 종료 |
3. 클라이언트 - 반복 구조와 종료 조건 만들기
→ 서버와 클라이언트의 송수신 코드를 while True 안으로 옮기고 종료 조건을 추가합니다.
chat_client/client.py를 다음과 같이 수정합니다. 사용자가 직접 메시지를 입력하고, exit은 서버에 보낸 뒤 종료합니다.
import socket
HOST = "127.0.0.1"
PORT = 5000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
client_socket.connect((HOST, PORT))
print("서버에 연결되었습니다.")
print("종료하려면 exit을 입력하세요.")
while True:
message = input("보낼 메시지: ")
# exit도 서버에 먼저 보내고 나서 종료
client_socket.send(message.encode("utf-8"))
if message == "exit":
print("서버와의 연결을 종료합니다.")
break
data = client_socket.recv(1024)
response = data.decode("utf-8")
print(response)
print("클라이언트 소켓을 닫았습니다.")
여기서 가장 중요한 포인트는 exit도 서버에 먼저 보내고 나서 종료한다는 것입니다.
# 올바른 순서
client_socket.send(message.encode("utf-8")) # 먼저 보내고
if message == "exit":
break # 그다음 종료
# 잘못된 순서 — 서버가 exit을 받지 못함
if message == "exit":
break # 보내기 전에 종료
client_socket.send(message.encode("utf-8"))
✔ 확인 기준: 서버와 클라이언트의 송수신 코드가 while True 안에 들어 있으면 완료입니다. break가 send()보다 먼저 실행되도록 작성하지 않았는지 확인하세요.
3. 실행 결과 확인하기
→ 서버를 먼저 실행하고 클라이언트에서 여러 메시지를 보낸 뒤 exit으로 종료합니다.
3.1 실행 확인하기
터미널 1에서 서버를 먼저 실행합니다.
python chat_server/server.py
서버를 시작합니다.
클라이언트 접속 대기 중...
터미널 2에서 클라이언트를 실행하고 메시지를 입력합니다.
python chat_client/client.py
서버에 연결되었습니다.
종료하려면 exit을 입력하세요.
보낼 메시지: 안녕하세요
서버 응답: 안녕하세요
보낼 메시지: 두 번째 메시지입니다
서버 응답: 두 번째 메시지입니다
보낼 메시지: exit
서버와의 연결을 종료합니다.
클라이언트 소켓을 닫았습니다.
클라이언트 실행 중 서버 터미널에는 다음이 출력됩니다.
클라이언트가 접속했습니다: ('127.0.0.1', 52344)
클라이언트 메시지: 안녕하세요
응답 메시지를 보냈습니다.
클라이언트 메시지: 두 번째 메시지입니다
응답 메시지를 보냈습니다.
클라이언트 메시지: exit
클라이언트가 종료를 요청했습니다.
서버를 종료합니다.
포트 번호 52344는 실행할 때마다 달라질 수 있습니다.
✔ 확인 기준: 여러 메시지를 주고받고 exit 입력 후 클라이언트와 서버가 모두 오류 없이 종료되면 완료입니다. exit 처리 위치가 send() 뒤에 있는지, 서버와 클라이언트의 send()와 recv() 순서가 맞는지 확인하세요.
3.2 정상 송수신 순서
반복문 안에서도 클라이언트 send() → 서버 recv() → 서버 send() → 클라이언트 recv() 순서가 유지됩니다.
① 클라이언트 input()
↓
② 클라이언트 send()
↓
③ 서버 recv()
↓
④ 서버 send()
↓
⑤ 클라이언트 recv()
↓
⑥ 다시 ①로 돌아감 (while True)
예를 들어 클라이언트가 안녕하세요를 입력하면 다음처럼 진행됩니다.
클라이언트: 안녕하세요 입력
↓
서버: 안녕하세요 수신
↓
서버: 서버 응답: 안녕하세요 전송
↓
클라이언트: 서버 응답: 안녕하세요 출력
↓
다음 메시지 입력 대기
3.3 exit 종료 순서
exit을 입력했을 때의 흐름은 일반 메시지와 다릅니다. 서버는 exit 메시지에 응답을 보내지 않고, 클라이언트도 exit을 보낸 뒤 서버 응답을 기다리지 않습니다. 이렇게 해야 양쪽이 서로 기다리는 상태에 빠지지 않습니다.
클라이언트 input("exit")
↓
클라이언트 send("exit") ← 먼저 보내고
↓
클라이언트 break ← 그다음 종료
↓
서버 recv("exit")
↓
서버 break
↓
양쪽 소켓 닫힘
✔ 확인 기준: 일반 메시지 흐름과 exit 흐름을 따로 설명할 수 있으면 완료입니다. exit에서는 클라이언트가 응답을 기다리지 않고 종료한다는 것을 이해했는지 확인하세요.
3.4 흔한 오류와 해결 방법
| 오류 상황 | 원인 | 해결 방법 |
| exit을 입력했는데 서버가 끝나지 않음 | 클라이언트가 exit을 서버에 보내기 전에 종료함 | send()를 먼저 실행하고 그다음 break를 실행한다 |
| 입력 후 응답이 오지 않음 | 서버의 send() 코드가 while True 밖에 있거나 exit 분기 안에 있음 | 서버 코드에서 send()가 while True 안 정상 흐름에 있는지 확인한다 |
| 서버가 빈 메시지를 계속 처리함 | 클라이언트가 갑자기 닫혀 recv()가 빈 데이터를 반환함 | 서버 코드에 if not data: break 조건이 있는지 확인한다 |
| while True가 끝나지 않음 | break 조건이 없거나 조건이 실행되지 않음 | 서버와 클라이언트 양쪽 모두 break 조건이 있는지 확인한다 |
| recv()에서 멈춘 것처럼 보임 | while 반복 중 상대방이 send()를 하지 않아 기다리는 상태 | 서버와 클라이언트의 send()/recv() 순서가 맞는지 확인한다 |
while True는 반드시 반복문 안에 종료 조건이 있어야 합니다. 서버와 클라이언트 양쪽 모두 종료 조건이 있어야 정상 종료가 됩니다. 한쪽에만 있으면 나머지 한쪽이 계속 기다리는 상황이 생길 수 있습니다.
4. 최종 코드 정리하기
→ 이번 강의에서 수정한 두 파일의 최종 코드를 확인합니다.
4.1 chat_server/server.py
import socket
HOST = "127.0.0.1"
PORT = 5000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((HOST, PORT))
server_socket.listen()
print("서버를 시작합니다.")
print("클라이언트 접속 대기 중...")
client_socket, client_address = server_socket.accept()
with client_socket:
print(f"클라이언트가 접속했습니다: {client_address}")
while True:
data = client_socket.recv(1024)
# 클라이언트 연결이 갑자기 끊겼을 때 종료하는 안전장치
if not data:
print("클라이언트 연결이 종료되었습니다.")
break
message = data.decode("utf-8")
print(f"클라이언트 메시지: {message}")
# 클라이언트가 exit을 보내면 종료
if message == "exit":
print("클라이언트가 종료를 요청했습니다.")
break
response = f"서버 응답: {message}"
client_socket.send(response.encode("utf-8"))
print("응답 메시지를 보냈습니다.")
print("서버를 종료합니다.")
4.2 chat_client/client.py
import socket
HOST = "127.0.0.1"
PORT = 5000
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
client_socket.connect((HOST, PORT))
print("서버에 연결되었습니다.")
print("종료하려면 exit을 입력하세요.")
while True:
message = input("보낼 메시지: ")
# exit도 서버에 먼저 보내고 나서 종료
client_socket.send(message.encode("utf-8"))
if message == "exit":
print("서버와의 연결을 종료합니다.")
break
data = client_socket.recv(1024)
response = data.decode("utf-8")
print(response)
print("클라이언트 소켓을 닫았습니다.")
4.3 최종 확인 표
| 확인할 코드 | 위치 | 의미 |
while True: |
서버, 클라이언트 | 송수신을 반복한다 |
input("보낼 메시지: ") |
클라이언트 | 사용자가 직접 메시지를 입력한다 |
client_socket.send(...) |
클라이언트 | 입력한 메시지를 서버로 보낸다 |
if message == "exit" |
서버, 클라이언트 | 종료 조건을 확인한다 |
break |
서버, 클라이언트 | 반복문을 종료한다 |
if not data: |
서버 | 클라이언트가 갑자기 종료된 상황을 처리한다 |
→ 다음 강의 (8강): 이번 강의의 반복 구조를 바탕으로 1:1 터미널 채팅 프로그램을 완성합니다. 서버와 클라이언트 양쪽에서 직접 메시지를 입력하고, 터미널에서 대화하듯 주고받는 구조로 발전시킵니다.