스프링에서 HttpServletRequest를 사용해 쿼리 처리하기
스프링에서 Url의 쿼리를 처리하기 위해서는 HttpServletRequest를 사용할 수 있었다. 예를 들어 다음과 같은 url에서 blogUrl에 해당하는 값인 kotlinworld.com을 처리하는 상황을 생각해 보자.
GET http://localhost:8081/blog/info?blogUrl=kotlinworld.com
이런 경우 우리는 HttpServletRequest 객체의 getParameter 함수를 호출함으로써 blogUrl 변수에 blogUrl 쿼리 파라미터에 대응되는 값을 가져올 수 있었다.
다음은 /blog/info 경로의 요청을 처리해 블로그 정보를 반환하는 getBlogInfo 함수가 HttpServletRequest 객체로부터 blogUrl 쿼리파라미터에 대한 정보를 가져와 BlogInfo 객체를 만들어 반환하는 예시이다.
@Controller
class HomePageController {
@GetMapping("/blog/info")
fun getBlogInfo(
httpServletRequest: HttpServletRequest
): ResponseEntity<BlogInfo> {
val blogUrl = httpServletRequest.getParameter("blogUrl") // Url로부터 blogUrl의 값을 가져옴
?: return ResponseEntity.badRequest().build() // blogUrl의 값이 없으면 400 Bad Request 반환
val blogInfo = blogInfoMap[blogUrl] // blogUrl에 해당하는 BlogInfo 리소스를 가져옴
?: return ResponseEntity.notFound().build() // BlogInfo 리소스가 없으면 404 Not Found 반환
return ResponseEntity.ok(blogInfo) // BlogInfo 리소스를 응답코드 201로 반환
}
}
private val blogInfoMap = mapOf(
"kotlinworld.com" to BlogInfo("조세영의 Kotlin World", 3),
"dummyUrl.dummyUrl" to BlogInfo("Dummy Blog", 3)
)
하지만, 이렇게 HttpServletRequest을 직접 다뤄 작업을 처리하는 것은 파라미터에 대한 추가적인 작업(바인딩 작업)을 필요로 하며, 정확히 어떤 쿼리 파라미터들이 해당 요청을 통해 오는지 이해하는 것을 어렵게 한다. 이런 문제를 해결하기 위해 @RequestParam 이 등장한다.
@RequestParam을 사용해 쿼리 파라미터의 값 자동으로 바인딩하기
@RequestParam을 사용하면, 쿼리의 파라미터 값을 자동으로 바인딩할 수 있다. 위의 코드를 @RequestParam을 사용해 변경해 보자. 그러면 코드는 다음과 같아진다.
@Controller
class HomePageController {
@GetMapping("/blog/info")
fun getBlogInfo(
@RequestParam(value = "blogUrl") blogUrl: String, // 키 값 바인딩
): ResponseEntity<BlogInfo> {
val blogInfo = blogInfoMap[blogUrl]
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(blogInfo)
}
}
더 이상 HttpServletRequest 객체를 노출시키지 않고 필요한 blogUrl만 파라미터로 받는다. 이제 이것이 어떻게 동작하는지 확인하기 위해 스프링 애플리케이션을 실행한 후, 다음 경로로 GET 요청을 날려보자.
GET http://localhost:8081/blog/info?blogUrl=kotlinworld.com
그러면 다음과 같이 200 응답과 함께 블로그 정보가 반환되는 것을 볼 수 있다.
@RequestParam으로 설정된 값이 쿼리에 포함되지 않을 때 어떻게 동작할까?
앞의 HttpServletRequest를 사용할 때는 blogUrl의 값을 HttpServletRequest로부터 가져오는 것을 시도하고, 만약 값이 없을 때는 400 Bad Request를 반환하도록 만들었다.
val blogUrl = httpServletRequest.getParameter("blogUrl") // Url로부터 blogUrl의 값을 가져옴
?: return ResponseEntity.badRequest().build() // blogUrl의 값이 없으면 400 Bad Request 반환
하지만, @RequestParam을 사용해 처리할 때는 처리되는 부분이 없는 것을 볼 수 있다.
@Controller
class HomePageController {
@GetMapping("/blog/info")
fun getBlogInfo(
@RequestParam(value = "blogUrl") blogUrl: String // 값이 없을 때 처리되는 부분이 없음
): ResponseEntity<BlogInfo> {
val blogInfo = blogInfoMap[blogUrl]
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(blogInfo)
}
}
지금부터 @RequestParam을 사용할 때 필요한 파라미터가 Url로 전달되지 않을 시 어떻게 처리할 수 있는지 알아보자. @RequestParam을 사용해 쿼리를 처리할 때는 기본적으로 해당 값이 오지 않으면, 응답코드 400(Bad Request)가 유저에게 전달된다.
예를 들어 다음 경로로 GET 요청을 날리면, 쿼리의 인자로 blogUrl이 없기 때문에, 스프링 애플리케이션은 응답 코드 400번을 클라이언트에 전달한다.
GET http://localhost:8081/blog/info
@RequestParam의 defaultValue 값 설정해 유저로부터 값이 전달되지 않을 때 기본값 정의하기
우리는 @RequestParam 어노테이션에 defaultValue 값을 설정함으로써 유저로부터 값이 전달되지 않을 때 기본값을 정의할 수 있다. 예를 들어 유저로부터 blogUrl이 전달되지 않을 때 값을 dummyUrl.dummyUrl로 설정하고 싶다면 다음과 같이 작성하면 된다.
@Controller
class HomePageController {
@GetMapping("/blog/info")
fun getBlogInfo(
@RequestParam(defaultValue = "dummyUrl.dummyUrl") // 값이 전달되지 않을 때 기본값 설정
blogUrl: String
): ResponseEntity<BlogInfo> {
val blogInfo = blogInfoMap[blogUrl]
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(blogInfo)
}
}
이제 다시 스프링 애플리케이션을 실행해 보자. 그러면 GET http://localhost:8081/blog/info 요청을 보냈을 때, 다음과 같은 결과를 볼 수 있다. 기본 값이 "dummyUrl.dummyUrl"이 blogUrl로 들어가 처리된다.
@RequestParam의 required 파라미터를 false로 설정해 nullable 한 파라미터 만들기
요청에 특정한 쿼리 파라미터가 포함되는 것이 필수가 아닌 선택이라면, @RequestParam의 required 필드를 false로 설정하면 된다. 이때 required를 false로 설정했다면, 타입을 nullable 한 타입으로 설정해야 한다. 예를 들어 blogUrl 쿼리 파라미터가 필수가 아닌 선택이라면 다음과 같이 작성할 수 있다.
class HomePageController {
@GetMapping("/blog/info")
fun getBlogInfo(
@RequestParam(required = false) blogUrl: String? // blogUrl을 선택적으로 받을 수 있게 만듦
): ResponseEntity<BlogInfo> {
val blogInfo = blogInfoMap[blogUrl]
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(blogInfo)
}
}
만약 위와 같이 작성하지 않고 다음과 같이 blogUrl을 non-nullable 하게 만들 경우 어떤 문제가 일어날 수 있을지 확인해 보자.
class HomePageController {
@GetMapping("/blog/info")
fun getBlogInfo(
@RequestParam(required = false) blogUrl: String // 잘못된 설정, blogUrl을 non-nullable로 만듦
): ResponseEntity<BlogInfo> {
val blogInfo = blogInfoMap[blogUrl]
?: return ResponseEntity.notFound().build()
return ResponseEntity.ok(blogInfo)
}
}
위와 같이 설정 후 다음 경로로 HTTP Request를 날려보자. 그러면 어떤 문제가 생기는지 명확히 보인다.
GET http://localhost:8081/blog/info
요청과 응답은 다음과 같다. 서버에서 오류가 발생했기 때문에 응답코드 500이 발생한다.
또한 서버 로그에는 다음과 같은 오류 로그가 나오는 것을 볼 수 있다. blogUrl이 non-null이어야 하는데, null 값이 들어와 애러가 발생한다. 이는 자바를 코틀린에서 사용할 때 플랫폼 타입으로 처리됨으로써 인해 생기는 오류로, 만약 자바 코드의 인자가 null이 될 수 있다면 코틀린에서도 nullable한 타입으로 설정해줌으로써 해결할 수 있다.
java.lang.NullPointerException: Parameter specified as non-null is null: method com.kotlinworld.testserver.HomePageController.getBlogInfo, parameter blogUrl
at com.kotlinworld.testserver.HomePageController.getBlogInfo(HomePageController.kt) ~[main/:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
...