스토리 홈

인터뷰

피드

뉴스

조회수 3340

개발자 커리어 전환기 2 | 3시간 만의 퇴사 결정, 비전공자로 개발에 뛰어들다.

Q) 안녕하세요 Juan Carlos(환 까를로스)님 자기소개 부탁드려요.네 안녕하세요. 지금 immersive 6기에서 개발자가 되기 위해 열심히 공부하고 있는 환 까를로스라고 합니다. 어쩌다보니 immersive 6기에서 전문 네비게이터로 생활하고 있어요.(웃음) 네비게이터는 페어프로그래밍을 할 때 드라이버가 코딩을 할 수 있도록 큰 그림을 그려주는 거라고 생각하시면 되요. 페어와 같이 코딩을 하면서 Immersive를 헤쳐나가고 있습니다.Q) 코드스테이츠 오시기 전에는 어떤 일을 하셨었나요?해외영업을 했습니다. 이 일을 선택한 이유는 조금 특별해요. 제가 취준생이었을 때 회사를 여러 곳을 지원을 했었습니다. 지원한 기업에서 합격 통보를 받았죠. 근데 막상 그 기업에 입사하려고 보니까 지방에서 근무를 해야 하는 거예요. 그전까지는 이런 것들을 생각도 안 하고 있다가, 막상 닥치니까 곰곰이 생각하게 되었어요.'내가 서울을 떠나서 잘 살 수 있을까?' 지방에서 산다는 거에 대해서 크게 생각하고 있지 않았었는데, 막상 닥치니까 고민이 많이 되더라구요. 제가 서울 토박이인데, 고향을 떠나서 사는 거는 제가 너무 힘들 것 같아서 포기하고 지금 현 직장(지금은 퇴사를 했죠)에 다니게 된 거예요. 그리고 제가 공대 출신인데 공대 출신이 서울에서 직장을 잡으려면 영업 밖에 없더라구요. 그래서 영업직을 선택했었습니다.Q) 그럼 직장을 나오게 된 계기가 있으신가요?새로운 것을 수용할 생각이 없는 경직된 조직문화가 너무 안 맞았어요. 저는 신입을 뽑는 이유는 조직이 시장의 흐름이나 세대의 변화에 맞춰 변하기 위해서라고 생각해요. 근데, 전에 팀은 변할 생각을 안 하더라고요. 야근까지 해가면서 업무개선을 해도 기존 방식을 고수하자는 피드백이 계속되니 열정이 사라지는 것을 느꼈죠. 제가 4년 정도 다녔는데, 퇴사를 고민하고 3시간 만에 결정하고 사표를 내고 나왔어요.저는 뭔가 다양한 경험을 하고 제 스스로가 발전하는 걸 좋아하는데, 발전한다는 느낌이 없었죠. 부서를 여러 곳으로 옮긴 이유도 제가 정확히 뭘 좋아하는지 모르니까 이것저것 해보면 알지 않을까 생각했어요. 영업 파트에서 일하면서도 기획부터 경영지원까지 다양한 일을 맡았었죠.Q) 3시간이면 정말 짧네요! 보통은 여러 번 고민하기 마련인데요. 그럼 퇴사하시고 나서는 무엇을 하셨나요?음... 사실 퇴사하고 나서 제가 맡았던 고객들이 경쟁사로 이직할 수 있게 도와주겠다고 하셔서 고민을 많이 했어요.  근데, 이왕 퇴사했는데 새로운 걸 해보고 싶었어요. 한 군데 계속 있으면 뭐랄까.. 나태해지는 것 같아서요.- 다른 분야의 직장을 잡으신 건가요?일단은 여행 가야지라고 생각해서, 스페인으로 떠났어요.  첫 번째로는 스페인의 순례길을 가기로 했죠. 1000km 정도 되는 길을 걸었던 것 같아요. 순례길을 걸으면서 다양한 사람들을 만나고 생각도 정리도 좀 하고 그랬어요. 거기에는 전 세계 퇴사한 사람이 다 모이는 것 같아요. 숙소에서 만난 친구들에게 물어보면 죄다 회사를 퇴사하고 왔다고 하더라구요(웃음) 그리고 그곳에서 개발자가 돼야겠다는 마음을 먹었습니다.Q) 어떤 경험을 하셨길래 그곳에서 개발자가 돼야겠단 마음을 먹으셨나요?먼저 이 얘기를 해야 하겠네요. 사실 제가 여행경비가 이렇게 많이 들지 몰랐어요. 순례길을 여행하다가 돈도 떨어져 가는데 직업이 있는 채로 순례길을 도는 사람들을 만나게 된 거예요. 세 명을 만났는데, 세 명 다 소프트웨어 엔지니어였습니다. 처음에는 브라질 개발자를 만났어요. 그때까지만 해도 별생각이 없었죠. 다음으로는 러시아 개발자를 만났습니다. 러시아 개발자 친구를 보면서 아 이런 게 디지털 노마드구나라는 생각을 갖게 되었죠. 그리고 마지막으로 스페인 개발자 친구를 만나니까 정말 개발자라는 직업이 부럽게 느껴지더라구요. Q) 디지털 노마드를 보고 개발자가 돼야겠단 결정을 하신 거네요! 그럼 코드스테이츠를 선택해주신 이유가 있으신가요? 아까 제가 생각보다 여행 경비가 많이 드는지 몰랐다고 했잖아요. 순례길만 여행하는데도 여행 경비가 다 떨어진거에요(웃음) 그래서 어쩔 수 없이 세계 여행의 꿈을 접고 한국으로 오게 되었죠. 그리고 한국으로 돌아오는 비행기 안에서 개발자가 되기로 결심을 했습니다. 내가 여행을 다니고 하고 싶은 것을 하면서도 일도 하고 그게 너무 좋아 보이는 거에요. 물론 한국의 현실은 많이 다르겠지만 그래도 개발자라면 가능하지 않을까라고 생각을 했습니다. 그리고 그 비행기에서 핸드폰으로 코딩 관련해서 검색을 하다가 코드스테이츠를 알게 되었어요. 알아보니까 교육철학도 좋고 저에게도 괜찮은 방식을 것 같아서 그 비행기 안에서 바로 결정을 하게 되었습니다. 퇴사할 때와 마찬가지로 일사천리로 결정을 했습니다.- 비행기 안에서 모든 결정이 이루어졌네요! 3시간 만에 퇴사를 결정하신 것 같이요!뭐 망설일 이유가 있나요. 자신감과 결단력 그게 제 장점이니까요(웃음)Q) 그럼 이제 Immersive 얘기를 해볼게요. Immersive에서의 생활은 어떠세요?생각했던 것보다 여유가 있어서 좋아요. 그전에는 되게 불안하고 빡빡하고 그럴 것 같은데 막상 해보니까 할만하더라고요. 그리고 일단 사람들이 너무 좋아요. 같이 지내는 사람들이 좋으니까 Immersive도 할만한 것 같아요.Q) 그러면 지금 Immersive에서는 어떤 것을 배우고 있나요?서버를 배우고 있어요. 프론트 쪽 하구요. 프로젝트를 하고 적용을 해봐야 완전히 내 것으로 만들 수 있을 것 같아요. 역시 직접 적용을 해봐야 정확히 알 수 있을 것 같습니다.서버를 배우고 있어요. 프론트 쪽 하구요. 자바스크립트라는 언어의 다양한 문법을 매일 체험해보고 있어서, 매일매일이 새롭습니다. (뭔가 이해할 만 하면 다른걸 배워서..) 프로젝트를 해봐야 완전히 내것으로 만들 수 있을 것 같아요.Q) 앞으로 어떤 개발자가 되고 싶으세요?거창하게 세상을 바꾸는 개발자! 이런 건 제 스타일은 아니에요(웃음) 저는 제가 하고 싶은 것을 하는 개발자. 만들고 싶은 것을 만드는 개발자가 되고 싶어요. 세상을 바꾸는 개발자도 내가 좋아하는 것, 내가 하고 싶은 것, 내가 만들고 싶은 것을 만드는 개발자가 되었을 때 가능하지 않을까요?Q) 프로젝트를 곧 하게 될 텐데 어떤 프로젝트를 하고 싶으신가요?제 경험에 기반한 프로젝트에요. 우리는 회사에서 주는 돈 그냥 받잖아요. 제가 회사를 나오고 받았던 돈들을 확인해보니 제대로 받지 못했다는 것을 알았어요. 그래서 사람들이 노동의 정당한 보상을 알고 받을 수 있도록 도와주는 프로그램을 만들고 싶어요. 주변만 봐도 대부분의 사람들이 이런 문제로 인해 문제를 가지고 있다고 생각해요.Q) 1년 후에 개발자가 되었다고 생각하면 어떤 모습일까요?개발자가 될 수 있을까요?(웃음) 아마 1년 후엔 야근에 쩔어있지 않을까요? 저는 이게 내 일이다라는 생각을 하면 엄청 파고드는 스타일이거든요. 개발자로 처음 들어간 직장에 남아 있거나 이직을 하고 있을 것 같아요. 사실 저는 계획을 잘 안 세우거든요. 그러니까 아무 준비 없이 퇴사하고 개발을 배우고 있죠. 설마 굶어죽기야 하겠어요?Q) 마지막으로 하고 싶은 말이 있나요?제가 퇴사하면서 방 정리도 같이 하게 됐어요. 정리를 하다 보니까 우연찮게 제 학창시절 생활기록부를 보게 되었습니다. 생활기록부에 장래희망을 적는 칸이 있잖아요. 근데 제가 깜짝 놀란 게 거기에 중학교 때부터 고등학교 때까지 줄곧 프로그래머로 적혀있던 거에요. 그동안 까맣게 잊고 살았는데 신기했어요.그리고 또 생각을 해보니까 대학교 때도 제가 컴공과는 아니지만 공대라서 C++을 해야했는데 그 과목에서 처음으로 A+을 받은 기억이 나더라구요. 이런 생각이 들면서 결국 나는 프로그래머를 선택할 운명이었나? 이런 생각도 들고. 결국에는 돌아돌아 이 길로 온 것 같아요. 그래도 돌아왔다고 해서 늦었다거나 아쉽지는 않아요. 제가 지금까지 걸어온 길이 분명히 프로그래밍을 하는데 도움이 된다고 생각하고 있으니까요.네 지금까지 환 까를로스님과의 인터뷰를 진행했었는데요. 정말 비하인드스토리가 엄청나네요. Immersive 성공적으로 수료하시고 원하시는 개발자가 되기를 바랍니다. 앞으로도 다양한 스토리를 가진 Immersive 수강생분들의 이야기로 찾아뵙겠습니다.
조회수 1395

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

레일즈 기반의 프론트엔드(브라우저에서 서버 사이드 렌더링 계층까지)와 자바 기반의 백엔드(내부 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
조회수 1774

Amazon SageMaker는 처음이지?

Overview브랜디 랩스를 사랑해주시는 여러분, 안녕하세요. 개발자 오-연주입니다. 지난 4월, Brandi Back-end 개발자 분들과 코엑스에서 열렸던 AWS Summit(04.18 - 04.19)에 다녀왔습니다!여러 세션을 듣는 와중에 우연히 AI machine learning 를 쉽게 도와주는 Cloud Machine learning Flatform인 Amazon SageMaker에 대해 들었습니다. 듣던 중 머닝러닝에서 학습을 시켜 그 데이터로 ‘Brandi 서비스와 연관지으면 어떨까’ 라는 생각을 했는데요. 그래서 오늘은 많은 분들의 관심사인 머신러닝 학습관련 Amazon Amazon SageMaker에 대한 글을 쓰려고 합니다.sage는 마법사, 현자라는 의미입니다.sageMaker를 create하자!“자, 퐈이팅 넘치게 신나게 sagemaker를 create해볼까요!” 했는데…Seoul Region이 없다!현재 지원되는 리전은 아직 네 군데입니다. 저는 제일 있어 보이는 미국 동부의 버지니아를 선택하겠습니다.1] EU (Iceland) 2] US West (Oregon) 3] USEast (N. Virginia) 4] US East (Ohio)SageMaker를 create하기 전에는 학습할 데이터와 학습 모델을 저장할 S3 Bucket이 필요합니다.1. Default 값으로 S3를 만드세요.중요한 점은, bucket 이름이 “sagemaker-” 로 시작되어야 한다는 것입니다. 그래야 나중에 notebook instance가 어느 곳에 데이터를 저장할지 알 수 있습니다.Next, Create bucket 버튼을 누르다 보니, S3 Bucket이 생성되었습니다.2. Create notebook instance 버튼을 눌러 SageMaker를 만들어 봅시다!원하는 이름을 지어줍니다. 저는 machineLearningTest 라고 지었어요. IAM role 선택하는 부분에서 None을 눌러 Default 값으로 sageMaker를 만듭니다.인고의 Pending 시간3. Pending이 끝나고 “open” action을 선택하면 Jupyter가 열립니다.Jupyter(Jupyter Notebook)는 오픈 소스로 라이브 코드, 등식, 코드에 대한 시각화를 위해 사용됩니다. 또한 description을 위한 텍스트 문서(마크다운 등)를 지원하는 웹 어플리케이션입니다. 이렇게 하면 코드에 대한 문서화가 가능합니다. 이 글에서는 Jupyter Notebook을 통해 데이터를 학습하고, 그 데이터를 테스트하겠습니다. 제가 진행한 전체 코드 스크립트(entire script)는 이 글의 마지막 부분에 기술있으니 참고해 주세요.자, 이제 드디어 머신러닝 학습을 시킬 차례입니다. 머신러닝 학습에 꼭 필요한 키워드 두 가지를 뽑아봤는데요. - Dataset: 정제된 데이터와 그 데이터에 대한 label을 정리해 놓은 데이터 모음      - Machine learning Algorithm: 기계학습 알고리즘 우리는 MNIST 데이터셋을 k-means 알고리즘으로 학습시킬 겁니다.1)MNIST Dataset기계학습 알고리즘을 사용할 때 가장 기본적으로 테스트하는 데이터셋으로 MNIST 데이터셋이 있습니다. 이것은 사람이 0부터 9까지 숫자 중 하나를 손글씨로 쓴 이미지 데이터와, 해당 이미지에 대한 레이블(0 - 9)이 6만 개 들어있는 학습 데이터셋입니다. 각 이미지는 가로와 세로가 각각 28 픽셀로서, 각 픽셀은 0부터 255 사이의 숫자가 있습니다. 다시 말해, 하나의 이미지는 28 x 28 = 784개의 숫자로 이루어진 데이터입니다. 하나의 이미지를 나타내는 데이터의 array > length가 784라고 표현할 수 있겠네요.MNIST dataset2)k-means지금 만든 SageMaker 학습 알고리즘은 AWS 튜토리얼에서 제시한 K-means를 사용할 예정입니다. k-means는 label 없이, 즉 정답을 모르는 상태로 학습을 하는 비지도 학습 (unsupervised learning) 알고리즘 중 가장 쉽고 많이 쓰입니다. 정답을 모르니, ‘비슷한 애들끼리 뭉쳐봐’ 라고 하고, 알고리즘은 비슷한 친구들끼리 뭉쳐 놓습니다. k-means에서 k는 ‘k개 덩어리로 뭉쳐주세요’라고 제시하는 숫자입니다. 우리는 0부터 9까지 비슷한 친구들끼리 모이게 하고 싶으니 k=10을 쓸 겁니다.지금부터 해야 할 TO DO!1. MNIST 데이터셋을 다운로드받고, 우리가 학습시키기 좋도록 정제하기(preprocessing)2. Amazon SageMaker를 통하여 데이터 학습시키기(training job)3. Amazon SageMaker를 통하여 학습된 데이터를 배포하기(Deploy the model)4. 배포된 모델에 요청을 보내 테스트 데이터에 대한 예측값을 받아오기(inference)4. Jupyter 노트북 인스턴스 생성하기Jupyter에 New Notebook(conda_python3)을 선택해 새로운 노트북을 생성합니다.5. 학습시키기 위한 기본 셋팅드디어 코딩 시작입니다! (의욕활활) 초기 설정해두었던 IAM role, S3 Bucket, MNIST 다운로드, 다운받은 데이터 등을 확인하세요. 글보다 코드로 주석을 보는 게 가독성이 더 좋습니다. 아래 노트북을 통해 마크다운, 주석처리를 통해 description을 해두었으니 참고 바랍니다.외부에서 MNIST 다운로드가 쉽도록 한 url로 MNIST를 다운받는데 성공했습니다. MNIST 데이터셋 내용물 중 하나를 jupyter notebook에 그려서 제대로 다운 받았는지 show_digit() 함수를 작성해 확인하겠습니다.서른 번째 데이터는 누군가 3을 손글씨로 쓴 이미지입니다.6. 머신러닝 학습하기이 세션에서는 기계학습 알고리즘 설정, 학습할 데이터 경로를 지정하겠습니다. 그 후 MNIST 학습 데이터를 S3 버킷에 옮겨 저장합니다.kmeans.fit() 함수를 호출해 직접 학습을 시켜볼까요? 학습 과정은 상당히 오래 걸린다고 했는데 다행히 4분 만에 학습이 끝났습니다.여기서 잠깐! 여기서 k = 10에 대해서 조금 더 알아보도록 할게요. cluster란 한 지점에 점을 찍고 데이터 분석을 한 뒤, 비슷한 데이터들의 군집을 만들어 주는 것입니다. k-means가 진행되면서 각 cluster의 중심이 서로가 잘 뭉치는 방향으로 이동합니다. 직접 그려봤어요(부끄).7. 학습된 모델을 배포하기학습을 시키면 테스트를 하거나 사용할 수 있어야겠죠? 학습된 모델을 배포해 주세요.8. 배포된 모델 테스트 진행하기배포된 모델에 valid_set 데이터로 검증 데이터를 진행합니다..predict() 함수를 호출하면 새로운 이미지가 어떤 cluster에 속했는지 예측 결과를 알려줍니다. 가장 가까운 cluster가 0번이라고 예측 결과를 반환했네요. 또한 cluster 중심과의 거리는 5.85라고 알려줍니다. 여기서 중요한 점은 cluster 번호와 실제 숫자는 일치하지 않는다는 겁니다. 알고리즘은 임의로 cluster 중심에 번호를 매기는데, 꼭 0번 클러스터가 숫자 ’0’을 뭉쳐놓은 건 아니에요!9. 데이터 예측해보기더 많은 데이터를 예측해볼까요? valid set에 있는 100개 데이터를 예측해봅시다! 각 cluster에 가까운 데이터들이 쭉 선정되었습니다. 정확하지는 않지만 비슷한 숫자 모양들이 서로 군집되어 나타납니다. 0과 2같은 숫자들은 잘 표현되지만, 알고리즘이 9랑 4를 헷갈리거나 5와 3을 헷갈리는 듯 하네요.FASHION MNIST로 SageMaker 머신러닝 학습 및 예측해보기자, 이제 몸도 풀었으니 제가 하고 싶었던 패션 관련 머신러닝 학습 및 예측을 진행해볼게요. 마침 옷 그림으로 MNIST와 매우 비슷한 데이터를 만들어 놓은 fashion-MNIST라는 데이터셋을 발견했어요!1. 패션 관련 MNIST 다운로드 받기패션 MNIST 데이터셋을 우선 다운받아 볼게요! 다운로드는 여기에서 받을 수 있습니다. 총 네 개의 파일을 다운로드 받으세요.- train-images-idx3-ubyte.gz : train set 이미지  - train-labels-idx1-ubyte.gz : train set 레이블  - t10k-images-idx3-ubyte.gz : test set 이미지  - t10k-labels-idx1-ubyte.gz : test set 레이블  다운로드 받은 패션 Mnist의 label은 아래와 같이 되어 있습니다. 숫자 0부터 9 대신에 각 이미지가 어떤 이미지인지 텍스트로 표현되어 있어요.LabelDescription0T-shirt/top1Trouser2Pullover3Dress4Coat5Sandal6Shirt7Sneaker8Bag9Ankle boot2. Fashion-MNIST 데이터셋을 이전에 사용했던 mnist.pkl.gz 와 같은 형태로 변환해주는 스크립트 작성해주기위에서 연습할 때는 mnist.pkl.gz 한 개 파일만 사용했는데요!?! 그래서 다운로드 받은 네 개의 파일을 똑같은 형식의 파일 하나로 만들어주는 파이썬 스크립트를 작성해 fashion-mnist.pkl.gz 파일로 만들었어요.import gzip import pickle import numpy as np # MNIST 데이터셋은 train, test 셋이 각각 image, label로 나누어 저장되어있는 4개의 파일로 구성 test_image_path = 't10k-images-idx3-ubyte.gz' test_label_path = 't10k-labels-idx1-ubyte.gz' train_label_path = 'train-labels-idx1-ubyte.gz' train_image_path = 'train-images-idx3-ubyte.gz' out_file_name = 'fashion-mnist.pkl.gz' # train label / images 추출 with gzip.open(train_label_path, 'rb') as train_label_f:     train_label = np.frombuffer(             train_label_f.read(), dtype=np.uint8, offset=8).astype(np.int64)   with gzip.open(train_image_path, 'rb') as train_image_f:     train_imgs = np.frombuffer(             train_image_f.read(), dtype=np.uint8, offset=16).reshape(-1, 784).astype(np.float32)   # test label / images 추출 with gzip.open(test_label_path, 'rb') as test_label_f:     test_label = np.frombuffer(test_label_f.read(), dtype=np.uint8, offset=8).astype(np.int64)   with gzip.open(test_image_path, 'rb') as test_image_f:     test_imgs = np.frombuffer(             test_image_f.read(), dtype=np.uint8, offset=16).reshape(-1, 784).astype(np.float32)   # 기존 60000개 training set에서 50000개는 train set으로 사용하고, 10000개는 valid set으로 활용 train_label, valid_label = train_label[:50000], train_label[50000:]  train_imgs, valid_imgs = train_imgs[:50000], train_imgs[50000:]   # train set, validati on set, test set을 튜플 자료형으로 저장 out_data = ((train_imgs, train_label),             (valid_imgs, valid_label),             (test_imgs, test_label))   # pickle file로 dataset 데이터 포맷 맞춰주기 with gzip.open(out_file_name, 'wb') as out_f:     pickle.dump(out_data, out_f) 이 과정을 통해 나온 결과물, fashion-mnist.pkl.gz 를 Jupyter Notebook이 있는 경로에 업로드합니다.fashion-mnist.pkl.gz가 업로드 되었습니다!3. 머신러닝 학습하기아까 사용했던 활용했던 숫자 MNIST 스크립트를 그대로 사용하겠습니다. show_digit()을 이름만 바꾼 show_fashion()으로 데이터를 살펴보니 드레스가 보입니다.조금 전에 했던 숫자 MNIST와 똑같은 과정을 SageMaker를 이용해, 학습 → 테스트 → 예측해보니 아래와 같은 예측 결과를 얻을 수 있었습니다. 신발은 신발끼리, 바지는 바지끼리, 가방은 가방끼리 분류된 게 너무나 신기합니다. (아까 진행한 숫자보다 더 학습이 잘 된 것 같은건 기분 탓일까요…?)머신러닝이라고 겁내지 않아도 됩니다! 유저들에게 더 좋은 서비스 제공할 수 있으니까요. 지금까지 브랜디 개발2팀의 단아한 개발자 오연ㅈ….참사를 막아주세요.앗, 잠시만요!! 중요한 것을 놓칠 뻔 했네요.저처럼 테스트를 하면 그냥 지나치지 마세요. 자동 결제로 출금되는 뼈 아픈 경험을 할 수도 있습니다. 반드시 이용했던 서비스들을 stop 하거나 terminate 해주세요. (Clean-up단계) 자세한 내용은 여기를 클릭하세요.지금까지 Brandi 개발 2팀, 단아한 개발자 오연주였습니다!# entire script (숫자 Mnist) # 오호 드디어 coding start! # 이제부터 Brandi의 단아한 개발자, 저를 따라오시면 됩니다 :) # 노트북 Block을 실행하는 방법은 Shift + Enter 입니다 from sagemaker import get_execution_role role = get_execution_role()  # 초기에 설정해 뒀던 IAM role 가져오기 bucket = 'sagemaker-julie-test' # 초기 단계에 만들었던 S3 Bucket 이름 적기 %%time import pickle, gzip, numpy, urllib.request, json   # 여기서 잠깐, 생소한 라이브러리 설명을 드릴게요! # pickle: python식 데이터 압축 포맷 # numpy: 수치 계산을 하기 위한 python package # Load the dataset urllib.request.urlretrieve("http://deeplearning.net/data/mnist/mnist.pkl.gz", "mnist.pkl.gz") with gzip.open('mnist.pkl.gz', 'rb') as f:     train_set, valid_set, test_set = pickle.load(f, encoding="latin1")     # matplotlib로 그리는 그림이 jupyter 노트북에 바로 보여줄 수 있도록 설정 %matplotlib inline import matplotlib.pyplot as plt # 도표나 그림을 그릴 수 있게 해주는 라이브러리 plt.rcParams["figure.figsize"] = (2, 10) # 그림의 크기 지정 def show_digit(img, caption='', subplot=None):     if subplot is None:         _,(subplot) = plt.subplots(1,1)         imgr = img.reshape((28, 28))     subplot.axis('off')     subplot.imshow(imgr, cmap='gray')     plt.title(caption)   # train_set의 그림과[0] 데이터 이름[1]을 예시로 보여준다 show_digit(train_set[0][30], 'This is a {}'.format(train_set[1][30]))   # 학습을 하기 위해 학습 알고리즘 및 데이터 경로 설정! from sagemaker import KMeans data_location = 's3://{}/kmeans_highlevel_example/data'.format(bucket) output_location = 's3://{}/kmeans_example/output'.format(bucket)   print('training data will be uploaded to: {}'.format(data_location)) print('training artifacts will be uploaded to: {}'.format(output_location))   kmeans = KMeans(role=role,                 train_instance_count=2,  # 장비 2대를 사용하여 학습하겠어요!                 train_instance_type='ml.c4.8xlarge',                 output_path=output_location,                 k=10,  # 아래 그림을 참고해 주세요!                 data_location=data_location) %%time   # 학습 시작! kmeans.fit(kmeans.record_set(train_set[0]))   %%time # 모델을 만든 후 사용하기 위하여 배포하기 kmeans_predictor = kmeans.deploy(initial_instance_count=1,                                 instance_type='ml.m4.xlarge')                                  # valid_set에 30번째 sample을 테스트 해보기 result = kmeans_predictor.predict(valid_set[0][30:31])  print(result)   %%time   # vaild_set에 있는 0번부터 99번까지의 데이터로 cluster를 예측 해보자 result = kmeans_predictor.predict(valid_set[0][0:100])   # 예측 결과에 대한 cluster 정보를 수집 clusters = [r.label['closest_cluster'].float32_tensor.values[0] for r in result]   # 각 cluster별 예측된 이미지 출력 for cluster in range(10):     print('\n\n\nCluster {}:'.format(int(cluster)))     digits = [ img for l, img in zip(clusters, valid_set[0]) if int(l) == cluster ]     height = ((len(digits)-1)//5)+1     width = 5     plt.rcParams["figure.figsize"] = (width,height)     _, subplots = plt.subplots(height, width)     subplots = numpy.ndarray.flatten(subplots)     for subplot, image in zip(subplots, digits):         show_digit(image, subplot=subplot)     for subplot in subplots[len(digits):]:         subplot.axis('off')     plt.show() 출처Getting Started - Amazon SageMaker CodeOnWeb - 머신러닝 초보를 위한 MNIST fashion-mnist 글오연주 사원 | R&D 개발2팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발문화 #개발팀 #업무환경 #인사이트 #경험공유
조회수 4195

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
조회수 1120

제니퍼 개발 이야기(UI/UX)_ 좀 더 쉽고 빠르게 더 멋지게 모니터링하자.

APM ,변화의 시작기업용 소프트웨어의 UI는 어렵다. 특히 애플리케이션 성능 관리(Application Performance Management, 이하 APM) 제품의 경우 더욱더 그렇다. APM은 애플리케이션의 성능 모니터링과 장애 예측을 통해 최적의 애플리케이션 상태를 보장하고 관리하는 일련의 시스템 관리 체계다. 사용자들은 애플리케이션의 성능을 모니터링하고 경우에 따라 발생할 수 있는 장애를 신속히 감지 및 예방해, 기업이 보유하고 있는 정보시스템의 성능을 최적의 상태로 유지하기 위해 APM을 구매하여 사용한다.  그런 이유로 초기 APM은 특정 부서나 특정 분야의 전문가만 다룰 수 있는 제품이었다. 사용법이 복잡하고 데이터의 분석을 통해 장애의 원인을 수동적으로 찾아야 하는 까닭에 APM 제품은 애플리케이션 모니터링 담당자나 개발자 등 전문가만 이해할 수 있는 영역으로 인식돼 왔다. 이런 APM이 최근 여러 현업 부서에서 사용하기 시작하면서 APM의 UI/UX에도 변화가 시작되었다. 기업 내 비즈니스가 다양해지고 복잡해 지면서 기업이 운영하고 관리하는 서비스에 대한 안정성 확보가 중요한 화두로 떠올랐다. 특히 APM은 서비스 지연이나 장애를 감지하고 대처하기 위한 영역으로 확장되고 있는데 모바일 등의 서비스를 통한 비즈니스 기회와 수익이 높아지고, 시장 경쟁이 더욱 치열해지면서 사용자 응답 지연이나 서비스 에러와 같은 문제들은 고객들에게 바로 다른 서비스로 전환을 하게 만드는 원인을 제공하게 된다. 이는 비즈니스 관점에서 매출이나 고객 충성도에 많은 영향을 미칠 수 있기 때문에 기업 내 IT 부서나 사용자 중심적인 서비스 관리 또한 중요한 관리 영역이 되고 있다.< 제니퍼소프트 개발자들이 만든 UI Framework인 JUI, 오픈소스로 공개하였다>APM, 사용자 만족을 위해 UI/UX를 더 쉽고 직관적으로 설계 제니퍼소프트의 APM 솔루션인 「제니퍼(JENNIFER)」는 이해가 어려운 제품을 사용자들이 좀 더 쉽게 이용하게 할 수 없을까 하는 고민에서 출발한 제품이다. 대게 APM 제품은 모니터링 제품의 특성상 이용자들이 모니터링 화면을 전광판으로 띄워 놓고 보거나 데스크에 서브 모니터를 두어 애플리케이션의 운영상태를 상시 모니터링한다. 만약 APM 의 UI/UX가 복잡하여 가시성 확보가 어렵거나 사용하기 어렵다면 중요한 정보를 즉각적으로 파악할 수 없다. 이런 이유로 제품의 UI가 보여주는 데이터의 시각화는 비전문가라 할지라도 애플리케이션의 성능 이상을 인지할 수 있도록 설계되어야 한다.제니퍼는 이런 모니터링의 중요한 본질을 담아 설계하였다. 2005년부터 사용자가 직관적으로 모니터링할 수 있는 뷰에 관심을 기울였는데, 이때만 해도 엔터프라이즈 제품은 UX/UI에 신경을 쓰지 않던 시기였다. 대부분의 APM 제품은 분석적인 요소가 강한 외산 제품이 출시되어 사용되고 있었고 사용자들은 사후 분석 요소가 강한 APM의 UI/UX에 아쉬움을 느꼈다.APM은 문제가 발생한 시점에 이를 직관적으로 인지하고, 분석하여 문제 해결에 실질적인 도움을 주고 이를 통해 얼마나 빠른 장애 대응을 할 수 있냐가 가장 중요한 관건이다. 제니퍼는 제품에 이런 기능의 의미와 가치에 대한 중요한 요소를 놓치지 않았다. 핵심적인 기능을 하나 추가하거나 화면의 적절한 유기적 배치, 심지어 그래프 색감을 선택하는 디자인적 고려에서 조차도, 요건이나 형식 맞추기에 급급하지 않았다. 해당 기능의 본질적인 의미와 가치와 쓰임새를 찾아 담기 위해 노력했다. 제니퍼 출시 이후, 국내 고객은 사후 분석에 치중한 해외 제품을 쓰지 않고 있다. APM 경쟁 업체들 또한 UI/UX에 많은 고민을 하고 있으며, 제품에 이를 반영하고 있다. 그리고 그 중요성은 점점 더 커지고 있다.  사용자의 폭이 넓어지고 관리해야 하는 시스템이 많아지면서 좀 더 쉽고 빠르게 혹은 직관적으로 문제를 파악하고 해결할 수 있는 UI/UX 제품을 선호하게 된 것이다.  이는 사용자에게 좋은 경험을 주는 것이 제품의 경쟁력을 가질 수 있는 중요한 요소임을 반증하는 것이 아닐까 한다. 제니퍼 개발 이야기 _ 2편 <제니퍼 제품 UI/UX 특징>
조회수 1463

AWS, Kubernetes 그리고 WAF

모니터링을 지속적으로 강화하다 보니 사용자 약관에 어긋나게 행동하는 주체가 눈에 띄기 시작한다. 특정 시간대에 판매 목록을 긁어가려고 시도하는 크롤러가 대표적이다. 비정상적인 서비스 이용을 탐지한 건 좋은데 이를 어떻게 차단할지가 또 고민이다. 차단 방법이야 많지만 가급적유지보수가 쉽고현재 서비스 구조에 살짝 얹기만 하면 되는그런 멋진 구조가 없을까 잠시 조사를 해보았다. 결론적으로는 현재 우리의 구조에선 1시간만 작업하고 펑펑 놀아도 되는 그런 방법은 없었다. 하지만 조금만 더 참고 기다리면 꽤 괜찮은 접근방법이 있을 것도 같더라. 우선 현 상황을 보자면 우리의 인프라는 주로Kubernetes가 서비스의 90% 이상을 통제하며웹 서비스는 주로 AWS ELB를 통해 인터넷 망에 노출한다.그러니 이론상으로는 AWS의 WAF, 그러니까 Web Application Firewall을 이용하면 손 안대고 코 풀기가 딱이다. 하지만 문제가 하나 있으니!!!AWS WAF는 ELBv2 그러니까 Application Load Balancer만 지원하는데 Kubernetes 1.5.x는 ELBv1만 지원한다. AWS WAF가 L4 로드밸런서인 ELBv1을 지원하던가 Kubernetes가 AWS ELBv2도 External Load Balancer로 선택가능하게 지원하던가 해야 Kubernetes + AWS ELB + WAF를 조합할 수 있다. 이 문제만 해결되면 금방 적용가능한 구성이라 매우 땡긴다. 설사 Kubernetes이 ELBv2는 지원하되 WAF 연동을 지원하지 않더라도 이를 수행하는 Kubernetes 플러그인을 개발하는 건 이틀이면 충분하지 싶다.왜 WAF인가?그러고 보니 여태 왜 이런 구성이 제일 낫다고 생각하는지 설명하지 않았다. 웹애플리케이션 방화벽을 구현하는 방법이야 AWS WAF 말고도 많지만 이러한 구성에는 분명한 장점이 있는데IP 평판 목록을 수집해서 한데 정리하는 서비스를 AWS가 제공하기 때문에 내가 이걸 구현한다고 시간낭비할 필요 없고매우 간단한 구조라서 처음 설치하고 설정하는데 30분에서 1시간이면 족하고Classless Inter-Domain Routing (CIDR) 표기법을 지원하므로 특정 아이피 대역을 막는 건 일도 아니며무엇보다 내가 관리하는 평판 목록도 쉽게 추가할 수 있다.이러니 “굳이 다른 솔루션을 찾아서 생고생해야 하나?”라는 생각이 들 수밖에 없다.다른 읽을꺼리How to Import IP Address Reputation Lists to Automatically Update AWS WAF IP Blacklists: AWS WAF의 구조와 WAF를 CloudFront에 적용하는 방법을 설명한다.AWS WAFがALB(Application Load Balancer)で利用出来るようになりました: AWS WAF를 ELBv2에 적용하는 방법을 설명한다.Akamai — Protect your organization with a web application firewall.Originally published at Andromeda Rabbit.#데일리 #데일리호텔 #개발 #개발자 #개발팀 #인사이트
조회수 1593

냉정과 열정 사이

냉정과 열정 사이라는 소설이 있다. 대학교 때 읽었던 소설인데 두 사람의 여정을 각자의 시선에서 다룬 소설이다. 에잇퍼센트에 인턴으로 입사해 9개월간 일하고 훨훨 날아간 병훈님과 나도 이 소설처럼 각자의 시선에서 지난 9개월을 되돌아보려 한다. (경고한다. 로맨틱하지 않다.)병훈님이 떠나는 날. 아마 여러분이 보는것과 내가 이 사진을 보는 느낌이 많이 다를거다.1. 만나기까지- 소병훈 이야기2015년 대학교 3학년이 시작될 때부터 졸업 이후에 대한 고민이 생겨났다. 대학원 진학과 취직은 수많은 대학생들의 공통된 고민이기에 수많은 조언이 넘쳐나지만 결론은 '나에게 맞는 길'을 선택하는 것이다. 내 인생 내가 선택해야지 언제까지 남들 좋다는 길로만 가겠는가. 둘 다 겪어보고 내가 선택하겠다고 다짐했다.졸업을 위해서는 대학원에서 과제연구를 1년 해야 했기에 대학원은 겪어 볼 수 있었다. 그러면 취직도 경험해보고 싶은데 어떻게 하지? 대기업에서 1~2개월 인턴을 했던 친구들에게 물어보니 한결같이 '놀고먹다 보니 월급이 나온다'는 경험담을 늘어놓았다. 하지만 정말로 취직해서 놀고먹으면 잘리겠지. 대기업 인턴은 패스. 스타트업 관련 세미나에서 한 VC의 '스타트업은 망해도 스타트업 인턴은 망하지 않는다'는 말을 들었다. 창업에 생각이 있으면 스타트업에서 인턴을 해보라는 말이었다. 그래서 '스타트업에서 일해보자'라고 결정했다.수많은 스타트업 중에서 왜 에잇퍼센트를 선택했다고 물으신다면, 빠른 속도로 성장하면서 변하고 있는 스타트업 속에서 일해보기를 원했기 때문이다. 그리고 당시의 나는 CTO의 멋진 말 한마디에 눈을 반짝이며 '이 회사에서 이 사람과 일하고 싶다'라고 생각하면 앞뒤 안 가리고 지원하는 이상주의자였다. 그래서 페이스북에서 호성님의 글을 읽고 '이 회사가 내가 생각하던 회사구나'라는 생각이 들어서 먼저 지원했던 회사를 포기하고, 에잇퍼센트 입사를 간절히(?) 원하게 되었다.- 이호성 이야기2016년 1월의 첫 번째 근무 날. 대표님이 모두를 불러 모았다. 그리고는 회사의 투자 유치 소식을 알려 주었다. (무슨 투자 유치 소식을 "오늘 저녁에는 치킨을 시켜 먹기로 했어요." 수준으로 재미없게 이야기하는지 모르겠다.) 투자를 받는 것이 확정되었으니 대표님이 내게 전달해 주신 미션은 개발자를 채용해서 제품 개발의 속도를 높이라는 것이었다. 사실 에잇퍼센트에 오기 전에 한 회사에만 오래 있기도 했고 개발자들과의 네트워킹도 게을리했던 터라 당장 좋은 개발자를 뽑을 수 있는 방법이 없었다.그래서 블로그에 회사를 알리는 글을 쓰기 시작하면서 주위 분들께 추천을 부탁드렸다. 그중 JDLab의 양주동 대표님이 괜찮은 학교 후배를 추천해 주신다고 해서 속으로 쾌재를 불렀다. 하지만 추천해 주신 친구가 애매하게 9개월만 일할 수 있는 상황이라고 하니 고민이 되었다. 주니어가 실제로 일을 잘 하게 되려면 꽤 긴 시간이 필요한데, 실제로 일을 잘할 수 있게 되었을 때쯤 회사를 떠나는 게 아닐까 하는 생각이 들었기 때문이다. 하지만 인력(당시 4명)에 비해 해야할 일이 너무나도 많았기에 누군가의 손이라도 빌려야 할 판이었다. 그래서 일단 병훈님을 만나 보기로 했다.2. 면접- 소병훈 이야기에잇퍼센트에 들어가는 과정은 상당히 길다.처음에 간단한 티타임을 시작으로 실제 코딩 문제를 풀어보게 하고, 그 뒤에서 다시 1대 n으로 토론하는 과정, 그리고 대표님과의 이야기로 면접이 이어진다. 요즘은 논술 문제도 있다고 들었다. (역시 취직은 어려워)내 경우는 '면접 보려는 것은 아니니 그냥 커피 한잔 하자'는 부님의 간단한 속임수에 넘어가 티타임을 가졌다. 카페에서 커피 한잔 하면서 부님과 에잇퍼센트는 어떠냐고 물어보려고 왔는데, 어느새 내 앞에는 호성님이 앉아 있었고, 메일로 코딩 문제를 받는 것으로 커피 한잔이 끝났다. 이 티타임은 면접보다는 나에게 회사를 소개하고 회사가 나에게 적합한지 보는 과정이었다.코딩 문제는 성호님의 글로 유명해진 pingpong을 포함한 take-home 과제였다. 문제를 받은 다음날 다른 회사 면접을 보고 온 뒤 밤샘으로 문제를 풀었던 것과 제출할 때 pingpong 문제만큼은 자신 있어했던 기억이 난다. 그때의 기억을 떠올리며 당시에 제출했던 코드를 보니 'Assignment를 쓰지 말 것'이라는 조건이 깨져있었다. 자신감 넘치던 과거의 내가 부끄러워지는 순간이다.마지막 면접 과정도 조금은 숨 막히는 경험이었다. 가볍게 대화하는 분위기 속에서 대학에서 들었던 전공과목 별로 하나하나 물어가며 내 지식의 바닥을 확인했다. 대학에서 3년간 들었던 전공과목은 많지만, 질문 들어오는 족족 '모르겠습니다' 밖에 할 수 없었다. 내가 답할 수 있는 수준을 찾으시려는지 점점 질문의 난이도가 낮아졌고, 마지막으로 스택과 큐를 물어보는 질문에 답하면서 '이 회사는 못 들어가겠구나'라는 생각이 들었다.동시에 진행했던 다른 회사에서 합격 메일이 왔기에 에잇퍼센트에 '0월 0일까지 합격/불합격 결과를 알려주세요'라는 당당한 요구를 한 뒤 떨어졌다는 메일을 기다리고 있었다. 하지만 예상치 못한 합격 메일을 받았고, 그 메일에는실력에 대해서는 회사에 오셔서 보여주세요라는 잊지 못할 문구가 있었다.그리고 첫 출근, 4월 4일 9시 20분에 출근해서 잠긴 문을 보며 에잇퍼센트의 첫 날을 맞이했다.- 이호성 이야기병훈님이 왔다고 하셔서 학교 선배인 부님과 함께 회사 옆 '피어나' 카페로 갔다. (당시만 해도 사무실에 회의실이 없어서 모든 미팅을 회사 옆 카페에서 해야만 했다.) 병훈님의 첫인상은 “꺼벙이"였다.공대에서 흔히 볼 수 있는 스타일. 하지만 말하고 이야기하는 것은 번뜩이는 느낌은 크게 들지 않았다. 아마 나정도로 평범한 느낌이었던 것 같다. 이야기를 하다 보니 다른 곳에 면접을 이미 본 상태였다. 일단 우리 회사와 나에 대해서 좋은 감정을 갖게 하는 것이 좋겠다 싶어서 이래저래 약을 팔았다. 그리고 면접 문제를 메일로 전달하겠다고 하고 첫 번째 만남을 마쳤다.며칠 뒤 제출한 과제를 가지고 다시 한번 병훈님을 만났다. 전공에 관련된 기본적인 질문들을 던졌다.(정확히는 졸업한 지 10년이 지나서 그냥 내가 기억나는 것들을 물어보았다.) 그런데 10개의 질문을 던지면 8개의 질문 은 원하는 답을 듣지 못했다. 실망했다. 겸손하고 배움의 자세가 갖춰져 있는 친구라는 생각은 들었다. 하지만 이렇게 모르는 친구를 뽑아도 될까 하는 생각이 들었다.하지만 뽑았다. 솔직히 그냥 학벌을 보고 뽑았다. 좋은 학교에 다니고 있는 친구이니 지금까지 최소한 한 번쯤은 최선을 다해 본 적이 있겠지 라는 생각을 했다. 무엇보다 당시 나는 꽤 급했다.합격 메일에는 ‘실력에 대해서는 회사에 오셔서 보여주세요'라는 내용을 적어서 보냈다. 부족한 만큼 회사에 와서 최선을 다해주었으면 하는 마음이었다. 그리고 입사할 주에 있을 워크숍 준비에 대한 요청도 함께 드렸다.지금 와서 이야기하는 것이지만 최근에 병훈님을 면접 봤다면 떨어뜨렸을 거다. 생각해 보면 이게 면접, 특히 주니어 면접의 어려움이다. 그 사람이 입사해서의 2주 정도는 예상해 볼 수 있지만 그 뒤는 예상하는 것은 너무나 어려운 일이다.3. 들어와서 처음 했던 일- 소병훈 이야기들어와서 처음으로 했던 일은 나를 대학생에서 직장인으로 바꾸는 일이었다.회사에 들어온 지 1~2개월이 지났을 때 외부 업체와 전문 통신을 개발하는 작업을 맡았다. 대학교에서 두 PC 사이의 전문 통신 프로젝트를 만들었던 기억이 있어 충분히 혼자서(그리고 짧은 기간에) 만들 수 있으리라 생각하고 작업을 시작했다. 기존의 코드를 조금씩 수정하고 추가하던 이전의 작업들과는 다르게 처음부터 만들어내는 일이었다.이 일을 하면서 지금까지 '하나의 동작을 하는 무언가'를 100% 혼자서 만든 적이 없다는 것을 깨달았다. 항상 기본 틀을 받아서 코딩하고, 어려울 때는 모범 답안을 보면서 힌트를 얻었으며, 그러고도 힘이 부치면 7~80%만 완성하고 (시간이 없었다는 핑계를 대면서) 넘어갔었다. 회사에서는 이 일이 '소켓 통신의 이해를 확인하기 위한 프로그래밍'이라고 설명되어 있지 않았고, 어디서 버그가 발생했는지 힌트를 얻을 수 없었다.대학 강의로 들었던 내용들과 전혀 다른 지식들이 필요했지만, 필요한 기초적인 요소들은 구글에서 찾을 수 있었다. 하지만 어떤 키워드를 검색해야 하는지부터가 문제였다. 검색해야 하는 단어를 알아내려고 시니어 개발자님들께 돌아가면서 물어봤다. (너무 자주 물어야 해서 한 분에게만 묻기 죄송했다.) 처음에는 학교에서 배운 내용은 쓸모없다고 생각했었는데, 지금 생각해보니 그마저도 없었으면 구글과 위키의 내용도 이해 못했을 것 같다.웹 개발에 대한 기초도 없고, 어디가 끝인지 확신도 없어서 개발 시간이 길어졌다. 야근을 반복했다. 노력한다고 해서 없던 능력이 생기지는 않았고, 결과로 커다란 똥덩어리 같은 코드가 만들어졌다. 다행히도 (달리는 중간에 몇 개의 부품을 갈아 끼운 이후에) 최소한의 기능은 정상적으로 돌아갔다.그렇지만 이 코드가 12월까지 구린 냄새를 피우고 있었다. 에러를 만들지는 않지만 가독성이 떨어지고 창의적인 구조 때문에, 유지/보수를 할 때마다 과거의 내 실력을 확인하는 좋은 지표가 되었다.- 이호성 이야기시간이 많다면 병훈님을 옆에 앉혀 두고 차근차근 알려주고도 싶고, 같이 스터디도 하고 싶었지만 내게는 당장 해야 하는 일이 쌓여 있었다. 다행히 팀에 계신 시니어 개발자 분들이 병훈님일 이래 저래 잘 돌봐 주었다. (20살이 넘는 청년에게 "돌봐 주었다"라는 표현이 적당한 가에 대해서 곰곰 생각해 보았는데, 흠. 역시 적절하다.) 병훈님께 처음 한 달 동안은 조각을 고치는 일, 작지만 급한 일 들을 맡겼다. 덕분에 시니어 개발자들이 다른 일들에 집중을 할 수 있었다. 그리고는 한 달이 지나자 하나의 일을 떼어서 맡겨볼 수 있겠다는 생각이 들었다. 단순히 개발하는 일뿐만 아니라 다른 회사 개발자들과 커뮤니케이션을 직접 하면서 일을 할 수 있도록 했다. 병훈 님은 잊어버렸을지는 모르겠지만 외부 업체로 처음 전화를 걸었을 때 우리 팀의 시니어 개발자들은 모두들 키보드에서 손을 놓고 병훈님의 대화를 노심초사하면서 듣고 있었다.이 프로젝트는 곧 병훈님이 예상한 일정을 넘어섰고, 얼마 이후에는 내가 예상한 일정도 넘어섰다. 병훈님이 끙끙 앓고 있는 게 보였다. 그 일을 다른 사람에게 넘겨야 하는가의 고민도 여러 번 했다. 병훈님이 만들어 낸 창의적(이라고 말하고 싶겠지만 상식을 벗어난)인 코드들을 뜯어고치고 싶다는 생각도 했다. 하지만 시간은 지났고 테스트를 통과한 코드는 에잇퍼센트 프로덕트의 한 자리를 차지했다.4. 무엇을 배웠을까?- 소병훈 이야기첫 번째로 회사가 어떻게 돌아가는 지를 배웠다. 작은(?) 스타트업이었기에 개발팀 외 다른 팀원들과도 친하게 지낼 수 있었고, 회사 내에서 생기는 사건들을 전부 들을 수 있었다. 그렇게 아이디어가 나오는 순간부터 제품으로 완성되는 과정을 볼 수 있었다. 또한 회사의 크고 작은 의사 결정 과정을 지켜볼 수 있었는데, 모든 의사 결정들에 원인과 논리적인 과정이 따른다는 점이 재밌었다.내가 알지 못하는 원인들과 다른 사람들이 결정하게 된 이유가 궁금해서 여기저기 물어보았고, 모두들 숨기지 않고 말해 주었다. 대표님과의 티타임에 찾아가서 묻기도 하고(모두에게 열려있었는데 단 2명이 왔었다), 퍼포먼스 마케팅이 궁금하다며 점심시간에 옆에 앉아 이야기하고, 전화 응대를 어깨너머로 들어보기 했다. 글로 적어보니 처음 초등학교에 들어간 8살 아이 같기도 하지만, 에잇퍼센트에 있으면서 물어보는 만큼 알 수 있었고, 그만큼 이전과는 다른 세상을 알 수 있었다. (그리고 그만큼 야근을 했다.)두 번째는 개발자가 되는 과정을 배웠다. 당연히 개발 실력도 늘었지만, 조금 더 보태서 개발자가 되는 과정을 배웠다고 말하고 싶다. 누구라고 말할 것 없이 남는 시간을 조금씩 쪼개 공부하고 있는 사람들, 새벽 4시가 넘었음에도 꼼꼼히 기록을 남기며 마무리하는 야간작업, 그리고 혼자서는 만들 수 없는 거대한 코드를 점진적으로 만들어가는 개발팀을 보면서 개발자라는 직업을 만날 수 있었다. 내가 본 개발자는 (에잇퍼센트의 개발자만의 특징일 수도 있지만,) 모든 결과를 '우연'으로 넘기지 않고 원인을 찾았고, 원하는 분야를 찾아서 스스로 공부하고, 삶의 즐거움을 하나씩 가지고 있는 사람들이었다.마지막으로 나 되돌아보기. 나는 내 실력을 과대평가하고 있었다. 회사에 들어오면서 '열심히 하겠다'라고 말했지만 몇 개월 '열심히' 뛰어서 갈 수 있는 거리에는 한계가 있었다. 다른 사람들이 불가능하다고 말해도 혼자서 '노력하면 할 수 있다'라고 생각하면서 오기로 붙잡고 있다가 결국 기한을 넘긴 적이 많았다. 시간이 지나면서 '할 수 있을 것 같다'는 자신감이 아니라 완성된 결과물을 보면서 실력을 확인했다. 항상 자신감이 넘치다 보니 매번 내 생각보다 실력이 뒤에 있었고, 그런 생각이 들 때마다 기숙사에서 공부를 했다.그렇지만 나를 과대평가했던 것처럼 나의 목표도 과대평가 했었다. 내가 도달하려고 했던 목표도 꾸준히 달리면 도달할 수 있는 거리에 있었고, 생각보다 멀리 있지 않았다. 다만 '꾸준히'의 기준이 몇 주, 몇 개월이 아니라는 것을 배웠을 뿐이다.- 이호성 이야기내가 입사하기 전에 에잇퍼센트에 여러 명의 개발 인턴이 있었다고 했다. (commit log에서만 만날 수 있는 그대들이여. 왜 버그를 내게 주고 갔는가.) 그리고 한 명을 제외하고는 회사를 모두 떠났다. 처음에 대표님이 인턴 채용 제안을 몇 번 하셨을 때 개발팀에는 인턴을 채용하지 않겠노라고 말했었다. 사람이 전부인 개발팀에서 떠나는 것이 예정된 사람을 뽑고 싶지 않은 마음이었다. 하지만 병훈님은 이런 내 생각을 바꿔 놓았다.연말 평가에서 성장에 대한 상을 받을 만큼 병훈님의 성장은 눈부셨다. 이제 좋은 주니어는 무엇인가에 대해서 병훈님을 기준으로 생각을 할 수 있게 되었다. 주니어 채용에 대한 성공체험을 했다고도 할 수 있겠다.상은 병훈님이 받는데 주는 사람이 더 좋아하네?좋은 주니어는 당연하게도 일정 시간이 지났을 때 높은 곳에 올라갈 수 있는 사람이다. 높은 곳에 올라가기 위한 조건은 다음과 같은 것들이 있다.1) 상대적으로 이미 높은 곳에 있을 것 전설 속에만 존재하는 시니어 같은 주니어 되시겠다. 고등학교, 대학교 때 많은 지식과 경험을 쌓아서 이미 현업에서 잘할 수 있는 친구들이다.2) 인지능력, 학습능력문제를 이해하고 정의하는 능력이 뛰어나고 논리적인 사고를 한다. 속칭 똑똑한 친구들이다. 문제를 자세하게 설명해 주지 않아도 문제의 본질을 이해할 수 있고, 답으로 가는 길을 빠르게 찾아낼 수 있다. 새로운 것을 빠르게 익히고 배움에 대한 두려움이 없다.3) 지적겸손배움에 대해 열린 자세를 가지고 있어야 한다. 나는 개인적으로 주니어의 경우 이 능력을 "내갈굼력"이라고도 부른다. 다른 사람들에게 지적과 갈굼을 받으면서도 그것이 배움으로 이어진다면 감사한 마음으로 받아들인다. 감사한 마음은 다시 지식을 전해주는 사람에게 긍정적인 피드백이 되어 더 많은 것을 알려주게 되는 선순환 구조를 만들어 간다.4) 태도긍정적이고 도전적인 태도를 갖추어야 한다. 자신의 인생을 발전적으로 개척해 나갈 태도. 그리고 자신을 둘러싼 환경에 감사하는 태도. 이 태도는 팀에도 많은 영향을 미친다.병훈님을 면접 볼 때의 나는 1) 만을 중요하게 생각했기에 병훈님을 떨어뜨려야 하나 하고 생각했다. 하지만 이제 와서 뒤돌아 보니 병훈님은 2), 3), 4)을 모두 갖추고 있는 인재였다. 아마 몇 년 뒤에는 1)도 충분히 갖추게 되리라.5. 어떻게 일했나?- 소병훈 이야기 9개월 동안 에잇퍼센트를 다니면서 항상 내 능력으로 조금 힘들지만 불가능하지 않을 만큼 업무가 들어왔다. 스프린트(2주) 단위로 업무를 나눠가지는데, 일방적으로 업무를 할당받지 않고 팀 회의로 업무를 나눠갖는다. 호성님이 업무를 강요하지도 않고 업무 일정도 각자가 정하지만, 모두가 보고 있다는 느낌과 '스스로를 과대평가하는 나' 때문에 매번 촉박한 일정에 시달리게 되었다. 그렇게 나는 손을 들고 해당 업무의 책임자가 된다. 초반에는(전문 개발할 때까지)는 아예 질문하지 않아서 혼자 끙끙 댔는데, 너무 안쓰러워 보였는지 옆에 앉은 연태님이 먼저 도와주셨다. 시간이 지나면서 길이 명확하게 보이지 않을 때는 시니어 개발자(대부분 연태님)에게 물어보면서 일을 진행했다. 어느 날 호성님이 에잇퍼센트처럼 '실패하면서 성장할 수 있는 환경'이 다른 회사에서는 없다고 말했었다. 생각해보니 내가 자유롭게 개발해도 테스트와 코드 리뷰를 거치면서 문제를 잡아낸다. 그러고도 버그가 생기면 실서버에서 디버깅해서 문제를 해결한다. 심적으로 매우 죄송한 마음이 들지만 추가적으로 다른 벌은 받지 않았다. 일을 시작하기 전부터 지레 겁먹을 필요는 없었다. 그 뒤로 길이 희미하더라도 우선 걸어가 봤다. 그러다가 도저히 길이 보이지 않을 때, 조언을 받는 방식으로 일을 진행했다. 시간이 더 오래 걸리면서 최종 결과물의 수준이 떨어지는 상황이 발생했지만, 코드 리뷰를 받으며 최소한의 수준은 맞춰졌다. (그러면서 시간은 더 오래 걸린다.) 최대한으로 생각해서 만들어도 항상 놓치는 부분이나 더 간단한 해결 방법이 있었고, 그때 느끼는 아쉬움과 안타까움이 다음 개발할 때 잊지 않고 기억나서 내가 성장한다는 느낌이 들었다. 막바지에는 개발을 시작하기 전에 항상 의자를 들고 해당 업무를 요청한 사람 옆으로 갔다. 말로 이야기면 Slack이나 Trello로 이야기하는 것보다 빠르고, 해당 문제를 직접 보면서 자세한 설명도 들을 수 있었다. 요청사항을 받아 개발하는 느낌이 아니고 함께 문제를 해결한다는 느낌으로 이야기하면서 실시간으로 여러 해결방안을 제시하면서 생각을 주고받았다. 문제를 해결하면서 회사에 장기적으로 도움이 되는 방향을 고민하다 보니 '주인의식을 가지고 일한다'는 느낌이 들었다. (정확히는 이왕 만드는 거 아름답게 만든다는 생각이었다.)- 이호성 이야기회사에서 병훈님의 별명은 '아기새'였다. 업무를 하면서도 사람들의 보살핌을 필요로 했지만 그것 외에도 이런저런 허술한 면을 많이 보여줘서 누가 붙였는지 기억이 나진 않지만 모두의 입에 착 붙어 있는 별명이었다. (개발팀 내에서는 간혹 '아. 이런. 손이 많이 가는 친구'로 불리기도 했다.)에잇퍼센트에는 퇴사하면 털린다. 다들 떠나지 마라.병훈님을 연태님 옆자리에 앉게 했다. 회사 내에서 스위퍼(스프린트 내의 개발 잡일들을 처리하는 담당) 팀도 연태님과 같이할 수 있도록 했다. 경험과 인내심이 많고 상냥한 언니 같은 연태님(남자)은 병훈님의 좋은 파트너가 되어 주었다. 그리고 세바님은 어려운 문제를 함께 해결해 주고 코드의 퀄리티에 대한 감시자(갈굼자)가 되어 주었다. 언젠가 병훈님이 개발자의 길을 가게 되어 첫 월급을 받게 되면 이 두 분에게는 빨간 내복을 사드려야 할 거다.처음에는 아기새의 Pull Request(반영하고자 하는 코드 뭉치)에는 코멘트가 수십 개가 달렸다. 그것들을 꾸역꾸역 고치고 나면 다시 그 절반 정도의 코멘트가 달리곤 했다. 하지만 병훈님이 떠날 때쯤에는 내 코드에 "이렇게 저렇게 고치는 게 더 좋은 것 같은데요?"라고 코멘트를 달곤 했으니 발전하지 못한 나는 부끄러울 따름이다.그리고 병훈님은 다른 팀일에도 참 관심이 많았다. 그러고 보면 나도 처음에 스타트업에서 일하기 시작했을 때 다른팀 일들도 왜 그렇게 재미있었는지 모르겠다. 하지만 분명한 것은 작은 조직에서는 다른 팀에 대한 관심이 개발을 잘하는 데 많은 도움이 된다.6. 떠나기 이주일 전- 소병훈 이야기정해졌던 퇴사일이 가까워지면서 새로운 업무는 들어오지 않았다. To-do list는 사라지고, 대신 '인수인계'라는 일이 생겨났다. 지금까지 했던 일들을 문서로 남기면서 새로운 책임자에게 넘겨주는 일이었다. 큰 그림을 그렸던 것들이 있는데 완성을 하지 못한다는 아쉬움이 컸다.호성님께 1,2월 프리랜서 제안서를 받게 된 건 우연이였다. 다 같이 점심을 먹을 때 우연히 호성님과 같은 테이블에 있었고, 1,2월에 남은 일정을 이야기하다 농담처럼 나온 제안이었다.제안서를 받은 날, 기숙사에서 많은 계산을 했다. 개발하는데 어느 정도 시간이 걸릴지, 제안서의 업무 기한을 변경한다면 일정이 어떻게 될지, 그렇게 받은 돈으로 어느 정도 도움이 될지. 충분히 가능한 일정이었다. 못해서 아쉬워하던 일이기도 했다. 그렇기에 더 고민했다.긍정적으로 고민하던 제안을 거절한 이유는 '여행 도중에도 계속 개발을 생각할까 걱정되어서'였다. 이번 여행에서 아쉬움이 남으면 다음은 언제가 될지 모른다는 생각이 들자, 내 시간이 더 귀하다는 생각이 들었다.그러면서 내가 조금이라도 더 필요하다고 말해주는 점이 고마웠다. 내가 생각해보지 못한 선택지를 받아서, 나의 가치관을 되짚어 본 느낌이었다.- 이호성 이야기병훈님과 같이 식사를 했다. 병훈님은 복학하기 전 유럽으로 여행을 다녀온다고 했다. 밤에 돌아다니면 위험하니까 숙소에서 코딩이나 하라고 살살 꼬셨다. 밤에 코딩하고 그 아르바이트비로 낮에 럭셔리하게 맛있는 것 먹고 다니면 얼마나 즐거운 여행이 되겠냐고. 제안서를 하나 작성해서 해야 할 일과 보수를 적어서 병훈님께 주었다. 왠지 넘어올 것만 같은 느낌이었다.병훈님이 하루 정도 생각해 보더니 "어정쩡한 상태가 될 것 같아요. 생각해보니 이런 제안을 주신 것만으로도 정말 감사합니다."라고 했다. 실패했다.회사 입장에서 업무를 잘 알고 있는 병훈님이 조금이라도 일을 더 해주면 하는 마음이었다. 하지만 다시 생각해보니 인생의 후배에게는 좋지 않은 권유였던 것 같다. 돈이 중요할 때가 아니라 세상에 대한 경험과 자신을 뒤돌아 볼 시간이 필요했던 거니깐.7. 떠나는 날케익이나 먹고 떠나랏!- 소병훈 이야기떠나는 날도 크게 다르지 않았다. 코드도 살펴보고 pull request도 적으면서 이전과 같은 하루를 보냈다. 그리고 마지막 날, 혹시 작별 인사를 하면서 내가 울지 않을까 걱정했지만 괜한 걱정이었다. 2달 전부터 작별 인사(라 쓰고 갈굼이라 읽는다)를 받아서 그런지 마지막 인사가 특별한 느낌이 들지 않았다.그렇지만 그 뒤로 며칠간 회사를 나왔다는 묘한 홀가분함과 그동안 했던 일들이 내 손을 떠난 공허함이 있었다. 내가 없으면 회사가 바뀌지 않을까 하는 조그만 기대도 있었지만, 다들 나 없이 잘 지내나 보다. 나는 조금 아쉬웠는데.- 이호성 이야기9개월이라는 시간이 참 금방 지났다. 남은 기간 동안 여행을 떠나는 병훈님에게 사람들이 "에이 그거 여행 가면 뭐해. 그냥 회사에서 일해"와 같은 장난을 수도 없이 했던 것 같다. 하지만 떠날 시간은 정해져 있었고 병훈님은 사람들의 박수를 받으며 떠났다. 마치 80분을 열심히 뛴 축구선수가 교체를 위해 떠날 때 받는 박수처럼.8. 떠나고 난 후- 이호성 이야기며칠 간은 아침 데일리 미팅이 왠지 허전하고, 슬랙으로 말을 걸면 대답을 할 것만 같았다. 하지만 또 새로운 사람이 회사에 들어오고 바쁘게 회사가 돌아가면서 금방 잊혀지긴 하더라. 아 그러고 보니 병훈님이 만든 코드에서 버그가 나올 때마다 우리는 회사에 남은 아기새 인형을 괴롭히긴 했다.병훈님이 떠나고 나서 같은 학교의 후배인 선희님이 회사에 마케팅 인턴으로 들어왔다. 선희님이 자기소개 시간에병훈 선배와 같은 동아리에..라고 말하자마자 전 직원이 다 뒤집어졌다. 그렇다. 우리에게 "병훈"과 "선배"는 함께할 수 없는 단어였다.여행을 갔다가 돌아온 아기새 병훈님이 와인을 하나 물어왔다. 그리고는 파닥파닥.군대 문제가 있기에 당분간 병훈님과 함께 오래 일할 수 있는 기회는 찾아오지 않을 것 같다. 아쉬운 마음이 든다. 에잇퍼센트에서의 병훈님을 "막 알에서 깨어나 호기심을 가지고 세상을 바라보는 아기새"로 기억해야겠다. 그리고 그 모습을 기억하며 나 또한 초심을 되새겨야지.우리가 다시 만날 그날까지 병훈님이 더 큰 날갯짓으로 더 넓은 세상을 여행하길 바란다. 9개월간 함께 해준 병훈님께 감사한다. 안녕!덧, 그나저나 난 또 어디에서 찾아야 하나. 이 다음 아기새를.#8퍼센트 #에잇퍼센트 #인턴 #조직문화 #후기 #팀워크 #팀플레이
조회수 1854

Android Studio JCenter 이용하기

 안녕하세요. 크몽개발팀 입니다.오늘 포스트 주제는 Android Studio JCenter 이용하기입니다.JCenter????  JCenter에 대해 처음 들으시는분들도 있을거같은데요.JCenter는 라이브러리들이 모여있는 저장소라고 보시면 되겠습니다.그렇다면 JCenter를 이용하여 무엇을 할까요?바로 외부 라이브러리들을 가져와서 프로젝트안에 Import 할려고 합니다. JCenter 사이트 링크 : https://bintray.com/bintray/jcenterJCenter 페이지에 접속하시면 위와 같은 페이지를 볼 수 있는데요.여기서 사용하고 싶은 라이브러리를 검색해보겠습니다.제가 검색한 라이브러리는 ImageLoder 라이브러인 'Glide'를 검색했습니다.빨간색으로 표시되있는게 제가 찾던 라이브러리 입니다.검색한 라이브러리를 클릭하면 위와 같이 상세페이지를 볼 수 있는데요.라이브러리 Github 주소 와 버젼에 대한 정보를 확인할 수 있습니다.그럼 이제 검색한 라이브러리를  Android Studio Gradle에 Import 하겠습니다. Android Studio에서 프로젝트를 생성하게 되면 위와 같이 3개의 그래들 파일을 볼 수 있는데요.자주 사용할 그래들 파일은 app폴더에 있는 그래들 파일입니다.그래들 파일을 열어보면 낯익은 코드들이 보이는데요.ADT에서는 매니페스트에서 버젼관리를 햇었는데 Studio에서는 그래들로 빠진거같습니다.그리고 빨간표시를 해둔곳이 바로 JCenter에서 검색한 라이브러리를 Import 하시면 되겠습니다.  위에 JCenter에서 찾은 라이브러리명을 입력하고 뒤에 버젼번호도 같이 입력합니다.그리고 Sync 버튼을 눌러주면 라이브러리가 Import 됩니다. External Libraries를 확인해보시면 라이브러리가 추가된걸 확인할 수 있습니다.---------------------------------------------------------------------------------------이렇게 JCenter를 이용하여 간단하게 라이브러리들을 Import를 할 수 있습니다.그리고 그래들 과 JCenter를 이용하여 라이브러리를 적용할때 가장 좋은점은'com.github.bumptech.glide:glide:3.+' 이런식으로 버젼에대한 값을 주면라이브러리 상위버젼이 나올경우 자동으로 최상위 버젼으로 라이브러리를 Import 해준다는 점입니다.이로써 라이브러리 버젼관리도 많이 편해질거 같습니다.이것으로 Android Studio JCenter 이용하기 포스트를 마치겠습니다.#크몽 #개발팀 #인턴 #인턴생활 #인사이트
조회수 2052

Backbone 적용기

Backbone이란?Backbone은 자바스크립트 프레임워크로 MVC 패턴을 적용하여 웹 애플리케이션 개발할 수 있도록 돕는 유용한 프레임워크입니다. MVC 패턴에 대해서는 밑에 더 자세히 설명하기로 하고 간단히 Backbone을 적용한 후의 장점을 소개하면 깔끔하게 뷰와 로직을 분리할 수 있어 코드를 유지 보수하는데 드는 시간이 줄며 기능 수정 혹은 기능 확장이 쉬워진다는 점등을 들 수 있습니다.또한, Backbone에서는 Underscore 라이브러리를 사용하는데, 이 라이브러리에서 제공하는 템플레이트 기능을 통해 뷰의 재사용과 설계를 쉽게 할 수 있다는 점도 장점입니다.만약 서버 측에서 RESTful한 URL을 제공한다면, Backbone을 사용하여 얻을 수 있는 이점이 더 확실해집니다. 모델에 RESTful한 URL을 제공하면, 간단하게 서버와 동기화하면서 그에 따르는 뷰의 변화 따위를 손쉽게 구현할 수 있습니다.RESTful한 인터페이스 설계에 대해서 궁금하시다면 이전에 올라온 글을 참조해보세요. Backbone 기반으로 설계된 여러 웹 애플리케이션 중에는 여러분이 잘 알고 있을만한 서비스들도 있을 것입니다.MVC 패턴?이미 MVC라는 용어에 익숙하신 분들도 많겠지만, 생소하신 분들을 위하여 간단히 정리해보면 MVC 패턴은 디자인 패턴 중의 하나로 모델(실제 쓰일 데이터)과 모델을 보여줄 뷰(인터페이스) 그리고 사용자로부터의 입력을 받아 모델과 뷰를 중재하는 컨트롤러로 나누어서 구현을 해나가는 방식을 말합니다. GoF 책에도 이 패턴이 소개되어 있지요.모델은 뷰나 컨트롤러와 무관하게 작성되는데 그런 모델을 뷰가 관찰하고 있다가 모델의 변화에 따라 적절히 뷰의 모습을 바꾸게 되므로 서로 투명하게 작동하게 됩니다. 즉 모델만 잘 설계해서 만들어주고 그에 따르는 뷰의 모습만 정의하면 그다음부터는 지저분하게 모델의 상태에 따르는 코드를 직접 처리할 필요가 없다는 장점이 있습니다.Backbone이 MVC 패턴을 적용하기 위한 프레임워크라고 하였지만, 실제로 Backbone에서는 MVC 패턴의 변형인 MVR 패턴을 사용합니다. 컨트롤러 대신 Router가 쓰이는 형식인데, 이 링크에서 Backbone의 Router에 대한 자세한 설명을 제공하고 있습니다. 하지만 Router가 컨트롤러의 역할을 대행하는 것은 아니고, 대부분의 Backbone 예제를 살펴보면 실제로 컨트롤러가 담당하는 업무들을 뷰에 이관하여 처리하는 것을 볼 수 있습니다. MV* 패턴 중에는 MVP 패턴이나 MVA 패턴 같은 MVC 패턴의 변형들이 존재합니다만 그 바탕을 이루는 Model-View의 관계는 변하지 않는 것을 볼 수 있습니다.Simple code snippet간단한 예제를 통해 실제 코드 상에서 어떤 식으로 Backbone을 적용하는지 알아보겠습니다.모델먼저 모델을 정의해야 합니다. 가령 밑의 코드에서는 사각형 모델을 정의하고 있는데요, 기본값을 지정해 줄 수 있고, 사각형 모델과 관련된 함수들을 정의해놓은 것을 볼 수 있습니다.var Shape = Backbone.Model.extend({ defaults: { x:50, y:50, width:150, height:150, color:'black' }, setTopLeft: function(x,y) { this.set({ x:x, y:y }); }, setDim: function(w,h) { this.set({ width:w, height:h }); }, });이렇게 Backbone.Model.extend 함수를 통해 모델의 청사진을 구성하게 됩니다. 이 모델을 이용하여 뷰를 구성할 수 있습니다.콜렉션Backbone.Collection.extend({ model: Shape });많은 상황에서 복수의 모델을 다루게 될 일이 생깁니다. 가령, 게시판에 올라온 글들은 게시물의 집합이라고 볼 수 있겠죠. 콜렉션을 통해서 이러한 복수의 모델의 집합을 만들어낼 수 있습니다. 위의 코드에서는 앞서 소개한 Shape 모델의 콜렉션을 정의한 것을 볼 수 있습니다. 모델과 마찬가지로 콜렉션도 뷰에 바인딩할 수 있고, 콜렉션에 관련한 이벤트(change, add, remove)를 뷰과 관찰하게 할 수 있습니다. 또한, Underscore 라이브러리에서는 콜렉션과 밀접하게 관련된 여러 함수를제공합니다.뷰var DocumentRow = Backbone.View.extend({ tagName: "li", className: "document-row", initialize: function() { this.model.bind('change:name', this.render); }, events: { "click .icon": "open", "click .button.edit": "openEditDialog", "click .button.delete": "destroy" }, render: function() { // render or update something } });기본적으로 뷰에 뷰와 관련된 모델이나 콜렉션을 바인딩하게 되는데요, 이 바인딩을 통해 뷰는 모델이나 콜렉션의 상태를 관찰하고 변화를 감지하여 바인딩 시 전달한 핸들러를 통해 적절한 행동을 수행할 수 있게 됩니다. 위의 예제를 보면 모델의 name 속성 변경 시 render 함수를 호출하도록 바인딩한 것을 알 수 있습니다. 또한, 뷰에 관련한 이벤트와 그에 관련된 핸들러를 events에 정의해놓을 수 있습니다. 보통 render 함수 내에서 뷰를 구성하거나 혹은 바인딩 된 모델, 콜렉션의 변화에 따르는 뷰의 변화를 적용하게 됩니다.뷰에 관련된 더 자세한 사항은 뷰 문서를 참조하시기 바랍니다.템플레이트var compiled = _.template("hello: <%= name %>"); compiled({name : 'moe'}); => "hello: moe"Underscore에서 제공하는 템플레이트 기능을 이용하여 문자열을 곧바로 html 요소로 만들어낼 수 있습니다. 또한, 템플레이트 내에 자바스크립트 함수 등을 삽입하는 기능도 제공합니다. 기본적으로 Underscore에서 템플레이트 기능을 제공하지만, 그 외에도 여러 라이브러리가 있습니다.가령 mustache를 이용해서도 똑같은 기능을 할 수 있습니다. 필요에 따라 유연하게 템플레이트 라이브러리를 바꿀 수 있다는 점이 매력이라고 볼 수 있습니다. Backbone 공식 사이트에서도 이러한 템플레이트 라이브러리를 이용하는 것을 권장하고 있습니다.Ember.jsBackbone이 나름의 역사가 있는 프레임워크이기 때문에 많이 쓰이고 있지만, 그 외에도 비슷한 기능을 제공하는 프레임워크가 많습니다. 그 중의 하나인 Ember.js가 있습니다. Ember.js의 장점이라면 기본적으로 Handlebars라는 템플레이트 라이브러리를 지원함과 동시에 Backbone보다 심화된 여러 기능을 제공하는 점이 있습니다.그러면서도 사용의 꼴이 Backbone과 비슷하므로 만약 Backbone을 사용해 본 적이 있다면 적응하기도 쉽습니다. 참고로 아래에 여러 MVC프레임워크를 소개하고 장/단점을 분석한 사이트의 링크를 달아두었는데 여타의 프레임워크보다 더 좋은 점수를 받기도 하였습니다.Backbone 말고 다른 MVC프레임워크를 원한다면, 특히 자체 템플레이트 라이브러리를 지원하는 프레임워크를 원한다면, Ember.js 사용을 고려해 보는 것이 어떨까요?더 읽어볼 만 한 것An Intro to Backbone.jsBackbone.js by exampleBackbone Tutorials위의 사이트들은 제가 Backbone을 공부하면서 참고한 사이트들입니다. 영문 사이트이지만 코드만 훑어 봐도 그 의도와 얼개는 파악할 수 있을 것으로 생각합니다. Backbone 공식 사이트에서 제공하는 튜토리얼 사이트도 방문해볼 가치가 있습니다. Backbone을 이용하여 개발한 간단한 서비스의 소스코드를 공개해 놓았습니다.The Top 10 Javascript MVC Frameworks ReviewedJourney Through The JavaScript MVC Jungle위 두 사이트에서는 앞서서 소개한 Backbone과 Ember.js 외의 여러 MV*패턴 프레임워크를 소개하고 장단점에 대하여 분석해놓았습니다.마치며이상으로 Backbone 도입과 그에 따르는 장점을 살펴보았습니다. 일반적인 홈페이지와 제작과는 약간 양상이 다른 웹플리케이션(웹 + 애플리케이션)개발자 분들은 프로젝트에 MVC 패턴 프레임워크를 적용해 보면 어떨까 하는 생각이 듭니다. 프로젝트의 생산성에 크게 이바지할 수 있으리라 생각됩니다.#스포카 #개발 #개발자 #인사이트 #Backbone #일지 #개발팀
조회수 1624

iOS Graphic Interface 살펴보기 (1/2)

1.intro: 애정하는 iOS, 애증의 Xcode프론트엔드 개발자가 가장 기쁠 땐 언제일까요? 여러 가지가 있죠. 직접 만든 스무스한 애니메이션을 볼 때, 고생해서 작업한 하드코어 고난도 레이아웃이 잘 작동할 때, 작업한 화면을 사람들이 ‘예쁘다ʼ고 말해줄 때 등등. 그러므로 iOS는 모든 프론트엔드 개발자가 동경하는 OS라고 말할 수 있습니다. 대부분의 굵직한 Transition들을 알아서 Animate해주고, 프레임레이트가 복잡한 레이아웃 효과도 부드럽게 표현해주기 때문에 ‘예쁘다ʼ, ‘쾌적하다ʼ는 말이 절로 나오는 OS이기 때문이죠. 물론 그만큼 손도 많이 갑니다. 사실 iOS는 신기한 점이 많습니다. Xcode를 사용하다 보면 Interface Builder에서 ctrl+드래그를 사용하여 Code로 Reference를 가져오는 방법부터 String값으로 찾아가는 Xib/StoryBoard 파일까지.. 다른 플랫폼 및 IDE에서는 겪어보지 못한 새로운 경험들을 만나죠. 덕분에 다년차 개발자의 멘탈도 Xcode-iOS를 만나면 탈탈 털립니다. 시간이 지나면 이 독특하고도 불편한 Xcode를 사랑하고, 저주하는 상황까지 생깁니다.그래서 오늘은 많은 iOS 루키들이 겁내고 괴로워하는 iOS의 Graphic Interface를 살펴보고자 합니다. 맨땅에 헤딩할 때 헬멧이라도 쓰고 있으면 그나마 덜 아프니까요.2.Point, PixelAndroid에서는 다양한 기종의 스크린을 지원하기 위해 자체적으로 dp라는 수치 개념을 만들어 사용합니다. 파편화된 디바이스들을 모두 지원하는 레이아웃을 구성하려고 고안한 효율적인 방법이죠. iOS에도 이와 같은 개념이 있습니다. 바로 포인트(Point)인데요. Xcode의 ImageAsset 파일을 열면 이런 것을 찾을 수 있습니다. 1X, 2X, 3X바로 이 화면에서 볼 수 있는 1x,2x,3x라는 문구가 포인트 개념을 설명하고 있습니다. 포인트는 디바이스의 물리적 픽셀을 2배, 3배로 압축해 사용하는 iOS 만의 독특한 단위입니다. 이 개념이 처음 쓰인 건 iPhone 4, 즉 레티나 디스플레이가 등장하면서부터 인데요, 기존의 iPhone 3Gs와 물리적 화면 크기는 동일한데, 4배의 픽셀 수를 가지는 레티나 디스플레이에 기존의 앱들을 그대로 보여주자니 픽셀 단위로 정의된 기존의 모든 이미지/레이아웃이 절반 크기로 줄어드는 문제가 발생했습니다. 따라서 별도의 작업 없이 디스플레이하기 위한 방법으로 고안된 게 바로 포인트입니다.포인트는 픽셀을 2배, 3배로 압축해 1포인트라는 단위로 규정하고, 그 단위를 Nib(Xib) 에디터 및 개발 과정에서 사용합니다. 앞으로 여러분이 iOS 개발을 하면서 접할 기본 단위는 바로 포인트가 될 겁니다. 2X 혹은 3X는 단어는 픽셀을 2배, 3배로 압축했다는 의미입니다. 개발자의 편의를 위해서 만들어진 개념이 오히려 개발자에게 혼동을 주는 아이러니한 상황이 펼쳐졌습니다. 사실 이 픽셀-포인트의 개념이 처음 등장했을 때는 꽤 편리했을 겁입니다. 당시만 해도 iPhone4와 iPhone3Gs의 해상도를 구분하지 않고 작업할 수 있는 획기적인 방법이었으니까요. 하지만 지금은 iPhone5, iPhone7 Plus, iPhone X 등 다양한 장비들이 등장했습니다. 그래서 iOS 개발자는 포인트를 단지, 픽셀의 또 다른 이름처럼 느낄 뿐입니다. 애플도 자신들이 이렇게 다양한 해상도의 iPhone을 출시하게 될 줄은 몰랐을 겁니다.애플의 해상도 춘추전국시대 / 출처: paintcodeapp3.Storyboard, Nib (Xib)iOS UI 디자인의 꽃이 무엇인지 묻는다면 그것은 단연 Storyboard와 Xib일 것입니다. Storyboard는 기획자들이 사용하는 그것과 유사한 개념입니다. 하나의 큰 틀에 화면 단위로 여러 장의 기획안을 놓고, 그것들의 시퀀스를 한 눈에 알아볼 수 있도록 하는 보드입니다.Storyboard는 Segue와 같은 시퀀스 설정을 직접 할 수 있고, 연결된 하나의 Flow를 시각적으로 펼치기 좋습니다. 프로토타이핑을 위한 적절한 툴인 셈이죠.UIStoryboard 예시 - 브랜디 iOS의 Main StoryboardNib(혹은 Xib, 이하 Xib로 지칭)는 조각조각 단위의 화면이나 재활용을 많이 하는 CollectionViewCell 등의 화면 작업에 적합합니다. 이 점이 Storyboard와는 다르죠. (CollectionViewCell에 대한 자세한 포스팅은 여기를 클릭하세요.)물론 Storyboard에서 할 수 있는 작업은 대부분 Xib로도 가능하지만, 각각의 용도를 다르게 해서 사용하는 경우가 많습니다. 예를 들어, 브랜디 iOS 프로젝트는 Storyboard에선 큰 틀의 화면을 다루고, Xib에서는 CollectionView Cell과 ReusableView, Custom Component등을 다루고 있습니다. UICollectionViewCell.xibStoryboard와 Xib로 인터페이스 작업을 할 때는 파일의 컨텐츠가 너무 비대해지지 않도록 조심해야 합니다. Storyboard가 비대해지면 많은 작업자가 동시에 파일을 수정할 수도 있는데, VCS를 사용하면서 Storyboard나 Xib 파일의 충돌이 발생하면 병합하는 과정이 매우 고통스럽습니다. 그러므로 Storyboard는 서로 충돌하지 않도록 더 큰 그림을 그리고, 해당 Storyboard를 Senior 개발자가 관리할 수 있도록 안전장치를 두도록 합시다. 야 이거 소스 건드린 사람 나와 Storyboard와 Xib는 기본적으로 XML 기반의 파일입니다. 혹시라도 충돌이 발생하면 UI로 확인이 불가능하기 때문에, Xcode에서 해당 Storyboard, Xib 파일을 우클릭한 후 Open As > Source Code 메뉴를 클릭하면 XML 형식으로 브라우징할 수 있습니다. 해당 충돌 부분을 찾아가서 수정하고 다시 확인하면 UI로 볼 수 있습니다.소스코드로 스토리보드 보기4.From Storyboard, to CodeStoryboard와 Xib에서 구현한 컴포넌트들을 ViewController의 SourceCode에서 다룰 일이 분명 생길 겁니다(언제나 그렇죠). 그럴 땐 Outlet이라는 개념을 이용해서 Storyboard 와 SourceCode를 연결하는데요.네, 코드가 아닙니다. 포토샵하는 기분으로 ctrl + 마우스 좌클릭 드래그를 해주시면 됩니 다. 이 기능은 다른 IDE에서 보기 힘든 건데요. 나름 쓸만합니다. 익숙해지면 여러 가지 컴포넌트, 유닛들을 Outlet으로 처리할 수 있습니다. 코딩을 자유롭게 할 수도 있고요. 예를 들어, LayoutConstraint를 Outlet으로 처리하면 해당 Constraint를 코드 시퀀스에 따라 자유자재로 변경할 수 있게 되는 것처럼 말이죠.물론 이보다 선행되어야 할 작업은 Storyboard에서 해당 ViewController가 연결될 ViewController를 지정하고, 해당 ViewController의 파일을 미리 만들어야 합니다.5.Extraction of ViewControllerStoryboard에서 ViewController A를 연결했는데, ViewController B 에서 ChildViewController로 ViewController A 를 사용하고 싶다면 어떻게 할 수 있을까요? (간장공장공장장) 당연한 이야기지만 코드를 통해 구현 가능합니다. 필요한 것은 Storyboard 파일명과, Storyboard에서 미리 지정한 ViewController A 의 Identifier, 두 가지입니다. Storybo/rd에서 ViewController A를 연결했는데, ViewController B 에서 ChildViewController로 ViewController A 를 사용하고 싶다면 어떻게 할 수 있을까요? 당연한 이야기지만 코드를 통해 구현 가능합니다. 필요한 것은 Storybo/rd 파일의 이름과, Storybo/rd에서 미리 지정한 ViewController A 의 Identifier, 두 가지입니다. instantiateViewController From Storyboard/**  현재 화면에 디스플레이중인 UIWindow 객체로부터 UITabBarController를 반환받는 메  소드  - parameter window: UIWindow  - returns: UITabBarController */ fileprivate func tabBarControllerFromStoryboard() -> BRTabBarController {  let storyBoard = UIStoryboard(name: "mainStoryboard", bundle: nil let viewController = storyBoard.instantiateViewController(withIdentifier: "mainTabBarController") return viewController as! BRTabBarController  // 잘못된 viewController를 추출한 경우 nil exception } 비슷한 방법으로 Xib에 작성된 View도 추출할 수 있습니다. Xib파일 하나에 여러 View가 정의되어 있다면, 각각의 View를 필요에 따라서 사용할 수도 있습니다.Extraction From Xiblet nib = UINib(nibName: NSStringFromClass(BRDropdownSelector.self) let components = nib.components(separatedBy: ".").last!, bundle: nil) let view = components.instantiate(withOwner: nil, options: nil).last as! BRDropdownSelector  // 잘못된 view를 추출한 경우 nil exception 6.LayoutConstraints For Flexible UI더 유연한 레이아웃 동작을 원한다면, Static하게 선언된 수치보다는 LayoutConstraint로 제한적 범위 안에서 유동적으로 동작할 수 있도록 View를 주물러 주는 게 좋습니다. 예를 들어, 어떤 두 컴포넌트 사이의 최대 너비를 100으로 지정하되, 컨텐츠 사이즈에 따라 더 작아질 수도 있도록 하려면, LayoutConstraints의 Less than or Equal기능을 사용하는 것처럼 말이죠.Less than or equalLess than or Equal뿐만 아니라 Greater than or Equal도 존재합니다. 상황에 맞게 사용하는 지혜가 필요하죠. LayoutConstraint에는 Multiplier라는 개념도 있습니다. 만약 컴포넌트 A 절반 너비의 컴포넌트 B를 작성하고 싶다면, 그리고 이 조건이 화면 크기와 관계없이 동일하게 적용되기를 원한다면, 컴포넌트 B의 너비를 컴포넌트 A와 동일하게 Constraint로 지정하고, Multiplier를 0.5로 지정하면 됩니다. Multiplier는 단어 그대로 ‘배수ʼ라는 의미입니다.이처럼 화면 해상도에 구애받지 않는 유연한 UI를 작성하고 싶다면 LayoutConstraint 의 사용은 필수입니다. 브랜디 iOS 앱이 다양한 해상도의 iOS 디바이스에서 동일한 비율 로 출력되는 것도 이러한 LayoutConstraint를 사용했기 때문이죠.7.View를 핸들링할 그곳앞서 정리한 방식들을 사용해서 Storyboard, Xib 파일을 훌륭하게 작성했다면, 이제는 ViewController의 소스코드로 돌아올 차례입니다. View Size를 이벤트에 따라 변경하거나, 숨겼던 View를 보여주는 등의 작업들을 할 차례입니다.Storyboard나 Xib에서 작업한 View를 코드 상에서 다룰 일은 많습니다. 99.78% 이상 ViewController에서 View를 다루어야만 하죠. 무조건입니다.viewDidLoad() 에서 View는 대부분의 초기화 작업을 합니다. 그것은 소스코드를 다루는 개발자에게도 마찬가지죠. Storyboard에서 연결한 Outlet들도 이 Function에서부터 nil값이 아니게 됩니다. 따라서 뷰에 필요한 초기화 작업 (Button의 Title 지정, ImageView의 이미지 지정 등) 을 viewDidLoad()에서 모두 하면 됩니다. viewDidLoad()는 그 이름처럼 ViewController가 생성되었을 때 단 한 번 호출됩니다. 다시 거치지 않는 코드이기 때문에 ViewController에서 사용할 변수들을 초기화하는 등의 작업도 이 자리에서 할 수 있습니다. viewDidLoadoverride func viewDidLoad() {      super.viewDidLoad()     /* do 초기화 in 여기 */ } 다만 여기서 아무리 해도 안 되는 작업이 있습니다. View 사이즈를 해상도에 맞게 변경하는 작업 같은 것 말이죠. LayoutConstraint를 통해 지정된 사이즈를 가져올 때, 화면을 꽉 채우도록 Constraint를 지정해도 로그를 찍으면 엉뚱하게 더 적은 값 이나 큰 값이 나올 수도 있습니다. 이런 경우에는 아무리 viewDidLoad()에서 열심히 Constraint의 값을 가져와도 결과가 똑같을 겁니다.개미지옥override func viewDidLoad() {      super.viewDidLoad()     // 백년동안 코딩해도 화면 해상도가 다르게 나와요 } viewWillAppear() 에서는 viewDidLoad()에서 작동하지 않던(?) 코드를 적용할 수 있는 자리입니다. Constraint들로 지정된 사이즈들은 viewWillAppear()에서부터 각 디바이스의 해상도에 맞게 적용됩니다. 여기서부터는 화면 크기에 맞춘 SubView들의 사이징이나 Constarint들로부터 추출한 값이 의미가 있습니다.viewWillAppearoverride func viewWillAppear() {     super.viewWillAppear()     // 이제 아마 화면이 나올 차례인가봐요 } viewDidAppear()는 출력된 화면에 실행할 코드를 작성하는 자리입니다. 화면이 등장한 이후 보여줄 팝업창이나, 튜토리얼을 출력하는 건 여기서 해야 합니다. viewWillAppear()는 예상되는 출력 화면에서 호출되기 때문에, 실제로는 화면이 없는 상황에서도 호출될 수 있습니다. 만약 해당 viewController의 출력이 확실히 완료된 후 에 실행되어야 하는 이벤트라면, 이 Function에서 코드를 작성해야 합니다. viewDidAppearoverride func viewDidAppear() {     super.viewDidAppear()     // 화면 출력이 끝났답니다. 마음껏 코딩하세요! } 네, 지금까지 루키들을 위한 GUI 만들기의 기본 과정은 다 알려드렸습니다. 많은 개념과 기능, 방법론이 존재하지만 일단 이 정도면 알아도 첫 번째 iOS 앱 UI를 만들 준비는 어느 정도 마친 겁니다. 그럼 마지막으로 UI를 구성하면서 유용하게 사용할 수 있는 팁을 알려드리겠습니다. 8.Little Tricks1) Clip it, or not Clip it.ImageView를 다루다 보면 자주 발생합니다. 지정된 ImageView의 사이즈보다 이미지가 크면 이미지가 ImageView의 영역을 빠져나가버리는 건데요. 이것은 Label이나 View에서도 동일합니다. 작성한 컨텐츠가 부모 View보다 큰 경우 부모 View의 프레임을 벗어납니다. 이런 경우, 재부팅하세요. clipsToBounds 값을 true로 지정해주면.. view.clipsToBounds = true 매-직! 이 작업은 코드뿐만 아니라 Storyboard상에서도 가능합니다. Xib에서도 동일합니다. Storyboard에서 클리핑2)Circular View요즘 많이 사용하는 동그라미 모양 프로필 이미지 때문에 고생하는 고심하는 개발자들이 많을 겁니다. iOS에서는 이 작업을 view의 Layer를 편집하는 방식으로 아주 간단하게 처리할 수 있습니다.self.layer.cornerRadius = self.frame.size/2.0 self.layer.masksToBounds = true self.clipsToBounds = true 위의 코드를 사용하면 아래와 같은 이미지를 출력할 수 있습니다.둥글게 클립핑된 최신 트렌드의 ImageView를 간단하게 출력했습니다. 물론 위에서 언급한 clipsToBounds 값을 true로 지정해주는 것도 잊지 마시고요. 이 코드를 응용하면 모서리가 둥근 직사각형 뷰도 만들 수 있습니다. 원하는 곡률을 적용할 수 있죠. view의 Layer를 다루는 방법을 공부한다면 다양한 상황에서 유용하게 사용할 수 있을 겁니다.3)NSAtrributedString 클라이언트가 다양한 형태의 Font, Color의 텍스트를 한 문장에 넣어달라고 한다면 어떻게 작업해야 할까요? 스타일마다 Label 묶음을 만들어서 각각의 단어를 지정해주는 방법이 있습니다. 하지만 텍스트 또는 문장 구성이나 스타일이 서로 다른 묶음으로 변경된다면 어떨까요? 또 다시 새로운 기준으로 Label 묶음을 만들어야 할까요? 이럴 때 사용하기 좋은 녀석이 바로 NSAttributedString입니다. 볼드체, 보통체가 혼합된 텍스트에 색상이 다른 텍스트가 혼재되어 있는 Attributed String이렇게 다양한 형태의 텍스트를 한 문장에 담을 수 있고, 변경되는 내용이 있더라도 코드로 간단하게 수정하면 됩니다. 브랜디 앱에서도 NSAttrributedString을 많이 사용하고 있습니다. 브랜디 iOS 앱의 간지나는 UI 속 요소요소를 차지하고 있는 중요한 녀석이죠. 4)Debug Wirelessly 각종 케이블이 난잡하게 널부러진 책상을 보면 한숨이 나옵니까? 걱정하지 마세요. 이제 하나는 줄일 수 있을 겁니다. Xcode로도 무선 디버깅을 할 수 있기 때문이죠. 먼저 디바이스를 맥에 연결하고, Xcode가 활성화된 상태에서 Window > Devices And Simulators 항목을 클릭합니다. Devices and Simulators그런 다음 출력된 화면에서 원하는 디바이스를 선택하고 Connect via Network를 체크 합니다. (디바이스에 암호가 설정되어 있어야 합니다.) 지구본 모양이 디바이스 오른쪽에 있다면 무선 디버깅이 가능한 상태입니다. 무선디버깅체크9.Outro: 긴 글을 마무리하며아장아장 걸음마 시절이던 첫 개발 프로젝트 작업이 생각납니다. 클라이언트는 끝도 없이 요구를 하는데 구현하는 방법을 몰라 막막했던 적이 많았습니다. 여러 실수를 겪고 나서야 많은 것을 알게 되었죠. 그때를 생각하면 이제 막 iOS 개발을 시작하는 분들께 하나라도 더 도와주고 싶답니다. 지금 막 iOS 개발자가 되었나요? 그렇다면 이 포스팅은 분명 당신의 검색 한 번, 실수 한 번을 줄여줄 수 있을 겁니다.글이정환 과장 | R&D 개발1팀[email protected]브랜디, 오직 예쁜 옷만#브랜디 #개발자 #개발팀 #인사이트 #경험공유 #iOS
조회수 1081

만땅에서 스푼까지 함께 달려온 찰스를 소개합니다

스푼을 만드는 사람들 여덟 번째 이야기마이쿤의 초창기 멤버 중 한 명인 'Charles' 를 인터뷰해보았다.그래서, 영어 유치원은 보내셨나요?https://brunch.co.kr/@mirr5510/17내가(Sunny) 처음 마이쿤에 입사하게 된 계기는 바로 Neil(대표)의 브런치 글과 마이쿤 관련 인터넷 기사를 읽고 나서였다. 많은 글 둘 중에 가장 궁금하고 특이하다고 생각했던 글이 바로 '영어 유치원' 보내자 였다.영어 유치원 보내자? 무슨 말이지? 하고 클릭해서 읽어보았다. 스푼 라디오라는 서비스 전 '만땅'이라는 배터리 공유 서비스를 시작할 때 첫 팀 빌딩에 관한 이야기였다. 닐의 주변 지인, 학교 후배들에게 함께 서비스를 만들자고 제안했을 때 유부남 팀원들에게 이렇게 말씀하셨다고 한다."우리, 아이들 영어 유치원 보내자"즉, 그만큼 잘하자. 우리 같이해서 성공하자라는 의미로 이렇게 말씀을 하신 것 같다. 그래서 찰스를 인터뷰할 때 가장 먼저 물어본 질문이었다. 그래서 아이들 영어 유치원은 보내셨는지 말이다.찰스 특징: 모자 좋아함"하하하.. 이미 저희 아이들은 많이 커서 유치원은 벌써 졸업했어요. 이제 테드랑 빅터의 차례가 아닐까 싶네요"찰스가 가장 좋아하는 맥주 'Charles' 당신이 궁금합니다.Q. 본인을 한 마디로 표현한다면?'동네형 또는 오빠' 저는 어색한 걸 싫어하고, 친화력이 좋은 편이기도 하고요. 사람들과의 편한 관계를 좋아해요. 그래서 먼저 보통 말을 먼저 잘 거는 편이에요"Q. 찰스도 혹시 딸 바보세요?"네, 저는 딸 바보예요. 아빠들은 딸 바보가 된다는 건 사실인가 봐요. 딸은 일단 아들과는 정말 달라요. 되게 예쁘고요.. 되게 애교가 많고요..(이때 눈이 반짝반짝하셨습니다) 아들은 보통 엄마를 찾던데, 딸은 항상 아빠를 찾더라고요. 아! 그리고 자다가도 아빠 들어오는 소리 들리면 나와서 뽀뽀해주고 다시 자러 가요. 6살인데 아빠한테 잔소리도 하고요. 마지막으로 하나만 더 자랑해도 돼요? 오늘 5일 만에 딸 얼굴을 봤는데 (안 자고 있을 때) 아빠가 엄~청 보고 싶었다면서 일주일치 뽀뽀를 엄청 많이 해줬답니다.. 이래서 다들 딸 바보가 되나 봐요."Q. 밀가루를 정말 좋아하신다고 들었습니다 feat. 코젤 다크"제 생각에 저는 탄수화물 중독자인 것 같습니다. 탄수화물을 정말 좋아해요. 특히나 칼국수를 정말 좋아하는데요. 맑은 거 말고 찐한 국물의 칼국수 있잖아요. 그거 너무 맛있어요. 제가 추천하고 싶은 칼국수집은, 논현동 영동시장에 있는 이름이 기억이 안 나는데.. 거기 진짜 칼국수 진짜 맛있습니다."P.S: 테드가 옆에서 조용히 슬랙으로 보내주셨습니다. 바로 이 칼국수 집이라네요. '손국시' https://m.blog.naver.com/PostView.nhn?blogId=rldudal0070&logNo=220165610372&proxyReferer=https://www.google.com/당신의 회사생활이 궁금합니다Q. 마이쿤의 초장기 멤버가 되신 계기를 더 알고 싶어요"저는 마이쿤에 입사하게 된 계기가 제 인생에서 가장 큰 결정이었어요. 저는 이 전 회사에서 9년 4개월 정도 근무를 했었어요. 그러던 어느 날 Neil이 회사에 놀러 오라고 하더라고요? 그래서 놀러 갔더니 보니까 이미 Yong 도 함께 일하고 있었고, 갑자기 닐이 사업 기획서를 보여주는 거예요. 이런저런 이야기 함께 나누다가 함께 일을 하자고 제안을 하더라고요. 정말 고민 많았어요. 마침 그때 제가 이직을 생각할 때였거든요. 그렇게 고민하고 와이프와 함께 의논을 했는데 고맙게도 와이프가 저를 믿어주고 응원해주었어요. 그리고 이런 생각을 했어요. "어차피 이직할 거라면 한 번 밑바닥에서 도전해보자!" 그리고 제 손으로 서비스를 함께 만들 수 있다는 것이 가장 큰 메리트이었고요. 드디어 내가 원하는 일을 하게 되었구나 생각했었죠. 무엇보다 서비스가 잘되면 우리 아이들에게 더 나은 미래와 경험을 줄 수 있다고 믿었고요. 무엇보다 잘 되지 않아도 살아가면서 나에게 정말 좋은 경험으로 남아 미래에 발판이 될 것이라고 생각했어요. 그렇게 저는 마이쿤의 초창기 멤버가 되었어요. 무엇보다 서비스에 대한 아이디어와 매력도가 높다고 느꼈고, 닐이 "영어 유치원 보내자!"라는 말에 혹했죠"Q. 첫 서비스를 실패했을 때 떠나지 않고 남았던 이유는?"제가 처음에 입사를 하자마자 와이프가 둘째를 임신한 걸 알게 되었어요. 근데 정말 너무 바빠서 집에도 못 들어가고 일을 했었어요. 가장으로서 남편으로서도 잘하고 싶었고, 일도 잘하고 싶었는데 마음처럼 쉽지가 않더라고요. 경제적으로 힘든 부분도 있었지만, 저는 이대로 이 팀이 헤어지기엔 너무나도 아쉬워서 남는 선택을 했어요. 저희 정말 열심히 했거든요. 진짜 정말 열심히 했는데 이대로 서비스가 잘 되지 않았다는 게 아쉬웠고, 이 일로 이 팀이 해체되는 게 너무 싫었어요. 그때 우리는 모두 정말 열심히 했지만 잘하진 못했었어요. 어떻게 가야 하는지 방향도 몰랐어요. 그래서 더욱 아쉬웠죠. 팀이 해체된다 할지 언정 후회 없이 헤어지고 싶었어요. 근데 저뿐만 아니라, 모든 사람들이 동의를 했어요. 우리 이번엔 열심히 하지 말고 '잘' 하자라고. 그리고 저는 외벌이에 유부남이라 팀원들이 저를 많이 배려해줬었죠. 와이프에게 가장 고마운 점이 그때 와이프가 그랬어요. "떠날 때 떠나더라도 후회 없이 해"이 말이 정말 큰 힘이 됐던 것 같아요. 와이프에게 많이 미안하고 고맙습니다."Q. 6년 동안 함께 해올 수 있었던 원동력은?"저는 정말로 솔직하게 여태 마이쿤에서 다른 곳으로 이직을 해야겠다는 생각을 해본 적이 없어요. 첫 번째 서비스가 망하고도 자발적으로 남은 이유도 이 팀과 함께 후회 없이 가고 싶다는 마음 때문이었어요. 저는 아직도 제가 성장하고 많이 배우고 있고, 배워야 한다고 생각하거든요. 어떠한 사람들과 일하느냐가 정말 중요한 것 같아요. 무엇보다 함께 시작하여 함께 실패하고 또다시 함께 일어났다는 점과 성장했다는 점이 기쁘고 뿌듯하고 더 큰 책임감을 느끼게 해 주거든요. 하지만 언젠가 제가 회사에 도움이 되지 않는 날이 온다면 그때는 스스로 떠날 생각입니다 (웃음)"Q. 리더로서의 삶은 어떤가요?"팀에 동료가 많아지게 되고 각각 다른 성격의 동료들이 생겨났어요. 각자 다들 일을 열심하 하고 잘하지만 팀으로서 하나가 되어 한 마음으로 커 나가는 건 다른 문제라고 생각해요. 각자의 개성을 살릴 수 있는 방법이 있을까? 내가 어떻게 하면 좋은 리더가 되어 후배들을 이끌어 줄 수 있을까? 하고 고민도 많이 해보고, 함께 이야기도 해보기도 하고요. 진심을 담아서 늘 말을 해요. 제 진심이 닿아야 팀원들도 저를 더 잘 따라 줄 테니까요. 면담을 통해서 불편한 것들을 해소해주려고도 노력하고 무엇보다 저도 그 들에게 많이 배우고 있어요. 아무리 신입이라도 해도 제가 생각하지 못한 부분을 배울 수 있거든요. 각자 살아온 환경과 경험이 다르고 본인이 잘하는 것들은 다 제 각각 다르니까요. 그래서 많이 노력하고 배우려는 리더가 되려고 노력 중입니다."Q. 새로운 서비스가 성장하면서 변한 게 있다면?Sunny 曰: "지난 만 5년 동안 마이쿤의 실패 그리고 재 도전 및 성장 과정을 모두 봐오셨잖아요. 뭐가 가장 많이 달라졌을까요? 정말 많은 것들이 변했겠지만요."Charles 曰: "저는 일단 스푼이라는 서비스가 성장하면서 좋아진 점이 정말 많아진 것 같아요. 그중 가장 큰 건 회사에 점점 더 전문적이고 실력 있는 분들이 입사하셨다는 겁니다. 물론 분위기는 예전하고 같을 수 없겠지요. 그때와 지금의 인원 차가 크니까요.분위기나 문화를 그때가 똑같이 유지를 한다는 건 불가능하다고 생각해요. 서비스의 규모에 맞게 함께 성장해 나가야 하니까요. 다양한 사람과 다양한 시선으로 보고 느껴야 회사 서비스가 더 성장할 수 있거든요. 그리고 서비스가 성장해야 우리 모두에게 좋은 일인 것이고요. 새로 입사하신 분들은 굉장히 능력자가 많으세요. 그분들에게 많이 배우려고 하고 있고 있고요"찰스가 좋아하는 진라면 + 파송송 + 계란탁 당신의 사생활이 궁금합니다Q. 좋은 아빠란 어떤 아빠라고 생각하세요?"내가 생각하는 좋은 아빠란?처음에는 애들과 재미있게 잘 놀아주면 그것만으로도 좋은 아빠라고 생각을 했었어요. 알고 보니 그것만으로는 부족하더라고요. 잘 놀아주는 것은 기본이고, 아이의 시선에서 세상을 바라봐 주고, 아이와 눈높이를 맞춰 주는 자세를 가져야 하고 제일 중요한 것은 아이의 마음을 공감해줘야 한다고 생각합니다. 이렇게 해야 정말 좋은 아빠가 아닐까?라고 생각합니다. 잘 놀아주는 것은 누구보다 잘한다고 생각합니다만 아직까지도 아이의 마음을 공감해주는 부분에서는  많이 부족합니다. 저도 모르게 아이를 혼내고, 반성하고 이 패턴이 반복이에요. 한때는 뱃속에 있을 때는 건강하기만을 원했는데, 막상 세상에 나와보니 어쩔 수 없나 봅니다."Q. 자녀분들이 개발자를 꿈꾼다면 추천하시나요?“개발자는 굉장히 매력적인 직업입니다.누구나 개발자가 될 수 있고, 즐길 수 있는 지금의 문화라면 저는 아이들에게 개발자가 되는 것을 추천해주고 싶어요. 성취감이라는 걸 얻을 수 있는 직업이기에, 그런 걸 느껴보게 해주고 싶기도 하고요.나의 재능으로 나를 포함한 누군가의 삶이 달라질 수 있는 경험은 좋은 경험이라고 생각해요. 무엇보다 개발자는 스스로 만들고 싶어 하는 욕구가 있는데요. 내가 개발한 서비스나 상품을 누군가가 사용하고 좋은 피드백을 준다면 정말 보람찬 일이거든요. 물론 부정적인 피드백을 받으면 상실감을 느낄 수도 있지만, 그 과정에서 또 성장해나갈 수 있기 때문에!”Q. 나중에 개발해 보고 싶은 서비스가 있나요?"있어요. 라면 서비스요. 저는 라면을 정말 좋아하거든요, 라면 중에서도 진라면을 가장 좋아하는데 진라면에 파 넣고 마늘 넣고 콩나물 넣고! 끓여먹으면 전 일주일 내내 먹을 수 있습니다. 그래서 '우리 함께라면'이라는 라면 서비스를 해보고 싶어요. 라면에 특화된 서비스죠. 제가 그래서 예전에 회사에서 사람들 한 명 한 명한테 혹시 라면 일주일 내내 먹을 수 있냐고 물어보기도 했었어요."Q. 만약 개발을 하지 않았더라면 지금 뭐하시고 계셨을 거 같으세요?"글쎄요. 저는 원래 사실 개발자가 되려던 마음은 없었어요. 군대 제대하고 우연히 한 번 해볼까? 했는데 시작하게 되어 업이 되었네요. 저는 아마 개발자가 아니라면 지금 공무원? 하지 않았을까 싶어요. 원래 저의 옛날 성향은 뭔가 모나지 않고, 평범한 사람이었거든요. 이렇게 큰 도전을 하기 전까지는요"당신이 마이쿤에서 우리와 함께 일해야 하는 이유는요저희는 정말 많은 실패와 역경을 거쳐왔고, 쓰러질 때마다 '함께' 일어났습니다. 이제서야말로 정말 본격적으로 성장하기 위해 달려야 하는 시점이에요.  해야 할 것도 정말 많고요. 회사와 서비스가 성장하는 경험을 할 수 있다는 것은 어디서 돈 주고도 못할 경험이라고 생각합니다. 지금 이 기회에 저희와 같은 배를 타신다면, 개개인이 노력한 만큼 서비스 성장에 기여하실 수 있고 본인 스스로도 성장하는데 많은 도움이 될 것이라고 생각해요. 이미 성장한 곳에서 경험을 하는 것도 분명 가치 있는 일이지만, 나 스스로가 성장에 기여할 수 있는 회사의 구성원이 될 수 있는 건 흔한 일이 아니라고 생각합니다.서비스 플랫폼 팀원들이 Charles를 한마디로 표현한다면?Kyu 曰:  '동네형' - 사실은 동네 아저씨에 더 가깝지만 마치 동네 형인 듯 다가와주는 사람.Sam 曰:  '장군님' - 어디서 자꾸 전리품(티셔츠, 스티커 등 )을 가지고 오신다.P.S 저희 어머님께서 NewRelic 티셔츠 편하다고 너무 좋아하십니다.Mark 曰:  '언니' - 가끔 삐지시는 거 같지만 언제나 잘 챙겨준다.

기업문화 엿볼 때, 더팀스

로그인

/