Q. 그런데 (그 느리다는) 리플렉션을 왜 그렇게 많이 쓰나?
A. 리플렉션 오버헤드는 DB 트랜잭션이 발생시키는 IO 타임에 비하면 새발의 피다.
또한 최신 JVM의 리플렉션 수행속도는 (당신의 생각보다) 엄청나게 빠르며,
원한다면 cglib을 통해 리플렉션 회피 초식을 펼칠 수 있으나 생각보다 성능향상폭은 미미하다.

Q. 좋다. 그럼 리플렉션의 장점은 무엇인가?
A. 컴파일-빌드-테스트 사이클을 빠르게 하기 위해서다. 95%의 작업은 Hibernate로 빠르게 작업하고 이로 인해 얻은 시간을 성능을 요구하는 5%의 작업에 투자하라.
좌우당간, 대부분의 Hibernate로 작성한 코드는 hand-coded JDBC 코드에 비해 성능이 거의 떨어지지 않는다.


1) 리플렉션 성능의 오해

리플렉션은 분명히 느립니다. 제가 손수 테스트한 기억에 의하면 JDK1.3 버전에서는 일반 메소드 호출에 비해 100배~1000배 가량 느렸습니다. 다시 조사해 보니 1000배 이상이군요. 이러한 이유로 특히 대규모 트랜잭션이 요구되는 시스템을 설계하시는 분들 중에 리플렉션에 대한 두려움을 가지신 분들이 많습니다.

그런데 현실은 조금 다릅니다. 실제 대규모 트랜잭션 시스템에서는 하나의 요청당 처리되는 시간은 1 나노초nano second를 기준으로 하면 7 ~ 9 정도의 magnitude, 즉 10ms(10^7 ns) ~ 1000ms(10^9 ns) 수준이 소요되는 것이 보통입니다. 반면에 한 번의 리플렉션 호출[fn]한 번의 완전한 리플렉션 호출은 1. getMehod, 2. 파라미터 박싱, 3. invoke 세 단계로 이루어지며 1번에서 가장 많은 시간이 소요됩니다. 일반적으로는 1 한번에 2와 3은 여러번 이루어지며, 특히 getMothod는 대부분 따로 캐싱하는 기법이 많이 사용되므로 magnitude 4 정도라 보는 것이 더 타당할 것입니다.[/fn] 은 JDK 1.3 기준으로 4 ~ 5 magnitude[fn]본문의 수치는 Component Development for the Java Platform, Halloway저 에서 발췌하였습니다. Java 및 J2EE에서 기술적인 의미의 컴포넌트 개념으로는 독보적인 책인데 별로 유명하진 않더군요. :-) 개인적으로는 매우 추천입니다.[/fn] 정도가 됩니다

하나의 트랜잭션 당 많이 잡아서 100번의 리플렉션 호출이 있다고 가정합시다. 이 경우 6 ~ 7 정도의 리플렉션 오버헤드가 발생할 것입니다. 시간으로 환산하면 1ms ~ 10ms 정도입니다. 이를 요청처리시간에 대비해 보면 최악의 경우 100%의 오버헤드, 일반적인 케이스라면 많이 잡아야 원래 성능의 1/10 ~ 1/100 정도의 오버헤드일 뿐 입니다. 아마 전체 평균을 따진다면 1/50을 넘기 힘들 겁니다. 게다가 JDK1.4, 1.5에서는 리플렉션의 성능이 수배 이상 빨라졌다고 하니, 오버헤드가 1/50을 넘을 가능성은 더욱 희박합니다.

이러한 데이터에 근거해 본다면, 제게는 별로 리플렉션을 피할 이유가 없을 것 같습니다. 이러한 오버헤드 대신 개발기간을 5%만 줄일 수 있다면 저는 과감히 사용하겠습니다. 이렇게 얻은 시간을 활용해서 시간을 투자해서 구조의 개선을 하는 것이 더 확실한 투자일 것 같네요.

그나저나 성능 관련해서 한 마디만 더...
왕복 2차선 도로 뚫어 놓고 제 속도 안 난다고 자동차 튜닝하려 하지 마세요. 차라리 길을 좀 넓힐 궁리를 하시죠.


2) 리플렉션의 장점

리플렉션은 런타임 정보에 의존하는 특성이 있습니다. 그럼 리플렉션을 활용하지 않는다면? 당연히 무엇인가 리플렉션을 대체할 자바코드가 static 하게 존재해야만 합니다. 이러한 자바코드를 손수 작성하든, 소스 생성기술을 활용하든 해야 한다는 얘기가 됩니다.
(물론 이러한 제약을 뛰어넘을 수 있는 cglib[fn]cglib은 바이트코드를 읽고/쓰고/조작할 수 있는 궁극의 라이브러리입니다. 문제는 너무 어렵다는 겁니다. 예제코드 한 30분 연구해보다가 눈돌아가서 gg치고 안보기로 했습니다. OTL[/fn] 같은 바이트코드 생성기술도 있습니다만 리플렉션보다 훨씬 어렵고 복잡합니다. 프로젝트 중간에 cglib 필요성이 생겨서 직접 써보자! 하고 쉽게 결정할 만한 기술은 아닌 것 같습니다.)

어쨌든 Hibernate에서는 transaparent POJO를 구현하기 위해 내부적으로 프록시를 만들어 내야 합니다. 만약 리플렉션을 사용하지 않는다면 이러한 프록시의 소스코드가 있어야 함은 자명한 일인데, EJB의 예에서 보듯 이는 런타임에는 아주 약간은 도움이 될 수도 있겠으나 대신 개발생산성을 저하시키는 부작용이 생깁니다. 이는 긴 시간은 아니지만 DeMarco가 이야기 했던 프로그래머의 flow 몰입에 반대급부를 미치기에는 충분한 시간입니다.

Hibernate가 택한 것은 그래서 소스 생성 기술이 아니라 리플렉션이었습니다. 성능을 약간 희생(?)하더라도 생산성을 높이기로 한 것이죠.

대신 Hibernate가 또 다른 학습커브를 만든다는 것이 문제라면 문제지요.


3) 이번 글의 결론

결론에는 반전이 있습니다.
지금까지 리플렉션에 대해 이러쿵 저러쿵 이야기 했지만, 실제로 Hibernate를 사용해 보시면 리플렉션을 사용한다기 보다는 거의 cglib을 사용하게 되기 때문에, Hibernate 관점에서는 크게 의미 있는 논의는 아니었습니다.
죄송합니다. :-)
(왜 Q&A 페이지에 아직도 리플렉션을 떡하니 둔 것이냐 킹!)

변명을 하자면 Hibernate 사용자 뿐만 아니라 일반 자바 유저 또는 안티(?)들에게 있어 리플렉션에 대한 FUD가 아직 높은 편인데, 이를 불식시키는데 일조했으면 하는 마음으로 쓴 글이니 너그러이 용서를...

그리고 마지막.
Hibernate를 사용할 때는 Gavin King의 말을 기억하시기 바랍니다.
"80~95%의 코드에서는 Hibernate를 사용해서 생산성을 높이고, 나머지 코드에서는 과감히 Hibernate 대신 다른 방법을 이용해서 성능을 높일 각오를 하시기 바랍니다."
(사실 이것도 제 멋대로 번역입니다. ^^; )

  1. Commented by Favicon of http://younghoe.info/ BlogIcon 영회 at 2006.12.15 00:47

    주석은 footnote 플러그인 설치해서 쓰세요. ^^

    • Commented by Favicon of http://crosscutter.info BlogIcon 지훈 at 2006.12.15 09:05

      HTML로 어떻게 해보려고 끙뜽대다 귀찮아서 그냥 올렸는데, 주석 플러긴이 있었군요. 감사.

  2. Commented by Favicon of http://younghoe.info/ BlogIcon 영회 at 2006.12.15 00:50

    내용은 예나 지금이나 좋은데
    글이 점차 길어지네요.. 볼 게 많아져서 고무적입니다. ^^

  3. Commented by Favicon of http://cbiscuit.info BlogIcon cbiscuit at 2006.12.15 09:46

    00 시 01 분...
    이거 혹시.. 업무시간에 쓰고..
    예약 포스팅 기능 사용한 거 아냐?

이전 Hibernate 성능 Q&A 요약번역이 반응이 꽤 좋았던 터라 이에 탄력받고 ^^;
이번에는 하나하나 Q&A를 짚어가며 고찰하는 형태로 글을 올려볼까 합니다.
물론 주된 테마는 '성능'입니다.

CAUTION!
제가 무슨 대단한 Hibernate 전문가도 아니고 그렇다고 많이 써 본 것도 아니므로 이 점 염두에 두시기 바랍니다. ^^
또한 제가 여기에 적는 것이 정답도 아니며 틀린 내용도 있을 수 있으므로, 그런 내용은 가차없이 지적해 주시면 감사하겠습니다.


첫번째 질문과 답은 다음과 같았습니다.

Q1. Hibernate의 성능은 어떤가?
A. 좋다! 고 우리는 주장한다. 정확하게 얘기해서 JDBC 드라이버 및 DB 환경이 제약조건1(이는 raw JDBC로 코딩해도 마찬가지다)이며, 질문은 다음과 같아야 한다.
이러한 제약조건하에서 쿼리 횟수를 얼마나 압축2하고, JDBC 위에서 성능3확장성(scalability)4를 향상시킬 수 있는가?

이 중에서 밑줄 친 키워드를 가지고 좀 더 깊이 들어가 보겠습니다.

1. JDBC 드라이버 및 DB 환경이 제약조건
우선적으로 염두에 두셔야 할 것은 Hibernate는 기본적으로 JDBC + SQL 조합을 사용하는 애플리케이션에 적용하는도구라는 점입니다. 이러한 조합은 도메인 자체가 복잡해서 저장 프로시저stored procedure 등으로 DB 수준에서 문제를풀어나가기에는 어려운 애플리케이션에 적합합니다. 저장 프로시저 vs Hibernate도 또한 재미있는 주제입니다만, 여기서비교는 하지 않겠습니다.

2. 쿼리 횟수를 얼마나 압축하느냐
이러한 애플리케이션은 DB와 서로 프로세스 수준 혹은 네트워크 수준에서 분리되어 있습니다. 따라서 같은 일을 하더라도 얼마나적은 횟수로 SQL을 던지는가가 중요한 문제가 됩니다. 그런데 애플리케이션을 문제영역을 구분하고 구조화할 수록 하나의 SQL로커버하는 영역이 작아집니다. 왜냐하면 각각의 구조화된 영역 내라는 제약이 있기 때문입니다. 많은 유스케이스는 SQL 한 두개로처리할 수 없게 되지요. 물론 조회화면의 경우 구조화는 무시하고 SQL 하나로 처리하는 것이 효율적인 경우가 많습니다만, 이때는 관리 vs 효율을 잘 저울질 해야 할 겁니다.
그런데 Hibernate는 구조화된 애플리케이션을 위한 도구입니다.그러면서 이러한 애플리케이션의 약점인 쿼리 횟수를 줄이기 위해 말 그대로 사력을 다합니다. 1차/2차 캐시, dirtychecking을 통한 UPDATE 등은 이러한 노력을 그대로 보여줍니다.

3. 성능
SQL에 익숙한 프로그래머가 손으로 짠 SQL과 Hibernate가 만들어 낸 SQL을 일대일로 비교하면 당연히 Hibernate가밀립니다. 너무나 당연한 이야기입니다만... 그런데 안타깝게도 손으로 짜는 SQL과 Hibernate가 만들어 내는 SQL은정확하게 1:1로 매핑이 안됩니다. 게다가 Hibernate는 SQL 프로그래머가 생각지도 또는 생각했더라도 실제로는 하기 힘든아래와 같은 일들을 합니다.
  • 1차/2차 캐시(여러번 등장합니다. 그만큼 중요한 개념이죠.)에 걸리면 불필요한 SQL은 DB에 전달조차 되지 않습니다.
  • Optimistic locking을 이용해서 DB lock 유지를 최소화 합니다.
  • UPDATE는 최대한 늦춰서 쿼리를 모아서 보냅니다.
Hibernate의 강점은 이러한 등등의 기법을 꽤나 투명하게 녹여냈고 비교적 적용하기가 쉽습니다.
문제는 공부를 해야 한다는 겁니다만....

어쨌든 성능 부분을 요약하면,
SQL 중심의 시스템은 빠른 하나를 만들어 내기는 쉽지만 전체적으로 빠르게 하는 것은 어렵고,
Hibernate 중심의 시스템은 빠른 하나의 쿼리는 못 만들지만 전체적으로 보면 빠르다고 할 수 있습니다.

4. 확장성(scalability)
scalability를 확장성으로 번역하는 것에 대해서 다소 개인적으로 선호하지는 않지만, 대중적인 용어이므로 그대로 사용합니다(전 번역 딴지맨...)
확장성은 얼마나 자원경쟁resource contention을 줄이느냐가 관건입니다. 동시다발적으로 다양한 서버 트랜잭션이 발생하는 상황에서 하나의 자원을 놓고 여러 트랜잭션이 경쟁하게 된다면, CPU가 수백 수십개 있어 봐야 도움이 안됩니다. 한 시점에 하나(또는 소수의) 트랜잭션만 전부 병목자원에서 블록킹이 걸려버리기 때문이죠.
병목자원의 1순위는 DB 그 자체입니다. 따라서 DB 액세스를 줄이는 방법을 강구해야 되는데, Hibernate에서는 위에서 사용한 여러가지 방법을 통해 DB 액세스를 줄이고 있습니다.
한 레벨 더 내려가면, 주요 병목자원 중 대표적인 것은 DB 데이터 그 자체입니다.
데이터의 정합성을 위해서는 데이터에 대한 locking이 필수적인데, Hibernate는 EJB의 기본 체제인 1 record - 1 instance 체제를 버렸습니다. 이는 경쟁contention을 유발하여 locking 시간을 길게 가지게 되는 pessimistic locking 대신 optimistic locking을 강력하게 지지하고 또 지원한다는 의미입니다.
  1. Commented by Favicon of http://cbiscuit.info BlogIcon cbiscuit at 2006.12.08 10:50

    커~ 내용 너무 좋다..

  2. Commented by Favicon of http://toby.epril.com BlogIcon 토비 at 2006.12.13 23:57

    좋은 내용입니다.
    Hibernate은 EntityBean과 같은 이상적인 오브젝트중심의 사고를 버리고 RDB의 아이디어에 가까운 타협을 통해서 OO의 장점과 RDB의 장점을 다 수용할 수 있는 실리를 택했다고 봅니다.
    그래서 Hibernate를 사용하는 애플리케이션 개발자들은 자신이 만들고 있는 코드에 의해서 어떤 SQL이 실행되어질 것인가를 머리 속에 같이 그릴 수 있는 능력이 필요하다고 항상 주장해왔습니다(만 사실 다들 관심이 없더군요. -_-)

    • Commented by Favicon of http://crosscutter.info BlogIcon 지훈 at 2006.12.15 00:12

      토비님 반갑습니다.
      TSE 참관기는 즐겁게 보았습니다.
      질투나서 더 이상 스프링 하기 싫어지네요 -_-

  3. Commented by Favicon of http://www.timberlandbaratas.com BlogIcon timberland at 2012.12.25 15:22

    Des combats entre les rebelles communistes et, http://www.timberlandbaratas.com Timberland?forces gouvernementales aux Philippines ont fait quatre morts parmi les rebelles et deux parmi les forces de l'ordre depuis jeudi, http://www.timberlandbaratas.com Timberland shops, à quelques jours de la reprise , http://www.timberlandbaratas.com Mujer Timberland?prévue à Oslo de négociations sur un cessez-le-feu, http://www.timberlandbaratas.com Hombre Timberland, ont indiqué samedi des responsables, http://www.timberlandbaratas.com botas timberland.Related articles:


    http://art.nstory.org/345 For cheap flights to Perth from UK

    http://blog.taesuz.com/465 Le président du Nouveau Centre (NC) Hervé Morin "sera candidat" à la présidentielle de 2012

원문은 Hibernate Performace Q&A 입니다.

* 원문 그대로 번역한 것은 아니고 꽤 많은 부분이 제 마음대로 요약/수정되었음을 염두에 두시기 바랍니다.

Q. Hibernate의 성능은 어떤가?
A. 좋다! 고 우리는 주장한다. 정확하게 얘기해서 JDBC 드라이버 및 DB 환경이 제약조건(이는 raw JDBC로 코딩해도 마찬가지다)이며, 질문은 다음과 같아야 한다.
이러한 제약조건하에서 쿼리 횟수를 얼마나 압축하고, JDBC 위에서 성능과 확장성(scalability)를 향상시킬 수 있는가?

Q. 그런데 (그 느리다는) 리플렉션을 왜 그렇게 많이 쓰나?
A. 리플렉션 오버헤드는 DB 트랜잭션이 발생시키는 IO 타임에 비하면 새발의 피다.
또한 최신 JVM의 리플렉션 수행속도는 (당신의 생각보다) 엄청나게 빠르며,
원한다면 cglib을 통해 리플렉션 회피 초식을 펼칠 수 있으나 생각보다 성능향상폭은 미미하다.

Q. 좋다. 그럼 리플렉션의 장점은 무엇인가?
A. 컴파일-빌드-테스트 사이클을 빠르게 하기 위해서다. 95%의 작업은 Hibernate로 빠르게 작업하고 이로 인해 얻은 시간을 성능을 요구하는 5%의 작업에 투자하라.
좌우당간, 대부분의 Hibernate로 작성한 코드는 hand-coded JDBC 코드에 비해 성능이 거의 떨어지지 않는다.

Q. 그럼, 확장성은 어떤가?
A. 확장성의 최대의 적은 resource-contention 이다. Hibernate는 resource-contention(즉 객체 synchronization에 대한 필요성)이 없다.
(DB에 대한 contention은 Hibernate나 hand-coded JDBC나 같으니 논외로 한다.)
보다 중요한 이슈는 얼마나 메모리를 효율적으로 사용하느냐인데,
Hibernate는 객체공유object sharing를 하지 않지만 생각보다 쓸데없이 객체를 만들지 않는다.
(반대로 객체공유를 하더라도 당신은 생각보다 객체를 많이 만들게 된다.)

Q. 어쨌든 객체풀링instance pooling은 왜 안하는가?
A. 쓸데 없기 때문이다. 게다가 객체풀링은 버그의 가능성이 훨씬 크다.

Q. 좋다. Hibernate는 쿼리 횟수를 최소화하도록 되어 있는가?
A. 좋은 질문! Hibernate는 쿼리를 항상 최소화 하며, 다음과 같이 최적화 한다.
  • 객체 캐싱. transaction-scope의 session 캐시가 디폴트로 제공된다. 또한 JVM 수준의 캐시도 활용할 수 있다.
  • SQL 실행 지연. INSERT와 UPDATE를 정말 필요로 하는 순간까지 늦춰서 SQL을 실행한다. 이는 DB lock을 가지고 있는 시간을 최소화 한다.
  • 변경이 없는 객체는 update시 무시.
  • 효율적인 collection 처리. 실제 변경된 collection 원소만 INSERT/UPDATE 한다.
  • 여러 update를 하나로 처리. 같은 객체에 대한 변경사항은 하나의 UPATE로 합쳐서 끝낸다.
  • 변경된 부분만 갱신. Hibernate는 객체의 변경내용만 UPDATE에 싣는다.
  • Outer join fetching. 매우 효율적인 outer-join fetching을 구현해 놓았다.
  • collection 초기화 지연.
  • 객체 초기화 지연.
이외에도 여러가지 옵션이 더 있다.

Q. 그럼 왜 "공식적인" Hibernate 벤치마크 결과를 제공하지 않는가?
A. 두 가지 이유다.
신뢰성. 벤더 제공자가 내놓는 결과를 당신은 액면 그대로 믿는가?
적용성. 하나의 벤치마크가 "당신의 애플리케이션"에 그대로 적용될 수 있을까?
좌우당간 리얼 월드에서의 리얼 애플리케이션에서의 리얼 사용자 패턴에 의한 성능이라면 자신있다.

Q. 결론을 한 문장으로 줄여봐라.
A. Hibernate는 적절하게(역자에 의한 강조) 사용되기만 한다면 매우 빠르며,특히 대규모 데이터를 기반으로 하는 대용량 멀티유저 애플리케이션에서 그러하다.


  1. Commented by Favicon of http://cbiscuit.info BlogIcon cbiscuit at 2006.12.05 20:19

    오오.. 일은 안하고 이거 했었단 마린가..

  2. Commented by Favicon of http://seal.tistory.com BlogIcon 물개선생 at 2006.12.06 08:28

    마음대로 요약/수정되는 번역 디게 좋아합니다. ^^* 결론이 다시 봐도 감동이군요. 이제 제발 지겨운 ORM 성능 논쟁은 없었으면.. 차라리 공부하기 힘들어서 ORM을 못쓰겠다고 하면 이해가 가겠는데요. 몇년 동안 수 많은 사람들이 그렇지 않다고 얘기를 하는데도, ORM 성능에 대한 사람들의 오해는 변함이 없군요.

    • Commented by Favicon of http://crosscutter.info BlogIcon 지훈 at 2006.12.06 09:12

      이전 프로젝트에서 벤치마크를 했는데 유닉스 박스 4대(8대?)에서 4000 TPS 찍었습니다.
      실제 요구사항은 MAX 2000 TPS였습니다.
      ORM의 진정한 어려움은 ORM이 적절한/비적절한 트랜잭션을 식별하는 방법에 있지 성능은 아니라고 봅니다.

  3. Commented by Favicon of http://crosscutter.info BlogIcon 지훈 at 2006.12.06 09:32

    특히 대용량 멀티유저 컨커런트 OLTP 시스템일수록 더욱 그 역량을 발휘할 수 있다고 봅니다.
    예를 들어 모 은행 시스템에서 어떤 트랜잭션은 한 번에 고객 테이블을 14번 액세스 한다고 합니다.
    여러 모듈이 결합되면서 각각 자신이 필요한 데이터를 스스로 가져오는 것이 원인인데,
    Hibernate의 1차캐시는 이러한 불필요한 DB 액세스를 단 한번으로 줄여주죠.
    게다가 상당히 많은 join 쿼리는 코드값 등의 딕셔너리 데이터를 가져오는 데 사용하게 되는데,
    이러한 join은 2차캐시로 아예 없애버릴 수 있으므로, 활용만 잘 한다면 훨씬 효율적인 쿼리가 가능하다고 봅니다.

현재 hibernate 3.2와 hibernate-annotations 3.2.0.GA 버전을 사용중이다.
ProgramCategory라는 열거형enum을 정의하고 이를 VARCHAR 컬럼으로 매핑해야 하므로,
커스텀타입custom type으로 매핑하였다.
그런데 HQL에서는 작동하지 않는다(그 이외의 경우에는 작동).

아래는 커스텀타입custom type의 정의이다.
@TypeDefs(
   {
   @TypeDef(
       name="ProgramCategory_CUSTOM",
       typeClass = abc.framework.hibernate.StringValuedEnumType.class,
       parameters = {
           @Parameter(
               name="enumClassName",
                value="abc.programmgt.model.ProgramCategory"
           )

       }
   )
   }
)

아래는 위에서 정의한 커스텀타입을 실제 매핑하는 코드이다.
@Type(type="ProgramCategory_CUSTOM")
@Column(name="CATEGORY")
public ProgramCategory getCategory() { return category; }

아래는 HQL의 where 절 일부.
...
and p.category = :category
...


다음은 HQL을 실행하는 코드.
ProgramCategory category = ...;
query.setParameter("category", category);

어노테이션으로 정의한 경우에는 위에서 정의한 커스텀타입이 작동하지를 않고,
디버거를 돌려 보니 타입 결정 시에 java.io.Serializable로 결정해 버린다!
:category 요부분이 ProgramCategory가 직렬화된 문자열로 바뀐다 -_-;

꽤나 문제 찾는 데에 시간이 걸린 관계로 일단 아래와 같이 강제로 String으로 변환(DB 타입은 VARCHAR임)해보니 해결은 되었으나,
query.setString(
   "category", category == null? null : category.getValue());
영 찜찜한 것은 어쩔 수가 없다.

버그리포트로 올려야 하나...

원문은 다음 링크 참조.

Java Persistence with Hibernate eBook available


드디어 기나긴 편집모드가 끝났다고 합니다.
사실상 이 책의 1st edition인 Hibernate in Action도 괜찮았지만,
전체 시스템 내에서의 역할과 연계에 대한 이야기가 별로 없고,
자잘한 매핑 옵션에 대한 설명이 부족했다고 봅니다.

또 HIA의 경우 hibernate 2.0을 대상으로 했기 때문에,
EJB 3.0 스펙을 지원하는 hibernate 3.x를 대상으로 한 2nd edition에 해당하는
이 책은 많은 관심이 가네요.

12월 초부터는 아마존에서 배송 시작이 가능할 것이라 합니다.
  1. Commented by Favicon of http://cbiscuit.info BlogIcon cbiscuit at 2006.11.23 19:12

    근 일주일째...
    너무.. 업데이트가 없다..