블로그 이사 했어요 ~

만성피로가 블로그를 이사 했습니다.

이글루스를 사용하다가 티스토리로 이사를 -_-;;;

이사한 김에 자바 기초를 다시 리뷰하면서 글을 작성하고 있습니다.

블로그 주소

http://maydaisy.tistory.com/

감사합니다.

by 만성피로 | 2009/02/10 19:32 | 트랙백 | 덧글(0)

파일 업데이트 사용에 대한 고민.

지난 번 DB 이용 방법에 대해서 약간의 고민을 했던 것과 비슷하게 파일 업데이트에 대한 것을 잠시 고민하고자 짧은 글이지만 핵심적인 이야기를 하고자 합니다.

파일을 업데이트 하는 방식 중

첫번째로 서버쪽 파일과 클라이언트 쪽 파일을 비교해서 최종 수정 시간이 언제인가를 확인 한 뒤에 서버 쪽이 더 최신의 수정 시간을 갖고 있을 경우 업데이트를 하는 방식.

두번째로 파일의 크기를 비교해서 서버쪽과 클라이언트 쪽의 파일 크기가 다르다면 업데이트 하는 방식.

마지막으로 클라이언트의 버전 정보를 서버쪽에 전송 받아서 버전이 구 버전인지 확인을 한 뒤 파일을 업데이트 하는 방식.

이렇게 3가지 방식을 두고 고민을 하고 있습니다.


일반적으로 게임에서 많이 볼 수 있는 파일 업데이트 방식은 두번쨰 방식을 주로 이용하는 것으로 알고 있는데 과연 두번째 방법이 좋은 것인지에 대해서는 확실히 알수는 없다고 생각 되는 부분이 존재합니다.

만약 클라이언트 쪽에서 바이너리 편집기를 이용해서 파일의 내용을 수정한다면 파일의 사이즈는 변화가 없기 때문에 서버 입장에서는 전혀 문제가 없다고 판정을 내린다는 점과 이 문제를 해결하고자 클라이언트의 파일들을 서버가 일일이 내용까지 확인한다는 것은 서버 입장으로는 무리수를 두는 시스템이라고 생각되는 부분입니다.

그리고 첫번째 방법은 단순히 파일의 최신 업데이트 날짜 비교가 아니여도 서버쪽에 파일 수정 시각과 클라이언트 쪽에 파일 수정 시각이 다를 경우에 업데이트를 한다고 약간 바꿔서 생각 한다면 편리할 것이라고 생각 되나.. 파일의 수정 시각은 클라이언트 쪽에서 위조가 가능 하다는 것으로 알고 있는 부분입니다.

마지막으로는 클라이언트 쪽에 버전 정보 역시 첫번째 방법과는 크게 다르지는 않을 것으로 보인다는 점.. 쉽게 가는 프로그래밍 이라면 물론 3번이 가장 속 편한 해결책이라 보입니다.

3 가지 방법 그 어느것도 장단점은 존재 한다고 봅니다.
과연 어떤 것을 이용하는 것이 좋을까요?
(이미 마음은 기울어졌지만...-_-;;)

아 그리고 3가지 방법 모두 다 객체스트림을 이용한 방법으로 구현할 계획입니다. ^^;

by 만성피로 | 2008/11/25 00:37 | 정리할 자료들 | 트랙백 | 덧글(2)

객체를 저장하고 읽어보자.

이번 글에서는 객체를 저장하고 로드하는 것에 대해서 이야기 하도록 하겠습니다.

이 이야기를 하기 전에 우리가 java.io 패키지를 사용 했던 것을 잠시 생각해 봅시다.

가장 처음에서는 문자 스트림을 이용한 예지를 가지고 공부 했었습니다.
주로 키보드 입력 받아들이기, 텍스트 파일 읽기/쓰기 입니다.

그리고 이후에 했던 것은 바이트스트림을 이용했습니다.
텍스트 파일이 아닌 바이너리 파일을 복사 또는 텍스트 파일을 바이트스트림을 이용해서 읽기 및 쓰기도 했었습니다.

마지막으로는 네트워크 프로그래밍 때 이용을 했었습니다.
네트워크 프로그래밍에서는 문자/바이트 스트림을 적절하게 이용하면 모두 이용할 수 있다는 것은 알고 있을 거라 생각됩니다.

그럼 이제 본론으로 돌아와 객체 입출력 이야기를 하도록 하겠습니다.

일단 객체를 가지고 입출력을 하기 이전에 문자스트림도 객체를 이용한 입출력이 아닐까 하는 생각을 할 수 있습니다.
문자열을 이용하려면 String 클래스의 레퍼런스 변수를 이용해야 했기 때문이죠.

그럼 이 부분에 대해서 조금 더 알아보고 넘어가도록 합시다.

객체를 입출력 하기 위해서는 가장 먼저 해당 객체의 클래스가 Serializable 인터페이스를 구현해야 객체 입출력을 할 수 있는 가장 기본 조건이 만들어 집니다.(이 인터페이스는 추상메소드가 없기 때문에 implements를 하는 것 자체가 구현이 되는 약간의 특이한 인터페이스라고 볼수 있습니다.)

그럼 String 클래스는 Serializable 인터페이스를 구현 했을까요?
API를 확인 해 보면 구현 하고 있는 것을 쉽게 확인이 가능합니다.
그리고 실제 String 클래스의 소스 파일에서도 구현을 하고 있다는 것을 쉽게 확인이 가능합니다.

String 클래스의 일부 소스

public final class String

implements java.io.Serializable, Comparable<String>, CharSequence

{

/** The value is used for character storage. */

...


그래서 객체입출력을 모르는 상태에서도 문자스트림을 이용하는데 전혀 문제가 없었습니다. 그렇지만 문자스트림을 객체입출력이라 보는 것은 좀 어렵다고 생각 됩니다. File 클래스 또한 Serializable 인터페이스를 구현 하고 있고 각각의 입출력을 하기 위한 문자/바이트 스트림 구조가 완성 돼 있기 때문입니다.

이 점에 대해서 대충 정리를 하자면 이런 식으로 걸고 넘어진다면 각각의 객체의 특징을 모호하게 만든다고 해야 할까요? ㅎㅎ
자바에서는 모든 클래스들은 Object의 자손들인데 여기서 부터 걸고 넘어져야 겠지요.. ㅎㅎ

그럼 객체 입출력을 위한 준비물 클래스를 소개 하도록 하겠습니다.
1. 위에서도 언급을 했던 Serializable 인터페이스를 구현 하는 클래스가 필요합니다.
2. 입출력을 위해서 ObjectInputStream, ObjectOutputStream 클래스가 필요합니다.
3. 문자/바이트 스트림 떄 처럼 Buffered..들을 이용할 수 있으므로 조건적으로 필요할 수 있는 부분입니다.

위에 준비물을 이용해서 우리가 기초적으로 했었던 입출력 예제와 동일하게 이용하면 됩니다.

클래스 사용 예)
ObjectOutputStream oos = new ObjectOutputStream(
     new BufferedOutputStream(new FileOutputStream("data")));
ObjectInputStream ois = new ObjectInputStream(
     new BufferedInputStream(new FileInputStream("data")));

여기 까지는 확실히 익숙한 소스라는 것을 쉽게 확인이 가능합니다.
그럼 다음으로..

이들을 저장 할 떄는 writeObject(객체명); 읽을 때는 readObject(); 메소드를 사용 하면 손쉽게 이용할 수 있습니다.
그런데 여기서 한가지 문제가 발생 되는 것이 존재합니다. 바로 오브젝트로 쓰고 오브젝트로 읽는 다는 점입니다.

객체를 저장 할 떄는 오브젝트 클래스로 저장 하지만 실제 파일은 바이너리 파일이므로 문제가 없습니다.
(위에서 확인을 해보면 바이트스트림을 이용한 것을 알 수 있습니다.)

그러나 파일을 읽을 때는 오브젝트 클래스로 읽어 들이는데 '오브젝트 클래스로 읽은 데이터를 어떻게 처리 할 것인가?'에서 문제를 발생 시키게 되는 것입니다.

SaveData 클래스의 객체를 저장 했다면 다시 SaveData 객체에 넣어줘야 하는 것이지요. 어떻게 해결 해야 할까요?
답은 간단합니다. 오브젝트 클래스로 읽어들였으면 SaveData 클래스로 캐스팅을 해주면 됩니다.

이점에 대해서는 Vector 클래스를 이용 할 때와 비슷하다고 생각하면 이해하는데 도움이 될 것입니다.

그럼 전체 소스를 보도록 합시다.

import java.io.*;
// 입출력에 사용 될 클래스
class SaveData implements Serializable {

// 맴버변수
     private String name;
     private String url;
     private int i;
     private int j;


// 기본 생성자
 public SaveData() {
  this.name = "만성피로";
  this.url = "http://maydaisy.egloos.com/";
  this.i = 100;
  this.j = 1000;
 }

// 오버로딩을 이용한 또 다른 생성자

 public SaveData(String name, String url, int i, int j) {
  this.name = name;
  this.url = url;
  this.i = i;
  this.j = j;
 }

// 출력 메소드

 public void print() {
  System.out.println(this.name);
  System.out.println(this.url);
  System.out.println(this.i);
  System.out.println(this.j+"\n");
 }
}

// 메인 클래스
public class ObjectTest {

// 메인 메소드

 public static void main(String[] args) {
  SaveData[] data = new SaveData[2];
  SaveData[] open = new SaveData[2];

// 각 객체에는 다른 생성자를 이용해서 객체에 연결해준다.

  data[0] = new SaveData();
  data[1] = new SaveData("인사불성", "???", 10, 20);

  try {
// 객체를 저장한다.
   ObjectOutputStream oos = new ObjectOutputStream(
     new BufferedOutputStream(new FileOutputStream("data")));
   oos.writeObject(data);
   oos.flush();
   oos.close();
   
// 저장 파일을 객체로 읽는다.
   ObjectInputStream ois = new ObjectInputStream(
     new BufferedInputStream(new FileInputStream("data")));

// 읽어들인 객체는 오브젝트 형이므로 캐스팅을 해줘야 한다.
   open = (SaveData[])ois.readObject();
   ois.close();
  } catch (Exception e) {
   e.printStackTrace();
  }
  
// 배열객체의 내용을 확인한다.
  for(int i=0;i<open.length;i++){
   open[i].print();
  }
 }
}

위와 같은 예제를 이용해서 객체를 저장 한 경우에 이 파일을 텍스트 파일로 읽으면 문자 깨짐 현상이 일어납니다. 당연히 바이너리 파일이기 때문이지요. 그러나 이 파일을 바이너리 코드 편집기를 통해서 열기를 해보면 잼있는 현상을 볼 수 있습니다.

그 예로 위에서 data[0] 객체에 int 형 변수 i에 1000 이라는 숫자를 넣어서 저장을 했습니다. 이것을 바이너리 편집기를 이용해서 열어보면..

16진 코드 사이에서 10진수 1000이 16진수로 저장 된 것을 발견 할 수 있습니다. 03 E8 부분이 10진수 1000이 되는 부분이지요.

이 부분을 수정해서 저장 한다면 당연히 객체를 읽어 오기만 하는 소스를 짜서 파일을 바이너리 편집기로 수정 한 뒤 읽어 들이면 값이 변화된 것을 확인 할 수 있습니다.

이 부분은 게임 세이브 파일을 해킹해서 수정해본 사람이라면 쉽게 이해가 될 것이라고 봅니다.
(파일에 코드가 뒤집힌 경우는 암호화 기법을 이용해서 마지막에 체크썸 코드를 넣는 방식을 취한 경우가 대부분일 겁니다.)

이 점을 생각해 일반적으로 게임에서 세이브를 하는 방식은 객체를 저장하는 방식을 취한다는 것을 예상 할 수 있습니다. 저 또한 그렇게 이용할 것이라고 생각하고 있답니다.

그럼 이번 글은 여기서 마치도록 하겠습니다.

개인적으로 조금 흥미를 유발하는 부분이 많아서 생각 했던 것보다 내용이 좀 길어졌네요 ^^;;

그럼 다음에 또 만나요 ~

by 만성피로 | 2008/11/24 21:51 | 정리할 자료들 | 트랙백 | 덧글(4)

화이트 보드 만들기 예제.

이번 글에서는 화이트보드를 만들어 보도록 하겠습니다.

그림은 보통 하얀색 도화지에 연필을 이용해서 그림을 그리게 됩니다.
이것을 자바로 만들어야 한다면 도화지와 연필을 만들어야 합니다.
## 혹시나 해서 씁니다. Canvas를 떠올리지 않는 편이 좋습니다. 혼란에 빠지지 마시길 ㅎㅎㅎ...

도화지는 화이트보드 클래스가 될 것이고, 연필은 펜 클래스가 될 것입니다.
설계한 방식에 따라 다르지만 여기서 펜 클래스는 펜의 객체 자체가 그림을 그리는 것은 아닙니다.
펜 클래스는 현재 그릴 그림이 사각형인지, 삼각형인지를 설정하게 되는 클래스 입니다.

잠깐 다른 것을 예로 들자면..
학생 정보를 담고 있는 Student 라는 클래스가 존재합니다. 이 안에는 각 학생에 대한 정보를 담을 수 있는
이름, 학번, 성적 필드가 존재하겠지요. 이와 마찬가지 입니다. 펜은 사각형 펜, 삼각형 펜 등이 될 수 있는 것 입니다.

가장 대표적인 클래스 두개를 이야기 했으니 만들어야 할 화이트보드의 인터페이스를 보도록 합시다.
위에 그림을 보면 좌측에는 8개의 버튼이 존재하고 가운데는 도화지에 해당하는 공란이 있습니다.

좀더 자세하게 설명을 하면
좌측에 버튼들은 하나의 패널 안에 GridLayout를 이용해서 배치 한 뒤 메인프레임에 적용 된 것이고,
가운데 공란은 보기에는 공란이지만 프레임에 Canvas를 적용한 것 입니다.

그럼 본격적으로 요구조건을 나열하겠습니다.

1차 요구조건
Pen 클래스
- 펜의 타입을 설정하는 int type를 맴버변수로 갖고 있다.
- 펜을 설정하는 setType, 설정을 리턴하는 getType 변수를 갖고 있다.

MyWhiteBoard 클래스
- 프레임을 상속한다.
- Pen 클래스의 객체를 맴버변수로 갖고 있다.
- MenuPanel 클래스의 객체를 맴버변수로 갖고 있다.
- MyCanvas 클래스의 객체를 맴버변수로 갖고 있다.
- 프레임 크기는 가로, 세로 500 전후 또는 임의로 설정한다.
- 실행 시 프레임은 윈도우 화면 정 중앙에 출력된다.
- 좌측에는 MenuPanel 클래스의 객체가 배치돼야 한다.
- 가운데는 MyCanvas 클래스의 객체가 배치돼야 한다.
- 프로그램 종료 이벤트를 적용한다.

--- 아래의 클래스들은 내부 클래스로 구현 한다.--------------
MenuPanel 클래스
- Panel 클래스를 상속한다.
- GridLayout를 이용해서 버튼을 배치한다.

MyCanvas 클래스
- Canvas 클래스를 상속한다.
- 마우스에 좌표를 기억할 xp, yp, xr, yr 맴버변수를 갖고 있다.
- 마우스가 눌러질 될 때와 떼어질 때 좌표를 기억하도록 하는 이벤트를 만든다.
- MouseListenet 인터페이스 또는 MouseAdapter 클래스를 이용하여 마우스 이벤트를 구현한다.
## 이벤트가 캔버스에 직접 그림을 그리는것은 아니다. 이벤트는 좌표를 기억하는 용도로 구현한다.

MenuListener 클래스
- ActionListener 인터페이스를 구현한다.
---------------------------------------------------------


2차 요구 조건
MenuPanel 클래스
- 각 버튼에 MenuListener 클래스를 이용하여 이벤트를 적용시킨다.

MenuListener 클래스
- 모두지우기는 MyWhiteBoard 클래스의 MyCanvas객체를 이용해서 clearRect() 메소드를 이용해서 지운다.
- 눌러진 버튼에 따라 MyWhiteBoard 클래스의 Pen객체에 type를 설정한다.
# type는 0 부터 사각형이고 6은 지우기이다. 그러나 임의의 방식을 이용해도 상관 없다.
# clearRect() 메소드를 이용해서 전체지우기를 할 때는 클릭 했을 때 바로지워진다.

MyCanvas 클래스
- 마우스가 떼질 때 발생하는 이벤트 메소드에서 draw() 메소드를 호출한다.
- draw() 메소드는 MyWhiteBoard의 MyCanvas객체를 이용해 그림을 그리는 역할을 한다.

3차 요구 조건
- 위에 조건을 종합해 보면 그림을 그리는 방식은 그래그를 하는 방식으로 그림을 그린다. 그런데 여기서 발생할 수 있는 문제가 한가지 존재 한다. 마우스 드래그 방향이 좌측 상단에서 우측 하단으로(↘) 그려 질 때는 전혀 문제가 없다. 그러나 다음의 방향으로 드래그를 할 경우에는 문제가 발생한다. 다음의 요구사항을 처리해라.
    1. 좌측 하단에서 우측 상단으로 드래그 해서 그린다.(↗)
    2. 우측 하단에서 좌측 상단으로 드래그 해서 그린다.(↖)
    3. 우측 상단에서 좌측 하단으로 드래그 해서 그린다.(↙)

- 부채꼴의 경우 그려지는 부채꼴의 각도는 임의로 한다. 다음에 그림 처럼 그려지도록 한다.
이 정도의 요구조건이면 완성 할 수 있을 듯 합니다. 해보시고 궁금한 점이나 부족한 부분이 있으면 댓글을 달아주시기 바랍니다.

그리고 완성 된 파일을 공개해 두겠습니다.
MyWhiteBoard.java

다 완성 했다면 본인의 실력을 한층 올릴 수 있도록 추가 기능을 넣어 보는 것도 좋은 공부입니다.

감사합니다. ^^;

by 만성피로 | 2008/11/20 16:05 | 정리할 자료들 | 트랙백 | 덧글(2)

1:1 채팅 프로그램 예제.

이번 글에서는 1:1 채팅 프로그램 예제에 대해서 이야기 하고자 합니다.

그럼 가장 먼저 해야 할 것은 설계 입니다. 채팅 프로그램이 어렵지는 않지만 처음 만들어 보는 것이라면 어떤 구조로 동작 할 것인지 설계를 하는 편이 좋습니다.

그럼 간단하게 설계를 하도록 합시다.

가장 먼저 java.net 패키지에서 어떤 클래스들을 사용해야 할지를 생각해 봅시다.

보통 기본 예제로 배우기 쉬운 것이 ServerSocket 클래스와 Socket 클래스를 이용한 에코서버 만들기 또는 메시지를 전달 할 수 있는 예제일 것입니다.

우리는 이 점을 이용해서 Socket클래스와 ServerSocket 클래스를 활용할 것이므로 메시지를 받을 때는 ServerSocket 클래스를 이용하고 메시지를 보낼 때는 Socket 클래스를 이용하면 됩니다.


두번 째로 채팅 프로그램에서 메시지를 주고 받는 방식에 대한 세부적인 설계가 필요합니다. java.net 패키지를 이용해서 어떻게 주고 받을 것인가를 간략하게 설계 했으나 메시지를 주고 받게 되는 것은 텍스트/바이트 스트림입니다.

메시지를 송신 할 때는 출력에 해당 하므로 PrintWriter 클래스를 활용할 것입니다.
그렇다면 당연히 메시지를 수신 할 때는 입력에 해당 하므로 BufferedReader 클래스를 활용할 것입니다.


세번 째로는 위에 두가지를 조금 정리 하도록 하겠습니다.
메시지를 받을 때는 ServerSocket  클래스를 사용합니다. 이 동작은 메시지 수신에 해당하는 동작이므로 BufferedReader 클래스와 같이 동작해야 합니다.
메시지를 보낼 때는 Socket 클래스를 사용합니다. 이 동작은 메시지 송신에 해당하는 동작이므로 PrintWriter 클래스와 같이 동작해야 합니다.

네번 째로는 송수신이 동시에 동작해야 합니다. 수신을 하면서도 송신이 가능해야 하므로 멀티쓰레드를 이용하는 편이 좋습니다.

조금 더 자세하기 이야기를 하자면 송신을 하는 방법은 송신을 해야 할 때만 송신을 하면 되는 것이므로 큰 문제가 없으나 수신의 경우에는 언제나 받을 준비가 되야 하기 때문에 수신을 위한 동작이 돌고 있어야 하면서도 송신도 가능해야 합니다.
만약에 이 동작이 단일 쓰레드 라면 이 부분에서 문제가 될 소지가 있습니다. 내가 메시지를 보내는 사이에 수신을 해야 한다면 어떻게 될까요?
일단 무식한 결론을 내리겠습니다. 단일 쓰레드를 이용해도 컴퓨터의 동작 빠르기 때문에 큰 문제가 없을 듯 합니다. 그러나 메시지를 받는데 1분이 걸리고 메시지를 보내는데 1분이 걸린다고 가정 했을 때는 단일 쓰레드의 경우에는 이부분에서 2분이라는 시간을 소요하게 됩니다.
멀티 쓰레드라면 보내는 것과 받는 것이 동시에 동작하므로 1분이 걸리게 되는 것이지요.
또한 다수의 채팅방에 경우에 여러 사람이 메시지를 전달 할 때 단일 쓰레드로 처리 하면 발생하는 문제가 있기 때문입니다. 100명이 메시지를 전달 한다면 마지막 메시지는 출력되는데 오래 걸리겠지요..
(여기서 전이중 통신 반이중 통신 등 네트워크와는 상관 없이 멀티쓰레드를 위한 예 이므로 네트워크적인 요소는 일단 접어두고 생각하셔야 합니다.)

마지막으로는 적절한 인터페이스와 몇번 포트를 사용할 것인가를 정해야 합니다. 인터페이스 구성은 프로그램을 짜는 사람마다 다르므로 예제는 그림으로 보여드리도록 하겠습니다.
포트 또한 알아서 정하셔도 됩니다. 예제에서는 10000번 포트를 사용했습니다.


1. ServerSocket 클래스는 메시지를 받을 때 사용한다.
2. Socket 클래스는 메시지를 보낼 때 사용한다.
3. 송신은 PrintWriter 클래스 사용, 수신은 BufferedReader 클래스 사용.
4. 송신과 수신은 동시에 동작할 수 있어야 한다.(멀티쓰레드 이용)
5. 적절한 인터페이스 구성과 사용할 포트번호를 정한다.

이를 간단하게 클래스 다이어그램으로 그리면 아래와 같습니다.

OneToOneChat - JFrame(상속), Runnable(구현), ActionListenet(구현)

-Socket serversocket
-Socket sendsocket

-ServerSocket server

-BufferedReader in

-PrintWriter out

적절한 인터페이스...

+OneToOneChat()

+setServer() : void

+process() : void

+run() : void

+send() : void

+exit() : void

+actionPerformed(ActionEvent e) : void


간략하게 설명을 하자면
OneToOneChat 클래스 하나로 구현이 가능합니다. 쉽기 때문이지요.

생성자를 이용해서 인터페이스를 구성합니다.

setServer() 메소드는 server 객체에 포트번호 10000번을 인수로 하는 ServerSocket 생성자를 이용해 셋팅을 해줍니다.

process() 메소드는 serversocket 객체를 이용해서 언제든지 메시지를 받을 수 있도록 설정합니다. 이 메소드가 하는 일은 상대방으로부터 메시지를 수신하는 역할을 합니다.

run() 메소드는 Runnable 인터페이스를 구현 한 것입니다. 이 메소드는 setServer(), process() 메소드를 동작 시키는 역할을 합니다.

send() 메소드는 sendsocket 객체를 이용해 메시지를 송신 하는 역할을 합니다.

exit() 메소드는 프로그램을 종료하면 상대방에게 프로그램을 종료 했다는 메시지를 보내고 프로그램 동작을 종료 하는 역할을 합니다.

actionPerformed(ActionEvent e) 메소드는 ActionListenet 인터페이스를 구현 한 메소드 입니다. 메시지를 전달 하는 send() 메소드를 동작 시키는 역할을 합니다.

기타..
프로그램 종료 이벤트는 무명 클래스로 동작하고 WindowAdapter 클래스를 이용합니다. 동작 시키는 이벤트는 exit() 메소드를 동작 시키는 역할을 합니다.
대화 상대가 없을 경우에 Exception을 발생 시켜 "호스트는 오프라인 또는 존재하지 않을 수도 있습니다." 메시지를 출력하도록 합니다.
멀티 쓰레드를 이용하므로 메인메소드와 run() 메소드는 동시에 동작 해야 합니다.

이것으로 필요한 설계에 조건은 대부분 다 모인 듯 합니다.

이제 이와 비슷한 토대로 작성을 하고 동작 되는지를 확인합니다.


그리고 소스를 아래 파일을 참고 하시기 바랍니다.
OneToOneChat.java

by 만성피로 | 2008/11/17 20:22 | 정리할 자료들 | 트랙백 | 덧글(1)

◀ 이전 페이지다음 페이지 ▶