Xplenty에서 개인 정보를 마스킹하는 방법

최근들어 사회적으로 개인정보의 취급이 엄격해지고 있습니다. 사외용 뿐만 아니라 사내용의 정보 전송에도 당연한 것처럼 개인정보의 유출이 없는지 주의를 하고 있습니다.

시스템의 고가용성을 유지하기 위해 도입한 마이크로서비스 아키텍처는, 같은 정보(형태와 의미를 모두 포함해)가 여러개의 시스템에서 관리되고 있습니다. 아키텍처의 특징에 의한 개인정보를 포함한 다양한 중복 정보의 존재는 데이터베이스 1곳에 모아 관리하는 모놀리식 아키텍처보다 훨씬 번잡해졌습니다. 또, AI나 데이터 사이언스의 대두에 의한 데이터 분석 기반의 구축으로 회사가 취급하고 있는 데이터를 1곳에 모으는 일도 매우 큰 과제가 되었습니다.

저희 Xplenty에서는, 다수의 시스템에 흩어져 있는 다양한 형태의 정보를 수집해, 집계나 결합등의 데이터 변환으로 고부가가치의 정보로의 변환에 적합한 SaaS형 서비스입니다. 이 점을 적극적으로 활용함으로써 회사가 관리하는 특정 개인정보에 통일된 보호정책을 유지 할 수 있습니다.

이번 블로그에서는 셔플, 부분 숨김, 암호화에 의한 개인정보의 보호책을 Xplenty로 어떻게 구현하는지 살펴보겠습니다.

개인정보 마스킹 유형

개인정보의 보호를 위한 방법은 아래와 같은 3종류의 방법이 있습니다.
각 방법에는 장점과 단점이 있으며 용도에 따라 사용할 필요가 있습니다.

  1. 셔플
    • 데이터의 문자나 숫자를 바꾸거나 순서를 변경
    • 프로덕션 데이터를 테스트 데이터로 제공하는 경우 유효
    • 의미를 유지하며 대체하기 위해 변환방법이 따로 필요
  2. 부분 숨김
    • 데이터의 일부를 의미 없는 문자로 대체
    • 형식은 유지 및 특정 부분(원래 값을 대체해야 함)만 표시하는 경우에 유효
    • 셔플과 마찬가지로 의미를 가진 부분에 대한 대체방법이 필요
  3. 암호화
    • 단방향 해시 함수등을 사용하여 다른 형태의 데이터로 변환
    • 종류가 적은 데이터의 원형을 파악할 수 없게 하는 경우에 유효
    • 암호화를 풀기 힘든 함수를 사용하는 것이 중요

개인정보 마스킹 실행 장소

개인정보의 보호를 위한 데이터 변환은 select 컴포넌트에서 이루어집니다.

thumbnail image

select 컴포넌트 안의 설정은 아래와 같습니다.
thumbnail image

thumbnail image

또한 각 필드의 설정은 그림의 빨간색 화살표가 표시된 부분을 클릭하면 나타나는 아래의 Expression Editor에서 이루어집니다. 물론 Expression에 해당 함수식을 복사해도 문제 없습니다.

thumbnail image

개인정보 마스킹 예

예제에 사용된 모든 데이터는 SmartHR의 샌드박스 환경에서 가져온 것입니다.

SmartHR의 샌드박스 환경과 API에 대해서는SmartHR API Specifications를 참조해 주시면 감사하겠습니다.

직원 번호 (숫자 전용)

원래 데이터 셔플 부분 숨김
00008 00903 ##008
00023 03103 0##23
00026 03104 000##
00025 03304 000##
00017 02502 ##017
00006 01400 000##
00001 00104 ##001
00024 03201 ##024
00019 02701 ##019
00018 02504 0##18
● 셔플
SPRINTF('%03d%02d',
	(int)SUBSTRING(emp_code, 3, Length(emp_code)) + (ROUND(RANDOM()*10) % 10),
	(int)SUBSTRING(emp_code, 0, 3) + (ROUND(RANDOM()*10) % 10)
)

● 부분 숨김
CASE ROUND(RANDOM() * 10) % 4 
WHEN 1 THEN 
	CONCAT(SUBSTRING(emp_code, 0, Length(emp_code)-2), '##')
WHEN 2 THEN 
	CONCAT(SUBSTRING(emp_code, 0, 1), '##', SUBSTRING(emp_code, Length(emp_code)-2, Length(emp_code)))
WHEN 3 THEN 
	CONCAT(SUBSTRING(emp_code, 0, 2), '##', SUBSTRING(emp_code, Length(emp_code)-1, Length(emp_code)))
ELSE
	CONCAT('##', SUBSTRING(emp_code, 2, Length(emp_code)))
END

이름(한자)

원래 데이터 셔플 부분 숨김
Last Name First Name Last Name First Name Last Name First Name
石井 玲佳 玲井 石佳 #井 玲#
古賀 祐奈 古奈 祐賀 #賀 #奈
菊池 舞麗 菊麗 舞池 菊#

舞#

湯浅 海琴 海浅 海浅 湯# #琴
高松 高暢 高# #
河野 咲枝 咲野 河枝 河# 咲#
上野 真綾 真野 上綾 #野 #綾
小山 泰治 泰山 小治 小# 泰#
大場 孝浩 大浩 大浩 #場 孝#
松原 敏雄 敏原 松雄 松# #雄
● 셔플(last_name, first_name)
CASE ROUND(RANDOM() * 10) % 2
WHEN 0 THEN
	CONCAT(SUBSTRING(last_name, 0, 1), SUBSTRING(first_name, 1, Length(first_name)))
ELSE
	CONCAT(SUBSTRING(Reverse(first_name), 1, Length(first_name)), SUBSTRING(Reverse(last_name), 0, 1))
END
CASE ROUND(RANDOM() * 10) % 2
WHEN 1 THEN
	CONCAT(SUBSTRING(first_name, 0, 1), SUBSTRING(last_name, 1, Length(last_name)))
ELSE
	CONCAT( SUBSTRING(Reverse(last_name), 1, Length(last_name)), SUBSTRING(Reverse(first_name), 0, 1))
END

● 부분 숨김(last_name, first_name)
CASE ROUND(RANDOM() * 10) % 2
WHEN 0 THEN
	SPRINTF('%s%s', SUBSTRING(last_name, 0, 1), REPLACE(SUBSTRING(last_name, 1, Length(last_name)), '.', '#'))
ELSE
	SPRINTF('%s%s', REPLACE(SUBSTRING(last_name, 0, Length(last_name)-1), '.', '#'), SUBSTRING(last_name, Length(last_name)-1, Length(last_name)))
END
CASE ROUND(RANDOM() * 10) % 2
WHEN 1
	THEN SPRINTF('%s%s', SUBSTRING(first_name, 0, 1), REPLACE(SUBSTRING(first_name, 1, Length(first_name)), '.', '#'))
ELSE
	SPRINTF('%s%s', REPLACE(SUBSTRING(first_name, 0, Length(first_name)-1), '.', '#'), SUBSTRING(first_name, Length(first_name)-1, Length(first_name)))
END

생일(년월일)

원래 데이터 부분 숨김
1948-12-22 19**--
● 部分隠し
CONCAT(
    SUBSTRING(birth_at, 0, 2), 
    REPLACE(SUBSTRING(birth_at, 2, Length(birth_at)), '[0-9]', '*')
)

성별

원래 데이터 부분 숨김
male 07cf4
female 273b9
● 暗号化
SUBSTRING(MD5(gender), 0, 5)

# MD5대신에 SHA256등의 다른 단방향 암호화 함수를 사용해도 됨

전화번호

원래 데이터 셔플 부분 숨김
090-5302-5398 090-2053-8953 090--
070-4596-8665 070-6945-5686 070--
070-7119-7796 070-9171-6977 070--
090-3071-1160 090-1730-0611 090--
070-1688-0980 070-8816-0809 070--
090-8426-7235 090-6284-5372 090--
070-7213-2746 070-3172-6427 070--
090-8151-8277 090-1581-7782 090--
090-3304-2880 090-4033-0828 090--
080-5377-6182 080-7753-2861 080--
● シャッフル
SPRINTF('%s-%s%s-%s%s',
	STRSPLIT(tel_number, '-').$0,
	SUBSTRING(Reverse(STRSPLIT(tel_number, '-').$1), 0, 2), 
	SUBSTRING(STRSPLIT(tel_number, '-').$1, 0, 2),
	SUBSTRING(Reverse(STRSPLIT(tel_number, '-').$2), 0, 2),
	SUBSTRING(STRSPLIT(tel_number, '-').$2, 0, 2)
)

● 部分隠し
CONCAT(STRSPLIT(tel_number, '-').$0, '-',
	REPLACE(STRSPLIT(tel_number, '-').$1, '[0-9]', '*'),'-', 
	REPLACE(STRSPLIT(tel_number, '-').$2, '[0-9]', '*')
)

우편번호

원래 데이터 부분 숨김
567-0896 #6#-#896
480-1122 #8#-11##
484-0863 4#4-0###
414-0026 4#4-002#
409-2946 ##9-#9#6
347-0104 #47-0#04
938-0178 9##-0#7#
950-1113 9##-1113
964-0935 9#4-09##
957-0066 9##-##66
● 部分隠し
(ROUND(RANDOM()*10) % 2 == 0 ?
	REPLACE(address#'zip_code', '[13568]' ,'#') :
	REPLACE(address#'zip_code', '[02457]' ,'#')
)

주소(번지)

원래 데이터 셔플_1 셔플_2 부분 숨김
4-23-11 4-11-23 10-27-14 4-2#-##
7-22-7 7-22-7 16-29-9 #-22-#
7-5-11 7-11-5 7-14-13 7-5-11
1-13-19 1-31-91 6-20-26 #-##-##
3-9-29 3-9-92 5-15-37 3-9-#9
7-27-30 7-72-3 9-34-39 7-#7-3#
8-22-21 8-22-12 8-23-25 8-22-2#
6-4-17 6-17-4 13-11-19 6-4-##
1-26-16 1-16-26 10-27-23 #-26-#6
7-23-10 7-10-23 14-27-19 7-#3-1#
● シャッフル_1
(ROUND(RANDOM()*10) % 2 == 0 ?
	SPRINTF('%s-%d-%d',
		STRSPLIT(address#'street', '-').$0,
		(int)STRSPLIT(address#'street', '-').$2,
		(int)STRSPLIT(address#'street', '-').$1
	) :
	SPRINTF('%s-%d-%d',
		STRSPLIT(address#'street', '-').$0,
		(int)Reverse(STRSPLIT(address#'street', '-').$1),
		(int)Reverse(STRSPLIT(address#'street', '-').$2)
	)
)

● シャッフル_2
SPRINTF('%d-%d-%d',
	(int)STRSPLIT(address#'street', '-').$0 + (ROUND(RANDOM()*10) %10),
	(int)STRSPLIT(address#'street', '-').$1 + (ROUND(RANDOM()*10) %10),
	(int)STRSPLIT(address#'street', '-').$2 + (ROUND(RANDOM()*10) %10)
)

● 部分隠し
(ROUND(RANDOM()*10) % 2 == 0 ?
	REPLACE(address#'street', '[13579]' ,'#') :
	REPLACE(address#'street', '[02468]' ,'#')
)

이메일 주소

원래 데이터 부분 숨김_1 부분 숨김_2
mailto:gaston.nikolaus@example.com ga*************@######.com ga*******@######.com
mailto:salvatore@example.com sa*******@######..com sa*******@######..com
mailto:bruno_abbott@example.net br**********@######.net br*******@######.net
mailto:arnold.shanahan@example.net ar*************@######.net ar*******@######.net
mailto:george@example.net ge****@######.net ge*******@######.net
mailto:celestina@example.org ce*******@######.org ce*******@######.org
mailto:avery.homenick@example.org av************@######.org av*******@######.org
mailto:tyson@example.net ty***@######.net ty*******@######.net
mailto:sang.hirthe@example.org sa*********@######.org sa*******@######.org
mailto:hunter@example.com hu****@######.com hu*******@######.com
● 部分隠し_1
SPRINTF('%s%s@%s%s',
	SUBSTRING(STRSPLIT(email, '@').$0, 0, 2),
	REPLACE(SUBSTRING(STRSPLIT(email, '@').$0, 2, Length(STRSPLIT(email, '@').$0)), '.', '*'),
	REPLACE(SUBSTRING(STRSPLIT(email, '@').$1, 0, LAST_INDEX_OF(STRSPLIT(email, '@').$1, '.') - 1), '.', '#' ),
	SUBSTRING(STRSPLIT(email, '@').$1, LAST_INDEX_OF(STRSPLIT(email, '@').$1, '.'), Length(STRSPLIT(email, '@').$1))
)

● 部分隠し_2
SPRINTF('%s%s@%s%s',
	SUBSTRING(STRSPLIT(email, '@').$0, 0, 2),
	'*******',
	'######',
	SUBSTRING(STRSPLIT(email, '@').$1, LAST_INDEX_OF(STRSPLIT(email, '@').$1, '.'), Length(STRSPLIT(email, '@').$1))
)

각종 사회보험번호

원래 데이터 셔플 부분 숨김_1 부분 숨김_2 부분 숨김_3
5063-196700-3 5107-007833-5 50##-#9#700-# 5**3-1####0-3 *0@@-@967@@-@
4598-668799-1 4603-997988-2 #59#-###799-1 4**8-6####9-1 *5@@-@687@@-@
4676-458881-5 4685-189071-8 ##7#-#5###1-5 4**6-4####1-5 *6@@-@588@@-@
5105-769271-1 5180-173803-8 5#05-7#927#-# 5**5-7####1-1 *1@@-@692@@-@
5323-496653-1 5358-356889-3 53#3-#9##53-1 5**3-4####3-1 *3@@-@966@@-@
8875-203981-7 8965-189912-7 88##-20##8#-# 8**5-2####1-7 *8@@-@039@@-@
8845-730489-5 8883-984208-6 ###5-73###9-5 8**5-7####9-5 *8@@-@304@@-@
2765-123674-2 2856-476816-9 27#5-#2##7#-2 2**5-1####4-2 *7@@-@236@@-@
8968-785929-6 9023-930160-4 8#68-#8####-6 8**8-7####9-6 *9@@-@859@@-@
4879-952886-9 4923-688784-6 ##79-952###-9 4**9-9####6-9 *8@@-@528@@-@
● シャッフル
SPRINTF('%04d-%06d-%d',
	((int)STRSPLIT(emp_ins_insured_person_number, '-').$0 + ROUND(RANDOM()*100)) % 10000,
	((int)Reverse(STRSPLIT(emp_ins_insured_person_number, '-').$1) + ROUND(RANDOM()*1000)) % 1000000,
	((int)STRSPLIT(emp_ins_insured_person_number, '-').$2 + ROUND(RANDOM()*10)) % 10
)

● 部分隠し_1
CASE ROUND(RANDOM() * 10) % 4 
WHEN 1 THEN
	REPLACE(emp_ins_insured_person_number, '[13579]', '#')
WHEN 2 THEN
	REPLACE(emp_ins_insured_person_number, '[02579]', '#')
WHEN 3 THEN
	REPLACE(emp_ins_insured_person_number, '[13468]', '#')
ELSE
	REPLACE(emp_ins_insured_person_number, '[02468]', '#')
END

● 部分隠し_2
SPRINTF('%s%s%s-%s%s%s-%s',
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)').$0,
	REPLACE(REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)').$1, '.', '*'),
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)').$2,
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)').$3,
	REPLACE(REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)').$4, '.', '#'),
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)').$5,
	REGEX_EXTRACT_ALL(emp_ins_insured_person_number, '(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)(\\\\d+)(\\\\d)-(\\\\d)').$6
)

● 部分隠し_3
REPLACE(REPLACE(emp_ins_insured_person_number, '\\\\d\\\\d-\\\\d', '@@-@'), '^\\\\d', '*')

요약

Xplenty의 select 컴포넌트에는 이러한 마스킹 외에도 다양한 기능을 구현할 수 있습니다. 한번 Xplenty의 공식 매뉴얼이나 X-console에서 기능을 확인해 보면 어떻습니까?
무료 평가판 신청을 통해 언제라도 Xplenty를 사용 해 보실 수 있습니다.