스프링 MVC form 태그 써 보셨어요?

귿이에요.

<form:checkboxes items=”${allAuthorities}” path=”authorities” delimiter=”<br/>” itemLabel=”name”  itemValue=”id” />

단 한 줄로..


저렇게 출력해줍니다. 괜찮죠? EL로 넘겨준 allAuthorities 이 녀석은 List 타입으로 도메인 객체 타입의 객체들을 담고 있죠. 흠… 화면에 보이는 값이 어째 좀 ‘사용자 비친화적(and 개발자 친화적)’입니다. name 말고 note를 출력하도록 할까요? 아~~주 간단합니다.

<form:checkboxes items=”${allAuthorities}” path=”authorities” delimiter=”<br/>” itemLabel=”note”  itemValue=”id” />

JSP에서 단어 하나만 바꿔주면 되죠.


짜잔… OSAF의 커스텀 태그는 스프링 form 태그를 기반으로 만들었으며, 정형적인 화면 개발 속도를 극대화 할 수 있도록 만들어 두었습니다.

다음에는 PropertyEditor 활용법을 살펴보겠습니다.

오랜만에 스프링 MVC 다시 정리

오늘 오후 네 시에 스터디가 있어서 오랜만에 13장을 다시 정리해봤습니다. 그 중 몇 개만 정리해둡니다.

MultiActionController 사용 방법은 두 가지
– 상속
– 위임

WebApplicationContext가 관리하는 빈
– 컨트롤러(controller)
– 핸들러 맵핑(handler mappings)
– 뷰 리졸버(view resolver)
– 로케일 리졸버(locale resolver)
– 테마 리졸버(theme resolver)
– 멀티파트 파일 리졸버(multipart file resolver)
– 예외 처리 리졸버(Handler exception resolver)

애노테이션 기반 컨트롤러 설정시 필요한 빈(자동 등록해줌)
– DefaultAnnotaionHandlerMapping
– AnnotationMethodHandlerAdapter

@RequestMapping 사용 방법
– 클래스 레벨
– 메소드 레벨(MAC와 비슷한 효과)
– 클래스 + 메소드 레벨 혼합(클레스 레벨에 Ant 패턴 사용해서 거르고, 메소드 레벨로 세부적으로.)

요청 처리 메소드 인자
– Servlet API(Session 사용시 Thread-safety 문제가 생기면, AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정.)
– WebRequest, NativeWebRequest
– Locale
– InputStream/Reader, OutputStream/Writer
– @RequestParam
– Map, Model, ModelMap
– Command/form objects
– Errors/BindingResult
– SessionStatus

요청 처리 메소드 반환 타입
– ModelAndView
– Model (뷰 이름은 CoC 사용)
– Map (위와 동일)
– View (모델은 커맨드 객체와 @ModelAttribute를 사용한 메소드가 반환하는 객체)
– String (위와 동일)
– void (응답을 response 객체를 사용해서 직접 처리하거나, CoC 사용)
– Other return type (해당 객체를 model attribute로 뷰에서 사용가능)

@RequestParam
– 요청 매개변수 바인딩

@ModelAttribute
– 메서드 매개변수 레벨: 모델 속성을 특정 메서드 매개변수로 맵핑할 때 사용.
– 메서드 레벨: 화면에서 사용할 implicite object를 제공할 때 사용.

@SessionAttributes
– @ModelAttribute의 이름 목록을 지니고 있다. 해당 모델 객체들을 세션에 저장하여 여러 요청에서 공통으로 사용.

@InitBinder
– 커스텀 프로퍼티 에디터 등록.

찾았다. WebBindingInitializer

이 녀석이 였구나.. 여러 컨트롤러에 PropertyEditor 적용할 때 필요한 녀석이..

레퍼런스를 저 키워드로 뒤지면 다음과 같은 코드가 나옵니다.

<bean class=”org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter”>
    <property name=”cacheSeconds” value=”0″ />
    <property name=”webBindingInitializer”>
        <bean class=”org.springframework.samples.petclinic.web.ClinicBindingInitializer” />
    </property>
</bean>

헤헷 이것만 가지고는 뭐.. 어쩌라는 건지 알 수 없죠. 저 클래스를 찾아봐야 합니다. 저 클래스를 찾는 방법은 여러 방법이 있지만, 제가 올려드리죠.

public class ClinicBindingInitializer implements WebBindingInitializer {

    @Autowired
    private Clinic clinic;

    public void initBinder(WebDataBinder binder, WebRequest request) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(“yyyy-MM-dd”);
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
        binder.registerCustomEditor(PetType.class, new PetTypeEditor(this.clinic));
    }

}

우왕~~ 귿이다~~ 이제 컨트롤러 마다 똑같은 프로퍼티 에디터 등록 안해도 되겠당.

Annotation-based controller configuration

0. 컴포넌트 스캐너 등록하기

우선, 컴포넌트 스캔 기능을 사용해서 @Controller 애노테이션이 붙어있는 클래스들을 bean으로 인식하도록 해야합니다. 따라서 context:component-scan 엘리먼트로 컨트롤러들이 위치한 패키지를 명시해 줍니다.

<context:component-scan base-package=”org.springframework.samples.petclinic.web” />

1. 컨트롤러 작성하기

완전 POJO로 컨트롤러를 작성할 수 있습니다. 획기적이네요. 일단 컨트롤러로 사용할 클래스는 이제 더이상 아무런 클래스도 상속받지 않아도 됩니다. 정말 그야말로 POJO입니다. 이 POJO에다가 @Controller 애노테이션을 붙여주면 컨트롤러가 됩니다.

2. Request Mapping 하기

원래는 Handler Mapper가 하던 일인데, 이제는 이것도 애노테이션이 해줍니다. @RequestMapping 애노테이션으로 해당 클래스 또는 메소드가 처리할 요청을 명시해주면 됩니다.

3. Form Controller로 사용하기

클래스 선언부에 @RequestMapping 애노테이션으로 폼을 요청할 Request를 설정해줍니다. 이 애노테이션이 붙어있는 클래스 안의 메소드 위에 같은 애노테이션을 사용하여 Request의 method에 따라 호출될 메소드를 설정할 수 있습니다.

@SessionAttributes 엘리먼트는 Session에 담을 attribute 를 나타냅니다. 주로 여러 폼에 걸쳐서 보여줄 데이터를 명시합니다.

@ModelAttribute는 두 가지 경우에 사용할 수 있는데, 메소드 위에 사용하면 폼에서 참조할 객체(Reference Data)를 나타낼 때 사용합니다. 아래의 예제에서 populatePetTypes() 메소드가 그 예에 해당합니다. 또 다른 경우는 메소드의 매개변수 앞에 이 애노테이션을 사용할 경우 인데, 이 때는 폼에 입력된 정보를 해당 애노테이션이 붙어있는 객체로 맵핑해 줍니다. processSubmit() 메소드의 인자를 보시면 됩니다.

@RequestParam은 Request의 특정 파라미터의 값을 가져옵니다. 물론 자동으로 해당 값을 이 애노테이션이 붙어있는 타입으로 변환해 줍니다. 기본타입만 가능하겠죠.

@Controller
@RequestMapping(“/editPet.do”)
@SessionAttributes(“pet”)
public class EditPetForm {

    private final Clinic clinic;

    @Autowired
    public EditPetForm(Clinic clinic) {
        this.clinic = clinic;
    }

    @ModelAttribute(“types”)
    public Collection<PetType> populatePetTypes() {
        return this.clinic.getPetTypes();
    }

    @RequestMapping(method = RequestMethod.GET)
    public String setupForm(@RequestParam(“petId”) int petId, ModelMap model) {
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute(“pet”, pet);
        return “petForm”;
    }

    @RequestMapping(method = RequestMethod.POST)
    public String processSubmit(@ModelAttribute(“pet”) Pet pet, BindingResult result,
            SessionStatus status) {
        new PetValidator().validate(pet, result);
        if (result.hasErrors()) {
            return “petForm”;
        }
        else {
            this.clinic.storePet(pet);
            status.setComplete();
            return “redirect:owner.do?ownerId=” + pet.getOwner().getId();
        }
    }

}

4. MultiActionController로 사용하기

@RequestMapping 애노테이션으로 처리할 URL을 메소드 위에 표기해 줍니다. 물론 class위에는 적을 필요가없겠죠. 메소드 단위니까요.

뷰는 CoC에 따라 요청 URL을 보고 판단합니다.
뷰를 명시할 수 있는 다른 방법이 있는지 궁금해지네요. 이 부분은 살펴봐야겠습니다.
ModelAndView 객체를 리턴타입으로 사용할 수 있다면 간단해 지겠지만 말이죠.

@Controller
public class ClinicController {

    private final Clinic clinic;

    @Autowired
    public ClinicController(Clinic clinic) {
        this.clinic = clinic;
    }

    @RequestMapping(“/welcome.do”)
    public void welcomeHandler() {
    }

    @RequestMapping(“/vets.do”)
    public ModelMap vetsHandler() {
        return new ModelMap(this.clinic.getVets());
    }

    @RequestMapping(“/owner.do”)
    public ModelMap ownerHandler(@RequestParam(“ownerId”) int ownerId) {
        return new ModelMap(this.clinic.loadOwner(ownerId));
    }

}

5. 커스텀 프로퍼티 에디터 등록하기.

다음과 같이 @InitBinder를 사용하여 바인딩 하는 녀석을 등록할 수 있습니다.

@Controller
public class MyFormController {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat(“yyyy-MM-dd”);
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // …
}

해당 바인더는 커맨드나 폼 객체 그리고 그에 따른 에러 객체를 제외한 @RequestMapping이 지원하는 모든 아규먼트에 적용됩니다. 흠.. 폼에서 사용자가 입력한 값은 이걸로 바인딩 하지 않는다는 건가?? -_-?? 이상하네.. 이 부분도 공부나 추가 정보가 필요함.

커스텀 프로퍼티 에디터 등록을 xml에서 하기

WebBindingInitializer 인터페이스 구현체에 프로퍼티 에디터를 등록해서 다음과 같이 설정해주는 듯 합니다. 이 것 역시 어떻게 구현하는지 공부가 필요함.

<bean class=”org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter”>
    <property name=”cacheSeconds” value=”0″ />
    <property name=”webBindingInitializer”>
        <bean class=”org.springframework.samples.petclinic.web.ClinicBindingInitializer” />
    </property>
</bean>

13.11. Convention over configuration 3

마지막으로 살펴볼 CoC는 ModelAndView 객체의 View에 해당하는 논리적인 View의 이름에 관련된 것입니다.

요청 -> view 이름 :: DefaultRequestToViewNameTranslator

앞에서 살펴봤던 예제 코드를 다시 살펴보겠습니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView(“issue/list”)
            .addObject(issueService.getAll());
    }

여기서 논리적인 뷰의 이름으로 issue/list 를 넘겨주고 있습니다. 이 이름은 다음과 같은 ViewReslover에 의해 WEB-INF/jsp/issue/list.jsp 를 랜더링 하게 됩니다.

    <bean id=”viewResolver”
        class=”org.springframework.web.servlet.view.InternalResourceViewResolver”>
        <property name=”viewClass”
            value=”org.springframework.web.servlet.view.JstlView” />
        <property name=”prefix” value=”/WEB-INF/jsp/” />
        <property name=”suffix” value=”.jsp” />
    </bean>

DefaultRequestToViewNameTranslator 를 사용하면 요청을 바탕으로 논리적인 View 이름을 생성하여 ViewResolver에 넘겨주게 됩니다. 특히나 이 클래스는 Spring의 DispatcherServlet  이 기본으로 가지고 있기 때문에 명시적으로 선언하지 않아도 됩니다.

샘플
 http://localhost:8080/gamecast/display.html -> display
 http://localhost:8080/gamecast/displayShoppingCart.html -> displayShoppingCart
 http://localhost:8080/gamecast/admin/index.html -> admin/index

위의 ModelAndView 코드를 다음과 같이 수정할 수 있습니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView()
            .addObject(issueService.getAll());
    }

이렇게 보니까 ModelAndView 라는 이름이 어색하지 않게 보이네요. 🙂