BTP

HANA 테이블 vs Document Store 선택 기준 #shorts #SAP #HANA

▶ YouTube에서 보기

📖 이 글에서 다룰 것

SAP HANA는 컬럼 스토어 기반의 관계형 엔진으로 잘 알려져 있지만, 그 안에는 JSON 문서를 그대로 저장하고 SQL로 조회할 수 있는 JSON Document Store가 함께 들어 있습니다. 문제는 "쓸 수 있다"와 "써야 한다"는 다르다는 점입니다. 이 글은 관계형 테이블 대비 Document Store가 실제로 유리해지는 시점과 그 대가로 포기하는 것이 무엇인지, 트레이드오프 관점에서 판단 기준을 세우는 데 집중합니다.

  • 컬렉션(Collection)과 컬럼 테이블의 구조적 차이를 설명할 수 있다
  • 스키마 가변 데이터에서 Document Store를 선택해야 하는 신호를 식별할 수 있다
  • CREATE COLLECTION부터 JSON_TABLE 조인, 운영 하드닝까지 직접 구현할 수 있다
  • 인덱싱·조인·타입 관련 함정을 사전에 회피할 수 있다

📚 미리 갖춰두면 좋은 배경

기본적인 ANSI SQL(SELECT, JOIN, WHERE)과 JSON 문법(객체, 배열, 중첩 구조)을 알고 있다면 충분합니다. HANA 컬럼 스토어의 동작 방식이나 SAP HANA Cloud 인스턴스 관리 경험이 있으면 트레이드오프 부분을 더 깊이 이해할 수 있습니다. NoSQL 데이터베이스(MongoDB 등) 경험은 필수가 아니지만, 있다면 비교 관점에서 도움이 됩니다.

🔧 테스트 환경

JSON Document Store는 SAP HANA 2.0 SPS 01부터 도입되었고, 이 글의 예제는 SAP HANA Cloud, SAP HANA Database 기준으로 작성했습니다. 두 환경의 활성화 방법이 다릅니다.

  • SAP HANA Cloud: HANA Cloud Central에서 인스턴스 편집 시 Document Store 추가 기능(additional capability)을 켜야 합니다. 기본값은 비활성이며, 켜지 않으면 CREATE COLLECTION이 실패합니다.
  • SAP HANA 2.0 (온프레미스): 테넌트 DB에 docstore 서비스를 추가해야 합니다. 예: ALTER DATABASE MYDB ADD 'docstore' (SYSTEMDB에서 실행).
  • 클라이언트: SQL 콘솔(Database Explorer 등)과, 3단계 예제용으로 Node.js + @sap/hana-client 드라이버.
  • 권한: 대상 스키마에 컬렉션 생성 권한이 있는 사용자. 운영에서는 최소 권한 원칙을 권장합니다.

💡 핵심 개념: 언제 컬렉션이 테이블을 이기는가

비유로 시작하면, 관계형 테이블은 규격 팔레트만 받는 자동화 창고입니다. 모든 화물이 같은 규격이라는 전제 덕분에 적재(압축)와 검색(스캔·집계)이 극도로 빨라집니다. 반면 Document Store의 컬렉션은 크기가 제각각인 소포를 그대로 받아주는 보관함입니다. 규격을 맞추는 비용이 사라지는 대신, 창고 전체를 훑는 작업은 상대적으로 불리해집니다.

기술적으로 컬렉션은 스키마 정의 없이 JSON 문서를 내부 바이너리 포맷으로 압축 저장합니다. 문서마다 필드 구성이 달라도 되고, 중첩 객체와 배열을 평탄화 없이 그대로 담습니다. 중요한 차별점은 HANA Document Store가 ACID 트랜잭션과 스냅샷 격리를 지원하고, 같은 데이터베이스 안에서 컬럼 테이블과 SQL로 조인할 수 있다는 것입니다. 별도 NoSQL 시스템을 두는 방식과 달리, 데이터 파이프라인·백업·보안 체계를 하나로 유지할 수 있습니다.

선택 기준을 신호로 정리하면 이렇습니다. Document Store가 적합한 신호: (1) 외부 파트너·디바이스마다 페이로드 구조가 달라 컬럼 추가가 끝나지 않는 경우, (2) 중첩 배열을 관계형으로 풀면 조인 5개짜리 EAV 모델이 되는 경우, (3) 원본 페이로드를 감사·재처리용으로 무손실 보존해야 하는 경우, (4) 스키마가 아직 확정되지 않은 신규 서비스의 수집 단계(랜딩존). 관계형 테이블이 적합한 신호: (1) 대량 집계·리포팅이 주 워크로드인 경우(컬럼 스토어 압축·스캔이 압도적으로 유리), (2) 외래키 수준의 참조 무결성이 필요한 경우(컬렉션에는 제약조건이 없음), (3) 동일 필드를 조건으로 초당 수천 건을 조회하는 경우(보조 인덱스 지원이 제한적이라 스캔 비용이 커짐). 실무에서는 양자택일보다 "컬렉션으로 받고, 핵심 필드만 컬럼 테이블로 승격"하는 하이브리드가 일반적으로 가장 안정적입니다.

💻 직접 해보기

시나리오는 국제 물류 플랫폼입니다. 여러 운송사가 각기 다른 구조의 화물 이벤트(위치, 온도 센서, 통관 상태)를 보내오고, 이를 FREIGHT_EVENTS 컬렉션에 수집한 뒤 관계형 마스터 CARGO_MASTER와 결합해 분석합니다.

1단계: 컬렉션 생성과 기본 조회

-- 컬렉션 생성 (스키마 정의 없음)
CREATE COLLECTION "LOGI"."FREIGHT_EVENTS";

-- 구조가 서로 다른 두 문서를 그대로 저장
INSERT INTO "LOGI"."FREIGHT_EVENTS" VALUES('{
  "eventId": "EV-1001", "cargoId": "CG-88231",
  "carrier": {"code": "NWX", "name": "NordWave Express"},
  "status": "IN_TRANSIT",
  "sensor": {"temperatureC": -18.4, "humidityPct": 61},
  "reportedAt": "2026-08-19T02:15:00Z"
}');

INSERT INTO "LOGI"."FREIGHT_EVENTS" VALUES('{
  "eventId": "EV-1002", "cargoId": "CG-88231",
  "carrier": {"code": "KMR"},
  "status": "CUSTOMS_HOLD",
  "customs": {"office": "ICN-T2", "reasonCodes": ["DOC_MISSING","XRAY"]},
  "reportedAt": "2026-08-19T05:40:00Z"
}');

-- 점(.) 표기로 중첩 필드 직접 조회
SELECT "eventId", "carrier"."code", "sensor"."temperatureC"
  FROM "LOGI"."FREIGHT_EVENTS"
 WHERE "status" = 'CUSTOMS_HOLD';

두 번째 문서에는 sensor가 없고 customs가 있지만 오류 없이 저장됩니다. 없는 필드를 조회하면 NULL로 반환되는 점이 관계형과 다른 핵심 감각입니다.

2단계: 실무 시나리오 — 관계형 조인과 애플리케이션 에러 처리

분석 화면에서는 화주 정보가 필요하므로 컬럼 테이블과 조인합니다. JSON_TABLE로 컬렉션을 관계형 뷰처럼 평탄화하면 옵티마이저가 다루기 쉬운 형태가 됩니다.

SELECT m.CARGO_ID, m.SHIPPER_NAME, e.STATUS, e.TEMP_C
  FROM JSON_TABLE("LOGI"."FREIGHT_EVENTS", '$'
         COLUMNS (
           CARGO_ID NVARCHAR(20) PATH '$.cargoId',
           STATUS   NVARCHAR(30) PATH '$.status',
           TEMP_C   DOUBLE       PATH '$.sensor.temperatureC'
         )) AS e
  JOIN "LOGI"."CARGO_MASTER" AS m
    ON m.CARGO_ID = e.CARGO_ID
 WHERE e.TEMP_C > -15;

수집 측 Node.js 코드는 잘못된 페이로드를 걸러내고 실패를 로깅해야 합니다.

const hana = require('@sap/hana-client');

async function ingestEvent(conn, payload) {
  if (!payload.eventId || !payload.cargoId) {
    console.warn('[ingest] skip: required key missing', payload.eventId);
    return false;
  }
  const stmt = conn.prepare(
    'INSERT INTO "LOGI"."FREIGHT_EVENTS" VALUES(?)');
  try {
    stmt.exec([JSON.stringify(payload)]);   // 파라미터 바인딩 필수
    return true;
  } catch (err) {
    // 301: unique 위반 없음 주의 — 중복은 앱 레벨에서 검증
    console.error('[ingest] fail', payload.eventId, err.code, err.message);
    throw err;
  } finally {
    stmt.drop();
  }
}

문자열 연결로 JSON을 SQL에 붙이면 따옴표 이스케이프 문제와 인젝션 위험이 생기므로, 위처럼 바인딩 변수를 쓰는 방식을 권장합니다.

3단계: 프로덕션 — 승격 파이프라인, 중복 방어, 권한

컬렉션에는 UNIQUE 제약이 없으므로 재전송 중복은 병합 단계에서 제거하고, 리포팅용 핵심 필드는 컬럼 테이블로 승격합니다.

-- 분석용 컬럼 테이블 (핵심 필드 승격 대상)
CREATE COLUMN TABLE "LOGI"."EVENT_FLAT" (
  EVENT_ID NVARCHAR(20) PRIMARY KEY,
  CARGO_ID NVARCHAR(20), STATUS NVARCHAR(30),
  TEMP_C DOUBLE, REPORTED_AT TIMESTAMP
);

-- 주기 배치: 중복은 PK가 걸러주도록 UPSERT 사용
UPSERT "LOGI"."EVENT_FLAT"
SELECT EVENT_ID, CARGO_ID, STATUS, TEMP_C, REPORTED_AT
  FROM JSON_TABLE("LOGI"."FREIGHT_EVENTS", '$'
         COLUMNS (
           EVENT_ID    NVARCHAR(20) PATH '$.eventId',
           CARGO_ID    NVARCHAR(20) PATH '$.cargoId',
           STATUS      NVARCHAR(30) PATH '$.status',
           TEMP_C      DOUBLE       PATH '$.sensor.temperatureC',
           REPORTED_AT TIMESTAMP    PATH '$.reportedAt'
         ))
WITH PRIMARY KEY;

-- 최소 권한: 수집 계정은 컬렉션 INSERT만
GRANT INSERT ON "LOGI"."FREIGHT_EVENTS" TO INGEST_USER;
GRANT SELECT ON "LOGI"."EVENT_FLAT" TO REPORT_ROLE;

테스트는 "이형 문서 저장 → 평탄화 후 건수·NULL 비율 검증" 형태의 회귀 케이스를 두는 것이 좋습니다. 예를 들어 배치 후 SELECT COUNT(*) FROM EVENT_FLAT WHERE REPORTED_AT IS NULL이 임계치를 넘으면 파이프라인을 중단시키는 식입니다. 성능 관점에서는 조회 빈도가 높은 쿼리를 컬렉션 직조회 대신 승격 테이블로 돌리는 것만으로도 대부분의 병목이 해소됩니다.

⚠️ 자주 만나는 함정

Q1. CREATE COLLECTION에서 "feature not enabled" 류 오류가 납니다. Document Store 기능이 켜져 있지 않은 경우입니다. HANA Cloud라면 HANA Cloud Central에서 인스턴스의 Document Store 옵션을 활성화하고, 온프레미스라면 SYSTEMDB에서 해당 테넌트에 docstore 서비스를 추가했는지 확인하세요. 활성화 후 인스턴스 재기동이 필요할 수 있습니다.

Q2. WHERE 조건이 분명히 맞는데 0건이 나옵니다. 컬렉션의 필드명은 대소문자를 구분합니다. "Status""status"는 다른 필드이며, 쌍따옴표 없이 쓰면 HANA가 대문자로 해석해 의도와 달라집니다. 또 하나, 문서마다 타입이 섞여 있으면(예: "temperatureC": "-18.4"처럼 문자열로 온 경우) 숫자 비교에서 조용히 제외됩니다. JSON_TABLE에서 명시적 타입 캐스팅을 걸어 정합성을 강제하는 것이 안전합니다.

Q3. 컬렉션 직조회가 데이터가 쌓일수록 느려집니다. Document Store는 관계형 테이블 같은 보조 인덱스 활용이 제한적이어서, 조건 검색이 사실상 스캔으로 동작하는 경우가 많습니다. 반복 조회 패턴이 굳어졌다면 그 필드는 3단계처럼 컬럼 테이블로 승격하는 것이 일반적인 해법입니다. 컬렉션은 원본 보존과 유연 수집, 테이블은 검색과 집계라는 역할 분담을 유지하세요.

Q4. 문서 일부만 수정하고 싶습니다. 관계형과 유사하게 UPDATE "FREIGHT_EVENTS" SET "status" = 'CLOSED' WHERE "eventId" = 'EV-1002' 형태가 동작합니다. 다만 대량 갱신이 잦다면 애초에 이벤트를 불변(append-only)으로 쌓고 최신 상태는 승격 테이블에서 관리하는 설계가 운영상 단순합니다.

🚀 더 파볼 주제

다음 주제로 이어가면 이해가 한층 깊어집니다. 첫째, 관계형 컬럼(NVARCHAR/NCLOB)에 JSON을 담고 JSON_VALUE·JSON_QUERY로 다루는 방식과 Document Store의 비교 — 소량이면 전자로 충분한 경우도 많습니다. 둘째, HANA Cloud의 파티셔닝과 대용량 컬렉션 운영 전략. 셋째, CAP(Cloud Application Programming Model) 서비스에서 네이티브 SQL로 컬렉션을 다루는 통합 패턴. 넷째, 수집 랜딩존을 데이터 레이크(HANA Cloud, data lake)와 비교 선택하는 아키텍처 논의입니다.

📚 더 읽어볼 자료

댓글 0

아직 댓글이 없습니다.