한국어 개인정보 모델을 학습하고 문서 검색에 연결하기
업무 문서에서 이름과 연락처를 지우고 나면, 검색에 필요한 정보도 함께 사라질 수 있습니다. 이름을 단서로 담당 부서를 찾거나 같은 조직의 업무를 모아 보는 경우가 그렇습니다.
문서 처리 프로젝트에서 저희가 풀어야 했던 문제도 여기에 있었습니다. 개인정보가 포함된 구간을 찾아 처리하면서, 문서를 다시 활용할 때 필요한 업무 문맥을 남겨야 했습니다. 한국어 개인정보 탐지 모델을 학습하고 서비스에 연결한 뒤에는 이름을 부서로 바꾸는 방식, 본문과 검색용 메타데이터를 분리하는 방식까지 검토했습니다.
이 글은 그 과정에서 선택한 구조와 실험하며 확인한 한계를 정리한 적용기입니다. 수치는 2026년 9월에 실행한 실험 기록을 기준으로 합니다. 서비스 연동 구현, 로컬 모델 평가, 가상 문서를 사용한 검색 실험은 각각의 근거 범위를 구분해 설명합니다.
공개 구현은 KoPII-Pipeline, 학습한 모델은 ko-pii-ner-roberta-base에서 확인할 수 있습니다.
개인정보 탐지와 치환 정책을 분리했습니다
문서에서 개인정보를 찾는 단계와 찾은 정보를 어떻게 바꿀지 결정하는 단계는 요구사항이 달랐습니다. 탐지기는 이름·전화번호 등의 유형과 원문 위치를 반환하고, 그 결과를 받은 서비스가 마스킹·가명·부서명 치환 중 허용한 정책을 적용하도록 나눴습니다.
예를 들어 다음은 설명용 가상 문장입니다.
처리 전: 김하늘이 배포 일정을 확인했습니다.
소속 조회 후: [이름:플랫폼개발팀 보호]이 배포 일정을 확인했습니다.
여기서 부서명은 인사·조직 정보 조회로 결정합니다. NER(Named Entity Recognition, 개체명 인식) 모델이 맡는 범위는 이름의 위치를 찾는 일입니다. 탐지한 이름으로 소속을 조회하고, 확인된 조직을 어떤 수준까지 표시할지는 서비스 정책이 맡습니다.
이 구분 덕분에 모델을 바꿀 때 인사정보 조회나 치환 로직을 함께 바꾸지 않아도 됐습니다. 반대로 조직이 개편됐을 때는 조회할 조직 정보를 갱신하면 됩니다. 직원 명부를 모델 가중치에 반영하는 방식으로 소속을 관리하지 않았습니다.
한국어 표현과 업무 문맥을 학습 데이터에 담았습니다
처음에는 공개 PII 모델을 도입 후보로 검토했습니다. 이름의 조사 경계, 한국식 식별번호, 업무 문맥에서의 오탐까지 확인할 필요가 있어 한국어 학습 모델을 만들고 같은 자료로 비교하는 작업을 진행했습니다. PII는 개인을 식별할 수 있는 정보를 뜻합니다.
데이터는 공개 자료와 업무 문서 합성을 결합했습니다. 공개 한국어 대화의 개인정보 주석을 활용하고, 한국어 문맥에 한국식 개인정보 값을 넣은 자료도 구성했습니다. 자체 합성에는 고객상담·사내메일·채용·공공민원 등의 문장 조각과 문서 양식을 사용했습니다.
합성할 때는 같은 문서 안에서 인물 정보가 일관되도록 하고, 이름 뒤에 붙는 조사도 고려했습니다. 주문번호나 제품명처럼 개인정보와 혼동하기 쉬운 표현은 개인정보가 없는 문서에도 넣었습니다. 숫자가 등장하는 문장을 전부 식별번호로 판단하면 이후 검색에 필요한 업무 정보까지 잃을 수 있기 때문입니다.
전체 데이터는 100,851건입니다. 학습 78,963건, 검증 9,994건, 내부 테스트 9,888건, KDPII 기반 별도 평가 2,006건으로 나눴습니다. 공개 원문과 표시된 값을 보존한 구간도 있으므로, 데이터 전체를 가상 개인정보로 새로 합성했다고 설명하지는 않습니다.
데이터 검사는 개인정보의 시작·끝 위치와 표면 문자열이 맞는지 확인하는 데서 시작했습니다. 같은 대화에서 파생된 자료는 그룹 단위로 나누고, 학습 계열 자료와 별도 평가 자료 사이의 동일 문장 중복도 점검했습니다. 다만 동일 문장을 제거했다고 비슷한 표현이나 템플릿까지 모두 독립적이라고 볼 수는 없습니다. 이 제약은 평가 결과를 해석할 때도 남겨 두었습니다.
파인튜닝에서 실제로 사용한 설정
기반 모델은 klue/roberta-base입니다. 20개 개인정보 유형을 BIO 형식으로 표현해 41개 클래스를 예측하는 토큰 분류 모델로 학습했습니다. BIO는 엔터티의 시작(B), 이어지는 부분(I), 엔터티가 아닌 부분(O)을 구분하는 표기입니다.
당시 실행은 1 epoch, batch size 32, learning rate 2e-5, 최대 입력 256토큰으로 진행했습니다. 인코더와 분류 헤드를 함께 갱신했고, 손실 함수는 Focal loss를 사용했습니다. 모델 선택에는 재현율에 더 큰 비중을 두는 F5를 적용했습니다. 개인정보 누락을 중요하게 본 선택이지만, F5로 모델을 골랐다는 사실만으로 누락을 막았다고 할 수는 없습니다.
학습 기록을 다시 확인하면서 설정 파일과 실제 실행 경로를 대조하는 일도 필요했습니다. 설정에는 O 클래스 가중치가 있었지만 실제 Focal loss 분기에는 전달되지 않았습니다. 그래서 이를 별도 가중치를 적용한 학습으로 기술하지 않았습니다. 학습 중 검증과 직후 테스트 역시 각각 2,000개 표본으로 실행했으며, 뒤에서 설명할 별도 비교 평가와 범위가 다릅니다.
또 하나 구분할 부분은 긴 입력입니다. 당시 학습은 256토큰을 넘는 부분을 잘랐습니다. 이후 서비스 추론에는 겹치는 창으로 문서 전체를 처리하는 방식을 넣었습니다. 추론 단계의 길이 대응을 학습 때도 적용한 것처럼 설명하지 않도록 실행 기록을 나눠 관리했습니다.
비교 점수가 낮으면 예측 경계부터 확인했습니다
초기 도입 후보였던 NVIDIA GLiNER-PII와 Fastino GLiNER2-PII를 한국어 RoBERTa와 비교했습니다. 한국어 대화 기반 홀드아웃 2,006건으로 탐지 성능을, 개인정보가 없는 합성 문서 580건으로 오탐을 확인했습니다. 세 모델 모두 같은 180자 창과 48자 겹침을 적용해 Apple M4 Pro의 MPS 환경에서 순차 실행했습니다.
아래는 9월 15일의 초기 비교입니다. Exact F1은 서로 대응하는 13개 개인정보 범주에서 시작·끝 위치와 유형이 모두 맞아야 정답으로 계산한 값입니다.
| 모델 | 공통 13개 범주 Exact F1 | 이름 Exact 재현율 | 비개인정보 문서 오탐 |
|---|---|---|---|
| 한국어 RoBERTa | 91.10% | 96.05% | 0 / 580 |
| NVIDIA GLiNER-PII | 8.73% | 12.43% | 559 / 580 |
| Fastino GLiNER2-PII | 19.87% | 18.36% | 182 / 580 |
차이가 커서 모델 점수뿐 아니라 추출 과정을 확인했습니다. 한국어에서는 김하늘의에서 김하늘만 찾아야 하는 상황이 자주 생깁니다. 설치한 두 GLiNER 라이브러리의 기본 단어 분리기로 정답 경계를 점검했더니, 이름 정답 354개 중 244개는 공통 입력 창 어디에서도 기본 단어 경계만으로 정확하게 표현할 수 없었습니다.
이 경우 이름이 포함된 어절을 찾아도 Exact 평가에서는 틀릴 수 있습니다. 초기 점수에는 가중치의 적합성뿐 아니라 라이브러리의 추출 단위도 반영돼 있었습니다. 두 공개 후보는 같은 한국어 자료로 추가 학습하지 않았고, 초기 비교에서 한국어 검증 자료로 라벨 표현이나 임계값을 최적화하지도 않았습니다. 이 표는 해당 조건에서의 적용 결과입니다.
후속 실험에서는 개발 자료 612건으로 후보별 30개 설정을 비교했습니다. 라벨 표현, 한국어 경계 처리, 임계값을 조합하고 개발 F1로 선택했습니다. 이 후속 실행 안에서 Fastino의 전체 F1은 19.45%에서 48.93%로 올랐지만 이름 재현율은 18.08%에서 6.21%로 낮아졌습니다. 새 실행기는 초기 비교와 처리 방식이 달라 초기 표의 숫자를 그대로 대체하지 않았습니다.
부서 조회에 사용할 이름을 찾는 것이 목적이라면 전체 F1만 보고 설정을 선택하기 어렵습니다. 저희는 전체 점수와 함께 이름 재현율, 개인정보 문자 재현율, 비개인정보 문서의 오탐을 따로 보게 됐습니다. 후속 설정 탐색은 이미 살펴본 평가 자료를 다시 사용한 결과이며, 미사용 외부 업무 문서에서의 검증은 남아 있습니다.
한국어 일반 NER 모델과의 추가 비교
다른 프로젝트에서 사용하는 soddokayo/koelectra-base-klue-ner도 비교했습니다. 이 모델의 사람 라벨 PS를 PERSON_NAME에 대응시켜, 같은 2,586건에서 이름만 별도로 평가했습니다. 장소를 상세 주소로, 일반 날짜를 생년월일로 바꿔 해석하지는 않았습니다.
| 모델 | 이름 정밀도 | 이름 재현율 | 이름 Exact F1 |
|---|---|---|---|
| 한국어 RoBERTa | 87.86% | 96.05% | 91.77% |
| KoELECTRA KLUE NER | 48.88% | 92.09% | 63.86% |
KoELECTRA도 이름을 많이 찾아냈습니다. 다만 이름 기준 오탐 341개 중 293개는 다른 개인정보 정답과 겹쳤습니다. 다른 개인정보를 이름으로 분류한 경우가 많았다는 뜻입니다. 이런 예측이 개인정보의 일부를 가릴 수도 있으므로, 모두 개인정보가 아닌 문구를 잘못 마스킹한 사례라고 해석하지 않았습니다. 위 표는 이름 유형과 경계의 정확도이며 전체 개인정보 보호율이 아닙니다.
두 비교 모두 프로젝트에서 관리하는 홀드아웃을 사용했습니다. 2,006건에는 대화 458건과 그 대화에서 나온 단문 1,548건이 함께 있습니다. 학습에 사용하지 않은 자료이지만 독립적인 외부 업무 코퍼스 검증은 아니며, 다른 서비스에서 같은 점수가 나올 것이라고 기대할 근거도 아닙니다.
서비스에서는 원문 위치와 처리 상태를 함께 관리했습니다
모델을 문서 처리 서비스에 연결하면서 중요했던 것은 결과를 원문의 어느 위치에 적용할지였습니다. 원문과 모델 입력의 토큰화가 달라지면 이름을 찾고도 다른 글자를 치환할 수 있습니다.
애플리케이션에는 모델과 같은 버전의 토크나이저와 라벨 설정을 두었습니다. 원문을 토큰 ID로 바꾼 뒤 원격 vLLM 서버의 토큰 분류 기능을 호출하고, 반환된 BIO 라벨을 원문 문자 위치로 복원했습니다. 토큰 ID 전송은 위치 정합성을 위한 선택이며 개인정보를 암호화하는 수단은 아닙니다.
긴 문서는 256토큰 창으로 나누고 32토큰을 겹쳐 처리했습니다. 같은 토큰에 여러 결과가 있으면 창 가장자리에서 더 멀리 떨어진 결과를 선택했습니다. 모델 응답의 토큰 수와 라벨 수, 확률값, 원문 범위를 검사하고 모든 창의 처리가 끝난 경우에만 결과를 사용하도록 했습니다.
일반 업로드 문서의 검수 경로는 다음과 같이 구성했습니다.
개인정보 모델은 GPU에서 추론하고, 검색 벡터는 기존 NPU 임베딩 경로가 담당하도록 역할을 나눴습니다. NER는 개인정보의 위치와 유형을 반환하고 임베딩은 문서와 질의를 검색할 벡터로 바꿉니다. 이번 모델 비교의 로컬 지연 시간을 운영 GPU·NPU 시스템의 응답 시간으로 사용하지는 않았습니다.
탐지 실패를 빈 결과와 구분하는 것도 중요했습니다. 서버가 응답하지 않았는데 개인정보가 없다고 처리하면 후속 단계로 원문이 넘어갈 수 있습니다. 탐지 실패 시 해당 실행을 중단하고, 수정한 페이지는 다시 점검하며, 개인정보 처리가 끝나지 않은 페이지는 승인되지 않도록 연결했습니다. 이 통제는 문서 본문 처리 단계에 적용한 것으로 원본 파일 보관과 앞단의 문서 분석까지 모두 비식별화했다는 뜻은 아닙니다.
다른 문서 수집 서비스로 확장할 때는 기존 암호화 볼트와 토큰 치환을 유지하고 탐지 결과를 연결하는 방식을 구현했습니다. 이 경로에서는 Python의 문자 위치와 JavaScript의 UTF-16 인덱스 차이도 확인했습니다. 이모지가 이름 앞에 있는 문장처럼 위치가 어긋나기 쉬운 사례를 검사하고, 치환할 원문과 탐지값이 맞지 않으면 처리를 멈추도록 했습니다. 해당 확장은 연동 코드와 자동 테스트까지 확인한 단계이며 실제 운영 서버를 경유한 전체 배포 검증과 구분합니다.
이름을 부서로 바꾸려면 조회 실패도 정책에 포함해야 했습니다
인사정보 조회 결과가 항상 하나의 부서로 정리되는 것은 아닙니다. 동명이인이 서로 다른 부서에 있거나, 명부가 오래됐거나, 외부인이 문서에 등장할 수 있습니다.
소속을 하나로 확인할 수 있을 때만 허용된 조직 수준으로 범주화했습니다. 확인할 수 없으면 설정에 따라 문서 단위 가명을 사용하고, 가명을 만들 조건도 없으면 이름 보호 표시를 적용했습니다. 서로 다른 사람이더라도 조회 결과가 같은 부서라면 부서 범주화는 가능하지만, 그 결과가 개인을 고유하게 식별했다는 뜻은 아닙니다.
탐지 실패와 조회 실패의 처리도 달랐습니다. 탐지 자체가 실패하면 어떤 개인정보가 남았는지 알 수 없어 후속 처리를 중단합니다. 이름은 탐지했지만 소속 조회에 실패했다면 이름을 보호한 상태로 부서 정보 없이 처리할 수 있습니다. 이 차이를 모델 응답과 조회 결과의 계약에 반영했습니다.
부서명이 문맥을 얼마나 남기는지 확인하려면 검색 실험이 필요했습니다. 동시에 부서명과 직급, 과제명이 함께 보일 때 개인을 다시 추정하기 쉬워지는지도 살펴봐야 했습니다.
검색에 필요한 정보를 어디에 둘 것인가
후속 실험에서는 가상 조직 3개와 문서 720개를 만들었습니다. 실제 직원이나 고객사 문서를 사용한 실험은 아닙니다. 같은 이름 탐지 결과에서 전체 마스킹, 본문에 팀명 표시, 본문 마스킹과 별도 팀 메타데이터 필터를 비교했습니다.
아래 표는 이름 탐지 오류의 영향을 분리하기 위해 정답 이름 구간을 사용한 조건입니다. 검색은 문자 bigram BM25로 평가했고, 팀을 지정한 질의에서 관련 문서 중 상위 5개에 포함된 비율을 Recall@5로 계산했습니다. 개인 추정은 조직 명부·직급·프로젝트를 아는 공격자가 처리된 본문만 보고 후보를 좁히는 가정입니다.
| 처리 방식 | 팀 질의 Recall@5 | 본문만 관찰할 때 개인 추정 기대 성공률 |
|---|---|---|
| 이름 마스킹 | 16.73% | 26.39% |
| 본문에 팀명 표시 | 81.30% | 73.21% |
| 본문 마스킹 + 별도 팀 메타데이터 필터 | 80.38% | 26.39% |
이 합성 과제에서는 팀명이 검색에 유용한 단서였습니다. 본문에 표시했을 때는 공격자가 사용할 단서도 늘어났습니다. 본문과 메타데이터를 분리한 조건에서는 본문에 팀명을 넣지 않고도 비슷한 검색 점수를 얻었습니다. 통계적 동등성을 검정한 결과는 아닙니다.
메타데이터를 분리했다고 추정 위험이 사라지지는 않았습니다. 소속 조회에 성공한 동일한 636개 문서만 비교했을 때, 메타데이터 정책의 개인 추정 기대 성공률은 본문만 보면 26.66%였지만 검색에 사용된 팀 필터까지 알면 79.66%로 높아졌습니다. 같은 부분집합의 본문 팀명 표시 조건과 같은 값입니다. 본문 공개 범위와 검색 필터·메타데이터의 접근 범위를 함께 설계해야 하는 이유입니다.
실제 NER 출력을 사용한 실험에서는 별도의 문제도 보였습니다. 모델이 가상의 업무참조 문자열을 여권번호로 오인했고, 이를 치환하면서 참조 번호로 문서를 찾는 성능이 나빠졌습니다. 탐지 지표에 더해 처리된 문서가 원래의 업무 질의에 답할 수 있는지도 점검해야 했습니다.
이 결과는 고정 템플릿과 제한된 질의, 후보를 좁히는 공격자 가정에서 나온 값입니다. 실제 임베딩 검색이나 생성형 답변의 품질, 현실의 개인 재식별 확률을 측정한 수치로 사용하지 않았습니다.
프로젝트의 선택을 재사용할 수 있는 구조로 남겼습니다
프로젝트에서 나눈 책임은 범용 패키지에도 반영했습니다. 탐지기는 원문 구간을 반환하고, 외부 속성 조회기는 그 구간의 값으로 필요한 속성을 찾으며, 정책은 본문에 무엇을 남길지 결정합니다. 검색기는 처리된 본문과 허용된 메타데이터를 사용합니다.
조직의 팀·본부·센터 계층은 코어에 고정하지 않았습니다. 다른 프로젝트에서는 업무 영역이나 지역 등 필요한 속성을 연결할 수 있게 했습니다. 문서 접근 범위는 애플리케이션이 결정해 전달하도록 했고, 조회 결과가 없거나 필터가 허용되지 않았을 때 전체 문서 검색으로 넓히지 않도록 했습니다. 이 라이브러리 자체가 제품 인증과 문서 권한을 모두 구현하는 것은 아닙니다.
이번 작업에서 계속 확인하게 된 것은 각 단계의 출력이 다음 단계에서 어떤 의미로 쓰이는지였습니다. 이름을 정확하게 찾아도 소속 조회 결과가 틀리면 잘못된 부서가 붙습니다. 개인정보를 넓게 가리는 정책은 검색 단서를 없앨 수 있습니다. 본문에서 감춘 조직 정보가 검색 필터를 통해 다시 드러날 수도 있습니다.
다음 검증은 실제 업무 문서에서 유형별 누락과 오탐, 소속 조회 정확도, 처리 후 검색 품질을 함께 측정하는 일입니다. 모델 점수와 서비스 동작을 연결해 확인할 수 있도록 데이터와 설정, 원문 위치, 평가 기준을 기록으로 남겼습니다.
실험 기록과 공개 자료
실험 기록은 이 글에서 참고한 공개 커밋에 고정해 연결했습니다. 최신 구현은 KoPII-Pipeline 저장소에서 확인할 수 있습니다.
- 한국어 PII 데이터셋과 한국어 RoBERTa 모델
- 초기 도입 후보 비교와 평가 조건
- KoELECTRA 비교 대상 모델 — 이름 전용 비교 조건과 결과는 본문에 정리했습니다.
- 한국어 설정 탐색과 조직 범주화 실험
- 메타데이터 검색 후속 실험
- 범용 문서 처리·검색 구조
