추후에 다른 groups를 추가할 경우를 고려하여 유지보수하기 편하게 ValidationGroups 클래스에 인터페이스 선언
public class ValidationGroups {
public interface NotBlankGroup{}
public interface PatternGroup{}
}
groups명은 자유롭게 정하면 된다.
NotBlankGroup
: NotBlank 애노테이션에 적용할 groups 생성PatternGroup
: Pattern 애노테이션에 적용할 groups 생성@GroupSequence({NotBlankGroup.class, PatternGroup.class})
public interface ValidationSequence {
}
Default.class를 제일 앞에 넣을 경우 제대로 동작하지 않아 제거
@Getter
@Setter
@AllArgsConstructor
public class MemberSaveForm {
@NotBlank(message = "아이디를 입력해주세요.", groups = NotBlankGroup.class)
@Pattern(regexp = "^(?=.*[a-zA-z])(?=.*\\d)[a-zA-Z\\d]{6,16}+$", message = "6자 이상 16자 이하의 영문 혹은 영문과 숫자를 조합", groups = PatternGroup.class)
private String loginId;
@NotBlank(message = "비밀번호를 입력해주세요.", groups = NotBlankGroup.class)
@Pattern(regexp = "^(?=.*[a-zA-z])(?=.*\\d)(?=.*\\W)[a-zA-Z\\d\\W]{10,16}$", message = "10자 이상 16자 이하의 영문, 숫자, 특수문자 조합", groups = PatternGroup.class)
private String password;
@NotBlank(message = "비밀번호 확인을 입력해주세요.", groups = NotBlankGroup.class)
@Pattern(regexp = "^(?=.*[a-zA-z])(?=.*\\d)(?=.*\\W)[a-zA-Z\\d\\W]{10,16}$", message = "10자 이상 16자 이하의 영문, 숫자, 특수문자 조합", groups = PatternGroup.class)
private String passwordConfirm;
}
groups = "인터페이스명"
@Controller
@RequiredArgsConstructor
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
@GetMapping("/join")
public String addMemberForm(@ModelAttribute("form") MemberSaveForm form) {
return "members/join";
}
@PostMapping("/join")
public String join(@Validated(ValidationSequence.class) @ModelAttribute("form") MemberSaveForm form, BindingResult bindingResult) {
//복합 룰 검증
if(form.getPassword() != null && form.getPasswordConfirm() != null) {
if (!form.getPassword().equals(form.getPasswordConfirm())) {
bindingResult.reject("passwordNotEqual", "동일한 비밀번호를 입력해주세요.");
}
}
if(bindingResult.hasErrors()) {
return "members/join";
}
memberService.join(new MemberSaveDto(form));
return "redirect:/members/join/complete";
}
}
@Valid
에는 groups 기능이 없으므로 groups를 사용하려면 @Validated
를 사용해야 한다. @Transactional
@SpringBootTest
@AutoConfigureMockMvc
class MemberServiceTest {
@Autowired
MockMvc mockMvc;
@Autowired
MemberService memberService;
@Test
@DisplayName("회원가입 성공")
public void join() throws Exception {
//given
MemberSaveForm form = new MemberSaveForm("test1234", "test1234!@", "test1234!@");
//when
mockMvc.perform(MockMvcRequestBuilders
.post("/members/join")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.flashAttr("form", form))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/members/join/complete"));
}
@Test
@DisplayName("회원가입 실패: loginId_empty")
public void joinX_loginId2() throws Exception {
//given
MemberSaveForm form = new MemberSaveForm("", "test1234!@", "test1234!@");
//when
mockMvc.perform(MockMvcRequestBuilders
.post("/members/join")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.flashAttr("form", form))
.andExpect(model().errorCount(1))
.andExpect(model().attributeHasFieldErrors("form", "loginId"))
.andExpect(model().attributeHasFieldErrorCode("form", "loginId", "NotBlank"));
}
@Test
@DisplayName("회원가입 실패: password_일치X")
public void joinX_password_confirm() throws Exception {
//given
MemberSaveForm form = new MemberSaveForm("test1234", "test1234!@", "test1234!#", "테스터", "010-1234-5678", "test@gmail.com", true, true, true, false);
//when
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders
.post("/members/join")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.flashAttr("form", form))
.andExpect(model().errorCount(1))
.andExpect(model().attributeHasErrors("form"))
.andReturn();
BeanPropertyBindingResult bindingResult = (BeanPropertyBindingResult) mvcResult.getModelAndView().getModelMap().getAttribute("org.springframework.validation.BindingResult.form");
ObjectError globalError = bindingResult.getGlobalError();
String defaultMessage = globalError.getDefaultMessage();
//then
Assertions.assertThat(defaultMessage).isEqualTo("동일한 비밀번호를 입력해주세요.");
}
}
post("/members/join")
: "/members/join"
URI로 POST 요청flashAttr("form", form)
: key-value 형태로 담기며 @ModelAttribute("form") MemberSaveForm form
파라미터와 매핑된다. flashAttr의 key이름과 @ModelAttribute의 name이 동일해야 한다. andExpect()
를 통해 응답결과를 검증한다.return "redirect:/members/join/complete";
리다이렉트 시키는 것을 확인할 수 있다. errorCount(1)
: 에러 개수attributeHasFieldErrors("form", "loginId")
: 필드 에러 유무 확인attributeHasFieldErrorCode("form", "loginId", "NotBlank")
: 필드 에러 코드 확인loginId=""
인 경우에 검증 우선 순위가 적용되지 않았더라면 NotBlank와 Pattern에서 모두 오류가 발생하기 때문에 errorCount 개수가 2개여야 한다. 하지만 현재 NotBlank -> Pattern 순서로 검증 우선 순위가 적용돼있어서 NotBlank 검증이 먼저 이루어지고 여기서 오류가 발생해 다음 순서인 Pattern 검증이 이루어지지 않고 반환된다. andReturn()
: 요청에 대한 응답결과가 MvcResult 타입으로 반환된다.mvcResult.getModelAndView().getModelMap().getAttribute("org.springframework.validation.BindingResult.form")
: ObjectError 객체를 얻기 위해 디버깅을 통해 위치와 반환값을 확인해보았다.mvcResult > modelAndView > model > "org.springframework.validation.BindingResult.form"
BeanPropertyBindingResult
bindingResult.getGlobalError()
: ObjectError 객체