프로젝트 개요
여러 사용자가 대화하는 TCP 채팅과 호텔 및 공연 밴드의 공통 시간대를 확보하는 HTTP 예약 시스템입니다. TCP는 연결을 유지하며 데이터를 순서대로 전달하는 통신 방식이고, HTTP는 요청에 응답을 돌려주는 방식입니다. Talk에서는 이름과 메시지를 입력해 전체 대화나 개인 대화를 나누고, Hotel Wedding Planner에서는 호텔과 밴드를 같은 시간에 예약할 수 있습니다.
Talk는 사용자 등록, 전체 메시지와 개인 메시지, 사용자 목록과 종료를 처리합니다. 예약 도구는 예약 가능한 시간대를 뜻하는 슬롯의 조회, 수동 예약과 취소, 가장 이른 공통 슬롯 예약과 예약 정리를 제공합니다. 두 애플리케이션은 터미널에서 명령을 입력하는 CLI 형태로 별도 실행하며 세션이나 데이터베이스를 공유하지 않습니다.
채팅에서는 느린 연결이 다른 사용자에게 미치는 영향을 격리하고, 예약에서는 응답 유실과 프로세스 중단 뒤에도 처리 결과를 확인할 수 있도록 설계했습니다. 채팅의 연결별 수신 스레드와 줄바꿈 명령 및 응답, 예약 API의 경로와 정상 응답은 호환성을 유지했습니다.
핵심 결과
- 느린 연결이 있을 때 정상 사용자 수신
- 900 / 900
- 정상 사용자 3명 × 300회, 반복 3회 모두 완료
- 정상 조건의 전체 수신
- 6,120 / 6,120
- 사용자 2명, 10명, 30명, 60명 조건의 합계
- 실제 통신과 CLI 회귀 검증
- 89개
- GitHub Actions의 4개 환경에서 각각 통과
- GitHub 검증 환경
- 4개
- Windows와 Ubuntu에서 Python 3.10과 3.12
수신 건수는 메시지를 보내려고 시도한 횟수가 아니라 정상 사용자의 프로그램이 실제로 받은 건수입니다. 회귀 검증은 코드가 바뀌어도 기존 기능과 오류 처리가 유지되는지 다시 확인하는 자동 테스트입니다. 아래 실험은 같은 컴퓨터 안에서 서버와 클라이언트가 통신하는 로컬 루프백 환경에서 수행했습니다.
수신자별 송신 큐로 느린 연결을 격리했습니다. 송신 큐는 사용자마다 보낼 메시지를 잠시 보관하는 대기열이며, 소켓은 프로그램이 네트워크 데이터를 보내고 받는 통신 창구입니다. 한 소켓의 전송이 끝나기를 기다린 뒤 다음 사용자에게 보내는 구현과, 각자의 큐에서 독립적으로 보내는 구현을 비교했습니다.
정상 수신자 3명에게 300회 방송을 계획한 실험에서 직접 송신 구현의 실제 수신은 반복할 때마다 900건 중 3건이었고, 송신 큐 구현은 900건을 모두 수신했습니다. 직접 송신 구현은 수신 측이 데이터를 읽지 않아 송신 측도 더 보내지 못하는 역압력 때문에 방송을 48회만 시도하고 47회를 소켓에 전송했습니다. 따라서 나머지 897건은 미전송 방송까지 포함한 미완료 관찰이며 메시지 유실률을 뜻하지 않습니다. 반복 3회 모두 같은 결과였으며 송신 큐 구현의 누락과 중복은 0건입니다. 읽기를 멈춘 연결 자체의 수신 성공을 의미하지 않습니다.
정상 조건에서는 직접 송신 구현도 모든 메시지를 전달했습니다. 송신 큐 구현은 연결마다 큐와 작업을 병행하는 실행 단위인 스레드를 두므로 전달 지연과 스레드 수가 증가했습니다. 느린 수신자 격리의 효과와 지연 비용을 함께 제시합니다. 지연의 p95는 측정한 전달 시간의 95%가 그 값 이하였다는 뜻으로, 평균보다 느린 쪽의 응답까지 살펴보는 지표입니다.
| 정상 수신자 수 | 직접 송신 전달 지연 p95 | 송신 큐 전달 지연 p95 |
|---|---|---|
| 2명 | 0.1528ms | 0.2434ms |
| 10명 | 0.3604ms | 0.6362ms |
| 30명 | 0.9054ms | 1.5795ms |
| 60명 | 1.6338ms | 3.2020ms |
표의 각 값은 같은 조건을 3회 실행한 회차별 p95의 중앙값입니다. 즉 각 회차에서 p95를 구한 뒤 그 값들을 정렬해 가운데 값을 제시했습니다. 모든 수신자가 받은 뒤 다음 메시지를 보내는 조건이며 인터넷 지연이나 최대 처리량을 측정한 값은 아닙니다. 정상 조건 12회의 수신은 두 구현 모두 6,120건 중 6,120건이었습니다.
해결하려던 문제
채팅은 정상적으로 읽는 사용자만 있으면 동작해도, 한 사용자가 수신을 멈추면 순차적인 소켓 쓰기가 막힐 수 있습니다. 입력 길이와 접속 수, 미등록 연결의 대기시간에도 상한이 필요했습니다. 비교한 직접 송신 구현은 사용자명을 공개한 뒤 등록 확인을 보내므로 다른 메시지가 먼저 도착할 수 있어 함께 점검했습니다.
예약에서는 서버가 이미 처리한 요청의 응답만 유실될 수 있습니다. 이때 재시도를 새 예약으로 취급하거나 실패로 단정하면 잘못된 결과를 알릴 수 있습니다. 이전 예약이 취소되고 같은 슬롯에 새 예약이 생긴 경우에는 과거 작업의 복구가 새 예약을 삭제하지 않아야 했습니다.
시스템 구조와 구현
Talk는 연결별 수신 스레드를 유지하고 수신자마다 크기가 제한된 송신 큐와 송신 스레드를 추가했습니다. 이 프로젝트에서 프레임은 줄바꿈으로 경계를 구분한 명령이나 응답 한 단위입니다. 큐가 가득 차거나 프레임 송신 제한시간을 넘기면 해당 연결을 정리합니다. 등록 확인을 큐에 넣은 뒤 사용자명을 공개해 다른 메시지보다 먼저 전달되도록 순서를 정했습니다. 종료 응답의 송신을 마친 연결은 송신 방향을 닫고 남은 입력을 제한된 시간 동안 읽어 버립니다. 거부한 프레임 뒤의 바이트는 명령으로 해석하지 않으며, 입력이 계속 들어와도 종료 기한을 늘리지 않아 연결 자원을 반환하도록 했습니다.
| 채팅의 자원 제한 | 기본값 |
|---|---|
| 허용 연결 수 | 128개 |
| 입력 한 줄, 끝의 줄바꿈 문자 LF 제외 | 8,192바이트 |
| 사용자명 | UTF-8 64바이트 |
| 연결 후 등록 대기시간 | 30초 |
| 연결별 송신 큐 | 128개 프레임 |
| 프레임 하나의 송신 제한시간 | 2초 |
호텔과 밴드는 하나의 로컬 HTTP 프로세스가 제공하는 별도 자원입니다. 프로그램 사이의 요청과 응답 규격인 API로 각각의 예약을 처리하고, 파일 기반 데이터베이스인 SQLite에 저장합니다. 트랜잭션은 여러 변경을 하나의 작업으로 묶어 전부 반영하거나 전부 취소하는 단위입니다. 예약 변경과 처리 결과를 같은 트랜잭션에 저장해 함께 확정하는 커밋을 수행합니다.
같은 작업 키로 다시 요청하면 저장한 결과를 반환합니다. 이는 같은 요청을 반복해도 예약이 중복 생성되지 않게 하는 멱등성 처리이며, 키를 다른 작업에 재사용하면 충돌로 처리합니다. 예약 응답에 이어 처리 기록의 첫 조회 응답도 유실될 수 있으므로, 기록 조회는 같은 시간 예산 안에서 제한된 횟수로 재확인합니다. 이 확인 때문에 예약 생성 요청의 시도 횟수를 늘리지는 않으며, 끝내 확인하지 못한 결과는 작업의 진행 상황을 저장하는 저널에 남깁니다.
아래 흐름의 보상은 호텔 예약 뒤 밴드 예약이 실패했을 때, 이미 만든 호텔 예약을 취소해 상태를 맞추는 처리입니다. 파일 저널은 예약 의도와 작업 식별자를 디스크에 남겨 프로그램이 종료됐다가 다시 시작해도 미완료 작업을 확인할 수 있게 합니다.
- 예약 의도 기록
파일 저널에 슬롯과 작업 식별자를 먼저 커밋
- 호텔과 밴드 요청
같은 작업 키로 재시도하고 서버가 변경과 처리 결과를 함께 저장
- 현재 예약 확인
과거 성공 결과와 현재 예약의 생성 식별자를 구분
- 보상 또는 보류
해당 작업의 예약만 조건부 취소하고 미확정 결과는 유지
- 재시작 복구
같은 파일 저널에서 끝나지 않은 작업을 다시 확인
슬롯 번호가 같아도 예약을 다시 만들면 생성 식별자가 달라집니다. 보상 취소는 이 식별자가 자신이 만든 예약과 일치하는지 검사하므로, 나중에 생성한 예약을 이전 작업의 복구가 삭제하지 않도록 합니다. 과거의 성공 기록만으로 예약 쌍을 확정하지 않고 양쪽의 현재 예약도 각각 확인합니다.
CLI는 같은 파일 저널을 여러 프로세스가 동시에 바꾸지 못하도록 잠금을 사용합니다. 자동 매칭은 별개의 수동 예약을 보존하며, 불필요한 예약 정리는 사용자가 메뉴 7을 선택할 때만 실행합니다. 함수를 직접 호출하면 실행 중 메모리에만 기록하는 저널을 기본으로 사용하므로 재시작 복구가 필요할 때는 파일 저널을 명시해야 합니다.
실험과 검증
| 검증 범위 | 확인한 내용 |
|---|---|
| 채팅 명령과 응답 규격 | 명령과 응답 호환성, 한글 바이트 분할 수신, 여러 명령의 한 번 수신, 중복 이름 동시 등록 |
| 연결과 종료 | 크기와 연결 상한, 등록 시간 만료, 느린 수신자, 종료 응답의 늦은 수신, 입력이 계속되는 연결의 종료와 재접속 |
| 실제 채팅 CLI | 입력 대기 중 서버 종료, 미입력 상태의 등록 시간 만료, 한글과 이모지 송수신 |
| 예약 API | 중복 예약 방지, 사용자별 상한, 응답 유실 뒤 재시도, 처리 기록 보존과 조회 |
| 자동 예약 복구 | 요청 전 기록, 경쟁 예약과 연속 응답 유실, 조회 재시도의 예산, 프로세스 중단 뒤 복구, 현재 예약 재확인과 조건부 취소 |
| GitHub Actions | Windows와 Ubuntu의 Python 3.10 및 3.12, 네 환경에서 각각 89개 통과 |
| 로컬 실행 환경 | Windows Python 3.10.21과 3.12.7에서 각각 89개 통과 |
채팅 측정은 Windows와 Python 3.12에서 같은 루프백 측정 도구로 수행했습니다. 정상 조건은 사용자 수마다 예열 5회와 측정 20회를 적용하고 3회 반복했습니다. 느린 수신자 조건은 정상 사용자 3명과 읽기를 멈춘 연결 1명을 두고, 두 구현에 동일한 작은 송수신 버퍼를 적용했습니다. 송신 큐 구현은 공개한 기본 설정인 큐 128개와 송신 제한시간 2초를 그대로 사용했습니다. 방송 300회를 5ms 간격으로 계획하고, 직접 송신 구현에서 송신이 막혀도 총 6초의 관찰 창을 유지했습니다. 송신 큐 구현은 매회 큐 상한을 넘은 연결 1개를 격리했습니다.
전달량은 서버의 큐 적재 수가 아닌 정상 클라이언트가 실제로 받은 메시지 식별자로 셌습니다. 누락된 메시지는 지연 분포에 나타나지 않으므로 수신 수와 누락 수를 먼저 확인합니다. 직접 송신 구현의 느린 수신자 조건에서 받은 3건만으로 계산한 낮은 지연을 송신 큐 구현의 전체 전달 지연과 직접 비교하지 않습니다.
GitHub Actions는 저장소에 설정한 절차에 따라 검사를 자동 실행하는 도구입니다. 실행 환경은 Ubuntu Python 3.10.22와 3.12.15, Windows Python 3.10.11과 3.12.10입니다. 네 환경에서 각각 89개씩 총 356개 검사를 통과했으며 실패와 오류 및 건너뛴 항목은 없습니다. 네 실행 기록에 담긴 소스 파일 20개의 해시도 모두 일치합니다. 로컬 Windows Python 3.10.21과 3.12.7에서도 분리한 가상환경을 사용해 각각 89개를 통과했습니다.
검증 실행기는 임시 포트와 임시 DB를 만들고 테스트별 결과, Python과 패키지 버전, 줄바꿈을 정규화한 소스 해시를 JSON으로 기록합니다. 소스 해시는 파일 내용에서 계산한 식별값으로, 서로 다른 환경에서 같은 코드를 검사했는지 대조하는 데 사용합니다. 외부 API나 클라우드 계정 없이 같은 검증을 다시 실행할 수 있습니다. 환경별 검증 기록과 GitHub Actions 실행 결과를 아래 자료에서 확인할 수 있습니다.
오류 분석과 개선 범위
| 점검한 문제 | 보강한 처리 |
|---|---|
| 읽기를 멈춘 사용자가 전체 전송을 지연 | 수신자별 송신 큐와 제한시간, 해당 연결만 정리 |
| 등록 확인 전에 다른 메시지 도착 | 확인 응답 적재 후 사용자 공개, 순서 경쟁 강제 검증 |
| 서버가 종료돼도 CLI가 입력을 계속 대기 | 소켓 종료와 입력 준비 상태 확인, 실제 subprocess 종료 검증 |
| 초과 프레임의 남은 입력 때문에 종료 응답을 읽기 전에 TCP 연결이 재설정됨 | 응답 송신 후 송신 방향 종료, 제한된 입력 폐기와 늦은 수신 검증 |
| 거부된 연결이 입력을 계속 보내 자원을 점유 | 입력 폐기의 기한을 고정하고 연결 반환과 재접속 검사 |
| 예약 응답과 첫 처리 기록 조회 응답이 연속으로 유실 | 변경과 결과의 원자적 저장, 같은 예산 안에서 처리 기록만 재확인 |
| 과거 작업의 보상이 새 예약 삭제 | 생성 식별자를 확인하는 조건부 취소 |
| 복구 중 현재 예약이 달라짐 | 현재 예약 쌍 재확인, 확인할 수 없는 결과 보존 |
송신 큐 적재나 소켓 쓰기 완료가 사용자의 읽음 확인은 아닙니다. 채팅의 영속 이력과 재접속 후 재전송도 제공하지 않습니다. 예약 복구는 보상 요청을 사용하는 방식으로 두 서비스를 하나의 원자적 트랜잭션으로 묶지 않습니다. 두 현재 예약 조회 사이 또는 확인이 끝난 뒤 외부 변경이 생길 수 있습니다.
60개 정상 연결에서 측정 프로세스의 최대 스레드 수 중앙값은 123개에서 183개, 최대 메모리는 26.129MiB에서 33.707MiB로 증가했습니다. 이 수치는 서버뿐 아니라 같은 프로세스의 클라이언트와 측정 도구를 함께 포함합니다.
파일 저널을 사용한 경우에만 프로세스 재시작 후 작업을 복구할 수 있습니다. 서버 처리 기록은 자동으로 지우지 않으며, max_elapsed는 재시도 예산이므로 요청과 전체 작업의 절대 실행시간을 보장하지 않습니다. 로컬 데모 인증과 SQLite 구성의 범위를 넘는 운영 환경은 별도의 설계와 검증이 필요합니다.
실행과 재현 방법
Python 3.10 이상에서 저장소 루트를 기준으로 실행합니다. 다음은 Windows PowerShell 예시이며 가상환경 활성화 없이 해당 인터프리터를 직접 사용합니다. Linux와 macOS 명령은 README에 제공합니다.
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-dev-lock.txt
.\.venv\Scripts\python.exe -B scripts/verify.py --output target/verification.json
채팅은 첫 번째 터미널에서 서버를 실행하고 다른 터미널 두 개에서 클라이언트를 실행합니다. 기본 주소는 127.0.0.1:8090입니다.
.\.venv\Scripts\python.exe Talk/myserver.py
.\.venv\Scripts\python.exe Talk/myclient.py
서로 다른 이름으로 등록한 뒤 send_all 안녕하세요, send_to Bob 개인 메시지, user_list, quit을 입력합니다. 예약은 첫 번째 터미널에서 API를 실행하고 두 번째 터미널에서 CLI를 실행합니다.
.\.venv\Scripts\python.exe Hotel/local_api.py
.\.venv\Scripts\python.exe Hotel/booking.py
기본 API 주소는 http://127.0.0.1:8081입니다. 새 데모 DB에서 메뉴 6을 실행하면 가장 이른 공통 슬롯을 예약하고 메뉴 1에서 결과를 확인할 수 있습니다. 같은 API 데이터와 파일 저널을 유지한 상태로 CLI를 다시 시작하면 미해결 자동 예약을 확인합니다. 복구 저널의 기본 위치는 Hotel/data/booking-journal.db이며 BOOKING_JOURNAL_PATH로 바꿀 수 있습니다.
측정을 다시 실행하려면 다음 명령을 사용합니다. 직접 송신 구현의 소스를 별도 디렉터리로 내보내고 --project를 그 경로로 지정하면 송신 큐 구현과 같은 도구로 비교할 수 있습니다.
.\.venv\Scripts\python.exe -B scripts/benchmark.py --project . --scenario all --trials 3 --label current-source --output target/benchmark.json
소스 코드와 검증 자료
- 소스와 검증 자료 다운로드
- GitHub 저장소
- 한국어 README, 영어 README
- 검증 요약, 직접 송신 측정, 송신 큐 측정, 비교 결과
- 회귀 검증 기록, GitHub Actions 실행 상태
- 설계와 장애 처리, 설계 범위와 근거
비교 대상 코드는 Git 이력에 보존했습니다. 다운로드 ZIP에는 같은 공개 소스와 문서, 그래프와 검증 결과를 담고 런타임 DB와 개인 복구 기록, 실제 인증정보와 유지보수 메모는 제외합니다. ZIP에서도 실행과 검증 자료를 확인할 수 있습니다.
소스와 문서 ZIP 다운로드GitHub에서 소스 코드 보기

