본문으로 바로가기

@ModelAttribute는 어떻게 Formatter 없이 작동할까?

@RequestParam 이나 @PathVariable로 들어온 문자열 값을 객체로 받기 위해서는 Formatter 가 필요하다. 아래 예제로 살펴보자.

  • title이라는 String형 변수와 length라는 int형 변수를 가진 DTO이다.
@Getter
@Setter
public class Music {

    String title;
    int length;
}

 

만약 @RequestParam이나 @PathVariable로 넘어온 문자열 값을 Music객체로 받고 싶다면 아래와 같이 코드를 작성하면 된다.

@ResponseBody
@GetMapping("/hello/{title}"){
public String hello(@Pathvariable("title") Music music)
    }

테스트 코드를 아래와 같이 실행해보면,

	@Test
    public void hello() throws Exception {
        mockMvc.perform(get("/hello/kh")
        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(content().string("kh"));
    }

Resolved Exception이 발생한다.

핸들러의 인자로 Music 타입을 받겠다고 했는데 실제로 주어진 것은 String 타입이라는 것이다. 즉, String이 Music으로 conversion되지 않아서 발생한 예외이다.

 

이제 Formatter를 작성해보자.

@Component
public class MusicFormatter implements Formatter<Music> {

    @Override
    public Music parse(String s, Locale locale) throws ParseException {
        Music music = new Music();
        music.setTitle(s);
        return music;
    }

    @Override
    public String print(Music music, Locale locale) {
        return null;
    }
}

Formatter를 Bean에 등록하면 테스트가 정상적으로 통과하는것을 볼 수 있다.

 

@RequestParam 의 경우도 마찬가지다.

  @GetMapping("/hello")
    public String hello2(@RequestParam("title") Music music) {
        return music.getTitle();
    }
  @Test
    @DisplayName("@RequestParam으로 받기")
    public void hello2() throws  Exception {
        mockMvc.perform(get("/hello")
                .param("title","kh"))
                .andDo(print())
                .andExpect(content().string("kh"));
    }

Formatter를 Bean으로 등록했다면 정상적으로 통과한다.

 

의아한 점이 있다. @ModelAttribute도 primitive type의 객체를 래퍼런스 타입의 객체로 바인딩해주는 로직을 가지고 있다고 알고있는데, 왜 @ModelAttribute은 Formatter 없이도 작동되는것일까?

먼저 @ModelAttribute 가 Formatter 없이도 정상 작동 되는지 확인해보자.

기존에 있던 Formatter는 삭제하거나 Bean에서 빼도록 하자.

 @PostMapping("/hello")
    public Music hello3(@ModelAttribute Music music) {
        return music;
    }
@Test
    @DisplayName("@ModelAttribute로 받기")
    public void hello3() throws Exception {
        mockMvc.perform(post("/hello")
                .param("title", "kh")
                .param("length", "10"))
                .andDo(print())
                .andExpect(jsonPath("title").value("kh"))
                .andExpect(jsonPath("length").value(10));
    }
}

 

열심히 구글링 해본 결과 @ModelAttribute는 다음과 같은 일이 일어난다고 한다.

  1. 파라미터로 넘겨준 타입의 오브젝트를 자동으로 생성한다.

    • 넘겨준 타입은 setter/getter가 구현되어 있어야 한다.
  2. 생성된 오브젝트에서 HTTP로 넘어온 값을 setter 메소드를 통해서 자동으로 바인딩 한다.

    /hello?title=kh&length=10
    

 

실제로 아래 코드처럼 setter와 getter를 제거하고 테스트를 실행해보면,

public class Music {
    String title;
    int length;
}

java.lang.AssertionError: No value at JSON path "title" 오류가 발생하면서 테스트가 실패한다.

 

결론


  • @PathVariable이나 @ReqeustParam으로 받은 값은 요청 파라미터를 메소드에서 1:1로 받기 때문에 무엇으로 변환하는지 Formatter를 통해 명시적으로 알려줘야 한다.
  • @ModelAttribute는 클라이언트가 전송하는 여러 파라미터들을 1대1로 객체에 바인딩하기 때문에 setter메소드만 구현했다면 자동으로 바인딩이 된다. 따라서 Formatter를 구현할 필요가 없다.