검증을 이전처럼 필드 하나하나 지정해서 오류를 출력하는 것은 정말 귀찮은 일이다.

 

다음 코드를 보자

 

public class Item {

 private Long id;
 
 @NotBlank
 private String itemName;
 
 @NotNull
 @Range(min = 1000, max = 1000000)
 private Integer price;
 
 @NotNull
 @Max(9999)
 private Integer quantity;
}

@NotBlank : 빈 값 혹은 공백일 경우 허용X

@NotNull : null을 허용하지 X

@Range(min = 1000, max = 1000000) : 해당 범위 안의 값이어야 함

@Max(9999) : 최대 9999까지만 허용

 

이렇게 간단하게 사용할 수 있는 것이 Bean Validation이다.

 

Bean Validation은 구현체가 아니라 검증 애노테이션과 인터페이스의 모음으로 Bean Validation 2.0(JSR-380) 기술표준이다.

 

스프링 부트에 spring-boot-starter-validation 라이브러리를 넣는 순간 Bean Validator를 인지하고 스프링에 통합하여 사용시 @Valid, @Validated (둘중 하나) 만 적용하면 검증이 적용된다.

 

검증 순서로는 @ModelAttribute 각각의 필드에 타입 변환을 시도하여 성공하면 Validator 적용하고 실패하면 typeMismatch로 FieldError를 추가한다.

 

즉, 바인딩에 성공한 필드만 검증을 적용한다.

 

Bean Validation이 제공하는 오류메시지를 변경하려면

 

errors.properties

NotBlank={0} 공백X 
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}

이렇게 적용하면 되는데, {0}은 필드명이고 {1}, {2} 는 각각 애노테이션마다 다르다

 

BeanValidation이 메시지를 찾는 순서를 보면

1. 생성된 메시지 코드 순서대로 messageSource에서 메시지 찾기(바로 위의 코드)

2. 애노테이션의 message 속성 사용

@NotBlank(message = "공백입니다. {0}")

3. 라이브러리가 제공하는 기본 값 -> 공백일 수 없습니다.

 

Bean Validation에서 FieldError(필드 하나) 가 아닌 ObjectError(여러 필드)가 발생하면

@ScriptAssert(lang = "javascript", script = "_this.price * _this.quantity >= 
10000")

도메인 객체가 있는 곳에 @ScriptAssert를 이렇게 적용해주면 된다.

 

하지만 @ScriptAssert는 실제 사용시 제약이 많고 복잡한 부분이 있어 ObjectError 부분만 따로 자바코드로 작성하는 것을 권장한다.

 if (item.getPrice() != null && item.getQuantity() != null) {
 int resultPrice = item.getPrice() * item.getQuantity();
 if (resultPrice < 10000) {
 bindingResult.reject("totalPriceMin", new Object[]{10000,
resultPrice}, null);}}

 

예를 들어, 등록시에는 수량을 최대 9999까지 등록할 수 있고 id 값이 필요 없지만 수정시에는 수량을 무제한으로 변경할 수 있고 id값이 필수 값인 상황이라면 어떻게 적용해야할까?

 

해결 방법은 두가지가 있는데,

 

첫번째로는 Bean Validation의 groups 기능을 이용하면 해당 문제를 해결할 수 있다.

두번째로는 도메인 객체를 직접 사용하지 않고 ItemSaveForm, ItemUpdateForm과 같은 별도의 모델 객체를 생성하여 해결할 수 있다.

 

먼저 groups 기능먼저 살펴본다.

 

SaveCheck

public interface SaveCheck {
}

 

UpdateCheck

public interface UpdateCheck {
}

 

적용시

@NotNull(groups = UpdateCheck.class)
private Long id;
 
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Max(value = 9999, groups = SaveCheck.class)
private Integer quantity;

id 값은 수정시에만 적용해야하고, 수량값은 등록시에만 9999개 제한을 걸어야 하므로 위와같이 groups로 적용시켜준다.

 

컨트롤러로 수정을 해주어야 하는데

 

등록 컨트롤러

@PostMapping("/add")
public String addItem(@Validated(SaveCheck.class) @ModelAttribute Item item,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {
}

 

수정 컨트롤러

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated(UpdateCheck.class)
@ModelAttribute Item item, BindingResult bindingResult) {
}

 

위의 코드처럼 @Validated에 적용할 것을 지정해주면 된다.

 

이렇게 groups 기능을 알아보았지만, groups 기능은 사실 실무에서 잘 사용되지 않는다.

 

그 이유는 실무에서는 회원 등록시 데이터만 전달받는것 뿐만 아니라 약관 정보와 같은 추가적인 정보도 받아야 해서 groups기능으로 하나하나 적용하기엔 너무 복잡해진다.

 

따라서 도메인 객체를 직접 전달받는 것이 아닌 별도의 폼 객체를 만들어 전달하는 것이 효율적이다.

 

등록 폼

@Data
public class ItemSaveForm {
 @NotNull
 @Max(value = 9999)
 private Integer quantity;
}

 

수정 폼

@Data
public class ItemUpdateForm {
 @NotNull
 private Long id;
 
 private Integer quantity;
}

 

등록 폼에는 수량의 제한이 있고, 수정폼에는 제한이 없어진 것을 볼 수 있고, Id 값은 수정 폼에만 존재하는 것을 확인할 수 있다.

 

컨트롤러도 설정해주자

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form,
BindingResult bindingResult, RedirectAttributes redirectAttributes)

먼저 @ModelAttribute를 도메인 객체가 아닌 폼을 통해 받는다.

 

if (form.getPrice() != null && form.getQuantity() != null) {
	int resultPrice = form.getPrice() * form.getQuantity();
	if (resultPrice < 10000) {
		bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);}}

폼을 이용하여 검증 로직을 구현하고

 

Item item = new Item();
item.setItemName(form.getItemName());
item.setPrice(form.getPrice());
item.setQuantity(form.getQuantity());

도메인 객체를 생성하여 폼의 내용들을 도메인 객체에 입력시킨다.

 

폼(DTO)를 이용한 방법을 사용하도록 하자

'Spring > SpringMVC' 카테고리의 다른 글

스프링 로그인 - 쿠키, 세션  (0) 2023.02.12
스프링 검증  (0) 2023.02.10
메시지, 국제화  (0) 2023.02.05
타임리프 - 스프링 통합과 폼  (0) 2023.02.05
타임리프 기본 기능 모음  (0) 2023.02.04

검증은 정말 중요하다. 예를들어, 웹페이지에서 아이디 비밀번호를 입력해서 로그인 해야하는데 입력하지 않고도 통과되면 안되듯이 꼭 검증을 해줘야 한다.

 

컨트롤러의 중요한 역할 중 하나인 검증은 클라이언트 검증과 서버 검증으로 나눌 수 있다.

 

클라이언트만 검증을 할 경우 버프스위트같은 프로그램 등으로 조작할 수 있으므로 보안에 매우 취약하다.

서버만 검증하게 된다면 즉각적인 고객 사용성이 부족해진다.

 

즉, 둘을 적절히 섞어 사용해야 하며 서버 검증은 필수적이다.

 

Map<String, String> errors = new HashMap<>();

검증 오류 결과를 보관할 errors라는 Map을 만들었다.

 

if (!StringUtils.hasText(item.getItemName())) {
 errors.put("itemName", "상품 이름은 필수입니다.");
}

만약 상품의 이름이 공백이라면 에러를 띄워야 하는데, 이 때 키값은 오류가 발생한 필드명으로 하였고 value값은 출력할 오류 문자열을 넣어주었다.

 

if (item.getPrice() != null && item.getQuantity() != null) {
 int resultPrice = item.getPrice() * item.getQuantity();
 if (resultPrice < 10000) {
 errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값= " + resultPrice);
 }
}

필드 하나만 다루지 않고 여러개의 필드를 같이 다뤄서 처리해야하는 경우는 필드 이름을 넣을 수 없으므로 globalError라는 키값을 사용했다.

 

if (!errors.isEmpty()) {
 model.addAttribute("errors", errors);
 return "validation/v1/addForm";
}

검증 실패시에는 모델에 errors를 담고 입력 폼이 있는 뷰 템플릿으로 보낸다.

 

<div th:if="${errors?.containsKey('globalError')}">
 <p class="field-error" th:text="${errors['globalError']}">전체 오류메시지</p>

만약 errors에 globalError가 있다면 출력하고 없으면 출력을 안한다.

 

여기서 errors?. 부분을 보면 errors가 null일때 보통은 NullPointException을 출력하는데 이것을 대신 null으로 반환하는 타임리프 문법이다. 즉, 없다면 if=null이 되며 출력되지 않는다.

 

이제부터 타입 오류처리와 뷰 템플릿에서의 중복 제거를 해보고 고객이 입력한 문자를 화면에 남기는 것을 해본다.

 

@PostMapping("/add")
public String addItemV1(@ModelAttribute Item item, BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
    if (!StringUtils.hasText(item.getItemName())) {
	 bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
	}
    
    if (item.getPrice() != null && item.getQuantity() != null) {
 		int resultPrice = item.getPrice() * item.getQuantity();
		if (resultPrice < 10000) {
			bindingResult.addError(new ObjectError("item",
            "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice));
 		}
    }
}

BindingResult 파라미터 위치는 검증할 대상 바로 뒤에 위치해야 한다

 

필드 하나만 오류 처리할 경우 FieldError 객체를 생성하여 bindingResult에 담아주고,

여러 필드를 다룬다면 ObjectError 객체를 생성하여 bindingResult에 담아주면 된다.

 

파라미터 값은 첫번째로 @ModelAttribute 이름이 들어가고 두번째는 오류가 발생한 필드명 세번째는 오류 기본 메시지를 입력해주면 된다.

 

<div th:if="${#fields.hasGlobalErrors()}">
 <p class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">글로벌 오류 메시지</p>
</div>

<div>
 <label for="itemName" th:text="#{label.item.itemName}">상품명</label>
 <input type="text" id="itemName" th:field="*{itemName}"
 th:errorclass="field-error" class="form-control" placeholder="이름을 입력하세요">
 <div class="field-error" th:errors="*{itemName}">
 상품명 오류
 </div>
 </div>

타임리프에서는 BindingResult에 대해 편리하게 검증 오류를 표현하는 기능을 제공한다.

 

여기서 #fields를 통해 BindingReuslt가 제공하는 검증 오류에 접근할 수 있고, th:errors로 필드에 오류가 있을 때만 출력하게 하며 th:errorclass로 th:field에서 지정한 필드에 오류가 있으면 class 정보를 추가한다.

 

이렇게 하면 검증은 다 완료되었지만, 검증 실패 시 데이터가 싹 다 날라가버린다.

 

이를 방지하기 위한 방법을 살펴본다.

 

if (!StringUtils.hasText(item.getItemName())) {
 bindingResult.addError(new FieldError("item", "itemName",
item.getItemName(), false, null, null, "상품 이름은 필수입니다."));
 }

파라미터를 살펴보면

 

1. 오류가 발생한 객체 이름

2. 오류 발생 필드명

3. 사용자가 입력한 값(검증에 실패한 값)

4. 타입 오류인지 검증 실패인지 구분 해주는 값

5. 메시지 코드

6. 메시지에서 사용하는 인자

7. 기본 오류 메시지

 

순이다.

 

사용자의 입력한 값이 컨트롤러의 @ModelAttribute에 바인딩 되는 시점에 오류가 발생하게 되면 모델 객체에서 사용자가 입력한 값을 유지하기 힘들다.

 

예를 들어 문자열을 받아야하는데 숫자를 받아버리면 다른 타입이므로 보관할 방법이 없다. 이러한 이유 때문에 오류 발생시 사용자 입력 값을 별도로 보관해야한다. 그리고 보관한 입력 값을 검증 오류 발생시 화면에 다시 출력하면 된다.

 

FieldError는 오류 발생시 사용자 입력 값을 저장하는 기능을 제공한다.

 

추가로 타임리프에서의 사용자 입력 값을 어떻게 유지하냐면

th:field="*{price}"

th:field로 지정해주면 오류 발생시 FieldError에서 보관한 값을 사용하여 출력한다.

 

타입오류로 바인딩에 실패하면 FieldError를 생성하여 사용자가 입력한 값을 넣어둔다. 그리고 해당 오류를 BindingReuslt에 담아 컨트롤러를 호출한다. 이렇게 된다면 타입 오류 발생시에도 오류 메시지를 정상 출력할 수 있다.

 

오류메시지에 대해 살펴보자

 

먼저 오류 메시지를 담아두는 errors.properties라는 파일을 만들고,

스프링 부트가 해당 메시지 파일을 인식할 수 있게

spring.messages.basename=messages,errors

 

errors.properties

required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}

 

errors를 추가해준다. 여기서 {0} {1}은 파라미터 값으로 이해하면 된다.

 

if (!StringUtils.hasText(item.getItemName())) {
 bindingResult.addError(new FieldError("item", "itemName",
item.getItemName(), false, new String[]{"required.item.itemName"}, null,
null));}
 
 if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
 bindingResult.addError(new FieldError("item", "price", item.getPrice(),
false, new String[]{"range.item.price"}, new Object[]{1000, 1000000}, null));}
 
 if (item.getQuantity() == null || item.getQuantity() > 10000) {
 bindingResult.addError(new FieldError("item", "quantity",
item.getQuantity(), false, new String[]{"max.item.quantity"}, new Object[]
{9999}, null));}

 if (item.getPrice() != null && item.getQuantity() != null) {
 int resultPrice = item.getPrice() * item.getQuantity();
 if (resultPrice < 10000) {
 bindingResult.addError(new ObjectError("item", new String[]
{"totalPriceMin"}, new Object[]{10000, resultPrice}, null));}}

사용시 위의 방식처럼 5번째 파라미터 값에 new String[]{"error message 키 값"} 으로 메시지 코드를 지정해 줄 수 있다.

여기서 메시지 코드가 배열 형식으로 여러 값을 전달할 수 있는데 순서대로 매칭되어서 처음 매칭되는 메시지가 사용된다.

 

6번째 파라미터 값에는 파라미터값이 있다면 파라미터 값을 new Object[]{0번째 파라미터 값, 1번째 파라미터값} 형식으로 사용한다.

 

하지만 이렇게 사용할 경우 파라미터값이 너무 많아 사용하기에 불편하다.

 

BindingResult가 제공하는 rejectValue(), reject()를 사용하면 간단히 검증할 수 있다.

 

if (!StringUtils.hasText(item.getItemName())) {
 bindingResult.rejectValue("itemName", "required");}
 
 if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
 bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);}

 if (item.getQuantity() == null || item.getQuantity() > 10000) {
 bindingResult.rejectValue("quantity", "max", new Object[]{9999}, null);}

 if (item.getPrice() != null && item.getQuantity() != null) {
 int resultPrice = item.getPrice() * item.getQuantity();
 if (resultPrice < 10000) {
 bindingResult.reject("totalPriceMin", new Object[]{10000,
resultPrice}, null);}}

해당 코드는 위의 코드와 같은 코드이고 매우 간소화된 것을 볼 수 있다.

 

rejectValue, reject()의 파라미터는 (둘은 필드에러냐 오브젝트에러냐의 차이)

1. 오류 필드명

2. 오류 코드

3. 오류 메시지에서의 파라미터 값 ex) {0} {1}

4. 기본 메시지

 

로 구성되어 있다.

 

여기서 왜 여기서는 오류가 발생한 객체 이름을 안묻는지 의문이 들 수 있다.

 

그 이유는 BindingResult는 무조건 검증할 @ModelAttribute 객체 뒤에 위치해야 하므로 BindingResult는 검증할 객체 대상을 이미 알고있으므로 생략 가능하다.

 

오류 코드는 자세하게 만들 수도 있고, 간단하게 만들 수도 있다.

 

예를 들어,

required.item.itemName:상품 이름은 필수 입니다.

이렇게 자세하게 적용 객체와 필드를 지정해서 할 수도 있고,

required: 필수 값 입니다.

간단하게 적용할 수도 있는데, 우선순위는 구체적인 것이 더 높다.

 

우선순위를 자세히 알아보기 위해 MessageCodesResolver 인터페이스의 구현체인 DefaultMessageCodesResolver 구현체를 알아본다.

 

객체 오류시에

1. code + "." + 객체명 ex) required.item

2. code ex) required

 

필드 오류시에

1. code + "." + 객체 이름 + "." + 필드 ex) required.item.itemName

2. code + "." + 필드 ex) required.itemName

3. code + "." + 필드 타입 ex) required.String

4. code ex) required

 

순서이다. rejectValue()나 reject() 메서드는 내부에서 MessageCodesResolver를 사용하여 메시지 코드를 생성한다.

 

중요한 것은 구체적으로 작성한 것이 우선순위가 높고 추상적일수록 우선순위가 낮다.

 

즉, 이것들을 종합한 순서는 다음과 같다.

 

1. rejectValue() 호출

2. MessageCodesResolver를 사용해 검증 오류 코드로 메시지 코드들 생성

3. new FieldError()를 생성하며 메시지 코드들 보관

4. th:errors 에서 메시지 코드들로 메시지를 순서대로 메시지에서 찾고 출력

 

그리고 타입 오류가 발생하면 스프링은 typeMismatch라는 오류코드를 출력하는데 이도 4가지의 메시지 코드가 출력되는데 몇가지만 고쳐서

typeMismatch.java.lang.Integer=숫자를 입력해주세요.
typeMismatch=타입 오류입니다.

이런 형태로 메시지를 지정하여 출력할 수 있다.

 

이러한 검증 로직들이 모두 컨트롤러에 한꺼번에 들어가있으면 가독성도 안좋을 뿐더러 배보다 배꼽이 더 큰 꼴이 되어버린다. 그래서 별도의 클래스로 분리해주어야 한다.

 

ItemValidaotr.class

@Component
public class ItemValidator implements Validator {

 @Override
 public boolean supports(Class<?> clazz) {
 return Item.class.isAssignableFrom(clazz);}
 
 @Override
 public void validate(Object target, Errors errors) {
 Item item = (Item) target;
 ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName","required");
 
 if (item.getPrice() == null || item.getPrice() < 1000 ||
item.getPrice() > 1000000) {
 errors.rejectValue("price", "range", new Object[]{1000, 1000000},
null);}

 if (item.getQuantity() == null || item.getQuantity() > 10000) {
 errors.rejectValue("quantity", "max", new Object[]{9999}, null);}

 if (item.getPrice() != null && item.getQuantity() != null) {
 int resultPrice = item.getPrice() * item.getQuantity();
 if (resultPrice < 10000) {
 errors.reject("totalPriceMin", new Object[]{10000,
resultPrice}, null);}}}}

Validator라는 인터페이스를 이용하면 먼저 supports()로 검증기가 지원하는지 여부를 확인하고, validate()로 검증 객체와 BindingResult를 받아 검증하는 로직을 작성한다.

 

private final ItemValidator itemValidator;

itemValidator.validate(item, bindingResult);

사용시에는 의존관계 주입을 해주고, validate() 메서드로 검증해주면 된다.

 

여기서 추가적으로 스프링의 도움을 받아보자

 

@InitBinder
public void init(WebDataBinder dataBinder) {
 log.info("init binder {}", dataBinder);
 dataBinder.addValidators(itemValidator);
}

@InitBinder는 해당 컨트롤러에만 영향을 주고,

 

WebDataBinder는 스프링의 파라미터 바인딩 역할을 해주고 검증 기능도 내부에 포함하므로 검증기를 자동으로 적용할 수 있다.

 

@PostMapping("/add")
public String addItemV6(@Validated @ModelAttribute Item item, BindingResult 
bindingResult, RedirectAttributes redirectAttributes)

이렇게 validator를 직접 호출하는 부분을 제거하고, 파라미터로 @Validated를 입력하니 기존과 동일하게 작동하는 것을 확인할 수 있다.

 

@Validator는 검증기를 실행하라는 애노테이션으로 WebDataBinder에 등록된 검증기를 찾아 실행한다. 만약 여러 검증기가 등록 되어있다면, 어떤 검증기가 실행되어야 하는지 support() 메서드로 구분하여 처리한다.

'Spring > SpringMVC' 카테고리의 다른 글

스프링 로그인 - 쿠키, 세션  (0) 2023.02.12
스프링 검증 - Bean Validation  (0) 2023.02.11
메시지, 국제화  (0) 2023.02.05
타임리프 - 스프링 통합과 폼  (0) 2023.02.05
타임리프 기본 기능 모음  (0) 2023.02.04

기존에 정해놓았던 문구가 있었는데, 업체가 문구를 바꿔달라고 요청한다면 해당 문구가 적혀있는 모든 코드들을 수정해야해서 유지보수 측면에서 본다면 하드코딩된 문구들이 많은 것은 좋지 않다. 그리고 해외에서 접속 시 한글이 아닌 영어로 페이지를 나타내고 싶다면 어떻게 해야할까?

 

이것을 해결해줄 메시지와 국제화에 대해 소개한다.

 

메시지

동적으로 문구를 출력하려면 먼저 message.properties 파일을 만들어주고 파일 내에 적용시킬 키워드와 값을 지정해준다.

 

message.properties

page.addItem=상품 등록
page.name=페이지 이름은 {0}

여기서 {0}은 파라미터 값으로 여기에 값을 지정해서 넣어줄 수 있다.

 

타임리프에서 활용시

<h2 th:text="#{page.addItem}">상품 등록</h2>
<p th:text="#{page.name(${item.itemName})}">페이지 이름은 상품</p>

타임리프로 적용시 #{적용시킬메시지} 해주면 message.properties 내의 문구만 수정한다면 동적으로 적용이 되고,

파라미터 값은 ()괄호 안에 지정해주면 파라미터 값이 출력된다.

 

 

국제화

해외는 보통 영어로 소통하니 영어로 된 메시지를 만들기 위해 message_en.properties 파일을 만들고 똑같이 메시지를 지정해준다.

 

message_en.properties

page.addItem=Item Add

이렇게하고 브라우저의 언어를 영어로 바꾸면 영어로 출력되는 것을 확인할 수 있다.

 

이 원리는 Accept-Language 헤더값에 따라 동적으로 출력되는 원리이다.

'Spring > SpringMVC' 카테고리의 다른 글

스프링 로그인 - 쿠키, 세션  (0) 2023.02.12
스프링 검증 - Bean Validation  (0) 2023.02.11
스프링 검증  (0) 2023.02.10
타임리프 - 스프링 통합과 폼  (0) 2023.02.05
타임리프 기본 기능 모음  (0) 2023.02.04

입력 폼 처리

th:object : 커맨드 객체를 지정

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

th:field : HTML 태그의 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>
 </form>

모델로 넘겨받은 item 객체를 th:object로 지정하고 *{...} 선택변수식으로 간단히 적용할 수 있다.

*{itemName}은 모델로 넘겨받은 item.itemName 혹은 item.getItemName과 같다.

 

th:field는 위의 설명처럼 id, name value 속성을 자동으로 처리해주므로 렌더링 시 id="itemName", name="itemName", value="" 로 속성이 생성된다.

 

렌더링 전

<input type="text" id="itemName" th:field="*{itemName}" class="form-control" placeholder="이름을 입력하세요">

렌더링 후

<input type="text" id="itemName" class="form-control" placeholder="이름을 입력하세요" name="itemName" value="">

 

체크박스

체크박스는 선택이 안되면 클라이언트에서 서버로 값을 보내지 않는다. 이로인해 다양한 문제가 발생할 수 있는데, 스프링 MVC에서는 해당 문제를 해결하기 위해 히든 필드를 만들어서 체크와 체크해제를 인식한다.

<input type="checkbox" id="open" name="open" class="form-check-input">
<input type="hidden" name="_open" value="on"/>

체크가 되면 open=on&_open=on 가 되어 open에 값이 있음을 확인하여 체크되어있음을 알 수 있다.

체크를 하지 않으면 _open=on 만 남게 되어 open에 값이 없으므로 체크되지 않음을 알 수 있다.

 

타임리프를 적용한 체크박스

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

렌더링 후

<input type="checkbox" id="open" class="form-check-input" name="open" value="true">
<input type="hidden" name="_open" value="on"/>
<label for="open" class="form-check-label">판매 오픈</label>

타임리프를 이용하면 체크 박스의 히든 필드를 자동으로 생성해주고 체크시 true 미체크시 false를 반환한다.

체크 박스에서 체크시 checked 속성이 추가되는데 th:field를 사용하면 true인 경우 자동으로 처리해준다.

 

멀티 체크박스

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

멀티 체크박스는 같은 이름의 여러 체크박스를 만들 수 있는데, name은 같아도 되지만 문제는 id 값은 중복이 되면 안된다.

each 루프를 돌릴 때 th:field가 중복이 발생하지 않게 1, 2, 3 과같은 숫자를 붙여준다.

그리고 label for은 동적으로 생성된 아이디를 참조해야 하므로 th:for="${#ids.prev('regions')}"로 매핑한다.

 

라디오 버튼

자바 ENUM을 활용하여 배열형태로 개발

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

폼 전송시 값이 있다면 item.itemType=FOOD를 없다면 item.itemType=null을 출력한다.

여기서 type.description은 enum의 메서드로 키워드의 값을 가져온다.

 

셀렉트 박스

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

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

<select> 태그를 사용하고 option태그로 선택지들을 만든다.

'Spring > SpringMVC' 카테고리의 다른 글

스프링 로그인 - 쿠키, 세션  (0) 2023.02.12
스프링 검증 - Bean Validation  (0) 2023.02.11
스프링 검증  (0) 2023.02.10
메시지, 국제화  (0) 2023.02.05
타임리프 기본 기능 모음  (0) 2023.02.04

+ Recent posts