Github Action과 Continuous Integration

텐저린티·2024년 4월 6일
0

🎯 사건의 발단

첫 직장 다닐때 우리팀은 CI는 커녕 테스트 코드도 안 만들고 작업을 했었다.

그때를 생각하면 도대체 어떻게 굴러갔던건지 이해가 안 된다.
애초에 개발을 하고 개발자가 직접 테스트를 하던가, 아니면 QA 분들이 엄청 고생했겠지.
심지어 제대로 동작하지 않는 코드 머지해서 한 번 롤백했던 기억도 있다.

참 여러모로 위태롭게 일했던 것 같다.

그러다가 부트캠프 하면서 CI/CD 에 대해서 배우고, 플젝에 적용해봤다.
CI/CD 파이프라인을 내가 구축하지 않아서 잘 체감하지는 못했지만, 확실한 장점은 자동화였던것 같다.

빌드를 실행해서 실제로 돌아가는 코드나 테스트가 아니라면 실패시켜서 애초에 머지를 시킬 수 없게 하는게 참 좋아보였다.
이참에 내가 직접 적용해보면서 잘 뜯어먹어보려고 한다.

나는 Github Action 을 활용해서 CI 하는 것만 설정해서, CD 부분은 없다.
나중에 MVP 만들고, 배포하러 올 때 다시 와서 추가 작성하겠당.

그리고 나는 컨벤션을 중요하게 생각해서 Spotless 도 도입했다.
혼자 하는 플젝에 무슨 뻘짓이냐고 한다면 서운하다.
차분하게 정리된 코드만큼 보기 좋은게 없다.

핵심 요약

  • Github Action CI 적용
  • Spotless 적용

🔍 톺아보기

CI/CD 개념 알기

핵심을 추려보자.

  • 지속적
  • 자동화
  • 개발방식
  • 릴리스 주기 단축

정리하자면, 자동화를 이용해 지속적으로 코드들 업데이트하여 릴리스 주기를 단축시키는 개발방식를 말한다.

구조를 살펴보면 좀 더 이해가 쉽다.

이렇게 지속적인 통합과 배포를 통해서 소프트웨어 개발 전반에 필요한 모든 제반을 자동화할 수 있다.
즉, 협업에 특화되어있는 방법론이라는 것.

CI (지속적 통합)

조금씩 늘어나는 코드 변경 사항을 개발 팀이 정기적으로 구현하고 테스트한 후, 공유 버전 관리 리포지토리에 병합하는 방식을 설명합니다. - ServiceNow

해석체를 쉽게 풀어보자면,

우리가 일주일 동안의 작업 목표가 있다고 해보자.
우리는 대부분 4일 정도 개발을 끝내고, 마지막 하루에서 이틀 정도 테스트를 하면서 개발을 하게 된다. 매일 조금씩 코드 작업을 하고, github 에 commit 하거나 push 한다.
이 과정을 자동화 빌드를 통해서 공통 레포지토지에 올려둠으로써 전체 팀원이 같은 상태의 코드에서 작업할 수 있게 된다는 말이된다.
그게 CI다.

좀만 더 쉽게 설명하자면, CI를 적용한 레포지토리에 코드 올리면 알아서 빌드해서 다른 사람이 쓸 수 있게 만들어준다. 이거다.

몇가지 특징 기능이 있다고 한다.

기능 플래그

개발자가 코드 액세스를 통제할 수 있는 기능이다.

아까 말했듯이 하루에 조금씩 작업을 했다면, 분명 첫째날과 둘째날의 코드는 아무 의미 없는 코드로, 다른 사람이 봐도 의미가 없을 수 있다. 오히려 다른 사람 빌드만 실패하게 만들 수 있다.
그러한 경우에 액세스 통제해서 누이좋고 매부 좋은 상황을 만들 수 있는 기능을 말한다.

안정적인 자동화된 테스트

CI의 근간은 자동화 테스트이다.

기능을 손상시킬 수 있는 코드 변경에 대한 보호 장치다.
또한 테스트 리포트를 통해 테스트를 평가해서, 효율성을 개선하는데도 도움이 된다.

CI 를 통해 레포지토리에 올라온 코드가 무조건 돌아간다는 것을 보장하는 기능을 말하는 것.

오류 수정의 우선순위 지정

오류를 더 빨리 파악하고 우선순위가 더 높은 수정 사항을 자동으로 지정해, 가장 중요한 문제를 더 빨리 해결할 수 있다.

일단 CI 를 수행하면서 각 빌드 작업에 대한 로그를 보면서 어디가 잘못되었는지 우선순위를 나눌 수 있다는 이야기도 되고,
다른 CI 도구를 사용해서 분석을 받을 수도 있다는 이야기다.
예를 들어서, Jetbrains 의 Space 같은 도구를 사용하면 CI 과정에서 발생한 일을 이슈로 만들어서 다시 작업할 수 있다고 한다.

이번 플젝에서 코드 개발은 나 혼자 뿐이므로 CI 자체가 필요할까 라는 생각이 있었지만,
위에 나와 있는 특징처럼 결국엔 CI는 필요했다.

언제나 레포지토리에는 빌드가 가능한 코드만 올라가게 하고,
배포랑 거의 비슷한 환경에서 빌드했을때 실패한다면, 그 부분을 최우선으로 수정하고,
나중에 프론트엔드 개발도 할 때 백엔드 수정할 부분이 있다면 바로 수정해서 PR 만 올리면 되니까 말이다.

CD (지속적 배포/제공)

계속 개발되는 코드를 필요한 위치에 자동으로 배포하는 것.

쉬운 말이긴 한데, 코드에 대한 변경 사항이 실제 환경에 배포되도록 하는것을 말한다.

지속적 배포 / 제공 차이

지속적 배포

코드 변경이 프로덕션으로 자동 푸시되는 것.
코드 변경이 이뤄지면 바로 배포까지 이어져, 바로 사용자가 사용하게 된다는 것을 말한다.

지속적 제공

코드 변경이 되어도 개발팀이 수동으로 배포를 트리할 때까지 기다려야 한다는 것.
지속적 배포보다는 안정성을 확보할 수 있으나, 좀 더 절차가 필요하다는 점에서 속도와 편리성이 떨어지는 특징이 있다.

CD 원칙

소규모, 빈번한 배포

CI에 의존해서 CD를 하면 좋다는 의미다.

최대한 작고 빈번하게 배포를 하면 변경 품질에 대해서 걱정할 필요가 없다.
그러니 자동화 빌드와 테스트로 안정성 검사를 모두 마친 CI를 활용해서 배포까지 이어서 제공하라는 소리다.

반복 작업 자동화

모든 반복적인 수동 작업은 자동화로 대체하는 것이 아무래도 이점이 있다.
해보면 안다.
결국 어떻게든 자동화로 대체해서 사용하고 있는 자기 자신을 보게 될 것이다.

지속적 개선

CD를 일단 시작하면 개선할 여지가 많이 보인다고 한다.
위에서 이야기했듯이 일단 해보면 여기저기 자동화 할 건덕지가 많다는 이야기인 것 같다.

공동 책임

원래는 각자의 책임에만 집중해서 개발하던 것을 이제 CD를 통해서 모두가 배포에 신경쓰게 된다는 이야기이다.
실제로, 저번 플젝에서 내가 문서화를 자동화 하고 싶을때 CD 파이프라인을 수정해서 배포한 적이 있다.
아마도 이렇게 각자의 요구에 따라서 CD를 수정하고 원하는 산출물을 얻는다는 소리인 것 같다.

🏗️ 구조

이게 내가 바라는 CI/CD 파이프라인이다.
아마 AWS EC2 를 쓰게 된다면 저기에 AWS CodeDeploy 를 사용해서 EC2에서 내가 빌드한 도커 이미지를 다운받고, 실행하도록 하게 될 것 같다.

암모턴,
나는 Github Action 을 활용하기로 했다.

사용한 이유는 간단하다.
1. Github 에서 제공했으니 믿음직 ❤️
2. 무료임 (젠킨스 저리가랏)
3. 사용하기만 하면 되는 유용한 action 다수 개발됨

이제 대세는 젠킨스가 아니라 github action 이 되지 않았을까 하는 생각임.

📺 진행과정

Github action 과 Spotless 적용하기 위해 각각 필요한게 있다.

Github Action

Github Action 은 파일로써는 하나만 필요하다.

이렇게 플젝 루트에 위와 같은 디렉토리를 구성하고, 이름은 자기 마음대로 yaml 파일을 만들면 된다.
저 상태로 github 에 push 를 하면 ci.yaml 파일에 설정한 조건에 부합하는 경우 해당 스크립트를 실행하게 된다.
.gitignore 에 포함시키지 말라는 소리.

name: Pull Request Test  
  
# 언제 작성한 트리거 동작하는가  
on:  
  pull_request:  
    types:  
      - opened # PR 생성  
      - synchronize # PR 브랜치에 새로운 커밋 푸시될 때  
      - reopened # PR 재오픈  
  
permissions: read-all  
  
jobs:  
  build-test:  # 수행하고자 하는 CI 파이프라인
    runs-on: ubuntu-latest  # 이러한 환경에서 구동
    permissions:  
      contents: read  
      pull-requests: write  
    steps:  # 수행하고자 하는 CI 파이프라인의 각 작업
      - name: Git Checkout  # 현재 Git으로 체크아웃 (필수)
        uses: actions/checkout@v3.0.2  
  
      - uses: dorny/paths-filter@v2  # 필터링
        id: changes  
        with:  
          filters: |  
            application:              # 아래 디렉토리에서 변경이 일어난 경우에만 필터링 해제
            - 'build.gradle'              
            - 'src/**'  

      - name: JDK Setup  
        if: steps.changes.outputs.application == 'true'   # 필터링 조건에 부합하면 현재 스텝 실행
        uses: actions/setup-java@v3  
        with:  
          java-version: 17  
          distribution: zulu  
          cache: 'gradle'  
  
      - name: application.yaml Creation  # 나는 application.yaml 을 올리지 않아서 따로 secret 으로 넣어줌
        if: steps.changes.outputs.application == 'true'  
        run: |  
          mkdir -p src/main/resources          
          echo "${{ secrets.APPLICATION_YAML_CONTENT }}" > src/main/resources/application.yaml  
          
      - name: Gradle Build  # 요게 CI 핵심
        if: steps.changes.outputs.application == 'true'  
        run: |  
          ./gradlew build --parallel  # 병렬 실행으로 조금이라도 빠르게 수행하도록 함
          
      - name: Coverage Report  # 테스트 커버리지 리포트
        if: steps.changes.outputs.application == 'true'  
        uses: madrapps/jacoco-report@v1.6.1  
        with:  
          token: ${{ secrets.GITHUB_TOKEN }}  
          title: Code Coverage Report  
          update-comment: true  # 이제 빌드 성공 시 리포트가 해당 PR의 코멘트로 기록됨
          min-coverage-overall: 10  
          min-coverage-changed-files: 10  
          paths: |  
            ${{ github.workspace }}/**/build/jacoco/jacoco.xml  
            
  style-test:  # 스타일 체크
    runs-on: ubuntu-latest  
    steps:  
      - name: Git Checkout  # 필수
        uses: actions/checkout@v3.0.2  
  
      - uses: dorny/paths-filter@v2  
        id: changes  
        with:  
          filters: |  
            application:              
            - 'build.gradle'              
            - 'src/**'  

      - name: JDK 설치  
        if: steps.changes.outputs.application == 'true'  
        uses: actions/setup-java@v3  
        with:  
          java-version: 17  
          distribution: zulu  
          cache: 'gradle'  
  
      - name: Style Check  # Spotless 체크
        if: steps.changes.outputs.application == 'true'  
        run: |  
          ./gradlew spotlessCheck

전체 파일이다.
주석을 달아 설명해놨다.
간단한 문법 몇 개로 CI 설정을 완료했다.

Spotless 설정

세 가지 설정이 필요하다.
1. build.gradle 에 작업 추가
2. pre-commit 스크립트 설정 (필수 X)
3. Github Action yaml 파일에 Spotless 체크 파이프라인 추가

build.gradle 설정

plugins {
	id 'com.diffplug.spotless' version '6.21.0'
}

spotless {  
    java {  
        googleJavaFormat()  
  
        removeUnusedImports()  // 불필요 import문 제거
        trimTrailingWhitespace() // 불필요한 공백 트림  
        indentWithSpaces(4) // 들여쓰기 4칸  
        endWithNewline() // 파일 끝에 개행 추가  
    }  
}

이렇게 하면 빌드 스크립트에 Spotless 가 생긴다.

Pre-commit 스크립트 설정

필수는 아니다.
만약에 설정을 안 할거면 바로 아래 나오는 yaml 에 스크립트를 추가하면 된다.

run: |
	./gradlew spotlessApply # 이거 먼저해야 함
	./gradlew spotlessCheck

근데 이렇게하면 사소한 불편함이 생긴다.
코드 레벨에서 컨벤션에 맞게 조정을 매번 해주지 않으면 조금의 수정이라도 있을때마다 commit 대상이 된다는 것.
단순히 import 문 정리, 컨벤션 정리 때문에 commit 이 하나씩 늘어나는 것은
코드 리뷰를 힘들게 만들기도 하고,
머지하기 불안하게 만들기도 한다.

우리가 사용할 pre-commit 훅을 이용하면 커밋을 하는 즉시 spoltessApply 가 적용되어, 컨벤션 부분에 대해서는 commit 대상이 아니게 된다.

#!/bin/sh  
  
targetFiles=$(git diff --staged --name-only)  
  
echo "Apply Spotless.."  
./gradlew spotlessApply  
  
# git 커밋마다 spotlessApply를 실행하면 모든 파일이 변경되기 때문에 변경된 파일만 add하도록 함  
for file in $targetFiles; do  
  if test -f "$file"; then  
    git add $file  
  fi  
done

CI yaml 에 파이프라인 추가

- name: Style Check  # Spotless 체크
     if: steps.changes.outputs.application == 'true'  
        run: |  
          ./gradlew spotlessCheck

이 부분 말하는 거다.
gradle 로 스팟리스 체크를 실행하면 된다.

🔑 결론

Github Action 을 활용해서 CI 파이프라인을 구축해봤다.
그 과정에서 컨벤션 체크로 인한 애로사항을 줄이기 위해서 Spotless 도 적용했다.
가급적 이렇게 둘을 세트로 만들어 사용하게 될 것 같아, 한 번의 포스팅으로 기록했다.

노파심

참고로 나는 TestContainers 를 활용해서 테스트 코드 작성을 했다.
이러한 경우에 로컬에서는 성공하는데 CI에서만 빌드가 오지게 실패하는 경우가 있다.

GogumaBookStoreServerApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1786
            Caused by: org.hibernate.service.spi.ServiceException at AbstractServiceRegistryImpl.java:276
                Caused by: org.hibernate.HibernateException at DialectFactoryImpl.java:191

아마 이런 에러 스택이 발생할 것이다.
자세히 보면 hibernate 에서 DialectFactoryImpl를 찾지 못했다는 소리다.
괜히 application.yaml 가서 dialect 설정하지 말고,
application.yaml 이나 application.properties 같이 DB 연결에 필요한 설정 정보가 누락되어 있다는 거다.

내가 해당 설정 파일을 git 에 올라가지 않도록 설정했는지 확인하고,
만약 설정했다면 해당 레포지토리 security > actions 에 들어가서 추가해주고,
그게 아니라면 해당 설정 파일에 DB 스키마,유저명,비밀번호를 제대로 확인해봐야한다.

참고로 Properties 라는 키워드로 에러가 발생하면 application.yaml 을 변경했는데, secret 에는 반영하지 않아서 발생한 문제이니 수정해주면 된다.

괜히 나처럼 삽질하지 말길.

🔗 참조

profile
개발하고 말테야

0개의 댓글