32강 개념 중심 ⏱ 약 20분

 

0. 학습 목표

→ 파일 전송을 세 단계 메시지로 나누는 이유와, 파일 조각을 JSON에 담기 위해 Base64가 필요한 이유를 이해합니다.

더보기

0.1 이번 글에서 다룰 내용

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

 

31강에서 파일 전송에는 "메타데이터(파일 이름·크기) 먼저, 바이너리 내용 나중"이라는 원칙이 필요하다는 것을 이해했습니다. 이번 강의에서는 그 원칙을 실제 메시지 규칙으로 구체화합니다. 파일 전송 흐름을 세 종류의 메시지로 나누어 설계하고, 각 메시지에 어떤 필드가 필요한지 확정합니다.

file_info  → 파일 이름과 크기 안내
file_chunk → 파일 내용 조각 전달 (여러 번 반복)
file_end   → 파일 전송 완료 안내

파일 조각은 바이너리이므로 JSON에 그대로 담을 수 없습니다. 이번 강의에서는 Base64 변환이 왜 필요한지도 함께 이해합니다.

구분 내용
이해할 것 파일 전송을 세 단계 메시지로 나누는 이유, Base64가 JSON에 필요한 이유
정리할 것 file_info · file_chunk · file_end 각 메시지의 필드 구성
확인할 것 Base64 인코딩·디코딩을 코드로 직접 실행해 왕복 변환 결과 확인

 

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

이번 강의는 개념 중심이므로 기존 파일을 직접 수정하지 않습니다. 다음 강의(33강)에서 구현할 메시지 구조를 먼저 설계합니다.

chat_server/
├── protocol.py           ← (33강에서 수정 예정)
└── server.py

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

이번 강의에서 설계할 메시지 타입은 세 가지입니다. 전송 실패 시에는 기존에 있던 error 메시지를 그대로 사용합니다.

메시지 타입 역할
file_info 파일 전송 시작 전 파일 정보 안내
file_chunk 파일 내용 일부 전달 (여러 번 반복)
file_end 파일 전송 완료 안내

 

1. 핵심 개념 이해하기

→ 파일 전송에 세 가지 메시지가 각각 왜 필요한지, 파일 조각을 JSON에 담으려면 왜 Base64가 필요한지 이해합니다.

더보기

1.1 파일 전송을 세 단계 메시지로 나누는 이유

31강에서 "파일 이름·크기(송장)를 먼저 보내고, 파일 내용(상자)을 나중에 보낸다"는 원칙을 이해했습니다.

그렇다면 왜 "file_info 하나 + 파일 내용 한 번에 전송"으로 끝내면 안 될까요?

 

파일이 크면 메모리에 전체를 올려야 합니다.

10MB 파일을 하나의 메시지에 담으려면 10MB짜리 JSON 문자열이 만들어지고,

동시에 여러 사람이 파일을 보내면 서버 메모리가 순식간에 찹니다.

그래서 파일을 일정 크기의 조각으로 나누어 반복 전송합니다. 이것이 file_chunk입니다.

 

조각으로 나눠 보내면 받는 쪽은 "언제 파일이 다 온 것인가"를 알 수 없습니다.

31강에서는 size만큼 받으면 끝난다고 설명했지만, JSON 메시지 기반 프로토콜에서는 받은 바이트를 직접 세기 어렵습니다. 명시적인 완료 신호가 있는 것이 훨씬 단순합니다. 이것이 file_end가 별도 메시지로 존재하는 이유입니다.

메시지 없으면 어떻게 되는가
file_info 없음 받는 쪽이 파일 이름·크기를 모른 채 데이터를 받게 됩니다
file_chunk 분할 없음 큰 파일을 메모리에 통째로 올려야 해서 서버 부담이 커집니다
file_end 없음 전송이 완료됐는지 중간에 끊겼는지 받는 쪽이 알 수 없습니다

 

1.2 JSON에 바이너리를 담을 수 없는 이유

이번 채팅 프로젝트는 모든 메시지를 JSON으로 주고받습니다.

그런데 JSON은 텍스트 기반 형식입니다. 문자열·숫자·불리언만 담을 수 있고, 바이너리 데이터를 그대로 넣을 수 없습니다.

 

이미지 파일의 첫 바이트는 b'\x89PNG\r\n\x1a\n'처럼 생겼습니다.

이것을 JSON 문자열 안에 직접 넣으면 인코딩 오류가 납니다.

바이너리 데이터를 JSON에 담으려면 먼저 텍스트로 변환해야 합니다. 이때 사용하는 것이 Base64입니다.

 

Base64는 바이너리 데이터를 A~Z, a~z, 0~9, +, / 같은 텍스트 문자로만 이루어진 문자열로 변환합니다.

결과는 원래 데이터보다 약 33% 커지지만, JSON 안에 안전하게 담아 전송할 수 있습니다.

바이너리: b'\x89PNG\r\n\x1a\n...'     (JSON에 담을 수 없음)
        ↓ base64.b64encode()
Base64:  'iVBORw0KGgoAAAANSUhEUg...'  (JSON에 담을 수 있는 텍스트 문자열)

Base64 동작 방식을 완전히 이해하지 않아도 됩니다.

"바이너리를 JSON에 담기 위해 텍스트로 변환하는 방법"이라고 이해하면 충분합니다.

✔ 확인 기준: file_info · file_chunk · file_end 세 메시지가 각각 왜 필요한지 말할 수 있고, "JSON은 텍스트 형식이라 바이너리를 Base64로 변환해야 담을 수 있다"고 설명할 수 있으면 완료.

 

2. 구조와 흐름으로 확인하기

→ 파일 전송 전체 흐름을 보내는 쪽·받는 쪽 양면으로 확인하고, 각 메시지 구조를 설계합니다.

더보기

2.1 파일 전송 전체 흐름

보내는 쪽
├── 파일 선택 → 파일 이름·크기 확인
├── file_info 전송
├── 파일을 일정 크기(CHUNK_SIZE)씩 반복 읽기
├── file_chunk 반복 전송
└── file_end 전송

받는 쪽
├── file_info 수신 → 파일 이름·크기 파악, 저장 경로 결정
├── file_chunk 수신 반복 → 조각을 파일에 이어 쓰기 ("wb" 모드)
└── file_end 수신 → 파일 닫기, 완료 표시

 

2.2 각 메시지 구조 설계

file_info는 파일 전송이 시작된다는 안내 메시지입니다. 31강에서 미리 본 기본 구조에서 file_idsender를 추가합니다. file_id는 여러 파일이 동시에 전송될 때 조각들이 어떤 파일에 속하는지 구분하는 식별자입니다.

{
    "type": "file_info",
    "file_id": "file_001",
    "filename": "photo.png",
    "size": 204800,
    "sender": "('127.0.0.1', 52344)"
}

file_chunk는 파일의 실제 조각을 담는 메시지입니다. 바이너리를 그대로 넣을 수 없으므로 Base64 문자열로 변환해 data 필드에 담습니다.

{
    "type": "file_chunk",
    "file_id": "file_001",
    "data": "aGVsbG8gZmlsZQ=="
}

file_end는 파일 전송이 끝났다는 신호입니다. 받는 쪽은 이 메시지를 받으면 열어 두었던 파일을 닫고 "파일 수신 완료"를 사용자에게 알립니다.

{
    "type": "file_end",
    "file_id": "file_001"
}

✔ 확인 기준: file_info에 파일 이름·크기·전송 ID가, file_chunk에 Base64 데이터가, file_end에 완료 신호만 담긴다고 각각 설명할 수 있으면 완료.

 

3. 판단 기준 정리하기

→ 각 메시지에 어떤 필드를 넣어야 하는지 정리하고, 파일 전송 설계에서 자주 하는 오해를 확인합니다.

더보기

3.1 메시지 타입별 포함 정보

타입 포함할 필드 역할
file_info file_id, filename, size, sender 파일 전송 시작 안내
file_chunk file_id, data (Base64 문자열) 파일 내용 조각 전달
file_end file_id 파일 전송 완료 안내

file_chunkdata는 반드시 Base64 문자열이어야 합니다. 바이너리를 그대로 넣으면 JSON 인코딩 오류가 발생합니다. 전송 실패가 발생하면 기존 error 메시지를 그대로 사용합니다.

초급 단계에서는 file_chunk에 조각 번호(chunk_index)를 넣지 않아도 됩니다. TCP가 전송 순서를 보장하므로 조각이 뒤바뀌는 일이 없기 때문입니다. 진행 표시줄을 구현하거나 수신 완결성을 더 엄격하게 검증하고 싶을 때 추가하면 됩니다.

 

3.2 자주 하는 오해

오해 올바른 이해
파일 전체를 JSON 하나에 넣으면 된다 큰 파일에서 메모리 부담이 커집니다. 조각으로 나누어 보내야 합니다.
바이너리를 JSON에 그대로 넣을 수 있다 JSON은 텍스트 형식입니다. 바이너리는 Base64 문자열로 변환해야 담을 수 있습니다.
file_end 없이 size로만 완료를 판단할 수 있다 JSON 메시지 기반 프로토콜에서는 바이트 수를 직접 세기 어렵습니다. 명시적 완료 신호가 단순하고 안전합니다.
file_id가 없어도 된다 여러 파일이 동시에 전송될 때 어떤 조각이 어떤 파일에 속하는지 구분할 수 없습니다.

 

4. 예시로 검토하기

→ 파일 정보 메시지와 파일 조각의 Base64 변환 흐름을 코드로 직접 확인합니다.

더보기

4.1 file_info 메시지 만들기

file_info는 파일 내용이 아니라 파일을 설명하는 메시지입니다. 일반 채팅 메시지와 구조가 같아 protocol.py의 기존 send_message()로 보낼 수 있습니다.

file_info = {
    "type": "file_info",
    "file_id": "file_001",
    "filename": "photo.png",
    "size": 204800,
    "sender": "user1"
}

print(file_info)
{'type': 'file_info', 'file_id': 'file_001', 'filename': 'photo.png', 'size': 204800, 'sender': 'user1'}

 

4.2 파일 조각을 Base64로 변환하기

파일 조각은 바이너리이므로 JSON 안에 그대로 넣을 수 없습니다. "이상하게 생긴 영문 문자열"이 출력되는 것이 정상이며 오류가 아닙니다.

import base64

chunk = b"hello file"                              # 바이너리 조각 (실제로는 파일 데이터)
encoded = base64.b64encode(chunk).decode("utf-8")  # 바이너리 → Base64 문자열

file_chunk = {
    "type": "file_chunk",
    "file_id": "file_001",
    "data": encoded
}

print(file_chunk)
{'type': 'file_chunk', 'file_id': 'file_001', 'data': 'aGVsbG8gZmlsZQ=='}

받는 쪽에서는 Base64 문자열을 다시 바이너리로 되돌립니다. b64decode()는 문자열을 그대로 받을 수 있으므로 추가 변환 없이 바로 전달합니다.

decoded = base64.b64decode(encoded)  # Base64 문자열 → 바이너리
print(decoded)
b'hello file'

두 방향 변환을 정리하면 다음과 같습니다.

보내는 쪽: 바이너리 조각 → base64.b64encode() → .decode("utf-8") → 문자열 → JSON에 담아 전송
받는 쪽:   문자열 수신 → base64.b64decode() → 바이너리 → 파일에 쓰기 ("wb" 모드)

✔ 확인 기준: base64.b64encode(chunk).decode("utf-8")로 Base64 문자열이 만들어지고, base64.b64decode(encoded)로 원래 바이너리가 복원된다는 흐름을 설명할 수 있으면 완료.

- file_info / file_chunk / file_end 세 메시지로 파일 전송 흐름을 나눈다
- file_end가 없으면 전송 완료와 중단을 구분할 수 없다
- JSON은 텍스트 형식이라 바이너리를 Base64 문자열로 변환해야 담을 수 있다
- b64decode()는 str을 직접 받으므로 추가 .encode() 없이 그대로 전달한다
- 33강에서 이 설계를 protocol.py와 실제 전송 코드로 구현한다

→ 다음 강의 (33강): 이번에 설계한 file_info · file_chunk · file_end 메시지 구조를 protocol.py에 추가하고, 파일을 "rb"로 열어 CHUNK_SIZE씩 읽은 뒤 Base64로 변환해 전송하는 코드를 작성합니다.