31강 개념 중심 ⏱ 약 20분

 

0. 학습 목표

→ 텍스트와 파일을 네트워크로 보낼 때 무엇이 다른지, 왜 메타데이터가 필요한지 이해합니다.

더보기

0.1 이번 글에서 다룰 내용

이번 글은 개념 중심 강의입니다.

 

30강까지 문자열 메시지를 encode("utf-8")로 바이트로 바꿔 보냈습니다.

파일도 결국 바이트로 보내는 것은 같습니다.

그러나 텍스트 메시지와 파일 데이터는 해석 방식과 전송 규칙이 다릅니다.

이번 강의에서는 코드를 작성하기 전에 그 차이를 이해합니다.

 

택배에 비유하면, 텍스트는 엽서입니다. 글자 하나하나의 의미가 그대로 보여 누구나 읽을 수 있습니다.

파일은 봉인된 택배 상자입니다. 파일의 내용물은 하나하나의 의미가 없이 전체가 하나의 의미가 됩니다.

 

구분 내용
이해할 것 텍스트와 바이너리는 모두 바이트로 전송되지만 해석 방식과 처리 규칙이 다르다는 점
정리할 것 encode()/decode()·"rb"/"wb" 모드·메타데이터 필요성·판단 기준
확인할 것 파일 이름·크기(메타데이터)와 파일 내용(바이너리)을 따로 전달해야 하는 이유

 

0.2 앞으로 만들 프로젝트 구조 미리보기

이번 강의는 개념 강의라 기존 채팅 코드를 수정하지 않습니다. 파일 송수신 파트에서 사용할 구조를 미리 살펴봅니다.

chat_server/
├── protocol.py
└── server.py

chat_client/
├── protocol.py
├── client.py
├── main.py
└── downloads/
    └── received_files/        ← (이후 파일 수신 강의에서 생성 예정)

파일 전송의 핵심 구조는 다음과 같습니다.

텍스트 메시지는 내용이 하나지만, 파일은 "정보"와 "내용"이 반드시 나뉩니다.

텍스트 메시지
└── {"type": "chat", "content": "안녕하세요"}    ← 하나의 JSON으로 전달

파일 전송
├── 메타데이터(JSON 텍스트): 파일 이름, 파일 크기    ← 먼저 전달
└── 바이너리 데이터: 파일 내용 자체                 ← 나중에 전달

 

1. 핵심 개념 이해하기

→ encode/decode와 바이너리 읽기 모드가 왜 다른지, 언제 어느 쪽을 써야 하는지 이해합니다.

더보기

1.1 텍스트 데이터 — encode와 decode

30강까지 채팅 메시지를 보낼 때 항상 encode("utf-8")을 거쳤습니다.

문자열은 사람이 읽는 형식이고, 네트워크는 바이트만 전달할 수 있어서입니다.

받는 쪽은 decode("utf-8")로 다시 문자열로 되돌립니다.

문자열 → encode("utf-8") → 바이트 → send() → recv() → 바이트 → decode("utf-8") → 문자열

이 흐름은 UTF-8이라는 약속된 규칙을 양쪽이 알기 때문에 가능합니다.

같은 방식으로 인코딩하면 반드시 같은 방식으로 디코딩해야 원래 문자열로 돌아옵니다.

 

1.2 바이너리 데이터 — encode/decode를 쓸 수 없는 이유

이미지·PDF·압축 파일은 UTF-8 문자열 규칙을 따르지 않는 바이트 덩어리입니다.

이 데이터를 억지로 decode("utf-8")하면 UnicodeDecodeError가 납니다.

택배 상자 안 물건을 "글자로 읽으려" 하는 것과 같습니다. 그래서 파일을 열 때 모드를 다르게 씁니다.

모드 의미 사용 예
"r" 텍스트 읽기 .txt, .csv 같은 텍스트 파일
"rb" 바이너리 읽기 .png, .pdf, .zip 같은 파일 — 파일 전송 시 송신 측
"w" 텍스트 쓰기 문자열 저장
"wb" 바이너리 쓰기 받은 파일 저장 — 파일 전송 시 수신 측

파일을 "rb"로 읽으면 bytes 객체가 반환됩니다. 이 bytesencode()/decode() 없이 그대로 소켓으로 보내고 그대로 파일에 저장합니다.

✔ 확인 기준: "텍스트 파일은 "r", 이미지·PDF 같은 파일은 "rb"로 읽고, 받은 파일은 "wb"로 저장한다. 이미지를 decode("utf-8")하면 UnicodeDecodeError가 난다"고 설명할 수 있으면 완료.

 

2. 메타데이터가 필요한 이유

→ 파일 내용만 보내면 무엇이 문제인지, 메타데이터로 어떻게 해결하는지 확인합니다.

더보기

파일 내용만 덜렁 보내면 받는 쪽은 다음 세 가지를 알 수 없습니다.

이 파일의 이름이 무엇인가?
얼마나 받아야 파일 하나가 끝나는가?
받은 데이터가 다 온 것인가, 아직 더 오는 것인가?

그래서 파일 내용보다 먼저 파일 정보(송장)를 JSON으로 보냅니다. 이것을 메타데이터라고 합니다.

{
    "type": "file_info",
    "filename": "photo.png",
    "size": 204800
}

받는 쪽은 이 메시지를 보고 "photo.png라는 파일이 204,800바이트 온다"고 준비합니다. 그 다음 바이너리 데이터 조각이 도착하고, size만큼 받으면 파일 하나가 완성됩니다.

1) 메타데이터(JSON 텍스트) 전송  →  "photo.png, 204800바이트가 갈게"
        ↓
2) 바이너리 데이터 조각 전송      →  실제 파일 내용
        ↓
3) size만큼 다 받으면 완료        ←  받는 쪽이 끝을 판단하는 기준

메타데이터와 바이너리를 따로 보내는 이유는 이렇습니다.

메타데이터는 기존 protocol.pysend_message()로 JSON 텍스트 메시지처럼 보낼 수 있습니다.

파일 내용은 send_message()를 쓸 수 없고(바이너리이므로) 소켓으로 직접 send()해야 합니다.

두 가지가 전송 방식 자체가 다르기 때문에 반드시 나뉩니다.

✔ 확인 기준: "파일 이름·크기는 JSON 텍스트 메시지(메타데이터)로, 파일 내용은 바이너리로 따로 전달하며, 받는 쪽은 size만큼 받으면 끝났다고 판단한다"고 설명할 수 있으면 완료.

 

3. 판단 기준 정리하기

→ 언제 텍스트로 다루고 언제 바이너리로 다룰지 기준과, 자주 하는 오해를 정리합니다.

더보기

3.1 상황별 처리 방식

상황 처리 방식
사용자가 입력한 채팅 문장 텍스트 — encode()/decode()
JSON 메시지 구조 (chat, whisper 등) 텍스트 — send_message() 그대로
파일 이름, 파일 크기 텍스트 — JSON 안에 담아 send_message()로 전달
이미지·PDF·zip 파일 내용 바이너리 — "rb"로 읽고 소켓으로 직접 전송
받은 파일 저장 바이너리 — "wb"로 저장

 

3.2 자주 하는 오해

오해 올바른 이해
파일 이름만 보내면 파일 전송이 된다 파일 이름은 정보(메타데이터)이고 내용이 아닙니다. 이름과 내용을 반드시 따로 보내야 합니다
파일 전체를 한 번에 보내면 항상 된다 큰 파일은 메모리·네트워크에 부담이 됩니다. 조각으로 나누어 보내는 방식이 필요합니다
바이너리 데이터가 깨져 보이면 오류다 바이너리 데이터는 사람이 글자로 읽기 어려울 뿐입니다. 이미지 뷰어·PDF 뷰어가 올바르게 해석합니다. decode()를 시도하지 말고 그대로 저장하세요

 

4. 예시로 검토하기

→ 문자열의 encode/decode와 파일 바이너리 읽기를 직접 확인하고, 메타데이터 추출까지 연결합니다.

더보기

4.1 문자열과 바이트 왕복하기

텍스트가 바이트로 바뀌고 다시 문자열로 돌아오는 과정입니다.

text = "안녕하세요"
data = text.encode("utf-8")    # 문자열 → 바이트

print(text)   # 안녕하세요
print(data)   # b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'

첫 줄은 사람이 읽는 문자열이고, 두 번째 줄은 네트워크로 보낼 수 있는 바이트 표현입니다. 같은 인코딩 방식으로 디코딩하면 문자열로 돌아옵니다.

data = b'\xec\x95\x88\xeb\x85\x95\xed\x95\x98\xec\x84\xb8\xec\x9a\x94'
text = data.decode("utf-8")    # 바이트 → 문자열
print(text)   # 안녕하세요

 

4.2 파일을 바이너리로 읽고 메타데이터 준비하기

파일을 바이너리 모드로 읽으면 사람이 읽기 어려운 바이트가 나옵니다. 이것은 오류가 아니라 파일의 실제 바이트를 본 것입니다. 앞의 \x89PNG는 "이 파일은 PNG 이미지"라는 표시입니다.

# sample.png 파일이 있다고 가정합니다
with open("sample.png", "rb") as file:   # 바이너리 읽기 모드
    data = file.read(20)                  # 앞부분 20바이트만 읽어 보기

print(data)   # b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR'

이 데이터를 억지로 decode("utf-8")하면 UnicodeDecodeError가 납니다.

이미지 바이트는 UTF-8 규칙을 따르지 않기 때문입니다.

파일 전송에서는 이 바이트를 그대로 보내고 그대로 저장해야 합니다.

실제 전송에서 필요한 메타데이터는 os 모듈로 간단히 만들 수 있습니다.

import os

filepath = "sample.png"

filename = os.path.basename(filepath)   # "sample.png"
filesize = os.path.getsize(filepath)    # 파일 크기(바이트 수)

# 메타데이터 → JSON 텍스트 메시지로 전달
metadata = {
    "type": "file_info",
    "filename": filename,
    "size": filesize
}

print(metadata)
# {'type': 'file_info', 'filename': 'sample.png', 'size': 204800}

metadata는 기존 send_message()로 그대로 보낼 수 있습니다. 받는 쪽은 이 메시지를 먼저 받고 준비한 뒤, 뒤따라 오는 바이너리 데이터를 filesize만큼 모아 파일로 저장합니다.

- 텍스트는 encode()와 decode()로 문자열과 바이트를 오간다
- 파일 내용은 바이너리 데이터로 다룬다 ("rb"로 읽고 "wb"로 저장)
- 바이너리를 decode()하면 UnicodeDecodeError가 난다
- 파일 이름·크기(메타데이터)는 JSON 텍스트 메시지로 먼저 전달한다
- 받는 쪽은 size만큼 받으면 파일 하나가 완성됐다고 판단한다

 

→ 다음 강의 (32강): 이번에 이해한 "메타데이터 먼저, 바이너리 나중" 개념을 실제 메시지 규칙으로 설계합니다. file_info 메시지에 어떤 필드가 필요한지 정하고, 받는 쪽이 size만큼 받아 파일을 완성하는 흐름을 protocol.py 약속으로 정리합니다.