MAC에서 비밀번호 없이 ssh 접속하기(config)

최근 맥북을 수리하면서 공개키(id_rsa.pub), 개인키(id_rsa) 그리고 호스트 정보가 있는 config 파일을 백업하지 않아 재설정하였다. 기록하기 위해 정리하는 것이지만 필요한 내용은 비밀번호 없이 ssh 접속을 위해 호스트 정보를 기록해두는 config 파일이다.

ssh 원격 접속

-> ssh user@host(domain|ip_address) -i identity_file

user : 원격 서버의 계정
host : 도메인이나 IP주소 (예 : 128.XXX.XX.XX, kimkevin.net)
-i : 개인키 파일 경로

원격 서버 ssh 접속시 인증에 사용할 개인키와 로그인 계정을 포함하는 기본적인 명령어이다.

비밀번호로 로그인

-> ssh dev@kimkevin.net
dev@kimkevin.net’s password:
...
Last login: Sat Jun 27 03:05:25 2020 from 210.94.83.207
dev@kevin:~$

비밀번호 없이 user@host로 로그인

vi ~/.ssh/authorized_key

ssh-rsa ..............................................................
......................................................................
......................................................................

서버의 ssh 폴더 안에 authorized_key 파일을 생성하고 공개키를 등록하자.

-> ssh dev@kimkevin.net
...
Last login: Sat Jun 27 03:05:25 2020 from 210.94.83.207
dev@kevin:~$

서버에 클라이언트의 공개키를 등록해두면 비밀번호 없이 로그인이 가능하다.

-> ssh dev@kimkevin.net -i ~/.ssh/another_id_rsa
...
Last login: Sat Jun 27 03:05:25 2020 from 210.94.83.207
dev@kevin:~$

만약 서버마다 다른키로 접속한다면 -i 옵션을 사용해 개인키 경로를 전달하면 된다.

호스트 이름으로 쉽게 로그인

-> ssh dev
...
Last login: Sat Jun 27 03:05:25 2020 from 210.94.83.207
dev@kevin:~$

계정과 호스트 정보 없이 자신이 정의한 호스트 이름으로 쉽게 로그인이 가능하다.

-> vi ~/.ssh/config

Host kevin
    HostName kimkevin.net
    User dev
    IdentityFile ~/.ssh/id_rsa

config파일을 생성하고 chmod 440 ~/.ssh/config 다음처럼 read권한만 주도록 하자.

-> ssh kevin
...
Last login: Sat Jun 27 03:05:25 2020 from 210.94.83.207
dev@kevin:~$

접속하는 원격서버가 여러개라면 호스트 각각 등록해두고 사용하면 더욱 편리하게 ssh 접속이 가능하다.

Google I/O 알고 즐기기

Google I/O 2018에 다녀온 지 벌써 해가 지났다. 다음 달 5월 7일부터 9일까지 Google I/O 2019가 열린다. 어떤 글을 적어볼까 고민하다 올해 Google I/O 참석을 기다리거나 고민 중인 사람에게 도움이 될 만한 개인적인 생각을 적어보기로 했다.

Google I/O 2018에 대한 이야기는 작년 키노트에서 공개했던 Google AI에 대해 스타트업 얼라이언스에서 구글 I/O 톺아보기 주제로 발표했던 자료를 보면 좋을 것 같다.

먼 곳까지 ‘왜’ 가야 할까?

Google I/O 2018

Google I/O 행사를 경험하지 못한 개발자가 가끔하는 질문이다. 사람마다 생각의 차이는 있겠지만 내 생각에는 몰입에서 오는 두근거림, 바로 현장감이다. 우리가 좋아하는 스포츠를 TV가 아닌 티켓을 구매하고 땀 흘리는 선수들의 경기를 보러 경기장에 가는 이유다.

하루의 삼 분의 일을 안드로이드에 투자하는 개발자로서 컨퍼런스 행사장에 가서 땀 흘리며 제품을 만든 개발자들의 발표를 직접 보며 현장감을 느끼기 위해라고 생각한다.

Google I/O 즐기려면

관심 있는 주제 위주로

Google I/O 2018 Android

스케줄과 세션들이 전체 공개되면 자신의 스케줄을 만들 수 있도록 Google I/O 2019 안드로이드 앱홈페이지를 제공한다. 최대한 빨리 자신이 관심 있거나 듣고 싶은 세션을 신청해서 등록해야 컨퍼런스 기간 동안 편하게 들을 수 있다. (하지만 예약 줄도 긴 것은 함정)

Google I/O 2018 Waiting for Session

인기 있는 세션들은 예약이 완료되기 때문에 미리 신청하는 것이 좋다. 미리 신청하지 못하더라도 예약한 사람 먼저 입장한 후에 자리가 남으면 대기자도 입장할 수 있다. 예약한 사람이라도 시간을 놓치게 되면 자리가 없을 경우 입장이 불가하다.

여행도 함께

Google I/O 2018 Keynote

개인적으로는 컨퍼런스 참여 기간 동안 회사에서 휴가를 지원한다면 앞뒤로 연차를 사용해 여행을 가자. 해외 컨퍼런스 참여하는 몇몇 개발자들이 리모트로 코워킹스페이스에서 일을 하면서 여행을 하거나 컨퍼런스 이전 또는 이후에 근교 여행을 다니는 것을 보았다.

구글 I/O가 화요일부터 목요일까지라면 행사 전주에 도착해서 가까운 도시나 관광지를 다녀 오는 것을 추천한다. 왜냐하면 구글 I/O 하루 직전에 도착하면 시차 적응 실패로 세션을 듣는 동안 몸은 수면 상태가 될 가능성이 높기 때문이다. 집중하기 어렵고 행사를 즐기기 어렵다.

나같은 경우에는 작년(2018) 5월의 휴일에 맞춰서 샌디에고로 출국해서 로스앤젤레스까지 두 도시를 여행하고 행사기간 전날부터는 마운틴 뷰에 지내며 샌프란시스코의 짧은 여행과 함께 Google I/O를 즐겼다. (2~3일 정도는 시착적응으로 고생했음..)

이동은 쉽고 편하게

Lyfy

렌트 차량 없이 공항에서 숙소로 이동할 때는 UberLyft를 가격을 비교한 다음에 가격이 저렴한 서비스를 이용했다. 우버의 경우, 처음 서비스 이용 시 $15 프로모션 코드를 제공하기 때문에 $15 이상인 경우에 사용하면 좋다.

LimeBike

4년 만에 샌프란시스코에 도착해서 보니 길 중간중간에 전동 킥보드와 자전거가 많이 보였다. 가장 많이 보였던 서비스는 LimeBird이다. Bird의 경우 실제 캘리포니아 운전면허증을 등록해야 이용이 가능하기 때문에 Lime을 주로 편하게 이용했다.

Turo

차량 렌트의 경우, 개인 간 차량 공유 서비스인 Turo를 이용했다. 지인의 추천 코드($25)로 5일간 정말 싼 가격에 깨끗한 차를 빌려서 편리하게 사용했다. (Get $25 off your first trip 나의 공유 링크지만 친구나 지인을 통해 링크를 공유받을 수 있음)

행사 전날 워밍업

Netflix I/O Party

작년(2018)에는 Netflix I/O Party와 Intel’s Google I/O Day Zero Party가 있었고 나는 인텔 행사에 참석했다. 일단 파티 신청이 완료되면 Uber나 Lyft를 넉넉하게 사용할 수 있는 쿠폰을 제공해주기 때문에 이동은 걱정하지 않아도 된다.

Netflix I/O Party - Movidius

간략하게 대부분의 부스는 대부분 Intel Modivius라는 소형 딥러닝용 USB 드라이브를 이용한 실제 딥러닝을 사례를 공유했다.

Turo

행사 기념품과 간단히 저녁 식사 그리고 구글 I/O가 시작하기 전에 워밍업을 한다는 생각으로 참여하기 좋다.

재미있는 구경거리 SANDBOX

Google I/O 2018 Sandbox

샌드박스(SANDBOX)는 안드로이드부터 구글 프로젝트를 살펴볼 수 있는 장소이다. 세션 중간이나 시간을 만들어서 근처 샌드박스를 구경해보자. 여러 세션에서 안드로이드 Jetpack을 소개했던 Yigit Boyar는 발표 이후에 샌드박스에서 있어서 개발자의 질문에 친절하게 대답해주었다.

현지 회사 방문과 개발자 만나보기

Visited Company

마운틴뷰에 도착한 점심부터 출국하는 날까지 많은 현지 개발자들을 만났다. 지인이 현지에서 근무하는 개발자를 소개시켜주어 덕분에 회사에 초대 받아 점심을 먹으며 회사를 구경할 수 있었다. Google, Facebook, Lyft, Coupang 등 현지 개발자를 만나서 많은 대화를 통해 많은 정보를 얻을 수 있어서 개인적으로 너무 만족했다. 미국 IT회사의 개발문화나 어떻게 개발하는지에 대해 자세히 질문할 수 있는 기회였다. 숙소 제공과 다른 개발자들을 소개 시켜준 지인과 회사 구경과 점심을 제공해주신 분들에게 너무 감사하다.

Google I/O를 즐기기 위해 미리 관심있는 주제 위주로 세션을 예약하고 여행도 함께하면서 이동은 쉽고 편하게 공유 서비스를 잘 활용하고 행사 전날 워밍업을 위해 파티도 가보고 재미있는 구경거리 SANDBOX도 즐기고 현지 회사 방문과 개발자 만나보기를 해보면 뜻깊은 경험이 될 것 같다.

끝으로

구글 I/O를 다녀오고 무엇보다 Android Architecture Component에 대해 관심을 더욱 가지게 되었고 실제 적용 사례 및 개발 경험을 다른 개발자들에게 공유할 수 있는 계기가 되었다.

다음에 구글 I/O를 다시 참석하게 된다면 제품의 개발 과정과 경험 그리고 궁금증을 이야기 나눌 수 있는 샌드박스를 더욱 잘 활용하면 좋을 것 같다.

구구절절 내용이 길어졌지만 다녀오고 나서 나에게 필요한 핵심은 사실 영어 공부다.

Python UTC -6 to KST (UTC +9)

개발 환경

  • Python 3.6.5
  • PyCharm on Mac

KST(한국 표준시)는 UTC/GMT에서 +9, 9시간을 더한 시간과 같다. 흔한 케이스는 아니겠지만 글의 업로드 시간으로 다음과 같은 데이터를 받았다.

2019-04-14T02:14:30-06:00

한국은 UTC 기준 +9이기 때문에 -6은 한국 시각이 아닌 것을 알 수 있다. 중요한 정보는 아니지만 궁금하니 지도로 UTC -6은 어딘지 한번 구경해보자.

협정_세계시

가령, 미국 로스앤젤레스에 거주하는 사용자가 2019년 4월 14일 오전 8시 14분 30초경에 쓴 글이다. 이제 한국인에게 읽기 편한 KST로 변경하기 위해 Python을 사용해 시간 문자열을 객체로 변환해보자.

from datetime import datetime

time_str = '2019-04-14T02:14:30-06:00'
updated_time = datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z')
print(updated_time)

다음을 실행하면 아래의 결과가 출력된다.

ValueError: time data '2019-04-14T02:14:30-06:00' does not match format '%Y-%m-%dT%H:%M:%S%z'

지정한 시간 포맷이 맞지 않는다는 ValueError이다. 유명한 서비스의 시간 문자열의 포맷이 이상한 걸까?

time_str = '2019-04-14T02:14:30-06'
updated_time = datetime.strptime(time_str, '%Y-%m-%dT%H:%M:%S%z')
print(updated_time)

시간 문자열을 조금 변경해서 해봤지만 마찬가지다.

데이터와 포맷을 여러번 변경해본 결과, 원인은 %z이다.(%Z: Time zone name) 데이터의 포맷이 맞는지 확인하기 위해서 시간 관련된 데이터 교환을 다루는 국제 표준인 ISO 8601를 살펴보았다.

기술 중인 시간이 UTC보다 한 시간 앞선다면 (겨울 동안의 베를린 지역의 시간처럼), 지역 지정자(zone designator)는 “+01:00”, “+0100” 혹은 간단히 “+01″가 될 수 있다.
from: https://ko.wikipedia.org/wiki/ISO_8601

ISO8601에는 적합한 날짜 포맷이며 두 가지(“+01:00”, “+0100”)로 표현 가능하다. 다음으로 datetime.strptime 공식 문서를 확인해보자.
strptime: 날짜 및 시간 문자열을 특정 포맷 형태의 datetime으로 변경
strftime: 특정 포맷 형태의 datetime으로 변경

%z : UTC offset in the form +HHMM or -HHMM (empty string if the object is naive). (empty), +0000, -0400, +1030

위와 같이 %z 지시자는 ‘:’을 지원하지 않는다. (앗!) 정확한 원인을 찾은듯하다.

콜론(:)을 지원하는 오픈소스 라이브러리를 찾던지 아니면 문자열을 수정해서 해결해야 한다. 파이썬에 익숙하지 않기 때문에 학습을 위해서 후자를 선택했다.

print(updated_time)
print(updated_time.rsplit(':', 1))
print(''.join(updated_time.rsplit(':', 1))

>>> 2019-04-14T02:14:30-06:00
>>> ['2019-04-14T02:14:30-06', '00']
>>> 2019-04-14T02:14:30-0600

rsplit를 사용해 오른쪽에서 첫 번째 콜론(:)을 기준으로 문자열을 나눈 다음 문자열 배열을 다시 합쳐주면 원하는 문자열 ‘2019-04-14T02:14:30-0600’ 만들 수 있다. 그리고 다시 처음처럼 datetime을 생성하자.

kst_updated_time =
datetime.strptime(''.join(time_str.rsplit(':', 1), '%Y-%m-%dT%H:%M:%S%z')
print(kst_updated_time)
>>> 2019-04-14T02:14:30-0600

정상적인 datetime 인스턴스가 출력된다. 이제 한국 타임존으로 변경해서 문자열을 출력해보자.

from pytz import timezone

KST = timezone('Asia/Seoul')

print(kst_updated_time.astimezone(KST))
print(kst_updated_time.astimezone().tzinfo)
print(kst_updated_time.astimezone().strftime('%Y-%m-%d %H:%M:%S'))

>>> 2019-04-14 17:14:30+09:00
>>> KST
>>> 2019-04-14 17:14:30

타임존을 한국 표준시(KST)로 설정하면 원했던 한국 표준시 UTC +9 시간으로 출력할 수 있다.

Android Data Binding의 DirtyFlag 알아보기

지난 Devfest Seoul 2018과 Devfest Busan 2018에서 ‘Android DataBinding for Modularization, ViewModel and Testing’이라는 주제로 발표를 했다.

데이터 바인딩의 꽃이라고 할 수 있는 양방향 데이터 바인딩, 이를 위한 DirtyFlag가 코드에서 어떻게 동작하는지 살펴보았다. 추가적으로 데이터 바인딩에 있는 Annotation의 사용법과 UI 모듈화, 그리고 Testing에 관해서 공유했다.

그냥.. 궁금했던 개발자 – Generated Binding Class

데이터 바인딩 라이브러리는 레이아웃(xml)의 변수(Variable)나 뷰(View)에 접근할 수 있는 바인딩 클래스이며 컴파일 타임에 생성된다. 예를 들어, layout 태그를 가지는 activity_main.xml 파일이 있다면 컴파일시 ActivityMainBinding 다음과 같은 Java 파일이 생성된다.

activity_main.xml

<layout …>
    <data>
        <import type="com.github.kimkevin.devfestseoul18.ValidationUtil"/>
        <variable
            name="vm"
            type="com.github.kimkevin.devfestseoul18.MainViewModel"/>
    </data>

    <android.support.constraint.ConstraintLayout …>
        <TextView
            android:text="@{vm.title}"…/>

        <EditText
            android:text="@={vm.authCode}"…/>

        <Button
            android:enabled="@{ValidationUtil.isCodeValid(vm.authCode)}"
            android:onClick="@{() -> vm.login()}"…/>
    </android.support.constraint.ConstraintLayout>
</layout>

위와 같은 xml이 Java 파일로 변경되었다면 레이아웃을 동작 시키기 위해 필요한 변수는 무엇일까?

  • 선언된 View에 접근하기 위한 ConstraintLayout, TextView, EditText, Button 4개의 변수
  • variable로 전달된 vm(ViewModel) 변수

그리고 추가적으로 특정 View와 이벤트를 전달하고 받을 수 있는 리스너가 필요하다.

  • View의 이벤트를 처리할 수 있는 리스너, Button의 onClickListener
  • 양방향 데이터 바인딩을 위한 역방향 바인딩 리스너, EditText에서 android:text에 사용되는 InverseBindingListener

실제로 생성된 ActivityMainBinding.class를 확인해보자.

public class ActivityMainTestBinding extends android.databinding.ViewDataBinding implements android.databinding.generated.callback.OnClickListener.Listener {
    …
    // views
    @NonNull public final android.widget.Button button;
    @NonNull public final android.widget.EditText editText;
    @NonNull private final android.support.constraint.ConstraintLayout mboundView0;
    @NonNull public final android.widget.TextView textView;
    // variables
    @Nullable private com.github.kimkevin.devfestseoul18.MainViewModel mVm;
    @Nullable private final android.view.View.OnClickListener mCallback1;
    // Inverse Binding Event Handlers
    private android.databinding.InverseBindingListener editTextandroidTextAttrChanged = new android.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
        …

TextView는 setText(vm.title) 호출로 데이터가 바인딩 되었을 것이고, EditText는 TextWatcher 등록으로 입력된 텍스트가 vm.authCode역바인딩 되었을 것이다. 그리고 Button에는 vm.login()호출되는 OnClickListener를 등록했을 것이다. 여기까지는 안드로이드 UI 개발을 해본 경험이 있다면 예측이 가능하다.

<Button android:enabled="@{ValidationUtil.isCodeValid(vm.code)}” />

만약 위의 코드가 동작하도록 실제 코드로 작성한다면 어떻게 만들 수 있을까? 실제로 개발하는 과정에서 있었던 나의 궁금증이었다. EditText의 텍스트가 변경될 때마다 button.setEnabled(ValidationUtil.isCodeValid(vm.authCode))을 리스너에서 호출해주면 될 것이라는 추측을 할 수 있다. 해답을 찾기 위해서 데이터 바인딩 라이브러리가 생성한 바인딩 클래스를 살펴보고 이를 주제로 발표하는 계기가 되었다.

/* flag mapping … */ 주석의 발견 – DirtyFlag

flag mapping

ActivityMainBinding.java의 하단에는 flag mppaing으로 시작하는 주석으로 16진수 flag와 데이터 바인딩에 사용되는 필드 정보(ex: vm)가 있다. 그럼 16진수의 flag는 어디에서 어떻게 사용되는 걸까? 단서를 찾아보자.

가장 먼저 flag 0은 0x1L(16진수)이며 vm.authCode와 관련 있는 코드를 찾아보았다.

private boolean onChangeVmAuthCode(android.arch.lifecycle.MutableLiveData<java.lang.String> VmAuthCode, int fieldId) {
    if (fieldId == BR._all) {
        synchronized(this) {
            mDirtyFlags |= 0x1L;
        }
        return true;
    }
    return false;
}

ViewModel의 authCode가 변경되면 mDirtyFlags|= 대입 연산자를 통해 vm.authCode(0x1L)와 OR 비트연산을 한다. 다음 0x2L 값을 가지는 flag 1의 vm과 관련 있는 코드를 찾아보았다.

public void setVm(@Nullable com.github.kimkevin.devfestseoul18.MainViewModel Vm) {
    this.mVm = Vm;
    synchronized(this) {
        mDirtyFlags |= 0x2L;
    }
    notifyPropertyChanged(BR.vm);
    super.requestRebind();
}

ViewModel이 바인딩 클래스에 setVm이 되면 위에서 처럼 mDirtyFlags는 |= 대입 연산자를 통해 vm(0x2L)과 OR 비트연산을 한다.

그러면 mDirtyFlags의 초기값을 무엇일까?

@Override
public void invalidateAll() {
    synchronized(this) {
            mDirtyFlags = 0x4L;
    }
    requestRebind();
}

ActivityMainBinding 바인딩 클래스의 생성자를 보면 가장 마지막에 invalidateAll() 함수가 호출된다. 되는데 이때 mDirtyFlags의 초기값이 설정된다.

DirtyFlag mapping

초기값이 0x4L인 이유는 플래그 비트가 flag << 1 하나씩 왼쪽으로 이동하기 때문이다. vm.authCode가 2진수 001(0x1L)이고 왼쪽으로 시프트 연산을 하면 vm은 2진수 010(0x2L)이다. 그럼 초기값 설정을 위해 플래그 비트를 하나 둔다면, 한번더 왼쪽으로 시프트 연산을 한 진수 100(0x4L)이 된다.

여기까지는 바인딩 클래스가 동작하기 위해서 필요한 플래그의 기본 정보에 대해서 살펴보았다.

DirtyFlags의 동작 살펴보기

지금까지 코드로 언급한 mDirtyFlags는 무엇일까? 처음 접하는 단어라 찾아보니 ZETAWIKI에서는 더티비트캐시 내용에 변경이 있었음을 기록한 플래그 비트이며 캐시 블록마다 더티 비트가 1개씩 있음. 0이면 ‘변동 없음’, 1이면 ‘변동 있음(갱신 필요[1])’의 의미 라고 말한다.

안드로이드의 데이터바인딩에서도 View(UI)에 데이터의 바인딩과 리바인딩을 위해 더티비트를 사용했다. 재밌는 비트라는 생각이 들었다.

그럼 데이터가 어떻게 바인딩 또는 리바인딩 되는지 직접 코드를 통해서 살펴보면서 앞서 가진 궁금증도 해결해보자.

View에 데이터가 바인딩되는 곳은 executeBindings함수에서 모두 호출된다.

@Override
protected void executeBindings() {
    ...
    dirtyFlags = mDirtyFlags;
    if ((dirtyFlags & 0x7L) != 0) {
        if ((dirtyFlags & 0x6L) != 0) {
             textViewAndroidStringTitleVmYear = textView...getString(R.string.title, vmYear);
        }
        // read vm.authCode.getValue()
        vmAuthCodeGetValue = vmAuthCode.getValue();
        // read ValidationUtil.isCodeValid(vm.authCode.getValue())
        vmAuthCodeLengthInt6 = ValidationUtil.isCodeValid(vmAuthCodeLength);
    }
    if ((dirtyFlags & 0x7L) != 0) {
        this.button.setEnabled(vmAuthCodeLengthInt6);
        TextViewBindingAdapter.setText(this.editText, vmAuthCodeGetValue);
    }
    if ((dirtyFlags & 0x4L) != 0) {
        this.button.setOnClickListener(mCallback1);
        TextViewBindingAdapter.setTextWatcher(this.editText, null, null, null, 
                                              editTextandroidTextAttrChanged);
    }
    if ((dirtyFlags & 0x6L) != 0) {
        TextViewBindingAdapter.setText(this.textView, textViewAndroidStringTitleVmYear);   
    }
}

코드를 보면 많은 mDirtyFlags의 조건문을 볼 수 있는데, Debug로 Breakpoint를 찍은 다음 플래그가 어떻게 변경되는지 살펴보자.

Debugging bbinding class by breakpoint - 1

바인딩 클래스를 생성하면 위에서 설명한 것처럼 mDirtyFlags0x4L로 초기화 된다.

Debugging bbinding class by breakpoint - 2

그리고 binding.setVm(viewModel)이 호출되면 mDirtyFlags는 flag vm(0x2L)와 mDirtyFlags |= 0x2L 비트 연산으로 0x06L이 된다. 바인딩 클래스가 생성되고 변수들이 셋되면 executeBindings()은 어떤 동작을 하게 될까?

Debugging bbinding class by breakpoint - 3

mDirtyFlags가 0x6L일 때, 동작하는 첫 번째 조건문의 0x6L & 0x7L 결과값은 0x6L로 0이 아니기 때문에 true가 된다. 마찬가지로 모든 조건문을 연산해보면 다음과 같은 결과를 확인할 수 있다.

Debugging bbinding class by breakpoint - 4

바인딩 클래스가 생성되고 변수들이 초기화 되면 executeBindings()에 있는 모든 조건문이 호출로 뷰에 데이터가 바인딩되고 리스너가 등록되는 것을 확인할 수 있다.

Debugging bbinding class by breakpoint - 5

그러면 글을 쓰도록 동기를 만들어 주었던 EditText의 텍스트 입력시 Button의 활성화는 어떻게 호출될까?

Debugging bbinding class by breakpoint - 6

먼저 ViewModel에 있는 데이터를 가져온 후에 this.button.setEnabled(vmAuthCodeLengthInt6)가 호출되는 것을 알 수 있다. executeBindings()에 많은 조건문이 있었던 이유는 데이터 변경시 필요한 로직 이외 다른 로직들은 변경 사항이 없기 때문에 호출되지 않도록 하기 위해서다. 이런 방식으로 데이터 바인딩 라이브러리는 데이터와 레이아웃을 바인딩하는데 필요한 글루 코드를 최소화할 수 있다.

평소에 개발관련 툴이나 라이브러리를 만드는 것에 관심이 많기 때문에 가끔씩 코드를 들여다 보면서 창작자의 철학이나 코드를 많이 배우고 있지만 이번 데이터 바인딩 라이브러리를 살펴보면서 더티 비트를 알게 되어 좋은 경험이 된 것 같다.

나를 위한 StackOverflow 명성(Reputation) 쌓기

StackOverflow Welcome Page

영어에 쫄지 말자, 개발자는 코드로 본다

올해 버킷리스트 중 하나가 StackOverflow REP 1000 달성하기였고 이 숫자를 달성하는 과정에서 배운점과 명성을 쌓기 위한 나만의 팁을 공유하려고 한다. 실제로 Github 오픈 소스 활동을 공유하는 글들은 많이 봤지만 StackOverflow 관련된 글은 거의 보지 못했다. 누군가가 이 글을 보고 시작 해보겠다는 생각을 가졌다면 나보다 충분히 더 잘 해낼 수 있는 사람일 것이다.

StackOverflow My Profile

StackOverflow는 많은 사람이 자신에게 놓인 문제를 공유하고 그와 비슷한 문제를 겪은 사람들이 경험을 공유할 수 있는 곳, 개발자 노다지라 할 수 있다. 국내 개발자들이 하루에 한 번 이상은 StackOverflow를 찾지만 대부분은 검색을 통해 필요한 정보를 얻는 정도다.

StackOverflow 활동을 시작한 계기는 내가 알고 있는 정보가 누군가에게 도움될 수 있으며 외국인 개발자와 소통을 연습할 수 있는 공간이기 때문이다. 가장 처음 올린 질문이 5분이 안 되어 ’This question was marked as an exact duplicate of an existing question.’로 이미 존재하는 질문이라 중복으로 처리되었고 다른 개발자가 존재하는 질문의 링크를 댓글로 달아준 적이 있다. 다른 질문은 올린지 얼마 되지 않아 다른 개발자가 나의 질문의 영어 문법을 고쳐주고 태그도 추가해주었다. 이렇게 빠른 시간내에 질문 정보의 질이 높아진다는 것은 집단지성 생태계의 균형을 맞추면서 정보의 자정작용을 하고 있음을 뜻한다.

영어에 쫄지 말자! 어차피 개발자는 코드로 본다. 실제로 코드를 보여달라는 댓글이 제일 많다
틀렸다고 창피해 하지 말자! 대부분 질문/답변에 누군가에 의해 수정됨으로 표시되어 있다. 수정할 거리를 찾는 하이에나가 많다.

Privilege(특권)

StackOverflow Privilege

Privilege는 StackOverflow에서 할 수 있는 특권을 말하는데 특정 REP를 달성하면 찬성/반대투표(Up/Downvote) 기능이 활성화되거나 게시글에 태그를 추가할 수 있고 댓글도 추가할 수 있다. 게임에서 어느 레벨 이상 되면 퀘스트를 시작할 수 있거나 아이템을 장착할 수 있는 느낌과 비슷하다. 지속해서 커뮤니티 활동을 지속하게 만드는 동기부여가 된다.

Badge(배지)

StackOverflow Privilege

Badge는 금, 은, 동 총 3가지로 특정 미션을 수행하면 획득할 수 있으며 난이도에 따라서 배지의 색이 다르다. 동 배지는 나도 모르게 하나씩 받을 때가 있으며 금과 은 배지는 확실히 받기가 어렵도록 되어있다. 가끔 리뷰나 가이드를 수행하면 동 배지를 줄 때가 있다.

StackOverflow Guide

예를 들어 댓글 10개를 남기는 미션이 있다면 프로필의 Next badge 패널에서 3개만 더 하면 달성할 수 있는 그래프가 나오기 때문에 자신의 원하는 배지를 선택하고 달성해 나가는 게이미피케이션(Gamification) 요소라고 할 수 있다. 배지를 받으려고 노력하다 보면 명성(REP)이 쌓이고 어렵게 느껴지는 서비스를 이를 통해 이해하게 된다.

Reputation(REP: 명성)

위에서 설명한 두 가지 요소의 근간이 되는 REP(Reputation), 명성이다. 커뮤니티 활동을 하면 닉네임, 명성, 배지(금, 은, 동)가 다른 사람들에게 노출된다. 기본적인 명성 획득/손실에 대한 설명은 다음과 같고 자세한 설명은 명성은 무엇인가를 읽어보길 추천한다.

언제 명성을 얻는가?

1. 내 질문이 찬성투표를 받으면 : +5
2. 내 답변이 찬성투표를 받으면 : +10
3. 내 답변이 채택된 경우 : +15
4. 내 답변을 채택한 경우 : +2
5. 편집이 승인될 경우(내용, 태그 등) : +2

언제 명성을 잃는가?

1. 내 질문이 반대투표를 받으면 : -2
2. 내 답변이 반대투표를 받으면 : -2
3. 다른 사람의 답변에 반대투표를 하면 : -1

REP 쌓는 팁

1. 원하는 분야의 태그로 검색하여 최신순으로 모니터링하자

StackOverflow Newest Questions

자신이 개발하는 분야를 태그로 검색해서 관련 최신 질문들을 볼 수 있고 빠른 답변을 하기에 좋다. 나 같은 경우에는 android 태그를 추가하여 구독하고 있다. 생각보다 내가 겪었던 문제들이 올라올 때가 많고 답변할 기회를 얻을 수 있다.

2. 질문이나 답변을 조금만 수정하자

StackOverflow My Reputation

개발하다 StackOverflow의 질문과 답변을 보고 있다면 틀린 스펠링이나 문법이 올바른지 정도만 확인하고 수정해보자. 수정 후 승인 시간도 빠르고 명성 2를 획득할 수 있다. edit이 이 방법으로 획득한 것이다. 생각보다 투자 대비 쏠쏠한 이익이 된다.

3. 태그 추가나 삭제하자

질문에 관련된 태그 추가하거나 삭제하여 수정할 경우, 승인되면 명성 2를 획득할 수 있다.

4. 좋은 질문을 하자

천 점대의 StackOverflow 사용자 중에서 일부는 질문만 하는 사람들도 있다. 프로필을 조회해서 했던 질문들을 살펴보면 몇백 개의 찬성투표를 받았다. 가장 흔하게 발생하면서 빨리 등록된 문제들이 찬성 투표를 많이 받을 수 있다. 질문만 하는 사람들이 명성이 높은 게 이해가 되지 않을 수도 있지만 그런 질문들을 찾고 하는 것도 능력이라 생각한다.

틀린 질문은 없다. 하지만 질문을 하더라도 문제 해결을 위한 최소한의 노력은 하였는가를 자신에게 물어본 후 질문하는 방식을 이해하고 질문하는 것이 좋다. 질문을 하기 전에 StackOverflow 질문하는 방법을 읽어 보길 바란다.

5. 빠르고 정확한 답변을 하자

답변을 할 때, 글만 있는 것 보다는 코드를 추가해서 빠르게 질문자가 수정해 볼 수 있도록 추가하자. 채택받을 확률이 높고 답변의 찬성 투표(명성10)와 채택(명성15)을 받으면 한번에 명성 25를 쌓을 수 있다.그리고 정확한 답변이라면 어느날 같은 이슈를 겪는 사람들이 찬성 투표를 잘한다. 가장 명성을 많이 쌓을 수 있는 방법이다.

6. 반대 투표(Downvote) 기능을 전략적으로 활용하자

질문에 달린 답변들 중에서 틀린 경우가 있다. 이럴 땐 반대 투표를 하고 자신이 생각하는 답변을 추가하자. 다른 사람의 답변에 반대 투표를 하면 명성이 -1되기 때문에 잘못된 방법이 아니다.

7. 1 ~ 2개의 답변이 있더라도 답변을 추가하자

명성이 백 점대로 올라가게 되면 답변이 하나도 달리지 않는 질문들을 많이 찾게 된다. 그런데 생각외로 1 ~ 2개의 답변이 있는 질문에 다른 방식의 답변을 추가하면 그만큼 인기 있는 질문이기 때문에 찬성투표를 잘 받을 수 있다.

글을 마치며

앞으로는 개인적인 목표나 별도의 시간을 정하지 않으려고 한다. 시간적으로 최대한 부담이 되지 않도록 빌드를 한다거나 남는 시간을 활용하는게 좋을 것 같다.

명성의 높이가 중요한 것은 아니다. 100K 이상 되면 외국에서는 이력서를 보지 않는다는 이야기도 있지만, 저 정도라면 바로 채용을 하더라도 잘할 수 있는 사람일 가능성이 높다. 그리고 명성이 높은 사람들의 답변을 찾아 읽어보면 공통적으로 글의 구성이나 질이 확연히 높다.

다른 사람들 이해시키기 위해 영어로 글을 적는 것 자체가 쉽진 않다. 가끔은 지식을 공유하기 위해 정리하는 것이 아니라 정리하기 위해 공유하는 느낌이 들때도 있다.

하지만 아무나 내가 한 질문이나 답변을 수정하거나 오타를 찾아 교정해 주기 때문에 나에게는 개발에 관련된 영어를 배우는데 큰 도움이 되었다. 내가 겪지 못한 다양한 문제들을 접할 수 있고, 다른 사람들의 코드를 읽고 디버깅 해볼 수 있으며 코드에 대해 해외 개발자와 이야기 할 수 있는 새로운 경험을 할 수 있다. 어느날 외국인과 일하게 된다면 자신이 알고 있는 지식이나 코드를 설명하기가 한결 편할 것 같다.

StackOverflow Good Day

개발자는 무엇보다 춤추게 하는 동기가 필요하다. 가끔씩 내가 했던 질문이나 답변이 누군가에게 도움이 되어 명성이 쌓여있으면 개발에 조금 더 집중하게 되고 동기부여가 된다.

서로 다른 노트북에서 ‘ONE’ 프로젝트 작업하기

New MacBook Pro

백팩에서 벗어나자

많은 개발자분 들이 무거운 15인치 노트북을 백팩에 넣고 회사에 출근하거나 세미나에 참석한다. 나도 13인치를 3년 정도 들고 다녔지만 무거운 건 마찬가지다. 새로운 모델이 나올 때마다 무게를 줄어들고 있지만 출퇴근길 백팩은 무겁다. 그래서 결국 얼마 전에 개인 노트북으로 맥북을 구매했다. 이제는 더운 여름날 저녁에 약속이 있더라도 무거운 가방과 등 사이의 땀으로 찝찝하지 않을 수 있다니 삶의 질이 향상되었다.

이제 두 장소에서 서로 다른 맥북을 사용하고 있다. 그럼 외부에서 작업하던 안드로이드 프로젝트(stage에 있는 작업 중인 코드)를 어떻게 그대로 집에서 다른 맥북으로 이어서 작업할 수 있을까?

Cloud Storage Service

iCould Drive

처음 사용해본 Cloud 서비스는 iCloud Drive였다. iCloud의 경우, 사진을 백업하기 위해서 자주 사용하긴 했는데 프로젝트를 동기화해보는 건 처음이다.

iCould Drive Billing (가격)

iCloud Drive Billing

요금의 경우, 무료로 5GB가 기본으로 제공되고, 50GB를 사용할 경우 한 달에 약 $1만 결제하고 저렴하게 사용할 수 있다.

iCloud Drive Sync

먼저 iCloud 계정으로 로그인을 한 다음에 iCloud Drive 폴더에 간단히 프로젝트를 옮겨주면 된다. 다른 맥북에서 같은 계정으로 로그인하면 iCloud Drive에 프로젝트가 동기화되면서 자동 생성한다. 만약 외부에서 작업하고 동기화가 완료되면 개인 맥북으로 프로젝트가 정상적으로 동기화가 된다.

하지만 사용하면서 아쉬웠던 점은 동기화할 때 시계 모양의 진행 상태가 표시되지만 현재 동기화 중인 파일을 파악하기가 어렵다. 다른 개발자 분들은 두 개의 맥북에서 어떻게 프로젝트를 진행하고 있는지 물어보고 알게 된 서비스는 DropboxUnison이다. 현재는 Dropbox를 사용하고 있지만, Unison의 경우, OSX, Unix, Windows에서 다른 호스트에 저장된 파일이나 폴더를 공유할 수 있다.

Unison is a file-synchronization tool for OSX, Unix, and Windows. It allows two replicas of a collection of files and directories to be stored on different hosts (or different disks on the same host), modified separately, and then brought up to date by propagating the changes in each replica to the other.

Dropbox

Dropbox와 git의 검색을 하면 Dropbox를 Private Github처럼 사용할 수 있는 방법들을 쉽게 찾을 수 있다. 만약 Github에서 Unlimited Private Repository을 사용하지 않거나 서버에 Git을 설치해서 사용하지 않는다면 쉽게 Dropbox를 이용해 프로젝트를 관리할 수 있다. 나 같은 경우, 외부에서 미처 작업을 마무리 하지 못해 커밋을 하지 못했더라도 집에서 남은 작업을 마무리 할 수 있는 환경이 필요했다.

Dropbox Billing (가격)

Dropbox Billing

Dropbox는 2GB가 기본적으로 제공되고 나머지는 Dropbox Basic(무료) 계정에서 용량 더 얻기로 추가적으로 용량을 추가할 수 있다. 그리고 유료의 경우 Dropbox Plus로 업그레이드를 하게 되면 매월 약 $10로 1TB의 용량을 사용할 수 있다. 위에서 소개한 iCloud Drive의 1TB의 용량과 같은 가격이다.

Dropbox 사용 후 장점

Dropbox file changed

Dropbox를 사용하면서 가장 큰 장점은 개인적인 느낌이지만 iCloud를 사용할 때보다 빠르고 무엇보다 현재 동기화 중인 파일들이 Finder의 우측에 아이콘으로 표시된다. iCloud를 사용할 때 아쉬웠던 점이 채워진 느낌이다. 그리고 오른쪽 위의 Dropbox 아이콘에 동기화 중인지 확인이 가능하기 때문에 완료를 쉽게 확인할 수 있다.

Dropbox 선택적 동기화

Dropbox Account Settings

Github에 프로젝트를 올릴 때, 컴파일이나 빌드 할 때마다 생성되는 build 폴더나 설치 명령어로 패키지를 다운 받을 수 있을 경우엔 Github의 레파지토리에 올라가지 않도록 gitignore에 경로를 추가한다. 만약, 코드를 수정할 때마다 빌드를 하면 build 폴더가 변경되어 Dropbox에서 동기화를 곧바로 진행되므로 이는 비효율적이다.

Dropbox Selected Sync

그래서 gitignore와 비슷한 기능이 Dropbox에서는 선택적 동기화를 제공한다. 동기화가 필요하지 않은 폴더들을 선택해서 동기화에서 제외해준다.

Dropbox File Changed

같은 프로젝트로 작업하고 Dropbox의 동기화가 끝났을 경우, 또 다른 맥북에서는 파일들의 변경을 인지하고 IDE(Android Studio)에서 프로젝트를 다시 로드하는 것을 권유한다.

Dropbox File Conflicted

만약에 동기화가 되기 전에 같은 파일을 수정할 경우, 다음과 같이 충돌을 알려주면서 임시 파일이 생성된다. 현재까지 5달간 사용해보면서 동기화가 끝나는 것을 잘 확인하고 바이너리 폴더를 제외한 소스 코드만 선택적 동기화를 잘 시켜주면 동기화 속도가 빠르므로 파일 충돌 문제를 쉽게 발생하지 않을 것이다.

만약 개인 서버를 운영하고 있다면 Unison을 설치해보거나 1TB의 용량을 매월 $10로 사용하기 아깝고 50GB 정도의 용량을 월 $1로 iCloud Drive를 이용할 수 있다. 마지막으로 약 10GB의 용량이 충분하다면 친구 초대를 통해 용량을 추가 획득해서 Dropbox를 사용해보길 바란다.

Android WeChat Pay 개발하기

Why WeChat Pay?

2015년 1월, 회사 서비스가 중국에 오픈하면서 베이징으로 출장을 다녀온 적이 있다. 처음으로 추가 했던 결제 플랫폼은 중국에서 높은 점유율을 차지하던 알리페이(Alipay)였고 1년이 지나 2016년 초에 다시 갔을 땐, 레스토랑부터 심지어 길에 있던 자판기에서 위챗페이(WeChat Pay)의 QR코드를 이용해 간편결제를 할 수 있을 만큼 성장 속도가 빨랐다. [통계로 보는 위챗] 중국 최대 메신저 ‘위챗’, ‘위챗페이’의 모든 것 다음 기사를 보면 “2016년 1분기를 기준으로 7억 6200만 명에 달합니다.”라는 내용을 볼 수 있다. 그만큼 젊은 층에서는 위챗이 알려진 만큼 위챗페이를 이용한 결제가 많아졌다.

계정 생성 및 애플리케이션 등록하기

WeChat Open Platform

개발을 시작하기 전에 중국어로 된 위챗 오픈 플랫폼에서 계정을 생성하고 앱을 등록해야 하며 이때 패키지 이름과 서명 정보를 등록해야 한다.

WeChat Payment Wiki Page

위챗페이 결제 플랫폼 위키에서 APP支付메뉴의 5번째 하위 메뉴 APP端开发步骤(APP 단말 개발)에 Android, iOS 플랫폼 별로 개발과정이 설명되어 있다. 하지만 페이지에서 다른 언어를 지원하지 않기 때문에 구글 번역기로 페이지 번역했지만 부족한 부분이 있어서 플리토(Flitto)로 번역한 한글 번역 – Android 개발 요점 설명 문서를 참고하자.

패키지 이름은 개발 중인 앱의 패키지 이름을 사용하고 서명의 경우, 위의 번역 문서대로 앱을 다운 받은 다음에 서명을 만들어서 입력하면 된다. 그리고 경험으로는 앱 정보 등록 시 서명을 만든 테스트 기기에서는 서명되지 않는 앱일지라도 위쳇 로그인이 가능하다.

SDK 설치하기

Installation Android SDK

위챗 로그인 개발로 추가된 SDK에 결제 API가 빠져서 이번에 업데이트(v3.1.1)를 진행했다. Android资源下载 페이지의 최상단에 있는 “使用微信分享、登录、收藏、支付等功能需要的库以及文件。点击下载 Android开发工具包(위쳇 모멘트, 로그인, 즐겨찾기 저장, 결제 등 기능이 필요한 문서는 Android를 통하여 다운받으시기 바랍니다)” 클릭해서 다운로드한 후 lib폴더 안에서 jar를 추가하면 된다. 만약 현재 글로벌(구글플레이)과 중국으로 서비스를 개발하고 있다면 productFlavors에서 중국 flavor를 추가해서 해당 디렉터리 안에 라이브러리 폴더를 만들어서 별도로 관리하는 편이 좋다.

안드로이드 클라이언트 개발

wechatpay_appid

개발을 시작하기 전에 위쳇 오픈 플랫폼에 앱을 등록한 후, 생성된 AppID가 필요하다. 또한 중국에 서비스를 하면서 페이팔, 알리페이, 구글 인앱결제를 개발 했었지만 위쳇페이에서는 다른 결제 플랫폼과는 다르게 통합주문(개인적으로는 선주문으로 이해함)이라는 과정이 있다. 서버 개발자라면 위쳇 페이 API 문서를 보면서 반드시 이해를 해야 하고, 클라이언트 개발자라도 결제가 정상적으로 되지 않는다면 파라미터나 해당 키값을 확인하기 위해서 알아 두는 것을 추천한다.

위쳇페이 결제 과정

wechatpay_flow

  • 1 클라이언트에서 상품의 아이디(ex: product_id)로 서비스의 서버에 주문을 넣는다.
  • 2 서버에서는 웨이신 통합 주문 API을 이용해 주문을 생성한다.
  • 3 prepaid_id가 포함된 데이터(nonce_str, mch_id, timestamp, sign)와 서비스의 상품 주문 아이디를 클라이언트에 응답으로 내려준다.
  • 4 클라이언트에서는 데이터를 가지고 위쳇 애플리케이션으로 API 요청을 보낸다.

위쳇페이 창구 열기

IWXAPI api = WXAPIFactory.createWXAPI((Context) purchasePointView, Constants.WEIXIN_APP_ID);
api.registerApp(Constants.WEIXIN_APP_ID);

if (api.isWXAppInstalled()) {
    PayReq request = new PayReq();
    request.appId = unifiedOrderItem.getAppId();
    request.nonceStr = unifiedOrderItem.getNonceStr();
    request.packageValue = "Sign=WXPay";
    request.partnerId = unifiedOrderItem.getPartnerId();
    request.prepayId = unifiedOrderItem.getPrepayId();
    request.timeStamp = String.valueOf(unifiedOrderItem.getTimeStamp());
    request.sign = unifiedOrderItem.getSign();
    api.sendReq(request);
} 
  • 5 위펫페이 결제 창구를 오픈하면서 WXPayEntryActivity가 실행된다.

WXPayEntryActivity

public class WXPayEntryActivity extends Activity implements IWXAPIEventHandler {
  public static final String TAG = "WXPayEntryActivity";
  public static final int PAYMENT_SUCCESS = 0;
  public static final int PAYMENT_ERROR = -1;
  public static final int PAYMENT_USER_CANCELED = -2;

  private IWXAPI api;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(new View(this));

    api = WXAPIFactory.createWXAPI(this, Constants.WEIXIN_APP_ID);
    api.handleIntent(getIntent(), this);
  }

  @Override
  protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);
    api.handleIntent(intent, this);
  }

  @Override
  public void onReq(BaseReq baseReq) { }

  @Override
  public void onResp(BaseResp baseResp) {
    if (baseResp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX) {
      Intent intent = new Intent(TAG);
      intent.putExtra("errCode", baseResp.errCode);
      sendBroadcast(intent);
    }
    finish();
  }
}
  • 6 결제를 하면 창구가 닫히면서 보낸 결제 정보를 WXPayEntryActivityonResp에서 확인한다.

WXAPIFactory

public final boolean openWXApp() {
    …
    } else {
      try {
this.context.startActivity(this.context.getPackageManager().getLaunchIntentForPackage("com.tencent.mm"));
    …
  }

WXAPIFactory에서 위쳇앱을 실행할 때, startActivity로 실행을 하기 때문에 흔히 액티비티간의 통신 방법으로 사용하는 setResult가 정상적으로 실행되지 않는다. 그래서 onResp에서 에러코드를 리턴해주기 위해 브로드캐스트(broadcase)를 사용했다.

  • 7 결제가 성공이면 서버로 부터 받은 주문 ID를 검증 요청한다.
  • 8 서버는 주문 ID를 이용해 웨이신 주문 확인 API로 주문을 최종 확인한다.
  • 9 클라이언트에게 결제 확인 응답을 보낸다.**

위쳇페이 결제 완료시 BaseResp.errorCode가 항상 -1로 리턴되는 이슈

위의 과정대로 라이브러리를 추가하고 파일을 추가해주면 간단하게 위쳇 앱에서 결제를 완료하고 개발중인 앱으로 결과까지 받아볼 수 있다. 하지만 그 결과가 -1이 되면 정상적으로 결제가 완료된 것을 뜻하지 않는다.

erroCode가 -1인 경우, WeChat Pay API 개발 문서를 보면 “발생가능한이유 : 사인(sign)오류, APPID 미등록, 프로젝트상에서 APPID 설정오류, 등록된 APPID와 세팅한것 맞지 않는 경우, 그 외” 이유로 인해 발생할 수 있다. app_id의 경우 보다는 sign 값의 오류일 가능성이 높다.

클라이언트에서 sign 생성하기

// request.sign = unifiedOrderItem.getSign();

SortedMap<String, String> data = new TreeMap<>();
data.put("appid", request.appId);
data.put("noncestr", request.nonceStr);
data.put("package", request.packageValue);
data.put("partnerid", request.partnerId);
data.put("prepayid", request.prepayId);
data.put("timestamp", request.timeStamp);
request.sign = createSign(data);

private String createSign(SortedMap<String, String> parameters) {
    StringBuffer sb = new StringBuffer();
    Set es = parameters.entrySet();
    Iterator it = es.iterator();
    while (it.hasNext()) {
      Map.Entry entry = (Map.Entry) it.next();
      String k = (String) entry.getKey();
      String v = (String) entry.getValue();
      if (null != v && !"".equals(v) && !"sign".equals(k)
          && !"key".equals(k)) {
        sb.append(k + "=" + v + "&");
      }
    }
    sb.append("key=" + "00123456789vmfflxhvmfflxhvmfflxh");
    LogUtil.d("Create Sign : " + sb.toString());
    String sign = MD5Util.MD5Encode(sb.toString(), "utf-8")
        .toUpperCase();
    return sign;
  }
}

서버에서 받은 sign값을 클라이언트에서 생성해서 위쳇으로 API 요청을 할 때 보내서 테스트해보자.

// data.put("partnerid", request.partnerId);
data.put("mch_id", request.partnerId);

이때 주의할 점은 WeChat Pay 결제 확인 API 문서에 있는 대로 키 값을 mch_id으로 사용해서는 안 된다.

// data.put("prepayid", request.prepayId);
data.put("prepay_id", request.prepayId);

마찬가지로 prepay_id를 키로 사용해 만든 sign 값으로 결제를 시도하면 결제가 실패한다. 위에 있는 키 값으로 정확한 데이터를 넣어주면 문제없는 sign 값을 생성할 수 있다. 그리고 중국을 서비스하기 위해서 외부 서비스를 연동할 때, 구글 검색은 크게 도움이 되지 않는다. 이때 클래스나 코드를 기반으로 바이두 검색을 추천한다. 중국어로 이해하긴 어렵겠지만, 자신의 코드를 비교해보면 겪고 있는 문제를 조금 더 쉽게 해결할 수 있다.

Read byte array from binary data of retrofit response

현재 서버에서 이미지를 binary data로 내려주면 클라이언트에서는 데이터를 읽은 후 byte array로 변환하고 있다. 이전에는 DEPRECATEDvolley 라이브러리를 이용해 Request를 커스텀하게 수정해서 사용했다.

public class ByteArrayRequest extends Request<byte[]> {
  private Response.Listener<byte[]> listener;
  …
  @Override
  protected Response<byte[]> parseNetworkResponse(NetworkResponse response) {
     return Response.success(response.data, HttpHeaderParser.parseCacheHeaders(response));
}

그리고 요즘에는 volley에서 Retrofit + RxJava 라이브러리로 변경하는 작업을 진행하는 중이다.

@GET(“books”)
Observable<Result<ResponseBody>> getImage(@Query(“id”) String id);

Retrofit으로 byte[]로 데이터 가져오기

Retrofit은 프로덕션에 적용한 지 얼마 안되었지만 Result에서 result.response()를 통해 얻은 객체 Response에는 크게 body()errorBody() 메소드가 있다. 여기에서 body는 ResponseBody가 되고 errorBody의 경우, 예를 들어 서버에서 404 코드와 에러 메시지가 포함된 에러 JSON 객체를 내려주면 String으로 받을 수 있다.

ResponseBody

public final InputStream byteStream() {
  return source().inputStream();
}

byte[] bytes() { 
  … 
  BufferedSource source = source();
  byte[] bytes;
  try {
    bytes = source.readByteArray();
  ...
}

String string() { 
  …
  BufferedSource source = source();
  try {
    Charset charset = Util.bomAwareCharset(source, charset());
    return source.readString(charset); 
  …
}

ReponseBody에서 InputStream을 가져와서 버퍼로 읽는 방법이나 byte[] 또는 String으로 서버로 받은 body를 가져올 수 있는데 String으로 binary 데이터를 읽게 되면 어떻게 될까?

String: ����JFIF��C��C��� ��    
�����+�}Yϭ�F39M>���������>���;��ˋ��uXʽ�w�ڤx\-[2g��k�S���H���m
[�V?[_W����#��v��}6�[��F�F�%����n�...

정상적인 이미지를 만들 수 없으므로 byte[]로 스트림에 있는 데이터를 가져와야 한다. 참고로 body.bytes()는 구현 코드를 살펴보면 body.source().readByteArray() 와 같은 동작이다.

body.source().readByteArray() : [-1, -40, -1, -32, 0, 16, 74, 70, 73, 70, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, -1, -37, 0, 67, 0, 16, 11, 12, 14, 12, 10, 16, 14, 13, 14, 18, 17, 16, 19, 24, 40, 26, 24, 2 … ]

이미지 Bitmap으로 변환하기

BitmapFactory의 decodeStream

InputStream input = body.byteStream();
Bitmap bitmap = BitmapFactory.decodeStream(input);

BitmapFactory의 decodeByteArray

Bitmap bitmap = BitmapFactory.decodeByteArray(body.bytes(), 0, body.bytes().length);

InputStream이나 byte array로 사용해서 쉽게 비트맵으로 이미지를 가져올 수 있다. 그리고 ResponseBody에 있는 BufferedSource에서 스트림으로 데이터를 읽기 때문에 주의해야 한다.

An elegant part of the java.io design is how streams can be layered for transformations like encryption and compression. Okio includes its own stream types called Source and Sink that work like InputStream and OutputStream – okio wiki 중에서

사실 위의 2번째 코드에서는 정상적으로 이미지를 그릴 수 없다.

byte[] bytes = body.bytes();
bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

그 이유는 body.byte()를 호출하게 되면 스트림에 있는 데이터를 모두 읽기 때문에 다음 두 번째 호출 body.byte().length는 0이 되어 정상적인 Bitmap이 생성되지 않는다.

How to check the latest version of android support library in your SDK?

In order to the Android Support Library, we need to add the support library to dependencies in build.gradle. For example, add the following lines.

dependencies {
compile ‘com.android.support:design:25.1.0'
compile ‘com.android.support:appcompat-v7:25.1.0’
}

When do you update the support library version?

In my case, when IDE recommends to update SDK at the right top corner or when I open the SDK manager which provides the SDK tools, platforms, and others, I download and update the selected packages.

After downloaded, targetSdkVersion of defaultConfig shows the warning message with yellow underline “Not targeting the latest versions of android compatibility modes apply. consider testing this version.” for being compatible with the latest Android changes, so let’s set targetSdkVersion to 25 (Android 7.1) and run gradle build.

Finally, we will see the notice which means the support library version must be replaced to the latest version you have in your SDK and the targetSdkVersion must be the same.

How to check the latest version of android support library in your SDK?

Andorid Support Repository is local maven repository which contains all the support libraries as AAR archives from Android.

m2repository installed from dl.google.com.

As the result, you will find the latest version of support library in your local SDK. Update 25.0.1 version for com.android.support:appcompat-v7, sync project with Gradle files and run project.

[한글번역] WeChat Pay – APP端开发步骤说明

WeChat Pay App Dev Menual

원문 : APP端开发步骤说明

참고 : 아래의 번역문은 제가 직접 번역을 한 것은 아닙니다. Flitto에서 직접 한국어, 영어 번역을 참여해서 획득한 포인트로 위챗페이 개발에 필요한 문서를 번역하기 위해 번역요청(중국어->한국어)을 보내서 받은 번역문입니다. 혹시라도 위챗개발 문서가 필요한 개발자분들에게 도움이 되고자 공유합니다. 다수의 번역가가 참여했기 때문에 전체 문장이 다소 어색할 수 있음을 알려드립니다. 그리고 안드로이드 메뉴얼만 번역하였고, 4000원 비용이 들었습니다.

Android 개발 요점 설명

1. 백그라운드 설정

위챗 오픈플랫폼에서 개발 어플리케이션을 신청하시면 위챗 오픈플랫폼에서는 어플리케이션의 고유 식별 아이디를 생성해 드립니다. 안전한 결제를 위해 오픈플랫폼에 업체의 어플리케이션 패키지 이름과 어플리케이션 서명(signature)을 연동시키셔야 합니다. 연동 후에 정상적으로 결제가 가능합니다. 설정화면은 <오픈플랫폼>의 <관리센터/어플리케이션 수정/개발데이터 수정>메뉴에 있으며 그림 8.8의 빨간 네모로 표시되어 있는 부분입니다.

8.8

패키지명: 어플리케이션 프로젝트의 AndroidManifest.xml에 설정한 package 이름값이다. 예를들어 DEMO에서 package값은 net.sourceforge.simcpux.

어플리케이션 서명: 프로젝트의 패키지명 및 컴파일에 사용된 keystore을 signing툴을 사용하여 생성한 32비트길이의 MD5문자열입니다. 테스트기기에 signing툴을 설치하고 실행하면 application의 서명을 생성합니다. 그림8.9와 같이 녹색문자열이 곧 어플리케이션의 서명이다. signing툴 다운로드 url https://open.weixin.qq.com/zh_CN/htmledition/res/dev/download/sdk/Gen_Signature_Android.apk

8.9

2. APP ID 등록하기

WeChat jar패킷을 개발자의 앱프로젝트로 가져옵니다. 하지만, API를 사용하려면 우선 위챗으로 APPID를 등록해야 합니다. 코드는 아래와 같습니다:

final IWXAPI msgApi = WXAPIFactory.createWXAPI(context, null);
// app id를 위챗에 등록하기
msgApi.registerApp("wxd930ea5d5a258f4f");

3. 결제 호출

업체서버로부터 결제 청구를 생성합니다. 먼저 통합구매 API를 호출해(자세한 내용은 7장 참조) 구매리스트를 생성 후, prepay_id 값을 받아서 다시 사인 후 앱에 전송하여 결제 청구를 제출한다. 아래는 웨이신 결제를 호출하는 메인 코드입니다.

IWXAPI api;
PayReq request = new PayReq();
request.appId = "wxd930ea5d5a258f4f";
request.partnerId = "1900000109";
request.prepayId= "1101000000140415649af9fc314aa427",;
request.packageValue = "Sign=WXPay";
request.nonceStr= "1101000000140429eb40476f8896f4c9";
request.timeStamp= "1398746574";
request.sign= "7FFECB600D7157C5AA49810D2D8F28BC2811827B";
api.sendReq(req);

주의: sign 필드명으로 생성되는 리스트는 API 발급 설정을 참고하세요.

4. 지불결과 콜백

위쳇 SDK Sample을 참조하여, net.sourceforge.simcpux.wxapi 패캣경로에서 WXPayEntryActivity클래스를 생성합니다.(패킷명 혹은 클래스명이 일치하지 않으면 콜백이 불가능합니다), WXPayEntryActivity클래스에서 onResp 함수 구현, 지불완료 후 위쳇 엡은 판매자 어플리케이션으로 돌아가면서 onResp 함수를 콜백합니다. 개발자는 해당 함수내에서 알림메세지를 받아야하며 기존으로 돌아가기 위한 에러코드를 판단해야 합니다. 지불 성공 후 백오피스로 가서 지불결과를 체크한 후, 유저의 실제 지불결과를 보여줍니다. 클라이언트의 복귀를 유저 결제결과로 적용하지 않도록 꼭 주의해야 합니다. 서버사이드에서 받은 결제 알림메세지 혹은 API복귀결과를 체크하여 기준으로 해야 합니다, 코드예시는 다음과 같습니다:

@Override
public void onResp(BaseResp resp) {
if (resp.getType() == ConstantsAPI.COMMAND_PAY_BY_WX){
Log.d(TAG,"onPayFinish,errCode=" + resp.errCode);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.app_tip);
}
}

콜백한 errCode 리스트:

명칭 기술 솔루션
0 성공 성공화면
-1 오류 발생가능한이유 : 사인(sign)오류, APPID 미등록, 프로젝트상에서 APPID설정오류, 등록된APPID와 세팅한것 맞지 않는 경우, 그 외
-2 사용자취소 발생가능한이유 : 사용자취소버튼 누르고 APP으로 복귀