Spring @RequestBody ํ๋ ๋ฐ์ธ๋ฉ์ด ๋์ง ์๋ ์ด์ (feat. Lombok, Jackson์ ๋ค์ด๋ฐ ์ฐจ์ด)
๊ฐ์
Spring์์ ํด๋ผ์ด์ธํธ์ ์์ฒญ์ JSON ํ์์ผ๋ก ๋ฐ๊ณ , @RequestBody๋ฅผ ์ฌ์ฉํ์ฌ JSON body์ ๋ฐ์ดํฐ๋ฅผ Java Object์ ํ๋์ ์ญ์ง๋ ฌํ(Deserialize)๋ฅผ ํตํด ๊ฐ ๋ฐ์ดํฐ๊ฐ ์ ์์ ์ผ๋ก ๋ฐ์ธ๋ฉ์ด ์ด๋ฃจ์ด์ง๊ฒ ๋ฉ๋๋ค.
ํ์ง๋ง, ํ๋์ ํน์ ๋ค์ด๋ฐ + Lombok์ @Getter๋ฅผ ์ฌ์ฉํ ๋ ๋ฐ์ธ๋ฉ์ด ์ ์์ ์ผ๋ก ๋์ง ์๋ ์ด์๊ฐ ์กด์ฌํ๋๋ฐ์, ์ด์ ๊ด๋ จํ์ฌ ๊ฐ๋ตํ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
### Send POST request with json body
POST http://localhost:8080/api/v1/test
Content-Type: application/json
{
"pId": "pId",
"poId": "poId"
}
์๋ฅผ ๋ค์ด, ์์ ๊ฐ์ ์๋ฒ ์ฝ๋๊ฐ ์กด์ฌํ๊ณ ํด๋ผ์ด์ธํธ์์ ์ JSON ํ์์ผ๋ก body ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ฉด,
์๋ฒ์ TestBody Object์ pId, poId๊ฐ ์ ์์ ์ผ๋ก ๋ฐ์ธ๋ฉ์ด ๋ฉ๋๋ค.
ํ์ง๋ง, ์์ ๊ฒฝ์ฐ pId๋ null์ด ๋๊ณ , poId๋ง ์ ์์ ์ผ๋ก ๋ฐ์ธ๋ฉ์ด ๋๋๋ฐ์ ์ด์ ๊ด๋ จํด์ ์ pId๋ ๋ฐ์ธ๋ฉ์ด ๋์ง ์๋์ง ์ดํด๋ณด๊ฒ ์ต๋๋ค.
@RequestBody์ ๋์๊ณผ์ ๊ด๋ จํด์๋ RequestResponseBodyMethodProcessor ํด๋์ค์ resolveArgument() ๋ฉ์๋๋ฅผ ํตํด JSON ๋ฐ์ดํฐ์ Java Object ๊ฐ ๋ฐ์ธ๋ฉ์ด ๋๋๋ฐ์, ์ด๋ฒ ํฌ์คํ ์์๋ ์์ธํ ์ค๋ช ์ ์๋ตํ๊ฒ ์ต๋๋ค.
(๊ถ๊ธํ์๋ค๋ฉด ์๋์ ๋น์ทํ๊ฒ ๋ธ๋ ์ดํฌ ํฌ์ธํธ๋ฅผ ๊ฑธ๊ณ , ๋ฐ๋ผ ๋ค์ด๊ฐ๋ณด์๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค. ๐ )
๋ถ์
์์๋๋ก๋ ์๋์ง๋ง, ๋๋ต์ ์ผ๋ก ์ ํด๋์ค๋ค์ ๋ํด ๋ธ๋ ์ดํฌ ํฌ์ธํธ๋ฅผ ๊ฑธ์ด๋๊ณ ๋ค์ด๊ฐ๋ค ๋ณด๋ฉด,
POJOPropertiesCollector ํด๋์ค์์ Object์ field๋ฅผ ์ฐพ์ ์ค์ ํ๋ ๋ถ๋ถ์ด ์๋๋ฐ ์ฌ๊ธฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
@RequestBody์ JSON <-> Object ์ญ์ง๋ ฌํ๋ ๋๋ต์ ์ผ๋ก ์๋์ ๊ฐ์ ํ๋ฆ์ผ๋ก ๋ค์ด๊ฐ๊ฒ ๋ฉ๋๋ค.
AbstractJackson2HttpMessageConveter > ObjectMapper > Deserializer... > BeanDeserializerFactory > ... > POJOPropertiesCollector > DefaultAccessorNamingStrategy > ...
๊ทธ๋ผ ์์ ํด๋์ค๋ ์ผ๋ถ ์๋ตํ๊ณ , POJOPropertiesCollector ํด๋์ค๋ถํฐ ๊ฐ๋ตํ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
POJOPropertiesCollector
collectAll()
collectAll() ๋ฉ์๋์ ์ฃผ์๋ ๋ง๊ณ , ์ด๊ฒ์ ๊ฒ ๋ฉ์๋๋ค๋ ๋ง์๋ฐ์
์์์ Java Object ์ ๋ฐ์ดํฐ๋ฅผ ์ค์ ํ๋ ๋ฉ์๋๊ฐ _addFields(props), _addMethods(props) ๋ฑ์ ๋๋ค.
์์์ _addFields(props) ๋ฉ์๋๋ฅผ ๊ฑฐ์น๋ฉด์ pId, poId ํ๋๊ฐ ์ค์ ์ด ๋์์ง๋ง,
์ด์ํ๊ฒ๋ _addMethods(props) ๋ฉ์๋๋ฅผ ๊ฑฐ์น๋ฉด "pid"๋ผ๋ ์ ์ ์๋ ํ๋๊ฐ ํ๋ ๋ ์ถ๊ฐ๊ฐ ๋ฉ๋๋ค.
(poId ํ๋์ ๋ค๋ฅด๊ฒ pId, pid๋ getter, setter๋ null์ธ ๊ฒ์ ๋ณผ ์ ์์ต๋๋ค.)
๊ทธ๋ผ _addMethods(props)์ ๋ฌธ์ ๊ฐ ์๋ ๊ฒ ๊ฐ์ผ๋, ๋ด๋ถ ์ฝ๋๋ฅผ ์กฐ๊ธ ๋ ๋ณด๊ฒ ์ต๋๋ค.
_addMethods(Map<String, POJOPropertyBuilder> props)
ํ์ฌ ๋ฉ์๋๊ฐ getter์ด๋ฉด _addGetterMethod()๋ฅผ ํธ์ถํ๊ณ , setter์ด๋ฉด _addSetterMethod()๋ฅผ ํธ์ถํฉ๋๋ค.
์ค์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ํ๋ "pId"์ ๋ํด Lombok์ด ์์ฑํ๋ Getter ๋ฉ์๋์ ๋ค์ด๋ฐ์ getPId()์ด๊ณ ,
์ด์ด์ _addGetterMethod()๋ฅผ ํธ์ถํฉ๋๋ค.
_addGetterMethod(Map<String, POJOPropertyBuilder> props, AnnotatedMethod m, AnnotationIntrospector ai)
_addGetterMethod()์์ ๋ฉ์๋์ ๋ค์ด๋ฐ์ ๊ฐ์ ธ์ค๋๋ฐ์,
ํ์ฌ ๋ก์ง์์๋ _accessorNaming.findNameForRegularGetter(m, m.getName())๋ฅผ ํธ์ถํฉ๋๋ค.
DefaultAccessorNamingStrategy
findNameForRegularGetter(AnnotatedMethod am, String name)
์์ findNameForRegularGetter() ๋ฉ์๋์์๋ ๋ง์ง๋ง return์์ legacyManglePropertyName() ๋ฉ์๋๋ฅผ ํธ์ถํฉ๋๋ค.
legacyManglePropertyName(final String basename, final int offset)
์ 159๋ผ์ธ๋ถํฐ ์์๋๋ ์ฝ๋๊ฐ Object์ ํ๋๋ช ์ ์์ฑํ๋ ๋ถ๋ถ์ ๋๋ค.
ํ๋๋ช ์ pId์ด์ง๋ง, 169๋ผ์ธ์ append() ๋ฉ์๋๋ฅผ ๋ณด๋ฉด ์๋ฌธ์๋ก ๋ณ๊ฒฝํ ๋ฌธ์๋ฅผ ๋ฃ๊ธฐ ๋๋ฌธ์ I๊ฐ ์๋ i๋ฅผ ์ ์ฅํ์ฌ ํ๋๋ช ์ ๋ง๋ค์ด๋ด๊ณ , ์ด๋ก์ธํด ํธ์ถ๋ถ์ธ POJOPropertiesCollector#_addGetterMthod()์์๋ pid๋ฅผ ๋ฐ์ธ๋ฉํ์ฌ ์ ์ฅํ๊ฒ ๋ฉ๋๋ค.
์๋ํ์ง ์์ pid๊ฐ ์ถ๊ฐ๋ก ํ๋กํผํฐ์ ์ ์ฅ์ด ๋๊ณ , ์ดํ _removeUnwantedProperties(props) ๋ฉ์๋์์๋ ํน์ ์กฐ๊ฑด(visible)์ ๋ฐ๋ผ pId๋ฅผ ํ๋์์ ์ ๊ฑฐํ์ฌ ์๋ํ์ง ์์ "pid" ๋ผ๋ ๋ค์ด๋ฐ์ ๊ฐ์ง ํ๋๊ฐ ์ ์ฅ๋์ด์ ๊ธฐ์กด์ "pId" ๋ค์ด๋ฐ์ ๊ฐ์ง ํ๋๋ null ์ฒ๋ฆฌ๊ฐ ๋๋ ๊ฒ์ผ๋ก ๋ณด์ ๋๋ค.
๊ทธ๋ผ ์ ์์ ๊ฐ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ ๊น์?
๊ฒฐ๋ก ๋ถํฐ ๋ง์๋๋ฆฌ๋ฉด Lombok์์ ์์ฑํ๋ getter/setter ๋ฉ์๋์ ๋ค์ด๋ฐ๊ณผ, Jackson์์ ์ธ์ํ๋ getter/setter ๋ฉ์๋์ ๋ค์ด๋ฐ์ด ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋ฐ์ํ๋ ๋ฌธ์ ์ ๋๋ค.
์ผ๋ฐ์ ์ธ ๊ฒฝ์ฐ์๋ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์ง ์์ง๋ง, "pId"์ ๊ฒฝ์ฐ getter์ ๋ค์ด๋ฐ์ด getPId()๊ฐ ๋์ด์ผ ํ ๊ฒ ๊ฐ์ง๋ง(์ค์ Lombok์ ๋ค์ด๋ฐ), IntelliJ์์๋ getpId()๋ก ๋ง๋ค์ด ์ฃผ๊ณ , Jackson์์๋ getpId()์ ๊ฒฝ์ฐ์๋ง ์ธ์ํ๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํฉ๋๋ค.
์ผ๋ฐ์ ์ผ๋ก ๋ฐ์ํ๋ ์ผ์ด์ค๋ ์๋๊ณ , ์์ ๊ฒฝ์ฐ ์ฒ๋ผ getter/setter์ ๋ค์ด๋ฐ์ด ์ฐ์๋ 2๊ฐ์ ๋๋ฌธ์๊ฐ ๋ ๋ ๋ฐ์ํ๋ ๊ฒ ๊ฐ์ต๋๋ค.
(aBc, pId, cId ...)
์ค์ ํ ์คํธ ํด๋ณด๊ธฐ
์ ํ ์คํธ๋ฅผ ๋ณด์๋ฉด ์์๊ฒ ์ง๋ง, Lombok๊ณผ IntelliJ๊ฐ ๋ง๋ค์ด๋ด๋ getter์ ๋ค์ด๋ฐ์ ์๋ก ๋ค๋ฆ ๋๋ค.
JavaBeans์ ์ปจ๋ฒค์
์ ์ด๋ฏธ์ง๋ Oracle์ ์กด์ฌํ๋ JavaBeans ๊ท์ฝ์์ 8.8 Capitalization of inferred names. ์ค ์ผ๋ถ ๋ฌธ์ฅ์ ๋๋ค.
๋ง์ง๋ง ๋ฌธ์ฅ์์ "๊ฐ์ฅ ์ฒ์ ๋ ๋ฌธ์๊ฐ ์ฐ์์ผ๋ก ๋๋ฌธ์๋ผ๋ฉด, ๊ทธ๋๋ก ๋๋ค"๋ผ๊ณ ๋์์๋๋ฐ์, ์ด๋ฌํ ์ด์ ๋๋ฌธ์
pId์ ๊ฒฝ์ฐ JavaBeans ๊ท์ฝ์ ๋ฐ๋ฅด๋ฉด IntelliJ๊ฐ ๋ง๋ค์ด๋ด๋ getpId()๊ฐ ๋ง๋ ๋ค์ด๋ฐ์ด๋ผ๊ณ ๋ณผ ์ ์์ต๋๋ค.
(์ ๋ getPId()๋ผ๊ณ ์๊ฐํ์๋๋ฐ, ์ด์ ๊ด๋ จํ์ฌ stackoverflow ์๋ ์ฌ๋ฌ ์๊ฒฌ๋ค์ด ์กด์ฌํฉ๋๋ค.)
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์์ ๊ฐ์ ํ๋์ ๋ค์ด๋ฐ์ ๋ํด์ Lombok์ ์ฌ์ฉํ๋ฉด JavaBeans์๋ ๋ค๋ฅด๊ธฐ ๋๋ฌธ์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋๋ฐ์,
์ผ๋ฐ์ ์ธ ์ํฉ์ ์๋์ง๋ง ๋ง์ Java ๊ฐ๋ฐ์๋ค์ด Lombok์ ์ฌ์ฉํ๋ ๊ฒ์ผ๋ก ๋ฏธ๋ฃจ์ด๋ณด๋ฉด, ๋์ผํ ์ด์๋ฅผ ๊ฒช์๋ ์ฌ๋์ด ์กด์ฌํ์ ๊ฒ ๊ฐ์๋ฐ์, ์ฐพ์๋ณด๋ ์ญ์ ์กด์ฌํ์ต๋๋ค.
https://github.com/FasterXML/jackson-databind/issues/3538
field name changed when deserializing bean · Issue #3538 · FasterXML/jackson-databind
Describe the bug A clear and concise description of what the bug is. RequestBody annotation bean with double upper word,filed will change. for example,bean with field name qAExportFieldList,when de...
github.com
์ ์ด์์์ ๋ง์ง๋ง ์ฝ๋ฉํธ๋ฅผ ๋ณด๋, (์ ๊ฐ ์ดํดํ ๊ฒ ๋ง๋ค๋ฉด) ์ด๋ฌํ ๊ฒฝ์ฐ, ์ฌ์ฉ์๊ฐ ๋นํ์ค์ ์ธ(JavaBeans์ ๋ค์ด๋ฐ ๊ท์ฝ์ ๋ฐ๋ฅด์ง ์๋)
ํ๋๋ช ๊ณผ getter/setter๊ฐ ์ฐ๊ฒฐ(๋ฐ์ธ๋ฉ?) ๋๋๋ก ์ด๋ ธํ ์ด์ ์ ์ฌ์ฉํด์ผ ํ๊ณ , ์ด๋ฅผ ํตํด Jackson์ด ํ๋กํผํฐ ๋ช ์ ์ธ์ํ ์ ์๋๋ก ์ฌ์ฉํด์ผ ํ๋ค๋ ๊ฒ์ 95%์ ๋ ํ์ ํ๋ค๊ณ ๋ง์ ํ๋ ๊ฒ ๊ฐ๋ค์.
์ cowtowncoder๊ฐ ๋จ๊ธด ๋ด์ฉ์ ์๋ชป ์ดํดํ์ฌ ๊ด๋ จ ์ด์๋ฅผ ๋ฑ๋กํด๋๊ธด ํ๋๋ฐ, ํด๋น ์ด์๊ฐ ๋ฐ์๋์ด ์์ ์ด ๋๋๊ฒ์ ์ฝ์ง ์์ ๋ณด์ด๋ค์.. ใ ใ
https://github.com/FasterXML/jackson-databind/issues/4421
Problem with property name on deserialization wrt non-standard property names · Issue #4421 · FasterXML/jackson-databind
(related to earlier issue #3538) Describe the bug If the field name is pId, the JSON data is not mapped to an object using @RequestBody annotation. Lombok's @Getter, @Setter annotation, the naming ...
github.com
ํด๊ฒฐ๋ฒ
๋ด์ฉ์ ๊ต์ฅํ ๊ธธ์ง๋ง, ์ฌ์ค ํด๊ฒฐ๋ฒ์ ๊ต์ฅํ ๊ฐ๋จํฉ๋๋ค.
1. @JsonProperty ์ด๋ ธํ ์ด์ ์ฌ์ฉ (JacksonAnnotationIntrospector ํด๋์ค์์ ์ฌ์ฉ)
2. Lombok ์ด๋ ธํ ์ด์ ๋์ , IDE๊ฐ ๋ง๋ค์ด์ฃผ๋ getter/setter ์ฌ์ฉ
3. ํ๋์ ๋ค์ด๋ฐ์ ๋ณ๊ฒฝ(pId -> poId) ํ๋ฉด ์ ์์ ์ผ๋ก ๋ฐ์ธ๋ฉ์ด ์ ๋ฉ๋๋ค.