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
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 | 중복 요청을 구분하는 키 |
status | PROCESSING, 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. 방어 방법
- 상태 전이 규칙을 둔다.
- 조건부 update를 사용한다.
- 상태 변경 이력을 남긴다.
- Optimistic Lock을 적용한다.
- 완료/취소 같은 종료 상태는 되돌리기 권한을 제한한다.
- 상태 변경 후 알림 발송은 멱등성 있게 처리한다.
상태 변경은 단순 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. 중복 요청 방지 체크리스트
- 프론트에서 버튼 중복 클릭을 막는가?
- 백엔드에서 DB unique 제약이 있는가?
- Prisma P2002를 409 Conflict로 처리하는가?
- 중복 조회 후 생성 방식만 믿고 있지 않은가?
- Idempotency Key가 필요한 API를 구분했는가?
- 같은 요청이 여러 번 와도 같은 결과를 반환할 수 있는가?
- 상담 신청, 사전예약, 주문 생성의 중복 기준이 명확한가?
- Queue Job 중복 등록을 막는가?
➕ 20-2. 동시성 제어 체크리스트
- 상태 변경에 조건부 update를 사용하는가?
- 관리자 동시 수정에 Optimistic Lock이 필요한가?
- 재고/쿠폰/포인트에는 트랜잭션과 Lock을 고려했는가?
- 배치 중복 실행에 Redis Lock을 사용하는가?
- Lock TTL이 설정되어 있는가?
- 내가 잡은 Lock만 해제하도록 설계했는가?
- Worker 작업은 멱등하게 설계되어 있는가?
- Webhook은 eventId unique로 중복 처리하는가?
➕ 20-3. 테스트 체크리스트
- 같은 API를 동시에 여러 번 호출해봤는가?
- 중복 row가 생성되지 않는가?
- 실패 요청이 500이 아니라 적절한 409/400으로 처리되는가?
- 상태 변경 동시 요청에서 한 요청만 성공하는가?
- Queue Job이 중복 등록되지 않는가?
- 같은 Webhook이 두 번 와도 상태가 한 번만 바뀌는가?
- 알림톡/SMS가 중복 발송되지 않는가?
- 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 답변 검증 기준
- 프론트 버튼 disabled만으로 해결하려 하지 않는가?
- DB unique 제약을 핵심 방어선으로 설명하는가?
- 조회 후 생성 방식의 race condition을 지적하는가?
- Prisma P2002를 409 Conflict로 처리하는가?
- 상태 변경에 조건부 update 또는 optimistic lock을 고려하는가?
- Worker/Queue/Webhook의 멱등성을 설명하는가?
- Redis Lock을 만능 해결책처럼 말하지 않는가?
- 동시 요청 테스트 방법을 포함하는가?
📌 요약
- 동시성은 여러 요청이나 작업이 거의 같은 시간에 실행되는 상황이며, 운영 서비스에서는 중복 저장, 상태 꼬임, 재고 초과, 중복 발송 같은 문제를 만들 수 있습니다.
- 중복 요청은 버튼 연타, 네트워크 재시도, 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 제약, 성공 이력 확인으로 멱등성을 확보해야 합니다.