출제기준 세세항목: 소프트웨어 버전관리 도구 · 빌드 자동화 도구
보고서를 쓰다 보면 '최종', '최종_수정', '진짜최종' 같은 파일이 쌓이곤 합니다. 혼자 쓰는 문서라면 그래도 버틸 만하지만, 여러 개발자가 수백 개 파일을 동시에 고치는 프로그램에서는 금방 감당할 수 없게 됩니다. 어제까지 잘 돌던 기능이 오늘 갑자기 고장 났을 때 무엇이 바뀌었는지 모르면 원인을 찾을 방법이 없습니다.
버전관리는 파일이 바뀔 때마다 그 내용과 시점, 바꾼 사람, 바꾼 이유를 기록해 두는 일입니다. 기록이 있으면 두 시점의 차이를 비교할 수 있고, 문제가 생기면 잘 돌던 시점으로 되돌릴 수 있으며, 같은 파일을 두 사람이 동시에 고쳐도 서로의 작업을 덮어쓰지 않게 조율할 수 있습니다.
소프트웨어공학에서는 개발 과정에서 생기는 산출물의 변경을 체계적으로 통제하는 활동을 형상관리(SCM)라고 부릅니다. 버전관리는 그중에서도 산출물의 변경 이력을 쌓고 꺼내 쓰는 부분을 맡는, 형상관리의 실무 핵심입니다. 그리고 이렇게 관리된 소스코드를 실제로 돌아가는 제품으로 만들어 내는 과정이 빌드이므로, 이 항목에서는 버전관리 도구와 빌드 자동화 도구를 함께 다룹니다.
도구가 달라도 쓰는 말은 비슷합니다. 모든 파일과 변경 이력이 보관되는 곳을 저장소(Repository)라고 합니다. 처음 프로젝트를 저장소에 올리는 일을 임포트(Import), 저장소에서 파일을 내 작업 공간으로 받아 오는 일을 체크아웃(Checkout)이라고 합니다. 반대로 내가 고친 내용을 저장소에 반영하는 일을 커밋(Commit)이라고 하며, 오래된 도구에서는 체크인(Check-in)이라고도 부릅니다.
다른 사람이 그사이 커밋한 내용을 내 작업 공간에 반영해 최신 상태로 맞추는 일은 업데이트(Update) 또는 동기화라고 합니다. 두 버전 사이에 무엇이 달라졌는지 줄 단위로 보여 주는 기능은 디프(Diff)입니다. 관리용 정보 없이 순수한 파일만 뽑아내는 일은 익스포트(Export)라고 합니다.
작업을 갈래로 나눌 때는 브랜치(Branch)를 씁니다. 원래 줄기(시험 자료에서는 트렁크 Trunk 또는 메인라인이라고도 부릅니다)는 그대로 둔 채, 새 기능을 실험할 별도 줄기를 만들어 거기서 마음껏 고치는 것입니다. 작업이 끝나면 갈래를 다시 원래 줄기에 합치는데, 이를 병합(Merge)이라고 합니다. 두 사람이 같은 파일의 같은 부분을 서로 다르게 고쳤다면 도구가 어느 쪽을 택할지 스스로 정할 수 없어 충돌(Conflict)이 납니다. 이때는 사람이 내용을 보고 직접 정리해야 합니다.
출시처럼 의미 있는 시점에는 태그(Tag)를 붙여 둡니다. 특정 커밋에 '1.0 출시판' 같은 이름표를 달아 두는 것이라, 나중에 그 시점의 코드를 정확히 다시 꺼낼 수 있습니다.
버전관리 도구는 이력을 어디에, 어떻게 보관하느냐에 따라 세 가지로 나눠 볼 수 있습니다. 비교 기준은 '저장소가 몇 개이고, 누가 가지고 있느냐'입니다.
공유 폴더 방식은 가장 단순한 형태로, 모두가 접근할 수 있는 한 폴더(또는 한 컴퓨터)에 파일과 이력을 두고 쓰는 방식입니다. 개발자는 자기 컴퓨터에서 고친 뒤 공유 폴더로 복사해 넣습니다. 누가 언제 무엇을 덮어썼는지 통제하기 어려워, 파일을 고치는 동안 잠가 두는 식으로 충돌을 막는 경우가 많습니다. 시험 자료에서는 SCCS, RCS 같은 초기 도구가 이 방식의 예로 소개됩니다.
클라이언트-서버 방식은 중앙 서버 한 곳에 저장소를 두고, 개발자들은 클라이언트 프로그램으로 그 서버에 접속해 체크아웃과 커밋을 합니다. CVS와 그 뒤를 이은 SVN(Subversion)이 대표적입니다. 이력이 한 곳에 모여 있어 관리가 쉽지만, 서버에 연결되지 않으면 커밋할 수 없고, 서버가 망가지면 이력 전체가 위험해집니다. 커밋하는 순간 바로 중앙에 반영되어 다른 사람에게 공유된다는 점도 특징입니다.
분산 방식은 개발자마다 원격 저장소 전체를 통째로 복제한 로컬 저장소를 가집니다. 대표 도구는 Git이고, Mercurial 같은 도구도 같은 계열입니다. 비행기 안처럼 인터넷이 없어도 내 로컬 저장소에 마음껏 커밋하고 이력을 볼 수 있고, 준비가 되면 원격 저장소로 올려 보냅니다. 모든 개발자가 이력의 사본을 갖고 있으니 중앙 서버가 고장 나도 복구할 길이 있습니다. 대신 '내 컴퓨터에 커밋한 것'과 '다른 사람과 공유한 것'이 별개라는 점을 익혀야 합니다.
Git에서는 파일이 네 군데를 거쳐 갑니다. 실제로 파일을 고치는 작업 디렉터리, 다음 커밋에 넣을 변경만 골라 담아 두는 스테이징 영역, 커밋이 쌓이는 로컬 저장소, 그리고 팀이 함께 쓰는 원격 저장소입니다.
명령도 이 흐름을 따라갑니다. 원격 저장소를 처음 내 컴퓨터로 복제할 때는 clone, 고친 파일을 스테이징 영역에 올릴 때는 add, 스테이징된 내용을 로컬 저장소에 기록할 때는 commit을 씁니다. 로컬 저장소의 커밋을 원격 저장소로 올려 보내는 것은 push입니다. 반대로 원격의 새 커밋을 가져오기만 하고 내 작업에 합치지는 않는 것이 fetch, 가져와서 바로 합치는 것까지 하는 것이 pull입니다.
SVN과 비교하면 차이가 분명해집니다. SVN의 커밋은 곧바로 중앙 서버에 반영되지만, Git의 commit은 내 컴퓨터에만 기록되고 push를 해야 비로소 다른 사람에게 보입니다. 그래서 Git에서는 작은 단위로 자주 커밋해 두었다가 정리된 뒤에 한꺼번에 공유하는 식의 작업이 자연스럽습니다. 브랜치를 만들고 합치는 비용도 가벼워서, 기능마다 브랜치를 따로 만드는 방식이 널리 쓰입니다.
개발자가 쓰는 소스코드는 그 자체로는 실행되지 않습니다. 컴파일해 기계가 이해하는 형태로 바꾸고, 필요한 외부 라이브러리를 챙기고, 테스트를 돌리고, 하나의 실행 파일이나 배포용 묶음으로 포장해야 비로소 제품이 됩니다. 이 일련의 과정을 빌드라고 합니다.
빌드를 사람이 손으로 하면 순서를 빠뜨리거나, 개발자마다 라이브러리 버전이 달라 '내 컴퓨터에서는 되는데'라는 말이 나오기 쉽습니다. 빌드 자동화 도구는 이 절차를 파일로 적어 두고 누가 언제 실행해도 같은 순서, 같은 재료로 같은 결과가 나오게 합니다. 특히 외부 라이브러리와 그 버전을 적어 두면 도구가 알아서 내려받아 붙여 주는 의존성 관리 기능이 큰 도움이 됩니다.
여기서 한 걸음 더 나간 것이 지속적 통합(CI, Continuous Integration)입니다. 개발자가 저장소에 커밋할 때마다 서버가 자동으로 빌드와 테스트를 돌려, 합친 코드가 깨졌는지를 바로 알려 주는 방식입니다. 문제를 몇 주 뒤가 아니라 몇 분 뒤에 알게 되니 고치기도 훨씬 쉽습니다.
Ant는 Apache에서 만든 초기 자바 빌드 도구입니다. 빌드 절차를 XML 파일(build.xml)에 하나하나 적어 두는 방식이라 원하는 대로 자유롭게 구성할 수 있습니다. 대신 정해진 틀이 없어 프로젝트마다 작성 방식이 제각각이 되기 쉽고, 외부 라이브러리를 자동으로 관리하는 기능은 기본으로 들어 있지 않습니다.
Maven도 Apache의 도구로, Ant의 자유로움 대신 규약을 택했습니다. 소스 폴더 위치, 빌드 단계의 순서(생명주기) 같은 것을 미리 정해 두어, 그 규약만 따르면 설정을 길게 적지 않아도 됩니다. 설정은 pom.xml이라는 XML 파일에 적고, 필요한 라이브러리 이름과 버전만 써 두면 중앙 저장소에서 자동으로 받아 오는 의존성 관리가 큰 장점입니다.
Gradle은 XML 대신 Groovy나 Kotlin 기반의 스크립트 언어(DSL)로 빌드를 적습니다. Maven처럼 의존성 관리를 하면서도 Ant처럼 유연하게 절차를 짤 수 있고, 바뀐 부분만 다시 빌드하는 증분 빌드와 결과 재사용(캐시)으로 속도를 높였습니다. 안드로이드 앱의 공식 빌드 도구로 쓰이는 것으로 잘 알려져 있습니다.
Jenkins는 성격이 조금 다릅니다. 직접 컴파일 규칙을 정의하기보다는, 저장소에 변경이 생기면 그것을 감지해 Ant·Maven·Gradle 같은 빌드를 자동으로 실행하고 결과를 알려 주는 지속적 통합 서버입니다. 자바로 만들어졌고, 웹 화면에서 작업을 설정하며, 다양한 플러그인으로 버전관리 도구나 테스트 도구와 연결합니다. Hudson이라는 도구에서 갈라져 나왔습니다.
정리하면 Ant·Maven·Gradle은 '어떻게 빌드할지'를 정의하는 도구이고, Jenkins는 '언제 빌드를 돌릴지'를 맡아 자동으로 실행해 주는 도구라고 구분하면 헷갈리지 않습니다.
쇼핑몰에 쿠폰 기능을 추가할 때, 서비스 중인 메인 줄기를 직접 고치지 않고 '쿠폰' 브랜치를 만들어 작업합니다. 그사이 메인 줄기에 급한 버그 수정이 들어와도 서로 방해받지 않습니다. 쿠폰 기능이 완성되고 테스트를 통과하면 메인 줄기에 병합하는데, 같은 줄을 서로 다르게 고친 곳이 있으면 충돌이 표시되므로 그 부분만 직접 정리합니다.
Git을 처음 쓰는 개발자가 '커밋했어요'라고 했는데 동료의 화면에는 변경이 보이지 않았습니다. Git의 커밋은 자기 컴퓨터의 로컬 저장소에만 기록되기 때문입니다. push로 원격 저장소에 올리고, 동료가 pull로 받아 와야 비로소 함께 볼 수 있습니다.
Jenkins가 저장소를 지켜보다가 새 커밋이 들어오면 Gradle 빌드와 테스트를 자동으로 실행하도록 설정했습니다. 누군가 실수로 컴파일되지 않는 코드를 올리면 몇 분 안에 실패 알림이 오므로, 원인이 된 커밋을 바로 찾아 고칠 수 있습니다.