HTML form 태그 와 @RequestBody가 호환되지 않는 이유와 해결 방법

Daniel·2025년 5월 19일
0

Back-End

목록 보기
49/54

문제 상황

블로그 프로젝트 중, 타임리프 기반의 회원가입 페이지에서 POST 요청으로 사용자가 입력한 정보를 백엔드에 전송하려 했다.
컨트롤러에서는 @RequestBody를 사용해 DTO 객체(AddUserRequest)에 바인딩하려 했는데, 바인딩되지 않고 에러가 발생했다.

  • FrontEnd 부분
<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>
  • BackEnd 부분
@Controller  
@RequiredArgsConstructor  
public class UserApiController {  
  
// ...
  
	@PostMapping("/user")
    public String signup(@RequestBody AddUserRequest request) {
        userService.save(request);
        
        return "redirect:/login";
    }

// ...
}

내 의도는 간단했다.
폼에서 POST 요청 → 백엔드에서 DTO로 받아 저장. 그런데 계속 400 Bad Request가 발생했다. 왜일까?

원인: Content-Type이 다르다

HTML <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

하지만 @RequestBodyJSON 형식으로 데이터를 전송할 때(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 에러가 발생하는 것이다.

해결 방법

1. @ModelAttribute 또는 파라미터 바인딩 사용

HTML <form>에서 보내는 application/x-www-form-urlencoded 형식은 @ModelAttribute그냥 객체 파라미터로 받는 방식에 자연스럽게 매핑된다.

@PostMapping("/user")
public String signup(AddUserRequest request) {
    userService.save(request);
    return "redirect:/login";
}
  • @ModelAttribute는 생략 가능 (Spring이 자동 처리)
  • 내부적으로 Setter를 통해 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

결론

  • HTML <form>은 기본적으로 JSON이 아니라 Form URL Encoded 방식으로 전송된다.
  • 이때는 @RequestBody가 아니라 @ModelAttribute나 그냥 DTO 매개변수로 받아야 한다.

Note

  • <form enctype="multipart/form-data">를 사용하는 경우에는 또 다른 방식(@ModelAttribute + MultipartFile)이 필요하다.
  • @RequestParam은 개별 파라미터 받을 때, @ModelAttribute는 객체로 받을 때 주로 사용된다.

이제는 상황에 맞는 요청 방식과 컨트롤러 바인딩 방식을 헷갈리지 않고 쓸 수 있을 것이다.
작지만 자주 겪는 오류였기에, 기록으로 남겨둔다.

profile
응애 나 애기 개발자

0개의 댓글