Spring Example: ToDo List #5 validtation 개발

함형주·2022년 9월 29일
0

Spring Example: ToDo

목록 보기
6/16

질문, 피드백 등 모든 댓글 환영합니다.

지난시간에 개발한 Controller에 세부적인 검증 로직을 개발하고 에러 메시지를 추가하겠습니다.

검증 요구사항

회원가입

  • 로그인 아이디와 비밀번호는 5~20자 사이의 영문소문자와 숫자로 이루어 짐, 공백 허용 x
  • 로그인 아이디는 기존 회원과 중복 불가
  • 이름은 2~20자 사이의 영문 소문자, 숫자, 한글 가능, 공백은 허용하되 앞뒤의 공백은 제거됨
  • 검증 오류시 비밀번호를 제외하고 기존의 값을 유지한 채 에러메시지를 표시

로그인

  • 로그인 아이디와 비밀번호는 비어있을 수 없음
  • 검증 오류가 발생하거나 로그인 아이디와 비밀번호가 맞지 않을 시 비밀번호를 제외하고 기존의 값을 유지한 채 에러메시지를 표시

ToDo 생성 및 수정

  • 제목은 비어있을 수 없으며 최대 20자까지 가능
  • 설명은 최대 100자까지 가능
  • 마감일은 오늘 이전의 날짜는 선택 불가능
  • 검증 오류시 기존의 값을 유지한 채 에러메시지를 표시

Annotation

@Length : 해당 필드의 값의 길이 지정
@NotEmpty : null, 공백("") 허용 x
@FutureOrPresent : Date 등의 타입에서 현재, 미래만 가능
@Validated : 이 어노테이션이 붙은 필드를 검증

th:field

th:field는 렌더링 후 html의 id, name는 field의 식별자로, value는 그 값으로 자동으로 생성합니다.
또한 에러가 발생하여 다시 html이 렌더링 될 때 해당 필드의 값을 유지하여 보여주는 기능을 가지고 있습니다.

회원가입 검증

MemberDto

@Getter @Setter
public class MemberDto {
    @Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
    private String loginId;
    @Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
    private String password;
    private String checkPassword;
    @Length(max = 20, min = 2, message = "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.")
    private String name;
}

HomeController

class HomeController {

	@PostMapping("/add")
    public String save(@Validated @ModelAttribute MemberDto memberDto, BindingResult bindingResult) {

        if (!bindingResult.hasFieldErrors("name") && !validateKorean(memberDto.getName()))
            bindingResult.rejectValue("name", "name", "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.");

        if (!bindingResult.hasFieldErrors("loginId") && !validateString(memberDto.getLoginId()))
            bindingResult.rejectValue("loginId", "loginId", "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.");

        if (!bindingResult.hasFieldErrors("password")) {
            if (!validateString(memberDto.getPassword()))
                bindingResult.rejectValue("password", "password", "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.");
            else if (!memberDto.getPassword().equals(memberDto.getCheckPassword()))
                bindingResult.rejectValue("checkPassword", "", "비밀번호가 일치하지 않습니다.");

        }

        if (bindingResult.hasErrors()) return "/member/add";

        Member member = new Member(memberDto.getLoginId(), memberDto.getPassword(), memberDto.getName().strip());
        if (memberService.save(member) == null) {
            bindingResult.rejectValue("loginId", "duplication", "이미 존재하는 ID 입니다.");
            return "/member/add";
        }
        return "redirect:/";

    }
    
    private boolean validateString(String str) {
        String temp = "";
        for (int i = 0; i < str.length(); i++) {
            if (!String.valueOf(str.charAt(i)).matches("[^a-z0-9]")) { // 영소문자나 숫자면
                temp += str.charAt(i);
            }
        }
        return str.equals(temp) ? true : false;
    }

    private boolean validateKorean(String str) {
        String temp = "";
        for (int i = 0; i < str.length(); i++) {
            if (!String.valueOf(str.charAt(i)).matches("[^ㄱ-ㅎㅏ-ㅣ가-힣a-z0-9\\s]")) { //소문자거나 한글이면 공백이면
                temp += str.charAt(i);
            }
        }
        return str.equals(temp) ? true : false;
    }

@Length에서 값 길이를 검증하고 validateString(), validateKorean()에서 정규식을 활용해 특수문자 여부 등 확인 정규식은 이 블로그를 참고했습니다.
에러 메시지가 중복되어 생성되지 않도록 이미 생성된 에러가 없을 경우에 이후 로직 적용하는 방식으로 개발
validation 어노테이션으로 1차 검증 후 로그인 Id 중복 검사

BindingResult의 void rejectValue()를 이용해 필드 오류를 생성하고 view로 전달

rejectValue(@Nullable String field, String errorCode)
rejectValue(@Nullable String field, String errorCode, String defaultMessage)
rejectValue(@Nullable String field, String errorCode, @Nullable Object[] errorArgs, @Nullable Strign defaultMessage)

add.html

    <form th:object="${memberDto}" method="post">
        <div class="d-grid gap-2 col-6 mx-auto">
            <label for="name" class="form-label">이름</label>
            <input type="text" id="name" class="w-100 form-control"
                   placeholder="2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다." th:field="*{name}">
            <div th:errors="*{name}"></div> <!--추가-->

            <label for="loginId" class="form-label">로그인 id</label>
            <input type="text" id="loginId" class="w-100 form-control"
                   placeholder="5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다." th:field="*{loginId}">
            <div th:errors="*{loginId}"></div> <!--추가-->

            <label for="password" class="form-label">비밀번호</label>
            <input type="password" id="password" class="w-100 form-control"
                   placeholder="5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다." th:field="*{password}">
            <div th:errors="*{password}"></div> <!--추가-->

            <label for="checkPassword" class="form-label">비밀번호 확인</label>
            <input type="password" id="checkPassword" class="w-100 form-control" th:field="*{checkPassword}">
            <div th:errors="*{checkPassword}"></div> <!--추가-->
                  </div>
      
    </form>

html에 타임리프 에러 메시지 추가
th:error는 해당 필드에 에러가 존재하면 에러 메시지를 찾아 출력

로그인 검증

LoginDto

public class LoginDto {
	@NotEmpty
    private String loginId;
    @NotEmpty
    private String password;
}

@NotEmpty로 1차 검증 후 로그인 Id와 비밀번호 일치 여부 검증

LoginController

class LoginController {

    @PostMapping("/login")
    public String login(@Validated @ModelAttribute LoginDto loginDto, BindingResult bindingResult, HttpServletRequest request) {

        if (bindingResult.hasErrors())
            return "/login/form";
        
        Optional<Member> loginMember = loginService.login(loginDto.getLoginId(), loginDto.getPassword());
        
        if (loginMember.isEmpty()) {
            bindingResult.reject("loginFail", "id password 에러");
            return "/login/form";
        }

        HttpSession session = request.getSession();
        session.setAttribute("loginMember", loginMember.get());

        return "redirect:/todo";
    }
}

BindingResult의 void reject()로 글로벌 에러 생성, view로 전달

reject(String errorCode)
reject(String errorCode, String defaultMessage)
reject(String errorCode, @Nullable Object[] errorArgs, @Nullable Strign defaultMessage)

form.html

    <form th:object="${loginDto}" method="post">
        <div th:if="${#fields.hasGlobalErrors()}"> <!--추가-->
            <p>id 혹은 비밀번호가 정확하지 않습니다.</p> <!--추가-->
        </div> <!--추가-->

        <div class="d-grid gap-2 col-6 mx-auto">
            <label for="loginId" class="form-label">로그인 ID</label>
            <input type="text" id="loginId" class="w-100 form-control" th:field="*{loginId}">
            <div th:errors="*{loginId}"></div> <!--추가-->

            <label for="password" class="form-label">비밀번호</label>
            <input type="password" id="password" class="w-100 form-control" th:field="*{password}">
            <div th:errors="*{password}"></div> <!--추가-->

#fields.hasGlobalErrors()로 글로벌 에러 여부 확인 가능

ToDo 검증

ToDoDto

public class ToDoDto {

    private Long id;
    @NotEmpty
    private String title;
    @Length(max = 100, message = "최대 100자까지 가능합니다")
    private String description;
    private Boolean isCompleted = false;
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate createdDate;
    @DateTimeFormat(pattern = "yyyy-MM-dd") @FutureOrPresent(message = "오늘 이전의 날짜는 선택할 수 없습니다.")
    private LocalDate dueDate;
    
}

ToDoController

class ToDoController { 

    @PostMapping("/todo/add")
    public String addToDo(@Validated @ModelAttribute("toDoDto") ToDoDto toDoDto, BindingResult bindingResult, HttpServletRequest request) {

        if (!bindingResult.hasErrors() && toDoDto.getTitle().length() >= 20)
            bindingResult.rejectValue("title", "length", "최대 20자까지 가능합니다.");

        if (bindingResult.hasErrors()) return "/todo/add";

        Optional<Member> findMember = memberService.findById(getSessionMember(request).getId());
        Optional<ToDo> createToDo = findMember.map(member -> ToDo.createToDo(
                toDoDto.getTitle(), toDoDto.getDescription(), toDoDto.getDueDate(),
                member));
        createToDo.ifPresent(toDo -> toDoService.save(toDo));

        return "redirect:/todo";
    }

    @PostMapping("/todo/update/{id}")
    public String update(@PathVariable Long id, @Validated @ModelAttribute("toDoDto") ToDoDto toDoDto, BindingResult bindingResult) {

        if (!bindingResult.hasErrors() && toDoDto.getTitle().length() >= 20)
            bindingResult.rejectValue("title", "length", "최대 20자까지 가능합니다.");

        if (bindingResult.hasErrors()) return "/todo/edit";

        if (id != null)
            toDoService.update(id, toDoDto.getTitle(), toDoDto.getDescription(), toDoDto.getDueDate());
        return "redirect:/todo";
    }
}

title의 길이 검증 로직 추가

add.html

<div class="row">
	<div class="col">
		<label class="form-label" th:for="*{title}">제목</label>
		<input type="text" class="w-100 form-control" placeholder="최대 20자까지 가능합니다" th:field="*{title}"><br>
		<div th:errors="*{title}"></div> <!--추가-->
	</div>
	<div class="col">
		<label class="form-label" th:for="*{dueDate}">마감일</label>
		<input type="date" class="w-100 form-control" th:field="*{dueDate}">
		<div th:errors="*{dueDate}"></div> <!--추가-->
	</div>
</div>
<label class="form-label" th:for="*{description}">설명</label>
<textarea class="w-100 form-control" placeholder="최대 100자까지 가능합니다" th:field="*{description}"></textarea>
<div th:errors="*{description}"></div> <!--추가-->
}

edit.html은 add.html과 동일

다음으로

스프링 부트의 검증 기능과 타임리프를 이용하면 일반적인 검증 로직과 에러 메시지를 쉽게 처리할 수 있습니다.
다음 시간에는 생각하지도 못하고 있던 로그아웃 기능과 스프링 인터셉터를 활용해 공통 관심사인 로그인 여부 등을 체크하는 기능을 개발하겠습니다.


github , 배포 URL (첫 접속 시 로딩이 걸릴 수 있습니다.)

profile
평범한 대학생의 공부 일기?

0개의 댓글