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 는 웹 조인 품질을 떠받치는 방어선이다.)
- Location —
engine/bouncer/ - Schema —
db/schema/bouncer.hcl,db/queries/bouncer/
책임¶
- 정책 저장 — per-guild 접근 통제 정책 (VPN/Proxy/Tor 토글, 국가 블랙리스트)
- IP 인텔리전스 조회 — port 로 추상화된 provider 에서 IP 메타데이터(국가, VPN/Proxy/Tor 플래그) 획득
- 판정(Decision) — 정책 + IP 메타데이터를 결합하여
allow | deny결정 - 감사 기록 — 모든 판정을
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 풀세트 적용 검토