RE-Heat 개발자 일지

스프링 MVC 2편 - [2] 스프링 통합과 폼 본문

백엔드/스프링

스프링 MVC 2편 - [2] 스프링 통합과 폼

RE-Heat 2023. 7. 12. 23:56

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard

 

스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 - 인프런 | 강의

웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실무 웹 개발에 필요한 모든 활용 기술들을 학습할 수 있

www.inflearn.com

인프런 김영한 님의 스프링 MVC 2편 강의를 토대로 정리한 내용입니다.

 

[1] 타임리프 스프링 통합

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

- 스프링의 SpringEL 문법 통합

- ${@myBean.doSomething()}처럼 스프링 빈 호출 지원

- 편리한 폼 관리를 위한 추가 속성

    th:object (기능 강화, 폼 커맨드 객체 선택)

    th:field, th:errors, th:errorclass

- 폼 컴포넌트 기능

    checkbox, radio button, List 등을 편리하게 사용할 수 있는 기능 지원

- 스프링의 메시지, 국제화 기능의 편리한 통합

- 스프링의 검증, 오류 처리 통합

- 스프링의 변환 서비스 통합(ConversionService)

 

■ 설정방법

타임리프 템플릿 엔진을 스프링 빈에 등록하고 타임리프용 뷰 리졸버를 따로 등록해야 하지만, 스프링 부트는 이런 부분을 자동으로 등록해 준다.

 

 

[2] 입력 폼 처리

■ 입력 폼 처리

th:object 커맨드 객체를 지정한다.

*{...} : 선택 변수 식. th:object에서 선택한 객체에 접근한다.

th:filed

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

 

 

FormItemController - addform

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

빈 item 객체를 model에 담아 넘긴다.

 

 

item.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>

① th:object="${item}" : form에서 사용할 객체 지정

② th:filed="*{item}"

    변수식 *{itemName}은 ${item.itemName}은 과 같다. th:object로 item을 지정해 줬으므로 선택 변수식을 적용할 수 있다.

 

 

실행화면

name="itemName"을 지워도 th:field="*{itemName}"값을 적용하면 자동으로 name값을 준다. id나 value도 자동으로 주어진다.

추가로 만일 th:filed에 th:field="*{itemNamexxxx}" 같이 값을 잘못 입력하면 오류 메시지가 떠서 개발할 때 편리하다.

IntelliJ 유료 버전은 오류 메시지를 더 자세하게 제공한다.

 

 

 

[3] 요구사항 추가

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

이 챕터에선 그 전에 판매여부, 등록지역, 상품 종류 등 요구사항을 추가하고 이에 맞는 상품종류, 배송방식, 상품 클래스를 추가한다.

 

 

예시 이미지

 

 

ItemType - 상품종류

public enum ItemType {
    BOOK("도서"), FOOD("음식"), ETC("기타");

    private final String description;

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

참고] 데이터 중 한정된 값만 갖는 데이터 타입이 열거 타입(enumeration type)이다. 

   ex) 월화수목금토일, 봄·여름·가을·겨울 등

 

 

DeliveryCode - 배송 방식

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

@Data
@AllArgsConstructor
public class DeliveryCode {
    private String code; //시스템 이름 FAST
    private String displayName; //빠른배송
}

 

 

Item - 상품 Data

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

참고] 롬복의 @Data는 @Getter/@Setter, @ToString, @EqualsAndHashCode @RequiredArgsConstructor 등을 합친 종합 선물세트. 

 

 

 

[4] 체크 박스 - 단일 1

 

addForm.html

<hr class="my-4">
<!-- 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>

 

 

FormItemController - addItem 일부 발췌

@PostMapping("/add")
public String addItem(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
    log.info("item.open={}", item.getOpen());

FormItemController 위에 @Slf4j를 추가한 뒤 로깅 작성.

 

실행로그

체크박스 선택 : item.open=true

체크박스 선택 X : item.open=null

HTML Form에서 체크박스를 체크하지 않으면 open값 자체가 넘어오지 않는다는 것을 확인할 수 있다.

 

■ HTML checkbox의 단점

HTML 체크박스는 선택이 되지 않으면 서버로 값 자체를 보내지 않는다. 그래서 사용자가 의도적으로 체크박스를 해제해도 값이 넘어온 지 판단할 수 없다는 게 문제점이다.

 

■ 해결책 - 히든 필드

스프링 MVC는 name="_open"처럼 기존 체크박스 이름에 _를 붙여서 전송하면 체크를 해제했다고 인식한다.

 

 

addForm.html - 히든 필드 추가

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

체크 박스를 체크하면 open=on&_open=on

체크 박스를 해제하면 _open=on 만 전송돼 구분 가능

 

체크 박스 해제 시 html FormData에선 _open:on만 넘어가는 것을 확인할 수 있으며, 실행로그를 살펴보면 item.open은 null이 아닌 false인 것을 알 수 있다. 

 

 

 

[5] 체크 박스 - 단일2

모든 input에 히든 필드를 일일이 추가하는 것은 상당히 번거로운 일이다. 그래서 타임리프는 이를 자동으로 처리하는 기능을 제공한다.

 

addForm.html - 타임리프 th:filed 추가

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

 

 

addForm.html - 렌더링 후 

자동으로 hidden 필드가 추가된 것을 확인할 수 있다.

 

■ 타임리프의 체크 확인

html에선 체크 박스를 체크하면 checked="checked" 속성이 추가되지만, 체크박스를 해제하면 아예 생략돼 의도적으로 체크를 해제(수정)했는지 판별하기 어렵다. 그래서 타임리프에선 th:field를 사용하면 값이 true일 때 checked를 자동으로 처리해 주는 기능을 제공한다.

 

 

[6] 체크 박스 - 멀티

 

예시

 

■ @ModelAttribute의 특별한 사용법

위 사항을 충족하려면 각각의 컨트롤러에 model.attribute(...)를 사용해 체크박스를 구성하는 데이터를 반복해서 넣어주어야 한다.

@ModelAttribute 적용 전

 

중복을 방지하기 위해 스프링 MVC는 @ModelAttribute를 제공한다.

아래와 같은 코드를 추가하면 해당 컨트롤러를 요청할 때마다 regions에서 반환한 값이 자동으로 모델에 담기게 된다.

@ModelAttribute("regions")
public Map<String, String> regions(){
    Map<String, String> regions = new LinkedHashMap<>(); //Hashmap을 쓰면 순서가 보장이 안되어서 LinkedHashMap으로 함.
    regions.put("SEOUL", "서울");
    regions.put("BUSAN", "부산");
    regions.put("JEJU", "제주");
    return regions;
}

참고] 이 값이 동적으로 변하지 않는다면 static 메소드화 해서 쓰는 게 바람직하나, @ModelAttribute를 쓴다고 성능이 유의미하게 저하되진 않는다. 

 

 

addForm.html - multi checkbox 추가

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

① th:for="${#ids.prev('regions')}"

th:each로 같은 이름(name)을 가진 여러 체크 박스를 만들 수 있으나, HTML 태그 속성에서 id는 중복되어선 안 된다. 그래서 타임리프는 each 루프 안에서 체크 박스를 반복해 만들 때 임의로 1, 2, 3 순서로 숫자를 붙여준다. 

 

① id값에 regions1, regions2 식으로 숫자가 추가된 것을 확인할 수 있다.

② th:field로 히든 필드에 _regions가 전부 들어가는 것도 확인할 수 있다. 히든 필드가 체크박스 숫자만큼 생성될 필요는 없지만, 문제 되지 않으므로 무시하도록 하자.

 

 

실행화면 - 서울·제주 체크

 

 

 

[7] 라디오 버튼

라디오 버튼은 여러 선택지 중에 하나를 선택할 때 사용할 수 있다.

 

FormItemController - itemTypes()

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

ItemType.values()로 Enum의 모든 정보를 배열로 반환

 

참고] Enum의 사용법

① values() : 열거된 모든 원소를 배열에 담아 순서대로 리턴

② ordinal() : 원소에 열거된 순서를 정수 값으로 리턴

    ex) ItemType it = ItemType.BOOK;
          System.out.println(it.ordinal()); => 결과값 : 1 (순서는 도서, 음식, 기타 순)

③ valueOf() : 매개변수로 주어진 String과 열거형에 일치하는 이름을 갖는 원소를 리턴

    ex) ItemType it = ItemType.valueOf("BOOK")
          System.out.println(it); => 결과값 : 1 (순서는 도서, 음식, 기타 순)

 

 

 

addForm.html - radio button

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

① th:value = "${type.name()}" => BOOK, FOOD, ETC

② th:text= "${type.description}" => 도서, 음식, 기타

 

실행화면 - 소스보기

라디오 버튼은 한 번 체크하면 null값을 보낼 수 없다. 따라서 체크 박스와는 달리 별도의 히든 필드가 필요 없다.

 

■ 타임리프에서 ENUM 직접 접근

<!-- radio button -->
<div>
    <div>상품 종류</div>
    <div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}"
         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>

<div th:each="type : ${T(hello.itemservice.domain.item.ItemType).values()}">

스프링 EL문법으로 ENUM을 직접 사용할 수 있다. ENUM에 values()를 호출하면 해당 ENUM의 모든 정보가 배열로 반환된다. 하지만 이렇게 사용하면 ENUM의 패키지 위치가 변경되거나 할 때 자바 컴파일러가 타임리프 컴파일 오류까지 잡기 힘드므로 추천하진 않는다.

 

[8] 셀렉트 박스

셀렉트 박스는 여러 선택지 중 하나를 선택할 때 사용할 수 있다.

 

FormItemController - deliveryCodes()

@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 - SELECT

<!-- SELECT -->
<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>

페이지 소스보기

타임리프가 옵션값을 차례대로 넣어주고, 느린 배송을 선택하면 selected="selected"까지 넣어주고 유지해 주는 것을 확인할 수 있다.