겉바속촉

[점프투스프링부트] 2-10. 질문 상세 본문

IT 일기 (상반기)/SPRING 기초

[점프투스프링부트] 2-10. 질문 상세

겉바속촉 2023. 2. 28. 13:54
728x90
반응형

 

점프투스프링부트 2-10

 

 

 

목표 : 질문 상세 기능 추가

 

 

1. 질문 상세 링크 추가하기

먼저 질문 목록의 제목을 클릭했을때 상세화면이 호출되도록 제목에 링크를 추가하자.

질문 목록 템플릿을 다음과 같이 수정하자.

 

[파일명: /sbb/src/main/resources/templates/question_list.html]

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

 

제목을 <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)할 때 | 문자를 사용한다.

 

 

 

 

2. 질문 상세 컨트롤러 만들기

이제 질문 목록 페이지에 접속하여 링크를 클릭해 보자. 아마도 다음과 같은 오류가 발생할 것이다.

 

 

 

 

http://localhost:8080/question/detail/2 URL 요청에 대한 매핑이 없기 때문에 404(Page not found) 오류가 발생한다.

오류를 해결하기 위해 질문 상세 페이지에 대한 URL 매핑을 QuestionController에 다음과 같이 추가하자.

 

 

[파일명:/sbb/src/main/java/com/mysite/sbb/question/QuestionController.java]

(... 생략 ...)
import org.springframework.web.bind.annotation.PathVariable;
(... 생략 ...)
public class QuestionController {

    (... 생략 ...)

    @GetMapping(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 애너테이션을 사용해야 한다.

 

이 때 @GetMapping(value = "/question/detail/{id}") 에서 사용한 id와 @PathVariable("id")의 매개변수 이름이 동일해야 한다.

 

위와 같이 수정하고 다시 URL을 호출하면 이번에는 404 대신 500 오류가 발생할 것이다.

왜냐하면 응답으로 리턴한 question_detail 템플릿이 없기 때문이다.

 

다음과 같이 question_detail.html 파일을 신규로 작성하자.

 

[파일명:/sbb/src/main/resources/templates/question_detail.html]

<h1>제목</h1>
<div>내용</div>

그러면 이제 오류없이 다음과 같은 화면이 나타날 것이다.

 

 

 

 

3. 서비스

이제 화면에 출력한 "제목", "내용" 문자열 대신 데이터의 실제 제목과 내용을 출력해 보자.

Question 데이터를 조회하기 위해서 QuestionService를 다음과 같이 수정하자.

 

[파일명:/sbb/src/main/java/com/mysite/sbb/question/QuestionService.java]

(... 생략 ...)
import java.util.Optional;
import com.mysite.sbb.DataNotFoundException;
(... 생략 ...)
public class QuestionService {

    (... 생략 ...)

    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 값으로 Question 데이터를 조회하는 getQuestion 메서드를 추가했다.

리포지터리로 얻은 Question 객체는 Optional 객체이기 때문에 위와 같이 isPresent 메서드로 해당 데이터가 존재하는지 검사하는 로직이 필요하다.

 

만약 id 값에 해당하는 Question 데이터가 없을 경우에는 DataNotFoundException을 발생시키도록 했다. DataNotFoundException 클래스는 아직 존재하지 않기 때문에 컴파일 오류가 발생할 것이다.

DataNotFoundException 클래스를 다음과 같이 작성하자.

 

[파일명: /sbb/src/main/java/com/mysite/sbb/DataNotFoundException.java]

package com.mysite.sbb;

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)가 나타날 것이다.

 

그리고 QuestionController에서 QuestionService의 getQuestion 메서드를 호출하여 Question 객체를 템플릿에 전달할 수 있도록 다음과 같이 수정하자.

 

[파일명:/sbb/src/main/java/com/mysite/sbb/question/QuestionController.java]

(... 생략 ...)
public class QuestionController {

    (... 생략 ...)

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

 

 

 

4. 템플릿

QuestionController의 detail 메서드에서 Model 객체에 "question" 이라는 이름으로 Question 객체를 저장했으므로 템플릿은 다음과 같이 수정할 수 있다.

 

[파일명:/sbb/src/main/resources/templates/question_detail.html]

<h1 th:text="${question.subject}"></h1>
<div th:text="${question.content}"></div>

 

 

 

5. 질문 상세 확인하기

이제 다시 질문 상세 페이지를 요청해 보자. 다음과 같은 화면이 나타날 것이다.

 

 

조회한 Question 데이터의 제목과 내용이 화면에 잘 출력된 것을 확인할 수 있다.

이번에는 다음처럼 33과 같은 존재하지 않는 id 값으로 페이지를 요청해 보자.

http://localhost:8080/question/detail/33

 

 

다음처럼 존재하지 않는 데이터를 조회하려고 할 경우에는 404 Not found 오류가 발생하는 것을 확인할 수 있다.

 

 

 

6. URL 프리픽스(prefix)

 

다음으로 넘어가기 전에 QuestionController의 URL 매핑을 잠시 살펴보자.

현재 QuestionController에는 다음 2개의 URL 매핑이 있다.

  1. @GetMapping("/question/list")
  2. @GetMapping(value = "/question/detail/{id}")

URL 매핑시 value 매개변수는 생략할수 있다.

 

URL의 프리픽스가 모두 /question으로 시작한다는 것을 알수 있다.

이런 경우 클래스명 위에 @RequestMapping("/question") 애너테이션을 추가하고

메서드 단위에서는 /question 를 생략한 그 뒷 부분만을 적으면 된다.

 

다음과 같이 QuestionController를 수정해 보자.

 

[파일명:/sbb/src/main/java/com/mysite/sbb/question/QuestionController.java]

(... 생략 ...)
import org.springframework.web.bind.annotation.RequestMapping;
(... 생략 ...)

@RequestMapping("/question")
@RequiredArgsConstructor
@Controller
public class QuestionController {

    private final QuestionService questionService;

    @GetMapping("/list")
    public String list(Model model) {
        (... 생략 ...)
    }

    @GetMapping(value = "/detail/{id}")
    public String detail(Model model, @PathVariable("id") Integer id) {
        (... 생략 ...)
    }
}

 

list 메서드의 URL 매핑은 /list 이지만 클래스에 /question이라는 URL 매핑이 있기 때문에

 /question + /list가 되어 최종적인 URL 매핑은 /question/list가 된다.

 

위와 같이 수정하면 기존과 완전히 동일한 기준으로 URL 매핑이 이루어 진다.

다만, 앞으로 QuestionController에서 사용하는 URL 매핑은 항상 /question 으로 시작해야 하는 규칙이 생긴 것이다.

 

 

 

 

728x90
반응형