중복 Race 처리

임도현·2026년 7월 9일
post-thumbnail

Race Condition ?

운영체제에서 race condition이라는 상황에 대해서 들어보았다.

race condition이란

둘 이상의 프로세스(또는 스레드)가 동시에 공유 자원(Shared Resource)에 접근하여 데이터를 수정할 때, 실행 순서(interleaving)에 따라 결과가 달라지는 현상

본래 서버에 어떤 요청이 랜덤하게 들어와도 그 결과는 같아야하는데 , 어떤 요청의 순서에 따라서 그 결과값이 달라진다면 , 그것은 서버의 Integrity , 완전함을 해치는 요소라서 클라이언트에게 모두 동일한 상태의 서버를 제공해 줄 수가 없는 것입니다.

중복 Race ?

Race Condition 중에서 발생할 수 있는 흔한 경우 중 하나가 바로 중복 Race이다.

동일한 생성 요청이 동시에 혹은 굉장히 짧은 시간내에 두개 이상의 요청이 들어올때, 중복 생성되는 경우를 중복 Race라고 합니다.

🔨 해결책

DB Unique Index 추가

가장 근본적인 해결책입니다.

DB에서 근본적으로 동일한 값을 넣지 못하도록 제약을 걸어두는 방식입니다.

model User {

  id    Int    @id @default(autoincrement())

  email String @unique

}

Unique Constraint, Unique Index 설정을 통해 DB 상에서 중복 생성을 막는 방식입니다.

Prisma Interactive Transaction 방식

try {
  return await prisma.$transaction(async (tx) => {
    const exists = await tx.user.findUnique({ where: { email } });

    if (exists) {
      throw new ConflictException('이미 존재하는 이메일입니다.');
    }

    return tx.user.create({
      data: { email },
    });
  });
} catch (e) {
  if (e.code === 'P2002') {
    throw new ConflictException('이미 존재하는 이메일입니다.');
  }

  throw e;
}

Prisma Interactive Transaction 방식을 사용해서,

async function을 넘기면, 그 함수 안에서 tx로 실행한 모든 Prisma 쿼리가 하나의 트랜잭션에 포함되는 방식입니다.

이 방식을 사용하게 되면 이미 존재하는 데이터의 경우 에러를 던지며, Prisma가 자동으로 rollback하는 기능이 추가되어, 중복 처리를 할 수 있다는 장점이 있습니다.

하나의 쿼리문이므로 결국 INSERT는 실행되지 않습니다.

분산 Lock , Redis Lock 추가

동시에 같은 요청 , 생성 요청 등 중복 Race가 발생하게 된다면, Redis가 먼저 그 요청을 확인 , 자원에 대한 Lock을 Redis가 구현하게 되는 방식입니다.

Redis는 메모리에 존재하기 때문에 DB보다 더 사용성이 높고 , 더 빠르게 접근 가능하기 때문에 더 반응성이 좋게 반응 할 수 있다.

const lockKey = `lock:user:${email}`;
const lockValue = crypto.randomUUID();

const acquired = await redis.set(lockKey, lockValue, 'NX', 'EX', 5); 
// Lock을 생성 및 자원 점유 ! 

if (!acquired) {
  throw new ConflictException('이미 처리 중인 요청입니다.');
  //redis에서 이미 처리중인 요청인지 확인
}

try {
  // 중복되면 안 되는 작업
} finally {
  // 내가 잡은 락일 때만 삭제
}

하지만 결국 중요한것은 Redis Lock 은 보조적인 수단이며 ,

근본적인 해결책은 DB상에서의 Unique Index, Constraint 를 걸어놔야지만 된다는것.

Redis Lock이 근본적인 해결책은 아니라는것을 명심해야한다!

가장 근본적이며 많이 채택하는 구조는

Client
   │
   ▼
Redis Lock 획득
   │
   ▼
Prisma Transaction 시작
   │
   ├── SELECT
   ├── INSERT
   ├── UPDATE
   └── ...
   │
Commit / Rollback
   │
   ▼
Redis Unlock

마무리

언제나 이 Transaction 관리가 백엔드 개발자에게 가장 중요한 덕목인것같다.

서버를 안정화 시키며 무수한 요청에 따라서 서버가 바뀌는것이 아니라, 서버는 항상 일정한 상태를 유지하는 것

그것에 대한 고민을 계속해서 해야하는것이 잘하는 개발자의 덕목중에 하나인것같다.

profile
성장을 즐기는 사람 🧍

0개의 댓글