JPA 엔티티의 복합 키는 Serializable이어야 하는 이유

    김영한 님의 <자바 ORM 표준 JPA 프로그래밍>을 읽다 보니, 복합 키를 매핑하는 @IdClass와 @EmbeddedId 식별자 클래스가 모두 Serializable 인터페이스를 구현해야 한다는 조건이 적혀 있었다. 책에서 그 이유는 자세히 설명되어 있지 않았는데, serialize라는 말 그대로 이 인터페이스를 구현해야 객체에서 DB 상의 복합 키로 직렬화가 가능하다는 정도의 의미일 것 같다는 생각은 들었지만, 구체적으로 어떤 역할을 하는 건지 궁금해졌다. 그래서 공부한 내용들을 정리해 본다.

     

    Serialization이 뭐야?

    Serialization은 쉽게 말하면, 자바의 클래스 정보를 이진 스트림으로 바꾸는 것을 말한다. 여기서 말하는 ‘클래스 정보'에는 클래스의 이름, 멤버 변수의 개수, 멤버 변수(자료형과 변수명, 값) 등이 포함되는데, A라는 클래스를 직렬화하면 A가 상속하고 있는 부모 클래스의 정보까지 모두 훑기 때문에 최종적으로는 Object 클래스까지 거슬러 올라가서 이 클래스의 정보를 총망라해서 이진 스트림으로 만든다고 보면 된다. 참고로 이렇게 클래스의 정보를 읽어오는 데에는 Reflection이 사용된다.

     

    어떤 클래스를 직렬화하려면 그 클래스는 Serializable 인터페이스를 구현하고 있어야 하는데, 이 Serializable 인터페이스를 보면 구현할 메서드가 없다. 그냥 ‘이 클래스는 직렬화할 수 있어~’라고 JVM에게 알려주는 용도라고 보면 되는데, 이런 인터페이스들을 마커 인터페이스(Marker Interface)라고 부른다.

     

    직렬화 과정에서는 ObjectDataOutputStream을 사용하게 되는데, 여러 스트림을 결합해 직렬화 결과를 내보낼 수 있다. FileOutStream을 직렬화 결과를 파일에 내보내 보자. 물론 텍스트가 아닌 이진 파일이기 때문에 Hex 편집기 등으로 열어 봐야 내용을 볼 수 있다. 직렬화된 파일을 읽을 때는 FileInputStreamObjectDataInputStream을 사용하면 된다.

    public class Serialization implements Serializable {
    
        private int value;
    
        public Serialization(int value) {
            this.value = value;
        }
    
        public static void main(String[] args) throws IOException, ClassNotFoundException {
                    // 직렬화
            Serialization serialization = new Serialization(100);
            FileOutputStream fos = new FileOutputStream("serialized.out");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
    
            oos.writeObject(serialization);
            oos.flush();
            oos.close(); // ./serialized.out에 Serialization의 내용이 직렬화됨
    
                    // 역직렬화
            FileInputStream fis = new FileInputStream("serialized.out");
            ObjectInputStream ois = new ObjectInputStream(fis);
    
            Serialization deserialized = (Serialization) ois.readObject();
                    // ./serialized.out의 내용을 바탕으로 Serialization 객체를 역직렬화
    
            System.out.println(deserialized.value); // 100
        }
    }

     

    serialized.out 파일에 Serialization 객체를 직렬화해 저장하고, 해당 파일의 내용을 다시 불러와 역직렬화하는 코드다. 역직렬화된 deserialized의 value를 출력해 보면 100이 제대로 출력되는 걸 볼 수 있다.

     

    직렬화된 데이터는 모든 걸 알고 있다

    참고로 serialized.out 파일을 열어 보면 다음과 같은 내용을 확인할 수 있다. 테스트에 사용한 앱은 macOS의 HextEdit인데, 우측에 쓰여 있는 문자열 변환은 영문 알파벳으로만 변환되어서 조금 어색하다. 세부 내용을 살펴보면 아래와 같다. (파일 내용 해석은 이 자료를 참조했다) 모두 16진수 숫자임을 감안하고 보자.

     

    • AC ED: serialization 프로토콜
    • 00 05: serialization 버전
    • 73: Object 객체임을 의미
    • 72: 클래스 객체임을 의미
    • 00 0D: 클래스명의 길이(13)
    • 53 65 72 69 61 6c 69 7a 61 74 69 6f 6e: 클래스명(Serialization)
    • 19 9C 11 9A 3B D0 31 22: Serial 버전 식별자(SerialVersionUID)
    • 02: 이 클래스가 직렬화를 지원함을 나타냄
    • 00 01: 클래스의 필드 개수(int value 1개)
    • 49: 필드의 타입. 0x49는 int 타입을 의미한다.
    • 00 05: 필드명의 길이(value = 5)
    • 76 61 6c 75 65: 필드명(value)
    • 78 70: (불명)
    • 00 00 00 64: value의 값(32비트 정수 100)

    보다시피 객체 하나의 모든 정보가 들어있다고 보면 된다. 특정 클래스를 상속했을 경우에는 이후에 부모 클래스의 정보도 모두 들어가게 된다. 그렇기 때문에 이 정보만 있으면 그대로 다시 자바 객체를 역직렬화해서 만들어낼 수 있는 것이다.

     

    JPA의 복합 키는 Serializable이어야 하는 이유

    자, 다시 원래 주제로 돌아와서, JPA의 복합 키는 왜 @IdClass를 사용하든 @EmbeddedId를 사용하든 Serializable 해야 할까? 사실 이 질문에는 함정이 하나 있는데, 복합 키 뿐만 아니라 JPA 엔티티의 모든 식별자는 Serializable 해야 하기 때문이다. 근데 나는 한 번도 그런 거 신경 써본 적이 없는데? 원시 타입은 기본적으로 Serializable하고, 기본키로 사용되는 자바의 객체 자료형(Long, String 등)도 실제로 코드를 까 보면 serializable 인터페이스를 구현하고 있기 때문이다.

     

    그렇다면 질문을 조금 바꾸는 게 좋겠다, JPA의 기본 키(식별자)는 왜 Serializable해야 할까? 기본적으로 직렬화는 객체의 전체 정보를 이진 데이터로 바꾸어서 다른 곳으로 전송하기 위해서 사용된다. 그렇다면, 기본키는 JPA 내에서 어디로 전송될까? 바로 캐시다. JPA는 기본적으로 캐시를 사용해서 이미 영속성 콘텍스트 캐시 내부에 있는 객체는 DB 연결 없이 캐시에 있는 값을 가져오는데, 이 때 각 객체를 식별자를 통해서 구분하게 된다. 이 때 식별자를 구분자로 사용하기 위해 식별자 객체 자체를 직렬화하는 것으로 보인다. 사실 이 부분에 대한 자료가 충분하지는 않아서 명확한 내부 구현을 알기는 어렵다. 또한 이렇게 클래스 정보를 직렬화를 사용해야만 가져올 수 있는 것도 아니다. 하지만 클래스의 전체 정보를 일괄적으로 보낼 수 있는 가장 보편적인 방법 중 하나로 Serialization이 이용되고 있는 것이라고 볼 수 있겠다.

     


     

    참고자료

    댓글