참고 링크 : https://wikidocs.net/book/7601
오라클DB와 intelliJ로 작업하였습니다
질문 상세 링크 추가하기
먼저 질문 목록의 제목을 클릭했을때 상세화면이 호출되도록 제목에 링크를 추가하자. 질문 목록 템플릿을 다음과 같이 수정하자.
src/main/resources/templates/question_list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="//www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>질문 목록</title>
</head>
<body>
<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>
</body>
</html>
제목을 <td> 엘리먼트의 텍스트로 출력하던 것에서 링크로 변경했다.
타임리프에서 링크의 주소는 th:href 속성을 사용한다. 타임리프에서 th:href 처럼 URL 주소를 나타낼때는
반드시 @{ 문자와 } 문자 사이에 입력해야 한다. 그리고 URL 주소는 문자열 /question/detail/과 ${question.id} 값이 조합되어 /question/detail/${question.id}로 만들어졌다. 이때 좌우에 | 문자없이 다음과 같이 사용하면 오류가 발생한다.
<a th:href="@{/question/detail/${question.id}}" th:text="${question.subject}"></a>
/question/detail/과 같은 문자열과 ${question.id}와 같은 자바 객체의 값을 더할 때는 반드시 다음처럼 |과 | 기호로 좌우를 감싸 주어야 한다.
<a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
타임리프는 문자열을 연결(concatenation)할 때 | 문자를 사용한다.
http://localhost:8080/question/detail/2 URL 요청에 대한 매핑이 없기 때문에 404(Page not found) 오류가 발생한다. 오류를 해결하기 위해 질문 상세 페이지에 대한 URL 매핑을 QuestionController에 다음과 같이 추가하자.
package com.gosari.repick_project.question;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
@RequestMapping("/question/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
@RequestMapping(value = "/question/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
return "question_detail";
}
}
요청 URL http://localhost:8080/question/detail/2의
숫자 2처럼 변하는 id 값을 얻을 때에는 위와 같이 @PathVariable 애너테이션을 사용해야 한다.
이 때 @RequestMapping(value = "/question/detail/{id}") 에서
사용한 id와 @PathVariable("id")의 매개변수 이름이 동일해야 한다.
위와 같이 수정하고 다시 URL을 호출하면 이번에는 404 대신 500 오류가 발생할 것이다. 왜냐하면 응답으로 리턴한 question_detail 템플릿이 없기 때문이다. 다음과 같이 question_detail.html 파일을 신규로 작성하자.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>질문 상세</title>
</head>
<body>
<h1>제목</h1>
<div>내용</div>
</body>
</html>
그러면 이제 오류없이 다음과 같은 화면이 나타날 것이다.
서비스
이제 화면에 출력한 "제목", "내용" 문자열 대신 데이터의 실제 제목과 내용을 출력해 보자.
Question 데이터를 조회하기 위해서 QuestionService를 다음과 같이 수정하자.
package com.gosari.repick_project.question;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
import com.gosari.repick_project.DataNotFoundException;
@RequiredArgsConstructor //QuestionRepository 생성자 생성해줌
@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");
}
}
}
- id 값으로 데이터를 조회하기 위해서는 리포지터리의 findById 메서드를 사용해야 한다.
- 하지만 findById의 리턴 타입은 Question이 아닌 Optional임에 주의하자.
- Optional은 null 처리를 유연하게 처리하기 위해 사용하는 클래스로
- isPresent로 null이 아닌지를 확인한 후에 get으로 실제 Question 객체 값을 얻어야 한다.
id 값으로 Question 데이터를 조회하는 getQuestion 메서드를 추가했다.
리포지터리로 얻은 Question 객체는 Optional 객체이기 때문에 위와 같이 isPresent 메서드로 해당 데이터가 존재하는지 검사하는 로직이 필요하다. 만약 id 값에 해당하는 Question 데이터가 없을 경우에는 DataNotFoundException을 발생시키도록 했다.
DataNotFoundException 클래스는 아직 존재하지 않기 때문에 컴파일 오류가 발생할 것이다.
DataNotFoundException 클래스를 다음과 같이 작성하자.
package com.gosari.repick_project.Exception;
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)가 나타날 것이다.
@ResponseStatus는 Controller나 Exception에 사용하여 status 정보를 설정하여 리턴해 준다.
그리고 QuestionController에서 QuestionService의 getQuestion 메서드를 호출하여 Question 객체를 템플릿에 전달할 수 있도록 다음과 같이 수정하자.
package com.gosari.repick_project.question;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/question/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(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";
}
}
템플릿
QuestionController의 detail 메서드에서 Model 객체에 "question" 이라는 이름으로 Question 객체를 저장했으므로 템플릿은 다음과 같이 수정할 수 있다.
<!DOCTYPE html>
<html lang="en" xmlns:th="//www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>질문 상세</title>
</head>
<body>
<h1 th:text="${question.subject}"></h1>
<div th:text="${question.content}"></div>
</body>
</html>
질문 상세 확인하기
이제 다시 질문 상세 페이지를 요청해 보자. 다음과 같은 화면이 나타날 것이다.
조회한 Question 데이터의 제목과 내용이 화면에 잘 출력된 것을 확인할 수 있다. 이번에는 다음처럼 33과 같은 존재하지 않는 id 값으로 페이지를 요청해 보자.
다음처럼 존재하지 않는 데이터를 조회하려고 할 경우에는 404 Not found 오류가 발생하는 것을 확인할 수 있다.
URL 프리픽스(prefix)
다음으로 넘어가기 전에 QuestionController의 URL 매핑을 잠시 살펴보자.
현재 QuestionController에는 다음 2개의 URL 매핑이 있다.
- @RequestMapping("/question/list")
- @RequestMapping(value = "/question/detail/{id}")
URL 매핑시 value 매개변수는 생략할수 있다.
URL의 프리픽스가 모두 /question으로 시작한다는 것을 알수 있다. 이런 경우 클래스명 위에 @RequestMapping("/question") 애너테이션을 추가하고
메서드 단위에서는 /question 를 생략한 그 뒷 부분만을 적으면 된다.
다음과 같이 QuestionController를 수정해 보자.
package com.gosari.repick_project.question;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.List;
@RequestMapping("/question") //URL프리픽스(prefix)
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
/*질문리스트*/
@RequestMapping("/list")
public String list(Model model){
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
/*질문상세*/
@RequestMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id);
model.addAttribute("question", question);
return "question_detail";
}
}
list 메서드의 URL 매핑은 /list 이지만 클래스에 /question이라는 URL 매핑이 있기 때문에 /question + /list가 되어 최종적인 URL 매핑은 /question/list가 된다. 위와 같이 수정하면 기존과 완전히 동일한 기준으로 URL 매핑이 이루어 진다. 다만, 앞으로 QuestionController에서 사용하는 URL 매핑은 항상 /question 으로 시작해야 하는 규칙이 생긴 것이다.
컨트롤러의 클래스 단위의 URL 매핑은 필수사항이 아니다. 컨트롤러의 성격에 맞게 사용하면 된다.
'Follow Work > SpringbootBoard' 카테고리의 다른 글
[StringBoot] 스태틱 디렉터리와 스타일시트 (9) (0) | 2022.08.12 |
---|---|
[StringBoot] 답변등록 (8) (0) | 2022.08.12 |
[StringBoot] 서비스 (6) (0) | 2022.08.11 |
[StringBoot] ROOT URL (5) (0) | 2022.08.11 |
[StringBoot] 질문 목록과 템플릿 (4) (0) | 2022.08.11 |