11강 구현 중심 ⏱ 약 40분

 

0. 학습 목표

→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.

더보기

0.1 이번 글에서 다룰 내용

이번 글은 구현 중심 강의입니다.

 

10강에서 threading.Thread(target=함수, args=(...))` 구조를 익혔습니다. 이번 강의에서는 그 구조를 실제 서버 코드에 적용합니다. 10강의 work(name) 함수가 이번 강의에서 handle_client(client_socket, client_address) 함수로 바뀌는 것입니다.

10강 (thread_basic.py):
threading.Thread(target=work, args=("A",))

11강 (chat_server/server.py):
threading.Thread(target=handle_client, args=(client_socket, client_address))

 

이번 강의의 목표는 아직 전체 채팅 브로드캐스팅이 아닙니다.

여러 클라이언트가 동시에 접속하고, 각자 서버와 메시지를 주고받을 수 있는 구조를 만드는 것입니다.

클라이언트 A 접속 → A 처리 스레드 시작 → 서버는 다시 접속 대기
클라이언트 B 접속 → B 처리 스레드 시작 → 서버는 다시 접속 대기
클라이언트 C 접속 → C 처리 스레드 시작 → 서버는 다시 접속 대기

 

이번 강의에서 만드는 handle_client() 함수와 스레드 생성 구조는 최종 채팅 서버의 핵심 뼈대입니다. 이후 강의에서 접속자 목록·브로드캐스팅·GUI 연결 기능이 추가되어도 이 구조는 그대로 유지됩니다.

구분 내용
이해할 것 메인 스레드와 클라이언트 처리 스레드의 역할 차이
만들 것 스레드 기반 chat_server/server.py와 테스트용 chat_client/client.py
확인할 것 클라이언트 여러 개를 동시에 실행해도 서버가 각각의 메시지를 처리하는지

 

0.2 이번 강의에서 직접 다루는 구조

이번 강의에서는 chat_server/server.py를 스레드 기반 구조로 수정하고, chat_client/client.py를 반복 송수신 구조로 정리합니다.

chat_server/
└── server.py      ← 이번 강의에서 수정 (스레드 기반 다중 접속 구조로 변경)

chat_client/
└── client.py      ← 이번 강의에서 수정 (반복 송수신 테스트용 구조로 정리)

이번 강의의 서버 구조입니다.

chat_server/server.py
├── handle_client(client_socket, client_address)  ← 클라이언트 1명 담당
│   └── while True: recv() → 메시지 처리 → send()
└── 메인 서버 루프
    ├── socket() / bind() / listen()
    └── while True: accept() → threading.Thread 생성 → start()

 

1. 실습 준비하기

→ 이번 강의의 실행 방식을 확인하고, 기존 서버 파일을 수정할 준비를 합니다.

더보기

이번 강의에서는 터미널을 최소 3개 사용합니다. 서버는 한 번만 실행하고, 클라이언트는 여러 터미널에서 각각 실행해 여러 사용자가 동시에 접속한 상황을 만듭니다.

터미널 1: python chat_server/server.py   ← 서버 (한 번만 실행)
터미널 2: python chat_client/client.py   ← 클라이언트 A
터미널 3: python chat_client/client.py   ← 클라이언트 B

 

8강까지 작성한 server.py가 있는지 먼저 확인합니다.

ls chat_server/
server.py

 

이번 강의에서는 기존 server.py 전체를 스레드 기반 구조로 새로 작성합니다. 기존 코드와 합치려 하면 오히려 혼란스럽습니다. 파일을 열고 전체 내용을 지운 뒤 처음부터 작성합니다.

✔ 확인 기준: 프로젝트 루트에서 chat_server/server.py 파일이 에디터에서 열리고 내용을 비울 수 있으면 완료. 현재 위치가 프로젝트 루트인지 확인하세요.

 

2. 서버 - handle_client() 함수 만들기

→ 클라이언트 한 명을 처음부터 끝까지 담당하는 handle_client() 함수를 단계별로 작성합니다.

더보기

2.1 import와 HOST, PORT 작성하기

chat_server/server.py를 열고 다음 코드를 작성합니다.

import socket
import threading          # 스레드를 만들기 위한 모듈

HOST = "127.0.0.1"
PORT = 5000
추가 항목 의미
import threading 스레드를 만들기 위한 모듈. 8강 코드에는 없던 줄
HOST = "127.0.0.1" 현재 컴퓨터에서 실습
PORT = 5000 서버가 사용할 포트

 

2.2 handle_client() 함수 추가하기

import 줄 아래에 handle_client() 함수를 추가합니다. 이 함수가 클라이언트 한 명을 처음부터 끝까지 담당합니다.

import socket
import threading

HOST = "127.0.0.1"
PORT = 5000

def handle_client(client_socket, client_address):
    print(f"클라이언트 처리 시작: {client_address}")

    with client_socket:
        while True:
            data = client_socket.recv(1024)

            if not data:                                  # recv() 결과가 비어 있으면 상대방 연결이 끊긴 것
                print(f"클라이언트 연결 끊김: {client_address}")
                break

            message = data.decode("utf-8")
            print(f"{client_address} 메시지: {message}")

            if message == "exit":                         # 클라이언트가 exit을 보내면 정상 종료
                print(f"클라이언트 종료 요청: {client_address}")
                break

            response = f"서버가 받은 메시지: {message}"
            client_socket.send(response.encode("utf-8"))  # 확인 응답을 클라이언트에게 보냄

    print(f"클라이언트 처리 종료: {client_address}")

handle_client()의 흐름을 정리하면 다음과 같습니다.

handle_client() 시작
        ↓
while True: recv() 대기
        ↓
if not data → 연결 끊김 → break
if message == "exit" → 정상 종료 → break
그 외 → 확인 응답 send()
        ↓
with 블록 종료 → client_socket 자동으로 닫힘
        ↓
handle_client() 종료

if not data: 조건에 주의합니다. recv() 결과가 비어 있다는 것은 상대방이 연결을 끊었다는 신호입니다. 이 조건이 없으면 연결이 끊긴 뒤에도 반복문이 계속 돌면서 오류가 발생합니다.

 

with client_socket: 구조를 쓰면 break로 반복문을 빠져나올 때 client_socket.close()를 따로 호출하지 않아도 됩니다. with 블록이 끝나면 자동으로 소켓을 닫아줍니다. 스레드가 여러 개 동시에 실행될 때 소켓을 깜빡하고 안 닫으면 리소스가 누적되어 나중에 이상한 문제가 생길 수 있습니다. with로 처리하는 습관을 들여 두면 좋습니다.

✔ 확인 기준: handle_client() 안에 recv() 반복문, if not data: break, if message == "exit": break, send() 코드가 들어 있으면 완료. send()exit 분기 밖에 있는지, if not data: 조건이 recv() 바로 뒤에 있는지 확인하세요.

 

3. 서버 - 메인 루프 만들기

→ accept()를 반복하면서 클라이언트마다 새 스레드를 생성하는 메인 루프를 추가합니다.

더보기

3.1 서버 소켓 생성과 접속 대기 추가하기

handle_client() 함수 아래에 메인 서버 루프를 추가합니다.

import socket
import threading

HOST = "127.0.0.1"
PORT = 5000

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

            response = f"서버가 받은 메시지: {message}"
            client_socket.send(response.encode("utf-8"))

    print(f"클라이언트 처리 종료: {client_address}")


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server_socket:  # with로 감싸면 서버 종료 시 자동으로 소켓을 닫음
    server_socket.bind((HOST, PORT))
    server_socket.listen()

    print("스레드 기반 서버를 시작합니다.")
    print("클라이언트 접속 대기 중...")

with socket.socket(...) as server_socket: 구조를 쓰면 서버가 종료될 때 server_socket.close()를 자동으로 호출합니다. 8강에서 try/finally로 소켓을 닫던 방식과 동일한 효과입니다.

 

3.2 accept() 반복과 스레드 생성 추가하기

listen() 아래에 while True 반복문과 스레드 생성 코드를 추가합니다. 이번 강의에서 가장 핵심이 되는 부분입니다.

import socket
import threading

HOST = "127.0.0.1"
PORT = 5000

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

            response = f"서버가 받은 메시지: {message}"
            client_socket.send(response.encode("utf-8"))

    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}")

        client_thread = threading.Thread(           # 클라이언트마다 새 스레드를 만들어 처리
            target=handle_client,
            args=(client_socket, client_address)
        )
        client_thread.start()                       # 스레드 시작 — 메인 흐름은 바로 다음 accept()로 돌아감

        print(f"현재 실행 중인 스레드 수: {threading.active_count()}")

 

이번 강의의 핵심 코드는 다음 세 줄입니다.

코드 의미
target=handle_client 새 스레드에서 실행할 함수 — 괄호 없이 이름만 전달
args=(client_socket, client_address) handle_client()에 전달할 값
client_thread.start() 스레드 실행 시작 — 이 줄 이후 메인 흐름은 다시 accept()로 돌아감

accept()while True 안에 있기 때문에 서버는 클라이언트 한 명을 처리하느라 멈추지 않고 계속 새 접속을 받습니다.

while True:
    accept()          ← 새 접속 기다림
    스레드 생성·시작   ← 처리는 스레드에 맡김
    다시 accept()     ← 바로 다음 접속을 기다림

client_thread.start() 다음에 client_thread.join()을 넣지 않아야 합니다. 10강에서 join()이 스레드가 끝날 때까지 기다린다고 배웠습니다. 여기서 join()을 넣으면 그 스레드가 끝날 때까지 메인 흐름이 기다리게 되어, 결국 한 명씩 순서대로 처리하던 8강 구조와 같아집니다. 서버의 while True 안에서는 join()을 쓰지 않습니다.

✔ 확인 기준: python chat_server/server.py 실행 후 "스레드 기반 서버를 시작합니다." 출력과 함께 대기 상태가 되면 완료. while True 안에 accept()가 있는지, threading.Thread 뒤에 start()를 호출했는지 확인하세요.

 

4. 클라이언트 - 반복 송수신 구조 만들기

→ 여러 터미널에서 동시에 실행할 수 있는 chat_client/client.py를 작성합니다.

더보기

이번 강의에서는 서버가 직접 답장을 입력하지 않습니다.스레드 기반 다중 처리 구조에 집중하기 위해 서버는 받은 메시지를 확인 응답으로 돌려줍니다. 클라이언트는 메시지를 보내고 그 응답을 받는 구조입니다.

 

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("보낼 메시지: ")
        client_socket.send(message.encode("utf-8"))   # 메시지를 바이트로 변환해서 보냄

        if message == "exit":                         # exit을 보낸 뒤에는 recv()를 시도하지 않음
            print("서버와의 연결을 종료합니다.")
            break

        data = client_socket.recv(1024)
        response = data.decode("utf-8")               # 받은 바이트를 문자열로 변환
        print(response)

print("클라이언트 소켓을 닫았습니다.")

exit을 입력한 뒤 recv()를 시도하지 않는 것이 중요합니다. 서버가 exit을 받은 뒤 소켓을 닫으므로, 클라이언트가 그 이후에 recv()를 호출하면 오류가 발생합니다. 위 코드에서는 exit을 보낸 즉시 break로 반복문을 빠져나오도록 되어 있습니다.

✔ 확인 기준: 서버가 실행 중인 상태에서 python chat_client/client.py를 실행하면 "서버에 연결되었습니다." 출력과 함께 입력 대기 상태가 되면 완료. 서버가 먼저 실행 중인지, HOSTPORT가 서버와 일치하는지 확인하세요.

 

5. 실행 결과 확인하기

→ 서버 1개와 클라이언트 2개를 실행해 동시 접속이 처리되는지 확인합니다.

더보기

5.1 정상 실행 확인

터미널 1에서 서버를 먼저 실행합니다.

python chat_server/server.py
스레드 기반 서버를 시작합니다.
클라이언트 접속 대기 중...

 

서버가 종료되지 않고 대기 상태로 남아 있으면 정상입니다.

 

터미널 2에서 첫 번째 클라이언트를 실행합니다.

python chat_client/client.py
서버에 연결되었습니다.
종료하려면 exit을 입력하세요.
보낼 메시지: 안녕하세요 A입니다
서버가 받은 메시지: 안녕하세요 A입니다

 

터미널 3에서 두 번째 클라이언트를 실행합니다.

python chat_client/client.py
서버에 연결되었습니다.
종료하려면 exit을 입력하세요.
보낼 메시지: 안녕하세요 B입니다
서버가 받은 메시지: 안녕하세요 B입니다

 

이 시점에서 서버 터미널(터미널 1)의 출력입니다.

스레드 기반 서버를 시작합니다.
클라이언트 접속 대기 중...
클라이언트 접속: ('127.0.0.1', 52344)
클라이언트 처리 시작: ('127.0.0.1', 52344)
현재 실행 중인 스레드 수: 2
('127.0.0.1', 52344) 메시지: 안녕하세요 A입니다
클라이언트 접속: ('127.0.0.1', 52345)
클라이언트 처리 시작: ('127.0.0.1', 52345)
현재 실행 중인 스레드 수: 3
('127.0.0.1', 52345) 메시지: 안녕하세요 B입니다

 

출력에서 확인할 내용입니다. 포트 번호 52344, 52345는 실행할 때마다 달라집니다. 정상입니다.

현재 실행 중인 스레드 수: 2에서 2인 이유는 메인 스레드 1개 + 클라이언트 A 처리 스레드 1개이기 때문입니다. B가 접속하면 3이 됩니다. 여러 스레드가 동시에 실행되면 서버 출력 순서가 섞일 수 있습니다. 정상입니다.

 

이번 강의의 성공 판단 기준: 클라이언트 A가 메시지를 입력하지 않고 기다리는 중에도 클라이언트 B가 서버와 메시지를 주고받을 수 있으면 다중 접속 구조가 동작하는 것입니다.

✔ 확인 기준: 클라이언트 A, B가 각각 서버 응답을 독립적으로 받으면 완료. 한 클라이언트가 입력을 기다리는 중에도 다른 클라이언트가 서버와 통신할 수 있으면 성공입니다. 서버 코드에서 client_thread.start()를 호출했는지, accept()while True 안에 있는지 확인하세요.

 

5.2 흔한 오류와 해결 방법

오류가 나오면 어느 단계에서 실패했는지 먼저 생각합니다. 포트 충돌·파일 경로 오류 등 기본 오류는 4~8강 오류 표를 참고하세요.

오류 상황 원인 및 해결 방법
NameError: name 'threading' is not defined import threading이 없음. 파일 위쪽에 추가한다
target=handle_client()라고 작성 스레드 시작 전에 함수가 바로 실행됨. target=handle_client처럼 괄호 없이 이름만 전달한다
클라이언트를 하나만 처리하고 다음 접속을 못 받음 accept()while True 밖에 있거나, join()start() 직후에 있음. while True 안에서 accept()를 호출하고, 서버 루프 안에서는 join()을 쓰지 않는다
클라이언트 접속은 되지만 서버 응답이 없음 send()exit 분기 안쪽에 있거나 누락됨. 서버 응답 코드가 if message == "exit" 분기 밖에 있는지 확인한다
클라이언트 exit 후 오류 발생 exit 이후에도 클라이언트가 recv()를 시도함. if message == "exit": break 뒤에 recv()가 없는지 확인한다
서버 출력 순서가 섞여 나옴 여러 스레드가 동시에 print()를 호출하는 것. 정상 동작이다. 출력이 섞이는 것은 스레드가 동시에 실행되고 있다는 의미다

 

6. 최종 코드 정리하기

→ 이번 강의에서 완성한 두 파일의 전체 코드를 한곳에 정리합니다.

더보기

6.1 chat_server/server.py

import socket
import threading

HOST = "127.0.0.1"
PORT = 5000

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

            response = f"서버가 받은 메시지: {message}"
            client_socket.send(response.encode("utf-8"))

    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}")

        client_thread = threading.Thread(
            target=handle_client,
            args=(client_socket, client_address)
        )
        client_thread.start()

        print(f"현재 실행 중인 스레드 수: {threading.active_count()}")

 

6.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("보낼 메시지: ")
        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("클라이언트 소켓을 닫았습니다.")

 

6.3 최종 확인 표

확인할 코드 위치 의미
import threading server.py 스레드 모듈 사용
def handle_client(client_socket, client_address): server.py 클라이언트 1명을 처리하는 함수
if not data: break server.py 연결이 갑자기 끊긴 경우 처리
while True: ... server_socket.accept() server.py 새 클라이언트 접속을 계속 받음
threading.Thread(target=handle_client, args=(...)) server.py 클라이언트마다 처리 스레드 생성
client_thread.start() server.py 스레드 실행 시작
if message == "exit": break client.py exit 입력 후 recv() 시도 방지

→ 다음 강의 (12강): clients 리스트를 추가해 접속자 목록을 관리하는 구조를 만듭니다. 클라이언트가 접속하면 목록에 추가하고, 종료하면 목록에서 제거합니다. 이 구조가 완성되면 13강에서 전체 채팅 브로드캐스팅을 구현할 수 있습니다.

 

7. [과제] 접속 시각을 서버 콘솔에 출력하기

→ 11강 코드에 datetime을 추가해 클라이언트가 접속한 시각을 콘솔에 찍고, 스레드가 동시에 도는 모습을 직접 확인합니다.

더보기

7.1 과제 설명과 목표

난이도 ★ · 출발 코드는 11강 최종 chat_server/server.py입니다.

11강 시점에는 아직 clients 목록도, broadcast()도 없습니다. handle_client()가 클라이언트 한 명을 스레드로 처리하기만 하는 가장 단순한 상태입니다.

 

목표는 클라이언트가 접속한 순간의 시각을 서버 콘솔에 한 줄 더 찍는 것입니다.

여러 클라이언트를 거의 동시에 접속시키면 시각이 섞여 찍히는데, 그 모습이 바로 "스레드가 동시에 돈다"는 증거입니다.

 

7.2 사용 라이브러리 — datetime

datetime은 Python 기본 내장 모듈이라 설치가 필요 없습니다. 현재 시각을 구하고 보기 좋은 문자열로 바꾸는 데 사용합니다.

from datetime import datetime          # datetime 클래스를 가져온다

now = datetime.now()                    # 지금 이 순간의 시각을 구한다
text = now.strftime("%H:%M:%S")         # 시:분:초 형태의 문자열로 바꾼다
print(text)                             # 예) 14:23:07
코드 의미
datetime.now() 코드가 실행되는 순간의 날짜와 시각을 담은 객체를 돌려준다
strftime("%Y-%m-%d %H:%M:%S") 연-월-일 시:분:초 형태의 문자열로 변환한다

자주 쓰는 형식 기호는 %Y(연도), %m(월), %d(일), %H(24시간제 시), %M(분), %S(초)입니다.

 

7.3 단계별로 코드 바꾸기

먼저 11강 최종 코드의 맨 위 import 줄에 datetime을 추가합니다.

import socket
import threading
from datetime import datetime          # 접속 시각을 구하기 위해 추가

이제 handle_client() 함수의 첫 부분에서 시각을 구해 출력합니다. 기존 "클라이언트 처리 시작" 줄 바로 아래에 한 줄을 더합니다.

def handle_client(client_socket, client_address):
    connect_time = datetime.now().strftime("%H:%M:%S")   # 이 스레드가 시작된 순간의 시각
    print(f"[{connect_time}] 클라이언트 접속 처리 시작: {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":                        # exit이면 이 클라이언트 종료
                print(f"클라이언트 종료 요청: {client_address}")
                break

            response = f"서버가 받은 메시지: {message}"
            client_socket.send(response.encode("utf-8"))  # 받은 내용을 그대로 되돌려 줌

    print(f"클라이언트 처리 종료: {client_address}")

바뀐 곳은 단 두 줄입니다. 함수 안에서 시각을 구하는 connect_time 줄과, 그 시각을 앞에 붙여 출력하는 print 줄입니다. connect_time을 함수 에서 구하는 것이 핵심입니다. 스레드마다 독립적으로 실행되기 때문에 각 클라이언트의 접속 시각이 따로 기록됩니다.

✓ 확인 기준: 클라이언트를 하나 접속시켰을 때 [14:23:07] 클라이언트 접속 처리 시작: ('127.0.0.1', 5xxxx) 형태로 시각이 함께 찍히면 완료입니다.

 

7.4 최종 코드 (chat_server/server.py)

import socket
import threading
from datetime import datetime

HOST = "127.0.0.1"
PORT = 5000

def handle_client(client_socket, client_address):
    connect_time = datetime.now().strftime("%H:%M:%S")
    print(f"[{connect_time}] 클라이언트 접속 처리 시작: {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

            response = f"서버가 받은 메시지: {message}"
            client_socket.send(response.encode("utf-8"))

    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}")

        client_thread = threading.Thread(
            target=handle_client,
            args=(client_socket, client_address)
        )
        client_thread.start()

        print(f"현재 실행 중인 스레드 수: {threading.active_count()}")
확인할 코드 의미
from datetime import datetime 시각을 다루기 위한 모듈 가져오기
datetime.now().strftime("%H:%M:%S") 현재 시각을 시:분:초 문자열로 변환
함수 에서 connect_time 선언 스레드마다 독립적인 접속 시각 기록

 

7.5 강사 팁 · 동시 실행 체감하기

이 과제의 진짜 목적은 시각 출력이 아니라 스레드의 동시성을 눈으로 보는 것입니다.

클라이언트 터미널 두세 개를 열고 거의 동시에 접속시켜 보세요.

[14:23:07] 클라이언트 접속 처리 시작: ('127.0.0.1', 51001)
[14:23:07] 클라이언트 접속 처리 시작: ('127.0.0.1', 51002)
[14:23:08] 클라이언트 접속 처리 시작: ('127.0.0.1', 51003)   ← 거의 같은 시각이 섞여 찍힘

시각이 거의 같거나 순서가 살짝 뒤바뀌어 찍히는 것이 정상입니다. 만약 한 클라이언트가 끝나야 다음이 찍힌다면 스레드가 아니라 순차 처리되고 있다는 뜻이므로, 메인 루프의 threading.Threadstart()를 다시 확인하세요.

 

중요한 점: connect_time을 함수 밖 전역으로 빼면 모든 클라이언트가 마지막에 갱신된 하나의 시각을 공유하게 됩니다. 스레드별 독립 변수의 중요성을 여기서 미리 느껴 두면, 과제 6에서 버퍼를 함수 안에 두는 이유를 자연스럽게 이해할 수 있습니다.