Devin.KR

요청 식별과 멱등성

95분 안팎

학습 목표

요청 키·본문·응답을 연결해 재시도 정책을 정의합니다.

개념

응답을 못 받았다고 실패한 것은 아닙니다

서버가 대여를 커밋하고 응답을 보내던 중 연결이 끊어질 수 있습니다. 클라이언트는 성공 여부를 모르므로 다시 요청합니다. 새 대여로 실행하면 이미 빌린 책이라는 409만 보거나, 반납 이후라면 같은 의도가 두 번째 대여를 만들 수도 있습니다. 요청 식별자는 불확실한 전달 결과를 같은 업무 요청으로 연결하는 장치입니다. 이번 레슨은 같은 회원의 같은 키와 같은 본문을 저장된 대여 결과로 재생하는 계약을 구현합니다.

회원 범위의 키를 사용합니다

loan_requests의 PRIMARY KEY는 member_id와 request_key의 조합입니다. 서로 다른 회원이 같은 문자열 키를 쓰는 것은 허용합니다. 로그인 회원은 세션 Principal에서 읽고 본문 memberId로 바꾸지 않습니다. 키 자체는 인증 정보가 아닙니다. 키가 있어도 로그인과 CSRF 검사를 통과해야 합니다. 키를 다른 회원의 결과를 읽는 전역 암호처럼 취급하면 개인 자원 경계가 깨집니다. DB 조회와 변경마다 두 값을 함께 조건으로 사용합니다.

입력 규칙은 명시적으로 제한합니다

Idempotency-Key 헤더는 영문 대소문자, 숫자, 밑줄, 하이픈으로 이루어진 1~64자입니다. 빈 문자열과 공백, 65자는 400입니다. 헤더가 없는 요청은 기존 대여 API 동작을 유지합니다. 헤더가 비어 있는 요청을 없는 헤더와 동일하게 처리하지 않습니다. 키를 매번 새로 만들면 같은 요청을 이어 붙일 수 없습니다. 클라이언트는 한 번의 대여 의도에서 키를 정하고 통신 실패 후 같은 값을 다시 보내야 합니다.

문자열 JSON 대신 업무 입력을 비교합니다

본문의 공백과 필드 순서가 달라도 검증된 bookId가 같으면 이 API에서는 같은 대여입니다. 그래서 borrow:v1:bookId=7이라는 정규화 문자열을 UTF-8로 인코딩하여 SHA-256으로 저장합니다. member_id는 키의 범위에 이미 포함됩니다. 이 계약에서는 bookId 이외의 알 수 없는 필드를 대여 의미에 사용하지 않습니다. 향후 다른 업무 필드가 추가되면 정규화 규칙도 함께 확장합니다. 임의 JSON의 hashCode나 공백 제거만으로 모든 본문의 동등성을 판단하지 않습니다.

기존 요청을 먼저 확인합니다

saved는 회원과 키로 body_hash와 loan_id를 조회합니다. 행이 있고 해시가 같으면 해당 ID를 돌려줍니다. 같은 키인데 도서가 다르면 KeyConflict를 던져 REQUEST_KEY_CONFLICT 409로 변환합니다. 새 책이 비어 있어도 기존 키의 의미를 덮어쓰지 않습니다. starter는 같은 해시의 기존 결과 대신 잘못된 ID -1을 반환합니다. TODO 위치에 if(existing!=null)return existing;를 복원하여 새 INSERT보다 앞에서 종료하게 합니다.

예약과 대여를 한 번에 커밋합니다

새 키라면 loan_requests에 예약 행을 넣고 loans.borrow를 호출합니다. 예약 행의 loan_id 0은 트랜잭션 내부의 임시값입니다. 실제 대여 ID를 얻으면 같은 행을 UPDATE하고 전체 트랜잭션이 커밋된 뒤 반환합니다. 외부 호출자는 미커밋 예약을 결과로 읽지 않습니다. 중간 실패가 생기면 예약과 이력, 현재 행 모두 취소됩니다. 0을 성공 결과로 응답하는 경로를 만들지 않습니다. 이 예제는 요청 결과의 관계를 서비스 트랜잭션과 테스트로 검증하며 예약 0 때문에 loan_id 외래키를 두지 않았습니다.

대여보다 요청 키를 먼저 차지합니다

같은 키의 두 요청이 먼저 책을 빌리기 시작하면 두 번째가 현재 대여 충돌로 끝나 재생 기회를 잃기 쉽습니다. 예약 PK를 먼저 INSERT하면 동일 회원·키의 경쟁은 요청 테이블에서 판정됩니다. 한쪽이 커밋하면 다른 쪽의 예약 INSERT는 중복키가 됩니다. 같은 본문이면 패배 호출도 승자의 결과를 재생합니다. 다른 회원이나 다른 키가 같은 책을 빌리려는 경쟁은 기존 active_loans PK가 처리합니다. 두 제약을 하나로 합치면 요청 동일성과 자원 점유를 구분할 수 없습니다.

트랜잭션 밖의 조정자를 둡니다

RequestLoans는 TransactionTemplate을 사용하여 요청별 작업을 시작합니다. propagation은 REQUIRES_NEW이므로 각 execute와 충돌 뒤 조회가 별도의 경계를 갖습니다. 안에서 호출하는 LoanTransactions.borrow의 REQUIRED 트랜잭션은 같은 매니저의 현재 작업에 참여합니다. 외부 조정 메서드 전체에 또 트랜잭션을 붙여 실패한 경계를 재사용하지 않습니다. DuplicateKeyException을 받은 시점에는 execute가 이미 실패한 작업을 롤백했으며 그 뒤 새 execute로 승자 결과를 읽습니다.

응답의 의미를 보존합니다

이 API의 생성 결과는 201, JSON의 id, Location의 /loans/{id}입니다. 재생 시에도 저장된 같은 ID로 세 값을 구성합니다. 저장하지 않은 현재 시각을 응답마다 덧붙이지 않습니다. 나중에 응답 필드가 늘어나면 저장 결과를 확장하거나 버전별 재생 계약을 정의해야 합니다. 여기서 멱등성은 같은 요청의 업무 효과가 추가되지 않는다는 뜻이며 모든 서버 로그와 전달 횟수가 같아진다는 뜻은 아닙니다. 결과 ID가 같아도 대여 이력이 증가하면 구현이 잘못된 것입니다.

반납 뒤의 오래된 키도 같은 요청입니다

대여가 반납되어 현재 행이 사라진 뒤에도 같은 키는 과거 대여의 결과를 돌려줍니다. 옛 키를 새 대여 키로 재사용하면 클라이언트는 이미 끝난 대여 ID를 받습니다. 새로운 대여 의도에는 새 키가 필요합니다. 이 실습은 키 만료나 삭제를 구현하지 않습니다. 따라서 보장 범위는 키 기록을 유지하는 기간입니다. 운영 설계에서는 최대 재시도 기간, 보존 기간, 정리 작업, 개인정보 접근 정책을 함께 정해야 하며 임의로 키를 지우면 늦은 재시도가 새 효과를 만들 수 있습니다.

실패한 키는 성공으로 기록하지 않습니다

failedAttemptDoesNotPoisonKey는 after-history에서 예외를 주입합니다. 호출 후 요청 행과 이력이 0인지 확인한 뒤 주입을 끄고 같은 키로 성공하는지 검사합니다. 예외를 catch해서 0 ID를 반환하거나 요청만 별도 트랜잭션으로 커밋하면 이 테스트가 실패합니다. BOOK_NOT_FOUND 같은 검증 실패도 성공 결과가 아닙니다. 이 미션은 성공한 대여만 키에 저장하며 실패 응답 자체의 영구 재생은 별도 정책으로 다루지 않습니다.

완료 근거를 결과와 행으로 제시합니다

lostResponseReplaysCommittedId는 첫 반환값을 네트워크에 전달하지 못했다고 가정하고 같은 키로 다시 호출합니다. changedBodyIsConflict는 다른 책으로 키를 재사용한 시도를 거절합니다. memberScopesKey는 회원별 범위를, replayAfterReturnDoesNotReborrow는 과거 결과의 보존을 검사합니다. 수정 후 이 테스트들의 결과를 누적 회귀 검사와 함께 읽습니다. 제약 타입의 일반 설명은 더 읽기로 연결하고 제출에서는 요청 키, 정규화 본문, 커밋 결과의 연결을 도서 대여 흐름으로 설명합니다.

따라하기

실패한 검사를 찾습니다

starter에서 실행합니다. lostResponseReplaysCommittedId가 같은 ID를 얻는지 확인합니다.

./mvnw test

TODO의 경계를 복원합니다

RequestLoans의 첫 saved 결과 확인 뒤 TODO에 if(existing!=null)return existing;를 넣습니다.

수정 후 전체 검사를 실행합니다

기존 ID 반환을 복원한 뒤 재실행합니다. 순차 재생과 다른 본문 거절, 반납 뒤 과거 ID 반환이 모두 통과해야 합니다.

./mvnw test

누적 결과를 집계합니다

요청 식별 실습의 전체 테스트가 끝난 뒤 보고서를 집계합니다. Tests=156, Failures=0, Errors=0, Skipped=0인지 본인의 결과를 확인합니다.

python3 report.py

확인 문제

실습

RequestLoans의 첫 saved 결과 확인 뒤 TODO에 if(existing!=null)return existing;를 넣습니다. 테스트는 삭제하거나 완화하지 않습니다. 정상·경계·실패 결과와 작업 종료를 확인하여 수정 코드와 실행 보고서를 제출합니다.

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

실행 명령

./mvnw test

기대 결과

누적 156개 검사, Failures=0, Errors=0, Skipped=0.

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

더 읽기

면접 질문

  • 대여 처리의 트랜잭션 범위를 설명합니다.