기술

멀티테넌트 설정에 안전한 기본값은 없었습니다 — 비면 안 뜨게 만들고 옛 코드에 대고 돌렸습니다

2026.10.0610분 읽기

사내 문서 지식그래프 검색 도구를 한 고객사에 운영 중인데, 같은 대표가 가진 두 번째 법인용으로 한 벌이 더 필요해졌습니다. 코드는 그대로 쓰고 설정만 갈라 두 벌을 띄우는 구성입니다. 그런데 설정 모듈을 열어 보니 회사를 가르는 값들에 기본값이 박혀 있었고, 그 기본값이 전부 첫 고객사를 가리키고 있었습니다.

위험의 모양은 한 줄입니다. 두 번째 법인 배포가 설정 하나만 빠뜨리면 첫 고객사 저장소에 씁니다. 그리고 그때 화면에 보이는 것은 정상 동작과 똑같습니다.


에러 로그 0건으로 남의 회사 저장소에 쓰는 경우

설정에 기본값이 있으면 그 설정은 빠뜨려도 에러가 나지 않습니다. 엔진은 정상 기동하고, 색인은 성공하고, 검색도 됩니다. 로그를 열어 봐도 실패가 한 건도 없습니다. 제대로 도는 것과 잘못 도는 것이 관측상 구분되지 않는 상태입니다.

같은 회사 안의 재색인 오류와 달리, 법인이 다른 저장소에 쓰면 다른 회사의 자료가 섞일 수 있습니다. 잘못 저장한 데이터를 지우는 것만으로 끝나지 않고 고객사 간 데이터 격리와 기밀성이 훼손되는 사고가 됩니다.

그래서 두 번째 법인 코드를 한 줄도 시작하기 전에 관문부터 넣기로 했습니다. 시작한 뒤에 넣으면 그 사이에 섞이지 않았다는 것을 나중에 증명해야 합니다. 증명할 방법이 마땅치 않은 일은 애초에 만들지 않는 편이 쌉니다.

기본값이 있는 설정과 비면 안 뜨는 설정을 BEFORE AFTER 로 비교한 그림


저장소만 나누면 되는 줄 알았는데 상태 파일이 같은 축이었습니다

처방은 단순합니다. 설정 모듈에 테넌트 식별자 하나를 필수로 두고, 회사를 가르는 값들을 거기서 파생시키는 것입니다. 같은 앱의 업무 화면 쪽이 서명 비밀키에 이미 쓰고 있던 방식을 그대로 옮겼습니다. 비어 있으면 안 뜨고, 뜨면 반드시 어느 테넌트인지가 정해져 있는 구조입니다.

고민은 방식이 아니라 개수였습니다. 앞서 적어 둔 계획서에는 파생시킬 값이 다섯이라고 되어 있었습니다. 실제로 코드를 열어 보니 여덟이었습니다.

계획서에 있던 것: 벡터 DB 컬렉션 이름 · 그래프 DB 접속 주소 · 벡터 DB 접속 주소 · 문서 입구 경로 · 파일 보관 경로

열어 보고 추가한 것: 상태 파일 셋 — 스캔 진행 상태 · 스캔 리포트 · 예열 결과

상태 파일이 왜 같은 축인지는 한 기계에 두 테넌트를 띄워 보면 바로 보입니다. 오늘 이미 돌았다는 표시를 서로 덮어씁니다. 그러면 한쪽 스캔이 조용히 건너뛰어집니다. 데이터가 섞이는 것도 아니고 실패하는 것도 아니라, 그냥 일이 안 돌고 아무도 모릅니다.

회사를 가르는 값을 DB·버킷·경로로만 세면 여기가 빠집니다. 한 기계에 둘을 띄울 때 둘이 같은 파일에 쓰는 것 전부가 격리 대상입니다. 잠금 파일, 실행 마커, 캐시, 리포트, 로그가 다 여기에 들어갑니다.


관문 셋을 서버 건드리기 전에 걸었습니다

테넌트 식별자가 비었나 — 모듈을 불러오는 시점에 예외를 던지고 멈춥니다

기본값 표에 없는 테넌트인데 여덟 값이 다 적혀 있지 않나 — 하나라도 비면 멈춥니다

그 값이 첫 테넌트의 값과 같나 — 여섯 항목에 대해 같으면 거절합니다

세 번째가 복붙 방지입니다. 값을 적어라까지만 하면 첫 테넌트 설정을 복사해 붙이고 고치다 만 것이 통과합니다. 경로는 홈 디렉터리 기호를 펼친 뒤에 비교합니다. 안 펼치면 같은 경로를 다르게 적은 것만으로 통과합니다.

다만 벡터 DB 접속 주소 하나는 같아도 통과시켰습니다. 한 저장소 안에서 컬렉션 이름으로 나뉘는 구조라, 거기서는 주소가 격리 단위가 아니기 때문입니다. 무엇으로 격리되는지를 먼저 정한 뒤에 관문을 걸어야 하는 이유가 이 지점입니다. 정하지 않고 걸면 정당한 공유까지 막습니다.

세 관문은 전부 모듈을 불러오는 시점에 걸립니다. 배포 스크립트가 서버를 건드리기 전 단계에서 한 번 불러와 보므로, 걸리면 옛 프로세스가 그대로 살아 있습니다. 관문이 실패했을 때 지금 돌아가는 것이 멈추지 않는 자리에 두는 것이 배치의 핵심입니다.

테넌트 식별자 확인부터 첫 테넌트 값과의 충돌 대조까지 관문 세 단계를 위에서 아래로 그린 흐름도


관문을 고치기 전 코드에 대고 떨어뜨려 봤습니다

여기까지는 흔한 이야기입니다. 이 작업에서 실제로 값을 한 것은 관문 자체가 아니라 관문을 검증한 방식이었습니다.

계획 단계에 이렇게 적어 뒀습니다. '관문에는 테스트를 붙이고, 고치기 전 판에 대고도 돌려 본다. 옛 코드에서도 통과하는 테스트는 아무것도 증명하지 않는다.' 그리고 실제로 했습니다.

식별자를 비운 채 고치기 전 코드를 불러오니 아무 일 없이 떴습니다. 관문이 없었으니 당연한 결과인데, 그 당연한 결과를 눈으로 본 것이 중요합니다. 새 코드는 같은 조건에서 예외로 멈췄습니다. 새로 쓴 테스트 5개를 옛 코드에 대니 다섯 다 실패했습니다. 전체 테스트는 881개가 통과했습니다.

이 두 줄이 관문이 실제로 무언가를 막는다는 유일한 증거입니다. 새 코드에서 테스트가 초록인 것은 아무것도 말해주지 않습니다. 물어야 하는 질문은 하나입니다. 이게 아니라면 무엇이 보일까. 답이 똑같이 보인다면 그 검사는 아무 일도 하지 않습니다.

이 습관은 아홉 날 전의 사고에서 왔습니다. 같은 앱의 배포 관문이 응답 코드 200 만 보고 있었는데, 재시작을 떼어 띄운 직후라 옛 프로세스가 200 을 주고 있었습니다. 그 관문은 한 번도 무언가를 막은 적이 없었고, 사고 둘이 성공으로 지나갔습니다. 강화도 세 번 했는데 셋 다 옳았고, 아무도 옛 판에 대고 돌려볼 생각을 안 해서 이것만 안 드러났습니다.

배포 확인도 같은 기준으로 셋을 봤습니다. 프로세스 교체(기동 전후 프로세스 ID 가 바뀌었나) · 헬스체크 응답에 실은 테넌트 값 · 실패 마커가 없는 것입니다. 헬스체크에 어느 테넌트로 떴는지를 실어 둔 것은 이 확인 때문입니다. 응답이 온다는 사실만으로는 어느 설정으로 떴는지를 알 수 없습니다.

같은 테스트 5개를 옛 코드와 새 코드에 각각 돌린 결과를 좌우로 비교한 그림


표는 사양이 아니라 기록으로 뒀습니다

기본값 표에는 첫 고객사만 등재했습니다. 두 번째 법인의 값은 미리 적지 않았습니다. 표가 두 회사의 배포 사양을 들고 있게 되면 그 표가 원천이 되고, 실제 배포와 갈릴 때 어느 쪽이 맞는지 알 수 없게 됩니다.

그래서 표의 역할을 둘로 못 박았습니다. 먼저 있던 쪽이 무엇을 쓰는지의 기록이고, 새 테넌트에게는 충돌 대조표입니다. 두 번째 법인의 저장소 주소와 경로는 그 배포의 설정 파일이 원천입니다.

마지막으로, 엔진 코드를 설정 없이 불러오는 자리가 다른 데 남아 있지 않은지 세어 봤습니다. 관문을 넣으면 그동안 조용히 기본값으로 돌던 자리가 전부 실패합니다. 그게 목적이긴 한데, 실패할 자리를 미리 세어 두지 않으면 실패한 것이 관문인지 사고인지 구분이 안 됩니다.


이 처방이 안 맞는 자리

테넌트가 하나뿐이고 앞으로도 하나라면 필수화는 매 배포와 매 로컬 실행에 값 하나를 더 적게 하는 순수 비용입니다. 이 처방은 두 번째가 올 것이 확정된 시점의 것입니다. 다만 그 시점은 두 번째 코드를 쓰기 전이어야 합니다.

같으면 거절하는 규칙은 정당한 공유도 막습니다. 여기서도 접속 주소 하나를 손으로 열어 줘야 했습니다. 공유 자원이 많은 구조에서는 예외 목록이 길어지고, 길어지면 관문이 아니라 잡음이 됩니다.

옛 판에 테스트를 대는 검증도 만능은 아닙니다. 관문처럼 없던 것을 새로 만든 변경에서는 잘 듣지만, 값을 고치거나 규칙을 넓히는 변경에서는 옛 판도 통과하는 것이 정상입니다. 그럴 때는 전후 결과를 대조하는 다른 도구가 필요합니다. 통과하는데 아무것도 보지 않는 테스트는 이쪽에서도 똑같이 생깁니다.


멀티테넌트 설정 관문 점검 목록

두 테넌트를 가르는 값에 기본값이 남아 있나 — 기본값이 있으면 그 설정은 빠뜨려도 에러가 나지 않습니다

격리 대상을 저장소까지만 세지 않았나 — 잠금 파일·실행 마커·리포트도 같은 축입니다

관문이 실패했을 때 지금 돌아가는 것이 멈추나 — 불러오는 시점·배포 전 단계에 두면 옛 프로세스가 삽니다

값을 적어라까지만 하고 끝냈나 — 첫 테넌트 값과 같으면 거절하는 규칙을 같이 겁니다

새로 만든 검사를 고치기 전 판에 대고 돌려 봤나 — 거기서도 통과하면 그 검사는 아무 일도 하지 않습니다


마무리

관문을 넣는 작업 자체는 70분이면 끝났습니다. 긴 것은 어떤 값을 필수로 둘지 세는 일이었고, 값을 한 것은 그 관문을 옛 판에 대고 떨어뜨려 본 몇 분이었습니다.

멀티테넌트에서 안전한 기본값은 없습니다. 비면 안 뜨는 것이 기본값입니다. 그리고 그렇게 만든 관문은 반드시 고치기 전 판에 대고 한 번 떨어뜨려 보는 편이 좋습니다. 새 코드에서 초록인 테스트는 관문이 있다는 것만 말하고, 관문이 무언가를 막는다는 것은 말하지 않습니다.

#멀티테넌트#설정관문#회귀검사#데이터격리#배포검증