728x90
Bean Validation
- 특정 구현체가 아닌 Bean Validation2.0 (JSR-380)이라는 기술 표준이다.
- 여러 검증 애노테이션과 여러 인터페이스의 모음으로 구성되어 있다.
- 이러한 Bean Validation을 구현한 기술 중 일반적으로 사용하는 구현체는 하이버네이트 Validator다.
- Bean Validatior를 활용하면 애노테이션 기반으로 각종 구현 로직을 간단하게 적용할 수 있다.
- 공식 사이트
http://hibernate.org/validator/
- 공식 메뉴얼
https://docs.jboss.org/hibernate/validator/6.2/reference/en-US/html_single/
- 검증 애노테이션 모음
🔍 Bean Validation 의존관계 추가
implementation 'org.springframework.boot:spring-boot-starter-validation'
- Bean Validation을 사용하기 위해선 build.gradle 파일에서 위의 코드를 추가해줘야 한다.
💡 Bean Validation 애노테이션 적용
- javax.validation 으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스다.
- org.hibernate.validator로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증 기능이다.
- 실무에서 대부분 하이버네이트 validator를 사용한다.
📌 Item 객체
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min=1_000, max=1_000_000)
private Integer price;
@NotNull
@Max(9_999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- @NotBlank : 빈값 + 공백만 있는 경우 허용하지 않는다.
- @NotNull : null 값을 허용하지 않는다.
- @Range(min = 1000, max = 10000) : 범위 안의 값이어야 한다.
- @Max(9999) : 최대 9999까지만 허용한다.
💡 검증기 생성
- 스프링과 통합하면 우리가 직접 이런 검증기를 작성하지 않지만 이렇게 사용하는구나 정도만 참고할 것
검증기 생성
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
검증 실행
Set<ConstraintViolation<Item>> violations = validator.validate(item);
📌 테스트 코드 (검증기 직접 구현)
package hello.itemservice.validation;
import hello.itemservice.domain.item.Item;
import org.junit.jupiter.api.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
public class BeanValidationTest {
@Test
void beanValidation() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Item item = new Item();
item.setItemName(" "); // 공백
item.setPrice(0);
item.setQuantity(10000);
Set<ConstraintViolation<Item>> validate = validator.validate(item);
for (ConstraintViolation<Item> violation : validate) {
System.out.println("violation = " + violation);
System.out.println("violation.getMessage() = " + violation.getMessage());
}
}
}
violation = ConstraintViolationImpl{interpolatedMessage='1000에서 1000000 사이여야 합니다', propertyPath=price, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{org.hibernate.validator.constraints.Range.message}'}
violation.getMessage() = 1000에서 1000000 사이여야 합니다
violation = ConstraintViolationImpl{interpolatedMessage='9999 이하여야 합니다', propertyPath=quantity, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{javax.validation.constraints.Max.message}'}
violation.getMessage() = 9999 이하여야 합니다
violation = ConstraintViolationImpl{interpolatedMessage='공백일 수 없습니다', propertyPath=itemName, rootBeanClass=class hello.itemservice.domain.item.Item, messageTemplate='{javax.validation.constraints.NotBlank.message}'}
violation.getMessage() = 공백일 수 없습니다
- 오류 메시지는 하이버네이트가 직접 생성해준다.
Bean Validation - 스프링 적용
- 스프링 부트가 spring-boot-starter-validation 라이브러리를 추가하면 자동으로 Bean Validator로 등록한다.
- LocalValidatorFactoryBean이 글로벌 Validator로 등록되며 @NotNull 과 같은 애노테이션 검증을 수행한다.
- 이렇게 글로벌 Validator가 적용되어 있기 때문에 @Valid / @Validated 애노테이션만 적용하면 된다.
- 또한 검증 오류 발생시 FieldError, ObjectError 객체를 생성해서 BindingResult에 담는다.
- 스프링 부트 설정 클래스에서 임의로 글로벌 Validator를 등록하면 스프링 부트는 Bean Validator를 글로벌 Validator로 등록하지 않는다.
📌 Item (검증 객체)
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min=1_000, max=1_000_000)
private Integer price;
@NotNull
@Max(9_999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- 검증 받고자하는 필드에 @NotBalnk 와 같은 검증 애노테이션을 붙이면 된다.
📌 컨트롤러
@Controller
@Slf4j
@RequestMapping("/validation/v3/items")
@RequiredArgsConstructor
public class ValidationItemControllerV3 {
private final ItemRepository itemRepository;
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 검증 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/addForm";
}
// 검증 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
}
- 검증 객체에 @Validated 검증 애노테이션을 붙여주고 검증 결과를 담기 위해 BindingResult 클래스를 바로 다음 위치에 매개변수로 설정한다.
🔍 검증 순서
- @ModelAttribute 애노테이션이 붙은 객체의 각각의 필드에 타입 변환을 시도한다.
- 타입 변환 성공시, 다음 필드 진행
- 타입 변환 실패시, typeMismatch로 FieldError 객체가 생성된다.
- Validator 적용
💡 바인딩에 성공한 필드만 Bean Validation이 적용된다.
- Bean Validator는 바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다.
- 모델 객체에 바인딩 받는 값이 정상으로 들어와야 검증의 의미가 있기 때문이다.
예)
- itemName에 문자 "A" 값이 들어온다. → 타입 변환 성공 → itemName 필드에 Bean Validation이 적용된다.
- price에 문자 "A" 값이 들어온다. → "A"를 숫자 타입 변환 실패 → typeMismatch FieldError 추가 → price 필드는 Bean Validation이 적용되지 않는다.
Bean Validation - 에러 코드
- Bean Validation을 적용하면 bindingResult에 등록된 검증 오류 코드가 검증 애노테이션 이름으로 등록된다. (MessageCodesResolver 사용)
예)
- @NotBlank
- NotBlank.item.itemName
- NotBlank.itemName
- NotBlank.java.lang.String
- NotBlank
- @Range
- Range.item.price
- Range.price
- Range.java.lang.Integer
- Range
📌 errors.properties
#Bean Validation 추가
NotBlank={0} 공백X
Range={0}, {2} ~ {1} 허용
Max={0}, 최대 {1}
- 에러 메시지 등록
- {0} 값은 검증 객체의 필드 명이다.
- {1}, {2}, ... 값은 애노테이션 속성 값이다.
🔍 Bean Validation 메시지 찾는 순서
- 생성된 메시지 코드 순서대로 messageSource에서 메시지를 찾는다.
- 매칭되는 메시지가 없으면 애노테이션의 message 속성을 통해 기본 메시지가 나온다.
- 예) @NotBlank(message = "공백! {0}")
Bean Validation - 오브젝트 오류
- @ScriptAssert() 애노테이션을 사용하면 Bean Validation에서 특정 필드(FieldError)가 아닌 오브젝트 관련 오류(ObjectError)를 처리할 수 있다.
📌 검증 객체 (Item)
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import org.hibernate.validator.constraints.ScriptAssert;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
@ScriptAssert(lang="javascript", script = "_this.price * this.quantity >= 10000", message = "총합 10000원 넘게 입력해주세요.")
public class Item {
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min=1_000, max=1_000_000)
private Integer price;
@NotNull
@Max(9_999)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- 다음과 같은 순서로 메시지 코드를 찾는다.
- ScriptAssert.item
- ScriptAssert
🔍 @ScriptAssert 문제점
- 실제 사용해보면 제약이 많고 복잡하다.
- 실무에서는 검증 기능이 해당 객체의 범위를 넘어서는 경우가 종종 생기는데 그 때 마다 대응하기가 어렵다.
- 그러므로 @ScriptAssert 애노테이션을 억지로 사용하는 것 보다 오브젝트 오류 관련 부분만 직접 자바 코드로 작성하는 것을 권한다.
🔍 오브젝트 오류 사용 방법 - 직접 자바 코드 구현
- @ScripteAssert() 애노테이션을 쓰기보단 직접 자바 코드로 구현할 것.
📌 컨트롤러
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 특정 필드가 아닌 복합 룰 검증
if(item.getPrice()!=null && item.getQuantity()!=null) {
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice<10_000) {
bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
}
}
...
}
- 이처럼 검증 객체에 대한 부분은 직접 자바 코드로 작성하는 것이 더 좋다.
- 만약 코드 중복이 걱정된다면 따로 메서드로 분리하면 된다.
🧷 정리
- 간단한 필드 검증에는 Bean Validation을 이용하여 검증 애노테이션( @Validated )을 이용한다.
- 복잡한 객체 검증에는 제약이 많은 애노테이션( @ScriptAssert() ) 애노테이션을 이용하기 보단 자바 코드로 직접 구현한다.
- 코드 재사용성이 높다면 모듈화 할 것.
Bean Validation의 한계
🔍 한계점
- 검증 조건을 등록할 때와 수정할 때를 서로 다르게 적용할 수가 없다.
- 예) 등록할 때는 최대 수량을 9999가지 설정하고, 수정할 때는 최대 수량 제한 해제
🔍 문제 해결
- 동일한 모델 객체를 상황에 따라 각각 다르게 검증하는 방법은 2가지가 있다.
- Bean Validation의 groups 기능 사용
- 사용이 복잡하기 때문에 실무에서 잘 안쓰인다.
- Item 객체를 직접 사용하지 않고, ItemSaveForm, ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어서 사용
- Bean Validation의 groups 기능 사용
💡 Bean Validation의 groups 기능 사용
- @Validated 애노테이션은 groups 속성 기능을 지원한다. ( @Valid 애노테이션은 groups 기능이 없다.)
- 등록시에 검증할 기능과 수정시 검증할 기능을 각각 그룹으로 나눠 적용한다.
- groups 기능을 사용하면 검증 객체의 코드가 많이 복잡해진다. 그러므로 실무에서 잘 쓰이지 않는다.
- 실무에서는 두 번째 방법인 등록용 폼 객체와 수정용 폼 객체를 분리해서 사용한다.
📌 저장용 groups
package hello.itemservice.domain.item;
public interface SaveCheck {
}
📌 수정용 groups
package hello.itemservice.domain.item;
public interface UpdateCheck {
}
📌 검증 객체 (Item)
package hello.itemservice.domain.item;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.sql.Savepoint;
@Data
//@ScriptAssert(lang="javascript", script = "_this.price * this.quantity >= 10000", message = "총합 10000원 넘게 입력해주세요.")
public class Item {
@NotNull(groups = UpdateCheck.class) // 수정 요구 사항 추가
private Long id;
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
private String itemName;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class})
@Range(min=1_000, max=1_000_000, groups = {Savepoint.class, UpdateCheck.class})
private Integer price;
@NotNull(groups = {Savepoint.class, UpdateCheck.class})
@Max(value = 9_999, groups = SaveCheck.class)
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- groups는 다수의 그룹도 설정할 수 있으며 필요에 따라 맞는 그룹을 선택해 검증할 수 있다.
📌 등록 컨트롤러
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute Item item, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 특정 필드가 아닌 복합 룰 검증
if(item.getPrice()!=null && item.getQuantity()!=null) {
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice<10_000) {
bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
}
}
// 검증 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/addForm";
}
// 검증 성공 로직
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v3/items/{itemId}";
}
- @Validated 애노테이션에 속성으로 SaveChack.class를 사용했다.
- Item 객체는 각각 @Validated 애노테이션에 작성된 인터페이스가 선언된 검증만 수행한다.
📌 수정 컨트롤러
@PostMapping("/{itemId}/edit")
public String editV2(@PathVariable Long itemId, @Validated(UpdateCheck.class) @ModelAttribute Item item, BindingResult bindingResult) {
// 특정 필드가 아닌 복합 룰 검증
if(item.getPrice()!=null && item.getQuantity()!=null) {
int resultPrice = item.getPrice() * item.getQuantity();
if(resultPrice<10_000) {
bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v3/editForm";
}
itemRepository.update(itemId, item);
return "redirect:/validation/v3/items/{itemId}";
}
- @Validated 애노테이션에 속성으로 UpdateCheck.class를 사용했다.
- Item 객체는 각각 @Validated 애노테이션에 작성된 인터페이스가 선언된 검증만 수행한다
💡 Form 전송 객체 분리
- 상품 등록과 상품 수정시 사용자와 주고 받을 전용 폼 전달 객체를 만들어서 사용한다.
- 상황에 맞는 전용 폼객체를 따로 만들어서 상황에 맞게 검증을 하고, 전송 객체이기에 사용자에게 노출해도 상관 없는 객체가 된다.
- 물론 이렇게 구현할 경우 도메인 객체를 한 번 더 변환을 해야하는 추가 과정이 생기지만 별도의 폼 객첼르 만들기 때문에 검증이 중복되지 않는다.
- 사용 에) HTML Form → ItemSaveForm → Controller → Item 생성 → Repository
📌 검증 객체 (Item)
package hello.itemservice.domain.item;
import lombok.Data;
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
📌 저장 검증 객체
package hello.itemservice.web.validation.form;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1_000_000)
private Integer price;
@NotNull
@Max(9999)
private Integer quantity;
}
📌 수정 검증 객체
package hello.itemservice.web.validation.form;
import lombok.Data;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1_000_000)
private Integer price;
// 수정에서는 수량은 자유롭게 변경 가능
private Integer quantity;
}
📌 저장 컨트롤러
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute("item") ItemSaveForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
// 특정 필드가 아닌 복합 룰 검증
if(form.getPrice()!=null && form.getQuantity()!=null) {
int resultPrice = form.getPrice() * form.getQuantity();
if(resultPrice<10_000) {
bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
}
}
// 검증 실패하면 다시 입력 폼으로
if(bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v4/addForm";
}
// 검증 성공 로직
// 폼 객체의 데이터를 기반으로 Item 객체를 생성한다. (변환 과정)
Item item = new Item(form.getItemName(),
form.getPrice(),
form.getQuantity());
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/validation/v4/items/{itemId}";
}
- Item 검증 객체 대신 ItemSaveForm을 전달 받는다.
- 그리고 @Validated 로 검증도 수행하고 검증 결과를 BindingResult로 받는다.
- @ModelAttribute("item") 에 item 이름을 넣어준 부분을 주의할 것.
- item을 적지 않으면 ItemSaveForm의 규칙에 의해 itemSaveForm이라는 이름으로 MVC Model에 담기게 된다. 이렇게 되면 뷰 템플릿에서 접근하는 th:object 이름도 함께 변경해야한다.
📌 수정 컨트롤러
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @Validated @ModelAttribute("item") ItemUpdateForm form, BindingResult bindingResult) {
// 특정 필드가 아닌 복합 룰 검증
if(form.getPrice()!=null && form.getQuantity()!=null) {
int resultPrice = form.getPrice() * form.getQuantity();
if(resultPrice<10_000) {
bindingResult.reject("totalPriceMin", new Object[]{10_000, resultPrice}, null);
}
}
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return "validation/v4/editForm";
}
// 폼 객체의 데이터를 기반으로 Item 객체를 생성한다. (변환 과정)
Item itemParam = new Item(form.getItemName(),
form.getPrice(),
form.getQuantity());
itemRepository.update(itemId, itemParam);
return "redirect:/validation/v4/items/{itemId}";
}
- Item 검증 객체 대신 ItemUpdateForm을 전달 받는다.
- 그리고 @Validated 로 검증도 수행하고 검증 결과를 BindingResult로 받는다.
- @ModelAttribute("item") 에 item 이름을 넣어준 부분을 주의할 것.
- item을 적지 않으면 ItemSaveForm의 규칙에 의해 itemSaveForm이라는 이름으로 MVC Model에 담기게 된다. 이렇게 되면 뷰 템플릿에서 접근하는 th:object 이름도 함께 변경해야한다.
Bean Validation - HTTP 메시지 컨버터
- @Valid / @Validated 애노테이션은 HttpMessageConverter( @RequestBody ) 에도 적용할 수 있다.
- 참고
- @ModelAttirbute 는 HTTP 요청 파라미터(URL 쿼리 스트링, POST Form)를 다룰 때 사용된다.
- @RequestBody 는 HTTP Body의 데이터를 객체로 변환할 때 사용한다. 주로 API JSON 요청을 다룰 때 사용한다.
📌 컨트롤러
package hello.itemservice.web.validation;
import hello.itemservice.web.validation.form.ItemSaveForm;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/validation/api/items")
public class ValidationItemApiController {
@PostMapping("/add")
public Object addItem(@RequestBody @Validated ItemSaveForm form, BindingResult bindingResult) {
log.info("API 컨트롤러 호출");
if (bindingResult.hasErrors()) {
log.info("검증 오류 발생 errors={}", bindingResult);
return bindingResult.getAllErrors();
}
log.info("성공 로직 실행");
return form;
}
}
테스트용 요청 정보 (POST MAN 프로그램으로 데이터 전송)
POST http://localhost:8080/validation/api/items/add
Context-Type : application/json
{"itemName":"hello", "price":1000, "quantity":10}
🔍 API 응답 3가지 경우
- 요청 성공
- JSON을 객체로 생성하는 것을 성공했고, 검증도 성공했다.
- 요청 실패
- JSON을 객체로 생성하는 것 자체를 실패했다
- 이 경우 HttpMessageConverter에서 요청 JSON을 객체로 생성하는 것 자체가 실패햇다.
- 지정한 객체(ex: Item)로 만들지 못했기 때문에 해당 요청 컨트롤러 호출도 되지 않고 Validator도 실행되지 않는다.
- 검증 오류 실패
- JSON을 객체로 생성하는 것은 성공했지만, 검증에서 실패했다.
- 이 경우 검증 실패 내역이 BindingResult 클래스에 들어있기 때문에 적절히 꺼내 담아 반환하면 된다.
💡 @ModelAttribute vs @RequestBody
- HTTP 요청 파라미터를 처리하는 @ModelAttribute 는 각각의 필드 단위로 세밀하게 적용된다. 그러므로 특정 필드가 타입이 불일치 하더라도 나머지 필드는 정상으로 처리할 수 있다. 그렇기 때문에 컨트롤러가 호출되고 나머지 필는 Validator가 실행 된다.
- HttpMessageConverter는 @ModelAttribute 와 다르게 필드 단위가 아닌 객체 전체 단위로 적용되기 때문에 메시지 컨버팅이 성공해서 객체가 만들어진 다음에 검증 애노테이션(ex : @Validated)이 적용된다. 그래서 타입 불일치일 경우 컨트롤러 호출 자체가 안되고 Validator가 실행되지 않는다.
- HttpMessageConverter 단계에서 실패하면 예외가 발생한다. 예외 발생시 원하는 모양으로 예외를 처리하는 방법은 나중에 예외 처리 방법에서 다룬다.
👀 참고자료
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-2/dashboard
728x90
'[ Spring ] > SpringMVC 2편' 카테고리의 다른 글
[Spring] 로그인 처리2 - 필터, 인터셉터 (0) | 2022.03.13 |
---|---|
[Spring] 로그인 처리1 - 쿠키, 세션 (0) | 2022.03.12 |
[Spring] 검증1 - Validation (0) | 2022.03.07 |
[Spring] 메시지, 국제화 (0) | 2022.03.06 |
[Spring] 타임리프 - 스프링 통합과 폼 (0) | 2022.03.05 |