스토리 홈

인터뷰

피드

뉴스

조회수 1056

[인터뷰] Humans of MEME, 그 마지막 주인공을 만나다. - 긍정의 힘을 지닌 듀크의 이야기

여러분 안녕하세요.미미박서의 평범하지만 특별한 이야기를 담아왔던 모뜨입니당!오홍 벌써 프로젝트의 마지막 이야기가 다가왔네요.Humans of MEME 의 마지막 주인공은바로 Global SCM 팀의 듀크입니다 !듀크의 솔직하고 담백한 이야기를들어보실까요 ?Q. 듀크가 담당하시는 업무인 SAP는 사내에서도 어렵다고 소문이 났는데요(쥬륵). SAP를 간략하게 소개해주신다면, 무엇인가요?A. 미미박스라는 회사가 원활하게 운영될 수 있도록 도와주는 시스템이 ERP(Enterprise Resource Planning : 전사적 자원 관리)이고 그 ERP 안에 여러가지 툴 중의 하나가 SAP이에요. 또 SAP에는 많은 프로그램들이 있는데, 그 프로그램을 개발하는 것이 abap 개발을 담당하고 있어요. 저는 컴퓨터를 전공하여 대학교 때부터 계속 컴퓨터만 해왔어요. SAP는 거의 대학교 과정에 없는 내용이라, 우연찮게 첫 직장에 들어가면서 처음 접했어요. 실무를 접하게 되면서 여러가지 상황에 대응하는 능력을 배우면서 적성에도 맞고 차차 젖어든 것 같아요. 전공에 따라 직업이 선택되기도 하지만 둘 사이의 직접적인 관련보다는 직업을 선택하는 것에 있어서 여러가지 경험 중의 한 단계인 것 같아요. 저도 컴퓨터가 전공이었지만 기획하고 여러가지 활동적인 일들도 하고 싶어서 찾아보기도 했었어요. 2가지 사이의 직접적인 연관은 없지만, 전공은 직업을 선택하는 데에 있어서 토대를 마련해주는 경험의 일종이라고 생각해요.  Q. 미미박스를 어떻게 만나게 되셨나요?A. 이전 직장 동료의 추천으로 미미박스에 합류하게 되었어요. 이전 직장의 동료들이 현재 미미박스의 동료들이기도 합니다(웃음). 저는 물론 하고 있는 업무도 중요하지만 동료와의 관계가 회사 생활의 50%를 차지한다고 생각해요. 동료와의 관계가 좋아야지 같이 시너지 효과를 내면서 분명히 업무 또한 잘 할 수 있는 것 같아요. 일도 마음도 잘 맞는 동료들과 함께 일을 하다보면 즐거운 일도 같이 공유하고 속상한 일이 있어도 서로 그때그때 풀 수 있어요. Q. 삶에서 도전적인 경험을 하신 적이 있으세요?A. 저는 늘 여린 외모때문에 주변 분들에게 약해보인다, 여려보인다 등 이런 얘기를 들은 적이 많아요. 그래서 그런지 몰라도 자꾸 무모한 도전을 해보려고 했던 과거 시절이 있었어요. 그 중의 하나로 대학교를 휴학한 후 자전거로 전국 일주를 다녀왔어요. 남들이 해보지 않은 경험을 해보고 싶었고 스스로 강해지고 싶다는 욕구도 있었어요. 저를 포함해서 친구들 3명과 같이 일주를 했어요. 저는 3이라는 숫자를 좋아해요. 2명이라면 싸울 수도 있는데 3명이라면 싸워도 2:1 이 되기 때문에 늘 그 자리에서 결론이 나거든요(웃음).서울에서 출발해서 미시령을 넘고, 강원도에서 부산으로 내려와, 부산에서 배를 타고 제주도를 갔어요. 제주도 한바퀴를 돌고 다시 배를 타고 목포에 도착했어요. 그렇게 목포에서 서울로 다시 올라왔습니다. 그렇게 총 한달 정도 걸렸어요.자전거로 한달 동안 전국을 돌면서 많은 사람들도 만났고 위험한 일도 많이 겪었어요. 무모하게 시작했던 것이지만 지금 돌이켜보면 가장 기억에 남고 제 자신의 한계를 시험해볼 수 있었던 것 같아요.자전거 전국일주를 하던 2002년의 듀크(좌)! WOWOWQ. 요즘 느끼시는 소소한 행복이 있으신가요?A. 최근에 아내가 아이를 출산했어요. 태어난지 현재 4개월 째가 되었는데 아이를 보는 낙에 살아가고 있어요. 제가 눈썹만 움직여도 아이는 꺄르르 웃으며 자지러지는데, 아이가 웃으며 결국 저도 웃거든요!저는 예전에는 운동하는 것이 특기이자 취미였어요. 이전에는 다른 즐거움이 분명히 있었는데 세월이 흐르다 보면서 또다른 즐거움을 맞이하고 있어요. 아내와 아이를 보면서 살아가는 데서 행복을 느끼고 에너지를 받는 것 같아요. Q. 듀크는 스스로 어떤 사람이고 싶으세요?A. 저는 늘 마음에 품고 있는 말이 있어요. 바로 ‘긍정의 힘’ 이라는 말이에요. 상황을 부정하고 의심하기보다 어려운 상황 속에서도 긍정적인 요소를 찾아낼 수 있어야 해요.먼저 긍정적인 마인드는 스스로를 변화시킬 수 있어요. 또한 저의 긍정적인 마인드를 통해 주변 사람들 또한 변화시킬 수 있는 것 같아요. 제가 긍정적인 에너지를 줌으로써 옆에 계신분들에게도 웃음을 전달할 수 있고 기쁜 순간들을 같이 할 수 있을 때 뿌듯해요. 앞으로도 저는 스스로에게도 긍정적으로, 주변 사람들에게도 긍정의 힘을 전파할 수 있는 사람이고 싶어요.듀크가 말한 긍정적인 마인드가 자신을 변화시키고나아가 주변 사람들도 변화시킬 수 있다는 힘과짧은 시간이나마 인터뷰를 진행하며 듀크의 긍정적인 기운을 느낄 수 있었어요 :)매일 행복할 수는 없지만행복한 일은 매일 있다는 말이 있듯이 여러분도 긍정의 힘을 믿어보시는 것은 어떠세요 !?이렇게 7번째 주인공 듀크를 마지막으로Humans of MEME 프로젝트가 끝나게 되었습니다.실화인가요?실화입니다.흫 여러분들은 이야기를 보며 어떠셨나요?저 모뜨는 인터뷰를 통해개인적으로나 회사의 속한 구성원으로서나새로운 자극을 받기도 하고 많이 성장할 수 있었던 시간이였습니다!판교 미미박스 본사 10층 플레이미미Humans of MEME 프로젝트는블로그에 올라오는 이야기 뿐만 아니라 미미박스 사내의 카페테리아에 매주마다 주인공들의 포스터가 붙여졌었답니다! (매주 포스터 구경하는 재미가 쏠쏠했다구여)Humans of MEME 는미미박서분들이 가장 많이 찾는 공간인 10층 플레이미미에서서로서로를 알아갈 수 있었던좋은 커뮤니케이션의 채널로서도 자리잡았었는데요!아쉽게도 프로젝트가 끝이 나게 되지만,미미박서 FOREVER 얍얍얍 미미박스 FOREVER 얍얍얍앞으로도 더 멋진 미미박서와 미미박스의 이야기로꾸준히 찾아오도록 하겠습니다 !안녕히계세요 !
조회수 5930

[SQL 데이터분석] 증감율 구하는 간단한 방법

sql에서는 = 등호가 비교연산자로 사용됩니다.대신 := 이렇게 콜론(:)과 등호(=)를 같이 쓰면 대입연산자로 쓸 수 있어요.select @prev := users.id // @prev 라는 임시변수에 users.id 값을 넣어라. from users가입일자로 사용자수를 구해보면, 아래처럼 가입일로 group_by 를 해서 구하죠.select date(created_at) as '가입일' , count(1) as '가입자수' from users group by 1 order by 1 desc;// 가입일 | 가입자수 // --------------------------- // 2017-08-02 100 // 2017-08-01 50그럼 전일 대비 증감율을 구하려면 어떻게 할까요?select date(created_at) as '가입일' , @prev as '전일 가입자수' , (count(1) - @prev) / @prev as '증감율' , @prev := count(1) as '가입자수' from users group by 1 order by 1 desc;// 가입일 | 전일 가입자수 | 증감율 | 가입자수 // -------------------------------------------------------- // 2017-08-02 50 1.0 100 // 2017-08-01 50 0 50증감율을 계산하는 count(1) / @prev까지는 @prev 에 전일 가입자수가 저장되어 있구요.@prev := count(1) 에서 당일 가입자수로 할당이 됩니다.저는 := 이 연산자를 알기 전엔 self-join 형태로 증감율을 구했는데데이터를 가오는 속도는 := 이 연산자가 훨씬 빠른것 같습니다.다음엔 self-join 으로 증감율을 구하는 법도 한 번 올려볼께요.#티엘엑스 #TLX #개발 #개발팀 #개발자 #꿀팁 #인사이트 #조언
조회수 2518

Radix? Redis!

얼마전부터 antirez twitter에서 radix tree 관련 트윗이 올라왔습니다. 얼마 지나지 않아 antirez가 radix tree를 구현한 rax 프로젝트를 공개하고 redis의 cluster hash_slot의 저장구조를 radix tree로 수정 되는것을 보았습니다.그동안 antirez의 코드 읽으면서 배우는 게 많았고, 자료구조에 관심이 많아서 살펴보기 시작했습니다. radix tree를 왜 구현 했는지, 어떻게 구현쟀는지 알아보고 radix tree를 redis에 어떻게 적용하였는지도 알아보겠습니다.antirez는 redis의 hash-slot -> key 구조에서 중복으로 인한 메모리 사용을 줄이기 위해 radix tree 를 만들었다고 합니다. 이 포스트에선 rax를 적용시킨 redis cluster로 이야기를 진행 하겠습니다.“현재는 hash-slot -> key에만 사용되지만 추후에는 다양한 곳에 사용 예정”이라는 트윗redis cluster?redis에는 cluster 기능이 있습니다.6대 이상의 redis 노드를 cluster 구성하면(최소 leader 3대, follower 3대 구성해야 cluster 가능) 16384개의 hash_slot이 노드 갯수에 맞게 분배가 됩니다. 즉 3대의 leader로 cluster 구성하면 각각의 leader는 0 ~ 5460, 5461 ~ 10922, 10923 ~ 16383 hash_slot을 나눠 가집니다.cluster 구성 후 client가 데이터 저장/삭제/조회 명령어를 redis server에 전송할 때 마다 key의 hash값을 구하고 어떤 leader hash_slot에 포함되는지 찾습니다.# example 127.0.0.1:7000> set hello world # hash_slot = crc16("hello") & 0x3FFF 계산된 값이 현재 접속한 leader의 hash_slot 범위에 있다면 그대로 실행 되지만 다른 leader의 hash_slot 이라면 에러를 발생하고 다른 leader로 이동하라고 힌트를 줍니다.cluster 구성 후에 노드를 추가 하거나 제거 할 경우 각 leader의 hash_slot을 재분배 하고, hash_slot에 맞게 key도 재분배 되어야 합니다. 단순하게 생각하면 leader의 hash_slot 재분배한 후 모든 key를 재계산하고 hash_slot에 맞는 leader에 할당 하는 겁니다.[현재까지 저장된 keys].forEach(v => { hash_slot = crc16(v) & 0x3FFF // leader에 할당된 hash_slot에 맞게 분배 }) 하지만 antirez는 redis Sorted set 데이터 타입의 구현체인 skiplist 을 이용하여 문제를 풀었습니다. skiplist는 member와 score를 저장하고, score를 기준으로 정렬합니다. skiplist의 member에는 key를 저장하고 score에는 key의 hash_slot을 저장합니다.(변수명 slots_to_keys)slots_to_keys 정보는 cluster 구성된 모든 노드가 저장합니다. 이후 재분배가 필요해지면 16384개 hash_slot을 leader 갯수에 맞게 재분배 하고 slots_to_keys에 저장된 “key:hash_slot” 정보를 가지고 해당 hash_slot의 key를 조회 및 재분배 합니다. 즉 slots_to_keys에 이용하여 재분배시 발생하는 계산을 없앤것입니다.잘 했구만 뭐가 문제냐?redis에 key가 추가/삭제 될때마다 slots_to_keys에 데이터가 저장되고 지워집니다. redis에 저장되는 key 갯수가 증가 할수록 slots_to_keys의 크기도 커짐을 의미 합니다.(※ 메모리 사용량)또한 leader 갯수에 맞게 16384개 hash_slot을 leader에 재분배하고, 각 hash_slot에 맞는 key를 찾고 할당 합니다. 예를들어 slots_to_keys에서 score 0인(hash_slot 0을 의미) member를 조회해서 0번 hash_slot에 할당, score 1인 member를 조회해서 1번 hash_slot에 할당 하는 방식으로 0 ~ 16383 hash_slot을 진행합니다.앞에서 말한 hash_slot에 속한 key를 조회 하는 GETKEYSINSLOT 명령어가 있는데 여기에 이슈가 있습니다.cluster GETKEYSINSLOT slot count # slot: hash_slot 번호 # count: 특정 hash_slot에서 조회할 key 갯수 # example 127.0.0.1:7000> cluster GETKEYSINSLOT 0 3 # 0번 hash_slot의 key를 3개 조회한다. "47344|273766|70329104160040|key_39015" "47344|273766|70329104160040|key_89793" "47344|273766|70329104160040|key_92937" 사용자가 특정 hash_slot에 몇개의 key가 저장 되었는지 모르기때문에 count에 Integer.MAX 를 대입하는데, redis는 hash_slot에 실제로 저장된 key 갯수와는 상관없이 client가 전달한 count만큼의 메모리를 할당합니다.} else if (!strcasecmp(c->argv[1]->ptr,"getkeysinslot") && c->argc == 4) { /* cluster GETKEYSINSLOT */ long long maxkeys, slot; unsigned int numkeys, j; robj **keys; // ... 명령어의 4번째 인자를 maxkeys에 할당, 즉 사용자가 입력한 count if (getLongLongFromObjectOrReply(c,c->argv[3],&maxkeys,NULL) != C_OK) return; // ... keys = zmalloc(sizeof(robj*)*maxkeys); numkeys = getKeysInSlot(slot, keys, maxkeys); addReplyMultiBulkLen(c,numkeys); for (j = 0; j < numkeys>zmalloc maxkeyscluster GETKEYSINSLOT unnecessarily allocates memory그래서 메모리도 적게 차지하면서(압축 가능) key와 key의 hashslot을 효율적으로 저장 및 조회가 가능한 자료구조가 필요했고 antirez는 radix tree를 선택합니다.※ 뜬금 없는데 2012년, redis 자료형에 Trie를 추가한 P/R이 생각났습니다.radix tree 구현한 rax 알아보기시작하기전 radix tree (Wikipedia) 위키 페이지의 그림을 보고 감을 잡은 후에 아래를 보시면 잘 읽힙니다.자! 이제부터 rax의 주석과 코드를 보면서 어떻게 구현됐는지 알아보겠습니다.Noderax의 노드 구성은 다음과 같습니다.typedef struct raxNode { uint32_t iskey:1; /* Does this node contain a key? */ uint32_t isnull:1; /* Associated value is NULL (don't store it). */ uint32_t iscompr:1; /* Node is compressed. */ uint32_t size:29; /* Number of children, or compressed string len. */ unsigned char data[]; } raxNode; 노드의 정보를 담고있는 32 bit(iskey, isnull, iscompr, size)와 key/value 그리고 자식 노드의 포인터를 저장하는 unsigned char data[]가 있습니다. 특이한 점은 key/value를 동일한 노드에 저장 하지 않고 key가 저장된 노드의 자식 노드에 value를 저장합니다.※ 사진 출처위 그림을 예로 32 bit 정보가 어떤걸 의미하는지 알아보겠습니다.iskey는 노드가 key의 종착역(iskey:1)인지 중간역(iskey:0)인지 나타내는 flag입니다. 1, 3 노드는 iskey:0 이고 2, 4, 5, 6, 7 노드는 iskey:1이 됩니다.isnull은 value의 null 여부를 표시합니다. unsigned char data[]에 key/value 그리고 자식 노드의 포인터를 저장하므로 value를 찾으려면 계산이 들어갑니다. 불필요한 연산을 줄이기 위해 만든 필드 같습니다.Trie는 각 노드에 한글자씩 표현 하지만 Radix는 압축을 통해 한 노드에 여러 글자 표현이 가능합니다. 이를 나태내는 플래그 iscompr 입니다. 노드가 압축된 노드(iscompr:1)인지 아닌지(iscompr:0)를 나타냅니다.size는 iscompr 값에 따라 의미가 다릅니다. iscompr이 1이면 저장된 key의 길이를 의미하고 iscompr이 0이면 자식노드의 갯수(저장된 key의 갯수)를 의미합니다.위 4개 정보를 이용해서 한 노드의 크기를 구하는 코드는 아래와 같습니다.#define raxNodeCurrentLength(n) ( \ sizeof(raxNode)+(n)->size+ \ ((n)->iscompr ? sizeof(raxNode*) : sizeof(raxNode*)*(n)->size)+ \ (((n)->iskey && !(n)->isnull)*sizeof(void*)) \ ) ※ 노드에 value 주소를 저장하거나, 마지막 자식 노드 포인터를 알고 싶을때 사용합니다.FindraxLowWalk 함수를 이용해 key가 존재 하는지 판단합니다.size_t raxLowWalk(rax *rax, unsigned char *s, size_t len, raxNode **stopnode, raxNode ***plink, int *splitpos, raxStack *ts) rax에 “ANNIBALE” -> “SCO” -> [] 로 저장 되어있을때 어떤 값을 리턴하는지 알아보겠습니다.*s 가 “ANNIBALESCO”이고 len이 11 인 경우# splitpos: 0, return value: 11 "ANNIBALE" -> "SCO" -> [] ^ | *stopnode *s가 “ANNIBALETCO”이고 len이 11인 경우# splitpos: 0, return value: 9 "ANNIBALE" -> "SCO" -> [] ^ | *stopnode *s의 길이 len과 return value가 같다면 rax에 key가 존재하는 것입니다. *s의 길이 len과 return value가 다른 경우 어디까지 매칭됐는지 보여주는 return value와 어떤 노드에 어디까지 일치했는지 표현하는 *stopnode, splitpos를 통해 추가 정보를 얻을수 있습니다.InsertraxLowWalk 함수를 이용해서 저장할 위치를 찾습니다. (*stopnode, splitpos, return value)1번에서 구해진 데이터를 이용해서 새로운 노드 생성 및 링크를 연결합니다.rax에 “ANNIBALE” -> “SCO” -> [] 상태에서 “ANNIENTARE”를 저장하는 과정입니다.1. raxLowWalk 함수를 이용하여 저장할 위치 탐색 splitpos: 4, return value: 4 "ANNIBALE" -> "SCO" -> [] ^ | *stopnode 2. *stopnode, splitpos 데이터를 이용하여 노드 분리 "ANNI" -> "B" -> "ALE" -> [] 3. iscompr: 0인 노드 "B"를 기준으로 새로운 key 저장 ("B"와 "E"는 같은 노드) |B| -> "ALE" -> [] "ANNI" -> |-| |E| -> "NTARE" -> [] RemoveraxLowWalk 함수를 이용해서 저장할 위치를 찾습니다. (*stopnode, splitpos, return value)1번에서 구해진 데이터를 이용해서 노드 제거 및 compress가 가능다면2가지 경우가 있습니다.마지막 노드만 iskey: 1이고, 연속으로 iscompr:1인 노드가 된 경우마지막 노드만 iskey: 1이고, iscompr:1 -> iscomplr:0 -> iscomplr:1 노드 구조가 된 경우입니다.첫번째 경우를 알아 보겠습니다. rax에 “FOO” -> “BAR” -> [] 상태에서 “FOO”를 지우는 과정입니다.1. raxLowWalk 함수를 이용하여 저장할 위치 탐색 splitpos: 3, return value: 3 "FOO" -> "BAR" -> [] ^ | *stopnode 2. 해당 key 삭제, 여기서는 자식노드가 있으므로 노드 삭제는 하지 않고 노드의 iskey: 0으로 세팅 "FOO" -> "BAR" -> [] 3. compress가 가능한 경우 진행 "FOOBAR" -> [] 두번째 경우를 알아 보겠습니다.0. "FOOBAR"와 "FOOTER"가 저장된 상황입니다. FOOTER를 지우는 경우입니다. |B| -> "AR" -> [] "FOO" -> |-| |T| -> "ER" -> [] 1. raxLowWalk 함수를 이용하여 저장할 위치 탐색 splitpos: 0, return value: 6 |B| -> "AR" -> [] "FOO" -> |-| |T| -> "ER" -> [] ^ | *stopnode 2. 해당 key 삭제 "FOO" -> "B" -> "AR" -> [] 3. compress가 가능한 경우 진행 "FOOBAR" -> [] cluster 정보는 어떻게 저장되나?기존 skiplist 자료구조를 이용했던게 어떻게 변경 되었는지 알아보겠습니다.server.cluster->slots_keys_count[hashslot] += add ? 1 : -1; if (keylen+2 > 64) indexed = zmalloc(keylen+2); indexed[0] = (hashslot >> 8) & 0xff; indexed[1] = hashslot & 0xff; memcpy(indexed+2,key->ptr,keylen); if (add) { raxInsert(server.cluster->slots_to_keys,indexed,keylen+2,NULL,NULL); } else { raxRemove(server.cluster->slots_to_keys,indexed,keylen+2,NULL); } 먼저 slots_keys_count 변수를 이용하여 각 hash_slot의 key 갯수를 저장합니다.그리고 key는 hash_slot(2 byte) + key, value는 NULL로 rax에 저장하여 특정 hash_slot에 속한 key 조회를 쉽게 만들었습니다.마치며rax 구현과 rax가 어떻게 redis에 적용됐는지 보면서 오랜만에 재밌게 코드를 읽은것 같습니다. 개인적으로 데이터 관련 유용한 무언가를 만드는게 목표인데, 이런 좋은 코드들을 하나 둘씩 제것으로 만드는것도 과정이라 생각하며 진행했습니다.앞으로 rax가 redis에서 어떻게 쓰일지 흥미롭고, Redis를 Saas 형태로 제공하는 업체들이 언제 적용할지도 궁금합니다.긴 글 읽어주셔서 감사합니다.cluster, rax 관련 antirez twitterRedis cluster Insertion cluster Issuesame amount data hash table vs radix treehashset + ziplist -> radix tree + listpack 1/5replace Hashset with Radix treeraxNode에서 사용한 flexible memberflexible memberrax 를 이용한 Redis Streams(2017.12.17일 업데이트)Redis Stream#잔디 #토스랩 #JANDI #기술스택 #도입후기 #Redis #인사이트
조회수 1523

서비스 중단 없이 Amazon EKS로 옮긴 이야기 - VCNC Engineering Blog

Amazon EKS는 AWS의 관리형 Kubernetes 서비스입니다. 2017년 11월 AWS re:Invent에서 프리뷰 버전이 출시되었고, 2018년 6월에 상용(GA) 버전이 미국 리전에만 출시되었습니다. 그래서 서울 리전을 사용해야 했던 타다 프로젝트에서는 Kubernetes 클러스터를 직접 kops로 설치하여 운영할 수 밖에 없었습니다.2019년 1월, 오랜 기다림 끝에 드디어 서울 리전에 EKS가 출시되어 기쁜 마음으로 EKS로 옮겨가게 되었습니다. 이 글에서는 직접 구축한 클러스터 대비 EKS의 특징에는 어떤 것이 있는지 살펴보고, 서비스 중단 없이 EKS로 옮기기 위한 전략을 공유하고자 합니다.EKS 서울 리전 출시를 염원하던 한국인(?)들EKS는 뭐가 다른가요?AWS에서 마스터 노드를 관리해줍니다.Kubernetes 클러스터는 마스터 노드와 워커 노드로 구성되어 있습니다. EKS는 이 중에서 마스터 노드를 직접 EC2로 띄울 필요 없이 AWS에서 관리해주는 서비스입니다. RDS를 사용할 때 직접 DB 인스턴스를 생성하지 않는 것과 비슷합니다. 별도의 설정 없이도 알아서 여러 가용 영역에 마스터 노드를 실행하여 HA(고가용성) 구성을 해주고, 비정상 마스터 노드를 자동으로 감지하고 교체합니다. 또한 자동화된 버전 업그레이드 및 패치를 지원합니다. EKS를 사용하더라도 워커 노드는 직접 EC2 인스턴스를 생성·관리해야 합니다.EKS 클러스터의 요금은 2019년 2월 현재 시간당 $0.20입니다. 타다에서는 기존에 t2.medium 3대를 마스터 노드로 사용하고 있었기 때문에 관리를 직접 하지 않는 대신 비용이 약간 증가하게 되었습니다.AWS IAM 기반 인증을 사용합니다.VCNC에서는 기존에 Kubernetes API에 접속할 때 가장 간단한 basic auth 인증 방식을 사용했습니다. 그 대신 외부 네트워크에서 접근할 수 없게 해두고 필요한 경우 Bastion 호스트를 통해 SSH 터널링하여 접속했습니다.EKS의 API 서버는 인터넷에 노출되어 있으며, 별도로 네트워크 접근 제한 설정을 할 수 없고 AWS IAM으로 사용자를 인증합니다. (물론 공개망에 노출되어 있으면 Kubernetes API 서버에 보안 취약점이 발견되는 경우 안전하지 않을 수 있는 단점이 있습니다. 앞으로 PrivateLink가 지원되면 해결될 것입니다.)IAM은 인증에만 사용되고, 특정 작업을 할 수 있는 권한은 Kubernetes 기본 RBAC로 관리됩니다. IAM 사용자나 역할을 RBAC 그룹에 매핑할 수 있습니다.EKS 인증 흐름도워커 노드 당 Pod 개수 제한이 있습니다.예를 들어 c5.large 인스턴스에는 29개의 Pod을 띄울 수 있습니다. (표 참고) 그러므로 기존 클러스터에서 노드 당 Pod이 몇 개나 되는지 미리 확인할 필요가 있습니다. 왜 이런 제약이 있을까요?Kubernetes에서는 네트워킹 플러그인으로 Pod 사이에 네트워크 통신하는 방식을 다양하게 설정할 수 있습니다. EKS는 기본적으로 amazon-vpc-cni-k8s를 사용합니다. 이 네트워킹 플러그인은 VPC 상에서 유효한 실제 IP를 Pod에 할당합니다.그러기 위해서는 하나의 EC2 인스턴스에서 여러 개의 IP를 받아와야 하고, 이를 위해 추가적인 네트워크 인터페이스(ENI)를 붙입니다. 그런데 인스턴스 타입에 따라 추가할 수 있는 ENI 수와 ENI 당 IP 수에 제한이 있습니다. 따라서 이 제한이 워커 노드 하나에 띄울 수 있는 Pod 개수 제한이 됩니다.flannel 등 오버레이 네트워크 기반의 다른 네트워크 플러그인을 사용하면 이러한 제약을 피할 수 있습니다. 하지만 EKS에서 기본 제공하는 방법을 그대로 사용하는 것이 좋고, Pod을 엄청나게 많이 띄워야 하는 상황이 아니어서 시도하지 않았습니다.EKS로 중단 없이 넘어가기개요타다의 Kubernetes 클러스터에서 돌아가는 서비스들은 모두 영속적인(persistent) 상태를 가지고 있지 않습니다. 따라서 EKS 클러스터 위에 동일한 서비스를 띄우고 외부 트래픽을 옮겨주기만 하면 특별히 데이터를 옮기지 않고도 이전이 가능했습니다. 또한 거의 대부분의 Kubernetes 리소스는 Helm 차트로 생성한 것이기 때문에 새로운 클러스터에 동일한 서비스를 띄우는 작업도 쉽게 할 수 있었습니다.이전 작업은 다음과 같은 순서로 진행했습니다.EKS 클러스터를 만들고 워커 노드를 생성모든 서비스 다시 설치트래픽을 새 클러스터로 보내기이전 클러스터 제거EKS 클러스터를 만들고 워커 노드를 생성타다의 AWS 환경은 거의 모두 Terraform으로 정의되어 관리되고 있습니다. EKS 클러스터와 워커 노드도 HashiCorp Learn의 문서를 참고해서 Terraform으로 생성했습니다. 해당 문서에 설명이 잘 되어 있어서 거의 그대로 따라할 수 있었습니다.EKS 클러스터 설정은 재사용 가능하도록 Terraform 모듈로 만들었습니다. 덕분에 테스트용 클러스터와 실서비스용 클러스터를 동일한 모듈로 변수만 바꿔서 설정할 수 있었습니다.모든 서비스 다시 설치타다의 Kubernetes 리소스는 Helm 차트로 관리되고 있어서 기존 차트를 거의 그대로 설치할 수 있었습니다. 사용자에게 직접적인 영향을 덜 주는 워커 서비스를 먼저 설치해서 제대로 동작하는 것을 확인한 뒤, 마지막으로 프론트엔드 서비스를 설치하였습니다.트래픽을 새 클러스터로 보내기타다의 모든 트래픽은 NLB로 들어온 뒤 NGINX를 거쳐 다시 적절한 Pod에 라우팅됩니다. 그러므로 타다의 모든 도메인은 NLB를 가리키고 있습니다.타다는 Route 53을 DNS 서버로 사용합니다. Route 53에는 가중치 기반 DNS 레코드를 설정할 수 있습니다. 이를 이용하여 일부 트래픽만 새 클러스터의 NLB로 보낼 수 있습니다. 처음에는 아주 적은 트래픽만 새 클러스터로 보내다가 문제 없이 작동하는 것을 확인한 다음 조금씩 트래픽을 늘려나갔습니다.DNS 가중치 설정으로 일부 트래픽만 새 클러스터의 NLB로 보낼 수 있습니다.DNS 설정에서 이전 클러스터로 가는 레코드를 완전히 제거한 뒤에도, DNS 캐시 등의 이유로 일부 클라이언트가 이전 클러스터에 접속할 수도 있습니다. 따라서, 이전 클러스터 NLB에 새 클러스터의 노드들을 붙여서 아직 DNS를 따라오지 못한 클라이언트들의 요청을 처리하였습니다.이전 클러스터 제거가장 신나면서 조심해야 하는 작업입니다. 먼저 이전 클러스터로 트래픽이 전혀 들어오지 않는 것을 확인하였습니다. 그 다음에는 Terraform에서 이전 클러스터 리소스에 대한 참조를 제거한 뒤, terraform destroy 명령으로 이전 클러스터와 관련된 리소스를 한번에 삭제할 수 있었습니다.맺음말Kubernetes는 깔끔한 추상화를 통해 컨테이너 기반 배포를 간단하게 만들어주지만, 직접 클러스터를 관리해야 하는 부담이 있었습니다. Amazon EKS는 이러한 부담을 많이 덜어주는 좋은 서비스입니다. 앞으로 EKS의 무궁한 발전을 기원합니다.VCNC에는 오랫동안 쌓아온 AWS 인프라 운영 경험이 있습니다. 타다에서는 그동안의 경험과 비교적 최근에 시작한 프로젝트의 이점을 살려 컨테이너, Infrastructure as Code 등 업계 표준의 인프라 관리 방법론을 적극 도입하려고 노력하고 있습니다. 앞으로도 이에 관해 기술 블로그에 더 자세히 공유할 계획이니 기대해주세요. 또한 저희와 함께 안정적인 서비스를 만들어나갈 좋은 분들을 기다리고 있으니 VCNC 채용에도 많은 관심 부탁드립니다.
조회수 1191

레진 기술 블로그 - 자바 기반의 백엔드와의 세션 공유를 위한 레일즈 세션 처리 분석

레일즈 기반의 프론트엔드(브라우저에서 서버 사이드 렌더링 계층까지)와 자바 기반의 백엔드(내부 API와 그 이후 계층)이 세션을 공유하기 위해 먼저 레일즈의 세션 처리 과정을 분석하고, 레일즈 세션 쿠키를 다루기 위한 자바 소스 코드를 공유합니다.여기저기 자랑하고 다녔으니 아시는 분은 아시다시피 레진은 구글앱엔진을 사용하고 있습니다. 지금이야 Java, Python, Node.js, Go 언어와 Flexible Environment 같은 다양한 선택지가 있지만, 레진이 입주할 당시만 해도 Java 7(subset), Python(subset)을 지원하는 Standard Environment라는 선택지 밖에 없었죠.최근 Saemaeul Undong 기술 부채 탕감의 일환으로 자바7, 스프링3.x, JSP(!) 기반의 백엔드에 포함되어 있던 프론트엔드를 레일즈 기반의 프론트엔드 서버(서버 사이드 렌더링을 담당하는 서버는 프론트일까요? 백엔드일까요?)로 분리하고 있습니다.서로 다른 세계의 존재들 - 자바와 레일즈의 세션을 공유해야하는 상황이 문제의 발단입니다.자바와 레일즈의 세션을 공유하는 여러가지 방법이 있겠지만, 가장 단순하고 효과적인 방법은 쿠키(cookie)라고 판단하고, 세션 encrypt/decrypt와 marshal/unmarshal을 동일한 방식으로 맞추기로 했습니다. (백엔드 API를 완전히 stateless하게 새로 만들면 좋겠지만, 코인은 벌어야 소는 키워야죠)이를 위해 레일즈의 세션 처리 과정을 분석하고 정리했습니다.레일즈의 actionpack의 action_dispatch/middleware/cookie.rb를 보면 EncryptedCookieJar 클래스의 초기화 과정은 다음과 같습니다(digest의 경우 따로 지정안하면 SHA1이 사용되는 듯):class EncryptedCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars def initialize(parent_jar) super if ActiveSupport::LegacyKeyGenerator === key_generator raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + "Read the upgrade documentation to learn more about this new config option." end secret = key_generator.generate_key(request.encrypted_cookie_salt || '') sign_secret = key_generator.generate_key(request.encrypted_signed_cookie_salt || '') @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) end private def parse(name, encrypted_message) debugger deserialize name, @encryptor.decrypt_and_verify(encrypted_message) rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end def commit(options) debugger options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end end key_generator는 EncryptedCookieJar에 포함된 SerializedCookieJars 모듈에 정의되어 있습니다:module SerializedCookieJars # ... def key_generator request.key_generator end end 흠… 좀 더 파보죠. request.key_genrator는 다음과 같습니다:class Request # ... def key_generator get_header Cookies::GENERATOR_KEY end #... end 흠… 좀 더 파야할 듯 ㅠㅠ.Cookies::GENERATOR_KEY는 다음과 같습니다:class Cookies #... GENERATOR_KEY = "action_dispatch.key_generator".freeze end action_dispatch.key_generator는 레일즈의 엔진 모듈에 해당하는 railties의 application.rb에 정의되어 있습니다:def key_generator # number of iterations selected based on consultation with the google security # team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220 @caching_key_generator ||= if secrets.secret_key_base unless secrets.secret_key_base.kind_of?(String) raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String, change this value in `config/secrets.yml`" end key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000) ActiveSupport::CachingKeyGenerator.new(key_generator) else ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token) end end # ... def env_config @app_env_config ||= begin validate_secret_key_config! super.merge( # ... "action_dispatch.key_generator" => key_generator, "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt, "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt, "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer, "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest ) end end 너무 깊이 판 느낌적느낌(?)이 있지만, 여기까지 왔으니 좀 더 파보겠습니다.핵심 알고리즘은 activesupport의 key_generator.rb, message_encryptor.rb, message_verifier.rb에 정의되어 있습니다.먼저, key_generator.rb의 핵심은 다음과 같습니다:class KeyGenerator def initialize(secret, options = {}) @secret = secret # The default iterations are higher than required for our key derivation uses # on the off chance someone uses this for password storage @iterations = options[:iterations] || 2**16 end # Returns a derived key suitable for use. The default key_size is chosen # to be compatible with the default settings of ActiveSupport::MessageVerifier. # i.e. OpenSSL::Digest::SHA1#block_length def generate_key(salt, key_size=64) OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size) end end 계속해서, message_encryptor.rb의 핵심은 다음과 같습니다:def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end def _encrypt(value) cipher = new_cipher cipher.encrypt cipher.key = @secret # Rely on OpenSSL for the initialization vector iv = cipher.random_iv encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" end def _decrypt(encrypted_message) cipher = new_cipher encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret cipher.iv = iv decrypted_data = cipher.update(encrypted_data) decrypted_data << cipher.final @serializer.load(decrypted_data) rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end def encrypt_and_sign(value) verifier.generate(_encrypt(value)) end def decrypt_and_verify(value) _decrypt(verifier.verify(value)) end (Hopefully)마지막으로, message_verifier.rb의 핵심은 다음과 같습니다:def initialize(secret, options = {}) raise ArgumentError, 'Secret should not be nil.' unless secret @secret = secret @digest = options[:digest] || 'SHA1' @serializer = options[:serializer] || Marshal end def valid_message?(signed_message) return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? data, digest = signed_message.split("--".freeze) data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end def verified(signed_message) if valid_message?(signed_message) begin data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} raise end end end def generate(value) data = encode(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end private def encode(data) ::Base64.strict_encode64(data) end def decode(data) ::Base64.strict_decode64(data) end def generate_digest(data) require 'openssl' unless defined?(OpenSSL) OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data) end # ... # encode, decode는 base64사용 이제 레일즈가 쿠키 기반의 세션을 어떻게 처리하는지 조금 눈에 들어옵니다. 그러나 우리의 최종 목표는 레일즈의 내부를 공부하는 것이 아니라, 자바에서 동일한 처리를 하는 것입니다. 모듈 의존성 따위는 가볍게 무시하고 무한복붙(?)을 시전해서, 레일즈의 세션 처리 과정을 눈으로 확인할 수 있도록 재구성했습니다:require 'openssl' require 'base64' require 'concurrent/map' class Object def blank? respond_to?(:empty?) ? !!empty? : !self end def present? !blank? end end class Hash # By default, only instances of Hash itself are extractable. # Subclasses of Hash may implement this method and return # true to declare themselves as extractable. If a Hash # is extractable, Array#extract_options! pops it from # the Array when it is the last element of the Array. def extractable_options? instance_of?(Hash) end end class Array def extract_options! if last.is_a?(Hash) && last.extractable_options? pop else {} end end end module SecurityUtils def secure_compare(a, b) return false unless a.bytesize == b.bytesize l = a.unpack "C#{a.bytesize}" res = 0 b.each_byte { |byte| res |= byte ^ l.shift } res == 0 end module_function :secure_compare end class KeyGenerator def initialize(secret, options = {}) @secret = secret # The default iterations are higher than required for our key derivation uses # on the off chance someone uses this for password storage @iterations = options[:iterations] || 2**16 end def generate_key(salt, key_size=64) OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size) end end class CachingKeyGenerator def initialize(key_generator) @key_generator = key_generator @cache_keys = Concurrent::Map.new end # Returns a derived key suitable for use. def generate_key(*args) @cache_keys[args.join] ||= @key_generator.generate_key(*args) end end class MessageVerifier class InvalidSignature < StandardError; end def initialize(secret, options = {}) raise ArgumentError, 'Secret should not be nil.' unless secret @secret = secret @digest = options[:digest] || 'SHA1' @serializer = options[:serializer] || Marshal end def valid_message?(signed_message) return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? data, digest = signed_message.split("--".freeze) data.present? && digest.present? && SecurityUtils.secure_compare(digest, generate_digest(data)) end def verified(signed_message) if valid_message?(signed_message) begin data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} raise end end end def verify(signed_message) verified(signed_message) || raise(InvalidSignature) end def generate(value) data = encode(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end private def encode(data) ::Base64.strict_encode64(data) end def decode(data) ::Base64.strict_decode64(data) end def generate_digest(data) require 'openssl' unless defined?(OpenSSL) OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data) end end class MessageEncryptor module NullSerializer #:nodoc: def self.load(value) value end def self.dump(value) value end end class InvalidMessage < StandardError; end OpenSSLCipherError = OpenSSL::Cipher::CipherError def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! sign_secret = signature_key_or_options.first @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end def encrypt_and_sign(value) verifier.generate(_encrypt(value)) end def decrypt_and_verify(value) _decrypt(verifier.verify(value)) end def _encrypt(value) cipher = new_cipher cipher.encrypt cipher.key = @secret # Rely on OpenSSL for the initialization vector iv = cipher.random_iv encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" end def _decrypt(encrypted_message) cipher = new_cipher encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret cipher.iv = iv decrypted_data = cipher.update(encrypted_data) decrypted_data << cipher.final @serializer.load(decrypted_data) rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end def new_cipher OpenSSL::Cipher.new(@cipher) end def verifier @verifier end end #key generate encrypted_cookie_salt = 'encrypted cookie' encrypted_signed_cookie_salt = 'signed encrypted cookie' def key_generator secret_key_base = 'db1c366b854c235f98fc3dd356ad6be8dd388f82ad1ddf14dcad9397ddfdb759b4a9fb33385f695f2cc335041eed0fae74eb669c9fb0c40cafdb118d881215a9' key_generator = KeyGenerator.new(secret_key_base, iterations: 1000) CachingKeyGenerator.new(key_generator) end # encrypt secret = key_generator.generate_key(encrypted_cookie_salt || '') sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt || '') encryptor = MessageEncryptor.new(secret, sign_secret, digest: 'SHA1', serializer: MessageEncryptor::NullSerializer) value = "{\"session_id\":\"6022d05887d2ab9c1bad8a87cf8fb949\",\"_csrf_token\":\"OPv/LxbiA5dUjVsbG4EllSS9cca630WOHQcMtPxSQUE=\"}" encrypted_message = encryptor.encrypt_and_sign(value) #encrypted_message = encryptor._encrypt(value) p '-----------encrypted value-------------' p encrypted_message # decrypt encrypted_message = 'bDhIQncxc2k0Rm9QS0VBT0hWc3M4b2xoSnJDdkZNc1B0bGQ2YUhhRXl6SU1oa2c5cTNENWhmR0ZUWC9zN05mamhEYkFJREJLaDQ3SnM3NVNEbFF3ZVdiaFd5YXdlblM5SmZja0R4TE9JbDNmOVlENHhOVFlnamNVS2g1a05LY0FYV3BmUmRPRWtVNUdxYTJVbG5VVUlRPT0tLXd1akRqOU1lTTVneU9LTWszY0I5bFE9PQ==--b0a57266c00e76e0c7d9d855b25d24b242154070' p '-----------decypted value-------------' puts encryptor.decrypt_and_verify encrypted_message p '---------------------------------------' 이 과정을 자바로 구현한 소스는 생략 깃헙에 올려두었습니다. 이 코드를 이용해서 서블릿 세션과 연동하는 방법은 추후 사측(?)과 협의되는 대로 공유할 예정입니다. 물론, 그 전에 쿠키를 공유할 필요가 없어지면(or 공유할 쿠키가 없어지면) 더 좋겠죠 :D
조회수 1398

SQS + Lambda

Overview안녕하세요. 저는 브랜디 R&D 본부 개발1팀의 기둥을 담당하는 이상근입니다. 오늘은 SQS(Simple Queue Service)와 Lambda를 간단한 예제와 함께 정리해보려고 합니다. 각 서비스에 대한 설명은 이미 매뉴얼로 쉽게 정리되어 있으므로, 이번 글에서는 서비스 간 구성을 집중적으로 살펴보겠습니다.1)SQS와 Lambda에 대하여SQS(Simple Queue Service)는 마이크로 서비스와 분산 시스템, 그리고 서버리스 애플리케이션을 쉽게 분리하고 확장할 수 있는 ‘완전관리형 메시지 대기열 서비스’입니다. 그리고 Lambda는 ‘이벤트 처리 방식의 서버리스 컴퓨팅 서비스’입니다. 아래 그림은 SQS와 Lambda Function을 이용해 메시지를 등록-조회-처리하는데 필요한 구성요소를 정리한 것입니다. SQS, Lambda ArchitectureProducer - 처리할 작업 메시지를 SQS에 등록Trigger - 큐(Queue) 대기열에 있는 메시지들을 조회하기 위해 CloueWatch의 스케줄 이벤트를 이용하여 매 분마다 Lambda Consumer 실행Consumer - Lambda Consumer는 큐 대기열에 있는 메시지 목록을 조회하여 각 메시지를 Lambda Worker에서 처리할 수 있도록 실행Worker - Lambda Worker는 메시지를 받아 작업을 처리하고 해당 메시지를 삭제큐 생성하기이번에는 큐 생성에 대해 살펴보겠습니다. ‘Create New Queue’를 클릭했을 때 지역(Region)에 따라 각각 다른 화면이 노출됩니다. Create New Queue Button타입 선택 화면항목 입력 화면두 번째 이미지와 같이 SQS에서는 Standard, FIFO 두 가지 타입을 제공하고 있습니다. 표준 대기열은 순서에 맞지 않게 메시지가 전송될 수 있습니다. 만약 순서를 반드시 유지해야 한다면 FIFO 대기열을 사용하거나, 순서 정보를 추가하고 사용해야 합니다. 하지만 FIFO 대기열의 경우 현재 미국 동부(버지니아 북부), 미국 동부(오하이오), 미국 서부(오레곤) 및 EU(아일랜드) 지역(Region)이서만 제공되고 있기 때문에 다른 곳에서는 사용할 수 없습니다. 2) 3) 1.Create New Queue ‘Create New Queue’에는 여러 항목이 있습니다. 우선 아래를 참조하여 각 항목에 적절한 내용을 기재합니다. Default Visibility Timeout : 대기열에서 조회한 메시지가 중복 조회되지 않기 위한 시간Message Retention Period : 메시지 보관 기간Maximum Message Size : 메시지 최대 사이즈Delivery Delay : 신규 메시지 전달 지연 시간Receive Message Wait Time : 조회된 메시지가 없을 경우, 사용 가능한 메시지를 기다리는 long polling 시간 설정Dead Letter Queue Settings : 정상적으로 처리되지 못한 메시지를 보관하기 위하여 메시지 수신 최대 수를 지정, 지정한 수신을 초과할 경우 지정한 큐에 메시지 저장2.큐 등록 확인 기본 값으로 설정한 큐 등록을 확인합니다. Queue List3.SQS 메시지 등록 import boto3, json sqs_client = boto3.client(     service_name='sqs',     region_name='xxxxxx' ) SQS 메시지 등록  response = sqs_client.send_message(     QueueUrl='https://sqs.xxxxxx.amazonaws.com/xxxxxx/sqs-test-1',     MessageBody='메시지 내용' )   print(json.dumps(response))   {"MD5OfMessageBody": "xxxxxxx", "MessageId": "xxxxx-xxxx-xxxxxx", "ResponseMetadata": {"RequestId": "xxxxxxx", "HTTPStatusCode": 200, "HTTPHeaders": {"server": "Server", "date": "Fri, 09 Feb 2018 08:01:13 GMT", "content-type": "text/xml", "content-length": "378", "connection": "keep-alive", "x-amzn-requestid": "xxxxxxx"}, "RetryAttempts": 0}} 4.AWS Console 메시지 등록 확인 View MessageDetail Message5.조회와 실행 1)SQS 메시지를 조회합니다.2)LambdaWorker 함수를 실행하고 > InvocationType으로 동기, 비동기 등의 실행 유형을 설정합니다. import boto3, json   def handle(event, context):     queue_url = 'https://sqs.xxxxxx.amazonaws.com/xxxxxx/sqs-test-1' sqs_client = boto3.client(         service_name='sqs',         region_name='xxxxxx'     )      lambda_client = boto3.client(         service_name='lambda',         region_name='ap-northeast-1'     )      # SQS 메시지 조회     response = sqs_client.receive_message(         QueueUrl=queue_url,         MaxNumberOfMessages=10,         AttributeNames=[             'All'         ]     )      print(json.dumps(response))      # {"Messages": [{"MessageId": "xxxxx-xxxx-xxxxxx", "ReceiptHandle": "xxxxx-xxxx-xxxxxx", "MD5OfBody": "xxxxxxx", "Body": "\uba54\uc2dc\uc9c0 \ub0b4\uc6a9", "Attributes": {"SenderId": "xxxxxxx", "ApproximateFirstReceiveTimestamp": "1518163931724", "ApproximateReceiveCount": "1", "SentTimestamp": "1518163466941"}}], "ResponseMetadata": {"RequestId": "", "HTTPStatusCode": 200, "HTTPHeaders": {"server": "Server", "date": "Fri, 09 Feb 2018 08:12:11 GMT", "content-type": "text/xml", "content-length": "1195", "connection": "keep-alive", "x-amzn-requestid": "xxxxxxx"}, "RetryAttempts": 0}}      for message in response['Messages']:         payload = {'message': message, 'queueUrl': queue_url}          # Lambda Worker 함수 실행         lambda_client.invoke(             FunctionName='lambda_worker',             InvocationType='Event',             Payload=json.dumps(payload)         ) 6.Lambda Consumer 함수 등록 Execution role : SQS ReceiveMessage, Lambda InvokeFunction, CloudWatchLogs7.확인-실행-삭제 1) 이벤트로 넘어온 메시지 내용을 확인하고2) 메시지 프로세스를 실행한 후3) SQS 메시지를 삭제합니다. import boto3, json   def handle(event, context):     sqs_client = boto3.client(         service_name='sqs',         region_name='xxxxxx'     )      message_body = json.loads(event['message']['Body'])      queue_url = event['queueUrl']     receipt_handle = event['message']['ReceiptHandle']      ###############     # 큐 메시지 처리     ############### # SQS 메시지 삭제     sqs_client.delete_message(         QueueUrl=queue_url,         ReceiptHandle=receipt_handle     ) 8.Lambda Worker 함수 등록 Execution role : SQS DeleteMessage, CloudWatchLogs9.CloudWatch의 Event Rule 등록 Event RulesCreate Rule10.1분에 한 번씩 지정한 Lambda 함수를 실행하여 SQS 메시지 확인 참고)이것만은 꼭 알아두세요! 여러 대의 서버에 메시지 사본을 저장하기 때문에 가끔씩 메시지 사본을 받거나 삭제하는 중엔 저장 서버 중 하나를 사용할 수 없을 수도 있다고 합니다. 이 경우, 해당 문제가 발생하면 사용할 수 없는 서버의 메시지가 삭제되지 않아, 메시지를 다시 가져와야 하는 문제가 생길 수 있습니다. 그러므로 애플리케이션에서 동일 메시지를 두 번 이상 처리하는 것도 대비해야 합니다.Conclusion지금까지 AWS 환경에서 SQS, Lambda, CloudWatch EventRule을 이용한 메시지 대기열 등록과 처리에 대한 기본 예제들을 실행해봤습니다. AWS의 다른 서비스들과 같이 아주 간단한 방법으로 메시지 대기열을 이용할 수 있었습니다. 오늘 살펴본 방법들을 활용하면 동영상 트랜스 코딩 등의 작업을 비롯해 분산 애플리케이션 간의 데이터 처리에도 유용하게 사용할 수 있을 겁니다. ps.아마존 형님들의 IT 인프라를 이용하여 편하게 개발에만 집중합시다. 참고 1) 각 서비스 매뉴얼은 아래 페이지 링크 참조하면 된다.SQSLambdaboto3 2)2018년 2월 기준이다. 3)표준 대기열과 FIFO 대기열의 특징은 아래와 같으며 자세한 내용은 매뉴얼에 정리되어 있다. 표준 대기열 : 무제한 처리량, 최선 정렬FIFO 대기열 : 높은 처리량, 선입선출 전송 글이상근 팀장 | R&D 개발1팀leesg@brandi.co.kr브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 1122

EOS Token 생성과 발행, 전송

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

프론트엔드 개발자(Front-End Developer)에 대해 알려드립니다!!

안녕하세요 크몽 개발팀입니다. 오늘은 일상적인 'IT 이야기'가 아닌 제가 맡고 있는 직책인'프론트엔드 개발자(Front-End Developer)'에 대해 포스팅을 해보고자 합니다.여러분들은 혹시 'Front End'라는 용어에 대해 알고 있으신가요? 저도 제일 처음 이 단어를 들었을땐 이게 대체 무슨 단어인가 했습니다.이 용어 외에도 'Back End'라는 단어도 있는데요, 물론 처음만나는 단어가 두개인만큼 두배로 어려워보일 수 있겠지만.. 놀라셨을 가슴 한번 쓸어내려드리고 전~혀 어렵지않다는 것을 차차 설명해드리겠습니다.----------------------------------------------------------------------------------------'프론트엔드'란??우선, 'Front End'라는 단어는 어떤의미의 단어일까요? 'W사'의 사전을 통해 알아보겠습니다.   한마디로 말씀드려서 '프론트엔드'는 사용자들에게 보이는 영역을 책임을 지는 것이며 '백엔드'는 시스템적인것으로 눈으로 보이지는 않지만 말 그대로 뒤에서 전산 처리를 하는 것을 말합니다.즉, '프론트엔드'는 시스템적으로 멋지게 만들어진 아이맥의 내부를 감싸는 껍데기를소비자들이 사고 싶게 만드는 디자인으로 구현하는 작업을 말하는 것입니다.크몽의 '조너선 아이브'같은 존재(?)라고 말씀드릴 수가 있겠군요.. 하하하하 (자뻑 죄송합니다(__);;)  '프론트엔드 개발자'의 목표는?'프론트엔드 개발자'의 미션은 두가지라고 말씀드릴 수가 있습니다.    첫번째로, 사용자들이 홈페이지를 친숙하고, 직접적으로 보여지도록 개발하는 것인데 딱 두가지!! 개발스킬과 미적감각을 동반하여야 합니다.여기서 중요한점은!! 쿤이는 디자인을 좋아라하지만 영감이 떠올릴만한 미술관과는 거리가 있다는 점점점점점점...앞으로 열심히 다녀보겠습니다!다시 본문으로 돌아가서..두번째는, 끊임없이 변화하는 웹세상에서 어떤툴과 테크닉을 썼는지 알고 있어야 한다는 점입니다.이 부분은 변화를 사랑하는 쿤이에겐 식은죽 먹기보다 쉽다고 하는게 맞겠네요!! (제발 그렇다고 해줘요..ㅠㅠ) '프론트엔드 개발자'가 쓰는 툴은?'프론트엔드 개발자'가 쓰는 툴의 몇몇은 웹사이트의 UI를 개발하는 툴에서 구할 수 있습니다. 첫번째로, 'HyperText Markup language'라고 불리는 'HTML' 되겠습니다.이 마크업언어는 어떤 웹사이트에서 중추적인 역활을 하는 그런 녀석입니다. 이 녀석은 말이죠... 자신의 이름을 문서의 앞뒤에 안써주면 자신의 정체도 모르는 그런녀석이구요,이 녀석의 명령어(태그)를 쓸땐 말이죠 명령어 끝에 닫는태그를 안해주면 크게는 문서 전체를 뒤죽박죽으로 만드는 그런 녀석이에요.어떨때는 파트너('CSS')와 함께 어디 놀러갈땐 각 장소(인터넷 익스플로어, 크롬 등..)에 따라 다른 매력을 발산해줘서 양파같이 까도까도 속을 모르는 그런 녀석이에요.두번째는, 'Cascading Style Sheet'라고 불리는 'CSS'입니다이 스타일 시트는 프레젠테이션효과를 주며 우리의 웹사이트가 단 하나밖에 없다는 희귀성을 부여할 수 있습니다. 이 녀석은 아까 말씀드렸듯이 'HTML'의 파트너에요. 남자는 여자하기 나름이란 말과 같이 'HTML'은 'CSS'하기 나름이라고 말씀드릴수가 있을 것같네요. 직접적으로 말씀드리자면 'HTML'이 몸이라고 보시면 'CSS'는 옷입니다. 'CSS'가 어떻게 스타일을 주는가에 따라서 웹사이트가 최신스타일룩을 보여줄 수도 있으며 잘못 쓴다면 90's 힙! to the 합!스타일을 보여 줄 수도 있습니다. 그래서 많은 사이트들이 사용자들에게 직접적으로 보여지는 스타일에 대해 신경쓰는 것이 이러한 이유라고 말씀드릴 수가 있습니다.세번째는, 'Content Management System'인 'CMS'입니다우리 한글로 표현하자면 내용관리시스템이란 것인데 아마 생소하실 것이라고 생각 듭니다. (실은 저도 생소했습니다ㅎㅎ)이 녀석은 한마디로 웹 사이트의 내용을 관리하는 시스템인데요.내용 관리 애플리케이션('CMA')과 내용 배포 애플리케이션('CDA')이 있는데요,그냥 약자로만 봤을 때엔 저기 아무 증권사나가서 한번쯤은 가입해야 될 것같은 분위기죠? 단호하게.. 아닙니다!! 연이자 2%할 것같은 'CMA'가 하는 일은 'HTML'에 들어갈 내용, 변경, 제거 등의 관리 프로그램이고,왠지 아이들의 미래를 위해 들어야될 것같은 'CDA'는 웹 사이트의 모든 수치(현행화)를 보고 편집할 수 있는 정보편집 프로그램입니다.우리가 흔히 볼 수 있는 형태로는 웹 기반 편찬(마법사템플릿 등), 형식 관리, 계정 제어, 데이터의 색인,테이터 탐색, 키워드 검색 등이 있을 수 있겠습니다.프론트엔드 개발자가 유의할 점은?프론트엔드개발자는 다음 두가지의 사항에 대해 유의해야 합니다.첫번째는, 접근성입니다. 앞서 말씀드렸듯이 이용자들에게 친숙한 모습으로 다가가야합니다.한번도 보지도 듣지도 못한 그런 UI로 이용자들에게 다가간다면 과연 잘 사용할 수 있을지가 문제일 겁니다.그런 맥락에서 말씀드리자면 모든기기에서 항상 똑같은 모습으로 이용자들을 맞이한다면각 기기에서 최적화 되지못한 화면들이 나와 이용자들에게 혼란을 줄지도 모를 일입니다.그렇기때문에 동적인 사이트를 만들어야 된다는 생각을 프론트엔드 개발자는 생각하고 있어야합니다.두번째는, 사용 간편성입니다. 만약 접근성이 좋아졌다고 하더라도 검색엔진에 최적화되지않은 사이트라면전세계적 검색사이트인 G사에서 사이트안의 컨텐츠와 연관된 내용을 검색하더라도 상위에 랭크 안되는 경우가 많습니다.그렇게 된다면 검색사이트로 원하는 사이트를 찾아들어가는 지금으로는 많은 잠재이용자들의 유입을 막아 더 이상 서비스가 성장하지못하는 상황까지 갈 수 있습니다.---------------------------------------------------------------------------------------- 이렇게 제가 하는일에 대해 포스팅을 하다보니 제가 맡은 업무가 우리 크몽서비스에 얼마나 큰 영향을 주는지 알 수 있었는데요... 갑자기 제 어깨에 곰한마리가 앉은 것같은 느낌이 드네요ㅠㅠㅠㅠ (아~ 피로야가라~!!!) 지금까지 제가 공부한 내용들을 간략하게 포스팅해보았는데요.담번엔 배운것들을 쓰는 과정을 시간이 허락한다면 보여줄 수 있는 포스팅으로 찾아 뵙겠습니다. :)#크몽 #개발자 #개발팀 #프론트엔드 #인사이트 #팀원소개
조회수 1515

StyleShare Engineering Blog?!

변정훈님 강의 모습생각해보기한 번도 생각해본 적이 없다! 왜 글을 작성해야 하는가?! 왜냐하면, 우리는 글로 먹고사는 사람도 아니고, 수려한 글솜씨도 없기 때문에?! 하지만, 이미 우리 사회는 PR의 시대를 뛰어넘어 미디어의 홍수에서 살아가고 있고, 매우 쉽게 무의식적으로 많은 글을 읽고 있다.하지만, 우리가 글을 작성하기 위해서 얼마나 많은 준비가 되어 있을까? 이 글을 쓰고 있는 필자 역시 글을 써본 경험이 거의 없다. 특히 회사의 이름을 걸고 글을 쓴다는 것은 매우 부담스러운 일이다.그래서 우리는 변정훈[Outsider’s Dev Story]님을 초대하여, 그분이 생각하는 블로그 일상과 엔지니어링 블로그에 대한 생각 공유의 시간을 가져보았다.엔지니어링 블로그회사 블로그 운영을 해보았는가?아쉽게도 변정훈 님도 회사 블로그를 운영해본 적은 없다고 하신다. 그 원인을 다음과 같은 이유로 해석을 하였다.주제가 많지 않다.개발보다 우선순위가 떨어진다.누구나 처음부터 글을 잘 적을 수 있는 건 아니다.그렇다. 이 글을 쓰는 중에도 위와 공감할 수 있는 부분이 적지 않다.우선 글을 많이 써보지 못한 필자로서도 어떤 글을 적어야 할지 난감하게 느껴지고, 업무 중에서도 중요도가 떨어지는 것은 분명하다. 마지막으로 주제 선정부터 매우 어렵게 느껴진다.그런 이유로 글쓰기를 즐겨 하시는 변정훈 님 조차 회사 블로그 운영을 잘 이끌어 본 적이 없다고 하신다.변정훈 님의 블로그는 2007년 부터 총 1,300여개 글이 게시되어 있다고 한다.왜 우리는 블로그를 운영하려 하는가?여러 가지가 있겠지만, 필자가 가장 공감한 부분은 이 부분이었다.팀 내 지식/경험 공유잠재적 입사자에게 기술 스택 및 문화 공유팀 전체의 실력 향상그동안 개발일을 해오면서, 몇 년 동안 풀리지 않는 큰 숙제 중에 하나가 좋은 개발자를 찾는 것이었다. 항상 사람을 찾는 것이 어렵다. 좋은 사람의 기준이 높아서인지 더 좋은 기업이 많아서인지 알 수는 없지만, 일하면서 느낀 가장 어려운 문제 중에 하나이다. 결국, 내부의 인력을 더 좋은 사람으로 만들고, 외부의 좋은 사람과도 교류의 장을 만들 수 있다는 희망을 품을 수 있게 되는 것이다.글 작성의 문턱을 어떻게 낮춰야 할까?좋은 점은 쉽게 공감이 되지만, 언제나 가장 어려운 것은 실천이 아닐까 싶다. 특히 회사에서 업무로 이런 일이 발생된다면, 많은 사람들이 엄청난 부담감을 가질 것이고, 결국 회사 엔지니어링 블로그는 대문만 남은 유명무실한 블로그가 될 가능성이 높다.그래서 변정훈 님은 이렇게 제안하셨다.월 1개 보다 적어도 된다.주제를 계속해서 제안하고 만들어 내야 한다.돌아가면서 작성한다.챙겨주는 사람이 필요하다.글도 리뷰하는게 좋다.부담감은 의도적으로 줄여야 …특히 변정훈님은 부담감을 줄이는 방법에 대해서, 팀 공유를 해주셨는데 잠시 소개하면 다음과 같다.나보다 모르는 사람 — 나 — 나보다 잘하는 사람언제나 어떤 기술에 대하여, 나보다 잘 모르는 사람과 나보다 잘 하는 사람이 있다는 것을 인지하고, 나보다 못하는 사람을 위해서 글을 작성한다는 것이다. 그러다 나보다 잘하는 누군가가 어쩌다 피드백을 준다면 오히려 매우 감사하게 새로운 지식을 터득하게 된다는 것이다. 역시 모든일에는 긍정적인 마인드가 중요하다. 세상 어딘가에는 나의 작은 지식이라도 필요로하는 사람들이 분명히 있을테니, 작은 용기를 가지고 세상에 누군가를 위해서 작성한다면, 세상은 분명 아름다워질 것이다.좋은 글, 좋은 주제란 무엇일까?사실 가장 어려운 이야기일지도 모른다. 변정훈님은 이런 내용을 좋다고 표현하셨는데, 잠시 정리하면 다음과 같다.개발팀의 문화어떻게 일하는가?프로젝트 수행 회고실패기개인적으로 실패기가 가장 적기 어려운 글이라고 생각한다. 하지만, 누군가에게는 가장 소중한 경험이 될지도 모르니, 간접경험의 공유는 특히 소중한 것일 수 있다.만약 회사 블로그의 글이 회사 내/외부의 사람들에게 지식 공유와 전달이 목적이라면, 그리고 좋은 문화를 계속 가지고 유지하기 위해서라면, 실패기가 어쩌면 가장 소중한 경험의 공유가 아닐까 생각해본다.질문과 답변을 통한 소통의 시간마지막으로 이 날 소개 받은 좋은 글의 흐름이란 다음과 같았다.하고자 했던 일 (Context)경험한 문제 사황 정리(격리된 상황)시도해 본 방법(내가 아는 지식)왜 동작이 안되는가? 왜 동작하는가?(가설)문제 상황 재현예제 코드관련 링크개념 설명지금까지 적은 이 글을 위의 원칙대로 다시 한번 살펴본다. 부족한 부분이 있는지, 수정할 부분이 있는지…이제 부터 회사 블로그를 더욱더 적극적으로 운영해보자!!!#스타일쉐어 #개발팀 #조직문화 #블로그 #기업문화 #사내복지
조회수 1711

아키텍트, 개발 리더십의 변화...

보통, 하나의 서비스를 개발하는데 얼마나 걸리며, 그 시간 동안 어떤 일을 '구체적'으로 진행시켜야 하느냐에 따라서 아키텍팅의 관점이 변화된다.자주 쓰는 장표 중의 하나이다. 간단하게 설명하면 과거의 비즈니스와 현재의 비즈니스의 차이를 디지털 서비스로 만들어 내는 기간으로 표시한 것이다.과거에는 하나의 디지털 비즈니스가 동작하기 위해서 데이터를 수집하고 분석, 기획, 구현, 실행하기까지 대부분 8.5개월에서 10개월 정도의 시간이 소요되었고, 이렇게 만들어진 서비스들은 실제 고객과 단절되어 있는, 내부 시스템에 가까웠다는 것을 표현한다.그리고, 디지털 비즈니스의 세계에서는 모바일로 실 고객과 커넥티드 되어 있으며, 각 비즈니스가 실제 수집부터 실행까지 1주에 동작되는 세계를 표현한다.이 차이는 정말 개발 조직과 개발 리더십에 많은 차이를 주게 된다.Classic Business에서는 8개월 이상의 방향성이 흔들리지 않도록, 전체적인 방향성이 흐트러지지 않도록 개발 리더십을 발휘하는 것이 중요했다. 특히, 초기의 개발 조직을 세팅하고 예산과 비즈니스의 완성과 실 서비스 후의 이익과 같은 경영적인 판단이 더 중요하던 시기였기 때문에, 실제 소프트웨어를 만들어내는 관점은 디테일하고, 기능적인 것에 집중화된 상태로 개발 조직이 구성되고, 리더십도 그것을 최대한 끌어내는 것에 집중했다.또한, 내부적 조직의 문제로 일이 더디게 진행되거나, 품질이나 세부적인 문제를 쥐어짜거나, 어떻게든 일정을 맞추기 위해서 조정하는 조정자의 역할도 매우 큰 상태였다. 개발 리더십도 그런 관점에서 구성되었고, 기술적인 변화도 거의 없이 초기에 결정된 상태로 대부분 진행되었다.그런데, Digital Business의 세계로 넘어오면 이것은 완전 다른 구도를 가지게 된다.1주 단위의 개발 및 배포까지 매우 유연한 상태로 가동되고, 이 단위는 기술적 선택과 실패가 매우 빠르게 반복되는 것을 의미하게 되며, 개발 조직은 말 그대로 작게 세분화되고, 전체적인 방향성은 계속 유동적으로 변화하게 된다.24시간 내에 하나의 개념이 수립되고, 이를 배포까지 진행시키기 위한 매우 다양한 시도들을 선택할 수 있게 하며, 기획 조직과 개발 조직이 하나의 '지표'나 '시각화'된 장표를 보고 빠르게 판단하게 할 수 있다.매우 빠른 순간 판단이 중요하며, '몇 분'간격으로 회사의 운명을 결정할 수 있는 서비스의 론칭도 가능하게 한다.관리적인 방법은 DevOps의 자동화된 환경과, 세분화된 배포 권한, 기획자들과의 유기적인 환경들을 보다 효율적으로 운용할 수 있는 방법들에 대해서 개발 리더십은 고민하게 된다.어떻게 빠르게 일을 효과적으로 움직일 것이며, 빠른 판단을 할 수밖에 없다. 빠르게 변화하는 기술 스택을 더 잘 알고 있는 것은 개발 조직이기 때문에, 아키텍트나 개발 리더의 권한은 계속 실무자에 가깝게 내려가게 되는 것이 순리에 가깝다.현재 DevOps를 지향하고 있는 개발 조직에서 아키텍트가 지향하는 것은 크게 개념적으로 변화한 것은 없다. '고객과 비즈니스를 이해하는 개발'임에는 틀림없으나, 기존의 아키텍팅과 많이 달라진 것은 실시간 서비스에 대한 분석과 기획의 변화, 데이터 중심의 개발 구조의 시각화를 통해서 개발 조직을 통제한다기보다는, 개발 조직을 숨 쉬게 만드는 '심장'과 같은 역할을 하게 된다.마치, 비즈니스가 빨라지면, 심장도 빨리 뛰고, 비즈니스가 좀 수월해지면 호흡을 고를 수 있는 형태...현재의 아키텍트는 개발 조직의 '심장'과도 같아.속도와 박자, 전체적인 흐름을 중시하는 것이 현재의 아키텍트의 역할이다.건축가인 아키텍트들에게는 엄청난 규칙과 법칙, 책임의 범위가 상당하다. 하지만, 소프트웨어 아키텍트들에게는 그런 책임이 법적으로 제시되고 있지 못하고 있다. 보통 소프트웨어 아키텍트라고 한다면, 부정적인 환경에서 제대로된 소프트웨어를 만들 수 없기 때문에 부당한 개발환경을 담당할 가능성이 없다는...그래서, SI현장에서 아키텍트는 거의 나오지 않는다고 봐야 한다. 슬프지만. 그리고, 마지막으로... 아키텍트는 '직위'나 '권위'가 아니다. '롤'일뿐이다. 그뿐...
조회수 1896

스포카에서 쓰는 오픈소스와 오픈소스 라이센스 (1)

안녕하세요. 스포카 프로그래머 박종규입니다. 이번 시간에는 스포카에서 쓰고있는 클라이언트 측 오픈소스와 그 오픈소스가 어떠한 라이센스가 적용이 되었는지 알아 보겠습니다.오픈소스(Open Source)먼저 간략하게 오픈소스의 정의에 대해서 짚어가도록 하겠습니다. 오픈소스는 소스코드를 외부에 공개하여 누구든지 제한없이 소프트웨어를 쓰고 소스코드를 볼 수 있는 소프트웨어를 말합니다. 통상적으로 오픈소스 소프트웨어를 오픈소스라고 부르기도 합니다. 대표적인 오픈소스로는 우리가 많이 쓰는 안드로이드OS와 크로미움 브라우저를 볼 수 있죠.프로젝트에 오픈소스를 적용?그렇다면 오픈소스의 정의도 알았고 제한없이 쓸 수도 있다고 하고 이렇게 많은 장점이 있는 오픈소스를 우리회사 프로젝트에 한 번 도입해볼까?라는 생각을 가지신 분들이 있겠지만 잠시만 기다려 주시길 바랍니다. 이러한 오픈소스는 오픈소스 라이센스라는 일종의 저작권이 적용이 되어 있어서 그 라이센스를 준수 해야합니다.오픈소스 라이센스(Open Source License)오픈소스 라이센스의 정의를 간략하게 보면오픈소스 라이센스는 오픈소스SW 개발자와 이용자간에 사용 방법 및 조건의 범위를 명시한 계약을 말한다. 따라서 오픈소스SW를 이용하기 위해서는 오픈소스SW 개발자가 만들어놓은 사용 방법 및 조건의 범위에 따라 해당 SW를 사용해야 하며, 이를 위반할 경우에는 라이선스를 위반함과 동시에 저작권 침해로 인해서 이에 대한 처벌을 받게 된다.라고 나와 있습니다. 즉 오픈소스이긴 하지만 오픈소스에 적용된 라이센스를 준수하지 않는다면 법적인 처벌을 받는다는 거죠. 그렇기 때문에 프로젝트에 오픈소스를 적용하려면 제일 먼저 라이센스를 확인해야 합니다.스포카 클라이언트에서는 어떠한 오픈소스를 쓰고 있을까?현재 스포카의 클라이언트측에서 사용하고 있는 오픈소스는 다음과 같습니다.jQueryLESSBackbone.jsD3.jsDataTables.js그럼 간략하게 이 오픈소스가 어떠한 역할을 하는지 간략하게 알아보겠습니다.jQueryjQuery(제이쿼리)는 브라우저 호환성이 있는 HTML 속 자바스크립트 라이브러리이며 클라이언트 사이드 스크립트 언어를 단순화 할 수 있도록 설계되었습니다. 즉 자바스크립트를 좀 더 편하게 쓸 수 있도록 개발된 라이브러리이죠.LESSLESS는 css를 동적으로 쓸 수 있게 해주는 자바스크립트 라이브러리 입니다. 기존 css에서 제공하지 않는 변수 및 연산식을 제공하기 때문에 코드를 재사용 할 수 있을 뿐만 아니라 개발시 소요되는 시간을 줄여줍니다. *.less로 개발된 코드는 less 컴파일러를 통해 *.css로 변환이 되어 클라이언트 페이지에 적용됩니다.Backbone.jsBackbone.js는 자바스크립트를 MVC 패턴으로 개발할 수 있게 도와주는 자바스크립트 라이브러리입니다.D3.jsD3.js는 데이터를 우리가 쉽게 볼 수있게 다양한 차트, 표, 그림으로 표현 할 수 있도록 기능을 제공해주는 자바스크립트 라이브러리입니다.DataTables.jsDataTables.js는 table를 만들어주는 기능을 제공하는 자바스크립트 라이브러리입니다.그렇다면 위 오픈소스에는 어떠한 라이센스가 적용되어 있을까?위의 오픈소스에 적용되어 있는 라이센스를 살펴보면jQuery : MIT, GPLv2LESS : apache license 2Backbone.js : MITD3.js : BSDDataTables.js : BSD, GPLv2같은 라이센스가 적용이 되어 있습니다. 그럼 하나씩 살펴보도록 하죠.듀얼라이센스먼저 jQuery와 DataTables.js에는 다른 오픈소스와 다르게 라이센스가 두개가 적용이 되어 있는 것을 볼 수 있습니다.이것을 흔히 듀얼라이센스라고 하는데 이 라이센스는 오픈소스를 쓰는 사용자가 두개의 라이센스중에서 하나를 선택해서 쓸 수 있는 라이센스입니다. 예를 들면 jQuery를 쓰는 사용자는 GPL 라이센스를 적용을 할 수도 있고 MIT 라이센스를 적용해서 쓸 수 있다는 뜻이죠.GPL 라이센스jQuery와 DataTables.js에 적용되있는 GPL라이센스에 대해서 알아 보겠습니다. GPL라이센스는 오픈소스에 가장 많이 적용된 라이센스 중에 하나입니다. 이 라이센스는 자유소프트웨어재단에서 만든 라이센스로 이 라이센스를 가진 오픈소스를 이용하여 응용 프로그램을 개발하는 경우에는 GPL라이센스가 적용이 됩니다. 그리고 GPL라이센스는 3가지의 버전이 있습니다.GPLv1GPL의 버전 1은 1989년 1월에 발표되었다(GPLv1 전문). 이것은 자유 소프트웨어에서의 두 가지 중요한 자유를 보장해 주었는데, 하나는 프로그램의 소스코드를 공개하지 않은 채 바이너리 파일만 배포하는 것을 막는 경우로 이것을 막기 위해 GPLv1에는 프로그램을 GPLv1로 배포할 때는 사람이 이해하기 쉬운 소스 코드를 같이 배포해야 한다는 조건이 들어갔다. 두 번째 문제는 프로그램에 추가적인 제약을 걸 가능성이 있다는 점이었고, 이를 막기 위해 GPLv1 프로그램을 수정한 프로그램은 원래 프로그램과 마찬가지로 GPLv1을 따라야 한다는 조건이 들어갔다.GPLv2자유 소프트웨어 재단(OSF)에서 만든 자유 소프트웨어 라이선스다. 미국의 리처드 스톨만(Richard Stallman)이 GNU-프로젝트로 배포된 프로그램의 라이선스로 사용하기 위해 작성했다. ‘① 컴퓨터 프로그램을 어떤 목적으로든지 사용할 수 있다 ② 컴퓨터 프로그램의 복사를 언제나 프로그램의 코드와 함께 판매 또는 무료로 배포할 수 있다 ③ 컴퓨터 프로그램의 코드를 용도에 따라 결정할 수 있다 ④ 변경된 컴퓨터 프로그램 역시 프로그램의 코드와 함께 자유로이 배포할 수 있다’라는 네 가지 조항을 명시하고 있다. 대부분의 소프트웨어에 대한 라이선스는 소프트웨어를 공유하거나 수정할 수 있는 자유를 금지하기 위 고안되었다. 반면에 GNU 일반 공중 라이선스는 자유 소프트웨어를 공유하고 수정할 수 있는 자유를 보장하기 위해 의도되었다. 즉, 소프트웨어가 사용자 모두에게 자유롭게 이용될 수 있도록 하는 것이다. 이 일반 공중 라이선스는 자유 소프트웨어 재단의 소프트웨어 대부분을 비롯하여, 저작자가 이 라이선스의 사용을 지정한 기타 모든 프로그램에 적용된다. (자유 소프트웨어 재단의 소프트웨어 중 일부는 이 라이선스 대신 GNU 라이브러리 일반 공중 라이선스가 적용된다.) 누구나 자신의 프로그램에 이 라이선스를 적용시킬 수 있다.GPLv3자유 소프트웨어 재단(FSF)과 이 재단의 GNU 프로젝트에 의해 배포되며 GNU 소프트웨어에 적용되는 공개 소프트웨어의 대표적인 라이선스 체계. GNU GPL이라고도 하며, 저작권(COPYRIGHT)의 반대라는 의미로 카피레프트(COPYLEFT)라고도 한다. 라이선스 사용료나 사용상의 제약 조건을 자유롭게 하여 소프트웨어 유통을 활성화하기 위한 의도에서 출발한 것으로 GNU 소프트웨어로 공개되는 원시 부호는 누구나 변경 또는 일반 공중 라이선스(GPL)로 재배포하고, 이를 이용하여 상업적 웹 사이트를 구축할 수도 있다. 그렇다고 저작권의 완전한 포기를 의미하는 것은 아니어서 GPL의 기본 원칙과 공개하는 측이 정의한 바를 충실하게 따르도록 되어 있다. 1990년대에 마련된 GPL V2.0에 이어 2005년에 V3.0이 발표되었다. GPL 버전 3은 2007년 6월 29일에 발표되었다. 2005년 후반에 자유 소프트웨어 재단에서 GPL의 세번째 판을 개발할 것이라고 발표했다. 바뀐 점 중에서 가장 중요한 4가지를 말하자면, 소프트웨어 특허에 대처하는 것, 다른 라이선스와의 호환성, 어떤 부분의 원시 코드와 무엇이 GPL이 포함되어야 하는 원시 코드를 구성하는지와 디지털 제한 관리(DIGITAL RESTRICTIONS MANAGEMENT)에 신경을 썼다.※참고GPL 라이센스가 적용된 오픈소스를 사용했다고 무조건 소스코드를 공개해야 하는 것은 아닙니다. 예를 들면 MySQL db를 이용하여 웹서비스를 개발해서 직접 서비스만 운영하는 경우 이것은 다른 곳에 배포하는 것이 아니므로 GPL 라이센스 의무사항이 적용되지 않습니다. 하지만 다른 곳에 제공하거나 파는 경우(쇼핑몰을 제작해서 파는 경우)에는 배포하는 것이 되므로 GPL라이센스가 적용이 됩니다. 따라서 이런 경우에는 상용라이센스를 구매해서 써야 합니다.MySQL에서 정의한 배포하는 대표적인 예는 다음과 같습니다.MySQL을 포함하고 있는 소프트웨어를 고객에게 팔아 그 소프트웨어를 고객이 소유한 장비에 설치하는 경우고객이 소유한 장비에 기본적으로 MySQL을 설치해야하는 소프트웨어를 파는 경우MySQL을 포함하고 있는 하드웨어 시스템을 고객에게 팔아서 고객이 있는 곳에 설치하는 경우MIT 라이센스MIT 라이센스는 MIT 공과대학교에서 학교 학생들의 소프트웨어 학습을 돕기 위해서 개발한 허가서입니다. 이 라이센스는 강력한 조항이 없어서 MIT 라이센스가 적용된 오픈소스를 이용하여 응용 프로그램을 개발할 시에 응용 프로그램을 오픈소스로 해야할 필요도 없고 소스코드를 공개할 의무가 없습니다. 또 상업적인 제한도 없습니다. 다만 응용 프로그램에 MIT 라이센스라고 표시와 라이센스 사본을 첨부만 해주면 됩니다.BSD 라이센스버클리의 캘리포니아 대학에서 배포하는 공개 소프트웨어의 라이선스입니다. BSD 라이센스는 자유소프트웨어 자작권의 하나로 BSD 계열 소프트웨어를 포함한 많은 프로그램에서 사용하고 있습니다. 이 라이센스는 라이센스라고 할 수 없을 만큼 미약해서 아무나 수정하고 배포하고 소스코드를 공개해야 할 의무가 없습니다. MIT 라이센스와 마찬가지로 라이센스 표시만 해주면 됩니다.Apach license 2아파치 라이센스는 아파치 소프트웨어 재단에서 만든 라이센스입니다. 이 라이센스 또한 MIT,BSD와 마찬가지로 소스코드 공개의 의무는 발생하지 않습니다. 하지만 “Apache”라는 이름에 대한 상표권을 침해하지 않아야 한다는 조항이 있어서 BSD라이센스보다 법적으로 완결된 내용을 담고 있습니다. 라이센스의 표시와 아파치 소프트웨어 재단에 개발된 소프트웨어라는 것을 밝혀야 합니다.참고한국저작권위원회위키백과KLDPwikiGNU공개SW포털MySQL KOREAKLDP 오픈소스라이센스가이드오픈소스 라이센스 비교표#스포카 #운영 #개발 #오픈소스 #개발자 #개발팀 #꿀팁 #인사이트 #조언

기업문화 엿볼 때, 더팀스

로그인

/