DIY 드라마는 그만: 왜 ActivityPub을 처음부터 구축하는 대신 Fedify를 사용해야 할까요?

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

여러분은 ActivityPub 같은 프로토콜로 구동되는 분산형 소셜 웹인 페디버스(fediverse)에 매료되었을 겁니다. 아마도 Mastodon, Lemmy, Pixelfed 등과 연결된 다음 세대 연합형 앱, 독특한 공간을 구축하는 꿈을 꾸고 있을지도 모릅니다. 직접 처음부터 ActivityPub를 구현하고 싶은 유혹이 강할 겁니다. 완전한 제어, 맞죠? 모든 바이트를 이해하는 것? 멋져 보입니다!

하지만 잠깐만요. 그 대장정을 시작하기 전에, 현실에 대해 이야기해 봅시다. ActivityPub를 올바르게 구현하는 것은 단순한 작업이 아닙니다. 그것은 마치 눈을 가린 채 외발자전거를 타면서 여러 복잡한 표준을 저글링하는 것과 같습니다. 정말 어렵습니다.

바로 여기서 **Fedify**가 등장합니다. Fedify는 ActivityPub 개발의 가장 까다로운 부분을 처리하도록 설계된 TypeScript 프레임워크로, 연합(federation) 휠을 재발명하는 대신 여러분의 앱을 특별하게 만드는 데 집중할 수 있게 해줍니다.

이 포스트에서는 DIY ActivityPub 구현의 일반적인 골칫거리를 분석하고, Fedify가 어떻게 강력한 통증 완화제 역할을 하는지 데이터가 표현되는 기본 방식부터 시작하여 보여드리겠습니다.

과제 : 데이터 모델링—ActivityStreams와 JSON-LD 유창하게 구사하기

ActivityPub는 핵심적으로 행동과 객체를 설명하기 위해 ActivityStreams 2.0 어휘에 의존하며, 이 어휘를 인코딩하는 구문으로 JSON-LD를 사용합니다. 강력하지만, 이 조합은 처음부터 상당한 복잡성을 가져옵니다.

첫째, 방대한 ActivityStreams 어휘를 이해하고 올바르게 사용하는 것 자체가 장애물입니다. 게시물(Note, Article), 프로필(Person, Organization), 행동(Create, Follow, Like, Announce) 등 모든 것을 사양에 정의된 정확한 용어와 속성을 사용하여 모델링해야 합니다. 수동으로 JSON을 구성하는 것은 지루하고 오류가 발생하기 쉽습니다.

둘째, 인코딩 계층인 JSON-LD는 직접적인 JSON 조작을 놀랍도록 까다롭게 만드는 특정 규칙을 가지고 있습니다:

  • 누락 vs. 빈 배열: JSON-LD에서 속성이 없는 것은 종종 빈 배열이 있는 것과 의미적으로 동일합니다. 애플리케이션 로직은 값을 확인할 때 이러한 경우를 동등하게 처리해야 합니다. 예를 들어, 다음 두 Note 객체는 name 속성에 관해 같은 의미를 갖습니다:
    // name 속성 없음
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": ""
    }
    // 다음과 동등:
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "name": [],
      "content": ""
    }
  • 단일 값 vs. 배열: 마찬가지로, 단일 값을 직접 보유하는 속성은 종종 해당 값을 포함하는 단일 요소 배열을 보유하는 것과 동등합니다. 코드는 동일한 의미에 대해 두 표현 모두를 예상해야 합니다. 여기 content 속성의 예시입니다:
    // 단일 값
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": "Hello"
    }
    // 다음과 동등:
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Note",
      "content": ["Hello"]
    }
  • 객체 참조 vs. 임베디드 객체: 속성은 전체 JSON-LD 객체를 직접 임베드하거나 해당 객체를 참조하는 URI 문자열만 포함할 수 있습니다. 애플리케이션은 URI만 제공된 경우 객체의 데이터를 가져올 준비가 되어 있어야 합니다(이를 역참조라고 함). 다음 두 Announce 활동은 의미적으로 동등합니다(URI가 올바르게 해석된다고 가정):
    {
      "@context": "https://www.w3.org/ns/activitystreams",
      "type": "Announce",
      // 임베디드 객체:
      "actor": {
        "type": "Person",
        "id": "http://sally.example.org/",
        "name": "Sally"
      },
      "object": {
        "type": "Arrive",
        "id": "https://sally.example.com/arrive",
        /* ... */
      }
    }
    // 다음과 동등:
    {
      "@context":
      "https://www.w3.org/ns/activitystreams",
      "type": "Announce",
      // URI 참조:
      "actor": "http://sally.example.org/",
      "object": "https://sally.example.com/arrive"
    }

애플리케이션 전체에서 이러한 모든 어휘 규칙과 JSON-LD 변형을 일관되게 수동으로 처리하려고 하면 필연적으로 장황하고 복잡하며 취약한 코드가 생성되어 연합을 깨뜨리는 미묘한 버그가 발생하기 쉽습니다.

Fedify는 포괄적인 타입 안전 Activity 어휘 API로 이 모든 데이터 모델링 과제를 해결합니다. ActivityStreams 타입과 일반적인 확장에 대한 TypeScript 클래스를 제공하여 자동 완성과 컴파일 타임 안전성을 제공합니다. 중요한 것은, 이러한 클래스가 내부적으로 모든 까다로운 JSON-LD 뉘앙스를 관리한다는 점입니다. Fedify의 속성 접근자는 일관된 인터페이스를 제공합니다—비기능적 속성(예: tags)은 항상 배열을 반환하고, 기능적 속성(예: content)은 항상 단일 값이나 null을 반환합니다. 이는 역참조 접근자(예: activity.getActor())를 통해 객체 참조와 임베디드 객체를 원활하게 처리하며, 필요할 때 URI를 통해 원격 객체를 자동으로 가져옵니다—이 기능은 **속성 하이드레이션**으로 알려져 있습니다. Fedify를 사용하면 깔끔하고 예측 가능한 TypeScript API로 작업할 수 있으며, AS 어휘와 JSON-LD 인코딩의 복잡한 세부 사항은 프레임워크가 처리하도록 할 수 있습니다.

과제 : 발견 및 신원—액터 찾기

데이터를 모델링할 수 있게 되면, 액터를 발견 가능하게 만들어야 합니다. 이는 주로 WebFinger 프로토콜(RFC 7033)을 포함합니다. /.well-known/webfinger에 리소스 쿼리(예: acct: URI)를 파싱하고, 요청된 도메인을 서버에 대해 검증하며, 정확하게 포맷된 JSON 리소스 설명자(JRD)로 응답할 수 있는 서버 엔드포인트를 구축해야 합니다. 이 JRD는 올바른 미디어 타입을 사용하여 액터의 ActivityPub ID를 가리키는 self 링크와 같은 특정 링크를 포함해야 합니다. 이 중 어느 부분이라도 잘못되면 액터가 보이지 않을 수 있습니다.

Fedify는 이를 크게 단순화합니다. setActorDispatcher() 메서드를 통해 제공하는 액터 정보를 기반으로 WebFinger 요청을 자동으로 처리합니다. Fedify는 올바른 JRD 응답을 생성합니다. 사용자 핸들을 내부 식별자에 매핑하는 것과 같은 더 고급 제어가 필요한 경우, mapHandle() 또는 mapAlias() 콜백을 쉽게 등록할 수 있습니다. 액터를 정의하는 데 집중하면 Fedify가 그들을 발견 가능하게 만드는 것을 처리합니다.

// 예시: 액터를 찾는 방법 정의
federation.setActorDispatcher(
  "/users/{username}",
  async (ctx, username) => { /* ... */ }
);
// 이제 GET /.well-known/webfinger?resource=acct:username@your.domain이 그냥 작동합니다!

과제 : 핵심 ActivityPub 메커니즘—요청 및 컬렉션 처리

액터 프로필을 제공하려면 신중한 콘텐츠 협상이 필요합니다. 액터의 ID에 대한 요청은 기계 클라이언트를 위한 JSON-LD(Accept: application/activity+json)와 브라우저를 위한 HTML(Accept: text/html)이 필요합니다. 인박스 엔드포인트에서 들어오는 활동을 처리하는 것은 POST 요청 검증, 암호화 서명 확인, 페이로드 파싱, 중복 방지(멱등성), 활동 유형에 따른 라우팅을 포함합니다. 올바른 페이지네이션이 있는 컬렉션(outbox, followers 등)을 구현하는 것은 또 다른 계층을 추가합니다.

Fedify는 이 모든 것을 간소화합니다. 핵심 요청 핸들러(Federation.fetch() 또는 @fedify/express와 같은 프레임워크 어댑터를 통해)는 콘텐츠 협상을 관리합니다. setActorDispatcher()로 액터를 정의하고 프레임워크(Hono, Express, SvelteKit 등)로 웹 페이지를 정의하면 Fedify가 적절하게 라우팅합니다. 인박스의 경우, setInboxListeners()를 사용하면 활동 유형별로 핸들러를 정의할 수 있으며(예: .on(Follow, ...)), Fedify는 KV Store를 사용하여 검증, 서명 확인, 파싱, 멱등성 검사를 자동으로 처리합니다. 컬렉션 구현은 디스패처(예: setFollowersDispatcher())를 통해 단순화됩니다. 데이터 페이지를 가져오는 로직을 제공하면 Fedify가 페이지네이션과 함께 올바른 Collection 또는 CollectionPage를 구성합니다.

// 인박스 핸들러 정의
federation.setInboxListeners("/inbox", "/users/{handle}/inbox")
  .on(Follow, async (ctx, follow) => { /* 팔로우 처리 */ })
  .on(Undo, async (ctx, undo) => { /* 실행 취소 처리 */ });

// 팔로워 컬렉션 로직 정의
federation.setFollowersDispatcher(
  "/users/{handle}/followers",
  async (ctx, handle, cursor) => { /* ... */ }
);

과제 : 안정적인 전달 및 비동기 처리—활동을 강력하게 보내기

활동을 보내는 것은 단순한 POST보다 더 많은 것이 필요합니다. 네트워크가 실패하고, 서버가 다운됩니다. 강력한 실패 처리와 재시도 로직(이상적으로는 백오프 포함)이 필요합니다. 들어오는 활동을 동기적으로 처리하면 서버가 차단될 수 있습니다. 많은 팔로워에게 효율적으로 브로드캐스팅(팬아웃)하려면 백그라운드 처리와 가능한 경우 공유 인박스 사용이 필요합니다.

Fedify는 **MessageQueue 추상화**를 사용하여 안정성과 확장성을 해결합니다. 구성된 경우(강력히 권장), Context.sendActivity()는 전달 작업을 대기열에 넣습니다. 백그라운드 워커는 구성 가능한 정책(예: outboxRetryPolicy)에 기반한 자동 재시도로 전송을 처리합니다. Fedify는 다양한 큐 백엔드(Deno KV, Redis, PostgreSQL, AMQP)를 지원합니다. 고트래픽 팬아웃의 경우, Fedify는 부하를 효율적으로 분산시키기 위해 **최적화된 2단계 메커니즘**을 사용합니다.

// 영구 큐(예: Deno KV)로 Fedify 구성
const federation = createFederation({
  queue: new DenoKvMessageQueue(/* ... */),
  // ...
});
// 이제 전송이 안정적이고 논블로킹
await ctx.sendActivity({ handle: "myUser" }, recipient, someActivity);

과제 : 보안—일반적인 함정 피하기

ActivityPub 서버를 보호하는 것은 중요합니다. 서버 간 인증을 위해 HTTP 서명(draft-cavage-http-signatures-12)을 구현해야 합니다—복잡한 과정입니다. 데이터 무결성과 호환성을 위해 연결 데이터 서명(LDS) 또는 FEP-8b32 기반 객체 무결성 증명(OIP)도 필요할 수 있습니다. 암호화 키를 안전하게 관리하는 것이 필수적입니다. 마지막으로, 원격 리소스를 가져오는 것은 제대로 검증되지 않으면 서버 측 요청 위조(SSRF) 위험이 있습니다.

Fedify는 보안을 염두에 두고 설계되었습니다. setKeyPairsDispatcher()를 통해 키를 제공하면 HTTP 서명, LDS, OIP의 생성 및 검증을 자동으로 처리합니다. 키 관리 유틸리티가 포함되어 있습니다. 중요하게, Fedify의 기본 문서 로더는 내장된 SSRF 보호 기능을 포함하여 명시적으로 허용되지 않는 한 개인 IP에 대한 요청을 차단합니다.

과제 : 상호 운용성 및 유지 관리—다른 시스템과 원활하게 작동하기

페디버스는 다양합니다. 서로 다른 서버들은 각자의 특이점을 가지고 있습니다. 호환성을 보장하려면 테스트와 적응이 필요합니다. 표준은 새로운 Federation Enhancement Proposals(FEP)와 함께 발전합니다. 또한 서버 기능을 알리기 위해 NodeInfo와 같은 프로토콜이 필요합니다.

Fedify는 광범위한 상호 운용성을 목표로 하며 적극적으로 유지 관리됩니다. 구현 차이를 완화하기 위한 ActivityTransformer와 같은 기능이 포함되어 있습니다. NodeInfo 지원은 setNodeInfoDispatcher()를 통해 내장되어 있습니다.

과제 : 개발자 경험—실제로 앱 구축하기

프로토콜을 넘어, 어떤 서버를 구축하는 것도 설정, 테스트, 디버깅을 포함합니다. 연합에서는 디버깅이 더 어려워집니다—메시지가 잘못 형성되었나요? 서명이 잘못되었나요? 원격 서버가 다운되었나요? 호환성 특이점인가요? 좋은 도구가 필수적입니다.

Fedify는 개발자 경험을 크게 향상시킵니다. TypeScript로 구축되어 있어 우수한 타입 안전성과 에디터 자동 완성을 제공합니다. **fedify CLI**는 일반적인 개발 작업을 간소화하도록 설계된 강력한 동반자입니다.

fedify init을 사용하여 선택한 런타임과 웹 프레임워크에 맞게 새 프로젝트를 빠르게 스캐폴딩할 수 있습니다.

상호 작용 디버깅 및 데이터 확인을 위해 fedify lookup은 매우 귀중합니다. WebFinger 검색을 수행하고 객체의 데이터를 가져와 외부에서 원격 액터나 객체가 어떻게 보이는지 검사할 수 있습니다. Fedify는 터미널에서 직접 파싱된 객체 구조와 속성을 표시합니다. 예를 들어, 다음을 실행하면:

$ fedify lookup @fedify-example@fedify-blog.deno.dev

먼저 진행 메시지를 표시한 다음 다음과 같은 액터의 구조화된 표현을 출력합니다:

// fedify lookup 명령의 출력(파싱된 객체 구조 표시)
Person {
  id: URL "https://fedify-blog.deno.dev/users/fedify-example",
  name: "Fedify Example Blog",
  published: 2024-03-03T13:18:11.857Z, // 단순화된 타임스탬프
  summary: "This blog is powered by Fedify, a fediverse server framework.",
  url: URL "https://fedify-blog.deno.dev/",
  preferredUsername: "fedify-example",
  publicKey: CryptographicKey {
    id: URL "https://fedify-blog.deno.dev/users/fedify-example#main-key",
    owner: URL "https://fedify-blog.deno.dev/users/fedify-example",
    publicKey: CryptoKey { /* ... CryptoKey 세부 정보 ... */ }
  },
  // ... inbox, outbox, followers, endpoints 등의 다른 속성 ...
}

이를 통해 데이터가 어떻게 구조화되어 있는지 쉽게 확인하거나 Fedify가 파싱한 실제 속성을 보고 상호 작용이 실패하는 이유를 해결할 수 있습니다.

개발 중 애플리케이션에서 나가는 활동을 테스트하는 것은 fedify inbox로 훨씬 쉬워집니다. 이 명령을 실행하면 메시지를 수신하기 위해 생성하는 임시 액터에 대한 주요 정보를 표시하는 공개적으로 접근 가능한 인박스 역할을 하는 임시 로컬 서버가 시작됩니다:

$ fedify inbox
✔ 임시 ActivityPub 서버가 실행 중입니다: https://<unique_id>.lhr.life/
✔ @<some_test_account>@activitypub.academy에 팔로우 요청을 보냈습니다.
╭───────────────┬─────────────────────────────────────────╮
│ Actor handle: │ i@<unique_id>.lhr.life                  │
├───────────────┼─────────────────────────────────────────┤
│   Actor URI:  │ https://<unique_id>.lhr.life/i          │
├───────────────┼─────────────────────────────────────────┤
│  Actor inbox: │ https://<unique_id>.lhr.life/i/inbox    │
├───────────────┼─────────────────────────────────────────┤
│ Shared inbox: │ https://<unique_id>.lhr.life/inbox      │
╰───────────────┴─────────────────────────────────────────╯

웹 인터페이스는 다음에서 사용 가능합니다: http://localhost:8000/

그런 다음 개발 중인 애플리케이션이 제공된 Actor inbox 또는 Shared inbox URI로 활동을 보내도록 구성합니다. 활동이 도착하면 fedify inbox는 요청이 수신되었음을 나타내는 요약 테이블만 콘솔에 출력합니다:

╭────────────────┬─────────────────────────────────────╮
│     Request #: │ 2                                   │
├────────────────┼─────────────────────────────────────┤
│ Activity type: │ Follow                              │
├────────────────┼─────────────────────────────────────┤
│  HTTP request: │ POST /i/inbox                       │
├────────────────┼─────────────────────────────────────┤
│ HTTP response: │ 202                                 │
├────────────────┼─────────────────────────────────────┤
│       Details  │ https://<unique_id>.lhr.life/r/2    │
╰────────────────┴─────────────────────────────────────╯

중요하게도, 수신된 요청에 대한 상세 정보—전체 헤더(Signature 등), 요청 본문(Activity JSON), 서명 확인 상태 포함—는 fedify inbox가 제공하는 웹 인터페이스에서만 사용 가능합니다. 이 웹 UI를 통해 개발 중에 들어오는 활동을 철저히 검사할 수 있습니다.

수신된 활동과 세부 정보를 보여주는 Fedify Inbox 웹 인터페이스 스크린샷.
Fedify Inbox 웹 UI는 상세한 활동 정보를 볼 수 있는 곳입니다.

단순히 보내는 것을 넘어 로컬 머신에서 라이브 페디버스와의 상호 작용을 테스트해야 할 때, fedify tunnel은 전체 로컬 개발 서버를 일시적으로 안전하게 노출할 수 있습니다. 이러한 도구 모음은 연합 애플리케이션을 구축하고 디버깅하는 과정을 크게 용이하게 합니다.

결론: 배관이 아닌 기능을 구축하세요

ActivityPub 프로토콜 모음을 처음부터 구현하는 것은 믿을 수 없을 정도로 복잡하고 시간이 많이 소요되는 작업입니다. 여러 기술 사양에 대한 깊은 이해, 암호화 서명, 보안 강화, 다양한 생태계의 뉘앙스를 탐색하는 것이 포함됩니다. 교육적이긴 하지만, 연합 애플리케이션의 실제 고유한 기능을 구축하는 과정을 극적으로 늦춥니다.

Fedify는 잘 설계되고, 안전하며, 타입 안전한 기반을 제공하여 데이터 모델링, 발견, 핵심 메커니즘, 전달, 보안, 상호 운용성 등 연합의 복잡성을 처리합니다. 이를 통해 애플리케이션의 고유한 가치와 사용자 경험에 집중할 수 있습니다. 저수준 프로토콜 세부 사항과 씨름하는 대신 페디버스에 대한 비전을 더 빠르고 안정적으로 구축하세요. Fedify를 시도해 보세요!

시작하는 것은 간단합니다. 먼저 선호하는 방법을 사용하여 **Fedify CLI를 설치**하세요. 설치가 완료되면 fedify init your-project-name을 실행하여 새 프로젝트 템플릿을 만드세요.

자세한 내용은 Fedify 튜토리얼Fedify 매뉴얼을 확인하세요. 즐거운 연합을 기원합니다!

13
0
3

1 comment

If you have a fediverse account, you can comment on this article from your own instance. Search https://hackers.pub/ap/articles/019631c0-992b-729c-9f4d-c4bf1ffb2456 on your instance and reply to it.

1