Docker 에 대한 생각

최근의 S/W환경을 보면 서비스는 점점 더 복잡해 지고 있고, 기술도 다양하고, 외부 환경도 급격하게 변화하고 있다.전통적인 Monolithic Architecture 환경에서는 이런 변화에 발빠르게 대응하는 것이 사실상 불가능 하다고 볼 수 있다. 이런 복잡하고도 빠르게 변하는 환경에 대응하기 위해 많은 시도들이 있어 왔으며, 최근에는 MSA(Micro Service Architecture)라는 사상이 주목을 받고 있다.

MSA라는 것은 독립적이고 단순한 서비스로 전체 서비스를 구성하고, 그에 따라 독립적인 팀이 각 서비스의 주인(개발과 운영에 대한 책임,권한을 모두 가짐)이 되는 것을 말한다.(마틴 파울러 아저씨의 블로그http://martinfowler.com/articles/microservices.html 에 보면 MSA의 개념에 대해 잘 정리가 되어 있다.) 이와 같이 서비스를 micro화 하게 되면 , 분산환경을 통한 scale-out 확장에도 용이하고 monolithic 방식에 비해 배포에 대한 부담이 줄어든다(전체 서비스에 영향을 주지 않고 배포를 할 수 있음). 장애가 발생해도 그 영향의 범위를 국소적으로 제한시킬수 있게 된다.

이렇게 MSA 사상을 도입하면 복잡성과 빠른 변화에 대응하는 것은 가능해 지지만 그에 비해 변경 및 추가된 어플리케이션을 build하고 테스트하고, 배포(ship)하고, 실행시키는 것은 쉬운 문제는 아니다.

분산 환경에 배포되어 있는 많은 machine들이 모두 동일한 환경을 가지고 있으리라는 보장이 없기 때문에 어플리케이션을 분산환경에 배포를 하기 위해서는 환경정보를 잘 확인하고 배포를 해야 한다. 자칫하다가는 장애를 만들어 내기 일쑤다. 개발환경과 테스트 환경, 운영환경이 모두 다를 것이기 때문에 어플리케이션이 동일하게 동작하리라는 보장을 하기가 힘들어 진다.

이런 배경에서 “어디에서나 동일하게 동작하는 소스코드”를 제공하기 위해서 등장한 기술이 Docker라고 볼 수 있다. Docker는 개발자가 개발한 어떤 어플리케이션이든(언어에 관계없이) 그 어플리케이션과  그 어플리케이션이 돌아가게 하는 최소한의 디펜던시만 Container에 담아 shipping할 수 있게 한다. 이런 작업을 “Dockerized”라고 부르게 되는데, 이렇게 Dockerized가 되면 어떤 환경에서도 – OSX, Windows, Redhat, Ubunto, 노트북, 서버, 클라우드 등- 동일하게 동작하는 것을 보장할 수 있다. 구글에서 돌아가는 모든 어플리케이션은 모두 container에 담겨져 있으면 이 Container갯수만 약 20억개 정도 된다고 한다.

이런 개념은 얼핏 보면 VMs과 별로 다르지 않은 것처럼 보이기도 하는데 아래 그림을 보면 그 차이를 이해할 수 있다.

개인 PC에서 개발하면서 개발환경에 웹서버, was, DB등을 설치해 본 경험이 있을 것이다. 당연하지만 이 개발환경에서의 path라든지 각종 시스템 파라미터 등은 테스트 환경이나 운영환경에서와 다를 수 밖에 없으므로 이런 환경의 차이로 인해 “내 환경에서만 동작하는 어플리케이션”이라는 비아냥을 받게 될때도 있을 것이다.

Docker를 이용하여 개발에 필요한 환경들을 모두 Container에 담아, 그 이미지를 QA에게 전달해서 테스트를 부탁할 수 있다. 테스트가 완료가 되면 그 이미지를 그대로 운영환경에 배포해서 바로 서비스를 시작 할수 있게 된다. Docker 이미지는 Git 처럼 버전관리가 가능하기 때문에 장애가 발생했을 경우 이전 버전으로 쉽게 롤백도 가능하다.

Docker Hub(hub.docker.com)에 가보면 여러가지 Docker 이미지가 공개되어 있는 것을 확인해 볼 수 있다. MySql 를 비롯해서 Redis, NginX 등 많은 제품들이 Docker Container로 제공되고 있으며 이 이미지들을 사용한다면 여러분의 노트북에서나 클라우드 환경의 서버에서나 동일하게 동작하는 것을 보장받을 수 있을 것이다.

Docker를 사용하기 위해서는 JVM처럼 각 플랫폼마다 별도로 제공되는 Docker플랫폼을 받아서 설치를 해야 한다. https://docs.docker.com/installation/#installation 에 보면 지원하고 있는 플랫폼을 확인해 볼 수 있는데, Amazon EC2와 Google Cloud Platform 이 눈에 띈다.

Docker 이미지를 Docker Hub에서 내려받고(pull), 수정내용을 반영(commit)하고 다시 Docker Hub로 올리는(Push) 과정은 Git과 거의 동일하다고 볼 수 있다.

Docker 에 패키징되어 있는 어플리케이션은 docker라는 명령을 통해 접근할 수 있으며 가이드 https://docs.docker.com/userguide/ 를 통해 사용방법은 크게 어렵지 않게 익힐수 있을거라 보여진다.

Docker로 인해 앞으로 S/W개발의 많은 모습들이 달라지게 되지 않을까 하는 생각이 든다.

Java8의 람다 표현식 ( Lambda Expression)

Java8 에 Lambda식이 새로 추가되었다. 람다 표현식에 대해서 잘 모른다면 그 표현식을 봤을 때 문맥을 이해하기가 쉽지 않다.

Java에서의 lambda expression을 어떻게 사용하고 왜 사용하는지 간단하게 정리를 해 보고자 한다.

람다식은 함수형 언어에서 선호하는 “정의”연산자 “->” 를 사용한다. 최근에 함수형 언어가 주목을 받고 있고, Java에서도 이를 지원하려는 움직임으로 보인다.

Scala, Erlang를 비롯한 함수형 언어라는 것의 의미를 먼저 살펴보자.

“함수형 언어”는 “명령형 언어”와 대조적인 관계에 있다. 명령형 언어에서는 상태를 바꾸는 것을 강조하는 것과 달리, 함수형 언어에서는 함수를 정의하는 것에 중점을 둔다.

명령형 언어에서 “a = 1” 이라고 선언을 하는 것은 “a라는 변수에 1을 담아라” 라는 의미이지만, 함수형 언어에서는 a 를 1로 정의한다는 의미로 사용한다. ( 그런 의미에서 R같은 언어에서는 대입 연산자를 A <- 1 이라고 표기하기도 한다)

대입과 정의가 비슷한 것 같아 보이지만 큰 차이가 있다. 대입은 프로그램의 수행중에 언제든지 값이 변경될 수 있고, 정의는 값이 변경될 수 없는 것을 의미한다.

즉 수학에서  f(x) = x + 1 이라고 정의된 함수가 있다면 입력값 x 에 대해 항상 x + 1 이라는 동일한 결과값을 기대할 수 있게 된다. ( 명령형 언어에서라면 변수의 값이 변경될 수 있으므로 같은 입력에 대해 결과값이 달라질 수 있음)

입력에 대해 결과값이 동일하다는 특징은 멀티 스레딩 환경에서 큰 장점이 있고(Thread safe), 이러한 특징은 손쉽게 병렬 프로그래밍을 할 수 있게 해 준다.

이런 특징이 멀티코어 프로세싱이 요구되고 있는 근래의 프로그래밍 환경에서 함수형 언어가 다시 주목을 받게 된 이유라고 볼 수 있다.

Java에서 Lambda 표현식을 사용하는 목적은, 예전처럼 변수를 직접 전달하여 그 값을 변경함으로서 흐름을 처리하지 않고, 행위 자체(Behavor Parameter)를 전달함으로서 함수형 프로그램이 지향하는 바를 얻기 위함이라고 생각된다.사실 이러한 목적으로는 기존에 이미 익명 클래스(Anonymous Class)를 통해 해결할수도 있었던 문제이지만, Lambda식을 사용함으로 해서 코드를 좀 더 깔끔하고 가독성 좋게 만들수 있게 된 것이다.

그리고 공식적으로 “자바는 함수형 프로그래밍을 지원한다!”라고 얘기하려고 한것이 아닐까 하는 생각도..

Lambda표션식이 생소한 이유중 하나가 바로 전에는 클래스가 없는 함수를 사용할 수 없었지만, 이 Lambda라는 놈은 클래스 없이 함수 구현체가 존재하기 때문에 자바에 친숙한 사용자일수록 처음에는 어색하게 느껴질 수 있을 것 같다.

“Java 에서 Lambda 표현식은 추상 메소드가 하나만 있는 인터페이스(Funtional Interface)를 익명 클래스 대신 구현할 수 있게 해 주는 방법” 이라고 할 수 있다.

추상 메소드가 하나만 있는 인터페이스들 중에 우리가 잘 알고 있는 것들이 있다. Java.util.Comparator , java.lang,Runnable 등이 그것인데 Comparator를 이용해서  lambda 표현식을 사용할때와 그렇지 않을 때 어떻게 코드가 달라지는지 살펴보자.

아래와 같이 User 클래스가 있다.


public class User implements Comparable{

     private Long userId;
     private String userName;
     private String emailAddress;
     private Date joinDate;

     // getter &amp;amp; setter 코드 생략

     @Override
     public int compareTo(Object o) {

         User otherUser = (User) o;
         if (this.userId &gt; otherUser.getUserId()) {
             return 1;
         } else if (this.userId &lt; otherUser.getUserId()) {
             return -1;
         } else {
             return 0;
         }
     }
 }

이 User 의 목록이 있을 때 userId를 기준으로 sorting을 하는 코드를 익명 클래스를 이용해서 만들면 아래와 같다.

userList.sort(new Comparator&lt;User&gt;() {
   
@Override
  
 public int compare(User user1, User user2) {
      
       return user1.compareTo(user2);
   
  }

});

익명 클래스를 사용하지 않고 “Lambda”표현식을 이용하여 아래와 같이 코딩할 수 있다.

userList.sort((User o1, User o2) -&gt; o1.compareTo(o2));

이렇게 Lambda 표현식은 함수의 argument로 전달이 될수도 있고, 아래와 같이 변수에 할당하는 것도 가능하다,

Comparator &lt;User&gt; userComparator = (User u1, User u2) -&gt; u1.compareTo(u2);


userList.sort(userComparator);

아래 그림을 통해 Lambda 표현식을 어떻게 작성하는지 살펴보자.
lamgda_exp

Lambda 표현식은 “->” 를 기준으로 왼쪽과 오른쪽으로 나누어 지는데,

왼쪽의 (User 01, User 02) 부분은 functional interface – 추상 메소드가 하나만 있는 인터페이스- 에 선언된 추상메소드의 argument 를 나타낸다.
오른쪽 부분은 추상메소드의 구현내용 – implementation-을 적어주는데 return type은 interface 의 추상메소드 signature를 따라 자동으로 결정된다. ( Runnable의 경우 void 임 )

람다 아키텍처

블로그를 한번 써 보기로 마음을 먹고 첫번째 포스팅이 될 것 같다.
페이스북에서 소개된 InfoQ의 Lambda Architecture 에 관한 내용을 우연히 보게 되었다.(http://www.infoq.com/articles/lambda-architecture-scalable-big-data-solutions).
사실 요즈음 함수형 언어와 Java 8의 람다 표현식에 관심을 갖고 이것저것 찾아보고 있던 중이라,
Lambda Architecture 라는 명칭을 보고 혹시 관계가 있는 것인가? 하는 마음에 들쳐보게 되었지만 실상을 그닥 관계가 없는 듯 하다.
왜 Lambda 라는 이름이 붙었을까?
Lambda(참다)는 그리스 문자의 열한번째 문자로 (uppercase Λ, lowercase λ) 로 표기한다.
수학에서는 함수 추상화를 위해 사용되고, 리스프같은 함수형언어는 람다 대수로 부터 직접 영향을 받아 탄생했다고 하니 Java에서는 람다 표현식이라는 말은 이런 의미로 가져다 이름을 지었다고 이해할 수 있겠지만.. 람다 아키텍처라니.. 좀 궁금해 졌다.
Storm Project로 잘 알려진 Nathan Marz 가 자신의 경험을 Lambda Architecture로 소개한 적이 있다고 한다.
요약하자면, 데이터를 batch처리와 실시간 처리로 나누어서 처리하여 대용량의 데이터를 빠르고 안전하게 처리하고자 하는 목적이라고 생각해 볼 수 있을 것 같다.

사실, 수억건이 넘는 데이터를 RDBMS에 넣어놓고 실시간으로 통계나 기타 분석 데이터 ( Data Processing Result )를 보고자 한다면 좀 난감한 상황이 될 수 있다. (사실 거의 불가능 하다고 할 수 있다)
필자도 과거 이동통신사의 NMS를 구축하면서 비슷한 고민을 한 적이 있다.
가입자의 통화데이터(Call Detail Log)에 포함된 여러가지 신호데이터를 이용해서 망(네트워크)의 품질을 계산하는 프로젝트인데, 초당 7,000건 이상씩 쏟아져 들어오는 데이터를 이용해서 최근 1분간의 통계데이터를 처리해서 제공해야 하고, 네트워크의 각 노드별 품질 분석 데이터를 hourly/daily/weekly/monthly 기준으로 제공을 해야 했었다.
지금에야 하둡에 MR에 Hbase, Storm, Spark등을 이용하면 그리 어렵지 않게 아키텍처를 구성할 수 있겠지만 2005년도에만 해도 이런 볼륨의 데이터를 처리하기란 그렇게 쉽지 않았다.
여튼 최근 1분간의 통계(near-real-time)와 주기적인(시간별,일별,주별,월별,분기별)  통계 데이터를 동시에 제공해야 했고, 초당 7,000건(하루에 약 6억건, 한달에 백억건)의 데이터를 가지고 실시간 처리도 문제지만 RDBMS에 넣어놓고 Query로 해결하기에는 당연하게도 불가능 했었다. 그래서 그 당시 여러가지로 고민을 하고 최종 적용했던 방식이 지금 얘기하고 있는 람다 아키텍처와 상당히 유사한 모습을 가지고 있었다.

위 그림에서 처럼 CDR이 발생되면  파일을 전처리(Splitting) 하고,  M/R을 돌린 결과를 메모리에 올려두고 실시간 뷰를 제공하였다. 보통 Speed Layer에서 사용되는 데이터는 최근1시간 데이터면 충분하고, 1시간이 지난 데이터는 메모리에서 디스크로 옮겨놓는다. 디스크로 옮겨진 데이터를 시간별/일별/월별/분기별로 통계를 생성해서 RDBMS에 저장해 둔다. 이때 RDBMS는 데이터에 억세스 하는 것을 빠르게 하고, 보관주기가 지난 데이터를 빠르게 삭제하기 위해 파티셔닝을 해 둔다.  Client 에서 쿼리를 하면 시간 조건을 보고 CEF에서 real time view에서 데이터를 가지고 올지, batch view에서 데이터를 가지고 올지 판단해서 쿼리를 실행하고 결과를 돌려준다. client에서는 조회하고자 하는 데이터가 real time view 에 있는지 batch view에 있는지 고려할 필요가 없다.
과거(2005년)에는 real time view를 구현하기 위해 직접 in-memory DB를 만들어 사용햇었지만(사실 버클리DB를 램디스크에 올려놓고 사용해 보기도 하는 등 여러가지 방법들을 적용해 보았지만…) , 요즘에는 다양한 NoSQL제품들이 많이 있으니 다양한 시도를 해 볼 수 있을 것 같다.
아래는 람다 아키텍처 그림

(이미지 출처 : http://lambda-architecture.net )

람다 아키텍처는 레퍼런스 아키텍처다. 각 레이어에 사용될 적당한 솔루션들이 많이 있으며 어느것도 정답은 될 수 없다.
중요한 점은 대용량 데이터를 가지고 실시간/누적 통계를 모두 제공해야 할 speed layer 와 batch layer를 잘 정의해서 적용 전략을 세우는 것이 아닐까 한다.
참고 자료