개발 slecs

결제 주문 데이터가 저장 안 되던 쿼리 분해 버그 수정

목차

결제 처리 로직에서 데이터베이스 INSERT 쿼리 결과를 잘못 다루고 있었다. 쿼리 함수가 반환하는 형태와 코드에서 기대하는 형태가 맞지 않아, 결제 주문 정보가 제대로 저장되지 않는 상황이 발생했고, 이를 수정했다.

문제: 배열을 튜플처럼 분해하는 실수

내가 사용 중인 데이터베이스 쿼리 함수는 rows 배열을 직접 반환한다. 즉 이런 식이다:

const rows = await query('INSERT INTO orders (...) VALUES (...)')
// rows는 바로 배열: [{ id: 123, total: 50000 }, ...]

그런데 당시 코드에서는 마치 튜플이 반환되는 것처럼 처리하고 있었다:

const [rows] = await query(...)  // ❌ 잘못된 분해
// 이렇게 하면 rows가 첫 번째 원소(행 객체)가 돼버린다

튜플 분해를 하면 배열의 첫 번째 요소만 변수에 할당된다. 그래서 rows는 전체 행 배열이 아니라 하나의 행 객체가 되고, 여러 행을 처리하거나 행 개수를 확인하는 로직이 모두 깨진다. 결제 정보를 데이터베이스에 저장해야 하는 INSERT 로직이 제대로 동작할 리 없다.

왜 이런 실수가 생겼나

데이터베이스 라이브러리마다 쿼리 결과 반환 방식이 다르다. 어떤 라이브러리는 { rows, rowCount }를 객체로 반환하고, 어떤 건 [rows, metadata] 처럼 튜플로 반환한다. 실제 코드에서 이런 실수가 생기는 원인은 보통 이렇다:

  1. 다른 ORM/라이브러리에서의 경험 — 이전에 쓰던 라이브러리는 튜플 반환이었는데, 지금 쓰는 건 배열 반환
  2. 습관적 분해 — 데이터베이스 결과처리는 항상 "뭔가 분해"하는 것이 일반적이라서, 자동으로 그 패턴을 따름
  3. 타입 정의의 모호함 — TypeScript 타입만으로는 "이게 배열인지 튜플인지" 구분이 명확하지 않을 수도 있음

특히 결제 같이 중요한 기능의 버그는 금전 거래 손실로 직결되므로, 코드리뷰와 테스트에서 특히 신경 써야 한다.

올바른 처리 방식

데이터베이스 함수의 반환값 형태에 따라 다르게 처리해야 한다:

반환값 형태 코드 패턴 예시
rows 직접 배열 할당만 const rows = await query(...)
객체 반환 객체 분해 const { rows } = await query(...)
튜플 반환 배열 분해 const [rows, meta] = await query(...)

이 fix에서는 첫 번째 패턴으로 수정했다. 즉, 배열을 직접 변수에 할당하는 식으로 변경했다.

배운 점과 예방책

이 버그를 겪으면서 느낀 점:

  • 결제/재무 기능은 "실패할 수 없는" 부분이다. 쿼리 실행이 성공했다고 해서 반환값을 제대로 읽지 못하면 전혀 쓸모가 없다.
  • 라이브러리를 도입할 때 쿼리 함수의 반환값 타입을 명확히 정의하고 문서화해야 한다.
  • TypeScript strict 모드도 이런 실수를 완전히 막지는 못한다. 덕 타이핑 구조가 겹칠 수 있기 때문이다.
  • INSERT 성공 후 반환값 검증은 테스트 코드에서 반드시 확인해야 한다. 단순히 "에러가 안 나는지"만으로는 부족하다.

앞으로 결제 관련 쿼리를 다룰 때는 먼저 쿼리 함수 시그니처를 두 번 확인하고, 각 환경에서 테스트를 돌려 실제 반환값을 확인하는 습관을 들여야겠다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.