๐Ÿ“„ ์ƒ์„ธํŽ˜์ด์ง€ ๋งŒ๋“ค๊ธฐ 1 (URL ํŒŒ๋ผ๋ฏธํ„ฐ, Optional, @PathVariable)

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

Spring Boot

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

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

  • @GetMapping("/detail/{id}") + @PathVariable๋กœ ํ•˜๋‚˜์˜ API๋กœ ์—ฌ๋Ÿฌ ์ƒํ’ˆ ์ƒ์„ธ ์ฒ˜๋ฆฌ
  • itemRepository.findById(id)๋Š” Optional์„ ๋ฐ˜ํ™˜ํ•˜๋ฏ€๋กœ ๋ถ€์žฌ ์ผ€์ด์Šค๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ
  • Model๋กœ ์กฐํšŒ ๊ฒฐ๊ณผ๋ฅผ ํ…œํ”Œ๋ฆฟ์— ๋ฐ”์ธ๋”ฉํ•ด detail.html ๋ Œ๋”๋ง

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

  • /detail/{id} ์š”์ฒญ์— ๋Œ€ํ•ด ํ•ด๋‹น ์ƒํ’ˆ์˜ ์ƒ์„ธ๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ
  • URL์˜ {id} ๊ฐ’์„ ๋ฐ›์•„ DB์—์„œ ๋‹จ๊ฑด ์กฐํšŒ
  • ์กฐํšŒ ์‹คํŒจ ์‹œ 404/๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋“ฑ ์•ˆ์ „ ์ฒ˜๋ฆฌ

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

  • ๋ชฉ๋ก โ†’ ์ƒ์„ธ(1๊ฑด) ํŒจํ„ด์€ ๋ชจ๋“  ์„œ๋น„์Šค์˜ ๊ธฐ๋ณธ
  • URL ํŒŒ๋ผ๋ฏธํ„ฐ, Optional ์•ˆ์ „ ์ฒ˜๋ฆฌ ํŒจํ„ด์€ ์‹ค๋ฌด ๋นˆ์ถœ
  • ์ดํ›„ ์žฅ๋ฐ”๊ตฌ๋‹ˆ, ์ฃผ๋ฌธ, ์ˆ˜์ •/์‚ญ์ œ๋กœ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ™•์žฅ

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

๊ฐœ๋…์„ค๋ช…
URL ํŒŒ๋ผ๋ฏธํ„ฐ/detail/{id}์ฒ˜๋Ÿผ ๊ฒฝ๋กœ ์ผ๋ถ€๋ฅผ ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š” ๋ฐฉ์‹
@PathVariable๊ฒฝ๋กœ ๋ณ€์ˆ˜ ๊ฐ’์„ ๋ฉ”์„œ๋“œ ์ธ์ž๋กœ ๋ฐ”์ธ๋”ฉ
Optional<T>๊ฐ’์ด ์—†์„ ์ˆ˜ ์žˆ์Œ์„ ํ‘œํ˜„ํ•˜๋Š” ์ปจํ…Œ์ด๋„ˆ
findById(id)Optional<Entity> ๋ฐ˜ํ™˜ (JPA ๊ธฐ๋ณธ)
Model์ปจํŠธ๋กค๋Ÿฌ โ†’ ๋ทฐ๋กœ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ
templates/*.htmlreturn "๋ทฐ๋ช…"์œผ๋กœ Thymeleaf ๋ Œ๋”๋ง

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

๐Ÿ“„ 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.*;

@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 item = itemRepository.findById(id)
                .orElse(null); // ํ•„์š” ์‹œ orElseThrow๋กœ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
        
        if (item == null) {
            return "redirect:/list"; // ๋˜๋Š” 404 ํŽ˜์ด์ง€๋กœ
        }
        model.addAttribute("item", item);
        return "detail"; // templates/detail.html
    }
}
  • {id} ์ž๋ฆฌ์— ์˜ค๋Š” ์ˆซ์ž๋ฅผ @PathVariable Long id๋กœ ๋ฐ›๋Š”๋‹ค
  • findById(id) ๊ฒฐ๊ณผ๋Š” Optional<Item> โ†’ ๋ถ€์žฌ ์ผ€์ด์Šค ์ฒ˜๋ฆฌ
  • ๋ชจ๋ธ์— ๋‹ด์•„ detail.html์—์„œ ${item.*}๋กœ ์ ‘๊ทผ

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

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title th:text="${item.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="${item.title}">์ƒํ’ˆ๋ช…</h4>
    <p th:text="${#numbers.formatInteger(item.price, 0, 'COMMA')}">๊ฐ€๊ฒฉ</p>
  </div>
</body>
</html>
  • ${item.title}, ${item.price}๋กœ ๋ฐ”์ธ๋”ฉ๋œ ๋ฐ์ดํ„ฐ ์ถœ๋ ฅ
  • th:replace๋กœ ๊ณตํ†ต Navbar ์žฌ์‚ฌ์šฉ

๐Ÿ“„ 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>
  <ul>
    <li th:each="it : ${items}">
      <a th:href="@{|/detail/${it.id}|}" th:text="${it.title}">์ƒํ’ˆ๋ช…</a>
      <span th:text="${it.price}">๊ฐ€๊ฒฉ</span>
    </li>
  </ul>
</body>
</html>
  • ๊ฐ ํ•ญ๋ชฉ์— /detail/{id} ๋งํฌ ์—ฐ๊ฒฐ

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

ํ‚ค์›Œ๋“œํ•œ ์ค„ ์š”์•ฝ
@GetMapping("/detail/{id}")์ƒ์„ธํŽ˜์ด์ง€ ๋ผ์šฐํŒ…
@PathVariableURL ๊ฒฝ๋กœ ๊ฐ’์„ ์ธ์ž๋กœ ์ˆ˜์‹ 
Optional๊ฐ’ ๋ถ€์žฌ๋ฅผ ํ‘œํ˜„, NPE ๋ฐฉ์ง€
findById๋‹จ๊ฑด ์กฐํšŒ ๋ฉ”์„œ๋“œ
Modelํ…œํ”Œ๋ฆฟ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ

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

  • ์ƒํ’ˆ/๊ฒŒ์‹œ๊ธ€/ํ”„๋กœํ•„ ์ƒ์„ธ
  • ๊ณต์ง€/์ฃผ๋ฌธ/๋Œ“๊ธ€ ๋‹จ๊ฑด ์กฐํšŒ
  • ๋ชฉ๋ก โ†’ ์ƒ์„ธ โ†’ ์ˆ˜์ •/์‚ญ์ œ ํ‘œ์ค€ ํ๋ฆ„

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

  • ํ•˜๋‚˜์˜ API๋กœ N๊ฐœ์˜ ์ƒ์„ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด URL ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ํ•„์ˆ˜
  • Optional๋กœ โ€œ์—†์Œโ€์„ ๋ช…์‹œ์ ์œผ๋กœ ๋‹ค๋ฃจ๋ฉด ์•ˆ์ •์„ฑ์ด ์˜ฌ๋ผ๊ฐ„๋‹ค
  • ํ…œํ”Œ๋ฆฟ ๋ฐ”์ธ๋”ฉ์œผ๋กœ UI/๋ฐ์ดํ„ฐ ๋ถ„๋ฆฌ๊ฐ€ ์ž์—ฐ์Šค๋Ÿฌ์›Œ์ง„๋‹ค

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