▩ 목 차 ▩
1. Serializable에 대해서 좀 살펴보자
2. 객체를 저장해보자
3. 객체를 읽어보자
4. transient라는 예약어는 Serializale과 떨어질 수 없는 관계다
5. NIO란 ?
5-1. NIO의 Buffer 클래스
■ 1. Serializable에 대해서 좀 살펴보자 ■
개발하다 보면, 생성한 객체를 파일로 저장할 일이 있을 수도 있고, 저장한 객체를 읽을 일이 생길 수도 있다. 그리고, 객체를 다른 서버로 보낼 때도 있고, 다른 서버에서 생성한 객체를 받을 일도 생길 수 있다.
==> 그럴 때 꼭 필요한 것이 바로 Serializable이다.
만약 클래스 파일에 읽거나 쓸 수 있도록 하거나, 다른 서버로 보내거나 받을 수 있도록 하려면 반드시 이 인터페이스를 구현(implements)해야만 한다. ==> Serializalbe인터페이스를 구현하면 JVM에서 해당 객체는 저장하거나 다른 서버로 전송할 수 있도록 해준다.
Serializalbe 인터페이스를 구현한 후에는 다음과 같이 serialVersionUID라는 값을 지정해 주는 것을 권장한다. [ 만약, 별도로 지정하지 않으면, 자바 소스가 컴파일될 때 자동으로 생성된다. ]
static final long serialVersionUID = 1L;
반드시 static final long으로 선언해야 하며, 변수명도 serialVersionUID로 선언해 주어야만 자바에서 알아듣는다.
여기서 어떤 값으로 지정해주면 될까?
==> 아무런 값이나 지정해주면 된다. 단, 이 값을 지정하는 이유가 있기 때문에, 필요에 따라서 값을 변경해야 하는 경우가 발생한다.
serialVersionUID라는 값은 해당 객체의 버전을 명시하는데 사용된다. 즉, 객체에 대한 정보를 바꾸게 되면 받는 입장에서 알아차리기 힘들다.[null값이 들어가는 경우도 있음] 그렇기에 객체의 버전을 바꿔 새롭게 동기화를 해준다.
■ 2. 객체를 저장해보자 ■
예제를 통해 객체를 저장하고, 저장한 객체를 읽어 들이는 작업을 해보자.
[ 자바에서는 ObjectOutPutStream이라는 클래스를 사용하면 객체를 저장할 수 있다. 반대로, ObjectInputStream이라는 클래스를 사용하면 저쟁해 놓은 객체를 읽을 수 있다. ]
[EX] - 저정할 객체 생성
package part27;
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() {
// TODO Auto-generated method stub
return "SerialDTO [ bookName=" + bookName + ", bookOrder=" + bookOrder
+ ", bestSeller=" + bestSeller + ", soldPerDay=" + soldPerDay
+ "]";
}
}
[EX] - ObjectOutPutStream 클래스를 사용하여 객체를 저장
package part27;
import static java.io.File.separator;
import java.io.FileInputStream;
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) {
FileInputStream fos = null;
ObjectOutputStream oos = null;
try {
fos = new FileInputStream(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();
}
}
}
}
}
위의 코드를 보자.
ffullPath 라는 이름으로 저장할 파일 위치와 파일명을 지정했다. 그 후 SerialDTO 클래스의 객체를 생성한 후 saveObject()라는 매개변수로 전달했다. saveObject()메소드 안의 숫자 주석을 살펴보자.
- FileOutputStream 객체를 생성했다.
- 객체를 저장하기 위해서 ObjectOuputStream 객체를 생성했다. 이 객체를 생성할 때 1번에서 생성한 객체를 매개 변수로 넘겼다. 이렇게 하면, 해당 객체는 파일에 저장된다.
- writeObject()라는 메소드를 사용하여 매개변수로 넘어온 객체를 저장한다.
[ ObjectOutputStream에는 wirte() 메소드를 사용하여 Int 값을 저장하고, writeByte() 메소드로 바이트 값을 저장하면 된다. ]
위의 코드를 실행한 결과 예외가 발생한다.
java.io.NotSerializableException: part27.SerialDTO
at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1197)
at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:354)
at part27.ManageObject.saveObject(ManageObject.java:26)
at part27.ManageObject.main(ManageObject.java:16)
위와 같이 예외가 발생하는 이유는 Serializable이 되어 있지 않아서 NotSrializableException이 발생한 것을 볼 수 있다.
즉, SerialDTO를 선언할때 Serializable을 구현하지 않았던 것이다. SerialDTO를 아래와 같이 고쳐보자.
package part27;
import java.io.Serializable;
public class SerialDTO implements Serializable{
// 중간 부분 생략
}
위와 같이 고치고 ManageObject 클래스를 실행시키면 아래와 같이 출력된다.
Write Success
생성된 파일을 열어보면 serial.obj 라는 파일이 생성되었을 것인데, 이 파일은 일반 텍스트 편집기로 열어서 보기는 어려울 것이다. 왜냐하면 객체가 바이너리로 저장되어 있기 때문이다.
■ 3. 객체를 읽어보자 ■
앞에서 저장한 객체를 읽어보자. 앞의 예제와 동일하게 Output 대신 Input으로 되어 있는 클래스들을 사용하면 된다.
[EX] - 객체 읽기
public void loadObject(String fullPath) {
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream(fullPath); //1
ois = new ObjectInputStream(fis); //2
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();
}
}
}
}
SerialDTO [ bookName=GodofJavaBook, bookOrder=1, bestSeller=true, soldPerDay=100]
위 코드를 보자.
앞에서 본 예제랑 다를 바 없는데, 아래에 있는 부분이 다를 것이다.
Object obj=ois.readObject();
SerialDTO dto =(SerialDTO)obj;
ObjectOuputStream에서는 write로 시작하는 메소드를 사용했지만, ObjectInputStream에서는 read로 시작하는 메소드를 사용한다.
즉, 객체를 읽을때에는 readObject() 메소드를 사용하면 된다. 이 메소드의 리턴 타입이 Object이므로, Object 타입으로 객체를 생성하여 받은 후 SerialDTO로 형 변환을 하면 된다.[ Object의 경우 모든 클래스의 부모이므로 부모가 되고, SerialDTO 경우 자식이 된다. 부모가 자식을 상속 받기 위해선 ()를 이용하여 형변환(캐스팅)을 해야 한다. ]
그리고 이와같이 한줄로도 표현이 가능하다.
SerialDTO dto = (SerialDTO) ois.Object();
결과는 아래와 같이 나오게 된다. 즉, 객체를 정상적으로 읽었다는 의미다.
SerialDTO [ bookName=GodofJavaBook, bookOrder=1, bestSeller=true, soldPerDay=100]
만약 Serializable 객체가 변경되었을 때 결과는 어떻게 될까? [ SerialDTO 클래스에 변수를 추가하여 확인해보자 ]
package part27;
import java.io.Serializable;
public class SerialDTO implements Serializable{
private String bookType="IT"
// 중간 부분 생략
}
위와 같이 코드를 변경하고 나서 객체를 읽는 ManageObject 클래스의 loadObject()메소드를 실행시켜보자.
java.io.InvalidClassException: part27.SerialDTO; local class incompatible: stream classdesc serialVersionUID = -4780259690429963542, local class serialVersionUID = -2239209548807746573
at java.base/java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:728)
at java.base/java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:2060)
at java.base/java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1907)
at java.base/java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2209)
at java.base/java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1742)
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:514
at java.base/java.io.ObjectInputStream.readObject(ObjectInputStream.java:472)
at part27.ManageObject.loadObject(ManageObject.java:56)
at part27.ManageObject.main(ManageObject.java:18)
위의 결과를 보게되면 serialVersionUID가 다르다는 InvalidClassException 예외 메시지가 출력된다.
변수가 추가 되는 등 Serializable 객체의 형태가 변경되면 컴파일시 serialVersionUID가 다시 생성되므로 이와 같은 문제가 발생한다.
==> 이러한 상황에서 예외가 발생하지 않도록 하려면 SerialDTO 클래스에 serialVersionUID를 다음과 같이 추가하자.
package part27;
import java.io.Serializable;
public class SerialDTO implements Serializable{
static final long serialVersionUID=1L;
private String bookType="IT"
// 중간 부분 생략
@Override
public String toString() {
// TODO Auto-generated method stub
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 part27;
import java.io.Serializable;
public class SerialDTO implements Serializable{
static final long serialVersionUID=1L;
private String bookTypes="IT"
// 중간 부분 생략
@Override
public String toString() {
// TODO Auto-generated method stub
return "SerialDTO [ bookName=" + bookName + ", bookOrder=" + bookOrder
+ ", bestSeller=" + bestSeller + ", soldPerDay=" + soldPerDay
+ ", bookType=" + bookTYpes+ "]";
}
}
위와 같이 코드를 변경하고 ManageObject클래스의 main() 메소드에서 saveObject()가 실행되지 않도록 주석처리 후 실행해보자.
SerialDTO [ bookName=GodofJavaBook, bookOrder=1, bestSeller=true, soldPerDay=100, bookTypes=null]
결과를 보게 되면, 변수의 이름이 바뀌어(bookType -> bookTypes) 저장되어 있는 객체에서 찾지 못하므로, 해당 값은 null로 처리된다.
==> 즉, Serializable을 구현해 놓은 상황에서 serialVersionUID를 명시적으로 지정하면 변수가 변경되더라도 예외는 발생하지 않는다.
하지만, 만약, 이렇게 Serializable한 객체의 내용이 바뀌었는데도 아무런 예외가 발생하지 않으면 운영 상황에서 데이터가 꼬일 수 있기 때문에 절대 권장하는 코딩 방법이 아니다.
따라서, 이렇게 데이터가 바뀌면 serialVersionUID의 값을 변경하는 습관을 가져야만 데이터에 문제가 발생하지 않는다. [ 앞의 예제처럼 변수가 변경될 경우 null이 떠서 데이터가 꼬이는데 이것을 serialVersionUID의 값을 변경시켜 새롭게 객체를 생성(동기화)시켜줌으로써 예방할 수 있다. ]
■ 4. transient라는 예약어는 Serializale과 떨어질 수 없는 관계다 ■
앞의 예제에서 SerialDTO의 bookOrder 선언문 앞에 transient라는 예약어를 추가하자.
transient private int bookOrder;
그리고 객체를 저장하고 읽어 오도록 ManageObject 클래스의 main() 메소드의 saveObject() 및 loadObject()를 실행하자.
결과는 다음과 같다.
Write Success
SerialDTO [ bookName=GodofJavaBook, bookOrder=0, bestSeller=true, soldPerDay=100, bookType=IT]
여기서 bookOrder 값을 보자. 분명히 SerialDTO의 객체를 생성할때 생성자의 bookOrder 부분에 1로 지정하고 저장하였다.
하지만 읽어낸 값을 살펴보면 0이 출력된 것을 볼 수 있다. 왜 이런 현상이 발생했을까?
==> 객체를 저장하거나, 다른 JVM으로 보낼 때, transient라는 예약어를 사용하여 선언한 변수는 Serializable의 대상에서 제외된다.
즉, 다시 말해서, 해당 객체는 저장 대상에서 제외되어 버린다.
무시할꺼면 왜 이런 변수를 만들지? 라는 생각을 할 것이다. [ 지금 상황에서는 이 변수를 사용하는데 전혀 문제 없긴 하다. 그냥 예제로만 보여준 것이다. ]
==> 예를들어, 패스워드를 보관하고 있는 변수가 있다고 생각해보자. 이 변수가 저장되거나 전송된다면 보안상 큰 문제가 발생할 수 있다. 따라서, 이렇게 보안상 중요한 변수나 꼭 저장해야 할 필요가 없는 변수에 대해서는 transient를 사용할 수 있다.
■ 5. NIO란 ? ■
NIO(New Input Output)가 생긴 이유는 단 하나다.
==> 속도 때문이다.
NIO는 IO에서 사용한 스트림을 사용하지 않고, 대신 채널과 버퍼를 사용한다.
채널은 물건을 중간에서 처리하는 도매상이라고 생각하면 된다. 그리고, 버퍼는 도매상에서 물건을 사고, 소비자에게 물건을 파는 소매상으로 생각하면 된다. [ 대부분 소매상을 통해서 거래를 하기 때문에 도매상과 이야기할 일이 없다. ]
즉, 자바에서 NIO에서 데이터를 주고 받을 때에는 버퍼를 통해서 처리한다.
예제를 통해 살펴보자.
[EX] - NIO
package part27;
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="\\godofjava\\text\\nio.text";
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(); //1
byte [] byteData =data.getBytes();
ByteBuffer buffer =ByteBuffer.wrap(byteData); //2
channel.write(buffer); //3
channel.close();
}
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.println((char)buffer.get()); //9
}
channel.close();
}
}
my first NIO sample.
위의 예제에서 writeFile()과 readFile() 메소드 위주로 알아보겠다.
writeFile()에 있는 주석을 차례로 보자.
- 파일을 쓰기 위한 FileChannel 객체를 만들려면, 이와 같이 FileOutputStream 클래스에 선언된 getChannel()이라는 메소드를 호출한다.
- ByteBuffer 클래스에 static으로 선언된 wrap()이라는 메소드를 호출하면, ByteBuffer 객체가 생성된다. 이 메소드의 매개 변수는 저장할 byte의 배열을 넘겨주면 된다.
- FileChannel 클래스에 선언된 write() 메소드에 buffer 객체를 넘겨주면 파일에 쓰게 된다.
readFile()에 있는 주석을 차례로 보자.
- 파일을 읽기 위한 객체도 FileInputStream 클래스에 선언된 getChannel()apthemfmf tkdydgkaus ehlsek.
- ByteBuffer 클래스에 static으로 선언되어 있는 allocate()메소드를 통해서 buffer라는 객체를 만들었다. 여기서의 매개변수는 데이터가 기본적으로 저장되는 크기를 의미한다.
- 채널의 read()메소드에 buffer 객체를 넘겨줌으로써, 데이터를 이 버퍼에다 담으라고 알려준다. 이렇게 알려주기만 하면 buffer에는 데이터가 담기기 시작한다.
- flip()메소드는 buffer에 담겨있는 테이터의 가장 앞으로 이동한다.
- get()메소드를 호출하면 한 바이트씩 데이터를 읽는 작업을 수행한다.
- ByteBuffer에 선언되어 있는 hasReamaninig()메소드를 사용하여 데이터가 더 남아 있는지를 확인하면서 반복작업을 수행한다.
다른 IO 관련 클래스들처럼 마지막에 close()메소드를 호출하여 Channel을 닫아야 한다.
결과는 아래와 같다.
my first NIO sample.
파일 데이터를 다룰 때에는 ByteBuffer라는 버퍼와 FileChannel이라는 채널을 사용하면 간단히 처리할 수 있다.
Channel의 경우 그냥 간단하게 FileInputStreamr과 FileOupputSteam를 이용하여 객체를 생성하여 read()나 write()메소드만 불러주면 된다고 생각하면 된다.
■ 5-1. NIO의 Buffer 클래스
NIO에서 제공하는 Buffer는 java.nio.Buffer 클래스를 확장하여 사용한다.
Buffer 클래스의 종류로는 CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer 등이 존재한다.
이러한 Buffer 클래스의 상태 및 속성을 확인하기 위한 메소드는 다음과 같다.
여기서 position이라는 말이 나오는데, 버퍼는 위치가 있다. 버퍼에 데이터를 담거나, 읽는 작업을 수행하면 현재의 "위치"가 이동한다.
그래야 그 다음 "위치"에 있는 것을 바로 쓰거나, 읽을 수 있기 때문이다.
- position() : 현재의 "위치"를 나타냄
- limit() : 읽거나 쓸 수 없는 "위치"를 나타냄
- capacity() : 버퍼의 크기를 나타내는 것
이 3개 값의 관계는 다음과 같다.
0 <= position <= limit <= 크기(capacity)
NIO를 제대로 이해하려면 이 세 개 값의 관계를 꼭 이해하고, 기억해야만 한다.
[EX] - 버퍼의 상태 및 속성을 확인하는 capacity(), limit(), position()
package part27;
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
위의 코드에서 숫자로 처리된 주석을 보자.
- capacity() 메소드의 결과는 버퍼의 크기를 나타낸다.
- limit의 pisition을 별도로 지정하지 않았으므로, 이 값의 기본 크기인 1024이다.
- 데이터를 추가한 후 버퍼의 positiondms 100이 된다.
- flip이라는 메소드를 호출한 다음에는 limit 값은 100이 되고, position 값은 0이다. CD플레어이에 되감기 버튼을 누르면 맨 앞으로 이동한다. 그런 작업을 하는 것이 바로 flip 메소드다.
앞에서 살펴본 position, limit, capacity의 관계에 mark를 추가하면 다음과 같다.
0 <= mark <= position <= limit <= 크기(capacity)
그러면 위치를 변경하는 메소드를 알아보자.
언뜻 보면 flip()메소드와 rewind()메소드가 비슷해보인다. 하지만, flip()은 limit값을 변경하지만, rewind()는 limit 값을 변경하지 않는다.
remaining()메소드나 hasRemaining()메소드를 사용하면 limit까지만 데이터를 읽을 수 있다.
mark() 메소드를 사용하여 특정 위치를 표시해 두고 다시 읽을 필요가 있을 때 rewind()메소드를 사용한다.
예제를 보며 확인해보자.
[EX] - 결과를 출력하는 메소드
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());
}
[EX] - Buffer에서 위치를 변경하는 메소드
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();
}
}
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
위의 코드를 보자.
하나의 데이터를 읽고(get), 위치를 표시하고(mark), 다시 읽고(get), 표시한 position[mark]으로 다시 이동한 후(reset), 처음으로 이동하고(rewind), 데이터를 지우는(clrear)작업을 수행했다.
NIO에 대해 알아보았다. NIO는 단지 파일을 쓰고 읽을 때에만 사용하는 것이 아니라, 파일 복사를 하거나, 네트워크로 데이터를 주고 받을 때에도 사용할 수 있다.
Serializable인터페이스와 NIO는 자바 개발자라면 꼭 알고 있어야 하는 내용이다.
'JAVA > 자바의신 2' 카테고리의 다른 글
25장 쓰레드는 개발자라면 알아두는 것이 좋아요 (0) | 2022.09.19 |
---|---|
24장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part3(Map) (1) | 2022.09.19 |
23장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part2(Set과 Queue) (0) | 2022.09.18 |
22장 자바랭 다음으로 많이 쓰는 애들은 컬렉션 - Part1(List) (0) | 2022.09.18 |
21장 실수를 방지하기 위한 제네릭 (0) | 2022.09.17 |