-
Retrofit2 @Streaming 제대로 알기(내부 원리부터 파일 다운로드까지)-고슴도치 군단카테고리 없음 2026. 4. 28. 09:31

@Streaming 어노테이션 개요
Retrofit2에서 HTTP 응답을 처리하는 기본 방식은 응답 전체를 메모리에 버퍼링하는 것임.
작은 JSON 응답이라면 문제없지만, 대용량 파일이나 동영상을 다운로드할 때는 메모리 부족이나 OOM(Out Of Memory) 오류가 발생할 수 있음.
@Streaming어노테이션은 이 버퍼링 동작을 비활성화하고, 서버 소켓에서 직접 데이터를 스트리밍 방식으로 읽을 수 있게 해주는 핵심 기능임.
기본 사용법
@Streaming은 인터페이스 메서드 선언 위에 붙이며, 반환 타입은 반드시ResponseBody여야 의미가 있음.Converter가 붙어있는 타입(예:
Call<User>)을 반환하면 Converter 내부에서 어차피 전체 본문을 읽기 때문에 스트리밍 효과가 없음.아래 예시처럼 선언하면 응답 바디를 소켓 스트림 그대로 전달받을 수 있음.
interface FileDownloadService { @Streaming @GET("files/{filename}") fun downloadFile(@Path("filename") String filename): Call<ResponseBody> // Kotlin suspend 버전 @Streaming @GET("files/{filename}") suspend fun downloadFileSuspend(@Path("filename") filename: String): Response<ResponseBody> }
@Streaming 없을 때 vs 있을 때 비교
두 모드의 차이는 단순히 편의성이 아니라 메모리 사용량과 커넥션 관리 방식 자체가 달라짐.
특히 100MB 이상의 파일 다운로드에서
@Streaming없이 처리하면 앱이 강제 종료되는 경우가 빈번함.아래 표는 두 모드를 명확하게 비교한 것임.
항목 @Streaming 없음 (기본) @Streaming 있음 응답 처리 방식 전체를 메모리에 버퍼링 소켓 스트림 직접 노출 메모리 사용 응답 크기 전체 청크 버퍼(~8KB) 수준 적합한 상황 소형 JSON, 텍스트 대용량 파일, 동영상, 이미지 소켓 반환 시점 수신 완료 후 자동 close() 호출 시 OOM 위험 대용량에서 높음 없음
내부 동작 원리
Retrofit2는 OkHttp 위에서 동작하며, 실제 스트리밍 처리는
OkHttpCall.parseResponse()내부에서 결정됨.@Streaming이 없으면Utils.buffer(rawBody)를 호출해 전체 바디를byte[]로 메모리에 올린 뒤 소켓을 즉시 반환함.@Streaming이 있으면 이 버퍼링 단계를 건너뛰고, OkHttp의BufferedSource(Okio 라이브러리)가 소켓과 연결된 상태 그대로ResponseBody를 호출자에게 전달함.[클라이언트 요청] │ ▼ OkHttp TCP 소켓 연결 → 서버 응답 수신 │ ├─ @Streaming 없음 ─────────────────────────────────┐ │ Utils.buffer(rawBody) │ │ → 전체 body를 byte[]로 메모리 로드 │ │ → BufferedSource 즉시 닫힘 (소켓 반환) │ │ → Converter가 byte[]를 변환 │ │ │ └─ @Streaming 있음 ─────────────────────────────────┘ 버퍼링 없음 → BufferedSource(소켓 연결 유지) 그대로 노출 → 호출자가 InputStream.read()로 청크 단위 읽기 → body.close() 호출 시 소켓 반환내부적으로
@Streaming어노테이션은ServiceMethod파싱 단계에서isStreaming플래그로 저장되고,OkHttpCall이 이 플래그를 확인해 버퍼링 여부를 결정하는 구조임.
실제 파일 다운로드 구현
스트리밍 응답을 받아 파일로 저장하는 것이 가장 일반적인 활용 패턴임.
핵심은
byteStream()으로InputStream을 얻고, 고정 크기 버퍼(통상 8KB)로 반복해서 읽으며 파일에 쓰는 것임.use블록을 활용하면body.close()가 자동으로 호출되어 소켓 누수를 방지할 수 있음.// CoroutineScope 내에서 실행 (IO Dispatcher) suspend fun downloadAndSave(filename: String, destFile: File) { val response = service.downloadFileSuspend(filename) if (!response.isSuccessful) { throw IOException("다운로드 실패: ${response.code()}") } response.body()?.use { body -> val inputStream = body.byteStream() val totalBytes = body.contentLength() // -1이면 알 수 없음 FileOutputStream(destFile).use { output -> val buffer = ByteArray(8192) var bytesRead: Int var downloadedBytes = 0L while (inputStream.read(buffer).also { bytesRead = it } != -1) { output.write(buffer, 0, bytesRead) downloadedBytes += bytesRead // 진행률 계산 (totalBytes가 -1이면 불가) if (totalBytes > 0) { val progress = (downloadedBytes * 100 / totalBytes).toInt() // LiveData나 Flow로 UI 업데이트 } } output.flush() } } ?: throw IOException("응답 body가 null임") }
OkHttp 커넥션 풀과 close() 관리
@Streaming사용 시 가장 중요한 주의사항은ResponseBody.close()호출임.닫지 않으면 OkHttp 커넥션 풀이 해당 커넥션을 회수하지 못해 누수가 발생하고, 일정 시간 후 새로운 요청이 커넥션을 할당받지 못하는 상황이 생길 수 있음.
아래 타임라인은 커넥션 생명주기를 보여줌.
요청 시작 → 커넥션 풀에서 소켓 할당 → 응답 헤더 수신 → body 스트림 오픈 (소켓 유지 중) → read() 반복 호출 → body.close() 호출 → 소켓 커넥션 풀로 반환 ← 반드시 여기까지 도달해야 함예외 상황에서도 반드시 close()가 호출되도록
try-finally또는 Kotlinuse블록을 사용하는 것이 원칙임.// try-finally 방식 (Java 스타일) val body = response.body() try { body?.byteStream()?.use { stream -> // 처리 } } finally { body?.close() }
주요 주의사항 정리
@Streaming사용 시 흔히 겪는 실수들을 정리한 것임.메인 스레드에서
byteStream().read()를 호출하면NetworkOnMainThreadException이 발생하므로 반드시 백그라운드에서 처리해야 함.아래 체크리스트를 참고해 안전하게 구현할 수 있음.
- 반환 타입:
Call<ResponseBody>또는suspend fun: Response<ResponseBody>사용 - Converter 주의: Gson, Moshi 등 Converter와 함께 쓰면 스트리밍 효과 없음
- 백그라운드 처리:
Dispatchers.IO또는 별도 스레드에서 실행 - close() 필수:
use블록 또는try-finally로 반드시 자원 해제 - 진행률 표시:
body.contentLength()가-1이면 Content-Length 헤더 없음을 의미함 - 대용량 전용: 소형 JSON이나 텍스트에는 굳이 사용할 필요 없음
마무리
@Streaming은 단순한 편의 기능이 아니라 OkHttp의 소켓 스트림 구조를 직접 활용하는 저수준 기능임.내부적으로는 Retrofit의
OkHttpCall이 버퍼링 단계를 건너뛰고, Okio의BufferedSource를 그대로 노출하는 방식으로 동작함.대용량 파일 다운로드나 미디어 스트리밍이 필요한 Android 앱 개발 시
@Streaming과 커넥션 관리 원리를 정확히 이해하고 있으면 OOM과 커넥션 누수 없이 안정적인 구현이 가능함. - 반환 타입: