깃허브에서 제공하는 CI/CD 서비스이다. 해당 서비스를 이용하여 코드를 자동으로 빌드하고 테스트하고 배포할 수 있다. 깃허브 저장소의 특정 이벤트에 대한 반응으로 실행되는 작업 단위는 액션이라 칭한다.
예를 들어, 리포지토리에 코드가 푸시될 때마다 빌드 및 테스트를 실행하여 배포하거나, 새로운 이슈가 열릴 때마다 알림을 보내는 등의 작업을 설정할 수 있다. 이를 통해 개발 및 배포 프로세스를 자동화하고 효율성을 높일 수 있다.
필자는 인텔리제이의 자체 깃허브 연동 기능을 이용하여 프로젝트를 연동시켰지만, 교재에서는 직접 터미널 명령어로 프로젝트를 연동시키는 법으로 진행하여, 명령어로 연동 시키는 법을 익히고자 다시 정리해보았다.
인텔리제이의 프로젝트를 연 후 터미널 창을 열고 git init
명령어를 입력한다.
git init
은 특정 폴더를 깃 저장소로 만들 때 사용하는 명령어이다.
명령어 입력 후 프로젝트 폴더에 숨김 폴더로 .git 폴더가 생기는데, 이 폴더에 코드의 변경 내역(버전) 관리를 위한 정보를 저장하게 된다. (해당 폴더를 지우면 버전 관리 내역이 모두 지워지므로 주의한다.)
깃허브에서는 리포지토리를 생성하였고, 로컬에서는 스프링 프로젝트를 깃 저장소로 생성한 것이다. 이제 이 둘을 연결시켜줘야 한다.
깃허브의 리포지토리와 로컬의 깃 저장소를 연결하기 위해 remote
명령어를 입력한다.
git remote add origin [깃허브 리포지토리의 SSH 주소]
add 명령어를 사용해 현재 프로젝트의 폴더의 모든 파일을 스테이지에 올린다. 스테이지란, 리포지토리에 파일들을 푸시하기전에 변경사항들을 미리 저장해두는 곳이다.
git add .
스테이지에 올린 사항들을 로컬 깃 저장소에 올리기 위해 commit
명령어를 입력한다.
커밋을 해야만 로컬 저장소에 변경 이력, 변경한 파일들이 업데이트 된다.
git commit -m [커밋 메시지]
브랜치명을 main으로 바꾼 후 push
명령어를 입력해 원격 저장소에 변경 내용들을 저장한다.
git branch -M main
-M 옵션
브랜치를 강제로 이동시키는 옵션이다. 따라서 해당 명령어를 실행하면 기존의 브랜치를 main으로 변경하게 된다.
cf. master라는 브랜치명이 노예제를 연상 시킨다는 이유로 브랜치명을 main으로 바꾸는 추세이다.
깃허브 액션 스크립트를 작성해 CI를 구현해본다.
프로젝트 최상단에 .github 디렉토리 생성 후 바로 하위 폴더로 workflows 디렉토리를 만들고 그 안에 ci.yml 파일을 작성해준다.
name: CI # - 1
on: # - 2
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest # - 3
steps: # - 4
- uses: actions/checkout@v3
- uses: actions/setup-java@v3
with:
distribution: 'corretto'
java-version: '17'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build with Gradle
run: ./gradlew clean build
name:
워크플로의 이름을 지정한다.
on:
워크플로우를 어느 시점에 실행할 지 정의한다.
위 워크플로우는 push 이벤트가 발생하고 해당 이벤트의 브랜치가 main일 때 실행하도록 설정되어 있다.
runs-on:
깃허브 액션이 실행될 환경을 지정한다.
위 워크플로우는 최신 버전의 Ubuntu 환경을 사용하도록 설정되어 있다.
steps:
워크플로우에서 실행할 단계를 정의한다. 각 단계는 별도의 작업 또는 명령어로 구성된다.
uses
지정한 리포지토리를 확인하고 코드에 대한 작업을 실행할 수 있다.
uses: actions/checkout@v3
checkout이라는 작업의 v3 버전을 실행한다. 해당 액션을 사용하면 워크플로우가 실행될 때 항상 깃허브 저장소의 최신 코드를 로컬 저장소로 체크아웃(원격 저장소의 내용을 로컬 저장소로 가져옴) 할 수 있게된다.
uses: actions/setup-java@v3
Java 개발 환경을 설정한다. 여기서는 Amazon Corretto 버전 17을 사용하도록 설정되어 있다.
name
각 단계의 이름을 지정한다.
with
GitHub Actions에서 특정 액션을 설정할 때 사용되는 매개변수를 지정하는 데 사용된다. 이를 통해 액션이 실행될 때 필요한 입력 값을 전달할 수 있다. 각 액션마다 with 블록에서 설정할 수 있는 매개변수들이 다르다.
run
실행할 명령어를 지정하는 키워드이다.
run: chmod +x gradlew
gradlew 파일에 실행 권한을 부여하는 명령이다. 보통 Gradle 프로젝트에는 프로젝트 루트 디렉토리에 gradlew 파일이 있는데, 이 파일은 Gradle 빌드를 실행하기 위한 스크립트 파일이다. 하지만 기본적으로 실행 권한이 없으므로, 해당 단계에서 chmod +x gradlew 명령어를 사용하여 실행 권한을 추가해줘야 한다.
run: ./gradlew clean build
Gradle을 사용하여 프로젝트를 빌드하는 명령이다. ./gradlew clean build 명령어는 Gradle 스크립트인 gradlew를 실행하고, clean build 파라미터를 전달하여 프로젝트를 클린 빌드합니다. clean은 이전 빌드 결과를 정리하고, build는 새로운 빌드를 실행한다. 따라서 해당 단계를 통해 Gradle을 사용하여 프로젝트를 빌드하고, 빌드 결과를 확인할 수 있다.
아래 명령어들을 통해 커밋을 하고 원격 저장소에 푸시를 진행하고, 깃허브 리포지토리의 [Action] 메뉴에 들어가 CI가 실행되는 것을 확인한다.
git add .
git commit -m [커밋 메시지]
git push origin main
위와 같은 과정을 거친 후, CI가 성공적으로 도입되었으면 좋겠다만, 위와 같은 오류가 발생하였다.
Build with Gradle 단계에서 오류가 발생하였는데, 상세 오류는 다음과 같았다.
Run ./gradlew clean build
Downloading https://services.gradle.org/distributions/gradle-8.5-bin.zip
............10%.............20%............30%.............40%.............50%............60%.............70%.............80%............90%.............100%
Welcome to Gradle 8.5!
Here are the highlights of this release:
- Support for running on Java 21
- Faster first use with Kotlin DSL
- Improved error and warning messages
For more details see https://docs.gradle.org/8.5/release-notes.html
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :clean UP-TO-DATE
Note: /home/runner/work/Blog_SpringProject/Blog_SpringProject/src/main/java/me/InKyung/Blog/springbootdeveloper/util/CookieUtil.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
> Task :compileJava
> Task :processResources
> Task :classes
> Task :resolveMainClassName
> Task :bootJar
> Task :jar
> Task :assemble
> Task :compileTestJava
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
BlogApplicationTest > contextLoads() FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:798
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at AutowiredAnnotationBeanPostProcessor.java:895
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at AutowiredAnnotationBeanPostProcessor.java:895
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at AutowiredAnnotationBeanPostProcessor.java:895
Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:651
Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:177
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:798
Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1773
Caused by: java.lang.IllegalStateException at OAuth2ClientProperties.java:68
TokenProviderTest > validToken(): 유효한 토큰인 경우에 유효성 검증에 성공한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
TokenProviderTest > validToken(): 만료된 토큰인 경우에 유효성 검증에 실패한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
TokenProviderTest > getAuthentication(): 토큰 기반으로 인증정보를 가져올 수 있다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
TokenProviderTest > getUserId(): 토큰으로 유저 ID를 가져올 수 있다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
TokenProviderTest > generateToken(): 유저 정보와 만료 기간을 전달해 토큰을 만들 수 있다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
BlogApiControllerTest > deleteArticle: 아티클 삭제에 성공한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:798
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at AutowiredAnnotationBeanPostProcessor.java:895
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at AutowiredAnnotationBeanPostProcessor.java:895
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at AutowiredAnnotationBeanPostProcessor.java:895
Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:651
Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:177
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:798
Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1773
Caused by: java.lang.IllegalStateException at OAuth2ClientProperties.java:68
BlogApiControllerTest > findArticle: 아티클 단건 조회에 성공한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
BlogApiControllerTest > findAllArticles: 아티클 목록 조회에 성공한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
BlogApiControllerTest > addArticle: 아티클 추가에 성공한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
BlogApiControllerTest > updateArticle: 아티클 수정에 성공한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
TokenApiControllerTest > createNewAccessToken: 새로운 액세스 토큰을 발급한다. FAILED
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145
12 tests completed, 12 failed
FAILURE: Build failed with an exception.
* What went wrong:
> Task :test FAILED
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///home/runner/work/Blog_SpringProject/Blog_SpringProject/build/reports/tests/test/index.html
* Try:
> Run with --scan to get full insights.
BUILD FAILED in 33s
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.5/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
8 actionable tasks: 7 executed, 1 up-to-date
Error: Process completed with exit code 1.
위 오류들을 처음 봤을 때는 테스트 코드에서 오류들이 발생하고 있음을 알 수 있었다. 또한 위 오류 코드에서 Caused by: java.lang.IllegalStateException at OAuth2ClientProperties.java:68
부분을 보아 결국은 OAuth2 관련 오류로 인해 테스트 코드들이 오류가 발생하고 있음을 인지하였다.
결국 오류 코드를 보아 발생하고 있는 문제를 한 문장으로 정의해 보자면 다음과 같다.
OAuth2 오류로 인해 발생한 테스트 코드 실행 문제.
하지만 로컬에서도 잘 실행 되었었고, AWS ElasticBeanstalk에 배포도 잘 되었던 상황에서, 왜 이러한 오류가 발생했을까라는 의문이 들었다. OAuth2 설정과 관련하여 로컬과 깃허브 환경의 차이가 무엇일지 먼저 생각해보았다.
로컬과 깃허브 환경에서 가장 큰 차이는 application.yml의 clint-id와 client-secret 값들을 보안의 이유 때문에 해당 부분은 임의의 값을 입력해 놓았던 부분이다. 물론 로컬에서는 application.yml을 .gitignore 파일에 포함시켜 해당 값들이 푸시되지 않게 따로 설정해놨다.
그래서 로컬에서는 OAuth2 인증이 정상적으로 동작하여 빌드가 정상적으로 동작하였고, 깃허브 환경에서는 OAuth2 부분이 정상적으로 동작하지 않아 테스트 코드에 있는 OAuth2 로직들이 오류를 발생시켜 테스트 코드들이 정상적으로 동작하지 않았던 것으로 예상이 되었다.
따라서 client-id 값과 clinet-secret 값을 깃허브 액션에서 Secrets로 환경변수를 다음과 같이 설정하여 application.yml을 수정하기로 하였다.
GitHub Action Screts 등록법
application.yml 수정
spring:
jpa:
show-sql: true
properties:
hibernate:
format_sql: true
defer-datasource-initialization: true
h2:
console:
enabled: true
security:
oauth2:
client:
registration:
google:
client-id: ${OAUTH2_CLIENT_ID}
client-secret: ${OAUTH2_CLIENT_SECRET}
scope:
- email
- profile
jwt:
issuer: qlql7748@gmail.com
secret_key: study-springboot
위와 같은 과정들을 거친 후 다시 깃허브 액션으로 돌아가 CI가 정상적으로 도입이 되었는지 확인해본다.
정상적으로 CI가 도입되었음을 확인할 수 있다.