제육's 휘발성 코딩
반응형

요구사항 분석

sec7  사진1

  • 디자이너 : 요구사항에 맞도록 디자인하고, 디자인 결과물을 웹 퍼블리셔에게 넘겨준다.
  • 웹 퍼블리셔 : 디자이너에서 받은 디자인을 기반으로 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, quantityInteger로 지정한 이유는 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">
  • 타임리프 절대 경로를 추가로 넣어서 템플릿이 렌더링되면 자동으로 바뀐다. 기존의 hrefth: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";
    }
  • 상품 등록을 완료한 페이지에서 새로고침을 하면 상품이 계속 추가되어버린다.

sec7  사진2

  • 상품저장 컨트롤러가 재호출되어버림 (상품등록 폼(GET) -> 전송시 POST로 서버에 요청 -> 뷰 반환)
  • 상품 등록 폼에서 데이터를 입력하고 저장하면 post /add + 상품데이터를 서버로 전송, 새로고침시 재실행

PRG

sec7  사진3

 @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편 - 백엔드 웹 개발 핵심 기술)를 토대로 정리한 내용입니다.

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^