NHN Business Platform 웹플랫폼개발랩 정상혁

@Inject와 @Test는 최근 Java 프로그래밍의 경향을 함축하는 상징입니다. @Inject는 javax.inject.Inject’ 애노테이션을 가리키기도 하지만, 이 글의 맥락에서는 @Autowire, @Resource 등 의존 관계 주입(dependency injection)을 표시하는 애노테이션의 대표라고 이해해 주었으면 합니다. @Test는 JUnit4 ‘org.junit.Test’ 애노테이션으로, 테스트 코드를 표시합니다.

최근의 많은 Java 프로그래밍 환경에서는 주요 객체간의 의존 관계를 @Inject로 정의하고, 실제 실행 환경에서는 프레임워크, 컨테이너가 객체를 조립하는 역할을 합니다. 그리고 @Test로 표시된 테스트 코드에서는 테스트하고자 하는 대상이 아닌 부분은 가짜 객체로 교체해서 실행합니다. @Inject를 써서 얻은 장점은 여러 가지이지만, @Test가 붙은 메서드를 작성하기 쉽게 한다는 점에서 둘은 밀접한 관계가 있습니다. 단위 테스트(unit test)의 중요성이 강조되는 분위기가 의존 관계 주입의 인기에 촉매가 되기도 했습니다[1].

그런데 과연 Android에서도 @Inject와 @Test를 사용하면 좋은 점이 있을까요? 그리고 @Inject와 @Test를 사용하려면 어떤 도구를 쓰고, 개발 과정 중에 무엇을 바꿔야 할까요? 이 글에서는 이 질문에 대한 고민을 정리해보고자 합니다. Eclipse에서 moreUnit이라는 플러그인을 설치하면 단축키 Ctrl + J를 눌러 실행 코드와 테스트 코드 사이를 빠르게 오가면서 개발할 수 있습니다. 이 글에서도 비슷한 느낌으로 @Inject와 @Test 사이를 오가며 Android에서의 활용을 검토하겠습니다.

Dependency Injection(DI)의 이점

의존 관계 주입(dependency injection, 이하 DI)을 활용한 프로그래밍에서는 객체가 자신이 사용할 객체를 스스로 선택하지 않고, 제3의 객체가 사용할 객체를 주입한다. 의존 관계 역전(Inversion of control)이라고 불리는 이 과정에서 반드시 프레임워크가 필요하지는 않다. 객체의 관계를 조립하는 팩토리 클래스를 만들어서 사용할 수도 있다. 다만 DI 프레임워크는 객체 조립 과정을 더 편리하게 한다.

DI의 이점은 크게 세 가지를 들 수 있다. 이미 Spring과 같은 DI 프레임워크를 경험한 사람들에게 새로운 내용은 아닐 것이다.

첫째, 객체의 생성 주기를 제어한다. DI 스타일이 유행하기 전까지는 한 번 생성해서 계속 재활용할 객체의 singleton 패턴을 직접 구현했다. 객체 생성 지점을 통제하기 위해서, 생성자를 private으로 바꾸고 getInstance() 메서드를 추가하는 등 각 클래스마다 코드를 추가해야 했다. DI 프레임워크에서는 ApplicaitonContext, Injector, ObjectGraph 등으로 불리는 통합 객체 저장소에 일반적인 객체를 등록하고, 이를 사용하는 쪽에서 @Inject같은 애노테이션으로 표시하여 객체를 주입받는다. 애플리케이션에서 한 번만 생성하던 객체를 사용자의 요청이 있을 때마다 생성되도록 변경할 때에도 이 객체를 사용하는 객체 측의 코드는 크게 변경할 필요가 없다.

둘째, 객체에 부가 기능을 추가한다. AOP(Aspect Oriented Programming)를 이용하면, 의존하는 객체에 같은 규약을 유지한 채로 로깅, 보안, 트랜잭션 처리, Exception 처리 등의 부가 기능을 추가할 수 있어서, 반복되는 코드를 줄이고 유연하게 각종 운영 정책을 변경할 수 있다.

셋째, 실행 환경에 따라 구현 객체를 바꿔치기한다. 예를 들면 다음과 같은 상황에서 사용할 수 있다.

  • WAS에 따라서 다른 구현 객체로 바꾸기(예: Tomcat이나 Jetty를 사용할 때에는 DataSourceTransactionManager를 사용하고, JBoss를 사용할 때에는 WAS에서 JNDI(Java Naming and Directory Interface)로 제공되는 JTA(Java Transaction API) 규약의 TransactionManager를 사용)
  • 로컬에서 Call by Value로 호출되는 비즈니스 객체를 원격 호출 객체로 바꾸기(Spring Remote 모듈)
  • 테스트 코드에서 테스트 대상이 아닌 객체를 테스트 전용(test double 또는 mock) 객체로 바꾸기

정리하면, DI 프레임워크는 중요한 객체 생성에 관여하여, 객체의 생성 주기나 구현 방식을 바꿔야 할 때 코드를 적게 수정할 수 있게 한다.

또한 DI 스타일을 의식해서 개발을 하다 보면, 객체 간 역할을 명확히 분담하게 되어 모듈별로 독립적인 작업을 진행하는 데 도움이 된다. 물론 DI 프로그래밍 스타일과 프레임워크를 활용하면서 그에 어울리는 클래스 설계 방식을 고려해야 그런 이점을 얻을 수 있으며, 단지 프레임워크를 쓰기만 한다고 해서 그런 이점이 생기지는 않는다.

주로 Java 진영에서 발달한 DI 기술은 C#, .NET, Python, Ruby 등에서도 DI 프레임워크가 탄생하는 데에 영향을 미쳤다. 반면 C, C++ 등은 리플렉션이나 GC 등의 기능이 부족해서 DI 프레임워크가 발전하기 어려운 면이 있다[2]. DI 프레임워크는 Java의 대표적인 특성이라 할 수 있다.

Java에서 @javax.inject.Inject는 표준 규약인 JSR-330[3]에서 정의하고, Spring, Google Guice, Jboss Seam, Glassfish와 같은 JavaEE6 표준 WAS 등이 지원한다.

Android에서도 @Inject가 의미 있을까

앞에서 봤듯이 적어도 Java의 서버 모듈에서는 DI 모델이 프로그래밍 세상을 지배했다고 말해도 과언이 아니다. 하지만 그런 장점이 Android에서 이어질 수 있을까? Android에서 DI 프레임워크를 사용하는 데는 여러 장벽이 있다.

첫째, 용량에 부담이 된다. 서버에서는 jar 파일 하나를 추가해도 큰 영향이 없지만, 모바일 기기에서는 훨씬 한정된 자원을 이용하기 때문에 jar 파일의 용량, apk 파일의 용량, 메모리 사용량에 민감하다. DI 프레임워크를 사용하기 위해 애플리케이션의 용량이 늘어난다면, 그 이득이 정말 비용을 상쇄할만한지 더욱 냉정하게 평가하게 된다.

둘째, 성능에 부담이 된다. 프레임워크를 사용하면 실행 시에 콜스택이 늘어날 수 있다. 일반 호출에 비해 성능이 안 좋은 리플렉션도 DI 프레임워크에서 많이 사용된다. 의존 관계를 해석하기 위해 애플리케이션 초기 로딩에 더 많은 일을 해야 할 가능성도 크다. 서버 쪽에서는 무시해도 좋은 정도의 부담일 수 있지만, 모바일 기기에서는 더욱 민감하고 조심스럽다.

물론 앞으로 모바일 기기의 하드웨어와 Android가 계속 발전하면 지금보다 애플리케이션 수준의 성능 최적화가 덜 중요해질 수도 있다. 그러나 많은 사용자가 사용할 수 있게 하려면 오래 전에 출시된 저사양 기기도 고려해야 한다. 특히 애플리케이션이 전세계를 대상으로 한다면 더욱 그렇다. 우리나라는 고사양 기기와 최신 버전 Android의 보급률이 높지만, 전세계에서는 2013년 3월 현재 Gingerbread(Android 2.3) 이하 버전을 사용하는 사용자가 53.7%나 된다[4]. Android에서는 유연하고 튼튼한 설계보다 하드웨어에 맞는 최적화를 중요하게 여기는 경향이 있으며, 저사양 기기가 시장에 있는 동안은 이 경향이 계속될 것이다.

예를 들면, Interface는 변경에 유연하게 대응할 수 있게 하고 Enum은 개발자의 실수를 막아주기 때문에 Java 개발에서는 많이 사용하지만, Android에서는 한때 최적화 관점에서 둘 다 권장하지 않았다. 이제는 큰 영향이 없다고 해도 많은 개발자들은 아직도 이를 의식하고 있다.

셋째, Android 애플리케이션의 실행 환경인 Dalvik에서는 cglib과 같은 런타임 바이트 코드 생성 라이브러리를 사용할 수 없다. 이로 인해 프레임워크의 기능이 한정되어 장점이 줄어든다. Spring과 같은 DI 프레임워크에서는 런타임 바이트 코드 생성을 많이 이용한다. 대표적으로 인터페이스가 없는 클래스에 AOP를 적용할 때 cglib과 같은 기술을 사용하는데, Android에서는 런타임 바이트 코드 생성으로 비슷한 기능을 구현할 수 없다.

그렇다고 프레임워크를 적용하기 위해서는 주요 기능을 모두 인터페이스로 선언해야 한다면 프레임워크를 적용하는 데 불편한 점이 많아진다. 애플리케이션 개발자가 작성해야 할 코드의 분량이 늘어나고, 기존 코드에 적용할 때는 구조를 많이 바꾸어야 하기 떄문이다. 여러 Android 프레임워크에서 이를 어떻게 극복했는지는 뒤에서 살펴보겠다.

넷째, 대체로 애플리케이션의 규모가 작아서 DI의 유연성으로 얻는 이득이 크지 않다. 화면 개수나 코드 분량으로 비교하면 Android 애플리케이션은 서버 쪽보다 규모가 작은 경우가 많다. 이렇게 작은 애플리케이션은 서버처럼 WAS의 종류를 바꿀 일도 없고, 모든 주요 객체에 기능을 더하는 등의 큰 변경이 별로 없기 때문에 유연성이 크게 필요하지는 않다. 얻는 이득이 불확실한데, DI를 쓰기 위해 성능이나 용량에 대한 부담을 감수하기는 결정을 내리기는 어렵다.

다섯째, Android 기본 프레임워크와 DI 프레임워크의 조화를 고려해야 한다. Activity, Service 등 주요 객체는 이미 Android 기본 프레임워크에서 등록되고 고유한 라이프사이클이 정의되어 있다. 특히 Activity의 생성, 소멸, 시작, 정지 등의 라이프사이클은 애플리케이션 개발자가 늘 의식하고 정교하게 그 안에서 할 일을 배분해야 한다. 이렇게 객체 등록과 라이프사이클 관리가 Android에 특화되어 있는데, 모든 객체에 일반화된 접근을 하는 DI 프레임워크를 동시에 사용하면 이득이 있을지 고민해야 할 것이다.

그리고 View 객체와 같은 Android UI 클래스의 속성은 주로 XML 안에서 정의하고, Android가 객체 생성을 담당한다. 이 부분은 이미 DI 스타일과 유사하다. View 클래스를 쓰는 Activity에서는 findViewById(int) 메서드로 참조를 얻는데, 이는 dependency lookup 방식이다. Android DI 프레임워크는 View 객체의 생성 지점을 Android에 맡긴 채 findByViewId()와 같은 메서드를 DI 스타일로 대체할 방법을 제공해야 할 것이다.

Android에서도 @Test가 의미 있을까

이번에는 @Test를 살펴보자. 마찬가지로 Android에서는 테스트 코드를 작성하기 어렵게 하는 요소가 많다.

첫째, Android 기본 프레임워크의 구조에는 Mock을 적용하기 어렵다. Android에서는 의존하는 객체를 가져올 때 Activity.getViewById(int), getSystemService(String)와 같은 상위 클래스 메서드를 활용한다. 따라서 테스트용 가짜 객체를 끼워 넣으려면 상위 클래스의 동작을 가로채야 한다. 이런 경우에는 Mockito의 spy() 메서드를 사용하는 등 특별한 방법을 동원해야 한다. 그리고 그런 특별한 방법으로만 테스트가 가능하다는 것은 설계를 개선해야 한다는 신호라고 보기도 한다. Mockito매뉴얼에서도 spy() 메서드는 레거시 코드를 테스트할 때와 같은 특별한 상황에서만 사용하라고 설명한다[5]. 객체 간의 의존 관계를 가져오는 코드 외에 getAssets() 메서드와 같이 자원을 가져오는 코드도 마찬가지다. Android에서 테스트 코드는 언제나 상위 클래스의 동작을 추적해서 어디서부터 테스트를 위한 환경과 연결시킬 것인가를 고민하게 한다. Activity나 Context와 같은 슈퍼 클래스가 너무 많은 역할을 한다고 생각한다.

서버 쪽의 Java에서는 상속보다는 위임으로 POJO(특정 실행 환경에 종속적이지 않은 단순한 Java 객체)를 활용하고 DI를 이용해 의존 관계를 구성한다. 이는 테스트용 객체를 활용하기 쉬운 구조다. Android에서도 DI 프레임워크를 적용하면 테스트용 객체를 주입하기 편하지만 앞 단락에서 말한 런타임 시 성능과 용량의 문제 등에 다시 부딪친다.

둘째, 기본 제공되는 Mock클래스의 기능이 빈약하다. android.test.mock 아래에 MockContext, MockApplication, MockResource 등 여러 Mock 클래스가 제공되기는 한다. 그러나 이 클래스들은 대부분의 동작을 UnsupportedOperationException을 던지는 껍데기일 뿐이다. 필요한 동작은 아래 코드처럼 직접 override해서 구현해야 한다.

예제 1 MockContext override

static public class MockServiceContext extends MockContext {
   @Overrride
   public getSystemService(String name){
    ……
   }
}

웹 개발에서 자주 쓰는 Spring의 MockHttpServetRequest와 같은 클래스에서는 테스트에 필요한 동작이 기본적으로 구현되어 있는데, 그런 클래스와 비교하면 Android의 기본 Mock클래스는 상당히 불편하다.

셋째, 기본 제공 테스트 프레임워크가 사용하기 쉽지 않다. Activity를 테스트하는 데에는 ActivityTestCase, ActivityUnitTestCase, ActivityInstrumentationTestCase2의 세 가지 클래스를 쓸 수 있는데, 이를 경우에 따라 능숙하게 활용하는 경지에 이르기까지는 시행착오를 겪어야 한다. 이 클래스가 다른 용도를 가진 이유는 Android의 독특한 스레드 모델 때문인데, 이를 깊이 이해해야만 제대로 활용할 수 있다. 이 클래스를 사용하여 테스트하다 보면 예상하지 못하게 동작하는데, 이에 대한 내용은 잘 문서화되어 있지도 않다. 예를 들면 아래와 같은 상황이 있다.

  • ActivityUnitTestCase에서 Dialog생성 등에 Event가 전달되면 BadToken Exception이 발생한다[6].
  • ActivityInstrumentationTestCase2에서 Dialog 객체를 생성 후 dismiss() 메서드를 호출하지 않으면 leak window Exception이 발생한다[7].

실제 프로젝트에서 위의 현상을 겪었고, 원인을 파악하는 데 꽤 많은 시간을 허비했었다.

그리고 ActivityUnitTestCase와 같은 기본 테스트 클래스들이 JUnit3 기반으로 되어 있다는 점도 아쉽다. 이미 JUnit4가 나온 지 한참 지났는데도 자유로운 메서드 명이나 Exception 테스트와 같은 JUnit4의 장점을 활용할 수 없다. Android 테스트에서는 테스트의 성격을 구분하고 필요해 따라 선택해서 실행할 수 있도록 @SmallTest, @MediumTest, @LargetTest와 같은 애노테이션을 제공한다. 이미 JUnit보다 더 정교하게 구분된 애노테이션을 활용하고도 있으면서도 기본 틀은 JUnit3라는 점은 균형이 맞지 않아 보인다.

넷째, 느린 테스트 실행 속도다. Android에서 JUnit 테스트를 작성하려는 의욕을 꺾는 가장 치명적인 장벽이다. 보통의 Java 환경에서는 IDE 안에서 로컬 PC에 설치된 JVM으로 JUnit 테스트를 실행시키고 바로 결과를 확인한다. 반면 Android의 실행 환경은 특별한 JVM인 Dalvik이기 때문에 JUnit 기반의 테스트도 에뮬레이터나 실제 기기에서 실행되어야 한다. 그래서 코드를 한 줄만 수정해도 apk 파일로 만들어 기기에 설치한 후에야 테스트할 수 있다. 여기에 걸리는 시간이 일반적인 JUnit 테스트에 비하면 매우 길다. 기기의 사양에 따라 다르지만 애플리케이션의 용량이 큰 경우 패키징과 설치에 수십 초가 걸리기도 했다. 그래서 빠르게 테스트 코드와 실행 코드를 수정해 가면서 디버깅하고 코드를 개선하는 리듬을 탈 수가 없다.

“Working Efficiently with Legacy Code”라는 책의 저자는 테스트 실행 시간이 0.1초가 넘어가면 느린 단위 테스트이고, 느린 테스트는 단위 테스트가 아니라고 주장했다[8]. 이 말은 단위 테스트와 아닌 것을 구분하기 위한 엄격한 기준이라기보다는, 단위 테스트가 갖추어야 할 특성과 가치를 강조하는 설명이라고 생각한다. 이 말에 따르면 Android에서 빠른 피드백이라는 가치가 있는 단위 테스트는 불가능하다는 좌절을 느낄 만도 하다. 그리고 통합 테스트의 의미만으로 JUnit을 써도 여전히 아쉬운 점이 많다. 에뮬레이터 상태에 따라서 테스트 성공 여부가 달라지기도 하기 때문이다.

다섯째, UI 테스트 본연의 어려움이다. 많은 Android 코드는 UI 생성과 이벤트를 다루고 있는데, 다른 분야에서도 이런 코드는 테스트하고 어렵고, 자동화 테스트는 깨지기가 쉽다. UI 객체의 속성은 자주 바뀌고, 익명 클래스 등을 통해서 처리되는 이벤트는 밖에서는 Mock 객체로 바꾸고 추적하기가 어렵다.

그럼에도 불구하고

그럼 과연 Android에서는 DI 프레임워크나 테스트코드가 의미 없는 것일까? 그렇지는 않다고 생각한다.

우선 Android에서 반복되는 View, Resource, SystemService에 대한 참조를 가져오는 코드를 DI 스타일로 개선하면 좀 더 코드가 짧아지고 가독성이 좋아지리라 기대할 수 있다. 핵심 클래스의 의존 관계가 DI로 표시되면 객체 간 관계를 파악하기 좀 더 쉽다. 기존에 웹 개발을 하면서 DI 스타일에 익숙한 개발자들이 새롭게 애플리케이션 개발을 한다면 더 빨리 코드에 익숙해질 수도 있다.

애플리케이션의 규모가 작아서 유연성이 필요하지 않다고 해도, DI 스타일이 제공하는 testability는 여전히 중요하다. 기기에서 테스트를 실행하기 전에 단위 테스트로 부분적인 기능을 검증할 수 있다면, 개발하고 오류를 수정하는 주기가 짧아진다. 이를 통해 초기 기능 개발의 시간을 줄이고, 향후에 더 빠르게 기능을 추가할 수 있을 것이다.

먼저 Android의 DI 프레임워크에서는 앞에서 말한 과제를 어떻게 풀고 있는지 살펴보겠다.

DI 프레임워크

여기에서는 Android에서 DI를 사용할 수 있는 프레임워크를 살펴보겠다. Android DI 프레임워크는 대표적으로 다음 6개가 있다.

  • Roboguice
  • Android Annotations
  • Transfuse
  • DroidParts
  • Dagger
  • Yasdic

항목별 비교

Android DI 프레임워크의 특징은 다음과 같다. 이 중에는 DI만 제공하는 프레임워크도 있고, DI를 비롯하여 많은 기능을 제공하는 프레임워크도 있다.

표 1 Android DI 프레임워크의 특징

프레임워크

Android 전용

Annotation Processing 활용

최근 활동(2013년 4월 1일 기준)

문서 수준

JSR330 지원

최신 버전

최근 릴리스

최근 커밋

Roboguice

O

X

2.0

2012. 4. 23.

2013. 2. 5.

O

Android Annotations

O

O

2.7.1

2013. 03. 4.

2013. 3. 5.

X

Transfuse

O

O

0.1.2

2012. 11. 06.

2013. 2. 1.

O

DroidParts

O

X

1.2.1

2013. 3. 30.

2013. 3. 30.

X

Dagger

X

O

0.9.1

2012. 11. 12.

2013. 3. 28.

O

Yasdic

X

X

1.0

2009. 6. 29.

2009. 10. 22.

X

Android 전용 여부

Dagger와 Yasdic을 제외한 나머지는 모두 Android 전용 프레임워크다. Dagger와 Yasdic은 Android 전용은 아니지만 경량이라 Android에서 문제 없이 실행된다. Dagger는 Android를 염두에 두고 개발했다고 밝히고 있다. 다만 Android 전용이 아닌 프레임워크는 View injection 등 Android에 특화된 DI 기능이 없으므로 View, Resource, SystemService등 Android의 기본 구성 요소를 DI로 사용하고 싶을 때에는 다소 불편하다.

Roboguice는 Android 전용이지만 핵심 DI 기능은 Google Guice에 의존적인데, 이 Google Guice 버전은 Android 전용이 아니다. 따라서 필수 바이너리가 비교적 크다.

안드로이드 전용 프레임워크 중 Roboguice, Android Annotations, Transfuse의 용량을 비교해 보았다. Dagger와 Yasdic은 View injection 등 안드로이드에 특화된 기능을 제공하지 않아서 제외했다.

98c25465de10a27716f86cc814754e62.png

그림 1 DroidParts, Transfuse, Android Annotations, Roboguice 용량 비교

jar 용량은 apk 파일에 필수로 패키징돼서 들어가야 할 파일만 용량을 합계해서 계산했다. Android Annotions와 Transfuse에서 컴파일할 때만 필요한 jar 파일은 apk 파일에서는 참조할 필요가 없으므로 제외했다. Roboguice가 약 622KB로 가장 용량이 컸고, Android Annotions는 62KB로 가장 작았다.

apk 추가 용량은 프레임워크를 안 썼을 때에 비교해 늘어난 apk 파일의 크기를 정리했다. 이를 위해 테스트용 앱을 먼저 프레임워크 없이 구현한 후에 각각의 프레임워크를 적용해서 다시 같은 기능을 구현했다. 용량 측정에 사용한 애플리케이션은 간단한 날짜 계산기로, Fragment 4개와 Activity 1개로 이루어졌고, DI 프레임워크에서 View Injection과 Resource Injection기능을 활용했다. 뒤에서 안내할 SVN 주소에서 이 애플리케이션의 소스를 확인할 수 있다. 역시나 Android Annoations가 가장 적은 용량으로 불과 10.6KB이 더 늘어났다.

Transfuse는 적용을 하려면 애플리케이션 구조를 많이 바뀌어야 하는 문제가 있어서 apk 추가 용량은 검증하지 못했는데, Android Annoations와 jar 파일 크기나 구현 원리가 비슷하기 때문에 apk 파일에 추가되는 용량도 비슷할 것으로 추정한다.

최근 프로젝트 활성화 수준

Yasdic을 제외한 나머지는 모두 비교적 최근에 커밋이 있었다. Android Annotations와 DroidParts는 최근에 새 버전도 릴리스한 것으로 보아 활발하게 개발 활동이 이뤄지고 있는 것으로 보인다.

문서 수준

Androi Annotations, Transfuse, Roboguice가 비교적 높은 수준으로 문서화되어 있다.

JSR-330 지원여부

JSR-330은 앞에서 언급했듯이 @javax.inject.Inject처럼 표준으로 정의된 애노테이션이다. 기능은 같더라도 표준 애노테이션을 활용하면 모듈의 이식성을 더 높일 수 있다. Roboguice, Transfuse, Dagger가 JSR-330을 지원한다.

Injection 대상 지원 범위

다음은 Android와 아무런 의존성이 없는 POJO 클래스를 비롯하여, View, SystemService 등 Android에 특화된 DI를 지원하고 있는지 여부를 정리한 표다. 예를 들면 View injection이 지원되는 프레임워크에서는 Activity.findViewById를 @InjectView와 같은 애노테이션으로 대체할 수 있다.

표 2 프레임워크의 Injection 대상 지원 범위

프레임워크

POJO

View

Resource

System Service

Extra bundle

Application

Fragment

Roboguice

O

O

O

O

X

O

X

Android Annotations

O

O

O

O

O

O

O

Transfuse

O

O

O

X

O

X

X

DroidParts

O

O

O

O

O

X

O

Dagger

O

X

X

X

X

X

X

Yasdic

O

X

X

X

X

X

X

위 프레임워크 중 Android 전용 프레임워크는 모두 Android에 특화된 DI 기능과 이를 위한 애노테이션을 지원한다. Android Annotations가 가장 넓은 범위를 지원하고, 그 다음으로는 DroidParts와 Roboguice순이다.

각 프레임워크에서 View injection을 어떻게 지원하는지 예를 들어 살펴보자. 먼저 일반적인 Android 프로그래밍 방식에서는 findViewById와 Casting으로 View객체를 가져온다.

예제 2 Android에서 일반적인 View 객체 탐색

public class WeatherActivity extends Activity {
    EditText city;
     @Override
    protected void onCreate(Bundle savedState) {
        super.onCreate(savedState) );
        setContentView(R.layout.weather);
        city = (EditText) findViewById(R.id.city);
    }
…
}

이를 각 프레임워크에서 구현한 방식은 다음과 같다.

예제 3 Roboguice의 View injection

@ContentView(R.layout.weather)
class WeatherActivity extends RoboActivity {
    @InjectView(R.id.city) TextView city;

    public void onCreate(Bundle savedState) { 
        super.onCreate(savedState);
        city.setText("Test");
....
    } 
}

예제 4 Android Annotations의 View injection

@EActivity(R.layout.weather)
public class WeatherActivity extends Activity {
    @ViewById TextView  city;

    @AfterViews
        void initText() {
        city.setText("Test");
    }
….
}

예제 5 DroidParts의 View injection

class WeatherActivity extends prg.droidparts.activity.Activity{ 
    @InjectView(id=R.id.city)  TextView city;
    @Override
    public void onPreInject() {
        setContentView(R.layout.weather);
    }

    @Override
    protected void onCreate(Bundle savedState) {
        super.onCreate(savedState);
        city.setText("Test");
    }
}

예제 6 Transfuse의 view injection

Activity(label = "@string/app_name")
@Layout(R.layout.city)
public class WeatherActivity {
    @Inject @View(R.id.city)
    TextView city;

    @OnCreate
    public void initText() {
        city.setText("test");
    }
}

@Inject, @InjectView 등 유사한 표기법을 지원하는 것을 확인할 수 있다. 단 이 애노테이션을 인식해서 처리하는 방식은 프레임워크마다 조금씩 다른데, Roboguice나 DroidParts는 프레임워크가 제공하는 상위 클래스에서 의존성을 주입하고, Android Annotations와 Transfuse는 애노테이션 프로세싱(annotation processing)으로 컴파일 타임에 코드를 생성한다. 그래서 Android Annotations는 Android의 기본 Activity 클래스를 상속받고, Transfuse는 아예 상위 클래스가 없는 코드를 보여준다.

애노테이션 프로세싱 활용 여부

앞에서 잠시 언급한 애노테이션 프로세싱은 소스 코드를 컴파일할 때 애노테이션 정보를 읽어서 소스 코드를 조작할 수 있는 기술이다. Java 6부터 기본 Java 컴파일러에 포함되었고, QueryDSL, Lombok에서도 이를 활용하고 있다.

런타임에 성능과 용량에 부담이 적기 때문에 Android에서는 이 기술이 특히 유용하다. DI 프레임워크로 조사 대상이었던 프레임워크뿐만 아니라, Android의 ORM(Object-relational mapping)인 Storm이라는 프레임워크에서도 이를 활용하고 있다. Android에서는 애노테이션 프로세싱이 런타임 바이트 코드 조작 기술을 많이 대체하고 있는 것으로 보인다.

참고로 Android에서 바이트 코드 조작 기술인 cglib이나 ASM을 대체할 수 있는 기술은 Dexmaker(https://code.google.com/p/dexmaker/)가 있다. Dexmaker는 Dalvik에서 실행될 수 있는 .dex 형식의 바이트 코드를 생성한다. 뒤에서 언급할 Mockito의 최신 버전이나 테스트 실행 프레임워크인 Vogar(https://code.google.com/p/vogar/)에서는 이를 활용하고 있는데, 아직 DI 프레임워크에 활용된 사례는 찾지 못했다.

프로젝트별 평가

Roboguice

유명한 애플리케이션에서 사용한 사례가 많은 DI 프레임워크이다. Facebook messenger, Groupon, Google Docs, Tripit, Digg에서 사용하고, NHN에서도 네이버 카페 애플리케이션이 사용하고 있다.

최신 버전에서는 JSR-330의 @Inject 애노테이션을 지원하고, Goggle Guice와 유사한 방식으로 DI를 사용할 수 있다는 장점이 있다. 애노테이션 프로세싱을 사용하지 않아서 다소 성능이나 용량에 부담이 있다.

Android Annotations

iLive 등 많은 애플리케이션에서 쓰이고 있으나, 이를 활용한 애플리케이션의 유명세는 Roboguice보다는 약해 보인다.

애플리케이션 개발자가 만드는 클래스에서는 프레임워크가 제공하는 특정 상위클래스를 상속하지 않지 않고 Activity 등 원래 Android에서 사용하는 클래스를 상속해서 구현한 후, 애노테이션 프로세싱으로 애플리케이션 개발자가 만든 클래스의 하위 클래스를 만드는 방식으로 동작한다. 이런 방식 때문에 Roboguice, GreenDroid, ActionBarSherlock 등 다른 프레임워크와 병행해서 사용할 수 있다.

그리고 View나 리소스의 ID를 직접 지정하지 않아도 변수 이름이나 메서드 이름으로도 추론할 수 있기 때문에 다른 소스 코드를 더욱 파격적으로 줄일 수 있다. DI 외에도 @Click 애노테이션으로 이벤트 메서드 지정, @Rest 애노테이션과 @Get 애노테이션으로 Spring RestTemplate을 호출하는 등 Android에서 코드를 줄일 수 있는 많은 기능을 제공한다.

단점으로는 애노테이션 프로세싱을 활용하기 때문에 IDE 설정이 처음에 다소 번거로운데, IntelliJ에서는 환경 구성이 잘 되지 않는다는 반응이 많다[9]. 그리고 자동 생성되는 클래스는 끝에 언더바(_)가 붙으므로, 클래스를 참조하는 XML 선언에서는 이를 고려해서 클래스 이름을 지정해야 한다.

Transfuse

@Activity와 같은 애노테이션을 활용하면 아예 Android의 상위 클래스를 상속받지 않을 수 있다. 심지어 onCreate()같은 메서드도 override 방식이 아닌, @OnCreate와 같은 애노테이션으로 지정한다.

JSR-330과 간단한 AOP를 지원하면서도 애노테이션 프로세싱을 활용하여 런타임의 부담이 적다는 장점이 있다. Activity 등록이나 Intent-filter처럼 AndroidManifest.xml에 들어갈 내용도 애노테이션 안에 포함해서 선언한다. 이 때문에 기존 애플리케이션에는 점진적으로 적용하기 어렵다는 단점이 있다.

가장 파격적으로 애노테이션을 활용한 프레임워크이고, 따라서 사용자에 따라서 선호도가 엇갈릴 것이라고 생각한다.

DroidParts

DroidParts는 DI뿐만 아니라 RestClient, JSON 해석, ORM, Async 작업 처리 등 많은 기능을 제공하는 프레임워크다. 이는 종합 선물 세트 같은 프레임워크를 원하는 사용자에게 장점일 수 있다.

DI 기능으로는 POJO뿐만 아니라 View, SystemService, Resource, BundleExtra 등의 Android 구성 요소를 DI 스타일로 개발할 수 있다.

문서화가 다소 부족하고, 애노테이션 프로세싱 방식보다는 런타임에서 처리하는 일이 많다는 단점이 있다.

Dagger

JSR-330을 지원하면서 애노테이션 프로세싱을 활용하는 경량 DI 프레임워크이지만 Android만을 위한 기능은 부족하다. 아직 프로젝트 초창기고, 문서도 많지 않다.

Yasdic

더 이상 업데이트되지 않고, 홈페이지에서도 Roboguice 같은 다른 프레임워크를 사용하는 것을 권장하고 있다.

총평

Android에서 DI 프레임워크가 쓰이기 어려운 장벽으로 용량 부담, 성능 부담, Dalvik의 제약, 애플리케이션의 규모를 들었다. 이를 기준으로 다시 프레임워크들의 특성을 살펴보자.

첫째, 용량 부담을 줄이기 위해 패키지 구성을 경량화했다. 서버 쪽에서 쓰이는 Spring이나 Google Guice등을 그대로 쓰지 않고, Android를 감안해서 작은 용량으로 개발하거나 의존성을 최소한으로 줄였다. 특히 애노테이션 프로세싱은 라이브러리에서 컴파일타임에 의존하는 부분과 런타임에 의존하는 부분을 구분해서 더욱 용량을 줄였다.

둘째, 성능 부담을 줄이기 위해서, 런타임에서 콜스택을 늘리지 않고 컴파일 타임에서 동작하는 애노테이션 프로세싱을 적극 활용했다.

셋째, Dalvik에서는 바이트 코드 조작 기술을 쓸 수 없지만, 애노테이션 프로세싱을 사용하여 소스를 생성한 후 컴파일하는 방법으로 대체했다.

넷째, 애플리케이션의 규모가 작아도 View나 Resource 같은 자원을 가져오는 코드가 간결해진다는 이점이 있다.

위 특성을 살펴보면 Android DI 프레임워크에서 애노테이션 프로세싱이 상당히 중요한 역할을 한다는 것을 알 수 있다. 따라서 애노테이션 프로세싱을 적극적으로 활용하면서 다양한 구성 요소에 injection을 지원하는 Android Annotations에 가장 높은 점수를 주고 싶다.

Roboguice는 비록 애노테이션 프로세싱 방식은 아니지만, 유명한 레퍼런스 애플리케이션이 많기 때문에 적용 사례를 중요시하는 관점에서는 높이 평가할 만하다. 즉, Android용 DI 프레임워크를 선택한다면 Android Annotations와 Roboguice가 가장 우선순위가 높다고 생각한다.

Transfuse는 파격에 가까울 정도로 적극적으로 애노테이션을 활용한다는 점에서 그 설계 방식을 참고하고 앞으로 발전을 지켜볼 만하다. 하나의 프레임워크에서 DI를 포함한 다양한 기능을 제공하는 사례를 참고하고 싶다면 DroidParts도 도움이 될 수 있다. 즉, Transfuse와 DroidParts는 강력히 추천하지는 않지만 분석 대상으로 유용하다.

Dagger와 Yasdic은 추천하지 않는다. 아직 프로젝트 초창기고 Android에 특화된 기능이 없어서 이점이 크지 않아 보인다. 특히 Yasdic은 현재 기능도 빈약한 데다가 발전 가능성도 없으니 깨끗이 잊어버리면 되겠다.

본격적인 평가를 내리려면 먼저 Android Annotations와 Roboguice를 활용하여 많은 실무 사례를 축적해야 할 것이다.