0. 궁금증
지난 [MVC-5] 포스팅에서는 스프링 MVC의 기본 기능과 애노테이션 기반 컨트롤러에 대해 알아보았다. 요즘은 React나 Vue.js 같은 프레임워크를 사용하는 CSR(Client-Side Rendering) 환경이 대세다. 이런 환경에서는 프론트엔드와 백엔드가 JSON 데이터로 통신하는 것이 표준(Standard)처럼 자리 잡았다.
- JSON 데이터 전송 → @RequestBody 사용
그렇다면, 과거 JSP나 타임리프 시절에 폼(Form) 데이터를 받던 @ModelAttribute는 이제 CSR 환경에서 쓸모가 없을까? 결론부터 말하면 "아니오"다. JSON만으로는 해결하기 어려운 상황들이 분명 존재하기 때문이다. 이번 글에서는 CSR 환경에서도 @ModelAttribute가 필수적인 상황(특히 파일 업로드)과, 실무에서 헷갈리기 쉬운 @RequestPart와의 차이점을 정리해본다.
1. JSON의 한계와 Multipart/form-data
JSON은 기본적으로 텍스트(Text) 기반의 포맷이다. 만약 사용자가 프로필 이미지를 업로드하면서 닉네임도 같이 변경하고 싶다면 어떻게 해야 할까? JSON 안에 이미지 바이너리 데이터를 넣으려면 Base64 인코딩을 해야 하는데, 이는 다음과 같은 단점이 있다.
- 데이터 크기 증가: 약 33% 정도 용량이 커진다.
- 서버 부하: 인코딩/디코딩 과정에서 CPU 연산이 필요하다.
따라서 파일 업로드가 포함된 요청은 JSON(application/json)이 아니라 multipart/form-data 방식을 사용해야 한다. 이 방식은 HTTP 요청 바디를 여러 부분(Part)으로 쪼개서, 하나는 텍스트, 하나는 파일 데이터로 보낸다. 이때 스프링 컨트롤러는 @RequestBody가 아닌 다른 방식으로 데이터를 받아야 한다.
2. 파일 업로드를 처리(⭐)
스프링에서 multipart/form-data를 처리하는 방법은 크게 두 가지다. 상황에 맞춰 적절한 것을 선택해야 한다.
2.1. @ModelAttribute: 단순하고 편리함 (추천)
HTML Form 태그가 전송하는 방식과 동일하게 동작한다. 요청 파라미터(key=value)를 하나씩 꺼내어 DTO의 Setter나 생성자에 꽂아준다.
- 특징: 구현이 매우 쉽다.
- 제약: 데이터 구조가 Flat(평평)해야 한다. 복잡한 JSON 구조(리스트, 중첩 객체)를 받기 어렵다.
💻 Backend Code
@PostMapping("/profile")
public void updateProfile(@ModelAttribute ProfileDto request) {
// request.getUsername() (String)
// request.getFile() (MultipartFile)
// 알아서 잘 들어온다.
}
💻 Frontend (JS)
const formData = new FormData();
formData.append("username", "h6bro");
formData.append("file", fileInput.files[0]);
// 별도의 처리가 필요 없다.
axios.post("/profile", formData);
2.2. @RequestPart: 복잡한 구조와 엄격한 구분
파일은 파일대로, 데이터는 JSON 형식 그대로 받고 싶을 때 사용한다.
Content-Type: application/json 헤더를 가진 파트를 찾아서 Jackson 라이브러리가 객체로 변환해준다.
- 특징: DTO 안에 List<String>이나 Address 같은 중첩 객체가 있어도 그대로 받을 수 있다. (@RequestBody의 장점 흡수)
- 단점: 프론트엔드 구현이 조금 번거롭다.
💻 Backend Code
@PostMapping("/register")
public void register(
@RequestPart("info") UserDto info, // JSON -> 객체 변환
@RequestPart("file") MultipartFile file // 파일
) { ... }
💻 Frontend (JS) - 주의!
const formData = new FormData();
// 1. 복잡한 데이터 객체
const userData = {
username: "h6bro",
tags: ["spring", "mvc"], // 리스트 가능
address: { city: "Seoul" } // 중첩 객체 가능
};
// 2. JSON을 Blob으로 감싸고 Content-Type을 명시해야 함 (필수)
const jsonBlob = new Blob([JSON.stringify(userData)], { type: "application/json" });
formData.append("info", jsonBlob);
formData.append("file", fileInput.files[0]);
axios.post("/register", formData);
2.3. 비교 요약: 언제 무엇을 쓸까?
| 구분 | @ModelAttribute | @RequestPart |
| 작동 원리 | 필드별 Setter 주입 | JSON Parser (Jackson) 동작 |
| 데이터 구조 | 단순한 구조 (필드 나열) | 복잡한 구조 (List, 객체 포함) |
| 프론트 난이도 | 하 (그냥 append 하면 됨) | 중 (JSON Blob 처리 필요) |
| 추천 상황 | 일반적인 파일 업로드 | 복잡한 회원가입, 게시글 작성 |
실무에서는 대부분의 경우 @ModelAttribute로 충분하다. 하지만 태그 리스트나 복잡한 설정 정보를 파일과 함께 넘겨야 한다면 @RequestPart를 적극 활용하자.
3. GET 요청의 검색 필터 (⭐)
CSR 환경이라도 데이터를 조회(Read)하는 GET 요청에는 Body가 없다. 따라서 @RequestBody를 쓸 수 없다. 검색 조건이 많을 때(검색어, 날짜, 정렬, 페이지 등), 이를 컨트롤러에서 @RequestParam으로 하나하나 받으면 코드가 지저분해진다. 이때도 @ModelAttribute를 사용하면 객체(DTO) 하나로 깔끔하게 파라미터를 바인딩할 수 있다.
// GET /products?keyword=spring&page=1&sort=desc
@GetMapping("/products")
public List<Product> search(@ModelAttribute SearchCondition condition) {
// 쿼리 파라미터가 condition 객체에 자동으로 매핑됨
return productService.search(condition);
}
마무리 (⭐⭐)
- JSON 데이터 전송 → @RequestBody
- 파일 업로드 (단순) → @ModelAttribute
- 파일 업로드 (복잡한 JSON 필요) → @RequestPart
- GET 요청 (검색 필터) → @ModelAttribute
무조건 @RequestBody만 고집하기보다는, HTTP 요청의 성격(Content-Type)과 데이터의 형태에 맞춰 적절한 애노테이션을 사용하는 것이 중요하다.
'Spring > MVC' 카테고리의 다른 글
| [MVC-5] 스프링 MVC 기본 기능 (0) | 2025.12.30 |
|---|---|
| [MVC-4] 스프링 MVC 시작하기: 애노테이션 기반 컨트롤러 (0) | 2025.12.30 |
| [MVC-3] Spring MVC 구조 이해 (0) | 2025.12.30 |
| [MVC-2] 직접 만드는 MVC 프레임워크 (0) | 2025.12.30 |
| [MVC-1] 서블릿에서 JSP까지: 자바 백엔드 웹 기술의 발전과 한계 (0) | 2025.12.30 |
