Bibbidi Bobbidi Boo
article thumbnail

* 적용기: 실제 개발에 적용하면서 배우게 된 내용 정리

* 현재 취준생으로 풋내기 개발자가 쓰는 글입니다.

* 그러니 조언과 지적 및 훈수는 언제나 환영입니다! 댓글로 많이 달아주세요!

 

개인 프로젝트에도 Git 브랜치 전략 중 하나인 Github-Flow를 적용하면서,

필수적으로 CI/CD를 적용할 일이 있었다.

 

CI/CD 도구 중 Github Actions를 사용하고,

추가로 Github Hooks까지 사용했는데,

 

이 글에서는 적용 전 CI/CD에 대해 공부한 내용부터,

Git Hooks과 Git Actions를 사용한 내용과 결과까지 정리해보고자 한다.


CI/CD

정의

CI/CD는 continous integration(지속적인 통합)과 Continuous Deployment(지속적인 배포)의 약자다.

  • CI(Continuous Integration, 지속적인 통합)

teamcity에서는 "CI"란 용어에 대해 "동일한 프로젝트에서 작업하는 모든 사람이 정기적으로 코드 베이스의 변경 사항을 중앙 저장소에 병합하도록 하는 방식"이라고 정의했다.

 

단순하게 '주 브랜치(git-flow의 develop 등)에 작업한 내역을 자주, 빠르게 merge 하자.'라고 받아들이면 될 것 같다.

여러 개발자가 동시에 작업하는 경우 동일한 코드를 작업하다가 충돌하는 상황이 발생하기도 하고, 버그가 생기기도 한다. 이때 자주 merge 하면 충돌이 줄어들고, 버그를 찾는데도 어떤 작업 내역인지 빠르게 발견이 가능하다.

 

CI는 자동화된 빌드, 테스트, 정적 코드 분석 등의 도구와 함께 사용한다.

  • CD(Continuous Delivery, Continuous Deployment, 지속적인 제공/지속적인 배포)

CD는 지속적인 제공과 지속적인 배포 두 가지의 약어를 지니고 있는데, 둘의 개념이 약간 다르다.

 

공통적으로는 빌드 후에 모든 코드 변경 사항을 테스트, 혹은 production 환경에 자동으로 배포하는 것을 말한다.

자동화된 테스트를 하는 CI에서 그치지 않고, 자동화된 릴리스 프로세스를 추가했다.

 

다만 지속적인 제공은 배포까지는 자동으로 하되, 실제 배포는 사람이 눌러서 하게 되며,

지속적인 배포 실제 배포까지 자동으로 수행해준다. (이를 사용하면 더 이상 출시일이 없다.)

 

아래 그림은 Atlassian에서 CI/CD를 그림으로 나타낸 것이다.

출처: atlassian


장점

CI/CD 파이프라인의 주요 목표는 사용자에게 제대로 작동하는 소프트웨어를 자주 빠르게 제공하는 것이다.

즉 매주, 매일, 매시간 변경 사항을 전달하게 된다.

 

이를 통해 먼저는 빠른 릴리즈가 가능하게 되며,

버그를 빠르게 식별하고 수정하며, 검토 시간 또한 단축된다.

자동으로 빌드, 테스트한 이후 merge 되므로 기본 브랜치는 안정된 상태임을 보장한다.

협업 시에도 코드 변경 사항 논의 전 빌드, 테스트, 배포가 수행되므로 불필요한 오류가 줄어든다.


종류

가장 많이 쓰는 CI/CD 도구에는 아래와 같다.

  • Jenkins
  • Circleci
  • TeamCity
  • Bamboo
  • GitLab
  • TravisCI
  • Github Actions
  • Google Colud Build 등등..

이 중 사용이 무료이면서,

처음 입문자들에게 접근하기 쉽다고 느꼈던 Github Actions를 사용하였다.


실제 적용하기: Github Hook과 Github Actions

목표

우선 내가 CI/CD를 사용함으로서 누리고 싶은 장점은 다음과 같았다.

  • 코드의 안정성
  • 검토 시간 절약
  • 버그 식별

이를 위해서 관련 자료를 찾아본 후, main 브랜치에 개발 내역을 merge 하기 전 다음을 검사하고자 했다.

  • build
  • test 실행
  • 코드 검사

그리고 코드 검사 시에는 ktlint와 detekt를 사용했다.

스타일 검사하기 위한 도구에는 기본적으로 Android Lint가 있었지만

StackOverFlow에서 찾아보니 ktLint에 비해 보편적으로 적용되고,

ktlint와 같은 경우에는 Kotlin에 좀 더 최적화되어있다고 하여 ktLint를 선택했다.

  • ktlint
    • kotlin 언어의 공식 코딩 컨벤션에 따라 스타일을 검사해주는 도구
    • lint의 일종, 하지만 lint보다 더 구체적이며, Kotlin 언어에 더 특화되어 있음
  • detekt
    • kotlin 언어의 정적 코드 검사 도구
    • 잠재적 버그, 코드 복잡성, 성능 저하까지 잡아줌.

원래는 모두 Main Branch에 Merge 하기 전 검사하고자 했으나,

Lint 검사와 같은 경우에는 다른 것에 비해 시간이 덜 걸리고, 개발하면서 수정하는 게 더 적합하다고 판단.

Commit 전에 수행하도록 했다.

Git Actions와 Git Hooks를 사용하면 위 사항을 진행할 수 있었다.

  • Git Actions
    • pull_request, push할 때 등 특정 이벤트가 트리거될 때 workflow를 실행하도록 지원
  • Git Hooks
    • 특정 상황에 특정 스크립트를 실행할 수 있도록 지원

결국 최종 목표는 다음과 같다.

  • Pre-Commit: Commit 전 Git Hook을 사용해 ktlint 검사
  • Pre-Merge: Main Branch에 Merge 전 Git Actions를 사용해 detekt 검사 + build + test 실행

Git Hooks 

하는 방법은 매우 단순하다. 우선 프로젝트 단위의 build.gradle.kts에 다음을 추가하여 플러그인을 설치하자.

(최신 버전은 다를 수도 있으니 주의!)

plugins {
    // ...
    id("org.jlleitschuh.gradle.ktlint") version "11.3.1")
}

allprojects {
    apply(plugin = "org.jlleitschuh.gradle.ktlint")
}

 

그리고 여기서 해당 명령어를 터미널에 입력만 하면 등록은 끝!

$ ./gradlew addKtlintCheckGitPreCommitHook

 

여기까지 설정하면 Commit 했을 때 자동으로 검사하여,

맞지 않는 경우 error을 발생시키고,

맞으면 commit을 해준다.

실패 시
성공 시

+) 추가

 

터미널에서 사용할 수 있는 ktlint 명령어들을 메모했다.

# 코드 스타일 검사
$ ./gradlew ktlintCheck

# 코드 스타일 검사 후 틀린 부분 자동 수정
$ ./gradlew ktlintFormat

# 등록한 hook 삭제
$ cd .git/hooks
$ rm ./pre-commit

Git Actions

Git Actions 작업을 하기 전, 

먼저는 detekt를 실행할 수 있도록 설정해야한다.

 

ktlint와 마찬가지로 build.gradle.kts에 아래를 추가하면 된다.

plugins {
    // ...
    id("io.gitlab.arturbosch.detekt") version "1.22.0"
}

allprojects {
    apply(plugin = "io.gitlab.arturbosch.detekt")
}

 

그리고 실제 잘 적동하는지 다음을 터미널에 쳐서 확인해본다.

$ ./gradlew detekt

 

다음으로 Github Repository에 가서 Workflow를 추가하면 되는데,

기존에 있는 샘플에서 수정하고자 다음을 선택했다.

Github Repository - Actions - Android 검색하면 나온당

 

그러면 다음 샘플로 제시된 코드를 수정할 수 있도록 나온다.

name: Android CI

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:

    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'
        cache: gradle

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build with Gradle
      run: ./gradlew build

 

샘플 코드에서 무슨 작업이 이루어지는지 확인해보자.

 

먼저 맨 처음 name작업의 이름으로, workflow가 돌아갈 때 표시될 이름이다.

name: Android CI

 

그리고 on언제 작업을 할 건지를 설정할 수 있다.

예시에는 main에 push 혹은 pull request를 하기 전 실행할 것임을 알 수 있다.

push와 pull_request 외에도 지원하는 더 많은 이벤트는 여기서 확인하자.

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

 

jobs어떤 작업을 할 건지 설정할 수 있다.

각 작업은 run-on에서 지정한 환경에서 실행한다.

steps는 순차적으로 실행시킬 내용 목록을 정리한 것이다.

 

아래에서는 ubuntu 환경에서,

jdk 11 설정 후 gradlew에 접근할 수 있도록 chmod 설정하고, build할 것임을 알 수 있다.

jobs:
  build:
	
    # 실행 환경 - ubuntu
    runs-on: ubuntu-latest
	
    # 순차적으로 실행
    steps:
    
    - uses: actions/checkout@v3
    
    # java 11 설정
    - name: set up JDK 11
      uses: actions/setup-java@v3
      with:
        java-version: '11'
        distribution: 'temurin'
        cache: gradle
	
    # 접근 권한 설정
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    # build
    - name: Build with Gradle
      run: ./gradlew build

 

샘플 코드에 있는 내용 외에도 더 자세한 workflow 구문은 여기서 확인할 수 있다!

 

 

그럼 이제 이것을 입맛대로 수정하면 되는데, 

workflow의 이름과, 실행할 작업만 약간 수정해주었다.

name: Pre-Merge Check

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:

  build:
    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'
        cache: gradle

    # Permission 주기
    - name: Grant execute permission for gradlew
      run: chmod +x gradlew

    # kotlin style 테스트
    - name: Run detekt
      run: ./gradlew detekt

    # 테스트 실행
    - name: Run UnitTest
      run: ./gradlew testdebugUnitTest

    # 앱 빌드
    - name: Build
      run: ./gradlew assembleDebug

 

설정 완료 후, main branch에 PR을 날리고 나면

다음처럼 merge 되기 전 workflow를 실행시켜서 결과를 보여준다.

실패했을 때
details를 클릭해서 상세 오류를 볼 수 있다.
성공했을 때

 

그 밖에도 PR 목록과 commit 내역에서도 통과 여부를 표시해준다.

PR 내역
commit 내역

보다시피 git hooks과는 다르게 위에서 실패해도 merge 하는 것을 따로 막지는 않도록 해두었다.


적용해 본 후 느낀 점

장점

일단 ✨자동화✨이니 신경을 크게 안 써도 되서 좋았다.

 

test나 build와 같은 경우에는 사실 오류가 생긴 적은 없어서

"안정된 소프트웨어"나 "버그 식별" 등의 장점을 느끼진 못했지만,

PR 한 번에 오류가 꼭 한 번은 났던 detekt에서 코드 검토 시간을 명확하게 줄일 수 있었다.

 

예를 들면 앱 실행을 보려고 하다가 when절의 else문이 없다고 안되길래

급하게 Exception을 해놓고 메시지를 안 넣었는데 메시지가 없다며 잡아주었다....

패키지 구조가 잘못되었을 때도 잡아줌..ㅠㅠ

 

그 외에도 UseCheckOrError 등을 해결하면서 error(), check() 메소드가 있다는 것 등등 배우게 된 것도 있다.

 

게다가 생각보다 간단해서, 그룹 프로젝트 때 적용하지 못한 것이 매우 아쉽기도 했다.

그룹 프로젝트에서 내 코드 혹은 동료 코드를 보면서 리뷰를 달 때

놓친 코드 스타일 하나하나까지 일일히 달아주었는데

만약 CI를 구축했다면 코드 스타일까지 우리가 검토하지 않아도 됐겠구나 싶어서 더욱 그랬다.


단점

아무래도 초반 구축 비용이지 않을까 싶다.

 

게다가 Github Actions와 Git hooks의 단점은 아니지만,

코드 스타일 도구인 ktlint와 detekt의 rule이 약간 까다로운 면이 없지 않아 있었다. 

 

detekt는 따로 rule을 추가하기까지 했는데,

TooManyFunctionsUnnecesssaryAbstarctClass 때문이다. 

 

전자인 TooManyFunctions의 경우 말 그대로 함수가 너무 많다라는 뜻인데

룰에 의하면 default가 11개로 제한되어있었다.

함수가 적으면 좋긴 하나, 프로젝트에는 도저히 나누고 싶어도 나눌 수 없던 파일이 존재했다.

때문에 제한하는 함수의 갯수를 조금 더 늘여주었다.

 

후자인 UnnecessaryAbtarctClass의 경우에는 abstract class가 abstract 메서드만 가지고 있다고 발생하는 문제인데,

프로젝트에서 di의 라이브러리 중 하나인 hilt를 사용중이었고,

RepositoryModule을 다음처럼 @Bind로 구현하고 있어서 어쩔 수 없이 얘만을 제외시키도록 했다.

@Module
@InstallIn(ViewModelComponent::class)
abstract class RepositoryModule {

    @Binds
    abstract fun bindHabitsRepository(
        defaultHabitsRepository: DefaultHabitsRepository
    ): HabitsRepository
}

 

위 내용을 토대로 실제 프로젝트에서 detekt rule을 추가할 때 추가한 코드는 여기서 볼 수 있다.

 

+) 추가

detekt처럼 ktlint 또한 .editorconfig라고 해서, 직접 룰을 커스터마이징할 수 있다.(직접 사용하진 않음)

만약 하고자 한다면 해당 링크를 참고하자.


참고 자료


마치며

해당 내용은 밑 이슈에서 코드와 함께 자세하게 확인할 수 있다.

profile

Bibbidi Bobbidi Boo

@비비디

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!