Ask-It 프로젝트는 실시간 Q&A 서비스로, 하나의 세션 안에서 여러 사용자가 질문과 답변을 주고받을 수 있는 기능을 제공했다.
이때 중요한 이슈 중 하나는 권한 관리였다. 누구나 질문을 올릴 순 있지만, 모든 유저가 동일하게 세션을 종료하거나 다른 사람을 호스트로 지정할 수 있다면 혼란이 생길 수 있기 때문이다.
최초 구현에서는 다음과 같이 슈퍼 호스트
(세션 생성자), 서브 호스트
, 참가자
세 가지 역할을 두고, 이 역할에 따라 권한을 달리 부여했다.
권한 | 슈퍼 호스트 | 서브 호스트 | 참가자 |
---|---|---|---|
1. 세션 종료 | O | X | X |
2. 타인에게 호스트 권한 부여 | O | X | X |
3. 타인의 호스트 권한 해제 | O | X | X |
4. 질문 삭제 | O | O | X |
5. 답변 삭제 | O | O | X |
6. 질문 고정 | O | O | X |
7. 질문에 대한 답변 완료 기능 | O | O | X |
이러한 로직은 if문으로 '역할'을 판단하여 처리되고 있었다.
기존에는 두 개의 테이블에서 각각 다른 방식으로 권한을 확인했다:
createUserId
컬럼과 현재 사용자의 ID를 비교하여 확인했다:// 예시 코드
if (session.createUserId === currentUserId) {
// 슈퍼 호스트임
}
isHost
컬럼으로 확인했다:// 예시 코드
if (userSessionToken.isHost) {
// 서브 호스트임
}
그리고 다음과 같이 역할을 기반으로 권한을 확인했다:
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) {
const { isHost } = await this.sessionAuthRepository.findByToken(token);
if(!isHost)
throw new ForbiddenException('권한이 없습니다.');
//슈퍼 호스트이거나 서브 호스트인 경우 삭제 권한 부여
하지만 이런 방식은 새로운 역할이 추가되거나 권한 범위가 변경될 때마다 코드를 수정해야 하는 단점이 있었다.
이를테면 위와 같은 코드에서 질문 삭제 권한을 서브 호스트에게서는 박탈한다고 한다면, 또 분기 처리가 생겨야할 것이다.
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) {
const { isHost } = await this.sessionAuthRepository.findByToken(token);
if(!isHost)
throw new ForbiddenException('권한이 없습니다.');
if(/*서브 호스트인 경우*/)
throw new ForbiddenException('권한이 없습니다.');
요구사항이 추가, 삭제될수록 권한 로직으로 인해 서비스 레이어가 복잡해진다는 문제가 있다. 이는 코드의 유지보수성을 떨어뜨리고, 새로운 기능 추가 시 개발자가 고려해야 할 사항을 증가시키는 원인이 되었다. 이러한 문제를 해결하기 위해 우리는 새로운 접근 방식이 필요했다.
역할 기반 권한 관리(Role Based Access Control, RBAC)는 말 그대로 “역할(Role)을 기반으로 권한(Permission)을 관리”하는 방법이다.
사실 처음부터 RBAC라는 용어를 알고 있었던 것은 아니었다.
우연히 DB의 권한 관리 로직을 공부하고 있었는데, 역할과 권한을 분리해두고, 사용자가 어떤 역할을 맡았는지에 따라 권한을 부여하는 방식이 인상적이었다.
예를 들어 MySQL에서 다음과 같이 “읽기 전용”, “쓰기 가능” 등의 역할을 생성하고, 해당 역할에 필요한 권한을 묶어둘 수 있다.
-- 역할 생성
CREATE ROLE 'role_emp_read', 'role_emp_write';
-- 역할에 권한 부여
GRANT SELECT ON employees.* TO 'role_emp_read';
GRANT INSERT, UPDATE, DELETE ON employees.* TO 'role_emp_write';
-- 사용자 생성
CREATE USER 'reader'@'127.0.0.1' IDENTIFIED BY 'qwerty';
CREATE USER 'writer'@'127.0.0.1' IDENTIFIED BY 'qwerty';
-- 사용자에게 역할 부여
GRANT 'role_emp_read' TO 'reader'@'127.0.0.1';
GRANT 'role_emp_read', 'role_emp_write' TO 'writer'@'127.0.0.1';
이는 “역할”에 권한을 묶어두고, “사용자”는 역할만 부여받으면 자동으로 그 역할이 가진 권한들까지 받아오는 구조다. 이런 방식은 새로운 권한을 추가하거나 제거할 때도 코드나 쿼리를 여러 곳 수정할 필요 없이, 역할-권한 매핑만 변경하면 되는 장점이 있다.
Ask-It 프로젝트에도 RBAC 개념을 적용하기 위해, 각 역할(Role
)과 권한(Permission
), 그리고 둘 사이를 연결해주는 RolePermission
테이블을 새로 정의했다.
이렇게 역할-권한 매핑을 더 세밀하게 관리할 수 있게 되었다. 예를 들어, 서브 호스트에게 질문 삭제 권한은 없지만 답변 삭제 권한은 주고 싶다면, 해당 역할의 권한 목록에서 DELETE_QUESTION 권한만 제외하면 된다. 또한 새로운 역할이 필요할 때도 기존 권한들을 조합해 손쉽게 새로운 역할을 만들 수 있다. 이는 권한 관리의 유연성을 크게 향상시켰다.
권한 체크 로직은 다음과 같이 개선했다.
async deleteQuestion(questionId: number, question: Question, { token }: BaseDto) {
const { role } = await this.sessionAuthRepository.findByTokenWithPermissions(token);
const granted = role.permissions.some(
({ permissionId }) => permissionId === Permissions.DELETE_QUESTION,
);
if (!granted)
throw new ForbiddenException('권한이 없습니다.');
// 이하 생략...
}
여기서 findByTokenWithPermissions
메서드는 UserSessionToken
을 조회한 후, 그에 연결된 Role → Permission
목록을 가져온다. 그리고 나서 “이 사용자가 DELETE_QUESTION
권한을 가지고 있는지” 여부를 확인해 권한을 부여한다.
이렇게 하면, “서브 호스트”에게서 질문 삭제 권한을 빼고 싶을 때 굳이 코드를 수정하지 않아도 된다. DB에서 “서브 호스트”의 RolePermission
레코드 중 DELETE_QUESTION
를 제거하기만 하면 끝난다. 권한 확장도 마찬가지 방식으로 손쉽게 할 수 있다.
이번 리팩토링을 통해 MySQL의 권한 관리 시스템에서 영감을 받아 RBAC를 도입해보았다. 실제로 AWS IAM도 이와 유사한 방식으로 사용자, 그룹, 역할, 정책을 분리하여 권한을 관리하는 것으로 알고 있다. 확실히 이 방식이 권한 관리가 복잡해질수록 유용한 방법이라는 생각이 들었다.
처음에는 단순한 조건문으로 시작했던 권한 체크 로직이 이제는 더 유연하고 확장 가능한 시스템으로 발전한 것 같아 뿌듯하다. 특히 새로운 역할이나 권한이 추가될 때마다 코드를 수정하지 않아도 된다는 점이 가장 큰 성과라고 생각한다. 앞으로 서비스가 성장하면서 더 복잡한 권한 구조가 필요해지더라도, 이 기반 위에서 충분히 대응할 수 있을 것 같다.
이번 리팩토링을 통해 배운 점은 '확장 가능한 설계의 중요성'이다. 초기에는 단순한 구현으로 시작하더라도, 서비스의 성장을 고려한 유연한 설계가 필요하다는 것을 깨달았다.