[Spring] ๐Ÿงจ JsonParseException ํ•ธ๋“ค๋ง๊ณผ Request Body ์บ์‹ฑ ์ „๋žต

zzoniยท2025๋…„ 6์›” 24์ผ

์Šคํ”„๋ง

๋ชฉ๋ก ๋ณด๊ธฐ
1/2

๊ด€๋ จ ๊ธ€ - [Spring] ๐Ÿงจ JSON ์—ญ์ง๋ ฌํ™” (JSON ํŒŒ์‹ฑ ์˜ค๋ฅ˜) & Bean Validation ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

โœ๏ธ ์™œ ์ด ์ž‘์—…์„ ํ•˜๊ฒŒ ๋˜์—ˆ์„๊นŒ?

JsonParseException์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ, ๋‹จ์ˆœํžˆ 400 ์—๋Ÿฌ๋งŒ ๋˜์ง€๋Š” ๊ฒƒ๋ณด๋‹ค๋Š”
์–ด๋–ค ์ค„(line)์—์„œ ์–ด๋–ค ๊ฐ’ ๋•Œ๋ฌธ์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ๊ตฌ์ฒด์ ์œผ๋กœ ์‘๋‹ต์— ๋‹ด์•„์ฃผ๊ณ  ์‹ถ์—ˆ๋‹ค.

ํ•˜์ง€๋งŒ, @RequestBody๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ InputStream์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์—
ํ•œ ๋ฒˆ ์ฝํžˆ๋ฉด ๋” ์ด์ƒ ๋‹ค์‹œ ์ฝ์„ ์ˆ˜ ์—†๋‹ค.
๐Ÿ‘‰ ๊ทธ๋ž˜์„œ Request Body๋ฅผ ์บ์‹ฑํ•˜๊ณ , ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•ด๋„ ๋ณธ๋ฌธ ๋‚ด์šฉ์„ ๋‹ค์‹œ ๊บผ๋‚ด ์“ธ ์ˆ˜ ์žˆ๋„๋ก ์ฒ˜๋ฆฌํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ๋‹ค.

์ด ๊ธ€์—์„œ๋Š”

๐Ÿ“Œ ContentCachingRequestWrapper๋ฅผ ์ ์šฉํ•œ Filter ๊ตฌ์„ฑ
ย ย ย ย ย ์›๋ฆฌ ๋ฐ ์ƒ๋ช…์ฃผ๊ธฐ(โ†’์–ธ์ œ ์บ์‹œ ์ •๋ฆฌ!?)
๐Ÿ“Œ @RestControllerAdvice๋ฅผ ํ™œ์šฉํ•œ ์˜ˆ์™ธ ์‘๋‹ต ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

๋“ฑ JsonParseException ์ฒ˜๋ฆฌ์— ๊ด€ํ•œ ์ „์ฒด ํ๋ฆ„์„ ์ •๋ฆฌํ•˜๊ณ , ํ•ด๋‹น ๋‚ด์šฉ์„ ๊ตฌํ˜„ํ•ด๋ณธ๋‹ค.



๐Ÿงจ JsonParseException ์ฒ˜๋ฆฌ

  • JSON ๋ฌธ๋ฒ• ์ž์ฒด๊ฐ€ ์ž˜๋ชป๋˜์–ด ํŒŒ์‹ฑ์ด ๋ถˆ๊ฐ€ํ•œ ๊ฒฝ์šฐ
  • ์ด ์˜ˆ์™ธ ๊ฐ์ฒด๋กœ๋ถ€ํ„ฐ ์–ป์„ ์ˆ˜ ์žˆ๋Š” ์ •๋ณด๋Š” ์ œํ•œ์ ์ด๋ฉฐ,
    • ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๋ผ์ธ(line)๊ณผ ์ปฌ๋Ÿผ(column) ๋ฒˆํ˜ธ
    • ์–ด๋–ค ํ…์ŠคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋‹ค ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ์ •๋ณด ์ •๋„๋งŒ ์ œ๊ณต๋œ๋‹ค.
    • ์˜ˆ์‹œ
      request : { "hi": ""2, }
      error log : JSON parse error: Unexpected character ('2' (code 50)) ... at line: 1, column: 10
      • ์‹ค์ œ ๋กœ๊ทธ ์บก์ฒ˜๋ณธ

๐Ÿงต ์–ด๋–ค text์—์„œ ์˜ˆ์™ธ๊ฐ€ ํ„ฐ์กŒ๋Š”์ง€ ๋ณด์—ฌ์ฃผ๊ณ  ์‹ถ์€๋ฐโ€ฆ?

โ‡’ ์–ด๋А ๋ผ์ธ์—์„œ ํ„ฐ์ง„ ์˜ˆ์™ธ์ธ์ง€ ์ œ๊ณตํ•˜๊ธฐ ์œ„ํ•ด RequestBody ์ถ”์ถœ ์‹œ๋„ โ†’ โŒ๋ถˆ๊ฐ€!โŒ

  • InputStream์„ ํ•œ ๋ฒˆ ์ฝ์œผ๋ฉด ๋‚ด์šฉ์ด ์†Œ์ง„๋˜๊ธฐ ๋•Œ๋ฌธ
    โ‡’ Request Body ์บ์‹ฑ ํ•„์š”



๐ŸงŠ ContentCachingRequestWrapper ์‚ฌ์šฉํ•˜์—ฌ Request Body ์บ์‹ฑ

  • ContentCachingRequestWrapper ๋ž€?
    HttpServletRequest๋ฅผ ๊ฐ์‹ธ๋Š” Wrapper๋กœ,
    InputStream๊ณผ Reader์—์„œ ์ฝ์€ ๋‚ด์šฉ์„ ์บ์‹ฑํ•ด ๋‚˜์ค‘์— ๋ฐ”์ดํŠธ ๋ฐฐ์—ด๋กœ ๋‹ค์‹œ ์ฝ์„ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค€๋‹ค.

  • ๐Ÿ“Œ ์ฆ‰, ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜๋”๋ผ๋„
    ย ย ย ย ย  request.getContentAsByteArray()๋ฅผ ํ†ตํ•ด Request Body๋ฅผ ๋กœ๊ทธ๋กœ ๋‚จ๊ธธ ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋Š” ๊ฒƒ!

  • ๋ฉ”๋ชจ๋ฆฌ ๊ฑฑ์ •์€?
    โ‡’ request์™€ lifecycle์ด ๊ฐ™๊ณ , ์‘๋‹ต์ด ๋๋‚  ๋•Œ ํ•จ๊ป˜ GC ๋Œ€์ƒ์ด ๋˜๋ฏ€๋กœ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๊ฑฑ์ • ํ•„์š” x
    โ‡’ ๋‚˜์ค‘์— ๋ถ€ํ•˜ํ…Œ์ŠคํŠธ๋ฅผ ์ง„ํ–‰ํ•ด๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.


๐Ÿ’ป CachingRequestFilter ๊ตฌํ˜„

๐ŸงŠ ContentCachingRequestWrapper ๋กœ request body ์บ์‹ฑ
CachingRequestFilter ๋ณต์‚ฌ ์ด๊ณณ

  • HTTP ์š”์ฒญ(Request)์˜ Body๋ฅผ ์บ์‹ฑํ•˜๊ธฐ ์œ„ํ•œ ์šฉ๋„
  • ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด ํ•„ํ„ฐ ์ ์šฉ
  • ์ฝ”๋“œ ์„ค๋ช… (line๋ณ„)
    • 5 : HTTP ์š”์ฒญ์ผ ๊ฒฝ์šฐ์—๋งŒ ๋‹ค์Œ ์ž‘์—… ์ง„ํ–‰
    • 6 : request ๊ฐ์ฒด๋ฅผ ContentCachingRequestWrapper๋กœ ๊ฐ์Œˆ
      • InputStream์—์„œ ์ฝ์€ ๋‚ด์šฉ์„ ๋‚ด๋ถ€์— ์บ์‹ฑ โ‡จ ์˜ˆ์™ธ ๋ฐœ์ƒ ํ›„์—๋„ request body ๋‚ด์šฉ์„ ๋‹ค์‹œ ๊บผ๋‚ด์“ธ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ.
    • 7 : ๋‹ค์Œ ํ•„ํ„ฐ๋‚˜ DispatcherServlet์œผ๋กœ ์บ์‹ฑ๋œ request ๊ฐ์ฒด ์ „๋‹ฌ
      • ๋‹ค์Œ ํ•„ํ„ฐ๊ฐ€ ์žˆ์Œ โ†’ ๋‹ค์Œ ํ•„ํ„ฐ๊ฐ€ ์‹คํ–‰๋จ (Filter Chain ์ค‘๊ฐ„ ๋‹จ๊ณ„)
      • ์ด ํ•„ํ„ฐ๊ฐ€ ๋งˆ์ง€๋ง‰ ํ•„ํ„ฐ โ†’ DispatcherServlet์œผ๋กœ ์ œ์–ด๋ฅผ ๋„˜๊น€ (Spring MVC ์ง„์ž…)
  • ์œ ์˜! ๋ฐ˜๋“œ์‹œ ๊ฐ€์žฅ ์•ž๋‹จ ํ•„ํ„ฐ์— ๋“ฑ๋กํ•ด์•ผ ํšจ๊ณผ โญ•๏ธ, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด.. ์ด๋ฏธ body๊ฐ€ ๋‹ค ์†Œ์ง„๋  ์œ„ํ—˜.
  • servletContext์— ํ•„ํ„ฐ ๋“ฑ๋ก - ํ•„ํ„ฐ ๋“ฑ๋ก ์ฝ”๋“œ ๋ณต์‚ฌ ์ด๊ณณ
    • 6 : servletContext.addFilter๋กœ ์œ„์—์„œ ๊ตฌํ˜„ํ•œ ์บ์‹ฑํ•„ํ„ฐ ๋“ฑ๋ก
    • 7 : "/*" - ๋ชจ๋“  ์š”์ฒญ์— ๋Œ€ํ•ด ํ•„ํ„ฐ ์ ์šฉ





๐Ÿš— JsonErrorControllerAdvice ๊ตฌํ˜„

JsonErrorControllerAdvice ๋ณต์‚ฌ ์ด๊ณณ

  • ์ฝ”๋“œ ์„ค๋ช… (line ๋ฐ ๋ฉ”์†Œ๋“œ๋ณ„)
    • 1 : @RestControllerAdvice(annotations = RestController.class)
      • ์˜ค์ง @RestController์— ์ ์šฉ + ๋ชจ๋“  ์‘๋‹ต์„ JSON ํ˜•์‹์œผ๋กœ ๋ฐ˜ํ™˜
    • 12 : getRequestBody ๋ฉ”์„œ๋“œ
      • ์š”์ฒญ ๋ณธ๋ฌธ์„ ContentCachingRequestWrapper ๋ฅผ ํ†ตํ•ด ์ถ”์ถœ
        ํ•„ํ„ฐ์—์„œ ์บ์‹ฑํ•ด๋‘” ๋ฐ”์ดํŠธ ๋ฐฐ์—ด์„ ๋‹ค์‹œ ์ฝ์–ด ๋ฌธ์ž์—ด๋กœ ๋ณต์›
    • 31 : extractErrorLineFromJson ๋ฉ”์„œ๋“œ
      • JsonParseException์˜ getLocation()์„ ํ†ตํ•ด
        ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ line ๋ฒˆํ˜ธ๋ฅผ ์ถ”์ถœ
      • ์ถ”์ถœํ•œ ์š”์ฒญ ๋ณธ๋ฌธ์—์„œ ํ•ด๋‹น ๋ผ์ธ์„ ๊ฐ€์ ธ์™€ ์—๋Ÿฌ ์‘๋‹ต์— ํ•จ๊ป˜ ๋‹ด์Œ
    • ์—๋Ÿฌ์‘๋‹ต : JsonSyntaxError ๋Š” ์ปค์Šคํ…€ ํด๋ž˜์Šค๋กœ ์—๋Ÿฌ๋ผ์ธ๋ฒˆํ˜ธ, ์—๋Ÿฌ๋ผ์ธ ๋ฌธ์ž์—ด, ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ด์Œ
      - ์˜ˆ์‹œ
      request body : { "hi" : 1
      response body :
      {
         "lineNumber": 1,
         "rejectValue": "{ \"hi\" : 1",
         "message": "JSON ๋ฌธ๋ฒ• ์˜ค๋ฅ˜ - Unexpected end-of-input: expected close marker for Object (start marker at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 1])"
      }


๋งˆ๋ฌด๋ฆฌ

์ด๋ ‡๊ฒŒ JsonParseException์ด ๋ฐœ์ƒํ–ˆ์„ ๋•Œ, ์˜ค๋ฅ˜๊ฐ€ ๋‚œ ๋ผ์ธ๊ณผ ๊ทธ ๋‚ด์šฉ์„ ์‘๋‹ต์— ๋‹ด์•„์ฃผ๋Š” ์ž‘์—…์„ ์ง์ ‘ ๊ตฌํ˜„ํ•ด๋ณด์•˜๋‹ค.
๊ตฌ๊ธ€์— ๋– ๋‹ค๋‹ˆ๋Š” Exception Handler ์ฝ”๋“œ๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐ์—๋Š” ์ต์ˆ™ํ–ˆ์ง€๋งŒ, ์ด๋ ‡๊ฒŒ ์‹ค์ œ ์˜ˆ์™ธ ๊ตฌ์กฐ๋ฅผ ๋œฏ์–ด๋ณด๊ณ  ๊ฐœ์„ ํ•ด๋ณธ ๊ฒฝํ—˜์€ ์ฒ˜์Œ์ธ ๊ฒƒ ๊ฐ™๋‹ค.
์ด ์ž‘์—…์ด ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์—๊ฒŒ๋Š” ๋””๋ฒ„๊น…์„ ๋” ์ˆ˜์›”ํ•˜๊ฒŒ, ์„œ๋ฒ„ ์ธก์—์„œ๋Š” ์˜ค๋ฅ˜ ์ƒํ™ฉ์„ ๋ณด๋‹ค ๋ช…ํ™•ํžˆ ๊ธฐ๋กํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐ์— ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ž€๋‹ค.
์ž์ž˜ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ์‹ค์šฉ์ ์ธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ํ•˜๋‚˜๊ฐ€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๊ณผ ํ˜‘์—…์˜ ํ’ˆ์งˆ์„ ๋ฐ”๊พธ๋Š” ์‹œ์ž‘์ ์ด ๋  ์ˆ˜ ์žˆ๋‹ค๊ณ  ๋ฏฟ๋Š”๋‹ค.

๊ธ€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค!! ํ‹€๋ฆฐ ๋‚ด์šฉ์ด๋‚˜ ๋” ๋‚˜์€ ๋ฐฉ์‹์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ํŽธํ•˜๊ฒŒ ์•Œ๋ ค์ฃผ์„ธ์š” ๐Ÿ˜Š

0๊ฐœ์˜ ๋Œ“๊ธ€