(BE 개발자 5개월차) 첫 오픈소스 기여 : Json Schema와 마이그레이션 그 마지막 이야기 (feat. PR Merge)
- -
👀 들어가기 전에
02.19 금일 부로 저의 기여가 드디어 Merge되었습니다!!!
https://github.com/swagger-api/swagger-core/pull/5034
fix: OAS 3.1 schema generation for raw Object properties by xeulbn · Pull Request #5034 · swagger-api/swagger-core
Pull Request Thank you for contributing to swagger-core! Please fill out the following information to help us review your PR efficiently. Description Related issue: #4682 This PR fixes an issue in...
github.com
Open Source인 Swagger의 이슈를 해결하는 과정에서 버그만 수정하게 된 것이 아니라 메인 로직(Json Schema 해석 로직) 변경까지 이어지게 되었고, 해당 변경 사항은 다음 Release에 반영됩니다.
분명히 제가 시작한 부분은 단순한 Json Schema 출력 교정이었습니다. 그런데 어떻게 하다 보니 Json Schema 해석의 로직까지 건드리게 되었고 그 과정에서 OAS 3.0과 3.1의 차이까지 깊게 파고들게 되었습니다.
이번 글에서는 그 과정을 차근차근 풀어보려 합니다.
👀 본론
1. 이슈의 시작!
이 이슈는 OAS 3.1에서 raw Object 필드가 다음과 같이 출력되는 문제에서 시작되었습니다.
{}
즉, 타입 정보가 없는 빈 스키마였습니다.
코드를 보면 원인은 명확했습니다.
// Before: OAS 3.1 Object → JsonSchema() (types 없음) → 출력이 {}
public Schema createProperty31() {
return new JsonSchema();
}
// After: OAS 3.1 Object → types=["object"] 명시 → 출력이 type: object
public Schema createProperty31() {
return explicitObjectType == null || explicitObjectType
? new JsonSchema().typesItem("object")
: new JsonSchema();
}
OAS 3.1에서는 Schema#getTypes()기반으로 직렬화가 이루어지는데 기존 구현은 types를 설정하지 않았기 때문에 {}로 출력되고 있었습니다. 그래서 해당 내용을 위의 After와 같이 OAS 3.1에서는 types가 실제 직렬화에 사용되기에 typesItem("object")를 명시적으로 추가해주어야 type: object로 인식이 잘됩니다.
2. 테스트 설계
Object 필드 하나만 테스트하면, 이후 리팩토링 과정에서 List<Object> 혹은 Map<String,Object>에서 동일한 문제가 재발할 수 있다고 판단했습니다. 그래서 재현 케이스를 아래 3종으로 고정했습니다.
- raw Object property
- List<Object>
- Map<String, Object>
그리고 OAS 3.1모드에서만 실패/성공이 나오도록 테스트를 구성했습니다.
여기까지가 이전 포스트에서 작성한 내용을 정리한 것입니다.
3. 이후의 과정 - 연쇄 빌드 붕괴
raw Object 처리가 변경되면서 YAML 출력 전체가 바뀌기 시작했습니다.
특히 union 타입에 object가 끼어들게 되었고, 기존 테스트들은 {} 출력을 전제로 맞춰져 있었기에 빌드가 계속 깨졌습니다.
이에 처음에는 테스트를 통과시키기 위해 기존 테스트의 출력 fixture를 수정하려 했습니다.
하지만...
4. 사건의 발생
수정이 계속 이어질수록
지금 나는 근본적인 문제를 해결하고 있는가,
아니면 지금 당장의 빌드만 통과시키고 있는가
에 대한 생각이 들었습니다.
그래서 잠시 작업을 멈추고 전체 흐름을 처음부터 다시 추적했습니다.
5. 근본 원인 발견 - 명시 타입 vs. 추론 타입
기존 테스트는 다음과 같은 상황을 허용하고 있었습니다.
@Schema(types = {"string","number"})
Object value;
개발자는 분명 "string" 또는 "number"만 허용하겠다고 명시했습니다.
하지만 내부 로직은 아래와 같이 작동하고 있었습니다.
- Java 타입이 Object → 추론 타입 : object
- 어노테이션 명시 타입 : string, number
- 내부에서 merge
이에 따라 아래와 같은 결과가 나오게 됩니다.
type:
- object
- string
- number
이제 Schema는 object도 허용하게 됩니다.
즉, 이는 아래 JSON도 가능하다는 의미입니다.
{
"value": {
"unexpected": "object"
}
}
여기서 아래와 같은 의문이 들었습니다.
OAS 3.1에서 types는 단일 문자열이 아니라 복수 타입을 표현하는데,
개발자가 명시한 union이 내부 추론 때문에 의도치 않게 넓어지는 게 맞는 동작인가?
@Schema(types =...)는 "이 타입들만 허용하겠다"라는 명시적 선언입니다.
여기서 추론 타입이 끼어들게 되어 union이 확장된다면 어노테이션을 붙인 의미 자체가 흐려진다고 판단했습니다.
그래서 결론을 아래와 같이 잡았습니다.
- 명시 types가 없는 경우 : 기존 처럼 resolve된 타입을 그대로 반영한다.
- 명시 types가 있는 경우 : resolve된 타입을 섞지 않고 명시된 types만 최종 Schema에 반영한다.
이 방향으로 코드를 수정하며 OAS 3.1에서는 더이상 setType()과 같은 단일 타입 중심의 API를 사용하지 않고 types(Set)기반으로 일관되게 처리하도록 정리하였습니다.
⭐️ 왜 OAS3.0에서는 되고, OAS 3.1에서는 Main 문제가 된 것일까?
OAS 3.0에서의 type은 아래와 같이 단일 문자열입니다.
type: object
type: string
즉, 스키마는 한 번에 하나의 타입만 가질 수 있습니다.
만약 여러 타입을 허용하고 싶다면 oneOf 혹은 anyOf와 같은 별도 구조를 사용해야 했습니다.

예를 들어
type: string
은 가능하지만
type:
- string
- number
은 OAS 3.0에서는 스펙 위반이었습니다.
즉, 스키마는 한 번에 하나의 타입만 가질 수 있습니다.
만약 여러 타입을 허용하고 싶다면 oneOf 혹은 anyOf와 같은 별도 구조를 사용해야 했습니다.
@Schema(types = {"string", "number"})
Object value;
OAS 3.0에서는 실제로는 이런 식의 union표현이 직접적으로 type에 반영되지 않습니다.
결국 내부적으로는 type이 하나만 설정되거나, oneOf의 형태로 표현됩니다.
즉, setType("string"), setType("number"), addType(...) 이런 방식이 실제로는 큰 의미의 차이를 만들지 않았습니다.
단일 타입 모델이었기 때문입니다.
OAS 3.1에서는
하지만, OAS 3.1부터는 다릅니다. OAS 3.1은 JSON Schema 2020-12를 따릅니다.

여기서부터가 달라지게 됩니다.
type:
- string
- number
이제 type은 배열이 될 수 있습니다. 즉 type 자체가 union입니다.
이말은 즉,
내부에서 object를 하나 더 addType()하면 union이 실제로 확장된다는 의미입니다.
6. 그래서 내린 결론
코드를 보면 다음과 같습니다.
ModelResolver.java
//Before
if (openapi31 && schemaAnnotation != null) {
for (String type : schemaAnnotation.types()) {
schema.addType(type);
}
}
//After
if (openapi31 && schemaAnnotation != null) {
if (schemaAnnotation.types().length > 0) {
schema.setTypes(new LinkedHashSet<>(Arrays.asList(schemaAnnotation.types())));
}
...
}
AnnotationsUtils.java
//Before
if (schema.types().length > 0) {
if (schema.types().length == 1) {
schemaObject.setType(schema.types()[0]);
}
for (String type : schema.types()) {
schemaObject.addType(type);
}
}
//After
if (schema.types().length > 0) {
schemaObject.setTypes(new LinkedHashSet<>(Arrays.asList(schema.types())));
}
OAS 3.1에서는 더 이상 setType()과 같은 단일 타입 중심 API를 사용하지 않고, types(Set) 기반으로 일관되게 처리되도록 정리했습니다.
7. 최종 Merge

PR이 Merge 되었습니다. 다음 release에 제가 수정한 Swagger가 포함됩니다.

정말 신났습니다. 물론 메인테이너 분께서 많은 도움을 주셨고, 리뷰 과정에서 로직과 방향을 함께 정리할 수 있었습니다.
8. 이번 기여의 회고
- 작은 수정이 레거시 전체에 영향을 줄 수 있다.
→ 너무 당연한 이야기지만, 이번 기회에 뼈저리게 느끼게 되었습니다. - 스펙을 정확히 이해하지 않으면 근본 원인을 해결할 수 없다.
- 테스트를 작성하는데 지켜야할 규율들을 다시금 배워갈 수 있었다.
→ Maintainer분께서 현재 사용 중이신 테스트 규약을 잘 알려주셔서 잘 배워갈 수 있었습니다.
특히 코드 스타일, 테스트 네이밍, 기존 로직을 존중하는 방식 등 대형 프로젝트에서의 코드 수정 방식에 대해 많이 배웠습니다.
덕분에 레거시를 분석하는 법도 성장할 수 있었던 것 같습니다.
👀 마치며
오픈소스 기여는 엄청난 실력이 있어야만 할 수 있는 일이라고 생각했습니다.
하지만 직접 해보니, 가장 중요한 것은 기존 레거시 코드를 이해하려는 태도와 왜 이렇게 동작하는 것인지를 끝까지 파고드는 끈기였던 것 같습니다. 특히 실무에서도 사용하는 Swagger에 직접 기여하게 되어 더욱 뜻깊었습니다.
많은 사용자들이 겪던 불편을 직접 마주하고 레거시 코드를 분석하고, 공식 문서를 읽고, 스펙과 구현을 맞춰가는 과정이 정말 즐거웠습니다.
다음 기회에도 꼭 다시 기여하고 싶습니다.
'Backend' 카테고리의 다른 글
| (BE 개발자 5개월차 독후감) [Unit Testing : 단위 테스트] 독후감 마무리 (feat. 가치 있는 테스트 작성하기) (0) | 2026.02.07 |
|---|---|
| (BE 개발자 5개월차) 연속 중복 제거 요구사항, 쿼리 대신 Read 전용 테이블을 선택한 이유 (ft. 현업에서 경험한 요구사항 풀어내기) (1) | 2026.02.02 |
| (BE 개발자 4개월차 독후감) Unit Testing : 단위 테스트 1차 독후감 (0) | 2026.01.24 |
| (BE 개발자 4개월차) 첫 오픈소스 기여 : Json Schema와 마이그레이션 (1) | 2026.01.14 |
| (BE 개발자 4개월차) 현업에서 고민한 DB I/O 줄이기 (0) | 2026.01.08 |
당신이 좋아할만한 콘텐츠
소중한 공감 감사합니다