
안녕하세요, 오늘은 Validation 에 대해 정리해보았습니다.
💡 Controller의 주요한 역할 중 하나는 Validation 이다. 즉, HTTP 요청이 정상인지 검증하는것이다.Validation

프론트 + 서버 + DB 모두 검증을 꼼꼼하게 하는것이 바람직하다.
Validation으로 많은 Error와 문제들을 방지할 수 있다.
@ModelAttribute 파라미터 뒤에 와야한다.@Data
public class CreateRequestDto {
private Long field1;
private String field2;
private Integer field3;
}
@Controller
public class BingdingResultController {
@PostMapping("/create")
public String create(
@ModelAttribute CreateRequestDtp request, // 1. @ModelAttribute 뒤에
BindingResult bindingResult // 2. BindingResult
) {
if(!StringUtils.hasText(item.getField2())) { // 3. Validation
bindingResult.addError( // 4. Error 추가
new FieldError("request", "field2", "field2는 필수 입니다.")
)
}
return "viewName";
}
}
@Data
public class CreateRequestDto {
private Long field1;
private String field2;
private Integer field3;
}
@Controller
public class BingdingResultController {
@PostMapping("/create")
public String create(@ModelAttribute CreateRequestDto request) {
// logic
return "viewName";
}
}

- 정상 적인 응답이 온다.
- viewName 페이지로 이동한다.
- 잘못된 요청

- 바인딩 오류가 발생한다.
- 검증 오류(400 Bad Request)가 발생하고 Controller가 호출되지 않는다.
- Error Page로 이동된다.
@Data
public class CreateRequestDto {
private Long field1;
private String field2;
private Integer field3;
}
@Controller
public class BingdingResultController {
@PostMapping("/create")
public String create(
@ModelAttribute CreateRequestDtp request,
BindingResult bindingResult
) {
// logic
return "viewName";
}
}
Postman
정상 요청

정상 적인 응답이 온다.
viewName 페이지로 이동한다.
잘못된 요청

@ModelAttribute 필드 or 객체에 파라미터 바인딩 오류가 발생
파라미터에 @ModelAttribute와 Binding Result가 함께 있는 경우
@ModelAttribute는 파라미터를 필드 하나하나에 바인딩한다.
만약 그중 하나의 필드에 오류가 발생하면 해당 필드를 제외하고
나머지 필드들만 바인딩 된 후 Controller가 호출된다.
현재 우리는 대부분 RestAPI를 만들고있고 Bean Validation을 사용하기 때문에
위의 방법(@ModelAttribute, BindingResult)은 깊게 다루지않고 넘어가겠습니다.
시간은 많이 없으나 알려드려야할것이 너무 많아요!!(쿠키, 세션, 필터, 인터셉터, JPA, Docker 등등..)
하지만, BindingResult는 Spring의 꽤 중요한 주요 기능이니 한번쯤 공부해주셔야 합니다.
@Getter
public class SignUpRequestDto {
@NotBlank
private String name;
@NotNull
@Range(min = 1, max = 120)
private Integer age;
}
Gradle
💡 Gradle은 JVM 기반의 오픈 소스 빌드 자동화 도구이다.(빌드 관리 도구) 프로젝트의 빌드, 테스트, 배포를 관리하는 데 사용된다. Groovy나 Kotlin DSL(Domain Specific Language)을 사용하여 **빌드 스크립트를 작성하며, 프로젝트의 의존성 관리, 태스크 정의, 플러그인 사용 등**을 통합적으로 제공한다. - 간단히 정리하자면 소프트웨어 개발 프로세스를 자동화하고 효율적으로 관리할 수 있게 해주는 도구build.gradle


SpringBoot, Java의 Version이 선언되어 있다.
라이브러리의 Version은 따로 명시해주지 않았다.
build 실행시

의존성 추가(build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 뒤에 version이 따로 적혀있지 않은 이유 → Spring Boot 기능
파일경로 jakarta.validation-api-${version}.jar
jakarta.validation

constraints 경로에서 다양한 어노테이션들을 확인할 수 있다.
org.hibernate.validator:hibernate-validator:${version}
→ 위 validation들이 동작하게 만들어주는 Validator

jakarta.validation-api : Interfaceorg.hibernate.validator:hibernate-validator : 구현체
@Range 어노테이션은 Hibernate Validator 에서만 동작하는것
@NotBlank, @NotNull 은 validation 표준 인터페이스
참고
@Data
public class TestDto {
@NotBlank // 테스트 하고싶은 어노테이션으로 변경
private String stringField;
@NotNull
@Range(min = 1, max = 9999)
private Integer integerField;
}
public class BeanValidationTest {
@Test
void beanValidation() {
// Spring과 통합하면 아래 두줄의 코드는 사용하지 않는다.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
TestDto dto = new TestDto();
// Test 하고싶은 상황을 만들어서 검증해보자
dto.setStringField(" ");
dto.setIntegerField(1);
Set<ConstraintViolation<TestDto>> violations = validator.validate(dto);
for(ConstraintViolation<TestDto> violation : violations) {
// 1. 아래의 결과에 Message가 있으면 Validation에 걸린것.
// Default Message가 있기 때문에 출력됨
// Message를 수정하고싶다면 어노테이션 속성값(message="입력")으로 설정할 수 있다.
// 2. 결과가 비어있으면 Validation에 걸리지 않은것.
System.out.println("violation = " + violation.getMessage());
}
}
}
위 코드로 하나하나 검증해볼 수 있다!
참고 :
org.hibernate.validator로 시작하면 하이버네이트를 사용할 때만 제공되는 검증 기능
즉, 다른 구현체로 validator를 교체하였을 경우 동작하지 않는다.
하지만 대부분org.hibernate.validator를 대부분 사용한다.
LocalValidatorFactoryBean 을 Global Validator로 등록한다.

LocalValidatorFactoryBean 을 등록하지 않는다.@Valid, @Validated 만 적용하면 된다.@Valid 는 JAVA 표준이고 @Validated 는 Spring 에서 제공하는 어노테이션이다.@Validated 를 통해 Group Validation 혹은 Controller 이외 계층에서 Validation이 가능하다.@Valid 는 MethodArgumentNotValidException 예외를 발생시킨다.@Validated 는 ConstraintViolationException 예외를 발생시킨다.BindingResult에 담아준다.@ModelAttribute 각각의 필드 타입에 맞추어 바인딩(변환) 시도
typeMismatch FieldError 발생Validator 적용
@ModelAttribute → 각 필드 바인딩 → 성공한 필드만 Bean Validation 적용typeMismatch FieldError 추가Bean Validation을 적용하고 bindingResult에 등록된 검증 오류를 확인해보면
오류가 어노테이션 이름으로 등록되어 있다.
Spring 에서는 오류 메시지 코드관리를 위해 MessageCodesResolver 인터페이스의
구현체인 DefaultMessageCodesResolver를 기본으로 사용한다.
예시코드
@Data
public class TestDto {
@NotBlank
private String field1;
@NotNull
private Integer field2;
}
@Slf4j
@Controller
public class BeanValidationController {
@PostMapping("/error-message")
public void test(
@Validated @ModelAttribute TestDto dto,
BindingResult bindingResult
) {
if(bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
}
}
}
@NotBlank , @NotNull 사용Postman Request

x-www-form-urlencoded : 작성 폼 제출출력결과

Field error in object 'testDto' on field 'field1': rejected value [null]; codes [NotBlank.testDto.field1,NotBlank.field1,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.field1,field1]; arguments []; default message [field1]]; default message [공백일 수 없습니다]
NotBlank 오류 코드를 기반으로 MessageCodesResolver 를 통해 메세지 코드 순서대로 생성
NotBlank.Object.fieldName
NotBlank.fieldName
NotBlank.(java.lang.String) 필드 타입
NotBlank
생성된 메시지 코드 순서대로 messageSource 에서 메시지 찾기
Message 기능을 학습해보자! (따로 해주세요 쉬워요!)애노테이션의 message 속성 사용
@Data
public class TestDto {
@NotBlank(message = "안돼 돌아가")
private String field1;
@NotNull
private Integer field2;
}
라이브러리가 제공하는 Default Message 사용 “공백일 수 없습니다”.
개발자들의 귀차니즘은 위대하다.

@Data
@ScriptAssert
public class TestDto {
@NotNull
@Range(min = 10, max = 9999)
private Integer field1;
@NotNull
@Max(999)
private Integer field2;
@NotBlank
private String field3;
}
실무에서는 @ScriptAssert 제약사항 때문에 사용하지 않는다.(실제 사용성은 떨어진다)
Object Error의 경우 Java 코드로 직접 Validation 한다.
@ScriptAssert 사용 X
- 요구사항 : field1 * field2 ≥ 10000
@Slf4j
@Controller
public class BeanValidationController {
@PostMapping("/error-message")
public void test(
@Validated @ModelAttribute TestDto dto,
BindingResult bindingResult
) {
// 실패로직
if(dto.getField1() != null && dto.getField2() != null) {
int result = dto.getField1() * dto.getField2();
if(result > 10000) {
bindingResult.reject("totalMin", new Object[]{10000, result}, null);
}
if(bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
}
}
// 성공로직 ...
}
}
정리하자면 Object Error는 로직으로 구현하자!
수정 API를 만들 때 id값을 항상 Controller에서 받도록 구성한다.
하지만 HTTP 요청은 사용자가 임의로 변경하여 요청할 수 있음으로
항상 서버에서 최종적으로 추가 검증을 진행 해야한다.
ex) 게시글 수정시 요청자 본인이 쓴 글인지 확인한다
저장시 Test 객체
@Data
public class Test {
// 자동생성
private Long id;
@NotNull
@Range(min = 10, max = 9999)
private Integer field1;
@NotNull
@Max(999)
private Integer field2;
@NotBlank
private String field3;
// 생성자
}
@Slf4j
@Controller
@RequiredArgsConstructor
public class BeanValidationController {
private final TestRepository testRepository;
@PostMapping("/save")
public void test(
@Validated @ModelAttribute Test test
) {
// Validation 성공시 repository 저장로직 호출
testRepository.save(test);
}
}
예시
등록시에는 field2가 최대 999이지만 수정시에는 제한이 없다면?
등록시에는 Repository에 저장될 Entity의 Id값이 없어도 되지만 수정시에는 id 값이 필수
수정시 Test 객체
@Data
public class Test {
@NotNull // id값 필수 요구사항 반영
private Long id;
@NotNull
@Range(min = 10, max = 9999)
private Integer field1;
@NotNull
// @Max(999) 무제한 요구사항 반영
private Integer field2;
@NotBlank
private String field3;
}
@Slf4j
@Controller
@RequiredArgsConstructor
public class BeanValidationController {
private final TestRepository testRepository;
@PutMapping("/{id}/edit")
public void test(
@PathVariable Long id,
@Validated @ModelAttribute Test test
) {
Test test = testRepository.findById(id);
test.setField1(test.getField1());
test.setField2(test.getField2());
test.setField3(test.getField3());
// repository 수정로직 호출 service.update()
testRepository.update(id, test);
}
}
@NotNull이 id값에 추가되고 @Max 가 사라져서 저장시 정상적으로 동작하지 않는다.
객체 id값에 @NotNull 이 추가되었기 떄문에 저장시 Validation Error 발생
field2 값에 @Max 가 사라져서 저장시에도 제한없이 저장됨
등록과 수정시 요구사항에 충돌이 발생한다.
해결방법 1 : 저장할 Object를 직접 사용하지 않고 SaveDto, UpdateDto 따로 사용한다.
여러분들이 자연스럽게 사용하고 있는 방법.(따라서 설명은 생략)
바인딩 받는 Object(RequestDto)를 저장할 Object(Entity)와 별도로 사용한다.
해결방법 2 : BeanValidation의 groups 기능을 사용한다.
제공되는 기능
// 저장용 group
public interface SaveCheck {
}
// 수정용 group
public interface UpdateCheck {
}
// Test 객체
@Data
public class Test {
@NotNull(groups = UpdateCheck.class) // 수정시에만 반영
private Long id;
@NotNull(groups = {SaveCheck.class, UpdateCheck.class}) // 둘다 적용
@Range(min = 10, max = 9999, groups = {SaveCheck.class, UpdateCheck.class})
private Integer field1;
@NotNull
@Max(999, groups = SaveCheck.class) // 저장시에만 반영
private Integer field2;
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class}) // 둘다 적용
private String field3;
}
@Slf4j
@Controller
@RequiredArgsConstructor
public class BeanValidationController {
private final TestRepository testRepository;
@PostMapping("/save")
public void test(
@Validated(SaveCheck.class) @ModelAttribute Test test // 저장 속성값 설정
) {
// Validation 성공시 repository 저장로직 호출
testRepository.save(test);
}
@PutMapping("/{id}/edit")
public void test(
@PathVariable Long id,
@Validated(UpdateCheck.class) @ModelAttribute Test test // 수정 속성값 설정
) {
Test test = testRepository.findById(id);
test.setField1(test.getField1());
test.setField2(test.getField2());
test.setField3(test.getField3());
// repository 수정로직 호출 service.update()
testRepository.update(id, test);
}
}
@Validated

@Valid 에는 속성값이 존재하지 않는다, groups 기능 지원하지 않는다.

groups 속성을 사용하면 등록과 수정시 각각 다르게 Validation이 적용된다.
하지만 가독성이 떨어지고 코드 복잡도가 올라간다.
실무에서는 등록시 작성할 폼(화면)과 수정시 작성할 폼 객체(화면) 자체를 분리해서 사용하기 때문에
여러분이 사용하는 현재의 방법(DTO 분리 방법) 그대로 진행하면 된다.
네이밍은 일관성있게 작성해주세요.(UpdateRequestDto, SaveRequestDto)
그래도 우리가 왜 DTO 객체를 분리하여 사용하는지와 기능의 존재 여부는 알고있어야 합니다 여러분.
ex) 등록시에는 약관동의 등 도메인 관련 데이터 이외에 추가로 받는 데이터도 있다.
도메인 저장에 필요한 추가 데이터를 DB나 다른곳에서 조회하여 가져올 수 있다. 직접 해보면 압니다.
오늘의 코드카타
class Solution {
public int solution(String myString, String pat) {
int answer = 0;
for (int i = myString.length() - 1; i >= 0; i--) {
String subStr = myString.substring(0, i + 1);
if (subStr.endsWith(pat)) {
answer++;
}
}
return answer;
}
}
public class Main {
public static void main(String[] args) {
Solution solution = new Solution();
System.out.println(solution.solution("banana", "ana"));
System.out.println(solution.solution("aaaa", "aa"));
}
}
문자열이 몇 번 등장하는지 세기