[์šฐ์‚ฌ๊ธฐ] Github Actions PR ํ…Œ์ŠคํŠธ์šฉ workflow ๋ฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ , ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ

2026. 1. 20. 23:21ยท์šฐ์‚ฌ๊ธฐ ๊ฐœ๋ฐœ์ผ์ง€ ๐Ÿฐ

 

 

ํ…Œ์ŠคํŠธ  workflow ์ž‘์„ฑ (feature → dev PR ์‹œ ์‹คํ–‰)

๊ธฐ์กด workflow๋Š” dev ๋ธŒ๋žœ์น˜์— push ํ•  ๋•Œ๋งˆ๋‹ค ํ…Œ์ŠคํŠธ์™€ ๋ฐฐํฌ๊ฐ€ ํ•จ๊ป˜ ๋™์ž‘์„ ํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ,

ํ…Œ์ŠคํŠธ๊ฐ€ ํฌํ•จ๋˜๋‹ค ๋ณด๋‹ˆ Github Actions ์‹คํ–‰ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๊ณ ,

BE๋Š” ํ˜ผ์ž ๊ฐœ๋ฐœํ•˜๋‹ค ๋ณด๋‹ˆ  PR ๊ณผ์ •์„ ์ƒ๋žตํ–ˆ์—ˆ๋Š”๋ฐ ์ด๋Ÿฌ๋ฉด์„œ ๊ฒ€์ฆ๋˜์ง€ ์•Š์€ ์ฝ”๋“œ๋“ค์ด dev์— merge๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์—ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฌธ์ œ์ ๋“ค์„ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ workflow๋ฅผ ํ…Œ์ŠคํŠธ์šฉ/๋ฐฐํฌ์šฉ ๋‘ ๊ฐ€์ง€๋กœ ๋ถ„๋ฆฌํ•˜๋Š” ์ž‘์—…์„ ์ง„ํ–‰ํ•ด๋ณด์•˜๋‹ค. ๐Ÿ”ฅ

 

name: Test on Pull Request

on:
  pull_request:
    branches:
      - dev
    types: [opened, synchronize, reopened]

 

feature ๋ธŒ๋žœ์น˜์—์„œ ๊ฐœ๋ฐœ ํ›„ dev ๋ธŒ๋žœ์น˜์— Pull Reqeuset๋ฅผ ์˜ฌ๋ฆฌ๋ฉด workflow๊ฐ€ ์‹คํ–‰๋œ๋‹ค.

type์„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋Š”๋ฐ opened์€ PR ์ตœ์ดˆ ์ƒ์„ฑ, synchronize๋Š” PR์— ์ƒˆ ์ปค๋ฐ‹์ด ์ถ”๊ฐ€๋  ๋•Œ, reopened๋Š” ๋‹ซํžŒ PR์„ ๋‹ค์‹œ ์—ด์—ˆ์„ ๋•Œ workfolw๊ฐ€ ์‹คํ–‰๋˜๊ฒŒ ์„ค์ •ํ–ˆ๋‹ค.

 

 

Testcontainers  ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•œ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ

DB์—ฐ๊ฒฐ์ด ํ•„์š”ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋“ค์€ H2 database๋ฅผ ์ด์šฉํ–ˆ์—ˆ๋Š”๋ฐ redis๋Š” ๋”ฐ๋กœ ํ…Œ์ŠคํŠธ ๊ตฌ์„ฑ์ด ๋˜์–ด์žˆ์ง€ ์•Š์•„์„œ Testcontainers ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•ด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ๋‹ค.

Docker๋ฅผ ํ†ตํ•ด ๊ฒฉ๋ฆฌ๋œ Redis์™€ PostgreSQL ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์ผํšŒ์šฉ์œผ๋กœ ๋„์šฐ๊ณ , ํ…Œ์ŠคํŠธ๊ฐ€ ๋๋‚˜๋ฉด ์ž๋™์œผ๋กœ ์‚ญ์ œ๋˜๋„๋ก ํ–ˆ๋‹ค.

 

 

build.gradle ์˜์กด์„ฑ ์ถ”๊ฐ€

    testImplementation("org.springframework.boot:spring-boot-testcontainers:4.0.1")

    testImplementation("com.redis.testcontainers:testcontainers-redis:1.6.4")
    testImplementation("org.testcontainers:junit-jupiter:1.21.3")
    testImplementation("org.testcontainers:postgresql:1.21.3")

 

 

application-test.yml ์ˆ˜์ •

spring:
    datasource:
    jpa:
        hibernate:
            ddl-auto: create-drop
        show-sql: true
    data:
        redis:

 

์ด ๋ถ€๋ถ„์„ ๋†“์ณ์„œ ๊ณ„์† table ์ƒ์„ฑ์ด ์•ˆ ๋ผ์„œ ๊ณ ์ƒํ–ˆ๋Š”๋ฐ ..

Testcontainers ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ด์šฉํ•˜๋ฉด ์•Œ์•„์„œ ์ปจํ…Œ์ด๋„ˆ ์ฃผ์†Œ๋ฅผ ์ฃผ์ž…ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์—, ๊ผญ! ๊ฐ’์„ ๋น„์›Œ์ค˜์•ผ ํ•œ๋‹ค.

 

 

 

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์ถ”์ƒํ™”

@ServiceConnection ์–ด๋…ธํ…Œ์ด์…˜์„ ์ด์šฉํ•ด์„œ Redis์™€ PostgreSQL ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์„ค์ •ํ•˜๋Š” ์ถ”์ƒ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค๊ณ , ํ•ด๋‹น ํด๋ž˜์Šค๋ฅผ ์ƒ์† ๋ฐ›์•„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ๋‹ค. ๊ธฐ์กด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋“ค์ด DescribeSpec์„ ์ƒ์† ๋ฐ›๊ณ  ์žˆ์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ์ƒ์„ฑ์ž์—์„œ body๋ฅผ ๋ฐ›์•„ DescribeSpec์—๊ฒŒ ๋„˜๊ฒจ์ฃผ๋Š” ๋กœ์ง๋„ ์ถ”๊ฐ€ํ–ˆ๋‹ค.

@SpringBootTest
abstract class IntegrationTestSupport(body: DescribeSpec.() -> Unit = {}) : DescribeSpec(body) {

    // Kotest์—์„œ Spring์„ ์“ฐ๋ ค๋ฉด ์ด ํ™•์žฅ์ด ํ•„์ˆ˜
    override fun extensions() = listOf(SpringExtension)

    companion object {
        // Redis
        @Container
        @ServiceConnection
        val redisContainer = GenericContainer(DockerImageName.parse("redis:alpine"))
            .withExposedPorts(6379)

        // PostgreSQL
        @Container
        @ServiceConnection
        val postgresContainer = PostgreSQLContainer<Nothing>(DockerImageName.parse("postgres:15-alpine")).apply {
            withDatabaseName("testdb")
            withUsername("test")
            withPassword("test")
        }
    }
}

 

ํ•˜์ง€๋งŒ ๊ณ„์† ๋˜๋Š” redis ์—ฐ๊ฒฐ ์‹คํŒจ ๋ฌธ์ œ ... ๐Ÿคฏ 

๊ณ„์†๋˜๋Š” ์‚ฝ์งˆ๊ณผ ๊ตฌ๊ธ€๋ง, ์˜จ๊ฐ– AI์™€ ์”จ๋ฆ„์„ ํ•˜๋ฉด์„œ ์ปจํ…Œ์ด๋„ˆ ์—ฐ๊ฒฐ ์„ฑ๊ณต์‹œํ‚จ ํ…Œ์ŠคํŠธ ์ถ”์ƒ ํด๋ž˜์Šค๋Š” ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

 

 

@SpringBootTest
abstract class IntegrationTestSupport(
    body: DescribeSpec.() -> Unit = {},
) : DescribeSpec(body) {
    override fun extensions(): List<Extension> = listOf(SpringExtension)

    companion object {
        val redisContainer =
            GenericContainer(DockerImageName.parse("redis:alpine"))
                .withExposedPorts(6379)
                .apply { start() }

        @ServiceConnection
        val postgresContainer =
            PostgreSQLContainer<Nothing>(DockerImageName.parse("postgres:15-alpine"))
                .apply {
                    withDatabaseName("testdb")
                    withUsername("test")
                    withPassword("test")
                }.apply { start() }

        @JvmStatic
        @DynamicPropertySource
        fun overrideProps(registry: DynamicPropertyRegistry) {
            registry.add("spring.data.redis.host") { redisContainer.host }
            registry.add("spring.data.redis.port") { redisContainer.getMappedPort(6379).toString() }
        }
    }
}

 

Redis ์ปจํ…Œ์ด๋„ˆ ์„ค์ •๋„ Redis ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์žˆ์–ด์„œ ์ ์šฉ์„ ํ•ด๋ดค๋Š”๋ฐ, redis config์—์„œ ๊ณ„์† host, port๋ฅผ ์ฐพ์ง€ ๋ชป ํ•˜๋Š” ์—๋Ÿฌ๊ฐ€ ์žˆ์–ด์„œ ์ผ๋‹จ GenericContainer๋กœ ๊ตฌํ˜„ํ–ˆ๋‹ค.

@ServiceConnection์€ ํƒ€์ž…์ด ๋ช…ํ™•ํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— redis ์ปจํ…Œ์ด๋„ˆ ์„ค์ •์—์„œ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜๋‹ค.

 

 

 

ํŠน์ • ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ œ์™ธ์‹œํ‚ค๊ธฐ

์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ํ…Œ์ŠคํŠธ๋‚˜ Google ํ† ํฐ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ๋Š” ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋งŒ ๋™์ž‘ํ•˜๊ฒŒ ํ•˜๊ณ  ์‹ถ์–ด์„œ @Tag("local-only") ์–ด๋…ธํ…Œ์ด์…˜ ๋‹ฌ๋ฆฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ๋™์ž‘ํ•˜์ง€ ์•Š๊ฒŒ ์„ค์ •์„ ํ•ด์คฌ๋‹ค. ํ•˜์ง€๋งŒ ํ•ด๋‹น workflow ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•ด๋ณด๋‹ˆ ์ œ์™ธ์‹œํ‚ค๊ณ  ์‹ถ์€ ์ฝ”๋“œ๋“ค์ด ๊ณ„์† ํ…Œ์ŠคํŠธ๊ฐ€ ๋˜๊ณ  ์žˆ์—ˆ๋‹ค. ๐Ÿคฏ

 

      - name: Run tests with Testcontainers
        run: ./gradlew test -PexcludeTags="local-only" --no-daemon
        env:
          SPRING_PROFILES_ACTIVE: test

 

 

workflow ์ž์ฒด์—๋Š” ๋ฌธ์ œ ์—†์—ˆ๊ณ  ๋˜ ๋‹ค์‹œ ์‹œ์ž‘๋œ ์‚ฝ์งˆ... 

๊ฒฐ๊ตญ GPT๊ฐ€ ํ•ด๋‹ต์„ ๋‚ด์คฌ๋Š”๋ฐ ๋‚ด๊ฐ€ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋˜ @Tag ์–ด๋…ธํ…Œ์ด์…˜์€ JUnit ํ…Œ์ŠคํŠธ์—๋งŒ ์ ์šฉ๋˜๊ณ ,

ํ˜„์žฌ ๋‚ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋“ค์€ Kotest ์—”์ง„์œผ๋กœ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— @Tags ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๊ณ  ํ•œ๋‹ค.

 

 

tasks.withType<Test> {
    useJUnitPlatform()

    systemProperty(
        "kotest.tags.exclude",
        project.findProperty("excludeTags") ?: "",
    )
}

 

build.gradle๋„ ์œ„์ฒ˜๋Ÿผ kotest ์ „์šฉ ํ•„ํ„ฐ๋ง ์„ค์ •์„ ํ•ด์คฌ๋”๋‹ˆ ์˜๋„ํ•œ๋Œ€๋กœ ์ง€์ •ํ•ด์ค€ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋“ค์ด ์ œ์™ธ๋๋‹ค.

ํ…Œ์ŠคํŠธ ์—”์ง„, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ, mock ๊ฐ์ฒด ๋‹ค๋ฃจ๋Š” ๋ฐฉ๋ฒ• ์—ด์‹ฌํžˆ ๊ณต๋ถ€ํ•ด์•ผ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค ..๐Ÿ˜‡๐Ÿ”ซ

 

 

 

์˜จ๊ฐ– ์‚ฝ์งˆ ๋์— ๋“œ๋””์–ด ํ…Œ์ŠคํŠธ ํ†ต๊ณผ์‹œ์ผฐ๋‹ค.

๋‹ค์Œ์€ dev ๋ฐฐํฌํ•˜๋Š” workflow ์ž‘์„ฑํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

 

 

์ €์ž‘์žํ‘œ์‹œ ๋น„์˜๋ฆฌ ๋ณ€๊ฒฝ๊ธˆ์ง€ (์ƒˆ์ฐฝ์—ด๋ฆผ)

'์šฐ์‚ฌ๊ธฐ ๊ฐœ๋ฐœ์ผ์ง€ ๐Ÿฐ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[์šฐ์‚ฌ๊ธฐ] AWS Lightsail ๋ฐฐํฌ ์‹œ, GitHub Actions๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฃผ์ž…ํ•˜๊ธฐ  (0) 2026.01.26
[์šฐ์‚ฌ๊ธฐ] Github Actions DEV ๋ฐฐํฌ์šฉ workflow ์ž‘์„ฑ  (1) 2026.01.21
[์šฐ์‚ฌ๊ธฐ] OAuth 2.0 Playground๋กœ ๊ตฌ๊ธ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธฐ  (0) 2026.01.15
'์šฐ์‚ฌ๊ธฐ ๊ฐœ๋ฐœ์ผ์ง€ ๐Ÿฐ' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€
  • [์šฐ์‚ฌ๊ธฐ] AWS Lightsail ๋ฐฐํฌ ์‹œ, GitHub Actions๋กœ ํ™˜๊ฒฝ๋ณ€์ˆ˜ ์ฃผ์ž…ํ•˜๊ธฐ
  • [์šฐ์‚ฌ๊ธฐ] Github Actions DEV ๋ฐฐํฌ์šฉ workflow ์ž‘์„ฑ
  • [์šฐ์‚ฌ๊ธฐ] OAuth 2.0 Playground๋กœ ๊ตฌ๊ธ€ ์†Œ์…œ ๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ ํ•ด๋ณด๊ธฐ
ํ•ด๋‹ˆ ๐ŸŒฑ
ํ•ด๋‹ˆ ๐ŸŒฑ
๊ธฐ๋ก์ด ์ž์‚ฐ์ด๋‹ค ( •ฬ€แด—•ฬ )ูˆโœ๏ธ
  • ํ•ด๋‹ˆ ๐ŸŒฑ
    haeni.dev
    ํ•ด๋‹ˆ ๐ŸŒฑ
  • ๋งํฌ

    • github
    • velog
  • ์ „์ฒด
    ์˜ค๋Š˜
    ์–ด์ œ
    • ๋ถ„๋ฅ˜ ์ „์ฒด๋ณด๊ธฐ (25)
      • ์šฐ์‚ฌ๊ธฐ ๊ฐœ๋ฐœ์ผ์ง€ ๐Ÿฐ (4)
      • Today I Learned ๐Ÿง (19)
      • ๋ถ„๋…ธ์˜ ํƒ€์ดํ•‘ ๋กœ๊ทธ ๐Ÿ”ฅ (2)
  • ๋ธ”๋กœ๊ทธ ๋ฉ”๋‰ด

    • ํ™ˆ
    • ํƒœ๊ทธ
    • ๋ฐฉ๋ช…๋ก
  • ๊ณต์ง€์‚ฌํ•ญ

  • ์ธ๊ธฐ ๊ธ€

  • ํƒœ๊ทธ

    ๊ฐœ๋ฐœ
    springboot
    ์ฝ”๋”ฉํ…Œ์ŠคํŠธ
    IT
    ๋ฐฑ์—”๋“œ
    AWS
    ๊ฐœ๋ฐœ์ž
    til
    ci/cd
    ์ฝ”ํ…Œ
  • ์ตœ๊ทผ ๋Œ“๊ธ€

  • ์ตœ๊ทผ ๊ธ€

  • hELLOยท Designed By์ •์ƒ์šฐ.v4.10.5
ํ•ด๋‹ˆ ๐ŸŒฑ
[์šฐ์‚ฌ๊ธฐ] Github Actions PR ํ…Œ์ŠคํŠธ์šฉ workflow ๋ฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ , ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ
์ƒ๋‹จ์œผ๋กœ

ํ‹ฐ์Šคํ† ๋ฆฌํˆด๋ฐ”