← 모든 글

신뢰 vs 권한 — 작은 팀의 보안 모델

결제·정산 현장에서 WHERE 없는 UPDATE 하나가 수백 건 데이터를 덮는다. 신뢰와 권한 분리는 전제가 다른 두 질문이며, 최소 권한은 의심이 아닌 구조 설계의 문제다.

결제·정산 현장에서 실수는 "롤백하면 되지"가 통하지 않는다

결제나 정산을 다루는 시스템에서 데이터 실수의 무게는 일반 서비스와 다르다. 게시글이 잘못 수정되면 되돌리면 그만이지만, 정산 상태 컬럼이 수백 건 잘못 덮이면 이미 지급된 것처럼 기록된 항목이 생기거나, 반대로 정상 지급 건이 미처리로 남는다. 이 상태에서 다음 정산 배치가 돌면 오류가 두 번 중첩된다. 롤백이 기술적으로는 가능해도 그 사이에 연동 시스템이 해당 상태를 이미 읽어갔다면 원상복구는 쿼리 한 줄이 아니라 사람이 건별로 확인하는 수작업이 된다.

HEDVION은 세 명이 결제 수집·정산 자동화·외부 연동을 동시에 운영한다. 팀원 수가 적다는 건 개발 속도가 중요하다는 뜻이기도 하지만, 누군가가 실수를 사전에 잡아줄 검토 레이어가 사실상 없다는 의미이기도 하다. 큰 조직에는 DBA가 따로 있고, 릴리즈 전 보안팀 리뷰가 있고, 프로덕션 접근 자체가 티켓 기반으로 통제된다. 우리에게 그 역할을 대신하는 것은 사람이 아니라 구조다 — 실수가 발생하더라도 파급 범위가 제한되도록 설계된 권한 체계가 그 구조다.

신뢰와 권한 분리는 전제가 다른 두 질문이다

"우린 서로 다 믿는데 왜 readonly 계정이 필요해?"라는 질문을 팀에서 받은 적이 있다. 답은 짧다. 신뢰는 의도에 관한 것이고, 권한 분리는 실수에 관한 것이다. 이 두 가지는 서로를 대체하지 않는다. 팀원이 악의를 가질 가능성을 상정하는 게 아니라, 선의를 가진 사람도 실수는 한다는 전제에서 시스템을 만드는 것이다. 신뢰와 권한은 각자 다른 리스크를 다루는 서로 다른 도구다.

조회 목적으로 프로덕션 DB에 접속할 때 write 권한이 있는 계정을 쓰면, 작업하던 UPDATE 쿼리에서 실수로 WHERE 절이 빠진 채 실행돼도 DB는 그냥 받아들인다. readonly 계정이라면 같은 쿼리가 에러로 끝난다. 장애가 되느냐 에러 한 줄로 끝나느냐의 차이는 계정 권한 하나에서 갈린다. 이건 신뢰의 문제가 아니라 설계의 문제다.

WHERE 없는 UPDATE 한 줄의 실제 파급 — 수치로 본 시나리오

구체적인 상황을 짚어보자. 우리 정산 테이블에는 settlement_status 컬럼이 있다. 값은 pending, processing, completed, failed 네 가지다. 어느 날 오후, 특정 가맹점의 상태를 수동으로 completed로 변경하는 작업 중 WHERE 조건을 빠뜨린 채 실행했다고 가정한다.

-- 의도한 쿼리
UPDATE settlements SET status = 'completed'
WHERE merchant_id = 'M_1234' AND settled_at = '2026-06-03';

-- 실제 실행된 쿼리 (WHERE 누락)
UPDATE settlements SET status = 'completed';

이 쿼리 하나로 pending 상태였던 수백 건이 일괄 completed로 전환된다. 자동화 배치는 pending 건을 골라 처리하도록 설계되어 있으니, 이 건들은 다음 정산 사이클에서 조용히 건너뛰어진다. 실제 지급이 이뤄지지 않았는데 DB에는 완료로 찍혀 있는 상태가 된다. 30분 뒤 가맹점 CS 문의가 쌓이기 시작하고, 그때서야 문제를 인지한다. 롤백을 해도 그 30분 사이에 정산 연동 API를 읽어간 외부 시스템은 이미 '완료' 상태를 가져갔고, 이 건들을 재처리하려면 외부 시스템 담당자와 건별로 맞추는 수작업이 뒤따른다. readonly 계정이었다면? 쿼리가 실행되는 순간 ERROR: permission denied for table settlements로 끝난다. 아무 일도 일어나지 않는다.

HEDVION이 실제로 운영하는 권한 구조

이론이 아니라 지금 우리가 실제로 쓰는 방식이다. DB 계정을 역할에 따라 네 종류로 나눈다. 애플리케이션 런타임 계정은 DML(INSERT·UPDATE·DELETE)만 허용하고 DDL은 주지 않는다. 배포 중 실수로 테이블 구조가 바뀌거나 삭제되는 상황을 구조적으로 막는다. 정산 배치 전용 계정은 해당 배치가 실제로 접근하는 테이블 목록을 처음 설계할 때 명시하고, 그 테이블에만 write 권한을 부여한다. 나머지 테이블은 readonly다. 분석·조회 전용 계정은 전체 테이블에 SELECT만 가능하다. 팀원이 슬랙에서 "이 거래 상태 좀 봐줄 수 있어?"라는 요청을 받고 프로덕션에 접속할 때는 이 계정으로만 들어간다. 마이그레이션 계정은 DDL을 포함하며 배포 시에만 활성화한다. 평소에는 비활성 상태로 둔다.

환경 분리는 설정 파일 수준이 아니라 자격증명 자체를 다르게 만드는 방식으로 강제한다. 로컬 개발 환경에서는 프로덕션 DB로 연결 자체가 되지 않도록 네트워크 레벨에서 막았다. 스테이징과 프로덕션도 자격증명이 완전히 다르다. 스테이징 비밀번호로 프로덕션 엔드포인트에 연결하려 하면 인증 자체가 실패한다. "실수로 프로덕션에 붙었다"는 상황이 구조적으로 발생하지 않는다. 시크릿 관리는 코드 바깥에 둔다. .gitignore 외에 pre-commit 훅을 추가로 운영하며, 훅은 스테이징·프로덕션 자격증명과 일치하는 패턴이 코드에 하드코딩돼 있는지 커밋 시점에 스캔한다.

자동화가 만드는 새로운 권한 위험

자동화 스크립트는 권한 문제의 사각지대가 되기 쉽다. 개발자가 직접 쿼리를 실행할 때는 "잠깐, 이거 프로덕션 맞나?" 하고 한 번 더 멈추게 된다. 하지만 자동화 배치는 아무도 묻지 않는다. 새벽 2시에 조용히 수백 건을 처리한다. 그리고 대부분의 팀은 자동화 스크립트를 만들 때 "일단 돌아가게" 먼저 만들다 보니 전체 권한이 있는 계정으로 연결해 두고 나중에 조이겠다고 생각한다. 그 "나중"은 오지 않는다.

우리는 정산 배치가 읽어야 하는 테이블과 써야 하는 테이블을 처음 설계할 때부터 명시적으로 정의하고, 거기에 맞는 전용 계정을 만드는 것을 코드 리뷰 체크리스트에 포함시켰다. 자동화 스크립트가 전체 권한 계정으로 연결되어 있으면 리뷰를 통과하지 못한다. 자동화는 사람보다 더 빠르고, 더 많이, 더 조용하게 실행되기 때문에 권한은 수동 작업보다 더 엄격하게 관리해야 한다. 사람은 실수 직후 인지하고 멈출 수 있지만, 배치는 문제를 인지한 시점에 이미 전체 사이클을 완료한 뒤일 수 있다.

지금 당장 써먹을 실행 시사점

추상적인 원칙이 아니라 이번 주 안에 실행할 수 있는 체크리스트다.

1. 프로덕션 DB에 접속할 때 쓰는 계정을 확인한다. 지금 당장 팀원들이 조회 목적으로 어떤 계정으로 접속하는지 확인하라. write 권한이 있는 계정을 쓰고 있다면, readonly 계정을 새로 만들고 조회용 접속을 그 계정으로 강제한다. CREATE USER analyst WITH PASSWORD '...'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO analyst; — 10분이면 된다.

2. 자동화 스크립트의 DB 계정을 전수 확인한다. 배치 스크립트, cron 잡, 외부 연동 핸들러가 어떤 계정으로 실행되는지 목록을 만들어라. 전체 권한 계정을 쓰고 있는 항목이 있으면, 해당 스크립트가 실제로 접근하는 테이블만 허용하는 전용 계정으로 교체한다. 이 목록을 만드는 것 자체가 현재 권한 상태를 처음으로 정확히 파악하는 작업이 된다.

3. 환경별 자격증명이 물리적으로 다른지 확인한다. 같은 DB 비밀번호가 로컬·스테이징·프로덕션에 걸쳐 공유되고 있다면 지금 바꿔야 한다. 환경 분리는 설정 파일로만 하는 게 아니라 자격증명 자체를 다르게 만드는 것이다.

4. pre-commit 훅 하나를 추가한다. detect-secrets 또는 간단한 grep 패턴으로 프로덕션 DB 호스트명, API 키 패턴이 커밋에 포함되는 걸 막는다. 설치와 설정 합쳐 30분이면 된다. .gitignore는 파일 단위로 막지만, 훅은 내용 단위로 막는다.

팀이 작을수록 사람이 실수를 잡아줄 기회도 적다. 그 공백을 채우는 건 신뢰가 아니라 구조다. 신뢰는 협업의 전제이고, 구조는 실수가 장애로 번지지 않도록 막는 실제 장치다. 둘은 대체재가 아니다.

* 위 링크는 인프런 affiliate 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

📚 추천 강의
한 입 크기로 잘라먹는 바이브코딩 (with Claude Code)
Claude Code로 바이브코딩, 개발자라면 꼭 들어야 할 필수 강의
강의 보러가기 →

* 위 추천 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.