[데브코스] Spring Boot REST API 실습 (23강) – REST API에서 @Valid는 쓰는데 BindingResult는 왜 거의 안 쓸까?

zuno·2025년 12월 25일

이번 강의에서는
REST API 환경에서 @Valid와 BindingResult를 어떻게 바라봐야 하는지에 대해 다뤘다.

솔직히 말하면
강의 내용을 처음 들었을 때는 나도 꽤 헷갈렸다.

“프론트에서 이미 다 검증하는데
굳이 백엔드에서 또 검증을 해야 하나?”

이 글에서는
왜 @Valid는 쓰지만 BindingResult는 거의 쓰지 않는지,
그리고 JSON 요청이 record DTO로 바인딩되는 정확한 흐름까지
초보자 관점에서 정리해본다.


1️⃣ REST API에서 BindingResult를 거의 쓰지 않는 이유

Spring MVC(타임리프, JSP) 기반에서는 이런 코드가 흔하다.

@PostMapping("/write")
public String write(
    @Valid PostForm form,
    BindingResult bindingResult
) {
    if (bindingResult.hasErrors()) {
        return "writeForm";
    }
    ...
}

👉 뷰(View)가 존재하는 서버 사이드 렌더링 구조에서는
에러가 나도 다시 화면으로 돌아가야 하기 때문에
BindingResult가 필수에 가깝다.


❌ 하지만 REST API에서는 상황이 다르다

REST API의 특징은 이렇다.

  • 서버는 화면을 그리지 않는다
  • JSON으로만 응답한다
  • 실패하면 명확한 에러 응답을 내려주면 끝이다

그래서 REST API에서 굳이:

  • 에러를 담아두고
  • 로직을 계속 진행하고
  • 분기 처리하는

👉 BindingResult 패턴이 잘 맞지 않는다.


2️⃣ @Valid는 쓰는데, 왜 BindingResult는 안 쓰나?

강의에서 나온 핵심 문장은 이거였다.

REST API에서는
@Valid에서 실패하면 그냥 “터지게 둔다”

즉,

@PostMapping
@Transactional
public RsData<PostDto> write(
    @Valid @RequestBody PostWriteForm form
) {
    ...
}

여기서 유효성 검증이 실패하면?

  • MethodArgumentNotValidException 발생
  • 컨트롤러 메서드 진입 ❌
  • 글로벌 예외 처리기로 바로 이동

👉 의도된 실패 흐름이다.


왜 이렇게 하는가?

1️⃣ 프론트엔드에서 이미 검증을 한다

  • 길이 체크
  • 필수 입력
  • 형식 검증

정상적인 사용자라면
서버로 잘못된 데이터가 올 일이 거의 없다.

2️⃣ 서버 검증은 “보안용 안전장치”다

  • Postman
  • curl
  • 해킹 시도
  • 직접 API 호출

👉 프론트를 우회하는 요청을 막기 위한 최후의 방어선

3️⃣ 실패 자체가 “정상 흐름”이다

REST API에서 유효성 실패는
복구해서 이어갈 일이 아니라, 바로 종료할 사건이다.

그래서 BindingResult를 쓰지 않는다.


3️⃣ BindingResult를 쓰면 뭐가 달라지나?

BindingResult를 쓰면:

  • 검증 실패해도
  • 메서드 안으로 진입하고
  • 개발자가 직접 처리해야 한다
if (bindingResult.hasErrors()) {
    ...
}

👉 REST API에서는
이 패턴 자체가 오히려 설계를 복잡하게 만든다.


4️⃣ 그래도 @Valid는 왜 꼭 써야 할까?

“프론트에서 다 검증하는데 왜 @Valid를 쓰지?”

👉 이 질문에 대한 답은 명확하다.

❗ 프론트 검증은 ‘신뢰 대상’이 아니다

  • 브라우저 개발자 도구
  • Postman
  • 스크립트
  • 자동화 공격

👉 서버는 절대 클라이언트를 신뢰하면 안 된다.

그래서:

  • 프론트 검증 → UX 개선용
  • 백엔드 @Valid → 보안 + 데이터 무결성

이렇게 역할이 나뉜다.


5️⃣ JSON 요청과 record DTO는 어떻게 연결될까?

클라이언트에서 이런 JSON을 보낸다고 가정하자.

{
  "title": "제목",
  "content": "내용"
}

그리고 서버에서는 이렇게 받는다.

record PostWriteForm(
    @NotBlank
    @Size(min = 2, max = 100)
    String title,

    @NotBlank
    @Size(min = 2, max = 5000)
    String content
) {}

❓ 이게 왜 받아지는 걸까?

그 이유는 Jackson(ObjectMapper) 때문이다.

Jackson은:

  • JSON의 key 이름
  • record 생성자의 파라미터 이름

을 기준으로 객체를 만든다.

즉,

"title"   → String title
"content" → String content

👉 이름이 정확히 같아야 매핑된다.


6️⃣ 이름이 다르면 어떻게 될까?

만약 JSON이 이렇게 온다면?

{
  "subject": "제목",
  "body": "내용"
}

하지만 record는:

record PostWriteForm(String title, String content) {}

👉 결과는?

  • title = null
  • content = null
  • @NotBlank 위반
  • 즉시 예외 발생

👉 컨트롤러 진입 자체가 실패한다.

이게 바로
“프론트와 백엔드가 데이터 계약을 맞춰야 하는 이유”다.


7️⃣ 그래서 강의에서 강조한 결론

  • REST API에서는 @Valid는 필수
  • BindingResult는 거의 사용하지 않는다
  • 검증 실패는 “처리 대상”이 아니라 “즉시 종료 대상”
  • 프론트 검증은 UX용, 서버 검증은 보안용
  • JSON key와 DTO(record) 필드명은 반드시 일치해야 한다

✍️ 개인 정리 한 줄

REST API에서의 유효성 검증은
“사용자를 도와주는 기능”이 아니라
“서버를 지키는 안전장치”다.

0개의 댓글