@Controller의 메소드 파라미터로 자유롭게 사용할 수 있는 파라미터들.

 

-HttpServletRequest, HttpServletResponse

 

-HttpSession

HttpSession오브젝트는 HttpServletRequest를 통해 가져올 수도 있지만, HTTP세션만 필요한 경우라면 HttpSession타입 파라미터를 선언해서 직접 받는 편이 낫다. HttpSession은 서버에 따라서 멀티스레드 환경에서 안정성이 보장되지 않는다. 서버에 상관없이 HttpSession을 안전하게 사용하려면 핸들러 어댑터의 synchronizeOnSession프로퍼티를 true로 설정해줘야 한다. 

 

-WebRequest, NativeWebRequest

WebRequest는 HttpServletRequest의 요청정보를 대부분 그대로 갖고 있는, 서블릿 API에 종속적이지 않은 오브젝트 타입. WebRequest는 원래 서블릿과 포틀릿 환경 양쪽에 모두 적용 가능한 범용적인 핸들러 인터셉터를 만들 때 활용하기 위해 만들어졌다. 따라서 스프링 서블릿/MVC의 컨트롤러에서라면 꼭 필요한 건 아니다. 따라서 스프링 서블릿/MVC의 컨트롤러에서라면 꼭 필요한 건 아니다. 

NativeWebRequest에는 WebRequest내부에 감춰진 HttpServletRequest와 같은 환경종속적인 오브젝트를 가져올 수 있는 메서드가 추가되어있다. 

 

-Locale

 

-InputStream,Reader

 

-OuputStream,Writer

 

-@PathVariable

@RequestMappin의 URL에 {} 로 들어가는 패스 변수를 받는다. 요청 파라미터를 URL의 쿼리 스트링으로 보내는 대신 URL패스로 풀어서 쓰는 방식을 쓰는 경우 유용하다. 

 예를들어 id가 10인 사용자를 조회하는 페이지의 URL을 쿼리 스트링으로 파라미터를 전달하면 보통 다음과 같다.

 

 /user/view?id=10

 

파라미터를 URL 경로에 포함시키는 방식은 

 

/user/view/10

 

문제는 이렇게 일부가 달라질 수 있는 URL을 특정 컨트롤러에 매핑하는 방법과 URL중에서 파라미터에 해당하는 값을 컨트롤러에서 참조하는 방법이다. @Controlloer는 URL에서 파라미터에 해당하는 부분에 {}을 넣는 URL템플릿을 사용할 수 있다. 컨트롤러 메서드 파라미터에는 @PathVariable 어노테이션 이용해 URI템플릿 중에서 어떤 파라미터를 가져올지를 지정할 수 있다. 

 

1
2
3
4
5
@RequestMapping("/member/{membercode}/order/{orderid}")
    public String lookup(@PathVariable("membercode"String code,
                         @PathVariable("orderid"int orderid) {
        ...
    }
cs

타입이 일치되지 않는 값이 들어오면 어떻게 될까? 예를들어 orderid는 int 타입변수로 전달돼야 하는데 다음과 같은 URL이 들어오면 타입 변환이 불가능하다. 이떄는 HTTP400- BadRequest응답 코드가 전달될 것이다.

 

-@RequestParam

단일 HTTP 요청 파라미터를 메서드 파라미터에 넣어주는 어노테이션이다. 가져올 요청파라미터의 이름을 @RequestParam 어노테이션의 기본 값으로 지정해주면 된다. 요청 파라미터의 값은 메서드 파라미터의 타입에 따라 적절하게 변환된다. 

 

@RequestParam에 파라미터 이름을 지정하지 않고 Map<String, String> 타입으로 선언하면 모든 요청 파라미터를 담은 맵으로 받을 수 있다. 파라미터 이름은 맵의 키에, 파라미터 값은 맵의 값에 담겨 전달된다. 

 

@RequestParam을 사용했다면 해당 파라미터가 반드시 있어야 한다. 없다면 HTTP 400 -Bad Request를 받는다.

 필수가 아니라 선택적으로 제공하려면 required 엘리먼트를 false로 설정해주면 된다. 

요청 파라미터가 존재하지 않을 때 사용할 디폴트 값도 지정할 수 있다.

 

public void view(@RequestParam(value="id", required=false , defaultValue=-1") int id) {...}

 

자바 코드를 컴파일할 때 디버깅 정보를 모두 삭제하는 수준의 최적화를 하지 않았다면 클래스 파일 안에 파라미터의 이름 정보가 남아 있다. 메서드 파라미터의 이름과 요청 파라미터의 이름이 일치한다면 @RequestParam의 이름은 다음과 같이 생략할 수도 있다. 

 public String view(@RequestParam int id) { ... }

 

 String, int 같은 단순 타입인 경우는 @Request Param을 아예 생략할 수도 있다. 이때는 메서드 파라미터와 같은 이름의 요청 파라미터 값을 받는다. 하지만 파라미터의 개수가 많고 종류가 다양하면 코드를 이해하는데 불편할 수 있다.

단순한 메서드가 아니라면 명시적으로 @RequestParam을 부여해주는 것 권장.

 

-@CookieValue

Http 요청과 함께 전달된 쿠키 값을 메서드 파라미터에 넣어주도록 @CookieValue를 사용할 수 있다. 어노테이션 기본 값에 쿠키의 이름을 지정해주면 된다.

 

public String check(@CookieValue("auth") String auth){...}

 

@CookieValue도 @RequestParam과 마찬가지로 지정된 쿠키 값이 반드시 존재해야만 한다. 지정한 쿠키 값이 없을 경우에도 예외가 발생하지 않게 하려면, @CookieValue의 required엘리먼트를 false로 선언해줘야 한다. 또한 디폴트 값으로 대신하게 할 수 있다. 

public String check(@CookieValue(value="auth", required=false, defalutValue="NONE" String auth) {...}

 

-@RequestHeader

요청 헤더정보를 메서드 파라미터에 넣어주는 어노테이션이다. 어노테이션의 기본 값으로 가져올 HTTP헤더의 이름을 지정한다. 

 

-Map,Model ,ModelMap

Model과 ModelMap은 모두 addAtrribute() 메서드를 제공해준다. 일반적인 맵의 put() 처럼 이름을 지정해서 오브젝트 값을 넣을 수 도 있고, 자동 이름 생성 기능을 이용한다면 오브젝트만 넣을 수도 있다. 예를 들어 다음과 같이 ModelMap에 User 타입의 오브젝트를 넣는다면 타입정보를 참고해서 "user" 라는 모델 이름이 자동으로 부여된다. 

@RequestMapping(...)

public void hello(ModelMap model){

   User user- new User(1,"Spring");

   model.addAttribute(user); -> addAttribute("user",user)와 동일

}

ModelMap과 Model의 addAllAttribute() 메서드를 사용하면 Collection에 담긴 모든 오브젝트를 자동 이름 생성방식 적용해서 모두 모델로 추가해준다. 

 

-@ModelAttribute

@ModelAttribute는 여기서 소개하는 것처럼 메서드 파라미터에도 부여할 수 있고 메서드 레벨에 적용할 수도 있다. 

두가지가 비슷한 개념이지만 사용 목적이 다름. @ModelAttribute는 이름 그대로 모델로 사용되는 오브젝트다. 하지만 일반적으로 컨트롤러가 뷰에 출력할 정보를 전달하기 위해 ModelAndView에 담아서 전달하는 모델과 의미가 좀 다름.

컨트롤러가 뷰에 전달하는 모델 오브젝트는 하나가 아니다. 그래서 맵 형태의 컬렉션을 이용해 여러 개의 모델 오브젝트를 담아서 전달하는 것이다. 보통 모델 맵을 모델이라고 부르기도 하니 이를 잘 구분해야한다.

@ModelAttribute는 모델 맵에 담겨서 뷰에 전달되는 모델 오브젝트의 한 가지라고도 볼 수 있다. 

기본적으로 므댄 @ModelAttribute는 별도의 설정 없이도 자동으로 뷰에 전달된다. 그렇다면 @ModelAttribute가 붙은 모델은 그 밖의 모델과 어떤 차이점이 있을까?

 지금까지는 MVC에서 모델은 컨트롤러가 생성해서 뷰에 전달해주는 것이라고만 설명했다. 물론 모델 정보를 생성하고 조작하는 것은 컨트롤러의 몫이다 그런데 컨트롤러가 사용하는 모델 중에는 클라이언트로부터 받는 HTTP요청정보를 이용해 생성되는 것도 있다. 단순히 검색을 위한 파라미터처럼 컨트롤러가 전달받아서 내부 로직에 사용하고 필요에 따라 다시 화면에 출력하기도 하는 요청 정보도 있다. 이렇게 클라이언트로부터 컨트롤러가 받는 요청정보중에서 하나 이상의 값을 가진 오브젝트 형태로 만들 수 있는 구조적인 정보를 @ModelAttribute모델이라고 부른다. @ModelAttribute는 이렇게 컨트롤러가 전달받는 오브젝트 형태의 정보를 가리키는 말이다. 

 

 컨트롤러가 클라이언트에 전달받는 정보 중 가장 단순한 형태는 요청파라미터다. GET메서드라면 URL에 name=String같은 쿼리스트링을 통해서 전달될 것이고, POST라면 <input type="text" name="name" value="Spring" /> 과 같이 폼의 필드 값으로 전달될 것이다.  HTTP메서드를 굳이 구분하지 않는다면 뭉뚱그려서 그냥 요청파라미터라고 하면된다. name과 같은 파라미터는 @RequestParam어노테이션으로 받으면 된다. 그렇다면 사용자가 제공하는 정보 중에서 단순히 @RequestParam이 아니라 @ModelAttribyte를 사용해서 모델로 받는 것은 어떤게 있을까? 사실 정보의 종류가 다른 건 아니다. 단지 요청 파라미터를 메서드 파라미터에서 1:1로 받으면 RequestParam이고, 도메인 오브젝트나 DTO의 프로퍼티에 요청파라미터를 바인딩해서 받으면 @ModelAttribute라고 볼 수 있다. 하나의 오브젝트에 클라이언트의 요청정보를 담아서 한번에 전달되는 것이기 때문에 이를 커맨드 패턴에서 말하는 커맨드 오브젝트라고 부르기도 한다.

 

 사용자 검색기능을 담당하는 컨트롤러가 있다고 하자. 검색 조건에는 id,name,level,email 네 가지가 사용된다고 해보자. 검색 페이지는 데이터가 바뀌지 않는 한 반복해서 접근할 수 있고 북마크가 가능해야 한다. 따라서 GET 메서드를 사용할 것이고, 이 정보는 URL의 쿼리 스트링 내의 파라미터로 다음과 같이 전달 될 것이다.

 

 /user/search?id=100&name=Spring&level3&email=admin@spring.com

 

 물론 검색조건 네 가지가 항상 다 채워져야 하는 것은 아닐 테니 일부는 생략될 수 있다. 어쨋든 컨트롤러의 역할은 이 검색조건을 받아서 검색 기능을 제공하는 서비스계층오브젝트에게 넘겨서 결과를 뷰에 담아 클라이언트에 돌려주는 것이다.

 

 가장 단순한 방법은 각 파라미터를 @RequestParam으로 받아서, 그대로 서비스 계층의 검색용메서드에 전달하는 것이다.

 검색조건은 추가될 수도 있고 변경될 수도 있다. 그에 따라 요청 파라미터 개수가 늘어난다. 그런데, 그때마다 서비스 계층의 검색 메서드 파라미터를 일일이 추가하는 것은 비효율적이다. 같은 타입의 파라미터가 여러 개 있으면 순서가 뒤바뀌어서 버그를 만들어낼 수도 있다. 그래서 이보다는 여러 개의 정보를 담을 수 있는 오브젝트에 모든 검색조건을 넣는 편이 낫다. 그러면 서비스 계층 메서드의 파라미터도 하나로 충분하다. 

 

 검색조건과 같은 정보를 @ModelAttribute가 붙은 파라미터 타입의 오브젝트에 모두 담아 전달해주는 것은 커맨드라고 부른다. UserSearch에 이 정보를 활용하는 부가기능까지 넣는다면 본격적인 커맨드 패턴의 오브젝트처럼 사용할 수 있기 때문.

 비즈니스로직에서 사용하는 검색조건 같은 정보 말고 웹 페이지의 폼 데이터받는 경우에도 @ModelAttribute파라미터로 사용. 폼의 필드에 담긴 정보를 도메인 오브젝트 등에 저장한 것을 컨트롤러 메서드가 한 번에 받을 수 있다. 사용자 정보 등록이 아니라 수정의 경우에도 마찬가지.

 

 수정용 폼을 띄우는 작업을 담당하는 컨트롤러를 생각해보자. 이때는 수정이 필요한 사용자 ID정도를 파라미터로 받아서 DB에서 기존 사용자 정보를 가져온 뒤에 폼을 출력하면서 사용자 정보를 넣어줄 것이다. 이때 User오브젝트가 컨트롤러에서 준비해서 뷰에 전달하는 개념의 모델로 활용된다. 

 결국 같은 User오브젝트인데 한 번은 컨트롤러에서 뷰로 전달하기 위해 사용하고, 다른 한번은 뷰를 통해서 출력된 폼의 정보를 다시 컨트롤러가 가져오는 용도로 사용한다. 이 두가지 모두 User는 모델로서 사용된다. 폼의 서브밋을 처리하는 컨트롤러라면 @ModelAttiribute를 통해 User오브젝트를 전달받을 것이다. 이 때  폼에서 입력한 정보에 오류가 있어서 User오브젝트에 바로 저장할 수 없다면, 다시 폼을 띄워주면서 잘못입력한 값을 보여주고 재입력을 요청할 것이다. 이 경우에는 일단 @ModelAttribute로 받은 User 정보가 다시 뷰에 출력되기 위한 모델로 사용된다. 그래서 이처럼 서브밋된 폼의 내용을 저장해서 전달받거나, 뷰로 넘겨서 출력하기위해 사용되는 오브젝트를 모델 애트리뷰트라 부르고 @ModelAttribute를 붙여주는 것이다. @ModelAttribute는 컨트롤러가 리턴하는 모델에 파라미터로 전달한 오브젝트를 자동으로 추가해준다. 

 

 -Erros,BindingResult

@ModelAttribute는 단지 오브젝트에 여러 개의 요청 파라미터 값을 넣어서 넘겨주는게 전부가 아니다. 

 @RequestParam과 달리 검증작업이 추가적으로 진행된다. 변환이 불가능한 타입의 요청 파라미터가 들어왔을때를 보자.

 

 @RequestParam은 스프링의 기본 타입 변환 기능을 이용해서 요청 파라미터값을 메서드 파라미터 타입으로 변환한다. 가장 단순한 것은 스트링이다. URL의 쿼리 스트링이나 폼 필드는 멀티 타입이 아니면 문자열로 오기떄문.

숫자 타입의 파라미터라면 스트링 타입으로 들어온 요청파라미터의 타입 변환을 시도한다. 성공한다면 int 같은 숫자형 타입의 메서드 파라미터로 전달되지만, 변환 작업 실패시 HTTP400-BadRequest응답이 클라로 전달된다.

 

 예를 들어 다음과 같은 메서드 선언이 있을때 요청 URL이 /user/view?id=abcd라면 어떻게 될까? 이떄는 id값인 abcd를 숫자로 변환 중에 예외가 발생할 것이다. 

 

public String view(@RequestParam int id) {..} 

 

 URL에서 조회조건에 해당하는 id와 같은 파라미터는 사용자의 입력 값이 아니라 다른 웹페이지의 링크에 미리 생성된 것일 가능성이 높다. 따라서 타입이 맞지 않는다는 건 프로그램 버그라고 봐야 한다. 메시지를 보여주고 싶다면 예외를 처리하는 핸들러 예외 리졸버를 추가해주면 된다. 

 

 그런데 @ModelAttribute를 사용했을 때는 다르다. @ModelAttribute오브젝트에 요청 프로퍼티 값을 넣다가 타입이 일치하지 않아서 예외가 발생하면?

 

 public String search(@ModelAttribute UserSearch,BindingResult result){ ... } 

 

 UserSearch의 setId()를 이용해 id값을 넣으려고 시도하다가 예외를 만나게 될 것이다. 하지만 이떄는 작업이 중단되고 HTTP400 응답 상태 코드가 클라로 전달X. 타입 변환에 실패하더라도 작업은 계속 진행된다. 단지 타입 변환 중에 발생한 예외가 BindException 타입의 오브젝트에 담겨서 컨트롤러로 전달될 뿐이다. 왜 바로 에러 처리하지않지?

 

 그 이유는 @ModelAttrivute는 요청 파라미터의 타입이 모델 오브젝트의 프로퍼티 타입과 일치하는지를 포함한 다양한 방식의 검증 기능을 수행하기 떄문. @ModelAttribute입장에서는 파라미터 타입이 일치하지 않는다는 건 검증 작업의 한가지 결과일 뿐이지, 예상치 못한 예외상황이 아니라는 뜻이다. 별도의 검증 과정없이 무조건 프로퍼티 타입으로 변환해서 값을 넣으려고 시도하는 @RequestParam과는 그런 면에서 차이가 있다. 

 

 사용자가 직접 입력하는 폼에서 들어오는 정보라면 반드시 검증이 필요하다. 버튼이나 링크에 미리 할당된 URL에 담겨 있는 파라미터와 달리 사용자가 입력하는 값에는 다양한 오류가 있을 수 있기 때문이다. 사용자가 입력한 폼의 데이터를 검증하는 작업에는 타입 확인뿐 아니라 필수정보의 입력 여부, 길이 제한, 포맷, 값의 허용범위 등 다양한 검증기준이 적용될 수 있다. 이렇게 검증과정 거친뒤 오류가 발견됐다고 하더라도 HTTP400과 같은 예외 응답 상태를 전달하면서 작업을 종료하면 안 된다. 어느 웹사이트에 가서 회원가입을 하는 중에 필수 항목을 하나 빼먹었다고 호출스택 정보와 함계 HTTP400에러 메시지가 나타나면 얼마나 황당??...

 

 

 그래서 사용자의 입력값에 오류가 있을 때는 이에 대한 처리를 컨트롤러에게 맡겨야 한다. 그러려면 메서드 파라미터에 맞게 요청정보를 추출해서 제공해주는 책임을 가진 어댑터 핸들러는 실패한 변환 작업에 대한 정보를 컨트롤러에게 제공해줄 필요가 있다. 컨트롤러는 이런 정보를 참고해서 적절한 에러 페이지를 출력하거나 친절한 에러메시지를 보여주면서 사용자가 폼을 다시 수정할 기회를 줘야 한다. 

 

 바로 이때문에 @ModelAttribute를 통해 폼의 정보를 전달받을때는 Errors, BindingResult타입의 파라미터를 같이 사용해야 한다. 같이 사용하지 않으면 스프링은 요청 파라미터 타입이나 값에 문제가 없도록 앱이 보장해준다고 생각한다. 단지 파라미터의 개수가 여러 개라 커맨드 오브젝트 형태로 전달 받을 뿐이라고 보는 것이다. 따라서 이때는 타입이 일치하지 않으면 BindingException예외가 던져진다. 이 예외는 @RequestParam처럼 친절하게 HTTP400 응답상태코드로 변환되지도 않으니 적절하게 예외처리를 해주지 않으면 사용자는 지저분한 에러 메시지를 만나게 될 것이다.

 

 폼에서 사용자 정보를 등록받는 add()메서드라면 반드시 다음과 같이 정의해야 한다.

 

 @RequestMapping(value="add",method=RequestMethod.POST)

 public String add(@ModelAttribute User user, BindingResult bindingResult) {...}

 

 BindingResult 대신 Errors타입으로 선언해도 좋다. 이 두가지 오브젝트에는 User오브젝트에 파라미터를 바인딩하다가 발생한 변환 오류와 모델 검증기를 통해 검증하는 중에 발견한 오류가 저장된다. 파라미터로 전달받은 bindingReulst를 확인해서 오류가 없다고 나오면, 모든 검증 통과한 것이므로 안심하고 user 오브젝트 내용을 DB에 등록하고 성공 페이지로 넘어가면 된다.  반대로 bindingResult 오브젝트에 오류가 담겨 있다면 다시 등록 폼을 출력해서 사용자가 잘못된 정보를 수정하도록 해야 한다. 스프링의 폼을 처리하는 커스텀 태그를 활용하면 BindingResult에 담긴 오류 정보를 적절한 메시지로 변환해서 화면에 출력해줄 수 있다.

 

BindingResul나 Errors를 사용할 때 주의할 점은 파라미터의 위치다. 이 두가지 타입의 파라미터는 반드시 @ModelAttribute파라미터 뒤에 나와야 한다. 자신의 바로 앞에 있는 @ModelAttribute파라미터의 검증 작업에서 발생한 오류만을 전달해주기 때문이다. 

 

-SessionStatus

 컨트롤러가 제공하는 기능 중에 모델 오브젝트를 세션에 저장했다가 다음 페이지에서 다시 활용하게 해주는 기능이 있다. 이 기능을 사용하다가 더 이상 세션 내에 모델 오브젝트를 저장할 필요가 없을 경우에는 코드에서 직접 작업완료 메서드를 호출해서 세션 안에 저장된 오브젝트를 제거해줘야 한다. 이때 필요한 것이 스프링의 SessionStatus오브젝트다. 파라미터로 선언해두면 현재 세션을 다룰 수 있는 SessionStatus오브젝트를 제공해준다. 세션 안에 불필요한 오브젝트를 방치하는 것은 일종의 메모리 누수.

 

-@RequestBody

이 어노테이션이 붙은 파라미터에는 HTTP요청의 본문 부분이 그대로 전달된다. 

일반적인 GET/POST요청 파라미터라면 @RequestBody를 사용할 일이 없을 것이다. 반면에 XML이나 JSON기반의 메시지를 사용하는 요청의 경우에는 이 방법이 매우 유용하다. 

 AnnotationMethodHandlerAdapter에는 HttpMessageConverter 타입의 메시지 변환기가 여러 개 등록되어 있다. @RequestBody가 붙은 파라미터가 있으면 HTTP 요청의 미디어 타입과 파라미터의 타입을 먼저 확인한다. 메시지 변환기 중에서 해당 미디어 타입과 파라미터 타입을 처리할 수 있는 것이 있다면 ,HTTP 요청의 본문부분을 통째로 변환해서 지정된 메서드 파라미터로 전달해준다. 

 

StringHttpMessageConverter 타입 변환기는 스트링 타입의 파라미터와 모든 종류의 미디어타입을 처리해준다.

따라서 다음과 같이 정의한다면 요청메시지의 본문 부분이 모두 스트링으로 변환돼서 전달될 것이다. 

 

  public void mssage(@RequestBody String body) {... } 

 

 XML본문을 가지고 들어오는 요청은 MarshallingHttpMessageConverter 등을 이용해서 XML이 변환된 오브젝트로 전달받을 수 있다. JSON타입의 메시지라면 MappingJacksonHttpMessageConverter를 사용할 수 있다. @RequestBody는 보통 @ResponseBody와 함께 사용.

 

-@Value

빈의 값 주입에서 사용하던 @Value 어노테이션도 컨트롤러 메서드 파라미터에 부여할 수 있다. 

 

-@Valid

JSR-303의 빈 검증기를 이용해서 모델 오브젝트를 검증하도록 지시하는 지시자다. 모델 오브젝트의 검증 방법을 지정하는데 사용하는 어노테이션이다.  

 

여기까지가 스프링의 AnnotationMethodHandlerAdapter 가 호출하는 컨트롤러 메서드의 사용 가능한 파라미터 타입과 어노테이션의 종류다. 스프링에서는 컨트롤러 메서드를 매우 자유롭게 작성할 수 있따. 다시 강조하지만 유연성이 보장된다고 방만하게 사용하면 오히려 코드를 관리하기 힘들어진다. 최종적으로 어떤파라미터 타입을 어떻게 사용할지는 개발자가 결정할 몫이다. 그만큼 책임도 뒤따른다는 사실을 잊지말자. 다양한 시나리오를 검토하고 그중에서 공통 패턴을 발견하도록 노력해서 최종적으로 적용할 수 있는 일관된 컨트롤러 메서드 작성 기준을 마련하고 모든 개발자가 따르도록 해야한다. 필요하다고 아무 타입이나 생각 없이 추가하고, 같은 정보를 여러가지 타입으로 중복해서 가져오고, 필요 없는 파라미터도 오류가 나지 않는다고 그냥 방치하고 순서도 제각각이라면 초기에 개발한 기능은 겨우 돌아가지만 코드 관리 필요할때는 악몽이다. !!!!

 

+ Recent posts