top bar

글 목록

2017년 12월 14일 목요일

[Spring] Request Bean Scope

스프링에서의 Bean이 기본적으로 'Singleton'이라는 것은 누구나 안다. 하지만 Singleton으로 관리되는 Bean의 Lifecycle을 'Scope'어노테이션을 이용해 제어 할 수 있다. 스프링의 Bean Scope는 총 5가지 인데, singleton, prototype, request, session, global session이 그것이다.

이 중에서, 조금은 특이한 방법으로 관리되는 request scope의 bean에 대해서 살펴 보고자 한다.

가령, 아래와 같은 HelloMessageGenerator 라는 클래스가 있다고 하자.

class HelloMessageGenerator {
    public String getMessage() {
        return "Hello " + this;
    }
}

HelloMessageGenerator의 getMessage메소드를 통해서 문자열을 가져오는데, 자바에서 유일하게 오버로딩 된 '+' 연산자를 통해서 'Hello [참조값]' 이라는 문자열을 반환한다. 이를 통해 getMessage를 호출하는데 사용되는 인스턴스의 참조값을 알 수 있다.

그리고 아래처럼 Spring Configuration에, 해당 클래스의 Bean 설정 코드를 작성한다.

@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator requestMessage() {
    return new HelloMessageGenerator();
}

HelloMessageGenerator의 빈 설정을 보면 Scope어노테이션에 'proxyMode' 라는 것이 눈에 띈다. Aop를 적용하려는 것도 아닌데 왜 proxyMode를 설정해야 할까?

'request scope' 라는 것은 말그대로 Bean의 lifecycle이 request와 같다는 것이다. 때문에 해당 Bean은 '요청 -> 응답'이 끝나면 제거 되며, 요청이 없으면 생성되지 않는다. reuqest scope의 이러한 속성 때문에 proxy를 설정해야 하는데, 이유는 이렇다. 

일단 아래처럼 Controller를 작성해 보자.

@Controller
public class ScopesController {
    @Resource(name = "requestMessage")
    HelloMessageGenerator requestMessage;
 
    @RequestMapping("/scopes")
    public String getScopes(Model model) {
        requestMessage.setMessage("Good morning!");
        model.addAttribute("requestMessage", requestMessage.getMessage());
        return "scopesExample";
    }
}

서블릿 컨테이너에 스프링 기반의 웹 어플리케이션이 로딩될때 (쉽게 말해 톰캣을 구동할때) 각 빈이 생성되고 빈 컨테이너에 등록되며 'Dependency Injection'이 일어난다. 한마디로 이 시점에 각 객체의 의존관계를 바탕으로 빈이 주입 된다는 것이다. 위 경우는 ScopesController에 HelloMessageGenerator의 인스턴스가 주입되어야 한다. 하지만 해당 빈은 request scope이기때문에 생성할 수 없다. 때문에 'proxy'를 임시적으로 생성하여 의존성 주입을 수행하는 것이다. 그 후에 진짜 요청이 들어오면 다시 초기화를 하여 빈을 사용하게 된다.

proxy는 일단 그렇고, 결과적으로 위 코드를 수행하면 view단에 찍히는 'requestMessage' 문자열이 요청 때마다 다른 값으로 노출 될 것이다. 참고로 session, global session scope 설정 시에도 proxyMode를 지정해 줘야 하는데, 원리는 request scope의 경우와 같다.

정리 끗.

참고 - http://www.baeldung.com/spring-bean-scopes

2017년 11월 11일 토요일

[JAVA] 로 풀어본 '식사하는 철학자들' 문제 (2) - 세마포어

세마포어 (Semaphore)



 '세마포어' 란, 1965년 E.W 다익스트라가 고안한 개념으로서, down과, up 이라는 두개의 함수로 조작하는 정수변수이다. 다시 말하면 Mutual Exclusion(Mutex, 뮤택스)를 조금 더 고도화 하여 구현한 개념이라고 할 수 있는데, 임계영역에 대한 lock을 수행한다. 참고로 세마포어는 뮤택스이지만, 뮤택스라고 해서 모두 다 세마포어는 아니다. (필자가 이해 하기론..)

 어떤이는 세마포어를 열쇠고리에 걸려있는 열쇠의 갯수라고 표현 하더라. 흠.. 그것도 적절한 표현 인 것 같다. 세마포어에 대한 down 연산을 할때는 세마포어 값이 0보다 큰지 검사한다. 만약 그렇다면 이 값을 감소시키고, 스레드는 임계영역에 대한 수행을 계속한다. 만약 이 값이 0이면 스레드는 down의 수행을 완료하지 않고 즉시 잠들게 된다. 값을 검사하고, 변경하고, 경우에 따라 잠드는 이러한 모든 동작은 분할할 수 없는 하나의 원자적 행위(atomic action) 이다. 만약 이 연산에 원자성이 보장 되지 않는다면, 세마포어를 적용한다 하더라도 동시성의 경쟁조건(race condition)에 의한 교착상태가 해결된다는 보장을 할 수 없을 것이다.

 JAVA에도 세마포어 구현체가 있다. 동시성의 영웅, 더그 리(Doug Lee) 형님께서 구현하신 'java.util.concurrent.Semaphore' 클래스가 그것이다. 이 클래스는 down 연산에 'acquire' 메소드를, up 연산에는 'release' 메소드를 사용한다. 의미적으로는 이러한 네이밍이 좀 더 직관적인 것 같다.

 위 클래스의 인스턴스를 생성할때, 세마포어의 'permits', 그러니까 열쇠의 갯수를 생성자의 파라메터로 받는다. 그리고 앞서 말한 acquire 와 release 메소드를 통해서 해당 값을 변경한다. 필자가 예상하기로는 값 변경 작업에 'AtomicInteger' 와 같은 연산의 원자성을 보장하는 방식을 사용 할 것 같았는데, 소스를 살펴보니 아래와 같은 'compareAndSetState' 라는 메소드를 사용 한 것이 눈에 띄었다.

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

 간단하게 말하면 'expect' 값을 'update' 값으로 swap 하는 작업이다. 주석을 살펴보니, 정확히는 모르겠지만 AtomicInteger와 같은 명확한 방식을 사용하지 못했던 히스토리가 있었던 것 같다. 그래서 'the lesser of evils'(차악.. 이라고 읽으면 되려나?)로 native 라이브러리를 사용해서 구현 했다고 한다. 'Unsafe'라는 객체는 java doc 설명도 제대로 안나와 있어 자세히 살펴보진 않았지만 어쨌든 native 라이브러리 이고, 뭐 값을 조작하는 역할을 하는 것 같다.


세마포어의 적용



 서두가 좀 길었다. 그리고 본 단락은 짧을 것이다 -_-; 앞서 살펴본 '식사하는 철학자들' 문제에 세마포어를 적용해보자. 적용 방식은 아주 간단하다. 생각하고(think), 왼쪽 오른쪽 포크를 잡고(take), 먹고(eating), 양 포크를 다시 테이블위에 놓는(put) 행위중에서 'think' 의 바로 다음 5가지 동작을 이진(binary) 세마포어로 묶는 것이다.

먼저 static 변수로 세마포어를 정의한다.

class Philosopher implements Runnable {
    
    .
    .
    public static final Semaphore semaphore = new Semaphore(1);
    .
    .

'이진' 세마포어 이므로 'permits'의 값을 1로 설정한다. (1과 0의 값만 갖도록)

그리고 아래와 같이 철학자들의 행위를 세마포어로 묶어보자.

@Override
public void run() {
    while (true) {
        think();
        try {
            semaphore.acquire();

            takeFork(this.number);
            takeFork((this.number + 1) % 5);
            eat();
            putFork(this.number);
            putFork((this.number + 1) % 5);

            semaphore.release();
        } catch (InterruptedException e) {
            System.out.println("Exception occured!");
            e.printStackTrace();
        }
    }
}

포크를 잡는 행위와 스파게티를 먹는 행위, 그리고 포크를 다시 놓는 행위는 임계영역(critical region)이라고 할 수 있다. 이러한 임계영역을 이진 세마포어로 보호함 으로서 '상호배제' 매커니즘을 적용하면 교착 상태가 발생하지 않는다. 하지만 프로그램을 실행하고 보니 이상한 점이 보였다.























흠.. 그렇다. 뭔가 5명의 철학자가 골고루 스파게티를 먹는 것 같지 않다. 그리고 이론상, 5명의 철학자와 5개의 포크가 있다면 동시에 최대 2명의 철학자가 스파게티를 먹을 수 있어야 한다. 한마디로 지금의 상황은 CPU가 그만큼 효율적으로 스케줄링을 하고 있지 않다는 것이다. 따라서 세마포어에 열쇠의 갯수(permits)를 1에서 2로 늘려 보았다. 이제 더 이상 이진 세마포어는 아니다.

public static final Semaphore semaphore = new Semaphore(2);

그리고 다시 프로그램을 수행했다.























이제 조금 더 고른 스케줄링이 되는 것으로 보인다. 주의해야 할 점은 세마포어의 'permits' 값을 5 이상으로 한다면, 임계영역이 보호되는 것은 말짱 도루묵이라는 것이다. 이는 세마포어를 적용하지 않은 것과 같은 상황이다.

일단 세마포어는 여기까지..



2017년 10월 29일 일요일

[JAVA] 로 풀어본 '식사하는 철학자들' 문제 (1) - 시작

(명시적 'lock'을 사용하는 것으로 예제 전면 수정...)

 요즘 '동시성'에 대해 관심을 가지고 짬을 내서 공부를 하다보니, 학부시절 운영체제를 수강할때 배웠던 '식사하는 철학자들' 문제가 불현듯 떠올랐다. 그래서 이번에는 이 문제를 JAVA로 어떻게 구현하고 해결 하는지 포스팅해 보려고 한다.

알 만한 사람들은 다 아는 아주 고전적이고 유명한 문제라, 개념 자체에 대해서는 간단하게만 정리하고 넘어 가겠다.

다섯명의 철학자가 원형테이블 주위에 앉아있다. 스파게티 한 접시가 각 철학자에게 주어진다. 이를 먹기 위해서는 두개의 포크가 필요하다. 접시와 접시 사이에 하나의 포크가 있다. 아래는 이러한 배치를 가진 테이블의 모습이다.




이 문제의 룰을 간단히 요약해보자.

- 철학자의 삶은 식사시간과 생각하는 시간의 반복이다. (필자 주 : 이 정의에 의미를 두지 말자..)
- 철학자는 배가 고프면 왼쪽 포크, 오른쪽 포크를 한번에 하나씩 잡으려고 시도한다.
- 두개의 포크를 잡게되면 철학자는 잠시동안 먹고, 포크를 테이블위에 올려놓고, 그리고 다시 생각한다.

여기서 핵심적인 질문은 아래와 같다

"각 철학자가 해야할 일을 그대로 수행하면서
절대로 중단 되지 않는 프로그램으로 작성될 수 있는가?"

만약 5명의 철학자들이 서로 사이좋게 양보하며 자신의 왼쪽, 오른쪽에 있는 두개의 포크를 잡고 골고루 식사를 할 수 있다면 그들에겐 아주 행복한 저녁이 될 것이다. 하지만 이를 소프트웨어, 특별히 멀티 스레드 기반의 어플리케이션이라고 보았을 때 이것을 '영원히' 보장 할 수 있는가? 가령, 5명의 철학자가 동시에 왼쪽 또는 오른쪽 포크를 잡는다면 어떻게 될까? 이들은 식사를 하지 못한채 망부석 마냥 한 없이 다른 한 쪽의 포크가 테이블에 올려지길 기다릴 것이다. 사실 이 '식사하는 철학자들' 문제는 교착상태(deadlock)를 다루는 아주 대표적인 예제이다. (참고 - <'운영체제론' 3rd Edition, Andrew S Tanenbaum 저>)

이 예제를 JAVA 스레드기반의 어플리케이션으로 아래와 같이 구현해 보았다.

먼저 이 예제의 'Shared Resource' 라고 할 수 있는 'Fork' 클래스 이다.

class Fork {

    Lock lock = new ReentrantLock();

    public void useFork() {
        lock.lock();
    }

    public void unUseFork() {
        lock.unlock();
    }
}

class Tableware {

    public static final List<Fork> forks = new ArrayList<>();

    static {
        forks.add(new Fork());
        forks.add(new Fork());
        forks.add(new Fork());
        forks.add(new Fork());
        forks.add(new Fork());
    }
}

각 'Fork' 객체는 소유권을 가지지 않은 스레드의 접근을 막기 위해 명시적 'lock' 필드를 가지고 있다. useFork 메서드 수행시 lock을 획득하고, unUseFork 메서드 수행시 lock을 해제 한다. 인덱스 범위는 0 ~ 4 이다.

class Philosopher implements Runnable {

    private String name;
    private int number;

    public Philosopher(String name, int number) {
        this.name = name;
        this.number = number;
    }

    public void think() {
        print(name + " thinking ...");
    }

    public void eat() {
        print(name + " eating ... yum-yum-yum");
    }

    public void takeFork(int i) {

        print(name + " attemp to take (" + i + ") fork ...");

        Fork fork = Tableware.forks.get(i);
        fork.useFork();

        print(name + " take (" + i + ") fork now!");
    }

    public void putFork(int i) {

        print(name + " put (" + i + ") fork down ...");

        Fork fork = Tableware.forks.get(i);
        fork.unUseFork();
    }

    @Override
    public void run() {
        while (true) {
            think();
            takeFork(this.number);
            takeFork((this.number + 1) % 5);
            eat();
            putFork(this.number);
            putFork((this.number + 1) % 5);
        }
    }
}

'Runnable' 인터페이스를 구현한 'Philosopher'(철학자) 클래스이다. 포크를 잡기위한 'takeFork' 메서드와 포크를 테이블에 올려놓기 위한 'putFork' 메서드가 정의되어 있고, 정적 'forks' 리스트에 접근하기 위해 Tableware(식기) 클래스를 사용하고 있다.

'run' 메서드에서는 해당 행위 메서드들을 순차적으로 나열함으로서 철학자들이 해야 하는 일을 구현하였다. 서두에 설명한대로, 생각하고(think), 왼쪽 그리고 오른쪽 포크를 잡으며(takeFork), 먹고(eat), 다시 포크를 내려 놓는다(putFork). 왼쪽, 오른쪽 포크의 인덱스를 구하기 위해 'modulo' 연산을 사용한다.

아래는 위 구현사항을 실행하기 위한 'main' 메서드이다.

public class DiningPhilosophers {

    public static void main(String[] args) {

        Philosopher a = new Philosopher("A", 0);
        Philosopher b = new Philosopher("B", 1);
        Philosopher c = new Philosopher("C", 2);
        Philosopher d = new Philosopher("D", 3);
        Philosopher e = new Philosopher("E", 4);

        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(a);
        exec.execute(b);
        exec.execute(c);
        exec.execute(d);
        exec.execute(e);
    }
}

이제 이 프로그램을 실행해보면 어떤 결과가 나타날까? 일정 시간은 5명의 철학자들이 돌아가면서 식사를 잘 하는 것처럼 보이다가... 아래와 같은 현상에 봉착한다.





 A, B, C, D, E 모두가 다른 한쪽 포크를 잡기 위해 (lock을 획득하기 위해) 시도 하고 있다. 하지만 안타깝게도 자신의 다른 한쪽 포크는 이미 다른 철학자가 잡고 있는 상태이다. 때문에 모든 철학자들이 영원히 식사를 하지 못하게 되는 교착상태에 빠져 버린다.

 '동시성' 문제는 이처럼 '공유자원' 으로 인해 발생한다. 하지만 요즘 많이들 이야기 하고 있는 '함수형 프로그래밍' 에서는 공유 자원이라는 개념자체가 희박하여 (있어도 함수형 언어의 'Runtime' 레벨에서 알아서 해주는..?) 부수효과를 예방하는 '순수함수' 위주로 프로그램을 작성하기 때문에 이런 문제를 걱정할 필요가 없을 수도 있다.

 하지만 아무리 요즘의 언어들이 저수준의 문제를 Runtime에 추상화하는 방향으로 발전하면서 개발자들에게 '비즈니스 로직'에만 집중하게 한다 하더라도, 이러한 프로그램의 본질적인 문제들을 탐구하려고 노력해야 좀 더 엔지니어로서 깊은 내공을 쌓을 수 있다는 것이 개인적인 생각이다.

 흠.. 동시성 예제를 살펴보다가 갑자기 잡설로 빠진 것 같다. 아무튼 교착상태에 빠진 배고픈 철학자들을 구제해 줄만한 해결책은 당연히 있다. 제목에서 이미 보았듯이 '시작' 이므로, 해결책에 대한 코드는 다음 포스팅에 정리 해야 겠다. 오늘은 이만..





2017년 8월 13일 일요일

[Elasticsearch] 문자열 Data Type과 Analyze


Elasticsearch 인덱스의 mapping 설정시 각 field의 '타입'을 정하게 되는데,
우리가 흔히 다루는 문자열에 관한 타입은 'keyword'와 'text'가 있다.
이제부터 문자열 타입에 관해 정리 하겠다.

INFO: 2.x 버전의 elasticsearch에서 문자열은 'string' type 하나만 지원 했지만, 5.x 버전부터 해당 타입은 제거 되었다.


Keyword datatype



정형화된 문자열 컨텐츠 (예를 들어 이메일 주소, 호스트 이름, 각종 상태코드, 우편번호 등등..) 를 색인할 경우엔 해당 필드에 'keyword' 타입을 적용한다. 해당 타입의 필드는 일반적으로 아래와 같은 경우에 사용하게 된다.

  • 검색 시 Filtering
  • Sorting
  • Aggregation

다만 'keyword' 타입의 필드는 'Analyze' 되지 않기 때문에, 문자열 값 자체로만 검색 할 수 있다. 예를들어, 'Hello World' 라는 문자열이 색인됬다고 가정했을때, 'Hello'나 'World' 만으로는 해당 문서를 검색할 수 없는 식이다.

아래와 같이 설정 한다.

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "my_keyword_field": {
          "type": "keyword"
        }
      }
    }
  }
}



Text datatype



전문(full text) 형식과 같은 문자열 (예를들어 메일 본문과 같은) 을 색인 할 때는 필드에 'text' 타입을 적용한다. 해당 타입으로 설정된 필드는 기본적으로 'analyzed' 속성이며, 'Analyzer'를 통해서 문자열이 각각 분리된 토큰으로 인덱스에 색인 된다. 따라서 메일 본문과 같이 큰 문자열 덩어리를 가지고 있는 document를 그 본문안의 각각 단어로 검색 할 수 있게 해 준다. (이른바 'Full Text Search'..)

다만 해당 필드를 'sorting' 작업에 사용할 경우, 메모리 점유와 성능 문제를 가져온다. 때문에 sorting 작업엔 거의 사용하지 않지만, 일반적으로 'aggregation' 작업에는 성능에 유의 하면서 사용을 하는 편인 것 같다.

아래와 같이 설정한다.

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "my_text_field": {
          "type": "text"
        }
      }
    }
  }
}

하지만, 문자열을 색인하는 'text' 타입 필드에 대해서,
때로는 sorting이나 aggregation에 성능이슈 없이 사용 (keyword 타입처럼) 해야할 경우가 생길 것이다. 이 경우엔 해당 필드를 text 타입으로 설정 하면서 동시에 keyword 타입의 용도로도 사용할 수 있는 방법이 있는데, 아래와 같다.

PUT my_index
{
  "mappings": {
    "my_type": {
      "properties": {
        "city": {
          "type": "text",
          "fields": {
            "raw": {
              "type":  "keyword"
            }
          }
        }
      }
    }
  }
}

위와 같이 'city' 라는 필드에 타입은 'text'로 설정 했지만, 해당 설정 하위에 'fields' 속성을 추가하여 그 하위에 'raw' 속성을 "type": "keyword" 로 설정 하였다. 이렇게 되면 필요에 따라 keyword 타입의 용도로 해당 필드를 사용 할 수 있게 된다.

예를 들어 아래와 같은 문자열이 색인 되었다고 했을 때,

PUT my_index/my_type/1
{
  "city": "New York"
}

아래에서 보는 것과 같이 query 작업엔 text 타입의 용도 ('york' 단어 하나로만 질의) 로 검색하고, sort 작업엔 keyword 타입의 용도로 사용 할 수 있다.

GET my_index/_search
{
  "query": {
    "match": {
      "city": "york"
    }
  },
  "sort": {
    "city.raw": "asc"
  },
  "aggs": {
    "cities": {
      "terms": {
        "field": "city.raw"
      }
    }
  }
}



2017년 7월 28일 금요일

[Elasticsearch] 'Docker Compose' 로 ELK 스택 구성


 이번엔 Docker를 이용해 ELK 스택을 구성해 보겠다. Docker는 LXC 기술중 하나로서, 이식성이 뛰어난 격리된 프로세스 컨테이너를 통해 어플리케이션을 구동하는 방식이다. Docker 관련 문서들은 차고 넘치니, 따로 정리하지는 않겠다.

Docker Compose는 이러한 Docker 컨테이너 여러개를 조합(Compose)하여 설정 할 수 있게 해주고, 따라서 다수의 컨테이너 설정을 하나의 파일에서 가능하게 해 준다. 또한 설정된 컨테이너를 한번에, 또는 각각 하나씩 구동 할 수도 있다.

하지만 단 하나의 컨테이너를 구동할때도 Docker Compose가 유용하다.
가령, 아래의 tomcat 컨테이너를 띄우는 Docker 명령이 있다고 하자.

$ docker run -it --rm -p 8888:8080 tomcat:8.0

 위의 설정과 동일한 컨테이너를 Docker Compose로는 아래와 같이 설정하여 구동할 수 있다.

version: '2'
services:
  tomcat:
    image: tomcat:8.0
    ports:
      - 8888:8080

뭔가 텍스트가 더 많이 들어 간 것 같지만 기분 탓일 거다. 일단 깔끔하게 YAML 형식을 지원하는 Docker Compose 설정이 보기가(가독성이) 더 좋아 보인다. 위의 기본 docker 명령 예제에서 입력한 옵션은 많지 않지만, 훨씬 더 복잡한 옵션을 입력하고 컨테이너를 구동한다고 했을때 옵션을 입력하기도, 확인하기도 어려울 것이다. 따라서, 복잡한 하나 또는 여러개의 컨테이너 설정, 그리고 그 컨테이너들간의 관계를 설정 해야할 때는 당연히 뒤도 안보고 Docker Compose를 사용해야 한다.



docker-compose.yml 작성



ELK를 위한 docker-compose.yml 파일의 기본 골격은 아래와 같다.

version: '2'
services:
 elasticsearch:
  .
  .
  .

 logstash:
  .
  .
  .

 kibana:
  .
  .
  .

하나씩 살펴보자.

1) Elasticsearch
elasticsearch:
  image: docker.elastic.co/elasticsearch/elasticsearch:5.4.0
  container_name: elasticsearch
  volumes:
   - ./elasticsearch/config/elasticsearch.yml:/usr/share
/elasticsearch/config/elasticsearch.yml
   - ./elasticsearch/data:/usr/share/elasticsearch/data
  environment:
   ES_JAVA_OPTS: "-Xmx2048m -Xms2048m"
  ports:
   - 9200:9200
   - 9300:9300

'volumes' 설정을 통해서 외부의 파일시스템과 컨테이너 내부의 파일시스템을 마운트한다. [외부디렉토리]: [내부디렉토리] 와 같은 형식으로 마운트할 수 있는데, 위에서 보는것과 같이 파일 자체(elasticsearch.yml)도 마운트 가능하다.

'environment'로 elasticsearch 인스턴스의 JVM 옵션을 줄 수 있다. 'ports' 또한 컨테이너 외/내부의 포트를 설정하여 해당 포트를 통한 통신을 가능하게 해 준다. 별거 없다. ㅇㅇ

2) Logstash
 logstash:
  image: docker.elastic.co/logstash/logstash:5.4.0
  container_name: logstash
  command: logstash -f /usr/share/logstash/pipeline/logstash.conf
  volumes:
   - ./logstash/config/:/usr/share/logstash/config/
   - ./logstash/pipeline/:/usr/share/logstash/pipeline/
  environment:
   LS_JAVA_OPTS: "-Xmx2048m -Xms2048m"
  ports:
   - 10080:10080
  depends_on:
   - elasticsearch

elasticsearch 설정과 흡사하다. 단, 'command' 설정으로 컨테이너를 구동할때 'logstash -f ... ' 명령어로 logstash 를 실행하도록 한다. 물론 '-f' 옵션 다음의 오는 경로는 컨테이너 내부 파일 경로이다.

'depends_on' 은 'elasticsearch' 컨테이너가 시작한 후에 logstash 컨테이너를 구동하도록 하는 설정이다.

3) Kibana
 kibana:
  image: docker.elastic.co/kibana/kibana:5.4.0
  container_name: kibana
  volumes:
   - ./kibana/config/:/usr/share/kibana/config
  ports:
   - 5601:5601
  depends_on:
   - logstash

설명할 건 다 했으니 생략.



docker-compose 구동



'docker-compose.yml' 파일이 위치하고 있는 디렉토리에서,
아래와 같은 명령을 통해 구동 한다.

$ docker-compose up -d

로그를 보고 싶다면 아래 명령어를 사용한다.

$ docker-compose logs -f --tail="100" [service_name]

설정파일의 전체 내용은 아래에 있다.

https://gist.github.com/JuPyoHong/17362d9e0b64256b627dc063b00cc357


2017년 7월 24일 월요일

[Elasticsearch] node settings must not contain any index level settings


엘라스틱 서치 클러스터 환경을 구축하다가 만난 에러.
5.x 버전 이후부터 더이상 'elasticsearch.yml' 설정 파일 안에 샤드와 레플리카 갯수 설정을 할 수 없다. 아래와 같은 에러를 만나기 때문.

elasticsearch    | [2017-07-24T06:53:06,197][WARN ][o.e.c.s.SettingsModule   ] [xxxxx]
elasticsearch    | *********************************************************************
elasticsearch    | Found index level settings on node level configuration.
elasticsearch    |
elasticsearch    | Since elasticsearch 5.x index level settings can NOT be set on the nodes
elasticsearch    | configuration like the elasticsearch.yaml, in system properties or command line
elasticsearch    | arguments.In order to upgrade all indices the settings must be updated via the
elasticsearch    | /${index}/_settings API. Unless all settings are dynamic all indices must be closed
elasticsearch    | in order to apply the upgradeIndices created in the future should use index templates
elasticsearch    | to set default values.
elasticsearch    |
elasticsearch    | Please ensure all required values are updated on all indices by executing:
elasticsearch    |
elasticsearch    | curl -XPUT 'http://localhost:9200/_all/_settings?preserve_existing=true' -d '{
elasticsearch    |   "index.number_of_replicas" : "1",
elasticsearch    |   "index.number_of_shards" : "10"
elasticsearch    | }'
elasticsearch    | *********************************************************************

위에서 볼 수 있듯이 아주 친절하게 설명을 해준다.

결국 해당 설정을 하기 위해서는 '/{index}/_settings', '/_all/_settings' 등의 PUT API를 사용하거나, 'index template' 을 사용할 수 밖에 없다.

왜 이렇게 바뀌었는지는 다음에 알아보도록 하자...



2017년 7월 19일 수요일

[잡담] 기술 말하기

커뮤니케이션과 기술 글쓰기



몇년전에 친구녀석이 나에게 이런말을 한 적이 있다.

"IT 개발자는 거의 혼자 하는 일이 많지 않아?"

워딩은 위와 비슷했지만 그 뉘앙스는 마치, "개발자들은 사무실 구석에 박혀서 혼자 일에 몰두하는 히키고모리 아니냐"라는 느낌이어서 상당히 불쾌 했었던 기억이 있다. 그 말에는 별다른 반박을 하지 않고 넘어 갔었다. (한마디 해줄껄.. ㅋㅋ)

단언컨데, 개발자는 '커뮤니케이션 능력'이 가장 중요하게 여겨지는 직업들 중 하나다. 실무에서 다루는 코드베이스는 보통 엄청나게 거대한 데다가, 보통 작게는 십수명에서부터 수십명의 개발자가 하나의 형상관리 저장소를 바라보고 (물론 Git과 같은 분산 버전 관리 시스템이라면 각 로컬에 저장소를 clone 하겠지만, 결국 원격 저장소는 하나다.) 업무를 진행하게 된다. 때문에 조직내의 안정화 된 룰을 기반으로 한 실무자간의 효율적이고 명확한 커뮤니케이션이 없을 때 프로젝트가 엉망진창이 될 것은 안 봐도 비디오다.



* 브랜치 중심의 개발
git 기반의 개발 프로세스 에서는 수많은 개발자들이 브랜치를 만들고 삭제하고 
Merge하는 일이 수도 없이 일어 난다. 그것도 하나의 코드베이스를 가지고 말이다.


내가 직전에 근무했던 회사 대표님은, 코드의 품질을 아주 많이 강조하는 분이셨다. 그분은 그것과 동시에 '기술 글쓰기' 도 중히 여기셨는데, '기술 글쓰기' 사내 교육을 만들어 개발자들로 하여금 필수로 수강하게 할 정도였다. 이는 방금 위에서 언급한 "커뮤니케이션"과 맥락을 같이 한다.

요즘 중소기업에서부터 대기업, 각종 벤처회사에 이르기까지 회사 업무의 대부분이 전산화 되어있고, 주로 '이메일'을 주고 받으며 소통을 한다. 특히나 개발자들간의 이메일을 통한 커뮤니케이션은 "기술 글쓰기" 그 자체다. 따라서 이러한 글쓰기에 능숙하지 않으면 서로 각자의 메일 본문을 이해하기가 힘들게 되면서 메일 핑퐁이 늘어나게 되고, 업무 효율은 그만큼 떨어 질 수 밖에 없다.


기술 말하기



'기술 글쓰기'는 이만큼 중요하다.
하지만 나는 더 나아가 '기술 말하기'에 대해서 이야기 하고 싶다.

예를 들어, 'AOP'에 대해서 말로 설명한다고 해보자.
분명히 아래처럼 말하는 사람이 있을 것이다.

"메서드의 앞뒤로 무언가 작업을 하게 해주는 거 아니에요?"

당연히 틀린말은 아니지만 아주 많이 부족하고 '허접'한 설명이다. 만약 경력이 적지않은 개발자가 이런식으로 말을 한다면 실력을 의심해야 할 필요가 있다. 좀 더 정확한 설명은 아래와 같을 것이다.

"'횡단 관심사'를 모듈화 하여 여기저기 산만하게 퍼져있는 코드의 중복을 제거하고, 단일책임원칙(SRP)에 충실한 클래스를 설계하는데 도움을 준다. 흔히 AspectJ나 Spring AOP가 많이 쓰이며, Advice를 작성하고, Pointcut을 설정하여 적용하게 된다. 보통은 트랜잭션 이나, 로깅, 인증 처리에 사용된다."

물론 위의 문장도 완벽하게 AOP를 설명하고 있지는 않지만,
'전자' 보다는 확실히 명확하고 기술적인 말하기다.

종종 '기술 글쓰기' 보다 '기술 말하기'를 더 많이 해야 할 때가 있다. 메일로 글을 주고 받는 것보다 직접 찾아가 말로 설명하는것이 때로는 훨씬 빠르고 정확하기 때문이다. 하지만 '빠를' 수 는 있을지언정 '정확'하게 전달하기란 쉽지 않다. 이 때문에 '기술 말하기'의 중요성을 강조하고 싶은 것이다.

물론 어려운 일이다. 나 스스로도 항상 커뮤니케이션에 신경 쓰려고 노력하고, 대화 할 때 좀더 정확하게 전달 될 수 있는 단어와 용어를 선택하려고 애쓰지만 잘 안될 때가 많다.

그러니까 이 글은, 나 자신에게 바치는 글이 되겠다.
더 노력하고 더 내공을 쌓아야 한다.


2017년 7월 16일 일요일

[Javascript] 함수 호출 방법과, this 스코프의 설정

개요



오늘은 자바스크립트에서 함수를 호출하는 방식을 정리하려고 한다.

각각 함수호출 방법과 그 방법을 사용할때 'this' 객체는 서로 상이하기 때문에, 잘 알아두고 사용해야 유효범위(이하 '스코프')의 혼란을 예방할 수 있다.


함수는 '객체'다



놀랍게도 자바스크립트 에서의 함수는 '객체' 다. 정확히 말하면 값으로 취급 될 수 있는 '1급 객체' 다. 이 말 뜻은, 함수 자체에도 속성을 가질 수 있다는 것이며, 그 속성은 또 다른 함수가 될 수 있다는 말이다.

따라서 함수는 아래와 같이 '그냥' 호출 할 수도 있지만,

function func() {
    // do something
}

func();

아래처럼 함수 객체의 속성 메서드인 'call', 'apply', 'bind' 로도 호출 될 수 있다.

func.call(...);
func.apply(...);
func.bind(...);


함수 호출과 'this'



1) 기본

아래처럼 일반 함수 호출에서의 'this' 는 'window'(머리객체) 이다. ('nodejs'의 경우는 'global')

function func() {
    console.log(this); // window
}

하지만 'use strict' 를 사용하면 undefined 이다.

function func() {
    'use strict'
    console.log(this); // undefined
}


2) 객체 속성 으로서의 함수

하지만 객체에 속한 '메서드' 라면, 얘기가 조금 달라진다.
아래에서 'this' 는 객체 자신이다.

const someone = {
  name: 'Someone',
  sayHello: function(otherName) {
    console.log(this.name + ' says hello to ' + otherName);
  }
};

someone.sayHello('thomson'); 
// 결과 > 'Someone says hello to thomson'


하지만 위에서 언급한 call, apply 메서드를 사용하면, 첫번째 파라메터로 전달되는 객체를 해당 함수 스코프에서의 'this' 로 할당 할 수 있다. 아래와 같다

const alex = {
    name: 'Alex'
};

someone.sayHello.call(alex, 'thomson'); 
// 'Alex says hello to thomson'

someone.sayHello.apply(alex, ['thomson']); 
// 'Alex says hello to thomson'

첫번째 파라메터로 'alex' 객체를 넘겨주었다. 때문에 'sayHello' 메서드 내부의 'this'는 'alex' 객체이다.
따라서 위와 같은 결과가 출력된다

하지만 'bind' 메서드는, 위의 call이나 apply와는 조금 다르게 동작한다.

const bound = someone.sayHello.bind(alex, 'thomson'); 
bound();
// 'Alex says hello to thomson'

'bind' 메서드는 특정 함수의 'this'와 파라메터를 설정한 뒤, 해당 함수를 반환한다.
따라서 bound() 와 같은 방식으로 파라메터를 넘기지 않고 호출해도 call, apply와 같은 결과가
출력 되는 것이다.

또 한가지, bind 메서드는 아래와 같이도 동작한다.

function func(x, y) {
    return x + y;
}

const bound = func.bind(null, 10);
console.log(bound(20)); // 30

bind 메서드의 첫번째 파라메터로 특정 객체를 함수 내부의 this 로 설정할 수 있지만 위의 예제에선 필요 없으므로 'null' 을 넘겼다. 그리고 두번째 파라메터로 10을 넘겼는데, 이는 'func' 함수의 첫번째 파라메터인 'x' 값을 고정 시키겠다는 의미이다. 따라서 'bound(20)' 의 결과는 '30' 이 된다.


2017년 7월 11일 화요일

[Elasticsearch] Elastic 라이센스 등록

 엘라스틱 진영의 생태계는 시간이 지날 수록 고도화 되는 것 같다. 그에따라 다양한 도구들과 기능들이 제공되고 있는데, 아쉽게도(?) 대부분이 유료 라이센스를 필요로 하는 기능들이다. 하지만 기본적인 ELK Stack을 구축한다고 했을 때, 무료 라이센스인 'BASIC' 버전 만으로도 충분히 쓸만한 시스템을 구축 할 수 있다.

아래 웹 페이지로 접근하여, 라이센스별 기능 제공 여부를 확인 할 수 있다.














위 처럼 'BASIC' 버전의 'Free License' 를 선택하면 라이센스 등록 페이지로 이동하고, 이름과 이메일, 국가 등의 간단한 정보만 입력하면 '라이센스 json 파일'을 입력한 이메일로 받을 수 있다.

License Update 방법



간단하다. 라이센스 파일은 위에서 말한 것 처럼 'json' 파일로 다운로드가 되는데, 이 파일을 xpack의 rest api 로 밀어 넣어 주면 된다.

$ curl -XPUT -u elastic:changeme 
'http://10.213.128.227:9200/_xpack/license?pretty&acknowledge=true' 
-H "Content-Type: application/json" -d @[license file name].json

여기서 주의할 점은, elasticsearch 클러스터 서버에 'X-Pack' 플러그인이 설치 되어 있어야 한다는 것이다. 만약 설치가 안되어 있다면, '/_xpack' 으로 시작하는 api 자체를 사용할 수 없다.


License 확인



아래의 rest api로 등록된 라이센스 정보를 알 수 있다

$ curl http://10.213.128.190:9200/_license

결과는 아래와 같다.

{
  "license": {
    "status": "active",
    "uid": "blahblah",
    "type": "basic",
    "issue_date": "2017-06-08T00:00:00.000Z",
    "issue_date_in_millis": 1496880000000,
    "expiry_date": "2018-06-08T23:59:59.999Z",
    "expiry_date_in_millis": 1528502399999,
    "max_nodes": 100,
    "issued_to": "Hong Ju Pyo (Coupang)",
    "issuer": "Web Form",
    "start_date_in_millis": 1496880000000
  }
}

basic 라이센스의 유효기간은 '1년'이며, 1년이 지났다면 역시 무상으로 라이센스를 갱신할 수 있다. 즉, 평생 무료라는 얘기..

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