๐Ÿ“„ ์ƒ์„ธํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ 2 & ์˜ˆ์™ธ์ฒ˜๋ฆฌ (Thymeleaf, @PathVariable, Optional, error.html)

EthAnalogยท2025๋…„ 8์›” 20์ผ

Spring Boot

๋ชฉ๋ก ๋ณด๊ธฐ
14/16
post-thumbnail

๐Ÿ”‘ ์˜ค๋Š˜ ๋ฐฐ์šด ํ•ต์‹ฌ 3์ค„ ์š”์•ฝ

  1. @GetMapping("/detail/{id}") + @PathVariable๋กœ URL์˜ id๋ฅผ ๋ฐ›์•„ ๋‹จ๊ฑด ์ƒ์„ธ ์ฒ˜๋ฆฌ
  2. itemRepository.findById(id)๋Š” Optional โ†’ ๋ถ€์žฌ ์‹œ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ/404๋กœ ์•ˆ์ „ ์ฒ˜๋ฆฌ
  3. ์Šคํ”„๋ง + ํƒ€์ž„๋ฆฌํ”„๋Š” templates/error.html๋งŒ ๋‘ฌ๋„ ๊ธฐ๋ณธ ์—๋Ÿฌ ํŽ˜์ด์ง€ ์ปค์Šคํ…€ ๊ฐ€๋Šฅ

โœ… ์–ด๋–ค ๊ธฐ๋Šฅ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์ธ๊ฐ€?

  • /detail/{id}๋กœ ๋“ค์–ด์˜จ ์š”์ฒญ๋ณ„๋กœ ๊ฐ ์ƒํ’ˆ์˜ ์ƒ์„ธ ํ™”๋ฉด ๋ Œ๋”๋ง
  • ๋ชฉ๋ก(list)์—์„œ ์ƒํ’ˆ๋ช… ํด๋ฆญ โ†’ ์ƒ์„ธ๋กœ ์ด๋™ ๋งํฌ ์ƒ์„ฑ
  • ์ž˜๋ชป๋œ ์ž…๋ ฅ/๋ฐ์ดํ„ฐ ์—†์Œ/์„œ๋ฒ„ ์—๋Ÿฌ ๋“ฑ ์˜ˆ์™ธ ์ƒํ™ฉ ์ฒ˜๋ฆฌ

๐Ÿ‘‰ ์™œ ์ด๊ฑธ ๋ฐฐ์›Œ์•ผ ํ•˜์ง€?

  • ์‹ค๋ฌด ํ‘œ์ค€ ํ๋ฆ„: ๋ชฉ๋ก โ†’ ์ƒ์„ธ(1๊ฑด) + ์ฒ ์ €ํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
  • URL ํŒŒ๋ผ๋ฏธํ„ฐ ์ฒ˜๋ฆฌ/Optional ์‚ฌ์šฉ/์—๋Ÿฌ ํŽ˜์ด์ง€ ์ปค์Šคํ…€์€ ๋ฉด์ ‘ ๋‹จ๊ณจ + ์‹ค๋ฌด ํ•„์ˆ˜

๐Ÿ“š ๊ฐœ๋… ์ •๋ฆฌ

๊ฐœ๋…์„ค๋ช…
@PathVariable/detail/{id} ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์„ ๋ฉ”์„œ๋“œ ์ธ์ž๋กœ ๋ฐ”์ธ๋”ฉ
Optional<T>๊ฐ’ ๋ถ€์žฌ ๊ฐ€๋Šฅ์„ฑ ํ‘œํ˜„(NPE ๋ฐฉ์ง€). isPresent, orElseThrow ๋“ฑ ์ œ๊ณต
Model์ปจํŠธ๋กค๋Ÿฌ โ†’ ํ…œํ”Œ๋ฆฟ์œผ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ์ปจํ…Œ์ด๋„ˆ
th:href, @{}ํƒ€์ž„๋ฆฌํ”„์—์„œ ์•ˆ์ „ํ•œ URL ์ƒ์„ฑ
error.htmltemplates/error.html์ด ์กด์žฌํ•˜๋ฉด ์Šคํ”„๋ง์ด ์—๋Ÿฌ ์‹œ ์ด ๋ทฐ๋ฅผ ๋ Œ๋”

โš™๏ธ ๊ตฌํ˜„ ํ๋ฆ„ ๋ฐ ์ฝ”๋“œ

1) ์ƒ์„ธ ์ปจํŠธ๋กค๋Ÿฌ: URL ํŒŒ๋ผ๋ฏธํ„ฐ + Optional + Model

๐Ÿ“„ src/main/java/com/apple/shop/ItemController.java

package com.apple.shop;

import com.apple.shop.item.Item;
import com.apple.shop.item.ItemRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class ItemController {

    private final ItemRepository itemRepository;
    public ItemController(ItemRepository itemRepository) {
        this.itemRepository = itemRepository;
    }

    @GetMapping("/detail/{id}")
    public String detail(@PathVariable Long id, Model model) {
        var optional = itemRepository.findById(id); // Optional<Item>
        if (optional.isPresent()) {
            model.addAttribute("data", optional.get());
            return "detail"; // templates/detail.html
        } else {
            return "redirect:/list"; // or return "error/404" ๋“ฑ
        }
    }
}
  • {id} ๊ฒฝ๋กœ์˜ ์ˆซ์ž๋ฅผ @PathVariable Long id๋กœ ๋ฐ›์Œ
  • findById(id) ๊ฒฐ๊ณผ๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ Optional ์•ˆ์ „ ์ฒ˜๋ฆฌ
  • model.addAttribute("data", ...) โ†’ ๋ทฐ์—์„œ ${data.*}๋กœ ์ ‘๊ทผ

2) detail.html: ๋ชจ๋ธ ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ

๐Ÿ“„ src/main/resources/templates/detail.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title th:text="${data.title}">์ƒ์„ธํŽ˜์ด์ง€</title>
  <style>
    .detail { text-align: center; }
    .detail img { max-width: 30%; display: block; margin: auto; }
  </style>
</head>
<body>
  <div th:replace="~{nav.html :: navbar}"></div>

  <div class="detail">
    <h4>์ƒ์„ธํŽ˜์ด์ง€</h4>
    <img src="https://placehold.co/300" alt="thumbnail">
    <h4 th:text="${data.title}">์ƒํ’ˆ๋ช…</h4>
    <p th:text="${#numbers.formatInteger(data.price, 0, 'COMMA') + '์›'}">๊ฐ€๊ฒฉ</p>
  </div>
</body>
</html>
  • ${data.title}, ${data.price} ๋“ฑ์œผ๋กœ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋„˜๊ธด ๊ฐ’ ์ถœ๋ ฅ
  • th:replace๋กœ ๊ณตํ†ต Navbar ์‚ฝ์ž…

3) ๋ชฉ๋ก์—์„œ ์ƒ์„ธ๋กœ ๋งํฌ ๋งŒ๋“ค๊ธฐ

๐Ÿ“„ src/main/resources/templates/list.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>๋ชฉ๋ก</title></head>
<body>
  <div th:replace="~{nav.html :: navbar}"></div>

  <h3>์ƒํ’ˆ ๋ชฉ๋ก</h3>
  <div class="card" th:each="i : ${items}">
    <img src="https://placehold.co/300">
    <h4 th:text="${i.title}">์ƒํ’ˆ๋ช…</h4>

    <!-- ๋ฐฉ๋ฒ• 1: ๋ฌธ์ž์—ด ์—ฐ๊ฒฐ -->
    <a th:href="@{'/detail/' + ${i.id}}">์ƒ์„ธ๋ณด๊ธฐ</a>

    <!-- ๋ฐฉ๋ฒ• 2: ํ…œํ”Œ๋ฆฟ ๋ฆฌํ„ฐ๋Ÿด ์Šคํƒ€์ผ(๊ฐ€๋…์„ฑ โ†‘) -->
    <!-- <a th:href="@{|/detail/${i.id}|}">์ƒ์„ธ๋ณด๊ธฐ</a> -->

    <p th:text="${#numbers.formatInteger(i.price, 0, 'COMMA') + '์›'}">๊ฐ€๊ฒฉ</p>
  </div>
</body>
</html>
  • ๋ชจ๋“  ์นด๋“œ๊ฐ€ /detail/1๋กœ ๊ฐ€๋Š” ์‹ค์ˆ˜๋ฅผ ๋ฐฉ์ง€ โ†’ ์ƒํ’ˆ๋ณ„ id๋กœ ๋งํฌ ์ƒ์„ฑ
  • @{} ๊ตฌ๋ฌธ์€ ์ปจํ…์ŠคํŠธ ๋ฃจํŠธ๊นŒ์ง€ ๊ณ ๋ คํ•ด ์•ˆ์ „ํ•œ URL ์ƒ์„ฑ

4) ์˜ˆ์™ธ ์ƒํ™ฉ ์ฒ˜๋ฆฌ (์ž…๋ ฅ ์˜ค๋ฅ˜/๋ฐ์ดํ„ฐ ์—†์Œ/์„œ๋ฒ„ ์—๋Ÿฌ)

(A) ์ž˜๋ชป๋œ ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ํƒ€์ž…(์˜ˆ: /detail/abc)

  • abc๋ฅผ Long์œผ๋กœ ๋ณ€ํ™˜ํ•˜์ง€ ๋ชปํ•ด 400/500 ์—๋Ÿฌ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
  • ์Šคํ”„๋ง์€ ๊ธฐ๋ณธ ์—๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ์ง€๋งŒ, ์‚ฌ์šฉ์„ฑ์„ ์œ„ํ•ด ์ปค์Šคํ…€ ์—๋Ÿฌ ํŽ˜์ด์ง€ ์ œ๊ณต์ด ๋ฐ”๋žŒ์ง

(B) ์กด์žฌํ•˜์ง€ ์•Š๋Š” id(์˜ˆ: /detail/9999)

  • findById ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Œ โ†’ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ or 404 ํŽ˜์ด์ง€ ๋ Œ๋”

(C) ์„œ๋ฒ„ ์—๋Ÿฌ(์˜ˆ: DB ๋‹ค์šด, ์ œ์•ฝ์กฐ๊ฑด ์œ„๋ฐ˜)

  • ๋‚ด๋ถ€ ์˜ˆ์™ธ ๋ฐœ์ƒ โ†’ ์—๋Ÿฌ ํŽ˜์ด์ง€๋กœ ํฌ์›Œ๋”ฉ

5) ๊ธฐ๋ณธ ์—๋Ÿฌ ํŽ˜์ด์ง€ ์ปค์Šคํ…€(error.html)

๐Ÿ“„ src/main/resources/templates/error.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค</title></head>
<body>
  <div th:replace="~{nav.html :: navbar}"></div>

  <h2>๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</h2>
  <p th:text="'Status: ' + ${status}"></p>
  <p th:text="'Error: ' + ${error}"></p>
  <p th:text="'Path: ' + ${path}"></p>
  <p th:text="${message}"></p>
  <!-- ์šด์˜ ํ™˜๊ฒฝ์—์„  exception ๋…ธ์ถœ์€ ์ง€์–‘ -->
</body>
</html>
  • ํƒ€์ž„๋ฆฌํ”„ ์‚ฌ์šฉ ์‹œ, templates/error.html์ด ์กด์žฌํ•˜๋ฉด ์—๋Ÿฌ ์‹œ ์ž๋™ ๋ Œ๋”
  • ๊ธฐ๋ณธ ์ œ๊ณต๋˜๋Š” ${status}, ${error}, ${path}, ${message} ๋ณ€์ˆ˜ ํ™œ์šฉ
  • ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ์ƒ์„ธ ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋…ธ์ถœํ•˜์ง€ ์•Š๊ธฐ

ํ™•์žฅ: ์ƒํ™ฉ๋ณ„ ํŽ˜์ด์ง€๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด templates/error/404.html, templates/error/500.html์„ ์ถ”๊ฐ€ํ•˜๋ฉด ์ƒํƒœ ์ฝ”๋“œ์— ๋งž์ถฐ ์ž๋™ ๋งคํ•‘๋จ.


๐Ÿ“Œ ์‚ฌ์šฉ๋œ ๊ฐœ๋… ์š”์•ฝ

ํ‚ค์›Œ๋“œํ•œ ์ค„ ์š”์•ฝ
@PathVariable๊ฒฝ๋กœ ๋ณ€์ˆ˜๋กœ ์ƒ์„ธ ๋ผ์šฐํŒ…
Optional๊ฐ’ ๋ถ€์žฌ ์•ˆ์ „ ์ฒ˜๋ฆฌ ์ปจํ…Œ์ด๋„ˆ
Modelํ…œํ”Œ๋ฆฟ์— ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
th:href @{}์•ˆ์ „ํ•œ ๋งํฌ ์ƒ์„ฑ
error.html์Šคํ”„๋ง + ํƒ€์ž„๋ฆฌํ”„ ๊ธฐ๋ณธ ์—๋Ÿฌ ํŽ˜์ด์ง€ ์ปค์Šคํ…€

๐Ÿ’ก ์ด๋Ÿฐ ๊ณณ์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”

  • ์ƒํ’ˆ/๊ฒŒ์‹œ๊ธ€/๊ณต์ง€/์ฃผ๋ฌธ ์ƒ์„ธ ํ™”๋ฉด
  • 404/500 ๋“ฑ ์ƒํƒœ๋ณ„ ์—๋Ÿฌ ํŽ˜์ด์ง€ ์ปค์Šคํ…€
  • ๋ชฉ๋ก โ†’ ์ƒ์„ธ โ†’ ์ˆ˜์ •/์‚ญ์ œ์˜ ํ‘œ์ค€ ํ”Œ๋กœ์šฐ ์•ˆ์ •ํ™”

โœ๏ธ ๊ฐœ์ธ ์ •๋ฆฌ ๋ฐ ํšŒ๊ณ 

  • ์ƒ์„ธ ํŽ˜์ด์ง€๋Š” URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์œ ์—ฐํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒŒ ์ •์„.
  • Optional๋กœ ์—†์Œ์„ ๋จผ์ € ์„ค๊ณ„ํ•˜๋ฉด ์˜ˆ์™ธ๊ฐ€ ๋‹จ์ˆœํ•ด์ง„๋‹ค.
  • ์—๋Ÿฌ ํŽ˜์ด์ง€๋ฅผ ๊น”๋”ํžˆ ์ปค์Šคํ…€ํ•˜๋ฉด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋ˆˆ์— ๋„๊ฒŒ ์ข‹์•„์ง„๋‹ค.

0๊ฐœ์˜ ๋Œ“๊ธ€