이 글은 Neil Bartlett 의 연재 글 “Getting started with OSGi”의 번역본입니다.
* 파트 1 – 첫번째 번들 : Your First Bundle
* 파트 2 – 프레임워크와 연동하기 : Interacting with the Framework
* 파트 3 – 번들간의 의존관계 : Dependencies between bundles
* 파트 4 – 서비스 등록하기 : Registering a Service
* 파트 5 – 서비스 사용하기 : Consuming a Service
* 파트 6 – 동적으로 서비스 추적하기 : Dynamic Service Tracking
* 파트 7 – 선언적 서비스 소개하기 : Introducing Declarative Services
에 이어 8번째 마지막 글입니다. 오탈자 및 이상한 번역은 댓글로 남겨주세요
Getting started with OSGi : OSGi 시작하기 파트 8 – 선언적 서비스와 의존관계
저자 : Neil Bartlett < njbartlett at gmail dot com >
역자 : 권 정혁 < guruguru at gmail dot com >
“Getting Started with OSGi” 시리즈에 돌아 오신걸 환영합니다. 레슨 시작전에 한가지 공지, 이 튜토리얼의 이전회들을 보시려면 제 블로그 페이지에서 링크들을 찾아보실수 있습니다.
지난번에는 선언적 서비스에 대해 처음으로 살펴봤습니다. 이번에는 선언적 서비스의 사용자 측면을 살펴볼 것입니다. 지난번에 java.lang.Runnable
인터페이스로 서비스를 등록했다는것을 기억하세요. 이제 우리는 그 서비스에 의존하는 컴포넌트를 만들어 볼 것입니다.
말씀드린대로, 선언전 서비스에 대한 스펙은 이전 레슨들에서 보았던 OSGi 의 글루(glue)코드보다 여러분 코드의 Application Logic 에 집중할수 있도록 해주는 것입니다. 이걸 염두에 두고, 코드를 살펴보겠습니다. 그전에 우린 프로젝트를 하나 만들어야 합니다. 이전 레슨의 순서를 따라서, 이번엔 “SampleImporter” 라는 이름으로 프로젝트를 생성하여 보세요.
새로 생성된 Eclipse 프로젝트에 아래 코드를 복사하여 src 폴더에 붙여넣습니다.
package org.example.ds; import org.eclipse.osgi.framework.console.CommandInterpreter; import org.eclipse.osgi.framework.console.CommandProvider; public class SampleCommandProvider1 implements CommandProvider { private Runnable runnable; public synchronized void setRunnable(Runnable r) { runnable = r; } public synchronized void unsetRunnable(Runnable r) { runnable = null; } public synchronized void _run(CommandInterpreter ci) { if(runnable != null) { runnable.run(); } else { ci.println("Error, no Runnable available"); } } public String getHelp() { return "\trun - execute a Runnable service"; } }
이 클래스는 Equinox 를 실행할때 osgi> 프롬프트 상에서 사용가능한 명령들을 확장하는 CommandProvider
인터페이스를 구현합니다. CommandProvider
를 작성하는 이유는, 그게 우리 코드를 인터랙티브하게 테스트하기 편하기 때문입니다. 커맨드 프로바이더에 대해서는 IBM developerWorks 에 Chris Aniszczyk 의 기사에 더 자세한 내용이 있습니다.
우리가 이 클래스에서 어떤 OSGi API 도 호출하지 않는다는 것을 주목하세요. 실제로 우리는 org.osgi.*
패키지에서 아무것도 import 할 필요도 없습니다. 우리가 의존하는 서비스는 java.lang.Runnable
의 인스턴스이며 setRunnable
메소드로 우리에게 전달되며, unsetRunnable
메소드에 의해 치워집니다. 우리는 이것을 dependency injection 의 형태로 볼수 있습니다.
다른 두개의 메소드 getHelp
와 _run
은 커맨드 프로바이더에 대한 구현 메소드들 입니다. 네, “_run” 은 앞에 밑줄이 있는 웃기는 이름입니다만, 이것은 Equinox console APIs 의 이상한 기능이며 OSGi 나 선언적 서비스와는 상관이 없습니다. 메소드의 이름 앞에 밑줄을 넣는 패턴은 Equinox console 에서 명령이 되므로, _run
메소드를 제공한다는 것은 우리가 “run” 명령을 추가했다는것입니다. 이 클래스에서 주의할 다른점은, runnable 필드가 업데이트/접근이 쓰레딩에 안전한 형태로 이루어져야 한다는 것에 주의해야 합니다. OSGi 가 본질적으로 멀티쓰레드이기 때문에 쓰레드 안전은 중요합니다. 우린 항상 우리의 코드를 쓰레딩에 안전하게 작성해야 합니다.
전과 같이, 우린 이것이 가능하게 하는 DS 선언을 가진 XML 파일을 제공해야 합니다. 아래 코드를 OSGI-INF/commandprovider1.xml
에 복사하세요.
<?xml version="1.0"?> <component name="commandprovider1"> <implementation class="org.example.ds.SampleCommandProvider1"/> <service> <provide interface="org.eclipse.osgi.framework.console.CommandProvider"/> </service> <reference name="RUNNABLE" interface="java.lang.Runnable" bind="setRunnable" unbind="unsetRunnable" cardinality="0..1" policy="dynamic"/> </component>
제가 지난번에 말하지 않은 중요한 단계가 있습니다. ( 지적해준 Seamus Venasse 에게 감사드립니다.) 여러분의 플러그인 프로젝트안에 build.properties
를 수정하여, OSGI-INF
폴더옆 체크박스를 켜야합니다. 이것은 번들이 Eclipse export wizard 를 이용하거나 PDE 빌드를 이용해서 Export 될때 해당 폴더가 꼭 포함되도록 하기위해 필요합니다. 또한 아래 라인을 번들 Manifest 에 추가해야 합니다.
Service-Component: OSGI-INF/commandprovider1.xml
위 DS 선언은 이전에 우리가 봤던것과 같은 implementation
과 service
두개의 같은 항목을 가지고 있습니다. implementation
노드는 컴포넌트를 실제 구현하는 클래스의 이름을 표시하며, service
노드는 DS 에게 컴포넌트를 서비스로 등록하도록 알려줍니다. 이 경우는 우리가 CommandProvider
인터페이스를 이용하여 등록함으로써, Equinox 콘솔이 우리 커맨드 프로바이더 의 존재를 알게 됩니다.
다음 엘리먼트인 reference
는 우리가 이전에 보지 못한것으로, DS 에 우리가 서비스에 의존하고 있다는 것을 선언해 줍니다. name
속성은 의존관계의 이름을 나타내는 문자열이며(이게 어디에 쓰이는지는 아직 알 필요가 없습니다) interface
속성은 우리가 의존하는 인터페이스 이름을 나타냅니다. bind
속성은 서비스가 사용 가능할때 DS 에 의해 호출될 구현클래스의 메소드 이름이며, 다시 말해서 Runnable
서비스가 서비스 레지스트리에 등록될때 DS 가 새로운 서비스 객체에 대한 레퍼런스를 얻어서 이 지정 메소드를 통해 우리에게 전달해 준다는 것입니다. 비슷하게 unbind
속성은 서비스가 사용 불가능해 질때 DS 에 의해 호출될 메소드 명입니다.
cardinality
속성은 DS 의 진정한 힘을 보여줍니다. 이 속성은 의존성이 필수(mandatory)인지 선택(optional)인지, 단일인지 다수인지를 지정하게 해줍니다. 가능한 값들은
다음과 같습니다.
- 0..1: 선택이면서 단일관계 , “0 또는 1” – “zero or one”
- 1..1: 필수이면서 단일관계 , “딱 1개” – “exactly one”
- 0..n: 선택이면서 복수관계 , “0 에서 여러개” – “zero to many”
- 1..n: 필수이면서 복수관계 , “1 에서 여러개” 또는 “적어도 한개 이상” – “one to many” or “at least one”
이 예제에서는 우린 옵션이면서 단일관계를 선택했으므로, 이것은 우리의 커맨드 서비스가 의존하는 서비스가 없더라도 대처할수 있다는것을 의미합니다. _run 메소드를 다시 살펴보면 이런 상황을 대처하기 위해 서비스 핸들에 대체 null 체크가 필요했다는것을 보실수 있습니다.
이 번들을 실행했을때 어떤 일이 일어나는지 봅시다. 지난번에 작성했던 SampleExporter 번들을 아직 실행중에 있다면, osgi> 프롬프트에서 “run” 명령을 입력했을때 아래 출력을 볼 수 있습니다.
Hello from SampleRunnable
멋집니다. 이것은 우리가 지난번 레슨에서 작성한 Runnable 서비스가 성공적으로 import 되었다는것을 알려줍니다. 이제 stop 명령을 이용해서 SampleExporter 번들을 중단시킵니다. 그리고 다시 “run” 명령을 입력하면 아래와 같은 메시지를 볼수 있습니다.
Error, no Runnable available
이것으로 보아, DS 가 Runnable 서비스가 없어져 버려서 우리의 unsetRunnable 메소드를 호출했다는것을 알수 있습니다.
다시 cardinality
속성을 살펴보면, 만약 우리가 “1..1” 로 변경해서 선택에서 필수 의존관계로 바꾸면 어떤일이 생길까요 ? 한번 변경하고 Equinox 를 재실행 해보세요. 만약 SampleExporter가 활성화 되어있다면 우리가 “run” 을 입력했을때 이전과 같은 “Hello from SampleRunnable” 메시지를 볼수 있습니다. 하지만 만약 SampleExporter가비활성화 되어 있다면 전혀 다른 메시지를 볼수 있습니다. 실제로 우린 모르는 명령을 입력했을때 나오는 Equinox 콘솔의 도움말을 보게 됩니다. 이것은 우리의 Command Provider 자체가 DS 에 의해 등록해제 되었다는 것을 의미합니다! 컴포넌트가 필수 의존관계를 충족시키지 못하기 때문에 DS 가 컴포넌트를 비 활성화 하고 제공하던 서비스 자체를 등록해제 한것입니다. 그래서 Equinox 콘솔이 run 명령 자체를 모르게 된것이죠.
이런 쉬운 변경이 우리가 선언적 서비스를 이용하는 가장 큰 이유중 하나입니다. 우리가 ServiceTracker
를 이용할때는 꽤 많은 양의 코드를 재작성해야 했다는것을 기억하세요.
여러분께선 제가 아직 언급하지 않은 policy
속성이 궁금하실 것입니다. 이 속성의 값은 static 또는 dynamic 이 될수있으며, dynamic은 서비스가 동적으로 변경될때 컴포넌트의 implementation이 이에 동적으로 대응이 가능한지를 말해줍니다. static이라면 DS가 해당 서비스가 변경될때마다 컴포넌트를 비활성화하고 새로운 인스턴스를 만들어야 합니다. 이건 매우 부하가 많은 접근방법이며, 여러분께선 가능하면 동적 변경을 지원하도록 컴포넌트 클래스를 코딩하는것이 좋습니다. 아쉽게도 static 이 기본값이기 때문에 dynamic 을 명시적으로 지정해주어야 합니다.
그럼 지금까지 선택 단일 및 필수 단일 관계를 살펴보았습니다. 그럼 복수관계는 어떨까요 ? 서비스 레지스트리에는 한개이상의 Runnable 이 등록되어있을구 있구, 그중 한가지와 연결(bind)할때 어떤것을 선택하는지는 임의로 결정됩니다. 아마도 우린 현재 등록된 모든 Runnable
을 실행하도록 하는 “runall” 명령을 개발할 수도 있습니다.
만약 우리가 cardinality
를 “0..n” 으로 바꾸면 무슨일이 벌어질까요 ? 아마 거의 동작할 겁니다. 다만 setRunnalbe
을 한번 호출하는것 대신, DS 는 레지스트리의 Runnable 인스턴스들의 setRunnable
을 한번씩 호출할 것입니다. 문제는 우리가 만든 클래스가 혼란스러울수 있다는 것입니다. 한개의 Runnable 필드를 세팅하는것보다 Runnable 들을 컬렉션에 넣어두는것이 필요합니다. 아래에 약간 수정된 버전의 클래스가 있습니다. 아래 코드를 복사해서 프로젝트의 src 폴더에 붙여넣기 하실수 있습니다.
package org.example.ds; import java.util.*; import org.eclipse.osgi.framework.console.CommandInterpreter; import org.eclipse.osgi.framework.console.CommandProvider; public class SampleCommandProvider2 implements CommandProvider { private Listrunnables = Collections.synchronizedList(new ArrayList ()); public void addRunnable(Runnable r) { runnables.add(r); } public void removeRunnable(Runnable r) { runnables.remove(r); } public void _runall(CommandInterpreter ci) { synchronized(runnables) { for(Runnable r : runnables) { r.run(); } } } public String getHelp() { return "\trunall - Run all registered Runnables"; } }
자 이제 OSGI-INF/commandprovider2.xml
를 만들고 아래를 복사합니다.
<?xml version="1.0"?> <component name="commandprovider2"> <implementation class="org.example.ds.SampleCommandProvider2"/> <service> <provide interface="org.eclipse.osgi.framework.console.CommandProvider"/> </service> <reference name="RUNNABLE" interface="java.lang.Runnable" bind="addRunnable" unbind="removeRunnable" cardinality="0..n" policy="dynamic"/> </component>
그리고 마지막으로 이 파일을 manifest 의 Service-Component
헤더에 추가합니다. 이렇게 보일것입니다.
Service-Component: OSGI-INF/commandprovider1.xml, OSGI-INF/commandprovider2.xml
이 선언은 우리가 bind,unbind 메소드의 이름을 변경하고, cardinality를 “0..n”으로 변경했다는것을 제외하면, 이전과 거의 비슷합니다. 실험삼아 몇개의 Runnable 서비스들을 등록하고 runall 명령이 동작하는지를 살펴보세요. 다음으로 만약 우리가 cardinality 를 “1..n”으로 바꾸면 어떻게 될까요 ? 어떻게 될지를 한번 생각해보고 확인해 보세요.
레슨은 여기까지 입니다. OSGi Alliance Community Event 가 다음주에 독일 Munich 에서 열린다는것을 잊지마세요. 아직 등록하기에 늦지 않았습니다. 거기서 봅시다! (역주: 역시 지나버린 이벤트입니다만.. 그냥 번역해 둡니다.)