TIL - 20260709

juni·2026년 7월 9일

TIL

목록 보기
398/470

0709 백엔드 실무 심화 (16/N): 동시성 제어, Lock과 중복 요청 방지


✅ 1. 동시성이란 무엇인가?

  • 동시성(Concurrency)은 여러 요청이나 작업이 거의 같은 시간에 동시에 실행되는 상황을 의미합니다.
  • 웹서비스에서는 여러 사용자가 동시에 상담 신청을 하거나, 관리자가 같은 주문을 수정하거나, Worker가 같은 Job을 처리하려고 할 때 동시성 문제가 발생할 수 있습니다.
  • 동시성 문제는 로컬에서 혼자 테스트할 때는 잘 안 보이다가, 운영에서 갑자기 데이터 꼬임으로 나타나는 경우가 많습니다.
사용자 A 요청
사용자 B 요청
사용자 C 요청
  ↓
거의 동시에 같은 API 호출
  ↓
같은 DB 데이터 조회/수정
  ↓
중복 저장 또는 잘못된 상태 발생 가능

➕ 1-1. 동시성 문제가 중요한 이유

  • 중복 상담 신청이 저장될 수 있습니다.
  • 같은 쿠폰이나 혜택이 중복 사용될 수 있습니다.
  • 재고가 0개인데 여러 주문이 성공할 수 있습니다.
  • 같은 알림톡이 여러 번 발송될 수 있습니다.
  • 같은 엑셀 Export Job이 중복 생성될 수 있습니다.
  • 관리자 두 명이 같은 주문 상태를 동시에 바꿔 이력이 꼬일 수 있습니다.
눈에 보이는 에러:
500 오류, API 실패

더 위험한 에러:
API는 성공했는데 DB 데이터가 조용히 꼬임
  • 동시성 문제는 “에러가 나는 문제”보다 “성공처럼 보이는데 데이터가 틀어지는 문제”라서 더 위험합니다.

✅ 2. 중복 요청이란 무엇인가?

  • 중복 요청은 같은 의도의 요청이 여러 번 들어오는 상황입니다.
  • 사용자가 버튼을 여러 번 누르거나, 네트워크 재시도, 브라우저 새로고침, Worker 재시도, Webhook 재전송 때문에 발생할 수 있습니다.

➕ 2-1. 중복 요청이 생기는 상황

사용자가 신청 버튼을 여러 번 클릭
모바일 네트워크가 불안정해서 요청 재시도
브라우저가 같은 요청을 다시 전송
Webhook이 같은 eventId로 재전송
Queue Worker가 실패 후 같은 Job 재시도
관리자가 저장 버튼을 여러 번 클릭

➕ 2-2. 실무 예시

고객이 상담 신청 버튼 클릭
  ↓
응답이 늦음
  ↓
버튼을 다시 클릭
  ↓
상담 신청 row가 2개 생성
  • 프론트엔드에서 버튼을 disabled 처리하는 것도 필요하지만, 백엔드에서 반드시 중복을 막아야 합니다.
  • 사용자는 API를 직접 호출할 수도 있고, 네트워크 재시도는 프론트 제어만으로 막기 어렵습니다.

✅ 3. 동시성 문제의 대표 유형

➕ 3-1. Lost Update

  • 두 요청이 같은 데이터를 읽고 각각 수정하면서, 한쪽 수정 결과가 사라지는 문제입니다.
관리자 A:
상담 상태 PENDING 조회

관리자 B:
상담 상태 PENDING 조회

관리자 A:
CALLING으로 변경

관리자 B:
CANCELLED로 변경

결과:
A의 변경이 B의 변경으로 덮어쓰기됨
  • 이력 없이 상태만 보면 누가 어떤 순서로 처리했는지 알기 어렵습니다.

➕ 3-2. Duplicate Insert

  • 같은 데이터가 중복 저장되는 문제입니다.
요청 A:
전화번호 01012345678 상담 신청 생성

요청 B:
거의 동시에 같은 전화번호 상담 신청 생성

결과:
중복 row 2개 생성
  • 코드에서 먼저 중복 조회를 하고 insert해도, 동시에 들어오면 둘 다 중복이 없다고 판단할 수 있습니다.
  • DB unique 제약이 필요합니다.

➕ 3-3. Overselling

  • 재고나 수량 제한이 있는 데이터에서 허용량보다 더 많이 처리되는 문제입니다.
남은 수량 1개
  ↓
사용자 A 주문 가능 판단
사용자 B 주문 가능 판단
  ↓
둘 다 주문 성공
  ↓
재고 -1 또는 초과 판매
  • 선착순 이벤트, 사전예약 수량 제한, 쿠폰 발급, 재고 관리에서 많이 발생합니다.

➕ 3-4. Double Execution

  • 같은 작업이 두 번 실행되는 문제입니다.
Worker가 알림톡 발송
  ↓
알림톡은 성공
  ↓
성공 로그 저장 전에 Worker 종료
  ↓
Job 재시도
  ↓
알림톡 중복 발송
  • Queue/Worker에서는 같은 Job이 여러 번 실행될 수 있다고 가정해야 합니다.

✅ 4. 프론트엔드 중복 방지와 백엔드 중복 방지

  • 중복 요청은 프론트엔드와 백엔드 양쪽에서 막아야 합니다.
  • 다만 보안과 데이터 정합성의 최종 책임은 백엔드에 있습니다.

➕ 4-1. 프론트엔드에서 할 수 있는 것

신청 버튼 클릭 후 disabled
로딩 상태 표시
응답 전 재클릭 방지
debounce/throttle 적용
중복 submit 방지
  • 사용자 경험을 개선하고 실수 클릭을 줄일 수 있습니다.
  • 하지만 이것만으로는 충분하지 않습니다.

➕ 4-2. 백엔드에서 반드시 해야 하는 것

DB unique 제약
Idempotency Key 확인
상태 전이 검증
트랜잭션 처리
조건부 update
Lock 적용
중복 Job 등록 방지
  • 백엔드는 같은 요청이 여러 번 와도 데이터가 꼬이지 않게 설계해야 합니다.

✅ 5. DB Unique 제약

  • 중복 저장을 막는 가장 기본적이고 강력한 방법은 DB unique 제약입니다.
  • 코드에서 중복 조회를 하는 것보다 DB 레벨에서 막는 것이 안전합니다.

➕ 5-1. 상담 신청 중복 방지 예시

model Consult {
  id        Int      @id @default(autoincrement())
  phone     String
  productId Int?
  eventId   Int?
  status    String
  createdAt DateTime @default(now())

  @@unique([phone, productId, eventId])
}
  • 같은 이벤트, 같은 상품, 같은 전화번호로 중복 신청을 막을 수 있습니다.
  • 비즈니스 기준에 맞게 unique 범위를 정해야 합니다.

➕ 5-2. P2002 처리 예시

try {
  return await this.prisma.consult.create({
    data: {
      phone: dto.phone,
      productId: dto.productId,
      eventId: dto.eventId,
      status: 'PENDING',
    },
  });
} catch (error) {
  if (error.code === 'P2002') {
    throw new ConflictException('이미 신청된 정보입니다.');
  }

  throw error;
}
  • Prisma에서 unique 제약 위반은 보통 P2002로 처리할 수 있습니다.
  • 사용자에게는 안전한 메시지를 보여주고, 서버 로그에는 필요한 정보만 남깁니다.

✅ 6. 중복 조회 후 생성 방식의 한계

  • 다음과 같은 방식은 혼자 테스트할 때는 정상처럼 보이지만, 동시에 요청이 들어오면 뚫릴 수 있습니다.
const existing = await this.prisma.consult.findFirst({
  where: {
    phone: dto.phone,
    productId: dto.productId,
  },
});

if (existing) {
  throw new ConflictException('이미 신청되었습니다.');
}

return this.prisma.consult.create({
  data: dto,
});

➕ 6-1. 왜 위험한가?

요청 A:
findFirst → 없음

요청 B:
findFirst → 없음

요청 A:
create 성공

요청 B:
create 성공

결과:
중복 생성
  • 조회와 생성 사이에 다른 요청이 끼어들 수 있습니다.
  • 그래서 중복 방지는 DB unique 제약과 함께 설계해야 합니다.

✅ 7. Idempotency Key

  • Idempotency Key는 같은 요청인지 구분하기 위한 고유 키입니다.
  • 같은 키로 요청이 여러 번 들어와도 결과가 한 번만 처리되게 만들 수 있습니다.
  • 결제, 주문, 알림 발송, Webhook, Export 생성처럼 중복 실행이 위험한 기능에 중요합니다.

➕ 7-1. Idempotency Key 예시

consult:create:phone:01012345678:product:15:event:3
payment:order:ORD-20260709-0001
notification:consult:123:CONSULT_CREATED
webhook:kakao:event:evt_12345
export:consults:admin:3:queryHash:abc123

➕ 7-2. 요청 Header 방식

POST /api/orders
Idempotency-Key: order-20260709-abc123
  • 클라이언트가 고유 키를 만들어 보내고, 서버가 이 키를 저장해 중복 처리를 막을 수 있습니다.
  • 결제 API에서 자주 쓰는 방식입니다.

✅ 8. Idempotency 테이블 설계

model IdempotencyKey {
  id          Int      @id @default(autoincrement())
  key         String   @unique
  status      String
  resourceType String?
  resourceId   Int?
  response    Json?
  errorMessage String?
  expiresAt   DateTime?
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  @@index([status, createdAt])
  @@index([expiresAt])
}

➕ 8-1. 주요 필드 설명

필드설명
key중복 요청을 구분하는 키
statusPROCESSING, SUCCESS, FAILED
resourceType생성된 리소스 종류
resourceId생성된 리소스 ID
response이전 성공 응답
expiresAt키 만료 시간
  • 같은 key로 이미 성공한 요청이 있으면 기존 결과를 반환할 수 있습니다.
  • 처리 중인 key가 있으면 중복 요청을 잠시 거절하거나 기존 처리 결과를 기다리게 할 수 있습니다.

✅ 9. Idempotency 처리 흐름

요청 수신
  ↓
Idempotency-Key 확인
  ↓
이미 SUCCESS면 기존 응답 반환
  ↓
PROCESSING이면 중복 요청으로 처리
  ↓
없으면 PROCESSING 저장
  ↓
실제 비즈니스 로직 실행
  ↓
성공 시 SUCCESS 저장
  ↓
응답 반환

➕ 9-1. 간단한 처리 예시

async createConsultWithIdempotency(dto: CreateConsultDto, key: string) {
  const existing = await this.prisma.idempotencyKey.findUnique({
    where: {
      key,
    },
  });

  if (existing?.status === 'SUCCESS') {
    return existing.response;
  }

  if (existing?.status === 'PROCESSING') {
    throw new ConflictException('이미 처리 중인 요청입니다.');
  }

  const idempotency = await this.prisma.idempotencyKey.create({
    data: {
      key,
      status: 'PROCESSING',
      expiresAt: dayjs().add(1, 'hour').toDate(),
    },
  });

  try {
    const consult = await this.prisma.consult.create({
      data: {
        phone: dto.phone,
        productId: dto.productId,
        status: 'PENDING',
      },
    });

    const response = {
      id: consult.id,
      status: consult.status,
    };

    await this.prisma.idempotencyKey.update({
      where: {
        id: idempotency.id,
      },
      data: {
        status: 'SUCCESS',
        resourceType: 'CONSULT',
        resourceId: consult.id,
        response,
      },
    });

    return response;
  } catch (error) {
    await this.prisma.idempotencyKey.update({
      where: {
        id: idempotency.id,
      },
      data: {
        status: 'FAILED',
        errorMessage: '요청 처리 실패',
      },
    });

    throw error;
  }
}
  • 실제 운영에서는 idempotencyKey 생성 자체도 unique 충돌을 고려해야 합니다.
  • 단순 조회 후 생성 방식은 동시에 들어온 요청에서 race condition이 생길 수 있으므로 주의해야 합니다.

✅ 10. 조건부 Update

  • 동시성 제어에서는 “조회 후 수정”보다 “조건을 포함한 update”가 더 안전한 경우가 많습니다.
  • 예를 들어 상태가 PENDING일 때만 DONE으로 바꾸는 식입니다.

➕ 10-1. 좋지 않은 방식

const consult = await this.prisma.consult.findUnique({
  where: {
    id: consultId,
  },
});

if (consult.status !== 'PENDING') {
  throw new BadRequestException('변경할 수 없습니다.');
}

await this.prisma.consult.update({
  where: {
    id: consultId,
  },
  data: {
    status: 'DONE',
  },
});
  • 조회와 업데이트 사이에 다른 요청이 상태를 바꿀 수 있습니다.

➕ 10-2. 조건부 updateMany 방식

const result = await this.prisma.consult.updateMany({
  where: {
    id: consultId,
    status: 'PENDING',
  },
  data: {
    status: 'DONE',
    completedAt: new Date(),
  },
});

if (result.count === 0) {
  throw new BadRequestException('이미 처리되었거나 변경할 수 없는 상태입니다.');
}
  • DB에서 조건과 업데이트를 한 번에 처리합니다.
  • 같은 상태 변경이 동시에 들어와도 하나만 성공하게 만들 수 있습니다.

✅ 11. Optimistic Lock

  • Optimistic Lock은 “충돌이 자주 나지 않을 것”이라고 가정하고, 수정 시점에 버전이 맞는지 확인하는 방식입니다.
  • 보통 version 컬럼이나 updatedAt을 사용합니다.

➕ 11-1. version 컬럼 예시

model Consult {
  id        Int      @id @default(autoincrement())
  status    String
  memo      String?
  version   Int      @default(1)
  updatedAt DateTime @updatedAt
}

➕ 11-2. 수정 흐름

관리자 A:
version=1 데이터 조회

관리자 B:
version=1 데이터 조회

관리자 A:
version=1 조건으로 수정 성공 → version=2

관리자 B:
version=1 조건으로 수정 시도
  ↓
실패
  ↓
이미 다른 관리자가 수정했다는 메시지 표시

➕ 11-3. Prisma 예시

const result = await this.prisma.consult.updateMany({
  where: {
    id: consultId,
    version: dto.version,
  },
  data: {
    memo: dto.memo,
    version: {
      increment: 1,
    },
  },
});

if (result.count === 0) {
  throw new ConflictException('다른 관리자가 먼저 수정했습니다. 새로고침 후 다시 시도해 주세요.');
}
  • 관리자 페이지에서 같은 데이터를 여러 명이 수정할 수 있다면 유용합니다.
  • 사용자에게 “이미 변경된 데이터”라는 안내를 줄 수 있습니다.

✅ 12. Pessimistic Lock

  • Pessimistic Lock은 “충돌이 발생할 가능성이 높다”고 보고, 데이터 수정 중 다른 요청이 접근하지 못하게 잠그는 방식입니다.
  • SQL에서는 SELECT ... FOR UPDATE 같은 방식이 있습니다.

➕ 12-1. 필요한 상황

재고 차감
쿠폰 선착순 발급
포인트 사용
정산 금액 변경
결제 상태 변경
  • 금전, 수량, 재고처럼 틀리면 큰 문제가 되는 데이터에 사용합니다.

➕ 12-2. 개념 흐름

트랜잭션 시작
  ↓
대상 row를 FOR UPDATE로 잠금
  ↓
현재 수량 확인
  ↓
수량 차감
  ↓
주문 생성
  ↓
트랜잭션 commit
  ↓
잠금 해제
  • 잠금 중에는 다른 트랜잭션이 해당 row를 수정하지 못하게 됩니다.
  • 하지만 Lock을 오래 잡으면 성능 저하와 대기 문제가 생길 수 있습니다.

✅ 13. Redis Lock

  • DB row가 아니라 작업 단위로 중복 실행을 막고 싶을 때 Redis Lock을 사용할 수 있습니다.
  • 배치 중복 실행, Export 중복 생성, 같은 리소스의 Worker 중복 처리 방지에 유용합니다.

➕ 13-1. Redis Lock 흐름

작업 시작
  ↓
Redis SET lockKey NX EX 60
  ↓
성공하면 작업 진행
  ↓
실패하면 이미 실행 중으로 판단
  ↓
작업 완료 후 lock 해제

➕ 13-2. Lock Key 예시

lock:consult:create:01012345678:product:15
lock:export:consults:admin:3:queryHash:abc123
lock:batch:daily-summary:2026-07-09
lock:notification:consult:123:created

➕ 13-3. 주의점

  • Lock에는 반드시 TTL을 설정해야 합니다.
  • 작업이 끝나면 Lock을 해제해야 합니다.
  • 내가 잡은 Lock만 해제해야 합니다.
  • 작업 시간이 Lock TTL보다 길어지면 중간에 Lock이 풀릴 수 있습니다.
TTL 없음:
작업 중 서버가 죽으면 Lock이 영원히 남음

TTL 너무 짧음:
작업 중 Lock이 풀려 중복 실행 가능

✅ 14. Redlock 개념

  • Redis Lock을 더 안전하게 운영하려면 Redlock 같은 분산 Lock 알고리즘을 사용할 수 있습니다.
  • 하지만 작은 서비스에서 처음부터 복잡하게 도입할 필요는 없습니다.

➕ 14-1. 1인 개발자 기준

초기:
DB unique 제약 + 조건부 update + 단순 Redis Lock

중요도가 높은 금전/재고:
DB transaction + row lock 검토

대규모 분산 환경:
Redlock 또는 전용 분산 Lock 검토
  • Lock은 만능 해결책이 아닙니다.
  • 가능하면 DB 제약과 트랜잭션으로 먼저 해결하고, 작업 단위 중복 방지에 Redis Lock을 보조로 쓰는 것이 좋습니다.

✅ 15. Rate Limit과 중복 요청 방지

  • 중복 요청과 과도한 요청을 줄이기 위해 Rate Limit을 적용할 수 있습니다.
  • 특히 상담 신청, 로그인, 인증번호 발송, 알림 재전송 API에 중요합니다.

➕ 15-1. 적용 대상

로그인 시도
인증번호 발송
상담 신청
사전예약 신청
엑셀 Export 요청
Webhook 수신
관리자 검색 API

➕ 15-2. 기준 예시

같은 IP에서 상담 신청:
1분에 5회 제한

같은 전화번호로 인증번호 발송:
3분에 1회 제한

관리자 엑셀 다운로드:
1분에 3회 제한

로그인 실패:
5회 실패 시 10분 제한
  • Rate Limit은 악의적 요청뿐 아니라 실수로 인한 반복 요청도 줄여줍니다.
  • Redis를 사용하면 IP, 전화번호, 계정 기준으로 제한을 걸 수 있습니다.

✅ 16. Queue Job 중복 방지

  • Queue 작업도 중복 등록과 중복 실행을 고려해야 합니다.
  • BullMQ에서는 jobId를 지정해 중복 등록을 줄일 수 있습니다.

➕ 16-1. 알림톡 Job 중복 방지

await this.notificationQueue.add(
  'send-consult-created-alimtalk',
  {
    consultId,
  },
  {
    jobId: `notification:consult:${consultId}:created`,
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 3000,
    },
  },
);

➕ 16-2. Export Job 중복 방지

같은 관리자
같은 검색 조건
같은 Export 타입
짧은 시간 내 중복 요청
  ↓
기존 PROCESSING ExportJob 반환
  • Export는 무거운 작업이므로 같은 조건의 요청이 여러 번 들어오면 기존 작업을 재사용하는 것이 좋습니다.

✅ 17. Webhook 중복 처리

  • Webhook은 외부 서비스가 같은 이벤트를 여러 번 보낼 수 있으므로 반드시 중복 처리가 필요합니다.
  • provider + eventId unique 제약을 사용하는 것이 좋습니다.
model WebhookEvent {
  id        Int      @id @default(autoincrement())
  provider  String
  eventId   String
  eventType String
  status    String
  payload   Json?
  createdAt DateTime @default(now())

  @@unique([provider, eventId])
}

➕ 17-1. 처리 기준

새 eventId:
WebhookEvent 생성 후 처리

이미 존재하는 eventId:
중복 이벤트로 판단
추가 상태 변경 없이 200 OK 반환
  • 중복 이벤트에 계속 에러를 반환하면 외부 서비스가 계속 재전송할 수 있습니다.
  • 이미 처리한 이벤트는 안전하게 200을 반환하는 방식이 좋습니다.

✅ 18. 상태 변경과 동시성

  • 상태 변경은 동시성 문제가 자주 발생합니다.
  • 관리자 두 명이 같은 상담이나 주문을 동시에 처리할 수 있기 때문입니다.

➕ 18-1. 위험한 상황

관리자 A:
상담을 DONE 처리

관리자 B:
동시에 상담을 CANCELLED 처리

결과:
최종 상태는 CANCELLED
하지만 고객에게 완료 알림톡이 이미 발송됐을 수 있음

➕ 18-2. 방어 방법

  1. 상태 전이 규칙을 둔다.
  2. 조건부 update를 사용한다.
  3. 상태 변경 이력을 남긴다.
  4. Optimistic Lock을 적용한다.
  5. 완료/취소 같은 종료 상태는 되돌리기 권한을 제한한다.
  6. 상태 변경 후 알림 발송은 멱등성 있게 처리한다.
상태 변경은 단순 update가 아니라
현재 상태 + 변경 가능 여부 + 권한 + 이력 + 후속 작업까지 함께 봐야 함

✅ 19. 동시성 테스트

  • 동시성 문제는 일반 수동 테스트로 발견하기 어렵습니다.
  • 같은 API를 동시에 여러 번 호출해봐야 합니다.

➕ 19-1. 간단한 테스트 방식

for i in {1..10}; do
  curl -X POST http://localhost:3000/api/consults \
    -H "Content-Type: application/json" \
    -d '{"phone":"01012345678","productId":15}' &
done

wait
  • 같은 전화번호로 동시에 10번 요청을 보냈을 때 중복 row가 생기지 않아야 합니다.
  • unique 제약, P2002 처리, 응답 코드가 정상인지 확인합니다.

➕ 19-2. 확인할 것

DB row가 1개만 생성되는가?
나머지 요청은 409 Conflict로 처리되는가?
서버가 500 에러를 반환하지 않는가?
로그에 민감정보가 노출되지 않는가?
알림톡 Job이 중복 등록되지 않는가?

✅ 20. 실무 체크리스트

➕ 20-1. 중복 요청 방지 체크리스트

  1. 프론트에서 버튼 중복 클릭을 막는가?
  2. 백엔드에서 DB unique 제약이 있는가?
  3. Prisma P2002를 409 Conflict로 처리하는가?
  4. 중복 조회 후 생성 방식만 믿고 있지 않은가?
  5. Idempotency Key가 필요한 API를 구분했는가?
  6. 같은 요청이 여러 번 와도 같은 결과를 반환할 수 있는가?
  7. 상담 신청, 사전예약, 주문 생성의 중복 기준이 명확한가?
  8. Queue Job 중복 등록을 막는가?

➕ 20-2. 동시성 제어 체크리스트

  1. 상태 변경에 조건부 update를 사용하는가?
  2. 관리자 동시 수정에 Optimistic Lock이 필요한가?
  3. 재고/쿠폰/포인트에는 트랜잭션과 Lock을 고려했는가?
  4. 배치 중복 실행에 Redis Lock을 사용하는가?
  5. Lock TTL이 설정되어 있는가?
  6. 내가 잡은 Lock만 해제하도록 설계했는가?
  7. Worker 작업은 멱등하게 설계되어 있는가?
  8. Webhook은 eventId unique로 중복 처리하는가?

➕ 20-3. 테스트 체크리스트

  1. 같은 API를 동시에 여러 번 호출해봤는가?
  2. 중복 row가 생성되지 않는가?
  3. 실패 요청이 500이 아니라 적절한 409/400으로 처리되는가?
  4. 상태 변경 동시 요청에서 한 요청만 성공하는가?
  5. Queue Job이 중복 등록되지 않는가?
  6. 같은 Webhook이 두 번 와도 상태가 한 번만 바뀌는가?
  7. 알림톡/SMS가 중복 발송되지 않는가?
  8. Export Job이 중복 생성되지 않는가?

✅ 21. AI를 활용해 동시성/중복 방지를 설계할 때 질문법

  • 동시성 문제는 단순히 “중복 방지 코드 짜줘”라고 하면 부족합니다.
  • 어떤 데이터가 중복되면 안 되는지, 동시에 몇 번 요청될 수 있는지, DB unique 기준과 상태 전이 기준을 함께 설명해야 합니다.

➕ 21-1. 좋은 질문 예시

NestJS + Prisma + PostgreSQL 서비스에서 상담 신청과 상태 변경의 중복 요청/동시성 문제를 막고 싶어.

상황:
1. 고객은 같은 이벤트에서 같은 전화번호로 한 번만 상담 신청할 수 있음
2. 사용자가 신청 버튼을 여러 번 누를 수 있음
3. 네트워크 문제로 같은 요청이 재시도될 수 있음
4. 상담 신청 성공 후 알림톡 Job을 Queue에 등록함
5. 관리자 두 명이 같은 상담 상태를 동시에 변경할 수 있음
6. 상태는 PENDING, CALLING, DONE, CANCELLED가 있음
7. DONE이나 CANCELLED 이후에는 일반 관리자가 다시 변경하면 안 됨
8. Webhook과 Worker는 같은 작업이 중복 실행될 수 있음

요청:
- DB unique 제약 설계
- Prisma P2002 처리
- Idempotency Key 적용 기준
- 조건부 update로 상태 변경하는 방법
- Optimistic Lock 적용 여부
- Queue Job 중복 방지
- Webhook 중복 처리
- 동시성 테스트 방법
을 실무 기준으로 정리해줘.

➕ 21-2. AI 답변 검증 기준

  1. 프론트 버튼 disabled만으로 해결하려 하지 않는가?
  2. DB unique 제약을 핵심 방어선으로 설명하는가?
  3. 조회 후 생성 방식의 race condition을 지적하는가?
  4. Prisma P2002를 409 Conflict로 처리하는가?
  5. 상태 변경에 조건부 update 또는 optimistic lock을 고려하는가?
  6. Worker/Queue/Webhook의 멱등성을 설명하는가?
  7. Redis Lock을 만능 해결책처럼 말하지 않는가?
  8. 동시 요청 테스트 방법을 포함하는가?

📌 요약

  • 동시성은 여러 요청이나 작업이 거의 같은 시간에 실행되는 상황이며, 운영 서비스에서는 중복 저장, 상태 꼬임, 재고 초과, 중복 발송 같은 문제를 만들 수 있습니다.
  • 중복 요청은 버튼 연타, 네트워크 재시도, Webhook 재전송, Worker 재시도 등 다양한 이유로 발생합니다.
  • 프론트엔드에서 버튼 disabled나 debounce를 적용하는 것은 도움이 되지만, 최종 방어선은 백엔드와 DB에 있어야 합니다.
  • 중복 저장 방지는 코드에서 조회 후 생성만으로는 부족하며, DB unique 제약과 Prisma P2002 처리가 필요합니다.
  • 결제, 주문, 알림, Webhook, Export처럼 중복 실행이 위험한 기능은 Idempotency Key로 같은 요청을 한 번만 처리하도록 설계할 수 있습니다.
  • 상태 변경은 조건부 update, 상태 전이 규칙, 변경 이력, 권한 검증을 함께 적용해야 안전합니다.
  • 관리자 동시 수정에는 Optimistic Lock이 유용하고, 재고/쿠폰/포인트처럼 충돌 위험이 큰 데이터에는 트랜잭션과 Lock을 검토해야 합니다.
  • Redis Lock은 배치 중복 실행이나 작업 단위 중복 방지에 유용하지만, TTL과 해제 조건을 잘못 설계하면 오히려 장애 원인이 될 수 있습니다.
  • Worker와 Webhook은 같은 작업이 여러 번 실행될 수 있다고 가정하고, Job ID, unique 제약, 성공 이력 확인으로 멱등성을 확보해야 합니다.

0개의 댓글