[자바]객체 직렬화(Serialization) 와 역직렬화(Deserialization)

allnight5·2022년 12월 22일
0

자바

목록 보기
8/12

참조 사이트 1
참조 사이트 2
참조 사이트 3
스프링 406에러 참조사이트

왜 데이터를 그냥 사용하면 안 되고 직렬화라는 과정을 거쳐야 하는 것일까?

직렬화가 필요한 이유


개발 언어를 무엇을 선택하든, 사용하는 데이터의 메모리 구조는 크게 2가지로 나뉜다.

  • 값 형식 데이터
    • int, float, char 등 값 형식 데이터는 스택에 메모리가 쌓이고 직접 접근이 가능하다.
  • 참조 형식 데이터
    • 객체와 같은 참조 형식 변수를 선언하면 힙에 메모리가 할당되고, 스택에서는 이 힙 메모리를 참조하는 구조로 되어 있다.

위 두 가지 데이터 중에서 디스크에 저장하거나 통신할 때는 값 형식 데이터만 사용할 수 있다. 참조 형식 데이터는 실제 데이터 값이 아닌 힙에 할당되어 있는 메모리 번지 주소를 가지고 있기 때문이다.

참조 형식 데이터를 사용할 수 없는 이유

예를 들어, 객체 A를 만들고 주소 값이 0x00045523라고 가정하자. 그리고 이 값을 파일에 포함하여 저장했다고 해보자. 이후 프로그램을 종료하고 다시 실행해서 주소 값 0x00045523을 가져오더라도 기존 A 객체의 데이터를 가져올 수 없다. 프로그램이 종료되면 기존에 할당되었던 메모리(0x00045523)는 해제되고 없어지기 때문이다.
네트워크 통신 또한 마찬가지이다. 각 PC마다 사용하고 있는 메모리 공간 주소는 전혀 다르다. 그러므로 내가 다른 PC로 전송한 A 객체 데이터(0x00045523)는 무의미하다. 이 데이터를 받은 PC의 메모리 주소 0x00045523에는 전혀 다른 값이 존재하기 때문이다.

그러니까 그냥 참조 형식 데이터(메모리번지 주소)만 보내게 된다면 할당되어있는 데이터저장소가 사라지기 때문에 전혀 값이없거나 할당위치가 다르기 때문에 다른값이 나오게 되서 원하는 값이 도착하지 않는다.

그래서 직렬화를 왜 하는가?

직렬화가 된 데이터는 언어에 따라서 텍스트 또는 바이너리 등의 형태가 되는데, 이러한 형태가 되었을 때 저장하거나 통신할 때 파싱이 가능한 유의미한 데이터가 된다.
즉, 직렬화를 하는 이유는 사용하고 있는 데이터를 파일 저장 혹은 데이터 통신에서 파싱할 수 있는 유의미한 데이터를 만들기 위함이다.

데이터 직렬화의 종류


  • CSV, XML, JSON 직렬화
    • 사람이 읽을 수 있는 형태.
    • 저장 공간의 효율성이 떨어지고, 파싱하는 시간이 오래 걸림.
    • 데이터의 양이 적을 때 주로 사용함.
    • 최근에는 JSON 형태로 통해 데이터를 직렬화를 많이 함.
    • 모든 시스템에서 사용이 가능함.
  • Binary 직렬화
    • 사람이 읽을 수 없는 형태.
    • 저장 공간을 효율적으로 사용할 수 있고, 파싱하는 시간이 빠름.
    • 데이터의 양이 많을 때 주로 사용함.
    • 모든 시스템에서 사용이 가능함.
    • ex) 프로토콜 버퍼, Apache Avro 등
  • Java 직렬화
    • Java 시스템 간의 데이터 교환이 필요할 때 사용함.

Java에서의 직렬화(Serialization)


  • 객체/데이터를 바이트 형태로 변환하여 네트워크를 통해 송수할 수 있도록 만드는 것
  • 다른 말로는 말그대로 객체를 직렬화하여 전송 가능한 형태로 만드는 것을 의미한다. 객체들의 데이터를 연속적인 데이터로 변형하여 Stream을 통해 데이터를 읽도록 해준다.
  • 이것은 주로 객체들을 통째로 파일로 저장하거나 전송하고 싶을 때 주로 사용된다.

--

그러나!!!자바에서는 전제조건이있다.

  • 직렬화를 위한 전제조건이 있다. 바로 직렬화가 가능한 클래스를 먼저 만드는 것이다.

public class A {
...
}
아래와같이 해주면된다. implements Serializable 추가해주거나
public class A implements Serializable {
...
}
아래와 같히 해주면 된다.

 Member member = new Member("김배민", "deliverykim@baemin.com", 25);
 byte[] serializedMember;
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(member);
            // serializedMember -> 직렬화된 member 객체
            serializedMember = baos.toByteArray();
        }
    }

Java에서의 역직렬화(Deserialization)


직렬화된 파일 등을 역으로 직렬화하여 다시 객체의 형태로 만드는 것을 의미한다. 저장된 파일을 읽거나 전송된 스트림 데이터를 읽어 원래 객체의 형태로 복원한다.

역직렬화 조건


  • 직렬화 대상이 된 객체의 클래스가 class path에 존재해야 하며 import되어 있어야 한다.
    • class path란?
      • JVM이 프로그램을 실행할 때, class 파일을 찾는 데 기준이 되는 파일 경로
  • 자바 직렬화 대상 객체는 동일한 serialVersionID를 가지고 있어야 한다.

직렬화 해보기(참조사이트1에서 가져옴)


User클래스


public class User implements Serializable{
    private String name;
    private transient String password;
    private String email;
    public int age;
    
    public User(String name, String password, int age) {
        this.name = name;
        this.password = password;
        this.age = age;
    }
    
    public String toString() {
        return "(" + name + ", " + password + ", " + email + ", " + age + ")";
    }
}

직렬화 & 역직렬화


public class MainClass {
    private static final String USERINFO_SER = "user.ser";
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        conductSerializing();
        conductDeserializing();
    }
    
    public static void conductSerializing() {
        try {
            FileOutputStream fos = new FileOutputStream(USERINFO_SER);
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            ObjectOutputStream out = new ObjectOutputStream(bos);
            
            User u1 = new User("이방원", "1234", "lby@abc.com", 30);
            User u2 = new User("무휼", "8877", "mh1398@abc.com", 25);
            
            ArrayList list = new ArrayList<>();
            list.add(u1);
            list.add(u2);
            
            out.writeObject(u1);
            out.writeObject(u2);
            out.writeObject(list);
            out.close();
            System.out.println("직렬화 완료");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void conductDeserializing(){
        try {
            FileInputStream fis = new FileInputStream(USERINFO_SER);
            BufferedInputStream bis = new BufferedInputStream(fis);
            ObjectInputStream in = new ObjectInputStream(bis);
            
            User u1 = (User) in.readObject();
            User u2 = (User) in.readObject();
            ArrayList list = (ArrayList) in.readObject();
            
            System.out.println(u1.toString());
            System.out.println(u2.toString());
            System.out.println("count :: " + list.size());
            System.out.println(list.toString());
            
            in.close();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

conductSerializing() 메서드에서 직렬화를 수행하여 user.ser 파일을 생성하고 이를 읽어서 바로 역직렬화 한다. 여기서 눈여겨 볼 것은 실행 결과이다.
역직렬화 결과를 보면 직렬화한 순서 그대로 출력됨을 알 수 있다. 즉, 직렬화와 역직렬화 할 때 순서가 매우 중요하다는 것이다!!
매번 순서를 고려해야하고 하나라도 맞지않으면 역직렬화에 실패한다. 같은 객체를 여러번 보내기보다 ArrayList와 같은 자료구조로 한번에 넣는 것이 더욱 효율적이다.

하지만 User가 계속 유지되면 좋겠지만 변수가 추가되거나 달라질수있다. 그럴때마다 다른곳들도 바꿔줘야하는문제점이 있는데 이러한 문제점을 해결하기 위해서 User에서 SerialVersionUID를 맨위쪽에 더해주는것이다.

public class User implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

1L은원하는 대로 변경해주면된다.
이렇게 선언한다면 추후에 User클래스에 변경이 생겨도 여전히 1이기 때문에 역직렬화를 성공적으로 할수있다.

순환 참조.


JPA에서 양방향으로 연결된 엔티티를 JSON 형태로 직렬화하는 과정에서, 서로의 정보를 계속 순환하며 참조하여 StackOverflowError 를 발생시키는 현상

1. Entity 대신 DTO 로 반환


가장 추천하는 방식이다.
Entity 클래스는 데이터베이스와 맞닿는 핵심 클래스이다.
Entity 클래스를 기준으로 수많은 클래스나 비즈니스 로직들이 동작하고 있다.
Entity 클래스를 통해 여러 클래스들이 영향을 받을 수 있으므로 Entity 클래스를 Request/Response 클래스로 사용하는 것은 강력하게 추천하지 않는다.
컨트롤러에서 Response 값으로 여러 테이블을 조인해야하는 경우가 많으므로, DB Layer 와 View Layer 의 역할 분리를 철저하게 해주자.
역시나 객체지향 설계에 있어서 역할과 책임은 중요한 요소인 것 같다.

2. @JsonManagedReference & @JsonBackReference


양방향 관계에서 직렬화 방향을 설정하여 순환 참조를 해결할 수 있도록 설계된 애노테이션

@JsonManagedReference

연관관계 주인 반대 Entity 에 선언
정상적으로 직렬화 수행

@JsonBackReference

연관관계의 주인 Entity 에 선언
직렬화가 되지 않도록 수행

3. @JsonIgnore


양방향 관계를 가지고 있는 두 엔티티 중 하나의 엔티티의 참조 필드에 직렬화를 제외시키는 방법 JSON 직렬화 과정에서 해당 애노테이션이 선언된 필드는 직렬화 대상에서 제외 해당 필드가 직렬화에 필요할 경우에는 적합하지 않은 방법

정리

  • 직렬화 가능한 클래스들의 상태를 확인하라.
  • 직렬화, 역직렬화 할 경우 객체들의 순서가 중요하다. ArrayList 등을 활용하면 손쉽게 할 수 있다.
  • SerialVersionUID 고유의 번호를 관리하라. 자동으로 생성해주지만 직접 관리하는 것을 권장.
profile
공부기록하기

0개의 댓글