Skip to main content

Command Palette

Search for a command to run...

[Ash] Scope

Updated
•2 min read•View as Markdown

Multi-Tenancy 기반의 코드

Ash로 멀티테넌트 앱을 개발하다 보면 아래와 같은 코드를 자주 작성하게 된다.

# 매번 tenant 와 actor 를 반복해서 전달해야 함
Clubs.create_club!(params, actor: current_user)
Clubs.list_notices(tenant: current_club, actor: current_user) 
Clubs.invite_member(params, tenant: current_club, actor: current_user)

Club 내부에서의 모든 action 들에 대한 권한 검증을 위해 현재 User 가 활동하는 Club 의 Context 와 User 정보를 매번 전달받게된다.

문제점:

  • 같은 tenant와 actor를 계속 반복

  • 실수로 tenant나 actor를 빼먹을 가능성이 있음

  • 코드가 길어지고 패턴이 일관되지 않음

Ash Scope

Ash Scope는 여러 옵션을 하나로 묶어서 전달하는 역할을 한다:

# 한 번만 설정
scope = %MyApp.Scope{user: current_user, club: current_club}

# 깔끔하게 사용
Clubs.create_club!(params, scope: scope)
Clubs.list_notices(scope: scope)
Clubs.invite_member(params, scope: scope)

Ash Scope의 역할

1. 옵션 변환

Scope를 전달하면 Ash가 내부적으로 개별 옵션으로 변환한다:

# 이렇게 호출하면
Clubs.create_club!(params, scope: scope)

# Ash가 내부적으로 이렇게 변환
Clubs.create_club!(params, actor: scope.user, tenant: scope.club)

2. 구현 방법

Ash.Scope.ToOpts 프로토콜을 구현해서 Ash에게 변환 방법을 알려준다:

defmodule MyApp.Scope do
  defstruct [:user, :club]

  defimpl Ash.Scope.ToOpts do
    def get_actor(%{user: user}), do: {:ok, user}
    def get_tenant(%{club: club}), do: {:ok, club}
    def get_context(_), do: {:ok, %{}}
    def get_tracer(_), do: :error
    def get_authorize?(_), do: :error
  end
end

3. 자동 생성

플러그에서 현재 사용자와 클럽 정보를 자동으로 Scope로 만든다:

defmodule MyAppWeb.AssignScope do
  def call(conn, _opts) do
    scope = %MyApp.Scope{
      user: conn.assigns.current_user,
      club: get_current_club(conn)
    }
    assign(conn, :current_scope, scope)
  end
end

사용 예시

LiveView에서

def handle_event("create", params, socket) do
  scope = socket.assigns.current_scope

  case Clubs.create_notice(params, scope: scope) do
    {:ok, _} -> {:noreply, put_flash(socket, :info, "생성 완료")}
    {:error, _} -> {:noreply, put_flash(socket, :error, "생성 실패")}
  end
end

복잡한 비즈니스 로직에서

def create_event_with_notifications(params, scope: scope) do
  with {:ok, event} <- Events.create(params, scope: scope),
       {:ok, _} <- Notifications.send_to_members(event, scope: scope),
       {:ok, _} <- Logs.record_activity(event, scope: scope) do
    {:ok, event}
  end
end

핵심 가치

Ash Scope는 단순히 코드 중복을 제거하는 도구다:

  • Before: actor: user, tenant: club을 매번 반복

  • After: scope: scope 하나로 통일

멀티테넌트 앱에서 반복되는 컨텍스트 정보를 깔끔하게 관리할 수 있게 해준다.

More from this blog

Ash Typescript

TL;DRElixir의 Ash Framework로 정의한 리소스를 TypeScript와 바로 연결해 주는 툴.백엔드 스펙을 따로 문서화하지 않아도 타입 안정성과 동기화를 자동으로 보장한다. 왜 사용하는가? Elixir로 백엔드를, TypeScript로 프런트엔드를 작성하다 보면 변경되는 API 스펙을 항상 동기화된 상태로 관리하기 쉽지 않다.결국 동기화를 위해서는 Graphql 을 사용하거나, 별도의 툴이 필요하다.AshTypescript ...

Sep 28, 20253 min read

Ash Reactor

TLDR; Reactor는 복잡한 워크플로우를 단계별로 정의하고 실행하는 라이브러리다. 각 단계가 독립적으로 돌아가며, 병렬 처리와 에러 핸들링도 쉽게 할 수 있다. 왜 쓸까? 프로젝트에서 "데이터 조회 → 가공 → 외부 API 호출 → 저장" 같은 복잡한 로직을 처리할 때가 있다.이런걸 그냥 함수로 쭉 연결하면 코드가 복잡해지고 에러 처리도 어려워진다. Reactor를 쓰면 각 단계를 명확히 분리하고 선언적으로 워크플로우를 관리할 수 있다. ...

Sep 21, 20254 min read

Ash StateMachine

TLDR; Ash StateMachine 은 상태 변경이 가능한 케이스를 명시하고, 미리 선언된 상태 변경만 가능하도록 한다. 왜 쓸까? 예를 들면, “새로 생성 → 진행 중 → 완료” 처럼 정해진 workflow 가 있다면, 로직으로 if/else 를 사용하는 것보다 상태머신으로 선언해 두는 게 안전하고 읽기 쉽다. 예제: Task 요구사항: 기본 상태는 :new :start 액션이 :new → :in_progress :finish ...

Sep 14, 20252 min read

Ash Oban

최근 작업중에 이메일 발송을 처리해야 했다.after_action 에 이마일 발송을 고려했는데 최종적으로는 AshOban 을 사용했다. AshOban이 뭔가? Ash Framework와 Oban을 이어주는 라이브러리다. 백그라운드 job을 Ash 스타일로 쓸 수 있게 해준다. 멤버 초대 이메일 보내기 클럽에 새 멤버를 초대하면 이메일을 보내야 하는 상황이다. 이걸 어떻게 처리할까? Action Hook으로 하면: 사용자가 서버에서 이메일 발송...

Aug 31, 20252 min read

kkbz.hop

14 posts