보안 컴플라이언스 작업이 필요했다.
운영 중인 서비스의 일부 컬럼이 암호화 대상이었다. 데이터는 두 가지 형식으로 나뉘었다. 컬럼 값 자체가 암호화 대상인 경우, 그리고 JSON 객체로 저장된 컬럼에서 특정 필드만 암호화가 필요한 경우. 신규 시스템이 아닌, 이미 트래픽이 흐르고 있는 운영 환경이었다.
작업은 두 갈래로 보면 정확하다. 하나는 암호화 모듈 을 새로 만드는 일, 다른 하나는 그 모듈을 운영 중 서비스에 적용 하는 일. 결과적으로 두 번째가 더 컸다.
암호화 전략
대칭키 방식의 AES-256-GCM 을 선택했다. 키 관리는 봉투 암호화 구조로 가져갔다. CMK 가 DEK 를 암호화하고, DEK 가 데이터를 암호화하는 2중 키 구조다. 키 유출 영향을 제한하고 키 회전을 단순화하는 방식인데, 원리 자체는 별도 기술 글에서 정리해 두었다.
키 저장소로는 관리형 비밀 저장소(managed secret store)를 선택했다. 관리형 KMS 와 시스템 설정 저장소도 후보였지만, 운영 비용과 DEK 저장이라는 용도를 고려하면 비밀 저장소가 적합했다. 시스템 설정 저장소는 키 저장 용도와 결이 맞지 않다는 피드백도 초기 설계 리뷰에서 받았다.
DEK 관리 단위 — row 단위에서 테이블 단위로
초기 설계는 row 단위 였다. 각 row 마다 별도의 DEK 를 생성하고, 그 DEK 를 row 안에 함께 저장하는 구조. 키가 유출되더라도 영향 범위가 해당 row 에 국한된다는 장점이 있었다.
그런데 초기 설계 리뷰에서 운영과 관리 복잡도가 너무 올라간다는 피드백을 받았다. 검토 끝에 테이블 단위 로 바꿨다.
row 단위는 row 가 늘어날 때마다 키 발급 호출과 저장 공간이 함께 늘어난다. 운영 환경에서 갖는 의미는 단순한 비용 증가가 아니다. 키 관리 API 의 호출 빈도, 백업/복원 시의 처리량, 마이그레이션 시 row 단위 키 발급 로직, 전반적으로 시스템을 무겁게 만든다.
테이블 단위는 영향 범위가 한 테이블로 넓어지는 대신, 운영이 단순해진다. 민감도 등급별로 키를 분리하는 식으로 영향 범위를 다른 기준으로 좁힐 수 있었다.
처음에 “이게 더 안전하다” 고 봤던 답이 운영을 만나면 흔들리는 사례였다. 트레이드오프의 양쪽을 다 보고 나서야 결정의 무게가 와닿는다.
사내 모듈 — 두 가지 패턴
앞서 본 두 데이터 형식은 처리 방식이 갈렸다.
하나는 컬럼 값 전체 를 하나의 암호문으로 치환하는 방식. 컬럼 자체가 민감 정보일 때 적용된다.
다른 하나는 JSON 객체 안 해당 필드 값만 암호문으로 치환하는 방식. 객체 구조와 비민감 필드는 평문으로 유지된다.
두 패턴을 하나의 모듈로 묶어 두지 않으면 호출부가 두 갈래로 분기된다. 모듈은 둘 다 일급 API 로 제공하는 방향으로 설계했다.
마이그레이션 — 무중단 3단계
운영 중인 서비스라 단번에 컬럼을 바꿀 수 없었다. 3단계로 나눴다.
- 준비. 암호화 컬럼을 DDL 로 추가하고, 코드에는 이중 쓰기를 적용한다. INSERT/UPDATE 는 평문과 암호문 양쪽에 동시에 쓰고, SELECT 는 암호화 컬럼이 있으면 복호화, 없으면 원본 평문을 읽는 fallback 분기를 둔다.
- 마이그레이션. 기존 평문 데이터를 일괄 암호화해 신규 컬럼을 채운다. dry-run 으로 대상 건수를 먼저 확인하고, 배치 사이즈를 조정해 실행한다.
- 정리. 신규 컬럼이 완전히 채워진 것을 검증한 뒤 평문 컬럼과 fallback 분기를 제거한다.
각 단계 사이에 PR 머지와 배포가 들어간다. 다음 단계의 코드는 이전 단계가 모두 배포된 후 에만 안전하다는 전제 위에서 작성된다.
WHERE 절과 HMAC
적용 도중에 발견된 제약이 있었다.
일부 컬럼이 WHERE 조건으로 쓰이고 있었다. 검색 조회나 중복 체크 같은 용도다. 이 컬럼을 그대로 암호화하면 쿼리가 깨진다. AES-GCM 은 같은 평문이라도 매번 다른 암호문을 만들기 때문에, WHERE email = '...' 식의 동등 비교가 무의미해진다.
검색 가능성을 유지하려면 결정적인 변환 이 필요했다. 단방향 해시인 HMAC 으로 별도 컬럼을 두기로 했다. 쓸 때 원본을 HMAC 으로 한 번, 암호문으로 한 번, 두 번 저장한다. 검색은 HMAC 컬럼에서, 실제 값 복원은 암호문 컬럼에서.
이런 제약은 사전 컬럼 분석으로는 잡히지 않았다. 컬럼 이름과 타입만 보고는 그게 쿼리에서 어떻게 쓰이는지 알기 어렵다. 코드 베이스를 직접 훑어야 보이는 종류의 제약이었다.
조직 확산 — 마이그레이션 자동화 Skill
모듈이 동작한다고 끝이 아니었다. 적용 대상 컬럼은 여러 서비스에 흩어져 있었고, 각 서비스마다 3단계 마이그레이션을 누군가가 직접 작성해야 했다.
매번 사람이 같은 절차를 반복하면 실수가 따라온다. 이슈 트래커 티켓 형식이 제각각이라 컬럼 정보 파싱이 어렵고, 마이그레이션 스크립트의 dry-run 형태도 사람마다 달라진다.
작업자 누구나 같은 절차로 마이그레이션할 수 있도록 자동화 Skill 을 만들었다. 이슈 트래커 티켓의 표준 메타데이터에서 컬럼 정보를 파싱하고, 모듈 API 에 맞는 마이그레이션 스크립트를 생성한다. dry-run 결과를 보고 실제 실행으로 넘어가는 흐름까지 표준화했다.
이슈 트래커 티켓 형식도 같이 정리했다. 서버, 데이터베이스, 테이블, 컬럼명, 타입, 민감 필드 — 표준 테이블 형식을 정의하고, 미달되는 description 은 Skill 이 추가 질문으로 보완한다.
해커톤에서 AI 도구를 처음 활용해 봤던 경험이, 그때는 짧은 시간에 결과물을 내는 도구로 썼다면, 이번에는 조직 표준 절차 를 만드는 쪽으로 옮겨갔다.
배운 점
모듈을 만드는 것 보다 적용 이 더 컸다. 봉투 암호화도, 두 패턴도, 3단계 마이그레이션도 출발은 표준 패턴이었는데, 운영에 옮기면서 다듬었다. DEK 단위는 운영 비용 때문에 row 에서 테이블로 바꿨다. AES-GCM 의 기밀성과 검색 가능성을 함께 살리려고 HMAC 보조 컬럼을 더했다. 같은 절차를 여러 서비스에 반복 적용하면서 Skill 로 정리했다. 그 과정이 작업의 실제 분량이었다.
설계는 한 번 정해서 끝나지 않는다. row 단위에서 테이블 단위로 갔고, 두 패턴이 모두 필요했고, 적용 도중 HMAC 이 추가됐다. 처음에 옳다고 본 답이 운영을 만나며 다듬어지는 흐름이 있었다.
마지막으로, 모듈을 쓸 수 있게 만드는 것 이 모듈을 만드는 것만큼 컸다. Skill 이 그 빈자리를 채워줬다. 보안 요건은 통과의 목표였지만, 결과적으로 민감 정보를 다루는 조직 표준 이 남았다.
참고
- 봉투 암호화 — 봉투 암호화의 CMK/DEK 2중 키 구조와 원리.
- 사내 해커톤 1등 회고 — AI 도구 활용의 출발점.