민감정보 분리 경험 기록
쿠팡 재직 중 진행한 민감정보 분리보관 Task 기록
서비스가 어느정도 궤도에 오르면 통상적으로 거쳐야 하는 과정 중 하나가 민감 정보 관리 감사인 것 같습니다. 이전 회사 재직 중이던 2020년 3월 ~ 10월 중순까지 소속 팀의 민감 정보 분리보관 Task를 진행했습니다.
- 이 문서는 '이렇게 해야 한다'가 아니라 민감 정보 감사를 대응하는 수많은 방법 중, 당시 회사의 고려 사항과 제가 속한 조직이 진행했던 경험을 잊지 않기 위한 취지입니다.
- 개괄적인 처리 흐름 위주로 기록합니다.
보안 조직의 요구사항 (기억나는 대로)
- 사내 무수히 많은 서비스들 중 민감 정보로 분류되는 Data를 직접적으로 저장하는 서비스들이 분리보관 구현 대상이다.
- 민감 정보는 이름, 주민등록번호처럼 명확한 값뿐만 아니라 [전화번호 + 주소]의 조합 등으로 개인을 특정할 수 있는 정보도 민감 정보로 간주한다.
- 분리보관 대상은 탈퇴자 및 1년 동안 서비스 이용기록이 없는 장기 미사용자이다.
- 장기 미사용자가 다시 서비스에 접속하면 분리보관되었던 정보는 원본 저장소로 원복되어야 한다.
- 분리보관시 대상 정보는 무조건 Hard Delete 해야 한다. (당시 회사 대부분의 서비스는 DB로 MySQL을 사용)
- 원본 저장소와 분리보관 저장소는 분리되어야 한다.
- 권장: 물리적 분리 / 차선: 논리적 Schema 분리
- 기존에 저장된 정보 역시 분리보관 migration이 되어야 한다. (만약 10년된 서비스라면 기존 10년치 data에 대해 분리보관 진행)
- 분리보관된 정보 중, 장기 미이용자는 1년 뒤 탈퇴자로 변경한다.
- 분리보관된 정보 중, 탈퇴자는 5년 뒤 분리보관 저장소에서 삭제 처리한다.
- 일반적으로 즉시 처리를 권고한다. 다만 서비스 특성상 굳이 즉시 처리를 하지 않아도 된다면 1~2일 이후의 지연 처리는 용인한다.
사내 지원
- 사용자 정보를 관리하는 팀(멤버팀)에서 탈퇴자, 장기 미이용자, 원복자에 대한 memberId를 Kafka(사용 권장) 및 API(deprecated)를 통해 제공.
당시 소속 팀 상황
- 소속 팀의 서비스는 2018년 1월에 시작되었기에 해당 Task가 마무리되는 2020년 10월 말 기준 약 3년치 정보가 누적.
- 사용자를 식별하는 memberId 필드와 민감정보로 분류되는 2개의 data를 DB에 적재 및 사용 (편의상 DataA, DataB라고 표현)
- DB는 MySQL, Backend는 Java/Spring, ORM으로 JPA 사용.
- 전체 테이블 중 민감 정보를 포함하는 테이블은 대략 10개 정도.
- memberId를 가진 테이블은 1개, 해당 테이블과 연관된 테이블들에 DataA, DataB가 포함된 경우가 다수.
분리보관 구현 설계
별도의 물리 DB를 따로 두지 않고 기존 물리 DB 안에 분리보관용 Schema를 구성. Spring/JPA에서 분리보관용 Schema에 대한 연결을 위해 multiple Database 설정.

팀 서비스는 반품 검수 도메인 특성상 실시간 처리를 하지 않아도 된다고 결론. 멤버팀에서 발행하는 Kafka message를 consume하여 분리보관 Schema의 masterLog table에 적재.


2개의 Batch Process(민감정보 분리 보관, 분리보관 이후 5년 뒤 삭제)를 개발. 1일 1회씩 batch 실행.
1. 민감정보 분리 보관 Batch
서비스 유휴시간(4:00AM ~ 7:00AM)에 분리보관을 진행할 Batch job 실행. 우선순위는 탈퇴자(withdraw-1,2) > 장기 미사용자(dormant) > 원복자(restore)
아래의 Flow는 Batch 전체의 진행 흐름이며, 각 분리보관 상세 흐름은 하위에 기술.

일반사용자 → 탈퇴자 처리 (withdraw-1)

장기 미이용자 → 탈퇴자 처리 (withdraw-2)

일반 사용자 → 장기 미이용자 처리 (dormant)

장기 미이용자 → 원복 처리 (restore)

2. 분리보관 이후 5년 뒤 삭제
서비스 유휴시간(07:00AM ~ 08:00AM)에 분리보관 이후 5년 지난 탈퇴자 정보 삭제.

과거 3년치 적재 정보에 대한 분리보관 migration 진행. (첫 target 일자를 2018-01-01로 설정)

기억에 남는 부분들
- RDB 특성상 각 테이블간의 관계가 다양하게 얽혀있음에도 불구하고 보안 정책상 대상 row의 Hard Delete 및 원복을 염두해야 했습니다. 따라서 Hard Delete시 Cascade를 무시하도록 JPA 관련 Option을 조사했던 기억이 납니다.
- 조회 대상 entity와 관계를 가지고 있는 정보가 Hard Delete되어 JPA 기능으로 찾지 못하는 경우, 해당 내용은 Null로 치환하도록 모든 코드를 수정했습니다.
- ORM을 통하여 테이블 entity 개발하는 경우, 일반적으로 PK 컬럼에 Auto Increment를 설정합니다. 원복시 기존 PK 값을 그대로 사용하여 insert가 되어야 하는데, 이 Auto Increment 제약이 너무 강력하여 원하는 PK값으로 원복이 되지 않는 경우가 발생했습니다. 따라서 원복시에는 JPA를 통한 insert가 아닌 native query를 이용하여 원하는 PK값으로 insert할 수 있도록 기능을 구현했습니다.
- 대상 정보를 발행하는 멤버팀의 사정으로 특정일에 3~4일치의 정보가 한번에 발행되는 일이 간혹 있습니다. 따라서 분리보관 Batch는 대상 양이 많아도 큰 부하 없이 처리되도록 200 row 단위의 pagination 및 tx를 설정했습니다.
- 만약 10만 row에 대해 분리보관 중에 하나가 실패하면 10만 row 전체 rollback이 아닌, 해당 실패 지점을 포함하는 200 row만 실패되도록 처리됩니다. 따라서 해당 일자 재실행시 실패한 200 row에 대해서만 조회하여 처리합니다.