Skip to content

Commit

Permalink
Merge pull request #142 from woowa-techcamp-2024/feature/139-restaura…
Browse files Browse the repository at this point in the history
…nt-exposure-webflux

[feature] 가게 노출의 요청과 응답을 비동기로 처리한다.
  • Loading branch information
jcw1031 authored Aug 28, 2024
2 parents e0918f8 + a3c448f commit ee624f9
Show file tree
Hide file tree
Showing 18 changed files with 163 additions and 97 deletions.
1 change: 0 additions & 1 deletion api-client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ subprojects {
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
implementation 'io.github.resilience4j:resilience4j-reactor'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package woowa.team4.bff.api.client.caller;

import java.util.List;
import java.util.concurrent.CompletableFuture;
import lombok.RequiredArgsConstructor;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import woowa.tema4.bff.api.client.caller.AsyncClientApiCaller;
import woowa.tema4.bff.api.client.caller.SyncClientApiCaller;
import woowa.tema4.bff.api.client.caller.WebClientCaller;
import woowa.team4.bff.api.client.config.SearchClientConfiguration;
import woowa.team4.bff.api.client.request.SearchRequest;
import woowa.team4.bff.api.client.response.SearchResponse;
import woowa.tema4.bff.api.client.caller.AsyncClientApiCaller;
import woowa.tema4.bff.api.client.caller.SyncClientApiCaller;
import woowa.tema4.bff.api.client.caller.WebClientCaller;

@Component
@RequiredArgsConstructor
public class SearchApiCaller {

private final SearchClientConfiguration searchClientConfiguration;
private final SyncClientApiCaller syncClientApiCaller;
private final AsyncClientApiCaller asyncClientApiCaller;
Expand All @@ -28,15 +28,15 @@ public SearchResponse send(SearchRequest SearchRequest) {
});
}

public CompletableFuture<List<SearchResponse>> sendAsyncCompletableFuture(
public CompletableFuture<SearchResponse> sendAsyncCompletableFuture(
SearchRequest SearchRequest) {
return asyncClientApiCaller.post(searchClientConfiguration.getUrl(),
SearchRequest,
new ParameterizedTypeReference<>() {
});
}

public Mono<List<SearchResponse>> sendAsyncMono(
public Mono<SearchResponse> sendAsyncMono(
SearchRequest SearchRequest) {
return webClientCaller.post(
searchClientConfiguration.getUrl(),
Expand Down
8 changes: 1 addition & 7 deletions domain/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
subprojects {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// util
implementation 'org.springframework.boot:spring-boot-starter-validation'
// mapper
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'

}
}
9 changes: 9 additions & 0 deletions domain/cache-domain-redis/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
dependencies {
// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// util
implementation 'org.springframework.boot:spring-boot-starter-validation'
// mapper
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
implementation project(":domain:exposure-common-domain")
Expand Down
5 changes: 5 additions & 0 deletions domain/common-domain/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dependencies {
// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
}
4 changes: 0 additions & 4 deletions domain/exposure-domain/build.gradle
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
dependencies {
// web
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// module
implementation project(":core-web")
implementation project(":api-client:cache-client")
implementation project(":api-client:search-client")
implementation project(":api-client:delivery-client")
implementation project(":api-client:coupon-client")
implementation project(":api-client:advertisement-client")
implementation project(":domain:exposure-common-domain")
// implementation project(":domain:search-domain-rdb")
// implementation project(":domain:search-domain-es")
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package woowa.team4.bff.exposure.service;

import static java.util.stream.Collectors.*;

import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -13,19 +18,11 @@
import woowa.team4.bff.api.client.coupon.response.CouponResponse;
import woowa.team4.bff.api.client.delivery.response.DeliveryTimeResponse;
import woowa.team4.bff.api.client.request.SearchRequest;
import woowa.team4.bff.api.client.response.SearchResponse;
import woowa.team4.bff.domain.ExposureRestaurantSummary;
import woowa.team4.bff.exposure.command.SearchCommand;
import woowa.team4.bff.exposure.external.caller.AsyncExternalApiCaller;
import woowa.team4.bff.exposure.external.result.ExternalApiResult;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

@Slf4j
@Service
@RequiredArgsConstructor
Expand All @@ -37,29 +34,31 @@ public class RestaurantExposureListService {
private final CacheApiCaller cacheApiCaller;
private final AsyncExternalApiCaller asyncExternalApiCaller;

public List<ExposureRestaurantSummary> search(SearchCommand command) {
SearchResponse searchResponse = searchApiCaller.send(new SearchRequest(command.keyword(),
command.deliveryLocation(), command.pageNumber()));
List<Long> restaurantIds = searchResponse.getIds();
// 비동기 호출
cacheApiCaller.sendAsyncMono(new RankingRequest(command.keyword())).subscribe();
List<ExternalApiResult> externalApiResults = getExternalResult(restaurantIds,
command.keyword())
.stream()
.sorted(
Comparator.comparing(ExternalApiResult::getAdRank)
.thenComparing(ExternalApiResult::getMin)
)
.skip(DEFAULT_PAGE_SIZE * command.pageNumber())
.limit(DEFAULT_PAGE_SIZE)
.toList();

return mergeSummariesWithExternalResults(externalApiResults);
public Mono<List<ExposureRestaurantSummary>> search(SearchCommand command) {
return searchApiCaller.sendAsyncMono(new SearchRequest(command.keyword(),
command.deliveryLocation(), command.pageNumber()))
.flatMap(searchResponse -> {
List<Long> restaurantIds = searchResponse.getIds();
// 비동기 호출
cacheApiCaller.sendAsyncMono(new RankingRequest(command.keyword())).subscribe();
return getExternalResult(restaurantIds, command.keyword())
.flatMap(externalApiResults -> Mono.just(externalApiResults
.stream()
.sorted(
Comparator.comparing(ExternalApiResult::getAdRank)
.thenComparing(ExternalApiResult::getMin)
)
.skip(DEFAULT_PAGE_SIZE * command.pageNumber())
.limit(DEFAULT_PAGE_SIZE)
.toList()
))
.map(this::mergeSummariesWithExternalResults);
});
}

public List<ExternalApiResult> getExternalResult(
public Mono<List<ExternalApiResult>> getExternalResult(
final List<Long> restaurantIds, final String keyword) {
return searchAsynchronouslyWebFlux(restaurantIds, keyword).block();
return searchAsynchronouslyWebFlux(restaurantIds, keyword);
}

/**
Expand All @@ -68,7 +67,7 @@ public List<ExternalApiResult> getExternalResult(
* @param restaurantIds
*/
public Mono<List<ExternalApiResult>> searchAsynchronouslyWebFlux(List<Long> restaurantIds,
String keyword) {
String keyword) {
// 캐시 외부 API 요청
Mono<List<CacheResponse>> restaurantSummaryMono = asyncExternalApiCaller
.getCacheWebFlux(restaurantIds)
Expand Down Expand Up @@ -150,10 +149,14 @@ public Mono<List<ExternalApiResult>> searchAsynchronouslyWebFlux(List<Long> rest
return restaurantIds.stream()
.map(id -> ExternalApiResult.builder()
.restaurantId(id)
.restaurantUuid(restaurantSummaryMap.get(id).getRestaurantUuid())
.restaurantName(restaurantSummaryMap.get(id).getRestaurantName())
.restaurantThumbnailUrl(restaurantSummaryMap.get(id).getRestaurantThumbnailUrl())
.minimumOrderAmount(restaurantSummaryMap.get(id).getMinimumOrderAmount())
.restaurantUuid(
restaurantSummaryMap.get(id).getRestaurantUuid())
.restaurantName(
restaurantSummaryMap.get(id).getRestaurantName())
.restaurantThumbnailUrl(restaurantSummaryMap.get(id)
.getRestaurantThumbnailUrl())
.minimumOrderAmount(
restaurantSummaryMap.get(id).getMinimumOrderAmount())
.reviewCount(restaurantSummaryMap.get(id).getReviewCount())
.rating(restaurantSummaryMap.get(id).getRating())
.menus(restaurantSummaryMap.get(id).getMenus())
Expand Down
8 changes: 8 additions & 0 deletions domain/menu-domain-rdb/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
dependencies {
// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// util
implementation 'org.springframework.boot:spring-boot-starter-validation'
// mapper
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
// module
implementation project(":event:event-publisher")
implementation project(":event:common-event")
Expand Down
8 changes: 8 additions & 0 deletions domain/restaurant-domain-rdb/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
dependencies {
// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// util
implementation 'org.springframework.boot:spring-boot-starter-validation'
// mapper
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
// module
implementation project(":event:restaurant-event")
implementation project(":event:event-publisher")
Expand Down
8 changes: 8 additions & 0 deletions domain/review-domain-rdb/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
dependencies {
// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// util
implementation 'org.springframework.boot:spring-boot-starter-validation'
// mapper
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
// module
implementation project(":domain:restaurant-domain-rdb")
implementation project(":domain:menu-domain-rdb")
Expand Down
8 changes: 8 additions & 0 deletions domain/search-domain-es/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
dependencies {
// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// util
implementation 'org.springframework.boot:spring-boot-starter-validation'
// mapper
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
// db
implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch:2.7.18'
// module
Expand Down
8 changes: 8 additions & 0 deletions domain/search-domain-rdb/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
dependencies {
// db
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
// util
implementation 'org.springframework.boot:spring-boot-starter-validation'
// mapper
implementation 'org.mapstruct:mapstruct:1.5.3.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.3.Final'
// module
implementation project(":domain:restaurant-domain-rdb")
implementation project(":domain:menu-domain-rdb")
Expand Down
5 changes: 2 additions & 3 deletions service/restaurant-exposure-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,13 @@ bootJar.enabled = true
jar.enabled = false

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// web
implementation 'org.springframework.boot:spring-boot-starter-webflux'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
// util
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// module
implementation project(":core-web")
implementation project(":domain:exposure-common-domain")
implementation project(":domain:exposure-domain")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.reactive.config.EnableWebFlux;

@SpringBootApplication
@EnableWebFlux
public class RestaurantExposureServiceApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
package woowa.team4.bff.restauarntexposureservice.exposure.controller;


import static woowa.team4.bff.common.utils.ApiUtils.success;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import woowa.team4.bff.common.utils.ApiUtils.ApiResult;
import reactor.core.publisher.Mono;
import woowa.team4.bff.domain.ExposureRestaurantSummary;
import woowa.team4.bff.exposure.command.SearchCommand;
import woowa.team4.bff.exposure.service.RestaurantExposureListService;
import woowa.team4.bff.restauarntexposureservice.exposure.utils.ApiUtils;
import woowa.team4.bff.restauarntexposureservice.exposure.utils.ApiUtils.ApiResult;

@RequestMapping("/api/v1/restaurant-summary")
@RestController
Expand All @@ -22,13 +22,13 @@ public class RestaurantSummaryApiController {
private final RestaurantExposureListService restaurantExposureListService;

@GetMapping("/cache")
public ApiResult<List<ExposureRestaurantSummary>> getRestaurantSummaries(
public Mono<ApiResult<List<ExposureRestaurantSummary>>> getRestaurantSummaries(
@RequestParam("keyword") String keyword,
@RequestParam("deliveryLocation") String deliveryLocation,
@RequestParam("pageNumber") Integer pageNumber
) {
List<ExposureRestaurantSummary> response = restaurantExposureListService.search(
new SearchCommand(keyword, deliveryLocation, pageNumber));
return success(response);
return restaurantExposureListService.search(
new SearchCommand(keyword, deliveryLocation, pageNumber))
.map(ApiUtils::success);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package woowa.team4.bff.restauarntexposureservice.exposure.utils;

import lombok.ToString;

/**
* API 응답을 표준화하기 위한 유틸리티 클래스이다. 이 클래스는 성공 및 오류 응답을 생성하는 메서드와 응답 형식을 정의하는 내부 클래스를 제공한다.
*/
public class ApiUtils {

/**
* 성공적인 API 응답을 생성
*
* @param response 응답 데이터
* @param <T> 응답 데이터의 타입
* @return 성공 상태와 응답 데이터를 포함한 ApiResult 객체
*/
public static <T> ApiResult<T> success(T response) {
return new ApiResult<>(true, response, null);
}

/**
* API 응답 결과를 나타내는 내부 클래스
*
* @param <T> 응답 데이터의 타입
*/
@ToString
public static class ApiResult<T> {

private final boolean success;
private final T response;
private final String error;

private ApiResult(boolean success, T response, String error) {
this.success = success;
this.response = response;
this.error = error;
}

public boolean isSuccess() {
return success;
}

public String getError() {
return error;
}

public T getResponse() {
return response;
}
}
}
Loading

0 comments on commit ee624f9

Please sign in to comment.