TIL - 20260620

juni·2026년 6월 20일

TIL

목록 보기
383/468

0620 백엔드 실무 심화 (1/N): 트랜잭션과 데이터 정합성


✅ 1. 트랜잭션이란 무엇인가?

  • 트랜잭션(Transaction)은 여러 개의 데이터베이스 작업을 하나의 작업 단위로 묶는 개념입니다.
  • 하나의 흐름 안에 있는 작업들이 모두 성공하면 DB에 반영하고, 중간에 하나라도 실패하면 전체를 되돌립니다.
  • 쉽게 말하면 “전부 성공하거나, 전부 실패해야 하는 작업 묶음”입니다.

➕ 1-1. 트랜잭션이 필요한 이유

  • 웹서비스에서는 하나의 요청 안에서 여러 테이블을 동시에 수정하는 경우가 많습니다.
  • 중간에 일부만 성공하고 일부는 실패하면 데이터가 꼬일 수 있습니다.
  • 이런 문제를 막기 위해 트랜잭션을 사용합니다.
좋지 않은 상황:
주문은 생성됨
주문 상세 저장 실패
재고 차감 실패
결제 이력 저장 실패

결과:
DB에 불완전한 주문 데이터가 남음
트랜잭션 적용:
주문 생성
주문 상세 생성
재고 차감
결제 이력 저장

하나라도 실패하면 전체 롤백

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

  • 데이터 정합성(Data Consistency)은 DB 안의 데이터가 서로 모순 없이 맞아떨어지는 상태를 의미합니다.
  • 예를 들어 주문 테이블에는 주문이 있는데 주문 상세 테이블에는 상품 정보가 없다면 정합성이 깨진 상태입니다.

➕ 2-1. 정합성이 깨지는 예시

orders 테이블:
id=1, userId=10, status='PAID'

order_items 테이블:
orderId=1에 해당하는 상품 없음
  • 주문은 결제 완료 상태인데 실제 주문 상품이 없는 이상한 데이터가 됩니다.
  • 이런 데이터가 쌓이면 관리자 페이지, 정산, 고객 응대, 통계가 모두 꼬일 수 있습니다.

➕ 2-2. 실무에서 정합성이 중요한 이유

  • 상담 신청 중복 방지
  • 주문/결제 상태 관리
  • 재고 차감
  • 포인트 적립/사용
  • 관리자 상태 변경 이력
  • 사전예약 신청 데이터 관리
  • 엑셀 다운로드 기준 데이터 유지
  • 광고 유입 코드와 신청 데이터 연결

✅ 3. 트랜잭션이 필요한 대표 상황

➕ 3-1. 주문 생성

1. 주문 생성
2. 주문 상품 생성
3. 재고 차감
4. 결제 대기 이력 생성
  • 주문만 생성되고 상품 정보가 저장되지 않으면 안 됩니다.
  • 재고 차감만 되고 주문이 실패해도 안 됩니다.

➕ 3-2. 상담 신청 등록

1. 상담 신청 저장
2. 유입 코드 저장
3. 중복 신청 여부 기록
4. 관리자 알림 생성
5. 문자 발송 이력 저장
  • 상담 신청은 저장됐는데 유입 정보가 빠지면 광고 성과 분석이 어려워집니다.
  • 문자 발송 이력은 있는데 실제 신청 데이터가 없으면 운영자가 혼란스러워집니다.

➕ 3-3. 관리자 상태 변경

1. 주문 상태 변경
2. 변경 이력 저장
3. 담당자 기록
4. 알림 발송 이력 저장
  • 주문 상태만 바뀌고 변경 이력이 없으면 누가 언제 바꿨는지 알 수 없습니다.
  • 운영/CS/정산이 있는 서비스에서는 이력 저장이 매우 중요합니다.

➕ 3-4. 포인트 사용

1. 사용자 포인트 차감
2. 포인트 사용 이력 생성
3. 주문 결제 금액 반영
  • 포인트만 차감되고 주문에 반영되지 않으면 고객 민원이 생깁니다.
  • 주문은 할인됐는데 포인트가 차감되지 않으면 금전적 손실이 생깁니다.

✅ 4. ACID

  • 트랜잭션은 보통 ACID라는 4가지 특성을 기준으로 설명합니다.
항목의미
Atomicity원자성
Consistency일관성
Isolation격리성
Durability지속성

➕ 4-1. Atomicity, 원자성

  • 트랜잭션 안의 작업은 모두 성공하거나 모두 실패해야 합니다.
주문 생성 성공
주문 상품 생성 실패

원자성 적용:
주문 생성도 함께 취소

➕ 4-2. Consistency, 일관성

  • 트랜잭션 전후로 데이터는 정해진 규칙을 만족해야 합니다.
재고는 음수가 되면 안 됨
주문에는 최소 1개 이상의 주문 상품이 있어야 함
사용자 포인트는 0보다 작아지면 안 됨

➕ 4-3. Isolation, 격리성

  • 여러 트랜잭션이 동시에 실행되어도 서로의 중간 상태를 함부로 보지 못해야 합니다.
A 사용자가 마지막 재고 1개를 구매 중
B 사용자도 동시에 같은 상품 구매 시도

격리성이 부족하면:
둘 다 구매 성공 처리될 수 있음

➕ 4-4. Durability, 지속성

  • 트랜잭션이 성공적으로 완료되면 그 결과는 DB에 안전하게 저장되어야 합니다.
  • 서버가 재시작되어도 커밋된 데이터는 유지되어야 합니다.

✅ 5. Commit과 Rollback

➕ 5-1. Commit

  • Commit은 트랜잭션 안의 작업을 최종적으로 DB에 반영하는 것입니다.
모든 작업 성공
  ↓
commit
  ↓
DB에 반영

➕ 5-2. Rollback

  • Rollback은 트랜잭션 안에서 발생한 변경을 되돌리는 것입니다.
작업 중 에러 발생
  ↓
rollback
  ↓
트랜잭션 시작 전 상태로 복구

➕ 5-3. 실무 예시

상담 신청 저장 성공
문자 발송 이력 저장 실패
  ↓
rollback
  ↓
상담 신청 저장도 취소
  • 단, 문자 발송처럼 외부 API가 이미 실행된 경우는 DB 롤백만으로 모든 것이 되돌아가지 않을 수 있습니다.
  • 그래서 외부 API와 트랜잭션을 함께 다룰 때는 더 조심해야 합니다.

✅ 6. Prisma 트랜잭션 기본

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

➕ 6-1. 배열 방식 트랜잭션

await prisma.$transaction([
  prisma.user.create({
    data: {
      email: 'test@example.com',
      name: '홍길동',
    },
  }),
  prisma.profile.create({
    data: {
      nickname: 'gildong',
    },
  }),
]);
  • 간단한 여러 작업을 한 번에 묶을 때 사용할 수 있습니다.
  • 하지만 앞 작업의 결과를 다음 작업에서 사용해야 하는 경우에는 적합하지 않을 수 있습니다.

➕ 6-2. 콜백 방식 트랜잭션

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를 다음 작업에서 사용할 수 있습니다.
  • 실무에서는 콜백 방식이 더 자주 사용됩니다.

✅ 7. NestJS Service에서 트랜잭션 사용하기

  • NestJS에서는 보통 Service 계층에서 트랜잭션을 처리합니다.
  • Controller는 요청과 응답을 담당하고, 실제 비즈니스 흐름은 Service에서 관리하는 것이 좋습니다.

➕ 7-1. 상담 신청 트랜잭션 예시

@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;
    });
  }
}
  • 중복 신청 확인, 신청 저장, 이력 저장을 하나의 트랜잭션으로 묶었습니다.
  • 이력 저장이 실패하면 상담 신청 저장도 롤백됩니다.

✅ 8. 트랜잭션 안에서 주의해야 할 것

➕ 8-1. 트랜잭션 안에서 너무 오래 걸리는 작업 금지

  • 트랜잭션은 DB 연결을 잡고 있는 상태입니다.
  • 트랜잭션 안에서 오래 걸리는 작업을 하면 다른 요청이 지연될 수 있습니다.
await prisma.$transaction(async (tx) => {
  const order = await tx.order.create({ data });

  // 좋지 않은 예시
  await sendSms(); 
  await callExternalPaymentApi();
  await uploadLargeFileToS3();

  return order;
});
  • 외부 API 호출, 문자 발송, 파일 업로드 같은 작업은 트랜잭션 안에 넣는 것을 신중하게 판단해야 합니다.

➕ 8-2. 외부 API는 DB처럼 롤백되지 않음

DB 트랜잭션:
rollback 가능

문자 발송 API:
이미 발송되면 rollback 불가

결제 API:
이미 승인되면 별도 취소 API 필요

S3 업로드:
이미 업로드되면 별도 삭제 필요
  • 외부 시스템은 DB 트랜잭션과 함께 자동 롤백되지 않습니다.
  • 그래서 외부 API가 포함된 작업은 실패 보상 전략이 필요합니다.

✅ 9. 보상 트랜잭션

  • 보상 트랜잭션(Compensating Transaction)은 이미 실행된 외부 작업을 되돌리기 위한 별도 작업입니다.
  • DB 롤백처럼 자동으로 되돌리는 것이 아니라, 실패했을 때 반대 작업을 직접 수행합니다.

➕ 9-1. 결제 예시

1. 주문 생성
2. 결제 승인 API 호출
3. DB 결제 이력 저장 실패
  ↓
보상 작업:
결제 취소 API 호출

➕ 9-2. S3 업로드 예시

1. S3 이미지 업로드
2. DB에 파일 메타데이터 저장 실패
  ↓
보상 작업:
S3에 업로드한 파일 삭제

➕ 9-3. 문자 발송 예시

1. 상담 신청 저장
2. 문자 발송 성공
3. 문자 발송 이력 저장 실패

문제:
문자는 이미 발송됨

대응:
실패 로그 저장
관리자 확인 대상 등록
재발 방지 로직 추가
  • 모든 외부 작업을 완벽히 되돌릴 수 있는 것은 아닙니다.
  • 그래서 외부 API는 실패 이력, 재시도, 관리자 확인 플래그를 함께 설계해야 합니다.

✅ 10. 중복 요청과 트랜잭션

  • 사용자가 버튼을 여러 번 클릭하거나, 네트워크 재시도로 같은 API가 여러 번 호출될 수 있습니다.
  • 이때 트랜잭션만으로는 중복 데이터를 완전히 막기 어려울 수 있습니다.

➕ 10-1. 중복 신청 예시

사용자가 상담 신청 버튼 더블 클릭
  ↓
POST /api/consults 두 번 호출
  ↓
두 요청이 동시에 DB 중복 확인
  ↓
둘 다 "기존 신청 없음"으로 판단
  ↓
중복 저장 가능
  • 이 문제는 단순히 findFirst 후 create만으로는 완전히 막기 어렵습니다.
  • DB 레벨의 unique 제약 조건이 필요합니다.

✅ 11. Unique 제약 조건

  • 중복을 반드시 막아야 하는 값은 DB에 unique 제약을 걸어야 합니다.
  • 프론트엔드 검증이나 백엔드 조건문만 믿으면 동시 요청에서 뚫릴 수 있습니다.

➕ 11-1. Prisma unique 예시

model Consult {
  id        Int      @id @default(autoincrement())
  phone    String   @unique
  name     String
  status   String
  createdAt DateTime @default(now())
}

➕ 11-2. 중복 에러 처리

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;
}
  • Prisma의 P2002는 unique constraint 실패에서 자주 볼 수 있는 에러입니다.
  • 사용자에게 DB 에러를 그대로 보여주지 말고, 이해 가능한 메시지로 바꿔야 합니다.

✅ 12. 동시성 문제

  • 동시성 문제는 여러 요청이 거의 동시에 같은 데이터를 수정할 때 발생합니다.
  • 재고, 포인트, 쿠폰, 선착순 신청, 사전예약 이벤트에서 자주 문제가 됩니다.

➕ 12-1. 재고 차감 문제

상품 재고: 1개

A 사용자 구매 요청
B 사용자 구매 요청

둘 다 재고 1개 확인
둘 다 구매 성공
결과:
재고 -1 또는 초과 판매

➕ 12-2. 선착순 이벤트 문제

사전예약 선착순 100명

동시에 200명이 신청
  ↓
신청 수 확인 후 저장 방식이면
100명을 초과해서 저장될 수 있음
  • 이런 경우에는 트랜잭션, unique 제약, 조건부 update, lock, queue 등을 함께 고려해야 합니다.

✅ 13. 조건부 업데이트

  • 조건부 업데이트는 특정 조건을 만족할 때만 데이터를 변경하는 방식입니다.
  • 재고 차감에서 자주 사용됩니다.

➕ 13-1. 재고 차감 예시

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인 경우에만 차감합니다.
  • 업데이트된 row가 0개라면 재고 부족으로 처리할 수 있습니다.
  • 단순히 먼저 조회하고 나중에 차감하는 것보다 안전합니다.

✅ 14. 트랜잭션 격리 수준

  • DB는 트랜잭션들이 서로 영향을 주는 방식을 제어하기 위해 격리 수준을 제공합니다.
  • 격리 수준이 높을수록 정합성은 강해지지만 성능에 부담이 생길 수 있습니다.

➕ 14-1. 대표 격리 수준

격리 수준설명
Read Uncommitted커밋되지 않은 데이터도 읽을 수 있음
Read Committed커밋된 데이터만 읽음
Repeatable Read트랜잭션 중 같은 조회 결과 유지
Serializable가장 강한 격리, 순차 실행에 가까움
  • 실무에서는 DB 종류와 기본 격리 수준을 이해해야 합니다.
  • 대부분의 일반 CRUD는 기본 격리 수준으로 충분하지만, 재고/쿠폰/선착순 같은 기능은 추가 고려가 필요합니다.

✅ 15. 트랜잭션과 Lock

  • Lock은 여러 트랜잭션이 같은 데이터를 동시에 수정하지 못하도록 잠그는 기능입니다.
  • 정합성을 높일 수 있지만, 잘못 사용하면 성능 저하나 데드락이 발생할 수 있습니다.

➕ 15-1. Lock이 필요한 상황

  • 재고 차감
  • 포인트 사용
  • 쿠폰 발급
  • 선착순 이벤트
  • 정산 처리
  • 동일 주문 상태 변경

➕ 15-2. 주의점

Lock을 너무 오래 잡으면:
다른 요청들이 대기함

Lock 순서가 꼬이면:
데드락 발생 가능

모든 곳에 Lock을 쓰면:
성능 저하
  • Lock은 정합성이 매우 중요한 구간에 제한적으로 사용해야 합니다.
  • 단순 조회나 일반 목록 API에 무분별하게 쓰면 안 됩니다.

✅ 16. 트랜잭션 설계 기준

➕ 16-1. 하나의 트랜잭션에 넣어야 하는 것

  • 같은 비즈니스 흐름에서 반드시 함께 성공해야 하는 DB 작업
  • 주문 생성과 주문 상세 생성
  • 상태 변경과 변경 이력 저장
  • 포인트 차감과 포인트 이력 저장
  • 상담 신청 생성과 신청 이력 저장
  • 결제 이력 생성과 주문 상태 변경

➕ 16-2. 트랜잭션 밖으로 빼는 것을 고려할 것

  • 문자 발송

  • 이메일 발송

  • 카카오 알림톡 발송

  • S3 대용량 파일 업로드

  • 외부 결제 승인

  • 외부 광고 API 호출

  • 오래 걸리는 엑셀 생성

  • 외부 작업은 실패 보상, 재시도, 별도 이력 테이블, Queue를 함께 고려하는 것이 좋습니다.


✅ 17. 실무 체크리스트

➕ 17-1. 트랜잭션 적용 체크리스트

  1. 여러 DB 작업이 반드시 함께 성공해야 하는가?
  2. 중간에 실패하면 앞 작업을 되돌려야 하는가?
  3. 상태 변경과 이력 저장이 함께 처리되는가?
  4. 주문/결제/포인트/재고처럼 금전적 영향이 있는가?
  5. 외부 API 호출이 트랜잭션 안에 들어가 있지는 않은가?
  6. 트랜잭션 시간이 너무 길어지지 않는가?
  7. 에러 발생 시 사용자 메시지와 서버 로그가 분리되어 있는가?

➕ 17-2. 데이터 정합성 체크리스트

  1. 중복을 막아야 하는 컬럼에 unique 제약이 있는가?
  2. 외래키 관계가 필요한 곳에 설정되어 있는가?
  3. 상태값이 무분별한 문자열로 흩어져 있지 않은가?
  4. 변경 이력이 필요한 기능에 history 테이블이 있는가?
  5. 삭제 시 연관 데이터 처리를 고려했는가?
  6. 관리자 수동 수정으로 데이터가 꼬일 가능성은 없는가?
  7. 엑셀 다운로드 기준 데이터가 실제 DB 상태와 일치하는가?

➕ 17-3. 동시성 체크리스트

  1. 같은 요청이 동시에 여러 번 들어올 수 있는가?
  2. 더블 클릭이나 재시도로 중복 저장될 수 있는가?
  3. 재고, 쿠폰, 선착순처럼 수량 제한이 있는가?
  4. 단순 조회 후 저장 방식으로 경쟁 조건이 생기지 않는가?
  5. 조건부 update 또는 unique 제약을 사용했는가?
  6. 트랜잭션 격리 수준이나 Lock이 필요한 구간인가?

✅ 18. AI를 활용해 트랜잭션을 설계할 때 질문법

  • 트랜잭션은 단순히 “코드에 transaction 넣어줘”라고 하면 안 됩니다.
  • 어떤 작업이 함께 성공해야 하는지, 외부 API가 있는지, 중복 요청이 가능한지 알려줘야 합니다.

➕ 18-1. 좋은 질문 예시

NestJS + Prisma에서 사전예약 신청 API를 만들고 있어.

요구사항:
1. 전화번호 기준 중복 신청은 막아야 함
2. 신청 정보 저장
3. 유입 코드 저장
4. 신청 상태 이력 저장
5. 관리자 알림 이력 저장
6. 카카오 알림톡 발송 필요
7. 사용자가 버튼을 여러 번 클릭할 수 있음
8. 동시 요청에서도 중복 저장이 되면 안 됨

질문:
- 어떤 작업을 트랜잭션 안에 넣어야 하는지
- 어떤 작업은 트랜잭션 밖에서 처리해야 하는지
- unique 제약 조건은 어디에 걸어야 하는지
- Prisma 코드 구조는 어떻게 잡으면 좋은지
설명해줘.

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

  1. 모든 작업을 무조건 트랜잭션 안에 넣지 않는가?
  2. 외부 알림톡 발송은 DB처럼 rollback되지 않는다고 설명하는가?
  3. 전화번호 중복 방지를 DB unique 제약으로 처리하는가?
  4. Prisma P2002 같은 중복 에러 처리를 설명하는가?
  5. 상태 변경과 이력 저장을 함께 고려하는가?
  6. 동시 요청과 더블 클릭 문제를 고려하는가?
  7. 실패 이력, 재시도, Queue 가능성을 언급하는가?

📌 요약

  • 트랜잭션은 여러 DB 작업을 하나의 작업 단위로 묶어 모두 성공하거나 모두 실패하게 만드는 개념입니다.
  • 데이터 정합성은 DB 안의 데이터가 서로 모순 없이 맞아떨어지는 상태를 의미합니다.
  • 주문, 결제, 포인트, 재고, 상담 신청, 관리자 상태 변경처럼 여러 테이블이 함께 바뀌는 기능에는 트랜잭션이 중요합니다.
  • Prisma에서는 $transaction을 사용해 트랜잭션을 처리할 수 있으며, 실무에서는 콜백 방식이 유용합니다.
  • 외부 API, 문자 발송, S3 업로드, 결제 승인 같은 작업은 DB 트랜잭션처럼 자동 rollback되지 않으므로 보상 작업이나 실패 이력 관리가 필요합니다.
  • 중복 요청과 동시성 문제는 백엔드 조건문만으로 부족할 수 있으며, DB unique 제약, 조건부 update, Lock, Queue 등을 함께 고려해야 합니다.
  • 트랜잭션은 정합성을 지키는 강력한 도구지만, 너무 오래 잡거나 외부 작업을 무리하게 넣으면 성능과 장애 위험이 커질 수 있습니다.

0개의 댓글