TL;DR

  • 이미 압축된 파일은 ZIP STORED 모드를 사용하고, 텍스트 계열은 DEFLATED로 선택해 CPU와 처리 시간을 최적화한다.
  • @Async는 프록시 기반이라 self-invocation을 피해야 하며, AsyncUncaughtExceptionHandler와 적절한 Executor 정책으로 운영 안정성을 확보해야 한다.
  • 대용량 업로드는 S3 Multipart Upload를 기본값으로 두고 part 크기·재시도·abort/lifecycle 정리를 함께 설계해야 한다.

1. 개념

이 문서는 ZIP 생성, Spring 비동기 처리(@Async), S3 Multipart Upload를 하나의 파이프라인으로 연결해 대용량 파일 처리 시스템의 성능·신뢰성·운영성을 높이는 아키텍처 패턴을 설명한다.

2. 배경

실무에서는 대용량 파일 번들링과 업로드 과정에서 CPU 과소비, 스레드풀 포화, 예외 누락, 업로드 실패 재시도 비용 같은 문제가 동시에 발생한다. 개별 기술 최적화만으로는 해결이 어렵고, 압축·비동기·스토리지 전송 전략을 함께 설계해야 한다.

3. 이유

압축 방식(STORED/DEFLATED), 실행 정책(CallerRunsPolicy), 예외 처리(AsyncUncaughtExceptionHandler), 업로드 방식(Multipart)을 목적에 맞게 조합하면 처리량과 안정성을 동시에 확보할 수 있다. 특히 실패 복구 비용과 운영 리스크를 크게 줄일 수 있다.

4. 특징

파일 유형별 ZIP 전략, 프록시 기반 @Async 동작 원리와 self-invocation 회피, 큐 포화 시 백프레셔 설계, S3 파트 단위 재전송/정리 전략까지 다뤄 실무 적용성을 높인 것이 핵심 특징이다.

5. 상세 내용

ZIP Streaming · @Async · S3 Multipart Upload 실전 패턴 완전 가이드

ZipOutputStream STORED mode, CallerRunsPolicy, AsyncUncaughtExceptionHandler, S3 Multipart Upload, @Async Self-Invocation Proxy Bypass에 대한 종합 연구


목차

  1. 용어 사전 (Terminology)
  2. 등장 배경과 이유 (Why It Emerged)
  3. 역사적 기원 (Historical Origins)
  4. 학술적/이론적 배경 (Academic Foundation)
  5. 진화 타임라인 (Evolution Timeline)
  6. 대안 비교 (Alternatives & Trade-offs)
  7. 상황별 최적 선택 (When Each is Effective)
  8. 실전 베스트 프랙티스 (Best Practices)
  9. 빅테크 실전 사례 (Big Tech Strategies)
  10. References

1. 용어 사전

ZIP 관련

용어 풀네임 / 의미 어원
ZIP - Phil Katz가 ARC보다 “빠르다(zippy)”는 의미에서 명명. 1989년
PKWARE Phil Katz’s softWARE 창시자 Phil Katz(1962~2000)의 이니셜
STORED Method 0 “저장만 한다” - 압축 없이 원본 데이터를 그대로 아카이브에 보관
DEFLATED Method 8 “공기를 빼다(deflate)” - LZ77 + Huffman 코딩 조합 압축
APPNOTE.TXT Application Note PKWARE가 공개한 ZIP 포맷 공식 사양 문서명
CRC-32 Cyclic Redundancy Check, 32-bit 순환(Cyclic) 다항식 코드 기반 잉여(Redundancy) 검사(Check). W. Wesley Peterson이 1961년 발명
LZ77 Lempel-Ziv 1977 Abraham Lempel과 Jacob Ziv가 1977년 발표한 슬라이딩 윈도우 기반 사전 압축
ZIP64 ZIP 64-bit Extension 4GiB/65,535개 항목 한계를 극복하는 64비트 확장. 2001년 도입

Java Concurrency 관련

용어 풀네임 / 의미 어원
JSR-166 Java Specification Request #166 JCP 등록 순번 166. “Concurrency Utilities” 사양
CallerRunsPolicy Caller + Runs + Policy 거부된 태스크를 제출자(caller) 스레드가 직접 실행하는 정책
AbortPolicy Abort + Policy 거부 시 RejectedExecutionException 예외를 던지는 정책
DiscardPolicy Discard + Policy 거부된 태스크를 조용히 삭제하는 정책
DiscardOldestPolicy Discard + Oldest + Policy 큐의 가장 오래된 태스크를 제거 후 재시도
Backpressure Back + Pressure 소비자 처리 용량 초과 시 생산자 흐름을 제어하는 메커니즘

Spring Async 관련

용어 풀네임 / 의미 어원
@Async Asynchronous 메서드를 별도 스레드풀에서 비동기 실행하도록 지시
AsyncUncaughtExceptionHandler Async + Uncaught + Exception + Handler void @Async 메서드의 잡히지 않은 예외를 처리하는 전략 인터페이스
@EnableAsync Enable + Async Java Config로 비동기 지원을 활성화하는 애노테이션
AsyncConfigurer Async + Configurer Executor와 예외 핸들러를 설정하는 인터페이스
Self-Invocation Self + Invocation 같은 클래스 내에서 this.method()로 자기 메서드를 호출하는 것

Spring AOP 관련

용어 풀네임 / 의미 어원
AOP Aspect-Oriented Programming 관점 지향 프로그래밍. Gregor Kiczales가 1997년 ECOOP에서 제안
CGLIB Code Generation Library 런타임 서브클래스 동적 생성 바이트코드 조작 라이브러리
JDK Dynamic Proxy JDK Dynamic Proxy Java 1.3(2000)에서 도입된 인터페이스 기반 프록시 메커니즘
AspectJ Aspect + Java Xerox PARC에서 2001년 개발한 완전한 AOP 구현체
CTW Compile-Time Weaving 컴파일 시점에 Aspect 코드를 바이트코드에 삽입
LTW Load-Time Weaving 클래스 로딩 시점에 Aspect 코드를 삽입

AWS S3 관련

용어 풀네임 / 의미 어원
S3 Simple Storage Service “단순한 저장 서비스”. Werner Vogels가 강조한 단순성 설계 원칙 반영
Multipart Upload Multi + Part + Upload 하나의 객체를 여러 조각으로 나누어 업로드
ETag Entity Tag HTTP 리소스의 특정 버전을 식별하는 태그. S3에서는 MD5 해시
Pre-signed URL Pre + Signed + URL 서버가 미리 서명한 시한부 직접 접근 URL

2. 등장 배경과 이유

2.1 ZipOutputStream STORED Mode

문제: 이미 내부 압축된 파일(PDF, XLSX, PNG, JPEG, MP4)에 DEFLATE를 적용하면:

  • 파일 크기가 오히려 증가할 수 있음 (압축 헤더 오버헤드)
  • CPU를 불필요하게 소비
  • STORED 대비 처리 속도가 약 33% 느림

해결: ZIP 포맷은 설계상 각 파일마다 독립적으로 압축 방식을 선택할 수 있다. 하나의 아카이브 안에서 어떤 파일은 DEFLATED로, 어떤 파일은 STORED로 저장 가능. 이는 tar+gzip처럼 전체 아카이브를 단일 스트림으로 압축하는 방식과 근본적으로 다르다.

STORED 사용 시 CRC32/size/compressedSize 필수 설정 이유:

ZIP Local File Header 구조:

[4 bytes] Magic: 50 4B 03 04 ("PK\x03\x04")
[2 bytes] Version needed
[2 bytes] General purpose bit flag  <- 비트3이 핵심
[2 bytes] Compression method        <- STORED=0, DEFLATED=8
[4 bytes] CRC-32
[4 bytes] Compressed size
[4 bytes] Uncompressed size

DEFLATED는 Data Descriptor 메커니즘을 자동 사용하여 압축 후 CRC32/크기를 뒤에 기록. STORED는 “압축하지 않는다 = 변환하지 않는다”이므로 CRC32와 크기를 사전에 알고 있어야 한다는 설계 원칙. Java의 ZipOutputStream은 STORED 엔트리에 대해 헤더에 직접 값을 기록하므로 putNextEntry() 호출 전에 반드시 설정해야 한다.

2.2 CallerRunsPolicy

문제: ThreadPoolExecutor의 큐가 가득 차고 maxPoolSize에 도달하면 새 작업을 거부해야 함. 기본 AbortPolicy는 예외를 던져 태스크가 유실될 수 있음.

해결: CallerRunsPolicy는 거부된 태스크를 제출한 스레드가 직접 실행. 그 스레드가 작업하는 동안 새 태스크를 받지 못하므로 자연적 백프레셔(natural backpressure) 달성. 공식 Javadoc: “This provides a simple feedback control mechanism that will slow down the rate that new tasks are submitted.”

2.3 AsyncUncaughtExceptionHandler

문제: void 반환 @Async 메서드에서 발생한 예외는 기본적으로 조용히 소멸. Future를 반환하면 .get() 또는 .exceptionally()로 처리 가능하지만, fire-and-forget 패턴에서는 예외가 완전한 블랙홀(black hole)에 빠짐.

해결: Spring 4.1에서 SPR-8995 이슈(2012년 1월 Dwayne D’Souza 등록)를 해결하기 위해 AsyncUncaughtExceptionHandler 인터페이스 도입.

2.4 S3 Multipart Upload

문제: S3 초기(2006년) 단일 PUT 업로드의 한계:

  • 최대 5GB 크기 제한
  • 3GB 업로드 도중 실패하면 처음부터 재시작
  • 단일 TCP 스트림으로는 가용 대역폭 100% 활용 불가
  • 긴 업로드 시간 동안 연결 끊김 위험 증가

해결: 2010년 11월 10일 Multipart Upload 도입으로 최대 5TB, 병렬 업로드, 파트 단위 재전송 지원.

2.5 @Async Self-Invocation Proxy Bypass

문제: Spring AOP는 프록시 기반. 같은 클래스 내에서 this.asyncMethod() 호출 시 프록시를 거치지 않아 @Async, @Transactional, @Cacheable 등의 애노테이션이 동작하지 않음.

[외부 호출자] -> [Proxy] -> [Bean.methodA()] -> this.methodB()  <- 프록시 우회!
                ^ AOP 적용됨                    ^ AOP 미적용

this는 프록시 객체가 아닌 원본 빈 인스턴스를 가리키기 때문. Spring 공식 문서: “Self invocation is not going to result in the advice associated with a method invocation getting a chance to run.”

해결: 별도 클래스(BundleWorker)로 분리하여 외부 빈 주입을 통해 프록시 경유.


3. 역사적 기원

3.1 ZIP 포맷의 탄생 (1989)

1980년대 중반 Phil Katz(1962~2000)는 당시 지배적 압축 포맷 ARC와 호환되는 PKARC를 어셈블리로 제작. SEA가 상표권 침해 소송 제기 -> Katz는 합의 후 완전히 새로운 ZIP 포맷을 1989년 창제. APPNOTE.TXT를 public domain으로 공개한 것이 ZIP이 사실상 표준이 된 결정적 이유.

3.2 CRC-32의 발명 (1961)

W. Wesley Peterson(1924~2009)이 D.T. Brown과 공동으로 1961년 “Cyclic Codes for Error Detection” (Proceedings of the IRE)에서 CRC를 공식 발표. 1975년 Georgia Tech과 Mitre Corporation 연구자들이 CRC-32 다항식(0x04C11DB7)을 최적화. 이 다항식이 Ethernet, PNG, ZIP에 채택됨.

3.3 java.util.concurrent (2004)

Doug Lea(SUNY Oswego 교수)가 1998년경부터 util.concurrent 라이브러리를 독자 개발하여 public domain으로 배포. 이것이 JSR-166(2002년 제안)의 기반이 됨. Expert Group: Doug Lea, Josh Bloch, Joe Bowbeer, Brian Goetz, David Holmes, Tim Peierls. Java 5.0(2004년 9월)에서 정식 포함.

3.4 Spring @Async 도입 (2009)

  • Spring 3.0 (2009.12): @Async 최초 도입
  • Spring 3.1 (2011.12): @EnableAsync, AsyncConfigurer 인터페이스
  • Spring 4.1 (2014): AsyncUncaughtExceptionHandler 추가 (SPR-8995)
  • Spring Boot 2.1 (2018): TaskExecutionAutoConfiguration 강화, ThreadPoolTaskExecutor 기본 사용

3.5 AOP의 학술적 기원 (1997)

Gregor Kiczales 외 6인이 ECOOP 1997 (핀란드 Jyvaskyla)에서 “Aspect-Oriented Programming” 논문 발표 (LNCS 1241, Springer-Verlag). 2017년 ECOOP Test of Time Award 수상. 핵심 문제 제기: OOP는 횡단 관심사(crosscutting concerns)를 캡슐화할 수 없다 -> Aspect, Join Point, Pointcut, Advice, Weaving 개념 정의.

3.6 Amazon S3 출시 (2006)

2006년 3월 14일 정식 출시. API Version: 2006-03-01 (현재까지 유일한 버전 번호 – AWS의 하위 호환성 보장 철학). Multipart Upload는 2010년 11월 10일 도입.


4. 학술적/이론적 배경

4.1 DEFLATE의 계보

LZ77 (1977, Lempel & Ziv)
  "A Universal Algorithm for Sequential Data Compression"
  IEEE Transactions on Information Theory
  -> 2004 IEEE Milestone, 2021 Jacob Ziv IEEE Medal of Honor
       |
       v
Huffman Coding (1952, David Huffman)
  "A Method for the Construction of Minimum-Redundancy Codes"
  MIT 정보이론 수업 중 개발, Proceedings of the IRE
       |
       v
DEFLATE = LZSS(LZ77 변형) + Huffman
  Phil Katz가 PKZIP 2.04(1992)를 위해 설계
  RFC 1951 (1996.05): DEFLATE Compressed Data Format Specification v1.3
  RFC 1950 (1996.05): ZLIB Compressed Data Format Specification

4.2 Thread Pool의 이론적 기반

  • Doug Lea, “Concurrent Programming in Java: Design Principles and Patterns” (1996, Addison-Wesley)
    • Worker Thread Pattern, Thread-Per-Message Pattern 체계화
    • ThreadPoolExecutor의 직접적 설계 근거
  • JSR-166 (2002): https://www.jcp.org/en/jsr/detail?id=166
  • Brian Goetz et al., “Java Concurrency in Practice” (2006, Addison-Wesley)

4.3 AOP 원본 논문

  • Gregor Kiczales et al., “Aspect-Oriented Programming”, ECOOP 1997
    • URL: https://www.cs.ubc.ca/~gregor/papers/kiczales-ECOOP1997-AOP.pdf
  • AOP Alliance (2002): Spring이 최초로 org.aopalliance.intercept.MethodInterceptor 구현

4.4 REST 이론

  • Roy Fielding (2000), “Architectural Styles and the Design of Network-based Software Architectures” (UC Irvine 박사논문)
    • AWS S3는 이 REST 원칙을 실용적으로 구현한 초기 사례

5. 진화 타임라인

5.1 ZIP 포맷

1989 -- PKZIP 0.9 + APPNOTE.TXT 공개 (STORED + Shrink 방식)
1992 -- PKZIP 2.04c: DEFLATE 정식 탑재 (Method 8)
1996 -- RFC 1951 (DEFLATE), RFC 1950 (ZLIB), RFC 1952 (GZIP) 표준화
2001 -- ZIP64 도입 (4GiB -> 16EiB 확장)
2006 -- APPNOTE 6.3.0: UTF-8 파일명 지원
        java.util.zip: Java 7에서 ZIP64 공식 지원

5.2 Java Concurrency

1996 -- Doug Lea, "Concurrent Programming in Java" 1판
1998 -- util.concurrent 독립 라이브러리 공개
2002 -- JSR-166 공식 제안
2004 -- Java SE 5.0: java.util.concurrent 정식 포함, ThreadPoolExecutor + 4가지 거부 정책
2011 -- Java SE 7: ForkJoinPool 추가
2013 -- Java SE 8: CompletableFuture 추가
2023 -- Java SE 21: Virtual Threads 정식 출시

5.3 Spring @Async

2009 -- Spring 3.0: @Async 최초 도입 (기본: SimpleAsyncTaskExecutor)
2011 -- Spring 3.1: @EnableAsync, AsyncConfigurer 도입
2014 -- Spring 4.1: AsyncUncaughtExceptionHandler 추가
2018 -- Spring Boot 2.1: TaskExecutionAutoConfiguration, ThreadPoolTaskExecutor 기본 사용
2023 -- Spring Boot 3.2: Virtual Thread 지원 (spring.threads.virtual.enabled=true)

5.4 Spring AOP Proxy

2000 -- Java 1.3: JDK Dynamic Proxy 도입
2002 -- CGLIB 등장 (서브클래싱 방식)
2004 -- Spring 1.0: JDK Proxy 기본, 인터페이스 없으면 CGLIB
2006 -- Spring 2.0: AspectJ 어노테이션 방식 지원
2017 -- Spring Boot 2.0: proxyTargetClass=true 기본값 (CGLIB 기본)
현재 -- Spring 6.x: AOT(Ahead-Of-Time) 처리 지원 (GraalVM)

5.5 AWS S3

2006-03 -- S3 GA 출시 (API Version 2006-03-01)
2010-11 -- Multipart Upload 도입 (최대 5TB, 10,000 파트)
2016-04 -- Transfer Acceleration (CloudFront Edge 활용)
2018-04 -- S3 Select GA (SQL로 객체 내 데이터 추출)
2020-12 -- Strong Read-After-Write Consistency
2023    -- S3 Express One Zone, S3 Mountpoint

6. 대안 비교

6.1 ZIP 압축 메서드 비교

Method 압축률 압축 속도 해제 속도 메모리 Java 지원
STORED (0) 없음 (1:1) 최고속 최고속 최소 java.util.zip 기본
DEFLATED (8) 60~70% 절감 중간 빠름 낮음 java.util.zip 기본
BZIP2 (12) 70~75% 절감 느림 매우 느림 높음 Commons Compress
LZMA (14) 75~80% 절감 매우 느림 중간 매우 높음 Commons Compress
Zstandard (93) 65~75% 절감 최고속 최고속 낮음 Commons Compress 1.16+

핵심: Zstandard는 DEFLATE 수준의 압축률 + 9배 이상 빠른 속도. BZIP2는 해제 속도가 특히 나빠 프로덕션 지양.

Java ZIP 라이브러리 비교

기준 java.util.zip Apache Commons Compress zip4j
의존성 없음 (JDK 내장) commons-compress JAR zip4j JAR
메서드 지원 STORED, DEFLATED 전체 STORED, DEFLATED
AES 암호화 미지원 제한적 완전 지원
ZIP64 Java 7+ 자동 처리 지원
멀티코어 병렬 미지원 지원 제한적
권장 단순 작업 엔터프라이즈 암호화 필요 시

6.2 ThreadPoolExecutor 거부 정책 비교

정책 동작 작업 손실 예외 권장 상황
AbortPolicy (기본) RejectedExecutionException 있음 있음 명시적 오류 처리 필요 시
CallerRunsPolicy 호출 스레드가 직접 실행 없음 없음 작업 손실 불가 + 백프레셔
DiscardPolicy 조용히 삭제 있음 없음 로그/센서 등 손실 허용
DiscardOldestPolicy 큐의 가장 오래된 것 제거 있음 없음 최신 데이터 우선 시

대안 전략 비교

전략 장점 단점 위험도
Unbounded Queue 작업 손실 없음 OOM 위험, maxPoolSize 무의미 높음
Custom Handler 메트릭/알림 가능 구현 필요 낮음
ForkJoinPool CPU 활용 극대화 I/O 집약 비효율 낮음
Virtual Threads (Java 21+) I/O 블로킹 자동 처리 CPU 집약 비효율, 백프레셔 없음 낮음

6.3 Spring 비동기 프레임워크 비교

기준 Spring @Async CompletableFuture Kotlin Coroutines WebFlux/Reactor Virtual Threads
학습 곡선 낮음 중간 중간 높음 낮음
코드 가독성 높음 (선언적) 중간 (체이닝) 높음 (순차적) 낮음 (함수형) 높음
백프레셔 없음 없음 제한적 완전 지원 없음
예외 처리 낮음 (void 시) 높음 높음 중간 높음
Spring MVC 통합 완벽 완벽 부분적 별도 스택 완벽
디버깅 높음 중간 높음 낮음 높음

6.4 S3 업로드 방식 비교

방식 최대 크기 병렬 재시도 단위
Single PUT 5 GB 불가 전체 파일
Multipart Upload 5 TB 가능 실패 파트만
TransferManager (SDK v1) 5 TB 자동 파트 단위
S3TransferManager (SDK v2) 5 TB 자동 (CRT 기반) 파트 단위
Transfer Acceleration 5 TB Multipart 결합 파트 단위

SDK v1 vs v2 비교

기준 SDK v1 TransferManager SDK v2 S3TransferManager
기본 Threshold 16 MB 8 MB
기본 Part Size 5 MB 8 MB
비동기 모델 동기 + Future 완전 비동기 (CompletableFuture)
CRT 통합 없음 지원 (고성능 C 런타임)
현재 상태 유지보수 모드 적극 개발 중 (권장)

6.5 Spring AOP Self-Invocation 해결책 비교

해결책 구현 복잡도 침투성 스레드 안전 Spring 팀 권장
클래스 분리 (BundleWorker) 낮음 없음 안전 최우선 권장
@Lazy self-injection 낮음 낮음 안전 권장 (대안)
ApplicationContext.getBean() 중간 높음 안전 비권장
AopContext.currentProxy() 중간 높음 위험 (ThreadLocal) 비권장
AspectJ CTW/LTW 높음 없음 안전 완전한 해결 (복잡)

7. 상황별 최적 선택

7.1 ZIP 압축 방식 선택

이미 압축된 파일인가? (PDF, PNG, JPEG, XLSX, MP4, ZIP)
+-- YES -> STORED (Method 0)
|         - CPU 절약, ~33% 빠름
|         - CRC32/size/compressedSize 사전 설정 필수
+-- NO -> 압축 가능한 파일 (CSV, JSON, XML, TXT, SQL)
         +-- Java 표준만 사용 -> DEFLATED (Method 8)
         +-- Commons Compress 의존성 허용 -> Zstandard (최고 성능)

7.2 ThreadPoolExecutor 작업 유형별 선택

작업 유형 권장 방식
I/O 집약 (DB, HTTP, 파일) Virtual Threads (Java 21+)
CPU 집약 (압축, 암호화) ThreadPoolExecutor + bounded queue
재귀/분할정복 ForkJoinPool
혼합 작업 + 정밀 백프레셔 ThreadPoolExecutor + CallerRunsPolicy

7.3 CallerRunsPolicy 적합성 판단

시나리오 적합성 이유
백그라운드 배치 작업 적합 백프레셔 효과
짧은 CPU 작업 적합 큐 포화 방지
HTTP 요청 처리 스레드 부적합 Tomcat 스레드 고갈 위험
외부 API I/O 대기 매우 부적합 긴 블로킹

7.4 @Async 반환 타입 선택

비동기 메서드 결과가 필요한가?
+-- NO (fire-and-forget: 이메일, 로그)
|   +-- void 반환 + AsyncUncaughtExceptionHandler 필수 등록
+-- YES
    +-- 단순 결과 -> CompletableFuture @Async + .handle()
    +-- 복잡한 체이닝 -> CompletableFuture 직접 사용 (Spring executor 주입)

7.5 S3 업로드 전략 선택

파일 크기 권장 전략 이유
< 5 MB Single PUT 오버헤드 없음
5~100 MB Single PUT 또는 Multipart 네트워크 안정성에 따라
100 MB ~ 5 GB Multipart Upload AWS 공식 권장 (100 MB 이상)
> 5 GB Multipart 필수 Single PUT 불가
클라이언트 직접 업로드 Pre-signed URL Multipart 서버 대역폭 절약

7.6 Self-Invocation 해결책 선택

@Async/@Transactional이 같은 클래스 내에서 호출되는가?
+-- YES -> 해결 필요
|   +-- 리팩토링 가능? -> 클래스 분리 (BundleWorker 패턴) <- 최우선
|   +-- 클래스 분리 어려움? -> @Lazy self-injection
|   +-- 완전한 AOP 필요? -> AspectJ CTW (빌드 설정 변경)
+-- NO -> 문제 없음

8. 실전 베스트 프랙티스

8.1 ZipOutputStream STORED Mode

올바른 STORED 엔트리 작성 패턴

// 베스트 프랙티스: Two-pass 스트림 방식
public void addStoredEntry(ZipOutputStream zos, Path file, String entryName)
        throws IOException {
    // Pass 1: CRC 계산
    CRC32 crc = new CRC32();
    long size;
    try (InputStream is = Files.newInputStream(file)) {
        byte[] buf = new byte[8192];
        int read;
        long count = 0;
        while ((read = is.read(buf)) != -1) {
            crc.update(buf, 0, read);
            count += read;
        }
        size = count;
    }
    // Pass 2: ZIP에 기록
    ZipEntry entry = new ZipEntry(entryName);
    entry.setMethod(ZipEntry.STORED);
    entry.setSize(size);
    entry.setCompressedSize(size);  // STORED는 동일
    entry.setCrc(crc.getValue());
    zos.putNextEntry(entry);
    try (InputStream is = Files.newInputStream(file)) {
        is.transferTo(zos);
    }
    zos.closeEntry();
}
// 안티패턴: CRC32 없이 STORED 시도
ZipEntry entry = new ZipEntry("file.bin");
entry.setMethod(ZipEntry.STORED);
entry.setSize(data.length);
// entry.setCrc(???) <- 누락!
zos.putNextEntry(entry); // -> ZipException: STORED entry missing size, compressed size, or crc-32

Hybrid 전략: 파일 타입별 자동 선택

private static final Set<String> PRE_COMPRESSED = Set.of(
    "pdf", "xlsx", "docx", "png", "jpg", "jpeg", "gif",
    "mp4", "mp3", "zip", "gz", "7z", "webp", "avif"
);

public int selectCompressionMethod(String fileName) {
    String ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
    return PRE_COMPRESSED.contains(ext) ? ZipEntry.STORED : ZipEntry.DEFLATED;
}

주요 함정

함정 증상 해결책
CRC32 미설정 ZipException: STORED entry missing CRC-32 setCrc() 필수
size != compressedSize ZipException: bad STORED entry size STORED는 두 값 동일
setMethod() 누락 기본 DEFLATED로 동작 entry.setMethod(ZipEntry.STORED) 명시
HTTP 스트림에서 pre-calculate 응답 지연, OOM DEFLATED NO_COMPRESSION 또는 temp file
Thread safety 데이터 손상 인스턴스당 하나의 스레드만
4GB 이상 ZIP64 필요 Commons Compress ZipArchiveOutputStream 사용

HTTP 스트리밍 시 대안: STORED는 CRC32 사전 계산이 필요하므로, 크기 미상 동적 스트림에는 DEFLATED + Deflater.NO_COMPRESSION 조합이 사실상 STORED 수준 처리량 + 사전 계산 불필요.

8.2 ThreadPoolExecutor + CallerRunsPolicy

프로덕션 설정 예시

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int cpuCores = Runtime.getRuntime().availableProcessors();

        executor.setCorePoolSize(cpuCores * 2);       // I/O bound 기준
        executor.setMaxPoolSize(cpuCores * 4);
        executor.setQueueCapacity(200);               // Integer.MAX_VALUE 절대 금지
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("async-task-");
        executor.setRejectedExecutionHandler(
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        executor.initialize();
        return executor;
    }
}

스레드 풀 크기 계산 공식

CPU bound: corePoolSize = CPU 코어 수 + 1
I/O bound: corePoolSize = CPU 코어 수 x (1 + 대기시간/처리시간)
  예: 4코어, I/O 비율 10:1 -> 4 x 11 = 44

CallerRunsPolicy 핵심 함정: Tomcat 스레드 블로킹

정상 상태:         [Tomcat Thread] -> 큐에 제출 -> [AsyncThread] 처리
CallerRuns 발동:  [Tomcat Thread] -> 큐 꽉 참 -> [Tomcat Thread] 직접 처리
                  ^ 이 동안 해당 Tomcat 스레드는 HTTP 요청을 받지 못함!

HTTP 요청 스레드 보호가 필요하면 CallerRunsPolicy 대신 커스텀 핸들러 + 메트릭/알람 사용

직관에 반하는 동작 방식 주의

작업 제출 흐름:
1. corePoolSize 미만 -> 새 스레드 생성
2. corePoolSize 이상, queue 여유 -> 큐에 추가  <- maxPoolSize 아직 아님!
3. 큐가 꽉 찼고, maxPoolSize 미만 -> 새 스레드 생성
4. 큐도 꽉 찼고, maxPoolSize 도달 -> RejectedExecutionHandler

queueCapacityInteger.MAX_VALUE(기본값)로 두면 큐가 절대 차지 않아 스레드 수가 corePoolSize 이상으로 절대 증가하지 않음.

Micrometer 모니터링

executor.active           -> 현재 실행 중 스레드 수
executor.queued           -> 큐 대기 태스크 수
executor.queue.remaining  -> 큐 잔여 용량 (0이면 위험!)
executor.pool.size        -> 현재 풀 크기
executor.completed        -> 완료 태스크 수

알람 기준: queued > queueCapacity x 80% 경고, queue.remaining < 10 긴급

8.3 AsyncUncaughtExceptionHandler

올바른 등록 방식 (AsyncConfigurer 구현)

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }

    @Override
    public Executor getAsyncExecutor() { /* ... */ }
}
// 가장 흔한 실수: @Bean으로만 등록 -> 실제로 호출되지 않음!
// Spring은 AsyncConfigurer의 메서드를 우선하며,
// @Bean으로 등록한 핸들러는 자동 연결되지 않음

프로덕션 구현 패턴

@Slf4j
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    private final MeterRegistry meterRegistry;

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        String methodName = method.getDeclaringClass().getSimpleName()
                          + "." + method.getName();

        // 1. 구조화된 로깅
        log.error("Async method failed: method={}, params={}", 
                  methodName, Arrays.toString(params), ex);

        // 2. Micrometer 메트릭
        meterRegistry.counter("async.exception",
            "method", methodName,
            "exception", ex.getClass().getSimpleName()
        ).increment();
    }
}

@Async + @Transactional 함정

// 함정: 같은 메서드에 선언 -> 트랜잭션이 호출자와 완전히 별개
@Async
@Transactional  // 새 스레드에서 새로 시작됨
public void processAsync(Long id) { ... }

// 베스트 프랙티스: 트랜잭션 경계를 명확히 분리
@Async
public void processAsync(Long id) {
    transactionalService.doInTransaction(id);
}

8.4 S3 Multipart Upload

Part Size 선택 기준

파일 크기 권장 Part Size 이유
< 100MB 단일 PutObject Multipart 불필요
100MB ~ 1GB 16MB 균형잡힌 병렬성
1GB ~ 10GB 32~64MB API 호출 수 최소화
10GB+ 64~100MB 10,000 파트 한계 고려

파트 수 한계: 최소 파트 크기 = 파일 크기 / 10,000

InputStream (크기 미상) 업로드 - SDK v2

// SDK 2.20+ 투명한 multipart 처리
S3AsyncClient multipartClient = S3AsyncClient.builder()
    .multipartEnabled(true)
    .multipartConfiguration(c -> c
        .thresholdInBytes(8 * 1024 * 1024L)   // 8MB 이상이면 multipart
        .minimumPartSizeInBytes(5 * 1024 * 1024L)
    )
    .build();

실패 처리 + AbortMultipartUpload (필수)

String uploadId = null;
try {
    uploadId = s3Client.createMultipartUpload(req -> req.bucket(bucket).key(key))
                       .uploadId();
    // ... 파트 업로드 ...
    s3Client.completeMultipartUpload(/* ... */);
} catch (Exception e) {
    if (uploadId != null) {
        s3Client.abortMultipartUpload(req -> req
            .bucket(bucket).key(key).uploadId(uploadId));
    }
    throw e;
}

Lifecycle Rule 자동 정리 (필수 설정)

불완전 파트에 대한 스토리지 비용이 계속 청구됨:

{
  "Rules": [{
    "ID": "abort-incomplete-multipart",
    "Status": "Enabled",
    "Filter": { "Prefix": "" },
    "AbortIncompleteMultipartUpload": {
      "DaysAfterInitiation": 3
    }
  }]
}

Pre-signed URL Multipart 흐름

1. 클라이언트 -> 서버: 업로드 초기화 요청
2. 서버 -> S3: CreateMultipartUpload -> uploadId 획득
3. 서버 -> 클라이언트: 파트별 Pre-signed URL 반환
4. 클라이언트 -> S3: 각 파트를 Pre-signed URL로 직접 업로드
5. 클라이언트 -> 서버: ETag 리스트 전송
6. 서버 -> S3: CompleteMultipartUpload

핵심: 대역폭이 서버를 통과하지 않음 -> 서버 비용 절감 및 병목 제거

8.5 @Async Proxy Bypass 해결

해결책 1: 클래스 분리 (가장 권장)

// 베스트 프랙티스: 별도 Worker 클래스로 분리
@Service
@RequiredArgsConstructor
public class OrderService {
    private final OrderAsyncWorker asyncWorker;

    public void processOrder(Order order) {
        asyncWorker.sendConfirmationEmail(order); // 프록시 경유 -> 비동기 실행
    }
}

@Component
public class OrderAsyncWorker {
    @Async("orderExecutor")
    public void sendConfirmationEmail(Order order) {
        // 진짜 비동기 실행
    }
}

해결책 2: @Lazy Self-Injection (현실적 대안)

@Service
public class NotificationService {
    @Autowired @Lazy
    private NotificationService self; // 프록시 주입

    public void notifyAll(List<User> users) {
        users.forEach(user -> self.notifyUser(user)); // 프록시 경유
    }

    @Async
    public void notifyUser(User user) { /* ... */ }
}

주의: @Lazy 없이 self-injection -> BeanCurrentlyInCreationException. Spring Boot 2.6+ 기본 순환 참조 금지.

Thread Pool 격리 (용도별 분리)

@Bean("fileProcessingExecutor")
public Executor fileProcessingExecutor() {
    ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
    e.setCorePoolSize(4);
    e.setMaxPoolSize(8);
    e.setQueueCapacity(50);
    e.setThreadNamePrefix("file-proc-");
    e.initialize();
    return e;
}

@Bean("emailExecutor")
public Executor emailExecutor() {
    ThreadPoolTaskExecutor e = new ThreadPoolTaskExecutor();
    e.setCorePoolSize(2);
    e.setMaxPoolSize(4);
    e.setQueueCapacity(100);
    e.setThreadNamePrefix("email-");
    e.initialize();
    return e;
}

// 사용 시 풀 이름 명시
@Async("fileProcessingExecutor")
public void processFile(String path) { /* ... */ }

SecurityContext 전파

@Bean("secureAsyncExecutor")
public Executor secureAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(4);
    executor.initialize();
    return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}

9. 빅테크 실전 사례

9.1 ZIP/Archive Streaming

Dropbox - 폴더 다운로드:

  • 파일을 4MB 블록으로 분할, SHA-256 해시로 식별
  • Magic Pocket(자체 exabyte급 blob 스토리지)에 저장
  • ZIP 생성 시 각 블록을 순차적으로 읽어 스트리밍 방식으로 압축
  • 스트리밍 프리페치로 100MB 파일 기준 28% 동기화 시간 단축 (89초 -> 64초)
  • 출처: Streaming File Synchronization - Dropbox Tech Blog

Netflix - Content Drive (CDrive):

  • 140억 개 이상의 파일/폴더 메타데이터 관리 (2024.10 기준)
  • 평균 Netflix 타이틀 하나 = ~200TB 원본 카메라 파일
  • Conform Pull 서비스: EDL 기반 미디어 조합 후 스트리밍 패키징
  • 출처: Content Drive - Netflix TechBlog

GitHub - Repository Archive:

  • git archive로 tarball/zipball on-demand 생성 후 캐시
  • 동일 커밋 ID -> 항상 동일 아카이브 보장

9.2 Thread Pool & Backpressure

Netflix - Hystrix (현재 Maintenance Mode -> Resilience4j 전환):

  • Bulkhead 패턴: 의존성별 독립 Thread Pool
  • 하루 Thread-isolated 명령 100억 건+, Semaphore-isolated 2000억 건+
  • 100+ HystrixCommand 타입, 40+ Thread Pool 운영
  • 풀 포화 시 즉시 Reject (큐 대기 없음) -> Fast Fail + Fallback
  • 출처: Introducing Hystrix - Netflix TechBlog

Uber - Cinnamon (2024):

  • QALM을 대체한 차세대 RPC 미들웨어
  • TCP-Vegas 알고리즘 변형(1994) + PID Controller(17세기 기술) 조합
  • 300% 오버로드에서 P50 레이턴시 50% 증가에 그침, 요청당 추가 지연 ~1us
  • Jaeger 기반 우선순위 전파 (tier 0~5, cohort 128 버킷)
  • 출처: Cinnamon - Uber Engineering Blog

Netflix - Virtual Threads 경험 (2024.07):

9.3 Async Processing

Uber - Kafka Dead Letter Queue:

  • 실패 메시지를 인라인 재시도 대신 별도 Retry Topic으로 이동 후 오프셋 커밋
  • Original Topic -> Retry Topic 1 (지연) -> Retry Topic 2 (더 긴 지연) -> Dead Letter Topic (수동 분석)
  • 핵심: 실패 메시지가 정상 처리를 블로킹하지 않음
  • 출처: Building Reliable Reprocessing and DLQ with Kafka - Uber Blog

LinkedIn - 하루 4조(4 Trillion) 건 이벤트 실시간 스트리밍 처리 (Kafka 중심)

9.4 S3 Multipart Upload

AWS 공식 성능 벤치마크:

방식 소요 시간 개선율
Single PUT ~72초 기준
Multipart (5MB 파트, 6병렬) ~45초 -38%
Multipart + Transfer Acceleration ~28초 -61%

Dropbox - Magic Pocket:

9.5 Spring AOP in Production

Alibaba - Sentinel:

  • Spring AOP 프록시 한계를 인식, Spring Cloud Circuit Breaker 표준 인터페이스로 통합
  • 더블십일 페스티벌에서 10년+ 핵심 트래픽 제어 지원
  • 런타임 Rule Dashboard로 코드 변경 없이 동적 설정

프로덕션 프록시 성능:

  • AspectJ CTW는 Spring AOP 대비 8~35배 빠름 (런타임 오버헤드 없음)
  • 그러나 빌드 설정 복잡도로 대부분 Spring AOP + 클래스 분리 선택

10. References

ZIP / DEFLATE

Java Concurrency

Spring @Async

Spring AOP

AWS S3

빅테크 엔지니어링 블로그