반응형
요구사항 분석
- 디자이너 : 요구사항에 맞도록 디자인하고, 디자인 결과물을 웹 퍼블리셔에게 넘겨준다.
- 웹 퍼블리셔 : 디자이너에서 받은 디자인을 기반으로 HTML, CSS를 만들어 개발자에게 제공한다.
- 백엔드 : 디자이너, 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고, 핵심 비즈니스 모델을 개발한다. 이후 HTML이 나오면 이 HTML을 뷰 템플릿으로 변환해서 동적으로 화면을 그리고, 또 웹 화면의 흐름을 제어한다.
상품 관리
상품 도메인
@Getter @Setter
public class Item {
private Long id;
private String itemName;
private Integer price; //null 가능성 o
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
@Getter
,@Setter
: 롬복 라이브러리price
,quantity
를Integer
로 지정한 이유는 null 가능성이 있기 때문에 프리미티브형으로 선언 해야한다.
상품 저장소
@Repository
public class ItemRepository {
private static final Map<Long, Item> store = new HashMap<>(); // 실무에선 동시성고려 ConcurrentHashMap 사용
private static long sequence = 0L; //AtomicLong 사용
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());
}
//소규모 프로젝트니까 dto 사용하지않고 여기서 만듬
public void update(Long itemId, Item updateParam) {
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore() {
store.clear();
}
}
Repository
를 생성할 때 동시성 문제를 고려해야한다. 동시성이란? 요청이 동시에 들어가는 상황으로 해당 값에 대한 결과가 다를 수 있으므로HashMap
이 아닌ConcorrentHashMap
,Long
이 아닌AtomicLong
을 사용하는 것이 올바르다.
상품 저장소 테스트
class ItemRepositoryTest {
ItemRepository itemRepository = new ItemRepository();
@AfterEach // 테스트가끝날때마다 초기화
void afterEach() {
itemRepository.clearStore();
}
@Test
void save() {
// given
Item item = new Item("ItemA", 10000, 10);
// when
Item saveItem = itemRepository.save(item);
// then
Item findItem = itemRepository.findById(item.getId());
assertThat(findItem).isEqualTo(saveItem);
}
@Test
void findAll() {
// given
Item item1 = new Item("Item1", 10000, 10);
Item item2 = new Item("Item2", 20000, 20);
itemRepository.save(item1);
itemRepository.save(item2);
// when
List<Item> result = itemRepository.findAll();
// then
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(item1, item2);
}
@Test
void updateItem() {
// given
Item item = new Item("Item1", 10000, 10);
Item savedItem = itemRepository.save(item);
Long itemId = savedItem.getId();
// when
Item updateParam = new Item("Item2", 20000, 30);
itemRepository.update(itemId, updateParam);
// then
Item findItem = itemRepository.findById(itemId);
assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
}
- Junit5에서의 테스트 방식이다. 테스트 코드를 작성할 때 given, when, then 구조를 사용하자.
상품 서비스 HTML
- 외부 css 등록시에
static
하위에 다운로드 파일을 넣은 후 코드가 띄워지는지 테스트 (안띄워질시 out폴더를 삭제후 서버 재시작- 인텔리제이 고질병) - html 파일 우클릭 - copy path - absolute path 해서 경로 복사 후 서버가 꺼진 상태에서 접속 가능 (정적 파일이기 때문에)
- static 하위에 저장했기 때문에 서버를 띄우고 경로에 맞게 들어가도 된다.
상품목록 - 타임리프
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
- 타임리프 절대 경로를 추가로 넣어서 템플릿이 렌더링되면 자동으로 바뀐다. 기존의
href
를th:href
로 덮어버림 (타임리프 특징 - 네츄럴 템플릿) - 뷰 템플릿에 렌더링하지 않고 html 파일을 직접열어도 어느정도 동작
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품
등록</button>
- 경로 변경 (
리터럴 대체 문법 : | ... |
) - 리터럴 대체 문법을 사용하지 않으면 th:onclick="'location.href=' + ' \ ' ' + @{/basic/items/add} + ' \ ' ' " 와 같이 써야 한다.
<tr th:each="item : ${items}">
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원id</a></td>
<td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
- 루프 설정 , 스프링의 PathVariable 같은 기능이 있다.
- 타임리프의 url 링크는
@{...}
을 사용한다. URL 링크 표현식 - 반복은 th:each를 사용 , 변수 표현식
${items}
: 컨트롤러에서 items를 model로 넘겨준 값 th:href="@{|/basic/items/${item.id}|}"
로도 간단히 사용할 수 있다. (2,3 번째 줄 동일 내용)th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
와 같이 쿼리 파라미터도 생성할 수 있다.
상품 등록
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control"
value="1" th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control"
value="상품A" th:value="${item.itemName}" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
value="10000" th:value="${item.price}" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control"
value="10" th:value="${item.quantity}" readonly>
</div>
- 변수 표현식 $를 통해 item이 들어올것을 표현
@PostMapping("/add") //v2 사용시 주석 처리 (중복 매핑 방지)
public String addItemV1(@RequestParam String itemName, // html name의 이름과 맞춤 (item)
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item
) {
itemRepository.save(item);
// model.addAttribute("item", item); 자동추가, 생략가능
return "basic/item";
}
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item
) {
itemRepository.save(item);
return "basic/item";
}
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
@ModelAttribute
는 Item 객체를 생성하고 요청 파라미터의 값을 프로퍼티 접근법(setXxx)로 자동 입력- *model.addAttribute 기능을 자동으로 처리, 즉 생략 가능 *
- 애노테이션 자체도 생략이 가능하며, 이름지정을 안할시 첫글자만 소문자로 자동 변환해준다.
상품 수정
<form action="item.html" th:action method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}"
readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A" th:value="${item.itemName}">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
value="10000" th:value="${item.price}">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol" value="10" th:value="${item.quantity}">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">저장
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
type="button">취소</button>
</div>
</div>
</form>
- th:action="item.html" 을 th:action 생략으로 가능하다. 참고(Html Form 전송을 GET,POST만 사용가능하다.)
- th:value=${} 설정
- location 설정
@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}";
}
- redirect 를 사용하면 302 상태코드 확인, 경로에 PathVariable 사용 가능
문제점
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
- 상품 등록을 완료한 페이지에서 새로고침을 하면 상품이 계속 추가되어버린다.
- 상품저장 컨트롤러가 재호출되어버림 (상품등록 폼(GET) -> 전송시 POST로 서버에 요청 -> 뷰 반환)
- 상품 등록 폼에서 데이터를 입력하고 저장하면 post /add + 상품데이터를 서버로 전송, 새로고침시 재실행
PRG
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
- 뷰 템플릿 이동이아닌 상품 상세화면(
http://localhost:8080/basic/items/3
으로 리다이렉트 호출
RedirectAttributes
- 고객의 입장에서는 상품 등록 후 상품 상세화면이 나오니 저장이 잘되었는지 의문이 들 수 있다.
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item saveItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", saveItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
redirectAttributes.addAttribute
로 넣은 값이PathVariable
에 있으면 넣고 없으면 쿼리 파라미터로 들어간다.- 예 )
http://localhost:8080/basic/items/3?status=true
- 예 )
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
- th:if 조건이 참이면 실행
${param.status}
: 타임리프에서 쿼리파라미터를param
을 사용하여 편리하게 조회하는 기능- 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원해준다.
본 포스팅은 인프런 김영한님 강의(스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술)를 토대로 정리한 내용입니다.
반응형