Serializable, NIO

de_sj_awa·2021년 5월 2일
0
post-custom-banner

1. Serializable 이란?

java.io 패키지에 있는 Serializable이라는 인터페이스를 API에서열어보자. 잘 살펴보면 이 인터페이스의 API에 선언된 변수나 메소드가 없다는 사실을 알 수 있다. 아무런 구현해야 할 메소드도 없는 이 인터페이스가 도대체 왜 있는 것일까?

개발을 하다보면, 생성한 객체를 파일로 저장할 일이 있을 수도 있고, 저장한 객체를 읽을 일이 생길 수도 있다. 그리고, 객체를 다른 서버로 보낼 때도 있고, 다른 서버에서 생성한 객체를 받을 일도 생길 수 있다. 그럴때 꼭 필요한 것이 바로 Serializable이다. 만약 만든 클래스가 파일에 읽거나 쓸 수 있도록 하거나, 다른 서버로 보내거나 받을 수 있도록 하려면 반드시 이 인터페이스를 구현(implements) 해야만 한다. Serializable 인터페이스를 구현하면 JVM에서 해당 객체는 저장하거나 다른 서버로 전송할 수 있도록 해준다.

그리고, Serializable 인터페이스를 구현한 후에는 다음과 같이 serialVersionUID라는 값을 지정해 주는 것을 권장한다. 만약 별도로 지정하지 않으면, 자바 소스가 컴파일될 때 자동으로 생성된다.

static final long serialVersionUID = 1L;

반드시 static final long으로 선언해야 하며, 변수명도 serialVersionUID로 선언해 주어야만 한다. 또한 이 값은 아무런 값이나 지정해주면 된다. 단, 이 값을 지정하는 이유가 있기 때문에, 필요에 따라서 값을 변경해야 하는 경우가 발생한다.

serialVersionUID라는 값은 어디에 사용되길래 지정해야 한다고 했을까? 이 값은 해당 객체의 버전을 명시하는 데 사용된다. A라는 서버에서 B라는 서버로 SerialDTO라는 클래스의 객체를 전송한다고 가정하자. 전송하는 A 서버에 SerialDTO라는 클래스가 있어야 하고, 전송받는 B 서버에도 SerialDTO라는 클래스가 있어야만 한다. 그래야만 그 클래스의 객체임을 알아차리고 데이터를 받을 수 있따.

그런데 만약 A 서버가 갖고 있는 SerialDTO에는 변수가 3개 있고, B 서버의 SerialDTO에는 변수가 4개인 상황이 발생하면 자바에서는 제대로 처리를 못하게 된다. 따라서, 각 서버가 쉽게 해당 객체가 같은지 다른지를 확인할 수 있도록 하기 위해서는 serialVersionUID로 관리를 해주어야만 한다. 즉, 클래스 이름이 같더라도, 이 ID가 다르면 다른 클래스라고 인식한다. 게다가, 같은 UID라고 할지라도, 변수의 개수나 타입 등이 다르면 이 경우도 다른 클래스로 인식한다.

2. 객체를 저장해보자

예제를 통해서 객체를 저장하고, 저장한 객체를 읽어들이는 작업을 수행해보자. 자바에서는 ObjectOutputStream이라는 클래스를 사용하면 객체를 저장할 수 있다. 반대로, ObjectInputStream라는 클래스를 사용하면 저장해 놓은 객체를 읽을 수 있다.

예제 코드

package e.io.object;

public class SerialDTO {
    private String bookName;
    private int bookOrder;
    private boolean bestSeller;
    private long soldPerDay;
    public SerialDTO(String bookName, int bookOrder, boolean bestSeller, long soldPerDay){
        super();
        this.bookName = bookName;
        this.bookOrder = bookOrder;
        this.bestSeller = bestSeller;
        this.soldPerDay = soldPerDay;
    }

    @Override
    public String toString(){
        return "SerialDTO [bookName=" + bookName + ", bookOrder=" + bookOrder
                + ", bestSeller=" + bestSeller + ", soldPerDay=" + soldPerDay
                + "]";
    }
}

보통은 다음과 같이 DTO를 만든다. 데이터를 가져오는 getBookName()과 같은 getter 메소드와 setBookName()과 같은 setter 메소드를 만들어야 하지만, 본 예제에서는 딱히 필요가 없으므로 일단 이렇게만 만들어 놓자.

이제 저장하는 클래스를 다음과 같이 만들자.

예제 코드

package e.io.object;

import static java.io.File.separator;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class ManageObject {
    public static void main(String[] args){
        ManageObject manager = new ManageObject();
        String fullPath=separator+"godofjava"+separator+"text"+separator+"serial.obj";
        SerialDTO dto = new SerialDTO("GodOfJavaBook", 1, true, 100);
        manager.saveObject(fullPath, dto);
    }
    public void saveObject(String fullPath, SerialDTO dto){
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try{
            fos = new FileOutputStream(fullPath);
            oos = new ObjectOutputStream(fos);
            oos.writeObject(dto);
            System.out.println("Write Success !!!");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(oos != null){
                try{
                    oos.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            if(fos != null){
                try{
                    fos.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }
}

ObjectOutputStream에서는 write() 메소드를 사용하여 int 값을 저장하고, writeByte() 메소드로 바이트 값을 저장하면 된다.

이제 ManagerObject 클래스를 실행해보자. 결과가 어떻게 나올까?
실행하면 다음과 같은 예외가 발생한다.

Serializable이 되어 있지 않다는 NotSerializableException이 발생한 것을 볼 수 있다. SerialDTO의 선언 부분을 제대로 살펴봤는지 모르겠지만, Serializable을 구현하지 않았다. 제대로 구현하려면 이 DTO 클래스의 선언부는 다음과 같이 되어 있어야만 한다.

package e.io.object;

import java.io.Serializable;

public class SerialDTO implements Serializable {
    private String bookName;
    private int bookOrder;
    private boolean bestSeller;
    private long soldPerDay;
    public SerialDTO(String bookName, int bookOrder, boolean bestSeller, long soldPerDay){
        super();
        this.bookName = bookName;
        this.bookOrder = bookOrder;
        this.bestSeller = bestSeller;
        this.soldPerDay = soldPerDay;
    }

    @Override
    public String toString(){
        return "SerialDTO [bookName=" + bookName + ", bookOrder=" + bookOrder
                + ", bestSeller=" + bestSeller + ", soldPerDay=" + soldPerDay
                + "]";
    }
}

이렇게 변경하고 재컴파일 후 다시 실행해보면 다음과 같은 실행 결과가 나온다.

그리고, 윈도우 사용자는 C:\godofjava\text 디렉터리에 serial.obj라는 파일이 생성되어 있을 것이다. 이 파일은 일반 텍스트 편집기로 열어서 보기는 어려울 것이다. 왜냐하면 자바 객체가 바이너리(binary)로 저장되어 있기 때문이다.

3. 객체를 읽어보자

앞에서 저장한 객체를 읽어보자. 앞의 예제와 동일하게 Output 대신 Input으로 되어 있는 클래스들을 사용하면 된다.

 public void loadObject(String fullPath){
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try{
            fis = new FileInputStream(fullPath);
            ois = new ObjectInputStream(fis);
            Object obj = ois.readObject();
            SerialDTO dto = (SerialDTO)obj;
            System.out.println(dto);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(ois != null){
                try{
                    ois.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
            if(fis != null){
                try{
                    fis.close();
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

main() 메소드는 다음과 같이 수정하자.

 public static void main(String[] args){
        ManageObject manager = new ManageObject();
        String fullPath=separator+"godofjava"+separator+"text"+separator+"serial.obj";
        //SerialDTO dto = new SerialDTO("GodOfJavaBook", 1, true, 100);
        //manager.saveObject(fullPath, dto);
        manager.loadObject(fullPath);
    }

실행 결과

SerialDTO의 toString() 메소드를 구현해 놓았기 때문에 다음과 같이 읽어 들인 객체의 내용이 출력되는 것을 볼 수 있다.

즉, 객체를 정상적으로 읽었다는 의미다.

이번에는 Serializable 객체가 변경되었을 때 어떤 상황이 연출되는지 확인해보자. 방금 실행한 예제에서 다른 클래스는 수정하지 않고, 다음과 같이 SerialDTO 클래스에 변수를 추가하자.

package e.io.object;

import java.io.Serializable;

public class SerialDTO implements Serializable {
    private String bookType="IT";
	// 이하 생략
}

그리고나서 저장되어 있는 객체를 읽는 ManagedObject 클래스를 다시 실행해보자. 결과는 다음과 같이 나온다.

java.io.InvalidClassException: e.io.object.SerialDTO; local class incompatible: stream classdesc serialVersionUID = -340355846837718525, local class serialVersionUID = 5529615262367075160
	at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:722)
	at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2022)
	at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1872)
	at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2179)
	at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1689)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:495)
	at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:453)
	at e.io.object.ManageObject.loadObject(ManageObject.java:52)
	at e.io.object.ManageObject.main(ManageObject.java:16)

보는 것과 같이 SerialVersionUID가 다르다는 InvalidClassException 예외 메시지가 출력된다. 앞서 말한 대로, 변수가 추가되는 등 Serializable 객체의 형태가 변경되면 컴파일시 serialVersionUID가 다시 생성되므로, 이와 같은 문제가 발생한다.

이러한 상황에서 예외가 발생되지 않도록 하려면 SerialDTO 클래스에 serialVersionUID를 다음과 같이 추가하면 된다.

package e.io.object;

import java.io.Serializable;

public class SerialDTO implements Serializable {
    static final long serialVersionUID = 1L;
    private String bookType="IT";
    private String bookName;
    private int bookOrder;
    private boolean bestSeller;
    private long soldPerDay;
    public SerialDTO(String bookName, int bookOrder, boolean bestSeller, long soldPerDay){
        super();
        this.bookName = bookName;
        this.bookOrder = bookOrder;
        this.bestSeller = bestSeller;
        this.soldPerDay = soldPerDay;
    }

    @Override
    public String toString(){
        return "SerialDTO [bookName=" + bookName + ", bookOrder=" + bookOrder
                + ", bestSeller=" + bestSeller + ", soldPerDay=" + soldPerDay
                + ", bookType=" + bookType + "]";
    }
}

그리고, ManageObject 클래스의 main() 클래스에 주석 처리해 놓은 saveObject() 메소드를 실행할 수 있도록 주석을 푼 후에 다시 실행해보자. 그러면 아무 예외 없이 실행이 가능할 것이다. 정상적인 경우라면 다음과 같은 메시지가 출력된다.

Write Success !!!
SerialDTO [bookName=GodOfJavaBook, bookOrder=1, bestSeller=true, soldPerDay=100, bookType=IT]

그런데 만약 이렇게 serialVersionUID를 지정해 놓은 상태에서 저장되어 잇는 객체와 읽는 객체가 다르다면 어떻게 될까?
방금 추가한 bookType의 변수명을 다음과 같이 bookTypes로 바꾸자.

package e.io.object;

import java.io.Serializable;

public class SerialDTO implements Serializable {
    static final long serialVersionUID = 1L;
    private String bookTypes="IT";
    private String bookName;
    private int bookOrder;
    private boolean bestSeller;
    private long soldPerDay;
    public SerialDTO(String bookName, int bookOrder, boolean bestSeller, long soldPerDay){
        super();
        this.bookName = bookName;
        this.bookOrder = bookOrder;
        this.bestSeller = bestSeller;
        this.soldPerDay = soldPerDay;
    }

    @Override
    public String toString(){
        return "SerialDTO [bookName=" + bookName + ", bookOrder=" + bookOrder
                + ", bestSeller=" + bestSeller + ", soldPerDay=" + soldPerDay
                + ", bookTypes=" + bookTypes + "]";
    }
}

이렇게 지정해 놓고, ManageObject 클래스의 main() 메소드에서 saveObject()가 실행되지 않도록 주석처리 후 다시 실행하자.
앞서 저장되어 있는 SerialDTO 객체에는 bookType이라는 변수가 있었다. 따라서 bookTypes라는 변수는 존재하지 않는다. 이 경우 다음과 같이 결과가 출력된다.

SerialDTO [bookName=GodOfJavaBook, bookOrder=1, bestSeller=true, soldPerDay=100, bookTypes=null]

즉, 변수의 이름이 바뀌면 저장되어 있는 객체에서 찾지 못하므로, 해당 값은 null로 처리된다. 지금까지 살펴본 것처럼 Serializable을 구현해 놓은 상황에서 serialVersionUID를 명시적으로 지정하면 변수가 변경되더라도 예외는 발생하지 않는다. 하지만, 만약 이렇게 Serializable한 객체의 내용이 바뀌었는데도 아무런 예외가 발생하지 않으면 운영 상황에서 데이터가 꼬일 수 있기 때문에 절대 권장하는 코딩 방법이 아니다. 따라서, 이렇게 데이터가 바뀌면 serialVersionUID의 값을 변경하는 습관을 가져야만 데이터에 문제가 발생하지 않는다. 추가로 이클립스와 같은 요즘 대부분의 자바 개발툴에서는 자동으로 serialVersionUID를 생성하는 기능을 제공하기도 한다.

4. transient

한 가지만 더 확인해보자.
SerialDTO의 bookOrder 선언문 앞에 transient라는 예약어를 추가하자.

transient private int bookOrder;

그리고, 객체를 저장하고 읽어 오도록 main() 메소드의 saveObject() 호출 부분이 실행되도록 되어 있는 상황에서 실행해보자. 이렇게 하면 결과는 다음과 같다.

Write Success !!!
SerialDTO [bookName=GodOfJavaBook, bookOrder=0, bestSeller=true, soldPerDay=100, bookTypes=IT]

여기서는 무엇보다도, bookOrder 값을 보자.
우리는 분명히 이 bookOrder 값을 1로 지정하고 저장했다. 하지만, 읽어낸 값을 살펴보면 0이 출력된 것을 볼 수 있다.
왜 이러한 현상이 발생했을까?

객체를 저장하거나, 다른 JVM으로 보낼 때, transient라는 예약어를 사용하여 선언한 변수는 Serializable의 대상에서 제외된다. 다시 말해서, 해당 객체는 저장 대상에서 제외되어 버린다. "무시될꺼면 무엇하러 이 변수를 만들어?"라고 생각하는 독자가 있을 수도 있다. 해당 객체를 생성한 JVM에서 사용할 때에는 이 변수를 사용하는 데 전혀 문제가 없다. 예를 들어 패스워드를 보관하고 있는 변수가 있다고 생각해보자. 이 변수가 저장되거나 전송된다면 보안상 큰 문제가 발생할 수도 있다. 따라서, 이렇게 보안상 중요한 변수나 꼭 저장해야 할 필요가 없는 변수에 대해서는 transient를 사용할 수 있다.

5. 자바 NIO란?

JDK 1.4에서부터 NIO(New IO)라는 것이 추가되었다. 이 NIO가 생긴 이유는 단 하나다. 속도 때문이다.

NIO는 지금까지 사용한 스트림을 사용하지 않고, 대신 채널(Channel)과 버퍼(Buffer)를 사용한다. 채널은 물건을 중간에서 처리하는 도매상이라고 생각하면 된다. 그리고, 버퍼는 도매상에서 불건을 사고, 소비자에게 물건을 파는 소매상이라고 생각하면 된다. 대부분 소매상을 통해서 도매상과 이야기 할 일이 없다. 자바 NIO에서 데이터를 주고 받을 때에는 버퍼를 통해서 처리한다. 먼저 예제를 통해서 살펴보자.

package e.network;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NioSample {

    public static void main(String[] args){
        NioSample sample = new NioSample();
        sample.basicWriteAndRead();
    }
    public void basicWriteAndRead(){
        String fileName="C:\\godofjava\\text\\nio.txt";
        try{
            writeFile(fileName, "My First NIO sample.");
            readFile(fileName);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    public void writeFile(String fileName, String data) throws Exception{
        FileChannel channel = new FileOutputStream(fileName).getChannel();
        byte[] byteData = data.getBytes();
        ByteBuffer buffer = ByteBuffer.wrap(byteData);
        channel.write(buffer);
        channel.close();
    }
    public void readFile(String fileName) throws Exception{
        FileChannel channel = new FileInputStream(fileName).getChannel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        channel.read(buffer);
        buffer.flip();
        while(buffer.hasRemaining()){
            System.out.print((char)buffer.get());
        }
        channel.close();
    }
}

이 예제에서는 writeFile()과 readFile() 메소드 위주로 자세히 알아보자.

public void writeFile(String fileName, String data) throws Exception{
        FileChannel channel = new FileOutputStream(fileName).getChannel(); //1.
        byte[] byteData = data.getBytes();
        ByteBuffer buffer = ByteBuffer.wrap(byteData);	//2.
        channel.write(buffer);	//3.
        channel.close();
    }
  1. 파일을 쓰기 위한 FileChannel 객체를 만들려면, 이와 같이 FileOutputStream 클래스에 선언된 getChannel() 이라는 메서드를 호출한다.
  2. ByteBuffer 클래스에 static으로 선언된 wrap()이라는 메소드를 호출하면, ByteBuffer 객체가 생성된다. 이 메소드의 매개 변수는 저장할 byte의 배열을 넘겨주면 된다.
  3. FileChannel 클래스에 선언된 write() 메소드에 buffer 객체를 넘겨주면 파일에 쓰게 된다.
 public void readFile(String fileName) throws Exception{
        FileChannel channel = new FileInputStream(fileName).getChannel(); //4.
        ByteBuffer buffer = ByteBuffer.allocate(1024); //5.
        channel.read(buffer); //6.
        buffer.flip(); //7.
        while(buffer.hasRemaining()){ //8.
            System.out.print((char)buffer.get()); //9.
        }
        channel.close();
    }
  1. 파일을 읽기 위한 객체도 FileInputStream 클래스에 선언된 getChannel() 메소드를 사용하면 된다.
  2. ByteBuffer 클래스에 선언되어 있는 allocate() 메소드를 통해서 buffer라는 객체를 만들었다. 여기서의 매개 변수는 데이터가 기본적으로 저장되는 크기를 의미한다.
  3. 채널의 read() 메소드에 buffer 객체를 넘겨줌으로써, 데이터를 이 버퍼에다 담으라고 알려 준다. 이렇게 알려주기만 하면, buffer에는 데이터가 담기기 시작한다.
  4. flip() 메소드는 buffer에 담겨 있는 데이터의 가장 앞으로 이동한다.
  5. get() 메소드를 호출하면 한 바이트씩 데이터를 읽는 작업을 수행한다.
  6. ByteBuffer에 선언되어 있는 hasRemaining() 메소드를 사용하여 데이터가 더 남아 있는지를 확인하면서 반복작업을 수행한다.

다른 IO 관련 클래스들처럼 마지막에 close() 메소드를 호출하여 Channel을 닫아야 한다.

이제 이 클래스를 실행해보자. 결과는 다음과 같이 출력된다.

My First NIO sample.

이 예제에서 사용한 것처럼 파일 데이터를 다룰 때에는 ByteBuffer라는 버퍼와 FileChannel이라는 채널을 사용하면 간단히 처리할 수 있다. Channel의 경우 그냥 간단하게 객체만 생성하여 read()나 write() 메소드만 불러주면 된다고 생각하면 된다. 그런데, Buffer 클래스는 간단하게 되어있지 않으므로 자세히 살펴보자.

6. NIO의 Buffer 클래스

NIO에서 제공하는 Buffer는 java.nio.Buffer 클래스를 확장하여 사용한다. 앞 절에는 ByteBuffer만 살펴 봤지만, 그 외에 CharBuffer, DoubleBuffer, IntBuffer, ShortBuffer 등이 존재한다. 이러한 Buffer 클래스(이하 버퍼)에 선언되어 있는 메소드는 flip()만 있는 것이 아니다. 먼저 버퍼의 상태 및 속성을 확인하기 위한 메소드를 살펴보자.

리턴 타입 메소드 설명
int capacity() 버퍼에 담을 수 있는 크기 리턴
int limit 버퍼에서 읽거나 쓸 수 없는 첫 위치 리턴
int position() 현재 버퍼의 위치 리턴

여기서 position(위치)이라는 말이 나오는데, 버퍼는 CD처럼 위치가 있다. 버퍼에 데이터를 담거나, 읽는 작업을 수행하면 현재의 "위치"가 이동한다. 그래야 그 다음 "위치"에 있는 것을 바로 쓰거나, 읽을 수 있기 때문이다.
따라서,

  • 현재의 "위치"를 나타내는 메소드가 position()이고,
  • 읽거나 쓸 수 없는 "위치"를 나타내는 메소드가 limit()이고,
  • 버퍼의 크기(capacity)를 나타내는 것이 capacity()

메소드다. 이 3개 값의 관계는 다음과 같다.

0 <= position <= limit <= 크기(capacity)

NIO를 제대로 이해하려면 이 세 개 값의 관계를 꼭 이해하고, 기억해야만 한다.

예제를 통해서 이 세개의 메소드와 친해져 보자.

package e.network;

import java.nio.IntBuffer;

public class NioDetailSample {
    public static void main(String[] args){
        NioDetailSample sample = new NioDetailSample();
        sample.checkBuffer();
    }
    public void checkBuffer(){
        try{
            IntBuffer buffer = IntBuffer.allocate(1024);
            for(int loop=0; loop<100; loop++){
                buffer.put(loop);
            }
            System.out.println("Buffer capacity="+buffer.capacity()); //1.
            System.out.println("Buffer limit   ="+buffer.limit()); //2.
            System.out.println("Buffer position="+buffer.position()); //3.
            buffer.flip(); //4.
            System.out.println("Buffer flipped !!!");
            System.out.println("Buffer limit   ="+buffer.limit());
            System.out.println("Buffer position="+buffer.position());
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

실행 결과는 다음과 같다.

Buffer capacity=1024
Buffer limit   =1024
Buffer position=100
Buffer flipped !!!
Buffer limit   =100
Buffer position=0

앞서 살펴본 각 값들의 관계에 mark를 추가하면 다음과 같다.

0 <= mark <= position <= limit <= 크기(capacity)

그러면 위치를 변경하는 메소드에 어떤 것들이 있는지 알아보자.

리턴 타입 메소드 설명
Buffer flip() limit 값을 position으로 지정한 후, position을 0(가장 앞)으로 이동
Buffer mark() 현재 position을 mark
Buffer reset() 현재의 position을 mark한 곳으로 이동
Buffer rewind() 현재 position을 0으로 이동
int remaining() limit - position 계산 결과를 리턴
boolean hasRemaining() position과 limit 값에 차이가 있을 경우 true를 리턴
Buffer clear() 버퍼를 지우고 현재 position을 0으로 이동하며, limit 값을 버퍼의 크기로 변경

언뜻 보면 flip() 메소드와 rewind() 메소드가 비슷해 보인다. 하지만, flip()은 limit 값을 변경하지만, rewind()는 limit 값을 변경하지 않는다. 그리고, remaining() 메소드나 hasRemaining() 메소드를 사용하면 limit까지만 데이터를 읽을 수 있다. 그리고, mark() 메소드를 사용하여 특정 위치를 표시해 두고 다시 읽을 필요가 읽을 때 reset() 메소드를 사용한다.

한 번에 이해하기 쉽지 않은 내용이다. 하지만 예제를 통해서 확인해 보면 보다 쉽게 이해할 수 있을 것이다. 먼저 결과를 출력하는 다음의 예제를 만들자.

public void printBufferStatus(String job, IntBuffer buffer){
        System.out.println("Buffer "+job+" !!!");
        System.out.format("Buffer Position=%d remaining=%d limit=%d\n" 
                ,buffer.position(), buffer.remaining(), buffer.limit());
    }

어떤 작업을 한 이후에 position, remaining, limit 값들을 출력하도록 했다.
이제 checkBufferStatus() 메소드를 다음과 같이 만들자.

 public void checkBufferStatus(){
        try{
            IntBuffer buffer = IntBuffer.allocate(1024);
            buffer.get();
            printBufferStatus("get", buffer);
            buffer.mark();
            printBufferStatus("mark", buffer);
            buffer.get();
            printBufferStatus("get", buffer);
            buffer.reset();
            printBufferStatus("reset", buffer);
            buffer.rewind();
            printBufferStatus("rewind", buffer);
            buffer.clear();
            printBufferStatus("clear", buffer);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

하나의 데이터를 읽고(get), 위치를 표시하고(mark), 다시 읽고(get), 표시한 position으로 다시 이동한 후(reset), 처음으로 이동하고(rewind), 데이터를 지우는(clear) 작업을 수행하도록 했다.

결과는 다음과 같다.

Buffer get !!!
Buffer Position=1 remaining=1023 limit=1024
Buffer mark !!!
Buffer Position=1 remaining=1023 limit=1024
Buffer get !!!
Buffer Position=2 remaining=1022 limit=1024
Buffer reset !!!
Buffer Position=1 remaining=1023 limit=1024
Buffer rewind !!!
Buffer Position=0 remaining=1024 limit=1024
Buffer clear !!!
Buffer Position=0 remaining=1024 limit=1024

이와 같이 버퍼에서 제공하는 메소드를 사용하면 데이터를 읽는 데 큰 어려움이 없을 것이다.

지금까지 간단하게 NIO에 대해 알아보았다. NIO는 단지 파일을 쓰고 읽을 때에만 사용하는 것이 아니라, 파일 복사를 하거나, 네트워크로 데이터를 주고 받을 때에도 사용할 수 있다.

참고

  • 자바의 신
profile
이것저것 관심많은 개발자.
post-custom-banner

0개의 댓글