콘텐츠로 이동

ADR-0033: Bouncer Domain for Join Access Control

Umbra 는 웹 조인 플로우의 접근 통제를 담당하는 Supporting 도메인 engine/bouncer/ 를 신설한다. 이 도메인은 VPN/Proxy/Tor 감지와 국가 기반 차단 정책을 평가하고, 향후 계정 연령·디바이스 지문·리스크 점수 등 확장을 위한 단일 책임 지점이 된다.

Status

Proposed

  • Decided at — 2026-04-18
  • Decided by — Pablo

Context

웹 조인(join.umbra.ink/{slug}, flows/web-join.md) 은 Free Plan 부터 제공되는 유일한 Recovery 관련 외부 진입점이다. 현재 흐름은 Discord OAuth2 를 완료한 모든 방문자를 guilds.join 으로 길드에 추가하며, 관리자가 네트워크·지역 기준으로 차단할 방법이 없다.

운영상 다음 문제가 반복된다.

  • Ban evasion — 기존에 밴 당한 사용자가 VPN 으로 IP 를 바꿔 재가입
  • Raid coordination — Tor / 공용 VPN 뒤에서 조직화된 대량 가입
  • 지역 기반 컴플라이언스 — 특정 국가(제재국, 운영 미지원 지역)에서의 접근 차단 요구

이 정책을 어디에 둘지 후보는 세 가지였다.

  • engine/member 에 통합 — 웹 조인 플로우 안에서 조건 체크로 추가
  • engine/guild 설정으로 추가 — 길드 설정 테이블에 정책 필드만 확장
  • 별도 도메인 신설 (engine/bouncer) — 접근 통제를 독립 Bounded Context 로 분리

세 번째 안은 초기 비용(폴더·스키마·이벤트 분리)을 감수하지만, 이후 확장(계정 연령, 디바이스 지문, 리스크 점수, 글로벌 차단 리스트 공유) 을 하나의 책임점으로 흡수할 수 있다. Member 는 "멤버십 상태" 에, Guild 는 "길드 식별·설정" 에 집중할 수 있게 된다.

추가로, 접근 통제 로직은 외부 IP 인텔리전스 공급자(MaxMind, ipquery, 유료 GeoIP2 등) 에 의존한다. 공급자 교체 가능성을 port/adapter 경계로 열어두려면 별도 도메인이 자연스럽다.

Decision

Supporting 도메인 engine/bouncer/ 를 신설한다.

분류

  • Type — Supporting (Core 가 아니다. 경쟁 우위의 핵심은 Recovery 이며, Bouncer 는 웹 조인 품질을 떠받치는 방어선이다.)
  • Locationengine/bouncer/
  • Schemadb/schema/bouncer.hcl, db/queries/bouncer/

책임

  1. 정책 저장 — per-guild 접근 통제 정책 (VPN/Proxy/Tor 토글, 국가 블랙리스트)
  2. IP 인텔리전스 조회 — port 로 추상화된 provider 에서 IP 메타데이터(국가, VPN/Proxy/Tor 플래그) 획득
  3. 판정(Decision) — 정책 + IP 메타데이터를 결합하여 allow | deny 결정
  4. 감사 기록 — 모든 판정을 bouncer.decisions 에 저장하여 사후 분석·튜닝 가능

책임 아님

  • 멤버 레코드 생성·변경 (Member 담당)
  • 길드 설정 전반 (Guild 담당)
  • 사용자 계정 신원 (Identity 담당)
  • Discord 권한 체크 (Licensing 담당)

인터페이스

Bouncer 는 웹 조인 플로우에서 Evaluate(ctx, guildID, ip) -> Decision 단일 진입점으로 호출된다. 실패(공급자 장애 등) 시 정책에 따라 fail-open/fail-close 를 설정 가능.

상세 port/adapter·스키마·이벤트는 domain/bouncer.md 참조.

의존 관계

  • Guild — 길드 존재 확인(Open Host Service 로 읽기)
  • Licensing — 기능 권한 체크 (Paid-only, ADR-0035)
  • Audit — 판정 결과를 감사 이벤트로 발행 (Outbox)
  • Notification — (옵션) 차단 건을 관리자에 알림

Outbox 단방향 원칙(ADR-0016) 준수. Bouncer 는 다른 도메인에 이벤트를 발행하지만 구독은 없음(MVP).

Consequences

Positive

  • 단일 책임 지점 — 모든 접근 통제 관련 변경이 한 도메인에 모인다
  • 확장 경로 명확 — 계정 연령, 디바이스 지문, 외부 리스크 피드 등 추가 시 영향 범위 국한
  • Provider 교체 가능 — port/adapter 경계로 GeoLite2 → GeoIP2 Anonymous IP 전환 시 adapter 만 교체
  • Member 도메인 단순 유지 — 웹 조인 플로우에서 조건 로직이 흩어지지 않음
  • 감사 가능bouncer.decisions 가 독립 테이블로 존재하여 튜닝·감사에 활용

Negative

  • 초기 비용 — 새 폴더·schema·sqlc 큐리·Go 패키지 추가 필요
  • 플로우 복잡도 ↑ — 웹 조인이 Bouncer gate 단계를 거쳐 분기가 하나 더 생김
  • 외부 의존 ↑ — 신규 외부 공급자(IP 인텔리전스) 의존 추가. 장애 시 fail 정책 설계 필요

Neutral

  • Supporting 분류라 Core 수준의 풀 Hexagonal 은 강제되지 않지만, 공급자 교체 여지를 위해 outbound port 는 명시적으로 분리한다 (Core 아닌 도메인 중 예외적으로 adapter 경계를 분명히 하는 패턴)

Alternatives considered

Alternative 1: Member 도메인에 통합

Pros

  • 추가 폴더·스키마 없음
  • 웹 조인 플로우 안에서 자연스럽게 체크

Cons

  • Member 는 "멤버십 상태" 도메인인데 접근 통제 로직이 섞임 (응집도 저하)
  • 향후 계정 연령·디바이스 지문 등 멤버와 무관한 정책이 추가되면 Member 비대화
  • IP 인텔리전스 provider 의존이 Member 에 스며들어 테스트·교체 어려움

Why rejected — 확장을 고려하면 도메인 경계가 흐려진다. 지금은 간단해 보이지만 Phase 2 (review queue, 리스크 점수) 에서 분리 비용이 커진다.

Alternative 2: Guild 설정으로만 추가

Pros

  • 최소 변경 (Guild 테이블에 access_policy_json 같은 필드만 추가)

Cons

  • Guild 는 "길드 식별·메타데이터" 도메인. 정책 평가 로직까지 가지면 책임 과대
  • 판정 기록(decisions) 저장소 위치 모호
  • provider 호출 adapter 를 Guild 가 가지는 건 부자연스러움

Why rejected — 정책 "저장" 만 생각하면 가능하지만 "평가·기록" 까지 포함하면 Guild 와 무관한 로직이 쌓인다.

Alternative 3: Edge(Cloudflare WAF) 에서만 처리

Pros

  • 구현 없음 — CF 규칙만으로 국가 차단 가능
  • 빠른 차단 (애플리케이션 도달 전)

Cons

  • per-guild 정책 불가능 (CF 는 전역)
  • VPN/Proxy/Tor 세밀 감지 약함 (IP intelligence 유료 플랜 필요)
  • 차단 이력을 Umbra 에서 참조·분석 불가
  • 대시보드 토글을 CF API 로 반영하는 역방향 파이프라인 필요

Why rejected — 대시보드에서 토글 가능한 per-guild 정책이 제품 요구사항이라 CF 전역 규칙으로는 충족 불가.

Alternative 4: access_control 이름

Pros

  • 중립적, 확장성 표현

Cons

  • 긴 이름, 다른 도메인명(audit, guild, member) 과 톤 불일치
  • "접근 통제" 는 authz 와 혼동 가능 (Licensing 권한 체크와 구분 모호)

Why rejected — "Bouncer" 는 클럽 문지기 은유로 직관적이며, 기존 도메인명의 단일 명사 컨벤션(audit, guild, member, recovery...) 과 정합한다.

Compliance

  • engine/bouncer/ 폴더 존재 여부는 architect agent 가 ARCHITECT.md 와 대조하여 검증
  • 다른 도메인에서 IP 인텔리전스 provider 를 직접 호출하지 않음 (grep 가능한 패키지 경계)
  • 판정 로직은 bouncer.Service.Evaluate 단일 진입점만 사용
  • bouncer.decisions 테이블은 Bouncer 전용 schema 에만 존재

Revisit triggers

  • Bouncer 기능이 4개 이상(VPN, Tor, 국가, 계정연령, 디바이스…) 으로 늘어나면 sub-context 분리 검토 (Recovery 내부 패턴 참고)
  • 판정 응답 시간이 웹 조인 UX(3초 이내 ack) 에 영향을 주면 캐시·비동기 전략 재검토
  • 정책 평가 부하가 높아지면 Core 승격 및 Hexagonal 풀세트 적용 검토

References

  • ADR-0016 — Outbox 로 Audit 이벤트 발행
  • ADR-0019 — Supporting 도메인은 단순 구조, 단 outbound port 는 명시
  • ADR-0034 — IP 인텔리전스 공급자 선정
  • ADR-0035 — Paid-only 플랜 제한
  • ADR-0011 — Plan 기반 기능 게이팅 맥락