티스토리 뷰

Java

Kryo serializer

파미페럿 2022. 1. 3. 15:15

보통 serialize 기능을 사용한다고 하면 Jackson을 많이 사용한다.

serialize는 간단하게 설명하면 객체를 byte[]로 변환해서 전송하는 것을 의미하며 여기에 압축, 암호화 등 원하는 로직이 들어가곤 한다.

반대는 deserialize로 byte[]를 받아서 객체로 변환하는 것이다.

 

여하튼 요지는 Jackson serializer를 많이 사용하는데, (Jackson serializer가 무엇인지는 추후 정리해보겠다.)

Jackson serializer말고도 Kryo serializer라는게 있다는 것을 알게 되는 기회가 생겨서 알게 된 내용들을 정리해본다.

 

 

Kryo serializer란?

빠르고 컴팩트하게 직렬화를 할 목적으로 만들어진 경량 직렬화 프레임워크이다.

아무래도 속도와 용량을 적게 차지하게 만들어진 직렬화 프레임워크인지라 Jackson보다 속도나 용량 면에서는 뛰어나다.

하지만 GenericJackson2JsonRedisSerializer과 같이 redis에서 사용할 수 있도록 RedisSerializer를 구현한 Jackson과 달리 kryo는 RedisSerializer를 구현한 클래스가 따로 없다. 즉, kryo는 Jackson과 같이 여러 가지 Serializer를 구현한 클래스를 제공하지 않아서 내가 사용하고자 하는 Serializer에 맞게 Kryo를 이용해서 구현해야 한다.

 

또한 kryo에서 직렬화에 기본적으로 사용하는 Output, Input 객체들은 thread-safe하지 않아서 이 점을 고려해서 사용해야 한다.

 

kryo에 대한 그 밖의 여러 가지 설명들이 있긴 하지만 그 이상 자세한 설명들은 아래 kryo github를 참고 바란다.

https://github.com/EsotericSoftware/kryo

 

GitHub - EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic

Java binary serialization and cloning: fast, efficient, automatic - GitHub - EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic

github.com

 

 

Kryo 기본 구현

kryo는 위에서 얘기했듯이 직렬화/역직렬화에 kroy의 Output, Input 객체를 사용한다.

 

serialize

kryo로 직렬화를 하는 기본 코드는 아래와 같다.

try {
	Kryo kryo = new Kryo();
	Output output = new Output(200, -1);
    	kryo.writeClassAndObject(output, o);
    	byte[] bytes = output.toBytes();
} catch (Exception e) {
	System.out.println("Could not kryo serialize");
}

 

kryo는 직렬화할 때 Output이라는 객체를 사용하는데 Output 객체 생성자에 들어가는 값은 아래와 같다.

Output output = new Output(bufferSize, maxBufferSize);

 

위 생성자말고도 OutputStream을 파라미터로 받는 생성자도 있어 암호화 또는 압축 기능이 있는 OutputStream을 넣어서(생성자 또는 setter) 직렬화할 때 압축과 암호화도 수행할 수 있다. 또한 Output은 바이트 배열 버퍼에 데이터를 쓰는 OutputStream 방식으로 ByteArrayOutputStream의 모든 기능을 제공한다.

 

 

kryo 직렬화 방식

kryo는 빠르고 가벼운 직렬화 프레임워크이다. 이러한 장점을 최대한 살리기 위해서는 kryo의 serialize 방식을 대략적으로 이해해야한다.

kryo는 미리 타입에 대응하는 serializer를 가지고 있다가 직렬화를 수행할 때 해당 타입에 맞는 serializer를 가져와서 직렬화를 수행한다. 기본적으로 ByteArraySerializer ~ VoidSerializer 33개 이상의 serializer는 등록되어 있지만 커스텀 클래스의 경우는 별도로 serializer를 지정해서 kryo에 등록해야 한다.

더보기
...
		addDefaultSerializer(byte[].class, ByteArraySerializer.class);
		addDefaultSerializer(char[].class, CharArraySerializer.class);
		addDefaultSerializer(short[].class, ShortArraySerializer.class);
		addDefaultSerializer(int[].class, IntArraySerializer.class);
		addDefaultSerializer(long[].class, LongArraySerializer.class);
		addDefaultSerializer(float[].class, FloatArraySerializer.class);
		addDefaultSerializer(double[].class, DoubleArraySerializer.class);
		addDefaultSerializer(boolean[].class, BooleanArraySerializer.class);
		addDefaultSerializer(String[].class, StringArraySerializer.class);
		addDefaultSerializer(Object[].class, ObjectArraySerializer.class);
		addDefaultSerializer(KryoSerializable.class, KryoSerializableSerializer.class);
		addDefaultSerializer(BigInteger.class, BigIntegerSerializer.class);
		addDefaultSerializer(BigDecimal.class, BigDecimalSerializer.class);
		addDefaultSerializer(Class.class, ClassSerializer.class);
		addDefaultSerializer(Date.class, DateSerializer.class);
		addDefaultSerializer(Enum.class, EnumSerializer.class);
		addDefaultSerializer(EnumSet.class, EnumSetSerializer.class);
		addDefaultSerializer(Currency.class, CurrencySerializer.class);
		addDefaultSerializer(StringBuffer.class, StringBufferSerializer.class);
		addDefaultSerializer(StringBuilder.class, StringBuilderSerializer.class);
		addDefaultSerializer(Collections.EMPTY_LIST.getClass(), CollectionsEmptyListSerializer.class);
		addDefaultSerializer(Collections.EMPTY_MAP.getClass(), CollectionsEmptyMapSerializer.class);
		addDefaultSerializer(Collections.EMPTY_SET.getClass(), CollectionsEmptySetSerializer.class);
		addDefaultSerializer(Collections.singletonList(null).getClass(), CollectionsSingletonListSerializer.class);
		addDefaultSerializer(Collections.singletonMap(null, null).getClass(), CollectionsSingletonMapSerializer.class);
		addDefaultSerializer(Collections.singleton(null).getClass(), CollectionsSingletonSetSerializer.class);
		addDefaultSerializer(TreeSet.class, TreeSetSerializer.class);
		addDefaultSerializer(Collection.class, CollectionSerializer.class);
		addDefaultSerializer(TreeMap.class, TreeMapSerializer.class);
		addDefaultSerializer(Map.class, MapSerializer.class);
		addDefaultSerializer(TimeZone.class, TimeZoneSerializer.class);
		addDefaultSerializer(Calendar.class, CalendarSerializer.class);
		addDefaultSerializer(Locale.class, LocaleSerializer.class);
		addDefaultSerializer(Charset.class, CharsetSerializer.class);
		addDefaultSerializer(URL.class, URLSerializer.class);
		OptionalSerializers.addDefaultSerializers(this);
		TimeSerializers.addDefaultSerializers(this);
        
        // Primitives and string. Primitive wrappers automatically use the same registration as primitives.
		register(int.class, new IntSerializer());
		register(String.class, new StringSerializer());
		register(float.class, new FloatSerializer());
		register(boolean.class, new BooleanSerializer());
		register(byte.class, new ByteSerializer());
		register(char.class, new CharSerializer());
		register(short.class, new ShortSerializer());
		register(long.class, new LongSerializer());
		register(double.class, new DoubleSerializer());
		register(void.class, new VoidSerializer());
...

 

kryo에 serializer를 등록하는 것은 아래 함수를 사용해서 할 수 있다.

kryo.register(object.class, new Serializer());

 

만일 serializer가 등록되어 있지 않다면 새롭게 해당 클래스의 serializer를 InstantiatorStrategy를 이용해서 클래스의 생성자를 통해 serializer를 만들어서 사용한다. 즉, 이렇게 될 경우 직렬화 뿐만 아니라 serializer를 생성하는 것까지 수행하므로 kryo의 빠르고 가벼운이라는 장점을 살릴 수 없다.

 

kryo 직렬화 함수

kryo에서 직렬화에 사용하는 함수는 3가지 종류가 있다. (오버로딩해서 직렬화에 사용하는 이름이 'write~'로 시작하는 함수들 함수는 3개가 넘는다.)

 

- writeObject

제일 기본 직렬호 함수로 object를 받아서 object를 bytes화 할 수 있는지 체크한 후 해당 클래스 serializer를 통해 ouput 객체에 직렬화한다.

readObject()와 쌍을 이뤄서 사용되며 readObject에서는 역직렬화한 결과가 어떤 타입인지 타입 명시를 해준다.

 

- writeObjectOrNull

writeObject와 비슷하지만 writeObjectOrNull은 serialize해서 output에 쓰는 값이 null일 수 있다.

readObjectOrNull과 짝을 이뤄 사용된다.

 

- writeClassAndObject

writeObject와 비슷하고 차이점을 느끼기 어렵다.

readClassAndObject와 짝을 이뤄 사용되며 readClassAndObject에서는 따로 타입 명시를 해주지 않는다.

 

deserialize

kryo로 역직렬화 하는 기본 코드는 아래와 같다.

try {
	Input input = new Input(bytes);
    	Object object = kryo.get().readClassAndObject(input);
} catch (Exception e) {
	System.out.println("Could not kryo deserialize");
}

 

Input 클래스 또한 Output 클래스와 마찬가지로 바이트 배열 버퍼에서 데이터를 읽는 InputStream 방식으로 ByteArrayOutputStream의 모든 기능을 제공한다.

 

kryo 역직렬화 함수

- readObject

writeObject와 쌍을 이뤄 사용되는 것으로 타입을 받아서 해당 타입의 serializer를 가져와서 byte[]를 해당 타입으로 읽는다.

아예 타입 명시를 받는다.

 

- readObjectOrNull

writeObjectOrNull과 쌍을 이뤄 사용되는 것으로 readObjectOrNull과 비슷하지만 null을 return할 수도 있다는 차이점이 있다.

타입 명시가 필요하다.

 

- readClassAndObject

writeClassAndObject와 쌍을 이뤄 사용되는 것으로 이미 클래스를 명시해서 직렬화 했기 때문에 따로 별도 타입 명시가 필요 없다.

 

 

* 참고로 writeObject-readObject, writeObjectOrNull-readObjectOrNull, writeObjectOrNull-readObjectOrNull의 쌍을 맞춰 사용하지 않을 경우 deserialize시 에러가 발생한다.

 

 

Redis KryoSerializer

kryo를 이용해서 serializer 하는 것을 Redisserializer로 구현해보면 코드는 아래와 같다.

public class KryoSerializer implements RedisSerializer {
	// thread 처리
    private ThreadLocal<Kryo> kryo = ThreadLocal.withInitial(() -> new Kryo());

    static final byte[] EMPTY_ARRAY = new byte[0];

    @Override
    public byte[] serialize(Object o) throws SerializationException {
        if(o == null) {
            return EMPTY_ARRAY;
        }

        try {
            Output output = new Output(200, -1);
            kryo.get().writeClassAndObject(output, o);
            return output.toBytes();
        } catch (Exception e) {
            log.error("Could not kryo serialize", e);
        }
    }

    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        try {
            Input input = new Input(bytes);
            return kryo.get().readClassAndObject(input);
        } catch (Exception e) {
            log.error("Could not kryo deserialize", e);
        }
    }

    public KryoSerializer() {
        this.kryo.get().setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
    }
}

 

 

주의할 점

1. 필드 정보가 수정되었을 경우

kryo는 스키마 정보 없이 필드 데이터만 각 필드별로 차지하는 byte로 계산해 직렬화 하므로 필드 순서가 바뀌었거나 이름을 바꿨을 경우 deserialize 시 에러가 발생할 수 있다.

 

2. 기본 생성자가 없는 클래스에 kryo를 사용할 경우

kryo는 커스텀 클래스에 대해 serializer를 지정해주지 않으면 DefaultInstantiatorStrategy를 이용해서 해당 클래스의 기본 생성자를 통해 serializer를 생성한다. 따라서 파라미터가 없는 기본 생성자가 커스텀 클래스에 존재하지 않을 시 에러가 발생한다.

이는 커스텀 클래스에 파라미터가 없는 기본 생성자를 만들어주면 해결될 일이지만(@NoArgsConstructor) 모든 커스텀 클래스에 파라미터가 없는 생성자를 만들기 여의치 않으면 serializer를 생성하는 InstantiatorStrategy 설정을 변경해주면 된다.

StdInstantiatorStrategy는 생성자 없이 reflection을 통해 serializer를 생성하므로 StdInstantiatorStrategy를 사용해주면 되지만 reflection의 경우 성능을 저하시키므로 아래 코드를 통해서 기본적으로 DefaultInstantiatorStrategy를 이용해 생성자로 serializer를 생성하고 만일 생성자가 없어 에러가 발생할 경우 StdInstantiatorStrategy를 사용하도록 설정하면 된다.

kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));

 

 

 

✋ kryo github

https://github.com/EsotericSoftware/kryo

 

GitHub - EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic

Java binary serialization and cloning: fast, efficient, automatic - GitHub - EsotericSoftware/kryo: Java binary serialization and cloning: fast, efficient, automatic

github.com

✋ Output

http://javadox.com/com.esotericsoftware.kryo/kryo/2.23.0/com/esotericsoftware/kryo/io/Output.html

 

Output (Kryo 2.23.0 API) - Javadoc Extreme

New Blog Post! Astyanax, the Cassandra Java library New blog post: Getting started with Astyanax, the open source Cassandra java library and connect your application to one of the most important NoSQL database. Read Blog Post

javadox.com

✋ Input

http://javadox.com/com.esotericsoftware.kryo/kryo/2.22/com/esotericsoftware/kryo/io/Input.html

 

Input (Kryo 2.22 API) - Javadoc Extreme

New Blog Post! Astyanax, the Cassandra Java library New blog post: Getting started with Astyanax, the open source Cassandra java library and connect your application to one of the most important NoSQL database. Read Blog Post

javadox.com

✋ StdInstantiatorStrategy

http://objenesis.org/apidocs/org/objenesis/strategy/StdInstantiatorStrategy.html

 

StdInstantiatorStrategy (Objenesis 3.2 API)

JavaScript is disabled on your browser. All Implemented Interfaces: InstantiatorStrategy public class StdInstantiatorStrategy extends BaseInstantiatorStrategy Guess the best instantiator for a given class. The instantiator will instantiate the class withou

objenesis.org

 

 

 

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함