top bar

글 목록

2017년 7월 9일 일요일

[Spring] POJO의 이해

개요



'POJO'란, 'Plain Old Java Object'의 약자이다. 이 용어의 유래를 잘 찾아보면 굉장히 허무할 것이다. 이유는 아래와 같다.

2000년 즈음에, 마틴파울러는 '평범한' JAVA 오브젝트에 비즈니스 로직을 넣어 사용하는 것이 기존에 널리 사용되던 EJB 빈보다 뛰어난 점에 주목하고 있었는데, 이러한 장점에 비해 폼나는 용어가 없어서 만들었다는 것이 바로 'POJO' 라는 것이다. 링크 참고

아래의 spring.io 웹사이트에, 스프링 관점에서 POJO의 이해를 돕는 글이 있다.

https://spring.io/understanding/POJO#understanding-pojos

위의 글 또한, POJO를 한 문장으로 잘 설명하고 있다.






여기서 'be bogged down' 이라는 문구가 낯설었는데, 이 숙어의 뜻을 찾아보니 '너무 복잡하거나 어려워서 다른것을 할 수 없는' 이라는 뜻이었다. 따라서 위 문장은 "프레임워크에 의해 확장된 즉, 그것으로 인해 'bogged down' 되지 '않은' 자바 오브젝트" 정도의 뜻이 되지 않을까 싶다. 뭐 이러한 뜻풀이는 난해 할 수도 있으니, 그냥 '평범한 자바 오브젝트' 라고 이해해야 겠다.


예시를 통한 이해



가령, 'JMS'를 통해 메시지 수신의 기능을 하는 클래스를 설계한다고 해보자. 아래와 같을 것이다.

public class ExampleListener implements MessageListener {

    public void onMessage(Message message) {
        if (message instanceof TextMessage) {
            try {
                System.out.println(((TextMessage) message).getText());
            }
            catch (JMSException ex) {
                throw new RuntimeException(ex);
            }
        }
        else {
            throw new IllegalArgumentException("Message type error");
        }
    }
}

위 클래스는 특정 메시징 솔루션(JMS)의 인터페이스를 구현(implements) 하고 있다. 다시 말하면 해당 인터페이스에 '종속'되어 있는 것이다. 따라서 다른 대안적인 메시징 솔루션을 적용하려고 한다면 매우 큰 변경 비용이 들 것은 자명하다. (당연히, 거대한 코드 베이스를 가지고 있는 대규모 프로젝트의 경우를 말한다)

이것을 'POJO'의 관점에서 접근한다면, 메시지를 처리하는 클래스가 특정 솔루션에 종속되지 않은, (원문에서는 'solution free'라고 표현한다) 상태로 설계 되어야 한다. 아래와 같다.

@Component
public class ExampleListenerPojo {

    @JmsListener(destination = "myDestination")
    public void processOrder(String message) {
        System.out.println(message);
    }
}

이것이 스프링이 추구하는 'POJO' 클래스의 모습이다. 'JMS'와 관련된 확장 코드가 보이지 않으며, 따라서 'JMS'와 커넥션을 연결하고 메시지를 처리하는 책임은 모두 어노테이션에 위임 된 것을 볼 수 있다.

그리고 만약 'JMS'에서 'RabbitMQ'로 메시징 솔루션을 변경한다고 했을때, 개발자는 '@JmsListener' 어노테이션을 '@RabbitListener' 어노테이션으로 교체 하기만 하면 된다. 또한 이러한 설계는 라이브러리에 종속적이지 않아 단위 테스트를 용이하게 해 준다. 여기서 스프링 'POJO'의 강력함이 드러난다고 볼 수 있다.

이렇듯 'POJO'를 통해, 스프링은 개발자의 코드와 외부 라이브러리의 'Coupling' 을 최소화 시켜준다. 이것은 다시말하면 개발자가 작성한 서비스 코드는 그 서비스 자체로 주체가 되는 것이 아니라 전체 어플리케이션의 한 part로서 동작하고, 전체 어플리케이션에 '연결'(wiring) 되게 함으로서, 좀더 확장성 있는 소프트웨어 설계를 가능하게 해준다는 것이다. 또한 이것은 'Inversion Of Control(IOC)'과 'Dependency Injection(의존성 주입)' 의 메인 컨셉이기도 하다.

하지만 단순히 클래스 설계시 'implements'와 'extends' 키워드를 사용하지 않는다고 해서 POJO 인가? 라고 했을때는 조금은 의아하다. 스프링을 사용한 프로젝트는 이미 어노테이션으로 떡칠(?)되어 있을 것이고, 이 어노테이션 자체도 특정 라이브러리에 종속되어 있기 때문이다.

'스프링은 완전한 POJO를 제공한다'에 대해선 반대의 입장이지만,
흠.. 내공이 부족해서 일까. POJO의 관해선 조금 더 연구가 필요 할 것 같다.



2017년 7월 6일 목요일

[MySQL] 주요 스토리지 엔진(Storage Engine) 간단 비교

MySQL은 크게 아래의 2가지 구조로 되어 있다.


  • 서버 엔진 : 클라이언트(또는 사용자)가 Query를 요청했을때, Query Parsing과, 스토리지 엔진에 데이터를 요청하는 작업을 수행.

  • 스토리지 엔진 : 물리적 저장장치에서 데이터를 읽어오는 역할을 담당.


여기서 중점적으로 봐야할 것은 '스토리지 엔진' 인데, 그 이유는 데이터를 직접적으로 다루는 역할을 하므로 엔진 종류 마다 동작원리가 다르고, 따라서 트랜잭션, 성능과 같은 주요 이슈에도 밀접하게 연관되어 있기 때문이다.

MySQL의 스토리지 엔진은 'Plug in' 방식이며, 기본적으로 8가지의 스토리지 엔진이 탑재 되어 있다. 아래의 명령으로도 탑재된 스토리지 엔진을 확인 할 수 있다.

mysql> SHOW ENGINES;

위에서 말한 것과 같이 Plug 'in' 방식 이기때문에 스토리지 엔진 교체 작업이 비교적 간단하다. 또한 기본 탑재 플러그인 외에 서드파티에서 제공하는 다양한 스토리지 엔진을 손쉽게 적용할 수 있다는 것이 장점이다.

각 플러그인의 '.so'(Shared Object) 파일을 얻었다면 아래와 같이 설치/삭제 할 수 있다.

INSTALL PLUGIN ha_example SONAME 'ha_example.so';

UNINSTALL PLUGIN ha_example;

그리고 'CREATE TABLE' 문을 사용하여 테이블을 생성할 때, 맨 마지막 구문에 스토리지 엔진의 이름을 추가함으로 아주 간단하게 설정 할 수 있다. 아래와 같다.

-- ENGINE=INNODB not needed unless you have set a different
-- default storage engine.
CREATE TABLE t1 (i INT) ENGINE = INNODB;
-- Simple table definitions can be switched from one to another.
CREATE TABLE t2 (i INT) ENGINE = CSV;
CREATE TABLE t3 (i INT) ENGINE = MEMORY;

자세한것은 공식 문서 참고

이 포스팅에서는 가장 많이 쓰인다고 알려진 스토리지 엔진인 MyISAM, InnoDB, Archive
의 3가지 스토리지 엔진을 비교/정리 하려고 한다.


  • InnoDB : 따로 스토리지 엔진을 명시하지 않으면 default 로 설정되는 스토리지 엔진이다. InnoDB는 transaction-safe 하며, 커밋과 롤백, 그리고 데이터 복구 기능을 제공하므로 데이터를 효과적으로 보호 할 수 있다.

    InnoDB는 기본적으로 row-level locking 제공하며, 또한 데이터를 clustered index에 저장하여 PK 기반의 query의 I/O 비용을 줄인다. 또한 FK 제약을 제공하여 데이터 무결성을 보장한다.

  • MyISAM : 트랜잭션을 지원하지 않고 table-level locking을 제공한다. 따라서 multi-thread 환경에서 성능이 저하 될 수 있다. 특정 세션이 테이블을 변경하는 동안 테이블 단위로 lock이 잡히기 때문이다.

    텍스트 전문 검색(Fulltext Searching)과 지리정보 처리 기능도 지원되는데, 이를 사용할 시에는 파티셔닝을 사용할 수 없다는 단점이 있다.

  • Archive : '로그 수집'에 적합한 엔진이다. 데이터가 메모리상에서 압축되고 압축된 상태로 디스크에 저장이 되기 때문에 row-level locking이 가능하다.

    다만, 한번 INSERT된 데이터는 UPDATE, DELETE를 사용할 수 없으며 인덱스를 지원하지 않는다. 따라서 거의 가공하지 않을 원시 로그 데이터를 관리하는데에 효율적일 수 있고, 테이블 파티셔닝도 지원한다. 다만 트랜잭션은 지원하지 않는다.


아래는 상세 도표다. (수시로 참고 해야겠당..)

InnoDB Storage Engine

Storage limits64TBTransactionsYesLocking granularityRow
MVCCYesGeospatial data type supportYesGeospatial indexing support Yes
B-tree indexesYesT-tree indexesNoHash indexesNo
Full-text search indexesYesClustered indexesYesData cachesYes
Index cachesYesCompressed dataYesEncrypted dataYes
Cluster database supportNoReplication supportYesForeign key supportYes
Backup / point-in-time recoveryYesQuery cache supportYesUpdate statistics for data dictionaryYes

MyISAM Storage Engine

Storage limits256TBTransactionsNoLocking granularityTable
MVCCNoGeospatial data type supportYesGeospatial indexing supportYes
B-tree indexesYesT-tree indexesNoHash indexesNo
Full-text search indexesYesClustered indexesNoData cachesNo
Index cachesYesCompressed dataYesEncrypted dataYes
Cluster database supportNoReplication supportYesForeign key supportNo
Backup / point-in-time recoveryYesQuery cache supportYesUpdate statistics for data dictionaryYes

Archive Storage Engine

Storage limitsNoneTransactionsNoLocking granularityRow
MVCCNoGeospatial data type supportYesGeospatial indexing supportNo
B-tree indexesNoT-tree indexesNoHash indexesNo
Full-text search indexesNoClustered indexesNoData cachesNo
Index cachesNoCompressed dataYesEncrypted dataYes
Cluster database supportNoReplication supportYesForeign key supportNo
Backup / point-in-time recoveryYesQuery cache supportYesUpdate statistics for data dictionaryYes



2017년 7월 5일 수요일

[Javascript] 생성자의 이해

시작에 앞서 잡설..



 과거 웹개발을 공부할때나, 아니면 커리어 초기에 개발업무를 진행 할때 '자바스크립트' 라는 언어는 그저 'onclick' 따위의 이벤트를 핸들링 하는 것과 Ajax를 이용한 비동기 요청을 구현하는 용도로 밖에 사용하지 않았다. 좀 더 활용했다라고 한다면 'JQuery UI' 를 이용해 캘린더 붙이는 정도...?

 하지만 새로운 회사로 이직하여 시작한 프로젝트는 npm 환경을 기반으로 하는 그야말로 트렌디한 온갖 자바스크립트 도구, 라이브러리, 프레임워크의 향연이었다. 또한 ECMAScript6 이후부터의 문법적 발전을 통해서, 객체의 해체할당, let과 const의 block scope, class 선언 방식 등장, 달라진 모듈 import와 export 문법 등등... 여러가지 새로운 개념을 익혀야 했다.

이러한 변화에 흐름에 당당히 맞서서(!?) 나는 요즈음 나비책, 부엉이책, 이름을알수없는새책 등을 탐독하고 있다 (코뿔소책은 그 두께에 엄두가 안나서 pass.. 그래도 필요한 부분은 틈틈히 보고 있다..)
모두 적어도 몇년전에 출판된, 비교적 과거의 문법과 이론을 담고 있는 책들이다.

그렇다고 변화에 흐름을 미련하게 역행 하겠다는 건 절대 아니다. 다만 우리가 역사를 연구 함으로 미래에 대한 통찰력을 얻는 것처럼, 자바스크립트의 과거 이론을 통해서 현재의 변화를 더 잘 이해하고 싶은 것 뿐이다.

생성자에 관해 정리하려다가 잡설이 길어졌다. 정리에 앞서, 자바스크립트 생성자에 대한 더 깊은 이해를 할 수 있도록 도와주신 실력자 'javarouka'님께 감사 드린다.


생성자 문법 정리



자바스크립트의 생성자는 다름아닌 '함수' 다. 자바의 경우 생성자는 일반 메소드와 시그니쳐가 비슷하지만, 자바의 생성자는 메소드처럼 호출 할 수 없다. 반드시 'new' 키워드와 함께 사용해야 한다.

하지만 자바스크립트의 생성자 함수는 'new'와 함께 사용할 수도, 그냥 일반적인 함수처럼 사용할 수도 있다. 아래의 예제 처럼 말이다.

function MyConstructor(prop) {
    this.prop = prop;
}

var obj1 = MyConstructor("prop value");
var obj2 = new MyConstructor("prop value");

하지만 obj1과 obj2 할당된 값은 아래와 같은 차이가 있다.

console.log(obj1) // undefined
console.log(obj2) // MyConstructor
console.log(obj2.prop) // "prop value"

obj1은 값이 정의되지 않은 'undefined' 값이고, obj2는 정상적으로 MyConstructor의 객체와 prop 속성값인 "prop value" 문자열이 할당 된 것을 볼 수 있다.

'new' 키워드와 함께 사용된 생성자 함수는 묵시적으로 객체를 생성하여 'this' 에 바인딩한다. 그리고 동적으로 속성을 정의한 뒤에 역시나 묵시적으로 'this' 를 반환한다. 따라서 obj2에는 정상적인 객체가 생성되어 메모리에 할당 되는 것이다.

하지만 생성자를 'new' 키워드와 사용하지 않으면 일반 함수 처럼 호출이 되고, 이때, 'this'는 브라우져의 자바스크립트 실행 환경에서 '머리객체(Head Object)'인 'window' 이다. 따라서  MyConstructor 생성자를 일반 함수로서 호출 하게 되면 머리객체인 'window'에 동적으로 'prop'속성이 생성되며, 아무것도 반환하지 않으므로 obj1는 'undefined'가 된다.

'window' 객체에 'prop' 속성이 생성되었는지 확인해보자.




.
.
.









 'window' 객체의 속성을 나열했더니 추가된 prop 이 보인다

이러한 생성자 함수를 'new'키워드와 사용하지 않을때 주의 사항은, 바로 위의 예제에서 'window' 머리객체에 'prop'이 추가 된 것처럼 절대 전역 유효범위를(global scope)를 어지럽혀선 안된다는 것이다. 이유는 자명하다. 전역 머리객체에 개발자가 의도하지 않은 속성이 추가 될 수 있고, 이는 찾아내기 어려운 버그 발생으로 이어지기 때문이다.


한편, 생성자 함수를 'new' 키워드와 함께 사용한다고 무조건 해당 생성자의 객체를 반환할까?
그것도 아닌것 같다.

function ReturnByObj() {
    this.key = "hello";
    return new Object();
}

function ReturnByString() {
    this.key = "hello";
    return "ret value";
}

const returnByObj = new ReturnByObj();
const returnByString = new ReturnByString();

console.log(returnByObj); // Object 객체
console.log(returnByObj.key); // undefined
console.log(returnByString); // ReturnByString 객체
console.log(returnByString.key); // "hello"

생성자 함수(처럼 보이는) ReturnByObj와 ReturnByString 에 'return' 문을 추가했다. 이 두 함수를 'new' 키워드와 함께 사용한 코드이다.

위의 console 문 주석을 보면 알 수 있듯이 'new' 키워드와 함께 사용해도, return 문의 값이 '객체' 라면,
해당 객체가 반환된다. 'ReturnByObj' 객체가 반환 되지 않는다는 말이다. 하지만 'ReturnByString' 생성자 함수 처럼 원시값(primitive type)을 리턴한다면, 우리가 기대했던대로 해당 생성자내부에서 생성된 'this' 객체('ReturnByString' 객체)가 반환된다.

중요한 사실은, 이러한 문법을 이해해야 생성자 함수를 일반 함수처럼 호출하거나, 일반 함수를 'new' 키워드와 같이 호출하는 등의 오용과 실수를 줄일 수 있다는 사실이다.



2017년 7월 3일 월요일

[Javascript] Closure의 변수 공유 문제

 무언가 난해한 개념으로 이해되는 'lexical scope'(어휘적 유효범위)는, 그냥 '유효범위' 라고 생각해도 상관이 없을 듯 싶다. (이하 유효범위라 하겠다) 자바스크립트는 함수를 실행할때, 해당 함수가 정의되었을 때의 유효범위를 사용하여 실행 한다. 아래의 코드를 보자.

function funcCreator() {
  const x = 10;
  function lexicalScopeFunc() {
    console.log(x);
  };
  return lexicalScopeFunc;
}

funcCreator()();

위의 코드는 'funcCreator' 함수를 실행하여 'lexicalScopeFunc' 가 반환되게 하고, 반환된 함수를 즉각 호출한다.  해당 함수를 호출 하면 console 창에 참조 'x' 의 값인 '10'이 출력된다. 

정작 'lexicalScopeFunc' 함수'만' 본다면, 참조 'x'는 해당 함수의 scope 밖에서 정의 되었다. 그런데 어떻게 console창에 참조 'x'의 값이 정상적으로 출력이 되는 것일까? 바로 해당 lexicalScopeFunc 함수가 실행될때 해당 함수가 정의 되었던 유효범위, 즉 'funcCreator' 함수의 scope 를 이용하여 실행하기 때문이다. 다시말해서, lexicalScopeFunc 함수는 자신이 선언된 funcCreator 함수 내부의 유효범위와 연결된 채 존재하는 함수인 것이다.

바로 이러한 개념을 '클로져(Closure)' 라고 한다.

이러한 클로져 개념에는 아주 고전적으로 논의되는 문제가 있는데, 바로 '변수 공유' 문제이다. 아래의 코드를 보면 문제가 무엇인지 명확해 진다.

var closureList = [];
for(var i = 0; i < 10; i++) {
  closureList.push(() => {
    console.log(i);
  });
}

for(var idx in closureList) {
    closureList[idx]();
}

closureList 배열안에 삽입한 요소는 각각 클로져 함수인데, 자신이 선언된 유효범위의 'i' 값을 출력한다. 때문에 위의 코드를 실행 했을때 우리는 console창에 0부터 9까지 출력되는 것을 기대할 것이다.

그러나 정작 코드를 실행해 보면 '10' 이 10번 출력된다. 이유를 살펴보자면, closureList 안에 삽입된 각각의 클로져 함수가 해당 유효범위(여기서는 전역범위가 될것이다)의 'i' 변수를 공유하기 때문이다. for문이 종료될때 변수 'i' 는 값이 10이 되고, 각각 클로져는 최종 값이 10인 변수 'i'를 출력하기때문에 그러한 결과가 나오는 것이다.

이를 해결하려면 어떻게 해야할까?

우선은 IIFE(Immediately Invoked Function Expressions)를 사용하여 유효범위를 강제한다. 
아래와 같은 코드이다.

var closureList = [];
for(var i = 0; i < 10; i++) {
  closureList.push(
    (function() {
      var localVar = i;
      return function() {
        console.log(localVar);
      };
    })()
  );
}

for(var i = 0; i < 10; i++) {
    closureList[i]();
}

closureList에 함수 객체를 삽입할때, IIFE를 통해 독립적인 유효범위를 생성하여 그안에서 함수를 반환하여 배열에 삽입한다. 이렇게 되면 IIFE의 유효범위 안에서 'i'는 'localVar' 라는 변수로 바인딩되고, 독립적인 유효범위 안의 변수가 된다.

그러나... 위 코드가 단순한 예제이기에 망정이지, 조금이라도 복잡한 비즈니스 코드로 작성한다면 IIFE는 읽기에 무척 짜증이 날 것 같다.

아주 단순한 해결 방법이 있는데, ECMAScript6 에서 제공하는 'let' 을 이용하여 변수를 선언하는 것이다.
아래의 코드를 다시 보자.

var closureList = [];
for(let i = 0; i < 10; i++) {
  closureList.push(function() {
    console.log(i);
  });
}

for(var idx in closureList) {
    closureList[idx]();
}

차이가 보이는가? closureList 배열에 함수를 삽입하는 for 구문안에서 인덱스를 'let'으로 선언했다. 다들 알겠지만 'var' 선언 방식의 scope는 함수의 유효범위를 사용한다. 하지만 'let'은 대부분의 프로그래밍 언어가 그렇듯이 '블록'의 유효범위이다. 때문에 반복되는 for 구문의 블록이 해당 let 변수의 독립적인 유효범위로 작용하기 때문에 우리가 원하는 0, 1, 2, .. 9 가 출력이 되는 것이다.


2017년 5월 10일 수요일

[Elasticsearch] AWS + Docker 기반 Elasticsearch 클러스터 구성

회사 업무 관계로, AWS + Docker 기반의 Elasticsearch 클러스터를 구성하게 되었다. 그냥 지나가면 까먹을 것 같아 정리 한다.

00. Prerequisite



docker : Docker version 1.13.0
docker-compose : docker-compose version 1.10.0
AWS instance Type : r4.large
Elasticsearch : 5.4.0

먼저 작업 디렉토리를 ~/elasticsearch 라고 가정하자.


01. Dockerfile 작성



아래와 같이 Dockerfile 을 작성한다.

FROM docker.elastic.co/elasticsearch/elasticsearch:5.4.0
 
WORKDIR /usr/share/elasticsearch
RUN bin/elasticsearch-plugin install discovery-ec2

베이스 이미지는 'docker.elastic.co/elasticsearch/elasticsearch:5.4.0' 이다.

AWS EC2 인스턴스를 사용해 클러스터를 구성해야 하기 때문에 기존의 zen discovery 는 사용할 수 없다. 따라서 docker 이미지를 구울때 'discovery-ec2' 플러그인을 설치해야 한다.


02. docker build



Dockerfile이 위치하고 있는 디렉토리에서, 아래와 같이 'elasticsearch-aws' 라는 이름으로
docker 이미지를 생성한다.

$ docker build -t elasticsearch-aws .

> build log
Sending build context to Docker daemon 4.096 kB
Step 1/4 : FROM docker.elastic.co/elasticsearch/elasticsearch:5.4.0
 ---> 4c093701c312
Step 2/4 : WORKDIR /usr/share/elasticsearch
 ---> c784b97dc1e0
Removing intermediate container a93494f1f6f3
Step 3/4 : RUN bin/elasticsearch-plugin install discovery-ec2
 ---> Running in a609bd73f8c7
-> Downloading discovery-ec2 from elastic
[=================================================] 100%??
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@     WARNING: plugin requires additional permissions     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* java.lang.RuntimePermission accessDeclaredMembers
* java.lang.RuntimePermission getClassLoader
See http://docs.oracle.com/javase/8/docs/technotes/guides/security
/permissions.html
for descriptions of what these permissions allow and 
the associated risks.
-> Installed discovery-ec2
 ---> 6108494a5131
Removing intermediate container a609bd73f8c7
Step 4/4 : EXPOSE 9200 9300
 ---> Running in 78105875f3c7
 ---> e6fd47d7d666
Removing intermediate container 78105875f3c7
Successfully built e6fd47d7d666

위의 Warning은 일단 무시하자.


03. elasticsearch.yml 작성



작업 디렉토리에 'config' 디렉토리를 생성하고,
elasticsearch YAML 설정파일을 작성하여 위치 시키자.

cluster.name: my-aws-cluster
 
node.name: my-node2
 
node.master: true
node.data: true
 
network.host: 0.0.0.0
network.publish_host: _ec2:privateIp_
 
discovery.type: ec2
 
discovery.zen.minimum_master_nodes: 1
discovery.zen.ping.unicast.hosts:
 - [this host ip]
 - [host ip 1]
 - [host ip 2]
 
xpack.security.enabled: false

위와 같이 'network.publish_host' 설정을 하게되면 ec2의 private ip 로 호스트가 바인딩 된다. 'discovery.type: ec2' 설정도 빠지면 안된다.


04. docker-compose.yml 작성



그냥 docker run 명령어로 필요한 옵션을 주어 동작을 하게 할 순 있지만, 옵션을 부여하는게 불편하고, 스크립트로 작성해도 유지보수가 귀찮다.

따라서 YAML 형식의 설정을 지원하는 docker-compose로 컨테이너를 구동하려고 한다.
아래와 같이 작업 디렉토리에 docker-compose.yml 파일을 작성한다.

version: '2'
services:
 elasticsearch:
  image: elasticsearch-aws
  container_name: elasticsearch
  volumes:
   - ./config/elasticsearch.yml:/usr/share/elasticsearch/config
/elasticsearch.yml
   - esdata:/usr/share/elasticsearch/data
  ports:
   - 9200:9200
   - 9300:9300
  environment:
   ES_JAVA_OPTS: "-Xmx2048m -Xms2048m"
 
volumes:
 esdata:
  driver: local


위에서 중요한 설정은 'volumes' 인데, 일단 해당 설정의 첫번째 라인은, 호스트 서버의 외부 elasticsearch.yml 파일을 컨테이너 내부의 elasticsearch.yml 파일에 덮어 쓰는 것이다.
그리고 두번째 라인은 'named volume' 으로서, 컨테이너 내부 디렉토리에 컨테이너 외부 디렉토리를 마운트시키는데, docker자체에서 'esdata' 라는 볼륨으로 관리된다.

해당 볼륨은 service의 이름과 합쳐져서 'elasticsearch_esdata' 라는 볼륨으로 생성되며,
아래와 같은 명령어로 확인 할 수 있다.

$ docker volume inspect elasticsearch_esdata
[
    {
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/elasticsearch_esdata/_data",
        "Name": "elasticsearch_esdata",
        "Options": {},
        "Scope": "local"
    }
]

위에 'Mountpoint' 값이 마운트된 컨테이너 외부 호스트 디렉토리를 가르킨다.


05. 구동



아래의 명령어로 구동시킨다. 백그라운드 구동일 경우 '-d' 옵션을 붙인다.

$ docker-compose up


06. 클러스터링



위의 01~04 단계를 거친 동일한 docker 기반 elasticsearch 설정을 가지고,
2대의 AWS 인스턴스에 elasticsearch 노드를 띄워서 클러스터링을 해 보려고 한다.
아래를 순서대로 진행한다.

① 1번 AWS 인스턴스에 'node1' elasticsearch 노드 구동
② 2번 AWS 인스턴스에 'node2' elasticsearch 노드 구동

① ② 를 차례대로 진행하면 node1 쪽 로그에 아래와 같이 출력된다.






로그를 보면 알겠지만 뒤늦게 구동된 node2 가 마스터노드인 node1을 발견했다는 내용을 확인 할 수 있다.

그리고 동시에 node2 쪽 로그에는 아래와 같이 출력된다.





반대로 마스터노드인 node1에서는 node2를 ec2 discovery를 이용해 찾고, 클러스터에 추가했다는 내용을 확인 할 수 있다.

이로서 클러스터 구축 완료!

2017년 5월 4일 목요일

[Elasticsearch] 5.x 버전에서 'head' 플러그인, 'bigdesk' 플러그인 적용 방법

개요



 정말 오랫만에 엘라스틱서치에 관한 포스팅을 하려고한다. 필자가 한창 다루던 시절의 엘라스틱서치의 버전은 1.x 대 중반 이었다. 하지만 그동안 엘라스틱 서치는 끊임없이 발전하여 2.x을 넘어 5.x버전의 엘라스틱서치가 릴리즈 되어 버렸다.

 그동안 어떤 스펙들이 추가되었고, 삭제되었는지는 차차 알아가야 하겠지만, 이제와서 엘라스틱서치를 설치하고 돌려보려니, 예전엔 아주 유용하게 쓰였던 'head' 와 'bigdesk' 플러그인을 이제는 'plug in' 하여 쓰지 못한다는 사실을 알게 되었다. 이유는 아래 문서에 잘 설명이 되어 있다.

https://www.elastic.co/kr/blog/running-site-plugins-with-elasticsearch-5-0

 요약하자면, 이제까지의 플러그인들은 말그대로 엘라스틱서치 클러스터 서버에 플러그 '인' 되어서 마치 웹서비스처럼 제공 되었다. 하지만 이것은 보안 취약점을 낳았고, 때문에 플러그인에 대한 지원이 되지 않는다(라고 이해했음 ㄷㄷ) 라는 것이었다. 대신에 기존의 플로그인을 대체할 'X-Pack' 이라는 서비스를 제공하는데, 아주 기본적인 Basic package를 제외하고는 유료인것 같다... ㄷㄷ

 하지만 방법은 있다. 각 플러그인을 'standalone' 으로 구동시키고, Elasticsearch 클러스터에 붙이는 것이다. 하나씩 알아보자.


Head 사용



비교적 간단하다.

1) git clone 으로 소스 내려받기

$ git clone git://github.com/mobz/elasticsearch-head.git

2) npm install

해당 소스의 루트 디렉토리에서, npm install 을 통해 의존성을 설치한다

$ npm install

3) npm 을 이용해 standalone server 를 구동

$ npm run start

> elasticsearch-head@0.0.0 start /Users/rover/Devspace
/elastic-practice/head
> grunt server

Running "connect:server" (connect) task
Waiting forever...
Started connect web server on http://localhost:9100

4) localhost:9100 으로 접속하여 확인
























Bigdesk 사용




일단 http://bigdesk.org/ 이곳에서 zip이든, tar든, 다운 받고 압축을 해제 한다.
압축을 해제 하면 아래와 같은 파일들이 보일 것이다.

$ tree bigdesk -L 1
bigdesk
├── LICENSE
├── NOTICE
├── README.md
├── bigdesk-1.0.0.jpg
├── bigdesk-2.0.0-SNAPSHOT.jpg
├── css
├── images
├── index.html
└── js


여기서 그냥 index.html 을 브라우져로 연다. 그리고 ES node REST endpoint 의 텍스트 필드에 'localhost:9200' 으로 입력하고 'connection' 을 클릭한다.

어찌보면 매우 간단한 standalone 방법인데,
위와 같이 하면 아래와 같은 alert 창을 볼 것이다.












어찌된 일인지 버전을 막아 놓았다.
그래서... 다른 방법이 없어 자바스크립트 소스를 조금 손대 보았다.

자바스크립트 소스를 뒤져보면, js/store/BigdeskStore.js 파일에 위의 버전을 체크하는 로직이 숨어있다. 아래와 같다

version = version.number;
var _vArray = version.split(".");
if (_vArray.length > 2 && _model.checkVersion(_vArray)) {
    _model.versionVerified(version);
    _model.initCluster(connection);
} else {
    _model.yellAboutVersion(version);
}

'_model.checkVersion' 함수에서 버전을 체크 한 후, false 일 경우 _model.yellAboutVersion 함수를 호출한다.

yellAboutVersion: function(version) {
    var message =
        "*********************************\n" +
        "Bigdesk may not work correctly!\n" +
        "Found ES node version: " + version + "\n" +
        "Requires ES node version: >= 1.0.0.RC1\n" +
        "*********************************";
    console.log(message);
    if (alert) { alert(message); }
}

따라서.... 저 버전을 체크하는 if else 문을 제거하고, 바로 _model.initCluster 함수를 호출하게 했다. 그렇게 되면 connection에서 가져온 클러스터 정보를 model에 세팅하게 되고, 아래처럼 정상적으로 클러스터 현황이 모니터링 된다.




















결론



불과 몇년전에 비해 엘라스틱서치는 많은 기능이 추가/제거 되었고 개선되었다.
그러한 흐름속에서 built-in 방식으로 사용되었던 플러그인들이 어떠한 이슈(위에서 설명한 것처럼 보안 이슈 등등)로 인해 없어지는건 자연 스러울 지도 모르겠다.

어찌됬든, 사라진 플러그인을 실무에 정식으로 사용하는건 무리가 있겠으나, 뭐 학습용으로는 괜찮지 싶다.

그리고 'X-pack' 이라는것이 일단은 유료니까..




2017년 3월 27일 월요일

[OpenLDAP] 설치와 맛보기 (Mac OS)

1. 시작..!



 LDAP이란... Lightweight Directory Access Protocol의 약자로, TCP/IP 위에서 디렉토리 서비스를 조회하고 수정하는 응용 프로토콜이다 (출처: 위키 백과)

라고 하는데.. 디렉토리 서비스라고 하니까 Zookeeper와 비슷한것 같기도 하고.. 감이 잘 오지 않는다. 마치 콜라병을 들고 어디에 쓰는 물건인고 알쏭달쏭 물음표 만개한 원시인이 된 것 같은 느낌이다.

일단 LDAP이 Zookeeper 와 비슷한 아이디어를 가지고 있다는 가정 하에, 설치해보고 실행해 보았다.


2. 설치



Mac의 OSX는 기본적으로 유닉스 기반의 운영체제이고, 그에따라 우리가 자주 사용하는 리눅스 커맨드(레드헷, 데비안을 통틀어)와 거의 동일한 shell 커맨드를 제공한다.(맥도 bash shell이었나..? 아무튼..)

하지만 레드헷 계열의 소프트웨어 패키지 관리자인 'yum' 과 같은 기능을 제공하지 않기 때문에, 'brew'를 이용하여 설치 해본다. (brew는 여기 참고)

$ brew install openldap

brew install이 끝나면 아래 경로에 설치된다

$ ls -al /usr/local/Cellar/openldap/2.4.43/
total 208
drwxr-xr-x  13 rover  admin    442  3 27 14:38 .
drwxr-xr-x   3 rover  admin    102  3 27 14:38 ..
-rw-r--r--   1 rover  admin  86459 12  1  2015 CHANGES
-rw-r--r--   1 rover  admin   2345 12  1  2015 COPYRIGHT
-rw-r--r--   1 rover  admin    366  3 27 14:38 INSTALL_RECEIPT.json
-rw-r--r--   1 rover  admin   2214 12  1  2015 LICENSE
-rw-r--r--   1 rover  admin   3494 12  1  2015 README
drwxr-xr-x  12 rover  admin    408 12  1  2015 bin
drwxr-xr-x  11 rover  admin    374 12  1  2015 include
drwxr-xr-x  14 rover  admin    476 12  1  2015 lib
drwxr-xr-x   3 rover  admin    102 12  1  2015 libexec
drwxr-xr-x  11 rover  admin    374 12  1  2015 sbin
drwxr-xr-x   3 rover  admin    102 12  1  2015 share


3. 설정



openldap 패키지에 포함되어 있는 'slapd' 라는 데몬을 이용해서 ldap 서버를 기동 하려고 한다.
참고로 slapd은 'Stand alone LDAP Daemon' 의 약자이다.

1) slapd.conf 설정

$ cd /etc/openldap/

위 경로에서 먼저 default값이 정의되어있는 샘플 설정파일의 이름을 바꾼다

$ cp slapd.conf.default slapd.conf

vi 에디터로 설정파일을 열어 보자

$ sudo vi /etc/openldap/slapd.conf

주요 설정은 아래와 같다.

 53 database        bdb
 54 suffix          "dc=asuraiv,dc=com"
 55 rootdn          "cn=Manager,dc=asuraiv,dc=com"
 .
 .
 .
 59 rootpw  secret
 .
 .
 63 directory       /private/var/db/openldap/openldap-data
 64 # Indices to maintain
 65 index   objectClass     eq

다른것들은 어느정도 직관적으로 이해 할 수 있지만, 'dc', 'cn' 과같은 용어들은 좀 낯설다.
이들은 일종의 프로토콜(규약)로서, 'X.500' 이라고 한다.

대략적인 의미는 아래와 같다. 한마디로 말하면 Directory Service에서 하나의 노드를 구성하는
개념이라고 볼 수 있다.

String  X.500 AttributeType
------------------------------
CN      commonName
L       localityName
ST      stateOrProvinceName
O       organizationName
OU      organizationalUnitName
C       countryName
STREET  streetAddress
DC      domainComponent
UID     userid

자세한 용도는 다음에 논의하기로 하자..

rootpw는 기본값인 'secret'으로 되어 있는데, ldap utility중 하나인 'slappassword' 를 사용하여
원하는 비밀번호를 SHA로 인코딩한 후, 설정 할 수 있다

$ slappasswd 
New password: 
Re-enter new password: 
{SSHA}/6go9FN2KA4jS+yBj5EQsCkvXydSj6mZ

 53 database        bdb
 54 suffix          "dc=asuraiv,dc=com"
 55 rootdn          "cn=Manager,dc=asuraiv,dc=com"
 .
 .
 .
 59 rootpw  {SSHA}/6go9FN2KA4jS+yBj5EQsCkvXydSj6mZ
 .
 .
 63 directory       /private/var/db/openldap/openldap-data
 64 # Indices to maintain
 65 index   objectClass     eq


2) ldap.conf 설정

같은 디렉토리에 ldap.conf 파일도 설정하자.

  8 BASE    dc=asuraiv,dc=com
  9 BINDDN  cn=Manager,dc=asuraiv,dc=com
 10 URI     ldap://localhost
 11 
 12 SIZELIMIT       0
 13 TIMELIMIT       0
 14 #DEREF          never
 15 TLS_REQCERT     demand

ldap은 프로토콜이다. 따라서 'ldap://' 프리픽스를 URI설정에 반드시 해 주어야 한다.


4. 구동



$ pwd
/usr/libexec
$ sudo ./slapd -h ldap://localhost -d 50
58d8fc20 @(#) $OpenLDAP: slapd 2.4.28 (Oct  3 2015 17:40:09) $
    root@osx201.apple.com:/Library/Caches/com.apple.xbs/Binaries/Open
LDAP/OpenLDAP-510.30~3/Objects/servers/slapd
58d8fc20 daemon: SLAP_SOCK_INIT: dtblsize=256
58d8fc20 main: Enabling TLS failed; continuing with TLS disabled.
58d8fc20 bdb_monitor_db_open: monitoring disabled; configure monitor 
database to enable
58d8fc20 slapd starting
58d8fc20 daemon: posting com.apple.slapd.startup notification


일단 'slpad starting' 메시지를 확인하면 구동 성공.

이것저것 알아보고 만져보다가 나중에 Spring Security와 연동해볼 생각이다. 하지만 다음 시간에....ㄷㄷ