약 2년전에 관련 내용으로 포스팅 한 적이 있지만 그때는 사용 방법에 관한 포스팅이었고 지금은 사용법보다는 사용하는 목적, 이유 등의 차원에서 다시 정리하고자 한다.
결과부터 말하자면
@RequestBody
1. 클라이언트에서 서버로 Ajax등과 같은 비동기 요청 시 Json 타입으로 보낼 때 사용한다.
2. Rest Api에서 주로 사용한다.
@ModelAttribute
1. 공통된 Model 값을 사용하고 싶을 때 쓰인다.
2. Client에서 Get 데이터를 보내고 싶을 때 사용한다.
3. SSR에서는 여러 화면에서 Form 양식을 재활용하고 싶을 때 사용한다.
4. 타임리프에서 Form 태그 기능(th:object), 세션, 검증과 같은 기능과 결합해서 사용한다.
@RequestBody
사용 목적
클라이언트에서 서버에 데이터를 전달할 때 데이터의 양식을 정할 수 있는데 위와 같이 Request Body(요청 본문)에 여러 Content Type을 설정할 수 있다.
그 중에 Json(JavaScript Object Notation) 타입을 서버로 보내서 서버는 Json을 Java 객체로 변환하는데 이 것이 일반적인 @RequestBody의 개념이다.
보통 클라이언트에서 서버로 Ajax등과 같은 비동기 요청 시 요청 타입을 Json으로 보낼 때 사용한다.(요즘엔 주로 Json만 사용)
그래서 Api로 구성된 Rest Api 서버에서는 클라이언트와 서버에서 주고 받는 데이터는 요즘은 대부분 Json으로 주고 받기 때문에 @RequestBody를 많이 이용한다.
반면에 Form Submit 시에는 Content Type을 Json으로 보낼 수 없기 때문에 Form Submit은 서버에서 @RequestBody로 받을 수 없다.
Post? Get?
@RequestBody는 Get, Post 둘 다 가능하며 원칙상 Request Body 안에 Json만 포함한다면 가능하다.
그렇지만 웬만해선 Post만 사용해야 한다. @RequestBody는 Body안에 Json을 포함해야하는 것이 룰이지만 Get은 Content Type을 설정해서 보내지 않는다. 데이터 타입을 Query String 형태로 보내는 것이 일반적이기 때문이다.
Post로는 Body에 Json으로 요청하는 것이 일반적이고 또 이런 경우 Restful Semantics이라는 표현을 쓰는데, Rest Api에서는 의미론적으로 Get에 Body를 담아버리면 Api의 의도를 파악하기가 어려울 수 있기 때문에 Post는 Post 답게 Get은 Get 답게 쓰는 것을 권장한다.
@ModelAttribute
메소드 레벨(Method Level)에서 @ModelAttribute의 사용 목적
메소드 레벨 위에 아래와 같이 @ModelAttribute를 만들면 모든 @RequestMapping 메소드가 공통된 Model를 가질 수 있다.
그러면 /test 호출 시 아래와 같이 value1과 value 2를 뿌려주면 오른쪽과 같은 결과가 나온다.
메소드 레벨에서의 @ModelAttribute는 하나의 컨트롤러 내에 쓰여진 모든 @RequestMapping 내에 공통 Model을 쓰고 싶은 경우에 사용할 수 있다. 예를 들어 모든 페이지에 Select Box 내에 동일한 값을 넣고 싶을 때 쓸 수 있다. 각각의 페이지마다 Select Box의 데이터를 하나하나 수정하는 것보다 Model 값으로 공통으로 만들어주는게 효율적이기 때문이다. @ControllerAdvice와 같은 전역 클래스에 @ModelAttribute를 적용하면 모든 @RequestMapping마다 공통된 Model을 갖는다.
메소드 파라미터(Method Parameter)에서 @ModelAttribute의 사용 목적
주로 Get 방식에서만 쓰인다.
메소드 파라미터에 쓰인 @ModelAttribute는 주로 Get 방식에서만 쓰인다.
Post 데이터도 받아지지만 주로 Get으로만 쓰인다. @RequestBody가 아닌 @ModelAttribute로 Post 데이터를 받아오면 JSON 직렬화 기능이 없다고 보면 된다.
Get인 경우에는 Query String을 요청 데이터로 보내고 Post인 경우엔 x-www-form-urlencoded 형태로 요청 데이터를 보낸다.
해당 데이터는 @ModelAttribute로 모두 자바 객체로 변환할 수 있다.
커맨드 객체(Command Object) - SSR에서의 재활용성
메소드 파라미터에 쓰인 @ModelAttribute는 커맨드 패턴을 이용한 커맨드 객체로 사용된다.
아래에서 빨간 테두리 영역이 커맨드 객체이다.
커맨드 패턴이란 실행될 기능을 캡슐화함으로써 주어진 여러 기능을 실행할 수 있는 재사용성이 높은 클래스를 설계하는 패턴을 말한다.
스프링 문서에서는 커맨드 객체를 "a JavaBean which will be populated with the data from your forms" 라고 표현하는데 Spring MVC에서 커맨드 객체는 Form 객체라고 부른다는 뜻이다.
다시 말해 Spring MVC에서 커맨드 객체는 클라이언트에서 넘어온 Form 데이터를 받아오는 객체라고 불린다.
"어? DTO 객체랑 다른게 없잖아? 그럼 커맨드 객체는 DTO 객체인가?"라고 생각할 수도 있다.
하지만 다른 개념이다.
DTO는 단순히 데이터를 전달하는데 초점이 맞춰져있는 개념이다.
커맨드 객체는 HTML 폼 데이터를 자바 클래스에 매핑하고 다시 자바 클래스를 HTML 폼 데이터에 뿌려줄 수 있는 폼 객체로서 커맨드 객체는 재사용성이 높은 캡슐화된 클래스에 초점이 맞춰져있는 개념이다.
커맨드 객체로 쓰인 @ModelAttribute는 재사용성에 초점을 맞춰야 한다. 재사용성이라는 것은 만약에 화면 4개가 순차적으로 연결되어있고 각각의 화면마다 같은 데이터 Form 데이터 양식을 전달해줘야 한다면 재사용되는 클래스인 커맨드 객체를 만들어서 사용하는 것이 좋다는 뜻이다.
이렇게 구현된다면 오류없이 이전에 제출한 데이터가 다음 화면에 온전하게 표현된다는 장점이 있다.
만약 '재사용성'의 목적이 아닌 @RequestBody처럼 단순히 데이터를 받아오는 DTO 역할로 쓰이기에는 좋지 않을 수도 있다.
기능이 동일한 아래 두개의 코드를 봐보자.
위의 코드는 클라이언트에서 넘어온 Form 데이터를 받아오기 위해 @ModelAttribute를 사용했다.
위의 코드는 @ModelAttribute를 사용하지 않았지만 사용한 것과 기능적으로 동일하다.
위의 코드를 보다시피 TestDto는 다른 곳에서 쓰이지 않아 재사용성도 없는 Class이며 Parameter도 고작 1개만 매칭시킬 뿐이다.
코드가 적다고해서 모두 좋은 코드는 아니듯이 고작 받아올 데이터가 memberName 하나 뿐이라면 @ModelAttribute를 쓰는 것보다 HttpServletRequest로 Get 요청을 받아오거나 @RequestParam으로 Get과 Post 데이터를 받아오는게 코드 분석에는 더 용이하고 직관적이다.
참고 - @RequestParam을 적으나 안적으나 데이터가 바인딩 되는건 매한가지라 안쓰는 경우가 생길 수 있지만 권장하지 않는다. @RequestParam은 스프링 기능을 이용하는데, MultipartResolver 추상화와 관련되어 데이터가 바인딩되고 @RequestParam을 적지 않으면 WebDataBinder 클래스가 데이터를 바인딩한다. 스프링 기능을 이용하면 단순 Primitive Type 뿐만 아니라 어지간한 타입은 다 제네릭으로 받기 때문에 웬만하면 바인딩이 가능하지만 WebDataBinder는 그러한 기능이 없다. 또 @RequestParam을 이용해서 데이터 바인딩 옵션을 여러가지 줄 수 있으므로 매번 달아주는 것을 습관화 하는 것이 좋다. 이 내용은 @ModelAttribute도 마찬가지다.
그렇기에 클린 코드 측면에서는 불필요한 @ModelAttribute의 커맨드 객체를 만들어서 쓰는 것은 좋지 않다.
하지만 설계적인 시선에서 바라 볼 때는 모든 Request 데이터는 @ModelAttribute 형식으로 통일해서 사용하는 것이 더 좋을 수도 있다. 모든 개발자가 동일한 룰에 맞춰서 개발하는 것이 추후 유지보수가 용이하기 때문이다. 특히 바로 아래 설명할 타임리프에서는 재사용성 측면의 장점 이외의 장점들도 더 많기 때문에 @ModelAttribute를 단순한 DTO로써 활용하는 것이 좋다.
보통 JSP 프로젝트에서는 커맨드 객체를 쓰기에는 타임리프와 같은 장점이 많지 않기 때문에 @ModelAttribute를 사용하지 않는 경우가 많다. 하지만 JSP 영역에서도 재사용성이 강조되는 Form 객체라면 쓰는 것이 좋다.
그래서 다시 요약하자면 결국엔 커맨드 객체와 DTO는 다르지만 @ModelAttribute를 재사용성보다는 단순히 DTO로 사용했을 때의 장점도 많기 때문에 커맨드 객체와 DTO를 동일시해서 부르는 경우가 있을 뿐이다.
타임리프에서 @ModelAttribute 사용하기
타임리프는 @ModelAttribute를 안쓰고 사용할 수 없을 정도로 굉장히 밀접한 템플릿 엔진이다.
타임리프에서는 @ModelAttribute를 사용함으로써 얻을 장점이 너무 많다.
Form 태그의 th:object
첫번째로 form 태그에서 th:object를 이용하면 커맨드 객체를 이용하기가 쉽다.
그래서 아예 th:object를 @ModelAttribute와 결합된 개념이다라고 설명하는 경우도 많다.
nameInfo라는 커맨드 객체를 form 태그에 th:object 안에 넣으면 nameInfo 객체 안에 있는 요소인 memberName을 *{}로 꺼내서 쓸 수 있다.
물론 th:object를 설정하지 않고 ${} 태그를 이용해서 직접 꺼낼 수도 있다.
th:object에 대한 개념을 찾아보면 @ModelAttribute와 연관지어서 "th:object는 커맨드 객체를 지정할 수 있다"라고 설명한 블로그가 굉장히 많은데, 정확히는 커맨드 객체 뿐만 아니라 모든 객체가 들어갈 수 있다.
위에서 설명한 코드 중 하나를 다시 가져와서 보자.
위의 코드에서 TestDto는 커맨드 객체(폼 객체)가 아니다.
TestDto는 Form 데이터를 받아오는 역할이 아니라 Database에서 받아오는 객체일 수도 있기 때문이다.
그렇기에 TestDto는 커맨드 객체가 아니라 단순히 View에 데이터를 전달하는 객체일 뿐이다.
그러므로 th:object는 커맨드 객체만을 위한 기술이라고 설명하면 안된다.
그냥 주로 @ModelAttribute와 같이 쓰는 기술일 뿐이다.
SessionAttributes
@ModelAttribute는 Session 기능과 연결할 수도 있는데 그게 바로 @SessionAttributes다.
아래와 같이 컨트롤러 상단에 @SessionAttributes를 적어주자.
그러면 컨트롤러 메소드에서 model.addAttribute로 @SessionAttribute에 설정된 값인 "nameInfo"로 값을 설정해주면 해당 값은 Session에 저장된다.
그래서 이런 경우는 지속적으로 사용자의 입력 값을 유지하고 싶을 때 사용하는 데, 마치 이전 데이터가 캐싱되는 듯한 느낌을 받을 수 있다. 적절한 예로는 장바구니가 있다. 이 기능을 이용하면 사용자는 어느 페이지를 갔다오더라도 장바구니가 유지된다. 사용자의 데이터를 DB에 저장시키지 않을 때도 유용하게 사용된다. Session을 직접 구현하는 것보다 Model을 커맨드 객체로 데이터 바인딩하는게 더 효율적이기 때문이다.
참고 - 비슷한 Annotation으로 끝에 s가 빠진 @SessionAttribute가 있는데 컨트롤러 밖(인터셉터 또는 필터 등)에서 만들어 준 세션 데이터에 접근할 때 사용한다.
bindingResult
커맨드 객체로 사용된 @ModelAttribute 뒤에 BindingResult를 적어주면 말 그대로 Binding에 대한 결과를 보여준다.
바인딩 시 타입 매칭으로 실패하는 경우에 바로 에러를 뱉지않고 bindingResult에 담겨서 실행된다.
다만, 개인적으로 느끼기에 bindingResult를 @ModelAttribute을 사용하는 Form 객체와 같이 사용하는건 개발의 자율성이 떨어진다고 보는 편이다.
게시글(/board)과 그 페이지 안에 있는 댓글의 관계를 생각해보면 댓글은 위와 같은 방식으로 구현하기가 애매하다.
왜냐면 bindingResult에 에러 값이 존재한다면 다시 원래 페이지(/board)를 return 하면서 오류가 난 필드 값에 정보를 뿌려주게 되는데,
첫번째로 이 과정에서 게시글은 @PostMapping을 하더라도 URI가 /board로 일치하는 반면에 댓글 등록은 URI가 /board/comment로 끝나서 결과 페이지의 URI(/board/comment)가 이전 페이지 URI(/board)와 일치하지 않는다는 단점이 존재한다.
두번째는 /board/comment를 호출하면 에러가 날 시에 새로고침을 해야하기 때문에 댓글 뿐 아니라 게시글에 관한 Form 데이터를 다시 저장했다가 담아줘야한다는 단점도 존재한다. 고작 댓글 등록의 기능을 위해서 게시글과 관련된 모든 Form 값을 다 받아서 뿌려줘야 하는건 효율성이 떨어진다. 그렇다고 댓글만 ajax로 처리하자니 개발의 통일성이 떨어지기 때문에 일관성이 없다.
그래서 서버검증은 @RequestBody에 한해서 하는 것이 좋다.
@Valid
타임리프에서 유효성 검사를 서버에서 할 수가 있는데 이 기능을 @ModelAttribute와 같이 사용할 수 있다.
아래 처럼 @Valid를 적어주고 적용된 클래스에 @NotNull을 붙여주면 해당 데이터가 null로 바인딩 되는 경우에는 bindingResult를 통해 에러 내용을 보여준다. 또한 @Validated를 통해 각각의 케이스마다 서로 다른 유효성 검사를 실행 시킬 수도 있다.
'🍃 Spring' 카테고리의 다른 글
@Component와 Static 메소드 중 Utility Class는 어디에 만들까? (0) | 2022.11.13 |
---|---|
ModelMapper, MapStruct 빠른 기초 사용법 (0) | 2022.10.29 |
Spring Boot로 만드는 Spring Security 로그인 구현 - JWT(2) (6) | 2022.10.18 |
Spring Boot로 만드는 Spring Security 로그인 구현 - Session(1) (2) | 2022.10.10 |
Spring에서 인터페이스가 실행이 되지? (0) | 2022.10.09 |