Conversation
| fun execute(request: Request): Result<BookResponse, BookError> { | ||
| val isbn = Book.normalizeIsbn(request.isbn) | ||
| if (isbn != null && bookRepository.findByIsbnAndDeletedAtIsNull(isbn) != null) { | ||
| return BookError.DUPLICATE_ISBN.err() |
There was a problem hiding this comment.
[MEDIUM] ISBN 중복 검사가 check-then-act라 동시 요청 시 레이스 컨디션 발생
Problem: findByIsbnAndDeletedAtIsNull 조회와 save 사이에 DB 락이나 유니크 제약이 없다. 두 요청이 같은 ISBN으로 거의 동시에 들어오면 둘 다 중복 검사를 통과한 뒤 각자 커밋되어, DUPLICATE_ISBN이 막으려는 불변식이 깨진 채로 활성 도서 2건이 동일 ISBN을 가지게 된다.
Evidence: Book.kt#L24의 isbn 컬럼에 unique = true가 없고, 레포지토리 전체를 확인해도 마이그레이션/스키마 파일이 없어 DB 레벨 유니크 제약이 전혀 없다. 같은 패턴이 UpdateBookService.kt#L25-L31에도 있다.
Fix direction: 소프트 삭제 특성상 단순 컬럼 유니크 제약은 부적합하므로(삭제된 도서의 ISBN은 재사용 가능해야 함), deleted_at IS NULL 조건의 부분 유니크 인덱스를 DB에 추가하고, 저장 시 발생하는 제약 위반 예외를 잡아 DUPLICATE_ISBN으로 변환하는 방식을 권장한다.
try {
val book = bookRepository.save(
Book(title = request.title, author = request.author, isbn = isbn),
)
return BookResponse.from(book).ok()
} catch (e: DataIntegrityViolationException) {
return BookError.DUPLICATE_ISBN.err()
}There was a problem hiding this comment.
수정 완료
-
인덱스
deleted_at IS NULL조건의 부분 인덱스는 PostgreSQL 기능으로 MySQL에서 사용이 불가능하여
함수 기반 유니크 인덱스를 통해 인덱스를 추가하여 활성 도서끼리만 ISBN 중복을 금지CREATE UNIQUE INDEX ux_books_active_isbn ON books ((IF(deleted_at IS NULL, isbn, NULL)));
-
예외 처리
예외를 전파하여 핸들러에서ConstraintKind.UNIQUE만 409로 반환하고 있습니다.
| if (owner != null && owner.id != id) { | ||
| return BookError.DUPLICATE_ISBN.err() | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] 동일한 ISBN 레이스 컨디션이 수정 API에도 존재
Problem: owner == null || owner.id == id 검사와 book.update(...) 사이에 락이나 DB 유니크 제약이 없다. 서로 다른 도서를 같은 새 ISBN으로 동시에 수정하면 둘 다 검사를 통과한 뒤 커밋되어 DUPLICATE_ISBN이 막으려는 불변식이 깨진다.
Evidence: 동일 패턴에 대한 근본 원인과 수정 방향은 CreateBookService.kt#L23 코멘트에 정리했다.
Fix direction: deleted_at IS NULL 부분 유니크 인덱스를 추가하고, book.update(...) 저장 시 발생하는 제약 위반 예외를 DUPLICATE_ISBN으로 변환하는 방식을 동일하게 적용한다.
연관 이슈
작업 사항
feat(#3): 도서 등록 API 추가feat(#3): 도서 단건 조회 API 추가feat(#3): 도서 수정 API 추가feat(#3): 도서 삭제 API 추가테스트
주의 사항 및 참고사항