loading...

Android용 Fat AAR gradle plugin 개발을 포기했다.

kingori profile image Sewon Ann ・1 min read

발단

요즘 안드로이드 앱을 개발할 땐 내부적으로 여러개의 gradle 모듈을 조합해서 구성하는 방식을 많이 적용한다. 잘 구성을 하면 아키텍처 제약사항을 확실하게 적용할 수 있고, 빌드 시간도 줄일 수 있다.

하지만 앱이 아닌 외부 배포용 라이브러리의 경우엔 모듈을 쪼갤경우, 의존 모듈도 모두 배포를 해야 하는 부담이 생긴다. 한두개라면 까짓거 하지 뭐! 라고 생각할 수 있지만, 잘개 쪼개서 한자릿 수를 넘어간다면 대책을 세워야 한다. 모두 배포하던지, 어떻게든 내부 모듈들을 잘 묶어서 한 통의 aar 로 만들어 배포하던지.

java 세상에서 jar로 배포할 경우에도 다들 이런 고민을 했기 때문에 fat jar 관련 내용은 굉장히 많고, gradle 상에서 fat jar를 만드는 노하우도 쉽게 찾을 수 있다. 하지만 aar 조립은 jar 조립보다 훨씬 어렵다.

어쨌든 해내야 할 업무라서 1주일 넘게 씨름을 했지만 너무 어려워 거의 포기한 상태이다. 지금까지 삽질의 기록을 간단히 남겨본다. 참고로 실험은 이미 배포된 aar을 묶는 것으로 했다.

전개

무에서 시작할 순 없으니 기존에 남들이 시도해 본 내용을 찾아봤다. 최근에도 개발이 이뤄지고 있는 fat-aar-android 를 시작점으로 잡았다.

이 gradle plugin 은 embed 라는 custom config를 사용해 의존 관계를 선언한다. 그리고 embed로 선언한 모듈의 내용이 aar에 함께 포함되는 방식이다. 멋져보이지만 testImplementation 과 같은 형태를 지원하지 않기 때문에 제약도 있다. 내 경우엔 aar에 함께 배포할 대상은 local module 이므로 굳이 저렇게 어렵게 가지 않아도 되어, plugin 을 가져다 수정해보기 시작했다.

flavor 처리

먼저 의존 module이 flavor를 가지는데, 배포 module이 그 flavor를 가지지 않았을 때 빌드 에러가 난다. 이 부분은 missingDimensionStrategy 를 이용해서 결정하도록 plugin 기능을 확장했다. 이 작업도 쉽진 않았다.

databinding

의존 module이 databinding을 쓸 경우, aar에는 특별한 디렉터리가 생긴다. databinding v1 에선 /data-binding 만 생기고, v2 에선 /data-binding/data-binding-base-class-log 가 생긴다. 문제는 저 디렉터리 내용물이 json 또는 bin 이라 대체 하위 라이브러리들의 내용들을 어떻게 조합할 지 감도 잡히지 않았다. 그런데 다행히 기계적으로 하위 aar들의 디렉터리 내용을 긁어다 조합하니 잘 돌아갔다. 그리고 이 작업을 위해선 배포 module 은 자신이 databinding을 쓰지 않더라도 무조건 databinding enabled를 true로 선언해 줘야 했다.

maven pom dependency

aar 자체는 의존 라이브러리 정보를 가지지 않는다. maven에 배포될 때 만들어지는 pom 파일을 통해 의존 라이브러리 정보를 표현한다. 따라서 여러 aar 들을 묶기 위해선 의존 aar 들의 의존 정보를 모두 가져다가, 배포 module의 pom 에 기록을 해야 한다. 자연스럽게 처리하려면 의존 module 의 dependency 를 싹 가져다 배포 module 의 dependency 에 넣어주면 되는데, 이 처리가 쉽지 않았다. 의존 module 의 dependency를 알아내려면 resolve 과정을 거쳐야 하는데, 이미 resolve 까지 다 한 이후엔 dependency 를 건드리지 못하는 것으로 추정된다. 결국 배포하려면 maven publish plugin 을 사용할 거라는 가정하에, maven publish extension 의 pom 생성 정보를 조작해서 pom 생성 시 내가 의도한 dependency 를 기록하도록 강제했다. 다만 배포 시 pom 이외에 gradle module metadata 도 배포될 수 있는데, 이 metadata 가 존재할 경우 의존관계는 pom 보다 gradle module metadata가 우선시되는 것 같다. 이 쪽을 건드리긴 쉽지 않아 그냥 아래와 같이 gradle module metadata 배포를 막아버렸다.

tasks.withType(GenerateModuleMetadata) {
    enabled = false
}

R

난 여기서 포기했다. 안드로이드 앱이나 라이브러리는 R 클래스를 가진다. 라이브러리의 R 클래스는 aar 에 존재하지 않고, 자신을 사용하는 쪽에서 생성한다. 즉 앱을 빌드하면 앱의 빌드과정에서 자신이 사용하는 라이브러리의 R 클래스들이 쫙 만들어진다. 이 내용은 build/generated/not_namespaced_r_class_sources 쪽에서 확인할 수 있다.

문제는 묶어서 배포하게 되면 배포 aar 에 묶인 하위 aar 들의 R 들이 생기지 않는다. 앱 입장에선 자신이 만들어야 할 R 대상은 배포 aar 하나 뿐이기 때문이다. 이를 위해 fat aar 플러그인에선 각 aar 들의 R 클래스를 직접 만들어서 aar 안에 집어넣었다. 내용은 이런 식이다.

class R {

  static final int my_resource_id = com.배포모듈.R.my_resouce_id;
}

즉 자신의 값을 나중에 만들어질 배포 모듈의 R 값으로 선언하는 식이다. 캬! 똑똑하다.

하지만 이 방식은 2가지 문제가 있다.

  1. 최종 R에 필드가 누락될 경우, 런타임에서 클래스 로딩 시점에 크래시가 발생한다.
  2. databinding v1 의 경우 switch 문이 다수 등장하는데, 여기서 constant expression 이 아니라고 하면서 빌드가 실패한다.

미리 만든 R과 최종 R의 필드가 일치하거나 최종 R이 더 필드 수가 많으면 문제가 없고, databinding v2 를 써도 문제가 없다. 하지만 R의 내용이 어떻게 될 지 모르는 상태에서 1번 문제를 낙관적으로 잘 되겠지 하고 넘길 순 없다.

물론, 아래와 같이 극단적으로 R 을 만들어버리면 실행은 되겠지만 (정말 필요한 R 필드는 당연히 생성된다) 퍼포먼스에 문제가 생긴다. 이런 필드가 천개도 넘게 생길 수 있기 때문이다.

class R {

  static final int my_resource_id;

  try {
       my_resource_id = com.배포모듈.R.my_resouce_id;
  } catch(Throwable t) { }
}

이 문제를 가장 아름답게 해결하려면, 결국은 배포 aar 에 포함된 하위 라이브러리의 R도 빌드 과정에서 자연스럽게 생성되어야 한다. 몇 가지 실행방안은 떠오르지만 하나같이 어려워보인다.

첫째는 LinkApplicationAndroidResourcesTask 를 속여서, 마치 추가로 R을 생성해야 할 라이브러리가 있는 척 하는 것이다. 그런데 내부적으로 보면 이를 위해 symbol file도 만들어야 하고, 이래저래 속이기 위해 해야 할 일이 많고, 또 쉽지 않아 보인다.

둘째는, 만들어진 R을 쓱 가져다 조작해서 만들어내는 것이다. 즉 com.배포모듈.R 이 만들어지면 이걸 가져다 똑같은 필드를 가진 com.의존모듈.R 을 만든다. 근데 이 작업이 잘 되려면 com.배포모듈.R 이 생성되고 추가적인 빌드 진행이 되기 전의 알맞는 타이밍에 실행되어야 하고, 클래스를 읽어다 같은 필드를 가지면서 패키지만 다른 클래스를 어떻게 만들어내는지도 잘 모르겠다. 런타임에 reflection을 쓰는 것도 아니고 class 자체를 만들어내야 해서.

그래서 현재는 R 클래스 생성에 막혀서 잠시 포기 상태이다. ㅠㅠ

Posted on by:

kingori profile

Sewon Ann

@kingori

Android Developer in Korea

Discussion

markdown guide