[Spring Boot] @ModelAttribute

치현·2024년 6월 13일
0

Spring Boot

목록 보기
2/4
post-thumbnail

강의를 보면서 막연히 "@ModelAttribute는 요청 파라미터 정보를 모아 객체로 만들어주는 것"로 생각했다. 그런데, @RequestBody, @RequestParam 등과 구체적으로 어떤 차이가 있는지 궁금해졌다.

@ModelAttribute가 무엇인지, 언제 사용하는지 알아보자.


@ModelAttribute란?

  • 클라이언트로부터 HTTP 요청 파라미터나 multipart/form-data 형태의 파라미터를 받아 객체로 생성할 때 사용
  • @RequestParam 이 붙여진 Controller 클래스에 지원
  • 모델의 생성자와 속성을 이용해 바인딩


사용법

@PostMapping("Something URI...")
public String order(@ModelAttribute Order order) {
	// Something logic...
}

공식 문서에는 모델 인스턴스가 생성되는 4가지 방법을 소개한다.
1. 함수 레벨 @ModelAttribute에 의해 생성
2. 클래스 레벨 @SessionAttributes에 의해 HTTP Session 값으로 부터 생성
3. Converter<String, T>에 의해 요청 파라미터 이름과 일치하는 모델 속성에 값을 바인딩해 생성
4. 기본 생성자로 생성
5. Primary Constructor에 의해 생성

관련 내용을 더 공부하고 추가하기로...

@ModelAttribute 생략 (Parameter Level)

@PostMapping("Something URI...")
public String order(Order order) {
	// Something logic...
}

어노테이션을 생략해도 객체로 바인딩된다.
@RequestParam은 String, int 등의 단순 타입 바인딩을 하고, @ModelAttribute는 복합 타입을 바인딩해 객체 인스턴스를 생성한다.

@ModelAttribute 직접 지정 (Method Level)

@ModelAttribute
public Order orderInstance(@RequestParam Long id) {
	return orderRepository.findById(id);
}

@PostMapping("Something URI...")
public String order(@ModelAttribute(binding=false) Order order) {
	// Something logic...
}

클라이언트 요청을 처리하기 전에 @ModelAttribute 어노테이션이 붙은 함수를 먼저 처리한다.

요청 파라미터가 바인딩되어 인스턴스가 자동으로 생성되는 것을 @ModelAttribute(binding=false)를 통해 막고,
일부 정보로만 인스턴스를 직접 생성해 반환함으로써 요청 처리 메서드의 모델 변수에 매핑할 수 있다.

모델 뿐만아니라 다양한 타입의 변수를 위와 같이 직접 매핑할 수 있다.

@BindParam을 이용해 매개변수 이름 지정

class Order {
	private final String memberName;
    
    public Order(@BindParam("member-name") String memberName) {
    	this.memberName = memberName;
    }
}

요청 파라미터 이름을 지정한다.

에러 처리

@PostMapping("Something URI...")
public String order(@ModelAttribute Order order, BindingResult result) {
	if (result.hasErrors()) {
    	return "redirect:/";
    }
	// Something logic...
}

바인딩에 실패하면 MethodArgumentNotValidException이 발생한다.

요청 핸들러 메서드에 BindingResult를 추가함으로써 해당 오류를 컨트롤 할 수 있다.

입력 값 검증

class Order {
	@NotNull
    private String memberName;
}
@PostMapping("Something URI...")
public String order(@Valid @ModelAttribute Order order, BindingResult result) {
	if (result.hasErrors()) {
    	return "redirect:/";
    }
	// Something logic...
}

@Valid 어노테이션을 인자에 명시함으로써 모델에서 요구하는 값을 검증할 수 있다.

검증에 실패할 경우, 기본적으로 MethodArgumentNotValueException이 발생한다.
Constraint 제약 조건이 있을 경우, HandlerMethodValidationException이 발생한다.


작동 원리

Spring Boot '3.2.5'

Spring Framework '6.1'부터 @ModelAttribute 매핑은 ModelAttributeMethodProcessor.createAttribute가 아닌 DataBinder.construct에서 처리한다.

DataBinder.construct는 타겟을 createObject 함수를 호출해서 생성한다.
실질적으로 바인딩을 처리하는 createObject 함수를 알아보자.

1. 생성자 로딩


BeanUtils.getResolvableConstructor에서 적절한 생성자를 모델 클래스에서 찾는다.

리플렉션을 이용해 접근하고, 다음의 기준으로 탐색한다.
1. 가장 매칭되는 인자 수가 적고, public 생성자 (기본 생성자 우선)
2. 가장 매칭되는 인자 수가 적고, protected 또는 private 생성자
3. NoSuchMethodException 발생 (Ignore)

기본 생성자 없이 인자 수가 같은 생성자가 2개 이상 있으면 IllegalStateException 오류가 발생하고 No primary or single unique constructor found 메시지가 출력된다.

2. 파라미터 파싱


인수로 넘어온 값을 모델의 속성에 바인딩 한다.

해당 값이 오브젝트 값 타입일 경우, createObject를 재귀 호출한다.
원시 값 타입일 경우, 타입 변환을 해 args 배열에 담아놓는다.

3. 인스턴스 생성


BeanUtils.instantiateClass를 호출해 인스턴스를 생성한다.

로딩한 생성자와 setter 메서드를 이용해 인스턴스에 값을 바인딩하는데, 실패한 값들은 무시한다.

상세한 동작은 글이 너무 길어져 생략했다. 궁금하다면 위에서 언급한 DataBinder.createObject를 살펴보면 된다.


결론

  • @ModelAttribute는 요청 파라미터 정보를 가지고 값을 바인딩해 인스턴스를 생성한다.
  • BindingResult로 바인딩에서 발생되는 오류를 핸들링할 수 있다.
  • 어노테이션을 생략해도 바인딩 된다.
  • 인스턴스를 생성하기 위한 생성자를 선택할 때, 가장 적인 인자를 가진 public - protected, private 순으로 선택한다.
    나머지는 setter 메서드로 바인딩한다.
  • @Valid 어노테이션으로 바인딩 값을 검증할 수 있다.

추후에 @ModelAttribute 메서드를 적용하는 다양한 방법에 대해 알아봐야겠다.


출처

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.html#page-title

https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-methods/sessionattributes.html

https://velog.io/@jmjmjmz732002/Springboot-ModelAttribute

https://lordofkangs.tistory.com/493

https://goodteacher.tistory.com/514

profile
Backend Engineer

0개의 댓글