elevne's Study Note

Spring Boot 복습 (4) 본문

Backend/Spring

Spring Boot 복습 (4)

elevne 2023. 4. 8. 11:54

저번 시간에 이어 이번에는 Bootstrap 을 이용하여 페이지를 꾸미고, 새로운 질문을 등록하는 기능을 개발하였다. 질문을 등록하는 것은 PostMapping 을 이용하여 Form 을 전달받게 되는데, 질문이나 내용을 등록할 때 비어있는 값으로 등록할 수 없도록 처리를 해줘야 한다. 이러한 Validation 을 진행하기 위해서 Spring Boot Validation 라이브러리가 사용된다. 우선 build.gradle 의 dependencies 에 implementation 'org.springframework.boot:spring-boot-starter-validation' 를 추가해준다. 해당 라이브러리를 추가해주면 아래와 같은 어노테이션들을 사용하여 입력 값들을 검증해볼 수 있다.

 

 

 

Spring Boot Validation

 

 

 

또, 위 어노테이션들을 사용하여 검증하기 위해서는 Form Class 가 필요하다. Subject, Content 에 대응하는 QuestionForm 클래스를 아래와 같이 작성해주었다.

 

 

 

package com.springboot.study.form;

import lombok.Data;

import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;

@Data
public class QuestionForm {
    
    @NotEmpty(message = "제목은 필수사항입니다.")
    @Size(max=200)
    private String subject;
    
    @NotEmpty(message = "내용은 필수사항입니다.")
    private String content;
}

 

 

 

작성한 QuestionForm 클래스를 사용하기 위해서는 컨트롤러를 아래와 같이 수정해야 한다.

 

 

 

    @PostMapping("/create")
    public String questionCreate(@Valid QuestionForm questionForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "question_form";
        }
        this.questionService.create(questionForm.getSubject(), questionForm.getContent());
        return "redirect:/question/list";
    }

 

 

 

questionCreate 메서드의 매개변수를 subject, content 대신에 QuestionForm 객체로 변경했다. subject, content 항목을 지닌 Form 이 전송되면 QuestionForm 의 subject, content 속성이 자동으로 Binding 되는 것이다. (Spring Framework 의 Binding 기능) @Valid 어노테이션은 QuestionForm 의 @NotEmpty, @Size 등으로 설정한 검증 기능을 동작하게 해준다. 이어지는 BindingResult 매개변수는 @Valid 어노테이션으로 인해 검증이 수행된 결과를 의미하는 객체이다. BindingResult 매개변수는 항상 @Valid 매개변수 바로 뒤에 위치해야 한다. 만약 2 개의 매개변수의 위치가 정확하지 않다면 @Valid 만 적용이 되어 입력값 검증 실패 시 400 오류가 발생한다. questionCreate 메서드는 bindingResult.hasErrors() 를 호출하여 오류가 있는 경우에는 다시 폼을 작성하는 화면을 렌더링하게끔 하는 것이다. 

 

 

 

답변 등록 Controller 메서드도 아래와 같이 수정해준다.

 

 

 

    @PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id,
                               @Valid AnswerForm answerForm, BindingResult bindingResult) {
        Question question = this.questionService.getQuestion(id);
        if (bindingResult.hasErrors()) {
            model.addAttribute("question", question);
            return "question_detail";
        }
        this.answerService.create(question, answerForm.getContent());
        return String.format("redirect:/question/detail/%s", id);
    }

 

 

 

그 다음으로는 게시판 글들을 페이지네이션 처리하는 해주어야 했다. 우선 아래와 같은 테스트 코드로 300 개의 더미데이터를 생성해주었다.

 

 

 

	@Test
	void testJpa() {
		for (int i = 1; i <= 300; i++) {
			String subject = String.format("테스트 데이터입니다:[%03d]", i);
			String content = "내용무";
			this.questionService.create(subject, content);
		}
	}

 

 

 

Question Repository 에 Page<Question> findAll(Pageable pageable); 메서드를 추가한다. Pageable 객체를 입력으로 받아 Page<Question> 타입 객체를 리턴하는 메서드이다. Question Service 도 아래와 같이 수정해준다.

 

 

 

    public Page<Question> getList(int page){
        Pageable pageable = PageRequest.of(page, 10);
        return this.questionRepository.findAll(pageable);
    }

 

 

 

getList 메서드는 이제 정수 타입의 페이지 번호를 입력받아 해당 페이지의 질문 목록을 리턴하는 메서드이다. Pageable 객체를 생성할 때 사용한 PageRequest.of(page, 10) 에서 page 는 조회할 페이지의 번호이고 10 은 한 페이지에 보여줄 게시물의 갯수이다. 이를 수정하였으니 Question Controller 도 아래와 같이 수정해줄 필요가 있다.

 

 

 

    @GetMapping("/list")
    public String list(Model model, @RequestParam(value="page", defaultValue = "0") int page){
        Page<Question> paging = this.questionService.getList(page);
        model.addAttribute("paging", paging);
        return "question_list";
    }

 

 

 

위에서 사용되는 Page 객체에는 아래와 같은 속성들이 들어있다.

 

 

 

Page

 

 

 

result

 

 

 

지금까지의 코드만으로는 우선 10 개 씩 분할되서 나오는 것을 확인할 수 있지만, 다음 페이지로 넘어갈 수가 없다. question_list.html 파일에 아래와 같이 작성해준다.

 

 

 

    <!-- 페이징처리 시작 -->
    <div th:if="${!paging.isEmpty()}">
        <ul class="pagination justify-content-center">
            <li class="page-item" th:classappend="${!paging.hasPrevious} ? 'disabled'">
                <a class="page-link"
                   th:href="@{|?page=${paging.number-1}|}">
                    <span>이전</span>
                </a>
            </li>
            <li th:each="page: ${#numbers.sequence(0, paging.totalPages-1)}"
                th:if="${page >= paging.number-5 and page <= paging.number+5}"
                th:classappend="${page == paging.number} ? 'active'"
                class="page-item">
                <a th:text="${page}" class="page-link" th:href="@{|?page=${page}|}"></a>
            </li>
            <li class="page-item" th:classappend="${!paging.hasNext} ? 'disabled'">
                <a class="page-link" th:href="@{|?page=${paging.number+1}|}">
                    <span>다음</span>
                </a>
            </li>
        </ul>
    </div>
    <!-- 페이징처리 끝 -->

 

 

 

또, 현재 질문은 등록한 순서대로 데이터가 표시되는데 게시판은 가장 최근에 작성한 게시물이 먼저 보이는 것이 일반적이다. 이를 구현하기 위해 Question Service 를 아래와 같이 수정해준다.

 

 

 

    public Page<Question> getList(int page){
        List<Sort.Order> sorts = new ArrayList<Sort.Order>();
        sorts.add(Sort.Order.desc("createdDate"));
        Pageable pageable = PageRequest.of(page, 10, Sort.by(sorts));
        return this.questionRepository.findAll(pageable);
    }

 

 

 

게시물을 역순으로 조회하기 위해서는 위와 같이 PageRequest.of 메서드의 세 번째 파라미터로 Sort 객체를 전달해야 한다. Sort.Order 객체로 구성된 리스트에 Sort.Order 객체를 추가하고 Sort.by(SortList) 로 Sort 객체를 생성할 수 있다. 결과를 확인해보면 페이지네이션, 페이지 이동 버튼 그리고 정렬이 전부 잘 되어있는 것을 확인할 수 있다.

 

 

 

result

 

 

 

 

 

 

 

Reference:

https://wikidocs.net/161357

'Backend > Spring' 카테고리의 다른 글

Spring Boot 복습 (6)  (0) 2023.04.10
Spring Boot 복습 (5)  (0) 2023.04.09
Spring Boot 복습 (3)  (0) 2023.04.07
Spring Boot 복습 (2)  (0) 2023.04.05
Spring Boot 복습 (1)  (0) 2023.04.04