CI의 필요성
진행중인 사이드 프로젝트는 혼자 개발중이지만, 실무에서는 개인 개발을 하는 경우가 거의 없음으로 팀 개발임을 가정을 해보았습니다. 개발자들이 각자 담당했던 코드를 통합하다보면 충돌과 같은 오류가 빈번하게 발생하곤 합니다. 물론, 1인 개발 시에도 브랜치를 어떻게 따느냐에 따라 충돌이 발생할 수 있습니다. 따라서, 통합 후 직접 테스트를 하나하나 실행해보아야 하고, 직접 테스트를 하는 것 만큼 테스트에 실수를 일으킬 수도 있습니다. CI 환경을 구축한다면 이러한 번거로움을 줄이고 프로젝트의 신뢰성을 높일 수 있습니다.
CI 툴 선택 - Git Actions
공유 레포지토리로 GitHub을 사용하고 있기 때문에, Git Actions라는 GitHub에서 제공하는 CI/CD 플랫폼을 사용하기로 결정했습니다. Git Actions의 동작 원리를 설명하자면, 저장소에 특정 이벤트(ex.브랜치 푸쉬)가 발생하면 설정된 워크플로우가 실행됩니다. 이 때 워크플로우는 YAML 형식으로 작성된 파일에 단계별로 정의되어 있습니다. 각 단계에서는 코드 체크아웃, 빌드, 테스트 등의 작업이 수행됩니다. 이러한 작업들은 GitHub에서 제공하는 가상 머신인 러너에서 실행됩니다. 이를 통해 코드 변경사항을 자동으로 빌드하고 테스트하여 품질을 검증하고, 필요에 따라 배포할 수 있습니다. 러너는 GitHub에서 제공하는 표준 환경인 깃허브에서 호스트하는 러너와 사용자의 자체 환경에서 호스팅 할 수 있는 러너 중에서 선택할 수 있습니다. 전 별도의 설정이 필요없는 깃허브에서 호스트하는 러너만으로도 원하는 작업을 문제없이 수행할 수 있어, 해당 러너에서 액션을 실행하도록 설정했습니다.
Git Actions 도입 시 마주한 다양한 문제와 해결 방법
워크플로우를 설정하고 액션을 시도하는 과정에서 세 가지의 문제를 마주쳤습니다. 워크플로우 파일을 몇 번이고 수정하고 커밋하여 반복해서 아래와 같이 액션 테스트를 진행했습니다.

첫 번째 문제와 해결 방법
git actions를 사용하기 위해선 워크플로우 파일을 정의하는게 우선입니다. Github 레포지토리 - Actions 에서 깃허브 액션에서 제공하는 워크플로우 파일 중 Java with Gradle 를 선택하여 프로젝트에 쓰이는 JDK의 버전에만 맞춰 수정했습니다.
name: Java CI with Gradle
on:
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
# 체크아웃 - 서버에 코드 내려받기
- uses: actions/checkout@v3
# JDK 설치
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
# gradle을 사용해 프로젝트 빌드 및 테스트
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
with:
arguments: clean build
워크플로우 파일을 정의하고, PR을 날려보니 유저 컨트롤러 테스트를 진행할 때, 아래와 같은 에러가 발생했습니다.
Error: Gradle script '/home/runner/work/lets-deal/lets-deal/gradlew' is not executable.
gradlew 는 Gradle 빌드 도구를 자동으로 내려받아 프로젝트를 빌드할 수 있는 Graddle Wrapper 스크립트를 실행하는 명령어로, 이 에러는 gradlew 명령어를 실행할 수 있는 권한이 없어서 나타나는 에러였습니다. 따라서, 빌드 직전에 러너에게 해당 권한을 부여하는 작업을 추가해주었습니다.
# gradlew 파일 실행 권한 부여
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew
이후 워크플로우를 실행해보니, 해당 에러는 나타나지 않았으나, 또 다른 문제가 나타났습니다..
두 번째 문제와 해결 방법
이번엔 유저 컨트롤러 테스트의 모든 테스트에서 아래와 같은 예외가 발생했습니다.
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:142
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:800
Caused by: org.springframework.beans.factory.BeanCreationException at ConstructorResolver.java:659
Caused by: org.springframework.beans.BeanInstantiationException at SimpleInstantiationStrategy.java:171
Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException at DataSourceProperties.java:186
이 예외는 데이터 소스를 구성할 수 없어 발생한 예외 로, 데이터 소스가 담긴 application.yaml 파일을 찾지 못해 나타난 문제라고 생각했습니다. 저는 보안을 위해 application.yaml 파일을 공개 프로젝트 레포지토리에 직접 담지 않고, 개인 프로젝트 레포지토리에 올려 공개 프로젝트 레포지토리의 하위 디렉토리로 포함시키는 GitHub Submodule을 사용했기 때문에 해당 서브모듈에 문제가 있을 것이라고 생각했습니다. 찾아보니 역시나 서브모듈을 체크아웃 하기 위해 어떠한 설정이 필요했던 것이었습니다. 그 설정은 체크아웃 시에 레포지토리에 포함된 서브모듈을 함께 체크아웃한다는 설정으로 아래와 같습니다.
- uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.ACTION_TOKEN }}
여기서 해당 서브모듈은 private 하기 때문에 접근할 때 필요한 깃허브 사용자 토큰을 전달해주어야 합니다. 해당 토큰은 공개되면 안되기 때문에 Github Actions의 secret 변수에 저장해 전달해주었습니다.
이후 워크플로우를 실행해보니, 또 !! 해당 예외는 나타나지 않고, 다른 예외가 발생하였습니다..
세 번째 예외와 해결 방법
java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:142
Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1770
Caused by: jakarta.persistence.PersistenceException at AbstractEntityManagerFactoryBean.java:421
Caused by: org.hibernate.exception.JDBCConnectionException at SQLStateConversionDelegate.java:98 Caused by: org.postgresql.util.PSQLException at ConnectionFactoryImpl.java:354
Caused by: java.net.SocketTimeoutException at NioSocketImpl.java:551
이 예외는 postgresDB와 연결이 실패했을 때 발생하는 예외입니다. 글을 쓰는 시점에서는 CD까지 구축한 상황이라, CD를 구축하며 해당 예외의 원인이 DB인스턴스의 보안 문제임을 알게 되었지만, 이 예외가 발생했을 시점에는 밤새 원인을 찾지 못했습니다. 데이터소스에 실제 DB인스턴스를 담고 있었는데, DB인스턴스 자체의 문제인지도 모르고 무작정 구글링을 통해 워크플로우 파일을 수정해가며 많은 삽질을 시도했습니다. 그러다 결국 755명의 개발자 오픈카톡방인 위XX아XXX에 "보통 깃액션에서 테스트 시엔 운영DB가 아닌 로컬DB를 사용해야 하나요?" 란 질문을 하였더니, "테스트DB, 보통 테스트 컨테이너를 사용한다"는 답변을 받았습니다. 러너에 로컬 컨테이너를 사용하여 테스트DB를 띄운다는 얘기인데, 생각해보니 굳이 로컬로 띄우는 테스트DB보다 속도가 더 느린 운영DB에 연결할 필요가 없었습니다. 또한, 아무리 Mock객체를 통해 테스트를 한다고 하더라도 테스트에서 예기치 못한 문제가 발생할 수도 있기에 액션 테스트 시, 운영DB에 연결하는 것은 옳지 않다고 판단되었습니다.
따라서, application.yaml 에 spring profiles를 사용해 로컬과 운영 환경을 나누어 설정하여, 로컬에선 localhost로, 운영에선 실제 인스턴스에 연결할 수 있도록 설정했습니다. (참고로 저는 postgres 뿐만 아니라 redis 도 사용하고 있기에 redis 컨테이너도 필요했습니다.)
# application.yaml
# local, prod 공통 설정
server:
#...생략...
jwt:
#...생략...
aws:
#...생략...
---
#local
spring:
config.activate.on-profile: local
servlet:
multipart:
maxFileSize: 5MB
maxRequestSize: 5MB
spring.jpa:
database: postgresql
hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
hibernate.ddl-auto: update
properties.hibernate.format_sql: true
show-sql: true
spring.datasource:
hikari.maximum-pool-size: 4
url: jdbc:postgresql://localhost:5432/letsdeal
username: postgres
password: postgres
platform: postgres
driver-class-name: org.postgresql.Driver
mybatis:
type-aliases-package: com.example.domain
mapper-locations: classpath:mapper/*.xml
spring.data.redis:
url: redis://localhost:6379
#prod
spring:
config.activate.on-profile: prod
#...생략...
해당 파일은 서브모듈인 개인 레포지토리에서 직접 커밋하여, 주 레포지토리에서 해당 서브모듈을 업데이트 후 푸쉬해야 수정한 내용을 주 레포지토리에 반영할 수 있습니다.
로컬 환경에서도 테스트 시에는 무조건 profile이 local인 설정만 가져올 수 있도록 모든 테스트 클래스에 아래와 같은 어노테이션을 붙여주었습니다.
@ActiveProfiles("local")
환경에 따라 설정을 분리했으니, 이제 워크플로우에서 프로젝트를 테스트할 때 로컬환경으로 실행하도록 설정해주면 됩니다. 또한, postgres와 redis 서버는 localhost를 사용하고 있으니 러너에 해당 연결 정보를 가진 컨테이너들을 띄워주어야 합니다.
name: Java CI with Gradle
on:
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:latest
env:
POSTGRES_DB: letsdeal
POSTGRES_PASSWORD: postgres
POSTGRES_USER: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
with:
submodules: true
token: ${{ secrets.ACTION_TOKEN }}
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew
- name: Start Redis
uses: supercharge/redis-github-action@1.1.0
with:
redis-version: 6
- name: Build with Gradle
run: ./gradlew build -x test
- name: Test with Gradle
run: SPRING_PROFILES_ACTIVE=[local] ./gradlew test
기존의 워크플로우에선 gradle 빌드 액션을 사용하였기에, 빌드와 테스트가 동시에 이루어졌으나 더 명확한 작업 분리를 위하여 빌드와 테스트를 나누어 작성하였습니다. 테스트 시엔 profile이 local로 실행되도록 설정했습니다. 그리고, 공식문서에 따라 postgres 컨테이너를 띄우고, 커스텀된 액션을 불러와 redis 컨테이너를 띄우도록 설정했습니다.
결과 : 워크플로우 실행 성공
모든 설정을 마치고 CI를 테스트해본 결과..!!!! 드디어, 빨간색 x 표시가 아닌 초록색 체크 표시를 볼 수 있었습니다..!! 또한, 정의한 워크플로우대로 잘 실행되는 것을 확인할 수 있었습니다.

참고 자료
https://docs.github.com/actions
https://docs.gradle.org/current/userguide/userguide.html
https://docs.spring.io/spring-boot/docs/2.0.6.RELEASE/reference/html/boot-features-profiles.html
https://bcp0109.tistory.com/362
'Technical Issue' 카테고리의 다른 글
| 1시간 30분 → 1초: 10만 사용자 대상 쪽지 전송 API 최적화 (0) | 2025.02.09 |
|---|---|
| 동시성 이슈를 고려한 Redisson 분산락 적용 (0) | 2023.11.03 |
| DB I/O 최소화를 위한 사용자 정보 캐싱 (0) | 2023.10.25 |
| Github Actions와 AWS CodeDeploy를 활용한 CD 구축 (0) | 2023.10.15 |
| 사용자 인증 및 인가 - JWT & Spring Security & Redis (0) | 2023.09.10 |