2025. 1. 21. 14:48ㆍAndroid/네트워킹
🌟 들어가기 전
최근 회사에서 관리하는 앱이 OOM 이슈가 자꾸 일어난다.
RxJava 배압 이슈인가 하고 확인해봤더니 그 원인은 아니었다.
Firebase의 Crashlytics를 뜯어본 결과 OutOfMemoryError는 okhttp 통신 시 발생하였고,
이 부분에서 메모리 누출이 의심되었다.
그렇다면 OkHttp 통신할 때 Memory를 최적화하기 위한 체크 포인트는 무엇이 있을까?
🔍 세부 내용
1. ConnectionPool 사용 추가
연결 관리를 최적화하기 위해 명시적으로 ConnectionPool을 추가하면, OkHttp에서 연결을 재사용하여 새로운 연결을 생성하거나 닫는 작업을 줄여준다.
// ConnectionPool 설정 추가
ConnectionPool connectionPool = new ConnectionPool(
10, // 유지할 유휴 연결 수
5, // 유휴 연결 유지 시간
TimeUnit.MINUTES // 시간 단위
);
client = new OkHttpClient().newBuilder()
.connectTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.cookieJar(new JavaNetCookieJar(cookieManager))
.connectionPool(connectionPool) // ConnectionPool 설정
.connectionSpecs(lists)
.addInterceptor(interceptor)
.build();
ConnectionPool이 효과적인 경우
- 빈번한 네트워크 요청: 동일한 서버로 다수의 요청을 보낼 때.
- 연결 누수 방지: 새로운 연결을 매번 생성하는 대신 재사용하여 메모리 누수를 방지.
- 유휴 연결 관리 실패: 유휴 연결이 해제되지 않고 메모리를 차지하는 경우.
ConnectionPool은 연결 재사용으로 성능을 최적화하지만,
응답 크기 관리, 메모리 사용 제한, 스트리밍 처리 등과 함께 사용해야 진정한 OOM 방지가 가능하다.
특히 대량의 데이터 또는 대규모 요청이 원인이라면
이를 분할 처리하거나 WorkManager와 같은 백그라운드 작업 관리와 병행해야 한다.
2. 동시 요청 제한
OkHttp의 Dispatcher를 사용해 동시 요청 수를 제한하면 메모리 사용량 급증을 방지할 수 있다.
Dispatcher dispatcher = new Dispatcher();
dispatcher.setMaxRequests(64); // 전체 요청 수 제한
dispatcher.setMaxRequestsPerHost(5); // 호스트당 요청 수 제한
client = new OkHttpClient().newBuilder()
.connectTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.cookieJar(new JavaNetCookieJar(cookieManager))
.dispatcher(dispatcher) // Dispatcher 설정
.connectionSpecs(lists)
.addInterceptor(interceptor)
.build();
Dispatcher로 제한할 때는 서버의 처리 능력에 맞춘 설정이 중요하다.
- 서버와 클라이언트 간 요청/응답 속도와 서버의 처리 한계를 고려해 적절한 동시 요청 수를 설정해야 한다.
- 예: 서버가 동시에 100개의 요청을 처리할 수 있다면, setMaxRequests를 80~90으로 설정.
Dispatcher는 네트워크 요청을 효율적으로 관리하고 앱의 성능과 안정성을 높이는 데 유용하다.
하지만 적절한 설정값을 찾지 못하면 오히려 성능 저하로 이어질 수 있으므로, 앱의 트래픽 패턴, 서버의 처리 능력, 네트워크 환경을 종합적으로 고려해 설정하는 것이 중요하다.
3. 응답 본문 닫기
모든 네트워크 요청에서 응답 본문을 닫아야 메모리 누수를 방지할 수 있다.
try (Response response = client.newCall(request).execute()) {
// 응답 처리
String body = response.body().string();
} catch (IOException e) {
e.printStackTrace();
}
4. 캐싱 추가
자주 요청하는 데이터가 있다면 OkHttp의 캐싱 기능을 활용하여 네트워크 요청을 줄이고 메모리 사용량을 최적화 해야한다.
// 10MB 캐시 설정
Cache cache = new Cache(new File(context.getCacheDir(), "http_cache"), 10 * 1024 * 1024);
client = new OkHttpClient().newBuilder()
.connectTimeout(timeout, TimeUnit.SECONDS)
.writeTimeout(timeout, TimeUnit.SECONDS)
.readTimeout(timeout, TimeUnit.SECONDS)
.cache(cache) // 캐시 추가
.cookieJar(new JavaNetCookieJar(cookieManager))
.connectionSpecs(lists)
.addInterceptor(interceptor)
.build();
5. RxJava 및 코루틴 관리
RxJava나 코루틴 사용 중 구독 관리를 철저히 해야 한다.
메모리 누수 가능성을 줄이기 위해 적절히 구독 해제 또는 취소 처리해야 한다.
- RxJava: CompositeDisposable로 관리
- 코루틴: Job이나 CoroutineScope에서 cancel() 호출
6. 응답 크기가 너무 크다면 스트리밍 방식으로 처리
- response.body().string(): 전체 응답 데이터를 메모리에 로드한 후 문자열로 변환. 큰 데이터일 경우 OOM 발생 가능.
- response.body().byteStream(): 데이터를 스트리밍 방식으로 처리. 데이터가 메모리에 모두 적재되지 않아 효율적.
ResponseBody body = response.body();
InputStream inputStream = body.byteStream();
Response response = client.newCall(request).execute();
try (InputStream inputStream = response.body().byteStream();
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream)) {
byte[] buffer = new byte[1024]; // 1KB 버퍼 크기
int bytesRead;
while ((bytesRead = bufferedInputStream.read(buffer)) != -1) {
// 데이터를 처리하거나 파일에 저장
processBuffer(buffer, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
대량 데이터(예: 이미지, 비디오, 대형 JSON)가 포함된 응답을 처리할 때, 응당 데이터가 MB 이상으로 클 경우에는
InputStream 사용을 권장한다.
🤔 배운 점 & 느낀 점
모바일 개발을 하면서 단순 API 호출만이 능사는 아니다.
그 안에서 최대한 최적화 하고 성능을 높이는 것이 더욱 중요하다는 것을 요즘 체감한다.
우선 OOM 이슈가 너무 빈번하게 발생하니 이것부터 줄여나가야겠다 ㅠㅠ
'Android > 네트워킹' 카테고리의 다른 글
[Android] 특정 국가 차단하기 (SIM 정보 이용) (0) | 2025.03.27 |
---|---|
Retrofit Singleton 패턴을 유지하면서 ApiService를 기능별로 분리하기 (1) | 2025.02.17 |
[Android/Java/Kotlin]ExecutorService (0) | 2023.05.29 |
[Android/Async]Process, Thread에 대한 기본개념 (0) | 2023.02.26 |