top bar

글 목록

2018년 7월 22일 일요일

[JAVA] Java9 모듈 프로그래밍 - 책 리뷰

이 글은 'Java9 모듈 프로그래밍(코시크 코타갈 지음, 한빛 미디어)'에 대한 리뷰 입니다. 책의 내용을 그대로 적은 것 보다는, 읽고 난 뒤 해당 내용에 대한 이해를 바탕으로 사견을 덧붙여 작성했습니다.






 자바 세상의 본격적으로 '함수형 프로그래밍'의 시작을 알린 '자바8'이 출시 된지도 꽤 많은 시간이 지났다. 급기야 '자바10'까지 발표되었으니, Oracle이 Sun을 인수하면서 진화론적으로 정체되어 있던 자바에 꽤나 공을 들이고 있는 모양세다.

 자바10의 Spec을 대충 훑어보니, 지역변수의 타입추론 기능과 G1 GC가 최적화 되는 등 소문대로 Minor한 변화가 이루어 진 것 같다. (여기서 Minor 라는 것은 '패러다임의 변화' 수준이 아니라는 말이다. 오해 ㄴㄴ) 이 시점에서 나의 눈에 들어온 것이 지금 리뷰하려고 하는 'Java9 모듈 프로그래밍' 이라는 책이었다.

 나는 기술서적을 고를때 대략 3가지 정도를 고려하는데, (물론 Yes24의 별점과 리뷰를 보는건 기본) 첫번째로 책의 디자인(...), 두번째로는 저자가 누구인지, 세번째로는 역자가 '테크 트랜스 그룹 T4'가 아닌지 이다. 세번째 이유는 해외에서 극찬을 받고 있는 'Spring In Action' 이라는 책을 해당 번역팀(?)이 번역한 역서를 읽다가, 메서드를 '방법'으로, 오버라이딩을 '과적(...)'으로 번역한 부분을 읽고 책을 던져 버릴 뻔 했어서다. 그래도 내용 자체는 좋으니 완독은 했다...

 아무튼간, 조금 더 최신의 자바에 대해 알고 싶었고, 위의 3가지 조건을 만족 했으며, 자바9의 가장 큰 변화라고 할 수 있는 '모듈'을 다뤘기에 해당 책을 구입하여 2회독을 한 뒤에야 이렇게 리뷰를 남기게 되었다. (특히나 번역이 너무나 매끄럽고 좋았다. 일면식도 없지만, 이 책을 번역해주신 역자께 감사드린다)

이 책은 타이틀이 모든것을 말해준다.


"모듈 프로그래밍"



 소프트웨어 개발에서 '모듈'이라는 용어는 매우 광범위하게 사용된다. 이는 크게 볼때 '시스템 아키텍쳐 관점에서의 컴포넌트', 작게는 '캡슐화된 클래스'가 될 수 있을 것이다. 하지만 이 책에서 말하는 '모듈'이라는 것은 캡슐화된 클래스의 모음, 즉 '라이브러리'에 가깝지만 '더욱 더 강력한 캡슐화가 이루어진 라이브러리'라고 할 수있다. 여기서 말하는 '더욱더 강력한 캡슐화'라는 것은 글을 써 내려가면서 언급하려고 한다.

저자는 책의 도입부에서 기존 자바를 사용할 때 만날 수 있는 2가지 문제점을 실용적이고 실감나는 사례를 들며 풀어 나간다. 여기서 2가지 문제점이란 바로 '완전하지 않은 캡슐화'와 '빌드된 바이너리의 (혹시 모를) 불안정성'이다.

 '완전하지 않은 캡슐화'라는 것은 무엇인가? 더 정확히 말하면 '라이브러리 외부'로 부터의 완전하지 않은 캡슐화이다. 기존의 자바에서는 '접근 지정자(또는 제어자)' 키워드를 이용해 '캡슐화'를 실천한다. 하지만 '접근 지정자'로 할 수 있는 캡슐화의 범위는 멤버 필드, 그리고 메서드뿐이다. 저자는 바로 이 점을 문제 삼는다. 여기서 문제란 라이브러리 외부로부터 오는 '타입' 그 자체에 대한 접근을 막을 수 없다는 것이다. 타입 자체를 캡슐화 하려면 패키지 단위로 해당 타입을 묶어야 하는데, 이것은 해당 타입을 필요로 하는 라이브러리 내부의 클래스 조차 접근하지 못하게 만드는 설계의 한계를 가져오고 만다.

 '빌드된 바이너리의 불안정성'은 바로 'NoClassDefFoundError' 오류로 한번에 설명이 된다. 물론 이 오류를 실무에서 보는 것은 아주 드물다. 하지만 그렇다고 해서 빌드 도구인 Gradle이나 Maven이 필요한 모든 Compiled Class들이 포함된 바이너리를 빌드 해 준다는 '보장'은 없다. 개발자의 실수로 Runtime에 필요한 라이브러리에 대해 빌드 스크립트에서 의존성을 'Test' 범위로 지정해 버린다거나, 빌드 도구의 실수(버그나 오류)로 몇몇 필수적인 Compiled Class가 누락될 수도 있다.


더 나은 캡슐화


 기존의 자바에서도 온갖 디자인 패턴을 통해 캡슐화 시도들이 있어 왔지만, 이것이 사용하지 말아야 할 클래스(예를 들어 'sun.misc.Unsafe' 같은)의 사용까지 막을 수는 없었다는게 저자의 설명이다. 이제 자바9는 비로소 '모듈' 시스템'을 도입 했다. 이것은, 클래스들이 강하게 결합된 기존의 모놀리식(Monolithic) 설계를 버리고 진정한 의미의 독립적인 '모듈' 작성을 가능케 한다.

 자바9는 뿌리까지 바뀌었다. 프로젝트 'Jigsaw'를 통해 자바 플랫폼 API 자체가 모듈 프로그래밍으로 재작성 되었고, 이제 자바 프로그램은 60메가 바이트에 달하는 'rt.jar' 를 통째로 포함하여 빌드할 필요없이 꼭 필요한 플랫폼 모듈만 포함하여 빌드 될 수 있다.

 또한 자바 응용 프로그램 개발자는  자바 프로젝트의 전통적인 소스 디렉토리 구조(src/main/java 와 같은)가 아닌,  'src' 디렉토리 하위에 모듈 별로 'root 디렉토리'를 생성하여 모듈 코드를 작성하게 된다. 그리고 각 root 디렉토리에는 'module-info.java'라는 특수한 이름을 가진 모듈 기술자(module descriptor)파일을 작성하게 되는데, 이 파일에 'requires' 키워드를 이용하여 타 모듈에 대한 의존성 설정을 하고,  'exports' 키워드를 이용하여 현재 모듈이 어떤 패키지를 외부에 노출(export)할 것인지를 정의하게 된다.

 따라서 라이브러리 외부의 프로그램은, 자신이 사용하는 라이브러리의 모듈에서 'export' 하는 패키지의 'public' 클래스만을 접근할 수 있게 된다. 결론적으로 기존 접근 지정자 사용에 '모듈 시스템'이 더해져 더욱 더 꽁꽁 감싸여진, 앞서말한 더욱 더 강력한 캡슐화가 가능해진 것이다.


신뢰성 있는 빌드  


 우리가 사용하는 빌드 도구들은 매우 유용하다. Maven이나 Gradle은 의존성 라이브러리를 자동으로 내려받아주고, '컴파일 - 테스트 - 아카이빙 - 리포팅' 에 이르는 전체적인 빌드의 자동화를 제공한다. 하지만 앞서 언급 했듯이, 이것이 빌드된 바이너리의 안정성을 보장하지는 못한다.

 어찌 되었든 JRE가 누락된 Compiled Class에 대해서 어느 시점에 로딩을 시도하여 'NoClassDefFoundError' 오류가 발생되는 것을 목도하지 않는 이상, 그 전에 모든 필요한 Compiled Class들이 바이너리에 안정적으로 포함되어 있는가를 확인할 수 있는 방법은 사실상 없다. (빌드된 바이너리를 decompile하여 import문에 정의된 클래스들이 잘 포함되어 있는지 일일히 확인하는 정신나간 사람은 없을 것이다) 이러한 문제에 대해 '모듈 시스템'은 어떤 솔루션을 제시 할까?

 자바9 '모듈 시스템'을 이용하여 작성된 바이너리를 구동할 때, 바로 '모듈 기술자'를 통해 해당 기술자에 명시된 모든 의존성을 확인한다. 여기서 중요한점은, 의존성을 확인하는 시점이 프로그램을 구동하여 자바 어플리케이션이 '초기화' 될 때 라는 것이다.

 이것은 우리에게 빌드된 바이너리에 대한 'Fail-Fast'를 제공한다. 특정 기능을 수행하지 않고는 확인할 길이 막막한 'NoClassDefFoundError'와 같은 Runtime 오류를 프로그램 구동시에 바로 잡아주어, 해당 오류에 대한 대처를 빠르게 할 수 있게하고, 그것을 통해 빌드된 바이너리의 신뢰성을 더할 수 있다는 것이다. 이는 '모듈 시스템'의 매우 중요하고 강력한 장점이라고 할 수 있다.

. . .


 지금 까지 이야기한 2가지 담론이 이 책을 이루는 뼈대라고 할 수 있다. 이 책에서는 이러한 뼈대를 바탕으로 독자로 하여금 간단한 '주소록' 프로그램을 모듈을 사용하여 예제로 작성하게 하고, 책을 읽어 가는 동안 점점 발전 시키며 '모듈 시스템'의 여러 기능들을 소개 하는데,

  • 자신이 의존하는 모듈의 의존성을 암시적으로 포함하는 '전이적 의존성'
  • 자신의 특정 패키지를 특정 모듈에만 export하는 '제한적 export'
  • 여러 모듈의 기능을 모아주는 '집합자 모듈'
  • 모듈간 loose coupling을 위한 간접 계층인 '서비스'

등의 기능들이 그것이다

 또한 다양한 모듈 설계 패턴, 링킹을 통해 어떻게 모듈이 해석되고 jar가 아카이빙 되는지, 또 자바9 이전의 레거시 버전에서 어떻게 마이그레이션 하는지에 대한 내용이 이어지는데 설명하는 수준이 깊지만 이해하기가 어렵지 않았다. (언제가 될진 모르겠지만) 자바9로 마이그레이션 하게 된다면 유용한 참고 자료가 될 것 같다.

. . .


리뷰를 마치며


 내가 다니는 회사는 매달 5만원의 도서 구입 쿠폰을 제공하기 때문에 기술 서적을 많이 사고, 많이 읽는 편이다. 하지만 이렇게 책 내용 및 번역의 완성도가 높아 술술 읽혔던 책은 'Java8 In Action' 이후 꽤 오랫만인것 같다.

 자바9의 '모듈 시스템'은 플랫폼 자체를 재작성할 정도로 너무나 큰 변화다. 따라서 실제 운영되고 있는 서비스에 적용하기에는 매우 높은 비용과 시간이 걸릴 것 같다. 사실, 서비스 레벨의 자바 프로그램에서 '모듈'이라는 것은 그 필요성이 간절하지도 않아 보인다. 어찌 되었든, 자바의 신선한 변화를 심도있게 엿볼 수 있었기에 매우 좋은 책을 읽은 것은 확실하다. (자바가 기존의 동적 언어들과 뭔가 비슷해져 가고 있는건 기분탓일까..)




2018년 7월 3일 화요일

[Javascript] Code Snippet - 객체로 Query String 만들기


Javascript 객체를 Query String으로 변환해보자.
Arrow Function을 인자로 받는 배열 고계함수를 사용한다.
buildQueryString(parameters) {
  return encodeURIComponent(
    Object.keys(parameters)
    .map(key => key + '=' + parameters[key])
    .join('&')
  );
}
결과 Query String을 'encodeURIComponent' 로 감싸는 것을 잊지 말자.
개발하다가 남겨두면 좋을 것 같아 메모.

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