ListView의 addHeaderView(Footer)를 RecyclerView 에서 구현하기

안드로이드 앱 개발시 가장 많이 사용하던 위젯은 ListView이다. 그리고 RecyclerView는 Android 5 Lollipop 버전이 업데이트 되었을 때, Support Library V7에 추가되었고 이를 사용하기 위해서는 build.gradle에 다음과 같이 추가하면 사용할 수 있다.

compile 'com.android.support:recyclerview-v7:${supportLibraryVersion}'

기존에 ListView에서 사용하던 상, 하단에 뷰 추가를 지원하던 addHeaderViewaddFooterView가 RecyclerView에서는 더이상 지원하지 않는다. 그래서 대부분의 안드로이드 개발자들은 StackOverFlow에 ‘How to add header view to recyclerview’ 다음과 같이 검색했을 것이다. 이미 RecyclerView에 HeaderView나 FooterView를 추가해주는 오픈 소스는 많이 공개되어 있다. 필요하다면 찾아서 사용하면 쉽게 뷰를 상하단에 추가할 수 있다. 결과적으로 기존 ListView에서는 addHeaderView가 어떻게 동작 했으며 RecyclerView에서는 그 기능을 동작하도록 할 수 있는지 한번 알아보려고 한다.

HeaderViewListAdapter (android.widget)
public void addHeaderView(View v, Object data, boolean isSelectable) {
    ...
    // Wrap the adapter if it wasn't already wrapped.
    if (mAdapter != null) {
        if (!(mAdapter instanceof HeaderViewListAdapter)) {
            mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, mAdapter);
        }
    …
    }
}

ListView의 addHeaderView의 내부를 들여다 보면 기존의 ListAdapter 대신에 HeaderViewListAdapter 어댑터 클래스를 사용하며 이 위젯은 래핑에 필요한 WrapperListAdapter 인터페이스를 상속하고 있으며 Adapter를 가지고 있는 껍데기가 된다. 기존의 Adapter를 HeaderViewListAdapter로 재생성(Wrapping)하면서 mAdapter를 파라미터로 넘겨주는 이유가 바로 이 때문이다. 따라서 HeaderViewListAdapter에서는 기존의 Adapter를 그대로 가지고 있으면서 화면에 상하단에 추가된 뷰를 노출시켜주는 함수를 더 가지고 있다. 다음으로 getView함수를 살펴보자.

public View getView(int position, View convertView, ViewGroup parent) {
    // Header (negative positions will throw an IndexOutOfBoundsException)
    int numHeaders = getHeadersCount();
    if (position < numHeaders) {
        return mHeaderViewInfos.get(position).view;
    }

    // Adapter
    final int adjPosition = position - numHeaders;
    int adapterCount = 0;
    if (mAdapter != null) {
        adapterCount = mAdapter.getCount();
        if (adjPosition < adapterCount) {
            return mAdapter.getView(adjPosition, convertView, parent);
        }
    }

    // Footer (off-limits positions will throw an IndexOutOfBoundsException)
    return mFooterViewInfos.get(adjPosition - adapterCount).view;
}

Header로 추가된 상단뷰의 수보다 position이 작은 경우에는 HeaderView를 리턴해주고 ListView에서 Row Item의 position은 Header 수 만큼 빼서 그려준다. 상하단에 추가된 뷰가 있을 경우, 그것을 보여주고 그게 아니라면 Adapter의 기본 로직으로 동작하게 된다.

HeaderRecyclerViewAdapter 구현하기
public abstract class HeaderRecyclerViewAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {

개발 중인 서비스에서 다양한 데이터 타입이나 뷰를 사용 한다면 일일이 대응되는 클래스(RecyclerView.Adapter 상속 받는)를 생성하기 보다는 HeaderRecyclerView를 사용하거나 구현 방법을 한번 보는 것을 추천한다. 목록을 보여주는 ListView와 RecyclerView는 기본 개념이 같이 때문에 내부 동작 또한 크게 차이는 없을 것이다.

달라진 것이 있다면 ListView에서 종종 findViewById의 비용을 줄이기 위해 사용하던 ViewHolder가 RecyclerView.ViewHolder로 추가되어 예전에는 Adapter에서 getView를 통해 View를 생성했다면 이제는 ViewHolder를 생성하고(onCreateViewHolder) 데이터 바인딩할 때(onBindViewHolder), 파라미터로 다시 받아 사용할 수 있다.

public class HeaderRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>{

  private int TYPE_HEADER = -1;
  private int TYPE_ROW_ITEM = -2;

…
  public void addHeaderView(View header) {
    mHeaderViews.add(header);
  }

  @Override
  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (isHeaderPosition(position)) {
      return new ViewHolder(mHeaderViews.get(position));
    } else {
      return mInnerAdapter.onCreateViewHolder(parent, position);
    }
  }

  @Override
  public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderPosition(position)) {
      //  bind HeaderViewHolder
    } else {
      //  bind ItemViewHolder
    }
  }

  @Override
  public int getItemViewType(int position) {
    // return 3 item type by position
  }
…

ListView의 addHeaderView 함수를 HeaderRecyclerViewAdapter에 추가한다. 그리고 RecyclerView.Adapter가 position과 ViewHolder의 타입에 따라 동작해야 하므로 위의 3가지의 함수가 동작에 있어서 가장 중요하다. 마찬가지로 만약에 FooterView를 RecyclerView에 추가하고 싶을 경우, 뷰의 식별자를 선언하고 ViewHolder를 생성하는 부분과 데이터를 바인딩하는 부분에 추가한다. 그리고 getItemViewType에서 화면상에 FooterView가 노출되는 position일 때 리턴해주면 된다. 마지막으로 앞으로 자주 사용할 RecyclerView와 많이 사용했던 ListView를 자유롭게 다뤄보자.

HangulParser – 한글 자소 조합과 분리

HangulParser는 Java와 Android 프로젝트에서 한글의 글자 하나를 자소(자음과 모음)로 분리하고, 자음과 모음을 하나의 글자로 조합 해주는 Java 라이브러리다. 간단한 안드로이드 앱을 개발하기 위해 찾아 보다가 한글 글자 하나는 유니코드 값을 가지고 초성, 중성, 종성으로 나눌 수 있는 문서를 읽고 만들게 되었다. 반대로 초성, 중성, 종성의 유니코드 값을 더하면 한 글자의 유니코드를 얻을 수 있다.

Reference App : 꽃별천지

Download

HangulParser Github

Java와 Android 프로젝트에서 빌드 툴로 Gradle을 사용하고 있다면 build.gradle 파일에 다음을 추가해서 빌드하면 쉽게 사용할 수 있다.

repositories {
  jcenter()
}

dependencies {
  compile 'com.github.kimkevin:hangulparser:1.0.0'
}

Usage

자소 분리 (Disassemble)

List disassemble(char hangul)

한 글자를 자소(초성, 중성, 종성)로 나누어 리스트로 반환한다.

Samples :

jasoList = HangulParser.getInstance().disassemble('한');
> [ㅎ, ㅏ, ㄴ]

예를 들어, ‘한’을 입력 값으로 넣어주면 결과는 [ㅎ,ㅏ,ㄴ]을 얻을 수 있다.

jasoList = HangulParser.getInstance().disassemble("한글");
> [ㅎ, ㅏ, ㄴ, ㄱ, ㅡ, ㄹ]

‘한글’을 입력할 경우에는 한 글자씩 분리해서 최종적으로는 ‘[ㅎ, ㅏ, ㄴ, ㄱ, ㅡ, ㄹ]’을 얻을 수 있다.

자소 결합 (assemble)

자소 리스트를 입력하면 조합해서 글자로 변환해준다.

public String assemble(List\<String\> jasoList)

Samples :

jasoList.add("ㅎ");
jasoList.add("ㅏ");
jasoList.add("ㄴ");
jasoList.add("ㄱ");
jasoList.add("ㅡ");
jasoList.add("ㄹ");

String hangul = HangulParser.getInstance().assemble(jasoList);
> 한글

‘[ㅎ, ㅏ, ㄴ, ㄱ, ㅡ, ㄹ]’을 입력값으로 넣어주면 결합해서 ‘한글’을 얻을 수 있다.

Proguard 난독화와 Crash Report Tool로 이슈 디버깅

프로젝트를 운영(유지보수)하면서 가장 중요하면서도 가장 기본이 되는 작업이 버그 리포트이다. 여기에선 Crash Report Tool로 Crashlytics를 사용했다. 그리고 회사 또는 팀의 정책, 작게는 개인에 따라 코드의 중요성이 높다면 난독화를 고민하게 되고 안드로이드에서는 무료인 Proguard 적용을 고려해보게 된다. (코드의 중요성이 낮더라도 한번 경험해보는 것도 좋다고 생각함) 하지만 난독화된 앱을 배포한 후 발생한 이슈를 디버깅하기 위해 Crash Report 콘솔에서 확인하면 이상한 문자를 접하게 된다.

Proguard 적용 전 이슈 발생했을 때

난독화가 되지 않은 앱을 디컴파일 해보면 소스 코드가 정상적으로 확인할 수 있다. 포함된 Android Support Library도 난독화가 되지 않은 상태이다.

그리고 앱에서 이슈를 발생시켜 보면 특정 함수에서 발생했으며 어떤 파일의 몇 번째 줄에서 발생했는데 ‘(MainActivity.java:21)을 통해서 알 수 있다.

public void forceCrash(View view) {
    throw new RuntimeException("This is a crash");
}

이슈는 다음과 같이 RuntimeException을 버튼 클릭 때 발생시키도록 만들었다.

Proguard 적용하기

프로젝트 생성 시 기본적으로 다음과 같이 build.gradle파일에 빌드 설정으로 다음과 같이 된다.

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

그리고 배포 시 Proguard 적용하기 위해서는 minifyEnabled를 true로 변경하면 된다.

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

ProGuard는 릴리스 모드(release)에서 빌드할 때는 적용되지만, 디버그 모드(debug)에서는 적용되지 않는다.

proguard를 적용해서 Signed APK파일을 만들고 디컴파일 했을 경우에는 위처럼 패키지부터 소스 파일까지 모두 특정 알파벳으로 난독화가 된 것을 확인할 수 있다.

앱에서 이슈를 발생시켜서 Crashlytics 콘솔에서 확인해보면 위에서 확인했던 이슈가 발생한 파일과 라인 수 부분이 Unknown Source로 변경되었다. 간단한 샘플 프로젝트이기 때문에 코드로 디버깅이 가능하지만 큰 규모의 프로젝트에서는 디버깅하는데 어려움을 겪을 수 있다.

그래서 proguard-rules.pro에 Proguard Rule을 추가하자.

-keepattributes SourceFile,LineNumberTable

다시 Proguard가 적용된 APK파일 생성하고 이슈를 발생 시켜보자.

Crashlytics를 확인하면 Unknown Source 대신에 이슈가 발생한 파일과 라인 수를 확인할 수 있다. 그리고 출력된 stack trace 오류에서 난독화된 패키지가 무엇인지 알고 싶다면 /outputs/mapping/release/mapping.txt 파일을 열어보자.

android.support.v4.app.Fragment -> android.support.v4.a.t:
    android.support.v4.util.SimpleArrayMap sClassMap -> aa
    java.lang.Object USE_DEFAULT_TRANSITION -> a
    int mState -> b
    android.view.View mAnimatingAway -> c
    int mStateAfterAnimating -> d
    android.os.Bundle mSavedFragmentState -> e
    android.util.SparseArray mSavedViewState -> f
    int mIndex -> g
    java.lang.String mWho -> h

Fragment가 속한 패키지는 android.support.v4.a.t로 변경된 것을 확인할 수 있으며 이에 속해 있는 변수나 함수들이 어떻게 변경되었는지 또한 확인할 수 있다. 난독화를 했다면 mapping.txt 파일은 보관해두고 만약 난독화된 코드의 stack trace를 디코딩하고 싶다면 mapping.txt를 이용해서 가능하기 때문이다.

retrace.bat -verbose mapping.txt stacktrace.txt > out.txt

out.txt파일에는 난독화된 코드의 stacktrace.txt가 해석된 stack trace가 출력된다.

ProGuard outputs the following files after it runs:

dump.txt
Describes the internal structure of all the class files in the .apk file
mapping.txt
Lists the mapping between the original and obfuscated class, method, and field names. This file is important when you receive a bug report from a release build, because it translates the obfuscated stack trace back to the original class, method, and member names. See Decoding Obfuscated Stack Traces for more information.
seeds.txt
Lists the classes and members that are not obfuscated
usage.txt
Lists the code that was stripped from the .apk

Proguard 사용하는 규칙

Proguard Manual – Proguard 사용 메뉴얼

-dontwarn [class_filter]
Specifies not to warn about unresolved references and other important problems at all. The optional filter is a regular expression; ProGuard doesn’t print warnings about classes with matching names. Ignoring warnings can be dangerous.
keep [,modifier,…] class_specification
Specifies classes and class members (fields and methods) to be preserved as entry points to your code.
-keepattributes [attribute_filter]
Specifies any optional attributes to be preserved. The attributes can be specified with one or more -keepattributes directives. The optional filter is a comma-separated list of attribute names that Java virtual machines and ProGuard support. Attribute names can contain ?, *, and ** wildcards, and they can be preceded by the ! negator.
-assumenosideeffects class_specification
Specifies methods that don’t have any side effects (other than maybe returning a value). In the optimization step, ProGuard will then remove calls to such methods, if it can determine that the return values aren’t used. With some care, you can also use the option to remove logging code.

Proguard를 적용하고 Signed Apk를 생성할 때, 가끔 발생하는 이슈들이 있는데, 그 중 하나가 Referenced Class를 찾을 수 없어서 IOException이 발생했기 때문이죠.

오류 메시지를 보면 Please correct the above warnings first로 발생한 Warning들을 고쳐달라고 합니다.

Proguard 적용 중에 왜 Warning이 발생했는지 궁금하시면 Problem while processing에서 확인할 수 있어요. 이때 위에 있는 규칙들 중에서 맞는 규칙을 찾아 proguard-rules.pro에 추가해준다.

-dontwarn okio.**

위의 이슈는 okio 패키지에 있는 파일들에 대해선 Warning을 뜨지 않도록 다음과 같이 설정을 해주면 된다.

-keep class com.example.classname
-keepattributes attribute

# examples
-keep class android.support.v4.app.** { *; }
-keep interface android.support.v4.app.** { *; }
-keep class android.support.v7.** { *; }
-keep interface android.support.v7.** { *; }

만약에 Class 변환 중에 이슈가 발생하거나 외부 라이브러리의 Proguard 적용을 원하지 않을 경우, 해당 패키지의 파일들을 Proguard가 적용되지 않도록 설정이 가능하다. 구글 라이브러리의 경우, Proguard가 불필요하기 때문에 Proguard적용에서 제외시켜주는게 좋다. 그리고 보통은 외부 라이브러리를 추가할 때, Proguard 적용시 주의사항으로 규칙들을 제공해주고 있으므로 잘 확인해서 추가만 해준다면 크게 문제가 발생하지 않는다.

decompile apk (APK 디컴파일하기)

가끔 진행하는 앱의 코드 난독화 확인을 위해서 기록해 두려고 한다. APK 디컴파일(Decompile)을 악용하지 말았으면 한다. 안드로이드 애플리케이션이 컴파일되면 .dex 파일이 생성된다. dex는 Dalvik Executable로 Dalvik에서 실행할 수 있는 파일이며 컴파일된 코드 파일을 의미한다.

Download dex2jar tool

1. dex파일을 jar(.class files)로 변환하기 위해서 dex2jar를 다운로드 한다.

2. Signed APK 파일의 확장명을 apk에서 zip으로 변경한다.

3. zip 압축을 풀면 classes.dex 파일을 다운받은 dex2jar 폴더로 복사한다.

$ ./d2j-dex2jar.sh classes.dex
sh: ./d2j-dex2jar.sh: Permission denied

4. 위처럼 dex2jar.sh 실행하면 권한 거부로 실행할 수 없게 된다.

$ chmod 764 *.sh
$ ./d2j-dex2jar.sh classes.dex
dex2jar classes.dex -> ./classes-dex2jar.jar

실행 권한을 변경한 후 다시 실행하면 classes-dex2jar.jar 파일이 생성된다.

Download JD_GUI

JD(Java Decompiler)-GUIjar파일 안에 있는 class파일들을 보기 위해서 다운로드 한다.

생성된 classes-dex2jar.jar를 열면 모든 class파일들을 볼 수 있다.

Slack Integration with Gitlab (Gitlab & Slack 연동하기)

It’s super easy way to enable Slack Integration, first you have to create new channel and then create an Incoming WebHooks on slack.

Create Webhook URL

Go into this following link with your teamdomain on Slack :

https://your_teamdomain.slack.com/services/new/incoming-webhook

Select your channel or create new channel that you want to send notifications and Click Add Incoming WebHooks integration.

You can get Webhook URL for sending data to Slack and also change curtomize name or icon.

These steps are all on Slack for integrating Gitlab. And now you have to use webhook url which gets from slack.

Enter the Webhook URL

Open your Gitlab and find Services right top of menu.

You can find Slack among the project services at the bottom.

Make Active checked and input webhook url from Slack and username, channel that you want. Save changes.

Finally, you will get this notification on your Slack chaneel.

failed to find target with hash string ‘android-XX’ in Jenkins CI

compileSdkVersion, minSdkVersion, and targetSdkVersion

First of all, read this ‘Picking your compileSdkVersion, minSdkVersion, and targetSdkVersion’ to know what these mean for building your project.

compileSdkVersion, minSdkVersion, and targetSdkVersion come in: they control what APIs are available, what the required API level is, and what compatiblity modes are applied, respectively. – Ian Lake’s blog

I’ve updated android latest compileSdkVersion, targetSdkVersion and buildToolsVersion on my Android project a couple of days ago, installing the latest version of the Android platform as android-24 and buildTools as 24.0.1.

android {
    compileSdkVersion 24
    buildToolsVersion '24.0.1'

    defaultConfig {
        targetSdkVersion 24
….

What went wrong :

But Jenkins gives this error through slack-plugin as below.

Jenkins Bot Message on Slack

Check Console Output on Jenkins

It failed to find android sdk 24 in android-sdk-linux directory because compileSdkVersion sets 23 to the newest API 24. So you can check list installed SDK package so far via command line.

$ cd /opt/android-sdk-linux/platforms
$ ls
android-10  android-15  android-16  android-17  android-18  android-19  android-20  android-21  android-22  android-23  android-8

You will understand it’s no android-24 folder, to compile app by API level 24 so let’s add android-24 to platforms and also Android SDK Build-tools, version 24.0.1. See the package list for Android SDK Tools, Android SDK Platform-Tools, Android SDK Build-tools, Google Play services, Google Admob and etc.

How To Update SDK

$ cd /opt/android-sdk-linux/tools
$ ./android list sdk --all
Packages available for installation or update: 156
   1- Android SDK Tools, revision 25.1.7
   2- Android SDK Tools, revision 25.2.1 rc1
   3- Android SDK Platform-tools, revision 24.0.1
   4- Android SDK Build-tools, revision 24.0.1
   5- Android SDK Build-tools, revision 24
   6- Android SDK Build-tools, revision 23.0.3
   7- Android SDK Build-tools, revision 23.0.2
   8- Android SDK Build-tools, revision 23.0.1

………

  29- SDK Platform Android 7.0, API 24, revision 2
  30- SDK Platform Android 6.0, API 23, revision 3
  31- SDK Platform Android 5.1.1, API 22, revision 2
  32- SDK Platform Android 5.0.1, API 21, revision 2
  33- SDK Platform Android 4.4W.2, API 20, revision 2
  34- SDK Platform Android 4.4.2, API 19, revision 4
  35- SDK Platform Android 4.3.1, API 18, revision 3

……….

 108- Google APIs, Android API 23, revision 1
 109- Google APIs, Android API 22, revision 1
 110- Google APIs, Android API 21, revision 1

Now we need to install No.29, ‘SDK Platform Android 7.0, API 24, revision 2’, to build in Jenkins successfully. So try to install by commend line as below.

$ sudo ./android update sdk -a -u -t 29

Try build it again in Jenkins. Finally, the problem was fixed and build was finished successfully.

CachePot으로 Activity 또는 Fragment 사이 간단한 데이터 통신

Java에서 흔히 객체로 데이터 통신 하기 위해 객체에 Serializable 인터페이스를 상속 받은 뒤 직렬화된 객체를 바이트 단위로 분해하여 전송한다. Android에서도 마찬가지로 Serializable을 사용하긴 하지만 보다 Parcelable을 많이 사용하고 있다. 하지만 Parcelable을 사용하기 위해서는 객체에 선언된 데이터가 추가 될 때마다 writeToParcelreadFromParcel을 통해 Parcel 객체에 읽고 쓰는 작업을 추가해야 데이터 전송이 가능하다. CachePot은 간단한 앱을 만들거나 Intent를 통한 다른 애플리케이션과 통신이 없다면 간단하게 데이터 캐시를 할 수 있는 인스턴스가 있으면 좋겠다고 생각되어 Generic을 사용해 간단하게 만든 안드로이드 라이브러리이다.

Download

현재 Gradle을 사용하는 중이라면 build.gradle에 아래를 추가하자.

repositories {
      jcenter()
}

dependencies {
      compile 'com.github.kimkevin:cachepot:1.0.0'
}

How To Use?

동기식 데이터 전달하기

  • Between Activity and A ctivity
  • Between Activity and Fragment
  • Between Fragment and Fragment

데이터가 메모리에 올라가기 때문에 결국은 사용자 인테페이스를 구성하는 Activity와 Fragment 등 아무대서나 동기적으로 데이터 전달이 가능하다.

1. Model 객체 전달

새로운 화면 생성 전 데이터 저장하기

KoreanFood foodItem = new KoreanFood(1, "Kimchi", "Traditional fermented Korean side dish made of vegetables");
CachePot.getInstance().push(foodItem);

새로운 화면 생성 후 데이터 가져오기

public class MainFragment extends Fragment{
    private KoreanFood foodItem;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        foodItem = CachePot.getInstance().pop(KoreanFood.class);
    }
}

2. Collection 또는 Map 객체 전달

새로운 화면 생성 전 데이터 저장하기

List<KoreanFood> foodItems = new ArrayList<>();
foodItems.add(new KoreanFood(1, "Kimchi", "Traditional fermented Korean side dish made of vegetables"));
foodItems.add(new KoreanFood(2, "Kkakdugi", "A variety of kimchi in Korean cuisine"));
CachePot.getInstance().push(foodItems);

새로운 화면 생성 후 데이터 가져오기

List<KoreanFood> foodItems = CachePot.getInstance().pop(ArrayList.class);

ArrayList나 HashMap, Stack, LinkedList 등과 같이 구현 클래스도 일반적인 모델 데이터를 전달하는 것과 같은 방식으로 데이터 전송이 가능하다.

비동기식 데이터 전달하기

ViewPager에서 position별로 Fragment에 객체 전달

요즘 앱에서 흔하게 ViewPagerFragmentStatePagerAdapter를 사용하고 있다. 그리고 Adapter에서는 getItem(int position)이 호출 될 때 Fragment를 생성하게 되는데 새로운 Fragment가 생성되고 CachePot 인스턴스에 저장된 데이터를 가져오기 전에 새로운 데이터를 캐시하게 되어 데이터를 가져올 때 오류가 발생하게 된다. 이를 방지하기 위해서 Fragment의 postiion을 함께 저장할 수 있는 기능을 제공한다.

Framgnet 생성 전에 position과 데이터 저장하기

private class PagerAdapter extends FragmentStatePagerAdapter {
    ...
    public Fragment getItem(int position) {
        CachePot.getInstance().push(position, foodItems.get(position));
        return FoodFragment.newInstance(position);
    }
}

생성된 Fragment에서 데이터 가져오기

public static FoodFragment newInstance(int position) {
    FoodFragment fragment = new FoodFragment();
    Bundle args = new Bundle();
    args.putInt(ARG_POSITION, position);
    fragment.setArguments(args);
    return fragment;
}
...
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
        final int position = getArguments().getInt(ARG_POSITION);
        koreanFoodItem = CachePot.getInstance().pop(position);
    }
}

‘Where is CEO?’ == ‘사장 어딨노?’ 사내 해커톤 후기

얼마전 플리토 사내에서 첫 번째 해커톤을 열었다. 보통 해커톤에 참여 할 경우에는 한 달 전부터 만들어 보고 싶은 것들을 꼭 팬으로 노트에 낙서하듯 적어본다. 딱히 이유는 없지만 팬으로 적었을 때에는 머리 속에서 더 오래 남아있는 것 같은 느낌이 들어서다.

그래서 이번은 회사 프로젝트는 아니지만 만들면 재밌을 것 같았던 ‘Where is Simon(CEO)?’로 결정했다. 플리토 청담 오피스는 현재 40명에 가까운 인원이 총 3개의 층을 사용하고 있고 사내 메신저로 가끔 누가 어디에 있는지 묻는 경우가 있어서 이를 확인할 수 있는 페이지가 있으면 좋겠다고 생각했었다. 가장 빈번히 찾는 대상은 어느 회사에서나 공공의 타겟 바로 대표님이다. 기본 아이디어는 ‘층마다 있는 무선 공유기(AP)에 접속한 직원들을 층별로 보여주자‘로 시작했다.

<img class=”aligncenter” src=”http://kimkevin.net/wp-content/uploads/2016/07/kk_160504_08.jpg” />

1. Mornitoring Agent – 층별 AP에 접속한 직원들 리스트 가져오기

가장 기본적으로 먼저 AP에 접속한 단말기의 정보를 가져와야 프로젝트가 시작될 수 있기 때문에 여러 방법들을 시도했다.

ipTIME 관리자 페이지로 리스트 가져오기

<img class=”aligncenter” src=”http://kimkevin.net/wp-content/uploads/2016/07/kk_160504_07.png” />

가장 먼저 iptime의 관리자 페이지에서 내부 네트워크의 접속 주소 정보를 리스트로 가져올 수 API가 있을까 했지만 실패다. 그래도 AP에 접속한 직원들의 MAC 주소 리스트를 쉽게 만들 수 있었다.

특정 AP에 접속한 기기정보 명령어로 가져오기

해커톤이 있는 전날 밤, 어떻게 직원들의 리스트를 가져올 수 있을지 찾아보다가 페이스북에 글을 남겼고 Bruce Lee님이 여러 링크를 댓글로 남겨주셨다.

<img class=”aligncenter” src=”http://kimkevin.net/wp-content/uploads/2016/07/kk_160504_01.png” />

그 중 하나가 AP에 연결된 클라이언트들의 리스트를 가져 올 수 있는 arp 명령어를 발견 했다.

$arp -a
? (192.168.2.1) at 90:9e:33:38:d3:98 on en0 ifscope [ethernet]
? (192.168.2.13) at (incomplete) on en0 ifscope [ethernet]
? (192.168.2.20) at c:8b:fc:b1:bd:ea on en0 ifscope [ethernet]
? (192.168.2.43) at d0:53:29:c7:8a:4b on en0 ifscope [ethernet]
? (192.168.2.54) at (incomplete) on en0 ifscope [ethernet]
? (192.168.2.63) at 44:12:10:b7:12:22 on en0 ifscope [ethernet]
? (192.168.2.71) at 28:5a:eb:a2:ed:94 on en0 ifscope [ethernet]
? (192.168.2.83) at d8:1c:72:d9:ce:e on en0 ifscope [ethernet]
? (192.168.0.112) at (incomplete) on en0 ifscope [ethernet]

첫 번째는 AP에서 할당한 내부 IP정보와 할당 받은 MAC 주소를 가져올 수 있다. PC뿐만 아니라 스마트폰, 태블릿이 AP에 접속하면 리스트에 포함이 된다. 여기에서 리스트의 가장 상단에 있는 192.168.7.1은 AP의 MAC 주소이다. 하지만 arp 명령어로 테스트를 해보면서 몇 가지의 이슈가 발생했다.

  1. AP의 2.4G, 5G는 MAC 주소가 다르다.
  2. MackBook은 얼마 후 ‘incomplete’상태로 변경되어 리스트에서 없어지고 Windows 노트북은 계속해서 유지된다.
  3. arp에 나오는 기기정보들은 내 MacBook이 AP에 접속한 후에 접속한 기기들만 포함된다.

해커톤은 24시간 내 개발을 해야하기 때문에 완벽함보다는 가능성이 있는 부분을 더욱 구체화 시키는 편이 더 좋다. 그래서 추후에 위의 3가지 이슈를 해결할 수 있는 다른 방법은 실제로 서비스할 수 있을 때 찾아서 수정하기로 한다.

AP에 접속한 단말 정보 주기적으로 가져오기
$arp -a | awk '{print $2, $4}' | sed -e 's/(/''/g' | sed -e 's/)/''/g'
192.168.2.1 90:7e:32:33:d2:x1
192.168.2.13 incomplete
192.168.2.20 c:8b:fc:b1:bd:ea
192.168.2.43 d0:53:29:c7:8a:4b
192.168.2.54 incomplete
192.168.2.63 44:12:10:b7:12:22
192.168.2.71 28:5a:eb:a2:ed:94
192.168.2.83 d8:1c:72:d9:ce:eg
192.168.0.112 incomplete

필요한 데이터를 추출한 다음에 incomplete된 Mac 주소와 AP의 Mac 주소를 제외하고 웹 서버에 층 정보와 Mac 주소 리스트를 등록한다. 그리고 새로운 기기가 Wifi가 연결될 때 자동으로 웹 서버에 등록할 수 없기 때문에 setInterval 로 10초당 한번씩 확인해서 갱신되었을 때마다 웹 서버에 등록을 시켜주면서 최신으로 업데이트하도록 구현하면 된다.(사실 깊게 생각 안 했고 cronJob으로도 가능할 듯)

Mornitoring Agent에서 층 구별하기

ap = {
"90:7e:32:33:d2:x1" : "6",
"90:8f:31:33:d1:93" : "7"
}

층 정보는 arp 명령어의 결과에서 받은 ‘192.168.2.1 90:7e:32:33:d2:x1’의 MAC 주소와 Agent에 등록된 AP MAC 주소를 비교해서 몇 층에 위치한 모니터링 서버인지 구별할 수 있다. 위를 비교해보면 결과물은 6층 AP임을 알 수 있다.

2. Express for Node.js – 웹 서버 만들기

이번 프로젝트는 간단한 Rest API와 웹 서버가 필요했기 때문에 빠르게 구현할 수 있는 Express를 사용했다.

직원들이 사용하는 단말의 MAC 주소 리스트 미리 등록하기
flittorians = {
"78:31:c0:c3:e4:13" : "김케빈",
"54:4d:80:94:7e:c1" : "김케빈 아이폰",
...
}

웹 서버에서는 직원들의 Mac 주소 리스트를 가지고 있으므로 층 별로 모니터링 하는 Agent에서 변화를 알려주면 층 별로 매칭되는 직원들의 최신 리스트를 가지고 있을 수 있게 된다. 그리고 이 정보가 필요할 땐 JSON으로 보내준다.

Slack Bot 연동하기

<img class=”aligncenter” src=”http://kimkevin.net/wp-content/uploads/2016/07/kk_160504_05.jpg” />

Bruce Lee님의 이번 주제는 ‘슬랙봇 만들기’이다. 새벽이 되었을 때 층 별 API를 요청을 하셨고, 간단한 작업이라 만들어 드렸더니 본인이 만들던 슬랙봇에 연결을 했다. 서로 말하고 시작한 프로젝트가 아니였는데 재밌다. 때로는 독립적인 작은 덩어리가 서로 연결되어 큰 덩어리가 된다. 이런게 바로 개발의 행복 그리고 소소한 재미를 느낄 수 있었다.

3. ejs & bootstrap 으로 웹 페이지 만들기

<img class=”aligncenter” src=”http://kimkevin.net/wp-content/uploads/2016/07/kk_160504_06.jpg” />

웹 프론트 작업은 새벽 4시정도 부터 시작할 수 있었는데 이번 해커톤은 혼자지만 생각보다 진행이 잘되어 발표시간(12시)까지는 8시간을 남겨놓은 상태라 여유로운 편이다. 웹페이지는 express 만들 때 탬플릿으로 ejs를 설정했기 때문에 간단하게 bootstrap을 사용하여 간단한 table로 구성했다. 처음에는 스타일 없이 작업을 하고 시간적 여유가 있다면 간단히 스타일을 주는건 결과물을 보는 청중에게 더욱 인정받는 결과물이 된다. 여기에서 table은 bootstap-table을 사용해서 간단히 스타일을 적용했다.

정리하면서..

그리고 국내에서든, 국외에서든 모든 해커톤에서는 시간 관리가 제일 중요하다. 아무리 뛰어난 아이디어라고 하더라도 발표 때 결과물이 없다면 또는 만들었지만 정상적으로 동작하지 않는다면 심사위원들에게 인정 받지 못하기 때문에 심사위원에게 무엇을 어떻게 어필할지 있을지 마지막까지 고민해야한다. 즉, 시간이 걸리는 작업은 되도록 나중에 하는 것이 낫다라는게 개인적인 생각이다. 새로운 팀빌딩으로 작업을 하기 때문에 밤새하지 않더라도 적어도 프로토타입을 보여줄 수 있을 정도면 충분한 것 같다

SlidingIconTabLayout 탭에 아이콘 넣기

구글에서는 현재 많은 샘플 코드들을 개발자가 전반적인 프로젝트 구조나 리소스, 코드 파일들을 볼 수 있도록 제공해주고 있다. 그 중에서 Material Design이 안드로이드 5.0에서 소개되면서 UI관련 소스들을 많이 추가되었고 많은 프로젝트에서 SlidingTabLayoutSlidingTabStrip를 사용한다.

프로젝트에서 ViewPager를 이용하게 되면 페이지의 이름을 보여주기 위한 레이아웃이 필요하게 되는데, 만약 탭의 이름을 텍스트로 보여주고 싶다면 구글에서 제공해주는 위의 두 파일로 충분히 구현할 수 있다. 하지만 텍스트를 사용할 경우, 다국어 지원을 하게 되면 여러가지 이슈가 발생하게 되는데 예를 들면, 한국어를 영어로 바꿀 경우에는 대부분 글자수가 많아진다. 이를 해결하기 위해서 흔히 탭의 이름을 아이콘으로 대체하고 ActionBar의 타이틀에 페이지의 이름을 노출하고 있다. 텍스트와 아이콘을 모두 지원하기 위해서 SlidingTabLayout에 아이콘도 추가할 수 있도록 간단한 인터페이스를 추가해서 오픈소스 SlidingIconTabLayout로 만들었다.

SlidingIconTabLayout

Download

repositories {
  jcenter()
}

dependencies {
  compile 'com.github.kimkevin:slidingicontablayout:1.0.0'
}

SlidingTabLayout.TabIconProvider 추가하기

SlidingIconTabLayout.TabIconProvider는 안드로이드 화면상에 노출되는 다수의 Fragment를 관리하기 위한 어답터이다. Fragment에 필요한 데이터나 리소스를 위치(Position)에 맞게 그려주기 위해서 FragmentPagerAdapter를 상속 받아야 하기 때문에 이때 아이콘을 텍스트 대신에 보여주기 위한 인터페이스가 필요하다.

public interface TabIconProvider {
    int getPageIconResId(int position);
}

SlidingTabLayout 수정

TabIconProvider를 상속받고 있다면 텍스트 대신 아이콘을 사용할 수 있다.

if (TextView.class.isInstance(tabTitleView)) {
    ((TextView) tabTitleView).setText(adapter.getPageTitle(i));
} else if (ImageView.class.isInstance(tabTitleView) && adapter instanceof TabIconProvider {
    TabIconProvider mTabIconProvider = (TabIconProvider) adapter;
    tabTitleView.setBackgroundResource(mTabIconProvider.getPageIconResId(i));
}

사용할 FragmentTabIconProvider를 추가해서 필요한 아이콘을 추가해주면 해당 Tab의 아이콘으로 노출된다.

모알보알 스쿠버다이빙, 삶의 균형을 위한 중성부력

새로운 도전! 스쿠버다이빙

scuba_diving_01

올해 8월 초, 5일간 필리핀 세부를 다녀왔다. 예전부터 꼭 하고 싶었지만, 2년 전 아일랜드 호핑으로 느꼈던 세부의 짠물에 대한 공포때문에 미뤄두고 있었던 스쿠버다이빙, 오픈워터에 도전하기로 마음 먹었다. 장소는 세부시티에서 가까운 막탄보다는 깨끗해서 투명한 바다, 많은 종류의 산호초와 사람만한 거북이, 화려한 색감의 열대어를 볼 수 있는 다이빙 포인트로 유명한 모알보알이다. 강사 말로는 예전에는 고래상어가 출몰해서 섬을 몇 바퀴 돌면서 머물다가 갔다고 했다.

막탄 공항에 도착하다

scuba_diving_02

막탄 공항을 빠져나와 택시타고 서부터미널로 향했다. 택시비는 약 500페소 정도면 갈 수 있다. 택시를 이용할 때는 잔돈이 있는게 좋다. 기사 아저씨들은 돈만 받으면 잔돈이 없다는 뻔한 말들을 하기 시작한다. 그래도 택시에 내리지 말고 버티다 보면 슬그머니 돈을 꺼내기 시작하면서 재미가 쏠쏠한 밀당이 시작된다. 터미널 안은 매표소가 별도로 없고 버스타고 출발하면 아저씨가 표를 주는데, 버스 이용요금은 116페소 한국돈으로 3300원 정도다. 만약 터미널이나 공항에서 벤이나 택시를 이용하게 되면 한국돈으로 6만원 ~ 10만원 든다고 한다. 여행하면서 로컬 버스를 타보는 것도 좋은 경험이 될 것 이라고 생각한다면 이것은 고생이다. 이동 시간이 4시간이라는 것만 빼고는… 세부시티로 돌아오는 길에는 약5시간 가까이 걸렸던 것 같다.

수심 30m에서 찾아온 나의 균형

scuba_diving_03

나에게 있어서 여행은 휴가, 휴식의 의미보다는 여행을 통한 배움을 위해서다. 그래서 꼭 1년에 한번 이상은 국내나 해외를 여행한다. 그럼 이번 오픈워터를 통해서는 무엇을 배웠을까? 이번 오픈워터 도전을 통해서 배운건 다른 여행보다 의미가 컸다. 스타트업 로켓에 탑승하기 전까지, 방세를 내며 여름 방학동안 공모전 공장을 운영해봤었고, 개발자에게는 좋은 근무환경을 제공하는 IT회사에서 근무도 해봤었다. 그리고 짧은 시간이었지만 미국에서 스타트업을 경험도 해보았다. 짧다면 짧고 길다면 긴 시간동안 많은 경험을 했던 만큼 나 자신에 대해 고민하고 생각해보면서 내놓은 결론이 있다. 삶에 있어서 가족, 친구, 지인, 회사, 꿈도 중요하지만 모든 곳에서 꼭 있어야 하는 무엇보다 중요한 바로 균형이라고 생각한다. 그것을 몸으로 느껴볼 수 있었던 경험이 바로 하루 3 ~ 5시간씩 물 속에서 있을 때였다.

두려움을 극복하라

scuba_diving_04

스쿠버다이빙에서 중성부력이라는 용어가 있다. 부력(위로 올라가려는 힘) 중력의 힘(아래로 내려가려는 힘)이 동일한 상태로, 물 속에서 뜨지도 가라 앉지도 않는 상태이며, 다이빙을 하면서 이 상태로 자신의 호흡만으로 즉, 폐 안의 공기를 이용해 수심을 유지하는 방법이다. 밸트에 납 때문에 중력으로 쉽게 밑으로 내려갈 수 있고, 부력조절기(BC)로 불리는 조끼같이 생긴 장비 때문에, BC에 공기를 넣어 중력의 반대의 힘 부력을 만들 수 있다.

스쿠버다이빙을 편하고 즐겁게 하기 위해서는 중성부력을 맞추지 못하면 다이빙하고 있는 수심이 낮아졌다 높아졌다하면서 이퀄라이징을 자주 해야하기 때문에 귀에 무리가 가고 귀가 아프기도 한다.

이런 이유보다 쉽게 중성부력을 잡지 못하는건 바로 내 마음속에 있는 두려움 때문이다. 두려움이 내 몸에 힘이 들어가게 만들고 힘이 들어가면 호흡이 가빠지다 보니 불안해지기 때문에 상황대처능력도 떨어지며 바로 앞에 있는 아름다운 바다를 보는 시야도 줄어들게 된다.

두려움 이겨내는 중성부력

scuba_diving_05

하루에 2 ~ 3시간 동안 바다 안에 있으면서 두려움을 잊기 위해서 많은 시도를 해봤지만 언제 어디서 나올지 모를 상어에 대한 걱정(강사도 실제로 큰 상어를 본적은 없다고 한다), 언제 마스크가 벗겨질지 모를 상황에 대한 걱정, 강사를 놓치진 않을까 하는 걱정, 아픈 귀에 대한 걱정, 공기통에 남은 공기의 양에 대한 걱정 등 물속에서 생각해야하는 것들이 많았다. 여러 환경적 요소들로 인해서 몸에는 힘이 잔득 들어가고 그로 인해 몸은 가라앉고, BC에 공기를 넣었다 빼는데 바빴던 것이다. 앞을 볼 시간은 줄어들었고 집중력은 떨어졌다. 즉 내가 보고 싶은 바다를 편안하게 보기 위해 필요했던건 바로 균형이었다.

그리고 문득 바다속에서 내가 처한 상황이 우리의 상황과 비슷하다는 생각이 들었다. 정신적으로 여유를 가지고 몸에는 힘을 뺀채로 넓은 시야로 눈 앞에 있는 문제들을 해결해야 하는데, 사소한 문제들로 인해서 균형이 깨져서 힘들어 하고 있진 않을까? 그게 금전적 문제, 직업, 직장이든 말이다.

직장인이라면 누구나 불안함을 지니고 산다. 하지만 생각해보면 그 불안감은 자기 자신이 만드는 것이다. 옆에서 상어가 오진 않을까 강사를 놓치진 않을까, 결국 나 자신이 만든 불안감으로 부터 시작되었다. 외부적 요인이 몸에 힘이 들어가게 했고 바다속으로 가라 앉도록 했던 것이다. 결국 부력(여유)이 중력(불안감)을 받쳐주지 못하기 때문이다. 자신이 여유를 생산하고 불안을 잘 컨트롤 할 수 있다면 그게 바로 균형일 것이다. 우리가 재미있고 행복하게 지내기 위해 가장 필요한 것은 균형을 맞추기 위한 중성부력이 아닐까 생각한다.