아침에 넣은 403 관문을 저녁에 걷고, 문서 검색 범위를 좁혔습니다
사내 문서 지식그래프 검색 도구를 만들고 있습니다. 어느 날 아침에 권한 관문 하나를 넣었고, 같은 날 밤에 그 관문을 걷어냈습니다. 하루 사이에 코드가 왕복한 것이 아니라, 그날 낮에 우리 손으로 사업의 정의를 바꿔 놓고 아침의 결정이 그 위에 서 있다는 것을 저녁에야 알아차린 것입니다.
관문 자체는 잘못 만든 것이 아니었습니다. 아침의 전제로는 맞았고 저녁의 전제로는 틀렸습니다. 문제는 우리가 전제를 바꾸고도 제가 그 변화를 따라가지 못했다는 데 있었습니다.
문서 창고가 둘이 되던 날
그날까지 이 도구가 검색하는 문서 저장소는 하나였습니다. 대표 개인의 클라우드 드라이브에 쌓인 지난 사업 자료였고, 벡터로 21만 건이 색인돼 있었습니다. 그날 두 번째 창고가 붙었습니다. 팀이 지금 실제로 일하는 공유 드라이브입니다. 한쪽은 끝난 일의 기록이고 다른 쪽은 진행 중인 일이라 성격이 갈립니다.
아침에는 대표 지시로 관문을 하나 넣었습니다. 색인된 문서를 답변 근거로 쓰는 것을 대표 계정만 허용하고, 나머지는 403 으로 막았습니다. 창구가 넷이라서(웹 채팅·스트리밍 채팅·메신저 봇·내부 질의) 네 곳을 각각 막지 않고 한 길목에서 판정하게 했습니다.
낮에는 다른 작업을 했습니다. 사업 대장을 68건에서 2건으로 줄였고(드라이브에 실제로 연결된 것만 남겼습니다), 역할을 셋으로 정리했고, 문서 출처 축을 '대표'와 '수행'으로 세웠습니다. 이 세 가지 변경 때문에 아침에 세운 관문의 전제가 사라졌습니다.
막기가 걸림돌이 된 순간
오후에 공유 드라이브 색인이 붙자 증상이 바로 나왔습니다. 사업 멤버로 붙어 있는 실무자가 자기 사업 폴더의 문서조차 못 보게 됐습니다. 관문은 '대표 문서를 보면 안 되는 사람'을 막으려고 세운 것인데, 그 사람이 볼 자격이 있는 문서까지 같이 막았습니다.
이 관문은 사용자별로 접근을 모두 허용하거나 모두 막습니다. 막을 대상이 창고 하나뿐일 때는 그게 가장 단순하고 읽기도 쉽습니다. 창고가 둘이 되는 순간 같은 코드가 걸림돌로 바뀝니다. 아침에는 실무자가 '자기 사업'을 가질 수 없는 구조였고, 오후에는 가질 수 있는 구조가 됐기 때문입니다.

제가 근거로 든 설계서는 그날 낮에 낡았습니다
저녁에 정책이 확정됐습니다. '사업이 폴더를 가리킬 때 대표 문서는 안 본다'는 한 줄입니다. 사업은 공유 드라이브의 폴더이고, 대표 문서함의 폴더는 어느 사업에도 안 붙으니 전부 지난 자료입니다.
그 자리에 오기까지 제가 같은 주장을 세 번쯤 되풀이했습니다. 열흘 전에 작성한 설계서에 '저장소를 나누지 말고 권한 축을 하나 더 세운다'고 적혀 있었고, 저는 그걸 근거로 '섞어 둬도 권한으로 막을 수 있다' 쪽을 계속 밀었습니다. 대표가 짚은 것은 이 한 줄이었습니다.
어차피 채팅 조회에서 대표 문서는 안 가져오잖아. 그러니까 그 매칭 자체가 잘못된 거라고.
열흘 전 설계는 공유 드라이브의 폴더를 대표 문서함 폴더 이름으로 번역해서 색인하고 있었습니다. 드라이브 문서가 남의 창고 이름을 뒤집어쓴 셈입니다. 이름을 맞춰 권한을 통과시킨 것이지 모델이 맞은 것이 아닙니다.
그 설계서는 사업 대장이 68건이고 권한 축이 둘이던 때 쓰였습니다. 그날 낮에 대장을 2건으로 줄이고 축을 셋으로 만든 손이 우리 손입니다. 설계서를 인용하는 순간 저는 이미 전제가 달라진 근거를 제시하고 있었고, 그래서 대표가 매번 한 칸씩 더 밀어야 했습니다.
막기를 걷고 좁히기로 바꾼 자리
밤에 관문 함수를 지웠습니다. 대신 권한에 따라 검색 범위와 허용 출처를 같이 정하는 쪽으로 바꿨습니다.
대표 문서검색 권한 | 검색 범위(폴더) | 허용 출처 |
|---|---|---|
있음 | 사업 폴더 + 지난 자료 | 대표·수행 둘 다 |
없음 | 사업 폴더만 | 수행만 |
중요한 건 이 두 열을 한 함수가 한 번에 돌려준다는 것입니다. 전에는 폴더 범위만 계산하는 함수가 있었고 허용 출처는 다른 자리에서 정했습니다. 따로 계산하는 길을 남겨 두면 언젠가 두 값이 갈리고, 갈린 결과는 에러가 아니라 '문서가 안 나온다'로만 보입니다.
관문 함수는 지우되 왜 걷었는지는 권한 모듈에 주석으로 남겼습니다. 다음 사람이 같은 자리에 같은 관문을 다시 세우는 것을 막으려는 목적입니다.
감점과 하드 필터는 다른 칸입니다
검색 쪽에서 출처를 다루는 방법이 둘입니다. 하나는 질문의 낱말에서 짐작한 출처로, 이건 감점입니다. 결과에서 빼지 않고 순위만 뒤로 밉니다. 다른 하나는 사람의 권한에서 계산한 허용 출처로, 이건 하드 필터입니다. 결과에서 아예 뺍니다.
둘 다 '출처'라는 같은 이름을 달고 있어서 한 인자로 합치고 싶어집니다. 합치면 권한이 감점으로 새어 나갑니다. 순위가 밀릴 뿐 결국 보이는데, 결과 화면이 멀쩡해서 테스트가 없으면 아무도 모릅니다. 그래서 '허용 출처는 하드 필터다'를 못 박는 테스트를 새로 넣고, 고치기 전 판에 대고 돌려서 그 테스트만 깨지는 것을 확인했습니다.
엔진은 출처의 옛 이름도 같이 받습니다. 안 그러면 옛 판이 적어 둔 값이 조건에 안 걸려 0건이 나오는데, 0건은 '권한이 없다'와 화면에서 구별되지 않습니다.

같은 날 같이 걸린 것 세 가지
권한 모델을 건드리면 주변에서 같이 걸리는 자리가 있습니다. 이날 확인된 지점은 세 곳입니다.
출처 값이 두 레포에 각각 있습니다. 서로 import 를 못 해서 복사해 둔 상수인데, 복사본은 시간이 지나면 '각자 정해도 된다'로 읽힙니다. 한쪽만 고치면 에러 없이 0건이 나오고 그건 권한 없음과 같은 모습입니다. 두 값의 일치를 테스트로 못 박았습니다.
없앤 역할 값은 DB 에서 같이 사라지지 않습니다. 옛 값을 가진 행이 남으면 어느 집합에도 안 들어가 가장 좁은 범위로 조용히 떨어집니다. 에러는 안 나고 '사업이 안 보인다'로만 나타납니다.
역할 값을 바꿀 때는 코드 배포가 먼저입니다. 이날은 DB 변경이 앞서서 몇 분 동안 두 사람이 전체 사업을 보지 못하는 시간이 생겼습니다.
역할은 결국 직원·대표·관리자 셋으로 정리됐습니다. '대표'는 직위 이름이라 '이름은 직위가 아니라 무엇을 할 수 있나로 짓는다'는 원래 규칙의 예외인데, 그 권한을 쓰는 사람이 하나뿐이라 두 이름이 같은 권한을 가리키는 상태를 남기지 않는 쪽을 선택했습니다. 예외라는 사실은 주석에 적어 뒀습니다.
권한 이름에 어느 창고인지를 넣었습니다
권한 이름도 같이 고쳤습니다. '문서검색'이던 것을 '대표 문서검색 권한'으로 바꿨습니다. 화면 라벨, 목록의 열 이름, 막힘 문구를 전부 같이 옮겼습니다.
창고가 하나일 때는 '문서 검색 권한이 없습니다'라는 문장의 뜻이 하나입니다. 창고가 둘이 되면 같은 문장이 어느 쪽 이야기인지 알 수 없는 문장이 됩니다. 이름에 대상을 넣어 두면 라벨·안내 문구·로그가 한 번에 맞습니다. 컬럼 이름은 그대로 뒀습니다. 문서 종류가 더 늘면 구조를 다시 볼 자리라 지금 마이그레이션까지 하면 두 번 일이 됩니다.
권한이 실제로 적용되는지는 메신저 봇에서 확인했습니다. 권한 없는 계정으로 물었을 때 로그에 '막힘(403): 대표 문서검색 권한이 없습니다'가 찍히고 사람이 읽을 문장으로 전달됐습니다. 테스트는 엔진 782개, 업무 화면 권한 테스트 10개(웹 채팅·스트리밍 채팅·메신저 봇·내부 질의의 네 진입 경로를 각각 검사합니다), 역할 정리 시점 830개가 통과했습니다. 결과로 엔진 폴더 198개 중 196개가 지난 자료로 분류됐습니다.
관문을 어디에 두느냐로 결과가 갈린 이야기는 소셜 계정 연결 쪽에서도 같은 모양으로 나왔습니다.
설계서를 인용하기 전에 세는 것
이날 남은 것을 점검 항목으로 정리하면 이렇습니다.
지금 근거로 드는 설계서가 전제한 숫자가 아직 맞는지 센다. 그 숫자를 바꾼 것이 우리 손이면 그 문서의 해당 전제는 이미 낡았습니다.
'이 사람은 보면 안 된다'는 요구를 막기로 풀지 좁히기로 풀지 고른다. 막을 대상이 앞으로도 하나뿐이면 막기가 단순하다.
권한이 결정하는 값이 둘 이상이면 한 함수가 한 번에 돌려주게 한다. 따로 계산하는 길을 남기지 않는다.
순위를 미는 것과 결과에서 빼는 것을 같은 이름으로 부르지 않는다. 권한 쪽은 반드시 빼는 쪽이다.
코드에서 지운 역할 값이 DB 에 남았을 때 어디로 떨어지는지를 테스트로 먼저 박는다. 그리고 코드를 먼저 배포한다.
설계서를 통째로 버리라는 얘기는 아닙니다. 열흘 전 문서의 '권한 축을 하나 더 세운다'는 결론은 출처 축을 만드는 데 그대로 살아 있었습니다. 낡은 것은 '저장소를 나누지 말라' 쪽 한 줄이었습니다. 전제가 바뀐 부분만 골라 뒤집어야지 문서를 통째로 무시하면 반대 방향의 오류가 생깁니다.

하루에 두 번 뒤집은 것에 대해
아침에 정책부터 확정했으면 관문을 안 넣었을 겁니다. 다만 그날 낮의 작업이 있어야 저녁의 정책이 나올 수 있었으니, 순서를 바꿀 수 있었는지는 확실하지 않습니다. 남은 일은 색인된 수행 문서 227건의 폴더 이름을 드라이브 기준으로 옮기는 것입니다.
기록으로 남은 것은 관문이 하루 만에 사라졌다는 사실이 아니라, 그 관문을 지탱하던 전제를 우리가 직접 바꿔 놓고도 반나절을 못 알아봤다는 쪽입니다. 요구를 막기로 풀 것인가 좁히기로 풀 것인가는 둘 중 하나가 늘 옳은 문제가 아닙니다. 대상이 하나일 때는 막기가 맞고 둘이 되면 좁히기가 맞습니다. 그 경계를 넘긴 것이 무엇이었는지를 먼저 찾는 편이 빠릅니다.
같은 축에서 반대 방향의 기록도 있습니다. 요청을 받고 세 겹을 걸었다가 두 겹을 도로 걷어낸 쪽입니다.