배경 및 목표

행동과학연구소 연동에서 검사 관련 대용량 파일을 주고받아야 했습니다. 기존 방식처럼 서버가 파일을 직접 받아 메모리에 로드한 뒤 처리·업로드하면, 파일 크기가 커질수록 그만큼 힙 메모리를 점유해 OOM(Out Of Memory) 위험이 있었습니다. 동시 요청이 겹치면 위험은 더 커집니다.

안전하게 처리하려면 파일을 청크 단위로 흘려보내는 별도 스트리밍 배치 애플리케이션을 구현해야 했는데, 이는 개발 공수가 크고 운영 포인트도 하나 늘어나는 선택이었습니다.

목표

  • 서버 직접 처리로 인한 대용량 파일 OOM 위험을 원천 제거한다.
  • 별도 배치 구현 없이, 이후 다른 외부 연동에서도 재사용 가능한 파일 업로드 플로우를 확보한다.

해결 방법과 해결 후보군

세 가지 방식을 검토했습니다.

방식설명한계
서버 프록시 처리서버가 파일을 받아 메모리/디스크에 적재 후 S3 업로드파일이 서버 메모리를 경유 → OOM 위험, 서버 대역폭 소모
스트리밍 배치 앱별도 배치가 청크 단위로 스트리밍 처리안전하지만 신규 배치 구현·운영 공수 큼
Presigned URL (채택)서버는 URL만 발급, 외부가 S3에 직접 업로드서버가 파일 데이터를 거치지 않음 → OOM 원천 제거 + 구현 간단

파일 바이트가 서버를 경유하지 않는 Presigned URL 방식이 OOM을 원천 제거하면서 구현 공수도 가장 작아 채택했습니다.

1. 제한 시간부 업로드 권한(Presigned URL) 발급

서버는 S3에 대한 만료 시간이 있는 업로드 권한(URL)만 발급합니다. 실제 파일 바이트는 서버 메모리를 전혀 거치지 않습니다.

fun generatePresignedUrl(fileName: String): String {
    val request = GeneratePresignedUrlRequest(bucket, fileName)
        .withMethod(HttpMethod.PUT)
        .withExpiration(Date.from(Instant.now().plus(Duration.ofMinutes(30))))
    return s3Client.generatePresignedUrl(request).toString()
}

2. 외부가 S3로 직접 업로드하는 플로우

외부 서비스는 발급받은 URL로 S3에 직접 PUT합니다. 서버는 업로드 경로에서 완전히 빠져, 파일 크기와 무관하게 메모리 사용량이 일정합니다.

sequenceDiagram
    participant Ext as 외부 서비스
    participant API as 서버
    participant S3 as AWS S3
    Ext->>API: 업로드 URL 요청
    API->>S3: Presigned URL 생성
    API-->>Ext: URL 응답 (30분 만료)
    Ext->>S3: 파일 직접 PUT (서버 미경유)
    S3-->>Ext: 업로드 완료
    Ext->>API: 업로드 완료 통지 (key)

기존에는 파일이 서버 메모리를 거쳤지만, 개선 후에는 URL 발급 요청/응답만 서버를 지나갑니다.

3. 재사용 가능한 공통 패턴으로 정리

버킷·키 규칙·만료 시간을 표준화한 공통 유틸리티로 정리하여, 이후 다른 외부 연동에서 파일 처리가 필요할 때 동일한 패턴을 재사용할 수 있도록 했습니다.


결과

지표기존개선
서버 메모리파일 크기만큼 점유 (OOM 위험)파일 미경유 (위험 0)
구현 범위스트리밍 배치 앱 별도 필요URL 발급 로직만
재사용성연동마다 개별 구현공통 패턴화

파일 데이터를 서버에서 분리하는 구조 전환만으로 OOM 위험을 없애고, 배치 신규 구현 공수까지 절감했습니다.


모니터링

  • 파일 처리 구간 서버 힙 메모리 사용량이 파일 크기와 무관하게 일정한지 관측한다(OOM 위험 해소 검증).
  • Presigned URL 발급 건수·S3 직접 업로드 성공률, 만료(403) 발생률을 관측한다.