--- kind: CASE slug: a05-f029-for-update-skip-locked title: SKIP LOCKED 로 잡은 조정 작업을 두 번째 연결이 그대로 받는다 topic: state-ownership-and-concurrency project: clean-architecture-backend-template status: 게시 전 sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 rootTreeNode: case:a05-f029-for-update-skip-locked evidenceCapturedOn: 2026-09-02 assets: - key: a05-f029-for-update-skip-locked file: ../../../final/evidence/rendered/a05-f029-for-update-skip-locked.svg - key: a05-f029-for-update-skip-locked-probe file: ../../../final/evidence/rendered/a05-f029-for-update-skip-locked-probe.svg evidence: - ../../../final/evidence/raw/a05-f029-for-update-skip-locked.txt - ../../../final/evidence/raw/a05-f029-for-update-skip-locked-probe.txt source: - 원본 분석 절은 `final/document.md#a05` §90 이다. 등급은 P2 이고, 잠금 수명과 작업 수명이 다르다는 판정과 두 작업자 시나리오 탐침이 그 절에 있다. 그 절도 수정으로 짧은 청구 쪽을 택한다. - 표에 소유자와 리스 열이 아예 없다는 것, 같은 패키지의 전달 큐 청구문과의 대조, 그리고 두 도달 조건은 이 기록에서 확인했다. --- # SKIP LOCKED 로 잡은 조정 작업을 두 번째 연결이 그대로 받는다 조정 작업 청구는 `FOR UPDATE SKIP LOCKED` 로 끝나는 SELECT 하나다. 트랜잭션도 열지 않고 표에 소유자나 리스를 쓰지도 않으므로, 그 조회가 끝나면서 잠금도 사라진다. 같은 패키지의 전달 큐는 같은 잠금 구문 뒤에 `UPDATE` 를 붙여 청구를 행에 남긴다. ## 관계 - **조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다** 이 사례가 위반하는 규칙이다. - **fenced lease — 만료 시각만으로는 부족한 이유** 수정 방향이 기대는 구조다. - **CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다** 조건을 where 절에 전부 넣고 갱신 결과로 판단하라는 규칙이다. ## 문제 청구 저장소의 javadoc 은 잠금 구문을 쓰는 이유를 전달 큐와 같다고 적는다. 같은 표를 폴링하는 두 작업자가 같은 작업을 둘 다 가져가면 안 된다는 것이다. ## 결론 청구 메서드는 질의 하나를 실행하고 끝난다. 그 클래스에서 트랜잭션을 여는 애너테이션도 포트 호출도 찾을 수 없다. 자동 커밋이면 SELECT 종료와 함께 암묵 커밋이 걸리고 행 잠금도 그 자리에서 풀린다. 표 쪽에도 남길 자리가 없다. 조정 작업 표의 열은 다음 확인 시각과 시도 횟수와 마지막 결과뿐이고, 이 표를 건드리는 마이그레이션은 만든 것과 색인 하나와 완결성 검사용 이름 리터럴이 전부다. 작업자도 전체 구간을 감싸지 않는다. 만기 조회로 목록을 받은 뒤, 제공자를 다녀오고 나서야 상태를 쓴다. 실제 PostgreSQL 16 에서 자동 커밋 연결 둘이 그 문장을 차례로 실행했다. 사이에 정산은 없다. 둘 다 같은 작업을 받았고 행 상태는 그대로다. 겹쳐 실행하지 않아도 재현된다는 것이 잠금이 이미 사라졌다는 뜻이다. 같은 패키지의 전달 큐는 다르게 한다. 이쪽은 잠금 구문을 CTE 로 감싸고 한 문장 안에서 소유자·리스 만료·펜스를 갱신하며 상태를 배송 중으로 바꾼다. 청구가 행에 남는 시점이 잠금이 풀리기 전이다. 조정 쪽에서만 그 쓰기가 생략됐다. 여기서 일어나는 일이 전송이 아니므로 판정을 한 단계 낮췄다. 깨지는 것은 javadoc 이 적어 둔 계약 쪽이다. ## 검증 환경 OpenJDK : 21.0.12 데이터베이스 : PostgreSQL 16.15, 실제 실행 확인 방식 : 청구 메서드와 작업자의 경계 선언 확인, 표를 건드리는 마이그레이션 전수, 실제 PostgreSQL 에서 두 연결로 같은 문장 실행 소스 수정 : x ca-skeleton.notification.platform.enabled 가 참이고 모드가 SERVING 이며 인스턴스가 둘 이상인 배포다. 출하 기본값이 거짓이고, 프로세스가 하나면 스케줄러가 자기 겹침을 막는다. ## 재현 조건 1. 청구 저장소의 javadoc 과 청구 문장, 그리고 그것을 실행하는 메서드를 읽는다. 2. 그 클래스와 작업자 클래스의 트랜잭션 경계 선언을 센다. 3. 조정 작업 표의 열과, 그 표를 건드리는 마이그레이션을 전부 나열한다. 4. 같은 패키지의 전달 큐 청구문을 읽는다. 5. PostgreSQL 에 알림 플랫폼 마이그레이션을 적용하고 만기 작업을 하나 넣는다. 6. 자동 커밋 연결 둘로 그 문장을 차례로 실행하고 받은 작업과 행 상태를 비교한다. ## 본문 조정 작업 저장소는 청구 질의를 `FOR UPDATE SKIP LOCKED` 로 끝낸다. javadoc 이 이유를 적어 두었다. ```text *
Claiming is {@code FOR UPDATE SKIP LOCKED} inside the select, for the same reason the delivery
* queue is: two workers polling the same table must not both take the same job.
```
## 청구 메서드는 트랜잭션을 열지 않는다
:::evidence key="a05-f029-for-update-skip-locked" alt="청구 저장소의 javadoc 과 청구 문장과 그것을 실행하는 메서드, 그 클래스의 트랜잭션 경계 선언 수, 같은 패키지 전달 큐의 청구문 전문, 작업자의 처리 순서와 그 클래스의 경계 선언 수, 조정 작업 표의 열 정의와 그 표를 건드리는 마이그레이션 전부, 그리고 알림 플랫폼 마스터 스위치의 출하 기본값과 배경 작업자 스케줄러의 스레드 수를 출력한 터미널 기록." caption="javadoc 은 전달 큐와 같은 이유라고 적음 · 청구는 질의 하나, 경계 선언 0 · 전달 큐는 같은 잠금 구문에 UPDATE 를 붙여 소유자·리스·펜스를 씀 · 표에는 그 열이 없음 · 마스터 스위치 기본값 false, 스케줄러 스레드 1 — 97줄 · exit 0" zoom="true"
:::
```java
public List