사이먼's 코딩노트

[SpringBoot] Question 등록 / 유효성 체크 및 에러 처리(1) 본문

Java/SpringBoot

[SpringBoot] Question 등록 / 유효성 체크 및 에러 처리(1)

simonpark817 2024. 5. 12. 19:59

[Question 등록]

  • 현재까지 제작된 SBB 프로그램의 기능은 전체 질문 리스트 조회(question/list), 질문 상세 내용보기(question/detail/{id}), 답변 등록(answer/create{id}) 및 보기가 포함되어 있다.
  • 이번에는 질문을 등록하는 버튼을 추가하고, 질문을 등록할 수 있는 화면 페이지를 만들어봅시다.
  • 아래는 Question 등록을 위해 question_list.html에서 버튼 코드를 추가한 모습이다.
<a th:href="@{/question/create}" class="btn btn-primary">질문 등록하기</a>
  • 기본 layout.html 템플릿이 상속된 html 태그 안에서 가장 하단에 위와 같은 코드를 추가한다.
  • 해당 버튼 역시 부트스트랩 클래스가 적용되어 있기 때문에 버튼에 디자인이 적용되어 있다.
  • 화면에 나타난 '질문 등록하기' 버튼을 지금 상태에서 클릭하게 된다면 당연히 QuestionController.java 클래스에 /question/create가 매핑되어 있지 않기 때문에 오류가 나타난다.

 

[QuestionController 추가]

  • 아래는 QuestionController.java 클래스에 코드를 추가한 모습이다.
@GetMapping("/create")
public String create(QuestionForm questionForm) {
    return "question_form";
}
  • '질문 등록하기' 버튼을 통한 /question/create 링크 요청은 GET에 해당하므로 @GetMapping 어노테이션을 사용하고 create() 메서드는 question_form 템플릿을 출력하도록 한다.
  • 여기서 주의할 점은 QuestionForm 변수는 model.addAttribute 없이 바로 뷰에서 접근할 수 있다는 것이다.
  • create() 메서드에 매개변수를 써준 이유는 question_form.html에서 questionForm 변수가 없으면 실행이 되지 않기 때문에 빈 객체라도 만드는 것이다.

 

[question_form.html 템플릿 생성]

  • 아래는 화면 페이지를 만들기 위해 question_form.html 템플릿을 새로 생성하여 작성한 코드이다.
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
    <h5 class="my-3 border-bottom pb-2">질문등록</h5>
    <form th:action="@{/question/create}" method="post">
        <div class="mb-3">
            <label for="subject" class="form-label">제목</label>
            <input type="text" name="subject" id="subject" class="form-control">
        </div>
        <div class="mb-3">
            <label for="content" class="form-label">내용</label>
            <textarea name="content" id="content" class="form-control" rows="10"></textarea>
        </div>
        <input type="submit" value="저장하기" class="btn btn-primary my-2">
    </form>
</div>
</html>
  • 다른 템플릿과 동일하게 layout.html를 기본적으로 상속받도록 하고, 템플릿에는 제목과 내용을 작성할 수 있는 텍스트 창을 추가하였다.
  • 제목은 일반적인 텍스트 타입의 input 태그를 사용하였고, 내용은 글자 수의 제한이 없는 textarea 태그를 사용하였다.
  • 입력한 내용을 '저장하기' 버튼을 통해 submit하게 되면 해당 폼이 /question/create로 POST 방식을 이용해 전송하도록 하였다.
  • 현재까지 구현된 내용을 정리하자면, localhost:8090/question/list 주소로 접속 시 '질문 등록하기' 라는 버튼이 새로 추가되었고, 해당 버튼을 클릭하면 질문을 등록할 수 있는 페이지가 등장한다.

질문 등록하기 버튼 생성

 

질문 등록 화면 페이지

 

[POST 방식으로 URL 매핑]

  • 여기까지 진행되었다면, 질문 제목과 내용을 작성하여 '저장하기' 버튼을 클릭했을 때 405 에러가 발생한다.
  • 405 에러는 'Method Not Allowed' 라는 의미로 /question/create URL을 POST 방식으로 처리할 수 없다는 것을 뜻한다.
  • POST 요청을 처리할 수 있도록 QuestionController.java 클래스에 아래와 같이 코드를 추가하였다.
@PostMapping("/create")
public String questionCreate(@RequestParam(value="subject") String subject, @RequestParam(value="content") String content) {
    Question q = this.questionService.create(subject, content);

    return "redirect:/question/list";
}
  • 위에서 추가했던 @GetMapping과는 별개로 이번에는 @PostMapping 어노테이션을 지정한 questionCreate() 메서드를 추가하였다.
  • 메서드 명은 @GetMapping에서 사용한 create() 메서드 명과 동일하게 오버로딩해도 되지만 구분을 짓고자 questionCreate()라고 메서드 명을 지었다.
  • questionCreate() 메서드는 화면에서 입력한 제목과 내용을 매개변수로받고 이 때, question_form.html에서 입력 항목으로 사용한 subject와 content의 이름과 @RequestParam의 value값이 일치해야 한다.
  • questionService를 통해 create() 메서드를 호출하여 제목과 내용을 넘겨주고 저장하도록 한다.

 

[Question 데이터 저장]

  • 마지막으로 QuestionService.java 클래스에 create() 메서드를 추가하여 실제로 질문 등록 후 저장이 가능하도록 코드를 추가해봅시다.
public Question create(String subject, String content) {
    Question q = new Question();
    q.setSubject(subject);
    q.setContent(content);
    q.setCreateDate(LocalDateTime.now());

    this.questionRepository.save(q);
    return q;
}
  • QuestionService.java 클래스에서 question 데이터 하나를 생성하기 위해 create() 메서드를 추가하였다.
  • create() 메서드는 입력받은 2개의 매개변수인 subject와 content를 사용하여 Question 객체를 생성하여 저장하였고, 마지막에 questionRepository를 통해 해당 데이터를 save() 하였다.
  • 이렇게 Question 등록 기능이 모두 구현되었다면, 로컬 서버를 다시 시작했을 때 문제없이 질문이 원할하게 등록되는 것을 확인할 수 있다.

 

[유효성 체크 및 에러 처리]

  • 위에서 Question 등록하는 기능을 구현했지만, 생각해보면 질문을 등록할 때나 답변을 등록할 때 사용자가 아무 내용도 입력하지 않고 등록할 수 있다는 것을 간과하였다.
  • 아무것도 입력하지 않은 상태에서 질문이나 답변이 등록되는 것은 상식적으로 있을 수 없는 일이기 때문에 이번에는 폼 클래스를 사용하여 유효성을 체크하는 방법을 도입해봅시다.
  • 여기서 폼 클래스란 컨트롤러나 서비스와 같이 웹 프로그램을 개발하는 주요 구성 요소 중 하나로서, 웹 프로그램에서 사용자가 입력한 데이터를 검증하는 데 사용한다고 생각하면 좋다.
  • 폼 클래스를 사용해 사용자로부터 입력받은 값을 검증하려면, Spring Boot Validation 라이브러리가 필요하기 때문에 build.gradle 파일에서 아래와 같은 코드를 dependencies 안에 추가해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'

 

  • 라이브러리를 추가했다면 ,먼저 질문 등록하는 부분에 유효성 체크 확인을 위한 QuestionForm.java 클래스를 생성하여 아래와 같이 코드를 추가해봅시다.
package com.sbs.sbb.Question;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class QuestionForm {
    @NotBlank(message="제목은 필수항목입니다.")
    @Size(max=200, message = "제목을 200자 이하로 입력해주세요.")
    private String subject;

    @NotBlank(message="내용은 필수항목입니다.")
    @Size(max=20000, message = "제목을 20,000자 이하로 입력해주세요.")
    private String content;
}
  • 질문을 등록할 땐 제목과 내용 2가지 작성 목록이 필요하기 때문에 subject와 content 변수 두개를 선언하였다.
  • subject, content 두 속성에는 모두 @NotBlank와 @Size 어노테이션을 적용하였다.
  • @NotBlank는 해당 값이 NULL 또는 빈 문자열("") 또는 공백(" ")을 허용하지 않음을 의미한다. 그리고 여기에 사용한 message는 검증이 실패한경우 화면에 표시할 오류 메시지이다.
  • @Size(max=200)은 최대 길이가 200바이트를 넘으면 안된다는 의미로, 이와 같이 설정하면 제목이 200자를 넘게되면 오류가 발생한다.
  • 폼 클래스는 입력값 검증할 때뿐만 아니라 입력 항목을 바인딩할 때도 사용한다. 즉, question_form.html 템플릿의 입력 항목인 subject와 content가 폼 클래스의 subject, content 속성과 바인딩된다.
  • 여기서 말하는 바인딩이란 템플릿의 항목과 폼 클래스의 속성이 매핑되는 과정을 의미한다.

 

[QuestionController 수정]

  • 다음은 QuestionForm을 컨트롤러에서 사용할 수 있도록 아래와 같이 QuestionController.java 클래스에 코드를 수정해봅시다.
@PostMapping("/create")
public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
    if(bindingResult.hasErrors()) {
        return "question_form";
    }

    Question q = this.questionService.create(questionForm.getSubject(), questionForm.getContent());

    return "redirect:/question/list";
}
  • questionCreate() 메서드의 매개변수를 @RequiredParam subject, content가 아닌 QuestionForm 객체로 변경하였다.
  • subject, content 항목을 지닌 폼이 전송되면 QuestionForm의 subject, content 속성이 자동으로 바인딩된다.
  • 여기서 QuestionForm 매개변수 앞에 @Valid 어노테이션을 적용하였고, 해당 어노테이션을 적용하면 QuestionForm의 @NotBlank, @Size 등으로 설정한 검증 기능이 동작하게 된다.
  • 다른 매개변수인 BindingResult는 @Valid 어노테이션으로 검증이 수행된 결과를 의미하는 객체이다.
  • 이 때 주의할 점은 BindingResult 매개변수는 항상 @Valid 매개변수 바로 뒤에 위치해야 한다. 만약 두 매개변수의 위치가 정확하기 않다면 @Valid만 적용되어 입력값 검증 실패 시 400 에러가 발생한다.
  • 따라서 questionCreate() 메서드는 bindingResult.hasErrors() 메서드를 조건문을 통해 호출하여 에러가 있는 경우 다시 입력을 하도록하고 에러가 없을 때 질문이 저장되도록 하였다.

 

[에러 메시지 표출을 위한 템플릿 수정]

  • 다음은 검증에 실패했다는 에러 메시지를 보여주기 위해 question_form.html 템플릿의 코드를 아래와 같이 수정해봅시다.
<html layout:decorate="~{layout}">
<div layout:fragment="content" class="container">
    <h5 class="my-3 border-bottom pb-2">질문등록</h5>
    <form th:action="@{/question/create}" th:object="${questionForm}" method="post">
        <div class="alert alert-danger" role="alert" th:if="${#fields.hasAnyErrors()}">
            <div th:each="err : ${#fields.allErrors()}" th:text="${err}" />
        </div>
        <div class="mb-3">
            <label for="subject" class="form-label">제목</label>
            <input type="text" name="subject" id="subject" class="form-control">
        </div>
        <div class="mb-3">
            <label for="content" class="form-label">내용</label>
            <textarea name="content" id="content" class="form-control" rows="10"></textarea>
        </div>
        <input type="submit" value="저장하기" class="btn btn-primary my-2">
    </form>
</div>
</html>
  • form 태그 안에 div 태그에 해당 코드를 추가했고, 검증에 실패할 경우 에러 메시지를 출력하도록 하였다.
  •  #fields.hasAnyErrors()를 th:if로 통해 타임리프 조건문을 사용해서 true라면 QuestionForm 검증이 실패한 것이고, 실패한 이유는 #fields.allErrors()로 확인할 수 있다.
  • 그리고 부트스트랩의 alert alert-danger 클래스를 통해 에러 메시지가 붉을 색으로 표시되도록 하였다.
  • 이렇게 에러를 표시하려면 타임리프의 th:object 속성이 반드시 필요하고 th:object는 form 태그의 입력 항목들이 QuestionForm과 연결된다는 점을 타임리프에게 알려주는 역할을 한다.
  • 위에서 처럼 th:object 속성을 추가했기 때문에 QuestionController.java 클래스에서 @GetMapping으로 매핑한 메서드의 매개변수에 QuestionForm 객체를 넣어줘야 한다.

 

  • 여기까지 구현이 완료됐다면 아래 사진과 같이 질문 작성 시 제목 또는 내용 또는 둘 다 입력을 하지 않았을 때 에러 메시지가 화면에 표시되는 것을 볼 수 있다.

제목, 내용 모두 입력하지 않았을 때

 

제목만 입력했을 때

 

내용만 입력했을 때

반응형