스프링 인프런 김영한

22. 입력 폼 처리

지금부터 타임리프가 제공하는 입력 폼 기능을 적용해서 기존 프로젝트의 폼 코드를 타임리프가 지원하는 기능을 사용해서 효율적으로 개선해보자.

22.1 add 폼

컨트롤러에서 model로 빈 Item객체를 넘겨준다.

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

22.2 view

view에서 th:object="${item}"으로 보낸 객체를 사용할 수 있다.
th:field="${item.itemName}" 으로 꺼내서 사용하면 알아서 id와 name을 넣어준다.

*{itemName}을 사용하면 어떤 객체의 것인지 지정을 안하고 값을 넣을수도 있다.

id name value를 알아서 만들어준다. 이렇게 중복문제를 다 해결해준다.

현재 어떤 객체를 사용하는지 껍데기를 넘겨준다.
item과 관련한것임을 알게 하고 그래서 프로퍼티명으로 id name value를 만들어줘서 아주 편리해진다.

<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>
</form>

th:object : 커맨드 객체를 지정한다.
*{...} : 선택 변수 식이라고 한다. th:object 에서 선택한 객체에 접근한다.

th:field
HTML 태그의 id , name , value 속성을 자동으로 처리해준다.

렌더링 전

<input type="text" th:field="*{itemName}" />

렌더링 후

<input type="text" id="itemName" name="itemName" th:value="*{itemName}" />

등록 폼
th:object 를 적용하려면 먼저 해당 오브젝트 정보를 넘겨주어야 한다.
등록 폼이기 때문에 데이터가 비어있는 빈 오브젝트를 만들어서 뷰에 전달해보자.

실수로 id를 틀리거나 name을 틀리는 경우가 잇는데 이런 문제를 해결해준다.

22.3 수정폼

등록폼은 약하고 수정폼에서는 더 잘느껴진다.
이미 item객체를 넘기고 있기 때문에 따로 작성해주지 않아도된다.

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

22.4 수정폼 view

값을 출력하기 위해서 thvalue를 뿌리고 있다.

<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="form-control" 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="form-control" value="10" th:value="${item.quantity}">
    </div>

기존은 이런 폼인데 th:object를 넣음으로써 id name value를 다지울수 있다.
요구되는 작업을 획기적으로 줄일 수 있는것이다.

<form action="item.html" th:action th:object="${item}" method="post">
    <div>
        <label for="id">상품 ID</label>
        <input type="text" id="id" th:field="*{id}" class="form-control" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" th:field="*{itemName}" class="form-control">
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" th:field="*{price}" class="form-control">
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" th:field="*{quantity}" class="form-control">
    </div>      

id를 다지워도되지만 label이 인식을 못해서 일단 남겨놓은 것이다.

수정 폼은 앞서 설명한 내용과 같다.
수정 폼의 경우 id , name , value 를 모두 신경써야 했는데
많은 부분이 th:field 덕분에 자동으로 처리되는 것을 확인할 수 있다

렌더링전

<input type="text" id="itemName" th:field="*{itemName}" class="form-control">

렌더링후

<input type="text" id="itemName" class="form-control" name="itemName" value="itemA">

정리
th:object , th:field 덕분에 폼을 개발할 때 약간의 편리함을 얻었다.
쉽고 단순해서 크게 어려움이 없었을 것이다.
사실 이것의 진짜 위력은 뒤에 설명할 검증(Validation)에서 나타난다.
이후 검증 부분에서 폼 처리와 관련된 부분을 더 깊이있게 알아보자

23. 요구사항 추가

타임리프를 사용해서 폼에서 체크박스, 라디오 버튼, 셀렉트 박스를 편리하게 사용하는 방법을 학습해보자.
기존 상품 서비스에 다음 요구사항이 추가되었다.

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

23.1 ItemType- 상품종류

상품 종류는 ENUM 을 사용한다.
설명을 위해 description 필드를 추가했다.

public enum ItemType {

    BOOK("도서"), FOOD("식품"), ETC("기타");

    private final String description;

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

    public String getDescription() {
        return description;
    }
}

23.2 배송 방식 - DeliveryCode

배송 방식은 DeliveryCode 라는 클래스를 사용한다.
단순한 자바코드이다.
code 는 FAST 같은 시스템에서 전달하는 값이고,
displayName 은 빠른 배송 같은 고객에게 보여주는 값이다

/**
 * FAST: 빠른 배송
 * NORMAL: 일반 배송
 * SLOW: 느린 배송
 */
@Data
@AllArgsConstructor
public class DeliveryCode {
    private String code;
    private String displayName;
}

23.3 Item - 상품

판매여부 등록지역 상품종류 배송방식이 추가되었다.
ENUM , 클래스, String 같은 다양한 상황을 준비되었다.
각각의 상황에 어떻게 폼의 데이터를 받을 수 있는지 하나씩 알아보자

@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;
    }
}

24. 체크 박스 - 단일1

굉장히 쉬워보인데 함정이 있으니 잘봐야한다.

단순 HTML 체크 박스

<!-- single checkbox -->
<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>

순수 html로짜는 코드가 골치아픈게 있다.
Boolean open으로 저장이된다.

체크 박스를 체크하면 HTML Form에서 open=on 이라는 값이 넘어간다.
스프링은 on 이라는 문자를 true 타입으로 변환해준다.
스프링 타입 컨버터가 이 기능을 수행하는 것이다.

체크를 안하면 그냥 값이 null로 아예넘어가지 않게된다.
그래서 일반적인 체크박스는 이런게 문제이다.

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

HTTP 요청 메시지 로깅
HTTP 요청 메시지를 서버에서 보고 싶으면 다음 설정을 추가하면 된다.
application.properties
logging.level.org.apache.coyote.http11=debug
HTTP 메시지 바디를 보면 open 의 이름도 전송이 되지 않는 것을 확인할 수 있다.
itemName=itemA&price=10000&quantity=10

HTML checkbox는 선택이 안되면 클라이언트에서 서버로 값 자체를 보내지 않는다.
수정의 경우에는 상황에 따라서 이 방식이 문제가 될 수 있다.
사용자가 의도적으로 체크되어 있던 값을 체크를 해제해도 저장시 아무 값도 넘어가지 않기 때문에
서버 구현에 따라서 값이 오지 않은 것으로 판단해서 값을 변경하지 않을 수도 있다.

이런 문제를 해결하기 위해서 스프링 MVC는 약간의 트릭을 사용하는데
히든 필드를 하나 만들어서, _open 처럼 기존 체크 박스 이름 앞에 언더스코어( _ )를 붙여서 전송하면 체크를 해제했다고 인식할 수 있다.
히든 필드는 항상 전송된다.

따라서 체크를 해제한 경우 여기에서 open 은 전송되지 않고 _open 만 전송되는데
이 경우 스프링 MVC는 체크를 해제했다고 판단한다.

체크 해제를 인식하기 위한 히든 필드

<input type="hidden" name="_open" value="on"/>

실행로그

FormItemController : item.open=true //체크 박스를 선택하는 경우
FormItemController : item.open=false //체크 박스를 선택하지 않는 경우

24.1 체크 박스 체크

open=on&_open=on

체크 박스를 체크하면 스프링 MVC가 open 에 값이 있는 것을 확인하고 사용한다.
이때 _open 은 무시한다.

24.2 체크 박스 미체크

_open=on
체크 박스를 체크하지 않으면 스프링 MVC가 _open 만 있는 것을 확인하고
open 의 값이 체크되지 않았다고 인식한다.

이 경우 서버에서 Boolean 타입을 찍어보면 결과가 null 이 아니라 false로 값이 넘어가게 된다.

스프링에서 트릭으로 값을 설정하는 것이다.

25. 체크 박스 - 단일2

타임리프사용하기

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

똑같은데 th:field를 넣어서 hidden을 없앤다.

<!-- single checkbox -->
<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>

페이지 소스보기를 하면 hidden필드가 자동으로 생성해준다.

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

25.1 상품 상세 적용

상품 상세에 적용해보자.

주의할점
item.html 에는 th:object 를 사용하지 않았기 때문에 th:field 부분에 ${item.open} 으로 적어주어야 한다.
disabled 를 사용해서 상품 상세에서는 체크 박스가 선택되지 않도록 했다

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

타임리프의 체크 확인
checked="checked"
체크 박스에서 판매 여부를 선택해서 저장하면 조회시에 checked 속성이 추가된 것을 확인할 수 있다.
이런 부분을 개발자가 직접 처리하려면 상당히 번거롭다.
타임리프의 th:field 를 사용하면 값이 true인 경우 체크를 자동으로 처리해준다

jsp를 사용할때는 각각 알아서 설정해주엇었다.

25.2 상품 수정에도 적용

<!-- single checkbox -->
<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>

상품 수정도 th:object, th:field 를 모두 적용해야 한다.
실행해보면 체크 박스를 수정해도 반영되지 않는다.
실제 반영되도록 다음 코드를 수정하자
open 이외에 나머지 필드도 업데이트 되도록 미리 넣어두자

public void update(Long itemId, Item updateParam) {
    Item findItem = findById(itemId);
    findItem.setItemName(updateParam.getItemName());
    findItem.setPrice(updateParam.getPrice());
    findItem.setQuantity(updateParam.getQuantity());
    findItem.setOpen(updateParam.getOpen());
    findItem.setRegions(updateParam.getRegions());
    findItem.setItemType(updateParam.getItemType());
    findItem.setDeliveryCode(updateParam.getDeliveryCode());
}

26. 체크 박스 - 멀티

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

등록 지역
서울, 부산, 제주

체크 박스로 다중 선택할 수 있다

26.1 컨트롤러

등록폼 컨트롤러에서 처리해준다.

Map<String, String> regions = new LinkedHashMap<>();
순서가 보장되도록 LinkedHashMap을 사용할 것이다.

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

그런데 이것이 add폼 상세폼 수정폼에서 모두 다 사용한다.
이 중복코드를 없애는 방법이 존재한다.

@ModelAttribute를 사용하는것인데 우리가 아는 일반적인 @ModelAttribute와는 다르다.

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

컨트롤러를 호출할때 자동으로 regions라는 이름으로 모델에 담아서 모델에 담긴다.
어떤 컨트롤러가 호출되든 추가가 된다.

@ModelAttribute의 특별한 사용법
등록 폼, 상세화면, 수정 폼에서 모두 서울, 부산, 제주라는 체크 박스를 반복해서 보여주어야 한다.
이렇게 하려면 각각의 컨트롤러에서 model.addAttribute(...) 을 사용해서 체크 박스를 구성하는 데이터를 반복해서 넣어주어야 한다.

@ModelAttribute 는 이렇게 컨트롤러에 있는 별도의 메서드에 적용할 수 있다.
이렇게하면 해당 컨트롤러를 요청할 때 regions 에서 반환한 값이 자동으로 모델( model )에 담기게 된다.
물론 이렇게 사용하지 않고, 각각의 컨트롤러 메서드에서 모델에 직접 데이터를 담아서 처리해도 된다.

이것을 미리 세팅영역에 만들어놓고 사용하는게 성능적으로는 더 좋을 수 있다.
이정도는 성능에 큰 영향을 미치지는 않는다.

26.2 addForm.html

<!-- multi checkbox -->
<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>

체크박스를 3개만들기 위해 th:each="region : ${regions}"담는다.
돌아가면서 체크박스를 생성하는데 th:field="*{regions}"로 item에 있는 item.regions를 가져오게 된다.
value는 each로 돌리면서 나오는 regions.key를 가져온다.

자동으로 생성되는 id값을 알아서 넣어준다.
동적으로 생성할때 #ids로 자동으로 생성되는 것을 인식해서 넣어준다.

th:for="${#ids.prev('regions')}"
th:text="${region.value}" 값으로는 value를 넣어준다.

정리
th:for="${#ids.prev('regions')}"
멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있다.
그런데 문제는 이렇게 반복해서 HTML 태그를 생성할 때, 생성된 HTML 태그 속성에서 name 은 같아도 되지만,
id 는 모두 달라야 한다.
따라서 타임리프는 체크박스를 each 루프 안에서 반복해서 만들 때 임의로 1 , 2 , 3 숫자를 뒤에 붙여준다.

결과

<input type="checkbox" value="SEOUL" class="form-check-input" id="regions1" name="regions">
<input type="checkbox" value="BUSAN" class="form-check-input" id="regions2" name="regions">
<input type="checkbox" value="JEJU" class="form-check-input" id="regions3" name="regions">

HTML의 id 가 타임리프에 의해 동적으로 만들어지기 때문에 <label for="id 값"> 으로 label 의대상이 되는 id 값을 임의로 지정하는 것은 곤란하다.
타임리프는 ids.prev(...) , ids.next(...) 을
제공해서 동적으로 생성되는 id 값을 사용할 수 있도록 한다.

-> 이 label을 기준으로 이전에 잇는 코드에서 자동생성되면 prev다음이면 next인듯 하다.

서울, 부산 선택

regions=SEOUL&_regions=on&regions=BUSAN&_regions=on&_regions=on

지역 선택X

_regions=on&_regions=on&_regions=on

_regions 는 앞서 설명한 기능이다.
웹 브라우저에서 체크를 하나도 하지 않았을 때,
클라이언트가 서버에 아무런 데이터를 보내지 않는 것을 방지한다.
참고로 _regions 조차 보내지 않으면 결과는 null 이 된다.

_regions 가 체크박스 숫자만큼 생성될 필요는 없지만 타임리프가 생성되는 옵션 수 만큼 생성해서 그런 것이니 무시하자

26.3 item.html추가

<!-- multi checkbox -->
<div>
    <div>등록 지역</div>
    <div th:each="region : ${regions}" class="form-check form-check-inline">
        <input type="checkbox" th:field="${item.regions}" th:value="${region.key}" class="form-check-input" disabled>
        <label th:for="${#ids.prev('regions')}" th:text="${region.value}" class="form-check-label">서울</label>
    </div>
</div>

주의할점
item.html 에는 th:object 를 사용하지 않았기 때문에 th:field 부분에 ${item.regions} 으로 적어주어야 한다.
disabled 를 사용해서 상품 상세에서는 체크 박스가 선택되지 않도록 했다.
타임리프의 체크 확인 checked="checked"
멀티 체크 박스에서 등록 지역을 선택해서 저장하면, 조회시에 checked 속성이 추가된 것을 확인할 수 있다.
타임리프는 th:field 에 지정한 값과 th:value 의 값을 비교해서 체크를 자동으로 처리해준다.

27. 라디오 버튼

라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다.
이번시간에는 라디오 버튼을 자바 ENUM을 활용해서 개발해보자.

상품 종류
도서, 식품, 기타
라디오 버튼으로 하나만 선택할 수 있다.

27.1 FormItemController - 추가

@ModelAttribute("itemTypes")
public ItemType[] itemTypes() {
    return ItemType.values();
}

ENUM의 .values를 하면 배열로 넘겨준다.

itemTypes 를 등록 폼, 조회, 수정 폼에서 모두 사용하므로 @ModelAttribute 의 특별한 사용법을 적용하자.
ItemType.values() 를 사용하면 해당 ENUM의 모든 정보를 배열로 반환한다. 예) [BOOK, FOOD, ETC]

27.2 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>

실행 결과, 폼 전송

itemType=FOOD //음식 선택, 선택하지 않으면 아무 값도 넘어가지 않는다.

실행 로그

item.itemType=FOOD: 값이 있을 때
item.itemType=null: 값이 없을 때

체크 박스는 수정시 체크를 해제하면 아무 값도 넘어가지 않기 때문에
별도의 히든 필드로 이런 문제를 해결했다.
라디오 버튼은 이미 선택이 되어 있다면 수정시에도 항상 하나를 선택하도록 되어 있으므로 체크 박스와 달리 별도의 히든 필드를 사용할 필요가 없다.

결과

<!-- radio button -->
<div>
    <div>상품 종류</div>
    <div class="form-check form-check-inline">
        <input type="radio" value="BOOK" class="form-check-input" id="itemType1" name="itemType">
        <label for="itemType1" class="form-check-label">도서</label>
    </div>
    <div class="form-check form-check-inline">
        <input type="radio" value="FOOD" class="form-check-input" id="itemType2" name="itemType" checked="checked">
        <label for="itemType2" class="form-check-label">식품</label>
    </div>
    <div class="form-check form-check-inline">
        <input type="radio" value="ETC" class="form-check-input" id="itemType3" name="itemType">
        <label for="itemType3" class="form-check-label">기타</label>
    </div>
</div>

선택한 식품( FOOD )에 checked="checked" 가 적용된 것을 확인할 수 있다

타임리프에서 ENUM 직접 사용하기
이렇게 모델에 ENUM을 담아서 전달하는 대신에 타임리프는 자바 객체에 직접 접근할 수 있다

타임리프에서 ENUM 직접 접근
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의 패키지 위치가 변경되거나 할때 자바 컴파일러가 타임리프까지 컴파일 오류를 잡을 수 없으므로 추천하지는 않는다

패키지이름이 모두 들어가기때문이다.

'기초단계 > SPRING' 카테고리의 다른 글

2023.06.27 Spring  (0) 2023.07.15
2023.06.24 Spring  (0) 2023.06.26
2023.06.10 Spring  (0) 2023.06.22
2023.06.09 Spring  (0) 2023.06.22
2023.06.07 Spring  (0) 2023.06.22

+ Recent posts