Spring Boot에서 @ModelAttribute를 사용하여 Query String을 DTO(PostSearchOptions)에 바인딩할 때, 값이 매핑되지 않는 문제가 발생했습니다.
문제 상황: Query String이 PostSearchOptions에 매핑되지 않음
예를 들어, 다음과 같은 컨트롤러가 있다고 가정해봅시다.
@GetMapping("/list")
public ResponseEntity<Page<PostSummaryResponse>> getAllPostsWithSearchOptionsByPage(
Pageable pageable,
@ModelAttribute PostSearchOptions postSearchOptions // 🔥 Query String → 객체 매핑
) {
return ResponseEntity.ok(
postService.getAllPostsWithSearchOptionsToPage(pageable, postSearchOptions)
);
}
이제 아래와 같이 요청을 보냈다고 가정하겠습니다.
GET /list?searchType=title&searchKeyword=Spring&startDate=2024-03-01&endDate=2024-03-15
📌 기대한 동작:
- PostSearchOptions 객체의 searchType, searchKeyword, startDate, endDate에 값이 정상적으로 바인딩되어야 함.
📌 실제 동작:
- 전체 값이 null로 매핑되지 않음.
원인 1: @Setter가 없어서 매핑되지 않음
@ModelAttribute는 기본적으로 "Setter"를 이용해서 값을 주입합니다.
즉, Setter가 없으면 값이 객체에 바인딩되지 않음.
📌 문제 코드 (PostSearchOptions)
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class PostSearchOptions {
private PostSearchType searchType; // 검색 유형 (title, content, author)
private String searchKeyword; // 검색어
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate startDate; // 시작일
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate endDate; // 종료일
private PostSortBy sortBy = PostSortBy.DATE; // 정렬 기준 (date, likes, comments)
private PostSortDirection sortDirection = PostSortDirection.DESC; // 정렬 방향 (asc, desc)
@Min(0)
private Long minViewCounts; // 최소 조회수
@Min(0)
private Long minCommentCounts; // 최소 댓글수
@Min(0)
private Long minLikes; // 최소 좋아요수
}
💡 Setter가 없기 때문에, Spring이 @ModelAttribute를 이용해서 값을 바인딩할 수 없음!
해결 방법: @Setter 추가
📌 이제 searchKeyword, searchType, startDate, endDate 등이 정상적으로 바인딩됨! 🚀
💡 왜 @NoArgsConstructor 가 있으면 @Setter가 필수적인가?
package org.springframework.web.method.annotation;
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
...
protected Object constructAttribute(Constructor<?> ctor, String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
if (ctor.getParameterCount() == 0) {
// A single default constructor -> clearly a standard JavaBeans arrangement.
return BeanUtils.instantiateClass(ctor);
}
// A single data class constructor -> resolve constructor arguments from request parameters.
String[] paramNames = BeanUtils.getParameterNames(ctor);
Class<?>[] paramTypes = ctor.getParameterTypes();
Object[] args = new Object[paramTypes.length];
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
boolean bindingFailure = false;
Set<String> failedParams = new HashSet<>(4);
...
if (ctor.getParameterCount() == 0) 분기에 의해서 파라미터 개수가 0개인 기본 생성자가 확인되면 우선 인스턴스(객체)를 생성하고, setter 메서드를 통한 바인딩을 시도합니다.
그렇지 않을 경우 필드에 맞는 파라미터를 가진 생성자를 찾아 바인딩을 시도합니다.
따라서
- @AllArgsConstructor는 모든 필드를 초기화하는 생성자를 자동 생성합니다.
- 만약@NoArgsConstructor가 객체 생성 후 값이 바인딩되려면 반드시 @Setter가 필요합니다.
✅ 원인 2: Enum 값이 Query String에서 변환되지 않음
📌 문제 코드 (PostSearchType Enum)
public enum PostSearchType {
TITLE, CONTENT, AUTHOR
}
이제 /list?searchType=title로 요청하면 searchType이 변환되지 않음
💡 왜?
- Spring Boot는 기본적으로 title → TITLE처럼 변환하지 않고,
- 대소문자가 완벽하게 일치해야지만 변환이 가능함.
즉, Query String에서 title이 들어오면 TITLE로 변환되지 않아서 원하는 동작이 되지 않는다.
해결 방법: @JsonCreator 또는 @Component Converter 추가
방법 1️⃣: @JsonCreator + @JsonValue 활용
Enum 클래스에 @JsonCreator를 추가하면 대소문자 무관하게 Enum을 매핑할 수 있음.
📌 변경된 PostSearchType
public enum PostSearchType {
TITLE("title"),
CONTENT("content"),
AUTHOR("author");
private final String value;
PostSearchType(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
@JsonCreator
public static PostSearchType from(String value) {
for (PostSearchType type : values()) {
if (type.value.equalsIgnoreCase(value)) { // ✅ 대소문자 무관 변환
return type;
}
}
throw new IllegalArgumentException("Invalid search type: " + value);
}
}
이제 /list?searchType=title을 보내면 PostSearchType.TITLE로 변환됨!
방법 2️⃣: Custom Converter 추가
Spring의 Converter 인터페이스를 활용하여 String → Enum변환을 자동으로 수행할 수도 있음.
📌 새로운 StringToEnumConverter 클래스 추가
public class StringToEnumConverter implements ConditionalGenericConverter {
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(new ConvertiblePair(String.class, Enum.class));
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.getType().isEnum();
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
try {
return Enum.valueOf((Class<Enum>) targetType.getType(), ((String) source).toUpperCase());
} catch (IllegalArgumentException e) {
return null;
}
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToEnumConverter());
}
...
}
📌 이제 Spring이 String 값을 Enum으로 자동 변환!
✅ /list?searchType=title → PostSearchType.TITLE로 변환!
이후에도 String을 Enum으로 바꿀 일이 많을 것 같아서 일단 방법2 적용했습니다.
최종 정리
원인 | 문제 | 해결 방법 |
Setter 없음 | PostSearchOptions에 값이 바인딩되지 않음 | @Setter 추가 |
Enum 변환 문제 | Query String (title) → Enum (TITLE) 변환 불가 | @JsonCreator, Converter 사용 |
'Web > Spring, SpringBoot' 카테고리의 다른 글
isDuplicate 속성을 가진 DTO가 isDuplicate, duplicate 응답하는 에러 (0) | 2025.04.02 |
---|---|
테스트에서 Entity vs DTO 이용 (0) | 2025.04.02 |
Spring Data JPA에서 커스텀 Repository 구현체 인식 및 작동 방식 (0) | 2025.03.20 |
스프링 부트 테스트 시 JPA Auditing 에러 (0) | 2025.03.12 |
Mockito를 이용한 테스트 vs. SpringBootTest의 차이점 (0) | 2025.03.05 |