elixir mix project (chat)

eesope·2025년 3월 3일

elixir

목록 보기
3/5

개요

Elixir의 Open Telecom Platform(OTP) 원칙에 따라 분산 환경(distributed process)에서 상태를 관리하는 chat 프로세스를 구현

init

mix new chat --sup

  • project 이름으로 chat을 사용
  • --sup 태그를 통해 supervisor 역할을 하는 서버 Chat.Server를 생성하고, 이 서버가 글로벌 서버로 동작하며 프로세스 충돌에 대비

모듈 구성

  • Chat.Application:

    • OTP 애플리케이션의 엔트리 포인트로, 애플리케이션이 시작될 때 ETS 테이블(저장소)을 생성
    • 워커 감독자를 자식 프로세스로 시작
  • Chat.WorkerSupervisor:

    • DynamicSupervisor를 사용해 필요할 때마다 새로운 워커(GenServer)를 동적으로 생성·관리
    • 전역 등록(global register)을 통해 분산 환경에서 감독자 역시 접근 가능토록 함
  • Chat.Worker:

    • 개별 워커를 담당하는 GenServer로, 이름으로 전역 등록되어 분산 시스템 어디서든 접근 가능
    • 실질적 기능 수행 담당, 종료 시 현재 값을 ETS 테이블에 저장해 상태 보존

각 모듈

역할

  • 애플리케이션 부트
    Chat.Application.start/2가 실행되면서 ETS 테이블(Chat.Store)이 생성되고, Chat.WorkerSupervisor가 최상위 Supervisor의 자식으로 시작됩니다.

  • Worker 생성 및 글로벌 등록
    필요할 때 Chat.WorkerSupervisor.start_worker(name)를 호출하여 새로운 워커를 생성합니다.
    각 워커는 Chat.Worker.start_link(name)을 통해 GenServer로 시작되며, via(name)을 이용해 글로벌 레지스트리에 등록되어 분산 환경에서도 접근할 수 있습니다.

  • 상태 관리 및 통신
    워커는 ETS 테이블에서 이전 상태를 조회하여 초기화하며, inc/2와 value/1 함수를 통해 외부 요청을 처리합니다.
    GenServer.cast/2를 사용해 비동기적으로 값을 증가시키고, GenServer.call/2로 동기적으로 현재 값을 조회합니다.

  • 프로세스 종료 및 상태 보존
    워커가 종료되면 terminate/2 콜백이 호출되어 현재 상태를 ETS 테이블에 저장합니다. 이를 통해 장애 발생 시에도 상태를 복구할 수 있습니다.

workflow

  1. 어플리케이션 초기화:
    한 터미널에서 애플리케이션(즉, Chat.Application)이 시작
    이 때 OTP 애플리케이션이 부팅 -> 전체 시스템의 최상위 트리가 구성

  2. 내부 구성 요소 초기화:
    Chat.Application은 자식 프로세스들을 시작

    @impl true
    def start(_type, _args) do
     children = [
       # Starts a worker by calling: Chat.Worker.start_link(arg)
       {Chat.Server, []}, # global chat server
       Chat.Supervisor, # dynamic supervisor managing proxy servers
       {Chat.ProxyServer, 6666} # for TCP connection
     ]
     ...
    • Chat.Server: 채팅 서버(GenServer)로서, 닉네임 등록, 메시지 전달 등 채팅 비즈니스 로직을 처리. 이 서버는 전역적으로 등록되어 하나의 인스턴스로 존재.
    • Chat.Supervisor: 동적으로 생성되는 프록시 서버(각 TCP 연결 처리 프로세스)를 관리하는 Supervisor
    • Chat.ProxyServer: TCP 리스닝 소켓을 열어 기본 포트(예: 6666)를 통해 외부 클라이언트의 연결 대기
  3. 클라이언트의 접속 및 요청 처리:

    • 외부 클라이언트(예: 사용자가 터미널에서 Chat.TCPClient나 다른 클라이언트를 통해 접속)는 Chat.ProxyServer가 열린 포트(6666)에 연결
    • 연결 수락 -> Chat.ProxyServer는 각 연결에 대해 별도의 프로세스를 생성하여 해당 클라이언트의 요청(예: "/NICK homer")을 처리
    • 이 요청은 Chat.ProxyServer를 통해 내부의 Chat.Server로 전달
    • Chat.Server는 요청을 처리하고 결과(예: 닉네임 등록 성공 또는 에러 메시지)를 반환
    • Chat.ProxyServer는 받은 응답을 다시 클라이언트에 전송
  4. 여러 터미널의 연결:

    • 하나의 PC에서 Chat 프로그램을 시작하면, 그 PC에는 하나의 애플리케이션, 하나의 Chat.Server, 하나의 Chat.Supervisor, 그리고 하나의 Chat.ProxyServer(리스닝 소켓)가 실행 중
    • 다른 터미널에서도 클라이언트 프로그램을 실행하면, 모두 동일한 애플리케이션 내의 Chat.ProxyServer에 접속하여, Chat.Server와 통신하게 됨

  • 애플리케이션, 서버, 수퍼바이저는 한 대의 PC에서 한 번만 실행
  • 각 터미널은 외부 클라이언트 역할로 접속하여 이미 실행 중인 내부 컴포넌트와 통신

이런 방식으로, 여러 터미널(즉, 여러 클라이언트)이 하나의 중앙 채팅 서버(및 관련 OTP 구조)와 연결되어 메시지를 주고받게 되는 시스템을 구현

모듈 간 관계 및 실행 순서

Chat.Application:

OTP 애플리케이션의 진입점입니다.
애플리케이션 시작 시, Chat.Application이 children 리스트에 따라 Chat.Server, Chat.Supervisor, 그리고 Chat.ProxyServer를 시작합니다.
ETS 테이블(Chat.Store)도 이 시점에 생성됩니다.
Chat.Server:

전역 등록되어 하나의 중앙 채팅 서버로 동작합니다.
클라이언트(또는 프록시 서버)에서 오는 요청을 GenServer.call을 통해 처리합니다.
상태는 ETS를 통해 저장 및 복원됩니다.
Chat.ProxyServer:

OTP 자식으로 등록되어, TCP 리스닝 소켓을 열고 클라이언트의 연결을 기다립니다.
각 클라이언트 연결마다 별도의 프로세스(생성된 Task나 spawn된 프로세스)를 생성하여 클라이언트의 명령어를 처리합니다.
처리한 명령어를 Chat.Server에 전달하고, 결과를 클라이언트에게 응답합니다.
소통 방법:

내부 통신:
Chat.ProxyServer는 클라이언트의 텍스트 명령어를 받아 GenServer.call을 사용해 Chat.Server와 통신합니다.
Chat.Server는 요청을 처리하고 결과를 반환합니다.
외부 통신:
Chat.ProxyServer는 TCP를 통해 클라이언트와 통신합니다. 클라이언트는 TCP 연결을 통해 명령어를 전송하고, 응답을 받습니다.
실행 순서:

Chat.Application이 시작되면서 ETS 테이블이 생성되고, children 리스트에 등록된 Chat.Server, Chat.Supervisor, Chat.ProxyServer가 차례로 실행됩니다.
Chat.ProxyServer는 TCP 포트(기본 6666)에서 연결을 수락하며, 여러 클라이언트 연결을 지속적으로 처리할 수 있는 accept 루프를 실행합니다.
클라이언트가 접속하면 ProxyServer의 각 연결 처리 프로세스가 클라이언트의 명령어를 파싱하여 Chat.Server에 요청하고, 결과를 다시 클라이언트에 전송합니다.

Passive 모드 (active: false) vs Active 모드 (active: true)
모드 특징
Passive - :gen_tcp.recv(socket, 0)을 명시적으로 호출해야 데이터 수신 가능

  • 응용 프로그램이 직접 데이터를 관리해야 함
  • 일반적으로 서버 구현에서 많이 사용됨
    Active - 데이터를 받으면 {:tcp, socket, data} 메시지가 프로세스에 전달됨
  • 핸들링이 빠르고 비동기적이지만, 과부하 시 메시지 큐가 쌓일 수 있음
    현재 코드에서 passive 모드를 사용하는 이유는?

프록시 서버가 요청을 명확히 제어하기 위해서
데이터를 순차적으로 처리하기 위해서 (active 모드는 무작위로 메시지를 받을 수 있음)


참고자료

https://en.wikipedia.org/wiki/Open_Telecom_Platform
https://serokell.io/blog/elixir-otp-guide#supervisor
https://hexdocs.pm/elixir/erlang-term-storage.html
https://elixirschool.com/en/lessons/advanced/otp_concurrency
ChatGPT

profile
go simple 🧑🏻‍💻

0개의 댓글