Gradle Custom Plugin Task를 활용한 Java Enum -> TypeScript Enum 생성(2/2)

YouMakeMeSmile·2021년 12월 22일
1

이전 글(Gradle Custom Plugin Task를 활용한 Java Enum -> TypeScript Enum 생성(1/2))에서는 현재 프로젝트에서 과거의 공통 코드 테이블을 사용하였던 부분을 Enum으로 사용하기 위해서 결정하였던 부분에 대한 이야기를 다루었다.

이번글에서는 Enum을 사용시 문제가 발생할 수 있는 Back-End <-> Front-End 간의 동기화 문제를 최소화 하기 위한 Java Enum으로 부터TypeScript Enum을 생성하는 Gradle Plugin 개발했던 경험을 이야기를 작성하려고 한다.


우선 Gradle 자체도 잘 모르는 상태에서 Gradel Plugin를 개발해야 하는 상태였다.사실 이것뿐만 아니라 알고 개발할 수 있는 작업이 없을것 같긴하다.
나는 먼저 Gradle Docs를 확인하였다. 그런데 예제들이 Groovy, Kotlin으로 작성되어 있었다. 해당 언어들을 공부한적이 없어서 정확한 문법을 알지는 못했지만 가독성이 뛰어난 언어들이여서 어느정도 읽을 수 있었다.


Gradle Plugin를 개발하기 위해서는 build.gradleplginjava-gradle-plugin를 추가하고 gradlePlugin에도 Plugin 기본 정보를 입력해야 한다.

plugins {
    id 'java'
    id 'java-gradle-plugin'
    id 'maven-publish'
}

group 'io.velog.youmakemesmile.plugin'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
    jcenter()
}

publishing {

}

dependencies {
    api 'com.github.javaparser:javaparser-core-serialization:3.23.0'
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
}

gradlePlugin {
    plugins {
        simplePlugin {
            id = 'io.velog.youmakemesmile.plugin'
            implementationClass = 'io.velog.youmakemesmile.plugin.MyPlugin'
        }
    }
}

test {
    useJUnitPlatform()
}

위와 같이 프로젝트를 구성하면 우선은 Gradle Plugin 프로젝트를 개발할 수 있게 된다. gradlePluginimplementationClass는 현재 Plugin에서 개발한 Plugin Task를 맵핑하기 위한 클래스로 Plugin interface의 구현체이다.

package io.velog.youmakemesmile.plugin;

import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class MyPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        project.getTasks().create("generateTypeScriptEnum", GenerateTypeScriptEnum.class);
    }
}

위와 같이 구현한 TaskClass와 해당 Task의 이름을 설정하게 되면 해당 Plugin를 사용하는 프로젝트의 Gradle Task에서 해당 Task가 실행가능 한것을 확인 할 수 있다.


실제로 동작하게될 Task Class를 살펴보려고 한다. 우선 완벽하게 Gradle PluginAPI를 모두 이해하고 구현한 Task가 아니기 때문에 해당 구현 내용을 맹목적으로 맹신한다면 곤란해질 수 있다는 점을 참고해야한다.
이전 글에서 이야기 했던 것처럼 Gradle Custom Tasks에 매우 가독성 뛰어나게 예제가 작성되어있다.

Task 클래스는 기본적으로 DefaultTask를 상속받아 구현하게 된다. DefaultTask에는 Task에서 사용할 수 있는 많은 Project에 정보를 제공하는 필드들이 존재한다. 나는 이를 이용하여 Task 실행시 Project의 실제 src에 접근하여 File를 탐색하는 방식으로 구현하기로 생각하였다.

public class GenerateTypeScriptEnum extends DefaultTask {

    @TaskAction
    public void initDir() throws IOException {
        String generatedPath = getProject().getProjectDir().getPath() + "/.generated/";
        String srcPath = getProject().getProjectDir().getPath() + "/src/";
        ...
    }
}

@TaskAction이 붙어있는 MethodTask 실행시 호출되며 나는 TypeScript가 생성될 디렉토리와 현재 Java 프로젝트의 소스 루트 경로를 선언하였다.


    @TaskAction
    public void initDir() throws IOException {

        String generatedPath = getProject().getProjectDir().getPath() + "/.generated/";
        String srcPath = getProject().getProjectDir().getPath() + "/src/";

        if (Files.exists(Paths.get(generatedPath))) {
            Files.walk(Paths.get(generatedPath))
                    .filter(Files::isRegularFile)
                    .map(Path::toFile)
                    .forEach(File::delete);
        }
        ...
    }
    

현재 구현 방식은 로컬에서 Java 프로젝트의 Gradle Task를 통해 TypeScript Enum을 생성하여 TypeScript 프로젝트에 해당 ts파일을 수동으로 이동 시킨후 해당 프로젝트의 CI/CD를 통해 배포하는 프로세스이다. 그렇기 때문에 기존에 생성된 TypeScript Enum이 존재 할 수 있기 때문에 생성전에 생성 디렉토리의 파일을 모두 삭제했다.


나는 해당 프로젝트의 소스 경로의 파일을 직접 읽는 방식으로 개발을 진행할 예정이였기 때문에 해당 Java을 찾고 이를 분석하여 TypeScript Enum으로 생성해야 했다. Java 파일을 분석하기 위해 사용한 것은 이전 프로젝트에서 사용한 경험이 있는 JavaParser를 통해 File를 읽어 분석하기로 생각했다.

    @TaskAction
    public void initDir() throws IOException {
		...
        if (Files.exists(Paths.get(generatedPath))) {
            Files.walk(Paths.get(generatedPath))
                    .filter(Files::isRegularFile)
                    .map(Path::toFile)
                    .forEach(File::delete);
        }
        Files.walk(Paths.get(srcPath))
                .filter(Files::isRegularFile)
                .filter(value -> value.toString().endsWith("java"))
                .forEach(file -> {
                    try {
                        CompilationUnit c = StaticJavaParser.parse(file);
                        if (c.getTypes().size() > 0 && c.getType(0) instanceof EnumDeclaration && c.getPackageDeclaration().isPresent()) {
                            EnumDeclaration enumDeclaration = (EnumDeclaration) c.getType(0);
                            String packagee = c.getPackageDeclaration().get().getName().asString();
                            if ((packagee.startsWith("io.velog.youmakemesmile"))) {
                                StringBuilder enumSb = new StringBuilder();
                                enumSb.append(String.format("enum %s {\n", enumDeclaration.getNameAsString()));

                                StringBuilder labelSb = new StringBuilder();
                                labelSb.append(String.format("const %sLabel = new Map<%s, string>([\n", enumDeclaration.getNameAsString(), enumDeclaration.getNameAsString()));
                                enumDeclaration.getEntries()
                                        .forEach(value -> {
                                            enumSb.append(String.format("\t %s = '%s', // code:%s, value:%s\n", value.getNameAsString(), value.getNameAsString(), value.getArgument(0).toString(), value.getArgument(1).toString()));
                                            labelSb.append(String.format("\t[%s.%s, '%s'],\n", enumDeclaration.getNameAsString(), value.getNameAsString(), value.getArgument(1).toString().replace("\"", "")));
                                        });
                                enumSb.append("}\n");
                                enumSb.append("\n");
                                labelSb.append("]);\n");
                                labelSb.append("\n");

                                StringBuilder exportSb = new StringBuilder();
                                exportSb.append("export {\n");
                                exportSb.append(String.format("\t%s,\n", enumDeclaration.getNameAsString()));
                                exportSb.append(String.format("\t%sLabel,\n", enumDeclaration.getNameAsString()));
                                exportSb.append("};\n");
                                PackageDeclaration packageDeclaration = c.getPackageDeclaration().get();
                                c.getPackageDeclaration().get().getNameAsString().split(".");
                                Files.createDirectories(Paths.get(generatedPath + packageDeclaration.getName().getIdentifier()));
                                Files.writeString(Paths.get(generatedPath + packageDeclaration.getName().getIdentifier() + "/" + enumDeclaration.getNameAsString() + ".ts"), enumSb.append(labelSb).append(exportSb), StandardOpenOption.CREATE);
                            }

                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                });

우선 소스 루트경로에서 부터 java로 끝나는 파일에 해당하는 파일만을 대상으로 JavaParser를 이용하여 분석을 진행하였다.
기본적으로 해당 Java 파일이 Enum를 구현한 파일인지 확인을 한후 특정 package에 있는 Enum만을 대상으로 생성하기 위한 체크를 하였다.
그리고 Java파일 생성의 경우 JavaParser를 이용하여 구조화된 API을 이용하여 생성할 수 있지만 TypeScript의 경우에는 이에 대응되는 라이브러리를 찾지 못하여 StringBuilder로 하나씩 파싱하여 구현하여 파일을 생성하였다.


위의 소스가 실행되면 해당 패키지 하위의 Enum에 해당하는 TypeScript Enum파일이 생성되며 이후 TypeScript에서 해당 EnumExport 하기위한 index.ts 파일도 생성한다.

        File[] a = new File(generatedPath).listFiles();
        for (File folder : a) {
            StringBuilder indexSb = new StringBuilder();
            Arrays.stream(folder.listFiles()).forEach(file -> {
                indexSb.append(String.format("export * from './%s';\n", file.getName().replace(".ts", "")));
            });
            Files.writeString(Paths.get(generatedPath + folder.getName() + "/" + "index.ts"), indexSb, StandardOpenOption.CREATE);
        }

위와 같이 구현한 후 해당 Plugin 프로젝트를 빌드하여 배포한 후 해당 Plugin를 사용할 프로젝트를 생성한후 다음과 같이 설정한다.

- setting.gradle -

pluginManagement {
    repositories {
        mavenLocal()
        mavenCentral()

    }
}

rootProject.name = 'test'
plugins {
    id 'java'
    id 'io.velog.youmakemesmile.plugin' version '1.0-SNAPSHOT'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

나는 로컬에서 테스트를 하기 위해 로컬에 Plugin를 배포하여 Enum을 구성한 프로젝트의 setting.gradlePlugin RepositorymavenLocal()를 추가하였으며 실제 프로젝트에서는 환경에서 사용하는 Maven를 구성하면 된다.
이후 build.gradleplugins에 위에서 구현한 plugin의 의존성을 추가하면 된다.


이렇게 구성한 프로젝트에 다음과 같이 Enum를 생성하면 다음과 같은 구조로 TypeScript가 생성되는 것을 확인할 수 있다.

public enum City {
   Seoul("S", "서울"),
   Busan("B", "부산");


   City(String code, String value) {
       this.code = code;
       this.value = value;
   }

   private String code;
   private String value;

   public String getCode() {
       return code;
   }

   public String getValue() {
       return value;
   }
}


위의 예시는 간단하게 테스트를 위해 Enum를 간단하게 구성하였으며 실제 프로젝트에서는 다양한 필드를 추가하여 사용할 수 있고 위에서 이야기한 것과 같이 Gradle Plugin의 모든 API를 파악하고 구현한 것이 아니기 때문에 더 좋은 방식이 있을 수 있다.

지금까지 서버와 화면간의 공통코드 동기화를 어느 정도 해결하기 위한 이야기를 작성했다. 이렇게 서버 Enum를 통해 TypeScript Enum를 생성하게 되면 어느 정도는 동기화가 된다고 생각하지만 완벽하게 일치할 수 없기 때문에 아직도 공통코드 서버를 사용해야 하는 것이 아닌가 하는 생각이 있다.


Plugin 프로젝트 Git
테스트 프로젝트 Git

profile
어느새 7년차 중니어 백엔드 개발자 입니다.

0개의 댓글