Android 9.0 : Google Map, Volley 관련 이슈

지난주 금요일 (18일) 기준으로 Galaxy S8에 9.0 Beta가 시작되어 업데이트 하여 사용하고 있었으나, 구글 맵을 불러오면 아래 오류가 노출되면서 앱이 죽는 문제가 있었다.

2019-01-21 10:38:14.551 31596-31686/com.*** E/AndroidRuntime: FATAL EXCEPTION: Thread-7
    Process: com.***, PID: 31596
    java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/ProtocolVersion;
        at ez.b(:[email protected]@14.7.99 (100408-223214910):3)
        at ey.a(:[email protected]@14.7.99 (100408-223214910):3)
        at fa.a(:[email protected]@14.7.99 (100408-223214910):15)
        at com.google.maps.api.android.lib6.drd.al.a(:[email protected]@14.7.99 (100408-223214910):6)
        at ed.a(:[email protected]@14.7.99 (100408-223214910):21)
        at ed.run(:[email protected]@14.7.99 (100408-223214910):8)
     Caused by: java.lang.ClassNotFoundException: Didn't find class "org.apache.http.ProtocolVersion" on path: DexPathList[[zip file "/data/user_de/0/com.google.android.gms/app_chimera/m/0000006a/MapsDynamite.apk"],nativeLibraryDirectories=[/data/user_de/0/com.google.android.gms/app_chimera/m/0000006a/MapsDynamite.apk!/lib/arm64-v8a, /system/lib64, /system/vendor/lib64]]
        at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:134)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
        at ad.loadClass(:[email protected]@14.7.99 (100408-223214910):4)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
        at ez.b(:[email protected]@14.7.99 (100408-223214910):3) 
        at ey.a(:[email protected]@14.7.99 (100408-223214910):3) 
        at fa.a(:[email protected]@14.7.99 (100408-223214910):15) 
        at com.google.maps.api.android.lib6.drd.al.a(:[email protected]@14.7.99 (100408-223214910):6) 
        at ed.a(:[email protected]@14.7.99 (100408-223214910):21) 
        at ed.run(:[email protected]@14.7.99 (100408-223214910):8) 

이 문제인데, 이 문제에 대해 제기된 이슈가 구글쪽 이슈 트래커에 존재하나 wont fix라 적혀있었다.

즉, 이 행동은 정상 행동으로 AndroidManifest.xml 의 application 태그 내부에 아래 코드를 적어주면 된다는 것이다.

  <uses-library
      android:name="org.apache.http.legacy"
      android:required="false" />

다만, Apache Http가 Android 6.0 부터 삭제되었음에도 불구하고 아직까지 사용하는지는 아직도 의문이긴 하다.

추가 1. Volley

Google 의 Http 통신 라이브러리인 Volley도 같은 문제가 발생하는 것 같다. 마찬가지로 해결 방법은 위와 같다.

Nextcloud in Docker – files not showing

언젠가 갑자기 Docker로 호스팅중이었던 NextCloud에 접속했을 때, 처음 DB 설정 화면이 그대로 뜨는 일이 있었다.

바로 다시 설정하고 들어갔을 때, 파일이 보이지 않았는데 실제로 nextcloud 컨테이너 안에서는 내용을 확인할 수 있었다.

이 문제에 대해 구글링을 시도한 결과 occ란 커맨드로 실행할 수 있는 것 같았다.

OCC란?

OCC는 NextCloud의 전신이었던 OwnCloud에 Console를 더한 약자로 NextCloud를 관리하기 위한 커맨드 라인 인터페이스라고 보면 된다. 호스팅하는 컨테이너의 /var/www/html 에 위치하고 있으며, 아래와 같은 명령어로 실행한다.

sudo -u www-data php occ -V

그리고 이 문제를 해결하기 위해 사용하는 파일 스캔 명령어는 다음과 같다.

sudo -u www-data nextcloud php occ files:scan --all

문제는 도커로 호스팅중이었기 때문에, 컨테이너 내에서 작업할 수 없고 외부에서 작업해야 한다.

최종 명령어 & 실행 결과

도커를 통해 실행하려면 아래 명령어를 사용하면 된다.

docker exec -u www-data nextcloud php occ files:scan --all

쉽게 보면 sudo 대신 docker exec 를 붙이는 것으로 해결이 가능한 셈이다.

위 명령어를 사용하면, 아래와 같은 출력이 나오면서 스캔이 완료된다.

Run Docker + Jenkins for Android Build

이번 글에서는 Vultr VC2 2core 4GB instance 에 Jenkins 를 올려 안드로이드 앱을 빌드하고 테스트하려 한다.

사용할 인스턴스는 기존에 사용중이던 Artifcatory 인스턴스이지만 사양을 올려 사용할 것이기 때문에, Docker 기본 설정 같은 것들은 이전 글인 ‘Upload Android Library into Gradle with Artifactory‘ 를 참조하면 된다.

DockerFile 커스텀하기

대부분 인터넷에 나온 CI/CD 적용기를 보면 이 단계부터 설명하는 글이 많은데, 처음 도커를 접하는 유저라면 상당히 골치아픈 작업이기도 하다. Dockerfile 자체가 자체 포맷으로 되어있기도 해서 그걸 익혀야 되는 문제점이 있다.

그리고 이번 글의 중점 취지는 아닌 것 같아 제작했던 Dockerfile 를 공유해서 바로 적용할 수 있게 했고, 그것이 WindSekirun/Jenkins-Android-Docker 이다.

DockerHub에도 공유되어 있으니, 바로 pulling 를 받으면 최신 안드로이드 환경 (API 28 + build tools 28.0.3) 을 사용할 수 있게 된다.

따라서 이번 글에서는 미리 제작된 도커 이미지로 이 과정을 대체한다.

먼저 실행되야 될 작업

본격적으로 이미지를 풀링 받기 전에, 이 이미지를 실행하기 위한 커맨드를 살펴볼 필요가 있다. (버전 1.0.1 같은 경우 2018-12-18 기준 최신으로, 가장 최신은 릴리즈 페이지를 참고하면 된다.)

sudo docker run -d -p 8080:8080 -p 50000:50000 -v /data/jenkins-android-docker:/var/jenkins_home windsekirun/jenkins-android-docker:1.0.1

여기에서 -d (백그라운드 작업) 과 -p(포트 바인딩) 은 넘겨도 되나 /data/jenkins-andorid-docker:/var/jenkins_home 부분에 신경을 써야 한다. 이 부분은 ‘실제 저장소내 공간:도커 컨테이너 공간’ 의 형식을 가지고 있는 디렉토리 바인딩 부분으로, 실제 저장소내 공간 부분에 작성한 폴더는 실제 존재하는 폴더여야 한다.

따라서 sudo 권한으로 아래 커맨드를 실행하면 된다.

mkdir /data/jenkins-android-docker
sudo chown -R 1000:1000 /data/jenkins-android-docker

이 ‘실제 저장소내 공간’에 모든 데이터가 들어가므로, 도커 컨테이너를 지워도 이 폴더가 남아있다면 데이터 또한 그대로 보존되게 된다.

위 mkdir 와 chown 을 시작했다면, 맨 위의 shell script 를 실행하면 된다.

귀찮은 사람들은 WindSekirun/Jenkins-Android-Docker 를 VPS 안에서 clone 받아서 sudo sh runImage.sh 를 실행하면 된다. mkdir 부터 docker run 까지 다 된다.

처음 관리자 설정하기

명령어로 도커를 시작했다면, 서버 주소:8080 으로 들어가면 Jenkins home 이 보일 것이다.

Jenkins 를 설정하는 사람이 관리자인지 확인하는 과정인데, 이를 확인하기 위해서는 도커 컨테이너에 접근할 필요가 있다.

SSH 에서 docker container ls 를 입력하면 현재 실행중인 컨테이너 정보가 나오는데, 그 곳에서 Jenkins-Android-Docker 를 찾는다.

위 정보에 따르면 jenkins-android-docker 가 설치된 컨테이너의 id는 d88376885153 이고, 이 컨테이너 id를 이용해 도커 컨테이너의 bash로 접근할 수 있다. 명령어는 docker exec -i -t [컨테이너 id] /bin/bash이다.

bash에 접근했으면 cat /var/jenkins_home/secrets/initialAdminPassword 로 도커 초기 관리자 비밀번호를 알아낸다.

아래 비밀번호를 복사해서 칸에 넣고 Continue 를 누르면 된다.

그 다음 Plugin 창이 나올텐데, 그 곳에서 Install Suggested Plugin를 누른다. 차후에 다시 설치가 가능하니, 지금은 기본만 설치한다.

설치가 다 되고, 관리자 계정을 만들면 Jenkins 를 사용할 준비가 모두 끝난다. 이제 blueocean 플러그인을 설치하여 첫 안드로이드 빌드를 해보도록 한다.

Blueocean 설치하기

Blueocean 은 Jenkins 에서 나온 새 UI/UX 툴로, 기존 Jenkins 가 다소 전문가의 영역에 가깝다고 하면 Blueocean 는 이를 좀 더 간결하고 알아보기 쉽게 만든 것이다.

Jenkins 메인에 접속되면 Manage Jenkins > Manage Plugin > Available 의 검색창에서 Blueocean 을 검색한다.

여기에서 Install without restart 를 누르면 플러그인 설치 페이지로 이동하는데, 여기에서 맨 마지막의 체크박스를 체크해서 바로 재시작될 수 있도록 한다.

여기까지 끝내면 첫 안드로이드 프로젝트를 빌드를 할 모든 준비가 완료된다.

프로젝트 빌드하기

다시 젠킨스 메인으로 돌아와서 옆의 Open Blueocean 을 누른다. 그러면 이제까지 보지 못했던 새로운 Jenkins 가 보이게 된다.

여기에서 ‘Create a new Pipeline’ 를 누른다.

프로젝트 저장소를 선택하고, 가져올 프로젝트를 맨 밑에서 설정한다. 만일 해당 프로젝트에 Jenkinsfile 가 없다면 설정하는 메뉴로 갈 것이고, 이미 있다면 바로 빌드를 시도할 것이다. 이번에는 Jenkins 로 연동해보지 않은 프로젝트를 설정했다.

그러면 이 페이지로 나오게 될텐데, 이 곳이 Jenkins 가 한 빌드당 거칠 파이프라인을 설정하는 곳이다. 가운데의 +를 누르게 되면 새 작업을 추가할 수 있다.


일단 여기에서는 간단히 빌드만 성공하는지 테스트할 것이므로, Add step 에서 Shell script 를 선택하고 ./gradlew assembleDebug --stacktrace를 입력해준다.

그 다음 위 Save를 누르면 파이프라인 저장 다이얼로그가 표시되고, Save&run 을 누르면 바로 빌드가 시작된다.

빌드 지켜보기

이제 프로젝트가 빌드될 때 까지 기다리는 것 만 남았다.

만일 오류가 나온다면 왜 오류가 나오는지 이제 구글링을 열심히 해볼 차례다. 아래는 지금까지 겪은 CI 오류를 정리해본 것이다.

흔한 오류

local.properties (No such file or directory)

assembleDebug 전에 ‘echo “sdk.dir=/opt/android-sdk-linux” >> local.properties’ 를 추가한다. 또는 젠킨스 관리 > Configure System > 맨 하단의 Android SDK Path 에 /opt/android-sdk-linux 를 적어준다.

File google-services.json is missing.

이 글을 참고하되 Environment Variable 를 Jenkins 내부에서 설정해주면 된다.

그리고 추가할 Shell script 는 echo $GOOGLE_SERVICES_JSON | base64 --decode --ignore-garbage > /app/google-services.json“` 이다.

Gradle build daemon disappeared unexpectedly

제일 골치아픈 문제로, 서버의 램 용량이 부족해서 Gradle 데몬이 죽는 현상이다. 이를 해결하기 위해서는 빌드 커맨드를 ./gradlew --no-daemon assembleDebug --stacktrace 로 설정하거나, 아니면 아예 메모리 제한을 거는 방법도 있다.

메모리 제한을 거는 방법은 현재 실행중인 컨테이너를 docker container kill [컨테이너 id]  – docker container stop [컨테이너 id] 로 삭제하고 (데이터는 상기했듯이 남아있다.) 컨테이너 실행할 때 -m 2500m 를 삽입한다. 2500m은 2.5g로 k, m, g 가 사용이 가능하다. 자신의 서버 환경에 맞게 적절히 조정하면 된다.

예제: sudo docker run -d -m 2500m -p 8080:8080 -p 50000:50000 -v /data/jenkins-android-docker:/var/jenkins_home windsekirun/jenkins-android-docker:1.0.1

빌드 성공

CI상 오류를 전부 해결하면 빌드 성공이 나오며, 이제야 첫 프로젝트의 빌드가 끝난 셈이다. 이제 다른 프로젝트를 연동하거나, 좀 더 심화해서 유닛 테스트나 마켓 업로드 기능 들을 구현하면 된다.

마지막으로 위 프로젝트의 빌드에 성공한 JenkinsFile는 다음과 같다. 다른 프로젝트의 루트 폴더에 똑같은 파일 이름으로 만들고 Jenkins 에서 추가하면 바로 인식이 된다.

pipeline {
  agent any
  stages {
    stage('Make Environment') {
      parallel {
        stage('Touch local.properties') {
          steps {
            sh 'echo "sdk.dir=/opt/android-sdk-linux" >> local.properties'
          }
        }
        stage('Touch google-services.json') {
          steps {
            sh 'echo $GOOGLE_SERVICES_JSON | base64 --decode --ignore-garbage > demo/google-services.json'
          }
        }
        stage('Display directory') {
          steps {
            sh 'ls -la'
          }
        }
      }
    }
    stage('assembleDebug') {
      steps {
        sh './gradlew --no-daemon assembleDebug --stacktrace'
      }
    }
  }
  environment {
    GOOGLE_SERVICES_JSON = ''
  }
}