개인적으둜 μ—λ„ˆμ§€ μ‚¬μ—…μ—μ„œ μ‚¬μš©λ˜λŠ” KPX의 μ€‘κ°œκ±°λž˜ API에 λŒ€ν•œ λͺ¨ν‚Ή μƒ˜ν”Œμ„ λ§Œλ“€μ–΄λ³΄λ©΄μ„œ μ½”ν‹€λ¦° 기반 μŠ€ν”„λ§ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ ν•™μŠ΅ν•΄λ³΄κ³  μžˆμŠ΅λ‹ˆλ‹€. 이 κ³Όμ •μ—μ„œ κ°„λ‹¨ν•˜κ²Œ κ²½ν—˜ν•œ Jackson κ΄€λ ¨ λ‚΄μš©μ„ μ μ–΄λ³΄κ³ μž ν•©λ‹ˆλ‹€. λ³Έ 글에 μž‘μ„±λœ 것은 였직 μ½”ν‹€λ¦°μ—λ§Œ ν•΄λ‹Ήν•˜λŠ” λ‚΄μš©μ΄ 아닐 수 μžˆμœΌλ―€λ‘œ μ£Όμ˜ν•˜μ—¬ μ°Έκ³ ν•˜μ‹œκΈΈ λ°”λžλ‹ˆλ‹€.

build.gradle.kts
dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") }

Jackson λΌμ΄λΈŒλŸ¬λ¦¬μ—μ„œλŠ” μ½”ν‹€λ¦° 지원을 μœ„ν•΄ jackson-module-kotlin λͺ¨λ“ˆμ„ μ œκ³΅ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. κ·Έλž˜μ„œ μ½”ν‹€λ¦° 기반 μŠ€ν”„λ§ ν”„λ ˆμž„μ›Œν¬μ—μ„œλ„ JSON 직렬화λ₯Ό μœ„ν•΄μ„œ Jackson 라이브러리λ₯Ό μ‚¬μš©ν•˜κ²Œ λ˜λ‚˜λ΄…λ‹ˆλ‹€. μŠ€ν”„λ§ λΆ€νŠΈ μžλ™ μ„€μ •μ—μ„œλŠ” μ½”ν‹€λ¦° μ§€μ›μœΌλ‘œ μ½”ν‹€λ¦° λͺ¨λ“ˆμ΄ ν΄λž˜μŠ€νŒ¨μŠ€μ— 있으면 μžλ™μœΌλ‘œ λ“±λ‘ν•˜κ³  ν•΄μ£Όκ³  있음이 μ•„λž˜μ™€ 같이 λ‚˜μ™€ μžˆμŠ΅λ‹ˆλ‹€.

Jackson’s Kotlin module is required for serializing / deserializing JSON data in Kotlin. It is automatically registered when found on the classpath. A warning message is logged if Jackson and Kotlin are present but the Jackson Kotlin module is not.

μœ„ μŠ€ν”„λ§ 곡식 λ¬Έμ„œμ˜ λ‚΄μš©μ— λ”°λ₯΄λ©΄ 클래슀 νŒ¨μŠ€μ— μ½”ν‹€λ¦° λͺ¨λ“ˆμ΄ ν¬ν•¨λ˜μ–΄ μžˆμ§€ μ•Šλ‹€λ©΄ WARN 둜그λ₯Ό 좜λ ₯ν•΄μ€€λ‹€κ³  λ˜μ–΄μžˆμ§€λ§Œ μ½”ν‹€λ¦° λͺ¨λ“ˆμ— λŒ€ν•œ μ˜μ‘΄μ„±μ„ μ œκ±°ν•˜κ³  μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ 싀행해도 κ΄€λ ¨ λ‘œκ·ΈλŠ” 확인할 수 μ—†μ—ˆμŠ΅λ‹ˆλ‹€. 그리고 λ§Œμ•½, μžλ™ 섀정이 μ•„λ‹Œ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ„ μœ„ν•΄ μƒμ„Έν•œ 섀정을 μœ„ν•΄ λ³„λ„μ˜ 빈으둜 λ“±λ‘ν•œλ‹€λ©΄ μ½”ν‹€λ¦° 기반 μŠ€ν”„λ§ μ• ν”Œλ¦¬μΌ€μ΄μ…˜μ—μ„œ ObjectMapper의 μ½”ν‹€λ¦° λͺ¨λ“ˆμ΄ μ œλŒ€λ‘œ λ“±λ‘λ˜μ–΄μžˆλŠ”μ§€ 검증해야할 것 κ°™μŠ΅λ‹ˆλ‹€.

μ œλ„€λ¦­ μœ ν˜•μ„ μœ„ν•œ TypeReference

μžλ°” 기반 μ½”λ“œμ—μ„œλŠ” μ œλ„€λ¦­ μœ ν˜•μ— λŒ€ν•œ 직렬화λ₯Ό μœ„ν•΄μ„œ TypeReference<>() {} 와 같은 ν˜•νƒœλ‘œ μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ•Όλ§Œ ν–ˆμŠ΅λ‹ˆλ‹€. μ½”ν‹€λ¦° λͺ¨λ“ˆμ—λŠ” readValue와 convertValue 에 λŒ€ν•œ ν™•μž₯ ν•¨μˆ˜λ₯Ό ν¬ν•¨ν•˜κ³  μžˆμ–΄ ꡬ체적인 클래슀 정보λ₯Ό μΆ”λ‘ ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.convertValue

data class MyStateObject(val name: String, val age: Int)

val obj = mapper.readValue<MyStateObject>(json)

Flatten ν•˜κ²Œ 직렬화할 수 μžˆλŠ” @JsonUnwrapped

{
  "FORE_SET_GEN_ID": "V000",
  "TRADE_DAY": "20210601",
  "FORECAST_VALUES": [
    {
      "CBP_GEN_ID": "AA11",
      "ESS_MTR_TYPE": 0,
      "HH_01": 0,
      "HH_02": 0,
      "HH_03": 0,
      "HH_04": 0,
      "HH_05": 0,
      "HH_06": 0,
      "HH_07": 0,
      "HH_08": 100.223,
      "HH_09": 100.223,
      "HH_10": 100.223,
      "HH_11": 100.223,
      "HH_12": 100.223,
      "HH_13": 100.223,
      "HH_14": 100.223,
      "HH_15": 100.223,
      "HH_16": 100.223,
      "HH_17": 100.223,
      "HH_18": 100.223,
      "HH_19": 0,
      "HH_20": 0,
      "HH_21": 0,
      "HH_22": 0,
      "HH_23": 0,
      "HH_24": 0
    }
  ]
}

Jackson μ—μ„œ μ œκ³΅ν•΄μ£ΌλŠ” @JsonUnwrapped μ–΄λ…Έν…Œμ΄μ…˜μ€ 빈 ν΄λž˜μŠ€μ— ν¬ν•¨λœ ν•„λ“œμ— λŒ€ν•΄μ„œ Flattened λ˜μ–΄ 풀어헀쳐진 ν˜•νƒœμ˜ JSON으둜 직렬화할 수 μžˆλŠ” 방법을 μ œκ³΅ν•©λ‹ˆλ‹€. KPX μ€‘κ°œκ±°λž˜ API 쀑 μ˜ˆμΈ‘κ°’ μ œμΆœμ—μ„œλŠ” HH_01 λΆ€ν„° HH_24 κΉŒμ§€ HH_XX ν˜•νƒœλ‘œ μ „λ‹¬λ˜λŠ” μ‹œκ°„ ν•„λ“œ 정보가 ν¬ν•¨λ˜κΈ° λ•Œλ¬Έμ—, λͺ¨λ“  μ‹œκ°„μ„ λ‚˜μ—΄ν•˜κΈ° λ³΄λ‹€λŠ” 각 μ‹œκ°„μ΄ Key둜 κ΅¬μ„±λ˜λŠ” Map으둜 된 ν•„λ“œμ— @JsonUnwrapped λ₯Ό μ‚¬μš©ν•˜λŠ”κ²Œ 쒋을 것 κ°™λ‹€λŠ” 생각을 ν–ˆμŠ΅λ‹ˆλ‹€.

combination not yet supported

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot define Creator property "values" as @JsonUnwrapped: combination not yet supported

μžλ°” 기반 μ½”λ“œμ™€ λ‹€λ₯΄κ²Œ μ½”ν‹€λ¦°μ—μ„œλŠ” μ–΄λ…Έν…Œμ΄μ…˜μ„ κ·ΈλŒ€λ‘œ μ‚¬μš©ν•˜λ©΄ μœ„μ™€ 같은 였λ₯˜κ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. μ½”ν‹€λ¦° λͺ¨λ“ˆ 이슈의 였래된 μ‘°μ–ΈλŒ€λ‘œ μ˜λ„ν•˜λŠ” 뢀뢄을 Prefix둜 μ œκ³΅ν•΄μ•Όν•˜λŠ” 것 κ°™μŠ΅λ‹ˆλ‹€. κ·ΈλŸ¬λ‚˜, @field:JsonUnwrapperd 둜 바꾸어도 JSON λ°μ΄ν„°λ‘œ μ „λ‹¬λ˜λŠ” μš”μ²­μ„ λ³€ν™˜ν•˜λŠ” κ³Όμ •μ—μ„œλŠ” λ™μΌν•œ 였λ₯˜λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. Jackson 은 직렬화λ₯Ό μˆ˜ν–‰ν•˜λŠ” κ³Όμ •μ—μ„œ μƒμ„±μž 그리고 Getter와 Setterλ₯Ό ν™œμš©ν•˜κ²Œ λ©λ‹ˆλ‹€. 그런데 Map 클래슀 νŠΉμ„± 상 Getter와 Setter ν•¨μˆ˜κ°€ μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” νŠΉμ΄ν•œ ν΄λž˜μŠ€κ°€ λ©λ‹ˆλ‹€. κ·Έλž˜μ„œ @JsonAnyGetter 와 @JsonAnySetter와 같은 μ–΄λ…Έν…Œμ΄μ…˜μ˜ 도움이 ν•„μš”ν•˜κ²Œ λ©λ‹ˆλ‹€.

The following declarations have the same JVM signature

Kotlin: Platform declaration clash: The following declarations have the same JVM signature (getValues()Ljava/util/Map;):
    fun `<get-values>`(): MutableMap<String, Double> defined in kr.kdev.mock.kpx.model.ForecastValue
    fun getValues(): Map<String, Double> defined in kr.kdev.mock.kpx.model.ForecastValue

λ¬΄μž‘μ • @JsonAnyGetterλ₯Ό 좔가해버리면 μ½”ν‹€λ¦°μ—μ„œ data 클래슀의 ν•„λ“œλ‘œ 인해 λ§Œλ“€μ–΄μ§€λŠ” Getter ν•¨μˆ˜μ˜ μ‹œκ·Έλ‹ˆμ²˜ 쀑볡 λ¬Έμ œκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. 이 경우 λ‹€μŒκ³Ό 같이 μ€‘λ³΅λ˜λŠ” ν•„λ“œμ— private ν‚€μ›Œλ“œλ₯Ό μ„ μ–Έν•˜λ©΄ λ¬Έμ œμ—μ„œ λ²—μ–΄λ‚  수 μžˆμŠ΅λ‹ˆλ‹€. λ˜ν•œ, μ—°μ‚°μž μ˜€λ²„λ‘œλ”©μœΌλ‘œ 값을 κ°€μ Έμ˜€κ³  μ„€μ •ν•˜λŠ”κ²Œ 더 κ°„νŽΈν•΄μ‘ŒμŠ΅λ‹ˆλ‹€.

ForecastValue.kt
@JsonNaming(PropertyNamingStrategies.UpperSnakeCaseStrategy::class) data class ForecastValue( val cbpGenId: String, val essMtrType: Int = 0, @JsonIgnore // NOTE: @JsonUnwrapped not support map object. private var values: MutableMap<String, Double> = mutableMapOf(), ) { @JsonAnyGetter fun getValues(): Map<String, Double> = values @JsonAnySetter fun setValue( key: String, value: Double, ) { values[key] = value } operator fun get(index: Int): Double = values["HH_%02d".format(index)] ?: 0.0 // EXAMPLE: (1..24).forEach { forecastValue[it] = 0.0 } operator fun set( index: Int, value: Double, ) { values["HH_%02d".format(index)] = value } }

UPPER_SNAKE_CASE 와 @ModelAttribute

KPX μ€‘κ°œκ±°λž˜ APIλŠ” λŒ€λ¬Έμžμ™€ μ–Έλ”μŠ€μ½”μ–΄(_)둜 μ΄λ£¨μ–΄μ§€λŠ” UPPER_SNAKE_CASE둜 ν•„λ“œλͺ…이 κ΅¬μ„±λ˜λŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€. @JsonNaming은 JSON 직렬화 μ‹œ λ³€ν™˜ν•˜λŠ” 방식을 λ³„λ„λ‘œ 지정할 수 있게 μ§€μ›ν•˜λ―€λ‘œ μ•žμ„œ ForecastValue 데이터 ν΄λž˜μŠ€μ— PropertyNamingStrategies.UpperSnakeCaseStrategy κ°€ 적용된 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

Terminal
{"HTTP_CODE":201,"HTTP_STATUS":"CREATED","DATA":{"FORE_SET_GEN_ID":"V001","TRADE_DAY":"20250219","FORECAST_VALUES":[{"CBP_GEN_ID":"A011","ESS_MTR_TYPE":0,"HH_01":0.0,"HH_02":0.0,"HH_03":0.0,"HH_04":0.0,"HH_05":0.0,"HH_06":0.0,"HH_07":0.0,"HH_08":0.0,"HH_09":0.0,"HH_10":0.0,"HH_11":0.0,"HH_12":0.0,"HH_13":0.0,"HH_14":0.0,"HH_15":0.0,"HH_16":0.0,"HH_17":0.0,"HH_18":0.0,"HH_19":0.0,"HH_20":0.0,"HH_21":0.0,"HH_22":0.0,"HH_23":0.0,"HH_24":0.0}]}}

@ModelAttributeλŠ” μŠ€ν”„λ§ 컨트둀러의 ν•Έλ“€λŸ¬ ν•¨μˆ˜ νŒŒλΌλ―Έν„° μ •μ˜ μ‹œ 데이터 바인딩을 μœ„ν•΄μ„œ μ‚¬μš©ν•˜λŠ” μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ 자주 μ‚¬μš©ν•˜λŠ” κ²ƒμž…λ‹ˆλ‹€. 그런데, @ModelAttributeλŠ” JSON 직렬화 방식이 μ•„λ‹ˆλ―€λ‘œ μ§€μ›ν•˜μ§€ μ•ŠμœΌλ―€λ‘œ @ModelAttributeκ°€ μ•„λ‹Œ @RequestParam을 μ‚¬μš©ν•˜μ—¬ UPPER_SNAKE_CASE ν˜•νƒœλ‘œ μ§€μ •ν•΄μ•Όν•©λ‹ˆλ‹€. λ‹€μŒμ€ μ˜ˆμΈ‘κ°’ 제좜 쑰회 API의 μš”μ²­ 쿼리 νŒŒλΌλ―Έν„° 뢀뢄을 μ •μ˜ν•œ 것에 λŒ€ν•œ μ˜ˆμ‹œμž…λ‹ˆλ‹€.

@GetMapping("/forecast/metering")
fun getForecastMetering(
    @RequestParam("FORE_SET_GEN_ID") foreSetGenId: String,
    @DateTimeFormat(pattern = "yyyyMMdd") @RequestParam("TRADE_DAY") date: LocalDate,
): ForecastMeteringResponse {
    
}

KPX μ€‘κ°œκ±°λž˜ API κ°€μ΄λ“œ λ¬Έμ„œλ₯Ό 보면 v1 μ—μ„œ v2둜 λ„˜μ–΄κ°€λ©΄μ„œ UPPER_SNAKE_CASE둜 일괄 λ³€κ²½λœ κ±Έ 인지할 수 μžˆμŠ΅λ‹ˆλ‹€. μ–΄λ–€ μ–Έμ–΄λ‘œ μž‘μ„±λœ μ„œλ²„μΈμ§€λŠ” λͺ¨λ₯΄κ² μœΌλ‚˜ λŒ€λΆ€λΆ„μ˜ 경우 카멜 μΌ€μ΄μŠ€λ₯Ό μ±„νƒν•˜λŠ” κ²½μš°κ°€ λ§Žμ•„μ„œ μžλ°” 개발자 μž…μž₯μ—μ„œλŠ” 생각보닀 λΆˆνŽΈν•œ 뢀뢄이라고 λŠκ»΄μ§€κ²Œ λ©λ‹ˆλ‹€.

이상 끝.