SpringBoot WebClient은 Spring 5.0에서 도입된 Reactive HTTP 클라이언트이다. RestTemplate의 대안으로, HTTP 요청을 보내기 위한 간단하고 직관적인 API를 제공하고 있다.
이 기록은 WebFlux 를 학습해가는 입장에서 BFF 를 구현한다는 가정하에 Member, Banner 의 목록을 제공하는 API 를 호출해서 하나의 API 로 묶어 FrontEnd 를 위한 API 를 작성 하는 것을 가정하였다. 상세한 부분은 이전에 작성된 글의 소스 를 참고하면 그 소스를 기본으로 하고 있다는 걸 알 수 있다.
1. Backend APIAPI 별로 응답 결과가 다른 것을 가정하여 아래와 같이 구성 하였다.
1) Member API 데이터를 가져올 사용자 목록 부분이 data 속성에 있는 것이 특징이다.
{
"code": 200,
"page": 1,
"limit": 3,
"message": "ok",
"data": [
{
"id": 6,
"name": "Hong Gil Dong",
"profile_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"age": 50,
"created_at": "2023-10-03 20:48:54"
},
{
"id": 5,
"name": "Hong Gil Dong",
"profile_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"age": 33,
"created_at": "2023-10-03 20:48:51"
},
{
"id": 4,
"name": "Hong Gil Dong",
"profile_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"age": 19,
"created_at": "2023-10-03 20:48:49"
}
]
}
Member API (localhost:8080/v1/api/member/list/1?limit=3) 의 응답 2) Banner API 해당 API 는 배너 목록 이외에는 상위에는 특별한 속성이 없다.
[
{
"id": 11,
"banner_name": "샘플베너 11",
"banner_image_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"created_at": "2023-10-03 20:33:30"
},
{
"id": 10,
"banner_name": "샘플베너 10",
"banner_image_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"created_at": "2023-10-03 20:33:29"
},
{
"id": 9,
"banner_name": "샘플베너 9",
"banner_image_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"created_at": "2023-10-03 20:33:27"
}
]
Banner API (localhost:8080/v1/api/banner/list/flux/1?limit=3) 의 응답
2. Backend For Frontend API 편의상 BFF 서버도 동일한 로컬서버 안에 구현했다.
1) 응답 API 의 Model WebClient 로 호출을 하고 응답을 받을때 다양한 구조의 데이터가 들어올거라 생각하고 bff/Response.java 라는 클래스에 inner class 로 먼저 Member 클래스를 만들어 주었다.
package com.example.webfluxapi.bff;
import com.example.webfluxapi.dto.MemberDTO;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
import java.util.List;
public class Response {
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public static class Member {
@JsonProperty("code")
private Integer code;
@JsonProperty("message")
private String message;
@JsonProperty("data")
private List<MemberDTO.Item> data;
}
}
bff/Response.java 아래는 Member 와 Banner 의 Item 모델 속성을 정의한 DTO 이다.
package com.example.webfluxapi.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.hibernate.validator.constraints.Length;
import org.springframework.data.relational.core.mapping.Column;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public class MemberDTO {
@Builder
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static class Item {
@JsonProperty("id")
private Integer id;
@JsonProperty("name")
private String name;
@JsonProperty("profile_url")
private String profileUrl;
@JsonProperty("age")
private Integer age;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@JsonProperty("created_at")
private LocalDateTime createdAt;
}
}
dto/MemberDTO.java package com.example.webfluxapi.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotEmpty;
import lombok.*;
import org.hibernate.validator.constraints.Length;
import org.springframework.data.relational.core.mapping.Column;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
public class BannerDTO {
@Builder
@Setter
@Getter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public static class Item {
@JsonProperty("id")
private Integer id;
@JsonProperty("banner_name")
private String bannerName;
@JsonProperty("banner_image_url")
private String bannerImageUrl;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
@JsonProperty("created_at")
private LocalDateTime createdAt;
}
}
dto/BannerDTO.java 2) BFF Controller 의 구현 package com.example.webfluxapi.controller;
import com.example.webfluxapi.bff.Response;
import com.example.webfluxapi.common.ApiResponse;
import com.example.webfluxapi.dto.BannerDTO;
import com.example.webfluxapi.dto.MemberDTO;
import com.example.webfluxapi.dto.ProductDTO;
import com.example.webfluxapi.mapper.MemberMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/bff")
public class BFFController {
private final MemberMapper memberMapper;
@Autowired
public BFFController(MemberMapper memberMapper) {
this.memberMapper = memberMapper;
}
// member api webclient 인스턴스
WebClient.Builder memberApi = WebClient.builder()
.baseUrl("http://localhost:8080/v1/api/member/list/1?limit=3")
.defaultHeader("Content-Type", "application/json");
// banner api webclient 인스턴스
WebClient.Builder bannerApi = WebClient.builder()
.baseUrl("http://localhost:8080/v1/api/banner/list/flux/1?limit=3")
.defaultHeader("Content-Type", "application/json");
@GetMapping("")
public Mono<ApiResponse> sample() {
// Response.Member 로 가져와서 data 속성에서 목록을 가져오는 부분
Flux<MemberDTO.Item> memList = memberApi.build().get()
.retrieve()
.bodyToFlux(Response.Member.class)
// Flux 에 포함된 Member 객체의 getData 를 호출해서 List 를 Flux로 변경함
.flatMapIterable(Response.Member::getData);
Flux<BannerDTO.Item> bannerList = bannerApi.build().get()
.retrieve()
.bodyToFlux(BannerDTO.Item.class);
// 두개의 Flux 를 하나의 Mono로 결합하고 각 Flux의 데이터가 튜플로 변경됨
return Mono.zip(
memList.collectList(),
bannerList.collectList()
).map(tuple -> {
List<MemberDTO.Item> mem = tuple.getT1();
List<BannerDTO.Item> banner = tuple.getT2();
return ApiResponse.builder()
.code(200)
.message("ok")
.member(mem)
.banner(banner)
.build();
}).switchIfEmpty(Mono.just(ApiResponse.builder()
.code(500)
.message("error")
.build()));
}
}
controller/BFFController.java 3) BFF API 응답 결과 {
"code": 200,
"message": "ok",
"member": [
{
"id": 6,
"name": "Hong Gil Dong",
"profile_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"age": 50,
"created_at": "2023-10-03 20:48:54"
},
{
"id": 5,
"name": "Hong Gil Dong",
"profile_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"age": 33,
"created_at": "2023-10-03 20:48:51"
},
{
"id": 4,
"name": "Hong Gil Dong",
"profile_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"age": 19,
"created_at": "2023-10-03 20:48:49"
}
],
"banner": [
{
"id": 11,
"banner_name": "샘플베너 11",
"banner_image_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"created_at": "2023-10-03 20:33:30"
},
{
"id": 10,
"banner_name": "샘플베너 10",
"banner_image_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"created_at": "2023-10-03 20:33:29"
},
{
"id": 9,
"banner_name": "샘플베너 9",
"banner_image_url": "https://pbs.twimg.com/profile_images/1159314102679793664/p7AMiPpV_400x400.jpg",
"created_at": "2023-10-03 20:33:27"
}
]
}
https://www.stacktips.com/articles/what-is-webclient-how-to-use-webclient-in-java-springboot