34강. 파일 수신과 저장 처리

0. 학습 목표
→ 서버가 file_info · file_chunk · file_end 메시지를 처리해 received_files/ 폴더에 파일을 저장하는 수신 쪽을 완성합니다.
0.1 이번 글에서 다룰 내용
이번 글은 구현 중심 강의입니다.
33강에서 파일을 보내는 쪽(send_file())을 완성했습니다.
서버에는 아직 파일 메시지를 처리하는 코드가 없었기 때문에 파일 조각들이 수신되고 사라졌습니다.
이번 강의에서는 받는 쪽을 완성합니다. 세 가지 핸들러 함수를 추가해 메시지 흐름을 저장 흐름으로 연결합니다.

file_info 수신 → 저장 파일 준비 (파일 열기)
file_chunk 수신 → Base64 디코딩 → 파일에 이어 쓰기 ← 반복
file_end 수신 → 파일 닫기 → 저장 완료
| 구분 | 내용 |
| 이해할 것 | 파일 조각이 도착할 때마다 Base64를 디코딩해 파일에 이어 써야 한다는 것, receiving_files 딕셔너리로 상태를 관리하는 이유 |
| 만들 것 | server.py에 파일 수신 상태 딕셔너리와 세 가지 핸들러 함수 추가 |
| 확인할 것 | 전송한 파일이 chat_server/received_files/ 폴더에 같은 이름으로 저장되는지 |
0.2 이번 강의에서 직접 다루는 구조
received_files/ 폴더는 server.py를 처음 실행할 때 os.makedirs()가 자동으로 만듭니다. 이번 강의에서 수정하는 파일은 server.py 하나입니다.
chat_server/
├── protocol.py
├── server.py ← ✏️ 이번 강의에서 수정
└── received_files/ ← ➕ 서버 실행 시 자동 생성
chat_client/
├── protocol.py
├── client.py
├── main.py
└── downloads/
└── received_files/
(➕ 새로 생성 · ✏️ 수정 · 표시 없음은 변경 없음)
이번 강의에서 server.py에 추가할 구성입니다.
server.py (추가 사항)
├── import base64, os 추가
├── RECEIVE_DIR ← 저장 폴더 경로 상수
├── receiving_files ← file_id별 수신 상태 관리 딕셔너리
├── handle_file_info() ← 저장 파일 열기, 수신 상태 등록
├── handle_file_chunk() ← Base64 디코딩, 파일에 이어 쓰기
└── handle_file_end() ← 파일 닫기, 수신 상태 제거
1. 실습 준비하기
→ 저장 폴더 경로와 수신 상태 딕셔너리를 정의하고, receiving_files가 왜 필요한지 이해합니다.
server.py 상단에 두 가지를 추가합니다.
RECEIVE_DIR는 파일을 저장할 폴더 경로이고,
receiving_files는 진행 중인 파일 전송 상태를 기억하는 딕셔너리입니다.
import base64 # ➕ Base64 디코딩
import os # ➕ 파일·폴더 처리
RECEIVE_DIR = os.path.join( # ➕ 저장 폴더 경로
os.path.dirname(os.path.abspath(__file__)), "received_files"
)
receiving_files = {} # ➕ 수신 상태 관리 딕셔너리
os.makedirs(RECEIVE_DIR, exist_ok=True) # ➕ 폴더가 없으면 자동 생성
os.path.dirname(os.path.abspath(__file__))는 server.py 파일이 있는 폴더를 가리킵니다.
상대 경로 대신 이 방식을 쓰면 어디서 서버를 실행하든 항상 chat_server/received_files/에 파일이 저장됩니다.
receiving_files가 필요한 이유가 있습니다.
파일 전송은 file_info → file_chunk → file_end 세 종류의 메시지가 따로따로 도착합니다.
file_info에서 파일을 열고, 이후 도착하는 file_chunk마다 같은 파일 객체에 계속 써야 합니다.
이 파일 객체를 함수 호출 사이에 기억해 두는 수단이 receiving_files입니다.
# receiving_files의 구조 예시
{
"photo.png_1718000000000": { # file_id (파일명+타임스탬프)
"filename": "photo.png",
"path": "/path/to/received_files/photo.png",
"file": <파일 객체>, # open()으로 열어 둔 상태
"received_size": 0,
"size": 204800
}
}
✔ 확인 기준: RECEIVE_DIR와 receiving_files = {}가 server.py 상단에 추가되고, import base64와 import os가 있으면 완료. 서버 실행 후 chat_server/received_files/ 폴더가 생성되는지 확인하세요.
2. server.py — handle_file_info() 만들기
→ file_info 메시지를 받으면 저장 파일을 열고 수신 상태를 등록합니다.

file_info 메시지가 도착하면 세 가지 일을 합니다. 필
수 정보를 확인하고, 저장 경로를 결정하고, 파일을 "wb" 모드로 열어 receiving_files에 등록합니다.
os.path.basename(filename)으로 파일 이름을 정제하는 것은 보안을 위해서입니다.
만약 보내는 쪽이 "../../etc/passwd"처럼 경로가 섞인 값을 보내도, basename()이 파일 이름 부분만 남겨 의도치 않은 위치에 파일이 저장되는 것을 방지합니다.
def handle_file_info(message): # ➕
file_id = message.get("file_id")
filename = message.get("filename")
size = message.get("size", 0)
if not file_id or not filename: # 필수 정보 누락 시 처리
print("파일 정보가 올바르지 않습니다.")
return
safe_filename = os.path.basename(filename) # 경로 제거 — 파일 이름만 안전하게 추출
save_path = os.path.join(RECEIVE_DIR, safe_filename)
file = open(save_path, "wb") # 바이너리 쓰기 모드로 파일 열기
receiving_files[file_id] = { # file_id 기준으로 수신 상태 등록
"filename": safe_filename,
"path": save_path,
"file": file,
"received_size": 0,
"size": size
}
print(f"파일 수신 시작: {safe_filename}, 크기: {size} bytes")
파일을 "wb" 모드로 여는 시점이 중요합니다. 이 파일 객체를 receiving_files에 저장해 두면, 이후 file_chunk가 올 때마다 같은 파일에 이어 쓸 수 있습니다. 파일을 매번 열고 닫지 않아도 됩니다.
✔ 확인 기준: handle_file_info()가 호출된 뒤 receiving_files에 해당 file_id가 등록되고, received_files/ 폴더 안에 빈 파일이 생성되면 완료.
3. server.py — handle_file_chunk()와 handle_file_end() 만들기
→ 파일 조각을 복원해 파일에 쓰고, 전송 완료 시 파일을 닫는 함수를 작성한 뒤 메시지 분기에 연결합니다.
3.1 handle_file_chunk() 만들기

file_chunk가 올 때마다 data 필드의 Base64 문자열을 바이너리로 복원해 파일에 씁니다. JSON 파싱 결과로 꺼낸 data는 이미 str이므로 base64.b64decode()에 그대로 전달할 수 있습니다.
def handle_file_chunk(message): # ➕
file_id = message.get("file_id")
data = message.get("data")
info = receiving_files.get(file_id) # file_id로 수신 중인 파일 찾기
if info is None:
print(f"알 수 없는 파일 조각입니다: {file_id}")
return
if data is None:
print(f"파일 조각 데이터가 없습니다: {file_id}")
return
chunk = base64.b64decode(data) # Base64 문자열 → 바이너리 복원
info["file"].write(chunk) # 복원한 바이너리를 파일에 이어 쓰기
info["received_size"] += len(chunk) # 받은 크기 누적
print(
f"파일 조각 저장: {info['filename']} "
f"({info['received_size']}/{info['size']} bytes)"
)
조각이 도착할 때마다 info["file"].write(chunk)가 호출됩니다. 파일 객체가 이미 열려 있으므로 자동으로 파일 끝에 이어 붙여집니다.
3.2 handle_file_end() 만들기

file_end를 받으면 열어 두었던 파일을 닫고 수신 상태에서 제거합니다. close()를 빠뜨리면 OS 버퍼에 남아 있는 데이터가 디스크에 기록되지 않아, 저장된 파일이 손상되거나 열리지 않을 수 있습니다.
def handle_file_end(message): # ➕
file_id = message.get("file_id")
info = receiving_files.get(file_id)
if info is None:
print(f"완료할 파일을 찾을 수 없습니다: {file_id}")
return
info["file"].close() # 파일 닫기 — 반드시 호출해야 저장 완료
print(f"파일 저장 완료: {info['path']}")
del receiving_files[file_id] # 수신 상태에서 제거
3.3 메시지 분기에 연결하기
handle_client()의 메시지 처리 분기 안에 세 줄을 추가합니다. 기존에 파일 메시지를 처리하지 못하던 자리입니다.
if message_type == "file_info": # ➕
handle_file_info(message)
continue
if message_type == "file_chunk": # ➕
handle_file_chunk(message)
continue
if message_type == "file_end": # ➕
handle_file_end(message)
continue
세 분기가 추가되면 서버의 파일 수신 흐름이 완성됩니다.
file_info → handle_file_info() → 파일 열기, receiving_files 등록
file_chunk → handle_file_chunk() → Base64 디코딩, 파일에 쓰기 ← 반복
file_end → handle_file_end() → 파일 닫기, receiving_files 제거
✔ 확인 기준: 세 함수가 모두 작성되고 handle_client()의 분기에 연결되었으면 완료. handle_file_end()에서 file.close()가 반드시 호출되는지 확인하세요.
4. 실행 결과 확인하기
→ 서버를 실행하고 33강에서 만든 테스트 스크립트로 파일을 전송해, received_files/ 폴더에 저장되는지 확인합니다.
4.1 정상 실행 확인

서버를 먼저 실행한 뒤 33강에서 만든 test_send_file.py로 파일을 보냅니다.
# 터미널 1 — 서버 실행
python chat_server/server.py
# 터미널 2 — 테스트 스크립트로 파일 전송
python test_send_file.py sample.png
서버 터미널에서 다음 순서로 출력되면 성공입니다. 파일 크기와 CHUNK_SIZE에 따라 조각 수는 달라집니다.
파일 수신 시작: sample.png, 크기: 204800 bytes
파일 조각 저장: sample.png (65536/204800 bytes)
파일 조각 저장: sample.png (131072/204800 bytes)
파일 조각 저장: sample.png (196608/204800 bytes)
파일 조각 저장: sample.png (204800/204800 bytes)
파일 저장 완료: .../chat_server/received_files/sample.png
저장이 끝나면 chat_server/received_files/ 폴더에 파일이 생깁니다.
chat_server/received_files/
└── sample.png
저장된 파일을 직접 열어 원본과 같은지 확인합니다. 이미지 파일은 그림이 정상적으로 보여야 하고, 텍스트 파일은 글자가 그대로여야 합니다.
✔ 확인 기준: 서버 터미널에 "파일 저장 완료" 메시지가 출력되고, chat_server/received_files/에 원본과 동일한 파일이 생성되면 완료. 저장된 파일을 열어 내용이 손상되지 않았는지 반드시 확인하세요.
4.2 흔한 오류와 해결 방법
| 오류 상황 | 원인 및 해결 방법 |
NameError: base64 |
import base64가 server.py 맨 위에 없습니다. 추가하세요 |
NameError: os |
import os가 없습니다. 추가하세요 |
FileNotFoundError |
os.makedirs(RECEIVE_DIR, exist_ok=True) 호출이 누락됐습니다. RECEIVE_DIR 정의 바로 아래에 있는지 확인하세요 |
| 저장된 파일이 열리지 않거나 내용이 깨짐 | "wb" 모드가 아니거나 handle_file_end()에서 file.close()가 호출되지 않았습니다 |
| 파일 크기가 원본보다 작음 | file_chunk 일부가 저장되지 않았거나 file_end 전에 연결이 끊겼습니다. 서버 로그에서 모든 조각이 수신됐는지 확인하세요 |
| "완료할 파일을 찾을 수 없습니다" 출력 | file_info가 오지 않은 채 file_end가 도착하거나, file_id가 맞지 않습니다. 33강의 send_file()에서 file_id가 일관되게 생성되는지 확인하세요 |
| 같은 이름의 파일을 보내면 덮어씀 | 저장 경로가 동일하기 때문입니다. 학습 단계에서는 허용하며, 이후 파일명 중복 처리를 추가할 수 있습니다 |
5. 최종 코드 정리하기
→ server.py에 추가된 전체 코드를 한곳에 정리합니다.
5.1 server.py — 추가된 코드 전체
아래 코드에서 상단의 import와 상수는 server.py 맨 위에, 세 함수는 기존 함수들과 나란히, 메시지 분기 세 줄은 handle_client() 안에 추가합니다.
import base64
import os
RECEIVE_DIR = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "received_files"
)
receiving_files = {}
os.makedirs(RECEIVE_DIR, exist_ok=True)
def handle_file_info(message):
file_id = message.get("file_id")
filename = message.get("filename")
size = message.get("size", 0)
if not file_id or not filename:
print("파일 정보가 올바르지 않습니다.")
return
safe_filename = os.path.basename(filename)
save_path = os.path.join(RECEIVE_DIR, safe_filename)
file = open(save_path, "wb")
receiving_files[file_id] = {
"filename": safe_filename,
"path": save_path,
"file": file,
"received_size": 0,
"size": size
}
print(f"파일 수신 시작: {safe_filename}, 크기: {size} bytes")
def handle_file_chunk(message):
file_id = message.get("file_id")
data = message.get("data")
info = receiving_files.get(file_id)
if info is None:
print(f"알 수 없는 파일 조각입니다: {file_id}")
return
if data is None:
print(f"파일 조각 데이터가 없습니다: {file_id}")
return
chunk = base64.b64decode(data)
info["file"].write(chunk)
info["received_size"] += len(chunk)
print(
f"파일 조각 저장: {info['filename']} "
f"({info['received_size']}/{info['size']} bytes)"
)
def handle_file_end(message):
file_id = message.get("file_id")
info = receiving_files.get(file_id)
if info is None:
print(f"완료할 파일을 찾을 수 없습니다: {file_id}")
return
info["file"].close()
print(f"파일 저장 완료: {info['path']}")
del receiving_files[file_id]
# handle_client() 메시지 분기에 추가
if message_type == "file_info":
handle_file_info(message)
continue
if message_type == "file_chunk":
handle_file_chunk(message)
continue
if message_type == "file_end":
handle_file_end(message)
continue
5.2 최종 확인 표
| 추가된 코드 | 역할 |
import base64, import os |
Base64 복원과 파일·폴더 처리에 필요한 모듈 |
os.path.dirname(os.path.abspath(__file__)) |
실행 위치에 상관없이 server.py 기준으로 received_files/ 경로를 결정 |
receiving_files = {} |
file_id별 수신 상태(파일 객체, 크기 등)를 함수 호출 사이에 기억 |
handle_file_info() |
저장 파일을 "wb"로 열고 receiving_files에 등록 |
handle_file_chunk() |
Base64 문자열(str)을 b64decode()로 직접 복원해 파일에 이어 쓰기 |
handle_file_end() |
파일 close(), receiving_files에서 제거 |
os.path.basename(filename) |
경로가 섞인 파일명에서 파일 이름만 안전하게 추출 |
→ 다음 강의 (35강): 파일 전송·수신이 동작하게 됐습니다. 35강에서는 GUI 쪽을 연결합니다. 파일 선택 다이얼로그 버튼을 main.py에 추가하고, ChatClient.send_file()을 호출해 사용자가 UI에서 직접 파일을 보낼 수 있도록 합니다.
파일 수신 구현, handle_file_info, handle_file_chunk, handle_file_end, receiving_files, base64.b64decode, os.makedirs, received_files, 파일 조각 저장, RECEIVE_DIR