Devin.KR

수정·삭제와 회귀 테스트

75분 안팎

학습 목표

HTTP 계약과 저장 결과를 함께 검증합니다.

개념

수정의 의미를 한 필드로 제한합니다

이번 PATCH는 도서의 title만 수정하는 API입니다. 부분 변경 메서드라도 지원하는 필드가 하나뿐이므로 title을 필수로 받고 누락과 null을 400으로 거부합니다. 빈 객체를 아무 변화 없는 성공으로 해석하지 않습니다. 이 정책을 명세에 적어 클라이언트가 PUT의 전체 교체나 일반 JSON Merge Patch와 혼동하지 않게 합니다. borrowed는 대여 이력에서 계산되는 조회 필드라 PATCH 요청의 수정 대상이 아닙니다.

서비스에서 검증과 대상 확인을 잇습니다

컨트롤러는 검증된 CreateBook과 경로 ID를 서비스로 넘깁니다. 서비스는 직접 호출에도 제목 규칙을 검사하고 find로 대상 존재를 확인한 뒤 repository.rename을 호출합니다. repository는 UPDATE books SET title=? WHERE id=?로 한 키의 값을 변경합니다. 제목은 바인딩하고 ID도 바인딩합니다. UPDATE 뒤 조회한 BookView를 반환해 200 응답에 ID·현재 제목·borrowed를 제공합니다. 메모리 객체만 rename해서 DB가 바뀌었다고 생각하지 않습니다.

영향 행 수가 없는 자원을 알려 줍니다

JdbcTemplate.update는 영향 행 수를 반환합니다. rename에서 0이면 404를 던집니다. 서비스가 앞서 찾았더라도 다른 연결이 삭제할 수 있으므로 최종 UPDATE 결과도 확인합니다. 이 실습의 synchronized는 한 서비스 객체의 요청만 직렬화하며 여러 JVM 전체를 잠그지 않습니다. 동시 변경 전체 정책은 아직 완성하지 않았습니다. 같은 제목으로 다시 수정한 경우의 행 수 의미는 사용 DB의 동작을 확인하며 다른 엔진에 그대로 일반화하지 않습니다.

삭제 성공은 빈 응답입니다

DELETE가 성공하면 204를 반환하고 JSON 도서나 성공 문구를 본문에 넣지 않습니다. 삭제 뒤 같은 ID의 GET은 404여야 하며 SELECT에서도 행이 없어야 합니다. 존재하지 않는 도서 DELETE는 이 계약에서 404입니다. 같은 삭제를 두 번 보내면 첫 응답은 204이고 다음은 404일 수 있습니다. 멱등성은 상태 코드가 매번 같다는 의미가 아니라 요청을 반복한 최종 자원 상태가 같은지와 관련된 성질입니다.

반납 이력도 보존할 이유를 정합니다

삭제 정책은 현재 borrowed가 false라는 이유만으로 허용하지 않습니다. 반납 완료 대여도 동아리의 기록이며 loans가 책을 참조합니다. 모든 이력이 있으면 FK가 삭제를 거부하고 API는 409 BOOK_CONFLICT를 반환합니다. CASCADE로 이력을 함께 지우거나 FK를 끄는 수정은 정책을 깨뜨립니다. 삭제 대신 목록 숨김이나 보관 상태를 원한다면 별도 요구로 설계합니다. 이번 미션은 물리 삭제와 참조 이력 보존의 경계를 명확히 합니다.

제약을 최종 판단으로 사용합니다

애플리케이션이 먼저 이력 개수를 읽고 0이면 삭제해도 두 문장 사이에 새 이력이 생길 수 있습니다. 그래서 실제 DELETE와 FK 결과를 신뢰합니다. JdbcRepository.delete 안의 DataIntegrityViolationException 매핑은 이 스키마의 해당 DELETE가 loans 참조 제약을 위반하는 범위에 한정합니다. 모든 저장 작업의 제약 오류를 무조건 삭제 충돌로 바꾸지 않습니다. 알 수 없는 SQL 오류는 마지막 안전망에서 500으로 처리하도록 구별합니다.

HTTP와 저장 결과를 함께 관찰합니다

patchPersistsAndKeepsIdentity는 PATCH가 200인지와 ID가 같은지 확인하고 후속 GET과 SQL SELECT의 제목을 대조합니다. 옆 도서 ID 8의 제목도 보존되는지 검사해 WHERE 누락을 잡습니다. deleteThenGetAndRepeat는 204 본문이 비어 있는지와 GET 404, 반복 DELETE 404, SQL 행 수 0을 함께 확인합니다. 응답 JSON만 예쁘게 바꾸는 구현과 DB만 바꾸고 잘못된 응답을 보내는 구현 모두 실패하도록 증거를 구성합니다.

실패 요청의 원래 상태를 보존합니다

invalidPatchDoesNotChangeTitle는 정상 도서에 빈 객체를 보내고 400 후 제목이 그대로인지 읽습니다. returnedHistoryAlsoBlocksDelete는 반납 완료 이력을 준비한 뒤 409와 부모·자식 행 보존을 확인합니다. activeLoanIsVisible은 미반납 이력으로 borrowed가 true임을 읽고 제목 수정 후에도 대여 상태가 유지되는지 확인합니다. 성공 경로만 붙인 테스트는 삭제 제한과 불변식을 놓치므로 경계와 실패를 별도 시나리오로 둡니다.

앞 모듈 결과를 잃지 않습니다

미션 starter는 앞 단계 소스와 SQL 모델·파일 회귀 테스트를 포함하고 이번 JDBC 경계의 미완성 부분을 추가합니다. BookRepository 인터페이스와 도서 응답 모양을 유지합니다. ApiConfiguration에서 JDBC 기본값을 추가하지만 catalog.path를 명시한 과거 테스트는 파일 저장소를 계속 확인합니다. 누적 검사가 실패할 때 새 API 요구만 살펴보지 말고 어떤 기존 계약이 달라졌는지 확인합니다. SQL 이관의 borrowed=true 거부와 원본 보존도 여전히 통과해야 합니다.

미션 제출을 리뷰 가능한 증거로 만듭니다

전체 테스트를 실행하고 report.py로 Tests·Failures·Errors·Skipped를 기록합니다. API-SPEC에는 PATCH 필수 제목과 DELETE 이력 정책, 오류 코드, 제목 길이 단위를 적습니다. MAX(id)+1의 삭제 후 재사용과 다중 인스턴스 한계도 숨기지 않습니다. starter에서는 rename이 실제로 UPDATE하지 않아 후속 GET과 SQL 비교가 실패합니다. SQL을 완성하고 204·404·409·500 시나리오까지 확인한 뒤 변경 파일과 실패에서 성공으로 바뀐 근거를 제출합니다.

사용한 API의 범위는 JdbcTemplate update 공식 API에서 확인합니다. 실습 결과는 제공된 버전과 임시 DB에서 직접 검사한 범위입니다.

따라하기

영향 행 수를 검사합니다

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

if (jdbc.update("UPDATE books SET title=? WHERE id=?", title, id) == 0)
 throw new ResponseStatusException(HttpStatus.NOT_FOUND);

누적 테스트를 실행합니다

UPDATE를 완성한 뒤 patchPersistsAndKeepsIdentity의 두 도서 값과 deleteThenGetAndRepeat의 빈 응답 검사를 실행합니다. 이력 충돌 테스트까지 포함한 로그를 남깁니다.

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

실행 보고서를 집계합니다

수정·삭제·후속 GET·SQL 대조까지 포함해 실행한 누적 결과입니다. 마지막 미션도 같은 95개를 모두 유지하고 실패와 오류 없이 통과시켜 제출합니다.

python3 report.py

실행 결과

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

확인 문제

실습

JdbcRepository.rename의 UPDATE를 완성합니다. 대상 ID와 옆 도서 보존, DELETE 이후 조회, 반납 이력 삭제 충돌을 함께 검사합니다. 테스트를 삭제하거나 비활성화하지 않습니다. 제공된 README와 API-SPEC의 제한을 설명하며 제출합니다.

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

실행 명령

./mvnw test

기대 결과

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

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

더 읽기

면접 질문

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