Jersey 2.x에 내장된 의존성 주입 기능 사용하기
Jersey는 Java의 REST 웹 서비스 표준인 JAX-RS의 레퍼런스 구현체입니다. Spring의 무거움이나 서블릿을 직접 쓸때의 번거로움이 덜해서 자주 사용하고 있습니다. 특히 Jersey와 몇몇 라이브러리의 통합을 제공하는 Dropwizard를 쓰면 REST 서비스를 빠르게 만들 수 있습니다.
Jersey 1.x에서는 Guice와의 연동을 지원했는데 2.0 이후에는 HK2라는 의존성 주입 프레임워크를 내장하게 되면서 다른 의존성 주입 라이브러리와는 통합이 쉽지 않게 되었습니다. Dropwizard도 0.8.0부터는 Jersey 2.x를 사용하고 있기 때문에 뭔가 대책이 필요했습니다. jersey2-guice 같은 해결책도 있지만 그냥 HK2를 그대로 써도 되지 않을까 해서 조사를 해보았습니다.
리소스에 의존성 주입
TwitterClient
라는 인터페이스가 있다고 가정해봅시다. 리소스 클래스의 생성자에 javax.inject.Inject 어노테이션을 붙여서 객체를 주입할 수 있습니다.
@Path("/tweets")
public class TweetsResource {
private final TwitterClient twitterClient;
@Inject
public TweetsResource(TwitterClient twitterClient) {
this.twitterClient = twitterClient;
}
// ...
}
Guice와 별로 다를 것은 없습니다. 해보진 않았지만 아마 setter/field 인젝션도 가능할겁니다.
바인딩 설정
Jersey에서는 일반적으로 ResourceConfig 객체에 리소스를 등록합니다. (Dropwizard에서는 Environment#jersey()
를 통해 얻을 수 있습니다.)
ResourceConfig config = new ResourceConfig();
config.register(TweetsResource.class);
마찬가지로 바인딩 설정 또한 ResourceConfig
에 등록할 수 있습니다. Guice의 AbstractModule
과 유사한 AbstractBinder를 상속받고 바인딩 DSL을 사용해서 설정할 수 있습니다.
config.register(new AbstractBinder() {
// ...
});
HK2의 바인딩 DSL도 Guice와 상당히 비슷한데, HK2는 구현이 앞에 오고 인터페이스가 뒤에 온다는 차이가 있습니다. (HK2의 용어로는 인터페이스 = contract, 구현 = service입니다.)
bind(new TwitterClientImpl()).to(TwitterClient.class)
: TwitterClientImpl 인스턴스를 TwitterClient 인터페이스에 바인딩bind(TwitterClientImpl.class).to(TwitterClient.class)
: TwitterClient 인터페이스의 구현 클래스로 TwitterClientImpl을 바인딩 (주입 시마다 새로 인스턴스 생성)bind(TwitterClientImpl.class).to(TwitterClient.class).in(Singleton.class)
: TwitterClient 인터페이스의 구현 클래스로 TwitterClientImpl을 바인딩 (주입 시 하나의 싱글턴 인스턴스 공유)bindAsContract(TwitterClientImpl.class)
:bind(TwitterClientImpl.class).to(TwitterClientImpl.class)
와 같습니다.
그 밖에도 bindFactory 같은 것들이 제공되는데 HK2 문서가 그다지 친절하지 않아서 완전히 파악하지는 못했지만 웬만하면 문제는 없을 것 같습니다.
Mock 객체 주입해서 테스트
Jersey 테스트 프레임워크와 적당한 Mock 라이브러리를 사용해서 TwitterClient의 Mock 객체를 주입하는 테스트를 다음과 같이 작성할 수 있습니다.
public class HK2Demo extends JerseyTest {
TwitterClient twitterClient;
@Override protected Application configure() {
twitterClient = mock(TwitterClient.class);
ResourceConfig config = new ResourceConfig();
config.register(TweetsResource.class);
config.register(new AbstractBinder() {
@Override protected void configure() {
bind(twitterClient).to(TwitterClient.class);
}
});
return config;
}
@Test public void testPost() {
target("/tweets").request()
.post(Entity.form(new Form("message", "Hi!")));
verify(twitterClient).tweet("Hi!");
}
}