외부 API를 사용하는 프로젝트를 운영하다 보면 HTTP 통신, 재시도 처리, 예외 매핑 코드가 서비스 전반에 흩어지기 쉽다.
초기에는 빠르게 개발할 수 있지만, 시간이 지날수록 유지보수 비용이 급격히 증가한다.
이 글에서는 실무에서 사용한 SDK 모듈 구조를 기준으로
Builder 패턴을 적용한 이유와, 지수 백오프 + 지터 알고리즘 기반 재시도 로직을 어떻게 설계했는지 정리한다.
SDK 모듈로 분리한 이유
외부 API 연동 코드를 SDK 형태로 분리하면 다음과 같은 장점이 있다.
첫째, 서비스 코드에서는 비즈니스 로직에만 집중할 수 있다.
둘째, HTTP 설정, 인증, 예외 처리 방식을 한 곳에서 통제할 수 있다.
셋째, 클라이언트 생성 방식이 통일되어 사용성이 좋아진다.
AWS SDK나 Google Cloud SDK 역시 대부분 SDK 모듈 형태로 제공되며, Builder 패턴을 기본 구조로 사용한다.
이는 장기 운영 관점에서 검증된 구조라고 볼 수 있다.
기본 HttpClient 설정 모듈화
SDK 내부에서 공통으로 사용할 HttpClient는 별도 설정 클래스로 분리했다.
기본 타임아웃과 재시도 옵션을 통일해 예측 가능한 통신 환경을 만든다.
package sdk.example.config;
import okhttp3.OkHttpClient;
import java.util.concurrent.TimeUnit;
public class HttpClientConfig {
public static OkHttpClient defaultClient() {
return new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.retryOnConnectionFailure(false)
.build();
}
}
SDK 사용자는 필요에 따라 HttpClient를 직접 주입할 수 있고, 지정하지 않으면 기본 설정을 사용한다.
공통 HTTP 요청 처리 구조
GET, POST, PUT, DELETE 요청은 하나의 유틸 클래스로 통합했다.
핵심 포인트는 재시도 로직을 HTTP 호출 외부에서 제어한다는 점이다.
package sdk.example.util;
import okhttp3.*;
import sdk.example.exception.CustomApiException;
import sdk.example.exception.ErrorCode;
public class HttpRequestUtil {
public static String get(OkHttpClient client, Headers headers, String url) {
Request request = new Request.Builder()
.url(url)
.headers(headers)
.get()
.build();
return RetryUtil.executeWithRetry(() -> {
try (Response response = client.newCall(request).execute()) {
if (response.body() == null) {
throw new CustomApiException(ErrorCode.EMPTY_RESPONSE);
}
String body = response.body().string();
if (!response.isSuccessful()) {
throw new CustomApiException(
ErrorCode.fromStatus(response.code()),
body
);
}
return body;
}
}, 3, 500);
}
}
HTTP 상태 코드에 따른 에러는 하나의 Exception 계층으로 통합해
SDK 사용자 입장에서 에러 처리가 단순해지도록 구성했다.
지수 백오프와 지터를 적용한 재시도 로직
네트워크 장애나 일시적인 서버 오류 상황에서 고정 딜레이 재시도는 오히려 부하를 증가시킬 수 있다.
이를 방지하기 위해 Exponential Backoff + Jitter 알고리즘을 적용했다.
package sdk.example.util;
import sdk.example.exception.CustomApiException;
import sdk.example.exception.ErrorCode;
import java.io.IOException;
import java.net.SocketException;
import java.util.Random;
public class RetryUtil {
public static <T> T executeWithRetry(ApiCall<T> call, int maxRetries, long baseDelay) {
int retryCount = 0;
Random random = new Random();
while (true) {
try {
return call.execute();
} catch (SocketException | IOException e) {
retryCount++;
if (retryCount > maxRetries) {
throw new CustomApiException(
ErrorCode.NETWORK_ERROR,
e.getMessage()
);
}
sleepWithBackoffAndJitter(baseDelay, retryCount, random);
}
}
}
private static void sleepWithBackoffAndJitter(
long baseDelay, int retryCount, Random random) {
try {
long delay = baseDelay * (1L << (retryCount - 1));
long jitter = (long) (random.nextDouble() * delay);
Thread.sleep(delay + jitter);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
}
}
}
이 방식은 재시도 요청이 동시에 몰리는 현상을 완화하고, 전체 시스템 안정성을 높이는 데 효과적이다.
Builder 패턴 기반 Client 구조
SDK의 핵심은 사용성이다.
Client 생성 시점부터 설정 흐름이 명확하도록 Builder 패턴을 적용했다.
package sdk.example.client;
import okhttp3.OkHttpClient;
import static sdk.example.config.HttpClientConfig.defaultClient;
public class ExampleApiClient {
public static RootBuilder builder() {
return new RootBuilder();
}
public static class RootBuilder {
public FeatureAClientBuilder featureA() {
return new FeatureAClientBuilder();
}
public FeatureBClientBuilder featureB() {
return new FeatureBClientBuilder();
}
}
public static class FeatureAClientBuilder {
private String baseUrl;
private String apiKey;
private OkHttpClient client;
public FeatureAClientBuilder withBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
return this;
}
public FeatureAClientBuilder withApiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
public FeatureAClientBuilder withHttpClient(OkHttpClient client) {
this.client = client;
return this;
}
public FeatureAClient build() {
if (this.client == null) {
this.client = defaultClient();
}
return new FeatureAClient(baseUrl, apiKey, client);
}
}
}
Builder 패턴을 적용하면 설정 단계가 명확해지고, IDE 자동 완성과도 잘 맞는다.
SDK에서 Builder 패턴이 적합한 이유
SDK 사용자는 다음과 같은 형태로 클라이언트를 생성할 수 있다.
ExampleApiClient.builder()
.featureA()
.withBaseUrl("https://api.example.com")
.withApiKey("api-key")
.build();
AWS SDK와 Google Cloud SDK 역시 동일한 방식으로 구성되어 있다.
복잡한 설정을 단계적으로 구성할 수 있고, 잘못된 설정을 build 단계에서 차단할 수 있다는 점이 장점이다.
공식 문서에서 권장하는 재시도 전략
AWS와 Google Cloud는 공식 문서에서
지수 백오프와 지터 알고리즘 적용을 명확하게 권장하고 있다.
AWS Architecture Blog
https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
Google Cloud Retry Strategy
https://cloud.google.com/storage/docs/retry-strategy
재시도 전략 | Cloud Storage | Google Cloud Documentation
의견 보내기 재시도 전략 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 페이지에서는 Cloud Storage 도구가 실패한 요청을 재시도하는 방법과 재시도 동작
docs.cloud.google.com
정리
외부 API 연동이 많은 프로젝트에서는 SDK 모듈 구조가 유지보수 비용을 크게 줄여준다.
Builder 패턴과 재시도 로직을 함께 설계하면 사용성과 안정성을 동시에 확보할 수 있다.
장기 운영이 필요한 서비스라면 한 번쯤은 SDK 구조를 고민해볼 만하다.
'백엔드 개발' 카테고리의 다른 글
| Spring Boot + SAP JCo 연동 SDK 만들기 (Builder 패턴 적용) (0) | 2026.03.27 |
|---|---|
| Spring Boot Redis 실무 활용: 클러스터/싱글 + 동기/비동기 + TTL + LocalDateTime (0) | 2026.02.13 |
| AWS SQS SDK 기반 Consumer 실무 적용기 (0) | 2026.02.06 |
| JPA vs MyBatis, 실무에서 왜 이렇게 고민하게 될까? (0) | 2026.02.05 |
| 안전하게 API 서버를 종료하는 방법 (Graceful Shutdown) (0) | 2025.12.11 |