[번역 : AOSA Volume 1, 10장] 지트시(Jitsi)

이 내용은 AOSA(The Architecture of Open Source Applications) Volume 1의 10장을 번역한 내용입니다. 오역이 상당히 많으니 원문을 꼭 참고해서 보시기를 강추합니다.

지트시(Jitsi)는 사람들이 비디오 및 음성 통화, 데스크톱 공유 그리고 파일과 메시지를 주고받을 수 있는 애플리케이션이다. 또한, 표준화된 XMPP(Extensible Messaging and Presence Protocol)와 SIP(Session Initiation Protocol) 및 야후(Yahoo!)와 윈도 라이브 메신저(MSN)와 같은 상업용 프로토콜까지 다양하게 지원하고 있다. 지트시는 마이크로소프트 윈도, 애플 맥 OS X, 리눅스 그리고 FreeBSD에서 동작한다. 대부분 자바(Java)로 개발되었지만, 일부는 네이티브 코드(native code)로 개발되었다. 이 장에서는, 지트시의 OSGi-기반 아키텍처와 구현과 프로토콜 관리방법을 살펴보고, 이 프로젝트를 준비하면서 배운 교훈을 살펴볼 것이다.

10.1. 지트시 설계

지트시(그 때는 SIP 커뮤니케이터)를 설계시에 고려했던 가장 중요한 세 가지는 다중 프로토콜(multi-protocol), 다중 플랫폼(cross-platform)과 개발자 편의(developer-friendliness)를 지원하는 것이었다. 개발자 관점에서, 다중 프로토콜 지원은 모든 프로토콜에 대한 공통 인터페이스를 가지는 것이다. 즉, 사용자가 메시지를 보낼 때, GUI(graphical user interface)는 현재 선택된 프로토콜이 실제로 sendXmppMessage나 sendSipMsg 메서드를 호출하는지 상관없이, 늘 sendMessage 메서드를 호출하도록 한다.
지트시는 큰 프로젝트이고, 대부분의 코드는 자바로 개발되었으며, 두 번째 제약(다중 플랫폼(cross-platform) 지원)을 만족시킨다. 여전히, JRE(Java Runtime Environment)는 웹캠(webcam)에서 비디오를 캡처하는 것과 같이, 지원하지 않거나 필요하지만 할 수 없는 것들이 있다. 그래서 윈도에서는 다이렉트쇼(DirectShow), 맥 OS X에서는 큐티킷(QTKit) 그리고 리눅스에서는 리눅스2(Linux2) 사용이 필요하다. 프로토콜과 마찬가지로, 비디오 통화를 제어하는 코드 일부는 위의 자세한 내용(그 자체로 충분히 복잡하다)에 대해서 상관하기 원하지 않는다.

마지막으로, 개발자-편의적(developer-friendly)이라는 것은 새로운 기능의 추가가 쉬워야 한다는 것이다. 오늘날, 많은 사람은 다양한 방법으로 VoIP를 사용하고 있다; 다양한 서비스 제공 업체와 서버 업체(vender)는 새로운 기능에 대한 많은 사용 사례와 아이디어를 가지고 있다. 우리는 그들(서비스 제공 업체나 서버 업체)이 원하는 방식으로 지트시가 사용하기 쉬운지 확인해야 한다. 새로운 것을 추가하기 원하는 개발자는, 수정하거나 확장하기 원하는 프로젝트에서 해당하는 일부만 읽고 이해해야 한다. 마찬가지로, 어느 개발자가 코드를 수정하게 되면, 다른 개발자의 작업에 영향을 주지 않아야 한다.

요약하면, 코드의 다른 부분은 상대적으로 독립적인 환경이 필요했다. 이것은 운영 체제에 따라 일부를 쉽게 교체할 수 있어야 했다; 프로토콜과 같이, 동시에 실행하거나 아직 동일하게 행동하거나; 완전히 변경하지 않고 일부 중 하나를 재작성하고 코드의 나머지 부분으로 어떤 변화 없이 동작할 수 있어야 한다. 마지막으로, 인터넷으로 플러그인 목록에서 플러그인을 다운로드 할 수 있는 기능뿐 아니라, 일부 기능(parts)을 쉽게 켜고 끌 수 있는 기능을 원했다.

간단하게, 자체 프레임워크의 개발을 고려했지만 생각을 바꿨다. 가능한 빠르게 VoIP와 IM 코드를 개발하기 시작했고, 플러그인 프레임워크 개발에 몇 달이 지났지만, 흥미롭진 않았다. 누군가 OSGi를 제안했고, 완벽하게 적합해 보였다.

10.2. 지트시와 OSGi 프레임워크

OSGi에 대해서는 책도 있기에, OSGi 프레임워크에 대한 것들을 살펴보지는 않겠다. 대신, OSGi가 제공하는 것이 무엇이고, 지트시에서 사용하는 방법에 대해서만 살펴볼 것이다.

다른 무엇보다, OSGi는 모듈(module)에 관련된 것이다. OSGi 애플리케이션의 기능은 번들(bundle)로 구분한다. OSGi 번들은 자바(Java) 라이브러리나 애플리케이션 배포에 사용하는 JAR 파일과 같다. 지트시는 번들의 모음이다. 하나는 윈도 라이브 메신저(Windows Live Messenger)에 연결하고, 다른 하나는 XMPP에 연결하고, 또 다른 하나는 GUI를 처리하는 등의 번들이 있다. 이 번들은 오픈소스 OSGi 구현체인 아파치 펠릭스(Apache Felix)가 제공하는 환경에서 동시에 동작한다.

이 모듈은 동시에 동작하는 것이 필요하다. GUI 번들은 메시지의 기록을 처리하는 번들을 통해서 차례대로 메시지를 저장하는 프로토콜 번들로 메시지를 보내야 한다. OSGi 서비스는 다른 모든 서비스가 볼 수 있는 번들 일부를 나타낸다. 한 OSGi 서비스는 대체로 자바 인터페이스의 모음이고, 로깅, 네트워크로 메시지 보내기 및 최근 전화 목록을 살펴보는 것과 같은 특정 기능을 사용할 수 있게 한다. 실제로 기능을 구현하는 클래스는 서비스 구현에 의해서 알 수 있다. 대부분은 마지막에 “Impl” 접미사와 같이, 구현하는 서비스의 인터페이스 이름을 사용한다(예로, ConfigurationServiceImpl). OSGi 프레임워크는 개발자에게 서비스 구현을 숨기게 하고, 외부에 번들이 노출되지 않게 한다. 이 방법은, 다른 번들은 서비스 인터페이스로만 번들을 사용할 수 있다는 것이다.

대부분의 번들은 액티베이터(activator)가 있다. 액티베이터는 start 메서드와 stop 메서드가 정의된 간단한 인터페이스이다. 지트시에서 펠릭스가 번들을 로딩하고 삭제할 때마다, 액티베이터가 위 메서드를 호출해서, 번들은 동작하거나 종료하는 것을 준비할 수 있다. 펠릭스에서 이 메서드를 호출할 때, BundleContext라는 참조를 전달한다. BundleContext는 번들이 OSGi 환경에 연결하는 방법을 제공한다. 이 방법으로, 번들은 사용하기 원하는 OSGi 서비스가 어느 것이든 찾거나 스스로 등록할 수 있다(그림 10.1).


그림 10.1 : OSGi 번들 활성화

다음으로, 실제로 동작하는 방법을 보자. 영속적으로 속성을 저장하고 검색하는 서비스를 생각해 보자. 지트시에서, 이 서비스는 다음과 같이 ConfigurationService라고 한다.

package net.java.sip.communicator.service.configuration;
 public interface ConfigurationService
 {
   public void setProperty(String propertyName, Object property);
   public Object getProperty(String propertyName);
 }

ConfigurationService의 아주 간단한 구현은 다음과 같다:

package net.java.sip.communicator.impl.configuration;

import java.util.*;
import net.java.sip.communicator.service.configuration.*;

public class ConfigurationServiceImpl implements ConfigurationService
{
  private final Properties properties = new Properties();
  public Object getProperty(String name)
  {
    return properties.get(name);
  }

  public void setProperty(String name, Object value)
  {
    properties.setProperty(name, value.toString());
  }
}

서비스가 net.java.sip.communicator.service 패키지에서 어떻게 정의되어 있는지 주의하고, 구현은net.java.sip.communicator.impl 패키지에 있다. 지트시에서 모든 서비스와 구현은 두 개의 패키지로 나뉘어 있다. OSGi는 번들이 자신의 JAR에 외부에서 볼 수 있는 일부 패키지를 만들 수 있게 하고, 그래서 패키지 분리는 번들이 오직 서비스 패키지만을 외부로 노출하고, 구현을 숨긴 채로 유지하기 쉽게 해준다.

사람들이 우리의 구현을 사용할 수 있도록 하기 위해서 우리가 해야하는 마지막 사항은, BundleContext에 등록하고, ConfigurationService의 구현을 제공하고 지정하는 것이다. 방법은 다음과 같다:

package net.java.sip.communicator.impl.configuration;

import org.osgi.framework.*;
import net.java.sip.communicator.service.configuration;

public class ConfigActivator implements BundleActivator
{
  public void start(BundleContext bc) throws Exception
  {
    bc.registerService(ConfigurationService.class.getName(), // service name
         new ConfigurationServiceImpl(), // service implementation
         null);
  }
}

ConfigurationServiceImpl 클래스가 BundleContext에 등록되면, 다른 번들이 이 클래스를 사용할 수 있다. 다음은 일부 번들이 Configuration 서비스를 사용하는 방법을 보여주는 예이다:

package net.java.sip.communicator.plugin.randombundle;

import org.osgi.framework.*;
import net.java.sip.communicator.service.configuration.*;

public class RandomBundleActivator implements BundleActivator
{
  public void start(BundleContext bc) throws Exception
  {
    ServiceReference cRef = bc.getServiceReference(ConfigurationService.class.getName());
    configService = (ConfigurationService) bc.getService(cRef);

    // And that's all! We have a reference to the service implementation
    // and we are ready to start saving properties:
    configService.setProperty("propertyName", "propertyValue");
  }
}

다시 한번, 패키지를 확인하자. net.java.sip.communicator.plugin 패키지에서 다른 번들이 정의한 서비스를 사용하는 번들은 유지하지만, 자체로 노출하거나 구현할 수 없다. Configuration 형태는 플러그인의 좋은 예이다: 지트시 사용자 인터페이스(user interface)에 부가적인 것으로, 사용자에게 애플리케이션의 특정 부분을 설정하게 한다. 사용자가 환경설정(preferences)을 변경할 때, 설정 폼은 ConfigurationService 또는 기능을 제공하는 번들과 직접 상호작용한다. 하지만, 어떤 번들도 어떤 방법(그림 10.2)으로 상호 작용할 필요가 없다.

그림 10.2 서비스 구조

10.3. 번들 빌드 및 동작

번들에 코드를 작성하는 방법을 살펴봤고, 이제 패키징(packaging)에 대해서 살펴볼 시간이다. 동작할 때, 모든 번들은 OSGi 환경에 세 가지를 지정해야 한다: 번들에서 사용할 수 있는 자바 패키지(예로, 패키지 노출), 번들에서 사용하기 원하는 패키지(예로, 패키지 불러오기) 그리고 번들의 BundleActivator 클래스 이름이다. 번들은 배포하는 JAR 파일의 메니페스트(manifest)로 처리한다.
위에서 정의한 ConfigurationService의 메니페스트 파일은 다음과 같다:

Bundle-Activator: net.java.sip.communicator.impl.configuration.ConfigActivator
Bundle-Name: Configuration Service Implementation
Bundle-Description: A bundle that offers configuration utilities
Bundle-Vendor: jitsi.org
Bundle-Version: 0.0.1
System-Bundle: yes
Import-Package: org.osgi.framework,
Export-Package: net.java.sip.communicator.service.configuration

JAR 메니페스트를 만들고 나면, 번들 자체를 만들 준비가 된 것이다. 지트시는 아파치 앤트(Apache Ant)로  모든 빌드와 관련된 작업을 처리한다. 지트시 빌드 프로세스(build process)에 번들을 추가하기 위해서는, 프로젝트 루트 디렉토리의 build.xml을 수정해야 한다. 번들할 JAR는 bundle-xxx의 작업으로, build.xml 파일의 아래쪽에 있다. 설정 서비스를 빌드하기 위해서는 다음이 필요하다:

<target name="bundle-configuration">
  <jar destfile="${bundles.dest}/configuration.jar" manifest=
    "${src}/net/java/sip/communicator/impl/configuration/conf.manifest.mf" >

    <zipfileset dir="${dest}/net/java/sip/communicator/service/configuration"
        prefix="net/java/sip/communicator/service/configuration"/>
    <zipfileset dir="${dest}/net/java/sip/communicator/impl/configuration"
        prefix="net/java/sip/communicator/impl/configuration" />
  </jar>
</target>

보면 알겠지만, 앤트 타켓(Ant target)은 설정 메니페스트를 이용해서 JAR파일을 만들고, service와 impl 계층에서 설정 패키지를 번들에 추가한다. 이제, 필요한 것은 펠릭스가 번들을 로딩하는 것이다.

지트시는 단지 OSGi 번들의 모음이라고 말했다. 사용자가 애플리케이션을 실행하면, 실제로 로드가 필요한 번들의 목록과 함께 펠릭스를 시작한다. 지트시 lib 디렉토리에 felix.client.run.properties 파일에 번들의 목록이 있다. 펠릭스는 시작 레벨로 정의된 순서대로 번들을 시작한다: 특정 레벨의 모든 번들은 다음 레벨의 로딩이 시작되기 전에 완료하는 것을 보장한다. 비록, 위에서 코드 예제까지는 보지 않았지만, 설정 서비스는 파일에 속성을 저장하기에, fileaccess.jar 파일에서 제공하는 FileAccessService 사용이 필요하다. 따라서 ConfigurationService가 FileAccessService 다음에 시작되는 지를 확인할 것이다:

⋮    ⋮    ⋮
felix.auto.start.30= \
reference:file:sc-bundles/fileaccess.jar

felix.auto.start.40= \
reference:file:sc-bundles/configuration.jar \
reference:file:sc-bundles/jmdnslib.jar \
reference:file:sc-bundles/provdisc.jar \
⋮    ⋮    ⋮

felix.client.run.properties 파일을 열면, 처음으로 다음의 패키지 목록을 보게 될 것이다:

org.osgi.framework.system.packages.extra= \
apple.awt; \
com.apple.cocoa.application; \
com.apple.cocoa.foundation; \
com.apple.eawt; \
⋮    ⋮    ⋮

패키지 목록은 시스템 클래스 패스에서 번들로 제공할 필요가 있는 패키지를 펠릭스에게 알려준다. 이것은, 목록에 있는 패키지는 번들로 패키징하지 않고도, 번들(예로, Import-Package 매니페스트 헤더에 추가되어 있다)에서 사용할 수 있다는 것이다. 목록 대부분은 운영체제 별 JRE 부분의 패키지를 포함하고 있고, 지트시 개발자는 새로운 것을 추가할 필요가 거의 없다; 대 부분의 패키지는 번들로 이용될 수 있다.

10.4. 프로토콜 프로바이더 서비스

지트시의 ProtocolProviderService는 모든 프로토콜 구현의 동작 방식을 정의한다. ProtocolProviderService 인터페이스는 번들(사용자 인터페이스와 같이)이 지트시가 연결한 네트워크로 메시지를 보내거나 받기, 전화 걸기 그리고 파일을 공유할 때 사용한다.

프로토콜 서비스 인터페이스는 모두 net.java.sip.communicator.service.protocol 패키지에 있다. 서비스를 지원하는 프로토콜마다 여러 구현 클래스가 있고, 모두 net.java.sip.communicator.impl.protocol.protocol_name으로 저장되어 있다.

service.protocol 패키지부터 시작해 보자. 가장 중요한 것이 ProtocolProviderService 인터페이스이다. 누군가 프로토콜-연관 작업을 수행할 때마다, BundleContext에서 해당 서비스의 구현을 찾아야 한다. 서비스와 그 구현은 지트시가 지원하고 있는 네트워크에 연결하거나, 연결 상태와 세부 정보를 검색할 수 있게 하고, 가장 중요한 채팅과 전화 통화와 같은 실 커뮤니케이션 작업을 구현하는 클래스의 참조를 얻을 수 있다.

10.4.1. 기능 셋

앞에서 언급한 바와 같이, ProtocolProviderService는 다양한 통신 프로토콜과 그 차이에 대한 활용(협약, 영향력)이 필요하다. 모든 프로토콜이 제공하는 메시지 보내기와 같은 기능에 대해서는 단순하지만, 일부 프로토콜 만이 지원하는 작업에 대해서는 까다롭다. 이 차이는 종종 서비스 자체에서 기인한다: 예로, 대부분의 SIP 서비스는 서버에 저장된 주소록을 지원하지 않지만, 다른 프로토콜에서는 비교적 잘 지원하는 기능이다. MSN과 AIM이 좋은 예이다: 한때, 두 서비스 모두 오프라인 사용자에게 메시지 보내는 기능을 제공하지 않았지만, 지금은 둘 다 제공한다(변경되었다).

결론으로 ProtocolProviderService가 다른 번들, GUI와 같이, 그에 맞춰 동작하는(실제로 전화하는 방법이 없다면, AIM 연락처에 전화 버튼을 추가해봐야 아무 소용이 없다) 차이를 처리하는 방법이 필요하다.

OperationSets이 도와준다(그림 10.3). 당연히, OperationSets은 기능의 집합이고, 지트시 번들이 프로토콜 구현을 제어하는 데 사용하는 인터페이스를 제공한다. 기능 셋 인터페이스에 있는 메서드 모두는 특정 기능과 연관되어 있다. 예로, OperationSetBasicInstantMessaging은 메시지 생성과 전송 그리고 지트시가 받은 메시지를 검색할 수 있게 리스너 등록 메서드를 포함하고 있다. 다른 예로, OperationSetPresence는 주소록에 있는 연락처의 상태에 질의하거나 상태를 설정하는 메서드가 있다. 따라서 GUI가 연락처 또는 연락처에 메시지를 보내는 상태를 갱신할 때, 존재 여부와 메시징을 지원하는지를 해당 업체에 문의할 수 있는 것이 첫 단계이다. ProtocolProviderService가 정의하고 있는 메서드는 다음과 같다:

public Map<String, OperationSet> getSupportedOperationSets();
public <T extends OperationSet> T getOperationSet(Class<T> opsetClass);

OperationSets은 추가하는 새로운 프로토콜이 OperationSet에서 정의한 기능 일부를 지원하도록 설계해야 했다. 예를 들어, 일부 프로토콜은 서버에 저장된 주소록을 제공하지 않더라도 서로 상태에 대한 질의를 허용한다.  따라서OperationSetPresence에서 존재 관리(presence management)와 친구 목록(buddy list) 검색 기능을 결합하는 것보다, perationSetPersistentPresence는 오직 주소록을 온라인으로 저장할 수 있는 프로토콜을 정의했다. 반면에, 전혀 받는 거 없이 오직 메시지 전송만을 허용하는 프로토콜을 제공하고 있는데, 그 이유는 메시지를 보내고 받는 식으로 안전하게 결합할 수 있기 때문이다.


그림 10.3 : 기능 셋

10.4.2. 계정, 팩토리 그리고 프로바이더 객체

ProtocolProviderService의 중요한 특징은 하나의 객체가 하나의 프로토콜 계정에 대응한다는 것이다. 따라서 사용자로 등록한 계정이 있다면, BundleContext에서 많은 서비스 구현을 사용할 수 있다.

이 시점에, 누가 프로토콜 프로바이더를 생성하고 등록하는지 궁금할 수 있다. 두 개의 엔티티(entities)가 연관되어 있다. 첫째, ProtocolProviderFactory가 있고, 이 팩토리는 번들에게 프로바이더를 초기화하고 서비스로 등록하게 하는 서비스이다. 프로토콜마다 하나의 팩토리가 있고 모든 팩토리는 특정 프로토콜의 프로바이더를 생성하는 책임이 있다. 팩토리 구현은 프로토콜 내부의 나머지 부분과 함께 저장된다. SIP에 대해 예로, net.java.sip.communicator.impl.protocol.sip.ProtocolProviderFactorySipImpl 클래스가 있다.

계정 생성에 연관된 두 번째 엔티티는 프로토콜 위자드다. 팩토리와 다르게, 위자드는 프로토콜 구현의 나머지로부터 분리할 수 있기 때문에 그래픽 유저 인터페이스와 연관이 있다. 위자드는 사용자에게 SIP 계정을 만들 수 있게 하고, 예로, net.java.sip.communicator.plugin.sipaccregwizz가 있다.

10.5. 미디어 서비스

IP기반으로 실시간 통신을 사용하는 경우, 이해하는 데 중요한 한가지가 있다: SIP와 XMPP 같은 프로토콜은 가장 일반적으로 인식하고 있는 VoIP 프로토콜이지만, 실제로 인터넷으로 음성 및 비디오를 전송하는 것은 아니다. 전송 작업은 RTP(Real-time Transport Protocol )가 처리한다. SIP와 XMPP는 RTP 패킷이 보내져야 하는 주소를 결정하고, 오디오와 비디오가 인코딩(예, 코덱)돼야 하는 포맷을 협상하는 등의 단지 RTP가 필요한 것을 준비하는 책임이 있다. 또한, 사용자의 위치, 존재 여부를 유지하고, 전화를 거는 등과 같은 많은 것들을 관리한다. 이것이 SIP와 XMPP와 같은 프로토콜이 종종 신호 프로토콜이라 불리는 이유이다.

지트시에서 맥락(context)이 의미하는 것이 무엇일까? 우선 맥락의 의미는, SIP 또는 재버(Jabber) 지트시 패키지에서 오디오나 비디오의 흐름을 조작하는 어떤 코드도 찾지 못하리라는 것이다. 이런 종류의 코드는 미디어서비스(MediaService)에 있다. 미디어 서비스와 구현은 net.java.sip.communicator.service.neomedia와net.java.sip.communicator.impl.neomedia 패키지에 있다.

왜 네오미디어(neomedia) 인가?

네오미디어(neomedia) 패키지 이름의 네오(neo)는, 원래 사용했던 비슷한 패키지를 교체했고 완전히 재 개발했다는 것을 나타낸다. 이것은 우리가 규칙중의 하나로 제시한 방법이다: 100%로 미래를 보증하는 애플리케이션을 설계하는데 많은 시간을 소비하는 것이 그렇게 가치 있지가 않다. 계정이 모든 것을 유지하는 간단한 방법은 없다, 그래서 나중에 변경을 해야 하는 것이 있게 마련이다. 게다가, 설계 단계에 공들이는 것은 준비한 시나리오가 일어나지 않을 수 있기에 필요치 않은 복잡성을 상당히 제시할 것이다.

미디어서비스(MediaService) 자체와 더불어, 특히 중요한 두 개의 인터페이스가 있는데, MediaDevice와 MediaStream이다.

10.5.1. 캡처, 스트리밍 그리고 재생

미디어 장치(Device)는 전화하는 동안(그림 10.4) 캡처와 재생하는 장치를 나타낸다. 마이크와 스피커, 헤드셋과 웹캠 모두는 미디어 장치의 예지만, 단지 그것들만이 미디어 장치가 아니다. 지트시에서 데스크톱 스트리밍과 전화 공유(sharing call)는 컨퍼런스 콜이 참여자로부터 받은 오디오를 믹스하기 위해 오디오믹서(AudioMixer) 장치를 사용하는 동안, 데스크탑에서 비디오를 캡처한다. 모든 경우에, 미디어 장치는 오직 하나의 미디어타입(MediaType)을 나타낸다. 즉, 미디어 장치는 오디오나 비디오중의 하나가 될 순 있지만, 둘 다는 안 된다. 이것은, 마이크가 내장된 웹캠을 가지고 있다고 예를 들면, 지트시는 두 개의 장치로 인식한다: 하나는 오직 비디오를 캡처할 수 있고, 다른 하나는 사운드만 캡처할 수 있다.

하지만, 장치 자체로, 전화나 비디오 통화를 하기에는 충분하지 않다. 미디어를 재생하고 캡처하는 것과 더불어, 필요한 한 가지가 네트워크로 미디어를 전송할 수 있어야 한다. 이것이 미디어스트림(MediaStream)이 필요한 이유다. 미디어스트림 인터페이스는 상대의 미디어 장치 연결하는 무엇이다. 미디어스트림은 전화하는 내내 교신하는 수신/발신 패킷을 나타낸다.

장치에 따라, 하나의 스트림은 오직 하나의 미디어타입을 처리할 수 있다. 이것은, 지트시에서 오디오/비디오 통화는 두 개로 분리된 미디어스트림을 만들고, 다음으로 오디오나 비디오 미디어 장치에 각자 연결해야 한다는 것이다.


그림 10.4 : 다른 장비를 위한 미디어 스트림

10.5.2. 코덱

미디어 스트리밍에서 중요한 또 하나의 개념(concept)은 코덱(codec)이라 부르는 미디어포맷(MediaFormats)이다. 기본적으로, 대부분의 운영 체제는 48KHz PCM이나 유사한 무언가로 오디오를 캡처한다. 이것은 종종 원시 오디오(raw audio)라고 부르고 WAV 파일(좋은 품질과 엄청난 크기)로 오디오의 일종이다. 인터넷으로 PCM 포맷의 오디오를 전송하는 것은 매우 비현실적이다.

코덱은 다양한 방법으로 오디오나 비디오를 표현하고 전송하기 위한 것이다. iLBC, 8KHz Speex 또는 G.729과 같은 일부 오디오 코덱은 낮은 대역폭이 필요하지만, 약간의 사운드를 손실한다. Speex와 G.722과 같은 광대역 코덱은 좋은 오디오 품질을 제공하지만, 더 많은 대역폭이 필요하다. 코덱은 대역폭 요구사항을 유지하면서 합리적인 수준으로 좋은 품질을 제공하려는 것이다. 대중적인 비디오 코덱인 H.264가 좋은 예이다. 트레이드 오프(trade-off)는 코덱의 변환과정에서 필요한 계산의 양이다. 지트시에서 H.264 비디오 통화를 사용하면, 좋은 품질의 이미지를 볼 수 있고 대역폭 요구사항도 매우 합리적이지만, CPU를 최대로 사용한다.

지나친 단순화겠지만, 코덱의 선택은 전적으로 타협이라는 것이다. 대역폭, 품질, CPU 사용률이나 이것들의 일부 조합을 희생한다. 사람들은 코덱에 대해 알 필요없이 VoIP와 작업한다.

10.5.3. 프로토콜 프로바이더로 연결하기

현재 지트시에서 프로토콜은 미디어서비스가 사용하는 모든 오디오/비디오를 동일한 방법으로 지원한다. 처음으로 시스템에서 사용할 수 있는 장치를 미디어서비스에 요청한다:

public List getDevices(MediaType mediaType, MediaUseCase useCase);

미디어타입은 오디오나 비디오 장치에 대한 연관 여부를 나타낸다. 미디어유즈케이스(MediaUseCase) 파라미터는 현재 오직 비디오 장치만을 고려했다. 이 파라미터는 미디어서비스에 일반 전화(MediaUseCase.CALL), 사용할 수 있는 웹캠 목록을 반환하는 경우, 또는 데스크탑 공유 세션(MediaUseCase.DESKTOP), 사용자 데스크톱의 참조를 반환한 경우에 사용할 수 있는 장치가 있는지 알려준다.

다음 단계는 특정 장치에서 사용할 수 있는 포맷의 목록을 가져오는 것이다. MediaDevice.getSupportedFormats 메서드로 한다:

public List getSupportedFormats();

장치 목록을 가져오면, 지원하는 것 중 한 개를 지정하기 위해, 목록 일부를 응답으로, 원격 당사자에게 보낸다. 이 교환은 Offer/Answer 모델 로 알려져 있고, 종종 SDP(Session Description Protocol)나 SDP 형태 일부를 사용한다.

Offer/Answer 모델이란? Offer/answer 코덱 협상 호 처리 기능은 발신측에서 수신 받기를 원하는 코덱 정보를 착신측에게 전송하여 제시(offer)하면, 착신측은 발신측에서 제시한 코덱 중에 서 수신받고자 하는 코덱을 SDP를 이용하여 응답(answer)한다. Offer/answer 방식을 이용하여 발신과 착신간의 코덱을 협상한 후에 선택된 코덱으로 통화하도록 한다. Offer/answer 코덱 협상 호 처리에 대한 표준은 IETF(www.ietf.org.) MMUSIC 워킹 그룹에서 2002년 6월에 “An Offer/ Answer Model with the SDP” RFC 3264로 제정되었다

포맷과 일부 포트 번호와 IP 주소를 교환한 후에, VoIP 프로토콜은 미디어스트림을 생성, 구성하고 시작한다. 대략, 이 초기화 과정은 다음의 순서를 따른다:

// first create a stream connector telling the media service what sockets
// to use when transport media with RTP and flow control and statistics
// messages with RTCP
StreamConnector connector =  new DefaultStreamConnector(rtpSocket, rtcpSocket);
MediaStream stream = mediaService.createMediaStream(connector, device, control);

// A MediaStreamTarget indicates the address and ports where our
// interlocutor is expecting media. Different VoIP protocols have their
// own ways of exchanging this information
stream.setTarget(target);

// The MediaDirection parameter tells the stream whether it is going to be
// incoming, outgoing or both
stream.setDirection(direction);

// Then we set the stream format. We use the one that came
// first in the list returned in the session negotiation answer.
stream.setFormat(format);

// Finally, we are ready to actually start grabbing media from our
// media device and streaming it over the Internet
stream.start();

이제, 웹캠에서 마이크로 “Hello world!”라고 말할 수 있다.

10.6. UI 서비스

지금까지 지트시에서 프로토콜, 메시지 전송 및 수신 그리고 전화 거는 것을 살펴봤다. 하지만 무엇보다, 지트시는 실제로 사람들이 사용하는 애플리케이션이고, 가장 중요한 부분 중의 하나가 지트시의 유저 인터페이스(user interface)이다. 유저 인터페이스에서 대부분 시간은 지트시가 노출하는 번들인 서비스를 사용한다. 하지만 반대의 경우도 일부 있다.
플러그인이 떠오르는 첫 번째 예이다. 지트시에서 플러그인은 종종 사용자와 상호 작용할 수 있어야 한다. 이것은 유저 인터페이스에서 기존의 윈도나 패널의 컴포넌트를 열고, 종료하고, 이동하고 또는 추가해야 한다는 것이다. 유아이서비스(UIService)가 여기에서 역할을 한다. 유아이서비스는 지트시에서 메인 윈도로 기본적인 제어를 할 수 있고, 이것이 맥 OS X 독의 아이콘과 윈도우즈 알림 영역을 사용자가 애플리케이션에서 제어하게 하는 방법이다.

간단하게 주소록을 활용하는 것에 추가로, 플러그인 또한 확장할 수 있다. 지트시에서 채팅 암호화(OTR)를 지원하는 플러그인이 좋은 예이다. OTR 번들은 유저 인터페이스의 다양한 부분에 몇 개의 GUI 컴포넌트를 등록해야 한다. 채팅 창에 자물쇠 버튼을 그리고 모든 연락처의 우 클릭(right-click) 메뉴에 하위 섹션을 추가한다.

희소식은 단지 몇 개의 메서드 호출로 이 작업을 수행할 수 있다는 것이다. OTR 번들의 OSGi 액티베이터인 OtrActivator는 다음의 코드를 가지고 있다:

Hashtable<String, String> filter = new Hashtable<String, String>();

// Register the right-click menu item.
filter(Container.CONTAINER_ID, Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID());

bundleContext.registerService(PluginComponent.class.getName(),
    new OtrMetaContactMenu(Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU), filter);

// Register the chat window menu bar item.
filter.put(Container.CONTAINER_ID, Container.CONTAINER_CHAT_MENU_BAR.getID());
bundleContext.registerService(PluginComponent.class.getName(),
           new OtrMetaContactMenu(Container.CONTAINER_CHAT_MENU_BAR), filter);

봐서 알 수 있듯이, 그래픽 유저 인터페이스에 컴포넌트를 추가하는 것은 단순히 OSGi 서비스에 등록하는 것이다. 다른(반대) 상황에서, UIService 구현은 PluginComponent 인터페이스의 구현을 기대한다. 새로운 구현체의 등록이 감지할 때마다, 구현체에 대한 참조를 획득하고 OSGi 서비스 필터에 표시된 컨테이너에 추가한다.

다음은 우클릭 메뉴 아이템은 어떻게 이런 일이 벌어지는지에 대한 것이다. UI 번들에서, 우클릭 메뉴를 나타내는 클래스는 MetaContactRightButtonMenu이고, 다음의 코드를 가지고 있다:

// Search for plugin components registered through the OSGI bundle context.
ServiceReference[] serRefs = null;
String osgiFilter = "(" + Container.CONTAINER_ID
    + "="+Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID()+")";

serRefs = GuiActivator.bundleContext.getServiceReferences(
        PluginComponent.class.getName(), osgiFilter);
// Go through all the plugins we found and add them to the menu.
for (int i = 0; i < serRefs.length; i ++)
{
    PluginComponent component = (PluginComponent) GuiActivator.bundleContext.getService(serRefs[i]);
    component.setCurrentContact(metaContact);
    if (component.getComponent() == null)
        continue;

    this.add((Component)component.getComponent());
}

그리고 그게 전부다. 지트시에서 보는 대부분 창은 정확하게 같은 것을 한다: 해당 컨테이너에 추가하기 원하는 필터를 가지고 있는 PluginComponent 인터페이스를 구현하는 서비스에 대해서 번들 컨텍스트를 검토한다. 플러그인은 목적지의 이름과 더불어 사인을 들고 있는 히치-하이커(hitch-hikers)와 비슷하고, 창을 만든다는 것은 운전자가 히치-하이커를 태우는 것이다.

10.7. 교훈

SIP 커뮤니케이터 작업을 시작했을 때, 가장 흔한 비판이나 질문 중의 하나가: “왜 자바를 사용하나요? 자바가 느린 것을 모르나요? 오디오/비디오 통화에 적절한 품질을 얻을 수 없을 것이다!” “자바는 느리다”라는 미신은 잠재적인 사용자에게 아직도 반복되고 있고, 그 이유로 지트시를 사용하는 대신에 스카이프(Skype)를 계속 사용한다. 하지만 이 프로젝트 작업에서 배운 첫 번째 교훈은, 효율성은 C++이나 다른 네이티브 언어들이 그렇듯이, 자바도 별로 관계가 없다는 것이다.

우리는, 자바를 선택한 결정이 모든 선택을 엄격하게 분석한 결과라고 생각하지 않는다. 단지, 윈도우와 리눅스에서 동작하는 것을 쉽게 빌드하는 방법을 원했고, 자바와 자바 미디어 프레임워크가 상대적으로 쉬운 방법을 제공하는 것으로 보였다.

몇 년 동안은, 이 결정을 후회할 이유가 별로 없었다. 오히려 반대로: 완벽히 투명하게 만들진 못하더라도, 자바는 이식성에 도움을 주었고, 한 OS에서 다음 OS에 적용하는데 SIP 커뮤니케이터 코드의 90%를 유지했다. 충분히 복잡한 프로토콜 스택 구현(예로, SIP, XMPP, RTP 등) 역시 모두 포함하고 있다. 코드의 그런 부분이 OS 세부사항에 대해 걱정할 필요가 없다는 것은 매우 유용하다고 입증이 되었다.

게다가, 자바의 대중성은 커뮤니티를 시작할 때 매우 중요하다고 판명이 되었다. 기여자(Contributors)는 그 자체가 부족한 자원이다. 애플리케이션 자체를 좋아하는 사람들인 기여자는 시간과 동기부여(이것들 모두 모으기가 어렵다)를 찾는 것이 필요하다. 새로운 언어를 배우는 것이 필요치 않으므로, 이점이 있다.

대부분의 기대와는 반대로, 당연히 여겨지는 자바의 성능 부족은 네이티브로 가는 이유가 되지 않는다. 네이티브 언어를 사용하기로 한 결정의 대부분 시간은 운영체제 통합으로 진행되었고, 운영체제 별 툴 을 제공하고 있다. 다음은 자바가 부족한 세 가지 중요한 부분에 대해 알아보자.

10.7.1. 자바 사운드 vs 포트오디오

자바 사운드(Java Sound)는 오디오를 캡처하고 재생하는 자바의 기본 API이다. 실행 환경의 일부고, 그래서 모든 플랫폼의 자바 가상 머신(Java Virtual Machine)에서 동작한다. SIP 커뮤니케이터나 지트시는 처음 일년동안은 자바 사운드를 사용했지만 너무 불편했다.

무엇보다, API가 사용하는 오디오 장치를 선택할 수 있는 옵션을 제공하지 않았다. 이건 큰 문제였다. 컴퓨터에서 오디오/비디오 통화를 할 때, 사용자는 종종 최상의 품질을 위해서 고급 USB 헤드셋이나 다른 오디오 장치를 사용한다. 컴퓨터에 여러 장치가 있는 경우, 자바사운드는 운영체제의 기본 장치로 모든 오디오를 보내고, 이것은 많은 경우에 좋지 않다. 많은 사용자는 기본 사운드 카드에서 실행하는 다른 애플리케이션 모두를 유지하기 원했고, 예로, 스피커로 음악 듣는 것을 유지할 수 있다. 무엇보다 중요한 것은, 많은 경우 SIP 커뮤니케이터는 사람들이 컴퓨터 앞에 없는 경우에도 사용자가 자신의 스피커로 전화 알림을 들을 수 있도록 한 다음에 전화를 받고 헤드셋을 사용하기 시작하기 위해서 오디오 알람을 한 장치에 보내고 다른 장치에 실제 오디오 콜을 보내는 것이 최선이다.
이 중 어느 것도 자바사운드로 할 수 없다. 게다가, 리눅스에서는 현재 대부분의 리눅스 배포판에서 사용하지 않는 OSS 를 사용한다.

우리는 다른 오디오 시스템을 사용하기로 했다. 멀티 플랫폼 자체의 절충을 고려하지 않고, 가능한, 우리가 모든 것을 처리하는 것을 피하고 싶었다. 포트오디오2(PortAudio2)가 아주 편리해 보였다. 자바가 지원하지 못하는 것이 있는 경우, 크로스 플랫폼을 지원하는 오픈 소스 프로젝트가 차선책이다. 포트오디오로 전환하면서 세밀한 구성의 오디오 렌더링과 위에서 기술한 것들의 캡처에 대한 지원을 구현할 수 있게 되었다. 또한, 윈도우, 리눅스, 맥 OS X, FreeBSD와 시간이 부족해서 지원하지 못했던 다른 운영체제에서도 동작한다.

10.7.2. 비디오 캡처와 렌더링

비디오도 오디오와 마찬가지로 중요하다. 하지만 JRE가 비디오를 캡처하고 렌더링하는 기본 API를 지원하지 않기에, 비디오는 자바로 만들지 못했다. 자바 미디어 프레임워크는 썬(Sun) 이 유지를 중단할 때까지 얼마 동안은 API가 유지될 것으로 보였다.

자연스럽게 포트오디오 스타일의 비디오 대안을 찾았지만, 이번에는 운이 없었다. 처음에 Ken Larson3에서 LTI-CIVIL 프레임워크를 사용하기로 결정했다. 이 프레임워크는 아주 좋았으며 꽤 오래 사용했다. 하지만 실시간 통신의 상황에서 사용했을 때는 차선책으로 결론이 났다.

그래서 지트시를 위해 완벽한 비디오 통신을 제공하는 유일한 방법은, 우리가 네이티브 그래버와 렌더러를 구현하는 것이라는 결론에 도달했다. 이것은 많은 복잡성과 많은 유지보수 로드를 추가할 것이기에, 쉬운 결정이 아니었지만, 선택의 여지가 없었다: 정말 좋은 품질의 비디오 통화를 하고 싶었고, 우리가 하고 있다!

우리의 네이티브 그래버와 렌더러는 리눅스, 맥 OS X 그리고 윈도우 각자의 Video4Linux 2, QTKit 그리고 DirectShow/Direct3D를 직접 사용한다.

10.7.3. 비디오 인코딩 디코딩

지트시의 전 버전인 SIP 커뮤니케이터가 처음부터 비디오 통화를 지원했다. 자바 미디어 프레임워크(Java Media Framework)가 H.263 코덱과 176×144 (CIF) 포맷으로 비디오를 인코딩할 수 있기 때문이다. H.263과 CIF가 무엇인지 아는 분들은 아마 미소 지을 것 같다; 만약 자바 미디어 프레임워크가 지원해야 하는 모든 것들을 가지고 있었다면, 우리 중 몇은 현재 비디오 채팅 애플리케이션에 사용할 것이다.

적절한 수준의 품질을 제공하기 위해, 우리는 FFmpeg과 같은 라이브러리를 사용해야 했다. 비디오 인코딩은, 실제로 성능 측면에서 자바가 가지고 있는 한계를 보여주는 몇 개의 영역 중의 하나이다. 그래서 다른 언어로 한다. FFmpeg 개발자가 가능한 최대한 효율적인 방법으로 비디오를 처리하기 위해서 여러 영역에서 어셈블러(Assembler)를 실제로 사용하는 증거가 있다.

10.7.4. 기타

더 좋은 결과를 위해 네이티브로 갈 필요가 있다고 결정한 다른 부분들이 꽤 많았다. 맥 OS X에서 Growl을 사용하는 시스템 트레이 알림과 리눅스의 libnotify가 그런 예이다. 그 외에, 마이크로소프트 아웃룩(Microsoft Outlook)과 애플 주소록(Apple Address Book)에서 연락처 데이터베이스에 질의하는 것, 목적지에 따른 소스 IP 주소의 결정, Speex와 G.722의 기존 코덱 구현을 사용하기, 데스크톱 스크린 캡처와 키 코드로 문자를 변환하는 것들이 있다.

중요한 것은 네이티브 솔루션 선택이 필요한 때는 언제든지, 할 수 있고, 했다. 이것이 가져다준 부분으로 : 지트시의 개발을 시작한 이후로 계속 픽스, 추가 또는 심지어 룩 앤 필과 성능을 개선하기 원했던 부분을 완전히 다시 개발했다. 그러나, 처음에 잘하지 못했던 그런 것들에 대해 후회를 하지 않았다. 의심스러웠다면, 간단하게 선택할 수 있는 옵션 중의 하나를 선택하고 받아들였을 것이다. 우리가 하는 무언가를 더 잘 알 때까지 기다릴 순 있지만, 그렇게 했다면, 지금의 지트시는 없었을 것이다.

10.8. 답례

이 장의 모든 그림에 대해서 야나 스탭체바(Yana Stamcheva)에게 정말 감사한다.

답글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.