개발하면서 gradle이 빌드 작업을 편리하게 관리해주는 건 알겠는데 사실 잘 모르고 쓴 적이 더 많았던 거 같다. 대충 Getting Started 내용 붙여넣고... 하는 식.
최근에 build.gradle을 만질 일이 자주 생겼다. Spring REST docs를 도입하고 restdocs-api-spec을 사용해서 OAS 파일로 export 한다던가... 이렇게 내보낸 yaml 파일을 빌드 디렉토리에서 resources/static으로 옮겨주는 작업을 gradle task로 등록하고 빌드 시 실행되게 한다던가... 아니면 editorconfig를 사용해서 포매팅이 안맞으면 빌드 실패를 뿜게 만든다던가 하는 식으로.
작업하는 김에 여기에 적어두려고 한다.
먼저 그래들 빌드 순서를 알아보자.
꽤 신기한 점 중 하나는 check와 test가 분리되어 있다는 것이다.
https://www.baeldung.com/gradle-test-vs-check
여기를 보면 설명이 잘 나와있다. 내용을 적당히 요약해보자면
- check 작업의 경우 '라이프사이클' 작업이라고 한다.
- 자체적으로는 아무 것도 수행하지 않는다. 대신, 하나 이상의 다른 작업을 실행한다.
- 기본적으로 check 작업은 test 작업만 실행한다.
- 만약 다른 플러그인을 추가하면? test 작업 외에 다른 작업이 추가될 수 있다.
- 즉 check 작업은 말 그대로 '검사하는' 역할인 것이다.
- 이 검사에는 테스트 뿐만이 아니라 소스 코드 스타일 적용, 라이브러리 취약점 검색, 통합 테스트 등이 적용될 수 있다.
https://gradlehero.com/gradle-check-task-essentials/
여기 내용도 같이 보자.
check
를 이해하기 위해서는 gradle에 존재하는 두 가지 타입의 작업(task)를 이해해야 한다.- actionable tasks : 빌드 작업에서 어떤 액션을 수행하는 작업
- lifecycle tasks : 워크플로우 작업으로 어떤 액션도 존재하지 않음.
- check는 라이프사이클 작업이다. 라이프사이클 작업은 다른 여러 작업을 결합하여 개발 워크플로우의 일부분을 구성한다.
- check의 목적은 모든 검증(verification) 작업을 하나의 작업으로 결합하는 것이다.
구체적으로 어떻게?
tasks.named("check").configure {
dependsOn(tasks.named("integrationTest"))
}
이렇게 하면 check가 integrationTest에 의존하게 된다. 즉 intergrationTest 다음에 check가 실행된다.
mustRunAfter
이라는 것도 있다. 대충 비슷해보이는데 차이가 뭘까?
A task may have dependencies on other tasks or might be scheduled to always run after another task. Gradle ensures that all task dependencies and ordering rules are honored when executing tasks, so that the task is executed after all of its dependencies and any "must run after" tasks have been executed.
출처는 공식문서다. 해석해보자.
작업은 다른 작업에 대한 종속성을 가지거나, 항상 다른 작업 후에 실행되도록 예약할 수 있습니다. Gradle은 작업을 실행할 때 모든 작업 디펜던시와 순서 규칙이 보장하여, 우리가 실행하려는 작업이 "모든 디펜던시"와 모든 "
must-run-after
" 작업이 실행된 후에 실행되도록 한다.
흠... 확실히 dependsOn
과 mustRunAfter
를 구별하는 것 같다.
이럴 땐 검색을 해보자. https://stackoverflow.com/questions/42033490/whats-the-difference-between-mustrunafter-and-dependson-in-gradle
taskA, taskB에 대해서 A -> B 순서로 실행하고 싶을 때 mustRunAfter인 경우와 dependsOn인 경우를 비교해보자.
- 만약
gradle taskB
를 실행하면?mustRunAfter
: A 다음에는 B가 실행된다는 규칙을 어기지 않았으므로 제대로 taskB만 실행된다.dependsOn
: B는 A에 의존하므로 taskA -> taskB가 실행되도록 한다. 즉 디펜던시로 엮어있으면 자동으로 가져와서 실행해준다.
만약 taskA, taskB, taskC가 있고 taskC가 taskA, taskB에 dependsOn
한다고 하자. 아래와 같다.
taskC {
dependsOn(taskA, taskB)
}
이때 taskC를 실행하면 그래들은 의존관계에 있는 taskA, taskB를 끌고와서 실행해주긴 하지만 그 순서를 보장하지는 않는다.
요약하자면 뭐냐?
dependsOn
은 각 작업에 대한 의존관계를 만들어주고mustRunAfter
는 각 작업에 대한 순서를 지정한다.- 의존관계는 말 그대로 그 작업을 수행하기 위한 일종의 필요조건이고, 순서는 그냥 순서다.
- 의존관계를 추가하면 의존관계에 있는 작업들을 미리 실행해서 조건을 만들어주기만 하고 이때 순서는 신경쓰지 않는다.
고민을 쭉 적어봤다.
- 일단 통합 테스트의 경우 단위 테스트 (혹은 슬라이스 테스트)와 분리할 필요가 있다. Gradle 공식 문서도 그렇고 다양한 가이드에서
check
작업이 통합 테스트를 의존하게 하는 예시를 볼 수 있다. 그러면 통합 테스트는 일반적인test
작업과 별도로 존재해야 하고, 이 작업은test
디렉토리 아래에 존재하는 모든 테스트에 대해 수행되므로... 별도 디렉토리로 빼야한다는 결론이 나온다. 실제로 다른 곳에서도 main / test / integrationTest 세 패키지를 따로 만들어 쓴다고. - https://docs.gradle.org/current/userguide/java_testing.html#sec:configuring_java_integration_tests
gradle 공식 문서에서 통합 테스트 환경 구성하는 법을 설명해준다. 나중에 보도록 하자. 공부할 게 너무 많다... - https://velog.io/@mu1616/javagradle%EB%8B%A8%EC%9C%84-%ED%85%8C%EC%8A%A4%ED%8A%B8unit-test%EC%99%80-%ED%86%B5%ED%95%A9-%ED%85%8C%EC%8A%A4%ED%8A%B8integration-test-%ED%99%98%EA%B2%BD-%EB%B6%84%EB%A6%AC
이 글도 꽤 괜찮아보인다. - https://toss.tech/article/how-to-manage-test-dependency-in-gradle
이것도 꽤 맛있어보이는 글이다. 멀티 모듈 환경에서 테스트 픽스쳐 사용하는 법에 대한 글이다.
우리에게 필요한 빌드 작업을 정리해보자.
- editorconfigCheck
- 코딩 스타일이 맞지 않는 파일이 있는지 검사한다.
- checkStyle
- 나중에 도입할 예정이다. gradle task 대신 sonarQube와 통합해서 처리할 수도 있다. 지정한 컨벤션과 일치하지 않는 표현이 있는지 검사한다.
- openapi3
restdocs-api-spec
을 사용하여 OAS 파일을 export 하는 역할을 수행한다. 해당 task 실행 시 check - test 과정이 실행되는 것으로 보아 아마 test task에 대한 디펜던시를 가지는 것으로 보인다.
- copyOasToResources
- 직접 정의한 gradle task이다. build 디렉토리에 생성되는 OAS 파일을 지정하여 /
src/resources/static
에 존재하는 기존 파일을 삭제하고 / 여기로 복사한다.
- 직접 정의한 gradle task이다. build 디렉토리에 생성되는 OAS 파일을 지정하여 /
순서도 고민해봤다.
- 1의 경우
check.dependsOn editorconfigCheck
를 통해 check를 실행하기 전 editorconfigCheck 검사가 실행되는 것을 보장해야 한다. - 2의 경우 역시 check에 디펜던시를 걸어주면 된다.
- 3의 경우 내부적으로 test 작업에 대한 디펜던시를 가지는 것으로 보인다. 아마 test 작업을 실행해야 asciidoc 스니펫이 만들어지고, 이를 바탕으로 OAS 파일을 만들기 때문이지 않을까... 라고 추측한다.
어쨌든든, 현재 구축된 CI/CD 워크플로우에서는 그래들의 clean build를 통해 실행하고 있다. 그렇다면 이 openapi3 작업을 1) 빌드 작업에 대해 추가하거나 2) 테스트 작업 실행 후 생성되도록 하는 게 맞을 것 같다.
그러면dependsOn
대신 이에 대응되는finalizedBy
를 써야 한다. 만약dependsOn
을 사용하여test.dependsOn('openapi3')
와 같이 사용하면 check 하면서 test를 실행하기 위해 필요한 openapi3를 실행하면서 openapi3의 디펜던시인 check를 다시 실행해야 하므로 순환 의존이 발생한다. 실제로 해보면 이렇게 뜬다.하지 말라는 건 하지 말자...Circular dependency between the following tasks: :check \--- :test \--- :openapi3 \--- :check (*)
아무튼test.finalizedBy('openapi3')
와 같이 설정해주면 된다. 그럼 test 작업이 끝나면 openapi3가 자동으로 실행되어, yaml 파일이 업데이트된다. - 4의 경우 openapi3가 끝나면 자동으로 실행되어야 한다. 마찬가지로
openapi3.finalizedBy('copyOasToResources')
와 같이 작성해주자.
끝...!
통합 테스트의 경우 일단 더더 공부를 해봐야겠다. 픽스쳐 구성에 대해서도 고민해봐야하고... 그리고 시간이 되는대로 추출한 OAS를 stoplight elements를 사용하여 정적 페이지로 호스팅하는 방법도 올려봐야겠다. 사실 찾아보면 대부분 swagger-ui를 사용하는 글만 있는데, 개인적으로는 swagger-ui의 경박한 디자인보다 stoplight의 심플한 감성을 더 좋아한다. 적용하는 방법은 꽤 간단해서 짧게 언급만 해보려고 한다.