Android App과 TDD - 임유진 (Kakao), GDG Korea Android 컨퍼런스 중에서

지난 2013/04/13일, 종로 페럼타워에서 열렸던 '구글 개발자와 함께하는 GDG Korea Android 컨퍼런스 '에서 공유된 내용입니다

발표 자료

요약

제가 핵심으로 기억하는 내용은 아래와 같습니다.

  • 앱이 기능이 점점 추가되고 복잡해져 가면서 코드가 점점 누더기가 되어갔다. 수정에 대한 부작용을 파악하기 어려워져 갔다. 그래서 테스트의 필요성이 느껴졌다.

  • Android의 기본 Test Framework는 너무 느려서 포기했다. 한번 실행에 30~40초가 걸렸다.

  • Robolectric도 검토했으나, 없는 기능이 많고 라이브러리 충돌 때문에 포기했다.

    • 테스트가 실패할 때 Robolectric의 버그인지 앱의 버그인지 확인하기 어려워서 디버깅에 시간이 걸렸다.

    • 암호화 관련 라이브러리에서 충돌이 일어남

  • 결국 UI쪽의 테스트보다는 UI를 벗어난 layer에서 핵심로직을 테스트하는데 집중했다.

  • 테스트 기법

    • android.util.Log를 호출하는 부분은 별도의 Wrapping 클래스로 작성. 테스트 프로젝트에서는 같은 패키지에 같은 클래스 이름으로 System.out으로 로그를 출력하는 클래스를 작성. class loader의 순서를 조정해서 테스트 코드에서는 테스트용 Logger클래스를 호출.

    • Background Thread에 대한 테스트도 래핑 클래스를 이용. 테스트 환경에서는 테스트 코드와 같은 쓰레드에서 동기적으로 실행되도록 Runnable.run을 호출하는 래퍼클래스를 호출함.

    • 같은 interface를 구현한 테스트용 Mock객체를 작성하고, 주입은 별도의 setter 메소드를 사용.

    • API에 대한 테스트는 정적파일을 통해서 함. 후임자가 API의 명세를 예시로 금방 확인할 수 있는 장점도 생김

    • Context에 대한 참조 등 Android에 대한 의존성을 제거하기 어려운 부분은 PowerMock + Mockito로 해결

  • 테스트할 수 있는 Layer를 구분하다 보니 설계 개선을 이끔

  • TDD에 대한 오해

    • 꼭 Dalvik에서 테스트해야 의미가 있다.

    • TDD로 모든 에러를 잡을 수 있다.

    • 개발 후에 만들어도 된다.

질문

좀 더 자세히 알고 싶은 점이 있어서 발표가 끝난 후에 아래 2가지를 질문했습니다.

1. Robolectric의 라이브러리 충돌의 구체적인 사례

앞에서 정리한 암호화 라이브러리와의 충돌사례를 알려주셨고, Robolectric의 버그 때문에 디버깅이 어려웠다는 이야기도 구체적으로 해주셨습니다.

2. Depenency Injection 프레임워크를 고려

검토는 했지만 쓰지는 않았고, 발표한 내용은 사례일 뿐이기 때문에 각자 생각하는 좋은 방법이 있으면 계속 시도해봤으면 좋겠다고 말씀해주셨습니다.

의견

추가로 제 의견을 덧붙이면, Roblectric에 빈틈이 많다는 단점은 저도 공감은 가지만 꼭 UI를 테스트하지 않더라도 Robolectric을 부분적으로 유용하게 쓸 수는 있다고 생각합니다. 예를 들면 android.util.Log 클래스를 쓰는 코드도 Robolectric을 쓰면 별도의 랩퍼 클래스가 없어도 편하게 테스트할 수 있습니다. ShadowLog라는 클래스를 사용하면 Console이나 특정 파일등 로그를 쓰는 위치도 좀 더 편하게 조정할 수 있습니다.

ShadowLog.stream = System.out;
Robolectric.bindShadowClass(ShadowLog.class);

그리고 멀티쓰레드에 대한 테스트를 할 때도 Robolectric의 RobolectricBackgroundExecutorService를 쓰면 편할 때가 있습니다. 이 클래스도 다른 쓰레드를 생성하지 않고 호출한 쪽과 같은 쓰레드에서 Runnable 클래스를 실행해 줍니다.

Android-annotations를 쓰면 @Background가 붙은 메소드는 BackgroundExecutor라는 클래스를 통해서 실행되는데, 이 클래스에 있는 executor라는 멤버변수를 교체하면 쓰레드의 생성 정책을 조절 할 수 있습니다. 따라서 테스트를 할 때는 아래와 같이 테스트용 Executor를 넣으면 자연스럽게 같은 쓰레드에서 동기적으로 Runnable 클래스를 실행할 수 있습니다.

BackgroundExecutor.setExecutor(new RobolectricBackgroundExecutorService());

참고로 구조적으로 Thread의 Executor를 바꿔치기 하기 힘든 경우에는 Awaitility라는 라이브러리도 사용해볼만도 합니다.

그리고 SDK버전에 따라서 다르게 돌아가는 코드가 있다면 Robolectric에서 아래와 같이 조작을 할 수 있습니다. (물론 PowerMock을 써도 같은 일을 할 수 있기는 합니다.)

Robolectric.Reflection.setFinalStaticField(Build.VERSION.class, "SDK_INT", Build.VERSION_CODES.JELLY_BEAN);

암튼 Robolectric에 빈틈이 많기에 큰 기대를 하지 말고 UI 레이어의 테스트에는 많은 욕심을 부리지 말자고 생각하지만 그래도 몇가지 매력적인 기능이 있어서 Robolectric을 아예 외면을 할 수는 없었습니다.

Mockito + Powermock의 조합은 강력하지만, 구조를 고치기 어려운 레가시 코드에만 한정해서 썼으면 한다는 의견입니다. 가능하다면 Powermock이 없어도 테스트할 수 있도록 구조를 개선하는 것이 더 클래스의 역할이 명확해지고, 앞으로 기능을 추가하거나 읽기에도 좋은 코드가 되기 때문입니다. 그렇게 구조개선을 하는데는 DI 프레임워크가 많은 도움이 되기도 합니다. DI 프레임워크를 쓰면 Context에 대한 직접 의존이나 안드로이드 기본 프레임워크의 final 메소드의 동작을 가로채야할 일이 적어져서 훨씬 테스트하기 편해집니다.

Helloworld에 올라온 Android에서 @Inject, @Test 에서 이에 대해 자세히 적었습니다.

소감

개인적으로 많은 고민을 했던 주제였고, 발표자께서 내리신 결론이 저와 거의 비슷했기에 무척 반가웠습니다. 저도 Android의 기본 테스트 클래스를 쓰면서 느꼈던 좌절감에 결국 JVM에서 테스트를 해야 TDD로서 의미가 있다고 느꼈습니다. UI에 대한 테스트보다는 안드로이드와 독립적인 Layer를 테스트하는 것이 ROI가 높고, 좋은 설계를 이끈다는 점도 공감이 갔습니다. 로그호출 부분이나 멀티쓰레드에 대한 테스트 등 제가 했던 고민도 보편적인 문제였다는 것도 확인했습니다. API의 호출결과를 정적 파일로 저장해두고 테스트 코드에서 파싱부터 검증하는 기법은 저도 Server to Server API클라이언트 모듈 테스트 때 많이 썼던 방법이였습니다

제가 편향된 생각을 가졌을지 늘 걱정이 되었는데, 같은 의견을 가지신 분이 구체적인 사례까지 공유해주셔서 많은 도움이 되었습니다. 앞으로 다른 분께도 안드로이드에서 TDD를 자신있게 권장해드릴 용기를 얻었습니다.

Spring tag library + Tomcat7.x 또는 Glassfish 2.2.x의 EL Injection 위험

취약점의 조건

아래 조건을 모두 충족시킨다면 코드는 치명적인 Remote code execution 취약성이 존재할 여지가 있습니다.

  1. EL 2.2를 지원하는 서블릿 컨테이너를 쓰거나 EL 2.2 라이브러리를 직접 jar파일로 참조해서 쓰고 있다. (대표적으로 Tomcat 7.x혹은 Glassfish 2.2.x)

  2. Spring 3.1.x 미만 버전을 쓰고 있다.

  3. Spring의 JSP Tag( <spring:message.. 등)을 쓰고 있다.

  4. Spring의 JSP Tag에서 EL을 지원하는 속성에 사용자가 입력한 값이 들어갈 수 있다.

위에서 첫번째 조건이 해당하지 않더라도 다른 취약점을 조심해야 합니다. EL 2.2을 사용하지 않더라도 JSP 2.0이상을 지원하는 서블릿 컨테이너를 쓰면서 2,3,4에 해당하는 코드라면 어플리케이션의 중요한 정보를 노출시킬 수 있는 취약점이 존재할 위험이 있습니다. Remote code execution만큼 심각하지는 않지만, 역시나 조치가 필요합니다.

이 취약 지점은 의도하지 않은 정보를 노출할 수 있는 위험성으로 이미 2011년 9월 지적되어 보안되었으나, 이번달인 2013년 1월에는 같은 취약 지점을 통해서 원격 코드를 실행하는 기법이 발견되어 위험성 등급이 올라갔습니다. 즉, 해당 라이브러리가 취약성을 유발한다는 점 자체는 새로운 것이 아니고, 이미 이를 보강하는 버전이 나와 있습니다.

이 취약점의 원인과 조치방법은 아래 링크에 정리해놓았습니다. (코드가 들어간 글은 GIST를 쓰는 것이 훨씬 편하네요.)

Markdown + Dropbox

  • 업데이트

    • 2019.04.10 : 현시점에서는 Calepin 서비스도 종료되었습니다.

    • 2013.03.05 : GIST에 썼던 글을 옮기어 봅니다.

      • Springnote 서비스 종료 전에 쓴 글이라서 Springnote의 마이그레이션 코드는 이제는 쓸수가 없네요)

자료를 정리하는 형식과 도구에 대한 고민

회사에서 만든 자료들은 MOSS(Microsoft Office SharePoint Server)나 위키, 메일함 등이 있으니 큰 아쉬움은 없는데, 개인적인 자료들은 어떻게 정리하고 보관, 공유할지가 늘 고민이 됩니다.

저는 공개하고 싶지 않은 것들은 에버노트드롭박스를 쓰고, 공유할 자료들은 스프링노트와 이글루스 블로그을 활용하고 있습니다.코드들은 주로 GithubGist에 올립니다.

이렇게 다양한 서비스를 쓰다보니 자료를 서비스 간에 이동을 할 일이 생기면 많이 불편합니다. 예를 들면 에버노트에 있는 자료를 블로그에 올린다거나 스프링노트에 있는 글을 구글사이트로 옮길 때가 그렇습니다.그냥 한 두 페이지면 복사해서 붙여넣기를 하겠지만, 여러 페이지를 옮길 때 그렇게 하면 단순 반복 작업에 들어가는 시간이 아깝습니다.웹 브라이저나 웹 에디터의 차이 때문에 칸 맞추기 같은 사소한 편집에 시간을 많이 허비하기도 합니다.웹이나 클라이언트 환경 등 다양한 환경에서 모두 편집이 간편했으면 하는 바램도 있었습니다. 우분투를 쓰는 시간이 많다보니 MS워드처럼 특정 OS가 종속적인 편집기는 별로 좋아하지 않기도 합니다. 그리고 정리한 내용을 자주 찾아보니 검색과 탐색 기능을 쓰기 쉬웠으면도 했습니다.

이번에 스프링노트가 종료될 예정이라 어떤 형식으로 자료를 정리할 것인지에 대한 고민이 더 커졌습니다.

요구사항을 정리하면

  • 자료를 이동하는 비용이 적어야하고 (이식성)

  • 자료의 모양 때문에 편집을 하는 시간이 되도록 없었으면 하고

  • 특정 환경에 종속적이지 않은 다양한 문서 편집기를 사용할 수 있어야 하고

  • 검색 기능을 간편하게 쓸 수 있어야 한다.

이런 점들을 고민하다보니 우선 정리하는 자료의 형식은 마크다운을 선택하게 되었습니다. Github와 Gist를 쓰면서 그 단순함에 만족을 했고, 텀블러, 트렐로등 지원하고 있는 서비스도 늘어가고 있어서 이식성도 높다고 생각했습니다.

그리고 원본 데이터를 파일단위로 관리하고 싶었습니다. 쓰던 서비스가 망하더라도 파일을 단순히 복사해서 다른 서비스로 옮길 수 있다면 이전 비용이 적을 것이기 때문이였습니다.

Github를 이용한 Octopress도 검토했으나, 버전관리까지는 필요없이 최종 파일만 관리하면 되었기 때문에 Dropbox를 썼으면 좋겠다고 생각했습니다.

사람 생각은 다 비슷한지, 이미 Dropbox + Markdown을 이용한 블로그와 위키 서비스가 나와 있었습니다.

Calepin( =Markdown + Dropbox)로 블로그 만들기

Calepin을 이용해서 http://book.benelog.net이라는 블로그를 만들어봤습니다.그동안 여러 곳에서 정리하던 읽은 책에 대한 메모를 Markdown으로 정리한 것입니다.

아래와 같이 .md 파일의 처음 시작에 날짜와 제목, URL, 태그 등의 정보를 달아주고, 파일을 [Dropbox 공유폴더]/Apps/Calepin이라는 폴더로 복사합니다.

Date: 2012-07-19
Title: 어제를 버려라
Slug: 어제를_버려라
Tags: IT업계

그리고 Calepin 사이트에 가서 'Publish’버튼을 누르면 약속된 폴더에서 파일정보를 읽어와서 블로그를 생성해 줍니다.

calepin

이 정보를 바탕으로 블로그를 만들어줍니다. 아래와 같이 굉장히 단순한 디자인인데, 저는 이 정도로 충분하다고 느꼈습니다.

book benelog net

scriptogr.am라는 서비스도 거의 비슷한 기능을 제공하는데, 블로그 테마 등 약간의 커스터마이징이 가능합니다. wikipackit도 사용법은 유사하고, 위키 페이지간의 링크 기능을 제공하지만, 현재까지는 페이지를 비공개로만 관리할 수 있습니다. 앞으로 유사한 서비스가 더 많이 나올수도 있고, 이전 비용도 크지 않으니 언제든지 좋은 서비스가 나오면 갈아탈 생각입니다.

스프링노트 마이그레이션

저는 스프링노트로 관리하던 페이지들 중 일부는 http://wikipackit.com 으로 옮겼습니다. 스프링노트의 API를 이용해서 Markdown형식으로 스프링노트 페이지를 다운로드하는 스크립트를 작성했습니다.Python과 Pandoc을 활용했습니다.

Eclipse Markdown Text Editor

편집기로는 주로 Eclipse에서 Markdown Text Editor을 사용하고 있습니다.Eclipse가 무거운 편이지만, Platform Runtime Binary를 찾아서 최소한 도로 설치하면 50MB가 넘지 않게 비교적 가볍게 쓸 수도 있습니다. 예를 들어 Eclipse 3.8의 최소설치를 위한 파일은 아래 링크에서 찾으실 수 있습니다.

편집을 하면서 HTML렌더링을 할 수도 있고, Eclipse의 단축키를 그대로 활용할 수도 있습니다. 예를 들어 파일을 찾을 때 Ctrl + Shift + R키를 눌러서 파일명의 일부를 입력해서 바로 찾아가는 것들이죠.

eclipse markdown plugin

이렇게 구축한 글쓰기와 자료정리 환경을 굉장히 만족하면서 쓰고 있습니다. Markdown을 지원하는 서비스나 도구들은 앞으로 계속 나올 듯하고, 스스로 만들기도 쉽기 때문에 더 편하게 쓸 수있는 가능성도 크다고 봅니다.