질문이 '대기 중(pending)', '승인(approved)', '신고(flagged)' 세 가지 상태 중 하나만 가질 수 있다고 상상해 보세요. 아니면 전화번호가 '집', '사무실', '휴대폰', '팩스' 중 하나로 분류되는 경우도 있겠죠(1982년이라면 말이지만요).
어떤 모델들은 이런 종류의 데이터를 필요로 합니다. 몇 가지 값 중 하나만 가질 수 있는 속성이며, 그 값의 집합은 거의 변하지 않는 것이죠.
순수 Ruby였다면 그냥 심볼(symbol)을 쓰면 되는 상황입니다.
PhoneNumberType이나 QuestionStatus 같은 별도 모델을 만들고 belongs_to 관계로 이 값을 관리할 수도 있지만, 그럴 가치는 없어 보입니다. YAML 파일에 담아둘 수도 있지만, 그러면 객체가 무엇을 할 수 있는지 파악하려고 완전히 다른 곳을 찾아 헤매야 합니다.
Rails 4.1에서는 바로 이 문제를 ActiveRecord enum으로 해결하고자 시도했습니다.
모델 안에서 몇 가지 값 다루기
ActiveRecord enum은 사용법이 꽤 간단합니다. 먼저 모델에 integer 타입 컬럼을 추가합니다:
bin/rails g model phone number:string phone_number_type:integer
그다음 해당 속성이 가질 수 있는 값을 나열합니다:
app/models/phone.rb
class Phone < ActiveRecord::Base
enum phone_number_type: [:home, :office, :mobile, :fax]
end
이제 숫자 대신 문자열로 속성을 다룰 수 있습니다.
이렇게 나오던 것이,
irb(main):001:0> Phone.first.phone_number_type
=> 3
이렇게 보이게 됩니다.
irb(main):002:0> Phone.first.phone_number_type
=> "fax"
문자열이든 정수든 어느 쪽으로든 속성을 변경할 수 있습니다:
irb(main):003:0> phone.phone_number_type = 1; phone.phone_number_type
=> "office"
irb(main):004:0> phone.phone_number_type = "mobile"; phone.phone_number_type
=> "mobile"
뱅(bang) 메서드를 활용할 수도 있습니다:
irb(main):005:0> phone.office!
=> true
irb(main):006:0> phone.phone_number_type
=> "office"
속성이 특정 값을 갖고 있는지 확인하는 메서드도 자동으로 생성됩니다:
irb(main):007:0> phone.office?
=> true
원하는 값을 가진 모든 객체를 조회할 수도 있습니다:
irb(main):008:0> Phone.office
Phone Load (0.3ms) SELECT "phones".* FROM "phones" WHERE "phones"."phone_number_type" = ? [["phone_number_type", 1]]
사용 가능한 모든 값과 각 값에 매핑된 숫자를 함께 확인하려면 phone_number_types 클래스 메서드를 사용하세요:
irb(main):009:0> Phone.phone_number_types
=> {"home"=>0, "office"=>1, "mobile"=>2, "fax"=>3}
덕분에 HTML 폼에 넣기도 아주 쉽습니다:
app/views/phones/_form.html.erb
<div class="field">
<%= f.label :phone_number_type %><br>
<%= f.select :phone_number_type, Phone.phone_number_types.keys %>
</div>
주의해야 할 몇 가지 사항
하지만 enum에도 단점은 없지 않습니다. 나중에 곤란한 상황에 빠지지 않으려면 다음 사항들을 염두에 두어야 합니다.
enum을 정의할 때는 순서가 중요합니다. 나중에 코드를 다시 살펴보다가 값들을 알파벳 순으로 정렬하는 게 낫겠다고 판단했다면:
app/models/phone.rb
class Phone < ActiveRecord::Base
enum phone_number_type: [:fax, :home, :mobile, :office]
end
기존 데이터의 타입은 더 이상 올바르지 않게 됩니다. 이 문제는 어떤 값에 어떤 숫자가 매핑되는지 enum에 직접 지정해 줌으로써 피할 수 있습니다:
app/models/phone.rb
class Phone < ActiveRecord::Base
enum phone_number_type: {fax: 3, home: 0, mobile: 2, office: 1}
end
하지만 근본적으로 가장 좋은 방법은 처음 정한 순서를 일관되게 유지하는 것입니다.
더 큰 문제는 Rails 밖의 세계와의 연동입니다. Rails는 enum 값을 문자열로 보여주지만, 실제로 데이터베이스에는 단순한 숫자로 저장됩니다. 따라서 원시 데이터(raw data)를 직접 들여다보는 사람은 그 숫자가 무엇을 의미하는지 전혀 알 수 없습니다. 이는 그 데이터베이스를 읽는 모든 애플리케이션이 enum 매핑 정보를 반드시 알아야 한다는 뜻이기도 합니다.
정말 다른 사람들이 이 매핑을 확인해야 한다면 데이터베이스나 YAML 파일로 내보낼 수도 있습니다. 하지만 그것은 DRY 원칙에 어긋납니다. enum을 두 곳에서 정의하게 되기 때문입니다. 여기까지 해야 하는 상황이라면, 처음부터 피하려 했던 방식, 즉 완전히 별도의 모델과 관계를 만들어 Phone이 PhoneNumberType에 belong_to 되는 구조로 가는 편이 더 나을 수 있습니다.
하지만 프로젝트를 단순하게 유지하고 싶다면, enum은 충분히 훌륭한 출발점입니다.
P.S. 혹시 놓치셨다면, 『Practicing Rails』가 7월 6일 월요일에 시작되는 Ruby Book Bundle에 포함됩니다. 다른 훌륭한 Ruby 서적 5종과 함께 대폭 할인된 가격에 만나보세요!