--- title: branch / feature-file-resource-handling-contract source_type: branch-note status: raw id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-023 kind: project-work-item project: ca-skeleton-operational-contract work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-023 inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1] refines: [] overrides: [] depends_on: [] contract_packet: 1 branch: feature-file-resource-handling-contract parent_branch: related_projects: [ca-skeleton] tags: [branch, ca-skeleton, file, resource] created: 2026-05-22 target_merge: status_label: in-progress contract_packet_sha256: 7db5c6a4eb77af61706b5f4688cf721f786789638595dca733d334c5dca3d3c0 --- # branch: feature-file-resource-handling-contract > Layer: `raw/branch-notes/` — file/resource 처리 실패 기준을 정의합니다. ## 부모 (필수) - **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]] > ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다. ## 브랜치 계약 패킷 - **생성 시 프로젝트 개정**: `1` - **패킷 스키마**: `contract_packet: 1` - **완료 조건**: file size·type·storage boundary test가 통과한다 ### 상속한 프로젝트 결정 | Decision Ref | Project Summary | Branch Application | Source | |---|---|---|---| | `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1` | Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 | file/resource 처리의 application·adapter 책임 경계에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] | ### 브랜치 지역 결정 > 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다. | Decision ID | Decision | Relation | Supporting Claims | Status | |---|---|---|---|---| ### 선언한 예외 | Override ID | Overrides | Reason | Approval | Status | |---|---|---|---|---| ## 묶음 - [[raw/company-tech-blogs/file-clamav-icap-gateway-scan]] - [[raw/official-docs/file-s3-presigned-url-upload]] - [[raw/official-docs/file-tus-resumable-upload-protocol]] - [[raw/official-docs/iana-media-types-registry]] - [[raw/official-docs/jdk-files-createtempfile]] - [[raw/official-docs/nginx-client-max-body-size]] - [[raw/official-docs/owasp-file-upload-cheat-sheet]] - [[raw/official-docs/owasp-path-traversal]] - [[raw/official-docs/spring-boot-multipart-reference]] - [[raw/official-docs/spring-mvc-async-streaming]] - [[raw/official-docs/spring-streaming-response-body]] > 본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화. ### 오류 기록 (본 feature 작업 중 발생) - (없음 — 현재 documented-only 단계) ### 면접 준비 (이 작업에서 나올 수 있는 면접 질문) - (없음 — Phase C2 실 구현 단계에 누적) ## 목표 multipart 실패만으로는 파일 처리 기준이 부족합니다. upload size, temp file cleanup, streaming failure, content type sniffing, path traversal 방지를 skeleton 기준에 포함해야 합니다. - 이슈: - PR: ## 범위 ### 포함 범위 - upload size limit. - multipart parse failure. - temp file cleanup. - download streaming failure. - content type sniffing 금지. - path traversal 방지. - resource exhaustion 분류. ### 제외 범위 - 실제 object storage adapter 구현. - antivirus scan 구현. - CDN/download product policy. ## TODO > TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Decisionized Work Items" / "테스트 계약" 참조. upload size/multipart parse/temp cleanup/streaming/content-type allowlist/path traversal/resource exhaustion/antivirus 위치 모두 결정 라인 또는 표 row로 반영됨. 잔존 TODO 없음. ## Work Item Contract 각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 `기준 작성`으로 남아 있으면 이 branch는 완료로 보지 않습니다. | field | required | rule | | --- | --- | --- | | Decision | yes | 구현자가 선택해야 하는 기본값 | | Allowed | yes | 허용되는 예외와 조건 | | Forbidden | yes | 절대 금지되는 구현/문서 상태 | | Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 | | Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 | | Failure condition | yes | review/build에서 실패로 판정할 상태 | | Canonical extraction target | yes | `wiki/projects` 승급 위치 | ## 진행 중 메모 - file/resource handling은 API contract와 runtime lifecycle 양쪽에 걸칩니다. ## 결정 사항 (decisions) - 2026-05-22: file/resource handling을 별도 운영 표면으로 분리. - 2026-05-22: antivirus/file scanning은 기본 off. 활성화 위치는 gateway, async worker, app inline 중 하나로 명시해야 하며 미정이면 업로드 feature 승급 불가. - 2026-05-22: upload size limit 기본값은 10MB, file sample은 core v1에 포함하지 않음. - 2026-05-22: size limit enforcement layer SSOT = Spring `spring.servlet.multipart.max-file-size` 10MB + global request size 12MB. gateway/WAF는 보조(20MB hard limit). Spring 단의 enforcement가 실패 시 envelope 응답 보장. - 2026-05-22: 3계층 분리는 의도된 defense-in-depth: gateway 20MB는 raw 413 직격 차단 (envelope 우회), global 12MB는 multipart 외 raw body 한도, Spring 10MB는 multipart 단일 file 한도. 모든 한도 위반은 Spring 단에서 분류되어 envelope 응답으로 변환. - 2026-05-22: temp file cleanup trigger = (1) success/failure on close (try-with-resources), (2) startup sweeper for orphaned files older than 1h, (3) JVM shutdown hook은 backup. file >1h not closed → orphan. - 2026-05-22: allowed content-type allowlist starting set = image/jpeg, image/png, image/webp, application/pdf, text/csv, application/zip. 추가는 endpoint별 registry 등록. - 2026-05-22: streaming download backpressure = response timeout 60s, max stream 100MB. 초과 시 truncate + ERROR log. - 2026-05-22: file storage abstraction은 outbound = object store call이 EXTERNAL_OUTBOUND_ALLOWED capability 요구. - 2026-05-22: antivirus default = scan position = "gateway" (외부 upload-가능 endpoint), in-app 검증은 disabled. 활성화 시 별도 worker로 분리. ## 결정-근거 매핑 > 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. Company-tech-blog / vendor blog 인용은 사례 (`company-case-study`) 로만 사용, 공식 best practice 단정 금지. | Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk | |---|---|---|---|---| | D1 | file/resource handling 을 별도 운영 표면으로 분리 | UNSUPPORTED_DECISION — 내부 조직/스코프 결정 | `internal-only` | 다른 branch (lifecycle/outbound) 와 책임 경계 lint 필요 | | D2 | antivirus/file scanning default = off, 활성화 위치는 gateway/worker/app 중 1개 명시 강제 | `raw/company-tech-blogs/file-clamav-icap-gateway-scan.md#CLAMAV-ICAP-C1`, `raw/company-tech-blogs/file-clamav-icap-gateway-scan.md#CLAMAV-ICAP-C2` (ICAP = `official-standard` strength) | `official-standard + official-vendor-doc` | gateway 위치 default 권고는 `company-case-study` 영역 — 공식 best practice 단정 금지 | | D3 | upload size limit default = 10MB, file sample core v1 미포함 | **메커니즘 SUPPORTED**: `raw/official-docs/spring-boot-multipart-reference.md#SB-MULTIPART-C1` (Servlet 5 `Part` API 채택, `official-vendor-doc`), `raw/official-docs/spring-boot-multipart-reference.md#SB-MULTIPART-C2` (Spring Boot default per-file 1MB / per-request 10MB, `official-vendor-doc`). **정량값 UNSUPPORTED_DECISION**: per-file 10MB default 는 Spring Boot upstream default (1MB) 와 다르며 ca-tmpl 자체 결정 — 외부 권고 부재 | `official-vendor-doc (mechanism only)` | 10MB 정량값은 ca-tmpl 자체 결정 — endpoint registry override 정책으로 보완 필요. 'file sample core v1 미포함' 은 internal scope 결정 (UNSUPPORTED) | | D4 | size limit enforcement SSOT = Spring (multipart 10MB + global request 12MB), gateway/WAF 는 보조 (20MB hard) | **메커니즘 SUPPORTED**: `raw/official-docs/spring-boot-multipart-reference.md#SB-MULTIPART-C3` (`MultipartProperties` 가 `spring.servlet.multipart` prefix 로 max size / 저장 위치 / disk flush threshold override 가능, `official-vendor-doc`), `raw/official-docs/spring-boot-multipart-reference.md#SB-MULTIPART-C4` (`max-file-size=-1` 로 unlimited 설정 가능, `official-vendor-doc`), `raw/official-docs/nginx-client-max-body-size.md#NGINX-CMB-C1` (`client_max_body_size size;` syntax, `official-vendor-doc`), `raw/official-docs/nginx-client-max-body-size.md#NGINX-CMB-C4` (초과 시 HTTP 413 응답, `official-vendor-doc`). **정량값 UNSUPPORTED_DECISION**: 12MB global / 20MB gateway 정량값은 ca-tmpl 자체 결정 — 외부 권고 부재 | `official-vendor-doc (mechanism only)` | 12MB / 20MB 정량값은 ca-tmpl 추론. nginx default 는 1MB (`NGINX-CMB-C2`) 임을 명시 — 20MB 는 의도적 override | | D5 | 3계층 분리 (gateway 20MB / global 12MB / Spring 10MB) 는 defense-in-depth | **SUPPORTED**: `raw/official-docs/owasp-file-upload-cheat-sheet.md#OWASP-FUP-C1` (extension allowlist 만으로는 불충분 → 다층 검증 필요, `official-reference`), `raw/official-docs/owasp-file-upload-cheat-sheet.md#OWASP-FUP-C2` (Content-Type 헤더 신뢰 불가 → server-side 검증 별도 필요, `official-reference`), `raw/official-docs/owasp-file-upload-cheat-sheet.md#OWASP-FUP-C3` (UUID/GUID 랜덤 파일명 essential, `official-reference`) | `official-reference` | OWASP cheatsheet 는 reference (표준 아님). 3계층 size 분리 자체는 size 검증의 defense-in-depth — OWASP 가 직접 '3-layer size limit' 권고하는 raw 인용은 없음, 다층 검증 원칙 일반화 | | D6 | temp file cleanup 3 trigger: try-with-resources + startup sweeper (>1h orphan) + JVM shutdown hook (backup) | **메커니즘 SUPPORTED**: `raw/official-docs/jdk-files-createtempfile.md#JDK-TEMPFILE-C6` (`DELETE_ON_CLOSE` 옵션으로 close 시 자동 삭제, `official-vendor-doc`), `raw/official-docs/jdk-files-createtempfile.md#JDK-TEMPFILE-C7` (shutdown-hook 또는 `File.deleteOnExit()` 로 자동 삭제 가능, `official-vendor-doc`). **정량값 UNSUPPORTED_DECISION**: 1h orphan threshold 는 ca-tmpl 자체 결정 — JDK doc 은 threshold 정량값 권고 없음 | `official-vendor-doc (mechanism only)` | 1h orphan threshold 외부 권고 부재. `JDK-TEMPFILE-C7` 의 `deleteOnExit()` 는 SIGKILL 등 abnormal termination 보장 없음 — startup sweeper 가 그 gap 메움 | | D7 | content-type allowlist starting set = image/jpeg, image/png, image/webp, application/pdf, text/csv, application/zip | **SUPPORTED**: `raw/official-docs/iana-media-types-registry.md#IANA-MEDIA-C1` (Media Types 의 assignment/listing 은 IANA 단일 registry, `official-standard`), `raw/official-docs/iana-media-types-registry.md#IANA-MEDIA-C5` (top-level types: `application`, `image`, `text`, ... — allowlist 6종이 모두 IANA top-level 내, `official-standard`), `raw/official-docs/owasp-file-upload-cheat-sheet.md#OWASP-FUP-C5` (webroot 밖 저장 + administrative access only, `official-reference`) | `official-standard + official-reference` | 6종 starting set 선정 자체는 ca-tmpl 도메인 결정 — IANA 는 registry 권위만, endpoint 별 권고 없음. 추가 endpoint registry 등록 정책으로 보완 | | D8 | streaming download = response timeout 60s + max stream 100MB, 초과 시 truncate + ERROR log | **메커니즘 SUPPORTED**: `raw/official-docs/spring-streaming-response-body.md#SPRING-STREAM-RB-C3` (`StreamingResponseBody` 의 명시된 use case = file download, `official-vendor-doc`), `raw/official-docs/spring-streaming-response-body.md#SPRING-STREAM-RB-C4` (`ResponseEntity` body 로 사용 가능 — status/header 커스터마이즈, `official-vendor-doc`). **정량값 UNSUPPORTED_DECISION**: 60s timeout / 100MB max stream / truncate 정책 모두 ca-tmpl 자체 결정 — Spring doc 은 정량값 권고 없음 | `official-vendor-doc (mechanism only)` | timeout 60s 는 Spring default 의존 (`SPRING-STREAM-RB-C8` — 컨테이너 의존) 과 다른 명시값. truncate 동작 자체는 Spring 가 보장하지 않음 — 자체 구현 필요 | | D9 | file storage abstraction = outbound, object store call 은 EXTERNAL_OUTBOUND_ALLOWED capability 요구 | UNSUPPORTED_DECISION — 내부 capability 모델 결정 | `internal-only` | capability 모델의 lint 필요 | | Path-traversal claim | filename 입력 검증 = normalized storage key only + opaque key (raw path passthrough 금지) — Decisionized Work Items 의 `path traversal` row 근거 | **SUPPORTED**: `raw/official-docs/owasp-path-traversal.md#OWASP-PT-C1` (path traversal = web root 밖 파일/디렉토리 접근 공격 정의, `official-reference`), `raw/official-docs/owasp-path-traversal.md#OWASP-PT-C2` (공격 벡터: `../` sequence + variation + absolute path, `official-reference`), `raw/official-docs/owasp-path-traversal.md#OWASP-PT-C3` (방어 원칙: "known good only" allowlist, sanitize 금지, `official-reference`) | `official-reference` | OWASP community wiki 는 reference (표준 아님). URL encoding (`OWASP-PT-C4`) / null byte (`OWASP-PT-C5`) variation 도 별도 검증 필요 — opaque key 정책이 모든 variation 차단 가정은 별도 contract test 필요 | | D10 | antivirus default scan position = gateway, in-app disabled, 활성화 시 별도 worker 분리 | `raw/company-tech-blogs/file-clamav-icap-gateway-scan.md#CLAMAV-ICAP-C2` (ICAP `official-standard`), `raw/company-tech-blogs/file-clamav-icap-gateway-scan.md#CLAMAV-ICAP-C3` (ICAP virus scan use case, `official-standard`), `raw/company-tech-blogs/file-clamav-icap-gateway-scan.md#CLAMAV-ICAP-C1` (ClamAV daemon model, `official-vendor-doc`) | `official-standard + official-vendor-doc` | ICAP 가 HTTPS E2E TLS 환경에서 적용 어려움 (raw 메모에 명시) — 본 결정의 제약. gateway 가 default 라는 정량 권고는 ca-tmpl 자체 추론 | | D11 | 대안 1 (Direct S3 presigned URL upload) — app via 3-layer 우회 가능하나 antivirus 위치 분리 필요 | `raw/official-docs/file-s3-presigned-url-upload.md#FS3-PRE-C1`, `raw/official-docs/file-s3-presigned-url-upload.md#FS3-PRE-C2`, `raw/official-docs/file-s3-presigned-url-upload.md#FS3-PRE-C4`, `raw/official-docs/file-s3-presigned-url-upload.md#FS3-PRE-C5` | `official-vendor-doc` | post-upload async scan + quarantine bucket 패턴 별도 설계 필요 (raw 메모 참조) | | D12 | 대안 2 (tus.io resumable upload) — 100MB+ 영상 적합하나 "1h orphan cleanup" 충돌 위험 | `raw/official-docs/file-tus-resumable-upload-protocol.md#TUS-RUP-C1`, `raw/official-docs/file-tus-resumable-upload-protocol.md#TUS-RUP-C3`, `raw/official-docs/file-tus-resumable-upload-protocol.md#TUS-RUP-C4`, `raw/official-docs/file-tus-resumable-upload-protocol.md#TUS-RUP-C5` | `official-standard` | tus session timeout 과 orphan threshold 분리 필요 — 채택 시 D6 의 1h threshold 수정 | ## 검증해야 할 주장 | Claim | Why uncertain | How to verify | Status | |---|---|---|---| | Spring 10MB + global 12MB + gateway 20MB 3계층이 ca-tmpl 트래픽 프로파일에 적합 | 정량값 외부 권고 부재 (D3/D4/D5) | k6 부하테스트로 413 응답 비율 + Spring multipart parser 동작 확인 | `planned` | | Temp file cleanup 3 trigger 가 모두 정확히 동작 (1h orphan 정확 식별) | JDK 공식 doc 인용 부재 (D6) | `TempFileCleanupContractTest` 로 정상/예외/timeout 3 경로 cleanup 확인 + startup sweeper orphan(>1h) 삭제 verify | `planned` | | Antivirus gateway 위치가 ca-tmpl 의 HTTPS termination 정책과 호환 | ICAP 가 HTTPS E2E TLS 환경에서 적용 어려움 (raw 메모) | gateway HTTPS termination 정책 확인 + ICAP server 통합 PoC | `needs-confirmation` | | ClamAV signature DB 갱신 주기 + 운영 책임 주체 (gateway team vs app team) | raw 에 명시 없음 (Usage Boundary 참조) | 운영 협약 문서 작성 + signature update cron 확인 | `needs-confirmation` | | Direct S3 대안 채택 시 EXTERNAL_OUTBOUND_ALLOWED capability 매핑 | raw `FS3-PRE-*` 는 S3 메커니즘만 보장, ca-tmpl 자체 capability 모델과의 매핑은 별도 | capability 모델 contract test + signing 호출 경로 추적 | `planned` | | tus 채택 시 session timeout 과 orphan threshold 가 정상 case 를 삭제하지 않음 | `TUS-RUP-C5` 는 max-size 만 정의, session lifetime 침묵 | tus session 정책 + ca-tmpl orphan threshold 분리 contract test | `planned` | | Content-type allowlist 6개 starting set 이 ca-tmpl 도메인 endpoint 별로 충분 | IANA registry 또는 endpoint 별 권고 부재 (D7) | endpoint별 use case 인터뷰 + allowlist 누락 endpoint inventory | `needs-confirmation` | | Streaming download 100MB / 60s timeout 이 적합 | 정량값 외부 권고 부재 (D8) | 실제 파일 크기 분포 측정 + truncate 발생률 확인 | `planned` | | Path traversal opaque key 정책이 모든 upload 경로 (legacy 포함) 적용 | raw 인용 부재 — OWASP 또는 Spring Security 공식 doc 권고 | `traversal test` + storage layer code review | `planned` | ## Decisionized Work Items | item | Decision | Allowed | Forbidden | Required test | | --- | --- | --- | --- | --- | | upload size | 10MB default | endpoint override with registry | unlimited upload | oversized upload test | | scanning | off by default, owner required if enabled | gateway/worker/app inline | "somewhere scans it" assumption | scan owner checklist | | path traversal | normalized storage key only | object-store opaque key | raw path passthrough | traversal test | | temp cleanup | bounded temp dir + cleanup on failure | streaming direct to storage | orphan temp files | cleanup test | ## 구현 가이드 현재 `documented-only` 단계이며 구현 위치·클래스·메커니즘 anchor는 아직 고정되지 않았다. 구현 결정은 기존 `## Decision Evidence Map / 결정-근거 매핑`의 D-row를 변경하지 않고 후속 구현 단계에서 연결한다. ## 엣지·실패·의존 - **실패·엣지 경로**: 아래 `## 테스트 계약`의 oversized upload, temp cleanup, path traversal, streaming failure, antivirus 위치 검사를 따른다. - **다른 계약 의존**: API contract와 runtime lifecycle 양쪽 경계를 소비하며, object store 호출 capability는 D9가 정의한 outbound 경계를 따른다. ## 테스트 계약 - oversized upload가 generic 500으로 처리되면 실패. - temp cleanup contract: 다음 3 trigger가 모두 구현되어야 함: (a) success/failure on close (try-with-resources), (b) startup sweeper for orphaned files > 1h, (c) JVM shutdown hook backup. 측정 방법: contract test `TempFileCleanupContractTest`에서 `File.createTempFile` 후 정상/예외/timeout 3 경로 각각의 cleanup 확인. orphan(>1h not closed) file이 startup sweeper에 의해 삭제되는지 verify. - path traversal input이 storage path로 전달되면 실패. - download stream failure가 traceId 없이 로그되면 실패. - antivirus 위치 명시 강제: `APP_FILE_UPLOAD_ENABLED=true`이면 결정 사항에 antivirus 위치(`gateway` 또는 `worker` 또는 `off` 중 1개)가 명시되어 있어야 함. 측정 방법: branch note의 결정 사항 라인에서 `antivirus.position` token grep. 미명시 시 readiness fail. default는 `gateway` 권고. ## 근거 (필수, 최소 1개+) > 본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조. | Source | 정당화하는 결정 | |---|---| | [[raw/company-tech-blogs/file-clamav-icap-gateway-scan]] | ClamAV + RFC 3507 ICAP (ca-tmpl antivirus gateway default의 표준 근거 | | [[raw/official-docs/file-s3-presigned-url-upload]] | app/gateway 부담 0 vs antivirus 위치 분리 필요 + quarantine bucket | | [[raw/official-docs/file-tus-resumable-upload-protocol]] | 100MB+ 영상에 적합 vs ca-tmpl "1h orphan cleanup"이 session 잘못 삭제 가능 | | [[raw/official-docs/spring-boot-multipart-reference]] | D3 (upload size 메커니즘) + D4 (Spring multipart enforcement SSOT 메커니즘) — `official-vendor-doc` | | [[raw/official-docs/nginx-client-max-body-size]] | D4 (gateway/WAF 보조 layer 메커니즘 — `client_max_body_size` directive) — `official-vendor-doc` | | [[raw/official-docs/owasp-file-upload-cheat-sheet]] | D5 (3-layer defense-in-depth: extension/Content-Type/저장 위치 다층 검증) + D7 (webroot 밖 저장) — `official-reference` | | [[raw/official-docs/jdk-files-createtempfile]] | D6 (temp file cleanup 메커니즘: `DELETE_ON_CLOSE` + shutdown-hook + `deleteOnExit`) — `official-vendor-doc` | | [[raw/official-docs/iana-media-types-registry]] | D7 (content-type allowlist 6종이 IANA top-level types 내) — `official-standard` | | [[raw/official-docs/spring-streaming-response-body]] | D8 (streaming download 메커니즘: `StreamingResponseBody` + `ResponseEntity`) — `official-vendor-doc` | | [[raw/official-docs/owasp-path-traversal]] | Path-traversal claim (공격 정의 + 벡터 + "known good only" allowlist 방어 원칙) — `official-reference` | ## 외부 근거 / 대안 조사 (2026-05-22 — Group G-J: File / Resource Handling) 본 branch의 Spring 10MB + global 12MB + gateway 20MB 3-layer + content-type allowlist 6종 + temp orphan 1h cleanup + antivirus gateway default + path traversal opaque key 결정에 대한 외부 source. - **채택 결정 (app via 3-layer + gateway antivirus + ClamAV/ICAP)**: - [[raw/company-tech-blogs/file-clamav-icap-gateway-scan]] — ClamAV + RFC 3507 ICAP (ca-tmpl antivirus gateway default의 표준 근거) - **검토한 대안**: - **대안 1: Direct S3 presigned URL upload (app via 우회)** — [[raw/official-docs/file-s3-presigned-url-upload]] (app/gateway 부담 0 vs antivirus 위치 분리 필요 + quarantine bucket) - **대안 2: tus resumable upload protocol** — [[raw/official-docs/file-tus-resumable-upload-protocol]] (100MB+ 영상에 적합 vs ca-tmpl "1h orphan cleanup"이 session 잘못 삭제 가능) - **대안 3: in-app ClamAV daemon scan** — `file-clamav-icap-gateway-scan` 동일 source 안에서 in-app/gateway/async 3종 비교 (in-app은 app instance에 daemon dependency) - **대안 4: Post-upload async scan (S3 + Lambda ClamAV)** — 동일 source (app/gateway 부담 0 vs scan 완료 전 객체 존재 → quarantine bucket 분리 필요) - **비교 핵심**: ca-tmpl "gateway default" 선택은 app instance scaling과 무관한 일정 throughput + in-app daemon dependency 회피. HTTPS E2E TLS 환경에서는 ICAP 적용 어려움 — 그 경우 post-upload async가 대안. tus 채택 시 ca-tmpl "1h orphan cleanup"은 session 정상 case도 삭제할 위험 — session ↔ orphan threshold 분리 보강 필요. ## 마주친 문제 - 아직 없음. ## 관련 일일 노트 - 없음. ## 완료 후 정리 - PR 링크: - 리뷰 메모: - 머지 결과 / 배포 환경: - **wiki 추출 대상** (verified만, `wiki/projects/`로만 추출): - `actually-implemented` 항목: - `locally-verified` 항목: - `prod-verified` 항목: - **추출하지 않을 항목** (planned / documented-only / abandoned):