Swagger-core: Spring ํ”„๋กœ์ ํŠธ์— ModelConverter๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?

์— ๋งŒ๋“  2015๋…„ 10์›” 29์ผ  ยท  4์ฝ”๋ฉ˜ํŠธ  ยท  ์ถœ์ฒ˜: swagger-api/swagger-core

Spring MVC ๋ฐ Spring ๋ถ€ํŠธ๊ฐ€ ์žˆ๋Š” ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
๋‚ด json ์„œ๋น„์Šค๋ฅผ ๋ฌธ์„œํ™”ํ•˜๊ธฐ ์œ„ํ•ด Swagger springfox๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. java.sql.Time ๋ฐ java.util.Date ์†์„ฑ์ด ์žˆ๋Š” ํด๋ž˜์Šค๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.
API๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด ํด๋ž˜์Šค๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

"TranslationLabel":{
    "properties":{
        "change_date":{
            "type":"string",
            "format":"date-time"
        },
        "change_time":{
            "$ref":"#/definitions/Time"
        }
    },
    "description":"Traduction de libellรฉ"
}

Swagger UI์—์„œ ๋ชจ๋ธ ์Šคํ‚ค๋งˆ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 {
  "change_date": "2015-10-29",
  "change_time": {
    "date": 0,
    "day": 0,
    "hours": 0,
    "minutes": 0,
    "month": 0,
    "seconds": 0,
    "time": 0,
    "timezoneOffset": 0,
    "year": 0
  }
}

"15:31"๊ณผ ๊ฐ™์ด "change_time"์„ ํ‘œ์‹œํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.
์ด๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด ModelConverter๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด ์˜ˆ์ œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์‚ฌ์šฉ์ž ์ง€์ • ๋ณ€ํ™˜๊ธฐ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. https://github.com/swagger-api/swagger-core/blob/master/modules/swagger-core/src/test/java/io/swagger/model/ ์žฌ์ •์˜/CustomConverterTest.java
๊ทธ๋Ÿฌ๋‚˜ ๋‚˜๋Š” ๊ทธ๊ฒƒ์„ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

์—ฌ๊ธฐ ๋‚ด ์ˆ˜์—…์ด ์žˆ์Šต๋‹ˆ๋‹ค :

class CustomConverter implements ModelConverter {
    <strong i="18">@Override</strong>
    public Property resolveProperty(Type type, ModelConverterContext context, Annotation[] annotations,
                                    Iterator<ModelConverter> chain) {
        final JavaType jType = Json.mapper().constructType(type);
        if (jType != null) {
            final Class<?> cls = jType.getRawClass();
            if (cls.equals(Time.class)) {
                HashMap<PropertyBuilder.PropertyId, Object> map =
                        new HashMap<>();
                map.put(PropertyBuilder.PropertyId.FORMAT,"HH:mm");
                map.put(PropertyBuilder.PropertyId.TYPE,"time");
                map.put(PropertyBuilder.PropertyId.EXAMPLE,"12:30");
                return PropertyBuilder.build("Time", "hh:mm", map);
            } else {
                return chain.next().resolveProperty(type, context, annotations, chain);
            }
        } else {
            return null;
        }
    }

    <strong i="19">@Override</strong>
    public Model resolve(Type type, ModelConverterContext context, Iterator<ModelConverter> chain) {
        return chain.next().resolve(type, context, chain);
    }
}

์ด์ œ ModelConverters์— ๋ณ€ํ™˜๊ธฐ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

ModelConverters converters = new ModelConverters();
converters.addConverter(new CustomConverter());

์ด ์ฝ”๋“œ๋ฅผ ์–ด๋””์— ๋„ฃ์–ด์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ๋ฌธ์„œ๋ฅผ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.
์ด๊ฑธ ์–ด๋””์— ๋„ฃ์–ด์•ผ ํ–ˆ์–ด?

๊ฐ€์žฅ ์œ ์šฉํ•œ ๋Œ“๊ธ€

Springfox ์—๋Š” ์ด๋ฅผ ๋‹ฌ์„ฑํ•˜๋Š” ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋™๋“ฑํ•œ ๋ณ€ํ™˜๊ธฐ๋ฅผ ์ •์˜ํ•˜๋ ค๋ฉด Docket.directmodelSubstitute(Time.class, String.class) ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์˜ˆ์ œ ๋“ฑ์€ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋ชจ๋“  4 ๋Œ“๊ธ€

Springfox ์—๋Š” ์ด๋ฅผ ๋‹ฌ์„ฑํ•˜๋Š” ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋™๋“ฑํ•œ ๋ณ€ํ™˜๊ธฐ๋ฅผ ์ •์˜ํ•˜๋ ค๋ฉด Docket.directmodelSubstitute(Time.class, String.class) ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์˜ˆ์ œ ๋“ฑ์€ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋‹ต๋ณ€ํ•ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.
๊ท€ํ•˜์˜ ์†”๋ฃจ์…˜์„ ์‹œ๋„ํ–ˆ์ง€๋งŒ "change_time":"hh:mm" ๋Œ€์‹  "change_time":"string"์„ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค.
String ์˜ ํ˜•์‹์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๊นŒ?

์•ˆ๋…•ํ•˜์„ธ์š” ์—ฌ๋Ÿฌ๋ถ„,

ModelConverters์™€ ๊ด€๋ จ๋œ ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‚ด Swagger.json์—๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ํ™•์žฅ๋œ ํ˜•์‹์˜ ์ผ๋ฐ˜ ํด๋ž˜์Šค๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

"JsonNode": {
"์œ ํ˜•": "๊ฐ์ฒด",
"์†์„ฑ": {
"floatingPointNumber": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๊ฐ’ ๋…ธ๋“œ": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"bigInteger": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"ํ…์ŠคํŠธ": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๋ถ€์šธ": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"์ปจํ…Œ์ด๋„ˆ ๋…ธ๋“œ": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"missingNode": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๋ฌผ์ฒด": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"ํฌ์กฐ": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"์ˆซ์ž": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"integralNumber": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"์งง์€": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"int": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๊ธด": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๋”๋ธ”": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"bigDecimal": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๋œจ๋‹ค": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๋…ธ๋“œ ์œ ํ˜•": {
"์œ ํ˜•": "๋ฌธ์ž์—ด",
"์—ด๊ฑฐ": [
"์ •๋ ฌ",
"๋ฐ”์ด๋„ˆ๋ฆฌ",
"๋ถ€์šธ",
"์žƒ์–ด๋ฒ„๋ฆฐ",
"์—†๋Š”",
"์ˆซ์ž",
"๋ฌผ์ฒด",
"ํฌ์กฐ",
"๋ˆ"
]
},
"๋ฐ”์ด๋„ˆ๋ฆฌ": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"๋ฐฐ์—ด": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
},
"์—†๋Š”": {
"์œ ํ˜•": "๋ถ€์šธ",
"๊ธฐ๋ณธ๊ฐ’": ๊ฑฐ์ง“
}
}
}

์ด์ œ ์ด๊ฒƒ์€ ํ™•์‹คํžˆ ๋‚ด๊ฐ€ ์›ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์‚ฌ์‹ค ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ๊ฒƒ์€ ์ด ํŠน์ • ํด๋ž˜์Šค(com.fasterxml.jackson.databind.JsonNode)(๋ฐ ๋‚ด๊ฐ€ ์„ ํƒํ•œ ๋‹ค๋ฅธ ํด๋ž˜์Šค)๊ฐ€ ํ™•์žฅ๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋ฅผ ์ˆ˜ํ–‰ํ•˜๊ธฐ ์œ„ํ•ด ์•„๋ž˜์— ํ‘œ์‹œ๋œ io.swagger.converter.ModelConverters ํด๋ž˜์Šค์—์„œ ์ œ๊ณต๋˜๋Š” ๋‚ด์žฅ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ณต๊ฐœ ๋ฌดํšจ addClassToSkip(๋ฌธ์ž์—ด cls) {
LOGGER.warn("ํด๋ž˜์Šค ๊ฑด๋„ˆ๋›ฐ๊ธฐ " + cls);
this.skippedClasses.add(cls);
}

์ด์ œ ์ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•œ ํ›„ ModelConverters ํด๋ž˜์Šค๋ฅผ ๋‹ค๋ฅธ ์‚ฌ๋žŒ๊ณผ ํ•จ๊ป˜ ๊ณต๊ธ‰์ž ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ•˜๋ ค๊ณ  ์‹œ๋„ํ–ˆ์ง€๋งŒ ๊ฒฐ๊ณผ๊ฐ€ ์—†์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์‹ค์ œ๋กœ ModelConverters ํด๋ž˜์Šค๋ฅผ ์ฃผ์ž…ํ•  ์œ„์น˜๋ฅผ ์•Œ์•„๋‚ผ ์ˆ˜ ์—†๋Š” ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋ˆ„๊ตฐ๊ฐ€ ์ด ๋ฌธ์ œ๋ฅผ ๋„์™€์ฃผ์„ธ์š”. ๋‚˜๋Š” ๊ฑฐ์˜ 3 ์ผ ๋™์•ˆ ๋‹ต์„ ์ฐพ๊ณ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž ์ •์˜ ๋ณ€ํ™˜๊ธฐ๋ฅผ ์‚ฌ์šฉํ•˜์‹ญ์‹œ์˜ค. ์ด๊ฒƒ์€ ๋‚ด ๋ถ„์„์— ๋”ฐ๋ฅธ ๋ฒ„๊ทธ์ž…๋‹ˆ๋‹ค. ModelResolver๋ฅผ ์ž์‹ ์˜ ์ฝ”๋“œ์— ๋ณต์‚ฌํ•˜๊ณ  ์ด์™€ ๊ฐ™์€ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ModelConverters.getInstance().addConverter(new ModelResolver(mapper));

ModelResolver์—์„œ ๋‹ค์Œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ์ˆ˜์ •ํ•ฉ๋‹ˆ๋‹ค.

private Model resolve(JavaType type, ModelConverterContext context, Iterator<ModelConverter> next) {
        if (type.isEnumType() || PrimitiveType.fromType(type) != null) {
            // We don't build models for primitive types
            return null;
        }

        if (type instanceof SimpleType && ((SimpleType) type).getRawClass().getName().startsWith("your classname")) {
            return yourConverter.resolve(type, context, next);
        }

ํ•ดํ‚น์ž…๋‹ˆ๋‹ค. ๋ฌธ์ œ๋Š” https://github.com/swagger-api/swagger-core/issues/2260 ๊ณผ ๋™์ผํ•ฉ๋‹ˆ๋‹ค.

์ด ํŽ˜์ด์ง€๊ฐ€ ๋„์›€์ด ๋˜์—ˆ๋‚˜์š”?
0 / 5 - 0 ๋“ฑ๊ธ‰