eincs2014-10-10T18:30:05+09:00http://eincs.comeincsroth2520@gmail.com구글의 데이터 처리 기술은 얼마나 대단할까?2013-08-05T10:00:00+09:00http://http://eincs.com/2013/08/why-google-is-great-tech-company<p><img class="left" style="width:100px;" src="http://eincs.com/images/2013/08/why-google-is-great-tech-company-title.png" />
구글은 정말 대단한 회사이다. <a href="http://www.google.com/about/company/">구글의 미션</a>은 “세상의 정보를 체계화하여 누구나 편리하게 이용할 수 있도록 하는 것”이며 이를 위해 다양한 서비스를 제공한다.
세상의 거의 모든 데이터를 자신들의 데이터센터에 모으기 위해 노력하고 있으며 다양한 서비스를 통해 원하는 정보에 쉽게 접근할 수 있게 해준다.
이를 위해 다양한 기술을 발전 시켰으며 이제는 거대한 데이터를 처리하여 웹서비스로 제공하는 능력만큼은 어떤 회사도 따라 갈 수 없을 정도가 되었다.
이 글은 <strong>구글이 데이터 처리 능력이 얼마나 위대한지 생각을 정리</strong>해보기 위한 글이다.</p>
<h2>구글의 논문으로부터 시작된 빅데이터 기술들</h2>
<p>요즘 빅데이터 기술이 많은 각광을 받고 있다. IT에 관심이 있다면 빅데이터라는 단어와 함께 <a href="http://hadoop.apache.org/">하둡</a>이라는 오픈소스에 대해 언급하는 것을 많이 접했을 것이다.
엄청나게 많은 데이터를 처리하고 분석하기 위해 실제로 가장 널리 사용되는 솔루션이 하둡이며 <a href="http://hadoop.apache.org/docs/stable/hdfs_user_guide.html">HDFS</a>, <a href="http://hadoop.apache.org/docs/stable/mapred_tutorial.html">MapReduce</a>, <a href="http://hbase.apache.org/">HBase</a>와 그 외 여러가지 다른 솔루션들을 제공한다.
이런 기술들이 실제 서비스에 적용되어 사용하기 시작한지 그다지 오래되지 않았고 이제야 널리 상용화 되는 단계라고 볼 수있다.
이 기술들은 <strong>구글이 발표한 논문들로부터 시작</strong>되었다. 더욱 놀라운 것은 이 논문들이 발표한 시점에 <strong>구글은 이미 상용에서 널리 사용</strong>하고 있었다는 것이다.</p>
<ul>
<li><strong><a href="http://hadoop.apache.org/docs/stable/hdfs_user_guide.html">HDFS</a></strong>: Hadoop File System은 대용량 데이터를 저장하기 위한 분산 파일시템이다.
거대한 데이터를 여러 컴퓨터에 나누어 저장하는 파일시스템 레이어를 제공하여 데이터의 양이나 시스템의 연산 능력에 선형확장성을 부여한다.
HDFS는 구글이 2003년에 발표한 <strong>Google File System (<a href="http://research.google.com/archive/gfs.html">GFS</a>)</strong>을 클론한 것이다. </li>
<li><strong><a href="http://hadoop.apache.org/docs/stable/mapred_tutorial.html">MapReduce</a></strong>: MapReduce는 큰 태스크를 분산시스템에서 효과적으로 처리하기 위한 기술이다.
비교적 간단한 연산을 구현하면 수 많은 컴퓨터를 이용하여 일을 분산 처리할 수 있도록 해준다.
2004년에 구글이 같은 이름으로 낸 논문이 있으며 하둡의 MapReduce는 구글의 <strong><a href="http://research.google.com/archive/mapreduce.html">MapReduce</a></strong>를 클론한 것이다.</li>
<li><strong><a href="http://hbase.apache.org/">HBase</a></strong>: HBase는 HDFS상에서 구현된 NoSQL이다. 여러 업체에서 제한된 용도로 사용된다. (<a href="http://appbetween.us/">비트윈</a>에서는 메인 데이터베이스로 사용한다)
HBase 또한 구글의 2006년에 발표한 <strong><a href="http://research.google.com/archive/bigtable.html">BigTable</a></strong>을 클론한것이다.</li>
</ul>
<p>위에서 언급된 하둡 솔루션들은 그에 대응하는 구글의 구현체와 설계가 매우 유사하다. 이 기술들이 개발이 시작된 시기도 구글이 논문을 발표한 시기 이후다.
실제로 <a href="http://en.wikipedia.org/wiki/Doug_Cutting">Doug Cutting</a>이 <a href="http://nutch.apache.org/">Nutch</a>라는 오픈소스 검색엔진 크롤러를 만들다가, 구글에서 발표한 GFS, MapReduce 논문을 보고 데이터를 저장하고 처리하는 부분을 만들었는데,
나중에 이 부분을 Yahoo!와 함께 따로 분리하여 시작된 프로젝트가 바로 하둡이다.</p>
<p>그런데 구글의 논문들의 발표 시기를 살펴보면 지금으로부터 10년이나 된 것도 있다.
그런데 논문을 읽어보면 구글이 구현 후 서비스에 적용하여 어느 정도 돌려보고 실제 사용성이 입증되고 나서 발표한 것이라는 것을 알 수 있다.
GFS 논문에는 GFS를 구글 서비스 백엔드에 적용하면서 많은 문제에 직면했었고 오랜시간동안 Linux코드를 고쳐가면서 적용했다는 이야기가 있다.
MapReduce 논문에는 그 당시에 이미 수 많은 MapReduce 프로그래밍 매일 매일 구글 클러스터에서 실행되고 있다는 내용이 있다.
BigTable논문의 경우 2년 반동안 디자인 개발 하고 실제 서비스에 배포까지 했었다는 이야기가 있다.
구글이 이 논문들을 발표한 시점에 구글은 이미 이 기술들을 서비스에 적용할 정도로 완성도 있었다는 것이다.
다시 말해, 하둡은 <strong>구글은 이미 서비스에 적용한 기술</strong>들의 논문을 보고 시작한 것이며, 구글이 아닌 다른 곳에서는 이제서야 본격적으로 사용을 시작하고 있는 것이다.</p>
<h2>구글의 막강한 NoSQL 기술들</h2>
<p>일반적인 웹 서비스를 구현할때에는 RDBMS가 널리 사용된다.
트래픽이 많은 글로벌 서비스들도 <a href="http://eincs.com/2012/06/nosql-is-not-useful/">NoSQL과 같은 분산데이터베이스를 메인 서비스 개발에 사용하지 않는다</a>.
여러가지 이유가 있겠지만 NoSQL이 트랜잭션이나 인덱싱이과 같은 기능들이 제대로 제공되지도 않아 어플리케이션 개발에 어려운 점이 많고,
신경써서 개발하면 RDBMS로도 확장성 있는 시스템을 충분히 개발할 수 있기 때문이다.
일반적인 NoSQL에서는 Row단위의 ACID 정도만 제공하는 것이 일반적이므로, 복잡한 서비스를 개발하기에는 불편한 점이 많다.</p>
<p>하지만 NoSQL과 같이 높은 확장성과 안정성을 보장하면서도 RDBMS 처럼 트랜잭션이나 인덱싱 같은 어플리케이션 개발에 유용한 기능이 있는 데이터베이스가 있다면 정말 좋을 것이다.
어플리케이션 레벨에서 확장성에 대한 신경쓰지 않아도 되고 일반적인 어플리케이션 구현에도 사용할 수 있을 것이다.
이런 데이터베이스를 만들어내기 위한 방법은 여러가지가 있을 수 있겠지만, 그 중 하나는 NoSQL상에서 트랜잭션이나 인덱스를 구현하는 것이다.
이를 위해 <strong>HBase상에서 트랜잭션을 구현하기 위한 여러 노력들이있었지만 대부분 실패</strong>했다고 볼 수 있다.
HBase에서 트랜잭션을 제공하기 위한 프로젝트인 <a href="http://staltz.blogspot.kr/2012/10/hacid-multi-row-transactions-in-hbase.html">HAcid</a>나 <a href="http://www.scpe.org/index.php/scpe/article/view/715">HBaseSI</a>는 절대적인 성능이 크게 떨어질뿐만 아니라 선형 확장성도 없으므로 실제 서비스 구현에 사용할 수 없다.
아마존의 <a href="https://github.com/awslabs/dynamodb-transactions">DynamoDB에도 트랜잭션 구현체</a>가 있지만 성능이 매우 떨어지고 실제 서비스에 사용할 정도인지는 잘 모르겠다.
결국 많은 업체에서 메인 데이터베이스로 RDBMS를 이용하고 데이터를 잘 샤딩하는 방법으로 확장성있는 시스템을 만드는 방법을 채택한다.
하지만 <strong>구글은 이미 오래전에 이 문제를 해결</strong>하였다.</p>
<p><strong><a href="http://research.google.com/pubs/pub36726.html">Percolator</a></strong>는 구글이 2010년에 발표한 BigTable상에서 돌아가는 트랜잭션 프로젝트이다. 검색결과 인덱스를 점진적으로 업데이트 하기 위해 Percolator를 구현 하였다.
BigTable도 일종의 NoSQL이며 분산데이터베이스이므로, NoSQL에서 동작하는 트랜잭션을 구현했다고 보면 된다.
Percolator 또한 앞서 살펴본 프로젝트들 처럼 논문이 발표된 시점에 이미 실제 돌아가는 서비스에 적용하여 사용 중 이었다.
Percolator를 실제로 적용봤더니, MapReduce를 통해 배치로 인덱스를 업데이트했던 것에 비해 인덱스된 문서의 평균 나이가 50%정도로 줄어들었다는 내용이 있다.
구글은 이미 수 년 전부터 <strong>NoSQL상에서 트랜잭션을을 구현하여 실제 서비스에 적용</strong>하여 사용하고 있는 것이다.</p>
<p><strong><a href="https://developers.google.com/datastore/">Datastore</a></strong>를 보면 더욱 대단하다. <a href="https://developers.google.com/datastore/">Datastore</a>는 구글의 <a href="https://developers.google.com/appengine/">App Engine</a>이나 <a href="https://cloud.google.com/products/compute-engine/">Compute Engine</a>에서 사용이 가능한 데이터 저장소 서비스이다.
엔티티 모델을 제공하며 <strong>트랜잭션 뿐만 아니라 인덱싱 기능도 제공</strong>한다. 게다가 지리적으로 떨어진 <strong>여러 데이터센터 간 복제</strong> 저장이 되므로 높은 안정성을 보장한다.
이것은 구글이 2011년에 발표한 <strong><a href="http://research.google.com/pubs/pub36971.html">Megastore</a></strong> 위에서 구현 된 것 이 Megastore가 트랜잭션과 여러 데이터센터간 복제를 담당하는 것이다.
Datastore가 Megastore상에서 구현되었다는 것은 <a href="http://googledevelopers.blogspot.kr/2013/05/get-started-with-google-cloud-datastore.html">구글 개발자 블로그 글</a>에서 확인할 수 있으며,
2013년 Google I/O에서의 <a href="http://commondatastorage.googleapis.com/io-2013/presentations/4050%20-%20Google%20Cloud%20Datastore%20codelab%20(2).pdf">발표 자료</a>에서도 언급되어 있다.
구글 Datastore는 오래전부터 <a href="https://developers.google.com/appengine/docs/java/datastore/">App Engine상에서 사용</a>에서 사용이 가능했으며,
최근에는 <a href="https://developers.google.com/datastore/docs/activate">Compute Engine에서도 사용 가능</a>하게 되었다.
구글은 이미 이런 기술들을 사내에서 널리 쓰고 있을 뿐만 아니라 App Engine이나 Compute Engine에서 <strong>서비스로 제공할 정도의 수준</strong>인 것이다.</p>
<h2>차원이 다른 기술적인 우위</h2>
<p>지금 까지만 정리한 내용만해도 구글의 기술력은 엄청나다. 그런데 구글은 이보다도 이미 더욱 진보된 기술을 개발하여 사용하고 있다.
구글에서 비교적 최근에 공개한 Colossus와 Spanner는 현재 구글의 기술이 구글이 아닌 외부의 분산시스템 기술들과 얼마나 큰 격차가 나는지 여실히 보여주는 좋은 예이다.</p>
<p><strong><a href="http://www.highlyscalablesystems.com/3306/storage-architecture-and-challenges/">Colossus</a></strong>는 GFS의 단점을 보완하고자 만든 <strong>새로운 버전의 GFS</strong>이다.
Colossus는 2009년 <a href="http://research.google.com/people/jeff/">Jeff Dean</a>의 <a href="http://www.cs.cornell.edu/projects/ladis2009/talks/dean-keynote-ladis2009.pdf">프리젠테이션</a>에서 처음으로 잠깐 언급되었고,
2010년 Faculty Summit에서 발표된 Andrew Fikes의 <a href="http://www.highlyscalablesystems.com/3306/storage-architecture-and-challenges/">프리젠테이션</a>에 특성에 대해 간단히 소개 된적이 있었다.
GFS와 다른 가장 큰 특징은 <strong>메타데이터를 샤딩하여 여러 컴퓨터에 나누어 분산 저장</strong>할 수 있게 되었다는 것인데,
이는 매우 <strong>큰 규모로의 확장을 가능</strong>하게 해주며, 좀 더 작은 파일을 사용할 수 있게 하여 <strong>어플리케이션 작성을 좀 더 단순하게 할 수 있도록</strong> 해준다.
이에 반해, HDFS에서는 하나의 중앙 서버에서 메모리상에 메타데이터를 관리하는데, 이에 대한 <a href="http://static.usenix.org/publications/login/2010-04/openpdfs/shvachko.pdf">한계에 대해 서술한 글</a>이 있다.
이 글에서는 데이터량이 커져 파일이 굉장히 많아지고, 클라이이언트가 매우 많아지는 경우, 단일 네임노드 아키텍처가 갖는 한계에 대해 잘 서술되어 있는데, 이는 Colossus에서는 해결된 문제이다.
페이스북의 메시징 시스템에서는 이 문제를 <a href="http://mvdirona.com/jrh/talksandpapers/kannanmuthukkaruppan_storageinfrabehindmessages.pdf">여러 HDFS/HBase클러스터</a>를 두는 것으로 해결 하였지만, 구글은 시스템 자체를 고친 것이다.
또한 Colossus에서 제공하는 <strong><a href="http://en.wikipedia.org/wiki/Reed%E2%80%93Solomon_error_correction">Reed-Solomon error correction</a></strong>은 페이스북이 2010년부터 <a href="http://www.slideshare.net/ydn/hdfs-raid-facebook">HDFS Raid에 적용하려고 노력</a>하고 있지만
아직 이슈(<a href="https://issues.apache.org/jira/browse/MAPREDUCE-1969">MAPREDUCE-1969</a>)는 해결되지 못했고 오픈 상태이다.</p>
<p><strong><a href="http://research.google.com/archive/spanner.html">Spanner</a></strong>는 2012년에 발표된 구글의 새로운 분산 데이터베이스이다. 하지만 Spanner에 대한 첫 언급은 Jeff Dean의 2009년 <a href="http://www.cs.cornell.edu/projects/ladis2009/talks/dean-keynote-ladis2009.pdf">프리젠테이션</a>에서 였다.
역시 구글은 발표하기 한참 전부터 Spanner를 개발하고 있었던 것이다. Spanner는 Colossus상에서 구현된 데이터베이스인데, 지리적으로 멀리 떨어진 <strong>여러 데이터센터간 운영</strong>이 가능하다.
여러 데이터센터간 운영되는 분산 환경에서, 높은 수준의 트랜잭션 뿐만 아니라 <strong>Semi-Relational 모델</strong>과 <strong>일종의 테이블 구조</strong>, <strong>SQL을 확장한 질의 언어</strong>까지도 제공한다.
일반적으로 NoSQL에서 데이터 모델을 어떻게 가져가느냐가 큰 문제 중 하나인데, 앞서 소개한 <a href="https://developers.google.com/datastore/">Datastore</a>에서는 부모/자식 관계를 가지는 엔티티 모델을 제공하여 이를 해결하였다.
<a href="http://appbetween.us/">비트윈</a>에서 HBase를 사용할때에도 Row-Column을 추상화하여 Tree형태의 데이터 구조 API를 만들어 사용하는데, 구글의 Spanner에서는 이를 한참 뛰어넘은 것이다.
Spanner에 대한 자세한 설명은 <a href="http://helloworld.naver.com/helloworld/216593">네이버의 개발자 블로그 글</a>에서도 다룬적이 있다.
이 것 또한 이미 상용 서비스에 적용하여 사용하고 있는데, 이미 광고 플랫폼에 적용되어 있으며, Gmail, Google 캘린더에 적용할 예정이라고 한다.</p>
<h2>결론</h2>
<p>구글은 정말 대단한 회사이다. 보시다시피 다른 회사에 비해 엄청나게 뛰어난 데이터 처리 기술을 가지고 있다.
특히 지리적으로 멀리 떨어진 여러 데이터센터간 분산 저장 하는 것은 다른 업체에서는 거의 하지 못하는 일이다.
<a href="http://cassandra.apache.org/">Cassandra</a>를 이용하여 <a href="http://www.datastax.com/dev/blog/deploying-cassandra-across-multiple-data-centers">여러 데이터센터간 배포</a>하는 경우가 있기는 하지만,
<strong>Strong Consistency를 만족시키면서 높은 수준의 트랜잭션을 제공하면서것과는 엄청난 차이</strong>가 있다.
이 처럼 다른 곳에서는 넘어서지 못한 레벨의 기술들을 고안하여 구현한지는 오래고 이미 상용 서비스에 적용하여 사용하고 있는것이 매우 놀랍다.
구글이 뛰어나기도 했지만 <strong>이런 해결책들이 필요한 문제를 많이 맞닥뜨리는 환경을 겪어왔기 때문</strong>에 크게 발전 할 수 있었던 것 같다.
그리고 구글이 뛰어난 기술을 가질 수 있었던 것은 문제가 생겼을때 <strong>기술적으로 정말로 올바른 방법을 찾기위해 지속적인 노력</strong>을 계속해왔기 때문이라고 생각한다.
물론 모두가 바퀴가 없다고 해서 바퀴를 만들 필요는 없다. 이미 존재하는 바퀴를 잘 조합해서 잘 돌아가는 시스템을 만드는 것이 옳은 방향인 경우가 대부분이며 뛰어난 능력이기도 하다.
하지만 좀 더 <strong>미래지향적인 방법을 선택하여 새로운 기술을 만들고 실제 서비스에 적용하여 성공</strong>시킬 수 있다면 정말로 뛰어난 기술지향회사라고 볼 수 있을 것이다.
뭐, 구글 처럼 돈 많이 벌면 이런 것에 투자할 여력도 많겠지. 역시나 결론은 구글 찬양.</p>
CAP Theorem, 오해와 진실2013-07-15T10:00:00+09:00http://http://eincs.com/2013/07/misleading-and-truth-of-cap-theorem<p><a href="http://en.wikipedia.org/wiki/CAP_theorem">CAP Theorem</a>은 분산시스템에서 일관성(Consistency), 가용성(Availability), 분할 용인(Partition tolerance)이라는 세 가지 조건을 모두 만족할 수 없다는 정리로, 세 가지 중 두 가지를 택하라는 것으로 많이 알려졌다.
특히 NoSQL이 인기를 끌면서 널리 퍼졌지만, <strong>정리 자체가 제대로 이해하기 어렵게 정의되어 있어서 이해하기가 쉽지 않다</strong>.
그 때문인지 VoltDB 개발자 스스로 VoltDB를 CA로 말하기도 했고, <a href="http://en.wikipedia.org/w/index.php?title=Membase&oldid=384896988">Couchbase가 위키피디아에 CA로 등재</a>되었다가 <a href="http://en.wikipedia.org/wiki/Couchbase_Server">나중에 CP로 수정</a>되는 사태가 발생할 정도이니 말 다했다.
그동안 이 문제에 대해 많은 논의가 이루어졌는데, 이 글에서는 이에 대해 다뤄보려고 한다.</p>
<figure>
<img src="http://eincs.com/images/2013/06/truth-of-cap-theorem-diagram.png" title="Diagram of CAP Theorem" alt="Diagram of CAP Theorem" />
<figcaption>CAP Theorem을 소개할 때 널리 사용되는 다이어그램이지만, 그림 자체가 틀렸을 뿐만 아니라 이 정리를 잘못 이해하기 쉽게 만든다. 이에 대해서는 글에서 차차 설명될 것이다.</figcaption>
</figure>
<h2>CAP Theorem, 무엇이 오해이고 무엇이 진실인가?</h2>
<p>CAP는 분산시스템 설계에서 기술적인 선택을 돕는다.
분산시스템이 모든 것 특성을 만족시킬 수 없음을 인지하게 하고 선택지를 제공해주는 것이다.
하지만 CAP를 명확하게 이해하기 위해서는 다음과 같은 문제들이 해결되어야 한다.</p>
<ul>
<li><strong>CAP Theorem을 다루는 글에서 나타나는 Partition tolerance의 정의가 일관성이 없다</strong>. 2000년에 <a href="http://www.cs.berkeley.edu/~brewer/cs262b-2004/PODC-keynote.pdf">Brewer가 이 정리를 발표</a>할 때 P에 대한 정의는 다음과 같았다.</li>
</ul>
<blockquote>The system continues to operate despite arbitrary message loss or failure of part of the system</blockquote>
<p>하지만 2002년에 이에 대해 <a href="http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.67.6951&rep=rep1&type=pdf">Gilbert와 Lynch가 이 정리에 대해 증명</a>에 사용된 P에 대한 새로운 정의는 다음과 같다.</p>
<blockquote>The network will be allowed to lose arbitrarily many messages sent from one node to another</blockquote>
<p>두 정의는 큰 차이가 있지만, 아직도 Brewer가 처음 내세운 정의가 많은 곳에서 사용되며 심지어 <a href="http://ko.wikipedia.org/wiki/CAP_%EC%A0%95%EB%A6%AC">한국 위키피디아</a>에도 이 버전으로 번역되어 있다.
하지만 <a href="http://blog.cloudera.com/blog/2010/04/cap-confusion-problems-with-partition-tolerance/">정확한 정의는 후자가 맞으며 그래야 이 정리가 성립</a>한다. 따라서 <strong>분할 내성 보다는 분할 용인이라고 번역</strong>하는 것이 더 올바를 것이다.</p>
<ul>
<li>
<p>CAP Theorem의 정의를 살펴보면 마치 C,A,P가 동등한 선상에 있는 것처럼 느껴진다.
이런 연유로 <strong>C,A,P가 모두 분산시스템의 특성에 대한 것으로 여겨지지만, 실상은 그렇지 않다</strong>.
앞에서 언급한 정의를 생각해본다면, C와 A는 분산시스템의 특성이지만, P는 그 분산시스템이 돌아가는 네트워크에 대한 특성이다.
이 정리가 처음 발표될 때의 P에 대한 정의는 P가 정말로 분산시스템의 특성인 양 서술해 놓았다. 네트워크가 분산시스템의 일부라고 해도 엄밀히 따지면 대상이 다르다고 봐야 한다.</p>
</li>
<li>
<p>CAP Theorem의 정의가 이해하기 어렵게 되어 있다. 세 가지 특성 중 두 개까지 고를 수 있다는 것으로도 많이 알려졌는데 마치 <strong>P를 포기하고 CA를 선택할 수도 있는 것 같은 기분을 들게</strong> 한다.
하지만 실상은 그렇지 않다. P의 정의는 네트워크가 임의의 메시지 손실을 할 수 있다는 것을 허용하느냐이다. 다시 말해 네트워크가 가끔 장애가 날 수 있다는 것을 인정하느냐는 것이다.
P의 선택을 배제하려면, <strong>절대로 장애가 나지 않는 네트워크를 구성해야 하지만 그런 것은 이 세상에 존재하지 않는다</strong>. 따라서 <strong>원하든 원하지 않든 P는 선택되어야 하며 결국 선택권은 C나 A중 하나</strong>밖에 없다.</p>
</li>
<li>
<p>또 하나의 중요한 문제는 <strong>C와 A가 비대칭적</strong>이라는 것이다. AP를 선택한 것으로 많이 알려진 Cassandra를 생각해보자.
이 시스템은 네트워크 장애 상황이든 정상 상황이든 C를 포기하도록 되어있다. HBase의 경우에는 장애상황일 때만 A를 포기하고 정상일 때는 A를 만족한다.
CAP Theorem은 네트워크가 장애 상황일 때만 대칭성이 있고 정상일 때에는 그렇지 않다.</p>
</li>
</ul>
<p>이 모든 문제와 모순을 해결하려면 CAP가 서술하는 상황을 장애상황으로 국한지어야 한다. 애초에 <strong>P는 네트워크 장애가 있을 수 있다는 것을 인정하느냐의 문제이고 이는 <a href="http://codahale.com/you-cant-sacrifice-partition-tolerance/">인정 할 수밖에 없는 문제</a>이므로 처음부터 선택</strong>되어있는 것이다.
결국, CAP Theorem이 내포하고 있는 의미는 <strong>분산시스템에서 네트워크 장애상황일 때 일관성과 가용성 중 많아도 하나만 선택할 수 있다</strong>는 것이다. 하지만 이런 식의 해석은 정상 상황일 때의 분산시스템 동작을 서술해주지 못한다.
이에 대해 <a href="http://cs-www.cs.yale.edu/homes/dna/">Daniel Abadi</a>가 <a href="http://cs-www.cs.yale.edu/homes/dna/papers/abadi-pacelc.pdf"><strong>PACELC</strong>라는 아주 우아한 표기법을 제시</a>하였다.</p>
<h2>CAP보다는 PACELC를 쓰자</h2>
<p>CAP는 분산시스템 디자이너의 선택에 도움을 주는 정리다. CAP가 장애 상황일 때의 선택에 대해 서술하는 것으로 생각하면, 정상 상황일 때의 선택에 대해 서술하지 못하게 된다.
그래서 정상상황일 때와 장애상황을 때를 나누어 설명하자는 것이 PACELC의 핵심이다. PACELC의 의미를 인용하자면 아래와 같다.</p>
<blockquote>
<p>if there is a partition (P) how does the system tradeoff between availability and consistency (A and C);
else (E) when the system is running as normal in the absence of partitions, how does the system tradeoff between latency (L) and consistency (C)?</p>
</blockquote>
<p><img src="http://eincs.com/images/2013/06/truth-of-cap-theorem-pacelc.jpg" alt="PACELC Diagram" title="PACELC" /></p>
<p>다시 말해 <strong>파티션(네트워크 장애)상황일 때에는 A와 C는 상충하며 둘 중 하나를 선택</strong>해야 한다는 것이다. 당연한 얘기다.
변경된 값을 모든 노드에서 바라볼 수 있도록 하려면 신뢰성 있는 프로토콜을 이용하여 데이터에 관여하는 모든 노드에 데이터가 반영되어야 한다.
하지만 네트워크 단절이 일어나 몇 개의 노드에 접근할 수 없을 때 C를 위해 데이터 반영이 아예 실패하던지 C를 포기하고 일단 접근 가능한 노드들이게 만 데이터를 반영하던지 둘 중의 하나만 골라야 한다.
또한, <strong>정상상황일 때에는 L과 C는 상충하며 둘 중 하나를 선택</strong>해야 한다. 모든 노드들에 새로운 데이터를 반영하는 것은 상대적으로 긴 응답시간이 필요하기 때문이다. 이를 몇 가지 NoSQL 시스템에 적용하면 다음과 같다.</p>
<ul>
<li><strong><a href="http://hbase.apache.org/">HBase</a></strong>는 <strong>PC/EC</strong>이다. 장애 상황일때 C를 위해 A를 희생한다. 그렇지 않은 경우에도 C를 위해 L을 희생한다.</li>
<li><strong><a href="http://cassandra.apache.org/">Cassandra</a></strong>는 <strong>PA/EL</strong>이 가능하도록 디자인 되었다. 설정에 따라 Eventual Consistency의 특성을 가지게 되는데, 이 경우 PA/EL이 된다.
장애 상황인 경우에는 가능한 노드에만 데이터를 반영하고 정상으로 복구되면 필요한 노드에 데이터를 모두 반영한다. 정상상황일때도 Latency를 위해 모든 노드에 데이터를 반영하지 않는다.</li>
<li><strong><a href="http://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed">Brewer가 제시한 가상의 분산시스템</a></strong>은 <strong>PA/EC</strong>이다. 정상적인 경우에는 모든 노드에서 같은 메세지를 볼 수 있도록 쓰기 연산이 일어나는데
P 상황인 경우, 이를 판단하여 일단 접근 가능한 만큼만 데이터를 반영한다
결과적으로 시스템은 디운되지 않지만 절단된 노드들 끼리는 데이터를 주고 받지 못하게 된다. 장애상황이 복구되면 이를 감지하여 전달하지 못한 데이터를 반영한다.
장애상황일때에만 C를 포기하고 보통의 상황에서는 강력한 C를 가져가는 것이다.
<img src="http://eincs.com/images/2013/06/truth-of-cap-theorem-diagram-brewer-state.jpg" alt="Brewers NoSQL Proposal" title="Diagram of CAP Theorem" /></li>
<li><strong><a href="http://www.mpi-sws.org/~druschel/courses/ds/papers/cooper-pnuts.pdf">PNUTS</a></strong>는 <strong>PC/EL</strong>이다. 장애상황일 때는 C를 위해 A를 희생하지만, 평소에는 L을 위해 C를 희생한다.
시스템이 정상일 때에도 C를 충족시키지 못하는데 네트워크 장애일 때 C를 위해 A를 포기한다니 말이 안 되는 것 같지만,
여기서 <strong>PC의 의미는 평소만큼의 C를 보장하기 위해 A를 희생시킨다</strong>는 의미로 받아들이면 된다.
PNUTS의 Consistency Level은 Timeline Consistency이다. 일반적으로 생각하는 Consistency보다는 약하지만,
장애상황일 때에도 정상상황일 때와 같은 Consistency Level을 보장하기 위해 요청 자체를 실패시킨다.</li>
</ul>
<p>위에서 소개한 사례 중 마지막 두 개를 CAP Theorem으로 표현하려고 해보려고 하려면 어떻게 표현해야 할지 잘되지 않는다.
이처럼 CAP로 표현하는 것보다 PACELC로 표현하는 것이 훨씬 명확하다. 앞서 살펴본 CAP의 문제점도 전혀 없다.</p>
<h2>못다 한 이야기</h2>
<p>RDBMS에 CAP를 적용하여 CA로 분류하는 것을 자주 보았다. Brewer가 처음 발표할때에도 이렇게 표현을 하기도 했고,
하나의 인스턴스로 운영되는 RDBMS의 경우 네트워크로 연결되어 있지 않으므로 네트워크 단절을 배제할 수 있다는 점에서 이렇게 분류하는게 어느 정도 납득은 간다.
하지만 CAP는 분산시스템이 전제조건이고 따라서 RDBMS에 CAP를 적용하는 것은 맞지 않다.
굳이 적용하기 위해 CAP정리를 다음과 같은 명제식으로 바꾸어 본다고 하자.</p>
<blockquote>
<p>p: 시스템이 분산시스템이다.<br />
q: C,A,P 모두 만족 시킬 수 없다.<br />
p->q</p>
</blockquote>
<p>여기서 RDBMS의 경우 분산시스템이 아니므로 p는 거짓이이며, q의 참, 거짓 여부와 상관없이 명제 전체가 수학적으로 참이 되므로 이 명제가 맞다고 주장할 수 있다.
하지만 이것은 석궁사건으로도 유명한 1995년 <a href="http://ko.wikipedia.org/wiki/1995년_성균관대학교_입학시험_오류_사건">성균관대학교 본고사 문제</a>에서 성균관 대학교가 주장한 사기 풀이법과 같은 논리이다.
따라서 RBMS에 CAP를 적용하려는 시도는 맞지 않다. CAP를 적용하려는 시스템이 분산시스템이 아니라면 CAP자체가 동작하지 않는 것이다.
석궁 사건과 같은 봉변을 당하고 싶지 않다면 CAP를 제대로 이해하자.</p>
<h2>결론</h2>
<p>애초에 CAP Theorem이 의도했던바는 명확하다. <strong>절대로 장애가 없는 네트워크는 존재하지 않으며, 따라서 분산시스템에서 P는 무조건 인정하고 들어가야 한다</strong>는 것이다.
따라서 네트워크가 단절되었을 때 시스템이 어떻게 동작할지 결정해야 하며, 이 때 C와 A를 모두 만족시키는건 불가능하므로 둘 중에 하나를 골라야 한다.
하지만 어려가지 문제로 CAP는 분산시스템에 대한 명확한 표현이 힘들다. <strong>PACELC를 이용하면 문제를 명확하게 이해할 수 있으며 시스템에 대해서도 깔끔하게 표현</strong>할 수 있다.
좀 더 자세한 내용에 대해 알고 싶다면 다음 글들을 참고하면 된다.</p>
<ul>
<li><a href="http://www.cs.berkeley.edu/~brewer/cs262b-2004/PODC-keynote.pdf">Towards Robust Distributed Systems</a>: CAP Theorem에 대해 처음 소개된 발표 자료</li>
<li><a href="http://groups.csail.mit.edu/tds/papers/Gilbert/Brewer2.pdf">Perspectives on the CAP Theorem</a>: CAP Theroem을 증명한 논문</li>
<li><a href="http://blog.cloudera.com/blog/2010/04/cap-confusion-problems-with-partition-tolerance/">CAP Confusion: Problems with ‘partition tolerance’</a>: P에 대한 정의를 정확하게 언급한 Cloudera 포스팅</li>
<li><a href="http://codahale.com/you-cant-sacrifice-partition-tolerance/">You Can’t Sacrifice Partition Tolerance</a>: CAP에서 P를 희생시킬 수 없다는 글</li>
<li><a href="http://dbmsmusings.blogspot.kr/2010/04/problems-with-cap-and-yahoos-little.html">Problems with CAP, and Yahoo’s little known NoSQL system</a>: PACELE를 처음 제안한 블로그 글</li>
<li><a href="http://cs-www.cs.yale.edu/homes/dna/papers/abadi-pacelc.pdf">Consistency Tradeoffs in Modern Distributed Database System Design</a>: IEEE에 실린 PACELE에 대한 글</li>
<li><a href="http://www.computer.org/cms/Computer.org/ComputingNow/homepage/2012/0512/T_CO2_CAP12YearsLater.pdf">CAP Twelve Years Later: How the “Rules” Have Changed</a>: Brewer가 CAP가 나온지 12년 후에 쓴 글</li>
<li><a href="http://dbmsmusings.blogspot.kr/2012/10/ieee-computer-issue-on-cap-theorem.html">IEEE Computer issue on the CAP Theorem</a>: PACELE를 처음 제안한 Daniel Abadi가 생각을 정리한 글</li>
</ul>
NoSQL은 생각보다 쓸만하지 않다2012-06-26T09:00:00+09:00http://http://eincs.com/2012/06/nosql-is-not-useful<p><strong><a href="http://en.wikipedia.org/wiki/NoSQL">NoSQL</a></strong>이라고 일컫는 분산 데이터베이스들이 요즘 트렌드다. 뛰어난 확장성과 가용성으로 각광을 받고 있다.
실제로 여러 소셜게임업체들이 NoSQL을 사용하며, 넷플릭스 또한 NoSQL인 <a href="http://techblog.netflix.com/2011/01/nosql-at-netflix.html">Hbase와 Cassandra를 주요 저장소로 사용</a>한다.
그리고 <a href="https://www.facebook.com/note.php?note_id=454991608919">페이스북의 메신저 시스템</a> 및 <a href="http://borthakur.com/ftp/RealtimeHadoopSigmod2011.pdf">실시간 분석 시스템</a> 또한 HBase기반으로 만들어졌다.
NoSQL을 사용하면 RDBMS에서의 불편한 것들이 모두 해결되고 높은 확장성을 가진 시스템을 구축할 수 있는 것 같지만 현실은 그렇지 못하다.
대부분의 서비스들은 특수한 경우를 제외하고는 <strong>NoSQL이 아닌 RDBMS로 구현</strong>되어 있다. <strong>일반적인 시스템을 구현</strong>하는데 있어서 NoSQL은 그다지 쓸만하지 않다.
<img src="http://eincs.com/images/2012/06/nosql-google-trend.jpg" alt="Google Trend of NoSQL and RDBMS" /></p>
<h2>대부분의 서비스는 RDBMS를 주요 저장소로 사용한다</h2>
<p>아직까지는 구글을 제외한 대부분의 다른 서비스들은 NoSQL을 제대로 사용하고 있지 못하다. <strong>거의 대부분의 기업들은 주요 저장소로 RDBMS를 사용하고 있는 것</strong>이다.
대표적으로 몇 가지 사례를 들어보면 다음과 같다.</p>
<ul>
<li>
<p><strong><a href="https://www.facebook.com/">페이스북</a></strong>은 MySQL을 사용한다. 2012년 3월 기준으로, <a href="http://newsroom.fb.com/content/default.aspx?NewsAreaId=22">페이스북의 MAU는 9억명</a>을 넘었다.
이 정도의 추세라면 2012년 중순에는 10억명을 돌파할 것이라고 한다.
이렇게 엄청난 수의 사용자 트래픽을 감당하기 위해서는 아주 특별한 방법을 통해 데이터를 저장해야 될 것 같지만, 실제로는 <a href="http://www.infoq.com/presentations/Facebook-Software-Stack">MySQL을 사용하여 저장 및 관리</a>한다.
물론 일반적인 방식대로 MySQL을 사용하고 있지는 않다. 데이터는 Key-Value형태로 저장되며, 이 데이터에 대한 Join연산은 웹 서버의 어플리케이션에서 직접 담당한다고 한다.
심지어 최근에 적용된 기능인 <a href="https://www.facebook.com/notes/facebook-engineering/building-timeline-scaling-up-to-hold-your-life-story/10150468255628920">타임라인도 MySQL을 사용하여 구현</a>되었다. 앞서 언급된 메세징 시스템이나 실시간 분석 시스템 등의 정도만 NoSQL을 이용하여 구현되었다.
<strong>대용량 데이터를 처리하기 위해 데이터를 샤딩하여 여러대의 MySQL 서버에 나누어 저장</strong>한다. 페이스북에서도 대부분은 여전히 RDBMS를 쓰고 있는 것이다.</p>
</li>
<li>
<p><strong><a href="https://twitter.com/">트위터</a></strong>도 MySQL을 사용한다. 트위터의 경우, 많이 들어오는 경우 <a href="http://yearinreview.twitter.com/en/tps.html">초당 수 천건의 트윗</a>이 들어온다.
올해 초의 <a href="http://blog.twitter.com/2012/02/post-bowl-twitter-analysis.html">슈퍼볼 결승전 때는 초당 트윗이 만건을 넘기기도</a> 하였다.
이렇게 엄청난 양의 트래픽을 처리하는 트위터 또한 주요 저장소로 MySQL을 사용한다.
처음에는 <a href="http://nosql.mypopescu.com/post/407159447/cassandra-twitter-an-interview-with-ryan-king">Cassandra로 바꾸려고 했었지만</a> 결국 <a href="http://http://engineering.twitter.com/2010/07/cassandra-at-twitter-today.html">MySQL 시스템을 유지하기로 결정</a>하였다.
이렇게 엄청난 수의 트윗에 대한 아이디를 발급하기 위해 <a href="http://engineering.twitter.com/2010/06/announcing-snowflake.html">Snowflake를 사용</a>한다.
트위터 또한 대용량 데이터를 처리하기 위해 NoSQL을 사용하기 보다는 <strong>데이터를 샤딩하여 여러대의 MySQL서버에 저장</strong>하는 것이다.
또한 <a href="http://highscalability.com/blog/2011/12/19/how-twitter-stores-250-million-tweets-a-day-using-mysql.html">Gizzard를 통해 데이터 레이어를 추상화 하여 MySQL에 데이터를 저장</a>한다.
Glizzard라는 미들웨어를 이용하여 RDBMS 이용하면서도 NoSQL에서 제공되는 분산 및 복제 기능을 가질 수 있는 것이다.</p>
</li>
<li>
<p><strong><a href="https://www.tumblr.com/">텀블러</a></strong>도 아직은 MySQL가 주요 저장소다. Tumblr는 엄격하게 말하면 블로깅 서비스이긴하지만, 체류시간이 페이스북 다음으로 두번째로 긴 소셜 서비스로 소개된다.
이렇게 수많은 사람들이 사용하는 Tumblr에서도 실제 데이터는 MySQL을 사용하여 저장하며, <a href="http://highscalability.com/blog/2012/2/13/tumblr-architecture-15-billion-page-views-a-month-and-harder.html">제한적으로 NoSQL을 사용</a>한다.
HBase같은 것들은 URL Shorter나 데이터 분석 등 제한적으로 사용되며, 메인 데이터는 샤딩하여 MySQL에 저장하는 것이다.
데이터를 사딩할때, 시간 순서대로 샤딩하는데, 특정 MySQL 샤드에 로드가 집중된다. 이는 <strong>Master-Slave구성으로 자주 일어나는 읽기 연산을 여러 Slave에 분산</strong>하는 것으로 해결하였다고 한다.</p>
</li>
<li>
<p><strong><a href="https://pinterest.com/">핀터레스트</a></strong>도 MySQL을 주요 저장소로 사용한다.
Pinterest는 특정 주제로 사진을 게시 및 공유 할 수 있는 서비스로, 단기간에 사용자 3000만명을 넘기며, 그것도 80%이상이 여성 사용자를 확보하면서 큰 주목을 받고 있다.
Pinterest는 <a href="http://highscalability.com/blog/2012/5/21/pinterest-architecture-update-18-million-visitors-10x-growth.html">데이터 분석을 위해 Hadoop을 이용</a> 하긴 하지만,
<a href="http://highscalability.com/blog/2012/2/16/a-short-on-the-pinterest-stack-for-handling-3-million-users.html">실제 데이터는 MySQL에 저장</a>한다. 역시 샤딩하여 데이터를 저장하고, 그리고 캐시 전략을 잘 사용하여 부하를 해결한다고 한다.</p>
</li>
<li>
<p><strong><a href="http://instagram.com/">인스타그램</a></strong>은 PostgreSQL을 사용한다.
인스타그램은 1500만명 이상이 사용하는 사진 공유 서비스이다. 그리고 페이스북에 인수되기도 하였다.
인스타그램에서는 RDBMS중 하나인 <a href="http://instagram-engineering.tumblr.com/post/13649370142/what-powers-instagram-hundreds-of-instances-dozens-of">PostgreSQL을 사용</a>한다.
인스타그램은 초기에 3명의 엔지니어가 개발하였으며, 12개의 EC2를 이용하여 PostgreSQL을 돌린다. PostgreSQL 인스턴스들은 오픈소스를 이용해 Master-Replica 형태로 운영된다.
그 외 시스템은 여러가지 오픈 소스를 이용하여 구성되어 있다. 이미 잘 구현된 기술이 있다면 새로운 기술을 가져다 쓰기보다는 오픈소스를 최대한 가져다 쓰라고 권하고 있다.
인스타그램이 적은 수의 엔지니어로 서비스를 만들 수 있었던 비결일 것이다.</p>
</li>
<li>
<p><strong><a href="http://evernote.com/">에버노트</a></strong>도 MySQL을 사용한다.
에버노트는 클라우드 노트 서비스이다. 2011년 한해동안 600만명에서 2000만명으로 유저가 증가할 정도로 <a href="http://gigaom.com/2011/12/27/evernote-2011-growth-users/">엄청난 속도로 성장하고 있는 서비스이며</a>
수 많은 노트들을 다양한 채널을 통해 관리할 수 있는 환경을 제공한다.
에버노트는 MySQL을 저장소로 사용하며, NoSQL이 아니라 <strong><a href="http://blog.evernote.com/tech/2012/02/23/whysql/">RDBMS를 사용하는 이유</a></strong>에 대해서도 기술 블로그에 올라왔었다.
RDBMS를 사용하는 이유는 ACID 때문이다. NoSQL에서는 일반적으로 높은 레벨의 트랜잭션이 지원되지 않는다.</p>
</li>
</ul>
<p>이처럼 대규모 트랜잭션을 처리해야하는 서비스들도 주요 저장소로 아직은 RDBMS를 사용한다.
페이스북의 메신저 시스템과 실시간 분석 시스템, 텀블러의 주소 길이 단축 시스템 정도만 HBase와 같은 NoSQL을 실험적으로 도입하는 단계이다.
NoSQL을 전면적으로 도입하려다 그만 둔 트위터도 있고 심지어 처음부터 잘되어있는걸 가져다 쓰라는 Instagram도 있다. 에버노트는 NoSQL을 쓰지 않는 이유를 명확히 밝혔다.
획장성이 가장 중요한 이슈일 것 같은 많은 서비스들이 아직도 NoSQL을 사용하지 않는 이유는 자명하다.</p>
<h2>NoSQL 기술은 아직은 걸음마 단계이다</h2>
<p><a href="http://nosql-database.org">오픈소스로 공개되어 있는 NoSQL은 굉장히 많다</a>. 많은 기업들이 NoSQL을 도입하기 위해 여러가지 시도들을 하고 있다.
하지만 아직까지 주요 데이터 저장소로 RDBMS를 사용하는 경우가 거의 대부분인 것이 현실이다. 왜 그럴까?
많은 이유가 있을 수 있겠지만, 그 중 중요한 하나는 배포 중인 NoSQL들이 범용적으로 사용하기에는 <strong>아직 부족한점이 너무도 많다</strong>는 것이다.</p>
<p>서비스를 구현하는데 반드시 필요한 것들이 있다. 바로 인덱싱과 트랜잭션이다.
이것들 없이도 어떻게든 잘 구현할 수 있는 특수목적의 시스템을 제외하면 이런 기능들은 제대로된 서비스를 만들기 위해서 반드시 필요하다.
이런 기능들이 제공되지 않으면 범용적인 사용이 불가능하며, 충분히 추상화되지 못한 상태에서 구상적인 문제를 해결하기 위해 쓸데없는 시간을 보낼 것이다.
현재 배포되어 있는 <strong>NoSQL들은 이와 같은 기능이 아예 없거나 제한적</strong>이다.
Transaction의 경우 NoSQL상에서 분산 트랜잭션을 구현하기 위한 Transaction을 구현하기 위한 시도들이 있었다.
<a href="http://www.globule.org/publi/CSTWAC_tsc2011.html">Elastras</a>와 <a href="https://www.usenix.org/legacy/event/hotcloud09/tech/full_papers/das.pdf">CloudTPS</a>이다. 하지만 그다지 효용은 없었던 것 같다.</p>
<p>사실 HBase에는 <a href="https://github.com/hbase-trx/hbase-transactional-tableindexed">TransactionalTable</a> 같은 트랜잭션을 제공하기 위한 프로젝트가 존재하기는 한다.
하지만 2011년 초에 사용하려고 써봤지만, 그닥 제대로 동작하는 것 같진 않았다. 심지어 더 이상 프로젝트가 유지 보수 되지 않는다고 한다.
이유는 잘 모르겠지만 <a href="https://github.com/yahoo/omid">Omid</a>라는 프로젝트에서 이 프로젝트가 제대로된 트렌젝션을 지원하지 못한다고 언급하는 것을 보면 그냥 망한 것 같다.
또한 <a href="http://staltz.blogspot.kr/2012/10/hacid-multi-row-transactions-in-hbase.html">HAcid</a>나 <a href="http://www.scpe.org/index.php/scpe/article/view/715">HBaseSI</a>등과 같은 여러가지 프로젝트들도 있다. 하지만 성능이 절대적인 매우 떨어지는 것 뿐만 아니라 성능에 대한 선형 확장성이 없다.
앞서 잠깐 언급한 Omid라는 프로젝트도 엄밀히 말해서는 선형 확장성이 없다고 볼 수 있다. NoSQL을 쓰는 가장 중요한 이유를 무용지물로 만들어버리는 것이다.
<img src="http://eincs.com/images/2012/06/nosql-baby.png" alt="구글을 제외한 다른 기업들은 걸음마 단계이다" /></p>
<h2>구글만은 모든 문제를 해결하였다</h2>
<p>Transaction과 관련하여 구글이 2010년에 논문을 내놓았다. <a href="http://research.google.com/pubs/pub36726.html">Percolator</a>인데 BigTable구조의 스토어에서 분산트랜잭션을 구현하는 방법에 대해 기술해 놓았다.
완벽하게 동작히며 선형 확장성도 있다. 이미 구글의 실제 돌아가는 시스템에서 성공적으로 사용되고 있다.
HBase상에서 돌아가는 분산트랜잭션인 HBaseSI를 소개하는 프리젠테이션에서는 자신들의 방법이 살짝 부족하긴 해도 알고봤더니
<a href="http://www.math.uwaterloo.ca/~hdesterc/websiteW/Data/presentations/pres2010/NIISI.pptx.pdf">구글의 Percolator와 비슷한 방법이라고 자랑질</a>을 할 정도이니 더 이상 언급하지 않아도 될 것 같다.
그리고 이미 구글의 클라우드 서비스인 AppEngine에서는 BigTable을 기본 데이터 저장소로 사용할 수 있도록 해주며, 인덱싱과 트랜잭션을 완벽하게 제공한다.
바로 구글의 <a href="https://developers.google.com/datastore/">Datastore</a> 서비스인데, <a href="http://research.google.com/pubs/pub36971.html">Megastore</a>라는 시스템상에서 돌아간다. 이런 기능들이 제공되는 뿐만아니라 여러 데이터베이스간 분산 저장을 한다고하니 정말 대단하다.</p>
<h2>결론</h2>
<p>거의 대부분의 서비스에서는 NoSQL을 사용하지 않는다. 그리고 그 이유는 <strong>트랜잭션과 같은 일반적인 서비스 구현에 필요한 기능들이 전혀 준비되어 있지 않기 때문</strong>이다.
사실 트랜젝션이나 인덱싱이 필요하지 않거나 중요하지 않은 특수한 시스템을 구현한다고 한다면, NoSQL에서 제공되는 기능으로도 충분한 것이다.
오히려 NoSQL을 사용하는 것이 시스템 구현에 있어서 편리한 부분이 있을 수 있다. 하지만 일반적인 서비스를 구현하기에 부족한 점이 너무나 많다.
하지만 <strong>구글의 경우 모든 것을 해결한 시스템을 BigTable상에 구현했</strong>으며 이미 몇 년동안 서비스하고 있다. 내리고 싶은 결론은 두 가지다.</p>
<ul>
<li>구글을 찬양하자.</li>
<li>바퀴를 만들지 말자.</li>
</ul>
<p>서비스를 구현하는데 주된 데이터 저장소로 NoSQL 사용을 고려하고 있다면,
구글을 제외한 다른 업체에서 일반적인 서비스를 구현하는데 있어서 NoSQL을 주된 저장소로 사용하고 있지 않다는 사실을 알고 결정하자.
구현하려는 시스템이 <strong>NoSQL로 충분히 구현할 수 있고 NoSQL을 이용하여 도움이 될떄 NoSQL을 선택</strong>해야할 것이다. 구글이 아니라면 말이다.</p>
<p>사실 현재 일하고 있는 VCNC에서 <a href="http://appbetween.us/">비트윈</a>이라는 서비스를 돌리고 있는데, 주저장소로 HBase를 쓰고 있다.
여러가지 이유가 있기는 하지만 비트윈에서 가장 많이 사용하는 기능은 메세징이며, 메세징은 서비스의 구현이 그다지 복잡하지 않다.
따라서 NoSQL로도 어느정도 처리가 가능하며, <a href="http://tech.naver.jp/blog/?p=1420">Line에서 주저장소로 HBase를 이용</a>하는 것도 같은 이유에서 일 것이다.</p>
[AirVideo] 아이폰 최고의 동영상 스트리밍 어플 AirVideo, 리눅스에서 AirVideo 서버를 돌리자!2010-05-15T16:37:33+09:00http://http://eincs.com/2010/05/install-airvideo-server-on-ubuntu-linux<p>전 아이폰 유저입니다. 작년 12월말 부터 아이폰을 사용하고 있습니다. 전 많은 어플들을 사용하고 있는데, 얼마 전부터 <a href="http://www.inmethod.com/air-video/">AirVideo</a>라는 어플을 사용하기 시작했습니다.</p>
<h2>AirVideo 어플은 어떤 어플인가?</h2>
<p><a href="http://www.inmethod.com/air-video/">AirVideo</a>는 <strong>개인 컴퓨터에 서버를 띄워놓고 그 서버를 통해 컴퓨터에 있는 동영상을 스트리밍하여 자신의 아이폰에서 받아 볼 수 있게 해주는 어플</strong>입니다.
아이폰에서는 mp4 포맷의 동영상만 재생하기 때문에 아이폰에서 동영상을 보기 위해선 사용자가 직접 동영상을 mp4로 인코딩 해야되기 때문에 불편함이 있었습니다.
하지만 AirVideo서버에서는 <strong>자동으로 mp4 포맷으로 인코딩하여 스트리밍 해주기 때문에 사용자가 동영상을 직접 인코딩할 필요가 없습</strong>니다.
또한 생각 외로 동영상이 끊기는 일이 거의 없어 호평을 받고 있습니다. <strong>‘평가용 무료어플을 다운 받을 필요 없이 바로 유료로 질러라’</strong>라는 말이 있을 정도입니다.</p>
<p><img src="http://eincs.com/images/2010/05/airvideo-loading-screen.jpg" alt="AirVideoIntro" /></p>
<p>하지만 전 그동안 이 어플을 사용하고 있지 않았습니다.
그 이유는 AirVideo의 개발사인 inmethod에서 제공하고 있는 AirVideo 서버는 <strong>공식적으로 Windows, Max OS X만 지원</strong>하기 때문입니다.
하지만 집에 있는 PC는 WOL(Wake On Lan)[1] 기능도 없고, 회사 PC는 방화벽 등 네트워크 설정때문에 외부에서 접근이 불가능하여 AirVideo 서버를 사용하기 힘든 환경[2] 이었습니다.
제게 개인 서버가 있긴 하나, 우분투 리눅스가 깔려있기 때문에 공식적인 AirVideo서버를 사용할 수 없었습니다.
또한 설치된 우분투가 서버버전으로 GUI환경이 제공되지 않는 버전이라 와인 등으로 윈도우 바이너리를 돌리기 힘든 환경이었습니다.
(혹시 GUI환경이 제공되는 리눅스를 사용하고 계신 분은 윈도우용 바이너리를 리눅스상에서 실행시켜주는 와인을 이용하여 GUI버전의 Window AirVideo 서버를 직접 구동시키실 수 있습니다)
따라서 그 동안 서버를 돌릴 마땅한 환경이 없어서 AirVideo 어플이 좋은건 알고 있엇지만, 사용하지 못하고 있었습니다.
하지만 방법을 찾던 중 리눅스에서 돌아가는 AirVideo 서버 모듈이 있다는 말을 듣고 우분투에 깔아보기로 하였습니다.</p>
<p><img src="http://eincs.com/images/2010/05/airvideo-server-support.jpg" alt="AirVideoSupportedPlatform" /></p>
<p>AirVideo에 더 자세한 정보를 알고 싶으시면, <a href="http://www.inmethod.com/air-video/">AirVideo 홈페이지</a>에 가셔서 확인하시기 바랍니다.</p>
<h2>AirVideo 서버를 우분투에 설치하자!</h2>
<p>AirVideo 서버를 우분투 리눅스에 설치하기 위해서는 서버를 설치하기전, <strong>서버 구동에 필요한 라이브러리 프로그램들을 먼저 설치</strong>해야 합니다.
이제 소개될 과정들을 차근차근 따라가시면 큰 어려움 없이 AirVideo 서버를 우분투 리눅스에서 돌리실 수 있을 것 입니다.</p>
<h3>1. AirVideo 서버 구동에 필요한 패키지 들을 설치하자!</h3>
<ol>
<li><code>libx264-dev</code> 최신 버전 설치를 위해 Stéphane Marguet의 PPA 리포지터리를 우분투에 추가한다.</li>
<li>libmp3lame-dev libfaad-dev libx264-dev libmp3lame-dev libfaad-dev mpeg4ip-server faac 패키지 들을 apt-get을 이용하여 설치한다.</li>
</ol>
<p>AirVideo 서버는 동영상 스트리밍 외에도 인코딩을 하기 때문에 몇 가지 패키지들을 추가로 필요로 합니다.
우분투 리눅스에서는 apt-get 명령어로 큰 어려움 없이 패키지를을 설치 할 수 있습니다. 하지만 한 가지 문제점이 있습니다.
다음 단계에서 설치할 FFmpeg는 libx264-dev를 필요로 하는데, <strong>현재 우분투 리포지터리에 올라와 있는 libx264-dev는 오래된 버전이라 최신버전(2010년)으로 설치해 줘야합니다.</strong>
오래된 버전을 설치하면 FFmpeg를 컴파일 할 때 오류를 내뱉게 됩니다. 최신버전이 있는 리포지터리를 우분투에 추가하여 최신 버전의 libx264-dev를 설치 할 수 있도록 합니다.</p>
<p>최신버전의 libx264-dev를 설치하기 위해 다음과 같은 명령어로 해당 버전이 올라와 있는 Stéphane Marguet의 PPA를 추가 합니다.</p>
<pre><code>sudo add-apt-repository ppa:user_id/ppa
sudo apt-get update
</code></pre>
<p>위 명령어에 대한 자세한 정보를 알고 싶으시다면,<a href="https://launchpad.net/~stemp/+archive/ppa">PPA 관련 사이트</a>에서 확인하도록 합시다.
이 사이트에서 PPA를 추가하기 위한 자세한 설명에 대한 <a href="https://launchpad.net/+help/soyuz/ppa-sources-list.html">페이지</a>를 직접 참고하셔도 됩니다.</p>
<p>이제 최신 버전의 libx264-dev가 설치된 리포지터리를 추가하였으므로, 아래와 같은 명령어로 필요한 패키지들을 설치합시다.</p>
<pre><code>sudo apt-get install libmp3lame-dev libfaad-dev libx264-dev libmp3lame-dev libfaad-dev mpeg4ip-server faac
</code></pre>
<p>필요한 패키지들이 자동으로 설치가 됩니다.</p>
<h3>2. 동영상 인코딩에 필요한 FFmpeg 를 설치하자!</h3>
<ol>
<li>커스터마이징 되어 있는 FFmpeg 소스파일을 AirVideo 홈페이지에서 다운받는다.</li>
<li>서버에 올려 컴파일하여 ffmpeg 실행 파일을 만든다.</li>
<li>ffmpeg 파일의 퍼미션(755)과 소유자/그룹 조정을 하여 적당한 폴더에 복사한다.</li>
</ol>
<p>앞에서 말씀드렸다시피, AirVideo서버에서 mp4로 인코딩하기 위해 FFmpeg라는 프로그램이 필요합니다.
AirVideo 서버 내부에서 FFmpeg 프로그램을 호출을 하는 형태로 되어있는데, 공식적으로 지원하는 Windows, OS X 서버에서도 이 프로그램을 사용합니다.</p>
<p>그런데 여기서 주의해야할 점은, <strong>AirVideo서버가 사용하는 FFmpeg는 AirVideo서버를 위해 커스터마이징 된 버전</strong>이라는 점 입니다.
따라서 <a href="http://www.ffmpeg.org/">FFmpeg</a> 에서 제공하는 정식버전을 사용하면 안되고, inmethod사의 홈페이지에서 커스터마이징된 버전을 다운받아 사용해야합니다.</p>
<p>[AirVideo 라이센스 페이지][AirVideoLicense]의 You can download the complete source code used to build FFMpeg here.에서 다운로드 받으시면 됩니다.
다운 받으면 압축파일이 있는데, 이 압축파일과 압축파일 안에 있는 아카이브를 모두 풀어 서버의 아무 폴더에 업로드를 시킵니다.</p>
<p><strong>소스파일이므로 컴파일 하여 사용해야 합니다.</strong> 다음과 같은 명령어로 FFmpeg 를 컴파일하여 실행 파일을 만들어 냅니다.
전 컴파일할 때 꽤 오래 걸렸으며, 15분 정도 걸렸다는 분도 있습니다. 아래 명령어들은 서버 설정에 따라 다르겠지만 보통은 root 권한으로 수행되어야 합니다.</p>
<pre><code>./configure --enable-pthreads --disable-shared --enable-static --enable-gpl --enable-libx264 --enable-libmp3lame --enable-libfaad --disable-decoder=aac
make
</code></pre>
<p>ffmpeg라는 파일이 생성되었을 것입니다. 이 파일이 실행파일입니다.
이 파일의 권한은 chmod 명령어를 이용하여 755로 줘야하고, 소속 그룹 및 사용자를 chown명령어를 이용하여 root:root 로 설정하여 줍니다.
사용자마다 다르겠지만 전 이 실행파일을 /usr/bin 폴더 안에 넣었습니다.</p>
<p>한 가지 팁을 더 설명드리자면, configure 할 대 libx264-dev가 오래된 버전이라는 에러메세지가 나오면
Stéphane Marguet의 PPA가 제대로 추가되었는지 확인하고 libx264-dev를 다시 설치해 주시기 바랍니다.</p>
<h3>3. AirVideo 서버를 설치하고 설정 파일을 설정하자!</h3>
<ol>
<li>AirVideoServerLinux.jar 파일과 test.properties 파일을 다운받아 서버에 올린다.</li>
<li>test.properties파일을 자신의 서버에 맞게 수정한다.</li>
<li>적당한 계정으로 java -jar AirVideoServerLinux.jar test.properties 하여 서버를 실행한다.</li>
</ol>
<p>이제 AirVideo 서버를 설치할 준비가 모두 끝났습니다. 이제 AirVideo서버를 본격적으로 시작해 봅시다.
<a href="http://www.inmethod.com/forum/posts/list/60/34.page#3935">AirVideo포럼의 한 포스팅</a>에서 AirVideoServer.jar 파일과 test.properties 파일을 다운 받습니다.
AirVideoServer.jar 는 AirVideo 리눅스용 서버를 돌리기 위한 jar파일이며, java 머신으로 실행되어야 합니다. 따라서 서버에 JRE 혹은 JDK가 설치되어 있어야 합니다.
iPad를 지원하는 Alpha 버전은 <a href="http://inmethod.com/air-video/download/linux/alpha2/AirVideoServerLinux.jar">http://inmethod.com/air-video/download/linux/alpha2/AirVideoServerLinux.jar</a>에서 다운 받을 수 있다고
합니다만, Alpha 버전에 대한 자세한 사항은 잘 모르겠습니다.</p>
<p>위 두 파일을 서버에 올립니다. 기본적으로 AirVideo서버는 root계정이 **아닌 특정 유저의 권한으로 실행되는 것이 좋습니다.
**왜냐면 AirVideo에서 스트리밍을 위해 참조하는 동영상 폴더와 동영상들이 AirVideo의 계정과 같아야 하기 때문입니다.
같지 않다면, 동영상들을 읽어오지 못하거나 자신의 사용자/사용자 그룹으로 파일의 설정을 바꿔버럽니다.
따라서 알맞게 권한과 사용자 그룹, 사용자를 설정합니다.</p>
<p><strong>이제 test.properties 파일을 바꿔봅시다.</strong> test.properties 파일에서 바꿀 설정 값들은 다음과 같습니다.</p>
<p><strong>path.ffmpeg</strong> : 앞 단계에서 컴파일하여 생성된 FFmpeg 실행파일인 ffmpeg의 경로입니다.
서버에 따라 값이 다르므로, 직접 확인하여 설정하셔야 합니다. 제 경우는 ffmpeg파일을 <code>/usr/bin</code> 폴더에 옮겨놓있기 때문에, <code>/usr/bin/ffmpeg</code> 였습니다.
<strong>path.mp4creator</strong> : mp4creator 프로그램의 경로입니다. ‘whereis mp4creator’ 명령어를 통해서 프로그램이 설치된 경로를 찾아 알맞게 수정합니다.
<strong>path.faac</strong> : faac 프로그램의 경로입니다. ‘whereis facc’ 명령어를 통해서 프로그램이 설치된 경로를 찾아 알맞게 수정합니다.
<strong>password</strong> : 서버의 비밀번호를 설정합니다. 설정하게 되면 AirVideo어플로 서버에 접속하게 되면 비밀번호를 요청하게 되므로, 보안상 필요한 경우 설정합니다.
<strong>folders</strong> : AirVideo 서버에서 참조할 동영상 폴더를 지정합니다.
<code><폴더 1>:<폴더 1위치>, <폴더 2>:<폴더 2위치></code>와 같은 형식으로 지정하시면 됩니다.</p>
<p>설정이 끝났다면, 아래 명령어를 통해 AirVideo 실행이 가능합니다.</p>
<p>java -jar AirVideoServerLinux.jar test.properties</p>
<p>만약 제대로 실행이 안된다면 나오는 메세지를 보고 수정을 하시거나, 특별한 메세지가 없는 경우, ffmpeg 등 프로그램이 제대로 설치되었나,
혹은 path값이 제대로 맞는지, 실행권한은 있는지 확인해보시기 바랍니다.</p>
<p>이제 아이폰의 AirVideo 어플에서 서버의 IP값을 넣어 제대로 접속되는지 확인합니다.
리눅스 서버 버전의 경우 PIN을 통한 접속은 지원하지 않으므로 꼭 IP를 쳐서 접속하여야 합니다.</p>
<h3>4. 기타 몇 가지 알아두면 좋은 사실들</h3>
<p><strong>컴퓨터가 켜질 때 자동으로 AirVideo서버를 구동시키기 위해선</strong> 다음과 같은 내용의 /etc/init/airvideo.conf 파일을 생성해주시면 됩니다.
파일 소유권은 root:root로 하시면 됩니다.</p>
<pre><code>start on runlevel [2345]
stop on shutdown
respawn
exec sudo -H -n -u {계정명} LANG=ko_KR.UTF-8 /usr/bin/java -jar {jar파일경로}/AirVideoServerLinux.jar {설정파일경로}/test.properties
</code></pre>
<p>위에서 계정명은 동영상 파일의 소유자로 해주시면 됩니다. 위와 같이 설정하면 서버가 명시된 계정의 권한으로 구동되게 됩니다.
이제 서버를 껏다 켜도 AirVideo서버가 자동으로 실행됩니다. 수동으로 서버의 실행/종료를 하기 위해서는 다음과 같은 명령어를 이용하시면 됩니다.</p>
<pre><code>sudo start airvideo
sudo stop airvideo
</code></pre>
<p><strong>서버는 반드시 동영상 폴더의 소유자와 같은 계정으로 구동되로록 합니다.</strong> 그렇지 않은 경우 동영상이 목록에 제대로 표시가 안되거나 동영상의 소유자가 맘대로 바뀌어버리게 되므로 주의합니다.</p>
<p>AirVideo 어플에서 폴더 혹은 동영상이 제대로 표시가 안된다면 서버에서 지원하는 LANG 변수 문제입니다.
**LANG 변수가 반드시 UTF-8(제 경우에는 ko_KR.UTF-8)로 되어있어야 합니다. **혹 EUC-KR등 다른 값으로 설정되어 있는 경우에는 동영상 혹은 폴더 목록이 제대로 표시되지 않는 것 같습니다.
전 기본값이 UTF-8로 설정되어 있어 별 문제가 없었지만 혹시 EUC-KR로 설정되어 있는 경우에는 쉘 스크립트를 이용하여 UTF-8 환경에서 서버를 구동시키도록 합시다.</p>
<p><strong>AirVideo의 리눅스 버전의 서버는 소개된 버전으로는 port 변경이 불가능 합니다.</strong> 무조건 기본 포트(45631)로 열리게 되고,
혹시 해당 포트가 열려져 있다면 포트 번호를 하나 증가(45632)시켜서 서버를 구동시킵니다. 만약 해당 포트로도 이미 열려있다면 번호를 하나 더 증가(45633)시켜서 서버를 구동시킵니다.
최대 45개 까지 증가시켜 여는 것으로 보입니다. 하지만 <strong>전 서버 소스를 변경하여 포트를 지정하여 열 수 있도록 변경하여 사용하고 있습니다.</strong>
이 방법으로 제 서버에는 AirVideo 서버를 여러개의 포트로 여러 대 구동시켜 여러 사람이 사용하고 있습니다. 이 방법에 대해선 추후 포스팅을 통해 소개하도록 하겠습니다.</p>
<p><strong>서버를 구동시키는 계정의 home폴더에는 숨긴폴더로 .air-video-server 폴더가 생깁니다.</strong> 이 안에는 서버가 필요한 정보들을 저장하게 되는데,
각 동영상의 썸네일 정보를 포함한 각종 정보 등을 저장합니다. 이런 방법을 사용하는 이유는 다음에 서버가 다시 구동되어도 이런 정보를 다시 추출하기 위해 또 다른 작업을 하지 않게 하여
더 빠른 속도를 추구하기 위함입니다. 그리고 당연하지만, <strong>설정파일을 바꾼 후 서버에 적용하기 위해선 서버를 재시작</strong>해야 합니다.</p>
<p>제 홈서버는 아톰 d510에 메모리는 4GB입니다. 그리고 OS는 우분투 2.6.31-14 서버버전입니다. 이번에 설치한 서버를 통해 WIFI로 동영상을 봤을 때,
초반 버퍼링은 평균수준이었고, 중간에 끊기는 현상은 없었습니다. 3G의 경우 버퍼링이 좀 길었고 가끔씩 끊기는 경우가 있었지만, 특별히 문제될 정도는 아니었습니다.
특히 이미 인코딩된 동영상을 스트리밍받는 경우 초반 버퍼링의 시간은 많이 단축되었습니다. 이 정도의 서버로 이 정도 퍼포먼스면 상당히 만족스럽습니다.</p>
<p>좀 더 자세한 AirVideo 리눅스 서버 설치와 관련된 정보는 아래를 참조해 주시기 바랍니다.</p>
<ul>
<li>위키 (<a href="http://wiki.birth-online.de/know-how/hardware/apple-iphone/airvideo-server-linux">http://wiki.birth-online.de/know-how/hardware/apple-iphone/airvideo-server-linux</a>)</li>
<li>클리앙의 서버 설치 팁에 관한 글 (<a href="http://clien.career.co.kr/cs2/bbs/board.php?bo_table=lecture&wr_id=53889">http://clien.career.co.kr/cs2/bbs/board.php?bo_table=lecture&wr_id=53889</a>)</li>
<li>설치 관련 네이버 블로그 포스팅 (<a href="http://blog.naver.com/zetaloki?Redirect=Log&logNo=30084302362">http://blog.naver.com/zetaloki?Redirect=Log&logNo=30084302362</a>)</li>
</ul>
<p>[1] Window가 설치된 PC에 AirVideo를 설치하여 시용하기 위해선 PC가 항상켜져있어야 하지만 전력소모가 심합니다.
아니면 WOL기능이 있어 필요할때만 인터넷상에서 PC를 켤 수 있어야 하는데 제 PC에는 그런 기능이 없습니다.<br />
[2] 원래 PIN번호로 사용할 수 있어야 한다고 하는데, 실제로는 불가능했습니다. 이유는 모르겠지만 회사 운영팀의 네트워크 설정 때문이 아닌가 생각합니다.<br />
[3] 유료 어플은 2.99 달러에 판매되고 있습니다. 평가용무료어플은 목록에 최대 3개의 동영상밖에 안보입니다.</p>
[NIO] Java NIO 패키지의 ByteBuffer의 allocate()와 allocateDirect()메서드 속도 비교!2009-09-10T19:01:17+09:00http://http://eincs.com/2009/09/compare-allocate-allocatedirect-method-of-bytebuffer<p>과거에 Java NIO관련 포스팅을 몇 번 했습니다. 그리고 꼭 <code>allocateDirect()</code>를 이용하여 커널버퍼를 직접 사용하는 것을 권장 했습니다.
하지만 <code>allocateDirect()</code> 메서드는 <code>allocate()</code>메서드에 비해 굉장히 오버헤드가 심하므로
한번만 <code>allocateDirect()</code>를 쓰고 해당 <code>ByteBuffer</code>는 재활용 해야합니다.
필요할 때마다 만들어 쓰지 않고 한 번 만들어서 필요할 때마다 다시 써야 좋은 퍼포먼스를 낼 수 있습니다.
하지만 <code>allocate()</code>로 할당할때와 <code>allocateDirect()</code>로 할당 할 때의 속도 차이는 대체 얼마나 나는 걸까요. 직접 비교해 보았습니다.</p>
<h2>1. 테스트 코드 작성</h2>
<p>테스트 코드를 작성하여 속도 비교를 하려고 했습니다. <code>ByteBuffer</code>를 <code>allocate()</code> 메서드로 할당, <code>allocateDirect()</code> 메서드로 할 당 한 것,
그리고 byte 배열을 할당한 것 3가지 케이스를 테스트 하려고 합니다. 모두 1024바이트의 버퍼를 생성해 보겠습니다. 테스트 코드는 다음과 같습니다.</p>
<pre><code>long startTime = System.currentTimeMillis();
for(int i = 0; i&amp;lt;1000000 ; i++){
ByteBuffer buf = ByteBuffer.allocate(1024);
// ByteBuffer buf = ByteBuffer.allocateDirect(1024);
// byte[] buf = new byte[1024];
}
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
System.out.println("elapsedTime = " + elapsedTime)
</code></pre>
<p>위에서 주석 처리한 부분을 하나씩 풀어 실행 시킬 것 입니다.</p>
<h2>2. 퍼포먼스 비교</h2>
<p>각 모두 for문을 통해 백만번 할당을 하면서 결과를 비교하겠습니다. 단위는 밀리새컨드입니다.</p>
<table>
<thead>
<tr>
<th></th>
<th>byte[]</th>
<th>non-direct ByteBuffer</th>
<th>direct ByteBuffer</th>
</tr>
</thead>
<tbody>
<tr>
<td>1차</td>
<td>328</td>
<td>375</td>
<td>10063</td>
</tr>
<tr>
<td>2차</td>
<td>360</td>
<td>391</td>
<td>8719</td>
</tr>
<tr>
<td>3차</td>
<td>312</td>
<td>375</td>
<td>8656</td>
</tr>
<tr>
<td>4차</td>
<td>344</td>
<td>375</td>
<td>8625</td>
</tr>
<tr>
<td>5차</td>
<td>313</td>
<td>375</td>
<td>8875</td>
</tr>
<tr>
<td>6차</td>
<td>296</td>
<td>359</td>
<td>8516</td>
</tr>
<tr>
<td>7차</td>
<td>313</td>
<td>375</td>
<td>8625</td>
</tr>
<tr>
<td>8차</td>
<td>312</td>
<td>375</td>
<td>8594</td>
</tr>
<tr>
<td>9차</td>
<td>297</td>
<td>375</td>
<td>8641</td>
</tr>
<tr>
<td>10차</td>
<td>313</td>
<td>406</td>
<td>8734</td>
</tr>
</tbody>
</table>
<p>결과를 보면 byte 배열 할당보다 non-direct ByteBuffer 할당이 살짝 느린 것으로 나타납니다만 그렇게 큰 차이는 아닙니다.
channel을 이용할 수 있다는 점에서 때론 byte 배열 할당보다 non-direct ByteBuffer를 이용하는 것이 효과적일 때도 있으니 그리 큰 차이는 아닙니다.
하지만 direct ByteBuffer는 위 두가지 경우와 큰 차이를 보입니다. 속다가 25배가넘네요.</p>
<p>참고로 1차의 기록은 JVM초기화에 필요한 시간도 포함이므로 큰 의미는 없습니다.</p>
<h2>3. Direct ByteBuffer는 재활용 하자!</h2>
<p>보시는바와 같이 <code>byte[]</code>와 non-direct ByteBuffer에 비해 direct ByteBuffer의 할당 속도가 매우 느린 것으로 나타납니다.
따라서 <code>ByteBuffer</code>가 필요할때마다 그때그떄 할당해서 사용한다면 프로그램 퍼포먼스에 큰 영향을 줄 수 있습니다. 이 오버헤드는 무시 할 수 있을 것 같지 않습니다.
따라서 <code>ByteBuffer</code>를 재활용하여 사용합시다. 재활용 방법으로는 ByteBufferPool이 가능 할 것입니다. 자세한건 Pool패턴을 참고합시다.</p>
[NIO] FileSequence를 이용한 JAVA NIO와 IO의 퍼포먼스 비교해보면... NIO의 압도적인 승리!2009-09-09T21:15:21+09:00http://http://eincs.com/2009/09/compare-java-nio-and-io<p>프로그래밍을 하다보면 고유한 값이 필요할 때가 있습니다.
DB와 연관되서 받아와야 되는 key값이라면 순차적으로 만들어야 될 필요가 있을 수도 있고, 그것이 쓰레드간 동기화, 프로세스간 동기화가 보장되어야 하는 경우도 있겠지요.
멀티쓰레드 프로그래밍은 요즘 너무나 당연시 되고 있고, 여러 프로세스를 띄워 업무를 처리하게 만드는 아키텍쳐 또한 굉장히 레어한 일은 아니지요.
이런 일을 처리할 때 File로 구현한 Sequence가 필요하겠죠. Sequence는 오라클의 Sequence를 생각하시면 될거 같네요.</p>
<p>이전 포스팅에서 <a href="http://eincs.com/2009/08/java-nio-bytebuffer-performance/" title="File Queue를 이용하여 NIO와 일반 IO의 퍼포먼스를 비교">File Queue를 이용하여 NIO와 일반 IO의 퍼포먼스를 비교</a>해 본 적이 있습니다. 이제 FileSequence를 이용하여 퍼포먼스 비교를 해보겠습니다.</p>
<pre><code>import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Sequence {
private static int LONG_BYTE_LENGTH = 8;
private ByteBuffer buffer = ByteBuffer.allocateDirect(LONG_BYTE_LENGTH);
private String fileName = null;
private RandomAccessFile seqFile = null;
private FileChannel channel = null;
private FileLock lock = null;
public static void createSequenceFile(long value, String directory, String fileName) throws Exception
public static Sequence getNewInstance(String directory, String fileName) throws Exception
public static Sequence getNewInstance(String directory, String fileName, long value) throws Exception
private IBSequence(String directory, String fileName) throws Exception
public synchronized boolean open() throws SysException
public synchronized long next() throws SysException
public synchronized long currentValue() throws SysException
private void setValue(long value) throws IOException
private long getValue() throws IOException
}
</code></pre>
<p>자세한 구현은 어렵지 않으니 생략하였습니다.
<code>next()</code> 메서드로 값을 하나 증가시키면서 그 값를 얻을 수 있으며 <code>currentValue()</code> 메서드로 현재 값만 얻어오고 값을 증가 시키진 않을 수도 있습니다.
다음은 일반 IO를 이용한 <code>setValue()</code>, <code>getValue()</code> 메서드의 구현입니다. 보시는바와 같이 RandomAccessFile의 메서드를 이용하여 구현하였습니다.</p>
<pre><code>private void setValue(long value) throws IOException {
seqFile.seek(0);
seqFile.writeLong(value);
}
private long getValue() throws IOException {
long res = 0;
seqFile.seek(0);
res = seqFile.readLong();
return res;
}
</code></pre>
<p>다음은 NIO를 이용한 구현입니다. fileChannel과 ByteBuffer을 이용하여 구현하였습니다.
여기서 주의할 점은, 위에서 <code>ByteBuffer.allocateDirect(LONG_BYTE_LENGTH);</code> 로 ByteBuffer를 할당했다는 점입니다.
<code>allocateDirect()</code> 메서드로 할당했을 때와 allocate() 메서드로 할당했을 때의 퍼포먼스를 비교해보면 큰 차이는 안나는 듯 합니다.
하지면 여전히 <code>allocateDirect()</code> 메서드를 이용하는 것이 더 좋은 퍼포먼스를 냅니다.</p>
<pre><code>private void setValue(long value) throws IOException {
seqFile.seek(0);
buffer.clear();
buffer.putLong(value);
buffer.flip();
channel.write(buffer);
}
private long getValue() throws IOException {
long res = 0;
seqFile.seek(0);
buffer.clear();
channel.read(buffer);
buffer.flip();
res = buffer.getLong();
return res;
}
</code></pre>
<h2>자바 NIO와 일반 IO 퍼포먼스 비교해보기</h2>
<p>다음은 일반 I/O로 구현한 Sequence와 NIO를 이용하여 구현한 Sequence의 퍼포면서 비교 표입니다.
<code>next()</code>메서드와 <code>currentValue()</code>메서드를 for문으로 1000000번 호출하면서 걸린 시간을 비교해 보았습니다. 단위는 밀리세컨드 입니다.</p>
<table>
<tbody>
<tr>
<th></th>
<th>일반 IO로 구현</th>
<th>NIO (allocate)</th>
<th>NIO (allocateDirect)</th>
</tr>
<tr>
<td>1차</td>
<td>87814</td>
<td>17016</td>
<td>15844</td>
</tr>
<tr>
<td>2차</td>
<td>85236</td>
<td>16982</td>
<td>15562</td>
</tr>
<tr>
<td>3차</td>
<td>87642</td>
<td>17454</td>
<td>15656</td>
</tr>
</tbody>
</table>
<p>현저히 차이가 납니다. 자바 NIO는 ByteBuffer를 통한 퍼포먼스 향상은 거의 없고 Scalable하고 Efficient한 Server를 제작 할 수 있다는 것과
Non-Blocking IO를 제작 할 수 있다는 점에 의의가 있다고 말씀하시는 분들이 있습니다.
하지만 이전 포스팅에서도 확인했던 것과 같이 커널 버퍼를 이용함으로써 더 NIO에서 얻는 퍼포먼스 향상도 상당하다는 것을 알 수 있습니다.</p>
[WPF] WPF에서 멀티쓰레드 프로그래밍을 하자! WPF에서 Thread 프로그래밍은 매우 쉽다!2009-09-04T21:30:51+09:00http://http://eincs.com/2009/09/wpf-multi-threaed-programming<p>프로그래밍에 대해 중급개발자, 초보개발자를 나누는 기준 중 하나가 멀티쓰레드 프로그래밍을 할 수 있는가입니다.
그 만큼 최근 프로그래밍할 때에는 멀티쓰레드가 매우 중요한 요소중 하나입니다.
많은 일을 처리하면서 반응에 즉각적인 반응을 보이는 프로그램을 만들기 위해서는 멀티쓰레드로 만드는 것이 필수 적이죠.
그 뿐만이 아니라 Performance를 유지하기 위해서라도 멀티쓰레드 프로그래밍은 필수입니다.
WPF에서 멀티쓰레드 프로그래밍을 하는 방법에 대해 알아보겠습니다.</p>
<p>아시겠지만, C나 C++에서 쓰레드를 만드려면 굉장히 복잡한 과정이 필요합니다.
하지만 Java에서는 굉장히 쉬운 방법으로 멀티쓰레드 프로그래밍을 할 수 있게 되었죠.
쓰레드를 만드는 방법이나 동기를 유지하는 방법도 비교적 쉽게 작성할 수가 있죠.
하지만 <code>java.util.concurrent</code> 패키지가 제공 및 향상되기 전에는 여전히 고려해야 할 것들이 많은 작업이었습니다.</p>
<p>WPF와 C#에서도 쓰레드 관련 클래스들이 많은데요, 굉장히 좋은 기능을 제공하는 클래스들이 있습니다.
<code>BackGroundWorker</code>와 <code>ThreadPool</code>입니다. 이번 포스팅에서는 이 두 가지 클래스의 사용법에 대해 간단히 알아보고
WPF에서 멀티쓰레드 프로그래밍시 주의할 점 하나에 대해서 짚고 넘어가도록 하겠습니다.</p>
<h2>1. BackgroundWorker로 비동기적인 작업을 수행하자!</h2>
<p><a href="http://msdn.microsoft.com/ko-kr/library/system.componentmodel.doworkeventargs.argument(VS.95).aspx">BackgroundWorker</a>는 비교적 무거운 작업을 비동기적으로 백그라운드에서 처리할때 사용하기 좋은 클래스입니다.
무거운 작업인 경우에 메세지 루프에 영향을 주어서 프로그램의 반응성을 나쁘게 만드는데요, 이럴때 이 클래스를 이용하면 간단히 문제를 해결 할 수 있습니다.
특히 업무의 진행량이나 업무가 완성된 후에 수행해야 할 어떤 일이 있는 경우, 아주 쉽게 구현할 수 있게 해줍니다. 간단한 예제와 함께 설명하겠습니다.</p>
<pre><code>BackgroundWorker _backgroundWorker = new BackgroundWorker();
// BackgroundWorker의 이벤트 처리기
_backgroundWorker.DoWork += _backgroundWorker_DoWork;
_backgroundWorker.RunWorkerCompleted += _backgroundWorker_RunWorkerCompleted;
_backgroundWorker.WorkerReportsProgress = true;
_backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(_backgroundWorker_ProgressChanged);
// BackgroundWorker 실행
// 매개변수를 넣어 실행시키는 것이 가능합니다.
// 매개변수라 다수인경우, 배열을 사용하면 됩니다.
_backgroundWorker.RunWorkerAsync(5000);
</code></pre>
<p>여기서 Async는 비동기적으로 수행하라는 뜻입니다. BackgroundWorker를 실행시키기 전에 이벤트 처리기를 등록시켜줘야하는데요,
보시면 아시겠지만 다양한 이벤트를 발생시킵니다. 이름만 보면 어떤 이벤트인지는 대략 짐작하시겠지만 간략히 설명해보겠습니다.
DoWork는 실제로 업무를 정의하는 이벤트 처리기 입니다.
RunWorkerCompleted는 DoWork에서 정의한 업무가 모두 끝났을 때 발생하는 이벤트이고, ProgressChanged는 업무가 진행됨에 따라 진행률이 바뀌었을때 발생하는 이벤트입니다.
이 이벤트는 WorkerReportsProgress를 true로 설정해 주어야 발생합니다. 이제 각 이벤트 처리기에 대해 알아보도록 하겠습니다.</p>
<pre><code>void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e) {
// Do something
Object argument = e.Argument;
// BackgroundWorker에서 수행할 일을 정의합니다.
}
void _backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
// ProgressChanged
// 진행률에 변화가 있을때 이벤트가 발생합니다.
// 현재 얼마나 진행했는지 보여주는 코드를
// 이곳에 작성합니다.
}
// Completed Method
void _backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
if (e.Cancelled) {
statusText.Text = "Cancelled";
} else if (e.Error != null) {
statusText.Text = "Exception Thrown";
} else {
// Do Something
// 일이 모두 마쳤을때 수행되어야할
// 코드를 이곳에 정의합니다.
statusText.Text = "Completed";
}
}
</code></pre>
<p>주석으로 모두 처리를 했으니 쉽게 알 수 있으실 겁니다. 이정도로 설명드리면 <code>BackGroundWorker</code>의 사용법에 대해선 대충 아시게 되셨을겁니다.
좀 더 자세한 사항은 <a href="http://msdn.microsoft.com/ko-kr/library/cc221403(VS.95).aspx">관련 MSDN</a>을 보시면 더 자세히 살펴보실 수 있습니다.</p>
<p>한가지 주의할 점이 있는데, 이벤트처리기에서 UI객체를 참조하여 그 값을 직접 바꿀 수 없다는 점입니다.
다시 말해 <code>BackGroundWorker</code>의 <code>DoWork</code> 이벤트 처리기에서 <code>TextBox.Text="contents"</code> 와 같은 코드를 실행시키면
<code>InvalidOperationException</code>이 발생하면서 프로그램이 다운됩니다.
<code>RunWorkerCompleted</code> 이벤트 처리기에선 이점에 대해선 이번 포스팅 마지막에 설명하도록 하겠습니다.</p>
<h2>2. ThreadPool로 작업을 비동기적으로 처리하자!</h2>
<p><a href="http://msdn.microsoft.com/ko-kr/library/cc221403(VS.95).aspx">ThreadPool</a>클래스 입니다. 이름만 보고 이 클래스가 어떤 클래스인지 아신다면 어느 정도 멀티쓰레드 프로그래밍에 대해선 어느정도 아시는 분 이실테지요.
굉장히 유명한 패턴인 <code>ThreadPool</code> 패턴을 구현한 클래스입니다. 간단히 이 패턴에 대해 설명드리겠습니다.</p>
<p><code>ThreadPool</code> 패턴은 멀티쓰레드 프로그램에서 Thread생성의 오버헤드와 시스템 리소스를 효율적으로 사용하게 해줍니다.
프로그램 시작시 Thread를 미리 몇 개정도를 만들어두어 Thread를 이용해야 하는 어떤 업무가 발생했을때 ThreadPool에 있는 쓰레드를 바로 가져다 쓰고
업무가 끝났을때에는 ThreadPool에 Thread를 반납합니다. 만약 ThreadPool에 쉬고 있는 Thread가 없다면 Thread가 반납될 때까지 Blocking되어 기다립니다.
이로서 얻어지는 점은,</p>
<ul>
<li>Thread가 필요할때 이미 만들어진 Thread를 가져다 쓰므로 Thread생성에 필요한 오버헤드를 발생시키지 않아 속도가 빠르다.</li>
<li>Thread를 무한정 많이 생성하지 않기 때문에 프로그램이 돌아가고 있는 시스템에 과도하게 부하를 주지 않을 수 있다.</li>
</ul>
<p>굉장히 유용한 패턴이죠. 간단히 설명을 했지만 이해가 안되시는 분은 꼭 Google과 같은 검색엔진을 이용해서 ThreadPool에 대해 알아보도록 합시다.</p>
<p>ThreadPool은 static으로 이미 만들어진 Thread를 이용하는 클래스입니다.
WaitCallback이라는 클래스에 정의된 업무를 ThreadPool에 넣으면 그 업무를 ThreadPool에 있는 Thread에서 처리합니다.
사용법은 다음과 같습니다.</p>
<pre><code>private readonly WaitCallback HardWorkDelegate = new WaitCallback(this.HardWorkCallback);
private void HardWorkCallback(object oArgument) {
// do something
// 어떤 업무에 관한 코드를 정의합니다.
}
private void CallWaitCallback() {
// ThreadPool에 해당 업무를 시킵니다.
ThreadPool.QueueUserWorkItem(this.HardWorkDelegate);
}
</code></pre>
<p>매우 심플하게 구현이 가능합니다. BackGroundWorker에서 설명드렸습니다만, 역시 ThreadPool에서도 UI객체를 컨트롤 하는 것이 불가능 합니다.
이제 이 부분에 대해서 설명드리겠습니다.</p>
<h2>3. Thread에서 InvalidOperationException 문제 해결!</h2>
<p>일단 UI쓰레드와 비UI쓰레드에 대해서 설명드리겠습니다.</p>
<p>WPF에는 UI쓰레드와 비UI쓰레드가 있습니다. UI쓰레드는 UI객체를 수정 및 변경할 수 있는 쓰레드입니다.
비UI쓰레드는 UI객체를 변경하려고 시도하면 <code>InvalidOperationException</code>이 발생합니다. 제대로 처리가 안되죠.
WPF 어플리케이션의 UI쓰레드는 오직 메인 쓰레드 하나입니다. 따라서, 위의 방법으로 백그라운드에서 돌아가는 쓰레드의 경우 UI객체를 수정 및 변경할 수가 없습니다.</p>
<ul>
<li><strong>WPF에는 UI Thread와 Non-UI Thread가 있다.</strong></li>
<li><strong>UI Thread에서만 UI객체를 수정 및 변경할 수 있다.</strong></li>
<li><strong>UI Thread는 Main Thraed 단 하나이다.</strong></li>
<li><strong>백그라운드 Thread는 UI객체를 수정 빛 변경할 수 없다.</strong></li>
</ul>
<p>하지만 꼭 변경해야 하는 상황이 발생합니다. 이런 경우에는 비UI쓰레드에서 UI쓰레드로 UI객체를 변경하라고 알려줘야 합니다.
이 업무를 수행할 수 있도록 해주는 클래스가 바로 <a href="http://msdn.microsoft.com/ko-kr/library/system.windows.threading.dispatcher(VS.95).aspx">Dispatcher</a>입니다. 간단한 예제를 통해 사용법을 알아보도록 하겠습니다.</p>
<pre><code>private delegate void AddTextDelegate(Panel p,(); String text);
private void AddText(Panel p, String text)
{
p.Children.Clear();
p.Children.Add(new TextBlock { Text = text });
}
private void TestBeginInvokeWithParameters(Panel p)
{
if (p.Dispatcher.CheckAccess())
AddText(p, "Added directly.");
else
p.Dispatcher.BeginInvoke(new AddTextDelegate(AddText), p, "Added by Dispatcher.");
}
</code></pre>
<p>위 예제는 MSDN에 있는 예제입니다. <code>p.Dispatcher.CheckAccess()</code>를 통해 현재 자신의 쓰레드가 UI객체를 변경할 수 있는 지를 알아낼 수 있습니다.
만약 변경할 수 없다면 <code>p.Dispatcher.BeginInvoke()</code> 함수를 통해 UI객체를 수정 및 변경합니다.
불편합니다만, 이런 방법으로 비UI쓰레드에서 UI객체를 수정 및 변경 할 수 있습니다. WPF 아키텍쳐 때문에 이런 방식을 채용한 것입니다.</p>
<h2>4. WPF에서 멀티 쓰레드 프로그래밍을 하자!</h2>
<p>위 에서 다양한 방법으로 WPF에서 여러 쓰레드를 이용하는 방법에 알아보았습니다. UI객체와 관련된 이슈사항이 존재하지만 쓰레드를 이용하는 방법이 비교적 쉽다는 것을 알 수 있습니다.
과거 WPF로 프로젝트를 수행할 때, MFC를 다루던 분 께서 저가 멀티쓰레드를 사용하려고 하자 굉장히 힘들거라는 반응을 보였습니다.
C++에서는 쓰레드를 만드는 것이 매우 복잡한 일이기 때문이지요. 하지만 java와 C#에서 쓰레드를 사용하는 방법을 알려주자 굉장히 신기하게 생각했습니다.
멀티쓰레드 프로그램으로 더 좋은 퍼포먼스, 반응성 좋은 어플리케이션을 만듭시다!</p>
네이버, 뭔가 이상하다? 네이버 검색엔진은 title태그를 무시하는듯? - 검색엔진 최적화 기법!2009-08-27T15:27:10+09:00http://http://eincs.com/2009/08/strange-naver-search-engine<p>네이버 검색엔진이 이상하게 작동하는 것을 발견하였습니다. 제가 잘못생각한건지 네이버의 검색엔진이 이상한건지 잘 모르겠지만, 아무래도 네이버의 문제인 듯합니다.
혹시나 원인을 아시는 분은 알려주시기 바랍니다. 문제점이 무엇이냐를 말씀드리기 전에 검색엔진에 대해서 간략하게 설명하고 넘어가도록 하겠습니다.</p>
<h2>1. 검색엔진의 간략적인 구조</h2>
<p><img src="http://eincs.com/images/2009/08/strange-naver-0.jpg" alt="image0" /></p>
<p>검색엔진은 크게 검색서버, 리포지토리(Repository), 검색벡엔드로 나뉩니다. 검색서버는 그야말로 사용자에게서 검색 쿼리를 전달받아 그 결과를 응답해주는 서버입니다.
리포지토리는 검색엔진이 수집한 웹페이지의 정보를 담고 있는 데이터베이스라고 말할 수 있죠. 그리고 웹을 서핑하면서 웹페이지를 수집하는 부분이 검색벡엔드입니다.</p>
<p>검색백엔드의 부분을 간략히 알아보면 크롤러가 있습니다. 이 크롤러는 웹을 돌아다니며 웹페이지를 수집하죠. 그래서 붙여진 이름이 크롤러입니다. 웹페이지를 긁어 모은다는 뜻이겠죠.
이렇게 긁어 모은 웹페이지는 리포지토리(Repository)에 저장됩니다.
검색엔진은 이렇게 저장된 리포지토리에 인덱싱되어있는 웹페이지를 빠르게 검색하여 사용자에게 응답을 주는 것입니다.</p>
<h2>2. 웹페이지의 모든 단어가 같은 중요도를 갖는 것은 아니다!</h2>
<p><img src="http://eincs.com/images/2009/08/strange-naver-1.jpg" alt="image1" /></p>
<p>리포지토리에 크롤러가 수집한 웹페이지 정보를 저장할 때, 그냥 웹페이지 내용만을 넣는 것이 아닙니다.
위 그림처럼 각 페이지를 파싱하여 내용을 분해한 뒤, 그 속에서 단어를 추출하여 구조화하여 저장해 둡니다.
검색할 때마다 저장된 웹페이지를 읽어 파싱하여 결과를 만들어야 한다면 시간이 너무 오래 걸리고 검색 엔진 서버에도 상당히 부하를 주기 때문입니다.
이렇게 단어를 추출할 때 단순히 단어를 스캔하는 것이 아니라 html 문서의 구조에 따라 단어의 중요도를 다르게 둡니다.</p>
<p><img src="http://eincs.com/images/2009/08/strange-naver-2.jpg" alt="image2" /></p>
<p>예를 들어보겠습니다. 같은 단어라도 그냥 문서에 포함된 것과 h1 태그 안에 있는 단어는 중요도가 다른 것으로 인식합니다.
그냥 본문에 있는 단어에 비해 제목에 있는 단어는 문서의 내용을 더 잘 나타내 주는 것으로 판단하는게 당연하기 때문입니다.
특히 title 안에 있는 내용은 웹페이지 자체의 제목이기 때문에 그 중요도는 매우 높습니다.</p>
<p>이런 HTML 문서 구조를 고려하여 검색엔진의 상위에 노출되도록 하는 기법을 “검색엔진 최적화 기법” 이라고 합니다.
노출되어야할, 중요한 단어는 최대한 title, h1, h2…태그에 넣도록 하고, 자신의 블로그 포스팅간에 거미줄 처럼 링크를 시켜두면 검색엔진 상위에 노출확률이 커지게 되는거죠.
그래서 전 항상 포스팅의 주제목외에 부제목을 쓸때에는 h2, h3 태그를 사용하고 있습니다.</p>
<ul>
<li><strong>같은 단어라도 HTML문서 구조에 따라 중요도가 다르다.</strong></li>
<li><strong>title, h1 태그 안에 있는 단어가 중요도가 높다.</strong></li>
<li><strong>이를 이용한 상위 노출 기법을 “검색엔진 최적화 기법” 이라한다.</strong></li>
</ul>
<p>여러분들의 블로그에도 “검색엔진 최적화 기법”을 적용해 보세요.</p>
<h2>3. 네이버는 title 태그를 무시하나요?</h2>
<p><a href="http://eincs.com/2009/08/java-nio-bytebuffer-channel">최근에 Java NIO에 관련된 포스팅</a>을 여러개 하였습니다. 물론 검색엔진을 통해 유입되는 분들이 많았습니다.
유입 키워드와 유입 경로를 확인하던차 네이버를 통해 들어오신 분들이 많아 한번 들어가 봤습니다.
그런데 아래 그림과 같이 웹페이지의 제목이 ## 태그 안에 지정한 서브 제목으로 되어 있더랍니다.
그 중 한 <a href="http://eincs.com/2009/08/java-nio-bytebuffer-channel">포스팅</a>을 보시면 아시겠지만,</p>
<pre><code><h3>1.1. ByteBuffer의 사용 방법!</h3>
</code></pre>
<p>처럼 작성되어 있는 것을 아실 수 있을 겁니다.</p>
<p><img src="http://eincs.com/images/2009/08/strange-naver-3.jpg" alt="image3" /></p>
<p>네이버는 h3 태그 제목을 웹페이지 제목으로 인식하나요? h1 태그도 아니라 서브제목의 서브제목인 h3 태그를 말이죠.
혹시나 해서 구글도 검색해보았습니다.</p>
<p><img src="http://eincs.com/images/2009/08/strange-naver-4.jpg" alt="image4" /></p>
<p>구글에선 title 안의 제목을 웹페이지 제목으로 제대로 인식하는 걸로 보입니다. 네이버…뭔가 이상합니다.</p>
<p>h1 태그를 사이트 제목으로 쓴는것이 관례라는 東氣號太님의 덧글을 보고 찾아보았는데, <a href="http://web.archive.org/web/20090819153407/http://www.pageoff.net/921">H1 태그는 한번만 써야 되는지에 대한 고찰을 다룬 포스트</a>를 찾을 수 있었습니다.
포스트의 내용을 보면 h1태그를 사이트 제목으로 쓰면서 한번만 써야되는지는 결국 개발자의 마음대로 라고 결론을 짓고 있는것으로 보이는데,
포스트 내용이나 달린 댓글의 내용을 보면 실제로 많은 사이트에서,</p>
<pre><code><h1 class="logo">사이트제목</h1>
</code></pre>
<p>와 같은 형식으로 페이지의 제목을 표현하는 것이 관례가 되어 있다는 것을 알게되었네요.
아무래도 검색엔진이라면 관례를 따르겠죠. 이게 맞는것 같습니다. 저도 h1은 최대한 한번망 사용하도록 고쳐야겠네요.
지금은 본문에 h1를 남발하고 있거든요.</p>
<p>일단 기본적으로 적용되어 있는 제 블로그의 글들의 구조는 다음과 같았습니다.</p>
<pre><code><div class=”titleWrap”>
<h1> 타이틀 </h1>
</div>
<div class=”article”>
<h1> 1. 제목 </h1>
<div> 내용 </div>
<h2> 1.1. 소제목 </h2>
<div> 내용 </div>
<h3> 1.1.1. 좀 더 작은 소제목</h3>
<div> 내용 </div>
<h3> 1.1.2. 좀 더 작은 소제목</h3>
<div> 내용 </div>
<h2> 2.1. 소제목 </h2>
<div> 내용 </div>
<h3> 2.1.1. 좀 더 작은 소제목</h3>
<div> 내용 </div>
<h3> 2.1.2. 좀 더 작은 소제목</h3>
<div> 내용 </div>
<h1> 2. 제목 </h1>
<div> 내용 </div>
</div>
</code></pre>
<p>위 구조에서 네이버에서 제목으로 가져간 부분은</p>
<pre><code><h2>1.1. 소제목 </h2>
</code></pre>
<p>라고 볼 수 있습니다. 네이버에서 가정한 것은 다음과 같다고 생각합니다.</p>
<ul>
<li><strong>h1은 블로그 타이틀로 쓰인다고 가정. (관례적으로)</strong></li>
<li><strong>h2는 당연히 글 제목으로 가정. (관례적으로)</strong></li>
<li><strong>블로그는 title보다 글 제목으로 검색되는게 더 좋다고 가정.</strong></li>
<li><strong>네이버 검색엔진은 블로그를 크롤링 할 때는 h2 태그를 글 제목으로 인식.</strong></li>
</ul>
<p>따라서, 네이버 검색엔진에서는 h2부분을 블로글에 게시된 글 제목으로 판단한 것으로 보입니다.</p>
<p>생각해보면 일반적으로 페이지 제목은 title에 들어가는게 맞죠. 그런데 검색시 title보다는 글의 제목만 나타나면 더 좋겠죠.
왜냐면 title에는 쓸데없이 “블로그 이름 + 글 제목” 이 들어가는 경우가 대부분이니까요.
그래서 네이버 검색엔진은 글 제목만 따오는 방법으로 h2을 참조하는 방법을 사용한 듯 합니다.
따라서 네이버는 일반적인 관례에 따라 네이버는 많은 블로그에서 사용하는 관례에 특화된 HTML문서 해독 방법을 사용한 것이라고 말씀드릴 수 있겠네요.</p>
<ul>
<li>관례를 따르도록 합시다.</li>
<li>h1 태그에는 블로그 제목을 쓰도록 합시다. h1은 한번만 사용합니다.</li>
<li>h2 태그에는 글의 제목을 쓰도록 합시다.</li>
<li>본문에는 h3, h4, h5, h6 태그만 사용하도록 합시다.</li>
<li>스타일은 css파일에 넣어 사용합니다.</li>
</ul>
<p>역시 머리가 나쁘면 고생을 하는군요. 이런 관례에 대해 접할 수 있는 기회가 없었습니다.
하지만, 아무리 생각해도 검색엔진에서는 title태그에서 페이지 제목을 가져가는게 맞다고 생각합니다.
하지만 관례가 있다면 그것을 따르는 것도 맞다고 생각합니다. 하지만 네이버 검색엔진은 이런 관례를 지나치게 가정하고, 시스템에 적용하는게 아닌가 하는 것이 제 개인적인 생각입니다.
관례는 지켜 마땅하지만, 검색엔진에서도 이런 관례를 기정 사실로 가정하는건 너무하지 않나 싶습니다.
검색엔진은 거의 모든 웹페이지에 대해 범용적으로 적용되야 마땅하다고 생각하기 때문입니다.</p>
[NIO] JAVA NIO와 일반 I/O로 구현한 파일 큐를 통한 파일 입출력 Performance 비교!2009-08-26T17:38:51+09:00http://http://eincs.com/2009/08/java-nio-bytebuffer-performance<blockquote>
<p>기존의 Java IO는 다른 언어에 비해 매우 느리다는 이야기가 많이 있습니다. 내부적으로 어떻게 돌아가는지 대략적으로나마 파악한다면 그럴 수 밖에 없었다는 사실을 알게 되실겁니다.
하지만 jdk1.3부터는 Java IO의 한계를 보완한 Java NIO를 사용하여 I/O에서 속도 향상을 낼 수 있습니다. 그러나 NIO의 사용법은 기존 I/O와는 매우 달라 배우기가 생각만큼 쉽지는 않습니다.
이번 포스팅에서는 Java NIO에 대해 알아보고, 예제를 통해 FileHandling의 Performance를 향상시키는 간단한 예제를 다뤄 NIO에 쉽게 접할 수 있도록 하겠습니다.
생각보다 길어져서 포스팅을 세 개로 나누겠습니다.</p>
<ul>
<li>기존 Java NIO의 단점과 NIO에서 어떻게 단점을 보완했는가?<br />
<a href="http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/">[NIO] JAVA NIO의 ByteBuffer와 Channel로 File Handling에서 더 좋은 Perfermance내기!</a></li>
<li>Java NIO의 Class들과 이들의 기본적인 사용법에 대해 알아보자.<br />
<a href="http://eincs.com/2009/08/java-nio-bytebuffer-channel/">[NIO] JAVA NIO의 ByteBuffer와 Channel 클래스 사용하기!</a></li>
<li>실제 FileHandling하는 간단한 예제와 실제로 Performance를 비교해보자.<br />
<a href="http://eincs.com/2009/08/java-nio-bytebuffer-performance/">[NIO] JAVA NIO와 일반 I/O로 구현한 파일 큐를 이용하여 파일 입출력 Performance 비교!</a></li>
</ul>
<p>차근차근 포스팅하도록 하겠습니다. 제가 미흡한 점이 많습니다. 혹시 내용상 오류나 오탈자를 발견하신 분은 바로 댓글로 태클 걸어주시면 감사하겠습니다^^</p>
</blockquote>
<h2>1. 파일 큐 (File Queue)</h2>
<p>자료구조에 나오는 큐(queue)에 대해선 잘 아실겁니다. 선입선출의 특성을 가지는 Collection이죠. 다양한 방법으로 구현이 가능합니다.
이번 포스팅에서 고려하고자 하는건 파일 큐(File Queue)입니다. 말 그대로 큐에 들어가는 내용을 파일에 넣자는 거죠.</p>
<blockquote>
<p>파일 큐(File Queue)는 파일에 내용을 저장하는 큐를 의미한다!</p>
</blockquote>
<p><img src="http://eincs.com/images/2009/08/java-nio-file-queue-0.jpg" alt="image0" /></p>
<p>일반적으로 메모리상에 내용을 저장하는 것이 속도가 빠르다는 것은 자명한 사실입니다. 파일 입출력은 당연히 메모리 입출력보다 느리지요.
하지만, 메모리는 휘발성이라는 위험부담이 있습니다. 빠르면 좋겠지만, 시스템의 다운에도 큐에 저장하는 내용이 매우 중요한 경우에 파일 큐를 쓰게 됩니다.</p>
<ul>
<li><strong>파일 큐는 메모리 큐보다 느리다.</strong></li>
<li><strong>파일 큐는 시스템의 다운에도 안전하지만 메모리 큐는 시스템이 다운되면 큐에 쌓인 내용이 날아가 버릴 위험성이 있다. (메모리는 휘발성, 파일은 안전함)</strong></li>
<li><strong>시스템 다운에도 내용이 보전되어야 할 중요한 자료를 저장하는 큐는 파일 큐로 만든다.</strong>
ex) 은행에서 금전 거래에 대한 이벤트 정보 (이벤트 큐에 넣겠죠) 그 외에도 많은 곳에 적용 될 수 있습니다.
어쨋든 큐는 요청과 처리를 비동기적으로 처리하면서 속도를 올릴 때 꼭 필요한 방법이니까요.</li>
</ul>
<p>위의 문제 때문에 파일 큐가 필요하다는 것은 자명한 사실입니다.</p>
<h2>2. 파일 큐 (File Queue)를 구현하기</h2>
<p>파일큐의 구현 방법을 아주 간단히 설명하겠습니다. 일단 파일 큐는 다음 인터페이스를 구현할 생각입니다.</p>
<pre><code>public interface ByteFileQue {
public boolean open() throws IOException
public int put(byte[] srcBuf);
public byte[] get() throws IOException;
public byte[] peek();
public int size();
public boolean close();
public boolean isClosed();
}
</code></pre>
<p><code>open()</code>은 파일을 오픈, <code>close()</code>는 파일을 닫는 메서드입니다. <code>put()</code>, <code>get()</code> 함수는 각각 바이트 배열을 큐에 넣고 빼는 메서드입니다. 간단한 인터페이스죠.</p>
<p>각각 <code>ByteFileQue</code>를 구현할 두 클래스가 있습니다. 각각 <code>SimpleByteFileQue</code>, <code>NIOByteFileQue</code> 라고 이름을 짓겠습니다.
보시면 당연히 아시겠지만, <code>SimeByteFileQue</code>는 일반 java I/O로 구현한 파일 큐이고, NIOByteFileQue는 NIO로 구현한 파일 큐입니다.
두가지 모두 <code>RandomAccessFile</code>을 이용하여 구현 하면 됩니다. <a href="http://eincs.com/2009/08/java-nio-bytebuffer-channel/">RandomAccessFile을 NIO를 이용하여 입출력하는 방법은 이전 포스팅</a>을 참고합시다.</p>
<p>파일의 맨 앞부분에 헤더를 만듭니다. 헤더에는 tail, haed의 파일 포인터 위치와 size에 대한 위치를 저장합니다.
따라서 get할때에는 tail에 대한 위치를 읽어 <code>seek(tail)</code> 과 같은 방법으로 파일포인터를 이동한후 정해진 길이만큼 파일을 읽어 반환을 하면 됩니다.
한 가지 주의할 점은 반드시 <code>ByteBuffer</code>를 <code>allocateDirect</code>를 이용하여 할당해야 한다는 점입니다.</p>
<ul>
<li><strong>자바상에서 DMA의 도움을 받을 수 있는 Direct Buffer를 사용하려면 ByteBuffer를 사용하여야 함!</strong></li>
<li><strong>ByteBuffer.allocateDirect()메소드를 사용해야 Direct Buffer가 생성됨!</strong> </li>
</ul>
<p>쓰레드간 동기화 부분도 고려해야 되는데, <code>synchronized</code> 블록이나 <code>synchronized</code> 메서드를 사용하는 방법과, <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/util/concurrent/locks/ReentrantLock.html">ReentrantLock</a>을 사용하는 방법이 있습니다.
테스트 해보았는데, 두 방법 모두 비슷한 퍼포먼스를 내므로, 둘 중 아무 방법을 사용하셔도 무방합니다.
하지만 [ReentrantLock][1]은 프로세스간 동기화를 유지시켜주는 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/channels/FileLock.html">FileLock</a>과 함께 사용할 수 없습니다.
따라서 synchronized 블록 혹은 synchronized 메서드로 동기화 하는 방법을 추천해드립니다.</p>
<blockquote>
<p>FileLock은 jdk1.4부터 지원되는 클래스입니다. 기존 jdk에서는 JVM상 돌아가는 프로세스간 파일 동기화를 지원해주지 않았습니다.
때문에, 따로 파일을 둬서 파일을 읽는 중인지 혹은 그렇지 않은지를 표시하여 동기화를 유지하였다고 합니다. 매우 불편한 방법이죠.
하지만 이제는 자바에서 FileLock으로 매우 쉽게 프로세스간 동기화를 구현할 수 있습니다. 다만 FileLock은 쓰레드간 동기화를 유지시켜주지는 않습니다.</p>
</blockquote>
<p>자세한 것은 각자 구현해 보도록 합시다.</p>
<h2>2. NIO 파일 큐와 일반 파일 큐의 속도 차이 비교하기</h2>
<p>생각보다 확연하게 차이가 납니다. 속도 차이를 나타내는 자료를 봅시다.</p>
<p>아래 자료는 1분간 get 메서드와 put 메서드가 몇개의 자료를 넣고 뺄 수 있는지에 대한 수행 자료입니다. NIO쪽이 월등히 많다는 것을 볼 수 있습니다.
일반 FileQueue는 put 함수를 반복하여 수행했을때 1분동안 38만건 정도 put할 수 있었고, Nio FileQueue는 130만 건 정도 수행 할 수 있었네요. 엄청나게 월등하죠.</p>
<p><img src="http://eincs.com/images/2009/08/java-nio-file-queue-1.jpg" alt="image1" /></p>
<p>아래 자료는 각각 Thread를 두개를 돌려 한 쪽 쓰레드에선 Queue에 자료를 넣고, 다른 쪽 큐에서는 자료를 꺼내는 작업을 반복시킨 것입니다.
따라서 쓰레드간 동기화시 Blocking 되는 overhaed까지 포함된 결과 입니다. 넣고 꺼내는 자료는 100개부터 1,000,000개 까지 수행하였습니다.
걸리는 시간의 단위는 밀리새컨드 입니다. 위에서 put/get만 수행할 때만큼 속도 차가 나지는 않죠. 이것은 Thread 동기화시 blocking 현상때문입니다.
속도가 빠른만큼 blocking 될 확률이 높죠. 그럼에도 불구하고 NIO가 여전히 월등한 퍼포먼스를 보여줍니다.</p>
<p><img src="http://eincs.com/images/2009/08/java-nio-file-queue-2.jpg" alt="image2" /></p>
<p>보시는 바와 같이 NIO가 월등히 좋은 퍼포먼스를 내는 것을 보실 수 있습니다.</p>
[NIO] JAVA NIO의 ByteBuffer와 Channel 클래스 사용하기!2009-08-26T13:22:40+09:00http://http://eincs.com/2009/08/java-nio-bytebuffer-channel<blockquote>
<p>기존의 Java IO는 다른 언어에 비해 매우 느리다는 이야기가 많이 있습니다. 내부적으로 어떻게 돌아가는지 대략적으로나마 파악한다면 그럴 수 밖에 없었다는 사실을 알게 되실겁니다.
하지만 jdk1.3부터는 Java IO의 한계를 보완한 Java NIO를 사용하여 I/O에서 속도 향상을 낼 수 있습니다. 그러나 NIO의 사용법은 기존 I/O와는 매우 달라 배우기가 생각만큼 쉽지는 않습니다.
이번 포스팅에서는 Java NIO에 대해 알아보고, 예제를 통해 FileHandling의 Performance를 향상시키는 간단한 예제를 다뤄 NIO에 쉽게 접할 수 있도록 하겠습니다.
생각보다 길어져서 포스팅을 세 개로 나누겠습니다.</p>
<ul>
<li>기존 Java NIO의 단점과 NIO에서 어떻게 단점을 보완했는가?
<a href="http://eincs.com/2009/08/java-nio-bytebuffer-channel-file/">[NIO] JAVA NIO의 ByteBuffer와 Channel로 File Handling에서 더 좋은 Perfermance내기!</a></li>
<li>Java NIO의 Class들과 이들의 기본적인 사용법에 대해 알아보자.
<a href="http://eincs.com/2009/08/java-nio-bytebuffer-channel/">[NIO] JAVA NIO의 ByteBuffer와 Channel 클래스 사용하기!</a></li>
<li>실제 FileHandling하는 간단한 예제와 실제로 Performance를 비교해보자.
<a href="http://eincs.com/2009/08/java-nio-bytebuffer-performance/">[NIO] JAVA NIO와 일반 I/O로 구현한 파일 큐를 이용하여 파일 입출력 Performance 비교!</a></li>
</ul>
<p>차근차근 포스팅하도록 하겠습니다. 제가 미흡한 점이 많습니다. 혹시 내용상 오류나 오탈자를 발견하신 분은 바로 댓글로 태클 걸어주시면 감사하겠습니다^^</p>
</blockquote>
<p>이전 포스팅에서 기존 Java IO의 단점과 NIO가 이런 단점을 어떻게 보완했는지에 대해서 자세히 알아봤습니다. 이제 실제 NIO 사용방법에 대해 간단히 알아보겠습니다. 사실, 라이브러리의 모든 것을 자세히 알아보다간 포스팅만 길어지고 별로 도움도 안되기 때문에 간단히 사용법만 알아보고 예제 코드를 소개해 보도록 하겠습니다.</p>
<h2>1. NIO의 Buffer클래스</h2>
<p><img src="http://eincs.com/images/2009/08/java-nio-channel-0.jpg" alt="image0" /></p>
<p>이전 포스팅에서도 말씀드렸지만, NIO에서 지원하는 많은 Buffer 클래스 중 ByteBuffer 클래스만 Direct Buffer를 지원합니다.
다시 말해서, 커널 버퍼에 직접 접근할 수 있는 NIO의 장점을 이용하기 위해서는 ByteBuffer의 allocateDirect()라는 메소드를 이용해서 ByteBuffer를 만들어 내야 합니다.
(allocate()메소드를 이용하면 Direct Buffer가 아닌 일반 Buffer가 만들어집니다) 다음을 꼭 기억하도록 합시다!</p>
<ul>
<li><strong>ByteBuffer만이 Direct Buffer가 가능!</strong></li>
<li><strong>ByteBuffer.allocateDirect()메소드를 사용해야 Direct Buffer가 생성됨!</strong></li>
</ul>
<p>따라서 Direct Buffer, 즉 커널 버퍼를 직접 사용하기 위해서는 CharBuffer와 같은 익숙한 데이터 타입의 Buffer를 이용하지 못하고,
불편하더라도 어쩔 수 없이 ByteBuffer를 이용하여야 합니다. 생각보다 ByteBuffer를 사용하는 것이 어렵진 않으니 걱정하진 마세요.</p>
<p>사실 ByteBufffer나 다른 데이터 타입의 Buffer나 사용법은 매우 비슷하고, NIO를 잘 다루기 위해선 특히 ByteBuffer를 잘 다뤄야 하기 때문에
ByteBuffer를 중점적으로 소개하도록 하겠습니다. 사용법은 대동소이하니 ByteBuffer만 잘 익히신다면 다른 종류의 Buffer를 사용하는 것은 크게 어렵지 않으실겁니다.</p>
<h3>1.1. ByteBuffer 생성 방법!</h3>
<pre><code>ByteBuffer buf1 = ByteBuffer.allocate(10); // direct buffer를 이용하는 것이 아님.
ByteBuffer buf2 = ByteBuffer.allocateDirect(10); // 커널 버퍼를 직접 다루는 버퍼!
buf2.clear();
...
</code></pre>
<h3>1.2. ByteBuffer의 네 가지 포인터!</h3>
<p>Buffer 에는 현재 쓰거나 읽을 위치, 유효하게 읽을 수 있는 위치, 현재 용량의 위치 등을 나타내는 포인터가 네가지가 있습니다.
position, limit, capacity, mark 로 붙여진 이 네가지 포인터에 대해서 빠삭하게 숙지하고 계셔야 Buffer를 잘 사용하실 수 있습니다.
다음은 이 네 가지 포인터에 대한 간략한 설명입니다.</p>
<ul>
<li>**position **: 현재 읽을 위치나 현재 쓸 위치를 가리킵니다. ByteBuffer에서 get()함수로 읽기를 시도할 경우 position위치부터 읽기 시작하며,
put()함수로 ByteBuffer에 쓰기를 시도할경우 position 위치부터 쓰기를 시작합니다.. 읽거나 쓰기가 진행될 때마다 position의 위치는 자동으로 이동합니다.</li>
<li>**limit **: 현재 ByteBuffer의 유요한 쓰기 위치나 유효한 읽기 위치를 나타냅니다. 다시 말해, “이 버퍼는 여기까지 읽을 수 있습니다” 혹은 “여기까지 쓸 수 있습니다”를 나타냅니다.
헷갈리시죠? 자세한 사용법은 아래서 알아보도록 합시다. 다르게 말하면 “여기서부터는 쓸 수 없습니다”, “여기서부터는 읽을 수 없습니다” 라고 표현 가능합니다.</li>
<li><strong>capacity</strong> : ByteBuffer의 용량을 나타냅니다. 따라서, 항상 ByteBuffer의 맨 마지막을 가리키고 있습니다. 그 때문에 position과 limit와는 달리 그 위치랄 바꿀 수가 없죠^^</li>
<li><strong>mark</strong> : 편리한 포인터입니다. 특별한 의미가 있는 것은 아니고, 사용자가 마음대로 지정할 수 있습니다.
특별히 이 위치를 기억하고 있다가 다음에 되돌아가야할 때 사용합니다. 이 포인터에 대해선 차차 사용할 일이 있을 때 사용하실테고, 이 포스팅에선 자세히 다루지는 않겠습니다.</li>
</ul>
<p>위의 포인터 중 값을 사용자가 지정할 수 있는 position, limit, mark 에는 getter함수와 setter 함수가 있습니다.
특이하게도 일반적인 getter함수와 setter함수와는 다르게 get/set으로 시작하지 않습니다!
position의 경우엔 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/Buffer.html#position()">position()</a>이 getter이고, <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/Buffer.html#position(int)">position(int newPosition)</a> 이 setter입니다.
limit도 마찬가지입니다만, mark만 좀 다릅니다만, 여기서는 소개하진 안겠습니다.</p>
<p>여튼, 위의 네 가지 포인터간에는 다음과 같은 룰이 적용됩니다.</p>
<pre><code>**0<=mark<=position<=limit<=capacity**
</code></pre>
<p>이 룰을 어기면서 position이나 limit, mark를 setter로 강제로 지정한다면 Exception이 발생합니다.</p>
<p><img src="http://eincs.com/images/2009/08/java-nio-channel-1.gif" alt="image1" /></p>
<p>위 그림이 개념도 입니다. 읽거나 쓰기 시작하면 position에서 부터 읽거나 쓰는 것이 발생하죠. 일단 이 부분에 대해서는 그냥 이런게 있구나~하고 넘어가시면 되겠습니다. 직접 사용해보면서 어떤 것인지 알아가는 것이 확실하니까요.</p>
<h3>1.3. ByteBuffer에 읽고 쓰기!</h3>
<p>ByteBuffer 에 읽고 쓰는 함수에는 get()과 put() 이 있습니다. 기본적으로는 byte배열을 읽고 씁니다.
그 외에 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/ByteBuffer.html#putInt(int)">putInt()</a>, <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/ByteBuffer.html#order(java.nio.ByteOrder)">getInt()</a> 등 다양한 타임에 대한 get/set을 지원합니다. 당연히 int의 경우 4byte를 사용하게 되죠.
<a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/ByteBuffer.html#order(java.nio.ByteOrder)]">order()</a>함수로 빅엔디안, 리틀엔디안 방식을 지정할 수 있습니다만, C/C++와는 다르게 java에서는 기본적으로 빅엔디안 방식을 사용하기 때문에
네트워크 프로그래밍을 할 때도 byteOrder를 바꿀 일이 거의 없습니다.</p>
<h2>2. NIO의 Channel 클래스</h2>
<p>NIO의 Channel은 Buffer에 있는 내용을 다른 어디론가 보내거나 다른 어딘가에 있는 내용을 Buffer로 읽어들이기 위해 사용됩니다.
예를 들면 네트워크 프로그래밍을 할 때 Socket을 통해 들어온 내용을 ByteBuffer에 저장하기 위해서나, ByteBuffer로 Packet을 작성 후 Socket으로 흘려 보낼 때 Channel을 사용합니다.
이런 Channel을 ServerSocketChannel 이나 Socket Channel 이라고 합니다.
ServerSocketChannel이나 SocketChannel의 경우 Selector를 이용하여 Non-Blocking 하게 입출력을 수행 할 수 있지만,
FileChannel은 Blocking만 가능합니다. 이 점은, 운영체제나 시스템 마다 File 입출력시 Non-Blocking을 지원해주지 않는 시스템이 있어 그런 것이라고 합니다.
FileChannel은 Blocking 모드만 가능합니다! 이에 관해선 이번에 다루지 않을 Selector와 매우 깊은 관련이 있습니다.
이전 포스팅에 소개해 드렸던 Non-Blocking Server를 만드는 것과 관련이 깊으니 다음에 한번 소개해 보도록 하죠.</p>
<p>일단 지금 관심을 가지고 이야기 할 것은 FileChannel입니다. FileChannel은 바로 File에 있는 내용을 ByteBuffer로 불러오거나 ByteBuffer에 있는 내용을 File에 쓰는 역할을 합니다.
이에 대해서 자세히 알아봅시다. 그전에 꼭 알아야 할것은, Channel은 직접 인스턴스화 할 수가 없다! 입니다. 직접 생성자를 이용해서 인스턴스화하는 것이 아니라,
OutputStream이나 InputStream에서 getChannel() 메소드를 이용하여 만들어내야 합니다. 다음을 꼭 기억합시다!</p>
<ul>
<li><strong>Channel은 직접 인스턴스화 할 수가 없다!</strong></li>
<li><strong>OutputStream/InputStream에서 만들어야한다!</strong></li>
</ul>
<p>FileChannel을 얻는 방법은 다음과 같습니다.</p>
<pre><code>FileInputStream fis = new FileInputStream("test.txt");
FileChannel cin = fis.getChannel();
FileOutputStream fos = new FileOutputStream("test.txt");
FileChannel cout = fos.getChannel();
RandomAccessFile raf = new RandomAccessFile("test.txt", "rw");
FileChannel cio = raf.getChannel();
</code></pre>
<p>위에서 보시는 것과 같이 OutputStream 이나 InputStream을 통해 FileChannel을 얻으실 수 있습니다.
단순히 OutputStream이나 InputStream 외에도 RandomAccessFile과 같이 FileHandling하는 객체에도 getChannel()이라는 메서드가 있다면 FileChannel을 얻을 수 있습니다.
FileInputStream이나 OutputStream의 경우, 파일포인터가 읽거나 쓰면 무조건 증가 합니다. 따라서 순차적으로 읽을 때 적당합니다.
하지만 파일 내용을 이리저리 탐색하면서 처리해야할때는 Stream을 이용하면 매우 불편하고 효율적이지도 않습니다.
따라서 이런 경우엔 파일 임의의 지점에서 읽거나 쓸 수 있는 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/RandomAccessFile.html">RandomAccessFile</a> 클래스를 이용하여야 합니다.
제세한건 java api문서를 확인합시다!</p>
<p>FileChannel에서 읽고 쓰는 방법은 다음과 같습니다.</p>
<pre><code>FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt");
ByteBuffer buf = ByteBuffer.allocateDirect(10);
FileChannel cin = fis.getChannel();
FileChannel cout = fos.getChannel();
cin.read(buf); // channel에서 읽어 buf에 저장!
buf.flip();
cout.write(buf); // buf의 내용을 channel에 저장!
</code></pre>
<p>보시는 것과 같이 ByteBuffer에 있는 내용을 읽고 쓸 수가 있습니다. read함수를 쓰면, position위치에서부터 limit 위치까지의 내용을 FileInputStream의 내용으로 채워 넣습니다.
write함수를 쓰면, position위체에서부터 limit위치까지의 내용을 FileOutputStream에 출력합니다. 이미지로 설명하면 다음과 같습니다.</p>
<p><img src="http://eincs.com/images/2009/08/java-nio-channel-2.jpg" alt="image2" /></p>
<p><img src="http://eincs.com/images/2009/08/java-nio-channel-3.jpg" alt="image3" /></p>
<p>한 가지 의문점이 생겨야 좋은데요, 만약 InputStream으로 만든 FileChannel에서 read를 하지 않고 write를 수행하면 어떻게 될까요?
반대로, OutputStream에서 만들어낸 FileChannel에서 write를 하지 않고 raed를 하는 경우 어떻게 될까요? 위의 경우 Exception이 발생합니다.
한번 확인해 보시구요, 다음을 정리하도록 합시다.</p>
<ul>
<li><strong>InputStream으로 만들어낸 FileStream에선 read 만 할 수 있다! (write하는 경우 Exception 발생!)</strong></li>
<li><strong>OutputStream으로 만들어낸 FileStream에선 write 만 할 수 있다! (read하는 경우 Exception 발생!)</strong></li>
</ul>
<p>이건 FileChannel을 만들어낼때 사용한 객체의 특성때문이라고 생각하시면 될 것 같습니다.</p>
<p>그런데, <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/RandomAccessFile.html">RandomAccessFile</a>의 경우에는 어떨까요. <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/RandomAccessFile.html">RandomAccessFile</a>은 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/RandomAccessFile.html#seek(long)">seek</a>로 탐색한 파일포인터 위치에서 읽거나 쓸 수 있는 객체입니다.
당연하게도, read/wrtie 모두 수행 가능합니다. 하지만 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/RandomAccessFile.html#seek(long)">seek</a>으로 설정한 파일포인터 부터 읽거나/쓰기가 가능합니다.</p>
<h2>3. ByteBuffer와 Channel 을 이용한 File 읽고 쓰기</h2>
<p>간단하게 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/io/RandomAccessFile.html">RandomAccessFile</a>과 ByteBuffer, FileChannel을 이용하여 File의 읽고 쓰기의 방법에 대해 알아보겠습니다. 다음 코드를 보시죠.</p>
<pre><code>RandomAccessFile raf = new RandomAccessFile("sample.txt", "rw");
FileChannel channel = raf.getChannel();
ByteBuffer buf = ByteBuffer.allocateDirect(10);
buf.clear();
raf.seek(10); // 파일의 10째 바이트로 파일포인터 이동
channel.read(buf); // channel에서 읽어 buf에 저장!buf.flip();
raf.seek(40); // 파일의 40째 바이트로 파일포인터 이동
channel.write(buf); // buf의 내용을 channel에 저장!
channel.close();
raf.close();
</code></pre>
<p>위 프로그램의 내용은, sample.txt에 있는 내용중 10번째 바이트부터 10개의 바이트를 읽어 ByteBuffer에 저장 후, 방금 읽어드린 10개의 바이트를 파일의 40번째 바이트부터 출력하는 예제입니다.
매우 간단한 프로그램 이지만, 이부분만 잘 이해하신다면 FileChannel을 이용하여 더 빠른 File 입출력을 구현하시는데에는 큰 어려움이 없으실 겁니다.
참고로 덧붙여서 말씀드리면, 실제로 그냥 byte배열을 이용하는 것보다 위 프로그램이 좀 더 좋은 퍼포먼스를 보여줍니다.</p>
<p>위에서 쓴 함수 중, clear()함수는 당연히 아시겠고, flip()함수는 ByteBuffer에 저장한 후 그 데이터를 읽기 위해서 반드시 써줘야 하는 함수입니다.
limit를 현재 position으로 설정한 후, position을 0으로 설정하는 함수인데, 그 원리를 잘 생각해보세요. flip()을 쓰지 않으면 position이 쓰기를 마친 지점에 그대로 있습니다.
이럴 경우 buffer에서 read를 수행하면 방금 write 한 것이 읽혀지는게 아니라 쓰고난 다음 index부터 읽혀집니다.</p>
[NIO] JAVA NIO의 ByteBuffer와 Channel로 File Handling에서 더 좋은 Perfermance 내기!2009-08-21T11:51:40+09:00http://http://eincs.com/2009/08/java-nio-bytebuffer-channel-file<blockquote>
<p>기존의 Java IO는 다른 언어에 비해 매우 느리다는 이야기가 많이 있습니다. 내부적으로 어떻게 돌아가는지 대략적으로나마 파악한다면 그럴 수 밖에 없었다는 사실을 알게 되실겁니다.
하지만 jdk1.3부터는 Java IO의 한계를 보완한 Java NIO를 사용하여 I/O에서 속도 향상을 낼 수 있습니다. 그러나 NIO의 사용법은 기존 I/O와는 매우 달라 배우기가 생각만큼 쉽지는 않습니다.
이번 포스팅에서는 Java NIO에 대해 알아보고, 예제를 통해 FileHandling의 Performance를 향상시키는 간단한 예제를 다뤄 NIO에 쉽게 접할 수 있도록 하겠습니다.
생각보다 길어져서 포스팅을 세 개로 나누겠습니다.</p>
<ul>
<li>기존 Java NIO의 단점과 NIO에서 어떻게 단점을 보완했는가?
[[NIO] JAVA NIO의 ByteBuffer와 Channel로 File Handling에서 더 좋은 Perfermance내기!][related0]</li>
<li>Java NIO의 Class들과 이들의 기본적인 사용법에 대해 알아보자.
[[NIO] JAVA NIO의 ByteBuffer와 Channel 클래스 사용하기!][related1]</li>
<li>실제 FileHandling하는 간단한 예제와 실제로 Performance를 비교해보자.
[[NIO] JAVA NIO와 일반 I/O로 구현한 파일 큐를 이용하여 파일 입출력 Performance 비교!][related2]</li>
</ul>
<p>차근차근 포스팅하도록 하겠습니다. 제가 미흡한 점이 많습니다. 혹시 내용상 오류나 오탈자를 발견하신 분은 바로 댓글로 태클 걸어주시면 감사하겠습니다^^</p>
</blockquote>
<h2>1. 기존 JAVA IO 왜 느리다고 했을까?</h2>
<p>Java가 다른 언어보다 느리다는 이야기가 많이 있습니다. 연산이 중요한 작업(CPU-Intensive)이라 C++로 짜는 것이 최선이거나,
CPU레벨에서 제공하는 방법을 이용하면 최적화가 가능하지만 Java로 짜면 이용하지 못하는 작업들이 있습니다.
이런 작업들은 Java 같은 언어로 짜기에는 퍼포먼스가 너무 안나와서 문제가 됩니다. 이런 작업들은 Java가 아닌 C++같은 언어로 작성해야겠죠.
하지만 대부분의 비즈니스 로직은 Java로도 충분한 퍼포먼스를 낼 수 있습니다. 일반적인 경우에 Java가 다른 언어보다 느리다는 말은 아마 <strong>I/O와 Swing 때문</strong>이 클 것 입니다.</p>
<p>Java 프로그래머라면 Java API로 IO를 많이 다뤄보셨을겁니다. <code>java.io</code> 패키지의 클래스로서, byte배열의 입출력을 담당하는 <code>OuputStream</code>, <code>InputStream</code> 외에
문자타입 자료의 입출력을 담당하는 <code>Wirter</code>와 <code>Reader</code>가 그것입니다. 물론 데코레이터패턴으로 다양한 방법으로 이용할 수 있는 클래스들도 있죠.</p>
<p>몇 가지 예시를 살펴보죠.</p>
<p><img src="http://eincs.com/images/2009/08/java-nio_bytebuffer-0.jpg" alt="image0" />
<img src="http://eincs.com/images/2009/08/java-nio_bytebuffer-1.jpg" alt="image1" /></p>
<p>실제 모양새를 상당히 추상화 시켜놓긴 했습니만 이해하기는 쉬우실 겁니다.</p>
<p>어쨋든 위와 같이 File에 문자 기반 I/O를 사용하기 위해선 File path <code>FileWriter</code>나 <code>FileReader</code>를 만들고 추가 기능을 위해
<code>PrintWirter</code>, <code>PrintReader</code>등의 클래스, 버퍼기능을 추가하여 속도향상을 하기 위해선 Buffered라는 접두어가 붙은 클래스를 이용하면 되었습니다.
참으로 데코레이터패턴을 효과적으로 쓴 케이스라고 할 수 있겠네요. 덕분이 처음 배우는 사람도 그나지 어렵지 않게 사용 할 수 있었습니다.</p>
<p>하지만 이런 기존 Java I/O는 상당히 느리고 비효율적이라는 평가를 많이 받았습니다. 실제로 그럴수밖에 없는 이유를 살펴보면 크게 두 가지입니다.
첫 번째 이유는 OS에서 관리하는 커널 버퍼에 직접 접근할 수 없었던 것이고, Blocking I/O여서 매우 비효율적이었다는 것이 두 번째 이유입니다. 이에 대해 자세해 살펴 보도록 하죠.</p>
<h3>1.1. 기존 자바 IO는 커널 버퍼를 직접 핸들링 할 수 없어서 느리다!</h3>
<p>기존 자바 IO에서는 커널 버퍼를 직접 접근하는 소위 Direct Buffer를 핸들링 할 수가 없었습니다.
소켓이나 파일에서 Stream이 들어오면 커널 버퍼에 쓰여지게 되는데, Code상에서 이에 접근 할 수 있는 방법이 없었기 때문입니다.
따라서 JVM이 JVM 내부의 메모리에 불러온 후에야 이 데이터에 접근 할 수 있었는데 “커널에서 JVM내부 메모리로 복사한다”라는 오버헤드가 있었기 때문에 느렸던 거죠.
JVM 내부의 메모리라고 하면 <code>int</code>, <code>char</code>변수의나 <code>byte[]</code> 등 자료형 이겠죠.
<code>int</code>, <code>char</code>, <code>long</code> 같은 primitive type 은 당연히 JVM내부에서 프로세스별로 할당된 스택에 저장되겠고,
배열은 JVM 내부 힙 메모리에 저장되겠죠. 말로만 하면 어려우니 그림과 함께 설명하도록 하겠습니다.</p>
<p><img src="http://eincs.com/images/2009/08/java-nio_bytebuffer-2.jpg" alt="image2" /></p>
<p>기존의 Java IO가 디스크에서 파일을 읽을 때의 과정은 다음과 같습니다.</p>
<ul>
<li>Process(JVM)이 file을 읽기 위해 kernel에 명령을 전달</li>
<li>Kernel은 시스템 콜(read())을 사용하</li>
<li>디스크 컨트롤러가 물리적 디스크로 부터 파일을 읽어옴</li>
<li>DMA를 이용하여 kernel 버퍼로 복사</li>
<li>Process(JVM)내부 버퍼로 복사</li>
</ul>
<p>따라서 다음 과 같은 문제점 이 생길 수 있죠.</p>
<ul>
<li>JVM 내부 버퍼로 복사할 때, CPU가 관여</li>
<li>복사 Buffer 활용 후 GC 대상이 됨</li>
<li>복사가 진행중인 동안 I/O요청한 Thread가 Blocking</li>
</ul>
<p>위 와 같은 오버헤드가 생길 수 있습니다. 이에 대해 자세히 알아보도록 하겠습니다.</p>
<h4>소중한 CPU 자원이 낭비되기 때문에 느리다.</h4>
<p>CPU가 관여해버기리 때문에, 버퍼가 복사하는 것 자체가 리소스를 잡아먹게 됩니다. 큰 오버헤드죠.
물리적 디스크에서 커널영역으로 복사하는 것은 DMA의 도움으로 CPU가관여하지 않기 때문에, 오버헤드가 매우 적습니다.
따라서, 만약 CPU 자원을 사용하여 내부 버퍼로 복사하지 않고 직접 커널 버퍼를 사용한다면 중요한 CPU 자원을 다른 곳에 써서 더 효율적으로 프로그래밍이 가능하겠죠.
디스크나에서 커널버퍼에 복사하는 과정은 CPU가 관여하지 않고 DMA가 해줍니다. CPU가 관여하지 않는다는 것은 CPU의 자원을 다른 곳에 쓸 수 있다는 것을 의미하죠.</p>
<h4>내부 버퍼로 사용한 메모리가 GC 대상이 되기 때문에 느리다.</h4>
<p>그리고 기존 Java I/O에서 내부 버퍼로 사용한 데이터 변수 - 기본적으로는 배열이 되겠죠 - 가 GC의 대상이 됩니다.
일단 버퍼로 사용하고 난 후에는 필요가 없어지기 때문이죠. Java에서 GC는 오버헤드입니다.
물론 JVM에 나올 때마다 GC의 성능이 매우 좋아지고 있긴 합니다만여전히 C나 C++에 비해 오버헤드이기 때문에 코딩을 할 때 최대한 피해야 합니다.</p>
<p>마지막 Blocking에 관한 부분은 다음 절에서 이야기 해보도록 하겠습니다.</p>
<h3>1.2. 기존 자바 IO는 Blocking IO 라서 느리다!</h3>
<h4>Thread에서 블로킹이 발생하기 때문에 느리다.</h4>
<p>위의 그림에서, 복사해올 때, I/O 요청한 Process, 정확히는 Thread가 블로킹됩니다.
OS에서는 디스크를 읽는 효율을 높이기 위해 파일에서 최대한 많은 양의 데이터를 커널 버퍼에 저장합니다.
따라서 기존 Java I/O 에서는 커널 버퍼에서 JVM 내부 버퍼로 복사하는 동안 다른 작업을 못하게 됩니다.
만약 또 커널 버퍼에 직접 접근 할 수 있다면 Thread가 복사하는 시간에 다른 작업을 할 수 있겠죠.</p>
<h4>기존 Java I/O로는 끔찍하게 비효율적인 Server Program을 만든다.</h4>
<p>Blocking 관련된 문제점은 이 뿐만이 아닙니다.
기존 Java I/O를 이용한 Server-Client Network 프로그램을 만들 때 더더욱 심각한 문제점을 야기합니다.
이에 대해선 간단한 예제와 함께 알아보도록 하죠.</p>
<pre><code>ServerSocket server = new ServerSocket(10001);
System.out.println(“접속을 기다립니다.”);
while(true){
Socket sock = server.accept();
Service service = new Service (sock);
service.start();
serviceList.add(service);
}
</code></pre>
<p>일번적으로 Java에서 소켓 프로그래밍을 할 때, 위와 같이 Server를 작성하죠. 아래는 Service라는 클래스를 정의한 코드입니다.</p>
<pre><code>class Service extends Thread {
private Socket socket = null;
private OuputStream out = null;
private InputStream in = null;
public Service(Socket socket)
{
this.socket = socket;
out = socket.getOutputStraem();
in = socket.getInputStream();
}
public run()
{
while(true) {
String request = in.read();
String response = processReq(request);
out.write(response);
}
}
}
</code></pre>
<p>기존의 Java로는 위와 같이 Server 프로그램을 작성했습니다. 위와 같이 작성하게되면 다음과 같은 특성이 있습니다.</p>
<ul>
<li>클라이언트가 접속할 때마다 블로킹됩니다.</li>
<li>클라이언트 접속할 때마다 Thread가 생성됩니다.</li>
</ul>
<p>이런 특성이 어떤 문제를 발생시키는지 알아보도록 하겠습니다.</p>
<p><strong>기존 I/O의 Server는 블로킹 되므로 느리다.</strong></p>
<p>클라이언트가 접속을 하게되면 새로운 Thread를 생성해야합니다. 자바에서 Thread를 생성하는건 c나 c++에 비하여 비교적 쉽지만 내부적으로는 매우 복잡한 프로세스를 가지고 있습니다.
따라서 Thread 생성하는 것은 비교적 시간이 오래 걸리는 오버헤드가 발생하는 작업입니다. 만약 한 클라이언트가 접속 후 짧은 시간 후에 다른 클라이언트가 Server에 접속을 시도한다면
Connection이 맺어지기 위해선 바로 앞서 접속한 클라이언트의 Thread가 모두 생성되기를 기다려야 합니다.
이렇게 waiting 해야 한다는 점에서 blocking I/O라고 볼 수가 있습니다.</p>
<p>이 뿐만 아니라 Service 클래스 내부에서 Socket resquest와 response를 사용할때도 blocking 됩니다.
이 점은 위에서도 언급했던 내용인데, socket에서 들어온 패킷을 읽기위해 read()함수를 호출하면 I/O를 요청 한 것이므로 해당 Thread가 블로킹 됩니다.</p>
<p><strong>클라이언트 접속할 때마다 Thread가 생성됩니다.</strong></p>
<p>클라이언트가 접속할 때마다 Thread가 생성되는데, 클라이언트가 접속 종료 후에는 Thread가 사용 중지가 되므로 GC대상이 됩니다.
Client가 빈번히 접속하고 접속을 끊는 네트워크 환경에서 큰 문제가 될 수가 있겠죠. 앞서 말했듯이 GC는 Java에서 매우 큰 오버헤드입니다.
물론 ThreadPool을 사용한다면 Thread가 GC되는 것에 대한 오버헤드는 줄일 수 있습니다만 여전히 또 다른 문제가 발생합니다. ThreadPool은 Pool이라는 패턴입니다.
Thread를 미리 N개를 생성해놓고 관리하고 있다가, Thread가 필요하게 되면 ThreadPool에서 하나를 꺼내 씁니다. 자원을 모두 사용하면 GC해버리는 것이 아니라
ThreadPool에 다 사용했음을 알리면 다음 번에 다시 꺼내 쓸 수 있습니다.
Java의 장점이나 단점중 하나인 GC의 오베헤드를 줄이기 위한 방법 중 하나로 꽤 자주 쓰이는 패턴입니다.</p>
<p>만약 서버에 매우 많은 수의 클라이언트가 접속을 시도하게 된다면 그 만큼의 Thread가 필요하게 됩니다. 그만큼 서버의 자원이 낭비되는것이죠.</p>
<p>위와 같은 이유로 기존의 Java I/O는 C나 C++과 같이 직접 시스템 콜을 사용하여 입출력을 하는 언어보다 느립니다. 후에 또다시 간단히 설명하겠지만,
C나 C++에서는 select()와 같은 시스템콜을 사용하기 때문에 서버 프로그램에서 클라이언트마다 Thread를 만들 필요가 없습니다.
Java NIO에서는 select() 시스템콜을 간접적으로나마 사용할 수 있도록 지원해줘서 Thread가 많이 필요하지 않으면서도 좋은 성능을 내는 서버를 만들 수 있도록 기술을 제공하고 있습니다.
하지만 jdk1.3부터 새로 생긴 java.nio 패키지의 NIO가 기존 Java I/O의 문제점을 어떻게 해결했는지 알아보도록 하겠습니다.</p>
<h2>2. NIO는 왜 기존 Java I/O 더 빠른가?</h2>
<p>위에서 기존의 Java I/O의 단점 대해 살펴봤습니다. 위와 같은 문제점이 있는 Java I/O는 jdk 1.3에 와서야 그 단점을 보완하는 NIO가 등장하게 되었습니다.
시스템 서비스를 사용하지도 못하고 여기저기 헛점 투성이인 Java I/O 의 문제점이 jdk 1.3에서야 보완된 이유는 Java의 철학 때문 이겠죠.
Java의 가장 중요한 특성인 플랫폼 간의 이식성, 다시말해 Once Write, Run AnyWay! 를 지키기 위해서 각 OS별로 system call이나 커널을 직접 이용하는 것은 기술적으로 매우 어려운 일이었습니다.
jdk 1.3에서야 통일된 인터페이스로 각 시스템 별로 네이티브 언어를 이용하여 그 기능을 구현해주는 노가다가 완성된거죠.
여튼, 각설하고 NIO가 어떻게 Java I/O를 단점을 보완했는지에 대해서 간단히 알아보도록 하겠습니다. 자세한건 다음 포스팅에서 설명할 것이기 때문에 내용이 짧아 질 것 같군요^^</p>
<h3>2.1. NIO는 Direct Buffer 로 커널 버퍼를 직접 핸들링하기 때문에 빠르다!</h3>
<p>기존 Java I/O에서의 JVM 내부 메모리로의 복사문제를 해결하기 위해 NIO에서는 커널 버퍼에 직접 접근할 수 있는 클래스를 제공해줍니다.
Buffer클래스들이 그것인데요, 내부적으로 커널버퍼를 직접 참조하고 있습니다. 일종의 포인터 버퍼라고 볼수있는데 운영체제가 제공해주는 효율적인 I/O 핸들링 서비스를 이용 할 수 있게 해줍니다.
따라서 위에서 발생한 복사문제로 인해 CPU자원의 비효율성, I/O 요청 Thread가 Blocking 되는 문제점 등이 해결될 수 있는 것이죠.</p>
<p><img src="http://eincs.com/images/2009/08/java-nio_bytebuffer-3.jpg" alt="image3" /></p>
<p>위 그림을 살펴보면 Buffer에는 여러가지 자료형을 지원합니다만, DirectBuffer는 [ByteBuffer][1]만 지원합니다.
따라서 커널 버퍼를 직접 사용하고 싶다면 불편하더라도 ByteBuffer만 사용해야합니다. Buffer를 만드는 방법은 다음과 같습니다.</p>
<pre><code>ByteBuffer buf = ByteBuffer.allocate(10);
ByteBuffer directBuf = ByteBuffer.allocateDirect(10);
</code></pre>
<p>아랫줄의 코드와 같이 사용하여야 커널 버퍼를 직접 이용하는 것입니다. 윗 줄은 기존 방식과 같은 거죠. JVM내부에 메모리가 할당 됩니다.</p>
<p>Direct Allocate [ByteBuffer][1]의 put(), get(), position(), flip(), clear() 등의 메소드로 커널 버퍼를 핸들링 할 수 있습니다. 자세한건 다음 포스팅에서 알아보도록 하겠습니다.</p>
<p>이번 포스팅에서는</p>
<blockquote>
<p>NIO에서는 커널 버퍼를 직접 이용할 수 있게 해주는 Buffer Class를 지원한다!
커널 버퍼를 직접 이용할수 있는건 <code>ByteBuffer.directAllocate(SIZE);</code> 로 생생된 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/ByteBuffer.html">ByteBuffer</a>뿐이며, 다른 Buffer들은 기존의 방식과 똑같다.</p>
</blockquote>
<p>만 기억하시면 됩니다.</p>
<h3>2.2. NIO에서 System Call을 간접적으로 사용가능하게 해주기 때문에 빠르다!</h3>
<p>위 에서 살펴보았던 서버프로그램의 예제를 기억하시나요? 엄청난 수의 Thread가 생성되고 GC가 되어야 했죠. NIO에서는 이점을 해결 했습니다.
c나 c++로 만들어진 Server Program은 Thread를 생성하지 않고도 많은 수의 클라이언트를 처리할 수 있습니다.
이를 가능케 해주는 것이 OS 레벨에서 지원하는 Scatter/Gather 기술과 Select() 시스템 콜입니다. Scatter/Gather은 시스템콜의 수를 줄이는 기술인데요, 덕분에 I/O를 빠르게 만들 수 있죠.
c나 c++에서는 이런 OS수준의 기술들을 이용하여 I/O속도를 향상시켜왔지만 java에서는 이런 시스템에서 제공하는 기술을 사용할 수 있는 방법이 없었죠. 하지만 NIO에서는 가능합니다.
이런 것을 가능하게 해주는 Class가 바로 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/channels/Channel.html">Channel</a>과 <a href="http://java.sun.com/j2se/1.5.0/docs/api/java/nio/channels/Selector.html">Selector</a>입니다.</p>
전위 표기법을 후위 표기법으로 바꾸기 - recursive2008-10-13T15:16:09+09:00http://http://eincs.com/2008/10/prefix-to-postfix-recursive<blockquote>
<p>Write a C function that transforms a prefix expression into a postfix one. Carefully state any assumptions you make regarding the input. How much time and space does your function take?</p>
</blockquote>
<p>전위표기법의 식을 후위표기법으로 변환하는 함수를 작성하는 문제이다. 이 문제를 해결하기 위해서 입력에 대한 몇 가지 가정을 하였다.</p>
<ul>
<li>식의 입력은 문자열로 한다.</li>
<li>입력되는 식은 모두 표현될 수 있는 전위표기법의 식이다. 잘못된 식인 경우 오류가 발생한다. (제대로 실행이 안되며, 전위표기법인지 확인하는 예외처리 매커니즘이 필요하다)</li>
<li>입력되는 식의 길이가 80이하로 가정한다.</li>
<li>연산자를 제외한 모든 문자는 피연산자로 간주한다. (_ ^ 등도 피연산자)</li>
</ul>
<p>이 문제를 해결하는 방법에 대해 생각해 보았다.</p>
<ul>
<li>중위표기법으로 변형했다가 다시 후위표기법으로 변형한다.</li>
<li>트리를 이용한다.</li>
<li>스택을 이용한다.</li>
<li>재귀 함수를 이용한다.</li>
</ul>
<p>첫번째와 두번째경우 중간과정을 거치는 것 해법이므로, 일단 전위표기법을 한 번에 후위표기법으로 만드는 함수를 구현하기로 한다. 세번쨰와 네번째의 기본적인 과정은 똑같다. 연산자가 단 한 개인 전위표기법을 후위표기법으로 바꾸는 것은 매우 쉽다.</p>
<pre><code>operator operand1 operand2 (prefix expression)
operand1 operand2 operator (postfix expression)
</code></pre>
<p>이렇게 간단한 식은 변형하기가 매우 쉽다. 연산자가 매우 많은 복잡한 식도 이 기본적인 룰을 이용하면 고칠 수 있다. 복잡한 식도 기본적으로는 이 하나의 룰로 변형이 되기 때문이다. 이 방법을 이용해 재귀적으로 문제를 쉽게 풀 수 있다.</p>
<pre><code>#define TRUE 1
#define FALSE 0
#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
char *PreToPostFix(char *, char *);
int isoperator(char );
char *PreToPostFix(char *Pre, char *Post)
{
int index; // operand1과 operand2의 사이를 가르키는 index
int NumOperator=0; // 피연산자를 구분할 때 쓰이는 연산자 갯수 카운터
int NumOperand=0; // 피연산자를 구분할 때 쓰이는 피연산자 갯수 카운터
int PreLen = strlen(Pre); // 주어진 prifix expression의 길이
char Operator[2]; // 연산자를 저장할 문자열
char operand1[80]; // 피연산자1을 저장할 문자열
char operand2[80]; // 피연산자2를 저장할 문자열
if(PreLen == 1) { // 길이가 1인 경우에 자기자신을 return
strcpy(Post, Pre);
return Post;
}
*operand1 = '\0'; // string.h 함수를 이용하기위해 초기화
*operand2 = '\0';
*Operator = *Pre; // operator를 얻는다
*(Operator+1) = '\0';
for(index=1; index&amp;lt;PreLen; index++) // 피연산자1과 피연산자 2를 구분하기 위한 index 찾기
{
if(isoperator(Pre[index]))
NumOperator++;
else
NumOperand++;
if(NumOperator +1 == NumOperand)
break;
}
strncat(operand1, Pre+1, index); // 피연산자1과 피연산자2를 각각 변수에 대입
strcat(operand2, Pre+index+1);
PreToPostFix(operand1, operand1); // 재귀적으로 각 연산자의 postfix형을 구한다.
PreToPostFix(operand2, operand2);
strcpy(Post, strcat(strcat(operand1, operand2), Operator)); //결과값을 구한다.
return Post; // 결과값 return
}
int isoperator(char ch) // 피연산자와 연산자를 구별하기 위한 함수
{
return (ch == '*' || ch == '/' || ch == '+' || ch == '-' || ch == '%');
}
</code></pre>
<p>이 소스 코드에 대한 Space complexity와 Time complexity를 구해본다. 이 소스에서 함수가 한번 실행되는데 필요한 메모리 공간은 다음과 같다.</p>
<table>
<tbody>
<tr>
<th>변수</th>
<th>바이트</th>
</tr>
<tr>
<td>char *Pre</td>
<td>4</td>
</tr>
<tr>
<td>char *Post</td>
<td>4</td>
</tr>
<tr>
<td>int index</td>
<td>4</td>
</tr>
<tr>
<td>int NumberOperator</td>
<td>4</td>
</tr>
<tr>
<td>int NumberOpreand</td>
<td>4</td>
</tr>
<tr>
<td>int PreLen</td>
<td>4</td>
</tr>
<tr>
<td>char Operator[2]</td>
<td>2</td>
</tr>
<tr>
<td>char operand1[80]</td>
<td>80</td>
</tr>
<tr>
<td>char operand2[80]</td>
<td>80</td>
</tr>
<tr>
<td>return adresss</td>
<td>4</td>
</tr>
<tr>
<td>총 바이트 수</td>
<td>190</td>
</tr>
</tbody>
</table>
<p>길이가 n인 prefix 로 표현된 식을 입력받았을 때 recursive로 호출되는 함수의 개수는 n-1 이므로, 공간복잡도는 190*(n-1) 이다.</p>
<p>시간복잡도는 가장 최악의 경우 <strong>**</strong><em>abcdefgef 와 같이 operand1이 최대로 길때이다. 이유는, 함수내 operand1과 operand2를 구별할 때 for문이 가장 오래 돌기 때문이다. 그 외의 stirng.h의 함수의 경우, 길이가 n정도 되는 문자열을 쭉 읽으면서 처리하는 것이기 때문에 O(n)이다. 따라서 함수를 쭉 한번 읽는 것의 최대차항 계수는 ‘Cn + …’ (C는 상수) 로 나타내 수 있다. 그런데 함수를 재귀적으로 n-1 번 호출하기 때문에, 단계의 수는 대략 (Cn + …)</em>(n-1) 가 됨을 알 수 있다. 따라서 이 함수의 시간 복잡도는 O(n2)가 된다.</p>
<p>그 외에 스택을 이용하여 구현할 수 있는 방법은 어렵지 않게 생각 할 수 있다.</p>
[ML Programming] ML 개발 환경 세팅하기 - Emacs와 ML컴파일러 연동시키기2008-10-08T16:42:56+09:00http://http://eincs.com/2008/10/setting-dev-environment-ml-programming<blockquote>
<p>Making ml developing environment with emacs<br />
ML언어로 프로그래밍 하기 위한 개발 환경 구축한다.<br />
편집기는 emacs를 사용하고, emacs에 sml-mode를 설치하여 쉽게 컴파일할 수 있는 환경을 만든다.</p>
</blockquote>
<h2>ML 컴파일러 설치</h2>
<p><a href="http://www.smlnj.org/install/index.html">http://www.smlnj.org/install/index.html</a> 에서 smlnj.exe 다운 가능.</p>
<blockquote>
<p>The installation updates the system PATH environment variable to point to the location of the SML/NJ executables, and adds a CM_PATH environment variable to point to the location of the libraries. Because of these updates, it is necessary to reboot the machine after the installation to ensure proper behavior of the compiler (Windows NT/2000 users can logoff instead of rebooting).</p>
</blockquote>
<p>이 부분의 환경변수들은 자동으로 등록되는 것 같다. 혹시나 등록되어 있지 않다면 SMLNJ_HOME 이라는 환경변수를 등록시켜 주시기 바란다. 환경변수가 무엇인지, 어떻게 등록하는지 따로 자세히 설명하지 않겠다. 윈도우즈XP를 사용하는 경우 아래 나온대로 환경변수를 등록시킬 수는 있다.</p>
<blockquote>
<p>After installing an SML/NJ compiler, you can start top-level interpreter by selecting the short cut “Standard ML of New Jersey 110.0.7” in the program group of the same name, or by running “sml.exe” in [SMLNJ_HOME]/bin on a command line.</p>
</blockquote>
<ul>
<li>smlnj.exe실행</li>
<li>환경변수의 PATH에 [SMLNJ_HOME]/bin 추가 (내컴퓨터 오른쪽 클릭->속성->고급탭->환경변수->PATH편집->마지막에 <code>[SMLNJ_HOME]/bin;</code> 을 추가해준다.</li>
<li>이제 cmd창에서 sml을 치면 ml언어 컴파일이 가능.</li>
</ul>
<p>일반 C/C++/Java를 사용했던 분들이라면 이 부분에서 혼동을 느끼실 것이다. 코드를 모두 써놓고 컴파일하면 실행파일이나 <code>.class</code>파일이 나오는것이 정상인데, ml은 그렇지 않다. 인터프리터언어이기 때문인데, 예시에서와 같이 한줄한줄 써서 그에대한 결과값을 화면에 보여준다. 익숙치 않겠지만, 배우다보면 차차 그 의미를 깨닫게 될 것이다.</p>
<ul>
<li>실행에서 cmd를 쳐 콘솔창을 연다.</li>
<li>sml이라고 치면 ml 인터프리터가 실행된다.</li>
<li><code>val abc = 11;</code> 이라고 친 후 엔터를 누르면,</li>
<li><code>val it = 11 : int</code> 라고 뜨면 설치가 제대로 된 것임.</li>
</ul>
<h2>emacs설치</h2>
<p><a href="http://www2.lib.uchicago.edu/~keith//tcl-course/emacs-tutorial.html">http://www2.lib.uchicago.edu/~keith//tcl-course/emacs-tutorial.html</a></p>
<ul>
<li>ntemacs22-bin-20070819.exe를 실행하여 emacs를 설치.</li>
<li>설치폴더의 bin안에 emacs.exe를 실행하여 emacs를 실행.</li>
<li>실행하면 창이 뜬다. 키보드를 누르면 글자가 입력된다.</li>
<li>새 파일 처럼 생긴 툴바의 아이콘을 누르면 find file 대화창이 뜬다. (새로운 파일의 저장경로를 적는것이다. 일반 에디터와 사용방법이 다르다. 대화창 제목인 find file과 기능과의 매치가 잘 안되는 것이 사실이다.)</li>
<li>이 창에서 경로 지정한 후 이름을 쓰면, 그 파일이 없는 경우 그 이름으로 새 파일을 만든다.</li>
<li>Ctrl+h 누른후 t를 누르면 튜토리얼로 emacs 사용법을 알아볼 수 있다. (emacs를 처음 사용한다면 한번 쯤은 보는 것이 좋을 듯. emacs를 처음 시작하고 나서 나오는 아래 링크 중 Emacs Tutorial를 눌러도 됩니다.)</li>
</ul>
<h2>emacs에 sml-mode 설치</h2>
<p><a href="http://www.smlnj.org/doc/Emacs/sml-mode.html">http://www.smlnj.org/doc/Emacs/sml-mode.html</a></p>
<blockquote>
<p>With luck your system administrator will have installed SML mode somewhere convenient,
so it will just magically all work?you can skip the rest of this getting started section.
Otherwise you will need to tell Emacs where to find all the SML mode .el files, and when to use them.
The where is addressed by locating the Lisp code on your Emacs Lisp load path?
you may have to create a directory for this, say /home/mjm/elisp, and then insert the following lines in your /home/mjm/.emacs file:</p>
<pre><code> (add-to-list ‘load-path “/home/mjm/elisp”)
(autoload ‘sml-mode “sml-mode” “Major mode for editing SML.” t)
(autoload ‘run-sml “sml-proc” “Run an inferior SML process.” t)
</code></pre>
<p>The first line adjusts Emacs’ internal search path so it can locate the Lisp source you have copied to that directory;
the second and third lines tell Emacs to load the code automatically when it is needed. You can then switch any Emacs buffer into SML mode by entering the command</p>
<pre><code> M-x sml-mode
</code></pre>
<p>It is usually more convenient to have Emacs automatically place the buffer in SML mode whenever you visit a file containing ML programs.
The simplest way of achieving this is to put something like</p>
<pre><code> (add-to-list ‘auto-mode-alist ‘(“\\.\\(sml\\|sig\\)\\’” . sml-mode))
</code></pre>
<p>also in your .emacs file. Subsequently (after a restart), any files with these extensions will be placed in SML mode buffers when you visit them.</p>
<p>You may want to pre-compile the sml-*.el files (M-x byte-compile-file) for greater speed?byte compiled code loads and runs somewhat faster.</p>
</blockquote>
<p>환경변수에 HOME이라는 변수를 새로 만든후, <code>.emacs</code> 파일을 저장할 위치의 경로를 써준다. 파일 이름은 없고 확장자만 emacs라고 생각하면 편하다.
기본으로 제공하는 메모장에서는 이런 형식으로 저장을 할 수가 없다. 따라서 아래서 소개한 에디터를 이용하기 바란다.
아무위치에나 만들어도 상관은없지만, emac설치폴더에 만드는 것이 편리하다. <code>.emacs</code> 파일에 들어갈 내용은 다음과 같다.</p>
<pre><code>;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; sml-mode
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(add-to-list 'load-path "C:/ntemacs22/sml-mode") <br />
;; the directory where the sml-mode is installed<br />
(autoload 'sml-mode "sml-mode" "Major mode for editing SML." t)
(autoload 'run-sml "sml-proc" "Run an inferior SML process." t)
(add-to-list ‘auto-mode-alist ‘(“\\.\\(sml\\|sig\\)\\’” . sml-mode))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; syntax coloring
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; M-x font-lock-mode ;; turn it on in the current buffer
(add-hook 'sml-mode-hook 'turn-on-font-lock) ;;turn it on in all sml-mode buffers
;; (global-font-lock-mode 1) ;; turn it on everywhere
</code></pre>
<p>이름은 없고 확장자만 emacs인 파일 이다. 메모장에선 이런 형태로 저장이 불가능하므로 다른 에디터를 이용해야 할 듯. 에디트플러스나 울트라에디터를 사용하면 편하다. 혹은 emacs를 이용하면 된다.</p>
<p><code>(add-to-list 'load-path "C:/ntemacs22/sml-mode")</code>에 sml-mode의 압축이 풀린 폴더의 경로만 써주면 된다. 주의할 점은 경로를 쓸때 \ 가 아니라 / 로 구분자 표시를 해줘야한다는 것이다. (이것때문에 고생했다)</p>
<p>이제 emacs에서 ml 프로그래밍을 할 수 있다. emacs에 간단한 ml 코드를 짜보고 컴파일 해보는 것으로 확인 할 수 있다. 이제 이맥스를 켜고 새로운 test.sml 이라는 새로운 파일을 만들고 다음 코드를 짜보자.
보면 알겠지만, 인수로 a와 b중 큰 수를 return하는 함수이다. 이렇게 설명하면 엄밀히 말해서 틀린설명인데, 자세한 것은 ML문법을 참고하기 바란다.</p>
<pre><code>fun max2 a b =
if a&gt;b then a else b;
</code></pre>
<p>syntax coloring[이 제대로 된다면 설치된 것이다. (syntax coloring은 문법에 맞게 highlight 되는 것을 말한다. 예약어따위에 일반 문자와 구별되는 색이 입혀지는 것이 보통이다)
syntax cloloring이 제대로 되어 있지 않다면 <code>.emac</code>파일을 확인하여 철자가 틀리지 않았는지 본다. 이제 컴파일을 해보자.</p>
<p>Alt+x를 누르고 run-sml이라고 친 후 엔터를 누르자. M+x와 동일. (여기서 M은 영문자 M이 아니라 Emacs에서 사용하는 특수키이다. 윈도우 시스템에선 Alt에 대응된다. 실제 Emacs 튜토리얼 문서 같은 것에 M+x라고 적혀 있다)
그러면 sml이라고 적혀진 문자열이 출력되는데 다시 엔터를 누르면 컴파일러가 실행된다.</p>
<p>여기에 <code>use "test.sml";</code> 이라고 치면 컴파일이 된다. 에러가 뜬다면 소스를 다시 확인하기 바란다. 컴파일이 되면 <code>max2 3 4;</code> 를 친 후 엔터를 누르면 함수가 실행이 된다.
<code>val it = 4:int</code> 라고 뜬다면 컴파일이 제대로 된 것이다.</p>
<p>다른 방법이 또 있는데, 상단 메뉴 중 tool에서 compile을 누르면 하단에 <code>make -k</code> 와 같은 문자열이 뜰 것이다. 모두 지우고 <code>sml test.sml</code>이라고 치면 컴파일이 된다.
이 때 주의할 점은, 컴파일이 끝나고 컴파일러가 그냥 종료된다는 것이다. 따라서, 드라이버 코드를 소스코드에 포함시켜줘야 할 것이다. 드라이버코드란 테스트코드를 말한다. 위와 같은 경우엔 max2 5 3; 이것이 드라이버 코드가 될 것이다.</p>
<h2>결어</h2>
<p>ml 프로그래밍 언어는 functional programming language 중 하나이다. 기존에 사용되는 c, c++, java와 같은 언어에는 없는 여러가지 프로그래밍 언어에는 없는 몇 가지 개념들이 많이 사용된다.
Emacs로 편리한 ml 프로그래밍 개발 환경을 만들어 쉽게 컴파일하자!</p>