TIL - 20260816

juni·2026년 8월 16일

TIL

목록 보기
431/468

0816 데이터베이스 실무 심화 (4/N): Transaction, 동시성 문제와 데이터 정합성


✅ 1. Transaction이란 무엇인가?

  • Transaction은 여러 DB 작업을 하나의 작업 단위로 묶는 기능입니다.
  • 중간에 하나라도 실패하면 전체를 되돌리고, 모두 성공해야 최종 반영합니다.
  • 실무에서는 상담 신청 저장, 상태 변경 이력 저장, 주문 처리, 포인트 차감, 알림 발송 기록 등에서 중요합니다.
작업 A 실행
  ↓
작업 B 실행
  ↓
작업 C 실행
  ↓
모두 성공 → commit
하나라도 실패 → rollback

➕ 1-1. Transaction이 필요한 이유

상담 상태는 바뀌었는데 이력이 안 남는 문제 방지
주문은 생성됐는데 결제 기록이 없는 문제 방지
재고는 줄었는데 주문 생성이 실패하는 문제 방지
관리자 상태 변경 중 일부만 반영되는 문제 방지
  • DB 작업이 하나일 때는 transaction이 크게 티 나지 않습니다.
  • 하지만 여러 테이블을 함께 수정하는 순간 transaction은 필수에 가까워집니다.

✅ 2. Transaction의 핵심 개념: Commit과 Rollback

  • Commit은 transaction 안의 변경사항을 최종 저장하는 것입니다.
  • Rollback은 transaction 안의 변경사항을 취소하는 것입니다.
BEGIN
  ↓
UPDATE consults
  ↓
INSERT consult_status_histories
  ↓
COMMIT

실패 시:

BEGIN
  ↓
UPDATE consults 성공
  ↓
INSERT consult_status_histories 실패
  ↓
ROLLBACK
  ↓
UPDATE consults도 취소

➕ 2-1. 상담 상태 변경 예시

관리자가 상담 상태를 NEW → CALLED로 변경

필요한 작업:
1. consults.status 업데이트
2. consult_status_histories 이력 추가

둘 중 하나만 성공하면 안 됨
  • 상태는 변경됐는데 이력이 없으면 나중에 누가 언제 바꿨는지 알 수 없습니다.
  • 반대로 이력만 있고 실제 상태가 안 바뀌어도 데이터가 꼬입니다.

✅ 3. ACID란 무엇인가?

  • Transaction은 보통 ACID라는 4가지 특성을 기준으로 설명합니다.
개념의미실무 예시
Atomicity원자성모두 성공하거나 모두 실패
Consistency일관성DB 규칙을 깨지 않음
Isolation격리성동시에 실행돼도 서로 꼬이지 않음
Durability지속성commit된 데이터는 유지

➕ 3-1. Atomicity

상태 변경 + 이력 저장
  ↓
둘 다 성공해야 함
  ↓
하나 실패하면 둘 다 취소

➕ 3-2. Consistency

없는 상품으로 상담 신청 불가
없는 관리자 ID로 상태 변경 이력 생성 불가
상태값은 정해진 enum 안에서만 저장

➕ 3-3. Isolation

관리자 A와 B가 동시에 같은 상담을 수정
  ↓
DB가 충돌을 어떻게 처리할지 필요

➕ 3-4. Durability

상담 신청 저장 완료
  ↓
서버가 재시작되어도 데이터는 DB에 남아야 함
  • 실무에서 가장 자주 체감하는 것은 Atomicity와 Isolation입니다.
  • 특히 동시에 같은 데이터를 수정할 때 문제가 많이 생깁니다.

✅ 4. Prisma에서 Transaction 사용하기

  • Prisma에서는 $transaction을 사용해 여러 DB 작업을 하나로 묶을 수 있습니다.

➕ 4-1. 배열 방식 transaction

await prisma.$transaction([
  prisma.consult.update({
    where: { id: consultId },
    data: { status: 'CALLED' },
  }),
  prisma.consultStatusHistory.create({
    data: {
      consultId,
      fromStatus: 'NEW',
      toStatus: 'CALLED',
      changedByAdminId: adminId,
    },
  }),
]);

➕ 4-2. Interactive transaction

await prisma.$transaction(async (tx) => {
  const consult = await tx.consult.findUniqueOrThrow({
    where: { id: consultId },
  });

  await tx.consult.update({
    where: { id: consultId },
    data: { status: 'CALLED' },
  });

  await tx.consultStatusHistory.create({
    data: {
      consultId,
      fromStatus: consult.status,
      toStatus: 'CALLED',
      changedByAdminId: adminId,
    },
  });
});

➕ 4-3. 배열 방식과 interactive 방식 차이

방식적합한 경우
배열 방식서로 의존성이 약한 여러 쿼리
Interactive이전 쿼리 결과를 다음 쿼리에 써야 할 때
  • 상태 변경처럼 기존 상태를 읽고, 그 값을 이력에 넣어야 한다면 interactive transaction이 더 적합합니다.

✅ 5. Transaction을 써야 하는 대표 상황

➕ 5-1. 상담 상태 변경

consults.status 업데이트
consult_status_histories 생성
audit_logs 생성

➕ 5-2. 상담 신청 생성

중복 신청 확인
consults 생성
lead_sources 연결
alimtalk_logs 생성 또는 발송 예약

➕ 5-3. 주문 생성

orders 생성
order_items 생성
customer_snapshot 저장
status_history 생성

➕ 5-4. 엑셀 ExportJob 생성

export_jobs 생성
검색 조건 저장
작업 상태 INIT 저장
audit_logs 생성

➕ 5-5. 관리자 권한 변경

admin_user_roles 변경
role_permissions 변경
audit_logs 생성
세션 무효화 기록
  • “A도 바뀌고 B도 반드시 같이 바뀌어야 한다”면 transaction 후보입니다.
  • 특히 상태값과 이력 테이블은 거의 항상 함께 봐야 합니다.

✅ 6. Transaction을 남용하면 생기는 문제

  • transaction은 강력하지만, 무조건 길게 잡으면 성능 문제가 생깁니다.
  • transaction이 길어질수록 lock을 오래 잡을 수 있고, 다른 요청이 기다릴 수 있습니다.
Transaction 시작
  ↓
DB row lock
  ↓
외부 API 호출 대기
  ↓
다른 요청 대기
  ↓
성능 저하 또는 timeout

➕ 6-1. Transaction 안에서 피해야 할 것

외부 API 호출
알림톡/SMS 실제 발송
긴 파일 처리
엑셀 생성
S3 업로드
사용자 입력 대기
너무 많은 row 대량 수정

➕ 6-2. 좋은 기준

DB 정합성에 필요한 최소 쿼리만 transaction 안에 둔다
외부 API 호출은 transaction 밖으로 분리한다
발송이 필요한 작업은 Queue/Job으로 넘긴다
transaction 시간을 짧게 유지한다
  • transaction은 짧고 명확해야 합니다.
  • DB 변경과 외부 연동을 한 덩어리로 묶는 것은 조심해야 합니다.

✅ 7. 상담 신청과 알림톡 발송의 transaction 기준

  • 상담 신청 저장과 알림톡 발송은 같이 일어나는 것처럼 보이지만 성격이 다릅니다.
  • 상담 신청 저장은 DB 정합성 문제이고, 알림톡 발송은 외부 API 문제입니다.

➕ 7-1. 위험한 구조

Transaction 시작
  ↓
consult 생성
  ↓
알림톡 API 호출
  ↓
alimtalk_logs 생성
  ↓
Transaction commit

문제:

알림톡 API가 느리면 transaction이 길어짐
알림톡 실패 때문에 상담 저장까지 실패할 수 있음
외부 API 장애가 DB 처리에 영향

➕ 7-2. 더 안전한 구조

Transaction 시작
  ↓
consult 생성
  ↓
notification_jobs 생성
  ↓
Transaction commit
  ↓
Worker가 알림톡 발송
  ↓
alimtalk_logs 업데이트
  • 고객 상담 신청은 먼저 안전하게 저장되어야 합니다.
  • 알림톡 발송은 실패해도 재시도 가능한 Job으로 분리하는 것이 좋습니다.

✅ 8. 동시성 문제란 무엇인가?

  • 동시성 문제는 여러 요청이 같은 데이터를 동시에 읽고 수정할 때 발생하는 문제입니다.
  • 웹서비스에서는 사용자가 많지 않아도 관리자 여러 명이 동시에 같은 상담을 수정하거나, 고객이 버튼을 여러 번 누르면 발생할 수 있습니다.
요청 A:
상담 상태 NEW 읽음

요청 B:
상담 상태 NEW 읽음

요청 A:
CALLED로 변경

요청 B:
CANCELED로 변경

결과:
마지막 요청이 이전 변경을 덮어씀

➕ 8-1. 실무에서 자주 발생하는 상황

상담 신청 버튼 중복 클릭
관리자 2명이 같은 상담 상태 변경
엑셀 다운로드 작업 중복 생성
같은 전화번호로 거의 동시에 신청
재고/예약 수량 동시 차감
중복 쿠폰/지원금 적용
  • 동시성 문제는 테스트에서 잘 안 보이다가 운영에서 갑자기 나타나는 경우가 많습니다.
  • “거의 동시에” 들어온 요청을 어떻게 처리할지 정해야 합니다.

✅ 9. Lost Update

  • Lost Update는 두 요청이 같은 데이터를 수정하면서 한쪽 변경이 사라지는 문제입니다.
초기 상태:
consult.status = NEW

관리자 A:
NEW를 읽고 CALLED로 변경

관리자 B:
NEW를 읽고 CANCELED로 변경

최종 상태:
CANCELED

문제:
A의 CALLED 변경이 사라짐

➕ 9-1. 해결 방법 후보

상태 변경 전 현재 상태 재확인
version 컬럼 사용
updatedAt 조건 사용
row lock 사용
상태 전이 규칙 적용

➕ 9-2. 상태 전이 조건 예시

await prisma.consult.updateMany({
  where: {
    id: consultId,
    status: 'NEW',
  },
  data: {
    status: 'CALLED',
  },
});
  • where에 현재 상태 조건을 넣으면 예상 상태일 때만 변경됩니다.
  • 변경된 row 수가 0이면 이미 다른 사람이 바꾼 것으로 판단할 수 있습니다.

✅ 10. Optimistic Lock

  • Optimistic Lock은 충돌이 자주 나지 않는다고 가정하고, 업데이트 시점에 버전이 맞는지 확인하는 방식입니다.
  • 관리자 화면에서 같은 상담을 동시에 수정할 가능성이 낮지만 없지는 않을 때 유용합니다.

➕ 10-1. version 컬럼

consults
  - id
  - status
  - version
  - updatedAt

➕ 10-2. 업데이트 흐름

1. 상담 상세 조회
   version = 3

2. 관리자가 상태 변경 요청
   id = 10
   version = 3

3. DB update 조건
   id = 10 AND version = 3

4. 성공 시
   version = 4

5. 실패 시
   이미 다른 사람이 수정함

➕ 10-3. Prisma 예시

const result = await prisma.consult.updateMany({
  where: {
    id: consultId,
    version: currentVersion,
  },
  data: {
    status: nextStatus,
    version: {
      increment: 1,
    },
  },
});

if (result.count === 0) {
  throw new ConflictException('이미 다른 관리자가 수정한 상담입니다.');
}
  • optimistic lock은 관리자 화면에서 사용자에게 “이미 수정된 데이터입니다. 새로고침 후 다시 시도해주세요.”라고 안내하기 좋습니다.

✅ 11. Pessimistic Lock

  • Pessimistic Lock은 충돌이 날 가능성이 있다고 보고, 먼저 row를 잠가 다른 transaction이 수정하지 못하게 하는 방식입니다.
  • PostgreSQL에서는 SELECT ... FOR UPDATE를 사용할 수 있습니다.

➕ 11-1. SQL 예시

BEGIN;

SELECT *
FROM consults
WHERE id = 10
FOR UPDATE;

UPDATE consults
SET status = 'CALLED'
WHERE id = 10;

COMMIT;

➕ 11-2. 특징

다른 transaction이 같은 row 수정을 기다림
데이터 충돌 방지에 강함
하지만 대기 시간이 생길 수 있음
lock을 오래 잡으면 성능 저하

➕ 11-3. 언제 쓸까?

재고 차감
좌석 예약
잔액 차감
정말 동시에 수정되면 안 되는 데이터
  • 상담 상태 변경 정도는 optimistic lock으로 충분한 경우가 많습니다.
  • 재고/예약/잔액처럼 숫자 차감이 중요한 경우 pessimistic lock을 더 진지하게 고려합니다.

✅ 12. Unique Constraint로 중복 방지하기

  • 중복 신청은 백엔드 로직으로만 막으면 동시에 들어온 요청에서 뚫릴 수 있습니다.
  • 최종 방어선은 DB unique constraint입니다.

➕ 12-1. 위험한 흐름

요청 A:
전화번호 중복 없음 확인

요청 B:
전화번호 중복 없음 확인

요청 A:
consult 생성

요청 B:
consult 생성

결과:
중복 데이터 2건 생성

➕ 12-2. DB unique 사용

CREATE UNIQUE INDEX uniq_consult_phone_product_date
ON consults (phone, product_id, request_date);

➕ 12-3. 주의

중복 기준을 먼저 정해야 함
전화번호만 unique면 재신청이 막힐 수 있음
날짜/상품/캠페인 기준을 고려해야 함
soft delete와 unique 충돌 주의
  • 중복 방지는 정책 문제입니다.
  • DB constraint는 정책이 정해진 뒤에 걸어야 합니다.

✅ 13. Upsert

  • Upsert는 있으면 업데이트하고, 없으면 생성하는 방식입니다.
  • 중복 생성 방지나 집계 데이터 갱신에 유용합니다.

➕ 13-1. Prisma upsert 예시

await prisma.leadSource.upsert({
  where: {
    source_campaign: {
      source: 'naver',
      campaign: 'galaxy-preorder',
    },
  },
  update: {
    consultCount: {
      increment: 1,
    },
  },
  create: {
    source: 'naver',
    campaign: 'galaxy-preorder',
    consultCount: 1,
  },
});

➕ 13-2. 적합한 경우

유입 소스별 일자 집계
방문자별 마지막 접속 정보
중복 가능성이 있는 설정값 저장
통계 row 생성/갱신

➕ 13-3. 주의

where 조건은 unique여야 함
업데이트와 생성의 의미가 명확해야 함
상담 신청 원본 데이터에는 남용하지 않기
  • upsert는 편리하지만, 원본 이벤트 데이터까지 덮어쓰면 이력이 사라질 수 있습니다.
  • 원본 로그성 데이터는 append-only가 더 안전한 경우가 많습니다.

✅ 14. Isolation Level

  • Isolation Level은 동시에 실행되는 transaction이 서로를 얼마나 격리할지 정하는 기준입니다.
  • 격리 수준이 높을수록 데이터 정합성은 강해지지만 성능 비용이 커질 수 있습니다.
수준특징
Read Committedcommit된 데이터만 읽음
Repeatable Readtransaction 안에서 같은 조회 결과 유지
Serializable가장 강한 격리, 직렬 실행처럼 보장

➕ 14-1. 실무 기준

대부분의 일반 API:
기본 isolation으로 충분한 경우 많음

정합성이 매우 중요한 작업:
격리 수준 조정 또는 lock 고려

중복/동시성 방지:
isolation만 믿지 말고 unique/version/lock 함께 고려

➕ 14-2. Prisma 예시

await prisma.$transaction(
  async (tx) => {
    // transaction 작업
  },
  {
    isolationLevel: 'Serializable',
  },
);
  • isolation level을 올리는 것은 만능 해결책이 아닙니다.
  • 먼저 unique constraint, 상태 조건, version 관리, lock 범위를 설계하는 것이 중요합니다.

✅ 15. Deadlock

  • Deadlock은 두 transaction이 서로가 잡고 있는 lock을 기다리면서 멈추는 상황입니다.
  • DB는 보통 deadlock을 감지하면 한 transaction을 실패시킵니다.
Transaction A:
consult 1 lock
  ↓
consult 2 lock 기다림

Transaction B:
consult 2 lock
  ↓
consult 1 lock 기다림

결과:
서로 기다림 → deadlock

➕ 15-1. Deadlock 줄이는 방법

항상 같은 순서로 row를 수정
transaction을 짧게 유지
불필요한 lock 줄이기
대량 업데이트를 작은 단위로 나누기
외부 API 호출을 transaction 밖으로 빼기

➕ 15-2. 실무 대응

deadlock 에러 로그 확인
재시도 가능한 작업인지 판단
수정 순서 통일
transaction 범위 축소
인덱스 누락으로 lock 범위가 커졌는지 확인
  • deadlock은 무조건 DB가 나쁜 것이 아니라 transaction 설계가 복잡할 때 생길 수 있습니다.
  • 발생 시 재시도 정책도 고려할 수 있습니다.

✅ 16. Idempotency란 무엇인가?

  • Idempotency는 같은 요청이 여러 번 들어와도 결과가 한 번 처리된 것과 같게 만드는 성질입니다.
  • 상담 신청 버튼 중복 클릭, 네트워크 재시도, 결제 webhook 중복 호출에서 중요합니다.
같은 요청 1번:
상담 신청 생성

같은 요청 2번:
새 상담을 또 만들지 않고 기존 결과 반환 또는 중복 처리

➕ 16-1. 필요한 상황

상담 신청 중복 클릭
결제 webhook 재시도
알림톡 발송 재시도
ExportJob 중복 실행
상태 변경 API 재요청

➕ 16-2. Idempotency Key

클라이언트 또는 서버가 요청 고유 key 생성
  ↓
DB에 처리 여부 저장
  ↓
같은 key 요청은 중복 처리하지 않음

➕ 16-3. 예시 테이블

idempotency_keys
  - id
  - key
  - requestHash
  - responseStatus
  - responseBody
  - createdAt
  • 모든 API에 idempotency가 필요한 것은 아닙니다.
  • 중복 실행되면 안 되는 API부터 적용하면 됩니다.

✅ 17. 상담 신청 중복 클릭 방지

  • 상담 신청은 프론트와 백엔드 양쪽에서 중복을 막아야 합니다.

➕ 17-1. 프론트 방어

제출 중 버튼 disabled
loading 상태 표시
중복 클릭 방지
성공 후 모달 닫기 또는 완료 화면 표시

➕ 17-2. 백엔드 방어

전화번호/상품/날짜 기준 중복 확인
unique constraint
idempotency key
ConflictException 409 반환

➕ 17-3. DB 방어

unique index
transaction
중복 요청 시 DB constraint error 처리
  • 프론트 방어만으로는 부족합니다.
  • 실제 중복 방지는 백엔드와 DB가 최종 책임을 가져야 합니다.

✅ 18. 상태 전이 규칙

  • 상태값은 아무 상태에서 아무 상태로 바뀌면 안 됩니다.
  • 예를 들어 이미 취소된 상담을 개통 완료로 바꾸는 것이 가능한지 정책이 필요합니다.

➕ 18-1. 상태 전이 예시

NEW
  ↓
CALLING
  ↓
CALLED
  ↓
CONVERTED

NEW
  ↓
CANCELED

CALLING
  ↓
PENDING

➕ 18-2. 금지 후보

CANCELED → CONVERTED
CONVERTED → NEW
DUPLICATED → CALLED

➕ 18-3. 백엔드 구현 예시

const allowedTransitions = {
  NEW: ['CALLING', 'CANCELED', 'DUPLICATED'],
  CALLING: ['CALLED', 'PENDING', 'CANCELED'],
  CALLED: ['CONVERTED', 'PENDING', 'CANCELED'],
  PENDING: ['CALLING', 'CANCELED'],
  CONVERTED: [],
  CANCELED: [],
  DUPLICATED: [],
} as const;
  • 상태 전이 규칙은 DB보다 백엔드 도메인 로직으로 관리하는 경우가 많습니다.
  • 다만 최종 상태 변경은 transaction과 이력 저장으로 묶어야 합니다.

✅ 19. 데이터 정합성이란 무엇인가?

  • 데이터 정합성은 데이터가 서로 모순되지 않고 일관된 상태를 유지하는 것입니다.
  • 예를 들어 상담 상태가 CALLED인데 상태 이력에는 NEW → CANCELED만 있다면 정합성이 깨진 것입니다.

➕ 19-1. 정합성 깨짐 예시

consults.status와 status_history 마지막 상태가 다름
orders.totalAmount와 order_items 합계가 다름
알림톡 발송 성공인데 상담 ID가 없음
상품이 삭제됐는데 상담에서 참조 중
관리자 상태 변경 로그에 관리자 ID가 없음

➕ 19-2. 정합성 유지 방법

transaction 사용
foreign key 사용
unique constraint 사용
상태 전이 규칙 사용
audit log 저장
정기 점검 쿼리 작성
  • 정합성은 코드만으로 지키기 어렵습니다.
  • DB 제약조건과 백엔드 로직을 함께 사용해야 합니다.

✅ 20. Audit Log와 Transaction

  • 관리자 주요 작업은 audit log로 남기는 것이 좋습니다.
  • 상태 변경, 상품 수정, 권한 변경, 엑셀 다운로드 같은 작업이 후보입니다.

➕ 20-1. Audit Log 예시

audit_logs
  - id
  - adminId
  - action
  - targetType
  - targetId
  - beforeValue
  - afterValue
  - ipAddress
  - createdAt

➕ 20-2. Transaction 안에 넣을까?

상태 변경과 audit log:
같은 transaction 권장

단순 조회 로그:
transaction 밖 또는 별도 로그로 충분

엑셀 다운로드 기록:
다운로드 요청 생성과 audit log는 함께 저장 가능

➕ 20-3. 기준

데이터 변경의 근거가 되는 audit log:
transaction 안에 포함

성능/분석용 이벤트 로그:
비동기 처리 가능
  • 중요한 변경 이력은 실제 변경과 함께 저장되어야 합니다.
  • 그래야 “변경됐는데 누가 바꿨는지 모르는 문제”를 막을 수 있습니다.

✅ 21. Queue/Worker와 데이터 정합성

  • 시간이 오래 걸리거나 실패할 수 있는 작업은 Queue/Worker로 분리하는 것이 좋습니다.
  • 하지만 Queue로 넘길 때도 DB 정합성을 고려해야 합니다.

➕ 21-1. ExportJob 예시

관리자 엑셀 다운로드 요청
  ↓
export_jobs row 생성
  ↓
Worker가 작업 처리
  ↓
S3 업로드
  ↓
export_jobs.status = DONE

➕ 21-2. Transaction 기준

API 요청 시:
export_jobs 생성은 transaction으로 저장

Worker 처리 시:
상태 PROCESSING → DONE/FAILED 변경
실패 사유 저장
재시도 횟수 저장

➕ 21-3. 주의

Job row 없이 Worker만 실행하지 않기
Worker 실패 시 상태 기록
재시도 중복 실행 방지
동일 Job이 동시에 처리되지 않게 lock 고려
  • Queue를 쓰면 API 응답은 빨라질 수 있지만, 작업 상태 관리가 필요해집니다.
  • Job 테이블은 운영자가 작업 상태를 확인할 수 있게 설계해야 합니다.

✅ 22. Transaction과 외부 API의 관계

  • DB transaction은 DB 내부 변경만 되돌릴 수 있습니다.
  • 외부 API 호출은 transaction rollback으로 되돌릴 수 없습니다.
Transaction 안에서 알림톡 발송
  ↓
알림톡 실제 발송 성공
  ↓
이후 DB 오류 발생
  ↓
DB rollback
  ↓
하지만 알림톡은 이미 발송됨

➕ 22-1. 해결 방향

DB에 발송 Job 저장
Transaction commit
Worker가 외부 API 호출
발송 결과를 DB에 기록
실패 시 재시도

➕ 22-2. Outbox Pattern 개념

비즈니스 데이터 저장
  ↓
같은 transaction 안에서 outbox event 저장
  ↓
commit
  ↓
worker가 outbox event 처리
  ↓
외부 API 호출
  • 외부 API는 DB transaction과 같은 방식으로 rollback되지 않습니다.
  • 그래서 외부 연동은 Job/Outbox로 분리하는 구조가 안전합니다.

✅ 23. 실무 체크리스트

➕ 23-1. Transaction 체크리스트

  1. 여러 테이블이 함께 변경되는가?
  2. 하나만 성공하면 데이터가 꼬이는가?
  3. 상태값과 이력을 함께 저장해야 하는가?
  4. audit log가 실제 변경과 함께 저장되어야 하는가?
  5. transaction 안에 외부 API 호출이 들어가 있지 않은가?
  6. transaction이 너무 길지 않은가?
  7. 실패 시 rollback되어야 하는 범위가 명확한가?
  8. Prisma $transaction 사용 방식이 적절한가?

➕ 23-2. 동시성 체크리스트

  1. 같은 상담을 여러 관리자가 동시에 수정할 수 있는가?
  2. 상담 신청 버튼 중복 클릭이 가능한가?
  3. 같은 전화번호 신청이 동시에 들어올 수 있는가?
  4. 상태 변경 시 현재 상태 조건을 확인하는가?
  5. version 또는 updatedAt 기반 optimistic lock이 필요한가?
  6. unique constraint로 최종 중복 방어가 되는가?
  7. deadlock 가능성이 있는 대량 수정이 있는가?
  8. 중복 요청에 409 Conflict를 반환하는가?

➕ 23-3. 데이터 정합성 체크리스트

  1. 현재 상태와 마지막 이력이 일치하는가?
  2. 주문/상담과 관련 snapshot이 필요한가?
  3. Foreign Key가 필요한 관계에 적용되어 있는가?
  4. 상태 전이 규칙이 문서화되어 있는가?
  5. audit log가 필요한 주요 작업이 정리되어 있는가?
  6. 외부 API 발송 실패가 원본 저장 실패로 이어지지 않는가?
  7. Queue/Worker 작업 상태가 DB에 기록되는가?
  8. 정합성 점검용 SQL을 만들 수 있는가?

✅ 24. AI에게 Transaction/동시성 설계를 점검시킬 때 좋은 질문법

NestJS + Prisma + PostgreSQL 기반 온라인 휴대폰 판매몰에서 상담 신청과 관리자 상태 변경 로직의 transaction/동시성 설계를 점검하고 싶어.

서비스 상황:
1. 고객은 상품 상세에서 상담 신청을 함
2. 같은 고객이 버튼을 여러 번 누를 수 있음
3. 같은 전화번호로 같은 상품에 당일 중복 신청이 들어올 수 있음
4. 관리자는 상담 목록에서 상태를 변경함
5. 상담 상태 변경 시 consults.status 업데이트와 consult_status_histories 생성이 필요함
6. 관리자 여러 명이 같은 상담을 동시에 수정할 수 있음
7. 상담 신청 후 알림톡 발송이 필요하지만 외부 API 실패로 상담 저장이 실패하면 안 됨
8. 엑셀 ExportJob과 알림톡 발송은 Worker로 분리할 수 있음

요청:
- transaction이 필요한 작업 범위
- Prisma `$transaction` 예시
- 상태 변경 이력 저장 구조
- 중복 신청 방지 기준
- optimistic lock 적용 후보
- unique constraint 후보
- 알림톡 발송을 transaction 밖으로 분리하는 구조
- Queue/Worker와 데이터 정합성 기준
- 409 Conflict 처리 기준
- 테스트해야 할 동시성 케이스
를 실무 기준으로 정리해줘.

➕ 24-1. AI 답변 검증 기준

transaction 안에 외부 API 호출을 넣지 않는가?
상태 변경과 이력 저장을 같은 transaction으로 묶는가?
중복 신청을 프론트 disabled만으로 끝내지 않는가?
DB unique constraint 필요성을 언급하는가?
관리자 동시 수정 문제를 고려하는가?
optimistic lock과 pessimistic lock 차이를 설명하는가?
Prisma에서 실제 구현 가능한 방식으로 설명하는가?
운영 DB에서 위험한 reset/수동 수정 제안을 하지 않는가?

📌 요약

  • Transaction은 여러 DB 작업을 하나의 단위로 묶어 모두 성공하면 commit하고, 하나라도 실패하면 rollback하는 기능입니다.
  • 상담 상태 변경처럼 consults.status 업데이트와 consult_status_histories 생성이 반드시 함께 일어나야 하는 작업은 transaction으로 묶는 것이 좋습니다.
  • Prisma에서는 $transaction을 사용하며, 기존 데이터를 읽고 다음 작업에 활용해야 한다면 interactive transaction 방식이 적합합니다.
  • Transaction 안에는 DB 정합성에 필요한 최소 작업만 넣고, 알림톡/SMS 발송, S3 업로드, 엑셀 생성 같은 외부/장기 작업은 밖으로 빼는 것이 안전합니다.
  • 동시성 문제는 여러 요청이 같은 데이터를 동시에 읽고 수정하면서 발생하며, 상담 신청 중복 클릭이나 관리자 동시 상태 변경에서 자주 생길 수 있습니다.
  • Lost Update를 막으려면 현재 상태 조건, version 컬럼, updatedAt 조건, optimistic lock, row lock 등을 상황에 맞게 사용할 수 있습니다.
  • 중복 신청은 프론트 버튼 disabled만으로 막을 수 없으며, 백엔드 로직과 DB unique constraint가 최종 방어선이 되어야 합니다.
  • 상태값은 아무 방향으로나 변경되면 안 되며, 상태 전이 규칙과 이력 저장 구조가 필요합니다.
  • 외부 API는 DB transaction으로 rollback할 수 없으므로, 알림톡/문자/웹훅 발송은 Job 또는 Outbox Pattern으로 분리하는 것이 안전합니다.
  • 데이터 정합성은 transaction, foreign key, unique constraint, 상태 전이 규칙, audit log, 정기 점검 쿼리를 함께 사용해야 지킬 수 있습니다.

0개의 댓글