본문 바로가기

JAVA/Spring

검증과 데이터바인딩

반응형

스프링 웹 MVC를 활용한 검증과 데이터바인딩

@ModelAttribute 애너테이션

  • 스프링의 Model 객체에 속성을 저장하고 싶을 때는 메서드에 @ModelAttribute를 설정
  • Model 객체에서 속성을 읽고 싶을 때는 메서드 인수에 @ModelAttribute 설정
  • 모델 속성의 스코프가 request 스코프로, 요청 처리가 끝나거나 요청을 리다이렉션 하면 모델 속성이 사라진다.

@SessionAttribute

  • @RequestMapping 을 설정한 메서드를 호출하기전에 컨트롤러의 @ModelAttribute를 설정한 모든 메서드가 호출된다.
  • 이런 동작 방식은 @ModelAttribute 메서드들이 데이터를 데이터베이스나 외부 웹 서비스에서 얻어 모델 속성을 채워 넣는 경우 적당하지 않다.
  • 데이터베이스나 외부 웹 서비스에서 데이터를 가져와 데이터 속성을 채워 넣는 경우에는 컨트롤러에 @SessionAttribute를 설정해서 모델 속성을 요청과 요청 사이에 유지되는 HttpSession에 저장하라고 지정할 수 있다.
  • @SessionAttribute을 사용하면 @ModelAttribute을 설정한 모델 속성을 HttpSession 세션에 찾을 수 없을 때만 @ModelAttribute를 설정한 메서드를 호출한다.
  • 그리고 메서드 인수에 @ModelAttribute를 설정한 경우에도 HttpSession에 해당 모델 속성을 찾을 수 없을 때만 새로운 모델 속성 인스턴스를 생성한다.

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import org.springframework.web.servlet.ModelAndView;

import sample.spring.chapter13.domain.FixedDepositDetails;
import sample.spring.chapter13.service.FixedDepositService;

@Controller
@RequestMapping(path = "/fixedDeposit")
@SessionAttributes(names = { "newFixedDepositDetails",
        "editableFixedDepositDetails" }, types = {FixedDepositDetails.class})
public class FixedDepositController {
    private static Logger logger = LogManager
            .getLogger(FixedDepositController.class);

    @Autowired
    private FixedDepositService fixedDepositService;

    @RequestMapping(path = "/list", method = RequestMethod.GET)
    @ModelAttribute(name = "fdList")
    public List<FixedDepositDetails> listFixedDeposits() {
        logger.info("listFixedDeposits() method: Getting list of fixed deposits");
        return fixedDepositService.getFixedDeposits();
    }

    @ModelAttribute(name = "newFixedDepositDetails")
    public FixedDepositDetails getNewFixedDepositDetails() {
        FixedDepositDetails fixedDepositDetails = new FixedDepositDetails();
        fixedDepositDetails.setEmail("You must enter a valid email");
        logger.info("getNewFixedDepositDetails() method: Returning a new instance of FixedDepositDetails");
        return fixedDepositDetails;
    }

    @RequestMapping(params = "fdAction=createFDForm", method = RequestMethod.POST)
    public String showOpenFixedDepositForm() {
        logger.info("showOpenFixedDepositForm() method: Showing form for opening a new fixed deposit");
        return "createFixedDepositForm";
    }

    @RequestMapping(params = "fdAction=create", method = RequestMethod.POST)
    public String openFixedDeposit(
            @ModelAttribute(name = "newFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {

        new FixedDepositDetailsValidator().validate(fixedDepositDetails,
                bindingResult);

        if (bindingResult.hasErrors()) {
            logger.info("openFixedDeposit() method: Validation errors - re-displaying form for opening a new fixed deposit");
            return "createFixedDepositForm";
        } else {
            fixedDepositService.saveFixedDeposit(fixedDepositDetails);
            sessionStatus.setComplete();
            logger.info("openFixedDeposit() method: Fixed deposit details successfully saved. Redirecting to show the list of fixed deposits.");
            return "redirect:/fixedDeposit/list";
        }
    }

    @RequestMapping(params = "fdAction=edit", method = RequestMethod.POST)
    public String editFixedDeposit(
            @ModelAttribute("editableFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {

        new FixedDepositDetailsValidator().validate(fixedDepositDetails,
                bindingResult);

        if (bindingResult.hasErrors()) {
            logger.info("editFixedDeposit() method: Validation errors - re-displaying form for editing a fixed deposit");
            return "editFixedDepositForm";
        } else {
            fixedDepositService.editFixedDeposit(fixedDepositDetails);
            sessionStatus.setComplete();
            logger.info("editFixedDeposit() method: Fixed deposit details successfully changed. Redirecting to show the list of fixed deposits.");
            return "redirect:/fixedDeposit/list";
        }
    }

    @RequestMapping(params = "fdAction=close", method = RequestMethod.GET)
    public String closeFixedDeposit(
            @RequestParam(name = "fixedDepositId") int fdId) {
        fixedDepositService.closeFixedDeposit(fdId);
        logger.info("closeFixedDeposit() method: Fixed deposit successfully closed. Redirecting to show the list of fixed deposits.");
        return "redirect:/fixedDeposit/list";
    }

    @RequestMapping(params = "fdAction=view", method = RequestMethod.GET)
    public ModelAndView viewFixedDepositDetails(
            @RequestParam(name = "fixedDepositId") int fixedDepositId) {
        FixedDepositDetails fixedDepositDetails = fixedDepositService
                .getFixedDeposit(fixedDepositId);
        Map<String, Object> modelMap = new HashMap<String, Object>();
        modelMap.put("editableFixedDepositDetails", fixedDepositDetails);
        logger.info("viewFixedDepositDetails() method: Fixed deposit details loaded from data store. Showing form for editing the loaded fixed deposit.");
        return new ModelAndView("editFixedDepositForm", modelMap);
    }
}
  • @SessionAttribute의 name 속성은 임시로 HttpSession에 저장할 모델 속성의 이름을 지정한다.
  • newFixedDepositDetails, editableFixedDepositDetails 이라는 이름의 모델 속성을 요청과 요청 사이에 HttpSession에 저장한다.
  • @ModelAttribute을 설정한 getNewFixedDepositDetails 메서드가 newFixedDepositDetails 모델 속성을 반환하고 @RequestMapping을 설정한 viewFixedDepositDetails 메서드가 editableFixedDepositDetails 모델 속성을 반환한다.
  • 컨트롤러는 @ModelAttribute나 @RequestMapping을 설정한 메서드를 통해 모델 속성을 제공하거나 직접 Model 객체에 모델 속성을 추가함으로써 모델 속성을 제공할 수 있다.
  • 이 모든 방식으로 제공되는 모델 속성이 @SessionAttribute 애너테이션을 통해 HttpSession에 저장될 모델 속성의 후보가 될 수 있다.
  • @SessionAttribute 애너테이션을 사용할 때는 더 이상 필요없어진 모델 속성을 HttpSession에서 반드시 제거해야 한다.
  • HttpSession에 저장된 모든 모델 속성을 제거할 때는 스프링 SessionStatus 객체의 setComplete 메서드를 호출한다.
  • @SessionAttribute의 names 속성을 사용해 HttpSession에 임시 저장할 모델 속성 이름을 설정했다. 모델 속성 중 일부 타입만 HttpSession에 저장하고 싶다면 @SessionAttribute의 types 속성을 사용한다.

스프링의 데이터 바인딩 지원

  • 요청에 들어 있는 요청 파라미터에 폼 기반 객체로 사용중인 모델 속성이 자동으로 설정된다.
  • 이런식으로 폼 지원 객체를 요청의 요청 파라미터로 자동 설정하는 처리 과정을 데이터 바인딩이라고 한다.
import java.util.Date;

public class FixedDepositDetails {
    private long id;

    private long depositAmount;

    private String email;

    private Date maturityDate;

    public FixedDepositDetails() {
    }

    public FixedDepositDetails(long id, long depositAmount, Date maturityDate,
            String email) {
        this.id = id;
        this.depositAmount = depositAmount;
        this.maturityDate = maturityDate;
        this.email = email;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public long getDepositAmount() {
        return depositAmount;
    }

    public void setDepositAmount(long depositAmount) {
        this.depositAmount = depositAmount;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Date getMaturityDate() {
        return maturityDate;
    }

    public void setMaturityDate(Date maturityDate) {
        this.maturityDate = maturityDate;
    }

    public String toString() {
        return "id :" + id + ", deposit amount : " + depositAmount
                + ", email : " + email;
    }
}
  • depositAmount, maturityDate 필드의 타입이 순서대로 long과 java.util.Date 이다.

WebDataBinder- 웹 요청 파라미터의 데이터 바인더

  • WebDataBinder는 폼 지원 객체에서 적절한 자바빈 스타일 세터 메서드를 찾기 위해 요청 파라미터 이름을 사용한다.
  • WebDataBinder는 찾은 자바빈 스타일 세터 메서드를 호출하면 요청 파라미터 값을 세터 메서드의 인수로 전달된다.
  • 세터 메서드가 String이 아닌 타입을 받는 메서드로 정의된 경우 WebDataBinder가 적절한 PropertyEditor를 호출해서 타입 변환을 수행한다.
  • 스프링은 WebDataBinder가 String 타입의 요청 파라미터값을 폼 지원 객체에 정의된 타입으로 변환할 때 사용할 수 있는 여러 내장 PropertyEditors 구현을 제공한다.
  • 기본으로 제공하는 프로퍼티 에디터로 CustomNumberEditor, FiledEditor, CustomDateEditor가 있다.
  • String값을 Integer, Long, Double 등의 java.lang.Number 타입 값으로 바꿀 때 CustomNumberEditor를 사용한다.
  • Date으로 변환할때는 CustomDateEditor를 사용한다.
  • java.text.DateFormat 인스턴스를 CustomDateEditor에 넘겨서 구문 분석과 날짜 문자열 생서에 사용할 날짜 형식을 지정할 수도 있다.
  • WebDataBinder에는 CustomNumberEditor 미리 등록되어 있지만, CustomDateEditor는 직접 등록해야 한다.

WebDataBinder 인스턴스 설정

  • @InitBinder를 설정한 메서드를 컨트롤러 클래스에 정의
  • WebBindingInitializer 구현을 웹 애플리케이션 컨텍스트 XML 파일에 설정
  • @ControllerAdvice를 설정한 클래스 안에 @InitBinder를 설정한 메서드를 선언
@InitBinder를 설정한 메서드를 컨트롤러 클래스에 정의


    @ModelAttribute(name = "newFixedDepositDetails")
    public FixedDepositDetails getNewFixedDepositDetails() {
        FixedDepositDetails fixedDepositDetails = new FixedDepositDetails();
        fixedDepositDetails.setEmail("You must enter a valid email");
        logger.info("getNewFixedDepositDetails() method: Returning a new instance of FixedDepositDetails");
        return fixedDepositDetails;
    }

    @InitBinder(value = "newFixedDepositDetails")
    public void initBinder_New(WebDataBinder webDataBinder) {
        logger.info("initBinder_New() method: Registering CustomDateEditor");
        webDataBinder.registerCustomEditor(Date.class, new CustomDateEditor(
                new SimpleDateFormat("MM-dd-yyyy"), false));
    }
  • value 속성값을 newFixedDepositDetails 이다.
  • initBinder_New 메서드가 초기화하는 WebDataBinder를 오직 newFixedDepositDetails 모델 속성에만 적용한다는 뜻이다.
  • @InitBinder를 설정한 메서드도 @RequestMapping을 설정한 메서드가 받을 수 있는 것과 똑같은 인수를 받을 수 있다.
  • 하지만 @InitBinder를 설정한 메서드는 모델 속성을 인수로 받거나 BindingResult (또는 Error) 객체를 인수로 받을 수 없다.
  • @InitBinder 메서드가 WebDataBinder 인스턴스와 함께 스프링 WebRequest나 java.util.Locale을 받는 것이 일반적이다.
  • 여기서는 @InitBinder 메서드의 반환 타입이 반드시 void 이어야 한다.
  • WebDataBinder의 registerCustomEditor 메서드를 사용해 PropertyEditor를 WebDataBinder 인스턴스에 등록한다.
  • @InitBinder를 설정한 메서드를 컨트롤러의 모델 속성마다 정의할 수 있고, @InitBinder를 설정한 메서드 하나를 컨트롤러의 모든 모델 속성에 적용할 수도 있다.
  • @InitBinder의 value를 지정하지 않으면 @InitBinder를 설정한 메서드가 초기화한 WebDataBinder 인스턴스를 컨트롤러의 모든 모델 속성에 적용할 수 있다.
WebBindingInitializer 구현설정
  • 처음에는 RequestMappingHandlerAdapter가 WebDataBinder를 초기화하고, 다음으로 WebBindingInitializer와 @InitBinder 메서드가 WebDataBinder를 초기화 한다.
  • 스프링 <annotation-driven> webDataBinder를 초기화하는 스프링 RequestMappingHandlerAdapter 인스턴스를 만든다.
  • 스프링 WebBindingInitializer 인터페이스의 인스턴스를 RequestMappingHandlerAdapter에 제공해서 WebDataBinder 인스턴스를 더 초기화하게 할 수 있다.
  • 추가로 컨트롤러 클래스에 @InitBinder 메서드를 정의해서 WebDataBinder 인스턴스를 더 초기화할 수도 있다.
  • 컨트롤러 클래스의 @InitBinder 메서드가 초기화하는 WebDataBinder는 해당 컨트롤러의 모델 속성에만 적용할 수 있다.
  • FixedDepositController의 모델 속성에만 CustomDateEditor가 필요하므로, @InitBinder를 설정한 FixedDepositController 클래스 안에 정의해서 CustomDateEditor를 WebDataBinder 인스턴스에 등록한다.
  • 스프링 WebBindingInitializer 인터페이스를 구현한 클래스의 인스턴스는 애플리케이션의 모든 컨트롤러에 적용할 WebDataBinder를 설정하는 역할은 한다.
  • <mvc:annotation-driven />는 스프링 컨테이너에 RequestMappingHandlerAdapter와 RequestMappingHandlerMapping 객체를 생성해 등록한다.
  • 다른 객체로 LocalValidatorFactoryBean과 FormattingConversionServiceFactoryBean이 있다.
  • <annotation-driven>는 RequestMappingHandlerAdapter와 RequestMappingHandlerMapping 객체를 커스텀화 할수 있도록 몇가지 속성을 제공한다.
  • RequestMappingHandlerAdapter와 RequestMappingHandlerMapping 객체에 대해 커스텀화 하고 싶은 내용을 <annotation-driven> 지원하지 않으면 <annotation-driven> 제거하고 웹 애플리케이션 컨텍스트 XML 파일에서 RequestMappingHandlerAdapter와 RequestMappingHandlerMapping 객체를 명시적으로 설정해야 한다.
  • <annotation-driven>는 커스텀 WebBindingInitializer 인스턴스를 RequestMappingHandlerAdapter에 설정하는 방법을 제공하지 않으므로, 웹 애플리케이션 컨텍스트 XML 파일에서 명시적으로 RequestMappingHandlerAdapter와 RequestMappingHandlerMapping 객체를 설정해야 한다.
  • ConfigurableWebBindingInitializer(WebBindingInitializer 구현)를 통해 CustomDateEditor 프로퍼티 에디터를 MyBank 애플리케이션의 모든 컨트롤러에서 사용할 수 있도록 설정하는 방법
    <bean id="handlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
        <property name="webBindingInitializer" ref="myInitializer" />
    </bean>

    <bean id="handlerMapping" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping"/>

    <bean id="myInitializer" class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
        <property name="propertyEditorRegistrars">
            <list>
                <bean class="sample.spring.chapter13.config.MyPropertyEditorRegistrar" />
            </list>
        </property>
    </bean>
  • 웹 애플리케이션 컨텍스트 XML 파일에서 명시적으로 RequestMappingHandlerAdapter와 RequestMappingHandlerMapping객체를 설정한다.
  • RequestMappingHandlerAdapter의 WebBindingInitializer 프로퍼티는 WebBindingInitializer 인터페이스르 구현하는 ConfigurableWebBindingInitializer 빈을 가리킨다.
  • ConfigurableWebBindingInitializer의 propertyEditorRegistrars 프로퍼티는 WebDataBinder에 등록할 하나 이상의 PropertyEditor를 지정한다.
  • MyPropertyEditorRegistrar 클래스가 webDataBinder에 CustomDateEditor 프로퍼티를 등록하는 방법이다.
public class MyPropertyEditorRegistrar implements PropertyEditorRegistrar {
    @Override
    public void registerCustomEditors(PropertyEditorRegistry propertyEditorRegistry) {
        propertyEditorRegistry.registerCustomEditor(Date.class, new CustomDateEditor(
                new SimpleDateFormat("MM-dd-yyyy"), false));

    }
}
  • 스프링의 PropertyEditorRegistrar 인터페이스를 구현하고 PropertyEditorRegistrar 인터페이스에 정의된 registerCustomEditos 메서드를 구현을 제공하는 MyPropertyEditorRegistrar을 보여준다.
  • registerCustomEditors 메서드에 전달한 propertyEditorRegistry 인스턴스를 사용해 프로퍼티 에디터를 초기화한다
  • 그리고 PropertyEditorRegistry의 registerCustomEditor 메서드를 사용해 WebDataBinder에 PropertyEditor 구현을 등록한다.
  • PropertyEditorRegistry의 registerCustomEditor를 사용해 CustomDateEditor 프로퍼티 에디터를 WebDataBinder 등록했다.
  • WebDataBinder를 초기화하기 위해 WebBindingInitializer를 사용하는 것은 복잡하다.
  • 단순한 대안은 @ControllerAdvice를 설정한 클래스 안에 @InitBinder를 설정한 메서드를 정의하는 것이다.
@ControllerAdvice를 설정한 클래스 안에 @InitBinder 메서드 정의
  • @ControllerAdvice 애너테이션도 @Component 애너테이션의 특별한 경우에 속한다.
  • @ControllerAdvice를 설정하면 해당 클래스가 컨트롤러를 지원한다는 뜻이다.
  • @ControllerAdvice를 설정한 클래스 안에 @InitBinder, @ModelAttribute, @ExceptionHandler를 설정한 메서드를 정의할 수 있고, 이런 메서드가 애플리케이션에 있는 모든 애너테이션을 설정한 컨트롤러 클래스에 적용된다.
  • 여러 컨트롤러 안에서 @InitBinder, @ModelAttribute, @ExceptionHandler 메서드를 중복해서 정의한다면, 해당 메서드는 @ControllerAdvice를 설정한 클래스에 정의 해야 한다.
  • 애플리케이션안에서 여러 컨트롤러에 적용되는 WebDataBinder 설정을 초기화하고 싶다면 여러 컨트롤러 안에 @InitBinder를 설정한 메서드를 정의하는 대신 @ControllerAdvice를 설정한 클래스 안에서 @InitBinder 메서드를 정의한다.
모델 속성 필드의 데이터 바인딩 과정 참여를 허용하거나 금지하기
  • WebDataBinder를 사용해 모델 속성의 필드가 데이터 바인딩 과정에 참여하도록 허용하거나 금지시킬 수 있다.
  • editableFixedDepositDetails 모델 속성은 viewFixedDepositDetails가 로드해서 임시로 HttpSession에 저장한다.
  • 악의적인 사용자가 id요청 파라미터값을 수정하면 WebDataBinder 과정에서 맹목적으로 FixedDepositController 객체의 값을 설정한다. 애플리케이션 데이터가 오염될 수 있음으로 FixedDepositController 객체의 id속성을 변경하는 것은 바람직하지 않다.
  • setAllowedFields와 setDisallowedFields 메서드를 제공한다.
    @InitBinder(value = "editableFixedDepositDetails")
    public void initBinder_Edit(WebDataBinder webDataBinder) {
        logger.info("initBinder_Edit() method: Registering CustomDateEditor");
        webDataBinder.registerCustomEditor(Date.class, new CustomDateEditor(
                new SimpleDateFormat("MM-dd-yyyy"), false));
        webDataBinder.setDisallowedFields("id");
    }
  • initBinder_Edit 메서드는 editableFixedDepositDetails 모델 속성에 대한 WebDataBinder 인스턴스를 초기화한다. setDisallowedFields 메서드는 editableFixedDepositDetails 모델의 속성의 id 필드가 데이터 바인딩 과정에서 참여하는 것을 금지하므로 요청 id라는 요청 파라미터가 들어있어도 모델 속성의 id 필드가 설정되지 않는다.
BindingResult 객체를 사용해 데이터 바인딩 검증 오류 조사
  • BindingResult 객체는 요청 파라미터를 모델 속성 필드에 바인딩한 결과를 컨트롤러 메서드에 제공한다.
  • 바인딩에서 타입을 변환하다가 오류가 발생하면 BindingResult 객체에 이 사실이 보고된다.
  • BindingResult 객체를 통해 전달되는 오류가 없을 때만 정기 예금을 생성하는 FixedDepositController의 openFixedDeposit 메서드

    @RequestMapping(params = "fdAction=create", method = RequestMethod.POST)
    public String openFixedDeposit(
            @ModelAttribute(name = "newFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {

        new FixedDepositDetailsValidator().validate(fixedDepositDetails,
                bindingResult);

        if (bindingResult.hasErrors()) {
            logger.info("openFixedDeposit() method: Validation errors - re-displaying form for opening a new fixed deposit");
            return "createFixedDepositForm";
        } else {
            fixedDepositService.saveFixedDeposit(fixedDepositDetails);
            sessionStatus.setComplete();
            logger.info("openFixedDeposit() method: Fixed deposit details successfully saved. Redirecting to show the list of fixed deposits.");
            return "redirect:/fixedDeposit/list";
        }
    }
  • BindingResult의 hasErrors 메서드는 BindingResult 객체 안에 바인딩이나 검증 오류가 하나 이상이 있으면 참을 반환한다.
  • BindingResult에 의해 오류가 보고되면 openFixedDeposit 메서드는 적절한 오류메시지와 함께 createFixedDepositForm 표시한다.
  • 오류를 검사하고 싶은 대상 모델 속성 바로 뒤에 BindingResult 객체를 위치 시켜야 한다.

스프링 검증지원

  • 스프링 웹 MVC 애플리케이션에서는 스프링 검증 API를 사용하거나 JSR 380 빈 검증, 제약사항을 모델 속성 필드에 사용해서 모델 속성을 검증할 수 있다.
스프링 Validator 인터페이스를 사용해 모델 속성 검증
package sample.spring.chapter13.web;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

import sample.spring.chapter13.domain.FixedDepositDetails;

public class FixedDepositDetailsValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return FixedDepositDetails.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        FixedDepositDetails fixedDepositDetails = (FixedDepositDetails) target;

        long depositAmount = fixedDepositDetails.getDepositAmount();
        String email = fixedDepositDetails.getEmail();

        if (depositAmount < 1000) {
            errors.rejectValue("depositAmount", "error.depositAmount.less",
                    "must be greater than or equal to 1000");
        }

        if (email == null || "".equalsIgnoreCase(email)) {
            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email",
                    "error.email.blank", "must not be blank");
        } else if (!email.contains("@")) {
            errors.rejectValue("email", "error.email.invalid",
                    "not a well-formed email address");
        }
    }
}
  • supports 메서드가 true로 반환하면 validate 메서드를 사용해 대상 객체를 검증할 수 있다.
  • FixedDepositDetailsValidator의 supports 메서드는 제공받는 객체 인스턴스의 타입이 FixedDepositDetails인지 검사한다.
  • supports가 true로 반환하면 FixedDepositDetailsValidator의 validate 메서드가 객체를 검증한다.
  • 검증할 객체 인스턴스를 받아서 Errors 인스턴스를 반환한다.
  • Errors 인스턴스는 검증하는 동안 생긴 오류를 저장하고 노출시킨다.
  • Errors 인스턴스는 오류를 Errors 인스턴스에 등록할때 사용하기 위한 여러가지 reject와 rejectValue 메서드를 제공한다.
  • 필드 수준의 오류를 보고 할때는 rejectValue를 사용하고 검증 중인 객체에 적용되는 오류를 보고할 때는 reject 메서드를 사용한다.
  • 스프링 ValidationUtils 클래스는 Validator를 호출하거나 빈 필드를 거부할때 쓸 수 있는 편의 메서드를 제공하는 유틸리티 클래스다.
  • FixedDepositDetails의 depositAmount 필드에 대한 검증 오류를 보고하기 위해 rejectValue 메서드에 전달한 파라미터를 보여준다.
  • 필드의 이름, 오류코드, 디폴트 오류 메시지가 rejectValue 메서드에 전달된다.
모델 속성의 검증 2가지 방법
  • Validator 구현의 validate 메서드를 명시적으로 호출한다.
  • WebDataBinder에 대해 Validator 구현을 설정하고, @RequestMapping의 모델 속성 인수에 JSR 380 @Valid를 설정한다.
@RequestMapping(params = "fdAction=create", method = RequestMethod.POST)
    public String openFixedDeposit(
            @ModelAttribute(name = "newFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {

        new FixedDepositDetailsValidator().validate(fixedDepositDetails,
                bindingResult);

        if (bindingResult.hasErrors()) {
            logger.info("openFixedDeposit() method: Validation errors - re-displaying form for opening a new fixed deposit");
            return "createFixedDepositForm";
        } else {
            fixedDepositService.saveFixedDeposit(fixedDepositDetails);
            sessionStatus.setComplete();
            logger.info("openFixedDeposit() method: Fixed deposit details successfully saved. Redirecting to show the list of fixed deposits.");
            return "redirect:/fixedDeposit/list";
        }
    }
  • openFixedDeposit 메서드가 FixedDepositDetailsValidator 인스턴스를 만들고 그 인스턴스의 validate 메서드를 호출한다.
  • BindingResult가 Errors의 하위 인터페이스이기 때문에 Errors 객체가 필요한 곳에 BindingResult 객체를 전달할 수 있다.
  • openFixedDeposit 메서드는 fixedDepositDetails 모델 속성과 BindingResult 객체를 validate 메서드에 전달한다.
  • BindingResult에 이미 데이터 바인딩 오류가 들어 있을수 있으므로 validate 메서드에 BindingResult 객체를 넘기면 BindingResult 객체에 검증 오류를 추가하는 것이다.
JSR 380 @Valid 애너테이션 사용해 모델 속성 검증
  • 모델 속성 인수에 추가하고 모델 속성에 대한 검증기를 WebDataBinder 인스턴스로 설정함으로써 스프링이 @RequestMapping 메서드에 전달하는 모델 속성을 자동으로 검증하게 만들 수 있다.
    @RequestMapping(params = "fdAction=create", method = RequestMethod.POST)
    public String openFixedDeposit(
            @Valid @ModelAttribute(name = "newFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {


        if (bindingResult.hasErrors()) {
            logger.info("openFixedDeposit() method: Validation errors - re-displaying form for opening a new fixed deposit");
            return "createFixedDepositForm";
        } else {
            fixedDepositService.saveFixedDeposit(fixedDepositDetails);
            sessionStatus.setComplete();
            logger.info("openFixedDeposit() method: Fixed deposit details successfully saved. Redirecting to show the list of fixed deposits.");
            return "redirect:/fixedDeposit/list";
        }
    }


    @InitBinder(value = "newFixedDepositDetails")
    public void initBinder_New(WebDataBinder webDataBinder) {
        logger.info("initBinder_New() method: Registering CustomDateEditor");
        webDataBinder.registerCustomEditor(Date.class, new CustomDateEditor(
                new SimpleDateFormat("MM-dd-yyyy"), false));
        webDataBinder.setValidator(new FixedDepositDetailsValidator());
    }
  • initBinder_New 메서드는 WebDataBinder의 setValidator 메서드를 호출해서 FixedDepositDetailsValidator를 newFixedDepositDetails 모델 속성에 대한 검증기로 설정했다.
  • 그리고 openFixedDeposit 메서드의 newFixedDepositDetails 모델 속성에 JSR 380의 @Valid를 설정했다.
  • openFixedDeposit 메서드가 호출되면 newFixedDepositDetails 모델 속성에 대해 데이터바인딩과 검증이 함께 실행되고, 데이터 바인딩과 검증결과를 BindingResult 인수를 통해 볼 수 있다.
  • @InitBinder이 모델 속성 이름을 지정하면 WebDataBinder에 설정한 검증기도 지정한 이름의 모델 속성에만 적용된다.
  • FixedDepositDetailsValidator는 newFixedDepositDetails 모델 속성에만 적용된다.
  • 애플리케이션의 여러 컨트롤러에 대해 같은 검증기를 여러번 적용해야 한다면 @ControllerAdvice를 설정한 클래스(WebBindingInitializer) 안에서 WebDataBinder에 대한 검증기를 설정해주는 @InitBinder 메서드를 정의한다.
JSR 380 애너테이션을 사용해 제약사항 지정
  • JSR 380 (빈 검증 2.0)에는 자바빈즈 컴포넌트의 프로퍼티에 대한 제약 사항을 지정할 때 사용하는 다양한 애너테이션 정의가 들어 있다.
import java.util.Date;

import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.Email;

public class FixedDepositDetails {
    private long id;

    @Min(1000)
    @Max(500000)
    private long depositAmount;

    @Email
    @Size(min = 10, max = 25)
    private String email;

    @NotNull
    private Date maturityDate;

    public FixedDepositDetails() {
    }

    public FixedDepositDetails(long id, long depositAmount, Date maturityDate,
            String email) {
        this.id = id;
        this.depositAmount = depositAmount;
        this.maturityDate = maturityDate;
        this.email = email;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public long getDepositAmount() {
        return depositAmount;
    }

    public void setDepositAmount(long depositAmount) {
        System.out.println("depositAmount - setter called");
        this.depositAmount = depositAmount;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public Date getMaturityDate() {
        return maturityDate;
    }

    public void setMaturityDate(Date maturityDate) {
        this.maturityDate = maturityDate;
    }

    public String toString() {
        return "id :" + id + ", deposit amount : " + depositAmount
                + ", email : " + email;
    }
}
  • JSR 380 애너테이션을 사용해 필드에 제약 사항을 지정한다.
  • @Min, @MAx, @Email, @Size, @NotNull 은 JSR 380에서 정의한 애너테이션 중 일부다.
  • 스프링 Validator 구현을 사용해서 객체를 검증한다면 Validator 구현에 들어 있는 제약 사항을 사용해야 한다.
  • JSR 380 애너테이션을 사용하기 위해 validation-api-2.0.0.FINAL과 hibernate-validation-6.0.4.FINAL에 대한 의존관계가 필요하다.
  • 하이버네이트 검증기 프레임워크는 JSR 380에 대한 참조 구현을 제공한다.
  • 하이버네이트 검증기 프레임워크는 JSR 380 애너테이션이 아닌 검증 애너테이션도 추가로 제공한다.
  • 하이버네이트 검증기의 @CreditCardNumber 애너테이션을 사용하면 필드 값이 올바른 신용카드 번호어야 한다고 지정할 수 있다.
  • 원하는 커스텀 제약 사항을 만들어 사용할 수 있다.
JSR 380 애너테이션을 사용해 객체 검증
  • JSR 380 프로바이더를 애플리케이션 클래스 경로에 찾을 수 있고, 웹 애플리케이션 컨텍스트 XML 파일에서 스프링 mvc 스키마의 mvc:annotation-driven 엘리먼트를 지정하면 스프링이 자동으로 JSR 380 지원을 활성화한다.
  • 이때 내부적으로는 <annotation-driven> 엘리먼트가 스프링 LocalValidatorFactoryBean 클래스의 인스턴스를 설정한다.
  • 인스턴스는 애플리케이션의 클래스 경로상에 JSR 380프로바이더가 있는지 감지하고, 감지한 프로바이더를 초기화한다.
  • LocalValidatorFactoryBean은 JSR 380의 Validator와 ValidatorFactory 인터페이스를 구현하면서 스프링 Validator 인터페이스도 구현한다.
  • 이런이유로 스프링 Validator 인터페이스의 validate 메서드를 호출하는 방식과 JSR 380 Validator의 validate를 호출하는 방식 중 어느쪽을 선택해도 괜찮다.
  • 모델 속성 인수 앞에 @Valid 애너테이션을 추가하기만 하면 @RequestMapping 메서드로 전달되는 모델 속성에 대한 검증을 스프링이 자동으로 수행하게 할 수 있다.
명시적으로 validate 메서드를 호출해서 모델 속성 검증
import org.springframework.validation.Validator;
import javax.validation.Valid;

@Controller
@RequestMapping(path = "/fixedDeposit")
@SessionAttributes(names = { "newFixedDepositDetails",
        "editableFixedDepositDetails" })
public class FixedDepositController {
    private static Logger logger = LogManager
            .getLogger(FixedDepositController.class);

    @Autowired
    private FixedDepositService fixedDepositService;

    @Autowired
    private Validator validator;


    @RequestMapping(params = "fdAction=create", method = RequestMethod.POST)
    public String openFixedDeposit(
            @ModelAttribute(name = "newFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {
        validator.validate(fixedDepositDetails, bindingResult);

        if (bindingResult.hasErrors()) {
            logger.info("openFixedDeposit() method: Validation errors - re-displaying form for opening a new fixed deposit");
            return "createFixedDepositForm";
        } else {
            fixedDepositService.saveFixedDeposit(fixedDepositDetails);
            sessionStatus.setComplete();
            logger.info("openFixedDeposit() method: Fixed deposit details successfully saved. Redirecting to show the list of fixed deposits.");
            return "redirect:/fixedDeposit/list";
        }
    }
  • LocalValidatorFactoryBean(스프링 Validator 인터페이스를 구현)은 FixedDepositController의 validator 인스턴스 변수에 자동으로 연결된다.
  • openFixedDeposit 메서드는 FixedDepositDetails 인스턴스를 검증하기 위해 LocalValidatorFactoryBean의 validate(Object, Errors) 메서드를 호출한다.
  • 검증 오류를 저장하기 위해 BindingResult 객체를 validate 메서드에 전달한다.
  • FixedDepositController가 FixedDepositDetails 객체를 검증하기 위해 JSR 380 API를 직접적으로 다루지 않는다.
  • 대신 FixedDepositController는 스프링 검증 API를 사용해 FixedDepositDetails 객체를 검증한다.
import javax.validation.ConstrainViolation;
import javax.validation.Validator;


@Controller
@RequestMapping(path = "/fixedDeposit")
@SessionAttributes(names = { "newFixedDepositDetails",
        "editableFixedDepositDetails" })
public class FixedDepositController {
    private static Logger logger = LogManager
            .getLogger(FixedDepositController.class);

    @Autowired
    private FixedDepositService fixedDepositService;

    @Autowired
    private Validator validator;

@RequestMapping(params = "fdAction=create", method = RequestMethod.POST)
    public String openFixedDeposit(
            @ModelAttribute(name = "newFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {

        Set<ConstrainViolation<FixedDepositDetails>> violations = validator.validate(fixedDepositDetails);
        Iterartor<ConstrainViolation<FixedDepositDetails>> itr = violations.iterator();

        if(itr.hasNext()) {
            ...
        }

    }
  • JSR 380에 따른 API를 사용해 FixedDepositDetails 객체를 검증하는 FixedDepositController 버전이다.
  • LocalValidatorFactoryBean은 FixedDepositController의 validator 인스턴스 변수에 자동 연결된다.
  • Validator의 validate 메서드를 호출하면 결과적으로 LocalValidatorFactoryBean의 validate(T) 메서드가 호출된다.
  • validate 메서드는 JSR 380 프로바이더가 보고한 제약 위반 사항이 들어 있는 java.util.Set를 반환한다.
  • validate 메서드가 반환하는 java.util.Set 객체를 살펴보고 제약 사항을 위반한 내용이 보고됐는지 찾을 수 있다.
JSR 380의 @Valid 애너테이션을 사용해 모델 속성 검증
  • 모델속성 인수에 JSR 380의 @Valid를 설정하면 @RequestMapping 에 전달되는 모델 속성을 스프링이 자동으로 검증하게 만들 수 있다.
    @RequestMapping(params = "fdAction=edit", method = RequestMethod.POST)
    public String editFixedDeposit(
            @Valid @ModelAttribute("editableFixedDepositDetails") FixedDepositDetails fixedDepositDetails,
            BindingResult bindingResult, SessionStatus sessionStatus) {

        if (bindingResult.hasErrors()) {
            logger.info("editFixedDeposit() method: Validation errors - re-displaying form for editing a fixed deposit");
            return "editFixedDepositForm";
        } else {
            fixedDepositService.editFixedDeposit(fixedDepositDetails);
            sessionStatus.setComplete();
            logger.info("editFixedDeposit() method: Fixed deposit details successfully changed. Redirecting to show the list of fixed deposits.");
            return "redirect:/fixedDeposit/list";
        }
    }
  • editableFixedDepositDetails 모델 속성에 설정한 @Valid로 인해 스프링이 자동으로 검증을 수행한다.

스프링 폼 태그 라이브러리

  • 스프링 폼 태그 라이브러를 사용하면 스프링 웹 MVC 애플리케이션에서 JSP 페이지를 더 편하게 작성할 수 있다.
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

<form:form modelAttribute="newFixedDepositDetails" name="createFixedDepositForm" method="POST"
action="${pageContext.request.contextPath}/fixedDeposit?fdAction=create">

    <form:input path="depositAmount" />
    <font style="color: #C11B17;">
    <form:errors path="depositAmount" />

    <input type="submit" value="Save">
</form:form>
  • tablib 디렉티브는 JSP 페이지에서 스프링 폼 태그 라이브러리 태그를 사용할 수 있게 한다.
  • errors 태그는 데이터 바인딩과 검증시 BindingResult에 추가된 바인딩 오류 및 검증 오류를 보여준다.
  • 특정 프로퍼티에 대응하는 오류 메시지를 표시하고 싶으면 프로퍼티의 이름을 path 속성값으로 지정한다.

자바 기반 설정을 사용해 웹 애너테이션 설정

  • 모두 스프링 빈을 루트 웹 애플리케이션 컨텍스트 XML(ContextLoarderListener가 읽음)과 자식 웹 애플리케이션 컨텍스트 XML(DispatcherServlet이 읽음), 그리고 web.xml로 설정한 DispatcherServlet과 ContextLoarderListener에서 정의했다.
  • 자바 설정방식에서는 루트와 자식 웹 애플리케이션 컨텍스트 빈이 다르게 취급되기 때문에 루트와 자식 웹 애너테이션 컨텍스트에 해당하는 @Configuration을 설정한 클래스를 따로 만들어야 한다.

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackages = { "sample.spring.chapter13.domain",
        "sample.spring.chapter13.dao", "sample.spring.chapter13.service" })
public class RootContextConfig {

}
  • RootContextConfig 클래스는 @ComponentScan을 사용해 도메인 엔티티와 DAO, 서비스를 등록한다. (applicationContext.xml과 동등)
package sample.spring.chapter13;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

@EnableWebMvc
@Configuration
@ComponentScan("sample.spring.chapter13.web")
public class WebContextConfig extends WebMvcConfigurerAdapter {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/jsp/");
        viewResolver.setSuffix(".jsp");
        registry.viewResolver(viewResolver);
    }
}
  • 웹 레이어에 속하는 빈을 설정하는 @Configuration을 설정하는 WebContextConfig 클래스다.
  • @EnableWebMvc는 스프링 mvc 스키마의 annotation-driven 과 같은 역할을 한다.
  • 이 애너테이션은 스프링 웹 MVC 애플리케이션에 필요한 객체를 설정한다.
  • 디폴트 설정은 오버라이드하려면 다른 설정에 해당하는 디폴트 메서드를 구현하는 WebMvcConfigurer 인터페이스를 구현한다.
  • WebContextConfig는 WebMvcConfigurer 클래스를 구현하면서 ConfigureViewResolvers 메서드를 오버라이드하여 JSP 페이지로 뷰을 연결해주는 InternalResourceViewResolver를 등록한다.
  • web.xml 파일을 사용하는 대신 스프링 AbstractAnnotationConfigDispatcherServletInitializer 클래스(스프링 WebApplicationInitializer를 구현)를 사용해 ServletContext를 프로그램에서 설정한다.
  • DispatcherServlet과 ContextLoarderListener를 ServletContext에 등록할 수 있다.
package sample.spring.chapter13;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class BankAppInitializer extends
        AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { RootContextConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebContextConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }
}
  • getRootConfigClasses 메서드는 루트 웹 애플리케이션 컨텍스트에 등록할 @Configuration또는 @Component을 설정한 클래스를 반환한다.
  • RootContextConfig 클래스가 루트 웹 애플리케이션 컨텍스트에 속하는 빈을 설정하므로, getRootConfigClasses 메서드는 RootContextConfig 클래스를 반환한다.
  • AbstractAnnotationConfigDispatcherServletInitializer는 ContextLoarderListener의 인스턴스를 루트 웹 애플리케이션 컨텍스트에 제공한다.
  • getServletConfigClasses 메서드는 자식 웹 애플리케이션 컨텍스트에 등록한 @Configuration 또는 @Component를 설정한 클래스를 반환한다.
  • AbstractAnnotationConfigDispatcherServletInitializer는 DispatcherServlet 인스턴스를 자식 웹 애플리케이션 컨텍스트에 제공한다.
  • getServletMappings 메서드는 DispatcherServlet의 서블릿 매핑을 지정한다.
  • web.xml과 XML을 사용하지 않고 스프링 웹 MVC 애플리케이션을 만들때 사용한다.

배워서 바로 쓰는 스프링프레임워크
애시시 사린, 제이 샤르마 지음
오현석 옮김

반응형

'JAVA > Spring' 카테고리의 다른 글

FactoryBean 사용  (0) 2023.11.02
빈 라이프 사이클 관리  (0) 2023.11.02
AOP  (0) 2022.01.28
캐싱  (0) 2022.01.28
데이터베이스 연결  (0) 2022.01.05