๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

ํ”„๋กœ์ ํŠธ/Airbnb Clone

Github Actions ์œผ๋กœ CI ํ…Œ์ŠคํŠธ ์ž๋™ํ™” ์ค‘ ๊ฒช์€ ์ด์Šˆ ์ •๋ฆฌ (Embedded Redis, Profile, Spring Rest Docs)

210๊ฐœ์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์™€ 99% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€

์ˆ™์†Œ ์˜ˆ์•ฝ ํ”Œ๋žซํผ์„ ๊ฐœ๋ฐœํ•˜๋ฉฐ ์ด 210์—ฌ ๊ฐœ์˜ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ณ , 99% ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€๋ฅผ ๋‹ฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ฝ”๋“œ์— ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ์„ ๋•Œ๋งˆ๋‹ค ์ „์ฒด ํ…Œ์ŠคํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ ์‹คํ–‰ํ•ด์•ผ ํ–ˆ๊ณ , ์ด๋Š” ์ ์  ๋ถˆํŽธํ•œ ์ž‘์—…์ด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ CI/CD ํˆด์ธ GitHub Actions๋ฅผ ๋„์ž…ํ•˜์—ฌ push ๋˜๋Š” pull_request๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ ์ž๋™์œผ๋กœ ํ…Œ์ŠคํŠธ๊ฐ€ ์‹คํ–‰๋˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

GitHub Actions๋ฅผ ์„ ํƒํ•œ ์ด์œ 

GitHub Actions๋ฅผ ์„ ํƒํ•œ ์ด์œ ๋Š” ๋‹จ์ˆœํ•ฉ๋‹ˆ๋‹ค.

  • Jenkins์ฒ˜๋Ÿผ ๋ณ„๋„์˜ ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•  ํ•„์š”๊ฐ€ ์—†์œผ๋ฉฐ,
  • ๋‹จ์ˆœํžˆ .github/workflows ๋””๋ ‰ํ† ๋ฆฌ์— YAML ํŒŒ์ผ๋งŒ ์ž‘์„ฑํ•˜๋ฉด ๋ฐ”๋กœ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.
  • ๋˜ํ•œ Groovy์™€ ๊ฐ™์€ ์ƒˆ๋กœ์šด ๋ฌธ๋ฒ•์„ ๋ฐฐ์šธ ํ•„์š” ์—†์ด ๋น ๋ฅด๊ฒŒ ์š”๊ตฌ ์‚ฌํ•ญ์„ ์ถฉ์กฑํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ฆ‰, ๊ฐ„๋‹จํžˆ ์„ค์ •ํ•˜๊ณ  ๋ฐ”๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

 

GitHub Actions์—์„œ ์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํŒจ

๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธ๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋•Œ๋Š” ๋ชจ๋“  ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ, GitHub Actions์—์„œ๋Š” ์ „์ฒด ํ…Œ์ŠคํŠธ ์ค‘ 70๊ฐœ๋‚˜ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

./gradlew build ์‹œ Github Actions ์—์„œ ๋ณด์—ฌ์ง€๋Š” ๊ธฐ์กด log

๊ธฐ์กด์— ./gradlew build๋ฅผ ์‹คํ–‰ํ–ˆ์„ ๋•Œ GitHub Actions์—์„œ ์ถœ๋ ฅ๋˜๋Š” ๋กœ๊ทธ๋Š” ํ•œ ์ค„์”ฉ ๊ฐ„๋‹จํžˆ ํ‘œ์‹œ๋˜์–ด ๋ฌธ์ œ์˜ ์›์ธ์„ ํŒŒ์•…ํ•˜๊ธฐ์— ๋ถ€์กฑํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ./gradlew test --stacktrace ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•ด ๋” ๋งŽ์€ ๋กœ๊ทธ๋ฅผ ์ถœ๋ ฅํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ๋ถ„์„ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๋ฌธ์ œ 1. Embedded Redis ์ด์Šˆ

์›์ธ ๋ถ„์„

./gradlew test --stacktrace๋กœ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•œ ๊ฒฐ๊ณผ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.

Caused by: org.springframework.boot.context.config.ConfigDataResourceNotFoundException at ConfigDataResourceNotFoundException.

 

ํ…Œ์ŠคํŠธ์šฉ DB๋กœ๋Š” ๋‚ด์žฅ H2์™€ Embedded Redis๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋Š”๋ฐ, ์ผ๋ถ€ ํ…Œ์ŠคํŠธ๋งŒ ์‹คํŒจํ–ˆ์œผ๋ฉฐ, ๊ทธ์ค‘ Redis ๊ด€๋ จ ํ…Œ์ŠคํŠธ๋งŒ ์‹คํŒจํ•œ ๊ฒƒ์œผ๋กœ ๋ณด์•„ ๋ฌธ์ œ๋Š” H2๊ฐ€ ์•„๋‹Œ Redis์— ์žˆ๋Š” ๊ฒƒ์œผ๋กœ ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

Embedded Redis๋ฅผ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๋„๋ก ์„ค์ •ํ–ˆ์ง€๋งŒ, ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์ง€ ์•Š์€ ๊ฒƒ์œผ๋กœ ๋ณด์˜€์Šต๋‹ˆ๋‹ค. ์ด์— Embedded Redis ์„ค์ •์„ ๋‹ค์‹œ ์ ๊ฒ€ํ•˜๊ณ  ์ˆ˜์ •ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๋ฐœ์ƒ ์›์ธ

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉํ•˜๋˜ application-test.yml ํŒŒ์ผ์— ์ž˜๋ชป๋œ ์„ค์ •์ด ํฌํ•จ๋˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค:

  data:
    redis:
      port: 6379
      host: localhost

 

์ด ์„ค์ •์œผ๋กœ ์ธํ•ด, build.gradle ์— Embedded Redis ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„ฃ์—ˆ์Œ์—๋„, Embedded Redis๊ฐ€ ์•„๋‹Œ ๋กœ์ปฌ ์ปดํ“จํ„ฐ์˜ ๋„์ปค๋กœ ๋„์›Œ์ง„ ๊ฐœ๋ฐœ์šฉ Redis๋ฅผ ์ฐธ์กฐํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ์—์„œ๋Š” Docker Redis๊ฐ€ ์‹คํ–‰ ์ค‘์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์— ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ–ˆ์ง€๋งŒ, GitHub Actions์—์„œ๋Š” Redis๊ฐ€ ์—†์–ด์„œ ์‹คํŒจํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. application-test.yml ์ˆ˜์ •

  • ํ…Œ์ŠคํŠธ์—์„œ Embedded Redis๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก test.yml ์—์„œ Redis ์„ค์ •์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.

2. Embedded Redis ์„ค์ • ์ถ”๊ฐ€

  • Embedded redis ์„ค์ • ๊ด€๋ จ ์œ ๋ช…ํ•œ ๊ธ€์ธ https://jojoldu.tistory.com/297 ์ฐธ๊ณ ํ•ด์„œ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ฝ”๋“œ์— ๋Œ€ํ•œ ์„ค๋ช…์€ ํ–ฅ๋กœ๋‹˜์˜ ๊ธ€์„ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”!
@Slf4j
@TestConfiguration
public class EmbeddedRedisConfig {

    private final int port = 6379;

    private RedisServer redisServer;

    @PostConstruct
    public void postConstruct() throws IOException {
        int port = isRedisRunning() ? findAvaliablePort() : this.port;
        this.redisServer = new RedisServer(port);
        redisServer.start();
    }

    @PreDestroy
    public void preDestroy() throws IOException {
        if (redisServer != null) {
            redisServer.stop();
        }
    }

    private int findAvaliablePort() throws IOException{
        for (int port = 10000; port <= 65535; port++) {
            Process process = executeGrepProcessCommand(port);
            if (!isRunning(process)) {
                return port;
            }
        }
        throw new IllegalArgumentException("Not Found Available port: 10000 ~ 65535");
    }

    private boolean isRedisRunning() throws IOException{
        return isRunning(executeGrepProcessCommand(this.port));
    }

    private boolean isRunning(Process process) {
        String line;
        StringBuilder pidInfo = new StringBuilder();

        try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            while ((line = input.readLine()) != null) {
                pidInfo.append(line);
            }
        } catch (Exception e) {}

        return StringUtils.hasLength(pidInfo.toString());
    }

	// mac, linux ์ „์šฉ
    private Process executeGrepProcessCommand(int port) throws IOException {
        String command = String.format("netstat -nat | grep LISTEN|grep %d", port);
        String[] shell = {"/bin/sh", "-c", command};
        return Runtime.getRuntime().exec(shell);
    }

	// window ์ „์šฉ
    private Process executeGrepProcessCommand(int port) throws IOException {
        String command = String.format("netstat -nao | find \"LISTEN\" | find \"%d\"", port);
        String[] shell = {"cmd.exe", "/y", "/c", command};
        return Runtime.getRuntime().exec(shell);
    }
}

 

ํ–ฅ๋กœ๋‹˜์˜ ์›๋ณธ ๊ธ€๊ณผ ๋‹ฌ๋ฆฌ ์ €๋Š” @TestConfiguration ์–ด๋…ธํ…Œ์ด์…˜์„ ๋ถ™์˜€์Šต๋‹ˆ๋‹ค.

@TestConfiguration ์€ ํ…Œ์ŠคํŠธ์—๋งŒ ์ ์šฉ๋˜๋Š” ์„ค์ • ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, ์ด @TestConfiguration ์€ @Configuration ๊ณผ ๋‹ค๋ฅด๊ฒŒ ์ปดํฌ๋„ŒํŠธ ์Šค์บ”์˜ ๋Œ€์ƒ์ด ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— ์ž„๋ฒ ๋””๋“œ ๋ ˆ๋””์Šค ์„ค์ •์„ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์—์„œ @Import ๋ฅผ ํ•ด์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

* ์ฐธ๊ณ 

- stackoverflow: Spring Boot: @TestConfiguration Not Overriding Bean During Integration Test

- why prefering @TestConfiguration than @Configuration in src/test

 

์ถ”๊ฐ€๋กœ, @Configuration์€ ํ…Œ์ŠคํŠธ ์‹œ์—๋„ ์ปดํฌ๋„ŒํŠธ ์Šค์บ”์˜ ๋Œ€์ƒ์ด ๋˜์–ด ์ž๋™์œผ๋กœ ๋นˆ์— ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ Redis ์„ค์ • ํด๋ž˜์Šค์—๋Š” @Configuration์„ ๋ถ™์ด๋Š”๋ฐ, ์ด๋Š” ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋„ ๋นˆ์œผ๋กœ ๋“ฑ๋ก๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ํ…Œ์ŠคํŠธ์šฉ ๋‚ด์žฅ Redis๋ฅผ ์„ค์ •ํ•˜๋Š” @TestConfiguration์˜ ๋นˆ๊ณผ ์‹ค์ œ Redis ์„ค์ • ํด๋ž˜์Šค์˜ ๋นˆ ์ด๋ฆ„์ด ์ค‘๋ณต๋œ๋‹ค๋ฉด ๋นˆ์˜ ์ด๋ฆ„ ์ค‘๋ณต ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ๊ฐ„๋‹จํžˆ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด, ์‹ค์ œ Redis ์„ค์ • ํด๋ž˜์Šค์— @Profile("!test")๋ฅผ ์ถ”๊ฐ€ํ•ด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•ด๋‹น ์„ค์ •์ด ์ ์šฉ๋˜์ง€ ์•Š๋„๋ก ์„ค์ •ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค

 

๋ฌธ์ œ 2. ์™ธ๋ถ€ ์„ค์ • ์ด์Šˆ (@Configuration ์— ๋Œ€ํ•œ ์˜คํ•ด)

ํ•ด๋‹น ์˜ค๋ฅ˜๋Š” ์™ธ๋ถ€ ์„ค์ •์ด ์ž˜๋ชป๋˜์–ด ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. (์˜ˆ: application.yml ์„ค์ • ์˜ค๋ฅ˜)

Caused by: java.lang.IllegalArgumentException at PropertyPlaceholderHelper.java:180

 

์ €๋Š” ๊ฒฐ์ œ API์™€ ์ด๋ฉ”์ผ API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ, ์™ธ๋ถ€์— ๋…ธ์ถœ๋˜์–ด์„œ๋Š” ์•ˆ ๋˜๋Š” ๊ฐ’๋“ค์„ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด application-secret.yml ํŒŒ์ผ์„ ๋ณ„๋„๋กœ ์ƒ์„ฑํ•ด ํ…Œ์ŠคํŠธ์— ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๊ฐ’๋“ค์„ ์ฐธ์กฐํ•˜๋Š” ๊ณณ์€ @Configuration ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ์„ค์ • ํด๋ž˜์Šค์˜€์œผ๋ฉฐ, ํ•ด๋‹น ํด๋ž˜์Šค๋Š” ํ…Œ์ŠคํŠธ ์‹œ์—๋„ ๋นˆ์œผ๋กœ ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ, ํ…Œ์ŠคํŠธ๋ฅผ ํ•จ์—๋„ @Autowired ๋กœ ์ฃผ์ž…๋ฐ›์€ ๋นˆ๋“ค์€ @Configuration ์— ์˜ํ•ด secret์™ธ๋ถ€ ์„ค์ • ํŒŒ์ผ์˜ ๊ฐ’์„ ์ฝ๊ฒŒ ๋˜๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ์˜ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ์ง€๋งŒ, Github Actions์˜ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•„์š”ํ•œ ์™ธ๋ถ€ ์„ค์ • ๊ฐ’์ด ๋ˆ„๋ฝ๋˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด GitHub Actions์˜ secret ๋ณ€์ˆ˜๋ฅผ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. application-secret.yml ํŒŒ์ผ์˜ ๋‚ด์šฉ์„ secret ๋ณ€์ˆ˜๋กœ ๋“ฑ๋กํ•œ ๋’ค, ./gradlew test๋ฅผ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— GitHub Actions์˜ ์‹คํ–‰ ํ™˜๊ฒฝ(Virtual Machine)์— ํ•ด๋‹น ํŒŒ์ผ์„ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•˜๋„๋ก ์„ค์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

      - name: Gradle ํ…Œ์ŠคํŠธ ์ˆ˜ํ–‰
        run: |
          echo "${{ secrets.APPLICATION_SECRET_PROPERTIES }}" > ./src/main/resources/application-secret.yml
          ./gradlew test --stacktrace

 

 

๋ฌธ์ œ 3. Spring Rest Docs ์ด์Šˆ

Gradle์—์„œ ํŠน์ • ํƒœ์Šคํฌ(Task)๊ฐ€ ์‹คํ–‰๋˜์ง€ ๋ชปํ•˜๊ณ  ์‹คํŒจํ•˜๋Š” ๊ฒฝ์šฐ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ์ž…๋‹ˆ๋‹ค. ์ œ ๊ฒฝ์šฐ, ์‹คํŒจํ•œ ํƒœ์Šคํฌ๋Š” :test๋กœ, ํ…Œ์ŠคํŠธ ์‹คํ–‰๊ณผ ๊ด€๋ จ๋œ ์ž‘์—…์ด์—ˆ์Šต๋‹ˆ๋‹ค.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':test'.

 

์ €๋Š” API ๋ฌธ์„œ๋ฅผ ์œ„ํ•ด Spring Rest Docs๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. build.gradle ํŒŒ์ผ์—๋Š” Gradle ํ…Œ์ŠคํŠธ๊ฐ€ ์„ฑ๊ณตํ•˜๋ฉด ์Šค๋‹ˆํŽซ(snippet)์„ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ํ™œ์šฉํ•ด API ๋ฌธ์„œ์ธ .html ํŒŒ์ผ์„ ํŠน์ • ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๋ณต์‚ฌํ•˜๋Š” ์ž‘์—…์„ ์ถ”๊ฐ€ํ•ด๋‘์—ˆ์Šต๋‹ˆ๋‹ค.

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
	asciidoctorExt
}

ext {
	snippetsDir = file('build/generated-snippets') // ํ…Œ์ŠคํŠธ ์„ฑ๊ณต ํ›„, ์ƒ์„ฑ๋œ ์Šค๋‹ˆํŽซ ์ €์žฅ ๊ฒฝ๋กœ

test {
	useJUnitPlatform()
	outputs.dir snippetsDir // REST Docs ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ๋ฌผ์„ snippetsDir์— ์˜ํ•ด ์ง€์ •๋œ ๊ฒฝ๋กœ์— ์ €์žฅ
}

asciidoctor {
	dependsOn test
	inputs.dir snippetsDir   // REST Docs์—์„œ ์ƒ์„ฑํ•œ ์Šค๋‹ˆํŽซ(build/generated-snippets)์„ ์ž…๋ ฅ์œผ๋กœ ์‚ฌ์šฉ
	configurations 'asciidoctorExt'
	sources { 
		include("**/index.adoc") // index.adoc ํŒŒ์ผ๋งŒ ๋ฌธ์„œํ™” ๋Œ€์ƒ์— ํฌํ•จ
	}
	baseDirFollowsSourceFile() // ๋‹ค๋ฅธ adoc ํŒŒ์ผ์„ include ํ•  ๋•Œ ๊ฒฝ๋กœ๋ฅผ baseDir๋กœ ๋งž์ถ˜๋‹ค
}

task createDocument(type: Copy) {
	dependsOn asciidoctor
	from file("build/docs/asciidoc") // ๋ฌธ์„œ๊ฐ€ ์ƒ์„ฑ๋œ ๊ฒฝ๋กœ
	into file("src/main/resources/static") // ๋ฌธ์„œ๋ฅผ ๋ณต์‚ฌํ•  ๊ฒฝ๋กœ
}

// ์ƒ์„ฑ๋œ API ๋ฌธ์„œ๋ฅผ jar ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •, jar๋กœ ๋ฐฐํฌ ์•ˆํ•  ์‹œ ํ•„์š” ์—†์Œ
bootJar {
	dependsOn createDocument
	from("${asciidoctor.outputDir}") {
		into 'static/docs'
	}
}

 

์œ„ ์„ค์ •์—์„œ test ํƒœ์Šคํฌ ์‹คํ–‰ ์ค‘ ์‹คํŒจํ–ˆ๋Š”๋ฐ, ./gradlew test --stacktrace ๋ช…๋ น์–ด๋งŒ์œผ๋กœ๋Š” ์›์ธ์„ ์•Œ๊ธฐ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋” ์ž์„ธํ•œ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ./gradlew test --stacktrace --info๋ฅผ ์‹คํ–‰ํ•œ ๊ฒฐ๊ณผ, ์˜ค๋ฅ˜ ์›์ธ์„ ์ฐพ์„ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์ œ๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

./gradlew test --stacktrace --info ์‹œ Github Actions ์˜ log

 

๋ฌธ์ œ ์›์ธ

snippets๊ฐ€ ์ €์žฅ๋  ๊ฒฝ๋กœ์ธ /stay/stay-image-update ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†๋‹ค๋Š” ์ ์ด ์ฃผ์š” ์›์ธ์ด์—ˆ์Šต๋‹ˆ๋‹ค. ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•ด๋‹น ๊ฒฝ๋กœ๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์ง€๋งŒ, GitHub Actions ํ™˜๊ฒฝ์—์„œ๋Š” ์ƒ์„ฑ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ฒฝ๋กœ ์„ค์ • ๊ณผ์ •

์ €๋Š” ๋„๋ฉ”์ธ๋ณ„๋กœ ์Šค๋‹ˆํŽซ์„ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด /stay/stay-image-update ์™€ ๊ฐ™์€ ํด๋” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.
์ฆ‰, Gradle์˜ build.gradle์—์„œ ์„ค์ •ํ•œ ext.snippetsDir ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ์Šค๋‹ˆํŽซ์˜ ๊ฒฝ๋กœ๋Š”

build/generated-snippets/stay/stay-image-update๋กœ ์ง€์ •๋˜๊ธฐ๋ฅผ ๋ฐ”๋žฌ์Šต๋‹ˆ๋‹ค.

@DisplayName("์ˆ™์†Œ ์ด๋ฏธ์ง€ ์—…๋ฐ์ดํŠธ API")
@Test
void changeStayImage() throws Exception {
    //given
    ...
        
    // when then
    mockMvc.perform(
                    RestDocumentationRequestBuilders.put("/stay/{stayId}/image", stayId)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(imageUrls))
                            .sessionAttr(LOGIN_MEMBER, 1L)
                            .cookie(new Cookie("JSESSIONID", "ACBCDFD0FF93D5BB"))
            ).andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.code").value("0200"))
            .andExpect(jsonPath("$.status").value("OK"))
            .andExpect(jsonPath("$.message").value("OK"))
            .andExpect(jsonPath("$.data").exists())
            .andDo(document("/stay/stay-image-update",     << โญ์ฃผ์˜ํ•ด์„œ ๋ณผ ๋ถ€๋ถ„!!!!โญ
                    preprocessRequest(prettyPrint()),
                    preprocessResponse(prettyPrint()),
                    pathParameters(
                            attributes(key("url").value("/stay/{stayId}/image")),
                            parameterWithName("stayId").description("์ด๋ฏธ์ง€ ์—…๋ฐ์ดํŠธํ•  ์ˆ™์†Œ Id")
                    ),
                    requestFields(
                            fieldWithPath("[]").type(JsonFieldType.ARRAY).description("์—…๋ฐ์ดํŠธํ•  ์ˆ™์†Œ ์ด๋ฏธ์ง€(์ตœ์†Œ 5๊ฐœ)")
                    ),
                    responseFields(
                            beneathPath("data"),
                            fieldWithPath("stayId").type(JsonFieldType.NUMBER).description("์ˆ™์†Œ ID"),
                            fieldWithPath("hostId").type(JsonFieldType.NUMBER).description("์ˆ™์†Œ ์ฃผ์ธID"),
                            ...
                            fieldWithPath("imageUrls").type(JsonFieldType.ARRAY).description("์ˆ™์†Œ ์ด๋ฏธ์ง€ url")
                    )
            ));
}

 

์ฒ˜์Œ์—๋Š” Github Actions VM์—์„œ build ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์–ด์„œ!? ์ถ”์ธกํ•ด์„œ ์‹œ๋„ํ–ˆ์ง€๋งŒ, ์œ ํšจํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋˜ ์ค‘, CircleCI ์‚ฌ์šฉ์ž์˜ ์ด์Šˆ์ธ Spring Java 8 project, REST DOCS, Failed to create directory ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ  /stay/stay-image-update๋ฅผ ์ƒ๋Œ€ ๊ฒฝ๋กœ์ธ stay/stay-image-update๋กœ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

์™œ "/" ๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์œ ํšจํ•œ์ง€ ์›์ธ์„ ์ฐพ์•„๋ณด์•˜์Šต๋‹ˆ๋‹ค.

 

์›์ธ ๋ถ„์„: ์šด์˜ ์ฒด์ œ์˜ ํŒŒ์ผ ์‹œ์Šคํ…œ๊ณผ ๊ถŒํ•œ ๊ด€๋ฆฌ ๋ฐฉ์‹์˜ ์ฐจ์ด

  1. ๋กœ์ปฌ ํ™˜๊ฒฝ (Windows)
    • /๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ๋กœ๋ฅผ ์ง€์ •ํ•˜๋”๋ผ๋„, Windows ์—์„œ๋Š” ํŠน์ • ๋“œ๋ผ์ด๋ธŒ(C:\ ๋“ฑ)๋‚˜ ์‚ฌ์šฉ์ž ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๋งคํ•‘๋˜์–ด ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.
    • ๋”ฐ๋ผ์„œ ๋กœ์ปฌ ํ™˜๊ฒฝ์—์„œ /stay/stay-image-update๋Š” ์‚ฌ์šฉ์ž ๋””๋ ‰ํ† ๋ฆฌ ํ•˜์œ„์— ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  2. GitHub Actions ํ™˜๊ฒฝ (Linux/Ubuntu ๊ธฐ๋ฐ˜)
    • /๋กœ ์‹œ์ž‘ํ•˜๋Š” ๊ฒฝ๋กœ๋Š” ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ธ์‹๋ฉ๋‹ˆ๋‹ค.
    • ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๋Š” ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ์— ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•  ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ GitHub Actions์—์„œ๋Š” ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ์ด ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค
    • ์ €๋Š” build/snippets ํ•˜์œ„์— ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๋ ค ํ–ˆ์ง€๋งŒ, ๊ฒฝ๋กœ๋ฅผ "/" ๋กœ ์‹œ์ž‘ํ•˜๋Š” /stay/stay-image-update ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ ๊ฒฝ๋กœ๋กœ ์ง€์ •ํ•˜๋ฉด์„œ Linux ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ์ž˜๋ชป ํ•ด์„๋œ ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Š” ์ ˆ๋Œ€ ๊ฒฝ๋กœ์™€ ์ƒ๋Œ€ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ์ดํ•ด ๋ถ€์กฑ์—์„œ ๋น„๋กฏ๋œ ์˜ค๋ฅ˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , ์ˆ˜์ •ํ•œ ๊ฒฐ๊ณผ ๋“œ๋””์–ด Github Actions ์—์„œ ./gradlew test ๋ฅผ ์„ฑ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

./gradlew test ์„ฑ๊ณต !

 

์ถ”๊ฐ€ ํ™•์ธ: Linux ํ™˜๊ฒฝ์—์„œ์˜ ๊ถŒํ•œ ์ œํ•œ

๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑ ๊ถŒํ•œ ํ™•์ธ
WSL(Windows Subsystem for Linux)์„ ์‚ฌ์šฉํ•ด ์‹ค์ œ๋กœ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๊ฐ€ ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ์— ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ–ˆ์Šต๋‹ˆ๋‹ค.

 

Windows ํ™˜๊ฒฝ

  • ํŠน์ • ๋””๋ ‰ํ† ๋ฆฌ์—์„œ /use ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•˜๋‹ˆ C:\ ๋“œ๋ผ์ด๋ธŒ์— ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋์Šต๋‹ˆ๋‹ค.

Windows ํ™˜๊ฒฝ

WSL (Linux ํ™˜๊ฒฝ)

  • ๋™์ผํ•œ ์ž‘์—…์„ WSL์—์„œ ์ˆ˜ํ–‰ํ•˜๋ฉด ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ(/)์— ํด๋”๋ฅผ ์ƒ์„ฑํ•˜๋ ค๋Š” ๊ฒƒ์œผ๋กœ ์ธ์‹๋ฉ๋‹ˆ๋‹ค.
  • ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž๋Š” ๋ฃจํŠธ ๋””๋ ‰ํ† ๋ฆฌ์— ํด๋” ์ƒ์„ฑ ๊ถŒํ•œ์ด ์—†๊ธฐ ๋•Œ๋ฌธ์— ์•„๋ž˜์™€ ๊ฐ™์ด ๋””๋ ‰ํ† ๋ฆฌ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

WSL ํ™˜๊ฒฝ

 

 

๋งˆ์น˜๋ฉฐ

ํ•˜๋ฃจ์ข…์ผ 10์‹œ๊ฐ„ ๋™์•ˆ ์‚ฝ์งˆํ•˜๋ฉฐ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋งŽ์€ ๊ฒƒ์„ ๋ฐฐ์šฐ๊ณ  ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  • @TestConfiguration๊ณผ @Configuration์˜ ์ฐจ์ด ๋ฐ ๋™์ž‘ ์›๋ฆฌ
  • Spring Boot ์˜ ์™ธ๋ถ€ ์„ค์ • ์šฐ์„ ์ˆœ์œ„
  • ์šด์˜ ์ฒด์ œ์˜ ํŒŒ์ผ ์‹œ์Šคํ…œ๊ณผ ๊ถŒํ•œ ๊ด€๋ฆฌ ๋ฐฉ์‹
  • ํŒŒ์ผ ๊ฒฝ๋กœ์˜ ์ ˆ๋Œ€ ๊ฒฝ๋กœ/์ƒ๋Œ€ ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ์ดํ•ด
  • GitHub Actions์—์„œ ๋กœ๊ทธ ํ™•์ธ ๋ฐ ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•

๋ฌผ๋ก , GitHub Actions์˜ ๋กœ๊ทธ๊ฐ€ ์—†์—ˆ๋‹ค๋ฉด ์ ˆ๋Œ€๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†์—ˆ์„ ๋ฌธ์ œ๋“ค์ด์—ˆ์Šต๋‹ˆ๋‹ค..
(๋ฌผ๋ก , ./gradlew test --info ๋ช…๋ น์–ด๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค ์ˆ˜์ค€์˜ ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐ๋Š” ์ธ๋‚ด๊ฐ€ ํ•„์š”ํ–ˆ์ง€๋งŒ์š”...)

 

์š”์ฆ˜ ๋‹ค์–‘ํ•œ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์„ ๊ฒช์œผ๋ฉด์„œ ๋Š๋ผ๋Š” ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.
์™œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ์›์ธ์„ ์ •์˜ํ•˜๊ณ  ๋ถ„์„ํ•ด์•ผ๋งŒ, ๋‹ค์Œ์— ๋น„์Šทํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ ์‘์šฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ์‚ฌ์‹ค์ž…๋‹ˆ๋‹ค.

์•ž์œผ๋กœ๋„ ํ•ญ์ƒ "์™œ ๋ฐœ์ƒํ–ˆ๋Š”์ง€"๋ฅผ ๊นŠ์ด ๋ถ„์„ํ•˜๋ฉฐ, ์ฒด๊ณ„์ ์œผ๋กœ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…์„ ํ•ด๊ฒฐํ•ด ๋‚˜๊ฐ€๋ ค ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ฒฝํ—˜์ด GitHub Actions๋ฅผ ์‚ฌ์šฉํ•ด CI ์ž๋™ํ™” ํ…Œ์ŠคํŠธ๋ฅผ ๊ตฌ์ถ•ํ•˜๋ ค๋Š” ๋ถ„๋“ค์ด๋‚˜, ์ €์™€ ๋น„์Šทํ•œ ์˜ค๋ฅ˜๋กœ ๊ณ ๋ฏผ ์ค‘์ธ ๋ถ„๋“ค์—๊ฒŒ ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ผ๋ฉฐ ๋งˆ์น˜๊ฒ ์Šต๋‹ˆ๋‹ค.