뭐? apk가 윈도우폰에서 돈다고? Project Astoria에 대해

이 글은 마이크로소프트 개발자 행사인 Build 2015의 “PROJECT ASTORIA”: Build Great Windows Apps with Your Android Code를 본 후기입니다.

제가 마이크로소프트를 과소평가했다는 생각이 들더군요.

위의 동영상을 보기 전에 풍문으로만 들리던 “윈도우폰이 안드로이드앱을 돌린다”에 대해서 저는 상식적으로 이렇게 생각했습니다… “apk를 appx로 전환하는게 있을거다”, “gradle 프로젝트를 읽어서 비주얼스튜더오 솔루션(일명 sln파일)으로 바꿔주는 project import 기능을 잘 만들었을거다”, “백버튼, 키보드 등의 이벤트를 interop시키는 wrapper일거다” 등등…

하지만 실체는 아래와 같습니다.

  • apk를 윈도우폰 스토어에 올릴 수 있습니다.
  • Android Studio (또는 IntelliJ)로 코딩합니다.
  • gradle에 추가된 몇 줄의 build 설정만으로 윈도우폰용으로 컴파일이 됩니다.
  • 가장 놀라운거: adb로 윈도우폰 에뮬레이터에 접속되고 앱을 디버깅합니다.
  • 맥에서도 동일한 개발 경험을 가집니다.

가장 궁금했던 점은, Build에서 “android subsystem을 넣었다”고 하는 말이었는데, 영상 마지막에 adb로 윈도우폰의 logcat이 나오는 것을 보면서 정말 짜릿함마저 느껴졌습니다.

“저걸 했단말야?”…입으로야 뭐든 못하겠냐고 농담했던걸 진짜로 하고 있네요.
이제 주요 화면의 영상 캡처와 제가 덧붙인 설명을 나열하겠습니다.

분석을 위한 APK 업로드
분석을 위한 APK 업로드
apk분석이 끝나면 아이콘을 리소스에서 뽑아주고 사용 라이브러리를 분석해서 아래 추가적으로 해 줄 것을 정리해줍니다.
apk분석이 끝나면 아이콘을 리소스에서 뽑아주고 사용 라이브러리를 분석해서 아래 추가적으로 해 줄 것을 정리해줍니다.

아래 정리된 것은 때론 Visual Studio 2008에서 Visual Studio 2012로 마이그레이션하는 것보다 더 수월해보이는 것도 있어보입니다.

gradle에 별도 빌드시스템으로 컴파일시키는데 영특한 어프로치입니다.
gradle에 별도 빌드시스템으로 컴파일시키는데 영특한 어프로치입니다.

이 부분이 참 재미난데, windowsCompile이라고 별도의 빌드툴로 만드는 것으로 아마도 이것은 android SDK의 Build Tool이 향상됨에 따라 같이 버전이 올라갈 것으로 예상됩니다. 역할은 과거 전형적인 COM interop, JNI 등이 하는 것처럼 인터페이스 핸들러를 연결해주는 것으로 예상됩니다. 이런 작업은 MS가 워낙 노하우가 있고 잘하던거라서 뭐 잘했겠거니 싶네요.

맥에서 윈도우폰 에뮬레이터가 동작합니다
맥에서 윈도우폰 에뮬레이터가 동작합니다

VisualStudio를 고집하지 않고 기존 안드로이드IDE를 그대로 활용하기에 맥에서도 그대로 프로그래밍이 가능하고 심지어 데모에서는 맥에서 윈도우폰 에뮬레이터를 동작시켜서 테스트합니다. 에뮬레이터 프로그램 이름이 xda.exe이고 아래 VMWare Fusion 아이콘이 있는거로 봐서 에뮬레이터는 버추얼머쉰으로 돌리나봅니다.

윈도우스토어 업로드도 apk로
윈도우스토어 업로드도 apk로

결과물은 apk로 나오고 윈도우스토어는 이 apk 파일을 그대로 받습니다. 스토어에 업로드가 완료되면 apk 검증 페이지가 나오므로 windowsCompile을 거친게 아닌 아무 apk를 넣는다고 되지는 않을 것 같습니다. 통상 윈도우앱을 올릴 때 WACK(Windows App Certification Kit)이 실행되는데 그것은 스토어 서버에서 수행되는걸로 보입니다.

아키텍처
아키텍처

위의 그림에서 보듯 apk를 Windows App Model인 appx가 콘테이너로 만들어서 처리합니다. appx는 윈도우폰이 받는 메시지, 다른 앱과의 연동, 키보드 등 시스템이벤트 처리 등을 하겠지요. 이렇게 하면 보통 성능이 느릴거라 생각하지만 그건 실제로 나와봐야 알 것 같습니다. 온전한 에뮬레이션은 아닌거 같으니(별도로 컴파일시키는게 성능상의 이유도 있겠죠) 구동속도는 별 차이가 없을 것 같다는 생각도 듭니다.

윈도우 스토어앱은 적다고 맨날 까였지만, 이정도까지 해줬으면 충분히 개발자를 배려했다는 생각이 듭니다. 불과 3년 전이었어도 불과 옛날의 MS였어도 이건 있을 수 없는 일이죠. 윈도우10 폰은 여러모로 기대가 됩니다. 매번 기대했지만 제발 이번만은 쫌…!

Advertisements

R.plurals는 단순한 if문이 아니다

안드로이드 리소스 중 plurals는 단어의 단수복수형을 처리해주는 리소스 타입입니다.

활용할 수 있는 대표적인 예가 1 friend, 2 friends가 있지요.

리소스는 아래와 같이 작성합니다.

    <plurals name="profile_label_friendsCount">
        <item quantity="one">%d friend</item>
        <item quantity="other">%d friends</item>
    </plurals>

이 리소스를 활용할 자바코드는 아래와 같겠죠.

    if (friendCount > 0)
        message = getResources().getQuantityString(R.plurals.profile_label_friendsCount, friendCount, friendCount);
    textViewFriendCount.setText(message);

지원되는 quantity는 총 6개입니다 (zero, one, two, few, many, other).
여기까지만 보면, “아! 그럼 0일 때 no friend라고 나오게 하려면 아래처럼 하면 되겠군”이라고 생각합니다.

    <plurals name="profile_label_friendsCount">
        <item quantity="zero">no friend</item> <!-- 추가 -->
        <item quantity="one">%d friend</item>
        <item quantity="other">%d friends</item>
    </plurals>

하지만 en-US 기준으로 “no friend”가 아닌 “0 friends”라고 나오는걸 알 수 있습니다.

하지만 이것은 버그가 아니랍니다 (code.google.com issue링크). 애초에 그렇게 만들어졌다는거죠. plurals라는 리소스 처리는, 언어마다의 숫자에 대한 표현이 문법적으로 다를 경우만 처리하는 것이지 if문의 대용으로 사용되는게 아닌거죠. 이는 API문서에도 주의를 주고 있습니다.

In English, a string for zero will be ignored even if the quantity is 0, because 0 isn’t grammatically different from 2, or any other number except 1 (“zero books”, “one book”, “two books”, and so on). Conversely, in Korean only the other string will ever be used.

위에 글을 읽어보면 영어는 one만 다르게 판단된다는군요. 한국어는 other만 취급되고요.

한편으론, 이건 무슨 기준으로 이런걸까요? StackOverflow의 한 답변에 의하면 이는 Unicode Common Locale Data Repository (일명 CLDR)라는 체계를 따르는 겁니다. 논리의 개념이 아닌 인간의 언어에 대한 개념으로 만들어진 것이지요. 이것은 오픈소스로 운영되고 있으며 근래 모던한 소프트웨어는 이 체계를 많이 따르고 있습니다.

어쨌거나 결론은, “No Friend”처리를 해야 할 때 우리는 아래처럼 평범한 if 문으로 처리해야 합니다.

    if (friendCount > 0)
        message = getResources().getQuantityString(R.plurals.profile_label_friendsCount, friendCount, friendCount);
    else // 친구 없을 때
        message = getString(R.string.profile_label_noFriend); // "no friend" string 표시
    textViewFriendCount.setText(message);

Espresso로 액티비티 내비게이션 테스트하기

어떤 액티비티에서 백버튼 눌렀을 때 원하는 액티비티로 연결되었는지 테스트할 때를 가정합니다. 때로는 다른 앱에서, 때로는 푸시에서 실행되는 액티비티일 것입니다.

이것을 테스트할 때 일일이 푸시 보내보고 누르고 하는건 어쩐지 원시적입니다. 이전에 작성된 Espresso를 이용하여 이것을 테스트해봅시다.

Espresso 셋업부터 구동까지를 알아보려면 기존 글 (Espresso로 안드로이드 UI테스트하기)을 참고해주세요. 정말 성실히 썼으니까 Espresso를 안써보셨으면 꼭 읽어보세요.

방법은 아주 간단합니다.
그저 그 액티비티를 실행시키고 다시 백눌러서 원하는 것이 떴는지 검사할 뿐입니다. 다만 QA팀이 하기엔 너무나 사소해서 미안해지는 것을 코드로 만들면 좋을겁니다.

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity_> {

    private MainActivity_ mActivity;

    public MainActivityTest(){
        super(MainActivity_.class);
    }
    @Before
    public void setUp() throws Exception {
        super.setUp();
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mActivity = getActivity();
    }

        // 답변글을 본 후 백버튼 누르면 그 답변에 대한 질문글이 보여야 한다.
    @Test
    public void navigateReplyViewToQuestionViewActivity(){
        Intent intent = new Intent(mActivity, ReplyViewActivity_.class);
        intent.putExtra(CommonValue.REPLY_ID, 632558); // 답변 ID를 지정.
        mActivity.startActivityForResult(intent, CommonValue.REQUEST_QUESTION_SELECT); // 액티비티 실행
        onView(withText(R.string.viewReply)).check(ViewAssertions.matches(isDisplayed()));  // 답변뷰의 요소가 나왔는지 검사.
        pressBack(); // 백버튼 누른다.
        onView(withId(R.id.relativeLayoutQuestionViewRoot)).check(ViewAssertions.matches(isDisplayed())); // 질문뷰의 요소가 나왔는지 검사.
    }
}

테스트는 navigateReplyViewToQuestionView 메소드이며 각 코드의 주요 부위에는 코멘트를 달았습니다. 액티비티를 띄우고, 백버튼 누르는 것을 모사합니다. 그리고 나오는 액티비티에 원하는 액티비티에만 있는 요소가 있는지 검사합니다.

위의 코드에서 navigateReplyViewToQuestionView 메소드에 우클릭하여 Run하면 실행됩니다. 뭘 어떻게 Run하는지 모르신다면 위에 언급한대로 이전 소개글5.실행을 읽어주세요.

점점 코드로 테스트하는 것이 많아질 수록 마음에 평화가 찾아옵니다.
테스트는 사랑입니다. 끝~

Espresso로 안드로이드 UI테스트하기

서론

Espresso는 2014년말 2.0 릴리즈부터 Android Support Library에 통합되어 제공하는 UI 테스트 프레임워크입니다. 사용자의 조작을 코드로 구현하여 재생하고 그에 대한 변화를 검사하는데 사용됩니다. 그래서 기본적인 click()외에도 swipeRight() 같은 것이 구비되어 있습니다.

이런 시도는 다른 플랫폼에도 있는데 마이크로소프트는 2010년부터 이를 Coded-UI Test라고 부르고 앱부터 웹페이지까지 폭넓은 기능을 제공하고 있습니다. 제가 직접 가서 들었던 강좌인데 웹페이지 테스트에 관심 있으신 분은 당시 녹화된 테크데이즈 세미나를 들을 수 있습니다.

이 글은, 기존에 여러 구버전 관련 설명들이 넘쳐나는 것에 시행착오를 겪으며 결국 글작성 현재 시점 Android Studio 1.1.0 + Espresso 2.0 + Support Library v11 + gradle 설정환경에 맞춘 것입니다. (이 글도 언젠가는 누구에게 혼란을 주는 구버전 글이 되겠지요…)

본론

본론은 다음 순서로 구성되어 있습니다.

  1. 주의사항
  2. gradle 셋업
  3. 패키지 셋업
  4. 테스트 작성
  5. 실행

1. 주의사항

마지막에 적으면 아무도 안읽을거기 때문에 미리 주의사항을 적습니다.

롤리팝에서 하세요

다른 안드로이드 버전에서 하면 Caused by: java.lang.ClassNotFoundException가 발생하는데 원인은 모르겠습니다. 다만 롤리팝에서는 잘됩니다. 안드로이드란 그런겁니다. 테스트 ‘구동’마저도 버전마다 안될 수 있군요. 유사한 문제가 있는 것 같지만 제 경우는 아니며, 버전별 동작 문제까지 해결하고 싶지는 않습니다.

기존 유닛테스트 적용한 경우: TestRunner 단일화

이 후에 나올 gradle 셋업을 할 때, 테스트하는 프로젝트 외에 참조되는 라이브러리에도 TestRunner가 있다면 그곳에도 동일한 TestRunner로 셋업을 해줘야 합니다. 그렇지 않으면 No Instrumentation Registered 에러가 발생합니다. 관련 StackOverflow 링크

즉, Espresso는 AndroidJUnitRunner를 사용하는데 이는 안드로이드 테스트에서 권장하는 InstrumentationTestRunner를 교체해야 한다는 것입니다.

이처럼 기존 유닛테스트를 작성했고 InstrumentationTestRunner를 사용한 분이라면, 기존 안드로이드 테스트 작성에 몇가지를 바꿔줘야 하는데 그렇게 괴롭진 않습니다.

예를 들어 test~ 로 메소드 이름을 지으면 자동으로 테스트 메소드가 되는 naming convension이 없고 전통적인 JUnit 스타일로 @Test라고 명시해야 합니다. 뭐 저는 이걸 더 좋아하긴 했습니다.

기존 코드 (아래)

public class TextTest extends TestCase {
    public void test_show_2087_to_2_1K() { // "test"로 시작하면 naming convension 인식됨.
        int number = 2087;
        assertEquals("2.1K", Text.toReadableBase1000(number));
    }
}

변경된 코드 (아래)

@RunWith(AndroidJUnit4.class) // 추가
@SmallTest // 추가
public class TextTest extends TestCase {
    @Test // 추가
    public void show_2087_to_2_1K() { // naming convension 안먹으로 간소화하자.
        int number = 2087;
        assertEquals("2.1K", Text.toReadableBase1000(number));
    }
}

애니메이션 끄기

개발자옵션에서 아래 그림과 같이 애니메이션을 사용안함으로 하셔야 합니다.
Screenshot_2015-03-07-14-30-06-(1)

애니메이션 끄는 것을 코드로 제어할 수 있다고 하지만 저는 Permission적용에서 실패하는데, 해보시려면 이 링크를 참고하세요. 이런거 한번에 안되면 붙들지 마세요. 우리 인생은 소중합니다.

그럼 이제 본격 셋업에 들어가보겠습니다.

2. gradle 셋업

아래와 같은 코드를 build.gradle에 추가합니다.
다른 프로젝트에 testInstrumentationRunner가 있으면 그곳에도 바꿔줍니다.

dependencies {
    // ...
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0'
    androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
    androidTestCompile 'com.android.support.test.espresso:espresso-contrib:2.0'
}

android {
    defaultConfig {
        minSdkVersion 10
        targetSdkVersion 21
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" // 추가.
    }
    sourceSets {
        packagingOptions {
            //...
            exclude 'META-INF/LICENSE.txt'
            exclude 'LICENSE.txt' // 이걸 꼭 추가!
        }
    }
}

gradle셋업에 대한 더 자세한 내용을 읽고 싶으시면 다음을 참고합니다.
https://code.google.com/p/android-test-kit/wiki/EspressoSetupInstructions

3. 패키지 셋업

Android 테스트는 androidTest라는 폴더를 기본으로 인식하고 아래 그림처럼 Android Studio에서 androidTest/java 부분을 초록색으로 테스트 관련 폴더라고 인식해줍니다.
캡처
위와 같이 java 폴더 이하를 동일한 hierarchy가 되도록 구성하는 것이 가장 보편적인 방법입니다.

4. 테스트 작성

짧은 테스트코드는 공식예제(github)에도 충분히 있으니 좀 더 복잡한 시나리오 하나를 아래 붙입니다.

/**
* 첫화면 테스트.
* Created by Youngjae on 2015-03-02.
*/
@RunWith(AndroidJUnit4.class)
@LargeTest
public class StartActivityTest extends ActivityInstrumentationTestCase2 {
    private StartActivity mActivity;

    public StartActivityTest() { // 생성자 지정 필수
        super(StartActivity.class);
    }
    @Before
    public void setUp() throws Exception {
        super.setUp();
        injectInstrumentation(InstrumentationRegistry.getInstrumentation()); // 필수
        mActivity = getActivity();
    }

    @Test
    public void login_and_listDisplayed(){
        // 로그인 버튼 클릭.
        onView(withId(R.id.buttonLogin)).perform(ViewActions.click());

        // 로그인에 힌트가 보이는지 확인.
        onView(withId(R.id.editTextUserId)).check(ViewAssertions.matches(withHint(R.string.hintEmailOrId)));

        // 로그인 수행
        onView(withId(R.id.editTextUserId)).perform(ViewActions.typeText("tester")); // ID 입력
        onView(withId(R.id.editTextUserPassword)).perform(ViewActions.typeText("qwerty1234")); // 패스워드 입력
        onView(withId(R.id.buttonLogin)).perform(ViewActions.click()); // 로그인 클릭

        // 5초 기다린다. 그 전에 성공하면 넘어감.
        onView(isRoot()).perform(TestExtension.waitId(R.id.customViewPagerMain, 5000));

        // 다음 화면으로 넘어가서 ViewPager가 제대로 나왔는지 확인.
        onView(withId(R.id.customViewPagerMain)).check(ViewAssertions.matches(isDisplayed()));
    }
}

보면, 모든 것이 View 기준으로 동작합니다. View에 해당 R.id가 있는지 검사하고, 클릭하고, 결과도 리소스와 그 visibility로 점검하니 그야말로 UI 테스트입니다.

이 테스트의 실제 동작은 StartActivity 후에 Fragment교체, MainActivity로의 startActivity()를 통한 전환이 일어남에도 onView(...)라는 이름처럼 눈에 보이는 것 기준으로만 테스트하므로 MainActivity, MainFragment 같은 것은 전혀 코드에 없음을 알 수 있습니다.

다시 말해, UI 디자인 뿐만 아니라 UX 흐름이 바뀌어도 핵심 리소스만 그대로 유지하면 UI테스트는 성공하겠지요.

중요: 위의 코드의 waitId()라는 메소드는 Espresso에 없는 것입니다.
이 코드는 주어진 시간동안 루프를 돌면서 해당 R.id가 발견되는지를 체크하므로 네트워크 지연에도 문제없이 대응할 수 있게 해줍니다. 원본은 http://stackoverflow.com/a/22563297/361100 에 있습니다.

참고로, Espresso 명령어 목록은 다음 링크에 한 페이지로 요약되어 있습니다.
https://code.google.com/p/android-test-kit/wiki/EspressoV2CheatSheet

5. 실행

작성한 테스트 메소드 또는 클래스에 우클릭해서 아래 그림과 같이 안드로이드 아이콘의 테스트를 실행합니다. 그 아래 사각형에 화살표있는 JUnit 실행 아이콘을 하면 안드로이드를 거치지 않으므로 에러가 납니다.
Untitled-2

한 번 실행하면 아래 그림처럼 Configuration에 등록되므로 여러번 실행하기에 편해집니다.
캡처

이처럼 작게는 메소드 단위부터 클래스 단위, 프로젝트 단위까지 실행 단위를 만들 수 있습니다.
실행해보면 혼자서 EditText에 타자치고 버튼누르고 재미있습니다.

결론

발전이 빠른 앱개발에서 UI를 테스트한다는건 전문 테스트 인력(직접 유닛테스트 코드를 작성할 수 있는 정도)이 없는 이상 서버 유닛테스트처럼 100% 커버리지를 목표로 하는 것이 불가능합니다. TDD를 적용하기에도 서버처럼 입출력이 명확하고 목적에만 집중할 수 있는게 아니라 각종 꾸밈과 애니메이션 효과도 넣어야 하기 때문이지요.

예를 들어 기존 경고문구가 회색인데 그레이로 바꿔달라고 디자이너가 요청하면 최악의 경우 레이아웃xml+색상파일+테스트코드 모두 수정해야 하기 때문입니다.

43940

저희 경우처럼 로그인/가입/몇가지 입력폼 등 거의 변하지 않는 부분에 조금씩 적용해서 단순한 QA항목은 코드로 대체하고 복잡한 시나리오만 사람이 하게 되면 더 많은 커버리지를 (결국엔) 달성할 것입니다.

끝~

Fragment에서의 ActionBar 조작은 OnCreateView에서 하자

이 글은 Vardhan님의 포스트를 요약한 것입니다.

우리는 보통 ActionBar를 조작할 때 Fragment.OnCreate에 코드를 넣습니다.
아래와 같이 말이죠.

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
                ActionBar actionBar = getActivity().getSupportActionBar();
                actionBar.setTitle("첫화면");
    }

하지만 Fragment와 엮이면서 ActionBar를 Fragment에서 조작할 때는, Fragment.OnCreate에서 조작할 때 Activity NullPointerException이 발생할 수 있습니다.

그 이유는 아주 복잡한 Activity-Fragment 라이프사이클과 관계 때문인데, 그 중 Activity가 destroy 되고 다시 살아날 때를 가정하면 다음의 흐름을 거칩니다.

생성

MainActivity onCreate
          Fragment onAttach
          Fragment onCreate
          Fragment onCreateView
          Fragment onViewCreated
          Fragment onActivityCreated
MainActivity onStart
          Fragment onStart
MainActivity onResume
          Fragment onResume

파괴

          Fragment onStop
MainActivity onStop
          Fragment onDestroyView
          Fragment onDestroy
          Fragment onDetach
MainActivity onDestroy

복원

          Fragment onAttach
          Fragment onCreate // 문제 발생지점
MainActivity onCreate
          Fragment onCreateView
          Fragment onViewCreated
          Fragment onActivityCreated
MainActivity onStart
          Fragment onStart
MainActivity onResume
          Fragment onResume

문제는 마지막의 “복원” 과정에서 발생합니다. Fragment.onCreate를 호출했지만, ActionBar는 Activity.onCreate를 지나야 활성화가 되기 때문에 Null인 상태인겁니다.

참고로, 복원 과정을 쉽게 테스트하려면 개발자 옵션에 “액티비티 유지 안함” 옵션을 켠 후 앱을 구동 → 홈버튼을 눌렀다가 → 다시 앱을 켜면 복원 과정을 재현할 수 있습니다.

뭔가 액티비티가 넘어갈 때 StackOverflowError 발생하면

1년간 안드로이드 프로그래밍을 하면서 StackOverflowError가 난 적은 없었는데 지난 주에 처음 겪었습니다. 대강 아래와 같은 문제가 발생합니다.

04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.content.Intent.writeToParcel(Intent.java:5503)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Parcel.writeParcelable(Parcel.java:1151)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Parcel.writeValue(Parcel.java:1070)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Parcel.writeMapInternal(Parcel.java:488)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Bundle.writeToParcel(Bundle.java:1552)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Parcel.writeBundle(Parcel.java:502)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.content.Intent.writeToParcel(Intent.java:5503)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Parcel.writeParcelable(Parcel.java:1151)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Parcel.writeValue(Parcel.java:1070)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Parcel.writeMapInternal(Parcel.java:488)
04-03 13:59:54.424: E/AndroidRuntime(28413):    at android.os.Bundle.writeT

이에 대해 검색하면 나오는 글을 보면 PutExtra를 쓸 때라고 합니다.

하지만 저는 그런 경우가 없었지요.

하루를 다 버리며 결국 찾았는데, 아래와 같은 코드 때문이었죠. 처음 putExtra든, 아래 putBundle이든 원인은 bundle에 parcelable을 계속 쌓다가 죽는겁니다. Activity가 넘어갈 때 발생한 것은, 새 Activity 때문이 아니라 아래로 깔리는 Activity가 SaveInstanceState를 하면서 발생한거죠.

    @Override
    public void onSaveInstanceState(Bundle bundle) {
        super.onSaveInstanceState(bundle);
        bundle.putBundle("bundle", bundle);
    }

하지만! 저는 정작 이렇게 코딩한 적이 없습니다.

그렇다면 왜 일어났냐면, 제가 쓰는 AndroidAnnotations를 이용해서 아래와 같이 코딩한 것이 결국 위의 문제를 만들었기 때문입니다.

    @InstanceState
    Bundle bundle;

이 문제는 AA 3.2에서 발생하며, 제가 버그 등록을 해서 3.3에서는 해결될 예정입니다.

TeamCity에서 안드로이드앱 배포하기

TeamCity는 제가 좋아하는 CI(Continuous Integration)툴, 다시 말해 소프트웨어 빌드 자동화 프로그램입니다. 몇 일 전에 버전 9이 나왔습니다. 무료로는 빌드설정 20개, 에이전트 3개의 제한된 수준으로 사용할 수 있으니 상당히 많은 혜택을 주는 셈입니다. 스타트업들에게는 뒤집어쓸만큼인거죠.

본론으로 들어가서, TeamCity에서 안드로이드 앱을 배포하는 방법을 알아봅시다. 다음의 상황을 가정합니다. 팀 내에서 알파버전 테스트, 즉 개밥주기(Dogfooding)를 할 때 두가지 방법을 사용할 수 있습니다.

  1. 구글 Play스토어가 제공하는 알파버전 배포방법을 이용.
  2. 빌드서버에서 직원들에게 이메일로 apk를 다운로드하도록 제공.
    • 오늘 설명할 방법.
    • 장점: 개발자가 소스코드 push만 하면 끝. 배포는 즉시.
    • 단점: 받는 사람이 매번 새로 깔아줘야 함.
  3. 매번 동료들 핸드폰 똥구멍에 선꽂고 구워주기 (이런건 이제 그만~)

즉, 빌드서버를 이용하는 방법은 양해를 구할 수 있는 팀 내에서 빠른 배포 사이클을 원할 경우 절대적인 장점을 지닙니다. Play스토어를 이용하는 방법은 외부인이 개입되는 클로즈베타에 유용합니다.

이제 다음의 과정을 거쳐서 설정할 것입니다.

  1. git에서 소스 다운로드 및 실행
  2. Build Step 작성: gradle부터 파일정리까지 (총 4단계)
  3. apk를 artifact로 인식
  4. 빌드 성공시 이메일로 알림
  5. 이메일에 원클릭 다운로드 링크 제공

참고: 제 TeamCity 및 안드로이드는 모두 윈도우 기반셋팅입니다. 윈도우에서 안드로이드 코딩하고 윈도우서버용 TeamCity를 설치했으니 다른 플랫폼은 경로나 소소한 셋팅이 다를 수 있습니다.

1. git에서 소스 다운로드 및 실행

자세한 설명은 생략합니다. Version Control Setting에서 git 주소를 연결하시면 됩니다. 또한, Triggers 메뉴에서 VCS Trigger를 만들어서 git이 갱신될 때마다 구동되도록 합니다.

저는 Trigger에 Quiet Period: 180초를 걸어서 push하고 가끔 파일 빼먹을 때가 있어서 급히 넣곤 하니까 3분 후 굽도록 했습니다.

2. Build Step 작성: gradle부터 파일정리까지

2.1 Clean

Build Step의 첫번째로, Gradle Clean을 합니다. 이전에 빌드한 데이터와 꼬일 수 있으니 비워주면 좋습니다.

  • Runner type: Gradle
  • Gradle tasks: clean
  • Additional Gradle command line parameters: --info
  • Gradle Wrapper: Use gradle wrapper to build project → 체크합니다.
  • Stacktrace: Print stacktract → 체크합니다.

설정 스크린샷은 아래와 같습니다. “어? 나보다 항목이 많은데?”라고 하시는 분은 제일 아래 노란색 “Show advanced options”를 클릭하세요. 중간에 Working directory는 git 다운로드 폴더를 임의로 지정했을 경우이며 취향일 뿐입니다.
캡처e1

2.2 gradle assembleRelease

소스를 컴파일하고 apk로 만들기 위한 assembleRelease 명령을 실행합니다. “2.1 Clean”과 거의 유사하며 다음만 다릅니다.

  • Gradle tasks: assembleRelease

센스있는 분이라면, 첫번째 스텝을 실행해보시고 성공하면 Build Steps 표에 나오는 “Copy build steps” 버튼을 이용해서 복사하시면 됩니다.

여기까지 하고 일단 한 번 Run 해서 확인합니다. 빌드 결과를 다음 스텝에서 인식하면 편하기 때문입니다.
여기까지 성공하면 80% 완료한겁니다.

2.3 unaligned 파일 삭제하기

assembleRelease를 하면 apk파일이 두 개 나옵니다- aligned, unaligned. 둘의 차이는 제가 올린 stackoverflow 질답글에서 알 수 있습니다. 결론은 unaligned는 지워도 되는 파일입니다.

  • Runner type: Command LIne
  • Working directory: apk나오는 경로 지정. Working Directory는 우측에 트리 아이콘을 눌러서 지정하세요. 제 경우 project/build/outputs/apk이지만 경우에 따라 다릅니다.
  • Run: Command Executable with parameters 선택.
  • Command executable: del
  • Command parameters: *unaligned.apk

참고: 리눅스나 맥OS용 TeamCity는 다르겠지요. 목적은 같을테니 내용을 파악하여 적용하시면 됩니다.

2.4 Rename (옵션)

다운받는 사람들에게 버전을 명확히 인식시키기 위해 빌드버전이 파일명에 나타나도록 합니다. 그래야 나중에 메신저로 “15버전 받으세요~”라는 식으로 말할 수도 있으니까요.

TeamCity는 한 번 구울 때마다 build.counter라는 값이 1씩 올라가니까 이걸 이용합니다. 위에 2.3과 거의 동일하니 역시 Copy Build Step을 이용해서 복사합니다.

  • Runner type: Command Line
  • Working directory: 2.3과 동일
  • Command executable: rename
  • Command parameters: bapul-release.apk bapul-release-%build.counter%.apk

물론 Command parameter는 맘에 드는 이름으로 지으세요. 저희 회사 이름 그대로 넣지 마시고요.

최종적으로, 빌드스텝 4개는 아래와 같아집니다.
캡처

3. apk를 artifact로 인식

Artifact가 사전에서는 말이 어려운데, 흔히 말하는 “산출물”입니다.
캡처

간단히 한 줄 설정이며, General Settings 메뉴에 들어가서 다음을 설정합니다.

  • Artifact paths: project\build\outputs\apk\bapul-release-%build.counter%.apk

역시 마찬가지로 우측 폴더트리 아이콘을 클릭하여 Step 2.4에 있던 경로를 참고하여 적어줍니다. 이러면 해당 apk 파일이 최종 산출물로 인식되어 원클릭 다운로드가 되도록 할 수 있습니다.

4. 빌드 성공시 이메일로 다운로드 링크 전송

빌드 성공시 서버가 Email을 보내려면 SMTP 설정이 되어있어야 겠지요. SMTP 설정은 사용하시는 메일서비스마다 다르겠고요. 저희는 구글앱스를 쓰는데 다음과 같습니다. 이 메뉴는 Administration > Email Notifier에 있습니다.
캡처

SMTP 설정은 했다치고, Notification은 개인마다 또는 그룹마다로 지정할 수 있습니다. 본인에게 먼저 테스트해보려면, 우상단 자기 profile을 들어간 후, Email Notifier에 “Add New Rule” 버튼을 클릭합니다.

“Builds from the selected build configurations”를 체크한 후 트리에서 원하는 프로젝트를 선택한 후, 우측 체크박스에서는 빌드 성공했을 때만 보내야 하니까 “Build is successful”에만 체크합니다.

아마도 메일이 잘 갈텐데 메일 받는건 다음과 같이 갑니다.

2014-12-13-14-01-25

위에 #15라는 링크를 클릭하면 브라우저가 뜨고 로그인을 하면 teamcity의 프로젝트 화면으로 갑니다. 거기서 Artifacts 탭에 간 후 파일을 선택하고 다운로드를 클릭하면 받아지지요.
그룹이나 특정 사람에게 보내려면 Administration > Users > 특정 사람 또는 Groups에서 해당 소속에게 일괄전송이 가능합니다.

5. 이메일에 원클릭 다운로드 링크 제공 (옵션)

TeamCity에 로그인은 한 번만 하면 그 후엔 자동로그인이 되니까 양해를 구할 수 있지만, 뭔지 알 수도 없는 프로젝트 화면을 팀원들이 보도록 하는건 어쩐지 불친절합니다. 받은 이메일에서 원클릭으로 apk를 받을 수 있도록 해야 할겁니다.

저는 TeamCity에 대해 무한히 좋아하는 마음을 가지고 있지만, 단 하나 부족한게 이메일 템플릿이 고정적이라는겁니다. 즉, Successful 메시지는 하나의 템플릿 파일에만 의존합니다. 그러므로, 이메일 템플릿을 요리하려면, 해당 템플릿에 if문을 넣어서 특정 프로젝트 결과에 대해서만 처리를 해줘야 합니다.

해당 이메일 템플릿 파일은 TeamCity 서버에 다음의 경로에 있습니다.
ProgramData라는 폴더는 숨김폴더임을 주의하세요.
Untitled-1

스크린샷에서 보이는 build_successful.ftl 파일을 수정합니다. 이 템플릿은 “FreeMaker”라는 자바기반 템플릿 언어에 기반합니다. 전체 파일은 제가 gist(클릭:열기)에 올려놓았습니다.

보면 아래와 같은 부분이 추가되었습니다. (제 프로젝트에 맞게 if문을 수정했습니다)

<#-- MODIFICATION START -->
  <#if buildType.externalId = "Android_BapulCube">
    <br>
    <a href='http://www.your-company.net/repository/download/${buildType.externalId}/.lastSuccessful/bapul-release-${build.buildNumber}.apk'>Click here to download APK.</a>
    <br>
  </#if>
<#-- MODIFICATION END -->

참고로, buildType.externalId는 해당 Build Configuration의 ID입니다. 해당 빌드셋팅가서 볼 수도 있고, 주소창에 http://www.your-company.com/viewType.html?buildTypeId=Android_BapulCube 형태로 보이기도 합니다. 빌드셋팅의 고유ID니까 특정 빌드 결과에 대한 것만 받는 경우이며, 다른 센스를 부리자면 project.name=”Android”와 같이 할 경우 Android라고 이름지은 프로젝트의 모든 빌드 결과에 대해 받게 되겠지요.

더 멋지게 꾸밀 분은 TeamCity Notification 설명서를 참고하세요.

여하간, 이렇게 설정하면 아래와 같이 소박하게 다운로드 링크가 나옵니다. “Click here to download APK.”라는 부분 보이시죠? 이제 팀원들은 (로그인을 했다면) 원클릭으로 apk를 다운받고 앱을 테스트할 수 있겠지요.

2014-12-13-14-00-36

더 센스를 부리자면 앱에 업데이트 프로그램을 구동시켜서 할 수도 있을겁니다. 그에 대한 방법은 여기서 다루지는 않겠습니다.

휴~ 끝났습니다.