스토리 홈

인터뷰

피드

뉴스

조회수 781

HBase Meetup - 비트윈에서 HBase를 사용하는 방법 - VCNC Engineering Blog

비트윈에서는 서비스 초기부터 HBase를 주요 데이터베이스로 사용하였으며 사용자 로그를 분석하는 데에도 HBase를 사용하고 있습니다. 지난 주 금요일(11월 15일)에 HBase를 만든 Michael Stack 씨가 한국을 방문하게 되어 ZDNet 송경석 팀장님의 주최 하에 HBase Meetup Seoul 모임을 가졌습니다. 그 자리에서 VCNC에서 비트윈을 운영하면서 HBase를 사용했던 경험들이나 HBase 트랜잭션 라이브러리인 Haeinsa에 대해 간단히 소개해 드리는 발표 기회를 가질 수 있었습니다. 이 글에서 발표한 내용에 대해 간단히 소개하고자 합니다.비트윈 서비스에 HBase를 사용하는 이유비트윈에서 가장 많이 사용되는 기능 중 하나가 채팅이며, 채팅은 상대적으로 복잡한 데이터 구조나 연산이 필요하지 않기 때문에 HBase 의 단순한 schema 구조가 큰 문제가 되지 않습니다. 특히 쓰기 연산이 다른 기능보다 많이 일어나기 때문에 높은 쓰기 연산 성능이 필요합니다. 그래서 메세징이 중심이 되는 서비스는 높은 확장성(Scalability)과 쓰기 성능을 가진 HBase가 유리하며 비슷한 이유로 라인이나 페이스북 메신저에서도 HBase를 사용하는 것이라고 짐작할 수 있습니다.로그 분석에도 HBase를 사용합니다비트윈은 사용자 로그 분석을 통해서 좀 더 나은 비트윈이 되기 위해서 노력하고 있습니다. 비트윈 사용자가 남기는 로그의 양이 하루에 3억건이 넘기 때문에 RDBMS에 저장하여 쿼리로 분석하기는 힘듭니다. 그래서 로그 분석을 위해 분산 데이터 처리 프레임워크인 Hadoop MapReduce를 이용하며 로그들은 MapReduce와 호환성이 좋은 HBase에 저장하고 있습니다. 또한 이렇게 MapReduce 작업들을 통해 정제된 분석 결과를 MySQL에 저장한 후에 다양한 쿼리와 시각화 도구들로 custom dashboard를 만들어 운영하고 있습니다. 이를 바탕으로 저희 Biz development팀(사업개발팀)이나 Data-driven팀(데이터 분석팀)이 손쉽게 insight를 얻어낼 수 있도록 돕고 있습니다.HBase를 사용하면서 삽질 했던 경험HBase를 사용하면서 처음에는 잘못 사용하고 있었던 점이 많았고 차근차근 고쳐나갔습니다. Region Split과 Major Compaction을 수동으로 직접 하는 등 다양한 최적화를 통해 처음보다 훨씬 잘 쓰고 있습니다. HBase 설정 최적화에 대한 이야기는 이전에 올렸던 블로그 글에서도 간단히 소개한 적이 있으니 확인해보시기 바랍니다.HBase 트랜잭션 라이브러리 해인사Haeinsa는 HBase에서 Multi-Row 트랜잭션을 제공하기 위한 라이브러리입니다. 오픈소스로 공개되어 있으며 Deview에서도 발표를 했었습니다. HBase에 아무런 변형도 가하지 않았기 때문에 기존에 사용하던 HBase 클러스터에 쉽게 적용할 수 있습니다. 비트윈에 실제로 적용되어 하루 3억 건 이상의 트랜잭션을 처리하고 있으며 다른 많은 NoSQL 기반 트랜잭션 라이브러리보다 높은 확장성과 좋은 성능을 가지고 있습니다.발표에서 사용했던 슬라이드를 첨부하였으니 도움이 되었으면 합니다.<iframe class="speakerdeck-iframe" frameborder="0" src="//speakerdeck.com/player/2b8092b02ff90131ef414aa7d272d735?" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true" style="border: 0px; background: padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 750px; height: 563px;">
조회수 1194

안드로이드 클라이언트 Reflection 극복기

비트윈 팀은 비트윈 안드로이드 클라이언트(이하 안드로이드 클라이언트)를 가볍고 반응성 좋은 애플리케이션으로 만들기 위해 노력하고 있습니다. 이 글에서는 간결하고 유지보수하기 쉬운 코드를 작성하기 위해 Reflection을 사용했었고 그로 인해 성능 이슈가 발생했던 것을 소개합니다. 또한 그 과정에서 발생한 Reflection 성능저하를 해결하기 위해 시도했던 여러 방법을 공유하도록 하겠습니다.다양한 형태의 데이터¶Java를 이용해 서비스를 개발하는 경우 POJO로 서비스에 필요한 다양한 모델 클래스들을 만들어 사용하곤 합니다. 안드로이드 클라이언트 역시 모델을 클래스 정의해 사용하고 있습니다. 하지만 서비스 내에서 데이터는 정의된 클래스 이외에도 다양한 형태로 존재합니다. 안드로이드 클라이언트에서 하나의 데이터는 아래와 같은 형태로 존재합니다.JSON: 비트윈 서비스에서 HTTP API는 JSON 형태로 요청과 응답을 주고 받고 있습니다.Thrift: TCP를 이용한 채팅 API는 Thrift를 이용하여 프로토콜을 정의해 서버와 통신을 합니다.ContentValues: 안드로이드에서는 Database 에 데이터를 저장할 때, 해당 정보는 ContentValues 형태로 변환돼야 합니다.Cursor: Database에 저장된 정보는 Cursor 형태로 접근가능 합니다.POJO: 변수와 Getter/Setter로 구성된 클래스 입니다. 비지니스 로직에서 사용됩니다.코드 전반에서 다양한 형태의 데이터가 주는 혼란을 줄이기 위해 항상 POJO로 변환한 뒤 코드를 작성하기로 했습니다.다양한 데이터를 어떻게 상호 변환할 것 인가?¶JSON 같은 경우는 Parsing 후 Object로 변환해 주는 라이브러리(Gson, Jackson JSON)가 존재하지만 다른 형태(Thrift, Cursor..)들은 만족스러운 라이브러리가 존재하지 않았습니다. 그렇다고 모든 형태에 대해 변환하는 코드를 직접 작성하면 필요한 경우 아래와 같은 코드를 매번 작성해줘야 합니다. 이와 같이 작성하는 경우 Cursor에서 원하는 데이터를 일일이 가져와야 합니다.@Overridepublic void bindView(View view, Context context, Cursor cursor) { final ViewHolder holder = getViewHolder(view); final String author = cursor.getString("author"); final String content = cursor.getString("content"); final Long timeMills = cursor.getLong("time"); final ReadStatus readStatus = ReadStatus.fromValue(cursor.getString("readStatus")); final CAttachment attachment = JSONUtils.parseAttachment(cursor.getLong("createdTime")); holder.authorTextView.setText(author); holder.contentTextView.setText(content); holder.readStatusView.setReadStatus(readStatus); ...}하지만 각 형태의 필드명(Key)이 서로 같도록 맞춰주면 각각의 Getter와 Setter를 호출해 형태를 변환해주는 Utility Class를 제작할 수 있습니다.@Overridepublic void bindView(View view, Context context, Cursor cursor) { final ViewHolder holder = getViewHolder(view); Message message = ReflectionUtils.fromCursor(cursor, Message.class); holder.authorTextView.setText(message.getAuthor()); holder.contentTextView.setText(message.getContent()); holder.readStatusView.setReadStatus(message.getReadStatus()); ...}이런 식으로 코드를 작성하면 이해하기 쉽고, 모델이 변경되는 경우에도 유지보수가 비교적 편하다는 장점이 있습니다. 따라서 필요한 데이터를 POJO로 작성하고 다양한 형태의 데이터를 POJO로 변환하기로 했습니다. 서버로부터 받은 JSON 혹은 Thrift객체는 자동으로 POJO로 변환되고 POJO는 다시 ContentValues 형태로 DB에 저장됩니다. DB에 있는 데이터를 화면에 보여줄때는 Cursor로부터 데이터를 가져와서 POJO로 변환 후 적절한 가공을 하여 View에 보여주게 됩니다.POJO 형태로 여러 데이터 변환필요Reflection 사용과 성능저하¶처음에는 Reflection을 이용해 여러 데이터를 POJO로 만들거나 POJO를 다른 형태로 변환하도록 구현했습니다. 대상 Class의 newInstance/getMethod/invoke 함수를 이용해 객체 인스턴스를 생성하고 Getter/Setter를 호출하여 값을 세팅하거나 가져오도록 했습니다. 앞서 설명한 ReflectionUtils.fromCursor(cursor, Message.class)를 예를 들면 아래와 같습니다.public T fromCursor(Cursor cursor, Class clazz) { T instance = (T) clazz.newInstance(); for (int i=0; i final String columnName = cursor.getColumnName(i); final Class<?> type = clazz.getField(columnName).getType(); final Object value = getValueFromCursor(cursor, type); final Class<?>[] parameterType = { type }; final Object[] parameter = { value }; Method m = clazz.getMethod(toSetterName(columnName), parameterType); m.invoke(instance, value); } return instance;}Reflection을 이용하면 동적으로 Class의 정보(필드, 메서드)를 조회하고 호출할 수 있기 때문에 코드를 손쉽게 작성할 수 있습니다. 하지만 Reflection은 튜토리얼 문서에서 설명된 것처럼 성능저하 문제가 있습니다. 한두 번의 Relfection 호출로 인한 성능저하는 무시할 수 있다고 해도, 필드가 많거나 필드로 Collection을 가진 클래스의 경우에는 수십 번이 넘는 Reflection이 호출될 수 있습니다. 실제로 이 때문에 안드로이드 클라이언트에서 종종 반응성이 떨어지는 경우가 발생했습니다. 특히 CursorAdapter에서 Cursor를 POJO로 변환하는 코드 때문에 ListView에서의 스크롤이 버벅이기도 했습니다.Bytecode 생성¶Reflection 성능저하를 해결하려고 처음으로 선택한 방식은 Bytecode 생성입니다. Google Guice 등의 다양한 자바 프로젝트에서도 Bytecode를 생성하는 방식으로 성능 문제를 해결합니다. 다만 안드로이드의 Dalvik VM의 경우 일반적인 JVM의 Bytecode와는 스펙이 다릅니다. 이 때문에 기존의 자바 프로젝트에서 Bytecode 생성에 사용되는 CGLib 같은 라이브러리 대신 Dexmaker를 이용하여야 했습니다.CGLib¶CGLib는 Bytecode를 직접 생성하는 대신 FastClass, FastMethod 등 펀리한 클래스를 이용할 수 있습니다. FastClass나 FastMethod를 이용하면 내부적으로 알맞게 Bytecode를 만들거나 이미 생성된 Bytecode를 이용해 비교적 빠른 속도로 객체를 만들거나 함수를 호출 할 수 있습니다.public T create() { return (T) fastClazz.newInstance();} public Object get(Object target) { result = fastMethod.invoke(target, (Object[]) null);} public void set(Object target, Object value) { Object[] params = { value }; fastMethod.invoke(target, params);}Dexmaker¶하지만 Dexmaker는 Bytecode 생성 자체에 초점이 맞춰진 라이브러리라서 FastClass나 FastMethod 같은 편리한 클래스가 존재하지 않습니다. 결국, 다음과 같이 Bytecode 생성하는 코드를 직접 한땀 한땀 작성해야 합니다.public DexMethod generateClasses(Class<?> clazz, String clazzName){ dexMaker.declare(declaringType, ..., Modifier.PUBLIC, TypeId.OBJECT, ...); TypeId<?> targetClassTypeId = TypeId.get(clazz); MethodId invokeId = declaringType.getMethod(TypeId.OBJECT, "invoke", TypeId.OBJECT, TypeId.OBJECT); Code code = dexMaker.declare(invokeId, Modifier.PUBLIC); if (isGetter == true) { Local<Object> insertedInstance = code.getParameter(0, TypeId.OBJECT); Local instance = code.newLocal(targetClassTypeId); Local returnValue = code.newLocal(TypeId.get(method.getReturnType())); Local value = code.newLocal(TypeId.OBJECT); code.cast(instance, insertedInstance); MethodId executeId = ... code.invokeVirtual(executeId, returnValue, instance); code.cast(value, returnValue); code.returnValue(value); } else { ... } // constructor Code constructor = dexMaker.declare(declaringType.getConstructor(), Modifier.PUBLIC); Local<?> thisRef = constructor.getThis(declaringType); constructor.invokeDirect(TypeId.OBJECT.getConstructor(), null, thisRef); constructor.returnVoid();}Dexmaker를 이용한 방식을 구현하여 동작까지 확인했으나, 다음과 같은 이유로 실제 적용은 하지 못했습니다.Bytecode를 메모리에 저장하는 경우, 프로세스가 종료된 이후 실행 시 Bytecode를 다시 생성해 애플리케이션의 처음 실행성능이 떨어진다.Bytecode를 스토리지에 저장하는 경우, 원본 클래스가 변경됐는지를 매번 검사하거나 업데이트마다 해당 스토리지를 지워야 한다.더 좋은 방법이 생각났다.Annotation Processor¶최종적으로 저희가 선택한 방식은 컴파일 시점에 형태변환 코드를 자동으로 생성하는 것입니다. Reflection으로 접근하지 않아 속도도 빠르고, Java코드가 미리 작성돼 관리하기도 편하기 때문입니다. POJO 클래스에 알맞은 Annotation을 달아두고, APT를 이용해 Annotation이 달린 모델 클래스에 대해 형태변환 코드를 자동으로 생성했습니다.형태 변환이 필요한 클래스에 Annotation(@GenerateAccessor)을 표시합니다.@GenerateAccessorpublic class Message { private Integer id; private String content; public Integer getId() { return id; } ...}javac에서 APT 사용 옵션과 Processor를 지정합니다. 그러면 Annotation이 표시된 클래스에 대해 Processor의 작업이 수행됩니다. Processor에서 코드를 생성할 때에는 StringBuilder 등으로 실제 코드를 일일이 작성하는 것이 아니라 Velocity라는 template 라이브러리를 이용합니다. Processor는 아래와 같은 소스코드를 생성합니다.public class Message$$Accessor implements Accessor { public kr.co.vcnc.binding.performance.Message create() { return new kr.co.vcnc.binding.performance.Message(); } public Object get(Object target, String fieldName) throws IllegalArgumentException { kr.co.vcnc.binding.performance.Message source = (kr.co.vcnc.binding.performance.Message) target; switch(fieldName.hashCode()) { case 3355: { return source.getId(); } case -1724546052: { return source.getContent(); } ... default: throw new IllegalArgumentException(...); } } public void set(Object target, String fieldName, Object value) throws IllegalArgumentException { kr.co.vcnc.binding.performance.Message source = (kr.co.vcnc.binding.performance.Message) target; switch(fieldName.hashCode()) { case 3355: { source.setId( (java.lang.Integer) value); return; } case -1724546052: { source.setContent( (java.lang.String) value); return; } ... default: throw new IllegalArgumentException(...); } }}여기서 저희가 정의한 Accessor는 객체를 만들거나 특정 필드의 값을 가져오거나 세팅하는 인터페이스로, 객체의 형태를 변환할 때 이용됩니다. get,set 메서드는 필드 이름의 hashCode 값을 이용해 해당하는 getter,setter를 호출합니다. hashCode를 이용해 switch-case문을 사용한 이유는 Map을 이용하는 것보다 성능상 이득이 있기 때문입니다. 단순 메모리 접근이 Java에서 제공하는 HashMap과 같은 자료구조 사용보다 훨씬 빠릅니다. APT를 이용해 변환코드를 자동으로 생성하면 여러 장점이 있습니다.Reflection을 사용하지 않고 Method를 직접 수행해서 빠르다.Bytecode 생성과 달리 애플리케이션 처음 실행될 때 코드 생성이 필요 없고 만들어진 코드가 APK에 포함된다.Compile 시점에 코드가 생성돼서 Model 변화가 바로 반영된다.APT를 이용한 Code생성으로 Reflection 속도저하를 해결할 수 있습니다. 이 방식은 애플리케이션 반응성이 중요하고 상대적으로 Reflection 속도저하가 큰 안드로이드 라이브러리에서 최근 많이 사용하고 있습니다. (AndroidAnnotations, ButterKnife, Dagger)성능 비교¶다음은 Reflection, Dexmaker, Code Generating(APT)를 이용해 JSONObject를 Object로 변환하는 작업을 50번 수행한 결과입니다.성능 비교 결과이처럼 최신 OS 버전일수록 Reflection의 성능저하가 다른 방법에 비해 상대적으로 더 큽니다. 반대로 Dexmaker의 생성 속도는 빨라져 APT 방식과의 성능격차는 점점 작아집니다. 하지만 역시 APT를 통한 Code 생성이 모든 환경에서 가장 좋은 성능을 보입니다.마치며¶서비스 모델을 반복적으로 정의하지 않으면서 변환하는 방법을 알아봤습니다. 그 과정에서 Reflection 의 속도저하, Dexmaker 의 단점도 설명해 드렸고 결국 APT가 좋은 해결책이라고 판단했습니다. 저희는 이 글에서 설명해 드린 방식을 추상화해 Binding이라는 라이브러리를 만들어 사용하고 있습니다. Binding은 POJO를 다양한 JSON, Cursor, ContentValues등 다양한 형태로 변환해주는 라이브러리입니다. 뛰어난 확장성으로 다양한 형태의 데이터로 변경하는 플러그인을 만들어서 사용할 수 있습니다.Message message = Bindings.for(Message.class).bind().from(AndroidSources.cursor(cursor));Message message = Bindings.for(Message.class).bind().from(JSONSources.jsonString(jsonString));String jsonString = Bindings.for(Message.class).bind(message).to(JSONTargets.jsonString());위와 같이 Java상에 존재할 수 있는 다양한 타입의 객체에 대해 일종의 데이터 Binding 기능을 수행합니다. Binding 라이브러리도 기회가 되면 소개해드리겠습니다. 윗글에서 궁금하신 점이 있으시거나 잘못된 부분이 있으면 답글을 달아주시기 바랍니다. 감사합니다.저희는 언제나 타다 및 비트윈 서비스를 함께 만들며 기술적인 문제를 함께 풀어나갈 능력있는 개발자를 모시고 있습니다. 언제든 부담없이 [email protected]로 이메일을 주시기 바랍니다!
조회수 2112

칸반(Kanban) 5개월 사용 후기

사실 개발 방법론이라는 것을 7개월 전만 해도 귓등으로 듣고 그게 왜 필요한지도 알지 못했던 것이 사실입니다. 부끄럽지만 애자일이 수많은 프로그래밍 언어중 하나인줄 알았죠.10개월 전만해도 우리 팀은 저를 포함해서 3명에 불과했고 모든 것은 메신저와 구글 드라이브로 일을 처리했습니다. 기억력이 좋지않지만 머릿속에서 각 팀원들이 언제까지 뭘하고 다음엔 무엇을 언제까지 해야겠다라는 것이 그려질 정도로 적은 숫자였죠. 개발방법론이 필요한 이유가 없으니 무관심한 것은 당연했습니다. 이 글을 읽으시는 분들 중에 아마 7개월 전의 저와 같은 생각을 하신 분이 있을지도 모르겠네요.지금 우리 팀은 11명으로 늘어났고(그중에 소프트웨어 개발팀만 7명) 그들 하나하나를 마이크로 매니징하기에는 저라는 인간이 너무나 머리가 아팠습니다. 그래서 도입한 것이 애자일 개발방법론이었는데 애자일은 비록 실패로 끝났지만 거기서 많은 교훈을 얻고 칸반으로 전환하는 원동력이 되었습니다.우리 팀은 애자일 개발선언 중에서도 "계획을 따르기보단 변화에 대응하기"라는 선언을 굉장히 맘에 들어했는데, 그 이유는 애자일 도입이전 우리의 상황이 그랬기 때문이었습니다. 매일매일 고객의 요구는 들어오고 경영진과의 대화에서 매일매일 우선순위가 바뀌고, 그에 따라 하던 작업이 마무리되지 않으면 브랜치를 새로 파서 다른 작업을 하고 미완성된 코드는 늘어났으며 그에 따라 불평불만도 늘어났습니다.여러 애자일 개발방법론 중에서도 우리가 선택했던 것은 eXtreme Programming(XP)이었는데, 우리에게 스크럼과 같은 1달간의 스프린트는 너무 길다, 2주간의 이터레이션(Iteration)으로 구성된 XP가 좋다라는 것이었습니다.우리는 스크럼 보드를 준비했고 거기에 포스트잇을 붙여가면서 아침마다 스크럼 회의를 했으며, 기록을 남기기위해 레드마인을 사용하였습니다.eXtreme Programming Flow Chart간단하게 왜 실패했는지 이유를 들어볼게요.1. 배포 계획(Release Plan)을 수립하기 힘들다물론 계획자체를 만들기 힘들다는 것이 아닙니다. 배포 계획을 만들어도 그대로 지켜지지 않았습니다. 큰 틀로 배포 계획을 만들고 작은 틀로 반복 계획(Iteration Plan)을 세우는 것이 목표였는데, 수립을 해봤자 절대 지켜지지 않았습니다. 우리와 같은 작은 스타트업의 작은 팀은 시장의 요구사항이라는 급류에 이리저리 쓸려 매일매일 계획과는 다른 일을 하고 있었거든요. 리팩토링할 시간은 커녕 테스트 코드를 짤 시간조차 없었습니다.(핑계일수도 있지만요)거짓말이 아니고 단 한번도 계획대로 되지 않았습니다.2. 팀원들의 시간 예측 능력 부족애자일은 팀원들이 시간 예측을 굉장히 잘한다는 가정하에 잘 돌아가는 방법론입니다. 모두가 함께 한자리에 모여 복잡도를 논의하고 그에 따른 프로젝트의 시간 예측을 하고 함께 번다운 차트(Burn-down chart)를 그리며 하하호호 잘 나아가야 하는데, 우리 팀은 그렇지 않았습니다. 물론 실력부족이라고 탓할 수도 있겠지만 실제로 스크럼 보드에 예측시간 8시간이라고 적어놓고 4시간정도만 지나면 다른 문제가 터지거나 다른 기능을 개발해야하는 둥 제대로 지켜지지 않았을 뿐더러 그런 방해요소가 없다고 하더라고 8시간보다 더 많이 걸리거나 더 적게 걸리기도 했습니다.예측시간을 측정하기 힘든 마이너한 이유중에 하나는, 스파이크 솔루션(Spike solution)를 개발하는데 얼마나 걸리는지 예측하지 못한 탓도 있었는데 이 세상에 없는 솔루션을 개발하는데 있어 이전의 경험만으로는 턱없이 부족했습니다.이런 이유들 때문에 우리는 XP를 버릴 수 밖에 없었습니다. 계획보다는 변화에 적응하자!라는 원대한 목표가 있었지만 애자일 개발방법론은 우리가 닥친 미친듯한 변화를 감당하기에는 벅찼습니다. 우리는 스크럼 보드를 점점 멀리하기 시작했고 다시 구글 드라이브로 돌아갔습니다.저는 구글 문서(Google Docs)에 우리가 해야할 요구사항을 적었습니다. 우선순위가 높은 일일 수록 상단에 두었습니다. 그 오른쪽에는 일을 해야할 사람의 이름을 적었습니다. 그렇게 적고 문서를 공유하면 팀원들은 그 문서를 보고 그 순서대로 일을 진행하였습니다. 일을 진행하다가 생기는 의문점은 급한 일일 경우 구두로 전달하고 급하지 않을 경우에는 메신저 또는 문서의 빈공간을 활용하여 적었습니다.완료된 요구사항은 취소선을 긋고 옅은 글씨로 처리하여 해야 할일과 완벽히 구분되도록 하였으며 한 사람당 해당 시간에 하나의 일만 처리하도록 규칙을 세웠습니다. 보류되는 일은 보류 섹션으로 할일을 옮기고 보류가 되는 이유를 적도록 했습니다. 혼자 해결하기 힘들경우 회의를 통하여 함께 해결할 수 있는 자리를 마련했구요.그런식으로 우리는 배포 시기를 최대한 맞추려고 노력했고 이상하게도 XP를 버리고 구글 문서로 갈아타니 일이 더욱 수월해져서 이제는 생각보다 일이 빨리 끝나는 것이었습니다. 그리고 더욱 놀라운 일은 지금까지 우리가 했던 방식이 칸반과 유사하다는 것이었습니다.저는 바로 칸반 보드를 도입했고 이에따라 애자일에서 배운 규칙/정신과 칸반의 장점을 혼합하여 우리 팀만의 칸반보드를 완성하였습니다. 현재 우리가 쓰고 있는 칸반 보드는 Kanboard의 오픈소스를 그대로 사용하고 있습니다.1. 활발한 커뮤니케이션을 토대로 개발한다. 절대 혼자 일하지 않는다- 지속적으로 팀의 동의(Team agreement)를 구한다.- Knoledge island를 탈출하라(자신이 알고있는 지식이 전부가 아니다).- 코드 병목현상(Code bottleneck)을 탈출하라. Collective ownership을 발동하라.2. 한 번에 한개의 일만 처리하라. 보류하는 일은 최소로 하라칸반의 핵심으로 한 번에 한개의 일만 처리하도록 합니다. 개발자의 뇌는 하나도 손은 두개이고 손가락은 열개이므로 한 번에 하나의 일만 처리해야 합니다. 한 개의 일이 끝나지 않으면 다음 일을 진행하지 않는 것을 규칙으로 합니다.3. 가능하다면 예측시간을 적는 습관을 들인다개발완료시간을 정확히 예측하는 것은 개발자들에게 정말 중요한 능력중에 하나입니다. 신제품을 시장에 빨리 내놓을 수록 피드백을 빨리 받을 수 있으며, 고객으로부터의 소중한 피드백은 개선된 다음 버전을 위한 초석이 되기 때문입니다. 사업적으로 성공하고 싶다면 예측시간을 꼭 적는 습관을 들여 자신이 정해진 시간 동안 얼마만큼의 일을 할 수 있는지 예측하는 일이 큰 도움이 됩니다.4. 더 좋은 방법이 있다면 기존의 방법을 과감히 버린다저의 철학과도 일치하는 이야기인데요, 우리 팀과 회사가 함께 좋아질 수 있는 방법을 발견한다면 과감히 현재의 방법을 버리고 새로운 방법을 시도한다라는 우리 팀만의 맹세입니다. 앞으로 항상 발전하겠다는 의지를 가지고 잠시 손을 놓고 한발짝 물러서서 비판적인 자세로 모든 것을 바라보는 시간을 가지는 것도 혁신의 첫발짝이라고 생각합니다.지금까지 우리 팀이 꾀한 겉으로 보기에 가장 큰 혁신은 기존의 속도가 느리고 사용하기 불편했던 솔루션을 과감히 버리고 새로운 서버와 새로운 언어로 전환하면서 마이그레이션 및 새로운 형태의 최적화된 솔루션을 구축했다는 것입니다.(물론 내부적으로 가장 큰 혁신은 기존의 방법을 버릴 수도 있다라는 생각을 가졌다는 것이지요)현재 저는 팀 매니저로서 User story(요구사항정의서) 관리, Release plan(배포 계획서), 와이어프레임을 포함한 기획서 등 최소한의 문서만 관리하고 있으며, 팀원들 또한 이 시스템에 만족하며 아직까지는 판단하기 이르지만 굉장히 좋은 방법인것 같습니다.5개월간 칸반을 사용하면서 팀원들로부터 받은 피드백은 다음과 같습니다.1. 매일 아침 15분씩 하는 스크럼 회의는 새로운 기능 또는 새로운 프로젝트를 진행할 때는 굉장히 유용하지만, 디버깅 또는 테스팅 기간에는 시간낭비다.이 말을 한 팀원의 말에 따르면, 우리 팀은 데이터베이스를 관리하는 사람, API를 만드는 사람 등등 각자의 역할이 확실히 나누어져 있는데 새로운 기능을 개발할때는 여러사람과 소통해야하는 경우가 많고 개발 스펙이 달라지거나(작게는 함수이름 변경 등) 여러 변수들이 작용할 수 있으므로 짧게 자주만나는 것이 좋다고 말했습니다.2. 회의도 시간낭비다- 회의는 가급적 개최하지 않고 가능하다면 1:1 구두로 해결한다.- 급한일이 아닐경우에는 이메일/메신저를 활용하도록 한다.3. 칸반 보드에 보류 칼럼, 테스팅 칼럼을 나눈다보류 칼럼과 테스팅 칼럼을 나누어 적어 어떤 할일이 보류되었으며 어떤 할일이 테스팅 중인이 확실히 하도록 했습니다. 이는 테스팅을 하는데 오래걸리는 기능들이 있으며 테스팅을 하는 동안 다른 기능을 개발할 수도 있다는 것이 큰 이유였습니다.우선 순위가 바뀌었을 때 할 일을 잠시동안 놓아둘 칼럼이 없다는 것이 보류 칼럼이 존재하는 가장 큰 이유였습니다. 그러나 보류 칼럼에 놓을 수 있는 할 일의 수는 개인당 1개로 제한하여 2개 이상의 보류하는 일이 없도록하여 경각심을 갖도록 하였습니다.앞으로의 계획은 전에 언급했던 와비파커(Warby Parker)의 기술팀이 도입한 와블스(Warbles) 시스템을 적용해보는 것입니다. 우리 팀이 어떻게 바뀔지 정말 기대가 됩니다.#비주얼캠프 #인사이트 #경험공유 #조언 #개발자 #개발팀
조회수 1060

아띠 #22. 매일 새로운 삶을 사는 라이더, 저스틴

Story #22. 매일 새로운 삶을 사는 라이더라이더 '저스틴'을 소개합니다.간단한 자기소개를 해줘!음... 한국에서는 주관식이 어렵다니까;;나는 30살 늦각지에 독립해서 자유를 만낀하며 살고있는! “저스틴” 이라고해저스틴이란 사람은 굉장한 열정을 가지고 항상 새로운 분야에 도전하려하고 많이 부딪치고어려움을 겪기도 하지만 많은 어려움속에서 경험을 통해 새로운것을 하고자 하는 사람이야.아띠는 어떻게 알게 되었어?아띠 인력거는 2013년. 10월 kbs파노라마에서 방영된 김난도 교수 내일이라는 프로그램에서 아띠인력거가 소개되면서 처음 알게 되었어. 언젠가 한번은 꼭 인력거를 타봐야겠다는 생각에 손님으로 인력거를 체험한 이후 손님이 아닌 직접 라이더가 되서 북촌 방문하는 사람에게 북촌 곳곳 숨은 명소와 재미난 이야기를 소개하고 싶어서 라이더를 지원하게 되었어왜? 손님으로  먼저 인력거를 탔어? 바로 지원해도 되잖아?간접적으로 느끼는거랑 직접적으로 느끼는거에 차이가 있었어! 경험했을때 생각보다 훨씬더 인력거의 매력에 매료되더라고. 매력이 무엇이었어? 내가 강남사람이라 그런지 모르겠는데 종로라는 공간이 너무 매력적이었어.서울이란 곳이 도시화되어 옛 정취를 느끼기 어려운지만 도시화된 사회속에서 새로운 역사와 . 한옥을 느낄 수 있고. 옛것을 느낄 수 있는곳으로 많이 놀러왔었어손님으로 탔을때 어떘어?아띠 라이더랑 북촌을 둘러보니 평소에 그냥 지나갔던 곳도 다시 보게 되었고, 정말 숨은 명소가 많다는 걸 알게되었어. 그래서 나같은 사람들에게 소개해 주고 싶어서 라이더가 되게 되었지저스틴을 손님으로 태웠던 라이더가 포레스트였다는데?응 포레였어. 그때 너무 궁금한게 많아서 포레에게 질문을 많이 했었지. 나중에 안 이야기지만 내가 혼자와서 혼자타서. 코치코치 많은걸 물어봐서 내가 스파이인줄알고 조심스럽게 이야기 했다고 하더라구포레랑은 언제 탔던거야? 2014년 2월~3월 정도 되었던거 같은데.  전화로 예약을 했을때 전화로 ij에게 인력거를 타고 싶다고 했어. 근데 ij가 지방출장중이라 새로지정된 포레로 타게 되었지. 면접은 누가 봤어?IJ랑 1:1로 면접을 봤어. IJ가 이러더라구 “잠깐 나가실까요? 걸으면서 면접 보시죠”IJ의 첫인상은 어땠어?이사람 뭐지? 뭘까? 티비에서 보던 그분이구나. 그 사람이구나.인터뷰를 이어가면 이어갈 수록 일반적인 사람이랑 다른 생각을 가지고 있구나. 상식을 깨는 사람이구나.면접은 어땠어?보통 인터뷰 볼때에는 지원사유 여러가지를 물어보지만. IJ의 인터뷰 내용은 자기가 추구하는 인생의 가치를 집중적으로 물어보았던거 같아.직장은 아니지만. 우리가 같이 일했을때 어떤 역할을 하고 기여가치에 대해 집중적인 질문을 받는데. 각 개인이 추구하는 인생의 목표나. 가치에 대해서 많이 물어보았던거 같아.그런것들을 물어봐서 정말 다르구나 느끼게 되었지.  첫 라이딩 어떘어?첫 라이딩은 사실 기억이 잘 안나. 처음에 북촌을 많이 소개시켜주고 싶었는데. 시작하고 나니까. 사람과 사람이 만나는거. 그런것 접점. 사람을 만나면서. 사람들에게 긍정적인 영향을 전파한다 생각했는데. 얻어가는게 많고. 사람들 사는 방식이 매우 다양하구나. 라이딩 하면서 기억에 남는 에피소드는 뭐가 있어?내가 R-3인데 R-3 등급이 되려면 100번 라이딩을 해야 되잔아. 생각해보면 100번 라이딩은 하루를 1번당 평균으로 7팀을 태웠으니 700팀을 태운거야. 1400명을 만난거지.근데 그중에서 기억에 남는 라이딩은대전 여자 태워서 돈 대신 스팸 받았던 이야기인데재작년 추석 연휴 시작되는 날이였어. 잭슨이랑. 야간 라이딩을 하던 중이였는데 지나가는 여성 한분이 짐을 많이 들고 있어서 태워줬어. 그런데 그분이. 고맙다면서 추석 선물 세트. 스팸 3호. 스팸 2개를 꺼내서 팁으로 주셔서 돈 대신 스팸을 받은 적이 있었지 그리고 예전에 아줌마 2분은 태웠었는데  그런데 그 후에 또 다시오셨는데  혼자 오셔서는 1시간 인생 푸념을 하셔서 듣어 드린 적도 있었구그리고 이 인력거가 한국분이 많이 타시지만  해외 이민가신지 20-30년 되신 분들이 오랜만에 고국을 방문하셔서 북촌에 오셔서 한국의 옛 모습을 보시고 감동받고 돌아가시는 모습이 너무 좋았어.마지막으로 북촌 사시는 할머니였는데. 인력거 타고 가는데 할머니가 짐을 무겁게 짊어지고 가시길래. 행선지를 물었는데. 북촌 근처였어. 그 분이 한사코 사양하셨는데. 모셔다 드렸어. 1주일인가. 뒤에 딸을 통해서 할머니가 이런 고마움을 받았다 해서. 음료수 한잔을 전달해 달라해서. 전달 받았던 경험이 이었지저스틴! 몸도 좋고 얼굴도 잘생겼는데 라이딩 하면서 로맨스는 없었어?(그게 쉽지가 않은게.) 많은 사람을 만나며 사사로운 감정을 가질 수 있지만 아띠라는 이름을 달고 하는것이기 때문에 사람들을 만나며 개인적인 감정을 가질 수 있는것을 만들지 않았던거 같아 아띠에 피해를 끼치고 싶지 않고 사람들을 아띠를 통해 만나지만 사람들은 아띠를 만나는 것이기 때문에 아띠에 영향을 끼치고 싶지 않았던거 같아. 나의 오지랖이지. 잡생각이 많았지. 본인은 어떤 라이더인거 같아? 인력거를 타는 동안. 본인이 느낄 수 잇는 가장 편안함? 안좋은 감정, 스트레스를 모두 날려버리고, 인력거를 타는 동안은 가장 편안한 상태가 되는 것 같아아띠가 변화되는 과정을 보았잖아. 어때? 어땠어?뭔가 젊은 친구들이 모여서 열정과 에너지를 쏟는 것을 보면서 감동도 받았지만. 성장하면서. 어려운 부분도 많고. 어려운 점도 많았을텐데. 50명이라는 라이더로 성장한게. 아띠인력거라는 회사가 사람들에게 좋은 인식과 윤리적인 행동을 하고 있다는 것을 느꼈어. 하지만 앞으로 가야할 길이 많다는 거. 노력해야 할게 많아.변화되는 모습에 항상 놀라워. 2년. 3년 시간에 많은 라이더가 일하고 있고. 무엇보다도 라이딩하는 친구들이회사 정규인원으로 속해 일하는게 아님에도 불구하고. 라이더 한명 한명이 아띠에 대한 애정과 애착을 가지고 있는 것을 보면 더 많은 성장을 할 수 있는 아띠라고 생각해.  아띠가 어떤 영향을 준거 같아?사회생활 하면서. 되게. 로직한. 제너럴한 삶을 살뻔 했는데. 아띠를 만나면서. 정말 내가 인생에 있어서 추구해야할 가치가 무엇인지 되돌아 보게 한. 단순히 내가 돈을 벌며 일을 하는 것 이상으로 내가 무엇을 해야 행복할 수 있고. 가치 있는 삶인지 일깨워 주는 곳. 저스틴에게 아띠란?1.o2. 산소다. 일상생활 속에 지쳐있을 때 숨 쉴수 있는 공간. 활력소가 되는.인력거를 타는 순간 원 없이 즐기고, 한 없이 웃고, 행복할 수 있는.2. 행복한 놀이터다. 원없이 즐기고 갈 수 있는. 무언가. 힐링과 재미를 느낄 수 있는. 마지막으로 아띠에게 바라는 점?사람들한테 앞으로도 계속 좋은 인상과 좋은 경험을 전달할 수 있는 아띠의 처음 모습 그대로 끝까지 남아있었으면 좋겠어. 아띠 화이팅이야!!아띠의 원년멤버로써 아직도 힘차게 페달을 밟고 있는  매일매일 새로운 삶을 사는 라이더, 저스틴의 스토리였습니다:)아띠를 직장이 아니라 행복한 놀이터라고 생각하는 저스틴이검은 머리 파뿌리 될때까지 힐링과 재미를 느낄 수 있는 아띠는 그런 공간이 되고 싶습니다!!
조회수 1203

우리가 세월호 현장에 간 이유 (실천)

기업의 목적은 이윤추구만이 아니다.1. 우리가 할 수 있는 것2014년 4월 16일 세월호 사건 당일 낮 우리의 하루는 평소와 같았다. 팀원들과 함께 점심을 먹던 식당에서 사고 소식을 뉴스로 처음 듣게 된다. 모든 방송들이 특종으로 사고를 보도하고 있었다. 이렇게 언론이 나서고 정부가 나서고 있기 때문에 모두 무사히 구조될 거라 생각을 하고 다시 일을 시작했다. 그리고 전원 구조라는 뉴스를 접하고 마음을 놓았었다.< 낮에는 분명 이런 뉴스들이 쏟아져 나왔다. >하지만 저녁시간이 되서 길거리로 나갈 노점 준비를 하던 팀원들이 술렁이기 시작했다. 생각했던 것보다 상황이 심각했고 사망자들이 나오기 시작했기 때문이였다.그리던 중 한 친구가 이야기를 했다. "저기도 충전하르라고 난리일 거라고.. 우리가 가면 딱인데.."우리끼리 말하는 용어로 "충전밥"을 먹어왔던 나는 어느 장소나 어떤 상황에서 휴대폰 충전이 필요한지 너무나도 잘 알고 있었다. 이번 사고는 더구나 자식의 생사를 확인하기 위해 생업에서 단숨에 달려온 부모님들이기 때문에 상황은 더 절박할 거라는 것을 너무나도 잘 알고 있었다. 개인적으로는 바로 팽목항으로 내려가고 싶었지만 대표인 나는 회사의 경제적인 상황을 고민해야만 했다. 우리 회사는 낮에는 앱 개발과 영업을 그리고 밤에는 생존을 위해 한 푼이라도 벌겠다고 1년째 노점상을 하고 있는 상태였다. 계산을 해보니 회사가 천만 원 정도의 손해를 볼 텐데 내려가야만 하는 것인가? 그렇게 현실적인 문제로 인해 머릿속은 정말 복잡했고 팀원들과 계속 뉴스를 지켜보면서 안타까운 마음은 계속 커져만 갔다.일이 손에 잡히질 않았고 같이 창업해서 부대표를 하고 있는 동생에게 물어봤다. 부대표도 알고 있었다 내려가면 우리가 얼마의 손해를 보게 되는지.. 그래서 나한테 가자는 말을 쉽게 꺼내지 못했었다."야 이거 우리가 가는 게 맞을까? 넌 꼭 가고 싶냐고?""형 이런? 충전기 있는 회사는 우리밖에 없을걸....""하긴 이런 걸? 만들어서 쓰는 특이한 회사가 또 있을까? 우리밖에 없을 거야. 그렇지?""형 나중에 후회하지 말자. 가자. 돈은 다녀와서 또 벌면 되잖아. "<  우리에겐 동시에 수백대의 스마트폰을 충전 할 수 있는 충전 테이블이 있었다. >그렇게 우리는 저 무식한? 테이블을 뜯어내고 서울에서 팽목항으로 달려가기로 결정을 했다.팀원들에게 말했다. "가자. 사람들 충전해주러...." 그제야 얼굴에 활기가 생긴 팀원들을 보며 마음 한편으로는 뿌듯했다. 그래도 내가 가슴이 따뜻한 친구들과 일을 하고 있었구나.우리는 행사 경험이 많았기 때문에 결정이 난 순간 일사천리로 실행에 옮겼다. 나와 동생은 충전실에서 충전 테이블을 뜯어내고 팀원들은 싣고 갈 승합차를 렌트하러 사무실을 나섰다. 미리 잡혀있던 투자사 미팅이나 업체 미팅에 전화를 걸어 양해를 구했다. 돈이 부족했기 때문에 조금씩 사비 또한 걷었다.그리고 나는 마지막으로 한 가지 조건을 붙였다."유니폼과 모자와 같이 회사를 알릴 수 있는 모든 것을 빼고 간다."우리는 창업 이후 만땅 서비스를 알리기 위해 단 한 번도 초록색 유니폼을 벗은 적이 없었다. 그래서 초록색 오빠들이라는 별명도 생겼고. 하지만 이런 현장에는 여러 회사들이 생색내고 간접 PR 하려고 대문짝만 한 기업 로고 붙여서 올 것이 뻔해 보였다. 우리만큼은 그냥 가서 조용히 도와주다 오자라는 취지에서였다. 그렇게 충전기와 비품에 회사 로고들을 떼어내고 유니폼을 벗어던졌다. 아마 창업이래 유니폼을 안 입고? 충전 서비스를 한 처음이자 마지막이 세월호 현장에서의 봉사활동이었다. 그리고 그 흔한 사진 한 장 조차 찍지 않았다.저녁에 출발을 해서 6시간을 달려 새벽쯤 팽목항에 도착을 했다. 우리는 충전 테이블을 설치하는 장소와 전기만 있으면 됐는데 이런 상황조차 여의치가 않았다. 충전 테이블을 들고 팽목항 주변을 수십 차례 돌아다녔다. 어디서 온 누구라고 밟히지 않았기에 관공서나 담당자분들의 도움 역시 없었다. 그래도 마지막으로 좋은 명당자리를 찾아냈다. 바로 화장실 앞이었다. 사람들의 동선에도 좋고 눈에 잘 띄고 비를 피할 수 있으니 금상첨화였다.그렇게 우리는 교대로 화장실 앞에서 근무를 서며 세월호 사고 현장에서 충전 봉사활동을 시작했다. 아무 말하지도 않았지만 휴대폰 충전이 필요한 수많은 부모님들과 구조대원들 사이에서 화장실 앞에 충전소가 설치되었다는 이야기는 빠르게 퍼져나갔다. 아침이 되었을 쯤에는 150개의 충전기가 모자를 만큼 공급이 수요를 따라가지 못할 정도였다. 우리가 충전해드린 그 휴대폰으로 가장 먼저 생존 소식을 가족들에게 전할 수 있다고 생각했지만 안타깝고 슬프게도 부모님들에게는 비극이 다가오고 있었다.2. 어른들의 잘못해맑게 웃으며 수학여행을 떠난 수백 명의 아이들이 차디찬 물속에서 구조의 손길을 기다리고 있었다. 비통하고 원통하고 너무나도 슬펐지만 우리가 할 수 있는 것 아무것도 없었다. 그저 부모님들 옆에서 위로의 말 한마디만을 전해 드리는 게 잘 동작하지 않는 휴대폰을 살펴봐 드리는 게 전부였다.아이들이 무슨 죄를 졌다고 이런 비극을 겪어야만 하는가?나를 포함한 이 나라의 어른들의 잘못된 판단과 대처로 팽목항 앞바다에서 아이들은 하나둘씩 죽어가고 있었다.어른들의 잘못으로 인해 죽어가야만 했던 했던 아이들...작은 배들이 사고 현장을 오가면서 발견된 시신을 뭍으로 옮겨왔다. 배가 들어올 때마다 시신을 확인해야 하는 부모님들의 그 심정을 누가 헤아릴 수 있겠는가? 그 충격이 너무 크기에 확인을 하는 건 주로 아버님들의 몫이었다. 배가 들어올 때마다 근처는 울음바다가 되었지만 아직 돌아오지 않은 학생들이 있었고 구조될 거라는 희망을 끈을 모두가 놓지 않았다.하지만 시간이 하루, 이틀, 사흘 지나면서 상황은 변해 갔다. "지금 들어오는 배에 있는 시신이 내 자식이었으면 좋겠어. 내 손으로 묻어 줄 수 있게 말이야." 한 아버님이 그렇게 피를 토하며 눈물을 흘리셨다. 그렇게 팽목항은 자식의 생존을 기다리는 현장에서 자식의 시신을 기다리는 현장으로 변해갔다.분명하게 내가 느낀 현장에서의 상황은 대처만 제대로 했어도 수많은 생명을 살릴 수 있었다. 아이들을 구하겠다고 전국에서 모여든 민간 잠수부들은 대부분 사고 현장에 접근조차 하지 못했다. 또한 사고 현장과 진행상황을 있는 그대로 보도하지 않았던 쓰레기 언론사 기자들의 모습을 내 눈으로 똑똑히 보았고, 무능력한 정부의 대처를 지켜보았다.그들에게 묻고 싶었다.당신 아이가 저안에 있어도 그렇게 기사를 쓸텐가?당신 아이가 저안에 있어도 이렇게 구조활동을 할텐가?그 와중에 사고 현장에 와서 기념? 사진만을 촬영하고 돌아가는 아주 발 빠른 정치인들도 볼 수 있었다.3. 현재의 위치에서 할 수 있는 것들우리는 4일간 팽목항 현장에 있으면서 숙박을 해결하기 위해 첫날 한 민박집을 찾았었다. 80세가 넘어 보이는 어르신이 운영하는 민박집이었는데 시꺼먼 남정네들이 새벽에 방을 구하러 왔다고 하기 놀래면서 사연을 물으셨다. 여차저차 상황을 말씀드렸더니 한 말씀을 하셨다."아이고 기특들 하네  내가 해줄 수 있는 건 내 집을 내어주는 거네. 돈은 내지 말아"그리고는 귀한 손님 올 때 대접하는 거라며 냉장고 한편에서 천으로 꼬깃꼬깃 싸인 냄새나는 무언가를 꺼내 보이셨다. 그렇게 우리에게 건네주셨던 시큼한 홍어회와 막걸리 한잔은 아직도 잊을 수가 없다.젊은 친구들은 충전소 한다고 하니 충전을 하는 거고 나는 민박집을 하니 집을 내어 줄 수 있으니 같은 거야. 현재의 위치에서 할 수 있는 것을 하는 것 그거면 된다고... 그렇게 우리는 따뜻한 어르신의 배려로 무료로 숙박을 해결할 수 있었다.우리가 한 것은 우리만이 가지고 있던 충전 테이블을 가지고 사고 현장으로 내려가서 부모님과 구조대원분들의 충전의 도운 것뿐이다. 빠르고 신속하게 내려가서 충전소를 설치했다. 우리 이후로 통신 3사에서도 대형 천막과 로고로 무장한 충전소들이 속속들이 들어섰지만 그 전체의 충전기들을 모두 합쳐도 우리의 가진 충전기의 절반조차 되지 않았다.우리는 그렇게 세월호 사고 현장속에서 4일을 보냈다. 사진 한 장의 기록조차 남기지 않았고 언론보도를 왜 안했냐고 물었지만 후회하지 않았다. 심지어는 서울로 돌아오는 길에 렌트한 승합차가 접촉사고 까지 나서 큰돈을 물어줘야만 했다.4. 실행과 실천의 차이우리 팀이 여태껏 해왔던 것은 빠른 실행이었다. 하지만 세월호 사건 현장으로 달려간 것은 단순히 행동에 옮기는 것이 아닌 우리가 할 수 있는 것을 하자라는 의미를 두고 행하는 것... 바로 "실천"이었다.기업의 목적은 이윤추구, 고용창출, 사회적 기여라고 배웠었다. 보통은 첫 번째가 이윤추구이고 어느 정도 여유가 생겼을 때 사회적 기여를 하지만 그 순서에는 정답이 없는 거라 생각한다. 배달의 민족이 적자인 상황에서도 어르신들에게 우유를 배달하는 캠페인을 하는 모습을 봤었고, 근무를 했던 LG전자에서는 직접 컴퓨터 교육 봉사활동을 나가거나 회사가 금전적으로 조용하게 티 내지 않고 많은 기부를 하는 올바른 실천을 하는 모습을 보면서 지내 왔었다. 그래서 창업을 하면 우리도 언젠가는 사회에는 보탬이 될 수 있는 상황에서는 그 실천을 해야 한다고 마음을 가지고 있었기에 충전을 제공하는 우리는 어찌 보면 세월호 현장으로 가야만 하는 유일한 회사들 중에 하나였을지 모른다.그리고 매일 같이 노점을 하면서 고생을 하고 무시를 당했지만 우리도 그 누군가에게 또는 그 어떤 상황에서는 사회에 조금이나마 보탬이 될 수 있는 구성원임을 배우는 계기가 되었다. 그리고 회사가 성장하는 만큼 여유가 생기는 만큼 더 사회에 보탬이 되는 실천을 하자라는 또 다른 목표를 가지게 되었다.#스푼 #Spoon #사회공헌 #봉사활동 #기업문화 #조직문화
조회수 4846

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

들어가기1부에서는, KeyStore 를 사용해 Shared Preferences 를 암호화 하는 법에 대해 알아봤습니다. 그리고 이 글에서는 Room을 사용한 Database 를 암호화 하는 방법에 대해 설명합니다.2018년 현재, 안드로이드 자체에서 데이터베이스를 암호화하는 기능을 제공해 주진 않습니다. 따라서 오픈 소스 프로젝트인 SQLCipher, SafeRoom 의 사용법 위주로 설명할 예정입니다. 또한 KeyStore 에 대칭키를 생성하는 기능은 API Level 23 이후에서만 가능하며, SQLCipher 가 Android KeyStore 를 지원하지 않고 있습니다.이로 인해 1부에서 소개한 키 암호화 메커니즘으로 보호한 별도의 키를 디스크 어딘가에 저장해 두고, 필요할 때만 복호화 해서 쓴 다음 복호화된 내용을 지우는 방식으로 구현해야 합니다. 하지만 이런 방식으로 사용하는 키는 메모리에 순간적으로 남기 때문에 좋은 공격 표면(Attack surface) 이 됩니다. 그 이유도 함께 다뤄 보겠습니다.SqlCipher team 에서 하루라도 빨리 현재의 char[] 형식의 passphrase 를 입력받는 대신, JCA 를 사용해 암호화하는 데이터베이스를 구현하길 기대해 봅시다.SqlCipher1부에서 보여드렸다시피 internal storage 에 저장한 데이터는 결코 안전하지 않습니다. 파일 DB 인 Sqlite 데이터는 포맷을 모르면 어차피 볼 수 없을테니 조금 다르지 않을까요? 그렇지 않다는 것을 다음 예에서 보여드리겠습니다. 루팅한 디바이스에서 adb pull명령으로 sqlite3 데이터베이스를 추출 후 내용을 열어보면 다음과 같습니다.$ hexdump -vC secure_database.sqlite3 00000000  53 51 4c 69 74 65 20 66  6f 72 6d 61 74 20 33 00  |SQLite format 3.| 00000010  10 00 02 02 00 40 20 20  00 00 00 02 00 00 00 04  |.....@  ........| 00000020  00 00 00 00 00 00 00 00  00 00 00 04 00 00 00 04  |................| 00000030  00 00 00 00 00 00 00 04  00 00 00 01 00 00 00 00  |................| 00000040  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................| 00000050  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 02  |................| 00000060  00 2e 01 5a 0d 0f 95 00  02 0e a9 00 0e a9 0f c9  |...Z............| 00000070  0e 6f 0e 6f 00 00 00 00  00 00 00 00 00 00 00 00  |.o.o............| ... 00000d30  00 00 00 00 00 82 37 03  07 17 57 57 01 83 4d 74  |......7...WW..Mt| 00000d40  61 62 6c 65 73 71 6c 69  74 65 62 72 6f 77 73 65  |ablesqlitebrowse| 00000d50  72 5f 72 65 6e 61 6d 65  5f 63 6f 6c 75 6d 6e 5f  |r_rename_column_| 00000d60  6e 65 77 5f 74 61 62 6c  65 73 71 6c 69 74 65 62  |new_tablesqliteb| 00000d70  72 6f 77 73 65 72 5f 72  65 6e 61 6d 65 5f 63 6f  |rowser_rename_co| 00000d80  6c 75 6d 6e 5f 6e 65 77  5f 74 61 62 6c 65 05 43  |lumn_new_table.C| 00000d90  52 45 41 54 45 20 54 41  42 4c 45 20 60 73 71 6c  |REATE TABLE `sql| 00000da0  69 74 65 62 72 6f 77 73  65 72 5f 72 65 6e 61 6d  |itebrowser_renam| 00000db0  65 5f 63 6f 6c 75 6d 6e  5f 6e 65 77 5f 74 61 62  |e_column_new_tab| 00000dc0  6c 65 60 20 00 00 00 00  00 00 00 00 00 00 00 09  |le` ............| ... [리스트 1] Internal storage 에 저장된 SQLite3 database 를 dump 한 결과.역시 기대했던대로 데이터가 하나도 암호화되어 있지 않은 것을 확인할 수 있습니다. 그렇다면 가장 간단한 방법은 SQLiteDatabase클래스를 확장하는 일일 텐데요, 문제는 이 클래스가 final 로 상속 불가능하게 되어 있단 점입니다. 이 때문에 암호화된 SQLiteDatabase 구현체는 이 클래스 및 이 클래스에 강하게 결합되어 있는 SQLiteOpenHelper 를 온전히 쓸 수 없다는 문제가 있습니다. 즉, 바닥부터 새로 만들어야 하는 상황인데요, 다행히도 Zetetic 사에서 만든 SQLCipher for Android 는 이 문제를 모두 해결해 주는 고마운 오픈 소스 프로젝트입니다.SqlCipher 의 사용법은 기존의 SQLiteDatabase 에 의존하던 로직들의 import namespace 만 바꿔주면 되도록 구현되어 있어 마이그레이션 비용도 거의 들지 않습니다.// 안드로이드에서 제공해 주는 SQLiteDatabase 클래스명 import android.database.sqlite.SQLiteDatabase; // SqlCipher 에서 제공해 주는 SQLiteDatabase 클래스명 import net.sqlcipher.database.SQLiteDatabase; // 프로그램 시작시 native library 를 로드해줘야 한다. class MyApplication extends android.app.Application {    @Override public void onCreate() {        super.onCreate();        net.sqlcipher.database.SQLiteDatabase.loadLibs(this);    } } [리스트 2] android SQLiteDatabase 에서 SqlCipher SQLiteDatabase 로 마이그레이션 하기물론 두 클래스는 전혀 타입 호환되지 않지만, net.sqlcipher.database.SQLiteDatabase 의 모든 메소드 및 field의 signature 가 기본 android.database.sqlite.SQLiteDatabase 와 같기 때문에 이런 변경이 가능합니다. SqlCipher 개발팀의 수고에 박수를 보냅니다.RoomRoom 은 SQL 을 객체로 매핑해 주는 도구입니다. Room 을 이용해 데이터베이스를 열 때는 보통 아래와 같은 코드를 사용합니다.object Singletons {    val db: DataSource by lazy {        Room.databaseBuilder(appContext, DataSource::class.java, "secure_database")            .build()    } } abstract class DataSource: RoomDatabase() {    abstract fun userProfileDao(): UserProfileDao } // 클라이언트 코드에서 아래와 같이 호출 val userProfile: UserProfile = Singletons.db.userProfileDao().findUserByUid(userId) [리스트 3] Room database 의 정의 및 활용Sqlite 의 기본 동작은 파일 데이터베이스에 단순 Read 및 Write 만 합니다. 따라서 데이터베이스 접근시 암호화/복호화 동작을 하는 callback 을 주입해야 데이터베이스를 암호화 할 수 있습니다. 그리고 RoomDatabase.Builder 클래스는 데이터베이스를 열때 우리가 주입한 일을 할 수 있는 hook method(openHelperFactory) 를 제공해 주고 있습니다. 다음 코드를 살펴봅시다.class RoomDatabase.Builder {    class Builder {        /**        * Sets the database factory. If not set, it defaults to {@link FrameworkSQLiteOpenHelperFactory}.        */        @NonNull        public Builder openHelperFactory(@Nullable SupportSQLiteOpenHelper.Factory factory)    } } interface SupportSQLiteOpenHelper {    /**     * Create and/or open a database that will be used for reading and writing.     */    SupportSQLiteDatabase getWritableDatabase();    /**     * Create and/or open a database. This will be the same object returned by {@link #getWritableDatabase}.     */    SupportSQLiteDatabase getReadableDatabase();    /**     * Factory class to create instances of {@link SupportSQLiteOpenHelper} using {@link Configuration}.     */    interface Factory {        /**         * Creates an instance of {@link SupportSQLiteOpenHelper} using the given configuration.         */        SupportSQLiteOpenHelper create(Configuration configuration);    } } [리스트 4] Room builder 의 SupportSQLiteOpenHelper 주입 메소드 및 SupportSQLiteOpenHelper.Factory 인터페이스 정의설명을 최대한 간소하게 하기 위해 관심가질 필요 없는 코드 및 코멘트는 모두 제외했습니다. 아무튼 SupportSQLiteOpenHelper 구현체를 주입하면 뭔가 데이터베이스 작업 이전에 우리의 로직을 실행할 수 있을 것 같습니다.사실 이 인터페이스의 핵심은 바로 getWritableDatabase(), getReadableDatabase() 구현입니다. javadoc 에도 있지만 두 메소드로 반환하는 인스턴스는 같아야 하며 또한 암호화를 지원해야 한다는 것을 알 수 있습니다.결국 우리 목표는 Room 과 데이터베이스 암호화 로직을 연결해 주는 SupportSQLiteDatabase 구현체를 만드는 것임을 알 수 있습니다. 이 인터페이스는 규모가 제법 크기 때문에 이게 만만한 일이 아님을 직감하실 수 있을 겁니다.saferoom 도입으로 SupportSQLiteDatabase 인터페이스 구현체 사용하기앞서 살펴봤듯 SupportSQLiteDatabase 구현에는 상당한 노력이 필요하단 것을 알 수 있습니다. 그런데 고맙게도 saferoom 이라는 오픈 소스 프로젝트가 우리의 귀찮음을 잘 해결해 주고 있습니다. saferoom 의 SupportSQLiteOpenHelper 구현체를 간단히 살펴보면 아래와 같습니다./** * SupportSQLiteOpenHelper.Factory implementation, for use with Room  * and similar libraries, that supports SQLCipher for Android.  */ public class SafeHelperFactory implements SupportSQLiteOpenHelper.Factory {    private final char[] passphrase;    public SafeHelperFactory(final char[] passphrase) {        this.passphrase = passphrase;    }    @Override    public SupportSQLiteOpenHelper create(final SupportSQLiteOpenHelper.Configuration configuration) {        return(new com.commonsware.cwac.saferoom.Helper(configuration.context,            configuration.name, configuration.version, configuration.callback,            this.passphrase));    }    /**     * NOTE: this implementation zeros out the passphrase after opening the database     */    @Override    public SupportSQLiteDatabase getWritableDatabase() {        SupportSQLiteDatabase result = delegate.getWritableSupportDatabase(passphrase);        for (int i = 0; i < passphrase>            passphrase[i] = (char) 0;        }        return(result);    }    /**     * NOTE: this implementation delegates to getWritableDatabase(), to ensure that we only need the passphrase once     */    @Override    public SupportSQLiteDatabase getReadableDatabase() {        return getWritableDatabase();    } } /**  * SupportSQLiteOpenHelper implementation that works with SQLCipher for Android  */ class Helper implements SupportSQLiteOpenHelper {    final OpenHelper delegate;    Helper(Context context, String name, int version, SupportSQLiteOpenHelper.Callback callback, char[] passphrase) {        net.sqlcipher.database.SQLiteDatabase.loadLibs(context);        this.delegate = createDelegate(context, name, version, callback);        this.passphrase = passphrase;    }    abstract static class OpenHelper extends net.sqlcipher.database.SQLiteOpenHelper {        SupportSQLiteDatabase getWritableSupportDatabase(char[] passphrase) {            SQLiteDatabase db = super.getWritableDatabase(passphrase); return getWrappedDb(db);        }    } } [리스트 5] Saferoom 의 SupportSQLiteOpenHelper 구현체.소스 코드를 보면 SQLiteDatabase 의 원래 요구사항을 만족하지 못하는 구현 부분도 보입니다만, 그래도 이 정도면 수고를 꽤 크게 덜 수 있어 훌륭합니다.그리고 로직을 잘 보면 데이터베이스를 연 직후 암호로 넘겨준 char[] 배열을 초기화 하는 코드가 있다는 점입니다. 이것이 바로 이 문서의 서두에서 말했던 attack surface 를 최소화 하기 위한 구현입니다. 이 글의 주제에서 벗어난 내용이기에 여기서는 다루지 않습니다만, 궁금하신 분들은 부록 1: in-memory attack 맛보기에서 확인하실 수 있습니다.SqlCipher + SafeRoom + Room 구현 및 코드 설명이상으로 데이터베이스 암호화 전략에 대해 살펴봤습니다. 이 장에서는 실제로 연동하는 방법에 대해 다룹니다.불행히도 2018년 현재 SqlCipher 는 Android KeyStore 를 지원하지 않고 있습니다. 그리고 인스턴스 생성에 쓸 비밀번호로 CharArray 가 필요한데, 이 값은 한번 정해지면 불변해야 합니다. 여기 사용할 키를 KeyStore 에 저장하면 문제를 깔끔하게 해결할 수 있을 것 같습니다. 하지만 1부에서 살펴봤듯이 하드웨어로 구현된 Android KeyStore 밖으로는 키가 절대로 노출되지 않는다고 합니다. 이 문제를 어떻게 해결해야 할까요?먼저, SqlCipher 에 사용하기 위해 KeyStore 로 생성한 AES256 키의 내용을 한번 살펴봅시다.val secretKey = with(KeyGenerator.getInstance("AES", "AndroidKeyStore"), {    init(KeyGenParameterSpec.Builder(alias,             KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)        .setKeySize(256)        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)        .build())    generateKey() }) val keyInfo = with(KeyFactory.getInstance(privKey.getAlgorithm(), "AndroidKeyStore"), {    factory.getKeySpec(privKey, KeyInfo::class.java) }) println("Key algorithm : " + secretKey.algorithm) println("Key format : " + secretKey.format) println("Encoded key size: " + secretKey.encoded?.size) println("Hardware-backed : " + keyInfo.isInsideSecureHardware) // 실행 결과 Key algorithm : AES Key format : null Encoded key size: null Hardware-backed : true [리스트 6] AndroidKeyStore 에 저장한 Key 는 어플리케이션에서 직접 쓸 수 없다.저희가 보유중인 개발 시료 Nexus 5 에서 실행한 결과 위와 같이 나타났습니다. secretKey.encoded 의 값이 메모리에 있다면 이 값을 SqlCipher 생성자에 넘겨줄 수 있겠지만 값이 null 이네요. 보안 측면에서는 다행일 지 모르지만 우리 구현에서는 쓸 수 없으니 문제입니다. 그래서 별 수 없이 임의로 키를 만들고(AndroidAesHelper#generateRandomKey()), 1부에서 소개했던 AndroidRsaCipherHelper 를 이용해 암호화한 값을 Shared Preferences에 저장하는 식으로 구현해 봅시다.val settingsPrefs = appContext.getSharedPreferences("app_settings", Context.MODE_PRIVATE) val settings = SecureSharedPreferences.of(settingsPrefs) val dbPass = with(settings, {    /*     * String.toCharArray() 같은 함수를 쓰면 로직이 좀더 간단해지지만, JVM 에서의 String은     * Immutable 하기 때문에 GC 이전에는 지울 방법이 없으므로 attack surface 가 더 오랫동안     * 노출되는 부작용이 있다. 따라서 key의 plaintext 는 가급적 String 형태로 저장하면 안된다.     */    var savedDbPass = getString("DB_PASSPHRASE", "")    if (savedDbPass.isEmpty()) {        // KeyStore 에 저장해도 SqlCipher 가 써먹질 못하니 그냥 1회용 키 생성 용도로만 활용한다.        val secretKey = AndroidAesCipherHelper.generateRandomKey(256)        // String 생성자 사용: 이 문자열은 heap 에 저장된다.        savedDbPass = String(Base64.encode(secretKey, Base64.DEFAULT))        putString("DB_PASSPHRASE",  AndroidRsaCipherHelper.encrypt(savedDbPass))        // 메모리 내에 plaintext 형태로 존재하는 attack surface 를 소멸시켜 준다.        secretKey.fill(0, 0, secretKey.size - 1)    } else {        // decrypt 메소드 내부에서 String 생성자 사용하므로 base64 인코딩된 plaintext 키는 heap 에 저장된다.        savedDbPass = AndroidRsaCipherHelper.decrypt(savedDbPass)    }    val dbPassBytes = Base64.decode(savedDbPass, Base64.DEFAULT)    /*     * SqlCipher 내부에서는 이 char[] 배열이 UTF-8 인코딩이라고 가정하고 있다.     * 그리고 UTF-8 인코딩에서는 byte range 의 char 는 1 바이트니까,     * 아래 변환을 거치더라도 키 길이는 32 byte(256 bit)가 유지된다.     *     * UTF-8 인코딩에서는 32 글자 != 32 바이트가 아님에 항상 유의해야 한다!     */    CharArray(dbPassBytes.size, { i -> dbPassBytes[i].toChar() }) }) [리스트 7] 암호화한 SqlCipher 용 passphrase 를 사용하는 방법.위 코드를 사용해 char[] 타입의 값 dbPass 를 얻을 수 있습니다. 리스트 7을 이용해 얻은 dbPass를 아래 코드에 사용하면 SqlCipher - SafeRoom - Room 의 연동이 끝납니다.val dataSource = Room.databaseBuilder(_instance, DataSource::class.java, "secure_database") .openHelperFactory(SafeHelperFactory(dbPass))                .build() // 메모리 내에 plaintext 형태로 존재하는 attack surface 를 소멸시켜 준다. dbPass.fill('0', 0, dbPass.size - 1) [리스트 8] SqlCipher - SafeRoom - Room 연동하기위 코드에서 볼 수 있듯, 임의로 저장한 키를 Base64 인코딩으로 변환, 그리고 그것을 다시 CharArray 로 변환하는 과정에서 key 가 메모리에 존재해야 하는 순간이 있습니다. 이 구간을 바로 공격 표면(attack surface) 이라고 합니다.JVM 단에서 넘겨주는 Passphrase 를 SqlCipher 내부에서 native 로 어떻게 처리하고 있는지는 SqlCipher SQLiteDatabase 구현및 SqlCipher crypto 구현 에서 확인할 수 있습니다.결과 확인하기SafeHelperFactory 를 주입한 Room database 파일을 추출 후 hexdump 로 확인해 보겠습니다.hwan@ubuntu:~$ hexdump -vC secure_database.sqlite3 00000000  8c 0d 04 07 03 02 11 eb  a4 18 33 4f 93 e8 ed d2  |..........3O....| 00000010  e9 01 21 d7 49 df 25 9a  f4 1d c7 1e ff 2d b0 13  |..!.I.%......-..| 00000020  fc 17 9b 4b b2 1c a3 1d  7d 1d 69 76 b1 ea ec e8  |...K....}.iv....| 00000030  1f 50 e4 c4 6c 50 e6 82  58 27 b9 fe 85 21 27 99  |.P..lP..X'...!'.| 00000040  ec 54 53 ba 32 c6 59 09  b4 30 65 39 a0 75 3e c4  |.TS.2.Y..0e9.u>.| 00000050  b8 f7 ea 47 14 df c4 f0  7c be 9f 62 26 49 1c b2  |...G....|..b&I..| 00000060  0f 63 00 7a 09 7e 33 e0  43 2b eb ea 80 21 bb 5d  |.c.z.~3.C+...!.]| 00000070  5c 04 ff 57 a3 a3 7f c2  19 42 b9 67 6c e3 d5 c8  |\..W.....B.gl...| ... 00000d30  c1 f3 93 1f 4e 5b 6a 70  39 c2 e9 2c 3e 8f 7e ff  |....N[jp9..,>.~.| 00000d40  73 3a 9a 39 0d 8a 1a 3e  6b d4 5b de 1f 6d c4 b8  |s:.9...>k.[..m..| 00000d50  fb 62 3e 21 09 0a 31 20  37 5d 8d 0a 39 6d 35 31  |.b>!..1 7]..9m51| 00000d60  26 d6 b0 22 41 7e 6c 54  7d 77 22 ba 1b f3 cf 5a  |&.."A~lT}w"....Z| 00000d70  e5 47 97 76 f0 89 e5 98  b3 37 3c 8d 43 af 0e b9  |.G.v.....7<.C...| 00000d80  18 74 fd f5 2a 41 d8 b1  d9 70 32 0b 5c 93 4b 0d  |.t..*A...p2.\.K.| 00000d90  bc 60 4c 25 9a ec 53 23  90 60 b2 52 a8 a1 b1 87  |.`L%..S#.`.R....| 00000da0  f3 3e 03 3e ac 0a 75 a0  61 d8 bd 07 b8 5a 48 66  |.>.>..u.a....ZHf| 00000db0  57 85 13 ac 04 26 55 30  34 46 57 bf 8b 42 c6 2d  |W....&U04FW..B.-| 00000dc0  9e 82 a2 df 77 bb b3 2e  96 43 70 23 23 03 df 1d  |....w....Cp##...| ... [리스트 9] Internal storage 에 저장된 SQLite3 database 를 dump 한 결과. 리스트 1과 비교해 보자.이로서 오픈 소스의 힘을 빌려 우리 앱의 데이터베이스를 비교적 간편하게 암호화 할 수 있음을 알 수 있습니다.맺으며이로서 Persistent data 암호화에 대한 설명을 마칩니다. Android KeyStore 가 API Level 23 이상의 기기에서만 100% 동작한다는 점은 2018년 현재까지는 큰 단점입니다. 하지만 사소한 데이터라 하더라도 보안의 중요성은 날로 강조되고 있습니다. 따라서 빠르던 늦던 고객 데이터 암호화에 투자해야 할 순간이 다가온다는 점은 변하지 않습니다.언젠가는 적용해야 할 고객 데이터 보호의 순간에, 이 글이 여러분의 앱의 보안에 조금이나마 도움이 된다면 좋겠습니다.부록 1: in-memory attack 맛보기앞서 계속 반복해서 설명드렸던 메모리 내의 attack surface 를 찾아내는 방법을 간단히 설명해 보겠습니다. 잘 지키려면 잘 공격하는 법을 알아야 하므로 알아두면 좋지 않을까요? 그리고 일반적인 앱 개발과는 다소 동떨어진 이 장의 내용이 이해되지 않으신다면 한줄요약한 메모리 내부의 값도 때로는 안전하지 않을 수 있다 는 한마디만 기억해 두시면 됩니다. 모든 데모는 LG Nexus 5(Hammerhead), 시스템 버전 6.0.1(M) 에서 실행한 결과며 시스템마다 약간의 차이는 있을 수 있습니다.마켓에 출시한 앱들은 debuggable:false 가 설정된 상태이므로 힙 덤프를 바로 뜰 수는 없습니다. 그런데 어떻게 in-memory attack 이 가능할까요? 다음 리스트는 디버그 불가능한 앱의 힙 덤프를 시도할 때 보안 정책 위반 오류가 발생함을 보여줍니다.hwan@ubuntu:~$ adb shell ps | grep "com.securecompany.secureapp" USER PID PPID VSIZE RSS WCHAN PC NAME u0_a431   25755 208   1700384 100888 sys_epoll_ 00000000 S   com.securecompany.secureapp hwan@ubuntu:~$ adb shell am dumpheap 25755 "/data/local/tmp/com.securecompany.secureapp.heap" java.lang.SecurityException: Process not debuggable: ProcessRecord{b6f96fc 25755:com.securecompany.secureapp/u0_a431}     at android.os.Parcel.readException(Parcel.java:1620)     at android.os.Parcel.readException(Parcel.java:1573)     at android.app.ActivityManagerProxy.dumpHeap(ActivityManagerNative.java:4922)     at com.android.commands.am.Am.runDumpHeap(Am.java:1248)     at com.android.commands.am.Am.onRun(Am.java:377)     at com.android.internal.os.BaseCommand.run(BaseCommand.java:47)     at com.android.commands.am.Am.main(Am.java:100)     at com.android.internal.os.RuntimeInit.nativeFinishInit(Native Method)     at com.android.internal.os.RuntimeInit.main(RuntimeInit.java:251) [리스트 10] debuggable=false 설정된 앱의 힙 덤프 시도시 발생하는 예외(SecurityException)SuperUser 는 가능할까요? SuperUser 권한으로 앱을 강제로 디버그 가능한 상태로 시작해 보도록 하겠습니다.hwan@ubuntu:~$ adb shell 32|shell@hammerhead:/ $ su 1|root@hammerhead:/ \# am start -D -n "com.securecompany.secureapp/MainActivity" && exit Starting: Intent { cmp=com.securecompany.secureapp/MainActivity } hwan@ubuntu:~$ \# adb shell ps | grep "com.securecompany.secureapp" USER PID PPID VSIZE RSS WCHAN PC NAME u0_a431   27482 211   1700384 100888 sys_epoll_ 00000000 S   com.securecompany.secureapp hwan@ubuntu:~$ adb forward tcp:12345 jdwp:27482 hwan@ubuntu:~$ netstat -an | grep 12345                                                           tcp4       0      0  127.0.0.1.12345         *.*                    LISTEN     hwan@ubuntu:~$ jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=12345 java.net.SocketException: Connection reset     at java.net.SocketInputStream.read(SocketInputStream.java:210)     at java.net.SocketInputStream.read(SocketInputStream.java:141)     at com.sun.tools.jdi.SocketTransportService.handshake(SocketTransportService.java:130)     at com.sun.tools.jdi.SocketTransportService.attach(SocketTransportService.java:232)     at com.sun.tools.jdi.GenericAttachingConnector.attach(GenericAttachingConnector.java:116)     at com.sun.tools.jdi.SocketAttachingConnector.attach(SocketAttachingConnector.java:90)     at com.sun.tools.example.debug.tty.VMConnection.attachTarget(VMConnection.java:519)     at com.sun.tools.example.debug.tty.VMConnection.open(VMConnection.java:328)     at com.sun.tools.example.debug.tty.Env.init(Env.java:63)     at com.sun.tools.example.debug.tty.TTY.main(TTY.java:1082) Fatal error:  Unable to attach to target VM. [리스트 12] SuperUser 권한으로도도 Java 디버거를 붙일 수 없다.다행히도 debuggable=false 로 릴리즈한 앱은 자바 디버거(jdb)를 붙일 수 없으니 프로그램 실행을 매우 정밀하게 제어할 수는 없다는 것을 알 수 있습니다(debuggable=true 설정된 앱에 위 과정을 실행하면 어떤 일이 벌어지는지 직접 확인해 보세요!).하지만 안드로이드의 앱은 ‘linux process’ 에서 실행되므로 SuperUser 권한으로 process 메모리 전체 dump를 뜨는 것은 막을 수 없습니다. 정공법으로는 /proc/PID/maps 의 내용을 분석하면 됩니다만 제가 안드로이드를 깊게 알고 있는 것은 아니라, 어느 영역이 dalvik heap 인지를 알아낼 수 없었습니다. 이 때문에 프로세스 메모리를 통째로 떠서 내용을 헤집어보는 방식으로 공격해 보겠습니다. 여담입니다만, 데모를 위해 공격한 앱은 dumpsys 명령으로 확인해보니 약 6MiB 의 Java heap 을 쓰고 있는데요, 이 크기를 줄이면 줄일 수록 공격이 더욱 수월할 겁니다.아래 데모에서는 안드로이드 기기용(arm-linux-gnueabi)으로 컴파일한 gdb 를 미리 설치한 결과를 보여드리고 있습니다. 참고로 여기 보이는 [heap] 은 아쉽지만 native heap 이므로 우리 공격 목표는 아닙니다.1|root@hammerhead:/ \# cd /proc/27482 1|root@hammerhead:/proc/27482 \# cat maps 12c00000-12e07000 rw-p 00000000 00:04 8519       /dev/ashmem/dalvik-main space (deleted) ... b7712000-b771f000 rw-p 00000000 00:00 0 [heap] bee86000-beea7000 rw-p 00000000 00:00 0 [stack] ffff0000-ffff1000 r-xp 00000000 00:00 0 [vectors] 1|root@hammerhead:/proc/27482 \# ifconfig wlan0     Link encap:Ethernet          inet addr:192.168.12.117          inet6 addr: fe80::8e3a:e3ff:fe5f:64c9/64 1|root@hammerhead:/proc/27482 \# gdbserver –attach :12345 27482 Attached; pid = 27482 Listening on port 12345 [리스트 13] SuperUser 권한으로 gdbserver 실행.hwan@ubuntu:~$ adb forward tcp:23456 tcp:12345 hwan@ubuntu:~$ netstat -an | grep 23456 tcp4       0      0  127.0.0.1.23456         *.*                    LISTEN     [리스트 14] 로컬 포트 23456 으로 원격 포트 12345 를 연결하는 과정.이제 모든 준비가 끝났습니다. 개발 기기에서 gdb로 원격 프로세스에 접근한 뒤, 메모리를 덤프해 봅시다.hwan@ubuntu:~$ ./gdb (gdb) target remote 192.168.12.117:12345 Remote debugging using 192.168.12.117:12345 0xb6f92834 in ?? () (gdb) dump memory /tmp/com.securecompany.secureapp.heap 0x12c00000 0xb771f000 (gdb) [리스트 15] gdb 로 메모리를 덤프하는 과정.덤프한 힙 덤프 파일 속에 있을지도 모르는 문자열을 검색해 봅시다. 그 전에 잠시, 데이터베이스에 사용할 키를 어떻게 처리했었나 되새겨 볼까요? if (savedDbPass.isEmpty()) {        // ...        // String 생성자 사용: 이 문자열은 heap 에 저장된다.        savedDbPass = String(Base64.encode(secretKey, Base64.DEFAULT))    } else {        // decrypt 메소드 내부에서 String 생성자 사용하므로 base64 인코딩된 plaintext 키는 heap 에 저장된다.        savedDbPass = AndroidRsaCipherHelper.decrypt(savedDbPass)    } [리스트 16] Base64 인코딩을 처리하기 위한 임시 String 생성 과정.우리 로직은 256 비트의 키를 Base64 변환해서 디스크에 저장합니다. 그리고 256비트의 byte array 를 base64 변환한 결과는 (4 * (256 / 3)) / 8 = 42.66 바이트 -> 4의 배수여야 하므로 44바이트입니다. 약 1.34 바이트의 pad 를 맞추기 위해 문자열의 끝에 =가 최소 1글자 이상은 있을 겁니다. 한번 찾아봅시다.hwan@ubuntu:~$ strings /tmp/com.securecompany.secureapp.heap ... /masterkey ... user_0/.masterkey em_s 1337 ... [리스트 17] strings 명령을 사용한 힙 덤프 파일내의 문자열 검색의외로 = 나 == 로 끝나는 문자열이 발견되지 않습니다. 하지만 안심하기는 이릅니다. 이건 단순히 (공격자의 입장에서) 운이 나빠서 발견되지 않은 것일 뿐입니다. 우리가 원하는 어떤 ‘순간’ 에 힙 덤프 명령을 내리지 않았기 때문에 그렇습니다. 우리의 구현은 attack surface 를 매우 짧은 시간동안만 메모리에 노출하기 때문에 이 순간이 짧으면 짧을 수록, 디바이스의 성능이 좋으면 좋을 수록 순간을 잡아내기가 더욱 어려워집니다. 즉, 이 문서에서 보여드린 방식으로 CharArray 의 내용을 아주 짧은 시간 동안만 사용하고 지워버리면 내용을 탈취하기 굉장히 어렵습니다. 하지만 안심하기는 이릅니다. nano-time 단위로 앱을 실행할 수 있는 환경을 가진 국가급 공격자는 여전히 있기 때문입니다.그리고 이 방법은 루팅하지 않은 기기에서는 절대 재현이 불가능하므로 루팅되지 않은 환경일 경우에만 실행 가능하도록 한다던가 하는 방식까지 더한다면 공격자가 더욱 우리 앱을 뚫기 힘들 겁니다.여담입니다만 독자 여러분들 중 GameGuardian 처럼 다른 게임의 메모리값을 마구 바꾸는 앱이 어떻게 동작하나 궁금하신 분들도 있을 겁니다. 그런 류의 앱들도 바로, 이 장에서 설명했던 방식으로 동작합니다.장황했던 이 장의 내용을 한줄로 요약하면 Android KeyStore 로 보호하지 않은 키는 많은 수고를 들이면 뚫을 수 있다고 할 수 있습니다.부록 2: SQLite database 의 UPDATE / DELETE 구현 특성SQLite3 의 구현특성상, UPDATE / DELETE 시에 이전 레코드의 값이 남아있는 경우가 있습니다. 암호화 했으니 좀더 안전하다곤 하지만 찌거기 값을 굳이 남겨둬서 공격자에게 더 많은 힌트를 제공할 필요도 없습니다.이 문서는 암호화 구현에만 초점을 맞췄기 때문에 상세하게 다루진 않습니다만, LINE Tech blog에 소개된 True delete 는 이 문제를 해결하기 위한 방법을 제시하고 있으므로 그 문서도 한번 읽어보시길 권합니다.더 보기SQLCipherSafeRoomAndroid SQLite3 True delete - by LINE tech blogDifference between java.util.Random and java.security.SecureRandomAttack surface on security measuresAOSP: DebuggingRootbeer: Simple to use root checking Android library#하이퍼커넥트 #개발 #개발자 #안드로이드 #앱개발 #모바일 #PersistentData #인사이트 #개발후기
조회수 1746

ZOYIFUL TALK (4) - 스니커 덕후, 워크인사이트 덕후가 되다

그는 원래, 유명 스포츠 의류 매장의 매니저였다. 관리하던 매장의 연매출은 100억 원을 훌쩍 넘겼다. 전국 1위 매출의 벽이 워낙 견고해 그의 매장에는 ‘산성’이란 별명이 붙기도 했다.어떻게 해야 매출을 올릴 수 있을지를 그는 늘 고민했다. 제품이 돋보이는 진열 방식을 연구하기도 하고, 명품 브랜드 매장처럼 밀려드는 손님들을 문 앞에 줄세워 매장의 인기를 뜨겁게 만들기도 했다.하지만 왠지 모르게 계속 답답했다. 장사가 잘 될 때에도 원인을 분명히 알기 어려웠기 때문이다.손님들의 움직임을 유심히 관찰하며 구매로 이어지는 패턴을 파악하려 해도 몸은 하나, 눈은 두 개 뿐. 시도해 보고 싶은 일들이 많았지만 적절한 데이터가 없다보니 개선안을 찾는데 어려움이 많았다.데이터를 제대로 이해하는 것이 앞으로 매장의 생사를 가르겠단 생각이 들었다. 제품에 대한 관심을 불러 일으키는 진열 방식이나 손님이 선호하는 이동 경로같은 소비자 행동 데이터가 궁금했다. 그렇게 데이터 분석 공부를 시작했다가, 워크인사이트를 제공하는 조이를 알게 되었다. 찾던 데이터가 있다는 것을 알게된 후 두근거리는 마음으로 조이의 문을 두드렸다.성공한 스니커 덕후이자 요즘은 주5회 크로스핏을 하러 간다는 에너자이저, 워크인사이트 세일즈 매니저 제이슨과 이야기를 나눠 보았다.ZOYI: 간단한 자기소개 부탁드려요!Jason: 안녕하세요, 조이에서 워크인사이트 세일즈를 담당하는 김한성입니다. 오프라인 매장을 운영하는 기업들에게 워크인사이트 서비스 소개부터 실제로 활용해 가치를 만들어 내는 단계까지 모든 과정을 총괄하고 있습니다.ZOYI: 워크인사이트에 꽂혀서 세일즈를 시작하셨다고 들었어요.Jason: 네, 제가 원래 무언가를 좋아하면 엄청 꽂히는 스타일이에요. 매장 데이터 활용에 대해 고민이 많던 시기여서 워크인사이트를 보자마자 꽂혔어요. 제가 원했던 데이터를 이미 분석하고 있는 회사가 있어서 놀랐죠. 저는 지금도 워크인사이트가 지금의 오프라인 리테일 생태계를 한 단계 진보시킬 제품이라고 확신하고 있어요.워크인사이트 말고 또 꽂힌 그것. 그는 스니커 덕후이기도 하다 (신발가게 아닙니다)ZOYI:어떻게 확신을 하게 되셨나요?Jason: 현업에 있으면서 누구보다 워크인사이트의 필요성을 느꼈거든요.차별화된 매장 운영을 위해서는 데이터가 꼭 쌓여야 한다고 생각했어요. 예컨대 주말과 평일, 오전과 오후에 일하는 알바생이 다르잖아요? 그럼 자주 오는 손님을 못 알아보고 필요 이상으로 친절한 응대를 할 때가 있어요. 그 손님은 이미 매장 구조나 물건을 다 알고 있으니 적당한 거리를 두고 응대하는 게 더 나은데 말이죠. 자주 오는 손님에 대한 데이터가 있었다면 그러지 않았을 거예요.또 시간대 별로 외부 유동인구나 매장 방문객이 달라지는데, 이를 고려하지 않고 점원을 동일하게 배치하는 경우가 있어요. 언제 사람이 붐비는지 잘 이해하고 있으면 훨씬 더 효율적으로 인력을 운용할 수 있겠죠.그런데 요즘같이 매장 구석구석 CCTV가 달려있고 기술이 발전한 세상에도, 고객들의 움직임이나 방문 기록 같이 소비자 행동을 알 수 있는 데이터가 하나도 없는 거예요. 답답함에 일을 그만두고 데이터 분석을 공부하다 조이를 만났습니다. 워크인사이트는 제가 원했던 데이터를 모두 다 수집할 뿐만 아니라 이미 상당한 수준의 분석까지 하고 있더라구요.오프라인 매장에서도 상당한 수준의 분석이 가능하다는 사실을 알고 놀랐다ZOYI: 실제로 영업을 해보니 어떻던가요.Jason: 정말 멋진 기술이지만, 새로운 개념이다보니 초반에 고객들의 신뢰를 구축하는 데 시간이 많이 들었죠. 시중에 있는 제품이라면 '이건 기존 제품과 어떤 점이 달라?'부터 대답해도 되는데, 처음 보는 제품을 소개하려니 '이건 뭐야? 이게 왜 필요해?'부터 묻기 시작하셨거든요.ZOYI: 꽤 어려우셨겠어요.Jason: 쉽지 않았죠. 특히 초반부터 대형 브랜드들을 상대로 세일즈를 하려니 더 그랬던 것 같아요.ZOYI: 대형 브랜드를 상대로 세일즈를 시작했던 이유가 있나요?Jason: 지금 당장 이 기술이 가장 필요한 곳이라 생각했기 때문이에요. 과거에는 어디에 매장을 오픈할 것인지를 결정하는 게 핵심이었어요. 예를 들어 드럭스토어 같은 경우는 몇 년 만에 매장을 1,000개씩 늘렸죠. 매장만 오픈하면 매출이 절로 늘었으니까요.반면 지금은 대한민국 어딜 가든 매장이 포화상태예요. 양적 성장보다 질적 성장이 중요한 시기가 된거죠. 이제는 오픈된 매장을 어떻게 효율적으로 운용하고 매출을 잘 올리느냐를 고민해야 하는 때가 온 건데, 이를 위해서는 소비자 행동 데이터를 제대로 들여다 볼 필요가 있었습니다. 저희가 그걸 잘 할 수 있었고요.ZOYI: 어떻게 세일즈 하셨나요?Jason: 방문객 데이터에 대한 고객 이해도를 높이고, 기술로 차별화된 가치를 드리기 위해 팀이 다함께 노력했어요. 덕분에 이제는 시장에서도 데이터 분석을 해야 한다는 것에 공감대가 어느정도 생겼습니다.또 다양한 분석을 하면서 활용 사례를 많이 만들어 나가다 보니 고객들도 이젠 스스로 이용 방법을 잘 찾아가고 계세요.ZOYI: 뿌듯하시겠어요.Jason: 네, 특히 제품을 도입한 후, 고객사가 저희를 신뢰하게 될 때에는 정말 뿌듯합니다. 예전엔 왠지 모르게 싸늘했던 고객사 담당자가 요즘은 제 기호에 맞는 커피를 주시더라고요. (웃음)일할 맛 나는 순간ZOYI: 데이터는 어떻게 활용되고 있나요?Jason: 매장이 큰 경우는 공간 활용도를 높이기 위해 매장을 구역별로 나눠서 마케팅해요. 한 H&B(헬스 앤 뷰티) 브랜드의 경우 매장에 방문객 분석 센서를 여러 개 설치해서 매장에서는 손님이 접근하지 않는 '데드존(Dead zone)'과 손님의 발길이 끊기는 지점을 파악했어요. 이를 이용해 동선이 끊기지 않게 연결하고 데드존을 살리는 리모델링을 했는데요, 리모델링 후 이전 대비 매출이 20%나 상승했어요.매장을 다시 방문한 손님들에 관한 데이터도 다양하게 활용하고 있어요. 예를 들어 백화점에 입점한 한 의류 브랜드는 아이템 진열 모양이 달라지지 않았을 때 재방문 고객이 매장에 머무는 시간이 짧아진다는 패턴을 파악했어요. 그래서 아이템 진열 방식을 자주 바꿔줬죠. 똑같은 아이템이라도 다르게 배치되면 새로운 제품이 나온 것 같은 느낌을 주잖아요. 쇼핑 경험을 개선하자 자연스레 매출이 늘었어요.ZOYI: 역시 세일즈 매니저세요. '어머, 이건 꼭 사야 해'라는 생각이 절로 들게 만드시네요.Jason: 네 맞아요. 이건 사야죠. 앞으로는 소비자의 행동 패턴이 실제로 어떻게 변하는지 구체적인 데이터를 파악하고 대응하는 기업만이 오래 살아남을 수 있다고 생각해요. 주먹구구식으로 운영하는 매장과 그렇지 않은 매장이 장기적으로는 많이 달라질 겁니다. 사소해 보이는 질적 차이가 쌓이고 쌓이면 커다란 격차를 만들어 낼 거니까요.ZOYI: 요즘은 크로스핏에 빠지셨다고 들었어요.Jason: 네, 가능하면 매일 가려고 노력하고 있어요. 저 혼자 하는 건 아니고 서너 명이 함께 다니는데요, 다른 분들도 같이 하자고 유혹 중입니다. 힘든 운동 끝판왕이긴 한데 하고나면 생물학적으로 살아있다는 느낌이 든달까, 몸에 활력이 막 생겨나요.ZOYI: 외부 미팅이 많은 직업이라 굉장히 바쁘실텐데, 운동 다닐 시간이 있으신가요?Jason: 네 가능합니다. 조이에서는 자신의 일만 제대로 하면 시간을 자율적으로 이용할 수 있어요. 고객사와 잘 맞추면 스케줄도 원하는대로 짤 수 있어요. 눈치 볼 필요 없이 운동 다녀와 또 열심히 일하곤 합니다.열심히 일한 자에게 자유가 주어질 지어다!!ZOYI: 제이슨의 건강한 삶을 응원합니다! 마지막으로 한마디 해주세요.Jason: 앞서 말했듯이 소비자 행동 데이터를 보고 매장 운영을 하면 효율성 측면에서 분명 더 나은 결과를 만들어요. 그게 지금 보기에는 미세한 차이더라도 시간이 지날수록 다른 '클래스'를 만들게 될 거예요.그런 부분에서 볼 때 워크인사이트는 점차 매장을 운영하는 기업들에게 차별화된 클래스를 만들어주는 제품이 될 것이라고 생각합니다. 갈수록 고객사에서 갈증을 느끼고 먼저 찾는 기술이 될 거예요. 꾸준한 성장을 위해 앞으로도 지치지 않고 최고의 가치를 드리는 데 최선을 다하겠습니다!#조이코퍼레이션 #팀원소개 #팀원인터뷰 #기업문화 #조직문화 #업무환경
조회수 1128

좋은 콘텐츠가 뭐냐고 물으신다면

초보 크리에이터의 고민“아무도 안 볼까 봐 두려워요.” “나 빼고 다 잘할 거 같아요.” “꾸미는 실력이 없어요.” “소재는 정했는데 누가 이미 하고 있어요.” 블로그나 포스트를 시작하기 전, 누구나 한 번쯤 하는 고민입니다. 네이버 포스트에서 회사생활을 주제로 100회 넘게 연재한 제가 가장 많이 받는 질문이기도 하지요. 누군가는 고민을 빨리 해결하고 싶은 마음에 바이럴, 마케팅 법칙들을 찾아보지만, 그런 법칙들이 마법처럼 모든 것을 해결해주지는 못할 거예요. 과연 나의 콘텐츠는 누가 ‘꾸준히’ 봐줄까요? 정답은 “아무도 안 본다”입니다. 너무나 당연한 사실인 걸 알면서도 실망하기는 매한가지! 누군가는 이 대답을 듣고 ‘실망했다’고 말할 정도였어요. 하지만 사실은 사실입니다. A블로그에는 레고에 관한 글이 100개가 있고, B블로그에는 딱 1개만 있다면 우리는 어떤 블로그가 더 보고 싶어질까요? 당연히 A블로그가 더 끌릴 겁니다. 이제 막 블로그를 시작했는데 사람들의 높은 관심을 기대한다면 그것은 욕심일 뿐이지요. “그럼 어떻게 하면 볼까요?” 이번 글의 화두입니다. 정성스럽게 만든 콘텐츠를 많은 사람들이 보고 즐긴다면 만든 크리에이터도, 보는 독자도 즐거울 겁니다. 이 고민을 해결하기 위해 우선 사람들이 많이 보는 콘텐츠들을 유심히 관찰해봤습니다. 잘 나가는 콘텐츠의 비밀콘텐츠를 예쁘게 꾸미는 기술, 찰진 유머, 많은 양의 게시글말고도 사람들이 즐겨보는 콘텐츠에는 분명 어떤 공통점이 있었습니다. 크게 네 가지를 찾았는데요. 등장인물사건줄거리메시지 1)등장인물화자 또는 콘텐츠를 제작한 크리에이터 이외에 누군가가 등장합니다. 화자 혼자 얘기하는 게 아니라 다른 누군가에게도 말할 기회를 준다는 것입니다. 즉 혼자만 주인공이 되어 독백하지 않고, 일상에서 다른 사람들과의 밀접한 관계를 통해 콘텐츠를 이어나간다는 의미입니다.2)사건다른 인물이 등장해서 화자와 함께 멀뚱멀뚱 서 있을까요? 아니죠. 여러 사건들을 만들어 나갑니다. 여기에서 말하는 ‘사건’은 그것이 알고싶다에서 나올 법한 무시무시한 일들이 아닙니다. 요리도 하고, 연애도 하고, 이별도 하고, 싸우기도 하는 사람과 사람 사이의 모든 일들을 말합니다.3)줄거리몇 개의 사건이 합쳐지면 하나의 줄거리(이야기)가 완성됩니다. 간단한 예로, 우리는 자려고 누운 침대에서 ‘아, 오늘은 ~한 하루였어’하며 그날의 기억들을 되새깁니다. 풀리지 않는 일들이 겹겹이 쌓였다면 그날은 ‘꼬인 날’이라고 표현하고요. 여기저기서 많은 축하를 받는 생일엔 즐거운 하루였다고 할 겁니다. 콘텐츠도 마찬가지입니다. 빵빵 터지는 블록버스터급 이야기들만 소재가 되진 않습니다. 순간 순간의 사건들이 합쳐져 하나의 콘텐츠가 만들어지는 것이죠. 4)메시지신기한 일은 줄거리가 여러 개로 구성되면 줄거리의 합은 독자에게 어떤 메시지를 전한다는 것입니다. 네이버 포스트에서 연재하고 있는 그 남자의 사회생활도 마찬가지입니다. 그저 사회 초년생의 좌충우돌 회사 생활을 그리고 있을 뿐인데, 독자들은 저를 ‘회사생활에 긍정적이고 도전적인 인물’로 평가합니다. “나는 회사생활을 즐기는 사원이야!“라고 말한 적이 단 한 번도 없는데 말입니다. 나영석 PD의 프로그램들/tvN 제공우리가 흔히 알고 있는 좋은 콘텐츠에는 반드시 네 가지의 공통점을 갖고 있습니다. 방송계의 절대지존, 나영석 PD의 프로그램만 봐도 그렇습니다. 사실 그의 프로그램은 단순합니다. 신서유기는 중국에서 노는 거고, 윤식당은 음식 파는 거고, 삼시세끼는 일어나서 잘 때까지 밥해 먹는 겁니다. 단순한 걸 넘어서서 ‘이게 될까?’ 싶을 정도의 소재입니다. 하지만 이 프로그램엔 다양한 인물들이 등장해 사건을 만들면서 이야기를 주도하고, 심지어 어떤 메시지를 시청자에게 선사합니다. 나영석 PD는 평범한 일상을 훌륭한 콘텐츠로 만들고 있습니다. 그가 사람들로부터 ‘천재’라는 말을 듣는 이유이기도 하지요. 아무리 사소한 소재여도 훌륭한 콘텐츠로 발돋움할 수 있게 도와주는 네 가지의 공통점! 이것을 우리는 ‘스토리텔링의 네 가지 요소’라고 말합니다. 누구나 할 수 있는 스토리텔링앞서 살펴봤듯이 스토리텔링은 꼭 복잡하거나, 거창하지 않아도 됩니다. 게다가 누구나 할 수 있고, 또 지금 당장이라도 도전할 수 있지요. ‘지금 당장’이라고 표현하니 고개를 갸우뚱할지도 모르겠습니다. 예전에는 하나의 이야기로 몇 천년을 우려먹었습니다. 단군신화가 대표적입니다. 사람이 된 웅녀를 응원하고, 환웅과 웅녀 사이에 태어난 단군을 신성하게 여겼습니다. 그렇다면 동굴 밖을 나간 호랑이는 어떻게 됐을까요? 그리고 만약 웅녀가 등장하지 않았다면 어떻게 됐을까요?단군신화는 그 누구도 의문을 제기하지 않았지만 현대는 다릅니다. 안방에서 드라마 도깨비를 보면서 시청자들은 중얼거리죠.“헐, 지은탁이 죽었어!”“삼신할매가 좀 도와주지ㅠㅠ”“이럴 거면 차라리 김신이랑 저승사자랑 사귀어라!”맘에 들지 않는 순간 시청자의 머릿속에 퍼뜩 떠오르는 생각 하나가 있습니다. “내가 해볼까?”서사 상태의 살촉 구조. 이야기의 전개에 따라 다양한 잠재성이 결말로 나타날 수 있다.그리고 이 생각을 행동에 옮기면 팬픽 소설부터 시작해 패러디 웹툰, 개그 콩트, 웹드라마 등등 다양한 이야기 형태가 나타나기 시작하죠. 김은숙 작가가 만든 드라마는 하나의 서사구조에 불과합니다. 독자들이 ‘작가’가 되면서 다양한 이야기를 뽐내기 때문입니다.'신과 함께'의 살촉 구조작가 혼자서 여러 형태의 이야기를 만들 수도 있습니다. 웹툰 신과 함께는 저승 편이 처음 선을 보였습니다. 하지만 그 이후엔 신화편, 이승 편이 나오면서 저승 편에서 주목받지 않았던 인물들을 다시 강조하기도 합니다. 심지어 이야기의 한 대목만을 각색해 뮤지컬로도 만들고, 주인공 김자홍의 직업을 바꿔 영화로도 만듭니다. 이야기의 형태가 바뀌니 다양한 결말을 만날 수도 있고요. 바흐찐의 재강조 이론러시아의 문학평론가 바흐찐(Mikhail Bakhtin)은 이런 구조를 ‘재강조 이론’이라고 말합니다. 하나의 이야기여도 어느 부분, 인물을 강조하는지에 따라 이야기의 서사가 달라지는 것을 말합니다.왜 이렇게 되었을까요? 하나의 이야기가 다양한 형태로 바뀔 수 있었던 결정적인 이유는 크게 두 가지가 있습니다. 첫 번째는 창작 주체가 변화했기 때문입니다. 이제는 드라마 <도깨비>를 보고 누구나 독자에서 작가가 될 수 있는 시대입니다. 마음에 안 들면 직접 두 팔을 걷어붙이고 이야기를 직접 꾸며나갈 수 있습니다. 두 번째는 창작 도구가 변화했기 때문입니다. 계산 기능을 수행하려고 개발된 컴퓨터는 오늘날 창작 도구(Creative Instrument)가 되었습니다. 사용자는 컴퓨터 하나로 글도 쓰고, 사진을 보정하고, 영상도 편집할 수 있습니다.창작 주체와 도구가 변화했기 때문에 이런 콘텐츠도 나올 수 있지요.잘 전하는 것도 중요하다콘텐츠는 크리에이터가 만들지만, 콘텐츠를 완성하는 건 바로 독자입니다. 독자가 읽어주지 않는 콘텐츠는 무용지물입니다. 그렇기 때문에 잘 만드는 만큼 잘 전하는 것도 중요합니다. ‘사랑한다’는 말 한마디도 전하는 방법은 무궁무진합니다. 만약 3번과 같은 이미지를 독자의 머릿속에 떠올리게 하려고 콘텐츠를 만들었는데 정작 독자는 아무 반응이 없다면 이는 분명 콘텐츠에 담긴 메시지를 잘못 전한 것이고, 전적으로 크리에이터의 실수입니다.현실과 콘텐츠 세상의 경계에 서 있는 우리크리에이터는 콘텐츠를 만들면서 자신만의 세상을 창조합니다. 이 세상에선 크리에이터의 생각이 곧 기준이고, 정의입니다. 독자는 항상 현실과 크리에이터가 만든 콘텐츠 세상의 경계에서 왔다 갔다 합니다. 무슨 말이냐고요? 우선 2016 리우 패럴림픽의 트레일러 영상을 먼저 감상해봅시다.영상을 보고 나면 장애를 가진 선수가 비장애 선수보다 훨씬 뛰어나 보일 겁니다. 인간의 오감 중 시각이 가장 인공적인 감각이란 것도 한몫했지만 귀에는 ‘Yes I Can’만 맴돌고 있으니 당신의 오감은 콘텐츠 하나 때문에 통제되고 있다는 건 과언이 아닙니다.“이건 전문가가 만든 거잖아요!”물론 빵빵한 예산과 고급 인력이 함께라면 두려울 게 없습니다. 그렇다면 예산 0원에 스마트폰으로 촬영한 콘텐츠는 어떨까요.복권을 긁는 주인공의 친구가 된 기분으로 영상을 보지 않았나요.(접니다…) 직접 긁는 복권도 아닌데 눈은 동그랗게 뜨게 되고 가슴도 콩닥콩닥 뛰었을 겁니다. 이 콘텐츠는 패럴림픽 트레일러 영상처럼 그 어떤 복잡한 기술을 쓴 것도 아니지만 보는 이의 시선과 마음을 사로잡는데에 성공했습니다. 재미있기 때문입니다.물론 콘텐츠를 만들 때 예산과 인력의 투입 규모에 따라 퀄리티가 달라지는 건 사실입니다. 그러나 복권 긁는 영상에서 알 수 있듯이 ‘많은 예산=재미있는 콘텐츠’는 절대 법칙이 아닙니다. 재미있는 콘텐츠는 보는 이의 공감각을 충분히 통제시켰는지에 따라 판가름됩니다. 쉽게 말하면 ‘현실을 잊게할 만큼 집중시켰는가’입니다. (‘크리에이터의 콘텐츠 세상에 독자가 들어왔는가!’라고도 말할 수 있겠네요.) 다시 돌아온 공감각의 시대공감각의 시대가 ‘다시’ 돌아왔다는 건 옛날에도 그런 시절이 있었다는 얘기입니다. 그렇습니다. 원시 시대, 우리의 먼 조상들은 사냥에서 멧돼지를 잡은 사람의 무용담을 듣고, 알통을 만져봤습니다. 또 멧돼지의 피비린내를 맡고, 맛보기도 했죠. 그야말로 인간의 모든 감각을 유기적으로 동원하는 공감각적인 세상이었습니다. 누구나 참여하고, 감정적인 경험을 했습니다. 그러나 ‘말’이라는 건 연기처럼 금방 사라집니다. 아무리 기억력이 좋아도 했던 말을 똑같이 기억할 수는 없습니다. 사람들은 사라지는 말을 잡기 위해 동굴 안에 그림을 그리고, 비석에 이름을 새기고, 종이에 글을 썼습니다. 모두 ‘보는 것’이었기 때문에 주로 시각에 의존했던 시절이었던 거죠. 디지털문화시대, 공감각이 다시 돌아오다이제, 디지털입니다. 아무리 재미있는 이야기를 콘텐츠로 만들어도 보는 이의 감각들을 집중시키지 못한다면 기억되지 못할 겁니다. AR, VR처럼 거창한(=돈 많이 드는) 콘텐츠가 아니어도 좋습니다. 복권 긁는 영상을 만든다면 보는 이도 가슴이 콩닥콩닥 뛰도록, 소리치는 장면을 묘사한다면 보는 이의 귀에 외침이 들리도록 해보세요. 콘텐츠 안에 담긴 메시지를 큐피트가 화살 날리듯 읽는 이의 마음에 팍! 팍! 꽂아보는 겁니다. 결론입니다. 스토리텔링은 잘 만드는 것도 중요하지만 독자에게 잘 전달하는 것도 매우 중요하다는 사실을 알았습니다. 하지만 이 두 가지를 동시에 잘하려면 바로 ‘아이디어’가 있어야 하는데요. 아이디어는 뻔한 것에서 탈출하는 것에서부터 시작합니다.아이디어를 얻는 방법1) +1 뻔한 것, 익숙한 것, 편한 것에서 딱 하나만 더해보세요. 회사생활을 다루는 콘텐츠는 많지만 제가 연재하는 포스트는 똑같은 주제로 레고를 이용해 연재합니다. 분명 소재는 똑같지만 하나가 더해졌다는 이유만으로 더 특별해 보입니다. 마찬가지로 GD&TOP 1집 앨범의 +1은 ‘색’입니다. 앨범 CD를 색깔별로 발매했기 때문에 팬들은 하나만 살 수는 없었을 겁니다. 이 앨범은 13만 장의 판매량을 기록해 2010년 음반 판매량 5위에 올랐습니다.2)Break 형식을 파괴하세요. SONY는 클럽에 입장하는 사람들에게 헤드폰을 나눠주고 Silent Party를 개최했습니다. 조용한 클럽이라…. 이상하지 않습니까? 조용한 미식가, 나이키의 밤에 하는 농구 등도 마찬가지, 당연하다는 것을 당연하지 않다고 생각하는 순간 재미있는 아이디어가 마구마구 떠오를 겁니다. 3)Life ‘등잔 밑이 어둡다’는 속담처럼 우리 주변에서 찾아봅시다. 저는 콘텐츠를 만들 때 무조건 주변의 사람, 사물에서 찾습니다. 과장님의 헛기침, 눈 깜빡거림, 웃음소리까지도 소재가 될 수 있기 때문입니다. 다시 강조하지만 소재는 대단한 것이 아닙니다. 사소한 것을 잡아내세요! 이제 우리의 콘텐츠를 우연히 마주친 독자를 구독자로 만들어 봅시다. 바이럴과 마케팅에 의존하지 않아도 사랑받는 콘텐츠를 만들고 싶다면 개성 있는 스토리텔링으로 승부합시다. 당신의 경험은 당신이 제일 잘 알고, 제일 잘 전달할 수 있으니까요. 참고김다영(2017). 디지털 시대, 소셜미디어 환경에서 디지털 콘텐츠 마케팅의 전략 제안. 유럽문화예술학논집, 제16집, 55-75Youtube, We’re The Superhumans: Rio Paralympics 2016 Trailer, Channel 4 (2018.01.18.)장근우, 「콘텐츠의 정석」, 예문아카이브(2017) 글장근우 대리 | People&Relations [email protected]브랜디, 오직 예쁜 옷만#브랜디 #기업문화 #조직문화 #업무환경 #인사이트 #경험공유 #콘텐츠
조회수 469

2021년의 테마 코로나19와 디지털 전환

안녕하세요 협업툴 플로우입니다. 전 세계 누구에게 물어봐도 2020년 가장 기억에 남는 키워드는 단연 코로나19가 아닐까 합니다. 2021년 1분기가 지난 지금까지도 코로나는 현재 진행형입니다. 익숙해져서 체감하지 못하고 있지만, 코로나19 이전과 이후의 삶이 많이 달라졌습니다. 식당에서는 QR 코드가 없이는 출입할 수 없게 되었고, 학교에서는 현장 교육보다 인터넷 강의를 더 선호하게 되었어요. 회사에서는 재택근무를 경험해보고, 화상회의를 통해 의사결정을 하게 되었습니다.마이크로소프트사의 최고 경영자 사타아 아델라는 “2년 걸릴 디지털 전환이 코로나19로 인해 2개월 만에 이루어졌다"라고 말했는데요. 일상 속에서도 일하는 환경 속에서도 많은 변화가 나타났습니다.왜 지금 디지털 전환인가?미국 하버드 경영 연구원의 연구결과에 의하면 디지털 전환을 이루어 낸 회사와 그렇지 못한 회사의 매출에는 큰 차이가 있었습니다. 3년간 평균 매출 성과를 비교해보니 약 20%의 차이가 발생했는데요. 단기적으로 보게 되면 20%의 차이가 얼마 되지 않지만, 복리효과를 생각하면 그 격차가 점점 벌어지게 됩니다. 한번 벌어진 기업의 격차는 다시 좁히기 힘들기 때문에 디지털 전환은 단순히 매출 경쟁이 아닌 기업의 생존에 필수 요소가 되었습니다.국내/해외 기업의 디지털 전환디지털 전환을 이루기 위해 국내/해외 할 것 없이 많은 기업에서 노력을 하고 있는데요. 아무리 보수적인 기업이라도 ‘업무 방식의 혁신과 디지털 전환’을 올해의 비전과 목표로 삼지 않은 곳을 찾기 어렵습니다.예상치 못한 코로나19 사태는 디지털 전환(디지털 트렌스포메이션)을 가속화하는 트리거가 되었는데요. 특히 대면으로 업무를 보는 사업장에는 발등에 불이 떨어졌습니다. 디지털 전환을 하기 위해 인재를 영입하기도 하고, 기업의 문화를 바꾸기도 합니다. 사내 메신저를 사용하거나 협업툴을 도입하는 기업들이 점차 증가하고 있습니다. 정부에서도 국내 기업들의 디지털 전환을 돕고자 "K-비대면 바우처 플랫폼" 사업을 진행하고 있죠.디지털 전환이 기업의 성장에 도움이 되나?HPE 아루바의 조사 결과에 따르면 디지털 전환이 업무 성과와 심리적인 만족도까지 높여준다는 연구결과가 있는데요. 디지털 전환이 직원들의 생산성과 동기부여, 직업 만족도, 행복감이 높아졌다는 놀라운 결과였습니다. 개인의 높은 만족도는 결국, 기업 성과로 이어졌음을 알 수 있습니다. 이제 디지털 전환은 피할 수 없는 기업들의 필수 과제가 되었습니다. 코로나가 잦아들면 다시 예전 방식으로 돌아가겠지라는 생각은 위험합니다. 지금 당장, 디지털 전환을 고려해야 할 시점입니다.협업툴 플로우 바로가기
조회수 1826

성장하는 PHP와 환대받지 못하는 개발자

https://kinsta.com/blog/php-7-2/ PHP v7.2 릴리즈최근(2017년 11월 30일)에 PHP  7.2 버전이 릴리즈 되었습니다.(다운로드 바로가기) PHP는 1995년에 만들어진 오래된 언어지만 여전히 많은 웹사이트들이 PHP로 만들어지고 있습니다. 특히 버전7로 넘어오면서 퍼포먼스가 비약적으로 좋아졌다는 평을 듣고 있습니다. 이번 7.2 버전에서는 아래와 같이 보안성강화와 프로그래밍 기능 향상을 제공하고 있습니다. (개선목록 바로가기)PHP 7.2.0 comes with numerous improvements and new features such as  Convert numeric keys in object/array castsCounting of non-countable objectsObject typehintHashContext as ObjectArgon2 in password hashImprove TLS constants to sane valuesMcrypt extension removedNew sodium extensionPHP로 만들어진 많은 사이트2017년 GitHub 통계를 보면 PHP는 GitHub에서 사용되는 337개의 언어들중에서 Top 5에 들어가는 매우 대중적인 언어입니다.https://octoverse.github.com/ WordPress, Drupal, Zoomla 와 같은 웹 기반의 오픈소스 컨텐츠 관리 시스템은 모두 PHP로 만들어 졌습니다. 그리고테크크런치(TechCrunch), 펩시 리프레시(Pepsi Refresh), 코메디닷컴(Comedy.com) 같은 기업들은 WordPress로 만들어진 사이트를 적극 활용하고 있기도 합니다. 다만 아쉬운 점은 아직도 5버전을 사용하여 개발한 사이트들이 많이 있다는 점입니다.https://kinsta.com/blog/php-7-2/환대받지 못하는 PHP 개발자PHP는 탁월한 접근성으로 인해 생각지도 못한 문제가 발생합니다. PHP가 누구나 사용할 수 있을 정도로 쉬운 구조이다보니 우리나라의 갑-을-병-정 으로 내려가는 SI 구조에서 저렴한 인력으로 구분되기 시작합니다. PHP 고급 개발자가 고급 대우를 못받게 되는 상황이 발생하는 것입니다. 또한 엔터프라이즈 개발에서 제외되다 보니 PHP 개발자는 점점 대규모 시스템 설계 경험이 적어지고 결국 중소규모의 서비스 개발에만 참여하게 되었습니다. 하지만 PHP도 충분히 대규모 서비스 개발이 가능한 언어이며 PHP The Right Way 와 같이 PHP를 잘 사용할 수 있는 방법들을 정리한 사이트를 보면 PHP의 저력을 확인할 수 있습니다.PHP 개발자를 위한 서비스 관리 도구PHP 개발에 있어서 아쉬운 부분이 있다면 개발 이후 운영에 관련된 부분입니다. 많은 국내 PHP 사이트들이 개발 이후 성능 분석이 되지 않은 상태에서 운영되고 있습니다. Java로 만들어진 엔터프라이즈 서비스들은 오픈 시점과 운영 과정에서많은 노력을 들여서 서비스 최적화 작업을 진행하는데 반해서, PHP로 개발된 서비스들은 사용자가 많아지더라도 튜닝 작업을 진행하는 경우가 거의 없습니다. 아쉬운 점은 이로 인해 PHP의 성능이 떨어진다는 오해가 발생하기도 한다는 것입니다.일반적으로 평균 응답시간을 계산하여 서비스의 상태를 파악하기도 하지만 하루 1만명이 들어오는 사이트에 100명이 10초 이상의 응답시간을 경험하더라도 나머지 인원이 0.1초의 응답시간을 갖는다면 서비스의 평균 응답시간은 0.2초 이내로 나오게 됩니다. 이런 고객의 장애를 해결하기 위해서는 사용하는 성능 분석 서비스가 이전까지는 솔루션으로만 제공되었기 때문에 고가이며 설치도 어려웠지만 최근에 서비스로 제공되기 시작하면서 비용도 저렴해지고 설치도 매우 쉬워졌습니다. 해외에서는 몇 년전부터 많은 PHP 개발자들이 모니터링 서비스인 뉴렐릭(https://newrellic.com)이나 앱다이나믹스(https://appdynamics.com)의 서비스를 통해 PHP 분석/모니터링 서비스를 사용하고 있습니다. 이런 서비스들은 당연히 한국에서도 사용이 가능합니다.https://newrelic.com/php국내 모니터링 서비스 중에서는 와탭(https://whatap.io)이 최근 PHP를 지원하고 있습니다. 어플리케이션의 성능을 분석하고 튜닝한 사이트와 안한 사이트의 성능 차이가 날수 있기 때문에 PHP로 만들어진 서비스의 운영 및 업데이트 작업을 진행하는 개발자 분들은 뉴렐릭이나 앱다이나믹스 또는 와탭을 사용하여 운영중인 서비스의 성능을 확인해 보시길 권하고 싶습니다. 대부분의 PHP 성능 모니터링 서비스는 트라이얼 기간을 제공해 주기 때문에 일정기간 무료로 서비스 사용이 가능합니다. 몇일간 성능을 분석하고 모니터링 한다면 서비스 운영 방식에 대한 인사이트도 얻을 수 있습니다. https://coderseye.com/best-php-frameworks-for-web-developers/PHP 성능 모니터링 서비스로 할수 있는 것들PHP 성능 모니터링 서비스는 정확히 표현하면 고객의 트랜잭션을 추적하는 서비스입니다. 서비스를 사용하는 모든 고객의 트랜잭션을 추적하여 서비스의 성능을 알아내는 방식입니다. 이런 어플리케이션 성능 모니터링 서비스는 대규모 서비스를 체계적으로 운영하는 위한 필수 도구입니다. 최근 서비스 형태로 제공되는 성능 모니터링 서비스들은 기존 운영자 위주의 기능에서 벗어나서 개발자와 운영자가 함께 참여하는 DevOps 환경에 맞는 기능을 제공하고 있습니다. 서비스를 운영하는 과정에서 응답시간의 상황을 실시간으로 확인할 수 있으며 문제가 발생한 쿼리를 빠르게 찾을 수 있도록 도와줍니다. 트랜젝션의 에러도 당연히 알수 있으며 문제가 발생한 메소드도 알수 있습니다. 코드상의 서비스 구조뿐만 아니라 실제 트랜잭션의 흐름을 알수 있기 때문에 서비스의 동작 구조도 함께 공유해가며 서비스를 발전시킬 수 있도록 도와줍니다. 결론PHP는 정말 빠르게 발전하고 있는 언어중에 하나입니다. 우리가 정보를 주고 받는 많은 서비스들이 PHP로 만들어 지고 있으며 언어의 구조도 모던하게 변화하고 있습니다. 특히 빠르게 변화하는 스타트업에서 사랑받는 언어이며 세계적으로도 많은 이들의 사랑을 받고 있는 언어입니다. 한편 PHP는 소규모에서만 적용한다는 인식과 함께 PHP로 시작했음에도 규모가 커지면서 서비스를 Java로 변경하는 경우에는 아쉬움이 남습니다. 하지만 PHP가 지속적으로 발전하고 있고 더 좋은 방향으로 나아가는 과정에서 더 좋은 PHP 개발자들이 나오기 시작할 거라 생각합니다. 그리고 뉴렐릭(https://newrelic.com)이나 앱다이나믹스(https://appdynamics.com) 아니면 와탭(https://whatap.io)과 같은 성능 분석 도구를 사용하여 PHP로 만든 서비스의 효율을 높이고 운영 관리를 체계화해 나간다면 국내에서도 페이스북과 같이 PHP로 개발하여 대규모로 서비스볼수 있을거라 생각합니다. http://php.net/archive/2017.php#와탭랩스 #개발자 #개발팀 #인사이트 #경험공유 #일지 #PHP
조회수 1074

안드로이드 클라이언트 Reflection 극복기 - VCNC Engineering Blog

 비트윈 팀은 비트윈 안드로이드 클라이언트(이하 안드로이드 클라이언트)를 가볍고 반응성 좋은 애플리케이션으로 만들기 위해 노력하고 있습니다. 이 글에서는 간결하고 유지보수하기 쉬운 코드를 작성하기 위해 Reflection을 사용했었고 그로 인해 성능 이슈가 발생했던 것을 소개합니다. 또한 그 과정에서 발생한 Reflection 성능저하를 해결하기 위해 시도했던 여러 방법을 공유하도록 하겠습니다.다양한 형태의 데이터Java를 이용해 서비스를 개발하는 경우 POJO로 서비스에 필요한 다양한 모델 클래스들을 만들어 사용하곤 합니다. 안드로이드 클라이언트 역시 모델을 클래스 정의해 사용하고 있습니다. 하지만 서비스 내에서 데이터는 정의된 클래스 이외에도 다양한 형태로 존재합니다. 안드로이드 클라이언트에서 하나의 데이터는 아래와 같은 형태로 존재합니다.JSON: 비트윈 서비스에서 HTTP API는 JSON 형태로 요청과 응답을 주고 받고 있습니다.Thrift: TCP를 이용한 채팅 API는 Thrift를 이용하여 프로토콜을 정의해 서버와 통신을 합니다.ContentValues: 안드로이드에서는 Database 에 데이터를 저장할 때, 해당 정보는 ContentValues 형태로 변환돼야 합니다.Cursor: Database에 저장된 정보는 Cursor 형태로 접근가능 합니다.POJO: 변수와 Getter/Setter로 구성된 클래스 입니다. 비지니스 로직에서 사용됩니다.코드 전반에서 다양한 형태의 데이터가 주는 혼란을 줄이기 위해 항상 POJO로 변환한 뒤 코드를 작성하기로 했습니다.다양한 데이터를 어떻게 상호 변환할 것 인가?JSON 같은 경우는 Parsing 후 Object로 변환해 주는 라이브러리(Gson, Jackson JSON)가 존재하지만 다른 형태(Thrift, Cursor..)들은 만족스러운 라이브러리가 존재하지 않았습니다. 그렇다고 모든 형태에 대해 변환하는 코드를 직접 작성하면 필요한 경우 아래와 같은 코드를 매번 작성해줘야 합니다. 이와 같이 작성하는 경우 Cursor에서 원하는 데이터를 일일이 가져와야 합니다.@Override public void bindView(View view, Context context, Cursor cursor) { final ViewHolder holder = getViewHolder(view); final String author = cursor.getString("author"); final String content = cursor.getString("content"); final Long timeMills = cursor.getLong("time"); final ReadStatus readStatus = ReadStatus.fromValue(cursor.getString("readStatus")); final CAttachment attachment = JSONUtils.parseAttachment(cursor.getLong("createdTime")); holder.authorTextView.setText(author); holder.contentTextView.setText(content); holder.readStatusView.setReadStatus(readStatus); ... } 하지만 각 형태의 필드명(Key)이 서로 같도록 맞춰주면 각각의 Getter와 Setter를 호출해 형태를 변환해주는 Utility Class를 제작할 수 있습니다.@Override public void bindView(View view, Context context, Cursor cursor) { final ViewHolder holder = getViewHolder(view); Message message = ReflectionUtils.fromCursor(cursor, Message.class); holder.authorTextView.setText(message.getAuthor()); holder.contentTextView.setText(message.getContent()); holder.readStatusView.setReadStatus(message.getReadStatus()); ... } 이런 식으로 코드를 작성하면 이해하기 쉽고, 모델이 변경되는 경우에도 유지보수가 비교적 편하다는 장점이 있습니다. 따라서 필요한 데이터를 POJO로 작성하고 다양한 형태의 데이터를 POJO로 변환하기로 했습니다. 서버로부터 받은 JSON 혹은 Thrift객체는 자동으로 POJO로 변환되고 POJO는 다시 ContentValues 형태로 DB에 저장됩니다. DB에 있는 데이터를 화면에 보여줄때는 Cursor로부터 데이터를 가져와서 POJO로 변환 후 적절한 가공을 하여 View에 보여주게 됩니다.POJO 형태로 여러 데이터 변환필요Reflection 사용과 성능저하처음에는 Reflection을 이용해 여러 데이터를 POJO로 만들거나 POJO를 다른 형태로 변환하도록 구현했습니다. 대상 Class의 newInstance/getMethod/invoke 함수를 이용해 객체 인스턴스를 생성하고 Getter/Setter를 호출하여 값을 세팅하거나 가져오도록 했습니다. 앞서 설명한 ReflectionUtils.fromCursor(cursor, Message.class)를 예를 들면 아래와 같습니다.public T fromCursor(Cursor cursor, Class clazz) { T instance = (T) clazz.newInstance(); for (int i=0; i Reflection을 이용하면 동적으로 Class의 정보(필드, 메서드)를 조회하고 호출할 수 있기 때문에 코드를 손쉽게 작성할 수 있습니다. 하지만 Reflection은 튜토리얼 문서에서 설명된 것처럼 성능저하 문제가 있습니다. 한두 번의 Relfection 호출로 인한 성능저하는 무시할 수 있다고 해도, 필드가 많거나 필드로 Collection을 가진 클래스의 경우에는 수십 번이 넘는 Reflection이 호출될 수 있습니다. 실제로 이 때문에 안드로이드 클라이언트에서 종종 반응성이 떨어지는 경우가 발생했습니다. 특히 CursorAdapter에서 Cursor를 POJO로 변환하는 코드 때문에 ListView에서의 스크롤이 버벅이기도 했습니다. Bytecode 생성 Reflection 성능저하를 해결하려고 처음으로 선택한 방식은 Bytecode 생성입니다. Google Guice 등의 다양한 자바 프로젝트에서도 Bytecode를 생성하는 방식으로 성능 문제를 해결합니다. 다만 안드로이드의 Dalvik VM의 경우 일반적인 JVM의 Bytecode와는 스펙이 다릅니다. 이 때문에 기존의 자바 프로젝트에서 Bytecode 생성에 사용되는 CGLib 같은 라이브러리 대신 Dexmaker를 이용하여야 했습니다. CGLib CGLib는 Bytecode를 직접 생성하는 대신 FastClass, FastMethod 등 펀리한 클래스를 이용할 수 있습니다. FastClass나 FastMethod를 이용하면 내부적으로 알맞게 Bytecode를 만들거나 이미 생성된 Bytecode를 이용해 비교적 빠른 속도로 객체를 만들거나 함수를 호출 할 수 있습니다. public T create() { return (T) fastClazz.newInstance(); } public Object get(Object target) { result = fastMethod.invoke(target, (Object[]) null); } public void set(Object target, Object value) { Object[] params = { value }; fastMethod.invoke(target, params); }  Dexmaker 하지만 Dexmaker는 Bytecode 생성 자체에 초점이 맞춰진 라이브러리라서 FastClass나 FastMethod 같은 편리한 클래스가 존재하지 않습니다. 결국, 다음과 같이 Bytecode 생성하는 코드를 직접 한땀 한땀 작성해야 합니다. public DexMethod generateClasses(Class<?> clazz, String clazzName){ dexMaker.declare(declaringType, ..., Modifier.PUBLIC, TypeId.OBJECT, ...); TypeId<?> targetClassTypeId = TypeId.get(clazz); MethodId invokeId = declaringType.getMethod(TypeId.OBJECT, "invoke", TypeId.OBJECT, TypeId.OBJECT); Code code = dexMaker.declare(invokeId, Modifier.PUBLIC); if (isGetter == true) { Local<Object> insertedInstance = code.getParameter(0, TypeId.OBJECT); Local instance = code.newLocal(targetClassTypeId); Local returnValue = code.newLocal(TypeId.get(method.getReturnType())); Local value = code.newLocal(TypeId.OBJECT); code.cast(instance, insertedInstance); MethodId executeId = ... code.invokeVirtual(executeId, returnValue, instance); code.cast(value, returnValue); code.returnValue(value); } else { ... } // constructor Code constructor = dexMaker.declare(declaringType.getConstructor(), Modifier.PUBLIC); Local<?> thisRef = constructor.getThis(declaringType); constructor.invokeDirect(TypeId.OBJECT.getConstructor(), null, thisRef); constructor.returnVoid(); }  Dexmaker를 이용한 방식을 구현하여 동작까지 확인했으나, 다음과 같은 이유로 실제 적용은 하지 못했습니다. Bytecode를 메모리에 저장하는 경우, 프로세스가 종료된 이후 실행 시 Bytecode를 다시 생성해 애플리케이션의 처음 실행성능이 떨어진다.Bytecode를 스토리지에 저장하는 경우, 원본 클래스가 변경됐는지를 매번 검사하거나 업데이트마다 해당 스토리지를 지워야 한다.더 좋은 방법이 생각났다. Annotation Processor 최종적으로 저희가 선택한 방식은 컴파일 시점에 형태변환 코드를 자동으로 생성하는 것입니다. Reflection으로 접근하지 않아 속도도 빠르고, Java코드가 미리 작성돼 관리하기도 편하기 때문입니다. POJO 클래스에 알맞은 Annotation을 달아두고, APT를 이용해 Annotation이 달린 모델 클래스에 대해 형태변환 코드를 자동으로 생성했습니다. 형태 변환이 필요한 클래스에 Annotation(@GenerateAccessor)을 표시합니다. @GenerateAccessor public class Message { private Integer id; private String content; public Integer getId() { return id; } ... }  javac에서 APT 사용 옵션과 Processor를 지정합니다. 그러면 Annotation이 표시된 클래스에 대해 Processor의 작업이 수행됩니다. Processor에서 코드를 생성할 때에는 StringBuilder 등으로 실제 코드를 일일이 작성하는 것이 아니라 Velocity라는 template 라이브러리를 이용합니다. Processor는 아래와 같은 소스코드를 생성합니다. public class Message$$Accessor implements Accessor { public kr.co.vcnc.binding.performance.Message create() { return new kr.co.vcnc.binding.performance.Message(); } public Object get(Object target, String fieldName) throws IllegalArgumentException { kr.co.vcnc.binding.performance.Message source = (kr.co.vcnc.binding.performance.Message) target; switch(fieldName.hashCode()) { case 3355: { return source.getId(); } case -1724546052: { return source.getContent(); } ... default: throw new IllegalArgumentException(...); } } public void set(Object target, String fieldName, Object value) throws IllegalArgumentException { kr.co.vcnc.binding.performance.Message source = (kr.co.vcnc.binding.performance.Message) target; switch(fieldName.hashCode()) { case 3355: { source.setId( (java.lang.Integer) value); return; } case -1724546052: { source.setContent( (java.lang.String) value); return; } ... default: throw new IllegalArgumentException(...); } } }  여기서 저희가 정의한 Accessor는 객체를 만들거나 특정 필드의 값을 가져오거나 세팅하는 인터페이스로, 객체의 형태를 변환할 때 이용됩니다. get,set 메서드는 필드 이름의 hashCode 값을 이용해 해당하는 getter,setter를 호출합니다. hashCode를 이용해 switch-case문을 사용한 이유는 Map을 이용하는 것보다 성능상 이득이 있기 때문입니다. 단순 메모리 접근이 Java에서 제공하는 HashMap과 같은 자료구조 사용보다 훨씬 빠릅니다. APT를 이용해 변환코드를 자동으로 생성하면 여러 장점이 있습니다. Reflection을 사용하지 않고 Method를 직접 수행해서 빠르다.Bytecode 생성과 달리 애플리케이션 처음 실행될 때 코드 생성이 필요 없고 만들어진 코드가 APK에 포함된다.Compile 시점에 코드가 생성돼서 Model 변화가 바로 반영된다. APT를 이용한 Code생성으로 Reflection 속도저하를 해결할 수 있습니다. 이 방식은 애플리케이션 반응성이 중요하고 상대적으로 Reflection 속도저하가 큰 안드로이드 라이브러리에서 최근 많이 사용하고 있습니다. (AndroidAnnotations, ButterKnife, Dagger) 성능 비교 다음은 Reflection, Dexmaker, Code Generating(APT)를 이용해 JSONObject를 Object로 변환하는 작업을 50번 수행한 결과입니다.성능 비교 결과 이처럼 최신 OS 버전일수록 Reflection의 성능저하가 다른 방법에 비해 상대적으로 더 큽니다. 반대로 Dexmaker의 생성 속도는 빨라져 APT 방식과의 성능격차는 점점 작아집니다. 하지만 역시 APT를 통한 Code 생성이 모든 환경에서 가장 좋은 성능을 보입니다. 마치며 서비스 모델을 반복적으로 정의하지 않으면서 변환하는 방법을 알아봤습니다. 그 과정에서 Reflection 의 속도저하, Dexmaker 의 단점도 설명해 드렸고 결국 APT가 좋은 해결책이라고 판단했습니다. 저희는 이 글에서 설명해 드린 방식을 추상화해 Binding이라는 라이브러리를 만들어 사용하고 있습니다. Binding은 POJO를 다양한 JSON, Cursor, ContentValues등 다양한 형태로 변환해주는 라이브러리입니다. 뛰어난 확장성으로 다양한 형태의 데이터로 변경하는 플러그인을 만들어서 사용할 수 있습니다. Message message = Bindings.for(Message.class).bind().from(AndroidSources.cursor(cursor)); Message message = Bindings.for(Message.class).bind().from(JSONSources.jsonString(jsonString)); String jsonString = Bindings.for(Message.class).bind(message).to(JSONTargets.jsonString());  위와 같이 Java상에 존재할 수 있는 다양한 타입의 객체에 대해 일종의 데이터 Binding 기능을 수행합니다. Binding 라이브러리도 기회가 되면 소개해드리겠습니다. 윗글에서 궁금하신 점이 있으시거나 잘못된 부분이 있으면 답글을 달아주시기 바랍니다. 감사합니다. 
조회수 1086

[인스팅터스] 이브의 성장운영팀을 소개합니다(with 매니저 E )

안녕하세요 :) EVE의 성장운영팀(Growth and Operation) 매니저 E입니다. 사회적 가치 증대, 고객만족, 채용 등의 업무를 맡아 진행중입니다.Q. 성장운영팀이란 ? '성장운영팀'이라고 하면 어떤 팀인지 잘 와닿지 않으실 텐데요, 조직의 '성장'과 '운영'을 맡고 있는 팀이라고 생각하시면 됩니다. 즉 성장운영팀은 임직원이 추구하는 사회적 가치가 실현될 수 있도록 임직원의 성장을 주도하고, 조직을 운영 및 관리하는 팀이죠. 성장운영팀에서는 제가 맡고 있는 업무 외에도 인사 업무, 세무회계 업무, 신사업 업무, 구매 업무 등 다양한 업무를 진행하고 있습니다. 업무들을 나열하고 보니 업무의 범위가 크게 느껴져 좀 무시무시하다는 생각도 들지만, 정해진 일을 고정적으로 수행하기 보다는 업무를 담당하고 있는 담당자가 주도적으로 업무를 체계화하고 확장해 나갑니다. 담당 업무에 대해 자율적으로 진척시켜 나갈 수 있는 권한과 책임이 주어지기 때문에 저 스스로가 성장하고 싶다는 동기 부여가 되는 거 같아요.  업무가 크게 겹치지 않아 팀 내부적으로 협업하는 작업이 많지 않지만 서로의 처지를 이해하는 팀원들의 끈끈한 정이 저희 팀의 특징이기도 해요. 매주 있는 팀 회의를 통해 자신이 맡고 있는 업무들을 공유함으로써 유기적으로 연결되고, 위기가 닥쳤을 때나 팀 목표를 설정했을 때 모두가 한 마음 한 뜻으로 헤쳐 나가고 있어요. 특히 워크샵이나 회식과 같은 사내 행사나 복지 제도에 대해서는 구성원 모두가 만족할 수 있도록 열렬히 의견을 낸답니다. 아마 성장운영팀 팀원 모두가 인스팅터스라는 조직 자체에 애정을 가지고 스스로 추구하는 가치가 명확한 사람들이기 때문인 거 같아요. Q. 성장운영팀에서 어떤 업무를 하나요 ? 저는 성장운영팀 안에서 고객만족 업무, 사회적 가치 증대 업무 등을 Chief로서 담당하고 인사 업무 중 채용 업무를 Deputy로 담당하고 있어요. 고객만족 업무에서는 고객응대, 고객지원, 그리고 CS 서비스 정책 등을 수립하는 것이 목표이고 사회적 가치 증대 업무에서는 B-Corporation 인증과 기부/후원 관리, 동물권 증진과 관련된 활동을 담당하고 있습니다. 채용 업무에서는 채용에 필요한 모든 실무를 담당합니다. 제가 담당하고 있는 업무의 성격이 완전히 다르기 때문에 스케줄 관리의 필요성을 많이 느끼는 편이에요. 벼락치기를 좋아하는 성격 덕분에 스케줄 관리에 실패한 경험이 있어 메일 발송, 공지사항 등록과 같은 작은 목표들을 만들어 단계적으로 완성해나갈 수 있도록 하고 있습니다. Q. EVE에 지원하게 된 계기는 무엇인가요 ? 학교를 다니면서 취업에 대한 걱정을 하지 않았다면 거짓말이겠지만, 인턴 경험이나 자격증 공부에 대한 생각이 들지 않을 정도로 여성주의 교지 편집위원회나 총여학생회 활동에 몰두했습니다. 학교를 다니는 것이 지긋지긋해져 졸업을 해야 겠다고 마음 먹었고 대책도 없이 졸업논문을 작성해 제출했습니다. 그치만 졸업할 당시에 어떤 직업을 갖겠다는 결정을 내리진 못하고 단지 동물권과 관련된 진로를 설정하고 싶다는 마음을 가졌던 기억이 납니다. 동물권 변호사가 되기 위해 로스쿨 진학이나 동물권과 관련된 시민 단체, 기업 등을 고려했으나, 여러 갈래의 길 속에서 이브에 입사 지원하는 길을 택했습니다.Q. 지원자에게 면접에 도움이 될 만한 TIP을 알려주세요 ! 면접 경험이 많지는 않지만 이브에서 본 면접은 질문부터 분위기, 면접관의 표정까지 기억에 남을 정도로 인상 깊었어요. 동물권이나 사람 간의 관계, 목표와 꿈 등 평소에 제가 고민했던 질문들이 면접장에서 나올 줄은 예상하지 못했기 때문이죠. 그래서 제가 드릴 수 있는 TIP은 평소에 지원자분이 가지고 있었던 고민들을 숨기지 마시고 면접이라는 찰나의 만남에서 솔직하게 말하라는 것입니다. 그 과정을 통해 이브와 지원자 간의 교집합을 찾고 이브라는 조직에 고민들이 녹아내려 사회적 가치로 실현될 수 있으면 좋겠습니다. Q. 평소 취미나 업무 외 일상은 ? 요즘 동글동글하고 노오란 스티커 붙이는 재미에 빠졌는데요, 회사에서 직원들끼리 하루에 한번 30분 이상 땀 흘리면서 운동을 하면 자신의 운동일지표에 스티커를 붙일 수 있는 귀여운 이벤트를 하고 있어요. 2019년 목표로 200번 운동하기를 말하고 나서 시작한 스티커 붙이기는 초창기 참여인원이 두 명에서 현재 다섯 명으로 늘어났답니다. 평일에는 2~3회 정도 운동을 하고, 2~3주에 한번씩 있는 사내 동물권 스터디에 함께 하고 있어요. 작년에는 매주 월요일마다 글쓰기 수업을 들었는데, 올해는 영어를 유창하게 말하는 것이 목표여서 영어 회화 학원을 등록할 예정입니다(넷플릭스에서 미드도 열심히 시청 중). 주말에는 친구들을 만나거나 두 멍뭉이들, 가족 그리고 조카님과 함께 시간을 보냅니다. Q. 내가 꿈꾸는 Career Path는 ?어떤 커리어를 쌓아야 겠다고 구체적인 로드맵을 그리진 않았지만 욕심내는 영역은 뚜렷하게 있습니다. 그 영역은 사회적 가치 증대 업무인데요, 스스로도 미흡하다고 생각하기에 더욱 욕심나는 영역인 것 같습니다. 자연, 평등, 건강이라는 가치가 쉽게 사소화되고 타협될 수 있는 이 구조적 모순 속에서 할 수 있는 것들을 하자라는 다짐, 그리고 개인의 가치와 사회 간의 괴리 속에서 지속가능한 의미들을 창출해내기 위해 노력하는 모습이 이브와 제가 닮아있는 지점 같습니다. Q. (정말 솔직하게) 회사의 장단점에 대해 말해주세요 ! 가끔 '이래도 될까?'와 같은 걱정을 할 때가 있습니다. 늘 관계에 대해 고민을 가지고 있다 보니 우리 안에서의 적당한 선은 무엇일까를 자주 생각하는데, 이브 안에서 맺은 관계들은 참고할 만한 레퍼런스가 없습니다. 미디어나 소설에서 보았던 고착화된 직장 동료 간의 관계나 경직된 상사-부하 간의 관계도 아니고, 그렇다고 친구 사이는 아니지만 편하고 즐겁고. 그래서 스스로 '내가 너무 눈치 없나' 생각하다가 '나는 나지 뭐' 이러다가도 '나 때문에 누군가 다치는 건 아닐까' 하는 생각에 빠지기도 합니다. 이 불투명한 경계 속에서 공존하는 존중과 침투가 이브의 장단점이지 않을까 합니다.evecondoms.com☘️생식 건강을 가장 먼저 생각하기에, 자연을 닮은 제품을 지향하기에, 소비자의 권리와 기업의 양심을 잃지 않기에 - 그래서 EVE는 성인용품이 아닌섹슈얼 헬스케어(Sexual healthcare) 브랜드입니다. 이브에 대해 더 알아보고 싶으시다면 지금 이브의 홈페이지에 방문해보세요:)

기업문화 엿볼 때, 더팀스

로그인

/