작성 배경
프로젝트지의 개발 환경에선Swagger 라이브러리를 고객사의 서비스에 직접 적용하기 어려운 제약이 있었습니다. 일반적인 Spring 기반 서비스라면 Springfox 또는 springdoc-openapi를 통해 Controller와 DTO 기반 API 명세를 자동 생성할 수 있지만, 고객사 환경에서는 서비스별 의존성 추가와 런타임 연동이 자유롭지 않았습니다.
그럼에도 고객은 API 명세 관리를 위해 Swagger를 사용하고 싶어 하셨습니다. 특히 현재 배포된 API 목록과 상세 request, response 구조를 Swagger UI에서 확인하고 싶다는 니즈가 있었고, 무엇보다 여러 개발팀에서 각자 개발한 서비스들의 API 수동 문서 작성과 최신화 지연으로 인한 VOC도 발생하고 있었습니다.
PoC의 출발점은 이 제약을 유지하면서도 Swagger 기반 문서화 경험을 제공할 수 있는지 검증하는 것이었습니다. 즉, 각 서비스에 Swagger 라이브러리를 직접 붙이지 않고도 중앙 Swagger 서버를 통해 배포 API 명세를 확인할 수 있는 구조를 만드는 것이 목표였습니다.
문제 정의
이 프로젝트의 핵심은 고객사의 전체 개발팀이 하나의 Swagger 서버에 접속하여 현재 배포된 API 명세를 확인할 수 있도록 만드는 것이었습니다. 따라서 개별 서비스 내부에서 Swagger 문서를 보여주는 방식보다, 중앙 Swagger 서버가 여러 API 명세를 수집하고 통합 제공하는 방식을 계획했습니다.
-
API 명세는 배포 시점에 수집하여 별도로 관리하는 중앙 Swagger 서버로 실시간 반영되는 것을 목표로 하였습니다.
-
명세 파일은 Swagger UI에서 바로 읽을 수 있는 YAML 형식이어야 했습니다.
-
향후 각 개발팀이 제가 제공하는Custom Annotation만 추가해도 명세가 자동 생성되는 구조로 확장 가능해야 했습니다.
PoC에서는 이 목표를 두 단계로 나누어 검증했습니다. 먼저 API들로부터 수집한 Req / Res 정보를 Swagger YAML 형식으로 자동 생성하여 별도 서버에서 통합 제공하고, 이후 Custom Annotation 기반으로 API와 VO를 분석하여 YAML을 생성하는 라이브러리화 방향을 검증했습니다.
PoC 구성 개요
서비스는 크게 세 영역으로 나눴습니다. swagger-apis는 자동 생성된 API 명세 YAML 파일을 보관하는 외부 저장소 역할을 하고, swagger-demo는 이 YAML 파일들을 읽어 Swagger UI에 노출하는 Spring Boot 서버입니다. swagger-file-generator는 Java 객체를 기반으로 Swagger YAML 파일을 생성할 수 있는지 확인하며 라이브러리화하여 각 개발팀에 제공하기 위한 프로젝트입니다.
|
폴더 |
역할 |
설명 |
|---|---|---|
|
swagger-apis |
명세 파일 저장소 |
API별 swagger.yaml 파일을 보관하는 외부 디렉터리입니다. |
|
swagger-demo |
Swagger 서버 |
Spring Boot와 Springfox를 이용해 여러 YAML을 Swagger UI에 등록합니다. |
|
swagger-file-generator |
YAML 생성 실험 |
Java 객체 모델을 YAML 문자열로 변환하고 파일로 저장합니다. |
이 구조의 장점은 Swagger UI 제공 책임과 API 명세 생성 책임을 분리할 수 있다는 점입니다. 서비스 코드는 명세 생성에 집중하고, 중앙 Swagger 서버는 YAML 파일을 기준으로 UI를 제공합니다.
중앙 Swagger 서버 구성
swagger-demo 프로젝트는 Spring Boot 기반의 Swagger 서버입니다. 이 서버는 자체 API를 문서화하는 것이 아니라, 외부, 즉 각 개발팀에서 제공한 여러 Swagger YAML 파일을 Swagger UI에서 선택 가능한 리소스로 등록합니다. 고객사 내의 각 개발팀들은 이 서버의 Swagger UI에 접속하여 현재 배포된 API 명세를 확인하는 방식으로 사용할 수 있습니다.
핵심 구현은 SwaggerResourcesProvider를 재정의한 부분입니다. Springfox가 기본으로 제공하는 Swagger 리소스 목록을 그대로 사용하지 않고, 외부 디렉터리에서 YAML 파일을 읽어 직접 SwaggerResource 목록을 구성합니다.
@Primary
@Bean
public SwaggerResourcesProvider swaggerResourcesProvider(
InMemorySwaggerResourcesProvider defaultResourcesProvider
) {
return () -> {
List<SwaggerResource> resources = new ArrayList<>();
List<File> innerFiles = createFilesFromExternalDir();
innerFiles.forEach(file -> {
String path = "/" + file.getParentFile().getParentFile().getName()
+ "/" + file.getParentFile().getName()
+ "/" + file.getName();
String title = Files.readAllLines(file.toPath()).stream()
.filter(f -> f.contains("title"))
.findFirst()
.map(m -> m.split(":")[1].split("\"")[1])
.orElseGet(() -> "No Title");
resources.add(loadResource(path, title));
});
return resources;
};
}
위 코드의 핵심은 Swagger UI에 표시할 리소스를 동적으로 구성한다는 점입니다. 각 YAML 파일의 경로는 Spring Boot 정적 리소스 경로로 만들어지고, YAML 내부의 title 값은 Swagger UI 드롭다운에 표시되는 이름으로 사용됩니다.
이 방식으로 서버를 구성하면 개별 개발팀은 Swagger UI 서버를 별도로 운영할 필요가 없습니다. 중앙 Swagger 서버만 배포해두면 각 API 명세가 YAML 파일 단위로 추가되고, Swagger UI에서 선택 가능한 항목으로 노출됩니다.
외부 YAML 수집 및 정적 리소스 반영
swagger-demo는 루트의 swagger-apis 디렉터리를 외부 명세 저장소처럼 사용합니다. 서버 실행 시 ../swagger-apis/ 아래의 API별 폴더를 순회하고, 각 폴더에 존재하는 .yaml 파일을 src/main/resources/static/swagger-apis/ 아래로 복사합니다.
private List<File> createFilesFromExternalDir() throws IOException {
String externalDir = "../swagger-apis/";
String innerDir = "src/main/resources/static/swagger-apis/";
List<String> externalFilePaths = Files.list(Paths.get(externalDir))
.filter(path -> Files.isDirectory(path))
.map(path -> Files.list(path)
.filter(f -> f.toString().contains(".yaml"))
.findFirst().get())
.map(path -> path.getParent().getFileName() + "/" + path.getFileName())
.toList();
return externalFilePaths.stream()
.map(filePath -> {
File newFile = new File(innerDir + filePath);
Files.createDirectories(newFile.toPath().getParent());
Files.copy(
Paths.get(externalDir + filePath),
Paths.get(newFile.getPath()),
StandardCopyOption.REPLACE_EXISTING
);
return newFile;
})
.toList();
}
정적 리소스 영역으로 복사된 YAML은 /swagger-apis/api2/swagger.yaml과 같은 URL로 접근할 수 있고, Swagger UI는 이 URL을 읽어 API 문서를 렌더링합니다. 운영 구조로 발전시킬 경우에는 현재 상대 경로로 둔 외부 명세 저장소 경로를 설정값으로 분리하는 보완이 필요합니다.
Custom Annotation 기반 라이브러리화 방향
PoC의 확장 방향은 해당 프로젝트를 공통 라이브러리로 만드는 것이었습니다. 각 개발팀은 자신의 프로젝트에 이 라이브러리를 의존성으로 추가하고, 명세화하고 싶은 API에 Custom Annotation을 붙입니다. 이후 프로젝트가 배포될 때 라이브러리가 동작하면서 annotation이 붙은 API를 탐색합니다.
이 접근은 고객사 환경의 제약을 우회하면서도 개발팀의 사용성을 높이는 방향입니다. 개발자는 복잡한 Swagger 설정을 직접 다루지 않고, 명세화 대상 API에 annotation만 추가하면 됩니다. 라이브러리는 Controller 또는 Handler 정보를 기준으로 requestBody와 response VO를 분석하고, Swagger YAML 생성에 필요한 정보를 수집합니다.
-
API 식별: Custom Annotation이 붙은 API 메서드를 탐색합니다.
-
요청 분석: requestBody 타입과 필드 구조를 순회합니다.
-
응답 분석: response VO 타입과 중첩 필드 구조를 순회합니다.
-
YAML 생성: Swagger 2.0 형식에 맞추어 paths, parameters, definitions, responses를 구성합니다.
-
서버 반영: 생성된 YAML 파일을 Swagger 서버가 읽을 수 있는 위치에 생성하거나 전송합니다.
이 구조가 완성되면 API 명세 최신화는 배포 흐름과 자연스럽게 연결됩니다. 프로젝트 배포 시 라이브러리가 annotation이 붙은 API를 기준으로 YAML을 생성하고, 중앙 Swagger 서버가 변경된 YAML을 읽어 Swagger UI에 반영합니다.
YAML 생성 프로젝트의 역할
swagger-file-generator는 Java 객체 모델로 Swagger YAML 파일을 생성할 수 있는지 확인하기 위한 실험 프로젝트입니다. 현재 구현은 Swagger 문서의 주요 구성요소를 VO 클래스로 표현하고, 각 객체의 toString() 메서드에서 YAML 문자열을 조립하는 방식입니다.
이 프로젝트는 완성된 라이브러리라기보다, annotation 기반 명세 생성 라이브러리를 만들기 전에 필요한 YAML 생성 로직의 가능성을 검증한 형태입니다. requestBody와 response VO를 순회한 결과를 SwaggerDefinition, SwaggerProperty, SwaggerParameter 같은 객체에 담으면 최종 Swagger YAML 파일로 변환할 수 있습니다.
SwaggerYamlContentFactory factory = SwaggerYamlContentFactory.builder()
.info(SwaggerInfo.builder()
.description("This is build test")
.version("1.0.0")
.title("TEST").build())
.host("localhost:8080")
.basePath("/execute")
.paths(Arrays.asList(
SwaggerPath.builder()
.url("/v1/tenants/KOREA/sample")
.httpMethod("post")
.tag("test")
.summary("For Sample Test")
.parameters(Arrays.asList(
SwaggerParameter.builder()
.name("SampleQdo")
.inType("body")
.required(true)
.type("object")
.description("sample qdo").build()))
.responses(Arrays.asList(
SwaggerResponse.builder()
.code("200")
.description("sample response").build()))
.build()))
.build();
String yamlFormat = factory.toString();
FileGenerator.writeObjectToFile(
yamlFormat,
FileGenerator.createInnerDirAndFile("test")
);
위 테스트 코드는 Java 객체를 조립한 뒤 factory.toString()으로 YAML 문자열을 만들고, FileGenerator를 통해 swagger.yaml 파일로 저장합니다. 실제 라이브러리화 단계에서는 이 객체 생성 부분이 reflection 기반 분석 결과로 대체될 수 있습니다.
Swagger 문서 모델링 방식
SwaggerYamlContentFactory는 Swagger 문서 전체를 조립하는 역할을 합니다. 내부에는 info, host, basePath, paths, commonParameters, commonDefinitions가 포함되어 있으며, 각 영역을 순서대로 문자열로 변환합니다.
@Getter
@Builder
public class SwaggerYamlContentFactory {
private SwaggerInfo info;
private String host;
private String basePath;
private List<SwaggerPath> paths;
private List<SwaggerParameter> commonParameters;
private List<SwaggerDefinition> commonDefinitions;
@Override
public String toString() {
return getBasicFormat()
+ getPathsFormat()
+ getCommonParametersFormat()
+ getCommonDefinitionsFormat();
}
}
이 모델은 Swagger 문서의 구조와 거의 일대일로 대응됩니다. SwaggerPath는 API URL과 HTTP method, parameter, response를 표현하고, SwaggerDefinition은 request 또는 response VO의 schema 정의를 표현합니다. SwaggerProperty는 VO 내부 필드 하나를 Swagger property로 변환하는 역할을 담당합니다.
|
클래스 |
Swagger 영역 |
주요 역할 |
|---|---|---|
|
SwaggerInfo |
info |
문서 설명, 버전, title 정보를 구성합니다. |
|
SwaggerPath |
paths |
API URL, HTTP method, parameter, response를 구성합니다. |
|
SwaggerParameter |
parameters |
header, query, body parameter를 표현합니다. |
|
SwaggerDefinition |
definitions |
request/response VO의 schema를 표현합니다. |
|
SwaggerProperty |
properties |
VO 필드의 타입, 예시, 설명을 표현합니다. |
|
SwaggerResponse |
responses |
응답 코드와 schema 참조를 표현합니다. |
이러한 모델링은 라이브러리화 시 중요한 기반이 됩니다. annotation이 붙은 API를 찾은 뒤 requestBody와 response VO를 순회하면, 그 결과를 위 객체들에 매핑할 수 있습니다. 이후 객체를 YAML로 직렬화하면 Swagger UI가 읽을 수 있는 명세 파일을 만들 수 있습니다.
전체 동작 흐름
최종적으로 지향한 구조는 각 개발팀 프로젝트, 공통 Swagger 생성 라이브러리, 중앙 Swagger 서버, Swagger UI가 이어지는 흐름입니다. 개발팀은 자신의 프로젝트에 라이브러리를 추가하고 annotation을 붙입니다. 배포 시점에 라이브러리는 API와 VO를 분석하고 YAML 파일을 생성합니다. 중앙 Swagger 서버는 생성된 YAML을 수집하여 UI에 반영합니다.
각 개발팀 프로젝트
custom annotation 적용
↓
공통 Swagger 생성 라이브러리
annotation 대상 API 분석
requestBody / response VO 순회
swagger.yaml 생성
↓
Swagger 서버
YAML 파일 수집 및 리소스 등록
↓
Swagger UI
전사 개발팀이 배포 API 명세 확인
이 흐름은 서비스별 Swagger 설정을 최소화하면서도, 고객께서 원하신 Swagger UI 기반 명세 조회 경험을 제공합니다. 또한 API 명세 생성 책임을 공통 라이브러리로 모으기 때문에 개발팀별 구현 편차를 줄일 수 있습니다.
기대 효과
첫 번째 기대 효과는 API 명세 접근성 향상입니다. 전사 개발팀은 중앙 Swagger UI에 접속하여 현재 배포된 API 명세를 확인할 수 있습니다. API를 제공하는 팀과 사용하는 팀이 같은 화면을 기준으로 소통할 수 있으므로 협업 비용이 줄어듭니다.
두 번째 기대 효과는 문서 최신화 부담 감소입니다. Custom Annotation 기반 라이브러리화가 완료되면 개발자는 명세화 대상 API에 annotation을 붙이는 것만으로 YAML 생성 흐름에 참여할 수 있습니다. 배포 시점에 requestBody와 response VO를 기준으로 명세가 생성되므로 수동 문서 작성보다 누락 가능성을 낮출 수 있습니다.
세 번째 기대 효과는 기존 환경 제약을 유지하면서도 Swagger 도입 효과를 얻을 수 있다는 점입니다. 각 서비스가 Swagger 라이브러리를 직접 깊게 통합하지 않아도, 중앙 Swagger 서버와 공통 라이브러리를 통해 Swagger UI 기반 문서화 경험을 제공할 수 있습니다.
-
전사 공통 Swagger UI를 통해 API 명세 조회 위치를 단일화할 수 있습니다.
-
개발팀별 Swagger 설정 편차를 줄이고 공통 라이브러리 기준으로 명세 생성 방식을 표준화할 수 있습니다.
-
배포 흐름과 명세 생성 흐름을 연결하여 최신 API 기준의 문서화를 지향할 수 있습니다.
PoC에서 확인한 한계
PoC 단계의 구현은 가능성을 검증하는 데 초점을 두었기 때문에 운영 적용을 위해 보완해야 할 부분도 존재합니다. 현재 swagger-demo는 외부 YAML 경로를 상대 경로로 사용하고 있어 실행 위치에 영향을 받습니다. 운영 환경에서는 이 경로를 설정값으로 분리하고, 명세 파일 저장소를 명확히 정의해야 합니다.
또한 YAML 생성 프로젝트는 문자열 조립 방식으로 Swagger 문서를 생성합니다. 이 방식은 이해하기 쉽지만, 문서 구조가 복잡해질수록 누락이나 들여쓰기 오류가 발생할 수 있습니다. 운영 수준의 라이브러리로 발전시키려면 YAML 직렬화 라이브러리나 OpenAPI 모델 객체를 사용하는 방식도 검토할 필요가 있습니다.
정리
이번 PoC의 핵심은 고객사의 기존 개발 환경 제약을 유지하면서 Swagger 기반 API 명세 제공 방식을 검증한 것입니다. 고객께서는 Swagger를 통한 API 명세 확인을 원하셨고, 개발팀은 수동 문서 작성과 명세 최신화 부담을 줄일 필요가 있었습니다.
이를 위해 중앙 Swagger 서버를 구성하고, 여러 Swagger YAML 파일을 Swagger UI에서 선택하여 볼 수 있도록 개발했습니다. 더 나아가 해당 프로젝트를 라이브러리화하여 각 개발팀이 Custom Annotation만 적용하면 배포 시점에 requestBody와 response VO를 순회하고 Swagger YAML을 생성하는 구조를 목표로 했습니다.
결론적으로 이 프로젝트는 Swagger를 각 서비스에 직접 붙이는 방식이 아니라, 공통 라이브러리와 중앙 Swagger 서버를 통해 API 명세를 자동 생성하고 공유하는 방식을 검증한 PoC입니다. 고객사의 전사 개발팀이 동일한 Swagger UI에서 배포 API 명세를 확인할 수 있는 기반을 마련했다는 점에서 의미가 있습니다.
NZ