본문 바로가기
공부/CI & CD

GiftFunding) GitHub Actions CI/CD 적용하기[1] - CI 적용

by son_i 2024. 5. 21.
728x90

GitHub Actions란 SW 개발 라이프 사이클 안에서 PR, Push 등의 이벤트 발생에 따라 자동화된 작업을 진행할 수있게 해주는 기능.

 

자동화된 작업이 필요한 경우 ?

1. CI/CD

   로컬 repository -> 원격 repository로 push하고 GitHub Actioins에서는 이벤트 발생에 따라 자동으로 빌드 및 배포하는 스크립트를 실행. 

   애플리케잇녀의 규모가 클수록 빌드, 배포 시간이 오래 걸리는데 이걸 자동화 시켜놓으면 시간 절약을 할 수있음

2. Testing

   팀 프로젝트 중 PR을 보내면 자동으로 테스트를 진행하는 것을 GitHub Actions로 구현 할 수 있다.

   => 테스트 성공 여부에 따라서 자동으로 PR을 Open 및 Close 할 수있음.

3. Cron Job

   GitHub Actions를 통해 특정 시간대에 스크립트를 반복 실행하도록 구현할 수 이음.

    ex) 매일 특정 시간에 크롤링 작업 진행.

 


하려는 작업 : main 브랜치에 push 발생하는 경우 자동으로 서버에 배포되게 하려고 함.

 

구체적인 작업 과정

1. main 브랜치에 PR이 발생하면 자동으로 테스트 및 빌드가 이루어진다.

2. 빌드된 jar 파일이 배포 서버에 복사된다.

3. 배포 서버에서는 기존에 실행되고 있는 서버를 중단한다.

4. 새로 빌드된 jar 파일로 서버를 실행한다.

 


워크플로우 파일을 직접 처음부터 구성해도 되지만 이미 만들어져있는 템플릿을 한 줄씩 공부하면서 사용해보려고 한다.

 

Actions 탭 > New workflow 클릭

 

 

Java with Gradle 클릭

 

그러면 이렇게 코드가 나온다.

 

 

코드 전문

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

name: Java CI with Gradle

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

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies.
    # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

    - name: Build with Gradle Wrapper
      run: ./gradlew build

    # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html).
    # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
    #
    # - name: Setup Gradle
    #   uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
    #   with:
    #     gradle-version: '8.5'
    #
    # - name: Build with Gradle 8.5
    #   run: gradle build

  dependency-submission:

    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies.
    # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md
    - name: Generate and submit dependency graph
      uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

 

이 코드를 공부하고 조금씩 수정해가며 내가 원하는대로 만들어서 사용하려고 한다.

 


1. WorkFlow 이름 및 on 지정

name: Java CI With Gradle

on:
  pull_request:
    branches: ["main"]
    
permissions:
  contents: read

name : WorkFlow 이름을 지정해주었다.

 

on : 해당 워크플로우가 언제 실행될지를 지정해주었는데

  main 브랜치에 PR이 발생할 때로 설정하였다.

 

permissions: 

  contents: read

리포지토리의 내용을 사용하여 작업. contents: read는 커밋을 나열하는 작업을 허용 .. 한다는데 공식문서에서는

무슨 말인지 아직 잘 모르겠다.

 

다른 참고한 블로그에서는 workflow가 액세스할 권한을 설정하는 것이라고 한다. read를 통해 읽을 수 있도록 한다.

 

2. jobs 설정 

jobs:
  build:

    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
    - uses: actions/checkout@v4
    - name: Set up JDK 17
      uses: actions/setup-java@v4
      with:
        java-version: '17'
        distribution: 'temurin'

    # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies.
    # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
    - name: Setup Gradle
      uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

    - name: Build with Gradle Wrapper
      run: ./gradlew build

    # NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html).
    # If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
    #
    # - name: Setup Gradle
    #   uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
    #   with:
    #     gradle-version: '8.5'
    #
    # - name: Build with Gradle 8.5
    #   run: gradle build

jobs: // job 설정을 의미

  build: //job 이름

    runs-on: //작업의 실행환경 runner 지정. 

 

    steps: // 작업들간에 실행될 순서 지정. 시퀀스 타입으로 작성되고 각 단계는  -로 구분한다.

                커맨드나 스크립트 실행은 run, 액션을 사용할 때는 uses

    - uses: actions/checkout@v4 //uses로 액션을 사용할 수 있다. checkout@v4 리포지토리 코드를 Runner로 복사해서 가져감.

    - name: Set up JDK 17 //name은 한 step의 이름을 의미. name을 기준으로 step을 나눔.

       uses: actions/setup-java@v4 // java를 셋업함. 셋업이 필요한 이유는 프로젝트 빌들르 통해 jar파일을 생성하기 위함.

       

템플릿이 제공해준 코드를 보면 

- name: Setup Gradle 부분은 동일하게 있는데

 

윗 부분 코드는 run: ./gradlew build이고

아랫 부분 코드는 run: gradle build로 되어있다.

 

처음에는 두 부분 모두 있어야 되는 줄 알았는데 읽어보니까 아니었다.

 

일단 처음 부분

# Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies.
# See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
- name: Setup Gradle
  uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

- name: Build with Gradle Wrapper
  run: ./gradlew build

# 다운로드된 종속성의 캐싱을 포함하여 GitHub Actions에서 최적으로 사용할 수 있도록 Gradle을 구성합니다.

라고 되어있고 아래는 ./gradlew build로 빌드 스크립트를 실행한다.

 

여기서 gradlew을 많이 봤었는데 이거의 개념에 대해 제대로 알아보기로 하였다. 


gradlew == gradle wrapper

- 새로운 환경에서 프로젝트를 설정할 때 java나 gradlew을 설치하지 않고 바로 빌드할 수 있게 해준다.

- gradlew는 shell script이다. (그래서 run을 통해 실행한다.)


다시 이어서 두 번째 부분은

# NOTE: The Gradle Wrapper is the default and recommended way to run Gradle (https://docs.gradle.org/current/userguide/gradle_wrapper.html).
# If your project does not have the Gradle Wrapper configured, you can use the following configuration to run Gradle with a specified version.
# - name: Setup Gradle
#   uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
#   with:
#    gradle-version: '8.5'
#
# - name: Build with Gradle 8.5
#    run: gradle build

그전에 일단 여기는 주석처리가 되어있음을 알 수 있었다.

 

# 참고: Gradle Wrapper는 Gradle(https://docs.gradle.org/current/userguide/gradle_wrapper.html) 을 실행하는 기본 및 권장 방법입니다.
# 프로젝트에 Gradle Wrapper가 구성되지 않은 경우 다음 구성을 사용하여 지정된 버전으로 Gradle을 실행할 수 있습니다.

라고 되어있다. Gradlew가 없는 상황에서 gradle build를 하는 방식을 알려주는 것 같다.

여기는 Setup까지는 똑같고 gradle build를 통해 빌드한다.

 

빌드란 ?, 빌드 도구 Gradle, gradle build vs ./gradlew build (tistory.com)

 

빌드란 ?, 빌드 도구 Gradle, gradle build vs ./gradlew build

프로젝트를 하면서 밥먹듯이 하는게 컴파일 빌드 실행 과정인데 부끄럽게도 빌드가 정확히 무슨 역할인지는 잘 모르고 있었던 것 같다. GitHub Actions로 CI코드를 작성하면서 build를 하는 코드 부

soni-developer.tistory.com

여기에 좀 더 자세히 정리해놨고 간단히는

 

새로운 환경에서 Java와 Gradle의 설치나 버전관리가 필요없이 바로 빌드할 수 있게 해주기 때문에 Gradle wrapper 사용이 권장된다.

 

따라서 나는 ./gradlew build를 사용해주었다.

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Set up Gradle
        run: ./gradlew build

 

 

다른 블로그 코드들을 보면 아래와 같이 gradlew에 관해 권한을 부여해주는 부분이 있었다.

- name: grant execute permission for gradlew
  run: chmod +x gradlew

 gradle script를 실행할 수 없다는 에러가 날 때 기본적으로 권한이 없어서 발생하는 에러라고 해서 일단 나는 넣지 않았다.

 

CI 코드 전문

name: Java CI With Gradle

on:
  pull_request:
    branches: ["main"]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Set up Gradle
        run: ./gradlew build

 

이렇게 작성해놓고 relese 브랜치에 CI workflow를 push하고 테스트 코드를 하나 실패하게 해서 같이 push했다.

 

그리고 relese -> main으로 pr을 던지면 아래와 같이 실패했다고 뜬다 !

 

 

그런데 지금 테스트 여부와 관계없이 actions/checkout@v4 부분에서 에러가 났다.

버전 4를 찾을 수 없다고 한다.  아 ㅡㅡ 보니까 오타가 났었다.

 

actions/checkout@4 -> v4로 변경하고 

remote relese 브랜치에 push해주었다.

 

또 PR을 만들 필요 없이 이미 만들어뒀던 PR에서 다시 검사를 진행하게 된다.

 

 

이번 실패는 권한이 없어서 발생한 에러였다.

 

위쪽에 넣으려다 말았던 권한 부여 코드를 추가해주었다.

 

CI 코드 전문

name: Java CI With Gradle

on:
  pull_request:
    branches: ["main"]

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle Wrapper
        run: ./gradlew build

 

이번에 발생한 오류는 GradleWrapperMain을 찾거나 로드할 수 없다는 오류이다.

 

오류를 찾아보기 전에 제일 위에 템플릿에서 제공해주던 set up gradle을 내가 넣어주지 않았는데 그래서 오류가 발생한 건가 했다. 아래 코드를 추가해 줘도 여전히 같은 오류가 났다.

- name: Setup Gradle
  uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0

 

원인 : 깃허브의 .gitignore로 clone할 때 gradle-wrapper.jar 파일이 remote repository에 없어서 발생하는 오류.

 

해결방법 :  .gitignore에서 gradle-wrapper.jar 를 제거해주고 깃허브 레포지토리에 gradle-wrapper.jar를 push 해주었다.

(위에서 추가해줬던 setup 부분 코드 없앰.)

 


또 오류가 발생했는데 아래와 같은 오류이다.

 

오류 전문

Run ./gradlew build
Downloading https://services.gradle.org/distributions/gradle-7.3.3-bin.zip
...........10%...........20%...........30%...........40%...........50%...........60%...........70%...........80%...........90%...........100%

Welcome to Gradle 7.3.3!

Here are the highlights of this release:
 - Easily declare new test suites in Java projects
 - Support for Java 17
 - Support for Scala 3

For more details see https://docs.gradle.org/7.3.3/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

> Task :compileJava FAILED
/home/runner/work/GiftFunding/GiftFunding/src/main/java/com/soeun/GiftFunding/repository/FundingProductRepository.java:7: error: package io.lettuce.core.dynamic.annotation does not exist
import io.lettuce.core.dynamic.annotation.Param;
                                         ^
/home/runner/work/GiftFunding/GiftFunding/src/main/java/com/soeun/GiftFunding/repository/FundingProductRepository.java:59: error: cannot find symbol
1 actionable task: 1 executed
        @Param("send_at") Timestamp send_at);
         ^
  symbol:   class Param
  location: interface FundingProductRepository
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
2 errors

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':compileJava'.
> Compilation failed; see the compiler error output for details.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 38s
Error: Process completed with exit code 1.

보면 gradlew이 gradle을 프로젝트 환경에 맞게 받아준 것을 알 수 있다.

 

 

그리고 보면 @Param 때문에 오류가 발생했다. 

package io.lettuce.core.dynamic.annotation does not exist
import io.lettuce.core.dynamic.annotation.Param;

이 부분이 문제인 것 같다. 여기서 lettuce는 redis의 라이브러리로 스프링에서 제공하는 spring-boot-starter-data-redis 를 쓰면 Lettuce를 사용할 수 있다. 

 

내가 동시성 이슈 해결을 위해 Redisson 을 사용하면서 저 의존성을 지웠나 하고 확인해봤는데

여기 버젓이 존재한다. 그럼 왜 찾을 수 없다는 걸까 ?!?!?!?!? 흠..,.,

 

생각을 해보니까 @Param은 엔티티를 참조하는 레포지토리에서 @Query를 쓸 때 메소드의 파라미터를 쿼리문에 넣어서 사용하고 싶을 때 써주는 어노테이션이었다. (java 8+ 부터는 컴파일러에 -parameters 추가로 @Param 생략가능)

 

그런데 import 문이 lettuce랑 무슨 관련인지 !? 다른 import 문으로 수정해주었다.

 

해결되었다 !

 

-----------------

그래도 아직 빌드 성공은 하지 못 헀고 다음과 같은 오류가 났다.

 

원인 : 테스트가 싹 다 실패해서 빌드 실패

  -> 내가 친구의 펀딩상품 조회 성공 메소드에 given 절을 false로 일부러 실패하게 두었다.

이거 하나 때문에 싹다 실패했다고 뜬 건가 ? 일단 다시 성공하도록 바꿔두고 build를 다시 진행해보았다.

 

여전히 같은 오류가 발생했고 오류가 났다는 줄을 확인해보니까 document로 restdocs 세팅을 해준 부분들이었다.

 

로컬에서 테스트를 돌릴 때는 IllegalStateException이 @AutoConfigureRestDocs 안 붙여줬을 때 발생했다.

 

일단 IllegalStateException은 사용자가 값을 제대로 입력했지만 코드가 값을 처리할 준비가 안 된 경우에 발생한다고 한다.

 

 

의심1) 아 ? build.gradle을 remote repo에 안 올려서 그런가 ????는 아님 있었는데 다시 push 해봤음

역시 안 되고

의심20 open-api-3.0.1.json 레포에 안 올렸었는데 push 해봤음. -> 관계없음 여전히 같은 오류

 

다시 오류 문구를 보니까 테스트 47개가 통과하고 9개가 실패했다는데 내가 만든 테스트는 총 47개이다... .뭐지 ?

 

의심3) DB 연결을 해주지 않아서 ? -> 아니다.

workflow에 아래 코드를 추가해주었는데 달라지는 건 없었다.

      - uses: mirromutth/mysql-action@v1.1
        with:
          host port: 3307
          container port: 3306
          mysql database: 'giftfunding'
          mysql user: 'giftfunding'
          mysql password: ${{ secrets.DatabasePassword}}

 

워크플로우에 다음 코드를 추가해서 test 정보를 볼 수 있도록 해보았다.

      - name: Test with Gradle
        run: ./gradlew --info test

 

그랬더니 모든 실패한 테스트들에서 Failed to create directory '~~~'가 떴다 !

 

확인을 해보니까 테스트 코드에서 MockMvcRestDocumentationWrapper.document("~~~") 할 때

build/generated-snippets/freind/list 경로에 스닛펫을 생성하는데 오류는 해당 디렉토리를 만들 수 없다는 오류였다 !

 

그럼 코드에 write 권한을 추가해주면 ??????

 

build.gradle에 스닛펫이 생성될 디렉토리를 지정하는 아래 코드를 추가했지만 여전히 실패

ext {
	set('snippetsDir', file("build/generated-snippets"))
}

생각해보면 로컬에서도 위 코드 없이 build.generated-snippets 경로에 잘 생겼으니까 관련이 없는 것 같다.

 

그냥 혹시나 하고 여기 권한을 write로 바꿔봤는데도 여전했다.

 

?!

테스트 결과를 발행하는 step에서 나는 오류를 다시 확인해봤는데

이렇게 나왔다 !

 

권한에 따른 오류라길래 

기존에

contents: read를 그냥 write-all로 바꿨다.

 

여전히 실패는 하지만 변화는 생겼다 !

테스트 결고가 이렇게 보이고

 

실패한 로그 detail을 들어가면 이렇게 뜬다 ! 그렇다면 아까까진 Publish Test Results 액션이 write 권한이 없어서 실행되고 있지 않았나보다.

 

아까는 Test with Gradle과 Publish Test Results 모두 실패했었는데 write 권한을 주고 후자는 성공했다.

그렇지만 여전히 test는 실패하고 있으니 빌드가 될 수가 없다 ,.,.,,,.,.,. 기존 원인과는 관련이 없다. 

 

드디어 오류를 찾았다 진짜 .. ㅋ

원인

ㅋ... 내가 document하고 모든 파일명 맨 앞에 /를 붙였는데 이게 문제였다 .....................................

document는 test 수행시 snippets로 생성해주는 메소드인데 build/generated-snippets 폴더에 adoc 조각들을 만든다.

붙이면 안 됐던 이유는 루트에서 만들려고 하니까  build/generated-snippets 폴더가 당연히 없어서 그랬나보다 !

 

모든 테스트 코드 document에서 맨 앞에 /를 없애주니까 테스트가 통과했다 .... 드디어  ㅜㅜㅜ

 

그렇다면 위의 build.generated-snippets를 통한 경로가 들어갔을 때도 통과해야한다. 

 

역시 통과.

 

이것으로 얻은 교훈,,, 모든 경로의 맨 앞에 /를 붙이는 것을 지양하자 생각을 하자 !!

https://discuss.circleci.com/t/spring-java-8-project-rest-docs-failed-to-create-directory/32428

여기서 힌트를 얻음 ,,,,,,,,,,,,,,,,,,,,,,, 정말 감사합니다 ,,,,,,,,,,,,,,,,,,,,,,


+ Test 실패 시 merge 못 하도록 하기

repository -> setting -> branches -> Add branch protection rule

 

적용할 branch 명을 작성, Require status checkes to pass before merging 체크

테스트 성공/실패 상태를 체크할 job 이름을 지정. build의 성공 여부를 체크할 것이기 때문에 build라고 써주었다.

 

main 브랜치에 PR시 테스트 성공 여부에 따라 Merge 가능 여부가 표시된다.
 => 테스트 실패 시 merge가 불가능

 

요렇게 빌드가 실패하니까 merge 할 수 없도록 바꼈다 !

 


+ 테스트 리포트 추가하기 (Unit Test Results 추가)

workflow에 아래 코드를 추가해줬다.

(v2로 바꾸면 뭐 다른 점이 있나해서 해봤는데 일단은 똑같이 나온다. 조금이라도 최신버전으로 적용.)

      - name: Publish Unit Test Results
        uses: EnricoMi/publish-unit-test-result-action@v1
        if: always() # 테스트가 실패하여도 Report를 보기 위해 `always`로 설정
        with:
          files: build/test-results/**/*.xml

 

요렇게 뜹니다.

구글링해서 본 다른 곳들에는 permissons를 지정하는 코드가 딱히 없어도 동작을 하는데 나는 permissons : write-all이 없으면 403 에러가 난다.

 

넣어주고 정상 동작하는 것을 확인했다.

 

일부러 실패하는 테스트를 만들어놓고 다시 빌드 해보았다.

 어떤 커밋 때문에 실패하는지, 어떤 오류가 나는지 알 수 있다.

 


+ 캐싱으로 CI 속도 향상

로컬에서 SW 개발 할 때 외부 패키지는 최초 한 번만 설치하면 되지만

CI 서버에서는 매번 이 작업을 다시 해야하므로 시간이 소요된다.

 

이럴 때 캐시(Cache) 액션을 사용하여 CI 서버에서 발생할 수 있는 불 필요한 패키지 재설치를 예방할 수 있다.

 

깃허브 캐시 액션

깃허브가 제공하는 캐시 액션을 사용하면 Github Actions에서 워크 플로우가 실행될 때 필요한 파일 중에서 잘 바뀌지 안는 파일들을 깃허브의 캐시에 올려놓고 CI 서버로 내려 받을 수 있다.

-> 설치해야하는 패키지가 많은 플젝의 경우 깃허브 캐시 액션을 사용하면 워크플로우 성능을 최적화 하는데 상당한 도움이된다. 

 

프로젝트에서 의존하고 있는 외부 패키지를 매번 네트워크를 통해 원격 패키지 저장소로부터 내려받는 대신에 깃허브 캐시에 저장해두고 활용할 수 있기 때문.

 

- 동작원리

주어진 키에 해당하는 데이터가 깃허브의 캐시에 존재하는 경우 해당 파일을 CI 서버의 특정 경로에 내려받아 준다.

주어진 키에 해당하는 데이터가 깃허브의 캐시에 존재하지 않는 경우 CI 서버의 특정 경로에 있는 파일을 모두 캐시에 저장하여 다음 워크플로우가 실행될 때 해당 데이터가 캐시에 존재하도록 해줌.

 

- 캐시 액션 사용

워크플로우 YAML 파일에서는 steps 키 하위의 uses 키에 사용하고자 하는 액션의 위치를

{소유자}/{저장소명}@{참조자}의 형태로 명시. 

 

GitHub에서 제공하는 캐시 액션의 소유자는 actions이고, 저장소 이름은 cache

 

워크플로우 체크아웃 액션 다음에 아래 코드를 추가해주었다.

      - name: Cache Gradle Packages
        uses: actions/cache@v3
        with:
          path: ~./gradle/caches
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}

with의 두 인자가 중요하다.

path : 패키지 저장소에서 내려받은 패키지를 CI 서버에 저장해두는 경로

key : 깃허브의 캐시에서 데이터를 읽거나 쓸 때 사용되는 식별자 명시

 

* 주의 할 점 : 새로운 패키지 설치하거나 기존 패키지 제거 or 버전 업그레이드 할 때는 깃허브의 캐시에 저장해놓은 패키지를 사용하면 안 됨. 

따라서 패키지 설치 내역에 변경이 있을 때는 키도 함께 변경이 될 수 있도록 gitHubActions의 hashFiles() 내장함수를 이용하여 *.gradle, gradle-wrapper.properties 파일의 SHA 해시값을 키에 포함시켜준다.

=> 이러면 해당 파일들에 변경사항이 생길 때 파일의 SHA 해시값도 바뀌게 되니까 서로 다른 캐시가 되어버린다.

 

또한 워크 플로우를 여러 운영체제에서 실행할 경우를 대비하여 GitHub Actions의 runner.os 컨텍스트도 키에 포함시켰다.

CI 서버의 path에 위치한 디렉토리나 파일을 key를 통해서 캐시에 올리거나 내려받게 됨.

 

캐시 히트 : 캐시에 주어진 key에 해당하는 파일이 존재하여 CI 서버의 path로 해당 파일들을 내려받는다.

캐시 미스 : 캐시에 주어진 key에 해당하는 파일이 존재하지 않아, 해당 작업의 종료 시점에 CI 서버 상의 paht에 있는 파일들을 캐시에 저장한다.

 

다시 말 해, 캐시에 해당 파일이 있으면 패키지 저장소에서 새로 내려받는 것이 아니라 캐시에서 가져오고

캐시에 해당 파일이 없으면 패키지 저장소에서 내려받아 캐시에 저장하는게 된다.

 

코드를 작성하고 PR을 날리면 최초 1회 실행이니까 캐시 미스가 된다.

Cache Gradle Packages : 입력된 key에 해당하는 값이 없었다고 나온다.

Post Cache Gradle Packages : Cache에 230 MB 크기의 압축파일을 저장하는 것이 확인된다.

 

소요 시간은 1m 28s.

 

캐시 액션 설정 후에 최초 워크플로우 실행 시에는 전혀 빨라지지 않는다.

다만 프로젝트에 필요한 모든 패키지가 깃허브의 캐시에 저장됨.

 

Actions 탭에서 수동으로 워크 플로우를 재실행한 결과는 다음과 같다.

 

Cache Gradle Packages : 깃허브의 캐시에서 주어진 키에 해당하는 230MB의 압축 파일을 읽어와서 압축을 푸는 것 확인.

Post Cache Gradle Packages : 캐시 히트이기 때문에 캐시를 저장하지 않는 것을 확인

 

소요 시간 : 45s 로 거의 두 배 가량 단축된 것을 확인할 수 있다.