--- tags: ModelAttribute --- @ModelAttrbute 1부 - model 객체 생성 과정 === # @ModelAttrbute ```java @ResetController public class A { @PostMapping("/opr/cmpgn/model") public String model(@ModelAttribute("user") @Valid User user, BindingResult result){ log.info("user : " + user); return "ok"; } } ``` 위 코드에서 아무런 requestParameter가 없어도 log.info 는 Null 이 아닌 Empty 상태의 인스턴스라고 로깅 해 준다. ``` User(name=null, age=null) ``` 왜 Null 이 아닐까? 우선 컨트롤러 메소드의 인자로 받아보는 user 객체의 생성 순서는 다음과 같다. (또는 model.getAttribute("user") 로 받아보는) 1. 모델객체 생성 // new User() 2. 프로퍼티 바인딩 // setName(), setAge().. 3. 검증 (@Valid 가 있는경우) 이제 왜 Null이 아닌지 알수 있다. 1번에서 객체가 초기화 되었기 때문이다. 이렇게 3가지 과정을 거치고 나면 비로소 메소드의 인자로 전달되는 것이다. 각 과정을 자세히 알아보자. ## 1. 모델객체 생성 인자를 보면 @ModelAttribute("user") User user 라고 되어있다. > @Valid는 3번에서 설명 하겠다. @ModelAttribute는 User 객체를 초기화 하고 model 로 관리하게 된다. `model.addAttribute("user",new User())` 상태라고 볼수 있다. user 인자를 사용 하는것은 `model.getAttribute("user")` 를 사용 하는것과 같은 것 이다. > 참고로 new User() 보다 좀더 상세하게 초기화 할수 있도록 `메소드 @ModelAttribute` 로 직접 지정 할 수가 있는데 3부에서 설명하겠다. ## 2. 프로퍼디 바인딩 (WebDataBinder의 역할 1) get 이던 post 던 요청 파라미터는 대체로 key-value 형식 이다. > post 는 body 에 담기긴 하지만 모양은 get과 같이 쿼리스트링 이다. @ModelAttribute 객체 내 필드들을 키값으로 찾고 벨류들이 바인딩이 된다. set + 키이름의 메소드를 찾아 바인딩 해준다. 가령 `?name=bonjugi&age=33` 같은 형태의 쿼리스트링이 있다고 치면, setName, setAge 의 메소드명을 찾아 각각 bonjugi, 33을 주입 해주는 식이다. ### 2.1 WebDataBinder 프로퍼티 바인딩 할때, WebDataBinder에 등록된 아래 3가지의 기술로 컨버팅 해서 바인드 할수 있다. - 커스텀 Property Editor - 컨버전서비스 (Converter, Formatter) - 디폴트 Property Editor > 참고 : Property Editor 가 먼저 세상에 나왔고, 나중에 컨버전 서비스가 나왔다. Property Editor 는 스레드세이프 하지도 않아 빈으로 등록할수도 없다는 단점이 있기에 보강 된것으로 생각된다. 때문에 개발자는 `컨버전서비스 (Converter, Formatter)` 에 더 관심을 갖고 학습하면 된다. 그래도 Property Editor 기술이 어딘가에는 쓰이고있긴 할거 같은데 못찾겠다.. HTTP는 요청은 모두 String 으로 되어있다. 그럼에도 컨트롤러에서 Integer나 Boolean 으로 변환되어 받아본적 있을 것이다. `@GetMapping public model (@ModelAttribute Integer a, @ModelAttribute Boolean b){...}` 이것이 WebDataBinder에 등록된, 컨버전 서비스에 등록된, 컨버터의 기술이다. 또한 @ModelAttribute 가 wrapper 객체이더라도 내부의 필드 또한 커버팅의 대상이 된다. > 예를들어 User 모데 내에 Integer age 필드 예를들어 이미 등록되어있는 컨버터들중 StringToBooleanConverter 는 String->Boolean 컨버팅을 담당한다. 재미를 위해 잠깐 소스를 까보자. ```java final class StringToBooleanConverter implements Converter<String, Boolean> { private static final Set<String> trueValues = new HashSet(4); private static final Set<String> falseValues = new HashSet(4); StringToBooleanConverter() { } public Boolean convert(String source) { String value = source.trim(); if ("".equals(value)) { return null; } else { value = value.toLowerCase(); if (trueValues.contains(value)) { return Boolean.TRUE; } else if (falseValues.contains(value)) { return Boolean.FALSE; } else { throw new IllegalArgumentException("Invalid boolean value '" + source + "'"); } } } static { trueValues.add("true"); trueValues.add("on"); trueValues.add("yes"); trueValues.add("1"); falseValues.add("false"); falseValues.add("off"); falseValues.add("no"); falseValues.add("0"); } } ``` > true, on, yes, 1 들은 모두 true로 컨버팅 해준다는 사실! 이렇게 우리는 요청파라미터에 truefalse=1 으로 보내도 true 를 받아볼수 있게 되는 것이다. 또한 기본 등록된 컨버터 외에도 커스텀 Converter 를 만들수 있는데, 다음과 같이 만들수 있다. ```java @Component public class StringToActionPatternConverter implements Converter<String, ActionPattern> { @Autowired private ActionPatternDAO actionPatternDAO; @Override public ActionPattern convert(String s) { Integer actionPatternId = Integer.parseInt(s); return actionPatternDAO.getAllActionPatternJoinUserActionPatternById(ActionPattern.of(actionPatternId)); } } ``` actionPatterId=1 이라는 요청을 ActionPatter 모델로 변경해주는 컨버터 이다. 다음과 같이 WebConfig 에 addFormatters를 오버라이드 해서 등록 할 수 있다. ```java @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(stringToActionPatternConverter); } ``` > spring-boot-stater-data-jpa 는 이렇게 영속화된 데이터를 조회해서 컨버팅 해주는 컨버터들이 기본으로 등록되어 있다고 하더라. 검증하진 못했는데 추후 검증되면 설명하겠다. ## 3. 완성된 객체에 검증 수행 (WebDataBinder의 역할 2) 2번 과정을 통해 모든 필드에 값이 주입되었다. 인자에 @Valid 를 붙이면 검증을 수행하고, 결과를 BindingResult 객체로 담아서 컨트롤러 메소드에 전달 해 준다. ```java @EqualsAndHashCode(of = "consultId") @Data public class Consult { private Integer consultId; private ConsultType consultType; @NotBlank(message = "이메일을 작성해 주세요.") @Email(message = "이메일 형식을 확인해 주세요.") private String email; @NotBlank(message = "이름을 작성해 주세요.") private String realName; @NotBlank(message = "내용을 작성해 주세요.") private String content; @NotBlank(message = "전화번호를 작성해 주세요.") @Pattern(regexp = "[0-9]{10,11}", message = "10~11자리의 숫자만 입력가능합니다") private String cellPhone; private ConsultStatus consultStatusCode; private LocalDateTime createTime; } ``` @NotBlack, @Email, @Pattern 같은 어노테이션들은 JSR303 자바 표준스펙에 따라 구현되어진 hibernate-validator 의 어노테이션 들이다. 이외에도 @Max, @Min, @Size, @CreditCardNumber, @NotBlack, @Range 등이 있다. > org.hibernate.validator 와, javax.validation.constraints 패키지에 들어있으니 참고하자. ## 결론 이런 과정을 거치고서야 비로소 user 객체를 컨트롤러 메소드블럭 내에서 사용할수 있게 된다. >`model.getAttribute("user")` 와 같다.