개요

 

Gradle 을 사용하다가 보면 처음에 가장 이해하기 어려운 것이 dependency 입니다. 

 

특히 UE4 를 사용해서 cross-platform 작업을 하시는 분들은 AutomationTool 을 사용하고 있기 때문에 내부가 어떻게 돌아가고 있는지 잘 모르는 경향이 있습니다. 사실 저도 XBox 와 PS4 쪽 작업을 할 때는 그런 고민을 별로 안 했습니다.

 

그 플랫폼들은 자체 포맷으로 packaging 하는 것을 제외하고는 Neighborhood 와 같은 도구를 통해 쉽게 deploy 할 수 있도록 해 줍니다. 뭔가 추가적인 모듈을 사용하고 싶다면 이미 배포된 SDK/XDK 의 것을 가져다 쓰면 됩니다. 즉 C++ 코드 내에서 작업이 수행됩니다.

 

문제는 Android 입니다( IOS 는 제가 잘 모르겠습니다 ). 일단 Android 에서는 프로그램 실행을 위한 기본 언어가 Java 입니다. Native C++ 을 사용할 수 있도록 NDK( Native Development Kit ) 를 제공하고 있지만, 이건 그냥 라이브러리라고 생각하시면 됩니다. 물론 몇 가지 디버깅 툴이 있기는 하지만 이 문서의 주제와는 관련이 없으므로 그냥 넘어 가도록 하겠습니다. 어쨌든 UE4 에서 작업해서 나온 "libUE4.so" 파일을 사용하기 위해서는 JNI( Java Native Interface )를 통해서 Java 코드와 연관시켜야 합니다.

 

사실 이런 모든 작업은 AutomationTool 을 통해서 수행됩니다. 하지만! BUT! 본인이 UE4 에서 제공하는 모듈 이외의 것을 사용하려고 한다면, 그때부터는 지옥이 열립니다.

 

저 스스로에 대해서 한탄스럽게 생각하는 거긴 하지만, 일반적으로 Windwos 개발자들은 MAKE, CMake, Ant, Gradle 과 같은 도구 사용에 익숙하지 못합니다. 그런 게 존재하는지도 모르는 경우가 많죠.

 

우리는 Microsoft 의 Visual Studio 는 Windows 플랫폼에서 개발하기 위한 기본 설정들을 모두 해 줍니다. 하지만 외부 라이브러리 소스들을 받으면 조금 어렵습니다. "*.sln" 이나 ".vcxproj" 같은 구성 파일들이 없으면 힘들죠. 이 상황에서 매우 쉬운 길을 택하면 그냥 자기가 가지고 있던 프로젝트에 전부 포함시켜 버립니다. 그나마 좀 깔끔하게 라이브러리로 사용하겠다는 분들은 직접 프로젝트를 만들어서 넣습니다.

 

그런데 수작업하는 것도 규모가 작은 소스를 사용할 때의 이야기지 엄청난 규모의 소스를 사용하고 있고, 그 소스들이 내부적으로 서로를 DLL 로 참조해야 하는 제약이 있다거나 하면 '멘붕'에 빠지게 됩니다.

 

이를 좀 더 쉽게 할 수 있도록 해 주는 것이 CMake 입니다. 하지만 "알아야 면장이라도 한다"는 속담이 있듯이, 내가 원하는 대로 설정하려면 최소한의 기본지식은 있어야 하는데 지금까지 경험해 보지 못한 용어와 구조때문에 머리가 아픕니다. Visual Studio 에서는 NMAKE 프로젝트라는 것이 나오기도 했습니다. 현재 UE4 빌드시스템이 NMAKE 를 사용하고 있죠.

 

그나마 Windows 플랫폼에서 C++ 작업을 좀 해 보신 분들이라면 NMake 에는 익숙할 겁니다. 하지만 Ant, Gradle 은 신세계입니다. 기본적으로 Java 를 위한 프로젝트를 구성하고 컴파일, 배포까지 수행합니다. 뭔가 하는 일이 어마어마하게 많죠.

 

Windows 가 개발자에게 매력있게 다가올 수 있는 이유는 엄청난 자동화와 매우 편리한 IDE 일 것입니다. 하지만 오픈소스 프로젝트들은 누가 본격적으로 관리하지 않는 오픈소스답게 초심자들에게는 불편한 점이 많습니다. 그래서 Ant, Gradle 같은 자동화 도구들을 만드는 것 같은데, Visual Studio 를 사용하는 입장에서는 매우 불편하게 느껴지는 것이 현실입니다. 그리고 Windows 플랫폼에서 응용프로그램 개발을 하는 분들이라면 C# 이나 C++ 를 사용하지, Java 는 거의 사용하지 않을 겁니다( 물론 사용하는 분들이 있기는 하겠지만 적어도 게임업계에서는 그렇습니다 ).

 

서론이 길었는데요, 어쨌든 UE4 에서 Android 개발을 하는데, 뭔가 커스텀한 일을 수행하고자 한다면, Java 와 Gradle 은 필수라는 겁니다. 예전에는 Ant 가 기본 빌드 시스템이었는데, 최근에 Gradle 로 갈아탔다고 하더군요.

 

최근에 회사에서 Gradle 에 대해서 분석하고 해 봤는데, 제가 형식에만 집중하고 알맹이는 쏙 빠뜨렸다는 생각이 들어서 공부를 더 하고 있습니다. Gradle build script, Groovy, UPL, 이 세 가지 부분에 집중했는데, 정말 중요한 부분을 빠뜨리고 공부했습니다. 사실 앞에서 언급한 것들은 기본으로 알아야 하는 것이고, dependency 야 말로 가장 중요한 부분입니다. 제가 겪은 대부분의 에러는 이쪽에서 발생했습니다( 물론 제가 경험이 엄청 많다는 이야기는 아닙니다 ).

 

이 문서에서는 Gradle 의 dependency 개념에 대해 소개하고, UPL 과 dependency 가 어떻게 연관되는 지 설명할 것입니다. 아직 Gradle 의 기본 개념과 Groovy 문법에 대해서 모르신다면, 공부를 하고 보시는 것이 좋을 것 같습니다. 제가 그런 부분에 대해서까지 설명하기에는 좀 부족함이 많네요. UPL 에 대해서는 제 블로그의 [ UnrealPluginLanguage ( UPL ) 이란 ] 에 정리해 뒀습니다.

 

 

Gradle Dependency

 

Dependency 를 매우 저렴하게 표현하자면, Visual C++ 에서 볼 수 있는 헤더 경로와 라이브러리 경로입니다. 예를 들어 다음과 같은 호출을 생각할 수 있겠죠.

 

 

내가 사용하고자 하는 모듈의 헤더와 라이브러리를 포함하지 않고서는 해당 모듈이 제공하는 기능을 이용할 수 없습니다. 컴파일 에러나 링크 에러가 나겠죠. 그러면 이런 헤더와 라이브러리는 어디에서 구해야 하는 걸까요? 보통 VS 로 개발할 때는 소스코드를 아예 프로젝트에 포함시키거나, 소스코드의 라이브러리를 받아서 import 합니다. 혹은 라이브러리를 그대로 받아서 import 하죠.

 

Linux 에 관심이 있어서 깔아 보신 분들이라면, 아래와 같은 형식으로 패키지를 설치하는 걸 보신 적이 있을 겁니다.

 

 

특정 플랫폼을 대상으로 하는 특정 버전의 gzip 을 설치하는 거죠. 이렇게 하면 자동으로 http 를 통해 설치하고자 하는 패키지의 소스코드를 다운로드해서 빌드해서 설치해 줍니다. 참 쉽죠?

 

어쨌든 Linux 같은 오픈소스 진영에서는 외부에 저장소( repository )를 두고 거기에서 소스를 다운로드 받는 것이 일반화되어 있는 것 같습니다. Linux 는 매우 변종이 많기 때문에 정확한 플랫폼과 버전을 지정하지 않으면 안 되겠죠. Windows 야 자기들이 OS 를 만드니까 알아서 패치할 수 있지만, Linux 에서는 쉽지 않습니다. Ubuntu 같은 OS 들도 자기들이 원하는 건 설치하겠지만, 남들이 만들어 놓은 것은 어떻게 할 수 없겠죠.

 

Gradle 은 아래 그림에서 보이듯이 gradle 이 dependency 를 받는 방법은 3 가지 입니다. Network 를 통해 외부 repository 에서 받는 경우가 있고, local repository 에서 받는 경우가 있습니다. 마지막으로 이미 netowrk 를 통해 받은 repository 를 caching 해 놓고 거기에서 가지고 오는 경우가 있습니다.

 

 

 

머리가 아프실테니, 이야기를 더 진행하기 전에 기본 용어부터 정리하도록 하겠습니다. 아래 인용부분은 [ 2 ] 의 번역입니다.

 

Configuration

 

특정 목적을 위해 그룹화된 dependency 들의 named set 을 의미합니다: 예를 들어 "implementation" configuration 은 프로젝트를 컴파일하는 데 요구되는 dependency 들입니다. Configuration 은 module 과 artifact 에 대한 접근을 제공합니다. Configuration 이라는 말은 dependency management 문맥 바깥에서는 다른 의미를 가질 수 있으니 주의하세요.

 

Dependency

 

Module 을 빌드하고 테스트하고 실행하는 데 요구되는 소프트웨어의 일부에 대한 pointer 입니다.

 

Dependency constraint

 

Dependency constraint 는 어떤 dependency 에 대해서 module 이 유효한 결과를 산출하도록 하기 위해 필요한 요구사항을 의미합니다. 예를 들어 dependency constraint 는 지원되는 module version 집합을 한정할 수 있습니다. Dependency constraint 는 transitive dependency 에 대한 요청과 같은 것을 표현하는 데 사용될 수도 있습니다. 더 많은 정보를 원하신다면, "Managing versions of transitive dependencies with dependency constraints" 항목을 참조하세요.

 

Module

 

Google Guava 와 같은 소프트웨어 조각을 의미합니다. 이것은 시간이 지남에 따라서 바뀌고 있고 모든 버전을 포함한 개념입니다. 모든 module 은 이름을 가지고 있습니다. Module 의 각 배포본( release )은 module version 에 의해 표현됩니다. 편리하게 사용될 수 있도록 하기 위해, module 은 repository 에서 호스팅될 수 있습니다.

 

Module metadata

 

Module 배포본은 metadata 를 제공할 수 있습니다. Metadata는 module 에 대해 자세히 기술하는 데이터입니다. 예를 들어 repository 의 좌표( coordinate )라든가, project 나 요구되는 transitive dependency 에 대한 정보같은 것들이 있습니다. Maven 에서 metadata 파일은 ".pom" 이라 불리며, Ivy 에서는 "ivy.xml" 이라 불립니다.

 

Module version

 

Module version 은 배포된 module 이 변화할 때마다 만들어진 독립된 집합을 의미합니다. 예를 들어 18.0 은 com.google:gujava:18.0 이라는 좌표를 사용하는 module version 임을 의미합니다. 경험적으로 볼 때, module version 의 스키마에 대한 제약은 없습니다. 시간, 숫자, "-GA" 같은 특별한 접미어들이 식별자로 허용되고 있습니다. 가장 널리 사용되고 있는 versioning 전략은 "semantic versioning" 입니다.

 

Repository

 

Repository( 저장소 ) 는 module 집합을 호스팅합니다. 각각은 module version 에 의해 식별되는 하나 이상의 배포본들을 제공합니다. Repository 는 binary repository product( 예를 들어 Artifactory 나 Nexus ) 나 파일 시스템의 directory sturcture 에 기반하고 있습니다. 더 많은 정보를 원한다면 "Declaring repository" 를 참조하세요.

 

Resolution rule

 

Resolution rule 은 dependency 가 풀리는( resolved ) 방식에 영향을 줍니다. Resolution rule 은 빌드 로직의 일부로 정의됩니다. 더 많은 정보를 원하신다면 "Customizing Dependency Resolution Behavior" 를 참조하세요.

 

정리하자면 어떤 module 은 module version 과 함께 배포( release )되며, 이것은 repository 좌표를 찍어서 얻어 올 수 있는 겁니다.

 

Declaring Repository

 

그러면 여기에서 "어떤 repository 들이 있을가?" 라는 의문이 생기겠죠. Repository 의 종류는 "Repository Types" 문서에서 찾아 볼 수 있습니다. 일반적으로는 세 가지 종류의 repository 를 사용하고 있습니다.

 

출처 : [ 3 ]

 

예를 들어 JCenter 를 사용한다고 하면, Gradle 빌드 스크립트에 다음과 같이 적습니다.

 

 

하지만 Crashlytics 와 같은 특정 서비스들은 자신만의 repository 를 제공하기도 합니다. 그런 경우에는 URL 을 통해 접근해야 하죠.

 

 

URL 위쪽에 "maven" 이라는 closure 로 묶여 있는 것을 볼 수 있는데요, 이것은 RepositoryHandler 라고 부릅니다. "Repository Types" 에서 지정하고 있는 repository 에 대해 구체적인 정보를 설정할 수 있게 해 주는 것이죠.

 

Declaring Dependency

 

이제 사용할 dependency 를 선언해야 합니다. 일반적으로는 module 제공하는 사람들이 repository 와 version 을 알려줍니다. 예를 들어 Fabric Crashlytics 같은 경우에는 documentation 에서 이런 예제를 제공하죠.

 

 

 

첫 번째 코드는 gradle 프로젝트의 "base/build.gradle" 에 입력할 내용이고 두 번째 코드는 "library/build.grale" 에 입력할 코드입니다.

 

보시면 알겠지만 하나는 "dependencies" closure 에 "classpath" 라는 식별자를 사용하고 있고, 다른 하나는 "compile" 이라는 식별자를 사용하고 있습니다. 이 두 가지는 차이가 있습니다. "classpath" 는 플러그인에 접근하는 데 사용하는 경로입니다. 하지만 "compile" 은 라이브러리에 접근하는 데 사용합니다.

 

여기에서 구체적으로 언급하기에는 너무 내용이 많습니다. 그러므로 "Gradle User Guide" 에 가서 링크를 찾아 다니면서 내용을 숙지하시는 것이 좋습니다. 여기에서는 dependency 가 뭐고 UPL 과 어떤 관계를 가지는 지에 대해서만 정리할 것입니다.

 

UPL 과 Dependency

 

 

UnrealPluginLanguage 는 크게 "AndroidManifest.xml", "buildAddition.gradle", "*Activiy.java" 파일들을 수정할 수 있도록 해주는 일종의 스크립트입니다. 가장 쉬운 예제는 "Engine/Source/Runtime/Advertising/AndroidAdvertising" 에 있는 "AndroidAdvertising" 모듈에서 찾아 볼 수 있습니다.

 

이 모듈에서 핵심적인 파일은 두 개 입니다.

 

 

"AndroidAdvertising.Build.cs" 에서는 "AndroidAdvertising_APL.xml" 을 등록합니다. 여기에서 "UPL" 이 아니라 "APL" 이라는 이름을 사용해서 헷갈리는 분들이 계실텐데요, "APL" 은 "Android Plugin Language" 의 약자로 UPL 의 전신입니다. Android 전용으로만 사용되었을 때 사용하던 용어입니다.

 

어쨌든 다음과 같이 UPL 을 등록합니다.

 

 

그러면 AutomationTool 에서 Android BuildTool 을 실행하면서 이 UPL 파일을 평가하게 되는거죠. 이 UPL 파일 안쪽에 보면, "<buildGradleAdditions>" 엘리먼트에서 다음과 같이 "buildAddtions.gradle" 파일에 코드를 삽입하는 것을 볼 수 있습니다.

 

 

이건 또 "implementation" 이라는 키워드를 사용하는데요, 프로젝트 컴파일에 사용하는 구성이므로 그렇습니다.

 

UPL 은 정말 많은 커스터마이징 기능을 가지고 있습니다. 그 목록은 [ UnrealPluginLanguage ( UPL ) 이란 ] 에서 찾아 보세요.

 

참고자료

 

[ 1 ] Introduction to Dependency Management, Gradle Docs.

[ 2 ] Dependency Management Terminology, Gradle Docs.

[ 3 ] Declaring Repositories, Gradle Docs.

+ Recent posts