WatchTower, Observe API Calls in browser

도입

adibfara/WatchTower(https://github.com/adibfara/WatchTower)는 브라우저 내 에서 OKHttp으로 인한 통신에 대한 요청 / 결과 데이터 및 헤더 등을 볼 수 있는 라이브러리로, 2019년 5월 24일에 첫 버전이 릴리즈된 라이브러리이다.

지금까지 OKHttp으로 인한 통신을 앱에서 검출하려면 주로 facebook/stetho(https://facebook.github.io/stetho/) 를 사용했으나, stetho의 기반은 Chrome developer tools에 작동한다는 점에서 이 WatchTower는 그러한 제약을 가지지 않고, 앱 내에서 NanoHTTPd를 사용하여 웹 서버를 열고 자료를 볼 수 있게 하는 것에 큰 차이점을 가진다.

또한 차후 확장하여 액티비티 내에서 볼 수 있게 하는 점도 가능하니, 기능은 Stetho에 비해 매우 빈약하지만 (Stetho가 네트워크 인터셉터 외에도 뷰 구조나 덤프 등을 제공하기 때문에) 네트워크 인터셉트 기능으로 한정하면 WatchTower가 더 이점을 가진다고 볼 수 있다.

당연하게도 Retrofit 또한 OKHttp가 기반이므로 Retrofit를 사용해도 위 기능을 전부 활용할 수 있다.

따라서 이 글에서는 기본적인 WatchTower의 사용법을 소개한다.

사용법

모듈의 gradle에 아래 종속성을 추가한다.

implementation 'com.snakyapps.watchtower:core:1.0.0'
debugImplementation 'com.snakyapps.watchtower:interceptor-okhttp:1.0.0'
releaseImplementation 'com.snakyapps.watchtower:interceptor-okhttp-no-op:1.0.0'

debugImplementation, releaseImplementation으로 나눠지는데 이는 디버그 빌드에서만 해당 행동을 할 수 있게 종속성 분야에서 강제하는 것이다. 따라서 별도로 BuildConfig.DEBUG로 판단할 필요가 없어진다.

1.0.0 문서에 따르면, 안드로이드 환경에서는 Service에서 WatchTower를 시작하는 것이 권장된다고 되어있어, Service 객체를 생성한다.

class WatchTowerService : Service() {

   override fun onBind(intent: Intent?): IBinder? {
       return null
  }

   override fun onCreate() {
       super.onCreate()
       val port = Config.confg.watchTowerPort
       WatchTower.start(WebWatchTowerObserver(port = port))

       Timber.d("WatchTower is started. URL: http://localhost:$port")
  }

   override fun onDestroy() {
       super.onDestroy()
       WatchTower.shutDown()
  }
}
<service
           android:name=".module.watch.WatchTowerService"
           android:enabled="true" />

위와 같이 작성하고 Manifest에 작성하였으면, 그 다음에는 OKHttp를 설정할 때에 Interceptor를 추가하면 된다.

현재 구축된 기반의 경우 Dagger의 Multibinding 기능을 이용하여 Set<Interceptor>LoggingInterceptor 순서로 추가하게 되어 있으므로, LoggingInterceptor 뒤에 WatchTower 자체의 Interceptor를 추가한다.

@Provides
fun provideClient(interceptors: Set<@JvmSuppressWildcards Interceptor>,
                     @Named("loginterceptor") logsInterceptor: Interceptor): OkHttpClient {
   val builder = OkHttpClient().newBuilder()
   builder.readTimeout(Config.config.timeout.toLong(), TimeUnit.MILLISECONDS)
   builder.connectTimeout(Config.config.connectTimeout.toLong(), TimeUnit.MILLISECONDS)
   builder.retryOnConnectionFailure(Config.config.retryOnConnectionFailure)
   if (interceptors.isNotEmpty()) {
       interceptors.forEach {
           builder.addInterceptor(it)
      }
  }

   if (!Config.config.notUseLogInterceptor) {
       builder.addInterceptor(logsInterceptor)
  }

   if (Config.config.useWatchTower) {
       builder.addInterceptor(WatchTowerInterceptor())
  }

       return builder.build()
}

마지막으로 Activity 에서 서비스를 시작하거나 중지하면 작동하게 된다.

이제, 앱을 빌드하게 되면 기기의 브라우저에서는 http://localhost:8085 를, PC에서는 http://기기의 로컬 주소:8085 를 입력하면 접속이 되고, OKHttp로 통해 통신이 진행될 경우 맨 위의 사진처럼 로그가 나오게 된다. 기기의 로컬 주소는 기기에서 https://www.whatismybrowser.com/detect/what-is-my-local-ip-address 에 접속하면 나온다.

마무리

간단한 사용법을 가지면서도, 편리한 사용법을 가져서 누구나 쉽게 적용할 수 있다는 포인트가 제일 강점이다. 구현하는 데에도 큰 시간이 들어가지 않으므로 적용해서 사용하면 큰 편리함을 얻을 수 있을 것 같다.

OkHttp Interceptor Multibindings with Dagger (ENG)

Introduction

Looking at the structure of the app I am currently using, the sub-project depends on the core module.
In the world, this structure called this a ‘Multi-Module projects’
Of course, the ‘Core’ module managed by the ‘JFrog artifactory’ server.

One reason for using Multi-Module projects structure is there are many boilerplate codes for an individual project.
Another reason is code can be recyclable with not depend on the specifics of individual projects.

So that, Dagger also managed both modules such as ‘Core’ module and sub-projects.
In ‘Core’ module, they have ‘@Module’ class, In sub-projects, they have project-specific ‘@Module’ class and ‘@Component’ class.

The thing with I am, ‘Retrofit’ or ‘OKHttpClient’ for networking module managed with ‘Core’ module, but Interceptor of OKHttpClient can have specifics of projects. Also, Interceptor might be 1 or more.

Solve this problem, Make interceptors into collections and inject when making an instance using ‘Multibinding’ feature of Dagger.

The language of example code is Kotlin and Java.

What is Multibindings?

First, we should explain about ‘Multibinding’ in Dagger.

Multibindings is a feature that has the power to collect the same type of instance with binding in a different module.
In Dagger, process with collections did not depend on individual bindings.

Dagger provides two kinds of Multibindings, one is ‘@IntoSet’ annotation, other is ‘@IntroMap’ annotation.

Two ‘Multibindings’ annotations provide a different solution for making collections.
One of the annotations, ‘@IntoSet’ provides the collection of instances with ‘Set’.
Another ‘@IntoMap’ provides the collection of instances with ‘Map<Class<*>, Provider<T>>’.

Today, I’ll use ‘@IntoSet’ annotation.

Make the Interceptor

First, I need Interceptor that provides into Dagger.

    class TestInterceptor : Interceptor {
        @Throws(IOException::class)
        override fun intercept(chain: Interceptor.Chain): Response? {
            val original = chain.request()
            val originalHttpUrl = original.url()
            val requestBuilder = original.newBuilder()
                .url(originalHttpUrl.newBuilder().build())
            return chain.proceed(requestBuilder.build())
        }
    }

Next, Provide Interceptor into ‘@Module’ annotated class.

    @Module
    public class AppInterceptorModule {
        @Provides
        @IntoSet
        public Interceptor provideTestInterceptor() {
            return new TestInterceptor();
        }
    }

The basic form of providing is same as other providers, except ‘@IntoSet’ annotations.
If you want to use ‘Multibindings’ feature in another class, You can set ‘Qualifier‘ annotation.

Inject when making an instance of OKHttpClient

    @Provides
        fun provideClient(interceptors: Set<@JvmSuppressWildcards Interceptor>): OkHttpClient {
            val builder = OkHttpClient().newBuilder()
            builder.readTimeout(Config.timeout.toLong(), TimeUnit.MILLISECONDS)
            builder.connectTimeout(Config.connectTimeout.toLong(), TimeUnit.MILLISECONDS)
            if (interceptors.isNotEmpty()) {
                interceptors.forEach {
                    builder.addInterceptor(it)
                }
            }
            return builder.build()
    }

Looking at the code of above, they have ‘Set’ parameters with named with ‘interceptors’.
Dagger will inject their collection of Interceptor into this parameter.

You can check the empty state of Set and add Interceptor into OKHttpClient using ‘builder.addInterceptor()’.

In this code, You might notice the ‘@JvmSuppressWildcards’ annotation in Type parameter of ‘Set’.
‘@JvmSuppressWildcards’ annotation is Kotlin annotations for interop with Java.

In default, Kotlin compiler converts ‘Set<Interceptor>’ into ‘Set<? extends Interceptor>’ type while Dagger needs `Set<Interceptor>. It can be a compile-time error.
In addition, If the type of Type parameters has a final modifier such as String, Kotlin compiler doesn’t generate wildcard.

Solve this problem, we can attach ‘@JvmSuppressWildcards’ to not convert to ‘Set’.

Looking generated code

When compiler builds success, we can check generated code in DaggerAppComponent class.

First, Dagger generated fields named ‘setOfInterceptorProvider’. this field contains an instance of Set.

    private Provider<Set<Interceptor>> setOfInterceptorProvider;

Here is code to assign ‘setOfInterceptorProvider’ field.

    this.setOfInterceptorProvider =
            SetFactory.<Interceptor>builder(2, 0)
                .addProvider(provideTestInterceptorProvider)
                .addProvider((Provider) logInterceptorProvider)
                .build();

In test projects, they have an interceptor in the ‘Core’ module and interceptor in sub-projects. so Dagger assigns the size of Set to 2.

Here is code to use ‘setOfInterceptorProvider’ field.

     this.provideClientProvider =
            BaseProvidesModule_ProvideClientFactory.create(
                builder.baseProvidesModule, setOfInterceptorProvider);

Then, Class named ‘BaseProvideModule_ProvideClientFactory’ uses ‘setOfInterceptorProvider’ to process binding into ‘provideClient’ methods.

public final class BaseProvidesModule_ProvideClientFactory implements Factory<OkHttpClient> {
    private final BaseProvidesModule module;
    private final Provider<Set<Interceptor>> interceptorsProvider;

    public BaseProvidesModule_ProvideClientFactory(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        this.module = module;
        this.interceptorsProvider = interceptorsProvider;
    }

    public OkHttpClient get() {
        return provideInstance(this.module, this.interceptorsProvider);
    }

    public static OkHttpClient provideInstance(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return proxyProvideClient(module, (Set)interceptorsProvider.get());
    }

    public static BaseProvidesModule_ProvideClientFactory create(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return new BaseProvidesModule_ProvideClientFactory(module, interceptorsProvider);
    }

    public static OkHttpClient proxyProvideClient(BaseProvidesModule instance, Set<Interceptor> interceptors) {
        return (OkHttpClient)Preconditions.checkNotNull(instance.provideClient(interceptors), "Cannot return null from a [email protected] @Provides method");
    }
}

Conclusion

‘Multibindings’ is a special feature to process case that provides similar types of ‘Dagger’ or ‘Guice’ that based on code generations.

Also, ‘Multi-Module projects’ structure makes ‘Multibindings’ feature more special that they can help to manage the ‘Core’ module without depends on specifics of the project and manage sub-projects only have the project-specific code.

OkHttp Interceptor Multibindings with Dagger

도입

현재 사용하고 있는 앱의 구조를 살펴보면, 하위 프로젝트가 코어 모듈을 의존하고 있는 형태로, 흔히 말하는 Multi-Module projects이다. 물론, 코어 모듈은 현재 Artifactory 로 관리되어 버전 관리도 되고 있다.

이렇게 구성한 이유로는 1. 의외로 프로젝트마다 들어가는 중복 코드가 많고, 2. 프로젝트 특성에 종속되지 않는 코드여야 재활용이 가능하기 때문이다.

그런 이유로 Dagger 또한 코어 모듈과 하위 프로젝트에서 관리되는데, 코어 모듈에서는 Base~Module 만 가지고 있고, 하위 프로젝트에서 App~Module 와 Component 클래스를 가지고 있다.

문제는, Retrofit, OKHttpClient 등의 객체를 Base~Module, 즉 코어 모듈에서 관리하고 있는데 프로젝트마다 필요한 Interceptor 는 하위 프로젝트에서 들어간다는 점이다. 또, 이 Interceptor가 1개가 아닌 여러개가 될 수 있다.

이를 해결하기 위해 이 글에서는 Dagger 의 Multibindings 기능을 이용하여 Provide 된 Interceptor 등을 Set<Interceptor> 로 받아서 최종적으로 OKHttpClient를 만들 때 추가하려 한다.

언어는 평소대로 Kotlin, 사용부는 Java이다.

Multibindings란?

먼저, Multibindings에 대한 설명이 필요할 것 같다.

Multibindings 는 서로 다른 모듈에 객체가 바인딩되어 있어도 하나의 컬렉션으로서 객체에 바인딩할 수 있는 기능이다. Dagger에서는 이 컬렉션을 분석하여 개개인의 바인딩에 의존하지 않고 처리를 해준다.

종류는 두가지로, @IntoSet@IntoMap 가 있는데, 각각 이름과 같이 Set<T> 와 Map<Class<*>, Provider<T>> 를 제공한다.

이 중에서 오늘 사용할 것은 @IntoSet 이다.

제공될 Interceptor 제작

먼저, Dagger 에 Interceptor 를 제공할 Interceptor 를 만드는데, 예제로 할 주제가 명확하게 떠오르지 않아 원본을 바로 반환하는 기본 형태의 Interceptor 를 만든다.

class TestInterceptor : Interceptor {
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response? {
        val original = chain.request()
        val originalHttpUrl = original.url()
        val requestBuilder = original.newBuilder()
            .url(originalHttpUrl.newBuilder().build())

        return chain.proceed(requestBuilder.build())
    }
}

그 다음, 제작한 Interceptor 를 Module 에 제공한다.

@Module
public class AppInterceptorModule {

    @Provides
    @IntoSet
    public Interceptor provideTestInterceptor() {
        return new TestInterceptor();
    }
}

평소와 같이 Provide를 하되 @IntoSet 라는 어노테이션을 추가적으로 부착하면 된다. 만일 Multibinding 기능을 다른 기능에도 활용하고 싶으면 Qualifier 어노테이션을 부착하면 된다.

필요한 객체에 제공하기

@Provides
    fun provideClient(interceptors: Set<@JvmSuppressWildcards Interceptor>): OkHttpClient {
        val builder = OkHttpClient().newBuilder()
        builder.readTimeout(Config.timeout.toLong(), TimeUnit.MILLISECONDS)
        builder.connectTimeout(Config.connectTimeout.toLong(), TimeUnit.MILLISECONDS)
        if (interceptors.isNotEmpty()) {
            interceptors.forEach {
                builder.addInterceptor(it)
            }
        }
        return builder.build()
}

자세히 보면 파라미터에 interceptors: Set<Interceptor> 가 보이는데, 이 쪽으로 Multibindings 으로 구성된 컬렉션이 추가된다. Set이므로 비어있지 않으면 간단히 forEach 로 builder 에 Interceptor를 추가하면 된다.

여기서 @JvmSuppressWildcards 란 어노테이션이 있는데, 코틀린 컴파일러는 기본적으로 Set<Interceptor> 를 Set<? extends Interceptor> 로 변환한다. 이 때, 대거가 제공하는 컬렉션 객체는 Set<Interceptor> 이므로 Set<? extends Interceptor> 를 찾을 수 없다면서 오류가 나온다.

단, 해당 타입 파라미터가 final 이면 Wildcards 가 생성되지 않는데, Set<String> 가 그렇다.

이 때에는 @JvmSuppressWildcards 를 붙여 Set<? extends Interceptor> 가 아닌 Set<Interceptor> 로 구성되게 하면 된다.

생성된 코드 살펴보기

위 작업까지 마치고 빌드가 성공했을 경우, DaggerAppComponent에 관련 부분이 생성된 것을 확인할 수 있다.

먼저, setOfInterceptorProvider 라는 필드가 생성되는데, 이 곳이 Set<Interceptor> 를 보관하는 곳이다.

private Provider<Set<Interceptor>> setOfInterceptorProvider;

값을 할당하는 부분은 다음과 같다.

this.setOfInterceptorProvider =
        SetFactory.<Interceptor>builder(2, 0)
            .addProvider(provideTestInterceptorProvider)
            .addProvider((Provider) logInterceptorProvider)
            .build();

테스트에 사용된 프로젝트에서는 코어 모듈에 Interceptor 가 1개, 제작한 Interceptor 1개로 총 두 개가 선언되어있어 Set<Interceptor>를 생성하는 SetFactory.Builder 에 2가 기재되있는 것을 확인할 수 있다.

만들어진 setOfInterceptorProvider 를 사용하는 곳은 바로 밑에 나온다.

 this.provideClientProvider =
        BaseProvidesModule_ProvideClientFactory.create(
            builder.baseProvidesModule, setOfInterceptorProvider);

그리고 해당 BaseProvidesModule_ProvideClientFactory 에서 setOfInterceptorProvider 의 값을 얻어 바인딩을 진행한다.

public final class BaseProvidesModule_ProvideClientFactory implements Factory<OkHttpClient> {
    private final BaseProvidesModule module;
    private final Provider<Set<Interceptor>> interceptorsProvider;

    public BaseProvidesModule_ProvideClientFactory(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        this.module = module;
        this.interceptorsProvider = interceptorsProvider;
    }

    public OkHttpClient get() {
        return provideInstance(this.module, this.interceptorsProvider);
    }

    public static OkHttpClient provideInstance(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return proxyProvideClient(module, (Set)interceptorsProvider.get());
    }

    public static BaseProvidesModule_ProvideClientFactory create(BaseProvidesModule module, Provider<Set<Interceptor>> interceptorsProvider) {
        return new BaseProvidesModule_ProvideClientFactory(module, interceptorsProvider);
    }

    public static OkHttpClient proxyProvideClient(BaseProvidesModule instance, Set<Interceptor> interceptors) {
        return (OkHttpClient)Preconditions.checkNotNull(instance.provideClient(interceptors), "Cannot return null from a [email protected] @Provides method");
    }
}

정리

Multibindings 기능은 코드를 생성하는 것에 기반을 두는 Dagger 나 Guice가 가지는 특별한 기능으로 여러 개의 비슷한 객체가 제공될 수 있는 경우에 좀 더 쉽게 처리할 수 있게 해준다.

특히, 코어 모듈과 하위 프로젝트를 따로 관리하는 입장으로서는 코어 모듈은 프로젝트 종속되지 않게 관리를, 하위 프로젝트에서는 프로젝트 특성을 띈 코드만 가지게 할 수 있다는 점이 이 Multibindings 를 한 층 더 특별히 만들어 주는 것 같다.