웹 애플리케이션에서 특정 역할(role)과 권한(permission)을 부여하는 기능은 매우 흔한 요구 사항입니다.
많은 웹 애플리케이션이 관리자와 일반 사용자를 구분해 접근 권한을 제한합니다. 이는 보통 사용자가 관리자인지 여부를 판단하는 간단한 불리언(boolean) 값으로 처리되곤 하지만, 실제로 역할과 권한 체계는 훨씬 더 복잡해질 수 있습니다.
애플리케이션의 핵심 가치는 특정 데이터와 작업에 대한 접근을 제한하는 데 있습니다. 이 부분은 절대 실수해서는 안 되는 영역입니다. 이 글에서는 기본적인 Ruby on Rails 애플리케이션에서 역할과 권한을 구현하는 방법을 자세히 설명합니다.
권한 관리에 Gem이 꼭 필요할까?
결론부터 말하면, 반드시 필요하지는 않습니다. 애플리케이션 규모가 작고 코드베이스에 의존성을 추가하고 싶지 않다면 직접 구현하는 것이 좋습니다.
다만 대안을 찾고 있다면, 역할과 권한 처리에 널리 쓰이는 인기 Gem들을 살펴보겠습니다.
Devise
인증(authentication)과 역할 관리를 위한 Gem으로, 복잡하지만 강력한 솔루션입니다. GitHub에서 21.7k 스타를 받은 이 글에서 가장 인기 있는 저장소이지만, 역할 관리 이상의 많은 기능을 제공합니다. 주로 인증 솔루션으로 알려져 있으므로, 정말 견고한 라이브러리가 필요한 경우에만 도입하는 것이 좋습니다.Pundit
단순한 Ruby 객체를 활용하는 Gem으로, 여기서 소개할 정책(policy) Gem 중 가장 사용하기 쉽습니다. 최소한의 인가(authorization) 기능을 제공하며 순수 Ruby를 사용하는 것과 비슷한 느낌입니다. GitHub에서 7.3k 스타를 기록하며 현재 가장 인기 있는 정책 Gem입니다.CanCan
특정 사용자가 접근할 수 있는 리소스를 제한하는 인가 라이브러리입니다. 다만 오랫동안 유지보수가 중단되어 Rails 3 이하 버전에서만 동작한다는 한계가 있습니다.CanCanCan
Ruby 및 Ruby on Rails용 또 다른 인가 라이브러리로, CanCan의 대체재이며 현재도 활발히 유지보수되고 있습니다. GitHub에서 4.9k 스타로 상대적으로 인기는 낮지만, 안정적으로 잘 관리되고 있어 실무에서 충분히 유용합니다.
이 Gem들은 모두 훌륭하지만, 사실 순수 Ruby만으로도 권한 시스템을 어렵지 않게 만들 수 있습니다. 이번 글에서는 Policy Object(정책 객체) 패턴이라는 전략을 사용해 Gem 없이 권한을 관리하는 방법을 보여드리겠습니다.
Policy Object 패턴이란?
Policy Object는 권한과 역할을 다루기 위해 사용되는 디자인 패턴입니다. 어떤 대상이 특정 작업을 수행할 수 있는지 확인해야 할 때마다 활용할 수 있으며, 복잡한 비즈니스 규칙을 하나의 객체 안에 캡슐화합니다. 덕분에 서로 다른 규칙을 가진 다른 정책 객체로 손쉽게 교체할 수 있습니다.
외부 의존성은 모두 정책 객체에 주입(injection)되어 권한 검사 로직이 캡슐화되므로, 컨트롤러와 모델 코드가 깔끔하게 유지됩니다. Pundit, CanCan, CanCanCan 같은 Gem들도 바로 이 패턴을 구현한 것입니다.
순수 Policy Object의 세 가지 규칙
- 반환값은 반드시 불리언(boolean)이어야 합니다.
- 내부 로직은 최대한 단순해야 합니다.
- 메서드 내부에서는 전달받은 객체의 메서드만 호출해야 합니다.
구현하기
먼저 네이밍 컨벤션부터 살펴보겠습니다. 파일명에는 _policy 접미사를 붙이고, 클래스명은 Policy로 끝냅니다. 메서드 이름은 항상 ? 문자로 끝나야 합니다(예: UsersPolicy#allowed?).
간단한 예제 코드는 다음과 같습니다.
class UsersPolicy
def initialize(user)
@user = user
end
def allowed?
admin? || editor?
end
def editor?
@user.where(editor: true)
end
def admin?
@user.where(admin: true)
end
end
어떤 상황에서 사용해야 할까?
앱에 제한된 접근 유형과 제한된 작업이 두 가지 이상 존재할 때 Policy Object를 적용하면 좋습니다. 예를 들어 게시글 생성에 다음 조건들이 붙는 경우를 생각해봅시다.
- 최소 한 개 이상의 태그가 필요하고,
- 관리자와 에디터만 생성할 수 있으며,
- 에디터는 이메일 인증을 완료해야 한다.
Policy Object 없이 작성한 컨트롤러 예제는 다음과 같습니다.
class PostsController < ApplicationController
def create
if @post.tag_ids.size > 0
&& (current_user.role == 'admin'
|| (current_user.role == 'editor' && current_user.verified_email))
# create
end
end
end
위 조건문은 너무 길고 지저분해서 가독성이 떨어집니다. 바로 이럴 때 Policy Object 패턴을 적용해야 합니다.
PostsCreationPolicy를 만들어 보겠습니다.
class PostsCreationPolicy
attr_reader :user, :post
def initialize(user, post)
@user = user
@post = post
end
def self.create?(user, post)
new(user, post).create?
end
def create?
with_tags? && author_is_allowed?
end
private
def with_tags?
post.tag_ids.size > 0
end
def author_is_allowed?
is_admin? || editor_is_verified?
end
def is_admin?
user.role == 'admin'
end
def editor_is_verified?
user.role == 'editor' && user.verified_email
end
end
Policy Object를 적용한 컨트롤러는 훨씬 깔끔해집니다.
class PostsController < ApplicationController
def create
if PostsCreationPolicy.create?(current_user, @post)
# create
end
end
end
Rails에서 Policy Object 활용하기
app/policies 디렉터리를 만들고 모든 정책 클래스를 그곳에 배치하세요. 컨트롤러에서 호출할 때는 개별 액션 안에서 직접 호출하거나, 아래처럼 before_action을 활용할 수 있습니다.
class PostsController < ApplicationController
before_action :authorized?, only: [:edit, :create, :update, :destroy]
def authorized?
unless ::PostsCreationPolicy.create?(current_user, @post)
render :file => "public/404.html", :status => :unauthorized
end
end
end
Policy Object 테스트하기
컨트롤러의 동작을 테스트하는 것도 간단합니다.
require 'rails_helper'
RSpec.describe "/posts", type: :request do
describe "when user is not allowed" do
let(:user_not_allowed) { create(:user, admin: false, editor: false) }
let(:tag) { create(:tag) }
let(:valid_attributes) { attributes_for(:post, tag_id: tag.id) }
before do
sign_in user_not_allowed
end
describe "GET /index" do
it "return code 401" do
diet = Post.create! valid_attributes
get edit_post_url(post)
expect(response).to have_http_status(401)
end
end
end
end
정책 자체를 테스트하는 것도 마찬가지로 쉽습니다. 각각 하나의 책임만 가지는 작은 메서드들이 많기 때문입니다.
require 'rails_helper'
RSpec.describe PostsCreationPolicy do
describe "when user is not allowed" do
let(:user) { create(:user, editor: false, admin: false) }
let(:user_editor) { create(:user, editor: true, email: verified) }
let(:tag) { create(:tag) }
let(:post) { create(:post, tag_id: tag.id) }
describe ".create?" do
context "when user is allowed" do
it "creates a new post" do
expect(described_class.create?(user_editor, post)).to eq(true)
end
end
context "when user is not allowed" do
it "does not create a new post" do
expected(described_class.create?(user, post)).to eq(false)
end
end
end
# ...더 많은 테스트 케이스
end
end
이렇게 모든 시나리오에서 객체의 생성 허용 여부를 검증할 수 있습니다.
마치며
Policy Object 패턴은 개념이 작지만 그 효과는 매우 큽니다. 단순하든 복잡하든 권한 처리가 필요할 때마다 정책 객체 적용을 고려해 보세요. RSpec으로 테스트할 때도 데이터베이스 레코드 없이 순수 Ruby 객체인 정책만 테스트하면 되므로, 테스트가 간단하고 빠르게 진행됩니다.