본문 바로가기
Backend/Spring

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

by 제이동 개발자 2023. 7. 2.
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