3건을 일괄 저장하고, 마지막 1건은 주키(Primary Key) 중복으로 설정했습니다. 저는 저장 처리를 시도하면서 IntegrityError 이후에도 2건이 남아있는 것을 확인했습니다. 예외를 포착하는 위치를 바꾸자 이번에는 0건이 되었습니다.
'전부 성공하거나, 전부 취소하거나'로 처리해야 하는 배치(batch)라면, 예외는 with con: 바깥에서 포착하고 싶습니다. 안쪽에서 로그만 내면 중간까지의 데이터가 커밋되어 버립니다.
LLM의 처리 결과를 request_id 별로 저장하는 것을 상정했습니다. 3번째 건만 ID를 중복시켰습니다.
CPython 3.14.8, SQLite 3.53.4에서 확인했습니다. Python 3.12 이후의 표준 라이브러리만으로 동작합니다.
import sqlite3
rows = [("r1", "답변A"), ("r2", "답변B"), ("r1", "답변C")]
def run(catch_inside):
...
inside error: UNIQUE constraint failed: result.request_id
inside rows: 2
outside error: UNIQUE constraint failed: result.request_id
...
에러 메시지는 같지만, 저장 건수만 다릅니다.
executemany()는 동일한 INSERT를 각 파라미터로 반복합니다. 이번 설정에서는 첫 번째 INSERT에서 트랜잭션이 시작됩니다.
1번째와 2번째 건은 성공합니다. 3번째 건에서 주키 제약 조건에 위반되어, 기본 충돌 처리(default conflict handling)로 해당 INSERT가 실패합니다. 앞선 2건은 트랜잭션 내에 남아 있습니다.
여기서 예외를 포착하는 위치가 중요하게 작용합니다.
inside는 예외를 처리한 후 with를 빠져나옵니다. 정상 종료로 간주되어 2건이 커밋됩니다. outside는 예외가 with 바깥으로 빠지기 때문에, 종료 처리에서 롤백(rollback)되어 0건이 됩니다.
제가 가진 sqlite3.Connection.__exit__.__doc__에도, 예외가 있으면 rollback, 없으면 commit이라고 적혀 있습니다. 관련 선행 정보는 Python 공식 설명과 SQLite의 충돌 처리를 참고했습니다.
같은 연결(connection)에 앞서 미커밋된 업데이트가 있다면, 그것도 롤백 대상이 됩니다. with con:보다 이전의 쓰기 작업까지 확인하고 싶습니다.
autocommit=True에서는 이 with를 이용한 롤백은 작동하지 않습니다. 코드에서는 LEGACY_TRANSACTION_CONTROL과 DEFERRED를 명시했습니다.
with con:을 빠져나와도 연결은 닫히지 않습니다. 샘플의 finally는 이후 처리를 위한 것입니다.
LLM 배치를 통째로 재시행하려면, 실패 시 저장 건수가 0건으로 돌아가는 지점까지 확인하고 싶습니다. 안쪽에서 로그를 내는 경우에도, 예외를 다시 발생(re-raise)시키면 롤백으로 이어집니다. 재시도 로직을 추가하기 전에, 중간에서 실패하는 입력을 통과시켜야 '실패 처리되었는데 저장되어 있음'이라는 불일치를 잡아낼 수 있습니다.