시간에 따른 데이터 처리의 문제점과 해결책
1. Rails Way™의 한계와 원장(Ledger) 기반 접근법
Rails는 데이터베이스 행을 직접 변경하는 “현재 상태” 모델을 기본으로 하여, 계좌 잔액 변경 내역 추적, 임의 시점 잔액 계산, 정확한 잔액 조정에 어려움이 있습니다. 이를 해결하기 위해 저자는 모든 잔액 변경을 불변(immutable) 기록으로 저장하는 원장(ledger)과 유사한 회계 시스템을 제안합니다. CustomerAccount 모델에 Mutation 레코드를 연결하여 특정 시점의 잔액을 집계하는 방식은 감사 및 예측에 용이하며, 제한적인 이벤트 소싱 형태로 볼 수 있습니다.
ruby
class CustomerAccount < ActiveRecord::Base
has_many :mutations, -> { order(created_at: :desc) }
def balance(at: Time.now)
mutations.where("created_at < ?", at).sum(:amount)
end
end
class Mutation < ActiveRecord::Base
belongs_to :customer_account
end
account.mutations.create!(amount: -purchase_amount) # 음수는 인출을 의미
2. 시간 간격(Intervals) 및 타임존 처리
“시간에 따른 데이터” 시스템에서 간격은 ‘시작 포함, 끝 배타적’으로 정의되는 경우가 많습니다. 이를 정확히 다루기 위해 다음을 권장합니다.
-
컬럼명에
_before,_after접미사를 사용하여 포함/배타 여부를 명확히 합니다. -
WHERE starts_at >= ? AND ends_before < ?와 같은 쿼리 방식을 사용합니다. -
범위 유형 인덱스를 지원하는 데이터베이스를 활용하고, 간격 사이에 발생할 수 있는 간극(gaps)을 처리할 수 있도록 설계합니다. 타임존 또한 시간에 따라 변하는 간격의 한 형태로, 특정 시점의 타임존 규칙을 적용해야 합니다.
3. 캐시 키 및 이벤트 순서 관리
-
타임스탬프 기반 캐시 키의 한계: Rails의
updated_at기반 캐시 키는 동시성 환경에서 Read-after-Write 일관성을 보장하기 어렵습니다. 동일한 시간에 여러 업데이트가 발생할 경우, 캐시 키가 제대로 갱신되지 않아 오래된 데이터가 제공될 수 있습니다. 이를 보완하기 위해locking_column을 캐시 키에 포함하는 것이 좋습니다. -
웹훅 및 이벤트의 순서: 웹훅이나 이벤트는 전송 채널의 직렬성과 무관하게 순서가 보장되지 않는 ‘집합(set)’으로 간주해야 합니다. 발신자가 명확한 순서 정보를 제공하지 않는 한, 수신된 이벤트의 순서를 임의로 추정해서는 안 됩니다. 순서가 중요한 경우, 시퀀스 번호를 직접 추가하거나 웹훅을 원격 리소스를 다시 읽는 신호로만 활용하는 것이 더 안전합니다.
ruby # Mutation 모델에 시퀀스 번호 추가 예시 (고객 계정 내) class Mutation < ActiveRecord::Base belongs_to :customer_account after_create do |m| m.class.connection.execute(<<~SQL) UPDATE #{m.class.table_name} SET seq = ( SELECT COUNT(*) + 1 FROM #{m.class.table_name} WHERE customer_account_id = #{m.customer_account_id} ) WHERE id = #{m.id} SQL end end