Handling Null in URL

About

API๋ฅผ ๊ตฌํ˜„ํ•˜๋‹ค๋ณด๋ฉด /posts?type=notice ์˜ ?type=notice, /posts/{id}, ์—์„œ id์ฒ˜๋Ÿผ URL์„ ํ†ตํ•ด ๊ฐ’์„ ์ „๋‹ฌํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ๋‹ค.

Spring MVC์—์„œ๋Š” ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์š”์ฒญ์œผ๋กœ ๋“ค์–ด์˜จ ํ•ด๋‹น ๊ฐ’๋“ค์„ ๊ฐ๊ฐ @RequestParam, @PathVariable์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ๋‹ค.

@RequestParam

Java

Spring์„ ์ž๋ฐ”๋กœ ์ž‘์„ฑํ•  ๋•Œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

PostController.java
@GetMapping("/posts")
public ResponseEntity<List<PostResponse>> requestParam(
    @RequestParam(defaultValue = "post", required = false) String type) {

    return ResponseEntity.ok()
        .body(postSearchService.search(type));
}

Kotlin

์ฝ”ํ‹€๋ฆฐ์—์„œ๋Š” ์–ธ์–ด ์ฐจ์›์—์„œ ํƒ€์ž… ๋’ค์— ?๋ฅผ ๋ถ™์ด๋Š” ๊ฒƒ์œผ๋กœ nullable ํƒ€์ž…๊ณผ non-nullable ํƒ€์ž…์„ ๊ตฌ๋ถ„ํ•˜๊ณ  (์™„์ „ํžˆ ๋‹ค๋ฅธ ํƒ€์ž…์ด ๋œ๋‹ค), ํ•จ์ˆ˜์—์„œ ๋””ํดํŠธ ๊ฐ’์„ ์ง€์›ํ•œ๋‹ค. ๋””ํดํŠธ ๊ฐ’์€ ์ด๋Ÿฐ ์‹์œผ๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๋‹ค.

fun speak(sentence: String = "Bow wow!") {
    println(sentence)
}

๊ทธ๋ ‡๋‹ค๋ฉด ์Šคํ”„๋ง์—์„œ๋„ ์ด ๊ธฐ๋Šฅ์„ ์‘์šฉํ•ด์„œ PostController ์˜ˆ์ œ๋ฅผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ž‘์„ฑํ•˜๋ฉด ์–ด๋–จ๊นŒ?

PostController.kt
@GetMapping("/posts")
fun requestParam(
    @RequestParam type: String? = "post"
): ResponseEntity<List<PostResponse>> {
    return ResponseEntity.ok()
        .body(postSearchService.search(type))
}

๊ทธ๋Ÿฌ๋‚˜ ์ด๋ ‡๊ฒŒ ํ•˜๊ฒŒ ๋˜๋ฉด ์‹ค์ œ๋กœ๋Š” type์—๋Š” null์ด ๋“ค์–ด๊ฐ„๋‹ค.

Spring์˜ ArgumentResolver (RequestParamArgumentResolver)๋ฅผ ๊ฑฐ์น˜๋ฉฐ, type์— ์• ์ดˆ์— null์ด ๋“ค์–ด๊ฐ„ ์ฑ„๋กœ ์‹œ์ž‘ํ•˜๋ฏ€๋กœ, ๋””ํดํŠธ ๊ฐ’์ด ์‚ฝ์ž…๋˜์ง€ ์•Š๋Š” ๊ฒƒ์ด๋‹ค.

๋”ฐ๋ผ์„œ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ง„ํ–‰ํ•˜๋ฉด ๋œ๋‹ค.

PostController.kt
@GetMapping("/posts")
fun requestParam(
    @RequestParam(defaultValue = "post", required = false) type: String?
): ResponseEntity<List<PostResponse>> {
    return ResponseEntity.ok()
        .body(postSearchService.search(type))
}

required = true

๋˜ ํ•˜๋‚˜ ์งš๊ณ  ๋„˜์–ด๊ฐˆ ๋ถ€๋ถ„์ด ์žˆ๋‹ค. required ์˜ต์…˜์— ๊ด€ํ•œ ๋‚ด์šฉ์ด๋‹ค. required์™€ defaultValue ์œ ๋ฌด์˜ ์กฐํ•ฉ์œผ๋กœ 4๊ฐ€์ง€๊ฐ€ ๋‚˜์˜ค๋Š”๋ฐ, ๊ฐ ์ƒํ™ฉ์—์„œ ์˜ค๋ฅ˜๋ฅผ ์ผ๋ถ€๋Ÿฌ ๋ฐœ์ƒ์‹œ์ผœ๋ณด์ž. (500์ด ๋‚˜์˜ค๋ฉด ์›์น˜ ์•Š๋Š” ์ƒํ™ฉ์ด ๋˜๋Š”๊ฑฐ๋‹ค!)

1๋ฒˆ ์ƒํ™ฉ - defaultValue ์—†์Œ, required=true, non-nullable

type์˜ ๋””ํดํŠธ ๊ฐ’์„ ์—†์• ๊ณ , ๋ฐ˜๋“œ์‹œ ์ „๋‹ฌ๋ฐ›๋„๋ก ํ•˜๊ณ  ์‹ถ๋‹ค. required=true๋ฅผ ๋„ฃ๊ณ  non-nullable ํƒ€์ž…์œผ๋กœ ํ•˜๋ฉด ๋ ๊นŒ?

@RequestParam(required = true) type: String

์ผ๋ถ€๋Ÿฌ ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•ด ์ด๋ ‡๊ฒŒ ๋””ํดํŠธ๊ฐ’์„ ์•ˆ๋„ฃ๊ณ  ์š”์ฒญํ•ด๋ณด์ž.

curl -i http://localhost:8080/posts

์ž, ์ด๋ ‡๊ฒŒ ์ „๋‹ฌํ•˜๋ฉด 500์ด ์•„๋‹Œ 400 Bad Request๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.

โœ… ๋ฐ”๋žŒ์งํ•˜๋‹ค! ๋‚˜๋จธ์ง€ ์ƒํ™ฉ๋„ ๊ณ„์†ํ•ด์„œ ํ™•์ธํ•ด๋ณด์ž.

2๋ฒˆ ์ƒํ™ฉ - defaultValue ์—†์Œ, required=false, nullable

@RequestParam(required = false) type: String?

type์„ ์ „๋‹ฌํ•˜์ง€ ์•Š์œผ๋ฉด null์ด ๋“ค์–ด๊ฐ€๊ณ , type์„ ์ „๋‹ฌํ•˜๋ฉด ํ•ด๋‹น ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™”๋œ๋‹ค. ๋‘˜ ๋‹ค ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

โœ… ๋ฐ”๋žŒ์งํ•˜๋‹ค.

3๋ฒˆ ์ƒํ™ฉ - defaultValue ์žˆ์Œ, required=true, non-nullable

@RequestParam(defaultValue = "post", required = true) type: String

type์„ ์ „๋‹ฌํ•˜์ง€ ์•Š์œผ๋ฉด ๋””ํดํŠธ ๊ฐ’ "post"๊ฐ€ ๋“ค์–ด๊ฐ€๊ณ , type์„ ์ „๋‹ฌํ•˜๋ฉด ํ•ด๋‹น ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™”๋œ๋‹ค. ๋‘˜ ๋‹ค ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

โœ… ๋ฐ”๋žŒ์งํ•˜๋‹ค.

4๋ฒˆ ์ƒํ™ฉ - defaultValue ์žˆ์Œ, required=false, nullable

@RequestParam(defaultValue = "post", required = false) type: String?

3๋ฒˆ ์ƒํ™ฉ๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค. ๋‹ค๋งŒ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ํ•ด๋‹น ๊ฐ’์„ ์‚ฌ์šฉํ•  ๋•Œ null์ด ๋“ค์–ด๊ฐ€์ง€ ์•Š์•˜์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  null ์ฒดํฌ๋ฅผ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

๐Ÿซค ๋ถˆํŽธํ•˜๋‹ค.

์ž˜ ๋˜๋Š” ์˜ˆ์ œ๋ฅผ ์•Œ์•„๋ดค์œผ๋‹ˆ ์ด์ œ required์™€ nullable์„ ๋‹ค๋ฅด๊ฒŒ ๋งŒ ํ˜ผ์ข… ์ผ€์ด์Šค๋ฅผ ์•Œ์•„๋ณด์ž. (๊ถ๊ธˆํ•˜๋‹ˆ๊นŒ!)

5๋ฒˆ ์ƒํ™ฉ - defaultValue ์—†์Œ, required=true, nullable

@RequestParam(required = true) type: String?

4๋ฒˆ ์ƒํ™ฉ๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋ฉฐ, ํ•ด๋‹น ๊ฐ’์„ ์‚ฌ์šฉํ•  ๋•Œ null์ด ๋“ค์–ด๊ฐ€์ง€ ์•Š์•˜์Œ์—๋„ null ์ฒดํฌ๋ฅผ ํ•ด์ค˜์•ผ ํ•œ๋‹ค.

๊ทผ๋ฐ ๋ฌธ์ œ๊ฐ€ ์žˆ๋‹ค. required=true์ธ๋ฐ type์„ ์ „๋‹ฌํ•˜์ง€ ์•Š์•„๋„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

ํ•„์ˆ˜๊ฐ’์œผ๋กœ ๋งˆํ‚นํ•˜๊ธฐ ์œ„ํ•ด required=true๋กœ ์„ค์ •ํ•œ ๊ฒƒ์ธ๋ฐ ํ•„์ˆ˜๊ฐ’ ์ฒดํ‚น์ด ์•ˆ๋œ๋‹ค๋‹ˆ...

โŒ ๋”์ฐํ•˜๋‹ค.

6๋ฒˆ ์ƒํ™ฉ - defaultValue ์—†์Œ, required=false, non-nullable

@RequestParam(required = false) type: String

type์„ ์ „๋‹ฌํ•˜๋ฉด ๋ฌธ์ œ ์—†์ด ๋™์ž‘ํ•œ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์ „๋‹ฌํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด NullPointerException์ด ๋ฐœ์ƒํ•œ๋‹ค.

java.lang.NullPointerException: Parameter specified as non-null is null: method com.litsynp.kotring.interfaces.PostController.requestParam, parameter type

๊ทธ๋ฆฌ๊ณ  500 Error๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค.

โŒ ์‹ค์ˆ˜๊ฐ€ ์•„๋‹ˆ๋ผ๋ฉด ์ด๋Ÿด ์ผ ์—†๊ฒ ์ง€๋งŒ ๋‹น์—ฐํžˆ ์ด๋Ÿฌ๋ฉด ์•ˆ๋œ๋‹ค.

7๋ฒˆ ์ƒํ™ฉ - defaultValue ์žˆ์Œ, required=true, nullable

@RequestParam(defaultValue = "post", required = true) type: String?

5๋ฒˆ๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•˜๋ฉฐ ๋ฌธ์ œ๋„ ๊ฐ™๋‹ค๊ณ  ํ•  ์ˆ˜ ์žˆ. ๋””ํดํŠธ๊ฐ’๋„ ์žˆ๊ณ  required=true์ธ๋ฐ ๊ตณ์ด nullable ํƒ€์ž…์„ ์“ธ ํ•„์š”๊ฐ€ ์žˆ์„๊นŒ?

๐Ÿซค ๋ฐ”๋žŒ์งํ•˜์ง€ ์•Š๋‹ค.

8๋ฒˆ ์ƒํ™ฉ - defaultValue ์žˆ์Œ, required=false, non-nullable

@RequestParam(defaultValue = "post", required = false) type: String

defaultValue๊ฐ€ ์žˆ๊ณ , required=false์ด๋‹ค. type์€ non-nullable์ด๋‹ค.

๐Ÿค” ์˜๋„ํ•œ ๋Œ€๋กœ๋Š” ๋™์ž‘ํ•œ๋‹ค...๋งŒ defaultValue๋งŒ ๋น ๋œจ๋ ค๋„ 6๋ฒˆ์ด๋ž‘ ๋˜‘๊ฐ™์•„์ง„๋‹ค. ์ฃผ์˜ํ•ด์•ผ๊ฒ ๋‹ค.

๊ฒฐ๋ก 

defaultValue๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜์ž.

  • required=true(๋˜๋Š” false - ์˜ค๋ฅ˜๋Š” ์•ˆ๋‚˜์ง€๋งŒ ์ฃผ์˜ ํ•„)

  • non-nullable (e.g., String)

defaultValue๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ์—” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•˜์ž.

  • required=false

  • nullable (e.g., String?)

@PathVariable

@PathVariable ์—๋„ required ์˜ต์…˜์œผ๋กœ null checking์„ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

Java

๋ณดํ†ต @PathVariable์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•œ๋‹ค.

@GetMapping("/posts/{id}")
public ResponseEntity<PostResponse> pathVariable(
    @PathVariable String id) {

    return ResponseEntity.ok()
        .body(postService.findById(id));
}

์‚ฌ์‹ค ์œ„์˜ ์˜ˆ์ œ๋ฅผ ํ™œ์šฉํ•ด๋„ @PathVariable์„ optionalํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๊ธด ์–ด๋ ต๋‹ค. /posts๋กœ ์š”์ฒญํ•˜๋“ , /posts/๋กœ ์š”์ฒญํ•˜๋“ , ํ•ด๋‹น URL๋กœ ๋งคํ•‘๋œ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ฐพ์ง€ ๋ชปํ•ด 404 Not Found๊ฐ€ ๋ฐ˜ํ™˜๋  ๊ฒƒ์ด๋‹ค.

@PathVariable์— required ์˜ต์…˜์„ ์–ต์ง€๋กœ ๋„ฃ์–ด๋ณด์ž๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

PostController.java
@GetMapping("/posts/{id}", "/posts/{id}/detail")
public ResponseEntity<*> optionalPathVariable(
    @PathVariable(required = false) String id) {

    if (id) {
        return ResponseEntity.ok()
            .body(postService.findById(id));
    }

    return ResponseEntity.ok()
        .body(postService.findAll());
}

@PathVariable์˜ ๊ฒฝ์šฐ defaultValue๋Š” ์ง€์›ํ•˜์ง€ ์•Š๋Š”๋‹ค.

Kotlin

Kotlin์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํ•  ์ˆ˜ ์žˆ๊ฒ ๋‹ค.

PostController.kt
@GetMapping("/posts/{id}", "/posts/{id}/detail")
fun optionalPathVariable(
    @PathVariable(required = false) id: String?
): ResponseEntity<*> {
    return id?.let {
        ResponseEntity.ok()
            .body(postService.findById(id));
    } ?: ResponseEntity.ok()
        .body(postService.findAll());
}

์ฐธ๊ณ ๋กœ required=true, non-nullable ํƒ€์ž…์œผ๋กœ ๋ฐ”๊ฟจ์„ ๋•Œ ๋‹น์—ฐํžˆ id๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š์œผ๋ฉด id๋ฅผ ์ „๋‹ฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์˜ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์‹œํ‚ฌ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ ์˜๋ฏธ๊ฐ€ ์—†์„ ๋ฟ ์•„๋‹ˆ๋ผ 500 Error๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

์ด๋Ÿฐ ๊ฒฝ์šฐ์—” ๊ทธ๋ƒฅ ๋”ฐ๋กœ ์ปจํŠธ๋กค๋Ÿฌ ๋ฉ”์†Œ๋“œ๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์ž.

More on the matter

REF

Last updated

Was this helpful?