Web/Spring, SpringBoot

Spring @ModelAttribute 매핑 오류 해결 방법 (Setter, Enum 변환)

조용우 2025. 3. 20. 20:31

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 사용

https://hyeon9mak.github.io/model-attribute-without-setter/