땃쥐네

[토이프로젝트] 게시판 시스템(board-system) 3. 프로젝트 생성 및 지속적 통합 본문

Project/Board-System

[토이프로젝트] 게시판 시스템(board-system) 3. 프로젝트 생성 및 지속적 통합

ttasjwi 2024. 9. 27. 17:22

 

 

이전 글에서 AWS 전반적인 인프라 환경을 구성했습니다.

 

이번에는 프로젝트를 생성하고,

풀리퀘스트에 커밋이 올라올 때마다 빌드테스트가 자동화되도록 하겠습니다.


1. 스프링부트 프로젝트 생성, Git - GitHub - Jira 연동

 

프로젝트를 생성해보겠습니다. 일단은 간단하게 프로젝트를 생성하고, 구조를 바꿀 일이 있다면 이후 글에서 바꾸도록 하겠습니다. 이번 글에서는 배포 자동화에 초점을 맞추고 있기 때문에 복잡한 설정은 하지 않을겁니다.

 

1.1 스프링부트 프로젝트 생성

 

  • Intellij Ultimate 를 쓴다면 Spring Boot Initializr 지원 기능이 있으므로 그걸 써도 됩니다.근데 그렇지 않은 분들이 더 많은 것 같고 웹의 Spring Initializr 로 프로젝트를 생성했습니다.
  • 저는 자바보다 코틀린을 좋아하고, 코틀린에 익숙해지고싶어서 Kotlin 을 사용할 것입니다.
  • JVM은 Java 21 버전을 기준으로 할 것이고, Spring Boot 3.3.4 기준으로 하며, 의존성은 Spring Boot Web 정도만 추가하여 시작할 거에요.

1.2 프로젝트 설정

1.2.1 build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.9.25"
    id("org.jetbrains.kotlin.plugin.spring") version "1.9.25"
    id("org.springframework.boot") version "3.3.4"
    id("io.spring.dependency-management") version "1.1.6"
}

group = "com.ttasjwi"
version = "0.0.1"

java {
    sourceCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs += "-Xjsr305=strict"
        jvmTarget = "21"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

1.2.2 settings.kts

 

rootProject.name = "board-system"

 

 

1.2.3 application.yml

spring.application.name: board-system

 

1.2.4 Main.kt

package com.ttasjwi.board.system

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class Main

fun main(args: Array<String>) {
    runApplication<Main>(*args)
}

1.3 프로젝트 실행

실행해보니 애플리케이션이 잘 구동되는걸 확인할 수 있고,

루트 경로로 접속해보면 Whitelabel Error Page 가 뜨는 것을 보아 스프링부트 웹 의존성이 잘 등록된 것 같아요.

1.4 Github 프로젝트 관리

 

이 상태로 프로젝트는 git 및 GitHub 리포지토리를 통해 유지/보수 할 것입니다.

 

 

1.5 Jira 연결

Jira 상단의 앱 > 더 많은 앱 살펴보기

 

 

Github for Jira 를 찾아서,

 

 

Get app 버튼 > Get it now 버튼 > Get Started 버튼

 

 

 

Jira와 Github 연결 작업을 수행합니다.

자신의 계정 또는 Organization 을 연결하고 필요한 리포지토리만 연결합니다.

 

이렇게 하면 로컬 프로젝트는 Git 에 의해 버전 관리되고, GitHub 에 push 하여 관리할 수 있으면서, Github 은 Jira 와 통합될 수 있습니다.

 


2. Github Actions와 지속적 통합(빌드 테스트)

 

이제 향후 리포지토리 보호를 위한 지속적 통합 설정입니다.

 

모든 커밋은 별도의 브랜치를 통해 작성되어야하고, Master 브랜치에 Pull Request 를 거쳐서 통합되도록 할거에요.

매 커밋마다 빌드 테스트가 진행되도록 하고, 만약 이 테스트가 실패하게 되면 병합되지 못 하게 할 겁니다.

 

커밋의 편의상 브랜치 내의 특정 지점에서 빌드가 실패될 수 있지만,

Merge 시점에서는 빌드가 실패해선 안 되도록 할거에요.

 

2.1 브랜치 생성

 

 

제가 서브 태스크를 BRD-61 이름의 티켓으로 생성했는데요. 이 티켓에 맞춰서 BRD-61_xxx 이름으로 브랜치를 생성합니다.

 

Jira와 잘 통합되어 있다면 이후 우리가 Github에 BRD-61 와 같이, Jira 티켓명이 포함된 브랜치 및 브랜치에 해당하는 커밋을 푸시하게 되면 Jira 가 추적할 수 있습니다.

 

2.2 CI 스크립트 작성

name: Build-Test

on:
  pull_request:
    branches:
      - master

jobs:
  Build-Test:
    runs-on: ubuntu-latest

    steps:
      - name: Check Out (체크 아웃)
        uses: actions/checkout@v4

      - name: Setup Java Version (Java 21 설정)
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '21'

      - name: Use Gradle Cache (Gradle 캐싱)
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew (gradlew에 권한 부여)
        run: chmod +x gradlew
        shell: bash

      - name: Test (테스트)
        run: ./gradlew build

 

프로젝트 루트 경로에 .github/workflows 폴더를 추가하고 위와 같은 yml 파일을 작성합니다.

master 브랜치의 풀리퀘스트에 커밋이 올라갈 때마다 이 스크립트가 실행됩니다.

 

스크립트에 대해 간단히 설명하면

 

on.pull_request.branches: master

master 브랜치의 풀리퀘스트에 커밋이 올라갈 때마다 이 스크립트가 실행됩니다.

 

runs-on

Github Actions에서 제공하는 ubuntu-latest 환경에서 실행되도록 했습니다.(이 환경 정보는 GitHub Actions Runner Images 를 참고해주세요.)

 

빌드 절차

ubuntu-latest 환경에서는 java 11 이 기본 설정이라서, Java 21 설정을 별도로 해주고

gradle 캐싱 및 gradlew 파일 권한 부여 설정을 진행한 뒤

build 명령어를 통해 빌드를 실제 실행하도록 했습니다.

2.3 커밋 및 푸시



변경 사항을 커밋합니다.

저는 커밋 템블릿을

 

종류: (티켓명) 커밋 제목 형태로 구성하기로 하였습니다.

이렇게 티켓명을 커밋에 포함시키면 JIRA 가 해당 커밋을 알 수 있습니다.

 

2.4 풀 리퀘스트

 

PR 에 [티켓명] 형태로 남기게 되면 이후 해당 영역이 링크로 바뀌게 되는데요.

 

이렇게 해당 티켓 페이지에 접근할 수 있습니다.

 

 

작성한 스크립트에 따라서 CI 가 잘 작동하는 것도 볼 수 있어요.

 

 

2.5 컴파일이 깨지거나 테스트가 실패하면?

 

이번에는 Main 클래스를 살짝 건들여서 컴파일 에러가 발생하도록 해볼게요.

이대로 기존 풀리퀘쪽에 푸시하면 어떤 일이 일어날까요?

 

혹은 테스트 코드가 실패하는 것도 문제가 되겠죠?

 

 

Github Actions 의 CI 스크립트에 작성된, 빌드 테스트가 실행되고 실패합니다.

하지만 그럼에도 이 브랜치는 Merge 가능해요.

 

이게 저희같은 토이프로젝트면 다행이지 프로덕션이였다면, 바로 Merge 되면 위험하겠죠?

 

2.6  Rule 추가

 

리포지토리 상단의 Settings 로 들어가서 Rules > Rulesets 로 들어갑니다

 

 

branch ruleset 을 생성하겠습니다.

 

 

- Ruleset Name : Ruleset 제목

- Enforcement status : Active 를 해야 활성화됩니다.

- Bypass list : 이 규칙을 열외시킬 역할, 팀, 앱 ( 저는 설정 안 했어요 )

 

 

대상이 되는 브랜치를 설정하는 부분입니다.

저는 Include by pattern 를 누르고 master 를 입력했는데, 이 방법 말고 default 브랜치로 설정해도 돼요.

 

 

 

Restrict Creations

- bypass 사용자들에 한해서만 이 브랜치로부터의 파생 브랜치 생성을 제한하는 것입니다.

- 조직화된 팀이라면 모르겠지만 이 프로젝트는 저의 개인 프로젝트라서 이 부분에 대해 제한을 걸진 않았어요.

 

Restrict updates

- bypass 사용자들에 한해서만 이 브랜치의 업데이트를 허용하는 것입니다.

- 브랜치가 가리키는 커밋이 달라지거나, 새로운 커밋을 덧붙이거나 하는 것에 제약을 두는 것이죠.

- 조직화된 팀이라면 모르겠지만 이 프로젝트는 저의 개인 프로젝트라서 이 부분에 대해 제한을 걸진 않았어요.

 

Restrict deletions

- bypass 사용자를 제외하고 브랜치 삭제를 금지하는 것입니다.

- 이건 기본값으로 활성화되어 있는데 master 브랜치를 삭제할 일은 향후에 없을거라서 활성화했습니다.

 

이런걸 막음

Require linear history

- 브랜치 흐름을 선형으로 유지하는 옵션입니다.

- 예를들어 마스터가 a-b-c 이렇게 되어 있는데 병합하려는 브랜치가 a-d-e 이렇게 되어 있다면 c와 e가 병합될 때는 일직선 선형으로 되지 않겠죠? 이런 것을 막습니다. (이런 병합을 3 way merge 라고 합니다.)

- 이 옵션이 활성화되면 여러 PR 을 위한 브랜치들이 있을 때 최신의 커밋을 기준으로 코드의 rebase 를 강제합니다. 

- 저는 3 way merge 큰 흐름의 커밋 이력을 읽기 힘들게 만든다고 생각하기 때문에 선호하지 않습니다. 그래서 이 옵션을 활성화할거에요. 작업 브랜치는 별도로 남겨서 확인하는게 낫다고 생각해요.

 

Require deployments to succeed

- push 하기 전에 배포에 성공해야만 함을 강제하는 옵션인데 현재 배포는 제가 따로 설정하지 않았기 때문에 이 부분은 아직 설정하지 않을거에요.

 

Requires signed commits

- GPG 와 같은 서명을 함께하여 커밋된 것들에 대해서만 허용하겠다는 옵션이에요.

- 이 부분은 참고 링크를 드리겠습니다.

 

 

Require a pull request before merging

- merge 전에는 반드시 pull request를 해야한다는 규약입니다.

- 저는 혼자 프로젝트를 진행함에도 이를 강제하겠습니다. (생각을 거치지 않거나 검증이 충분히 되지 않은 상태에서 master 브랜치에 바로 푸시하는 것은 위험함)

 

여기에는 추가 설정들이 더 있는데 잠시 읽어보겠습니다.

 

Requires approvals

동의를 구해야한다는 것입니다. 근데 저는 혼자 하는 프로젝트이므로 0으로 설정

 

Dismiss stale pull request approvals when new commits are pushed

풀 리퀘스트에 새로운 커밋이 푸시되면 기존의 승인은 자동으로 무효화되는 옵션입니다. 리뷰어는 변경된 내용을 다시 검토하고 PR을 재승인해야 합니다.

 

Require review from Code Owners

PR에서 특정 파일이나 디렉토리의 수정이 있을 때, 해당 파일의 소유자(또는 지정된 팀/사용자)가 PR을 승인해야 머지가 가능하도록 하는 기능입니다.

 

Require approval of the most recent reviewable push

가장 최근에 푸시된 커밋에 대해 반드시 승인이 필요함을 강제합니다.

PR이 승인된 후, 해당 PR 브랜치에 새로운 커밋이 푸시되면, 이전에 받은 승인은 무효화됩니다.

PR에 새로운 커밋이 추가되면, 그 커밋에 대해 다시 승인을 받아야만 PR을 머지할 수 있습니다.

 

Require conversation resolution before merging

PR을 머지하기 전에 리뷰어나 PR 작성자가 모든 코멘트를 해결하고 "Resolve conversation" 버튼을 눌러 해당 대화를 해결해야만 합니다.

 

 

Require status checks to pass

- 지정된 상태검사(status checks) 를 정의하고, 최근의 커밋이 status checks 의 기준에 부합되지 않으면 머지되지 못 하도록 합니다.

- status checks 는 여러가지 방법이 있는데, 그 중 가장 대표적인 것이 github actions 와 같은 CI/CD 도구들을 실행하는 것입니다.

- 즉 우리가 올린 github actions 스크립트를 실행했는데 실패했다면 merge 를 못 하게 합니다.

 

Require branches to be up to date before merging

병합 대상의 브랜치가 최신의 상태가 아니면 merge 를 못 하게 강제합니다.

 

 

Do not require status checks on creation

 

PR 생성 시점의 status checks 를 하지 않는다는 옵션입니다.

예를 들어 단일 커밋으로만 PR 이 구성되어 있고, 혹시 나중에 push 시점 실행되는 빌드-배포  스크립트가 있다면 굳이 그럴 때는 상태검사를 할 필요가 없을텐데 그런걸 막을 수 있을 것 같긴 합니다.

 

근데 지금 상태에서는 push 시점 실행되는 스크립트가 없고 브랜치 방어에 취약하므로 이 옵션은 활성화시키지 않을거에요. 이후 push 스크립트가 추가되면 이 옵션을 활성화시켜도 될 것 같습니다.

 

 

 

status checks 추가 방법

저기서 Add checks 버튼을 누르고, github actions 폴더에 넣어둔 파일에서 name에 해당하는 부분있죠? 그걸 수동으로 입력해주면 추가 가능합니다.(타이핑하면 그제서야 인식해서 완성은 되는데 처음에는 뜨지 않아서 헷갈렸어요)

 

 

force 푸시 막기

- 대상 브랜치로 강제 푸시(push force)를 막아버립니다

- 가끔 가다가 강제 푸시를 해야하는 경우가 있긴한데 그런 경우는 드물기도 하고 평소에는 막아뒀다가 팀원 내 합의가 있으면 잠시 풀고 강제 푸시를 하는 것이 보안상 좋습니다.

 

Require code scanning results

코드 스캐닝 도구( CodeQL, Sonarqube 등...)를 지정할 수 있게 되며, 실행되어 그 결과가 나오기 전까지 PR을 머지하지 못하도록 막는 설정입니다.

여기서 나온 결과가 우리가 지정한 레벨 이상이면 통과되지 못 하게 됩니다. 

 

검색을 해서 조사해보면 Sonarqube 같은 도구들은 github actions 스크립트를 별도로 작성해서 활성화시키면 된다고 하는데, 당장은 학습곡선이 있기도 해서 사용하지 않겠습니다.

 

 

 

여기까지 하면 빌드테스트가 깨진 것 때문에 merge 가 허용되지 않습니다.

 

 

다시 컴파일이 가능해지도록 코드를 변경하고 푸시를 해보겠습니다.

 

 

이제 상태검사가 통과가능해지고, merge 또한 가능해집니다.

저는 merge commmit 을 통해 새로운 커밋을 만드는 것(three way merge)을 비활성화했기 때문에

squash and merge 와 Rebase and merge 만 가능해집니다.

 

 

저는 중간단계 커밋까지 커밋으로 남기는 것은 불필요하다고 생각하여, Squash and merge 를 하겠습니다.

이 경우 현재 PR의 번호와 함께 (#xxx) 커밋 제목이 자동으로 생성되는데 가능한 이대로 하시는 것을 추천드립니다.

 

 

Github 에서 나중에 커밋 제목을 보면 # 부분에 링크가 걸려서 해당 PR로 이동할 수 있습니다.

그래서 (#숫자) 부분은 남기는 것을 추천드립니다.

나중에 해당 PR 의 자세한 내역을 확인을 살피는데 도움이 되기 때문입니다.'

 

 

참고로 이렇게 Squash and merge 를 거치게 되면 이렇게 병합된 커밋이 하나로 생성되고 방금까지 작업했던 브랜치는 별도로 남아있게 되는데요. 이후 작업물을 만드시려면 master 브랜치를 origin/master 에 merge(fast forward merge) 시키고 그 위에서 진행하셔야합니다.

 

혹시 이전의 master 에서 파생되어서 작업하고 있었다? 그러더라도, origin/master 에 rebase 해서 작업해야합니다. 아까 RuleSet에 선형으로 커밋 이력을 남기는 것을 강제했기 때문입니다.

 

이렇게 하면 CI 까지는 얼추 마무리 된것 같아요. 이후 필요에 따라 아까 말씀드린 코드분석도구 (sonarqube 등...) 을 추가하거나, ruleset 을 변경한다거나 하는 것들을 하면 좋을 것 같긴합니다.


 

+ 작업물에 해당하는 프로젝트는 GitHub Repository 에 공개해뒀습니다.

https://github.com/ttasjwi/board-system

 

GitHub - ttasjwi/board-system

Contribute to ttasjwi/board-system development by creating an account on GitHub.

github.com

 

Comments