TIL

7_4.트랜잭션 예외 처리와 외부 API 연동 실습

꿀승 2025. 2. 9. 22:57
728x90
반응형
SMALL

학습내용

  1. 트랜잭션과 외부 API 연동
  2. 트랜잭션 내 외부 API 호출 처리 전략
  3. OpenFeign 사용

학습정리

1. 트랜잭션과 외부 API 연동

  • 트랜잭션과 외부 API 호출의 관계
    1. 트랜잭션 내 API 호출
      • 네트워크 오류나, 외부 시스템 장애로 전체 트랜잭션이 실패 할 수 있음.
    2. API 호출 실패 시 데이터 정합성 문제
      • API 호출 실패시 이미 수행된 데이터베이스 작업이 롤백 되지 않는다면 데이터 정합성이 깨질 수 있음.
    3. 분산 트랜잭션 관리의 어려움
      • 데이터베이스 트랜잭션과 외부 API 호출을 하나의 트랜잭션으로 관리하기는 어렵다.

        보통 외부 API 호출 먼저 실행 후에 작업 진행 (트레이드오프)

  • 네티워크 오류 및 외부 시스템 장애 상황 고려
    1. 네트워크 오류
    2. 외부 시스템 장애
    3. 타임아웃 및 예외 처리
    4. 재시도 로직 적용

2. 트랜잭션 내 외부 API 호출 처리 전략

  • Retry 기벌 활용 (재시도 전략)

    • 의존성 추가

      implementation 'org.springframework.retry:spring-retry'
    • Main 클래스 설정 추가

      @EnableRetry //추가
      @SpringBootApplication
      public class SeungApplication {
      
        public static void main(String[] args) {
          SpringApplication.run(SeungApplication.class, args);
        }
      }
    • 사용방법 (해당 서비스에 적용)

        @Transactional
          //value: ServiceException.class 예외가 발생했을 때
          //maxAttempts: 최대 3번 재시도
          //backoff : 재시도 간격 딜레이는 3초 (3000ms) 기다린 후 다시 시도 
        @Retryable(value = ServiceException.class, maxAttempts = 3, backoff = @Backoff(delay = 3000))
        public void save() {
            // ... 비즈니스 로직
          throw new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT); // retry 실행
        }
  • 타임아웃 설정 (FeignClient 활용)

      private static final long CONNECT_TIMEOUT = 10000;
      private static final long READ_TIMEOUT = 60000;
    
      private Builder feignBuilder() {
        return Feign.builder()
            .client(new OkHttpClient())
            .encoder(new JacksonEncoder())
            .decoder(new JacksonDecoder())
            .options(new Request.Options(
                //연결시간 10초
                CONNECT_TIMEOUT, TimeUnit.MILLISECONDS,
                //읽기시간 60초
                READ_TIMEOUT, TimeUnit.MILLISECONDS,
                true
            ))
            //재시도 정책은 사용 안함, 위에 사용한 Retry 사용
            .retryer(Retryer.NEVER_RETRY);
      }

3. OpenFeign 사용

  • 의존성 추가

    implementation 'io.github.openfeign:feign-core:13.0'
    implementation 'io.github.openfeign:feign-jackson:13.0'
    implementation 'io.github.openfeign:feign-okhttp:13.0'
    implementation 'io.github.openfeign.form:feign-form:3.8.0'
    
  • 외부 API 응답 구조 작성

    @Getter
    @Setter
    @ToString
    @NoArgsConstructor
    @FieldDefaults(level = AccessLevel.PRIVATE)
    public class ExternalProductResponse {
    
      Boolean result;
      ExternalError error;
      ExternalPage message;
    
      @Getter
      @Setter
      @ToString
      @NoArgsConstructor
      @FieldDefaults(level = AccessLevel.PRIVATE)
      public static class ExternalError {
    
        String errorCode;
        String errorMessage;
      }
    
      @Getter
      @Setter
      @ToString
      @NoArgsConstructor
      @FieldDefaults(level = AccessLevel.PRIVATE)
      public static class ExternalPage {
    
        List<ExternalResponse> contents;
        ExternalPageable pageable;
      }
    
      @Getter
      @Setter
      @ToString
      @NoArgsConstructor
      @FieldDefaults(level = AccessLevel.PRIVATE)
      public static class ExternalPageable {
        ...생략
      }
    
      @Getter
      @Setter
      @ToString
      @NoArgsConstructor
      @FieldDefaults(level = AccessLevel.PRIVATE)
      public static class ExternalResponse {
        ...생략
      }
    }
  • OpenFeign 인터페이스 작성

    import feign.Headers;
    import feign.Param;
    import feign.RequestLine;
    
    //Headers : 모든 요청의 헤더에 컨텐트타입 추가
    @Headers("Content-Type: application/json")
    public interface ExternalShopClient {
        //Feign의 요청 형식(RequestLine) 을 정의
      @RequestLine("GET /products?page={page}&size={size}")
      ExternalProductResponse getProducts(@Param("page") Integer page,
          @Param("size") Integer size);
    }
    
  • Feign config 설정파일

    @Configuration
    public class OpenFeignConfig {
      //application.yml에 설정한 외부 api url
      @Value("${external.external-shop.url}")
      private String externalShop;
    
      private static final long CONNECT_TIMEOUT = 10000;
      private static final long READ_TIMEOUT = 60000;
    
      //Feign클라이언트 Bean 생성
      @Bean
      public ExternalShopClient externalShop() {
        return feignBuilder()
            .target(ExternalShopClient.class, externalShop);
      }
    
      private Builder feignBuilder() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    
        return Feign.builder()
            .client(new OkHttpClient()) //OkHttpClient 사용
            .encoder(new JacksonEncoder(objectMapper)) //Jackson기반 json변환 설정
            .decoder(new JacksonDecoder(objectMapper))
            //앞서 타임아웃 설정과 동일
            .options(new Request.Options(
                CONNECT_TIMEOUT, TimeUnit.MILLISECONDS,
                READ_TIMEOUT, TimeUnit.MILLISECONDS,
                true
            ))
            .retryer(Retryer.NEVER_RETRY);
      }
    }
    
    • 외부 API가 여러개 일 경우 Feign클라이언트 Bean 생성을 추가 작성
  • 사용방법

    @Slf4j
    @Service
    @RequiredArgsConstructor
    public class ProductExternalService {
    //해당 인터페이스 주입
    private final ExternalShopClient externalShopClient;
    
    @Transactional
    //retry 사용
    @Retryable(value = ServiceException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
    public void save() {
      //사용 
      ExternalProductResponse responses = externalShopClient.getProducts(1, 10);
    
      ...생략
    }
    }

추가적으로

  • 해당 예시에서 사용된 OpenFeign은 github에서 가져온 것으로 SpringCloud와는 조금 다를 수 있음.
728x90
반응형
LIST