elevne's Study Note

Spring Boot 복습 (3) 본문

Backend/Spring

Spring Boot 복습 (3)

elevne 2023. 4. 7. 11:28

이번에는 우선 템플릿으로 사용할 HTML 파일을, 템플릿엔진으로 Thymeleaf 를 사용하여 작성해보았다. templates 디렉토리에 question_list.html 이라는 이름의 파일을 생성하고, 컨트롤러와 해당 파일을 아래와 같이 작성하였다.

 

 

 

Controller

 

package com.springboot.study.controller;

import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.List;

@RequiredArgsConstructor
@Controller
public class MainController {

    private final QuestionRepository questionRepository;

    @GetMapping("/question/list")
    public String list(Model model){
        List<Question> questionList = this.questionRepository.findAll();
        model.addAttribute("questionList", questionList);
        return "question_list";
    }

}

 

 

@RequiredArgsConstructor 어노테이션은 questionRepository 속성을 포함하는 생성자를 생성해준다. 이는 롬복이 제공하는 어노테이션으로 final 이 붙은 속성을 포함하는 생성자를 자동으로 생성하는 역할을 한다. 

 

 

question_list.html

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<table>
    <thead>
        <tr>
            <th>제목</th>
            <th>작성일시</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="question : ${questionList}">
            <td th:text="${question.subject}" />
            <td th:text="${question.createdDate}" />
        </tr>
    </tbody>
</table>

</body>
</html>

 

 

result

 

 

 

위와 같이 작성하면 리스트가 잘 뜨는 것을 확인할 수 있다. 

 

 

 

그 다음으로는 Root URL, localhost:8080 처럼 도메인명, 포트 뒤에 아무것도 붙이지 않은 URL 을 사용할 수 있게끔 매핑을 설정해주었다. MainController 에 아래 코드를 추가해주었다.

 

 

 

    @GetMapping("/")
    public String root(){
        return "redirect:/question/list";
    }

 

 

 

redirect 는 /question/list URL 로 페이지를 리다이렉트 시킨다. redirect 외에도 forward 를 사용할 수 있다. redirect 는 URL 로 리다이렉트하고 이는 완전히 새로운 URL 로 요청이 된다. 반대로 forward 는 URL 로 forward, 기존 요청 값이 유지된 상태로 URL 이 전달된다.

 

 

 

지금까지는 QuestionController 에서 QuestionRepository 를 직접 사용하여 질문 목록 데이터를 조회하였지만, 대부분의 프로젝트에서는 Repository 를 직접 호출하지는 않고 중간에 Service 를 두어 데이터를 처리한다. Service 는 Spring 에서 데이터 처리를 위해 작성하는 클래스이다. 서비스를 사용하는 이유로 우선 모듈화가 된다는 점이 있다. 컨트롤러가 여러 개의 Repository 를 사용하여 데이터를 조회한 후 가공하여 return 할 때, 이러한 기능을 서비스로 만들어두면 컨트롤러에서는 해당 서비스만 호출해서 사용하면된다. 또한 Controller 은 Repository 없이 서비스를 통해서만 DB 에 접근하도록 구현하는 것이 보안상 안전하다. 마지막으로 이전에 작성한 Question, Answer 과 같은 DB 와 직접 맞닿아 있는 Entity 클래스를 컨트롤러나 템플릿 엔진에 직접 전달하여 사용하는 것은 바람직하지 않다. Controller 나 템플릿 엔진은 데이터 객체의 속성을 변경하여 비즈니스 로직을 처리해야 하는 경우가 많은데 Entity 를 직접 사용하여 속성을 변경하면 테이블 컬럼이 변경되어 악영향을 끼칠 수 있기 때문이다. 이를 위해서 Entity 클래스 대신에 사용할 DTO (Data Transfer Object) 클래스가 필요하다. 이러한 Entity 클래스를 DTO 클래스로 변환해주는 역할도 Service 에서 이루어지는 것이다.

 

 

 

우선 QuestionService 를 작성하였다.

 

 

 

package com.springboot.study.service;

import com.springboot.study.entity.Question;
import com.springboot.study.repository.QuestionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@RequiredArgsConstructor
@Service
public class QuestionService {

    private final QuestionRepository questionRepository;

    public List<Question> getList() {
        return this.questionRepository.findAll();
    }

}

 

 

 

Service Class 로 만들어주기 위해서 @Service 어노테이션을 사용하면 된다. getList() 메서드를 Controller 에서 사용하면 되는 것이다.

 

 

그 다음으로는 질문 목록의 제목을 클릭했을 때 상세화면이 호출되도록 제목에 링크를 추가한다. 질문 목록 템플릿의 일부를 아래와 같이 수정한다.

 

 

 

    <tr th:each="question : ${questionList}">
        <td>
            <a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}" />
        </td>
        <td th:text="${question.createdDate}" />
    </tr>

 

 

 

위에서는 th:href 속성을 사용하였다. 타임리프에서 URL 주소를 나타낼 때는 @{ } 로 감싸주어야 하고, 문자열과 ${} 객체 값이 조합되어 사용될 때는 | | 로 감싸주고 사용해야 한다. 그 다음으로는 우선 Service 에서 id 를 이용하여 Question 을 검색하는 기능을 작성하였다.

 

 

 

    public Question getQuestion(Integer id) {
        Optional<Question> question = this.questionRepository.findById(id);
        if (question.isPresent()) {
            return question.get();
        } else {
            throw new DataNotFoundException("QUESTION NOT FOUND");
        }
    }

 

 

 

위에서 발생시키는 DataNotFoundException 은 실제로는 없는 Exception 이다. Custom Exception 을 따로 또 아래와 같이 작성해준다.

 

 

 

package com.springboot.study.exceptions;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "ENTITY NOT FOUND")
public class DataNotFoundException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    public DataNotFoundException(String message){
        super(message);
    }
}

 

 

 

DataNotFoundException 은 RuntimeException 을 상속하여 만들어졌다. 만약 DataNotFoundException 이 발생하면 @ResponseStatus 어노테이션에 의해 404 (HttpStatus.NOT_FOUND) 가 나타나게 된다.

 

 

 

Controller 에는 아래와 같이 작성해준다.

 

 

 

    @GetMapping(value = "/question/detail/{id}")
    public String detail(Model model, @PathVariable("id") Integer id){
        Question question = this.questionService.getQuestion(id);
        model.addAttribute("question", question);
        return "question_detail";
    }

 

 

 

result

 

 

 

question_detail 까지 model 에 담은 question 을 활용하여 작성해주면 결과가 잘 나타나는 것을 확인할 수 있다.

 

 

 

QuestionController 의 URL 매핑을 살펴보면 URL prefix 가 모두 /question 으로 시작하는 것을 확인할 수 있다. 이런 경우 Class 에 @RequestMapping("/question") 어노테이션을 추가해주면 메서드 단위에는 /question 을 생략한 그 뒷 부분만을 적으면 된다.

 

 

 

그 다음으로는 question_detail 에 답변 등록 기능을 추가해줄 차례였다. 아래와 같이 작성하였다.

 

 

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1 th:text="${question.subject}"></h1>
<div th:text="${question.content}"/>

<h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
<div>
    <ul>
        <li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li>
    </ul>
</div>

<form th:action="@{|/answer/create/${question.id}|}" method="post">
    <textarea name="content" id="content" rows="15"></textarea>
    <input type="submit" value="답변등록">
</form>
</body>
</html>

 

 

 

그 후 해당 답변 기능에 대한 매핑을 새로 정의해주어야 했다.

 

 

 

package com.springboot.study.controller;

import com.springboot.study.entity.Question;
import com.springboot.study.service.AnswerService;
import com.springboot.study.service.QuestionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@RequestMapping("/answer")
@RequiredArgsConstructor
@Controller
public class AnswerController {

    private final QuestionService questionService;
    
    private final AnswerService answerService;

    @PostMapping("/create/{id}")
    public String createAnswer(Model model, @PathVariable("id") Integer id, @RequestParam String content) {
        Question question = this.questionService.getQuestion(id);
        this.answerService.create(question, content);
        return String.format("redirect:/question/detail/%s", id);
    }
}

 

 

 

@RequestParam 어노테이션이 추가되었다. 이는 템플릿에서 답변으로 입력한 내용을 얻기 위해 추가된 것으로, 템플릿의 답변 내용에 해당하는 textarea 의 name 속성명이 content 이기 때문에 여기에서도 변수 명을 content 로 사용한 것이다. 

 

 

 

다음으로는 AnswerService 클래스를 작성하였다.

 

 

 

package com.springboot.study.service;

import com.springboot.study.entity.Answer;
import com.springboot.study.entity.Question;
import com.springboot.study.repository.AnswerRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

@RequiredArgsConstructor
@Service
public class AnswerService {

    private final AnswerRepository answerRepository;


    public void create(Question question, String content) {
        Answer answer = new Answer();
        answer.setContent(content);
        answer.setCreateDate(LocalDateTime.now());
        answer.setQuestion(question);
        this.answerRepository.save(answer);
    }
}

 

 

 

result

 

 

 

잘 동작하는 것을 확인할 수 있을 것이다.

 

 

 

 

 

 

 

Reference:

https://wikidocs.net/161357

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

Spring Boot 복습 (5)  (0) 2023.04.09
Spring Boot 복습 (4)  (0) 2023.04.08
Spring Boot 복습 (2)  (0) 2023.04.05
Spring Boot 복습 (1)  (0) 2023.04.04
웹 개발 공부 (MyBatis)  (0) 2022.11.16