TASFA: 블로그 파일 송수신 아키텍처(한국어/Korean)
by
gg582 · 2026-05-27 00:55:53 · 88 views
TASFA
TASFA는 이 프로젝트에서 파일 업로드와 다운로드에 사용하는 전송 프로토콜이다.
평범한 HTTP/XHR 위에 구축되며, 청크 단위 암호화, 파일 수준 순차 큐, 그리고 **서버 권한 6슬롯 지수귀문도(Hexagonal Tortoise Problem) 복구 격자(recovery lattice)**를 추가한다.
라우트
업로드:
POST /file/upload/initPOST /file/upload/statusPOST /file/upload/renegotiatePOST /file/uploadPOST /file/upload/completePOST /file/upload/cancel
다운로드:
GET /file/download/:id/handshakeGET /file/download/:id/chunk/:chunk_indexGET /file/download/:idGET /assets/tasfa/img/:filename/handshakeGET /assets/tasfa/img/:filename/chunk/:chunk_indexGET /assets/tasfa/uploads/:filename/handshakeGET /assets/tasfa/uploads/:filename/chunk/:chunk_index
업로드 프로토콜
브라우저는 먼저 업로드 세션을 협상한 뒤, TASFA 헤더와 함께 청크(기본값 16 MiB, 모바일 8 MiB)를 전송한다:
X-TASFA-Upload-IDX-TASFA-Upload-TokenX-TASFA-Chunk-IndexX-TASFA-Hash-TagX-TASFA-Raw-ScalarX-TASFA-Magic-Scalar
서버는 각 청크를 미리 할당된 임시 파일의 chunk_index * chunk_size 오프셋에 직접 쓴다. 더 이상 전송 블록(transport block)은 없다.
일반 청크가 반복적으로 실패하면, 브라우저는 해당 실패 청크를 X-TASFA-Stream-Mode: aes-256-gcm 폴팁 요청으로 전송한다. 폴팁은 AES-GCM 인증 태그를 포함한 암호문 크기를 서버가 정상 수락하며, 적응형 병렬 윈도우 안에서 처리된다. 폴팁 요청에도 동일한 HTP 해시 태그와 균형 스칼라 헤더가 포함된다.
나머지(마지막 부분 청크)
파일 크기가 청크 크기의 배수가 아니라면, 마지막 청크는 **나머지(remainder)**이다. 정확한 바이트 범위를 단일 blob으로 전송하며, 서버는 올바른 오프셋에 기록한다. 패딩이나 분할은 수행하지 않는다.
세션 초기화 응답
init 엔드포인트는 다음을 포함한 응답을 반환한다:
chunk_size— 협상된 청크 크기current_parallel_chunks— 서버가 승인한 현재 업로드 윈도우max_parallel_chunks— 클라이언트가 동시에 업로드할 수 있는 청크 수dispatch_pacing_ms— 측정된 링크가 나쁠 때만 적용되는 작은 전송 간격upload_secret— 암호화 스트림 검증용 보조 서버 시크릿stream_key_hex,stream_iv_seed_hex,stream_mode— 세션 암호화 키(aes-256-gcm)modulus_M— 해당 세션에 사용되는 HTP 모듈러스group_count— 완전한 6슬롯 HTP 그룹의 수client_stripes— 클라이언트 워커 스케줄러용 고정값32
chunk_size는 chunk_index * chunk_size 파일 오프셋을 정의하므로 세션 중간에는 바꾸지 않는다. 진행 중인 업로드에서는 병렬도와 재시도 동작을 조절하고, 학습된 청크 크기 힌트는 다음 TASFA 세션 협상에 반영한다. 링크가 좋으면 다음 힌트를 *2로 올리고, 링크가 나빠지면 /2로 낮춘다.
업로드 청크 응답
청크가 수락되면 서버는 204 No Content와 다음 헤더로 응답한다:
X-TASFA-Accepted: 1X-TASFA-Chunk-Complete: 1
청크 인덱스가 이미 state.bin에서 완료로 표시되어 있고, 해당 인덱스가 서버의 retry_targets에 없으면 서버는 동일한 헤더로 204를 반환하지만 청크 본문은 폐기한다.
HTP 서버 권한 복구 격자
HTP는 전송 프로토콜이 아니며 암호학적 증명도 아니다. 청크 수준에서 손상 가능성을 순위화하여 클라이언트가 전체 파일이 아닌 고위험 의심 청크만 재전송하도록 돕는 서버 측 엔진이다.
청크 그룹화
청크는 연속된 6개 요소 정점으로 그룹화된다:
그룹 g: [ v0 , v1 , v2 , v3 , v4 , v5 ]
chunk g*6+0 ... g*6+5
마지막 그룹은 부분일 수 있으나, 불완전한 그룹은 절대 패딩되지 않고 HTP 검증에서 완전히 제외된다. 제로 패딩은 인위적인 토폴로지를 주입하므로 금지된다.
원시 스칼라(raw scalar)
각 청크에 대해 클라이언트는 평문 청크의 SHA-512를 계산하고, 처음 8바이트를 빅엔디안 부호 없는 64비트 정수 H로 해석한 뒤 다음을 도출한다:
raw_scalar = H % modulus_M
마법 선 불변식(magic line invariant)
완전한 그룹에 대해 세 개의 선을 정의한다:
L1 = v0 + v1 + v2 (mod M)
L2 = v2 + v3 + v4 (mod M)
L3 = v4 + v5 + v0 (mod M)
불변식은 L1 == L2 == L3을 요구한다. 원시 스칼라가 이를 만족하지 않으면, 클라이언트는 오직 v3와 v5만 조정하여 균형을 맞춘다:
delta2 = (L1 - L2) mod M
delta3 = (L1 - L3) mod M
v3_balanced = (v3_raw + delta2) mod M
v5_balanced = (v5_raw + delta3) mod M
나머지 모든 정점은 원시 스칼라를 유지한다. 클라이언트는 둘 다 전송한다:
X-TASFA-Raw-Scalar— 수정되지 않은raw_scalarX-TASFA-Magic-Scalar— 균형 잡힌 값(v3_balanced또는v5_balanced, 나머지는 raw와 동일)
서버는 두 스칼라를 htp.bin에 별도로 저장하므로, 인위적인 균형 제약과 무관하게 원본 토폴로지를 분석할 수 있다.
왜 오직 v3와 v5만인가?
육각 격자는 두 개의 자유도를 가진다. v0,v1,v2,v4를 고정하고 v3,v5를 조정하면 세 개의 선 방정식을 유일하게 만족시키면서, 델타를 최소화하고 그룹 내에서 국소적이게 유지할 수 있다.
서버 권한 HTP 복구
클라이언트는 무지한 재전송 에이전트(dumb retransmission agent)이다. 클라이언트는 수리 대수를 계산하지 않고, 비용 임계값을 평가하지 않으며, 의심 순위를 도출하지 않는다. 모든 것은 서버에만 존재한다.
서버 검증 흐름
POST /file/upload/complete 동안 서버는 다음을 수행한다:
htp.bin에서 청크별 레코드(해시 태그, 원시 스칼라, 균형 스칼라)를 모두 로드한다.- 완전한 6슬롯 그룹만 검증한다(부분 그룹은 건너뛴다).
- 실패한 그룹마다 각 슬롯이 참여하는 선 방정식을 분석하여 **의심 점수(suspicion scores)**를 계산한다.
의심 신뢰도 점수(그룹별)
실패한 그룹에 대해 서버는 각 슬롯을 세 개의 선 방정식과 비교하여 평가한다:
| 슬롯 | 방정식 |
|---|---|
| v0 | L1, L3 |
| v1 | L1 |
| v2 | L1, L2 |
| v3 | L2 |
| v4 | L2, L3 |
| v5 | L3 |
각 슬롯의 의심 점수는 토폴로지에서만 결정론적으로 도출된다:
score = in_fail / total_fail
여기서 in_fail은 해당 슬롯이 참여하는 실패 선 방정식의 수이고, total_fail은 해당 그룹의 총 실패 방정식 수다. 임의의 신뢰도 상수는 사용하지 않는다.
통과 방정식에만 등장하는 슬롯은 의심 목록에서 제외된다.
점수는 모든 실패 그룹에 걸쳐 집계되며, 한 청크가 여러 그룹에 나타나면 최대 점수가 유지된다.
복구 비용 임계값
서버는 수축을 요청하기 전에 수축이 직접 재시도보다 저렴한지 평가한다:
repair_worthwhile(의심_수, 전체_청크_수, 청크_크기, rtt_ms):
if 의심_수 < 3 → false (토폴로지 의미 없음)
재시도_비용 = 의심_수 * 청크_크기 * rtt_factor(rtt_ms)
수리_비용 = 메타데이터_바이트 + 서버_CPU_비용 + 추가_RTT_비용
return 재시도_비용 > 수리_비용
추상 비용 모델은 클라이언트가 재전송할 바이트와 서버 측 분석 오버헤드를 비교한다. 큰 청크나 높은 지연 시간은 수축을 더 매력적으로 만들고, 많은 수의 작은 의심 청크는 직접 재시도를 더 저렴하게 만든다. 구체적인 숫자는 서버 측 구현 세부사항이며 프로토콜 상수는 아니다.
임계값이 수리를 거부하면, 서버는 모든 의심 청크를 재시도 대상으로 포함하여 needs_retry를 반환한다. 클라이언트는 이를 일반 업로드 엔드포인트를 통해 재전송한다.
서버 측 재귀적 수축
수리가 유효하다면, 서버는 그룹 수준 수축을 수행한다: 각 원본 완전한 6슬롯 그룹은 자신의 불변식 서명을 인코딩한 단일 스칼라로 축소된다. 서버는 그룹의 세 선 값 L1, L2, L3을 계산하고, 잔차 r12 = (L1-L2) mod M과 r23 = (L2-L3) mod M을 도출하여, 그룹 집계값을 (r12 * r23) mod M으로 설정한다. 통과 그룹은 r12 = r23 = 0이므로 집계값이 0이고, 실패 그룹은 선 불일치의 토폴로지를 보존하는 비영 결정론적 서명을 받는다. 이 그룹 집계값이 더 높은 수준의 HTP 격자의 정점이 된다. 연속된 6개의 그룹 집계값이 레벨-1 슈퍼그룹을 형성하고, 동일한 선 불변식이 재평가된다:
- 레벨-1 슈퍼그룹이 통과하면, 기본 레벨-0 그룹의 의심 청크가 지워진다(그룹 수준에서 실패 패턴이 일관된다).
- 레벨-1 슈퍼그룹이 실패하면, 기본 레벨-0 그룹의 의심 청크가 유지된다.
- 수축이 의심 집합을 줄이면(더 적은 청크), 서버는 축소된 대상을 저장하고 축소된 목록으로
needs_retry를 반환한다. - 수축이 집합을 줄이지 못하면, 서버는 원래 의심 청크의 직접 재시도로 폴백한다.
- 수축 수준은 세션 메타데이터에 증가되어 클라이언트가 진단을 보고할 수 있게 한다.
클라이언트는 수축 그룹을 보거나 계산하지 않는다. 오직 retry_targets만 수신한다.
재전송 수락
클라이언트가 이미 수신된 것으로 표시된 청크를 재전송할 때, 일반 업로드 엔드포인트는 해당 청크 인덱스가 현재 서버의 retry_targets 목록에 있을 때만 재전송을 수락한다. 재전송된 청크가 저장된 후 서버는 retry_targets에서 이를 제거한다.
프로토콜 가시 복구 응답
HTP 실패 시 서버가 복구 또는 재시도를 결정하면, complete 엔드포인트는 409와 함께 다음을 반환한다:
htp_status:"needs_retry"retry_targets: 재전송할 청크 인덱스 배열(의심 점수 내림차순)suspicion_scores:{chunk_index, score}객체 배열contraction_level: 적용된 서버 측 수축 통과 횟수retry_reason: 사람이 읽을 수 있는 설명(예:"htp group inconsistency detected")
비용 임계값이 직접 재시도가 더 저렴하다고 판단하면, retry_targets는 전체 의심 목록을 포함하고 contraction_level은 0이다.
서버가 수축을 통해 의심을 성공적으로 줄이면, retry_targets는 축소된 목록을 포함하고 contraction_level이 증가한다.
모든 의심 청크가 재전송되고 성공적으로 검증되면, 다음 complete 호출은 SHA-256 최종화로 진행한다.
파일 수준 순차 큐
한 번에 하나의 파일만 업로드한다. 여러 파일을 선택하면:
- 각 파일은 자신의 에셋, 미리보기 카드, HTP 세션을 갖는다.
- 파일은
FileUploadQueue에 인큐된다. - 활성 파일이 완료(성공 또는 실패)되면 큐는 자동으로 다음 파일로 진행한다.
- 일괄 "업로드 대기 파일" 버튼은 항상 활성화되어 있으며, 클릭하면 모든 보류 중인 파일을 인큐하고 펌프를 시작한다.
이는 브라우저 연결 풀 고갈을 방지하고 정지 감지를 안정적으로 유지한다.
런타임 설정
- 업로드 청크 크기: 데스크톱
16 MiB, 모바일8 MiB - 적응형 업로드 청크 크기 힌트: 최소
8 MiB, 최대 데스크톱32 MiB/ 모바일16 MiB; 성공 시*2, 실패 시/2 - 다운로드 청크 크기: 데스크톱
8 MiB, 모바일4 MiB, 클라이언트가 더 큰 세션을 힌트하면 최대32 MiB - 기본 브라우저 업로드 병렬성:
16, 성공 시*1.15, 실패 시*0.85 - 최대 브라우저 업로드 병렬성:
blog.settings의max_upload_parallel_chunks, 상한40 - 최대 동시 업로드 세션:
blog.settings의max_total_parallel_uploads, 상한64 - 최대 업로드 크기:
blog.settings의max_upload_size - 최대 브라우저 다운로드 세션: 서버 정의, 현재 세션당 최대
48개 청크 요청 - 다운로드 병합(span 그룹 크기): 성공 시
*1.2, 실패 시*0.8, 최대16개 청크 - 업로드 xhr 타임아웃: 청크 크기에 따라 조정되며 최소
180 s - 업로드 세션 fetch 타임아웃:
30 s
TASFA는 임베디드/항공우주식 저대역폭 프로파일이 아니라 일반적인 고대역폭 서버 배포를 기준으로 튜닝된다. 브라우저의 origin당 HTTP 연결 제한은 워커 풀에 의해 자연스럽게 존중된다.
클라이언트 적응
업로드 클라이언트는 청크 완료 시간, 재시도, 타임아웃, 사용 가능한 경우 Network Information API 힌트를 측정한다. 이 입력을 /file/upload/init과 /file/upload/renegotiate에 보내고, 서버는 현재 병렬 윈도우와 최대 윈도우를 돌려준다. 깨끗하게 완료되는 청크가 이어지면 활성 윈도우를 협상된 최대치까지 *1.2로 기하급수적으로 올리고, 일시적 실패는 *0.8로 낮춘다. AES-GCM 폴팁도 같은 적응형 윈도우 안에서 처리된다. watchdog은 멈춘 XHR을 중단한 뒤 서버 비트맵에서 이어받아 전송이 방치되지 않게 한다.
다운로드도 같은 고처리량 편향을 사용한다. 핸드셰이크에는 클라이언트의 선호 청크 크기 힌트가 포함되고, 활성 다운로드는 성공한 청크 그룹 뒤에 span과 병렬도를 각각 *1.2로 키운다. 실패 시 *0.8로 줄인다. 짧은 응답, 타임아웃, 네트워크 오류는 청크 인덱스 단위로 다시 큐에 넣으며, 같은 청크가 높은 재시도 예산을 모두 소진하기 전까지는 전체 다운로드를 실패로 끝내지 않는다.
후반부 RTT 예측 (라그랑주 외삽)
큰 파일(청크 수가 많은 세션)에서 서버는 클라이언트가 보고한 청크별 RTT 샘플을 최대 8개까지 누적한다. 샘플이 3개 이상 쌓이면 라그랑주 다항식 외삽으로 마지막 청크 인덱스까지의 RTT를 추정하고, 남은 청크 수를 곱해 예상 남은 시간을 산출한다.
- 업로드: 클라이언트는 각 청크 전송 완료 후
X-TASFA-Chunk-RTT헤더(밀리초)를 다음 청크 요청에 첨부할 수 있다. - 다운로드: 클라이언트는 이전 청크의 RTT를
chunk_rtt_ms쿼리 파라미터로 별도로 보고할 수 있다.
서버는 상태 응답 JSON에 predicted_remaining_ms를 추가하며, 다운로드 청크 응답에는 X-TASFA-Predicted-Remaining-Ms 헤더를 담는다. 수학 연산은 의도적으로 가볍게 유지한다: 샘플 상한 8개, O(n²) 단순 라그랑주, 예측값은 30초 상한으로 클리핑한다.
Wynn 가속 선제 방어
TASFA는 평문 바이트를 예측하거나 예측된 청크 내용을 신뢰하지 않는다. 예측 대상은 클라이언트가 관측한 전송 품질 수열, 즉 완료 시간, 처리량, 재시도, 타임아웃, 짧은 응답뿐이다. 클라이언트는 최신 품질 샘플에 Wynn epsilon 변환의 Aitken Delta^2 형태를 적용한다:
S_hat = S0 - ((S1 - S0)^2 / (S2 - 2*S1 + S0))
S_hat은 [0, 1] 범위로 제한되고 다음 청크의 방어 신호로만 사용된다. 예측 품질이 높으면 일반 경로를 유지하고 적응형 윈도우를 키운다. 중간 이하로 낮으면 다음 청크를 guarded로 표시해 전송 전 활성 병렬 윈도우를 한 칸 낮춘다. 매우 낮으면 다음 업로드 청크를 병렬 처리 가능한 AES-GCM 폴팁으로 선제 전환하거나, 다운로드의 span과 병렬도를 다음 range 그룹 요청 전에 줄인다. 이는 암호학적 오라클이 아니라 전방향 혼잡/실패 방어다.
저장 모델
각 업로드 세션은 하나의 임시 파일을 미리 할당한다:
- 임시 파일:
data/tasfa/uploads/<upload_id>/upload.bin.part - 메타데이터:
data/tasfa/uploads/<upload_id>/meta.json - 빠른 이진 메타:
data/tasfa/uploads/<upload_id>/meta.bin - 상태:
data/tasfa/uploads/<upload_id>/state.json,data/tasfa/uploads/<upload_id>/state.bin - HTP level-0:
data/tasfa/uploads/<upload_id>/htp.bin - 세션 잠금:
data/tasfa/uploads/<upload_id>/session.lock
서버는 더 이상 blocks.bin이나 chunk_counts.bin을 유지하지 않는다. 청크 완료는 단일 비트맵 쓰기이다.
세션 메타데이터는 또한 다음 정점별 배열을 저장한다:
hash_tags— 청크당 SHA-512 16진수 문자열 배열magic_scalars— 청크당 균형 스칼라 배열htp_retry_targets— 현재 서버가 발행한 재시도 대상 목록htp_suspicion_scores— 현재 의심 순위htp_contraction_level— 적용된 서버 측 수축 통과 횟수
이들은 모든 청크 업로드 시 업데이트되며 완료 시 검증된다.
삭제 PIN
완료된 업로드는 일회용 삭제 PIN을 받는다. 평문 PIN은 12자리 16진수 문자열(6바이트 무작위)이며, 한 번만 반환되고 해시만 저장된다.
다운로드 프로토콜
- 클라이언트가
GET /.../handshake를 통해 핸드셰이크를 요청한다. - 서버는
session_id,session_token,chunk_size,chunk_count, 병렬성 힌트, 그리고 세션 암호화 키(stream_key_hex,stream_iv_seed_hex)를 반환한다. - 클라이언트는 적응형
span=...으로 청크 그룹을 가져온다. 세션 키가 있을 때 모든 청크는 AES-256-GCM으로 암호화되어 전송된다. - 클라이언트는 브라우저에서 Web Crypto API와 세션 키를 사용하여 청크를 복호화한다.
- 브라우저는 응답을 하나의 연속 버퍼로 조립한다.
비트맵을 통한 DoS 완화
업로드와 다운로드 상태는 모두 밀집 이진 비트맵(청크당 1바이트, '0' / '1')으로 추적된다.
업로드 측
- 서버는
state.bin에서 이미'1'로 표시된 청크 인덱스의 재전송을 거부한다. 단, 해당 청크가 서버의 재시도 대상 목록에 명시적으로 있을 경우는 예외이다. 공격자는 임의의 청크를 재생하여 디스크 I/O를 소모할 수 없다. state.bin은pwrite(..., 1, chunk_index)로 O(1) 원자적 쓰기가 가능하며, 핫 패스에서 JSON 파싱이 전혀 없다.complete핸들러는 비트맵을 다시 열어 설정된 비트를 세고,received_chunks == chunk_count가 될 때까지 최종 확정을 거부한다. 이는 잘린 파일 공격을 방지한다.
세션 강화
- 업로드 ID와 토큰은 각각 16바이트·24바이트 무작위 16진수 문자열이다.
- 세션 잠금(
session.lock에 대한flock)은 동시 요청으로부터 경쟁하는 비트맵 업데이트를 방지한다.
워커 스케줄러
서버는 고정형 라운드 로빈 워커 스케줄러를 사용한다. 워커 수는 CPU 코어 수와 같다(최소 2, 최대 64). 동일한 upload_id에 속하는 청크는 캐시 지역성과 순서를 위해 항상 동일한 워커로 전달된다. 작업은 워커별 큐를 통해 제출되고 조건 변수로 신호된다.
비동기 최종화
POST /file/upload/complete는 비동기적으로 처리된다. 첫 번째 호출은 202 Accepted와 {"processing": true}를 반환하고 백그라운드 최종화 워커를 시작한다. 이후 호출은 최종화 캐시를 폴링하며, 워커가 완료되면 캐시된 상태와 본문이 즉시 반환된다. 이를 통해 장시간 실행되는 HTP 검증과 최종화 작업이 HTTP 연결을 차단하지 않도록 한다.
자체 검토 체크리스트
| 질문 | 답변 |
|---|---|
| Q1: 클라이언트가 수리 대수를 계산하는가? | 아니오. 클라이언트는 무지한 재전송 에이전트이다. 모든 의심 도출, 신뢰도 점수, 비용 임계값, 수축 로직은 서버 측 전용이다. |
| Q2: 수축 전에 복구 비용 임계값이 명시적으로 평가되는가? | 예. htp_repair_worthwhile은 재시도_비용_추정치(바이트 × RTT)와 수리_비용_추정치(서버 분석 오버헤드)를 비교한다. 의심_수 < 3이거나 재시도가 더 저렴하면 false를 반환하여 직접 재시도로 폴백한다. |
| Q3: 부분 그룹이 제로 패딩되는가? | 아니오. 오직 완전한 6슬롯 그룹(chunk_count / 6)만 검증된다. 불완전한 마지막 그룹은 완전히 제외된다. |
| Q4: 응답에 이진 플래그가 아닌 의심 점수가 포함되는가? | 예. 모든 needs_retry 응답은 suspicion_scores를 {chunk_index, score} 객체로 포함한다. |
| Q5: 수축이 원래 그룹 토폴로지를 보존하는가? | 예. htp_contract_groups는 각 원본 완전 그룹을 단일 고위준 정점으로 취급하며, 의심 청크를 그룹 간에 재셔플하지 않는다. |
| Q6: 성공적인 재전송 시 재시도 대상이 지워지는가? | 예. handler_file_upload는 재시도 재전송을 수락한 후 htp_retry_targets에서 해당 청크를 제거한다. |
| Q7: 서버가 순서 뒤바뀐 스칼라를 swap으로 직접 복구하는가? | 예. htp_try_swap_repair는 6슬롯 그룹의 720가지 전체 순열을 탐색하여 불변식을 복원하는 순열을 찾으면 htp.bin 레코드를 즉시 재배열하고 해당 그룹을 재전송 없이 통과시킨다. |