스토리 홈

인터뷰

피드

조회수 1761

레진 기술 블로그 - IntersectionObserver를 이용한 이미지 동적 로딩 기능 개선

구글 크롬 51 버전부터 DOM 엘리먼트의 노출 여부를 비동기로 처리하는 IntersectionObserver API를 사용할 수 있게 되었습니다. 이 기능을 이용하면 이미지의 동적 로딩이나 광고 배너의 노출 측정 등을 효율적으로 사용할 수 있다고 구글 개발자 블로그에서 소개하고 있습니다. 이 글에서는 기존의 이미지 동적 로딩에 대한 문제점을 짚어보고 여러 예제를 통해 IntersectionObserver의 사용 방법을 익혀 기존 기능을 개선할 수 있도록 합니다. 사용한 예제들은 브라우저의 호환성을 고려하지 않고 구글 크롬을 기준으로 작성하였습니다.기존의 이미지 동적 로딩 구현이미지의 개수가 많거나 용량이 큰 페이지를 불러올 경우 쓸데없는 네트워크 비용이 증가하고 이미지를 불러오는 과정에서 서비스 속도에 문제가 발생할 소지가 있어서 이미지가 사용자에게 보일 때만 불러오는 동적 로딩 기능이 필요합니다. IntersectionObserver를 소개하기 전에 먼저 기존 라이브러리들이 이미지 동적 로딩을 어떤 방법으로 구현하고 있는지 간단하게 알아보도록 합니다.엘리먼트 가시성 판단이미지 동적 로딩에서 가장 중요한 코드는 해당 엘리먼트가 현재 화면 내에 보이는지 알아내는 것입니다. 이를 위해 엘리먼트의 크기와 위치 값을 돌려주는 Element.getBoundingClientRect 함수를 사용하는 경우가 많습니다. 아래는 이 함수를 사용한 간단한 구현 예제입니다.function isInViewport(element) { const viewportHeight = document.documentElement.clientHeight; const viewportWidth = document.documentElement.clientWidth; const rect = element.getBoundingClientRect(); if (!rect.width || !rect.height) { return false; } var top = rect.top >= 0 && rect.top < viewportHeight; var bottom = rect.bottom >= 0 && rect.bottom < viewportHeight; var left = rect.left >= 0 && rect.left < viewportWidth; var right = rect.right >= 0 && rect.right < viewportWidth; return (top || bottom) && (left || right); } 이벤트 처리위에서 구현한 isInViewport 함수는 언제 호출해야 할까요? 먼저 문서를 처음 불러왔을 때 호출해야 합니다. 그 후에는 동적 로딩이라는 단어에 맞게 사용자의 동작에 따라 보이지 않던 엘리먼트가 보이게 되는 이벤트를 감지해야 합니다. 마우스나 터치로 스크롤을 통해 문서의 위치가 바뀌거나 브라우저의 크기가 바뀔 수도 있고 모바일 기기의 화면을 돌려서 볼 수도 있습니다. 데스크톱의 경우 scroll, resize 이벤트를, 모바일의 경우 orientationchange 이벤트의 처리를 생각해야 합니다.const images = Array.from(document.querySelectorAll('img')); document.addEventListener('scroll', () => { images.forEach(image => { if (isInViewport(image)) { image.onload = () => images.splice(images.indexOf(image), 1); image.src = 'original_image_path'; } }); }); 간단하게 위 코드와 같이 구현할 수 있습니다. 물론 실제 서비스를 위해서는 수정할 점이 몇 가지 있습니다. 동적 로딩의 대상이 되는 이미지를 구분하기 위해 해당 엘리먼트에만 특정 클래스를 부여하거나 HTML5에서 지원하는 data 속성을 이용하기도 합니다. 스크롤이나 리사이즈 이벤트가 과도하게 발생하는 경우가 많으므로 throttle 또는 debounce 등을 사용해 실행 빈도를 조절할 수도 있습니다. 일부 라이브러리에서는 requestAnimationFrame을 이용해 이벤트 핸들러를 처리하기도 합니다.TADA레진코믹스에서는 이미지 동적 로딩을 위해 서비스 초기에 Unveil 라이브러리를 사용했었습니다. 그러나 적용 후 몇 가지 아쉬움이 있어 따로 TADA 라이브러리를 제작했습니다. 먼저 마크업 구조상 이미지를 태그가 아닌 다른 태그에 배경 이미지로 사용하는 경우를 지원해야 했습니다. 그리고 모바일 웹에 주로 많이 적용하는 수평 스크롤 구역에 대한 처리도 필요했습니다. 문서의 스크롤 이벤트로는 처리가 되지 않기 때문에 특정 엘리먼트를 받아 그 엘리먼트의 스크롤 이벤트 핸들러를 등록할 수 있도록 해야 했습니다. 이를 해결하기 위해 만든 라이브러리를 현재 서비스에 적용 중이지만 이 라이브러리 역시 아래와 같은 문제점들을 가지고 있습니다.</> < id>기존 이미지 동적 로딩의 문제점getBoundingClientRect 함수의 문제점위 코드에서 특정 엘리먼트가 현재 화면 내에 보이는지 검사할 때 사용하던 Element.getBoundingClientRect 함수에는 치명적인 단점이 있습니다. 이 함수를 호출할 때마다 브라우저는 엘리먼트의 크기와 위치값을 최신 정보로 알아오기 위해 문서의 일부 혹은 전체를 다시 그리게 되는 리플로우(reflow) 현상이 발생한다는 점입니다. 호출 횟수가 적을 경우에는 부담이 되지 않지만, 이 함수는 위에서 구현한 것처럼 스크롤이나 리사이즈 이벤트가 발생할 때마다 등록한 모든 엘리먼트를 순환하면서 호출하게 됩니다. 이 코드들이 하나의 메인 스레드에서 실행되기 때문에 스크롤을 할 때마다 실행 속도가 눈에 띄게 느려질 수도 있습니다.외부 도메인 문서를 사용하는 iframe최신 브라우저들은 동일 도메인 정책에 따라 iframe 내의 외부 도메인 문서에서 현재 문서에 접근하지 못 하게 막고 있습니다. 이 제한은 서비스 개발에서 겪게 되는 문제는 아니고 외부 광고 플랫폼 개발자의 입장에서 발생하는 문제입니다. 광고 이미지의 표시나 클릭 이벤트의 처리 등은 iframe 내에서 처리할 수 있지만 광고 이미지를 지연 로딩한다거나 이 광고가 사용자에게 노출이 되었는지 기록하는 등의 기능은 iframe 내에서 불가능합니다. 그래서 광고를 적용하는 서비스 개발자에게 스크립트를 제공하고 서비스 문서 내에 삽입하는 방식으로 처리하고 있습니다. 이러한 외부 광고가 여러 개라면 삽입해야 하는 코드가 늘어날 뿐만 아니라 코드 내에서 스크롤이나 리사이즈 이벤트 등을 각각 사용하기 때문에 위에서 언급한 문제점들이 배가될 수 있습니다.기타 이벤트에 대한 처리대부분의 동적 로딩 라이브러리들은 적용할 엘리먼트에 특정 클래스 또는 data 속성을 부여하면 코드를 추가 작성하지 않더라도 쉽게 사용할 수 있습니다. 하지만 화면에서 보이지 않던 엘리먼트가 갑자기 나타나는 현상은 스크롤이나 리사이즈 이벤트에서만 발생하는 것이 아닙니다. 더보기 버튼을 눌렀을 때 숨겨져 있던 엘리먼트를 노출할 수도 있고 AJAX 호출 후 엘리먼트를 생성한 후 보여줄 수도 있습니다. 이런 경우 일괄적으로 처리하기가 어렵우므로 해당 이벤트가 발생할 때 수동으로 처리하는 수밖에 없습니다.IntersectionObserver위에 나열한 문제들을 효과적으로 처리하기 위해 크롬 51/엣지 15/파이어폭스 55 버전부터 IntersectionObserver를 지원하기 시작합니다. 우리말로 번역하면 교차 감시자 정도가 될 이 기능은 등록한 엘리먼트가 보이는 영역에 나타나거나 사라질 때(용어에 충실하자면 대상 엘리먼트의 영역이 루트 엘리먼트 영역과 교차하기 시작하거나 끝났을 때) 비동기로 이벤트를 발생시켜 줍니다. 기본적인 사용법은 아래와 같습니다.const intersectionObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { // do something observer.unobserve(entry.target); } }); }); intersectionObserver.observe(element); 먼저 IntersectionObserver 생성자에 콜백 함수를 인자로 넘겨주고 생성한 인스턴스의 observe 메소드를 통해 동적으로 처리할 엘리먼트를 등록합니다. 콜백 함수는 IntersectionObserverEntry 객체 목록을 전달받으므로 순환문을 통해 각 엘리먼트에 대한 처리를 완료하고 필요한 경우 unobserve 메소드를 이용해 감시 대상에서 해제할 수 있습니다.IntersectionObserver 예제아래 예제들은 기본적으로 아래 코드를 바탕으로 작성되었습니다. 각 예제들의 최상단에 엘리먼트에 짝을 이룬 표시등을 두었고 콜백 함수가 호출될 때마다 파동 효과를 주었으며 이 때 isIntersecting 속성을 기준으로 표시등을 켜지거나 꺼지게 되어 있습니다.const io = new IntersectionObserver(entries => { entries.forEach(entry => { // isIntersecting 속성으로 해당 엘리먼트가 보이는 지 표시 }); }); Array.from(document.querySelectorAll('.box')).forEach(box => { io.observe(box); }); 스크롤 이벤트를 다루지 않더라도 엘리먼트의 가시성 여부를 isIntersecting 속성을 통해 알 수 있습니다.<iframe class="demo" data-fr-src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/basic-verticalscroll.html" width="600" height="400" frameborder="0">수평 스크롤의 경우에도 overflow 속성이 적용된 컨테이너 엘리먼트의 스크롤 이벤트를 처리하지 않더라도 상관없습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/basic-horizontalscroll.html" width="600" height="200" frameborder="0">더보기 버튼에 대한 클릭 이벤트를 따로 등록하지 않아도 동작합니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/basic-unfold.html" width="600" height="240" frameborder="0">리사이즈 이벤트 역시 따로 처리할 필요가 없습니다. 새 창을 띄운 후 창 크기를 조절하면서 확인할 수 있습니다.위의 모든 예제들은 <iframe> 태그 안에서 실행되고 있습니다. 이처럼 작성한 코드가 외부에서 실행이 되더라도 IntersectionObserver API는 문제없이 동작합니다.IntersectionObserver 생성자 옵션rootroot 옵션에는 가시성의 판단 기준이 될 HTML 엘리먼트를 지정합니다. observe 메소드로 등록하는 엘리먼트들은 반드시 이 루트 엘리먼트의 자식이어야 합니다. 이 옵션을 지정하지 않을 경우 브라우저 화면에서 현재 보이는 영역인 뷰포트가 기본이 됩니다. 아래 예제에서 알 수 있듯이 루트 엘리먼트를 지정하면 현재 화면과는 상관없이 루트 엘리먼트와 등록한 엘리먼트들의 영역이 교차하는 지 판단하게 됩니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-root.html" width="600" height="400" frameborder="0">rootMargin루트 엘리먼트의 마진값을 지정할 수 있습니다. CSS에서 사용하는 형식과 같기 때문에 “10px”, “10px 20px”, “10px 20px 30px 40px” 형태가 모두 가능하며 음수값으로 지정할 수도 있습니다. 기본값은 0이고 루트 엘리먼트를 지정한 상태라면 퍼센트값을 사용할 수도 있습니다. 이 옵션을 이용하면 이미지 동적 로딩에서 해당 엘리먼트가 화면에 나타나기 전에 이미지를 불러오기 시작해 이미지 공백을 줄이는데 유용하게 이용할 수 있겠습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-rootmargin.html" width="600" height="400" frameborder="0">위 예제에서는 root 옵션은 지정하지 않고 rootMargin 옵션을 각각 0, 100px, -100px, 50%로 지정했습니다. 하지만 iframe 내에서 실행을 하게 되면 이 옵션이 정상적으로 동작하지 않습니다. 프로젝트 페이지의 이슈 댓글에는 동일 도메인의 프레임 안에서는 동작하는 것으로 논의되고 있지만 뷰포트에는 해당하지 않거나 버그 또는 아직 크롬에 반영이 되지 않아 보입니다.이 예제는 새 창을 띄워 프레임을 벗어나면 정상적으로 동작합니다. 스크롤을 내리다보면 엘리먼트마다 IntersectionObserver 콜백 함수가 다른 위치에서 호출됨을 알 수 있습니다.thresholdthreshold 옵션은 엘리먼트가 콜백 함수의 호출 시점을 정하는 옵션입니다. 0과 1을 포함한 그 사이의 숫자 또는 숫자 배열을 지정할 수 있는데 이 숫자는 엘리먼트의 전체 영역 중에 현재 보이는 영역의 비율입니다. 이 비율의 경계를 넘나들 때마다 콜백 함수가 호출됩니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-threshold-value.html" width="600" height="400" frameborder="0">위 예제에서는 threshold 옵션을 각각 0, 0.5, 1, [0, 1]로 지정했습니다. rootMargin 예제처럼 엘리먼트마다 콜백 함수가 다른 위치에서 호출됩니다. 두 번째와 세 번째 상자는 콜백 호출 시점에 isIntersecting 값이 항상 참이기 때문에 표시등이 정상적으로 표시되지 않습니다. isIntersecting 속성을 기준으로 처리해야 할 작업이 있다면 반드시 threshold 속성에 0을 포함시켜야 정상적으로 동작합니다. threshold 속성은 아래 예제처럼 콜백 함수의 인자로 받는 IntersectionObserverEntry 객체의 intersectionRatio 속성과 같이 사용하기에 유용합니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/option-threshold-ratio.html" width="600" height="400" frameborder="0">위 예제에서는 threshold 옵션을 각각 [0, 1], [0, 0.5, 1], [0, 0.25, 0.5, 0.75, 1]로 지정하고 콜백이 호출되면 intersectionRatio 값을 기준으로 표시등의 배경 투명도를 바꾸도록 했습니다.IntersectionObserver의 활용이미지 동적 로딩지금껏 알아본 IntersectionObserver를 이용해 이미지 동적 로딩을 간단하게 구현해봅니다. 이미지 엘리먼트를 구성할 데이터가 배열로 존재한다고 가정하고 ES6에서 지원하는 템플릿 문자열을 사용해 배열을 순환하면서 이미지 목록을 생성합니다. 그 후 IntersectionObserver를 초기화하고 만들어진 엘리먼트들을 등록합니다. 콜백 함수에서는 엘리먼트가 보이는 상태일 때 이미지를 로딩하고 해당 엘리먼트를 감시 해제합니다. id="comics"> const comics = [ { alias: 'eunsoo', id: 6080299074584576, title: '은수' }, ... ]; const template = comics => ` ${comics.map(comic => ` ${comic.id}"> ${comic.alias}" class="info">${comic.title} `).join('')} `; document.getElementById('comics').innerHTML = template(comics); const io = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (!entry.isIntersecting) { return; } const target = entry.target; const id = target.dataset.id; target.querySelector('.info').style.backgroundImage = `url(https://cdn.lezhin.com/v2/comics/${id}/images/wide?width=600)`; observer.unobserve(target); }); }); Array.from(document.querySelectorAll('.comic')).forEach(el => { io.observe(el); }); 아래 예제를 실행하면서 브라우저의 개발자 도구를 열고 네트워크 탭을 살펴보면 이미지 엘리먼트가 보이기 시작할 때 불러오기 시작하는 것을 확인할 수 있습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/demo-lazyload.html" width="600" height="400" frameborder="0">무한 스크롤IntersectionObserver 기능을 이용하면 무한 스크롤 역시 쉽게 구현할 수 있습니다. 아래 예제에서는 스크롤의 끝부분에 감시를 할 엘리먼트를 두고 그 엘리먼트가 노출이 될 때마다 콘텐츠를 추가로 불러오도록 작성하였습니다. id="items"> id="sentinel"> const count = 20; let index = 0; function loadItems() { const fragment = document.createDocumentFragment(); for (let i = index + 1; i <= index + count; ++i) { const item = document.createElement('p'); item.classList.add('item'); item.textContent = `#${i}`; fragment.appendChild(item); } document.getElementById('items').appendChild(fragment); index += count; } const io = new IntersectionObserver(entries => { entries.forEach(entry => { if (!entry.isIntersecting) { return; } loadItems(); }); }); io.observe(document.getElementById('sentinel')); loadItems(); 실행 결과를 아래 예제에서 확인할 수 있습니다.<iframe class="demo" src="https://cdn.rawgit.com/fallroot/intersectionobserver-examples/master/demo-infinitescroll.html" width="600" height="400" frameborder="0">마무리IntersectionObserver API는 아직 몇몇 브라우저의 최신 버전에서만 사용할 수 있지만 지원하지 않는 브라우저를 위해 Polyfill을 제공하고 있습니다.IntersectionObserver API는 웹 광고 플랫폼 제공자와 사용자 모두에게도 좋은 소식이라 봅니다. 이 API가 정착된다면 광고 노출 여부를 측정하기가 쉬워지고 서비스에 더해졌던 리소스 낭비를 줄일 수 있을 것이라 생각합니다.레진코믹스는 크롬 브라우저 사용자의 비중이 높은 편이기 때문에 빠른 시일 안에 적용해 볼 예정입니다. 이미지 동적 로딩 기능의 개선과 정주행 기능과 같이 현재 스크롤 이벤트를 과하게 사용하고 있는 코드에 대한 부담을 덜 수 있기를 기대하고 있습니다.참고자료Intersection Observer API SpecificationIntersectionObserver’s Coming into ViewIntersection Observer API
조회수 801

아마존 인플루엔서를 통해 매출을 늘리는 법

인사말안녕하세요 대한민국 셀러들의 성공적인 아마존 진출을 도와주는 컨설팅 회사이자 대행사인 컨택틱의 이이삭 대표입니다. 오늘 여러분들에게 소개하고 싶은 내용은 아마존에서 발표한 새로운 개념인, '아마존 인플루엔서 프로그램'입니다. 이 기능을 잘 활용하면 브랜드 오너들의 경우 매출을 증폭할 수 있는 하나의 방법이 되지 않을까 싶습니다.우선 본론으로 들어가기 앞서 Amazon Associates Program과 그에 속해있는 세부 분야인 Amazon Influencer Program에 대한 소개를 드리는 것이 좋을 것 같습니다.Amazon Associates ProgramAmazon Associates Program을 한 마디로 설명드리자면, '영업 인센티브 제도'라고 할 수 있습니다. 즉, 누군가가 인터넷으로 아마존의 특정 상품을 특정 URL (본인의 Amazon Associates Link)로 홍보하여, 그 링크를 클릭하고 상품을 구매한 고객이 있을 경우, 구매한 액수만큼 %로 커미션 (수수료)를 지급 받는 개념입니다. 예를 들어, 제가 Amazon Associates Program에 이수하여, 저의 Associates 링크로 제 블로그에 $1000짜리 캐논 DSLR 카메라를 홍보했고, '홍길동'이라는 사람이 그 링크를 클릭하여 그 카메라를 구매했다면, 제 앞으로 4% (카테고리에 따라 최대 10%까지 수령 가능)의 수수료인 $40가 지급됩니다.아주 멋진 프로그램이죠? 하지만 정작 중요한 것은 정확히 얼마를 받을 수 있는지입니다. 카테고리마다 커미션이 다른데요, 최저 0%에서 최대 10%까지 가능합니다. 웬만한 카테고리는 4% 커미션을 지급 받을 수 있습니다. 정확한 커미션율은 아래 표를 참고해주세요:카테고리별로 Amazon Associates 커미션율Amazon Influencer ProgramAmazon Influencer Program은 Amazon Associates Program의 속편입니다. Amazon Associate가 되면 Associates Central이라는 어드민 페이지에서 본인의 링크를 만들 수 있게 되는 등 어드민 페이지에 접속이 가능해지는데, 이 시스템의 단점이 있었습니다. 그것은 바로 "나만의 아마존 랜딩페이지가 없다"는 것이었습니다. 이게 무슨 말이냐면, Associates Central에서 링크를 만들더라도, 기존재하는 특정 아마존 상품의 링크를 걸 수도 있고, 아니면 특정 '검색어 결과'에 대한 링크를 나의 홍보 링크로 사용할 수 있었는데, 결국 '나의 아마존 내의 미니몰' 같은 것을 만들 수가 없었다는 것입니다. 하지만 Amazon Influencer Program을 통해서 이제 인플루엔서들이 본인만의 아마존 미니몰을 만들 수 있게 되었습니다 (예를 들어: amazon.com/shop/influencername). 이럼으로써 더이상 하이퍼링크(URL)를 '클릭'하지 않아도, 해당 인플루엔서들의 팔로워들이 아예 인터넷 검색창에 위처럼 특정 인플루엔서의 아마존 미니몰 URL을 직접 입력하고 방문하고 제품을 구매해도 여전히 인플루엔서에게는 커미션이 지급 되게 되었습니다.Social Media Promo Codes이제 Amazon Associates Program과 Amazon Influencer Program에 대한 이해를 하셨다면, 다음으로 셀러입장에서 '어떻게 이 프로그램을 잘 사용해서 그 수많은 인플루엔서들에게 내 상품을 적극적으로 홍보하라고 독려할 수 있을까'를 고민하셔야될 것입니다. 그리고 아마존에서 이 문제를 쉽게 해결할 수 있도록 여러분들께 하나의 기능을 제공하였습니다. 그것이 바로 Social Media Promo Codes입니다. 이 기능은 아마존 브랜드 레지스트리 프로그램에 이수중인 '브랜드 오너'만 이용이 가능한데요, 아래에서 보이듯이 메뉴를 찾는 것은 쉽습니다:Social Media Promo Codes가 어떤 형태의 Promotion인지 구체적으로 설명하려면 이 포스트가 너무 길어지기 때문에, 한 마디로만 요약해드리자면, '내 브랜드 상품 중에 몇 가지를 선정한 특정 URL을 생성하여, 해당 제품들에 대해서는 고객들이 굳이 별도로 promotional code를 입력하지 않아도, 장바구니에 담자마자 미리 설정해둔 할인가로 구매할 수 있게 해주는 URL 생성식의 promotion' 기능입니다. 어쨌거나 중요한 것은, 이제 이걸 이용하게 되면 생성 페이지에서 아마존이 이런 행사를 브랜드 측에서 하고 있다는 것을 브랜드가 인플루엔서들에게 쉽게 알려줄 수 있도록 'Share this promo code with Amazon Influencers and Associates' 라는 기능을 추가한 것입니다.막상 브랜드 측에서 이렇게 할인을 제공하는 행사를 하기로 크게 마음 먹었는데, 페이스북에서 힘들게 홍보하고 인스타에서 힘들게 홍보하고 구글에서 힘들게 홍보하는 것도 결국 다 일입니다. 하지만 애초에 브랜드를 대신해서 이런 행사를 홍보해줄 수 있는 인플루엔서들이 가세해준다면? 브랜드 입장에서는 손도 안대고 코를 풀 수 있는 격이 되는 것입니다.마치며이 기능을 활용해서 브랜드 오너들은 인플루엔서 마케팅에 들어가는 수고와 비용을 조금 덜 수 있게 되었으면 좋겠습니다! 그리고 아마존 인플루엔서 인맥을 잘 활용해서 매출을 증폭할 수 있는 기회가 되길 희망합니다.그럼 오늘도 즐거운 글로벌 셀링 되세요! 컨택틱   서울특별시 강남구 강남대로 62길 11, 8층 (역삼동, 유타워)   대표 전화: 02-538-3939   해외 부서: 070-7771-1727   영업 부서: 070-7771-1728   이메일: support@kontactic.com   유튜브: https://www.youtube.com/channel/UC8OxbQGAnMqWGpGj5weLcZA  홈페이지: https://www.kontactic.com
조회수 1852

모두가 Yes라고 할 때,

조금 오래된 광고 카피라이트지만,뇌리에 박혀 버린 말이 있다.모두가 예스라고 답할 때, 노라고 말할 수 있는 사람!모두가 노라고 답할 때, 예스라고 말할 수 있는 사람!(2001년 동원증권 CF 중에서 카피라이트 문구)한 때는 그것이 멋져 보였다.왠지 자신만의 주관이 뚜렷하고,개성이 있는 인재상처럼 느껴졌고남들과는 다른 창의성, 혁신의 뉘앙스가묻어나는 행동처럼 비쳤다.그렇다고 믿었다.이것이 맞느냐!아니다 저것이 더 낫다!이건 안된다.아니다 된다라는 이분적인 회의는결론 도출이 안 되는평행선을 달리기가 될 수 있다.예, 그렇습니다, 맞습니다 또는 아니요, 그렇지 않습니다. 틀립니다라는 대답만으로는 부족하다.그 주장이 나오게 된 원인과그렇게 생각하는 근거는사람에 따라 다를 수 있다.그렇기에 더 많은 시나리오와그만큼 많은 대안과 출구전략들이나타나야 하는 게 정상이다. 그 이후에Yes와 No에 대하여, 더 명확하게는Go or Stop 사이에서 최종 결정은 마지막에 정리되어야 한다.(물론 Plan B와 Plan Z까지 첨부해서...)1. 시작은 Why로부터...어떠한 프로젝트 의제에 대하여생각은 다 다를 수 있다.탐탁지 않은 부분이 있어 반대할 수 도 있고,적극적으로 밀어붙이고자 찬성할 수 도 있다.적극적 반대도 있고, 어정쩡한 찬성도 있다.여전히 반반 사이에서 부동층을 형성할 수도 있다.이러한 고착상태에서 의견을 모을 수 있는 방법은 논리이다.논리는 순서이다.원인과 근거를 제시하는 것부터 시작이다.때문에모두가 Yes를 외칠 때, Why라고 묻는 것이다.모두가 No라고 외칠 때, Why를 묻는 것이다.어린아이가 성장하면서 호기심과 궁금증이 많아지면서 "왜요?", 왜 그래요?"라는 말의 빈도가 높아진다.마찬가지로한창 성장하고 있는 회사에는"왜"라는 질문이 매우 중요하다.문제를 진행할지 안 할지 이전에문제의 근본적인 원인을 되짚는 것이 우선이다.Why는 몇몇 리더들이 불편해하는 질문이기도 하다.특히 시간에 쫓기며,빠른 결정을 해야 할 때는 더더욱중간 단계를 skip 하길 원한다."그냥 하라면 해!""그건 이미 다 결정된 거야""지금 와서 돌이킬 순 없어."라는 식의 반응은 어디선가 많이 들어보지 않았을까?꼰대라고 여기던 직장 상사라던가,고압적인 교수님이라던가,고지식한 군대 선임에게서도...그러한 조직 내지는 리더에게Why라는 물음은 군말이 많다,대든다,오지랖이다,주제넘는다라는 핀잔으로 돌아오곤 했다.그렇게 하나둘씩 입을 다물기 시작하고,나중에는 거수기들만 남아있는 회의, 의사결정 자리가 되어버리지.2. 본론은 룰(Rule)로부터...일반적으로 스타트업의 의사결정은 동료들과 문제에 대한 해결방안, 대안을격렬하게 논의하면서 진행된다.스타트업에서회의의 진짜 묘미는바로 다양한 아이디어와 의견을 도출하되마지막은 결론이 정리될 수 있어야 한다는 점이다.중구난방으로 쏟아진 의견은 자칫 회의가 산으로 갈 수가 있다.정리되지 않은 아이디어들은 다음 날이 되면 우리가 뭣 때문에 회의를 한지 방향성을 잃게 만들기도 한다.역으로,제한적으로 과한 통제는시계 초침이 "똑딱, 똑딱" 느껴질 정도로지루하고, 숨 막히는 회의가 될 수도 있다.그렇기에 회의에는 룰이 필요하다.최소한 정해 놓아야 할 룰은 다음과 같다.1) 회의 전 사전검토에 대한 룰(회의 내용 사전 숙지 및 검토),2) 회의시간 한도의 룰(무한정 회의는 삼가자),3) 구성원 간의 발언 룰(발언자/사회자/경청자가 지켜야 할 룰),4) 결과 정리의 룰(의견을 정리, 취합하고, 다음 단계로 넘어갈 수 있도록 액션을 정할 것)적어도 위의 4가지 rule은 경험적으로,많은 시행착오를 거치면서 필수적이라고 깨달았다.  3. 결론은 How로부터...난 하와이를 좋아한다.가 본적 없는 여행지인 하와이가 아니라나름대로 이름 지은Howhy(하우 와이)!아재 개그인가....ㅠ.,ㅠWhy라는 질문으로 문제의 본질을 찾는다면,How는 질문으로 문제의 해결책을 찾는다.그래서 어떻게 할 건데!How는 육하원칙의 하나이지만,다른 단어들과 동등하다고 생각하지 않는다.How 안에는 언제 해야 할지,무엇을 해야 할지,누가 해야 할지,어디서 해야 할지를 포함한다.따라서,Why와 How는 문제 해결로 가는가장 중요한 열쇠이다.그럼에도(주)클린그린의 회의가 이상적이지는 않다.습관화가 덜 되어서인지,뭔가 간과한 부분이 있는 건지,아니면,회의 진행에 있어 여전히 미숙한 건지딱 하나 꼬집어 말하기는 어렵지만늘 100% 만족할만한 회의는 없었다.하지만 분명한 것은이전보다는 효율적이고,보다 다양한 의견과 정리된 결론으로진일보하였다는 점이다.제품이나 서비스만 피봇 되는 게 아니다.회사도,시스템도,업무도,사람도 피드백과 수정을 거쳐발전해 나가는 것이다.우리는 계속 발전하고 있는 중이다.#클린그린 #스타트업 #창업가 #창업자 #마인드셋 #조언
조회수 1444

제 2회 크몽어워즈, 비하인드 스토리를 공개합니다. :D

안녕하세요! 크몽팀의 마리입니다. :)지난 주 금요일인 12월 12일, 크몽팀은 즐겁고 분주하게크몽어워즈를 준비하느라 여념이 없었어요. 그 시간들을 공유하고자 이렇게 사진과 스토리로 보여드려요.다 함께 공유하면서 행사 당일의 생생함을 느껴보는건 어떨까요~  그럼 크몽어워즈 비하인드 스토리로 꼬꼬 꼬꼬!!!     이번 어워즈를 준비하면서 크몽의 2014년을 정리하는 의미로전반적인 컬러와 분위기를 따뜻하고 포근하게 변경했답니다. 확실히 분위기가 변하고나니까 팀원들 역시 즐겁게 준비하게 되었어요. 크몽어워즈가 열렸던 파티쿡 바로 앞 배너!배너를 먼저 세워두니 6시부터 급 입장하시는 분들이 속출 하지만 저희는 7시부터 입장을 받았기 때문에 한 시간 일찍 오신 분들께서뻘쭘해하실까봐 ㅠ.ㅠ) 시간을 맞춰서 와주시기를 부탁드렸답니다.   어워즈 준비에 필요한 소품들을 만들기 위해 무려 10명이 넘는 팀원들은입이 터지도록 풍선을 훅훅 훅훅  여러분들께서 파티때 차고 넘쳤던 풍선들을 보셨다면그것은 저희가 슉슉이(펌프)가 아닌 손수 입으로 불어제낀 풍선이랍니다.    이러기 있기 없기? 흡사 풍선파티 같았던 크몽어워즈     풍선을 부느라 입이 터진 멤버들, 잠시 쉬고 있었으나실은 쉴 시간 조차 없다!  일 일 일 일!!!!!!!!!!!!!!어워즈 오픈까지 약 20분 남은 시간, 어지럽게 늘어져있는 의자들을 옮겨라!!!!    의자를 옮기는 멤버들 ㅋㅋㅋㅋ 이 사진은 뭔가 깨알같이 재미있어요 조금 더 자세히 들여다볼까요?  원래 이 사진은 죠(Joe)님과 쿤(Kun)님이 함께 대본을 외우고 있는 모습인데요.어쩐지 죠 님의 시선이 대본이 아닌 다른 곳을 보고있는 것만 같아요.  이런 느낌이 더 강하달까요   혹은 이런 사진! 시간이 없다보니 팀원들은 너나할거 없이 의자들고 슉슉 이동!    손님맞이를 하는 크몽 웰컴보드! 연말파티 버젼으로 꾸며보았어요 :) 크몽의 마스코트인 원숭이들이 여섯마리나!!!! 몽끼끼 몽끼끼~     이번 어워즈에 가장 심혈을 기울인 핑거푸드! :) 크몽팀도 뜨악하게 만들었던 엄청난 퀄리티의 파티쿡 핑거푸드의 비쥬얼이 블링블링~저는 다이어트 중이라 몇 가지 집어먹지 못했지만 팀원들의 닭다리 뜯는 모습만 봐도얼마나 맛있는지 눈에 훤해요    엠씨보다말고 샌드위치 흡입하는 MC Joe    몇 없는 토니님의 잘 나온 사진을 공개합니다!!! 저희가 말죽거리 잔혹사 교복같다고 계속 그랬더랬죠~ 크몽 스티커를 붙여마치 원래 크몽에서 제작한 옷인듯 매칭한 토니님의 센스에 놀랍니다  ●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●● 그리고 7시 크몽어워즈 오픈!!!!!크몽어워즈가 오픈!하고 판매자분들이 차례로 입장하시기 시작하셨어요~저녁 4시부터 갑자기 내린 폭설때문인지 강남대로가 꽉 막혀서 겨우겨우 도착하셨지만무려 신청인원의 절반 이상이 와주셔서 어워즈를 잘 이어갈 수 있었답니다.  함정이 있다(좋아 자연스러웠어).jpg 금요일 저녁 퇴근하고 7시까지 오시기가 힘드셨을텐데, 웃음기를 가득 머금고 어워즈에 참석해주셨어요. :)   한 시간의 여유있는 식사와 함께 크몽어워즈의 막이 올랐더랬죠~ :) 다양한 판매자분들과 구매자분들이 크몽의 2014년, 2015년을 궁금해하셨고활발한 질문과 인터뷰가 이어졌습니다. 물론 그 전에 크몽의 대표이신 토니님의 오프닝 멘트가 있었어요~ ~ 토니님의 CEO 느낌 물씬물씬 ~  그리고 이어지는 상위랭킹 판매자 분들의 노하우 전수가 있었어요.크몽에서 많은 판매량과 단골 구매자를 보유하고 계신 쎈쓰박 님! 디자인 머신으로 불릴만큼다양한 활동을 하고 계신 쎈쓰박님의 인터뷰는 상당히 인상적이었답니다~ 판매자분들의 자기소개 시간도 유익했어요~실명은 모르지만 닉네임을 듣는 순간 '아~' 하는 분들이었으니까요!  카테고리별 & 재능의 테마 별 테이블을 마련하다보니 다양하고 풍성한 주제로 대화를 하시는 판매자분들이 많이 계셨답니다. 이쪽은 훈남 훈녀 테이블이네요~이 자리에서 대화를 했었어야 했는데.... (사심)    조 님과 쿤 님은 MC를 보면서 만담을 했었는데요. ㅋㅋㅋㅋㅋㅋ생각보다 많은 분들이 엄마미소를 띄고 봐주셨답니다. 두 분께서 열심히 MC를 봐주신 덕분에 어워즈의 마지막, 시상식까지 올 수 있었어요!    귀여운 포즈해주세요~라는 말에 다리까지 숑 들어주신 재능인 님 ㅎㅎㅎㅎㅎ곰돌이 니트가 인상적이어서 인터뷰를 했었는데 외모처럼나긋나긋한 말투셨어요~  베스트 드레서를 뽑는 자리, 진정한 베스트 드레서라면개인기와 워킹쯤이야. YELLOW의 드레스코드를 꼭 맞춰오신 판매자분과상품이 갖고 싶어 나오신 용기있는 분! 이 분의 직업은 경호원이셨어요 ㅋㅋㅋㅋㅋ무려 조 님께 어택을 가하신 후 당당히 상품 획득~! 흑역사는 작게 ☞☜   이렇게 획득하게 되었습니다.  조 지못미  아무튼, 크몽팀의 선물은 질만큼 양도 푸짐했습니다.계속 계속 계속 받게되는 선물들이죠.   모두들 만족스러운 어워즈가 되었겠죠 :) 크몽팀 역시 재미있게 잘 보내고, 판매자분들께서도 나가시면서작년에 비해 풍부한 프로그램과 따뜻한 분위기, 편안한 자리, 맛있는 음식이 있어서 좋았다고 말씀해주셨어요. 내년 어워즈를 기대하시는 분들이 많을 것 같네요.  아이 좋아라!   그럼 다같이 기념사진 한 방 찍고! 제 2회 크몽 어워즈를 마무리해볼까요?    크몽 어워즈에 참여해주신 많은 분들께 정말로 감사드리며어워즈를 위해 고생하신 팀원분들 수고하셨어요~  ★ 하단 링크를 통해 가시면 크몽 어워즈 수상자 및 더 많은 사진들을 보실 수 있습니다! ★    #크몽 #디자이너 #디자인팀 #이벤트개최 #이벤트후기 #경험공유
조회수 1116

웹 서비스 개발자가 APM을 사용해야 하는 이유

백엔드 서비스를 만들고 운영하는 개발자라면, 지금 바로 APM 서비스를 사용해 보세요. 와탭의 APM은 국내 수많은 Enterprise 기업에서 자사의 서비스를 분석하기 위해 사용되고 있으며 많은 효과를 보고 있습니다. 북미에서는 이미 수많은 스타트업이 DevOps의 기본 도구로 APM을 선택하고 있습니다. APM은 원래 대규모 서비스를 운영하는 분들이 전문적으로 사용하고 있었지만 최근 트렌드는 운영자에서 개발자로 이동하고 있는 서비스 이기도 합니다. 특히 와탭의 APM은 개발자 분들을 위한 스택 분석 기능이 있습니다. 개발자라면 와탭 APM 서비스가 제공하는 아래의 3가지 스택 분석 기능을 꼭 사용해 보세요. 유니크 스택탑 스택액티브 스택많은 개발자들이 자신이 만든 서비스가 어떻게 동작하는지 또는 웹 서비스에 어떤 영향을 주고 있는지 알지 못합니다. 하지만 와탭 애플리케이션 성능 모니터링(APM) 서비스를 사용하면 메소드가 애플리케이션에서 어떻게 사용되는지 얼마나 사용되는지 알수 있습니다. 와탭은 다른 APM 서비스와 다르게 10초에 한번씩 활동중인 트랜잭션을 검사하여 트랜잭션에 콜스택정보를 저장하고 있습니다. 그리고 이렇게 저장된 스택정보를 가지고 3가지 형태로 가공하여 보여주는데, 이 것이 유니크 스택 / 탑 스택 / 액티브 스택입니다. 먼저 유니크 스택은 가장 많이 사용된 스택 정보를 보여주는 방식입니다. 트랜잭션에서 실행되고 있는 메소드가 A 이고 이를 호출한 메소드가 모두 일치하는 스택을 유니크 스택이라고 합니다.1. A() ← C()2. A() ← C()3. B() ← D()4. B() ← E()5. B() ← F()위와 같은 경유 유니크 스택은 아래와 같이 통계를 내어 보여 줍니다. 40% A()    A()    C()20% B()    B()    D()20% B()    B()    E()20% B()    B()    F()이렇게 콜스택 정보 전체를 기준으로 분석을 하는 경우에는 성능에 영향을 주는 기능 단위의 분석이 가능합니다. 하지만 성능에 영향을 많이 주는 메소드를 알고 싶을 때가 있습니다. 이런 경우에 사용하는 것이 탑 스택 분석입니다. 아까와 같은 상황을 예를 들겠습니다.1. A() ← C()2. A() ← C()3. B() ← D()4. B() ← E()5. B() ← F()이런 상황에서 탑 스택 분석은 아래와 같이 가장 많이 사용되느 메소드를 알려줍니다. 60% B()    33% D()    33% E()    33% F()40% A()    100% C()유니크 스택에서는 A() ← C() 가 가장 많이 사용된 스택이라는 것을 알려주지만 탑 스택에서는 B() 메소드가 가장 많이 사용된 메소드라는 것을 알려줍니다. 이 두가지 내용을 통해 가장 많이 사용되는 메소드의 집합가 가장 많이 호출되는 메소드를 알아 낼 수 있습니다. 만일 서비스를 메소드 단위에서 개선하고 싶다면 이 정보를 기반으로 개선 작업을 진행하면 많은 도움을 받을 수 있습니다. 위에 화면에서 메소드를 선택하면 메소드를 호출한 스택들의 정보를 확인 할 수 있습니다. 마지막으로 액티브 스택입니다. 액티브 스택은 WAS 서버와 URL 그리고 발생 시간을 기준으로 저장된 콜스택의 정보를 보여줍니다. 서비스 성능이 떨어진 시간대의 콜스택 정보를 확인 함으로써 메소드 구간에서의 튜닝 정보를 제공합니다. 액티브 스택은 핵심 기능이 하나더 있습니다. 바로 서비스가 동작하는 스탭정보에 통합됨으로써 문제를 바로 확인할 수 있는 기능입니다. 와탭의 APM에서만 분석가능한 기능이며 특허로 등록되어 있습니다. 액티브 스택은 통계 관점이 아니라 실행 관점에서 문제를 바라보고 있습니다. 우리가 만든 웹 어플리케이션을 고객에 입장에서 보면 아래와 같이 동작합니다. 고객 → 웹 서비스 요청 → 서버 접속 → 서비스 접속 → 애플리케이션1 → 메소드 1 → DB 1접근 → Query 1 → Query 2 → 메소드 2 → 파일 접근 → 메소드 3 → 결과 취합 → WAS 통과 → 웹 서비스 결과 반환 일반적으로 애플리케이션 모니터링은 이런 상항을 아래와 같이 보여줍니다. 서비스 접속 → Query 1 → Query 2 → 파일 접근 → 트랜잭션 종료와탭의 애플리케이션 모니터링은 수집된 콜 스택 정보를 기반으로 아래와 같이 보여줍니다.  서비스 접속 → Query 1 → 메소드 2 → Query 2 → 파일 접근 →메소드 3 → 트랜잭션 종료위에 상황은 트랜잭션에서 메소드 2와 메소드 3이 수집된 경우에 트랜잭션의 스탭의 실행시간에 맞쳐서 정보를 재구성하는 것을 보여주고 있습니다. 이렇게 확인하게 된다면 메소드에서 발생하는 성능 문제를 확인 할 수 있습니다. APM 서비스는 와탭 / 뉴렐렉 / 데이터 독과 같은 서비스들을 통해서 2주에서 한달간 언제든 무료로 사용가능합니다. 다만 메소드에 대한 분석 기능은 와탭의 APM에서만 제공하는 기능들이 많습니다. 개발자라면 한번쯤 와탭의 APM 서비스를 통해 자신이 만들고 운영하고 있는 서비스에서 가장 많이 사용되는 메소드가 무엇인지 확인 해 보시기 바랍니다. Tip!! APM은 개발시에 사용하는 디버깅 도구라기 보다는 막대한 량의 트랜잭션이 발생하는 운영과정에서 사용되는 도구입니다. 트랜잭션 자체가 적다면 원하는 데이타가 안 나올 수 도 있습니다. 와탭으로 모니터링 하기 - 목차 바로가기#와탭랩스 #개발자 #개발팀 #인사이트 #경험공유 #일지 #서비스소개
조회수 845

스타트업 PR이 나아가야 할 방향

어느덧  3년째 사업을 이어가고 있다. 우리 팀은 나를 제외한 전체가 개발자기 때문에 내가 기획, 디자인, PR, 마케팅에 대한 업무를 맡고 있는데 자원의 한계만큼이나 굉장히 얕게 접근할 수밖에 없는 것이 사실이다. 그러나, 여기서도 내가 무언가 느낀 점이 있기 때문에 인터넷 세상에 이 경험을 공유하고자 한다.특히 PR에 대해서는 할 말이 꽤 많다. 짧게 짧게 여러 번 올려볼 예정이다. 나는 PR에 대해 처음 연구할 때, 귀스타브 르 봉의 군중심리, 지그문트 프로이트의 정신분석학에 대해 공부하였고 이를 PR에 최대한 접목시키려 노력했다. 그런데, 내가 많은 PR 담당자를 만나고 느낀 건 대부분이 본질적인 부분보다는 정량적이고 실질적으로 눈 앞에 보이는 성과를 달성하는데 집중하고 있다는 것이었다.PR 담당자들이 기자들과의 관계나 언론보도 등에 매우 신경을 쓰고 있는걸 볼 수 있는데, 이 부분은 매스미디어 마케팅의 영역에 가깝다. 내가 하고 싶은 말(회사의 성과나 대표의 인터뷰 등)을 신문사에 전달하여 대중과 소통하는 것은 PR 담당자가 할 일이 아니라고 본다. PR은 말 그대로 Public relation이다. 대중들과 어떻게 소통하느냐 대중들이 우리의 회사, 서비스, 조직을 어떻게 판단하고 어떤 이미지로 각인하느냐 등에 대한 본질적인 질문에 대해 답해야 한다. 외부에서 바라보는 회사에 대한  이미지뿐 아니라, 내부 팀원들이 비치는 분위기, 인상 또한 PR에 해당한다.그리고 그러한 독특한 인상과 메시지는 회사가 추구하는 방향과 일치해야만 한다. PR은 정량적인 성과와 직접적으로 빠르게 연결되는 데 초점을 맞춰선 안된다. 대중의 인식은 매우 느리게 움직인다. 신생아의 이름에도 유행이 있듯이, 우리가 원하는 물줄기를 대중으로부터 만들어나가는 행위는 매우 느리게 작동한다. 외부에서 우리 회사 '조커팩'이라는 회사를 판단하는 데는 공통된 이미지가 존재한다. 나는 그 이미지를 구축하기 위해  수년간 다양한 분야에서 노력해왔다.초기 기업은 대표의 이미지가 회사의 이미지와 직결되는 경우가 많기 때문에, PR 담당자는 대표와 밀접한 관계를 맺고 대표가 대외에 노출되는 이미지나 빈도 등에 매우 신경을 써야 한다. PR 담당자가 있는 회사들을 보면, 거의 대부분이 아주 비슷한 방향으로 움직이고 있다. '나의 회사는 굉장히 성과를 잘 내고 있는 회사고, 조직원들이 행복해하는 그런 꿈의 회사다.' 대부분이 그러한 포지션으로 PR을 이어나가고 있는데, 이는 회사의 개성을 파괴하는 역할을 하는 일등 공신이다. 분명히 회사 내부와 대표를 들여다보면 저마다 독특한 매력과 가치를 지니고 있다. 그런데, 그것이 외부적으로는 동일하게 표현이 된다면 오히려 대외 이미지를 돈 주고 갉아먹는 꼴이 된다.우리가 관심을 갖는 수 많은 일들은 평탄함과 평범함에서 오지 않는다. 때로는 매우 부정적인 가십거리가 귀에 익고 머릿속에 강인하게 자리 잡는다. 영화나 드라마, 소설에도 기승전결이 있다. 기업 PR에서도 기승전결이 필요하다. 하나의 드라마를 표현할 수 있어야 한다. 회사가 힘들 때도 과감하게 그것을 표현하고 그것을 어떻게든 극복하는 드라마를 대중들에게 보여줘야 한다. 보도자료 한 두개 더 나간다고 회사 이미지가 달라지지 않는다. 회사의 이미지는 매우 사소한 것이 모여서 만들어지는 것이다. 그리고 그 사소하게 보이는 것들을 매우 면밀하고 주도적으로  컨트롤할 수 있어야 PR을 효과적으로 해나갈 수 있다. 그것이 기업 선전의 본질이다. 이를 위해선 PR 담당자가 조직을 어느 정도 장악할 수 있어야 한다. 그들의 라이프 스타일에 대한 어느 정도의 통제 없이는 회사가 일관된 이미지를 구축해 나갈 수 없다. 전통적으로, 대중을 장악해야 하는 독재국가나 독점기업의 PR 담당자의 힘은 매우 막강했다. PR의 역할은 간단하기 때문에 오해가 없어야 한다. 독특한 기업의 이미지를 일관성있게 조금 느리더라도 면밀하게 주도적으로 만들어나가는 것이 PR의 핵심이다. 이를 위해선 수 많은 연구가 필요하다. 현재 트렌드가 어디서  기인했는지, 과거의 트렌드는 어떻게 흘러가는지, 왜 특정 사회현상이 사람들로부터 매우 폭발적으로 관심을  끄는지, 왜 스티브 잡스와 같은 영웅에 사람들은  집착하는지 등 연구해야 할 것이 태산이다.PR 담당자는 눈 앞의 ROI에 신경 쓸 필요가 없다고 생각한다. 대중이 회사를 바라보는 시각을 장악하는 일은 매우 느리게 진행될 수밖에 없다. 보도자료에 집착하게 되면 단기적 성과를 빠르게 알리는데 급급할 수밖에 없기 때문에 이미지의 일관성을 상실할 확률이 높다.조만간, 내가 수년간 연구한 PR와 심리학 등 대중심리 연구와의 연관성 등에 대해 포스팅하게 될 것 같다.군중심리를 연구한 사람들은 알겠지만, 이게 대외적으로 기술했을 때 매우 위험한 부분들이 많다. 그러나, 우리는 세상이 그렇게 아름답지 않다는 것을 인정해야 한다. 개인의 합리성이 집단으로 모였을 때 어떻게 변화되는지, 조금 불쾌하게 느껴질지라도 깊숙하게 연구할 필요가 있다. 그게 PR 담당자가 기업 선전을 위해 할 일이다. 
조회수 5978

안드로이드 앱의 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 #개발후기 #인사이트
조회수 523

스스로 성취한 경험의 가치

초등학교 시절이었다.스스로 무언가를 결정하는 것이 참 어려웠다.개인적인 취향을 묻는 질문에도 늘 다른 친구들의 의견을 힐끗힐끗 의식하고, 다수의 의견에 편승하는 것을 늘 편안하게 여겼다. 튀기 싫어하는 내성적인 성향 때문이기도 했겠지만, 나 스스로의 주관과 생각이 너무 부족하다는 생각에 어린 시절 꽤 진지하게 '자아'에 대해 고민했던 기억이 난다.중학교 때였다.공부에 대한 압박과 스트레스가 아주 아주 막연하고 먹먹하게 지배하던 초기였는데, '무작정 오래', '무작정 열심히'는 전혀 도움이 되지 않는 솔루션이라는 것을 직감적으로 알아챘던 것 같다. 스스로 '공부란 무엇인가'에 대해 깊이 깊이 고민을 했었고, 어느 순간 아주 단순한 스스로의 답을 가진 것이 내겐 꽤 뿌듯했던 기억으로 남아 있다. 그때 내가 정의했던 공부란, '내가 모른다는 것을 확인하는 과정'이라는 것과 그 과정을 통해 '알고 싶은 욕구를 자극하는 것'이 내 공부의 방법이었다. 모르긴해도 이 시절이 내 지적 수준이 가장 높았던 시절이었던 듯하다.고등학교 시절이었다.인생의 관문을 선택하는 중요한 시기였는데, 내 인생에 파격적인 진로 전환을 스스로 내렸다. 누구도 예상하지 못했던 첫 번째 큰 결정이었고, 그 때의 결정으로 지금의 인생을 살고 있다. 그 순간에도 순전히 내 스스로의 믿음과 판단에 의존했다.내가 다시 20대로 돌아간다면 꼭 해보고 싶은 일이 있다. 아마 지금 아쉬움이 남아 있기 때문일 것이다. 아무에게도 의지하지 않고 혼자서 해내는 성취감을 훨씬 많이 느껴보고 싶다.용기 없어서 해보지도 않았던 일, 잘 하지 못할까봐 주춤했던 선택, 나한테 도움이 안될 거라 속단했던 포기, 혼자서는 무리라고 여겼던 판단, 남에게 부담을 줄까 사양했던 결정들... 그런게 많이 아쉽다.20대에는 무엇이든 해보고 실패해도 되는 자유를 망설였다. 오히려 10대에 내렸던 스스로의 판단과 결단보다도 아쉬운 시절이 나의 20대 였다.스스로 판단했던 어린 시절보다도, 혼자서 실행하지 못했던 20대가 아쉽다. 막연히 무언가에 의지하고 있었고, 은근히 내 실체가 아닌 자존심 뒤에 숨어 있던 것 같다.심적으로 의지하지 않고, 내가 결정하고 내가 감당하는 선택. 그게 어른이 되는 길임을 지금은 너무나 확신한다.혼자서 해봤니?모든 두려움은 스스로 해보지 않아서이다.모든 불안함은 혼자 남겨질지도 모른다는 두려움 때문이다.어른이 된다는 것은 '자립'을 한다는 의미이다.공부를 하는 이유는 '스스로'살아갈 수 있다는 용기를 얻기 위함이고, 세상을 알게 된다는 것은 '내 맘대로 되는 일이 별로 없다'는 현실을 깨닫게 된다는 의미이다.날개를 펼칠 준비가 되었니?지금 의미있는 시간을 보내고 있다는 것은 스스로 둥지를 떠나 날아오르기 위한 준비를 하고 있다는 것이다. 스스로.스스로 날개짓을 하지 않으면누구도 날 수 없다.누구도 대신 날개짓을 해주지 못한다.누구도...
조회수 2213

시뮬레이션에서의 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#퍼즐데이터 #개발팀 #개발자 #개발후기 #인사이트
조회수 655

클라이언트가 내게 와인을 권했다.(feat.작업후기)

지난 2주간 새로운 프로젝트를 맡아서 일을 했답니다. 플젝의 내용은 이런 것이었어요. 회사소개 문구 좀 세련되게 고쳐달라. 음...그렇습니다. 회사소개서를 만들다보면 처음 의뢰는 디자인으로 오기 마련이예요. 하지만 정작 자료를 받아보면 디자인은 부차적인 문제죠. 일단은 내용이... 뭔 말인지 모르겠어!!... 또는 노잼이야!!.. 아니면 문맥이 이상해!! 또는 상투적이야!! 지나치게 노골적이거나!! ... 등등의 문제들이 있습니다.그래서 대부분은 텍스트부터 손대기 마련이랍니다. 이젠 익숙해져서 아예 앗싸리 처음부터 이렇게 텍스트 기획부터 들어가는 경우가 많아요. 이번 프로젝트는 특이하게 디자인말고 문구수정만 맡아서 하게 되었습니다. 사실 디자인은 꽤나 괜찮더라구요. 다만 뭐랄까...텍스트가 지나치게 평범해서 마치 체크남방에 뿔테안경, 카키색 카고바지를 착용하고 인케이스 백팩을 맨 착한오빠 느낌이랄까요.  일단 미팅부터 진행해보고자 강남구청역으로 슝슝 달려갔습니다.1.이번 클라이언트는 와인회사였어요. 소믈리에 양성교육과 와인유통, 콘텐츠제작등을 하고 있는 곳이죠. 건물에 1층은 오져버리게 세련된 바&카페였고 2,3층 교육장이 있고, 4층에 사무실이 있고..테라스도 있고... 뭐여. 이쁘잖아? 네, 건물이 예뻤습니다. 미팅은 1층 바에서 진행했어요.보통 대표님은 내향형대표님과 외향형대표님이 있는 듯 합니다. 이번 대표님은 전자에 가까웠어요. 그리 말이 많은 편도 아니었고 조용한 성격에 상당히 전문가느낌을 물씬 풍기는 그런 인상이셨죠. 하지만 정작 와인얘기가 나오면서부턴 각성한 마법전사마냥 눈이 반짝거리시더니 봇물 터져벌임.2.일단 전 와인을 1도 모릅니다. 물론 마셔보기는 했으나 이 맛이 저 맛이고 떫고 달다..정도를 구분할 수 있는 정도?.. 네, 혀가 있다면 누구나 구별할 수 있는 그 정도의 맛만 알고있는 상태였습니다. 게다가 비싼 와인일수록 떫다....라는 뜬소문이 장착된 상태라 이마트에서 파는 8,000원짜리 기획와인이나 꼴짝꼴짝 마시는 정도였죠. 술을 즐기긴 하지만 뭔가 와인은 선뜻 혼술로 즐기기엔 좀 뭐랄까.... 선입견이 좀 있었던 것 같아요. 이건 조낸 특별한 날에 까야해. 라는...?3.텍스트를 만들려면 일단 와인을 이해해야 했습니다. 이 술이 당최 뭔지 알아야 뭔가 구상을 하든 말든 할테니까요...그래서 일단 싸디싼 와인을 홀짝이며 와인책을 뒤적뒤적거리기 시작했어요.최근 개봉한 '부르고뉴, 와인에서 찾은 인생' 도 찾아 보았죠. 오우 영화가 상당히 재밌더라구요. 혹시 못보신 분들은 꼭 한 번 찾아보셨으면 합니다. 진심 그 영화보면 와인멍청이라고 해도 어느 순간 혜안이 열리는 듯한 기분입니다. 그리고 끝나면 와인이 땡기죠.개꿀잼입니다. 진심4.이번 컨셉은 와인은 '언어다!' 라는 컨셉이었어요. 사실 술이란 게 그렇잖아요. 소주는 소주를 마실 때 하는 대화가 있고, 맥주는 맥주 나름이 대화가 있습니다.뭔가 인생의 크으으으 쓴 맛을 느끼고 나눌 때는 소주가 제격이고...청춘의 짠내나는 한숨을 담은 편맥과 오땅....수다와 근황얘기에 적합한 수제맥주...비오는 날 거나하게 취하고 흥청이망청이 노래부르고싶은 막걸리..등등 술과 대화는 뗄레야 뗄 수 없거든요. 와인은 또 와인 나름대로의 대화가 있기 마련이죠. 그래서 언어라고 규정해 봤어요. 술자리는 꼭 목소리로 오고가는 대화 대신에 잔끼리 부딪히며 마시는 와중에 느껴지는 무언의 대화가 있기 마련이거든요. 중간에 뭔가 굉장히 어색해지면 '야야야 짠해 짠!' 이라고 끊어주는 역할을 하는 것처럼 말이예요. 뭔가 잔을 기울여 마신다는 것은 그 자체로 하나의 언어가 된다고 생각해요.짠해 짠.5.자 그래서...텍스트를 다듬기 시작했습니다. 진심 4줄 쓰는게 이렇게 힘든 건지 오랜만에 깨달았습니다.이렇게 일단 언어와 와인의 속성을 뽑아서 사랑의 작때기 마냥 서로 연관있는 것 끼리 연결시켜 주었어요. 은유라는 것은 유사속성끼리 서로 묶는 게 먼저거든요.그리고 각각 속성을 연결시켜 문장으로 만들어냈어요. 논리는 이런 식이었어요.'와인은 언어다.''언어는 사고방식과 행동을 규정한다.''와인은 우리의 삶을 바꾼다.'이런 3단 논법으로 갔던거죠. 몇몇 키워드들이 등장했어요. 오감, 깊어짐, 가벼움, 묵직함, 섹시함, 섬세함, 감각 등등..말이예요. 이제 이 녀석들을 문장안에 잘 녹여서 하나로 만들어야 해요. 이 때 만큼은 존윅에 나오는 총기소믈리에가 된 것같은 느낌이죠.그래서 기존 텍스트를 이렇게저렇게 바꾸고 만들고 난리를 쳤습니다. 자세한 과정은 재미가 없으니 생략하도록 할께요. 여튼 이렇게 14개의 사업영역에 대한 텍스트가 모두 만들어졌습니다. 텍스트를 만들 때는 몇가지를 고려해야해요!~일단 읽혀야 해요. 끝까지 읽히고 나선 찰져야 해요. 입천장에 달라붙은 양반김마냥 입에 챡!! 붙어야 하죠.  그리고 무엇보다 쉬워야 해요. 와인은 안그래도 전문가들만 알고있다라는 느낌이 강력한데 영어나 한자어가 수두룩하면 읽는 사람은 느에에에에 핵노잼! 하면서 뒤로가기를 누르기 마련이니까요. 그리고 입으로 말해도, 글로 써도 둘 다 어색하지 않은 글이면 더더욱 좋겠죵. 그래서 문장에 구성할 때 운율을 잘 짜요. 3.3.5라던지 3.4.3이라던지 음보를 잘 짜주면 딜리버리 쩌는 스피치를 만들 수 있어요. 그리고 라임도 잘 맞춰주면 좋아요. 이응이응이 가득한 어절에 하나씩 파열음이나 된소리를 넣어주면 엑센트가 살면서 일종의 리듬감을 만들어 준달까요.그렇게 머리를 두번짜고 세번짰더니!!  이런 것이 만들어졌어요!대표님은 맘에 든다고 끄덕이끄덕이를 하셨고(으아아아아...감사합니다!!!)  전 2주간 시달리던 긴장에서 봉인해제될 수 있었어요. 대표님께선 와인을 적극 권하셨습니다. 한 잔 잡숴봐~ 이런 느낌은 아니었고. 진심으로 와인이 삶을 보는 눈을 바꿀 수 있다고 믿고 계셨어요. 근데 사실 이 점은 저도 동일해요. 술이란 것은 대화를 동반한다고 했잖아요. 심지어 아무 말없이 술만 기울여도 뭔가 그 분위기라는 것이 전하는 메시지가 있기도 하구요. 이게 주종에 따라 조금씩 어투나 언어가 달라지는 느낌입니다. 분명 와인은 와인 나름의 대화와 분위기가 있기 마련이죠. 그리고 그 분위기를 이해하기 위해선 내 몸속에 들어가는 이 알싸한 것들이 당최 뭔지 이해해야 해요.와인을 배운 다는 건 내 몸속에 또 하나의 언어를 채워넣는 느낌이죠. 말로 내뱉는 언어가 아닌 혀와 코끝으로 느끼는 언어말이예요.그래서 말인데, 대표님이 그렇다면 이번 기회에 소믈리에 기초과정을 한 번 들어보라고 권하셨어요. 우왕굿이예요. 자랑입니다. 이제 더 이상 와인코르크도 제대로 못따서 코르크 빠뜨려서 둥실둥실 할 일은 없을 것 같아요.클라이언트 비즈니스를 한다는 게 장단점이 있습니다. 모든 디자이너와 콘텐츠제작자는 '나만의' 무언가를 만들고 싶어해요. 그건 숙명과도 같은 숙제죠. 내 것을 만들면서 느끼는 뿌듯함은 굉장한 쾌감을 줍니다. 하지만 그에 못지않게 클라이언트 비즈니스도 짜릿한 매력이 있어요. 다른 사람들이 몸담고 있는 세계를 맛볼 수 있잖아요. 특히 이번 프로젝트 처럼 전혀 관심도 없었던 영역을 '일을 하기 위해' 공부했다가 매력이를 느껴버리는 경우엔 더더욱요. 매번 새로운 일을 하시는 대표님들을 만날 때마다 새로운 세계를 조금씩 테이스팅하는 기분입니다. 이번엔 진짜로 알싸한 와인을 테이스팅 하게 될 것 같구요. 조만간 소믈리에 과정을 듣게되면 이 언어가 얼마나 기가 맥히게 매력적인지 꽐라가 되어서 글을 주저리 해보도록 하겠습니다. 역시 글 중에 글은 취중끄적 아니겠습니까.이렇게 또 하나의 일이 끝났습니다. :)물심양면으로 지원해주시고 생에 두번 없을 기회까지 제공해주신 와인비전 대표님께 감사드립니다!~ 딱히 돈을 받거나 광고하는 것은 아니지만 새로운 언어에 관심있으신 분이라면 한번쯤 들려보세요 :) http://winevision.kr/
조회수 1646

Microsoft, LINE WORKS 그리고 콜라비와 함께하는 협업툴 트렌드 세미나 

지난 7월 10일에 있었던 글로벌 협업툴 트렌드에 관한 세미나가 성황리에 마무리 되었습니다. 마이크로소프트, 라인웍스, 콜라비, 라이온아이스 등 국내외 협업 문화에 대해 오랫동안 주시해왔던 여러 전문가들의 인사이트에 대해 들어볼 수 있었던 뜻깊은 시간이었습니다.협업 세미나는 마이크로소프트 Greatwall 실에서 진행되었는데요. LS산전, SDS, NBP, SK텔레콤, 코오롱, 한화, 대한축구협회, 뉴스킨 코리아, 제일펑타이, 국토교통과학기술진흥원, 카이스트, 한국 웰스파고(Wells Fargo) 등 정말 다양한 곳에서 많은 분들이 협업 세미나를 찾아주셨습니다. 최근 도입된 52시간 제도와 더불어 실리콘밸리에서 불고 있는 "딥워크" 열풍 등, 글로벌 협업 트렌드에 대해서 많은 분들이 관심있게 지켜보고 계셨구나 하는 생각이 들었습니다.라인 웍스 - 모바일을 활용한 협업 생산성 향상라인 웍스 이우철 이사님께서는 협업툴의 트렌드가 어떻게 PC에서 모바일로 이동하고 있는지에 대한 인사이트를 공유해주셨습니다. 일본에서 이미 성공적으로 안착한 라인 웍스는 다양한 일본 내 기업들에 서비스를 제공하고 있습니다. 라인웍스를 도입한 일본 내 기업들은 외근이나 현장에서도 모바일을 통해 효율적으로 업무 처리를 할 수 있었다는 점에 주목하고 있다고 합니다. 국내의 경우에도 이전에는 이메일을 통한 협업이 메인이었다면 점차적으로 모바일로 협업 트렌드가 이동하고 있다고 합니다. 이데일리, MBC와 같은 국내 기업들이 라인 웍스로 전환하고 나서 보다 쉽게 협업을 할 수 있었다고 하는데요. 네이버 UI에 익숙한 국내의 경우 별도의 협업툴에 대한 교육이 필요 없어 빠르게 전환이 가능했다고 합니다.콜라비 - 실리콘밸리 협업툴 트렌드이어  콜라비의 조용상 대표님께서 실리콘밸리에서의 협업 트렌드에 대해 설명해주셨습니다. 사실 협업툴 트렌드는 미국이, 그리고 그 중에서도 실리콘밸리가 이끌다고 있다고 보아도 무방한데요. 우리가 잘 알고 있는 슬랙의 시발점도 실리콘밸리이고, 협업툴 시장의 공룡인 아틀라시안 역시 실리콘밸리 출신입니다. 그런 실리콘밸리에서 최근 새롭게 집중하고 있는 것이 바로 칼 뉴포트의 "딥 워크"라고 합니다. 콜라비는 메신저 때문에 하루에도 수십 조각으로 쪼개진 시간들에 집중했습니다. 메신저로 일할 경우 매 15분 마다 방해를 받고 있다는 사실, 알고 계셨나요? 다시 말해서 하루에 몰입을 제대로 할 수 있는 시간이 단 한시간도 없다고 하네요. 이런 사태를 방지하기 위해 콜라비는 메신저 기반이 아닌, 원페이지 기반으로 만들어졌습니다. 하나의 페이지 안에서 모든 업무를 몰입해서 처리할 수 있도록 말이죠. 라이온아이스 - 일본의 업무 혁신 방향성과 일본 기업의 협업툴 활용 현황라이온아이스의 허성욱 대표님께서는 일본에서의 협업툴 트렌드에 대한 인사이트를 공유해주셨습니다. 일본도 국내와 마찬가지로 근로자의 업무 시간에 제한을 두는 법률이 있는데요. 바로 월 잔업 45시간이라는 법입니다. 또한 완전고용상태를 이루는 현재 일본의 취업 시장과 인구 감소라는 문제 때문에 현재 일본 기업들은 어떻게 하면 더 효율적으로 일할 수 있는지에 대한 고민을 끊임 없이 하고 있다고 합니다.정부 보조금, 이민자 환영과 같은 정부 차원의 정책 외에도 기업 차원에서의 노력의 일환으로는 협업툴 도입이 활발하게 이루어지고 있다고 하는데요. 현재 일본내에서 가장 인지도가 높은 협업툴은 라인 웍스로, 일본 시장 점유율 1위라고 합니다. 실제로 비즈니스 챗(Business Chat) 시장의 규모도 매년 성장하고 있고, 이에 더해 허성욱 대표님은 앞으로 비즈니스 챗뿐만 아니라 원페이지 협업툴에 대한 니즈 역시 늘어날 것을 예상했습니다. 마이크로소프트 - 마이크로소프트 사례를 통해 본 기업문화변화 방향과 미래의 일하는 방식마지막으로 마이크로소프트의 박상준 부장님께서는 디지털 트랜스포메이션, 혹은 4차 혁명이라고도 알려진 새로운 세대에서의 협업 문화에 대해서 마이크로소프트의 사례를 통해 설명해주셨습니다. 미래의 근무 형태는 보다 더 다양해지고, 보안 수준의 향상 및 인공지능의 도입이 더욱 활발히 이루어지며, 이로 인해 일하는 방식 역시 변화가 촉구될 것이라고 합니다.마이크로소프트에 새로 취임한 CEO인 사티야 나델라는 "Know-it-all(뭐든지 다 아는)" 마인드 셋에서 "Learn-it-all(뭐든지 다 배우는)" 마인드셋으로 변화가 필요한 시점이라고 말했다고 하는데요. 또한 사무실 환경을 변화시키고 리모트 워크를 위한 툴을 제공하는 등 업무 생산성을 높이기 위한 마이크로소프트 내부적인 노력 역시 공유해주셨습니다. 그리고 마이크로소프트가 기존에 제공 되고 있는 툴의 형태에만 의존하지 않고 변화하는 업무 형태에 맞추어 발전해 나가는 모습에 대해서도 설명했습니다.  글로벌 협업툴 트렌드네 분 연사 모두 기존의 이메일로만 진행하는 업무는 더 이상 효율적이지 않다는 것에 동의했습니다. 그리고 라인웍스는 이에 대한 해답으로 모바일 기반의 협업툴을, 콜라비는 원페이지 협업툴, 그리고 마이크로소프트는 업무 방식의 혁신을 이야기했습니다. 또한 칼 뉴포트의 '딥워크 무브먼트'에 대한 이야기도 종종 언급되었습니다. 세미나에 참여하셨던 많은 분들께서도 변화하는 업무 방식과 이에 맞는 가장 효율적인 협업 트렌드에 대해 알아가실 수 있는 유익한 시간이 되었기를 바라며, 앞으로도 저희 협업툴 콜라비는 더 많은 기업들이 효율적으로 협업을 진행할 수 있도록 고민하고 노력하겠습니다. 협업툴 콜라비 알아보기
조회수 886

스타트업은 뭘 해도 욕을 먹는다

얼마 전 재미있는(& 공감되는....) 바이럴 영상 하나를 봤다.통통한 여자 BJ에게 누군가 키가 작고 통통한데 치마를 입어도 되겠냐고 질문을 하자, "살이 찐 사람들은 뭘 입어도 욕해요. 뚱뚱한 사람이 바지를 입으면 아 저렇게 뚱뚱한 애들은 치마를 입어야 허벅지가 덜 두꺼워보이는데 막 이러구요, 치마를 입고 돌아다니면 저 다리로 왜 치마를 입고 있어 이러구요, 검은색 달라붙는 티를 입으면 뚱뚱한 애가 왜 저렇게 달라붙는 티를 입고 있어 이러구요, 또 막 박스티나 흰색 티를 입으면 뚱뚱한 애들은 검은색 달라 붙는 옷을 입어야 말라보이지 막 이러구요. 그냥 무조건 욕을 먹거든요 뭘해도, 그냥 입으세요. 당당하게" 참 웃픈 현실이 아닐 수 없다 (영상링크: 뚱뚱한 사람들은 어디 서러워서 옷 입고 다니겠나)그런데 요즘 들어서는 좀 다른 의미에서 괜히 와닿는다.스타트업 또한 뭘 해도 욕을 먹는다놀면 논다고, 일하면 바쁜 척 한다고, 힘들어 하거나 고민하면 그러게 그렇게 힘든 걸 왜 하고 있냐고, 즐겁게 지내고 있으면 생각이 없다고, 실패하면 그럴 줄 알았다고, 이런 과정을 다 거쳐서 오랜 시간 끝에 지분율따라 큰 성취를 거두면 욕심 많고 이기적이라고, 다 그런건 아니지만 그런 소리를 누구에게든 꼭 한 번씩은 듣게 마련이다. 모두를 만족시킬 수는 없다.Haters hate, doubters doubt. 그러니 바깥 소리에 너무 신경쓰지 말고 자기 주관대로 제 갈 길을 가는 것이 옳다.스스로에게 당당할 수 있으면 된다.#라이비오 #스타트업 #마인드셋 #운영 #인사이트 #경험공유

기업문화 엿볼 때, 더팀스

로그인

/