안드로이드의 BuildConfig 활용

안드로이드는 앱을 빌드하는 과정에서 여러 파일을 자동으로 생성한다. BuildConfig 클래스도 그 중 하나로, 현재 빌드에 관한 정보를 상수 필드로 갖고 있어 코드에서 직접 참고할 수 있다.

package me.sunphiz.kotlin.myapplication;

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "me.sunphiz.kotlin.myapplication";
  public static final String BUILD_TYPE = "debug";
  public static final String FLAVOR = "";
  public static final int VERSION_CODE = 1;
  public static final String VERSION_NAME = "1.0";
}
if( BuildConfig.DEBUG )
{
    // 내 코드
}

파일은 /build/generated/source/buildConfig/.. 이하에 위치하고, <application id>.BuildConfig 의 이름을 갖는다.

기본 상수 외에 build.gradle를 통해 내가 필요한 필드를 직접 추가할 수도 있다. 이 상수는 빌드 타입에 따라 buildType 뿐 아니라 defaultConfig, productFlavors에도 선언이 가능하다. buildConfig를 여러 개 선언하다보면 번거로워진다. 이 때 Groovy의 맵 자료 구조 iteration을 이용하면, 아래처럼 buildConfig 값을 맵 형식으로 따로 정리할 수 있다.

ext {
    extraStringBuildConfig = [ NAME : '"Dog발자"' ]
    extraBooleanBuildConfig = [ KOREAN : 'true' ]
}

android {
    defaultConfig {
        ...

        extraStringBuildConfig.each{ k, v ->
            buildConfigField 'String', k, v
        }

        extraBooleanBuildConfig.each{ k, v ->
            buildConfigField 'boolean', k, v
        }
    }
    ...
}

그 밖에

참조

Glide 이미지 라이브러리 커스터마이징

Glide의 커스터마이징은 공식 문서(영문)에 더 자세히 설명되어 있다. 내게 필요한 기능이 무엇인지 참고하고, 더 자세한 내용은 꼭 공식 문서를 보자.

Glide는 Fresco, Picasso, Universal Image Loader 등과 함께 안드로이드에서 손 꼽히는 이미지 라이브러리다. Glide를 이용하면 원격지(remote)/로컬(local) 이미지를 가져와 화면에 표시할 수 있다. 가져온 이미지는 캐싱을 통해 로딩 속도를 개선할 수 있으며, 필요하다면 이미지 모서리 깎기(rounding)같은 재처리와 애니메이티드 gif도 표시할 수 있다. 따로 HTTP 라이브러리를 쓰지 않고 동작하지만, Volley나 OkHttp 라이브러리와도 호환 동작할 수 있다. 이제는 사라진 범프(Bump) 앱에서 사용할 목적으로 개발/적용한 것이 이 라이브러리의 시작으로 알려져 있다.

Glide의 사용법은 쉬울 뿐 아니라 공식 문서(영문)가 예시와 함께 친절하게 작성되어 있다. 그러므로, 여기서는 기본적인 사용방법 보다는 Glide의 AppGlideModule를 통해 Glide의 동작을 커스터마이징을 할 수 있는 고급 옵션들에 대해 살펴보자.

Glide의 고급 설정(configuration)을 위해서는 준비가 필요하다.

  1. AppGlideModule 추상클래스의 구현체 추가
  2. @GlideModule 어노테이션을 구현체에 선언
  3. 구현체 컴파일을 위해 build.gradle에 Glide 어노테이션 프로세서 추가
  4. 프로가드 설정에 AppGlideModule 클래스의 -keep 선언 추가

준비가 끝나면, 아래와 같이 구현체의 applyOptions() 안에 고급 설정을 추가할 수 있다.

@GlideModule
public class YourGlideModule extends AppGlideModule
{
    @Override
    public void applyOptions( Context _context, GlideBuilder _builder )
    {
        // 고급 설정 추가
    }
}

메모리 캐시

GlideBuilder.setMemoryCache()를 통해, 메모리 캐시 크기를 정할 수 있다. 캐싱할 화면의 갯수를 정수로 지정하거나 직접 캐시 크기를 지정할 수 있다. 화면의 갯수로 지정할 때는  앱의 가장 긴 화면을 염두하여 지정하면 같은 이미지를 원격지에서 반복적으로 로딩하지 않고 캐싱할 수 있을 것이다.

비트맵 풀

GlideBuilder.setBitmapPoolScreens()를 통해, 비트맵 캐시 크기를 정할 수 있다. 정하는 방법은 메모리 캐시와 유사하다.

디스크 캐시

GlideBuilder.setDiskCache()를 통해, 디스크 캐시를 위한 폴더 이름과 디스크 캐시 폴더의 최대 용량를 정할 수 있다. 폴더의 기본 이름은 ‘glide’, 용량은 250mb이다.

리퀘스트 옵션 기본 값 변경

GlideBuilder.setDefaultRequestOptions()에 RequestOptions 객체를 전달하는 방식으로 변경할 수 있다. Glide.with() 메소드에서 반환하는 RequestManager.apply()를 통해 매 이미지 요청마다 설정할 수도 있지만, 여기서 설정하면 모든 요청의 기본 값으로 적용된다.

이미지 저장 방식(원본만, 재처리 후 사본 만, 모두)이나, 출처(원격지만, 로컬만, 모두), 이미지의 decode 포멧 등을 명시적으로 선언할 수 있다.

로그

GlideBuilder.setLogLevel()을 통해, 로그 레벨을 조절할 수 있다. 개발할 때는 Log.DEBUG, 공개할 때는 Log.ERROR 정도가 적당해 보인다.

모든 설정을 추가한다면, 아래와 같은 코드가 될 수 있다.

@GlideModule
public class YourGlideModule extends AppGlideModule 
{
    @Override
    public void applyOptions( Context _context, GlideBuilder _builder )
    {
        // 메모리 캐시
        MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context).setMemoryCacheScreens(2).build();
        _builder.setMemoryCache(new LruResourceCache(calculator.getMemoryCacheSize()));

        // 비트맵 풀
        int bitmapPoolSizeBytes = 1024 * 1024 * 30; // 30mb
        _builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));

        // 디스크 캐시
        int diskCacheSizeBytes = 1024 * 1024 * 100; // 100 MB
        _builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "cacheFolderName", diskCacheSizeBytes));

        // 리퀘스트 옵션 기본 값
        _builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.RGB_565).disallowHardwareBitmaps());

        // 로그 레벨
        _builder.setLogLevel(Log.DEBUG);
    }
}

그 밖에

참조

Volley의 Apache HTTP client 라이브러리 참조 제거

Volley는 2013년 구글에서 공개한 안드로이드 네트워크 라이브러리다. 구글 플레이 스토어 팀에서 개발/적용 후 공개한 것이 Volley의 시작으로 알려져 있다. 지금은 인기가 예전만 못하지만, 공개 당시에는 가장 좋은 네트워크 라이브러리 중 하나였다. Volley에 대한 자세한 소개는 공식 트레이닝 사이트(한글)를 참조하자.

공식 Apache HTTP client 라이브러리(이하, 라이브러리)는 자바 진영의 대표적인 HTTP 라이브러리로, 수많은 개발자를 네트워크와 관련된 혼란에서 구제했다. 안드로이드도 이 라이브러리를 이식해주어, 안드로이드 앱 개발자들에게 추가적인 학습 없이도 네트워크 관련 기능을 쉽게 쓰도록 해주었다.

다만, 지금와서 보면 원래 라이브러리의 어느 버전을 이식한 것인지 불분명하고 원래 라이브러리는 오래 전에 Apache HttpComponents로 이름까지 바뀌었다. 배터리와 데이터 사용량 등에 민감하고 네트워크 상황이 빈번히 바뀌는 모바일 환경의 특징도 충분히 반영하지 못한 것 같다. 정확한 이유는 찾지 못했으나 개인적으로 폭발적인 안드로이드 성장세에 최대한 빨리 HTTP 관련 라이브러리 지원을 하고자 했던 당시 상황이 영향을 준 것 같다. 이와 관련하여, 안드로이드의 HTTP 라이브러리 비교분석 글을 읽어보면 좋다.

앞서 언급한 이유인지 모르지만, 안드로이드는 M(6.0)과 P(9.0) 두 차례에 걸쳐 이 라이브러리 지원 종료를 예고했다. 지원 제거가 곧 사용 불가를 의미하는 것은 아니어서, M 이상은 build.gradleP 이상은 AndroidManifest.xml 에 명시적 선언을 하면 여전히 사용할 수 있다. 하지만, 안드로이드는 기능적으로 개선된 HTTPUrlConnection 사용을 권장한다.

이런 안드로이드의 정책에 맞춰 Volley도 minSdkVersion이 9 이상이면 v1.1.0부터 라이브러리를 참조하지 않는다. 참조할  필요가 없으니 명시적 선언도 제거할 수 있다. Volley를 사용하고 있다면 최신버전을 사용하고 라이브러리 참조도 제거해보자.

그 밖에

  • Volley는 장기적으로 조건부 참조 코드도 제거할 것이라 한다. Volley 라이브러리 업데이트가 예전만큼 활발하지 않아 언제 반영될지는 알 수 없지만 말이다.

참조

Volley와 Stetho로 네트워크 디버깅

Stetho는 2015년 페이스북에서 공개한 안드로이드 디버깅 도구다. 크롬 브라우저에 내장된 개발자 도구인 DevTools를 활용해 안드로이드 앱의 네트워크, 데이터베이스, 프리퍼런스, 레이아웃 등을 디버깅 할 수 있다. 더 자세한 기능 소개는 공식 페이지를 참조하자.

네트워크 라이브러리와 연계 동작하는 라이브러리들은 보통 Volley를 지원한다. 하지만, Stetho의 네트워크 디버깅 기능은 앱이 OkHttp나 안드로이드의 HttpUrlConnection를 써야 쓸 수 있다. 환경이 맞다면 좋겠지만, 이미 개발된 앱의 네트워크 라이브러리를 Stetho 쓰자고 바꾸기는 어렵다. 같은 고민인지, 아래처럼 Volly의 RequestQueue 객체 생성 시에 OkHttp 라이브러리의 특정 클래스 객체만 활용하는 우회법을 제안하는 글이 많다.

OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());
mRequestQueue = Volley.newRequestQueue(getApplicationContext(), new OkHttpStack(client));

물론 네트워크 라이브러리를 바꾸는 것에 비해 수정은 적지만, 쓰지 않아도 되는 라이브러리를 추가하는건 여전하다. 이 같은 고민을 한다면 포기하기 전에 여기를 살펴보자. Stetho의 UrlConnectionManager를 활용하는 기능이 포함된 StethoVolleyHurlStack 클래스를 제공하여, OkHttp를 사용하지 않고도 Stetho의 네트워크 디버깅을 사용하도록 도와준다.

RequestQueue queue = Volley.newRequestQueue(this, new StethoVolleyHurlStack());

2020년 1월 테스트로는 잘 되지만, 1년 넘게 commit이 없고 pull-request도 처리 안하는 것으로 미루어 보아 유지보수가 중단된 것 같다.

그 밖에

  • 데이터베이스, 프리퍼런스, 레이아웃 디버깅과 같은 다른 기능들은 위와 같은 수정 없이 Stetho 라이브러리 적용만으로 쓸 수 있다.

참고

The user 12345 does not meet the requirements to access device identifiers

안드로이드는 휴대폰을 구분할 수 있는 변경 불가능한 식별자(이하 편의상 UID, Unique identifier)를 제공해 왔다. 공식 문서에 언급된 API들은 아래와 같다.

Build나 TelephonyManager 클래스를 통해 제공되는 이 기능이 안드로이드 10(API29 혹은 Q OS)부터 개인정보 보호 강화 정책에 따라 추가 제약을 받는다고 한다. targetSdkVersion 기준 28까지는 READ_PHONE_STATE 권한을 가진 앱은 UID를 읽을 수 있으나, 29부터 READ_PRIVILEGED_PHONE_STATE 권한이 있어야 한다. 그러므로, TelephonyManager.getDeviceId() API를 각각 O OS와 Q OS에서 호출한다면 아래와 같이 동작할 것이다.

O OS + READ_PHONE_STATE 권한이 없을 때

Caused by: java.lang.SecurityException: getDeviceId: uid 10237 does not have android.permission.READ_PHONE_STATE.

O OS + READ_PHONE_STATE 권한이 있을 때

2019-12-31 14:59:56.528 32231-32231/me.sunphiz.android.myapplication2 D/sunphiz: deviceId= 230064929080355

Q OS + READ_PHONE_STATE 권한이 없을 때

Caused by: java.lang.SecurityException: getDeviceId: The user 10265 does not meet the requirements to access device identifiers.

Q OS + READ_PHONE_STATE 권한이 있을 때

2019-12-31 14:54:39.741 5812-5812/me.sunphiz.android.myapplication2 D/sunphiz: deviceId= null

Q OS + READ_PRIVILEGED_PHONE_STATE 권한이 있을 때

2019-12-31 14:59:56.528 32231-32231/me.sunphiz.android.myapplication2 D/sunphiz: deviceId= 230064929080355

그럼 READ_PRIVILEGED_PHONE_STATE 권한은 어떻게 얻을 수 있을까? 결론만 이야기하면, 다운로드 앱(구글 플레이 스토어 등에서 다운받아 설치한 앱)은 저 권한을 얻을 방법이 없다. 저 퍼미션이 선언된 시스템의 AndroidManifest.xml을 보면, protectionLevel이 signatureOrSystem이기 때문이다. 이 조건은 단말 제조사만 충족할 수 있으므로, 다운로드 앱은 targetSdkVersion 29부터 UID에 접근할 수 없다는 결론이 된다.

그럼 어떻게하란 말인가? UID를 이용하던 기능이 있다면, 아래 방법 중 하나를 적용해보자.

  1. targetSdkVersion을 28이하로 유지
  2. 광고 및 사용자 분석을 위한 고유 식별자‘로 대체 고려
  3. Android 식별자 사용 권장사항‘을 보고 기능 변경

그 밖에

UID는 변경이 불가능한 값이라 편리하다. 동시에, 내 서비스를 통해 값이 유출된다면 의도치 않은 사용자 피해가 생긴다. 조금 번거롭더라도 더 안전한 방향으로 개선하자.

참조