728x90
사용자 정의 애너테이션 : 이메일 중복 확인
- 오류 메세지 추가
- 사용자정의애너테이션이름.커맨드객체이름.필드이름 = 오류메세지
MemberValid.member.email = \uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uC774\uBA54\uC77C \uC8FC\uC18C\uC785\uB2C8\uB2E4.
- 사용자 정의 애너테이션 생성
- @interface를 만듦
- message, groups(), payload() 속성이 필수적이다.
- message : 오류 발생 시, 반환되는 기본 메세지
- groups : 특정 유효성 검사를 그룹으로 설정한다.
- payload : 사용자가 추가한 정보를 전달한다.
- @Retention(RetentionPolicy.속성값)
- Runtime : 런타임 할 때도 .class 파일에 유지. 주로 사용됨.
- @Target : 필드, 메서드, 클래스 등 타겟 애너테이션을 작성
- message, groups(), payload() 속성이 필수적이다.
@Constraint(validatedBy = MemberDTOEmailValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MemberValid {
String message() default "";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
}
- 객체에 제약 사항 애너테이션 선언
import kr.co.chunjae.validator.MemberValid;
import javax.validation.constraints.*;
import org.hibernate.validator.constraints.NotEmpty;
public class MemberDTO {
@MemberValid // 사용자 정의 애너테이션
@Pattern(regexp = "^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", message = "{Pattern.member.email}")
@NotEmpty
private String email;
}
- 사용자 정의 애너테이션은 JSR-380과 같이 사용 가능하다. 둘다 자바에서 지원하는 유효성 검사이기 때문이다.
- 이때, String 변수의 @NotEmpty는 hibernate 것을 사용한다. > 오류가 났음…
- ConstraintValidator 인터페이스 구현체 생성
- javax.validation.ConstraintValidator 인터페이스 구현
- initialize(), isValid() 메서드를 구현해야 한다.
- initialize() : 사용자 정의 애너테이션 정보를 읽음
@Component
@Log4j
public class MemberDTOEmailValidator implements ConstraintValidator<MemberValid, String> {
@Autowired
private MemberService memberService;
public void initialize(MemberValid constraintAnnotation){ //@MemberValid 정보 초기화 메서드
}
// 유효성 검사 메서드
public boolean isValid(String value, ConstraintValidatorContext context){
boolean result = true;
try{
result = memberService.searchEmail(value);
}catch (MemberDTOEmailException e){
return true;
}
if(result){ // value값을 가진 이메일이 이미 존재한다
return false;
}else{
return true;
}
}
}
- ConstraintValidator<사용자 정의 애너테이션 이름, 도메인 클래스의 멤버 변수 타입>
- isValid(도메인 클래스의 멤버 변수 타입, ConstraintValidatorContext)
- 컨트롤러의 요청 처리 메서드의 파라미터에 @Valid
- 뷰 폼 페이지에서 form:errors로 오류 메세지 출력
Validator 인터페이스 유효성 검사
- Validator 인터페이스의 구현체 생성
- @InitBinder를 선언한 메서드를 컨트롤러에 추가
- @Valid로 유효성 검사
- form:errors 태그로 오류 메세지 출력
- 도메인 객체에 JSR-380 애너테이션이 존재하면 안 된다.
package kr.co.chunjae.domain;
import kr.co.chunjae.validator.MemberValid;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.*;
import org.hibernate.validator.constraints.NotEmpty;
@Getter
@Setter
@ToString
public class MemberDTO {
/*@MemberValid // 사용자 정의 애너테이션
@Pattern(regexp = "^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", message = "{Pattern.member.email}")
@NotEmpty
private String email;
@Size(min=1, max=50, message = "1자 이상 50자 이하를 입력해 주세요.")
private String password;
@Size(min=1, max=30)
private String name;
@Min(value=1)
private int age;
@Size(max=30)
private String mobile;*/
private String email;
private String password;
private String name;
private int age;
private String mobile;
}
- Validator 인터페이스 구현
- supports(Class<?> clazz) : 해당 객체에 유효성 검사를 시행할 수 있는지 확인
- validate(Object, Errors) : 해당 객체에 유효성 검사를 실행하고 오류 발생 시, 오류 관련 정보를 Errors 객체에 저장.
- errors.rejectValue(”커맨드 객체의 필드명”, “메세지”)
package kr.co.chunjae.validator;
@Component
@RequiredArgsConstructor
public class MemberDTOEmailValidator implements Validator {
private final MemberService memberService;
@Override
public boolean supports(Class<?> clazz) {
return MemberDTO.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
MemberDTO memberDTO = (MemberDTO) target;
// 이메일 존재 여부 확인
if (memberService.searchEmail(memberDTO.getEmail())) { // 이메일이 이미 존재한다
errors.rejectValue("email", "email.not.available");
}
}
}
- message 리소스 파일
email.not.available = \uC774\uBBF8 \uC874\uC7AC\uD558\uB294 \uC774\uBA54\uC77C \uC8FC\uC18C\uC785\uB2C8\uB2E4.
- 컨트롤러에 @InitBinder로 validator를 추가한다.
@AutoWired
private MemberDTOValidator memberDTOValidator;
@InitBinder
public void initBinder(WebDataBinder binder){
binder.setValidator(memberDTOValidator);
}
- 컨트롤러 요청 처리 메서드의 파라미터 앞에 @Valid
@PostMapping("/save")
public String save(@Valid @ModelAttribute("member") MemberDTO memberDTO, Errors errors, Model model){
// 유효성 검사 오류 결과 확인
if(errors.hasErrors()){
return "save";
}
}
- 모델 맵핑 전에 @Valid로 유효성 검사 실행됨.
- Errors 객체에 오류 관련 정보가 저장됨.
- 폼 페이지에 form:errors로 오류 메세지 출력
<p>아이디 : <form:input path="email" name="email" /> <form:errors path="email"/></p>
=> 이렇게하면 Validator 인터페이스 구현 객체의 유효성만 검사되고 JSR-380 애너테이션 유효성 검사는 무시된다.
JSR-380과 Validator 인터페이스 동시에 사용하기
- 도메인 객체 JSR-380 애너테이션 선언
import kr.co.chunjae.validator.MemberValid;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.*;
import org.hibernate.validator.constraints.NotEmpty;
@Getter
@Setter
@ToString
public class MemberDTO {
@MemberValid // 사용자 정의 애너테이션 - 이메일 중복 검사
@Pattern(regexp = "^[a-zA-Z0-9+-_.]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$", message = "{Pattern.member.email}")
@NotEmpty
private String email;
@Size(min=1, max=50, message = "1자 이상 50자 이하를 입력해 주세요.")
private String password;
@Size(min=1, max=30)
private String name;
@Min(value=1)
private int age;
@Size(max=30)
private String mobile;
}
- 사용자 정의 애너테이션 생성 : 이메일 중복 검사
- @interface 생성
- ConstraintsValidator 구현
- initialize()
- isValid()
- @interface
import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
// 이메일 중복 검사 사용자 애너테이션
@Constraint(validatedBy = MemberDTOEmailValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MemberValid {
String message() default "{MemberValid.member.email}";
Class<?>[] groups() default {};
Class<?>[] payload() default {};
}
- ConstraintsValidator 인터페이스 구현
- @interface의 @Constraint(validatedBy = MemberDTOEmailValidator.class)
import kr.co.chunjae.exception.MemberDTOEmailException;
import kr.co.chunjae.service.MemberService;
import lombok.extern.log4j.Log4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
@Component
@Log4j
public class MemberDTOEmailValidator implements ConstraintValidator<MemberValid, String> {
@Autowired
private MemberService memberService;
public void initialize(MemberValid constraintAnnotation){ //@MemberValid 정보 초기화 메서드
}
// 유효성 검사 메서드
public boolean isValid(String value, ConstraintValidatorContext context){
boolean result = true;
try{
result = memberService.searchEmail(value);
}catch (MemberDTOEmailException e){
return true;
}
if(result){ // value값을 가진 이메일이 이미 존재한다
return false;
}else{
return true;
}
}
}
- @Validator 인터페이스 구현 객체 생성 : 전화번호 패턴 확인
- supports(), validate() 메서드 구현
@Component
@Log4j
public class MemberDTOMobileValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return MemberDTO.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
MemberDTO memberDTO = (MemberDTO) target;
String mobile = memberDTO.getMobile();
// 전화번호 형식 확인
String pattern = "^\\d{2,3}-\\d{3,4}-\\d{4}$";
if(mobile == null || !Pattern.matches(pattern, mobile)){
errors.rejectValue("mobile", "mobile.not.available");
}
}
}
- 원래 이 구현 객체를 컨트롤러의 @InitBinder에서 WebDataBinder.setValidator()로 추가해주어야 하지만, jsr-380과 validator 동시 사용하게 만드는 validator 구현 객체를 생성하고 해당 validator 구현 객체를 넣어줄 것이다.
- @Validator 구현 객체 생성 : JSR-380, @Validator 둘 다 사용하게 만드는 객체
- beanValidator, springValidator를 인스턴스(멤버변수)로 가진다.
- beanValidator Set에서 하나씩 꺼내서 유효성 검사를 한다. 오류 발생 시, errors 객체에 errors.rejectValue()로 저장한다.
- springValidator Set에서도 하나씩 꺼내서 validate()로 유효성 검사를 한다.
import kr.co.chunjae.domain.MemberDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import javax.validation.ConstraintViolation;
import java.util.HashSet;
import java.util.Set;
// jsr-380 + spring validator
@Component
public class MemberDTOValidator implements Validator {
// jsr-380 연동
@Autowired
private javax.validation.Validator beanValidator;
// spring validator
private Set<Validator> springValidators;
// 생성자
public MemberDTOValidator(){
springValidators = new HashSet<>();
}
public void setSpringValidators(Set<Validator> springValidators){
this.springValidators = springValidators;
}
@Override
public boolean supports(Class<?> clazz) {
return MemberDTO.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
MemberDTO memberDTO = (MemberDTO) target;
// jsr-380 beanValidator
Set<ConstraintViolation<Object>> violations = beanValidator.validate(target);
for(ConstraintViolation<Object> violation : violations){
// 오류 발생 필드 저장
String propertyPath = violation.getPropertyPath().toString();
String message = violation.getMessage(); // 오류 발생 메세지 저장
// 오류 발생된 필드와 메세지를 Errors 객체에 저장
errors.rejectValue(propertyPath, "", message);
}
// spring validator
for(Validator validator: springValidators){
validator.validate(target, errors); // 발생된 오류 정보를 전달
}
}
}
정리
- jsr-380 : errors.rejectValue()로 오류 메세지 추가
- validator : validator.validate()로 오류 메세지 추가
- 컨트롤러의 @InitBinder 선언한 메서드에서 WebDataBinder.addValidator()로 방금 구현한 객체를 validator로 넣어준다.
@Autowired
private MemberDTOValidator memberDTOValidator;
// jsr-380 + spring validator
@InitBinder
public void initBinder(WebDataBinder binder){
binder.setValidator(memberDTOValidator);
}
- 컨트롤러의 요청 처리 메서드의 파라미터 앞에 @Valid 확인, Errors 객체 확인.
- servlet-context.xml에 빈 추가
- annotation-driven에 validator 추가
<annotation-driven enable-matrix-variables="true" validator="validator" />
- 뷰 페이지의 <form:errors path=”필드명” />로 오류 메세지 출력
- validator 구현 객체를 빈으로 등록
<!-- JSR-380 유효성 검사를 위해 LocalValidatorFactoryBean을 등록 -->
<beans:bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<beans:property name="validationMessageSource" ref="messageSource"/>
</beans:bean>
<!-- validator 구현 객체 -->
<beans:bean id="memberDTOMobileValidator" class="kr.co.chunjae.validator.MemberDTOMobileValidator"/>
<beans:bean id="memberDTOValidator" class="kr.co.chunjae.validator.MemberDTOValidator">
<beans:property name="springValidators">
<beans:set>
<beans:ref bean="memberDTOMobileValidator"/>
</beans:set>
</beans:property>
</beans:bean>
+선택적 유효성 검사
- 회원정보 수정 시, 이메일은 수정하지 못하게 readonly로 되어 있는데 바인딩 될 때, 유효성 검사가 수행되어 이미 존재하는 이메일이라 수정이 되지 않았다.
- 따라서, 수정 가능한 mobile, age만 유효성 검사 에러가 뜨면 페이지에 에러 메세지가 출력되고, 다른 필드의 유효성 검사는 무시하는 것을 구현하고 싶었다.
⇒ errors.hasFieldErrors("age")
를 사용하자!
- update.jsp
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>update</title>
</head>
<body>
<%--@elvariable id="member" type="kr.co.chunjae.domain.MemberDTO"--%>
<form:form modelAttribute="member" action="/member/update" method="post" name="updateForm">
<p>id: <form:input path="id" name="id" value="${member.id}" readonly="true" /> </p>
<p>email: <form:input path="email" name="email" value="${member.email}" readonly="true"/> </p>
<p>password: <form:password path="password" name="password"/> <form:errors path="password"/> </p>
<p>name: <form:input path="name" name="name" value="${member.name}" readonly="true"/> </p>
<p>age: <form:input path="age" name="age" value="${member.age}"/> <form:errors path="age"/> </p>
<p>mobile: <form:input path="mobile" name="mobile" value="${member.mobile}"/> <form:errors path="mobile"/> </p>
<input type="button" value="수정" onclick="update()">
</form:form>
</body>
<script>
// 기존 비밀번호와 일치했을 경우에만 회원정보 수정 가능
const update = () => {
const passwordDB = '${member.password}';
const password = document.getElementById("password").value;
if (passwordDB == password) {
document.updateForm.submit();
} else {
alert("비밀번호가 일치하지 않습니다!");
}
}
</script>
</html>
- controller
@Controller
@Log4j
@RequiredArgsConstructor
@RequestMapping("/member")
public class MemberController {
@PostMapping("/update")
public String update(@Valid @ModelAttribute("member") MemberDTO memberDTO, BindingResult errors){
log.info(errors); // 다른 필드의 오류가 잡힌다.
if(errors.hasFieldErrors("age") || errors.hasFieldErrors("mobile")){
return "update";
}
boolean result = memberService.update(memberDTO);
if(result) {
return "redirect:/member?id="+memberDTO.getId();
}else{
return "update";
}
}
}
- log.info(errors); 에서 이메일 중복 유효성 검사를 통과하지 못한 것을 알 수 있다.
- 하지만 errors.hasFieldErrors("age")를 통해 age 필드 에러만 있을 때, 다시 수정 페이지로 이동하게 하였다.
정리 !
유효성 검사를 하는 방법에는 두 가지가 있다.
JSR-380을 사용하기
1-1. 정해진 애너테이션
1-2. 사용자 지정 애너테이션 생성
spring 유효성 검사인 Validator 인터페이스를 구현하기
JSR-380 + Validator 동시에 사용하기
1-1. JSR-380 기본 애너테이션 사용
- 도메인 객체의 필드 위에 알맞은 애너테이션을 선언한다.
- 메세지 리소스 파일 혹은 애너테이션 안에 message 속성값에 오류 메세지를 설정한다.
- 컨트롤러의 요청 처리 메서드의 파라미터 앞에 @Valid 선언하여 유효성 검사 시행
- 뷰 페이지의 <form:errors path=”필드”/>로 에러 메세지 출력
1-2. 사용자 지정 애너테이션 사용
- 도메인 객체의 필드 위에 사용자 지정 애너테이션을 선언한다.
- @interface 객체로 사용자 지정 애너테이션을 생성한다.
- message, groups(), payload()
- ConstraintValidator 인터페이스 구현 객체를 생성한다.
- initialize(), isValid() 메서드 구현
- 컨트롤러의 요청 처리 메서드의 파라미터 앞에 @Valid
- 뷰 페이지의 <form:errors />로 오류 메세지 출력
- Validator 인터페이스 구현
- implements Validator 로 구현 객체를 만든다.
- supports() : 해당 객체의 유효성 검사를 진행할 수 있는지 판단
- validate() : 유효성 검사
- 컨트롤러에 @InitBinder 선언 메서드를 생성한다.
- WebDataBinder.addValidator(구현객체)로 validator 추가한다.
- JSR-380 + Validator 동시 사용
- 기본 애너테이션, 사용자 지정 애너테이션, Validator 인터페이스 구현까지 동일하게 수행
- Validator 구현 객체를 하나 더 생성한다.
- jsr-380 유효성 검사인 javax.validation.Validator 변수 인스턴스 추가
- Set<org.springframework.validation.Validator> 변수 인스턴스 추가
- for문으로 validate() 실행하며 유효성 검사 수행
- servlet-context.xml에 validator 구현 객체, LocalValidatorFactoryBean 빈 등록
= 깃허브에서 전체 코드 확인하기 =
https://github.com/thegreatjy/ChunjaeFullStack/tree/main/Spring_Study/memberFrameWorkPractice
728x90
'공부 > Spring' 카테고리의 다른 글
[Spring] 게시판 연습 웹 프로젝트 / 간단한 페이징, ajax 댓글 등록 (2) | 2024.01.04 |
---|---|
[Spring] 회원관리 연습 웹 프로젝트 - 1 / 스프링 폼, 스프링 유효성 @Valid (1) | 2023.12.30 |
[Spring] 스프링 환경설정 xml 파일(web, root-context, servlet-context) (0) | 2023.12.29 |