'JAVA PROGRAMMING'에 해당되는 글 105건

  1. 2011/09/12 블루투스 프로그래밍
  2. 2010/02/24 Intent Filter 란?
  3. 2010/02/21 12.3 Externalizable
  4. 2010/02/21 12.2 Serializable
  5. 2010/02/21 12.1 Serialization

블루투스 프로그래밍

JAVA PROGRAMMING/Android 2011/09/12 23:52 by 킨테리 KinTeL



원문 :  http://www.cyworld.com/csm47/3486608



안드로이드는 블루투스 프로토콜 스택을 포함하고 있기 때문에 블루투스 디바이스들과 무선으로 데이터를 교환할 수 있다. 어플리케이션 프레임웍은 안드로이드 블루투스 API를 사용해 블루투스에 억세스 할 수 있다. 블루투스 API를 사용하면 다음과 같은 작업을 할 수 있다.

  • 다른 블루투스 디바이스 검색
  • 페어링 된 블루투스 디바이스를 위한 로컬 블루투스 아답터 퀘리
  • RFCOMM 채널 설정
  • SDP(Service Discovery Protocol)을 통한 다른 디바이스와의 커넥션
  • 양방향 데이터 전송
  • 복수 커넥션 관리


- 기초

이 문서는 블루투스를 사용해 통신하는데 필요한 4가지 주요 태스크(블루투스 셋업, 페어링 되어 있거나 주변에 있는 기기 검색, 디바이스와 연결, 디바이스간 데이터 전송)를 수행하기 위해 안드로이드 블루투스 API를 어떻게 사용하는가를 설명한다.
모든 블루투스 API는 android.bluetooth 패키지에 들어있다. 다음은 블루투스 연결을 만드는데 필요한 클래스들의 요약이다.

  • BluetoothAdapter - 로컬 블루투스 아답터 하드웨어를 나타낸다. BluetoothAdapter는 모든 블루투스를 통한 상호작용의 엔트리포인트이다. 이 객체를 사용해서 다른 블루투스 디바이스 찾기, 페어링 된 디바이스 퀘리, 알려진 MAC address를 사용해 BluetoothDevice 인스턴스 얻기, 다른 디바이스에서 부터의 통신 요구를 기다리기 위한 BluetoothServerSocket 만들기를 할 수 있다.
  • BluetoothDevice - 상대방의 블루투스 디바이스를 나타낸다. 이 객체를 사용하면 BluetoothSocket을 통해 상대방 디바이스와 커넥션을 요구하거나 이름, 주소, 클래스, 페어링 상태등의 정보를 퀘리할 수 있다.
  • BluetoothSocket - 블루투스 소켓을 위한 인터페이스를 나타낸다. 어플리케이션이 InputStream과 OutputStream을 사용해서 다른 블루투스 디바이스와 데이터 교환을 할 수 있는 연결 포인트이다.
  • BluetoothServerSocket – Incoming 리퀘스트를 위해 listen하고 있는 오픈된 서버소켓(TCP ServerSocket과 유사)을 나타낸다. 두대의 안드로이드 디바이스를 연결하기 위해 한쪽의 디바이스는 이 클래스를 사용해서 서버소켓을 오픈해야만 한다. 원격 블루투스 디바이스가 디바이스에 커넥션 리퀘스트를 때 BluetoothServerSocket은 커넥션이 연결되면 연결된 BluetoothSocket을 리턴해준다.
  • BluetoothClass - 블루투스 디바이스의 일반적 특성과 기능을 나타낸다. 이 클래스는 디바이스의 디바이스 클래스와 서비스를 정의하는 읽기 전용 속성의 집합이다.


- 블루투스 퍼미션

어플리케이션에서 블루투스 기능을 사용하려면 최소한 BLUETOOTH와 BLUETOOTH_ADMIN 둘중에 하나의 블루투스 퍼미션을 선언해줘야 한다. 커넥션 요구, 커넥션 accept, 데이터 전송등의 블루투스 통신을 하기 위해서는 BLUETOOTH 퍼미션이 필요하다.
디바이스 discovery를 시작하거나 블루투스 설정을 조작하려면 BLUETOOTH_ADMIN 퍼미션이 필요하다.
BLUETOOTH_ADMIN 퍼미션을 사용하려면 BLUETOOTH 퍼미션도 꼭 있어야만 한다. 매니페스트 파일에 블루투스 퍼미션을 선언해준다.

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />




- 블루투스 셋업

어플리케이션이 블루투스로 통신을 하기 전에 디바이스가 블루투스를 지원하는지 확인할 필요가 있다. 그리고 블루투스를 지원한다면 활성화 되었는지도 확인해줘야 한다. 만일 블루투스를 지원하지 않으면 블루투스 기능을 비활성화 시켜야 한다. 블루투스를 지원하지만 활성화 되어 있지 않으면 사용자가 어플리케이션을 떠나지 않고 블루투스를 활성화하도록 요구할 수 있다. 이 작업은 BluetoothAdapter를 사용해서 단계로 수행할 수 있다.

1.BluetoothAdapter 를 얻는다.
모든 블루투스 액티비티를 위해 BluetoothAdapter가 요구된다. BluetoothAdapter를 얻기 위해서는 스태틱 메소드인 getDefaultAdapter()를 호출하면 된다. 그러면 디바이스의 블루투스 아답터를 나타내는 BluetoothAdapter 인스턴스를 리턴한다.

BluetoothAdapter mBTAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBTAdapter == null) {
// device does not support Bluetooth
}


2.블루투스 활성화
블루투스가 활성화 되어있는지 확인해야 한다. isEnabled()를 호출해서 블루투스가 현재 활성화되어 있는지 확인한다. 메소드가 false를 리턴하면 블루투스가 비활성화되어 있는 것이다. 블루투스를 활성화 시키려면 ACTION_REQUEST_ENABLE 인텐트로 startActivityForResult()를 호출하면 된다.

If (!mBTAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) ;
}


그림 1과 같이 블루투스를 활성화하기 위한 퍼미션을 요구하는 대화창이 나타난다. 사용자가 “Yes”를 선택하면 시스템은 블루투스를 활성화시키고 그 과정이 끝나면 어플리케이션으로 포커스가 돌아오게 된다.
블루투스 활성화가 성공하면 액티비티는 onActivityResult() 콜백에서 RESULT_OK를 리턴받게 된다. 블루투스가 에러로 인해 (또는 사용자가 “No”를 선택해서) 활성화되지 못하면 RESULT_CANCELED가 리턴된다. 옵션으로 블루투스 상태가 변경될 마다 시스템이 브로드캐스하는 ACTION_STATE_CHANGED 인텐트를 listen하도록 할 수도 있다.

- 디바이스 검색

BluetoothAdapter를 사용하면 디바이스 discovery 또는 페어링 된 디바이스 목록을 퀘리해서 원격 블루투스 디바이스를 찾을 수 있다.
디바이스 discovery는 주변의 활성화 된 블루투스 디바이스를 찾고 각각에 대한 정보를 요구하는 검색 단계이다. 하지만 통신가능 범위에 들어있는 블루투스 디바이스라 해도 현재 discoverable 하도록 활성화 되어 있어야만 discovery 요구에 응답한다. 디바이스가 discoverable 상태인 경우 discovery 요구에 디바이스 이름, 클래스, MAC 주소같은 정보를 공유함으로서 응답한다. 이 정보를 사용해서 discovery를 수행한 디바이스는 발견된 디바이스에 커넥션을 시작하도록 선택할 수 있다.
일단 원격 디바이스와 처음으로 연결이 이루어지면 자동으로 사용자에게 페어링을 할 것인가 물어보게 된다. 디바이스 페어링이 이루어지면 상대 디바이스에 대한 기본 정보(디아비스 이름, 클래스, MAC 주소 등)가 저장되고 그 내용은 블루투스 API를 통해 읽을 수 있게 된다. 이미 알고 있는 원격디바이스의 MAC 주소를 사용하면 아무때나 (물론 해당 디바이스가 통신 가능범위에 있다는 가정 하에) discovery를 수행할 필요 없이 바로 커넥션 과정을 시작할 수 있다.
페어링과 연결된것의 차이점은 잘 알고 있어야 한다. 페어링은 두 디바이스가 각자 상대방의 존재를 알고 있고 인증과정에 사용할 link-key를 공유하고 있어 서로간에 암호화 된 연결을 설정할 수 있다는걸 의미한다. 연결된것은 디바이스가 현재 RFCOMM 채널을 공유하고 있어 서로 데이터를 전송할 수 있는 상태를 의미한다.
현재 안드로이드 블루투스 API는 RFCOMM 커넥션을 설정하기 전에 디바이스가 페어링 되어야만 한다. (블루투스 API에서 암호화된 커넥션을 시작하려고 할 때 페어링이 자동을 이루어진다.)
다음의 섹션은 페어링 된 디바이스를 찾거나, 디바이스 discovery를 사용해 새 디바이스를 찾는 방법을 설명한다.
주: 안드로이드 디바이스는 기본적으로 not discoverable 상태이다. 시스템 설정을 통해 짧은 시간동안 디바이스를 discoverable 상태로 만들거나 어플리케이션에서 직접 discoverable 상태로 만들어 줄 수 있다.

- 페어링 된 디바이스 퀘리

디바이스 discovery를 수행하기 전에 원하는 디바이스가 이미 페어링 되어 있는가 확인해 볼 필요가 있다. 확인하기 위해서 getBondedDevices()를 호출하면 된다. 그러면 페어링 된 디바이스들의 집합인 BluetoothDevices 를 돌려준다. 예를 들어 페어링 된 모든 디바이스를 퀘리한 다음 ArrayAdapter를 사용해 페어링 된 디바이스의 이름을 보여줄 수 있다.

Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
if (pairedDevices.size() <> 0) {
for (BluetoothDevice device : pairedDevices) {
mArrayAdapter.add(device.getName() + “\n” + device.getAddress());
}
}


BluetoothDevice 객체에서 연결을 시작하기 위해 필요한 정보는 MAC address만 있으면 된다. 위의 예제에서 이 정보는 사용자에게 보여지는 ArrayAdapter의 일부분에 저장되어 있다. MAC 주소는 나중에 연결을 시작하기 위해 추출할수도 있다.

- 디바이스 discovery

디바이스 discovery를 시작하려면 startDiscovery()를 호출하면 된다. 이 과정은 비동기식이라 메소드를 호출하면 discovery가 성공적으로 시작되었나 결과를 알려주는 boolean값을 곧바로 돌려준다. Discovery과정은 보통 12초간의 inquiry scan후 발견된 각 디바이스에 대해 이름을 가져오기 위한 page scan으로 이루어진다.
어플리케이션은 각 발견된 디바이스에 대한 정보를 받기 위해 ACTION_FOUND 인텐트를 위한 BroadcastReceiver를 등록해야만 한다. 각 디바이스마다 시스템이 ACTION_FOUND 인텐트를 브로드캐스트 한다. 이 인텐트는 각각 BluetoothDevice와 BluetoothClass가 들어있는 EXTRA_DEVICE와 EXTRA_CLASS 필드를 전달한다. 예제로 디바이스가 발견되었을 때 브로드캐스트를 처리하는 핸들러를 등록하는 방법이다.

Final BroadcastReceiver mReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
mArrayAdapter.add(device.getName() + “\n” + device.getAddress());
}
}
};

BroadcastReceiverIntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND;
registerReceiver(mReceiver, filter);


커넥션을 시작하기 위해 BluetoothDevice 객체에서 필요한 정보는 MAC 주소뿐이다. 이 예에서는 사용자에게 보여지는 ArrayAdapter의 일부분에 저장되어 있다.

주의: 디바이스 discovery를 수행하는건 블루투스 아답터에게 매우 부담이 큰 작업으로 매우 많은 리소스를 요구한다. 커넥션 할 디바이스를 찾았다면 커넥션을 시작하려고 시도하기 전에 cancelDiscovery()를 호출해서 discovery를 멈춰야 한다. 또한 이미 다른 디바이스와 커넥션 되어 있으면 discovery과정동안 대역폭이 활 떨어질수도 있기 때문에 커넥션 된 상태에서는 discovery를 하지 않아야 한다.
- Discoverable 활성화

다른 디바이스가 자신의 디바이스를 검색할 수 있도록 해 주려면 startActivityForResult(Intent, int)에 ACTION_REQUEST_DISCOVERABLE 액션 인텐트를 넣어 호출해주면 된다. 이 메소드를 호출하면 어플리케이션을 멈추지 않고 시스템 설정을 통해 discoverable 모드를 활성화 하도록 요청한다. 기본적으로 디바이스는 120초동안 discoverable 모드로 있게 된다. EXTRA_DISCOVERABLE_DURATION 인텐트 extra를 추가해서 시간을 바꿔줄 수 있다. (최대 300초)

Intent discoverableIntent = new Intent(BluetoothAdpater.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300);
startActivity(discoverableIntent);




그림 2와 같은 다이얼로그가 떠서 사용자에게 디바이스를 discoverable 상태로 만들도록 허가할 것인지 묻는다. “Yes”를 선택하면 디바이스는 정해진 시간동안 discoverable상태가 된다. 액티비티는 result code에 디바이스가 discoverable되는 시간값이 들어가서 onActivityResult() 콜백을 호출받게 된다. 사용자가 “No”를 선택하거나 에러가 발생하면 result code는 Activity.RESULT_CANCELLED가 된다.
디바이스는 discoverable 시간동안 아무 반응이 없이 조용히 있는다. 만일 discoverable모드가 변경될 때 통보를 받고 싶으면 ACTION_SCAN_MODE_CHANGED 인텐트에 대한 BroadcastReceiver를 등록할 수 있다. 이 인텐트에는 각각 이전 스캔모드와 변경된 새 스캔모드가 들어있는 EXTRA_PREVIOUS_SCAN_MODE와 EXTRA_SCAN_MODE라는 extra 필드를 가지고 있다. 각 필드에 들어갈 있는 값은 SCAN_MODE_CONNECTABLE_DISCOVERABLE, SCAN_MODE_CONNECTABLE, SCAN_MODE_NONE으로 각각 discoverable 모드, discoverable은 아니지만 커넥션을 받아들일 수는 있는 모드, discoverable도 아니고 커넥션도 받아들일 수 없는 모드를 나타낸다.
원격 디바이스와 커넥션을 시작하고 싶은 경우는 자신의 디바이스를 discoverable모드로 만들 필요는 없다. 원격 디바이스가 커넥션을 시작하기 전에 디바이스를 발견해야만 하기 때문에 내 디바이스의 discoverable 모드를 활성화 시키는건 어플리케이션이 서버소켓을 사용해서 incoming 연결을 accept할 때만 필요하다.

- 디바이스 커넥션

두 디바이스에서 실행되는 어플리케이션간에 커넥션을 만들기 위해서는 서버쪽과 클라이언트쪽 메카니즘을 모두 구현해 줘야만 한다. 한 디바이스는 서버소켓을 열어줘야 하고 다른 디바이스가 서버 디바이스의 MAC 주소를 사용해서 커넥션을 시작해야만 하기 때문이다. 서버와 클라이언트는 같은 RFCOMM 채널에 각각 커넥션 된 BluetoothSocket을 가지고 있을 때 서로 커넥트 된 것으로 간주된다. 이 지점에서 각 디바이스는 입, 출력 스트림을 얻어 데이터 전송을 시작할 수 있다. 이 섹션에서는 두 디바이스간에 커넥션을 시작하는 방법에 대해서 설명한다.
서버 디바이스와 클라이언트 디바이스는 서로 다른 방법으로 필요한 BluetoothSocket을 얻는다. 서버는 incoming 연결이 accept될 때 소켓을 받게 된다. 클라이언트는 서버로의 RFCOMM 채널을 열 때 소켓을 받게 된다.



한가지 구현 테크닉은 두 디바이스를 모두 서버로 동작하도록 하기 위해 서버소켓을 열고 커넥션을 기다리는 것이다. 그러면 어느 디바이스건 클라이언트로서 상대 디바이스로 커넥션을 시작할 수 있다. 다른 방법으로는 한 디바이스는 명시적으로 서버로 지정해 서버소켓을 열고 커넥션을 기다리고 다른 디바이스는 단순히 클라이언트로 커넥션을 시작할 수 있다.
주) 두 디바이스가 미리 페어링 되어 있지 않으면 안드로이드 프레임웍은 그림 3과 같이 자동으로 페어링을 요구하는 다이얼로그를 띄워준다. 그러므로 디바이스를 커넥트 하려고 할 때 어플리케이션은 디바이스가 미리 페어링 되어 있는지 여부를 걱정할 필요가 없다. RFCOMM 커넥션 시도는 사용자가 성공적으로 페어링을 마치거나 페어링을 거부하거나 또는 어떤 이유로건 페어링이 실패할 때 까지 블럭된다.

서버로 동작
두 디바이스를 커넥트하려고 할 때 하나의 디바이스는 BluetoothServerSocket을 열어 서버로 동작해야만 한다. 서버소켓의 목적은 incoming 커넥션 요구를 기다리다 accept되면 커넥션 된 BluetoothSocket을 제공해 주는 것이다. BluetoothServerSocket에서 BluetoothSocket이 얻어지고 더 이상의 커넥션을 accept할 필요가 없으면 BluetoothServerSocket은 제거해도 된다.

UUID란...
Universally Unique IDentifier(UUID)는 유일하게 정보를 식별하는데 사용하기 위한 128비트 포맷의 표준화 된 문자열 ID이다. UUID의 포인트는 이 숫자가 충분히 크기 때문에 랜덤하게 아무 숫자나 골라도 다른 UUID들과 겹치지 않는다는 것이다. 여기서는 어플리케이션의 블루투스 서비스를 식별하는데 사용된다. 어플리케이션에 사용할 UUID를 얻기 위해서 인터넷상의 여러가지 랜덤 UUID 생성기중에 하나를 사용할 수 있고 fromString(String)으로 UUID를 초기화 하면 된다.
서버소켓을 셋업하고 연결을 accept하는 기본적인 절차이다.

1.listenUsingRfcommWithServiceRecord(String, UUID)를 호출해서 BluetoothServerSocket을 얻어온다.
스트링은 서비스에 대한 식별할 수 있는 이름으로 시스템이 디바이스의 새 SDP 데이터베이스 엔트리에 자동으로 그 이름을 기록한다. UUID 또한 SDP엔트리에 포함되어 클라이언트와 커넥션 agreement를 위한 기초가 된다. 즉 클라이언트가 디바이스와 커넥션하려고 시도할 때 커넥션하길 원하는 서비스를 유일하게 식별하는 UUID를 제공한다. 커넥션이 이뤄지기 위해서는 이 UUID가 일치해야만 한다.

2.accept()를 호출해서 커넥션 요구를 listen하기 시작한다.
이 메소드는 블럭킹 호출이다. 커넥션이 accept되거나 익셉션이 발생해야만 리턴된다. 리모트 디바이스가 listen하고 있는 서버소켓에 등록한 UUID와 일치하는 커넥션 요구에만 연결이 만들어진다. 성공하면 accept()는 커넥션 된 BluetoothSocket을 리턴한다.


3.더 이상의 추가 커넥션이 필요하지 않으면 close()를 호출한다.
이 메소드를 호출하면 서버소켓과 관련된 리소스를 release한다. 하지만 accept()가 리턴한 커넥션 된 BluetoothSocket은 닫지 않는다. TCP/IP와 달리 RFCOMM은 클라이언트에서 한번에 하나의 커넥션만 허용하기 때문에 대부분의 경우에 커넥션이 만들어지면 곧바로 BluetoothServerSocket을 close()하는게 합리적이다.

accept()는 블럭킹 메소드라 어플리케이션의 다른 동작을 막기 때문에 메인 액티비티 UI 스레드에서 호출하면 안된다. 일반적으로 새로운 스레드에서 BluetoothSocket이나 BluetoothServerSocket에 관련된 모든 작업을 처리하는게 합리적이다. 다른 스레드에서 BluetoothServerSocket의 accept() 같이 블럭킹 것을 취소하고 바로 리턴하도록 하려면 close()를 호출하면 된다. 그리고 BluetoothServerSocket 또는 BluetoothSocket의 모든 메소드는 스레드-세이프하다.

예제) incoming 연결을 accept하는 서버 컴포넌트를 위한 간단한 스레드
private class AcceptThread extends Thread {
private final BluetoothServerSocket mmServerSocket;

public AcceptThread() {
// Use a temporary object that is later assigned to mmServerSocket,
// because mmServerSocket is final
BluetoothServerSocket tmp = null;
try {
// MY_UUID is the app's UUID string, also used by the client code
tmp = mAdapter.listenUsingRfcommWithServiceRecord(NAME, MY_UUID);
} catch (IOException e) { }
mmServerSocket = tmp;
}
public void run() {
BluetoothSocket socket = null;
// Keep listening until exception occurs or a socket is returned
while (true) {
try {
socket = mmServerSocket.accept();
} catch (IOException e) {
break;
}
// If a connection was accepted
if (socket != null) {
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(socket);
mmServerSocket.close();
break;
}
}
}
/** Will cancel the listening socket, and cause the thread to finish */
public void cancel() {
try {
mmServerSocket.close();
} catch (IOException e) { }
}
}

이 예제에서 한개의 incoming 커넥션만 필요하기 때문에 커넥션이 accept되고 BluetoothSocket이 얻어지자 마자 어플리케이션은 얻은 BluetoothSocket을 별도의 스레드로 보낸 다음 BluetoothServerSocket을 닫고 루프를 빠져나온다.
accept()가 BluetoothSocket을 리턴할 때 소켓은 이미 커넥션 되어 있기 때문에 따로 connect()를 호출할 필요는 없다. manageConnectedSocket()은 어플리케이션에서 데이터 전송을 위한 스레드를 시작하는 fictional 메소드이다.
일반적으로 incoming 커넥션을 listen하는게 끝나면 곧바로 BluetoothServerSocket을 닫아준다. 이 예제에서도 BluetoothSocket이 얻어지자 마자 close()를 호출했다. 또한 listen하고 있는 서버소켓을 멈출 필요가 있을 때 private BluetoothSocket을 닫을 있는 public 메소드를 스레드에서 제공하기도 한다.

클라이언트로 동작
원격 디바이스와 커넥션을 시작하려면 우선 원격 디바이스를 나타내는 BluetoothDevice 객체를 얻어야만 한다. 그리고 나면 BluetoothDevice를 사용해서 BluetoothSocket을 얻어 커넥션을 시작한다.

기본적인 절차이다.

1.BluetoothDevice를 사용해서 createRfcommSocketToServiceRecord(UUID)를 호출해서 BluetoothSocket을 얻는다.
이 호출은 BluetoothDevice에 연결하는 BluetoothSocket을 초기화한다. 여기서 건네지는 UUID는 서버 디바이스가 자신의 BluetoothServerSocket(listenUsingRfcommWithServiceRecord(String, UUID)를 사용해서)을 열었을 때 사용한 UUID와 일치해야만 한다. 동일한 UUID를 사용하는건 UUID스트링을 어플리케이션 코드에 하드코딩하고 서버와 클라이언트 양쪽 코드에서 그걸 참조하면 되는 간단한 문제이다.

2.connect()를 호출해서 연결을 시작한다.
시스템은 UUID를 매치하기 위해 원격 디바이스 SDP lookup을 수행한다. Lookup이 성공하고 원격 디바이스가 커넥션을 accept하면 연결동안 사용할 RFCOMM채널을 공유하고 connect()가 리턴한다. 메소드는 블럭킹 호출이다. 어떤 이유로건 커넥션이 실패하거나 connect() 메소드가 time out (약 12초)이 되면 exception을 발생한다.
connect()는 블럭킹 호출이기 때문에 이 커넥션 절차는 언제나 메인 액티비티 스레드와 독립된 별개의 스레드에서 수행되어야만 한다.
주: connect()를 호출할 때 디바이스는 언제나 디바이스 discovery를 수행하고 있지 않는지 확인해야만 한다. Discovery가 진행중이면 커넥션 시도는 확연히 느려져서 실패할 가능성이 커진다.

예제) Bluetooth 커넥션을 시작하는 스레드

private class ConnectThread extends Thread {
private final BluetoothSocket mmSocket;
private final BluetoothDevice mmDevice;
public ConnectThread(BluetoothDevice device) {
// Use a temporary object that is later assigned to mmSocket,
// because mmSocket is final
BluetoothSocket tmp = null;
mmDevice = device;
// Get a BluetoothSocket to connect with the given BluetoothDevice
try {
// MY_UUID is the app's UUID string, also used by the server code
tmp = device.createRfcommSocketToServiceRecord(MY_UUID);
} catch (IOException e) { }
mmSocket = tmp;
}

public void run() {
// Cancel discovery because it will slow down the connection
mAdapter.cancelDiscovery();
try {
// Connect the device through the socket. This will block
// until it succeeds or throws an exception
mmSocket.connect();
} catch (IOException connectException) {
// Unable to connect; close the socket and get out
try {
mmSocket.close();
} catch (IOException closeException) { }
return;
}
// Do work to manage the connection (in a separate thread)
manageConnectedSocket(mmSocket);
}
/** Will cancel an in-progress connection, and close the socket */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}

cancelDiscovery()는 커넥션이 만들어지기 전에 호출되는걸 볼 수 있다. 커넥션이 되기 전에라도 언제나 호출할 수 있고 실제적으로 실행 여부를 확인하지 않고 호출해도 안전하다. (하지만 그래도 상태를 확인하고 싶으면 isDiscovering()을 사용하면 된다.) manageConnectedSocket()은 데이터 전송을 위한 스레드를 시작하는 어플리케이션에 있는 fictional 메소드이다.
BluetoothSocket이 끝나면 clean up을 위해 언제나 close()를 호출해줘야 한다. 이 메소드를 호출해 줌으로서 곧바로 커넥션 된 소켓을 닫고 내부 리소스를 clean up 하게 된다.

- 연결 관리

두 디바이스를 성공적으로 커넥션하게 되면 각 디바이스는 커넥션 된 BluetoothSocket을 가지게 된다. 이 소켓을 통해 디바이스간에 데이터를 교환할 수 있게 된다. BluetoothSocket을 사용해서 임의의 데이터를 전송하기 위한 일반적 절차는 매우 간단하다.

1.각각 getInputStream()과 getOutputStream()을 사용해 소켓을 통한 전송을 처리할 InputStream과 OutputStream을 얻는다.
2.read(byte[])와 write(byte[])를 사용해서 데이터를 읽고 쓴다.

물론 implementation을 위해 고려해야 세부사항들이 있다. 먼저 무엇보다 모든 읽고 쓰기를 위한 별도의 스레드를 사용해야 한다. 이건 read(byte[])와 write(byte[])는 모두 블럭킹 호출이기 때문에 매우 중요하다. read(byte[])는 스트림에서 무언가 읽을게 있을때까지 블럭되어 있는다. write(byte[])는 일반적으로는 블럭되지 않지만 원격 디바이스가 충분히 빠르게 read(byte[])를 호출하지 않아 버퍼가 꽉 차는 경우 플로우 컨트롤을 위해 블럭될수도 있다. 그러므로 스레드의 메인 루프는 InputStream으로부터 읽기 전용으로 사용되어야 한다. 스레드의 분리된 public 메소드가 OutputStream으로 쓰기를 시작하도록 사용될 있다.

예제)
private class ConnectedThread extends Thread {
private final BluetoothSocket mmSocket;
private final InputStream mmInStream;
private final OutputStream mmOutStream;

public ConnectedThread(BluetoothSocket socket) {
mmSocket = socket;
InputStream tmpIn = null;
OutputStream tmpOut = null;
// Get the input and output streams, using temp objects because
// member streams are final
try {
tmpIn = socket.getInputStream();
tmpOut = socket.getOutputStream();
} catch (IOException e) { }
mmInStream = tmpIn;
mmOutStream = tmpOut;
}

public void run() {
byte[] buffer = new byte[1024]; // buffer store for the stream
int bytes; // bytes returned from read()
// Keep listening to the InputStream until an exception occurs
while (true) {
try {
// Read from the InputStream
bytes = mmInStream.read(buffer);
// Send the obtained bytes to the UI Activity
mHandler.obtainMessage(MESSAGE_READ, bytes, -1, buffer)
.sendToTarget();
} catch (IOException e) {
break;
}
}
}
/* Call this from the main Activity to send data to the remote device */
public void write(byte[] bytes) {
try {
mmOutStream.write(bytes);
} catch (IOException e) { }
}

/* Call this from the main Activity to shutdown the connection */
public void cancel() {
try {
mmSocket.close();
} catch (IOException e) { }
}
}

컨스트럭터가 필요한 스트림을 얻고 한번 실행되면 스레드는 InputStream을 통해 들어오는 데이터를 기다린다. read(byte[])가 스트림에서의 데이터를 리턴하면 그 데이터는 부모 클래스의 Handler 멤버를 사용해 메인 액티비티로 보내진다. 그리고 다시 스트림에서 데이터를 읽기 위해 기다리기 위해 돌아간다. Outgoing 데이터를 보내는건 단순히 메인 액티비티에서 스레드의 write() 메소드를 호출해 전송할 데이터를 전달해주면 된다.

스레드의 cancel() 메소드는 아무때나 BluetoothSocket을 닫아 connection을 멈출 수 있기 때문에 중요하다. 메소드는 블루투스 connection 사용이 끝나면 언제나 호출되어야 한다.

저작자 표시 비영리 변경 금지

'JAVA PROGRAMMING > Android' 카테고리의 다른 글

블루투스 프로그래밍  (0) 2011/09/12
Intent Filter 란?  (0) 2010/02/24

Intent Filter 란?

JAVA PROGRAMMING/Android 2010/02/24 19:09 by 킨테리 KinTeL


원문 바로가기-->[ http://androidhuman.tistory.com/entry/내가-누군지-말해줘-인텐트-필터-Intent-Filter ]
암시적 인텐트(Implicit Intent)를 처리하려면 인텐트 객체에 들어 있는 여러 정보를 이용해야 한다는 것을 앞에서 알아보았습니다. 인텐트 객체 내의 정보들을 통해 호출해야 할 컴포넌트의 정보를 알았으니, 그 다음엔 이 정보를 바탕으로 각 컴포넌트별로 비교를 해보며 적절한 컴포넌트를 찾아야 합니다.

이렇게 인텐트 객체 내의 여러 정보들을 바탕으로 가장 적절한 컴포넌트를 찾는 과정을 인텐트 해석(Intent Resolving)이라 합니다.  이 과정에서 인텐트 객체의 정보와 각 컴포넌트의 정보를 비교하게 되며, 이을 위해 각 컴포넌트는 자신이 받을 수 있는 인텐트의 종류를 메니페스트 파일에 정의합니다. 이를 인텐트 필터(Intent Filter)라 합니다,
 
안드로이드 시스템 내부에서는 수많은 어플리케이션들에 의해 수많은 인텐트들이 발생합니다. 이 중에서 자신에게 필요햔 인텐트만을 받기 위해 인텐트 필터가 있는 것이죠.



하지만, 인텐트 필터가 없다고 해서 인텐트를 받을 수 없는 것은 아닙니다. 인텐트 필터는 인텐트 해석 과정이 필요한 암시적 인텐트(Implicit Intent)를 받을 때만 필요할 뿐, 호출 대상 컴포넌트가 명시되어있는 명시적 인텐트(Explicit Intent)는 이러한 인텐트 필터가 없어도 대상 컴포넌트를 호출할 수 있습니다.

- 어플리케이션간 경계가 없는 안드로이드

명시적 인텐트를 사용하면 굳이 인텐트 필터를 정의할 필요도 없고, 간단히 컴포넌트를 호출할 수 있는데 왜 암시적 인텐트가 있는 것일까요? 그 이유는 바로 "명시적" 인텐트의 이름에서도 알 수 있듯이, 명시적 인텐트는 호출 대상 컴포넌트의 이름을 정확히 알아야 하기 때문입니다.

안드로이드가 다른 시스템과 가장 다른 점이 바로 "어플리케이션 간에 경계가 없다"는 것입니다. 이는, 서로 다른 어플리케이션 내의 컴포넌트라 할지라도 사용자가 사용하는 입장에서 볼 때에는 마치 하나의 어플리케이션처럼 동작하는 것이죠. 

 
")//]]>

위의 영상에서는 바코드 스캐너와 책 관리 어플리케이션이 예로 나옵니다. 바코드 스캐너 어플리케이션은 책 관리 어플리케이션 내에 포함되어 있지 않지만, 마치 책 관리 어플리케이션의 일부인 것처럼 사용하는 것을 확인할 수 있습니다. 이런 식으로, 자신이 필요하다면 다른 사람이 만든 어플리케이션 내의 컴포넌트를 사용할 수 있으므로 개발 시간을 줄여줄 뿐 아니라, 똑같은 걸 다시 만들 필요가 없으니 효율도 증가하겠죠?

- 왜 암시적 인텐트를 사용하는가?

자, 이제 본격적인 문제는 여기서부터입니다. 자신이 만든 컴포넌트라면 컴포넌트의 이름을 정확하게 알 수 있겠지요. 하지만, 다른 사람이 만든 컴포넌트의 이름을 알기란 어렵습니다. 즉, 명시적 인텐트를 사용하여 해당 컴포넌트를 호출할 수가 없죠. 따라서 암시적 인텐트를 통해 컴포넌트를 호출하게 됩니다.

암시적 인텐트를 사용하는 이유는 이 뿐만이 아닙니다. 안드로이드가 다른 시스템과 다른 것 중 또 하나는 바로 Native 어플리케이션 (Home, Dialer 등...)까지도 사용자가 별도로 제작한 것으로 대체할 수 있다는 것입니다.

Native 어플리케이션을 호출할 때 만약 명시적 인텐트를 사용한다면 사용자가 만든 어플리케이션으로 Native 어플리케이션을 대체하는 것은 불가능하겠지요? 하지만, 암시적 인텐트를 사용하여 호출 대상 컴포넌트가 처리해야 할 작업만을 명시한다면 특정 작업의 처리를 꼭 하나의 어플리케이션만 하는 것이 아니라, 여러 가지의 어플리케이션을 통해 할 수 있게 됩니다. 마치 우리가 그림 파일을 편집할 때 포토샵을 쓰는 사람도 있고, 페인트샵을 쓰는 사람도 있는 것과 마찬가지죠.

- 인텐트 필터의 구성요소

인텐트 필터는 인텐트 객체 내의 정보들을 바탕으로 인텐트를 필터링하기에 인텐트 객체 내의 정보들을 바탕으로 자신이 받을 수 있는 정보들을 정의하며. 이 정보들 중 인텐트 필터에서 주로 필터링하는 항목은 action, data (데이터 주소 유형 및 데이터 타입), category입니다.

이러한 인텐트 필터의 내용들을 어플리케이션이 실행되기 전에 안드로이드 시스템에서 알고 있어야 다른 어플리케이션에서 해당 어플리케이션의 컴포넌트를 필요로 하는 인텐트를 발생시켜도 해당 컴포넌트를 실행시킬 수 있습니다. 때문에, 이러한 인텐트 필터의 내용들은 메니페스트 파일인 AndroidManfest.xml 파일의 각 컴포넌트 태그 내에 정의됩니다.

1.<activity android:name=".LifeCycleTester"
2.          android:label="@string/app_name">
3.    <intent-filter>
4.        <action android:name="android.intent.action.MAIN" />
5.        <category android:name="android.intent.category.LAUNCHER" />
6.    </intent-filter>

지금부터는 인텐트 필터 내의 각 속성들에 대해 하나씩 알아보도록 하겠습니다.

1. action (액션) 필터

인텐트 객체 내의 action을 검사하여 인텐트 필터에 정의된 액션과 일치하는지 여부를 검사합니다. 이 검사를 통과하려면 인텐트 객체 내의 액션이 인텐트 필터에 정의된 액션과 일치해야 합니다. 단, 인텐트에 액션이 아예 정의되어있지 않은 경우에는 액션 필터를 통과할 수 있습니다.


위에서 보면 인텐트 객체에 정의된 액션의 이름과 인텐트 필터에 정의된 액션의 이름이 조금 다른데, 사실 표현 방식만 다르지 결국은 똑같은 값입니다. Intent 클래스의 API를 보면, Intent 객체 내의 상수로 정의된 ACTION_VIEW 의 실제 값은 android.intent.action.VIEW 인 것을 확인할 수 있습니다.


2. category (카테고리) 필터

인텐트 객체 내의 category 항목을 검사하여 인텐트 필터에 정의된 카테고리와 일치하는지 여부를 검사합니다. 액션 검사에서는 인텐트 객체 내에 아예 액션이 정의되어있지 않은 경우 액션 검사를 통과할 수 있었던 것에 반해 카테고리 검사는 인텐트 객체에 정의된 카테고리가 인텐트 필터에 정의된 카테고리들과 일치해야 합니다.

예를 들면, 인텐트 필터에서 CATEGORY_DEFAULT와 com.androidhuman.TEST_CATEGORY가 정의되어 있을 경우 카테고리로 CATEGORY_DEFAULT를 가지는 인텐트 객체, com.androidhuman.TEST_CATEGORY를 가지는 객체, 혹은 둘 다 가지고 있는 인텐트 객체가 카테고리 검사를 통과할 수 있습니다. 하지만, CATEGORY_DEFAULT는 가지고 있는데 com.androidhuman.NEW_CATEGORY를 가지는 인텐트 객체의 경우 인텐트 필터에서 NEW_CATEGORY가 정의되어있지 않으므로 카테고리 검사를 통과할 수 없습니다.

이 때문에, 암시적 인텐트를 만들 때 카테고리를 추가하지 않을 경우 안드로이드에서 자동으로 CATEGORY_DEFAULT(android.intent.category.DEFAULT)를 추가해주게 됩니다. 때문에 카테고리를 특별히 추가해주지 않은 인텐트를 받을 수 있게 하려면 카테고리 필터에 android.intent.category.DEFAULT 를 추가해줘야 합니다. 그렇지 않으면 어떠한 암시적 인텐트도 받을 수 없게 됩니다.



3. data (데이터) 필터

인텐트 객체 내의 data 항목 및 type을 검사하여 인텐트 필터에 정의된 값과 비교하여 일치 여부를 검사합니다.
데이터 검사는 크게 데이터의 주소(URI)를 검사하는 부분과 데이터의 유형(type, MIME type)을 검사하는 부분으로 나누어집니다.

데이터의 주소를 검사하는 부분은 데이터의 주소를 세분화하여 검사할 수 있도록 되어있습니다. URI(Uniform Resource Identifier)는 다음과 같은 구조로 구성되어있습니다.

scheme://host:port/path

만약, http://google.com 을 각 요소별로 나누어본다면 schemehttp, host google.com 이 되겠죠? 이와 마찬가지로 안드로이드 시스템에서 사용하는 주소들도 각각의 요소들로 나눌 수 있습니다.
예를 들어 content://com.androidhuman.provider.SimpleMemo/memos/1 같은 주소의 경우 schemecontent, hostcom.androidhuman.provider.SimpleMemo, pathmemos/1 이 되겠지요??
데이터의 유형을 필터링하는 것은 type(mimeType)을 이용합니다. 보통 아래와 같이 정의됩니다.

<data android:mimeType = "video/mpeg" android:scheme = "http">
<data android:mimeType = "audio/*" android:scheme = "http">

mimeType은 위와 같은 형식으로 정의되며, 큰 범주/큰 범주의 하위 범주 형식으로 정의됩니다. (video/mpeg의 경우 큰 범주가 video, video의 하위 범주가 mpeg) 하위 범주에는 와일드카드 문자(*) 를 사용하여 해당 범주 내의 모든 형식을 허용할 수도 있습니다.

이런 방식으로 첫번째 <data> 필터를 해석하면 "http 스키마를 가진 mpeg 형식의 비디오 데이터"를 가진 인텐트를 허용하는 것임을 알 수 있고, 두번째 필터는 "http 스키마를 가진 모든 오디오 데이터"를 가진 인텐트를 허용하는 것임을 알 수 있습니다.


지금까지 인텐트 객체 및 인텐트 필터의 각 필터들 및 인텐트 필터의 통과 조건들에 대해 알아보았습니다. 다음 강좌에서는 NotePad 예제에 정의되어있는 인텐트 필터들을 보고 해석하면서 오늘 배운 내용들을 다시 한번 정리해보도록 하겠습니다. :)
저작자 표시 비영리 변경 금지

'JAVA PROGRAMMING > Android' 카테고리의 다른 글

블루투스 프로그래밍  (0) 2011/09/12
Intent Filter 란?  (0) 2010/02/24

12.3 Externalizable

JAVA PROGRAMMING/JAVA 2010/02/21 22:10 by 킨테리 KinTeL



12.3.1 Externalizable



객체 직렬화의 또 다른 방법으로는 Externalizable 인터페이스를 사용할 수 있습니다. 그 기본 개념은 Serializable 인터페이스와 같습니다. 그것도 그럴 것이 Externalizable 자체가 Serializable 인터페이스를 상속한 인터페이스이기 때문입니다. 

▣ 인터페이스끼리의 상속
◈ 인터페이스가 어떻게 상속이 되느냐고 말하는 분도 있겠지만, 인터페이스는 인터페이스끼리 상속의 개념이 적용됩니다. 인터페이스끼리 상속해서 더 큰 인터페이스를 만들게 됩니다.

▣ 객체 직렬화를 위한 Externalizable 인터페이스
◈ public interface Externalizable extends Serializable {
◈         public void writeExternal(ObjectOutput out) throws IOException;
◈         public void readExternal(ObjectInput in)     
◈             throws IOException, ClassNotFoundException;
◈ }

Externalizable 인터페이스는 Serializable 인터페이스보다 약간 복잡합니다. Serializable은 메서드를 포함하지 않은 표시 인터페이스이기 때문에 implements Serializable만 붙여주면 되지만, Externalizable 인터페이스는 2개의 메서드를 포함하고 있기 때문에 2개의 메서드를 구현해야만 직렬화가 가능합니다. 보통의 경우 Serializable 인터페이스보다 미세한 직렬화를 다루기 위해서 사용합니다.

▣ Externalizable의 구현
◈ 사용자가 직접 객체의 직렬화를 위한 구현을 해야 한다.

Serializable에서는 자동으로 데이터가 기록되지만 Externalizable에서는 2개의 메서드를 구현해야 하며, 이 두 개의 메서드에 데이터를 읽어오고 기록하는 구현을 사용자가 직접 하게 됩니다. 즉 객체를 저장하고 읽어오는 부분을 직접 제어하겠다는 것입니다. 이 때 기록하는 부분은 writeExternal()를 사용하고, 읽어내는 부분은 readExternal()을 사용합니다. 

▣ Externalizable 인터페이스
◈ writeExternal() 메서드 : 기록하는 부분을 제어
◈ readExternal() 메서드 : 읽어내는 부분을 제어

말로만 해서 무슨 소용이 있겠습니까! 직접 예제를 만들어서 확인해 보도록 하겠습니다. 다음은 Serializable 대신에 Externalizable을 구현한 예입니다.

『chap12\ExternalObject.java』
ⓙ───────────────────────────────────────
/**
Externalizable을 구현한 예
**/
import java.io.*;
public class ExternalObject implements Externalizable {
    private int id; //부서
    private String name; //이름
    private float height; //신장
    public ExternalObject(){}
    public ExternalObject(int id, String name, float height) {
        this.id = id;
        this.name = name;
        this.height = height;
    }
    public void readExternal(ObjectInput oi) 
            throws IOException, ClassNotFoundException {
        System.out.println("readExternal() 메서드 호출");
        id = oi.readInt();
        name = (String)oi.readObject();
        height = oi.readFloat();
    }
    public void writeExternal(ObjectOutput oo) throws IOException {
        System.out.println("writeExternal() 메서드 호출");
        oo.writeInt(id);
        oo.writeObject(name);
        oo.writeFloat(height);
    }
    public String toString(){
        return id + ":" + name + ":" + height;
    }
} //end of ExternalObject class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac ExternalObject.java
***/
───────────────────────────────────────ⓑ

Externalizable 인터페이스를 구현하는 순서는 다음과 같습니다. 

▣ Externalizable 인터페이스를 구현하는 순서
◈ 1. implements Externalizable
◈ 2. public void writeExternal(ObjectOutput oo) 메서드 구현
◈ 3. public void readExternal(ObjectInput oi) 메서드 구현
◈ 4. 매개변수 없는 생성자 구현(반드시)

이러한 절차에서 구현한 위의 예제를 보시면 implements Externalizable부터 붙인 후에 readExternal()과 writeExternal()를 구현하고 있습니다. 이들 메서드의 내부를 한번 보도록 하겠습니다. 먼저 writeExternal()부터 보도록 하겠습니다.

▣ writeExternal()의 구현
◈ public void writeExternal(ObjectOutput oo) throws IOException {
◈         System.out.println("writeExternal() 메서드 호출");
◈         oo.writeInt(id);
◈         oo.writeObject(name);
◈         oo.writeFloat(height);
◈ }

이 부분은 매개변수로 넘어오는 ObjectOutput의 객체 oo를 이용해서 기록하고 싶은 데이터를 차례대로 기록해 주고 있습니다. 물론 여러분들이 필요로 하는 대부분의 writeXXX() 메서드는 이미 존재합니다. 여기서는 간단히 int, String, float만을 기록하였지만 ObjectOutput 인터페이스가 제공해 주는 writeBoolean(), writeByte(), writeBytes(), writeChar(), writeChars(), writeDouble(), writeFloat(), writeInt(), writeLong(), writeShort(), writeUTF() 등을 전부 사용할 수 있습니다.

그리고 이것을 다시 읽어오는 방법은 기록한 차례대로 읽어들이면 됩니다. readExternal() 내부는 다음과 같습니다. 

▣ readExternal()의 구현
◈ public void readExternal(ObjectInput oi) throws IOException, ClassNotFoundException {
◈         id = oi.readInt();
◈         name = (String)oi.readObject();
◈         height = oi.readFloat();
◈ }

정확하게 순서를 맞추어 호출해 주어야 합니다. 만약 여러분들이 이것의 순서를 바꾼다면 에러를 만나게 될 것입니다. 

▣ writeExternal()과 readExternal()의 데이터를 기록하고 읽는 순서
◈ writeExternal()로 데이터가 기록된 정확한 순서대로 readExternal()를 이용해서 읽어내야 한다.

그리고 마지막으로 직렬화된 데이터를 읽어들여서 객체를 만들기 위해 인자 없는 생성자가 필요합니다. 이것을 만들어 주지 않으면 에러 메시지에서 인자 없는 생성자를 요구할 것입니다.

▣ 매개변수 없는 생성자
◈ public ExternalObject(){}

▣ 디폴트 생성자를 주석 처리한 후 실행한 결과
◈ writeExternal() 메서드 호출
◈ writeExternal() 메서드 호출
◈ writeExternal() 메서드 호출
◈ Exception in thread "main" java.io.InvalidClassException: ExternalObject; no valid constructor
◈         at ObjectStreamClass.<init>(ObjectStreamClass.java:375)
◈         at ObjectStreamClass.lookup(ObjectStreamClass.java:249)
◈         at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1010)
◈         at ObjectOutputStream.writeObject(ObjectOutputStream.java:278)
◈         at ExternalObjectMain.main(ExternalObjectMain.java:9)

위의 순서대로 예제를 구현하면 다음과 같습니다.

『chap12\ExternalObjectMain.java』
ⓙ───────────────────────────────────────
/**
Externalizable을 이용한 직렬화를 테스트하는 예
**/
import java.io.*; 
public class ExternalObjectMain{        
    public static void main(String[] args) 
            throws IOException, ClassNotFoundException{
        FileOutputStream fos = new FileOutputStream("external.dat"); 
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        ExternalObject so1 = new ExternalObject(1, "홍길동", 170.25f);
        ExternalObject so2 = new ExternalObject(2, "김삿갓", 190.01f);
        ExternalObject so3 = new ExternalObject(3, "암행어", 180.34f);
        oos.writeObject(so1);
        oos.writeObject(so2);
        oos.writeObject(so3);
        oos.close();

        FileInputStream fis = new FileInputStream("external.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        ExternalObject rso1 = (ExternalObject)ois.readObject();
        ExternalObject rso2 = (ExternalObject)ois.readObject();
        ExternalObject rso3 = (ExternalObject)ois.readObject();
        System.out.println(rso1.toString());
        System.out.println(rso2.toString());
        System.out.println(rso3.toString());
        ois.close();
    } //end of main
} //end of ExternalObjectMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac ExternalObjectMain.java
C:\javasrc\chap12>java ExternalObjectMain
writeExternal() 메서드 호출
writeExternal() 메서드 호출
writeExternal() 메서드 호출
readExternal() 메서드 호출
readExternal() 메서드 호출
readExternal() 메서드 호출
1:홍길동:170.25
2:김삿갓:190.01
3:암행어:180.34
***/
───────────────────────────────────────ⓑ

Externalizable 인터페이스를 테스트하는 예제는 일반 Serializable 인터페이스를 테스트하는 예제와 다른 것이 없습니다. 다만 직렬화 객체를 Externalizable 인터페이스로 구현했을 뿐입니다. 이로써 여러분은 직렬화의 두 가지 사용법인 Serializable과 Externalizable 인터페이스를 이용하는 방법을 모두 알아보았습니다.
저작자 표시 비영리 변경 금지

'JAVA PROGRAMMING > JAVA' 카테고리의 다른 글

12.3 Externalizable  (0) 2010/02/21
12.2 Serializable  (0) 2010/02/21
12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21

12.2 Serializable

JAVA PROGRAMMING/JAVA 2010/02/21 22:10 by 킨테리 KinTeL



12.2.1 Serializable



앞에서는 객체 직렬화의 기본적인 개념과 간단한 원리를 알아보았습니다. 이번 절에서는 ObjectInputStream과 ObjectOutputStream을 사용해서 실제로 객체를 파일에 저장하고, 다시 객체를 복원하는 방법에 대해 알아보도록 하겠습니다. 그리고 transient를 사용해서 특정 멤버 필드가 직렬화되는 것을 방지하는 방법에 대해 살펴보겠습니다.

▣ 직렬화의 학습내용
◈ ObjectInputStream과 ObjectOutputStream을 이용한 직렬화
◈ transient를 이용한 직렬화 방지
◈ 직렬화 불가능한 경우

먼저 Serializable을 이용한 객체 직렬화의 구현 방법에 대해서 알아보도록 하겠습니다. Serializable의 구현은 일반 클래스에 implements Serializable만 붙여주면 됩니다. 이 인터페이스가 구현되면 객체의 저장이나 복원은 Object 스트림이 알아서 처리합니다.

제일 먼저 여러분은 Serializable을 구현한 클래스가 필요합니다. 여러분은 implements Serializable을 구현한 후 해당 클래스의 객체를 생성할 것입니다. 그리고 생성된 객체를 직렬화하려 할 것입니다.

▣ Serializable의 구현과 직렬화될 객체 생성
◈ public class SerialObject implements Serializable{
◈         //...
◈ }
◈ SerialObject so = new SerialObject(); //직렬화할 객체

Serializable이 구현된 클래스의 객체를 직렬화하기 위해서는 직렬화할 목표지점이 필요합니다. 직렬화를 위한 목표지점은 파일이 될 수도 있으며, 네트웍의 특정지점이 될 수도 있습니다. 현재는 파일 스트림에 객체를 저장하도록 하겠습니다. 그렇다면 다음과 같이 먼저 File 스트림을 생성해야 합니다. 

▣ 직렬화를 위한 FileOutputStream 생성
◈ FileOutuputStream fos = new FileOutputStream("serialobject.dat");

이렇게 생성된 스트림은 다시 Object 스트림으로 변환해야 합니다. 9장의 Java Stream에서 Object 스트림은 2차 스트림이라고 배웠으며, ObjectInputStream과 ObjectOutputStream 자체가 바이트 단위의 스트림이기 때문에 다음과 같이 FileOutputStream은 ObjectOutputStream으로 쉽게 변환될 수 있습니다.

▣ 직렬화를 위한 ObjectOutputStream 생성
◈ FileOutuputStream fos = new FileOutputStream("serialobject.dat");
◈ ObjectOutputStream oos = new ObjectOutputStream(fos);

최종적으로 ObjectOutputStream이 만들어졌다면 serialobject.dat 파일로 객체를 저장하면 됩니다. 이 때 ObjectOutputStream에서는 writeObejct(Object obj)를 이용해서 객체를 저장하게 됩니다.

▣ 객체 직렬화(Serialization)
◈ SerialObject so = new SerialObject(); //직렬화 객체의 생성
◈ FileOutuputStream fos = new FileOutputStream("serialobject.dat"); //파일 스트림 생성
◈ ObjectOutputStream oos = new ObjectOutputStream(fos); //객체 스트림으로 변환
◈ oos.writeObject(so); //객체 저장
◈ oos.close(); //스트림 닫기

물론 이 반대의 경우도 존재할 수 있습니다. 위의 serialobject.dat라는 파일에 저장된 객체를 읽어들인다고 가정하죠. 그러면 serialobject.dat에 FileInputStream을 생성한 후 ObjectInputStream으로 변환하고, readObject() 메서드를 이용해서 저장된 객체를 읽어들이면 됩니다. 

▣ 객체 역직렬화(Deserialization) 
◈ FileInputStream fis = new FileInputStream("serialobject.dat"); //파일 스트림 생성
◈ ObjectInputStream ois = new ObjectInputStream(fis); //객체 스트림으로 변환 
◈ Object obj = ois.readObject(); //객체 읽기
◈ SerialObject temp = (SerialObject)obj; //형복원
◈ ois.close(); //스트림 닫기

객체를 복원해서 읽을 들일 때 사용하는 readObject()의 리턴형이 Object형이기 때문에 다운캐스팅이 필요합니다. 이러한 일련의 과정을 순서대로 정리하면 다음과 같습니다.

▣ 객체 직렬화 과정
◈ 목표지점에 출력 스트림을 생성한다.
◈ 생성된 스트림을 Object 출력 스트림으로 변환한다.(ObjectOutputStream)
◈ 직렬화된 객체를 객체스트림을 통해서 전송하거나 읽어낸다.
◈ 예) ObjectOutputStream -> writeObject(Object obj)
◈ 스트림을 닫는다.

▣ 객체 역직렬화 과정
◈ 목표지점에 입력 스트림을 생성한다.
◈ 생성된 스트림을 Object 입력 스트림으로 변환한다.(ObjectInputStream)
◈ 객체 스트림을 통해서 직렬화된 객체를 읽어낸다.
◈ 예) ObjectInputStream -> readObject()
◈ 스트림을 닫는다.

▣ writeObject()와 readObject()    
◈ writeObject()는 객체를 스트림에 기록하는데 사용하고, readObject()는 스트림으로부터 객체를 복원할 때 사용한다.

▣ Serializable 인터페이스    
◈ 표시 인터페이스이기 때문에 구현해야 할 메서드가 없다. 이 인터페이스는 단지 붙여만 주면 된다.



12.2.2 객체 스트림



객체 스트림은 자바 IO에서 제공해 주기 때문에 여러분은 직렬화할 객체만 생각하면 됩니다. 자! 그렇다면 앞에서 배운 순서대로 객체 직렬화를 구현해 보도록 하겠습니다. 여러분이 할 일은 직렬화의 대상이 되는 클래스에 impelements Serializable만 붙여주면 됩니다.

『chap12\SerialObject.java』
ⓙ───────────────────────────────────────
/**
Serializable을 이용한 객체 직렬화의 구현
**/
import java.io.*;
public class SerialObject implements Serializable {
    private String name; // 이름
    private String dept; // 부서
    private String title; // 직책
    public SerialObject (String name, String dept, String title) {
        this.name = name;
        this.dept = dept;
        this.title = title;
    }
    public String toString() {
        return name + ":" + dept + ":" + title;
    }
} //end of SerialObject class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialObject.java
***/
───────────────────────────────────────ⓑ

위의 예제는 너무나 단순한 예제입니다. 단지 implements Serializable을 붙였다는 것밖에는 일반 클래스와 다른 점이 없습니다. 

자! 이제 임의의 파일에 객체 스트림을 연결해서 객체를 읽고 기록해 보도록 하겠습니다. 예리한 분들은 '직렬화될 클래스의 멤버 변수들이 모두 private으로 선언되었는데, 이것은 상관없을까?' 하는 의구심이 생길 것입니다. 직렬화는 접근지정자에 상관 없이 수행된답니다. 그러니 걱정하지 마세요. 아래의 예제는 객체를 기록한 후, 다시 읽어내는 예를 보여주고 있습니다.

『chap12\SerialObjectMain.java』
ⓙ───────────────────────────────────────
/**
SerialObject를 이용해서 객체를 기록한 후 다시 읽어내는 예
**/
import java.io.*; 
public class SerialObjectMain {
    public static void main(String[] args) throws Exception {
        //직렬화를 이용한 객체 저장
        FileOutputStream fos = new FileOutputStream("serialobject.dat"); 
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        SerialObject so1 = new SerialObject("홍길동", "총무부", "과장");
        SerialObject so2 = new SerialObject("김삿갓", "영업부", "과장");
        SerialObject so3 = new SerialObject("암행어", "인사부", "과장");
        oos.writeObject(so1);
        oos.writeObject(so2);
        oos.writeObject(so3);
        oos.close();
        //직렬화를 이용한 객체 복원
        FileInputStream fis = new FileInputStream("serialobject.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerialObject rso1 = (SerialObject)ois.readObject();
        SerialObject rso2 = (SerialObject)ois.readObject();
        SerialObject rso3 = (SerialObject)ois.readObject();
        System.out.println(rso1);
        System.out.println(rso2);
        System.out.println(rso3);
        ois.close();
    } //end of main
} //end of SerialObjectMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialObjectMain.java
C:\javasrc\chap12>java SerialObjectMain
홍길동:총무부:과장
김삿갓:영업부:과장
암행어:인사부:과장
***/
───────────────────────────────────────ⓑ

먼저 serialobject.dat 파일에 File 출력 스트림을 생성합니다. 그리고 이 File 출력 스트림을 다음과 같이 Object 출력 스트림으로 변환합니다. 

▣ ObjectOutputStream 생성
◈ FileOutputStream fos = new FileOutputStream("serialobject.dat"); 
◈ ObjectOutputStream oos = new ObjectOutputStream(fos);

스트림이 개설되었다면 직렬화가 가능한(implements Serializable로 구현된) 객체를 만들어야겠죠. 위의 예제에서는 3개의 객체를 준비했습니다.

▣ 직렬화를 위한 객체 생성
◈ SerialObject so1 = new SerialObject("홍길동", "총무부", "과장");
◈ SerialObject so2 = new SerialObject("김삿갓", "영업부", "과장");
◈ SerialObject so3 = new SerialObject("암행어", "인사부", "과장");

그리고 이 객체들을 serialobject.dat 파일에 기록하는 부분은 다음과 같습니다. 

▣ 스트림을 이용한 객체의 직렬화
◈ oos.writeObject(so1);
◈ oos.writeObject(so2);
◈ oos.writeObject(so3);

객체가 3개이기 때문에 3번 기록해야겠죠. 마지막으로 출력 스트림을 닫습니다. 이 부분까지 수행되면 serialobject.dat가 만들어지고, 객체 3개가 순서대로 기록됩니다. 소스를 간단하게 하기 위해서 저장한 객체를 바로 역직렬화하고 있습니다. 우선, 객체를 읽어내기 위해서 serialobject.dat 파일에 File 입력 스트림을 생성합니다. 그리고 생성된 File 입력 스트림을 Object 입력 스트림으로 변환합니다. 

▣ 역직렬화를 위한 ObjectInputStream의 생성
◈ FileInputStream fis = new FileInputStream("serialobject.dat");
◈ ObjectInputStream ois = new ObjectInputStream(fis);

변환된 Object 입력 스트림으로 객체를 읽어냅니다. 앞에서 3번 저장했으니 3번만 읽어내면 됩니다. 그리고 Object 입력 스트림으로 읽을 때, 반환형이 Object형이기 때문에 여러분은 강제 다운캐스팅시켜서 사용해야 합니다.

▣ ObjectInputStream을 통해서 객체 읽어내기
◈ SerialObject rso1 = (SerialObject)ois.readObject();
◈ SerialObject rso2 = (SerialObject)ois.readObject();
◈ SerialObject rso3 = (SerialObject)ois.readObject();

실제 이 객체들을 이용할 수 있는지 SerialObject 클래스의 멤버 메서드를 호출하는 부분은 다음과 같습니다.

◈ System.out.println(rso1.toString());
◈ System.out.println(rso2.toString());
◈ System.out.println(rso3.toString());

마지막으로 Object 입력 스트림을 닫으시면 모든 작업은 끝이 납니다. 위의 예제에서 ObjectOutputStream과 ObjectInputStream에 대해서 조금 더 알아보도록 하죠.

Object 스트림은 스트림의 한 종류입니다. 객체를 직렬화시켜서 저장하는 스트림이라고 말할 수 있습니다. 객체를 객체 Object 출력 스트림에 기록할 때는 ObjectOutputStream 클래스의 writeObject(Object obj) 메서드를 사용합니다. writeObject 메서드의 원형은 다음과 같다.

▣ writeObject() 메서드의 원형
◈ public final void writeObject(Object obj) throws IOException

writeObject(Object obj) 메서드의 인자로 넘어 온 객체가 Serializable 인터페이스나 Externalizable 인터페이스를 구현했는지 검사합니다. 주어진 객체가 Serializable 인터페이스를 구현했다면, writeObject() 메서드는 자동으로 객체의 상태를 스트림에 기록하게 됩니다. 만약 객체가 Serializable이나 Externalizable 인터페이스 중 어느 것도 구현하지 않았다면, NotSerializableException을 발생시킵니다.

직렬화되어 있는 객체는 ObjectInputStream클래스의 readObject() 메서드를 사용해서 복원할 수 있습니다. readObject의 원형은 다음과 같습니다.

▣ readObject() 메서드의 원형
◈ public final Object readObject() throws OptionalDataException, ClassNotFound Exception, IOException

readObject()는 연결된 스트림으로부터 객체의 상태 정보를 읽어내고, writeObject() 메서드와 반대로 readObject() 메서드는 스트림에 기록되어 있는 객체의 상태 정보를 기반으로 원래의 객체로 복원해 줍니다.



12.2.3 transient



객체 스트림을 이용해서 직렬화할 때 객체의 모든 상태정보 즉 멤버 필드를 직렬화하게 됩니다. 하지만 클래스를 디자인하다 보면 순간적으로 사용하고 버려야 하는 필요 없는 정보도 있습니다. 이러한 정보를 제외시키기 위해서 transient 키워드를 사용합니다.

클래스를 만들다 보면 중요하지는 않지만, 클래스 내에서 전역 변수로 사용하기 위해서 어쩔 수 없이 멤버 변수로 만드는 경우가 있습니다. 여러분도 클래스를 웬만큼 만들어 보았다면 이런 경험은 흔할 것입니다. 이러한 정보까지 직렬화할 필요는 없습니다. 이 때 저장할 필요가 없다고 생각된다면 접근지정자 다음에 transient를 붙이면 직렬화에서 제외됩니다.

▣ transient 키워드
◈ 멤버 변수를 직렬화의 대상에서 제외할 때 사용하는 키워드
◈ 멤버 변수 앞에 transient를 명시

transient는 해당 멤버 변수를 직렬화에서 제외시키기 때문에 역직렬화했을 때 멤버 변수의 값은 무조건 디폴트 값으로 초기화됩니다. 객체의 경우 디폴트로 null을, int는 0으로 설정하게 됩니다. 아래의 예제는 transient의 예를 보여주고 있습니다.

『chap12\TransientObjectMain.java』
ⓙ───────────────────────────────────────
/**
transient를 이용해서 멤버 변수를 직렬화에서 제외시키기
**/
import java.io.*;
class TransientObject implements Serializable {
    private String name;        // 이름
    private String dept;        // 부서
    private transient String title;        // 직책
    public TransientObject (String name, String dept, String title) {
        this.name = name;
        this.dept = dept;
        this.title = title;
    }
    public String toString() {
        return name + ":" + dept + ":" + title;
    }
} //end of TransientObject class

public class TransientObjectMain{        
    public static void main(String[] args) throws Exception {
        FileOutputStream fos = new FileOutputStream("serialobject2.dat"); 
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        TransientObject so1 = new TransientObject("홍길동", "총무부", "과장");
        TransientObject so2 = new TransientObject("김삿갓", "영업부", "과장");
        TransientObject so3 = new TransientObject("암행어", "인사부", "과장");
        oos.writeObject(so1);
        oos.writeObject(so2);
        oos.writeObject(so3);
        oos.close();
        FileInputStream fis = new FileInputStream("serialobject2.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        TransientObject rso1 = (TransientObject)ois.readObject();
        TransientObject rso2 = (TransientObject)ois.readObject();
        TransientObject rso3 = (TransientObject)ois.readObject();
        System.out.println(rso1.toString());
        System.out.println(rso2.toString());
        System.out.println(rso3.toString());
        ois.close();
    } //end of main
} //end of TransientObjectMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac TransientObjectMain.java
C:\javasrc\chap12>java TransientObjectMain
홍길동:총무부:null
김삿갓:영업부:null
암행어:인사부:null
***/
───────────────────────────────────────ⓑ

TransientObject 클래스는 Serializable을 구현하였으며, 멤버 변수 title은 transient로 명시하였습니다.

▣ 멤버 변수에 transient의 사용
◈ private transient String title;

직렬화할 때 String title 부분을 제외했기 때문에 역직렬화한 후 객체를 복원했을 때 title 부분이 null로 출력되고 있습니다. 

◈ 홍길동:총무부:null
◈ 김삿갓:영업부:null
◈ 암행어:인사부:null

transient는 복잡한 개념이 아닙니다. 단지 객체를 직렬화하는데 제외하겠다는 의미 이외에는 별다른 뜻은 없습니다. 하지만 프로그램상에서 반드시 저장해야 할 것과 저장하지 말아야 할 것을 구분하는 작업은 전적으로 프로그래머에게 달려 있습니다. 그리고 스태틱 변수는 기본적으로 transient라는 것도 기억해 두시기 바랍니다. 스태틱은 직렬화의 대상에서 자동으로 제외됩니다. 

▣ 직렬화의 조건
◈ 직렬화될 필드는 반드시 non-static, non-transient로 선언되어야만 직렬화할 때 포함된다.

직렬화될 필드는 non-static, non-transient로 선언해야만 직렬화할 때 포함됩니다. 스태틱 변수는 공유 메모리 개념을 가지고 있기 때문에 직렬화에서 제외됩니다.



12.2.4 직렬화되지 않는 경우



라이브러리 차원에서 미리 직렬화가 구현된 클래스들도 있습니다. 그리고 직렬화할 수 없는 클래스들도 존재합니다. 해당 클래스가 직렬화가 가능한지 아닌지를 알기 위해서는 자바 API에서 Serializable 인터페이스가 구현되었는 지를 확인하시면 됩니다. 여러분들이 라이브러리 관련 객체를 직렬화하고 싶다면 자바 API를 참조하셔서, 그 객체가 Serializable 인터페이스를 구현했는지 확인하시고 사용하시길 바랍니다.

직렬화를 구현한다고 해서 모든 클래스들이 직렬화가 가능한 것은 아닙니다. 다음과 같은 경우 직렬화를 할 수 없습니다.

▣ 직렬화가 불가능한 경우
◈ 직렬화가 불가능한 객체를 포함한 경우
◈ 하위 클래스는 직렬화를 구현했지만 상위 클래스에서는 직렬화가 구현되지 않은 상태에서 상위 클래스의 생성자에 매개변수가 있는 경우

특정 클래스의 객체를 포함한 경우 아예 직렬화가 불가능한 경우가 있습니다. 다음은 직렬화할 수 없는 대표적인 클래스들을 보여주고 있습니다. 

▣ 직렬화 불가능한 클래스들
◈ 이벤트 어댑터
◈ 이미지 필터
◈ AWT 클래스
◈ beans
◈ Socket
◈ URLConnection

두 번째의 경우는 하위 클래스는 직렬화를 구현했지만 상위 클래스는 직렬화를 구현하지 않은 상태이며, 그리고 상위 클래스의 생성자에 매개변수가 있는 상태라면 직렬화할 때 문제가 발생할 수 있습니다. 엄밀하게 말하면 역직렬화에 문제가 생길 수 있습니다. 이 경우 직렬화는 가능하지만 역직렬화가 불가능한 상태가 됩니다. 

먼저 직렬화할 수 없는 객체를 포함한 경우부터 보도록 하겠습니다. 다음의 클래스는 Serializable은 구현했지만 멤버 변수로 직렬화 불가능한 Socket 클래스의 객체를 포함하고 있습니다. 그렇기 때문에 직렬화할 때 NotSerializableException이 발생합니다.

『chap12\MyNetwork.java』
ⓙ───────────────────────────────────────
/**
Serializable은 구현했지만 직렬화할 수 없는 객체를 포함한 경우
**/
import java.io.*;
import java.net.*;
public class MyNetwork implements Serializable{
    private Socket socket; //직렬화 불가능한 객체
    public MyNetwork() throws IOException{
        socket = new Socket();
    }
} //end of MyNetwork class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac MyNetwork.java
***/
───────────────────────────────────────ⓑ

『chap12\NotSerialMain.java』
ⓙ───────────────────────────────────────
/**
MyNetwork의 객체를 직렬화할 때 에러발생
**/
import java.io.*;
public class NotSerialMain{
    public static void main(String[] args) 
            throws IOException, ClassNotFoundException{
        MyNetwork m = new MyNetwork();
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(m);
        oos.close();
        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        Object o = ois.readObject();
    } //end of main
} //end of NotSerialMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac NotSerialMain.java
C:\javasrc\chap12>java NotSerialMain
Exception in thread "main" java.io.NotSerializableException: java.net.Socket
    at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1054)
    at ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1332)
    at ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1304)
    at ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1247)
    at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1052)
    at ObjectOutputStream.writeObject(ObjectOutputStream.java:278)
    at NotSerialMain.main(NotSerialMain.java:10)
***/
───────────────────────────────────────ⓑ

위의 에러는 직렬화가 불가능한 객체를 직렬화하려 했기 때문에 발생한 에러입니다. 이 때 직렬화가 불가능한 객체를 transient로 처리해서 직렬화되는 것을 방지하면 에러를 제거할 수 있습니다.

▣ 직렬화가 불가능한 객체를 transient로 처리한 경우
◈ public class MyNetwork implements Serializable{
◈         private transient Socket socket;
◈         public MyNetwork() throws IOException{...}
◈ } 

직렬화할 수 없기 때문에 transient로 직렬화를 못하게 하는 것입니다. 

두 번째의 경우는 직렬화할 때 상위 클래스와 하위 클래스가 존재할 때, 하위 클래스에서만 직렬화를 구현하였다면 직렬화하지 못하는 경우가 발생합니다. 다음과 같은 경우에 직렬화할 수 없는 상황이 발생합니다.

▣ 상속구조에서 직렬화할 수 없는 상황(엄밀하게 말하면 직렬화는 가능하지만 역직렬화가 불가능한 상태)
◈ 상위 클래스의 생성자에 매개변수가 있는 생성자만 존재
◈ 상위 클래스는 Serializable을 구현하지 않은 상태
◈ 하위 클래스는 상위 클래스를 상속받은 뒤 Serializable을 구현한 상태

이와 같은 상태에서 하위 클래스의 객체를 생성한 후 직렬화시키는 것은 가능합니다. 하지만 역직렬화하는 것이 불가능합니다. 이것은 상위 클래스가 직렬화되지 않았기 때문에 상위 클래스의 정보가 없는 상황이 발생합니다. 상위 클래스를 직렬화하지 않으면 상위 클래스의 정보도 직렬화되지 않기 때문에 무조건 디폴트 생성자를 이용하게 됩니다. 이러한 경우를 예제로 만들어 보죠.

『chap12\SerialConsMain.java』
ⓙ───────────────────────────────────────
/**
상속구조에서 직렬화할 수 없는 상황
**/
import java.io.*;
class Parent extends Object{
    public Parent(String str){
        //...
    }
} //end of Parent class

class Child extends Parent implements Serializable{
   public Child(String str) throws IOException {
       super(str);
   }
} //end of Child class

public class SerialConsMain{
   public static void main(String[] args) 
        throws IOException, ClassNotFoundException{
      Child c = new Child("test");
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      ObjectOutputStream oout = new ObjectOutputStream(bout);
      oout.writeObject(c);
      oout.close();
      ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
      ObjectInputStream oin = new ObjectInputStream(bin);
      Object o = oin.readObject();
   } //end of main
} //end of SerialConsMain class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialConsMain.java
C:\javasrc\chap12>java SerialConsMain
Exception in thread "main" java.io.InvalidClassException: 
    Child;    no valid constructor
    at ObjectStreamClass.<init>(ObjectStreamClass.java:379)
    at ObjectStreamClass.lookup(ObjectStreamClass.java:253)
    at ObjectOutputStream.writeObject0(ObjectOutputStream.java:1010)
    at ObjectOutputStream.writeObject(ObjectOutputStream.java:278)
    at SerialConsMain.main(SerialConsMain.java:19)
***/
───────────────────────────────────────ⓑ

위의 예제는 하위 클래스의 객체를 직렬화한 상태에서 다시 역직렬화를 할 때 상위 클래스의 정보가 없기 때문에 역직렬화를 할 수 없는 상황을 예제로 보여주고 있습니다. 위의 문제를 해결하는 것은 아주 간단합니다. 상위 클래스에 implements Serializable을 붙여주면 됩니다. 그렇게 되면 역직렬화할 때 상위 클래스의 정보를 보고 매개변수있는 생성자를 찾아서 역직렬화를 완성하게 되는 것입니다.

▣ 에러 해결책
◈ 상위 클래스에 implements Serializable을 붙여준다.
저작자 표시 비영리 변경 금지

'JAVA PROGRAMMING > JAVA' 카테고리의 다른 글

12.3 Externalizable  (0) 2010/02/21
12.2 Serializable  (0) 2010/02/21
12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21

12.1 Serialization

JAVA PROGRAMMING/JAVA 2010/02/21 22:09 by 킨테리 KinTeL



12.1.1 클래스와 객체의 관계



객체 직렬화를 논하기 전에 우선 클래스와 객체의 관계에 대해서 알아보도록 하죠. 클래스(Class)와 객체(Object)의 관계는 클래스의 기초 개념을 학습하면서 이미 다루었던 내용입니다. 여기서는 약간 다른 측면에서 생각해 보도록 하죠. 

▣ 클래스와 객체의 관계
◈ 클래스의 형정보와 객체의 메모리 사이의 관계

자바 파일을 컴파일한 후 생성된 .class 파일은 클래스의 모든 정보를 담고 있으며, 객체를 생성하기 위해서는 반드시 해당 .class 파일을 로딩해야 합니다. .class 파일은 클래스의 형정보를 담고 있으며, 형정보 없이 객체를 생성하는 방법은 존재하지 않습니다. .class에 포함된 형정보가 로딩되었다면 객체를 생성할 수 있으며 메서드 또한 호출할 수 있습니다. 그렇다면 다음과 같은 질문을 던질 수 있습니다.

▣ 질문
◈ 클래스의 형정보로 만든 객체의 메모리가 어떠한 형태로 되어 있는가?
◈ 메서드를 호출했을 때 객체의 메모리와 형정보가 어떠한 방식으로 동작하는가?

형정보를 이용해서 객체의 메모리를 만들 때 멤버 메서드와는 관계가 없습니다. 일반적으로 멤버 변수의 크기와 객체의 메모리의 크기는 같습니다. 자바에서는 메모리를 직접 접근할 수 없기 때문에 메모리의 크기를 증명할 수는 없지만 ANSI C++에서는 아주 당연한 것입니다. C++에서 객체의 메모리를 계산하기 위해서 sizeof라는 연산자를 제공하고 있습니다. 실제 이것을 테스트하기 위한 간단한 예제를 만들어 보도록 하죠.

『chap12\objtest\objmain.cpp』
ⓙ───────────────────────────────────────
/**
객체의 메모리 크기를 계산하는 예제
**/
#include <stdio.h>
class RefObj{
private:
    int a;
    int b;
public:
    void SetData(int a, int b){
        this->a = a;
        this->b = b;
    }
    void PrintData(){
        printf("a=%d  b=%d\n", a, b);
    }
};
int main(void){
    RefObj r1;
    RefObj r2;
    r1.SetData(100,200);
    r1.PrintData();
    r2.SetData(1000,2000);
    r2.PrintData();
    printf("size of RefObj r1 : %d byte\n", sizeof(r1));
    printf("size of RefObj r2 : %d byte\n", sizeof(r2));
    return 0;
}
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>CL.EXE  objmain.cpp
C:\javasrc\chap12>objmain.exe
a=100  b=200
a=1000  b=2000
size of RefObj r1 : 8 byte
size of RefObj r2 : 8 byte
***/
───────────────────────────────────────ⓑ

▣ 참고
◈ RefObj라는 클래스는 C++의 형식의 클래스이지만 자바의 클래스와 비슷한 모양을 하고 있습니다. C++에서는 메서드의 선언과 구현을 분리시키지만, 최대한 자바와 비슷한 형식으로 구현하기 위해서 인라인(inline) 메서드 형식으로 구현하고 있습니다. 

위의 예에서 RefObj형의 객체의 메모리를 생성하기 위해서 다음과 같이 메모리를 생성하고 있습니다. 

▣ RefObjet형의 객체의 메모리 생성
◈ RefObj r1;
◈ RefObj r2;

표준 C++에서는 클래스형의 변수 선언은 메모리의 생성을 의미합니다. 그렇기 때문에 위의 구문은 객체의 메모리 생성을 의미합니다. 

위의 예에서 sizeof 연산자를 이용해서 RefObj r1과 r2의 메모리를 계산하고 있습니다. 이 때 나타나는 메모리의 크기는 8바이트입니다. 이것은 int형 멤버 변수 2개를 가지고 있기 때문에 RefObj라는 클래스의 메모리는 8바이트가 되는 것입니다. 그렇다면 다음과 같은 결론을 내릴 수 있습니다. 

▣ 객체의 메모리 크기
◈ 객체의 메모리의 크기는 멤버 변수들의 전체 메모리의 크기와 같다.

객체의 메모리는 멤버 변수의 메모리와 동일한 크기를 가지게 됩니다. 만약 RefObj r1과 r2를 이용해서 메서드를 호출한다면, r1과 r2의 메모리와 RefObj 클래스의 형정보를 조합해서 메서드를 호출하게 됩니다.

객체의 메모리와 메서드의 호출【chap12\classobject.bmp】
 




메서드의 형태는 클래스의 정보가 있는 부분에 있으며, 객체의 메모리는 메모리 영역에 독립적으로 존재합니다. 클래스의 형정보와 객체의 메모리를 조합해서 메서드를 호출하거나 멤버 필드에 값을 변경할 수 있습니다. 가령 메서드 내에서 멤버 변수가 사용되어진다면, r1의 멤버 변수인지 r2의 멤버 변수인지만 구분해 주면 됩니다.

▣ 참고
◈ 위의 r1과 r2의 메서드가 호출되어지고 메서드 내부에서 멤버가 사용되어질 때, r1의 멤버 변수인지 r2의 멤버 변수인지를 구분하는 방법은 의외로 간단하다. 단순히 메서드를 호출할 때 숨어있는 매개변수가 하나 있다고 생각하면 된다. 이 숨어있는 매개변수가 바로 객체의 참조값이 된다. C++로 생각하면 주소겠지만 자바에서는 참조값을 들고다닌다고 생각하면 쉽게 해결할 수 있다. 실제 메서드에서 사용된 멤버가 어느 객체의 멤버 변수인지는 첨부된 참조값을 이용해서 구분할 수 있다.

클래스의 형정보와 객체의 메모리만 있다면 언제든지 메서드를 호출할 수 있습니다. 이것은 형태와 내용의 절묘한 조합입니다.

위의 그림에서 r1과 r2의 메모리는 독립적으로 존재하면서 RefObj 클래스의 형정보를 공유하는 형식으로 메서드를 호출하게 됩니다. 위의 방식은 C++에서 이용되는 방식이지만 자바에서도 이것은 동일합니다. 형정보와 객체의 메모리는 독립적으로 존재하면서 유기적으로 동작하게 됩니다.

객체의 메모리는 프로그램이 실행되는 동안 유지되는 순간적인 메모리입니다. 이 순간적인 객체의 메모리를 지속적으로 보관할 방법에 대해서 생각해 보도록 하죠. 그렇다면 여러분은 다음과 같은 질문을 던질 것입니다.

▣ 질문
◈ 객체의 어떠한 데이터를 저장할까?

쉽게 생각해 보면 위의 RefObj r1과 r2의 메모리를 저장하면 됩니다. 그리고 다시 복원하면 될 것입니다. 하지만 단순히 r1과 r2의 메모리를 저장한다면 각각 8바이트씩 저장될 것이며, 나중에 복원할 때 8바이트의 데이터가 무엇인지 알 방법이 없습니다. 즉 객체의 메모리와 약간의 정보를 포함시켜서 저장해야 할 것입니다.

▣ 객체 저장
◈ 객체의 메모리 자체와 객체에 대한 정보를 포함시켜야 한다.

객체의 메모리와 객체에 대한 약간의 정보를 포함시켜서 객체를 저장할 수 있다고 가정하죠. 그렇다면 다시 객체로 복원하는 메커니즘이 필요할 것입니다. 객체의 메모리와 정보를 해석해서 원래의 객체로 복원하는 것을 수작업으로 할 수 있지만, 매번 저장하고 복원할 때마다 구현하는 것은 불합리한 방법입니다.

▣ 직렬화(Serialization)
◈ 객체를 저장하는 기법

▣ 역직렬화(Deseialization)
◈ 직렬화된 객체를 복원하는 기법

자바에서는 객체를 저장하고 저장된 객체를 복원하는 작업을 자동화시켰으며, 객체를 저장하는 기법을 직렬화(Serialization)라고 하며 다시 복원하는 작업을 역직렬화(Deseialization)라고 합니다.

우리가 이 장에서 하려고 하는 작업은 가상머신에 존재하는 객체의 메모리 그 자체를 저장하거나, 통째로 네트웍으로 전송하려고 하는 것입니다. 저장을 하든 네트웍으로 전송을 하든 간에, 객체는 일련의 바이트의 형태로 되어 있어야 합니다. 약속된 규칙에 의해서 객체의 메모리를 한 줄로 늘어선 바이트의 형태로 만드는 것을 객체의 직렬화(Serialization)라고 하며, 다시 객체의 형태로 복원하는 작업을 우리는 객체의 역직렬화(Deserialization)라고 합니다.



12.1.2 직렬화의 개념



객체 직렬화란 객체를 바이트로 저장하는 기술을 말합니다. 즉 가상머신 내에 존재하는 특정 객체의 메모리를 바이트의 형태로 변환하는 것을 말합니다. 물론 바이트로 변환된 데이터를 다시 객체로 복원할 수도 있습니다. 객체를 바이트로 변환하는 것을 직렬화(Selializaiton)라고 하며, 바이트로 변환된 것을 다시 복원하는 작업을 역직렬화(Deselialization)라고 합니다. 

▣ 직렬화를 하는 이유
◈ 객체의 메모리는 순간적이기 때문에 영구적으로 보관하기 위해서

직렬화를 하는 이유는 객체의 메모리가 프로그램이 실행되는 동안에 유지되는 순간적인 메모리이기 때문에 이것을 지속적으로 보관하기 위해서입니다. 객체의 메모리에서 필요한 데이터를 저장한 후 다시 복원하는 기술 또한 필요합니다. 이러한 기법은 가상머신 차원에서 제공해 주고 있습니다.

몇 가지 작업만 해주면 가상머신 내부에서 직렬화가 자동으로 이루어집니다. 하지만 여기서는 직렬화의 개념 파악을 위해서 수작업으로 직렬화시키는 방법을 알아본 후 자바에서 이용할 수 있는 직렬화를 학습하도록 하겠습니다. 먼저 직렬화할 클래스부터 작성하도록 하죠.

『chap12\Employee.java』
ⓙ───────────────────────────────────────
/**
직렬화의 대상 클래스
**/
public class Employee {
    private String name;    // 이름
    private String dept;    // 부서
    private String title;    // 직책
    public Employee (String name, String dept, String title) {
        this.name = name;
        this.dept = dept;
        this.title = title;
    }// 생성자
    public String toString(){
        return name + ":" + dept + ":" + title;
    }
    public String getSerialData(){
        String data = "name=" + name + "\r\n";
                data += "dept=" + dept + "\r\n";
                data += "title=" + title;
        return data;
    }
} //end of Employee class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac Employee.java
***/
───────────────────────────────────────ⓑ

Employee라는 클래스의 객체를 다음과 같이 만들었을 때 해당 객체를 구성하는 핵심적인 데이터가 존재할 것입니다.

▣ Employee형의 객체 생성
◈ Employee e = new Employee("홍길동", "총무부", "부장");

Employee e의 멤버 변수에 해당하는 중요한 데이터는 name, dept, title이 될 것입니다. 그렇다면 name, dept, title의 데이터를 저장한다면 다시 객체로 복원할 수도 있을 것입니다. 물론 실제 직렬화에서는 데이터만 저장되는 것이 아니라 형정보도 추가되지만 여기서는 개념적인 직렬화의 예만을 테스트하도록 하겠습니다.

name, dept, title의 데이터를 저장하기 위해서 우선 저장할 규칙을 만들어야 합니다. 저장할 규칙은 다음과 같이 정하도록 하죠. 이 규칙은 여러분이 원하는대로 정하시면 됩니다.

▣ 저장하는 규칙
◈ name=홍길동
◈ dept=총무부
◈ title=부장

저장 규칙의 형태대로 만들기 위해서 Employee 클래스 내에 getSerialData()라는 메서드를 만들어 두었습니다. 

◈ public String getSerialData(){
◈         String data = "name=" + name + "\r\n";
◈                 data += "dept=" + dept + "\r\n";
◈                 data += "title=" + title;
◈         return data;
◈ }

직렬화할 클래스가 준비되었다면 실제 Employee 객체를 생성한 후 객체를 저장하는 클래스를 만들어 보겠습니다.

『chap12\SerialStream.java』
ⓙ───────────────────────────────────────
/**
객체의 직렬화를 담당하는 클래스
**/
import java.io.*;
public class SerialStream{
    private FileWriter fw;
    public SerialStream(FileWriter fw){
        this.fw = fw;
    }
    public void saveObject(Employee e) throws IOException{
        String str = e.getSerialData();
        fw.write(str);
    }
    public void close() throws IOException{
        fw.close();
    }
} //end of SerialStream class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialStream.java
***/
───────────────────────────────────────ⓑ

출력 스트림을 생성자의 매개변수로 준 후 saveObject(Employee e)만 호출하면 객체가 해당 출력 스트림으로 저장될 것입니다. Employee 객체를 저장하는 방법은 다음과 같습니다.

◈ Employee e = new Employee("홍길동", "총무부", "부장");
◈ SerialStream outs = new SerialStream(new FileWriter("serial.dat"));
◈ outs.saveObject(e);
◈ outs.close();

위와 같이 하면 serail.dat 파일에 저장하려고 하는 객체의 정보가 기록될 것입니다. 이제 반대로 serial.dat 파일에 있는 데이터를 읽어온 후 다시 객체로 복원하는 클래스를 만들어 보도록 하죠.

『chap12\DeSerialStream.java』
ⓙ───────────────────────────────────────
/**
객체의 역직렬화를 담당하는 클래스
**/
import java.io.*;
import java.util.*;
public class DeSerialStream{
    private BufferedReader br;
    public DeSerialStream(FileReader fr){
        this.br = new BufferedReader(fr);
    }
    public Employee restoreObject() throws IOException{
        String temp, name=null, dept=null, title=null;
        while((temp = br.readLine()) != null){
            StringTokenizer st = new StringTokenizer(temp, "=");
            String str = st.nextToken();
            if(str != null){
                if(str.equals("name")){
                    name = st.nextToken();
                }else if(str.equals("dept")){
                    dept = st.nextToken();
                }else if(str.equals("title")){
                    title = st.nextToken();
                }else{
                    System.out.println("잘못된 데이터입니다");    
                }
            }
        }
        if( (name!=null) && (dept!=null) && (title!=null)){
            Employee e = new Employee(name, dept, title);
            return e;
        }else{
            return null;
        }
    }
    public void close() throws IOException{
        br.close();
    }
} //end of DeSerialStream class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac DeSerialStream.java
***/
───────────────────────────────────────ⓑ 

입력 스트림을 이용해서 DeSerialStream 객체를 생성한 후 restoreObject()를 호출하면, 스트림에 연결된 파일로부터 데이터를 읽어들인 후 객체로 복원해서 리턴하는 예제입니다. 실제 객체를 복원하는 방법은 다음과 같습니다.

◈ DeSerialStream ins = new DeSerialStream(new FileReader("serial.dat"));
◈ Employee r = ins.restoreObject();
◈ System.out.println("원본 Employee e:" + e + " " + e.hashCode());
◈ System.out.println("복원 Employee e:" + r + " " + r.hashCode());
◈ ins.close();

위의 객체를 저장하고 복원하는 방법을 테스트하는 예는 다음과 같습니다.

『chap12\SerialTest.java』
ⓙ───────────────────────────────────────
/**
객체를 저장하고 복원하는 방법을 테스트하는 예
**/
import java.io.*;
public class SerialTest{
    public static void main(String[] args) throws IOException{
        Employee e = new Employee("홍길동", "총무부", "부장");
        SerialStream outs = new SerialStream(new FileWriter("serial.dat"));
        outs.saveObject(e);
        outs.close();
        DeSerialStream ins = new DeSerialStream(new FileReader("serial.dat"));
        Employee r = ins.restoreObject();
        System.out.println("원본 Employee e:" + e + " " + e.hashCode());
        System.out.println("복원 Employee e:" + r + " " + r.hashCode());
        ins.close();
    } //end of main
} //end of SerialTest class
//㉶--------------------------------------------㉳
/***
C:\javasrc\chap12>javac SerialTest.java
C:\javasrc\chap12>java SerialTest
원본 Employee e:홍길동:총무부:부장 23671010
복원 Employee e:홍길동:총무부:부장 17332331
***/
───────────────────────────────────────ⓑ




12.1.3 직렬화의 방법



앞에서 배운 수동으로 직렬화하는 방법은 단순히 직렬화의 의미를 파악하기 위해서 테스트한 것이며, 실제 직렬화는 이보다 더 복잡한 과정을 필요로 합니다. 자바의 직렬화는 직렬화의 메커니즘이 내부적으로 완벽하게 감추어져 있기 때문에 객체 직렬화를 직접 구현을 하는 것이 아니라 규칙에 맞게 사용하는 방법을 배우는 것이라고 보면 됩니다. 자바에서 직렬화를 구현하기 위해서는 Serializable이나 Exteranalizable 인터페이스를 이용합니다.

▣ 직렬화를 위한 인터페이스
◈ Serializable 인터페이스
◈ Exteranalizable 인터페이스

객체 스트림에 저장될 객체의 클래스는 반드시 Serializable이나 Externalizable 둘 중 하나를 구현해야 합니다. 이들 중 하나의 인터페이스를 구현함으로써 생성된 객체가 저장(직렬화)될 의사가 있음을 반드시 밝혀야 합니다.

▣ 객체 직렬화를 위한 Serializable 인터페이스의 원형
◈ public interface Serializable {
◈         //...
◈ }

▣ 객체 직렬화를 위한 Externalizable 인터페이스의 원형
◈ public interface Externalizable extends Serializable {
◈         public void writeExternal(ObjectOutput out) throws IOException;
◈         public void readExternal(ObjectInput in)
◈                 throws IOException, ClassNotFoundException;
◈ }

▣ Serializable이나 Externalizable을 구현한다는 의미
◈ Serializable이나 Externalizable을 구현한 클래스로 객체를 생성했다면, 해당 객체가 저장(직렬화)될 의사가 있음을 밝히는 것이다.

Serializable 인터페이스는 어떠한 메서드도 포함하고 있지 않은 표시(Marker) 인터페이스입니다. Serializable 인터페이스를 구현하기 위해서는 단순히 implements Serializable을 붙여만 주면됩니다. 

▣ Serializable 인터페이스의 구현
◈ public class MyObject implements Serializable{
◈         //...
◈ }

Serializable의 표시만 해주면 해당 클래스의 객체는 자동으로 멤버 필드들의 값을 저장하고 복구할 수 있는 능력을 가지게 됩니다. 반면, Externalizable 인터페이스를 구현한 클래스는 저장할 멤버 필드들의 종류와 값을 writeExternal()과 readExternal()을 이용해서 저장하고 복원하는 과정을 직접 구현해야 합니다. 

▣ Serializable 인터페이스
◈ 객체가 직렬화될 수 있다는 것을 나타내는 표시이다. 이 때 데이터의 저장과 복구는 자동으로 이루어진다.
▣ Externalizable 인터페이스
◈ 직렬화의 권한은 있지만 객체저장과 복구의 방법을 사용자가 직접 구현해야 합니다. 즉 프로그래머가 데이터의 저장과 복구를 제어할 수 있다.
저작자 표시 비영리 변경 금지

'JAVA PROGRAMMING > JAVA' 카테고리의 다른 글

12.3 Externalizable  (0) 2010/02/21
12.2 Serializable  (0) 2010/02/21
12.1 Serialization  (0) 2010/02/21
11.3 리플렉션 프로그래밍  (0) 2010/02/21
11.2 정적 바인딩 클래스와 동적 바인딩 클래스  (0) 2010/02/21
11.1 Reflection  (0) 2010/02/21
1 2 3 4 5  ... 21 
BLOG main image
(주)KinTeL 회장 김형기

카테고리

분류 전체보기 (159)
My Name Is KinTeL (3)
농구 인생 (1)
사진 이야기 (13)
미디어 (5)
JAVA PROGRAMMING (105)
C# Programming (2)
FLEX (6)
Database (4)
About Eclipse (1)
참조 - Reference (9)
모든 무료 정보 (2)
좋은글 (2)


Statistics Graph
Total : 73,500
Today : 11
Yesterday : 43

최근에 받은 트랙백