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 non-@Nullable @Provides method");
    }
}

정리

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

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

add Whitelist in Doze mode

최근에 알람 앱을 개발할 일이 있었는데, Doze 모드로 들어가면 알람이 잘 안울리는 일이 있었다. 이 때 사용자에게 ‘배터리 최적화 예외’로 추가해달라고 요청을 해야되는데, 이 글에서는 간단하게 해당 방법을 알아보려 한다.

권한 추가

필요한 권한은 <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />로 위험하지 않은 권한이니 AndroidManifest.xml에만 적어주면 된다.

코드 구현

Doze mode는 6.0 이상에서 구현되었으므로 그 이하에서는 체크할 필요가 없고, 그 전에 이미 배터리 예외 모드에 추가되어 있으면 요청할 필요가 없어진다.

이를 반영한 코드는 다음과 같다.

@SuppressLint({"BatteryLife", "InlinedApi"})
    private void checkBatteryOptimization(Consumer<Boolean> callback) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { // 버전 체크
            Log.d(TAG, "checkBatteryOptimization: The version is too low to be checked.");
            callback.accept(true);
            return;
        }

        String packageName = getApplication().getPackageName();

        // since REQUEST_IGNORE_BATTERY_OPTIMIZATIONS is **not** dangerous permission,
        // but we need to check that app has `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
        if (PackageManager.PERMISSION_GRANTED != getApplication().getPackageManager()
                .checkPermission(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
                        getApplication().getPackageName())) { // 권한 체크
            Log.d(TAG, "checkBatteryOptimization: application hasn't REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission");
            return;
        }

        PowerManager powerManager = (PowerManager) getApplication().getSystemService(Context.POWER_SERVICE);
        boolean ignoringBatteryOptimizations = powerManager.isIgnoringBatteryOptimizations(packageName);
        if (ignoringBatteryOptimizations) { // 예외사항에 이미 추가되었는지 확인
            Log.d(TAG, "checkBatteryOptimization: Already ignored Battery Optimizations.");
            callback.accept(true);
            return;
        }

        Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
        intent.setData(Uri.parse(String.format("package:%s", packageName)));
        startActivity(intent);
    }

마지막으로 사용하는 곳에서 체크하면 된다.

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
public void onResume() {
    checkBatteryOptimization(result -> DailyAlarmAnalyze.schedule());
}

결과

Reuse code by Modularizing with Dagger 2 in Android

도입

흔히 코딩의 효율성을 높이는 방법으로 ‘재사용성’ 이 많이 강조된다. 이 이야기는 그렇게까지 새로운 이야기는 아니고, 예전부터 재사용성을 위해 클래스화를 하여 여러 곳에서 사용할 수 있게 하는 코딩 방법은 널리 사용되고 있었다.

하지만, 의존성 주입(Dependency Injection) 이 도입된 이후부터 이러한 클래스는 하나의 ‘의존성’ 으로서 DI 프레임워크 등에 주입되고 다른 곳에서 새로운 인스턴스를 생성할 필요 없이 외부의 한 곳에서 관리할 수 있게 되었다.

이 글에서는 안드로이드에서 Dagger 2 라는 Google의 의존성 주입 라이브러리를 통해 앱의 프로세스 로직 어디서나 사용할 수 있는 TextToSpeech에 대한 Singleton 클래스를 만들고, 다양한 곳에서 사용하려 한다.

참고로 언어는 평소대로 Kotlin을 사용했다. 단 사용하는 부분은 호환성을 위해 Java를 사용했다.

인터페이스 설계

먼저, 제작할 클래스에서 사용될 public methods에 대해 정의한다. 이는 다음에 나올 이야기와도 연결이 되는데, 제작할 클래스가 다른 클래스에 의존할 수 있기 때문이다.

상기했던 ‘TextToSpeech’ 에 대해 구현해야 될 기능과 public method는 다음과 같다.

  • fun speak(msg: String)
    • msg가 비어있거나, TextToSpeech가 제대로 Initialize 되지 않았을 경우에는 실행하지 않고 바로 반환한다.
    • 사용자 설정에서 ‘음성 안내’ 가 켜져있을 때만 작업을 진행한다.
    • TextToSpeech로 통해 재생할 메세지에 대한 고유 키인 UtteranceID 를 생성하고 이를 TextToSpeech 클래스에 반환한다. 이 때, SDK 버전이 21 미만일 경우에는 HashMap<String, String>TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID 를 Key로 하여 반환하고, 21 이상일 경우에는 TextToSpeech.speak() 메서드의 4번째 파라미터에 값을 반환한다.
  • override fun onInit(status: Int)
    • TextToSpeech 클래스의 생성자 파라미터 중 두 번째 파라미터인 TextToSpeech.OnInitListener 인터페이스의 메서드로 TextToSpeech의 엔진 초기화 상태를 알려주는 메서드이다.
    • status 가 TextToSpeech.SUCCESS 일 때 초기화가 성공했다는 플래그로 전환하고, TextToSpeech 의 설정을 변경한다. 여기에서는 대상 언어를 한국어로 하고 TTS 시작, 완료, 에러에 대해 알 수 있는 UtteranceProgressListener 를 설정한다.

이 두 가지 메서드와 기능으로 비롯해, 제작할 클래스가 필요로 하는 클래스(의존성)는 다음과 같다.

  • Application: TextToSpeech 생성자 파라미터 중 첫번째 파라미터에 Context를 반환해야 한다.
  • PreferenceRepository: 사용자 설정을 불러오는 클래스

그리고 제작할 클래스에 필요한 필드는 다음과 같다.

  • application: Application
  • preferenceRepository: PreferenceRepository
  • textToSpeech: TextToSpeech
  • isInitialize: Boolean

applicaiton, preferenceRepository 의 경우 외부에서 주입될 것이므로 필드 생성자로 할당하고, 나머지 두 개는 내부 상태를 관리할 것이므로 일반적인 필드로 구성한다.

마지막으로, 제작할 클래스의 이름은 ‘TTSPlayer’ 라 짓고, 다음부터 실제 구현에 들어갈 것이다.

클래스 제작

먼저, 필요한 필드와 public methods를 전부 정의한다.

class TTSPlayer constructor(val application: MainApplication,
                            val preferenceRepository: PreferenceRepository) : TextToSpeech.OnInitListener {
    private val textToSpeech: TextToSpeech = TextToSpeech(application, this)
    private var isInitialize: Boolean = false

    override fun onInit(status: Int) {
    
    }

    fun speak(msg: String) {
      
    }

    companion object {
        @JvmField
        val TAG = TTSPlayer::class.java.simpleName
    }
}

이제 상기 메서드의 기능을 구현하면 되다.

onInit(status: Int)

onInit의 기능을 다시 살펴보면 다음과 같다.

  • TextToSpeech 클래스의 생성자 파라미터 중 두 번째 파라미터인 TextToSpeech.OnInitListener 인터페이스의 메서드로 TextToSpeech의 엔진 초기화 상태를 알려주는 메서드이다.
  • status 가 TextToSpeech.SUCCESS 일 때 초기화가 성공했다는 플래그로 전환하고, TextToSpeech 의 설정을 변경한다. 여기에서는 대상 언어를 한국어로 하고 TTS 시작, 완료, 에러에 대해 알 수 있는 UtteranceProgressListener 를 설정한다.

이를 코드로 나타내면 다음과 같을 것이다.

override fun onInit(status: Int) {
        if (status == TextToSpeech.SUCCESS) {
            isInitialize = true
            textToSpeech.language = Locale.KOREA

            textToSpeech.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
                override fun onDone(utteranceId: String?) {
                    Log.d(TAG, "onDone: done with $utteranceId")
                }

                override fun onError(utteranceId: String?, errorCode: Int) {
                    super.onError(utteranceId, errorCode)
                    Log.d(TAG, "onError: error with $utteranceId - code $errorCode")
                }

                override fun onError(utteranceId: String?) {
                    Log.d(TAG, "onError: error with $utteranceId")
                }

                override fun onStart(utteranceId: String?) {
                    Log.d(TAG, "onStart: start with $utteranceId")
                }
            })
        }
    }

먼저, status 가 TextToSpeech.SUCCESS 값을 나타내면, 초기화 플래그 필드인 isInitialize를 true 로 만들고, 언어를 한국어로 설정한다.

다음에, UtteranceProgressListener 라는 추상 클래스를 익명함수로서 TextToSpeech에 구현하는데, API 21 기준으로는 onError(utteranceId: String?, errorCode: Int) 가 필요하고 그 미만으로는 onError(utteranceId: String?) 가 필요하다.

그러므로 양 쪽 API 버전 대응을 위해 onError 두 개의 메서드 둘 다 오버라이딩하고, 각각의 메서드에 로그 메세지를 출력하도록 한다.

speak(msg: String)

speak의 기능을 다시 살펴보면 다음과 같다.

  • msg가 비어있거나, TextToSpeech가 제대로 Initialize 되지 않았을 경우에는 실행하지 않고 바로 반환한다.
  • 사용자 설정에서 ‘음성 안내’ 가 켜져있을 때만 작업을 진행한다.
  • TextToSpeech로 통해 재생할 메세지에 대한 고유 키인 UtteranceID 를 생성하고 이를 TextToSpeech 클래스에 반환한다. 이 때, SDK 버전이 21 미만일 경우에는 HashMap<String, String>TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID 를 Key로 하여 반환하고, 21 이상일 경우에는 TextToSpeech.speak() 메서드의 4번째 파라미터에 값을 반환한다.

먼저, 첫 번째 기능과 두 번째 기능의 반환 기능은 쉽게 구현이 가능하다.

fun speak(msg: String) {
        if (msg.isEmpty()) return
        if (!isInitialize) {
            Log.d(TAG, "speak: initialize failed")
            return
        }

        if (!preferenceRepository.isCodeAuthed) {
            Log.d(TAG, "speak: user doesn't authed")
            return;
        }
    }

그 다음 세 번째 기능인 UtteranceID는 각 메세지에 대해 고유적이어야 하므로 UUID.randomUUID() 를 사용한다. 해당 API가 생성하는 UUID는 버전 4로 RFC4122 에 맞춰 랜덤으로 생성되는 문자열이다.

그리고, 상기한 API 버전에 따른 분기 처리를 진행한다.

fun speak(msg: String) {
        if (msg.isEmpty()) return
        if (!isInitialize) {
            Log.d(TAG, "speak: initialize failed")
            return
        }

        if (!preferenceRepository.isCodeAuthed) {
            Log.d(TAG, "speak: user doesn't authed")
            return;
        }

        val utteranceId = UUID.randomUUID().toString()
        val map = hashMapOf<String, String>()
        map[TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID] = utteranceId

        if (Build.VERSION.SDK_INT >= 21) {
            textToSpeech.speak(msg, TextToSpeech.QUEUE_FLUSH, null, utteranceId)
        } else {
            textToSpeech.speak(msg, TextToSpeech.QUEUE_FLUSH, map)
        }
    }

Dagger 2에 주입하기

이렇게 해서, 두 개의 메서드에 대한 구현이 완료되었다. Dagger 2에 주입되기 위해서는 모듈 이라고 하는 클래스에 선언할 필요가 있는데, 이 글에서는 모듈의 정의나 모듈을 선언하는 컴포넌트 클래스에 대해서는 설명하지 않는다.

단순히 만든 클래스를 Dagger에 주입하려면 다음과 같은 코드를 모듈에 정의하면 된다.

@Provides
    TTSPlayer provideTTSPlayer(MainApplication application, PreferenceRepository preferenceRepository) {
        return new TTSPlayer(application, preferenceRepository);
    }

그런데, 이번에 제작한 TTSPlayer의 경우에는 여러 번 인스턴스가 생성되면 안 되기 때문에, 싱글톤으로서의 선언이 필요하다.

이 때 사용하는 어노테이션은 @Singleton 으로 해당 클래스와 모듈에 정의된 메서드에 부착하면 된다.

@Singleton
class TTSPlayer @Inject constructor(val application: MainApplication,
                                    val preferenceRepository: PreferenceRepository) : TextToSpeech.OnInitListener {
    @Provides
    @Singleton
    TTSPlayer provideTTSPlayer(MainApplication application, PreferenceRepository preferenceRepository) {
        return new TTSPlayer(application, preferenceRepository);
    }

사용하기

제작한 TTSPlayer를 사용하기 위해서는 필드나 생성자로 통해 TTSPlayer를 주입받고 사용하면 된다. 이 때 사용되는 어노테이션은 @Inject 로 생성자로 통해 주입받을 때에는 생성자 메서드에 부착을, 필드로 통해 주입받을 때에는 필드 하나마다 부착해주면 된다.

@InjectViewModel
public class CodeAuthViewModel extends BaseViewModel {
    @Inject PreferenceRepository mPreferenceRepository;
    @Inject TTSPlayer mTTSPlayer;

    @Inject
    public CodeAuthViewModel(@NonNull MainApplication application) {
        super(application);
    }

    ...
    
    public void checkCode(String result) {
        mPreferenceRepository.setCodeAuthed(true);
        mTTSPlayer.speak("인증되었습니다.");
        ...
    }
}

위 예제에서는 CodeAuthViewModel 이라는 뷰모델 클래스에서 사용하므로 필드로 통해 TTSPlayer를 주입받고, checkCode 라는 메서드에서 TTSPlayer.speak(msg) 코드를 사용했다.

생성된 클래스 살펴보기

위 예제를 보면 그 어디에도 mTTSPlayer 라는 필드에 의존성을 주입하는 부분이 없는데, 이 주입하는 부분은 사용자가 생성한 코드가 아닌 Dagger 2 라이브러리가 생성한 CodeAuthViewModel_MembersInjector 라는 클래스에서 담당한다.

@Generated(
        value = "dagger.internal.codegen.ComponentProcessor",
        comments = "https://google.github.io/dagger"
)
public final class CodeAuthViewModel_MembersInjector implements MembersInjector<CodeAuthViewModel> {
    private final Provider<PreferenceRepository> mPreferenceRepositoryProvider;

    private final Provider<TTSPlayer> mTTSPlayerProvider;

    public CodeAuthViewModel_MembersInjector(
            Provider<PreferenceRepository> mPreferenceRepositoryProvider,
            Provider<TTSPlayer> mTTSPlayerProvider) {
        this.mPreferenceRepositoryProvider = mPreferenceRepositoryProvider;
        this.mTTSPlayerProvider = mTTSPlayerProvider;
    }

    public static MembersInjector<CodeAuthViewModel> create(
            Provider<PreferenceRepository> mPreferenceRepositoryProvider,
            Provider<TTSPlayer> mTTSPlayerProvider) {
        return new CodeAuthViewModel_MembersInjector(mPreferenceRepositoryProvider, mTTSPlayerProvider);
    }

    @Override
    public void injectMembers(CodeAuthViewModel instance) {
        injectMPreferenceRepository(instance, mPreferenceRepositoryProvider.get());
        injectMTTSPlayer(instance, mTTSPlayerProvider.get());
    }

    public static void injectMPreferenceRepository(
            CodeAuthViewModel instance, PreferenceRepository mPreferenceRepository) {
        instance.mPreferenceRepository = mPreferenceRepository;
    }

    public static void injectMTTSPlayer(CodeAuthViewModel instance, TTSPlayer mTTSPlayer) {
        instance.mTTSPlayer = mTTSPlayer;
    }
}

MemberInjector 클래스는 필드로 통해 주입받는 클래스 (필드 인젝션)이 사용된 클래스에 생성되는 파일이다. 이 MemberInjector 클래스는 해당 예제에서 사용한 두 개의 필드 인젝션인 PreferenceRepository 와 TTSPlayer 에 대한 Provider 클래스와 생성자, 그리고 CodeAuthViewModel 의 필드에 할당하는 코드를 가지고 있다.

MemberInjector 클래스는 마찬가지로 Dagger에 의해 생성된 Factory 클래스에서 관리되고, Factory 클래스는 DaggerAppComponent 의 클래스에서 Map<Class, Provider<ViewModel>> 의 한 항목에 추가된다.

최종적으로 ViewModel를 얻어올 때 ViewModelProvider.of 에서 Map<Class, Provider<ViewModel>> 에 접근하고, 해당 항목에 있는 CodeAuthViewModel_Factory 항목을 가져오는데 이 때 필요한 PreferenceRepository, TTSPlayer 가 주입되어 사용할 수 있는 방식이다.

정리

이 글에서는 Dagger로 관리하기 위한 의존성 작성에 있어서 설계부터 작성, Dagger에 주입 및 사용하는 것 까지 살펴보았다.

언뜻보면 평소 기능을 구현하는 것과 같지만 공통적으로 사용한다는 것을 의식하면 프로젝트에 종속되지 않는 기능을 만들 수 있어 다양한 곳에서 활용할 수 있다. 특히 이러한 모듈을 하나씩 구현하다보면 프로젝트에 종속되지 않는 코어 모듈을 만들 수 있고, 하위 프로젝트들이 이를 사용함으로서 좀 더 빠른 개발을 진행할 수 있다.

실제로, 위치 계측이나 파일 다운로드, 저장소와 통신하기 위한 Repository 등 많은 클래스가 Dagger에 의해 관리되어 다른 곳에서도 계속 사용할 수 있게 구성이 되어있고 Upload Android Library into Gradle with Artifactory 글에서 도입된 Artifactory로 버전 관리도 되고 있다.

Screenshot on personal artifactory

이번에 제작한 클래스 자체의 기능은 그렇게 크지 않지만, 규모를 늘려가며 구현을 지속적으로 하다보면 좀 더 효율성이 있는 개발을 할 수 있지 않을까 생각해본다.