[안드로이드] 빌드 구성의 이해 (defaultConfig, buildType, flavor)

Janzizu·2023년 11월 13일
1
post-thumbnail

안드로이드 앱 개발에는 다양한 요소들이 복잡하게 얽혀있다.
그 중에서도 특히 빌드 구성은 앱의 성능과 안정성 그리고 다양한 버전 관리등에
굉장히 중요한 역할을 한다.
이번 글에서는 안드로이드에서 앱의 빌드 구성에 대해 알아보려고 한다.

Default Config

안드로이드의 build.gradle(app) 파일 내부를 살펴보면 android{} 블록 안에 defaultConfig가 위치해있는 것을 확인할 수 있다.
defaultConfig는 앱의 기본 설정을 정의하는 요소중 하나로, manifest파일에 대한 정보를 제공한다.
이름 그대로 default이기 때문에 모든 빌드 유형에 공통적으로 적용된다.

보통 아래와 같이 사용한다.

// in build.gradle(app)
android {
    defaultConfig {
        applicationId "com.example.myapp"
        minSdkVersion 16
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"
    }
}
  • applicationId : 앱의 고유한 식별자인 패키지 이름 지정
  • minSdkVersion : 앱이 지원하는 최소 Android API레벨 지정
  • targetSdkVersion : 앱이 목표로 하는 Android API레벨 지정
  • versionCode : 앱의 빌드번호 (앱을 업데이트할 때 사용되며, 업데이트된 앱을 더 높은 versionCode로 제출해야 함)
  • versionName : 앱의 버전 이름 (사용자에게 표시되는 것)

위와같은 설정들은 기본 설정이며, 빌드유형에 따라서도 오버라이드를 통해 다른 설정값을 지정할 수 있다.

Build Type

안드로이드 앱을 빌드할 때는 기본적으로 debugrelease 두 가지 빌드 유형이 제공된다.
build.gradle(app) 을 보면 아래와 같이 빌드 유형별로 각기 다른 설정을 해줄 수 있다.

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
        
    debug {
        minifyEnabled false
    }
}

debug빌드 유형은 개발 중에 코드를 테스트하고 디버깅할 때 주로 사용되며,
release빌드 유형은 앱을 실제로 배포할 때 사용된다.

// qa 빌드 유형에 대해 설정값을 바꾼 예시
buildTypes {
	qa {
      applicationIdSuffix ".qa"
      minSdkVersion 15
        // QA 특화 설정
        ...
    }
}

디버그와 릴리즈 외에도, 예를 들어 테스트 환경(QA)을 위한 별도의 빌드 유형을 생성하여 빌드타입을 구성할 수 있다.
새로운 빌드 유형에서 설정값을 변경하면 해당 빌드 유형은 변경된 설정값을 사용하게 된다.

  • applicationIdSuffix는 패키지 이름에 추가적인 접미사를 붙여 고유한 패키지 이름을 생성하는 데 사용된다.
   <기본 빌드유형>        <qa 빌드 유형>
com.example.myapp -> com.example.myapp.qa

Product Flavor

위에서 본 빌드타입과는 별개로 제품 버전다르게 설정할 수 있다.
예를 들어 무료 버전과 유료 버전을 나누거나, 여러 디바이스 또는 서비스에 특화된 버전을 만들 수 있다.

product flavor는 모듈 수준의 build.gradle에 위치해있으며 기본 구조는 아래
와 같다.

flavorDimensions "version"
productFlavors {
	free {
    	dimension "version"
    	applicationIdSuffix ".free"    
    	versionCode 10
        versionName "1.0.0"
        buildConfigField 'boolean', 'isFree', "true"
        
    }
    
    paid {
    	dimension "version"
        applicationIdSuffix ".paid"		
        versionCode 20
        versionName "20.0.0
        buildConfigField 'boolean', 'isFree', "false"
        resValue 'string', 'app_name', "My App Free"
    }
}
  • flavorDimensions

flavorDimensionsproductFlavors를 그룹화하고 관리하는데 사용되는 개념이다. 위의 예에서 flavorDimensions의 version은 freepaid를 그룹화하고 있다.

  • applicationIdSuffix

productFlavors내에서도 applicationIdSuffix를 사용해 정의할 수 있으며,
만약 buildTypesproductFlavors에서 둘 다 사용하는 경우
Gradle에서는 flavor 다음에 빌드 유형 (flavor + build Type)을 구성한다.

Debug 타입의 free flavor의 application ID 

=> com.example.myapp.free.debug
  • buildConfigField
if (BuildConfig.isFree) {
	// 광고 ON (무료버전에 대한 처리)
} else {
	// 광고 OFF (유료버전에 대한 처리)
}

gradle 빌드 시스템에서 사용되는 메소드로, BuildConfig.java 파일에 새로운 필드를 추가하는 역할을 한다.
빌드 타입이나 빌드 변형에 따라 코드에서 이용할 상수를 정의할 수 있다.

free, paid 라는 두 가지의 flavor에 대해 isFree라는 boolean타입의 필드를 추가로 정의하였다.
이렇게 설정한 필드는 앱 소스코드에서 BuildConfig클래스를 통해 접근이 가능하며, 이를 통해 빌드 시간에 설정한 값에 따라 코드를 분기 처리할 수 있다.

  • resValue
val appName = getString(R.string.app_name)

resValue 또한 @string/app_name으로 접근하여 해당 값을 사용할 수 있는데, 플레이버에 따라 다른 리소스 값을 사용하고 싶을 때 유용하다.

BuildTypes & ProductFlavors

Gradle이 생성하는 빌드 변형의 개수는 빌드유형 수 * 버전의 수 이다.

빌드 유형은 release, debug, qa 3개
플레이버는 free, paid 2개
총 6개의 빌드 변형이 생기게 된다.

빌드 변형을 생성할 때는 flavor 이름이 먼저 오고 이후 빌드 타입이 붙는다.

Source set

프로젝트의 소스코드와 리소스파일이 위치하는 디렉토리를 정의하는데에 사용한다.
sourceSets를 사용하여 구성요소별로 소스코드 혹은 리소스파일 경로를 수정할 수 있다.

  • main : Main Source Set는 앱의 기본 소스코드와 리소스를 포함한다.

    • src/main/java : 소스코드
    • src/main/res : 리소스 파일
  • androidTest : 앱의 UI및 기능 테스트에 사용되는 코드와 리소스를 포함

    • src/androidTest/java : 테스트 코드 위치
    • src/androidTest/res : 테스트에 사용되는 리소스 파일
  • 이외 : main외에도 빌드타입과 flavor에 따라 다르게 사용되는 소스 세트를 정의할 수 있다.

sourceSets {
    free {
        res.srcDirs = ['src/free/res']
        java.srcDirs = ['src/free/java']
        manifest.srcFile 'src/free/AndroidManifest.xml'
    }
    
    paid {
    	res.srcDirs = ['src/paid/res']
    }
    
    main {
        res.srcDirs = ['src/main/res']
        java.srcDirs = ['src/main/java']
        manifest.srcFile 'src/main/AndroidManifest.xml'
    }
}

위와같이 flavor에 따라 소스코드나 리소스 혹은 매니페스트 경로를 변경하여 지정할 수 있다.

  • free : 리소스, 소스코드, 매니페스트 모두 별개의 경로에 지정 (free 플레이버에 속한 빌드 변형(freeDebug, freeRelease에서만 사용)
  • paid : 소스코드, 매니페스트는 디폴트 main과 동일하게 쓰되 리소스 파일 경로 따로 지정 (paid 플레이버에 속한 빌드 변형(paidDebug, paidRelease)에서만 사용)
  • main : 가장 기본이 되는 경로. 모든 빌드 변형에서 공통으로 사용

이렇게 구성하면 각 빌드 변형에 따라 필요한 코드와 리소스를 효율적으로 관리할 수 있다.

소스코드

  • 디렉터리에 있는 모든 소스 코드가 함께 컴파일 되어 단일 출력 생성

main소스세트는 앱의 기본 소스코드와 리소스를 포함하므로, 다른 빌드타입이나 플레이버에 대한 소스세트를 정의하더라도 항상 사용된다.
따라서 main소스세트를 포함해 다른 빌드 변형에 속한 소스 세트 디렉토리에 동일한 kotlin 또는 자바 클래스가 정의되어 있으면 빌드 오류가 발생한다는 것이다.

예를들어 free앱을 빌드할 때 src/free/MainActivity.ktsrc/main/MainActivity.kt 를 중복으로 정의할 수 없다.

-> 중복 클래스 오류 발생.
-> 서로 클래스명을 다르게 하거나, 패키지명을 다르게 하여 정의해야한다.

서로 다른 소스 세트동일한 파일서로 다른 버전을 포함하는 경우
우선 순위는 아래와 같다.

빌드 변형 (src/freeDebug/)
빌드 유형 (src/debug/)
제품 버전 (src/free/)
기본 소스 세트 (src/main/)
라이브러리 종속 (기본 소스세트에서 공통으로 사용하는 라이브러리)

freeDebug' 빌드 변형을 빌드할 때는
'src/freeDebug/'
'src/debug/'
'src/free/'
'src/main/' 순서로 파일을 찾아 사용하게 된다.

매니페스트

  • 소스 코드와 마찬가지로 여러 매니페스트(모든 소스 코드의 ) 가 단일 매니페스트로 함께 병합된다.

( 3개의 매니페스트 파일을 우선순위가 낮은 파일에서 높은 파일로 병합하는 과정)

  • <manifest> 요소의 속성은 병합되지 않음.
    우선순위가 가장 높은 매니페스트의 속성만 사용

  • <uses-feature><uses-library> 요소의 android:required 속성은 or병합을 사용함.
    충돌이 있으면 true가 적용됨.

  • <uses-sdk>요소의 속성은 아래의 상황을 제외하고 항상 우선순위가 높은 매니페스트 값 사용

    • 우선순위가 낮은 매니페스트에 더 높은 minsdk가 있다면 오류 발생

    • 우선순위가 낮은 매니페스트에 더 낮은 targetSdkVersion값이 있으면,
      병합도구는 우선순위가 높은 매니페스트의 값을 사용하며 라이브러리가 작동하는지 확인하는데 필요한 모든 시스템 권한을 추가함.

      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.example.myapp">
      
       <uses-permission android:name="android.permission.CAMERA" />
       <uses-sdk android:targetSdkVersion="23" />
      
       <application>
           <!-- ... -->
       </application>
      </manifest>

      (1) 우선순위가 낮은 매니페스트

      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.example.myapp">
      
       <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
       <uses-sdk android:targetSdkVersion="26" />
      
       <application>
           <!-- ... -->
       </application>
      </manifest>

      (2) 우선순위가 높은 매니페스트

      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.example.myapp">
      
       <uses-permission android:name="android.permission.CAMERA" />
       <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
       <uses-sdk android:targetSdkVersion="26" />
      
       <application>
           <!-- ... -->
       </application>
      </manifest>

      병합된 매니페스트에는 두 매니페스트의 내용이 모두 포함되며, 우선순위가 높은 free/manifest의 값을 사용한다.
      따라서 병합된 매니페스트에는 카메라 권한과 외부 저장소 쓰기 권한이 모두 포함되어 있고, targetSdkVersion은 26으로 설정된다.

  • <intent-filter>요소는 매니페스트 간에 일치하지 않으며, 각 각 고유하게 취급된다. 병합된 매니페스트의 공통 상위요소에 추가됨.

    • main/manifest에서 MainActivity.kt를 런처(홈)로 하는 인텐트 필터가 있고, free/manifest에서 FreeActivity.kt를 런처로 하는 인텐트 필터를 추가했다. 매니페스트는 어떻게 될까?

       <activity
              android:name="MainActivity"
              android:exported="true">
              <intent-filter>
                  <action android:name="android.intent.action.MAIN" />
                  <category android:name="android.intent.category.DEFAULT" />
                  <category android:name="android.intent.category.LAUNCHER" />
                  <category android:name="android.intent.category.HOME" />
              </intent-filter>
      
              <meta-data
                  android:name="android.app.lib_name"
                  android:value="123" />
          </activity>	

      (1) main/manifest 내의 MainActivity

              <activity
              android:name="FreeActivity"
              android:exported="false">
              <intent-filter>
                  <action android:name="android.intent.action.MAIN" />
                  <category android:name="android.intent.category.DEFAULT" />
                  <category android:name="android.intent.category.LAUNCHER" />
                  <category android:name="android.intent.category.HOME" />
              </intent-filter>
      
              <meta-data
                  android:name="android.app.lib_name"
                  android:value="12345" />
          </activity>

      (2) free/mainfest 내의 FreeActivity

      매니페스트 병합의 결과는 다음과 같다.

      두 액티비티가 매니페스트에 각 각 등록되기 때문에 앱을 실행했을 때,
      사용자 선택 다이얼로그 창이 뜨게 된다.

      이것은 매니페스트 병합을 통해 해결할 수 있다.

      (+ 참고로 매니페스트 병합은 Merged Manifest에서 볼 수 있음)

병합 규칙 마커

매니페스트 충돌에 대해 해결할 수 있는 속성이다.
두 개 이상의 매니페스트 파일을 병합할 때, 병합 도구는 우선순위가 높은 매니페스트 파일에서 이 마커를 찾는다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp"
    xmlns:tools="http://schemas.android.com/tools">

tools 네임스페이스를 선언해주어야 한다.

노드마커

  • tools:node="merge" : 모든 속성과 모든 중첩된 요소를 병합

    <activity android:name="com.example.ActivityOne"
       android:windowSoftInputMode="stateUnchanged">
       <intent-filter>
           <action android:name="android.intent.action.SEND" />
           <category android:name="android.intent.category.DEFAULT" />
       </intent-filter>
    </activity>

    우선순위가 낮은 매니페스트

    <activity android:name="com.example.ActivityOne"
       android:screenOrientation="portrait"
       tools:node="merge">
    </activity>

    우선순위가 높은 매니페스트

    <activity android:name="com.example.ActivityOne"
       android:screenOrientation="portrait"
       android:windowSoftInputMode="stateUnchanged">
       <intent-filter>
           <action android:name="android.intent.action.SEND" />
           <category android:name="android.intent.category.DEFAULT" />
       </intent-filter>
    </activity>

    병합된 매니페스트 결과

  • tools:node="merge-only-attributes" : 속성만 병합 (중첩된 요소 x)

    <activity android:name="com.example.ActivityOne"
       android:windowSoftInputMode="stateUnchanged">
       <intent-filter>
           <action android:name="android.intent.action.SEND" />
           <data android:type="image/*" />
           <category android:name="android.intent.category.DEFAULT" />
       </intent-filter>
    </activity>

    우선순위가 낮은 매니페스트

    <activity android:name="com.example.ActivityOne"
       android:screenOrientation="portrait"
       tools:node="merge-only-attributes">
    </activity>

    우선순위가 높은 매니페스트

    <activity android:name="com.example.ActivityOne"
       android:screenOrientation="portrait"
       android:windowSoftInputMode="stateUnchanged">
    </activity>

    병합된 매니페스트 결과

  • tools:node="remove" : 병합된 매니페스트에서 요소를 제거

    <activity-alias android:name="com.example.alias">
     <meta-data android:name="cow"
         android:value="@string/moo"/>
     <meta-data android:name="duck"
         android:value="@string/quack"/>
    </activity-alias>

    우선순위가 낮은 매니페스트

    <activity-alias android:name="com.example.alias">
     <meta-data android:name="cow"
         tools:node="remove"/>
    </activity-alias>

    우선순위가 높은 매니페스트

    <activity-alias android:name="com.example.alias">
     <meta-data android:name="duck"
         android:value="@string/quack"/>
    </activity-alias>

    병합된 매니페스트 결과

  • tools:node="removeAll" : 동일한 상위 요소 내에서 모든 요소 삭제

    <activity-alias android:name="com.example.alias">
     <meta-data android:name="cow"
         android:value="@string/moo"/>
     <meta-data android:name="duck"
         android:value="@string/quack"/>
    </activity-alias>

    우선순위가 낮은 매니페스트

    <activity-alias android:name="com.example.alias">
     <meta-data tools:node="removeAll"/>
    </activity-alias>

    우선순위가 높은 매니페스트

    <activity-alias android:name="com.example.alias">
    </activity-alias>

    병합된 매니페스트 결과

  • tools:node="replace" : 우선순위가 낮은 요소를 완전히 대체함

    <activity-alias android:name="com.example.alias">
     <meta-data android:name="cow"
         android:value="@string/moo"/>
     <meta-data android:name="duck"
         android:value="@string/quack"/>
    </activity-alias>

    우선순위가 낮은 매니페스트

    <activity-alias android:name="com.example.alias"
       tools:node="replace">
     <meta-data android:name="fox"
         android:value="@string/dingeringeding"/>
    </activity-alias>

    우선순위가 높은 매니페스트

    <activity-alias android:name="com.example.alias">
     <meta-data android:name="fox"
         android:value="@string/dingeringeding"/>
    </activity-alias>

    병합된 매니페스트 결과

    속성 마커

    노드마커 에서 모든 요소에 적용한것과 달리, 특정 속성에만 병합 규칙을 적용할 때 사용한다. 이러한 속성마커들은 특정 빌드 변형 또는 플레이버에만 적용되어야 하는 설정이나 요소를 다룰 때 유용하게 사용될 수 있다.

    노드마커 에서처럼 remove, replace등이 있다.

  • tools:remove="attr, ..."
    위 속성마커는 특정 속성을 제거하도록 지시하는 역할을 하는데, 이는 해당 속성이
    병합 과정에서 완전히 제거되어 최종 매니페스트에 포함되지 않도록 하는 것이다.
    이를 통해 특정 빌드 변형 또는 플레이버에서만 필요한 속성을 제거하거나,
    특정 속성이 다른 빌드 설정에서 충돌을 일으키는 것을 방지할 수 있다.

    <activity android:name="com.example.ActivityOne"
       android:windowSoftInputMode="stateUnchanged">

    우선순위가 낮은 매니페스트

    <activity android:name="com.example.ActivityOne"
       android:screenOrientation="portrait"
       tools:remove="android:windowSoftInputMode">

    우선순위가 높은 매니페스트

    <activity android:name="com.example.ActivityOne"
       android:screenOrientation="portrait">

    병합된 매니페스트 결과

    병합 정책

참고

빌드 구성
빌드변형구성
매니페스트 관리

0개의 댓글

관련 채용 정보