안드로이드 앱 개발에는 다양한 요소들이 복잡하게 얽혀있다.
그 중에서도 특히 빌드 구성
은 앱의 성능과 안정성 그리고 다양한 버전 관리등에
굉장히 중요한 역할을 한다.
이번 글에서는 안드로이드에서 앱의 빌드 구성에 대해 알아보려고 한다.
안드로이드의 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"
}
}
위와같은 설정들은 기본 설정이며, 빌드유형
에 따라서도 오버라이드를 통해 다른 설정
값을 지정할 수 있다.
안드로이드 앱을 빌드할 때는 기본적으로 debug
와 release
두 가지 빌드 유형이 제공된다.
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)을 위한 별도의 빌드 유형을 생성하여 빌드타입을 구성할 수 있다.
새로운 빌드 유형에서 설정값을 변경하면 해당 빌드 유형은 변경된 설정값을 사용하게 된다.
<기본 빌드유형> <qa 빌드 유형>
com.example.myapp -> com.example.myapp.qa
위에서 본 빌드타입과는 별개로 제품 버전
을 다르게
설정할 수 있다.
예를 들어 무료 버전과 유료 버전을 나누거나, 여러 디바이스 또는 서비스에 특화된 버전을 만들 수 있다.
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
는 productFlavors
를 그룹화하고 관리하는데 사용되는 개념이다. 위의 예에서 flavorDimensions의 version은 free
와 paid
를 그룹화하고 있다.
productFlavors
내에서도 applicationIdSuffix
를 사용해 정의할 수 있으며,
만약 buildTypes
와 productFlavors
에서 둘 다 사용하는 경우
Gradle에서는 flavor 다음에 빌드 유형
(flavor + build Type)을 구성한다.
Debug 타입의 free flavor의 application ID
=> com.example.myapp.free.debug
if (BuildConfig.isFree) {
// 광고 ON (무료버전에 대한 처리)
} else {
// 광고 OFF (유료버전에 대한 처리)
}
gradle 빌드 시스템에서 사용되는 메소드로, BuildConfig.java 파일에 새로운 필드를 추가하는 역할을 한다.
빌드 타입
이나 빌드 변형
에 따라 코드에서 이용할 상수
를 정의할 수 있다.
free, paid 라는 두 가지의 flavor에 대해 isFree
라는 boolean타입의 필드를 추가로 정의하였다.
이렇게 설정한 필드는 앱 소스코드에서 BuildConfig
클래스를 통해 접근이 가능하며, 이를 통해 빌드 시간에 설정한 값에 따라 코드를 분기 처리할 수 있다.
val appName = getString(R.string.app_name)
resValue 또한 @string/app_name
으로 접근하여 해당 값을 사용할 수 있는데, 플레이버에 따라 다른 리소스 값을 사용하고 싶을 때 유용하다.
Gradle이 생성하는 빌드 변형의 개수는 빌드유형 수 * 버전의 수
이다.
빌드 유형은 release
, debug
, qa
3개
플레이버는 free
, paid
2개
총 6개의 빌드 변형이 생기게 된다.
빌드 변형을 생성할 때는 flavor 이름이 먼저 오고 이후 빌드 타입이 붙는다.
프로젝트의 소스코드와 리소스파일이 위치하는 디렉토리를 정의하는데에 사용한다.
sourceSets
를 사용하여 구성요소별로 소스코드
혹은 리소스파일
경로를 수정할 수 있다.
main : Main Source Set는 앱의 기본 소스코드와 리소스를 포함한다.
androidTest : 앱의 UI및 기능 테스트에 사용되는 코드와 리소스를 포함
이외 : 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
에 따라 소스코드나 리소스 혹은 매니페스트 경로를 변경하여 지정할 수 있다.
freeDebug
, freeRelease
에서만 사용) main
과 동일하게 쓰되 리소스 파일 경로 따로 지정 (paid 플레이버에 속한 빌드 변형(paidDebug
, paidRelease
)에서만 사용)이렇게 구성하면 각 빌드 변형에 따라 필요한 코드와 리소스를 효율적으로 관리할 수 있다.
main
소스세트는 앱의 기본 소스코드와 리소스를 포함하므로, 다른 빌드타입이나 플레이버에 대한 소스세트를 정의하더라도 항상 사용
된다.
따라서 main
소스세트를 포함해 다른 빌드 변형에 속한 소스 세트 디렉토리에 동일한 kotlin
또는 자바 클래스
가 정의되어 있으면 빌드 오류
가 발생한다는 것이다.
예를들어 free
앱을 빌드할 때 src/free/MainActivity.kt
와 src/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">
병합된 매니페스트 결과
참고