관리자 페이지나 화면 모니터링 로그를 개발하다가 사용자의 이메일, 전화번호, 이름 같은 민감한 개인정보가 그대로 화면에 노출되는 상황을 경험해봤을까?
관리자 페이지나 시스템 로그 파일에 개인정보를 날것(Raw Data) 그대로 남겨두는 것은 개인정보보호법 및 ISMS 인증 기준 위반일 뿐만 아니라 심각한 데이터 유출 사고로 이어질 수 있다. 다만 매번 화면을 그릴 때마다 정규식을 직접 작성하여 이메일이나 전화번호를 마스킹하는 것은 여간 번거로운 일이 아니다.
이번에는 문자열 유형(이메일, 전화번호, 이름)을 자동으로 감지하거나 지정하여 안전하게 마스킹해주는 PHP 서포트 함수가 정확히 뭔지, 왜 필요한지, 어떻게 작성해서 실무에 활용하는지 완벽하게 정리해서 소개하겠다.
개인정보 마스킹은 데이터를 식별할 수 없도록 일부 문자열을 '*'(아스타리스크) 등의 대체 문자로 가리는 작업이다.
실무에서 가장 자주 다루는 데이터 형태는 이름, 전화번호, 이메일 세 가지다. 각각의 문자열은 구조와 길이가 다르기 때문에 일률적으로 처리하면 한글 깨짐이나 이메일 도메인 손상 등의 문제가 발생할 수 있다.
| 데이터 유형 | 원본 데이터 예시 | 마스킹 처리 결과 | 마스킹 처리 규칙 |
|---|---|---|---|
| 이름 (한글/영문) | 홍길동 / 남궁길동 | 홍*동 / 남궁**동 | 외자(2자)는 뒷글자, 3자 이상은 성과 마지막 글자 외 중앙 마스킹 |
| 전화번호 | 010-1234-5678 | 010-****-5678 | 중간 자릿수(3~4자리) 전체 마스킹 (하이픈 유무 무관) |
| 이메일 | user1234@domain.com | us****@domain.com | 아이디 앞 2자리 유지 후 남은 아이디 마스킹, 도메인 보존 |
다만 대부분의 개발자들은 단순 `substr()` 함수를 사용하거나 고정된 길이로 문자열을 자르는 방식을 사용하곤 한다. 이 경우 멀티바이트(한글) 문자가 깨지거나 이메일 아이디가 짧은 경우 도메인 영역까지 가려지는 치명적인 버그가 생긴다.
✗ 잘못된 코드 (한글 깨짐 및 짧은 이메일 처리 오류)
<?php
// 1. 단순 substr 사용 시 한글 깨짐 발생
$name = "홍길동";
$masked_name = substr($name, 0, 1) . "*" . substr($name, 2);
// 출력: 홍* (한글이 UTF-8 3바이트이므로 문자가 깨짐)
// 2. 고정 길이 마스킹 시 아이디가 짧으면 도메인 영역이 침범됨
$email = "ab@domain.com";
$parts = explode('@', $email);
$masked_email = substr($parts[0], 0, 2) . "***@" . $parts[1];
// 출력: ab***@domain.com (아이디 'ab'가 그대로 노출된 채 별표만 붙음)
?>✓ 올바른 코드 (mb_string 및 유연한 길이 체크 적용)
<?php
// 1. mb_substr 함수를 사용하여 멀티바이트 한글 안전 처리
$name = "홍길동";
$length = mb_strlen($name, 'UTF-8');
$masked_name = mb_substr($name, 0, 1, 'UTF-8') . '*' . mb_substr($name, 2, 1, 'UTF-8');
// 출력: 홍*동
// 2. 이메일 아이디 길이에 맞춘 가변 마스킹
$email = "ab@domain.com";
list($id, $domain) = explode('@', $email);
$id_len = mb_strlen($id, 'UTF-8');
$masked_id = ($id_len <= 2) ? mb_substr($id, 0, 1, 'UTF-8') . '*' : mb_substr($id, 0, 2, 'UTF-8') . str_repeat('*', $id_len - 2);
$masked_email = $masked_id . '@' . $domain;
// 출력: a*@domain.com
?>
실무에서 유용하게 쓸 수 있도록 자동 타입 감지 기능과 개별 타입 지정 기능을 갖춘 공통 서포트 함수 `mask_user_data()`를 구현해보자.
<?php
/**
* 개인정보 문자열(이름, 전화번호, 이메일)을 감지하여 안전하게 마스킹 처리하는 서포트 함수
*
* @param string $string 마스킹할 원본 문자열
* @param string $type 마스킹 유형 ('auto', 'name', 'phone', 'email')
* @return string 마스킹 완료된 문자열
*/
function mask_user_data($string, $type = 'auto') {
$string = trim((string)$string);
if (empty($string)) {
return '';
}
// 자동 타입 판별
if ($type === 'auto') {
if (filter_var($string, FILTER_VALIDATE_EMAIL)) {
$type = 'email';
} elseif (preg_match('/^[0-9]{2,3}-?[0-9]{3,4}-?[0-9]{4}$/', $string)) {
$type = 'phone';
} else {
$type = 'name';
}
}
switch ($type) {
case 'email':
$parts = explode('@', $string, 2);
if (count($parts) !== 2) return $string;
$id = $parts[0];
$domain = $parts[1];
$len = mb_strlen($id, 'UTF-8');
if ($len <= 2) {
$masked_id = mb_substr($id, 0, 1, 'UTF-8') . '*';
} else {
$masked_id = mb_substr($id, 0, 2, 'UTF-8') . str_repeat('*', $len - 2);
}
return $masked_id . '@' . $domain;
case 'phone':
// 숫자만 추출
$clean_phone = preg_replace('/[^0-9]/', '', $string);
$len = strlen($clean_phone);
if ($len === 11) { // 01012345678
return preg_replace('/^(d{3})(d{4})(d{4})$/', '$1-****-$3', $clean_phone);
} elseif ($len === 10) { // 0101235678 또는 0212345678
if (substr($clean_phone, 0, 2) === '02') {
return preg_replace('/^(d{2})(d{4})(d{4})$/', '$1-****-$3', $clean_phone);
}
return preg_replace('/^(d{3})(d{3})(d{4})$/', '$1-***-$3', $clean_phone);
}
return $string; // 규격에 맞지 않으면 원본 반환
case 'name':
default:
$len = mb_strlen($string, 'UTF-8');
if ($len <= 1) {
return $string;
} elseif ($len === 2) {
return mb_substr($string, 0, 1, 'UTF-8') . '*';
} else {
$first = mb_substr($string, 0, 1, 'UTF-8');
$last = mb_substr($string, -1, 1, 'UTF-8');
$middle = str_repeat('*', $len - 2);
return $first . $middle . $last;
}
}
}
?>
작성한 `mask_user_data()` 서포트 함수를 실제 회원 데이터 출력 및 로그 기록 시점에 적용하는 방법이다.
<?php
$user_list = [
['name' => '홍길동', 'phone' => '010-1234-5678', 'email' => 'hong@example.com'],
['name' => '남궁성', 'phone' => '01098765432', 'email' => 'ab@test.co.kr'],
['name' => 'John Doe', 'phone' => '02-123-4567', 'email' => 'johndoe1004@gmail.com']
];
foreach ($user_list as $user) {
echo "이름: " . mask_user_data($user['name']) . "
";
echo "전화번호: " . mask_user_data($user['phone']) . "
";
echo "이메일: " . mask_user_data($user['email']) . "
";
echo "-----------------------------------
";
}
?>이름: 홍*동
전화번호: 010-****-5678
이메일: ho**@example.com
-----------------------------------
이름: 남*성
전화번호: 010-****-5432
이메일: a*@test.co.kr
-----------------------------------
이름: J******e
전화번호: 02-***-4567
이메일: jo********@gmail.com
-----------------------------------
서포트 함수를 개발할 때 흔히 범하는 실수 중 하나는 DB에 저장되는 원본 데이터 자체를 마스킹하여 저장하는 것이다.
✗ 잘못된 사용법 (데이터베이스 변경)
<?php
// DB에 저장할 데이터 자체를 마스킹하면 원본 복구가 불가능해짐!
$masked_email = mask_user_data($_POST['email']);
$db->query("UPDATE users SET email = '{$masked_email}' WHERE id = 1");
?>✓ 올바른 사용법 (출력 및 로그 레이어에서만 적용)
<?php
// DB에는 원본 데이터를 그대로 유지하고, 뷰(HTML)나 로그파일에 기록할 때만 서포트 함수 사용
$user = $db->findUser(1);
// 화면 출력 시점에만 호출
echo "<td>" . htmlspecialchars(mask_user_data($user['email'])) . "</td>";
// 시스템 로그 기록 시점에만 호출
error_log("User logged in: " . mask_user_data($user['phone']));
?>
개인정보 마스킹은 웹 서비스의 보안과 사용자 신뢰를 지키기 위한 핵심 기본기다. 개발 편의를 이유로 로그나 화면에 날것의 데이터를 출력하던 작은 습관을 바꾸는 것이 모여서 서비스 전체의 강력한 보안을 만든다는 점을 잊지 말자. 이 글의 `mask_user_data()` 서포트 함수 코드를 참고해 공통 라이브러리 파일에 추가해 두면, 개인정보 유출 리스크 없이 안전하고 깔끔한 웹 서비스를 구축할 수 있을 것이다.