Skip to main content

컬럼 마스킹이란?

컬럼 마스킹(Column Masking) 은 민감한 컬럼의 값을 사용자 권한에 따라 동적으로 가림 처리 하는 기능입니다. 원본 데이터를 변경하지 않고, 조회 시점에 SQL 함수가 실행되어 마스킹된 값이 반환됩니다.
💡 왜 필요한가요? 고객 정보 테이블에는 이메일, 전화번호, 주민번호 같은 개인정보가 포함되어 있습니다. 분석가는 고객 구매 패턴을 분석해야 하지만, 개인 식별 정보는 볼 필요가 없습니다. 컬럼 마스킹을 사용하면 역할에 따라 적절한 수준의 데이터만 노출할 수 있습니다.
기본 개념은 권한 관리의 “열 수준 보안” 섹션에서 소개했습니다. 이 문서에서는 다양한 마스킹 패턴과 고급 활용법을 상세히 다룹니다.

동작 원리

컬럼 마스킹은 SQL 함수 로 구현됩니다. 함수는 원본 값을 입력받아, 사용자 권한에 따라 원본 또는 마스킹된 값을 반환합니다.
관리자 (admin 그룹):
  SELECT email FROM customers → alice@company.com

분석가 (analysts 그룹):
  SELECT email FROM customers → al***@company.com

일반 사용자:
  SELECT email FROM customers → ***@***.***

다단계 마스킹 패턴

관리자 / 분석가 / 일반 사용자 3단계

대부분의 기업에서는 다음 3단계 마스킹을 적용합니다.
수준대상이메일 예시전화번호 예시
원본관리자, 데이터 소유자alice@company.com010-1234-5678
부분 마스킹분석가, 운영자al***@company.com010-****-5678
완전 마스킹일반 사용자@.***-**-***

마스킹 함수 작성

이메일 마스킹

CREATE OR REPLACE FUNCTION production.policies.mask_email(email_col STRING)
  RETURNS STRING
  COMMENT '이메일 주소를 역할에 따라 마스킹합니다'
  RETURN
    CASE
      WHEN IS_ACCOUNT_GROUP_MEMBER('admin') THEN email_col
      WHEN IS_ACCOUNT_GROUP_MEMBER('analysts') THEN
        CONCAT(LEFT(email_col, 2), '***@', SPLIT(email_col, '@')[1])
      ELSE '***@***.***'
    END;

전화번호 마스킹

CREATE OR REPLACE FUNCTION production.policies.mask_phone(phone_col STRING)
  RETURNS STRING
  COMMENT '전화번호를 역할에 따라 마스킹합니다'
  RETURN
    CASE
      WHEN IS_ACCOUNT_GROUP_MEMBER('admin') THEN phone_col
      WHEN IS_ACCOUNT_GROUP_MEMBER('analysts') THEN
        CONCAT(LEFT(phone_col, 3), '-****-', RIGHT(phone_col, 4))
      ELSE '***-****-****'
    END;

주민번호 마스킹

CREATE OR REPLACE FUNCTION production.policies.mask_ssn(ssn_col STRING)
  RETURNS STRING
  COMMENT '주민등록번호를 역할에 따라 마스킹합니다'
  RETURN
    CASE
      WHEN IS_ACCOUNT_GROUP_MEMBER('admin') THEN ssn_col
      WHEN IS_ACCOUNT_GROUP_MEMBER('analysts') THEN
        CONCAT(LEFT(ssn_col, 8), '******')  -- 생년월일만 노출
      ELSE '******-*******'
    END;

카드번호 마스킹

CREATE OR REPLACE FUNCTION production.policies.mask_card_number(card_col STRING)
  RETURNS STRING
  COMMENT '카드번호를 마스킹합니다 (마지막 4자리만 노출)'
  RETURN
    CASE
      WHEN IS_ACCOUNT_GROUP_MEMBER('admin') THEN card_col
      ELSE CONCAT('****-****-****-', RIGHT(REGEXP_REPLACE(card_col, '[^0-9]', ''), 4))
    END;

테이블에 마스킹 적용

마스킹 함수 연결

-- 이메일 컬럼에 마스킹 적용
ALTER TABLE production.ecommerce.customers
  ALTER COLUMN email SET MASK production.policies.mask_email;

-- 전화번호 컬럼에 마스킹 적용
ALTER TABLE production.ecommerce.customers
  ALTER COLUMN phone SET MASK production.policies.mask_phone;

-- 주민번호 컬럼에 마스킹 적용
ALTER TABLE production.ecommerce.customers
  ALTER COLUMN ssn SET MASK production.policies.mask_ssn;

마스킹 해제

-- 특정 컬럼의 마스킹 제거
ALTER TABLE production.ecommerce.customers
  ALTER COLUMN email DROP MASK;

고급 패턴

패턴 1: CURRENT_USER 기반 마스킹

-- 본인 데이터는 원본, 타인 데이터는 마스킹
CREATE OR REPLACE FUNCTION production.policies.mask_salary(
  salary_col DECIMAL(10,2),
  owner_email STRING
)
  RETURNS DECIMAL(10,2)
  RETURN
    CASE
      WHEN IS_ACCOUNT_GROUP_MEMBER('admin') THEN salary_col
      WHEN CURRENT_USER() = owner_email THEN salary_col  -- 본인 급여는 원본
      ELSE NULL  -- 타인 급여는 NULL
    END;

-- 적용 (마스킹 함수에 다른 컬럼도 전달 가능)
ALTER TABLE production.hr.employees
  ALTER COLUMN salary SET MASK production.policies.mask_salary
  USING COLUMNS (email);
💡 USING COLUMNS 절을 사용하면 마스킹 함수에 같은 행의 다른 컬럼 값 을 추가 인자로 전달할 수 있습니다.

패턴 2: 해싱(Hashing) 마스킹

-- 원본 대신 해시값을 반환 (분석용 조인키로 활용 가능)
CREATE OR REPLACE FUNCTION production.policies.hash_email(email_col STRING)
  RETURNS STRING
  RETURN
    CASE
      WHEN IS_ACCOUNT_GROUP_MEMBER('admin') THEN email_col
      ELSE SHA2(email_col, 256)  -- 해시값으로 대체
    END;
해싱 마스킹은 원본을 볼 수 없지만, 동일 값은 동일 해시를 생성 하므로 분석가가 고유 사용자 수 파악이나 테이블 간 조인에 활용할 수 있습니다.

패턴 3: 범위형 마스킹 (숫자)

-- 정확한 나이 대신 연령대로 표시
CREATE OR REPLACE FUNCTION production.policies.mask_age(age_col INT)
  RETURNS STRING
  RETURN
    CASE
      WHEN IS_ACCOUNT_GROUP_MEMBER('admin') THEN CAST(age_col AS STRING)
      ELSE CONCAT(CAST(FLOOR(age_col / 10) * 10 AS STRING), '대')
    END;

마스킹 확인 및 모니터링

-- 테이블에 적용된 마스킹 함수 확인
DESCRIBE TABLE EXTENDED production.ecommerce.customers;

-- 마스킹이 올바르게 동작하는지 테스트
-- (관리자 계정으로 실행)
SELECT email, phone FROM production.ecommerce.customers LIMIT 5;

-- 특정 사용자가 보는 결과를 시뮬레이션하려면
-- 해당 사용자 계정으로 직접 쿼리를 실행해야 합니다

주의사항 및 제한

항목설명
컬럼당 하나하나의 컬럼에 하나의 마스킹 함수만 적용할 수 있습니다
반환 타입 일치마스킹 함수의 반환 타입이 컬럼의 데이터 타입과 일치해야 합니다
소유자 면제테이블 소유자(OWNER)에게는 마스킹이 적용되지 않습니다
성능마스킹 함수는 행 단위로 실행되므로, 대량 데이터에서는 성능에 주의합니다
WHERE 절 영향마스킹된 컬럼으로 WHERE 절 필터링 시, 마스킹된 값으로 비교됩니다
Managed Table만외부 테이블에는 컬럼 마스킹을 적용할 수 없습니다

행 필터 + 컬럼 마스킹 조합

행 필터와 컬럼 마스킹을 함께 적용 하면 더욱 정밀한 보안을 구현할 수 있습니다.
-- 행 필터: 지역별 데이터 제한
ALTER TABLE production.ecommerce.customers
  SET ROW FILTER production.policies.region_row_filter ON (region);

-- 컬럼 마스킹: 이메일, 전화번호 마스킹
ALTER TABLE production.ecommerce.customers
  ALTER COLUMN email SET MASK production.policies.mask_email;

ALTER TABLE production.ecommerce.customers
  ALTER COLUMN phone SET MASK production.policies.mask_phone;

-- 결과: 서울 담당 분석가는
-- 1) 서울 지역 고객만 보이고 (행 필터)
-- 2) 이메일/전화번호는 부분 마스킹됩니다 (컬럼 마스킹)

모범 사례

원칙설명
정책 스키마 분리마스킹 함수를 production.policies 스키마에 모아 관리합니다
일관된 마스킹 수준조직 전체에서 동일한 PII 유형에 동일한 마스킹 패턴을 적용합니다
COMMENT 필수각 함수에 목적, 대상 그룹, 마스킹 수준을 명시합니다
태그 연동PII 태그가 있는 컬럼에 자동 마스킹을 적용합니다 (태그 참조)
정기 검증새 그룹이 추가되면 마스킹 함수도 업데이트합니다

실전 인사이트: 이메일 마스킹을 안 했다가 GDPR 벌금 이야기

유럽 시장을 대상으로 서비스하는 한 이커머스 기업의 사례입니다. 분석 팀이 마케팅 세그먼트 분석을 위해 고객 데이터를 쿼리하는데, 이메일 주소가 마스킹 없이 그대로 노출 되고 있었습니다. 내부 분석 목적이라 큰 문제가 없다고 생각했지만, GDPR 감사에서 다음이 지적되었습니다:
지적 사항내용
필요 이상의 개인정보 접근마케팅 세그먼트 분석에 이메일 원본이 필요하지 않음
접근 통제 미흡분석가 50명 전원이 동일한 수준의 PII 접근 권한 보유
감사 추적 부재누가 언제 어떤 고객의 이메일을 조회했는지 추적 불가
GDPR 위반으로 연 매출의 2%에 해당하는 벌금 이 부과될 수 있다는 경고를 받고, 2주 만에 긴급 마스킹을 적용했습니다.
⚠️ 핵심 교훈: “내부 분석용이니까 괜찮겠지”는 GDPR, 개인정보보호법 등에서 통하지 않습니다. 데이터 최소화 원칙(Data Minimization) 에 따라, 업무 수행에 필요한 최소한의 정보만 노출해야 합니다. 마케팅 분석에는 해시된 이메일로 충분하고, CS 팀에는 부분 마스킹이면 충분합니다.

실전 인사이트: 다단계 마스킹의 실전 설계

현실적인 마스킹 등급 설계

이론적으로는 3단계(관리자/분석가/일반)가 깔끔하지만, 실제 조직에서는 더 세분화된 등급이 필요합니다.
등급대상 역할이메일전화번호카드번호주민번호
L0 (원본)DPO, 법무팀, 보안감사원본원본원본원본
L1 (부분)CS 팀, 운영자al***@company.com010-****-5678****-5678생년월일만
L2 (분석용)데이터 분석가, ML 엔지니어SHA256 해시NULLNULLNULL
L3 (완전)외부 파트너, BI 뷰어************
-- 4단계 마스킹 함수 예시
CREATE OR REPLACE FUNCTION production.policies.mask_email_v2(email_col STRING)
  RETURNS STRING
  RETURN
    CASE
      -- L0: DPO, 법무팀은 원본 확인 가능
      WHEN IS_ACCOUNT_GROUP_MEMBER('data_protection_officer') THEN email_col
      -- L1: CS팀은 부분 마스킹 (고객 식별은 가능)
      WHEN IS_ACCOUNT_GROUP_MEMBER('customer_support') THEN
        CONCAT(LEFT(email_col, 2), '***@', SPLIT(email_col, '@')[1])
      -- L2: 분석가는 해시값 (조인키로 활용 가능)
      WHEN IS_ACCOUNT_GROUP_MEMBER('analysts') THEN
        SHA2(LOWER(TRIM(email_col)), 256)
      -- L3: 그 외는 완전 마스킹
      ELSE '***@***.***'
    END;
💡 L2 분석용 해시 마스킹의 핵심: 분석가에게 해시값을 반환하면, 원본 이메일은 볼 수 없지만 동일 이메일은 동일 해시 를 생성하므로 고유 사용자 수 집계, 테이블 간 조인이 가능합니다. 반드시 LOWER(TRIM())을 적용하여 “Alice@Company.com”과 “alice@company.com”이 같은 해시가 되도록 정규화하세요.

마스킹 적용 시 흔한 실수들

실수결과해결 방법
IS_ACCOUNT_GROUP_MEMBER 오타모든 사용자에게 완전 마스킹 적용 (ELSE 절로 빠짐)적용 후 각 그룹별 테스트 쿼리를 반드시 실행하세요
테이블 OWNER에게도 마스킹 기대OWNER에게는 마스킹이 적용되지 않음OWNER를 서비스 프린시펄로 설정하여 실제 사람은 OWNER가 되지 않도록 합니다
마스킹된 값으로 WHERE 필터링WHERE email = 'alice@company.com'이 분석가에게는 항상 0건 반환마스킹된 컬럼은 필터 조건으로 사용하지 마세요. 별도 인덱스 컬럼을 활용합니다
마스킹 함수 없이 뷰로 대체뷰를 우회하여 원본 테이블에 직접 접근 가능뷰가 아닌 컬럼 마스킹을 사용해야 원본 테이블 접근 시에도 마스킹이 적용됩니다

실전 인사이트: 마스킹 함수의 성능 영향 측정

마스킹 함수는 행 단위로 실행 되므로, 대량 데이터에서 성능 영향이 있을 수 있습니다. 실측 결과를 공유합니다.

성능 벤치마크 (1억 행 테이블 기준)

쿼리 유형마스킹 없음마스킹 적용 (CASE + 문자열 함수)마스킹 적용 (SHA256 해시)오버헤드
SELECT COUNT(*)1.2초1.2초1.2초0% (마스킹 컬럼 미사용)
SELECT email LIMIT 10000.3초0.4초0.5초30~60%
SELECT * FROM ... GROUP BY region8.5초9.1초9.8초7~15%
Full table scan (모든 컬럼)45초52초58초15~29%
💡 실전 최적화 팁:
  • 마스킹된 컬럼을 SELECT *로 항상 가져오지 말고, 필요한 컬럼만 명시 하세요
  • 집계 쿼리에서 마스킹된 컬럼이 필요 없다면 제외하면 성능 저하가 거의 없습니다
  • SHA256 해시는 단순 문자열 치환보다 비용이 높으므로, 성능이 중요한 배치 작업에서는 미리 해시된 컬럼을 별도로 생성 하는 것도 방법입니다
  • 마스킹 함수 내에서 IS_ACCOUNT_GROUP_MEMBER()는 세션당 1번만 평가되므로, 이 부분의 오버헤드는 무시할 수 있습니다

정리

핵심 개념설명
Column MaskSQL 함수로 컬럼 값을 동적으로 마스킹합니다
다단계 마스킹관리자/분석가/일반 사용자에게 다른 수준의 마스킹을 적용합니다
USING COLUMNS같은 행의 다른 컬럼을 마스킹 함수의 추가 인자로 전달합니다
해싱 마스킹원본 대신 해시값을 반환하여 분석 가능성을 유지합니다
행 필터 + 마스킹두 기능을 조합하여 정밀한 보안을 구현합니다

참고 링크