인생을 코딩하다.

[Java] Serializable과 NIO 본문

Java

[Java] Serializable과 NIO

Hyung1 2020. 11. 25. 21:43
728x90
반응형

렬화(Serialization)

  • 인스턴스의 상태를 그대로 저장하거나 네트워크로 전송하고 이를 다시 복원하는 (Deserialization)하는 방식
  • ObjectInputStream과  ObjectOutputStream 시용
  • 보조스트림
  • 객체의 상태를 영속화 하는 메커니즘
  • 객체를 다른 환경(File, db)에 저장했다가 나중에 재구성 할 수 있게 만드는 과정

언제쓸까?

  • 객체의 상태를 영속해야 할 필요가 있을때 , 즉 어딘가에 저장해야 할 필요가 있을때 (여기서 저장이라는 것은 파일이나 데이터베이스가 될 수도 있고, 캐시와 같은 메모리가 될 수도 있다.)
  • 정보를 전달할 필요가 있을 때 (다른 VM (버츄얼머신) 에게 객체의 정보를 전송해야 할 시에 바이트 스트림으로 변환해서 전송해야 할 때)

Serializable 인터페이스

  • 직렬화는 인스턴스의 내용이 외부(파일, 네트워크)로 유출되는 것이므로 프로그래머가 객체의 직렬화 가능 여부를 명시함
  • 구현 코드가 없는 mark interface
  • Serializable 인터페이스룰 구현한 후에는 다음과 같이 serialVersionUID라는 값을 지정해주는 것을 권장한다 우리가 별도로 지정하지 않으면, 자바 소스가 컴파일 될 때 자동으로
static final long serialVersionUID = 1L;

가 생성된다.  각 서버가 쉽게 해당 객체가 같은지 다른지를 확인할 수 있도록 하기위해 지정해준다. serialVersionUID를 명시적으로 지정하면 변수가 바뀌더라도 예외는 발생하지 않는다. 만약 Serializable한 객체의 내용이 바뀌었는데도 아무런 예외가 발생하지 않으면 운영 상황에서 데이터가 꼬일 수 있기 때문에 절대 권장하는 코딩 방법이 아니다. 따라서, 이렇게 데이터가 바뀌면 seriaVersionUID의 값을 변경하는 습관을 가져야만 데이터에 문제가 발생하지 않는다.

class Person implements Serializable { // implements Serializable -> 직렬화하겠다는 의도를 표시
...
String name;
String job;
...
}

ObjectInputStream &  ObjectOutPutStream

데이터를 파일에 직접 쓸 수는 없다. 어떤 데이터를 파일에 쓰고 읽는다고 하였을 때, 어떤 프로토콜을 만들어서 쓰고 읽을 수는 있겠지만, 이 바이너리 상태를 그대로 파일에다 쓰고 읽는 것은 이 클래스가 어떤 정보를 가지고 있는지, 이 클래스 이름이 뭔지도 다 들어가게 된다. 그렇게 됬을때는 우리가 파일에 직접 쓸 수는 없고 ObjectOutputStream(보조스트림)을 이용하여 쓰면 된다.

 

예제

package java8.stream.serizlization;

import java.io.*;
import java.security.spec.RSAOtherPrimeInfo;

class Person implements Serializable {
    String name, job;

    public Person(String name, String job) {
        this.name = name;
        this.job = job;
    }

    public String toString() {
        return name + "," + job;
    }
}

public class SerializationTest {
    public static void main(String[] args) {

        Person personJung = new Person("정형일", "개발자");
        Person personLee = new Person("이승진", "교수");

        try (FileOutputStream fos = new FileOutputStream("serial.dat");
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(personJung);
            oos.writeObject(personLee);
        } catch (IOException e) {
            System.out.println(e);
        }

        try (FileInputStream fis = new FileInputStream("serial.dat");
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            Person p1 = (Person) ois.readObject();  // writeObject로 썻으니까 readObject로 읽으면 된다. 반환값이 오브젝트로 되고, 안전하게 Person으로 다운캐스팅 해준다.
            Person p2 = (Person) ois.readObject();

            System.out.println(p1);
            System.out.println(p2);

        } catch (IOException e) {
            System.out.println(e);
        } catch (ClassNotFoundException e) {
            System.out.println(e);
        }
    }
}

파일에 썼다가 그대로 오브젝트로 읽어들였다. 이렇게 사용 할 수 있는게 Serialization이고 직렬화의 기능이다.

 ObjectInputStream을 이용하여 직렬화 한 후 직렬화된 정보를 ObjectOutputStream를 이용하여 역직렬화

 

Object -> writeObject -> DB, File, Memory -> readObject -> object

객체(Object)는 writeObject를 거쳐서 직렬화가 되어 어딘가에 보관되었다가 readObject 의해 역직렬화가 되어 객체로 재생성 됨

 

변수를 직렬화하지 않을 때는 transient 라는 키워드를 쓴다. 직렬화 할 수 없는 애들이 있다. ex) socket 

transient String job;

String job 앞에 transient를 붙여주면 아래의 결과가 나온다.

정형일,null
이승진,null

Process finished with exit code 0

 

직렬화는 시스템 내부적으로 많이 구현하고 있다.

transient라는 예약어는 Serializable과 떨어질 수 없는 관계다.

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

Externalizable

Serializable는 구현해야 하는 메서드가 없다. 그래서 마크 인터페이스라고 한다. 그에 비해 Externalizable는 구현해야 하는 메서드가 있다.

writeExternal와 readExternal가 있는데 직접 읽고 쓰는 걸 구현하는 메서드다.

package java8.stream.serizlization;

import java.io.*;
import java.security.spec.RSAOtherPrimeInfo;

class Person implements Externalizable {
    String name;
    transient String job;

    public Person(String name, String job) {
        this.name = name;
        this.job = job;
    }

    public String toString() {
        return name + "," + job;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
    ...
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
	...
    }
}

public class SerializationTest {
    public static void main(String[] args) {

        Person personJung = new Person("정형일", "개발자");
        Person personLee = new Person("이승진", "교수");

        try (FileOutputStream fos = new FileOutputStream("serial.dat");
             ObjectOutputStream oos = new ObjectOutputStream(fos)) {
            oos.writeObject(personJung);
            oos.writeObject(personLee);
        } catch (IOException e) {
            System.out.println(e);
        }

        try (FileInputStream fis = new FileInputStream("serial.dat");
             ObjectInputStream ois = new ObjectInputStream(fis)) {
            Person p1 = (Person) ois.readObject();  // writeObject로 썻으니까 readObject로 읽으면 된다. 반환값이 오브젝트로 되고, 안전하게 Person으로 다운캐스팅 해준다.
            Person p2 = (Person) ois.readObject();

            System.out.println(p1);
            System.out.println(p2);

        } catch (IOException e) {
            System.out.println(e);
        } catch (ClassNotFoundException e) {
            System.out.println(e);
        }
    }
}

 

자바 NIO(NEW IO)란?

JDK 1.4에서부터 NIO라는 것이 추가되었다. 이 NIO가 생긴 이유는 단 하나다. 속도 때문이다. NIO는 지금까지 사용한 스트림을 사용하지 않고, 대신 채널과 버퍼를 사용한다. 채널은 물건을 중간에서 처리하는 도매상이라고 생각하면 된다. 그리고, 버퍼는 도매상에서 물건을 사고, 소비자에게 물건을 파는 소매상으로 생각하면 된다. 파일 데이터를 다룰 때 ByteBuffer라는 버퍼와 FileChannel이라는 채널을 사용하면 stream을 사용하지 않고 간단히 처리할 수 있다. channel의 경우 그냥 간단하게 객체만 생성하여 read()나 write() 메소드만 불러주면 된다고 생각하면 된다. 그런데 Buffer 클래스는 간단하게 되어있지 않으므로 자세히 살펴보자

FileChannel channel = new FileInputStream(fileName).getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024); // 매개 변수는 데이터가 기본적으로 저장되는 크기
channel.read(buffer); // read() 메소드에 buffer 객체를 넘겨줌으로써, 데이터를 이 버퍼에다 담으라고 알려준다. buffer에는 데이터가 담기기 시작한다.
buffer.flip(); flip() 메소드를 buffer에 담겨있는 데이터의 가장 앞으로 이동한다.

 

NIO의 Buffer 클래스

NIO에서 제공하는 Buffer은 java.nio.Buffer 클래스를 확장하여 사용한다. 이러한 Buffer 클래스에 선언되어 있는 메소드는 flip()만 있는 것이 아니다. 먼저 버퍼의 상태 및 속성을 확인하기 위한 메소드를 살펴보자.

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

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

 

리턴 타입 메소드 설명
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값을 버퍼의 크기로 변경

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


그런데 직렬화는 잘 안쓴다 왜 잘 안쓸까? 

 

보안, 유지보수성, 테스트, 그 외 다수(Ex, 싱글톤 문제, 역직렬화 폭탄 등)의 문제가 있다.

 

보안 - 보이지 않는 생성자, readObject

class Person implements Externalizable {
    String name;
    transient String job;

    public Person(String name, String job) {
        this.name = name;
        this.job = job;
    }

사실 이것은 하나의 생성자와 다를게 없다. 그래서 보이지 않는 생성자라고도 불린다.

 

... 작성 중

728x90
반응형

'Java' 카테고리의 다른 글

[Java] Call by value의 메모리 관리 과정  (2) 2021.01.09
[Java] 열거형(enum)  (0) 2021.01.05
[Java] 보조 스트림  (0) 2020.11.24
문자 단위 입출력 스트림  (0) 2020.11.24
[Java] 바이트 단위 입출력 스트림  (0) 2020.11.23
Comments