빈 검증에 대하여

Bean Validation

검증 애노테이션과 여러 인터페이스의 모음이며 마치 JPA 가 표준 기술이고 그 구현체로 하이버네이트가 있는 것과 같다.

Bean Validation 을 사용하려면 다음과 같은 의존관계를 추가해야 한다.

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-validation'

 

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Item {
	private Long id;

	@NotBlank
	private String itemName;

	@NotNull
	@Range(min = 1000, max = 100000)
	private Integer price;

	@NotNull
	@Max(9999)
	private Integer quantity;
}


위에 코드가 기본적으로 빈 검증을 사용하는 코드이다.

참고

javax.validation.constraints.NotNull;
org.hibernate.validator.constraints.Range;

javax.validation 으로 시작하면 특정 구현에 관계없이 제공되는 표준 인터페이스이며,
org.hibernate.validator 로 시작하면 하이버네이트 validator 구현체를 사용할 때만 제공되는 검증기능이다.
실무에서는 대부분 하이버네이트 validator 를 사용하므로 자유롭게 사용해도 된다.


Bean Validation 적용

@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult, RedirectAttribute redirectAttribute) {
	
    if(BindingResult.hasErrors()) {
		return "validation/v3/addForm";
	}

	Item savedItem = itemRepository.save(item);
	redirecAttributes.addAttribute("itemId", savedItem.getId());
	redirecAttributes.addAttribute("status", true);

	return "redirect:/validation/v3/items/{itemId}";
}



스프링 MVC 는 Bean Validator 를 자동으로 등록해 주는데 스프링 부트가 spring-starter-validation 라이브러리를 넣으면 자동으로 Bean Validator 를 인지하고 스프링에 통합하게 된다.

 

바인딩에 성공한 필드만 Bean Validation 에 적용


쉽게 이야기 하자면 아래와 같다.
itemName 에 문자 “A" 입력 -> 타입변환성공 -> itemName 필드에 BeanVaildation 적용
price 에 문자 ”B" 입력 -> “B" 를 숫자 타입 변환 시도 실패 -> typeMismatch FieldError 발생 -> price 필드는 BeanValidation 적용 X

따라서 만약 price 에 @NotNull, @Min(1000) 을 등록을 해놔도 해당 Bean Validation 은 아예 실행조차 되지 않는다.
따라서 타입 변환 에러가 출력되어 스프링에서 기본 제공해주는 메시지만 출력이 된다.

숫자를 입력해 주세요.

기본적인 Bean Validation 에러 코드

Bean Validation 에서 제공하는 에러 코드는 애노테이션 이름으로 등록이 된다.
아래 코드를 보면 이해가 쉽다.

 

public class Item {
    private Long id;

    @NotBlank
    private String itemName;

    @NotNull
    @Range(min = 1000, max = 10000)
    private Integer price;
}


위와 같이 등록을 하게 되면 스프링은 자등으로 아래아 같이 오류 코드를 만들어 준다.

NotBlank.item.itemName;
NotBlank.itemName;
NotBlank.java.lang.String
NotBlank

Range.item.price
Range.price
Range.java.lang.Integer
Range


이렇게 등록된 오류코드는 메시지에서 활용할 수가 있다.

NotBlank.item.itemName=특정상품
NotBlank.itemName=바나나
NotBlank=값이없습니다.

Range.item.price={0} 가격이 {1} ~ {2} 사이입니다.

price=상품


여기서 {0} 는 필드명이며 {1}, {2} 는 애노테이션 속성 값이다.
위에 코드를 보면 필드명이 price 로 되어 있고, Rage 의 속성 값으로 1000, 10000 이 있다.
따라서 price 가격이 1000 ~ 10000 사이 입니다. 라고 나오게 된다.
추가적으로 위에 코드를 보면 price 를 상품 이라고 지정해 주었는데 이렇게 설정하면 {0} 에 상품 이라고 등록이

되므로 상품 가격이 1000 ~ 10000 사이 입니다. 라고 출력이 된다.

전체흐름
@NotBlank 실패 -> MessageCodesResolver 가 코드를 여러 개 생성 -> messages 또는 errors.properties 에서 순서대로 탐색 -> NotBlank 발견 -> “값이 없습니다.” 를 bindingResult 에 적재 후 실행

BeanValidation 이 메시지를 찾는 순서

1. 생성된 메시지 코드 순서대로 messageSource(messages.properties) 에서 메시지를 찾기
2. 애노테이션의 message 속성을 사용 ( @NotBlank(message = "공백“) )
3. 라이브러리가 제공하는 기본 값 사용(공백일 수가 없습니다.)

GlobalError 추가


@Validated 를 사용하지 못하는 2개 이상의 오류 사항들은 아래와 같이 사용하면 된다.
즉, 회원가입 할 때 이름이랑 비밀번호가 값이 들어오지 않았을 경우에 사용 할 수가 있다.

bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);

// 위와 같이 설정하면 messages.properties 에서 아래 정보를 찾게 된다.
totalPriceMin=총 금액은 {0}원 이상이어야 합니다. (현재: {1})
<p th:each="err : ${#fields.globalErrors()}" th:text="${err}"></p>
// 총 금액은 10000원 이상이어야 합니다. (현재: 8000)


에러 코드는 totalPriceMin 이며 메시지에 사용할 파라미터는 100000, resultPrice 이고 기본 메시지는 없음(null) 로 설정한 부분입니다.

즉, 아래와 같습니다.

void reject(String errorCode, Object[] errorArgs, String defaultMessage)


errorCode : 메시지 코드
errorArgs : 메시지에서 사용할 치환 값
defaultMessage : 메시지를 못 찾았을 때 쓸 기본 메시지

수정시 요구사항이 다를 경우

만약 등록, 수정을 할 때 동일한 UserDTO 를 사용하는 경우
데이터를 등록할 때와 수정할 때는 요구사항이 다를 수 있다.

등록할 때는 UserDTO 의 ID 값이 필요하지만 수정할 때는 ID 값이 필요가 없을 수도 있다.
하지만 @NotNull 로 ID 를 등록해 놓으면 수정할 때 무조건 값을 넣어야 하는 불편함이 생길 수 있다.

동일한 모델 객체를 등록할 때와 수정할 때 각각 다르게 검증하는 방법은 2가지가 있다.
1. BeanValidation 의 groups 기능을 사용
2. 동일한 모델을 사용하지 않고 파생된 2가지의 모델로 쪼개어 객체를 만들어 사용

실무에서는 2번째 방법을 가장 많이 사용하게 된다.


첫번째 groups 기능을 사용하는 경우는 아래와 같다.

public interface SaveCheck {}

public interface UpdateCheck {}


위와 같이 두가지 인터페이스를 만든 뒤 각각의 애노테이션에 적용해주면 된다.

@Data
public class Item {
    @NotNull(groups = UpdateCheck.class) // 수정시에만 적용
    private Long id;

    @NotBlank(groups = {SaveCheck.class, UpdateCheck.class}) // 수정, 등록에 모두 적용
}
@PostMapping("/add")
public String addItemV2(@Validated(SaveCheck.class) @ModelAttribute ...) {...}


이렇게 적용하고 싶은 부분에 대하여 설정만 해주면 된다.

하지만 코드를 짜면 알겠지만 코드가 복잡해지고 유지보수 하기가 힘들어진다.
따라서 2번째 방법처럼 각각의 전용 모델을 만들어 구분해 주면 더 쉽게 검증을 적용하고 안전성을 높일 수 있다.


'Java' 카테고리의 다른 글

좌충우돌 원격 서버 배포 및 Jenkins CI/CD 구축기  (0) 2026.04.28
필터와 인터셉터  (0) 2026.03.16
마이바티스  (0) 2026.03.05
ConcurrentHashMap 에 대하여  (3) 2025.06.06
유용한 람다식 함수들  (2) 2025.02.04