이전 글에서는 스프링 MVC의 기본적인 컨트롤러 구현 방법을 살펴봤다.
이 글에서는 메시지를 출력하는 방법과 커맨드 객체의 값을 검증하는 방법에 대해 다룰 것이다.
사용자 화면에 보일 문자열은 JSP에 직접 코딩한다. 예를 들어 로그인 폼의 일부 코드가 다음과 같다고 하자.
<label>이메일</label>
<input tyoe="text" name="email">
'이메일'과 같은 문자열은 로그인 폼, 회원가입 폼, 회원 정보 수정 폼 등에서 반복해서 사용된다.
이렇게 문자열을 직접 하드코딩하면 동일 문자열을 변경할 때 해당 문자열이 들어간 모든 폼을 찾아 하나하나 변경해야 한다.
또 다른 문제점이 있다. 만약 전세계를 대상으로 서비스를 제공한다면, 사용자의 언어 설정에 따라 '이메일' 또는 'E-mail'과 같이 각 언어에 맞게 문자열을 표시해야 한다. 그런데 여기서 문자열이 하드코딩되어 있다면, 언어별로 뷰 코드를 따로 만드는 번거로운 상황이 발생한다.
하드코딩함으로써 생기는 이 두 가지 문제점을 해결하는 방법은,
뷰 코드에서 사용할 문자열을 언어별로 파일에 보관하고,
뷰 코드는 언어에 따라 알맞은 파일에서 문자열을 읽어와 출력하는 것이다.
스프링은 자체적으로 이 기능을 제공하고 있다. 문자열을 별도 파일에 작성하고 JSP 코드에서 이를 사용하려면 다음 작업을 하면 된다.
메시지 파일은 자바의 프로퍼티 파일 형식으로 작성한다.
다음은 메시지 파일인 label.properties 파일의 예시 코드이다.
member.register=회원가입
term=약관
term.agree=약관동의
next.btn=다음단계
member.info=회원정보
email=이메일
name=이름
password=비밀번호
password.confirm=비밀번호 확인
register.btn=가입완료
register.done=<strong>{0}님</strong>, 회원 가입을 완료했습니다.
go.main=메인으로 이동
스프링 설정 클래스 중 하나에 추가해주면 된다.
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
ms.setBasenames("message.label");
ms.setDefaultEncoding("UTF-8");
return ms;
}
주의❗
빈의 아이디를 "messageSource"로 지정해야 한다. 다른 이름을 사용할 경우 정상적으로 동작하지 않는다.
위의 커스텀 태그를 사용하려면 태그 라이브러리 설정을 추가해야 한다.
<%@taglib prefix="spring" uri="http://www.springframework.org/tags" %>
메시지 파일로부터 읽어올 것이므로, 다음 코드를
<title>회원가입</title>
<spring:message> 태그로 메시지를 출력하기 위해 다음과 같이 바꿔주자.
<title><spring:message code="member.register" /></title>
스프링은 로케일(지역)에 상관없이 일관된 방법으로 문자열(메시지)을 관리할 수 있는 MessageSource 인터페이스를 정의하고 있다. 특정 로케일에 해당하는 메시지가 필요한 코드는 MessageSource의 getMessage() 메서드를 이용해서 필요한 메시지를 가져와 사용하는 식이다.
package org.springframework.context;
import java.util.Locale;
public interface MessageSource {
String getMessage(String code, Object[] args, String defaultMessage,
Locale locale);
String getMessage(String code, Object[] args, Locale locale)
throws NoSuchMessageException;
... // 일부 메서드 생략
}
getMessage() 메서드의
MessageSource의 구현체로는 자바의 프로퍼티 파일로부터 메시지를 읽어오는 ResourceBundleMessageSource 클래스를 사용한다. 이 클래스는 메시지 코드와 일치하는 이름을 가진 프로퍼티 값을 메시지로 제공한다. 또한, 해당 프로퍼티 파일이 클래스 패스에 위치해야 한다.
따라서, <spring:message> 태그를 실행하면 내부적으로 MessageSource의 getMessage() 메서드를 실행해서 필요한 메시지를 구한다.
<spring:message> 태그의 code 속성에 지정한 메시지가 존재하지 않으면 500 에러가 발생한다.
앞서 작성한 label.properties 파일을 보면 다음과 같은 프로퍼티를 포함하고 있다.
register.done=<strong>{0}님</strong>, 회원 가입을 완료했습니다.
이 프로퍼티는 값 부분에 {0}을 포함하는데, 이는 인덱스 기반 변수 중 0번 인덱스의 값으로 대치되는 부분이다.
MessageSource의 getMessage()는 인덱스 기반 변수를 전달하기 위해 Object 배열 타입의 파라미터를 사용한다.
Object[] args = new Object[1];
args[0] = "자바";
messageSource.getMessage("register.done", args, Locale.KOREA);
<spring:message> 태그를 사용할 때는 arguments 속성을 사용하여 인덱스 기반 변수값을 전달한다.
Ex.
<spring:message code="register.done" arguments="${registerRequest.name}" />
다음 방법 중 하나를 사용하자.
<spring:message code="register.done" arguments="${registerRequest.name},${registerRequest.email}" />
<spring:message code="register.done">
<spring:argument value="${registerRequest.name}" />
<spring:argument value="${registerRequest.email}" />
<spring:message>
위에서 열심히 폼을 만들고 잘못 입력했을 때의 화면을 짠 들, 실제 입력한 값에 대한 검증 처리를 하지 않으면 비정상적인 값을 입력해도 동작해버리는 문제가 발생한다.
또한, (예를 들어) 회원가입 폼에서 중복된 이메일 주소를 입력해 다시 폼을 보여줄 때 왜 실패했는지 이유를 알려주지 않는다면 사용자는 혼란을 겪게 될 것이다.
위에서 언급한 두 가지 문제, 폼 값 검증과 에러 메시지 처리는 어플리케이션을 개발할 때 놓쳐서는 안될 중요한 문제이다.
따라서 스프링에서는이 두 가지 문제를 처리하기 위해 다음 방법을 제공한다.
스프링 MVC에서 커맨드 객체의 값이 올바른지 검사하려면 다음의 두 인터페이스를 사용한다.
객체를 검증할 때 사용하는 Validator 인터페이스는 다음과 같다.
package org.springframework.validation
public interface Validator {
boolean supports(Class<?> clazz);
void validate(Object target, Errors errors);
}
- 에러 코드에 해당하는 메시지가 인덱스 기반 변수를 포함하는 경우 Object 배열 타입의 errorArgs 파라미터를 이용해서 변수에 삽입될 값을 전달한다.
- defaultMessage 파라미터를 가진 메서드를 사용하면 에러 코드에 해당하는 메시지가 존재하지 않을 때, 익셉션을 발생시키는 대신 defaultMessage를 출력한다.
- rejectIfEmpty() : (field에 해당하는 프로퍼티 값이) null이거나 빈 문자열("")인 경우 에러 코드로 errorCode를 추가한다.
- rejectIfEmptyOrWhiteSpace() : (field에 해당하는 프로퍼티 값이) null이거나 빈 문자열인 경우, 그리고 공백 문자(스페이스, 탭 등)로만 값이 구성된 경우 에러 코드 추가
에러 코드를 지정한 이유는 알맞은 에러 메시지를 출력하기 위함이다.
Errors에 에러 코드를 추가하면 JSP는 스프링이 제공하는 <form:errors> 태그를 사용해서 에러에 해당하는 메시지를 출력할 수 있다.
🎈 <form:errors> 태그의 path 속성: 에러 메시지를 출력할 프로퍼티 이름 지정
스프링 MVC는
를 설정하는 방법을 제공한다.
이를 사용하면 @Valid 애노테이션을 사용해서 커맨드 객체에 검증 기능을 적용할 수 있다.
글로벌 범위 Validator는 모든 컨트롤러에 적용할 수 있는 Validator이다. 글로벌 범위 Validator를 적용하려면 다음 두 가지를 설정하면 된다.
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
return new RegisterRequestValidation();
}
...
}
스프링 MVC는 getValidator() 메서드가 리턴한 객체를 글로벌 범위 Validator로 사용한다. 이렇게 글로벌 범위 Validator를 지정하면 @Valid 애노테이션을 사용해서 Validator를 적용할 수 있다.
위와 같이 글로벌 범위 Validator를 지정하고 나면 @Valid 애노테이션을 사용해서 Validator를 적용할 수 있다.
Ex.
import javax.validation.Valid;
...
@Controller
public class RegisterController {
...
@PostMapping("/register/step3")
public String handleStep3(@Valid RegisterRequest regReq, Errors errors) {
...
}
...
}
이렇게 커맨드 객체에 해당하는 파라미터에 @Valid 애노테이션을 붙이면 글로벌 범위 Validator가 해당 타입을 검증할 수 있는지 확인한다. 이때 검증 수행 결과는 Errors 타입 파라미터로 받는다.
❗ @Valid 애노테이션 사용시 주의할 점
Errors 타입 파라미터가 없으면 검증 실패 시 400 에러를 응답한다.
@InitBinder 애노테이션을 이용하면 컨트롤러 범위 Validator를 설정할 수 있다.
Ex.
@Controller
public class RegisterController {
...
@PostMapping("/register/step3")
public String handleStep3(@Valid RegisterRequest regReq, Errors errors) {
...
}
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new RegisterRequestValidator());
}
}
위 코드를 보면 커맨드 객체 파라미터에 @Valid 애노테이션을 적용하고 있다. 글로벌 범위 Validator를 사용할 때와 마찬가지로 handleStep3() 메서드에는 Validator 객체의 validate() 메서드를 호출하는 코드가 없다.
어떤 Validator가 커맨드 객체를 검증할지는 initBinder() 메서드가 결정한다.
@InitBinder 애노테이션을 적용한 메서드는 WebDataBinder 타입 파라미터를 갖는데, WebDataBinder.setValidator() 메서드를 이용해서 컨트롤러 범위에 적용할 Validator를 설정할 수 있다.
@InitBinder가 붙은 메서드는 컨트롤러의 요청 처리 메서드를 실행하기 전에 매번 실행된다.
Bean Validation 스펙에 @Valid, @NotNull, @Digits, @Size 등의 애노테이션이 정의되어 있다. 이 애노테이션들을 사용하면 Validator 작성 없이 애노테이션만으로 커맨드 객체의 값 검증을 처리할 수 있다.
❗ 글로벌 범위 Validator를 따로 설정했다면 해당 설정은 삭제하자.
스프링 MVC는 별도로 설정한 글로벌 범위 Validator가 없을 때, OptionalValidatorFactoryBean을 글로벌 범위 Validator로 사용한다.