| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- docker
- EC2
- Integration Test
- 제2종오류
- JPA
- MySQL
- 책임분리
- SolveGO
- 백엔드 로그인 구현
- 테스트 코드
- query optimization
- Service Layer
- springboot
- backend
- performance test
- CI
- rest api
- Spring Security
- SelfHostedRunner
- 제1종오류
- Spring Boot
- ERD설게
- 틀린문제조회
- B+Tree
- JWT
- 백엔드 회원가입 구현
- AWS
- 백엔드
- Slice Test
- branch protection
- Today
- Total
노트
[SolveGO #16] "맘대로 해, 되감으면 되니까" - Git과 CI 적용 본문

우리는 흔히 3차원 공간에 산다고 말한다. 여기서 "차원"이라는 의미는 무엇일까?
"차원"은 독립적으로 움직일 수 있는 "방향"의 수를 나타낸다. 우리는 독립적으로 우리의 신체를 x,y,z 3가지 방향으로 움직일 수 있기 때문에 3차원 공간에 존재한다고 볼 수 있을 것이다.
"저 사람은 4차원적인 기질이 있다"라는 말은 이러한 관점에서 볼 때, 상당히 의미가 깊은 말이다. 흔히 우리가 이런 말을 사용하는 대상은, 기발한 생각을 하거나 어디로 튈지 모르는 사람이 될 때가 많다. 이는 우리의 세상이 움직이고 이해할 수 있는 3가지 방향에 자신만의 축이 하나 더 존재하여 추가로 다른 방향으로 움직인다는 의미이고, 다른 사람들은 그 방향을 인지하지 못하기에 기발하거나 독특하다는 생각을 하게 되는 것이다.
한편, 그 하나의 추가적인 차원을 시간으로 다루게 되면 많은 이의 흥미를 유발하는데, 이는 시간 여행이라는 컨셉으로 많은 영화나 게임에서도 다뤄진다. 롤(리그오브레전드)에서도 이를 기반으로 한 챔피언이 있는데, 교전을 하다가 자신이 위험한 상황에 처하게 되면 시간을 되돌려 몇 초 전의 위치로 이동하는 독특한 기술을 가지고 있어 출시 초기부터 많은 사람들의 관심을 받았던 캐릭터이다.

이렇듯 사람들은 이렇게 "시간 여행"이라는 설정에 열광하게 되는데, 이는 3차원에 사는 우리의 한계를 뛰어넘어 4차원에 사는 게 어떤 기분일지를 판타지로나마 접하고 싶은 열망의 투영일 것이다. 특히 현실세계에서 우리는 어떤 잘못을 하게 되면 그것을 주워담을 수 없다. 시간축을 자유롭게 이동할 수 없기 때문이다. 그러나 에코같이 시간축을 자유롭게 이동할 수 있다면 이전에 했던 잘못들이 존재하기 이전으로 되돌릴 수 있고, 더 지혜롭게 삶을 살 수 있지 않을까.
그런데 개발자들은 시공간은 아니더라도 일종의 코드 공간에서 시간을 되돌릴 수 있는 기능을 발명해냈다. 에코와 비슷하게 작업을 하다가 위험한 상황에 처하게 되면 코드를 이전의 시간선으로 되돌릴 수 있게 되는 기능 말이다. 심지어 새로운 시간선을 만들 때마다, 그 시간선이 언제든지 돌아갈만큼 안전한지 파악하며 그러한 시간선들만 유지할 수도 있다. 이를 통해 문제가 있는 코드가 main 시간선에 합류하지 못하게 막고, 상대적으로 안전한 시간선들의 집합(branch)을 유지할 수 있다.

우리 개발자는 Git을 통해서 과거의 시간선으로 코드를 되돌릴 수 있게 되고, 그 시간선이 최소한 작성된 테스트와 빌드 기준을 통과하는지 확인하기 위해 CI, 즉 Continuous Integration을 수행한다.
그러면 지금부터 테스트 코드까지 작성한 SolveGO 프로젝트에 Git과 CI를 적용해보자. 이를 통해 앞으로의 개발 과정에서 코드의 시간선을 기록하고, 새로운 변경 사항이 main에 합류해도 안전한지 자동으로 검사할 수 있도록 만들 것이다.
Git
먼저 application.yaml에서는 DB 비밀번호나 JWT Secret 같은 민감정보를 직접 작성하지 않고, 환경변수로 주입받도록 설정한다. 이렇게 하면 GitHub에 코드를 올리더라도 설정 파일 안에 민감정보가 직접 노출되는 것을 막을 수 있다.
spring:
datasource:
url: ${DB_URL:jdbc:mysql://localhost:3306/solvego}
username: ${DB_USERNAME:root}
password: ${DB_PASSWORD:}
jpa:
hibernate:
ddl-auto: create
show-sql: true
properties:
hibernate:
format_sql: true
jwt:
secret: ${JWT_SECRET}
expiration: 600000
springdoc:
override-with-generic-response: false
server:
port: 8080
그리고 아래와 같이 Git을 초기화하여 현재 프로젝트 폴더를 로컬 Git 저장소로 만든다. 이후 현재 파일들을 스테이징하고, 하나의 커밋으로 기록한다.
git init
git status
git add .
git commit -m "feat: implement SolveGO MVP backend"
git branch -M main
git remote add origin <github-repo-url>
git push -u origin main
여기서 매우 자주 사용하는 깃 명령어
git push origin main
은 git push origin main:main의 축약형이다. 로컬 브랜치 main을 원격 브랜치 main에 push 한다는 의미이다. 그리고 일반적으로 from : to 의 방향성을 지닌다. 그리고 이 : 전후의 브랜치 이름이 같을 경우 중복을 생략하여 하나의 브랜치명으로 나타내는데, 그것이 위에 나온 것이다.
또한 여기서 -u는 --set-upstream의 줄임말이다. 로컬 main 브랜치가 원격의 origin/main 브랜치를 추적하도록 설정한다. 이후에는 git push나 git pull만 입력해도 Git이 현재 브랜치의 기본 원격 브랜치를 알 수 있다.

그리고 push까지 정상적으로 완료하면 아래와 같이 깃허브에 정상적으로 push가 완료된 것을 확인할 수 있다.

GitHub Actions CI
이렇게 Git을 초기화하고 commit함으로써 로컬 저장소에서 작업한 코드의 시간선을 기록하여 이제 개발 중 문제가 생기더라도 이전 커밋으로 돌아가거나, 새로운 브랜치를 만들어 별도의 시간선에서 작업할 수 있게 되었다. 또한 이 커밋들을 원격 저장소인 GitHub에 push함으로써, 로컬에만 존재하던 코드의 시간선을 원격 저장소에서도 관리할 수 있게 되어, 다른 사람도 같은 저장소를 clone하거나 fetch하여 동일한 코드 이력을 공유할 수 있다.
그렇다면 이제 남은 것은 새로운 시간선이 main에 합류해도 되는지 검사하는 일이다. 이 역할을 CI가 수행한다. 코드를 브랜치에 push하거나 Pull Request를 생성하면 GitHub Actions가 자동으로 실행된다. 이때 빌드와 테스트 코드가 통과하는지 확인하고, 실패할 경우 해당 변경 사항이 main 브랜치에 병합되지 못하도록 막게 할 예정이다. 즉 Git이 코드의 시간선을 기록하고 되돌릴 수 있게 해준다면, CI는 새로 만들어진 시간선이 main에 합류해도 되는지 자동으로 검사하는 관문 역할을 할 것이다.

우리는 CI를 GitHub Actions CI를 이용하여 적용할 것인데, 여기서 주목할 부분은 GitHub Actions CI는 push 자체를 막는 도구가 아니다. 브랜치에 push가 발생하거나 Pull Request가 생성되면, 그 이후에 GitHub Actions가 실행되어 빌드와 테스트를 수행한다.
즉 흐름은 다음과 같다.
push / PR 생성
↓
GitHub Actions CI 실행
↓
빌드, 테스트, 커버리지 검사
↓
성공 또는 실패 표시
따라서 테스트가 실패하더라도 feature 브랜치에 push하는 것 자체는 가능하다. 다만 테스트를 실패하여 CI가 실패하면 해당 변경사항은 작성된 테스트와 빌드 기준을 통과하지 못한 상태로 표시되고, main 브랜치에 병합될 수 없도록 막을 수 있다. 이때 필요한 것이 Branch Protection이다. Branch Protection은 main 브랜치에 대해 다음 규칙을 강제한다.
main 직접 push 금지
Pull Request 필수
CI 통과 필수
결국 GitHub Actions CI와 Branch Protection을 함께 사용하면 다음 구조가 된다.
GitHub Actions CI
= 새 코드가 테스트를 통과하는지 검사
Branch Protection
= 테스트를 통과하지 못한 코드가 main에 들어오지 못하게 차단
즉 Git이 코드의 시간선을 기록한다면, CI는 새 시간선을 검사하고, Branch Protection은 검증되지 않은 시간선이 main에 합류하지 못하도록 막는 관문 역할을 한다.
GitHub Actions CI는 GitHub에서 제공하는 자동화 도구로, 특정 이벤트가 발생했을 때 미리 정의한 작업을 자동으로 실행한다. SolveGO에서는 브랜치에 push되거나 main 브랜치로 Pull Request가 생성될 때, GitHub Actions가 자동으로 테스트를 실행하도록 설정했다.
Branch CI의 나머지 jobs, services, env, steps 구성은 뒤에서 설명할 PR CI와 거의 동일하므로 여기서는 트리거 설정만 설명한다.
name: Branch CI
on:
push:
branches:
- main
- 'feature/**'
- 'fix/**'
- 'chore/**'
- 'infra/**'
이 설정은 다음 브랜치에 push가 발생하면 CI를 실행한다는 의미이다.
main
feature/**
fix/**
chore/**
infra/**
즉 기능 개발 브랜치나 인프라 작업 브랜치에 코드를 push하면, GitHub Actions가 자동으로 테스트를 수행한다.
feature 브랜치 push
↓
Branch CI 실행
↓
테스트 / 커버리지 검사
이때 feature 브랜치는 작업 중인 코드가 올라가는 공간이므로, push 자체는 허용된다. 대신 CI 결과를 통해 현재 브랜치가 테스트를 통과하는 상태인지 확인할 수 있다.
name: PR CI
on:
pull_request:
branches:
- main
jobs:
pr-test:
name: PR backend tests
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: solvego_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot --silent"
--health-interval=10s
--health-timeout=5s
--health-retries=10
env:
SPRING_PROFILES_ACTIVE: test
DB_URL: jdbc:mysql://127.0.0.1:3306/solvego_test
DB_USERNAME: root
DB_PASSWORD:
JWT_SECRET: testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Grant execute permission for Gradle
run: chmod +x gradlew
- name: Run tests with coverage
run: ./gradlew test jacocoTestReport
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: pr-jacoco-coverage-report
path: build/reports/jacoco/test/html
name: PR CI
on:
pull_request:
branches:
- main
이 설정은 main 브랜치를 대상으로 Pull Request가 생성되거나 갱신될 때 CI를 실행한다는 의미이다.
Pull Request to main
↓
PR CI 실행
↓
테스트 / 커버리지 검사
↓
통과해야 main merge 가능
이 PR CI는 이후 Branch Protection과 연결된다. 즉 PR backend tests가 통과하지 않으면 해당 Pull Request는 main에 병합될 수 없다.
jobs:
pr-test:
name: PR backend tests
runs-on: ubuntu-latest
jobs는 GitHub Actions에서 실행할 작업 단위이다. 여기서는 Ubuntu 환경에서 백엔드 테스트를 실행한다.
runs-on: ubuntu-latest
는 GitHub가 제공하는 최신 Ubuntu 가상 환경에서 작업을 실행하겠다는 의미이다.
중요한 부분은 이 이름이다.
name: PR backend tests
이 이름은 Branch Protection에서 required status check로 사용된다. 따라서 이 이름을 바꾸면, GitHub의 보호 규칙에서 요구하는 체크 이름도 같이 수정해야 한다.
services:
mysql:
image: mysql:8.0
SolveGO 테스트는 MySQL을 사용한다. 그래서 CI 환경에서도 테스트용 MySQL 컨테이너를 함께 실행한다.
env:
MYSQL_ALLOW_EMPTY_PASSWORD: yes
MYSQL_DATABASE: solvego_test
이는 테스트용 DB를 생성하는 설정이다.
DB 이름: solvego_test
username: root
password: 없음
운영 환경이 아니라 CI 테스트 환경이기 때문에 빈 비밀번호를 허용했다.
env:
SPRING_PROFILES_ACTIVE: test
DB_URL: jdbc:mysql://127.0.0.1:3306/solvego_test
DB_USERNAME: root
DB_PASSWORD:
JWT_SECRET: testtesttesttesttesttesttesttesttesttesttesttesttesttesttesttest
이 설정은 GitHub Actions 환경에서 Spring Boot 테스트를 실행할 때 사용할 환경변수다. 로컬의 application.yaml에서는 DB 정보와 JWT Secret을 환경변수로 받도록 설정해두었기 때문에, CI에서도 동일하게 환경변수를 주입해야 한다.
SPRING_PROFILES_ACTIVE=test
는 테스트 프로파일을 사용하겠다는 의미이다.
- name: Checkout source code
uses: actions/checkout@v4
GitHub 저장소의 코드를 CI 실행 환경으로 가져온다.
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
SolveGO는 Java 17을 사용하므로, CI 환경에도 JDK 17을 설치한다.
- name: Grant execute permission for Gradle
run: chmod +x gradlew
Ubuntu 환경에서 gradlew를 실행할 수 있도록 실행 권한을 부여한다.
- name: Run tests with coverage
run: ./gradlew test jacocoTestReport
테스트를 실행하고 JaCoCo 커버리지 리포트를 생성한다.
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: pr-jacoco-coverage-report
path: build/reports/jacoco/test/html
생성된 HTML 커버리지 리포트를 GitHub Actions artifact로 업로드한다. 이를 통해 CI가 끝난 뒤에도 GitHub에서 테스트 커버리지 결과를 다운로드하여 확인할 수 있다.
따라서 이제부터 main 브랜치는 Branch Protection에 의해 보호된다. main에는 직접 push할 수 없고, 모든 변경사항은 별도 브랜치에서 작업한 뒤 Pull Request를 통해 병합해야 한다. feature, fix, chore, infra 브랜치에는 작업 중인 코드가 push될 수 있다. 이때 GitHub Actions는 Branch CI를 실행하여 테스트와 커버리지 리포트를 생성한다. 다만 이 단계의 CI 결과는 현재 브랜치의 상태를 확인하기 위한 것이며, push 자체를 막지는 않는다.
반면 main 브랜치로 Pull Request를 생성하면 PR CI가 실행된다. Branch Protection에서는 이 PR CI의 status check를 필수 조건으로 설정했기 때문에, PR backend tests가 통과하지 않으면 main에 merge할 수 없다. 즉 feature 브랜치는 실험과 작업을 위한 시간선이고, main 브랜치는 CI를 통과한 코드만 합류할 수 있는 안정적인 시간선인 셈이다.

먼저 어떤 commit을 원격저장소에 등록하면, 이후에 자동으로 작성한 CI/test가 수행되는 모습을 볼 수 있다.

그리고 이 테스트가 성공으로 끝나면 체크표시가, 실패로 끝나게 되면 X표시가 남게 된다.
그래서 개발자는 이 표시를 통하여 현재 원격저장소에 있는 커밋이 테스트를 통과하는지의 유무를 확인할 수 있다.
그러나, 이는 어디까지나 개발자에게 알려주기 위한 작업일 뿐, push 자체를 막는 기능은 아님을 기억해둘 필요가 있다. CI test는 push가 일어난 뒤에야 비로소 이뤄지는 과정이기 때문이다. 만약 push 자체를 사전에 막고 싶다면 로컬의 pre-push hook을 사용할 수 있다. 하지만 이는 현재 로컬 머신에만 적용되는 보조 장치이며, --no-verify 옵션으로 우회할 수도 있다. 따라서 원격 저장소 차원에서는 feature 브랜치의 push를 막기보다는, CI와 Branch Protection을 통해 검증되지 않은 코드가 main 브랜치에 병합되지 못하도록 막는 방식을 사용한다.

이렇게 다른 브랜치에서 커밋을 하게 되면 main 브랜치에서 PR을 하겠냐는 버튼이 생긴다.

PR을 열면 GitHub Actions가 CI를 실행하여 테스트를 수행하고, GitHub는 main 브랜치와 병합 가능한 상태인지 충돌 여부를 확인한다.

Branch Protection은 PR CI가 통과해야 merge 버튼을 누를 수 있게 한다. merge가 완료되면 main 브랜치에 push 이벤트가 발생하고, Branch CI가 다시 실행되어 main의 최종 상태를 확인한다.
또한 매 CI마다 JaCoCo를 통해 테스트 커버리지 리포트를 생성하도록 설정하였다. 테스트 커버리지는 전체 코드 중 테스트 실행 과정에서 실제로 수행된 코드의 비율을 의미한다. 아래는 실제 CI 후에 테스트 커버리지를 나타낸 표이다.

이를 통해 개발자는 어떤 코드가 테스트에 의해 실행되었고, 어떤 영역이 아직 테스트되지 않았는지 확인할 수 있다. 다만 현재는 coverage threshold를 설정하지 않고 리포트만 생성하도록 했다. 즉 커버리지가 낮더라도 CI가 실패하지는 않는다. 추후 기능이 더 많아지고 테스트 기준을 강제할 필요가 생기면, coverage threshold를 설정하여 기준 미달 시 CI를 실패시키도록 변경할 수 있다. 이 경우 Branch Protection과 결합하여 테스트 커버리지 기준을 만족하지 못한 PR이 main에 병합되지 못하도록 막을 수 있다.

이로써 SolveGO의 코드 시간선을 Git으로 기록하고, CI를 통해 새로운 시간선이 main에 합류해도 되는지 자동으로 검사할 수 있게 되었다. 이제는 이 안정적인 main 시간선을 실제 서버 환경에 수동으로 배포해보고, 이후에는 main에 변경 사항이 병합될 때마다 테스트와 배포가 자동으로 이어지도록 CD, 즉 Continuous Deployment를 구축해보자.
'Project > SolveGO' 카테고리의 다른 글
| [SolveGO #18] 배포도 알아서 해주세요 - CD 적용 (0) | 2026.07.12 |
|---|---|
| [SolveGO #17] 땅을 다지는 기초공사 - Docker 패키징 & 수동 배포 (0) | 2026.07.10 |
| [SolveGO #15] 큰 힘에는 큰 책임이 따른다 - Controller Slice Test & Integration Test (1) | 2026.07.06 |
| [SolveGO #14] 롤 밴픽의 원리 - Service Unit Test & Integration Test (0) | 2026.07.05 |
| [SolveGO #13] 실력 상승의 정도(正道) - Repository Slice Test (0) | 2026.07.03 |