mirror of
https://github.com/cna-bootcamp/phonebill.git
synced 2025-12-06 08:06:24 +00:00
API Gateway Swagger 통합 문제 분석 완료
주요 문제점 식별: - Gateway 라우팅 경로 불일치 (product-service: /products/**, bill-service: /api/v1/bills/**) - OpenAPI 서버 정보와 실제 Gateway 경로 매핑 누락 - Swagger UI에서 "Try it out" 기능 미작동 다음 단계: 라우팅 경로 통일화 및 OpenAPI 서버 정보 수정 예정 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
02bcfa5434
commit
2a719048f8
@ -3,7 +3,6 @@
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<entry key="AUTH_SERVICE_URL" value="http://localhost:8081" />
|
||||
<entry key="BILL_SERVICE_URL" value="http://localhost:8082" />
|
||||
<entry key="CORS_ALLOWED_ORIGINS" value="http://localhost:3000" />
|
||||
<entry key="JWT_ACCESS_TOKEN_VALIDITY" value="18000000" />
|
||||
@ -12,6 +11,7 @@
|
||||
<entry key="PRODUCT_SERVICE_URL" value="http://localhost:8083" />
|
||||
<entry key="SERVER_PORT" value="8080" />
|
||||
<entry key="SPRING_PROFILES_ACTIVE" value="dev" />
|
||||
<entry key="USER_SERVICE_URL" value="http://localhost:8081" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
|
||||
@ -8,12 +8,20 @@ ext {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Common module dependency
|
||||
implementation project(':common')
|
||||
// Common module dependency (exclude WebMVC, Security, and non-reactive Redis for WebFlux)
|
||||
implementation(project(':common')) {
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-web'
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-security'
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-data-jpa'
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-data-redis'
|
||||
}
|
||||
|
||||
// Spring Cloud Gateway (api-gateway specific)
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
|
||||
|
||||
// Redis for health checks and caching
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
|
||||
|
||||
// Circuit Breaker (api-gateway specific)
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
|
||||
|
||||
|
||||
@ -101,27 +101,8 @@ public class GatewayConfig {
|
||||
)
|
||||
.uri("lb://product-service"))
|
||||
|
||||
// KOS Mock Service 라우팅 (내부 서비스용)
|
||||
.route("kos-mock-service", r -> r
|
||||
.path("/kos/**")
|
||||
.filters(f -> f
|
||||
.circuitBreaker(cb -> cb
|
||||
.setName("kos-mock-cb")
|
||||
.setFallbackUri("forward:/fallback/kos"))
|
||||
.retry(retry -> retry
|
||||
.setRetries(5)
|
||||
.setBackoff(java.time.Duration.ofSeconds(1), java.time.Duration.ofSeconds(5), 2, true)))
|
||||
.uri("lb://kos-mock-service"))
|
||||
|
||||
// Health Check 라우팅 (인증 불필요)
|
||||
.route("health-check", r -> r
|
||||
.path("/health", "/actuator/health")
|
||||
.uri("http://localhost:8080"))
|
||||
|
||||
// Swagger UI 라우팅 (개발환경에서만 사용)
|
||||
.route("swagger-ui", r -> r
|
||||
.path("/swagger-ui/**", "/v3/api-docs/**")
|
||||
.uri("http://localhost:8080"))
|
||||
// 주의: Gateway 자체 엔드포인트는 라우팅하지 않음
|
||||
// Health Check와 Swagger UI는 Spring Boot에서 직접 제공
|
||||
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -7,13 +7,13 @@ import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springdoc.core.properties.SwaggerUiConfigParameters;
|
||||
import reactor.core.publisher.Mono;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Swagger 통합 문서화 설정
|
||||
@ -35,8 +35,8 @@ import java.util.List;
|
||||
@Profile("!prod") // 운영환경에서는 비활성화
|
||||
public class SwaggerConfig {
|
||||
|
||||
@Value("${services.auth-service.url:http://localhost:8081}")
|
||||
private String authServiceUrl;
|
||||
@Value("${services.user-service.url:http://localhost:8081}")
|
||||
private String userServiceUrl;
|
||||
|
||||
@Value("${services.bill-service.url:http://localhost:8082}")
|
||||
private String billServiceUrl;
|
||||
@ -44,33 +44,29 @@ public class SwaggerConfig {
|
||||
@Value("${services.product-service.url:http://localhost:8083}")
|
||||
private String productServiceUrl;
|
||||
|
||||
@Value("${services.kos-mock-service.url:http://localhost:8084}")
|
||||
private String kosMockServiceUrl;
|
||||
@Value("${services.kos-mock.url:http://localhost:8084}")
|
||||
private String kosMockUrl;
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public SwaggerConfig() {
|
||||
this.webClient = WebClient.builder()
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Swagger UI 설정 파라미터
|
||||
*
|
||||
* SpringDoc WebFlux에서는 기본 설정을 사용하고 필요시 커스터마이징합니다.
|
||||
*
|
||||
* @return SwaggerUiConfigParameters
|
||||
*/
|
||||
@Bean
|
||||
public SwaggerUiConfigParameters swaggerUiConfigParameters() {
|
||||
// Spring Boot 3.x에서는 SwaggerUiConfigParameters 생성자가 변경됨
|
||||
SwaggerUiConfigParameters parameters = new SwaggerUiConfigParameters(
|
||||
return new SwaggerUiConfigParameters(
|
||||
new org.springdoc.core.properties.SwaggerUiConfigProperties()
|
||||
);
|
||||
|
||||
// 각 마이크로서비스의 OpenAPI 문서 URL 설정
|
||||
List<String> urls = new ArrayList<>();
|
||||
urls.add("Gateway::/v3/api-docs");
|
||||
urls.add("Auth Service::" + authServiceUrl + "/v3/api-docs");
|
||||
urls.add("Bill Service::" + billServiceUrl + "/v3/api-docs");
|
||||
urls.add("Product Service::" + productServiceUrl + "/v3/api-docs");
|
||||
urls.add("KOS Mock::" + kosMockServiceUrl + "/v3/api-docs");
|
||||
|
||||
// Spring Boot 3.x 호환성을 위한 설정
|
||||
System.setProperty("springdoc.swagger-ui.urls", String.join(",", urls));
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -131,18 +127,24 @@ public class SwaggerConfig {
|
||||
.GET("/api-docs", request ->
|
||||
ServerResponse.temporaryRedirect(URI.create("/swagger-ui.html")).build())
|
||||
|
||||
// 서비스별 API 문서 프록시
|
||||
.GET("/v3/api-docs/auth", request ->
|
||||
proxyApiDocs(authServiceUrl + "/v3/api-docs"))
|
||||
// Gateway API 문서 직접 제공
|
||||
.GET("/v3/api-docs/gateway", request ->
|
||||
ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue(getGatewayApiDoc()))
|
||||
|
||||
.GET("/v3/api-docs/bills", request ->
|
||||
// 서비스별 API 문서 프록시
|
||||
.GET("/v3/api-docs/user", request ->
|
||||
proxyApiDocs(userServiceUrl + "/v3/api-docs"))
|
||||
|
||||
.GET("/v3/api-docs/bill", request ->
|
||||
proxyApiDocs(billServiceUrl + "/v3/api-docs"))
|
||||
|
||||
.GET("/v3/api-docs/products", request ->
|
||||
.GET("/v3/api-docs/product", request ->
|
||||
proxyApiDocs(productServiceUrl + "/v3/api-docs"))
|
||||
|
||||
.GET("/v3/api-docs/kos", request ->
|
||||
proxyApiDocs(kosMockServiceUrl + "/v3/api-docs"))
|
||||
proxyApiDocs(kosMockUrl + "/v3/api-docs"))
|
||||
|
||||
.build();
|
||||
}
|
||||
@ -156,30 +158,117 @@ public class SwaggerConfig {
|
||||
* @return ServerResponse
|
||||
*/
|
||||
private Mono<ServerResponse> proxyApiDocs(String apiDocsUrl) {
|
||||
// 실제 구현에서는 WebClient를 사용하여 마이크로서비스의 API 문서를 가져와야 합니다.
|
||||
// 현재는 임시로 빈 문서를 반환합니다.
|
||||
return ServerResponse.ok()
|
||||
return webClient.get()
|
||||
.uri(apiDocsUrl)
|
||||
.retrieve()
|
||||
.onStatus(status -> status.isError(), clientResponse ->
|
||||
Mono.error(new RuntimeException("Service unavailable")))
|
||||
.bodyToMono(String.class)
|
||||
.onErrorReturn(getDefaultApiDoc(apiDocsUrl))
|
||||
.flatMap(body ->
|
||||
ServerResponse.ok()
|
||||
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
|
||||
.bodyValue("{\n" +
|
||||
.bodyValue(body)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway API 문서 생성
|
||||
*
|
||||
* Gateway 자체의 OpenAPI 문서를 생성합니다.
|
||||
*
|
||||
* @return Gateway API 문서 JSON
|
||||
*/
|
||||
private String getGatewayApiDoc() {
|
||||
return "{\n" +
|
||||
" \"openapi\": \"3.0.1\",\n" +
|
||||
" \"info\": {\n" +
|
||||
" \"title\": \"Service API\",\n" +
|
||||
" \"title\": \"PhoneBill API Gateway\",\n" +
|
||||
" \"version\": \"1.0.0\",\n" +
|
||||
" \"description\": \"마이크로서비스 API 문서\\n\\n" +
|
||||
"실제 서비스가 시작되면 상세한 API 문서가 표시됩니다.\"\n" +
|
||||
" \"description\": \"통신요금 관리 서비스 API Gateway\\n\\n" +
|
||||
"이 문서는 API Gateway의 헬스체크 및 관리 기능을 설명합니다.\"\n" +
|
||||
" },\n" +
|
||||
" \"paths\": {\n" +
|
||||
" \"/status\": {\n" +
|
||||
" \"/health\": {\n" +
|
||||
" \"get\": {\n" +
|
||||
" \"summary\": \"서비스 상태 확인\",\n" +
|
||||
" \"summary\": \"헬스 체크\",\n" +
|
||||
" \"description\": \"API Gateway 서비스 상태를 확인합니다.\",\n" +
|
||||
" \"responses\": {\n" +
|
||||
" \"200\": {\n" +
|
||||
" \"description\": \"서비스 정상\"\n" +
|
||||
" \"description\": \"서비스 정상\",\n" +
|
||||
" \"content\": {\n" +
|
||||
" \"application/json\": {\n" +
|
||||
" \"schema\": {\n" +
|
||||
" \"type\": \"object\",\n" +
|
||||
" \"properties\": {\n" +
|
||||
" \"status\": { \"type\": \"string\" }\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}");
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" },\n" +
|
||||
" \"/actuator/health\": {\n" +
|
||||
" \"get\": {\n" +
|
||||
" \"summary\": \"Actuator 헬스 체크\",\n" +
|
||||
" \"description\": \"Spring Boot Actuator 헬스 체크 엔드포인트\",\n" +
|
||||
" \"responses\": {\n" +
|
||||
" \"200\": {\n" +
|
||||
" \"description\": \"헬스 체크 결과\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" },\n" +
|
||||
" \"components\": {\n" +
|
||||
" \"securitySchemes\": {\n" +
|
||||
" \"bearerAuth\": {\n" +
|
||||
" \"type\": \"http\",\n" +
|
||||
" \"scheme\": \"bearer\",\n" +
|
||||
" \"bearerFormat\": \"JWT\",\n" +
|
||||
" \"description\": \"JWT 토큰을 Authorization 헤더에 포함시켜 주세요.\\nFormat: Authorization: Bearer {token}\"\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* 기본 API 문서 생성
|
||||
*
|
||||
* 서비스에 접근할 수 없을 때 반환할 기본 문서를 생성합니다.
|
||||
*
|
||||
* @param apiDocsUrl API 문서 URL
|
||||
* @return 기본 API 문서 JSON
|
||||
*/
|
||||
private String getDefaultApiDoc(String apiDocsUrl) {
|
||||
String serviceName = extractServiceName(apiDocsUrl);
|
||||
return "{\n" +
|
||||
" \"openapi\": \"3.0.1\",\n" +
|
||||
" \"info\": {\n" +
|
||||
" \"title\": \"" + serviceName + " API\",\n" +
|
||||
" \"version\": \"1.0.0\",\n" +
|
||||
" \"description\": \"" + serviceName + " 마이크로서비스 API 문서\\n\\n" +
|
||||
"서비스가 시작되지 않았거나 연결할 수 없습니다.\"\n" +
|
||||
" },\n" +
|
||||
" \"paths\": {},\n" +
|
||||
" \"components\": {}\n" +
|
||||
"}";
|
||||
}
|
||||
|
||||
/**
|
||||
* URL에서 서비스명 추출
|
||||
*
|
||||
* @param apiDocsUrl API 문서 URL
|
||||
* @return 서비스명
|
||||
*/
|
||||
private String extractServiceName(String apiDocsUrl) {
|
||||
if (apiDocsUrl.contains("8081")) return "User Service";
|
||||
if (apiDocsUrl.contains("8082")) return "Bill Service";
|
||||
if (apiDocsUrl.contains("8083")) return "Product Service";
|
||||
if (apiDocsUrl.contains("8084")) return "KOS Mock Service";
|
||||
return "Unknown Service";
|
||||
}
|
||||
}
|
||||
@ -3,12 +3,12 @@ package com.unicorn.phonebill.gateway.config;
|
||||
import org.springframework.boot.web.codec.CodecCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
|
||||
/**
|
||||
* WebFlux 설정
|
||||
*
|
||||
* Spring Cloud Gateway에서 필요한 WebFlux 관련 빈들을 정의합니다.
|
||||
* Spring Cloud Gateway에서 필요한 WebFlux 관련 커스터마이징을 제공합니다.
|
||||
* ServerCodecConfigurer는 Spring Boot가 자동으로 제공합니다.
|
||||
*
|
||||
* @author 이개발(백엔더)
|
||||
* @version 1.0.0
|
||||
@ -18,21 +18,9 @@ import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
public class WebFluxConfig {
|
||||
|
||||
/**
|
||||
* ServerCodecConfigurer 빈 정의
|
||||
* CodecCustomizer 빈 정의
|
||||
*
|
||||
* Spring Cloud Gateway가 요구하는 ServerCodecConfigurer를 직접 정의합니다.
|
||||
*
|
||||
* @return ServerCodecConfigurer
|
||||
*/
|
||||
@Bean
|
||||
public ServerCodecConfigurer serverCodecConfigurer() {
|
||||
return ServerCodecConfigurer.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* CodecCustomizer 빈 정의 (선택적)
|
||||
*
|
||||
* 필요한 경우 코덱을 커스터마이징할 수 있습니다.
|
||||
* 코덱 설정을 커스터마이징합니다.
|
||||
*
|
||||
* @return CodecCustomizer
|
||||
*/
|
||||
|
||||
@ -1,16 +1,17 @@
|
||||
package com.unicorn.phonebill.gateway.controller;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.ReactiveRedisTemplate;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* API Gateway 헬스체크 컨트롤러
|
||||
@ -19,10 +20,11 @@ import java.util.Map;
|
||||
*
|
||||
* 주요 기능:
|
||||
* - Gateway 자체 상태 확인
|
||||
* - Redis 연결 상태 확인
|
||||
* - 각 마이크로서비스 연결 상태 확인
|
||||
* - 전체 시스템 상태 요약
|
||||
*
|
||||
* Note: Redis는 API Gateway에서 사용하지 않으므로 Redis health check 제거
|
||||
*
|
||||
* @author 이개발(백엔더)
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-08
|
||||
@ -30,11 +32,21 @@ import java.util.Map;
|
||||
@RestController
|
||||
public class HealthController {
|
||||
|
||||
private final ReactiveRedisTemplate<String, Object> redisTemplate;
|
||||
private final WebClient webClient;
|
||||
|
||||
@Autowired
|
||||
public HealthController(ReactiveRedisTemplate<String, Object> redisTemplate) {
|
||||
this.redisTemplate = redisTemplate;
|
||||
@Value("${services.auth-service.url:http://localhost:8081}")
|
||||
private String authServiceUrl;
|
||||
|
||||
@Value("${services.bill-service.url:http://localhost:8082}")
|
||||
private String billServiceUrl;
|
||||
|
||||
@Value("${services.product-service.url:http://localhost:8083}")
|
||||
private String productServiceUrl;
|
||||
|
||||
public HealthController() {
|
||||
this.webClient = WebClient.builder()
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024)) // 1MB
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,16 +83,13 @@ public class HealthController {
|
||||
public Mono<ResponseEntity<Map<String, Object>>> detailedHealth() {
|
||||
return Mono.zip(
|
||||
checkGatewayHealth(),
|
||||
checkRedisHealth(),
|
||||
checkDownstreamServices()
|
||||
).map(tuple -> {
|
||||
Map<String, Object> gatewayHealth = tuple.getT1();
|
||||
Map<String, Object> redisHealth = tuple.getT2();
|
||||
Map<String, Object> servicesHealth = tuple.getT3();
|
||||
Map<String, Object> servicesHealth = tuple.getT2();
|
||||
|
||||
boolean allHealthy =
|
||||
"UP".equals(gatewayHealth.get("status")) &&
|
||||
"UP".equals(redisHealth.get("status")) &&
|
||||
"UP".equals(servicesHealth.get("status"));
|
||||
|
||||
Map<String, Object> response = Map.of(
|
||||
@ -88,7 +97,6 @@ public class HealthController {
|
||||
"timestamp", Instant.now().toString(),
|
||||
"components", Map.of(
|
||||
"gateway", gatewayHealth,
|
||||
"redis", redisHealth,
|
||||
"services", servicesHealth
|
||||
)
|
||||
);
|
||||
@ -126,16 +134,9 @@ public class HealthController {
|
||||
* @return 시스템 상태
|
||||
*/
|
||||
private Mono<Map<String, Object>> checkSystemHealth() {
|
||||
return Mono.zip(
|
||||
checkGatewayHealth(),
|
||||
checkRedisHealth()
|
||||
).map(tuple -> {
|
||||
Map<String, Object> gatewayHealth = tuple.getT1();
|
||||
Map<String, Object> redisHealth = tuple.getT2();
|
||||
|
||||
boolean allHealthy =
|
||||
"UP".equals(gatewayHealth.get("status")) &&
|
||||
"UP".equals(redisHealth.get("status"));
|
||||
return checkGatewayHealth()
|
||||
.map(gatewayHealth -> {
|
||||
boolean allHealthy = "UP".equals(gatewayHealth.get("status"));
|
||||
|
||||
return Map.<String, Object>of(
|
||||
"status", allHealthy ? "UP" : "DOWN",
|
||||
@ -173,27 +174,6 @@ public class HealthController {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis 연결 상태 점검
|
||||
*
|
||||
* @return Redis 상태
|
||||
*/
|
||||
private Mono<Map<String, Object>> checkRedisHealth() {
|
||||
return redisTemplate.hasKey("health:check")
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.map(result -> Map.<String, Object>of(
|
||||
"status", "UP",
|
||||
"connection", "OK",
|
||||
"response_time", "< 3s",
|
||||
"timestamp", Instant.now().toString()
|
||||
))
|
||||
.onErrorReturn(Map.<String, Object>of(
|
||||
"status", "DOWN",
|
||||
"connection", "FAILED",
|
||||
"error", "Connection timeout or error",
|
||||
"timestamp", Instant.now().toString()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 다운스트림 서비스 상태 점검
|
||||
@ -201,21 +181,90 @@ public class HealthController {
|
||||
* @return 서비스 상태
|
||||
*/
|
||||
private Mono<Map<String, Object>> checkDownstreamServices() {
|
||||
// 실제 구현에서는 Circuit Breaker 상태를 확인하거나
|
||||
// 각 서비스에 대한 간단한 health check를 수행할 수 있습니다.
|
||||
return Mono.fromCallable(() -> Map.<String, Object>of(
|
||||
"status", "UP",
|
||||
"services", Map.<String, Object>of(
|
||||
"auth-service", "UNKNOWN",
|
||||
"bill-service", "UNKNOWN",
|
||||
"product-service", "UNKNOWN",
|
||||
"kos-mock-service", "UNKNOWN"
|
||||
),
|
||||
"note", "Service health checks not implemented yet",
|
||||
// 모든 서비스의 health check를 병렬로 수행
|
||||
Mono<Map<String, Object>> authCheck = checkServiceHealth("auth-service", authServiceUrl);
|
||||
Mono<Map<String, Object>> billCheck = checkServiceHealth("bill-service", billServiceUrl);
|
||||
Mono<Map<String, Object>> productCheck = checkServiceHealth("product-service", productServiceUrl);
|
||||
|
||||
return Mono.zip(authCheck, billCheck, productCheck)
|
||||
.map(tuple -> {
|
||||
Map<String, Object> authResult = tuple.getT1();
|
||||
Map<String, Object> billResult = tuple.getT2();
|
||||
Map<String, Object> productResult = tuple.getT3();
|
||||
|
||||
// 전체 서비스 상태 계산
|
||||
boolean anyServiceDown =
|
||||
"DOWN".equals(authResult.get("status")) ||
|
||||
"DOWN".equals(billResult.get("status")) ||
|
||||
"DOWN".equals(productResult.get("status"));
|
||||
|
||||
Map<String, Object> services = new ConcurrentHashMap<>();
|
||||
services.put("auth-service", authResult);
|
||||
services.put("bill-service", billResult);
|
||||
services.put("product-service", productResult);
|
||||
|
||||
return Map.<String, Object>of(
|
||||
"status", anyServiceDown ? "DEGRADED" : "UP",
|
||||
"services", services,
|
||||
"timestamp", Instant.now().toString(),
|
||||
"summary", String.format("Total services: 3, Up: %d, Down: %d",
|
||||
countServicesByStatus(services, "UP"),
|
||||
countServicesByStatus(services, "DOWN"))
|
||||
);
|
||||
})
|
||||
.onErrorReturn(Map.of(
|
||||
"status", "DOWN",
|
||||
"error", "Failed to check downstream services",
|
||||
"timestamp", Instant.now().toString()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 개별 서비스 health check
|
||||
*
|
||||
* @param serviceName 서비스 이름
|
||||
* @param serviceUrl 서비스 URL
|
||||
* @return 서비스 상태
|
||||
*/
|
||||
private Mono<Map<String, Object>> checkServiceHealth(String serviceName, String serviceUrl) {
|
||||
return webClient.get()
|
||||
.uri(serviceUrl + "/actuator/health")
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.timeout(Duration.ofSeconds(3))
|
||||
.map(response -> {
|
||||
String status = (String) response.getOrDefault("status", "UNKNOWN");
|
||||
return Map.<String, Object>of(
|
||||
"status", "UP".equals(status) ? "UP" : "DOWN",
|
||||
"url", serviceUrl,
|
||||
"response_time", "< 3s",
|
||||
"details", response,
|
||||
"timestamp", Instant.now().toString()
|
||||
);
|
||||
})
|
||||
.onErrorReturn(Map.of(
|
||||
"status", "DOWN",
|
||||
"url", serviceUrl,
|
||||
"error", "Connection failed or timeout",
|
||||
"timestamp", Instant.now().toString()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스 상태별 개수 계산
|
||||
*
|
||||
* @param services 서비스 맵
|
||||
* @param status 확인할 상태
|
||||
* @return 해당 상태의 서비스 개수
|
||||
*/
|
||||
private long countServicesByStatus(Map<String, Object> services, String status) {
|
||||
return services.values().stream()
|
||||
.filter(service -> service instanceof Map)
|
||||
.map(service -> (Map<String, Object>) service)
|
||||
.filter(service -> status.equals(service.get("status")))
|
||||
.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 애플리케이션 업타임 계산
|
||||
*
|
||||
|
||||
@ -8,6 +8,9 @@ server:
|
||||
idle-timeout: ${SERVER_NETTY_IDLE_TIMEOUT:60s}
|
||||
http2:
|
||||
enabled: true
|
||||
# HTTP 헤더 크기 제한 설정
|
||||
max-http-header-size: 64KB
|
||||
max-http-request-header-size: 64KB
|
||||
|
||||
spring:
|
||||
application:
|
||||
@ -19,6 +22,14 @@ spring:
|
||||
# Spring Cloud Gateway 설정
|
||||
cloud:
|
||||
gateway:
|
||||
# HTTP 관련 설정
|
||||
httpclient:
|
||||
pool:
|
||||
max-connections: 1000
|
||||
max-idle-time: 30s
|
||||
response-timeout: 60s
|
||||
connect-timeout: 5000
|
||||
|
||||
default-filters:
|
||||
- name: AddRequestHeader
|
||||
args:
|
||||
@ -50,7 +61,6 @@ spring:
|
||||
locator:
|
||||
enabled: false
|
||||
|
||||
|
||||
# JSON 설정
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
@ -66,19 +76,19 @@ cors:
|
||||
# JWT 토큰 설정
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:1800}
|
||||
refresh-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:86400}
|
||||
access-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:180000}
|
||||
refresh-token-validity: ${JWT_ACCESS_TOKEN_VALIDITY:86400000}
|
||||
|
||||
# 서비스 URL 설정
|
||||
services:
|
||||
auth-service:
|
||||
url: ${AUTH_SERVICE_URL:http://localhost:8081}
|
||||
user-service:
|
||||
url: ${USER_SERVICE_URL:http://localhost:8081}
|
||||
bill-service:
|
||||
url: ${BILL_SERVICE_URL:http://localhost:8082}
|
||||
product-service:
|
||||
url: ${PRODUCT_SERVICE_URL:http://localhost:8083}
|
||||
kos-mock-service:
|
||||
url: ${KOS_MOCK_SERVICE_URL:http://localhost:8084}
|
||||
kos-mock:
|
||||
url: ${KOS_MOCK_URL:http://localhost:8084}
|
||||
|
||||
# Circuit Breaker 설정
|
||||
resilience4j:
|
||||
@ -132,7 +142,7 @@ management:
|
||||
enabled: true
|
||||
health:
|
||||
redis:
|
||||
enabled: true
|
||||
enabled: false
|
||||
circuitbreakers:
|
||||
enabled: true
|
||||
info:
|
||||
@ -164,13 +174,27 @@ springdoc:
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
disable-swagger-default-url: true
|
||||
# HTTP 431 오류 방지를 위한 설정
|
||||
config-url: /v3/api-docs/swagger-config
|
||||
use-root-path: true
|
||||
# 큰 헤더 처리를 위한 설정
|
||||
csrf:
|
||||
enabled: false
|
||||
# 서비스별 URL 등록
|
||||
urls:
|
||||
- name: Auth Service
|
||||
url: /v3/api-docs/auth
|
||||
- name: Bill Service
|
||||
url: /v3/api-docs/bills
|
||||
- name: Product Service
|
||||
url: /v3/api-docs/products
|
||||
- name: "Gateway API"
|
||||
url: "/v3/api-docs/gateway"
|
||||
- name: "User Service"
|
||||
url: "/v3/api-docs/user"
|
||||
- name: "Bill Service"
|
||||
url: "/v3/api-docs/bill"
|
||||
- name: "Product Service"
|
||||
url: "/v3/api-docs/product"
|
||||
- name: "KOS Mock Service"
|
||||
url: "/v3/api-docs/kos"
|
||||
use-management-port: false
|
||||
show-actuator: false
|
||||
|
||||
# 애플리케이션 정보
|
||||
info:
|
||||
|
||||
@ -7,6 +7,7 @@ import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@ -44,6 +45,15 @@ public class SwaggerConfig {
|
||||
.addSecuritySchemes("bearerAuth", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi billApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("bill")
|
||||
.displayName("Bill Service")
|
||||
.pathsToMatch("/bills/**", "/bill/**", "/api/v1/bills/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
return new Info()
|
||||
.title("Bill Service API")
|
||||
|
||||
@ -70,6 +70,9 @@ spring:
|
||||
# 서버 설정
|
||||
server:
|
||||
port: ${SERVER_PORT:8082}
|
||||
# HTTP 헤더 크기 제한 설정
|
||||
max-http-header-size: 64KB
|
||||
max-http-request-header-size: 64KB
|
||||
servlet:
|
||||
encoding:
|
||||
charset: UTF-8
|
||||
|
||||
@ -107,6 +107,7 @@ configure(subprojects.findAll { it.name == 'api-gateway' }) {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
|
||||
|
||||
// Actuator for health checks and monitoring
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
@ -120,6 +121,7 @@ configure(subprojects.findAll { it.name == 'api-gateway' }) {
|
||||
|
||||
// Testing (WebFlux specific)
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
|
||||
// Configuration Processor
|
||||
|
||||
@ -7,6 +7,7 @@ import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@ -39,9 +40,18 @@ public class SwaggerConfig {
|
||||
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("8084")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
.addSecuritySchemes("bearerAuth", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi kosApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("kos")
|
||||
.displayName("KOS Mock Service")
|
||||
.pathsToMatch("/api/v1/kos/**", "/api/v1/mock-datas/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
|
||||
@ -27,6 +27,9 @@ spring:
|
||||
|
||||
server:
|
||||
port: ${SERVER_PORT:8084}
|
||||
# HTTP 헤더 크기 제한 설정
|
||||
max-http-header-size: 64KB
|
||||
max-http-request-header-size: 64KB
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
@ -58,7 +61,7 @@ logging:
|
||||
# Swagger/OpenAPI
|
||||
springdoc:
|
||||
api-docs:
|
||||
path: /api-docs
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html
|
||||
tags-sorter: alpha
|
||||
|
||||
@ -7,6 +7,7 @@ import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@ -44,6 +45,15 @@ public class SwaggerConfig {
|
||||
.addSecuritySchemes("bearerAuth", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi productApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("product")
|
||||
.displayName("Product Service")
|
||||
.pathsToMatch("/products/**", "/product/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
return new Info()
|
||||
.title("Product Service API")
|
||||
|
||||
@ -54,6 +54,9 @@ spring:
|
||||
# Server 개발 설정
|
||||
server:
|
||||
port: ${SERVER_PORT:8083}
|
||||
# HTTP 헤더 크기 제한 설정
|
||||
max-http-header-size: 64KB
|
||||
max-http-request-header-size: 64KB
|
||||
error:
|
||||
include-stacktrace: always
|
||||
include-message: always
|
||||
@ -117,7 +120,7 @@ management:
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true
|
||||
path: /api-docs
|
||||
path: /v3/api-docs
|
||||
swagger-ui:
|
||||
enabled: true
|
||||
path: /swagger-ui.html
|
||||
|
||||
@ -7,6 +7,7 @@ import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import io.swagger.v3.oas.models.servers.Server;
|
||||
import org.springdoc.core.models.GroupedOpenApi;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@ -39,9 +40,18 @@ public class SwaggerConfig {
|
||||
.addServerVariable("port", new io.swagger.v3.oas.models.servers.ServerVariable()
|
||||
._default("8081")
|
||||
.description("Server port"))))
|
||||
.addSecurityItem(new SecurityRequirement().addList("Bearer Authentication"))
|
||||
.addSecurityItem(new SecurityRequirement().addList("bearerAuth"))
|
||||
.components(new Components()
|
||||
.addSecuritySchemes("Bearer Authentication", createAPIKeyScheme()));
|
||||
.addSecuritySchemes("bearerAuth", createAPIKeyScheme()));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GroupedOpenApi userApi() {
|
||||
return GroupedOpenApi.builder()
|
||||
.group("user")
|
||||
.displayName("User Service")
|
||||
.pathsToMatch("/api/v1/auth/**", "/api/v1/users/**")
|
||||
.build();
|
||||
}
|
||||
|
||||
private Info apiInfo() {
|
||||
|
||||
@ -1,3 +1,9 @@
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
# HTTP 헤더 크기 제한 설정
|
||||
max-http-header-size: 64KB
|
||||
max-http-request-header-size: 64KB
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: user-service
|
||||
@ -55,10 +61,6 @@ spring:
|
||||
time-to-live: 1800000 # 30분
|
||||
cache-null-values: false
|
||||
|
||||
# 서버 설정
|
||||
server:
|
||||
port: ${SERVER_PORT:8081}
|
||||
|
||||
# CORS
|
||||
cors:
|
||||
allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user