[시즌2] 개인서버 개발/시즌2 설계(완)

AnnotationProcessor를 통한 project 내 ServerInfo Enum 생성

북극곰은콜라 2023. 12. 25. 01:13
반응형

 


개요

중복되는 코드를 줄이기 위해 개인 서버 프로젝트를 gradle을 활용한 subproject 구조로 잡았었다.
새로 생성되는 subproject에서 중복 로직들을 모은 LIB project를 implementation 하여, 중복을 줄이고자 시도 중이다.
문제는, 프로젝트가 추가될 때마다 LIB project의 코드를 추가해 주는 방식은, 목표했던 편의성과는 거리가 있다.
이에 프로젝트 전체를 지속적으로 모니터링하여, 자동으로 최신화하는 방법을 모색했다.
목표는 수동으로 코드의 추가 없이, 신규 프로젝트의 설정 및 properties 등을 스크랩하여 추가할 수 있는 방안을 마련하는 것이다.

 


AnnotationProcessor란

JAVA 5에서 신규 도입된 개념으로
Compile 시점에 JAVA의 소스를 AST(Abstract Syntax tree) 구조로 참조할 수 있으며, 수정 및 생성이 가능하게 해주는 기능이다.
AnnotationProcessor라는 이름처럼, annotation 기반으로 element를 참조한다.
대표적으로 AnnotationProcessor를 활용한 library로 lombok, queryDSL 등이 있다.

Abstract Syntax Tree란

 

AST란 소스코드를 트리구조로 추상화한 것이다.
컴파일러에서 자주 사용되는 자료구조이며, 소스코드를 분석하는 데 사용된다.
javac의 컴파일 단계에서 쓰이는 것은 물론이며, javascript의 lint, formatter등에서도 활용된다.
더 깊게 들어가면 CFG(context-free-grammar), DOM(Document Object Model) 등 여러 개념들이 함께 사용된다. 더 자세한 사항은 다른 포스팅에서 다루도록 하겠다.
이번 포스팅에서 AST는 소스코드를 분석하여 트리구조화 시키는 자료구조 정도로 이해하고 넘어가겠다.

javax.annotation.processing.Processor

AnnotationProcessor에서 제공하는 대표적인 interface 및 abstract class이다.
일반적으로 AbstractProcessor를 구현한다.
필수 구현 method로 process(Set <TypeElement>, RoundEnvironment)가 있다.
compile 시점에 process method가 invoke 된다고 생각해도 무방하다.

RoundEnvironment

annotated element를 받을 수 있는 evvironment이다.
Annotation을 element를 찾아서, code generate 등 작업을 진행할 수 있도록 도와준다.

ProcessingEnvironment

processing을 위한 환경을 제공하는 인터페이스
AbstractProcessor에서 기본적으로 구성해서 제공해 준다.
이번에는 getFiler를 통해 filer를 불러와서 generated에 java 파일을 생성하려 한다.

TypeElement (Element)

Element는 소스코드상의 type, parameter 등 여러 정보를 AST로 분석하여 제공해 준다.
이번 요구사항에서는 크게 활용되지 않기 때문에 생략하겠다.
Java Interface로 추상화해서 제공하기 때문에, 사용법은 비교적 간단하다. (코드 분석 및 제공은 Javac에서 해준다...)

Google AutoService

This is an annotation processor library that helps us with generating Java Service Provider Interface (SPI) configuration files.
...
Google AutoService is an open source code generator tool, developed under the Google Auto project. There are also two other tools besides AutoService: AutoValue and AutoFactory.
The purpose of this library is to save effort and time and, at the same time, to prevent misconfiguration.
간단하게 구글에서 제공하는 AnnotationProcessor 라이브러리로, Java SPI 설정파일, manifest 등 메타파일을 자동으로 생성해 주는 역할을 한다.
해당 라이브러리로 AnnotationProcessor를 구성하기 위한 작업을 간소화하고 안정성을 확보할 수 있다.
물론 주의사항으로, 의존성이 복잡한 구조로 엮이는 경우 의도하지 못한 상황이 발생할 수 있다. 대표적으로 local, release, test 환경의 의존성 구조가 크게 달라지는 경우, 의존성의 순서와 관련하여 특정 processor가 load 하지 못하거나 실패하게 될 수 도 있다.

 


AnnotationProcessor기반 serverInfo 수집 processor 구현

목표

1. compile 시점에 동작
2. Project의 root로부터 하위 모든 file을 full scan
3. 모든 yml file search 및 파싱
4. spring.application.name 및 server.port 속성들을 찾아서 객체화
5. name port 등을 속성으로 가지는 enum java파일 생성
6. 다른 subproject에서 generate 된 enum 파일을 사용하여, 프로젝트 내 모든 serverInfo를 get 할 수 있음

Gradle 구조

Root project 'pbear-root'
\--- Project ':pbear-spring'
     +--- Project ':pbear-spring:pbear-devtool'
     +--- Project ':pbear-spring:pbear-lib'
     \--- Project ':pbear-spring:pbear-sample'

build.gradle (Root Project)

...

dependencies {
	...

    if (project.name != 'pbear-devtool') {
        compileOnly project(':pbear-spring:pbear-devtool')
        annotationProcessor project(':pbear-spring:pbear-devtool')

        if (project.name != 'pbear-lib') {
            implementation project(':pbear-spring:pbear-lib')
        }
    }
}

...
AnnotationProcessor가 동작하는 project인 pbear-devtool 생성
devtool은 annotationProcessor를 구현할 library가 될 것이며 추후 다른 기능을 추가하기 위해 모든 프로젝트에서 의존성을 갖게 한다.
또한 pbear-lib도 모든 project에서 의존성을 가지게 되는 library성 프로젝트이다.

build.gradle (devtool)

version = '0.0.1'

bootJar { enabled = false }

dependencies {
    implementation 'org.yaml:snakeyaml:2.2'
    implementation 'com.google.auto.service:auto-service:1.1.1'
    annotationProcessor 'com.google.auto.service:auto-service:1.1.1'
}
dependencies로
1. yml 파싱을 위한 snakeyaml
2. annotationProcessor meta 생성을 위한 Google의 auto-service
Main Class가 없기 때문에, spring boot plugins의 bootjar는 disable

build.gradle (lib)

...

tasks.named('compileJava') {
    it.dependsOn('clean')
}
지속적으로 serverInfo가 최신화되기 위한 compileJava의 clean 의존성 설정

Processor 구현

Custom Annotation Class

public @interface EnablePBearServerInfoDevtool {
}
적용되는 프로젝트를 특정하기 위한 annotation class

Processor

@SupportedAnnotationTypes("com.pbear.devtool.annotation.EnablePBearServerInfoDevtool")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class)
@SuppressWarnings({"unused"})
public class ServerProperitesProcessor extends AbstractProcessor {
  private static final String SERVER_ENUM_PACKAGE_NAME = "com.pbear.devtool";
  private static final String SERVER_ENUM_CLASS_NAME = "Server";

  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    roundEnv.getElementsAnnotatedWith(EnablePBearServerInfoDevtool.class)
        .stream()
        .filter(element -> element.getKind() == ElementKind.CLASS)
        .findFirst()
        .ifPresent(element -> this.createServerResource((TypeElement) element));

    return true;
  }

  ...
}

기본 구성

@SupportedAnnotationTypes("com.pbear.devtool.annotation.EnablePBearServerInfoDevtool")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
로 target annotation 및 java version 지정

Google AutoService 선언

AbstractProcessor extends

EnablePBearServerInfoDevtool 어노테이션이 하나라도 있으면 실행이 되며 비즈니스로직이 실행된다.

createServerResource()

private void createServerResource(TypeElement typeElement) {
    try {
      System.out.println(typeElement.getQualifiedName());
      File currentDir = new File(System.getProperty("user.dir"));
      File rootDir = this.getRootProjectDirectory(currentDir);
      System.out.println("root dir:" + rootDir.getCanonicalPath());
      List<File> ymlFileList = this.matchChildFileExtension(rootDir);
      List<ServerInfo> serverInfoList = this.getSpringServerInfoList(ymlFileList);
      System.out.println(serverInfoList);
      this.writeServerInfoEnum(serverInfoList);

    } catch (IOException e) {
      throw new RuntimeException(e);
    }
}
1. 실행되는 현 directory를 기준으로 root를 찾는다.
2. root를 기준으로 하위 디렉터리를 순회하며 yml을 찾는다. (nio files 활용)
3. spring.application.name 및 server.port를 찾아서 ServerInfo 생성
4. enum file 작성

 

private void writeServerInfoEnum(final List<ServerInfo> serverInfoList) throws IOException {
    FileObject fileObject = processingEnv
        .getFiler()
        .createSourceFile(SERVER_ENUM_PACKAGE_NAME + "." + SERVER_ENUM_CLASS_NAME);
    try (PrintWriter out = new PrintWriter(fileObject.openWriter())) {
      String tab = "  ";

      // package com.pbear.devtool;
      out.print("package ");
      out.print(SERVER_ENUM_PACKAGE_NAME);
      out.println(";");
      out.println();

      // public enum Server {
      out.print("public enum ");
      out.print(SERVER_ENUM_CLASS_NAME);
      out.println(" {");

      ...
    }
}
AbstartProcessor에서 제공하는 processingEnv를 통해 generated에 sourceFile을 작성했다.
작성은 PrintWriter를 통해 손수 작성...

 


devtool 사용

package com.pbear.lib;

import com.pbear.devtool.annotation.EnablePBearServerInfoDevtool;

@EnablePBearServerInfoDevtool
public class ServerInfoDevtool {
}
간단하게 Annotation을 달아서 사용

compile 후 build/generated/annotationProcessor/java 밑으로 java 파일이 생성된 것을 확인할 수 있다.

 


Next

처음 그린 그림은 zookeeper를 통한 service registry를 사용하는 구성이었다.
Service Registry를 통해 msa구조를 잡으면 cloud 환경에서는 적은 수고로 서버 간 연동 및 오토스케일링을 대응할 수 있다.
하지만 로컬 서버 하나 운영하는 상황에서 이는 상당히 불필요한 작업이다.
1. 개발환경에서 zookeeper에 대한 대응을 해야 한다.
2. 동적으로 서버를 discovery 할 수 있는 장점이, 1 서버 구성에서는 무의미하다.
3. zookeeper를 관리해야 한다.

이에, 이런 부분을 대체하기 위해 AnnotationProcessor를 구현해서 Server 정보를 빌드 시점에 모아서 쓸 수 있도록 시도 중이다.
하지만, 이러면 앞단의 gateway는 서버가 추가될 때마다 다시 빌드해서 배포해야 하는 상황이 발생할 것이다.
이에 대한 해결책은 아직 고민 중이다.

일단 다음은 일단 github에 올라가면 안 되는 secret properties를 배포 시점에 로드하는 구성을 lib에 구현하고자 한다.

 


REFERENCE

https://taes-k.github.io/2021/04/18/java-annotation-processing/
https://github.com/taes-k/sample-annotation-processing
https://www.baeldung.com/java-annotation-processing-builder

 

 

 

반응형