1주차 Unit 7.4 — Serializable과 transient

Psj·2026년 5월 11일

F-lab

목록 보기
49/230

Unit 7.4 — Serializable과 transient

F-LAB JAVA · 1주차 · Phase 7 · 외부 세계와의 통신 · 1주차 마지막 Unit


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 직렬화(serialization)는 왜 필요한가?
  • Serializable이 마커 인터페이스(메서드 0개)인 이유는?
  • serialVersionUID를 명시하지 않으면 언제 무슨 일이 터지는가?
  • transient는 정확히 무엇을 막는가?
  • writeObject / readObject 후킹 메서드는 어떻게 호출되나?
  • 왜 Effective Java는 "Java 직렬화를 피하라"고 하는가?
  • 역직렬화 공격(Insecure Deserialization)이 무엇이고 어떻게 막는가?
  • ILIC에서 직렬화 형식을 어떻게 선택하는가?

🎯 핵심 한 문장

Java 직렬화 = 객체를 바이트 스트림으로 변환하는 JVM 내장 메커니즘 (Java 1.1+)
— 강력하지만 보안·성능·호환성 면에서 현대적 대안(JSON, Protobuf)에 모두 진다.
Effective Java는 명시적으로 "새 코드에서는 사용하지 말 것"을 권한다.

비유 — 진공 포장과 도시락

시대모델비유
객체 (메모리)RAM의 살아있는 객체갓 만든 따끈한 밥상
직렬화바이트 스트림으로 변환진공 포장 — 우편 발송 가능
역직렬화바이트에서 객체 복원진공 포장 해체 → 다시 밥상
transient직렬화 제외 필드"이 반찬은 빼고 포장"

그러나 함정: 진공 포장지 안에 폭발물(악성 객체)이 들어있으면, 푸는 순간 터진다. 이게 역직렬화 공격.


🧭 9개 섹션 로드맵

1. 직렬화의 탄생         — 왜 객체를 바이트로 만들어야 했나
2. Serializable 인터페이스 — 마커, 마법, 그리고 함정
3. serialVersionUID      — 명시 안 하면 터지는 시한폭탄
4. transient의 두 얼굴    — 보안과 효율
5. 직렬화 메커니즘 내부   — writeObject · readObject 후킹
6. 역직렬화 공격         — Java의 최대 보안 사고
7. 현대적 대안           — JSON · Protobuf · Externalizable
8. ILIC 실무 코드         — Redis 세션 · JPA Entity · 캐시
9. 면접 질문 + 자기 점검 + 1주차 졸업 시험

1️⃣ 직렬화의 탄생 — 왜 필요한가

1.1 객체는 메모리에만 산다

Shipment s = new Shipment("BL-2024-001", "SEOUL", "TOKYO");
// s는 Heap의 어딘가에 있다. JVM이 꺼지면 사라진다.

세 가지 시나리오에서 객체를 메모리 밖으로 꺼내야 한다.

① 디스크 저장 — 영속화

// 프로세스 재시작 후에도 객체를 복원하고 싶다
saveToFile(shipment, "shipment.dat");
// ... 나중에
Shipment restored = loadFromFile("shipment.dat");

② 네트워크 전송 — 분산

// RMI, EJB 시대: 다른 JVM에 객체를 보내고 싶다
remoteService.processShipment(shipment);
// 객체가 어떻게 TCP 패킷으로 변하는가?

③ 세션 / 캐시 — 외부 저장소

// HttpSession을 Redis에 저장하고 싶다 (스케일 아웃)
redisTemplate.opsForValue().set("session:abc", userSession);

1.2 Java 1.1의 답 — Serializable

1997년, Java 1.1에서 java.io.Serializable 도입.
"클래스에 이 인터페이스만 붙이면 JVM이 알아서 바이트로 만들어 준다."

public class Shipment implements Serializable {
    private String blNo;
    private String origin;
    private String destination;
    // getter/setter
}

// 직렬화
try (ObjectOutputStream out = new ObjectOutputStream(
        new FileOutputStream("shipment.dat"))) {
    out.writeObject(shipment);
}

// 역직렬화
try (ObjectInputStream in = new ObjectInputStream(
        new FileInputStream("shipment.dat"))) {
    Shipment restored = (Shipment) in.readObject();
}

당시 (1997) 의 매력:

  • 코드 한 줄(implements Serializable)로 영속화
  • 깊은 객체 그래프도 자동 처리
  • 별도 라이브러리 불필요

현재의 시각: 이게 다 함정이었다. 이후 25년간 Java의 가장 큰 보안 부채.


2️⃣ Serializable 인터페이스 — 마커, 마법, 함정

2.1 인터페이스 정의

public interface Serializable {
    // 메서드 없음. 진짜 비어있다.
}

마커 인터페이스(Marker Interface) — 메서드 없이 타입에 의미만 부여.
컴파일러와 JVM에게 "이 클래스는 직렬화해도 됨"을 알려준다.

2.2 무엇이 자동으로 직렬화되나

public class Shipment implements Serializable {
    private String blNo;              // ✓ Serializable
    private LocalDate eta;            // ✓ Serializable
    private BigDecimal freight;       // ✓ Serializable
    private List<Cargo> cargoes;      // ✓ Serializable (List 구현체에 따라)
    private DataSource dataSource;    // ❌ NotSerializableException
}

기본 규칙:

  • 모든 필드를 자동으로 직렬화 시도
  • 필드 타입이 Serializable이 아니면 → NotSerializableException
  • static 필드는 직렬화 안 됨 (클래스 단위라서)
  • transient 필드는 직렬화 안 됨

2.3 함정 1 — 캡슐화 파괴

public class Member implements Serializable {
    private String password;   // private — 외부에서 못 봐야 함
    private String email;
}

// 직렬화 → 바이트로 변환
byte[] data = serialize(member);

// 바이트를 열어보면? password 평문이 그대로 보임
// (HEX 에디터로 확인 가능)

private도 안전하지 않다. 직렬화는 캡슐화를 우회.

2.4 함정 2 — 공개 API의 일부가 된다

public class Shipment implements Serializable {
    private String blNo;
}

이 클래스의 바이트 형식이 곧 API다. 필드명 한 줄만 바꿔도 기존 직렬화 데이터를 못 읽는다.

// 변경 후
public class Shipment implements Serializable {
    private String billOfLadingNo;   // ❌ 이전 데이터 읽기 불가
}

→ 직렬화 결정 = 수십 년의 API 유지보수 부담.

2.5 함정 3 — 생성자가 호출되지 않는다

public class Shipment implements Serializable {
    private String blNo;
    private LocalDate createdAt;

    public Shipment(String blNo) {
        this.blNo = blNo;
        this.createdAt = LocalDate.now();   // ❌ 역직렬화 시 실행 안 됨
        validate();                         // ❌ 검증 우회
    }
}

역직렬화는 객체를 메모리에 그냥 만들어낸다 — 생성자, 검증, 불변식 모두 우회.
이게 보안 사고의 근본 원인.


3️⃣ serialVersionUID — 시한폭탄

3.1 무엇인가

public class Shipment implements Serializable {
    private static final long serialVersionUID = 1L;
    // ...
}

직렬화된 바이트의 버전 식별자. 역직렬화 시 클래스의 serialVersionUID와 비교.

직렬화 데이터의 UID  vs  현재 클래스의 UID
       달라?           →  InvalidClassException

3.2 명시 안 하면?

JVM이 클래스 구조(필드명, 타입, 메서드 시그니처)로 자동 계산한다.

public class Shipment implements Serializable {
    // serialVersionUID 명시 안 함
    private String blNo;
}

계산 방식: SHA-1 기반 해시 → -5847248139482917658L 같은 값.

문제:

  • 클래스가 살짝만 바뀌어도 UID가 바뀐다
    • 필드 추가 → UID 변경
    • 메서드 추가 → UID 변경
    • import 정리 → 영향 없음
  • 컴파일러 버전이 다르면 같은 코드에서도 UID 다를 수 있음
  • 운영 사고 직격타: 새 버전 배포 → 기존 Redis 세션 전부 못 읽음 → 모든 사용자 강제 로그아웃

3.3 권장 사례

public class Shipment implements Serializable {

    private static final long serialVersionUID = 1L;    // ✓ 명시
    // 또는
    @Serial
    private static final long serialVersionUID = 1L;    // Java 14+, IDE 친화적

    private String blNo;
}

IDE 설정: IntelliJ → Settings → Editor → Inspections → "Serializable class without 'serialVersionUID'" 활성화.

3.4 언제 UID를 올리는가?

// v1
public class Shipment implements Serializable {
    private static final long serialVersionUID = 1L;
    private String blNo;
}

// v2 — 호환 가능한 변경 (필드 추가)
public class Shipment implements Serializable {
    private static final long serialVersionUID = 1L;    // ✓ 유지
    private String blNo;
    private String carrier;   // 새 필드 — 역직렬화 시 null
}

// v3 — 호환 불가능한 변경 (필드 타입 변경)
public class Shipment implements Serializable {
    private static final long serialVersionUID = 2L;    // ✓ 올림
    private Long blNo;        // String → Long
}

호환 가능 변경: 필드 추가, transient 추가, 접근 제어자 변경
호환 불가 변경: 필드 삭제, 타입 변경, 클래스 계층 변경


4️⃣ transient — 직렬화 제외

4.1 기본 사용

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

    private String userId;
    private String name;

    private transient String password;        // ✓ 직렬화 X
    private transient String authToken;       // ✓
    private transient HttpServletRequest req; // ✓ Serializable 아님
}

4.2 두 가지 용도

용도 1 — 보안 (Security)

비밀번호, 토큰, 비밀키, 신용카드 번호 등은 절대 직렬화돼선 안 된다.

public class PaymentRequest implements Serializable {
    private String orderId;
    private BigDecimal amount;

    // ❌ 직렬화되면 디스크/Redis에 평문 노출
    private String cardNumber;

    // ✅
    private transient String cardNumber;
}

실제 사고 시나리오 (ILIC):

  • 외부 운송사 API 인증 토큰이 직렬화돼서 Redis에 저장됨
  • Redis 덤프가 백업 시스템으로 전송됨
  • 백업 권한이 있는 직원이 토큰 평문을 봄
  • → GDPR / 개인정보보호법 위반

용도 2 — 직렬화 불가능한 필드 (Non-Serializable)

public class CachedShipment implements Serializable {

    private String blNo;

    private transient DataSource dataSource;        // 직렬화 불가
    private transient EntityManager em;             // 직렬화 불가
    private transient Logger log = LoggerFactory.getLogger(getClass());
}

transient 안 붙이면: NotSerializableException: javax.sql.DataSource → 직렬화 자체가 실패.

4.3 transient 필드의 역직렬화 후 상태

public class UserSession implements Serializable {
    private String userId;
    private transient String name;
}

// 1) 직렬화 시점
session.userId = "user-001";
session.name = "박승제";
serialize(session);
// 바이트에는 userId만 들어감

// 2) 역직렬화 후
UserSession restored = deserialize(bytes);
restored.userId;  // "user-001"
restored.name;    // null  ← 기본값

기본값 룰:

  • 객체 참조: null
  • int, long: 0
  • boolean: false
  • double: 0.0

null 가능성을 항상 고려하거나, readObject에서 초기화 (5장 참조).

4.4 final transient의 함정

public class CachedQuote implements Serializable {

    private final String key;
    private final transient BigDecimal computed;  // ❌ 역직렬화 후 null
}

final transient 필드는 역직렬화 시 초기화할 방법이 없다 (생성자 안 호출). → null이 영원히 박힘.

해결: final을 빼거나, readResolve에서 새 객체 반환.


5️⃣ 직렬화 메커니즘 내부

5.1 후킹 메서드 4종

직렬화 동작을 가로채는 메서드들. 이름이 정확해야 JVM이 인식한다 (오타 = 그냥 무시됨).

public class Shipment implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    private String blNo;
    private transient BigDecimal lazyFreight;

    // 1) 직렬화 직전 호출
    @Serial
    private void writeObject(ObjectOutputStream out) throws IOException {
        out.defaultWriteObject();             // 기본 필드 처리
        out.writeObject(encryptedFreight());  // 추가 처리
    }

    // 2) 역직렬화 직후 호출
    @Serial
    private void readObject(ObjectInputStream in)
            throws IOException, ClassNotFoundException {
        in.defaultReadObject();               // 기본 필드 복원
        this.lazyFreight = decryptFreight((byte[]) in.readObject());

        // ✓ 검증을 여기서 수행 (생성자가 호출되지 않으므로)
        if (blNo == null || blNo.isBlank()) {
            throw new InvalidObjectException("blNo 필수");
        }
    }

    // 3) 직렬화 시 자기 자신 대신 다른 객체를 직렬화하고 싶을 때
    @Serial
    private Object writeReplace() {
        return new ShipmentProxy(this);   // Proxy 패턴
    }

    // 4) 역직렬화 후 다른 객체를 반환하고 싶을 때 (싱글톤 핵심)
    @Serial
    private Object readResolve() {
        return Cache.instance(this.blNo);
    }
}

5.2 readObject — 검증의 마지막 보루

생성자가 호출되지 않으므로, 불변식 검증을 여기서 해야 한다.

@Serial
private void readObject(ObjectInputStream in)
        throws IOException, ClassNotFoundException {
    in.defaultReadObject();

    // 비즈니스 규칙 검증
    if (freight != null && freight.signum() < 0) {
        throw new InvalidObjectException("운임은 음수 불가");
    }
    if (cargoes == null) {
        cargoes = new ArrayList<>();   // null 방지
    }
    if (createdAt == null) {
        createdAt = LocalDateTime.now();
    }
}

이걸 빼먹으면? 악의적으로 조작된 바이트로 불변식이 깨진 객체를 만들 수 있다 → 보안 사고.

5.3 readResolve — 싱글톤의 핵심

public class Currency implements Serializable {

    public static final Currency KRW = new Currency("KRW");
    public static final Currency USD = new Currency("USD");

    private final String code;
    private Currency(String code) { this.code = code; }

    @Serial
    private Object readResolve() {
        return switch (code) {
            case "KRW" -> KRW;
            case "USD" -> USD;
            default -> throw new InvalidObjectException("알 수 없는 통화");
        };
    }
}

// 없으면?
Currency krw1 = Currency.KRW;
Currency krw2 = deserialize(serialize(Currency.KRW));
krw1 == krw2;   // ❌ false  — 싱글톤 깨짐!

enum을 쓰면 자동으로 안전 (Java가 보장). 싱글톤은 enum이 Effective Java의 권장.

5.4 Externalizable — 완전 수동 제어

public class FastShipment implements Externalizable {

    private String blNo;
    private long timestamp;

    public FastShipment() {}   // public 무인자 생성자 필수!

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(blNo);
        out.writeLong(timestamp);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        this.blNo = in.readUTF();
        this.timestamp = in.readLong();
    }
}

Serializable vs Externalizable:

항목SerializableExternalizable
인터페이스 메서드0개 (마커)writeExternal / readExternal 강제
자동 처리✓ 모든 필드❌ 직접 다 써야 함
생성자 호출public no-arg 생성자 호출
성능느림 (리플렉션)빠름
호환성 관리UID 자동/수동직접
실무 사용거의 X거의 X (JSON/Protobuf로 대체)

Externalizable은 학문적 관심 정도. 실무에선 어차피 Java 직렬화 자체를 안 쓴다.


6️⃣ 역직렬화 공격 — Java의 최대 보안 사고

6.1 무엇이 문제인가

역직렬화 = 바이트로부터 객체를 만들어내는 것.
그런데 객체는 그냥 데이터가 아니다. 메서드를 가지고 있고, readObject 후킹으로 임의 코드를 실행할 수 있다.

class EvilGadget implements Serializable {
    @Serial
    private void readObject(ObjectInputStream in) throws IOException {
        Runtime.getRuntime().exec("rm -rf /");   // ❌ 역직렬화만 해도 실행됨
    }
}

공격자가 EvilGadget의 바이트를 만들어 보내면, readObject()만 호출해도 RCE(원격 코드 실행).

6.2 실제 사고 — Apache Commons Collections (2015)

가장 유명한 사례. Apache Commons Collections 라이브러리의 InvokerTransformer를 활용한 가젯 체인.

// 공격자가 만드는 페이로드 (개념적)
Transformer chain = new ChainedTransformer(new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", ...),
    new InvokerTransformer("invoke", ...),
    new InvokerTransformer("exec", new Object[] {"calc.exe"})
});

// 이걸 LazyMap으로 감싸서 HashMap에 넣고 직렬화
// → 서버에서 역직렬화하면 calc.exe 실행

피해 범위:

  • WebLogic, WebSphere, JBoss 등 거의 모든 Java 엔터프라이즈 서버
  • Jenkins, OpenNMS, Cisco 제품들
  • CVE 수십 개
  • 공격 코드는 라이브러리 한 줄도 변형하지 않은 순정 코드를 활용 — 패치 어려움

6.3 OWASP Top 10

A08:2021 — Software and Data Integrity Failures
(이전: A8:2017 — Insecure Deserialization)

"역직렬화는 가능하면 모두 피하라. 어쩔 수 없이 써야 하면 untrusted source에서는 절대 하지 말라."

6.4 방어 1 — 신뢰할 수 없는 데이터는 절대 역직렬화 X

가장 강력한 방어: 외부에서 받은 바이트를 Java 직렬화로 풀지 않는다.

  • HTTP API → JSON (Jackson)
  • gRPC → Protobuf
  • 메시지 큐 → JSON 또는 Avro

6.5 방어 2 — ObjectInputFilter (Java 9+)

어쩔 수 없이 Java 직렬화를 써야 한다면, 허용 목록(allow-list) 으로 차단.

ObjectInputStream in = new ObjectInputStream(input);

// 클래스 화이트리스트
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.ilic.shipment.*;" +     // 우리 도메인만 허용
    "java.lang.String;" +
    "java.util.ArrayList;" +
    "!*"                          // 나머지 모두 거부
);
in.setObjectInputFilter(filter);

Object obj = in.readObject();    // 화이트리스트 외 클래스 → 즉시 차단

전역 설정 (Java 9+):

java -Djdk.serialFilter="com.ilic.*;java.base/*;!*" -jar app.jar

6.6 방어 3 — Serial Filter Factory (Java 17+)

더 정교한 컨텍스트 기반 필터링. 일반 애플리케이션에서는 #2까지로 충분.

6.7 핵심 원칙

"역직렬화는 임의 코드 실행과 동등하다고 가정하라."
— Java Secure Coding Guidelines


7️⃣ 현대적 대안

7.1 비교표

형식사람이 읽기크기속도언어 독립보안실무
Java Serialization느림❌❌레거시만
JSON (Jackson)보통보통압도적 1위
Protobuf작음빠름gRPC, 성능 critical
MessagePack작음빠름JSON의 바이너리 버전
Avro작음빠름Kafka, 스키마 진화

7.2 JSON (Jackson) — 90% 케이스의 정답

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());   // LocalDate 지원

// 직렬화
String json = mapper.writeValueAsString(shipment);
// {"blNo":"BL-2024-001","origin":"SEOUL","destination":"TOKYO"}

// 역직렬화
Shipment restored = mapper.readValue(json, Shipment.class);

장점:

  • 사람이 읽음 → 로깅·디버깅 편함
  • 언어 무관 → JS, Python 클라이언트와 통신
  • 스키마 진화 유연 (필드 추가/삭제 영향 적음)
  • Serializable 불필요

단점:

  • 사이즈는 Protobuf보다 큼 (보통 2~5배)
  • 타입 정보 손실 (역직렬화 시 명시 필요)

7.3 Java 16+ Record와 JSON

public record ShipmentDto(
    String blNo,
    String origin,
    String destination,
    LocalDate eta
) {}

// Jackson이 자동 처리 (별도 설정 불필요)
ShipmentDto dto = mapper.readValue(json, ShipmentDto.class);

→ HashMap Unit에서 봤듯이 불변 DTO는 Record. JSON 직렬화에 완벽 적합.

7.4 Protobuf — 성능이 critical할 때

// shipment.proto
syntax = "proto3";

message Shipment {
    string bl_no = 1;
    string origin = 2;
    string destination = 3;
    int64 eta_epoch_day = 4;
}
// 컴파일러가 Java 클래스 생성
Shipment s = Shipment.newBuilder()
    .setBlNo("BL-2024-001")
    .setOrigin("SEOUL")
    .build();

byte[] bytes = s.toByteArray();      // 직렬화
Shipment restored = Shipment.parseFrom(bytes);   // 역직렬화

언제 쓰나:

  • gRPC 통신
  • 초당 수만 TPS의 내부 서비스 통신
  • 모바일 ↔ 서버 트래픽 절약

ILIC 시나리오: 운송사 API와 대량 데이터 교환 (수십만 건 화물 정보) 시 검토 가치.


8️⃣ ILIC 실무 — 어디서 무엇을 쓰는가

8.1 시나리오별 선택 매트릭스

시나리오형식이유
HTTP REST APIJSON표준, 언어 독립, 디버깅 쉬움
Redis 세션 (Spring Session)JSON (또는 Java)보안상 JSON 권장
Redis 캐시 (@Cacheable)JSON가독성, 다른 언어 접근 가능
JPA Entity 영속화DB (직렬화 무관)컬럼별 매핑
외부 메시지 큐 (Kafka)JSON 또는 Avro스키마 진화
gRPC 내부 통신Protobuf성능
로컬 파일 캐시JSON사람이 열어볼 수 있음
절대 안 씀Java Serialization보안, 성능, 호환성 모두 열등

8.2 Redis 세션 — Spring Session

@Configuration
@EnableRedisHttpSession
public class SessionConfig {

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        // ❌ Java 직렬화 (Spring Session 기본값 — 위험)
        // return new JdkSerializationRedisSerializer();

        // ✅ JSON
        return new GenericJackson2JsonRedisSerializer();
    }
}

왜 바꿔야 하나:

  • Redis 데이터를 다른 시스템(모니터링, 분석)에서 읽을 수 있어야 함
  • serialVersionUID 문제 회피 (배포 후 세션 깨짐)
  • 역직렬화 공격 표면 축소

8.3 JPA Entity의 transient

@Entity
public class Shipment {

    @Id @GeneratedValue
    private Long id;

    private String blNo;

    @Transient          // ⚠ JPA 어노테이션 (@javax.persistence.Transient)
    private BigDecimal calculatedFreight;

    transient String tempCache;   // ⚠ Java 키워드 (직렬화용)
}

두 transient의 차이 — 핵심:

종류역할영향
@Transient (JPA)DB에 저장 XHibernate가 컬럼 매핑 안 함
transient (Java 키워드)직렬화 XObjectOutputStream 무시

둘은 별개. 같이 쓸 수도 있다:

@Transient                 // DB 저장 X
private transient Logger log;   // 직렬화도 X

⚠️ 면접 단골: "엔티티의 @Transienttransient 차이는?"

8.4 캐시 - 직렬화 가능한 키와 값

@Service
public class FareCache {

    @Cacheable(value = "fares", key = "#routeKey")
    public BigDecimal getFare(String routeKey) {
        return fareCalculator.compute(routeKey);
    }
}

Redis 캐시 시:

  • 키, 값 모두 직렬화 필요
  • RedisCacheConfiguration에서 JSON serializer 명시
  • BigDecimal 같은 타입은 Jackson 설정으로 처리 (WRITE_BIGDECIMAL_AS_PLAIN)

8.5 DTO — Record + Jackson

public record ShipmentResponse(
    Long id,
    String blNo,
    String origin,
    String destination,
    LocalDate eta,
    BigDecimal freight
) {
    public static ShipmentResponse from(Shipment entity) {
        return new ShipmentResponse(
            entity.getId(),
            entity.getBlNo(),
            entity.getOrigin(),
            entity.getDestination(),
            entity.getEta(),
            entity.getFreight()
        );
    }
}
  • Record → 불변 + equals/hashCode/toString 자동
  • Jackson → 자동 직렬화
  • Serializable 키워드 한 글자도 안 씀 → 직렬화 공격 면역

8.6 외부 운송사 API 인증 토큰 보호

public class CarrierCredential {

    private String carrierId;

    // ❌ 캐시에 평문으로 박힘
    private String apiToken;

    // ✅
    @JsonIgnore                 // Jackson: JSON 직렬화 제외
    private transient String apiToken;   // Java 직렬화도 제외 (혹시 모르니)

    // 토큰은 호출 시점에 KMS/Vault에서 fetch
    public String getApiToken() {
        return secretManager.fetch(carrierId);
    }
}

다중 방어:

  • @JsonIgnore — Jackson이 JSON에 안 넣음
  • transient — Java 직렬화에도 안 들어감
  • 메모리 캐시도 안 함 — 매번 KMS에서 fetch

9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Serializable이 마커 인터페이스인 이유?JVM에게 "직렬화 허용" 신호. 메서드 없이 타입에 의미만 부여
serialVersionUID 명시 안 하면?클래스 변경 시 자동 계산값 변화 → InvalidClassException. 운영 사고
transient의 두 가지 용도?보안(비밀번호) + 직렬화 불가 타입(DataSource)
@Transient vs transient 차이?JPA의 DB 매핑 제외 vs Java 직렬화 제외. 별개
Java 직렬화의 보안 위험?readObject 후킹으로 임의 코드 실행 가능 (RCE)
역직렬화 공격 방어?1) untrusted 데이터 직렬화 X 2) ObjectInputFilter 화이트리스트
readObject에서 검증?생성자가 호출 안 되므로 불변식 검증을 여기서
Externalizable이 Serializable과 다른 점?메서드 강제, no-arg 생성자 호출, 수동 제어
싱글톤 직렬화 시 깨지지 않게?readResolve 또는 enum 사용
현대 대안? 무엇 선택?JSON(범용) / Protobuf(성능) / Avro(스키마 진화)

9.2 자기 점검 체크리스트

기본 이해

  • Serializable이 마커 인터페이스인 의미를 설명할 수 있다
  • serialVersionUID의 역할과 명시 권장 이유를 안다
  • transient가 막는 것이 무엇인지 정확히 안다
  • writeObject / readObject / readResolve의 호출 시점을 안다
  • @Transienttransient의 차이를 설명할 수 있다

실전 적용

  • readObject에서 불변식 검증 코드를 작성할 수 있다
  • 비밀번호/토큰 필드에 transient를 빠짐없이 붙인다
  • Redis 세션을 JSON 직렬화로 설정할 수 있다
  • ObjectInputFilter로 역직렬화 화이트리스트를 설정할 수 있다
  • DTO는 Record + Jackson 패턴으로 설계할 수 있다

면접 대비 — 5분 답변

  • Java 직렬화의 4가지 함정 (캡슐화 파괴 · API 고착 · 생성자 우회 · 보안)
  • serialVersionUID 운영 사고 시나리오
  • 역직렬화 공격 메커니즘과 Apache Commons Collections 사례
  • 현대 대안 선택 기준 (JSON · Protobuf · Avro)
  • ILIC에서 직렬화 형식을 어떻게 선택하는가

🎯 핵심 요약 — 3줄 정리

1. Java 직렬화는 "있지만 쓰지 말 것"

  • 마커 인터페이스 한 줄로 강력하지만, 캡슐화 파괴 · API 고착 · 생성자 우회 · 보안 4중 함정
  • Effective Java가 명시적으로 "새 코드에선 피하라" 권고
  • Serializable 안 쓰면 직렬화 공격 자체가 불가능

2. 어쩔 수 없이 쓴다면 — 다중 방어

  • serialVersionUID 항상 명시
  • 비밀번호/토큰/Non-Serializable 필드는 transient
  • readObject에서 검증
  • ObjectInputFilter로 화이트리스트
  • 가능하면 enum 또는 readResolve로 싱글톤 보장

3. 실무는 JSON · Protobuf

  • 90% 케이스: JSON (Jackson) — REST API · 캐시 · 세션 · 메시지큐
  • 성능 critical: Protobuf — gRPC
  • DTO는 Record + Jackson 패턴

🎓 1주차 졸업 시험 — 24문항 자기 점검

막힘없이 답할 수 있다면 1주차 합격.

OOP & 클래스 (Phase 1~2)

  1. C 구조체로 OOP를 흉내낼 수는 있지만 Java와 결정적 차이는?
  2. 클래스가 가져야 할 두 가지는?
  3. 자식 클래스에서 super(...)를 명시적으로 호출해야 하는 경우는?
  4. 다형성에서 "형 변환을 해도 호출되는 건 원래 객체의 메서드"의 의미는?
  5. instanceof 검사를 가장 하위 자식부터 해야 하는 이유는?

SOLID (Phase 3)

  1. SRP 위반 사례를 자기 코드에서 하나 찾아 설명할 수 있는가?
  2. OCP를 인터페이스 없이 적용하는 것이 가능한가?
  3. LSP가 깨지면 다형성도 깨지는 이유는?
  4. DIP와 Spring의 DI는 어떤 관계인가?

JVM & GC (Phase 4~5)

  1. Member m = new Member()에서 m과 객체 본체는 각각 어디에?
  2. 자바에 pass by reference가 없다는 말의 정확한 의미는?
  3. 약한 세대 가설을 한 문장으로 설명할 수 있는가?
  4. Eden → Survivor → Old로 객체가 이동하는 조건은?
  5. Mark-and-Sweep과 Mark-and-Compact의 차이는?
  6. G1 GC가 큰 Heap에 적합한 이유는?

데이터 구조 (Phase 6)

  1. String a = "abc"; String b = "abc";일 때 a == b가 true인 이유는?
  2. 루프 안에서 String을 +로 합치면 무슨 일이?
  3. ArrayList의 중간 삭제가 O(n)인 이유는?
  4. HashMap의 LoadFactor 0.75 의미는?
  5. TreeMap이 HashMap보다 느린 이유와, 그럼에도 쓰는 이유는?

I/O & 직렬화 (Phase 7)

  1. try 안에서 close()를 하면 무엇이 누수되는가?
  2. NIO가 전통 I/O보다 동시 접속에 유리한 이유는?
  3. transient를 비밀번호에 안 붙이면 어떤 보안 사고가 가능한가?
  4. File보다 Files/Path를 권장하는 두 가지 이유는?

profile
Software Developer

0개의 댓글