스토리 홈

인터뷰

피드

뉴스

조회수 6993

안드로이드 앱의 Persistent data를 제대로 암호화해 보자! (1/2)

들어가기오늘 소개해드릴 글은 안드로이드에서 좀 더 안전하게 파일 시스템에 데이터를 저장하는 방식에 관한 내용입니다. 이 글은 중급 이상, 상급 이하 안드로이드 개발자를 대상으로 작성했으며 완독하는데 약 20분 정도가 필요합니다. 최대한 쉽게 쓰려고 노력했습니다만 이 글이 잘 이해되지 않는 독자분들은 이 문서 말미의 더 보기 섹션에 링크된 외부 문서들을 읽어보시는 편이 좋습니다.1부에서는 Shared preferences 에 저장하는 데이터를 암호화 하는 방식에 대해 다루고 있으며, 2부에서는 데이터베이스를 암호화 하는 방식에 대해 다루겠습니다.내 앱의 데이터, 과연 유출로부터 안전할까?안드로이드 공식 사이트의 저장소 개발 가이드 문서는 데이터를 저장하는 여러 가지 방법을 소개하고 있습니다. 그 중 ‘내부 저장소’ 의 다음 특징은 눈여겨볼 만 합니다.기기의 내부 저장소에 파일을 직접 저장할 수 있습니다. 기본적으로, 내부 저장소에 저장된 파일은 해당 애플리케이션의 전용 파일이며 다른 애플리케이션(및 사용자)은 해당 파일에 액세스할 수 없습니다. 사용자가 애플리케이션을 제거하면 해당 캐시 파일은 제거됩니다.즉, 다른 애플리케이션에 노출하면 곤란한 중요한 정보들은 내부 저장소에 담아두면 안전하다고 할 수 있습니다. 하지만, 정말일까요? 다음 예제를 이용해 내부 저장소에 저장한 사용자의 중요한 정보를 어떻게 탈취하는지 알아보겠습니다. 예제 앱은 충성 사용자에게 보상하기 위해 사용자가 앱을 몇 번 실행시켰는지를 기록합니다.class AppRanTimesRecordingActivity : AppCompatActivity() {    privateval sharedPrefs by lazy {        // Shared preferences 는 Internal storage 에 저장된다.        getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)    }    private var appRanCount = 0    override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        accessToSharedPrefs()        appRanCount++        Toast.makeText(applicationContext, "App has ran $appRanCount times!!", Toast.LENGTH_LONG).show()        finish()    }    override fun onDestroy() {        saveSharedPrefs()        super.onDestroy()    }    private fun accessToSharedPrefs() {         sharedPrefs.run { appRanCount = getInt(KEY_APP_RAN_COUNT, 0) }    }    private fun saveSharedPrefs() {        sharedPrefs.edit().run({            putInt(KEY_APP_RAN_COUNT, appRanCount)            apply()        })    }    companion object {        private const val SHARED_PREF_NAME = "MySecureSettings"        private const val KEY_APP_RAN_COUNT = "appRanCount"    } } [리스트 1] MODE_PRIVATE 로 보호하는 SharedPreferences 사용앱의 데이터는 /data/data/com.securecompany.secureapp 에 저장되어 있습니다만, 앱을 release 모드로 빌드하면 adb 명령으로도 볼 수 없으니 안전하다고 할 수 있을 겁니다. 실제로 adb 명령을 이용해 저장한 파일을 보려고 시도하면 아래와 같은 오류가 발생합니다.$ adb shell "run-as com.securecompany.secureapp ls -al /data/data/com.securecompany.secureapp" run-as: Package 'com.securecompany.secureapp' is not debuggable 그렇다면 디버거로도 볼 수 없으니 내부 저장소에 저장한 데이터가 안전하다고 말 할 수 있을까요?그렇지 않습니다! 안드로이드는 루팅이 매우 손쉬운 운영체제기 때문에 설령 release 모드로 빌드한 앱이라 하더라도 adb 명령을 이용해 모두 접근할 수 있습니다. 루팅한 기기에서 우리가 제작한 SecureApp의 내부 저장소 구조를 아래와 같이 확인할 수 있습니다.$ adb shell "sudo ls -al /data/data/com.securecompany.secureapp" drwxrwx--x u0_a431 u0_a431 2018-06-04 14:15 cache drwxrwx--x u0_a431  u0_a431           2018-06-04 14:15 code_cache drwxrwx--x u0_a431  u0_a431           2018-06-04 14:15 shared_prefs $ adb shell "sudo ls -al /data/data/com.securecompany.secureapp/shared_prefs" -rw-rw---- u0_a431 u0_a431 111 2018-06-04 14:15 MySecureSettings.xml $ adb shell "sudo cat /data/data/com.securecompany.secureapp/shared_prefs/MySecureSettings.xml" <?xml version='1.0' encoding='utf-8' standalone='yes' ?>     별다른 테크닉이 없더라도 인터넷에 널린 수많은 루팅 방법으로 기기를 루팅하면 제아무리 내부 저장소에 저장한 데이터라도 이렇게 손 쉽게 유출이 가능하다는 것을 확인할 수 있습니다. 이런 방식의 보안 기법은 불투명성에 의지한 보안이라고 하여, 방법을 전혀 모르는 공격자에게는 유효한 방식입니다만 이 글을 읽는 독자 수준의 개발자라면 취약점을 금세 파악할 수 있다는 단점이 있습니다.그렇다면 암호화를 적용하면 되지 않을까?맞습니다. 어차피 유출을 피할 수 없다면, 데이터를 암호화하면 됩니다. 그래서 암호화 로직으로 데이터를 암호화해 보도록 하겠습니다. 이 코드는 AES / CBC / PKCS5Padding 방식을 사용해 주어진 데이터를 암호화합니다. 각 용어를 간략하게 설명하자면 다음과 같습니다.AES: 미국에서 개발된 블럭 암호화 방식으로 좀 더 나은 보안성을 가진다. 데이터를 일정 크기(블럭)로 나눠 암호화하며 보통 128비트, 192비트, 256비트 단위로 암호화한다. 키의 길이는 암호화 방식에서 사용할 블럭 크기와 완전히 같아야 하는 특징이 있다.CBC: 블럭을 회전시키는 방식을 말한다. 최초로 소개된 블럭 회전 알고리즘인 ECB(Electronic Code Book) 의 보안 취약점을 해결하기 위한 방식으로 같은 데이터 입력에 대해 완전히 다른 결과를 내므로 보안성이 좀 더 높다. 하지만 CBC 방식을 위해서는 초기화 벡터(Initialisation Vector, IV)를 반드시 사용해야 한다.IV : CBC 블럭 회전방식에 사용하는 초기화 값. 암호화할 데이터와 키가 변하지 않더라도 이 값만 바뀌면 결과가 크게 달라진다. 암호화 key 와는 전혀 무관한 값이기 때문에 외부에 노출되더라도 보안 위협은 적은 편이며 암호화 요청마다 다른 IV 를 사용해 보안성을 높일 수 있다. 다만, 키 길이와 일치하는 길이의 IV 가 필요하다.PKCS5Padding: 블럭 암호화 방식은 입력 데이터의 길이가 블럭의 길이 혹은 그 배수와 일치해야 하는 문제점이 있다. 입력 데이터가 블럭 길이보다 짧을 경우 원칙적으로 암호화가 불가능하다. 이런 어이없는 단점을 보완하기 위한 방식으로, 입력 데이터를 강제로 블럭 크기만큼 맞춰주는 알고리즘의 일종이다.object AESHelper {    /** 키를 외부에 저장할 경우 유출 위험이 있으니까 소스 코드 내에 숨겨둔다. 길이는 16자여야 한다. */    private const val SECRET_KEY = "HelloWorld!!@#$%"    private const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS5PADDING"    fun encrypt(plainText: String, initVector: String): String {        val cipherText = try {            with(Cipher.getInstance(CIPHER_TRANSFORMATION), {                init(Cipher.ENCRYPT_MODE,                         SecretKeySpec(SECRET_KEY.toByteArray(), "AES"),                         IvParameterSpec(initVector.toByteArray()))                return@with doFinal(plainText.toByteArray())            })        } catch (e: GeneralSecurityException) {            // 특정 국가 혹은 저사양 기기에서는 알고리즘 지원하지 않을 수 있음. 특히 중국/인도 대상 기기            e.printStackTrace()            ""        }        return Base64.encodeToString(cipherText, Base64.DEFAULT)    }    fun decrypt(base64CipherText: String, initVector: String): String {        val plainTextBytes = try {            with(Cipher.getInstance(CIPHER_TRANSFORMATION), {                init(Cipher.DECRYPT_MODE,                        SecretKeySpec(SECRET_KEY.toByteArray(), "AES"),                        IvParameterSpec(initVector.toByteArray()))                val cipherText = Base64.decode(base64CipherText, Base64.DEFAULT)                return@with doFinal(cipherText)            })        } catch (e: GeneralSecurityException) {            // 특정 국가 혹은 저사양 기기에서는 알고리즘 지원하지 않을 수 있음. 특히 중국/인도 대상 기기            e.printStackTrace()            ByteArray(0, { i -> 0 })        }        return String(plainTextBytes)    } } [리스트 2] 간단히 구현한 AES128 암호 및 해독 로직그리고 위의 AESHelper 를 이용해 SharedPreference 에 들어갈 자료를 암호화해 봅시다.class MainActivity : AppCompatActivity() {    privateval iv by lazy { lazyInitIv() }    privateval sharedPrefs by lazy {        getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)    }    private var appRanCount = 0    override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)        setContentView(R.layout.activity_main) // Shared preferences 는 Internal storage 에 저장된다. accessToSharedPrefs()        appRanCount++ Toast.makeText(applicationContext, "App has ran $appRanCount times!!", Toast.LENGTH_LONG).show()    }    override fun onDestroy() {        saveSharedPrefs()        super.onDestroy()    }    private fun accessToSharedPrefs() {        sharedPrefs.run({            val appRanCntEncrypted = getString(KEY_APP_RAN_COUNT, "")            if (appRanCntEncrypted.isEmpty()) {                return@run            }            appRanCount = AESHelper.decrypt(appRanCntEncrypted, iv).toInt()        })    }    private fun saveSharedPrefs() {        sharedPrefs.edit().run({            putString(KEY_APP_RAN_COUNT, AESHelper.encrypt(appRanCount.toString(), iv))             apply()        })    }    private fun lazyInitIv(): String {        return sharedPrefs.run({            var iv = getString(KEY_SESSION_IV, "")            if (iv.isEmpty()) {                // 2001년 - 2286년 동안에는 항상 13자리로 나타난다. 그러므로 16자리 IV가 보장된다.                iv = "${System.currentTimeMillis()}000"                edit()                    .putString(KEY_SESSION_IV, iv)                    .apply()            }            return@run iv        })    }    companion object {        private const val SHARED_PREF_NAME = "MySecureSettings"        private const val KEY_APP_RAN_COUNT = "appRanCount"        private const val KEY_SESSION_IV    = "ivForSession"    } } [리스트 3] 리스트 2를 활용해 데이터를 암호화해 저장.저장한 SharedPreferences 를 확인해 보면 다음과 같은 결과를 얻을 수 있습니다.$ adb shell "sudo cat /data/data/com.securecompany.secureapp/shared_prefs/MySecureSettings.xml" <?xml version='1.0' encoding='utf-8' standalone='yes' ?>    1528095873216000    F9dq8ezypMPeUsHpPIUcnQ==     역시 기대대로 암호화되었네요. IV 는 노출돼도 상관없는 정보라고 했으니 괜찮겠죠. 이제 우리 앱의 사용자는 설령 기기를 잃어버리더라도 소중한 정보가 암호화되어 있으니 문제없을 겁니다.라고 생각한다면 오산입니다! 불행히도 안드로이드는 디컴파일이 매우 쉬운 플랫폼이기 때문에 이런 식의 암호화는 사실 그다지 효과가 있지 않습니다. 심지어 IV 가 그대로 노출되어 있기 때문에 공격자에게 큰 힌트가 되었습니다. IV 는 키와는 다른 값이므로 유출되어도 상관없다곤 하지만, 어쨌든 암호화 과정에서 중요하게 다뤄지는 정보임에는 매한가지이므로 사용자에게 굳이 노출할 필요는 없습니다.다소 극단적인 예를 들어 설명했습니다만 요지는 이렇습니다. 어떤 식으로든 우리의 로직 내에서 키를 관리하는 방식으로는 완벽하게 암호화했다고 말할 수 없습니다. AESHelper 소스의 첫 줄에 있는 내용을 다시 한번 살펴봅시다. /** 키를 외부에 저장할 경우 유출 위험이 있으니까 소스 코드내에 숨겨둔다. 길이는 16자여야 한다. */    private const val SECRET_KEY = "HelloWorld!!@#$%" 불행히도 이 소스에 적힌 코멘트는 틀렸습니다. jadx 나 bytecode-viewer 로 획득한 우리 앱의 APK 파일을 디컴파일 해 봅시다.@Metadata(   mv = {1, 1, 10},   bv = {1, 0, 2},   k = 1,   d1 = {"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u000e\n\u0002\b\u0007\bÆ\u0002\u0018\u00002\u00020\u0001B\u0007\b\u0002¢\u0006\u0002\u0010\u0002J\u0016\u0010\u0006\u001a\u00020\u00042\u0006\u0010\u0007\u001a\u00020\u00042\u0006\u0010\b\u001a\u00020\u0004J\u0016\u0010\t\u001a\u00020\u00042\u0006\u0010\n\u001a\u00020\u00042\u0006\u0010\b\u001a\u00020\u0004R\u000e\u0010\u0003\u001a\u00020\u0004X\u0082T¢\u0006\u0002\n\u0000R\u000e\u0010\u0005\u001a\u00020\u0004X\u0082T¢\u0006\u0002\n\u0000¨\u0006\u000b"},   d2 = {"Lcom/securecompany/secureapp/AESHelper;", "", "()V", "CIPHER_TRANSFORMATION", "", "SECRET_KEY", "decrypt", "base64CipherText", "initVector", "encrypt", "plainText", "production sources for module app"} ) public final class zzw {   private static final String A = "HelloWorld!!@#$%";   private static final String B = "AES/CBC/PKCS5PADDING";   public static final zzw INSTANCE; // ... } [리스트 4] 리스트 2를 디컴파일한 결과. 키가 그대로 노출됨을 확인할 수 있다.이름은 난독화했지만 문자열이 그대로 노출된 상태이므로 공격자가 단서를 찾기란 매우 쉬울 겁니다. 더군다나 원본 소스의 내용이 짧으니 아무리 난독화 했더라도 내용을 파악하기란 그리 어렵지도 않을 것이고요.여기서 일부 독자분들은 ‘그럼 이 로직을 JNI 로 만들면 되지 않냐?’ 라고 반문하실 수도 있습니다. 하지만 JNI 로 컴파일한 .so 파일조차 objdump 같은 명령으로 내용을 다 들춰볼 수 있습니다. 특히 Kotlin 구현처럼 static const 형태로 소스코드에 적어두면 공격자 입장에서는 .data 세그먼트 만 확인하면 되죠. 그렇다면 .data 세그먼트를 회피하기 위해 로직으로 키를 생성하도록 작성했다고 해 봅시다. 좀 더 난이도가 올라가긴 하겠지만 숙련된 공격자라면 .text 세그먼트를 이 잡듯이 뒤져 실마리를 찾을 수 있을 겁니다. 물론 이 정도 수준의 역공학을 할 수 있는 사람의 수는 적지만, 아예 없지는 않으니 문제는 여전히 남아 있습니다. 한번 확인해 볼까요?static const char* SECRET_KEY = "HelloWorld!!@#$%" void encrypt(char* plainText, char* initVector, char[] result) {    char* now = malloc(sizeof(char) * 13);    itoa(&time(NULL), now, 10);    char* iv = malloc(sizeof(char) * 16);    strcpy(iv, *now);    strncpy(iv, "000", 3);   const struct AES_ctx aesCtx = { .RoundKey = 16, .Iv = *iv }    AES_init_ctx(aesCtx, SECRET_KEY);    // ... } [리스트 5] C 로 작성한 AESHelper 로직(일부).$ objdump "mySecureApp/build/obj/local/armeabi-v7a/libAESHelper.so" section .data    # 의미 불명의 문자열 발견! 혹시 key 는 아닐까???    00000200 db "HelloWorld!!@#$%", 16    00000210 equ $ - 00000200 section .text    global _start    _start:    # ...        mov rsi, 00000200  # 이 명령 앞뒤로 조사해보면 저 문자열의 용도를 파악할 수 있다.        mov rdx, 00000210        syscall    # ... [리스트 6] ARM EABI V7용으로 컴파일한 바이너리를 디스어셈블 한 결과.즉, 어떤 방식으로 구현하건 암호화에 쓸 키를 소스 코드에 박아두는 것은 그다지 현명한 선택이 아니란 것입니다. 더군다나 안드로이드에서 앱을 만든다는 것은 내 로직이 공격자에게 낱낱이 까발려져 있다는 것을 의미합니다. 중요한 데이터를 .text 에 들어가도록 숨기는 것도 가능하긴 하지만, 그런 방식은 나중에 유지보수하는 사람에게도 골치 아플 겁니다. 소스 코드가 그만큼 어려워질 테니까요. 그리고 그런 방식으로 정보를 숨긴다 하더라도 최정예 크래커 집단, 예를 들어 국정원 같은 수준이라면 그 정도는 큰 어려움 없이 파훼 가능합니다.꿈도 희망도 없는 상황처럼 보입니다만 다행히도 안드로이드는 이런 문제를 해결해 주는 KeyStore API 를 제공하고 있습니다.KeyStore 를 도입하자Android KeyStore 시스템 문서의 첫 머리에 적혀있는 글은 다음과 같습니다.The Android Keystore system lets you store cryptographic keys in a container to make it more difficult to extract from the device. Once keys are in the keystore, they can be used for cryptographic operations with the key material remaining non-exportable. Moreover, it offers facilities to restrict when and how keys can be used, such as requiring user authentication for key use or restricting keys to be used only in certain cryptographic modes.Android Keystore 시스템은 암호화 키를 ‘컨테이너’ 에 저장하도록 해 기기에서 키를 추출하기 더욱 어렵게 해 줍니다. 일단 키를 Keystore 에 저장하면 키를 추출 불가능한 상태로 암호화에 사용할 수 있습니다. 또한 Keystore 는 키 사용 시기와 방법(예: 사용자 인증 등의 상황)을 통제하고, 특정 암호화에서만 키를 사용하도록 허용하는 기능도 제공합니다.좀더 쉽게 다시 설명하자면, 암호화에 쓸 키를 소스코드 내부 어딘가가 아니라, 시스템만이 접근 가능한 어딘가(컨테이너)에 저장해 문제를 해결해 준다는 뜻입니다. 여기서 키가 저장되는 ‘컨테이너’ 는 기기별로 구현이 다를 수 있습니다만 핵심은 사용자 어플리케이션이 그 영역에 접근할 수 없다는 점입니다. 이 때문에 KeyStore 를 사용해서 키를 안전하게 저장할 수 있습니다.또한 앱에서 등록한 KeyStore 는 앱 삭제 시 함께 제거되므로, 똑같은 package name 으로 앱을 덮어씌우는 등의 공격으로 키를 유출할 수도 없습니다. 이는 여러 앱에서 공유하는 KeyChain 과는 다른 특성이며 기능 활성화를 위한 별도의 입력이 필요 없다는 장점이 있습니다.[그림 1] KeyChain API 사용시 나타나는 시스템 다이얼로그. 어려운 용어가 난무하는 등 사용자 경험이 그다지 좋다고 말할 수 없다.반면 Android M 이상에서는, KeyGenParameterSpec.Builder#setUserAuthenticationRequired(boolean) API 로 시스템 다이얼로그의 표시 유무를 제어할 수 있습니다.Secure SharedPreferences 구현하기앞서 설명드렸던 KeyStore 를 사용해 SharedPreferences 의 내용을 암호화하는 로직입니다. 소스 코드의 길이가 꽤 길기에, github gist 링크로 대신합니다. 독자 여러분들을 위해 최대한 쉽고 간단한 형태로 구현했으므로 필요에 맞춰 커스터마이징 하는게 좋습니다.AndroidCipherHelper.kt - KeyStore 에서 생성한 랜덤 패스워드를 이용해 입력받은 문자열을 암호화 하는 로직. IV 설정 등 귀찮은 작업을 피하기 위해 비대칭 암호화 알고리즘을 사용했다. 또한 암호화 및 복호화 과정에서 비대칭키의 Public key 로 암호화하고, Private key 로 해독하도록 구현했다. TEE 를 올바르게 구현한 기기(안드로이드 23 이상 + 메이저 하드웨어 제조사)에서 동작하는 한, 이 데이터의 내용이 유출되더라도 복호화는 오직 이 로직 내부에서만 할 수 있다.SecureSharedPreferences.kt - AndroidCipherHelper 가 문자열 위주로 암호화하므로, 모든 입력값을 문자 형태로 변환 후 입출력한다.결과 확인Secure SharedPreferences 를 실제로 구현한 뒤, 앱의 shared preferences 를 열어보면 아래와 같은 결과가 나타납니다.$ adb shell "run-as com.securecompany.secureapp cat /data/data/com.securecompany.secureapp/shared_prefs/MySecureSettings.xml" <?xml version='1.0' encoding='utf-8' standalone='yes' ?>     oh+XL/vQqAdxNzFEkKVOfcZAkP7jh92tcKpxzM6bbv9iGUk2lR7ayJsR6FZXt3rAKC+4sLVTP1cy e+NpgZ67wjoeBM4maMjXjSkovc8cO8rVVsQLqedJtW3gGOItTTCkjIQGh+TsBDjz8C3IdmNSKqGE GmBwQBoV0QuO+uO6cdPI/Gx816P0kcLmr5xsAy9XUwJeTE9947sYydiztJsgkKxuiGFLJK435pAb UhatjSFse4MpBCugHcLUVg5UXGwQcfbJuuQ/CBcmQmYb3MldNzLfOWtsQiwQJpz0J12fsYlQOBnO UnLVcND+DU17cP+Q4Cjah8VwmiY1a0shMn09Rw==         ozh8dKH+yCRSWoiW0HQtF/bWD7Aw6rfjzklT302AlTOpYmVdEiIfVoTK97bsyK1mXbwN5Qpas82Q dYgnnZl9sfY8pzyXHM0dtm88euB5vgmzljb04LClF3oRZ7Qi5ZRyK90kQ/HN/6EgYvf6zEwR7Ydg 08kJ/bde4Z5lSz+kJ79dHEpE+QAV48U0F0/yp12+xKFRNbaBLBaaWclUNF10jONPKjC3HS/aQozT 1ngQWSKzPq87B0OFExraSPDoLT8zx8ElhTgEtpBRcUwtzmSnhGvgtIUhziFpZBbdvuqAGZ+L5El1 T7H9ipEosN3Aivh/5rz9dntJe3mJvfCFdFITlA==     (Android L 이상이라고 가정할 경우)앱의 개발자조차 키를 알 수 없기 때문에, 파일을 유출하더라도 이를 깨는것은 현재로선 매우 어렵습니다. 즉, 우리 앱은 사용자 데이터를 안전하게 보호하고 있다고 자신 있게 말할 수 있습니다.AndroidKeyStore 파헤쳐보기그렇다면 어떤 방식으로 AndroidKeyStore 가 동작하고, 왜 안전한지 좀더 상세히 살펴보겠습니다.“AndroidKeyStore” 문자열의 중요성Android Keystore system 문서에 따르면 Android Keystore Service 에 접근하기 위해서는 아래와 같이 코드를 작성해야 한다고 합니다. val keyStore = java.security.KeyStore.getInstance("AndroidKeyStore") [리스트 7] Android Keystore 인스턴스 획득 방법여기서 주의할 점은 AndroidKeyStore 라는 문자열입니다. 반드시 정확한 문자열로 입력해야 합니다. 왜냐면 이는 Google 이 안드로이드의 보안 시스템을 Java Cryptography Architecture(JCA) 표준에 맞춰 구현했기 때문에 그렇습니다. 그리고 JCA 표준을 구현하면 JVM 인스턴스(안드로이드도 변형 JVM 의 일종입니다) 내에서 동작하는 모든 로직이 Security 클래스에 등록된 암호화 구현체를 사용할 수 있게 됩니다. 즉, Google 이 컨트롤 할 수 없는 서드파티 로직(우리의 앱 혹은 각종 안드로이드 오픈 소스들)에서도 Android Keystore 를 표준 Java 방식으로 사용할 수 있도록 구현했기에 이런 방식으로 호출해야 하는 겁니다.물론 안드로이드에서는 AIDL 파일을 제공받는 방식 혹은 Context#getSystemService(String) 메소드로 서비스 인스턴스를 획득할 수도 있습니다. 하지만 첫 번째 방식은 바인드된 서비스가 언제든 Kill 될 수 있다는 문제가 있습니다. 그리고 두 방식의 공통적인 문제점은 보안을 사용하는 모든 로직에 if (currentEnvironment == "Android") then... 같은 예외 처리 로직을 넣어줘야 한다는 점입니다. 전 세계 모든 오픈소스 개발자들이 안드로이드로의 포팅을 위해 그런 귀찮은 작업을 해 줘야 하는 일인데.. 그게 가능할까요?“AndroidKeyStore” JCA Provider 등록 과정앞서 AndroidKeyStore 라는 문자열의 중요성을 알아봤습니다. 그렇다면 왜 중요한지도 알아두면 좋겠죠?안드로이드는 linux 기반의 운영체제입니다. 시스템 부팅 직후 실행되는 init.rc 스크립트에서는 /system/bin/app_process 명령을 실행하는데 이 명령은 Android Runtime 위에서 실행되는 Zygote process를 초기화 합니다.Zygote 는 간단하게 설명하자면 안드로이드 앱 실행속도를 향상시키기 위한 일종의 공용 런타임 같은 것입니다. 그리고 앱이 실행되면 Zygote 에 설정된 내용이 사전에 로드되는데, 아까 언급한 초기화 과정 중에 아래와 같은 내용이 있습니다.package com.android.internal.os; /**  * Startup class for the zygote process.  *  * Pre-initializes some classes, and then waits for commands on a UNIX domain  * socket. Based on these commands, forks off child processes that inherit  * the initial state of the VM.  *  * Please see {@link ZygoteConnection.Arguments} for documentation on the  * client protocol.  *  * @hide  */ public class ZygoteInit {    private static final String TAG = "Zygote"; /**     * Register AndroidKeyStoreProvider and warm up the providers that are already registered.      *     * By doing it here we avoid that each app does it when requesting a service from the      * provider for the first time.      */     private static void warmUpJcaProviders() {        // ...        // AndroidKeyStoreProvider.install() manipulates the list of JCA providers to insert        // preferred providers. Note this is not done via security.properties as the JCA providers        // are not on the classpath in the case of, for example, raw dalvikvm runtimes.        AndroidKeyStoreProvider.install();        Log.i(TAG, "Installed AndroidKeyStoreProvider in "                 + (SystemClock.uptimeMillis() - startTime) + "ms.");        // ...    } // ... } [리스트 8] ZygoteInit.java 의 JCA provider 설치 및 속도향상 과정package android.security.keystore; /**  * A provider focused on providing JCA interfaces for the Android KeyStore. *  * @hide  */ public class AndroidKeyStoreProvider extends Provider {    public static final String PROVIDER_NAME = "AndroidKeyStore";    public AndroidKeyStoreProvider() {        super(PROVIDER_NAME, 1.0, "Android KeyStore security provider");        // ...    } /**     * Installs a new instance of this provider.     */    public static void install() {        // ....        Security.addProvider(new AndroidKeyStoreProvider());        // ...    } } [리스트 9] AndroidKeyStoreProvider.java - “AndroidKeyStore” 라는 이름의 JCA provider 등록 과정이런 일련의 과정을 거쳐 시스템에서 등록한 AndroidKeyStore 라는 이름으로 Android KeyStore 서비스에 접근할 수 있게 됩니다. 그리고 안드로이드에서 사용 가능한 KeyStore provider 들의 종류를 뽑아보면, 아래와 같은 결과가 나타납니다.// List all security providers for (Provider p : java.security.Security.getProviders()) {    System.out.println(String.format("== %s ==", p.getName()));    for (Provider.Service s : p.getServices()) {        System.out.println(String.format("- %s", s.getAlgorithm()));    } } output: == AndroidKeyStoreBCWorkaround == == AndroidOpenSSL == ... == AndroidKeyStore ==    - AndroidKeyStore     - HmacSHA256    - AES    ... [리스트 10] 안드로이드 M(6.0.1)에서 지원하는 KeyStore provider 목록(중요) AndroidKeyStore 의 Hardware 레벨 지원 여부 확인다시 Android KeyStore 시스템의 설명으로 돌아가 봅시다.Key material of Android Keystore keys is protected from extraction using two security measures:…Key material may be bound to the secure hardware (e.g., Trusted Execution Environment (TEE), Secure Element (SE)) of the Android device. When this feature is enabled for a key, its key material is never exposed outside of secure hardware.Android KeyStore 는 키의 추출을 방지하기 위해 두 가지 보안 조치를 사용합니다:…키는 안드로이드 기기의 보안 하드웨어(e.g., Trusted Execution Environment (TEE), Secure Element (SE)) 에서만 동작할 수 있습니다. 이 기능이 활성화되면 키는 절대로 보안 하드웨어 밖으로 노출되지 않습니다.그런가보다 싶지만 유심히 읽어봐야 할 대목이 있습니다. 바로 Key material may be bound to … 부분입니다. is 가 아니라 may be 랍니다. 즉, 키가 하드웨어에 저장되지 않을 수도 있다는 사실입니다. 물론 문서에는 언급되어 있지 않지만 안드로이드 시스템 특징상 제조원가 절감을 위해 디바이스 제조사들이 KeyStore 를 소프트웨어로 구현할 수도 있다는 뜻입니다. AOSP 의 Keymaster 구현을 살펴보면 sw_enforced 라는 키워드가 있습니다. 이 keymaster API 를 하드웨어 제조사에서 Keymaster HAL 을 통해 호출하는데 만약 sw_enforced 인스턴스를 넘기는 형태로 구현할 경우 그 하드웨어는 KeyStore 를 지원하지만 (API Level 18), 그것이 반드시 별도의 보안 하드웨어 위에서 동작한다고 말할 수는 없습니다.그리고 “Inside Android Security” 의 저자 Nicolay Elenkov 에 의하면 Android M 이전의 Software-backed KeyStore 는 root 된 기기에서 유출 가능하다고 합니다. 링크의 내용이 다소 길기 때문에 요약하자면 software 기반의 KeyStore 구현은 키를 /data/misc/keystore/user_X(여기서 X 는 uid - 시스템이 앱마다 부여하는 id)에 저장하는데 이 파일의 내용은 keystore-decryptor 로 풀어볼 수 있다고 합니다. 그리고 하드웨어 보안을 지원하지 않는 기기를 확보하지 못해 실 기기에서는 확인할 수 없었습니다만, 에뮬레이터에서 실제로 확인해 본 결과 사실이었습니다.즉, (Android KeyStore)를 쓰더라도 Android M 이전의 기기에서는 우리 앱의 데이터가 100% 안전하다는 장담을 할 수는 없습니다. 아직까지 이 문제를 해결할 방법은 찾지 못했습니다만 아래와 같은 로직으로 ‘이 기기에서의 앱 실행은 안전하지 않을 수 있다’ 같은 안내를 띄우는 정도의 가이드는 개발 가능합니다.val privKey = (keyEntry as KeyStore.PrivateKeyEntry).privateKey val factory = KeyFactory.getInstance(privKey.getAlgorithm(), "AndroidKeyStore") val keyInfo: KeyInfo try {    keyInfo = factory.getKeySpec(privKey, KeyInfo::class.java)    println("HARDWARE-BACKED KEY???? " + keyInfo.isInsideSecureHardware) } catch (e: InvalidKeySpecException) {    // Not an Android KeyStore key. e.printStackTrace() } [리스트 11] KeyInfo API 로 키가 하드웨어로 안전하게 보호되고 있는지를 확인하는 방법다행히도 저희가 보유 중인 개발 시료에서 모두 확인해본 결과 모두 true 로 확인되는 것으로 보아 전 세계의 대중적인 API Level 18 이상인 Android 기기에서는 KeyStore 를 안심하고 사용할 수 있다는 결론을 얻었습니다.다만 API Level L 이전의 Android KeyStore 에는 사용자가 Lock screen 을 설정하지 않을 경우 초기화 된다거나, 직접 확인하진 못했지만 앱을 삭제하더라도 KeyStore 가 완전히 초기화되지 않는 등의 문제도 있다고 하니 유의하는 것이 좋겠습니다.맺으며이상으로 KeyStore 를 사용해 데이터를 암호화하는 방법에 대해 알아봤습니다. 저희 하이퍼커넥트에서도 현재 제작 중인 안드로이드 앱 일부에서 이 기능을 탑재해 고객 여러분들의 데이터를 안전하게 보호하려 노력하고 있습니다. 또한 iOS 도 Secure enclave라 하여 비슷한 기능을 제공하고 있으며 역시 저희 개발진은 이 기술의 적극 도입을 위한 노력을 진행 중입니다.물론 여기 적혀있는 내용들은 Android M(API Level 23) 이후에서만 100% 안전하기 때문에 저희는 그 이전의 안드로이드 버전에서도 데이터를 안전하게 저장할 방법에 대해 지금도 계속 고민 중입니다.또한 눈치 빠른 독자분들은 이 기법을 잘 응용하면 외부 저장소에 저장하는 파일도 암호화 할 수 있다는 사실을 깨달으셨을 겁니다. 이 기법은 요즘 데이터 불법 유출로 몸살을 앓고 있는 웹툰 앱들에도 유용합니다. 임시로 다운로드 한 이미지 파일을 KeyStore 가 생성해주는 키로 암호화해 버리고, WindowManager.LayoutParams#FLAG_SECURE 를 사용해 화면 캡쳐까지도 막아버린다면 대부분의 어설픈 유출 시도는 손쉽게 막으실 수 있으리라 생각합니다.꽤 길었던 1부가 끝났습니다. 2부에서는, 2017년 5월에 소개된 Room을 사용한 안드로이드 데이터베이스를 암호화하는 법에 대해 소개하겠습니다.더 보기Android KeyStore 시스템블록 암호 운용 방식초기화 벡터문자 인코딩AES 암호화RSA 암호화Padding(Cryptography)AOSP KeyStore implementation requirementsHow the Android keystore system can be secureJCA reference guideUnderstanding Android zygote and DalvikVMAndroid InternalsKeystore redesign in Android M - by Nicolay ElenkovAnalysis of Secure Key Storage Solutions on Android#하이퍼커넥트 #개발 #개발자 #안드로이드 #모바일 #앱개발 #PersistentData #개발후기 #인사이트
조회수 1880

입사 후 4개월, 나는 그동안 무엇을 했을까

8월 18일에 입사하여 글을 쓰는 오늘까지 4개월이란 시간이 흘렀다. 트레바리는 4개월을 한 시즌으로 묶어 운영하는 멤버십 서비스이기 때문에 트레바리에서 4개월을 일했다는 건 한 시즌에 필요한 모든 시기를 거쳤다는 의미이다. 4개월을 함께 해야지만 비로소 트레바리를 한 번 했다고 말할 수 있게 된다. 나는 이제서야 트레바리에서 한 번 일했다.트레바리에서의 한 번을 보내며 나는 그동안 무엇을 했는지 정리해보려고 한다. 할 말이 많다 보니 이번 글에서는 기능적으로 무엇을 했는지만 이야기할 예정이다. 어떻게 일했는지, 잘했던 점은 무엇이었는지, 아쉬웠던 점은 얼마나 많았는지에 대한 건 아쉽지만 다음 글에 담기로 했다. 4개월 동안 내가 진행한 일 중 큰 단위의 작업 위주로 살펴보고자 한다.4개월 동안 내가 한 일은 크게 두 가지다. 첫 번째는 기존의 웹 서비스를 개선하는 일. 두 번째는 노가다로 했던 일들에 IT를 끼얹는 일. 두 가지 일에 대한 요구 사항들은 모두 추상적인 문장으로 주어졌고, 나의 역할은 그 추상적인 요구들을 정리하여 실질적인 기능으로 정의하고 구현하는 것이었다. 그동안 어떤 요구 사항들이 있었고 그에 대해 어떤 결과물을 내었는지 정리해보았다.1. 독후감을 활성화되게 만들어주세요.입사 후 최우선으로 개선이 요구됐던 부분은 독후감이었다. 독후감은 트레바리 서비스가 독특하다는 평가를 듣는 이유 중 하나이다. 아무리 돈을 내고 온 멤버일지라도 우리가 내어준 400자의 독후감이라는 숙제를 해오지 않으면 독서 모임에 참가하지 못한다. 우리 크루들은 독후감을 통해 자신의 생각을 한번 정리하고 참가한 독서 모임이 아무런 준비 없이 맞닥뜨리는 독서 모임보다 더 풍성해짐을 안다. 그렇기에 멤버들이 트레바리 홈페이지에서 더 열심히 독후감을 쓰고, 더 많이 다른 사람들의 독후감을 읽고, 더 다양하게 대화할 수 있도록 만들어야 했다.디자인 개선[이전 디자인]일을 시작하자마자 가장 먼저 갈아치우기 시작한 건 디자인이었다. 페이지에 보이는 정보들의 가독성이 나빴다. 독후감 정보와 관련 없는 이미지 배경을 가지고 있었고, 모바일에서는 본문을 포함하여 모든 요소들의 배열이 일정하지 않았다. 가장 문제였던 점은 좋아요 기능이 있음에도 불구하고 유저들이 좋아요 버튼이 있는지를 몰라 활성화가 되지 않는 것이었다.(대표님 얼굴과 우왕이라는 글자가 떡하니 자리 잡은 곳이 좋아요 버튼이다.) 답댓글 없이 한 줄로만 나열된 댓글도 불편했다.[변경한 디자인]전반적으로 컨텐츠가 더 잘 보일 수 있도록 변경했다. 불필요한 배경 이미지를 빼고 책 정보를 추가했다. 좋아요 버튼도 보다 쉽게 인지할 수 있도록 보편적인 모양의 하트로 바꾸었다. 한 줄로만 나왔던 댓글에는 대화하는 듯한 느낌의 UI로 변경하고 답댓글 기능을 추가했다. 특히 모바일에서 더 편하게 쓸 수 있도록 각 요소들을 일정하게 배열했고, 이미지로는 보이지 않겠지만 독후감을 읽고 목록으로 다시 갈 때마다 다른 모임 정보가 뜨는 이상한 시나리오도 개선했다.넛지 만들기더 나은 디자인만으로는 부족했다. 멤버들이 실제로 더 많이 좋아요를 누르고 댓글을 달고 싶게 만들어주는 넛지가 필요했다. 좋아요 수에 따라 재밌는 워딩이 나오고, 댓글 입력 창의 워딩이 항상 다르고 등의 디테일한 요소들을 살렸다. 페이스북 공유하기 기능도 추가했으며 우리 모임에 놀러 오는 멤버들을 보여주는 UI도 추가하는 등의 작업을 진행했다. 하지만 결정적인 한 방이 필요했고 그 한 방은 이달의 독후감 기능이었다.이달의 독후감 선정 기능홈페이지 밖의 운영에서 돌아가던 이달의 독후감이라는 시스템이 있었다. 매 모임마다 가장 좋았던 독후감을 선정하는 것이었는데 잘 알지 못하는 멤버들이 많아 좋아요 수 자체가 적었고, 선정된 독후감을 찾아보기 어려워 활성화가 되지 못했었다. 그래서 이달의 독후감 시스템을 홈페이지에 어워드 형태로 옮겨오면 동기부여와 동시에 별도의 안내 없이도 이달의 독후감 시스템을 학습시킬 수 있겠다고 생각했다.그래서 결과는?결과는 데이터로 나타났다. 디자인과 기능 개선 후 독후감 한 개 당 평균 좋아요/댓글 수가 대략 150% 증가했다. 크루들이 매번 이달의 독후감을 선정하고 하이라이트 구문을 뽑는 오퍼레이션도 줄일 수 있었다. 변경 후 독후감 쓰는 것이 더 즐거워졌다는 멤버 피드백도 종종 들을 수 있었다.2. 멤버십 신청 페이지를 개선해주세요.멤버십 신청 페이지는 트레바리 멤버가 아닌 유저들이 가장 많이 보게 되는 페이지다. 트레바리가 어떤 곳인지 어필하고 결제까지 진행하게 하는 가장 중요한 역할을 하고 있다. 흔히들 말하는 판매 페이지로 트레바리에서 가장 중요한 서비스인 독서 모임을 파는 곳이다. 그 중요성에 비해 디자인과 기능이 모두 엄청나게 부실했고 개선해야 했다.디자인 개선[이전 디자인]대체 트레바리가 어떤 곳인지 알 수 있는 부분이 하나도 없었다. 독서 모임에 대한 설명마저 줄글이 전부였다. 내가 트레바리 독서 모임에 가면 어떤 분위기를 즐길 수 있고 만나는 사람들은 어떨지 상상하기 어려웠다. 모바일에서는 특히 불편했고 필수적인 정보들만 보이는 곳에 불과했다.[변경한 디자인]각 독서 모임에 대한 소개가 풍성하지만 편하도록 변경했다. 이전과 다르게 사진을 많이 활용하여 트레바리 독서 모임이 어떤 분위기인지 보여주고 싶었다. 설명 글도 더 잘 읽힐 수 있도록 배치를 중점적으로 신경 썼고 포인트 컬러를 틈틈이 사용했다. 같은 모임이지만 다양한 시간과 장소가 있는 독서 모임인 경우에는 한 페이지에서 한 번에 볼 수 있도록 구성했다. 모바일 접속자가 압도적으로 많은 만큼 모바일 UI에 많은 시간을 들였다.결제 기능 추가멤버십 신청 페이지에서 가장 큰 문제는 결제였다. 실시간 결제가 아닌 계좌 이체로만 가능하게 되어있다 보니 엄청나게 불편했다. 유저들도 수동으로 이체를 해야 했고, 담당하는 대표님도 24시간 잠도 못자며 휴대폰을 붙잡고 있다가 계좌 이체 알림이 올때마다 등록 처리를 해주어야 했다.(매 시즌 대표님 혼자 몇천 명의 계좌 이체를 확인하고 등록해주셨다. 그래서 멤버십 신청 기간 때에는 제대로 자보신 적이 없다고...)그런데 트레바리는 작은 회사에다 무형의 서비스를 팔고 있다 보니 PG사를 통한 결제를 붙이는 게 어려웠다. PG를 제외한 편하게 결제할 방법을 찾다가 토스 결제를 찾아보게 되었다. 찾자마자 바로 미팅을 진행했고 토스 측에서도 트레바리의 가치를 잘 봐주셔서 미팅부터 결제 연동까지 빠르게 진행하여 구현했다.사랑해요 토스그래서 결과는?막상 개선하여 배포하니 예상보다 저조한 유저 반응이 나타났다. 물론 지난 시즌보다는 훨씬 더 많은 유저분들이 등록하시기는 하였으나 기대했던 목표치에는 못 미쳤다. 디자인이 좋아지고 이용하기 편해지면 당연히 등록 효율이 몇 배로 높아질 거라고 생각했으나 생각처럼 되지 않았다. 신청 기간 내내 저조한 이유에 대한 가설을 세우고, 변경하고, 데이터 보기를 반복했다. 그 과정에서 몇몇 유저분들과 인터뷰를 진행했고 막판에 등록에 영향을 미치는 것은 의외로 홈페이지 사용성이 아닌 다른 곳에 있음을 발견했다. 아쉽게도 늦게 원인을 찾아 더 많은 것을 해보기 전에 신청 기간이 끝나버렸지만, 다음 시즌에는 이번 시즌보다 뾰족하고 탁월하게 개선할 수 있겠다고 생각했다. 영훈님과 함께 공부도 시작해보기로 했다.결제 부분에서는 자세한 데이터를 공개할 수는 없지만 많은 유저들이 토스를 통해 결제를 진행했다. 원래도 트레바리는 N빵 할 일이 많아 토스 송금을 이용하는 유저들이 많았지만 이번 결제 연동을 덕분에 새로 쓰게 된 분도 많아진 것 같았다. 핀테크에 대한 막연한 불안감 때문에 쓰지 않았다는 유저들도 있었지만 막상 써보니 엄청나게 편해서 놀랐다는 피드백도 많이 받았다. 아마도 트레바리에서는 앞으로 계속 토스 송금/결제를 활발하게 사용할 것 같다.3. 트레바리가 어떤 곳인지 보여줍시다.위에서 말한 등록에 영향을 미치는 것은 서비스에 대한 설득이었다. 그동안 트레바리는 지인의 소개로 오는 유저들이 많았고, 기사를 보고 오는 유저들이 많았고, 소문을 듣고 오는 유저들이 많았다. 그래서 따로 트레바리가 어떤 곳인지 잘 설명할 필요성이 적었던 것 같다. 이제는 유저들이 점점 많아지면서 트레바리가 어떤 곳인지 적극적으로 보여줄 필요가 있었고 방치되어 있던 랜딩페이지를 끄집어냈다.[이전 랜딩페이지]이곳만 봐서는 트레바리가 어떤 곳인지 알 수 없었다. 트레바리가 얼마나 매력적인지 어필이 되지 않았고 어떤 활동을 할 수 있는지도 알기 어려웠다.[변경한 랜딩페이지]트레바리가 지향하는 가치들을 더 많이 설명했다. 중간중간 트레바리 사용설명서 영상을 볼 수 있는 곳을 추가했고 실제 멤버들의 후기도 담았다. 트레바리는 독서 모임을 제공하는 서비스이므로 대표적인 독서 모임이 무엇이 있는지도 보여주고 싶었다. 각종 미디어에서 이야기하는 트레바리와 멤버들만을 위한 혜택도 정리해두었다.그래서 결과는?급하게 만드느라 트레바리의 매력을 아직 반의반도 못담았다고 생각한다. 랜딩페이지만 봐도 트레바리가 어떤 곳이고 트레바리를 통해 당신이 얼마나 더 멋있어질 수 있는지를 보여주고 싶다. 랜딩페이지는 꾸준히 만지고 다듬어야 할 과제로 남겨두었다.4. 손에 잡히는 무언가를 주기(일명 손잡무)한 시즌을 끝낸 멤버들에게 각자가 한 시즌 동안 무엇을 해왔는지 쥐여주고 싶었다. 그래서 시즌 말 약 1700명의 멤버들 모두에게 개개인이 이뤄온 활동 데이터를 이미지로 재밌게 엮어 나눠주었다. 기발한 워딩과 이미지는 이 방면에 재능이 있는 세희님과 지현님이 함께해주셨다.[1705 시즌 손잡무][1709 시즌 손잡무]1700명 모두에게 개인화된 이미지를 노가다로 만들어주는 것은 불가능했다. 자동화를 하기 위해서 SQL 쿼리를 통해 필요한 로우 데이터를 추출하고, 스케치라는 디자인 툴을 활용해 이미지 생성을 자동화했다. 덕분에 모든 멤버들의 이미지를 한땀한땀 만드는 노가다를 피하면서도 개인화된 컨텐츠를 제작할 수 있었다. 이 방법은 이다윗님의 코드로 100명 이상의 네임택 한 번에 디자인하기 글을 보고 영감을 받아 가능했다.그래서 결과는?성공적이었다. 인스타그램에 #트레바리를 검색하면 손잡무를 나눠준 시점에 많은 멤버분들이 공유해주신걸 볼 수 있다.(개근상 받으신 분들이 제일 많이 공유해주셨다.) 이미지를 공유해주시면서 4개월 동안 얼마나 즐거웠고 많이 배웠는지에 대한 후기도 소상히 적혀있는 경우도 많아 더욱 뿌듯한 결과물이었다.5. 그 외 각종 버그/개선 요구 사항 해결도 해주세요.각종 UI 및 사용성 개선여러 페이지들의 UI를 개선하고 기능을 개선하여 배포하였다. 자잘한 기능 추가부터 페이지 통째로 갈아엎기까지 손을 댈 수 있는 리소스만큼 건들여보고 개선했다. 이 과정에서는 우선순위를 정하는 일이 중요했는데 우선순위에 대한 이야기는 후에 다시 해볼 예정이다.각종 버그/요구 사항 해결 + 그에 따른 CS내가 만든 것도 많았지만(…) 그거말고도 도대체 개발자가 없을땐 홈페이지가 어떻게 굴러갔지 싶을 정도로 버그가 많았다. 버그도 많고 요구되는 개선 사항도 많았다. 줄어들고는 있지만 아직까지도 버그 및 요구 사항에 응대하는 시간이 하루에 한 시간씩은 꼬박꼬박 들고 있다. 더 많이 줄여나가는 것을 목표로 하고 있다.자동화독서 모임을 이끄는 크루들이 노가다를 하느라 고생하는 시간이 많다. 위에서 이야기 했던 계좌 이체 확인이 가장 큰 사례이다. 그 외에도 개설되는 클럽 데이터 입력을 어드민에서 며칠동안 노가다로 진행해야하는 등의 낭비가 많았다. 이런 부분에서 IT를 끼얹어 공수를 덜 들이고 빠르게 끝낼 수 있도록 엑셀 import 등의 기능을 구현했다.트레바리의 한 번을 끝마치며 나는 그동안 무엇을 했는지 정리해보았다. 쓰다보니 만족스러운 것보다 아쉬운 것들이 눈에 더 많이 들어온다. 무엇이 아쉬웠나 하면 할말이 너무나도 많아 다른 글에 써보기로 하고 이번 글은 기능적인 이야기로만 마무리했다.돌이켜 생각해보면 트레바리에서 쓰이는 기술 스택인 루비도 레일스도, 서버 인프라도 하나 모르는 나를 믿고 이 모든걸 배우고 익힐때까지 기다려준 크루들이 새삼 대단하다고 느낀다. 그 과정에서 실수로 인한 버그도 엄청 많았고 그 버그 때문에 불필요하게 운영 코스트가 늘어났을 때도 있었지만 나무란 적 한 번 없이 격려와 함께 기다려주고 믿어주었다. 그래서 더 열심히 달릴 수 있었던 것 같다.아쉬움과 감사함 때문에라도 다음 4개월에는 일을 더 '잘'하는 사람이 되어야겠다고 다짐했다. 앞으로도 계속 성공하고 실패하고, 배우고 성장한 일들을 꾸준히 기록해나가며 일을 더 잘하는 사람이 되고 싶다. 다음 4개월은 지난 4개월보다 보다 더 실질적이고 큰 변화들을 만들 수 있는 사람이 되어야 겠다. 걱정반 기대반이지만 설레는 마음으로 새로운 시즌을 맞이하며 글을 끝맺으려 한다.어떻게 하면 더 잘할지 고뇌하는 모습의 크루들#트레바리 #기업문화 #조직문화 #CTO #스타트업CTO #CTO의일상 #인사이트
조회수 33949

소규모팀에 적합한 QA 프로세스 구축기(스타일쉐어팀의 QA방식)

안녕하세요. 스타일쉐어에서 PM을 맡고있는 박성환 입니다. 스타일쉐어팀이 QA프로세스를 도입한 것은 약 4개월 정도 되었습니다. 기존에는 QA 프로세스 없이 진행했었지만 주요 기능에 대한 오류감소 및 릴리즈 안정성 확보를 위해 도입을 고민하게 되었습니다.QA프로세스를 처음 도입할때 많은 고민이 있었습니다. 대규모 서비스에 적용하는 QA프로세스를 그대로 도입하기에는 인력 + 시간이 모두 부족했기에 시간과 인력이 많이 투여되는(다만, 안정성이 높음) 명세기반 테스트는 최소화하고, 도입 가능한 서비스(구글플레이의 단계적 배포, Crashlytics)를 활용해 부족한 부분을 커버하는 형식으로 저희 식의 간략화된 QA프로세스를 만들었습니다.(인력 + 시간이 상대적으로 제한적인 스타트업에 좀 더 효율적인 방식.)스타일쉐어팀의 QA 기간 : 앱 업데이트 당 3일(테스트/수정/릴리즈까지의 모든 기간)테스트 인원 : 2명 (1차QA 1명, 최종확인 1명)마이너 버그 수정 버전에서는 QA진행하지 않음스타일쉐어팀의 QA프로세스는 “주요 사용 케이스의 동작 확인” + “수많은 사용 패턴에 대한 대응”으로 정리할 수 있습니다. 저희 팀이 진행하고 있는 방식을 조금 더 자세히 설명해 드리자면 아래와 같습니다.(API 테스트, 자동화 테스트를 제외한 앱 릴리즈 전 진행하는 사용성 테스트에 대한 내용만을 담았습니다.)1. QA일정스타일쉐어 앱의 업데이트 주기는 4주에 1회로 진행하고 있습니다. 그 중 1주 단위의 스프린트가 3주 동안 진행되고 4주차 스프린트는 QA 및 릴리즈 스프린트로 진행됩니다. 매 스프린트에서 담당 엔지니어가 수정 혹은 추가된 단위기능에 대해 간단한 테스트가 끝나면 4주차에 알파 빌드 및 전 구성원이 설치/사용해보고 동시에 1차 QA(통합 테스트)를 진행하게 됩니다. 1차 QA의 버그들을 수정하면 베타버전 빌드 및 최종 확인을 진행한뒤 문제없으면 바로 릴리즈가 되어 사용자에게 신규 버전을 제공합니다.2. 주요 사용 케이스의 동작 확인1) 1차 QA(명세기반 테스팅)4주차에 신규 알파버전이 생성되면 1차 QA를 진행하게 됩니다. 스타일쉐어는 전담 QA담당자가 없습니다. 1차 QA는 다른 파트 엔지니어 1명이 테스트를 진행하고 2차는 PM이 최종확인 후 릴리즈 됩니다. 이 단계에서는 Test case를 바탕으로한 명세기반 테스트로 진행됩니다.테스트 케이스(TC)를 통한 테스팅은 핵심적인 기능 및 주 사용케이스에 대한 검수작업이라고 보시면 됩니다. 게임 혹은 복잡도가 높은 서비스의 경우에는 매 업데이트마다 모든 케이스에 대한 테스트가 어렵고 비효율적이기 때문에 리스크 분석기법, 탐색적 테스팅, 경계값 테스팅 등과 같은 방식을 사용하지만 스타일쉐어 서비스의 경우 상대적으로 복잡도가 낮아 매 업데이트 마다 대부분의 기능에 대한 테스팅을 진행합니다(TC로 100% 커버리지를 목표로 하지 않습니다. 불가능하다는 것을 인정하고 진행하는 것이 효율적). 테스트케이스 작성시에 유의했던 부분은 쉽고 명확하게 케이스를 명시해서 오류에 대한 판단이 명확하도록 하고 스타일쉐어 앱을 처음 본 사람도 바로 테스트가 가능하도록 작성하고 있습니다. (스트레스 테스트는 특이 사항이 있을 경우에만 진행합니다.)2) 교차 테스팅스타일쉐어의 경우에는 1차QA 과정을 담당 엔지니어가 아닌 다른 파트의 엔지니어(iOS버전 테스트의 경우 web, backend, Android 개발자 중 1명이 진행)가 1차 테스트를 진행합니다. 이 방식의 장점은 매번 같은 사람이 테스트하는 것보다 다른 백그라운드를 가진 엔지니어가 테스트 함으로써 다양한 시각으로 테스트를 하게 되 오류발견이라던지 서비스 개선 아이디어를 찾는데 더 효과적이었습니다. 그리고 신규 입사자의 경우 가장 먼저 테스트 담당자로 참여할 수 있도록 합니다(가장 빠르게 서비스 플로우를 이해할 수 있는 방법).3) 최종확인1차 QA 및 전사 베타버전 사용의 피드백을 통해 나온 버그/주요 기능에 대해 마지막 점검하는 절차입니다. 이 부분은 제품책임자(PM)가 담당을 하며, 이 부분을 통과하면 릴리즈 단계로 진행되어 사용자에게 업데이트 된 앱이 전달됩니다.3. 수많은 사용 패턴에 대한 대응단계적 출시(안드로이드)1차 QA과정인 테스트케이스를 통한 테스팅은 명시되어 있는 패턴과 제한적인 환경(Device, 해상도, 인터넷 환경 등등)에서의 주요 케이스에 대한 테스팅만 가능합니다. 하지만 사용자는 수많은 환경 및 사용패턴으로 서비스를 사용하기 때문에 이 부분을 TC의 스크립트로 모두 추가하고 살펴보기란 불가능에 가깝습니다. 그래서 저희 팀은 단계적 출시를 도입해서 대응하고 있습니다.모든 테스트 과정을 완료한 뒤 구글플레이 개발자 콘솔에서 앱 업데이트시 ‘지금 출시’가 아닌 ‘단계적 출시’로 선택합니다. 그리고 비율을 선택할 수 있는데 이 비율은 업데이트가 적용되는 사용자 비율을 설정하는 기능입니다. 즉, 전체 사용자가 아닌 미리 지정한 비율의 사용자에게만 업데이트 버전을 제공함으로써 우선적으로 우리가 예상하지 못한 버그나 불편한 부분이 있는지 확인해볼 수 있습니다. 스타일쉐어팀의 경우 5%의 사용자 비율로 단계적 출시를 1~2일 동안 진행한뒤 버그 리포팅 및 CS내용 확인 후 100% 대상으로 업데이트를 진행합니다.(5% 단계적 출시 이후 패치된 버전을 배포하면 해당그룹(5%)에게만 업데이트 됩니다.)이 부분은 오류에 대한 대응 및 새로운 기능에 대한 부분적인 반응을 볼 수 있는 용도로도 사용할 수 있어 매우 활용도가 높습니다.(신규 앱에 대해서는 해당 기능 사용이 불가능합니다. 업데이트시에만 사용가능합니다.)4. 도입효과1) Crash Free Sessions(Crashlytics)4월 13일 기준으로 Crash Free Sessions는 전체 사용자 중 99.8%의 안정성을 가져가고 있으며(이전에는 95~96%), 기존에는 주말과 같이 사용자가 많은 경우 그만큼 크래시 발생빈도도 높았지만 최근 버전에서는 주말/평일 관계없는 그래프를 보이고 있습니다.2) Crash Report(Flurry)위 지표는 1월~3월 까지의 Flurry의 안드로이드 버전 Crash Report를 캡처한 화면입니다. 1월 초만 해도 일 40회 정도의 크래시가 발생했다면 최근은 일 3~5회 정도로 개선된 모습을 확인할 수 있습니다.5. 마무리다만, 이러한 노력에도 버그는 여전히 존재합니다. 그래서 저희 QA프로세스도 개선할 방향을 모색하고 있는데, 현재의 개선 목표는 ‘퀄리티는 유지하되 속도는 빠르게’ 라는 방향으로 진행 중입니다. 그물을 더 촘촘히 짜듯이 명세기반 테스트의 규모를 늘리는 것에는 시간적/효율적인 한계가 분명히 존재하므로 자동화 테스팅(UI)의 강화를 통해서 부족한 부분을 채워보기 위한 시도를 준비하고 있습니다.하루라도 빠른 서비스의 개선도 매우 중요하지만 그만큼 우리가 전달하고자 하는 것을 문제없이 사용자에게 제공하는 것도 속도만큼 중요하다 생각 합니다. 문제없이 전달하기 위해 계속해서 고민하고 시도해볼 수 있도록 하겠습니다.#스타일쉐어 #개발 #개발팀 #개발자 #노하우 #인사이트
조회수 1407

경험 부족한 스타트업의 devops 도입기 2편

출처 : 구글 이미지 검색그 동안 테스트코드작성, 코드리뷰를 집중적으로 수행했는데요. 아직은 엔지니어 모두가 걸음마 단계여서 실무리듬에 코드리뷰와 TDD를 끼워넣진 않았습니다. 대신 각자 리서치를 수행하고 매주 수요일 SW 세미나에서 lesson&learn 공유하는 식으로 devops를 공부했습니다.회고2주를 되돌아보고 느낌점을 한 문장으로 요약하면 다음과 같습니다.기술부채의 이자율은 고정 값이 아니다. 시간이 흐를수록 점점 더 높아진다.코드리뷰부터 말씀드리겠습니다. android와 iOS의 경우 앱 개발기간 3개월 동안 커밋한 어떠한 코드도 리뷰하지 않은 상황이었습니다. devops를 계기로 두 프로젝트 간의 코드리뷰를 드디어 시작했는데요. 방대한 코드를 빠르게 이해하기 위해 코드리뷰에 앞서 시각화된 자료를 준비해 아키텍쳐리뷰부터 수행하였습니다. 아니나 다를까 두 클라이언트의 유저스토리가 완벽하게 똑같음에도 불구하고 클래스 설계며 구현상의 코드며 개발 상의 내용이 완전히 갈라져 있음을 목도했습니다.출처 : 구글 이미지 검색iOS, android 환경적 차이로인해 어쩔 수 없이 코드의 다름이 나타나는 경우도 있었지만 대다수의 차이는 코드리뷰를 하지 않아서였습니다. 코드리뷰를 진행하면서 조금 신기했던 사실은 아주 간단한 요구사항(기능)도 개발자 개성에따라 구현법이 각양각생이라는 점입니다. 한 가지 문제에도 다양한 해결법이 존재하는만큼 각 구현법 마다 강점과 약점이 존재하기 때문에 코드리뷰의 필요성이 생각보다 더 크다는 점을 깨달았습니다. 앞으로 클라이언트에는 고도화된 유저스토리가 계속 추가될 예정인데 두 클라이언트간 갈라진 구현상의 설계는 분명히 피처 딜리버리에 병목지점으로 작용될 것입니다. 두 갈래로 나뉜 클라이언트를 어떻게 설계적으로 통합시켜 나갈지 지속적으로 고민해봐야 겠습니다. 또한 더 이상 차이가 벌어지지 않도록 지금부터 추가되는 피쳐들이라도 코드리뷰를 수행하는 환경에서 개발되도록 해야할 의무감도 느꼈습니다.테스트 코드도 마찬가지로 기술부채가 생각보다 많이 쌓였음을 깨달았습니다. 스위처의 클라이언트의 기술적 난이도는 낮은 편입니다. 그런데 그럼에도 불구하고 기존 코드에 테스트코드를 입혀 SUT로 만드는 일은 여간 까다로운 일이 아니었습니다. 기존 코드는 비즈니스로직과 I/O(DB,Network, BLE), UI 코드간의 커플링이 높아서 막상 어느것 하나 테스트코드를 입히기 쉽지 않았습니다. 테스트코드를 작성하기 위해서는 논리단위의 클래스들을 떼어내는 리팩토링이 병행되어야만 했습니다. 테스트코드 없이 작성한 코드는 시간이 지날 수록 테스트코드가 비집고 들어갈 틈 또한 점점 없애는듯 합니다. 그래도 이러한 현상들은 몸소 체험하면서 확신을 갖게된 사실도 있었습니다.테스트코드가 존재함으로서 SUT의 설계는 옳은방향으로 향한다.기존 코드에 테스트코드를 입히려고 이리저리 애쓰다보면 무관한 기능들이 뭉쳐있는 비대한 클래스는 발견하게 됩니다. 테스트코드를 입히기 까다로운 이 거대한 클래스를 쪼개야할 필요성을 느끼게 되는데요. 이 시점에서 개발자는 테스트코드가 있기 전에 절대 하지 않던 리팩토링 고민을 하게 됩니다. 치열하게 고민하는 과정에서 리팩토링에 실패하면 제대로된 테스트코드를 작성하기가 불가능해집니다. 즉, 테스트코드를 작성 했다면 분명히 설계상의 리팩토링이 일어 났을 확률이 높습니다.스위처 어플리케이션의 내 주변의 스위처 목록 페이지를 예를 들어보겠습니다. 해당 스크린에서는 유저가 여러개의 스위처를 확인하기 때문에 몇 가지 비즈니스 룰에 의해 스위처들의 정렬 순서가 결정됩니다. 그래서 유저는 여러개의 스위처가 검색되어도 내가 가장 사용할 확률이 높은 스위처를 최상단에서 만나는데요. 그 정렬 역할을 맡은 클래스가 switcher sorting(이름이 잘 기억안나네요..) 입니다.저희 안드로이드 개발자는 이 클래스를 첫 SUT로 만들기로 결정했고 일 주일간 테스트코드를 작성하려고 노력했습니다. 그러나, 생각보다 쉽지 않았습니다. SW세미나때 코드를 리뷰하면서 발견한 사실인데 swithcer sorting는 단순히 비즈니스룰에 사용되는 정보 뿐만 아니라 꽤나 무거운 무거운 switcher 클래스도 의존하고 있었습니다. 정작 sorting 우선순위를 결정하는데 필요한 정보는 switcher 클래스가 갖고있는 정보들 중 극히 일부분이었는데 말이죠. 이렇게 큰 클래스 때문에 테스트 코드를 짜려면 안드로이드 라이브러리인 BluetoothDevice와 Context 인스턴스를 공급하는 목업 클래스가 필요한 상황이 벌어질 수도 있었습니다. 더 큰 문제는 비대한 클래스로 인해서 test의 fixture를 구성하는데 수십 줄의 코드가 필요 했다는 사실입니다. 자연스럽게 테스크코드를 작성하면서 리팩토링의 필요성을 느끼게 되었습니다. 가까운 미래에 스위처 개발자가 성공적으로 switcher sorting 클래스를 SUT로 만들었다면 이 클래스의 설계 또한 분명 리팩토링을 거쳐 더 좋은 방향으로 거듭 났을 것 입니다.앞으로 2주간 할 일어떠한 일이든 균형이 중요하다고 생각합니다. 마냥 기술부채를 털어낸답시고 리서치와 공부만 하고 있을 수는 없습니다. 동아리가 아닌 회사이기 때문에 시장의 니즈에 맞춰서 분명히 다시 피쳐를 개발하는 속도를 높이는 가속 패달을 밟아야 할 시점이 올 것입니다.출처 : 구글 이미지 검색너무 이르지도 않게 그렇다고 너무 느리지도 않게 적절한 시점에 고객이 불만을 터뜨리지 않을 정도의 SW 안정성을 보장하는 최소한의 devops 수준을 달성해야합니다. 어느정도까지가 devops를 도입해야 오버엔지니어링이 아닌 기술부채를 탕감하면서 동시에 I/O 초중기 목표를 달성할 수 있는지 치열하게 고민하고 부딪혀보며 기민하게 대응해야 겠습니다.앞으로의 2주간 할 일은 다음 질문 두 가지에 대한 대답을 하면서 자연스럽게 도출될 것 같습니다.테스트코드 작성을 위한 TDD를 어떻게하면 엔지니어가 효과적으로 학습할 수 있을 것인가?코드리뷰를 스프린트 일과에 어떻게 자연스럽게 안착시킬 것인가?#스위쳐 #Switcher #개발 #개발팀 #문제해결 #인사이트 #DevOPS #데브옵스
조회수 1219

jekyll의 메커니즘을 이해하고 커스터마이징하기

편집자 주-PHP 기반의 서비스를 기준으로 설명했다.-서버의 프로그램은 ‘서버 스크립트’로 표기했다.-HTML/html: 약어로 사용할 경우엔 대문자, 파일명으로 사용할 경우엔 소문자로 표기했다.목차jekyll이 어렵게 느껴지는 이유 jekyll은 모든 화면을 미리 만들어둔다.서버 스크립트 없이 검색 기능을 어떻게 만들까?이미지 캡션 추가이미지 사이즈 대응부록: 글 반영 과정, 도메인 연결 방법, 추가 옵션에 대하여Overview기술 블로그인 브랜디 랩스를 관리하기에 jekyll은 안성맞춤인 도구입니다. 1년 넘게 탈 없이 잘 사용하고 있죠. 물론 커스터마이징을 하려면 고생이 이만저만이 아닙니다. 그 과정은 jekyll을 이용한 Github 블로그 만들기에도 잘 나와있습니다. 도대체, jekyll은 왜 이리도 어려운 걸까요? 브랜디 랩스를 사례로 설명하겠습니다.jekyll이 어렵게 느껴지는 이유일반적인 웹서비스는 정적 리소스와 동적 스크립트의 조합으로 이뤄집니다. 예를 들어 PHP 서비스에서는 정적인 부분을 아파치 웹서버로, 동적인 부분을 PHP 스크립트로 작동합니다.하나의 게시글이 생기면 PHP 스크립트가 데이터베이스에 row 생성을 요청합니다. 게시글 등록 요청을 마치고, 글 목록 화면 요청을 한다면 데이터베이스에서 등록된 글목록을 정리해 HTML 양식으로 응답값을 만들어줄 것입니다.PHP 기반의 블로그 프로그램하지만 jekyll은 컨셉부터 다릅니다. 아주 생소한 메커니즘을 갖고 있습니다. 파일 기반의 데이터를 정적인 리소스로 빌드해서 서비스하죠. 게시글마다 md 파일이나 html 파일을 생성합니다. 글을 작성하고 배포하기 위한 빌드를 진행하면 응답할 html 화면을 만들고, 파일로 저장해 준비합니다. 이 상태에서 유저가 특정 화면을 요청하면 미리 생성한 html 파일을 찾아 꺼내주기만 하면 되죠. 다시 말해, 데이터베이스를 조회하고 HTML 양식으로 응답값을 만드는 과정이 생략되는 것입니다.실제로 Github page가 아파치 서버를 쓰는지는 알 수 없지만 개념 설명을 위해 동일하게 그렸다.jekyll은 모든 화면을 미리 만들어둔다.jekyll은 유저가 요청할 수 있는 모든 화면을 미리 빌드하는 방식을 씁니다. 앞서 다뤘던 브랜디 랩스의 gnav 영역의 회사소개, 채용 화면도 미리 빌드해둬야 합니다. 저자를 소개하는 프로필 페이지도 마찬가지죠. 글이 많아지면서 점점 길어지는 글 목록 화면도 예외는 아닙니다. 글 목록을 보여주는 화면이 많아지만 페이지 수만큼 미리 만들어야 합니다.위의 이미지는 jekyll이 동작하는 메커니즘을 간단히 정리한 것입니다. jekyll을 커스터마이징하려면 완전히 새로운 관점으로 접근해야 합니다. 지금부터는 브랜디 랩스의 검색 기능 구현 과정을 살펴보면서 커스터마이징을 자세히 알아보겠습니다.서버 스크립트 없이 검색 기능을 어떻게 만들까?검색을 하려면 작성된 모든 글의 제목과 내용에 원하는 키워드가 있는지 찾아야 합니다. 하지만 검색어는 변동값이므로 미리 빌드하는 방식으로는 커버할 수 없습니다. 검색어마다 화면을 미리 만들 수 없기 때문입니다.이럴 때는 클라이언트 스크립트는 활용해야 합니다. 서버 스크립트를 쓸 수 없기 때문에 어쩔 수 없는 선택이기도 합니다. 검색에 필요한 정보를 json 파일로 빌드시키고 자바 스크립트를 이용해서 검색하도록 했습니다.먼저 최상위 경로에 search.json을 만듭니다. 파일 시작점에 아래와 같은 패턴이 있다면 빌드 대상으로 인식됩니다.--- --- 이전에 쓴 jekyll 문서를 PDF로 배포하기에서 pdf.html 파일을 만들 때도 비슷한 방법을 사용했습니다.--- --- [ {% for post in site.posts %} { "title" : "{{ post.title | escape }}", "category" : "{{ post.category }}", "tags" : "{{ post.tags | join: ‘, ’ }}", "url" : "{{ site.baseurl }}{{ post.url }}", {% if post.author %}{% for author in site.data.authors %}{% if post.author == author.name %} "author" : "{{author.koname}}", "email" : "{{author.email}}", {% endif %}{% endfor %}{% endif %} "date" : "{{ post.date }}", "content" : "{{ post.content | strip_html | replace: "\", ‘’ | replace: ‘"’, ‘\"’ | replace: ' ‘,’ ' | replace: ' ‘, ’ ' }}" } {% unless forloop.last %},{% endunless %} {% endfor %} ] ▲서머리 데이터를 만드는 json 파일search.json은 모든 페이지의 제목과 내용을 정리해 json으로 만들어야 하기 때문에 site.posts변수를 이용해 만들었습니다. post내용에는 글의 저자, 작성일, 제목, 내용 등 필요한 정보가 있으니 출력하면 됩니다. json을 만드는 것이므로 내용에 “가 들어가면 안되 "으로 치환시켰습니다. 마지막으로 HTML 태그는 검색에 필요하지 않기 때문에 luqid strip_html 함수를 이용해 제거했습니다.http://labs.brandi.co.kr/search.json위의 URL을 클릭하면 브랜디 랩스에서 검색에 사용하는 json을 볼 수 있습니다. 빌드하면 search.json이 만들어지는 것을 확인할 수 있습니다. 이제 json을 로딩하고 해당 키워드를 가진 글을 찾아내기만 하면 됩니다. json 내에 제목과 내용에 입력한 키워드가 있을 때 아래와 같은 UI로 표현했습니다. 기능 구현은 Simple-Jekyll-Search를 이용했습니다. 1)이미지 캡션 추가블로그는 이미지를 많이 사용하고, 상황에 맞게 노출도 해야 합니다. 아래 이미지는 최종적으로 적용한 이미지와 캡션의 결과 화면입니다. {% include figure.html file="/assets/20190415/05.png" alt="05" caption="커스터마이징한 gnav 영역" width="fitcontent" border="true" %} 위와 같이 구성하려고 html과 css를 다음과 같이 구성했습니다. 커스터마이징한 Gnav영역 ▲캡션 html 소스figure { margin: 1em auto; } figcaption { text-align: center; font-weight: bold; color:#999; } ▲캡션에 관련된 css 소스이미지는 가운데 정렬했고, 캡션 텍스트도 옅은 회색으로 가운데 정렬했습니다. 하지만 편집을 담당하는 장근우 대리는 개발자가 아니므로 태그를 입력해달라고 하기엔 무리가 있었습니다. 좀 더 편리한 방식이 없을지 고민하다가 liquid 템플릿의 include 기능을 쓰면 되겠다는 생각이 들었죠. 아래는 브랜디 랩스 원고에 이미지를 넣을 때 쓰는 liquid 문법입니다.{% include figure.html file="/assets/easydebug/5.png" alt="07" caption="커스터마이징한 Gnav영역" %} liquid 템플릿 엔진에서 include할 때 추가 파라미터를 전달할 수 있습니다. file, alt, caption은 파라미터로 전달하고, include되는 파일에서 전달할 내용을 바탕으로 프로그램을 구현할 수 있습니다. {{include.caption}} ▲ /_includes/figure.html이미지 사이즈 대응작은 이미지를 확대하면 이렇게 된다.대부분은 이미지는 화면에 꽉 차지만, 어떤 이미지는 사이즈가 너무 작아 원래의 사이즈로 보여줘야 했습니다.{% include figure.html width="fitcontent" border="true" file="/assets/easydebug/5.png" alt="07" caption="커스터마이징한 Gnav영역" %} ▲사이즈와 외곽 테두리 선에 스펙을 추가했다.추가 전달 인자를 넣고, figure.html 파일에서도 사이즈 대응을 했습니다. {{include.caption}} ▲완성된 /_includes/figure.html 파일figure { margin: 1em auto; } figure.percent100 { width: 100%; } figure.percent90 { width: 90%;} figure.percent80 { width: 80%;} figure.percent70 { width: 70%;} figure.percent60 { width: 60%;} figure.percent50 { width: 50%;} figure.percent40 { width: 40%;} figure.percent30 { width: 30%;} figure.percent20 { width: 20%;} figure.percent15 { width: 15%;} figure.percent10 { width: 10%;} figure.percent5 { width: 5%;} figure.fitcontent { width: fit-content;} figcaption { text-align: center; font-weight: bold; color:#999; } ▲완성된 css이제 원하는 사이즈를 지정해 이미지 상황별 적절한 대응을 할 수 있게 되었습니다.Conclusionjekyll은 브랜디 랩스를 운영하기에 아주 유용한 도구입니다. 기본 템플릿도 훌륭하지만 상황과 편의에 맞게 변경하면 개성 있는 기술 블로그를 만들 수 있을 겁니다. 물론 커스터마이징이 어려울 수 있지만 jekyll의 메커니즘을 이해한다면 금방 적응할 수 있을 겁니다. 이제 블로그를 만들 모든 준비가 끝났습니다. 자, 도전해봅시다!부록1.글 반영 과정jekyll을 이용해서 글을 작성했나요? 이제 Github 저장소에 push하면 글이 반영될 겁니다. push하는 과정을 보면 빌드된 파일을 push하는 게 아니라, 원본에 해당하는 md파일 또는 html 파일을 push하는 걸 알 수 있습니다. push하면 Github page에 바로 반영되지 않고, 몇 분 정도 걸립니다. 이것을 통해 작성한 글이 저장소에 push되면 스케줄러나 트리거에 의해 빌드된다는 걸 유추할 수 있습니다. 아마도 빌드 결과를 위한 저장소가 따로 있고, 빌드된 결과가 저장되는 것이라 예상합니다.2.도메인 연결 방법jekyll 서비스에서는 구매한 도메인을 간편하게 연결할 수 있습니다. 프로젝트의 가장 위쪽에 CNAME 파일을 만들고 push하면 금방 적용됩니다.CNAME 파일3.추가 옵션에 대하여자료를 조사하던 중에 공식 사이트의 빌드 추가 옵션을 찾았지만 0.2초 정도로 큰 차이가 없었습니다. 만약 별도의 옵션이 없다면 빌드 결과는 _site 폴더로 모일 겁니다.공식 사이트 빌드 옵션옵션을 넣어 빌드옵션을 넣지 않고 빌드참고1) GitHub - christian-fei/Simple-Jekyll-Search: A JavaScript library to add search functionality to any Jekyll blog.글천보성 팀장 | R&D 개발2팀[email protected]브랜디, 오직 예쁜 옷만
조회수 1102

Vue, 어디까지 설치해봤니?

Overview새로운 사용환경 구축에 도전하는 건 개발자의 운명과도 같습니다. 오늘은 여러 장점을 가지고 있는 Vue (프론트엔드 자바스크립트 프레임워크)를 도전해보겠습니다. Vue는 다른 프레임워크에 비해 가볍고, 개발하기에 편합니다. 그럼 우선 Vue를 설치합시다! Vue 설치CDNhttps://unpkg.com/vue 주소를 script 태그에 직접 추가 Vue.js 파일다운개발용, 배포용 버전을 다운 받아 script 태그에 추가개발용 버전은 개발에 도움이 되는 모든 경고를 출력하기 때문에 개발 중에만 사용하고, 실제 서비스에서는 배포용 버전으로 사용해야 한다. NPM 설치규모가 큰 프로젝트 경우 컴포넌트별 독립적으로 관리할 수 있는 싱글 파일 컴포넌트 방식 추천 Vue를 설치하는 방법은 여러 가지가 있습니다. 각자 특성에 맞게 편리한 방법으로 설치해주세요. 이번 글에서는 싱글 파일 컴포넌트 방식을 사용할 것이므로 NPM vue-cli 를 설치해 프로젝트를 구성하겠습니다. # vue-cli 전역 설치, 권한에러시 sudo 추가 $ npm install vue-cli -global vue-clivue-cli를 사용하면 뷰 애플리케이션을 개발하기 위한 초기 프로젝트 구조를 쉽게 구성할 수 있습니다. 다만, 싱글 파일 컴포넌트 체계를 사용하려면 .vue 파일을 웹 브라우저가 인식할 수 있는 형태의 파일로 변환해 주는 웹팩(Webpack)이나 브라우저리파이(Browserify)와 같은 도구가 필요합니다. vue-cli 설치 명령어 vue init webpack : 고급 웹팩 기능을 활용한 프로젝트 구성 방식. 테스팅,문법 검사 등을 지원vue init webpack-simple : 웹팩 최소 기능을 활용한 프로젝트 구성 방식. 빠른 화면 프로토타이핑용vue init browserify : 고급 브라우저리파이 기능을 활용한 프로젝트 구성 방식. 테스팅,문법 검사 등을 지원vue init browserify-simple : 브라우저리파이 최소 기능을 활용한 프로젝트 구성 방식. 빠른 화면 프로토타이핑용vue init simple : 최소 뷰 기능만 들어간 HTML 파일 1개 생성vue init pwa : 웹팩 기반의 프로그레시브 웹 앱(PWA, Progressive Web App) 기능을 지원하는 뷰 프로젝트여러 설치 명령어 중에 특성에 맞는 초기 프로젝트를 생성하세요. 1) vue init webpack 실행# 해당 프로젝트 폴더에서 실행 $ vue init webpack   # 현재 디렉토리에서 프로젝트 생성 여부 ? Generate project in current directory? (Y/n) # 프로젝트 이름 ? Project name (vue_ex) # 프로젝트 설명 ? Project description (A Vue.js project) # 프로젝트 작성자 ? Author (곽정섭 ) # 빌드 방식 ? Vue build (Use arrow keys) # vue-router를 설치 여부 ? Install vue-router? (Y/n) # 코드를 보완하기 위해 ESLint를 사용 여부 ? Use ESLint to lint your code? (Y/n) # ESLint 사전 설정 선택 ? Pick an ESLint preset (Use arrow keys) # 단위 테스트 섧정 ? Set up unit tests (Y/n) # 테스트 러너 선택 ? Pick a test runner (Use arrow keys) # Nightwatch로 e2e 테스트를 설정 여부 ? Setup e2e tests with Nightwatch? (Y/n) # 프로젝트가 생성 된 후에`npm install`을 실행해야합니까? ? Should we run `npm install` for you after the project has been created? (recommended) (Use arrow keys) 2) 고급 웹팩 기능을 활용한 프로젝트 구성 방식으로 설치3) 설치완료4) package.json 파일에 설정된 라이브러리 설치$ npm install 5) 개발모드 실행# 해당 프로젝트 폴더에서 실행(소스수정시 자동 새로고침) $ npm run dev 6) http://localhost:8080/ 브라우저 실행7) Yeah, You got it!!!!추가 도구: Vue Devtools(크롬 확장 플러그인)Vue Devtools(크롬 확장 플러그인)은 Vue를 사용할 때, 브라우저에서 사용자 친화적으로 검사하고 디버그할 수 있습니다.크롬 개발자 도구에 Vue 탭이 추가됨ConclusionVue를 설치하는 여러 방법 중 고급 웹팩 기능을 활용한 프로젝트 구성을 알아봤습니다. 다음 글에서는 Vue 인스턴스 및 디렉티브(지시문) 사용법을 다뤄보겠습니다.참고설치방법 — Vue.js 글곽정섭 과장 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유 #Vue
조회수 884

Culturalization of Video Game Soundtracks: An Interview with Pierre Langer, Managing Director & Founder of Dynamedion

 Game culturalization, the process of cultural adaption, is the key to successfully launching video games in foreign markets. The main aspects are to make content suitable, understandable, and meaningful for the gamers of the targeted markets. To achieve these objectives, it is necessary to look into the five central pillars of culturalization: history, religion, ethnic and cultural tensions, geopolitical situations, and in-game elements.One in-game element that must be considered is music. To learn more, we interviewed the video game music expert and composer Pierre Langer, founder and managing director of Dynamedion based in Mainz, Germany. Pierre will tell us more about his internationally renowned company, the video game music business, and the culturalization process of video game soundtracks.  Pierre Langer  Dear Pierre, please let us know more about you and your company and the key services that you provide.  Pierre Langer: Dynamedion was founded by Tilman Sillescu and me in early 2000. We started with work-for-hire audio in the German games industry doing music composition, sound design and later also interactive audio integration and Live Orchestra production. We were the first to produce with live orchestra for a German game, and we eventually rolled this out as a service for other composers and game developers all over the world.Today we are one of the biggest game audio studios in the world with nearly 50 people doing music composition, music licensing, sound design, source sound recordings, audio integration, audio software development, live orchestra and live choir recording, and orchestration and arrangement for all sorts of media. We are still very much focused on video games, having worked on more than 1,800 games, but we also do a lot of movie trailers, TV series, and films.In 2009 we started a sub company of Dynamedion called BOOM library, which produces original sound effects collections as products that can be licensed by audio professionals throughout the world. BOOM Library is today recognized as one of the most popular and high-quality sound effects libraries in the world. Apart from that we also run two side labels with royalty-free stock music in a unique adaptive format (SmartSound) and a new product line of virtual software instruments (SONUSCORE). Our latest addition to our services is that we have become well known for high end vehicle recordings (cars, airplanes, helicopters, bikes, tanks, etc.) – that is a lot of fun, but also a huge challenge to source all sorts of rare or weird or super expensive vehicles.So, in short: we are specialists for everything that has to do with music & sound for games – everything except voice overs, and our music or sound effects or live productions have been used and heard in nearly every large game worldwide. As an example, we recently have been involved in these titles: Assassin’s Creed Series, Elder Scrolls Online, Monster Hunter Online, Battlefield V, League of Legends, Destiny 1 & 2, Lineage II, Horizon Zero Dawn, Fortnite, Mortal Kombat Series, World of Tanks, Hitman Series, Total War Series.Currently we are working on five super large unannounced titles, all international.  What part of the world do your requests mainly come from?  Pierre Langer: It is very international, really. Up until 2009 we had a very strong (overly strong I would say) position in Germany, working on nearly every German game title, quite some in France and some occasional overseas projects. Meanwhile this has completely changed: we are doing a good amount of German titles, but the major part comes from the US, UK, Scandinavia, Japan, Korea and China – China being one of the most important markets now.  Have you experienced a shift or a change over the years in game creation from Western countries to an international mix?  Pierre Langer: Absolutely! It seems that the five big “individual” markets (North America, Europe, China, Japan / Korea) are getting closer to each other. Even very self-sustaining markets, like the Japanese market, are opening up for more international projects coming in, but they are also looking into getting their own games distributed internationally, and of course into becoming as successful as possible worldwide. And then there is a huge amount of projects coming from all the emerging markets, so it seems that there is really no end to a lot of new great games. The biggest challenge with a new game certainly is to make yourself “heard” or do something special that your competition does not do, in order to stand out in a new market.  Orchestral Session - Dynamedion  What is culturalization in terms of video game soundtracks and sound effect production?  Pierre Langer: It is actually a very straightforward thing and kind of a no-brainer, since audio is a rather inexpensive asset for a game, while it has a huge emotional and atmospheric impact. Culturalization of a game means that you adapt the game to the specific requests of a new market. Western world audiences are used to different things than Chinese players, for example. So, if a Chinese game developer wants to push a game into the Western market, the game should be “westernized” so to say. This certainly already happens with gameplay mechanics and with graphics and – of course – with the localization. But simply changing the texts and voice over from Chinese to English doesn’t adapt a Chinese game to an EU or US audience. The look and feel of a game need to change as well, and this is where music and sound “culturalization” comes in: adapting the music and sounds (and the way of implementation and audio functionality in the game) to the specific audience that is being targeted. This does of course work in all directions – Japan to China, China to Europe, Europe to Korea, etc.  Can you give us some examples of audio culturalization in specific markets? (E.g. MENA, South America, China/Asia)  Pierre Langer: Let me go back a few years, to our very first larger game title we did music and sound culturalization for. It was “Runes of Magic” by Runewaker Entertainment, a developer based in Taiwan. The game was not extremely successful in Taiwan and Mainland China, but a German publisher by the time (Frogster) saw some great potential in that game. So, they licensed the title and got the rights to publish it in Europe and the US. In some respects, the game was a mess for a Western audience, partly due to the music and the sound + the implementation of all audio. The marketing people at Frogster understood this very quickly and started working on all these issues. The music and sound side was done in a matter of a few weeks: they asked us to replace the soundtrack by using music we had in our back catalogue (music for games that we had written, that either failed, or that had been unsuccessful – which we kept the rights to) and write a few new themes that would work as the iconic main themes of the game, so that the audience has something new and recognizable. We did that, with a full focus on writing and licensing music that would be ideal for the target audience. Then we did a similar thing with the sound effects: we simply threw out all the stuff that was in there and replaced it with sounds that where produced to fit a Western audience. To give you a very quick example: Asian players are used to high frequency sounds, very aggressive, very loud, the whole sound atmosphere being very crowded. European and US players are used to low frequency sounds – sub-bass, deep impacts, rumbling and more focused sound design (you hear one thing prominently, and everything else gets balanced down to make space for the one important sound going on). This is a very clear and super important difference – and it is also easy to fix with some new content and some new mixing.  What are typical issues that occur in sound culturalization?  Pierre Langer: Typical issues are that there needs to be some trust from the developer to the sound team. In most cases, the developer asks for culturalization from their home market to a foreign market. So, a US developer asking us to adapt the sound to fit a Chinese audience better needs to trust us that we know what we are doing, since the US developer doesn’t know themselves (otherwise they wouldn’t need us). Then there is always a big challenge with the correct audio integration. The most important bit is certainly to replace music and sound effects, to get a fitting new set of assets for the target market. However, even the best assets do not help if they are poorly integrated. Simply swapping them is not enough if the way they are being played back is not fitting. This then needs some more time and attention and focus, since we need to work with the developer directly to e.g. add some audio functionality, balance mix and master the audio, or introduce an interactive music system. It can be a very elaborate thing, but you can achieve a lot of additional quality with the most basic strategies that only cost a lower 5 digit budget.  Dear Pierre, thank you for your time and effort in providing us such enlightening insights into your work!About Pierre:Pierre was born near Frankfurt / Germany. After years of playing in bands as a guitar player in his teens, he decided to take his studies in classical music at the Johannes Gutenberg University in Mainz..A few months before his final exams he met Tilman Sillescu in early 2000, Dynamedion was founded a few weeks later. In the first years of Dynamedion Pierre worked on basically every single bit of the job you can do as an audio person in the games business: music composition, sound design, audio integration, audio management, design of audio tool chains, recording, mixing, mastering, project management, etc.As the thing grew and all the other guys joined in, Pierre focused more and more on the business side of things, leaving the creative work to the really focused experts.Nowadays Pierre enjoys keeping in touch with all the different clients of Dynamedion, thinking up new product lines and business ideas to further expand the reach and prominence of Dynamedion and all related sub-labels such as BOOM Library, Sonic Liberty, Sonuscore... and more to come.The Interview was conducted by Moritz Demmig. 
조회수 1247

EOS Token 생성과 발행, 전송

이번시간에는 배포한 Contract를 통해 Token 발행과 전송을 해보겠습니다. 이를 위한 준비는 아래 2미디엄 글을 참조해주세요EOS Smart Contract 를 위한 준비EOS Smart Contract 배포먼저 저번 시간에 배포한 token 발행 abi 를 확인해 보겠습니다.$ cleos get abi hexlanthenryget abiabi를 확인하다보면 actions 라는 항목에 총 3개의 action이 있음을 확인할 수 있습니다. 이 3개의 name이 실행할 수 있는 action입니다. token발행은 create action을 통해 진행할 수 있습니다.Token 생성$ cleos push action hexlanthenry create '["hexlanthenry", "10000000000.0000 HEX"]' -p hexlanthenrycreate action 실행 결과create action 을 통해 ‘HEX’ 토큰을 100억개 생성했습니다. create 라는 action의 인자는 account_name(hexlanthenry), maximum_supply(10000000000.0000 HEX) 입니다. 즉 첫번째 인자는 토큰의 발행자를 나타내며, 두번째 인자는 토큰의 최대 수량을 나타냅니다.이 인자가 어떻게 들어가는지는 abi 의 struct 를 확인하면 알 수 있습니다.abi의 create structparameter 1 : account_name type— issuerparameter 2 : asset type — maximum_supply+ 저번 강의에서 공지한데로 다음 포스팅에서는 abi가 무엇을 뜻하는지, 이를 통해 어떻게 action을 실행할 수 있는지 알아보도록 하겠습니다.Token 발행생성과 발행 이 2개의 개념이 헷갈릴 수 있습니다. create action을 통한 생성은 최대 발행량을 결정 하는 것이며, issue action 은 토큰을 유통 시키는 것입니다.create : token 생성과 동시에 최대 발행량 결정issue : token 의 유통따라서 issue action을 통해 이전에 생성한 HEX token을 발행해보겠습니다.$ cleos push action hexlanthenry issue '["hexlanthenry", "10000.0000 HEX", "initial issue"]' -p hexlanthenryissue contract 실행 결과issue action 역시 data로 어떤 인자가 들어가는지는 abi를 통해 확인 가능합니다.abi의 issue structparameter 1 : account_name type — toparameter 2 : asset type — quantityparameter 3 : string type — memomemo 는 transfer 가 어떤 목적인지에 대해 설명해주는 인자 입니다. 생략해도 되는 값으로, 원하시면 parameter 개수를 유지하는 선에서 empty string을 넣으시면 됩니다. memo를 어떻게 쓰면 유용한지에 대해서도 다른 포스팅에 담도록 하겠습니다.issue가 잘 실행 되었는지 확인해 보겠습니다.$ cleos get currency balance hexlanthenry hexlanthenry저는 issue 를 4번 수행한 후 balance 를 체크 했기 때문에 총 40000개의 HEX token이 존재하는 것을 확인 할 수 있습니다.hexlanthenry 의 HEX token개수예외사항1create 하지 않은 token을 issue 할 경우해당 symbol 이 존재하지 않음예외사항2생성한 token 수보다 많은 양을 issue 할 경우maximum supply를 초과함Token transfer마지막으로 token을 다른 계정에 전송 해보도록 하겠습니다. 다른계정에 token을 보내야 하기 때문에 계정을 생성하거나 존재하고 있는 계정을 사용하시면 됩니다.아래 명령으로 hexlanthenry 계정이 babylion1234 계정으로 10000개의 HEX 토큰을 보냅니다.$ cleos push action hexlanthenry transfer '["hexlanthenry", "babylion1234", "10000.0000 HEX", "first"]' -p hexlanthenrytransfer 실행결과transfer 시 들어가는 data에 대해서도 abi를 확인해보겠습니다. 다른 action보다 많은 인자를 필요로 합니다. [“hexlanthenry”, “babylion1234”, “10000.0000 HEX”, “first”]abi의 transfer structparameter 1 : account_name type — fromparameter 2 : account_name type — toparameter 3 : asset type — quantityparameter 4 : string type — memo실제로 babylion1234 계정을 확인해 보면, 방금 배포한 HEX token을 보유하고있는 것을 확인할 수 있습니다.babylion1234의 HEX 보유이번 포스팅에서는 token을 생성과 발행 그리고 전송을 다뤄봤습니다. EOS는 Ethereum 과 달리 토큰 발행을 매우 쉽게 진행할 수 있습니다. 이 두 dapp의 차이에 대해서도 포스팅을 하고 싶으나 우선 다음 포스팅에서는 contract 개발의 기초를 다루도록 하겠습니다.감사합니다.#헥슬란트 #HEXLANT #블록체인 #개발자 #개발팀 #기술기업 #기술중심
조회수 22530

Next.js 튜토리얼 9편: 배포하기

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기 2편: 페이지 이동 3편: 공유 컴포넌트4편: 동적 페이지5편: 라우트 마스킹6편: 서버 사이드7편: 데이터 가져오기8편: 컴포넌트 스타일링9편: 배포하기 - 현재 글개요아래와 같은 궁금증이 생긴 적이 있나요?어떻게 내가 만든 Next.js 애플리케이션을 배포할 수 있나요?아직 배포에 대해 이야기하지 않았지만 배포하는 것은 꽤 간단하고 직관적입니다.Node.js를 동작할 수 있는 곳이라면 어디든 Next.js 애플리케이션을 배포할 수 있습니다. 매우 간단한 ▲ZEIT now로 배포하는 데에도 불구하고 어떤 잠금 장치도 없습니다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.Build와 Start처음으로 프로덕션에 우리의 Next.js 애플리케이션을 빌드해야 합니다. 빌드는 최적화된 프로덕션의 코드 세트를 생산합니다.이를 위해 간단히 다음의 npm 스크립트를 추가하세요:그런 다음 하나의 포트에서 Next.js를 시작해야 합니다. 사이드 렌더링을 수행하고 페이지를 제공합니다. (위의 명령으로 빌드됩니다)이를 위해 다음의 npm 스크립트를 추가하세요:이러면 3000 포트에서 우리의 애플리케이션이 시작됩니다.이제 프로덕션에서 애플리케이션을 동작시키 위해 다음의 명령어를 실행할 수 있습니다:두 개의 인스턴스 실행하기애플리케이션의 인스턴스 두 개를 실행시켜 봅시다. 대부분 앱을 수평으로 확장하기 위해 이 작업을 수행합니다. 처음으로 start npm 스크립트를 다음과 같이 변경해봅시다:만약 Winodws라면 next start -p %PORT%로 스크립트를 변경해야 합니다.이제 애플리케이션을 처음으로 빌드해봅시다.npm run build그러면 터미널에서 다음의 명령어로 실행시켜 봅시다:PORT=8000 npm startPORT=9000 npm startWinodws에서는 다른 명령어를 실행시켜야 합니다. 하나의 옵션은 애플리케이션에 cross-env npm 모듈을 설치하는 것입니다.그런 다음 커맨드 라인에서 cross-env PORT=9000 npm start를 동작시켜 주세요.두 개의 포트 모두에서 애플리케이션에 접근할 수 있나요?- 네. http://localhost:8000와 http://localhost:9000 둘 다 접근할 수 있습니다.- http://localhost:8000에서만 접근 가능합니다.- http://localhost:9000에서만 접근 가능합니다.- 둘 다 접근할 수 없습니다.한 번의 빌드로 많은 인스턴스 실행시키기보다시피 애플리케이션을 한 번 빌드해야 합니다. 그런 다음 원하는만큼의 많은 포트들을 시작할 수 있습니다.▲ZEIT now에 배포하기Next.js 애플리케이션을 빌드하고 시작하는 방법을 배웠습니다. npm 스크립트를 사용하여 모든 것을 수행했습니다. 그래서 원하는 배포 서비스를 사용해서 동작하도록 애플리케이션을 설정할 수 있습니다.하지만 ▲ZEIT now를 사용하면 딱 한 번의 과정만 수행하면 됩니다.다음과 같은 npm 스크립트만 추가해주세요:그런 다음 now를 설치해주세요. 설치 후 다음 명령어를 적용해주세요:now기본적으로 애플리케이션의 루트 디렉터리 안에서 "now" 명령어를 실행합니다.여기에서 애플리케이션을 시작하는 포트로 8000 포트를 지정했지만 ZEIT now에 배포할 때 변경하지 않았습니다.그러면 ZEIT now에 배포할 때 애플리케이션에 접근할 수 있는 포트는 어떤 것일까요?- 8000- 443 (혹은 언급되는 포트가 없음)- URL에 언급한 모든 포트- 에러를 표시한다. "443 포트에서만 시작할 수 있습니다"ZEIT는 항상 443 포트를 사용합니다실제로 8000 포트에서 애플리케이션을 시작해도 now에 배포될 때는 443 포트를 사용해서 접근할 수 있습니다. ("https" 웹사이트의 기본 포트)이것은 ▲ZEIT now의 특징입니다. 원하는 포트에서 애플리케이션을 시작해야 합니다. ▲ZEIT now는 항상 443 포트로 매핑합니다.로컬에서 애플리케이션 빌드하기▲ZEIT now는 npm build 스크립트를 발견하고 빌드 인프라 내부에 빌드합니다.하지만 모든 호스팅 제공자가 이와 같은 것을 가지고 있지는 않습니다.이 경우 로컬에서 다음의 명령어를 사용해서 빌드할 수 있습니다:npm run build그런 다음 .next 디렉터리를 사용하여 애플리케이션을 배포하세요.커스텀 서버를 사용하여 애플리케이션 배포하기우리가 막 배포한 애플리케이션은 커스텀 서버 코드를 사용하지 않았습니다. 하지만 만약 사용한 경우에는 어떻게 배포할 수 있을까요?다음의 브랜치로 체크아웃하세요:커스텀 서버를 사용하여 애플리케이션을 실행하기 위해 애플리케이션에 Express를 추가해주세요:npm install --save express애플리케이션 빌드하기이를 위해 next build를 사용하여 애플리케이션을 배포할 수 있습니다. 다음의 npm 스크립트를 추가해주세요:애플리케이션 시작하기프로덕션 애플리케이션임을 알리기 위해 커스텀 서버 코드를 생성해야 합니다.이를 위해 server.js로부터 이 코드를 살펴봅시다.이 부분을 살펴봅시다:그러면 프로덕션으로 이와 같이 애플리케이션을 시작할 수 있습니다.그래서 "npm start" 스크립트는 다음처럼 변경됩니다:마무리Next.js 애플리케이션을 배포하는 것에 대해 거의 다 배웠습니다.문서에서 Next.js 배포하기에 대해 더 배울 수 있습니다.배포에 대한 질문이 있다면 자유롭게 Slack에서 물어보거나 issue를 제출하세요.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 3062

코딩, 어떤 언어로 시작하지?

경영학과 학생 윤수는 요즘 주변에서 이런 말을 자주 듣는다."앞으로는 코딩을 모르면 문맹이다.""4차 산업혁명 시대에 대비해야 한다.""소프트웨어가 세상을 잡아먹고 있다."정확히 뭔 소리인지는 모르겠지만 일단 프로그래밍을 배우기로 결심한다. 그런데 '프로그래밍 언어'라는 게 또 뭐가 이렇게 많은지... 어떤 걸로 시작해야 할지 도무지 감이 안 잡힌다.그래서 공대생 친구들에게 물어본다. 친구 1: "일단 가장 기본인 C부터 배워."친구 2: "한국에서 제일 많이 쓰는 자바부터 배워."친구 3: "파이썬이 배우기 쉬워."친구 4: "요즘은 자바스크립트가 대세지."누가 정답일까? 사실 이 중에 "틀린" 사람은 없다. 각자 관심 분야와 목적이 다르기 때문이다. 만약 다음 달에 아이폰 어플을 개발해야 한다면 어쩔 수 없이 곧장 스위프트를 배워야 한다. 내일모레까지 사이트 레이아웃을 완성해야 한다면 더 물을 것도 없이 그냥 HTML과 CSS를 시작하면 된다. 그러나 일반적으로 첫 프로그래밍 언어를 추천하라면 내 톱 초이스는 단연 파이썬, 그다음은 HTML/CSS + 자바스크립트일 것이다. 이 3개의 기준으로 평가하였다:1. 배우기에 얼마나 어려운 언어인가?2. 이 언어에 대한 수요가 얼마나 있는가?3. 나는 프로그래밍으로 뭘 하고 싶은가?*우연히 보게 된 CSDojo라는 유튜브 채널에서 너무 훌륭하게 정리해줘서 참고하였다.1. 배우기에 얼마나 어려운 언어인가?나는 고등학교 때 C를 독학하는 것으로 프로그래밍에 입문했다가 금방 질려서 그만두었다. 창업에 관심이 생겨 다시 시작했는데 이번에는 파이썬으로 배워보았다. 그제야 흥미를 느꼈고, 프로그래밍이 마냥 어렵기만 한 게 아니라는 걸 깨달았다.어떤 차이가 있는 걸까?파이썬은 C보다 쓰기 쉽다. C로 수백 줄을 써야 하는 프로그램을 파이썬 몇십 줄로 쓸 수 있다. 파이썬 코드 몇 줄이면 쓸모 있고 흥미로운 결과물을 만들어낼 수 있다는 뜻이다. "Life is short, use Python"이라는 말이 있을 정도다. 물론 파이썬이 무조건적으로 C보다 좋은 건 아니다. 파이썬 프로그램은 C 프로그램보다 느리기 때문에 특정 업무에는 적합하지 않다. 하지만 그런 건 지금 신경 쓸 필요가 없다. 첫 프로그래밍 언어로 "컴퓨터적인 사고력"을 익히고 나면 새로운 언어를 배우는 것은 어렵지 않기 때문에, 일단은 배우기 쉬운 언어로 시작해라.비교적 배우기 쉬운 언어:    - Python    - Ruby    - JavaScript2. 이 언어에 대한 수요가 얼마나 있는가?시장에서 필요로 하는 언어를 배우는 게 좋다. 세계에서 가장 큰 웹사이트들은 어떤 기술을 사용하는지 살펴보자:출저: 위키피디아위 테이블에는 미국의 웹 서비스들만 정리되어 있다. 지역과 직군에 따라 요구하는 언어가 다르기 때문에 로켓펀치, 더팀스, 위시켓, 링크드인, 인디드, 사람인, 잡코리아 등의 구인/구직 사이트에서 직접 살펴보는 걸 추천한다.인기가 많은 언어일수록 커뮤니티가 크기 때문에, 도움을 받을 수 있는 자료들이 많고 가져다 쓸 수 있는 코드가 많다는 장점도 있다. 세계 최대 규모의 프로그래밍 커뮤니티인 스택오버플로우의 언어 점유율을 참고해보자.출저: 스택오버플로우이 중에서 HTML/CSS(웹 레이아웃), SQL(데이터베이스), Bash/Shell(Unix)은 아주 특수한 경우에 쓰이는 언어이기 때문에 제외하고 보자. 수요가 높은 언어:    - JavaScript    - Java    - Python3. 나는 프로그래밍으로 뭘 하고 싶은가?분야별로 자주 사용되는 언어가 있다. 간단하게 정리하자면:1. 데이터 과학, 공학 => Python, R, MATLAB2. 웹 프런트엔드 => HTML/CSS + JavaScript3. 웹 백엔드 (서버) => Python, Ruby, JavaScript, Java, Go, C, C++, PHP 등4. 아이폰 어플 => Objective-C, Swift (이제는 거의 Swift로 넘어갔다)5. 안드로이드 어플 => Java, Kotlin (슬슬 Kotlin으로 넘어가고 있다)6. 게임 개발 => C#, C++7. 임베디드 시스템 => C, C++결론: 왜 파이썬, 자바스크립트인가?앞서 이야기했듯 당장 급히 배워야 하는 언어가 있으면 그 언어를 배우면 된다. 교수님이 연구실에서 다음 주부터 C언어를 쓴다고 하면, 뭐 어쩌겠나? 그냥 당장 C를 배우는 수밖에. 하지만 조금 더 여유롭게, 제대로 프로그래밍을 배우고 싶다면 이 포스트에 나와 있는 세 가지 기준을 고려해서 결정하는 걸 추천한다. 배우기 쉬운 언어로는 파이썬, 루비, 자바스크립트를 선정했고, 수요가 높은 언어로는 자바스크립트, 자바, 파이썬을 선정했다. 두 기준에 모두 부합하는 언어는 파이썬과 자바스크립트이다. 이 중 무엇을 택할지는 3번 기준으로 결정하면 된다. 데이터 분석에 관심 있으면 파이썬부터, 웹 개발에 관심 있으면 자바스크립트부터 시작하면 된다. 참고로 자바스크립트를 하기 위해서는 기본적으로 HTML과 CSS를 알아야 한다!데이터 과학과 웹 개발 둘 다 관심 없으면 자바스크립트보다는 파이썬으로 시작하는 걸 추천한다. 개인적인 의견이지만 초보자 입장에서 파이썬 언어가 자바스크립트보다 깔끔하다고 생각하기 때문이다. 또한 HTML과 CSS를 미리 배워야 하는 수고를 덜 수 있다.어디서 배우지?온라인으로 프로그래밍을 가르치는 사이트가 굉장히 많다. 해외에는 Codecademy, Treehouse, Coursera, MIT OpenCourseWare 등이 있고 한국에는 인프런, 엘리스, 코드잇, 생활코딩 등이 있다. 국내외 서비스를 통틀어서 가장 추천하는 곳은 코드잇이다. 영어로 된 수업이 당연히 더 좋을 것이라 생각한다면 큰 오산이다. Codecademy나 Treehouse는 쉽고 재미있지만 막상 수업을 다 들어도 직접 무언가를 할 수 있겠다는 생각이 들지 않는다. 반면 Coursera나 MIT OpenCourseWare는 대학 수업과 흡사하기 때문에 지루하고 어려워서 이수율이 5% 정도밖에 되지 않는다. 코드잇은 내용의 깊이와 재미를 모두 잡았다. 심도 있는 내용을 난해하지 않고 간결하게 풀어내어 졸업률이 60%나 된다. 코드잇 수업 안 들은 사람은 있어도 하나만 들은 사람은 없다는 말이 있을 정도인데, 수강 후기를 보면 정말 수강생들의 애정이 드러난다. 무료로 샘플을 들어볼 수 있으니 일단 한 번 해보도록.Python으로 배우는 프로그래밍 기초 수업, HTML/CSS로 배우는 웹 퍼블리싱 수업, JavaScript로 배우는 인터랙티브 웹 수업을 모두 들으면 자신감을 갖고 프로그래밍 커뮤니티에 입문할 수 있을 것이다.이제 어떤 프로그래밍 언어를 어디서 배워야 하는지 알았으니, 주저 말고 시작해보길 바란다!#코드잇#코딩교육 #개발자양성 #교육기업 #인사이트 #경험공유
조회수 2604

시뮬레이션에서의 Process Mining(프로세스 마이닝) 활용

시뮬레이션은 실제로 실행하기 어려운 실험을 간단히 행하는 모의실험을 뜻하며, 특히 컴퓨터를 이용하여 모의실험을 할 때는 컴퓨터 시뮬레이션이라고 일컬어집니다.  시뮬레이션은 특수한 하드웨어를 사용하는 3D 가상현실이나 비행 시뮬레이션 등 다양한 분야에 사용되고 있으며, 이벤트 중심의 로그를 다루는 프로세스 마이닝에서는 이산 사건 시뮬레이션을 중심으로 연구가 이뤄지고 있습니다.이산사건(discrete event) 시뮬레이션은 시간이 경과함에 따라 시뮬레이션 이 진행되는 것이 아니라 시스템 외부 혹은 내부에서 사건이 발생했을 때만 모델을 실행시킵니다. 이산사건 시뮬레이션에서 사건이란 시스템의 외부 혹은 내부에서 발생하는 추상적인 신호를 말하며, 이산 사건이란 임의의 시각에 불규칙으로 일어나는 사건을 의미합니다.이산 사건 시뮬레이션 모델을 잘 만들기 위해서는 사건 시간과 사건에 대한 정확한 기술이 필요한 데, 이를 위해 프로세스 마이닝이 사용될 수 있습니다.[그림] 프로세스 마이닝 기반의 시뮬레이션 모델 도출 (Discovering Simulation Model, Rozinat et a l., 2009)이것은 기존에 시뮬레이션 모델링이 현실 세계에서의 관찰 및 수작업에 의해 이뤄졌다면, 좀 더 쉽고 정확한 모델링을 위해서는 데이터 기반의 AS-IS 프로세스 파악에 능한 프로세스 마이닝을 사용해 볼 수 있지 않을까 하는 의문에서 출발합니다.아래 표와 같이 프로세스 마이닝과 시뮬레이션은 AS-IS 모델과 TO-BE 모델 각각의 영역에서 서로 보완하는 역할을 담당하고 있습니다. [표] 프로세스 마이닝과 시뮬레이션 단계별 역할 비교단계프로세스 마이닝 (AS-IS)시뮬레이션 (TO-BE)프로세스 설계프로세스 마이닝을 통해 도출한 실제 프로세스 모델을 바탕으로 프로세스 (재)설계다양한 대안 모델에 대한 검증 수행구현 및 실행구현하고자 하는 프로세스 모델의 표준 모델 준수 여부 확인시뮬레이션을 통해 테스트 및 검증 완료된 프로세스 모델 구현모니터링 및 분석표준 모델 준수 모니터링 및 병목 지점, 재작업 구간 분석시뮬레이션을 통한 병목 개선 구간 및 자원 수요 예측, 작업 시간 효율화 효과 분석 이러한 연구들을 바탕으로 최근에는 생산 공정 내 작업 현황 파악 및 성과 측정을 위해 생산 시스템의 이벤트 로그를 저장하고 분석하여, 제조 공정에 대한 시뮬레이션 모델 요소를 도출하려는 연구가 진행되고 있습니다. 이를 통해 프로세스 마이닝에서 찾은 병목 구간 등 문제점을 바탕으로 어떻게 개선할 것인지, 프로세스 변경 혹은 개선이 어떤 결과로 이어질지 What-if 분석을 통해 의사 결정을 위한 예측 방법이 제공되고 있습니다. 시뮬레이션 수행의 결과로 많은 수행 결과가 출력되며, 좀 더 나아가 사건과 이벤트에 대한 상세 기록들이 로그 데이터 형태로 나올 수 있습니다. 시뮬레이션이 가상 현실이라는 관점에서 현실에 대한 프로세스 마이닝 분석은 가상 현실에 대해 마찬가지로 유효합니다. 실제로 시뮬레이션 모델링을 하고 나서 시뮬레이션 모델링이 현실을 반영할 수 있도록 잘 되었는지 검증할 필요가 있는데, 시뮬레이션 로그에 대한 프로세스 마이닝 분석을 통해 해당 프로세스 모델을 도출할 수 있습니다.  얻어진 모델을 현실 세계에서 얻어진 프로세스 모델과 동일한 기준에서 비교하고 이에 대한 차이를 다시 시뮬레이션 모델이 반영하는 순환적 구조를 통해 좀 더 정확한 시뮬레이션 모델을 얻게 됩니다.  [참고 문헌]https://en.wikipedia.org/wiki/Simulation#퍼즐데이터 #개발팀 #개발자 #개발후기 #인사이트
조회수 7383

Kafka 모니터링

Kafka 도입 이후에 점진적으로 모니터링을 개선해나간다. Kafka와 그 제반 환경에 대해 이해한만큼 모니터링을 구성하고 모니터링 시스템에서 피드백을 받아 다시 학습하고 그렇게 배운 것을 토대로 다시 모니터링을 구성한다. 그 과정을 따라 나가며 Kafka 를 어떻게 모니터링하면 좋을지 알아보자.프로세스 모니터링아무래도 가장 기초적이면서 중요한 지표는 Kafka 프로세스가 잘 살아 있는지 확인하는 것이다. 다섯 대로 구성한 클러스터라면 상시 Kafka 프로세스가 확인되어야 한다. 만약 Kubernetes의 StatefulSet으로 Kafka 클러스터를 구성한 경우라면 Kafka 프로세스 다섯과 프로세스 모두를 엮는 서비스, 그러니까 로드밸런서 하나를 포함해 총 여섯 개의 프로세스를 확인해야 한다. DataDog(통칭 멍멍이)을 이용해 모니터링하는 경우라면 다음과 같이 설정하면 된다.Monitoring Kafka ClusterKafka는 Zookeeper를 이용하므로 ZooKeeper 역시 동일하게 모니터링하면 된다.DataDog을 이용한 메트릭 모니터링`dd-agent는 Kafka 관련 메트릭을 Broker, Consumer, Producer 세 측면에서 수집한다.Monitoring Kafka with DatadogMonitoring Kafka performance metrics위의 두 문서가 Kafka 모니터링의 상세한 측면을 기술하는데 멍멍이를 이용하지 않더라도 꼭 한번 읽어볼만하다. 두 문서가 매우 훌륭하므로 이 글에서는 Kubernetes 환경에 초점을 맞춰 주목할 점만 살펴본다.Kubernetes 환경에서 멍멍이 에이전트는 보통 PetSet으로 구성한다. 말인즉 Kubernetes Worker 한 대마다 에이전트를 한 대씩 띄워서 Worker 안에서 작동하는 모든 도커 인스턴스의 메트릭을 수집한다. 일단 에이전트를 설정하고 나면 아래와 같이 Kafka 모니터링이 정상 작동하는지 확인하면 된다.kube exec -it dd-agent-17vjg -- /opt/datadog-agent/agent/agent.py info kafka ----- - instance #kafka-kafka-0.broker-9999 [OK] collected 46 metrics - instance #kafka-kafka-1.broker-9999 [OK] collected 46 metrics - instance #kafka-kafka-2.broker-9999 [OK] collected 46 metrics - Collected 138 metrics, 0 events & 0 service checks Emitters ======== - http_emitter [OK]Broker의 경우는 설정하기가 비교적 쉽다. Kubernetes에서 Kafka 같은 Stateful cluster는 StatefulSet으로 구성하게 되는데 이때 호스트 주소가 kafka-0, kafka-1 같이 예측 가능한 이름으로 정해지기 때문에 kafka.yaml을 미리 작성해두기 쉽다.instances: - host: kafka-0.broker port: 9999 # This is the JMX port on which Kafka exposes its metrics (usually 9999) - host: kafka-1.broker port: 9999Producer와 Consumer 모니터링은 이와는 다르다. 구현하기 나름이지만 Producer 또는 Consumer가 되는 응용프로그램은 Stateless cluster일 때가 많고 그런 경우에는 Kubernetes에서 Deployment로 클러스터를 구성한다. 이때는 StatefulSet인 경우와 달리 호스트 주소가 worker-903266370-q3rcx와 같이 예측하기 힘들게 나오므로 에이전트에 미리 설정을 넣을 수가 없다. 상당히 까다로운 문제이다.Consumer 모니터링Kafka의 설계는 매우 단순하면서도 강력해서 감탄하곤 한다. 하지만 복잡한 문제를 단순하게 풀어냈다고 해서 이를 둘러싼 환경을 제대로 모니터링하는 것도 쉽다는 뜻은 아니다. 특히 Consumer groups이 제대로 제 몫을 하고 있는지 파악하기는 더 어렵다. Consumer group마다 모니터링 체계를 갖추자니 번거롭다. 게다가 그런 번거로움을 극복하더라도 Kafka에 문제가 있는 경우를 탐지하기는 여전히 어렵다. 예를 들어 Consumer에게 가야 할 메시지 중 5%가 실제로는 전달되지 않는다 하면 이를 Consumer가 알기는 어려울 것이다. 이 외에도 Consumer 측 모니터링이 엄청나게 까다로운 문제임은 Burrow: Kafka Consumer Monitoring Reinvented에서 잘 밝혔다.Burrow: Kafka Consumer Monitoring Reinvented에 등장하는 Burrow는 Kafka를 세상에 내놓은 LinkedIn 엔지니어링 팀이 개발한 Kafka 컨슈머 모니터링 도구이다. 커뮤니티에서는 대체로 현존하는 가장 뛰어난 모니터링 도구라고 인정하는 분위기이다. 그러니 다른 도구도 많지만 우선 Burrow로 모니터링을 강화하기로 한다.Burrow로 Consumer 모니터링하기Burrow는 Dockerize가 잘 되어 있기 때문에 사용하기 어렵지 않다. LinkedIn이 공식 도커 이미지까지 제공했더라면 더 좋겠으나 GitHub에 Dockerfile과 docker-compose.yml을 올려놓아서 도커를 잘 아는 사람이라면 큰 어려움 없이 바로 설정하고 설치할 수 있다. 컨테이너 환경의 관례대로 주요 설정을 환경변수로 미리 빼놨으면 더 좋았겠지만 …알람 받기Burrow는 문제가 생겼을 때 알람을 발송하는 기능이 있다. 위키에는 이메일 알람과 HTTP 알람(Webhook)을 어떻게 설정하는지 설명한다. 그런데 Burrow 소스코드를 살펴보면 문서화되지 않은 알람 기능도 있으니… 바로! Slack 알람을 제공한다. 아직 공식 문서가 없고 소스코드도 godoc 관례에 맞춰 설명해놓은 부분이 전혀 없기 때문에 소스코드를 읽거나 GitHub 이슈에서 논의된 내용을 토대로 설정해야 한다.[slacknotifier] enable=true url=https://hooks.slack.com/services/xxxx/xxxxxxxxxx group=local,critical-consumer-group group=local,other-consumer-group threshold=0 channel="#general" username=burrower interval=5 timeout=5 keepalive=30멍멍이로 메트릭을 꾸준히 수집하고 이슈가 생겼을 때 알람을 받고자 한다면 packetloop/datadog-agent-burrow를 이용하면 된다.This plugin will push the offsets for all topics (except the offsets_topic) and consumers for every kafka cluster it finds into Datadog as a metric.멍멍이 에이전트에 필요한 파일과 설정을 넣고 나면 아래와 같이 메트릭이 수집된다.kafka.topic.offsets 와 kafka.consumer.offsets 이렇게 두 개의 메트릭만 수집하지만 각 메트릭을 cluster, topic, consumer 세 개의 토픽으로 세분화하기 때문에 실제로는 꽤 다양한 지표를 멍멍이에서 확인하고 이용할 수 있다.알`람 설정하기앞서 살펴봤지만 프로세스 모니터링 등은 어렵지 않다. 클러스터에서 한대라도 빠지면 바로 알람을 받는다. 끝!하지만 그 외의 지표는 알람의 기준을 설정하기가 힘들다. 예를 들어 Burrow의 kafka.topic.offsets 값이 600이면 정상인가? 그렇다면 700은? 또는 400은? 도무지 감을 잡을 수가 없다. 이럴 때는 멍멍이가 제공하는 Outlier detection기능으로 알람을 걸면 쉽다. 이 기능은 쉽게 말해 평소와 다른 행동을 감지했을 때 알람을 보낸다. 그러므로 정상의 범위를 확실하게 모를 때 아주 유용하다.설정 자체는 DBSCAN 또는 MAD라는 알고리즘이 등장하는 것만 빼곤 여타의 모니터링과 다르지 않기 때문에 매우 쉽다.참고 문헌How to Monitor KafkaCollecting Kafka performance metricsOriginally published at Andromeda Rabbit.#데일리 #데일리호텔 #개발 #개발자 #개발팀 #인사이트 #기술스택 #스택소개 #Kafka

기업문화 엿볼 때, 더팀스

로그인

/