[Spring] HTTP 요청 데이터 바인딩(@RequestParam, @ModelAttribute, @RequestBody, @RequestHeader)
HTTP 요청 데이터 바인딩
클라이언트가 서버로 요청을 보낼 때는 일반적으로 요청에 필요한 데이터를 path parameter, request variable, body, header 등에 실어서 요청합니다. Spring에서는 요청 데이터를 바인딩할 수 있는 어노테이션들을 제공하는데 주로 사용하는 어노테이션은 @PathVarible
, @RequestParam
, @RequestBody
, @ModelAttribute
, @RequestHeader
등이 있습니다.
1. 경로 변수(Path Variable) 바인딩
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
}
경로 변수는 URL 경로에 포함된 동적인 값을 나타내며(ex - /members/{member_id}) 경로 변수(Path Variable) 값을 매개변수로 바인딩하기 위해서는 Spring MVC에서 제공하는 @PathVariable
어노테이션을 사용합니다. value와 name은 같은 속성이며 속성 값과 일치하는 URL의 Path Variable을 찾아 바인딩합니다. 속성 명은 생략하여 사용할 수 있으며 속성 값이 없을 경우 파라미터 명과 일치하는 Path Variable을 바인딩해 줍니다.
@RestController
public class DataBindingController {
// 첫 번째 예시
// 속성 명, 속성 값 모두 명시
@GetMapping("/v0/members/{member_id}")
public ResponseEntity<String> getMemberDetailV0(
@PathVariable(name = "member_id") Long memberId
) {
return ResponseEntity.ok("Member id = " + memberId);
}
// 두 번째 예시
// 속성 값만 명시
@GetMapping("/v1/members/{member_id}")
public ResponseEntity<String> getMemberDetailV1(
@PathVariable("member_id") Long memberId
) {
return ResponseEntity.ok("Member id = " + memberId);
}
// 세 번째 예시
// 속성 명, 속성 값 모두 없을 경우 파라미터 명인 'memberId'로 path variable을 찾아 바인딩
@GetMapping("/v2/members/{memberId}")
public ResponseEntity<String> getMemberDetailV2(
@PathVariable Long memberId
) {
return ResponseEntity.ok("Member id = " + memberId);
}
}
위 세 가지 예시 중 속한 팀에 코드 컨벤션을 따라 사용하시면 되지만 저는 첫 번째나 두 번째를 추천합니다. 세 번째의 경우는 Path Variable이 2개 이상인 경우 직관적이지 않아 혼동을 줄 수 있기 때문에 개인적으로는 좋지 않다고 생각합니다.
2. 요청 파라미터(Request Parameter) 바인딩
요청 파라미터(Request Parameter)는 URL 끝에 ?
뒤에 이름과 값의 쌍으로 이루어진 데이터이며 바인딩하기 위해서는 Spring MVC에서 제공하는 @RequestParam
, @MedelAttribute
를 사용합니다.
2-1. @RequestParam
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
// 매개변수가 필요한지 여부
boolean required() default true;
// 요청 매개변수가 제공되지 않았거나 값이 비어 있는 경우 사용하는 default value
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
@RequestParam
은 단일 요청 파라미터 값을 매개변수로 바인딩해 주는 어노테이션입니다. 즉, @RequestParam
어노테이션 1개당 1개의 요청 파라미터를 바인딩해 줄 수 있습니다. name 속성을 생략할 수 있으며 생략할 경우 매개변수 명으로 요청 파라미터 값을 바인딩하고, 또한 @RequestParam
어노테이션을 생략해도 매개변수 명으로 요청 파라미터를 바인딩할 수 있습니다.
// 첫 번째 예시
// 속성 명, 속성 값 모두 명시
@GetMapping("/v0/request-param/members")
public ResponseEntity<String> getMemberDetailV0RequestParam(
@RequestParam(name = "name") String name
) {
return ResponseEntity.ok("Member info : " + name);
}
// 두 번째 예시
// 속성 값만 명시
@GetMapping("/v1/request-param/members")
public ResponseEntity<String> getMemberDetailV1RequestParam(
@RequestParam("name") String name,
@RequestParam("age") int age
) {
return ResponseEntity.ok("Member info : " + name + ", " + age);
}
// 세 번째 예시
// 속성 명, 속성 값 모두 없을 경우 파라미터 명으로 request parameter를 찾아 바인딩
@GetMapping("/v2/request-param/members")
public ResponseEntity<String> getMemberDetailV2RequestParam(
@RequestParam String name,
@RequestParam int age
) {
return ResponseEntity.ok("Member info : " + name + ", " + age);
}
// 네 번째 예시
// @RequestParam 생략
@GetMapping("/v3/request-param/members")
public ResponseEntity<String> getMemberDetailV3RequestParam(
String name,
int age
) {
return ResponseEntity.ok("Member info : " + name + ", " + age);
}
@RequestParam
역시 마찬가지로 너무 많은 생략은 오히려 코드의 가독성이 저하되고, 혼동을 줄 수 있기 때문에 첫 번째나 두 번째 예시와 같은 방법으로 사용하시는 것을 권장합니다.
2-2. @ModelAttribute
@ModelAttribute
의 속성인 name은 보통 모델 객체를 뷰에 전달할 때 사용하는 속성으로 데이터 바인딩과는 다른 영역이기 때문에 설명은 생략하겠습니다.
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Reflective
public @interface ModelAttribute {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean binding() default true;
}
@ModelAttribute
은 요청 파라미터들의 값들을 객체 필드에 바인딩해 주는 어노테이션입니다. 요청 파라미터들이 많을 경우 사용하는 유용한 어노테이션으로 요청 파라미터 명과 일치한 프로퍼티(JavaBeans - Property)에 바인딩됩니다.@ModelAttribute
는 생략이 가능하며 프로퍼티에 해당되는 쿼리 파라미터가 없을 경우 기본값으로 저장됩니다.
@Getter
@AllArgsConstructor
public class MemberDto {
private String name;
private int age;
private String cellphone;
}
@RestController
public class DataBindingController {
@GetMapping("/v0/model-attribute/members")
public ResponseEntity<String> getMemberDetailV0ModelAttribute(
@ModelAttribute MemberDto memberDto
) {
return ResponseEntity.ok("Member info : " + memberDto.getName() + ", " + memberDto.getAge() + ", " + memberDto.getCellphone());
}
}
위 예제 코드에서 볼 수 있듯이 요청 파라미터 name, age를 자동으로 객체 필드에 바인딩되고 요청 파라미터로 넘어오지 않은 cellphone 같은 경우는 기본 값 null로 저장된 것을 볼 수 있습니다.
@ModelAttribute
이 데이터 바인딩이 되는 조건은 다음과 같습니다.
- 생성자가 2개 이상인 경우 Setter를 이용하여 바인딩한다.
- 생성자가 1개인 경우
- 기본 생성자 : 기본 생성자로 객체 생성 후 Setter를 이용하여 바인딩한다.
- 매개변수를 가진 생성자 : 매개변수를 가진 생성자로 요청 파라미터 값을 바인딩한 객체를 생성 후 Setter를 이용하여 재 바인딩한다.
따라서 제가 추천하는 방법은 모든 객체는 불변 객체로 만들어 사용하는 것을 원칙으로 하기 때문에 Setter를 사용하지 않고, @AllArgsConstructor
와 Getter를 사용하여 객체에 바인딩하는 것을 권장합니다.
// 추천 - 불변 객체 O
@Getter
@AllArgsConstructor
public class MemberDto {
private String name;
private int age;
private String cellphone;
public String getMemberInfo() {
return name + ", " + age + ", " + cellphone;
}
}
// 비추천 - 불변 객체 X
@Getter
@Setter
public class MemberDto {
private String name;
private int age;
private String cellphone;
}
3. 요청 헤더(Request Header) 바인딩
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestHeader {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
String defaultValue() default ValueConstants.DEFAULT_NONE;
}
요청 헤더 값을 바인딩하기 위해서는 Spring MVC에서 제공하는 @RequestHeader
어노테이션을 사용합니다. name 속성을 이용하여 요청 header에서 key 값을 찾아 value를 반환합니다. @RequestHeader
어노테이션 역시 name 속성을 생략할 수 있으며 생략 시 매개변수 명으로 header에서 key 값을 찾아 value 값을 매개변수에 바인딩합니다.
// 첫 번째 예시
// 속성 명, 속성 값 모두 명시
@GetMapping("/v0/request-header")
public ResponseEntity<String> getMemberDetailV0RequestHeader(
@RequestHeader(name = "Authorization") String token
) {
return ResponseEntity.ok("Token info : " + token);
}
// 두 번째 예시
// 속성 값만 명시
@GetMapping("/v1/request-header")
public ResponseEntity<String> getMemberDetailV1RequestHeader(
@RequestHeader("Authorization") String token
) {
return ResponseEntity.ok("Token info : " + token);
}
// 세 번째 예시
// 속성 명, 속성 값 모두 없을 경우 매개변수 명(authorization)으로 헤더를 찾아 바인딩
@GetMapping("/v2/request-header")
public ResponseEntity<String> getMemberDetailV2RequestHeader(
@RequestHeader String authorization
) {
return ResponseEntity.ok("Token info : " + authorization);
}
이 역시 코드의 가독성과 혼동을 피하기 위해 첫 번째나 두 번째처럼 사용하길 권장합니다.
4. 요청 바디(Request Body) 바인딩
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {
boolean required() default true;
}
요청 바디의 데이터를 객체로 바인딩하기 위해서는 Spring MVC에서 제공하는 @RequestBody
어노테이션을 사용합니다. 주로 JSON이나 XML 형식의 데이터를 자바 객체로 변환하며 주로 POST 또는 PUT 메서드와 함께 사용됩니다.
@Getter
public class MemberDto {
private String name;
private int age;
private String cellphone;
public String getMemberInfo() {
return name + ", " + age + ", " + cellphone;
}
}
@RestController
public class DataBindingController {
@PostMapping("/v0/request-body/members")
public ResponseEntity<String> saveMemberV0(
@RequestBody MemberDto memberDto
) {
return ResponseEntity.ok("Member info : " + memberDto.getMemberInfo());
}
}
@RequestBody
는 내부에서 ObjectMapper
를 통해 객체에 데이터를 바인딩시켜 줍니다. ObjectMapper
는 바인딩할 객체(dto)의 기본 생성자가 필요하며 reflection을 통해 필드에 값을 넣어주기 때문에 Getter or Setter 중 1개만 있어도 데이터가 바인딩이 됩니다. 따라서 불변 객체로 사용하기 위해 Getter를 사용하고 Setter를 사용하지 않는 것을 추천합니다.