MySQL에 저장돼 있는 개인정보의 암호화 방식을 바꾸기로 결정했다. 새 방식으로 쓰는 코드를 만드는 것보다 이미 저장된 값을 어떻게 할지가 훨씬 어려웠다. 값이 수백만 건이라 한 번에 바꾸는 것도 간단하지 않았다.
Table of contents
Open Table of contents
선택지 — 세 가지 방식
선택지는 셋이었다.
1. 전부 다시 암호화한다
2. 읽을 때 옛것이면 새 방식으로 다시 저장한다
3. 옛것은 그대로 두고 읽을 때만 옛 방식으로 푼다
1은 member 가 424만 행이라 한 번에 UPDATE 하면 그동안 서비스가 느려지고, 2는 안 읽히는 행이 영영 안 바뀌며, 3은 decryptV1 을 영원히 유지해야 한다. 그래서 둘째를 기본으로 하고 접근이 드문 값만 cron 배치로 처리하는 방식을 골랐다.
어느 방식인지 표시하기
두 방식이 섞여 있으려면 값마다 어느 방식으로 암호화됐는지 알아야 했다. 풀어 보고 실패하면 다른 방식으로 시도하는 방법도 있지만 그것은 느리고 불확실하다.
그래서 값 앞에 v2: 접두를 붙이고 접두가 없으면 옛것으로 보게 했다.
public function decrypt(string $stored): string {
if (str_starts_with($stored, 'v2:')) {
return $this->decryptV2(substr($stored, 3));
}
return $this->decryptV1($stored); // 접두 없는 것은 v1
}
str_starts_with 하나로 갈리므로 풀어 보고 실패하면 다른 방식으로 시도할 필요가 없다.
진행 상황을 셌다
두 방식이 섞여 있는 상태가 언제 끝나는지도 알아야 해서 접두별 건수를 매일 남기게 했다.
SELECT SUM(phone_enc LIKE 'v2:%') AS v2,
SUM(phone_enc NOT LIKE 'v2:%') AS v1,
COUNT(*) AS total
FROM member;
v2 120만 · v1 304만 · total 424만으로 시작했고 하루 약 40만 건이 바뀌어 8일이면 끝난다는 계산이 나왔다. 자주 읽히는 행은 조회 함수가 encryptV2 로 UPDATE 하며 바꾸고, 안 읽히는 행은 cron 배치가 NOT LIKE 'v2:%' 로 2,000건씩 끊어 가져가 처리했다. 이런 숫자가 없으면 언제 끝날지를 계속 짐작만 하고 있게 된다.
옛 방식을 언제 지울지
전부 옮겨지면 옛 방식을 지울 수 있지만 백업에서 복원하면 옛 방식으로 저장된 값이 다시 나타나므로 바로 지울 수는 없었다.
그래서 백업 보관 기간이 지난 뒤로 지우는 시점을 잡았다. 옛 방식에 쓰던 키도 그 시점까지는 함께 남겨야 했다. 지우는 조건과 시점을 미리 적어 두지 않으면 옛 코드가 영원히 남는다.
경계 값으로 확인하기
실제로 바꾸기 전에 빈 값과 특수 문자가 든 값과 아주 긴 문자열을 각각 넣어 가며 미리 시험해 봤다.
빈 문자열에서 한 번 걸렸는데 옛 방식은 그것을 그대로 두고 새 방식은 무언가를 만들어 냈다. 그렇게 되면 원래 빈 문자열이었는지 아닌지가 구분되지 않는다. 암호화된 값을 WHERE에 넣어 찾던 자리도 grep으로 전부 찾아 고쳐야 했다.
정리
- 방식을 바꿀 때
MySQL에 이미 저장된 값을 어떻게 할지가 먼저다 - 한 번에 바꾸는 것과 영원히 유지하는 것 사이를 고른다
- 값에 어느 방식인지 접두를 붙인다
- 시도해 보고 판단하는 대신 그 접두를 보고 판단한다
- 접두로
GROUP BY해 방식별COUNT(*)로 완료 시점을 계산한다 - 접근이 드문 값은
cron배치로 따로 처리한다 - 백업 복원을 감안해 옛 방식 제거 시점을 잡는다
- 빈 문자열 같은 경계 값으로 미리 확인한다