
Android Studio에서 Kotlin으로 팀원들과 함께 앱 개발 프로젝트를 진행하게 되었습니다. 팀원 모두가 앱 개발에 대한 지식이 부족한 상태이지만, 감사하게도 현직 개발자분을 멘토로 모시게 되어 함께 차근차근 공부해나가는 과정을 기록하게 되었습니다. 같은 궁금증을 가진 분들에게도 도움이 되길 바랍니다.
작성한 소스 코드와 의존성 라이브러리를 묶어서 APK, ABB와 같이 배포할 수 있는 형태로 패키징 해주는 빌드 배포 도구(build toolKit)입니다.
Gradle 파일은 Kotlin DSL(Domain Specific Language, 도메인 특화 언어)를 사용합니다. 이전에는 Groovy DSL을 기본으로 사용했습니다.
— Gradle 파일의 확장자는 .kts입니다.
그럼, Gradle 파일을 왜 사용하나요?
Android Studio는 코드 편집만 할 수 있는 IDE이기 때문입니다. 즉, 앱을 빌드하기 위해서는 별도의 빌드 배포 도구가 필요합니다.
→ 즉, Android Studio와 빌드 시스템이 서로 독립적으로 실행되는 것입니다.
프로그램을 생성하면 Gradle 파일도 같이 생성됩니다. 앱을 빌드하여 패키징하는 과정은 모두 Gradle이 담당하게 됩니다.
프로젝트에서 어떻게 소스 파일을 컴파일하고 패키징할지 정의하며, 안드로이드 애플리케이션이 정상적으로 실행될 수 있도록 다양한 라이브러리와 플러그인을 설정합니다.
그렇다면, 앱의 빌드 과정과 그 안에서 Gradle은 어떻게 동작할까요?
Gradle의 동작을 살피기 전, 안드로이드 앱의 빌드 프로세스를 간략하게 알고 넘어가면 좋습니다.
빌드 프로세스는 소스코드 → APK 파일 생성까지의 과정을 의미합니다.

위 이미지에서 점선으로 묶인 영역이 Gradle과 Android Plugin이 관리하는 부분입니다. 이를 기억하고 단계별로 살펴보겠습니다.
컴파일러는 소스코드를 Dex(Dalvik Executable) 파일로 변환합니다.
소스코드 → 바이트코드(중간 코드 형태) → Dex 파일
컴파일러가 소스코드를 Dex 파일로 변환하는 과정입니다.
여기서 Dex 파일은 Android Runtime에서 궁극적으로 실행되는 코드로, 여러 개의 클래스와 메서드 정보를 포함한 실행 파일입니다.
이때, 소스코드 뿐만 아니라 리소스도 AAPT(Android Asset Packaging Tool)에 의해 처리됩니다. 리소스는 XML 레이아웃 파일, 이미지, 문자열 등이 있을 것입니다.
APK Packager는 Dex 파일과 컴파일된 리소스를 하나의 APK로 결합합니다.
APK Packager는 디버그 또는 릴리즈 키 저장소를 사용하여 APK에 서명합니다.
안드로이드 애플리케이션을 설치하고 실행하려면 APK 파일에 반드시 서명이 되어 있어야 합니다. 서명은 앱의 진위성과 무결성을 보장하는 과정입니다.
빌드 프로세스가 끝나면 외부 사용자에게 배포, 테스트 또는 출시하는 데 사용할 수 있는 디버그/릴리즈된 APK 패키징이 완료됩니다.
여기까지 안드로이드 앱의 빌드 프로세스였습니다. 그렇다면 Gradle이 이 과정에서 어떻게 동작하는지를 알아 보겠습니다.
Gradle 빌드 실행 단계는 세 단계로 나뉩니다.

초기화 단계 (Initialization)
Gradle은 프로젝트의 구조와 모듈을 설정합니다. 이 과정에서 settings.gradle.kts 파일을 통해 여러 모듈이 있는지 확인하고, 빌드할 모듈과 그 환경을 설정합니다.
→ 이는 안드로이드 빌드 프로세스에서 각 모듈이 별도로 컴파일될 준비를 마치는 과정입니다. 실제 빌드가 진행되기 전에 이뤄집니다.
구성 단계 (Configuration)
모든 모듈의 build.gradle.kts 파일을 분석하고, 필요한 작업과 의존성을 설정합니다. 이 단계에서 빌드에 필요한 모든 작업들이 결정됩니다.
→ build.gradle.kts 파일에서 정의된 컴파일 SDK 버전, 의존성, 플러그인, 빌드 타입(디버그, 릴리즈) 등의 설정을 기반으로 빌드 그래프 생성합니다. 초기화 단계와 마찬가지로 이 단계 또한 실제 빌드가 일어난 것이 아닙니다.
ex) 컴파일 SDK 버전이 결정되고, 리소스 처리에 필요한 AAPT 도구가 설정됩니다.
의존성 관리란?
필요한 라이브러리나 외부 모듈이 이 단계에서 확인되고, Gradle이 자동으로 라이브러리를 다운로드하여 프로젝트에 포함합니다.
실행 단계 (Execution)
Gradle이 구성 단계에서 설정한 빌드 그래프에 따라 실제 빌드 작업을 실행합니다.
→ 소스 코드 컴파일, 리소스 처리, APK 패키징, APK 서명, 빌드 최적화 등의 단계를 수행합니다. 이는 안드로이드 앱 빌드 프로세스에서 Gradle이 개입한다고 언급했던 영역의 대부분이 실행 단계에서 이뤄짐을 알 수 있습니다.
의존성 관리 (Dependency Management)
implementation "com.squareup.retrofit2:retrofit:2.9.0"프로젝트 설정 및 구성
compileSdkVersion 34, applicationId "com.example.myapp”플러그인 관리
id("com.android.application"), id("kotlin-android")빌드 프로세스 자동화
assembleDebug, assembleRelease빌드 타입 및 플래버 관리
멀티모듈 프로젝트 지원
build.gradle.kts 파일 코드 내부 구조이제 실제 Gradle 파일 내부를 보겠습니다.
가장 먼저, Gradle 설정 파일은
의 두 가지로 나뉩니다.
└── MyApp/ # Project
├── gradle/
│ └── wrapper/
│ └── gradle-wrapper.properties
├── build.gradle(.kts) ← 프로젝트 루트
├── settings.gradle(.kts)
└── app/ # Module
│ ├── build.gradle(.kts) ← 모듈(앱)
│ ├── build/
│ ├── libs/
│ └── src/
│ └── main/ # Source set
│ ├── java/
│ │ └── com.example.myapp
│ ├── res/
│ │ ├── drawable/
│ │ ├── values/
│ │ └── ...
│ └── AndroidManifest.xml
두 가지 경로의 Gradle 파일의 차이는 해당 파일이 담당하는 범위입니다. 프로젝트 루트의 build.gradle.kts 파일에는 프로젝트의 모든 모듈에 공통적으로 적용되는 빌드 설정을 작성합니다. 그렇다면, 모듈 중에 하나인 앱에 대한 build.gradle.kts 파일에는 해당 모듈에서 적용되는 빌드 설정이 작성되겠죠?
조금 더 설명을 덧붙이자면 앱의 기능을 모듈화하여 개발하는 과정에서 여러 개의 모듈이 생겼을 때, 각 모듈 하위 디렉토리에는 별도의 build.gradle.kts 파일들이 생길 것입니다.
여기서 잠깐…
공식 문서에서 제공하는 예제 코드와 실제 Android Studio에서 프로젝트를 생성했을 때 기본 제공된 코드가 달랐습니다. 더 찾아보니, 이는 Gradle 설정 방식의 차이였습니다.
직접 플러그인 ID와 버전 지정
plugins {
id("com.android.application") version "8.6.0" apply false
id("com.android.library") version "8.6.0" apply false
id("org.jetbrains.kotlin.android") version "1.9.23" apply false
}
위는 공식 문서의 빌드 구성 가이드에서 제공하는 예제 코드입니다. 각 플러그인과 버전을 명시적으로 보여주는 방식입니다.
Gradle Version Catalog 사용
// 프로젝트 전체에 적용할 플러그인 정의
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
}
제가 Android Studio에서 프로젝트를 생성했을 때 제공된 코드입니다. 가장 먼저 눈에 띄는 변화는 플러그인과 버전에 대한 명시가 잘 보이지 않는다는 점입니다.
이는 Gradle 7.0 이상에서 도입된 Version Catalog 기능을 사용한 것입니다. 버전에 대한 정보는 libs.versions.toml 파일에 정의되어 있으며, 이 파일에 정의된 버전 카탈로그 항목을 참조하여 의존성을 관리하는 방식입니다.
플러그인을 참조할 때는 alias을 사용합니다.
| Version Catalog 방식 | 직접 버전 명시 방식 | |
|---|---|---|
| 장 | - 중앙 관리 : 의존성과 플러그인의 버전을 libs.versions.toml 파일에서 관리 → 모든 플러그인의 버전을 한 곳에서 관리할 수 있어 버전 관리 용이 | - 직관적 : 각 플러그인과 그 버전이 명확히 적혀있어, 버전 정보 용이 |
| 점 | - 유지보수성 : 프로젝트가 커질수록 유리. 특히 여러 모듈에서 같은 의존성을 사용할 때, 버전이 일관되게 관리됨 | - 작은 프로젝트 : 소규모 프로젝트나 모듈이 많지 않은 경우. 이 방법이 더 작관적이고 설정 간단 |
| 단점 | - 간접적 : 플러그인의 버전이 libs.versions.toml 파일에 정의되기 때문에 직접적인 버전 정보는 build.gradle.kts 파일에서 확인할 수 없음 (버전 확인 번거로움) | - 버전 관리 어려움 : 프로젝트가 커지거나 모듈이 많아질 경우, 각 모듈에서 같은 플러그인의 버전을 일관되게 유지하기 어려움 → 의존성을 업데이트할 때 각 모듈을 일일이 수정해야 할 수 있음 |
개발하면서 의존성 관리는 정말 중요한 부분입니다. 특히 회사 내에서 여러 사람이 함께 개발을 하고 기능을 추가하며 새로운 라이브러리를 도입할 때, 기존 라이브러리와의 호환성을 확인하는 작업은 어렵습니다.
과거에는 한땀 한땀 의존성을 맞췄지만 이제는 Version Catalog처럼 Android Studio 내에서 버전 관리 방법을 제공해주기 때문에 조금 더 용이해진 것 같습니다. 또한, 작은 규모에서는 직접 버전 명시 방식을 사용하는 경우도 있습니다.
약간 샛길로 빠졌지만 다시 build.gradle.kts 파일로 돌아오겠습니다.
// 플러그인
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.jetbrains.kotlin.android)
}
// Android 설정
android {
namespace = "com.example.myapplication" // 앱의 고유한 패키지 이름
compileSdk = 34 // 앱을 컴파일할 때 사용하는 android SDK 버전 // 설정에 따라 미치는 영향
defaultConfig { // 앱의 기본 구성 값 정의
applicationId = "com.example.myapplication" // 앱의 고유 식별자 (Play 스토어에 배포 시 다른 앱과 구별할 수 있도록 함)
minSdk = 24 // 앱이 실행될 수 있는 최소 android 버전
targetSdk = 34 // 앱이 최적화된 android 버전 // 설정에 따라 미치는 영향
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes { // debug와 release 타입
release { // ProGuard 등의 최적화를 설정 // 왜 설정 해야되는지
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
}
// 이 모듈에서 사용할 외부 라이브러리와 의존성을 정의
dependencies {
implementation(libs.androidx.core.ktx) // implementation = 일반적인 의존성 추가
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit) // testImplementation = 유닛 테스트를 위한 의존성 추가
androidTestImplementation(libs.androidx.junit) // androidTestImplementation = 안드로이드 테스트를 위한 의존성 추가
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
여기까지 Gradle의 개요를 다뤘습니다.
이제 Gradle에 대해 개발 과정에서 알고 있으면 좋을 부분들을 간략하게 짚어 보겠습니다! 아래의 내용에서 키워드를 추출해 더 자세한 내용을 찾아보시면 좋겠습니다.
협업 환경에서 Gradle을 사용하는 경우, 프로젝트의 의존성 버전, 플러그인 버전, SDK 버전 등을 일관성 있게 관리하는 것이 중요합니다.
이를 위해 공통의 버전 관리 전략을 설정해야 합니다. 이를 통해 모든 개발자가 동일한 환경에서 작업할 수 있습니다.
compileSdkVersion, minSdkVersion, targetSdkVersion을 동일하게 맞춰야 합니다.버전 충돌을 방지하기 위해 가능한 최신의 안정적인 버전을 사용하는 것이 좋습니다. 또한, 대규모 프로젝트일수록 버전 고정이나 호환성 테스트가 필요할 수 있습니다.
Kotlin DSL에서 버전 관리를 효과적으로 하기 위해 buildSrc 디렉토리를 활용할 수 있습니다.
buildSrc를 활용한 의존성 버전 관리// buildSrc/src/main/kotlin/Dependencies.kt
object Versions {
const val kotlin = "1.8.0"
const val retrofit = "2.9.0"
const val coroutines = "1.6.0"
}
object Deps {
const val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib:${Versions.kotlin}"
const val retrofit = "com.squareup.retrofit2:retrofit:${Versions.retrofit}"
const val coroutines = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.coroutines}"
}
// app/build.gradle.kts
dependencies {
implementation(Deps.kotlinStdLib)
implementation(Deps.retrofit)
implementation(Deps.coroutines)
}
→ 의존성 버전 변경 시 buildSrc의 하나의 파일만 수정하면 전체 프로젝트에서 버전을 일관성 있게 관리할 수 있습니다.
Build Variants는 안드로이드 프로젝트에서 서로 다른 빌드 설정(예: debug, release 또는 특정 기능별 빌드)을 구성할 수 있는 기능입니다.
서로 다른 API 키, 리소스, 코드 등을 사용해 여러 가지 버전의 앱을 빌드할 수 있습니다.
flavors : free, paid, ...build-type : debug, release, alpa, beta, ...android {
...
// 빌드 타입 설정
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
debug {
isDebuggable = true
}
}
// 제품 플래버 설정
productFlavors {
free {
applicationIdSuffix = ".free"
versionNameSuffix = "-free"
}
paid {
applicationIdSuffix = ".paid"
versionNameSuffix = "-paid"
}
}
// 빌드 타입과 제품 플래버 조합 (Build Variants)
flavorDimensions("version")
}
위 예시에서는 debug, release 빌드 타입과 free, paid 플래버를 설정하여 총 4가지 빌드 버전(debugFree, releaseFree, debugPaid, releasePaid)을 만들 수 있습니다.
Gradle에서 테스트 환경을 설정하여 단위 테스트(Unit Test)와 UI 테스트를 쉽게 할 수 있습니다.
코드의 개별 단위(주로 함수나 메서드)가 기대한 대로 동작하는지 확인하는 테스트입니다. 아래는 단위 테스트를 지원하는 Kotlin 메서드 5가지입니다.
JUnit
@Test 주석을 추가하여 테스트 작성// build.gradle.kts
dependencies {
testImplementation("junit:junit:4.13.2")
// JUnit 5를 사용하려면 아래를 추가
testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0")
}
// 테스트 코드
import org.junit.Assert.*
import org.junit.Test
class CalculatorTest {
@Test
fun addition_isCorrect() {
val result = 2 + 3
assertEquals(5, result)
// assertEquals(expected. actual) : 두 값이 동일한지 확인 (동일 ? 테스트 성공 : 실패)
}
@Test
fun subtraction_isCorrect() {
val result = 5 - 3
assertEquals(2, result)
}
}
Kotlin Test
// build.gradle.kts
dependencies {
testImplementation(kotlin("test")) // 코틀린 테스트 의존성 추가
testImplementation(kotlin("test-junit")) // JUnit과 연동
}
// 테스트 코드
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue
class SampleTest {
@Test
fun testStringLength() {
val str = "Kotlin"
assertEquals(6, str.length)
}
@Test
fun testStringContains() {
val str = "Kotlin"
assertTrue(str.contains("Kot"))
// 조건이 참인지 확인
}
}
MockK
// build.gradle.kts
dependencies {
testImplementation("io.mockk:mockk:1.13.2") // MockK 의존성 추가
}
// 테스트 코드
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlin.test.Test
import kotlin.test.assertEquals
class UserServiceTest {
@Test
fun testGetUserName() {
// UserRepository 모킹
val mockRepository = mockk<UserRepository>()
// 특정 조건에서 가짜 데이터가 반환되도록 설정
every { mockRepository.getUserName(1) } returns "John Doe"
val userService = UserService(mockRepository)
val result = userService.getUserName(1)
assertEquals("John Doe", result)
verify { mockRepository.getUserName(1) } // 메서드 호출 여부 검증
}
}
class UserService(private val repository: UserRepository) {
fun getUserName(id: Int): String {
return repository.getUserName(id)
}
}
interface UserRepository {
fun getUserName(id: Int): String
}
Spek (Behavior-Driven Development Framework)
// build.gradle.kts
dependencies {
testImplementation("org.spekframework.spek2:spek-dsl-jvm:2.0.17") // Spek DSL 의존성 추가
testRuntimeOnly("org.spekframework.spek2:spek-runner-junit5:2.0.17") // Spek 테스트 실행기 (JUnit 5 사용)
}
// 테스트 코드
import org.spekframework.spek2.Spek
import org.spekframework.spek2.style.specification.describe
import kotlin.test.assertEquals
object CalculatorSpec : Spek({
describe("a calculator") { // given
val calculator = Calculator()
context("addition") { // when
it("should return the sum of two numbers") { // then
assertEquals(5, calculator.add(2, 3))
}
}
context("subtraction") {
it("should return the difference of two numbers") {
assertEquals(2, calculator.subtract(5, 3))
}
}
}
})
class Calculator {
fun add(a: Int, b: Int): Int = a + b
fun subtract(a: Int, b: Int): Int = a - b
}
Robolectric
// build.gradle.kts
dependencies {
testImplementation("org.robolectric:robolectric:4.9") // Robolectric 의존성 추가
}
// 테스트 코드
import android.app.Activity
import android.widget.TextView
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import kotlin.test.assertEquals
// Robolectric을 사용하여 안드로이드 컴포넌트를 모킹해 테스트를 실행하는 테스트 러너
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28]) // 테스트할 SDK 버전
class MainActivityTest {
@Test
fun testActivityCreation() {
// 안드로이드 Activity를 테스트 환경에서 빌드하여 생성하는 메서드
val activity = Robolectric.buildActivity(MainActivity::class.java).create().get()
val textView = activity.findViewById<TextView>(R.id.textView)
assertEquals("Hello, World!", textView.text)
}
}
// MainActivity 예제
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = TextView(this)
textView.text = "Hello, World!"
setContentView(textView)
}
}
앱의 UI 요소에 대한 테스트를 실행하며 올바르게 작동하는지 확인합니다.
ex) 버튼 클릭, 사용자 입력에 대한 응답 등
Espresso
// build.gradle.kts
dependencies {
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
}
// MainActivity에서 버튼을 눌렀을 때, TestView의 텍스트가 변경되는 시나리오를 테스트해보자
// MainActivityTest.kt
package com.example.myapp
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
// 액티비티를 실행하는 Rule 설정 (MainActivity 실행 및 테스트 중 관리)
@get:Rule
var activityScenarioRule = ActivityScenarioRule(MainActivity::class.java)
@Test
fun testButtonClickChangesText() {
// 버튼을 찾아서 클릭하는 동작 수행
onView(withId(R.id.button)).perform(click())
// textView를 찾아서 텍스트가 변경되었는지 확인
onView(withId(R.id.textView)).check(matches(withText("Hello, Espresso!")))
}
}
Gradle에 대해 가볍게 공부해보려고 하다가 가볍지 않다는 사실을 깨달았습니다. Gradle의 동작 방식을 이해하기 위해서는 안드로이드 앱 빌드 프로세스를 알아야 하는 것처럼 말입니다. 이를 주제로 각 잡고 공부해 본 것은 처음이었지만, 알아야 할 부분에 대한 멘토님의 제안 덕분에 공부의 방향을 잡기 수월했던 것 같습니다.
부족한 내용과 지식이지만 방향을 잡고 키워드를 찾아 깊이를 더해가는 공부를 해야겠다고 마음을 먹으면서 이번 주제를 마무리하겠습니다.
실제로 세팅하고 개발하는 과정에 많은 문제가 발생하겠죠? 😂
참고 자료
https://developer.android.com/build?hl=ko
https://patrick-dev.tistory.com/m/53
https://velog.io/@mong7399/Android-Studio-Gradle-%EC%9D%B4%EB%9E%80
https://docs.gradle.org/current/userguide/build_lifecycle.html