(이 글은 몇 달 전 제 뉴스레트 구독자들에게 보낸 내용입니다. 마음에 드셨다면 구독하고 비슷한 글을 더 만나보세요!)
얼마 전 제가 운영하는 앱 하나에서 꽤 묘한 상황을 마주했습니다.
Article 모델이 있다고 가정해 보겠습니다. 이 글들은 처음에는 초안(draft) 상태로 생성됩니다. 초안은 최대한 가벼워야 합니다. 본문이 없어도, 심지어 제목이 없어도 즉시 생성하고 수정할 수 있어야 하죠.
하지만 이 글들을 발행할 때는 추가적인 검증이 필요합니다. 제목과 본문 등 필수 요소가 모두 갖춰져 있어야 하는 것입니다.
if 문을 여러 개 작성해 직접 검사할 수도 있습니다. 하지만 그렇게 하면 Rails가 제공하는 편리한 폼 처리 기능을 활용할 수 없습니다. Rails가 잘 처리하는 영역은 Rails에 맡기는 것이 나중에 골치 아픈 일을 크게 줄여준다는 사실을 저는 경험으로 알고 있습니다. 그렇다면 검증(validation) 기능을 그대로 활용할 방법은 없을까요?
if와 unless는 어떨까?
검증에는 :if와 :unless 옵션을 사용할 수 있지만, 항상 깔끔한 결과를 주지는 않습니다.
class Article < ActiveRecord::Base
# 초안 모드에서 검증
validates_presence_of :author_id
# 발행 시에만 검증
validates_presence_of :body, unless: lambda { |o| o.draft? }
...
end
게다가 이건 제가 원하는 발행 방식이 아닙니다. 저는 글 발행을 글이 놓인 상태(state)가 아니라 하나의 행위(action)로 바라보고 싶습니다. 속성을 설정하고 이를 검사하는 방식은 올바른 해답이 아닙니다.
잘 알려지지 않은, 문서화도 부족한 Rails 기능
이 문제는 Rails의 검증 컨텍스트(validation context)에 딱 맞는 케이스입니다. 저는 DHH가 작성한 gist를 통해 커스텀 검증 컨텍스트를 처음 접했습니다. 하지만 이것이 무엇이고 어떻게 사용하는지에 대한 좋은 설명을 찾기가 쉽지 않았습니다.
커스텀 컨텍스트에 대한 공식 문서는 상당히 빈약합니다. API 문서에서 관련 언급을 찾을 수 있는 곳은 단 한 군데뿐이었고, 정작 검증 관련 API 문서에는 커스텀 컨텍스트에 대한 설명이 전혀 없습니다! 기능의 이름조차 모른다면 관련 문서를 어떻게 찾을 수 있겠습니까? 그래서 더욱 어려웠습니다.
결국 좋은 개요 글을 여기서 찾았습니다: https://blog.arkency.com/2014/04/mastering-rails-validations-contexts/. 읽어볼 가치가 충분한 글입니다. 그럼 앞선 예제에 적용하면 어떤 모습이 되는지 살펴보겠습니다.
class Article < ActiveRecord::Base
# 초안 모드에서 검증
validates_presence_of :author_id
# 발행 시에만 검증
validates_presence_of :body, on: :publish
...
end
class ArticleController < ApplicationController
respond_to :html
def create
@article = Article.create(article_params)
respond_with @article
end
def publish
@article = Article.find(params[:id])
@article.attributes = article_params
@article.save(context: :publish)
respond_with @article
end
private
def article_params
params.
require(:article).
permit(:body, :title, :author_id)
end
end
꽤 괜찮죠? 다만 조금 어색한 점이 있다면 publish 메서드에서 update 대신 save를 사용해야 한다는 것입니다. update는 검증 컨텍스트를 파라미터로 받을 수 없기 때문입니다. (어쩌면 풀 리퀘스트를 보낼 좋은 기회일지도 모르죠?)
저장 그 이상의 활용
커스텀 검증 컨텍스트는 레코드를 상황에 따라 다르게 저장하는 것 이상으로 유용합니다. 컨텍스트는 valid?와도 함께 동작합니다.
@article.valid?(:publish)
즉, "이 객체가 해당 속성을 만족하는가? 만족하지 않는다면 그 이유는 무엇인가?"라는 질문에 답해야 하는 모든 곳에서 검증 컨텍스트를 활용할 수 있습니다.
DHH는 valid?(:context)에 위임하는 -able?로 끝나는 메서드를 만드는 패턴을 보여줬는데, 저는 이 모습이 정말 마음에 듭니다.
class Article
validate :has_been_published, on: :view
validate :is_not_spam, on: :view
def viewable?
valid? :view
end
private
def has_been_published
if published_at.future?
errors.add(:published_at, "is in the future")
end
end
def is_not_spam
if is_spam?(body)
errors.add(:body, "has been detected as spam")
end
end
end
이렇게 하면 검사 결과로 단순한 true/false 이상의 정보를 얻을 수 있습니다.
unless @article.viewable?
# @article.errors가 채워져 있음
end
이 글이 조회 가능한가요? 불가능하다면, 그 이유는 무엇일까요?
적당히 가벼운 해법
이런 종류의 검증 동작을 구현하는 방법은 몇 가지 더 있습니다. 자체 검증 로직을 갖춘 커스텀 ActiveModel 서비스 객체를 만들 수도 있고, 검증에 :if나 :unless 옵션을 붙일 수도 있습니다. 하지만 Rails의 많은 기능이 그렇듯, 커스텀 검증 컨텍스트는 가독성과 유연성 사이에서 균형 잡힌 훌륭한 절충안입니다.