Backend/Spring

[Spring] HTTP 요청 데이터 바인딩(@RequestParam, @ModelAttribute, @RequestBody, @RequestHeader)

제이동 개발자 2023. 7. 2. 17:19
728x90

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 어노테이션을 사용합니다. valuename은 같은 속성이며 속성 값과 일치하는 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를 사용하지 않고, @AllArgsConstructorGetter를 사용하여 객체에 바인딩하는 것을 권장합니다.

// 추천 - 불변 객체 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를 사용하지 않는 것을 추천합니다.

728x90