Spring Example: ToDo List #8 메시지 소스 분리

함형주·2022년 10월 7일
0

Spring Example: ToDo

목록 보기
9/16

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

지금까지 작성한 코드를 되돌아 보던 중 HomeController에서 직접 작성한 정규식 검증 로직을 @Pattern으로 처리할 수 있음을 알았습니다. 코드를 그에 맞춰 리팩토링하고 하드코딩으로 작성한 에러 메시지들을 errors.properties로 메시지 소스를 분리하여 사용하겠습니다.

@Pattern

MemberDto

public class MemberDto {
    @Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
    @Pattern(regexp = "^[a-z0-9]*$", message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
    private String loginId;
    @Length(max = 20, min = 5, message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
    @Pattern(regexp = "^[a-z0-9]*$", message = "5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.")
    private String password;
    private String checkPassword;
    @Length(max = 20, min = 2, message = "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.")
    @Pattern(regexp = "^[ㄱ-ㅎㅏ-ㅣ가-힣a-z0-9\\s]*$", message = "2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.")
    private String name;
}

@Pattern 어노테이션에서 정규식 검증 로직 처리

HomeController

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

        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:/";

    }

@Pattern을 적용하여 컨트롤러를 단순하게 변경

검증 시연 장면

@Length와 @Pattern 에서 동시에 오류가 발생하면 에러메시지가 두 번 출력됨
때문에 검증 어노테이션에서 기본 메시지를 정의하여 사용하기 보단 에러메시지를 분리해 사용

메시지 소스를 분리하여 사용하는 이유

이 예제에서는 기존의 방식으로는 에러메시지가 중복으로 출력되었기에 메시지 소스를 분리하였지만 이보다 더 중요한 이유가 있습니다.
메시지가 java파일과 html 등에 하드코딩 되어 있다면 추후에 수정이 필요할 때 모든 소스코드를 뒤져가며 일일히 수정해야 하지만 메시지 소스를 분리하여 사용한다면 그 파일만 수정하면 모든 부분에 적용이 되므로 시간을 절약할 수 있고 원치않는 오타를 방지할 수 있습니다.
또한 메시지 작성시 디테일에 단계를 두어 범용적으로 사용하거나 세밀하게 사용하기 쉽습니다.
이 프로젝트는 굉장히 작기 때문에 에러 메시지만 분리하여 사용하고 메시지 작성 시 단계를 나누지 않겠습니다.

에러메시지 분리

errors.properties

required.memberDto.loginId=5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.
required.memberDto.password=5~20자의 영문 소문자, 숫자만 사용 가능합니다. 공백은 허용되지 않습니다.
required.memberDto.name=2~20자의 영문 소문자, 숫자, 한글만 사용 가능합니다. 앞뒤의 공백은 제거됩니다.
duplication.memberDto=이미 존재하는 ID 입니다.
wrong.memberDto.checkPassword=비밀번호가 일치하지 않습니다.

loginFail.loginDto=id 혹은 비밀번호가 정확하지 않습니다.

required.toDoDto.title=1~20자까지 가능합니다.
required.toDoDto.description=최대 100자까지 가능합니다.
required.toDoDto.dueDate=오늘 이전의 날짜는 선택할 수 없습니다.

application.yml

spring:
  messages:
    basename: errors

스프링부트가 에러메시지 소스를 적용할 수 있도록 설정 추가 (default = messages)

메시지 적용

MemberDto

public class MemberDto {
    @Length(max = 20, min = 5)
    @Pattern(regexp = "^[a-z0-9]*$")
    private String loginId;
    @Length(max = 20, min = 5)
    @Pattern(regexp = "^[a-z0-9]*$")
    private String password;
    private String checkPassword;
    @Length(max = 20, min = 2)
    @Pattern(regexp = "^[ㄱ-ㅎㅏ-ㅣ가-힣a-z0-9\\s]*$")
    private String name;
}

디폴트 메시지를 사용하면 th:errors에서 경우에 따라 에러 메시지가 모두 출력되므로 디폴트 메시지 제거

ToDoDto

public class ToDoDto {
    private Long id;
    @Length(max = 20, min = 1, message = "{required.toDoDto.title}")
    private String title;
    @Length(max = 100, message = "{required.toDoDto.description}")
    private String description;
    private Boolean isCompleted;
    private LocalDateTime createdDateTime;
    @DateTimeFormat(pattern = "yyyy-MM-dd") @FutureOrPresent(message = "{required.toDoDto.dueDate}")
    private LocalDate dueDate;

String title 필드의 어노테이션을 @NotEmpty -> @Length 로 변경하여 한 번에 처리

HomeController

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

        if (!memberDto.getPassword().equals(memberDto.getCheckPassword()))
            bindingResult.rejectValue("checkPassword", "wrong");

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

        Member member = new Member(memberDto.getLoginId(), memberDto.getPassword(), memberDto.getName().strip());
        if (memberService.save(member) == null) {
            bindingResult.reject("duplication");
            return "/member/add";
        }
        return "redirect:/";

    }

로그인id의 중복 여부를 처리하는 부분은 에러 출력 시 loginId 필드가 겹쳐 global 에러를 처리하는 BindingResult.reject로 변경하여 처리

ToDoController

    @PostMapping("/todo/add")
    public String addToDo(@Validated @ModelAttribute("toDoDto") ToDoDto toDoDto, BindingResult bindingResult, HttpServletRequest request) {
        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";
    }

ToDoDto에서 @Length로 바꾸었기에 길이 검증 로직 제거

add.html (member/add)

<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:if="${#fields.hasErrors('name')}" th:text="#{required.memberDto.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:if="${#fields.hasErrors('loginId')}" th:text="#{required.memberDto.loginId}"></div>
<div th:if="${#fields.hasGlobalErrors()}" th:text="#{duplication.memberDto}"></div>

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

<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>

#{}문법으로 메시지 소스 조회 가능

form.html

<div th:if="${#fields.hasGlobalErrors()}" th:text="#{loginFail.loginDto}"></div>

다음으로

이로써 모든 todo list 개발이 모두 끝났습니다. 다음 시간에는 프로젝트를 되돌아보고 정리하며 마치도록 하겠습니다.


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

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

0개의 댓글