Devin.KR

입력 검증

75분 안팎

학습 목표

DTO 검증과 업무 검증의 위치를 구분합니다.

개념

검증이 필요한 시점을 찾습니다

저장 후 제목이 너무 길다는 것을 알아내면 클라이언트는 실패를 받았는데 DB에는 잘린 값이나 일부 변경이 남을 수 있습니다. 입력 검증은 변경보다 앞에서 끝나야 합니다. 이번 계약은 제목을 필수로 받고 비공백·UTF-16 길이 200 이하·탭과 줄바꿈 없음으로 제한합니다. 이미 저장 가능한 문자열인지와 현재 업무 상태에서 변경 가능한지는 다른 판단입니다. 앞의 것은 요청 DTO에서 표현하고 뒤의 것은 서비스가 저장된 상태를 보고 판단합니다.

제목 계약을 정확히 적습니다

공백만 있는 제목은 거부하지만 정상 제목의 앞뒤 공백은 자동 제거하지 않습니다. 서버가 입력을 바꿀 때는 그 정책을 명세에 적어야 합니다. 이 실습은 같은 제목 두 권도 허용하며 title에 UNIQUE를 추가하지 않습니다. 200이라는 수치는 사람이 보는 글자 수가 아니라 Java String.length가 세는 UTF-16 코드 단위입니다. 일부 이모지는 두 단위를 쓰므로 이 한계를 문서에 남깁니다. 테스트의 한글 가 반복은 길이 경계를 명확하게 재현하려는 입력입니다.

DTO 애노테이션을 조합합니다

CreateBook 레코드의 title에 jakarta.validation.constraints.NotBlank·Size(max=200)·Pattern을 붙입니다. NotBlank는 null과 공백 제목을 막고 Size는 길이 상한을 표현합니다. Size만으로 null을 거부한다고 생각하지 않습니다. Pattern은 탭·CR·LF를 허용하지 않는 패턴으로 기존 파일 API의 제목 계약을 유지합니다. Spring Boot 3에서는 jakarta 패키지를 사용합니다. javax.validation을 섞어 컴파일이 되지 않으면 import와 의존성 버전을 함께 확인합니다.

검증 호출을 연결합니다

애노테이션이 있어도 HTTP 인자에서 검증을 요청하지 않으면 기대한 시점에 실행되지 않을 수 있습니다. BookController의 POST와 PATCH 요청 인자에 @Valid를 붙이고 starter-validation 의존성을 둡니다. JSON을 CreateBook으로 읽은 뒤 Bean Validation이 검사하며 위반하면 MethodArgumentNotValidException으로 처리됩니다. 서비스를 직접 호출하는 이전 테스트와 CLI 경로를 위해 서비스에도 제목 불변식 검사를 남깁니다. 웹 경계 검증과 핵심 규칙 보호의 목적이 다르기 때문입니다.

파싱 실패는 DTO 검증보다 먼저입니다

열린 중괄호 하나만 보낸 요청은 DTO를 만들 수 없습니다. 따라서 NotBlank 위반이 아니라 HttpMessageNotReadableException 경로입니다. 잘못된 숫자 경로도 제목 검증과 무관한 타입 변환 오류입니다. 이 모듈은 이 경로들을 400 INVALID_INPUT으로 통일합니다. 문법 오류를 잡으려고 서비스 안에서 JSON 문자열을 다시 읽지 않습니다. 요청 역직렬화는 웹 계층에서 한 번 수행하고 서비스에는 검증 가능한 객체를 전달합니다.

업무 상태 검증을 분리합니다

대여 이력이 있는 도서를 삭제할 수 있는지는 title에 붙인 애노테이션이 판단할 수 없습니다. 요청 모양이 정상이어도 DB의 참조 관계가 삭제를 거부할 수 있습니다. 이것은 현재 상태와의 충돌이므로 409로 구별합니다. 없는 도서 수정은 정상 제목이라도 404입니다. 반대로 잘못된 제목의 요청은 자원을 찾기 전에 웹 검증에서 거부될 수 있습니다. 여러 오류가 함께 있을 때 어느 검사가 먼저인지도 테스트와 명세가 같은 정책을 사용해야 합니다.

DB 제약은 마지막 경계를 지킵니다

API만 유효성을 검사하면 SQL 도구나 다른 프로그램이 빈 제목을 쓸 수 있습니다. 앞 모듈 스키마의 NOT NULL·VARCHAR(200)·CHECK를 유지합니다. 다만 TRIM 기반 CHECK와 Java isBlank의 공백 범위가 완전히 같다고 가정하지 않습니다. 탭과 줄바꿈 금지는 API 규칙이며 모든 직접 SQL 입력을 동일하게 차단하는 스키마는 아닙니다. 각 경계가 보장하는 범위를 구체적인 입력으로 확인하며 제약 오류 메시지를 그대로 사용자에게 반환하지 않습니다.

거부 결과보다 부수 효과를 확인합니다

400만 확인하면 저장 후 예외를 던지는 잘못된 구현도 놓칠 수 있습니다. invalidInputsLeaveDatabaseUnchanged는 각 요청 상태를 검사한 뒤 SELECT COUNT(*) FROM books가 0인지 확인합니다. PATCH 검사는 기존 제목을 다시 읽어 보존됐는지 확인합니다. 테스트 안에서 임시 파일 DB를 쓰므로 이전 실행의 행 수에 기대지 않습니다. 입력을 바꾸며 실행할 때 정상 요청으로 만든 행을 실패 결과와 섞지 않고 시나리오마다 기준 상태를 준비합니다.

경계값으로 검증 누락을 찾습니다

빈 문자열, 일반 공백, 탭, 줄바꿈 포함 제목, null, 제목 필드 누락, 길이 201을 모두 거부합니다. 길이 200 정상 제목은 저장하고 앞뒤 공백도 보존해야 합니다. 201만 실패시키면 상한을 199로 잘못 구현해도 알아채지 못하므로 바로 아래 성공 경계를 같이 검사합니다. 따옴표는 금지 문자가 아니어서 JDBC 레슨의 SQL 모양 제목도 통과해야 합니다. 입력 안전과 도메인 계약을 혼동하지 않습니다.

실패 보고서에서 다음 행동을 정합니다

expected 400 but was 201이면 웹 검증 호출과 서비스 조건을 확인합니다. expected 201 but was 400이면 정상 경계와 패턴을 확인합니다. 테스트 Errors가 있으면 검증 판정 이전에 환경이나 코드 예외가 생긴 것이므로 Failures와 따로 읽습니다. starter에서는 Size와 서비스 길이 검사 일부가 빠져 경계 테스트가 실패합니다. DTO와 서비스 양쪽을 수정한 뒤 테스트 전체를 실행해 기존 공백·탭 거부와 새 길이 제한이 함께 유지되는지 확인합니다.

사용한 API의 범위는 Spring 6.0 요청 검증 공식 설명에서 확인합니다. 실습 결과는 제공된 버전과 임시 DB에서 직접 검사한 범위입니다.

따라하기

DTO와 서비스의 상한을 연결합니다

실습 starter의 해당 TODO를 아래 코드와 주변 문맥을 보고 완성합니다. solution과 테스트는 먼저 접어 두고 각 인자의 의미를 설명합니다.

@NotBlank @Size(max=200)
@Pattern(regexp="[^\\t\\n\\r]*") String title

누적 테스트를 실행합니다

제목 DTO와 서비스 검사를 수정한 뒤 null·공백·200·201 경계를 실행합니다. invalidInputsLeaveDatabaseUnchanged에서 상태만 아니라 DB 행 수까지 확인하는 부분을 읽습니다.

./mvnw test > test.log 2>&1

실행 보고서를 집계합니다

아래 집계는 길이 상한과 무변경 검사를 포함한 모범 답안의 실행 결과입니다. 실패가 0이어도 테스트를 건너뛰었다면 완료가 아니므로 Skipped도 읽습니다.

python3 report.py

실행 결과

Tests=95
Failures=0
Errors=0
Skipped=0

확인 문제

실습

DTO의 Size와 서비스의 길이 검사를 보완합니다. 200 성공·201 거부·null·공백·잘못된 JSON의 DB 무변경을 확인합니다. 테스트를 삭제하거나 비활성화하지 않습니다. 제공된 README와 API-SPEC의 제한을 설명하며 제출합니다.

시작 코드·테스트 내려받기

실행 명령

./mvnw test

기대 결과

누적 테스트 95개, 실패·오류·건너뜀 0입니다.

모범 답안모범 답안 내려받기

더 읽기

면접 질문

  • 도서 조회·추가 API의 메서드와 상태 코드를 설명합니다.