새소식

Welcome to the tech blog of Junior Backend Developer Seulbin Kang!

Backend

(BE 개발자 4개월차) 첫 오픈소스 기여 : Json Schema와 마이그레이션

  • -

👀 들어가기 전에

 

개발을 하다 보면 "기술 스택"이라는 단어를 자연스럽게 쓰게 된다.

하지만 어느 순간부터는 이런 궁금증이 생겼었다.

우리가 매일 사용하는 이 도구들은 누가 만들었고,
어떤 판단의 결과로 지금의 형태가 되었을까?

 

Spring, Swagger, Kafka, Elasticsearch 등등

채용 공고에서 흔히 보이는 이 기술들은 대부분은 오픈소스다.

막연하게 "언젠가 나도 이런걸 만들어보고 싶다"는 생각만 하다가, 최근 어느 정도 대규모의 코드도 읽히기 시작하면서

실제 오픈소스 코드를 보기 시작했다.

 

그 무렵 운이 좋게도 인제님께서 진행하시는 오픈소스 기여 모임을 발견하게 되었고 바로 참여하였다.

아래 링크에서 자세한 정보를 확인하실 수 있습니다!! 오픈소스에 관심있으시다면 강!추! 합니다.

https://medium.com/@injae-kim

관심있는 프로젝트의 이슈를 하나씩 파보는 과정에서 내 눈에 들어온 이슈가 바로 Swagger(OpenAPI) 3.1 관련 버그였다.


👀 본론

문제 상황 : 직렬화 문젠가?

이슈의 요지는 간단해 보였다.

Open API 3.1  스펙 생성 시,
Java의 Object 타입 필드가 빈 스키마로 생성된다.

깃허브 이슈

 

재밌었던 점은 Open API 3.0.x에서는 정상이라는 것이다.

즉, 3.1에서만 문제가 발생한다는 것이다.

처음에는 자연스럽게 위 이슈를 올려주신 분처럼 생각했다.

YAML 3.1 직렬화 과정에서 type 정보가 누락되고 있는거 아닐까?

 

그래서 우선 버그를 재현하고 디버깅을 진행하기 위해 댓글을 남겨놓고 테스트를 작성했다.


1단계 : 직렬화 이전부터 이미 비어있네?

테스트의 목적은 아래와 같다.

YAML로 찍기전, Schema 객체 내부에 type 정보가 존재하고 이를 뽑아오는 과정에서 즉, 직렬화 과정에서 문제가 생기는게 맞는가?

public class ObjectFieldOas31ReproTest {

    @Test
    public void testModelUsingObjectTypedPropertyLosesTypeInformationForOas31() throws Exception {

        Map<String, Schema> schemas =
                ModelConverters.getInstance(true)
                        .readAll(PojoUsingObjectField.class);

        Schema pojo = schemas.get("PojoUsingObjectField");
        Schema myField = (Schema) pojo.getProperties().get("myField");

        System.out.println("=== BEFORE YAML31 SERIALIZATION ===");
        System.out.println("myField.getType()  = " + myField.getType());
        System.out.println("myField.getTypes() = " + myField.getTypes());
        System.out.println("myField.class      = " + myField.getClass());
        System.out.println("===================================");

        // 실제 OAS 3.1 YAML 출력 확인
        String actual = Yaml31.mapper().writeValueAsString(schemas);

        System.out.println("===== ACTUAL YAML31 =====");
        System.out.println(actual);
        System.out.println("=========================");

        String expectedYaml =
                "PojoUsingObjectField:\n" +
                "  type: object\n" +
                "  properties:\n" +
                "    myField:\n" +
                "      type: object";

        // 버그 재현
        io.swagger.v3.core.matchers.SerializationMatchers
                .assertEqualsToYaml31(schemas, expectedYaml);
    }

    private static class PojoUsingObjectField {

        private Object myField;

        public Object getMyField() {
            return myField;
        }

        public void setMyField(Object myField) {
            this.myField = myField;
        }
    }
}

 

이때, 나온 출력값은 아래와 같다.

 

Yaml31 Serialization 전후 비교

 

즉, 직렬화 과정에서 type이 날라간 것이 아니라, 애초에 OAS 3.1 경로에서 Schema 자체가 잘못 만들어지고 있었다.

문제의 위치가 출력에서 발생한 것이 아니라 생성되는 단계에서 발생하고 있는 것이었다.


2단계 : OAS 3.0 vs 3.1 코드 비교

ModelResolver 내부에서 Java 타입 → Open API Schema로 변환되는 로직을 추적했고,

// ModelResolver.java 중
if (primitiveType != null) {
    Schema primitive = openapi31
            ? primitiveType.createProperty31()
            : primitiveType.createProperty();

    model = primitive;
    isPrimitive = true;
}

// PrimitiveType.java 중
OBJECT(Object.class) {

    @Override
    public Schema createProperty() {
        return explicitObjectType == null || explicitObjectType
                ? new Schema().type("object")
                : new Schema();
    }

    @Override
    public Schema createProperty31() {
        return new JsonSchema();
    }
};

문제는 아래 지점에서 발견할 수 있었다.

OBJECT(Object.class) {
    @Override
    public Schema createProperty() {
        return explicitObjectType == null || explicitObjectType ? new Schema().type("object") : new Schema();
    }
    @Override
    public Schema createProperty31() {
        return new JsonSchema();
    }
};

 

OAS 3.0.x에서는 type("object")가 명시되지만, OAS 3.1 경로에서는 JsonSchema만 생성되고 types가 설정되지 않아 

raw Object가 빈 스키마로 직렬화되는 문제가 발생했다.


해결 : OAS 3.1 직렬화 경로에 맞도록

Open API 3.1에서는 Schema#getTypes()를 사용하고 있기 때문에 

typesItem으로 넣어줘야 인식이 된다.

return new JsonSchema().typesItem("object");

검증 : 하나의 테스트로는 부족하다.

이 문제는 단순히 Object field하나만 해당하던 문제가 아니다.

  • Object
  • List<Object>
  • Map<String,Object>

모두 동일한 변환 경로를 타고 있었다.

 

그래서 아래 세가지 케이스를 모두 테스트로 검증해두었다.

 

1. Raw Object property

@Test(description = "Repro #4682: In OAS 3.1, raw Object property should not be rendered as an empty schema")
    public void oas31_rawObjectProperty_shouldBeObjectSchema() {
        Map<String, Schema> schemas = ModelConverters.getInstance(true).readAll(PojoUsingObjectField.class);
        Schema pojo = schemas.get("PojoUsingObjectField");
        Assert.assertNotNull(pojo, "PojoUsingObjectField schema should exist");

        Schema myField = (Schema) pojo.getProperties().get("myField");
        Assert.assertNotNull(myField, "myField schema should exist");
        Assert.assertTrue(isObjectSchema(myField), "Expected raw Object property to be an object schema in OAS 3.1, but was: type=" + myField.getType() + ", types=" + myField.getTypes() + ", class=" + myField.getClass());
    }

 

2. List<Object> item schema

@Test(description = "OAS 3.1: List<Object> items schema should be object")
    public void oas31_listOfObject_items_shouldBeObjectSchema() {
        Map<String, Schema> schemas = ModelConverters.getInstance(true).readAll(PojoUsingListOfObject.class);
        Schema pojo = schemas.get("PojoUsingListOfObject");
        Assert.assertNotNull(pojo, "PojoUsingListOfObject schema should exist");
        Schema itemsProp = (Schema) pojo.getProperties().get("items");
        Assert.assertNotNull(itemsProp, "items property schema should exist");
        Schema itemSchema = itemsProp.getItems();
        Assert.assertNotNull(itemSchema, "List<Object> items schema should exist");
        Assert.assertTrue(isObjectSchema(itemSchema), "Expected List<Object> items to be an object schema in OAS 3.1, but was: type=" + itemSchema.getType() + ", types=" + itemSchema.getTypes() + ", class=" + itemSchema.getClass());
    }

 

3. Map<String, Object> additionalProperties

@Test(description = "OAS 3.1: Map<String,Object> additionalProperties schema should be object")
    public void oas31_mapStringToObject_additionalProperties_shouldBeObjectSchema() {
        Map<String, Schema> schemas = ModelConverters.getInstance(true).readAll(PojoUsingMapStringToObject.class);
        Schema pojo = schemas.get("PojoUsingMapStringToObject");
        Assert.assertNotNull(pojo, "PojoUsingMapStringToObject schema should exist");
        Schema additionalProp = (Schema) pojo.getProperties().get("additional");
        Assert.assertNotNull(additionalProp, "additional property schema should exist");
        Schema valueSchema = (Schema) additionalProp.getAdditionalProperties();
        Assert.assertNotNull(valueSchema, "Map<String,Object> additionalProperties (value schema) should exist");
        Assert.assertTrue(isObjectSchema(valueSchema), "Expected Map<String,Object> additionalProperties to be an object schema in OAS 3.1, but was: type=" + valueSchema.getType() + ", types=" + valueSchema.getTypes() + ", class=" + valueSchema.getClass());
    }

 

 


👀 마치며.. 

이번 수정은 코드로 보면 진짜 몇 줄 되지도 않는 수정이다.

하지만 이슈는 생각보다 미치는 영향이 컸다.

 

이번 경험을 통해 레거시 코드를 읽는다는 것이 

  • 왜 이렇게 설계되었는지
  • 각각의 버전별로 어떤 것을 요구하고 어떤 것이 달라졌는지
  • 어디까지과 호환이고 어디서부터가 변경된 부분인지

를 이해하는 점임을 다시 한 번 느낄 수 있었다.

아직 PR에 대한 피드백은 없지만, 설령 머지되지 않더라도 충분히 재밌는 경험을 했다.

 

대형 오픈소스를 직접 만져볼 수 있었다는 것만으로도 오랜만에 심장뛰는 개발을 한 것 같다.

다음에는 이 글에 피드백과 결과를 작성하러 오겠다. 😄

 

 

2026.01.27 

Maintainer가 PR 브랜치에 master브랜치 병합해줬다!!

Swagger Core 메인테이너 중 한 분인 Daniel Kmiecik이 PR브랜치에 대해 master → PR 브랜치 병합을 진행해주셨다!

해당 이슈가 실제로 검토되고 있다는 것을 확인할 수 있었다!!

 

스터디 활동의 마무리를 하며 오픈소스에 기부도 해보았다.!!

Swagger에 기여를 했기에 Swagger에 하고 싶었지만, 기부를 받지는 않아 테스트 코드 쓰느라 자주 쓰는 JUnit에 기부도 하였다!!

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.