LYnLab

블로그취미로그

Rails Global ID로 전역 객체 식별하기

Global ID는 Rails의 모든 객체를 식별할 수 있는 URI(Uniform Resource Identifier)입니다.

2023-03-18#프로그래밍#Ruby

💡 이 글은 작성된지 1년 이상 지났습니다. 정보글의 경우 최신 내용이 아닐 수 있음에 유의해주세요.

Global ID는 Rails의 모든 객체를 식별할 수 있는 URI(Uniform Resource Identifier)입니다. 생성된 문자열은 다음과 같은 형식을 갖습니다.

gid://MyApplication/ModelName/123

Rails에서 사용하기

Global ID는 rails에 이미 내장되어 있습니다. 우선 다음과 같이 initializer를 설정합니다.

require "global_id"

GlobalID.app ||= (ENV["APP_NAME"] || "MyApplication")

ActiveRecord 모델에서는 다음과 같이 설정하고,

# 모델에 선언
class Member < ApplicationRecord
  include GlobalID::Identification
end

객체의 Global ID 값을 확인하거나, 반대로 Global ID로 해당하는 객체를 찾을 때는 다음과 같이 합니다.

### 객체의 Global ID 값 확인
gid = Member.find(39).to_global_id
gid.to_s
# => "gid://MyApplication/Member/39"

### Global ID로 객체 불러오기
gid = "gid://MyApplication/Member/39"
GlobalID::Locator.locate(gid)
# => #<Member:... @id=39>

Signed Global ID

만약 Global ID를 외부에 노출해야한다면 모델의 이름이나 id 값이 평문으로 보이는 것이 꺼림칙할 수 있습니다. 예를 들어 JWT 형식의 API 토큰에 회원 정보를 싣고 싶을 때, 해당 회원의 id 숫자가 페이로드에 그대로 들어있다면 보안상 좋지 않겠죠.

이러한 상황에서는 Signed Global ID를 사용할 수 있습니다.

우선 initializer에 다음 한 줄을 추가합니다.

SignedGlobalID.verifier ||= GlobalID::Verifier.new(
  Rails.application.key_generator.generate_key("signed_global_id")
)

이 단계에서 생성한 키 값이 Global ID를 암호화/복호화하는 데에 사용됩니다. 암호화된 Global ID는 다음과 같이 사용할 수 있습니다.

sgid = Member.find(39).to_s
# => "XXXXXXXXXXXXXX"

GlobalID::Locator.locate_signed(sgid)
# => #<Member:... @id=39>

또한 암호화된 Signed Global ID의 유효 기간을 설정할 수도 있습니다. 만료 기간이 필수적인 API 토큰에 사용하거나, 한정된 시간동안 리소스를 외부에 공유하는 상황 등에 유용합니다.

expiring_sgid = Member.find(39).to_sgid(expires_in: 1.hour)

응용: GraphQL

Global ID는 GraphQL의 전역 객체 식별(Global Object Identification)의 수요와 궁합이 잘 맞습니다. GraphQL, 특히 Relay에서는 데이터 캐시나 refetch를 위해 모든 노드의 전역적인 식별자를 필요로 하는데, 이 때에 사용하기 적당한 것이지요.

우선 각 타입에 Relay Node 인터페이스를 지정합니다.

class Types::MemberType < GraphQL::Schema::Object
  implements GraphQL::Types::Relay::Node
end

GraphQL 스키마 설정에 다음 정보들을 추가해줍니다.

class ApplicationSchema < GraphQL::Schema
  # ...

  def self.id_from_object(object, type_definition, query_ctx)
    object.to_sgid
  end

  def self.object_from_id(object_id, query_ctx)
    GlobalID::Locator.locate(object_id)
  end
end

마지막으로, Global ID를 통해 획득한 객체가 GraphQL상의 어떤 타입에 맵핑되는지에 대한 정보가 필요합니다. 이는 타입 리졸버를 통해 구현합니다.

class ApplicationSchema < GraphQL::Schema
  def self.resolve_type(abs_type, object, query_ctx)
    case object
    when Member then Types::MemberType
    else raise GraphQL::Schema::InvalidTypeError
    end
  end

  # ...
end

참고 자료

관련된 글

Rails와 GitHub Actions에 커버리지 레포트를 달아보자

이 블로그의 CMS이기도 한 Shiori를 대폭 리팩토링하면서 테스트가 얼마나 잘 작성되어있는지 궁금해졌습니다.

Ruby on WebAssembly: 살짝 맛보기

Ruby 3.2에 추가된 WebAssembly 지원을 간단하게 테스트해봅시다.

Ruby의 and와 &&는 다르다

Ruby로 프로그램을 짜다보면 반드시 밟게되는 지뢰가 있습니다. 바로 or 과 ||, 혹은 and 와 && 연산자의 우선순위가 달라서 발생하는 일입니다.

작성한 댓글은 giscus를 통해 GitHub Discussion에 저장됩니다.

크리에이티브 커먼즈 라이선스크리에이티브 커먼즈 저작자표시크리에이티브 커먼즈 동일조건변경허락

본 사이트의 저작물은 별도의 언급이 없는 한 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.

© 2011 - 2024 Hoerin Doh, All rights reserved.

LYnLab 로고GitHubTwitterInstagram