# 사용자·기기·세션·주문 식별자를 구분하는 방법

> 계정 수, 브라우저·앱 설치 ID 수, 세션 수와 주문 수를 구분합니다. 여러 기기에서 보인 한 계정과 여러 계정이 사용한 한 브라우저를 합성 이벤트 13행으로 분석하고, 미연결 매출을 보존하는 Python 예제를 제공합니다.

- 발행: 2026-09-06
- 주제: 마케팅 데이터 분석
- 발행처: 매드업 마케팅사업본부 AI 마케팅 개발실
- 웹 원문: https://madobi.madup.com/blog/user-device-session-order-identifiers/

## 세 줄 요약

- 계정 ID, 브라우저·앱 설치 ID, 세션 ID와 주문 ID는 서로 다른 대상을 식별한다. 관측된 ID 수를 실제 사람 수나 물리적 기기 수로 바로 해석할 수는 없다.
- 합성 이벤트 13행에서는 확인된 계정 4개, 범위를 포함한 클라이언트 ID 5개와 세션 키 5개가 집계된다. 같은 문자열 ID도 발급 영역이 다르면 별도로 관리해야 한다.
- 합성 주문 150,000원 중 140,000원은 확인된 계정에 연결되고 10,000원은 미연결로 남는다. 나중의 로그인이나 같은 브라우저라는 이유만으로 미확인 기록을 임의로 채우지 않는다.

---
합성 로그에서 `user_id`의 서로 다른 문자열은 u1·u2·u3 세 개입니다. 하지만
계정의 발급 영역까지 확인하면 계정은 네 개입니다. A 사업의 u1과 B 사업의 u1은
서로 다른 계정으로 정했기 때문입니다. ID의 글자가 같다는 이유만으로 데이터를
합치면 별도 계정의 기록이 섞일 수 있습니다.

반대로 A 사업의 같은 u1이 웹과 앱에 모두 나타난 경우에는, 공통 인증 체계에서
확인한 같은 계정이라는 근거가 있습니다. 이 둘을 별도 사람으로 세는 것도 적절하지
않습니다. 무엇을 식별하는 ID인지와 어느 범위에서 유일한지를 함께 정해야 합니다.

## 계정·클라이언트·세션·주문 ID의 역할

| 식별자 | 예제에서 식별하는 대상 | 주의할 해석 |
| --- | --- | --- |
| user_id | 인증 체계에서 확인한 계정 | 한 계정이 실제 사람 한 명이라는 보장은 별도 문제 |
| device_id | 웹 브라우저 또는 앱 설치의 클라이언트 식별자 | 물리적 기기 대수와 바로 같다고 보지 않음 |
| session_id | 클라이언트 안의 분석 세션 | ID 하나의 전역 유일성을 가정하지 않음 |
| order_id | 원장에 연결된 주문 | 주문 관측 이벤트 수와 구분 |

Google은 GA4의 Device ID가 웹에서는 client ID, 앱에서는 app-instance ID에
기반한다고 설명합니다. 이름에 device가 들어 있어도 하드웨어의 고정 일련번호를
뜻한다고 단정할 수는 없습니다.
[GA4 Device ID 설명](https://support.google.com/analytics/answer/9356035?hl=en)

로그인 계정도 실제 사람과 항상 일대일 관계라고 가정하지 않습니다. 하나의 계정을
여러 사람이 사용하거나 한 사람이 여러 계정을 가질 수 있습니다. 분석 결과에는
‘확인된 계정 수’, ‘관측된 클라이언트 ID 수’처럼 실제로 센 대상을 적어야 합니다.

## 한 계정이 여러 곳에 나타나고 한 브라우저에 여러 계정이 나타날 수 있다

예제의 A/u1 계정은 웹의 d1과 앱의 d1에 나타납니다. d1이라는 문자열은 같지만
서로 다른 스트림에서 발급한 클라이언트 ID입니다. 계정은 공통 인증 체계에서 확인한
A/u1로 연결하고, 클라이언트 ID는 웹과 앱을 구분해서 보관합니다.

A의 웹 클라이언트 shared에서는 u2와 u3가 각각 로그인한 기록이 있습니다.
이 사실을 근거로 두 계정을 하나의 계정으로 합치지는 않습니다. 같은 브라우저에서
여러 계정이 관측됐다는 진단 정보로 남깁니다.

<figure style="overflow-x:auto">
<svg viewBox="0 0 720 282" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="합성 예제에서 A의 웹 d1과 앱 d1은 같은 계정 u1과 연결되고, A의 웹 shared에서는 서로 다른 계정 u2와 u3가 관측됩니다. ID 관계는 일대일이 아닐 수 있습니다." style="width:100%; min-width:640px; height:auto; font-family:inherit">
  <rect width="720" height="282" rx="16" fill="#fffdf8"/>
  <text x="24" y="32" fill="#2a2d3e" font-size="16" font-weight="700">계정과 클라이언트 ID의 관계는 항상 일대일이 아닙니다</text>
  <text x="24" y="66" fill="#5f6273" font-size="13">여러 클라이언트에서 확인한 한 계정</text>
  <rect x="24" y="88" width="150" height="48" rx="9" fill="#eef0f7"/>
  <text x="42" y="119" fill="#3e4a78" font-size="14">A · 웹 · d1</text>
  <rect x="24" y="157" width="150" height="48" rx="9" fill="#eef0f7"/>
  <text x="42" y="188" fill="#3e4a78" font-size="14">A · 앱 · d1</text>
  <path d="M182 112 H196 V146 H218 M182 181 H196 V146" fill="none" stroke="#3e4a78" stroke-width="2"/>
  <rect x="226" y="121" width="116" height="50" rx="9" fill="#fff1cd"/>
  <text x="247" y="152" fill="#2a2d3e" font-size="17" font-weight="700">A / u1</text>
  <text x="374" y="66" fill="#5f6273" font-size="13">한 클라이언트에서 확인한 여러 계정</text>
  <rect x="374" y="116" width="156" height="61" rx="9" fill="#eef0f7"/>
  <text x="391" y="142" fill="#3e4a78" font-size="13">A · 웹</text>
  <text x="391" y="164" fill="#3e4a78" font-size="15">shared</text>
  <path d="M537 146 H549 V112 H563 M549 146 V181 H563" fill="none" stroke="#3e4a78" stroke-width="2"/>
  <rect x="572" y="88" width="124" height="48" rx="9" fill="#fff1cd"/>
  <text x="597" y="119" fill="#2a2d3e" font-size="16" font-weight="700">A / u2</text>
  <rect x="572" y="157" width="124" height="48" rx="9" fill="#fcebee"/>
  <text x="597" y="188" fill="#2a2d3e" font-size="16" font-weight="700">A / u3</text>
  <text x="24" y="243" fill="#2a2d3e" font-size="13">확인된 계정 연결과 클라이언트 관측을 구분합니다. 실제 사람 수를 확정하지 않습니다.</text>
  <text x="24" y="266" fill="#5f6273" font-size="12">합성 데이터 · A의 웹·앱은 같은 계정 발급 체계라는 입력 계약</text>
</svg>
<figcaption>클라이언트에서 마지막으로 본 계정을 모든 과거 이벤트에 일괄 적용하지 않습니다.</figcaption>
</figure>

## 합성 이벤트 13행에 어떤 식별자가 있는가

아래는 독립 사업 영역 A·B의 합성 이벤트입니다. `—`는 해당 ID를 확인하지 못한
상태를 뜻합니다. 세션 ID s1·s2·s3는 원리를 설명하기 위한 코드이며 실제 GA4
세션 ID 생성 결과가 아닙니다.

| 행 | 영역 | 스트림 | 클라이언트 ID | 세션 ID | 계정 ID | 기록 |
| --- | --- | --- | --- | --- | --- | --- |
| e1 | A | web | d1 | s1 | — | 조회 |
| e2 | A | web | d1 | s1 | u1 | 로그인 |
| e3 | A | web | d1 | s1 | u1 | o1 주문 50,000원 |
| e4 | A | app | d1 | s1 | u1 | 조회 |
| e5 | A | app | d1 | s1 | u1 | o2 주문 30,000원 |
| e6 | A | web | shared | s2 | u2 | 로그인 |
| e7 | A | web | shared | s2 | u2 | o3 주문 20,000원 |
| e8 | A | web | shared | s2 | u3 | 로그인 |
| e9 | A | web | shared | s2 | — | 로그아웃 |
| e10 | B | web | d1 | s1 | u1 | o1 주문 40,000원 |
| e11 | A | web | — | — | — | 조회 |
| e12 | A | web | d2 | s3 | — | 조회 |
| e13 | A | web | d2 | s3 | — | o4 주문 10,000원 |

확인된 계정은 A/u1, A/u2, A/u3, B/u1 네 개입니다. 구매와 연결된 계정은
A/u1, A/u2, B/u1 세 개이고, A/u3에는 로그인 기록만 있습니다. 확인된 계정 수와
구매 계정 수의 범위도 다릅니다.

d2라는 새로운 클라이언트 ID가 나왔다고 해서 새로운 사람이 방문했다고 바로
결론 내리지 않습니다. 기존 식별자가 바뀐 경우인지 별도 환경인지 이 데이터만으로
알 수 없기 때문입니다.

## 세션 ID만 세면 세션을 놓칠 수 있다

세션 ID 문자열은 s1·s2·s3 세 종류입니다. 하지만 같은 s1이 서로 다른 클라이언트와
영역에 등장합니다. 예제에서는 클라이언트의 범위를 포함한 세션 키를 사용합니다.

```python
session_key = (tenant_id, stream_id, device_id, session_id)
```

이 기준에서는 A/web/d1/s1, A/app/d1/s1, A/web/shared/s2, B/web/d1/s1,
A/web/d2/s3의 다섯 세션이 있습니다. 위 표기의 슬래시는 설명용이며, 코드에서는
문자열을 이어 붙이지 않고 튜플로 구성 요소를 구분합니다.

<figure style="overflow-x:auto">
<svg viewBox="0 0 720 236" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="합성 데이터의 세션 ID 문자열은 s1 s2 s3 세 종류지만 사업 영역, 스트림과 클라이언트 범위를 함께 사용하면 세션 키는 다섯 개입니다." style="width:100%; min-width:640px; height:auto; font-family:inherit">
  <rect width="720" height="236" rx="16" fill="#fffdf8"/>
  <text x="24" y="32" fill="#2a2d3e" font-size="16" font-weight="700">세션 ID 문자열의 종류와 범위를 갖춘 세션 키는 다릅니다</text>
  <rect x="24" y="56" width="326" height="119" rx="12" fill="#fcebee"/>
  <text x="44" y="87" fill="#a52b35" font-size="14">session_id만 고유화</text>
  <text x="44" y="133" fill="#2a2d3e" font-size="30" font-weight="700">3종류</text>
  <text x="180" y="132" fill="#5f6273" font-size="14">s1 · s2 · s3</text>
  <rect x="370" y="56" width="326" height="119" rx="12" fill="#eef0f7"/>
  <text x="390" y="87" fill="#3e4a78" font-size="14">영역·스트림·클라이언트까지 포함</text>
  <text x="390" y="133" fill="#2a2d3e" font-size="30" font-weight="700">5개</text>
  <text x="490" y="132" fill="#5f6273" font-size="14">예제의 세션 키</text>
  <text x="24" y="207" fill="#5f6273" font-size="12">합성 데이터 · 필요한 ID가 없는 e11은 세션 키를 임의 생성하지 않고 별도 집계</text>
</svg>
<figcaption>로그인 계정이 바뀌었다는 이유만으로 클라이언트 기반 세션 키가 바뀌지는 않습니다.</figcaption>
</figure>

Google도 GA4 밖에서 세션을 분석할 때 `user_id` 또는 `user_pseudo_id`와
`session_id`를 함께 고려하도록 안내합니다. 실제 데이터에서는 속성·스트림과
ID 발급 범위까지 확인해 자신의 세션 정의를 정해야 합니다.
[Google Analytics 세션 설명](https://support.google.com/analytics/answer/9191807?hl=en)

이 예제는 클라이언트 기반 세션 키를 선택했으므로 클라이언트 ID가 없으면 해당
세션에 연결하지 않습니다. 확인된 계정 ID만 있는 이벤트의 계정 정보까지 버리지는
않습니다. 계정 기반 세션 키 등 다른 정의를 사용할 때는 로그인 전후와 계정 전환이
세션 집계에 미치는 영향도 따로 확인해야 합니다.

## 계정 ID가 없는 기록을 한 사람으로 합치지 않기

예제의 e1, e9, e11, e12, e13에는 `user_id`가 없습니다. 이 다섯 행의 계정을
모두 `'anonymous'` 같은 문자열로 채운 뒤 한 사람으로 세지 않습니다. ID가 없다는
상태와 동일한 계정이라는 근거는 다릅니다.

같은 클라이언트에 나중에 u1이 로그인했다는 이유로 e1을 자동으로 u1의 기록으로
바꾸지도 않습니다. 이 예제는 이벤트 시점에 원천에서 확인한 계정만 사용하는
정책을 택했습니다. 어떤 근거로 과거 기록을 연결할 수 있는지 검토한 별도 모델과
구분하기 위해서입니다.

미연결 기록은 버리지 않습니다. e13의 주문 10,000원은 주문 원장에 연결돼 있으므로
전체 주문 금액에는 포함하고, 특정 계정에는 배정하지 않습니다.

| 계정 범위 | 주문 수 | 연결된 주문 금액 |
| --- | ---: | ---: |
| A / u1 | 2건 | 80,000원 |
| A / u2 | 1건 | 20,000원 |
| A / u3 | 0건 | 0원 |
| B / u1 | 1건 | 40,000원 |
| 계정 미연결 | 1건 | 10,000원 |
| 전체 | 5건 | 150,000원 |

<figure style="overflow-x:auto">
<svg viewBox="0 0 720 250" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="합성 주문 금액 150000원 중 확인된 계정에 연결된 금액은 140000원이며, 계정 미연결 주문 10000원은 별도 집계해 전체 매출에서 누락하지 않습니다." style="width:100%; min-width:640px; height:auto; font-family:inherit">
  <rect width="720" height="250" rx="16" fill="#fffdf8"/>
  <text x="24" y="32" fill="#2a2d3e" font-size="16" font-weight="700">계정에 연결되지 않은 매출도 전체 집계에서 보존합니다</text>
  <rect x="24" y="56" width="326" height="126" rx="12" fill="#eef0f7"/>
  <text x="44" y="87" fill="#3e4a78" font-size="14">확인된 구매 계정 3개에 연결</text>
  <text x="44" y="132" fill="#2a2d3e" font-size="29" font-weight="700">140,000원</text>
  <text x="44" y="163" fill="#5f6273" font-size="13">A/u1 · A/u2 · B/u1</text>
  <rect x="370" y="56" width="326" height="126" rx="12" fill="#fff1cd"/>
  <text x="390" y="87" fill="#684d13" font-size="14">계정을 확인하지 못한 주문 1건</text>
  <text x="390" y="132" fill="#2a2d3e" font-size="29" font-weight="700">10,000원</text>
  <text x="390" y="163" fill="#5f6273" font-size="13">A/o4 · 특정 계정에 임의 배정하지 않음</text>
  <text x="24" y="216" fill="#2a2d3e" font-size="15">전체 주문 금액은 150,000원입니다.</text>
  <text x="24" y="239" fill="#5f6273" font-size="12">합성 데이터 · 계정 연결 범위와 전체 주문 범위를 나눠 보고합니다.</text>
</svg>
<figcaption>계정 분석에 포함한 매출이 전체 매출의 어느 부분인지 함께 확인해야 합니다.</figcaption>
</figure>

계정에 연결된 고객만 분석하면 비로그인 구매나 연결이 빠진 거래가 결과에서
빠질 수 있습니다. 이 차이를 숨긴 채 전체 고객의 성향으로 일반화하지 않습니다.
연결률과 미연결 매출을 함께 남기면 분석이 대표하는 범위를 확인할 수 있습니다.

## 주문 ID도 발급 영역을 포함해야 한다

A의 o1은 50,000원, B의 o1은 40,000원인 별도 주문입니다. 주문 ID 문자열만
고유화하면 o1·o2·o3·o4 네 개가 남지만, 영역을 포함한 주문 키는 다섯 개입니다.

```python
order_key = (tenant_id, order_id)
```

같은 주문이 여러 구매 이벤트에서 반복 관측될 수도 있습니다. 예제는 영역과 주문
ID가 같고 확인된 계정·금액도 일치하면 주문 단위에서 한 번만 셉니다. 계정이나
금액이 충돌하면 어느 값을 택할지 임의로 결정하지 않고 오류를 냅니다.

이벤트 수를 세는 키와 거래를 세는 키는 목적이 다릅니다. 구매 이벤트가 하나 더
도착했다고 실제 주문이 하나 늘었다고 판단하지 않습니다.

## GA4의 User-ID 설정과 분석용 데이터 열을 구분하기

예제 JSON의 `user_id`는 수집 후 데이터에 기록된 계정 정보 열입니다. 이 구조를
그대로 gtag의 개별 이벤트 매개변수 설정 예제로 사용하면 안 됩니다.

Google은 웹의 User-ID를 `config` 또는 `set`으로 관리하는 설정값으로 설명합니다.
로그인 상태에서는 확인한 ID를 사용하고 로그아웃 때는 실제 `null`로 지우도록
안내합니다. 빈 문자열이나 문자열 `'null'`로 대신하는 방식과 구분해야 합니다.
[GA4 User-ID 설정 안내](https://developers.google.com/analytics/devguides/collection/ga4/user-id)

수집 상태를 제대로 지우지 않으면 로그아웃 뒤 활동이 이전 계정에 붙을 수 있습니다.
실제 적용에서는 로그인 전·로그인 후·로그아웃 후·계정 전환의 동작을 각각 확인해야
합니다. 분석 단계에서 ID를 바꾸는 것으로 잘못 수집된 인증 상태가 자동으로
복원되지는 않습니다.

또한 Google은 Analytics로 개인을 식별할 수 있는 이메일 주소나 개인 전화번호
등을 보내지 않도록 안내합니다. 제품 정책에 맞는 내부 식별자를 사용하고,
데이터의 수집·연결 목적과 동의·보관 범위는 별도로 확인해야 합니다.
[Google Analytics의 PII 전송 방지 안내](https://support.google.com/analytics/answer/6366371?hl=en)

## GA4 사용자 수와 직접 센 계정 수가 같아야 하나

GA4는 보고 ID 설정에 따라 User-ID, Device ID와 모델링을 사용할 수 있습니다.
직접 `user_id`의 고유값만 센 계정 수가 모든 보고서의 사용자 수와 같아야 하는 것은
아닙니다. 비교 전에 보고 ID와 지표의 정의를 확인해야 합니다.
[GA4 Reporting identity](https://support.google.com/analytics/answer/10976610?hl=en)

이 예제는 관측된 ID의 범위를 구분하는 분석입니다. GA4의 내부 사용자 통합,
모델링, 세션 생성이나 개인 식별 로직을 재현하지 않습니다. 데이터에 없는 실제
사람 수를 추정값 없이 확정해 표시하지도 않습니다.

## Python에서 범위를 갖춘 키로 집계하기

예제에서는 사업 영역이 계정의 발급 범위를 정하고, 스트림은 웹과 앱의 클라이언트
ID 범위를 정합니다. 실제 시스템에서는 ID의 발급 주체와 유일성 범위가 어디인지
확인한 뒤 대응시켜야 합니다.

```python
account_key = (tenant_id, user_id) if user_id is not None else None
client_key = (tenant_id, stream_id, device_id) if device_id is not None else None
```

각 이벤트에서 확인한 계정을 그대로 사용하므로 입력 행의 순서를 바꿔도 배정 결과가
달라지지 않습니다. 클라이언트별로 여러 계정이 보였는지, 한 계정에 여러 클라이언트가
보였는지는 별도 진단 목록으로 반환합니다. 이 진단 목록으로 미확인 ID를 자동
채우지는 않습니다.

[사용자·기기·세션·주문 식별자 예제 ZIP](/downloads/marketing-metrics-07.zip)을 풀고 폴더 안에서 실행합니다.
Python 3.9 이상과 표준 라이브러리만 사용합니다.

```sh
python3 analysis.py
python3 -m unittest
```

결과는 확인된 계정 4개, 클라이언트 ID 5개, 세션 키 5개와 주문 키 5개입니다.
계정이 없는 이벤트 5행과 클라이언트·세션 키가 없는 이벤트 1행도 별도로 나옵니다.
구매 계정별 금액과 미연결 금액을 더하면 전체 주문 금액 150,000원이 됩니다.

검증은 다른 영역의 같은 ID를 분리하는지, 같은 계정의 웹·앱 구매를 연결하는지,
한 클라이언트의 다른 계정을 섞지 않는지 확인합니다. 계정은 있지만 클라이언트가
없는 경우, 구매 이벤트가 반복된 경우, 주문의 계정이 충돌한 경우와 빈 ID도
검사합니다.

<img src="/madobi/thinking.png" alt="서로 다른 식별자의 관계를 확인하는 매도비" width="112" height="112" loading="lazy" style="display:block; margin:1.5rem auto; width:112px; height:112px" />

리포트의 ‘사용자’라는 이름 옆에 어떤 ID를 어느 범위에서 셌는지 남겨 보세요.
확인된 연결과 미연결 기록을 구분해야 사람·계정·기기·방문·거래를 섞지 않고
분석 결과를 설명할 수 있습니다.

공식 문서는 2026년 9월 6일 확인했습니다. 예제의 모든 식별자와 거래는 합성 데이터입니다.

마케팅 데이터 모델·품질·지표 정의 연재 7편입니다.
[이전 글: 한 행이 무엇을 뜻하는지 정하는 데이터 모델](/blog/data-grain-orders-items-model/)

[다음 글: 전환율의 분모가 바뀌면 캠페인 순위가 달라지는 이유](/blog/conversion-rate-denominators-campaign-ranking/)

🐹