네부캠 첫 프로젝트에서 팀 협업 도구로 GitHub를 사용했다. 사실 GitHub를 실제 협업에 활용한 건 이번이 처음이었다. 특강에서 배운 내용을 프로젝트에 적용해보면서 Git 사용법을 익힐 수 있었지만, 익숙하지 않아서 답답한 순간이 많았다. 특히 실수로 Master branch에서 commit 해서 rollback 해야하는 상황이 있었는데, 이후에 commit된 다른 동료들의 수많은 작업들이 사라질까 조마조마했던 기억이 있다. 그래서 다음 프로젝트에서는 Git을 좀 더 잘 활용할 수 있도록 꼭 따로 공부해야겠다고 다짐했고, 프로젝트가 끝난 바로 그 주말 부스트코스의 'GitHub으로 따라하는 버전관리' 강의를 수강했다. 아래는 그 기록이다.
Git은 프로그램의 소스코드를 관리하는 프로그램이다. 소프트웨어 개발을 할 때, 여러 사람이 참여하는 방식으로 개발의 효율을 올리곤 한다. 팀원들이 개발 도중에 수정한 내용을 다른 팀원이 바로 확인하고, 서비스에서는 배포된 소프트웨어의 버전 관리를 함으로써 개발의 효율성을 높일 수 있다. Git은 이러한 소프트웨어 버전 관리 시스템(VCS, Version Control System)의 한 종류이다.
git은 분산된 환경을 통해 소스코드를 관리한다. 분산 환경 시스템에는 중앙서버와 클라이언트가 존재한다. 이때 코드를 모아놓는 원격 컴퓨터를 remoter server, 그 코드들의 사본을 받아 개발하는 개인/지역 컴퓨터를 local client라고 한다면, Git에서도 git server와 git client가 존재한다. git client는 git server의 사본을 가지고 각자의 로컬 환경에서 개발할 수 있다.
Repository
repository란 github 상에서 우리의 프로그램을 담는 저장소이다. Repository에서는 우리 코드를 저장할 수 있을 뿐 아니라 커밋 히스토리, pull request 등 협업을 위한 여러 작업을 할 수 있다. 개발하는 프로젝트를 담는 폴더라고 생각하면 된다.
Commit
commit은 코드의 변경사항을 저장한다. repository에 file을 여러번 업로드하고 이를 각각 commit하면, commit history를 통해 그 과정을 확인하고 어떻게 바뀌었는지 알 수 있다. 각 commit은 고유한 ID를 가지며, 이를 통해 특정 시점의 코드 상태로 돌아갈 수 있다.
Issues
Issues는 개발자 간의 의사소통 수단이다. 개발을 하면서 소통해야 할 상황이 많이 생기는데, bug가 발생해 debug를 해야 하거나 새로운 기능을 만들어야 할 때가 그 예이다. 이런 상황의 대부분은 코드를 보고 의견을 교환하며 해결해야 한다. repository에서 issues는 이런 기능을 수행한다. issues에서는 ‘issue 생성’을 통해 해결하고 싶은 문제를 업로드하고 의견을 달 수 있다. 이때 해당 이슈가 어떤 것인지 알려주는 labels, 해당 이슈를 처리할 사람이 누구인지 지정하는 assignee 등의 옵션을 추가할 수 있다.
Clone
clone이란 github의 repository의 내용을 내 컴퓨터(로컬) 환경에 복제하는 것이다. 이를 통해 로컬환경에서 github의 파일들을 받아 개발할 수 있다.
Explorer → Clone Repository → Clone from URL(code 버튼을 누르면 나오는 https 주소 입력) → 새로운 폴더 생성 후 select repository location
git config
로컬 환경에서 작업 후 commit을 진행할 때, 누가 해당 커밋을 했는지 기록하고 확인할 필요가 있다. 해당 작업을 수행하는 git 명령어가 git config이다.git config --global user.name suvinn git config --global user.email suvii6603@gmail.com
이 명령어는 git config에 global 옵션을 더해 해당 값을 기본값으로 활용하며 사용자의 이름, 사용자 이메일을 등록한다. 첫 clone 후 commit 시, 위와 같은 설정이 필요하다.
Push
로컬 환경에서 개발을 하며 중간중간 commit을 진행하고 난 이후, 원격 저장소인 github에 업로드하려 한다. 이때 활용하는 git 명령어가 push이다. push 하면 로컬 환경에서 개발한 코드 뿐 아니라 그동안 개발하면서 중간중간 commit했던 이력들 또한 업로드된다.
Pull
로컬에서 개발을 하던 와중 다른 팀원이 본인의 작업물을 push 함으로써 원격 저장소에 변화가 생겼다. 원격 저장소의 내용을 현재 내 로컬 환경에 반영하고 합쳐 개발을 계속하고자 한다. 이때 활용하는 명령어가 git pull이다. git pull은 fetch와 merge가 동시에 진행된다.
Fetch & Merge
fetch는 원격 저장소에 있는 내용을 로컬 저장소로 가져온다. 이를 통해 로컬 저장소와 원격 저장소 간의 차이를 비교할 수 있다. 충돌되는 상황은 발생하지 않는지, 충돌한다면 어떻게 해결하면 좋을지 확인한 후 merge를 통해 두 branch를 병합한다. (pull = fetch + merge
)
Sync
sync는 원격 저장소의 내용을 현재 내 로컬 환경에 반영하고 합치는 pull과, 로컬 환경에서 개발한 코드 및 commit 이력을 원격 저장소에 업로드하는 push를 동시에 진행한다. (sync = pull + push
)
git init
지금까지 github의 repository를 먼저 만들고 clone하는 과정을 거쳐 원격 저장소로부터 로컬 저장소를 구성할 수 있었다. 반대로 로컬에서 개발하고 git을 이용해 형상관리를 하고 원격 저장소에 업로드 할 수도 있다. 이때 로컬 저장소에 필요한 명령어가 git init이다. git init을 통해 로컬 저장소를 git을 통해 관리할 수 있게 된다. (Clone Repository ↔ Initialize Repository)
Explorer → Open Folder → 새로운 폴더 생성 후 select repository location → Initialize Repository
git add
add는 변경사항을 commit 대기 상태(staging area)로 만든다.
commit은 버전을 기록할 때마다 활용한다. commit을 할 때 여러 파일의 수정사항을 기록해도 좋지만, 한 commit에 한 개의 파일의 수정사항만 저장할 수 있다면 다른 팀원들이 commit history를 볼 때 조금 더 수월하게 볼 수 있을 것이다. 이때 활용하는 명령어가 git add이다. git add는 git commit에 포함될 파일을 지정한다. git add를 통해 하나의 파일을 지정하면 해당 파일을 stage에 올린다.
git checkout
Checkout은 head를 바꾼다. (detached head state)
프로젝트를 진행하던 중 코드 상에 버그가 있어 실행이 되지 않는다는 것을 알게 되었을 때, commit을 잘 해왔다면 해당 commit history로 돌아갈 수 있다. 그렇게 된다면 commit history에 있는 버전들을 활용해 좀 더 빠르게 debug를 할 수 있는 것이다. 이때 활용하는 명령어가 git checkout이다. git checkout을 활용해 기존의 commit history로 저장소를 변경한 후 오류를 수정한 다음 다시 commit을 통해 버전을 업데이트 할 수 있다.
git reset
reset은 HEAD의 branch를 바꾼다.
if(attached){
HEAD의 branch를 바꾼다.
} else{
== checkout
}
Create branch
하나의 저장소 안에서 여러 개의 작업을 해야할 때, 프로젝트를 바탕으로 독립적으로 개발을 할 수 있을 수 있는 저장소(작업영역)
큰 개발 프로젝트를 진행할 때에는 여러 개발 팀, 혹은 팀원으로 나뉘어 동시에 개발한다. 머신러닝 개발 프로젝트를 하는 팀에서는 데이터를 전처리하는 팀, 모델을 개발하는팀, 모델을 서빙하는 팀이 있을 수 있다. 팀끼리 버그를 수정하거나 새로운 기능을 개발해야 하는 일이 많을 것이기 때문에, 각 팀별로 버전을 관리할 필요성이 있다. 이때, 각 팀에서는 개발 프로젝트의 소스코드를 바탕으로 개발을 해나가야 할 것이다. 각 팀별로 개발을 완성하고, 마지막에 프로젝트에 병합함으로써 새로운 버전을 생성하면 다른 팀원들과 충돌이 나지 않으면서 프로젝트를 진행할 수 있을 것이다.
혹은 개발을 하다보면 실험적인 작업을 해야할 때가 있다. 실험적인 작업은 버려질 가능성이 있는 작업이기 때문에, main 작업으로 하기에는 위험 요소가 있다. 이 상황에서, 프로젝트 전체를 모두 복사하고 작업하는 방법도 있겠지만, 그러지 않고 실험적인 작업만 따로 빼서 작업할 수 있도록 하는 것이 branch이다.
브랜치(branch)는 프로젝트를 바탕으로 독립적으로 개발을 할 수 있을 수 있는 저장소(작업영역)이다. 저장소를 처음 만들 때 동시에 만들어지며 프로젝트 version을 관리하는 branch를 master branch라 한다. 이후 기능 개발 시 master branch를 바탕으로 하거나, 혹은 다른 branch를 바탕으로 또 다른 branch를 생성해 독립적으로 개발을 진행할 수 있다.
create brach + checkout branch
Merge into current branch
새로운 기능 개발 혹은 버그를 고친다고 할 때, master branch를 바탕으로 새로운 branch를 만들어 진행한다. 개발을 다 마치고 이를 master branch에 병합하여 새로운 버전을 만들고자 할 때, 우리는 Merge into current branch를 통해 master branch에 해당 기능 branch을 병합할 수 있다. 이후 필요하지 않은 branch는 delete 할 수 있다.
git remote
로컬 저장소에서 시작한 프로젝트를 원격 저장소와 연결하는 명령어이다. 로컬 저장소에서 작업한 프로젝트를 새로운 원격저장소에 push를 하기 위해 git remote를 쓴다. 이때 사용하는 명령어는 git remote add
이다.
Source Control → Remote → Add Remote → 원격 저장소(repository) 주소 입력 → 저장소 이름 설정: **origin** 입력 → push
git log --oneline
git log --oneline --graph --all
동료들과 함께 같은 저장소(Repository)를 바라보고 개발을 진행하고 있는 상황으로, 김왼손씨와 이오른씨가 같은 저장소(Repository)에 작업을 했다고 가정해보자.
김왼손씨는 아침 일찍 출근한 뒤 열심히 개발을 진행하고 개발 내용들을 각각 add하여 commit으로 나누어 저장 한 후 원격 저장소에 push한 후 퇴근했다. 이오른씨는 오후에 출근해 김왼손씨와 같이 여러 파일을 수정하고 commit한 후 원격 저장소에 push 하려했으나, 원격저장소는 오른씨의 push를 "reject" 한다.
그 이유는, 왼손씨가 개발한 내용들이 오른씨의 개발 내역에는 포함 되어 있지 않았기 때문이다. 이대로 push가 진행 된다면 자칫 왼손씨가 개발한 부분에 오른씨의 개발 내용이 겹치게 되면서 덮어써져 날아가버릴 것이 분명해보인다.
이럴경우, 오른씨는 앞서 배웠던 "pull" 기능을 통해서 원격 저장소에 새롭게 추가된 개발 내역을 현재 개발하고 있는 지역 저장소로 끌어 올 수 있다. 또한 "pull"은 "fetch"와 "merge"를 통해 단계적으로 적용이 가능하다. fetch를 사용해서 원격 저장소의 "변경 내용"을 다운로드 받고, merge를 사용해서 해당 변경 내용을 현재 지역 저장소로 반영 하는 절차를 거치게 된다.
협업을 진행하다 보면 동시에 같은 파일의 같은 줄을 수정하게 되는 충돌은 피할 수 없다.
이때, 어떻게 하면 충돌을 해결 할 수 있을까? 원격 저장소의 내용을 내려 받았을 때 충돌이 발생 한 경우, 아래의 세 가지 경우 중 하나를 선택 하여 다시 commit해서 통합된 버전을 만들 수 있다.
Pull request
만든 코드들이 잘 작동하지 않거나, 검토가 필요하여 push를 눌러 원격 저장소에 바로 반영하기 꺼려질 때, Pull request(PR)을 활용할 수 있다. Pull request란 직접 master 브랜치에 push하는 것이 아니라, master 브랜치로 부터 새로운 브랜치를 만든 다음 개발 내역을 해당 브랜치에 적용하고, 그 내용이 master에 반영되기 전 검토를 해달라는 요청을 보내어 팀원들과 리뷰를 마친 후 반영 하는 방식이다. 그 과정이 아주 엄격하게 관리 될 수도 있고, 간단한 리뷰를 통해서 이루어질 수도 있다.
Pull request에서 발생한 Conflict
코드를 작업하면서 발생 했던 conflict들이 pull request에서도 발생 할 수 있다. 바로 같은 파일을 수정한 2개의 브랜치가 순서대로 merge되는 상황이 그렇다. 왼손씨가 a.py를 수정하고 "left" 라는 브랜치에 push한 후 PR을 만들고, 오른씨도 a.py를 수정하고 "right"라는 브랜치를 만들어 PR을 주었을 때, 먼저 왼손씨의 코드를 리뷰하고 master branch에 "left" 브랜치가 merge되는 순간 "right" 브랜치에서 conflict가 발생하게 된다. pull request에서 발생한 conflict는 최신화된 master 브랜치를 "right" 브랜치에 pull 해서 충돌을 해결한 뒤, 다시 push 하면 해결 할 수 있다.