13강. 전체 채팅 브로드캐스팅 구현

0. 학습 목표
→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.
0.1 이번 글에서 다룰 내용
이번 글은 구현 중심 강의입니다.
12강까지 여러 클라이언트가 서버에 접속할 수 있었고, 서버는 접속 중인 클라이언트 목록도 관리했습니다. 하지만 클라이언트끼리 서로의 메시지를 볼 수는 없었습니다. 이번 강의에서는 그 마지막 연결을 완성합니다.
한 클라이언트가 보낸 메시지를 서버가 받아 나머지 클라이언트 전체에게 전달하는 것을 브로드캐스팅이라고 합니다.
클라이언트 A: "안녕하세요"
↓
서버가 메시지 수신
↓
clients 목록 순회
↓
클라이언트 B에게 전달
클라이언트 C에게 전달
(A에게는 보내지 않음)
서버 쪽 변경만으로는 부족합니다. 전체 채팅에서는 내가 입력하는 도중에도 다른 사람의 메시지가 도착할 수 있습니다. 클라이언트가 보내는 일과 받는 일을 동시에 처리하려면 클라이언트에도 수신 전용 스레드를 추가해야 합니다. 이번 강의에서 수정하는 파일은 두 개입니다.
| 파일 | 추가 내용 |
chat_server/server.py |
broadcast() 함수 — clients 목록을 순회해 메시지 전달 |
chat_client/client.py |
receive_messages() 함수 — 수신 전용 스레드 |

| 구분 | 내용 |
| 이해할 것 | 브로드캐스팅은 받은 메시지를 clients 목록을 순회해 보낸 사람 제외 나머지에게 전달하는 구조 |
| 만들 것 | broadcast() 함수와 수신 스레드가 추가된 server.py, client.py |
| 확인할 것 | 한 클라이언트가 보낸 메시지가 다른 클라이언트 터미널에 출력되는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 두 파일을 모두 수정합니다.
chat_server/
└── server.py ← 이번 강의에서 수정 (broadcast() 함수 추가)
chat_client/
└── client.py ← 이번 강의에서 수정 (수신 전용 스레드 추가)
각 파일에서 추가되는 구조입니다.
chat_server/server.py
├── 기존: clients, print_clients(), handle_client(), 메인 루프
└── 추가: broadcast(message, sender_socket)
└── clients 순회 → sender 제외 → send()
chat_client/client.py
├── 기존: connect(), send() 반복
└── 추가: receive_messages(client_socket)
└── 수신 전용 스레드로 실행 (daemon=True)
1. 실습 준비하기
→ 12강 서버의 한계를 확인하고, 브로드캐스팅 규칙을 정리합니다.
12강에서 완성한 서버가 정상 실행되는지 확인합니다.
python chat_server/server.py
접속자 목록 관리 서버를 시작합니다.
클라이언트 접속 대기 중...
확인 후 Ctrl + C로 서버를 종료합니다. 이번 강의에서는 두 파일 모두 수정합니다. 12강 서버가 할 수 없었던 것을 정리합니다.
| 없는 것 | 이번 강의에서 추가 |
| 메시지를 다른 클라이언트에게 전달 | broadcast() 함수 |
| 보낸 사람 제외 처리 | if client != sender_socket: 조건 |
| 전달 실패 시 소켓 정리 | broadcast() 안의 try/except |
| 클라이언트의 동시 송수신 | receive_messages() 수신 스레드 |
이번 강의의 브로드캐스팅 규칙입니다. 아직 사용자 이름을 따로 만들지 않아 클라이언트 주소를 임시 이름처럼 사용합니다.
| 규칙 | 내용 |
| 보낸 클라이언트 | 자기 메시지를 다시 받지 않는다 |
| 나머지 클라이언트 | 보낸 사람의 메시지를 받는다 |
| 메시지 형식 | 클라이언트 주소와 메시지를 함께 표시한다 (예: ('127.0.0.1', 52344): 안녕하세요) |
| 종료 | exit 입력 시 연결을 종료한다 |
✔ 확인 기준: 12강에서 만든 서버가 정상 실행되면 완료. chat_server/server.py가 존재하는지, 포트 5000이 다른 프로그램에 점유되어 있지 않은지 확인하세요.
2. 서버 - broadcast() 함수 만들기
→ broadcast() 함수를 만들고 handle_client()에서 호출하도록 연결합니다.
2.1 broadcast() 함수 추가하기
chat_server/server.py를 열고 print_clients() 함수 아래에 broadcast() 함수를 추가합니다.
import socket
import threading
HOST = "127.0.0.1"
PORT = 5000
clients = []
def print_clients():
print(f"현재 접속자 수: {len(clients)}")
def broadcast(message, sender_socket):
for client in clients:
if client != sender_socket: # 보낸 사람에게는 다시 보내지 않음
try:
client.send(message.encode("utf-8"))
except:
if client in clients: # 전달 실패 시 해당 소켓을 목록에서 제거
clients.remove(client)
| 코드 | 의미 |
for client in clients: |
접속 중인 모든 클라이언트를 순회 |
if client != sender_socket: |
보낸 사람은 제외 — 이 조건이 없으면 자기 메시지가 자기 화면에도 나타남 |
try/except |
이미 끊어진 소켓으로 전송을 시도하면 오류가 발생할 수 있어 예외 처리 |
broadcast() 안의 try/except는 빈 except:를 사용했습니다. 전송 실패 원인이 다양하고(네트워크 끊김, 소켓 이미 닫힘 등) 어떤 예외가 발생할지 정확히 특정하기 어려우므로 이렇게 처리합니다. 지금은 "전달 실패 시 목록에서 제거한다"는 흐름을 이해하는 것이 핵심입니다.
💡 강사 팁 — 스레드 안전성과 Lock
현재 코드에서 clients.append()는 메인 스레드에서, clients.remove()는 처리 스레드에서 동시에 실행될 수 있습니다. 지금 실습 규모에서는 문제가 드러나지 않지만, 동시 접속이 많아지면 두 스레드가 같은 리스트를 동시에 수정하다가 예상치 못한 오류가 날 수 있습니다.
이런 상황에 대비하는 도구가 threading.Lock()입니다. 자물쇠처럼 한 스레드가 리스트를 수정하는 동안 다른 스레드는 기다리게 만드는 방식입니다.
import threading
clients = []
clients_lock = threading.Lock() # 자물쇠 생성
# 접속 시 (메인 스레드)
with clients_lock:
clients.append(client_socket)
# 종료 시 (처리 스레드)
with clients_lock:
clients.remove(client_socket)
지금 당장 코드에 추가할 필요는 없습니다. 이번 실습에서는 clients[:] 복사본 순회로 충분합니다. 다만 "왜 Lock이 필요해지는지"를 지금 경험해두면 Part 5 이후 멀티스레드 코드를 다룰 때 훨씬 빠르게 납득할 수 있습니다.
2.2 handle_client()에서 broadcast() 호출하기
handle_client() 함수 안에서 메시지를 받은 뒤 broadcast()를 호출하도록 수정합니다. 기존에 서버가 보내던 확인 응답(response = f"서버가 받은 메시지: {message}") 대신 브로드캐스팅으로 교체합니다. 이번에 바뀐 줄을 주석으로 표시했습니다.
import socket
import threading
HOST = "127.0.0.1"
PORT = 5000
clients = []
def print_clients():
print(f"현재 접속자 수: {len(clients)}")
def broadcast(message, sender_socket):
for client in clients:
if client != sender_socket:
try:
client.send(message.encode("utf-8"))
except:
if client in clients:
clients.remove(client)
def handle_client(client_socket, client_address):
print(f"클라이언트 처리 시작: {client_address}")
with client_socket:
while True:
data = client_socket.recv(1024)
if not data:
print(f"클라이언트 연결 끊김: {client_address}")
break
message = data.decode("utf-8")
print(f"{client_address} 메시지: {message}")
if message == "exit":
print(f"클라이언트 종료 요청: {client_address}")
break
# 12강: response = f"서버가 받은 메시지: {message}" → client_socket.send(response.encode())
# 13강: broadcast()로 교체
broadcast_message = f"{client_address}: {message}" # 누가 보낸 메시지인지 포함
broadcast(broadcast_message, client_socket) # sender 제외하고 전달
if client_socket in clients:
clients.remove(client_socket)
print(f"접속자 목록에서 제거: {client_address}")
print_clients()
print(f"클라이언트 처리 종료: {client_address}")
브로드캐스팅이 실제로 일어나는 핵심 두 줄입니다.
broadcast_message = f"{client_address}: {message}" # 누가 보낸 메시지인지 함께 포함
broadcast(broadcast_message, client_socket) # sender 제외하고 전달
메인 루프는 12강과 동일합니다. 서버 시작 문구만 "전체 채팅 서버를 시작합니다."로 바꿉니다.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((HOST, PORT))
server_socket.listen()
print("전체 채팅 서버를 시작합니다.")
print("클라이언트 접속 대기 중...")
while True:
client_socket, client_address = server_socket.accept()
print(f"클라이언트 접속: {client_address}")
clients.append(client_socket)
print(f"접속자 목록에 추가: {client_address}")
print_clients()
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, client_address)
)
client_thread.start()
✔ 확인 기준: broadcast() 함수가 handle_client() 위에 정의되어 있고, handle_client() 안에서 broadcast()를 호출하는 두 줄이 if message == "exit": 분기 밖에 있으면 완료. broadcast_message와 broadcast(...) 두 줄이 exit 처리 이후가 아닌, 그 앞에 있는지 확인하세요.
3. 클라이언트 - 수신 스레드 추가하기
→ 메시지를 보내면서 동시에 받을 수 있도록 chat_client/client.py에 수신 전용 스레드를 추가합니다.
3.1 수신 스레드가 필요한 이유
12강까지의 클라이언트는 다음 순서로만 동작했습니다.
메시지 입력 → send() → recv() → 다음 입력
전체 채팅에서는 언제 다른 사람이 메시지를 보낼지 알 수 없습니다. 내가 다음 메시지를 입력하려고 기다리는 중에도 B의 메시지가 도착합니다. 입력과 수신을 한 흐름으로 처리하면 한 쪽이 막혀 있는 동안 다른 쪽을 처리할 수 없습니다. 해결 방법은 10강에서 배운 스레드입니다.
| 역할 | 담당 |
| 사용자가 입력한 메시지 보내기 | 메인 흐름 |
| 서버에서 오는 메시지 받기 | 수신 스레드 (receive_messages()) |
3.2 receive_messages() 함수 추가하기
chat_client/client.py를 열고 import 줄 아래에 수신 함수를 추가합니다.
import socket
import threading
HOST = "127.0.0.1"
PORT = 5000
def receive_messages(client_socket):
while True:
try:
data = client_socket.recv(1024)
if not data: # 서버 연결이 끊어진 경우
print("서버와의 연결이 끊어졌습니다.")
break
message = data.decode("utf-8")
print(f"\n{message}") # \n으로 줄 내려 출력 — 입력 줄과 구분
except:
break
print(f"\n{message}")에서 앞에 \n을 붙이는 이유가 있습니다. 터미널에서 사용자가 입력 중인 줄 다음에 수신 메시지가 이어서 붙지 않도록 줄을 하나 내려 출력합니다. 입력과 수신 출력이 완전히 분리되어 보이지 않는 것은 현재 단계에서 정상이며, GUI 단계에서 개선됩니다.
3.3 수신 스레드 시작하기
connect() 직후에 수신 스레드를 만들고 시작합니다. 메인 흐름의 input() 반복문 전에 스레드가 먼저 실행되어야 합니다.
import socket
import threading
HOST = "127.0.0.1"
PORT = 5000
def receive_messages(client_socket):
while True:
try:
data = client_socket.recv(1024)
if not data:
print("서버와의 연결이 끊어졌습니다.")
break
message = data.decode("utf-8")
print(f"\n{message}")
except:
break
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
client_socket.connect((HOST, PORT))
print("서버에 연결되었습니다.")
print("전체 채팅을 시작합니다. 종료하려면 exit을 입력하세요.")
receive_thread = threading.Thread(
target=receive_messages,
args=(client_socket,)
)
receive_thread.daemon = True # 메인 흐름 종료 시 수신 스레드도 함께 종료 — start() 전에 설정
receive_thread.start()
while True:
message = input("")
client_socket.send(message.encode("utf-8"))
if message == "exit":
print("서버와의 연결을 종료합니다.")
break
print("클라이언트 소켓을 닫았습니다.")
receive_thread.daemon = True를 설정하는 이유입니다.
일반 스레드는 메인 프로그램이 끝나더라도 스레드 자신의 작업이 끝날 때까지 프로그램이 종료되지 않습니다.
수신 스레드는 서버 연결이 끊기지 않으면 while True에서 계속 기다립니다.
사용자가 exit을 입력해 메인 흐름이 끝나도 수신 스레드가 남아 있으면 프로그램이 깔끔하게 종료되지 않습니다.
daemon = False (기본): 메인 흐름 종료 → 수신 스레드가 끝날 때까지 대기 → 프로그램 종료
daemon = True: 메인 흐름 종료 → 수신 스레드 자동 종료 → 프로그램 종료
daemon = True를 빠뜨리면 클라이언트에서 exit을 입력해도 프로그램이 바로 종료되지 않고 터미널이 멈춘 것처럼 보입니다. 또한 daemon 설정은 반드시 start() 전에 해야 합니다. start() 이후에 설정하면 RuntimeError가 발생합니다.
✔ 확인 기준: 서버가 실행 중일 때 python chat_client/client.py를 실행하면 "서버에 연결되었습니다." 출력과 함께 입력 대기 상태가 되면 완료. receive_thread.daemon = True가 start() 전에 있는지, args=(client_socket,)에 쉼표가 있는지 확인하세요.
4. 실행 결과 확인하기
→ 서버 1개와 클라이언트 2개를 실행해 메시지가 서로 전달되는지 확인합니다.
4.1 서버와 클라이언트 2개 실행하기
터미널 1에서 서버를 먼저 실행합니다.
python chat_server/server.py
전체 채팅 서버를 시작합니다.
클라이언트 접속 대기 중...
터미널 2와 3에서 각각 클라이언트를 실행합니다.
python chat_client/client.py
서버에 연결되었습니다.
전체 채팅을 시작합니다. 종료하려면 exit을 입력하세요.
두 번째 클라이언트가 접속하면 서버 터미널에 접속자 수가 2로 증가합니다.
클라이언트 접속: ('127.0.0.1', 52344)
접속자 목록에 추가: ('127.0.0.1', 52344)
현재 접속자 수: 1
클라이언트 처리 시작: ('127.0.0.1', 52344)
클라이언트 접속: ('127.0.0.1', 52345)
접속자 목록에 추가: ('127.0.0.1', 52345)
현재 접속자 수: 2
클라이언트 처리 시작: ('127.0.0.1', 52345)
4.2 메시지 브로드캐스팅 확인하기
터미널 2(클라이언트 A)에서 메시지를 입력합니다.
안녕하세요 A입니다
터미널 3(클라이언트 B)에 다음처럼 출력되면 성공입니다.
('127.0.0.1', 52344): 안녕하세요 A입니다
터미널 3(클라이언트 B)에서 메시지를 입력하면 터미널 2(클라이언트 A)에 표시됩니다.
('127.0.0.1', 52345): 안녕하세요 B입니다
입력 줄과 받은 메시지가 터미널에서 섞여 보일 수 있습니다. 사용자가 입력 중인 도중에 수신 스레드가 메시지를 출력하기 때문입니다. 현재 단계에서는 정상이며, GUI 단계에서 입력 창과 메시지 창이 분리되어 개선됩니다.
이번 강의의 성공 판단 기준: 클라이언트 A가 입력한 메시지가 클라이언트 B 터미널에 표시되고, B가 입력한 메시지가 A 터미널에 표시되면 완료입니다. A 자신의 화면에는 자기 메시지가 다시 나타나지 않아야 합니다.
4.3 흔한 오류와 해결 방법
오류가 나오면 어느 단계에서 실패했는지 먼저 생각합니다.
| 오류 상황 | 원인 및 해결 방법 |
| 메시지를 보내도 다른 클라이언트에 보이지 않음 | broadcast()를 호출하지 않았거나 clients 목록이 비어 있음. handle_client() 안에서 broadcast()를 호출하는지, 접속 시 clients.append()가 실행되는지 확인한다 |
| 내가 보낸 메시지가 내 화면에도 다시 보임 | sender_socket 제외 조건이 없음. broadcast() 안에 if client != sender_socket: 조건을 추가한다 |
| 클라이언트가 다른 사람 메시지를 받지 못함 | 수신 스레드가 시작되지 않았음. client.py에 receive_thread.start()가 있는지 확인한다 |
exit 입력 후 프로그램이 바로 종료되지 않음 |
receive_thread.daemon = True가 없음. start() 전에 daemon = True를 설정한다 |
RuntimeError: cannot set daemon after start |
daemon = True를 start() 이후에 설정함. 반드시 start() 전에 둔다 |
| 클라이언트 종료 후 서버에서 전송 오류 발생 | 이미 끊어진 소켓으로 send()를 시도함. broadcast() 안에 try/except가 있는지 확인한다 |
| 입력 줄과 수신 메시지가 섞여 보임 | 입력과 수신이 터미널에서 동시에 발생하는 것. 정상 동작이다. GUI 단계에서 개선된다 |
✔ 확인 기준: 클라이언트끼리 메시지가 전달되면 완료. 서버의 broadcast()가 handle_client() 안에서 호출되는지, 클라이언트의 receive_thread.start()가 있는지 확인하세요.
5. 최종 코드 정리하기
→ 이번 강의에서 완성한 두 파일의 전체 코드를 한곳에 정리합니다.
5.1 chat_server/server.py
import socket
import threading
HOST = "127.0.0.1"
PORT = 5000
clients = []
def print_clients():
print(f"현재 접속자 수: {len(clients)}")
def broadcast(message, sender_socket):
for client in clients:
if client != sender_socket:
try:
client.send(message.encode("utf-8"))
except:
if client in clients:
clients.remove(client)
def handle_client(client_socket, client_address):
print(f"클라이언트 처리 시작: {client_address}")
with client_socket:
while True:
data = client_socket.recv(1024)
if not data:
print(f"클라이언트 연결 끊김: {client_address}")
break
message = data.decode("utf-8")
print(f"{client_address} 메시지: {message}")
if message == "exit":
print(f"클라이언트 종료 요청: {client_address}")
break
broadcast_message = f"{client_address}: {message}"
broadcast(broadcast_message, client_socket)
if client_socket in clients:
clients.remove(client_socket)
print(f"접속자 목록에서 제거: {client_address}")
print_clients()
print(f"클라이언트 처리 종료: {client_address}")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((HOST, PORT))
server_socket.listen()
print("전체 채팅 서버를 시작합니다.")
print("클라이언트 접속 대기 중...")
while True:
client_socket, client_address = server_socket.accept()
print(f"클라이언트 접속: {client_address}")
clients.append(client_socket)
print(f"접속자 목록에 추가: {client_address}")
print_clients()
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, client_address)
)
client_thread.start()
5.2 chat_client/client.py
import socket
import threading
HOST = "127.0.0.1"
PORT = 5000
def receive_messages(client_socket):
while True:
try:
data = client_socket.recv(1024)
if not data:
print("서버와의 연결이 끊어졌습니다.")
break
message = data.decode("utf-8")
print(f"\n{message}")
except:
break
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as client_socket:
client_socket.connect((HOST, PORT))
print("서버에 연결되었습니다.")
print("전체 채팅을 시작합니다. 종료하려면 exit을 입력하세요.")
receive_thread = threading.Thread(
target=receive_messages,
args=(client_socket,)
)
receive_thread.daemon = True
receive_thread.start()
while True:
message = input("")
client_socket.send(message.encode("utf-8"))
if message == "exit":
print("서버와의 연결을 종료합니다.")
break
print("클라이언트 소켓을 닫았습니다.")
5.3 최종 확인 표
| 확인할 코드 | 위치 | 의미 |
def broadcast(message, sender_socket): |
server.py |
전체 전달 함수 — 보낸 사람 제외 |
if client != sender_socket: |
server.py broadcast() |
보낸 사람에게는 다시 보내지 않음 |
try/except |
server.py broadcast() |
전달 실패 시 목록에서 해당 소켓 제거 |
broadcast_message = f"{client_address}: {message}" |
server.py handle_client() |
누가 보낸 메시지인지 포함 |
broadcast(broadcast_message, client_socket) |
server.py handle_client() |
브로드캐스팅 호출 |
def receive_messages(client_socket): |
client.py |
수신 전용 함수 |
receive_thread.daemon = True |
client.py |
메인 종료 시 수신 스레드도 함께 종료 |
receive_thread.start() |
client.py |
수신 스레드 시작 — input() 반복 전에 실행 |
→ 다음 강의 (14강): "왜 메시지 규칙이 필요한가?"를 다룹니다. 현재 단순 문자열 메시지의 한계(일반 채팅과 시스템 메시지 구분 불가, exit 단어 오인 등)를 확인하고, 메시지 종류를 구분할 수 있는 JSON 기반 메시지 구조로 발전할 준비를 합니다.
6. [과제] 서버 콘솔 명령어 만들기
→ 13강 코드의 메인 스레드에 관리자 입력 루프를 추가해 /list 명령으로 접속자 수와 목록을 조회합니다.
6.1 과제 설명과 목표
난이도 ★★ · 출발 코드는 13강 최종 chat_server/server.py입니다. 13강에는 broadcast()가 있고 clients는 다시 리스트입니다. 과제 2의 딕셔너리 변형은 잊고, 13강 원본 리스트 구조에서 시작하세요.
지금까지 서버 관리자는 접속 로그를 눈으로 흘려보기만 했습니다. 이번에는 관리자가 직접 명령을 입력해 현재 상태를 조회하도록 작은 명령어 루프를 만듭니다. /list를 입력하면 접속자 수와 목록을 보여주는 것이 목표입니다.
문제는 server_socket.accept()가 클라이언트를 기다리며 메인 스레드를 붙잡고 있다는 점입니다. 그래서 접속을 받는 일과 관리자 입력을 받는 일을 서로 다른 스레드로 나눠야 합니다.
6.2 단계별로 코드 바꾸기
먼저 관리자 명령을 처리하는 함수를 새로 만듭니다. input()으로 명령을 받아 /list인지 확인합니다.
def admin_console():
while True:
command = input() # 관리자가 콘솔에 입력한 명령을 받는다
if command == "/list": # 접속자 조회 명령
print(f"현재 접속자 수: {len(clients)}") # 리스트 길이로 인원 확인
for client in clients:
print(f" - {client.getpeername()}") # 각 소켓의 상대 주소를 출력
else:
print("알 수 없는 명령입니다. (/list)")
getpeername()은 소켓에 연결된 상대방의 주소를 돌려주는 함수입니다. 13강에는 접속자 이름이 따로 없으므로 주소로 목록을 보여줍니다.
이제 기존 접속 대기 루프를 별도 함수로 감싸, 관리자 콘솔과 동시에 돌 수 있게 합니다.
def accept_clients(server_socket):
while True:
client_socket, client_address = server_socket.accept() # 접속을 기다린다
print(f"클라이언트 접속: {client_address}")
clients.append(client_socket) # 13강과 동일: 리스트에 추가
print(f"현재 접속자 수: {len(clients)}")
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, client_address)
)
client_thread.start()
마지막으로 메인 부분에서 접속 대기를 백그라운드 스레드로 돌리고, 메인 스레드는 관리자 콘솔을 담당하게 합니다.
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((HOST, PORT))
server_socket.listen()
print("관리자 콘솔 서버를 시작합니다. (/list 입력 가능)")
accept_thread = threading.Thread( # 접속 받기를 별도 스레드로
target=accept_clients,
args=(server_socket,)
)
accept_thread.daemon = True # 메인이 끝나면 같이 종료
accept_thread.start()
admin_console() # 메인 스레드는 관리자 입력 담당
✓ 확인 기준: 클라이언트가 접속한 상태에서 서버 콘솔에 /list를 입력하면 접속자 수와 주소 목록이 출력되고, 클라이언트 채팅도 평소처럼 동작하면 완료입니다.
6.3 최종 코드 (chat_server/server.py 핵심부)
broadcast()와 handle_client()는 13강 그대로이므로, 새로 추가·변경한 부분 위주로 정리합니다.
import socket
import threading
HOST = "127.0.0.1"
PORT = 5000
clients = []
def broadcast(message, sender_socket):
for client in clients:
if client != sender_socket:
try:
client.send(message.encode("utf-8"))
except:
if client in clients:
clients.remove(client)
def handle_client(client_socket, client_address):
print(f"클라이언트 처리 시작: {client_address}")
with client_socket:
while True:
data = client_socket.recv(1024)
if not data:
break
message = data.decode("utf-8")
if message == "exit":
break
broadcast_message = f"{client_address}: {message}"
broadcast(broadcast_message, client_socket)
if client_socket in clients:
clients.remove(client_socket)
print(f"클라이언트 처리 종료: {client_address}")
def admin_console():
while True:
command = input()
if command == "/list":
print(f"현재 접속자 수: {len(clients)}")
for client in clients:
print(f" - {client.getpeername()}")
else:
print("알 수 없는 명령입니다. (/list)")
def accept_clients(server_socket):
while True:
client_socket, client_address = server_socket.accept()
print(f"클라이언트 접속: {client_address}")
clients.append(client_socket)
print(f"현재 접속자 수: {len(clients)}")
client_thread = threading.Thread(
target=handle_client,
args=(client_socket, client_address)
)
client_thread.start()
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:
server_socket.bind((HOST, PORT))
server_socket.listen()
print("관리자 콘솔 서버를 시작합니다. (/list 입력 가능)")
accept_thread = threading.Thread(
target=accept_clients,
args=(server_socket,)
)
accept_thread.daemon = True
accept_thread.start()
admin_console()
| 확인할 코드 | 의미 |
def admin_console() |
관리자 명령을 받는 입력 루프 |
accept_clients() 스레드 |
접속 받기를 백그라운드로 분리 |
client.getpeername() |
접속자 소켓의 상대 주소 조회 |
accept_thread.daemon = True |
메인 종료 시 접속 스레드도 함께 종료 |
6.4 대안 과제 (쉬움) · 본인 메시지 수신 여부 바꿔 보기
콘솔 명령어가 부담스러운 학생을 위한 더 쉬운 버전입니다. 13강 broadcast()에서 보낸 사람 본인에게도 메시지가 가도록 조건을 바꿔 차이를 관찰합니다.
def broadcast(message, sender_socket):
for client in clients:
# 기존: if client != sender_socket → 본인 제외
# 변경: 조건을 빼면 본인에게도 전송됨
try:
client.send(message.encode("utf-8")) # 본인 포함 모두에게 전송
except:
if client in clients:
clients.remove(client)
if client != sender_socket 조건을 없애면 내가 보낸 메시지가 내 화면에도 다시 찍힙니다. 단체 채팅방에서 내 메시지가 내 화면에 보이는 것과, 무전기처럼 보낸 사람에게는 안 들리는 것의 차이를 직접 비교해 보면 좋습니다.
6.5 강사 팁 · 막히는 지점
중요한 점. accept()와 input()은 둘 다 "무언가 들어올 때까지 기다리는" 블로킹 함수입니다. 한 스레드에서 둘을 같이 돌리면 한쪽이 다른 쪽을 막아 버립니다. 그래서 반드시 스레드를 나눠야 한다는 점을 강조하세요.
강사 팁. 여러 스레드가 동시에 clients 리스트를 수정하면 이론적으로 충돌이 생길 수 있습니다. 지금 단계에서는 깊이 다루지 않되, "나중에 Lock이라는 안전장치가 필요해진다"는 정도만 언급해 두면 이후 학습의 연결고리가 됩니다.