스토리 홈

인터뷰

피드

뉴스

조회수 3078

채널 iOS에 Redux를 적용하게 된 7가지 이유.

친숙한 MVC 패턴개발자라면 누구에게나 친숙한 MVC (모델 - 뷰 - 컨트롤러) 패턴은 꽤 오랜 시간 동안 사용됐고 아직까지 많은 개발자들에게 사랑받고 있는 패턴이다. 그 이유로는 이 패턴이 일단 진입장벽이 낮기도 하지만 코드 재사용성, 동시 개발의 용이성 때문이다. 만약 당신이 초보 iOS 개발자라면 높은 확률로 MVC 패턴을 쓰게될 것인데 그 이유는대부분의 예제 및 튜토리얼이 MVC 패턴을 쓰고 있고iOS의 IDE인 Xcode에서 (Swift 는 예외지만) 클래스를 생성할때 기본으로 이름에 ViewController라고 들어간다.위와 같은 이유로 많은 iOS 개발자에 영향을 주리라 생각된다. (2011년도부터 iOS 세계에 빠진 저자도 사실 iOS에서는 software architectural design pattern으로는 MVC가 넘사벽이라고 생가하고 있었기에) 문제는 상대적으로 복잡도가 높아지거나 코드의 양이 많은 제품의 개발에서는 생산성이나 가독성에 그다지 도움을 주지 못하는 데 있다고 생각한다. 예를 들어, 한 페이지의 복잡도가 높아지면 ViewController 파일 한 개의 코드 라인이 기하급수적으로 증가한다. 또 (코드 관리에 매우 신경을 쓰지 않는 이상) 객체 간의 통신 및 데이터의 통일성이 없어져서 가독성이 떨어지기 쉽고, 기능을 추가할 때 생산성이 점점 떨어지게 된다.왜 MVC 패턴은 이렇게 문제가 생기는걸까라는 질문에서부터 시작해보자.MVC 패턴, 도대체 뭐가 문제인가?!그림 1. 보편적인 MVC 패턴의 구조보편적으로, MVC 패턴의 구조는 위의 그림과 같다. 그림을 간단히 설명하자면:뷰에서 이벤트가 발생하면 컨트롤러에 알린다컨트롤러는 그것을 처리하고 모델에 업데이트를 하라고 전달한다.모델은 업데이트를 하고 컨트롤러에 다시 알린다컨트롤러는 모델이 업데이트되었다는 것을 뷰에 알린다뷰는 모델의 업데이트된 값에 따라 다시 뷰를 그린다그림 1과 위의 설명만 놓고 보면 각각의 역할이 명확하다고 생각한다. 구조가 복잡하지 않기 때문에 초보자들도 쉽게 이해하고 적용 가능하다는 것이 장점이다. 하지만 MVC 패턴은 객체 간에 어떤 방향으로 커뮤니케이션 해야 하는지에 대해서는 강제하지 않기 때문에 파생된 패턴들이 많이 있다. 실제로 구글에서 “MVC pattern”이라고 검색을 하면 위 그림과 다른 MVC 패턴 이미지들을 볼 수 있다. 그 한 가지 예가 밑에 그림 2이다.그림 2. 또 다른 MVC 패턴의 구조그림 2를 보면 그림 1과는 다른 커뮤니케이션 방향을 나타내고 있다. 바꿔 말하면 개발자가 원하면 언제든지 세 가지 구조 안에서 방향을 유동적으로 바꿔 써도 무방하다는 것이 된다 (그것이 원하는 MVC 패턴이든 아니든지 간에). MVC의 변형으로써는 여러 가지가 있지만, 대표적인 것들은 아래의 그림과 같이 MVP, MVVM 같은 것들이 있다.그림 3. MVC, MVP, MVVM 패턴의 비교실제 저자도 MVC 패턴이 커뮤니케이션 방향을 강제하지 않는 것과 관련해 문제를 겪은 경험이 여러 번 있었던 것을 기억한다. 한가지 예를 들어보자.ViewA.swift (뷰)protocol ViewADelegate {       func updateA() }   class ViewA : UIView {        var delegate: ViewADelegate?       //update through protocol      func didClickOnA() {          self.delegate?.updateA()     }      //update through notification     //maybe same kind of update can happen in other views      func didClickOnAA() {         NotificationCenter.default.post(             name: NSNotification.Name(rawValue: “updateFromA”),              object: nil         )     }      func render(_ model: product) {         //update based on model      }  } ViewController.swift (컨트롤러)class ViewController : UIViewController, ViewADelegate {       Var viewA: ViewA?     Var product = Product()     func viewDidLoad() {         self.viewA = ViewA()         self.viewA.delegate = self         // ...         self.view.addSubview(self.viewA)     }      func updateA() {         self.product.update(name: “aa”, version: “123”)         self.viewA.update(self.product)         //re-render viewA     }  } Product.swift (모델)class Product {       var name = “”     var version = “”     init() {         NotificationCenter.default.addObserver(             self,             selector: #selector(self.doSomething),             name: “updateFromA”, object: nil)     }      deinit {         NotificationCenter.default.removeObserver(self)     }      func update(name: String, version: String) {         self.name = name         self.version = version     }      func doSomething() {          //do something…          //notify viewA or any objects through notification     }  } 조금 극단적인 예처럼 보이긴 하지만 실제 개발을 하다 보면 충분히 일어날 수 있는 상황이다. 코드에 대해 간략하게 설명하자면:ViewA에서는 delegate와 notification으로 각각 ViewController와 Product에 이벤트를 날리고 있고ViewController에서는 delegate method를 구현해서 Product를 업데이트 후, 다시 ViewA를 그리라는 로직을 가지고 있다.Product 에서는 객체를 업데이트 할 수 있는 메소드가 있고 notification을 통한 업데이트를 하고 있다.이건 아주 간단한 예이지만 프로젝트가 커진다면 특정 이벤트에 대해 데이터가 업데이트되는 경로가 달라질 수 있다. ViewA -> Product -> SubProduct -> Product -> ViewA 의 경로라던가, ViewA -> Controller -> Product -> SubProduct -> Controller -> ViewA 의 경로 등이 가능하다. 이처럼 특정 이벤트에 대해 여러 가지 체인형식으로 업데이트가 이루어질 경우 그 경로를 일일이 추적하는데 시간이 걸릴 수밖에 없는 구조를 가지고 있는 것을 볼 수 있다.(프로젝트의 크기가 어느정도 커지게 된다면 이렇게 될지도 ㅎㅎ)이런 케이스가 발생하는 근본적인 이유는 결국 MVC 패턴의 장점이라고도 말할 수 있는 유연성과 양방향 커뮤니케이션 때문이다. 이 패턴 자체가 문제가 있는 것은 아니지만 결국 코드는 사람이 작성하는 것이기에 생산성과 가독성을 떨어뜨리는 결과를 초래할 가능성이 높다. 여기에서 우리는 기존 웹 개발에서 쓰이고 있던 Redux 도입을 생각하게 된 것이다.Redux는 무엇인가?Redux 로고Redux는 Facebook의 Flux 를 모태로 삼고 있고 예측 가능한 상태를 자바스크립트 프로그램에서 구현하기 위한 애플리케이션 아키텍쳐이다. Redux는 본래 자바스크립트에서 시작한 오픈소스 프로젝트이지만 다른 개발자들에게 영감을 주었고 2015년 말쯤 iOS 플랫폼에서는 ReSwift(Redux + Swift)가 생겨났다. ReSwift는 결국 Redux랑 크게 다르지 않고 Redux의 세 가지 법칙을 따른다.Single source of truth — 애플리케이션의 전체 상태(State, 또는 데이터)는 트리 형태의 하나의 저장소(Store)로 저장된다.Changes with pure functions — 상태 트리를 변경하는 리듀서(Reducer)는 순수 함수(pure function)이어야한다.Read-only states — 상태는 오직 액션(Action, 어떤 일이 일어날 것인지 설명할 수 있는 객체)으로만 변화가 가능하다.쉽게 말하자면 “Redux는 한 개의 상태 저장소를 가지고 있고 그 안에 있는 데이터만이 신뢰할 수 있으며 저장소의 상태는 오직 순수 함수인 리듀서를 통해서만 변화가 가능하다” 라고 축약 할 수 있다.그림 4 Redux 패턴의 구조위의 그림 4을 보면 충분히 프로그램의 흐름이 어떤 식으로 흐르는지 감이 왔으리라 생각한다.이벤트가 뷰에서 생성되면 그에 해당이 되는 액션을 통해 알린다.액션은 특정 리듀서에서 처리한다.리듀서는 액션에 따라 저장소를 업데이트한다.저장소에 변화가 오면 구독(Subscribe)을 하고 있는 모든 객체에 알린다.이것이 Redux의 커뮤니케이션 사이클이다. Redux만으로도 충분히 여러가지 블로그 주제가 나올 정도로 할 이야기가 많지만 여기까지만 하고, 좀 더 자세한 디테일을 알기 원한다면 옆의 링크를 클릭하시면 된다. :) -> 리덕스 공식 링크Redux vs. MVCMVC와 Redux에 대해 소개를 했으니 간단히 비교해 보자.The Flow — Redux는 데이터 및 애플리케이션의 흐름을 강제한다. 저장소의 변화는 오직 액션을 통해서만 가능하기 때문이다. 이와 다르게 MVC는 강제성이 없기 때문에 여러가지 파생 패턴이 생길 수 있다.Unidirectional flow — Redux에서 흐름은 액션으로만 변화가 일어나기 때문에 오직 한쪽으로만 흐른다. MVC에서는 양방향이 될 수도 있고 한 방향이 될 수도 있지만 보통 양방향이다.Stores — Redux에서는 상태 및 데이터가 하나의 저장소에 있기 때문에 관리하기가 쉬운 반면, MVC에서는 여러 군데에 상태가 분리되어 있기 때문에 동기화에 신경을 써야 한다. (로컬 데이터 스토리지를 쓴다면 문제가 해결되기는 하지만 패턴 이외에 추가적인 노력이 필요함)그 이외에 여러가지 다른 점이 있겠지만, 위의 3가지가 가장 다른 점이라고 저자는 생각한다.채널 데스크 iOS에 Redux를 적용하게 된 이유이제 MVC와 Redux의 차이점을 알게 되셨으리라 생각한다. 우리 팀이 채널 데스크 iOS에 Redux를 적용한 이유를 소개하려고 한다. 아직 모든 부분에 완벽히 적용한 상태는 아니지만 (부분적으로 Notification, Delegate 그리고 Reactive를 쓰고 있다) 그럼에도 Redux를 적용함으로써 얻는 이점이 많다고 느끼고 있다.Explicit data flow — 새로운 개발자가 왔을 때나 여러 명이 작업을 할때 애플리케이션의 전체 흐름을 파악하고 이해하기 쉽다.Unidirectional flow — 데이터 관련 부분을 전부 Redux로 대체하니 모든 데이터 흐름이 한 방향으로 강제되었다. 덕분에 데이터가 어디에서 왔고 어디로 가는지를 파악하기 매우 쉽다.Single storage — 한 곳에서만 데이터를 관리하기 때문에 데이터에 관한 부분은 리듀서만 잘 짜 놓으면 관리하기 쉬워진 점이 있다. Redux를 적용하기 전에 CoreData를 데이터 저장소로 쓰고 있었는데, 어느 시점에 어떻게 저장되는지 눈에 들어오지 않아 불편한 점을 Redux를 사용함으로써 해결할 수 있었다.Immutability and data consistency — 변경 가능한(Mutable) 객체는 보통 iOS 개발에서나 다른 플랫폼 개발에서 장점일수도 있다. 하지만 데이터의 일관성이 깨지기 쉽다. 만약 A에서의 데이터와 B에서의 데이터가 다르면 어떤 것을 신뢰해야 하는지의 문제도 생길 수 있다. 우리는 Redux의 저장소에 있는 데이터를 모두 변경 불가능한 객체(Immutable, Swift에서는 Struct을 쓴다)로 구현하여 이 문제를 해결하였다. 이 부분은 코딩할 때 불편한 점이 조금 있지만, 그 불편함을 감수할만한 가치가 있다고 생각한다.Predictability — 저장소는 오직 액션을 통해서만 변경할 수 있다는 점이 무엇보다 장점인 것 같다. MVC와 같이 데이터를 어디서든 변경할 수 있다면 데이터와 관련된 버그를 찾는 데 소비하는 시간이 길어지곤 한다. Redux는 어떤 액션이 어디에서 불리는지 아는 것만으로도 그 시간을 비약적으로 단축할 수 있다.Maintainability — 저장소, 상태, 액션 그리고 리듀서로 역할과 레이어를 분리하게 되니 보통 코드 라인이 100줄을 넘지 않는다. 그만큼 유지보수 비용이 적어졌다.Organized Code — MVC 패턴에서는 비지니스 로직이 뷰에 들어가는 경우가 있기도 했었는데 Redux의 가이드라인을 따름으로써 자연스럽게 대부분의 뷰는 그저 데이터를 받고 시각화하는 dummy 뷰의 형태가 되었다. 비즈니스 로직이 완전히 뷰와 분리됨으로써 뷰의 복잡도와 코드를 관리하기가 쉬웠다.ReSwift 도입 시 주의할 점ReSwift 도입을 고려하는 분들을 위해 몇 가지 주의할 점을 소개하겠다.Performance — ReSwift에서는 저장소가 변경될 때마다 newState: 메소드가 호출이 되어 화면을 업데이트할 수 있게 되어있다. 채널 데스크 같은 경우는 실시간 애플리케이션(Real-time application)이라서 API 이벤트와 Socket 이벤트가 자주 발생해서 저장소가 변경되는데, 도입 초기 단계에 이 부분을 간과해서 화면이 거의 멈출 정도로 퍼포먼스가 나오지 않았었다. 만약 ReSwift를 적용했는데 퍼포먼스가 나오지 않는다면 newState: 함수 부분을 최적화하거나 미들웨어(middleware)를 만들어서 batch 형식으로 액션을 처리하는 방식을 고려해봐야 한다.Not thread safe — ReSwift는 thread-safe 하지 않아서 초반에 알 수 없는 crash들이 자주 발생했었다. 저자 같은 경우는 ActionWrapper를 만들어서 액션은 항상 메인스레드에서 처리되도록 강제했다.글을 마무리하며..Redux는 이미 자바스크립트 개발에서는 React와 함께 많이 쓰이고 있지만 iOS에서는 아직도 생소한 아키텍쳐이다. ReSwift는 아직 2년도 되지 않은 프로젝트이고 자바스크립트에서 처럼 유용한 Redux 미들웨어도 많지 않다. 또한 인지도도 MVC, MVVM, MVP에 아직 미비한 편이다. 프로덕션에 참고할 만한 예제도 찾기 어려웠기에 초기 러닝 커브는 조금 있었던 것으로 회상한다. 그럼에도 불구하고 우리 팀은 ReSwift를 적용해 보다 깨끗하고 유지보수하기 쉬운 프로그램을 만들 수 있었고 만족하며 사용하고 있다. 기존 MVC의 불편함을 아시는 분들은 충분히 도입할 가치가 있다고 생각한다.#조이코퍼레이션 #개발자 #개발팀 #인사이트 #경험공유 #일지 #Redux
조회수 2456

AWS 서비스를 활용한 Kubernetes 클러스터 구축 - VCNC Engineering Blog

Kubernetes 클러스터를 상용 환경에서 운영하기 위해서는 몇 가지 추가 구성요소를 설치해야 합니다. 예를 들어 Ingress를 만들더라도 실제로 트래픽을 받아줄 Ingress Controller를 설치해두지 않았으면 소용이 없습니다. 그리고 모니터링을 위해 컨테이너의 로그나 CPU/메모리 사용량 등을 수집, 조회할 수 있는 서비스도 필요합니다.다행히 이러한 추가 구성요소 또한 Kubernetes 클러스터 위에서 일반 애플리케이션과 거의 같은 방식으로 작동하므로 설치하는 것이 어렵지는 않습니다. 다만 클러스터를 원하는 대로 구성할 수 있는 만큼 선택의 폭이 넓어서 여러 가지 해법을 놓고 고민하게 될 수 있습니다. 이 글에서는 타다 서비스를 위해 Kubernetes 클러스터를 구성할 때 어떤 선택을 했는지, 특히 AWS 환경에서는 어떤 서비스들을 활용할 수 있는지 공유합니다.서비스를 외부에 노출: NGINX Ingress Controller + NLBIngress Controller 고르기Kubernetes에서 클러스터 내부 서비스를 외부에 HTTP(S)로 노출할 때는 Ingress를 사용할 수 있습니다. TLS 암호화, 로드밸런싱, 호스트명/경로 기반 라우팅 등을 제공해서 상당히 편리한데, Ingress가 실제로 작동하기 위해서는 Ingress Controller가 필요합니다.시중에는 다양한 종류의 Ingress Controller 솔루션이 나와 있습니다. 그중 Kubernetes 프로젝트에서 공식 지원하는 NGINX Ingress Controller와 AWS ALB 로드밸런서를 이용하는 AWS ALB Ingress Controller를 두고 고민을 했습니다.타다에서는 클라이언트(모바일 앱)에 실시간 이벤트를 전달하기 위해 gRPC를 사용하고 있어서 gRPC를 지원하지 않는 ALB는 선택할 수 없었습니다. 그리고 AWS ALB Ingress Controller는 현재 Ingress 하나마다 ALB를 1개 생성하는 구조여서 앞으로 노출할 서비스 수가 늘어난다면 비용 효율이 떨어진다고 판단했습니다. 따라서 NGINX Ingress Controller를 선택하게 되었습니다.NGINX Ingress Controller는 NGINX 웹서버를 기반으로 하므로 gRPC 모듈을 비롯하여 다양한 NGINX 모듈을 통해 굉장히 세세한 부분까지 설정할 수 있습니다. NGINX Ingress Controller는 Ingress나 Ingress가 가리키는 서비스의 엔드포인트에 변화가 생길 때마다 동적으로 NGINX 설정을 업데이트하는 방식으로 동작합니다.NGINX Ingress Controller 로드밸런싱NGINX Ingress Controller를 사용해도 외부에서 오는 트래픽을 적절히 분배해 줄 외부 로드밸런서는 필요합니다. AWS의 로드밸런서는 Classic ELB, ALB, NLB가 있습니다. 앞서 설명했듯이 ALB는 gRPC를 지원하지 않아서 Classic ELB를 TCP 모드로 사용하거나 NLB를 사용해야 합니다. Classic ELB는 동시에 많은 연결을 처리하려면 웜 업이 필요한 단점이 있어 NLB를 사용하기로 하였습니다.최근 NLB가 TLS termination을 지원하기 시작했지만, HTTP/2와 gRPC를 사용하기 위해 필요한 ALPN 정보를 설정할 수 없어서 NGINX 수준에서 TLS 암호화를 처리하고 있습니다. NLB 수준에서 TLS 처리를 하면 무료로 자동 갱신되는 ACM 인증서를 사용할 수 있는 등 여러 가지 이점이 있어서 아쉽습니다.Kubernetes에서 LoadBalancer 타입의 서비스를 생성하면 알아서 AWS 로드밸런서를 만들어줍니다. 하지만 이렇게 해서 NLB를 생성하는 방식은 아직 알파 기능입니다. 따라서 먼저 NodePort 타입의 서비스를 생성하여 모든 노드의 특정 포트에 NGINX를 노출한 다음, 별도로 생성한 NLB에 노드들이 속한 오토스케일링 그룹을 연결해주는 방식으로 직접 설정하게 되었습니다.정리해보면 외부에서 오는 트래픽을 처리할 때는 다음과 같은 과정을 거칩니다.모든 서브도메인(*.tadatada.com)은 NLB를 가리킵니다.NLB의 443 포트로 암호화된 HTTP 또는 gRPC 요청이 들어옵니다. NLB는 적절한 Kubernetes 노드 중 하나의 특정 포트(예: 30000번)로 요청을 전달합니다.Kubernetes 노드에서는 포트 번호를 보고 NGINX 서비스로 향하는 요청임을 알 수 있고 NGINX 컨테이너 중 하나로 요청을 전달합니다.NGINX는 복호화를 한 다음 HTTP Host 헤더를 확인하여 요청을 전달할 Ingress를 알아냅니다. 그리고 해당 Ingress의 엔드포인트 중 하나로 복호화한 요청을 프록시합니다.애플리케이션 컨테이너가 요청을 처리합니다.트래픽 흐름: NLB → NodePort → NGINX Ingress Controller → 내부 서비스Pod에 IAM 역할 부여: kube2iamS3, SQS 등 IAM으로 인증하는 AWS 서비스에 접근하려면 인증 정보가 필요합니다. EC2에서는 액세스 키를 직접 넣는 대신 EC2 인스턴스 프로파일로 인스턴스에 IAM 역할을 부여할 수 있습니다. 하지만 하나의 Kubernetes 노드 (=EC2 인스턴스)에는 여러 Pod이 실행될 수 있기 때문에 Pod마다 다른 IAM 역할을 부여하기를 원한다면 인스턴스 프로파일을 활용할 수 없게 됩니다. (인스턴스 프로파일에는 하나의 IAM 역할만 부여 가능)kube2iam을 사용하면 다음과 같이 Pod 어노테이션으로 IAM 역할을 지정할 수 있습니다.apiVersion: v1 kind: Pod metadata: name: aws-cli labels: name: aws-cli annotations: iam.amazonaws.com/role: role-arn spec: ... 설치나 사용법은 문서를 참고하면 어렵지 않은데, 원리를 간단히 설명해 보겠습니다. EC2 인스턴스 안에서는 특정 IP 주소(169.254.169.254)로 접속하면 EC2 메타데이터 API에 접근할 수 있습니다. AWS SDK는 EC2 메타데이터 API를 통해서 인스턴스 프로파일에 붙은 IAM 역할과 IAM 역할에 해당되는 액세스 키 쌍을 받아오게 됩니다.kube2iam은 모든 노드에 실행되면서 Pod 내부에서 EC2 메타데이터 서버 주소로 나가는 모든 요청을 가로챕니다. 그리고 인스턴스 프로파일 정보와 액세스 키 발급 요청을 kube2iam 서버가 대신 처리합니다. 따라서 Pod 안에서는 인스턴스 프로파일이 부여된 EC2 인스턴스 내부에 있는 것처럼 느껴지게 됩니다.추후 AWS SDK에 EKS 지원이 추가되면 별도로 데몬을 설치하지 않고도 Pod에 IAM 역할을 줄 수 있게 될 것으로 보입니다.로그 수집: fluentd + CloudWatch LogsKubernetes의 컨테이너가 stdout/stderr로 출력하는 로그는 노드에만 쌓이고 컨테이너를 재시작하거나 삭제하면 함께 삭제됩니다. 또한 노드의 디스크가 꽉 차는 것을 방지하기 위해 일정 크기를 넘으면 오래된 로그는 없어집니다. 그러므로 로그가 사라지지 않도록 계속 어딘가에 모아두어야 합니다.AWS에서 활용할 수 있는 로그 저장 서비스에는 CloudWatch Logs가 있습니다. fluentd를 DaemonSet으로 노드마다 하나씩 실행해서 컨테이너 로그를 CloudWatch Logs로 전송할 수 있습니다.CloudWatch Logs에 저장한 로그는 최근 나온 CloudWatch Logs Insights로 검색, 분석할 수 있습니다. 아직 나온 지 얼마 되지 않아서 기능이 많지는 않지만, 간단히 조회하는 용도로는 충분합니다.CloudWatch Logs Insights 사용 예모니터링: PrometheusEC2 인스턴스 하나에 서비스 하나를 띄워서 사용할 때는 CloudWatch로 CPU 사용률 등의 지표를 측정할 수 있었습니다. 하지만 Kubernetes를 사용하면 여러 서비스가 하나의 인스턴스에서 동시에 실행될 것이므로 인스턴스 수준의 지표는 무의미합니다. 특히 최소 실행 단위인 컨테이너 수준의 CPU 사용률 같은 값을 측정해야 하는데, CloudWatch를 사용하기에는 과금 체계가 적합하지 않습니다.기본 제공되는 5분 간격의 EC2 지표는 무료지만 CloudWatch에 커스텀 지표를 올리게 되면 지표 당 비용을 지불해야 합니다. 이 때 '지표'는 지표 이름 + 고유한 차원(dimension)의 조합입니다. 예를 들어 CPUUtilization이라는 이름의 지표가 PodName=server-aaaaaaaa과 PodName=server-bbbbbbbb라는 다른 차원으로 올라온다면 각각을 다른 지표로 취급합니다. 따라서 지표 수가 너무 많아지지 않게 조정해야 하는데 그러면 상세하게 모니터링하기가 어렵습니다.비용 문제도 있고, Kubernetes의 여러 가지 정보를 CloudWatch로 내보내는 기존 도구가 없었기 때문에 다른 방법을 찾아보게 되었습니다. 그래서 Kubernetes 모니터링을 위해 많이 사용하는 Prometheus를 선택했습니다. Prometheus를 온전히 사용하기 위해서는 다양한 컴포넌트들이 필요한데, Prometheus Operator Helm 차트를 사용하면 비교적 쉽게 구축할 수 있습니다.Prometheus는 Kubernetes 클러스터 모니터링 외에 애플리케이션 모니터링에도 사용할 수 있습니다. 타다의 애플리케이션들은 Spring Boot로 작성되어 있는데 Spring Boot Actuator와 Micrometer의 Prometheus 지원을 사용해서 애플리케이션 수준의 지표도 Prometheus로 모니터링하고 있습니다. 특히 Prometheus Operator를 사용하면 모니터링 대상을 추가할 때 Prometheus 설정 파일을 수정하지 않아도 Kubernetes에 ServiceMonitor 리소스를 등록하기만 하면 되어서 편리합니다.Prometheus로 수집된 지표는 Grafana 대시보드로 시각화하고, 정해진 조건에서 Alertmanager를 통해 PagerDuty와 Slack에 알림을 보냅니다.Grafana 대시보드의 모습자동 처리량 확장: Cluster AutoscalerKubernetes에서 자동 처리량 확장은 크게 두 종류로 나눌 수 있습니다. 먼저 Horizontal Pod Autoscaler로 CPU, 메모리 사용량에 따라 Pod의 수를 자동으로 조정할 수 있습니다. HPA가 실제로 동작하기 위해서는 오토스케일링을 위한 지표를 제공하는 Metrics Server를 설치해야 합니다. 그런데 부하가 증가해서 HPA가 Pod 수를 늘리려고 할 때 워커 노드에 여유가 충분하지 않으면 새로운 Pod을 실행할 수 없어서 소용이 없습니다. 이 때 워커 노드의 수를 자동으로 조정해주는 것이 Cluster Autoscaler입니다. Cluster Autoscaler는 노드 수를 증가시키기만 하는 것이 아니라 여유가 생겼을 때 노드 수를 자동으로 줄여서 불필요한 비용이 발생하지 않도록 해줍니다.AWS 환경에서 Cluster Autoscaler는 EC2 API를 통해 EC2 오토스케일링 그룹의 Desired Capacity 값을 필요한 노드 수로 조정하는 방식으로 작동합니다. 따라서 Cluster Autoscaler에는 EC2 API를 호출할 수 있는 IAM 권한을 주어야 합니다. 이를 위해 위에서 소개한 kube2iam을 사용할 수 있습니다. 그리고 Cluster Autoscaler가 오토스케일링 그룹을 자동으로 발견할 수 있도록 미리 정해진 태그를 붙여야 합니다.한 가지 주의할 점은 노드의 오토스케일링 그룹이 여러 가용 영역(AZ)에 걸쳐있으면 안 된다는 것입니다. 오토스케일링 그룹이 여러 AZ에 속한 경우 AZ 간 인스턴스 수의 균형을 맞추려고 하는데 이 과정에서 인스턴스가 예기치 않게 종료될 수 있습니다. 이 때 해당 노드에 실행되어 있던 Pod이 안전하게 종료되지 않을 수 있기 때문에 AZ마다 오토스케일링 그룹을 따로 만들고 AZ 간 균형은 Cluster Autoscaler가 맞추도록 설정해야 합니다.도움이 되는 링크들위에서 소개한 컴포넌트들은 다음과 같은 Helm 차트를 통해 설치해서 사용하고 있습니다.stable/nginx-ingressstable/kube2iamincubator/fluentd-cloudwatchstable/prometheus-operatorstable/metrics-serverstable/cluster-autoscalerEKS Workshop: AWS 환경에서 Kubernetes 운영할 때 참고할 만한 정보가 많이 있습니다.Kubernetes Slack 채널: #eks 채널에는 AWS 직원들도 접속해 있어서 높은 확률로 답변을 받을 수 있습니다.
조회수 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]브랜디, 오직 예쁜 옷만
조회수 2642

Next.js 튜토리얼 3편: 공유 컴포넌트

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기2편: 페이지 이동 3편: 공유 컴포넌트 - 현재 글4편: 동적 페이지5편: 라우트 마스킹6편: 서버 사이드7편: 데이터 가져오기8편: 컴포넌트 스타일링9편: 배포하기개요Next.js는 전부 페이지에 관한 것입니다. React 컴포넌트를 export하고 그 컴포넌트를 pages 디렉터리 안에 넣어 페이지를 생성할 수 있습니다. 그러면 파일 이름을 기반으로 고정된 URL를 가지게 됩니다.export 된 페이지들은 JavaScript 모듈이므로 다른 JavaScript 컴포넌트를 이 페이지들 안에 import 할 수 있습니다.이는 어떤 JavaScript 프레임워크에서든 가능합니다.이번 편에서는 Header 컴포넌트를 만들고 여러 페이지들에서 사용해 볼 예정입니다. 마지막에는 하나의 Layout 컴포넌트를 구현하고 어떻게 이것이 여러 페이지들의 모양을 정의하는데 도움이 되는지 살펴볼 것입니다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.Header 컴포넌트 구현하기Header 컴포넌트를 구현해봅시다.다음과 같은 components/Header.js를 추가해주세요.이 컴포넌트는 애플리케이션에서 이용가능한 페이지에 대한 두 개의 링크가 있습니다. 또한 보기 쉽도록 링크를 스타일링 하였습니다.Header 컴포넌트 사용하기다음으로 페이지들 안에 Header 컴포넌트를 import하고 사용해봅시다. index.js 페이지를 다음과 같이 변경해주세요:about.js 페이지도 똑같이 변경할 수 있습니다.지금 http://localhost:3000로 이동하면 새로운 Header가 보이고 페이지 이동이 가능합니다.이 애플리케이션에서 간단한 몇 가지를 수정해봅시다!- 애플리케이션을 종료하세요.- conponents 디렉토리의 이름을 comps로 바꾸세요.- ../components/Header 대신에 ../comps/Header로부터 Header를 import 하세요.- 애플리케이션을 다시 실행시키세요.동작하나요?- 네- 아뇨. "컴포넌트를 찾을 수 없습니다"라는 에러가 발생합니다.- 아뇨. "컴포넌트는 components 디렉토리 안에 있어야합니다"라는 에러가 발생합니다.- 아뇨. "comps는 잘못된 디렉터리입니다"라는 에러가 발생합니다.컴포넌트 디렉토리예상대로 잘 동작합니다.꼭 특정한 디렉토리에 컴포넌트를 둘 필요는 없습니다. 원하는 대로 이름을 설정할 수 있습니다. 특정한 디렉토리는 pages 디렉토리뿐입니다.pages 디렉토리 안에 컴포넌트를 생성할 수 있습니다.Header 컴포넌트는 이를 가르키는 URL이 필요하지 않기 때문에 pages 디렉토리 안에 두지 않았습니다.레이아웃 컴포넌트애플리케이션 안의 다양한 페이지에서 공통의 스타일을 사용할 예정입니다. 이를 위해 공통 레이아웃 컴포넌트를 만들고 각 페이지에서 사용할 수 있습니다. 여기 예시가 있습니다:components/MyLayout.js에 다음의 내용을 추가해주세요:위와 같은 코드를 작성하면 다음같이 원하는 페이지에서 레이아웃을 사용할 수 있습니다:http://localhost:3000 페이지로 이동하여 확인할 수 있습니다.이제 레이아웃에서 {props.children}을 지워보고 무슨일이 일어나는지 살펴봅시다.무슨 일이 일어날까요?- 아무 일도 일어나지 않을 것이다- 표시되는 페이지의 내용이 사라질 것이다- "레이아웃은 내용이 필요합니다"라는 에러가 발생할 것이다- 브라우저의 컴포넌트에 대한 경고 메시지가 표시될 것이다하위 컴포넌트 렌더링하기{props.children}을 삭제하면 Layout은 아래와 같이 Layout 엘리먼트 하위에 둔 내용들을 랜더링하지 못합니다:이것은 레이아웃 컴포넌트를 생성하는 방법 중 하나입니다. 다음은 레이아웃 컴포넌트를 생성하는 다른 방법들입니다: 컴포넌트들 사용하기공유 컴포넌트를 사용하는 두 가지 경우를 다뤘습니다.1. 공통 Header 컴포넌트2. 레이아웃스타일을 지정하고 페이지 레이아웃 및 기타 원하는 모든 작업에 컴포넌트들을 사용할 수 있습니다. 더불어 NPM 모듈에서 컴포넌트를 import 하고 사용할 수도 있습니다.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 1813

잔디의 새싹 같은 안드로이드 개발자 Gary를 만나다.

맛있는 인터뷰 : 안드로이드 개발자(Android Developer) Gary 편집자 주잔디와 함께 하는 멤버는 총 35명. 국적, 학력, 경험이 모두 다른 이들이 어떤 스토리를 갖고 잔디에 합류했는지, 무슨 일을 하고 있는지 궁금해하는 분들이 많습니다. 잔디 블로그에서는 이 궁금증을 해결해 드리고자 ‘맛있는 인터뷰’를 통해 ‘잔디’ 멤버들의 이야기를 다루고 있습니다.인터뷰에서 가장 기초적인 질문. 예상하신 자기소개 부탁한다.G : 잔디 개발자 중 제일 어린! 핵심 키워드다. 강조 부탁한다. 가장 어린! 안드로이드 플랫폼 개발을 맡은 Gary다(이하:G). 잔디 입사한 지는 9개월 되었다. 특별한 일 없이 정말 열~! 심히 개발만 했다. 일주일 전부터 식당 선택을 강요(?)했다. 이 식당을 선택한 이유는 무엇인가?G : 음식이 푸짐하게 나오기도 하지만 조명이 좋다. 인터뷰 중 사진을 찍는다고 들었는데 그 사진이 예쁘게 나올 것 같아서 선정했다. 게다가 음식이 정갈하다. 안에 룸도 있어서 회식하기도 좋다. 내 입이 복잡하질 않아서 뭘 먹어도 다 맛있다. 어차피 다 맛있는 거 사진이라도 잘 나와야 한다 싶어서 선택하게 되었다. 잔디에 들어오게 된 계기는?G : 모집 공지가 뜨길래.. 농담이다^^; 전 직장과의 시스템이 다른, 자체 서비스를 하는 회사에 가보고 싶었다. 전 직장에서는 기획, 서버, 디자인, 개발을 각각 다른 회사에서 진행했다. 내가 속한 회사는 개발만 진행했다. 처음부터 끝까지 한 곳에서 서비스하는 회사를 가보고 싶었다. 또한, 기술 스택이 너무 좋았다. 이를 통해 앞으로 나의 실력이 일취월장할 수 있겠다는 생각이 들었다. 9개월 동안 많이 발전한 것 같은가?G : 상당히 많이 발전했다. 어떤 부분에서 발전한 것 같은가?G : 이전 회사에서는 일정에 쫓기다 보니 개발 자체에서 설계라는 게 없었다. 손 가는 데로, 일단 만들고 보자라는 식으로 신기술이고 뭐고 공장처럼 찍어내는 것이 일이었다. 하지만 잔디는 다르다. 2주 단위로 생각을 통해 설계도 해보고 ‘객체지향 설계 5대 원칙’ 등 다양한 사항을 고려해보며 많은 고민 끝에 개발하는 것이 너무 좋다. 이를 통해 개발자로서 더욱 성장한 것이 느껴진다. 이전 회사보다는 일찍 퇴근하는 편인가?G : 이전 회사는 철야, 야근 거의 매일 했다. 입사한 지 9개월 정도 되었는데 이제야 사람답게 사는 것 같다. 그동안은 짐승인 줄 알았다. 일어나면 출근하고 퇴근하면 자고. 9개월 동안 사람답게 살면서 잠도 푹 잤다. 몸이 편해지면 사람 얼굴이 확 산다고 하던데 생긴 건 나아지지 않더라. 옛말도 틀린 말이 있나 보다. (하하) 주말엔 뭐하시는가?G : 집돌이 성향이라 집에 있는 경우가 많다. 밖에 나가면 이웃 주민들과 소통하기도 한다. 지금 사는 곳은 취업한 지 얼마 안 된 사회초년생들에게 국가에서 주는 ‘행복주택’이라는 곳이다. 그곳엔 대부분 사회초년생이라 연령대가 20대 후반에서 30대 초반 정도 된다. 그분들과 소통을 하며 지내고 있다. 나이가 비슷하니 공감대 형성도 되고 행복주택에서 행사 같은 걸 많이 해서 문화생활 영위하는 듯하다. 말 그대로 사람 사는 듯한 느낌이다. 서울 집값이 너무 비싸다. 행복주택은 저렴한 편인가?G : 다른 원룸에 비교하면 저렴한 편이다. 보증금이 들어가지만, 월세는 10만 원 전후다. 서울에서 월세 10만 원 전후라면 저렴한 편 아닌가? 사회 초년생분들께 추천한다. 이웃들도 좋고 문화생활도 하고. 엄청나지 않은가? 신청 후 당첨이 되어야 하지만 당첨되기 쉬운 편이다. 꼭 한번 도전해봐라. 처음 들어왔을 때 잔디는 어땠는가?G : 사실 처음 들어왔을 때 사무실이 시끌벅적하며 소통이 아주 활발할 줄 알았다. 그런데 딱 들어오니 어?! 음? 오?!… 조용하다. 진짜 활발한 곳은 따로 있었다. 목소리가 아닌 손가락으로 얘기하는 곳. 잔디 앱이었다. 사무실 자체는 너무 조용한데 잔디 앱 안에서 매우 활발하다. 아이러니하지만 의사소통은 아주 활발했다. 잔디의 생활 중 가장 마음에 드는 문화는?G : 영어 이름을 사용하며 상호 간에 존중하는 문화다. 30년 동안 한글 이름으로만 살았는데. 회사만 오면 Gary라고 부른다. 첨엔 이게 날 부르는 건지도 잘 몰랐지만 익숙해지니 너무 좋다. 수평적인 관계의 시작이 무엇인지 알게 되었다. 이전 회사와는 다르게 잔디에서는 수평적인 관계여서 의견 개진이 편했다. 의견을 나눌 수 있다는 것이 너무 좋았다. 물론 손끝으로 얘기하지만:D 감정표현도 이모티콘으로 한다. 표정은 무표정이지만 손가락은 웃고 있달까? 내 얼굴은 무표정인데 프랑키 (파랑몬스터 캐릭터)가 웃어준다. 이젠 오프라인으로도 소통이 되었으면 한다. 사무실 밖에선 말이 많으신 분들인데. 사무실만 들어오면 조용하시다. 손으로 말하고 계시니까. (웃음) 회식은 자주 하는가?G : 팀마다 다른 것 같다. 팀 내에서 석 달 치 회식비를 모아서 한 번에 하던가 쪼개서 자주자주 하던가. 어차피 쓰는 돈은 같으니까. 이루고 싶은 꿈이 있다면?G : 백수가 되고 싶다. (비장) 충분한 불로소득이 있는. 소득과 상관없이 내 맘대로 살 수 있는 그런 백수가 되고 싶다. 회사를 가고 싶으면 회사를 가고 사업을 하고 싶으면 사업을 하는 그런 백수(하하). 사실 프로그래머로서 직업을 정했을 때는 최고의 프로그램을 만들어보는 게 꿈이었다. 또한, 안드로이드로 시작했으니 구글에서 종지부를 찍자! 이런 꿈을 꾸었다. 그런데 이전 직장이 너무 힘들었나 보다. 꿈이 변했다. 백수로. 하루는 7개월 정도 만에 칼퇴근하고 집에 갔더니 어머니께서. “너 잘렸냐?”라고 물어보시더라. 그땐 내 회사 생활에 문제가 있구나 싶었다. 백수는 아니지만 일과 삶의 밸런스가 맞는 삶을 살고 싶다. 다음 인터뷰어에게 하고 싶은 질문이 있나?G : 회사 내에 다른 팀원들과 지금 먹고 있는 음식을 같이 먹을 수 있다면 누구랑 같이 먹고 싶은가?#토스랩 #잔디 #JANDI #팀원소개 #인터뷰 #기업문화 #조직문화 #사내문화 #팀원자랑
조회수 1727

타운어스 신규플랫폼을 오픈하며

안녕하세요. 타운컴퍼니 R&D 파트의 백대현입니다.타운컴퍼니는 단체들를 위한 공동구매 플랫폼인 타운어스를 운영하는 스타트업입니다. 기술블로그를 시작하며 최근 오픈한 타운어스 2.0에 대한 소개, 타운컴퍼니 기술조직인 R&D 파트에 대한 소개를 드리려 합니다.타운어스는 대학생들이 학교 내에서 자주 진행하는 공동구매가 굉장히 복잡하고 만족하기 어렵다는 문제를 해결하기 위해 시작되었습니다. 수작업으로 진행하던 공동구매를 플랫폼화 시켜 편하게 공동구매를 진행할 수 있게 만들고, 전국단위로 공동구매를 진행하여 최저가로 구매할 수 있도록 하였습니다.2014년 9월 오픈베타서비스인 캠퍼스샵을 시작하였고 이어서 2015년 6월 정식서비스 타운어스를 런칭하여 2년 넘게 폭발적으로 성장해왔습니다.사실 그 동안 타운어스 사이트에 대한 아쉬움이 많았습니다. 사이트가 초기에 린(Lean)하게 만들어진 이후에 몇차례 업데이트를 거쳐 타운어스 비즈니스를 받쳐주고 있었지만 내부 개발팀의 부재, 인력부족으로 유지보수가 잘 되지 않고 있었습니다. 그 와중에 몇 차례의 비즈니스 전략 변경(Pivot)을 거치면서 기술과 비즈니스의 괴리가 점점 벌어지게 되었습니다.타운어스 신규플랫폼타운컴퍼니에서는 여러 고민을 거쳐 한층 업그레이드 된, 이제 대학생 뿐만 아니라 모든 단체를 위한 공동구매 커머스 플랫폼으로 거듭나고자 타운어스 2.0 프로젝트를 시작하며 기획팀, 디자인팀, 개발팀으로 이루어진 R&D 파트를 출범하고 기술조직을 구성하였습니다.새로 출범한 타운컴퍼니 R&D 파트에서는 물려받은 기존 코드베이스를 유지하며 플랫폼을 개선할 것인지, 부채를 청산하고 새로운 코드베이스를 쌓을 것인지 많은 고민을 했습니다. 결론적으로 새로운 코드베이스로 프로젝트를 진행하기로 하였는데 대표적인 이유는 아래와 같았습니다.타운어스 2.0에서 변경되어야할 기획이 상당히 많아 재설계가 필요함비즈니스적 전략 변경을 서비스에 반영하기 위해 변경해야할 기획이 상당히 많고 그 동안 기술에 반영되지 않은 비즈니스 요구사항이 많아 여려 요구사항이 복합적으로 고려되어야함서비스 아키텍처가 연속성이 없음초기에 MVP로 시작된 코드베이스가 연속적인 기술조직이 발전시키지 못하고 외주 개발과 인력 변경을 거치다 보니 일관적이지 않고 누수가 많음버그 빚더미인력부족으로 인해 그 동안 유지보수가 되지 않아 해결해야할 버그가 복합적으로 존재함 (해결되지 않은 리포팅된 이슈 450여개 OMG)위 외의 여러 이유로 타운컴퍼니 R&D 파트에서는 기술부채 파산을 선택하고, 새로운 코드베이스에서 프로젝트를 시작하기로 하였습니다.신규플랫폼 간략 소개결론적으로 타운어스 신규플랫폼은 전체적인 일정과 인력을 고려하여 우선 부분적으로 오픈하게 되었습니다. 가장 많은 사용자들이 사용하는 단체의류공동구매에 대한 서비스를 먼저 개발하게 되었고 그 외 카테고리의 공동구매는 기존플랫폼에서 서비스하고 있습니다.이렇게 시작한 타운어스 신규플랫폼의 현재 오픈한 변경 사항은 크게 아래와 같습니다.전체적인 UX/UI 업데이트공동구매를 더 편하게 시작하고 진행하고 마무리할 수 있도록 개선“공동구매방”을 친숙하게 사용할 수 있도록 유도하였습니다.단체의류 커스터마이징 기능 강화커스터마이징 의류 종류 확장 : 과잠, 코치자켓, 티셔츠 (점진적으로 확대할 예정)커스터마이징 예상가격 계산 기능편하게 단체의류 디자인을 미리 해볼 수 있는 서비스를 제공합니다.최근 오픈을 시작으로 타운컴퍼니 R&D 파트에서는 지속적으로 서비스를 발전시켜 세상 모든 단체활동을 위한 공동구매, 타운어스에 걸맞는 공동구매 커머스 플랫폼으로 만들어갈 예정입니다.기술 스택과 조직 문화타운컴퍼니 R&D 파트의 기술에 대해서는 꾸준히 소개를 드리려 합니다. 때문에 오늘은 조직 문화와 기술 스택에 대해서 간단하게 소개를 드리겠습니다.프로젝트 진행협업툴로 JIRA, Confluence, Slack을 사용하고 있습니다.프로젝트는 Agile Kanban 방식으로 테스트 주도 개발, 코드 리뷰, 페어프로그래밍을 통해 진행하고 있습니다.서비스에 대한 충분한 고민 이후에 개발을 진행하려 노력합니다.기술 스택Back-End는 Django 1.11 (DRF) 기반으로 개발하며, AWS, MySQL, Vagrant, Docker 같은 기술을 사용하고 있습니다.Front-End는 Angular 5.1을 사용해서 개발하고 있으며 Less, RxJS, Webpack 등의 기술을 사용하고 있습니다.UX/UI에 Sketch와 Zeplin을 주로 사용하고 있습니다.CI(Continuous Integration)로 Travis-CI, 배포 관리로 Fabric과 AWS CLI, 버그 리포팅으로 Sentry.io, 플랫폼 모니터링으로 ELK(ElasticSearch, Logstash, Kibana)를 사용하고 있습니다.지향하는 조직 문화 및 지원자유롭고 주도적인 조직 환경시간에 쫓겨 비루한 코드를 생산하지 않도록유연한 리모트 근무지시가 아니라 스스로 할 일을 찾는 자율 속에서 책임을 다하는 문화자유롭게 의견을 제시할 수 있는 편한 분위기건강한 비판과 토론을 장려하는 커뮤니케이션 문화이상을 추구하되 비즈니스에 기반한이상적인 기획, 디자인, 개발을 지향하지만 비즈니스 영역에 기반한 의사결정회사 모든 조직들이 더 부가가치가 높은 업무에 집중할 수 있는 환경 조성을 위해 노력TOY TIME매주 금요일 4시부터 7시까지 3시간 자유로운 주제로 프로젝트 진행세미나, 컨퍼런스 참가 적극 장려 및 도서 지원저희는 아직 성장하고 있는 만큼 개선할 점도 많고 배워야하는 부분도 많이 있습니다. 그 만큼 기술블로그를 통해 저희가 고민하고 겪었던 기술을 공유하고 소통하며 서로 성장할 수 있는 기회가 되었으면 합니다.잘 부탁드립니다.#타운컴퍼니 #서비스소개 #기업문화 #사내복지 #조직문화 #원격근무 #디지털노마드 #재택근무
조회수 1787

핀다(Finda)의 '따끈따끈한' 신입개발자 남은우:

핀다(Finda) 개발자 남은우님의 스타트업 생생LIFE 입니다원문은 링크를 통해 확인하실 수 있습니다!안녕하세요! 금융상품 추천서비스 '핀다'에서 프론트 엔드 웹 개발자로 근무하고 있는 남은우라고 합니다~ ^^저는 입사한지 6개월차가 되는 따끈따끈한 신입 개발자입니다. 올해 처음 웹 개발을 배우기 시작해서 인턴으로 들어오기까지 많은 것을 경험했는데요~ 제 이야기를 통해서 스타트업에서 일하기를 희망하시는 분들에게 조금이나마 도움이 되었으면 좋겠습니다. :)<핀다 개발자 남은우, 출처 : 핀다>스타트업에 지원하게 된 이유대학교 4학년 마지막 학기, 저는 아직 졸업하고 싶지 않은 철 없던 마음에... 휴학 할 명분(?)을 만들기 위해서 여기 저기 대외 활동을 찾고 있었어요. 그러던 중 우연히 지원한 소프트웨어 개발자 양성 과정에 운 좋게도 덜컥!! 합격해 버렸습니다. 6개월간 진행된 팀 프로젝트를 위해 배운 웹 개발에 흥미가 생겨서 본격적으로 개발 공부를 시작했는데요. 시간이 지날수록 개발 능력은 조금씩 늘어갔지만, 불안감도 나날이 커져갔습니다. 그 이유는 바로 '실무 경험'이 없었기 때문이었죠.제가 배운 개발 능력을 발휘할 수 있는 곳을 찾던 중에 스타트업 인턴즈를 만나게 되었습니다. 스턴에서 진행한 4주간의 코칭은 사회 초년생인 저에게 어찌보면 '치트키' 같은 시간이었어요. 자신에게 맞는 스타트업을 찾기 위해 3가지 핵심가치를 설정하거나, 면접 필수 요소, 기업분석 방법까지!!! 코치님의 여러가지 조언과 꿀팁들 덕분에 저에게 꼭 맞는 회사를 선택할 수 있었던 것 같아요.스타트업에서의 경험입사 첫째 날, 인턴임에도 불구하고 서비스 개발에 바로 투입(?) 되었습니다. 처음 제가 맡은 업무는 코드 리팩토링이었는데요. 이미 작성되었던 코드를 새로운 아키텍쳐로 변경하면서 구조에 대한 이해도를 높일 수 있었어요. 이 경험을 바탕으로 이후에 새롭게 추가되는 카테고리 개발이나 다른 채널들의 신규 소개 페이지 등을 빠르게 만들 수 있게 되었습니다.가장 좋았던 것은 커뮤니케이션이었는데요. 기획, 디자인, 개발의 유기적인 소통이 중요했기 때문에 개발자임에도 기획 미팅에 들어가거나, 디자인에 대한 의견을 낼 때가 많았습니다!! 팀원들 또한 열린 마음으로 저의 의견을 적극적으로 받아들여 주셨기 때문에, 새로운 아이디어를 낼 때가 많았던 것 같아요. 그리고 개발뿐만 아니라 여러 경험을 통해 서비스가 완성되는 과정을 지켜보는 것 또한 큰 자산이라고 생각했어요.<핀다 개발자 남은우, 출처 : 핀다>스타트업에 입사를 희망하는 분들에게스타트업은 대부분 바로 업무에 투입가능한 사람을 원하는 경우가 많아요. 따라서 지원하기 위해 어느 정도 준비가 필요하겠죠? 입사 후에 모든 일을 척척 수행할 수 있는 사람이면 좋겠지만, 전문적이지는 않더라도 자신이 지원하게 된 회사가 어떤 서비스를 제공하는지 파악하거나, 해당 서비스를 사용해보는 것이 좋아요.요새 드라마나 영화에 종종 스타트업 이야기들이 많이 나오는 것 같아요. 하지만 매스컴에 비춰지는 것이 자유분방하고 즐거운 모습뿐인 것 같아 조금 아쉬운 마음이 들기도 합니다. 회사에 따라 다르겠지만, 스타트업 특성상 조금 더 빠르게 달려야 할 때가 많거든요. 대신 남들보다 조금 더 빠르게 성장할 수 있다는 것!!! 입사를 희망하시는 여러분도 자신과 맞는 회사를 찾고, 꼭 특급 성장의 기회를 잡으셨으면 좋겠습니다.#핀다 #입사후기 #팀원소개 #팀원인터뷰 #팀원자랑 #기업문화 #조직문화
조회수 2310

ZOYIFUL TALK (1) 사무실이 마음에 들어 왔다가 개발에 재미 들렸죠

유저 반응을 볼 때가 즐겁다는 프론트엔드 엔지니어 인턴 Mino조이에서 소프트웨어 엔지니어 인턴으로 살아간다는 것이 어떤지 궁금해 하시는 분들이 많아 4개월차 소프트웨어 엔지니어 인턴 미노(본명 천민호)를 Zoyiful Talk 첫 번째 주자로 모셨습니다.ZOYI: 미노 안녕하세요! 인턴으로 조인하신지 벌써 4개월이 지나셨다면서요. 우선 간단한 소개부터 해주세요. 회사에서 무슨 일을 하고 있나요?MINO: 안녕하세요, 채널(Channel)이라는 조이 신규 서비스를 개발하고 있는 엔지니어 미노입니다. 채널은 소비자와 커머스 기업을 연결해주는 소통 창구 같은 서비스인데요, 저는 그 중에 웹 프론트엔드를 개발하고 있습니다.ZOYI: 프론트엔드가 뭔가요? 좀 더 설명해 주세요.MINO: 프론트엔드는 흔히 ‘웹 개발자’라 하는데요, 웹이나 앱에서 서비스 이용자가 경험하는 부분을 개발합니다. 이용자에게 더 좋은 시각적 효과를 주고, 더 편리한 경험을 제공하기 위해 기술을 이용하죠. 이를 구현하기 위해 자바스크립트라는 언어를 사용하고, react.js를 프레임워크로 사용하고 있습니다.ZOYI: 원래부터 프론트 개발을 많이 하셨었나요?MINO: 프론트엔드는 HTML 작성할 수 있는 정도? 아니면 레일즈로 간단한 홈페이지 게시판 만드는 정도였어요. 자바스크립트는 조이에서 처음 배워봤고요.사실 개발 시작한 것 자체가 작년 9–10월이니 이제 반 년 좀 넘었네요. 코딩은 2년 전부터 시작했었는데 거의 알고리즘 공부가 위주였고 최근에야 제대로 개발을 한 것 같아요ZOYI: 조이에는 어떻게 조인하게 되신 거예요?MINO: 대학 개발 동아리 회장을 할 당시 대회 후원사가 필요해서 레드(CEO)한테 컨택한 적이 있거든요. 후원을 받고 나서 레드의 권유로 회사에 한 번 놀러왔는데, 사무실이 생각보다 좋더라고요. (웃음)스타트업 하면 좁은 공간에 다닥다닥 붙어있는 모습을 생각했었는데… 깔끔한 공간이 인상깊었어요.높은 천장과 통유리 채광을 자랑하는 조이 사무실에 반했다고 합니다.ZOYI: ㅎㅎㅎ 직접 일해보니 어때요? 실제로도 깨끗하던가요?MINO: 레드의 책상이 좀 더럽긴 하지만…은 농담이고요, 실제로 일해보니 더 좋은 것 같아요. 책상도 넓고… 제가 이렇게 하얀 느낌을 좋아하거든요.ZOYI: 조이에서의 4개월을 지내보니 어때요?MINO: 음… 4개월 지나고 나니, 이제야 내가 뭘 모르고 뭐가 부족한지를 알 수 있게 된 것 같아요. 잘한다고 말하긴 아직 부끄럽지만, 적어도 구글링으로 뭘 찾아야 할지는 알 수 있게 됐어요.ZOYI: 안해본 것들을 했잖아요, 주로 어떻게 습득을 했어요?MINO: 사람마다 좀 다를 수 있는데 저는 그냥 시간 날때마다 조이 오픈소스 프로젝트들을 하나하나 열어보면서 이게 어떻게 동작하나를 봤어요. 그래도 모르면 물어보면서 Follow up 받고… 동료들한테 부담없이 물어볼 수 있어서 좋았어요. 촉진제같은 역할을 해 준 것 같아요.한 번은, 전혀 새로운 분야이고 처음 접해보는 언어를 다루는 거라 익숙치 못해 하루종일 구글링을 한 적이 있어요. 그런데도 오늘 커밋 했냐, 뭐했냐 이런 얘기가 없고… 당신의 성장을 그냥 지켜보겠다는 태도인 거예요. 처음엔 익숙하지가 않았는데, 그런 분위기 덕분에 결과적으로 리서치를 잘 하고 일을 성공적으로 마무리 할 수 있었어요.ZOYI: 동료들과 교류가 많은 편인가요?저는 프론트엔드를 하다보니 주로 개발팀 멤버들과 많은 시간을 보내는데요, 업무 외적으로도 되게 재미있어서 친하게 지낼 수 도 있고 그래요. 꾸준히 소통하려 하는 게 느껴져요. 나를 막연히 6개월 후 나가는 인턴이 아니라, 함께 성장해 가는 동료로 생각하고 있구나. 하는 기분이 들죠.ZOYI: 푸스볼 중독이라는데?MINO: 푸스볼도 ZOYI에서 처음 배웠는데, 이건 정말, 최고의 레져인 것 같습니다 (목소리 톤 올라감). 가격 대비 효율이 최고예요. 하루 한 번 이상 꼭 하고 있습니다.10분만 해도 맥박이 빨라진다는 엄연한 스포츠, 푸스볼ZOYI: 본인의 푸스볼 랭킹은?MINO: 글쎄요, 디케이(하드웨어 디자이너)보단 잘하지 않을까요? ㅎㅎZOYI: 인턴 끝나면 생각나겠어요, 그러고 보니 인턴도 이제 두 달 남았네요. 돌아가면 하고싶은 일이 있나요?MINO: 아직 고민중이예요. 사실 조이 들어오기 전에는 프론트, 웹 개발자는 정말 안하겠다고 생각했었는데 지금은 이게 재미있다는 생각이 들어요.초반에 누가 “잘 하게 되면 점점 재미있어 질거다”라고 말해준 적이 있는데, 그 말이 공감이 돼요. 점점 배워가면서 지금은 어느정도 의도한 대로 구현이 되니까…이젠 재미있는 거예요. 새로운 분야를 알게 된 느낌? 그래서 앞으로 프론트엔드 개발자로 일해도 좋고, 뭐든 최대한 많은 경험을 하고 많은 지식을 습득해 보고 싶어요.ZOYI: 좋은 계기가 되었네요, 인턴 생활은 만족스러워요?MINO: 네, 생각하던 것 이상으로 좋았어요. 주도적으로 일을 해 나갈 수 있다는 점과, 하나하나 해 나갈 때마다 내가 성장하고 있는 느낌이 좋아요. 사실 처음 입사할 땐 단순히 반복작업만 할 줄 알았거든요. ZOYI엔 뭔가 ‘네 꿈을 펼쳐봐라~’하는 태도가 있는데, 저는 거기에 잘 맞았던 것 같아요.ZOYI: 그렇다면 향후 ZOYI 지원을 고민하시는 분께 어떤 조언 한마디 해주시겠어요?MINO: 주변에 많은 친구들이 ‘난 안될거야’라고 생각하고 지원조차 안하는 경우가 많은데, 저는 일단 지원해 보라고 말해주고 싶어요. 저도 지원할 당시 굉장히 걱정을 했었거든요. 나는 알고리즘 공부밖에 못해봤고, 서버도 용어 하나도 모르는데 내가 잘 할 수 있을까?하는 생각.막상 회사에 들어오고 난 지금은 생각이 많이 달라졌어요. 인턴에게 중요한 자질은 완벽함보다 가능성인 것 같아요. 그 가능성이란 게 대단한 스펙이 아니라, 기초를 탄탄히 가지고 있는 거예요. 그리고 나면 회사에 와서 충분히 성장할 수 있어요.그 좋은 사례가 션(CTO)인 것 같아요. 함께 일하면서 CTO가 되어가는 모습을 곁에서 보는 게 참 좋았어요. 내부에서 우리가 성장해 더 큰 역할을 맡을 수 있는 조직이란 게 참 좋아요.ZOYI: 조언 감사합니다. 남은 기간 ZOYI에서 기대하는 점이 있다면?MINO: 이번 주부터 시작될 개발팀 위클리 세션이 기대돼요. 각자가 알고 있는 기술을 다른 멤버들과 공유하는 시간인데요, 조이가 워낙 다양한 기술을 다루다 보니 제가 담당하지 않는 분야에 대해서는 잘 모르는 게 많거든요. 같이 일하는 사람들은 어떤 분야에 대해 일하고 있는지 기술적으로 알아보고 싶어요.ZOYI: 좋은 시도네요. 마지막으로 글 읽으시는 분들께 한마디 하시겠어요?MINO: ZOYI는 잘하는 사람들이 와서 더 잘하게 되는 곳이 아니라 가능성 있는 사람들이 와서 잘하게 되는 곳이라고 생각해요. 누구에게나 열려 있으니 편히 찾아와 주셨으면 좋겠어요 ^^#조이코퍼레이션 #개발팀 #개발자 #개발환경 #업무환경 #팀원인터뷰 #팀원소개 #팀원자랑
조회수 4832

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

Next.js 튜토리얼 7편: 데이터 가져오기

* 이 글은 Next.js의 공식 튜토리얼을 번역한 글입니다.** 오역 및 오탈자가 있을 수 있습니다. 발견하시면 제보해주세요!목차1편: 시작하기 2편: 페이지 이동 3편: 공유 컴포넌트4편: 동적 페이지 5편: 라우트 마스킹6편: 서버 사이드 7편: 데이터 가져오기 - 현재 글8편: 컴포넌트 스타일링9편: 배포하기개요꽤 그럴듯한 Next.js 애플리케이션을 만드는 방법과 Next.js 라우팅 API의 모든 장점을 배웠습니다.대부분의 경우 데이터 소스에서  원격으로 데이터를 가져와야 합니다. Next.js는 페이지에 데이터를 가져오기 위한 표준 API를 제공합니다. getInitialProps라 불리는 비동기 함수를 사용하여 구현할 것입니다.주어진 페이지에 원격 데이터 소스를 통해 데이터를 가져오고 원하는 페이지에 props을 통해 전달할 수 있습니다. 서버와 클라이언트 둘 다 동작하도록 getInitialProps를 작성할 수 있습니다. 그래서 Next.js는 클라이언트와 서버에서 모두 사용할 수 있습니다. 이번 편에서는 getInitialProps를 사용하여 공개된 TVmaze API에서 가져온 데이터로 배트맨 TV 쇼에 대한 정보를 보여주는 애플리케이션을 구현할 예정입니다.설치이번 장에서는 간단한 Next.js 애플리케이션이 필요합니다. 다음의 샘플 애플리케이션을 다운받아주세요:아래의 명령어로 실행시킬 수 있습니다:이제 http://localhost:3000로 이동하여 애플리케이션에 접근할 수 있습니다.배트맨 쇼 데이터 가져오기데모 애플리케이션 내의 home 페이지에 블로그 포스트 목록이 있습니다. 배트맨 TV 쇼 목록을 표시할 것입니다.쇼의 데이터들을 하드코딩하는 대신에 원격 서버에서 그 정보를 가져옵시다.여기서는 TV 쇼를 가져오기 위해 TVMaze API를 사용합니다.TV 쇼 정보를 검색하는 API 입니다.먼저 isomorphic-unfetch를 설치해야 합니다. 데이터를 가져올 때 사용할 라이브러리입니다. 브라우저 fetch API 구현을 간단히 할 수 있도록 만들어진 것이지만 클라이언트와 서버 환경에서 모두 동작합니다.npm install --save isomorphic-unfetchpages/index.js를 다음과 같이 변경해주세요:위의 페이지에 있는 모든 내용은 아래에 표시된 Index.getInitialProps를 제외하고는 익숙할 것입니다:애플리케이션의 어떤 페이지에든 추가할 수 있는 정적 비동기 함수입니다. 이것을 사용하여 데이터를 가져오고 가져온 데이터를 props를 통해 페이지로 보낼 수 있습니다.보다시피 배트맨 TV 쇼 데이터를 가져오고 'shows' props를 통해 페이지로 전달합니다.위에서 보았던 getInitialProps 함수에서 가져온 데이터 숫자를 콘솔에 출력합니다.이제 브라우저 콘솔과 서버 콘솔을 살펴봅시다. 그리고 페이지를 새로고침 해주세요.페이지를 새로고침 한 후 출력되는 메시지는 어디에서 보였나요?- 서버 콘솔- 브라우저 콘솔- 둘 다- 어떤 콘솔에도 출력되지 않았다서버에서만 출력됩니다이 경우 메시지는 서버에서만 출력됩니다.이는 서버에서 페이지가 랜더링되기 때문입니다.이미 데이터를 가지고 있어 클라이언트에서 다시 정보를 가져올 필요가 없습니다.post 페이지 구현하기TV 쇼에 대한 자세한 정보를 보여주는 "/post" 페이지를 구현해봅시다.먼저 server.js를 열고 /p/:id 라우트를 다음과 같이 바꿔주세요.위처럼 바꾼 코드를 적용하기 위해 애플리케이션을 재실행시켜주세요.이전에는 title 쿼리 파라미터를 페이지에 매핑했습니다. 이제 id로 이름을 바꿔야합니다.다음과 같은 내용으로 pages/post.js를 변경해주세요.페이지의 getInitialProps을 살펴봅시다:여기에서 함수의 첫 번째 파라미터는 context 객체입니다. 정보를 가져올 때 사용할 수 있는 쿼리 필드를 가지고 있습니다.예제에서 쿼리 파라미터로부터 보여지는 ID를 선택하고 TVMaze API로부터 데이터를 가져옵니다.이 getInitialProps 함수에서 표시할 제목을 출력하는 console.log를 추가했습니다. 이제 어디에서 출력되는지 볼 수 있습니다.서버와 클라이언트의 콘솔를 둘 다 열어주세요.그 다음 홈페이지 http://localhost:3000로 이동하여 배트맨 쇼 제목을 클릭하세요.위에서 애기했던 console.log 메시지가 보여지는 장소는 어디인가요?- 서버 콘솔- 브라우저 콘솔- 콘솔 둘 다- 아무 콘솔에서도 출력되지 않는다클라이언트 사이드에서 데이터 가져오기브라우저 콘솔에서 메시지를 볼 수 있습니다.클라이언트 사이드를 통해 포스트 페이지에 이동했기 때문입니다. 그런 다음 클라이언트 사이드로부터 데이터를 가져오는 것은 가장 좋은 방법입니다.예를 들어 http://localhost:3000/p/975에 직접 이동한다면 클라이언트가 아닌 서버에서 메시지가 출력되는 것을 볼 수 있습니다.마무리데이터를 가져오고 서버 사이드에서 렌더링하도록 만드는 Next.js의 가장 중요한 기능 중 하나를 배웠습니다.대부분의 유스 케이스에서 충분히 사용할 수 있는 getInitialProps의 기본을 배웠습니다. 더 많은 것을 배우고 싶다면 Next.js의 문서 중 data fetching 문서를 참고할 수 있습니다.#트레바리 #개발자 #안드로이드 #앱개발 #Next.js #백엔드 #인사이트 #경험공유
조회수 1344

레진 기술 블로그 - Kotlin의 빛과 그림자

핀터레스트의 안드로이드 개발팀이 코틀린을 도입하면서 겪은 어려움과 해결책을 소개한 The Case Against Kotlin을 foot번역하고 자의적으로 해석하고 요약했습니다. 저자 라이언 쿡(Ryan Cooke)은 현재 코틀린이 가트너의 하이프 사이클에서 “뻥튀기된 기대감의 산(Peak of Inflated Expectations)” 쯤에 있다고 말합니다. 레진시 개발동에서는 이미 코틀린을 부분적으로 도입했고, 현재는 범위를 넓혀가는 중인데요… 정말 괜찮은 걸까요?문제: 학습 곡선자바 개발자로서 문법에 익숙해지는 데 1주일 정도 걸립니다.코틀린을 이미 잘하는 사람이 없으면 베스트 프랙티스들을 찾아보면서 해야하는 데 시간이 듭니다.코틀린 사용을 가속화 시키는 데 팀 트레이닝을 계속 해야합니다. -> 기회비용 많이 듭니다.하기 싫어 하는 사람도 있고…혼자서 알아서 잘 배우는 사람도 있고…해결책: 학습 곡선코틀린은 아직 말년병장성숙한 언어가 아닙니다! 지금도 자라나고 있습니다! 그게 제일 무서워..책도 있고 인터넷 리소스도 있지만, 코틀린 신봉자가 하나 있어서 다 가르쳐주는 게 짱입니다.필자가 코틀린을 하고 싶었던 이유는 생산성인데요, 동료들 중에는 그렇게 느꼈던 사람들이 많지 않은 것 같습니다. 정착이 되면 보이겠죠.문제: 빌드 속도Gradle 빌드 속도는 보통 30초, 클린 빌드는 75초 까지 걸립니다.코틀린은 보통 빌드 속도의 25%, 클린 빌드의 40% 밖에 안나옵니다.해결책: 빌드 속도알아서 하셈 ㅋ코틀린 파일 하나 변환 -> 클린 빌드 시 조금 시간이 더 걸립니다. 파일을 많이 변환할수록 느려지긴 하지만 체감하긴 어렵습니다.보통 빌드할 때는 코틀린 파일 많아도 상관 없습니다.결론: 클린 빌드할 때 느려진다는 걸 체감할 겁니다.문제: 개발 안정성코틀린의 문법이나 특성이 문제가 아니라, 코드를 생산성 있게 작성하는 자신을 막는 새로운 문제들 때문이라고 생각합니다.사실 그냥 코틀린 배우기 싫은 거 같아요.예를 들면, 코틀린 애노테이션 프로세서 툴(kapt) 때문에 빌드가 안 되고, 무조건 클린 빌드로만 개발을 했던 적이 있습니다.이거… 코틀린 때문 아니야?!?!?! 하는 의심들 많았죠.고치느라 시간이 많이 흘렀습니다.또 어떤 문제가 튀어나올지에 대한 두려움이 커지네요.해결책: 개발 안정성그냥 IDE 나 언어의 stable 버전만 업데이트 하세요.안정된 버전들만 사용하면 그나마 힘든 일 없을거예요.정말?문제: 정적 분석FindBugs, PMD, Error Prone, Checkstyles and LintJava 는 이와 같은 툴들로 인해 Code Review에 쓸데없는 걸 줄이거나 룰을 적용할 수 있는데,코틀린에는… 이런 게 없… 분석을 위한 게 아직… 없습니다… 사람들이 알아서 다 찾아야 합니다.해결책: 정적 분석그냥 손가락빨고 기다려야 합니다. 아니면, 직접 만드세요!문제: 나 돌아갈래~돌아가기 쉽지 않습니다. 자바를 코틀린으로 옮기기에는 쉬운데, 반대는… 어렵습니다!코드가 깨지고, 변수명부터, 이런 저런 부분들을 다시 구현해야합니다.코틀린스럽거나, 코틀린의 고유한 기능들을 사용했다면, 여기서부터 헬이죠.해결책: 나 돌아갈래~되돌아오는 건 쉽지 않기 때문에 잘 생각해야 합니다.유닛 테스트가 정말 잘 된 파일들부터 바꾸세요.간단하고 재사용 가능한 잘 모듈화된 파일들을 먼저 바꾸세요.결론이 글은 고려해야 할 리스크에 대해서 나열했습니다.단점들은 구글과 젯브레인과 스택오버플로우가 차차 해결해 줄 겁니다.TL;DR 코틀린으로 작성하는 건 쉽지만, 되돌리기는 어렵습니다.그래서 말인데… 레진코믹스에서 코틀린 삽질을 함께 할 개발자를 모십니다!
조회수 17484

Nodejs 기반의 개발 환경 클린하게 재 구성하기

다양한 언어 기반으로 개발 환경을 구축하여 만들다보면, 소프트웨어 버전관리 해야할 일이 흔히 생기곤 한다. 특히, 종종 대격변이 있는 버전의 판올림으로인해 충돌이 나거나 심볼릭 링크가 유실되는 경우들이 간혹 있는데 이번에도 그런 케이스였다.최근 node.js 기반으로 다양한 프로젝트 (vue.js, react.js등)를 진행하다가 이것저것 환경을 만지고 고치다보니 결국 node.js 를 완전히 클린하게 삭제해야 할 일이 생겼다.아마 이 환경에 결정타를 먹인 것이 OSX 환경에서 El Capitan에서 작업하던 Node.js를 그대로 high sierra로 OSX를 판올림 하면서 퍼미션 권한의 문제가 생긴건지, 노드 패키지 관리나 npm이 정상적으로 동작하지 않으면서 개발환경을 재 설정 할 수 밖에 없게 되었는데, 그 과정에 기름을 부어버리듯 당시에 brew로 설치한 노드가 brew로 삭제가 되지 않는 문제가 발생해버렸다.결국 환경을 처음부터 재 설치 해야하는 과정을 겪어야했는데 기존에 설치된 다양한 패키지 모듈의 찌꺼기들이 남아서 한방에 클린 설치를 할 수 있는 방법이 없을까 싶어 구글링을 해본 결과 앞서서 수많은 시행착오를 겪은 선배님들의 아주 좋은 작업 방식이 있어서 아래에 방법을 공유해본다.요세미티에서 nodejs 정리하는 법 [1]Uninstall nodejs from OSX Yosemite# 첫번째:lsbom -f -l -s -pf /var/db/receipts/org.nodejs.pkg.bom | while read f; do  sudo rm /usr/local/${f}; donesudo rm -rf /usr/local/lib/node /usr/local/lib/node_modules /var/db/receipts/org.nodejs.*# 완전히 nodejs + npm 을 날려버리는 방법 :# /usr/local/lib 경로로 가서 node 와 관련된 노드 모듈을 전부 삭제cd /usr/local/libsudo rm -rf node*# /usr/local/include 경로로 가서 node 와 관련된 노드 모듈 전부 삭제cd /usr/local/includesudo rm -rf node*# 만약 brew 로 인스톨을 했다면 아래와 같은 방법으로 삭제도 가능함. (저는 아래는 brew자체가 망가졌었는지 판올림으로 인한 권한 문제인지 brew로는 삭제 불가능했음.)brew uninstall node# home 디렉토리나 local, lib, include등의 폴더와 관련된 모든 파일은 아래의 경로에 있으니 찾아 들어가서 삭제cd /usr/local/binsudo rm -rf /usr/local/bin/npmsudo rm -rf /usr/local/bin/nodels -las# 아마 혹시 모르니까 클린하게 아래의 명령어도 한번 돌려주자sudo rm -rf /usr/local/share/man/man1/node.1sudo rm -rf /usr/local/lib/dtrace/node.dsudo rm -rf ~/.npmhomebrew를 사용하는 유저들 중에 npm이 제대로 동작하지 않으면 아래와 같은 방법으로도 처방이 가능하다. [2]rm -rf /usr/local/lib/node_modulesbrew uninstall nodebrew install node --without-npmecho prefix=~/.npm-packages >> ~/.npmrccurl -L https://www.npmjs.com/install.sh | sh클린하게 설치를 끝나고 react-native를 컴파일하는 과정에서 깃에 관련된 오류가 발생한다면 아래의 방법을 사용해보자. [3]오류메세지 :xcrun: error: invalid active developer path (/Library/Developer/CommandLineTools), missing xcrun at: /Library/Developer/CommandLineTools/usr/bin/xcrun솔루션 :xcode-select --install엘케피탄에서 하이시에라로 osx를 업데이트 하면서 homebrew의 링크가 깨졌다면 아래의 방법으로 다시 붙여준다. [4]sudo chown -R "$USER":admin /usr/localsudo chown -R "$USER":admin /Library/Caches/Homebrewbrew link libpng참고 출처 :[1] : https://gist.github.com/TonyMtz/d75101d9bdf764c890ef[2] : https://stackoverflow.com/questions/32893412/command-line-tools-not-working-os-x-el-capitan-macos-sierra-macos-high-sierra[3] : https://stackoverflow.com/questions/39778607/error-running-react-native-app-from-terminal-ios[4] : https://github.com/mikepurvis/ros-install-osx/issues/28 #더팀스 #THETEAMS #풀스택개발자 #Node.js #백엔드 #인사이트 #꿀팁

기업문화 엿볼 때, 더팀스

로그인

/