구매 이벤트 중복을 제거할 때 transaction_id부터 확인하는 이유
구매 이벤트의 전송 ID와 주문 ID를 구분하고, 정상 재구매를 지우지 않으면서 중복·값 충돌·ID 누락을 처리합니다. GA4 웹·앱의 차이, Google Ads의 조건과 합성 데이터 8행을 검증하는 Python 예제를 담았습니다.
결제를 마친 고객이 완료 화면을 새로고침했습니다. 같은 구매 이벤트가 다시 전송됐고, 서버에서도 결제 완료 기록을 보냈습니다. 화면에서는 한 번 산 상품인데 수집 로그에는 여러 줄이 남을 수 있습니다. 반대로 같은 고객이 같은 금액으로 두 번 주문한 기록은 실제로 두 건일 수 있습니다.
중복 제거는 어느 행을 지울지 결정하는 작업입니다. 기준을 넓게 잡으면 정상 매출이 사라지고, 좁게 잡으면 중복이 남습니다. 앞선 지표 정의 예제에서 주문 수와 구매자 수를 구분했다면, 여기서는 거래를 식별하는 키와 값이 충돌할 때의 처리를 확인할 수 있습니다.
전송 ID와 주문 ID는 무엇이 다른가
전송 ID는 수집한 이벤트를 구분하고, 주문 ID는 사업상 거래를 구분합니다. 한 거래를 브라우저와 서버가 각각 알리면 서로 다른 전송 ID를 가질 수 있습니다. 같은 메시지를 재전송할 때 기존 전송 ID를 유지하는 설계도 가능합니다. 어느 쪽이든 전송 ID가 고유하다는 사실만으로 주문도 고유하다고 판단할 수는 없습니다.
다음은 자체 수집 로그를 단순화한 합성 예제입니다. 특정 플랫폼의 실제 내보내기 스키마나 자동 중복 제거 결과를 모사한 데이터는 아닙니다. 모든 행의 결제 시각은 같고 통화는 KRW입니다. 시각과 금액만으로 거래를 구분하기 어려운 상황을 넣었습니다.
| 행 | 상점 | 주문 ID | 고객 | 금액 | 전송 ID | 수집 경로 |
|---|---|---|---|---|---|---|
| p1 | A | 100 | u1 | 50,000원 | ev1 | 웹 |
| p2 | A | 100 | u1 | 50,000원 | ev1 | 재전송 |
| p3 | A | 100 | u1 | 50,000원 | ev2 | 서버 |
| p4 | A | 101 | u1 | 50,000원 | ev3 | 웹 |
| p5 | B | 100 | u2 | 30,000원 | ev4 | 웹 |
| p6 | A | 102 | u3 | 40,000원 | ev5 | 웹 |
| p7 | A | 102 | u3 | 45,000원 | ev6 | 서버 |
| p8 | A | 없음 | u4 | 20,000원 | ev7 | 웹 |
p1과 p2는 동일 메시지의 반복이고, p3은 같은 거래를 다른 경로에서 보낸 기록입니다. 전송 ID만 고유화하면 p3이 남습니다. 반면 고객·금액·시각으로 고유화하면 실제 두 번째 주문인 p4가 p1과 함께 묶여 사라질 수 있습니다.
주문 ID 하나만으로 중복을 제거해도 되나
주문 번호가 유일한 범위를 먼저 확인해야 합니다. 예제에서는 상점 A와 B가 모두
100번 주문을 발급했습니다. 주문 ID만 키로 쓰면 정상적인 B의 30,000원 주문이
A의 주문과 섞입니다. 이 데이터에서는 (상점 ID, 주문 ID) 쌍이 거래를 구분합니다.
연결하는 시스템마다 번호 형식이 다를 수도 있습니다. 한쪽에서 앞에 0을 붙이고 다른 쪽에서 숫자로 바꾸거나, 접두어를 임의로 제거하면 매칭 결과가 달라집니다. 키의 정규화 규칙은 원천 시스템의 계약을 확인한 뒤 정해야 합니다. 비슷하게 보인다는 이유로 문자를 지우는 방식은 피하는 편이 좋습니다.
거래 ID에는 고객의 이메일이나 전화번호를 직접 넣지 않습니다. 거래를 구분하는 데 필요한 식별자와 개인을 식별하는 정보는 분리해서 설계해야 합니다.
GA4 웹과 앱의 transaction_id 처리는 같은가
Google은 같은 transaction_id를 가진 구매를 중복 제거한다고 안내하면서,
이 기능이 웹 스트림에서 수집한 데이터에 적용되고 앱 스트림에는 적용되지 않는다고
명시합니다. 거래마다 고유한 값을 사용해야 하며 빈 문자열을 보내지 않도록
안내합니다. GA4 거래 ID 중복 제거 조건
웹에서 확인한 결과를 앱의 구매 이벤트나 MMP 내보내기에 그대로 기대하면 안 됩니다. 수집 경로가 바뀌면 식별자와 처리 규칙을 다시 확인해야 합니다.
Google Ads도 조건을 구분해야 합니다. 공식 문서는 같은 전환 액션에서 같은 거래 ID가 전달되면 중복 전환을 식별한다고 설명합니다. 서로 다른 전환 액션의 숫자를 합치는 작업까지 자동으로 해결된다는 의미로 읽을 수는 없습니다. Google Ads 거래 ID 안내
같은 주문의 금액이 다르면 무엇을 남겨야 하나
p6과 p7은 상점 A의 같은 102번 주문이지만 금액이 40,000원과 45,000원으로 다릅니다. 더 큰 값을 남기면 45,000원, 먼저 도착한 값을 남기면 도착 순서에 따라 40,000원 또는 45,000원이 됩니다. 어느 규칙도 데이터만 보고 정답이라고 할 수 없습니다.
부분 취소 전후 상태일 수도 있고, 한쪽에 배송비가 포함됐을 수도 있습니다. 상품 금액과 주문 금액을 섞었거나 다른 주문을 잘못 연결했을 가능성도 확인해야 합니다. 주문 원장에서 무엇을 확정 값으로 사용하는지, 수정 이력을 어떤 순서로 반영하는지 정해져 있어야 하나의 값을 선택할 수 있습니다.
예제는 해당 그룹 전체를 확인 보류로 분류합니다. 주문 ID가 없는 p8도 보류합니다. 이 행들을 매출 0원으로 바꿔 버리는 대신 원본과 사유를 함께 남깁니다.
보류 행이 많은 채널은 검증된 매출 소계가 상대적으로 낮아질 수 있습니다. 따라서 이 소계로 채널 성과를 바로 순위 매기면 수집 품질 차이를 성과 차이로 해석할 위험이 있습니다. 집계 값 옆에 보류 행 수와 사유를 함께 두고, 의사결정에 영향을 줄 정도라면 원천 데이터 확인을 먼저 해야 합니다.
Python에서 거래를 고유화하는 기준
예제는 상점·거래 ID가 같은 행을 묶고, 고객·통화·금액·실제 결제 시각이 모두 같을
때만 한 주문으로 확정합니다. 여기서 결제 시각은 이벤트가 서버에 도착한 시각과
다릅니다. 입력의 occurred_at은 변경되지 않는 실제 결제 시각이라는 전제를 둡니다.
핵심 분기는 다음과 같습니다. 전체 코드는 입력 검증과 사유 기록을 함께 포함합니다.
for key in sorted(groups):
group = groups[key]
canonical = group[0][1]
if any(item[1] != canonical for item in group[1:]):
quarantine.extend(
{"record": dict(item[0]), "reason": "conflicting_order"}
for item in group
)
continue
orders.append(canonical)
duplicates.extend(
{"record": dict(item[0]), "reason": "same_order_same_values"}
for item in group[1:]
)
값이 같은 그룹에서는 어느 행을 대표로 선택해도 확정 주문의 사업상 값이 같습니다. 값이 다른 그룹에서는 대표를 고르지 않습니다. 그래서 입력 순서가 바뀌어도 먼저 도착한 값을 우연히 정답으로 채택하는 문제가 생기지 않습니다.
잘못된 통화·금액·시각은 집계를 중단하고, 식별자가 없는 행은 보류합니다. 이 예제는 원시 데이터를 한 번에 읽는 배치 처리입니다. 여러 서버가 동시에 같은 거래를 저장할 때의 잠금, 영속 상태, 전송 ID 충돌 검사까지 구현한 운영 시스템은 아닙니다.
같은 데이터를 다시 처리해도 주문 수가 유지되는가
같은 입력을 한 번 더 처리해도 확정 주문 목록이 바뀌지 않는 성질을 멱등성이라고 부릅니다. 로그 파일이 다시 도착하거나 과거 데이터를 재집계할 때 필요한 기준입니다. 예제의 검증에서는 단순히 ‘중복이 줄었다’는 결과보다 다음 성질을 확인합니다.
수식으로는 검증된 거래 목록을 만드는 연산을 F라고 할 때 F(F(E)) = F(E)를
확인할 수 있습니다. 이미 고유화한 거래를 다시 고유화해도 결과가 같아야 합니다.
원본을 두 번 이어 붙이거나 입력 순서를 뒤집었을 때도 검증된 거래 목록이 같은지
별도로 검사합니다. 보류 건이 존재한다는 사실까지 사라져서는 안 되므로 원시 수신
행 수와 보류 이력은 따로 남깁니다.
구매 중복 분류 예제 ZIP을 풀고 폴더 안에서 실행할 수 있습니다. Python 3.9 이상과 표준 라이브러리만 사용합니다.
python3 analysis.py
python3 -m unittest
출력의 summary에서는 8행·335,000원이 주문 3건·130,000원, 중복 2행·100,000원,
보류 3행·105,000원으로 나뉜 것을 확인할 수 있습니다. 각 목록에는 확정한 거래나
분류한 원본과 사유가 남습니다. 테스트는 정상 재구매 보존, 상점별 ID 범위,
충돌 그룹 전체 보류, ID 누락, 재전송과 순서 변경, 멱등성까지 확인합니다.
태그 수집은 어떻게 검증해야 하나
수집 태그를 확인할 때는 테스트 거래마다 실제 거래 ID가 전달되는지 보고, 같은 거래를 다시 보냈을 때와 새로운 거래를 보냈을 때를 나눠 확인합니다. Google은 웹 전자상거래 검증에 DebugView를 안내하며, 같은 거래 ID를 재사용하면 두 번째 이벤트의 값이 달라도 무시될 수 있다고 설명합니다. 테스트에서 새 거래를 확인하려고 이전 ID를 계속 쓰면 정상적인 중복 제거를 수집 실패로 오해할 수 있습니다. GA4 전자상거래 수집 검증 안내
완료 화면 새로고침, 브라우저·서버 동시 전송, 정상적인 두 번째 결제, 취소·수정 주문을 각각 점검하면 문제가 생기는 조건을 구분할 수 있습니다. 이 글의 Python 검증은 합성 로그의 분류에 대한 결과이며 실제 광고 계정이나 수집 태그를 변경한 기록은 아닙니다.
원본의 행이 줄어든 만큼 설명도 남아야 합니다. 어떤 주문이 하나로 합쳐졌는지, 정상 주문이 보존됐는지, 어떤 값이 아직 결정되지 않았는지까지 확인해야 중복 제거 결과를 리포트에 사용할 수 있습니다.
공식 문서는 2026년 9월 6일 확인했습니다. 플랫폼별 적용 조건은 현재 사용하는 제품·스트림·전환 액션의 문서와 설정을 함께 확인해야 합니다.
마케팅 데이터 모델·품질·지표 정의 연재 2편입니다. 이전 글: GA4·MMP·주문 매출이 다를 때 먼저 맞춰야 할 지표 정의
🐹
자주 묻는 질문
GA4 transaction_id 중복 제거는 앱에도 적용되나요?
Google의 공식 안내에 따르면 transaction_id를 이용한 이 중복 제거는 웹 스트림에서 수집한 데이터에 적용되며 앱 스트림에는 적용되지 않습니다. 앱 이벤트와 MMP 데이터는 해당 수집 경로의 규칙을 별도로 확인해야 합니다.
같은 고객이 같은 금액을 결제하면 중복 주문인가요?
그 조건만으로는 판단할 수 없습니다. 같은 고객이 동일한 상품을 다시 사거나 같은 시각에 서로 다른 주문을 완료할 수 있습니다. 실제 거래 식별자를 사용하고, 주문 번호가 어느 상점 범위에서 유일한지도 확인해야 합니다.
같은 주문 번호인데 금액이 다르면 큰 금액만 남겨도 되나요?
최댓값을 선택할 근거가 없다면 먼저 확인해야 합니다. 부분 취소, 수정 주문, 상품·주문 단위 혼합이나 수집 오류일 수 있습니다. 예제는 해당 주문 그룹을 모두 보류하고, 권위 있는 주문 원장의 상태를 확인한 뒤 처리하도록 구성했습니다.
중복 제거 후 집계액이 줄면 데이터가 정확해진 건가요?
금액이 줄었다는 사실만으로 정확성을 판단할 수 없습니다. 정상 거래를 지우거나 확인 보류 건을 제외해도 금액은 줄어듭니다. 원본을 확정·중복·보류로 구분하고, 각 행과 금액이 어디에 포함됐는지 대조해야 합니다.