Serializable
java.io 패키지 Serializable 인터페이스
API를 보면 선언된 변수나 메소드가 없다
이런 아무런 구현해야 할 메소드고 없는 이 인터페이스가 도대체 왜 있는 것일까?
개발하다 보면, 생성한 객체를 파이로 저장할 일이 있을 수도 있고, 저장한 객체를 읽을 일이 생길 수도 있다, 그리고, 객체를 다른 서버로 보낼 떄도, 다른 서버에서 생성한 객체를 받을 일도 생길 수 있다
그럴 때 꼭 필요한 것이 Serializable 이다
클래스가 파일에 읽거나 쓸 수 있도록 하거나, 다른 서보로 보내거나 받을 수 잌있도록 하려면
반드시 이 인터페이스를 implements 해야만 한다
Serializable 인터페이스를 구현 하면 JVM에서 해당 객체는 저장하거나 다른 서버로 전송할 수 있도록 해 준다
Serializable 인터페이스를 구현한 후 serialVesionUID라는 값을 지정해 주는 것을 권장한다
별도로 지정하지 않으면, 자바 소스가 컴파일될때 자동으로 생성된다.
static final long serialVersionUID = 1L;
반드시static final long 으로 선언, 변수명도 serialVesionUID로 선언해 주어야만
자바에서 "아~ 이 값을 이렇게 설정했군요, 알았어요."라고 생각한다
어떤 값으로 지정해주면 될까??
아무런 값이나 지정해주면 된다 단, 필요에 따라서 값을 변경해야 하는 경우가 발생한다
serialVersionUID 값은 해다 객체의 버전을 명시하는 데 사용
각 서버가 쉽게 해당 객체가 같은지 다른지를 확인할 수 있도록 하기 위해서 serialVersionUID로 관리를 해주어야만 함
클래스 이름이 같더라도, 이 ID가 다르면 다른 클래스라고 인식, 같은 UID라고 할지라도, 변수의 개수나 타입 등이 다르면 이 경우도 다른 클래스로 인식함
객체를 저장해 보자
객체를 저장하고, 저장한 객체를 읽어 들이는 작업
ObjectOutputStream 클래스를 사용하면 객체를 저장할 수 있다
ObjectInputStream 클래스를 사용하면 저장해 놓은 객체를 읽을 수 있다
ObjectOutputStream으로 객체를 저장하는 것을 알아보자
먼저 저장할 객체를 만들자
DTO를 만들었다
보통은 다음과 같이 DTO를 만든다
getter, setter 메소드도 만들어야하지만, 지금은 딱히 필요가 없으므로 일단 이렇게만 만드어 두었다
이제 이 객체를 저장하는 클래스를 만들자
main() 메소드에서 fullPath라는 이름으로 저장할 파일의 위치와 파일명 저장
and, SerialDTO 클래스의 객체 생성 후
saveObject() 메소드의 매개 변수로 전달
saveObject()의 내용
코드의 주요 부분을 살펴보자
먼저 FileOutputStream 객체를 생성
객체를 저장하기 위해 ObjectOutputStream 객체 생성후 FileOutputStream 객체를 매개 변수로 전달해 해당 객체를 파일에 저장
writeObject() 메소드를 사용해 객체를 저장
ObjectOutputStream에서는 write()를 사용해서 int 값을 저장하고
writeByte()로 바이트 값을 저장한다이 외에도 각 타입별로 데이터를 저장하도록 되어 있다
자세한 내용은 API 문서를 참고하고
일단, 여기에서는 writeObject() 메소드를 사용해서 객체 값을 저장했다
실행결과는
NotSerializableException이 발생한다
SerialDTO의 선언 부분에 Serializable을 구현하지 않아서 발생한 Exception이다
이렇게 변경하고 실행해보면
결과는 다음과 같다
Write Success
가 출력되고
path로 지정했던 디렉터리에 path에 지정했던 이름을 가진 파일이 생성되어 있을 것이다
이 파일은 일반 텍스트 편집기로 열어서 보기는 어려울 것이다
왜냐하면
자바 객체가 바이너리로 저장되어 있기 때문이다
객체를 읽어보자
저장한 객체를 읽어보자
Input으로 되어 있는 클래스들을 사용하면 된다
ObjectInputStream에서는 read로 시작하는 메소드를 사용한다
객체를 읽을 때에는 readObject() 메소드를 사용하면 된다
이 메소드의 리턴 타입이 Object이므로 Object로 받은 후에 SerialDTO로 형 변환을 하면 된다
다음과 같이 한 줄로 표현해도 무방하다
SerialDTO dto = (SerialDTO)ois.readObject();
main() 메소드는 다음과 같이 수정하자
실행 결과는
SerialDTO의 toString() 메소드를 구현해 놓았기 때문에 다음과 같이 읽어 들인 객체의 내용이 출력되는 것을 볼 수 있다
즉, 객체를 정상적으로 일었다는 의미
Serializable 객체가 변경되었을 때 어떤 상황이 연출되는지 확인해보자
SerialDTO 클래스에 변수를 추가함으로써
(import java.io.Serializable이 제 실수로 빠져있습니다 있다고 가정하고 생각하십쇼)
이제 저장되어 있는 객체를 읽는 ManageObject 클래스를 다시 실행해보자
결과는
InvalidClassException 예외 메시지가 출력된다
변수가 추가되는 등 Serializable 객체의 형태가 변경되면 컴파잀시
serialVersionUID가 다시 생성되므로, 이와 같은 문제가 발생한다
InvalidClassException 이 발생되지 않도록 하려면 SerialDTO 클래스에 serialVersionUID를 추가하면 된다
(import java.io.Serializable이 제 실수로 빠져있습니다 있다고 가정하고 생각하십쇼)
saveObject() 메소드를 실행할수 있도록 주석을 푼 후에 다시 실행해보자, 그러면 아무런 예외 없이 실행이 가능하다
그런데, serialVersionUID를 지정해 놓은 상태에서 저장되어 있는 객체와 읽는 객체가 다르면 어떻게 될까?
bookTyep의 변수명을 bookTypes로 변경함으로써
(import java.io.Serializable이 제 실수로 빠져있습니다 있다고 가정하고 생각하십쇼)
main() 메소드에서 saveObject()가 실행되지 않도록 주석처리 후 다시 실행하자
앞서 저장되어 있는 SerialDTO 객체에는 bookType이라는 변수가 있었다. 따라서 bookTyeps라는 변수는 존재하지 않는다
이 경우 다음과 같이 결과가 출력된다
SerialDTO [bookName = GodOfJavaBook, bookOrder = 1, bestSeller = true, soldPerDay = 100, bookTypes = null]
변수의 이름이 바뀌면 저장되어 있는 객체에서 찾지 못하므로, 해당 값은 null로 처리된다
Serializable을 구현해 놓은 상황에서 serilalVersionUID를 명시적으로 지정하면 변수가 변경되더라도 예외는 발생하지 않는다
하지만, 이렇게 Serializable한 객체의 내용이 바뀌었는데도 아무런 예외가 발생하지 않으면 운영 상황에서 데이터가 꼬일 수 있기 때문에 절대 권장하는 코딩 방법이 아니다.
따라서,
이렇게 데이터가 바뀌면 serialVersionUID의 값을 변경하는 습관을 가져야만 데이터의 문제가 발생하지 않는다
transient
SerialDTO의 bookOrder 선언문 앞에 transient라는 예약어를 추가하자
객체를 저장하고 읽어 오도록 main() 메소드의 saveObject() 호출 부분이 실행되도록 되어 있는 상태에서 실행하자
실행 결과는 다음과 같다
SerialDTO [bookName = GodOfJavaBook, bookOrder = 0, bestSeller = true, soldPerDay = 100, bookTypes = IT]
우리는 분명히 이 bookOrder 값을 1로 지정하고 저장했다
하지만, 읽어낸 값을 살펴보면 0이 출력되어있다
왜??
객체를 저장하거나, 다른 JVM으로 보낼 때, transient 에약어를 사용하여 선언한 변수는 Serializable의 대상에서 제외된다
다시 말해서, 해당 객체는 저장 대상에서 제외되어 버린다
"무시할꺼면 뭣하러 이 변수를 만들어?"
보안상 중요한 변수나 꼭 저장해야 할 필요가 없는 변수에 대해서는 transient를 사용할 수 있다
NIO
JDK 1.4에서부터 NIO (New IO) 추가
NIO가 생긴 이유는 단 하나. 속도 때문
간단히 NIO에 대한 개요를 잡는 정도만 살펴보자
NIO는 스트림을 사용하지 않고 채널Channel과 버퍼Buffer를 사용한다
체널은 물건을 중간에서 처리하는 도매상, 버퍼는 도매상에서 물건을 사고, 소비자들에게 물건을 파는소매상으로 생각하면 됌.
NIO에서 데이터를 주고 받을 때에는 버퍼를 통해서 처리한다
writeFile()과 readFile() 메소드 위주로 자세히 알아보자
writeFile()
파일을 쓰기 위한 FileChannel 객체를 만들려면
FileOutputStream 클래스에 선언된 getChannel()이라는 메소드를 호출한다
ByteBuffer 클래스에 static 으로 선언된 wrap() 메소드를 호출하면
ByteBuffer 객체가 생성된다 메소드의 매개 변수는 저장할 byte의 배열을 넘겨주면 된다
FileChannel 클래스에 선언된 write() 메소드에 buffer 객체를 넘겨주면 파일에 쓰게 된다
readFile()
파일을 읽기 위한 FileChannel 객체를 만들기 위해
FileInputStream 클래스에 선언된 getChannel() 메소드를 호출하고
ByteBuffer 클래스에 선언되어 있는 allocate() 메소드를 통해서 buffer라는 객체를 만들었다
매개 변수는 데이터가 기본적으로 저장되는 크기를 의미한다
read() 메소드에 buffer 객체를 넘겨줌으로써, 데이터를 이 버퍼에다 담으라고 알려준다
알려주기만 하면 buffer에는 데이터가 담기기 시작한다
flip() 메소드를 호출해 buffer에 담겨있는 데이터의 가장 앞으로 이동한다
get() 메소드를 호출하면 한 바이트씩 데이터를 읽는 작업을 수행한다
ByteBuffer에 선언되어 있는 hasRemaining() 메소드를 사용해서 데이터가 더 남아 있는지를 확인하면서 반복작업을 수행한다
IO 관련 클래스들처럼 마지막에 close() 메소드를 호출하여 Channel을 닫아야 한다
결과는 다음과 같다
My first NIO sample.
파일 데이터를 다룰 때에는 ByteBuffer라는 버퍼와 FileChannel이라는 체널을 사용하면 간단히 처리할 수 있다
Channel의 경우 그냥 간단하게 객체만 생성하여 read()나 write() 메소드만 불러주면 된다고 생각하면 된다
그런데, Buffer 클래스는 간단하게 되어있지 않으므로 자세히 살펴보자
Buffer of NIO
NIO의 Buffer는 java.nio.Buffer 클래스를 확장하여 사용
ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer등이 존재
이러한 Buffer 클래스(이하 버퍼)에 선언되어 있는 버퍼의 상태 및 속성을 확인하기 위한 메소드
리턴 타입 | 메소드 | 설명 |
int | capacity() | 버퍼에 담을 수 있는 크기 리턴 |
int | limit() | 버퍼에서 읽거나 슬 수 없는 첫 위치 리턴 |
int | position() | 현재 버퍼의 위치 리턴 |
버퍼는 CD처럼 위치가 있다
버퍼에 데이터를 담거나, 읽는 작업을 수행하면 현재의 "위치"가 이동한다
그래야 다음 "위치"에 있는 것을 바로 쓰거나, 읽을 수 있기 때문
따라서,
- 현재의 "위치"를 나타내는 메소드가 position()이고
- 읽거나 쓸 수 없는 "위치"를 나타내는 메소드가 limit()
- 버퍼의 크기(capacity)를 나타내는 것이 capacity()
메소드다
이 3개 값의 관계는 다음과 같다
0 <= position <= limit <= 크기(capacity)
NIO를 제대로 이해하려면 이 세 개 값의 관계를 꼭 이해하고, 기억해야만 한다
위 세 개의 메소드를 써보자
capacity() 메소드의 결과는 쉽게 이해할 것
limit의 position을 별도로 지정하지 않았으므로, 이 값은 기본 크기인 1024이다
데이터를 추가한 후 버퍼의 position은 100이 된다
flip() 이라는 메소드를 호출한 다음 limit값은 100이 되고 position 값은 0이다
CD플레이어에 되감기 버튼을 누르면 맨 앞으로 이동한다. 그런 작업을 수행하도록 하는 것이 flip() 메소드다
결과는 다음과 같다
버퍼의 위치를 변경하는 메소드
리턴 타입 | 메소드 | 설명 |
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값을 버퍼의 크기로 변경 |
위 메소드들을 활용해 보자
어떠한 작업을 한 이후에 position, remainin, limit 값들을 출력하도록 했다
하나의 데이터를 읽고(get), 위치를 표시하고(mark), 다시 읽고(get), 표시한 position으로 다시 이동한 후(reset), 처음으로 이동하고(rewind), 데이터를 지우는(clear) 작업을 수행하도록 했다
결과는 다음과 같다
NIO는 단지 파일을 쓰고 읽을 때에만 사용하는것이 아니라, 파일 복사를 하거나, 네트워크로 데이터를 주고 받을 때에도 사용할 수 있다
자세한 NIO에 대한 내용은 NIO에 대해서만 정리해 놓은 서적이나 문서를 참고하기 바란다
-
댓글