ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Github Packages로 private maven repository 만들기
    빌드 자동화 도구/Gradle 2024. 7. 25. 16:39

     

    업무 중에 두 프로젝트에서 공통된 부분을 어떻게 관리할 수 있을지 고민하던 중에 선택한 방법을 공유해봅니다.

     


     

    글은 아래와 같은 순서로 진행됩니다.

    1. 작업을 시작한 배경

    2. 어떻게 개선할 것인가

    3. 로컬환경에서 publish 하기

    4. Github Packages 사용해서 gradle registry 만들기

    5. 라이브러리 적용하기

    6. 빈등록 이슈 해결

    7. CI/CD 파이프라인 생성 (optioanl)

     

     

     

     

    1. 작업을 시작한 배경

    회사 코드를 바로 쓸 수 없으니, 비슷한 상황을 가정해보겠습니다

    저는 Greeting Project를 만들고 있습니다. 초기에는 간단한 기능들로 구성된 'Simple Server'를 구현해서 프로젝트를 운용 중이었습니다.

     

     

     

     일을 하다보면 언제나 그렇듯.. 일정이 얼마남지 않은 상황에서 '특별한 기능'을 추가해야한다는 요구사항이 들어왔습니다!

    이 '특별한 기능'은 많은 부하가 발생할 것이 예상되어 별도의 환경에 배포를 하게 되었고, 별도의 Github 레포지토리를 생성해서 관리하게 되었습니다.

     하지만 중복되는 코드에 대한 대응을 하기에는 시간이 부족했습니다. 'Greeting Service'의 로직이 양쪽 서버에서 다 필요했기 때문에, 새로운 'Special Server'를 생성하면서 'Simple Server'에 있는 'Greeting Service'를 그대로 가져다 사용을 하였습니다.

     

     

     

    두 개의 레포지토리를 만들어서 급한 불은 껏지만, 시간이 얼마 지나지 않았음에도 'GreetingService'에 변경점이 생기기 시작합니다.

    변경점이 발생하면 개발자 직접 양쪽 서버에 반영을 해야했지만 이 것은 쉬운 일이 아니죠..

     

     

    지금은 아주 작은 차이이지만 각기 다른 의존성이 엮이기 시작하면 분리가 더욱 힘들어질 것이 너무나도 분명합니다.

    다시 하나로 관리하는 것이 시급하다는 판단을 하였습니다.

     

     

     

    2. 어떻게 개선할 것인가?

    개선이 필요하다는 것은 분명하니, 어떤 방법들이 있고 어떤 방법이 현재의 프로젝트 상황에 가장 적합한지를 생각해봅니다.

     

    Try1. Repo1 / deploy2

    근본적으로 코드의 중복이 발생하는 원인은 레포지토리가 2개로 나누어졌기 때문이라고 생각합니다.

    두 개의 레포지토리를 1개로 합치되, 멀티 모듈 형태로 관리하여 배포는 따로 하는 옵션입니다.

     

    • 장점. 관리 포인트가 감소
    • 단점. 프로젝트 내의 복잡도 증가, 인프라 추가 대응 필요  

     

    Try2. Repo3 / deploy2

    기존에 적용되어있는 인프라를 그대로 사용하면서 중복되는 부분만 라이브러리 형태로 분리하는 형태입니다.

     

    • 장점: 개별 프로젝트 단순하게 유지, 인프라 변동 필요없음
    • 단점: 관리 포인트 증가

     

     

    현실적으로 적용이 가능한 옵션은 위의 2개 정도로 추려볼 수 있었습니다.

    저희 팀은 2번 옵션을 선택해서 작업을 진행하기로 결정하였습니다. 현재 상태에서 프로젝트가 크게 변경이 없을 것으로 예상하였기 때문에, 구축되어있는 인프라를 변경하고 테스트하는 것보다 코드 수준에서 변경하고자 하였습니다. 그리고 개별 프로젝트를 단순하게 유지하는 기를 원하는 것도 또 다른 이유 중 하나였습니다.

     

     

     

    3.  로컬환경에서 publish 하기

    step1. 새로운 프로젝트를 생성하고 공통으로 사용되는 코드를 migration 합니다.

    중복이 발생하고 있는 GreetingService.java의 코드입니다.

     

    'share-lib' 라는 새로운 프로젝트를 만들고 내부에 GreetingService 를 생성하겠습니다.

     

    라이브러리로 사용할 것이니, 스프링부트 진입점은 필요없으니 제거하겠습니다.

    ShareLibApplication.java와 관련 테스트를 제거한 상태입니다. 

     

    @Service 어노테이션 사용을 위한 최소한의 의존성만 남겨둡니다.

    dependencies {
        implementation 'org.springframework:spring-context:6.0.8'
    }

     

     

    step2. 로컬환경에 Publish 해보기

    publish를 위해 build.gradle에 추가 해주어야할 부분은 크게 2가지 입니다.

    plugins에 maven-publish를 추가해줍니다.

    // build.gradle
    
    plugins {
    	id 'java-library'
    	id 'maven-publish'
    }

     

    publishing 이라는 블록을 추가해줍니다.

    우선은 잘 동작하는지 로컬에 게시를 해보겠습니다.

    // build.gradle
    
    group = 'com.blog'
    version = '0.0.1'
    
    
    publishing {
        publications {
            'service-layer'(MavenPublication) {
                groupId = group
                artifactId = "service-layer"
                version = version
    
                from components.java
            }
        }
        repositories {
            maven {
                name = "local"
                url = uri("${projectDir}/repo") // 테스트용 로컬 경로
            }
        }
    }

     

     

    publish 명령어를 실행시킵니다.

    $ ./gradlew publish

     

    루트 프로젝트 / repo 경로에 gradle에 설정한대로 라이브러리 파일들이 생성되는 것을 확인할 수 있습니다!

     

     

    4.  Github Packages 사용해서 maven registry 만들기 ( 공식문서 )

    우선, 위의 로컬에서 테스트하면서 생성한 ./repo 경로는 삭제를 합니다.

    그리고 3번에서 작성한 코드를 올려줄 Github Repository를 하나 생성합니다.

    이 레포지토리를 외부에서 참조할 예정입니다.

    publish의 결과물을 github packages에 저장하도록 변경해줍니다.

    publishing {
        publications {
            'service-layer'(MavenPublication) {
                groupId = group
                artifactId = "service-layer"
                version = version
    
                from components.java
            }
        }
    
        repositories {
            maven {
                name = "shared-service" // repositories 속에 여러 개의 maven {} 이 있을 때 쉽게 구분하기 위한 이름
                url = uri("https://maven.pkg.github.com/blog-example/-Lib-share-service")
            }
        }
    }

     

    공식문서를 살펴보면 gradle registry의 경우에는 다음과 같은 형식으로 uri를 사용하도록 합니다.

    https://maven.pkg.github.com/<작성자Id>/<레포이름>
    
    혹은
    
    https://maven.pkg.github.com/<organization 이름>/<레포이름>

     

    publish 명령어를 실행시키면 서버로 부터 401 예외가 발생합니다.

     

    Package를 write할 수 있는 권한을 갖은 토큰을 적용해야합니다

     

    공식문서의 예제처럼 credential을 추가해줍니다

    repositories {
            maven {
                name = "service-layer"
                url = uri("https://maven.pkg.github.com/blog-example/-Lib-share-service")
                credentials {
                    username = <github Id>
                    password = <github token>
                }
            }
        }
        
        
        
      // 위의 방법이 안되면 이런 방법도 있습니다.
      
      repositories {
            maven {
                name = "service-layer"
                url = uri("https://maven.pkg.github.com/blog-example/-Lib-share-service")
                credentials(HttpHeaderCredentials::class.java) {
                    name = "Authorization"
                    value = "Bearer <github token>"
                }
                authentication {
                    register("header", HttpHeaderAuthentication::class)
                }
            }
        }

     

     

    다시 publish를 합니다

    $ ./gradlew publish

     

    성공적으로 publish가 되었다면 깃헙 레포지토리에서 packages 섹션이 활성화 된것을 볼 수 있습니다.

    눌러서 들어가보면, 위에서 로컬에 만들었던 것과 동일한 파일들을 확인할 수 있습니다.

     

    5.  라이브러리 적용하기

    사용하는 측은 간단합니다. 

    build.gradle 파일에, registry 주소가 어디인지 등록하고 가져오고자하는 버전의 라이브러리를 dependencies에 추가하면 됩니다.

     

    ...
    
    repositories {
    	mavenCentral()
    	maven {
    		url = uri("https://maven.pkg.github.com/blog-example/-Lib-share-service")
    		credentials {
    			username = <github id>
    			password = <github token>
    		}
    	}
    }
    
    ...
    
    dependencies {
    	...
        implementation ('com.blog:service-layer:0.0.1')
    }
    
    ...

     

     

    dependencies 추가 후에 gradle을 새로고침 해보면 External Libraries에 com.blog.service-layer가 추가된 것을 확인할 수 있습니다!

     

     

    GreetingService를 사용하고 있는 Controller 파일에 가보면 해당 라이브러리로부터 GreetingService를 import 할 수 있는 것을 확인가능합니다.

     

     

    6. 빈 등록 이슈 해결

    이렇게 한번에 된다면 좋겠지만 쉽게 될리가 없습니다 ㅎㅎ. 실행을 시켜보니 GreetingService의 Bean 등록이 필요하다고 이야기합니다.

     

    그러면 라이브러리가 dependencies에 추가되서 사용되려고할 때 component scan 이 되도록 다시 library 코드로 돌아가서 환경설정을 추가해줍니다. 간단하게 컴포넌트 스캔을 하는 Configuration을 작성해줍니다.

     

    package com.example.share_lib.service;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan(basePackages = {"com.example.share_lib.service"})
    public class ServiceConfig { }

     

     

    그리고 Library를 사용하는 측에, '내가 추가해야하는 Configuration이 있다!' 라는 것을 알려주는 파일을 작성해줍니다.

    • /resources/META-INF/spring 이라는 폴더를 만들어줍니다.
    • org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일을 생성합니다
    • 위의 파일 내에, Config 파일이 위치한 경로를 작성해줍니다.

     

    @AutoConfiguration과 @Conditional 을 사용하는 config 파일을 등록해서 사용하는 용도이지만, 별다른 조건이 필요없는 간단한 라이브러리이기 때문에 위와 같이 사용해보았습니다.

     

    그러면 업데이트 사항이 생겼으니 버전은 0.0.2로 올려서 다시 publish 해보겠습니다!

    이런식으로 Release note 같은 것도 작성해주면 더 라이브러리 같네요 ㅎㅎ

     

     

    사용처에서도 0.0.2로 버전업을 해주면, Config 파일이 추가되는 것을 확인할 수 있습니다.

     

    포스트맨을 사용해서 동작까지 확인 완료!

     

     

    7. CI/CD 파이프라인 생성 (optioanl)

    지금까지도 충분하지만.. 유지보수를 고려하시는 분이라면 분명히 귀찮겠는데? 라는 생각을 하셨을 것이라고 생각합니다.

    '매번 변경사항 생길 때마다, 코드 수정해서 깃에 올리고 package도 버전을 올려야하나?'

     

    저는 그리하여 git 의 tag에 버전을 명시하면 해당 tag 버전과 동일한 library 버전이 packages에 publish 되도록

    github action을 구성해보았습니다.

     

    먼저 외부에서 version과 github credential을 받을 수 있도록 build.gradle을 변경합니다

    version, gitId, gitToken 이라는 변수를 선언하고 재사용하는 방식을 사용했습니다.

    ...
    
    def projectVersion =  System.getenv("LIB_VERSION") ?: "0.0.1"
    def gitId = System.getenv("GIT_ID") ?: ""
    def gitToken = System.getenv("GIT_TOKEN") ?: ""
    
    group = 'com.blog'
    version = projectVersion
    
    ...
    
    
    publishing {
    
    	...
        
        repositories {
            maven {
                name = "service-layer"
                url = uri("https://maven.pkg.github.com/blog-example/-Lib-share-service")
                credentials {
                    username = gitId
                    password = gitToken
                }
            }
        }
    }

     

    gradle에 정의한 환경변수가 잘 적용될 수 있도록 ./.github/workflows/<action_name>.yml을 작성해줍니다

    name: Publish by version tag
    
    on:
      push:
        tags:
          - '[0-9]+.[0-9]+.[0-9]+'
    
    jobs:
      build:
        name: Publish by version tag
        runs-on: ubuntu-latest
    
        steps:
          -   name: Checkout
              uses: actions/checkout@v4
    
          -   name: Set up Java JDK
              uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9
              with:
                distribution: "oracle"
                java-version: "17"
    
          -   name: Extract tag name
              id: extract_tag
              run: echo "TAG_NAME=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
    
          -   name: Cache Gradle dependencies
              uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9
              with:
                path: ~/.gradle/caches
                key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
                restore-keys: ${{ runner.os }}-gradle
    
          -   name: Publish to GitHub Packages
              run: ./gradlew publish
              env:
                GIT_ID: ${{ secrets.GIT_ID }}
                GIT_TOKEN: ${{ secrets.GIT_TOKEN }}
                LIB_VERSION: ${{ env.TAG_NAME }}

     

    action에서 사용할 GIT_ID와 GIT_TOKEN을 레포지토리 시크릿에 등록해줍니다.

     

     

    0.0.3 태그를 생성해서 push를 하고 action이 완료될 때까지 기다립니다.

    $ git tag 0.0.3
    $ git push origin 0.0.3

     

    패키지의 버전이 0.0.3 버전으로 잘 올라갔고, simple-server 에서도 0.0.3을 implementation했을 때 잘 작동합니다!!

     

     

    끝!

    감사합니다 :)

     

     

     


    참고

    [Github Packages]

    https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-gradle-registry

     


    [Gradle 관련]
    https://docs.gradle.org/current/userguide/publishing_setup.html
    https://docs.gradle.org/current/userguide/publishing_maven.html#publishing_maven

     

     

    [Credentials 관련]

    https://medium.com/widgetlabs-engineering/using-a-maven-repository-in-a-private-github-repository-with-gradle-46ab73f198a6

    https://levelup.gitconnected.com/how-to-publish-a-private-repository-via-github-packages-github-workflows-and-gradle-4b9ebec6aef8

     

     

    [AutoConfigure 관련]
    https://blog.karsei.pe.kr/133

     

     

     

    '빌드 자동화 도구 > Gradle' 카테고리의 다른 글

    Multi Module Project 생성하기  (0) 2024.04.08
Designed by Tistory.