최근들어 사회적으로 개인정보의 취급이 엄격해지고 있습니다. 사외용 뿐만 아니라 사내용의 정보 전송에도 당연한 것처럼 개인정보의 유출이 없는지 주의를 하고 있습니다.
시스템의 고가용성을 유지하기 위해 도입한 마이크로서비스 아키텍처는, 같은 정보(형태와 의미를 모두 포함해)가 여러개의 시스템에서 관리되고 있습니다. 아키텍처의 특징에 의한 개인정보를 포함한 다양한 중복 정보의 존재는 데이터베이스 1곳에 모아 관리하는 모놀리식 아키텍처보다 훨씬 번잡해졌습니다. 또, AI나 데이터 사이언스의 대두에 의한 데이터 분석 기반의 구축으로 회사가 취급하고 있는 데이터를 1곳에 모으는 일도 매우 큰 과제가 되었습니다.
저희 Xplenty에서는, 다수의 시스템에 흩어져 있는 다양한 형태의 정보를 수집해, 집계나 결합등의 데이터 변환으로 고부가가치의 정보로의 변환에 적합한 SaaS형 서비스입니다. 이 점을 적극적으로 활용함으로써 회사가 관리하는 특정 개인정보에 통일된 보호정책을 유지 할 수 있습니다.
이번 블로그에서는 셔플, 부분 숨김, 암호화에 의한 개인정보의 보호책을 Xplenty로 어떻게 구현하는지 살펴보겠습니다.
개인정보 마스킹 유형
개인정보의 보호를 위한 방법은 아래와 같은 3종류의 방법이 있습니다.
각 방법에는 장점과 단점이 있으며 용도에 따라 사용할 필요가 있습니다.
- 셔플
- 데이터의 문자나 숫자를 바꾸거나 순서를 변경
- 프로덕션 데이터를 테스트 데이터로 제공하는 경우 유효
- 의미를 유지하며 대체하기 위해 변환방법이 따로 필요
- 부분 숨김
- 데이터의 일부를 의미 없는 문자로 대체
- 형식은 유지 및 특정 부분(원래 값을 대체해야 함)만 표시하는 경우에 유효
- 셔플과 마찬가지로 의미를 가진 부분에 대한 대체방법이 필요
- 암호화
- 단방향 해시 함수등을 사용하여 다른 형태의 데이터로 변환
- 종류가 적은 데이터의 원형을 파악할 수 없게 하는 경우에 유효
- 암호화를 풀기 힘든 함수를 사용하는 것이 중요
개인정보 마스킹 실행 장소
개인정보의 보호를 위한 데이터 변환은 select 컴포넌트에서 이루어집니다.
select 컴포넌트 안의 설정은 아래와 같습니다.
또한 각 필드의 설정은 그림의 빨간색 화살표가 표시된 부분을 클릭하면 나타나는 아래의 Expression Editor에서 이루어집니다. 물론 Expression에 해당 함수식을 복사해도 문제 없습니다.
개인정보 마스킹 예
예제에 사용된 모든 데이터는 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
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를 사용 해 보실 수 있습니다.