Java로 Builder Program 만들기 (1) - Java JGit Library 사용해서 Git Controller하기

최근 CI/CD를 구현하면서 Builder라는 것에 대해 많이 생각하게 되었다.

 

단순히 빌드( 개념 정리는 여기 클릭 )를 진행해 주는 프로그램이라 생각했었고, 단순히 사용만 하던 이 Builder라는 녀석을 직접 Java로 구성해보려 하니 막막했다.

 

특정된 FrameWork이나 특정된 언어(Java, PHP 등)로 구성된 Project만 빌드 해주는 builder가 아니라 사용자의 입맛대로 (원하는 언어 혹은 FrameWork, Git Repository 등을 사전에 설정하면 원하는 방식으로) Build 해주는 프로그램을 만들어 보려고 한다.


Java Builder 이미지 메이킹

builder가 하는 역할이나 흐름도에 대해 머릿속에서 대략적으로 생각했던 것들을 손으로 정리해 보았다.

원하고자 하는 기능들만 나열했을 경우 크게 BuilderClass, GitClass, DockerClass, ArchiveClass 4가지의 메인 Class(Controller)가 필요하다. 각각의 Class는 특정 기능들을 담당하게 되고, Main method가 있는 Builder Class에서 다른 클래스들을 사용하여 전반적인 Build 작업을 실행하게 한다.

 

[Java <-> Git ] 유연한 Library 찾기

Java의 장점은 수많은 Java 개발자들이 많다는 것이다. 때문에 많은 Java개발자들이 각자의 방식대로 특정 기능을 수행하게 만든 수많은 외부 라이브러리들이 존재한다. Java와 Git을 연결해주는 Library들은 많았고, 나는 그중 JGit이라는 라이브러리를 채택하여 사용하였다.

 

개발을 진행함에 있어서 나는 고민했다. Abstract Class 및 Interface를 최대한 활용하여 개발을 진행하고 싶었는데 해당 프로젝트는 각 기능을 맞은 큰 부분의 Class(위의 Builder, Docker, Git, Arcive)가 각각 1개면 충분하였기에, Interface는 초기 설계도라는 명목하에 꼭 필요한 함수들의 선언을 목적으로 사용했다.

 

해당 빌더는 메모리의 효율을 극대화하고 싶었기 때문에 꼭 필요한 Library 이외의 Dependency 주입을 받고 싶지 않아 Lombok을 사용하지 않았고, 때문에 Abstract Class의 경우 Interface를 구현하는 구현 설계의 목적으로 Getter 및 Setter를 정의하였다.

 

또한 Git Class의 경우 JGit Library를 사용하기 때문에 자칫 잘못하면 JGit Class가 될 우려가 있으므로 JGitUtil Class를 만들어 유틸 사용에 관련된 모든 기능 정의는 JGitUtill Class에서 진행해 주었다.

 

각각의 Main Class의 경우 명칭을 Controller로 진행할까 하였으나, 해당 프로젝트의 경우 Framework를 사용하지 않고, Model 및 View가 필요 없기에 자칫 잘못하면 MVC 패턴의 C와 헷갈릴까 봐 Manager라는 명칭으로 지어주었다.

 

JGit Dependency 주입받기

나는 Java Build Tool로 Gradle을 사용하였다. JGit에 대한 상세한 문서는 여기를 참고하였다. 처음으로 build.gradle 파일에서 해당 라이브러리에 대한 의존성 주입을 받았다. 나는 4.4.1 버전을 사용하였는데 이 버전 이후로 추가된 RemoteAddCommand 객체를 사용하고 싶었기 때문이다. 본인이 사용하는 라이브러리에 대한 문서는 구글 번역을 돌려서라도 최소한의 버전별 추가(혹은 삭제)된 기능은 파악할 필요가 있다.

# build.gradle

dependencies {
    implementation group: 'org.eclipse.jgit', name: 'org.eclipse.jgit', version: '4.4.1.201607150455-r'
}

 

 

Make JGitUtil Class 

이제 실질적으로 Git 명령어를 컨트롤 해줄 JGitUtil Class를 만들어야 할 차례이다. 위에 말한 것 처럼 Interface와 Abstract Class를 꼭 사용하여 개발하려고 노력하였다. JGit을 사용하기 위해서는 git pull, git clone 등 git 명령어를 사용할 method name 정의를 설계도의 목적으로 interface에 작성하였다.

package mingyu.interfaces.util;

import org.eclipse.jgit.api.Git;
import java.io.File;

public interface JgitUtilInterface {
    public abstract void clone(Git git) throws Exception; // git clone
    public abstract void remoteAdd(Git git) throws Exception; // git remote add
    public abstract void push(Git git) throws Exception; // git push
    public abstract void add(Git git, String filePattern) throws Exception; // git add
    public abstract void rm(Git git, String filePattern) throws Exception; // git rm
    public abstract void commit(Git git, String msg) throws Exception; // git commit -m
    public abstract void pull(Git git) throws Exception; // git pull origin/main
    public abstract void lsRemote(Git git) throws Exception; // git ls-remote branch
    public abstract void checkOut(File dir) throws Exception; // git checkout
}

JgitUtilInterface에 명시해놓은 method 생김새를 실제 JgitUtil에서 내용을 붙여 그대로 개발할 것이기 때문에 interface에서는 git 작업에 필요한 function만 정의하였다. 위에서 말한 것 처럼 해당 빌더를 무겁지 않게 만들기 위해 필요가 없거나 Resource를 많이 잡아먹는 Library는 안 사용하려 한다.

 

때문에 Getter Setter를 자동으로 편하게 생성해주는 Lombok을 사용하지 않기로 하였고 Getter & Setter는 Abstract Class를 사용하여 구현하였다.

package mingyu.abstracties;


import org.eclipse.jgit.transport.CredentialsProvider;

public abstract class JgitUtilAbstract {
    private String userId = null; // 예) "rlaalsrb0466@naver.com"
    private String userPass = null; // 예) "git Password"
    private String userName = null; // 예) "mingyu"
    private String userEmail = null; // 예) "rlaalsrb0466@naver.com"
    private String hash = null;  // 예) "origin/main"
    private String url = null; // 예) "https://gitlab.com/mingyukim/project1"
    private String localPath = null; // 예) /app/builder/javaBuilder/tmp/build/source/202212271342/
    private String branchName = null; // 예) master , main , dev , staging 등
    private CredentialsProvider cp = null; // new UsernamePasswordCredentialsProvider(userId, userPass)

    public String getUrl() { return url; }
    public void setUrl(String url) { this.url = url; }
    // 이하 get set method
}

위의 Interface의 구현부 및 Abstract Class의 상속을 받은 JGitUtill Class의 경우 여러개의 객체를 생성해서 각개전투 하는 방식이 아닌, 한 객체만 생성하면 해당 Job 처리를 끝낼 때 까지 (혹은 그 이후에도) 1개의 객체로 컨트롤을 해야하기 때문에 싱글턴 패턴을 사용하여 개발해주었다. 이 Util은 GitManager Class에서 요긴하게 사용된다.

package mingyu.utiles;

import java.io.File;
import java.util.Collection;

import mingyu.interfaces.util.JgitUtilInterface;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.PullCommand;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.RemoteAddCommand;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.transport.URIish;

import mingyu.abstracties.JgitUtilAbstract;

public class JgitUtil extends JgitUtilAbstract implements JgitUtilInterface {

    private static JgitUtil instance;

    public static JgitUtil getInstance() {
        if (instance == null) {
            synchronized (JgitUtil.class) {
                instance = new JgitUtil();
            }
        }
        return instance;
    }

    public static Git init(File dir) throws Exception {
        return Git.init().setDirectory(dir).call();
    }

    public Git open(File dir) throws Exception {
        Git git = null;
        try {
            git = Git.open(dir);
        } catch (RepositoryNotFoundException e) {
            git = JgitUtil.init(dir);
        }
        return git;
    }

    @Override
    public void clone(Git git) throws Exception {
        File localPath = File.createTempFile(getLocalPath(),"");
        Git result = Git.cloneRepository()
            .setURI(getUrl())
            .setDirectory(localPath)
            .call()    
        }
    }

    @Override
    public void remoteAdd(Git git) throws Exception {
        // add remote repo:
        RemoteAddCommand remoteAddCommand = git.remoteAdd();
        remoteAddCommand.setName(getHash());
        remoteAddCommand.setUri(new URIish(getUrl()));
        // 아래 remoteAddCommand 객체 하위에 접근해 원하는 셋팅을 더 해줄 수 있음.
        remoteAddCommand.call();
    }

    @Override
    public void push(Git git) throws Exception {
        // 원격지에 push
        PushCommand pushCommand = git.push();
        pushCommand.setCredentialsProvider(getCp());
        pushCommand.setForce(true);
        // 아래 pushCommand 객체 하위에 접근해 원하는 셋팅을 더 해줄 수 있음.
        pushCommand.call();
    }

    @Override
    public void add(Git git, String filePattern) throws Exception {
        git.add().addFilepattern(filePattern).call();
    }

    @Override
    public void rm(Git git, String filePattern) throws Exception {
        git.rm().addFilepattern(filePattern).call();
    }

    @Override
    public void commit(Git git, String msg) throws Exception {
        git.commit()// 커밋
            .setAuthor(getUserName(),getUserEmail())// 작성자
            .setMessage(msg)// 커밋 메세지
            .call();
    }

    @Override
    public void pull(Git git) throws Exception {
        PullCommand pull = git.pull();
        pull.setRemoteBranchName(getBranchName())
            .setCredentialsProvider(getCp())
            .call();
    }

    @Override
    public void lsRemote(Git git) throws Exception {
        Collection<Ref> remoteRefs = git.lsRemote()
            .setCredentialsProvider(getCp())
            .setRemote("origin")
            .setTags(false)
            .setHeads(true)
            .call();
        // for (Ref ref : remoteRefs) {
        // 나중에 요긴하게 사용해보자
        // System.out.println(ref.getName() + " -> " + ref.getObjectId().name());
        // }
    }

    @Override
    public void checkOut(File dir) throws Exception {
        Git gitRepo = Git.cloneRepository().setURI(getUrl()) // remote 주소
            .setDirectory(dir) // 다운받을 로컬의 위치
            .setNoCheckout(true)//
            .setCredentialsProvider(getCp()) // 인증 정보
            .call();
        gitRepo.checkout().setStartPoint(getHash()) // origin/branch_name
            .setAllPaths(true)
            .call();
        gitRepo.getRepository().close();
    }
}

 

Make GitManager Class

이제 위에서 만들어놓은 JGitUtil을 직접적으로 컨트롤 해 줄 GitManager Class를 만들 차례이다. 이 GitManager 객체는 추후 Main method를 가지고 있는 Gradle Project의 Main Class인  BuilderManager Class에서 컨트롤 된다. 이 Class 또한 Method의 기본 정의는 Interface에서 진행한다.

package mingyu.interfaces;

public interface GitManagerInterface {
    
    // 실제로 git에서 local로 source를 download 받게 하는 method 
    public abstract void sourceCodeDownload(String dateTime, String sourceDirectoryPath);
    
}

이제 GitManager Class를 만들어 보자. 위의 인터페이스의 sourceCodeDownload를 Override하여 실제로 구현하고, 생성자에서 JGitUtil 객체를 생성 및 내부 변수들을 초기화하는 것 까지 진행한다.

 

해당 생성자는 4개의 필수 값을 입력받아서 JGitUtil을 사용하기 좀 더 쉽게 초기화한다. 생성자의 값들은 Builder를 사용하는 사용자가 jar명령어를 사용할 때 원하는 git repository url, 원하는 branch 이름을 properties로 넘겨받는데 그걸 토대로 사용한다.

 

본 Builer의 목적은 누구나 어떤 언어에도 종속받지 않고 Source를 실제 서비스 환경처럼 Build하여 dockerImage 혹은 Tgz파일로 만드는 것에 있기 때문이다.  Exception은 Log4J2를 사용하여 Log로 쌓아 에러 관리에 용이하게 하는 습관을 들이고 있다.

package mingyu.classes;

import mingyu.interfaces.GitManagerInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import mingyu.utiles.JgitUtil;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import java.io.File;

public class GitManager implements GitManagerInterface {

    private Logger logger;
    JgitUtil jgitUtil;

    public GitManager(String gitUserId, String gitUserPassword,
                      String gitBranchName, String gitRepositoryURL) {

        logger = LoggerFactory.getLogger("GitManagerLogger");

        jgitUtil = JgitUtil.getInstance();
        jgitUtil.setUrl(gitRepositoryURL);
        jgitUtil.setUserId(gitUserId);
        jgitUtil.setUserPass(gitUserPassword);
        jgitUtil.setCp(new UsernamePasswordCredentialsProvider(jgitUtil.getUserId(), jgitUtil.getUserPass()));
        jgitUtil.setHash("origin/" + gitBranchName);
        jgitUtil.setBranchName(gitBranchName);
    }

    @Override
    public void sourceCodeDownload(String dateTime, String sourceDirectoryPath) {
        try {
            jgitUtil.setLocalPath(sourceDirectoryPath);
            File dir = new File(sourceDirectoryPath);

            jgitUtil.checkOut(dir);
            Git git = jgitUtil.open(dir);
            git.checkout();
            jgitUtil.remoteAdd(git);
            jgitUtil.pull(git);
        } catch (Exception exception) {
            System.out.println(exception.getMessage());
            logger.error("sourceCodeDownloadError :: ", exception.getMessage());
        }
    }

}

 

Make BuilderManager (Main) Class

이제 위에서 만들어놓은 GitManager 및 앞으로 만들 모든 Manager Class의 기능을 컨트롤 할 Main Class인 BuilderManager Class를 만든다. 내용은 간단하다. 나는 최근 테스트용도로 만들어놓은 "https://gitlab.com/mingyukim/project1.git" 프로젝트를 예시로 사용했다. 해당 프로젝트는 Laravel Framework로 만들었기 때문에 나는 Build시 사용할 도커 이미지를 내가 공부하며 만들어놓은 mingyu94/php8.1:0.5(php 8.1 및 버전에 맞는 자주 사용하는 php 확장 프로그램을 미리 설치해서 이미지로 만들어 개인 docker hub에 사전에 배포해 놓은 이미지) 를 사용할 것이다.

package mingyu.classes;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public class BuilderManager {

    private Logger logger;
    DockerManager dockerManager;
    GitManager gitManager;

    public BuilderManager(String gitRepositoryURL, String dockerRepositoryImage, String gitCommitBranch) {
        logger = LoggerFactory.getLogger("BuilderManagerLogger");
        dockerManager = new DockerManager(dockerRepositoryImage, gitRepositoryURL);
        gitManager = new GitManager("your-git-id", "your-git-passoword", gitCommitBranch, gitRepositoryURL);
    }

    public static void main(String[] args) throws Exception {
        // args[0] : "git repository url" when use git pull command
        // args[1] : "dockerRepo/dockerImage:tag" when use build
        // args[2] : CI_COMMIT_BRANCH

        String gitRepositoryURL; // = args[0];
        String dockerRepositoryImage; // = args[1];
        String gitCommitBranch; // = args[2];


        if (args == null || args.length < 3) {
            // git source를 내려받을 repo url
            gitRepositoryURL = "https://gitlab.com/mingyukim/project1.git";
            // 빌드 할 때 사용할 도커이미지
            dockerRepositoryImage = "mingyu94/php8.1:0.5";
            // git repo에서 내려받을 원하는 branch 이름
            gitCommitBranch = "main";
        } else {
            gitRepositoryURL = args[0];
            dockerRepositoryImage = args[1];
            gitCommitBranch = args[2];
        }

        BuilderManager builderManager = new BuilderManager(gitRepositoryURL, dockerRepositoryImage, gitCommitBranch);

        // 해당 날짜 지정
        DateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
        String dateTime = dateFormat.format(new Date());
        // 현재 gradle 최상위 폴더/tmp/build/source/yyyyMMddHHmmss
        String sourceDirectoryPath = "./tmp/build/source/" + dateTime;

        // 최신 source code 다운로드
        builderManager.gitManager.sourceCodeDownload(dateTime, sourceDirectoryPath);

    }
}

 

결론

위에서 설정한 git repository url("https://gitlab.com/mingyukim/project1.git")에 있는 원하는 브랜치 ("main")의 source들을 원하는 폴더 ("_GRADLE_PATH_/tmp/build/source/yyyymmddhhiiss/")에 잘 다운로드 된 것을 확인할 수 있다.

이제 저 녀석을 원하는 도커 이미지를 사용하여 Build하는 과정 (PHP의 경우 composer install , php artisan route&cache&config:clear,cp .env.example .env 등)을 거쳐 DockerImage 혹은 Tgz(압축)파일로 만드는 등 여러가지 작업을 구현해 볼 예정이다.