Files

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 장애가 숨어 버린다