ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JAVA] Reflection & Serializable
    Mhwan's Study/JAVA & Kotlin 2021. 8. 10. 00:55

    리플렉션과 Serializable을 공부하면서 Serializable이 내부적으로 리플렉션을 이용하기 때문에 같이 포스팅합니다.

     

    Reflection

    리플렉션은 run-time에 동적으로 클래스들의 정보를 알아내고, 실행할 수 있는 것을 말합니다.

    일반적인 객체 생성은 compile-time에 우리가 작성한 클래스와 메소드가 컴파일되어 JVM의 메모리 영역에 올라와 있는데, 리플렉션은 그것이 아니라 run-time에 되는 것이 가장 큰 차이일 것입니다.

    그래서 reflection을 통해 클래스를 분석하고(객체의 생성자, 변수, 메소드 private 포함) 객체를 만들고, 메소드를 실행할 수도 있게 됩니다.(Reflection을 쓰면 private으로 된 메소드를 호출할 수도 있습니다.)

    리플렉션을 제가 써본 경험은 두번 정도인데, 한번은 Spring 공부할때 DB를 불러오며 초기화 할때 사용했고, 한번은 네이버 인턴으로 근무하면서 안드로이드의 SpannableStringBuilder의 private 메소드를 호출하기 위해 이용해본 기억이 납니다.

    Reflection이 java에만 있는 기술인 것으로 아는데, Java는 컴파일 될때 Class Loader가 동적로딩으로 바이트 코드를 JVM의 메소드 영역에 클래스의 정보를 로드하게 됩니다. 즉 이미 메모리에 로드되어있으므로 Run-time시점에 클래스의 정보를 가져올 수 있게 되는 것입니다.

     

    이렇게 리플렉션은 꽤나 유용한 기술이라는 생각을 했습니다. 더 상세한 포스팅은 아래 링크를 참고하세요

    (https://thisisnew-storage.tistory.com/10?category=815046)

     

    Serializable

    자바 시스템 내부에서 사용되는 객체나 데이터를 외부의 자바 시스템에서 이용할 수 있도록 byte 형태로 변환하는 기술 (직렬화)
    byte로 변환된 데이터를 다시 객체로 변환하는 기술 (역직렬화)

    이를 통틀어 Serializable이라고 합니다. 특히 어떤 객체든 java.io.Serializable만 구현하면 되기 때문에 간단하게 사용할 수 있습니다.

    그리고 Serialiazable을 이용할때는 serialVersionUID 값을 설정하기를 권장합니다.

    직렬화 과정

    직렬화 하려는 Object객체를 ObjectOutputStream을 이용해 byte형태로 바꿉니다. 이때 serialVersionUID도 같이 저장합니다.
    직렬화로 byte[]로 된다면 아래와 같이 동작하게 될 것입니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        Member member = new Member("배명환""abc123@email.com"27);
        byte[] serializedMember;
        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(member);
                // serializedMember -> 직렬화된 member 객체 
                serializedMember = baos.toByteArray();
            }
        }
        // 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
        System.out.println(Base64.getEncoder().encodeToString(serializedMember));
    cs

     

    역직렬화 과정

    함께 저장한 serialVersionUID를 불러와 값을 체크하고 이 값이 같으면 역직렬화를 진행, 다르면 예외를 발생시킵니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
        // 직렬화 예제에서 생성된 base64 데이터 
        String base64Member = "...생략";
        byte[] serializedMember = Base64.getDecoder().decode(base64Member);
        try (ByteArrayInputStream bais = new ByteArrayInputStream(serializedMember)) {
            try (ObjectInputStream ois = new ObjectInputStream(bais)) {
                // 역직렬화된 Member 객체를 읽어온다.
                Object objectMember = ois.readObject();
                Member member = (Member) objectMember;
                System.out.println(member);
            }
        }
    cs

    따라서 seraiVersionUID값은 해당 클래스의 버전이 서로 맞는지 체크하는 용도로 사용하며, 이를 작성하지 않으면 컴파일 시 JVM이 자동으로 생성하게 되는데, 이는 os에 따라 JVM이 달라지기 때문에 serialVersionUID가 달라 예외가 발생할 수 있게 됩니다. 이 때문에 Serializable을 사용할때 serialVersionUID값을 함께 설정하기를 권장하는 것입니다.

     

    그럼 여기서 reflection을 언제 사용할까를 보면, 역직렬화 하는 과정에서 readObject()메소드를 사용하는 것을 볼 수 있습니다. 필요에 따라 Serializable을 구현한 클래스에 readObject()메소드를 구현했을 수도 있는데, 이때 내부적으로 reflection을 이용해 직렬화된 객체의 readObject()메소드를 호출합니다.

     

    이렇게 내부적으로 reflection을 사용해 직렬화를 처리하기 때문에, 리플렉션 처리중에 필요 없는 객체들도 추가적으로 생성될 수 있고 이는 과도한 GC 발생을 야기할 수 있어 성능적인 단점이 있습니다.

    이 때문에 안드로이드에서는 직접 구현해야되서 불편하지만 Parcelable 사용을 권장합니다. (최근에는 kotlin을 사용한다면 parcelize라는 어노테이션을 이용해 더 쉽게 이용할 수 있으니, 이걸 사용하는게 가장 좋을 것 같습니다.)

     

    참고 : https://medium.com/@sunminlee89/직렬화-serialization-와-java-serializable-android-parcelable까지-8e1a8723aa77, https://techblog.woowahan.com/2550/

    댓글

Designed by Mhwan.