기록을 불러오고 있습니다.
기록을 불러오고 있습니다.
LAB 02 / Database · Consistency
같은 재고를 읽은 트랜잭션이 충돌하는 순간과 행 잠금의 동작을 따라갑니다.
재고가 하나인데 여러 요청이 모두 성공했다고 응답할 수 있습니다. 사진 저장도 한도 검사와 실제 저장 사이에 다른 요청이 끼어들 수 있습니다.
두 트랜잭션이 같은 이전 값을 읽고, 각자 검사한 뒤 변경하기 때문입니다. @Transactional만으로 여러 트랜잭션의 검사와 쓰기가 자동 직렬화되지는 않습니다.
설정을 바꾸면 재생이 초기화됩니다. 자동 재생하거나 한 단계씩 실행해 보세요.
선택한 실행 순서의 최종 결과 · 재생 중간값과 구분됩니다.
L 잠금 · W 대기 · R 읽기 · U 쓰기 · C 커밋 · X 거부 · ↗ 잠금 해제. 가로로 움직이면 모든 단계를 볼 수 있습니다.
READ COMMITTED를 가정한 결정적 교육 모델입니다. 락 없음은 SELECT 후 애플리케이션이 계산한 절대값으로 UPDATE하는 경우의 lost update를 보여줍니다. 실제 DB의 원자적 조건부 UPDATE, 낙관적 락, REPEATABLE READ/SERIALIZABLE에서는 대기·충돌·재시도 결과가 달라질 수 있습니다. 잠금이 있는 경우 도착한 요청은 차례로 읽습니다.
R은 읽기, U는 쓰기입니다. 모두 먼저 읽은 뒤 같은 절대값을 쓰는 순서와, 한 요청이 끝나야 다음 요청이 읽는 순서를 비교하세요. 비관적 락에서는 동일 행의 잠금 해제를 기다린 뒤 값을 다시 읽습니다.
겹친 실행에서는 오래된 값으로 판단할 수 있습니다. 그러나 순차 실행이면 같은 문제가 나타나지 않습니다. 이 실험은 특정 lost update 시나리오입니다.
SELECT FOR UPDATE로 같은 행의 경쟁 작업을 직렬화합니다. 일반 SELECT를 모두 차단하는 것은 아닙니다. 대기·타임아웃·교착과 재시도 비용이 생깁니다.
원리를 설명하는 코드입니다. 실제 프로젝트 원본과 구분하며, 엔티티·도메인 타입·import 일부는 생략했습니다.
@Transactional
public boolean reserve(Long id) {
Stock stock = stockRepository.findByIdForUpdate(id).orElseThrow();
if (stock.getQuantity() <= 0) return false;
stock.decrease(); // 같은 트랜잭션의 dirty checking UPDATE
return true;
}
// 락 없는 모델은 findById()로 읽은 절대값을 나중에 기록합니다.
// 실제 DB에서 원자 UPDATE나 버전 검사를 쓰면 결과가 달라집니다.@Transactional
public void savePhoto(Long albumId, PhotoInput input) {
Album album = albumRepository.findByIdForUpdate(albumId)
.orElseThrow(AlbumNotFoundException::new);
if (photoRepository.countByAlbumId(albumId) >= 20) {
throw new PhotoLimitExceededException(); // RuntimeException
}
photoRepository.save(Photo.of(album, input));
}
// 서비스 프록시를 통해 호출. 모든 사진 저장 경로가 같은 행을 잠급니다.
// S3/외부 API 호출을 잠금을 잡은 DB 트랜잭션에 포함하지 마세요.public interface AlbumRepository extends JpaRepository<Album, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select a from Album a where a.id = :id")
Optional<Album> findByIdForUpdate(@Param("id") Long id);
}
// PostgreSQL의 SELECT ... FOR UPDATE에 해당하는 잠금 의도.
// 실제 SQL은 Hibernate dialect/버전에 따라 확인합니다.위 재고 카운터 시뮬레이션과 실제 사진 INSERT 사례는 서로 다른 모델입니다. 실제 결과가 모든 실행 환경에서 동일하다고 보장하지 않습니다.
READ COMMITTED 가정입니다. REPEATABLE READ/SERIALIZABLE은 스냅샷과 직렬화 실패가 다를 수 있습니다. 실제 사진 한도는 존재하는 사용자/앨범 행을 먼저 잠그고 같은 트랜잭션에서 개수를 세고 삽입해야 합니다. 모든 쓰기 경로가 이 규칙을 따라야 합니다.