블로그 프로젝트 중, 타임리프 기반의 회원가입 페이지에서 POST
요청으로 사용자가 입력한 정보를 백엔드에 전송하려 했다.
컨트롤러에서는 @RequestBody
를 사용해 DTO 객체(AddUserRequest
)에 바인딩하려 했는데, 바인딩되지 않고 에러가 발생했다.
<form th:action="@{/user}" method="POST">
<!-- 토큰을 추가하여 CSRF 공격 방지 -->
<input type="hidden" th:name="${_csrf?.parameterName}" th:value="${_csrf?.token}" />
<div class="mb-3">
<label class="form-label text-white">Email address</label>
<input type="email" class="form-control" name="email">
</div>
<div class="mb-3">
<label class="form-label text-white">Password</label>
<input type="password" class="form-control" name="password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
@Controller
@RequiredArgsConstructor
public class UserApiController {
// ...
@PostMapping("/user")
public String signup(@RequestBody AddUserRequest request) {
userService.save(request);
return "redirect:/login";
}
// ...
}
내 의도는 간단했다.
폼에서 POST
요청 → 백엔드에서 DTO로 받아 저장. 그런데 계속 400 Bad Request가 발생했다. 왜일까?
<form>
기본 전송 방식HTML의 <form>
태그는 기본적으로 application/x-www-form-urlencoded
타입으로 데이터를 전송한다.
이는 데이터가 key=value&key2=value2
형태로 인코딩되어 전송된다는 뜻이다.
POST / HTTP/1.1
Host: localhost
Content-Type: applicaion/x-www-from-urlencoded
email=test@gmail.com&password=test
하지만 @RequestBody
는 JSON 형식으로 데이터를 전송할 때(Content-Type: application/json
) 사용해야 한다.
POST / HTTP/1.1
Host: localhost
Content-Type: applicaion/json
{
"email": "test@gmail.com",
"password": "test"
}
즉, <form>
으로 전송하는 요청은 JSON이 아니기 때문에 @RequestBody
에서는 읽을 수 없다.
이 때문에 DTO 바인딩에 실패하고 400 에러가 발생하는 것이다.
@ModelAttribute
또는 파라미터 바인딩 사용HTML <form>
에서 보내는 application/x-www-form-urlencoded
형식은 @ModelAttribute
나 그냥 객체 파라미터로 받는 방식에 자연스럽게 매핑된다.
@PostMapping("/user")
public String signup(AddUserRequest request) {
userService.save(request);
return "redirect:/login";
}
@ModelAttribute
는 생략 가능 (Spring이 자동 처리)AddUserRequest
에 값을 바인딩이렇게만 바꿔도 폼 데이터는 문제없이 DTO로 매핑된다.
전송 방식 | Content-Type | 컨트롤러 수신 방식 |
---|---|---|
HTML <form> | application/x-www-form-urlencoded | @ModelAttribute , 일반 객체 |
JS Fetch/AJAX (JSON 전송) | application/json | @RequestBody |
쿼리 스트링 (GET 요청 등) | N/A (?email=a&password=b ) | @RequestParam |
<form>
은 기본적으로 JSON이 아니라 Form URL Encoded 방식으로 전송된다.@RequestBody
가 아니라 @ModelAttribute
나 그냥 DTO 매개변수로 받아야 한다.<form enctype="multipart/form-data">
를 사용하는 경우에는 또 다른 방식(@ModelAttribute
+ MultipartFile
)이 필요하다.@RequestParam
은 개별 파라미터 받을 때, @ModelAttribute
는 객체로 받을 때 주로 사용된다.이제는 상황에 맞는 요청 방식과 컨트롤러 바인딩 방식을 헷갈리지 않고 쓸 수 있을 것이다.
작지만 자주 겪는 오류였기에, 기록으로 남겨둔다.