토비의 스프링 Vol.2 - 4장 스프링 @MVC

2020-11-04

스프링 @MVC

@RequestMapping 핸들러 매핑

@MVC의 가장 큰 특징은 핸들러 매핑과 핸들러 어댑터의 대상이 오브젝트가 아니라 메소드라는 점이다.

클래스/메소드 결합 매핑정보

DefaultAnnotationHandlerMapping의 핵심은 매핑정보로 @RequestMapping 애노테이션을 활용한다는 점이다. 그런데 @RequestMapping은 타입 레벨뿐 아니라 메소드 레벨도 붙일 수 있다. 스프링은 이 두 가지 위치에 붙은 @RequestMapping의 정보를 결합해서 최종 매핑벙보를 생성한다.

@RequestMapping 애노테이션 엘리먼트
  • String[] value(): URL 패턴
    • URL 패턴 및 와일드카드
      • @RequestMapping("/hello"), @RequestMapping("/main*"), @RequestMapping("/view.*"), @RequestMapping("/admin/**/user")
    • path variable
      • @RequestMapping("/user/{userid}")
    • 디폴트 접미어 패턴
      • @RequestMapping({"/hello", "/hello/", "/hello.*"})
  • RequestMethod[] method(): HTTP 요청 메소드
    • RequestMethod는 HTTP 메소드를 정의한 이늄(enum)이다. GET, HEAD, POST, PUT, DELETE, OPTIONS, TRACE 7개의 HTTP 메소드가 정의되어 있다. @RequestMapping에 HTTP 요청 메소드를 추가해주면 같은 URL이라고 하더라도 요청 메소드에 따라 다른 메소드에 매핑해줄 수 있다.
      • @RequestMapping(value="/user/add", method=RequestMethod.GET), @RequestMapping(value="/user/add", method=RequestMethod.POST)
  • String[] params(): 요청 파라미터
    • 요청의 파라미터와 그 값을 비교해서 매핑해준다. 같은 URL을 사용하더라도 HTTP 요청 파라미터에 따라 별도의 작업을 해주고 싶을 때 사용한다.
      • @RequestMapping(value="/user/edit", params="type=admin"), @RequestMapping(value="/user/edit", params="type=member")
    • 특정 파라미터가 존재하지 않아야 한다는 조건을 지정할 수도 있다.
      • @RequestMapping(value="/user/edit", params="!type")
    • params도 배열로 선언되어 있으므로 하나 이상을 지정할 수 있다.
  • String[] headers(): HTTP 헤더
    • HTTP 헤더정보. 자주 사용되지는 않는다.
      • @RequestMapping(value="/view", headers="content-type=text/*")
        타입 레벨 매핑과 메소드 레벨 매핑의 결합

        타입(클래스와 인터페이스) 레벨에 붙는 @RequestMapping은 타입 내의 모든 매핑용 메소드의 공통 조건을 지정할 때 사용한다. 그리고 메소드 레벨에서 조건을 세분화해주면 된다. 메소드 레벨의 매핑은 클래스 레벨의 매핑을 상속받는다고 보면 된다.

        메소드 레벨 단독 매핑

        메소드 레벨의 매핑조건에 공통점이 없는 경우라면 타입 레벨에서는 조건을 주지 않고 메소드 레벨에서 독립적으로 매핑정보를 지정할 수도 있다. 타입 레벨에는 조건이 없는 @RequestMapping을 붙여두면 된다. 컨트롤러 클래스에 @Controller 애노테이션을 붙여서 빈 자동스캔 방식으로 등록되게 했다면, 이때는 클래스 레벨의 @RequestMapping을 생략할 수도 있다.

        타입 레벨 단독 매핑

        @RequestMapping을 타입 레벨에 단독으로 사용해서 다른 타입 컨트롤러에 대한 매핑을 위해 사용할 수도 있다.

클래스 레벨의 URL 패턴이 /*로 끝나는 경우에는 메소드 레벨의 URL 패턴으로 메소드 이름이 사용되게 할 수 있다.

@RequestMapping("/user/*")
public class UserController {
  @RequestMapping public String add(...) { }
  @RequestMapping public String edit(...) { }
}

타입 상속과 매핑

@RequestMapping이 적용된 클래스를 상속해서 컨트롤러로 사용하는 경우 서브클래스에서 @RequestMapping을 재정의하면 슈퍼클래스의 정보는 무시된다. 서브클래스에서 재정의하지 않는 한 상속을 통해서도 유지된다.

매핑정보 상속의 종류
  • 상위 타입과 메소드의 @RequestMapping 상속
    • 슈퍼클래스에만 @RequestMapping을 적용하고 이를 그대로 상속한 서브클래스에는 아무런 @RequestMapping을 사용하지 않았을 경우 슈퍼클래스에 정의된 모든 매핑정보를 그대로 서브클래스가 물려받는다.
  • 상위 타입의 @RequestMapping과 하위 타입 메소드의 @RequestMapping 결합
    • 슈퍼클래스에는 타입에만 @RequestMapping이 선언되어 있고, 서브클래스에는 타입 레벨에는 아무 매핑정보가 없고 메소드에만 @RequestMapping이 있는 경우 이때도 슈퍼클래스의 타입 레벨 @RequestMapping이 상속되어 서브클래스의 타입 레벨에 @RequestMapping이 정의되어 있는 것처럼 각 메소드의 매핑정보가 결합돼서 최종적인 메소드 레벨의 매핑조건을 만들어낸다.
  • 상위 타입 메소드의 @RequestMapping과 하위 타입의 @RequestMapping 결합
    • 슈퍼클래스에는 메소드에만 @RequestMapping이 있고, 서브클래스에는 반대로 클래스 레벨에 @RequestMapping이 부여된 경우 이때도 마찬가지로 @RequestMapping 정보가 그대로 상속된 후에 결합된다.
  • 하위 타입과 메소드의 @RequestMapping 재정의
    • 상속 또는 구현을 통해 만들어진 하위 타입에 @RequestMapping을 부여하면 상위 타입에서 지정한 @RequestMapping 매핑정보를 대체해서 적용된다. @RequestMapping은 상속 또는 구현을 통해 하위 타입에서 재정의할 수 있다.
  • 서브클래스 메소드의 URL 패턴 없는 @RequestMapping 재정의
    • 클래스 상속에서 오버라이드한 하위 메소드에 한해서는 URL 조건이 없는 @RequestMapping을 붙였을 경우에 상위 메소드의 @RequestMapping의 URL 조건이 그대로 상속된다.
      제네릭스와 매핑정보 상속을 이용한 컨트롤러 작성

      제네릭스를 활용해서 상위 타입에는 타입 파라미터와 메소드 레벨의 공통 매핑정보를 지정해놓고, 이를 상속받는 개별 컨트롤러에는 구체적인 타입과 클래스 레벨의 기준 매핑정보를 지정해주는 기법을 사용할 수 있다.


@Controller

@Controller에서는 컨트롤러 역할을 담당하는 메소드의 파라미터 개수와 타입, 리턴 타입 등을 자유롭게 결정할 수 있다. 여기에 @RequestMapping을 비롯한 다양한 애노테이션을 메소드나 파라미터에 적용해서 컨트롤러 로직에 대한 추가적인 정보를 제공할 수도 있다.

스프링은 메소드의 파라미터와 리턴 값이 어떻게 선언됐는지 살펴보고, 이를 이용해 적절한 파라미터 값을 준비해서 호출해준다. 리턴 값도 타입에 따라 적절한 방식으로 사용한다. 따라서 @Controller에서 허용되는 파라미터와 리턴 값의 종류를 알고 있다면 필요한 정보만을 주고받을 수 있는 최적화된 메소드를 설계할 수 있다.

메소드 파라미터의 종류

  • HttpServletRequest, HttpServletResponse
  • HttpSession
  • WebRequest, NativeWebRequest
  • Locale
  • InputStream, Reader
  • OutputStream, Writer
  • @PathVariable
  • @RequestParam
  • @CookieValue
  • @RequestHeader
  • Map, Model, ModelMap
  • @ModelAttribute
  • Errors, BindingResult
  • SessionStatus
  • @RequestBody
  • @Value
  • @Valid

    리턴 타입의 종류

    자동 추가 모델 모브젝트와 자동생성 뷰 이름
  • @ModelAttribute 모델 오브젝트 또는 커맨드 오브젝트
  • Map, Model, ModelMap 파라미터
  • @ModelAttribute 메소드
  • BindingResult
    메소드 리턴 타입 종류
  • ModelAndView
  • String
  • void
  • 모델 오브젝트
  • Map/Model/ModelMap
  • View
  • @ResponseBody

    @SessionAttributes와 SessionStatus

    도메인 오브젝트를 사용해 수정 폼을 처리할 때 발생하는 문제들을 해결하는 방법

  • 히든 필드
    • 브라우저 화면에는 보이지 않지만 HTML 소스를 열어보면 그 내용과 필드 이름까지 쉽게 알아낼 수 있어 보안에 심각한 문제를 일으킨다.
  • DB 재조회(DB에서 조회 후 수정할 내용을 덮어 씌운 후 업데이트)
    • DB의 부담이 증가한다.
  • 계층 사이의 강한 결합(수정할 필드들만을 위한 메소드와 계층을 준비)
    • 코드를 재사용하기가 힘들다.

      위의 세 방법은 단점이 많다. 따라서 스프링의 해결책을 사용하자.

      @SessionAttributes
  • @SessionAttributes 애노테이션을 클래스 레벨에 부여하고 폼의 정보를 담을 모델 이름을 넣어준다.
    • 컨트롤러 메소드가 생성하는 모델 정보 중에서 @SessionAttributes에 지정한 이름과 동일한 것이 있다면 이를 세션에 저장해준다.
    • @ModelAttribute가 지정된 파라미터가 있을 때 이 파라미터에 전달해줄 오브젝트를 세션에서 가져온다.
  • @SessionAttributes의 설정은 클래스의 모든 메소드에 적용된다.
    SessionStatus
  • @SessionAttributes를 사용할 때는 더 이상 필요 없는 세션 애트리뷰트를 코드로 제거해줘야 한다.
  • 작업을 마무리하는 코드에서는 작업을 성공적으로 마쳤다면 SessionStatus 오브젝트의 setComplete() 메소드를 호출해서 세션에 저장해뒀던 오브젝트를 제거해줘야 한다.
    등록 폼을 위한 @SessionAttributes 사용
  • 복잡한 도메인 오브젝트의 경우 미리 초기화 해두거나, 사용자의 입력을 돕기 위해 디폴트 값을 보여주는 경우, 초기화된 오브젝트를 세션에 저장해두고 사용해야 한다. 등록 폼의 입력 값에서도 잘못된 것이 있으면 다시 폼을 띄워서 재입력을 요구해야 한다.
  • 등록과 수정 화면에 상관없이 폼을 출력할 때 폼의 내용을 담은 모델 오브젝트를 필요로 하기에, 스프링의 폼 태그를 사용하기 위해 @SessionAttributes를 이용해 모델 오브젝트를 저장했다가 다시 사용하는 게 좋다.

모델 바인딩과 검증

컨트롤러 메소드에 @ModelAttribute가 지정된 파라미터를 @Controller 메소드에 추가하면 세 가지 작업이 자동으로 진행된다.

  1. 파라미터 타입의 오브젝트를 만든다. @ModelAttribute User user라는 파라미터 선언이 있다면 User 타입의 오브젝트를 생성한다. 이를 위해 디폴트 생성자가 반드시 필요하다. @SessionAttributes에 의해 세션에 저장된 모델 오브젝트가 있다면, 새로운 오브젝트를 생성하는 대신 세션에 저장되어 있는 오브젝트를 가져온다.
  2. 준비된 모델 오브젝트의 프로퍼티에 웹 파라미터를 바인딩해준다. 스프링에 준비되어 있는 기본 프로퍼티 에디터를 이용해서 HTTP 파라미터 값을 모델의 프로퍼티 타입에 맞게 전환한다. 전환이 불가능한 경우라면, BindingResult 오브젝트 안에 바인딩 오류를 저장해서 컨트롤러로 넘겨주거나 예외를 발생시킨다.
  3. 모델의 값을 검증한다. 바인딩 단계에서 타입에 대한 검증은 끝났지만, 그 외의 검증할 내용이 있다면 적절한 검증기를 등록해서 모델의 내용을 검증할 수 있다. 스프링에서는 컨트롤러 로직과 검증 로직을 분리할 수 있다.

스프링에서 바인딩이라고 말할 때는 오브젝트의 프로퍼티에 값을 넣는 것을 말한다. 반대로 프로퍼티로부터 값을 읽어오는 경우도 있다. 스프링은 바인딩 과정에서 변환 작업을 위해 기본적으로 두 가지 종류의 API를 제공한다.

1. PropertyEditor

디폴트 프로퍼티 에디터

바인딩 과정에서는 변환할 파라미터 또는 모델 프로퍼티의 타입에 맞는 프로퍼티 에디터가 자동으로 선정돼서 사용된다. 기본적으로 스프링이 제공하는 프로퍼티 에디터의 종류와 지원 타입이 어떤 것인지 기억해두고 활용하면 된다.

커스텀 프로퍼티 에디터

스프링이 지원하지 않는 타입을 파라미터로 사용한다면, 직접 프로퍼티 에디터를 만들어서 적용할 수 있다. PropertyEditor 인터페이스를 직접 구현하기보다는 기본 구현이 되어 있는 PropertyEditorSupport 클래스를 상속해서 필요한 메소드만 오버라이드해주는 편이 낫다.

@InitBinder

@InitBinder가 붙은 initBinder() 메소드는 메소드 파라미터를 바인딩하기 전에 자동으로 호출된다. 그래서 스프링의 디폴트 프로퍼티 에디터만 갖고 있는 WebDataBinder에 커스텀 프로퍼티 에디터를 추가할 수 있는 기회를 제공해준다.

  • WebDataBinder에 커스텀 프로퍼티 에디터를 등록하는 방법
    • 특정 타입에 무조건 적용되는 프로퍼티 에디터 등록
    • 특정 이름의 프로퍼티에만 적용되는 프로퍼티 에디터 등록

      커스텀 프로퍼티 에디터가 디폴트 프로퍼티 에디터보다 우선순위가 높다.

      WebBindingInitializer

      모든 컨트롤러에 적용해도 될 만큼 많은 곳에서 필요한 프로퍼티 에디터라면 WebBindingInitializer 인터페이스를 구현해서 한 번에 모든 컨트롤러에 적용하는 편이 좋다.

      프로토타입 빈 프로퍼티 에디터

      프로퍼티 바인딩을 할 때마다 매번 새로운 프로퍼티 에디터 오브젝트를 새로 만드는 대신 프로퍼티 에디터를 싱글톤 빈으로 등록해두고 이를 공유해서 쓸 수는 없다. 프로퍼티 에디터는 멀티스레드 환경에서 싱글톤으로 만들어 여러 오브젝트가 공유해서 사용하면 안 된다. 자칫하면 다른 스레드에서 변환하는 값으로 덮어쓰일 수 있다. 그래서 바인딩 할 때마다 매번 새로운 프로퍼티 에디터 오브젝트를 만들어 사용한다.

프로퍼티 에디터가 다른 스프링 빈을 참조해야 한다면 프로퍼티 에디터를 빈으로 등록해야 한다. 하지만 프로퍼티 에디터는 싱글톤 빈으로 등록해서 공유할 수 없으니 프로토타입 스코프의 빈으로 만들어져야 한다.

폼의 파라미터가 모델의 프로퍼티에 바인딩될 때 단순 타입이 아닌 경우 바인딩하는 방법

  • 별도의 codeid 필드로 바인딩
    • 매번 컨트롤러나 서비스 계층 코드에서 id에 해당하는 임시 프로퍼티 갑을 이용해서 도메인 오브젝트 타입의 프로퍼티를 설정해주는 작업을 해야 한다.
  • 모조 오브젝트 프로퍼티 에디터
    • id만 가진 오브젝트는 모조 오브젝트라고 하고, 이런 오브젝트를 만드는 프로퍼티 에디터를 모조 프로퍼티 에디터라고 부른다.
    • 위험성은 있지만 매우 유용한 방법이다.
  • 프로토타입 도메인 오브젝트 프로퍼티 에디터
    • DB에서 Code 오브젝트를 새로 읽어와야하기 때문에 미미하지만 성능에 부담을 준다. 하지만 캐시를 사용하면 된다.

      2. Converter와 Formatter

      PropertyEditor는 매번 바인딩을 할 때마다 새로운 오브젝트를 만들어야 한다. 그래서 스프링 3.0에는 PropertyEditor를 대신할 수 있는 새로운 타입 변환 API가 도입됐다. 바로 Converter 인터페이스다. PropertyEditor와 달리 Converter는 변환 과정에서 메소드가 한 번만 호출된다. 그래서 멀티스레드 환경에서 안전하게 공유해서 쓸 수 있다.

      Converter

      문자열과 오브젝트 사이의 양방향 변환 기능을 제공하는 PropertyEditor와 다르게 Converter 메소드는 소스 타입에서 타깃 타입으로의 단방향 변환만 지원한다. 물론 소스와 타깃을 바꿔서 컨버터를 하나 더 만들면 양방향 변환이 가능해진다. Converter는 소스와 타깃의 타입을 임의로 지정할 수 있다. 따라서 범용적으로 사용할 수 있는 컨버터를 정의할 수 있다.

      ConversionService

      ConversionService는 여러 종류의 컨버터를 이용해서 하나 이상의 타입 변환 서비스를 제공해주는 오브젝트를 만들 때 사용하는 인터페이스다. 보통 ConversionService를 구현한 GenericConversionService 클래스를 빈으로 등록해서 사용하면 된다. GenericConversionService는 스프링의 다양한 타입 변환 기능을 가진 오브젝트를 등록할 수 있는 ConverterRegistry 인터페이스도 구현하고 있다. 따라서 GenericConverter를 이용하면 단순히 타입의 종류뿐 아니라 부가적인 정보를 활용한 변환 로직을 작성할 수 있다. ConverterFactory는 제네릭스를 활용해서 특정 타입에 대한 컨버터 오브젝트를 만들어주는 팩토리를 구현할 때 사용한다.

  • @MVC 컨트롤러의 메소드 파라미터를 위해 사용하는 WebDataBinder에 GenericConversionService를 설정하는 방법
    1. @InitBinder를 통한 수동 등록
    2. ConfigurableWebBindingInitializer를 이용한 일괄 등록
      Formatter와 FormattingConversionService

      포맷터는 스프트링 타입의 폼 필드 정보와 컨트롤러 메소드의 파라미터 사이에 양방향으로 적용할 수 있도록 두 개의 변환 메소드를 갖고 있다. Formatter는 그 자체로 Converter와 같이 스프링이 기본적으로 지원하는 범용적인 타입 변환 API가 아니다. 따라서 Formatter 구현 오브젝트를 GenericConverter 타입으로 포장해서 등록해주는 기능을 가진 FormattingConversionService를 통해서만 적용될 수 있다.

  • FormattingConversionServiceFactoryBean을 사용했을 때 자동으로 등록되는 포맷터
    • @NumberFormat
    • @DateTimeFormat
      바인딩 기술의 적용 우선순위와 활용 전략
  • 사용자 정의 타입의 바인딩을 위한 일괄 적용: Converter
  • 필드와 메소드 파라미터, 애노테이션 등의 메타정보를 활용하는 조건부 변환 기능: ConditionalGenericConverter
  • 애노테이션 정보를 활용한 HTTP 요청과 모델 필드 바인딩: AnnotationFormatterFactory와 Formatter
  • 특정 필드에만 적용되는 변환 기능: PropertyEditor

    필드를 지정해서 제한적으로 변환 기능을 적용하는 경우를 제외하면, 프로퍼티 에디터보다는 안전하고 장점이 많은 컨버터나 포맷터 등으로 대체하는 것이 좋다.

    WebDataBinder 설정 항목

    WebDataBinder는 HTTP 요청정보를 컨트롤러 메소드의 파라미터나 모델에 바인딩할 때 사용되는 바인딩 오브젝트다.

  • WebDataBinder에 자주 활용되는 설정 항목
    • allowedFields, disallowedFields
    • requiredFields
    • fieldMarkerPrefix
    • fieldDefaultPrefix

      Validator와 BindingResult, Errors

      Validator

      스프링의 Validator는 스프링에서 범용적으로 사용할 수 있는 오브젝트 검증기를 정의할 수 있는 API다. @Controller로 HTTP 요청을 @ModelAttribute 모델에 바인딩할 때 주로 사용된다.

  • 스프링에서 Validator를 적용할 수 있는 네 가지 방법
    1. 컨트롤러 메소드 내의 코드
    2. @Valid를 이용한 자동검증
    3. 서비스 계층 오브젝트에서의 검증
    4. 서비스 계층을 활용하는 Validator
      JSR-303 빈 검증 기능

      최근에 표준 스펙으로 인증받은 JSR-303 빈 검증(bean validation) 방식도 스프링에서 사용할 수 있다. 스프링에서는 LocalValidatorFactoryBean을 이용해 JSR-303의 검증 기능을 사용할 수 있다. LocalValidatorFactoryBean은 JSR-303의 검증 기능을 스프링의 Validator처럼 사용할 수 있게 해주는 일종의 어댑터다.

      스프링의 Validator는 PropertyEditor, ConversionService와 마찬가지로 WebDataBinder를 이용해 초기화할 수 있으므로 WebBindingInitializer를 이용해 모든 컨트롤러에 일괄적으로 적용할 수도 있다. 특히 애노테이션을 활용하는 JSR-303 빈 검증 기능은 필드 단위로 적용이 가능하기 때문에 하나의 Validator를 다양한 모델에 적용할 수 있다.

      BindingResult와 MessageCodeResolver

      BindingResult에는 모델의 바인딩 작업 중에 나타난 타입 변환 오류정보와 검증 작업에서 발견된 검증 오류정보가 모두 저장된다. 이 오류정보는 보통 컨트롤러에 의해 폼을 다시 띄울 때 활용된다. 등록된 오류정보에는 필드와 에러 코드가 기본적으로 들어간다. 이 에러코드는 MessageCodeResolver를 통해 좀 더 상세한 메시지 키 값으로 확장된다.

  • 스프링이 디폴트로 사용하는 MessageCodeResolver인 DefaultMessageCodeResolver는 이를 확장해서 네 개의 메시지 키 후보를 생성한다.
    1. 에러코드.모델이름.필드이름: field.required.user.name
    2. 에러코드.필드이름: field.required.name
    3. 에러코드.타입이름: field.required.User
    4. 에러코드: field.required

      이 네 가지 키 후보는 위에서부터 우선적으로 메시지를 찾는 데 사용된다.

  • 검증 작업 중에 발견한 오류가 특정 필드에 속한 것이 아니라면 reject()를 사용해 모델 레벨의 글로벌 에러로 등록할 수 있다. 이때는 필드 이름이 없으므로 에러 코드와 모델 이름을 이용해서 DefaultMessageCodeResolver가 두 개의 메시지 키 후보를 만들어준다.
    1. 에러코드.모델이름: invalid.data.user
    2. 에러코드: invalid.data
      MessageSource

      스프링의 MessageSource 구현은 두 가지 종류가 있다. 하나는 코드로 메시지를 등록할 수 있는 StaticMessageSource이며, 다른 하나는 messages.properties 리소스 번들 방식을 사용하는 ResourceBundleMessageSource다. 보통 ResourceBundleMessageSource가 기본적으로 사용된다.

  • MessageSource는 기본적으로 네 가지 정보를 활용해 최종적인 메시지를 만들어 낸다.
    1. 코드
    2. 메시지 파라미터 배열
    3. 디폴트 메시지
    4. 지역정보

      모델의 일생

      모델은 MVC 아키텍처에서 정보를 담당하는 컴포넌트다. 브라우저와 같은 웹 클라이언트에서 전달된 정보를 담아 비즈니스 로직에 사용되도록 전달되는 자바오브젝트이며, 비즈니스 로직에 의해 생성된 정보를 클라이언트로 보내는 동안에 유지하고 있는 오브젝트이기도 하다.

      HTTP 요청으로부터 컨트롤러 메소드까지
  • @ModelAttribute 메소드 파라미터
    • 컨트롤러 메소드의 모델 파라미터와 @ModelAttribute로부터 모델 이름, 모델 타입 정보를 가져온다.
  • @SessionAttribute 세션 저장 대상 모델 이름
    • 모델 이름과 동일한 것이 있다면 HTTP 세션에 저장해둔 것이 있는지 확인한다. 만약 있다면 모델 오브젝트를 새로 만드는 대신 세션에 저장된 것을 가져와 사용한다.
  • WebDataBinder에 등록된 프로퍼티 에디터, 컨버전 서비스
    • WebBindingInitializer나 @InitBinder 메소드를 통해서 등록된 변환 기능 오브젝트를 이용해 HTTP 요청 파라미터를 모델의 프로퍼티에 맞도록 변환해서 넣어준다. 커스텀 프로퍼티 에디터, 컨버전 서비스, 디폴트 프로퍼티 에디터 순으로 적용된다. 타입 변환에 실패하면 BindingResult 오브젝트에 필드 에러가 등록된다. 필드마커나 필드 디폴트가 발견되면 그에 따라 지정된 필드의 값이 설정되기도 한다. 빈 폼을 나타내기 위한 요청에서처럼 새로 만들어진 모델 오브젝트에 바인딩할 HTTP 파라미터가 없다면 이 과정은 생략된다.
  • WebDataBinder에 등록된 검증기
    • 모델 파라미터에 @Valid가 지정되어 있다면 WebBindingInitializer나 @InitBinder 메소드를 통해 등록된 검증기로 모델을 검증한다. 검증 로직을 갖고 있는 Validator 또는 JSR-303의 애노테이션 방식의 검증기를 이용한다. 검증 결과는 BindingResult 오브젝트에 등록된다. WebDataBinder에 등록된 검증기가 없거나 @Valid 애노테이션이 없다면 이 과정은 생략된다.
  • ModelAndView의 모델 맵
    • 모델 오브젝트는 컨트롤러 메소드가 실행되기 전에 임시 모델 맵에 저장된다. 이렇게 저장된 모델 오브젝트는 컨트롤러 메소드의 실행을 마친 뒤에 추가로 등록된 모델 오브젝트와 함께 ModelAndView 모델 맵에 담겨 DispatcherServlet으로 전달된다.
  • 컨트롤러 메소드와 BindingResult 파라미터
    • HTTP 요청을 담은 모델 오브젝트가 @ModelAttribute 파라미터로 전달되면서 컨트롤러 메소드가 실행된다. 메소드의 모델 파라미터 다음에 BindingResult 타입 파라미터가 있다면 바인딩과 검증 작업의 결과가 담긴 BindingResult 오브젝트가 제공된다. BindingResult는 ModelAndView의 모델 맵에도 자동으로 추가된다.
      컨트롤러 메소드로부터 뷰까지
  • ModelAndView의 모델 맵
    • 컨트롤러 메소드의 실행을 마치고 최종적으로 DispatcherServlet이 전달받는 결과다. 메소드 안에서 직접 또는 간접적으로 생성된 @ModelAttribute 오브젝트가 ModelAndView의 모델 맵에 담겨 있다. 폼의 정보를 담은 @ModelAttribute 오브젝트뿐 아니라 바인딩과 검증 결과를 담은 BindingResult 타입의 오브젝트도 ModelAndView의 모델 맵에 자동으로 추가된다.
  • WebDataBinder에 기본적으로 등록된 MessageCodeResolver
    • WebDataBinder에 등록되어 있는 MessageCodeResolver는 바인딩 작업 또는 검증 작업에서 등록된 에러 코드를 확장해서 메시지 코드 후보 목록을 만들어준다. 메시지 코드 후보를 만드는 로직은 WebDataBinder에 디폴트로 등록된 것을 사용하면 된다.
  • 빈으로 등록된 MessageSource와 LocaleResolver
    • LocaleResolver에 의해 결정된 지역정보와 MessageCodeResolver가 생성한 메시지 코드 후보 목록을 이용해 MessageSource가 뷰에 출력할 최종 에러 메시지를 결정한다. MessageSource는 기본적으로 messages.properties 파일을 이용하는 ResourceBundleMessageSource를 등록해서 사용한다.
  • @SessionAttribute 세션 저장 대상 모델 이름
    • 모델 맵에 담긴 모델 중에서 @SessionAttribute로 지정한 이름과 일치하는 것이 있다면 HTTP 세션에 저장된다. 세션에 저장된 모델 오브젝트는 다음 요청에서 활용된다.
  • 뷰의 EL과 스프링 태그 또는 매크로
    • 뷰에 의해 최종 콘텐트가 생성될 때 모델 맵으로 전달된 모델 오브젝트는 뷰의 표현식언어(EL)를 통해 참조돼서 콘텐트에 포함된다. MessageSource에 의해 결정된 에러 메시지는 JSP라면 스프링 전용 태그나, 템플릿 엔진이라면 스프링 매크로를 통해 출력할 수 있다.

      스프링 MVC를 구성하는 세 가지 컴포넌트(모델, 뷰, 컨트롤러) 중에서 컨트롤러와 뷰는 주로 DispatcherServlet의 기본 전략을 바꾸거나 재설정함으로써 결정할 수 있다. 반면에 모델은 컨트롤러와 뷰보다 훨씬 다이내믹한 생명주기를 갖고 있다.


JSP 뷰와 form 태그

사용자의 입력 값에 대한 피드백을 제공해줘야 하는 폼을 다루는 뷰는 조금 복잡하다. 모델정보뿐 아니라 잘못 입력된 값이나 바인딩과 검증 작업의 오류 메시지도 함께 출력해줘야 하기 때문이다.

EL과 spring 태그 라이브러리를 이용한 모델 출력

JSP EL

<div> 이름: ${name} </div>

스프링 SpEL

스프링의 SpEL은 JSP EL보다 유연하고 강력한 표현식을 지원한다. 모델 값을 단순히 출력하는 것 이상의 조작이 필요하다면 SpEL을 사용할 수 있다.

지역화 메시지 출력

언어별로 messages.properties 파일을 만들어두고 지역정보를 따라 메시지를 가져오는 것은 폼의 에러 메시지에서만 사용할 수 있는 방법은 아니다. 화면에 출력할 일반 메시지에서도 지역정보에 따라 메시지를 가져와서 출력하려면 spring 태그 라이브러리의 message 태그를 사용하면 된다.

spring 태그 라이브러리를 이용한 폼 작성

단일 폼 모델

HTML의 <form>태그로 만들어진 폼 뷰를 사용하는 데는 등록과 수정이 있다. 등록에는 빈 폼이 뜨고 사용자 입력을 받아서 이를 저장하는 폼이고, 수정에는 처음부터 폼에는 내용이 채워서 출력되고 이를 수정해서 저장할 수 있도록 하는 폼이다.

<spring:bind>와 BindingStatus

스프링은 JSP EL을 사용했을 때의 한계를 극복하고 스프링의 모델과 바인딩 결과정보를 최대한 활용할 수 있도록 <spring:bind>라는 태그를 제공한다. <spring:bind> 태그는 path를 지정할 수 있다. <spring:bind path="user.name">...</spring:bind> <spring:bind><spring:bind> 태그 내부에서 사용할 수 있도록 BindStatus 타입의 오브젝트를 status라는 이름의 변수로 등록해준다. BindStatus는 path로 지정한 user.name 프로퍼티에 관련된 많은 정보를 제공해준다.

  • BindStatus 오브젝트의 프로퍼티

프로퍼티 | 타입 | 내용 :——–:|:————:|:———: expression | String | 폼 <input> 태그의 name으로도 사용될 수 있는 프로퍼티 이름이다. user 모델 오브젝트의 name 프로퍼티라면 name이 지정된다. value | String | 바인딩 오류가 없는 경우에는 모델의 프로퍼티 값을 갖고 있고, 필드의 바인딩 오류가 있는 경우에는 이전에 입력했던 값을 얻을 수 있다. errorMessages | String[] | 필드에 할당된 모든 에러 메시지를 담은 스트링 배열을 돌려준다. 필드 하나에 여러 개의 바인딩 오류가 등록된 경우에는 하나 이상 에러 메시지를 가질 수 있다. errorMessage | String | 첫 번째 에러 메시지만 돌려준다. errors | Errors | 바인딩 오류 정보를 얻을 수 있는 Errors 타입 오브젝트를 돌려준다. Errors 인터페이스를 통해 다시 errorCount 같은 정보를 얻을 수 있다.

form 태그 라이브러리

스프링의 form 태그 라이브러리를 이용하면 <spring:bind>보다 훨씬 간결한 코드로 동일한 기능을 하는 코드를 만들 수 있다.

<form:form>

HTML <form> 태그를 만들어준다. 또한 <form:form> 태그 내부의 입력 필드에 적용할 모델의 이름을 지정할 수 있다.

  • 자주 사용되는 애트리뷰트
    • commandName, modelAttribute
    • method
    • action
      <form:input>

      HTML의 <input type="text"> 태그를 생성한다. 필수 애트리뷰트는 path뿐이다. 그 외에 <input> 태그에 적용 가능한 size, maxlength, readonly 같은 표준 HTML 애트리뷰트와 onclick, onkeydown 같은 이벤트 애트리뷰트 등을 지원한다. <form:form>의 commandName에 해당하는 모델 오브젝트의 path 프로퍼티를 바인딩해준다.

  • path
    • id를 따로 지정하지 않았다면 <input> 태그의 id, name에 할당된다. 또 value 애트리뷰트에 지정할 모델의 프로퍼티 이름으로 사용된다. 이전 폼 서브밋에 오류가 있어서 폼이 다시 뜬 경우에는 모델에 바인딩 값 대신 직전에 잘못 입력한 값이 value에 들어간다. 따라서 모델의 프로퍼티에서는 지원하지 않는 타입의 정보도 출력할 수 있다.
  • cssClass, cssErrorClass
    • cssClass는 <input> 태그의 class 애트리뷰트 값을 지정할 때 사용한다. path에 해당하는 프로퍼티에 바인딩 오류가 있다면, cssClass 대신 cssErrorClass에 지정한 값이 CSS class로 지정된다. 이를 이용해서 바인딩 오류 시 스타일을 다르게 만들 수 있다.

      value 애트리뷰트에 출력되는 모델 오브젝트의 프로퍼티 값은 PropertyEditor나 ConversionService가 등록되어 있다면 이를 통해 문자열로 변환된다.

      <form:label>

      폼의 레이블 출력에 사용되는 <label> 태그를 만들어준다. 필수 애트리뷰트는 path다. <form:input>과 마찬가지로 cssClass, cssErrorClass 애트리뷰트를 이용해서 바인딩 오류가 없을 때와 있을 때의 CSS 클래스를 각각 지정할 수 있다.

      <form:errors>

      바인딩 에러 메시지를 출력할 때 사용한다. 기본적으로 <span> 태그를 사용해 에러 메시지를 감싼다. 필수 애트리뷰트는 없지만 path 설정에 따라서 에러 메시지의 종류를 선택할 수 있다.

  • 주요 애트리뷰트
    • path
    • delimiter
    • cssClass
  • 에러 메시지를 출력하는 방식
    • 입력 필드와 함께 출력
    • 상단 또는 하단에 일괄 출력
      <form:hidden>

      <input type="hidden"> 태그를 작성한다. @SessionAttribute를 사용하면 폼을 띄울때 참고한 모델 오브젝트를 저장해뒀다가 폼을 바인딩할 때 다시 사용하기 때문에, 폼에 출력하지 않는 프로퍼티 값은 그대로 유지할 수 있다. 다만 경우에 따라 히든 필드를 HTML에 직접 넣어야 할 경우가 있다면 <form:hidden>을 사용한다. path만 지정해주면 된다. <form:hidden path="loginCount" />

      <form:password>, <form:textarea>

      각각 HTML <input type="text"><textarea> 태그를 생성한다. path만 디폴트 애트리뷰트이며, 각각 대응되는 HTML 태그의 표준 애트리뷰트를 사용할 수 있다. cssClass와 cssErrorClass 두 가지를 이용해서 바인딩 에러가 있을 때와 없을 때의 CSS 클래스를 각각 지정할 수 있다.

      <form:checkbox>, <form:checkboxes>

      HTML의 <input type="checkbox"> 태그를 만들어준다. <form:checkbox>는 자동으로 필드마커가 붙은 히든 필드를 등록해주기 때문에 편리하다. 필수 애트리뷰트는 path뿐이다. 하지만 체크박스에 대한 설명을 담은 label을 함께 사용하는 경우가 일반적이다.

<form:checkboxes>는 한 번에 여러 개의 체크박스를 만들 때 사용한다. <form:checkboxes>는 맵이나 컬렉션을 통해서 제공된 레이블-값 목록을 이용해 여러 개의 체크박스를 출력한다. 맵의 키는 체크박스의 value에 할당되고 맵의 값은 체크박스의 레이블에 출력된다. <form:checkboxes>를 이용해 모델 오브젝트를 items에 지정해준다. items에는 모델의 이름이 아니라 맵 오브젝트 자체가 필요하므로 EL을 이용해 오브젝트를 넣어줘야 한다. <form:checkboxes path="interests" items="${interests}" /> 맵 대신 오브젝트 리스트를 사용할 수도 있다. 이때는 오브젝트의 어떤 프로퍼티가 레이블이고 어떤 프로퍼티가 값에 해당하는지를 지정해줘야 한다.

<form:radiobutton>, <form:radiobuttons>

HTML <input type="radio"> 태그를 생성해준다. 기본적인 사용 방법은 <form:checkbox>와 비슷하다. 다만 라디오버튼은 체크박스와 달리 단일 선택이므로, 각각을 구분할 수 있는 값을 명확히 지정해줘야 한다.

컬렉션이나 맵을 이용한 라디오버튼 출력 방식은 목록을 서버에서 관리할 수 있기 때문에 편리하다. 코드에서 생성한 단순 목록을 사용할 수도 있고, DB에서 읽어온 목록을 사용할 수도 있다. 또는 이늄을 이용할 수도 있다. 물론 이때는 value의 값과 이늄 오브젝트를 변환해주는 프로퍼티 에디터나 컨버터를 사용해야 한다.

<form:select>, <form:option>, <form:options>

HTML의 <selsct><option>을 생성해준다. <form:select><form:option><form:radio>와 사용 방법이 비슷하다. 라디오버튼 대신 드롭다운 박스나 선택창이 나타나고, path를 <form:select>에 따로 지정한다는 정도가 다를 뿐이다.

목록을 수동으로 지정하려면 <form:option>을 이용하면 된다. <option>목록을 뷰에서 지정하지 않고 맵이나 리스트를 이용해 자동생성하려면 <form:options>를 사용하면 된다.

<form:select path="type">
   <form:options items items="${types}" itemLable="name" itemValue="id" />
</form:select>

<form:options><form:option>을 함께 사용할 수도 있다.

path로 지정한 모델의 type 프로퍼티는 value 값을 전달받는다.

커스텀 UI 태그 만들기

스프링의 spring 태그 라이브러리이든 form 태그 라이브러리이든 그 자체로 사용하기보다는 다시 애플리케이션 UI 스타일에 따라 커스텀 태그를 만들어 사용하면 편리하다. form 태그는 상대적으로 간결하지만 일정한 사용 패턴이 있다면 다시 태그화하는 것이 편리하다.


메시지 컨버터와 AJAX

메시지 컨버터는 XML이나 JSON을 이용한 AJAX 기능이나 웹 서비스를 개발할 때 사용할 수 있다. HTTP 요청 프로퍼티를 모델 오브젝트의 프로퍼티에 개별적으로 바인딩하고 모델 오브젝트를 다시 뷰를 이용해 클라이언트로 보낼 콘텐트로 만드는 대신 HTTP 요청 메시지 본문과 HTTP 응답 메시지 본문을 통째로 메시지로 다루는 방식이다. 메시지 컨버터는 파라미터의 @RequestBody와 메소드에 부여한 @ResponseBody를 이용해서 쓸 수 있다.

메시지 컨버터의 종류

사용할 메시지 컨버터는 AnnotationMethodHandlerAdapter를 통해 등록한다. 일반적으로 하나 이상의 메시지 컨버터를 등록해두고 요청 타입이나 오브젝트 타입에 따라 선택되게 한다.

  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • FormHttpMessageConverter
  • SourceHttpMessageConverter
  • Jaxb2RootElementHttpMessageConverter
  • MarshallingHttpMessageConverter
  • MappingJacksonHttpMessageConverter
    JSON을 이용한 AJAX 컨트롤러: GET + JSON

    AJAX는 비동기 자바스크립트와 XML(Asynchronous JavaScript and XML)의 약자로 자바스크립트를 이용해 서버와 비동기 방식의 통신을 해서 웹 페이지를 갱신하지 않은 채로 여러 가지 작업을 수행하는 프로그래밍 모델을 말한다. 이름과는 달리 AJAX에서 서버와 주고받는 메시지는 XML로 제한된 것은 아니다. 최근에는 서버 측 라이브러리 지원에 힘입어서 자바스크립트의 오브젝트를 표현하는 표기법으로 사용되던 JSON(JavaScript Object Notation)이 AJAX의 인기 메시지 포맷으로 자리 잡았다.

    JSON 기반의 AJAX를 지원하려면 컨트롤러는 결과를 JSON 포맷으로 만들어서 돌려줘야 한다. JSON 지원 뷰를 사용하거나 @ResponseBody를 이용하면 된다.

    JSON을 이용한 AJAX 컨트롤러: POST(JSON) + JSON

    URL의 파라미터나 패스를 사용해 정보를 전달하기에는 적절하지 않은 내용이라면 POST 메소드를 사용하고 메시지 본문은 JSON 포맷으로 작성하는 것이 편리하다.


MVC 네임스페이스

스프링은 최신 @MVC 기능을 손쉽게 등록할 수 있게 해주는 mvc 스키마의 전용 태그를 제공한다.

<mvc:annotation-driven>

이 태그는 애노테이션 방식의 컨트롤러를 사용할 때 필요한 DispatcherServlet 전략 빈을 자동으로 등록해준다. 또, 최신 @MVC 지원 기능을 제공하는 빈도 함께 등록하고 전략 빈의 프로퍼티로 설정해준다. 라이브러리의 존재 여부를 파악해서 자동으로 관련 빈을 추가해주는 기능도 제공된다.

  • <mvc:annotation-driven>에 의해 자동으로 등록되는 빈
    • DefaultAnnotationHandlerMapping
    • AnnotationMethodHandlerAdapter
    • ConfigurableWebBindingInitializer
    • 메시지 컨버터
    • <spring:eval>을 위한 컨버전 서비스 노출용 인터셉터
  • 직접 만든 커스텀 검증기, 컨버전 서비스 등을 적용할 때는 다음과 같은 애트리뷰트를 사용해 등록하면 된다.
    • validatior
    • conversion-service
      <mvc:interceptors>

      Handler Interceptor의 적용 방법

      1. 핸들러 매핑 빈의 interceptors 프로퍼티를 이용해 등록한다.
      2. <mvc:interceptors>를 이용한다.
        <mvc:view-controller>

        <mvc:view-controller> 태그에 매핑할 URL 패턴과 뷰 이름을 넣어주면 해당 URL을 매핑하고 뷰 이름을 리턴하는 컨트롤러가 자동으로 등록된다.


@MVC 확장 포인트

애노테이션 방식의 컨트롤러를 만들 때 적용되는 기능을 확장할 수 있도록 준비된 확장 포인트 중에서 유용한 것들

AnnotationMethodHandlerAdapter

  • SessionAttributeStore
  • WebArgumentResolver
  • ModelAndViewResolver

URL과 리소스 관리

<mvc:default-servlet-handler/>를 이용한 URL 관리

<mvc:default-servlet-handler/>

스프링 3.0.4부터는 URL을 내부에서 바꿔치기하는 UrlRewriteFilter 대신 스프링이 제공하는 디폴트 서블릿 핸들러를 이용해 디폴트 서블릿과 URL 매핑 문제를 해결할 수 있다. <mvc:default-servlet-handler/>는 디폴트 서블릿으로 포워딩하는 기능을 가진 핸들러로 모든 요청을 매핑해주는 매핑 전략 빈을 추가하고 가장 낮은 우선순위를 부여한다.

<url:resource/>를 이용한 리소스 관리

스프링 3.0.4에서 추가된 <url:resource/>를 활용하면 정적 파일로 구성된 웹 리소스도 손쉽게 모듈화해서 사용할 수 있다.

<mvc:resources>에 의해 등록되는 핸들러도 304 응답 코드를 처리하는 기능을 갖고 있다. 따라서 정적 파일을 최적화된 방식으로 다룰 수 있게 해준다.


스프링 3.1의 @MVC

새로운 RequestMapping 전략

  • @RequestMapping을 지원하는 DispatcherServlet 전략의 변화

DispatcherServlet 전략 | 스프링 3.0 | 스프링 3.1 :———————:|:———–:|:———-: HandlerMapping | DefaultAnnotation, HandlerMapping | RequestMapping, HandlerMapping HandlerAdapter | AnnotationMethod, HandlerAdapter | RequestMapping, HandlerAdapter HandlerExceptionResolver | AnnotationMethodHandler, ExceptionResolver | ExceptionHandler, ExceptionResolver

@RequestMapping 메소드와 핸들러 매핑 전략의 불일치

스프링 3.1에서는 @RequestMapping을 처리하는 전략 클래스를 새롭게 설계해서 매핑과 어댑터 전략의 구조와 구현을 깔끔하게 만들었고 @RequestMapping 관련 전략의 확장성도 좋아지게 만들었다.

HandlerMethod

HandlerMethod는 @RequestMapping이 붙은 메소드의 정보를 추상화한 오브젝트 타입이다. 컨트롤러 오브젝트 대신 추상화된 메소드 정보를 담은 오브젝트를 핸들러 매핑의 결과로 돌려주고, 핸들러 어댑터는 HandlerMethod 오브젝트의 정보를 이용해 메소드를 실행한다. HandlerMethod를 도입한 덕분에 역할에 맞지 않는 매핑 작업이 핸들러 어댑터에서 일어난다거나 핸들러 매핑의 결과가 뭉뚱그려져서 컨트롤러 오브젝트로 리턴되는 설계와 구현의 불일치 문제는 사라졌다.

  • HandlerMethod에 담긴 핵심 정보
    • 빈 오브젝트
    • 메소드 메타정보
    • 메소드 파라미터 메타정보
    • 메소드 애노테이션 메타정보
    • 메소드 리턴 값 메타정보

      웹 요청을 HandlerMethod 오브젝트에 매핑하는 RequestMappingHandlerMapping은 DispatcherServlet이 시작될 때 모든 컨트롤러 빈의 메소드를 살펴서 매핑 후보가 될 메소드를 추출한 뒤 이를 HandlerMethod 형태로 저장해두고, 실제 요청이 들어오면 저장해둔 목록에서 요청 조건에 맞는 HandlerMethod 오브젝트를 찾아 돌려준다.

      @RequestMapping 전략 선택

      구버전 호환성을 중요시하는 스프링답게 스프링 3.1 @MVC는 스프링 3.0의 디폴트 전략을 사용해 작성된 코드가 스프링 3.1에서도 동일한 전략으로 실행될 수 있도록 기본 전략을 변경하지 않았다. 따라서 매핑과 어댑터 전략의 기본 구성과 클래스는 스프링 3.0과 동일하다. 새로운 스프링 3.1의 새로운 전략을 본격적으로 활용하려면 자바 코드를 이용한 @MVC 설정 방식이 권장된다.

      @RequestMapping 핸들러 매핑: RequestMappingHandlerMapping

      @RequestMapping 매핑정보를 다루는 전략 클래스는 RequestMappingHandlerMapping이다. RequestMappingHandlerMapping의 역할은 클라이언트의 요청을 @RequestMapping이 붙은 메소드로 매핑해주는 것이다. 매핑 결과는 @RequestMapping 메소드의 정보를 추상화한 HandlerMethod 타입 오브젝트다.

      요청 조건

      매핑의 기준이 되는 정보를 스프링에서는 요청 조건이라고 한다. HTTP 서블릿 요청에 대한 매핑 조건이라는 의미다.

  • @RequestMapping에 포함된 요청 조건

엘리먼트 | 요청 조건 | 지원 버전 :——-:|:———:|:———: value, 디폴트 | URL 패턴 | 3.0, 3.1 method | HTTP 요청 메소드 | 3.0, 3.1 params | 파라미터 | 3.0, 3.1 headers | HTTP 헤더 | 3.0, 3.1 consumes | Content-type 헤더 | 3.1 produces | Accept 헤더 | 3.1

RequestMappingHandlerMapping은 @RequestMapping의 6가지 조건에 확장 포인트를 통해 등록되는 커스텀 조건을 더해서 총 7가지 요청 조건을 사용한다. 이 매핑 조건을 기준으로 핸들러 메소드를 선정한다. @RequestMapping이나 커스텀 조건은 타입(클래스, 인터페이스)과 메소드에 각각 지정할 수 있으므로 하나의 핸들러 메소드를 선정할 때 참고하는 요청 조건은 모두 14가지다. 두 가지 이상의 조건이 지정된 경우에는 조건을 조합하거나 결합해서 사용한다. 한 요청 조건에 여러 개의 값이 사용되기도 한다. 위의 엘리먼트는 모두 배열타입이므로 {}를 사용하면 여러 개의 값을 넣을 수 있다. RequestMappingHandlerMapping은 이 14가지 요청 조건을 조합해서 핸들러 메소드의 최종 요청 조건을 만들어 사용한다.

요청 조건의 결합 방식

커스텀 조건을 제외한 나머지 @RequestMapping의 6가지 요청 조건의 결합 방식

  • URL 패턴: PatternsRequestCondition
  • HTTP 요청 방법: RequestMethodsRequestCondition
  • 파라미터: ParamsRequestCondition
  • 헤더: HeadersRequestCondition
  • Content-Type 헤더: ConsumesRequestCondition
  • Accept 헤더: ProducesRequestCondition

    @RequestMapping 핸들러 어댑터

    HandlerMethod 타입의 핸들러를 담당하는 핸들러 어댑터는 RequestMappingHandlerAdapter다. RequestMappingHandlerAdapter에는 스프링 3.0에서 지원하던 메소드 파라미터와 리턴 타입에 몇 가지 타입과 애노테이션이 추가됐다.

    파라미터 타입
  • @Validated/@Valid
  • @Valid와 @RequestBody
  • UriComponentsBuilder
  • RedirectAttributes와 리다이렉트 뷰
  • RedirectAttributes와 플래시 애트리뷰트
    확장 포인트

    WebArgumentResolver는 스프링 3.0의 @MVC에서 지원하는 핸들러 메소드의 파라미터 타입 확장 포인트다. 스프링 3.1에서는 메소드 호출과 모델 바인딩, 파라미터 변환 등을 담당하는 핸들러 어댑터가 바뀌었기 때문에 확장 포인트도 달라진다.

  • 파라미터: HandlerMethodArgumentResolver
  • 리턴 값: HandlerMethodReturnValueHandler 이 두 가지 확장 포인트는 RequestMappingHandlerAdapter가 기본적으로 제공하는 파라미터와 리턴 값 처리 클래스에서도 사용된다.

    @EnableWebMvc와 WebMvcConfigurationSupport를 이용한 @MVC 설정

    스프링 3.1의 최신 @MVC 전략을 사용하려면 XML 설정에 <mvc:annotation-driven>을 넣거나 인프라 빈 설정용 애노테이션을 이용해 @MVC 빈이 등록되게 해야한다. 본격적으로 @MVC의 기능을 활용하려면 자바코드를 이용한 @MVC 빈 등록 및 설정 방식을 사용하는 것이 좋다.

    @EnableWebMvc와 WebMvcConfigurer

    @Configuration 클래스에 @EnableWebMvc를 붙여주면 <mvc:annotation-config/>을 XML에 넣었을 때와 동일하게 스프링 3.1의 최신 전략 빈이 등록된다.

스프링은 @Enable 전용 애노테이션으로 등록되는 인프라 빈에 대한 추가 설정을 위해 설정용 빈을 활용하는 방법을 제공한다. 인프라 빈의 설정을 담당하는 기능을 가진 클래스를 만들어 빈으로 등록하면 @Enable 전용 애노테이션을 처리하는 단계에서 설정용 빈을 활용해 인프라 빈의 설정을 마무리 한다.

이렇게 @Enable 전용 애노테이션의 설정을 위해 사용되는 빈을 설정자 또는 컨피규어러(configurer)라고 한다. 이런 빈들이 구현해야 할 인터페이스의 이름은 대부분 Configurer로 끝난다. 빈 설정자라고 부른다.

@EnableWebMvc의 빈 설정자가 구현해야 할 인터페이스는 WebMvcComfigurer다. WebMvcConfigurer는 인터페이스니 모든 메소드를 구현해야 한다. 설정이 필요 없는 경우에는 메소드를 비워두면 된다. 사용하지 않는 메소드가 많아지는 것이 불편하다면 필요한 메소드만 오버라이딩해서 사용할 수 있게 만들어진 WebMccConfigurerAdapter 클래스를 상속할 수도 있다.

각 메소드를 이용한 설정 기능과 이용 방법
  • addFormatters()
    • 포매터는 문자열로 된 폼 값과 모델 오브젝트의 프로퍼티사이의 변환 작업을 지원해주는 MVC용 컨버터다.
    • addFormatters() 메소드가 제공하는 FormatterRegistry를 이용해 등록하는 방법
    • 포매터를 @Beab 메소드를 사용해 빈으로 정의한 뒤 addFormatters에서 포매터 빈을 가져와 레지스트리에 저장하는 방법
    • 포매터 빈이 XML이나 다른 @Configuration 클래스에 정의되어 있다면 @Autowired를 이용해 포매터 빈을 주입받아 넣어주면 된다.
    • addFormatterForFieldType() 메소드를 이용하여 포매터가 특정 타입의 필드에만 적용되게 만들거나 addFormatterForFieldAnnotation()을 이용해 특정 애노테이션이 붙은 필드에만 적용되는 포매터로 등록하는 방법
  • configureMessageConverters()
    • 직접 메시지 컨버터를 구성하고 싶을 때 configureMessageConverters()의 List<HttpMessageConverter<?>> 타입 파라미터를 이용해 사용할 메시지 컨버터를 추가하면 된다.
    • 디폴트 메시지 컨버터를 유지한 채로 메시지 컨버터를 추가하고 싶다면 고급 설정 방법인 WebMvcConfigurationSupport 확장을 이용해야 한다.
    • 다른 빈에 의존하는 빈 형태로 만들어야 하는 게 아니라면 직접 오브젝트를 생성해서 등록하면 된다.
    • 스프링이 제공하는 디폴트 메시지 컨버터 중에서 일부만 선택하고, 직접 구현한 컨버터를 추가할 수도 있다.
  • getValidatior()
    • 디폴트로 등록되는 JSR-303 검증기용 LocalValidatorFactoryBean을 대신할 범용 검증기를 등록할 경우에 사용한다.
  • addArgumentResolvers()
    • RequestMappingHandlerAdapter의 파라미터 처리용 확장 포인트인 HandlerMethodArgumentResolver를 추가할 수 있는 메소드다.
  • addReturnValueHandlers()
    • RequestMappingHandlerAdapter의 리턴 값 처리용 확장 포인트인 HandlerMethodReturnValueHandler를 추가할 때 사용한다.
  • configureHandlerExceptionResolvers()
    • 디폴트로 등록되는 핸들러 예외 전략을 새롭게 구성하려고 할 때 사용한다. 메시지 컨버터와 마찬가지로 이 메소드에서 핸들러 예외 리졸버를 추가하면 디폴트 예외 리졸버는 무시된다.
    • 디폴트 핸들러 예외 전략은 그대로 두고 새로운 예외 리졸버를 추가하려면 WebMccConfigurationSupport를 이용해야 한다.
  • addInterceptors()
    • 인터셉터를 등록해주는 <mvc:interceptors>의 자바 코드 버전이다. 파라미터로 제공되는 InterceptorRegistry를 이용해 인터셉터 오브젝트 또는 빈을 추가한다.
  • addViewControllers()
    • URL 패턴을 그대로 뷰 이름으로 돌려주는 간단한 컨트롤러를 등록하는 메소드다.
  • addResourceHandlers()
    • 스프링 3.0.4에서 추가된 <mvc:resources>의 기능을 담당하는 메소드다.
  • configureDefaultServletHandling()
    • <mvc:default-servlet-handler/>를 등록한 것과 같은 설정 결과를 가져오려면 configureDefaultServletHandling() 메소드를 작성하기만 하면 된다.

      WebMvcConfigurer의 메소드를 이용해 클래스를 만들고 빈으로 등록해주면 자동으로 @MVC 전략에 적용된다.

      @MVC 설정자 빈 등록 방법

      @EnableWebMvc를 위한 설정정보는 WebMvcConfigurer 타입의 설정자 빈을 만들어 등록하면 된다.

  • @EnableWebMvc가 붙은 @Configuration 클래스에 @Bean 메소드를 넣어서 설정자 빈을 등록하는 방법
  • 서블릿 컨텍스트용 @Configuration 클래스가 직접 WebMvcConfigurerAdapter를 상속하게 만드는 방법
  • WebMvcConfigurer 구현 빈을 등록하는 대신 WebMvcConfigurationSupport 클래스를 상속하는 빈을 등록하는 방법
    @MVC 전략용 설정 빈 등록

    WebMvcConfigurer를 통해서는 등록되지 않는 @MVC 관련 빈도 있다. 이 빈들은 WebConfig에 @Bean 메소드를 넣어서 등록해주면 된다.

    뷰, 뷰 리졸버, 지역정보 리졸버, 멀티파트 리졸버, 메시지 소스처럼 서블릿 컨텍스트에 빈 형태로 등록해줘야 하는 것들은 모두 @Bean 메소드를 이용해 등록하면 된다.


정리

스프링이 제공하는 기능이 대부분 그렇듯이 @MVC도 극한의 유연성과 확장성을 얻을 수 있게 설계됐따. 그 덕분에 웹 프레젠테이션 계층의 다양한 기술 조합과 활용 방법을 충족할 수 있으면서, 동시에 재사용 가능하도록 기능별로 분리된 기능과 확장 포인트를 잘 활용해서 최적화된 간결한 코드로 웹 프레젠테이션 계층을 개발할 수 있다.

스프링 웹 기술의 가장 큰 장점은 유연성을 활용해서 개별 애플리케이션의 특성에 맞춰 확장하고 최적화할 수 있다는 점이다. 특히 애노테이션과 관례를 적극 활용하는 @Controller는 더욱 간단하고 편리한 방법으로 전용 웹 프레임워크를 개발하고 구성하는 데 많은 도움이 될 것이다.

출처 : https://github.com/Masssidev/toby-vol2