캡스톤 팀 프로젝트 백엔드 팀원은 2명이다. 이처럼 한 프로젝트를 여러 사람이 개발할 때 코드의 안정성은 굉장히 중요하다.
환경 변수 설정, 비즈니스 로직 구성, Git 충돌 등 여러 사람들이 코드를 공유하면서 발생할 수 있는 문제점은 매우 많다.
실수를 방지하기 위해서 테스트 코드를 작성하지만 매 PR 코드리뷰 때마다 각 리뷰어들이 일일히 테스트 코드를 돌려보면서 리뷰하면 생산성이 저하된다.
단순히 PR의 테스트가 잘 돌아가고 문제가 없다라는 코멘트보다는 테스트의 성공을 보장하고 테스트 커버리지를 측정할 수 있는 수단이 필요하다.
그래서 CI라는 개념과 함께 빌드 및 테스트 자동화를 도입해보기로 했다.
CI를 단편적으로 설명하기보다는 CI/CD 개념을 같이 설명하는 것이 더 도움이 될 거 같다.
CI/CD는 애플리케이션 개발 단계를 자동화하여 애플리케이션을 보다 짧은 주기로 고객에게 제공하는 방법이다.
여기서 지속적인 통합을 나타내는 CI(Continuous Integration)는 애플리케이션을 빌드, 테스트하여 이상이 없는 경우 소스코드를 레포지토리 병합하는 과정을 자동화하는 것을 의미한다.
즉, 여러 개발자들이 하나의 프로젝트를 같이 개발할 때 발생하는 불일치를 최소화해주고 공유하는 코드의 신뢰성을 높이는 개념이다.
CI/CD 툴로는 설치형 CI/CD 툴인 Jenkins, 클라우드형 CI/CD 툴인 Travis CI 등이 있는데, 이것들을 사용하지 않은 이유는 다음과 같다.
현재 huemap 프로젝트는 Github에서 제공해주는 기능들을 사용하며 Project Management를 하고있다.
Issue를 발행하고, Projects를 통해 전체적인 프로세스 일정을 관리하며, Github 하나에서 전부 관리하고 있다.
이처럼 Github 저장소를 기반으로 하나로 통일된 환경에서 CI 수행이 가능하면 좋다고 생각하여 채택하게 되었다.
Github Actions은 간단하게 말하면 Github에서 제공하는 CI/CD 툴이다.
build, test, deploy 등 필요한 Workflow를 등록해두면 Gihtub의 특정 이벤트 push, pull request가 발생했을 때 해당 워크 플로우를 수행한다.
예를 들어 Pull Request를 올리면 자동으로 해당 코드의 테스트를 수행하여 수행한다던지 main branch에 코드를 push 하면 자동으로 코드를 배포하는 등 여러 가지 반복적인 작업을 자동으로 수행해준다.
추가로 생활코딩의 영상도 참고하여 공부하였다.
Github Actions 를 사용한 CI 환경을 구축해보자.
PR를 올렸을 때 자동으로 빌드 및 테스트를 수행한다.
.github/workflows/backend-ci.yml
name: backend-ci
on:
push:
branches:
- 'main'
pull_request:
branches:
- 'main'
jobs:
code-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Grant execute permission for gradlew
working-directory: ./backend
run: chmod +x gradlew
- name: Test with Gradle
working-directory: ./backend
run: ./gradlew build
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: ${{ always() }}
with:
files: backend/build/test-results/**/*.xml
on
): 설정한 조건이 발생하면 워크플로우를 실행한다. (ex. push, pull request, commmit 등)runs-on
): 워크플로우를 실행하는 운영체제 환경, Github에서 호스팅하는 러너를 사용할 수 있다.uses
, run
): commands 또는 actions를 실행할 수 있는 태스크, actions을 사용할 때는 uses
를 사용하고, commands를 사용할 때는 run
을 사용한다.working-directory
: 한 레포지토리 안에 backend, frontend 등이 있으므로 수행 대상 디렉토리를 설정EnricoMi/publish-unit-test-result-action@v1
: PR에 테스트 결과 코멘트로 등록하기
자동 생성된 테스트 결과 코멘트
현재 작성한 워크플로우 흐름은 다음과 같다.
plugins {
...
id 'jacoco'
}
jacoco {
toolVersion = "0.8.8"
}
tasks.named('test') {
...
finalizedBy jacocoTestReport
}
jacocoTestReport {
executionData(fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec"))
reports {
html.enabled true
xml.enabled true
csv.enabled false
}
}
플러그인에 jacoco 를 추가해주고, junit test가 수행될 때 같이 돌 수 있도록 finalizedBy를 넣어준다.
jacocoTestReport는 바이너리 커버리지 결과를 사람이 읽기 좋은 형태의 리포트로 저장한다. html 파일로 생성해 사람이 쉽게 눈으로 확인할 수도 있고, SonarQube 등으로 연동하기 위해 xml, csv 같은 형태로도 리포트를 생성할 수 있습니다. 프로젝트에서 csv 파일은 사용하지 않을 것 같아 커스텀하게 false로 지정하고, 나머지는 Codecov에 전송할 때 필요한 파일들의 확장자이기 때문에 true로 설정해주었다.
plugins {
id 'org.springframework.boot' version '2.7.4'
id 'io.spring.dependency-management' version '1.0.14.RELEASE'
id 'java'
id 'jacoco'
}
group = 'com.huemap'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
jacoco {
toolVersion = '0.8.8'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
finalizedBy jacocoTestReport
}
jacocoTestReport {
executionData(fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec"))
reports {
html.enabled true
xml.enabled true
csv.enabled false
}
}
codecov:
require_ci_to_pass: yes
comment:
layout: "reach,diff,flags,files,footer"
behavior: default
require_changes: false
branches:
- main
해당 파일은, Codecov에서만 읽어가는 파일이다.
꼭 필요한 파일은 아니지만 커스텀하게 지정하고싶은게 있다면 반드시 생성해야하는 파일이다.
우리는 PR에 report를 날려주어야 하기 때문에 해당 파일로 조작이 필요했다.
그래서 다른 기능은 넣지 않았고 간단히 CI 통과여부와 PR comments 를 넣어 주었습니다.
그리고 해당 파일은 프로젝트 root 디렉토리에 추가해주어야한다.
marketplace에 등록되어있는 Codecov의 action을 사용한다.
기존에 작성해두었던 Workflow yml에 위 코드를 추가한다.
테스트를 돌리면 jacoco가 생성한 report를 codecov에 업로드한다.
file
은 jacoco가 생성한 report의 경로이며, ./build/reports/jacoco/test/jacocoTestReport.xml
는 jacoco의 기본 설정이다.
name: backend-ci
on:
push:
branches:
- 'main'
pull_request:
branches:
- 'main'
jobs:
code-coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
- name: Grant execute permission for gradlew
working-directory: ./backend
run: chmod +x gradlew
- name: Test with Gradle
working-directory: ./backend
run: ./gradlew build jacocoTestReport
- name: Codecov
uses: codecov/codecov-action@v3.1.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
file: ./backend/build/reports/jacoco/test/jacocoTestReport.xml
- name: Publish Unit Test Results
uses: EnricoMi/publish-unit-test-result-action@v1
if: ${{ always() }}
with:
files: backend/build/test-results/**/*.xml
codecov로 들어가서 github으로 회원가입 후 repository를 등록하고, 발급된 토큰을 Github Actions이 수행될 때 환경변수 값으로 주입될 수 있도록 Github secrets에 등록한다.
CODECOV_TOKEN=912I31235-6221-1024-4723-9d998281jd82(예시)에서 ‘=’을 기준으로 뒷 부분만 등록해야한다.
PR을 올려서 workflow을 실행해보면 위와 같은 결과가 나온다.
codecov 레포트가 코멘트로 잘 달렸고, codecov 내에서도 잘 올라간 것을 확인할 수 있다.
아직 데이터가 충분하지 않아서 전체 코드커버리지가 잘 측정되지 않는다.
Github과 연계성이 좋은 Github Actions을 활용해서 CI를 구축하였다. 코드 리뷰를 하는 사람은 직접 코드를 돌려보지 않아도 빌드와 테스트가 성공한다는 사실을 알 수 있었고 코드커버리지까지 확인할 수 있었다.
최종적으로 70.09%의 테스트 커버리지를 달성하였다. 아쉬웠던 점은 단위 테스트를 하다보니 실수로 누락된 테스트들이 있었다. 그래서 코드 커버리지가 일정 수준을 넘지 못하면 merge가 불가능하게 만들어서 테스트 코드 작성을 강제할 수도 있다는데, 추후에 적용할 수 있으면 적용해볼 예정이다.