Android/App개발2019. 6. 4. 14:02

https://github.com/google/dagger

 

google/dagger

A fast dependency injector for Android and Java. Contribute to google/dagger development by creating an account on GitHub.

github.com

의존성주입 라이브러리인 Dagger2를 알아보기 전에 Dagger1을 먼저 알아보자


2012년에 자바로 의존성을 주입하는 아이디어를 좋아한 스퀘어 개발자 몇명이 만들었다. 결국 어노테이션 기반의 코드생성과 guice와 비슷한 api를 갖지만 더 유연하고 빠르게 만들었다.
Dagger의 동작방식은 주입하고자 하는 모든 의존성을 위한 프로바이더를 포함하는 모듈들을 정의하여 객체그래프에 모듈을 로딩하고 결국 필요한 타겟에 내용을 주입하는 방식이다. 충분히 단순한구조(구현은 단순하지 않음)는 개발자가 코드를 분리할 수 있게 도움을 주고 클래스 상단에 객체를 생성하는 보기싫은 코드들을 라이브러리가 생성하는 인젝터들에 옮겨준다.
하지만 이런 잇점과 동시에 빠른 풀리퀘스트(하나 혹은 둘)를 해결하지 못하는 문제가 있다. 이는 기본아키텍쳑와 관련되어 있다.

- 런타임에 그래프 구성 : 성능에 악영향. 
- 리플랙션(예:Class.forName()) : 읽기 힘든 자동생성 코드와 프로가드의 악몽 발생
- 보기 싫은 자동생성코드 : 팩토리로 부터 수동 생성한 경우와 비교하여 아주 보기 싫음

결국 이런 이유로 수많은 이슈들이 트래킹되었고 이 이슈들은 Dagger2에서 해결하는것으로 이슈가 클로즈 되었다.

Dagger2

스퀘어의 원작자와 구글의 코어라이브러리 팀이 Dagger의 다음 이터레이션을 가져왔다.
약속대로 여러 이슈들이 해결되어 새버전이 배포되었다.

- 리프랙션 없음 : 프로가드 지옥도 해결
- 런타입 그래프구성 없음 : 성능 향상
- 추적가능 : 더 좋은 코드와 리플랙션 제거로 인해 더 읽이 쉽고 따라하기 쉬워짐.

Module

Dagger2에서 가장 변경이 없는것이 모듈이다. 주입하고자 하는 의존성의 프로바이더 메서드를 정의하면 된다. 

SharedPreference가 필요하다고 외쳐보자!

@Module 
public class ApplicationModule { 
    private Application mApp; 
    public ApplicationModule(Application app) { 
        mApp = app; 
    } 
    @Provides
    @Singleton
    SharedPreferences provideSharedPrefs() { 
        return PreferenceManager.getDefaultSharedPreferences(mApp); 
    } 
}

Component

Dagger를 아는 사람은 모듈에서 ingect = {} 가 사라진것을 알수 있다. Dagger2는 콤포넌트가 이를 대체한다.

@Singleton 
@Component(modules = {ApplicationModule.class}) 
public interface ApplicationComponent { 
    void inject(DemoApplication app); 
    void inject(MainActivity activity); 
}

주입하고자 하는 타겟들을 정의하면 되며 만일 누락되면 "cannot find method" 컴파일에러가 날것이다.

Application

다음은 콤포넌트를 담을 컨테이너이다.
응용 프로그램에 따라 더 복잡한 방법으로 구성 요소를 저장할 수도 있지만이 예제에서는 응용 프로그램 클래스에 저장된 단일 구성 요소로 충분하다.

public class DemoApplication extends Application { 
    private ApplicationComponent mComponent; 
    @Override 
    public void onCreate() { 
        super.onCreate(); 
        mComponent = DaggerApplicationComponent.builder() 
                    .applicationModule(new ApplicationModule(this))
                    .build(); 
    } 
    public ApplicationComponent getComponent() { 
        return mComponent; 
    } 
}

특별한것이 없다 해당 콤포넌트를 build하고 인스턴스화 해서 저장한다.
위 코드에서 이해가 안되는 부분이 있을수 있는데 DaggerApplicationComponent가 어디서 나왔냐 하는것일것이다. 이는 Dagger%COMPONENT_NAME%형식으로 명명되어 자동생성된 클래스이다. 컴파일러가 자동으로 만들어준다. 

Injection

마지막으로 위에서 설정한 인젝션 설정을 SharedPreference에 접근하고자 하는 액티비티내에서 사용하게 된다.

public class MainActivity extends Activity { 
    @Inject SharedPreferences mSharedPrefs; 
    @Override 
    public void onCreate(Bundle savedInstanceState) { 
        super.onCreate(savedInstanceState); 
        ((DemoApplication) getApplication())
                    .getComponent()
                    .inject(this); 
        mSharedPrefs.edit()
                    .putString("status", "success!")
                    .apply(); 
    } 
}

예상한데로 @Inject어노테이션은 변경되지 않았고 인젝션내에서만 일부 변경이 있다.  

Named Injection


동일 타입의 여러 객체가 필요한 경우 예를 들어 SharedPreference를 용도가 다르거나 다른파일을 지정하는 등의 경우 어떻게 할것인가?
이 경우 이름을 부여할 수 있다. @Named 어노테이션을 추가하는것만으로 가능하다.

@Provides 
@Named("default") 
SharedPreferences provideDefaultSharedPrefs() { … } 

@Provides 
@Named("secret") 
SharedPreferences provideSecretSharedPrefs() { … }

주입 대상에서도 동일하게..

@Inject @Named("default") SharedPreferences mDefaultSharedPrefs; @Inject @Named("secret") SharedPreferences mSecretSharedPrefs;


Lazy Injection

성능을 고려하면 지연된 주입이 반드시 필요하다 모든 의존성을 앱실행 초기에 하면 앱이 늦게 시작될테니까. 이를 위해 지연된 주입을 지 원한다.

@Inject Lazy mLazySharedPrefs; 
void onSaveBtnClicked() { 
    mLazySharedPrefs.get()
                .edit().putString("status", "lazy...")
                .apply(); 
}

위와 같이 정의 두면 호출전까지 주입이 되지 않는다. 이 후에는 주입된 상태를 유지한다.

Provider Injection

팩토리 패턴이 가능하다. 객체를 주입하지 않고 여러 인스턴스를 만들어야 하는 경우... 이 경우 Provider가 가능하다.

@Inject 
Provider mEntryProvider; 
Entry entry1 = mEntryProvider.get(); 
Entry entry2 = mEntryProvider.get();

프로바이더가 Entry객체의 두가지 인스턴스를 만든다. 인스턴스가 모듈에서 어떠게 생성될지는 알아서...

짜증나게 하는 지점!

inject() 메서드는 강력한 타입체크를 수행한다. 이는 디버깅에 유리하지만 베이스클래스로부터 주입하는 경우 복잡하게 한다.
직관적으로 베이스클래스를 대상으로 하는 inject()메서드를 만들것이다 하지만 이는 서브클래스에 대해서는 동작하지 않는다.

해결책은 멀까? 

방법#1 은 베이스클래스에 abstract 메서드를 정의하는것이다. 

@Override 
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState); 
    injectComponent(((DemoApplication) getApplication())
                .getComponent()); 

protected void injectComponent(ApplicationComponent component);

그리고 서브클래스에서 구현한다.

@Override
protected void injectComponent(ApplicationComponent component) { 
    component.inject(this); 
}

방법#2 는 리플랙션을 사용하는것이다. 이는 당신이 다루고자 하는 타입에 해 당하는 inject()메서드를 찾는것으로 귀결된다.

먼저 당신이 정의한 콤포넌트의 모든 메서드들을 타입별로 캐시한다.

// Keep the returned cache in some helper. 
Map<Class, Method> buildCache() { 
    Map<Class, Method> cache = new HashMap<>(); 
    for (Method m : ApplicationComponent.class.getDeclaredMethods()) { 
        Class[] types = m.getParameterTypes(); 
        if (types.length == 1) { 
            cache.put(types[0], m); 
        } 
    } 
    return cache; 
}

그리고 주입시에 getClass()로 캐시로부터 꺼내고 invoke를 호출한다.

// Use for injecting targets of any type. void inject(ApplicationComponent component, Object target) { 
    Method m = cache.get(target.getClass()); 
    if (m != null) { 
        m.invoke(component, target); 
    } 
}

기본클래스에서 모든 설정을 숨기거나 이것을 수행하는 코드를 다루는 누군가에게는 유용할것이다.

짜증2 는 콤포는트 구현이 재빌드를 요구하고 클래스가 사라질때는 컴파일에러가 발생한다는 것이다. 심간한 불편함은 아니지만 프로젝트 구성 초기에는 좀 귀찮을 수 있다.

원문 : https://blog.gouline.net/dagger-2-even-sharper-less-square-b52101863542

 

Dagger 2: Even Sharper, Less Square

It was only a matter of time before version 2.0 of the well-known dependency injection library Dagger hit production and that seemed like…

blog.gouline.net

한글로 된 잘 정리된 문서

https://medium.com/@jason_kim/tasting-dagger-2-on-android-%EB%B2%88%EC%97%AD-632e727a7998

Posted by 삼스