[Deep Quest 개발노트] 2026.01.04 TRD 리뷰 기록

Ooleem·2026년 1월 4일

DeepQuest

목록 보기
1/2
post-thumbnail

정글에서 모의면접 시스템을 만들었던 경험이 계기가 되어, 정글 선배님의 프로젝트에 공동 개발자로 참여하게 되었다.

https://www.deepquest.app/

"Deep Quest"라는 기술면접 대비 서비스이고, 유료화와 연동된 포인트 시스템 도입부터 참여하게 되었다.
작성되어 있는 포인트 시스템 PRD를 바탕으로 TRD를 처음으로 직접 작성해봤고(물론 클로드가), 나름 꼼꼼히 검토한 다음에 제출했다고 생각했지만 피드백 받은 부분이 상당히 많았다.

TRD 작성, 리뷰 한 번 왔다갔다 한 것만으로도 엄청나게 배우는 부분들이 많다. 기록해본다.

TRD 초안 작성/검토

검토해서 수정한 내용

  • 클로드가 먼저 작성한 내용을 나름 꼼꼼히 검토했다.
  • PRD에는 중복 클릭 방지를 위한 멱등성 키가 반드시 필요하다고 언급되어 있었는데, TRD 초안에는 선택사항인 것처럼 기재되어 있었다. 이 부분은 바로 수정 요청했다.
  • 또한 초안에서는 멱등성 키를 클라이언트에서 생성한 다음에 서버로 보내는 방식으로 구현되어 있었는데, 이렇게 되면 클라이언트에서 위변조 가능성이 있기 때문에 서버에서 생성하도록 수정했다.
  • 일단 이 정도까지만 짚어낼 수 있었고, 수정해서 제출했다.

작성하면서 알게 된 내용

프론트엔드 캐싱 전략

  • 유저의 현재 포인트 잔액, 또는 거래 내역을 불러오는 API의 경우 시간을 정해 두고(staleTime) React Query로 캐싱할 수 있으며, 트랜젝션이 발생하면 자동으로 해당 캐시에 대해 invalidate하도록 할 수 있다.

NOT IN (서브쿼리) 문제

  • 다음 마이그레이션 코드에서,
INSERT INTO user_points (user_id, balance, created_at, updated_at)
SELECT id, 0, NOW(), NOW()
FROM users
WHERE id NOT IN (SELECT user_id FROM user_points);
  • 서브쿼리 결과에 NULL이 하나라도 있으면, 연산 결과가 UNKNOWN이 되어 메인 쿼리에서 데이터가 반환되지 않는다
  • 또한 서브쿼리 결과를 메모리에 전부 로드하므로 성능도 좋지 않다
  • NOT EXIST, 또는 LEFT JOIN + IS NULL을 대신 사용하는 게 좋다
INSERT INTO user_points (user_id, balance, created_at, updated_at)
SELECT u.id, 0, NOW(), NOW()
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM user_points up
  WHERE up.user_id = u.id
);

또는 PostgreSQL의 경우 UNIQUE 제약이 있는 경우에 다음과 같이 사용할 수도 있다.

INSERT INTO user_points (user_id, balance, created_at, updated_at)
SELECT id, 0, NOW(), NOW()
FROM users
ON CONFLICT (user_id) DO NOTHING;

참고 : https://velog.io/@hskhyl/NOT-IN-NOT-EXISTS-LEFT-JOIN-IS-NULL-%EC%84%B1%EB%8A%A5%EB%B9%84%EA%B5%90

리뷰 받은 부분

피드백 받은 내용

멱등성 키 생성 규칙

  • 실제 서비스 흐름에 따라서 멱등성 키가 겹칠 수 있으므로, 생성 규칙을 만들 땐 꼭 흐름을 같이 생각해야 한다
    : 이 부분은 내가 안이했다.. 좀 더 꼼꼼히 확인했어야 했다.

상황과 맥락을 고려한 Lock 전략 선택

  • Prisma의 경우 FOR UPDATE를 지원하지 않는다
    : Pessimistic Lock을 선택할 경우 Raw SQL문을 사용해야 한다
  • Supabase를 사용하는 Serverless 환경에서는 Lock으로 잠기는 상황이 많아질 경우 connection pool 고갈 위험을 생각해야 한다
    : 데이터베이스 아키텍쳐, 티어마다 connection pool 최대 제한이 달라질 수 있음을 염두해 둬야 한다
  • 결제/포인트 시스템에서는 일반적으로 보수적인 접근을 해야 하므로 Pessimistic Lock을 선택하라는 조언이 많긴 하다
    : 하지만 우리는 MVP 단계이기도 하고, 특히 지금 상황에서는 특정 유저의 포인트를 여러 사람이 접근해서 차감/증가시킬 일이 없다는 걸 생각했어야 한다
    : 따라서 지금 상황에는 Optimistic Lock이 더 적절했다

서버에서 결정할 수 있는 값은 절대 클라이언트에서 받지 말 것

  • 서비스 요청 시 요청 금액 cost를 클라이언트에서 받도록 구현되어 있었는데, 이 경우에도 멱등성 키와 마찬가지로 위변조의 위험이 있다. 클라이언트를 믿지 마라!
  • 서비스 금액 정보는 서버의 환경 변수로 관리하는 것이 좋다

새로 공부하게 된 내용

Optimistic Lock vs. Pessimistic Lock

Saga Pattern

Connection Pool

Feature Flag (Phased Rollout, Canary Deployment)

분량이 하나같이 많다.. 각각 따로 글을 써볼 생각이다

profile
개발 / 성장 노트

0개의 댓글