최근 대량 데이터 등록 기능을 확인하던 중, 일정 개수 이상부터 요청이 정상 처리되지 않는 이슈를 확인했다.
처음에는 단순 요청 건수 문제인가 싶었는데, 요청 구조와 바인딩 방식을 따라가 보니 원인은 Spring @ModelAttribute 컬렉션 바인딩 제한에 있었다.
이번 글에서는 어떤 상황에서 문제가 발생했는지, 왜 이런 현상이 생기는지, 그리고 최종적으로 어떻게 해결했는지를 정리해보려고 한다.
보안상 실제 제품명 및 필드명은 일부 마스킹하여 작성했다.

1. 문제 상황
폼 데이터 기반 요청에서 리스트 형태의 데이터가 대량으로 전달될 때 에러가 발생했다.
요청 데이터는 대략 아래와 같은 형태였다.
- `targetList[0]`
- `targetList[1]`
- ...
- `targetList[255]`
- `targetList[256]`
- `targetList[257]`
즉, 컬렉션 형태의 요청 데이터가 256번 인덱스 이상으로 전달되고 있었다. 이 요청은 @ModelAttribute 기반으로 바인딩되고 있었는데, 특정 인덱스를 넘는 순간 정상 처리되지 않았다. 255번까지는 전송이 되는데, 256부터 전송되지 않았다..
처음에는 단순히 요청 건수가 많아서 생기는 현상인가? 싶었는데 진짜 원인은 Spring이 FormData를 컬렉션으로 바인딩하는 방식 자체의 제한 때문이었다.
2. 원인
Spring은 @ModelAttribute를 이용해 요청 데이터를 객체에 바인딩할 때, 컬렉션(List 등)에 대해 자동 확장(auto grow) 기능을 사용한다.
예를 들어 요청 필드명이 아래처럼 들어오면
targetList[0].name=...
targetList[1].name=...
targetList[2].name=...
Spring 바인더는 컬렉션 크기를 자동으로 늘려가며 각 인덱스에 값을 채운다. 그런데 이 자동 확장에는 제한값이 존재한다.
Spring DataBinder의 기본 설정은 다음과 같다.
autoGrowCollectionLimit = 256
즉, 컬렉션을 무한정 늘리지 않고 일정 범위까지만 자동 확장하도록 제한하고 있던 것이었다...! @ModelAttribute 바인딩 과정에서 컬렉션 확장이 한도를 초과하면서 예외가 발생하게 되어 255개까지는 등록이 가능했는데 256개부터 업로드되지 않았던 것이다.
3. 해결
기존의 @ModelAttribute 방식은 첨부파일 업로드 처리와 비즈니스 로직이 하나의 흐름에 섞여 있는 구조였다.
비즈니스 로직이 정말 첨부파일 업로드 방식까지 알아야 할까?
비즈니스 로직은 “어떤 데이터를 처리할 것인가”에 집중해야지, 그 데이터가 multipart/form-data로 들어왔는지, 파일 업로드와 함께 묶여 있었는지까지 책임질 필요는 없다고 판단해서, 첨부파일 업로드에 대한 책임을 비즈니스 로직에서 분리했고, 그에 맞춰 요청 모델도 다시 정리했다.
최종적으로는 @ModelAttribute 기반의 FormData 바인딩을 @RequestBody 기반의 JSON 요청으로 변경했다.
물론 @InitBinder를 사용해 autoGrowCollectionLimit 값을 조정하는 방식으로 해결할 수 있지만, 이 기능은 애초에 입력 항목 수에 대한 명확한 상한이 없는 구조라서, 바인더 제한값을 늘려 가며 대응하는 것보다는, 요청 구조 자체를 대량 리스트 처리에 더 적합한 형태로 바꾸는 편이 더 적절하다고 판단했다.
| 변경 전 | 변경 후 |
| @ModelAttribute, FormData 기반 요청 | @RequestBody, JSON 기반 요청 |
| 첨부파일 업로드 처리와 비즈니스 데이터 처리의 경계가 모호함 | 비즈니스 로직은 순수한 데이터 처리에 집중 |
| 필드명에 인덱스 포함 예: targetList[0], targetList[1], targetList[256] | 배열 전체를 request body로 전달 |
기존 방식은 아래처럼 인덱스 기반의 필드명을 사용했다.
targetList[0].name=...
targetList[1].name=...
targetList[2].name=...
...
targetList[256].name=...
이 경우 Spring 바인더는 각 인덱스를 해석하면서 컬렉션을 직접 확장해야 한다.
반면 @RequestBody로 변경하면, 아래처럼 JSON 배열 전체를 한 번에 전달할 수 있다.
{
"targetList": [
{ "name": "..." },
{ "name": "..." },
{ "name": "..." }
]
}
4. 정리
이번 이슈를 정리하면 다음과 같다.
1) Spring @ModelAttribute는 컬렉션 바인딩 시 자동 확장(auto grow)을 사용한다.
2) DataBinder의 기본 autoGrowCollectionLimit 값은 256이고, 인덱스 기반 FormData에서 `targetList[256]` 같은 값이 들어오면 바인딩 실패가 발생할 수 있다.
3) 이번 케이스에서는 @ModelAttribute → @RequestBody로 변경하여 해결했다.
5. 느낀 점
이번 이슈를 통해 어떤 요청 방식이 어떤 바인딩 메커니즘을 타는지를 알게 되었다.
땜빵을 하면 빨리 해결할 수 있겠지만 비슷한 문제가 나중에 꼭 다시 나타나서(업보빔을 맞기 때문에) 이번에는 대량 리스트를 다루는 구조 자체를 다시 보고, ModelAttribute보다 RequestBody가 더 적합하다고 판단해 방향을 바꿨다.
결과적으로는 요청 구조도 더 명확해졌고, 문제 원인도 훨씬 깔끔하게 정리할 수 있었다. 개발 -> QA 단계에서 발견되지 않았던 에러가 운영에서 터져서 많이 당황했지만, 운영을 하면서 배우게 되는 것도 있는 거 같다.역시 직접 써봐야 한다는 걸 뼈저리게 느꼈다..
출처 및 근거
아래 문서들을 기준으로 원인과 해결 방향을 확인했다.
1) Spring Framework Reference - Validation, Data Binding, and Type Conversion
링크:
https://docs.spring.io/spring-framework/reference/core/validation/data-binding.html
Spring은 DataBinder를 사용해 request parameter 등을 객체에 바인딩하고, 이때 단순한 단일 필드만 처리하는 것이 아니라, nested property path를 따라 내부 객체를 생성하고 값을 채우는 방식으로 동작한다.
Data binding is useful for binding user input to a target object where user input is a map with property paths as keys, following JavaBeans conventions. DataBinder is the main class that supports this, and it provides two ways to bind user input:
Type conversion is applied as needed to convert user input. If the constructor parameter is an object, it is constructed recursively in the same manner, but through a nested property path. That means constructor binding creates both the target object and any objects it contains.
property path란?
객체의 필드(속성) 위치를 문자열 경로로 표현해서 값을 매핑하는 방식으로 targetList[0].name, accounts[2].email등 점(.)이나 인덱스([0])가 포함된 문자 경로를 해석해서 객체에 값을 넣는 것을 property paths 기반 바인딩이라고 한다.
즉, `targetList[0]`, `targetList[1]` 같은 형태가 Spring 바인딩 메커니즘 안에서 해석된다.
2) Spring DataBinder Javadoc
링크:
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/validation/DataBinder.html#setAutoGrowCollectionLimit-int-
DataBinder에는 `DEFAULT_AUTO_GROW_COLLECTION_LIMIT` 상수가 정의되어 있다.

setAutoGrowCollectionLimit(int autoGrowCollectionLimit) 메서드 설명을 통해 컬렉션/배열 auto-grow 제한을 설정할 수 있음을 확인할 수 있다.

즉, Spring은 컬렉션 바인딩 시 auto-grow 제한을 가지고 있고, 이 값을 설정할 수 있다.
DataBinder (Spring Framework 7.0.5 API)
Create the target with constructor injection of values. It is expected that setTargetType(ResolvableType) was previously called and that getTarget() is null. Uses a public, no-arg constructor if available in the target object type, also supporting a "prima
docs.spring.io
'🏰 Back-end' 카테고리의 다른 글
| SonarQube | Webhook HMAC 검증 적용기 (0) | 2026.06.12 |
|---|---|
| SonarQube | 같은 소스코드를 분석했는데 분석 시간이 14배 늘어났다. (0) | 2026.06.02 |
| Template Method + Reader 추상화로 확장 가능한 Validator 만들기 (0) | 2026.01.22 |
| [SonarQube] SonarQube 업그레이드에 따른 SonarScanner 버전을 확인해보자 (SonarQube 10.5.1) (0) | 2024.10.27 |
| [Github] OAuth 앱 권한 부여 방법 (0) | 2024.04.03 |