[Spring] @ModelAttribute 및 중첩 커맨드 객체, Model & ModelAndView

| @ModelAttribute


@ModelAttribute는 스프링에서 JSP파일에 반환되는 Model 객체에 속성값을 주입하거나 바인딩할 때 사용되는 어노테이션이다. 컨트롤러(Controller) 객체에서 2가지 방법으로 사용된다.


@ModelAttribute("serverTime")
public String getServerTime(Locale locale) {
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
return dateFormat.format(date);
}

먼저 메서드에 @ModelAttribute를 붙이는 경우다. 이 때 serverTime 속성을 Model 객체에 아래와 같은 코드를 실행함으로서 반환되는 dateFormat.format(date) 값을 바인딩한다. 이 값은 JSP 파일에서 사용할 수 있다.


@RequestMapping(value="/memJoin", method=RequestMethod.POST)
public String memJoin(@ModelAttribute("mem") Member member) {
service.memberRegister(member);
return "memJoinOk";
}

다음은 컨트롤러 메서드의 인수에 어노테이션을 부착하는 경우다. @ModelAttribute을 써서 HTTP 요청에 들어있는 속성값들을 Member 커맨드 객체에 자동적으로 바인딩 하게 된다. 만일 @ModelAttribute([NAME]) 형태로 붙일경우 JSP파일에서 ${[NAME].property} 형태로 Model 객체의 값을 사용할 수 있게 된다.


@Controller
@RequestMapping("/member")
public class MemberController {

@Resource(name="memService")
MemberService service;

@ModelAttribute("serverTime")
public String getServerTime(Locale locale) {
Date date = new Date();
DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.LONG, locale);
return dateFormat.format(date);
}

@RequestMapping(value="/memJoin", method=RequestMethod.POST)
public String memJoin(@ModelAttribute("mem") Member member) {
service.memberRegister(member);
return "memJoinOk";
}
}

위 두 가지 방식을 동시에 써서 @ModelAttribute를 써서 JSP파일에 전달하는 Model 객체에 serverTime 속성과 mem으로 표현되는 Member 객체의 데이터를 넘겨줄 수 있다. 


<%@ page language="java" contentType="text/html; charset=EUC-KR"
pageEncoding="EUC-KR"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>memJoinOk</title>
</head>
<body>
<h1> memJoinOk</h1>

ID : ${mem.memId} <br/>
PW : ${mem.memPw} <br/>
Mail : ${mem.memMail} <br/>
Phone : ${mem.memPhone.memPhone1} - ${mem.memPhone.memPhone2} - ${mem.memPhone.memPhone3} <br/>
<P> The time on the server is ${serverTime}. </P>
<a href="/mvc/resources/html/memJoin.html"> Go MemberJoin</a>
</body>
</html>


| 중첩 커맨드 객체


사용자가 HTTP로 사용자 데이터를 보내올 때 커맨드 객체를 여러 번 중첩시켜서 보낼 수 있다. 이 중첩 커맨드 객체는 VOList 형태로 데이터를 받을 수 있다. HTML 파일에서 데이터를 전송할 때 데이터의 name 속성은 아래처럼 커맨드 객체의 속성명과 인덱스 그리고 속성을 명시해야한다.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>Member Join</h1>
<form action="/lec19/member/memJoin" method="post">
...

PHONE1 : <input type="text" name="memPhones[0].memPhone1" size="5"> -
<input type="text" name="memPhones[0].memPhone2" size="5"> -
<input type="text" name="memPhones[0].memPhone3" size="5"><br />
PHONE2 : <input type="text" name="memPhones[1].memPhone1" size="5"> -
<input type="text" name="memPhones[1].memPhone2" size="5"> -
<input type="text" name="memPhones[1].memPhone3" size="5"><br />

...
</form>
<a href="/lec19/resources/html/login.html">LOGIN</a> &nbsp;&nbsp; <a href="/lec19/resources/html/index.html">MAIN</a>
</body>
</html>

PHONE1, PHONE2의 중첩 커맨드 객체는 아래의 List형태의 MemPhone 객체 리스트로 받을 수 있다.


public class Member {

private String memId;
private String memPw;
private String memMail;
private List<MemPhone> memPhones;

...
}
public class MemPhone {

private String memPhone1;
private String memPhone2;
private String memPhone3;

public String getMemPhone1() {
return memPhone1;
}
public void setMemPhone1(String memPhone1) {
this.memPhone1 = memPhone1;
}
public String getMemPhone2() {
return memPhone2;
}
public void setMemPhone2(String memPhone2) {
this.memPhone2 = memPhone2;
}
public String getMemPhone3() {
return memPhone3;
}
public void setMemPhone3(String memPhone3) {
this.memPhone3 = memPhone3;
}

}


| Model & ModelAndView 


컨트롤러에서 뷰에 데이터를 전달하기 위해 사용되는 객체로 ModelModelAndView가 있다. ModelModelAndView의 차이는 Model은 데이터만을 전달하기 위한 객체이고 ModelAndView는 데이터와 뷰의 이름을 함께 전달하는 객체다.


아래 코드를 보면 Model은 객체에 속성값을 넣어 JSP에 해당 속성값을 제공하는 역할을 한다. ModelAndViewModel이 하는 역할 뿐만 아니라 해당 뷰의 이름을 추가하여 전달할 수 있다. 또한 메서드의 리턴값에도 차이가 있다.

@RequestMapping(value = "/memModify", method = RequestMethod.POST)
public String memModify(Model model, Member member) {

Member[] members = service.memberModify(member);

model.addAttribute("memBef", members[0]);
model.addAttribute("memAft", members[1]);

return "memModifyOk";
}


@RequestMapping(value = "/memModify", method = RequestMethod.POST)
public ModelAndView memModify(Member member) {

Member[] members = service.memberModify(member);

ModelAndView mav = new ModelAndView();
mav.addObject("memBef", members[0]);
mav.addObject("memAft", members[1]);

mav.setViewName("memModifyOk");

return mav;
}


이 글을 공유하기

댓글(3)

  • 2019.08.22 13:18 신고

    안녕하세요 자세한 강좌를 올려 주셔서 잘 보고 있습니다.
    궁금한 부분이 있는데요
    @ModelAttirbue 가 메소드에 붙으면 해당 컨트롤러(클래스) 안에 있는 모든 서버스에
    같이 넘어 가게 되는 건가요?
    예제를 보면 memJoin 서비스를 호출할때 같이 serverTime 가 view에 넘어가자나요?
    만약에 memModify나 memDelete 서버스가 더 있다고 할때
    이 서비스를 호출 할때도 같이 serverTime가 넘어 가는 것인지
    어떤 조건이 있어서 특별한 조건이 만족할 때만 serverTime가 넘어 가는 것인지 궁금합니다.

    • 2019.08.23 17:34 신고

      댓글 감사합니다! Model 객체에 serverTime이 추가되어 넘어갑니다. 따라서 memModify, memDelete 서비스를 추가해도 같이 model에 추가되어 view 단으로 넘어갑니다!

  • 크게될개발자
    2021.04.21 07:00

    양질의 포스팅 인상깊게 보았습니다!

    검색을 하던 중에 들어오게 됐는데요.
    중첩이란 키워드 때문에 오게 되었는데 이렇게 질문까지 드리게 되네요.

    바인딩에 대해서 지금 제가 겪고 있는 문제에 자문을 여쭙고자 합니다.

    다음과 같이 컨트롤러 내에 핸들러 메서드가 있고, 그 파라미터 타입인 Article 은 다음과 같습니다.
    간단히 게시물 작성을 처리하는 핸들러이며, 게시물 작성 시 파일을 여러개 첨부할 수 있습니다.

    // 핸들러
    public ResponseDto addArticle(Article article, @CurrentAccount Account currentAccount) {

    (...생략...)

    }

    // Article
    public class Article {
      private Integer id;

      (...중략...)

      private List<MultipartFile> multipartFiles;
    }


    게시물의 첨부파일 정보를 바인딩 받을 때의 테크닉에 대해 조언을 얻고자 합니다.
    질문을 요약하면
    커맨드 객체 내 필드의 타입이
    특정 Collection 인 경우의 바인딩을 처리할 수 있는 방법을 알고자 합니다.


    기존에 제가 MultipartFile 타입을 바인딩 받는 방식은, 메서드에 해당 타입의 파라미터를 별개로 두고 바인딩 받았습니다.

    그런데 위의 코드처럼
    커맨드 객체인 Article 내에 List<MultipartFile> multipartFiles 로
    필드를 추가 후,
    MultipartFile 타입의 바인딩을
    Article 내의 List<MultipartFile> 필드로 깔끔하게 바인딩 받고자 합니다.
    하지만 방법을 몰라서 어떻게 해야 좋을지 조언을 구하고 싶습니다.

    일단 제가 지금까지 시도 해본 것은 다음과 같습니다.

    앞서 말씀드렸듯이
    Article 타입 파라미터와는
    아예 별개의 파라미터로 바인딩 받도록 처리하던 방식입니다.

    가장 기본적인 방식으로
    Map<String, MultipartFile> fileMap 또는 List<MultipartFile> fileList 등으로 파라미터를 따로 두어 바인딩을 받았습니다.

    (혹은 MultipartRequest 또는 MultipartHttpServletRequest 타입의 파라미터를 두고 아예 Multipart 타입 요청 객체를 주입 받아서 꺼내는 방식 또한 마찬가지겠지요.)

    다음으로 시도한 것은
    바인딩 시 Validation 및 조금 더 커스텀 처리를 해주면 좋겠다 싶어서
    HandlerMethodArgumentResolver 를 구현해서 바인딩 받아보았습니다.

    그 다음 시도해본 것은
    어차피 맥은 위와 크게 다르지 않습니다. 그저 특정 Collection 타입으로 감싼 파일 전용 래퍼 DTO 를 만들고, 위에서 했던 처리를 조금 더 편하게 하던 방식이었으나, 완성된 코드를 보아하니 나만의 은밀한 비밀 x 끔찍한 혼종과도 같았습니다.

    가장 큰 이유는
    Wrapper 객체를 만들고 쓰는 이유가 연관된 것 끼리 한꺼번에 모아 담아 처리하고자 만드는 건데,
    나는 지금 Article과 연관된 MultipartFile 처리를 한꺼번에 하고 싶은 건데 따로 놀고 있는 것 입니다.

    결국 뭘 어떻게 지지고 볶든
    Article 과 MultipartFile 바인딩을 처리하는 파라미터는 분리되어 있었기 때문입니다.

    아.......

    Article 객체 내에 존재하는
    private List<MultipartFile> multipartFiles 필드 딱 하나만 저격해서 커스텀하게 바인딩 처리할 수 있으면 좋겠는데....

    HandlerMethodArgumentResolver 를 사용해서 구현하는 경우는
    multipartFiles 필드 외에도 Article의 다른 모든 필드 또한 빠짐없이 손수 바인딩 해줘야 하므로 원하는 것 이상으로
    쓸데없이 코드가 장황해집니다.

    더불어 가장 큰 문제는 확장 및 유지보수의 관점입니다.
    Article 내 필드가 추가되거나 삭제될 때 마다 항상 아규먼트 리졸버도 항상 같이 신경써야 할 여지가 생깁니다.

    더욱 더 지저분하고 장황해지고,
    별 것도 아닌 처리에
    저 혼자만의 약속 인냥 코드가 만들어지는 것이 너무나 보기가 싫었습니다.

    만약 제3자가 뭐 하나 수정하다 문제가 생기면 코드를 추적하면서 굉장히 짜증나고 더러워 보이기 그지 없지 않을까.

    생각이 들었습니다.

    혹시나 HandlerMethodArgumentResolver 구현 시, 특정 필드만 적용되는 방법이 있을까요. 아니면 더 매력적인 방법이 있을까요

    지금 문제를 해결하면서,
    누구나 읽기 쉽고 유지보수에도 영향을 안주는 형태로
    간단하게 일을 처리하려면
    어떻게 해야 좋을지 자문을 여쭙고자 합니다.


    질문을 정리하겠습니다.

    컨트롤러의 핸들러 메서드에는
    파라미터로 Article 타입 하나만 선언해서 스프링이 지원해주는 기본적인 바인딩을 제공받되, 그 안에 있는 List<MultipartFile> 타입의 필드인 multipartFiles 하나만
    제 나름대로 커스텀하게 바인딩 해주고 싶은데, 좋은 방법이 있을지 배우고 싶어 두서없이 질문 드립니다.

    public class Article {
      private Integer id;

      (...중략...)

      private List<MultipartFile> multipartFiles; // 딱 요놈만!
    }

Designed by JB FACTORY