서버를 데이터베이스에 연결하는 일에서 시작해, 누가 어떤 데이터에 접근할 수 있어야 하는지 고민하게 됐습니다. PostgreSQL의 권한 구조를 공부하며 ByOrbit의 데이터 접근 방식을 설계한 경험을 공유합니다.
시작은 팀원의 서버 연결 요청이었습니다
안녕하세요. ByOrbit 프로젝트에서 데이터베이스 설계에 참여하고 있는 인턴 최지원입니다.
저희 팀은 성분 데이터베이스의 스키마 설계부터 마이그레이션 관리까지 데이터베이스 전반을 다루고 있습니다. 이번에 소개할 작업은 그중에서도 “누가 데이터베이스의 어디까지 접근할 수 있는가”를 정하는 권한 설계입니다.
계기는 팀원의 요청이었습니다. API 서버를 연결하려면 DB에 접근할 수 있어야 했고, 이를 위한 인증 정보가 필요했습니다. 당시에는 서버에서 service_role 키를 사용하고 있었으며, 저희 팀의 운영 방식상 서버 연결에 필요한 키를 관리자에게 요청해야 했습니다.
기존 키를 전달하면 연결 자체는 해결할 수 있었습니다. 하지만 그 전에 확인해야 할 것이 있었습니다.
이 서버에 정말 그만큼의 권한이 필요할까?
service_role로 실행되는 요청은 RLS, 즉 행 수준 보안 정책을 우회할 수 있습니다. 필요한 관리 작업에는 유용하지만, 제한된 데이터만 다루면 되는 서버에 같은 권한을 주면 접근 범위가 필요 이상으로 넓어질 수 있습니다.
저는 이 키를 마스터키에 비유했습니다. 특정 방 하나에 들어가야 하는 사람에게 여러 방을 열 수 있는 열쇠를 건네는 셈이었기 때문입니다.
그래서 기존 키를 그대로 공유하는 대신, 서버가 맡은 일에 필요한 권한만 부여하는 방향으로 접근 방식을 다시 살펴보기로 했습니다.
RLS 앞에도 확인해야 할 권한이 있었습니다
역할에 따라 접근 범위를 나누려면 먼저 PostgreSQL의 권한 구조를 이해해야 했습니다.
이번 작업에서는 일반적인 데이터 조회·수정 권한을 세 가지 질문으로 나눠 정리했습니다.
확인 단계질문관련 권한스키마 접근이 공간 안의 객체에 접근할 수 있는가?스키마 USAGE테이블 접근이 테이블에서 어떤 작업을 할 수 있는가?SELECT, INSERT, UPDATE, DELETE 등행 접근그 작업을 어떤 데이터에 적용할 수 있는가?RLS 정책
스키마와 테이블에 대한 접근 권한은 GRANT로 부여하고, RLS가 적용되는 테이블에서는 정책에 따라 접근 가능한 행을 제한합니다. RLS만 설정하는 것으로 스키마와 테이블의 접근 권한까지 생기는 것은 아닙니다.
건물에 비유하면 이해하기 쉬웠습니다. 스키마 권한은 건물에 들어가는 권한, 테이블 권한은 특정 방에서 작업하는 권한, RLS는 그 방에 있는 자료 중 어떤 것을 다룰 수 있는지 정하는 규칙에 가깝습니다.
조회 권한을 설명하기 위한 예시를 간단히 적으면 다음과 같습니다.
-- public 스키마 안의 객체에 접근할 수 있도록 허용
GRANT USAGE ON SCHEMA public TO authenticated;
-- transports 테이블에 대한 조회 작업을 허용
GRANT SELECT ON TABLE public.transports TO authenticated;첫 번째 구문만으로는 테이블을 조회할 수 없습니다. USAGE는 스키마 안의 객체에 접근하기 위한 권한이고, 실제 조회에는 테이블의 SELECT 권한이 필요합니다. 위 구문 역시 RLS 정책까지 완성한 설정은 아닙니다.
이 구분을 이해하고 나니 권한 문제를 살펴보는 순서도 분명해졌습니다. 데이터가 조회되지 않을 때 RLS 정책만 확인할 것이 아니라, 그 요청에 스키마와 테이블을 사용할 권한이 있는지부터 살펴봐야 했습니다.
정책이 허용하더라도, 테이블을 조회할 권한 자체가 없으면 접근할 수 없습니다.
같은 테이블을 사용해도 맡은 일은 다릅니다
테이블 접근을 허용했다고 해서 모든 계정이 같은 작업을 해야 하는 것은 아닙니다.
ByOrbit에서는 운영 업무를 수행하는 계정과 인턴 계정에 필요한 권한을 구분했습니다. 원고에 정리한 정책에서도 operations.read, operations.update와 같은 운영 권한과 reference.read, reference.create, reference.update 같은 권한을 나눠 사용하고 있습니다.
여기서 RLS는 요청에 해당 권한이 있는지 확인하는 역할을 맡습니다. 예를 들어 정책 안에서 authorize('reference.read')를 호출해, 요청한 계정에 그 권한이 있는지 검사하는 방식입니다. 이때 authorize()는 PostgreSQL이 기본으로 제공하는 명령이 아니라, 역할과 권한을 확인하도록 정의하는 함수입니다.
이 부분을 정리하면서 “접근할 수 있다”는 표현도 더 구체적으로 써야 한다고 느꼈습니다.
조회만 가능한 것인지, 새로운 데이터를 등록할 수 있는 것인지, 기존 데이터를 수정하거나 삭제할 수 있는 것인지에 따라 필요한 권한이 달랐습니다. 단순히 사용자를 허용하거나 차단하는 것보다, 그 사람이 실제로 수행할 작업을 먼저 정리하는 것이 중요했습니다.
같은 ‘역할’이라는 말도 구분해서 써야 했습니다
또 하나 헷갈렸던 부분은 role이라는 용어였습니다.
정책에는 TO authenticated라고 적혀 있는데, 서비스 안에서는 user, intern, api처럼 다른 역할 이름을 사용하고 있었습니다. 처음에는 모두 같은 종류의 역할처럼 보였지만, 두 가지를 구분해야 했습니다.
먼저 authenticated는 실제 PostgreSQL 역할입니다. DB의 일반적인 Data API 인증 흐름에서, 로그인한 사용자의 요청은 이 역할로 처리됩니다.
반면 이번에 살펴본 구조의 user, intern, api는 서비스 안에서 업무를 구분하기 위한 역할입니다. 이 정보는 JWT라는 인증 토큰의 user_role 같은 항목에 담을 수 있고, RLS 정책에서 권한을 판단하는 데 사용합니다.
예를 들어 API 전용 계정이라면 데이터베이스 요청에는 authenticated 역할을 사용하면서, 토큰에는 user_role 값으로 api를 담는 구조입니다.
이렇게 구분하니 각 설정의 책임도 명확해졌습니다.
GRANT는 실제 데이터베이스 역할에 권한을 부여하고, RLS는 요청에 담긴 업무 역할과 권한 정보를 활용해 접근 조건을 판단합니다. JWT에 업무 역할을 추가하는 것과 PostgreSQL 역할을 새로 생성하는 것은 서로 다른 작업입니다.
API 서버에도 필요한 만큼의 권한만
개념을 정리한 뒤에는 API 서버의 접근 방식을 다시 설계했습니다. 방향은 일반적인 데이터 조회·수정에서 service_role 키에 의존하지 않고, 서버의 업무에 맞는 인증과 권한 검사를 거치도록 하는 것이었습니다.
원문 도식에 정리한 흐름은 API 전용 계정으로 인증하고, 업무 역할이 담긴 토큰을 발급받아 요청하는 방식입니다. 데이터베이스에서는 요청을 처리하는 PostgreSQL 역할의 접근 권한과 RLS 정책을 함께 확인하도록 구성하는 것입니다.
이때 중요한 것은 계정 이름을 api로 정하는 일이 아니었습니다. 그 계정이 어떤 테이블을 사용해야 하는지, 조회만 필요한지 아니면 등록과 수정도 해야 하는지, 어느 데이터까지 접근해야 하는지를 정하는 일이었습니다.
목표는 관리자에게서 받은 강한 권한을 계속 전달하는 대신, 서버가 맡은 일을 수행할 수 있는 범위를 명시하는 것이었습니다.
물론 역할을 나눴다고 인증 정보 관리가 불필요해지는 것은 아닙니다. 전용 계정과 토큰도 보호해야 합니다. 권한을 제한하는 것은 보안에 대한 걱정을 없애는 일이 아니라, 노출되거나 잘못 사용됐을 때 영향을 받을 수 있는 범위를 줄이는 방향의 설계라고 이해했습니다.
인턴으로서 배운 것은 ‘연결 이후’를 보는 일이었습니다
이번 작업은 서버를 연결하기 위한 요청에서 시작됐습니다. 하지만 실제로 고민한 것은 연결 방법보다, 연결된 서버에 무엇을 허용할 것인지였습니다.
처음 요청만 놓고 보면 인증 정보를 전달하는 것으로 끝낼 수도 있었습니다. 그 과정에서 권한 범위를 다시 살펴본 덕분에 스키마 접근, 테이블 작업 권한, RLS 정책이 어떻게 함께 동작하는지 정리할 수 있었습니다.
특히 기억에 남는 것은 권한을 한꺼번에 넓히면 당장의 작업은 쉬워질 수 있지만, 왜 그 권한이 필요한지는 더 설명하기 어려워진다는 점입니다. 반대로 필요한 작업부터 정리하면 설정해야 할 권한도, 확인해야 할 조건도 조금씩 명확해졌습니다.
다음에는 이 구조를 ByOrbit의 구매자 계정에도 확장할 계획입니다. 구매자가 자신에게 허용된 페이지와 데이터에만 접근하도록 계정 유형별 권한을 설계하는 것이 다음 과제입니다.
그때는 구매자라는 역할을 구분하는 것에 더해, 같은 구매자 역할을 가진 계정 사이에서도 누구의 데이터인지 구분하는 조건을 함께 살펴봐야 합니다. RLS는 이런 사용자별 행 접근 조건을 표현하는 데 사용할 수 있습니다.
아직 배워야 할 것이 많지만, 이번 작업 이후 데이터베이스를 볼 때 확인하는 질문이 하나 늘었습니다.
“데이터가 잘 나오는가?”에서 그치지 않고, “이 요청에 이 데이터가 나와도 되는가?”를 함께 묻게 됐습니다.
앞으로의 설계에서도 이 질문을 놓치지 않으려고 합니다.
