대여 상태 전이
90분 안팎
학습 목표
가능·대여 중·반납 상태와 실패 조건을 명세합니다.
개념
대여 상태를 먼저 합의하는 이유
동아리 회원이 도서 7번을 빌렸는데 도서 목록에는 대여 가능으로 보이면 다음 회원도 대여를 요청합니다. 버튼 문구만 바꿔서는 이 모순을 막지 못합니다. 서버가 어떤 상태에서 어떤 요청을 받아들이는지 정해야 합니다. 이 레슨에서는 아직 SQL을 실행하지 않고 한 권의 이벤트를 순서대로 처리하는 작은 프로그램으로 업무 규칙을 고정합니다. 상태 전이가 명확해야 다음 레슨에서 함께 저장할 데이터와 실패 후 되돌릴 데이터를 정할 수 있습니다.
도서와 대여 기록은 수명이 다릅니다. 도서는 반납 뒤에도 같은 도서이고, 대여 기록은 회원이 바뀔 때마다 새로 만들어집니다. 그래서 도서의 현재 상태는 AVAILABLE 또는 LOANED로 보고, 개별 대여 기록에는 미반납 또는 반납 완료를 표시합니다. RETURNED를 도서의 영구 최종 상태로 두면 반납한 책을 다시 빌릴 수 없습니다. 상태 이름보다 그 상태가 어느 자원에 붙는지 먼저 설명합니다.
실습 환경과 입력 약속
브라우저 실습은 JavaScript로 표준 입력을 읽으며 별도 설치가 필요하지 않습니다. 첫 줄은 이벤트 수 N이고 이어지는 N줄은 BORROW 회원ID 또는 RETURN 회원ID입니다. N은 0 이상, 회원ID는 양의 정수이며 입력은 이 형식을 만족한다고 가정합니다. 마지막 출력은 상태, 현재 회원ID, 거부 건수를 공백으로 나눈 한 줄입니다. 대여 가능한 상태의 회원ID는 0으로 표시합니다. 실제 회원을 0으로 만들지 않으므로 빈 상태와 혼동하지 않습니다.
뒤의 Java 실습은 JDK 17과 Spring Boot 3.1.5, 메모리 H2를 사용합니다. ZIP을 새 폴더에 풀고 포함된 Maven wrapper로 ./mvnw test를 실행합니다. 시스템 Maven이나 운영 설정은 필요하지 않습니다. 작성 검증은 준비된 캐시에서 오프라인으로 실행했습니다. 테스트는 MockMvc를 사용하므로 서버를 켜지 않습니다. 각 ZIP은 앞 모듈의 완료 코드와 회귀 검사를 포함하며 starter의 일부 실패를 고치는 방식입니다. 공통 실행 환경은 여기서 안내하고 뒤에서는 변경할 메서드만 다룹니다.
상태와 요청자를 함께 검사합니다
AVAILABLE에서 BORROW가 들어오면 현재 회원을 요청 회원으로 바꾸고 LOANED가 됩니다. LOANED에서 BORROW는 요청자가 현재 회원과 같아도 거절합니다. 이미 처리한 요청을 같은 결과로 돌려주는 재시도 정책은 다음 모듈에서 요청 식별자와 함께 다룹니다. 지금은 두 번째 대여가 새로운 요청이라는 계약입니다. 따라서 상태가 대여 중이면 회원 이름이나 화면 버튼에 관계없이 거부 건수만 증가시킵니다.
RETURN은 LOANED 상태이고 현재 회원과 요청 회원이 같을 때만 성공합니다. 성공 후 현재 회원을 0으로 지우고 AVAILABLE로 돌아갑니다. 대여 가능한 상태에서의 반납, 다른 회원의 반납은 거절하며 현재 상태와 현재 회원을 바꾸지 않습니다. 권한 오류가 발생했는데 먼저 회원을 지우면 원래 누가 빌렸는지 잃습니다. 조건을 검사하는 코드와 변경하는 코드를 분리해 성공 분기에만 대입문을 둡니다.
표를 코드의 분기로 옮깁니다
상태 표는 세 칸으로 적습니다. 첫 칸은 현재 상태와 소유자 조건, 둘째 칸은 이벤트, 셋째 칸은 다음 상태와 거부 여부입니다. AVAILABLE와 BORROW는 성공, LOANED와 BORROW는 거부, LOANED와 소유자의 RETURN은 성공, 나머지 RETURN은 거부입니다. 먼저 이 네 규칙을 작은 입력으로 확인한 뒤 반복문을 붙입니다. 이벤트가 성공했을 때만 현재 회원 변수를 갱신하면 상태와 소유자 값이 서로 어긋날 가능성을 줄일 수 있습니다.
한 권만 다루는 실습에서는 owner가 0인지로 상태를 유도할 수 있습니다. 별도의 available 불리언까지 두면 owner는 1인데 available은 true인 모순을 만들 수 있습니다. owner가 0이면 AVAILABLE, 그 외에는 LOANED라는 불변식을 사용합니다. DB 구현에서는 active_loans에 해당 도서 행이 있는지가 현재 대여를 나타냅니다. 이력이 몇 건 있는지만 보고 현재 상태를 판단하면 이미 반납한 기록까지 대여 중으로 오해할 수 있습니다.
거절은 상태를 보존하는 사건입니다
BORROW 1, RETURN 2, RETURN 1 순서라면 두 번째 이벤트는 거절되어도 세 번째 이벤트는 성공해야 합니다. 거부 건수는 1이고 최종 상태는 AVAILABLE입니다. 거절한 RETURN 2에서 owner를 0으로 바꾸는 구현은 마지막 요청까지 거부하므로 거부 건수가 2가 됩니다. 최종 상태만 검사하면 이 결함을 놓칠 수 있습니다. 상태·현재 회원·거부 건수를 함께 기대값으로 두어 중간 실패가 후속 처리에 준 영향까지 관찰합니다.
이벤트 수가 0일 때는 반복문을 실행하지 않고 AVAILABLE 0 0을 출력합니다. 입력 전체를 trim한 뒤 빈 줄을 이벤트라고 세지 않습니다. 첫 줄의 숫자와 실제 이벤트 줄을 구분하고 N개만 처리합니다. Number 변환을 하지 않은 회원ID 문자열과 숫자를 엄격 비교하면 소유자 반납이 실패할 수 있습니다. 동일한 형태로 변환한 값끼리 비교합니다. 형식이 보장된 입력과 업무상 거절을 구분하면 잘못된 파싱을 권한 오류로 착각하지 않습니다.
흔한 실패를 입력으로 좁힙니다
Expected AVAILABLE 0 1인데 실제 LOANED 1 1이면 성공한 반납 뒤 현재 회원을 지우는 분기를 확인합니다. 거부 건수만 다르면 요청을 무시했는지, 거절을 두 번 세었는지 확인합니다. ReferenceError: owner is not defined는 업무 규칙보다 변수 선언 범위를 먼저 봐야 하는 오류입니다. 루프 안에서 owner를 매번 새로 선언하면 사건 사이의 상태가 사라집니다. 현재 상태는 루프 바깥에 두고 이벤트만 루프 안에서 읽습니다.
완료 설명에는 정상 대여·반납 한 쌍, 타인의 반납, 연속 대여, 사건 0개를 각각 제시합니다. “모든 요청이 성공한다”가 완료 기준은 아닙니다. 거절해야 할 요청이 상태를 보존하는지도 성공 기준입니다. 이 프로그램은 DB 동시성이나 실제 로그인 세션을 검증하지 않습니다. 단일 도서의 순차 규칙을 확정하는 용도로 사용하고, 저장의 원자성과 자원 접근 보호는 누적 Java 실습에서 함께 확인합니다. 제어 흐름의 문법을 다시 확인하려면 더 읽기 장을 참고합니다.
따라하기
허용된 대여를 확인합니다
브라우저 JavaScript 실행 창에 코드를 그대로 입력합니다. 각 조각은 독립된 예제입니다. 출력 값으로 상태와 소유자 검사의 결과를 확인합니다.
let owner = 0;
const member = 1;
if (owner === 0) owner = member;
console.log('LOANED ' + owner);실행 결과
LOANED 1
타인의 반납을 보존합니다
브라우저 JavaScript 실행 창에 코드를 그대로 입력합니다. 각 조각은 독립된 예제입니다. 출력 값으로 상태와 소유자 검사의 결과를 확인합니다.
let owner = 1, rejected = 0;
const member = 2;
if (owner === member) owner = 0;
else rejected++;
console.log(owner, rejected);실행 결과
1 1
정상 반납을 확인합니다
브라우저 JavaScript 실행 창에 코드를 그대로 입력합니다. 각 조각은 독립된 예제입니다. 출력 값으로 상태와 소유자 검사의 결과를 확인합니다.
let owner = 1;
const member = 1;
if (owner === member) owner = 0;
console.log(owner === 0 ? 'AVAILABLE' : 'LOANED');실행 결과
AVAILABLE
연속 이벤트를 채점합니다
실습의 TODO에 상태 표의 성공 분기 두 개와 거절 분기를 작성합니다. 사건 0개, 타인 반납, 재대여 입력을 각각 실행하고 최종 상태뿐 아니라 거부 건수도 비교합니다.
확인 문제
실습
한 권의 이벤트 N개를 처리합니다. BORROW는 비어 있을 때, RETURN은 현재 회원일 때만 성공합니다. 거절은 상태를 바꾸지 않습니다. 마지막에 AVAILABLE 0 거부건수 또는 LOANED 회원ID 거부건수를 출력합니다. 입력 형식과 범위는 본문을 따릅니다.
모범 답안
const fs = require('fs');
const lines = fs.readFileSync(0, 'utf8').trim().split(/\r?\n/);
const n = Number(lines[0]);
let owner = 0, rejected = 0;
for (let i = 1; i <= n; i++) {
const [event, value] = lines[i].trim().split(/\s+/);
const member = Number(value);
if (event === 'BORROW' && owner === 0) owner = member;
else if (event === 'RETURN' && owner === member) owner = 0;
else rejected++;
}
console.log(owner === 0 ? 'AVAILABLE 0 ' + rejected : 'LOANED ' + owner + ' ' + rejected);
더 읽기
면접 질문
- 대여 처리의 트랜잭션 범위를 설명합니다.