TIL

8장 외부 API 패턴

8.1 외부 API 설계 이슈

8.1.1 API 설계 이슈: FTGO 모바일 클라이언트

8.1.2 API 설계 이슈: 다른 종류의 클라이언트

8.2 API 게이트웨이 패턴

8.2.1 API 게이트웨이 패턴 개요

API 게이트웨이 아키텍처

API 게이트웨이 소유권 모델

프런트엔드 패턴을 위한 백엔드

8.2.2 API 게이트웨이 장단점

8.2.4 API 게이트웨이 설계 이슈

성능과 확장성

리액티브 프로그래밍 추상체

부분 실패 처리

아키텍처의 선량한 시민 되기

8.3 API 게이트웨이 구현

8.3.1 기성 API 게이트웨이 제품/서비스 활용

8.3.2 API 게이트웨이 자체 개발

넷플릭스 주울

스프링 클라우드 게이트웨이

OrderConfiguration 클래스

@Configuration
@EnableConfigurationProperties(OrderDestinations.class)
public class OrderConfiguration {
    
    @Bean
    public RouteLocator orderProxyRouting(RouteLocatorBuilder builder, 
                                          OrderDestinations orderDestinations) {
        return builder.routes()
            .route(r -> r.path("/orders") // 기본적으로 /orders로 시작하는 요청은 모두 orderDestinations.getOrderServiceUrl로 라우팅
              .and().method("POST").uri(orderDestinations.getOrderServiceUrl()))
            .route(r -> r.path("/orders")
              .and().method("PUT").uri(orderDestinations.getOrderServiceURL()))
            .route(r -> r.path("/orders/**")
              .and().method("POST").uri(orderDestinations.getOrderServiceURL()))
            .route(r -> r.path("/orders/**")
              .and().method("PUT").uri(orderDestinations.getOrderServiceURL()))  
            .route(r -> r.path("/orders")
              .and().method("GET").uri(orderDestinations.getOrderHistoryServiceUrl()))
            .build();
    }

    @Bean
    public RouterFunction<ServiceResponse> orderHandlerRouting(OrderHandlers orderHandlers) {
        return RouterFunctions.route(GET("/orders/{orderId}"), orderHandlers::getOrderDetails);
    }

    @Bean
    public OrderHandlers orderHandlers(OrderService orderService, 
                                       KitchenService kitchenService,
                                       DeliveryService deliveryService,
                                       AccountingService accountingService) {
        return new OrderHandlers(orderService, kitchenService, deliveryService, accountingService);
    }
}
@ConfigurationProperties(prefix = "order.destinations")
public class OrderDestinations {
    @NotNull
    public String orderServiceUrl;

    public String getOrderServiceUrl() {
        return orderServiceUrl;
    }

    public void setOrderServiceUrl(String orderServiceUrl) {
        this.orderServiceUrl = orderServiceUrl;
    }
// ...
}

OrderHandlers 클래스

public class OrderHandlers {

    private OrderServiceProxy orderService;
    private KitchenService kitchenService
    private DeliveryService deliveryService;
    private AccountintService accountingService;

    // 생성자

    // API를 조합하여 주문 내역을 조회
    // 리액티브 스타일로 네 서비스를 병렬 호출한 결과를 조합
    public Mono<ServerResponse> getOrderDetails(ServerRequest serverRequest {
        String orderId = serverRequest.pathVariable("orderId");

        Mono<OrderInfo> orderInfo = orderService.findOrderById(orderId);
        Mono<Optional<TicketInfo>> ticketInfo = 
            kitchenService
                .findTicketByOrderId(orderId)
                .map(Optional::of)
                .orErrorReturn(Optional.empty());
        Mono<Optional<DeliveryInfo>> deliveryInfo = 
            deliveryService
                .findDeliveryByOrderId(orderId)
                .map(Optional::of)
                .orErrorReturn(Optional.empty());
        Mono<Optional<BillInfo>> billInfo = 
            accountingService
                .findBillByOrderId(orderId)
                .map(Optional::of)
                .orErrorReturn(Optional.empty());

        Mono<Tuple4<OrderInfo, Optional<TicketInfo>, 
            Optional<DeliveryInfo>, Optional<BillInfo>>> combined = 
            Mono.when(orderInfo, ticketInfo, deliveryInfo, billInfo);

        Mono<OrderDetails> orderDetails = combined.map(OrderDetails::makeOrderDetails);
        return orderDetails.flatMap(person -> ServerResponse.ok()
            .contentType(MediaType.APPLICATION_JSON)
            .body(fromObject(person)))
            .onErrorResume(OrderNotFoundException.class, 
              e -> ServerResponse.notFound().build());
    }
}

OrderServiceProxy 클래스

@Service
public class OrderServiceProxy {
    private OrderDestinations orderDestinations;
    private WebClient client;
    // 생성자

    public Mono<OrderInfo> findOrderById(String orderId) {
        Mono<ClientResponse> response = client
            .get()
            .url(orderDestinations.orderServiceUrl + "/orders/{orderId}", orderId)
            .exchange(); // 서비스 호출
        return response.flatMap(resp -> 
            switch (resp.statusCode()) {
              case OK:
                return resp.bodyToMono(OrderInfo.class);
              case NOT_FOUND:
                return Mono.error(new OrderNotFoundException());
              default:
                return Mono.error(new RuntimeException("Unkown" + resp.statusCode()));
    }
}

8.3.3 API 게이트웨이 구현: GraphQL

GraphQL 스키마 정의

type Query { # 클라이언트에서 실행 가능한 쿼리를 정의
  orders(consumerId : Int!): [Order] # Consumer가 주문한 여러 Order 반환
  order(orderId : Int!): Order # 주어진 Order 반환
  consumer(consumerId: Int!): Consumer # 주어진 Consumer 반환
}

type Consumer {
  id: ID
  firtName: String
  lastName: String
  Orders: [Order] # 한 소비자는 여러 주문이 가능
}

type Order {
  orderId: ID
  consumerId: Int
  consumer: Consumer
  restaurant: Restaurant
  deliveryInfo: DeliveryInfo
  # ...
}

type Restaurant {
  id: ID
  name: String
  # ...
}

type DeliveryInfo {
  status: DeliveryStatus
  # ...
}

enum DeliveryStatus {
  PREPARING
  READY_FOR_PICKUP
  PICKED_UP
  DELIVERED
}

GraphQL 쿼리 실행

# 단건 조회
query {
  consumer(consumerId:1) # 소비자 정보를 조회하는 consumer 쿼리 지정
  { # 반환할 Consumer 필드
    firstName
    lastName
  }
}

query { # 앨리어스로 두 소비자를 구분해서 각각 조회
  c1: consumer(consumerId:1) {id, firstName, lastName}
  c2: consumer(consumerId:2) {id, firstName, lastName}
}

# 복잡한 쿼리
query {
  consumer(consumerId:1) {
    id
    firstName
    lastName
    orders {
      orderId
      restaruant {
        id
        name
      }
      deliveryInfo {
        estimatedDeliveryTime
        name
      }
    }
  }
}

스키마를 데이터에 연결

const resolvers = {
  Query: {
    orders: resolveOrders, # orders 쿼리 리졸버
    consumer: resolveConsumer,
    order: resolveOrder
  },
  Order: {
    consumer: resolveOrderConsumer, # order.consumer 필드 리졸버
    restaurant: resolveOrderRestaurant,
    deliveryInfo: resolveOrderDeliveryInfo
  }
# ...
}
function resolveOrders(_, { consumerId }, context) {
  return context.orderServiceProxy.findOrders(consumerId);
}

배칭/캐싱으로 로딩 최적화