KotlinPoet usages

먼저 이 글은 아래의 글을 참조로 하였다.

KotlinPoet を使って Android プロジェクトでコードジェネレーションする (KotlinPoet를 사용해 Android 프로젝트의 코드 제네레이션을 한다)

はじめて KotlinPoet を使ったときのメモ (처음 KotlinPoet를 사용했을 때의 메모)

문자열, Literal, 타입 출력

만약 아래의 코드를 작성하고 싶다고 해보자.

Log.d("TAG", "Something new!");

그러면, 아래의 코드를 사용할 수도 있다.

val tag = "TAG"
val message = "Something new!"

builder.addCode("android.util.Log.d(\"$tag\", \"$message\");")

아니면, 아래의 코드를 사용할 수도 있다.

val tag = "TAG"
val message = "Something new!"

builder.addStatement("%T.d(%S, %S)", ClassName.bestGuess("android.util", "Log"), tag, message);

당연히 후자가 마음에 든다.

이 처럼, %S, %L, %T 등을 이용하여 표현식을 작성할 수 있다.

  • %T: 타입에 대한 format, 주로 ClassName 객체를 반환한다. ClassName.bestGuess(packageName, simpleName) 로 해당 패키지 이름과 클래스 이름을 가진 클래스를 찾아낸다. 여기서는 android.util 클래스의 Log를 찾으라고 했으니, android.util.Log 가 검출된다. 그리고, 이 bestGuess로 찾아낸 타입들은 알아서 import문이 추가된다.
  • %S: 문자열에 대한 format, 해당 문자열에 quota를 더해서 출력한다.
  • %L: 리터럴에 대한 format, 해당 문자열을 그대로 출력한다.

Static by @JvmStatic

Static 를 하고자 하는 메서드의 생성 Spec(FunSpec) 에 .addAnnotation(JvmStatic::class) 를 추가하면 된다.

Property의 생성 및 초기화

val parameterName = "person"
val typeName = ClassName.bestGuess("pyxis.uzuki.live.demo", "Person);

PropertySpec.builder(parameterName, typeName, KModifier.PRIVATE).initializer("Person()").build()

builder 의 3번째 파라미터는 KModifier 의 가변 인자(vararg) 로, Modifier 를 여러 개 붙일 수 있다.

Property의 val/var (mutable)

val parameterName = "person"
val typeName = ClassName.bestGuess("pyxis.uzuki.live.demo", "Person);

PropertySpec.builder(parameterName, typeName, KModifier.PRIVATE).initializer("Person()").mutable(true).build()

간단하게 mutable(true) 만 추가해주면 된다.

Function의 파라미터

val parameterName = "person"
val typeName =ClassName.bestGuess("pyxis.uzuki.live.demo", "Person)

FunSpec.builder("apply")
   .addModifier(KModifier.PUBLIC)
   .addParameter(parameterName, typeName)

해당 FunSpec의 builder 에 addParameter 메서드를 사용하면 된다.

if 조건문

this.beginControlFlow("if ($variableName.length == 0)")
            .addCode(String.format("%s = %s;\n", variableName, defValue))
            .endControlFlow()

중요한건 beginControlFlow 등은 중괄호만 생성해준다는 것이다. 따라서 if 나 for 는 저렇게 하드코딩 하거나 위의 %L, %S 등을 이용해 적절하게 표시해야 한다.

Primitive 처리

Int, Double 등을 처리할 때 Primitive 는 ClassName 로 처리할 수 없다는 오류를 발생시킬 수 있는데, 아래 메서드를 사용하면 된다.

val ANY = ClassName("kotlin", "Any")
val ARRAY = ClassName("kotlin", "Array")
val UNIT = Unit::class.asClassName()
val BOOLEAN = ClassName("kotlin", "Boolean")
val BYTE = ClassName("kotlin", "Byte")
val SHORT = ClassName("kotlin", "Short")
val INT = ClassName("kotlin", "Int")
val LONG = ClassName("kotlin", "Long")
val CHAR = ClassName("kotlin", "Char")
val FLOAT = ClassName("kotlin", "Float")
val DOUBLE = ClassName("kotlin", "Double")
val STRING = ClassName("kotlin", "String")

fun String.bestGuess(): TypeName {
    return when (this) {
        "int" -> INT
        "byte" -> BYTE
        "short" -> SHORT
        "long" -> LONG
        "char" -> CHAR
        "float" -> FLOAT
        "double" -> DOUBLE
        "String" -> STRING
        "java.lang.String" -> STRING
        "boolean" -> BOOLEAN
        "Unit" -> UNIT
        "Any" -> ANY
        else -> ClassName.bestGuess(this)
    }
}

 

Generate Kotlin Code with KotlinPoet uses Annotation Processor

도입

Annotation Processor (주석 처리기) 는 컴파일 시점에 특정 어노테이션을 기반으로 하여 클래스를 생성하고, 메서드를 생성하고, 필드를 생성하는 기술로 JDK 1.5 부터 도입되었다.

그 외 설명은 아래 글을 참고하는게 더 빠를지도 모른다.

(번역) Annotaiton Processing 101

Annotation Processing : Don’t Repeat Yourself, Generate Your Code.

이미 Glide, Dagger, ButterKnife, Data-binding 등이 이 Annotation Processor 기능을 사용해서 코드를 생성하고 하여금 개발자가 모든 코드를 직접 작성하지 않아도 자동으로 생성하는 기능을 가지고 있다.

현재는 주로 자바로 자바의 코드를 생성하는 방법이 주로 쓰이고 있지만, 오늘은 Kotlin을 이용해 Kotlin 코드를 작성하는 법을 작성해보려 한다.

본 프로젝트의 예제로 사용된 프로젝트는 WindSekirun/AttributeParser 이다.

프로젝트 모듈 설정

총 3개의 모듈이 필요하다.

Annotation을 담을 annotation 모듈, APK 파일 내에 포함되지 않고 컴파일 시간에 실행되는 complier 모듈, 그리고 이 두 개를 의존해서 사용하는 demo 모듈이다.

annotation 모듈, complier 모듈은 android-library 플러그인이 아닌 kotlin 플러그인을 사용한다.

annotation 모듈의 build.gradle

apply plugin: 'java'
apply plugin: 'kotlin'

targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

complier 모듈의 build.gradle

apply plugin: 'kotlin'

targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    compile project(':attribute-parser')
    compile 'com.squareup:kotlinpoet:0.7.0'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
}

어노테이션 제작

간단하게 Class에 부착해 해당 클래스의 이름을 가져와 그 이름을 기반으로 클래스를 만들어보자.

package pyxis.uzuki.live.attribute.parser.annotation

@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE)
@Retention(AnnotationRetention.SOURCE)
annotation class CustomView

Target 는 Class 및 File, 어노테이션 실행 시점은 SOURCE로 고정한다.

이 어노테이션에는 별도의 필드는 필요 없기에, 두지 않는다.

프로세서 제작

complier 모듈에 kt 파일을 하나 생성해보고 AbstractProcessor 를 상속하면 된다.

package pyxis.uzuki.live.attribute.parser.compiler

import com.google.auto.service.AutoService
import pyxis.uzuki.live.attribute.parser.compiler.utils.supportedAnnotationSet
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedSourceVersion
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor::class)
class Processor : AbstractProcessor() {
    override fun getSupportedAnnotationTypes() = supportedAnnotationSet

    override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {
        // TODO: implement something

        return true
    }

}

하나씩 살펴보면..

  • @SupportedSourceVersion: JDK 컴파일 타겟 버전. 여기서는 RELEASE_8
  • @AutoService(Processor::class): META-INF/services/javax.annotation.processing.Processor 를 생성해주는 구글에서 개발한 어노테이션… 이나 현재 시간 기준으로 먹히지 않아 별도로 생성해줘야 함.
  • getSupportedAnnotationTypes() = supportAnnotationSet: supportAnnotationSet 는 별도 파일에 정의된 Property로, Set<String> 를 반환한다.
val supportedAnnotationSet = classNameSetOf(CustomView::class)

fun <T : KClass<*>> classNameSetOf(vararg elements: T) = elements.map { it.java.name }.toHashSet()

이런 식으로 정의하면 된다.

  • process: 실제로 processor 의 내용이 구현될 곳

RoundEnvironment 는 getSupportedAnnotationTypes 에서 선언한 각종 대상 어노테이션을 사용중인 Element (구조 요소) 를 반환하는 역할을 한다.

주로 사용되는 메서드는 RoundedEnvironment.getElementsAnnotatedWith(Class<*>) 인데, 이름대로 해당 어노테이션 클래스를 사용하는 element 의 리스트를 반환한다.

for (element in env?.getElementsAnnotatedWith(CustomView::class.java)!!) {
            val typeElement = element as TypeElement
            val className = typeElement.asClassName()
            className.canonicalName
            className.packageName()
            className.simpleName()
        }

element 는 VariableElement, TypeElement 등이 있는데 여기서는 TypeElement 로 cast를 시켜서 ClassName 객체를 구해내게 한다.

여기서 ClassName 는 Kotlin 코드를 좀 더 쉽게 작성할 수 있게 도와주는 KotlinPoet 의 클래스이다. packageName, simpleName 등을 파싱할 수 있게 한다.

META-INF 생성

원래라면 AutoService 가 생성해야 하지만 18년 3월 17일 기준으로는 자동 생성되지 않는다.

src/main/resources/META-INF/services/javax.annotation.processing.Processor 라는 파일을 만들고, 내부에 구동할 Annotation Processor 의 풀네임을 적는다.

pyxis.uzuki.live.attribute.parser.compiler.AttributeParserProcessor

클래스 생성

ClassName 를 가지고 클래스를 생성해보자.

private fun writeAttributes(simpleName: String) {
        val fileName = "${simpleName}Attributes"
        val builder = TypeSpec.objectBuilder(fileName)
        val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
                ?.replace("kaptKotlin", "kapt")
                ?.let { File(it, "$fileName.kt") }
                ?: throw IllegalArgumentException("No output directory")

        val typeSpec = builder.build()
        val fileSpec = FileSpec.builder("", fileName).addType(typeSpec).build()
        fileSpec.writeTo(kaptKotlinGeneratedDir)
}

companion object {
        const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}

맨 첫번째 줄부터 읽어보면,

  1. ${simpleName}Attributes -> 클래스 이름이 StyleView 이면 StyleViewAttributes
  2. builder = TypeSpec.objectBuilder(fileName) -> fileName 란 이름을 가진 object 생성 (object StyleViewAttributes)
  3. kaptKotlinGeneratedDir: Kotlin 에서 annotation processor 를 처리하는 kapt 는 기본적으로 build/generated/source/kaptKotlin/… 에 생성되기 때문에 IDE에서 못 찾는다. 따라서 kapt.kotlin.generated 란 키로 찾은 processingEnv의 출력 경로 옵션에서 kaptKotlin 을 kapt로 교체, File 객체로 만든다. 만일 옵션이 없을 경우에는 IllegalArgumentException 을 발생시킨다.
  4. val typeSpec = builder.build() -> TypeSpec 객체를 생성한다.
  5. val fileSpec = FileSpec.builder(“”, fileName).addType(typeSpec).build() -> 파일을 생성하는 Spec 객체를 생성한다.
  6. fileSpec.writeTo -> 주어진 경로에 fileSpec 를 작성한다.

이 순서가 된다.

이 코드를 넣고, 데모에서 annotation 모듈과 complier 모듈을 의존한 후 어떤 특정 클래스에 @CustomView 어노테이션을 붙이고 빌드하면 그 클래스의 이름을 가진 Attribute 클래스가 생성될 것이다.

만일 이 클래스에 Property, function 등을 붙이고 싶다면 builder 밑에 작성해주면 된다.

아래는 실제로 AttributeParser 에서 사용중인 코드이다.

private fun writeAttributes(holder: CustomViewHolder) {
        val classTypeName = holder.className
        val classTypeParameterName = holder.simpleName.substring(0, 1).toLowerCase() + holder.simpleName.substring(1)
        val simpleName = holder.simpleName
        val fileName = simpleName + Constants.ATTRIBUTES

        val builder = TypeSpec.objectBuilder(fileName)

        val models = getModelList(simpleName, mAttrBooleanMap, mAttrColorMap, mAttrDimensionMap,
                mAttrIntegerMap, mAttrIntMap, mAttrFractionMap, mAttrFloatMap, mAttrResourceMap, mAttrStringMap)

        for (model in models) {
            builder.addProperty(createAttrsFieldSpec(model))
        }

        builder.addProperty(createRFieldSpec())
        builder.addFunction(createObtainApplyMethodSpec(classTypeName, classTypeParameterName, holder.simpleName))
        builder.addFunction(createApplyMethodSpec(classTypeName, classTypeParameterName, models))
        builder.addFunction(createPrintVariableMethodSpec(simpleName, models))
        builder.addFunction(createBindAttributesMethodSpec(simpleName, models))

        val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
                ?.replace("kaptKotlin", "kapt")
                ?.let { File(it, "$fileName.kt") }
                ?: throw IllegalArgumentException("No output directory")

        val typeSpec = builder.build()
        val fileSpec = FileSpec.builder(holder.packageName, fileName).addType(typeSpec).build()
        fileSpec.writeTo(kaptKotlinGeneratedDir)
 }

마무리

원본 라이브러리인 JavaPoet 와 달리 KotlinPoet 는 문서의 양이 너무 적긴 하지만,  충분히 클래스 파일을 생성할 수 있다.

다음 글에서는 KotlinPoet 의 구체적 사용법 등을 몇 가지 예제로 살펴보려고 한다.

커스텀 뷰의 XML 속성 파싱 라이브러리, AttributeParser 소개

분명히 블로그 조회수 1만 찍으면 mvvm에 관해 올린다고 몇일 전 언급한 적이 있지만…

도입

최근에 앱을 개발하면서 자주 커스텀 뷰를 많이 쓰게 된다.

보통 앱들 보면 타이틀 바 라던가 그런 공통적 요소가 많이 있는데, 한 번만 잘 설계해두면 xml의 속성만을 바꾸는 것으로 쉽게 되니 빠르게 개발하기엔 좋다.

그러나… XML의 속성이 10개 이상 넘어갈 경우에는 무심코 머리를 짚게 된다.

val typedArray = context.obtainStyledAttributes(attrs, R.styleable.CombinedButton) ?: return
textPrimary = typedArray.getString(R.styleable.CombinedButton_textPrimary)
textSecondary = typedArray.getString(R.styleable.CombinedButton_textSecondary)
textPrimaryColor = typedArray.getColor(R.styleable.CombinedButton_textPrimaryColor, Color.BLACK)
textSecondaryColor = typedArray.getColor(R.styleable.CombinedButton_textSecondaryColor, Color.BLACK)
textPrimarySize = typedArray.getDimension(R.styleable.CombinedButton_textPrimarySize, resources.getDimensionPixelSize(R.dimen.combined_text_view_default_size).toFloat())
textSecondarySize = typedArray.getDimension(R.styleable.CombinedButton_textSecondarySize, resources.getDimensionPixelSize(R.dimen.combined_text_view_default_size).toFloat())
textExtraSpace = typedArray.getInt(R.styleable.CombinedButton_textExtraSpace, 1)
fontPrimaryText = typedArray.getString(R.styleable.CombinedButton_fontPrimaryText)
fontSecondaryText = typedArray.getString(R.styleable.CombinedButton_fontSecondaryText)
textPrimaryStyle = typedArray.getInt(R.styleable.CombinedButton_textPrimaryStyle, 0)
textSecondaryStyle = typedArray.getInt(R.styleable.CombinedButton_textSecondaryStyle, 0)

아마 1월 31일 쯔음에 Reflection 을 이용해서 비슷한 걸 만든 기억이 있지만, 오히려 기존에 했던 것과 그렇게 차이가 나지 않아 안 쓰게 되었다.

그러면, 좀 더 자동화되는 라이브러리를 만들어서 혼자 잘 쓰자! 라고 생각해서 심야 영화를 본 뒤 카페에서 공부하면서 작업한 결과, 오늘 소개한 AttributeParser 가 완성되게 되었다.

이론 기술

처음으로 Reflection 기반의 Annotation Processing 가 아닌, Annotation Processor 를 통한 Annotation Processing 기반으로 작성되었다. 가볍게 쓰고 + private 같은거를 싹다 무시하고 개발하기에는 Reflection가 최고이지만 각각 장단점이 있길 마련이다.

florent37의 Dagger-Auto-inject 를 포크해서 기능 추가할 때 살짝 Annotation Processor 를 공부한 적은 있지만, 처음부터 설계하고 작성하기는 처음이다.

간단히 Annotation Processor 에 대해 설명하자면, 컴파일 시간에 대상 어노테이션을 긁어 모아 클래스 파일을 생성하고, 그걸 사용하는 개념이다. Dagger, DataBinding, ButterKnife, Glide, PermissionDispatcher 등 이미 많은 라이브러리에서 사용되고 있고, 런타임 시간이 아닌 컴파일 시간에 실행되 오버헤드가 없고, 런타임에 포함되는 코드가 적거나 없고, 실제로 생성된 코드를 보며 디버깅 하기 쉽다는 등 장점이 많다.

그러면 왜 지금까지 Reflection만을 사용했는가.. 하면, annotation processor 는 컴파일 시간때 클래스 파일을 생성하므로 JavaPoet 같은라이브러리를 사용하여 자바 코드를 직접 작성해야 한다. 보통 코드를 짤 때 IDE의 지원을 받아서 실제로 키보드로 치는 코드 양은 적으나 annotation processor 를 위해 작성하는 코드는 IDE의 지원을 일절 받을 수 없는 맨땅의 코드라 처음 입문하기가 상당히 어렵다. 시간도 많이 들어가기도 하고. 그에 대비해 Reflection 는 상대적으로 빠른 시간 안에 기능을 구현할 수 있어 Reflection 을 적극적으로 사용하게 된 것이다.

그러다가 실제로 작성해보고 싶기도 했고, 자동화의 끝판왕은 자동 코드 파일 생성이라고 굳게 믿고 있기 때문에 Annotation Processor로 구현하기로 마음을 먹은 것이다.

설계

1월 31일날 만든 AttrParser 의 단점을 먼저 살펴보자.

맨 위의 TypeArray.getString() 을 반복하지 않아도 된다는 장점은 있지만…

  1. 쓸데없이 annotation 을 하나로 통합해서 type를 계속 적어줘야 한다.
  2. 같은 이유로 default value를 설정할 때, boolean 이나 float 를 int형으로 바꿔서 하고 있어 매우 헷갈린다
  3. Index를 수동으로 적어줘야 한다.

라는 단점이 있다. 개인적으로 느낀거지만 이전과 다를 바가 없다고 느꼈다.

그래서, 이번 AttributeParser 는 아래 사항을 주안점으로 삼았다.

  1. Annotation Processor 를 이용한 코드 파일 생성
  2. 각 type에 따른 Annotation 분리
  3. 자동 로그 출력 기능 (타입, 변수명, 실제 값까지)

Annotation Processor를 사용하기 위해서는 총 2개의 모듈이 필요하다. 하나는 annotation 이나 기타 클래스를 담은 일반 라이브러리와 annotation 모듈을 의존하는 compiler 모듈이다. 그리고 데모 앱에서는 이 두개 모듈을 각각 implementation, annotationProcessor (또는 kapt) 로 구성할 필요가 있다.

어노테이션들은 다음같이 구성했다.

package pyxis.uzuki.live.attribute.parser.annotation

@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class AttrInt(val value: String = "", val defValue: Int = 0)

총 5줄 정도의 아주 간단한 어노테이션이다.

대충 잡은 설계로는, CustomView 어노테이션으로 커스텀 뷰 단위를 나누고, 그 안에서 Attr 어노테이션의 위치와 필드명, 타입을 찾는다. 그리고 서로 매핑하여 데이터를 가공하고,  CustomView 어노테이션이 붙은 클래스를 기반으로 하는 커스텀 클래스를 만든다. 그 안에서 각각의 값을 가지고 있기 위한 field들과 적용하기 위한 메서드들이 생성될 것이다.

컴파일러 모듈은 조금 더 많은데, 각각 어노테이션을 매핑하기 위한 모델이나 홀더, 그리고 유틸과 상수값들을 정의하고, AttributeParserProcessor 란 클래스에서 이 클래스들을 사용하여 생성하는 역할을 한다.

AttributeParserProcessor 에서는 처음으로 지정된 어노테이션이 어느 위치에 있는지 전부 찾아서 매핑한 다음, CustomView 어노테이션의 갯수만큼 반복을 돌려서 안에 있는 Attr 어노테이션 등을 파싱하는 것이다.

Reflection 과 다르게 Annotation Processor 는 런타임 코드에 바로 주입될 수 없으므로 파싱한 결과는 우선 생성된 클래스의 private static 필드로서 가지고 있고, 사용자가 apply 메서드 등을 부르면 해당 값에 설정하는 역할을 할 것이다.

이런 과정을 거쳐서 생성된 클래스가 바로 밑의 클래스이다.

package pyxis.uzuki.live.attribute.parser;

import android.content.res.TypedArray;
import android.util.AttributeSet;
import java.lang.String;
import pyxis.uzuki.live.attribute.parser.demo.R;
import pyxis.uzuki.live.attribute.parser.demo.StyleView;

public class StyleViewAttributes {
  private static boolean booleanTest;

  private static int colorTest;

  private static float dimensionTest;

  private static int intTest;

  private static float floatTest;

  private static int resourceTest;

  private static String stringTest;

  private R r;

  public static void apply(StyleView styleView, AttributeSet set) {
    apply(styleView, styleView.getContext().obtainStyledAttributes(set, R.styleable.StyleView));
  }

  public static void apply(StyleView styleView, TypedArray array) {
    bindAttributes(array);

    styleView.booleanTest = booleanTest;
    styleView.colorTest = colorTest;
    styleView.dimensionTest = dimensionTest;
    styleView.intTest = intTest;
    styleView.floatTest = floatTest;
    styleView.resourceTest = resourceTest;
    styleView.stringTest = stringTest;
  }

  public static void printVariables() {
    android.util.Log.d("StyleView", "==================== StyleView ====================" + 
    "\nboolean booleanTest = " + booleanTest +  
    "\nint colorTest = " + colorTest +  
    "\nfloat dimensionTest = " + dimensionTest +  
    "\nint intTest = " + intTest +  
    "\nfloat floatTest = " + floatTest +  
    "\nint resourceTest = " + resourceTest +  
    "\njava.lang.String stringTest = " + stringTest +  
    "\n====================================================");
  }

  private static void bindAttributes(TypedArray array) {
    if (array == null) return;

    booleanTest = array.getBoolean(R.styleable.StyleView_booleanTest, false);
    colorTest = array.getColor(R.styleable.StyleView_colorTest, 0);
    dimensionTest = array.getDimension(R.styleable.StyleView_dimensionTest, 0.0f);
    intTest = array.getInt(R.styleable.StyleView_intTest, 0);
    floatTest = array.getFloat(R.styleable.StyleView_floatTest, 0.0f);
    resourceTest = array.getResourceId(R.styleable.StyleView_resourceTest, 0);
    stringTest = array.getString(R.styleable.StyleView_stringTest);

    array.recycle();
  }
}

물론, 사용자가 위의 클래스를 신경 쓸 필요는 전혀 없다. 단순히 apply 에 필요한 파라미터만 주입해주면 값이 설정되는 구조이다.

마무리

평소에 그나마 공부를 해왔던 덕분에, 실제로 만드는 시간은 약 7시간 정도 걸린 것 같다.

실제로 써보니 Reflection 보다 강력한 부분도 있었고, 좀 안 좋은 부분도 있었지만 설계만 잘 하면 오래토록 쓸 수 있을 것 같다.

마지막으로, 오늘 만든 라이브러리는 당연하겠지만 Github에 업로드 되어있다.

WindSekirun/AttributeParser

bintray 에 업로드도 요청했으니, 아마 오늘 밤쯤에는 올라가지 않을까 싶다.