WEB 커리큘럼 3주차 - 서비스를 통한 질문, 답변 출력

이은지·2023년 9월 23일
0

GDSC-Web

목록 보기
4/7

1. 도메인 별로 파일 분류하기

  • 도메인: 질문, 답변, 사용자처럼 굵직한 요구사항 또는 문제 영역을 대표하는 말
  • 우리 프로젝트 도메인 구성하기
    • question - 질문 (com.mysite.sbb.question)
    • answer - 답변 (com.mysite.sbb.answer)
    • user - 사용자 (com.mysite.sbb.user)

2. 질문 목록과 템플릿

❗404 오류

현 상태에서 http://localhost:8080/question/list 에 접속하면, 404 error가 뜬다.

→ /question/list URL에 대한 매핑이 있는 컨트롤러가 필요 !!

🔆QuestionController 생성하기

💡 @ResponseBody 이용

  • 자바 객체를 HTTP요청의 body 내용으로 매핑하여 클라이언트로 전송
  • 즉, @ResponseBdoy 어노테이션을 사용하면 http 요청 body를 자바 객체로 전달받을 수 있음
package com.mysite.sbb.question;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class QuestionController {

    @GetMapping("/question/list")
    @ResponseBody
    public String list() {
        return "question list";
    }
}

💡 템플릿 이용하기

  • 템플릿은 자바 코드를 삽입할 수 있는 HTML 형식의 파일
  • 스프링부트에서 사용할 수 있는 템플릿 엔진에는 Thymeleaf, Mustache, Groovy, Freemarker, Velocity 등이 있음
  1. 타임리프 템플릿 엔진 설치

    경로: gdsc\web-board\build.gradle

dependencies {
    (... 생략 ...)
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}
  1. queston_list 템플릿 만들기

    경로: gdsc\web-board\src\main\resources\templates\question_list.html

 <h2>Hello Template</h2>
  1. 데이터 조회하여 템플릿에 전달하기
  • @RequiredArgsConstructor 어노테이션
    • questionRepository 속성을 포함하는 생성자를 생성
    • 롬복이 제공하는 어노테이션으로 final이 붙은 속성을 포함하는 생성자를 자동으로 생성하는 역할
    • 롬복의 @Getter, @Setter가 자동으로 Getter, Setter 메서드를 생성하는 것과 마찬가지로 @RequiredArgsConstructor는 자동으로 생성자를 생성
    • 따라서 스프링 의존성 주입 규칙에 의해 questionRepository 객체가 자동으로 주입됨
  • Model 객체는 자바 클래스와 템플릿 간의 연결고리 역할
    • Model 객체에 값을 담아두면, 템플릿에서 그 값을 사용할 수 있음
    • Model 객체는 따로 생성할 필요 없이 컨트롤러 메서드의 매개변수로 지정하기만 하면 스프링부트가 자동으로 Model 객체를 생성
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class QuestionController {

    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";
    }
}

🔆템플릿에서 전달받은 데이터 사용하기

  • th: : 타임리프 템플릿 엔진 속성으로 자바 코드와 연결되는 부분
  • 분기문 속성
    • th:if=”${question != null}”
  • 반복문 속성
    • th:each=”question : ${questionList}”
    • th:each=”question, loop : ${questionList}”
  • 텍스트 속성
    • th:text=”${question.subject}”
<table>
    <thead>
        <tr>
            <th>제목</th>
            <th>작성일시</th>
        </tr>
    </thead>
    <tbody>
        <tr th:each="question : ${questionList}">
            <td th:text="${question.subject}"></td>
            <td th:text="${question.createDate}"></td>
        </tr>
    </tbody>
</table>



3. ROOT URL

  • http://localhost:8080 처럼 도메인명과 포트 뒤에 아무것도 붙이지 않은 URL
  • 아직 루트 URL에 대한 매핑을 만들지 않았기 때문에 브라우저에서 루트 URL에 접속하면 404 페이지가 나타남

🔆MainController 수정

  • redirect:<URL> - URL로 리다이렉트 (리다이렉트는 완전히 새로운 URL로 요청이 된다.)
  • forward:<URL> - URL로 포워드 (포워드는 기존 요청 값들이 유지된 상태로 URL이 전환된다.)
package com.gdsc.webboard.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class MainController {
    @GetMapping("/") //'/' URL을 매핑
    public String root(){
        return "redirect:/question/list";
    }

}



4. 서비스

🔆서비스란

  • 스프링에서 데이터 처리를 위해 작성하는 클래스

🔆서비스는 왜 필요할까?

  • 모듈화
    • 어떤 컨트롤러가 여러 개의 리포지토리를 사용하여 데이터를 조회한 후 가공하여 리턴할 때, 이 기능을 서비스로 만들어 두면 컨트롤러에서는 해당 서비스를 호출하여 사용하면 됨. 만약 서비스가 없다면, 해당 기능을 필요로 하는 모든 컨트롤러가 동일한 기능을 중복으로 구현해야함
  • 보안
    • 컨트롤러는 리포지토리에 직접 접근하지 않고, 서비스를 통해서만 데이터베이스에 접근할 수 있도록 구현하는 것이 보안상 안전
  • 엔티티 객체와 DTO 객체의 변환
    • 엔티티 클래스는 데이터베이스와 직접 맞닿아 있는 클래스 이므로, 컨트롤러나 템플릿 엔진에 전달하여 사용하는 것은 좋지 ㅇ낳음
    • 엔티티를 직접 사용해 속성을 변경한다면 테이블 컬럼이 변경되어 엉망이 될 수 있기 때문
    • 따라서, 엔티티 대신 사용할 DTO 클래스가 필요
    • 서비스는 컨트롤러와 리포지토리의 중간자적인 입장에서 엔티티 객체와 DTO 객체를 서로 변환하여 양방향에 전달하는 역할을 함

🔆 QuestionService 추가하여 사용하기

  1. QuestionService.java
  • 경로: src\main\java\com\gdsc\webboard\question\QuestionService.java
  • @Service 어노테이션을 통해 서비스로 인식
  • @RequiredArgsConstructor 어노테이션을 통해 questionRepository 객체는 생성자 방식으로 DI규칙에 의해 주입됨
package com.gdsc.webboard.question;

import java.util.List;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class QuestionService {

    private final QuestionRepository questionRepository;

    public List<Question> getList() {
        return this.questionRepository.findAll();
    }
}
  1. QustionController 수정
  • 리포지토리 대신 서비스를 사용하도록 수정함
  • 앞으로 작성할 컨트롤러들도 리포지토리를 직접 사용하지 않고 Controller -> Service -> Repository 구조로 데이터를 처리할 예정
package com.gdsc.webboard.question;

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.PathVariable;

import java.util.List;

@RequiredArgsConstructor //questionRepository 속성을 포함하는 생성자를 자동으로 롬복에서 생성
@Controller
public class QuestionController {

    private final QuestionService questionService;

    @GetMapping("/question/list")
    public String list(Model model){ //모델: 컨트롤러에서 html 템플릿에 데이터를 넘기고 싶을때 사용
        List<Question> questionList = this.questionService.getList();
        model.addAttribute("questionList", questionList);
        return "question_list";
    }



5. 질문 상세

🔆 질문 목록 템플릿(question_list.html)에 상세 링크 추가

  • 질문 목록의 제목을 클릭했을 때 상세 화면이 호출되도록 제목에 링크를 추가
  • th:href 속성: URL 주소를 나타낼때는 반드시 @{ 문자와 } 문자 사이에 입력
    • 타임리프는 문자열을 연결(concatenation)할 때 | 문자를 사용
<table>
    <thead>
        <tr>
            <th>제목</th>
            <th>작성일시</th>
        </tr>
    </thead>
    <tbody>
    <tr th:each="question, index : ${questionList}">
        <td>
            <a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
        </td>
        <td th:text="${question.createDate}"></td>
    </tr>
    </tbody>
</table>

🔆 질문 상세 템플릿(question_detail.html) 만들기

<!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}"></div>

</body>
</html>

🔆 QuestionService에 질문 조회 기능 추가

  • id 값으로 Question 데이터를 조회
  • 리포지토리로 얻은 Question 객체는 Optional이기 때문에 isPresent 메서드로 해당 데이터가 존재하는지 검사하는 로직이 필요
  • 만약 id 값에 해당하는 Question 데이터가 없을 경우에는 DataNotFoundException이 발생
package com.gdsc.webboard.question;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@RequiredArgsConstructor
@Service
public class QuestionService {
    private final QuestionRepository questionRepository;

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

    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 클래스

  • RuntimeException을 상속
  • 만약 DataNotFoundException이 발생하면 @ResponseStatus 애너테이션에 의해 404 오류(HttpStatus.NOT_FOUND)가 나타남
package com.gdsc.webboard.question;

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

    }
}

🔆 질문 상세 컨트롤러 만들기

  • @PathVariable 어노테이션
    • URI값에 가변형 변수를 전달 받아 처리하는 방식
    • 사용법
      • @GetMapping(PostMapping, PutMapping 등 다 상관없음)에 {변수명}
      • 메소드 정의에서 위에 쓴 변수명을 그대로 @PathVariable("변수명")
      • 즉, @GetMapping(value = "/question/detail/{id}") 에서 사용한 id와  @PathVariable("id")의 매개변수 이름이 동일해야 함
  • QuestionService의 getQuestion 메서드를 호출하여 Question 객체를 템플릿에 전달
package com.gdsc.webboard.question;

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.PathVariable;

import java.util.List;

@RequiredArgsConstructor //questionRepository 속성을 포함하는 생성자를 자동으로 롬복에서 생성
@Controller
public class QuestionController {

    private final QuestionService questionService;
		(... 생략 ...)
     @GetMapping("/question/detail/{id}")
    public String detail(Model model, @PathVariable("id") Integer id){ //모델: 컨트롤러에서 html 템플릿에 데이터를 넘기고 싶을때 사용
        Question question = this.questionService.getQuestion(id);
        model.addAttribute("question", question);
        return "question_detail";
    }
}



6. URL 프리픽스(prefix)

  • @RequestMapping("/question")
    • 경로를 /question으로 시작을 한다는 의미
@RequestMapping("/question")
@RequiredArgsConstructor //questionRepository 속성을 포함하는 생성자를 자동으로 롬복에서 생성
@Controller
public class QuestionController {

    private final QuestionService questionService;

    @GetMapping("/list")
    public String list(Model model){ //모델: 컨트롤러에서 html 템플릿에 데이터를 넘기고 싶을때 사용
        List<Question> questionList = this.questionService.getList();
        model.addAttribute("questionList", questionList);
        return "question_list";
    }

    @GetMapping("/detail/{id}")
    public String detail(Model model, @PathVariable("id") Integer id){ //모델: 컨트롤러에서 html 템플릿에 데이터를 넘기고 싶을때 사용
        Question question = this.questionService.getQuestion(id);
        model.addAttribute("question", question);
        return "question_detail";
    }
}
profile
소통하는 개발자가 꿈입니다!

0개의 댓글