룩핀 안드로이드 100만 다운로드 달성 기념 리뷰

안드로이드 100만

# 2016-03-25 ~ 2018-03-02 (708일)

룩핀 안드로이드 다운로드 100만까지 걸린 날이다.
708일 이라는동안 정말 많은 일 들이 있던 것 같다.
처음부터 같이 시작한 팀원,
새롭게 들어온 팀원,
아쉽게도 퇴사하게 된 팀원 모두가 고생하고 각자 자기자신의 인생을 투자해가며 힘낸 결과물이다.

708일이라는 기간동안의 있었던 일들을 담아내기엔 내 글솜씨도 부족하고 글을쓰더라도 너무나도 길어질걸 알기에 이 글에 담는 것은 과거보다는
현재와 100만 다운로드이후에 대한이야기를 짧게 하고싶다.

처음부터 지금까지 안드로이드를 혼자 맡아오고 있다.
다운로드수가 늘수록 안드로이드 개발을 맡고있는 나로서 배포와 개발에 대한 부담감은 점점 커져갔고
물론 그 부담감이 없도록 코드를 짜고 생각을 하며 개발을 해야하지만 혼자 다양한 토끼를 잡기란 나에게 있어서 생각보다 쉽지 않았었고 혼자 한 플랫폼을 맡아서 간다는게 분명히 장점도 있고 단점도 있고 그랬던 것 같다. 내가 느낀 장점과 단점에 대해서는 적지는 않으려고 한다.
직접 연락(메신저 등등)해서 물어봐 주신다면 언제든지 이야기 해드릴 것이다.
언제가는 클라이언트 개발을 혼자 맡아서 하시는 분들이 있다면 만나서 다양한 이야기 그리고 가지고있는 고민에 대한 이야기를 해보고 싶었다.

그리고 100만 다운로드까지 달려오며 제일 아쉬웠던 사실은 Fabric을 많이 신경을 못쓰며 달려온 부분 이였다.
100만 다운로드 앱이 돼었고 배포에대한 부담감도 커진만큼
이제 부터라도 내가 어떻게든 에전보다 더욱더 신경쓰고 챙겨가며 가고싶다.
그리고.. 개인적인 욕심이지만 꼭 CRASH-FREE USERS 100%를 달성하고싶다.
(현재 CRASH-FREE USERS가 너무 엉망은 아닙니다.)

최대한 짧게 쓰려했지만 조금 길어진 것 같다.. 마지막으로 꼭 쓰고 싶었던 말이 있었다.

룩핀에 지금도 있고 또한 있었던 분들께 다시한번 감사드리고 또한 수고도 많으셨습니다.
또한 유저분들께도 정말 감사의 말씀 전합니다.

Android findViewById에 대해서(성능 및 자세한 고찰)

글에 앞서 소개

안녕하세요? 룩핀에서 안드로이드 개발을 하고 있는 오경식이라고 합니다.
혹시 잘못 된 내용이나 궁금한 점이 있으실 경우에는 언제든지 메일이나 메시지 부탁드리겠습니다.

이글을 쓰게된 계기

안드로이드 개발을 하면서 List관련 화면에서 View를 재사용하지 않고 구현하는 경우에 findViewById를 계속 호출 하게 되어 성능저하가 일어날 수 있다는 것으로 많이들 알고 있으실 겁니다.
예전 kmshack님의 ListView 성능 개선 글을 읽어보시면 글 중 ListView에 findViewById의 값 비싼 호출을 반복하는 문제를 해결하기 위한 ViewHolder Pattern 및 그 외 문제점에 대한 글을 쓰셨습니다.
지금 현재 많이들 쓰고 계신 RecyclerView에서는 이 문제를 해결하기위해 ViewHolder Pattern을 꼭 사용하도록 하였습니다만

최근 kunny님이 쓰신 kotlin Android Extensions - 리사이클러뷰의 뷰홀더에서 올바르게 사용하는 방법을 보시면 뷰홀더를 Kotlin Android Extensions를 이용해 저 처럼 잘못 작성하시는 경우에는 kotlin 코드의 바이트코드를 자바코드로 변환하시면 kunny님 글에 있듯이 호출 비용이 큰 findViewById를 매번 호출하고 있었습니다.

해당 글을 읽고 기존 코드를 수정하면서 findViewById가 왜 그런 호출 비용이 커서 성능 관련 이슈를 발생시키는지에 대한 궁금증이 생겨 그 범인에 대해서 알아보고 글을 쓰게 되었습니다.

범인을 찾아보자

findViewById이 성능상의 문제가 될 수 있는 이유를 찾아서 설명하기 위해서 안드로이드 소스코드를 찾아 보게 되었습니다.

View

1
2
3
4
5
6
public final View findViewById(int id) {
if (id < 0) {
return null;
}
return findViewTraversal(id);
}

을 보시면 id가 0 이상인 경우 즉 유효한 경우에 findViewTraversal 을 호출 합니다.

1
2
3
4
5
6
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
return null;
}

findViewTraversal을 보시면 단지 전달 된 id가 View의 id와 같은지 검사하고, 그렇지 않으면 null을 반환합니다.
딱히 성능에 영향을 끼칠만한 요소는 보이지 않습니다.
그렇다면 ViewGroup을 보시겠습니다.

ViewGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
protected View findViewTraversal(int id) {
if (id == mID) {
return this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return v;
}
}
}
return null;
}

전달 된 id가 ViewGroup의 ID와 같은지 여부를 확인하고 같은경우에 자기자신을 반환하고 아닌 경우 자기 자신이 가진 Child View에 대해 루프를 돌려 각 Child View에 findViewById를 호출하고 있습니다.
그렇다면 만약 ViewGroup안에 Child View가 10개가 있고 그 해당 뷰들을 찾아 넘길려면 최소 10번의 loop를 돌게됩니다.

리스트처럼 뷰가 반복적으로 사용되는 곳에서 위 메서드가 호출되는 상황이 반복된다면 꽤나 큰 리스크라고 생각이 됩니다.

결론

코드를 잘 짜도록 하자.

bubbleseekbar를 리뉴얼 해봤습니다.

글에 앞서

안녕하세요?
룩핀에서 안드로이드 개발을 하고 있는 오경식 이라고 합니다.
해당 프로젝트는 woxingxiao님이 github에 올린 BubbleSeekBar 라는 프로젝트를 포크하여만들었습니다.
해당 프로젝트를 진행하게 된 계기는 Issue에 있는 사용자들의 새로운 요구 기능들이 저 또한 원하는 기능들 이기도 하고 마침.. woxingxiao 님도..

쿨

이런 말씀을 하셨기에 진행하게 되었습니다.
그리고… 많은 Star 부탁드립니다.

라이브러리 저장소 링크

BubbleSeekBar for Android Renewal by sikeeoh!

This project is a fork of woxingxiao/BubbleSeekBar project.

리뉴얼을 진행하면서 기존 Issue에 등록된 요구기능들 추가 및 제가 필요한 기능들을 기존 구조를 해치지 않고 새로 추가해 봤습니다.

  • 기존 Section영역에 Text는 무조건 내용이 고정이였는데요 이부분을 Custom하게 바꿀수 있도록 작업하였습니다.
  • Mark만 보여주고 뒤에 Track이 그려지는걸 원하지 않는 기능을 원하시는 분들이 있었기에 Track을 Hide할수있는 기능을 추가하였습니다.

  • Section영역에 Custom한 녀석들만 보여줄지 기본 데이터들을 보여줄지 둘다합쳐서 보여줄지를 선택할 수 있는 속성이 추가되었습니다.

  • 간혹가다 Section Text Visibility 속성이 적용이 안되던 버그를 수정하였습니다.

Demo

demo1
demo2


demo3
demo4

그 외 자세한 사항은 저장소에 가시면 확인 하실 수 있습니다.

Implementation VS Api in Android Gradle Plugin - 번역

최초 작성일 : 2017-08-28 17:23:49
최종 수정 : 2017-11-29

글에 앞서

안녕하세요?
룩핀에서 안드로이드 개발을 하고 있는 오경식 이라고 합니다.
이 글은 인도 벵갈루루의 Bedanta Bikash Borah님이 Medium에 올린 Implementation Vs Api in Android Gradle plugin 라는 글을 번역해서 만들었습니다.
번역하면서 읽으시는 분들이 편하게 느끼기 위해 원글과 아주 약간의 어투 및 기타 차이가 있을 수 있습니다.
번역을 흔쾌히 허락해주신 Bedanta Bikash Borah님께 감사하다는 말씀 드립니다.
또한 번역이 미흡해 번역의 질이 낮게 느껴질 수 있을텐데 많은 조언 부탁드립니다.
부족한 번역 글 읽어주셔서 감사합니다.
승낙

Implementation Vs Api in Android Gradle plugin 3.0

나는 프로젝트에서 Android Gradle 플러그인 3.0을 사용하는 동안 compile 키워드가 Deprecated 되었고 새롭게 나온 implementationapi를 위해 더 이상 compile이 추후에 사용되지 않는 것으로 되었습니다.
위 두 가지 모두 예제를 통해 이해 해 보도록 하겠습니다.

샘플 어플(kotlin)은 여기있습니다.

라이브러리 모듈이 4개인 프로젝트를 가정 해 보겠습니다.

  • LibraryA

  • LibraryB

  • LibraryC

  • LibraryD

종속성(의존성) 트리는 다음과 같습니다.

모든 라이브러리 모듈 속에는 간단하게 작성 된 클래스 파일이 들어 있습니다.

LibraryD:

1
2
3
4
5
class ClassD {
fun tellMeAJoke(): String {
return "You are funny :D"
}
}

LibraryC:

1
2
3
4
5
class ClassC {
fun tellMeAJoke(): String {
return "You are funny :C"
}
}

LibraryB:

1
2
3
4
5
6
7
class ClassB {
val b = ClassD()
fun whereIsMyJoke(): String {
return b.tellMeAJoke()
}
}

LibraryA:

1
2
3
4
5
6
7
class ClassA {
val c = ClassC()
fun whereIsMyJoke(): String {
return c.tellMeAJoke()
}
}

위의 클래스 파일에서 LibraryALibraryB는 각각 LibraryC 그리고 LibraryD에 종속된다는 것을 알 수 있습니다.

LibraryA -> LibraryC

LibraryB -> LibraryD

따라서 build.gradle 파일에 이러한 종속성을 추가 해줘야 합니다.

Compile (2.0) or Api (3.0):

새로운 api 키워드는 이전 compile 키워드와 정확히 동일 합니다.
따라서 모든 compileapi로 바꿔도 제대로 동작합니다.
이제 LibraryB에서 api 키워드를 사용하여 LibraryD의 의존성을 추가해 보겠습니다.

1
2
3
4
dependencies {
. . . .
api project(path: ':librayD')
}

마찬가지로 LibraryB 가 App module에 추가 됩니다.

1
2
3
4
dependencies {
. . . .
api project(path: ':libraryB')
}

이제는 LibraryBLibraryD 모두 App module에서 접근 할 수 있습니다.
sample App에서 두 라이브러리는 다음과 같이 접근 할 수 있습니다.

Implementation (3.0):

implementationapi와 다른 점을 찾아야 할 시간입니다.
에제로 돌아가서 이번에는 implementation키워드를 이용해서 LibraryCLibraryA로 가져와 봅시다.

1
2
3
4
dependencies {
. . . .
implementation project(path: ':libraryC')
}

App module도 똑같이

1
2
3
4
dependencies {
. . . .
implementation project(path: ':libraryA')
}

이제 App module에서 LibraryC에 접근하려고 시도하면 Android Studio에서 오류가 발생합니다.

이는 우리가 api 대신 implementation을 사용한다면 App module에서 LibraryC에 직접 접근 할 수 없다는 것을 의미합니다.
그래서 implementation을 사용함으로써 이득은 무엇일까요?

Implementation vs api:

첫 번째 시나리오에서 LibraryDapi를 사용하여 컴파일 됩니다.
LibraryD 내부에서 변경이 되면, Gradle은 LibraryD, LibraryBLibraryB를 import하는 다른 모든 모듈(Module X)을 재 컴파일 해야 합니다.

첫번째 시나리오

그러나 두 번째 시나리오에서 LibraryC가 내부적으로 변경이 되면 Gradle은 LibraryCLibraryA만 다시 컴파일 하면 됩니다.
App module 에서는 LibraryC를 직접 가져오지 못하고 또한 다른 클래스들은 LibraryC안에 구현 된 것들을 직접 사용 할 수 없으니까요.

두번째 시나리오

모듈을 많이 사용하는 프로젝트에서 작업하는 경우 (이 Fragmented Podcast 에서 어마무시한 빌드 시간 관련 이야기를 들었다)이 방법을 사용하면 빌드 프로세스의 속도가 크게 빨라질 수 있습니다.
샘플 프로젝트에서 이 작업을 시도했지만 약간의 개선이 있었습니다.
다음은 모든 시나리오에 대한 빌드 보고서 입니다.

Full Build:

Full Build

Change in LibraryD:

Change in LibraryD

Change in LibraryC:

Change in LibraryC

api 와 implementation에 대한 요약 설명

api는 의존성에 추가하는 모듈이 의존하고 있는 다른 모듈 까지 접근이 가능합니다.
예 ) ModuleX 에서 LibraryA 를 api project(path: ':LibrayA')로 의존성에 추가하면 ModuleX에서 LibraryC 클래스를 접근 할 수 있습니다.

implementation는 의존성에 추가하는 모듈 외 추가하는 모듈이 의존하는 다른 모듈에는 접근이 불가능 합니다. 즉 위에 상황을 보자면 implementation project(path: ':LibraryA')로 의존성에 추가하면 ModuleX에서 LibraryC 클래스에는 접근할 수 없습니다.

위에서 보았듯이 이로 인한 차이가 발생하여 빌드 속도의 개선이 있을 수 있습니다.
다시 한번 미흡한 글 읽어주셔서 감사합니다.

TL;DR(Too Long; Didn’t Read.):

모든 compileimplementation으로 바꾸고 프로젝트를 빌드해보세요.
만일 여러분이 성공적으로 잘 된다면 훌륭한 프로젝트 입니다.
그렇지 않으면 종속성이 있는지 찾아보고 api키워드를 사용하여 해당 라이브러리를 사용합시다.

룩핀 안드로이드 Buddybuild 적용기 그리고 여러분도 적용해보자

소개

안녕하세요?
룩핀에서 안드로이드 개발을 혼자 맡고 있는 오경식 이라고 합니다.
자세한 소개는 소개링크 로 대체 하겠습니다.

글을 쓰게 된 계기

Lookpin 안드로이드 프로젝트에 CI를 붙이려고 알아보던 도중 buddybuild라는 나온지 얼마안되고 따끈따끈하고 좋은 서비스(?)라는 걸 알게 되었고 룩핀 안드로이드에 붙이게 되었습니다.
그나마 여러 CI서비스중 최근에 출시되고 규모가 있고 어느정도 검증된 서비스인 것 같지만 최근에 출시해서 그런지 워낙 자료가 부족 하고 한번 쯤 정리하는 것도 나쁘지 않을 것 같아서 적게 되었습니다.

저장소 관련 내용은 Github 기준으로 작성 되었습니다.

buddybuild 란?



홈페이지 소개

  • Ship apps faster with
    A continuous integration, continuous deployment, and user feedback platform for iOS and Android development teams.

지속적인 통합 및 배포 그리고 사용자 피드백을 위한 플랫폼 서비스 입니다.

Repository 추가

- 회원 가입

1. 회원가입을 합시다.

- Repository 추가 (Github Organization Repo)

1. Organization에 있는 Repo 같은경우 Add a with SSH를 이용해서 등록해야 합니다.
Github.com을 통해 진행 할 경우 Repository List에는 개인의 저장소 밖에 안뜨기 때문 입니다.

2. ssh-key를 추가해줍시다.

3. 진행중.

4. 등록 중 Build가 Fail난 경우 Build가 통과 될 수 있게 프로젝트 수정해주세요.

5. Build가 성공한경우


- Repository 추가 (My Repository)


설정

- Dashboard

- Adding a Github Webhook

Organization Repo 이신 분 들은 Webhook을 설정하라고 나올 겁니다.
Webhook은 buddybuild에게 repository에 대한 코드 변경 사항을 알립니다.
새로운 변경 사항이 푸시 될 때마다 buddybuild가 자동으로 새 빌드를 시작하여 최신 빌드 상태를 유지 하도록합니다.

Adding a Github Webhook docs

- App Setting

App Setting 쪽은 자신에게 맞게 설정 해주시면 됩니다.

빌드 조건 및 빌드 관련 각종 설정

테스트 관련 설정

각 브랜치별로 테스트들을 어떻게 구성해서 진행할지 설정 할 수 있고
UI Test 나 Unit Test 등등 설정 할 수 있습니다.
실제 디바이스로 하는 UI Test는 돈이 드니.. 잘 선택 해 주시길 바랍니다.

- Add Slack

Build 결과를 Slack 특정 채널에 notify 하고 싶은경우 설정 하면 됩니다.
저 같은 경우 모든 브랜치 관련 빌드를 #Commit 채널로 받고 있습니다.


Slack 표시

Deploy

배포 같은 경우 배포그룹을 만들어서 각 그룹마다 조건을 걸어서 배포 할 수 있습니다.
예) 특정 브랜치가 빌드 될때마다 , 10시간마다, 수동으로 등등

배포는 설정해둔 이메일로 배포결과를 보내거나 또는 Google Play로 바로 배포도 가능합니다.
저 같은 경우 master 브랜치가 빌드가 성공한 경우에는 buddybuild를 통해 자동으로 google play beta 배포를 진행하며 develop 브랜치 같은경우에는 이메일로 배포를 하여 테스트를 진행 합니다.

배포그룹

구글 플레이 배포 설정

이메일 배포

Build Detail

빌드 상세 화면 입니다.

테스트 결과

빌드 결과물 배포( 배포 그룹 선택해서도 가능 )

빌드 결과물 배포후 테스터들 상태

빌드가 실패한 경우 메일

사용한 후기

유저가 잘 활용 할 수 있도록 구성된 사이트와 설정이 편하고 많은 기능들 그리고 Docs에 반해 buddybuild를 선택했는데 아직까지는 후회가 없습니다.
하지만 아직 테스트 코드가 적다보니 제가 계속 노력해 나가야 하는 부분이라고 생각됩니다.
그리고 buddybuild에는 많은 기능들이 있는데 아직 그 기능들을 완전히 다 못 쓰는 부분들은 점차 발전해나가고 정리할 수 있는 부분들은 문서로 정리해두려고 합니다.

그리고 예전 안드로이드의 배포 관련 해서는 매우 수동적 이였습니다.
예를들어 테스트 apk 배포를 기기에 하나하나 설치하는 식으로 진행하거나 직원분들의 apk 재설치 요구 등등 이런 요청에 대한 수동적인 대응으로 인해 중간중간 시간을 빼앗기고 집중도 깨지고 그랬습니다.
하지만 buddybuild를 통해 배포 자동화와 배포 및 테스트 관련 규칙들도 정리가 돼서 뿌듯해 하고 있습니다.

아직 CI 나 Deploy 관련 서비스를 적용 하지 않은 기업들이 있다면 적용해보면 좋을 듯 싶습니다.

생각 보다 그렇게 오래 안걸려요!

결론

우리모두 간지나게 BuddyBuild Build status badges 달아요!

기타 문의

혹시 잘못 된 정보나 궁금한 점이 있다면 제 소개링크
에 있는 메일이나 페이스북 메시지로 문의 주세요!

감사합니다.