본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 11. · 13 Views
Riverpod 3.0 Retry 자동 재시도 완벽 가이드
Riverpod 3.0에 새로 추가된 Retry 기능을 활용하여 네트워크 오류나 일시적인 실패 상황에서 자동으로 재시도하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 실무 예제와 함께 설명합니다.
목차
- Retry 기능 소개 (3.0 신규)
- defaultRetry 설정
- AsyncValue.retrying 상태
- ProviderException 처리
- 커스텀 재시도 로직
- 지수 백오프 구현
1. Retry 기능 소개 (3.0 신규)
김개발 씨는 날씨 앱을 개발하던 중 골치 아픈 문제에 부딪혔습니다. 사용자가 새로고침 버튼을 누를 때마다 가끔씩 네트워크 오류가 발생했고, 사용자들은 매번 수동으로 다시 시도해야 했습니다.
"분명 일시적인 네트워크 문제인데, 자동으로 재시도할 방법은 없을까요?" 선배 박시니어 씨가 웃으며 말했습니다. "Riverpod 3.0에 딱 맞는 기능이 추가됐어요!"
Retry 기능은 Riverpod 3.0에 새로 추가된 기능으로, Provider가 실패했을 때 자동으로 재시도를 처리합니다. 마치 전화 통화가 끊겼을 때 자동으로 재연결을 시도하는 것처럼, 일시적인 오류 상황에서 개발자가 직접 재시도 로직을 작성하지 않아도 됩니다.
네트워크 요청, API 호출 등 실패 가능성이 있는 작업에 특히 유용합니다.
다음 코드를 살펴봅시다.
// weather_provider.dart
@Riverpod(retry: true)
Future<Weather> weather(WeatherRef ref) async {
// API 호출 - 실패 시 자동으로 재시도됩니다
final response = await http.get(
Uri.parse('https://api.weather.com/current'),
);
if (response.statusCode != 200) {
// 실패하면 자동으로 재시도
throw Exception('날씨 데이터를 가져올 수 없습니다');
}
return Weather.fromJson(jsonDecode(response.body));
}
김개발 씨는 입사 6개월 차 플러터 개발자입니다. 오늘도 열심히 날씨 앱을 개발하던 중, 사용자들의 불만이 쏟아지기 시작했습니다.
"가끔씩 날씨가 안 뜨는데요?" "오류가 뜬 후에 수동으로 새로고침해야 해요." 김개발 씨는 로그를 확인해봤습니다. 대부분 일시적인 네트워크 타임아웃이었습니다.
잠깐 후에 다시 시도하면 정상적으로 작동했습니다. "이걸 어떻게 해결하지?" 고민하던 중, 선배 박시니어 씨가 다가왔습니다.
"아, 그거라면 Riverpod 3.0에 새로 추가된 Retry 기능을 쓰면 되겠네요." 그렇다면 Retry 기능이란 정확히 무엇일까요? 쉽게 비유하자면, Retry는 마치 자동 응답 전화기와 같습니다.
통화 중이거나 전화를 받지 않으면 일정 시간 후 자동으로 다시 전화를 걸어주는 것처럼, Provider가 실패하면 자동으로 다시 시도해주는 기능입니다. 개발자는 "재시도해 주세요"라고 한 번만 설정하면, 나머지는 Riverpod가 알아서 처리합니다.
Retry 기능이 없던 시절에는 어땠을까요? 개발자들은 try-catch 블록을 직접 작성하고, 재시도 횟수를 세고, 지연 시간을 계산하는 복잡한 로직을 일일이 구현해야 했습니다.
코드가 길어지고, 프로젝트마다 비슷한 코드를 반복해서 작성했습니다. 더 큰 문제는 각 개발자마다 재시도 로직이 달라서, 일관성 없는 사용자 경험을 제공했다는 점입니다.
바로 이런 문제를 해결하기 위해 Riverpod 3.0의 Retry 기능이 등장했습니다. Retry를 사용하면 단 한 줄의 설정만으로 자동 재시도가 가능해집니다.
또한 일관된 재시도 정책을 프로젝트 전체에 적용할 수 있습니다. 무엇보다 사용자 경험 개선이라는 큰 이점이 있습니다.
사용자는 일시적인 오류 상황에서 수동으로 재시도 버튼을 누를 필요가 없어집니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 첫 번째 줄 @Riverpod(retry: true)를 보면 이 Provider가 재시도 기능을 활성화했다는 것을 알 수 있습니다. 이 한 줄이 핵심입니다.
다음으로 http.get() 호출에서 네트워크 요청이 일어나고, 실패하면 예외가 발생합니다. 마지막으로 예외가 발생하면 Riverpod가 자동으로 이 함수를 다시 호출해 재시도합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 쇼핑몰 앱을 개발한다고 가정해봅시다.
상품 목록을 불러오는 API가 간헐적으로 실패하는 상황에서, Retry 기능을 활용하면 사용자는 오류 화면을 보는 대신 자동으로 재시도되어 상품 목록을 볼 수 있습니다. 많은 글로벌 기업에서 이런 자동 재시도 패턴을 적극적으로 사용하고 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 모든 Provider에 무분별하게 retry를 적용하는 것입니다.
이렇게 하면 진짜 오류 상황에서도 계속 재시도하여 서버에 불필요한 부하를 줄 수 있습니다. 따라서 네트워크 요청이나 일시적인 오류가 예상되는 경우에만 사용해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다.
"와, 이렇게 간단하게 해결할 수 있었군요!" Retry 기능을 제대로 이해하면 더 안정적이고 사용자 친화적인 앱을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - retry: true는 기본 재시도 정책(3회 시도)을 사용합니다
- 일시적인 네트워크 오류가 예상되는 Provider에만 사용하세요
- 무한 재시도를 방지하기 위해 적절한 재시도 횟수를 설정하는 것이 중요합니다
2. defaultRetry 설정
김개발 씨가 Retry 기능을 적용하고 나니, 이번엔 새로운 고민이 생겼습니다. "프로젝트 전체에서 일관된 재시도 정책을 쓰고 싶은데, Provider마다 설정하기엔 너무 번거로운데요?" 박시니어 씨가 웃으며 답했습니다.
"그럴 땐 defaultRetry를 사용하면 돼요."
defaultRetry는 ProviderScope 레벨에서 전역 재시도 정책을 설정하는 방법입니다. 마치 회사의 업무 규칙을 한 번에 정해두면 모든 직원이 따르는 것처럼, 앱 전체의 재시도 동작을 일관되게 관리할 수 있습니다.
Provider별로 개별 설정도 가능하여 유연성도 확보할 수 있습니다.
다음 코드를 살펴봅시다.
// main.dart
void main() {
runApp(
ProviderScope(
overrides: [
// 전역 재시도 정책 설정
defaultRetry.overrideWith((ref) {
return (error, stackTrace) async {
// 최대 3번까지 재시도
for (var i = 0; i < 3; i++) {
await Future.delayed(Duration(seconds: i + 1));
try {
return await ref.retry();
} catch (e) {
if (i == 2) rethrow; // 마지막 시도에서 실패하면 예외 발생
}
}
};
}),
],
child: MyApp(),
),
);
}
김개발 씨는 날씨 앱에 Retry 기능을 적용한 후 뿌듯함을 느꼈습니다. 하지만 곧 새로운 문제를 발견했습니다.
프로젝트에 Provider가 20개가 넘는데, 각각 비슷한 재시도 로직을 작성해야 했습니다. "이거 참 비효율적인데..." 김개발 씨가 중얼거리자, 옆에서 코드 리뷰를 하던 박시니어 씨가 물었습니다.
"뭐가 비효율적인데요?" "각 Provider마다 똑같은 재시도 로직을 반복해서 작성해야 하잖아요. 한 번에 설정할 방법은 없을까요?" 박시니어 씨가 고개를 끄덕였습니다.
"아, 그거라면 defaultRetry를 쓰면 됩니다." defaultRetry란 정확히 무엇일까요? 쉽게 비유하자면, defaultRetry는 마치 회사의 기본 업무 규칙과 같습니다.
회사에서 "출근 시간은 오전 9시"라고 정해두면 모든 직원이 그 규칙을 따르는 것처럼, defaultRetry로 재시도 정책을 정해두면 모든 Provider가 그 정책을 따릅니다. 물론 특정 부서는 다른 규칙을 쓸 수 있는 것처럼, 개별 Provider는 자체 재시도 로직을 사용할 수도 있습니다.
defaultRetry가 없던 시절에는 어땠을까요? 개발자들은 각 Provider마다 동일한 재시도 로직을 복사해서 붙여넣었습니다.
그러다 재시도 정책을 변경해야 할 때는 모든 Provider를 찾아다니며 수정해야 했습니다. 실수로 하나라도 놓치면 일관성 없는 동작이 발생했고, 디버깅도 어려워졌습니다.
바로 이런 문제를 해결하기 위해 defaultRetry가 등장했습니다. defaultRetry를 사용하면 한 곳에서 재시도 정책을 관리할 수 있습니다.
또한 정책 변경이 용이해집니다. 무엇보다 코드 중복을 제거하여 유지보수성이 크게 향상됩니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ProviderScope의 overrides 파라미터에서 전역 설정을 적용합니다.
이것이 핵심입니다. 다음으로 defaultRetry.overrideWith()를 통해 커스텀 재시도 로직을 정의합니다.
for 루프를 사용해 최대 3번까지 재시도하며, 각 시도 사이에 점진적으로 늘어나는 지연 시간을 둡니다. 마지막으로 ref.retry()를 호출하여 실제 재시도를 수행합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 대규모 전자상거래 플랫폼을 개발한다고 가정해봅시다.
수백 개의 Provider가 있는 상황에서, defaultRetry를 사용하면 결제 API, 상품 API, 사용자 API 등 모든 네트워크 요청에 일관된 재시도 정책을 적용할 수 있습니다. 특히 Black Friday 같은 트래픽 폭주 상황에서 이런 일관된 재시도 정책은 시스템 안정성에 큰 도움이 됩니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 너무 공격적인 재시도 정책을 설정하는 것입니다.
예를 들어 지연 시간 없이 10번씩 재시도하면 서버에 과부하를 줄 수 있습니다. 따라서 적절한 재시도 횟수와 지연 시간을 설정해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 도움으로 defaultRetry를 적용한 김개발 씨는 감탄했습니다.
"이제 한 곳만 수정하면 모든 Provider에 적용되네요!" defaultRetry를 제대로 활용하면 더 체계적이고 유지보수하기 쉬운 코드를 작성할 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - defaultRetry는 앱의 진입점(main.dart)에서 설정하는 것이 일반적입니다
- 개별 Provider는 자체 retry 로직으로 기본 정책을 오버라이드할 수 있습니다
- 서버 부하를 고려하여 적절한 재시도 간격을 설정하세요
3. AsyncValue.retrying 상태
김개발 씨가 앱을 테스트하던 중 궁금증이 생겼습니다. "재시도 중일 때 사용자에게 뭔가 보여주고 싶은데, 로딩 중인지 재시도 중인지 어떻게 구분하죠?" 박시니어 씨가 화면을 가리키며 말했습니다.
"AsyncValue.retrying 상태를 활용하면 됩니다."
AsyncValue.retrying은 Provider가 재시도 중일 때의 상태를 나타냅니다. 마치 교통 신호등이 빨강, 노랑, 파랑으로 구분되는 것처럼, AsyncValue는 loading, data, error, retrying 등 다양한 상태를 제공합니다.
이를 통해 재시도 중일 때 특별한 UI를 보여줄 수 있습니다.
다음 코드를 살펴봅시다.
// weather_screen.dart
class WeatherScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final weatherAsync = ref.watch(weatherProvider);
return weatherAsync.when(
// 첫 로딩
loading: () => CircularProgressIndicator(),
// 재시도 중 - 이전 데이터와 함께 표시
data: (weather) {
if (weatherAsync.isRetrying) {
return Column(
children: [
WeatherWidget(weather), // 이전 데이터 표시
Text('업데이트 중...'), // 재시도 중 표시
],
);
}
return WeatherWidget(weather);
},
// 오류 발생
error: (error, stack) => ErrorWidget(error),
);
}
}
김개발 씨는 앱에 Retry 기능을 적용하고 테스트를 시작했습니다. 네트워크를 끊었다가 다시 연결하면서 동작을 확인하던 중, 이상한 점을 발견했습니다.
"음, 재시도할 때도 똑같이 로딩 스피너만 나오네? 사용자는 이게 처음 로딩인지 재시도 중인지 모를 텐데..." 옆에서 코드를 보던 박시니어 씨가 물었습니다.
"재시도 중일 때 다른 UI를 보여주고 싶은 거예요?" 김개발 씨가 고개를 끄덕였습니다. "네, 예를 들어 이전 날씨 정보는 보여주고 '업데이트 중'이라는 메시지를 추가하고 싶어요." "그럼 AsyncValue.retrying 상태를 확인하면 됩니다." AsyncValue.retrying이란 정확히 무엇일까요?
쉽게 비유하자면, AsyncValue는 마치 택배 배송 상태와 같습니다. "상품 준비 중", "배송 중", "배송 완료", "재배송 중" 등 다양한 상태가 있는 것처럼, AsyncValue도 loading, data, error, retrying 같은 상태를 제공합니다.
특히 retrying 상태는 "재배송 중"과 비슷합니다. 이전 배송이 실패했지만 다시 시도하고 있다는 것을 알려주는 것이죠.
retrying 상태가 없던 시절에는 어땠을까요? 개발자들은 재시도 중일 때와 처음 로딩할 때를 구분할 방법이 없었습니다.
그래서 재시도 중에도 전체 화면이 로딩 스피너로 바뀌어버렸습니다. 사용자는 이전에 보던 데이터가 사라지고 다시 기다려야 했습니다.
이는 좋지 않은 사용자 경험이었습니다. 바로 이런 문제를 해결하기 위해 retrying 상태가 추가되었습니다.
retrying 상태를 사용하면 이전 데이터를 유지하면서 업데이트 중임을 알릴 수 있습니다. 또한 더 세밀한 UI 제어가 가능해집니다.
무엇보다 사용자 경험 개선이라는 큰 이점이 있습니다. 사용자는 재시도 중에도 이전 정보를 계속 볼 수 있습니다.
위의 코드를 한 줄씩 살펴보겠습니다. 먼저 ref.watch(weatherProvider)로 날씨 데이터를 구독합니다.
이것이 AsyncValue 객체를 반환합니다. 다음으로 when() 메서드로 상태별 UI를 정의합니다.
핵심은 data 케이스 내부에서 weatherAsync.isRetrying을 확인하는 부분입니다. 재시도 중이라면 이전 날씨 데이터와 함께 "업데이트 중..." 메시지를 표시합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 소셜 미디어 앱을 개발한다고 가정해봅시다.
피드를 새로고침할 때 네트워크 오류가 발생하면, retrying 상태를 활용하여 이전 피드는 계속 보여주고 상단에 "새 게시물 불러오는 중..." 배너를 띄울 수 있습니다. Instagram, Twitter 같은 앱들이 이런 패턴을 사용합니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 retrying 상태를 확인하지 않고 loading만 체크하는 것입니다.
이렇게 하면 재시도 중에도 전체 화면이 로딩 화면으로 바뀌어 버립니다. 따라서 data 상태 내부에서 isRetrying을 확인해야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. retrying 상태를 적용한 김개발 씨는 테스트를 다시 해봤습니다.
"오, 이제 재시도 중에도 이전 날씨가 보이네요! 훨씬 자연스러워요." AsyncValue.retrying을 제대로 활용하면 더 부드럽고 사용자 친화적인 앱을 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - isRetrying은 data 상태와 함께 사용하는 것이 일반적입니다
- 재시도 중일 때는 이전 데이터를 유지하면서 작은 인디케이터를 추가하는 것이 좋습니다
- 너무 많은 재시도는 사용자를 혼란스럽게 할 수 있으니 적절한 횟수를 설정하세요
4. ProviderException 처리
김개발 씨가 재시도 기능을 테스트하던 중 문제를 발견했습니다. "어?
서버에서 401 인증 오류가 나는데도 계속 재시도하네요. 이건 재시도해도 소용없는데..." 박시니어 씨가 고개를 끄덕였습니다.
"그럴 땐 ProviderException으로 재시도를 멈춰야 해요."
ProviderException은 재시도하지 말아야 할 오류를 명시적으로 표시하는 특별한 예외입니다. 마치 의사가 "더 이상 치료가 불가능합니다"라고 선언하는 것처럼, ProviderException을 던지면 Riverpod는 재시도를 중단합니다.
인증 오류, 권한 오류 등 재시도해도 해결될 수 없는 상황에 사용합니다.
다음 코드를 살펴봅시다.
// user_provider.dart
@Riverpod(retry: true)
Future<User> currentUser(CurrentUserRef ref) async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/user'),
headers: {'Authorization': 'Bearer $token'},
);
// 401: 인증 오류 - 재시도 불가
if (response.statusCode == 401) {
throw ProviderException('로그인이 필요합니다');
}
// 403: 권한 오류 - 재시도 불가
if (response.statusCode == 403) {
throw ProviderException('접근 권한이 없습니다');
}
// 500: 서버 오류 - 재시도 가능 (일반 Exception)
if (response.statusCode == 500) {
throw Exception('서버 오류가 발생했습니다');
}
return User.fromJson(jsonDecode(response.body));
} catch (e) {
// ProviderException은 그대로 전달
if (e is ProviderException) rethrow;
// 기타 오류는 재시도 가능
rethrow;
}
}
김개발 씨는 사용자 정보를 불러오는 기능을 개발하고 있었습니다. Retry 기능을 적용했더니 대부분의 네트워크 오류는 잘 처리되었습니다.
하지만 한 가지 문제가 있었습니다. 테스트 중에 일부러 잘못된 토큰을 사용해봤더니, 401 인증 오류가 발생했습니다.
그런데 앱은 계속해서 재시도를 했습니다. 1초 후, 2초 후, 3초 후...
결국 재시도 횟수를 모두 소진한 후에야 오류가 표시되었습니다. "이건 재시도해봐야 소용없는데..." 김개발 씨가 중얼거렸습니다.
박시니어 씨가 화면을 보더니 말했습니다. "아, 인증 오류는 재시도해도 안 되죠.
ProviderException을 사용해야 해요." ProviderException이란 정확히 무엇일까요? 쉽게 비유하자면, ProviderException은 마치 의사의 최종 진단과 같습니다.
감기 같은 일시적인 병은 약을 먹고 쉬면 낫지만(일반 Exception - 재시도 가능), 만성 질환은 치료 방법을 바꿔야 합니다(ProviderException - 재시도 불가). ProviderException은 "이 문제는 다시 시도해도 해결되지 않습니다"라고 Riverpod에게 알려주는 역할을 합니다.
ProviderException이 없던 시절에는 어땠을까요? 모든 예외가 똑같이 취급되었습니다.
네트워크 타임아웃이든 인증 오류든 무조건 재시도했습니다. 그 결과 불필요한 네트워크 요청이 반복되었고, 사용자는 오류 메시지를 보기까지 오래 기다려야 했습니다.
서버 입장에서도 실패할 게 뻔한 요청을 계속 받아서 리소스 낭비가 발생했습니다. 바로 이런 문제를 해결하기 위해 ProviderException이 도입되었습니다.
ProviderException을 사용하면 재시도 가능한 오류와 불가능한 오류를 구분할 수 있습니다. 또한 불필요한 재시도를 방지하여 네트워크 리소스를 절약합니다.
무엇보다 사용자에게 빠르게 오류를 알려줄 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 HTTP 요청을 보내고 응답 상태 코드를 확인합니다. 401(인증 오류)이나 403(권한 오류)처럼 재시도해도 해결되지 않는 경우에는 ProviderException을 던집니다.
반대로 500(서버 오류)처럼 재시도하면 해결될 수 있는 경우에는 일반 Exception을 던집니다. catch 블록에서는 ProviderException인 경우 그대로 다시 던져서 재시도가 일어나지 않도록 합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 뱅킹 앱을 개발한다고 가정해봅시다.
계좌 조회 API를 호출할 때 네트워크 타임아웃은 재시도하지만, 계좌 비밀번호 오류는 ProviderException으로 처리하여 즉시 사용자에게 알려줍니다. 재시도해봐야 비밀번호가 갑자기 맞아질 리 없으니까요.
많은 금융 앱에서 이런 식으로 오류를 분류하여 처리합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 모든 오류를 ProviderException으로 처리하는 것입니다. 이렇게 하면 재시도 기능이 전혀 작동하지 않게 됩니다.
따라서 정말로 재시도가 불가능한 경우에만 ProviderException을 사용해야 합니다. 네트워크 타임아웃, 서버 오류 같은 일시적인 문제는 일반 Exception으로 던져야 합니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. ProviderException을 적용한 김개발 씨는 다시 테스트를 해봤습니다.
"오, 이번엔 인증 오류가 발생하자마자 바로 오류 화면이 뜨네요! 불필요한 재시도가 없어졌어요." ProviderException을 제대로 활용하면 더 효율적이고 사용자 친화적인 앱을 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 4xx 오류(클라이언트 오류)는 대부분 ProviderException으로 처리합니다
- 5xx 오류(서버 오류)는 일반 Exception으로 처리하여 재시도를 허용합니다
- ProviderException 메시지는 사용자에게 직접 표시될 수 있으므로 명확하게 작성하세요
5. 커스텀 재시도 로직
김개발 씨가 앱을 테스트하던 중 팀장님께 피드백을 받았습니다. "재시도 횟수를 3번이 아니라 5번으로 늘릴 수 있을까요?
그리고 각 시도마다 간격도 다르게 하고 싶은데요." 박시니어 씨가 웃으며 말했습니다. "커스텀 재시도 로직을 만들면 됩니다."
커스텀 재시도 로직은 Provider별로 고유한 재시도 정책을 정의하는 방법입니다. 마치 각 식당마다 고유한 레시피가 있는 것처럼, Provider마다 다른 재시도 횟수, 지연 시간, 조건을 설정할 수 있습니다.
기본 retry: true보다 더 세밀한 제어가 가능합니다.
다음 코드를 살펴봅시다.
// product_provider.dart
@Riverpod()
Future<Product> product(ProductRef ref, String id) async {
// 커스텀 재시도 로직: 최대 5번, 점진적 지연
return ref.retry(
maxAttempts: 5,
delayFactor: (attempt) {
// 첫 시도: 1초, 두 번째: 2초, 세 번째: 4초...
return Duration(seconds: math.pow(2, attempt).toInt());
},
shouldRetry: (error) {
// 특정 오류만 재시도
if (error is ProviderException) return false;
if (error is HttpException) {
final statusCode = error.statusCode;
// 408(타임아웃), 429(과도한 요청), 5xx(서버 오류)만 재시도
return statusCode == 408 ||
statusCode == 429 ||
statusCode >= 500;
}
return true;
},
operation: () async {
final response = await http.get(
Uri.parse('https://api.example.com/products/$id'),
);
return Product.fromJson(jsonDecode(response.body));
},
);
}
김개발 씨는 프로젝트가 점점 복잡해지는 것을 느꼈습니다. 어떤 API는 빠르게 응답하지만 자주 실패하고, 어떤 API는 느리지만 안정적이었습니다.
팀장님은 각 API의 특성에 맞춰 재시도 정책을 다르게 가져가고 싶어 했습니다. "기본 retry: true는 너무 단순한데, 더 세밀하게 제어할 방법은 없을까요?" 김개발 씨가 물었습니다.
박시니어 씨가 고개를 끄덕였습니다. "물론이죠.
커스텀 재시도 로직을 작성하면 됩니다." 커스텀 재시도 로직이란 정확히 무엇일까요? 쉽게 비유하자면, 커스텀 재시도 로직은 마치 맞춤형 운동 루틴과 같습니다.
모든 사람에게 똑같은 운동을 시키는 게 아니라, 개인의 체력과 목표에 맞춰 세트 수, 휴식 시간, 강도를 조절하는 것처럼, 각 Provider의 특성에 맞춰 재시도 횟수, 지연 시간, 조건을 커스터마이징할 수 있습니다. 커스텀 재시도 로직이 없던 시절에는 어떠했을까요?
개발자들은 retry: true라는 단순한 옵션만 사용할 수 있었습니다. 모든 Provider가 똑같은 재시도 정책을 따랐고, 세밀한 제어가 필요한 경우에는 직접 try-catch와 루프를 작성해야 했습니다.
결과적으로 코드가 복잡해지고, Provider마다 재시도 로직이 중복되었습니다. 바로 이런 문제를 해결하기 위해 커스텀 재시도 로직이 등장했습니다.
커스텀 재시도를 사용하면 Provider별로 최적의 재시도 정책을 적용할 수 있습니다. 또한 특정 오류만 재시도하는 조건부 로직을 작성할 수 있습니다.
무엇보다 지수 백오프 같은 고급 패턴을 쉽게 구현할 수 있다는 큰 이점이 있습니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 ref.retry()를 호출하는데, 여기에 여러 파라미터를 전달합니다. maxAttempts: 5는 최대 5번까지 재시도한다는 의미입니다.
delayFactor는 각 시도 사이의 지연 시간을 계산하는 함수로, 지수 백오프를 구현합니다. shouldRetry는 어떤 오류를 재시도할지 결정하는 함수로, ProviderException은 재시도하지 않고 특정 HTTP 상태 코드만 재시도하도록 설정했습니다.
마지막으로 operation에는 실제로 수행할 작업을 정의합니다. 실제 현업에서는 어떻게 활용할까요?
예를 들어 동영상 스트리밍 앱을 개발한다고 가정해봅시다. 영상 메타데이터를 가져오는 API는 빠르게 재시도(짧은 지연, 적은 횟수)하고, 고화질 영상을 다운로드하는 API는 천천히 재시도(긴 지연, 많은 횟수)하도록 설정할 수 있습니다.
YouTube, Netflix 같은 플랫폼에서 이런 차별화된 재시도 전략을 사용합니다. 하지만 주의할 점도 있습니다.
초보 개발자들이 흔히 하는 실수 중 하나는 너무 복잡한 재시도 로직을 작성하는 것입니다. 조건이 너무 많아지면 디버깅이 어려워지고, 예상치 못한 동작이 발생할 수 있습니다.
따라서 가능한 한 단순하게 유지하되, 정말 필요한 경우에만 복잡한 로직을 추가해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
커스텀 재시도 로직을 적용한 김개발 씨는 팀장님께 보고했습니다. "이제 중요한 API는 5번까지 재시도하고, 일반 API는 3번만 재시도하도록 설정했습니다!" 팀장님이 만족스러운 표정을 지었습니다.
커스텀 재시도 로직을 제대로 활용하면 더 안정적이고 최적화된 앱을 만들 수 있습니다. 여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 대부분의 경우 기본 retry: true로 충분하며, 특별한 요구사항이 있을 때만 커스텀 로직을 사용하세요
- shouldRetry 함수에서는 재시도 가능한 오류와 불가능한 오류를 명확히 구분하세요
- 재시도 횟수와 지연 시간은 서버 부하와 사용자 경험을 모두 고려하여 설정하세요
6. 지수 백오프 구현
김개발 씨가 서버 개발자와 회의를 하던 중 조언을 들었습니다. "재시도할 때 매번 같은 간격으로 하면 서버 부하가 심해져요.
지수 백오프를 사용해 주세요." 박시니어 씨가 고개를 끄덕이며 설명했습니다. "지수 백오프는 재시도 간격을 점점 늘려가는 패턴이에요."
지수 백오프는 재시도할 때마다 대기 시간을 지수적으로 늘리는 전략입니다. 마치 문을 두드릴 때 처음에는 바로 다시 두드리지만, 응답이 없으면 점점 더 오래 기다리는 것과 같습니다.
첫 번째는 1초, 두 번째는 2초, 세 번째는 4초, 네 번째는 8초 식으로 늘어납니다. 서버 부하를 줄이고 성공 확률을 높이는 업계 표준 패턴입니다.
다음 코드를 살펴봅시다.
// exponential_backoff_provider.dart
@Riverpod()
Future<Data> dataWithBackoff(DataWithBackoffRef ref) async {
const maxAttempts = 5;
const baseDelay = Duration(seconds: 1);
const maxDelay = Duration(seconds: 32);
for (var attempt = 0; attempt < maxAttempts; attempt++) {
try {
// 실제 API 호출
final response = await http.get(
Uri.parse('https://api.example.com/data'),
);
if (response.statusCode == 200) {
return Data.fromJson(jsonDecode(response.body));
}
throw HttpException('서버 오류: ${response.statusCode}');
} catch (e) {
// 마지막 시도에서 실패하면 예외 던지기
if (attempt == maxAttempts - 1) rethrow;
// 지수 백오프: 1초, 2초, 4초, 8초, 16초...
final delay = Duration(
seconds: math.min(
baseDelay.inSeconds * math.pow(2, attempt).toInt(),
maxDelay.inSeconds,
),
);
print('재시도 ${attempt + 1}/$maxAttempts - ${delay.inSeconds}초 후 재시도');
await Future.delayed(delay);
}
}
throw Exception('모든 재시도 실패');
}
김개발 씨는 앱을 운영하던 중 서버 개발자로부터 연락을 받았습니다. "김개발 씨, 앱에서 재시도 요청이 너무 많이 와서 서버가 힘들어해요.
특히 서버가 바쁠 때 계속 1초 간격으로 요청이 오면 부담이 커요." 김개발 씨는 당황했습니다. "그럼 어떻게 해야 하죠?" 서버 개발자가 설명했습니다.
"지수 백오프를 사용해 주세요. 처음에는 빨리 재시도하되, 계속 실패하면 점점 간격을 늘리는 거예요." 박시니어 씨가 옆에서 보충 설명을 해주었습니다.
"맞아요, 지수 백오프는 업계 표준 패턴이에요. AWS, Google Cloud 같은 대형 서비스들도 다 사용하는 방법이죠." 지수 백오프란 정확히 무엇일까요?
쉽게 비유하자면, 지수 백오프는 마치 친구에게 전화를 거는 것과 같습니다. 처음에는 전화를 받지 않으면 1분 후에 다시 걸어봅니다.
또 안 받으면 2분 후, 그 다음은 4분 후, 8분 후... 이런 식으로 점점 간격을 늘립니다.
친구가 회의 중이라면 계속 짧은 간격으로 전화하는 것보다, 시간을 두고 다시 연락하는 게 더 합리적이니까요. 지수 백오프가 없던 시절에는 어땠을까요?
개발자들은 재시도할 때 고정된 간격을 사용했습니다. 1초마다 계속 재시도하거나, 아예 재시도를 포기했습니다.
고정 간격으로 재시도하면 서버가 과부하 상태일 때 상황을 더 악화시켰습니다. 여러 클라이언트가 동시에 1초마다 재시도하면, 서버는 회복할 시간이 없었습니다.
바로 이런 문제를 해결하기 위해 지수 백오프가 표준으로 자리 잡았습니다. 지수 백오프를 사용하면 서버에 가해지는 부하를 크게 줄일 수 있습니다.
또한 서버가 회복할 시간을 확보할 수 있습니다. 무엇보다 성공 확률이 높아진다는 큰 이점이 있습니다.
서버가 일시적으로 과부하 상태일 때, 시간을 두고 재시도하면 서버가 회복된 후 요청이 성공할 가능성이 높아집니다. 위의 코드를 한 줄씩 살펴보겠습니다.
먼저 상수들을 정의합니다. maxAttempts는 최대 재시도 횟수, baseDelay는 기본 지연 시간, maxDelay는 최대 지연 시간입니다.
for 루프에서 각 시도를 수행하고, 실패하면 지수 백오프 계산을 합니다. 핵심은 baseDelay.inSeconds * math.pow(2, attempt)입니다.
이것이 1초, 2초, 4초, 8초로 늘어나는 지수 함수입니다. math.min()으로 최대 지연 시간을 제한하여 무한정 늘어나는 것을 방지합니다.
실제 현업에서는 어떻게 활용할까요? 예를 들어 채팅 앱을 개발한다고 가정해봅시다.
메시지를 전송할 때 네트워크 오류가 발생하면, 지수 백오프를 사용하여 재시도합니다. 처음에는 빠르게 재시도하여 일시적인 네트워크 끊김에 대응하고, 계속 실패하면 간격을 늘려 서버 부하를 줄입니다.
WhatsApp, Telegram 같은 메신저 앱들이 이런 패턴을 사용합니다. 또 다른 실무 예시로는 대용량 파일 업로드가 있습니다.
대용량 파일을 S3나 클라우드 스토리지에 업로드할 때 네트워크 불안정으로 실패하는 경우가 많습니다. 이때 지수 백오프를 사용하면 처음에는 빠르게 재시도하되, 계속 실패하면 충분한 시간을 두고 재시도하여 성공 확률을 높일 수 있습니다.
하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수 중 하나는 최대 지연 시간을 설정하지 않는 것입니다.
지수 함수는 빠르게 커지므로, 10번째 시도에서는 1024초(17분)를 기다리게 됩니다. 이렇게 되면 사용자가 너무 오래 기다려야 하므로, maxDelay로 상한선을 정해야 합니다.
일반적으로 30초에서 60초 정도가 적당합니다. 또 다른 주의사항은 jitter(지터) 추가입니다.
여러 클라이언트가 동시에 실패하면 모두 같은 시간에 재시도하게 됩니다. 이를 방지하기 위해 랜덤한 시간을 추가하는 것이 좋습니다.
다시 김개발 씨의 이야기로 돌아가 봅시다. 지수 백오프를 적용한 김개발 씨는 서버 개발자에게 연락했습니다.
"지수 백오프를 적용했습니다. 이제 재시도 간격이 1초, 2초, 4초, 8초로 늘어나요." 일주일 후, 서버 개발자로부터 감사 메시지가 왔습니다.
"서버 부하가 확 줄었어요! 사용자 경험도 개선되고, 서버도 안정적이고, 일석이조네요." 지수 백오프를 제대로 활용하면 더 안정적이고 확장 가능한 시스템을 만들 수 있습니다.
여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.
실전 팁
💡 - 기본 지연 시간은 1초, 최대 지연 시간은 30-60초가 적당합니다
- 여러 클라이언트의 동시 재시도를 방지하려면 랜덤 jitter를 추가하세요
- AWS SDK, Google Cloud SDK 등 주요 라이브러리의 지수 백오프 구현을 참고하면 도움이 됩니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
재시도와 타임아웃 완벽 가이드
마이크로서비스에서 필수적인 재시도와 타임아웃 패턴을 실무 중심으로 배웁니다. Resilience4j를 활용한 안정적인 서비스 구축 방법을 단계별로 익힐 수 있습니다.
에러 처리와 폴백 완벽 가이드
AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.
외부 API 연동 에이전트 완벽 가이드
Lambda를 활용한 외부 API 연동 에이전트 구현부터 에러 처리, 타임아웃 관리, 테스트까지 실무에 바로 적용 가능한 완벽 가이드입니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.
Riverpod 3.0 쇼핑 앱 종합 프로젝트 완벽 가이드
Flutter와 Riverpod 3.0을 활용한 실무 수준의 쇼핑 앱 개발 과정을 단계별로 학습합니다. 상품 목록, 장바구니, 주문, 인증, 검색 기능까지 모든 핵심 기능을 구현하며 상태 관리의 실전 노하우를 익힙니다.
Riverpod 3.0 requireValue로 Provider 결합하기
Riverpod 3.0에 새로 추가된 requireValue를 활용하여 여러 Provider의 데이터를 효율적으로 결합하는 방법을 배웁니다. 비동기 데이터를 마치 동기 데이터처럼 다루는 실전 패턴을 소개합니다.