개발을 하다보면 데이터 검증을 해야하는 경우가 있다.
예컨데, 로그인 기능의 경우
Id와 Password가 입력되지 않은채로
서비스레이어에 들어가서 비지니스 로직을
수행하는경우 의도치 않은 에러가 발생한다.

이러한 에러는 충분히 서버단에서 충분히 검증을 수행한다면
사용자에게 더 친숙하게 좋은 표현으로 돌려줄 수 있을것 같다는 생각이 든다.
그전에, 왜 굳이 서버단에서 다시한번 검증을 해야 하는가에 대해서 알아보자
1. 보안과 안전성이 뛰어나다.
clientSide에서만 데이터 검증을 수행하는 경우, 악의적인 사용자가 값을 조작해서 서버로 전송할 수 있다.
2. 복잡한 비지니스 로직에 대한 검증이 가능하다.
단순히 빈 값의 검증이 아니라 과거 데이터를 조회해서 비교하고 그 값이 유효한지 등의 검사를 clientSide에서 수행하려면 화면에 불필요한 정보까지 해주어야 하는데, 별로 효율적이지 않다.
3. 보안성이 향상된다.
2번의 연장선으로, 중요한 데이터를 client에서만 검증하는것은 보안 취약점을 만들 수 있다. 정보를 client에게 보내지 않고 서버에서 검증을 통해 중요한 정보를 보호할 수 있다.
4. client-server간 통신 오버헤드 감소
client에서 검증을 하면 그에 대한 결과를 서버로 함께 보내게 되는데, 이로인해 네트워크 트레래픽이 늘어나고, 오버헤드가 발생할 수 있다.
5. 다중 플랫폼 및 클라이언트 호환성
다양한 플랫폼이나, 다양한 client에서 애플리케이션을 사영하는 경우, 서버에서 일괄적으로 데이터 검증이 가능하다.
수강신청 도메인으로 사이드프로젝트를 진행하면서 만든 코드를
같이 리팩토링하는 과정을 통해 정리를 해보려고 한다.
1. if문으로 값을 검증하는 방법
@PostMapping("/create")
public Map<String, String> createCourse(@RequestBody CourseCreate requestDto) {
if(!StringUtils.hasText(requestDto.code())) {
throw new RuntimeException("코드값을 입력해주세요!");
}
if(!StringUtils.hasText(requestDto.title())) {
throw new RuntimeException("강의명을 입력해주세요!");
}
if(!StringUtils.hasText(requestDto.courseType().getType())) {
throw new RuntimeException("전공/교양여부를 선택해주세요!");
}
Map<String, String> resultMap = new HashMap<>();
Long course = courseService.save(requestDto);
resultMap.put("courseId", String.valueOf(course));
return resultMap;
}
가장 직관적인 방법이다.
그러나
1. 검증해야할 데이터가 많아질 경우 if문이 너무나 길어지게 된다.
2. 검증해야할 데이터가 많아질 경우 누락이 발생할 가능성이 있다.
3. 검증해야할 게 많다.(단순히 빈값에 대한 비교만 하지는 않는다.)
2. string-boot-start-validation 의존성을 활용하기
이러한 경우를 대비해
스프링부트는 validation을 제공한다.
build.gradle에 sptirng-boot-starter-validation 의존성을 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
응답을 받는 dto에 다음과 같은 어노테이션을 붙여준다.
public record CourseCreate(
String code,
@NotBlank(message = "강의 제목을 입력해주세요.")
String title,
String description,
@Max(value = 100,message = "수강인원은 100명을 넘을 수 없습니다.")
Integer maxCount,
CourseType courseType,
@Min(value = 0, message = "학점은 0보다 아래일 수 없습니다.")
@Max(value = 3, message = "학점은 4보다 높을 수 없습니다.")
Integer score
) {
//생략
}
마지막으로 컨트롤러의 if문을 제거한다.
@PostMapping("/create")
public Map<String, String> createCourse(@RequestBody @Validated CourseCreate requestDto, BindingResult result) {
Map<String, String> resultMap = new HashMap<>();
if(result.hasErrors()) {
// 처음 발견된 에러에 대해서 에러메세지 출력
FieldError fieldError = result.getFieldErrors().get(0);
String errorField = fieldError.getField();
String errorMessage = fieldError.getDefaultMessage();
// {"title":"강의 제목을 입력해주세요."}
resultMap.put(errorField, errorMessage);
return resultMap;
}
Long course = courseService.save(requestDto);
resultMap.put("courseId", String.valueOf(course));
return resultMap;
}
이후 준비된 강의제목이 입력되지 않은 상황의 테스트 케이스를 실행한다.

하지만 아직 부족하다..
1 .현재는 한개의 url 예제로 진행하지만, 실무에서 검증해야 하는 요청은 엄청나게 많다.
그때마다 검증하는 코드를 작성하는것은 다소 비효율적이라고 느껴진다.
2 .예외를 반환하는 상황인데 HttpStauts가 200(isOk())로 떨어지는게 조금은 마음에 걸린다.
3 .데이터 검증과 이에따른 예외처리가 중요하다고 할지라도
이렇게 모든 메소드마다 예외처리를 하다보면 비지니스 로직에 집중하기 어려워진다.
3. @ControllerAdivce 어노테이션과 @ExceptionHandler 어노이션 이용
*현재 프로젝트에서는 json형태로 값을 주고받고 있어서 @ControllerAdvice 대신 @RestControllerAdvice를 사용했다.
@RestControllerAdvice
public class ExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> notValidExceptionHandler(MethodArgumentNotValidException e) {
//강의명을 입력하지 않은 tastCase error는
// MethodArgumentNotValidException이다.
//에러를 캐치해서 map 형태의 json데이터로 변환 - start
FieldError error = e.getFieldError();
String errorField = error.getField();
String errorMessage = error.getDefaultMessage();
Map<String, String> map = new HashMap<>();
map.put(errorField,errorMessage);
return map;
//에러를 캐치해서 map 형태의 json데이터로 변환 - end
}
}
새로만든 ExceptionController 이다.
MethodArgumentNotValidException 에러가 발생하면 해당 에러를 잡아서
Map<String, String> 형태의 json 데이터를 body에 담아준다.
이로인해 컨트롤러는 단순히 http요청을 전달하는 역할만을 수행 할 수 있게 되었다.
@PostMapping("/create")
public Map<String, String> createCourse(@RequestBody @Validated CourseCreate requestDto) {
Map<String, String> resultMap = new HashMap<>();
Long course = courseService.save(requestDto);
resultMap.put("courseId", String.valueOf(course));
return resultMap;
}

테스트케이스가 성공적으로 동작하는 모습.
http응답값도 400에러로 변경되었고,
body에 의도한 에러정보도 잘 확인된다.