고유성 검증의 위험

The Perils of Uniqueness Validations

Posted by npmachine on May 6, 2017

Derek Prior의 The Perils of Uniqueness Validations를 번역한 글입니다.


당신의 레일즈 애플리케이션은 아마도 주요 부분 몇 군데에서 고유성 검증을 사용하고 있을 것이다. 이 유효성 검사는 중복 레코드가 감지 될 때 미려한 사용자 경험을 제공하지만 잠시 후에 볼 수 있듯이 데이터 무결성을 보장하기에 충분하지 않다.

무엇이 잘 못 될 수 있나

예제 사용자 클래스를 들여다보자.

class User
  validates :email, presence: true, uniqueness: true
end

사용자 객체를 데이터베이스에 저장할 때, 레일즈는 해당 이메일을 가진 사용자 레코드가 이미 존재하는지 SELECT 쿼리를 수행하여 모델 유효성 검사를 수행한다. 레코드가 유효하다면 레일즈는 INSERT 문을 실행하여 사용자 객체를 저장한다. 이는 개발 과정에서 잘 작동하며 단일 프로세스 웹서버를 단독으로 실행한다면 운영 환경에서도 유용할 것이다.

그렇지만 보통 하나의 웹브릭 프로세스만 실행하지는 않는다. 분당 처리 능력을 최대로 끌어올리기 위해 여러 허로쿠(역자주: 클라우드 서비스) 프로세스 위에 유니콘을 실행한다. 만약 두 프로세스가 같은 이메일 주소를 가진 사용자를 동시에 생성하려고 한다면 무슨 일이 생기는 살펴보자.

문제가 생겼다. 우리는 데이터 일관성을 위해 고유성 검증을 원했지만 실패했다. 왜일까? 데이터베이스에 우리의 의도를 전달하지 않았기 때문이다.

고유 색인으로 해결하자

users.email 필드에 고유 제약 조건을 생성하여 데이터베이스에도 정책을 수립하자. 고유 색인으로 이를 설정할 수 있다.

class AddEmailIndexToUser
  def change
    # If you already have non-unique index on email, you will need
    # to remove it before you're able to add the unique index.
    add_index :users, :email, unique: true
  end
end

다른 방법으로는 다음처럼 마이그레이션이나 모델을 생성할 때 고유 색인을 설정할 수 있다.

rails generate model user email:string:uniq

색인을 설정하고 나면, 위에서의 시나리오가 어떻게 동작하나?

이제 데이터베이스는 일관성을 보장하지 않는 데이터 전쟁에서 최후의 보루 역할을 하게 되었다. 두번째 저장 시도는 ActiveRecord::RecordNotUnique 예외를 발생시킨다. 대부분의 경우, 이는 애플리케이션 오류로 이어진다. 더 나은 사용자 경험을 제공하고자 한다면, 콘트롤러에서 rescue 구문으로 처리하거나 클래스에서 rescue_from으로 예외를 처리할 수 있다.

Where Else Can Unique Indexes Help

고유 색인을 유용하게 사용할 수 있는 다른 곳은 어디인가

레일즈 애플리케이션은 has_one 관계를 여러 개 가질 수 있다. has_one 관계를 설정하면 관계 메소드는 콜렉션이 아닌 단일 객체를 처리하도록 설정된다. 그렇지만 has_one 자체는 데이터 무결성 보장과 관련이 없다.

예를 들어, 애플리케이션에 Profile 클래스를 추가한다고 가정하자. 사용자는 하나의 프로필을 갖게되고, 클래스는 다음과 같다.

class User
  has_one :profile
  validates :email, presence: true, uniqueness: true
end

class Profile
  belongs_to :user
end

우리는 profiles 테이블에 중복된 user_id 값이 없기를 기대하지만, has_one 은 이를 보장하지 않는다. 데이터 불일치를 방지하기 위해 profiles의 user_id에 고유한 색인을 추가해야 한다.

내가 애플리케이션에서 문제를 찾는 방법

You could search your project for validates_uniqueness_of, uniqueness: and has_one and then cross reference that with a list of indexes pulled from your database, or you could let a gem do that for you. Consistency Fail is a gem that finds missing unique indexes for you. Install and run it as detailed in the README.

validates_uniqueness_of , uniqueness: 및 has_one 을 프로젝트에서 검색한 다음 데이터베이스에서 가져온 색인 목록과 상호 참조 할 수도 있고, 혹은 젬을 사용할 수도 있다. Consistency Fail 은 누락된 고유 색인을 발견하는 젬이다. README에서 안내한 대로 설치하고 실행하면 된다.

One problem I’ve seen with consistency_fail is that the has_one searches do not properly recognize polymorphic relationships. It will suggest a unique index on the id column when what you really need is a compound index on the type and id columns.

consistency_fail 에서 보았던 한 가지 문제점은 has_one 검색이 다형성 관계를 제대로 인식하지 못한다. 다형성 관계에서 type과 id 열의 복합 색인이 필요한 경우에, id 열에만 고유 색인을 제안하고 있다.

결론

Rails는 많은 일을 하지만, 데이터 무결성 검증은 그 중 하나가 아니다. 그렇지만 관계형 데이터베이스는 데이터 무결성을 보장하도록 되어있다. 데이터베이스가 그 일을 하게 하자.


npmachine

a mediocre engineer