Computer >> 컴퓨터 >  >> 시스템 >> Android

다중 라이브러리 안드로이드 프로젝트 완벽 가이드: 로컬·원격 개발 모범 사례

이 글에서는 안드로이드에서의 다중 라이브러리(multi-library) 프로젝트를 다뤄보려 합니다. 흔한 주제는 아니지만, 그렇다고 특별히 낯선 것도 아닙니다.

업무를 하다 보면 다중 라이브러리 프로젝트를 접할 일이 있거나, 더 나은 구조와 체계를 위해 기존 라이브러리를 여러 하위 모듈로 분리하는 것을 고민해 본 적이 있을 것입니다. 어떤 경우든, 본격적으로 시작하기 전에 앞으로 마주하게 될 상황을 미리 파악해 두는 것이 중요합니다.

안드로이드 라이브러리 개발의 기본 구조

안드로이드에서 직접 라이브러리를 만드는 일은 매우 의미 있는 경험입니다. 다른 개발자들(혹은 자신 자신)에게 도움이 되는 코드를 작성할 기회를 얻게 되니까요.

라이브러리는 단독 프로젝트로 존재할 수 없기 때문에, 일반적으로 하나의 프로젝트 안에서 애플리케이션과 짝을 이루어 함께 관리됩니다. 덕분에 기능을 추가하거나 버그를 수정한 뒤 프로젝트 내 앱에서 바로 테스트할 수 있어, 라이브러리 개발이 매우 간단한 과정이 됩니다. 이는 곧 실제 개발자가 라이브러리를 통합하는 과정을 로컬 환경에서 그대로 시뮬레이션하는 셈입니다.

그런데 만약 내가 만든 라이브러리가 내가 동시에 개발 중인 또 다른 라이브러리에 의존하고 있다면 어떻게 될까요?

아직 모르셨다면 알아두어야 할 사실이 있습니다. 라이브러리(AAR) 내부에는 다른 로컬 라이브러리를 포함시킬 수 없습니다. 원격 저장소에 배포된 라이브러리는 의존성(dependency) 형태로 참조할 수 있지만, 로컬에 있는 것은 안 됩니다.

안드로이드는 이를 공식적으로 지원하지 않으며, 지난 몇 년간 FatAar 같은 우회 방법들이 등장했지만 문제를 완전히 해결하지 못했고 현재는 유지보수되지 않고 있습니다. Google Issue Tracker에도 해당 기능 요청이 오래전부터 등록되어 커뮤니티의 많은 관심을 받고 있습니다. 하지만 지금부터 우리가 극복할 수 있는 벽과 그럴 수 없는 벽을 하나씩 살펴보겠습니다.

프로젝트 구조가 다음과 같다고 가정해 봅시다.

-- App
|
-- OuterLib
|
--- InnerLib

InnerLib는 원래 프로젝트의 일부가 될 수 없다면 어디에 두어야 할까요? 그리고 InnerLib 내부의 기능을 개발하면서 로컬 환경에서 작업하려면 어떻게 해야 할까요?

이제부터 이 질문들에 대한 답을 찾아보겠습니다.

Git 서브모듈(Submodule)

대부분의 기술적 문제에는 정답이 하나만 있는 경우가 드뭅니다. 보통 여러 해결책이 존재하고, 각각의 장단점이 있습니다. 결국 핵심은 어떤 단점을 감수하는 것이 나에게 맞는가입니다.

첫 번째 질문인 "InnerLib는 어디에 위치해야 하는가"에 대해 선택할 수 있는 방법은 크게 두 가지입니다.

  1. InnerLib를 원래 프로젝트의 서브모듈(submodule)로 만들기
  2. InnerLib를 별도 저장소에 두고 원격 의존성으로 관리하기

Git 서브모듈에 익숙하지 않다면 Git 공식 문서가 좋은 출발점입니다. 문서의 한 구절을 인용하면 다음과 같습니다.

한 프로젝트를 진행하다 보면 그 안에서 다른 프로젝트를 사용해야 할 때가 종종 있습니다. 👉 서드파티가 만든 라이브러리일 수도 있고, 여러 부모 프로젝트에서 공통으로 사용하기 위해 별도로 개발 중인 라이브러리일 수도 있습니다. 👈 이런 상황에서 공통적인 문제가 발생합니다. 두 프로젝트는 독립적으로 관리하면서도, 동시에 한쪽에서 다른 쪽을 사용할 수 있어야 한다는 것입니다.

바로 우리의 사용 사례와 정확히 일치하는 상황입니다. 서브모듈을 사용하면 모든 코드가 한곳에 모여 관리가 쉽고, 로컬 개발도 편하다는 장점이 있습니다.

하지만 서브모듈에는 몇 가지 약점이 있습니다. 대표적으로 서브모듈이 어떤 브랜치를 가리키고 있는지 항상 신경 써야 한다는 점입니다. 메인 저장소에서는 릴리스 브랜치에 있는데 서브모듈은 기능(feature) 브랜치에 있다면? 이를 눈치채지 못한 채 릴리스한다면, 프로덕션에 올라갈 준비가 되지 않은 코드가 배포됩니다. 끔찍하죠.

이것을 여러 명이 함께 일하는 개발팀 환경에서 생각해 보세요. 무심코 저지른 실수 하나가 큰 비용으로 돌아올 수 있습니다.

첫 번째 방법이 부담스럽다면, 라이브러리를 별도 저장소에 호스팅하는 것이 두 번째 선택지입니다. 저장소 설정 자체는 간단하지만, 문제는 그다음 로컬에서 어떻게 작업하느냐입니다.

로컬 개발 환경 구성하기

프로젝트 설정이 끝나면 OuterLib의 build.gradle 파일에 다음과 같은 의존성 선언이 들어갑니다.

dependencies {
implementation 'url_to_remote_inner_lib_repository'
}

개발 사이클을 효율적으로 만들려면 어떻게 해야 할까요? InnerLib에서 새 기능을 개발했을 때 OuterLib나 애플리케이션에서 바로 테스트하려면?

먼저 떠오르는 방법은 InnerLib를 OuterLib 프로젝트에 로컬 모듈로 임포트하되, OuterLib의 .gitignore에 InnerLib를 등록하는 것입니다. Android Studio 왼쪽 메뉴에서 프로젝트명을 우클릭한 후 New → Module로 이동하면 손쉽게 할 수 있습니다.

다중 라이브러리 안드로이드 프로젝트 완벽 가이드: 로컬·원격 개발 모범 사례
모듈 임포트 방법 (1단계)

열리는 창에서 좌측 하단의 Import 옵션을 선택하면 됩니다.

다중 라이브러리 안드로이드 프로젝트 완벽 가이드: 로컬·원격 개발 모범 사례
모듈 임포트 방법 (2단계)

여기까지는 쉬워 보이지만, 함정이 있습니다.

InnerLib 파일을 수정해도 .gitignore 처리된 복사본에는 변경 사항이 반영되지 않습니다. 따라서 변경을 할 때마다 InnerLib 내부에서 작업한 후, OuterLib에 매번 다시 임포트해야 결과를 확인할 수 있습니다.

이건 뭔가 잘못됐습니다. 더 나은 방법이 분명 있을 것입니다.

settings.gradle로 파일 동기화하기

settings.gradle 파일에 몇 줄만 추가하면 InnerLib에서 발생한 변경 사항이 즉시 반영되도록 만들 수 있습니다.

모듈을 임포트할 때 Android Studio는 InnerLib의 복사본을 생성하여 캐싱합니다. 그렇기 때문에 변경할 때마다 재임포트가 필요했던 것입니다. projectDir 속성을 사용하면 Android Studio가 파일을 어디에서 참조할지 지정할 수 있습니다.

기존 settings.gradle은 다음과 같습니다.

include ':outerLib', ':innerLib', ':app'

InnerLib를 로컬에서 참조하도록 바꾸면 다음과 같습니다.

include ':outerLib', ':innerLib', ':app'
project(':innerLib').projectDir = new File('PATH_TO_INNER_LIB')

이 방식을 사용하면 InnerLib 파일이 실제 작업 디렉터리에 연결되므로, 변경 사항이 즉시 반영됩니다.

하지만 문제가 남아 있습니다. 평소에는 OuterLib 작업 시 InnerLib의 원격 버전을 사용하고 싶을 것입니다. 위처럼 settings.gradle에 직접 경로를 박아 넣으면 로컬 작업만 가능해지고, 당연히 이 상태 그대로 커밋하고 싶지는 않겠죠.

Maven Local 활용하기

위 방식이 마음에 들지 않는다면 다른 방법도 있습니다. 라이브러리를 Maven에 공개 배포하듯이, mavenLocal을 통해 로컬에도 배포할 수 있습니다. maven local은 개발 머신에 존재하는 로컬 저장소 집합입니다.

운영체제별 mavenLocal 경로는 다음과 같습니다.

  • Mac → /Users/YOUR_USERNAME/.m2
  • Linux → /home/YOUR_USERNAME/.m2
  • Windows → C:\Users\YOUR_USERNAME\.m2

핵심은 라이브러리를 로컬에 배포한 뒤 프로젝트에서 이를 참조하는 것입니다. 이렇게 하면 프로젝트를 InnerLib에 연결할 수 있습니다.

이 구성을 프로젝트에 적용하려면 다음 단계를 따릅니다.

  1. mavenLocal()을 repositories 절에 추가하여 프로젝트가 로컬 저장소를 검색할 수 있도록 합니다.
buildscript {
repositories {
mavenLocal()
}
}
...
allprojects {
repositories {
mavenLocal()
}
}
  1. dependencies 절의 implementation 라인을 원격 라이브러리를 참조하는 것처럼 InnerLib를 가리키도록 변경합니다.
  2. InnerLib를 로컬에 배포하기 위해 publishingLocally.gradle이라는 파일을 만들고 다음 내용을 작성합니다.
apply plugin: 'maven-publish'
project.afterEvaluate {
publishing {
publications {
library(MavenPublication) {
setGroupId groupId // 라이브러리 패키지명
setArtifactId artifactId
version versionName // 예: 1.0
artifact bundleDebugAar
pom.withXml {
def dependenciesNode = asNode().appendNode('dependencies')
def dependencyNode = dependenciesNode.appendNode('dependency')
dependencyNode.appendNode('groupId', 'your_group_id')
dependencyNode.appendNode('artifactId', 'your_artifact_id')
dependencyNode.appendNode('version', 'your_version')
}
}
}
}
}
  1. 애플리케이션 수준의 build.gradle 파일에 다음 라인을 추가합니다.
apply from: './publishingLocally.gradle'

이 방법이 너무 좋아 보린다면, 실제로 그렇습니다. 한편으로는 원격 라이브러리를 다루듯 로컬에서 매끄럽게 개발할 수 있지만, 다른 한편으로는 InnerLib에 변경이 생길 때마다 로컬에 다시 배포해야 합니다. 무거운 작업은 아니지만, 번거로운 작업을 계속 반복해야 한다는 점은 피할 수 없습니다.

로컬과 원격을 자유롭게 전환하는 최종 해결책

우리의 목표는 InnerLib를 수정할 때마다 재배포해야 하는 수고를 없애는 것입니다. 프로젝트가 변경 사항을 스스로 인식하도록 만들어야 합니다.

앞서 살펴본 settings.gradle 방식이 그 역할을 해주지만, 커밋 문제가 있었습니다. 이 문제를 해결해 로컬과 원격 모두에서 InnerLib를 유연하게 다루기 위해 gradle.properties 파일에 파라미터를 정의하는 방법을 사용합니다.

gradle.properties 파일은 개발 환경을 구성하는 프로젝트 수준 설정을 저장하는 곳입니다. 팀 내 모든 개발자가 일관된 개발 환경을 유지하도록 도와줍니다.

이 파일에서 익숙하게 본 설정으로는 AndroidX 지원(android.useAndroidX=true)이나 JVM 인수(org.gradle.jvmargs=-Xmx1536m) 등이 있습니다.

여기에 로컬 작업 여부를 나타내는 파라미터를 추가해 보겠습니다.

workingLocally = false

이 파라미터를 통해 로컬 환경에서 작업하는지, 프로덕션 코드로 작업하는지 구분할 수 있게 됩니다. 먼저 settings.gradle 내용을 파라미터 값이 true인지 확인하는 조건문으로 감싸겠습니다.

include ':outerLib', ':innerLib', ':app'
if (workingLocally.toBoolean()) {
project(':innerLib').projectDir = new File('PATH_TO_INNER_LIB')
}

이렇게 하면 파라미터가 true일 때만 프로젝트가 InnerLib 파일을 로컬 머신에서 가져옵니다.

다음으로 build.gradle 파일의 로직도 변경해야 합니다. dependencies 블록에서 라이브러리를 원격으로 가져오는 대신, 로컬 의존 여부에 따라 분기하도록 만들 수 있습니다.

dependencies {
if (workingLocally.toBoolean()) {
implementation project(':innerLib')
} else {
implementation 'url_to_remote_repository'
}
}

⚠️ 주의: 로컬 작업 중일 때는 gradle.properties 파일을 절대 커밋해서는 안 됩니다.

긴 여정이었고 다소 지칠 수도 있지만, 이제 다중 라이브러리 프로젝트에서 로컬과 원격을 자유롭게 오가며 개발할 수 있는 견고한 환경을 갖추게 되었습니다.

과정 중에 문제를 겪거나 다른 의견이 있다면 언제든 댓글로 남겨주세요.


freeCodeCamp의 오픈 소스 커리큘럼은 4만 명 이상의 사람들이 개발자로 취업하는 데 도움을 주었습니다. 무료로 코딩을 배워 보세요.