top bar

글 목록

2015년 6월 24일 수요일

[JAVA] List 의 toArray() 메서드

 List 컨테이너의 인스턴스를 배열(array)로 만드는것이 'toArray' 메서드이다. 하지만 이 메서드 사용에 있어서 아래와같은 아리송한 코드를 자주 봤을 것이다.
List<String> stringList = new ArrayList<String>();
stringList.add("A");
stringList.add("B");
stringList.add("C");
        
String[] stringArray = stringList.toArray(new String[0]);
메서드 이름에서 직관적으로 알 수 있듯이, List를 Array로 바꿔주는 메서드인데, 파라메터로 들어가는 인자가 이상하다.

위의 예제에서는 String 배열 인스턴스가 파라메터로 넘어갔는데, size를 '0'으로 명시했다.
이것이 무슨의미일까? 정리하자면 아래와 같다.

1. List를 toArray 메서드에 파라메터로 넘어가는 배열 객체의 size만큼의 배열로 전환한다.
2. 단, 해당 List size가 인자로 넘어가는 배열 객체의 size보다 클때, 해당 List의 size로 배열이 만들어진다.
3. 반대로 해당 List size가 인자로 넘어가는 배열객체의 size보다 작을때는, 인자로 넘어가는 배열객체의 size로 배열이 만들어진다.

여기서 위의 예제에서의 stringArray의 크기는 '3' 이다. 인자로 넘어가는 배열의 size가 '0'이므로, 원래 List의 size로 배열이 만들어진 것이다.

하지만 아래와 같은 예제라면 stringArray의 크기가 얼마가 될까?
List<String> stringList = new ArrayList<String>();
stringList.add("A");
stringList.add("B");
stringList.add("C");
        
String[] stringArray = stringList.toArray(new String[5]);

System.out.println("array size : " + stringArray.length);
출력 결과는 아래와 같다
array size : 5
뭐, 당연한 결과다. toArray 메서드의 인자로 넘어가는 배열 size가 원래 List사이즈보다 크므로, 인자로 넘어가는 배열 size로 배열이 만들어진다.

끗.

2015년 6월 16일 화요일

[JAVA] List - ArrayList

List 개요



 아마도, JAVA 개발자들이 가장 많이 쓰는 컨테이너 중에 하나일 듯 싶다. List는 특별한 시퀀스(Sequence)로 객체들을 유지, 관리한다.

List는 두가지 타입이 있다.

ArrayList
LinkedList
요소(객체)들을 무작위로 Access하는데 O(1)의 시간으로 속도 면에서 탁월하다. 하지만 중간에 요소들을 추가하거나 삭제할 때는 효율이 별로 좋지 않다.
최적의 순차 Access를 제공하며, 중간에 요소 추가/삭제 면에서 ArrayList보다 훨씬 빠른 속도를 보인다. 반면에 무작위 Access ArrayList보다는 느리다.


예제 - ArrayList



 List인터페이스는 매우 다양한 메소드를 제공한다. 예제를 통해 각 메소드들을 이해하도록 하자. 이제부터 ArrayList의 메소드들을 살펴본다.

※ 예제에는 Pet(애완동물)클래스의 다양한 서브클래스를 이용해 생성된 객체들을 가지고 테스트 해본다. 예제에서 Pets.arrayList() 메소드는 무작위로 선택된 Pet객체들로 채워진 ArrayList를 반환한다.

1) add , contains , remove
List<Pet> pets = Pets.arrayList(7);
        
print("1: " + pets);
Hamster h = new Hamster();

pets.add(h);
        
print("2: " + pets);
print("3: " + pets.contains(h));

pets.remove(h);

print("4: " + pets);
print("5: " + pets.contains(h));
> 결과
1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
2: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Hamster]
3: true
4: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
5: false
7개의 Pet 객체들로 채워진 pets 라고하는 ArrayList가 만들어졌다. 거기에 add 메서드로 'Hamster'의 인스턴스 'h'를 추가했고(여기서 ArrayList의 크기는 자동으로 조절된다), contains 메서드로 인스턴스인 'h'가 존재하는지 확인했다.

그 후에 추가했던 'h'를 remove 메서드를 이용해 삭제하고, 다시 리스트 내용과 contains메서드 결과를 보면 제대로 삭제 된것을 확인 할 수 있다.

add 메서드는 아래와같이 쓸 수도 있다.
pets.add(3, new Hamster());
> 결과
[Rat, Manx, Cymric, Hamster, Mutt, Pug, Cymric, Pug]
첫번째 파라메터는 추가되는 객체가 들어갈 index이고, 두번째 파라메터는 추가되는 객체를 나타낸다. 결과를보면 index 3에 Hamster 객체가 추가된것을 볼 수 있다.

2) get , indexOf
List<Pet> pets = Pets.arrayList(7);

print("1: " + pets);

Pet p = pets.get(2);
print("2: " + p + " " + pets.indexOf(p));
        
Pet cymric = new Cymric();
print("3: " + pets.indexOf(cymric));
> 결과
1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
2: Cymric 2
3: -1
get 메서드는 파라메터로 넘어간 index의 객체를 반환한다. 여기서는 'Cymric' 객체가 반환된다. indexOf 메서드는 파라메터로 넘어간 객체가 해당 리스트에서 몇번째 인덱스인지에 대한 int값을 반환한다. 여기서는 index '2'에 위치해 있는것을 알 수 있다.

하지만 3번의 결과에서 같은 'Cymric' 객체를 만들어 indexOf를 호출해보면 해당 객체의 위치를 찾지 못하고 '-1'을 반환한다. 왜일까?

이유는 indexOf 메서드는 파라메터로 넘어간 객체의 '주소값'을 해당 리스트 객체의 주소값과 매칭시킨다. (contains 메서드도 마찬가지) 때문에 같은 클래스로부터 생성된 객체라도 주소값이 다르기때문에 다른 객체로 인식이 되는 것이다. 같은 클래스의 객체를 무조건 같은 객체로 인식하게 하고싶다면, 'equals' 메서드를 오버라이딩 하자.

3) subList , containsAll , removeAll
List<Pet> pets = Pets.arrayList(7);

List<Pet> sub = pets.subList(1, 4);

print("1: " + pets);
print("subList: " + sub);
print("2: " + pets.containsAll(sub));

pets.removeAll(sub);
print("3: " + pets);
> 결과
1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
subList: [Manx, Cymric, Mutt]
2: true
3: [Rat, Pug, Cymric, Pug]
subList 메서드는 말그대로 해당 리스트에서 지정된 범위 만큼 복사하여 새로운 리스트를 생성한다. 위의 예제의 범위는 index '1'부터 '4'전까지 이다.

containsAll 메서드는 말그대로 파라메터로 넘어간 리스트가 해당 리스트에 존재하는지의 여부를 반환한다. 여기서 중요한건, 요소의 순서와는 무관하다는 것이다.

removeAll 메서드는 역시 말그대로 파라메터로 넘어간 리스트를 pets리스트에서 제거한다. 역시 순서는 중요하지 않다.

4) addAll , clear , isEmpty
List<Pet> pets = Pets.arrayList(7);
print("1: " + pets);

List<Pet> newPets = Pets.arrayList(3);      
print("2: " + newPets);
        
pets.addAll(newPets);       
print("3: " + pets);

pets.clear();
print("4: " + pets.isEmpty());
> 결과
1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
2: [Manx, Cymric, Rat]
3: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug, Manx, Cymric, Rat]
4: true
addAll 메서드는 말그대로 파라메터로 넘어온 리스트를 합친다. 3번 결과를 보면 1번과 2번 결과가 합쳐진 것을 볼 수 있다.

clear 메서드는 리스트의 내용물을 모두 지우는 메서드이고, isEmpty 메서드는 해당 리스트가 비어있는지 여부를 반환한다.

여기서 addAll 메서드는 아래와 같이 쓰일 수도있다.
pets.addAll(3, newPets);
add 메서드때와 같이 첫번째 파라메터로 넘어온 인덱스부터 합치려는 리스트를 끼워 넣는다. 결과는 알아서 확인하시길..

5) retainAll
List<Pet> pets = Pets.arrayList(7);     
print("1: " + pets);

List<Pet> sub = Arrays.asList(pets.get(0), pets.get(4));
print("sub: " + sub);
        
pets.retainAll(sub);
print("2: " + pets);
> 결과
1: [Rat, Manx, Cymric, Mutt, Pug, Cymric, Pug]
sub: [Rat, Pug]
2: [Rat, Pug]
retainAll 메서드는 말하자면 '교집합' 이다. 위의 예제에서는, pets 리스트의 0번째 요소와 4번째 요소로 sub 리스트를 만들었고, retainAll 메서드를 이용, pets와 sub리스트중 겹치는 요소만 pets에 남겼다. 2번결과를 보면 알 수 있다.




2015년 6월 15일 월요일

[Linux] What does “&” at the end of a linux command mean? - 기묘한 명령어들

'&'의 의미?



리눅스 커맨드 라인에서 '&'(앰퍼센드)의 의미는 무엇일까?
아래 페이지에 다양한 의견들이 있다.

http://superuser.com/questions/152688/why-run-a-linux-shell-command-with

의미는 간단하다 아래와 같이 간단한 bash shell 명령을 날려보자.
$ echo "Hello world" &
결과는? 아래와 같다.
$ echo "Hello world" &
[1] 18251
Hello world
명령어를 실행과 동시에 프로세스의 PID를 뱉고서 Backdground로 사라진다. 해당 명령어가 짧게 끝나는 단발성 프로세스라 그렇지 tomcat이라던가 apache와같은 계속적으로 메모리와 cpu점유율을 유지하는 프로세스 라면, 프로세스를 'kill' 하기 전까지 Background에서 돌고 있을 것이다.

결론적으로 커맨드 라인 끝에 '&' 라는것은, 실행한 프로세스를 Background로 보내고, 사용자를 즉시 prompt로 돌아오게 하는 명령인 것이다.

What does '> /dev/null 2>&1' mean?



그렇다면 우리가 자주쓰는 '> /dev/null 2>&1' 이라는건 뭘까? 아래 블로거의 글을 참고하자.

http://www.xaprb.com/blog/2006/06/06/what-does-devnull-21-mean/

참 자주쓰이는 명령이지만 의미도 모른채 쓰는경우가 얼마나 많던가? 먼저 이것을 설명하려면 UNIX(또는 Linux)의 3가지 I/O 개념을 알아야한다.

별거없다 바로 'stdin' , 'stdout', 'stderr'이다. 말그대로 표준 입출력과 에러 출력이다. 사용자들은 리눅스의 쉘 커맨드 라인에서, 이러한 I/O 시스템을 이용해 운영체제와 소통한다.

하지만, 이들은 때때로 숫자로 표현된다. 'stdin'은 '0', 'stdout'은 '1', 'stderr'는 '2'와 같은 식이다. 따라서 위의 예제는 표준 에러 출력 '2' 를 표준출력 '1' 로 redirecting 하는 것이다. 이때, destination의 숫자앞에는 '&'가 꼭 붙어야 한다. 안 그러면 '1'이라는 텍스트 파일이 생성되버리고, 그곳에 에러출력이 redirect 된다.

'/dev/null'은 무엇일까? 한마디로 쓰레기 처리장이라고 보면 되겠다. bit-bucket 이라고도 불리며, 어떤 것이든 dump 할 수 있는 공간(?)이다.

한 문장으로 설명하자면, '이 명령어의 모든 출력을 'blackhole'로 걍 보내 버리자' 라는 것이다.

2015년 6월 6일 토요일

[Elasticsearch] Tokenizer와 Token Filter

1. 개요



지난 포스트엔, Full Text Search 에 효율적인 'Inverted Index'에 대해서 알아 보았다. 요약하자면, Full Text에 대해서 해당 문장을 분리/가공 하고, 그 각각의 terms를 document와 매핑시키는 인덱스 리스트를 만드는 것이다. (책 뒷편의 '단어색인' 리스트와 비슷한 개념이다)

이번 포스트에는 Inverted Index를 만들기 위해 수행되는 분리/가공의 작업들이 Elasticsearch에서 어떤방식으로 일어나는지 알아 보고자 한다.


2. Tokenizer와 Token Filter



Inverted Index를 생성하는 과정을 되짚어보면, 먼저 각 단어(term또는 token)를 분리하는 작업이 먼저 수행되었고, 그 다음으로 분리된 단어들을 검색 가능 하도록(searchable) 가공하는 작업을 수행했다.

전자는 Tokenizer, 후자는 Token Filter가 수행한다. 이러한 Tokenizer와 Token Filter는 매우 다양한 종류가 있는데, 이 둘을 합친것이 바로 'Analyzer'(분석기) 인 것이다.


INFO : 문자열이 Tokenizing 되기전에 'Character Filtering'이라는 작업을 거치는데, 이는 문장이 분리되기전에 깔끔하게 정리하는(tidy up) 역할을 한다. 예를들어 contents 안에 들어있는 HTML태그를 걷어내는 작업이라던가, 문장속에 '&'을 영단어 'and'로 바꿔주는 작업 등이다. 이번 포스트에서는 상세한 설명을 생략하도록한다...(시간음슴)

2.1 Tokenizer


토큰들을 분리하는 작업을 수행한다. 대표적으로 'whitespace' 토크나이저가 있는데, 말그대로 공백을 기준으로 단어들을 분리한다.

'_analyze' API를 사용하여 테스트해보자
$ curl -XPOST 'localhost:9200/_analyze?tokenizer=whitespace&pretty' -d 'Around the World in Eighty Days'
{
  "tokens" : [ {
    "token" : "Around",
    "start_offset" : 0,
    "end_offset" : 6,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "the",
    "start_offset" : 7,
    "end_offset" : 10,
    "type" : "word",
    "position" : 2
  }, {
    "token" : "World",
    "start_offset" : 11,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "in",
    "start_offset" : 17,
    "end_offset" : 19,
    "type" : "word",
    "position" : 4
  }, {
    "token" : "Eighty",
    "start_offset" : 20,
    "end_offset" : 26,
    "type" : "word",
    "position" : 5
  }, {
    "token" : "Days",
    "start_offset" : 27,
    "end_offset" : 31,
    "type" : "word",
    "position" : 6
  } ]
}
'tokenizer'라는 매개변수를 지정했고 결과는 보는 바와 같다. 
'Around th World in eighty Days' 문장이 공백 기준으로 분리된다.

하나만 더 살펴보자, letter 토크나이저는 공백뿐만아니라 특수문자를 기준으로 토큰을 분리한다. 다음과 같다.
$ curl -XPOST 'localhost:9200/_analyze?tokenizer=letter&pretty' -d 'Around&the*World#in^Eighty@Days'
{
  "tokens" : [ {
    "token" : "Around",
    "start_offset" : 0,
    "end_offset" : 6,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "the",
    "start_offset" : 7,
    "end_offset" : 10,
    "type" : "word",
    "position" : 2
  }, {
    "token" : "World",
    "start_offset" : 11,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "in",
    "start_offset" : 17,
    "end_offset" : 19,
    "type" : "word",
    "position" : 4
  }, {
    "token" : "Eighty",
    "start_offset" : 20,
    "end_offset" : 26,
    "type" : "word",
    "position" : 5
  }, {
    "token" : "Days",
    "start_offset" : 27,
    "end_offset" : 31,
    "type" : "word",
    "position" : 6
  } ]
}
whitespace 토크나이저와 같은 결과이지만, '특수문자'로 토큰이 분리된것을 볼 수 있다.

더 많은 토크나이저는 아래를 참고하자
https://www.elastic.co/guide/en/elasticsearch/reference/1.4//analysis-tokenizers.html

2.2 Token Filter


분리된 토큰들을 가공한다. 토큰 필터는 마찬가지로 _analyze api로 테스트해 볼 수 있는데, 'filters'라는 매개변수를 통해 지정한다.

먼저 대표적인 필터인 'lowercase' 토큰필터를 테스트해보자.
$ curl -XPOST 'localhost:9200/_analyze?tokenizer=whitespace&filters=lowercase&pretty' -d 'Around the World in Eighty Days'
{
  "tokens" : [ {
    "token" : "around",
    "start_offset" : 0,
    "end_offset" : 6,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "the",
    "start_offset" : 7,
    "end_offset" : 10,
    "type" : "word",
    "position" : 2
  }, {
    "token" : "world",
    "start_offset" : 11,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "in",
    "start_offset" : 17,
    "end_offset" : 19,
    "type" : "word",
    "position" : 4
  }, {
    "token" : "eighty",
    "start_offset" : 20,
    "end_offset" : 26,
    "type" : "word",
    "position" : 5
  }, {
    "token" : "days",
    "start_offset" : 27,
    "end_offset" : 31,
    "type" : "word",
    "position" : 6
  } ]
}

토크나이저는 whitespace를 사용했다. 분리된 토큰들이 모두 '소문자'로 바뀐것을 볼 수 있다.

더많은 필터들은 아래를 참고하자
https://www.elastic.co/guide/en/elasticsearch/reference/1.4//analysis-tokenfilters.html

3. 사용자 지정 Analyzer



이러한 다양한 토크나이저와 필터들을 조합하여 사용자 지정 분석기를 사용할 수 있다.
이러한 분석기는 인덱스를 생성할 때 아래와 같이 등록 할 수 있다.
$ curl -XPUT 'localhost:9200/test_index?pretty' -d
'{
   "settings" : {
      "analysis" : {
         "analyzer" : {
            "<analyzer_name>" : {
               "tokenizer" : "<tokenizer_name>",
               "filter" : ["<filter1_name>", "<filter2_name>", ... ]
            }
         }
       }
    }
}
자, 이제 'whitespace' 토크나이저와 'snowball', 'lowercase', 'synonym' 이렇게 3가지의 토큰 필터를 적용한 사용자지정 분석기를 적용 해보자.

먼저 아래와같이 'test_index'를 생성할때 분석기를 지정한다.
$ curl -XPUT 'localhost:9200/test_index?pretty' -d '{
   "settings" : {
      "analysis" : {
         "analyzer" : {
            "my_analyzer" : {
               "tokenizer" : "whitespace",
               "filter" : ["snowball", "lowercase", "my_filter"]
            }
         },
         "filter" : {
            "my_filter" : {
               "type" : "synonym",
               "synonyms" : ["quick, fast", "jump, hop => hop"]
            }
         }
      }
   }
}'

'synonyms' 필터는 따로 옵션을 설정해야하는 필터이므로, 'filter' 설정을 통해서 먼저 사용자지정 필터를 만들고나서, analyzer를 구성할때 등록한다

이렇게 지정한 분석기를 테스트해보자.
$ curl -XPOST 'localhost:9200/test_index/_analyze?analyzer=my_analyzer&pretty' -d 'The Quick Rabbit Jumped'
{
  "tokens" : [ {
    "token" : "the",
    "start_offset" : 0,
    "end_offset" : 3,
    "type" : "word",
    "position" : 1
  }, {
    "token" : "quick",
    "start_offset" : 4,
    "end_offset" : 9,
    "type" : "SYNONYM",
    "position" : 2
  }, {
    "token" : "fast",
    "start_offset" : 4,
    "end_offset" : 9,
    "type" : "SYNONYM",
    "position" : 2
  }, {
    "token" : "rabbit",
    "start_offset" : 10,
    "end_offset" : 16,
    "type" : "word",
    "position" : 3
  }, {
    "token" : "hop",
    "start_offset" : 17,
    "end_offset" : 21,
    "type" : "SYNONYM",
    "position" : 4
  } ]
}

먼저 whitespace 토크나이저가 공백으로 단어들을 분리했다. 그리고 'lowercase'가 각 단어들을 소문자로 바꾸었다. 그리고 'synonym' 토큰필터에 등록한대로, quick이라는 단어의 동의어로 'fast'라는 단어도 색인되었다.

흥미로운점은 Jumped라는 단어가 'jump'로 인식되어 hop으로 색인되었다는 것이다.
우리가 synonym 토큰필터에는 단지 'jump, hop => hop' 으로 설정하여, jump 또는 hop이라는 단어를 그냥 hop으로 색인되도록 설정했을뿐인데 어떻게 'Jumped'가 'jump'로 인식이 되었을까?

lowercasing을 거쳐서 jumped라고 색인되었겠지만 그렇다고 'jump'와 완전히 같은 단어는 아니다. 비밀은 'snowball'이라는 토큰필터였다. 이 'snowball'이라는 토큰필터는 다양한 언어의 형태소 분석을 수행한다.

정리하자면, 'Jumped'라는 단어는 lowercasing 필터를 거쳐 'jumped'로, snowball 필터를 거쳐서 뿌리가되는 단어인 'jump'로 인식이 되었고, synonym 필터를 거쳐서 동의어인 'hop'로 색인된 것이다!

4. 결론



이렇게 Elasticsearch는 토크나이저와 토큰필터를 이용해 Full Text를 분석하여 Inverted Index를 만든다. 이를 통해 Full Text Search를 가능하게 하는것이다.

'검색엔진' 이라는것이 처음에는 생소하고 막연했지만, Elasticsearch 를 공부하면서 모든것은 아니지만 어느정도 검색엔진이 어떻게 동작하는지 이해할 수 있는것 같다.

이번 Elasticsearch Analyzer를 공부하면서 더욱더 그 이해가 깊어질 수 있었다.