Bean Validation
Spring에서는 클라이언트에서 받아온 정보를 Validation을 이용하여 검증을 하였는데 Spring Boot에서 제공해주는 Bean Validation이라는 기능을 이용하여 조금더 수월하게 검증을 이뤄보려고 합니다.
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-validation'
Item
@Data
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;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
Item이라는 객체를 담을 그릇을 만들었는데 각 필드(컬럼)들 위에 각각 다른 어노테이션이 붙어있습니다.
@NotBlank -> null, "", " "들을 허용하지 않는다.
@NotNull -> null을 허용하지 않는다.
@Range(min = 1000, max = 1000000) -> 들어올 값의 범위 지정
@Max(9999) -> 최대값 지정
각 필드에 필요한 검증들을 넣어주면 자동으로 검증을 하여 에러코드를 만들어줍니다.
이렇게 어노테이션으로 등록한 Validation을 사용하기 위해서는 해당 컨트롤러의 ModelAttribute앞에 @Validated어노테이션을 붙여줘야 합니다.
결과화면
위에 보이는 빨간색 글씨들이 BeanValidation에서 기본으로 들어가 있는 에러코드입니다.
검증 순서
BeanValidator는 바인딩에 실패한 필드(컬럼)에는 BeanValidator를 적용하지 않습니다.
즉, 가격 입력창에는 숫자만 입력이 되어야하는데 문자가 들어오게 된다면 BeanValidator가 실행되지 않습니다.
@ModelAttribute -> 각각의필드타입변환시도 변환에 성공한 필드만 BeanValidation 적용
가격 입력창에 "AA"를 입력하면 숫자타입으로 변환이 실패하게 되고 에러코드를 터미널에서 확인할 수 있습니다.
codes [typeMismatch.item.price,typeMismatch.price,typeMismatch.java.lang.Integer,
typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [item.price,price]; arguments []; default message [price]];
default message [Failed to convert property value of type 'java.lang.String'
to required type 'java.lang.Integer' for property 'price';
nested exception is java.lang.NumberFormatException: For input string: "AA"]
해당 오류는 "AA"라는 문자가 들어와서 바인딩에 실패했다는 오류인데 해당 오류의 codes부분에 값중 하나를 errors.properties에 에러코드메세지를 등록해 주면 됩니다.
errors.properties
typeMismatch.item.price=가격은 숫자를 입력해주세요.
이번에는 상품명 이름에서 에러를 발생시킨 뒤 에러코드를 확인해보겠습니다.
Field error in object 'item' on field 'itemName': rejected value [];
codes [NotBlank.item.itemName,NotBlank.itemName,NotBlank.java.lang.String,NotBlank];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.itemName,itemName];
arguments []; default message [itemName]]; default message [공백일 수 없습니다]
itemName이라는 필드에 NotBlack 어노테이션이 실행되어 에러코드를 생성하는데 자세히보면 codes 마지막에 어노테이션 자체를 code로 사용이 가능합니다. 역시 errors.properties에 등록이 가능한 이유입니다.
errors.properties
NotBlank={0} 공백X
Range={0}, {2} ~ {1}
허용 Max={0}, 최대 {1}
여기서 사용되는 매개변수는 {0}은 필드명, {1}, {2}는 각 어노테이션에 등록된 값입니다. 필드에서 에러가 발생하면 errors에서 key를 찾게 되는데 보다 자세한 key가 레벨이 높게 부여 되어 해당 에러를 찾게 됩니다. 그리고 어노테이션을 넣을때 직접 message를 등록하는 방법도 있습니다.
@NotBlank(message = "공백은 입력할 수 없습니다.")
private String itemName;
이렇게 각 필드에 발생하는 오류를 처리하는 방법을 소개드렸습니다. 그렇다면 필드가 없는 혹은 다른 객체에서 영향을 받는 ObjectError는 어떻게 처리할 수 있을지 소개하겠습니다.
그냥 이전의 컨트롤러에서 if문으로 조건을 처리하여 bindingResult.rect를 사용하시길 권장 드립니다...
이유는 필드처럼 객체파일에 등록이 가능하지만 그역시 복잡한 에러가 들어오게 되면 오히려 가독성이나 사용적인 측면에서 더 복잡한 코드가 발생될 수 있으므로 이전의 Validation방식을 사용하는 것을 추천 드립니다.
BeanValidation의 한계
BeanValidation은 객체파일을 만들때 각 필드 위에 각 어노테이션을 기입하여 검증을 거쳤습니다.
Item을 등록할 때, 수정할 때 모두 사용이 될 것 입니다. 만약 기획에 변경이 되어 등록할 때와 수정할 때 검증 조건을 다르게 해야한다면 해당 검증은 난감해질것 입니다.
해결방법
- BeanValidation의 groups기능 사용
- Item을 직접 사용하지 않고, ItemSaveForm과 ItemUpdateForm 같은 폼 전송을 위한 별도의 모델 객체를 만들어 사용
groups 적용
2개의 groups으로 사용할 SaveCheck인터페이스, UpdateCheck인터페이스 생성. 두 인터페이스를 생성햇다면 Item에 등록한 필드 위의 검증 어노테이션에 각 class를 등록해줍니다.
@Data
public class Item {
@NotNull(groups = UpdateCheck.class)
private Long id;
@NotBlank(groups = {UpdateCheck.class,SaveCheck.class})
private String itemName;
@NotNull(groups = {UpdateCheck.class,SaveCheck.class})
@Range(min = 1000, max = 1000000,groups = {UpdateCheck.class,SaveCheck.class})
private Integer price;
@NotNull(groups = {UpdateCheck.class,SaveCheck.class})
@Max(value = 9999,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;
}
}
각 필드에 원하는 검증 조건 어노테이션을 만들고 분리하여 group을 넣어준 모습입니다. 모두 완료했다면 각 매핑 컨트롤러에서 사용할 인터페이스를 @Validated에 넣어줍니다.
@Validated(UpdateCheck.class) @ModelAttribute Item item
@Validated(SaveCheck.class) @ModelAttribute Item item
이렇게 된다면 각 컨트롤러에 원하는 조건들만 검증하여 사용이 가능합니다.
하지만 해당 group의 방식은 모든 필드에 모든 조건마다 넣어주어야하며 복잡해지는 코드가 되기 쉽습니다.
저장, 수정 객체 분리
앞서 설명드린 group을 사용하게 되면 코드가 복잡해지기 때문에 실무에서는 검증 조건이 다를경우 객체를 분리하여 따로 검증 어노테이션을 추가합니다.
등록 객체
@Data
public class ItemSaveForm {
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
@NotNull
@Max(value = 9999)
private Integer quantity;
}
수정 객체
@Data
public class ItemUpdateForm {
@NotNull
private Long id;
@NotBlank
private String itemName;
@NotNull
@Range(min = 1000, max = 1000000)
private Integer price;
private Integer quantity;
}
이렇게 따로 분리하여 각 등록, 수정 컨트롤러에서 ModelAttribute타입을 맞추어 주시면 됩니다.