API Gateway Swagger 통합 기능 구현 완료

주요 변경사항:
- Gateway 라우팅 경로 통일화 (/api/{service}/**)
  * user-service: /api/auth/**
  * bill-service: /api/bills/** (내부적으로 /api/v1/bills/**로 변환)
  * product-service: /api/products/** (내부적으로 /products/**로 변환)
  * kos-mock: /api/kos/** 추가

- OpenAPI 서버 정보 동적 수정
  * 각 서비스의 OpenAPI JSON에 Gateway 경로 정보 주입
  * "Try it out" 기능이 Gateway를 통해 정상 동작하도록 개선

- Swagger UI 설정 개선
  * 서비스별 이모지와 한글 설명 추가
  * 표시 순서 최적화 (User → Bill → Product → KOS → Gateway)

- 서비스별 GroupedOpenApi 빈 추가
  * 각 서비스별 상세 정보와 기능 설명 포함
  * 일관된 API 문서 구조 제공

이제 API Gateway의 Swagger UI에서 모든 마이크로서비스 API가 통합되어 표시되며,
실제 테스트도 가능합니다.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
hiondal 2025-09-10 10:48:59 +09:00
parent 2a719048f8
commit 2df9b7d14f
3 changed files with 204 additions and 15 deletions

View File

@ -18,10 +18,10 @@ import java.util.Arrays;
* 마이크로서비스별 라우팅 규칙과 CORS 정책을 정의합니다. * 마이크로서비스별 라우팅 규칙과 CORS 정책을 정의합니다.
* *
* 라우팅 구성: * 라우팅 구성:
* - /auth/** -> auth-service (인증 서비스) * - /api/auth/** -> user-service (인증 서비스)
* - /bills/** -> bill-service (요금조회 서비스) * - /api/bills/** -> bill-service (요금조회 서비스)
* - /products/** -> product-service (상품변경 서비스) * - /api/products/** -> product-service (상품변경 서비스)
* - /kos/** -> kos-mock (KOS 목업 서비스) * - /api/kos/** -> kos-mock (KOS 목업 서비스)
* *
* @author 이개발(백엔더) * @author 이개발(백엔더)
* @version 1.0.0 * @version 1.0.0
@ -75,8 +75,9 @@ public class GatewayConfig {
// Bill-Inquiry Service 라우팅 (인증 필요) // Bill-Inquiry Service 라우팅 (인증 필요)
.route("bill-service", r -> r .route("bill-service", r -> r
.path("/api/v1/bills/**") .path("/api/bills/**")
.filters(f -> f .filters(f -> f
.rewritePath("/api/bills/(?<segment>.*)", "/api/v1/bills/${segment}")
.filter(jwtAuthFilter.apply(new JwtAuthenticationGatewayFilterFactory.Config())) .filter(jwtAuthFilter.apply(new JwtAuthenticationGatewayFilterFactory.Config()))
.circuitBreaker(cb -> cb .circuitBreaker(cb -> cb
.setName("bill-service-cb") .setName("bill-service-cb")
@ -89,8 +90,9 @@ public class GatewayConfig {
// Product-Change Service 라우팅 (인증 필요) // Product-Change Service 라우팅 (인증 필요)
.route("product-service", r -> r .route("product-service", r -> r
.path("/products/**") .path("/api/products/**")
.filters(f -> f .filters(f -> f
.rewritePath("/api/products/(?<segment>.*)", "/products/${segment}")
.filter(jwtAuthFilter.apply(new JwtAuthenticationGatewayFilterFactory.Config())) .filter(jwtAuthFilter.apply(new JwtAuthenticationGatewayFilterFactory.Config()))
.circuitBreaker(cb -> cb .circuitBreaker(cb -> cb
.setName("product-service-cb") .setName("product-service-cb")
@ -100,6 +102,20 @@ public class GatewayConfig {
.setBackoff(java.time.Duration.ofSeconds(2), java.time.Duration.ofSeconds(10), 2, true)) .setBackoff(java.time.Duration.ofSeconds(2), java.time.Duration.ofSeconds(10), 2, true))
) )
.uri("lb://product-service")) .uri("lb://product-service"))
// KOS Mock Service 라우팅 (인증 불필요 - 목업용)
.route("kos-mock-service", r -> r
.path("/api/kos/**")
.filters(f -> f
.rewritePath("/api/kos/(?<segment>.*)", "/kos/${segment}")
.circuitBreaker(cb -> cb
.setName("kos-mock-cb")
.setFallbackUri("forward:/fallback/kos"))
.retry(retry -> retry
.setRetries(2)
.setBackoff(java.time.Duration.ofSeconds(1), java.time.Duration.ofSeconds(5), 2, true))
)
.uri("lb://kos-mock"))
// 주의: Gateway 자체 엔드포인트는 라우팅하지 않음 // 주의: Gateway 자체 엔드포인트는 라우팅하지 않음
// Health Check와 Swagger UI는 Spring Boot에서 직접 제공 // Health Check와 Swagger UI는 Spring Boot에서 직접 제공

View File

@ -78,14 +78,19 @@ public class SwaggerConfig {
public GroupedOpenApi gatewayApi() { public GroupedOpenApi gatewayApi() {
return GroupedOpenApi.builder() return GroupedOpenApi.builder()
.group("gateway") .group("gateway")
.displayName("API Gateway") .displayName("🌐 API Gateway")
.pathsToMatch("/health/**", "/actuator/**") .pathsToMatch("/health/**", "/actuator/**")
.addOpenApiCustomizer(openApi -> { .addOpenApiCustomizer(openApi -> {
openApi.info(new io.swagger.v3.oas.models.info.Info() openApi.info(new io.swagger.v3.oas.models.info.Info()
.title("PhoneBill API Gateway") .title("PhoneBill API Gateway")
.version("1.0.0") .version("1.0.0")
.description("통신요금 관리 서비스 API Gateway\n\n" + .description("통신요금 관리 서비스 API Gateway\n\n" +
"이 문서는 API Gateway의 헬스체크 및 관리 기능을 설명합니다.") "이 문서는 API Gateway의 헬스체크 및 관리 기능을 설명합니다.\n\n" +
"**주요 기능:**\n" +
"- 마이크로서비스 라우팅\n" +
"- JWT 인증/인가\n" +
"- Circuit Breaker\n" +
"- CORS 처리")
); );
// JWT 보안 스키마 추가 // JWT 보안 스키마 추가
@ -106,6 +111,106 @@ public class SwaggerConfig {
}) })
.build(); .build();
} }
/**
* User Service OpenAPI 그룹 정의
*
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi userServiceApi() {
return GroupedOpenApi.builder()
.group("user-service")
.displayName("📱 User Service")
.pathsToMatch("/api/auth/**")
.addOpenApiCustomizer(openApi -> {
openApi.info(new io.swagger.v3.oas.models.info.Info()
.title("User Service API")
.version("1.0.0")
.description("사용자 인증 및 관리 서비스\n\n" +
"**주요 기능:**\n" +
"- 사용자 로그인/로그아웃\n" +
"- JWT 토큰 발급/갱신\n" +
"- 사용자 정보 관리")
);
})
.build();
}
/**
* Bill Service OpenAPI 그룹 정의
*
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi billServiceApi() {
return GroupedOpenApi.builder()
.group("bill-service")
.displayName("💰 Bill Service")
.pathsToMatch("/api/bills/**")
.addOpenApiCustomizer(openApi -> {
openApi.info(new io.swagger.v3.oas.models.info.Info()
.title("Bill Inquiry Service API")
.version("1.0.0")
.description("통신요금 조회 서비스\n\n" +
"**주요 기능:**\n" +
"- 월별 요금 조회\n" +
"- 요금 상세 내역\n" +
"- 조회 이력 관리")
);
})
.build();
}
/**
* Product Service OpenAPI 그룹 정의
*
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi productServiceApi() {
return GroupedOpenApi.builder()
.group("product-service")
.displayName("📦 Product Service")
.pathsToMatch("/api/products/**")
.addOpenApiCustomizer(openApi -> {
openApi.info(new io.swagger.v3.oas.models.info.Info()
.title("Product Change Service API")
.version("1.0.0")
.description("통신상품 변경 서비스\n\n" +
"**주요 기능:**\n" +
"- 상품 목록 조회\n" +
"- 상품 변경 신청\n" +
"- 변경 이력 관리")
);
})
.build();
}
/**
* KOS Mock Service OpenAPI 그룹 정의
*
* @return GroupedOpenApi
*/
@Bean
public GroupedOpenApi kosMockServiceApi() {
return GroupedOpenApi.builder()
.group("kos-mock")
.displayName("🔧 KOS Mock Service")
.pathsToMatch("/api/kos/**")
.addOpenApiCustomizer(openApi -> {
openApi.info(new io.swagger.v3.oas.models.info.Info()
.title("KOS Mock Service API")
.version("1.0.0")
.description("KOS 외부 연동 목업 서비스\n\n" +
"**주요 기능:**\n" +
"- 요금 조회 목업\n" +
"- 상품 변경 목업\n" +
"- 테스트 데이터 제공")
);
})
.build();
}
/** /**
* Swagger UI 리다이렉트 라우터 * Swagger UI 리다이렉트 라우터
@ -153,6 +258,7 @@ public class SwaggerConfig {
* API 문서 프록시 * API 문서 프록시
* *
* 마이크로서비스의 OpenAPI 문서를 프록시하여 제공합니다. * 마이크로서비스의 OpenAPI 문서를 프록시하여 제공합니다.
* Gateway 경로로 서버 정보를 수정하여 반환합니다.
* *
* @param apiDocsUrl API 문서 URL * @param apiDocsUrl API 문서 URL
* @return ServerResponse * @return ServerResponse
@ -164,6 +270,7 @@ public class SwaggerConfig {
.onStatus(status -> status.isError(), clientResponse -> .onStatus(status -> status.isError(), clientResponse ->
Mono.error(new RuntimeException("Service unavailable"))) Mono.error(new RuntimeException("Service unavailable")))
.bodyToMono(String.class) .bodyToMono(String.class)
.map(this::modifyOpenApiServers)
.onErrorReturn(getDefaultApiDoc(apiDocsUrl)) .onErrorReturn(getDefaultApiDoc(apiDocsUrl))
.flatMap(body -> .flatMap(body ->
ServerResponse.ok() ServerResponse.ok()
@ -172,6 +279,67 @@ public class SwaggerConfig {
); );
} }
/**
* OpenAPI 문서의 서버 정보를 Gateway 경로로 수정
*
* @param openApiJson 원본 OpenAPI JSON
* @return 수정된 OpenAPI JSON
*/
private String modifyOpenApiServers(String openApiJson) {
try {
// JSON 파싱을 위한 간단한 문자열 치환
// 실제 프로덕션에서는 Jackson ObjectMapper 사용 권장
String modified = openApiJson;
// 서버 정보를 Gateway 기반으로 수정
if (openApiJson.contains("user-service") || openApiJson.contains("8081")) {
modified = addGatewayServerInfo(modified, "/api/auth", "User Service");
} else if (openApiJson.contains("bill-service") || openApiJson.contains("8082")) {
modified = addGatewayServerInfo(modified, "/api/bills", "Bill Service");
} else if (openApiJson.contains("product-service") || openApiJson.contains("8083")) {
modified = addGatewayServerInfo(modified, "/api/products", "Product Service");
} else if (openApiJson.contains("kos-mock") || openApiJson.contains("8084")) {
modified = addGatewayServerInfo(modified, "/api/kos", "KOS Mock Service");
}
return modified;
} catch (Exception e) {
// JSON 수정 실패 원본 반환
return openApiJson;
}
}
/**
* OpenAPI JSON에 Gateway 서버 정보 추가
*
* @param openApiJson 원본 OpenAPI JSON
* @param basePath Gateway 기반 경로
* @param serviceName 서비스명
* @return 수정된 OpenAPI JSON
*/
private String addGatewayServerInfo(String openApiJson, String basePath, String serviceName) {
// servers 섹션을 Gateway 정보로 교체
String serverInfo = "\"servers\": [" +
" {" +
" \"url\": \"" + basePath + "\"," +
" \"description\": \"" + serviceName + " via API Gateway\"" +
" }" +
" ],";
// 기존 servers 정보가 있으면 교체, 없으면 info 다음에 추가
if (openApiJson.contains("\"servers\"")) {
return openApiJson.replaceFirst(
"\"servers\":\\s*\\[[^\\]]*\\],?",
serverInfo
);
} else {
return openApiJson.replaceFirst(
"(\"info\":\\s*\\{[^}]*\\},?)",
"$1\n " + serverInfo
);
}
}
/** /**
* Gateway API 문서 생성 * Gateway API 문서 생성
* *

View File

@ -181,18 +181,23 @@ springdoc:
# 큰 헤더 처리를 위한 설정 # 큰 헤더 처리를 위한 설정
csrf: csrf:
enabled: false enabled: false
# 서비스별 URL 등록 # 서비스별 URL 등록 (Gateway 통합 순서)
urls: urls:
- name: "Gateway API" - name: "📱 User Service (인증)"
url: "/v3/api-docs/gateway"
- name: "User Service"
url: "/v3/api-docs/user" url: "/v3/api-docs/user"
- name: "Bill Service" display-name: "User Service API"
- name: "💰 Bill Service (요금조회)"
url: "/v3/api-docs/bill" url: "/v3/api-docs/bill"
- name: "Product Service" display-name: "Bill Inquiry API"
- name: "📦 Product Service (상품변경)"
url: "/v3/api-docs/product" url: "/v3/api-docs/product"
- name: "KOS Mock Service" display-name: "Product Change API"
- name: "🔧 KOS Mock Service (외부연동)"
url: "/v3/api-docs/kos" url: "/v3/api-docs/kos"
display-name: "KOS Mock API"
- name: "🌐 Gateway API (게이트웨이)"
url: "/v3/api-docs/gateway"
display-name: "API Gateway"
use-management-port: false use-management-port: false
show-actuator: false show-actuator: false