🏢 Discord 기술 스택 분석

24 min read
discordelixirrustreactwebrtccloudflaregcpscylladbcassandrareal-timevoicechatgamingerlang

디스코드가 전 세계 1.5억 명에게 실시간 음성/비디오 통화와 채팅을 제공하는 기술 스택 심층 분석 - Elixir, Rust, WebRTC로 구축한 차세대 커뮤니케이션 플랫폼

🏢 Discord 기술 스택 분석

게이머를 위해 시작했지만 전 세계 커뮤니티의 홈이 된 Discord의 기술 스택을 심층 분석합니다.
"Imagine a place..." - 실시간 커뮤니케이션의 미래를 만드는 기술


📊 회사 개요

서비스 규모

  • 월간 활성 사용자: 1.5억 명+
  • 일일 메시지: 40억+ 건
  • 음성 통화: 월 40억+ 분
  • 서버(커뮤니티): 1,900만+ 활성 서버
  • 동시 접속자: 피크 1,500만+

엔지니어링 조직

  • 엔지니어: 300명+ (전체 직원 700+)
  • 문화: "Build Belonging"
  • 원격 우선: 전 세계 분산 팀
  • 기술 다양성: Elixir, Rust, Python, Go

기술적 도전과제

  1. 초저지연: 전 세계 실시간 음성/비디오
  2. 대규모 동시성: 수백만 동시 연결
  3. 안정성: 게임 중 끊김 없는 통신
  4. 확장성: 급격한 성장 대응

🏗️ 아키텍처 Overview

시스템 다이어그램

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Clients   │────►│  Gateway    │────►│   Guilds    │
│(Desktop/Web)│     │  (Elixir)   │     │  (Elixir)   │
└─────────────┘     └─────────────┘     └─────────────┘
       │                    │                    │
       │              ┌─────▼─────┐       ┌─────▼─────┐
       │              │WebRTC SFU │       │  Message   │
       │              │  (Rust)   │       │   Store    │
       │              └───────────┘       └───────────┘
       │                    │                    │
       └────────────────────┤                    │
                      ┌─────▼─────┐       ┌─────▼─────┐
                      │   Media   │       │ ScyllaDB  │
                      │  Server   │       │(Cassandra)│
                      └───────────┘       └───────────┘

핵심 설계 원칙

  1. Erlang/OTP: 고가용성과 fault tolerance
  2. Actor Model: 수백만 개의 경량 프로세스
  3. Event Sourcing: 모든 상태 변경 추적
  4. Microservices: 서비스별 최적 언어 선택
  5. Edge Computing: 글로벌 엣지 네트워크

기술 진화

  • 2015: Python 모놀리스로 시작
  • 2016: Elixir/Erlang 마이그레이션
  • 2017: Rust로 성능 최적화
  • 2019: Go 서비스 추가
  • 2021: 대규모 인프라 재설계
  • 2023: AI 기능 통합

🧩 기술 스택 상세

Backend 기술

Elixir/Erlang 코어

# Discord의 Guild (서버) GenServer
defmodule Discord.Guild do
  use GenServer

  defstruct [:id, :name, :owner_id, :channels, :members, :roles]

  # Guild 시작
  def start_link(guild_id) do
    GenServer.start_link(__MODULE__, guild_id,
      name: {:via, Registry, {Discord.GuildRegistry, guild_id}})
  end

  # 메시지 처리
  def handle_cast({:message, %{channel_id: channel_id, content: content} = message}, state) do
    # 권한 확인
    if authorized?(message.author_id, channel_id, state) do
      # 메시지 저장
      MessageStore.save(message)

      # 온라인 멤버에게 브로드캐스트
      state.members
      |> Enum.filter(&Member.online?/1)
      |> Enum.each(fn member ->
        Gateway.push(member.session_id, {:message, message})
      end)

      # Presence 업데이트
      Presence.track(self(), "guild:#{state.id}", message.author_id, %{
        status: "online",
        typing: false
      })
    end

    {:noreply, state}
  end

  # 음성 채널 참가
  def handle_call({:join_voice, channel_id, user_id}, _from, state) do
    case VoiceServer.allocate(state.region) do
      {:ok, server} ->
        # WebRTC 연결 정보 생성
        credentials = generate_webrtc_credentials(user_id)

        # 음성 상태 업데이트
        new_state = update_voice_state(state, user_id, channel_id, server)

        {:reply, {:ok, %{server: server, credentials: credentials}}, new_state}

      {:error, reason} ->
        {:reply, {:error, reason}, state}
    end
  end

  # 대규모 Guild 최적화
  defp maybe_lazy_load_members(state) do
    if length(state.members) > 1000 do
      # 대규모 서버는 멤버 지연 로딩
      %{state | members: :lazy}
    else
      state
    end
  end
end

Rust 고성능 서비스

// Discord의 음성 서버 (Rust)
use tokio::net::UdpSocket;
use webrtc::peer_connection::RTCPeerConnection;
use std::sync::Arc;
use dashmap::DashMap;

pub struct VoiceServer {
    connections: Arc<DashMap<u64, VoiceConnection>>,
    mixer: Arc<AudioMixer>,
    config: ServerConfig,
}

struct VoiceConnection {
    user_id: u64,
    peer_connection: RTCPeerConnection,
    audio_track: Arc<dyn MediaStreamTrack>,
    opus_encoder: OpusEncoder,
    jitter_buffer: JitterBuffer,
}

impl VoiceServer {
    pub async fn new(config: ServerConfig) -> Result<Self> {
        let socket = UdpSocket::bind(&config.bind_address).await?;

        Ok(Self {
            connections: Arc::new(DashMap::new()),
            mixer: Arc::new(AudioMixer::new(config.sample_rate)),
            config,
        })
    }

    pub async fn handle_audio_packet(&self, packet: AudioPacket) -> Result<()> {
        // Opus 디코딩
        let pcm_data = self.decode_opus(packet.data)?;

        // Jitter buffer에 추가
        if let Some(mut conn) = self.connections.get_mut(&packet.user_id) {
            conn.jitter_buffer.push(pcm_data, packet.timestamp);

            // 믹싱을 위한 오디오 준비
            if let Some(frame) = conn.jitter_buffer.pop() {
                self.mixer.add_source(packet.user_id, frame);
            }
        }

        // 다른 참가자에게 전송
        self.broadcast_mixed_audio(packet.channel_id).await?;

        Ok(())
    }

    async fn broadcast_mixed_audio(&self, channel_id: u64) -> Result<()> {
        let participants = self.get_channel_participants(channel_id);

        for user_id in participants {
            // 각 사용자별 맞춤 믹스 (자신 제외)
            let mixed = self.mixer.mix_excluding(user_id);

            // Opus 인코딩
            let encoded = self.encode_opus(mixed)?;

            // WebRTC DataChannel로 전송
            if let Some(conn) = self.connections.get(&user_id) {
                conn.peer_connection
                    .send_audio(encoded)
                    .await?;
            }
        }

        Ok(())
    }
}

// 저지연 오디오 믹서
struct AudioMixer {
    sources: Arc<DashMap<u64, AudioSource>>,
    sample_rate: u32,
}

impl AudioMixer {
    fn mix_excluding(&self, exclude_user_id: u64) -> Vec<i16> {
        let mut output = vec![0i32; 960]; // 20ms @ 48kHz

        self.sources.iter()
            .filter(|entry| *entry.key() != exclude_user_id)
            .for_each(|entry| {
                let source = entry.value();
                for (i, &sample) in source.samples.iter().enumerate() {
                    output[i] = output[i].saturating_add(sample as i32);
                }
            });

        // 클리핑 방지
        output.iter()
            .map(|&s| (s.max(-32768).min(32767)) as i16)
            .collect()
    }
}

Backend 기술 스택:

  • 언어: Elixir (50%), Rust (20%), Python (15%), Go (15%)
  • 프레임워크: Phoenix (Elixir), Actix (Rust), FastAPI
  • 실시간: WebSocket, WebRTC
  • 메시징: NATS, RabbitMQ
  • RPC: gRPC, Protobuf

Frontend 기술

React Desktop App

// Discord Desktop App (Electron + React)
import { useEffect, useState } from "react";
import { Gateway } from "@discord/gateway";
import { VoiceConnection } from "@discord/voice";

interface ChannelViewProps {
  channelId: string;
  guildId: string;
}

export function ChannelView({ channelId, guildId }: ChannelViewProps) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [voiceState, setVoiceState] = useState<VoiceState | null>(null);
  const gateway = useGateway();

  useEffect(() => {
    // 실시간 메시지 구독
    const unsubscribe = gateway.subscribe(`channel:${channelId}`, (event) => {
      switch (event.type) {
        case "MESSAGE_CREATE":
          setMessages((prev) => [...prev, event.message]);
          break;

        case "MESSAGE_UPDATE":
          setMessages((prev) =>
            prev.map((msg) =>
              msg.id === event.message.id ? event.message : msg
            )
          );
          break;

        case "TYPING_START":
          showTypingIndicator(event.userId);
          break;
      }
    });

    return unsubscribe;
  }, [channelId]);

  // 음성 채널 연결
  const joinVoiceChannel = async () => {
    const connection = new VoiceConnection({
      guildId,
      channelId,
      selfDeaf: false,
      selfMute: false,
    });

    // WebRTC 연결 설정
    await connection.connect();

    // 로컬 오디오 스트림
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: {
        echoCancellation: true,
        noiseSuppression: true,
        autoGainControl: true,
      },
    });

    connection.setInputStream(stream);
    setVoiceState(connection);
  };

  return (
    <div className="channel-view">
      <MessageList messages={messages} />
      <MessageInput channelId={channelId} />
      <VoiceControls
        connected={!!voiceState}
        onJoin={joinVoiceChannel}
        onLeave={() => voiceState?.disconnect()}
      />
    </div>
  );
}

// 최적화된 메시지 렌더링
const MessageList = React.memo(({ messages }: { messages: Message[] }) => {
  const virtualizer = useVirtualizer({
    count: messages.length,
    getScrollElement: () => scrollRef.current,
    estimateSize: () => 50,
    overscan: 5,
  });

  return (
    <div ref={scrollRef} className="message-list">
      {virtualizer.getVirtualItems().map((virtualItem) => (
        <MessageItem
          key={messages[virtualItem.index].id}
          message={messages[virtualItem.index]}
          style={{
            height: `${virtualItem.size}px`,
            transform: `translateY(${virtualItem.start}px)`,
          }}
        />
      ))}
    </div>
  );
});

Frontend 기술:

  • Desktop: Electron + React
  • Web: React + TypeScript
  • Mobile: React Native
  • 상태관리: MobX, Zustand
  • UI: 자체 디자인 시스템

Data Infrastructure

메시지 저장소

# Discord의 메시지 저장 시스템
from cassandra.cluster import Cluster
from cassandra.policies import DCAwareRoundRobinPolicy
import msgpack
import zstd

class MessageStore:
    def __init__(self):
        self.cluster = Cluster(
            contact_points=['scylla-1', 'scylla-2', 'scylla-3'],
            load_balancing_policy=DCAwareRoundRobinPolicy(local_dc='us-west')
        )
        self.session = self.cluster.connect('discord')

    async def save_message(self, message):
        # 메시지 압축
        compressed = zstd.compress(
            msgpack.packb({
                'content': message.content,
                'embeds': message.embeds,
                'attachments': message.attachments,
            }),
            level=3
        )

        # ScyllaDB에 저장
        query = """
            INSERT INTO messages (
                channel_id, message_id, author_id,
                content, timestamp, edited_timestamp
            ) VALUES (?, ?, ?, ?, ?, ?)
            USING TTL ?
        """

        # 메시지 보관 정책에 따른 TTL
        ttl = self.get_message_ttl(message.channel_id)

        await self.session.execute_async(
            query,
            (
                message.channel_id,
                message.id,
                message.author_id,
                compressed,
                message.timestamp,
                message.edited_timestamp,
                ttl
            )
        )

        # 검색 인덱스 업데이트
        await self.update_search_index(message)

    async def get_messages(self, channel_id, before=None, limit=50):
        query = """
            SELECT * FROM messages
            WHERE channel_id = ?
            AND message_id < ?
            ORDER BY message_id DESC
            LIMIT ?
        """

        rows = await self.session.execute_async(
            query,
            (channel_id, before or MAX_ID, limit)
        )

        messages = []
        for row in rows:
            # 압축 해제
            data = msgpack.unpackb(
                zstd.decompress(row.content)
            )
            messages.append(Message.from_db(row, data))

        return messages

데이터 인프라:

  • 주 데이터베이스: ScyllaDB (Cassandra 호환)
  • 캐시: Redis Cluster
  • 검색: Elasticsearch
  • 분석: Google BigQuery
  • 메시지 큐: NATS, Kafka

Infrastructure & DevOps

글로벌 인프라

# Kubernetes 배포 설정
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: discord-guild-service
  namespace: discord-prod
spec:
  serviceName: guild-service
  replicas: 100
  podManagementPolicy: Parallel
  selector:
    matchLabels:
      app: guild-service
  template:
    metadata:
      labels:
        app: guild-service
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - labelSelector:
                matchExpressions:
                  - key: app
                    operator: In
                    values:
                      - guild-service
              topologyKey: kubernetes.io/hostname
      containers:
        - name: guild
          image: discord/guild-service:v2.3.0
          resources:
            requests:
              memory: "4Gi"
              cpu: "2"
            limits:
              memory: "8Gi"
              cpu: "4"
          env:
            - name: BEAM_SCHEDULER_COUNT
              value: "8"
            - name: ERL_MAX_PORTS
              value: "1048576"
          livenessProbe:
            httpGet:
              path: /health
              port: 4000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: 4000
            initialDelaySeconds: 5
            periodSeconds: 5

인프라 기술:

  • 클라우드: Google Cloud Platform + 자체 데이터센터
  • CDN: Cloudflare (글로벌 엣지)
  • 컨테이너: Kubernetes
  • 모니터링: Prometheus, Grafana, Datadog
  • CI/CD: GitLab CI, ArgoCD

💡 핵심 기술 인사이트

1. Elixir/Erlang의 힘

# Discord의 분산 시스템 설계
defmodule Discord.Presence do
  use Phoenix.Presence,
    otp_app: :discord,
    pubsub_server: Discord.PubSub

  # 수백만 사용자의 presence 추적
  def track_user(guild_id, user_id, meta) do
    track(self(), "guild:#{guild_id}", user_id, meta)
  end

  # CRDT 기반 동기화
  def handle_diff(diff, state) do
    # Conflict-free 상태 병합
    for {user_id, %{metas: [meta | _]}} <- diff.joins do
      # 사용자 온라인 이벤트
      broadcast_user_presence(user_id, :online, meta)
    end

    for {user_id, _} <- diff.leaves do
      # 사용자 오프라인 이벤트
      broadcast_user_presence(user_id, :offline, %{})
    end

    {:ok, state}
  end
end

2. WebRTC 최적화

// 커스텀 WebRTC SFU (Selective Forwarding Unit)
pub struct DiscordSFU {
    router: MediaRouter,
    bandwidth_estimator: BandwidthEstimator,
    quality_controller: QualityController,
}

impl DiscordSFU {
    pub async fn handle_rtp_packet(&self, packet: RtpPacket) -> Result<()> {
        // 1. 대역폭 추정
        self.bandwidth_estimator.update(&packet);

        // 2. 품질 제어 (시뮬캐스트/SVC)
        let target_quality = self.quality_controller.decide_quality(
            packet.ssrc,
            self.bandwidth_estimator.get_estimate()
        );

        // 3. 선택적 포워딩
        let recipients = self.router.get_recipients(packet.ssrc);

        for recipient in recipients {
            // 수신자별 최적화
            let adapted_packet = match recipient.connection_quality {
                Quality::High => packet.clone(),
                Quality::Medium => self.downscale_packet(&packet, 720),
                Quality::Low => self.downscale_packet(&packet, 480),
            };

            // NACK, FEC 처리
            recipient.send_with_redundancy(adapted_packet).await?;
        }

        Ok(())
    }
}

3. 대규모 실시간 동기화

// Go로 작성된 Presence 서비스
type PresenceService struct {
    shards   map[uint64]*PresenceShard
    ring     *consistent.Ring
    redisCli *redis.ClusterClient
}

type PresenceShard struct {
    mu       sync.RWMutex
    users    map[uint64]*UserPresence
    lastSync time.Time
}

func (ps *PresenceService) UpdatePresence(userID uint64, status Status) error {
    // Consistent hashing으로 샤드 결정
    shardID := ps.ring.Get(fmt.Sprintf("user:%d", userID))
    shard := ps.shards[shardID]

    shard.mu.Lock()
    defer shard.mu.Unlock()

    // 로컬 업데이트
    presence := &UserPresence{
        UserID:    userID,
        Status:    status,
        UpdatedAt: time.Now(),
    }
    shard.users[userID] = presence

    // Redis에 비동기 동기화
    go ps.syncToRedis(userID, presence)

    // 구독자에게 브로드캐스트
    ps.broadcastUpdate(userID, presence)

    return nil
}

// 효율적인 bulk 동기화
func (ps *PresenceService) BulkSync() {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        for _, shard := range ps.shards {
            shard.mu.RLock()
            updates := make([]*UserPresence, 0, len(shard.users))

            for _, presence := range shard.users {
                if presence.UpdatedAt.After(shard.lastSync) {
                    updates = append(updates, presence)
                }
            }
            shard.mu.RUnlock()

            if len(updates) > 0 {
                ps.bulkSyncToRedis(updates)
                shard.lastSync = time.Now()
            }
        }
    }
}

📈 성과 & 지표

기술적 성과

  • 메시지 지연: < 100ms (글로벌 평균)
  • 음성 지연: < 60ms (동일 지역)
  • 가용성: 99.95%+
  • 동시 음성 채널: 수백만 개
  • 메시지 처리량: 초당 수백만 건

확장성 성과

  • 서버당 사용자: 최대 80만 명 (Midjourney)
  • 최대 음성 참가자: 10,000명 (Stage Channels)
  • 파일 업로드: 500MB (Nitro)
  • 화면 공유: 4K 60fps 지원

🎓 Discord에서 배울 점

✅ 적용 가능한 패턴

  1. Actor Model: Erlang/OTP의 강력함
  2. 언어별 최적화: 적재적소에 최적 언어 사용
  3. WebRTC SFU: 대규모 실시간 통신
  4. Consistent Hashing: 분산 시스템 샤딩
  5. Event Sourcing: 상태 관리와 히스토리

❌ 주의사항

  1. Elixir 학습곡선: 함수형 + Actor 모델
  2. 다중 언어 복잡성: 운영 오버헤드
  3. 실시간 요구사항: 네트워크 품질 의존
  4. 스케일 도전: 대규모 서버 관리

📚 추천 리소스


🔮 미래 전망

현재 집중 분야

  1. AI 통합: 대화 요약, 번역, 모더레이션
  2. Forum Channels: 구조화된 대화
  3. Activities: 앱 내 미니게임/활동
  4. 크리에이터 수익화: 서버 구독, 티켓팅
  5. 엔터프라이즈: Slack 대체 시장

기술 투자 영역

  • Rust 확대: 더 많은 핵심 서비스
  • AI/ML: 콘텐츠 모더레이션, 추천
  • 엣지 컴퓨팅: 더 낮은 지연시간
  • 블록체인: Web3 커뮤니티 지원
  • AR/VR: 메타버스 통합

"우리는 단순한 채팅 앱이 아닌, 전 세계 커뮤니티가 모이는 디지털 공간을 만들고 있습니다."

  • Discord Engineering

마지막 업데이트: 2025-01-28

Found this helpful? Share it with others!
Tweet

🔗 Related Content

You might also be interested in these articles

🏢 company

🏢 Uber 기술 스택 분석

우버가 매일 2,500만 건의 라이드를 실시간으로 매칭하는 기술 스택 심층 분석 - Go, Java, Node.js로 구축한 글로벌 실시간 마켓플레이스

15 min read
uber, microservices +12
Read more
🏗️ stack

🔄 Realtime Collaboration Stack

Figma, Notion 같은 실시간 협업 앱 구축을 위한 기술 스택 - WebSocket, CRDT, WebRTC로 만드는 동시 편집 시스템

21 min read
nodejs, socketio +13
Read more
🏢 company

🏢 Figma 기술 스택 분석

피그마가 브라우저에서 실시간 협업 디자인 툴을 구현한 기술 스택 심층 분석 - C++, WebAssembly, WebGL로 만든 차세대 디자인 플랫폼

22 min read
figma, c++ +11
Read more
🏢 company

🏢 Netflix 기술 스택 분석

넷플릭스가 전 세계 2.4억 사용자에게 스트리밍 서비스를 제공하는 기술 스택 심층 분석 - 마이크로서비스, AWS, Chaos Engineering의 교과서

9 min read
netflix, microservices +11
Read more
🏢 company

🏢 Notion 기술 스택 분석

노션이 수천만 사용자에게 All-in-One 워크스페이스를 제공하는 기술 스택 심층 분석 - 블록 기반 에디터, 실시간 협업, 그리고 확장 가능한 데이터베이스

24 min read
notion, typescript +11
Read more
🏢 company

🏢 Airbnb 기술 스택 분석

에어비앤비가 전 세계 400만 개의 숙소를 연결하는 글로벌 마켓플레이스 기술 스택 심층 분석 - Ruby on Rails에서 Service-Oriented Architecture로의 진화

24 min read
airbnb, ruby +12
Read more

Found this helpful?

Help us improve this content by contributing on GitHub or sharing your feedback with the community.