좋지 않은 상황:
주문은 생성됨
주문 상세 저장 실패
재고 차감 실패
결제 이력 저장 실패
결과:
DB에 불완전한 주문 데이터가 남음
트랜잭션 적용:
주문 생성
주문 상세 생성
재고 차감
결제 이력 저장
하나라도 실패하면 전체 롤백
orders 테이블:
id=1, userId=10, status='PAID'
order_items 테이블:
orderId=1에 해당하는 상품 없음
1. 주문 생성
2. 주문 상품 생성
3. 재고 차감
4. 결제 대기 이력 생성
1. 상담 신청 저장
2. 유입 코드 저장
3. 중복 신청 여부 기록
4. 관리자 알림 생성
5. 문자 발송 이력 저장
1. 주문 상태 변경
2. 변경 이력 저장
3. 담당자 기록
4. 알림 발송 이력 저장
1. 사용자 포인트 차감
2. 포인트 사용 이력 생성
3. 주문 결제 금액 반영
| 항목 | 의미 |
|---|---|
| Atomicity | 원자성 |
| Consistency | 일관성 |
| Isolation | 격리성 |
| Durability | 지속성 |
주문 생성 성공
주문 상품 생성 실패
원자성 적용:
주문 생성도 함께 취소
재고는 음수가 되면 안 됨
주문에는 최소 1개 이상의 주문 상품이 있어야 함
사용자 포인트는 0보다 작아지면 안 됨
A 사용자가 마지막 재고 1개를 구매 중
B 사용자도 동시에 같은 상품 구매 시도
격리성이 부족하면:
둘 다 구매 성공 처리될 수 있음
모든 작업 성공
↓
commit
↓
DB에 반영
작업 중 에러 발생
↓
rollback
↓
트랜잭션 시작 전 상태로 복구
상담 신청 저장 성공
문자 발송 이력 저장 실패
↓
rollback
↓
상담 신청 저장도 취소
$transaction을 사용해서 여러 DB 작업을 하나의 트랜잭션으로 묶을 수 있습니다.await prisma.$transaction([
prisma.user.create({
data: {
email: 'test@example.com',
name: '홍길동',
},
}),
prisma.profile.create({
data: {
nickname: 'gildong',
},
}),
]);
await prisma.$transaction(async (tx) => {
const order = await tx.order.create({
data: {
userId: 1,
status: 'PENDING',
},
});
await tx.orderItem.create({
data: {
orderId: order.id,
productId: 10,
quantity: 1,
},
});
await tx.product.update({
where: {
id: 10,
},
data: {
stock: {
decrement: 1,
},
},
});
});
order.id를 다음 작업에서 사용할 수 있습니다.@Injectable()
export class ConsultService {
constructor(private readonly prisma: PrismaService) {}
async createConsult(dto: CreateConsultDto) {
return this.prisma.$transaction(async (tx) => {
const existingConsult = await tx.consult.findFirst({
where: {
phone: dto.phone,
},
});
if (existingConsult) {
throw new ConflictException('이미 신청된 전화번호입니다.');
}
const consult = await tx.consult.create({
data: {
name: dto.name,
phone: dto.phone,
productName: dto.productName,
status: 'PENDING',
},
});
await tx.consultHistory.create({
data: {
consultId: consult.id,
action: 'CREATED',
memo: '상담 신청 생성',
},
});
return consult;
});
}
}
await prisma.$transaction(async (tx) => {
const order = await tx.order.create({ data });
// 좋지 않은 예시
await sendSms();
await callExternalPaymentApi();
await uploadLargeFileToS3();
return order;
});
DB 트랜잭션:
rollback 가능
문자 발송 API:
이미 발송되면 rollback 불가
결제 API:
이미 승인되면 별도 취소 API 필요
S3 업로드:
이미 업로드되면 별도 삭제 필요
1. 주문 생성
2. 결제 승인 API 호출
3. DB 결제 이력 저장 실패
↓
보상 작업:
결제 취소 API 호출
1. S3 이미지 업로드
2. DB에 파일 메타데이터 저장 실패
↓
보상 작업:
S3에 업로드한 파일 삭제
1. 상담 신청 저장
2. 문자 발송 성공
3. 문자 발송 이력 저장 실패
문제:
문자는 이미 발송됨
대응:
실패 로그 저장
관리자 확인 대상 등록
재발 방지 로직 추가
사용자가 상담 신청 버튼 더블 클릭
↓
POST /api/consults 두 번 호출
↓
두 요청이 동시에 DB 중복 확인
↓
둘 다 "기존 신청 없음"으로 판단
↓
중복 저장 가능
findFirst 후 create만으로는 완전히 막기 어렵습니다.model Consult {
id Int @id @default(autoincrement())
phone String @unique
name String
status String
createdAt DateTime @default(now())
}
try {
return await this.prisma.consult.create({
data: {
name: dto.name,
phone: dto.phone,
status: 'PENDING',
},
});
} catch (error) {
if (error.code === 'P2002') {
throw new ConflictException('이미 신청된 전화번호입니다.');
}
throw error;
}
P2002는 unique constraint 실패에서 자주 볼 수 있는 에러입니다.상품 재고: 1개
A 사용자 구매 요청
B 사용자 구매 요청
둘 다 재고 1개 확인
둘 다 구매 성공
결과:
재고 -1 또는 초과 판매
사전예약 선착순 100명
동시에 200명이 신청
↓
신청 수 확인 후 저장 방식이면
100명을 초과해서 저장될 수 있음
const result = await prisma.product.updateMany({
where: {
id: productId,
stock: {
gt: 0,
},
},
data: {
stock: {
decrement: 1,
},
},
});
if (result.count === 0) {
throw new BadRequestException('재고가 부족합니다.');
}
stock > 0인 경우에만 차감합니다.| 격리 수준 | 설명 |
|---|---|
| Read Uncommitted | 커밋되지 않은 데이터도 읽을 수 있음 |
| Read Committed | 커밋된 데이터만 읽음 |
| Repeatable Read | 트랜잭션 중 같은 조회 결과 유지 |
| Serializable | 가장 강한 격리, 순차 실행에 가까움 |
Lock을 너무 오래 잡으면:
다른 요청들이 대기함
Lock 순서가 꼬이면:
데드락 발생 가능
모든 곳에 Lock을 쓰면:
성능 저하
문자 발송
이메일 발송
카카오 알림톡 발송
S3 대용량 파일 업로드
외부 결제 승인
외부 광고 API 호출
오래 걸리는 엑셀 생성
외부 작업은 실패 보상, 재시도, 별도 이력 테이블, Queue를 함께 고려하는 것이 좋습니다.
NestJS + Prisma에서 사전예약 신청 API를 만들고 있어.
요구사항:
1. 전화번호 기준 중복 신청은 막아야 함
2. 신청 정보 저장
3. 유입 코드 저장
4. 신청 상태 이력 저장
5. 관리자 알림 이력 저장
6. 카카오 알림톡 발송 필요
7. 사용자가 버튼을 여러 번 클릭할 수 있음
8. 동시 요청에서도 중복 저장이 되면 안 됨
질문:
- 어떤 작업을 트랜잭션 안에 넣어야 하는지
- 어떤 작업은 트랜잭션 밖에서 처리해야 하는지
- unique 제약 조건은 어디에 걸어야 하는지
- Prisma 코드 구조는 어떻게 잡으면 좋은지
설명해줘.
P2002 같은 중복 에러 처리를 설명하는가?$transaction을 사용해 트랜잭션을 처리할 수 있으며, 실무에서는 콜백 방식이 유용합니다.