Tuesday, October 6, 2015

Gradle 기반으로 Android Annotation Apt Processor 만들기.

이 포스팅은 Android Annotation Processing Setup using Gradle - Jens Diller을 기반으로 작성하였습니다.

Annotation Processing은 다른 포스팅에서도 설명했듯이 Compile Time에 java에서 annotation 에 대한 스캔과 처리를 하는 것 입니다. 물론 AndroidAnnotations Framework를 사용하면 편리한 기능을 많이 사용할 수 있지만 개발자라면 누구나 이것이 어떻게 동작하고 만들어지는 방법에 대해서 이해를 할 것이라고 생각합니다. 그래서 Android Project에서 Annotation Processor를 빌드하는 방법을 설명하고자 합니다.

1. Android Project 만들기

"Start a new Android Studio project" 을 선택하여 새로운 프로젝트를 생성합니다.

저는 Application Name : Sample , Company Domain : jurano.com , Package name : com.jurano.annotation으로 프로젝트를 진행하도록 하겠습니다. 이 부분은 사용자가 지정하기 나름이니 알아서 하시면 됩니다.

알맞은 Minimum SDK Version을 선택한 뒤 Next

일단 우리에게는 Activity가 필요없으니 "Add No Activity" 선택 하고 Finish를 선택해줍니다. 그러면 프로젝트 생성끝!

2. 새로운 모듈 추가하기.

Annotation Processor를 적용하려면 두개의 Module이 더 필요합니다. 생성할 모듈은 2개로 Annotation API를 정의하는 Java Library와 Annotation Processor 를 정의하는 라이브러리로 나누어지게 됩니다.File → New → New Module... 을 가면 아래와 같은 화면이 뜨게됩니다.

여기서 Java Library를 선택해줍니다.

선택한 뒤에 Library name : api, Java package name : com.jurano.annotation , Java class name : FullScreen 으로 정의한뒤 프로젝트를 생성시켜줍니다.그럼 API에 대한 Java library 생성이 완료된것이며 위와 똑같은 방식으로 Annotation Processor 프로젝트를 생성하여 줍니다.

Library name : compiler, Java package name : com.jurano.annotation , Java class name : AnnotationProcessor


3. Gradle Build 설정하기

Annotation Processor를 사용하기 위해서는 총 4가지의 build.gradle 파일을 수정해야 합니다. 앞으로 상위 build, api, compiler, app 순으로 설정해야 할 정보들에서 설명하도록 하겠습니다.

3.1. Project: Sample build.gradle (부모 Gradle 파일)

상위 build 파일에서 먼저 android-apt 프로세스 사용을 위한 빌드 정보를 입력해주어야 합니다. 기존에 생성되는 코드는 아래와 같이 구성 됩니다. 첫 줄에 작성되 있는 것과 같이 해당 gradle 파일은 Top-level 빌드 파일입니다. 따라서 아래에 있는 build 파일들에 대한 repostiory 는 jcenter이며, 위에 작성되어 있는 buildscritp 부부은 android를 build할때 사용되는 build tool에 대한 version을 나타내게 됩니다.

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

위에 android-apt에 대한 모듈을 dependency에 추가시켜야 하위 gradle 파일에서 해당되는 프로세서를 사용할 수 있습니다. 따라서 아래의 라인을 dependecy에 추가시켜주어야 합니다.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add
    }
}

현재 1.8 버전까지 나와 있으며 해당버전 사용에 있어 문제가 없기 때문에 최신버전을 이용하였습니다. But 1.7 버전 사용시 android processor가 정상적으로 실행되지 않았습니다. 이유는 잘 모르겠으나 1.7 버전은 사용하지 않는것이 좋습니다.(2틀날림...) 해당 프로젝트의 상위 모듈은 위의 한 줄만 추가하면 모두 끝났습니다.

3.2. Api library

api library의 경우에는 java이고 다른 library를 물리지 않고 사용자 정의 Annotation을 정의하는 부분이기 때문에 다른 사항은 필요하지 않습니다. 아래 코드는 아까 java library 생성시 생성된 코드로 다른 것을 추가하거나 제거할 필요는 없습니다.

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

하지만 위에 있는 포스팅에 "One more thing: If you're using Java 8 as your SDK, you need to ensure and tell the compiler to build using Java 7 or you will get the following error during pre-dexing:" 다음과 같은 문구가 존재합니다. 결국 Java 1.8을 사용하고 있다면 1.7을 사용하기 위해서 1.7하도록 하라는 것인데 이 부분에 있어 저는 1.7을 사용하기 때문에 추가하지 않았습니다.

sourceCompatibility = JavaVersion.VERSION_1_7  
targetCompatibility = JavaVersion.VERSION_1_7  

3.3. Compiler library

Compiler library의 경우에는 추가사항이 존재합니다. 먼저 기존 build정보는 아래와 같습니다.

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

위의 정보에서 내가 생성한 library인 api에 대한 사용권한을 추가해야합니다. 따라서 아래의 코드를 추가해줍니다. 아래의 dependency를 추가함으로써 compiler에서 api에서 생성한 annotation에 대한 사용이 가능하게 됩니다. 따라서 무조건 필요한 부분입니다.

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project (':api') // add
}

3.4. App Sample

마지막으로 App에 대한 build정보입니다. 해당 build 파일이 수정할 부분이 가장 많습니다. 먼저 아래는 기본 소스 코드 입니다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.jurano.annotation"
        minSdkVersion 22
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.0'
}

가장먼저 추가해야 하는 것은 android-apt를 사용할 것이라는 정의 입니다. 2번째 라인에 추가되어 있는 부분으로 이를 사용함을 정의합니다. 그리고 dependency 에 내가 사용하는 내 로컬 library인 api와 compiler에 대한 빌드 정보를 아래와 같이 추가해줍니다. 컴파일러 library의 경우에는 apt 를 이용하여 빌드되게 아래 코드와 같이 구성해야 합니다.

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"
    ...

}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.0'
    compile project(':api')
    apt project(':compiler')
}
    

위와 같이 작성하면 모든 build 파일에 대한 설정을 완료하였습니다. Annntation Processor에 대한 동작원리를 이해한다면 충분히 따라오실 수 있으셨을 겁니다.

4. Java Class 생성하기

이제 사용자가 정의한 새로운 Anntation과 그것에 대한 사용법을 보도록 하겠습니다. 이것도 동일하게 api → compiler → app 방식으로 진행하도록 하겠습니다.

4.1. API 클래스 구현

먼저 저희가 장으로 생성한 Class는 아래와같이 구성이 되어있습니다. 하지만 저희는 @interface가 필요 함으로 과감하게 바꿔줍니다. 또한 api라는 package안에 annotation을 만들도록 하겠습니다.

package com.jurano.annotation;

public class FullScreen {
}
package com.jurano.annotation.api;

public @interface FullScreen {
}

또한 타켓이나 용도 지정이 가능해지는데 이부분에 있어서는 이 포스팅을 참고해주시기 바랍니다. [What is java annotation 03] - Creating your Own Annotations?


4.2. Annotation Processor 정의

이제 Annotation에 대한 정의는 위와 같이 하면 끝난것입니다. 이제 다음으로 Annotation Processor를 정의하러 Compiler 모듈로 이동합니다. 기존에 있는 소스코드를 package를 추가하여 아래와 같이 이동시켰습니다. 또한 Annotation Processor를 사용하기 위해서 시작이 되는 class는 AbstractProcessor class를 상속받아야 함으로 변경된 코드는 이를 반영하였습니다.

package com.jurano.annotation;

public class AnnotationProcessor {
}
package com.jurano.annotation.compiler;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.TypeElement;

public class AnnotationProcessor extends AbstractProcessor{
    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

또한 AnnotationProcessor를 실행시키기 위해서는 몇가지 작업이 필요합니다. 따라서 "compiler/src/main/resources/META-INF/services"에 폴더를 생성해주고 "javax.annotation.processing.Processor"파일을 생성해주어야 합니다. 구조가 만들어지면 다음과 같은 프로젝트 형식이 되게 됩니다.

해당 파일의 내부에는 Annotation Processor가 처음 실행되는 class의 이름을 작성해줍니다. 저는 Annotation Processor의 시작점은 AnnotationProcessor로 할것이기 때문에 아래와 같이 작성해주었습니.

com.jurano.annotation.compiler.AnnotationProcessor

위와같이 작성해주고 함수 내부는 클래스를 생성하는 함수를 작성해야 하는데 함수를 작성하는 모듈에 대한 내용은 다른 포스팅을 통하여 설명하겠습니다. 여기서는 간단하게 Annotation이 있을 경우에 특정경로에 파일이 작성되도록 하는 것으로 대체 하겠습니다. 코드는 아래와 같습니다.

package com.jurano.annotation.compiler;

import com.jurano.annotation.api.FullScreen;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

public class AnnotationProcessor extends AbstractProcessor{

    @Override
    public Set getSupportedAnnotationTypes() {
        return Collections.singleton(FullScreen.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
        for(Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(FullScreen.class)){
            TypeElement annotatedClass = (TypeElement) annotatedElement;
            // TODO Code generate
            String annotatedClassName = annotatedClass.getSimpleName().toString();

            try {
                FileOutputStream output = new FileOutputStream("E://log.txt");
                output.write(annotatedClassName.getBytes());
                output.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        return false;
    }
}

4.3. Android Application에서 빌드되는지 확인하기.

마지막으로 Android Application에서 이것이 사용이 가능한 것인지 이제 구현하여 확인해보도록 하겠습니다. 만들어진 App에서 ConfirmFullScreen이라는 class 를 생성하고 Annotation을 위에다가 선언해 줍니다. 코드는 아래와 같습니다.

package com.jurano.annotation;

import com.jurano.annotation.api.FullScreen;

@FullScreen
public class ConfirmFullScreen {
}

빌드를 실행하게 되면 아래와 같이 파일에 텍스트가 작성되게 됩니다. 필요한 기능에 대해서 Java class를 generated 하도록 소스를 작성한다면 자주 반복되는 코드와 같은 기능을 손쉽게 작업을 할 수 있습니다.

소스는 GradleAndroidAnnotationProcessor에서 확인할 수 있습니다. 다음 포스팅은 소스코드 생성에 대해서 다뤄보도록 하겠습니다. 끝

5. 현재문제점

Compiler 부분의 java process가 죽지않아서 매번 프로세스를 강제 종료하고 있습니다. 이부분은 해결해서 다시올리도록하겠습니다.. ㅠ 참고한 프로젝트에서도 compiler가 죽지 않는 문제가 있어서 이 부분은 확인이 필요한 부분입니다.

Monday, September 28, 2015

[How to become an open source contributor?] 2시간만에 컨트리뷰터 되기!

1. 준비되셨나요?

초보도 2시간만에 오픈소스 컨트리뷰터가 될 수 있는가?!
필요한 것: GitHub 계정, 초급 git 사용법, 발사이즈 토익점수, 근성

로직 등 코드에 기여하는 것은 상위 단계의 일이고(뛰기 전에 걸어야 한다는 격언을 생각합니다), 일단 코드 리뷰 수준의 기여를 하도록 합시다.

음 ... 뭐가 있을까 보다가 핫하고 핫한 도커docker를 한 번 보도록 합시다.


2. 오타 찾기

이제부터 오타를 찾을겁니다. 자주 틀리는 영어 표현을 보도록 하죠.
enviroment,
timout,
annotaion,
limitaion,
developement,
11st,
12nd,
2th,
3nd,
pubic,
positoin,
defualt,
seting,
mousenter,
gurantee,
lisence,
milis,
statmeent,
...
등등 자주 틀리는 표현은 많습니다!

근성을 갖고 찾아보도록 합시다. 하지만 쉽지 않을 수도 있습니다... ㅠ_ ㅠ


차...찾았다! 후..후후...후후후후후 인간은 실수의 동물


3. Pull Request(PR)

자, 이제 본격적으로 pull request를 위한 작업을 해보죠.

fork를 뜨고,

clone을 뜨고, (소스트리로 떠보았습니다)


코드 수정을 하기 전에...

웬만한 대형 repo에는 다 Developer Certificate of Origin나 Contributor License Agreement 같은 시스템이 있고, 컨트리뷰트 가이드가 있습니다. 이건 약관에 동의하는 거라고 생각하면 이해가 쉽습니다.
가이드대로 해야 한다! 중요하죠... 그렇지 않은 경우 merge를 할 수 없다는 메시지를 받게 될 겁니다 ^_^ 이슈로 다루길 원하는 repo도 있고, 주석 수정 등 작은 오타는 바로 PR해주길 바라는 repo도 있고, 이것은 해당 repo의 분위기(?)에 따라 다릅니다.

가이드대로 다음처럼 해 봅시다.




다음과 같이 오타를 수정 ^_^! timeout으로!

근데 이 때 주의해야 할 점! indentation이나 new line 타입, encoding을 깨서는 안된다는 것!
얌전하게 고칠것만 고칩시다. (쓰지맙시다 윈도우즈 메모장...ㅠㅠ)


이제 다시 원본 docker/docker repo로 가서,

New pull request에서 compare across forks,

fork한 repo를 선택하고,

create pull request를 합니다. 잡아먹지 않는답니다요! 아닐수도

영어는 못하지만 언어는 의사소통이라 하였더라. 애써 손짓발짓을 해봅니다.

그리고 지릅니다.


4. Pull Request 그리고 나서...

PR 날렸더니 ... 오잉 이게 모람. GordonTheTurtle 아저씨가 친절하게 뭔갈 알려줬다. 이대로 해봅시다.
아 ... 모든 커밋 메시지에 저게 들어가야 한다는 거구나;;
우...우렁거북이세여 ;; (사람이 아니고 봇같아요 거북이봇.)

흔한 거북의 예

아아 친절해 ㅜㅜ 집앞 맥도날드 알바분보다 더 ㅜㅜ




푸시하려면 빌드테스트를 하고 하는 것이 마땅하거늘, 요즘엔 travis-ci나 jenkins같은 빌드시스템이 이것을 관리해줍니다! 키야 주긴다잉


기다립니다.

오 사람이 나타났습니다!


vendor는 import된거기때문에 합치지는 않을거니까 docker/libkv로 가보랍니다. 귀여운 이모티콘과 함께..
무를 뺐으면 칼이라도 썰어야지!
가자!
갑니다.

똑같이 (이제는 amend를 빼먹지 말고... 적용된 모습이 보입니까? 물론 이건 repo 정책에 따라 다릅니다!)


빌드 성공!


또 기다립니다.

주의: 자다가 일어났는데 메일이 왔으면 설렐 수 있습니다.

아앗 머지됐어

드디어 메일이 왔습니다! LGTM(Looks good to me)!!!


이러면 저도 14명의 컨트리뷰터 중 한 명!(;;)
(물론 전 아직도 잡아먹히지 않았습니다.)

위와 관련하여 좋은 말씀이 있어 나누고자 합니다.(?)
Naver Labs 이형규님의 "Contributing to Docker": http://www.slideshare.net/HyeongKyuLee/docker-contribution-slideshare-38684955


5. 마치며

참 쉽죠??
이러한 주석 수정 등의 활동이 오픈 소스 활동으로 인정받을 수 있느냐라는 논란이 제기되곤 합니다. 그리고 위는 뭔가 꼼수를 부린 것 같이 찝찝한 느낌이 들 수도 있다고 생각합니다.
하지만 천 리 길도 한 걸음부터라고 하듯, 사소한 움직임이 변화의 시작!입니다. (게다가 대부분의 커미터들은 좋아합니다. 물론 예를 갖추어 ... 커미터는 컨트리뷰터를 무조건 좋아해!는 도끼병입니다)
이런 소소한 활동이 세상의 소프트웨어와 여러분들의 일상에 변화를 줄 수 있기를 바랍니다. ^_^

김태희, http://github.com/KimTaehee


Monday, September 21, 2015

[Unit testing] 2. Play JUnit~!

JUnit을 시작해보자 ^_^

* 개발환경: OS X Yosemite / Android Studio 1.3.2

1. 프로젝트 생성

기본 옵션으로 Blank Activity 프로젝트를 생성한다.

2. Unit Test 폴더 구조 생성

Junit 테스트의 경우, src/test/java를 기본 테스트 폴더로 인식한다. 그렇기 때문에 폴더명을 수정해준다~! 마우스 우클릭 > Refactor > Rename > "androidTest"를 "test"로 수정

* 테스트 폴더가 안보인다면 좌측 상단을 Project로 선택해주면 된다

3. 빌드 스크립트 수정

app의 build.gradle 파일을 열어서 dependency에 testCompile 구문을 추가해준다. 그 다음, Sync now 클릭!

4. Test Artifact 설정

좌측 하단 Build Variant 탭을 클릭한 후 Test Artifact를 Unit Tests로 설정해 준다. 그러면 아래와 같이 폴더 아이콘이 초록색으로 변경된다~!

여기까지 성공했다면, 환경설정 끝!

5. Unit Test 코드 작성

정상적으로 작동하는지 알아보기 위해 간단히 Calculator 클래스를 만들어서 두 정수를 더하는 메소드를 테스트 해보자 ^_^! 아래와 같이 Calculator랑 CalculatorTest 클래스를 생성하고 코드를 써준다~~

클래스 생성해주고

테스트 코드를 작성해준다! ^_^!

6. 테스트 하기~~

테스트 java 폴더에서 우클릭하여 Run 'All Tests' 클릭하여 테스트 해준당!

끄읏~~~~~

덧.

내 환경에서는 위와 같이 했을 때 다음과 같이 fail이 2개가 생겼다 ㅜ_ㅜ

로그 내용
java.lang.RuntimeException: Method setUp in android.test.ApplicationTestCase not mocked. See http://g.co/androidstudio/not-mocked for details. at android.test.ApplicationTestCase.setUp(ApplicationTestCase.java) at org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:86) at org.junit.runners.Suite.runChild(Suite.java:128) at org.junit.runners.Suite.runChild(Suite.java:27) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
java.lang.RuntimeException: Method setUp in android.test.ApplicationTestCase not mocked. See http://g.co/androidstudio/not-mocked for details. at android.test.ApplicationTestCase.setUp(ApplicationTestCase.java) at org.junit.internal.runners.JUnit38ClassRunner.run(JUnit38ClassRunner.java:86) at org.junit.runners.Suite.runChild(Suite.java:128) at org.junit.runners.Suite.runChild(Suite.java:27) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) at org.junit.runners.ParentRunner.run(ParentRunner.java:363) at org.junit.runner.JUnitCore.run(JUnitCore.java:137) at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:78) at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:212) at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:68) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

이럴 경우 app의 build.gradle 스크립트에 다음과 같이 testOption을 추가해주면 된다.

Sync now를 해주고 다시 Run'All Test'를 하면!

All Tests Passed! 진짜 끝~~~~~~^_^

다음 글에서는 robolectric에 대해 알아보자!

참고 논문 및 사이트

1. TDD/JUnit 조금 더 알기 http://www.slideshare.net/WonchangSong1/abouttddj-unit

2. Android Studio에서 Junit 기반 단위 테스트(Unit Test) 환경 구축하기 http://xinics.tistory.com/101#recentTrackback

3. Android Developer https://developer.android.com/training/testing/unit-testing/local-unit-tests.html

4. Android Tools Project Site http://tools.android.com/tech-docs/unit-testing-support