5.0 KiB
5.0 KiB
Fallback 예시
좋은 예시
예시 1. 외부 추천 실패 시 빈 추천 목록으로 degrade한다
@Service
@RequiredArgsConstructor
public class RecommendationIntegrationService {
private final RecommendationClient recommendationClient;
public RecommendationResult getRecommendations(String userId) {
try {
return recommendationClient.getRecommendations(userId);
} catch (ExternalRecommendationTemporaryFailure ex) {
return RecommendationResult.degradedEmpty();
}
}
}
좋은 이유:
- 추천은 soft dependency로 다룰 수 있다
- 핵심 기능을 깨지 않고 degraded mode를 제공한다
- fallback 위치가 integration 경계에 있다
예시 2. 캐시된 공개키로 fallback한다
@Service
@RequiredArgsConstructor
public class JwkIntegrationService {
private final JwkClient jwkClient;
private final JwkCache jwkCache;
public JwkSetResult getJwkSet() {
try {
JwkSetResult result = jwkClient.fetch();
jwkCache.put(result);
return result;
} catch (ExternalJwkTemporaryFailure ex) {
return jwkCache.get()
.orElseThrow(() -> ex);
}
}
}
좋은 이유:
- 조회성 데이터에 짧은 TTL 캐시 fallback을 적용할 수 있다
- fallback 가능성과 불가능성이 함께 표현된다
- 외부 실패를 무조건 숨기지 않는다
예시 3. CircuitBreaker fallback을 명시적으로 둔다
@Service
@RequiredArgsConstructor
public class ExternalProfileService {
private final CircuitBreakerFactory<?, ?> circuitBreakerFactory;
private final ExternalProfileClient externalProfileClient;
public ProfileSupplementResult getSupplement(String userId) {
return circuitBreakerFactory.create("external-profile")
.run(
() -> externalProfileClient.getProfile(userId),
throwable -> ProfileSupplementResult.degradedUnavailable()
);
}
}
좋은 이유:
- Spring Cloud CircuitBreaker의 공식 fallback 모델을 따른다
- fallback 결과가 별도 degraded result로 표현된다.
예시 4. 이메일 발송은 비동기 접수로 degrade할 수 있다
@Service
@RequiredArgsConstructor
public class MailIntegrationService {
private final MailClient mailClient;
private final MailOutboxRepository mailOutboxRepository;
public MailDispatchResult sendVerificationMail(MailCommand command) {
try {
mailClient.send(command);
return MailDispatchResult.sent();
} catch (ExternalMailTemporaryFailure ex) {
mailOutboxRepository.enqueue(command);
return MailDispatchResult.acceptedForRetry();
}
}
}
좋은 이유:
- 즉시 발송 실패를 비동기 재처리로 전환한다
- API 의미를 “즉시 완료”가 아니라 “접수됨”으로 명확히 바꿀 수 있다
- hard dependency를 soft dependency로 바꾸는 사례다.
나쁜 예시
예시 1. 결제 확정 실패를 성공처럼 fallback한다
public PaymentCaptureResult capture(CaptureCommand command) {
try {
return paymentClient.capture(command);
} catch (Exception ex) {
return PaymentCaptureResult.success();
}
}
나쁜 이유:
- 실제 결제 확정 실패를 성공처럼 숨긴다
- 정합성과 감사 가능성을 깨뜨린다
- fallback을 쓰면 안 되는 대표 사례다
예시 2. 오래된 캐시를 무기한 사용한다
public ExchangeRateResult getRate(String currency) {
try {
return exchangeRateClient.getRate(currency);
} catch (Exception ex) {
return foreverCache.get(currency);
}
}
나쁜 이유:
- stale budget이 없다
- 오래된 데이터를 최신 사실처럼 쓰게 된다
- 운영에서 품질 저하를 통제할 수 없다
예시 3. controller에서 fallback을 직접 구현한다
@RestController
@RequiredArgsConstructor
public class UserController {
private final ExternalProfileClient externalProfileClient;
@GetMapping("/api/v1/users/{userId}")
public ApiResult<UserResponse> get(@PathVariable String userId) {
try {
ExternalProfileResponse response = externalProfileClient.getProfile(userId);
return ApiResult.success(UserResponse.from(response));
} catch (Exception ex) {
return ApiResult.success(UserResponse.withoutProfile());
}
}
}
나쁜 이유:
- fallback이 controller로 새어 나갔다
- provider-aware 로직이 presentation 경계에 있다
- 공통 observability와 정책 일관성이 깨진다
예시 4. fallback 발생을 전혀 기록하지 않는다
try {
return recommendationClient.getRecommendations(userId);
} catch (Exception ex) {
return RecommendationResult.degradedEmpty();
}
나쁜 이유:
- degraded mode가 운영에서 보이지 않는다
- fallback rate를 추적할 수 없다
- upstream 장애가 숨어 버린다