Spring MVC v2 - 스프링 통합과 폼

Kwon Yongho·2023년 4월 25일
0

Spring-MVC-v2

목록 보기
2/13
post-thumbnail

스프링 통합과 폼

  1. 타임리프 스프링 통합
  2. 입력 폼 처리
  3. 요구사항 추가
  4. 체크 박스
  5. 라디오 버튼
  6. 셀렉트 박스

해당 프로젝트는 전전 게시글인 Spring-MVC 간단한 웹 사이트 만들기 코드를 가져와서 진행했습니다.

오류해결

Springmvc 폴더를 thymeleaf-form으로 이름 변경 후 실행 했을 때 저번 게시판 메인 홈페이지가 뜨는 오류가 있었는데 캐시 문제라는 걸 알았습니다. 저는 엣지를 사용하기 때문에 설정 -> 개인 정보,검색 및 서비스 -> 검색 데이터 지우기에서 캐시를 지웠더니 해결됬습니다.

1. 타임리프 스프링 통합

  • 타임리프는 스프링이 없어도 동작하지만, 스프링과 통합을 위한 다양한 기능을 제공한다,

스프링 통합으로 추가되는 기능

  • 스프링의 SpringEL 문법 통합
  • ${@myBean.doSomething()} 처럼 스프링 빈 호출 지원
  • 편리한 폼 관리를 위한 추가 속성
  • 폼 컴포넌트 기능
    • checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원
  • 스프링의 메시지, 국제화 기능의 편리한 통합
  • 스프링의 검증, 오류 처리 통합
  • 스프링의 변환 서비스 통합(ConversionService)

build.gradle 타임리프 라이브러리

implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

2. 입력 폼 처리

  • 타임리프가 제공하는 입력 폼 기능을 적용해보자.

BasicItemController 변경

    @GetMapping("/add")
    public String addForm(Model model) {
        model.addAttribute("item", new Item());
        return "basic/addForm";
    }

addForm.html 변경

    <form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" th:field="*{price}" class="form-control"
                   placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>
  • th:object="${item}": <form> 에서 사용할 객체를 지정한다. 선택 변수 식( *{...} )을 적용할 수 있다.
  • th:field="*{itemName}": =${item.itemName}, 앞서 th:object로 item을 선택했기 때문에 선택 변수 식을 적용할 수 있다.
  • th:field: id, name, value 속성을 모두 자동으로 만들어준다.

editForm.html 변경

    <form action="item.html" th:action th:object="${item}" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" th:field="*{price}" class="form-control"
                   placeholder="가격을 입력하세요">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" th:field="*{quantity}" class="form-control" placeholder="수량을 입력하세요">
        </div>
  • 수정 폼의 경우 id , name , value 를 모두 신경써야 했는데, 많은 부분이 th:field 덕분에 자동으로 처리되는 것을 확인할 수 있다.

3. 요구사항 추가

  • 타임리프를 사용해서 폼에서 체크박스, 라디오 버튼, 셀렉트 박스를 편리하게 사용하는 방법을 학습해보자.

요구사항

  • 판매 여부
    • 판매 오픈 여부
    • 체크 박스로 선택할 수 있다.
  • 등록 지역
    • 서울, 부산, 제주
    • 체크 박스로 다중 선택할 수 있다.
  • 상품 종류
    • 도서, 식품, 기타
    • 라디오 버튼으로 하나만 선택할 수 있다.
  • 배송 방식
    • 빠른 배송
    • 일반 배송
    • 느린 배송
    • 셀렉트 박스로 하나만 선택할 수 있다.

결과 미리 확인하기

코드 추가

Item

package com.example.springmvc.domain.item;

import lombok.Data;

import java.util.List;

@Data
public class Item {

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

    private Boolean open; //판매 여부
    private List<String> regions; //등록 지역
    private ItemType itemType; //상품 종류
    private String deliveryCode; //배송 방식

    public Item() {}

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

ItemType

package com.example.springmvc.domain.item;

public enum ItemType {

    BOOK("도서"), FOOD("식품"), ETC("기타");
    private final String description;

    ItemType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

DeliveryCode

package com.example.springmvc.domain.item;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * FAST: 빠른 배송
 * NORMAL: 일반 배송
 * SLOW: 느린 배송
 */

@Data
@AllArgsConstructor
public class DeliveryCode {
    private String code;
    private String displayName;
}

4. 체크 박스

4-1. 체크 박스 - 단일1

addForm.html 추가

        <div>판매 여부</div>
        <div>
            <div class="form-check">
                <input type="checkbox" id="open" name="open" class="form-check-input">
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>

Form 확인

  • 체크o

  • 체크x

  • 체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어간다.

  • HTML에서 체크 박스를 선택하지 않고 폼을 전송하면 open 이라는 필드 자체가 서버로 전송되지 않는다.

로그 확인

  INFO 7108 --- [nio-8080-exec-1] c.e.s.w.item.basic.BasicItemController   : item.open=true
  INFO 7108 --- [nio-8080-exec-7] c.e.s.w.item.basic.BasicItemController   : item.open=null
  • HTML checkbox는 선택이 안되면 클라이언트에서 서버로 값 자체를 보내지 않는다.
  • 사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장시 아무 값도 넘어가지 않기 때문에, 서버 구현에 따라서 값이 오지 않은 것으로 판단해서 값을 변경하지 않을 수도 있다.
  • 이런 문제를 해결하기 위해서 스프링 MVC는 약간의 트릭을 사용하는데, 히든 필드를 하나 만들어서, _open 처럼 기존 체크 박스 이름 앞에 언더스코어( _ )를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다.
  • 히든 필드는 항상 전송된다.
  • 체크를 해제한 경우 여기에서 open은 전송되지 않고, _open만 전송되는데, 이 경우 스프링 MVC는 체크를 해제했다고 판단한다.

addForm.html 히든 필드 추가

        <div>판매 여부</div>
        <div>
            <div class="form-check">
                <input type="checkbox" id="open" name="open" class="form-check-input">
                <input type="hidden" name="_open" value="on"/> <!-- 히든 필드 추가 -->
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>

Form 확인

  • 체크o
  • 체크x

로그 확인

  INFO 7108 --- [nio-8080-exec-1] c.e.s.w.item.basic.BasicItemController   : item.open=true
  INFO 7108 --- [nio-8080-exec-7] c.e.s.w.item.basic.BasicItemController   : item.open=false
  • 체크 박스를 체크하면 스프링 MVC가 open 에 값이 있는 것을 확인하고 사용한다. 이때 _open은 무시한다.
  • 체크 박스를 체크하지 않으면 스프링 MVC가 _open 만 있는 것을 확인하고, open의 값이 체크되지 않았다고 인식한다.

4-2. 체크 박스 - 단일2

  • 개발할 때 마다 이렇게 히든 필드를 추가하는 것은 상당히 번거롭다. 타임리프가 제공하는 폼 기능을 사용하면 이런 부분을 자동으로 처리할 수 있다.

addForm.html 변경

        <div>판매 여부</div>
        <div>
            <div class="form-check">
                <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>

로그 확인

  INFO 7108 --- [nio-8080-exec-1] c.e.s.w.item.basic.BasicItemController   : item.open=true
  INFO 7108 --- [nio-8080-exec-7] c.e.s.w.item.basic.BasicItemController   : item.open=false
  • hidden이 없어도 타임리프에서 자동으로 hidden에 name이 _open인 input을 자동으로 생성해준다.

상품 상세 및 수정도 코드 추가

        <div>판매 여부</div>
        <div>
            <div class="form-check">
                <input type="checkbox" id="open" th:field="*{open}" class="form-check-input">
                <label for="open" class="form-check-label">판매 오픈</label>
            </div>
        </div>
  • 체크 박스를 체크하고 POST하면 상세 Form으로 이동 되는데 체크박스가 checked="checked" 되어 있는 것을 확인 할 수 있다. 타임리프의 th:field 를 사용하면, 값이 true인 경우 체크를 자동으로 처리해준다.

4-3. 체크 박스 - 멀티

  • 체크 박스를 멀티로 사용해서, 하나 이상을 체크할 수 있도록 해보자.

BasicItemController 추가

    @ModelAttribute("regions")
    public Map<String, String> regions() {
        Map<String, String> regions = new LinkedHashMap<>();
        regions.put("SEOUL", "서울");
        regions.put("BUSAN", "부산");
        regions.put("JEJU", "제주");
        return regions;
    }

@ModelAttribute

  • 등록, 상세, 수정 폼에서 모두 체크 박스를 반복해서 보여줘야 되기 때문에 중복 코드가 발생한다.
  • @ModelAttribute 는 이렇게 컨트롤러에 있는 별도의 메서드에 적용할 수 있다.
  • 해당 컨트롤러를 요청할 때 regions 에서 반환한 값이 자동으로 모델( model )에 담기게 된다.

@ModelAttribute가 있는 메서드는 컨트롤러가 호출 될 때마다 사용되므로 객체도 계속 생성된다. 이런 부분은 미리 생성해두고 재사용하는 것이 효율적이다.

addForm.html 추가

        <div>
            <div>등록 지역</div>
            <div th:each="region : ${regions}" class="form-check form-check-inline">
                <input type="checkbox" th:field="*{regions}" th:value="${region.key}"
                       class="form-check-input">
                <label th:for="${#ids.prev('regions')}"
                       th:text="${region.value}" class="form-check-label">서울</label>
            </div>
        </div>
  • th:for="${#ids.prev('regions')}": 멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다. 하지만 id는 모두 달라야 한다.
  • 타임리프는 체크박스를 each 루프 안에서 반복해서 만들 때 임의로 1, 2, 3 숫자를 뒤에 붙여준다.
  • 타임리프는 ids.prev(...), ids.next(...)을 제공해서 동적으로 생성되는 id 값을 사용할 수 있도록 한다.

로그 확인(서울,제주 선택 / 미선택)

[nio-8080-exec-7] c.e.s.w.item.basic.BasicItemController   : item.regions=[SEOUL, JEJU]
[nio-8080-exec-3] c.e.s.w.item.basic.BasicItemController   : item.regions=[]

  • _regions는 앞서 설명한 기능이다
  • 참고로 _regions 조차 보내지 않으면 결과는 null 이 된다.

5. 라디오 버튼

  • 라디오 버튼을 자바 ENUM을 활용해서 개발해보자.

BasicItemController 추가

    @ModelAttribute("itemTypes")
    public ItemType[] itemTypes() {
        return ItemType.values();
    }
  • itemTypes 역시 등록, 수정, 상세에서 사용하므로 @ModelAttribute 사용
  • ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다. 예) [BOOK, FOOD, ETC]

addForm.html 추가

        <div>
            <div>상품 종류</div>
            <div th:each="type : ${itemTypes}" class="form-check form-check-inline">
                <input type="radio" th:field="*{itemType}" th:value="${type.name()}" class="form-check-input">
                <label th:for="${#ids.prev('itemType')}" th:text="${type.description}" class="form-check-label">
                    BOOK
                </label>
            </div>
        </div>

로그 확인(도서 선택 / 미선택)

[nio-8080-exec-3] c.e.s.w.item.basic.BasicItemController   : item.itemType=BOOK
[nio-8080-exec-7] c.e.s.w.item.basic.BasicItemController   : item.itemType=null

타임리프에서 ENUM 직접 사용하기

<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">
  • ${T(hello.itemservice.domain.item.ItemType).values()}: 스프링EL 문법으로 ENUM을 직접 사용할 수 있다. ENUM에 values() 를 호출하면 해당 ENUM의 모든 정보가 배열로 반환된다.
  • 이렇게 사용하면 ENUM의 패키지 위치가 변경되거나 할때 자바 컴파일러가 타임리프까지 컴파일 오류를 잡을 수 없으므로 추천하지는 않는다.

6. 셀렉트 박스

  • 셀렉트 박스를 자바 객체를 활용해서 개발해보자.

BasicItemController 추가

    @ModelAttribute("deliveryCodes")
    public List<DeliveryCode> deliveryCodes() {
        List<DeliveryCode> deliveryCodes = new ArrayList<>();
        deliveryCodes.add(new DeliveryCode("FAST", "빠른 배송"));
        deliveryCodes.add(new DeliveryCode("NORMAL", "일반 배송"));
        deliveryCodes.add(new DeliveryCode("SLOW", "느린 배송"));
        return deliveryCodes;
    }

addForm.html 추가

        <div>
            <div>배송 방식</div>
            <select th:field="*{deliveryCode}" class="form-select">
                <option value="">==배송 방식 선택==</option>
                <option th:each="deliveryCode : ${deliveryCodes}" th:value="${deliveryCode.code}" th:text="${deliveryCode.displayName}">FAST</option>
            </select>
        </div>
  • 위에 방식과 똑같다.

참고
김영한: 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술(인프런)
Github - https://github.com/b2b2004/Spring_MVC

0개의 댓글