<!DOCTYPE html><html><head><metacharset="UTF-8"><title>Title</title></head><body><ul><li>상품 관리
<ul><li><ahref="/basic/items">상품 관리 - 기본</a></li></ul></li></ul></body></html>
packagehello.itemservice.domain.item;importorg.springframework.stereotype.Repository;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;@RepositorypublicclassItemRepository{privatestaticfinalMap<Long,Item>store=newHashMap<>();//저장소, static 사용privatestaticlongsequence=0L;//시퀀스 번호 생성기, static 사용//상품 저장publicItemsave(Itemitem){item.setId(++sequence);store.put(item.getId(),item);returnitem;}//단일 상품 조회publicItemfindById(Longid){returnstore.get(id);}//모든 상품 조회publicList<Item>findAll(){returnnewArrayList<>(store.values());}//상품 정보 수정publicvoidupdate(LongitemId,ItemupdateParam){ItemfindItem=findById(itemId);findItem.setItemName(updateParam.getItemName());findItem.setPrice(updateParam.getPrice());findItem.setQuantity(updateParam.getQuantity());}//저장소 초기화publicvoidclearStore(){store.clear();}}
상품 저장소 테스트
Ctrl + Shift + T를 통해서 테스트를 생성하자.
packagehello.itemservice.domain.item;importorg.junit.jupiter.api.AfterEach;importorg.junit.jupiter.api.DisplayName;importorg.junit.jupiter.api.Test;importjava.util.List;importstaticorg.assertj.core.api.Assertions.assertThat;classItemRepositoryTest{ItemRepositoryitemRepository=newItemRepository();@AfterEachvoidafterEach(){itemRepository.clearStore();//저장소 초기화}@DisplayName("상품 저장 테스트")@Testvoidsave(){//givenItemitem=newItem("itemA",10000,10);//whenItemsavedItem=itemRepository.save(item);//thenItemfindItem=itemRepository.findById(item.getId());assertThat(findItem).isEqualTo(savedItem);}@DisplayName("모든 상품 조회 테스트")@TestvoidfindAll(){//givenItemitem1=newItem("item1",10000,10);Itemitem2=newItem("item2",20000,20);itemRepository.save(item1);itemRepository.save(item2);//whenList<Item>result=itemRepository.findAll();//thenassertThat(result.size()).isEqualTo(2);assertThat(result).contains(item1,item2);}@DisplayName("상품 정보 수정 테스트")@TestvoidupdateItem(){//givenItemitem=newItem("item1",10000,10);ItemsavedItem=itemRepository.save(item);LongitemId=savedItem.getId();//whenItemupdateParam=newItem("item2",20000,30);itemRepository.update(itemId,updateParam);ItemfindItem=itemRepository.findById(itemId);//thenassertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());}}
상품 서비스 HTML
부트스트랩
https://getbootstrap.com/docs/5.0/getting-started/download/로 이동
컨트롤러 로직은 itemRepository에서 모든 상품을 조회한 다음에 모델에 담는다. 그리고 뷰 템플릿을 호출한다
@RequiredArgsConstructor를 통해서 final 이 붙은 멤버변수만 사용해서 생성자를 자동으로 만들어준다.
@PostConstruct를 통해서 테스트용 데이터를 추가한다.
@PostConstruct는 해당 빈의 의존관계가 모두 주입되고 나면 호출된다.
packagehello.itemservice.web.item.basic;importhello.itemservice.domain.item.Item;importhello.itemservice.domain.item.ItemRepository;importlombok.RequiredArgsConstructor;importorg.springframework.stereotype.Controller;importorg.springframework.ui.Model;importorg.springframework.web.bind.annotation.*;importjakarta.annotation.PostConstruct;importjava.util.List;@Controller@RequestMapping("/basic/items")@RequiredArgsConstructorpublicclassBasicItemController{privatefinalItemRepositoryitemRepository;/**
* 테스트용 데이터 추가
*/@PostConstructpublicvoidinit(){itemRepository.save(newItem("testA",10000,10));itemRepository.save(newItem("testB",20000,20));}//상품 목록@GetMappingpublicStringitems(Modelmodel){List<Item>items=itemRepository.findAll();model.addAttribute("items",items);return"basic/items";}}
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
다음과 같이 리터럴 대체 문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
<span th:text="|Welcome to our application, ${user.name}!|">
반복문
th:each="변수명 : ${컬렉션명}"처럼 사용한다.
컬렉션명에는 말 그대로 컬렉션의 이름이 들어간다.
컨트롤러 단에서 모델에 추가한 이름 그대로 적용된다.
변수명은 말 그대로 변수명이다.
일반적인 자바의 for-each를 생각하면 된다.
변수명.속성명으로 값을 꺼내서 쓸 수 있다.
하위 태그도 함께 반복된다.
예시 : tr에 th:each 사용 시 tr과 tr 아래의 td도 함께 반복
변수 표현식
${...}처럼 사용한다.
모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회할 수 있다.
프로퍼티 접근법을 사용한다.
에시 : item.getPrice()
내용 변경
th:text=${...}처럼 사용한다.
기존 html 태그의 값을 덮어씌운다.
만약 <td th:text="${item.price}">10000</td>이 있을 때 item.price의 값이 0이라면 최종 출력은 0이 된다.
조건문
th:if="조건문"처럼 사용한다.
th:if 내부의 조건문의 결과가 참일 때만 해당 영역이 랜더링된다.
상품 상세
컨트롤러
스프링 부트 3.2 이상이면 아래 컨트롤러 메소드에서 오류가 발생할 것이다.
스프링 부트 3.2부터 자바 컴파일러에 -parameters 옵션을 넣어주어야 애노테이션의 이름을 생략할 수 있다.
//단일 상품 조화@GetMapping("/{itemId}")publicStringitem(@PathVariableLongitemId,Modelmodel){Itemitem=itemRepository.findById(itemId);model.addAttribute("item",item);return"basic/item";}
<!DOCTYPE HTML><htmlxmlns:th="http://www.thymeleaf.org"><head><metacharset="utf-8"><linkhref="../css/bootstrap.min.css"th:href="@{/css/bootstrap.min.css}"rel="stylesheet"><style>.container{max-width:560px;}</style></head><body><divclass="container"><divclass="py-5 text-center"><h2>상품 등록 폼</h2></div><h4class="mb-3">상품 입력</h4><formaction="item.html"th:actionmethod="post"><div><labelfor="itemName">상품명</label><inputtype="text"id="itemName"name="itemName"class="form-control"placeholder="이름을 입력하세요"></div><div><labelfor="price">가격</label><inputtype="text"id="price"name="price"class="form-control"placeholder="가격을 입력하세요"></div><div><labelfor="quantity">수량</label><inputtype="text"id="quantity"name="quantity"class="form-control"placeholder="수량을 입력하세요"></div><hrclass="my-4"><divclass="row"><divclass="col"><buttonclass="w-100 btn btn-primary btn-lg"type="submit">상품 등
록</button></div><divclass="col"><buttonclass="w-100 btn btn-secondary btn-lg"onclick="location.href='items.html'"th:onclick="|location.href='@{/basic/items}'|"type="button">취소</button></div></div></form></div><!-- /container --></body></html>
상품 등록 처리 - @ModelAttribute
상품 등록 폼에서 전달된 데이터로 실제 상품을 등록해보자.
데이터는 상품 등록 폼에서 POST 방식으로 서버에 데이터를 전달한다.
POST localhost:8080/basic/items/add
content-type: application/x-www-form-urlencoded
컨트롤러 v1
테스트 시 실제로 상품이 잘 저장되는 것이 확인된다.
다만 @RequestParam으로 변수를 하나하나 받아서 Item을 생성하는 과정이 불편하다.
//상품 등록 v1@PostMapping("/add")publicStringaddItemV1(@RequestParamStringitemName,@RequestParamintprice,@RequestParamIntegerquantity,Modelmodel){Itemitem=newItem();item.setItemName(itemName);item.setPrice(price);item.setQuantity(quantity);itemRepository.save(item);model.addAttribute("item",item);return"basic/item";}
컨트롤러 v2
이번에는 addItemV1을 주석 처리한 후에 아래 코드를 적용해보자.
@ModelAttribute을 통해서 전달된 데이터가 자동으로 Item에 대입되도록 수정했다.
@ModelAttribute를 사용했기 때문에 자동으로 model에 “item”이라고 추가된다.
/**
* 상품 등록 v2
* @ModelAttribute("item") Item item
* model.addAttribute("item", item); 자동 추가
*/@PostMapping("/add")publicStringaddItemV2(@ModelAttribute("item")Itemitem,Modelmodel){itemRepository.save(item);//model.addAttribute("item", item); //자동 추가, 생략 가능return"basic/item";}
컨트롤러 v3
이번에는 addItemV2을 주석 처리한 후에 아래 코드를 적용해보자.
@ModelAttribute에서 속성명을 빼버렸고, 또한 파라미터에서 Model 자체를 빼버렸다.
그래도 잘 동작하면서 모델에 item으로 등록된 것을 확인할 수 있다.
핸들러 어댑터쪽에서 자동으로 모델에 클래스명으로 Item을 등록했다.
기본은 클래스명으로 등록이지만 대신에 첫글자는 소문자로 변환해서 등록한다.
/**
* 상품 등록 v3
* @ModelAttribute name 생략 가능
* model.addAttribute(item); 자동 추가, 생략 가능
* 생략시 model에 저장되는 name은 클래스명 첫글자만 소문자로 등록 Item -> item
*/@PostMapping("/add")publicStringaddItemV3(@ModelAttributeItemitem){itemRepository.save(item);return"basic/item";}
컨트롤러 v4
이번에는 addItemV3을 주석 처리한 후에 아래 코드를 적용해보자.
@ModelAttribute 자체를 생략해버렸지만 핸들러 어댑터가 알아서 잘 처리해준다.
/**
* 상품 등록 v4
* @ModelAttribute 자체 생략 가능
* model.addAttribute(item) 자동 추가
*/@PostMapping("/add")publicStringaddItemV4(Itemitem){itemRepository.save(item);return"basic/item";}