나를 위한 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으로 복귀

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파일들을 볼 수 있다.