직렬화란?
직렬화(Serialize)란 Object 또는 Data를 파일 또는 네트워크를 통해서 스트림(송수신)이 가능하도록 바이트 형태의 연속적인(serial) 데이터로 변환하는 기술입니다.
시스템적으로 살펴보면, JVM의 힙(heap) 혹은 스택(stack) 메모리에 상주하고 있는 객체 데이터를 직렬화를 통해 바이트 형태로 변환하여 데이터베이스나 파일과 같은 외부 저장소에 저장해두고, 다른 컴퓨터에서 이 파일을 가져와 자바 객체로 변환해서 JVM 메모리에 적재하는 것으로 보면 됩니다.
직렬화의 장점은?
자바에서의 직렬화는 외부 파일이나 네트워크를 통해 클라이언트 간에 객체 데이터를 주고 받을 때 사용됩니다.
그런데 문득 이런 의문이 들 수 있습니다.
CSV나 JSON과 같은 훌륭한 데이터 포맷이 있는데 굳이 직렬화를 사용하는 이유가 뭘까?
실제로 JSON은 웹 뿐만 아니라 게임 쪽에서도 설정 파일로 쓰이거나 데이터를 교환할 때 범용적으로 사용됩니다.
그리고 직렬화는 오로지 자바 프로그램에서만 사용이 가능하지만, JSON 형태로 객체 데이터를 저장해두면 파이썬, 자바스크립트에서도 범용적으로 사용이 가능합니다.
이에 대해서 직렬화의 장점은 다음과 같습니다.
1. 직렬화는 자바의 고유 기술인 만큼 당연히 자바 시스템에서 개발에 최적화 되어있습니다.
2. 자바의 광활한 레퍼런스 타입에 대해 제약 없이 외부에 내보낼 수 있습니다.
예를들어 기본형(int, double, string) 타입이나 배열(array)과 같은 타입들은 웬만한 프로그램이 언어가 공통적으로 사용하는 타입이기 때문에, 이러한 값들은 JSON 으로 충분히 상호 이용이 가능합니다.
하지만 자바에서 지원하는 컬렉션이나 클래스, 인터페이스 타입들, 사용자가 커스텀으로 클래스를 정의하여 사용하는 경우 한계가 있습니다.이들을 외부에 내보내기 위해선 각 데이터를 매칭시키는 별도의 파싱(parsing)이 필요합니다.
직렬화를 이용하면 비록 파이썬이나 자바스크립트와 같은 다른 시스템에서는 사용하지 못하겠지만, 직렬화의 기본 조건만 지킨다면 특별한 작업 없이 바로 외부에 내보낼 수 있습니다. 그리고 역직렬화를 통해 읽어들일 때 데이터 타입이 자동으로 맞춰지기 때문에 자바 클래스의 기능들을 곧바로 다시 이용할 수 있습니다.
자바 직렬화 사용법 및 사용규칙
1. 객체를 직렬화 하기 위해서는 java.io.Serializable 인터페이스를 implements 해야 합니다.
Serializable 인터페이스는 아무런 내용도 없는 마커 인터페이스로서, 직렬화를 고려하여 작성한 클래스인지를 판단하는 기준으로 사용합니다.
2. ObjectOutputStream 객체 직렬화
직렬화에는 ObjectOutputStream을 사용합니다.
public static void main(String[] args) {
// 직렬화할 고객 객체
Customer customer = new Customer(1, "홍길동", "123123", 40);
// 외부 파일명
String fileName = "Customer.ser";
// 파일 스트림 객체 생성 (try with resource)
try (
FileOutputStream fos = new FileOutputStream(fileName);
ObjectOutputStream out = new ObjectOutputStream(fos)
) {
// 직렬화 가능 객체를 바이트 스트림으로 변환하고 파일에 저장
out.writeObject(customer);
} catch (IOException e) {
e.printStackTrace();
}
}
코드를 실행하면 Customer.ser 파일이 생성됩니다. 직렬화 된 파일이라는 것을 명시하는 것이 좋기 때문에 네이밍을 .ser이나 .obj로 많이 지정하는 편입니다. .txt로 해도 문제는 없습니다.
파일 내용을 보면 사람이 읽을 수 없는 문자 형태로 되어 있는 것을 볼 수 있습니다.
3. ObjectInputStream 객체 역직렬화
역직렬화에는 ObjectInputStream을 사용합니다.
단, 역직렬화를 할 때에는 주의사항이 있는데 직렬화 대상이 된 객체의 클래스가 외부 클래스라면, 클래스 경로(Class Path)에 존재해야 하며 import 된 상태여야 합니다.
public static void main(String[] args) {
// 외부 파일명
String fileName = "Customer.ser";
// 파일 스트림 객체 생성 (try with resource)
try(
FileInputStream fis = new FileInputStream(fileName);
ObjectInputStream in = new ObjectInputStream(fis)
) {
// 바이트 스트림을 다시 자바 객체로 변환 (이때 캐스팅이 필요)
Customer deserializedCustomer = (Customer) in.readObject();
System.out.println(deserializedCustomer);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
이렇게 역직렬화를 이용하게 되면, 생성자로 객체 초기화 없이 바로 객체에 정보를 가져와 인스턴스화 하여 사용할 수 있습니다.
직렬화 요소 제외
객체의 모든 인스턴스를 직렬화 하기에는 너무 무겁거나 혹은 중요한 정보는 외부에 노출시키고 싶지 않은 경우, 직렬화 할 요소를 직접 선택할 필요가 있습니다. 이럴때 transient 키워드를 필드 변수 타입 앞에 명시해주면 직렬화 대상에서 제외 됩니다.
객체 상속 관계에서의 직렬화
만약 부모-자식 상속 관계에서 부모 클래스가 Serializable을 구현했다면 자식 클래스는 Serializable을 구현하지 않아도 직렬화가 가능합니다. 반대로 부모 클래스는 Serializable을 구현하지 않았는데 자식 클래스만 구현했다면 직렬화 할 때 부모 클래스의 필드는 무시되고 자식의 필드만 직렬화가 됩니다.
SerialVersionUID
Serializable 인터페이스를 구현하는 모든 직렬화된 클래스는 serialVersionUID(이하 SUID) 이라는 고유식별번호를 부여받습니다. 이 식별 ID는 클래스를 직렬화, 역직렬화 하는 과정에서 동일한 특성을 갖는지 확인하는 데 사용됩니다. 클래스의 내부 구성이 수정될 경우 기존에 직렬화한 SUID와 현재 클래스의 SUID 버전이 다르기 때문에 이를 인지하고 InvalidClassException 예외를 발생시켜 값이 불일치 되는 현상을 방지합니다.
단, 직렬화 스펙상 serialVersionUID 값을 명시하는 것은 필수가 아니며, 만일 클래스에 SUID 필드를 명시하지 않는다면 시스템이 런타임 시에 클래스의 이름, 생성자 등과 같이 클래스의 구조를 이용해 암호 해시함수를 적용해 자동으로 클래스 안에 내부적으로 식별번호를 생성하게 됩니다.
만일 네트워크로 객체를 직렬화하여 전송하거나 협업을 하는 경우 수신자와 송신자가 모두 같은 버전의 클래스를 가지고 있어야 하는데, 만일 클래스가 조금만 변경사항이 있으면 모든 사용자에게 재배포해야 하는 에로사항이 생겨 프로그램을 관리하게 어렵게 만듭니다.
따라서 직렬화 클래스는 웬만한 상황에서는 SUID를 직접 명시해주어 클래스 버전을 수동으로 관리하는 것을 권장하는 편입니다. SUID를 직접 명시해주면 클래스의 내용이 변경되어도, 클래스의 버전을 시스템이 자동 생성된 값으로 변경되지 않기 때문입니다. 이외에도 런타임 시에 SUID를 생성하는 시간도 많이 소요되기 때문에 미리 명시를 강력히 권장하는 바 입니다.
SUID는 아래와 같이 선언합니다.
class Member implements Serializable {
// serialVersionUID 꼭 명시 할 것
private static final long serialVersionUID = 123L;
private String name;
private int age;
private String address;
// private String email; // 새로 추가한 클래스 구성 요소
...
}
클래스 SUID를 수동으로 명시하더라도 절대 만능이 아닙니다. 단순히 필드 변수 하나를 추가하는 정도는 문제가 없겠지만 자바 직렬화는 상당히 타입에 엄격하여 필드 타입을 변경하게 된다면 수동 직렬화라고 하더라도 예외를 막을 수 없습니다.
자주 변경 될 소지가 있는 클래스는 직렬화를 사용하지 않는 것이 좋습니다. 추후 버전에서 이전 버전에 영향 없이 소스 코드를 수정하는 것은 매우 어렵기 때문입니다.
직렬화의 문제점
Serializable 인터페이스를 클래스에 구현만 하면 아주 간단하게 직렬화가 가능한 클래스가 되어 외부로 내보낼 수 있습니다. 그러나 간단한 선언 방법과는 다르게 직렬화를 구현한 대가는 매우 비쌉니다. 직렬화를 구현한 순간부터 많은 위험성을 갖게되기 때문입니다.
1. 직렬화는 용량이 크다.
직렬화는 객체에 저장된 데이터 값 뿐만 아니라 타입 정보, 클래스 메타 정보를 가지고 있으므로 용량을 은근히 많이 차지합니다. 그래서 같은 정보를 직렬화로 저장하느냐 JSON으로 저장하느냐는 파일 용량 크기가 거의 2배이상 차이가 납니다.
DB나 Cache 등에 외부에 저장할 때, 장기간 동안 저장하는 정보는 직렬화를 지양해야 합니다.
2. 역직렬화는 위험하다.
결론부터 말하자면 직렬화 설정 자체는 문제가 없지만, 남이 만든 것을 역직렬화 하는 과정에서 나도 모르게 공격 당할 위험성이 있습니다. 대표적으로 가젯(gadget)이나, 객체를 외부로 전송하는 과정에서 중간에서 누가 가로채 파일 바이트 내용을 조작하는 식의 공격이 가능합니다. 따라서 신뢰할 수 없는 데이터는 절대 역직렬화 하면 안되며, 직렬화의 잠재적인 위험성을 회피하는 가장 좋은 방법은 아무것도 역직렬화 하지 않는것입니다.
3. 릴리즈 후에 수정이 어렵다.
클래스가 Serializable을 구현하게 되면 직렬화된 바이트 스트림 인코딩도 하나의 공개 API가 되는 것입니다.
그래서 직렬화를 구현한 클래스가 널리 퍼지면 그 직렬화 형태도 영원히 지원해야 합니다. 클래스의 내부 구현을 수정한다면 원래의 직렬화 형태와 달라지게 되기 때문입니다.
즉, Serializable을 구현한 순간부터 해당 객체의 유지보수는 직렬화에 묶이게 됩니다.
4. 클래스의 캡슐화가 깨진다.
만일 직렬화 할 클래스에 private한 필드가 있어도 직렬화를 하게 되면 그대로 외부로 노출이 됩니다. 직렬화 형태가 하나의 공개 API가 되어 캡슐화가 깨지게 됩니다.
5. 버그와 보안에 취약하다.
자바에서는 객체를 생성자를 이용해 만드는 것이 기본이지만, 역직렬화는 언어의 기본 메커니즘을 우회하여 객체를 바로 생성하도록 합니다. 즉, 역직렬화는 숨은 생성자이기도 한 것입니다.
문제는 만일 어느 객체가 생성자를 통해 인스턴스화 할 때 불변식이나 허가되지 않은 접근을 설정하였을 경우 이를 무시하고 생성된다는 점입니다.
예를들어 클래스가 생성자 입력값으로 이상한 값을 걸러내는 로직이 있는데, 이를 역직렬화로 생성하게 될 경우 이를 걸러낼 수 없는 문제가 생길 수 있습니다.
6. 새로운 버전을 릴리즈 할 때 테스트 요소가 많아진다.
만약 직렬화 가능한 클래스가 업데이트 되면, 구버전의 직렬화 형태가 신버전에서 역직렬화가 가능한지 테스트 해야 할 것입니다. 즉, 테스트 양이 직렬화 가능 클래스의 수와 릴리즈 횟수에 비례하게 됩니다.
결론
직렬화는 많은 단점과 위험 요소가 존재합니다. 그러나 직렬화는 1997년에 탄생하여 여전히 자바 생태계 곳곳에서 쓰이고 있습니다. 그래서, 만일, 어쩔 수 없이, Serializable을 구현해야 한다면, 그에 따른 비용이 적지 않으니 클래스를 설계 할 때마다 이득과 비용을 잘 저울질 해야합니다. 하지만 시간과 노력을 들여서라도 JSON 등으로 데이터 표현하는 것을 추천하는 바 입니다.
만약 직렬화를 대체할 수 없다면..
Serializable을 구현한 클래스만을 지원하는 프레임워크를 사용해야 하거나 레거시 시스템 때문에 어쩔 수 없이 클래스에 직렬화를 설정해야 할 경우, 역직렬화 필터링(ObjectInputFilter)과 같은 역직렬화 방어 기법을 사용하면 됩니다. 데이터 스트림이 역직렬화되기 전에 필터 조건문을 수행하여 특정 클래스만 허용하거나 제외하도록 할 수 있습니다.
REFERENCE
'백엔드 > Java' 카테고리의 다른 글
[Java] 오류와 예외 처리(Exception Handling) (0) | 2024.01.05 |
---|---|
[Web] 서블릿(Servlet)이란? (0) | 2024.01.05 |