Janus 미디어 서버 플러그인 아키텍처 분석

WebRTC 미디어 서버인 Janus는 모듈화된 구조를 통해 다양한 비즈니스 로직을 유연하게 확장할 수 있는 설계를 채택하고 있다. 본문에서는 Janus의 핵심 메커니즘인 플러그인 로딩 방식과 런타임 관리 원리를 살본다.

Janus 계층 구조

Janus는 명확한 두 계층으로 분리되어 있다. 코어 계층은 스레드 풀 관리, 네트워크 이벤트 처리, WebRTC 프로토콜 스택 구현 등 인프라 기능을 담당한다. 반면 플러그인 계층은 실제 미디어 처리 로직, 시그널링 명령 해석, 비즈니스 규칙 적용 등을 수행한다.

이러한 분리는 빈번하게 변경되는 요구사항에 신속히 대응할 수 있게 한다. 새로운 기능이 필요할 때 기존 코드를 수정하지 않고 독립적인 플러그인을 작성하여 배포하면 된다.

동적 라이브러리 로딩 메니즘

Janus의 플러그인 시스템은 POSIX 표준인 dlopendlsym 인터페이스에 기반한다. 이 API들을 이해하면 Janus의 내부 동작을 쉽게 파악할 수 있다.

#include <dlfcn.h>

/* 공유 라이브러리를 메모리에 적재 */
void* dlopen(const char* filepath, int flags);

/* 적재된 라이브러리 내 심볼 주소 획득 */
void* dlsym(void* handle, const char* symbol_name);

dlopen의 두 번째 인자는 바인딩 시점을 제어한다. RTLD_LAZY는 실제 참조 시점에 바인딩하고, RTLD_NOW는 즉시 모든 볼을 해석한다. Janus는 후자를 사용하여 런타임 심볼 오류를 방지한다.

동적 로딩 예제

간단한 산술 연산 라이브러리를 작성하여 동적 로딩 과정을 확인해 보자.

라이브러리 소스 (math_ops.c):

#include <stdio.h>

int accumulate(int x, int y, int z) {
    return x + y + z;
}

/* 컴파일 명령: gcc -fPIC -shared -o libmath.so math_ops.c */

로더 구현 (loader.c):

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef int (*triadic_func)(int, int, int);

int main(void) {
    void *lib_handle = dlopen("./libmath.so", RTLD_NOW | RTLD_LOCAL);
    if (!lib_handle) {
        fprintf(stderr, "라이브러리 로딩 실패: %s\n", dlerror());
        return EXIT_FAILURE;
    }

    triadic_func calc = (triadic_func)dlsym(lib_handle, "accumulate");
    if (!calc) {
        fprintf(stderr, "심볼 해석 실패: %s\n", dlerror());
        dlclose(lib_handle);
        return EXIT_FAILURE;
    }

    int outcome = calc(5, 10, 15);
    printf("연산 결과: %d\n", outcome);

    dlclose(lib_handle);
    return EXIT_SUCCESS;
}

Janus 플러그인 로딩 흐름

Janus는 지정된 디렉터리를 순회하여 공유 객체 파일을 발견하면 즉시 메모리에 적재한다. 핵심 로직은 다음과 같이 단순화할 수 있다.

DIR *plugin_dir = opendir(configured_path);
struct dirent *entry;

while ((entry = readdir(plugin_dir)) != NULL) {
    if (!is_valid_plugin_file(entry->d_name))
        continue;
    
    char full_path[PATH_MAX];
    snprintf(full_path, sizeof(full_path), "%s/%s", 
             configured_path, entry->d_name);
    
    void *module = dlopen(full_path, RTLD_NOW | RTLD_GLOBAL);
    if (!module)
        continue;
    
    /* 팩토리 함수 획득 */
    janus_plugin* (*instantiate)(void);
    instantiate = dlsym(module, "create");
    
    if (instantiate) {
        janus_plugin *instance = instantiate();
        register_plugin(instance);
    }
}

각 플러그인은 반드시 create라는 이름의 팩토리 함수를 노출해야 한다. 이 함수는 janus_plugin 구조체를 반환하며, 해당 구조체에는 다양한 콜백 함수 포인터가 포함된다.

필수 구현 인터페이스

플러그인 개발자가 반드시 구현해야 하는 메서드 목록은 다음과 같다.

메서드역할
init설정 파일 파싱, 내부 자원 초기화
destroy정리 작업 및 메모리 반환
get_api_compatibilityJanus 코어와의 API 버전 호환성 검증
get_version숫자 형태 버전 정보
get_version_string가독성 있는 버전 문자열
get_description기능 설명 문서화
get_name간결한 식별자
get_package유일한 네임스페이스 (예: janus.plugin.voicemail)
create_session클라이언트 세션 객체 생성
handle_messageJSON 시그널링 메시지 처리
handle_admin_message관리 API 명령 처리
setup_media미디어 전송 채널 준비
slow_link네트워크 품질 저하 알림 처리
hangup_media연결 종료 이벤트 처리
query_session런타임 세션 상태 조회
destroy_session세션 자원 해제

다음 세 가지는 선택적 구현이다.

  • incoming_rtp: 수신 RTP 패킷 가로채기
  • incoming_rtcp: 수신 RTCP 피드백 처리
  • incoming_data: SCTP DataChannel 데이터 수신
  • data_ready: DataChannel 전송 가능 상태 확인

필수 인터페이스를 모두 구현하면 Janus 코어는 해당 플러그인을 정상적인 컴포넌트로 인식하고, 시그널링 흐름에 통합하여 동작시킨다.

태그: Janus WebRTC 미디어서버 동적라이브러리 dlopen

5월 25일 08:42에 게시됨