하노이 탑 문제 정리

읽고있는 파이썬 책에 하노이 탑이 예제로 나왔다. 이거 학생때 배웠던거지! 하며 가벼운 마음으로 보는데 순간 하나도 이해가 되지 않았다. 어떻게 풀었는지도 기억이 나질 않는다. 심지어 소스를 보았는데도 이해가 가지 않는다. 뭐지! 하는 위기감에 정리해 본 하노이 탑 문제.

하노이 탑 ( 출처 : 위키백과 )

  1. 설명
    하노이의 탑(Tower of Hanoi)은 퍼즐의 일종이다. 세 개의 기둥과 이 기둥에 꽂을 수 있는 크기가 다양한 원판들이 있고, 퍼즐을 시작하기 전에는 한 기둥에 원판들이 작은 것이 위에 있도록 순서대로 쌓여 있다.게임의 목적은 다음 두 가지 조건을 만족시키면서, 한 기둥에 꽂힌 원판들을 그 순서 그대로 다른 기둥으로 옮겨서 다시 쌓는 것이다.

    1. 한 번에 하나의 원판만 옮길 수 있다.
    2. 큰 원판이 작은 원판 위에 있어서는 안 된다.

    하노이의 탑 문제는 재귀 호출을 이용하여 풀 수 있는 가장 유명한 예제 중의 하나이다. 그렇기 때문에 프로그래밍 수업에서 알고리즘 예제로 많이 사용한다. 일반적으로 원판이 n개 일 때, 2n-1번의 이동으로 원판을 모두 옮길 수 있다. 메르센 수라고 부른다).참고로 64개의 원판을 옮기는 데 약 18446744073709551615번을 움직여야 하고, 한번 옮길 때 시간을 1초로 가정했을 때 64개의 원판을 옮기는 데 5849억 4241만 7355년 걸린다.

  2. 참고 그림
    300px-Tower_of_HanoiTower_of_Hanoi_4

문제의 분해

  1. 원판이 한개일경우 풀이
    1번 기둥의 1번 원반을 3번 기둥에 옮긴다.
  2. 원판이 두개인 경우 풀이
    1번 기둥의 1번 원반을 2번 기둥에 옮긴다.
    1번 기동의 2번 원반을 3번 기둥에 옮긴다.
    2번 기둥의 1번 원반을 3번 기둥에 옮긴다.
  3. 원판이 세개인 경우 풀이
    1 번 기둥의 1 번 원반을 3 번 기둥에 옮긴다.
    1 번 기둥의 2 번 원반을 2 번 기둥에 옮긴다.
    3 번 기둥의 1 번 원반을 2 번 기둥에 옮긴다.
    1 번 기둥의 3 번 원반을 3 번 기둥에 옮긴다.
    2 번 기둥의 1 번 원반을 1 번 기둥에 옮긴다.
    2 번 기둥의 2 번 원반을 3 번 기둥에 옮긴다.
    1 번 기둥의 1 번 원반을 3 번 기둥에 옮긴다.
  4. 원판이 n개인 경우 풀이
    1번 기둥에서 n-1번 까지의 원판을 2번(여분의 기둥)으로 옮긴다.
    1번 기둥에서 n번 원판을 3번으로 옮긴다.
    2번 기둥의 n-1번 까지의 원판을 3번으로 옮긴다.

1~4의 과정을 보면 하노이 탑 문제는

hanoi(n) = 2hanoi(n-1) + move(n)

로 정의 될 수 있다. 좀더 상세히 정리를 하면

hanoi(n, startPeg, endPeg) = hanoi(n-1, startPeg, extraPeg) + move(n) + hanoi(n-1, extraPeg, endPeg)

가 된다.

문제 풀이

  1. 리컬시브 구조를 이용한 풀이
    def hanoiRecursive(ndisks, startPeg=1, endPeg=3):
        global phase
        if ndisks:
            extraPeg = 6 - endPeg - startPeg
            hanoiRecursive(ndisks-1, startPeg, extraPeg)
            print(startPeg, "번 기둥의", ndisks, "번 원반을", endPeg, "번 기둥에 옮깁니다.")
            hanoiRecursive(ndisks-1, extraPeg , endPeg)
  2. Stack을 이용한 풀이
    def hanoiStack(ndisks, startPeg=1, endPeg=3):
        stack = []
        stack.append((ndisks, startPeg, endPeg, False))
        while len(stack) > 0:
            disks, start, end, move = stack.pop()
            extraPeg = 6 - end - start
            if move:
                print(start, "번 기둥의", disks, "번 원반을", end, "번 기둥에 옮긴다.")
            elif disks > 0:
                stack.append((disks - 1, extraPeg, end, False))
                stack.append((disks, start, end, True))
                stack.append((disks - 1, start, extraPeg, False))

 

아이 아빠가 되고 알게된 기묘한 변화

지난 7월 30일 자정 즈음 만삭의 아내가 옅은 산통을 호소했다. 혹시나 하는 마음에 괜찮다고 하는 아내를 데리고 병원 응급실을 찾았다. 아니나 다를까 응급실에서 진찰을 받으니 아이가 나올준비를 하고 있으니 입원해야한다고 한다. 두말 할것 없이 곧바로 입원을 했다.

새벽 3시즈음 아내의 산통이 본격적으로 시작되었다. 산통으로 온몸을 부들부들 떠는 아내를 보니 너무나도 미안했다. 아이에겐 말하기 미안하지만 이때 뱃속 아이는 전혀 생각나지 않았다. 그저 아내가 잘못되는 것은 아닌지 내가 대체 아내한테 무슨 몹쓸짓을 한건지 너무 두렵고 불안하고 또 불안했다.

한참의 산통이 지나고 아이가 나올거 같아 분만실로 이동했다. 그리고 다시 힘든 3시간이 지났다.  곧 의사 선생님이 “나온다! 더! 더!” 하고 목소리를 높였고 바로 아이의 울음소리가 터져나왔다. 의사 선생님 손에 들어 올려지는 핏덩이를 보는데 코끝이 시큰했다. 간호사의 안내를 받아 탯줄을 자를때 이미 나는 콧물과 눈물을 질질 짜고 있었다.

탯줄이 잘린 아이는 영아 침대로 옮겨 졌다. 정말 서럽게 우는 그 아이를 보면서 내 의식에는 기묘한 변화가 생겼다. 그리고 보니 이런 기묘한 느낌은 결혼때도 경험 했었다.

나에게 결혼은 쌍성이 탄생하는 느낌이었다. 별 걱정 없이 나를 중심으로 돌던 의식이 아내와 쌍성을 이루며 그 사이에 새로운 중심을 만드는 느낌이었다. 우리 결혼은 나와 아내 사이의 절묘한 균형을 중심으로 돌고 있었다. 나와 아내의 인력 사이의 절묘한 중심이 내가 이해한 결혼 생활이었다.

반면 아이의 탄생은 이 쌍성계을 과감히 해체했다. 아이가 뿜어내는 강력한 인력은 쌍성 사이의 절묘한 균형을 완벽히 압도했다. 서로에 이끌려 돌고 돌던 쌍성계는 더이상 기존의 중심을 유지할 수 없었다. 아이의 강력한 인력에 끌려가버린 쌍성은 이제 아이를 중심으로 돌고 도는 행성계가 되었다.

이제 아이가 태어난지 3주가 되었다. 이제 본격적으로 힘들어 질거라는 주변의 이야기를 나는 아직 이해하지 못하고 있다. 그저 바라보는 것만으로도 행복하다 라는 어머니의 말씀이 이해가기 시작한다.

쓰고 나서 읽어보니 이거 완전 중2병인데 내 실력으론 더이상의 표현이 불가능해서 여기서 접어야겠다.

Android – Intent extras size limit (android.os.TransactionTooLargeException) 문제 회피한 이야기

안드로이드 어플리케이션 개발을 하다보면 종종 Activity나 Service에 Intent의 Extra로 데이터를 전달하는데 뭔가 애매한 경우가 있다. 그냥 Intent Extra로 ID나 쿼리 값만 전달하고 각 화면에서 필요한 시기에 데이터를 불러와도 되긴 하지만 어플리케이션 특성상 서버에서 데이터를 가져오는 경우가 많아 좋지 상황이 발생하는 경우도 있다. 특히 Activity에서 데이터를 불러오는 동안 UI들이 공백 상태로 상태로 노출되어 영 보기가 좋지 않고 불러오기가 실패했을 경우의 처리는 역시 애매하다 사실 실패의 경우는 화면마다 재시도 UI 만들기가 귀찮다.

그래서 나는 이전 화면이나 로딩화면으로 잠깐 나왔다 사라지는 Fake Activity에서 데이터를 불러온 뒤 Intent Extra로 데이터 자체를 넘겨 주는 방식을 선호한다. 불러오기를 실패했을 경우엔 그냥 다시 시도해주세요 메세지 한번 띄워주면 그만이니 실패 처리도 간단해진다. 그래서 내가 만든 어플리케이션에서는 Intent의 Extra에 데이터 객체를 담아 던져주는 경우가 많다. 물론 Java premetive type 밖에 담지 못하는 Intent의 특성상 데이터 객체는 반드시 Binary나 json으로 Serialize해야한다. serialize/deserialize 하는 비용 문제가 있긴 하겠지만 뭐 서버 사이드도 아니고 클라이언트에서 크게 문제될 일이 없었다.

한동안은 별 무리 없이 이 방법을 잘 썼다. 한가지 예외사항에 부딪히기 전까지는 말이다. 특정 상황에 데이터 객체의 크기가 생각 이상인 경우가 있었고 결국 Intent가 전달할 수 있는 사이즈를 넘어 버려 ‘android.os.TransactionTooLargeException’ 발생하며 앱이 죽는 경우가 생겼다. 이걸 어찌하나 하고 구글링을 좀 하다가 그냥 DataHolder를 만들어 쓰면 괜춘하다는 글을 봤다.

나름 어떻게 적용해 볼까 고민하다가 아래처럼 해봤다.

  1. DataHolder 클래스 생성 (둘중 뭐가 좋을지는 아직 잘 모르겠음)
    1. Application 클래스에 추가하거나….
      public class MyApp extends Application {
          private Map<String, Object> mDataHolder = new ConcurrentHashMap<>();
          
          public String putDataHolder(Object data){
              //중복되지 않는 홀더 아이디를 생성해서 요청자에게 돌려준다.
              String dataHolderId = UUID.randomUUID().toString();
              mDataHolder.put(dataHolderId, data);
              return dataHolderId;
          }
      
          public Object popDataHolder(String key){
              Object obj = mDataHolder.get(key);
              //pop된 데이터는 홀더에서 제거
              mDataHolder.remove(key);
              return obj;
          }
      }
    2. 별도의 DataHolder 스태틱 클래스를 추가하거나….
      public class DataHolder {
          private static Map<String, Object> mDataHolder = new ConcurrentHashMap<>();
          
          public static String putDataHolder(Object data){
              //중복되지 않는 홀더 아이디를 생성해서 요청자에게 돌려준다.
              String dataHolderId = UUID.randomUUID().toString();
              mDataHolder.put(dataHolderId, data);
              return dataHolderId;
          }
      
          public static Object popDataHolder(String key){
              Object obj = mDataHolder.get(key);
              //pop된 데이터는 홀더를 제거
              mDataHolder.remove(key);
              return obj;
          }
      }
  2. 데이터를 보내는 측(대충 이런느낌으로?)
    BigData bigData = .......;
    Intent intent = new Intent(....);
    String holderId = DataHolder.putDataHoler(bigData);
    intent.putExtra("holderId", hoderId);
    startActivity(intent);
  3. 데이터를 받는 Activity (도 대충 이런 느낌으로)
    String holderId = getIntent().getStringExtra("hoderId");
    BigData bigData = (BigData)DataHolder.popDataHolder(hoderId);

매우 간단하고 별거 아닌 꼼수인데 매우 잘 동작하고 편하고 결과도 좋다.

치명적인 단점은 당연하겠지만 Application 내에서만 작동한다는 것이다. Applicatoin간의 Intent 호출을 할 경우에는 당연히 망한다. 받는 측에서 Null Pointer Exception이 뙇! 하기도 전에 요청하는 측에서 DataHolder가 뭔지도 모르겠지……. 그러려면 DataHolder를 외부 서비스로 만들어 볼까? …근데 그 서비스로는 데이터 객체를 어떻게 전달하나….. Intent? 그게 안되서 이렇게 한건데? 역시 꼼수는 꼼수일 뿐……

외부 전달시의 꼼수는 file로 저장한 다음 uri만 던져주는 방식으로 하면 된다고 한다. 근데 뭐 지금은 그럴일 없으니 그런 구현은 생략!

참고 사이트 : https://www.neotechsoftware.com/blog/android-intent-size-limit

Android Gradle Build Setting 정리

안드로이드 프로젝트 진행하며 사용한 그래들 빌드 세팅 정리

Application Bulid 설정

  1. 프로젝트의 gradle 버전 업데이트
    1. 프로젝트 root디렉토리의 build.gradle 파일 수정하여 gradle 버전을 1.5 이상으로 올려준다. 프로젝트를 module 단위로 관리할것 아니면 상관 없지만 그냥 기분으로라도 올려준다.
      buildscript {
          repositories {
              jcenter()
          }
          dependencies {
              //그래들 버전이 1.5 이상이어야 module의 aar 파일명 변경(baseName) 옵션이 정상 동작 한다
              classpath 'com.android.tools.build:gradle:1.5.0'
          }
      }
  2. app/build.gradle파일 수정 및 Flavor 리소스 분리
    1. defaultConfig에 setProperty로 ouput 파일의 이름을 설정해준다. 파일 이름이 versionName과 versionCode가 추가되도록 하면 빌드 후 apk별 버전 관리가 쉬워진다.
    2. Flavor를 추가한다. dev, staging, product 세개의 Flavor로 나누가 각각 개발용, QA용, 상용 배포용 으로 나누어 빌드 할 수 있도록 한다. 대충 아래같은 느낌적 느낌.
      apply plugin: 'com.android.application'
      
      android {
          compileSdkVersion 23
          buildToolsVersion "23.0.2"
      
          defaultConfig {
              applicationId "net.abh0518.androidbuildsetting"
              minSdkVersion 16
              targetSdkVersion 16
              versionCode 1
              versionName "1.0"
              //apk파일 이름에 version 정보가 들어가도록 내용 추가
              setProperty("archivesBaseName", "app-v${versionName}c${versionCode}")
          }
          buildTypes {
              release {
                  minifyEnabled false
                  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
              }
          }
      
          //빌드별 Flavor 설정을 해준다.
          productFlavors{
              dev{
                  applicationId "net.abh0518.androidbuildsetting.dev"
              }
              staging{
                  applicationId "net.abh0518.androidbuildsetting.staging"
              }
              product{
                  applicationId "net.abh0518.androidbuildsetting.product"
              }
          }
      }
    3. Flavor에 맞게 리소스 파일 분리
      app/src 디렉토리 아래에 flavor과 동일한 이름의 디렉토리들을 생성하고 실행 환경에 따른 리소스 파일을 분리한다. main이 기본이고 동일한 구성으로 변경 사항들만 dev, staging, product쪽에 추가해준다. 리소스들은 빌드시 선택된 Flavor값들이 main리소스에 override된다. 대충 아래와 같은 느낌. Flavor 관련 자세한 설명은 구글 문서의 Flavor 항목을 참고하자. (https://developer.android.com/studio/build/build-variants.html?hl=ko)
      app - AndroidBuildSetting - [~:AndroidStudioProjects:AndroidBuildSetting] 2016-06-09 10-35-32
  3. 빌드
    1. Android Studio를 사용한다면 Build Variant 메뉴를 사용해서 빌드될 Flavor를 선택할 수 있다.
    2. Termianal사용시는 프로젝트 root에서 gradlew assemble${Flaver name}${BuildType} 명령으로 빌드할 수 있다.
      ./gradlew assembledevDebug //dev를 Debug로 빌드
      ./gradlew assemblestagingDebug //staging을 Debug로 빌드 
      ./gradlew assembleproductDebug //product를 Debug로 빌드
      ./gradlew assembleproductRelease // pruduct를 Release로 빌드, 릴리즈의 경우 gradle에 signing 설정이 되어있지 않아면 unsigned apk가 나온다.

Library Module Bulid 설정

다른 파트들과 협업을 하면서 우리의 코드를 안드로이드 라이브러리(aar)파일로 만들어 배포하기 위한 빌드 설정이다.

  1. 프로젝트에 Android Library 모듈을 추가한다.
  2. 버전 관리를 위해 library module의 java source에 Version.java 를 추가해 준다. 이유는 Android Module을 빌드할때는 Application빌드와 달리 Gradle의 versionCode와 versionName 항목들이 무시되기 때문이다. 따라서 별도로 Version정보 클래스를 제공하지 않으면 Library를 사용하는 사람 입장에서 버전을 확일할 방법이 없어진다. 적당한 위치에 아래처럼 만들어주면 된다.
    1. 대충 이런 느낌
      package net.abh0518.applibrary;
      
      public class Version {
          public final static int versionCode = 1;
          public final static String versionName = "1.0.0";
      }
  3. Library 모듈의 build.gradle 파일을 수정 (여기서는 app_library/build.gradle 파일)
    1. Version.java코드에서 버전정보를 읽어들여 output파일 명에 버전 정보를 추가한다.
    2. Application 의 설정과 동일하게 Flavor를 나누어서 관리하면 좋다. 사용자에게 개발버전, QA버전, 상용버전을 나누어서 제공할 수 있으면 좋지 않을까? 단 Flavor를 추가한 이후 defaultPublishConfig를 반드시 설정해야하는데 이것은 같은 프로젝트 내에서 모듈을 include할때 선택할 Flavor가 된다. Android Studio버그인지 빌드 시스템의 문제인지는 모르겠는데 Studio의 Build Vriant에서 선택해 놓은 Module의 Flavor가 참조하는 쪽의 Flaovr와 연결되진 않고 그냥defaultPublishConfig 설정에 맞춰 빌드되어 참조된다. 근데 뭐 난 Library모듈을 프로젝트 내부에서 쓰진 않는 상황이라 무시하고 그냥 dev로 잡아버렸다.
    3. Flavor를 설정했다면 module의 resource 파일 설정도 Application때와 동일하게 설정해 준다.
    4. Library Module의 build.gradle은 대충 이런 느낌….
      apply plugin: 'com.android.library'
      
      // Android Library 프로젝트는 그래들에서 제공하는 버전 코드와 버전 네임 항목을 무시하므로 버전 관리를 위해서 별도 파일 처리를 해준다.
      def moduleVersionCode  = 0
      def moduleVersionName = "0.9.0"
      
      // Java 소스 파일에서 버전 정보를 읽어온다.
      File versionFile = file('src/main/java/net/abh0518/applibrary/Version.java')
      versionFile.eachLine { line ->
          //버전 코드 가져오기
          def group = (line =~ /versionCode( )*=( )*[0-9]+/)
          if(group.hasGroup() && group.size() > 0){
              moduleVersionCode = group[0][0].toString().replaceAll("versionCode( )*=( )*", "")
          }
      
          //버전 네임 가져오기
          group = (line =~ /versionName( )*=( )*\".*\"/)
          if(group.hasGroup() && group.size() > 0){
              moduleVersionName = group[0][0].toString().replaceAll("(versionName( )*=( )*)|\"", "")
          }
      }
      
      android {
          compileSdkVersion 23
          buildToolsVersion "23.0.2"
      
          defaultConfig {
              minSdkVersion 16
              targetSdkVersion 16
              //output 파일 이름 설정
              setProperty("archivesBaseName", "library-v${moduleVersionName}c${moduleVersionCode}")
          }
      
          buildTypes {
              release {
      //            minifyEnabled false
      //            debuggable false
      //            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
              }
              debug{
      //            minifyEnabled false
      //            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
                  debuggable true
              }
          }
          productFlavors{
              dev{
              }
              staging{
              }
              product{
              }
          }
          //라이브러리 모듈에 Flavor를 추가했을 경우 아래 옵션으로 기본 Flavor를 설정해 주어야 Android Studio에서 Run이 정상 동작을 한다.
          defaultPublishConfig "devDebug"
      }
      
      dependencies {
          compile fileTree(dir: 'libs', include: ['*.jar'])
      }
  4. 빌드해 봅시다.
    1. Studio에서 별도로 aar로 빌드하는 방법은 찾지 못했다. terminal에서 application을 빌드하면 aar은 무더기로 함께 빌드 되어 있었다. module만 따로 빌드하는 방법은 찾지 못했다. 빌드된 aar파일은  “projectRoot/$moduleDir/build/outputs/aar” 에 있다.
    2. Terminal에서 aar만 따로 빌드 할수 있긴 하다. “./gradlew :${moduleName}:aR” 명령으로 가능하다. aar파일 위치는 A와 동일.

      ./gradleW :app_library:aR
  5. 배포된 aar 사용하기, 사용하는 application의 gradle 설정을 아래처럼 하면 된다고 한다. (귀찮아서 테스트 안해봄)
    dependencies {
        compile 'package.name.of.your.aar:myaar@aar'
    }
    
    repositories{
        flatDir{
            dirs 'libs'
        }
    }

귀찮으니까 그냥 한번에 빌드

  1. 고객쪽에 Application dev, staging, product판과 Library Module dev, staging, product 판을 자주 전달해야하는데 매번 flavor 별로 따로 빌드하는게 귀찮다. project rootdptj shell로 그냥 한번에 끝내자. 대충 아래 느낌으로….
    #!/bin/bash
    
    //ouput 파일을 모아놓을 장소
    package_app_dir="release_app_package"
    package_module_dir="release_module_package"
    
    # Application의 build.gradle 파일에서 버전 정보를 가져온다.
    appVersionCode=$(echo "$fullName"  | sed -n '/versionCode /p' ./app/build.gradle)
    appVersionName=$(echo "$fullName"  | sed -n '/versionName /p' ./app/build.gradle)
    [[ $appVersionCode =~ [0-9]+ ]]
    appVersionCode=${BASH_REMATCH[0]}
    [[ $appVersionName =~ [0-9]+.[0-9+.[0-9]+ ]]
    appVersionName=${BASH_REMATCH[0]}
    
    # 패키지 디렉토리 및 그래들 빌드 환경을 초기화
    rm -rf $package_app_dir
    mkdir $package_app_dir
    rm -rf $package_module_dir
    mkdir $package_module_dir
    ./gradlew clean
    
    # 스테이징 디버그, 상용 디버그, 배포용 빌드
    ./gradlew assembledevDebug
    find ./app -name '*-debug.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradlew assemblestagingDebug
    find ./app -name '*-debug.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradlew assembleproductDebug
    find ./app -name '*-debug.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradlew assembleproductRelease
    find ./app -name '*-unsigned.apk' -exec mv {} ./$package_app_dir/ \;
    
    ./gradleW :app_library:aR
    find ./app_library -name '*.aar' -exec mv {} ./$package_module_dir/ \;

GitHub Link : https://github.com/abh0518/android_build_setting

조잡 어플리케이션 시리즈 2-1 : Android SMS Tethering Swich Application Upgraded

조잡 어플리케이션 시리즈 2 : Android SMS Tethering Swich Application 의 마지막에 이야기 했던대로 ‘꺼’ 명령 날리는 것 조차 까먹는 일이 비일비재 해졌다. 인간의 선천적 게으름과 망각은 어쩔 수 없나보다.

아침마다 꺼진 폰을 보고 망연자실 하는 것도 한두번이지 이럴바에 그냥 Keep Alive 시간을 체크하는 Timer를 넣어 보기로 했다. 그리고 하는 김에 자체적으로 테더링을 on/off 할 수 있도록 해봤다. (영어가 개판인건 무시합시다.)

activity_main.xml - sms_tethering_switch - [~:AndroidStudioProjects:sms_tethering_switch] 2016-06-03 13-52-21

별거 없다. 전과 비교해서 화면 상단에 테더링을 on/off 할수 있는 토글 버튼 하나 추가 되었고 하단 부분에 keep alive 설정이 추가되었다. keep alive에 분 단위로 시간을 써주면 해당 시간이 지난 후 테더링을 자동으로 꺼준다. AlarmManager를 쓰면 되는 정말 별거 아닌 기능이다.

이제 아침마다 테더링으로 인해 배터리가 다 날라간 핸드폰을 마주하는 일이 없겠지?

github link : https://github.com/abh0518/sms_tethering_switch

조잡 어플리케이션 시리즈 2 : Android SMS Tethering Swich Application

3년 전에 만들었던 SMS Forward에 이어 최근에 만든 조잡 어플리케이션 2호 Android SMS Tethering Swich Application 이다.

지난 3년간 SMS Forward Application으로 업무폰의 데이터를 개인폰으로 선물하면서 아무런 불편없이 모바일 라이프를 잘 하던 중 어느날 한계점이 찾아왔다. 데이터 선물하기로 개인폰에 보낼 수 있는 데이터는 2기가가 한계인데(SKT 이거 좀 늘려랏!) 어느순간 지하철에서 페북과 유투브 동영상을 자주 보게 되면서 2기가로는 택도 없는 상태가 되었기 때문이다. 그래서 생각난 것이 업무폰을 테더링 머신으로 들고다니면 어떨까? 하는 것이었다.

처음은 뭐 괜찮은듯 했지만 곧바로 새로운 장애물에 부딪혔는데 3년이나 된 업무폰이 배터리가 거의 다 맛이 가서 골골댄다는 것이다. 테더링 2시간이면 폰이 꺼졌다. 업무용 폰을 새로 지급 받을까 했지만 제대로 쓰지도 않을 비싼 폰을 새로 받기는 좀 찜찜하기도 해서 이러지도 저러지도 못하고 있다가 마침 안드로이드 개발자인 나에게 테스트용 새 폰이 지급되었다. 그래서 다음과 같은 뻘짓을 해봤는데

1. 업무폰으로 데이터 쉐어링 서비스 신청해서 데이터 전용 USIM을 발급 받는다. 이 데이터 전용 USIM은 전화 발신, SMS 발신 기능이 제한된다.
2. 개발자 테스트용 폰에 데이터 전용 USIM을 장착한다.
3. 이제 배터리가 빵빵한 개발자 테스트용 폰을 테더링 머신으로 들고다닌다.

제법 괜찮았다. 다만 한가지 불편함이 있는데 아무리 배터리 빵빵한 신형폰도 테더링을 켜놓음녀 10시간도 버티기 어렵다는 것이다. 그래서 테더링 폰을 가방에 넣고 다니면서 필요할때만 테더링을 껏다 켰다 했어야 했는데 이거 은근 불편하고 잠깐만 아차하면 배터리 광탈 당하는 상황을 면치 못했다.

그러다 한가지 재미있는 사실을 알게 되었는데 데이터 전용 USIM도 SMS 수신은 된다는 것이다. 그래서 생각해본게 SMS Foward 비슷하게 SMS로 테더링을 on/off 시키는 앱을 만들어 보는 것이었다. 테더링 on/off 메소드가 public으로 제공되지 않아 난감했지만 우리에게 역시 구글신이 계시다. 여기저기 구글링하니 곧바로 방법이 나오더라. 결국 Java reflection으로 WifiService 객체에 숨겨진 테더링 제어 메소드를 찾아 돌리는 방법밖에 없더라.

여튼 만들고 나니 잘 동작했다. 만든 시점부터 현재까지 SMS로 개발폰의 테더링을 on/off 하고 있는데 확실히 이전보다 불편함이 줄었다. 무엇보다 SKT가 데이터 요금 강화하려는 의도인지 전화/SMS가 무료인 요금제를 내놓아서 문제비용 걱정도 없어졌다.

sms_tethering_application

UI는 SMS Forward보단 좀 복잡해졌다. 일단 SMS로 테더링을 켜는 문자열과 SMS로 테더링을 끄는 문자열을 입력하면 그걸 기준으로 SMS를 필터링 해서 테더링을 on/off한다. 그리고 테더링 제어 결과를 SMS로 알려주는 기능도 넣긴 했는데 안타깝게도 개발에 사용한 데이터 쉐어 USIM이 문자 발송 제한이 걸려있어 제대로 작동하는지 테스트 해보진 못했다.

하루 정도 테스트후 개발폰의 SMS를 열어보면 무슨 정신 분열자가 같은 느낌이 난다. 좌측이 테더링 머신, 우측이 개인폰이다. 테더링 머신의 테더링 결과 문자발송 실패 아이콘을 보면 뭔가 가슴아프다.

sms_tethering_machinesms_my_phone

 

 

 

 

 

 

 

 

 

 

현재까지 불편없이 잘 쓰고 있다. 다만 SMS로 ‘꺼’ 날리는 거 조차 잊어서 종종 방전된 개발폰을 보아야 하는 문제점이 있는데 이건 해결이 불가능 하다. 테더링 켜지고 2시간 뒤 자동으로 꺼지는 기능이라도 추가해야 하나?

github link : https://github.com/abh0518/sms_tethering_switch

조잡 어플리케이션 시리즈 1 : Android SMS Forward Application

3년전 SK Planet에 입사하며 고민중 하나가 회사에서 데이터가 빵빵하게 제공되는 업무폰을 지원해준다는 것이었다. 보통들은 개인폰을 해지하고 업무폰으로 갈아탔지만 난 왠지 모르게 개인폰을 해지하기 싫어서 폰을 두개씩 달고 다니기 시작했다. 아이폰은 계속 쓰고 싶지만 그렇게 하기엔 업무폰을 안드로이드로 선택할 경우 지원되는 티스토어와 호핀 혜택이 너무 빵빵했다는 것도 이유중 하나다.

그래서 개인용 아이폰을 최저요금으로 유지하고 업무폰을 데이터 선물하기와 티스토어, 호핀 전용 머신으로 사용하게 되었다. 그렇게 사용하던 중 불편한 점을 하나 발견했는데 업무폰으로 연계되는 서비스를 사용할 때 마다 방 구석 깊숙히 충전잭에 박혀있는 폰을 꺼내서 SMS인증을 해줘야 한다는 것이다.

그래서 귀차니즘에 안드로이드용 SMS 전달 앱을 만들어 써봤다니 제법 편리해졌다. 데이터 선물하기든 호핀이든 웹이나 아이폰 앱에서 결제한 후 내 개인폰으로 전달된 SMS로 인증하면 끝이이니 이런 신세계가!

sms_forward_application

UI는 그낭 SMS를 전달할 번호 입력하고 save하면 끝이고 SMS는 필터링이고 나발이고 오는건 죄다 전달 해버리는 뭔가 조악한 놈이지만 아직까지 잘 쓰고 있다.

github link : https://github.com/abh0518/sms_forward

주짓수 수련기 2 – Guard

등을 바닥에 대고 누워 상대를 바라본 상태에서 취할 수 있는 가드 자세에 대한 정리다.

  1. Closed Guard
    등을 바닥에 대고 누운 상태에서 양 다리로 상대의 허리를 강하게 감싸안은 모습의 자세다. 주짓수의 다양한 공격들이 파생될 수 있는 매우 공격적인 자세이다. 그냥 보기에는 클로즈 가드를 취한 사람이 바닥에 깔려서 불리해 보이지만 상대의 허리를 감싸안은 다리로 상대방의 무게중심을 흔들어 넘어뜨리거나 관절기등을 걸 수 있기 때문에 실제로는 그 반대이다. Closed Guard를 당하는 입장에서 가드를 탈출하여 상대의 위에 올라타 마운트 자세를 취하는 것을 Closed Guard Pass 라고 한다. 마운트와 가드 패스는 부분은 다음에 정리…

    1. 기본 강좌 1
    2. 강좌 2
    3. 응용 강좌
  2. Open Guard
    클로즈 가드와 반대로 상대허리를 다리로 완전히 감싸안지 못한 상태의 자세이다. 클로즈 가드 상태에서 상대를 얽어맨 발이 풀리거나 상대의 허리가 다리 밖으로 빠져 대치되어있는 상태를 총칭하는거 같은데 명확하지는 않다. 그냥 누운 상대는 다리를 들어 상대를 견제하고 일어선 상대는 상대의 다리를 넘어 마운트를 하려는 대치 상태를 말하는거 같다. 누운 사람이 상대의 허리를 확실히 빼앗지 못한 상태이기 때문에 누가 전적으로 유리하다고 말하기는 어렵다. 초보 입장에서 봤을땐 상대방과의 대치 상태에서 스윕과 마운트를 하거나 클로즈 가드를 취하기 위한 중간 단계라고 생각해도 무방할거 같다. 클로즈 가드와 마찬가지로 다양한 기술들로 상대를 넘어뜨릴수 있다. 그러나 개인적으로 오픈 가드 자세를 오래 유지하는걸 좋아하지 않는다(물론 양쪽이 대치 상태이므로 이게 내 맘대로 되는건 아니다). 오픈 가드에서 기술을 시전하다가 실패했을 경우 마운트 당할 확률이 클로즈 가드에 비해서 좀 높다. 조금이라도 내가 유리하게 클로즈 가드를 가던가 확실하게 스윕 전략을 짜고 스파이더 가드나 데라히바 가드를 취해야 할거 같은 자세이다(초보의 생각). 사실 오픈 가드는 자세라기보단 누운 사람과 서있는 사람의 대치/공방 상태라고 봐야할거 같다. 보통 시합이 시작되면 양측다 서있는 상태에서 보통 곧바로 오픈 가드 상태로 돌입하여 공방이 이어지기 때문이 공격기술을 연마하기 전에 다양한 오픈 가드 동작에 능숙해져야 안정적인 경기를 운영할 수 있을거 같다. 엉덩이 빼기를 잘 연습해 놔야 오픈 가드 상태에서 위치 이동이나 스윕등을 익히기 수월해 진다.

    1. 기본 강좌 1
    2. 기본 강좌 2
    3. 응용 강좌 1
    4. 응용 강좌 2
    5. 응용 강좌 3
  3. Spider Guard
    등을 바닥에 대고 누운 상태에서 양 팔로 각각 상대방의 소매를 붙잡고 상대방의 팔굽 안쪽에 발을 끼우고 밀어넣어 꼼짝 못하게 하는 자세이다. 오픈가드=>스파이더(한손 or 양손)=> 스윕or클로즈 가드 연계가 많이 된다.

    1. 기본 강좌 1
    2. 응용 강좌 1
  4. De la riva Guard (데라히바 가드)
    데라히바 라는 주짓수 괴수(!)분이 창안한 독특한 스타일의 가드다. 초보가 당장 흉내내기엔 어려운거 같다. 기본 자세에서 파생되는 기술이 무수히 많아 매우 유명한 기술이라고 한다.

    1. 강좌

가드 정리는 여기까지! 헉헉!

주짓수 수련기 1 – 기본 동작

주짓수를 다시 배우기 시작은 했는데 주 2회 정도만 하다보니 계속 까먹는다. 그래서 기초 동작부터 하나씩 자료를 정리해보기로했다.

  1. 엉덩이 빼기(새우빼기)
    주짓수 도장에 가면 가장 처음 배우는 동작이다. 처음 해보면 내가 지금 뭐하는 짓인가 싶은데 주짓수의 모든 기술에 응용되는 아주 중요한 동작이다. 개인적으로 뒤에 나오는 브릿지와 함께 주짓수의 시작이자 끝이라고 생각된다. 주짓수 모든 기술중에 이 동작이 안쓰이는게 없을 정도다. 정말 정말 중요한 동작으로 연습을 제대로 하지 않으면 나중에 여러 기술들을 배워도 상대방에거 전혀 먹히지 않는 갑갑한 상황을 만나게 된다. 복싱에 스텝이 있으면 주짓수에는 브릿지와 엉덩이빼기가 있다 고 말하고 싶을 정도다. (이거 써놓고 보니 초보가 건방지네;;) 주짓수가 그래플링과 그라운딩을 주특기로 하는 운동인 관계로 상대방과 바닥에서 뒹구는 상태에서 다양한 기술을 걸기 위해 양측의 몸 사이에 절절한 틈새를 만들고 유리한 위치를 잡아야하는데 이러한 동작들의 핵심이 되는 기초 동작이다. 특히 압박당하는 위기 상황을 탈출하기 위해서는 끊임없이 엉덩이 빼기와 브릿지를 해야할정도로 매우 중요한 동작이다. 초심자도 엉덩이 빼기와 브릿지만 잘해도 엔간해선 쉽게 탭 당하지 않을 정도다.

    1. 기본 강좌 1
    2. 기본 강좌 2
    3. 응용
  2. 브릿지
    엉덩이 빼기와 함께 처음 배우는 동작. 올림픽 레스링에서 자주 보단 그것(?!)과 매우 비슷한데(사실 그동작 맞다) 엉덩이 빼기와 함께 주짓수의 모든 동작에 응용되는 아주 중요한 기초 동작이다. 압박당하는 상황에서 탈출용으로 쓰이기도 하지만 주요 공격 기술인 암바나 초크등을 시전할 때 필요한 강한 허리힘의 원천이 되는 동작이다. (물론 초보인 내 생각일 뿐일수도 있다;;;)

    1. 기본 강좌 1
    2. 응용

첫날 정리는 여기까지…

2. 유전자 알고리즘

집단지성 프로그래밍 이란 책을 보며 알게된 알고리즘이다. 그냥 여기저기 이야기만 들었지 실질적으로 정체를 파악하고 구현을 해보게 된건 이번이 처음이다.

유전자 알고리즘에 대한 자세한 설명은 엔하위키-유전자 알고리즘(http://ko.wikipedia.org/wiki/%EC%9C%A0%EC%A0%84_%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98)에 아주 자세히 잘 나와있다.

그냥 내식대로 간단히 정리하자면 무수히 많은 경우의 수 중 일부의 경우들을 샘플링하여 1세대 데이터군을 만들고 이 데이터들중 최적을 선택하여 교배하고 돌연변이 발생 시켜 더 나은 다음 세대군을 만드는 과정을 여러번 반복하여 최적의 결과를 찾아가는 알고리즘이다. (나 왜이리 글을 못쓰지?)

알고리즘 순서를 대충 요약하면 다음과 같다.

  1. 무수히 많은 경우의 수 중 일부의 경우를 랜덤하게 샘플링하여 하나의 데이터 세트를 구성한다. 이런 한 세트를 세대라고 부른다. 최초 샘플링에 의해서 만들어진 세대를 1세대 라고 한다.
  2. 한 세대가 구성이 되었으면 이중 최적에 가까운 일정 수를 선택한다. 이 선택된 데이터들을 엘리트라고 한다. 엘리트 선택 비율은 딱히 정해져있진 않은거 같다. 난 10% 정도를 엘리트로 선택하기로 했다.
  3. 선택된 엘리트들을 교배(교차)시키거나 돌연변이를 일으켜 이전 세대와 동일한 구성원 수를 갖는 다음 세대를 구성한다.
  4. 새로운 세대로 2~3과정을 여러번 반복한다.
  5. 더 이상 좋은 결과가 나오지 않거나 일정 횟수 이상 반복한 결과를 취한다.

연습삼아 만들어 본 것은 난수로 이루어진 정수들로 부터 0에 근접한 수를 만들어 내는 프로그램이다. 프로그램 돌아가는 과정은 다음과 같다.

  1. 랜덤함수를 이용하여 일정수 이상의 난수를 생성하여 1세대를 구성한다.
  2. 구성된 세대를 오름차순으로 정렬한다. 정렬할때는 절대값을 사용하여 음수도 0보다 큰 수로 정렬되게 한다.
  3. 정렬된 수중에서 0에 가까운 10%의 수를 선택한다.
  4. 버려진 90%의 구성원을 대치할 새로운 구성원을 만든다. 일정한 확률을 정하여 구성원 수가 모두 찰때까지 교차(교배) 혹은 돌연변이를 일으킨다.
    1. 교차(교배) : 선택된 가장 작은 10%수중 랜덤으로 두개를 선택하여 교배(교차)시킨다. 첫번째 수의 앞 16bit, 두번째 수의 뒤 16bit를 취한 값을 합하여 새로운 수를 만든다.
    2. 돌연변이 : 선택된 가장 작은 10% 수중 랜덤을 하나를 선택해서 돌연변이를 일으킨다. 랜덤하게 몇개의 비트를 선택하여 inverse 시킨다.
  5. 2~4를 계속 반복한다.
  6. 최종 결과를 확인한다.

실험 결과는 아래 처럼 나왔다.

  1. 실험 1
    1. 조건 : 세대구성수 100, 세대교체 횟수 100, 엘리트 비율 10%, 돌연변이 발생 확률 10%, 총 시도 횟수 5회
    2. 결과 : -1, 43, 10, 515, -2
  2. 실험 2
    1. 조건 : 세대구성수 1000, 세대교체 횟수 10, 엘리트 비율 10%, 돌연변이 발생 확률 10%, 총 시도 횟수 5회
    2. 결과 : 344, -81, -3588, -1045, 109
  3. 실험 3
    1. 조건 : 세대구성수 10, 세대교체 횟수 1000, 엘리트 비율 10%, 돌연변이 발생 확률 10%, 총 시도 횟수 5회
    2. 결과 : 2, 16, 16, -1, 0
  4. 실험 4
    1. 조건 : 세대구성수 1000, 세대교체 횟수 1000, 엘리트 비율 10%, 돌연변이 발생 확률 10%, 총 시도 횟수 5회
    2. 결과 : 1, 1, 1, 0, 1
  5. 실험 5
    1. 조건 : 세대구성수 10000, 세대교체 횟수 100, 엘리트 비율 10%, 돌연변이 발생 확률 10%, 총 시대 횟수 5회
    2. 결과 : -1, -1, -1, 0, 1
  6. 실험 6
    1. 조건 : 세대구성수 100, 세대교체 횟수 10000, 엘리트 비율 10%, 돌연변이 발생 확률 10%, 총 시대 횟수 5회
    2. 결과 : 1, 0, 1, -1, -1

 

예전에 페북에 끄적거렸던과 달리 실험2와 실험3을 비교해 보면 세대수가 많아도 세대 교체횟수가 적절하지 않으면 그 반대의 경우보다 결과가 좋지 않게 나오는거 같다. 오히려 적은 구성수도 돌연변이가 적절히 발생하면서 세대교체가 많이 이루어지면 자연히 좋은 결과를 내는것 같다. 그리고 실험 4~6을 보면 전체 경우수의 일정 비율 이상을 샘플링 한다면 구성원수와 교체횟수를 극단적으로 치우치게 하지만 않는다면 다 좋은 결과를 도출해 내는것 같다.

실험1 기준으로 32bit 정수 표현의 모든 경우의 수를 비교하는것( O(2^32) )로 반드시 0을 찾을 수 있다.)에 대비하여 100개의 원소를 100번 정렬하는 것( O(100*log(100) * 100) )으로 거의 근사치(2~16)를 구한것을 보면 완벽한 최적값을 요구하는 조건이 아니라면 상당히 유용한 알고리즘인것 같다.

특이한 점은 1과 같이 어느정도 최적치에 근접한 이후부터는 변화가 거의 없다. 아마 세대의 모든 엘리트들의 값이 1로 통일된 상태에서는 교배도 더이상 의미가 없고 돌연변이만을 통해서 0에 도달해야하는데 돌연변이 같은 우연발생으로 최적에 도달하려는 시도는 논리적으로 성공을 보장할 수는 없는 부분이니 운에 맡기는 방법밖에 없기 때문에 그런 것이 아닐까 한다.

결과적으로 유전자 알고리즘은 경우의 수를 모두 따지기 어려운 매우 큰 집단에 대해서 일정 수준의 최적치를 얻기에는 좋은 방법인것 같다. 다만, 완벽한 최적값을 기대한다면 실망할 여지가 크다.

아래는 토비 세가란의 집단지성프로그래밍 책을 참고하여 만든 코드다. net.abh0518.genetic 패키지가 유전자 알고리즘 구현 부분이다.
https://github.com/abh0518/ProgrammingCollectiveIntelligence