스토리 홈

인터뷰

피드

뉴스

조회수 4198

LSTM Tutorial

Summary:이 포스팅은 LSTM에 대한 기본 개념을 소개하고, tensorflow와 MNIST 데이터를 이용하여 구현해봅니다.LSTM1. 개념 설명LSTM(Long Short Term Memory)은 RNN(Recurrent Neural Networks)의 일종으로서, 시계열 데이터, 즉 sequential data를 분석하는 데 사용됩니다.기존 RNN모델은 구조적으로 vanishing gradients라는 문제를 가지고 있습니다. RNN은 기본적으로 Neural network이기 때문에 chain rule을 적용하여 backpropagation을 수행하고, 예측값과 실제 결과값 사이의 오차를 줄여나가면서 각 시간 단계의 gradient를 조정합니다. 그런데, 노드와 노드(시간 단계) 사이의 길이가 길어지다보면, 상대적으로 이전의 정보가 희석됩니다. 이 문제는 시퀀스 상 멀리 떨어져 있는 요소, 즉 오래 전에 발생한 이벤트 사이의 연관성을 분석할 수 없도록 만듭니다.LSTM은 RNN의 문제를 셀상태(Cell state)와 여러 개의 게이트(gate)를 가진 셀이라는 유닛을 통해 해결합니다. 이 유닛은 시퀀스 상 멀리 있는 요소를 잘 기억할 수 있도록 합니다. 셀상태는 기존 신경망의 은닉층이라고 생각할 수 있습니다. 셀상태를 갱신하기 위해 기본적으로 3가지의 게이트가 필요합니다. Forget, input, output 게이트는 각각 다음과 같은 역할을 합니다.Forget : 이전 단계의 셀 상태를 얼마나 기억할 지 결정합니다. 0(모두 잊음)과 1(모두 기억) 사이의 값을 가지게 됩니다. Input : 새로운 정보의 중요성에 따라 얼마나 반영할지 결정합니다. Output : 셀 상태로부터 중요도에 따라 얼마나 출력할지 결정합니다.게이트는 가중치(weight)를 가진 은닉층으로 생각할 수 있습니다. 각 가중치는 sigmoid층에서 갱신되며 0과 1사이의 값을 가지고 있습니다. 이 값에 따라 입력되는 값을 조절하고, 오차에 의해 각 단계(time step)에서 갱신됩니다.2. 응용 (MNIST data)MNIST는 손으로 쓴 숫자 이미지 데이터입니다. 하나의 이미지는 가로 28개, 세로 28개, 총 784개의 값으로 이루어져 있습니다.Many-to-One model는 여러 시퀀스를 넣었을 때 나오는 최종 결과물만을 이용하는 모델입니다. 이를 이용하여 784개의 input으로 1개의 output값(A) 을 도출합니다. 이 A를 하나의 층에 통과시켜 10개의 숫자 label중 하나를 할당합니다.784개의 입력값을 사이즈가 28인 벡터가 28번 이어지는 시퀀스(time step)로 보고, input의 크기를 28, 시퀀스 길이를 28로 각각 설정합니다. 28개의 input은 C라고 표현되어 있는 LSTM 셀로 순차적으로 들어가게 됩니다.output의 크기는 셀의 크기와 같으며, 64로 설정하였습니다. 셀크기가 너무 작으면 많은 정보를 담지 못하기 때문에 적당히 큰 값으로 설정합니다. 전체 output은 64개의 값을 가지고 있는 벡터 28개의 집합이 되고, 마지막 벡터만 사용합니다.1층의 fully connected layer를 이용하여 64차원 벡터를 10차원으로 줄이고 softmax를 이용하여 0부터 9까지 중 하나의 값을 예측합니다.LSTM으로부터 나온 예측값을 실제갑과 비교하여 cost를 개산합니다. cost function은 cross-entropy를 이용합니다. AdamOptimizer를 이용하여 cost를 최소화하는 방향으로 모델을 최적화 시킵니다.3. 토의구현 시 어려웠던 점을 중심으로 서술하였습니다. 전체 코드는 여기를 참고해주세요.batch sizebatch_size = 128 batch_x, batch_y = mnist.train.next_batch(batch_size) MNIST의 train data의 크기는 55,000개 입니다. 이는 (55000, 784) 크기의 데이터를 학습시켜야 한다는 것을 의미합니다. 이것을 한번에 학습시킨다는 것은 매우 어려운 일입니다. 전체 데이터를 메모리에 올리기 힘들뿐만 아니라, 너무 큰 data 한번에 학습시키면 가장 작은 cost값으로 수렴하기 힘들어진다는 문제가 있습니다. (너무 작아도 마찬가지입니다.) 그렇기 때문에 큰 덩어리를 일정크기의 작은 덩어리로 잘라서 모델에 넣어 학습시는데, 이 작은 덩어리의 크기를 batch size라고 합니다.작은 덩어리로 짜르는 것이 중요한 이유는, 작은 덩어리 단위로 모델에 밀어넣고(propagation) 네트워크의 파라미터들을 조정(update)하기 때문입니다. batch size는 분석하려고 하는 데이터가 어떻게 구성되어있는지에 따라 결정되는 경우가 많습니다. 어떤 수준의 batch size가 좋다고 이야기하기 어렵고, 아주 크지 않은 값으로 설정합니다.unstack모델 구현 시 static RNN을 사용하였습니다. Static RNN에서는 unstack을 해주지 않으면 TypeError가 발생합니다.unstack( value, num=none, axis=0, name=‘unstack’)unstack은 R차원(rank)의 데이터를 R-1 차원으로 줄여주는 역할을 합니다. value로부터 axis 차원을 기준으로 num개로 자른다고도 할 수 있습니다. 이 예제로 예를 들어보겠습니다.batch_x = batch_x.reshape((batch_size, input_steps, input_size)) x = tf.unstack(X, input_steps, axis=1) outputs1, states1 = tf.nn.static_rnn(lstm_cell, x, dtype=tf.float32) 실제 학습이 진행되는 순서로 보자면, batch size만큼 불러온 인풋 데이터는 (128, 784)에서 (128, 28, 28) 형식의 3차원 벡터로 reshape해 줍니다. 그리고 다시 unstack을 통해 time step을 기준으로(axis=1) 28개의 텐서를 만듭니다. 다시말해, (128, 28, 28)이라는 3차원 형식의 벡터는 (128, 28)이라는 2차원 벡터 28개로 변환되어 모델에 입력되게 됩니다. 이런 변환이 필요한 이유는 28*28의 크기를 가진input들을 차례로 넣게 되면 처리속도가 제한적이기 때문입니다. unstack을 이용하면 하나의 batch 안에 있는 input을 한꺼번에 한줄씩 병렬적으로 처리할 수 있게 됩니다.Dynamic RNN에서는 unstack을 해주는 과정이 필요 없습니다. Static과 Dynamic의 차이는 추후 포스팅에서 자세히 다루도록 하겠습니다.Training cycle참고한 다른 예제코드들은 서로 다른 스타일의 사이클로 학습시키고 있었습니다. 스타일은 크게 두가지로 나누어볼 수 있었습니다. 하나의 방법은 전체 학습 횟수를 정해놓고 while문을 통해 학습시키는 방법이었습니다. 다른 방법은 똑같은 데이터를 몇번 반복해서 학습시킬지 결정하는 것입니다. 이 반복 횟수를 epoch이라고 합니다. epoch의 사전적 의미는 ‘시대’ 또는 ‘세’이지만 예제 코드에서 만나는 epoch은 전체 데이터를 학습시키는 반복회수라고 이해하시면 되겠습니다. (이 두가지 방법은 스타일의 문제일 뿐입니다. 이것을 언급한 이유는 개인적으로 epoch을 처음 접했을 때 생소했기 때문입니다.for epoch in range(training_epochs): avg_cost = 0 total_batch = int(mnist.train.num_examples/batch_size) for i in range(total_batch): batch_x, batch_y = mnist.train.next_batch(batch_size) batch_x = batch_x.reshape((batch_size, input_steps, input_size)) c, _ = sess.run([cost2, optimizer2], feed_dict={X:batch_x, Y:batch_y}) avg_cost += c/total_batch 위의 코드는 두번째 스타일이고, 각 epoch마다 cost값과 test data로 예측의 accuracy를 계산하여 출력하였습니다. 당연하게도 학습이 반복 될수록 cost는 감소하고 accuracy는 증가하였습니다.4. 정리기본적으로 도식을 통해 input size, time step, hidden_size에 대한 개념을 이해하는 것이 도움이 됩니다.tensor의 shape을 이해하는 것이 중요하다고 생각합니다. input과 output의 형식(shape)을 머리속에 떠올릴 수 있다면 에러를 줄일 수 있고 해결하기도 수월합니다.batch size의 의미, unstack을 하는 이유, epoch의 의미를 알아두면 좋겠습니다.ReferenceDEEPLEARNING4J 초보자를 위한 RNNs과 LSTM 가이드Colah’s blog, Understanding LSTM Networks이태우, 엘에스티엠 네트워크 이해하기김성훈, 모두의 딥러닝 lec 9-2. Vanishing gadient
조회수 1830

[Buzzvil Career] 좋은 데이터 애널리스트는 어떤 사람일까?

모바일 잠금화면 미디어 플랫폼 사업자 버즈빌은 어떠한 인재를 찾는지 지원자에게 잘 알리려고 노력합니다. 그럼 지원자도 버즈빌이 자신에게 맞는 기업인지 알 수 있을 테니까요. Buzzvil Career에서는 각 직무에 대해 더욱 심도 있는 정보를 제공합니다. 현재 채용에 관련한 자세한 내용은 여기에서도 확인 가능합니다. 이 게시물은 데이터 애널리스트 Elia와의 인터뷰를 담고 있습니다. 그는 좋은 데이터 애널리스트는 어떤 사람인지에 대해 이야기합니다. 데이터를 좋아하고 데이터에 기반한 기업 성장에 기여하고 싶다면 이 글에 주목해주세요.업무에 대해 설명해주세요.안녕하세요. 버즈빌의 데이터 애널리스트 Elia입니다. 팀에서 일한 지 어느덧 4년이라는 세월이 흘렀네요. 데이터 분석을 위한 툴을 세팅하고 많은 양의 가공된 데이터를 공유하고 있습니다. 데이터로 무엇을 하고 어떻게 활용할지 고민하는 것이 제 일입니다. 또 저는 올바른 결정을 내리고 효율적으로 업무를 수행할 수 있도록 A/B 테스팅과 다양한 연구를 수행하고 있습니다. 마지막으로 SQL 세션을 열어서 사람들이 데이터에 유연하게 접근하고 잘 사용할 수 있도록 합니다.왜 버즈빌을 선택 했나요?가까운 지인이 이 회사를 추천해줬습니다. 분위기가 친근했고 한국에도 이런 곳이 있다는 게 놀라웠습니다. 그리고 버즈빌은 석촌 호수 바로 앞에 있어서 전망이 훌륭한데 특히 봄이 되면 벚꽃을 볼 수 있습니다. 그리고 무엇보다 사무실은 저희 집과 가깝습니다. 그러니 제가 거절할 이유가 없었죠.버즈빌은 어떤 곳인가요?버즈빌은 데이터 애널리스트로서 성장할 수 있는 곳입니다. 팀 규모가 그렇게 크지 않아서 유연합니다. 많은 사람과 이야기를 할 수 있고 사람들을 어떻게 도울 수 있을지, 어떤 일을 이루어야 하는지 스스로 고민할 수 있기 때문에 컨설턴트 같은 존재입니다. 그만큼 특정 역할에 고정되지 않습니다. 데이터 분석은 새로운 분야입니다. 그래서 회사가 그것을 어떻게 생각하는지에 따라 담당자가 할 수 있는 일이 달라집니다. 다행히 버즈빌리언은 새로운 아이디어와 제안에 개방적인 태도를 보입니다. 버즈빌처럼 새로운 분야를 배우는 걸 좋아하는 집단도 없을 겁니다. 그래서 담당자가 얼마나 능동적인지에 따라 할 수 있는 일이 많아집니다. 정말 독특한 문화를 가졌죠.팀 분위기는 어떤가요?여기서는 다양한 프로젝트에 대해 데이터를 조사하고 돌파구를 찾아야 합니다. 초집중해야 하며 테스트를 수행하고 데이터를 분석하는 방법을 발견해야만 합니다. 올바른 의사 결정을 내리는 것은 쉬운 일이 아니기 때문에 데이터 애널리스트가 필요하죠. 이 역할이 왜 필요한지 사람들이 알 수 있도록 자신을 잘 표지셔닝을 해야 합니다. 그래야 새로운 프로젝트에 참여할 수 있는 기회가 더 많아지고 기업 성장에 더욱 직접적으로 기여할 수 있죠.좋은 데이터 애널리스트는 어떤 사람일까요?# 커뮤니케이션 데이터 애널리스트는 효과적으로 딱 필요한 말만 잘 전달하는 커뮤니케이터가 되어야 합니다. 같은 말을 반복하거나 요점에서 자꾸 벗어나면 안 되죠. 버즈빌에서 데이터 분석가로 일하려면 다양한 팀과 일하기 때문에 소통을 효과적으로 잘해야만 합니다. 그래야 데이터 연구 결과가 정확할 수 있기 때문이죠. 이는 더 나은 비즈니스 의사 결정으로 이어지죠.#적극성 데이터 애널리스트는 능동적일수록 더 성장할 것입니다. 당신의 역량이 향상될 것이고 되고 직장에서 다양한 사람들과 상호작용하는 요령을 익힐 수 있습니다. 버즈빌은 데이터 애널리스트가 다양한 연구를 수행하기 좋은 인프라를 갖추고 있는데 이것은 데이터 분석이 새로운 분야라는 점에서 매우 플러스입니다. 따라서 버즈빌은 새로운 기회를 얼마든지 제공할 수 있기 때문에 탐험을 즐기는 사람을 찾고 있습니다.*버즈빌은 현재 채용 중입니다. (전문연구 요원 포함) 자세한 내용은 아래 버튼을 눌러주세요!모바일 잠금화면 미디어 플랫폼 사업자 버즈빌은 어떠한 인재를 찾는지 지원자에게 잘 알리려고 노력합니다. 그럼 지원자도 버즈빌이 자신에게 맞는 기업인지 알 수 있을 테니까요. Buzzvil Career에서는 각 직무에 대해 더욱 심도 있는 정보를 제공합니다. 현재 채용에 관련한 자세한 내용은 여기에서도 확인 가능합니다. 이 게시물은 데이터 애널리스트 Elia와의 인터뷰를 담고 있습니다. 그는 좋은 데이터 애널리스트는 어떤 사람인지에 대해 이야기합니다. 데이터를 좋아하고 데이터에 기반한 기업 성장에 기여하고 싶다면 이 글에 주목해주세요.업무에 대해 설명해주세요.안녕하세요. 버즈빌의 데이터 애널리스트 Elia입니다. 팀에서 일한 지 어느덧 4년이라는 세월이 흘렀네요. 데이터 분석을 위한 툴을 세팅하고 많은 양의 가공된 데이터를 공유하고 있습니다. 데이터로 무엇을 하고 어떻게 활용할지 고민하는 것이 제 일입니다. 또 저는 올바른 결정을 내리고 효율적으로 업무를 수행할 수 있도록 A/B 테스팅과 다양한 연구를 수행하고 있습니다. 마지막으로 SQL 세션을 열어서 사람들이 데이터에 유연하게 접근하고 잘 사용할 수 있도록 합니다.왜 버즈빌을 선택 했나요?가까운 지인이 이 회사를 추천해줬습니다. 분위기가 친근했고 한국에도 이런 곳이 있다는 게 놀라웠습니다. 그리고 버즈빌은 석촌 호수 바로 앞에 있어서 전망이 훌륭한데 특히 봄이 되면 벚꽃을 볼 수 있습니다. 그리고 무엇보다 사무실은 저희 집과 가깝습니다. 그러니 제가 거절할 이유가 없었죠.버즈빌은 어떤 곳인가요?버즈빌은 데이터 애널리스트로서 성장할 수 있는 곳입니다. 팀 규모가 그렇게 크지 않아서 유연합니다. 많은 사람과 이야기를 할 수 있고 사람들을 어떻게 도울 수 있을지, 어떤 일을 이루어야 하는지 스스로 고민할 수 있기 때문에 컨설턴트 같은 존재입니다. 그만큼 특정 역할에 고정되지 않습니다. 데이터 분석은 새로운 분야입니다. 그래서 회사가 그것을 어떻게 생각하는지에 따라 담당자가 할 수 있는 일이 달라집니다. 다행히 버즈빌리언은 새로운 아이디어와 제안에 개방적인 태도를 보입니다. 버즈빌처럼 새로운 분야를 배우는 걸 좋아하는 집단도 없을 겁니다. 그래서 담당자가 얼마나 능동적인지에 따라 할 수 있는 일이 많아집니다. 정말 독특한 문화를 가졌죠.팀 분위기는 어떤가요?여기서는 다양한 프로젝트에 대해 데이터를 조사하고 돌파구를 찾아야 합니다. 초집중해야 하며 테스트를 수행하고 데이터를 분석하는 방법을 발견해야만 합니다. 올바른 의사 결정을 내리는 것은 쉬운 일이 아니기 때문에 데이터 애널리스트가 필요하죠. 이 역할이 왜 필요한지 사람들이 알 수 있도록 자신을 잘 표지셔닝을 해야 합니다. 그래야 새로운 프로젝트에 참여할 수 있는 기회가 더 많아지고 기업 성장에 더욱 직접적으로 기여할 수 있죠.좋은 데이터 애널리스트는 어떤 사람일까요?# 커뮤니케이션 데이터 애널리스트는 효과적으로 딱 필요한 말만 잘 전달하는 커뮤니케이터가 되어야 합니다. 같은 말을 반복하거나 요점에서 자꾸 벗어나면 안 되죠. 버즈빌에서 데이터 분석가로 일하려면 다양한 팀과 일하기 때문에 소통을 효과적으로 잘해야만 합니다. 그래야 데이터 연구 결과가 정확할 수 있기 때문이죠. 이는 더 나은 비즈니스 의사 결정으로 이어지죠.#적극성 데이터 애널리스트는 능동적일수록 더 성장할 것입니다. 당신의 역량이 향상될 것이고 되고 직장에서 다양한 사람들과 상호작용하는 요령을 익힐 수 있습니다. 버즈빌은 데이터 애널리스트가 다양한 연구를 수행하기 좋은 인프라를 갖추고 있는데 이것은 데이터 분석이 새로운 분야라는 점에서 매우 플러스입니다. 따라서 버즈빌은 새로운 기회를 얼마든지 제공할 수 있기 때문에 탐험을 즐기는 사람을 찾고 있습니다.*버즈빌은 현재 채용 중입니다. (전문연구 요원 포함) 자세한 내용은 아래 버튼을 눌러주세요!버즈빌과 함께하고 싶은 분은 지금 바로 지원 해주세요! (전문연구요원 포함)
조회수 2505

SQLAlchemy의 연결 풀링 이해하기

안녕하세요. 스포카 프로그래머 김재석입니다.SQLAlchemy는 파이썬 데이터베이스 툴킷으로는 가장 독보적인 수준으로 우아한 기능을 제공하고 있어 많은 사람이 애용하고 있습니다. 스포카에서도 파이썬 프로젝트인데 데이터베이스에 접근해야 한다면 필수로 이용하고 있죠.오늘은 SQLAlchemy의 연결 풀에 대한 기본 개념과 실전에서 연결 풀링과 관하여 알면 좋을 여러 이슈에 대해 다뤄보고자 합니다.연결 풀링 개념연결 풀링은 차후에 발생할 데이터베이스 요청에 대비하여 데이터베이스 연결을 캐싱하는 기법입니다. 빈번한 데이터베이스 요청이 여러 사용자에 의해 발생할 때, 매번 연결을 생성하고 닫는 과정을 반복하면 이에 대한 비용이 크기 때문에 이 기법을 사용하여 연결 생성 과정을 줄일 수 있습니다. 짧은 요청이 빈번하게 발생하는 웹 서비스와 같은 형태가 연결 풀과 궁합이 잘 맞습니다.SQLAlchemy의 기본 풀: 큐 풀(QueuePool)SQLAlchemy 역시 연결 풀을 기본적으로 채택하고 있는데, 그중 기본으로 제공하는 것은 큐 풀(QueuePool)입니다. 큐 풀은 설정된 pool_size와 max_overflow를 바탕으로 복수의 연결 풀을 구성해서 운용합니다. SQLite를 제외한1 모든 데이터베이스에서 기본값으로 이용하므로, 이 글에서는 큐 풀의 관리 방법을 주로 다루도록 하겠습니다.큐 풀의 생애주기큐 풀이 처음부터 연결을 미리 만드는 것은 아닙니다. 일단 0개로 시작합니다.요청이 들어올 때, 큐 풀에 유효 연결이 없으면 하나 생성합니다.설정된 pool_size까지는 더 연결이 필요하지 않은 상황이라도 연결을 종료하지 않습니다.요청이 들어올 때, pool_size까지 다 찼다 할지라도 유효 연결이 없으면 초과하여 하나 생성합니다.4번 이후부터는 오버플로 상황이기 때문에, 큐 풀은 적극적으로 오버플로를 방지하기 위해 새로 들어오는 연결을 종료하여 pool_size에 총연결 수를 맞춥니다.QueuePool이 관리하는 연결이 pool_size + max_overflow까지 다 찬 상황에서 요청이 들어오면, 일단 기다리게 합니다. 기본값으로는 30초를 기다립니다.30초를 기다려도 반환되는 연결이 없다면 TimeoutError 예외를 발생시킵니다.적절한 큐 풀 설정값서비스가 작을 때는 기본값이면 충분하지만, 서비스 사용량이 많아지고 규모 문제가 발생하게 된다면 설정을 현재 상황에 맞춰 바꿔주는 게 좋습니다. 보통 QueuePool 관련 위 언급한 2가지 값(pool_size, max_overflow)을 바꿔주는 게 좋은데 기본값은 5, 10입니다.pool_size: 현재 구성에서 연결 생성 부담을 최소화할 수 있는 가장 작은 값이 되어야 합니다.max_overflow: 현재 구성에서 데이터베이스, 웹 인스턴스가 물리적으로 버틸 수 있는 최댓값이 되어야 합니다.pool_size가 과하게 설정되어있으면 데이터베이스 입장에서 너무 많은 연결을 점유하고 있으니 비효율적입니다. 그렇다고, 너무 적게 설정한다면 오버플로가 자주 발생하여 풀링으로 얻을 수 있는 효율을 누리지 못합니다. 즉, 파이썬 측에서 비효율적입니다.max_overflow가 데이터베이스나 웹 인스턴스의 한계치보다 너무 빡빡하게 잡혀있으면 조금만 사용자 유입이 늘어도 TimeoutError를 쉽게 만나거나 서비스 속도 저하를 자주 경험하게 됩니다. 그렇다고 무한으로 두면 사용량 폭증시 이해할 수 없는 에러 파티를 경험하게 될 것입니다. (데이터베이스나 파이썬 앱, 혹은 둘 다 드러눕습니다.)결국 서비스마다 그만의 퍼포먼스와 장비 한계치가 있으니만큼 내부에서 스트레스 테스트를 통한 벤치마킹으로 적정 값을 뽑아내는 것을 추천합니다.큐 풀 관하여 자주 밟는 문제개발할 때는 문제가 없었는데, 상용 서버를 띄우면 수분 이내로 서버가 TimeoutError 예외를 발생하며 응답을 안 합니다.SQLAlchemy 쓰는 서비스를 만들어서, 개발 잘 하고 배포했는데 프로덕션에서 잠깐 잘 돌더니 TimeoutError를 내뱉으며 픽픽 죽어버리는 경험을 많이 하는 것 같습니다. 이 에러 자체는 Session이 큐 풀에 연결을 받기 위해 기다리다가 못 참고 TimeoutError를 내는 것인데요. 위의 생애주기 기준, 7번에 해당하는 상황이죠. 큐 풀의 timeout 기본값은 30이니까 30초 동안 풀의 모든 연결이 점유된 상태에서 아무것도 받지 못한 상태가 된 것이라고 보시면 됩니다.위와 같은 경험이라면 서비스 사용량이 폭증하는 쪽보다는 십중팔구 기존에 점유한 Session에서 제대로 연결을 반환해주지 않아서 발생하는 문제입니다. 특히 웹서비스라면 Flask 등에서 요청 시마다 Session이 연결을 불러다 써놓고 Pool에 돌려주는 일을 빼먹는 실수가 잦은데, Flask를 쓰고 계신다면 Flask-SQLAlchemy 등을 쓰셔서 생애주기 관리 자체를 타 라이브러리에 위임하시거나, 현재 구조상에서 요청이 끝나는 시점에 맞춰 session.close()를 적절히 호출해주시면 됩니다. (사실 Flask-SQLAlchemy가 해주는 것도 딱 이 수준입니다.)어느 날 갑자기 연결이 왕창 늘어버렸어요.역시 웹서비스 개발하다보면 발생하는 이슈입니다. SQLAlchemy를 쓰면 Session 활용을 암시적으로 하게 될 때가 많습니다. Session이 실제로 요청을 보내는 시점에서야 연결을 시도하기 때문에, 예상치 못한 기능 변경으로 연결 폭증을 겪는 것인데요. 제가 자주 본 것은 Flask의 생애주기중 before_request 구현에서 데이터베이스에 접근하는 것입니다.본래 데이터베이스 연결이 필요한 엔드포인트에서만 접속이 발생하던 것이, before_request에 붙으면서 모든 엔드포인트가 데이터베이스 연결을 하게 되면 사용량이 폭증하기 쉽게 되는데요. 이처럼 전역적인 영역에서 DB 접근을 하는 시나리오를 최소화하는 정책으로 실수를 완화할 수 있습니다.마치며SQLAlchemy의 연결 풀의 동작 방식을 이해하면 상용 서비스를 운영할 때 발생하는 데이터베이스 부하 문제를 진단하고 해결하는 데 많은 도움이 됩니다. pool_size와 max_overflow의 적정값은 서비스에 따라, 인프라의 사양에 따라 다르므로 이를 잘 파악하여 효율적으로 연결 풀이 운영될 수 있도록 세팅하는 것을 추천합니다.연결 풀을 관리하는 방법으로는 SQLAlchemy내의 기본 큐 풀을 쓰는 것 외에 Pgpool-II과 같은 미들웨어를 연결하는 안도 있습니다. 추후 이에 대해서도 다루어보도록 하겠습니다.SQLAlchemy 0.7부터 SQLite 같은 파일 기반 데이터베이스에서는 기본적으로 NullPool을 채택합니다. 파일 기반 데이터베이스에는 네트워크 연결이 일어나지 않기 때문에, 연결 비용이 적기 때문입니다. NullPool은 이름에서 알 수 있듯이 연결 풀을 유지하지 않고2 풀에 연결이 들어오는 즉시 폐기합니다. ↩큐 풀의 pool_size를 0으로 하는 것과 같다고 착각할 수 있으나, 큐 풀은 pool_size가 0일 때 pool_size가 무한대인 것으로 인식합니다. 따라서 풀을 만들지 않으려면 NullPool을 쓰는 것이 적절합니다. ↩#스포카 #개발팀 #개발자 #인사이트 #업무일지 #후기
조회수 1052

DevOps 문화 안에서의 APM의 역할 [1] (DevOps+JENNIFER)

 DevOps의 시작언제나 그랬듯이 소프트웨어 개발 트렌드는 계속 변화하고 있다. A부터 Z까지 모든 것을 새롭게 개발했던 것과 달리 아키텍처나 사용하는 용도에 따라 개방형 플랫폼이나 오픈소스 등을 활용하여 원하는 소프트웨어를 쉽게 개발할 수 있게 되었다. 또한 클라우드로 인해 애플리케이션과 서비스 개발에 대한 새로운 패러다임이 나타나고 있다. 기존의 온-프레미스 환경에서는 물리적 서버 준비, 운영체제 설치, 서비스 배포 등에 수많은 시간이 걸렸지만, 클라우드를 활용하면서 단시간에 원하는 자원을 준비하고 배포할 수 있게 되었다.이러한 변화로 개발자의 영역이 좀 더 넓어지는 계기가 되었다. 이는 전통적인 비즈니스 환경에서 개발, 빌드, 테스트, 배포, 운영에 이르는 프로세스를 효율적으로 운용할 수 있게 되어 고객의 요구사항을 빠르게 반영할 수 있게 되었다. 이것이 바로 DevOps의 시작이다. 하지만 다양한 오픈소스의 탄생과 클라우드 환경의 확산 등으로 인해 정말로 새로운 기능에 대한 개발이 빨라졌을까? 그렇다면 이에 따른 문제는 없을까? 개발 프로세스의 병목 구간DevOps의 필수 조건인 테스트 및 배포의 자동화가 이뤄지면 운영 단계에서는 반영된 사항들에 대해 주기적으로 모니터링을 해야 한다. 만약에 반영된 소스코드에 장애를 발생시킬 수 있는 잠재적 버그가 존재한다면 이를 어떻게 운영 단계에서 찾을 수 있을까? 예를 들어 특정 서비스의 피크타임에 부하가 급증한다면 앞서 말한 상황에 대한 버그가 발생할 확률이 상대적으로 높아진다. 하지만 장애의 원인이 될 수 있는 요소는 매우 다양하기 때문에 단순히 트래픽 문제로 속단할 수는 없다.직접 개발한 소프트웨어만의 문제가 아닐 수도 있으며, 제품 개발시 생산성 향상을 위해 도입된 다른 종류의 오픈소스에서 문제가 될 수도 있다. 실은 이런 류의 프로젝트들은 상용 제품이 아니므로 문제가 발생하면 상당히 곤란한 경우가 생기곤 한다. DevOps를 위한 환경이 구성되고, 고객의 요구사항을 빠르게 반영할 수 있는 시스템이 갖춰졌더라도 결국에는 앞서 말한 다양한 종류의 잠재적, 환경적인 문제들로 인해 병목이 발생할 수 있다.  모니터링 단계에서 APM의 역할개발 프로세스의 마지막 관문인 모니터링 단계는 DevOps에서 매우 중요한 역할을 한다. 하지만 안타깝게도 이미 반영된 실제 서비스에서 모니터링을 성공적으로 마치고 피드백 수집 단계로 넘어가기 위해서는 앞서 말했던 장애의 원인을 빠르게 진단해야 한다. 경우에 따라 많은 시간이 소모되기도 하기도 하며, 이는 바로 생산성 저하로 이어진다. 또한 새로운 프로세스 진행을 더욱더 보수적으로 만드는 원인이 된다.DevOps를 완벽하게 실현하기 위해서는 모니터링 단계에서 서비스 배포 이후의 서버에 들어오는 트랜잭션에 대한 상태를 배포 전과 비교할 수 있어야 하며, 응답을 지연시킬만한 요소들을 빠르게 인지할 수 있어야 한다. 그리고 배포된 소스코드로 인해 서비스 장애가 발생하는 상황이 온다면 이를 처리하기 전까지 어떻게든 서비스 장애를 지연시켜야만 한다. 이러한 이유로 DevOps 진영에서는 APM의 역할은 매우 중요한 이슈이다. 우리는 제니퍼를 통해 앞서 말한 기능들을 활용하는 방법에 대해 알아볼 것이다. 모니터링 프로세스모니터링 단계는 아래 그림과 같이 문제의 발견 및 조치, 문제해결시 재배포 단계로 나눌 수 있다.  제니퍼 대시보드를 통해 액티브서비스 상태와 트랜잭션 변화 추이를 모니터링 할 수 있는데, 만약에 새로 배포된 소스코드에 문제가 있다면 처리 중인 액티브서비스가 쌓이게 되고 , 트랜잭션 분포도 차트는 기존에 그려졌던 패턴과 다르게 보여지게 된다.이런 시점에 운영에서는 설정 여부에 따라 이벤트를 발생 시킬 수 있다. E-Mail이나 SMS, Slack과 같은 메신저 등으로 각각의 담당자들에게 서비스 상태를 알려줄 수 있으며, 담당자에게 이벤트 메시지가 전달되었다면 제니퍼를 통해 두가지 조치를 할 수 있게 된다. 먼저 개발자는 스마트 프로파일링 기능을 통해 원인분석을 하고, 운영에서는 서비스가 최악의 상태가 되기 전에 트랜잭션 유입을 차단하여 다른 화면으로 리다이렉트 시켜주는 PLC 기능을 사용할 수 있다.제니퍼에서는 서버에서 하나의 요청에 대한 처리가 끝나면 곧바로 수집되는 데이터를 트랜잭션이라하며, 현재 수행 중인 상태에 대한 실시간 데이터를 액티브서비스라고 정의한다.   모니터링 기준 값 설정서비스를 배포하기 전에 모니터링 단계를 원활하게 수행하기 위해서는 제니퍼 관리 화면에서 몇가지 설정을 해야한다. 먼저 서비스 장애 발생시 이벤트 알림 및 서비스 부하량 제어 설정의 기준이 되는 값인 전체 에이전트의 평균 액티브서비스 개수를 알아야 한다. 하지만 서비스가 운영되는 환경에 따라 기준 값이 너무 다르기 때문에 어느 정도 안정적으로 서비스가 운영되고 있다고 생각하는 시점에 대략적으로 기준 값을 정하면 된다.에이전트란 모니터링 대상 애플리케이션에 기생하여 성능 데이터를 수집하고, 이를 서버로 전송하는 역할을 하는 모듈을 말한다. 참고로 모니터링 대상 애플리케이션은 플랫폼 환경에 따라 차이가 있을 수 있는데, 일반적으로 WAS(Web Application Server)나 웹 서버를 말한다.  액티브서비스는 처리가 완료되지 않은 상태이므로 서비스 장애의 원인분석을 위한 데이터로는 적합하지 않다. 그렇기 때문에 액티브서비스 개수는 기준 값이 될 수 없으며, 개발자는 처리가 완료된 트랜잭션 데이터의 응답시간을 기준 값으로 제니퍼의 프로파일링 관련 설정을 해야 한다. 설정된 값을 기준으로 트랜잭션 분포도 차트에서 가상의 선을 긋고, 그 선 위에 있는 트랜잭션을 대상으로 스마트 프로파일링 기능을 수행할 수 있다.  본문에서는 모니터링 단계에서 직면하게 되는 문제점과 이를 해결하기 위한 APM의 역할과 필요성 대한 이야기를 했다. 다음 편에서는 본격적으로 제니퍼를 활용하여 모니터링 프로세스를 어떻게 수행하는지에 대해 알아볼 것이다.2편에서 계속...
조회수 1264

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 #블록체인 #개발자 #개발팀 #기술기업 #기술중심
조회수 3103

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

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

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

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

코인원 개발자는 어떻게 일할까?

코인원의 파이콘 한국 2018 참여기 그 두번째 이야기!코인원의 핵심, 코인원의 자랑 ‘코인원 개발자’들에 대한 이야기를 나눠보려 합니다.지금의 코인원이 있을 수 있는 이유는 바로 더 좋은 프로덕트를 만들어내기 위해 치열하게 고민하는 개발 크루들의 노력이 있었기 때문입니다.코인원은 지난 파이콘 한국 2018 ‘열린공간(Open Speak Talking)’ 세션에 참여했습니다. 이 시간을 통해 그동안 코인원 개발 크루들이 축적한 지식과 경험 그리고 노하우를 공유했어요. 파이콘이 개발자들을 위한 행사인만큼, 코인원의 개발문화와 환경, 채용 원칙에 대한 심도깊은 이야기가 오고갔답니다.그래서 예고했던대로 코인원 개발자들의 ‘Mini Interview’를 통해 개발 크루들은 기술본부를 어떻게 만들어나가고 있는지 자세히 알려드릴게요 :-)'파이콘 한국 2018' 후기 1탄 현장스케치▼코인원X'파이콘 한국 2018' 현장스케치!파이썬 개발자들의 즐거운 축제, ‘파이콘 한국 2018’이 지난 19일 성황리에 막을 내렸습니다. 이번 행사...blog.naver.com지난 8월 19일 진행된 파이콘 열린공간 현장, Open Speak Talking!진우님(CTO)을 집중 심문하고 있는 피플팀 수장 대경님코인원 기술본부는 어떻게 구성되어 있나요?진우님(CTO) : 안녕하세요, 코인원 CTO(Chief Technical Officer, 최고기술책임자) 이진우 입니다. 현재 코인원 기술본부는 총 8개의 팀으로 이루어져 있는데요. Core팀, Web팀, API팀, APP팀, QA팀, SRE팀, 데이터팀, 기술연구팀으로 나뉘어진답니다. 코인원 기술본부는 사용자에게 편리한 서비스를 제공하기 위해 프로덕트를 만들어나가고 있어요. 먼저, 사용자에게 직접적으로 보여지는 화면인 프론트엔드를 책임지는 ‘Web팀과 APP팀’ 그리고 사용자에게 보이지 않지만 시스템의 안쪽에서 수행되는 백엔드를 책임지며 프론트엔드에서 필요로하는 결과를 제공하는 ‘Core팀, API팀, QA팀, SRE팀, 데이터팀, 기술연구팀’이 인터렉티브하게 일하고 있습니다.저희는 암호화폐 거래소 코인원을 더욱 더 잘 구축하고, 개선하고, 안전하게 운영하는 것을 목표로 삼고 있어요. 또한 개발 뿐만 아니라, 지속적인 블록체인 연구를 통해 거래소에 최신 기술 트렌드를 반영할 수 있도록 항상 고민하죠.파이콘의 질문왕, 웹팀의 경화님 :)저 멍때리는거 아니에요, OST에 집중하고 있는거에요. (feat. 새로찬가로찬님)눈감은거아니에요, 뜬거에요. (feat. 킹갓제너럴대현님)코인원 개발자들은 어떻게 일하나요?경화님 (Web developer) : 코인원 개발자들은 서로 어떤 업무를 하고 있는지 눈으로 트래킹 할 수 있고, 투명하게 업무를 공유할 수 있는 개발환경을 만들고 있어요. 저희는 협업툴로 Jira와 Confluence를 사용하고 있습니다. 협업툴로 업무의 효율성을 높이면서 새로운 개발업무에 집중하는데 많은 도움이 되고 있어요. 또한 협업툴 이외에도 새로운 기술 도입에 긍정적이라 자기주도하에 여러가지 기술을 실무에 적용해볼 수 있다는 점이 매력적이죠.새로찬님 (Engineer) : 주어진 요구사항에 맞게 그대로 개발하기 보다는 요구사항의 필요성에 대해 공감하고 더 좋은 방향으로 만들 수 있도록 기획, 디자인, 개발 모든 단계에서 능동적으로 의견을 제시합니다. 기술본부에서는 빠르게 변화하는 시장상황에 맞추기 위해 매일 아침마다 PM, 개발자, 디자이너가 모여 *데일리스크럼을 진행하는데요, 서로의 Task나 Project 진행상황을 공유하고 최고의 프로덕트를 사용자에게 전달하기 위해 노력하고 있어요. *데일리스크럼이란? 정해진 시간에 개발크루들이 모여서 어제 했던 일과 오늘의 할 일 등을 공유하고, 다른 개발 크루들이 이야기하는 업무현황을 들으면서 내가 기여할 수 있는 부분과 이슈를 빠르게 파악할 수 있는 자리에요! 대현님 (Engineer) : 서비스를 만드는데 있어 주도적으로 그리고 적극적으로 개발업무를 수행합니다. 예를 들어, 코인원 Live Service에서 Manual하게 처리하고 있었던 이슈들을 자동화하는 프로젝트가 있었는데요. 수동으로 해결할 수 있는 부분이지만, 이를 서비스 기획자분들과 연계해 Task로 만들어 해결방안을 얻을 수 있었습니다. 또한 여기서 그치는게 아니라, 계속해서 코인원 사용자들을 위해 필요한 기능들을 추가하는 프로젝트에 참여하면서 많은 재미를 느꼈죠.왼쪽부터 파이콘의 나이스걸 (윤정님), 개발자 (희수님), ddddeveloper (선우님)개발본부의 공식포즈, 쁘쁘브브브이 종헌님 ㅇ_ㅇV코인원 개발자가 되면 어떤 점들이 좋나요?희수님 (Engineer) : 이전에 경험하지 못했던 거래소 프로덕트를 만들어볼 수 있다는 점이 정말 좋습니다. 저는 원래 암호화폐와 핀테크에 관심이 많았는데요. 코인원에서 거래소 프로덕트를 직접 만들면서 관심 분야에 대해서도 더 많이 알게 되고, 또 이런 서비스를 개발할 때 중요한 것이 무엇인지 고민해보면서 배워가는 것이 정말 많아요. 제가 코인원 합류 이전에 개발한 프로덕트들과는 성향이 많이 달라서 더 재미를 느끼고 있어요!종헌님 (Web developer) : 함께 일하는 모든 개발자 분들이 더 효율적인 개발 프로세스를 만들기 위해 치열하게 고민하는 것을 보면서 매일 놀라고 있어요. 서비스를 개발하며 느끼는 문제점을 도출하면서 치열하게 토론하고, 그 문제들을 해결하기 위한 아이디어들을 직접 시도해 볼 수 있어서 좋아요. 일을 더 효율적으로, 즐겁게 할 수 있는 환경을 만드는 과정이 코인원만의 애자일을 만들어나가는 것 같아 더 열정적으로 일할 수 있는 것 같습니다.선우님 (Web developer) : 많은 사용자들이 직접 이용하는 서비스를 만드는 개발자로서 다양한 문제 상황을 직면하고 해결해 나가며 보고 배우는게 많습니다. 또 코인원 기술본부는 여느 코인원 조직처럼 언제든 자신의 의견을 자유롭게 이야기하는 분위기가 형성돼있어요. 특히 하나의 프로젝트가 끝나면 회고(Retro)를 진행하는데, 프로젝트의 진행과정, 이슈, 결과물을 공유하면서 저 자신을 성장시켜나갈 수 있는 요소들이 많습니다.안녕하세요, 코인원 신입개발자 (a.k.a CTO) 입니다. (인사성 밝음밝음)“코인원에서는 어떤개발자를 원하나요?진우님 (CTO) : 블록체인을 좋아하고, 개발을 좋아하시는 분이라면 언제든지 환영입니다. 코인원 개발 크루들을 생각했을 때 ‘몰입’이라는 단어가 가장 먼저 떠오르는데요. 모두가 마니아적이고 덕후기질이 있어, 자기주도적으로 업무를 하고 적극적으로 개발할 것들을 찾고 실행합니다.  앞으로 코인원 개발 크루로 합류하실 분들 또한, 적극적으로 아이디어를 내고, 그 아이디어를 실현할 수 있도록 프로젝트를 리딩할 수 있는 분이었으면 해요. 물론 개발하는데 있어서는 누구보다 신중한 자세가 필요하겠죠? 누구보다도 내가 짠 개발코드를 꼼꼼하게 검토하고, 표용력이 있어 좋은 아이디어에는 귀 기울일 줄 아는 분들이셨으면 좋겠습니다. 참고로 Node.js, Python, Spring, C#, AngularJS를 업무에 많이 활용하고 있답니다.개발을 사랑하시는 분! 블록체인을 함께 탐험할 준비가 되신 분! 코인원 개발자 채용에 많은 관심 부탁 드립니다.코인원 개발자 채용에 많은 관심 부탁드립니다 :-)지금까지 미니 인터뷰를 통해 코인원 개발 크루들의 이야기를 들어봤습니다:) 블록체인이라는 새로운 기술 영역에서 매일매일 즐겁게 도전하고 있는 코인원 개발팀에 합류하고 싶은 분들은 현재 코인원 개발자 채용이 진행되고 있으니 지금 바로 확인해주세요!#코인원 #블록체인 #기술기업 #암호화폐 #스타트업인사이트 #기업문화 #조직문화 #팀원소개 #인터뷰
조회수 4831

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

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

Next.js 튜토리얼 5편: 라우트 마스킹

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기 2편: 페이지 이동 3편: 공유 컴포넌트4편: 동적 페이지5편: 라우트 마스킹 - 현재 글6편: 서버 사이드7편: 데이터 가져오기8편: 컴포넌트 스타일링9편: 배포하기개요이전 편에서는 쿼리 문자열을 이용하여 동적 페이지를 생성하는 법을 배웠습니다. 생성한 블로그 게시물 중 하나에 대한 링크는 다음과 같습니다:http://localhost:3000/post?title=Hello Next.js하지만 이 URL은 구립니다.다음과 같은 URL를 가지면 어떨까요? http://localhost:3000/p/hello-nextjs더 낫지 않나요?이번 편에서 이것을 구현할 예정입니다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.라우트 마스킹라우트 마스킹이라 불리는 Next.js의 특별한 기능을 사용할 예정입니다.기본적으로 애플리케이션에서 표시되는 실제 URL와 다른 URL이 브라우저에 표시됩니다.블로그 포스트 URL에 라우트 마스크를 추가해봅시다.pages/index.js에 다음과 같은 코드를 작성해주세요:다음의 코드 블럭을 살펴봅시다:<Link> 엘리먼트에서 "as"라는 또다른 prop를 사용하였습니다. 이는 브라우저에서 보여질 URL입니다. 애플리케이션에 표시되는 URL은 "href" prop에 지정되어 있습니다.첫 번째 블로그 포스트를 클릭하면 블로그 포스트로 이동할 것입니다.그 다음에 뒤로가기 버튼을 클릭하고 앞으로가기 버튼을 클릭해보세요. 무슨 일이 일어날까요?- 에러가 발생할 것이다- 인덱스 페이지로 돌아가고 포스트 페이지로 다시 이동할 것이다- 인덱스 페이지로 이동하지만 그 후에는 아무런 일도 일어나지 않을 것이다- 인덱스 페이지로 돌아가고 에러가 발생할 것이다히스토리 인식본 것처럼 라우트 마스킹은 브라우저 히스토리를 활용하여 잘 작동합니다. 해야 할 일은 링크에 "as" prop를 추가하는 것뿐입니다.새로고침하기home 페이지로 돌아가세요: http://localhost:3000/첫 번째 포스트 제목을 클릭하면 post 페이지로 이동합니다.브라우저를 새로고침하면 무슨 일이 일어날까요?- 예상대로 페이지가 첫 번째 포스트를 랜더링 할것이다- 페이지가 로드되지 않고 계속 로딩 중일 것이다- 500 에러가 발생할 것이다- 404 에러가 발생할 것이다 404서버에 불러올 페이지가 없기 때문에 404가 에러가 발생합니다. 서버는 p/hello-nextjs 페이지를 불러오려고 시도하지만 우리는 index.js와 post.js 두 개의 페이지밖에 없습니다.이 방법으로는 프로덕션으로 이 애플리케이션을 실행할 수 없습니다. 이 문제를 고쳐야 합니다.Next.js의 커스텀 서버 API는 이 문제를 해결할 수 있는 방법입니다.다음 편에서 이것을 사용하는 방법을 배울 예정입니다.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 4038

제니퍼5 기능 소개

JENNIFER 5의  유용한 기능을 소개해 드리는 시간. 새로운 기능을 검토해 보시고, 지금 바로 사용해 보세요. 제니퍼소프트는 제품을 사용하는 고객이 최적의 환경에서 모니터링 할 수 있도록 하는데 최고의 가치를 두고 모든 제품을 개발합니다.제니퍼(JENNIFER)는 웹 애플리케이션 (Java EE, .NET, PHP) 시스템 모니터링을 위한 APM(Application Performance Monitoring) 솔루션입니다. 제니퍼는 경량화(Light-Weight), 실시간(Real-Time), 그리고 개별 트랜잭션 모니터링(Individual Transaction Monitoring) 등 기술기반의 '직관적인 통합 성능관리 솔루션'으로 이미 국내외 1200여 개 고객사를 통해 검증된 바 있습니다.  또한, 시대의 요구 사항인 모바일, 클라우드, 그리고 빅 데이터 시장의 온전한 모니터링 체계를 위하여, 웹 서비스 사용자 모니터링 (Web Service Real-User Monitoring), 웹 서비스 중심의 토폴로지 뷰(Web Service Topology View), 클라우드(대규모 시스템) 환경을 고려한 아키텍처, HMTL 5기반의 N스크린(N-Screen)까지도  지원하는 APM 제품입니다.웹 서비스 중심 토폴로지 뷰 (Web Service Topology View)제니퍼 토폴로지 뷰(Topology View)는 기업의 웹 서비스를 중심으로 연결된 서비스에 대한 가시성(Visibility)을 확보하는 것이 핵심 기능입니다. WAS를 중심으로 연결된 서비스(DB, 외부 연계 서비스, HTTP 등) 사이에 발생하는 트랜잭션, 즉, 구간에서 처리되는 트랜잭션까지 실시간으로 모니터링 할 수 있습니다.구간 엑티브 서비스 모니터링: 구간에서 처리되고 있는 엑티브 서비스를 실시간 모니터링 하여 병목 지점과 그 원인을 분석할 수 있습니다.X-view 연계 분석: 병목 지점이 되는 구간에서 처리되는 모든 트랜잭션에 대한 분석을 할 수 있습니다.구간 실시간 모니터링: 실시간 차트를 통해 원하는 구간에 대한 모니터링이 가능합니다.웹 서비스 사용자 응답시간 모니터링(RUM)클라이언트 구현 기술의 발전과 모바일 기기의 대중화로 인해 기업의 서비스는 복잡해졌습니다. 사용자는 모바일을 통해 언제, 어디서든 기업의 서비스를 이용할 수 있게 되었고, APM은 이제 서버사이드 영역의 모니터링뿐만 아니라, 실제 사용자의 만족도를 높일 수 있는 사용자 중심의 성능 모니터링 기능을 요구하고 있습니다. 이러한 변화에 따라 제니퍼는 프런트 앤드(Front-End) 영역의 모니터링을 위한 실제 사용자 모니터링, 즉 RUM(Real User Monitoring)을 지원합니다.이 기능은 웹 표준 조직 기구인 W3C의 Timing Navigation API를 사용하여 별도의 모듈 설치 없이 브라우저에서부터 서버사이드까지 전 영역에 대한 응답시간 측정이 가능하며, 서버와 네트워크로 이어지는 애플리케이션 수행 경로를 시각적으로 표시하여 애플리케이션 성능에 대한 깊이 있는 분석이 가능합니다.제니퍼 for CLOUD최근 IT 흐름의 큰 변화 중 하나는 클라우드(대용량 시스템)입니다. 클라우드 환경의 큰 특징은 트랜잭션의 양에 따라 하드웨어의 제약을 받지 않고 필요에 따라 서버 수를 조절하며 운영할 수 있어야 한다는 것입니다. 제니퍼는 자동감지(Agent Auto Detection), 일괄 설정(Central Configuration), 일괄 배포 (Central Deployment) 기능을 통하여 클라우드 환경에서의 애플리케이션 성능 모니터링을 지원합니다.스마트 프로파일링(Smart Profiling)제니퍼의 개별 트랜잭션의 응답시간을 활용한 엑스 뷰 기반의 분석은 이미 수많은 고객사에서 검증된 트랜잭션 모니터링 기법입니다. 하지만, 프로파일링 분석은 개발자 혹은 성능 튜닝의 전문가가 아니면 어려움을 겪는 것이 사실이었습니다. 이에 제니퍼는 누구나 쉽게 프로파일링 데이터를 분석 할 수 있는 스마트 프로파일링(Smart Profiling)을 제공합니다. 이 기능을 통해 사용자는 Method, SQL, 외부 서비스 중 응답시간이 느린 구간을 선택하여 해당 시점의 프로파일을 쉽게 분석할 수 있습니다.실시간 Connection Pool 모니터링실시간 Connection Pool 모니터링은 Instance별 Connection Pool을 실시간으로 모니터링하는 기능입니다. 이 기능은 액티브 서비스 차트와 함께 모니터링하며 액티브 서비스 점유가 주로 DB Connection에 있을 경우 Connection Pool 설정을 조정할 수 있어 서비스 성능을 개선할 수 있다는 것입니다. 다른 측면에서 DB Connection을 특정 애플리케이션에서 많이 사용할 경우 현재 Connection을 사용하고 있는 Active Service를 찾아내어 원인을 분석할 수 있습니다.실시간 애플리케이션 변경 이력 모니터링기업에서 운영하는 서비스는 수많은 고객의 요구사항을 반영하고 서비스 개선을 위해 하루에도 여러 번 애플리케이션을 변경합니다. 모니터링 관점에서 애플리케이션 변경 시점은 곧 서비스 장애가 일어날 가능성이 가장 많은 시점입니다. 그렇기에 모니터링이 가장 필요한 시점이기도 합니다. 애플리케이션 변경 감지 기능은 변경 전후의 성능 변화를 실시간으로 모니터링하고, 변경 시점에 변경된 소스코드를 추적하여 어떤 소스코드가 변경되었는지 추적할 수 있습니다.  이를 통해 개발자와 운영자 모두가 쉽고 빠르게 서비스의 변화를 감지하고 대응할 수 있는 장점이 있습니다. 엑스 뷰(X-View) > 애플리케이션 연계 분석애플리케이션 연계 분석은 검색한 X-View의 개별 트랜잭션을 기반으로 애플리케이션 단위 호출건수, 평균 응답 시간, 최대 응답 시간, 평균 SQL 시간, 평균 CPU 시간을 함께 분석할 수 있는 기능입니다.  또한 이러한 성능 수치들이 높은 애플리케이션의 개별 트랜잭션을 분석하여 성능에 영향을 미치는 애플리케이션을 튜닝 할 수 있습니다.엑스 뷰(X-View) > 연계 트랜잭션 분석제니퍼는 하나의 요청으로부터 시작된 다수의 트랜잭션 간의 상관 관계를 모니터링하거나 분석할 수 있습니다. 하나의 서버에서 처리된 서로 다른 업무 트랜잭션들을 연계할 수 있으며, 다른 서버에서 발생된 트랜잭션을 연계할 수 있습니다. 프로토콜 후킹 방식(HTTP, RMI)과 GUID를 활용한 연계방식을 지원합니다.엑스 뷰(X-View) > 자동 스택트레이스제니퍼를 포함한 대부분의 APM은 트랜잭션이 느린 원인을 분석하기 위해 매서드 프로파일링 기능을 제공합니다. 하지만 매서드 프로파일링 기능은 잘못된 설정으로 성능에 영향을 주거나 실제 느린 매서드를 찾지 못할 경우가 많습니다. 또한, 로직을 잘 알아야 하므로 성능 전문가가 아닌 이상 사용이 매우 어려운 단점이 있습니다.제니퍼는 이런 제약사항을 없애고 좀 더 쉽게 사용하기 위해 자동 스택트레이스 기능을 제공합니다.  이 기능은 성능 전문가가 아니더라도 느린 트랜잭션이 발생했을 때 해당 시점에 자동적으로 스텍트레이스(Stacktrace)를 남겨서 원인을 쉽고 빠르게 분석할 수 있습니다. 제니퍼소프트는 항상 제니퍼 고객의 모니터링 환경을 좀 더 쉽고 유용하게 할 수 있도록 고민하고 있습니다.  더 좋은 기능을 제공할 수 있도록 노력하겠습니다. 
조회수 2240

비트윈의 멀티티어 아키텍처를 위한 프레젠터 이야기 - VCNC Engineering Blog

블로그 첫 글에서 비트윈의 시스템 아키텍처에 대해 다룬 적이 있습니다. 시스템 구성의 미래에 대한 계획으로 멀티티어 아키텍처에 대해 언급했었는데, 이는 프로토콜을 단순화시키고 배포 자동화를 가능하게 하기 위해서 클라이언트와 비즈니스 로직을 담당하는 서버 사이에 일종의 게이트웨이를 두는 것이었습니다. 그 외에도 여러 가지 필요성이 생겨 해당 역할을 담당하는 프레젠터라는 것을 만들게 되었고 비트윈의 채팅 시스템에 적용하게 되었습니다. 만드는 과정 중에 여러 기술적인 문제들이 있었고 이를 해결하기 위한 노력을 하였습니다. 이 글에서는 비트윈 시스템에서의 프레젠터에 대해 이야기 하고자 합니다.프레젠터프레젠터는 일종의 게이트웨이 입니다. 기존의 시스템에서는 클라이언트들이 ELB를 통해 채팅 서버에 직접 TCP 연결을 하였습니다. 하지만 비트윈 PC버전과 자체 푸시 서버를 만들면서 ELB로는 해결할 수 없는 부족한 점들이 생겼고, ELB의 부족한 점을 채워줄 수 있는 시스템이 필요하게 되었습니다. ELB를 대체하는 역할 외에도 다른 여러 필요했던 기능들을 제공하는 프레젠터를 만들기로 하였습니다.프레젠터는 ELB의 역할을 할 뿐만 아니라 여러 다른 기능들도 제공합니다.프레젠터의 기능패킷을 적절한 샤드로 중계비트윈에서는 커플 단위로 샤딩하여 같은 커플의 채팅 요청에 대해서는 같은 채팅 서버에서 처리하고 있습니다. Consistent Hash를 통해 커플을 여러 채팅 서버로 샤딩하고 ZooKeeper를 이용하여 이 정보를 여러 채팅 서버 간 공유합니다. 프레젠터 또한 ZooKeeper와 연결을 하여 어떤 채팅 서버가 어떤 커플을 담당하는지에 대한 정보를 알고 있도록 설계되어 있습니다. 따라서 프레젠터는 첫 연결 시 보내는 인증 패킷을 보고 해당 채팅 연결에서 오는 요청들을 어떤 채팅 서버로 보내야 할지 판단할 수 있습니다. 어떤 채팅 서버로 보낼지 판단하는 과정은 처음 한 번만 일어나며, 이후 패킷부터는 자동으로 해당 채팅 서버로 중계합니다.프레젠터의 이런 기능 덕분에 클라이언트는 더 이상 어떤 채팅 서버로 붙어야 하는지 알아내는 과정 없이 아무 프레젠터와 연결만 맺으면 채팅을 할 수 있게 되었습니다. 기존에는 클라이언트들이 여러 채팅 서버 중 어떤 서버에 붙어야 하는지 확인하는 작업을 한 후에 할당된 채팅 서버로 연결 맺어야 했습니다. 그래서 클라이언트가 채팅 서버와 연결을 맺기 위해 다소 복잡한 과정을 거쳐야 했지만, 이제는 클라이언트가 프레젠터의 주소로 연결 요청만 하면 DNS Round Robin 통해 아무 프레젠터와 연결하는 방식으로 프로토콜을 단순화할 수 있었습니다. 덕분에 새로운 채팅 서버를 띄울 때마다 ELB를 Warm-Up 시켜야 했던 기존 시스템의 문제가 없어졌습니다. 그래서 비트윈 개발팀의 오랜 염원이었던 채팅 서버 오토스케일의 가능성도 열렸습니다.많은 수의 연결을 안정적으로 유지PC버전과 푸시 서버를 만들면서 기존의 채팅 연결과 다르게 많은 수의 연결이 장시간 동안 유지 되는 경우를 처리할 수 있어야 했습니다. 기존에는 TCP 릴레이를 하도록 설정된 ELB가 연결들을 받아주었습니다. 한 머신당 6만 개 정도의 Outbound TCP 연결을 맺을 수 있는데, ELB도 트래픽에 따라 여러 대의 머신에서 돌아가는 일종의 프로그램이므로 이 제한에 걸린다고 생각할 수 있습니다. 따라서 많은 수의 연결을 맺어놓고 있어야 하는 경우 ELB에 문제가 생길 수 있다고 판단했습니다. (과거 ELB가 연결 개수가 많아지는 경우 스케일아웃이 안되는 버그 때문에 문제가 된 적이 있기도 했습니다) 또한 클라이어트 연결당 내부 연결도 하나씩 생겨야 하면 클라이언트가 연결을 끊거나 맺을 때마다 서버 내부 연결도 매번 끊거나 연결해야 하는 오버헤드가 발생합니다.이를 해결하기 위해 프레젠터에서는 TCP 연결을 Multiplexing하는 프로토콜을 구현하여 적은 수의 내부 연결로 많은 수의 클라이언트 연결을 처리할 수 있도록 하였습니다. 서버 내부에서는 고정된 개수의 몇 개의 연결만 맺어 놓고 이 연결들만으로 수많은 클라이언트 연결을 처리할 수 있습니다. 이처럼 TCP Multiplexing을 하는 것은 Finagle과 같은 다른 RPC 프로젝트에서도 지원하는 기능입니다.TCP Multiplexing 프로토콜을 통해 많은 수의 클라이언트 연결을 소수의 서버 내부 연결로 처리합니다.또한, 프레젠터는 많은 수의 SSL 연결을 처리해야 하므로 암복호화 로직을 실행하는데 퍼포먼스가 매우 중요하게 됩니다. 채팅 서버 한 대를 제거하거나 하는 경우 많은 연결이 한꺼번에 끊어지고 연이어 한꺼번에 연결을 시도하게 되는 경우가 있을 수 있는데, 이 때 대량의 SSL Handshaking을 하게 됩니다. 기존 서버들로 대량의 SSL Handshaking을 빠른 시간안에 처리하기 위해서는 높은 퍼포먼스가 필요합니다. Java로 작성된 프로그램만으로 이런 퍼포먼스 요구사항을 달성하기 어려우므로, 클라이언트와의 연결을 담당하는 부분은 OpenSSL, libevent를 이용한 C++로 코드로 작성하였습니다. 인증 패킷을 파싱하거나 패킷들을 릴레이 하는 등의 로직을 담당하는 부분은 Alfred라는 Netty를 이용하여 만든 인하우스 RPC 라이브러리를 이용해 작성되었습니다. 연결을 담당하는 부분은 TCP 연결을 유지하는 역할과 들어온 패킷들을 Netty로 작성된 모듈로 릴레이 하는 역할만 담당하므로 매우 간단한 형태의 프로그램입니다. 짧은 시간 안에 어럽지 않게 구현할 수 있었습니다.클라이언트의 연결을 받아주는 역할을 하는 부분은 C++, 실제 로직이 필요한 부분은 Java로 작성하였습니다.여러 네트워크 최적화 기술의 지원ELB에는 여러 네트워크 최적화 기술들을 아직 제공하지 않는 경우가 있습니다. 대표적으로 HTTP/2 혹은 SPDY, QUIC, TCP Fast Open 등이 있습니다. 특히 모바일 환경에서는 SSL Handshaking 등 부가적인 RTT로 인한 지연을 무시할 수 없으므로 이런 기술들을 이용한 초기 연결 시간 최적화는 서비스 퀄리티에 중요한 부분 중 하나입니다. ELB는 AWS에서 관리하는 서비스이므로 AWS에서 이런 기능들을 ELB에 적용하기 전에는 이용할 수 없지만, 프레젠터는 직접 운영하는 서버이므로 필요한 기능을 바로바로 적용하여 서비스 품질을 높일 수 있습니다. ELB에서 이미 제공하는 최적화 기술인 SSL Session Ticket이나 다른 몇몇 기술은 이미 적용되어 있고 아직 적용하지 않은 기술들도 필요에 따라 차차 적용할 예정입니다.프레젠터의 구현C++ 연결 유지 모듈프레젠터는 퍼포먼스를 위해 C++로 작성되었습니다. 이는 Pure Java를 이용한 암복호화는 프레젠터에서 원하는 정도의 퍼포먼스를 낼 수 없기 때문입니다. 처음에는 OpenSSL과 libevent를 이용해 작성된 코드를 JNI를 통해 Netty 인터페이스에 붙인 event4j라는 인하우스 라이브러리를 이용하려고 했으나, 코드가 복잡하고 유지보수가 어렵다는 점 때문에 포기하였습니다. 그 후에는 netty-tcnative를 이용해보고자 했으나 테스트 결과 연결당 메모리 사용량이 큰 문제가 있었고, 이를 수정하기에는 시간이 오래 걸릴 것 같아 포기하였습니다. 결국, 페이스북에서 오픈소스로 공개한 C++ 라이브러리인 folly를 활용하여 프레젠터를 작성하게 되었습니다. folly의 네트워크 API들이 OpenSSL과 libevent를 이용해 구현되어 있습니다.릴레이 로직프레젠터는 첫 인증 패킷을 파싱하여 릴레이할 채팅 서버를 판단하며, 이후의 패킷부터는 실제 패킷을 까보지 않고 단순 릴레이 하도록 설계하였습니다. 처음의 Netty 파이프라인에는 Alfred 프로토콜을 처리할 수 있는 핸들러들이 설정되어 있어 인증 패킷을 파싱 할 수 있으며 인증 패킷에 있는 정보를 바탕으로 어떤 채팅 서버로 패킷을 릴레이 할지 결정합니다. 그 이후 파이프라인에 있던 핸들러를 모두 제거 한 후, 읽은 byte 스트림을 Multiplexing Protocol 프레임으로 감싸서 그대로 릴레이 하는 매우 간단한 로직을 담당하는 핸들러 하나를 추가합니다. 덕분에 로직 부분의 구현도 매우 간단해질 수 있었으며, 채팅 서버에 API가 추가되거나 변경되어도 프레젠터는 업데이트할 필요가 없다는 운영상 이점도 있었습니다.Multiplexing Protocol프레젠터의 Multiplexing Protocol은 Thrift를 이용하여 직접 정의 하였으며, 비트윈 개발팀 내부적으로 사용 중인 RPC 라이브러리인 Alfred에 이 프로토콜을 구현하였습니다. Thrift를 통해 C++과 Java로 컴파일된 소스코드를 각각 프레젠터의 연결 처리 부분과 로직 처리 부분에서 이용하여 통신합니다. 프레젠터에서는 Multiplexing된 TCP 연결들을 Stream이라고 명명하였으며 이는 SPDY나 HTTP/2에서의 호칭 방법과 유사합니다. SPDY나 HTTP/2도 일종의 Multiplexing 기능을 제공하고 있으며, 프레젠터의 Multiplexing Protocol도 SPDY 프레임을 많이 참고하여 작성되었습니다.수 많은 클라이언트와의 TCP연결을 Stream으로 만들어 하나의 내부 TCP연결을 통해 처리합니다.Alfred에서는 Multiplexing 된 TCP 연결을 Netty의 Channel 인터페이스로 추상화하였습니다. Netty에서 TCP 연결 하나는 Channel 하나로 만들어지는데, 실제 Stream도 Channel 인터페이스로 데이터를 읽거나 쓸 수 있도록 하였습니다. 이 추상화 덕분에 비트윈 비즈니스 로직을 담당하는 코드에서는 Stream으로 Multiplexing 된 TCP 연결을 마치 기존의 TCP 연결과 똑같이 Channel을 이용해 사용할 수 있었습니다. 그래서 실제 비즈니스 로직 코드는 전혀 건드리지 않고 프레젠터를 쉽게 붙일 수 있었습니다.로드 밸런싱클라이언트는 Route53에서 제공하는 DNS Round Robin 기능을 이용하여 아무 프레젠터에 연결하여 채팅 요청을 날리게 됩니다. 하지만 무조건 동등하게 Round Robin 하게 되면 새로 켜지거나 하여 연결을 거의 맺지 않고 놀고 있는 프레젠터가 있는데도 연결을 많이 맺고 있는 기존 프레젠터에에 연결이 할당되는 문제가 생길 수 있습니다. 충분한 시간이 흐르면 결국에는 연결 개수는 동등하게 되겠지만, 처음부터 놀고 있는 프레젠터에 새로운 연결을 가중치를 주어 할당하면 로드를 분산되는 데 큰 도움이 될 것입니다. 그래서 Route53의 Weighted Routing Policy 기능을 이용하기로 하였습니다. 현재 연결 개수와 CPU 사용량 등을 종합적으로 고려하여 Weight를 결정하고 이를 주기적으로 Route53의 레코드에 업데이트합니다. 이런 방법으로 현재 로드가 많이 걸리는 서버로는 적은 수의 새로운 연결을 맺게 하고 자원이 많이 남는 프레젠터로 더 많은 새로운 연결이 맺어지도록 하고 있습니다.스케일 인/아웃AWS에서는 트래픽에 따라 서버 개수를 늘리기도 하고 줄이기도 하는 AutoScaling 이라는 기능이 있습니다. 프레젠터가 스케일 아웃될때에는 프레젠터가 스스로 Route53에 레코드를 추가하는 식으로 새로운 연결을 맺도록 할 수 있습니다. 하지만 스케일 인으로 프레젠터가 제거될 때에는 Route53에서 레코드를 삭제하더라도 함부로 프레젠터 서버를 종료시킬 수 없습니다. 종종 클라이언트의 DNS 캐싱 로직에 문제가 있어, Route53에서 레코드를 삭제되었는데도 불구하고 이를 업데이트하지 못해 기존 프레젠터로 연결을 시도하는 경우가 있을 수 있기 때문입니다. 따라서 프레젠터 클러스터가 스케일 인 될 때에는 기존의 모든 연결이 끊어지고 충분한 시간 동안 새로운 연결이 생기지 않은 경우에만 서버를 종료시켜야 합니다. AutoScaling Group의 LifeCycleHook을 이용하여 위와 같은 조건을 만족 시켰을 때에만 프레젠터 서버를 완전히 종료시키도록 하였습니다.못다 한 이야기프레젠터라는 이름이 이상하다고 생각하시는 분들이 있을 것으로 생각합니다. 멀티티어 아키텍처를 이야기할 때 프레젠테이션 티어, 어플리케이션 티어, 데이터베이스 티어로 구분하곤 하는데 이 프레젠테이션 티어에서 나온 이름입니다. 지금은 실제 프레젠터가 하는 역할과 프레젠테이션 티어가 보통 맡게 되는 역할에는 많은 차이가 있지만, 어쩌다 보니 이름은 그대로 가져가게 되었습니다.프레젠터에서 AutoScaling을 하기 위해 LifeCycleHook을 이용합니다. 이때 프레젠터를 위해 LifeCycleHook 이벤트를 처리하는 프로그램을 직접 짠 것이 아니라 비트윈 개발팀이 내부적으로 만든 Kharon이라는 프로그램을 이용하였습니다. Kharon은 인스턴스가 시작되거나 종료될 때 실행할 스크립트를 작성하고 인스턴스의 특정 위치에 놓는 것만으로 LifeCycleHook을 쉽게 이용할 수 있게 하는 프로그램입니다. Kharon 덕분에 비트윈 내 다양한 시스템에서 별다른 추가 개발 없이 LifeCycleHook을 쉽게 활용하고 있습니다. 후에 Kharon에 대해 자세히 다뤄보도록 하겠습니다.정리비트윈 개발팀에서는 오랫동안 유지되는 수많은 채팅 서버 연결들을 처리하고 클라이언트와 서버 간 프로토콜을 단순화시키는 등 여러 이점을 얻고자 ELB의 역할을 대신하는 프레젠터를 만들었습니다. 프레젠터를 만드는 과정에서 여러 기술적 문제가 있었습니다. 이를 해결하기 위해 C++로 연결 유지 모듈을 따로 작성하였고 Multiplexing Protocol을 따로 정의하였으며 그 외 여러 가지 기술적인 결정들을 하였습니다. 이런 과정에서 시행착오들이 있었지만 이를 발판 삼아 더 좋은 기술적 결정을 내리기 위해 고민하여 결국 기존 시스템에 쉽게 적용할 수 있고 쉽게 동작하는 프레젠터를 만들어 이용하고 있습니다.

기업문화 엿볼 때, 더팀스

로그인

/