테마
RP Migration Guide — canonical_sub 도입
이 가이드는 이미 logi 와 연동된 RP 가 기존 sub 외래키를 유지한 채 canonical_sub 기반 user 해석으로 안전하게 전환하는 절차를 다룹니다. logi 가 첫 통합 트리거를 production 에 켜기 전에 RP-side 작업이 완료되어 있어야 합니다.
전체 그림
기존 RP DB 는 다음과 같이 logi user 를 참조하고 있습니다:
users (RP own table)
logi_sub string unique ← logi 의 user.id마이그레이션 후:
users (그대로 유지)
logi_sub string unique
logi_identity_links (신규)
primary_user_id string ← canonical_sub
linked_user_id string unique ← 흡수된 sub
merged_via string
occurred_at datetimeRP 의 모든 user lookup 은 canonical resolver 를 통과합니다:
ruby
def find_canonical_user(sub)
canonical = LogiIdentityLink.find_by(linked_user_id: sub)&.primary_user_id || sub
User.find_by(logi_sub: canonical)
end5단계 deploy 순서
logi 와 협조하는 joint deploy 입니다. 각 단계는 다음 단계 전에 24h soak 권장.
Phase 1 — 스키마 + 비활성 코드
logi_identity_links테이블 추가 + 인덱스- canonical resolver 코드 추가 (사용 안 함, feature flag
enforce_canonical_resolution = false) - existing user lookup path 는 그대로
logi_sub직접 사용
Phase 2 — webhook receiver 활성화
/webhooks/logiendpoint 활성화 →user.merged수신 시logi_identity_linksrow INSERT- HMAC 검증 +
event_iddedupe 구현 - 이 시점에는 logi 가 아직 통합을 발화하지 않으므로 row 가 들어오지 않음 — receiver 의 코드 경로 자체가 dry-run
Phase 3 — polling reconciler 활성화
- 분 단위
GET /api/v1/events?since=<cursor>polling job - cursor 를 자기 DB 에 저장 (transaction 안에서 cursor advance + event apply)
Phase 4 — login-time resolver 활성화
- BearerAuthentication (또는 동등) concern 이 매 token 검증 시
canonical_sub를 보고logi_identity_links와 비교, 차이가 있으면 즉시 row insert - 이는 webhook/polling 누락의 마지막 안전망
Phase 5 — enforce_canonical_resolution = true flip
- 모든 user lookup 이 canonical resolver 를 통과하도록 변경
- 기존
User.find_by(logi_sub:)직접 호출이 코드베이스에 남아 있는지 grep 으로 확인 (bin/rails runner+ custom audit 권장)
flip 후에야 logi 가 통합 트리거를 production 에서 켭니다.
Rails 기준 코드 패턴
Concern 예시
ruby
# app/controllers/concerns/logi_canonical_resolution.rb
module LogiCanonicalResolution
extend ActiveSupport::Concern
private
def resolve_logi_user(sub:, canonical_sub:, linked_subs: [])
# 1) login-time fallback: webhook/polling 누락 시 여기서 보정
if canonical_sub != sub && !LogiIdentityLink.exists?(linked_user_id: sub)
LogiIdentityLink.create!(
primary_user_id: canonical_sub,
linked_user_id: sub,
merged_via: "login_time_fallback",
occurred_at: Time.current
)
end
# 2) 항상 canonical 으로 lookup
User.find_by(logi_sub: canonical_sub)
end
endPundit policy 주의
Pundit policy 가 record.user_id == current_user.id 같은 직접 비교를 한다면, 통합 후 같은 사람이지만 두 row 가 다른 logi_sub 를 가진 케이스에서 권한이 빠질 수 있습니다. 권장 패턴:
ruby
class ApplicationPolicy
def same_canonical?(record_user)
LogiCanonical.same_owner?(current_user, record_user)
end
endLogiCanonical.same_owner? 는 두 user 의 logi_sub 를 각각 canonical 으로 resolve 한 뒤 비교. 자세한 patterns 는 Rails 통합 가이드 참고.
Merge Reconciler
webhook 또는 polling 으로 user.merged 가 들어오면:
ruby
class MergeReconciler
def apply(event)
survivor = event[:data][:survivor_canonical_sub]
merged = event[:data][:merged_sub]
LogiIdentityLink.create_or_find_by!(linked_user_id: merged) do |link|
link.primary_user_id = survivor
link.merged_via = event[:data][:merged_via]
link.occurred_at = Time.parse(event[:data][:triggered_at])
end
# 도메인 데이터의 logical merge 는 RP 정책에 따라.
# 예: merged 의 active session 강제 종료, 두 user 의 주문 history 합치기, etc.
end
endcreate_or_find_by! 가 idempotent 를 보장하므로 같은 webhook 이 두 번 와도 안전.
무엇이 안 바뀌나
- 기존 logi_sub 외래키는 그대로 유지. row 의 logi_sub 가 마이그레이션으로 바뀌지 않습니다.
- 새로 가입한 user 의 logi_sub 도 그대로.
sub은 OIDC 상 stable. - RP 의 외부 API 응답 — user 식별자를 외부에 노출했다면 그 값도 그대로.
무엇이 바뀌나
- 통합이 일어난 사용자가 새 토큰을 받을 때
canonical_sub가sub과 다름. linked_subs가 채워질 수 있음.- 모든 RP-side user lookup 이 canonical resolver 한 hop 을 거쳐야 함.
검증 체크리스트
flip 전:
- [ ]
LogiIdentityLink테이블 + UNIQUE(linked_user_id) 인덱스 - [ ] HMAC +
event_iddedupe 구현된 webhook receiver - [ ] cursor 기반 polling reconciler (분 단위 권장)
- [ ] login-time canonical resolver
- [ ]
User.find_by(logi_sub:)직접 호출 모두 canonical resolver 경유로 교체 - [ ] Pundit policy / scope 가 canonical 비교 사용
- [ ] feature flag
enforce_canonical_resolution = true로 flip 준비
logi 운영자에게 flip 완료를 통보한 뒤 logi 가 T1 트리거를 production 에 enable 합니다.