이 영역을 누르면 첫 페이지로 이동
천천히 꾸준히 조용히 블로그의 첫 페이지로 이동

천천히 꾸준히 조용히

페이지 맨 위로 올라가기

천천히 꾸준히 조용히

천천히 꾸준히 조용히.. i3months 블로그

[Spring Web MVC] Spring MVC와 Thymeleaf 예시

  • 2022.08.18 17:43
  • Spring/Spring Web MVC
반응형

 

 

 

스프링 MVC를 사용해 아주 간단한 쇼핑몰 프로젝트를 진행해보자.

 

 

 

 

여기서 검은색은 컨트롤러고, 컨트롤러를 통해 뷰를 호출한다.

 

이렇게 요구사항이 정리되면 디자이너 / 웹 퍼블리셔 / 백엔드 / 프론트엔드 개발자가 업무를 분담해서 프로젝트를 진행한다.

 

디자이너 : 디자인하고 결과물을 웹 퍼블리셔 혹은 프론트엔드 개발자에게 넘겨준다.

웹 퍼블리셔 : 디자인을 바탕으로 HTML CSS를 만들어 개발자에게 넘겨준다.

백엔드 : HTML CSS 가 나오기 전까지는 시스템을 설계하고 비즈니스 모델을 개발한다. 이후 화면이 나오면 뷰 템플릿으로 변환해 동적으로 화면을 구성하고 흐름을 제어한다.

 

프론트엔드 : 디자인을 바탕으로 HTML CSS를 만드는 역할도 하고, HTTP API를 통해 웹 클라이언트가 요구하는 데이터와 기능을 제공한다. 이 때 백엔드 개발자는 뷰 템플릿을 사용하지 않아도 된다.

 

 

 

//@Data
@Getter @Setter
public class Item {

    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    public Item(){

    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }

}

 

먼저 상품을 설계하자. 아이디 / 가격 / 수량을 가진다.

 

여기서 롬복의 @Data 애너테이션을 사용하면 Getter / Setter / RequiredArgConstructor / ToString / EqualsAndHash.. 등등 여러 가지가 자동으로 생성된다.

 

필요하지 않은 요소들을 추가하게 되면 의도하지 않은 동작이 발생할 수 있으니 조심하자. 필요한 요소만 가져다 쓰는게 합리적이다.

 

 

@Repository
public class ItemRepository {

    private static final Map<Long, Item> store = new HashMap<>(); // 실무에서는 ConcurrentHashMap
    private static long sequence = 0L;

    public Item save(Item item){
        item.setId(++sequence);
        store.put(item.getId(), item);
        return item;
    }

    public Item findById(Long id){
        return store.get(id);
    }

    public List<Item> findAll(){
        return new ArrayList<>(store.values());
    }

    public void update(Long id, Item updateParam){
        Item findItem = findById(id);
        findItem.setItemName(updateParam.getItemName());
        findItem.setPrice(updateParam.getPrice());
        findItem.setQuantity(updateParam.getQuantity());
        // 이런 작업은 DTO 를 통해 작업하는 편이 좋음.
    }

    public void clearStore(){
        store.clear();
    }
}

 

 

Repository 에는 몇 가지 비즈니스 로직과 데이터베이스에 접근하는 작업을 처리한다. (여기서는 Map으로 저장.)

 

 

@Controller
@RequestMapping("/basic/items")
//@RequiredArgsConstructor
public class BasicItemController {
    private final ItemRepository itemRepository;

    @Autowired // 생성자 하나면 생략 가능
    public BasicItemController(ItemRepository itemRepository){
        this.itemRepository = itemRepository;
    }

    @GetMapping
    public String items(Model model){
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "basic/items";
    }

    // for test
    @PostConstruct
    public void init(){
        itemRepository.save(new Item("zasdf", 1,2));
        itemRepository.save(new Item("z423asdf", 121,23));
    }

}

 

 

생성자가 하나 뿐이면 @Autowired 애너테이션을 생략할 수 있고, 롬복의 @RequiredArgsConstructor 애너테이션을 사용하면 final 이 붙은 멤버변수에 대해 생성자도 자동으로 만들어준다.

 

@Controller 애너테이션이 붙어있으니 GetMapping 으로 해당 URL 요청이 들어올 때 basic/items 이름의 뷰를 찾는다.

 

 

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <link th:href="@{/css/bootstrap.min.css}"
            href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>

<div class="container" style="max-width: 600px">
    <div class="py-5 text-center">
        <h2>상품 목록</h2>
    </div>
    <div class="row">
        <div class="col">
            <button class="btn btn-primary float-end"
                    onclick="location.href='addForm.html'"
                    th:onclick="|location.href='@{/basic/items/add}'|"
                    type="button">상품
                등록</button>
        </div>
    </div>
    <hr class="my-4">
    <div>
        <table class="table">
            <thead>
            <tr>
                <th>ID</th>
                <th>상품명</th>
                <th>가격</th>
                <th>수량</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="item : ${items}"> <!-- 모델에 있는 items를 꺼내온다. 프로퍼티 접근법을 사용-->
                <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId = ${item.id})}" th:text="${item.id}">회원 아이디</a></td>
                <td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId = ${item.id})}" th:text="${item.itemName}">상품명</a></td>
                <td th:text="${item.price}">가격</td>
                <td th:text="${item.quantity}">수량</td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>

 

 

 

동적 웹 페이지를 보여주기 위해 뷰 템플릿으로 타임리프를 사용하자.

 

<html xmlns: th="http://www.thymeleaf.org"> 로 타임리프 사용을 선언한다.

 

th:href 를 사용하면 뷰가 렌더링 될 때 기존의 href 값을 지정한 값으로 갈아치운다. (값이 없으면 새로 생성한다.)

대부분의 HTML 속성을 th: ~~~ 를 통해 변경할 수 있다.

 

HTML 페이지를 그대로 열어보면 th 가 적용되지 않고, 뷰 템플릿을 통해 열어보면 th: ~~~ 의 값이 ~~~ 로 대체되면서 HTML 값을 동적으로 변경할 수 있다. 

순수 HTML 속성을 그대로 유지하면서 뷰 템플릿도 사용할 수 있어 타임리프를 네츄럴 템플릿 이라고도 한다.

 

@{...} 를 통해 URL 링크를 표현할 수 있다. URL 링크 표현식이라고 부른다.

"@[/basic/items/{itemId}(itemId=${item.id})}" 링크 표현식을 사용해 경로를 템플릿처럼 편하게 사용할 수 있고, 쿼리 파라미터도 생성할 수 있다. (간단할 경우 리터럴 문법을 사용하자.)

 

타임리프에서는 문자와 표현식은 분리돼있어 더해서 사용해야 한다.

'abc' + ${alpha.d} + '!' 원래는 이런식으로 사용해야 된다. 하지만 리터럴 대체 문법인 | | 를 사용하면?

"|abc${alpha.d}|" 이런식으로 더하기 없이 편하게 사용할 수 있다.

 

th:each 를 통해 반복 출력을 구현할 수 있다.

자바의 forEach 문법과 비슷하게 사용한다.

 

${...} 를 통해 모델에서 값을 조회해서 사용할 수 있다. 

이 때 프로퍼티 접근법을 사용해 getter, setter를 자동으로 호출한다.

 

th:action HTML Form 을 통해 데이터를 전송할 때 action에 값이 없으면 현재 URL에 데이터를 전송한다.

상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 같게 하고 HTTP 메서드를 통해 두 요청을 구분하는 방식을 사용해 하나의 URL로 등록 폼과 등록 처리를 깔끔하게 처리할 수 있다.

 

 

    @PostMapping("/add")
    public String save(@RequestParam String itemName,
                       @RequestParam int price,
                       @RequestParam Integer quantity,
                       Model model){
        Item item = new Item(itemName, price, quantity);

        itemRepository.save(item);
        model.addAttribute("item", item);
        return "basic/item";
    }

 

상품을 추가하고 보여줄 때도 모델에 정보를 넣어주고 뷰를 재사용하면 된다.

 

 

    @PostMapping("/add")
    public String save(@ModelAttribute("item") Item item){


        itemRepository.save(item);        
        return "basic/item";
    }

 

@ModelAttribute 애너테이션을 사용하면 좀 더 간단하게 처리할 수 있다.

 

해당 객체를 만들어서 값을 넣어주고, 모델에 데이터를 넣어주는 역할을 처리해준다.

@ModelAttribute("item") 에서 item이 모델에서의 키 값으로 사용한다. (지정하지 않으면 클래스명의 첫글자를 소문자로 한 이름을 사용)

 

 

 

    @PostMapping("/add")
    public String addItemV4(Item item) {
        itemRepository.save(item);
        return "basic/item";
    }

 

 

극한으로 줄이면 @ModelAttribute 애너테이션도 생략할 수 있는데... 항상 적절하게 줄이도록 하자.

 

 

    @GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable Long itemId, Model model){
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "basic/editForm";
    }

    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
        itemRepository.update(itemId, item);
        return "redirect:/basic/items/{itemId}";
    }

 

 

상품 수정도 처리하는 URL을 같게 하고 요청 메서드 타입으로 구분했다.

 

Post 방식으로 해당 URL이 요청되면 @ModelAttribute 애너테이션으로 상품 정보를 받아오고 업데이트 후 상품 상세 정보 페이지로 리다이렉트된다.

 

 

 

 

 

새로고침은 마지막에 서버에 전송한 데이터를 다시 전송하는 작업이다.

상품 등록 폼에서 데이터를 입력하고 저장하면 POST / add 를 통해 서버에게 데이터를 전송한다.

여기서 새로고침을 하면? 다시 POST / add 를 통해 서버에게 데이터를 전송한다.

 

 

 

 

POST 로 상품을 등록하는 작업 뒤에 리다이렉트를 추가해서 문제를 해결할 수 있다.

 

이러면 새로고침을 한다고 해도 마지막으로 전송한 데이터가 리다이렉션이라 상품이 중복돼서 저장되지 않는다.

 

이런 방식을 PRG (Post Redirection Get) 라고 부른다. 

 

 

RedirectAttributes 객체를 파라미터로 넘겨 웹 페이지를 리다이렉트로 넘겼을 때 추가 정보를 전달할 수 있다.

 

@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
    Item savedItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", savedItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/basic/items/{itemId}";
}

 

 

URL 인코딩 / PathVariable 처리 / 쿼리 파라미터까지 처리해준다.

PathVariable 로 바인딩되는 값은 그대로 치환해서 사용하고, 나머지는 쿼리 파라미터로 사용한다. (?status = true)

 

타임리프를 사용하면 클라이언트의 요청 메세지의 쿼리 파라미터를 가져올 수 있고, 쿼리 파라미터의 값에 따라 HTML을 동적으로 변형할 수 있다. (th:if / param.~~~ 사용)

 

 

 

반응형
저작자표시 (새창열림)

'Spring > Spring Web MVC' 카테고리의 다른 글

[Spring Web MVC] Thymeleaf (2)  (0) 2022.08.20
[Spring Web MVC] Thymeleaf (1)  (0) 2022.08.19
[Spring Web MVC] 응답 정보 다루기  (0) 2022.08.17
[Spring Web MVC] 요청 정보 다루기  (0) 2022.08.17
[Spring Web MVC] Spring MVC  (0) 2022.08.16

댓글

이 글 공유하기

  • 구독하기

    구독하기

  • 카카오톡

    카카오톡

  • 라인

    라인

  • 트위터

    트위터

  • Facebook

    Facebook

  • 카카오스토리

    카카오스토리

  • 밴드

    밴드

  • 네이버 블로그

    네이버 블로그

  • Pocket

    Pocket

  • Evernote

    Evernote

다른 글

  • [Spring Web MVC] Thymeleaf (2)

    [Spring Web MVC] Thymeleaf (2)

    2022.08.20
  • [Spring Web MVC] Thymeleaf (1)

    [Spring Web MVC] Thymeleaf (1)

    2022.08.19
  • [Spring Web MVC] 응답 정보 다루기

    [Spring Web MVC] 응답 정보 다루기

    2022.08.17
  • [Spring Web MVC] 요청 정보 다루기

    [Spring Web MVC] 요청 정보 다루기

    2022.08.17
다른 글 더 둘러보기

정보

천천히 꾸준히 조용히 블로그의 첫 페이지로 이동

천천히 꾸준히 조용히

  • 천천히 꾸준히 조용히의 첫 페이지로 이동

검색

방문자

  • 전체 방문자
  • 오늘
  • 어제

카테고리

  • 분류 전체보기 (679)
    • Algorithm (205)
      • Data Structure (5)
      • Theory && Tip (33)
      • Baekjoon (166)
      • ALGOSPOT (1)
    • Spring (123)
      • Spring (28)
      • Spring Web MVC (20)
      • Spring Database (14)
      • Spring Boot (6)
      • Spring 3.1 (11)
      • Spring Batch (6)
      • Spring Security (16)
      • JPA (12)
      • Spring Data JPA (5)
      • QueryDSL (4)
      • eGovFramework (1)
    • Programming Language (74)
      • C (25)
      • C++ (12)
      • Java (19)
      • JavaScript (15)
      • Python (1)
      • PHP (2)
    • Computer Science (142)
      • Machine Learning (38)
      • Operating System (18)
      • Computer Network (28)
      • System Programming (22)
      • Universial Programming Lang.. (8)
      • Computer Architecture (4)
      • Compiler Design (11)
      • Computer Security (13)
    • Database (21)
      • Database (7)
      • MySQL (3)
      • Oracle (3)
      • Redis (5)
      • Elasticsearch (3)
    • DevOps (20)
      • Docker && Kubernetes (8)
      • Jenkins (4)
      • Amazon Web Service (8)
    • Mobile (28)
      • Android (21)
      • Flutter (7)
    • 💡 솔루션 (17)
    • 👥 모각코 (10)
    • 💬 기록 (8)
    • 📚 공부 (6)
    • -------------- (25)

최근 글

나의 외부 링크

메뉴

  • 홈
반응형

정보

i3months의 천천히 꾸준히 조용히

천천히 꾸준히 조용히

i3months

블로그 구독하기

  • 구독하기
  • RSS 피드

티스토리

  • 티스토리 홈
  • 이 블로그 관리하기
  • 글쓰기
Powered by Tistory / Kakao. Copyright © i3months.

티스토리툴바