블로그 이미지
평범하게 살고 싶은 월급쟁이 기술적인 토론 환영합니다.같이 이야기 하고 싶으시면 부담 말고 연락주세요:이메일-bwcho75골뱅이지메일 닷컴. 조대협


Archive»


 
 

머신러닝 모델 개발 삽질기

빅데이타/머신러닝 | 2017.04.24 14:27 | Posted by 조대협

머신러닝 모델 개발 삽질 경험기


조대협 (http://bcho.tistory.com)


딥러닝을 공부하고 CNN 모델을 기반으로 무언가를 만들어보겠다는 생각에, 해외 유명 연예인 얼굴 사진을 가져다가 분류하는 얼굴 인식 모델을 만들어 보기로 하였다.

아직도 진행중이지만, 많은 시행 착오를 겪었는데 같은 시행 착오를 겪지 않고 경험을 공유하기 위해서 겪었던 시행 착오들을 정리해 본다.

학습 데이타 확보 및 분류

먼저 학습용 데이타를 수집 하는 것이 가장 문제 였다. 인터넷에서 사진을 모아서 학습 데이타로 사용해도 되겠지만, 아무래도 저작권 및 초상권 문제가 있고, 일일이 사진을 하나씩 받아서 수집하거나 또는 별도의 수집기를 만드는 것도 부담이 되었다.

그래서 찾은 것이 pubfig라는 셀럽 얼굴 데이타인데 http://www.cs.columbia.edu/CAVE/databases/pubfig/

상용 목적이 아니라 연구용 목적이면 사용이 가능하다. 이 데이타는 파일 URL, 셀럽 이름 형태로 라벨링이 되어 있기 때문에, 학습에 적합하리라고 생각하고, 이 파일을 기반으로 데이타를 수집하였다.


여기서 생긴 문제는, 이 데이타가 오래된 데이타라서 존재하지 않는 파일이 다수 있었고, 이 경우 파일을 저장하고 있는 사이트에서, 404 Not found와 같은 이미지를 리턴하였기 때문에, 이를 필터링해야 하였고, 같은 사진이 중복되서 오는 문제등이 있었기 때문에,상당량을 일일이 필터링을 해야 했다.


그리고, 사진상에, 여러 얼굴이 있는 이미지가 많았기 때문에, VISION API로 얼굴을 인식해서 얼굴 사진만 잘라낼 요량이었기 때문에, 독사진만을 일일이 보고 골라내야 했다. 나중에 생각해보니 VISION API로 얼굴이 한명만 인식이 되는 사진만 필터링을 했으면 됐을텐데. 불필요한 작업이 많았다.

라벨을 문자열로 쓴 문제

학습 데이타에 대한 라벨을 생성할때, 괜히 가독성을 높힌다고 라벨을 문자열로 해서 각 사람의 이름을 사용하였다.

CNN에서 마지막은 Softmax는 matrix이기 때문에, 라벨 문자열을 나중에 list.indexOf를 이용하여 배열로 변경할 예정이었는데, 파이썬에서는 쉽게 될지 몰라고, 텐서플로우 코드에서는 이 과정이 쉽지 않았다.

그래서..

결국은 라벨 데이타를 문자열이 아니라, 0~44의 int 값으로 재 생성한후에,


   batch_label_on_hot=tf.one_hot(tf.to_int64(batch_label),

       FLAGS.num_classes, on_value=1.0, off_value=0.0)


tf.one_hot 함수를 이용하여, 1*45 행렬로 바뀌어서 사용하였다.

학습용 및 검증용 데이타를 초기에 분류하지 않았던 문제

학습데이타를 준비할때, 학습 데이타를 학습용과 검증용으로 따로 분류를 해놨어야 하는데, 이 작업을 안해서, 결국 모델을 만들다가 다시 학습 데이타를 7:3 비율로 학습 데이타와 검증용 데이타로 분류하는 작업을 진행하였다.

학습 데이타의 분포가 골고르지 못했던 문제

사진을 모으는 과정에서 필터링 되서 버려지는 데이타가 많았고, 원본 데이타 역시 사람별로 사진 수가 고르지 못했기 때문에, 결과적으로 모여진 학습 데이타의 분포가 사람별로 고르지 못했다.

학습데이타가 많은 셀럽은 200~250장, 적은 사람은 50장으로 편차가 컸다.


이로 인해서 첫번째 모델의 학습이 끝난 후에, 모델을 검증해보니, 학습 데이타를 많이 준 사람으로 대부분 분류를 해냈다. 47개의 클래스 약 6000장의 사진으로 5시간 학습 시킨 결과, 예측을 검증하는 과정에서 90%이상을 모두 브래드피트로 인식해내는 문제가 생겼다. (내 맥북이 브레드피트를 좋아하는가??)


그래서 결과적으로 학습데이타와 검증 데이타를 클래스별로 분포를 같게 하기 위해서, 클래스당 약 50 장의 샘플 사진으로 맞춰서 예측 결과가 편중되는 현상을 해결하려고 하였다.

학습 순서가 클래스별로 된 문제

클래스별 학습 데이타의 양을 균일하게 맞췄음에도 불구하고, 모델의 학습 결과가 특정 클래스들로 편향되는 현상이 발생하였다.

이는 학습을 시킬때, 골고루 학습을 시켜야 하는데, 학습 데이타를 순서대로 학습을 시켰기 때문에 발생한 문제이다. 즉 풀어서 말하자면, “브래드 피트"를 20번 학습 시키고, “안젤리나 졸리"를 20분 학습 시키고, “브루스 윌리스”를 20번 학습 시켜서 모델이 첫 학습데이타 쪽으로 편향되는 현상이 발생한것인데, 이를 해결하려면 학습 데이타를 랜덤으로 만들어서 학습시켜야 한다.

예를 들어 “브래드 피트”,”안젤리나 졸리",”브루스 윌리스",”안젤리나 졸리",”브루스 윌리스", ”안젤리나 졸리",“브래드 피트” …. 이런식으로 말이다.

즉 코드 상에서 배치 데이타를 읽어올때 셔플 처리를 하면되는데 이를 위해서 데이타를 읽는 부분을 다음과 같이 변경 하였다.


def get_input_queue(csv_file_name,num_epochs = None):

   train_images = []

   train_labels = []

   for line in open(csv_file_name,'r'):

       cols = re.split(',|\n',line)

       train_images.append(cols[0])

       # 3rd column is label and needs to be converted to int type

       train_labels.append(int(cols[2]) )

                           

   input_queue = tf.train.slice_input_producer([train_images,train_labels],

                                              num_epochs = num_epochs,shuffle = True)

   

   return input_queue


get_input_queue 함수에, csv_file_name을 인자로 주면, 이 파일을 한줄 단위로 읽어서, 첫번째는 파일명, 세번째는 라벨로 읽은 후에, 각각 train_images와  train_lables에 각각 string과 int 형으로 저장한다

그 다음이 배열을 가지고 tf.train.slice_input_producer를 사용하면 배열에서 데이타를 읽어 드리는 input queue 를 생성하는데, 이때 인자로 shuffle = True로 주면 데이타를 리턴 할때 순차적으로 리턴하지 않고 셔플된 형태로 랜덤하게 리턴한다.


def read_data(input_queue):

   image_file = input_queue[0]

   label = input_queue[1]

   

   image =  tf.image.decode_jpeg(tf.read_file(image_file),channels=FLAGS.image_color)

   

   return image,label,image_file


다음으로, 이 큐를 이용하여 이미지 파일명과, 라벨을 읽어서 이미지 파일 데이타(텐서)와 라벨로 읽는 코드를 read_data라는 함수로 구현하였다. 입력값은 input_queue인데, input queue에서 데이타를 읽으면 첫번째는 이미지 파일명, 두번째는 라벨이 되는데, 첫번째 파일명을 tf.image.decode_jpeg함수를 이용하여 텐서로 읽은후, 읽은 이미지 데이타와 라벨을 리턴하였다.


def read_data_batch(csv_file_name,batch_size=FLAGS.batch_size):

   input_queue = get_input_queue(csv_file_name)

   image,label,file_name= read_data(input_queue)

   image = tf.reshape(image,[FLAGS.image_size,FLAGS.image_size,FLAGS.image_color])

   

   batch_image,batch_label,batch_file = tf.train.batch([image,label,file_name],batch_size=batch_size)

                                                      #,enqueue_many=True)

   batch_file = tf.reshape(batch_file,[batch_size,1])


   batch_label_on_hot=tf.one_hot(tf.to_int64(batch_label),

       FLAGS.num_classes, on_value=1.0, off_value=0.0)

   return batch_image,batch_label_on_hot,batch_file


마지막으로, 배치로 데이타를 읽는 함수 부분에서 앞에 정의한 get_input_queue와 read_data 함수를 이용하여 데이타를 shuffle 된 상태로 읽은 후에, tf.train.batch를 이용하여 일정한 개수 (배치) 형태로 리턴하도록 하였다.


그 결과 예측 결과가 한쪽으로 편향되는 현상을 없앨 수 는 있었다.

샘플 데이타의 부족

데이타 편향 현상은 잡았지만, 클래스의 수(45)에 대비하여, 샘플데이타의 수(클래스당 50개)로 부족하여, 학습을 계속 진행해도 cross entropy 함수는 4~7 사이에서 왔다갔다 하면서 더 이상 0으로 수렴하지 않았고 정확도되 0~35% 사이를 왔다갔다 하면서 수렴을 하지 않았다.


그래서, 학습 이미지의 색이나, 방향등을 변경하는 방법으로 데이타를 뻥튀기 하려고 하는데, 이 부분은 아직 작업중.

그외에 자잘한 삽질

모 그외에도 엄청 여러가지 삽질을 하고 있다. 그래도 모델 하나 제대로 만들어봐야 겠다는 생각에 끝까지 우격다짐으로 진행하고 있지만, 학습을 돌다가 스크린 세이버나, 절전 모드로 들어가서 학습이 중단된 사례. 모델을 개발하다가 중간에 텐서 플로우 버전이 올라가서 코드를 수정한 일. 맥에서 개발하다가 윈도우 머신에 GPU로 바꿨더니, 파이썬 2.7이 아니라 파이썬 3.5만 지원을 해서, 2.7 코드를 모두 다시 고친일등.


머신러닝이 과학이나 수학보다 노가다라는데, 몸소 느끼는 중.


구글 클라우드의 대용량 분산 큐 서비스인 Pub/Sub 소개 #2


node.js를 통하여 메세지를 보내고 받기

조대협 (http://bcho.tistory.com)


node.js에서 메세지 보내고 받기


이번 글에서는 node.js를 이용하여 실제로 pub/sub에 메세지를 보내고 받도록 해보자


키 파일 준비 하기

Pub/Sub에 접속하기 위해서는 보안 인증을 위해서 키 파일이 필요하다.

키 파일은 구글 클라우드 콘솔에서, API manager 메뉴로 들어가서 Credential 부분에서 Create Credential을 선택하면 아래와 같은 화면이 나온다.

다음으로, 메뉴에서 Service account key를 선택하여 키를 생성한다.


키가 생성이 되면 json 파일로 다운로드가 된다.

여기서는 편의상 키 파일명을 “pubsub-key.json”이라고 하겠다.


메세지 보내기

node.js를 이용해서 메세지를 보내보자. 먼저 코드를 보자


var gcloud = require('gcloud');

var pubsub = gcloud.pubsub({

projectId:'terrycho-sandbox',

keyFilename: '/Users/terrycho/keys/pubsub-key.json'

});


var topic = pubsub.topic('projects/terrycho-sandbox/topics/repository-changes.default');


for(var i=0;i<3;i++){

topic.publish({

data:{

userId:process.argv[2]+i,

name:'terry.cho'

}

},function(err){

if(err != null) console.log("Error :"+err);

});

};


pub/sub을 사용하려면 구글 클라우드 라이브러리인 gcloud 모듈이 필요하다.

명령어 창에서 npm install gcloud를 이용해서, gcloud모듈을 먼저 인스톨 해놓자.

다음으로, gcloud라이브러리에서 pubsub 객체를 만든다. 여기서는 projectId와, keyFilename을 지정한다.

projectId는 사용하고자 하는 본인의 구글 프로젝트 ID를 넣으면 되고 (여기서는 ‘terrycho-sandbox’), 키 파일은 앞에서 준비한 키 JSON 파일의 경로를 설정하면 된다.


pubsub 객체가 생성되었으면, 메세지를 보낼 topic을 가져와야 한다. topic은 다음과 같은 pubsub.topic메서드를 이용해서 불러올 수 있다. 이때, topic의 경로를 아래와 같이 적어준다.

var topic = pubsub.topic('projects/terrycho-sandbox/topics/repository-changes.default');

여기서 사용한 topic은 projects/terrycho-sandbox/topics/repository-changes.default 이다.

topic을 받아왔으면, 이 topic에 실제로 메세지를 publish하면 되는데, topic.publish( {메세지},{error callback 함수}); 형태로 지정하면 된다.


pub/sub은 앞의 글에서 설명한바와 같이, message와, message attribute 두가지로 분리가 되는데,

message는

data :{

// 여기에 메세지 정의

}


형태로 정의해서 전달하고,message attribute는

attributes:{

  key1:’value1’,

  key2:’value2’
}


형태로 전달한다.

실제 사용예를 보면 다음과 같다.

var registerMessage = {
 data: {
   userId: 3,
   name: 'Stephen',
   event: 'new user'
 },
 attributes: {
   key: 'value',
   hello: 'world'
 }
};


위의 보내기 예제에서는 userId와, name 필드 두개만, 메세지로 3번 보내도록 하였다.

data:{

userId:process.argv[2]+i,

name:'terry.cho'

}

메세지 받기

메세지를 전달하였으면 이제 메세지를 읽어보도록 한다. 전체 코드는 다음과 같다.


var gcloud = require('gcloud');

var pubsub = gcloud.pubsub({

projectId:'terrycho-sandbox',

keyFilename: '/Users/terrycho/keys/pubsub-key.json'

});


var topic = pubsub.topic('projects/terrycho-sandbox/topics/repository-changes.default');

var options = {

 reuseExisting: true, // if the subscription is already exist reuse subscription, option is not changed

 interval:10,

 maxInProgress:5,

 autoAck:false

};


topic.subscribe('nodejs-subscription',options,function(err,subscription,apiResponse){

if(err != null){

console.log('subscription creation failed :'+err);

exit(1);

}

console.log('Subscription :'+subscription);

subscription.on('error',function(err){

console.log('error:'+err);

});


subscription.on('message',function(message){

// read message from queue

console.log(message);

// send ack

subscription.ack(message.ackId,function(err,apiResponse){

console.log('info:sent ack');

});

});

});

topic을 생성하는 것까지는 앞의 메세지 보내기 부분의 코드와 같다.

topic으로 부터 메세지를 받기 위해서, subscription에 대한 옵션을 설정한다.


var options = {

 reuseExisting: true,

 interval:10,

 maxInProgress:5,

 autoAck:false

};

reuseExisting은 별도로 subscription을 생성하지 않고, 기존의 subscription을 사용한다. 이 경우에는, 기존 subscription의 옵션이 그대로 적용되며, 새로운 option이 적용되지 않는다.

interval은 10초 단위로 subscription을 polling 하는 것이고, maxInProgress는 한번에 읽어올 수 있는 메세지 수를 정의한다.

autoAck는 메세지를 보낸 후에, 자동으로 ack를 보내는 옵션인데, 여기서는 false로 하였기 때문에, 수동으로 ack를 보내야 한다.


옵션을 정의하였으면 아래와 같이 topic에 대한 subscription에 대해서, 메세지를 subscription 한다.

topic.subscribe('nodejs-subscription',options,function(err,subscription,apiResponse){

‘nodejs-subscription’은 subscription 이름이고, options는 subscription에 대한 옵션 그리고 마지막은 콜백함수 있다.

메세지를 받았을때 처리 방법을 정의해야 하는데, 앞의 콜백 함수에서 전달되는 subscription객체에 “message”라는 이벤트에 대해서 핸들러를 작성하면 메세지를 받을 수 있다.


아래는 핸들러 코드이다.

subscription.on('message',function(message){

// read message from queue

console.log(message);

// send ack

subscription.ack(message.ackId,function(err,apiResponse){

console.log('info:sent ack');

});

});


메세지가 들어오면 console에 메세지를 출력하고, subscription.ack를 이용하여 ack를 보낸다. 이때, 메세지에서 들어오는 message.ackId를 인자로 하여, 그 메세지에 대해서 ack를 보낸다.


다음은 명령어를 실행하여 메세지를 보내는 실행화면이다.


그리고 다음은 메세지를 받는 프로그램을 수행하여 실제로 메세지를 받은 결과 화면이다.




참고

  • node.js pub/sub 라이브러리 레퍼런스 https://googlecloudplatform.github.io/gcloud-node/#/docs/v0.36.0/pubsub


node.js에서 Redis 사용하기


조대협 (http://bcho.tistory.com)


Redis NoSQL 데이타 베이스의 종류로, mongoDB 처럼 전체 데이타를 영구히 저장하기 보다는 캐쉬처럼 휘발성이나 임시성 데이타를 저장하는데 많이 사용된다.

디스크에 데이타를 주기적으로 저장하기는 하지만, 기능은 백업이나 복구용으로 주로 사용할뿐 데이타는 모두 메모리에 저장되기 때문에, 빠른 접근 속도를 자랑한다.

 

이유 때문에 근래에는 memcached 다음의 캐쉬 솔루션으로 널리 사용되고 있는데, 간단하게 -밸류 (Key-Value)형태의 데이타 저장뿐만 아니라, 다양한 데이타 타입을 지원하기 때문에 응용도가 높고, node.js 호환 모듈이 지원되서 node.js 궁합이 좋다. 여러 node.js 클러스터링 하여 사용할때, node.js 인스턴스간 상태정보를 공유하거나, 세션과 같은 휘발성 정보를 저장하거나 또는 캐쉬등으로 다양하게 사용할 있다.

 

Redis 제공하는 기능으로는 키로 데이타를 저장하고 조회하는 Set/Get 기능이 있으며, 메세지를 전달하기 위한 큐로도 사용할 있다.

 

큐로써의 기능은 하나의 클라이언트가 다른 클라이언트로 메세지를 보내는 1:1 기능뿐 아니라, 하나의 클라이언트가 다수의 클라이언트에게 메세지를 발송하는 발행/배포 (Publish/Subscribe) 기능을 제공한다.




그림 1 RedisPublish/Subscribe의 개념 구조

 

재미있는 것중에 하나는 일반적인 Pub/Sub 시스템의 경우 Subscribe 하는 하나의 Topic에서만 Subscribe하는데 반해서, redis에서는 pattern matching 통해서 다수의 Topic에서 message subscribe 있다.

예를 들어 topic 이름이 music.pop music,classic 이라는 두개의 Topic 있을때, "PSUBSCRIBE music.*"라고 하면 두개의 Topic에서 동시에 message subscribe 있다.

 

자료 구조

 

Redis 가장 기본이 되는 자료 구조를 살펴보자. Redis 다양한 자료 구조를 지원하는데, 지원하는 자료 구조형은 다음과 같다.

1)       String

Key 대해서 문자열을 저장한다. 텍스트 문자열뿐만 아니라 숫자나 최대 512mbyte 까지의 바이너리도 저장할 있다.

 

2)       List

Key 대해서 List 타입을 저장한다. List에는 값들이 들어갈 있으며, INDEX 값을 이용해서 지정된 위치의 값을 넣거나 있고, 또는 push/pop 함수를 이용하여 리스트 앞뒤에 데이타를 넣거나 있다. 일반적인 자료 구조에서 Linked List 같은 자료 구조라고 생각하면 된다.

 

3)       Sets

Set 자료 구조는 집합이라고 생각하면 된다. Key 대해서 Set 저장할 있는데, List 구조와는 다르게 주의할점은 집합이기 때문에 같은 값이 들어갈 없다. 대신 집합의 특성을 이용한 집합 연산, 교집합, 합집합등의 연산이 가능하다.

 

4)       Sorted Set

SortedSet Set 동일하지만, 데이타를 저장할때, value 이외에, score 라는 값을 같이 저장한다. 그리고 score 라는 값에 따라서 데이타를 정렬(소팅)해서 저장한다. 순차성이나 순서가 중요한 데이타를 저장할때 유용하게 저장할 있다.

 

5)       Hashes

마지막 자료형으로는 Hashes 있는데, 해쉬 자료 구조를 생각하면 된다.Key 해쉬 테이블을 저장하는데, 해쉬 테이블에 저장되는 데이타는 (field, value) 형태로 field 해쉬의 키로 저장한다.

키가 있는 데이타를 군집하여 저장하는데 유용하며 데이타의 접근이 매우 빠르다. 순차적이지 않고 비순차적인 랜덤 액세스 데이타에 적절하다.

 

설명한 자료 구조를 Redis 저장되는 형태로 표현하면 다음과 같다.

 



Figure 36 redis의 자료 구조

 

기본적으로 /밸류 (Key/Value) 형태로 데이타가 저장되며, 밸류에 해당하는 데이타 타입은 앞서 언급하 String, List, Sets, SortedSets, Hashes 있다.

 

Redis 대한 설명은 여기서는 자세하게 하지 않는다. 독립적인 제품인 만큼 가지고 있는 기능과 운영에 신경써야할 부분이 많다. Redis 대한 자세한 설명은 http://redis.io 홈페이지를 참고하거나 정경석씨가 이것이 레디스다http://www.yes24.com/24/Goods/11265881?Acode=101 라는 책을 추천한다. 단순히 redis 대한 사용법뿐만 아니라, 레디스의 데이타 모델 설계에 대한 자세한 가이드를 제공하고 있다.

 

Redis 설치하기

개발환경 구성을 위해서 redis 설치해보자.

 

맥의 경우 애플리케이션 설치 유틸리티인 brew 이용하면 간단하게 설치할 있다.

%brew install redis

 

윈도우즈

안타깝게도 redis 공식적으로는 윈도우즈 인스톨을 지원하지 않는다. http://redis.io에서 소스 코드를 다운 받아서 컴파일을 해서 설치를 해야 하는데, 만약에 이것이 번거롭다면, https://github.com/rgl/redis/downloads 에서 다운로드 받아서 설치할 있다. 그렇지만 이경우에는 최신 버전을 지원하지 않는다.

그래서 vagrant 이용하여 우분투 리눅스로 개발환경을 꾸미고 위에 redis 설치하거나 https://redislabs.com/pricing https://www.compose.io  같은 클라우드 redis 환경을 사용하기를 권장한다. ( 클라우드 서비스의 경우 일정 용량까지 무료 또는 일정 기간까지 무료로 서비스를 제공한다.)

 

리눅스

리눅스의 경우 설치가 매우 간단하다. 우분투의 경우 패키지 메니저인 apt-get 이용해서 다음과 같이 설치하면 된다.

%sudo apt-get install redis-server

 

설치가 끝났으면 편하게 redis 사용하기 위해서 redis 클라이언트를 설치해보자.

여러 GUI 클라이언트들이 많지만, 편하게 사용할 있는 redis desktop 설치한다. http://redisdesktop.com/ 에서 다운 받은 후에 간단하게 설치할 있다.

 

이제 환경 구성이 끝났으니, redis 구동하고 제대로 동작하는지 테스트해보자

%redis-server

명령을 이용해서 redis 서버를 구동한다.

 



Figure 37 redis 기동 화면

 

redis desktop 이용해서 localhost 호스트에 Host 주소는 localhost TCP 포트는 6379 새로운 Connection 추가하여 연결한다.

 

 



Figure 38 redis desktop에서 연결을 설정하는 화면

 

연결이 되었으면 redis desktop에서 Console 연다.

 



Figure 39 redis desktop에서 콘솔을 여는 화면

 

Console에서 다음과 같이 명령어를 입력해보자

 

localhost:0>set key1 myvalue

OK

 

localhost:0>set key2 myvalue2

OK

 

localhost:0>get key2

myvalue2

 

localhost:0>

Figure 40 redis desktop에서 간단한 명령을 통해서 redis를 테스트 하는 화면


위의 명령은 key1 myvalue라는 값을 입력하고, key2 myvalue2라는 값을 입력한 후에, key2 입력된 값을 조회하는 명령이다.

 

Redis desktop에서, 디비를 조회해보면, 앞서 입력한 /밸류 값이 저장되어 있는 것을 다음과 같이 확인할 있다.

\


Figure 41 redis에 저장된 데이타를 redis desktop을 이용해서 조회하기

 

node.js에서 redis 접근하기

 

이제 node.js에서 redis 사용하기 위한 준비가 끝났다. 간단한 express API 만들어서 redis 캐쉬로 사용하여 데이타를 저장하고 조회하는 예제를 작성해보자

 

node.js redis 클라이언트는 여러 종류가 있다. http://redis.io/clients#nodejs

가장 널리 쓰는 클라이언트 모듈로는 node-redis https://github.com/NodeRedis/node_redis 있는데, 예제는 node-redis 클라이언트를 기준으로 설명한다.

 

예제는 profile URL에서 사용자 데이타를 JSON/POST 받아서 redis 저장하고, TTL(Time to Leave) 방식의 캐쉬 처럼 10 후에 삭제되도록 하였다.

그리고 GET /profile/{사용자 이름} 으로 redis 저장된 데이타를 조회하도록 하였다.

 

먼저 node-redis 모듈과, json 문서를 처리하기 위해서 JSON 모듈을 사용하기 때문에, 모듈을 설치하자

% npm install redis

% npm install JSON

 

package.json 모듈의 의존성을 다음과 같이 정의한다.

 

 

{

  "name": "RedisCache",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0",

    "redis":"~2.6.0",

    "JSON":"~1.0.0"

  }

}

 

Figure 42 redisJSON 모듈의 의존성이 추가된 package.json

 

다음으로 express 간단한 프로젝트를 만든 후에, app.js 다음과 같은 코드를 추가한다.

 

 

// redis example

var redis = require('redis');

var JSON = require('JSON');

client = redis.createClient(6379,'127.0.0.1');

 

app.use(function(req,res,next){

      req.cache = client;

      next();

})

app.post('/profile',function(req,res,next){

      req.accepts('application/json');

     

      var key = req.body.name;

      var value = JSON.stringify(req.body);

     

      req.cache.set(key,value,function(err,data){

           if(err){

                 console.log(err);

                 res.send("error "+err);

                 return;

           }

           req.cache.expire(key,10);

           res.json(value);

           //console.log(value);

      });

})

app.get('/profile/:name',function(req,res,next){

      var key = req.params.name;

     

      req.cache.get(key,function(err,data){

           if(err){

                 console.log(err);

                 res.send("error "+err);

                 return;

           }

 

           var value = JSON.parse(data);

           res.json(value);

      });

});

 

Figure 43 app.jsredis에 데이타를 쓰고 읽는 부분

 

redis 클라이언트와, JSON 모듈을 로딩한후, createClient 메서드를 이용해서, redis 대한 연결 클라이언트를 생성하자.

 

client = redis.createClient(6379,'127.0.0.1');

 

app.use(function(req,res,next){

      req.cache = client;

      next();

})

 

다음 연결 객체를 express router에서 쉽게 가져다 있도록, 미들웨어를 이용하여 req.cache 객체에 저장하도록 하자.

 

HTTP POST /profile 의해서 사용자 프로파일 데이타를 저장하는 부분을 보면

req.accepts('application/json'); 이용하여 JSON 요청을 받아드리도록 한다.

JSON내의 name 필드를 키로, 하고, JSON 전체를 밸류로 한다. JSON 객체 형태로 redis 저장할 있겠지만 경우 redis에서 조회를 하면 객체형으로 나오기 때문에 운영이 불편하다. 그래서 JSON.stringfy 이용하여 JSON 객체를 문자열로 변환하여 value 객체에 저장하였다.

다음 req.cache.set(key,value,function(err,data) 코드에서 redis 저장하기 위해서 redis 클라이언트를 req 객체에서 조회해온후, set 명령을 이용해서 /밸류 값을 저장한다. 저장이 끝나면 뒤에 인자로 전달된 콜백함수 호출 되는데, 콜백함수에서, req.cache.expire(key,10); 호출하여, 키에 대한 데이타 저장 시간을 10초로 설정한다. (10 후에는 데이타가 삭제된다.) 마지막으로 res.json(value); 이용하여 HTTP 응답에 JSON 문자열을 리턴한다.

 

HTTP GET으로 /profile/{사용자 이름} 요청을 받아서 키가 사용자 이름은 JSON 데이타를 조회하여 리턴하는 코드이다.

app.get('/profile/:name',function(req,res,next) 으로 요청을 받은 , URL에서 name 부분을 읽어서 키값으로 하고,

req.cache.get(key,function(err,data){ 이용하여, 키를 가지고 데이타를 조회한다. 콜백 함수 부분에서, 데이타가 문자열 형태로 리턴되는데, 이를 var value = JSON.parse(data); 이용하여, JSON 객체로 변환한 후에, res.json(value); 통해서 JSON 문자열로 리턴한다.

 

코드 작성이 끝났으면 테스트를 해보자 HTTP JSON/POST REST 호출을 보내야 하기 때문에, 별도의 클라이언트가 필요한데, 클라이언트는 구글 크롬 브라우져의 플러그인인 포스트맨(POSTMAN) 사용하겠다. https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop

 

포스트맨 설치가 끝났으면, 포스트맨에서 HTTP POST/JSON 방식으로 http://localhost:3000/profile 아래와 같이 요청을 보낸다.

 



Figure 44 포스트맨에서 HTTP POSTprofile 데이타를 삽입하는 화면

 

요청을 보낸후 바로 HTTP GET으로 http://localhost:3000/profile/terry 조회를 하면 아래와 같이 앞에서 입력한 데이타가 조회됨을 확인할 있다. 이때 위의 POST 요청을 보낸지 10 내에 조회를 해야 한다. 10초가 지나면 앞서 지정한 expire 의해서 자동으로 삭제가된다.



Figure 45 포스트맨에서 사용자 이름이 terry인 데이타를 조회하는 화면

 

Redisdesktop에서 확인을 해보면 아래와 같이 문자열로 terry 사용자에 대한 데이타가 저장되어 있는 것을 확인할 있다.



Figure 46 redis desktop 에서 입력된 데이타를 확인하는 화면

 

10초후에, 다시 조회를 해보면, terry 키로 가지는 데이타가 삭제된 것을 확인할 있다.

 

지금까지 가장 기본적인 redis 대한 소개와 사용법에 대해서 알아보았다. redis 뒤에 나올 node.js 클러스터링의 HTTP 세션을 저장하는 기능이나, Socket.IO 등에서도 계속해서 사용되는 중요한 솔루션이다. Redis 자체를 다루는 것이 아니라서 자세하게 파고 들어가지는 않았지만, 다소 운영이 까다롭고 특성을 파악해서 설계해야 하는 만큼 반드시 시간을 내서 redis 자체에 대해서 조금 자세하게 살펴보기를 권장한다.

monk 모듈을 이용한 mongoDB 연결


조대협 (http://bcho.tistory.com)


mongoDB 기반의 개발을 하기 위해서 mongoDB를 설치한다. https://www.mongodb.org/ 에서 OS에 맞는 설치 파일을 다운로드 받아서 설치한다.

설치가 된 디렉토리에 들어가서 설치디렉토리 아래 ‘./data’ 라는 디렉토리를 만든다. 이 디렉토리는 mongoDB의 데이타가 저장될 디렉토리이다.

 

mongoDB를 구동해보자.

% ./bin/mongod --dbpath ./data



Figure 1 mongoDB 구동화면


구동이 끝났으면 mongoDB에 접속할 클라이언트가 필요하다. DB에 접속해서 데이타를 보고 쿼리를 수행할 수 있는 클라이언트가 필요한데, 여러 도구가 있지만 많이 사용되는 도구로는 roboMongo라는 클라이언트가 있다.

https://robomongo.org/download 에서 다운로드 받을 수 있다. OS에 맞는 설치 파일을 다운로드 받아서 설치 후 실행한다.

 

설치 후에, Create Connection에서, 로컬호스트에 설치된 mongoDB를 연결하기 위해서 연결 정보를 기술하고, 연결을 만든다





Figure 2 robomongo에서 localhost에 있는 mongodb 연결 추가

 

주소는 localhost, 포트는 디폴트 포트로 27017를 넣으면 된다.

 

환경이 준비가 되었으면 간단한 테스트를 해보자. 테스트 전에 기본적인 개념을 숙지할 필요가 있는데, mongoDBNoSQL 계열중에서도 도큐먼트DB (Document DB)에 속한다. 기존 RDBMS에서 하나의 행이 데이타를 표현했다면, mogoDB는 하나의 JSON 파일이 하나의 데이타를 표현한다. JSON을 도큐먼트라고 하기 때문에, 도큐먼트 DB라고 한다.

 

제일 상위 개념은 DB의 개념에 대해서 알아보자, DB는 여러개의 테이블(컬렉션)을 저장하는 단위이다.

Robomongo에서 mydb라는 이름으로 DB 를 생성해보자



Figure 3 robomongo에서 새로운  DB를 추가 하는 화면

 

다음으로 생성된 DB안에, 컬렉션을 생성한다. 컬렉션은 RDBMS의 단일 테이블과 같은 개념이다.

Robomongo에서 다음과 같이 ‘users’라는 이름의 컬렉션을 생성한다



Figure 4 robomongo에서 컬렉션(Collection) 생성

 

users 컬렉션에는 userid를 키로 해서, sex(성별), city(도시) 명을 입력할 예정인데, userid가 키이기 때문에, userid를 통한 검색이나 소팅등이 발생한다. 그래서 userid를 인덱스로 지정한다.

인덱스 지정 방법은 createIndex 명령을 이용한다. 다음과 같이 robomongo에서 createIndex 명령을 이용하여 인덱스를 생성한다.



Figure 5 users 컬렉션에서 userid를 인덱스로 지정

 

mongoDB는 디폴트로, 각 컬렉션마다 “_id”라는 필드를 가지고 있다. 이 필드는 컬렉션 안의 데이타에 대한 키 값인데, 12 바이트의 문자열로 이루어져 있고 ObjectId라는 포맷으로 시간-머신이름,프로세스ID,증가값형태로 이루어지는 것이 일반적이다.

_id 필드에 userid를 저장하지 않고 별도로 인덱스를 만들어가면서 까지 userid 필드를 별도로 사용하는 것은 mongoDBNoSQL의 특성상 여러개의 머신에 데이타를 나눠서 저장한다. 그래서 데이타가 여러 머신에 골고루 분산되는 것이 중요한데, 애플리케이션상의 특정 의미를 가지고 있는 필드를 사용하게 되면 데이타가 특정 머신에 쏠리는 현상이 발생할 수 있다.

예를 들어서, 주민번호를 _id로 사용했다면, 데이타가 골고루 분산될것 같지만, 해당 서비스가 10~20대에만 인기있는 서비스라면, 10~20대 데이타를 저장하는 머신에만 데이타가 몰리게 되고, 10세이하나, 20세 이상의 데이타를 저장하는 노드에는 데이타가 적게 저장된다.

이런 이유등으로 mongoDB를 지원하는 node.js 드라이버에서는 _id 값을 사용할때, 앞에서 언급한 ObjectId 포맷을 따르지 않으면 에러를 내도록 설계되어 있다. 우리가 앞으로 살펴볼 mongoosemonk의 경우에도 마찬가지이다.

 

이제 데이타를 집어넣기 위한 테이블(컬렉션) 생성이 완료되었다.

다음 컬렉션 에 대한 CRUD (Create, Read, Update, Delete) 를 알아보자

SQL 문장과 비교하여, mongoDB에서 CRUD 에 대해서 알아보면 다음과 같다.

CRUD

SQL

MongoDB

Create

insert into users ("name","city") values("terry","seoul")

db.users.insert({userid:"terry",city:"seoul"})

Read

select * from users where id="terry"

db.users.find({userid:"terry"})

Update

update users set city="busan" where _id="terry"

db.users.update( {userid:"terry"}, {$set :{ city:"Busan" } } )

Delete

delete from users where _id="terry"

db.users.remove({userid:"terry"})

Figure 6 SQL문장과 mongoDB 쿼리 문장 비교


mongoDB에서 쿼리는 위와 같이 db.{Collection }.{명령어} 형태로 정의된다.

roboMongo에서 insert 쿼리를 수행하여 데이타를 삽입해보자



Figure 7 mongoDB에서 users 컬렉션에 데이타 추가

 

다음으로 삽입한 데이타를 find 명령을 이용해 조회해보자



Figure 8 mongoDB에서 추가된 데이타에 대한 확인

 

mongoDB에 대한 구조나 자세한 사용 방법에 대해서는 여기서는 설명하지 않는다.

http://www.tutorialspoint.com/mongodb/ mongoDB에 대한 전체적인 개념과 주요 쿼리들이 간략하게 설명되어 있으니 이 문서를 참고하거나, 자세한 내용은 https://docs.mongodb.org/manual/ 를 참고하기 바란다.

https://university.mongodb.com/ 에 가면 mongodb.com에서 운영하는 온라인 강의를 들을 수 있다. (무료인 과정도 있으니 필요하면 참고하기 바란다.)

 

mongoDBnode.js에서 호출하는 방법은 여러가지가 있으나 대표적인 두가지를 소개한다.

첫번째 방식은 mongoDB 드라이버를 이용하여 직접 mongoDB 쿼리를 사용하는 방식이고, 두번째 방식은 ODM (Object Document Mapper)를 이용하는 방식이다. ODM 방식은 자바나 다른 프로그래밍 언어의 ORM (Object Relational Mapping)과 유사하게 직접 쿼리를 사용하는 것이 아니라 맵퍼를 이용하여 프로그램상의 객체를 데이타와 맵핑 시키는 방식이다. 뒷부분에서 직접 코드를 보면 이해가 빠를 것이다.

 

Monk를 이용한 연결

첫번째로 mongoDB 네이티브 쿼리를 수행하는 방법에 대해서 소개한다. monk라는 node.jsmongoDB 클라이언트를 이용할 것이다.

monk 모듈을 이용하기 위해서 아래와 같이 package.jsonmonk에 대한 의존성을 추가한다.


{

  "name": "mongoDBexpress",

  "version": "0.0.0",

  "private": true,

  "scripts": {

    "start": "node ./bin/www"

  },

  "dependencies": {

    "body-parser": "~1.13.2",

    "cookie-parser": "~1.3.5",

    "debug": "~2.2.0",

    "express": "~4.13.1",

    "jade": "~1.11.0",

    "morgan": "~1.6.1",

    "serve-favicon": "~2.3.0",

    "monk":"~1.0.1"

  }

}

 

Figure 9 monk 모듈에 대한 의존성이 추가된 package.json

 

app.js에서 express가 기동할때, monk를 이용해서 mongoDB에 연결하도록 한다.

var monk = require('monk');

var db = monk('mongodb://localhost:27017/mydb');

 

var mongo = require('./routes/mongo.js');

app.use(function(req,res,next){

    req.db = db;

    next();

});

app.use('/', mongo);

Figure 10 monk를 이용하여 app.js에서 mongoDB 연결하기

 

mongoDB에 연결하기 위한 연결 문자열은 'mongodb://localhost:27017/mydb' mongo://{mongoDB 주소}:{mongoDB 포트}/{연결하고자 하는 DB} 으로 이 예제에서는 mongoDB 연결을 간단하게 IP,포트,DB명만 사용했지만, 여러개의 인스턴스가 클러스터링 되어 있을 경우, 여러 mongoDB로 연결을 할 수 있는 설정이나, Connection Pool과 같은 설정, SSL과 같은 보안 설정등 부가적인 설정이 많으니, 반드시 운영환경에 맞는 설정으로 변경하기를 바란다. 설정 방법은 http://mongodb.github.io/node-mongodb-native/2.1/reference/connecting/connection-settings/ 문서를 참고하자.

 

이때 주의깊게 살펴봐야 하는 부분이 app.use를 이용해서 미들웨어를 추가하였는데, req.dbmongodb 연결을 넘기는 것을 볼 수 있다. 미들웨어로 추가가 되었기 때문에 매번 HTTP 요청이 올때 마다 req 객체에는 db라는 변수로 mongodb 연결을 저장해서 넘기게 되는데, 이는 HTTP 요청을 처리하는 것이 router에서 처리하는 것이 일반적이기 때문에, routerdb 연결을 넘기기 위함이다. 아래 데이타를 삽입하는 라우터 코드를 보자

 

router.post('/insert', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

     

      db = req.db;

      db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Inserted");

            

         });

});

Figure 11 /routes/mongo.js 에서 데이타를 삽입하는 코드


req 객체에서 폼 필드를 읽어서 userid,sex,city등을 읽어내고, 앞의 app.js 에서 추가한 미들웨어에서 넘겨준 db 객체를 받아서 db.get('users').insert({'userid':userid,'sex':sex,'city':city},function(err,doc) 수행하여 데이타를 insert 하였다.

 

다음은 userid필드가 HTTP 폼에서 넘어오는 userid 일치하는 레코드를 지우는 코드 예제이다. Insert 부분과 크게 다르지 않고 remove 함수를 이용하여 삭제 하였다.


router.post('/delete', function(req, res, next) {

      var userid = req.body.userid;

     

      db = req.db;

      db.get('users').remove({'userid':userid},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Removed");

            

         });

});

Figure 12 /routes/mongo.js 에서 데이타를 삭제하는 코드

 

다음은 데이타를 수정하는 부분이다. Update 함수를 이용하여 데이타를 수정하는데,

db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

와 같이 ‘userid’userid 인 필드의 데이타를 },{'userid':userid,'sex':sex,'city':city} 대치한다.

 

router.post('/update', function(req, res, next) {

      var userid = req.body.userid;

      var sex = req.body.sex;

      var city = req.body.city;

      db = req.db;

      db.get('users').update({userid:userid},{'userid':userid,'sex':sex,'city':city},function(err,doc){

      //db.get('users').update({'userid':userid},{$set:{'sex':'BUSAN'}},function(err,doc){

             if(err){

                console.log(err);

                res.status(500).send('update error');

                return;

             }

             res.status(200).send("Updated");

            

         });

});

Figure 13 /routes/mongo.js 에서 데이타를 수정하는 코드


전체 레코드를 대치하는게 아니라 특정 필드만 수정하고자 하면, $set: 쿼리를 이용하여, 수정하고자하는 필드만 아래와 같이 수정할 수 있다.

db.collection('users').updateOne({_id:userid},{$set:{'sex':'BUSAN'}},function(err,doc){

 

마지막으로 데이타를 조회하는 부분이다. /list URL은 전체 리스트를 리턴하는 코드이고, /get ?userid= 쿼리 스트링으로 정의되는 사용자 ID에 대한 레코드만을 조회해서 리턴한다.

router.get('/list', function(req, res, next) {

      db = req.db;

      db.get('users').find({},function(err,doc){

           if(err) console.log('err');

           res.send(doc);

      });

});

router.get('/get', function(req, res, next) {

      db = req.db;

      var userid = req.query.userid

      db.get('users').findOne({'userid':userid},function(err,doc){

           if(err) console.log('err');

           res.send(doc);

      });

});

Figure 14 /routes/mongo.js 에서 데이타를 조회하는 코드

 

이제 /routes/mongo.js 의 모든 코드 작업이 완료되었다. 이 코드를 호출하기 위한 HTML 폼을 작성하자.

 

<!DOCTYPE html>

<html>

<head>

<meta charset="UTF-8">

<title>Insert title here</title>

</head>

<body>

 

<h1> Native MongoDB Test Example</h1>

<form method='post' action='/insert' name='mongoform' >

      user <input type='text' size='10' name='userid'>

      <input type='submit' value='delete' onclick='this.form.action="/delete"' >

      <input type='button' value='get' onclick='location.href="/get?userid="+document.mongoform.userid.value' >

      <p>

      city <input type='text' size='10' name='city' >

      sex <input type='radio' name='sex' value='male'>male

      <input type='radio' name='sex' value='female'>female

      <p>

      <input type='submit' value='insert' onclick='this.form.action="/insert"' >

      <input type='submit' value='update' onclick='this.form.action="/update"' >

      <input type='button' value='list'  onclick='location.href="/list"' >

     

</form>

</body>

</html>

Figure 15 /public/monksample.html

 

node.js를 실행하고 http://localhost:3000/monksample.html 을 실행해보자



Figure 16 http://localhost:3000/monksample.html 실행 결과

 

아래 insert 버튼을 누르면, 채워진 필드로 새로운 레코드를 생성하고, update 버튼은 user 필드에 있는 사용자 이름으로된 데이타를 업데이트 한다. list 버튼은 컬렉션에서 전체 데이타를 조회해서 출력하고, delete 버튼은 user 필드에 있는 사용자 이름으로된 레코드를 삭제한다. get 버튼은 user 필드에 있는 사용자 이름으로 데이타를 조회하여 리턴한다.

다음은 list로 전체 데이타를 조회하는 화면이다.

 


Figure 17 /list를 수행하여 mongoDB에 저장된 전체 데이타를 조회하는 화면


이 코드의 전체 소스코드는 https://github.com/bwcho75/nodejs_tutorial/tree/master/mongoDBexpress 에 있으니 필요하면 참고하기 바란다


다음 글에서는  node.js의 mongoDB ODM 프레임웍인 mongoose 이용한 접근 방법에 대해서 알아보기로 한다.



안드로이드 채팅 UI 만들기 #2 


나인패치 이미지를 이용한 채팅 버블


조대협 (http://bcho.tistory.com)


지난 글에서는 ListView를 이용하여 스크롤이 가능한 텍스트 기반의 간단한 채팅창을 만들어보았다.

이번글에는 채팅 메세지에 이미지로 채팅 버블을 입히는 방법을 알아보도록 한다.


채팅 버블 이미지를 입히는 방법이나 원리는 간단한데, 채팅 메세지를 출력하는 TextView에 백그라운드이미지를 입혀서 출력하면 된다. 그런데 여기서 문제가 생기는데, 채팅 메세지 버블의 크기는 메세지의 글자수에 따라 가변적으로 변경되기 때문에, 일반적인 이미지를 백그라운드로 넣어서 가로로 늘이거나 줄이게 되면 채팅창이 이상하게 가로로 늘어날 수 가 있다.. (아래 그림에서 가로로 늘렸을때 말꼬리 부분 삼각형이 원본과 다르게 늘어난것을 확인할 수 있다) 



< 원본 이미지 > 



<가로로 늘린 이미지 >



그래서 필요한 것이, 특정 부분만 늘어나게 하는 것이 필요한데. 이렇게 크기가 변경되어도 특정 구역만 늘어나게 하는 이미지를 나인패치 이미지 (9-patch image라고 한다.). 나인패치 이미지를 이용하여 말풍선을 느리게 되면, 말꼬리 부분은 늘어나지 않고 텍스트가 들어가는 영역만 늘어난다. 




< 나인패치 이미지를 가로로 늘린 경우> 


나인패치 이미지 만들기


나인 패치 이미지는 안드로이드 SDK에 내장된 draw9patch라는 도구를 이용해서 만들 수 있다.

보통 안드로이드 SDK 가 설치된 디렉토리인 ~/sdk/tools 아래 draw9patch라는 이름으로 저장되어 있다.

실행하면 아래와 같은 화면이 뜨는데, 

좌측은 일반 이미지를 나인패치 이미지로 만들기 위해서 늘어나는 영역을 지정하는 부분이고 (작업영역), 우측은 가로, 세로등으로 늘렸을때의 예상 화면 (프리뷰)을 보여주는 화면이다.




그러면 9 patch  이미지는 어떻게 정의가 될까? draw9patch에서 가이드 선을 드래그해서 상하좌우 4면에, 가이드 선을 지정할 수 있다.



좌측은 세로로 늘어나는 영역, 상단을 가로로 늘어나는 영역을 정의하고, 우측은 세로로 늘어났을때 늘어나는 부분에 채워지는 이미지를, 하단은 가로로 늘어났을때 채워지는 이미지 영역을 지정한다. 

이렇게 정의된 나인 패치 이미지는 어떻게 사용하는가? 일반 이미지 처럼 사용하면 되고, 크기를 조정하면 앞서 정의한데로, 늘어나는 부분만 늘어나게 된다.


나인패치 이미지를 채팅 메세지에 적용하기 


그러면 이 나인패치이미지를 앞에서 만든 채팅 리스트 UI에 적용하여 말 풍선을 만들어보도록 하자.

여기서는 채팅 버블을 좌측 우측용 양쪽으로 만들도록 하고, 서버에 연결된 테스트용이기 때문에, 메세지를 입력하면 무조건 좌/우 버블로 번갈아 가면서 출력하도록 한다.


앞의 코드 (http://bcho.tistory.com/1058) 에서 별도로 바위는 부분은 ChatMessageAdapter의 getView 메서드만 아래와 같이 수정하고 채팅 버블로 사용할 이미지를 ~/res/drawable/ 디렉토리 아래 저장해 놓으면 된다. 




그러면 수정된 getView 메서드를 살펴보도록 하자.


   @Override

    public View getView(int position, View convertView, ViewGroup parent) {

        View row = convertView;

        if (row == null) {

            // inflator를 생성하여, chatting_message.xml을 읽어서 View객체로 생성한다.

            LayoutInflater inflater = (LayoutInflater) this.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);

            row = inflater.inflate(R.layout.chatting_message, parent, false);

        }


        // Array List에 들어 있는 채팅 문자열을 읽어

        ChatMessage msg = (ChatMessage) msgs.get(position);


        // Inflater를 이용해서 생성한 View에, ChatMessage를 삽입한다.

        TextView msgText = (TextView) row.findViewById(R.id.chatmessage);

        msgText.setText(msg.getMessage());

        msgText.setTextColor(Color.parseColor("#000000"));


        // 9 패치 이미지로 채팅 버블을 출력

        msgText.setBackground(this.getContext().getResources().getDrawable( (message_left ? R.drawable.bubble_b : R.drawable.bubble_a )));


        // 메세지를 번갈아 가면서 좌측,우측으로 출력

        LinearLayout chatMessageContainer = (LinearLayout)row.findViewById(R.id.chatmessage_container);

        int align;

        if(message_left) {

            align = Gravity.LEFT;

            message_left = false;

        }else{

            align = Gravity.RIGHT;

            message_left=true;

        }

        chatMessageContainer.setGravity(align);

        return row;


    }


수정 내용은 간단한데, 좌측/우측용 채팅 버블을 출력하는 부분과, 좌측 버블은 좌측 정렬을, 우측 버블은 우측 정렬을 하는 내용이다.


채팅 버블을 적용하는 방법은 TextView에서 간단하게 setBackground 메서드를 이용하여 백그라운드 이미지를 나인패치 이미지를 적용하면 된다.


msgText.setBackground(this.getContext().getResources().getDrawable( (message_left ? R.drawable.bubble_b : R.drawable.bubble_a )));


나인패치이미지가  resource아래 drawable 아래 저장되어 있기 때문에, getResource().getDrawable() 메서드를  이용하여 로딩 한다.


        LinearLayout chatMessageContainer = (LinearLayout)row.findViewById(R.id.chatmessage_container);

        int align;

        if(message_left) {

            align = Gravity.LEFT;

            message_left = false;

        }else{

            align = Gravity.RIGHT;

            message_left=true;

        }

        chatMessageContainer.setGravity(align);


다음으로는 채팅 버블을 번갈아 가면서 좌/우측에 위치 시켜야 하는데, 채팅 버블의 위치는 채팅 메세지를 담고 있는 LinearLayout을 가지고 온후에, LinearLayout의 Gravity를 좌우로 설정하면 된다.


나인패치 이미지를 이용한 완성된 채팅 버블 UI는 다음과 같다.