스프링 부트 게시판 프로젝트 - 7 | 회원 컨트롤러, 회원가입 페이지 개발

seren-dev·2022년 8월 24일
1

회원 컨트롤러 개발 - 회원가입

폼 등록용 객체 생성

폼 등록용 객체를 생성할 때, Bean Validation에서 제공하는 어노테이션을 사용하여 값을 검증하도록 한다.

UserForm

package hello.board.controller;

import lombok.Getter;

import javax.validation.constraints.NotBlank;

@Getter
public class UserForm {

    @NotBlank
    private String loginId;

    @NotBlank
    private String password;

    @NotBlank
    private String name;

    private Integer age;
}
  • 나이는 null일 수 있기 때문에 원시 타입이 아닌 Integer타입으로 선언한다.
    • User 엔티티도 age 타입을 Integer로 변경한다.
  • @NotBlank : 빈값이거나 공백만 있는 경우를 허용하지 않는다.

Bean Validation 이란?
Bean Validation은 특정한 구현체가 아니라 Bean Validation 2.0(JSR-380)이라는 기술 표준이다.
쉽게 이야기해서 검증 애노테이션과 여러 인터페이스의 모음이다. 마치 JPA가 표준 기술이고 그 구현체로 하이버네이트가 있는 것과 같다.
Bean Validation을 구현한 기술 중에 일반적으로 사용하는 구현체는 하이버네이트 Validator이다. 이름이 하이버네이트가 붙어서 그렇지 ORM과는 관련이 없다.
하이버네이트 Validator 관련 링크

참고: [Spring Boot] @NotNull, @NotEmpty, @NotBlank 의 차이점 및 사용법

  • @NotNull: null을 허용하지 않음
  • @NotEmpty: null, "" 둘 다 허용하지 않음
  • @NotBlank: null, "", " " 모두 허용하지 않음

회원등록 컨트롤러

UserController

package hello.board.controller;

import hello.board.entity.User;
import hello.board.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;


@Controller
@Slf4j
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @GetMapping("/signup")
    public String signupForm(@ModelAttribute UserForm userForm) {
        log.info("signupForm");
        return "users/signupForm";
    }

    @PostMapping("/signup")
    public String signup(@Valid @ModelAttribute UserForm userForm, BindingResult bindingResult) {

        log.info("signup");
        
        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "users/signupForm";
        }


        User user = User.builder()
                .loginId(userForm.getLoginId())
                .password(userForm.getPassword())
                .name(userForm.getName())
                .age(userForm.getAge()).build();

        userService.join(user);

        log.info("signup success");
        return "redirect:/";
    }
}
  • BindingResult를 사용하여 스프링이 제공하는 검증 오류 처리 방법을 사용한다.

BindingResult 주의점

  • BindingResult는 검증할 대상 바로 다음에 와야한다.
  • BindingResult bindingResult 파라미터의 위치는 @ModelAttribute UserForm userForm 다음에 와야 한다. BindingResult가 UserForm 객체의 바인딩 결과를 담기 때문이다.

회원 등록 화면

templates/users/signupForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
          href="../css/bootstrap.min.css" rel="stylesheet">
    <style>
    .container {
    max-width: 560px;
    }
    .field-error {
    border-color: #dc3545;
    color: #dc3545;
    }
</style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>회원 가입</h2>
    </div>
    <h4 class="mb-3">회원 정보 입력</h4>
    <form action="" th:action th:object="${userForm}" method="post">

        <div>
            <label for="loginId">ID</label>
            <input type="text" id="loginId" th:field="*{loginId}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{loginId}" />
        </div>
        <div>
            <label for="password">비밀번호</label>
            <input type="password" id="password" th:field="*{password}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{password}" />
        </div>
        <div>
            <label for="name">이름</label>
            <input type="text" id="name" th:field="*{name}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{name}" />
        </div>
        <div>
            <label for="age">나이</label>
            <input type="text" id="age" th:field="*{age}" class="form-control"
                   th:errorclass="field-error">
            <div class="field-error" th:errors="*{age}" />
        </div>
        <hr class="my-4">

        <div class="row">
            <div class="col">
                <button class="w-100 btn btn-primary btn-lg" type="submit">회원
                    가입</button>
            </div>
            <div class="col">
                <button class="w-100 btn btn-secondary btn-lg"
                        th:onclick="|location.href='@{/}'|"
                        type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>
  • 타임리프의 사용자 입력 값 유지
    th:field="*{loginId}"
    타임리프의 th:field 는, 정상 상황에는 모델 객체의 값을 사용하지만, 이 필드에 오류가 발생하면 BindingResult에서 보관한 값을 사용해서 값을 출력(value)한다.

th:field의 기능

  • id, name, value 속성 자동 생성
  • th:errorclass 와 같이 사용될 경우, th:field 값에 에러가 있으면 class 속성에 errorclass 값을 추가한다.
  • th:field 값에 오류가 생기면, 정상 상황에는 모델 객체의 값을 사용하지만, 이 필드에 오류가 발생하면 FieldError 에서 보관한 값을 사용해서 값을 출력(value)한다.
    th:fieldinput 태그에 적용 하는 필드다.
  • 오류 메시지 출력
    타임리프 화면을 렌더링 할 때 th:errors 가 실행된다. 만약 이때 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다.

타임리프 스프링 검증 오류 통합 기능
타임리프는 스프링의 BindingResult를 활용해서 편리하게 검증 오류를 표현하는 기능을 제공한다.

  • #fields : #fieldsBindingResult가 제공하는 검증 오류에 접근할 수 있다.
    ex) th:if="${#fields.hasGlobalErrors()}"
    th:if="${#fields.hasErrors('loginId')}"
  • th:errors : 해당 필드에 오류가 있는 경우에 태그를 출력한다. th:if 의 편의 버전이다. 오류가 있다면 생성된 오류 메시지 코드를 순서대로 돌아가면서 메시지를 찾는다. 그리고 없으면 디폴트 메시지를 출력한다.
    • 에러가 없으면 출력X
  • th:errorclass : th:field 에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.

문제점

  • 하나의 필드에만 오류가 나도 전체 필드에 오류가 난 것처럼 전체 필드에 오류메시지가 출력된다.
  • 오류가 생기면 입력된 값이 사라진다.

어디에서 문제가 생겼는지 알아보기 위해 UserController에서 로그를 찍는다.

@PostMapping("/signup")
    public String signup(@Valid @ModelAttribute("userForm") UserForm userForm, BindingResult bindingResult) {

        log.info("signup");
        if (bindingResult.hasErrors()) {
            log.info("errors = {}", bindingResult);
            return "users/signupForm";
        }
        log.info("signup success");
        ...
}
hello.board.controller.UserController    : errors = org.springframework.validation.BeanPropertyBindingResult: 3 errors

Field error in object 'userForm' on field 'loginId': rejected value [null]; codes [NotBlank.userForm.loginId,...
Field error in object 'userForm' on field 'password': rejected value [null]; codes [NotBlank.userForm.password,...
Field error in object 'userForm' on field 'name': rejected value [null]; codes [NotBlank.userForm.name,...
  • 폼에서 값을 입력하면 아예 값을 받아오지 못하는 것 같다. 이를 확인하기 위해 파라미터로 HttpServletRequest를 추가한 다음 getParameter메서드를 사용하여 값을 받아오는지 확인한다.
String loginId = request.getParameter("loginId");
System.out.println(loginId);
  • 입력한 값을 HttpServletRequest로 가져오면 값이 나타난다.
    -> 값이 UserForm 객체에 바인딩되지 않는 것 같다.

해결

  • UserForm@Setter가 없었기 때문에 값이 바인딩되지 않았다.
  • @ModelAttribute는 Setter를 통해 객체에 값을 바인딩하는데, Setter 메서드가 없어서 값을 바인딩하지 못했다.

스프링MVC는 @ModelAttribute 가 있으면 다음을 실행한다.

  • UserForm 객체를 생성한다.
  • 요청 파라미터의 이름으로 UserForm 객체의 프로퍼티를 찾는다. 그리고 해당 프로퍼티의 setter를 호출해서 파라미터의 값을 입력(바인딩) 한다.
  • 예) 파라미터 이름이 loginId 이면 setloginId() 메서드를 찾아서 호출하면서 값을 입력한다.

UserForm 수정 (@Setter 추가)

package hello.board.controller;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotBlank;

@Getter @Setter
public class UserForm {

    @NotBlank
    private String loginId;

    @NotBlank
    private String password;

    @NotBlank
    private String name;

    private Integer age;
}


DB에도 회원 가입이 반영 된 것을 확인할 수 있다.


typeMismatch 오류 메시지 설정

나이는 숫자만 입력해야 한다. 다른 타입을 입력한다면 오류 메시지가 출력되는데, 스프링에서 기본으로 설정한 오류 메시지 대신 직접 오류 메시지를 설정한다.

application.properties 수정
spring.messages.basename=messages,errors

errors.properties 추가
typeMismatch.java.lang.Integer=숫자를 입력해주세요

결과 화면

0개의 댓글