이전 글(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.gradle
의 plgin
에 java-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
프로젝트를 개발할 수 있게 된다. gradlePlugin
의 implementationClass
는 현재 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);
}
}
위와 같이 구현한 Task
의 Class
와 해당 Task
의 이름을 설정하게 되면 해당 Plugin
를 사용하는 프로젝트의 Gradle Task
에서 해당 Task
가 실행가능 한것을 확인 할 수 있다.
실제로 동작하게될 Task Class
를 살펴보려고 한다. 우선 완벽하게 Gradle Plugin
의 API
를 모두 이해하고 구현한 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
이 붙어있는 Method
가 Task
실행시 호출되며 나는 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
에서 해당 Enum
을 Export
하기위한 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.gradle
에 Plugin Repository
에 mavenLocal()
를 추가하였으며 실제 프로젝트에서는 환경에서 사용하는 Maven
를 구성하면 된다.
이후 build.gradle
의 plugins
에 위에서 구현한 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
를 생성하게 되면 어느 정도는 동기화가 된다고 생각하지만 완벽하게 일치할 수 없기 때문에 아직도 공통코드 서버를 사용해야 하는 것이 아닌가 하는 생각이 있다.