10강 스레드 개념과 기초 사용법

0. 학습 목표
→ 이번 글에서 무엇을 이해하고, 무엇을 만들고, 무엇을 확인할지 먼저 정리합니다.
0.1 이번 글에서 다룰 내용
이번 글은 개념 중심 강의입니다.
9강에서 현재 서버의 한계를 확인했습니다. 서버가 클라이언트 A와 대화 중이면 B와 C를 자연스럽게 처리하기 어렵고, 이를 해결하려면 클라이언트마다 독립적인 처리 흐름이 필요하다는 것을 이해했습니다. 그 독립적인 처리 흐름을 만드는 도구가 스레드입니다.
이번 강의에서는 아직 다중 접속 서버를 완성하지 않습니다. 대신 스레드가 무엇인지 이해하고, Python의 threading 모듈로 스레드를 만들고 실행하는 방법을 익힙니다. 스레드를 이해하기 위해 thread_basic.py라는 작은 연습 파일을 만들어 순차 실행과 스레드 실행의 차이를 직접 눈으로 확인합니다.
이번 강의에서 익히는 구조는 11강에서 그대로 서버 코드에 이어집니다.
# 10강
thread_basic.py의 work(name)
↓
# 11강
chat_server/server.py의 handle_client(client_socket, client_address)

| 구분 | 내용 |
| 이해할 것 | 스레드가 필요한 이유와 threading.Thread의 기본 구조 |
| 만들 것 | thread_basic.py — 순차 실행과 스레드 실행을 비교하는 연습 파일 (최종 서버 파일 아님) |
| 확인할 것 | 여러 작업의 출력이 순서대로만 나오지 않고 섞여 나타나는지 |
0.2 이번 강의에서 직접 다루는 구조
이번 강의에서는 프로젝트 루트에 thread_basic.py를 만듭니다. 기존 서버·클라이언트 파일은 수정하지 않습니다.
chat_server/
└── server.py ← 8강까지 완성한 파일, 이번 강의에서 수정하지 않음
chat_client/
└── client.py ← 8강까지 완성한 파일, 이번 강의에서 수정하지 않음
# 10강
thread_basic.py ← 이번 강의에서 생성 (스레드 연습용, 최종 서버 파일 아님)
thread_basic.py 안에서 이번 강의에서 다루는 흐름입니다.
thread_basic.py
├── work(name) 함수 정의 → 스레드가 실행할 작업
├── 순차 실행으로 확인 → A 끝난 뒤 B 시작
└── 스레드 실행으로 확인 → A·B 동시 실행, 출력 섞임
1. 스레드 핵심 개념 이해하기
→ 스레드가 무엇인지 정의하고, 채팅 서버에 왜 필요한지 이해합니다.
1.1 지금까지의 서버는 한 흐름으로만 실행되었다
지금까지 작성한 코드는 위에서 아래로 한 줄씩 실행되었습니다. 한 줄이 끝나야 다음 줄이 실행되는 방식입니다. 이 흐름은 이해하기 쉽지만, 서버에는 한 가지 문제가 생깁니다.
클라이언트가 언제 메시지를 보낼지 알 수 없으므로 서버는 recv()에서 계속 기다립니다. 기다리는 동안 서버의 실행 흐름은 그 자리에 묶여 있습니다.
A의 recv() 대기 중
↓ (기다리는 중...)
B도 메시지를 보내고 싶음
↓
서버 흐름은 여전히 A 쪽에 묶여 있음
↓
B는 기다려야 함
채팅 서버에서는 "기다리는 작업"이 클라이언트 수만큼 동시에 일어납니다. 한 흐름만으로는 이 상황을 자연스럽게 처리하기 어렵습니다.
1.2 스레드는 프로그램 안의 작은 실행 흐름이다
스레드 = 하나의 프로그램 안에서 실행되는 별도의 작업 흐름
스레드를 사용하면 서버 프로그램이 여러 개 생기는 것이 아닙니다. 하나의 서버 프로그램 안에서 여러 작업 흐름이 동시에 움직이는 것입니다.
하나의 서버 프로그램
├── 메인 흐름: 새 클라이언트 접속을 기다림
├── 스레드 1: 클라이언트 A의 메시지 처리
├── 스레드 2: 클라이언트 B의 메시지 처리
└── 스레드 3: 클라이언트 C의 메시지 처리
| 용어 | 쉬운 설명 |
| 프로세스 | 실행 중인 프로그램 하나 |
| 스레드 | 프로그램 안에서 움직이는 작업 흐름 |
| 메인 스레드 | 프로그램이 처음 시작될 때 기본으로 실행되는 흐름 |
| 작업 스레드 | 특정 작업을 따로 처리하기 위해 추가로 만든 흐름 |
1.3 채팅 서버에 스레드가 필요한 이유
스레드를 사용하는 이유는 단순히 속도를 높이기 위해서가 아닙니다.
이번 과정에서는 기다리는 작업을 분리하기 위해 스레드를 사용합니다. 각 클라이언트가 언제 메시지를 보낼지 알 수 없으므로, 클라이언트마다 별도 스레드를 두어 각자 기다리게 만드는 것이 핵심입니다.
| 역할 | 하는 일 |
| 메인 스레드 | 새 클라이언트 접속을 계속 받음 (accept() 반복) |
| 클라이언트 처리 스레드 | 특정 클라이언트의 메시지를 기다리며 처리 |
✔ 확인 기준: 채팅 서버에서 스레드가 "속도"가 아니라 "기다리는 작업의 분리"를 위해 필요하다고 설명할 수 있으면 완료. 스레드가 서버 프로그램을 여러 개 실행하는 것이라고 오해하고 있지 않은지 확인하세요.
2. 비유와 기본 형태로 구조 이해하기
→ 주방 비유로 순차 실행과 스레드 실행의 차이를 이해하고, threading.Thread 기본 형태를 확인합니다.
2.1 주방 요리사로 비유하기
순차 실행은 요리사 한 명이 A 요리를 끝낸 뒤에야 B 요리를 시작하는 것과 같습니다. A 손님은 빠르게 받지만 B 손님은 A가 끝날 때까지 기다려야 합니다. 스레드 실행은 A 요리와 B 요리를 동시에 진행하는 것과 같습니다. A 냄비에 불을 올린 뒤 기다리는 시간 동안 B 재료를 손질할 수 있습니다.

| 주방 비유 | 서버 역할 |
| 요리사 한 명이 순서대로 요리 | 순차 실행 — 한 클라이언트가 끝날 때까지 대기 |
| 여러 냄비를 동시에 올림 | 스레드 실행 — 각 클라이언트를 독립 흐름으로 처리 |
| 불 올리고 기다리는 시간 | recv() 대기 — 이 시간에 다른 클라이언트 처리 가능 |
| 냄비마다 다른 요리 진행 | 클라이언트마다 다른 스레드가 처리 |
2.2 Python에서 스레드를 만드는 기본 형태
Python에서는 threading 모듈을 사용합니다. 기본 형태는 다음과 같습니다.
import threading
def work():
print("스레드에서 실행할 작업입니다.")
thread = threading.Thread(target=work) # 스레드 객체 생성 — 아직 실행 안 됨
thread.start() # 스레드 실제 시작
| 코드 | 의미 |
threading.Thread(target=work) |
새 스레드 객체를 만든다. work 함수를 실행할 준비를 한다 |
target=work |
스레드가 실행할 함수 이름을 지정한다 |
thread.start() |
스레드를 실제로 시작한다. 이 줄 이후 함수가 별도 흐름에서 실행된다 |
가장 자주 하는 실수는 target에 함수 이름 뒤에 괄호를 붙이는 것입니다.
# 잘못된 형태 — work()가 바로 호출되고, 그 반환값(None)이 target에 전달됨
threading.Thread(target=work())
# 올바른 형태 — work 함수 자체를 스레드에 넘기고, 스레드가 나중에 실행
threading.Thread(target=work)
target=work()로 작성하면 스레드를 만드는 그 줄에서 work()가 바로 실행되어 버립니다. 에러 메시지도 바로 나오지 않아서 왜 스레드가 예상대로 동작하지 않는지 한참 헤매게 됩니다. target= 뒤에는 항상 괄호 없이 함수 이름만 씁니다. 나중에 handle_client(client_socket)처럼 인자가 있는 함수를 넘길 때도 마찬가지입니다.
2.3 스레드 실행 순서

① threading.Thread(target=함수) — 스레드 객체 생성 (아직 실행 안 됨)
↓
② thread.start() — 스레드 실제 시작
↓
③ 메인 흐름과 스레드 흐름이 동시에 진행됨
↓
④ thread.join() (선택) — 스레드가 끝날 때까지 메인 흐름이 기다림
join()은 "이 스레드가 완전히 끝날 때까지 기다려라"는 의미입니다. 이번 예제에서는 "모든 작업이 끝났습니다."를 정확히 마지막에 출력하기 위해 사용합니다. 다중 접속 채팅 서버에서는 스레드가 클라이언트 연결이 끊길 때까지 계속 실행되므로 join()을 쓰지 않는 경우도 많습니다.
✔ 확인 기준: target=work와 target=work()의 차이를 설명할 수 있고, start()와 join()의 역할을 구분할 수 있으면 완료.
3. 실습 준비하기
→ 프로젝트 루트에 thread_basic.py 파일을 만듭니다.
터미널에서 프로젝트 루트 폴더로 이동한 뒤 파일을 만듭니다.
touch thread_basic.py
threading과 time은 Python 기본 라이브러리이므로 별도 설치가 필요하지 않습니다. Python이 실행되는지 먼저 확인합니다.
python --version
Python 3.12.0
✔ 확인 기준: 에디터에서 thread_basic.py 파일이 열리면 완료. 현재 위치가 프로젝트 루트인지 확인하세요.
4. 순차 실행과 스레드 실행 비교하기
→ thread_basic.py에 코드를 작성하며 순차 실행과 스레드 실행의 차이를 직접 눈으로 확인합니다.
4.1 순차 실행으로 먼저 확인하기
thread_basic.py에 다음 코드를 작성합니다.
import time
def work(name):
for i in range(3):
print(f"{name} 작업 중: {i + 1}")
time.sleep(1) # 1초 기다림 — 클라이언트가 메시지를 보낼 때까지 기다리는 상황과 비슷
work("A")
work("B")
print("모든 작업이 끝났습니다.")
실행합니다.
python thread_basic.py
A 작업 중: 1
A 작업 중: 2
A 작업 중: 3
B 작업 중: 1
B 작업 중: 2
B 작업 중: 3
모든 작업이 끝났습니다.
A의 작업 1~3이 모두 출력된 뒤에야 B의 작업이 시작됩니다. 채팅 서버에서 이 구조를 그대로 사용하면 클라이언트 A가 종료할 때까지 B는 처리되지 않습니다.
4.2 스레드로 바꿔서 실행하기
thread_basic.py를 다음처럼 수정합니다. import threading을 추가하고, 두 work() 호출을 스레드로 바꿉니다.
import threading
import time
def work(name):
for i in range(3):
print(f"{name} 작업 중: {i + 1}")
time.sleep(1)
thread_a = threading.Thread(target=work, args=("A",)) # 스레드 생성 — 아직 실행되지 않음
thread_b = threading.Thread(target=work, args=("B",)) # 스레드 생성 — 아직 실행되지 않음
thread_a.start() # A 스레드 시작 — 독립 흐름으로 실행 시작
thread_b.start() # B 스레드 시작 — 독립 흐름으로 실행 시작
thread_a.join() # A 스레드가 끝날 때까지 메인 흐름이 기다림
thread_b.join() # B 스레드가 끝날 때까지 메인 흐름이 기다림
print("모든 작업이 끝났습니다.")
실행합니다.
python thread_basic.py
A 작업 중: 1
B 작업 중: 1
A 작업 중: 2
B 작업 중: 2
A 작업 중: 3
B 작업 중: 3
모든 작업이 끝났습니다.
A가 모두 끝난 뒤 B가 시작되는 것이 아니라, 두 작업의 출력이 섞여 나타납니다. 실행 환경에 따라 A와 B의 순서가 조금 달라질 수 있습니다. 중요한 것은 출력이 "A 세 번, B 세 번"으로 완전히 나뉘지 않고 섞인다는 점입니다. 이것이 두 작업이 동시에 진행되고 있다는 증거입니다.
4.3 args — 스레드 함수에 값 전달하기
work(name) 함수는 name이라는 인자가 필요합니다. 스레드에서 이 값을 전달하려면 args를 사용합니다. args는 반드시 튜플 형태여야 합니다.
# 잘못된 형태 — 튜플이 아니라 문자열로 전달됨, 예상치 못한 동작 가능
args=("A")
# 올바른 형태 — 값이 하나여도 쉼표를 붙여 튜플로 만듦
args=("A",)
값이 두 개 이상이면 자연스럽게 튜플이 됩니다. 값이 하나일 때만 쉼표를 빠뜨리기 쉬우므로 주의합니다.
# 인자가 두 개인 경우 — 쉼표 없어도 튜플
args=(client_socket, client_address)
# 인자가 하나인 경우 — 쉼표 필수
args=(client_socket,)
4.4 join — 스레드가 끝날 때까지 기다리기

import threading
import time
def work(name):
for i in range(3):
print(f"{name} 작업 중: {i + 1}")
time.sleep(1)
thread_a = threading.Thread(target=work, args=("A",))
thread_b = threading.Thread(target=work, args=("B",))
thread_a.start()
thread_b.start()
work("메인 스레드") # 추가
thread_a.join() # join 제거 후 테스트
thread_b.join() # join 제거 후 테스트
print("모든 작업이 끝났습니다.")
join()은 해당 스레드가 완전히 끝날 때까지 메인 흐름이 그 자리에서 기다리게 합니다. 이번 예제에서는 "모든 작업이 끝났습니다."를 정확히 마지막에 출력하기 위해 사용합니다. 다음 강의에서 만드는 다중 접속 채팅 서버에서는 클라이언트 처리 스레드가 연결이 끊길 때까지 계속 실행되므로, 메인 흐름에서 join()을 기다리지 않는 경우도 있습니다.
✔ 확인 기준: 실행 후 A와 B 출력이 섞여 나타나면 완료. 순서가 예시와 조금 달라도 괜찮습니다. args=("A",)와 args=("A")의 차이를 설명할 수 있는지, target=work()처럼 괄호를 붙이지 않았는지 확인하세요.
5. 흔한 오해와 오류 정리하기
→ 스레드 코드에서 자주 만나는 오류와 스레드에 대한 오해를 정리합니다.
5.1 자주 만나는 오류
오류가 나오면 어느 단계에서 실패했는지 먼저 생각합니다.
| 오류 상황 | 원인 및 해결 방법 |
target=work()라고 작성 |
스레드가 실행하기 전에 함수가 바로 호출됨. target=work처럼 괄호 없이 함수 이름만 전달한다 |
args=("A")라고 작성 |
문자열 하나가 튜플로 전달되지 않음. args=("A",)처럼 쉼표를 포함한다 |
| 출력 순서가 예시와 다름 | 스레드 실행 순서는 항상 고정되지 않음. 출력이 섞여 나오면 성공으로 본다 |
| 프로그램이 바로 끝난 것처럼 보임 | start() 호출이 없거나 작업 시간이 너무 짧음. start() 호출 여부와 time.sleep()이 있는지 확인한다 |
NameError: name 'threading' is not defined |
threading 모듈을 import하지 않음. 파일 위쪽에 import threading을 추가한다 |
5.2 스레드에 대한 흔한 오해
| 오해 | 올바른 정리 |
| 스레드를 쓰면 항상 프로그램이 빨라진다 | 항상 그렇지는 않다. 이번 과정에서는 기다리는 작업을 분리하기 위해 사용한다 |
| 스레드는 프로그램을 여러 개 실행하는 것이다 | 하나의 프로그램 안에서 여러 작업 흐름을 만드는 것이다 |
| 스레드를 만들면 자동으로 채팅방이 완성된다 | 클라이언트 처리 흐름을 분리하는 단계이며, 접속자 관리와 브로드캐스팅은 이후 강의에서 다룬다 |
start() 없이도 스레드가 실행된다 |
스레드 객체를 만든 뒤 반드시 start()를 호출해야 실행된다 |
6. 최종 코드 정리하기
→ 이번 강의에서 완성한 thread_basic.py 전체 코드를 한곳에 정리합니다.
6.1 thread_basic.py
import threading
import time
def work(name):
for i in range(3):
print(f"{name} 작업 중: {i + 1}")
time.sleep(1)
thread_a = threading.Thread(target=work, args=("A",))
thread_b = threading.Thread(target=work, args=("B",))
thread_a.start()
thread_b.start()
thread_a.join()
thread_b.join()
print("모든 작업이 끝났습니다.")
6.2 최종 확인 표
| 확인할 코드 | 의미 |
import threading |
스레드 기능을 제공하는 모듈 불러오기 |
def work(name) |
스레드가 실행할 함수 정의 |
threading.Thread(target=work, args=("A",)) |
스레드 객체 생성. work 함수에 "A"를 전달 |
thread_a.start() |
스레드를 독립 흐름으로 시작 |
thread_a.join() |
스레드가 끝날 때까지 메인 흐름이 기다림 |
이번 강의에서 만든 thread_basic.py는 최종 서버 파일이 아닙니다. 여기서 익힌 threading.Thread(target=함수, args=(값,)) 구조가 11강에서 그대로 handle_client() 스레드 생성 코드로 이어집니다.
이번 강의: threading.Thread(target=work, args=("A",))
11강: threading.Thread(target=handle_client, args=(client_socket, client_address))
→ 다음 강의 (11강): chat_server/server.py에 threading.Thread를 적용합니다. 클라이언트가 접속할 때마다 handle_client() 스레드를 만들어, 여러 클라이언트를 동시에 처리하는 서버를 완성합니다. 이번 강의의 thread_basic.py 구조가 그대로 확장됩니다.