메서드에 @ModelAttribute
를 파라미터로 선언했을 경우 처리되는 과정은 다음과 같다.
-
파라미터 타입의 오브젝트를 새로 만든다. 때문에 디폴트 생성자가 필수로 필요하다.
@SessionAttributes
를 통해 저장된 오브젝트가 있으면 새로 만들지 않고 세션에서 가져온다. -
HTTP 요청을 생성(혹은 가져온) 오브젝트 프로퍼티에 바인딩 해준다.
이 과정에서 각 프로퍼티에 맞게 타입을 변환해준다.
만약 타입 변환 오류가 발생할 시BindingResult
오브젝트에 오류를 저장해서 컨트롤러로 넘겨준다. -
검증작업을 수행한다. 2번의 과정에서 타입에 대한 검증은 이미 끝냈고, 그 외의 검증은 검증기를 통해 등록할 수 있다.
프로퍼티 바인딩
프로퍼티 바인딩이란 오브젝트의 프로퍼티에 값을 넣는 행위를 말한다.
프로퍼티에 맞게 타입을 적절히 변환하고 해당 프로퍼티의 수정자 메서드를 호출하는 것이다.
스프링에선 크게 두가지의 프로퍼티 바인딩을 지원하는데
첫번째는 애플리케이션 컨텍스트 XML 설정파일로 빈을 정의할 때 사용했던 <property>
태그이다.
이 태그를 통해 빈의 프로퍼티에 값을 주입했었다.
두번째는 HTTP 요청 파라미터를 모델 오브젝트 등으로 변환하는 경우이다.
@ModelAttribute
뿐만 아니라 @RequestParam
, @PathVariable
등도 해당된다.
근데 잘 생각해보면, 프로퍼티 바인딩이 일반 primitive 타입
이 아닌 경우에도 가능했던 적이 있었다.
루트 웹 애플리케이션 컨텍스트에서 dataSource
빈을 설정할 때다.
1 | <bean id="dataSource" class="org.springframework..SimpleDriverDataSource"> |
보다시피 value
에 문자열로 클래스명을 전달하고 있다.
그런데 driverClass
프로퍼티는 String
타입이 아닌 Class
타입이다. 하지만 잘 바인딩 된다.
이는 스프링이 제공하는 프로퍼티 바인딩 기능을 사용했기 때문이다.
스프링은 프로퍼티 바인딩을 위해 2가지 API를 제공한다.
PropertyEditor
스프링이 기본적으로 제공하는 바인딩용 타입 변환 API이다.
PropertyEditor
는 스프링 API가 아니라 자바빈 표준에 정의된 API이다.
GUI 환경에서 비주얼 컴포넌트를 만들 때 사용하도록 설계되었고, 기본적인 기능은 문자열과 자바빈 프로퍼티 사이의 타입 변환이다.
스프링은 이PropertyEditor
를문자열-오브젝트
상호변환이 필요한 XML 설정이나 HTTP 파라미터 변환에 유용하게 사용할 수 있다고 판단하여 이를 일찍부터 사용해왔다.
스프링은 20여가지 정도의 PropertyEditor
를 만들어 디폴트로 제공하고 있다.
아래의 링크에서 확인할 수 있다.
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/propertyeditors/package-summary.html
이 디폴트 PropertyEditor
들은 바인딩 과정에서 파라미터 타입에 맞게 자동으로 선정되어 사용된다.
PropertyEditor 구현
디폴트 프로퍼티 데이터에 등록되지 않은 타입을 파라미터로 사용하고 싶을 경우, 직접 PropertyEditor
를 만들어 적용할 수 있다.
아래와 같은 enum
이 하나 있고,
1 | public enum Level { |
아래와 같이 컨트롤러를 등록하고 /user?level=1
과 같이 호출하면 자동으로 Level enum
으로 변환해서 받고 싶다고 하자.
(level
파라미터가 Integer
이지만 변환이 간단하므로 문제될 것 없다)
1 |
|
현재는 당연히 변환이 불가능하므로 오류가 발생한다.
Level
타입에 대한 PropertyEditor
를 만들어야 한다.
아래는 프로퍼티 에디터가 변환할 때의 동작 방식이다.
setValue()
, getValue()
는 그냥 getter
,setter
이기 때문에 손댈 것 없고,
실제로 우리가 구현해야 할 메서드는 setAsText()
와 getAsText()
이다.
현재 우리한테 필요한 부분은 문자열 -> 오브젝트
의 과정이므로 setAsText()
메서드를 구현해서 Level enum
에 대한 PropertyEditor
를 만들어보겠다.
1 | public class LevelPropertyEditor extends PropertyEditorSupport{ |
이제 이 PropertyEditor
를 userSearch
메서드에서 사용할 수 있게 등록해줘야 한다.
PropertyEditor 등록
PropertyEditor
를 추가하기 전에 먼저 컨트롤러에서 메서드 바인딩이 일어나는 순서를 알아보자.
AnnotationMethodHandlerAdapter
는 @RequestParam
, @PathVariable
, @ModelAttribute
와 같이 HTTP 요청을 변수에 바인딩하는 애노테이션을 만나면 먼저 WebDataBinder
라는 것을 만든다.
WebDataBinder
는 여러가지 기능을 포함하는데, 여기에 HTTP 요청 문자열을 파라미터로 변환하는 기능도 포함되어 있다.
즉, 우리가 만든 PropertyEditor
를 사용하려면 이 WebDataBinder
에 직접 등록해줘야 한다.
근데 WebDataBinder
의 변환 과정이 외부로 노출되지 않으므로, 직접 등록해 줄 방법은 없다.
그래서 스프링이 제공하는 WebDataBinder
초기화 메서드를 사용해야 한다.
- @InitBinder
컨트롤러 클래스에 아래와 같이@InitBinder
애노테이션이 부여되고,WebDataBinder
를 인자로 받는 메서드를 하나 생성하자.
1 |
|
그리고 WebDataBinder
의 registerCustomEditor
메서드에 PropertyEditor
를 적용할 타입과 PropertyEditor
인스턴스를 전달해주면 된다.
이후 다시 /user?level=1
을 호출해보면 level
변수에 Level.BASIC
오브젝트가 들어가있는 것을 확인할 수 있다.
WebDataBinder
대신WebRequest
를 받을 수도 있다!
initBinder
메서드는 클래스내의 모든 메서드에 대해 파라미터를 바인딩하기 전에 자동으로 호출된다.
바인딩 적용 대상은 @RequestParam
, @PathVariable
, @CookieValue
, @RequestHeader
, @ModelAttribute
의 프로퍼티 이다.
기본적으로 PropertyEditor
는 지정한 타입과 일치하면 항상 적용된다.
여기에 프로퍼티 이름을 추가 조건으로 주고, 프로퍼티 이름까지 일치해야만 적용되게 할 수 있다.
이러한 타입의 PropertyEditor
는 이미 PropertyEditor
가 존재할 경우 사용한다.
WebDataBinder
는 바인딩 시 커스텀 PropertyEditor
가 있을 경우 이를 선적용하고, 없을 경우 디폴트 PropertyEditor
를 적용하기 때문이다.
아래는 적절한 예시이다.
1 | public class MinMaxPropertyEditor extends PropertyEditorSupport{ |
이렇게 해두면 추가하는 유저의 age
값은 1~50
까지로 제한된다.
PropertyEditor
를 등록할 때 프로퍼티 이름으로 age
를 지정했기 때문에 id에는 적용되지 않는다.
참고로 이 방식은 프로퍼티 이름이 필요하므로 @RequestParam
같은 단일 파라미터 바인딩에는 적용되지 않는다.
- WebBindingInitializer
@InitBinder
방식은 범위가 컨트롤러 하나로만 제한되므로 다른 컨트롤러에서 사용하려면 또 다시 등록해줘야 한다.
만약 해당PropertyEditor
가 모든 곳에 적용해도 될 만큼 필요한PropertyEditor
라면 등록하는 방법을 달리하여 모든 컨트롤러에 적용해줄 수 있다.
먼저WebBindingInitializer
인터페이스를 구현한 클래스를 작성한다.
1 | public class MyWebBindingInitializer implements WebBindingInitializer{ |
이제 이 클래스를 빈으로 등록하고 AnnotationMethodHandlerAdapter
의 webBindingInitializer
프로퍼티에 DI
해주면 전체적으로 적용된다.
1 | <bean class="org.springframework..AnnotationMethodHandlerAdapter"> |
프로토타입 PropertyEditor
앞서 작성했던 PropertyEditor
등록 코드들을 보면, 매번 new 키워드
로 PropertyEditor
를 생성하고 있다.
이 부분이 뭔가 부담스럽게 생각되어 PropertyEditor
를 빈으로 등록하는 방식을 생각할 수 있는데, 이는 위험한 상황을 초래한다.
위의 PropertyEditor
동작방식을 다시 살펴보면, 변환과정에서 항상 set -> get
의 순서로 2개의 메서드를 사용하고 있음을 볼 수 있다.
이 말인 즉, PropertyEditor
는 짧은 시간이나마 상태를 가진다는 것을 의미한다.
상태를 가지는 오브젝트는 절대 빈으로 등록되서는 안된다.
매번 new 키워드
로 생성되는 부분이 부담스러워 보일 수 있으나, 실상 PropertyEditor
는 워낙 간단한 클래스라 자주 생성되도 별로 문제가 되지 않는다.
그러므로 싱글톤으로 PropertyEditor
를 생성하는 실수를 하지 않도록 주의해야 한다.
근데 개발을 하다보면, PropertyEditor
에서 다른 빈을 DI
받아야 할 경우가 가끔 생긴다.
예를 들면 아래와 같이 변환할 프로퍼티가 하나의 도메인 오브젝트에 대응하는 경우이다.
1 | public class User{ |
이런 경우, 일반적인 방법으로는 변환할 수 없다. 요청 파라미터는 평범한 문자열이기 때문이다.
이 상황을 해결할 수 있는 방법은 2가지가 있다.
- 모조 PropertyEditor
Code
오브젝트로 변환하되, 완벽하지 않은 오브젝트로 변환하는 방법이다.
1 | public class CodePropertyEditor extends PropertyEditorSupport{ |
이런식으로 전달받은 id
값만 채운 불완전한 Code
오브젝트를 돌려주는 것이다.
이런 방식을 모조 PropertyEditor
라고 부른다.
하지만 이 방식은 조금 위험하다. 다른 프로퍼티들의 값이 모두 null
인 불완전한 오브젝트로 변환해주기 때문이다.
이런 오브젝트는 업데이트가 발생하면 심각한 문제를 초래할 수 있으나,
사실상 이러한 코드성 도메인 오브젝트는 다른 테이블에서 참조하는 용도로만 사용하는 것이 대부분이다.
그래서 이런 부분만 유의해주면 매우 유용하게 활용할 수 있다.
- 프로토타입 도메인 오브젝트 PropertyEditor
PropertyEditor
를 프로토타입 빈으로 등록하고, 서비스나DAO
객체를DI
받아Code
오브젝트를 조회해오는 방법이다.
1 |
|
이 방식의 장점은 항상 완전한 도메인 오브젝트를 리턴해주므로, 앞서 제기했던 위험이 없어진다.
단점으로는 매번 DB에서 조회를 해야하므로 성능에 조금 부담을 주는 단점이 있다.
하지만 JPA와 같이 엔티티 단위의 캐싱 기법이 발달한 기술을 사용할 경우, DB에서 조회하는 대신 메모리에서 바로 읽어올 수 있으므로 DB 부하에 대한 걱정은 하지 않아도 된다.
Converter
PropertyEditor
는 근본적인 단점이 있다.
상태를 가지고 있으므로 싱글톤으로 등록할 수 없고, 항상 새로운 오브젝트를 만들어야 한다는 점이다.
스프링 3.0이후로 이러한 PropertyEditor
의 단점을 보완해주는 Converter
라는 타입 변환 API가 등장하였다.
Converter
는 PropertyEditor
와 달리 변환과정에서 메서드가 한번만 호출된다.
즉, 상태를 가지지 않는다는 뜻이고, 싱글톤으로 등록할 수 있다는 뜻이다!
Converter 구현
아래는 Converter
인터페이스이다.
1 | public interface Converter<S, T>{ |
양방향 변환을 지원하던 PropertyEditor
와는 달리, 단방향 변환만을 지원한다.
(양방향을 원하면 그냥 반대방향의 Converter
를 하나 더 만들면 된다.)
게다가 한쪽 타입이 무조건 String
으로 고정되는 불편함 없이 직접 지정 가능하다.
아래는 전달받은 파라미터를 Level
타입으로 변환해주는 Converter
이다.
1 | public class LevelConverter implements Convert<Integer, Level>{ |
타입을 바로 Integer
로 지정함으로써 지저분한 타입변환 코드를 제거할 수 있다.
Converter 등록
PropertyEditor
처럼 직접 등록할 수 없고, ConversionService
타입의 오브젝트를 통해서 WebDataBinder
에 등록해야 한다.
ConversionService
타입의 오브젝트를 빈으로 등록하고 이를 DI
받아 WebDataBinder
에 등록하는 방식이므로 PropertyEditor
에 비해 부담이 적다.
ConversionService
를 등록하는 방법은 2가지가 있다.
첫째로 직접 클래스를 만들고 GenericConversionService
를 상속받은 뒤, addConverter()
메서드로 Converter
들을 등록하는 방식이다. 이후 빈으로 등록한다.
둘째는 추가할 Converter
들을 빈으로 등록해두고 ConversionServiceFactoryBean
을 이용해서 Converter
들이 추가된 GenericConversionService
를 빈으로 등록하는 방식이다.
직접 클래스를 만들지 않고 설정만으로 가능하므로 좀 더 편리하다.
아래는 두번째 방법이다.
1 | <bean class="org.springframework..ConversionServiceFactoryBean"> |
그리고 컨트롤러에서 아래와 같이 해주면 된다.
1 |
|
매번 개별적으로 등록해줘야하는 PropertyEditor
와는 달리 하나의 ConversionService
에 Converter
들을 일괄적으로 지정할 수 있어 매우 편리하다.
때에 따라서는 여러개의 ConversionService
를 만들어놓고 사용하기도 한다.
하지만 WebDataBinder
는 하나의 ConversionService
타입 오브젝트만 허용한다는 점은 알고있어야 한다.
Converter
도 PropertyEditor
처럼 WebBindingInitializer
를 이용해 일괄등록 할 수 있다.
하지만 ConversionService
를 등록할 떄는 ConfigurableWebBindingInitializer
를 이용하는 것이 더 편리하다.
1 | <bean class="org.springframework..ConversionServiceFactoryBean"> |
이게 전부 ConversionService
를 싱글톤 빈으로 등록할 수 있기에 생겨난 방법들이다.
만약 spring 설정에서
<mvc:annotation-driven />
를 사용했을 경우,
위의 두 방법처럼ConversionService
를 등록하는 것이 불가능하다.
이럴경우<mvc:annotation-driven conversion-service="conversionService"/>
처럼 엘리먼트를 이용해서 등록해줘야 하며, 이렇게 등록할 경우 모든 클래스에 자동으로 적용된다.
Formatter
위의 두 가지 외에 Formatter
라는 타입 변환 API가 하나 더 있다.
근데 이는 스프링에서 기본으로 제공하는 API가 아니라서, 절차가 조금 까다롭다.
일단 Formatter
인터페이스는 아래와 같다.
1 | // Formatter interface |
이 인터페이스를 구현해서 Formatter
를 만들면 된다. 보다시피 Locale
을 파라미터로 받을 수 있어 컨트롤러에 사용하기 좀 더 특화되었다고 할 수 있다.
근데… Formatter
는 스프링 기본 API가 아니라서 GenericConversionService
에 직접 등록할 수 없다.
Formatter
를 GenericConverter
로 포장해서 등록해주는 FormattingConversionService
를 통해서만 등록될 수 있다.
그리고 Formatter
를 본격적으로 사용하려면 이게 끝이 아니라
애노테이션을 연결시켜야 하므로 AnnotationFormatterFactory
도 사용해야 한다.
이래서 굳이 Locale
이 타입 변환에 필요한 경우가 아니라면 Converter
를 사용하는 편이 낫다.
당장은 FormattingConversionServiceFactoryBean
을 통해 FormattingConversionService
를 등록하고, 거기서 기본으로 등록되는 Formatter
만 사용해도 유용하다.
1 | <bean class="org.springframework..FormattingConversionServiceFactoryBean" /> |
이렇게 빈으로 등록하고 위와 같은 방식으로 conversionService
를 주입해주면 된다.
이 또한 만약 spring 설정에서
<mvc:annotation-driven />
를 사용했을 경우 사용이 불가능하다.
참고로FormattingConversionServiceFactoryBean
은<mvc:annotation-driven />
사용 시 디폴트로 등록해주는ConversionService
라 위처럼conversion-service
엘리먼트를 이용해 따로 등록해 줄 필요없다.
- @NumberFormat
첫째로 사용할 수 있는 애노테이션 기반 포멧터이다.
이는NumberFormatter
,CurrencyFormatter
,PercentFormatter
와 연결되어 있다.
엘리먼트로style
과pattern
을 줄 수 있다.
style
은Number
,Currency
,Percent
세 가지를 설정할 수 있고, 각각 위의Formatter
와 연결된다.
style
에 없는 패턴을 사용하고 싶을 경우pattern
엘리먼트를 통해 직접 지정할 수 있다.
아래는pattern
엘리먼트를 사용한 예제이다.
1 | class Product{ |
request 인자로 price=$100,000
과 같이 넘겨줘도 price
프로퍼티에서 변환해서 받을 수 있고, 뷰로 내려줄 때에 $100,000
의 형태로 내려줄 수 있다.
- @DateTimeFormat
강력한 날짜, 시간 라이브러리인Joda Time
을 이용하는 애노테이션 기반 포멧터이다.
이는DateTimeFormatter
와 연결되어 있다.
엘리먼트로style
과pattern
을 줄 수 있다.
S
(short),M
(medium),L
(long),F
(full) 4개의 문자를 날짜와 시간에 대해 1글자씩 사용해 스타일을 지정한다.
1 | "FS") (style= |
이는 yyyy'년' M'월' d'일' EEEE a h:mm
포멧으로 매핑된다. 물론 각각의 지역정보에 따라 다르게 출력된다.
style
에서 지정한 패턴이 마음에 들지 않는 경우, pattern
엘리먼트를 통해 직접 지정 가능하다.
1 | "yyyy/MM/dd") (pattern= |
바인딩 기술 활용 전략
위의 3가지 방법은 각기 장단점이 있기 때문에 하나만 골라 사용하는 것은 바람직하지 않다.
아래는 어떤 경우에 어떤 바인딩 기술을 활용하는 것이 좋은지에 대한 몇 가지 시나리오이다.
-
사용자 정의 타입 바인딩을 위한 일괄 적용 :
Converter
앞의Level enum
처럼 애플리케이션에서 정의한 타입이면서 모델에서 자주 활용되는 타입이라면Converter
로 만들고ConversionService
로 묶어서 일괄 적용하는 것이 편리하다. -
메타정보를 활용하는 조건부 바인딩 :
ConditionalGenericConverter
바인딩이 특정 조건(필드, 메서드 파라미터, 애노테이션 등)에 따라 다르게 동작할 때에는ConditionalGenericConverter
를 이용해야 한다. 구현이 까다롭다. -
애노테이션을 통한 바인딩 :
AnnotationFormatterFactory
,Formatter
애노테이션을 통해 바인딩 하고 싶을 경우 사용하면 좋다. -
특정 필드에만 바인딩 :
PropertyEditor
특정 모델의 특정 필드에 제한해서 바인딩을 적용해야 할 경우PropertyEditor
를 사용하는 것이 편리하다. 필드 이름을 메서드 파라미터로 전달할 수 있기 때문이다.
이렇듯 여러 바인딩 기술들을 등록하다 보면 서로 중복되는 부분이 발생할 것이다.
이럴 경우 우선순위에 의해 바인딩이 적용된다.
Custom PropertyEditor > ConversionService > Default PropertyEditor
중복 시 위의 순서로 바인딩된다.
그리고 WebBindingInitializer
를 통해 등록한 공통 바인딩은 @InitBinder
보다 우선순위가 뒤쳐진다.
WebDataBinder 설정 항목
WebDatBinder
에는 PropertyEditor
, ConversionService
등록 외에도 여러 유용한 바인딩 옵션들이 있다.
- allowedFields, disallowedFields
@ModelAttribute
를 사용할 경우 근본적인 보안 문제가 하나 있다.
@SessionAttributes
를 사용해 변경할 필드만 폼에 표출했다고 하더라도 사용자가 임의로 폼을 조작하여 전달하는 값에 대해서는 변경을 막지 못한다는 점이다.
폼에는 표출하지 않았지만 사용자가 예를 들어level
이라는 필드를 폼에 추가하여 전송할 경우 실제 값이 바뀌는 일이 생길 수도 있다는 것이다.
이를 대비해 폼에 표출한 필드 외에는 모델에 바인딩 되지 않도록 설정할 필요가 있다.
여기에 사용되는 것이 위의 두 속성이다.
allowedFields
에는 바인딩을 허용할 필드 목록을 넣을 수 있고, disallowedFields
에는 바인딩을 금지할 필드 목록을 넣을 수 있다.
1 |
|
이러면 위의 지정한 필드명 외에 다른 필드는 아무리 HTTP 요청으로 보내봐야 바인딩 되지 않는다.
게다가 *level*
처럼 와일드카드도 사용할 수 있다.
-
requiredFields
필수 파라미터를 지정할 수 있다.
@ModelAttribute
의 특성 상 파라미터가 들어오지 않았다고 바로 에러를 발생 시키지 않고BindingResult
에 검증 결과를 저장한다.
하지만setRequiredFiedls()
메서드로 필수 파라미터를 지정해 줄 경우, 파라미터가 들어오지 않으면 바로 에러가 발생한다. -
fieldMarkerPrefix
input checkbox
는 조금 특별한 성질이 있다.
1 | <input type="checkbox" name="type" value="on" /> |
폼에 이와 같은 체크박스가 있다고 했을 때, 이를 체크하고 전달하면 type=on
의 형태로 데이터가 전달되지만, 체크하지 않고 전달하면 아예 값을 전달하지 않는다는 점이다.
즉 수정폼에서 기존에 체크되어있던 체크박스를 해제하고 전달할 경우 아무런 값도 전달되지 않기 때문에 사용자는 값을 변경할 수 없는 문제가 발생하게 되는 것이다.
이럴 때 필드마커
라는 것을 이용해 해결 할 수 있는데, 아래와 같다.
1 | <input type="checkbox" name="type" /> |
_type
의 앞에 붙은 _
를 필드마커
라고 하는데, 스프링은 이런 필드마커가 있는 필드를 발견할 경우, 필드마커를 제외한 이름의 필드가 폼에 존재한다고 생각한다.
즉, 체크박스를 선택하지 않아 type
파라미터가 전달되지 않았지만, _type
필드가 전달 되었으므로 스프링은 type
필드가 폼에 있다고 판단하는 것이다.
그리고 이처럼 _type
은 전달되고 type
은 전달되지 않았을 경우, 체크박스를 해제했기 때문이라 생각하고 해당 프로퍼티 값을 리셋해준다.
리셋 방식은 boolean 타입이면 false, 배열타입이면 빈 배열, 그 외라면 null을 넣어주는 것이다.
WebDataBinder
의 setFieldMarkerPrifix()
메서드는 이 필드마커를 변경해주는 메서드이다. 기본값은 _
이다.
- fieldDefaultPrefix
필드 디폴트
는 히든 필드를 이용해 체크박스의 디폴트 값을 지정하는데 사용한다.
1 | <input type="checkbox" name="type" value="A"/> |
!type
히든 필드를 지정해서 type
필드의 기본값을 지정해줬다.
이럴 경우 체크박스를 선택하지 않아 type
필드가 전달되지 않을 경우, 디폴트 값인 Z가 전달된다.
모델 프로퍼티 값이 단순값이 아닐 경우 유용하게 사용할 수 있다.
이 또한 setFieldDefaultPrefix()
메서드를 이용해 접두어를 변경해 줄 수 있다. 기본값은 !
이다.
검증
@ModelAttribute
의 바인딩 작업이 실패로 끝나는 경우는 2가지가 있다.
첫째로 @ModelAttribute
가 기본적으로 실행하는 타입 변환에서 오류가 발생했을 경우이고,
둘째로 검증기(validator)
를 통과하지 못했을 경우이다. 이는 사용자가 직접 정의하는 부분이다.
사실상 폼의 서브밋을 처리하는 컨트롤러 메서드에서는 검증기를 이용한 검증 작업은 필수이다.
검증 결과에 따라 다음 스텝으로 넘어가든, 다시 폼을 띄워 수정을 요구하든 해야한다.
이 과정에서 쓰이는 API인 Validator
, BindingResult
, Errors
에 대해 알아보자.
Validator
오브젝트 검증기를 정의할 수 있는 API이다. @ModelAttribute
바인딩 때 주로 사용된다.
아래는 Validator
인터페이스이다.
1 | public interface Validator{ |
supports()
는 이 검증기가 검증할 수 있는 타입인지 확인하는 메서드이고,
이를 통과할 경우 validate()
를 통해 검증이 진행된다.
validate()
의 검증과정에서 아무 문제가 없으면 메서드를 정상 종료하면 되고,
문제가 있을 시 Errors
인터페이스에 오류정보를 등록해주면 된다.
이후 이 오류정보를 통해 컨트롤러에서 적절한 작업을 해주면 되는 것이다.
자바스크립트로 입력값을 검증했을 경우 서버에서 검증작업을 생략해도 될까?
안된다. 서버의 검증작업을 생략하면 매우 위험해진다.
브라우저에서 자바스크립트가 동작하지 않게 할수도 있고, 강제로 폼을 조작할수도 있고, Burp suite 같은 것을 사용하여 전달되는 데이터를 변경할 수도 있다.
그러므로 서버 검증작업은 필수로 있어야 한다.
아래는 Validator
구현의 예시이다.
1 | public class UserValidator implements Validator{ |
주석에도 써놓았지만 오류 정보를 등록하는 방법이 다양하다.
rejectValue()
에 사용된 name
은 필드 이름이며, name.required
는 에러 코드를 정의한 것이다.
(이 에러코드는 messageSource
와 함께 사용될 수 있다. 사용 방법 보기)
age
필드를 검증할때, 보다시피 에러코드에 파라미터를 전달할수도 있으며 디폴트 메세지도 전달할 수 있다.
제일 아랫부분처럼 2가지 이상의 필드에 대해 검증하는 경우, 필드명을 생략 가능하다.
ValidationUtils
같은 유틸리티 클래스도 제공되니 잘 활용하면 좋다.
Validator
는 싱글톤 빈으로 등록 가능하기 때문에 서비스 로직을 이용하여 검증작업을 진행할 수도 있다. 대표적인 것이 아이디 중복 검사이다.
근데 사실 이 정도 검증까지 가면 좀 모호해지는게 있는데, 검증이 수행되는 계층이다.
검증 작업을 컨트롤러 로직이라고 보는 개발자도 있는 반면, 대부분이 서비스 계층과 연관이 있으니 서비스 계층의 로직이라고 보는 개발자도 있다.
이는 개인이 잘 판단하면 될 문제인 것 같다.
중요한 것은 어느 곳에서 사용하든, 위와 같이 검증로직은 따로 분리되어 있는 것이 좋다.
Validator 사용하기
- 컨트롤러 메서드 내에서 검증
Validator
는 빈으로 등록 가능하니 이를 컨트롤러에서DI
받은 뒤, 각 컨트롤러 메서드에서validate()
를 직접 호출해서 검증을 진행하는 방식이다.
(모델 오브젝트의 타입은 굳이 확인할 필요 없으므로supports()
는 생략 가능하다)
1 |
|
@Valid
를 이용한 자동 검증
JSR-303의 @javax.validation.Valid
애노테이션을 사용하는 방법이다.
컨트롤러에서 직접validate()
를 호출하여 검증하던 방식과 달리, 바인딩 과정에서 자동으로 검증이 진행되도록 할 수 있다.
1 |
|
WebDataBinder
에는 보다시피 Validator
타입의 검증용 오브젝트도 등록할 수 있다.
그리고 아래 @ModelAttribute
를 사용하는 부분에 추가로 @Valid
애노테이션을 사용해주면 자동으로 검증작업이 수행된다.
개인적으로 위의 방식보다 훨씬 나아 보인다 ㅋㅋ
참고로 @InitBinder
말고 WebBindingInitializer
를 이용해 모든 컨트롤러에 일괄 적용할 수도 있다.
- 서비스 계층에서 검증
자주 사용되지 않지만Validator
가 싱글톤 빈으로 등록되기에 서비스 계층에서도 얼마든지 DI 받아 사용할 수 있다.
서비스 계층에서 반복적으로 같은 검증작업을 수행할 경우 사용하기도 한다.
근데 이럴 경우,BindingResult
타입 오브젝트를 직접 만들어서validate()
에 전달해야 하는데, 이때는BeanPropertyBindingResult
를 사용하는 것이 적당하다.
서비스 계층을 활용하는
Validator
Validator
는 싱글톤 빈으로 등록될 수 있으므로 다른 빈을 DI받아 사용할 수 있다.
앞서 예시로 들었던 ID 중복 검사처럼,Validator
내에서 서비스 계층 빈을 사용하여 검증할 수도 있다.
게다가 이 경우 결과를BindingResult
에 담으면 되므로 서비스 계층에서 번거롭게 예외를 던지던 방식을 제거할 수 있다.
대신 이 방식을 사용하면 컨트롤러에서 서비스 계층을 두번 호출한다는 단점이 있다.
하지만 전체적으로 코드가 깔끔해지고 역할 분담이 확실해지는 장점이 있다.
JSR-303 빈 검증 기능
@Valid
를 포함하고 있는 JSR-303
의 빈 검증 방식도 스프링에서 사용할 수 있다.
1 | public class User{ |
이런식으로 모델에 특정 애노테이션만 작성해주면 된다. Validator
를 직접 구현해서 검증기를 만들필요 없이 간단하게 검증작업을 진행할 수 있다.
이 검증 방식을 사용하려면 LocalValidatorFactoryBean
을 사용해야 한다.
LocalValidatorFactoryBean
이 생성하는 클래스 타입은 Validator
이므로 이를 빈으로 등록한 뒤 DI
받아 사용하면 된다.
(컨트롤러에서 직접 생성해도 되고, WebDataBinder
에 등록해도 된다)
BindingResult의 에러 코드
앞서 Validator
에서 errors.rejectValue("name", "name.required")
와 같이 에러코드를 지정하던 작업이 기억날 것이다.
이 정보는 보통 컨트롤러에 의해 폼을 다시 띄울 때 활용된다.
스프링은 등록된 에러 코드를 아래의 같은 파일에서 찾아와 에러 메세지로 활용한다.
1 | name.required=이름은 필수로 입력하셔야 합니다. |
근데 이렇게 지정한 에러코드로 바로 메세지를 찾는것은 아니고, 스프링의 MessageCodeResolver
라는 것을 거쳐 에러코드를 확장하는 작업을 한번 거친다.
(스프링의 디폴트 MessageCodeResolver
는 DefaultMessageCodeResolver
이다.)
이 리졸버를 거치게 되면 우리가 등록한 에러코드 name.required
는 아래와 같이 4가지 에러코드로 확장된다.
- 에러코드.모델이름.필드이름 : name.required.user.name
- 에러코드.필드이름 : name.required.name
- 에러코드.타입이름 : name.required.User
- 에러코드 : name.required
이 4가지 에러코드는 위에서부터 우선순위를 가진다.
즉 메세지 파일이 아래와 같다면,
1 | name.required.user.name=무언가 잘못된 메세지 |
우리는 에러코드를 분명 name.required
라고 지정했지만 계속해서 무언가 잘못된 메세지
가 출력될 것이다.
그러므로 에러코드 지정 시 어떤 에러코드로 확장되는지 정확히 알고 있어야 한다.
아니면 위처럼 의도하지 않은 상황이 발생할 수 있기 떄문이다.
(개인적으로 좀 혼란스러운 방식이라고 생각한다…)
위는 한가지 예시였을 뿐이고, 검증방식(rejectValue
, reject
, 타입 오류 등, JSR-303
)에 따라 에러코드가 확장되는 룰이 다르니 사용할 떄 주의해야 한다.
MessageSource
위에서 확장된 에러코드는 마지막으로 MessageSourceResolver
라는 것을 거쳐 실제 메세지로 생성된다. 이 때 사용하는 것이 이 MessageSource
이다.
이는 디폴트로 등록되지 않으니 스프링의 빈으로 직접 등록해줘야 한다.
MessageSource
는 2가지 종류가 있는데 보통 ResourceBundlerMessageSource
를 사용한다.
이는 일정시간마다 메세지 파일 변경 여부를 확인해서 메세지를 갱신해주므로 서버가 구동중인 상황에도 메세지를 변경해줄 수 있다.
1 | <bean id="messageSource" class="org.springframework...ResourceBundleMessageSource" /> |
속성값으로 메세지 파일을 지정해주지 않을 경우 디폴트로 messages.properties
파일이 사용된다.
MessageSource
는 아래의 4가지 정보를 활용해 최종 메세지를 생성한다.
-
코드
메세지 파일은키=벨류
의 형태로 등록되어 있기 때문에, 메세지를 찾을 키 값은 필수이다.
앞서 우리가Errors
에 등록했던 에러 코드가 이 키 값인 것이다. -
메세지 파라미터 배열
앞서 에러코드를 등록하며Object[]
타입의 파라미터를 넘겨줬던 것을 기억할 것이다.
해당 파라미터는 메세지를 생성하는데 사용될 수 있다.
메세지 파일은 아래와 같이 작성된다.
1 | field.min={0}보다 작은 값을 사용할 수 없습니다. |
파라미터는 1개 이상 올 수 있기때문에 Object
배열을 사용한다.
-
디폴트 메세지
코드에 맞는 메세지를 찾지 못하였을때 디폴트 메세지를 지정해줄 수 있다.
에러코드를 충실히 적용했다면 이 부분은 생략하거나null
로 주면 된다.
참고로 코드에 해당하는 메세지도 없고, 디폴트도 없을 경우 예외가 발생하니 주의해야 한다. -
지역정보
LocaleResolver
에 의해 결정된 현재의 지역정보를 사용할 수 있다.
지역정보에 따라 다른 프로퍼티 파일을 사용 가능하다.
만약messages.properties
파일을 사용했다면Locale
이ENGLISH
일 경우messages_en.properties
파일이 사용된다.
이를 통해 다국어 서비스를 적용할 수 있다.
모델의 사이클
모델은 MVC 아키텍쳐에서 정보를 담당하는 컴포넌트이다.
요청 정보를 담기도 하고, 비즈니스 로직에 사용되기도 하고, 뷰에 출력되기도 한다.
또한 이 모델은 아주 여러곳을 거쳐가며 만들어지고, 변형된다.
그러므로 모델의 사이클에 대한 지식은 스프링 MVC를 사용할 떄 가장 중요하다고 할 수 있다.
-
HTTP 요청에서 컨트롤러 메서드까지
왼쪽에서 오른쪽으로 보면 된다. -
컨트롤러 메서드에서 뷰까지
오른쪽에서 왼쪽으로 보면 된다.
모델의 생성, 변형, 사용이 한눈에 볼 수 있게 잘 표시되어 있다.