0. 적용하게된 계기
회사에서 신규프로젝트 관련해서 스프링 프로젝트 환경세팅을 진행하게 되었는데이것저것 세팅하고 마무리 작업 중 GET 요청시 파라미터 받는부분을 어떻게 해야할까에 대한 이야기가 나왔다.
클라이언트에서는 스네이크 케이스로, 서버에서 사용하는 프로퍼티명은 카멜 케이스로 만들어 작업하다보니
POST 요청의경우 request body로 들어오는 명칭이 동일하지 않은 데이터에대해
property-naming-strategy: ?
위와같은 스프링부트 프로퍼티 설정으로 RequestResponseBodyMethodProcessor 아규먼트 리졸버에서 사용되는 MappingJackson2HttpMessageConverter안의 ObjectMapper의 네이밍전략을 원하는대로 설정해 전체반영되게끔 할 수 있지만..
GET 요청 파라미터를 DTO 클래스로 받게되면 ServletModelAttributeMethodProcessor 아규먼트 리졸버를 타게되고 내부적으로 파라미터명과 이름이 동일한 DTO 클래스내의 변수를 찾아 바인딩 시켜주기 때문에 별도의 작업이 필요했다.
물론 해결법이야 여러가지가 있다. 필터로 처리할수도 있고 유틸메소드로 빼서 컨트롤러단에서 처리를 할수도 있지만 이는 추가적인 로직으로 불필요한 일을 추가하는 느낌을 개인적으로는 지울수가 없었다.. 반면 프로세서는 단순히 컴파일시점에 스네이크 케이스용 setter메소드를 만들고 기존 아규먼트 리졸버에서 처리되게 하는데에서 그치기 때문에 만들어 놓으면 개발이 간편하고 더 효율적이라고 생각이 들었다.
하지만 어노테이션 프로세서를 만들려면 몇가지 알고 가야하는 부분들이 있었고 종종 개발이 막히는 부분이 생길경우 레퍼런스가 많지 않아 돌파하는게 쉽지는 않았다.. 어노테이션 프로세서를 만들려면 무엇을 알아야 했을까?
1. AST(Abstract Syntax Tree)
첫번째로는 AST가 있다. 컴파일러는 컴파일할때 AST라고 하는 소스 코드의 추상 구문 구조의 트리를 생성하게 되며 각 트리의 노드는 코드에서 발생되는 구조를 나타낸다. 이 단계에서 컴파일러는 구문분석을 진행하며 이때문에 이따가 나올 코드를 보면 접근제어자 부터 메소드 바디, 반환타입 등 모든걸 구조화해 직접 구문분석단계에서 컴파일러가 재구성된 구조를 가지고 진행하게끔 만들어줘야한다.
한번 더 말하자면 기계가 이해할 수 있는 언어로 컴파일 하는 과정 중에 AST를 기반으로 구문분석을 진행하고 다음 단계로 넘어가기 때문에 직접 AST를 재구성하면 마치 코드가 어노테이션만 붙여도 자동으로 생성되는것처럼 보일수 있게 만들 수 있다는것
개발시 자주쓰는 롬복같은 경우에도 컴파일 과정에서 생성되는 AST를 재구성하게끔 처리가 되어있어 어노테이션만 붙여줘도 실제 컴파일된 파일에는 코드가 생성되어있다.
참고 :
Back to the Essence - Java 컴파일에서 실행까지 - (1)
Back to the Essence - Java 컴파일에서 실행까지 - (1)Java 11 JVM 스펙을 기준으로 Java 소스 코드가 어떻게 컴파일되고 실행되는지 살짝 깊게 알아보자. 이번엔 1탄 컴파일 편이다. 2탄 실행 편은 여기에..
homoefficio.github.io
https://www.geeksforgeeks.org/compilation-execution-java-program/
Compilation and Execution of a Java Program - GeeksforGeeks
A Computer Science portal for geeks. It contains well written, well thought and well explained computer science and programming articles, quizzes and practice/competitive programming/company interview Questions.
www.geeksforgeeks.org
2. JAVA 어노테이션 프로세서
두번째로는 자바에서 어노테이션 프로세서를 어떤 형태로 만들어야하고 어떻게 적용시키고 있는건지를 알아야했다.
이번에 작업하면서 만든 커스텀 어노테이션 프로세서 소스를 보면서 이야기 해보자
<커스텀 어노테이션 프로세서>
@AutoService(Processor.class)
@SupportedAnnotationTypes("processor.SnakeSetter")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class SnakeSetterProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
final JavacProcessingEnvironment javacProcessingEnvironment = (JavacProcessingEnvironment) processingEnv;
Context context = javacProcessingEnvironment.getContext();
TreeMaker treeMaker = TreeMaker.instance(context);
Names names = Names.instance(context);
Trees trees = Trees.instance(processingEnv);
Consumer<JCTree.JCClassDecl> jcClassDeclConsumer = jcClassDecl -> {
List<JCTree> members = jcClassDecl.getMembers();
for(JCTree member : members) {
if (member instanceof JCTree.JCVariableDecl) {
JCTree.JCVariableDecl castedMember = (JCTree.JCVariableDecl) member;
JCTree.JCVariableDecl param = treeMaker.Param(names.fromString("_" + castedMember.name.toString()), castedMember.vartype.type, null);
JCTree.JCMethodDecl setter = treeMaker.MethodDef(
treeMaker.Modifiers(1),
names.fromString(makeSetterName(castedMember.name.toString())),
treeMaker.TypeIdent(TypeTag.VOID),
com.sun.tools.javac.util.List.nil(),
com.sun.tools.javac.util.List.of(param),
com.sun.tools.javac.util.List.nil(),
treeMaker.Block(0,
com.sun.tools.javac.util.List.of(treeMaker.Exec(treeMaker.Assign(treeMaker.Ident(castedMember), treeMaker.Ident(param))))
),
null);
jcClassDecl.defs = jcClassDecl.defs.prepend(setter);
}
}
};
TreePathScanner<Object, CompilationUnitTree> scanner = new TreePathScanner<>() {
@Override
public Trees visitClass(ClassTree classTree, CompilationUnitTree unitTree) {
JCTree.JCCompilationUnit compilationUnit = (JCTree.JCCompilationUnit) unitTree;
if (compilationUnit.sourcefile.getKind() == JavaFileObject.Kind.SOURCE) {
compilationUnit.accept(new TreeTranslator() {
@Override
public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
super.visitClassDef(jcClassDecl);
jcClassDeclConsumer.accept(jcClassDecl);
}
});
}
return trees;
}
};
for(Element element : roundEnvironment.getElementsAnnotatedWith(SnakeSetter.class)) {
final TreePath path = trees.getPath(element);
scanner.scan(path, path.getCompilationUnit());
}
return true;
}
private String makeSetterName(String fieldName) {
return "set" + Character.toUpperCase(fieldName.charAt(0)) + makeSnakeCase(fieldName).substring(1);
}
private String makeSnakeCase(String fieldName) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < fieldName.length(); i++) {
char ch = fieldName.charAt(i);
if (Character.isUpperCase(ch)) {
stringBuilder.append('_');
stringBuilder.append(Character.toLowerCase(ch));
} else {
stringBuilder.append(ch);
}
}
return stringBuilder.toString();
}
}
<커스텀 어노테이션>
package processor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface SnakeSetter {
}
위 소스는 참고란에 있는 블로그에 나와있는 내용을 기반으로 만들었으며 필요한 부분만 일부 수정하는것으로 진행했다. 먼저 setter를 만들 클래스에 선언할 @SnakeSetter라는 어노테이션을 하나 만들었고 최종적으로 어노테이션 프로세서는 @SnakeSetter 이 달려있는 클래스의 멤버변수를 순회하며 setSnake_case 와 같은 이름의 setter를 생성하게 된다.
1. 어노테이션 프로세서는 기본적으로 AbstractProcessor를 상속하여 구현해야한다.
2. 프로세서에 선언된 @AutoService는 구글에서 만든 라이브러리의 결과물인데 프로세서를 등록하는 번거로운 과정을 생략할 수 있게 도와준다. 번거로운 과정이란 META-INF 폴더 하위에 파일을 생성하여 프로세서를 기입하는 과정인데 @AutoService를 달면 컴파일 후 결과물에 자동으로 생기게 된다.
3. @SupportedAnnotationTypes 과 @SupportedSourceVersion은 이 프로세서가 어떤 자바버젼의 어떤 어노테이션에 대해 지원하는지 메타정보를 나타내는 어노테이션이다. 이 부분은 AbstractProcessor에 각 항목과 관련된 메소드가 있는데 이 메소드를 오버라이딩해서 작업하는것도 가능하다.
4. 그 뒤로 나오는 소스들은 AST를 직접 구성하여 컴파일 시점에 코드가 생성될 수 있게끔 만들어주는 구현부이다. 워낙 이해하기 난해하고 이후 다시 볼 기회가 많지 않을거라 생각해 필요한 부분만 수정하여 사용했고 크게 시간을 쓰고싶지 않았지만.. setter 메소드를 만들때 파라미터명을 필드명과 동일하게 하니 실제 컴파일된 파일에서
public void setCustom_variable(int customVariable) {
this.customVariable = customVariable;
}
위 항목중 this. 키워드가 빠져 제대로 동작하지 않았었다.. 이것저것 해보고 많은 레퍼런스를 뒤져봤지만 나오는 내용은 없어 현타가 오던 중 혹시 하는 마음에 파라미터명에 _ 를 붙이니 정상적으로 setter가 만들어졌다.
이유를 찾고싶었지만 워낙 구현부는 레퍼런스가 적고 lombok 소스코드를 하나하나 쫓아가기에는 시간이 없어 우선 패스했다.
5. 구현부는 무조건 process 메소드를 상속하여 사용해야 하며 return 값으로 true를 넘길경우 다른 어노테이션 프로세서가 더이상 처리하지 않게 된다.
참고 :
https://www.happykoo.net/@happykoo/posts/255
해피쿠 블로그 - [Java] Lombok은 어떻게 동작할까? - AST, 애노테이션 프로세서(2)
누구나 손쉽게 운영하는 블로그!
www.happykoo.net
3. 프로세서 컴파일하기
어노테이션 프로세서도 결국 자바 소스이고 컴파일 해야 사용할 수 있는것은 당연하지만 별다른 설정없이 컴파일하면 오류를 만날 수 있다.
이를 방지하기 위해 인텔리제이용, gradle용으로 옵션을 추가하였는데 다음과 같다.
<compiler.xml>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="processor.main" options="--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED" />
</option>
</component>
인텔리제이는 인텔리제이 메타정보 폴더안의 compiler.xml 하위에 위 마크업을 추가하면 된다.
<build.gradle>
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.0'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'com.google.auto.service:auto-service:1.0-rc5'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc5'
}
compileJava {
options.compilerArgs.add('--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED')
options.compilerArgs.add('--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED')
options.compilerArgs.add('--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED')
options.compilerArgs.add('--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED')
options.compilerArgs.add('--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED')
}
4. 커스텀 어노테이션 프로세서 적용하기
커스텀 어노테이션 프로세서 관련해 서칭을 해보면 거의 대부분의 예시가 다중 모듈 프로젝트로 구성해서 진행한것으로 보이는데 우선 지금 적용하려는 프로젝트가 다중 모듈이 아닐 뿐더러 나중에 다른 프로젝트에도 쓰일 수 있지 않을까 싶어 별도로 프로젝트를 만들어 진행했다.
3번에서 컴파일 옵션 추가 후 빌드하면 나오는 jar 파일을 가져와 적용하고자 하는 프로젝트에 의존성을 추가하면 쉽게 사용할 수 있다.
implementation files("libs/processor-1.0-SNAPSHOT.jar")
annotationProcessor files("libs/processor-1.0-SNAPSHOT.jar")
** java compile option에 --add-exports flag는 자바 11 이상에서만 쓸 수 있는것으로 보여 내용 추가
SonarQube Could not create the Java Virtual Machine Unrecognized option: --add-exports=java.base/jdk.internal.ref=ALL-UNNAMED
Trying to use SonarQube, Community edition, for my first experience with SonarQube. I have just now downloaded it, and am following instructions to start a local instance. I am running Java v10.0.2...
stackoverflow.com
'개발' 카테고리의 다른 글
| [k8s] 쿠버네티스 노드 Not Ready 분석 및 해결하기 (0) | 2022.03.03 |
|---|---|
| [k8s] 쿠버네티스 컨테이너 런타임 변경하기 (0) | 2022.03.02 |
| [k8s] could not find a JWS signature in the cluster-info ConfigMap for token ID - 쿠버네티스 join 에러 (0) | 2022.02.28 |
| [k8s] 로컬환경에서 쿠버네티스 구축하기 (0) | 2022.02.21 |
| 오라클 VirtualBox로 centOS 환경세팅 하기 (0) | 2022.02.21 |