CodeGym /행동 /JAVA 25 SELF /transient 필드, serialVersionUID

transient 필드, serialVersionUID

JAVA 25 SELF
레벨 43 , 레슨 1
사용 가능

1. transient 자세히 알아보기

Java에서 키워드 transient는 직렬화기에게 “이 필드는 건드리지 말고, 객체를 저장할 때는 제외해!”라고 말하는 방법입니다. 필드를 transient로 선언하면, 그 필드는 직렬화된 바이트 스트림에 포함되지 않습니다. 이는 민감한 데이터(예: 비밀번호)나 저장할 필요가 없는 일시적 계산 결과에 특히 유용합니다.

예시: 왜 transient가 필요한가?

사용자 클래스가 있다고 가정해 봅시다:

import java.io.Serializable;

public class User implements Serializable {
    private String username;
    private transient String password; // 비밀번호는 저장하지 않음!

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    // 여기에는 게터와 세터가 있습니다
}

이 클래스의 객체를 직렬화하면 password 필드는 파일(또는 다른 스트림)에 기록되지 않습니다. 즉, 역직렬화 시 비밀번호는 기본값을 갖게 됩니다 — 객체는 null, 숫자는 0, booleanfalse입니다.

실제에서는 어떻게 동작할까?

작은 실험을 해봅시다. 먼저 사용자를 직렬화합니다:

import java.io.*;

public class TransientDemo {
    public static void main(String[] args) throws Exception {
        User user = new User("vasya", "qwerty123");

        // 객체를 파일에 저장
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.ser"));
        out.writeObject(user);
        out.close();

        // 이제 객체를 다시 읽음
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.ser"));
        User restored = (User) in.readObject();
        in.close();

        System.out.println("Username: " + restored.username);
        System.out.println("Password: " + restored.password);
    }
}

결과:

Username: vasya
Password: null

보시다시피 password 필드는 복원되지 않았습니다 — transient이므로 직렬화기가 이를 무시합니다.

transient는 어디에, 왜 사용할까?

  • 비밀번호와 토큰. 절대 직렬화하지 마세요!
  • 캐시되거나 임시 데이터. 예를 들어, 런타임에 즉석에서 계산할 수 있는 필드라면.
  • 직렬화할 수 없거나 직렬화할 필요가 없는 객체. 예: DB 연결에 대한 참조, 스트림, 소켓.

transient 필드의 동작 특징

객체가 역직렬화되면, transient로 표시된 모든 필드는 기본값을 받습니다. 해당 필드에 의미 있는 값을 다시 부여해야 한다면 readObject 메서드를 사용해 수동으로 채울 수 있습니다(캐시를 재계산하거나, 사용자에게 비밀번호를 다시 받아오는 등).

2. serialVersionUID: 클래스 버전의 고유 식별자

serialVersionUID는 직렬화 가능한 클래스의 ‘버전’을 규정하는 long 타입의 특수 정적 필드입니다. 직렬화 시 serialVersionUID 값이 기록되고, 역직렬화 시 JVM은 현재 클래스의 값과 이를 대조합니다. 값이 일치하지 않으면 예외가 발생하고, 객체는 복원되지 않습니다.

serialVersionUID를 어떻게 선언하나?

아주 간단합니다:

private static final long serialVersionUID = 1L;

보통 Serializable을 구현하는 클래스 내부에 바로 선언합니다:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    // ... 나머지 필드와 메서드
}

serialVersionUID가 필요한가?

클래스의 객체를 파일에 저장한 뒤, 클래스 구조를 변경했다고 가정해 봅시다(필드를 추가하거나 이름을 바꾸는 등). serialVersionUID가 다르면 JVM은 해당 클래스가 이전 버전과 호환되지 않는다고 판단하여 객체의 역직렬화를 허용하지 않습니다. 이는 예기치 않은 오류를 방지합니다.

serialVersionUID를 선언하지 않으면 어떻게 되나?

serialVersionUID를 명시적으로 선언하지 않으면, 컴파일러가 클래스 구조를 바탕으로 값을 생성합니다. 하지만 작은 변경(예: 필드 추가 또는 삭제)만으로도 serialVersionUID가 달라질 수 있습니다. 그 결과, 이전 버전의 클래스로 저장된 객체를 역직렬화할 수 없게 됩니다.

따라서 항상 serialVersionUID를 명시적으로 지정하는 것을 권장합니다!

데모: serialVersionUID 불일치

1) 먼저 클래스를 만들고 객체를 직렬화합니다:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 1L;
    private String username;

    public User(String username) {
        this.username = username;
    }
}

2) 그런 다음 serialVersionUID를 변경합니다:

import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 2L; // 1L에서 2L로 변경!
    private String username;

    public User(String username) {
        this.username = username;
    }
}

결과:

java.io.InvalidClassException: User; local class incompatible: stream classdesc serialVersionUID = 1, local class serialVersionUID = 2

JVM이 명확히 경고합니다: “버전이 호환되지 않습니다!”

serialVersionUID 값은 어떻게 선택할까?

대부분 단순한 값들(1L, 2L, 42L)을 사용하고, 대형 프로젝트에서는 IDE가 ‘긴’ 값을 생성해 주기도 합니다. 중요한 점은 클래스 구조가 호환되지 않게 변경될 때에만 이를 바꾸는 것입니다.

3. 실습: transient 필드와 serialVersionUID 실전

예시: transient 필드가 있는 클래스

학습용 애플리케이션(예: 연락처 관리자)을 약간 수정해, 사용자 클래스에 직렬화에 포함되면 안 되는 인증용 임시 세션 토큰 필드를 추가해 봅시다.

import java.io.Serializable;

public class Contact implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String phone;
    private transient String sessionToken; // 임시 토큰

    public Contact(String name, String phone, String sessionToken) {
        this.name = name;
        this.phone = phone;
        this.sessionToken = sessionToken;
    }

    @Override
    public String toString() {
        return "Contact{" +
               "name='" + name + '\'' +
               ", phone='" + phone + '\'' +
               ", sessionToken='" + sessionToken + '\'' +
               '}';
    }
}

이제 객체를 직렬화하고 역직렬화해 보겠습니다:

import java.io.*;

public class TransientAndSUIDDemo {
    public static void main(String[] args) throws Exception {
        Contact c = new Contact("Ivan", "+19990001122", "token-12345");

        // 객체 저장
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("contact.ser"));
        out.writeObject(c);
        out.close();

        // 객체 복원
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("contact.ser"));
        Contact restored = (Contact) in.readObject();
        in.close();

        System.out.println("직렬화 전: " + c);
        System.out.println("역직렬화 후: " + restored);
    }
}

출력:

직렬화 전: Contact{name='Ivan', phone='+19990001122', sessionToken='token-12345'}
역직렬화 후: Contact{name='Ivan', phone='+19990001122', sessionToken='null'}

보시다시피 sessionToken 필드는 복원되지 않았습니다 — transient입니다.

예시: serialVersionUID 실험

1) 먼저 serialVersionUID = 1L로 객체를 직렬화합니다.
2) 그런 다음 serialVersionUID2L로 바꾸고 같은 파일을 역직렬화해 봅니다.

결과: 위에서 본 것처럼 InvalidClassException이 발생합니다.

4. 왜 명시적으로 serialVersionUID를 지정하는 것이 좋을까?

  • 명시적이 암시적보다 낫습니다. 호환성을 직접 통제할 수 있습니다. 클래스 구조가 치명적으로 변하지 않았다면 기존 serialVersionUID를 유지하고, 객체는 문제없이 역직렬화됩니다.
  • 자동 생성은 위험할 수 있습니다. 사소한 변경도 계산된 값을 바꿔, 저장된 데이터의 호환성을 ‘깨뜨릴’ 수 있습니다.
  • IDE가 도와줍니다. 대부분의 IDE(예: IntelliJ IDEA)는 serialVersionUID를 자동으로 생성해 줄 수 있습니다.

5. transientserialVersionUID를 사용할 때 흔한 실수

실수 1: 민감한 필드를 transient로 표시하는 것을 잊음.
그 결과 비밀번호나 토큰이 직렬화된 파일에 실수로 포함될 수 있습니다. 이는 민감 정보 노출로 이어질 수 있어 위험합니다.

실수 2: serialVersionUID를 명시적으로 선언하지 않음.
클래스를 변경한 뒤, 이전 객체를 역직렬화할 수 없게 됩니다. JVM이 호환되지 않는다고 판단하기 때문인데, 실제로는 구조가 치명적으로 바뀌지 않았을 수도 있습니다.

실수 3: 불필요하게 serialVersionUID를 변경함.
게터를 추가하거나 주석만 바꾼 경우라면 serialVersionUID를 바꿀 필요가 없습니다. 그렇지 않으면 기존 데이터가 역직렬화되지 않습니다.

실수 4: serialVersionUIDstatic 또는 final이 아님.
필드는 반드시 private static final long serialVersionUID로 선언해야 합니다. 그렇지 않으면 JVM이 올바르게 인식하지 않습니다.

실수 5: transient 필드를 역직렬화 후 복원하지 않음.
그 값이 객체의 동작에 중요하다면 readObject에서 복원하세요 — 그렇지 않으면 객체가 올바르게 동작하지 않을 수 있습니다.

1
과제
JAVA 25 SELF, 레벨 43, 레슨 1
잠금
타임스탬프 개인 데이터: 엔티티 버전 관리
타임스탬프 개인 데이터: 엔티티 버전 관리
1
과제
JAVA 25 SELF, 레벨 43, 레슨 1
잠금
급여의 비밀: 일시적 소실과 기본값 복원
급여의 비밀: 일시적 소실과 기본값 복원
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION