CodeGym /행동 /JAVA 25 SELF /직렬화의 압축과 프로파일링

직렬화의 압축과 프로파일링

JAVA 25 SELF
레벨 45 , 레슨 4
사용 가능

1. 서론: 왜 직렬화를 최적화해야 할까?

현대 애플리케이션에서 직렬화는 네트워크 프로토콜부터 분산 캐시, 서비스 간 데이터 교환까지 곳곳에서 사용됩니다.

여기서 직렬화의 속도와 크기는 매우 중요합니다. 직렬화가 느리면 애플리케이션이 데이터를 저장하거나 로드할 때 지연이 발생하고, 네트워크나 디스크는 헛되이 대기하게 됩니다. 객체가 너무 크면 디스크 공간을 많이 차지하고, 네트워크 전송이 오래 걸리며, 메모리와 대역폭에 추가 부담을 줍니다.

전형적인 과제로는 큰 객체 그래프를 파일이나 캐시에 저장하기, 최소 지연으로 네트워크를 통해 객체를 전달하기, 그리고 멀티스레드 시스템에서 데이터를 빠르게 직렬화/역직렬화하기 등이 있습니다.

결론은 간단합니다. 직렬화 최적화는 “프리미엄 기능”이 아니라, 고성능·확장형 애플리케이션을 위한 필수적인 실천입니다.

2. 직렬화된 데이터 크기 최적화

불필요한 데이터 제외: 키워드 transient

기본적으로 객체의 모든 필드는 transient로 표시된 것을 제외하고 직렬화됩니다. 저장할 필요가 없는 필드(예: 캐시, 임시 데이터, 서비스 참조)는 transient로 표시하세요:

public class User implements Serializable {
    private String name;
    private transient String sessionToken; // 직렬화되지 않음
}

장점:

  • 직렬화된 객체의 크기가 줄어듭니다.
  • 파일이나 네트워크에 불필요하거나 위험한 데이터가 남지 않습니다.

수동 직렬화: 인터페이스 Externalizable

무엇을 어떻게 직렬화할지 완전한 제어가 필요하다면 인터페이스 Externalizable을 구현하고 직렬화를 명시적으로 기술하세요(writeExternal/readExternal 메서드):

public class Person implements Externalizable {
    private String name;
    private int age;
    private transient String secret;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
        // secret은 직렬화하지 않음
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        name = in.readUTF();
        age = in.readInt();
    }
}

장점:

  • 필요한 필드만 직렬화됩니다.
  • 호환성을 유지하면서 직렬화 포맷을 변경할 수 있습니다.

압축: 직렬화된 데이터 압축

직렬화된 객체는 특히 반복되는 문자열과 큰 컬렉션을 포함할 때 크기가 커지기 쉽습니다. 압축을 사용해 크기를 줄일 수 있습니다.

GZIPOutputStream 예시:

try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data.gz")))) {
    out.writeObject(bigObject);
}

ZipOutputStream 예시:

try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("data.zip"))) {
    zip.putNextEntry(new ZipEntry("object"));
    ObjectOutputStream out = new ObjectOutputStream(zip);
    out.writeObject(bigObject);
    out.flush();
    zip.closeEntry();
}

장점:

  • 파일 크기가 몇 배(특히 큰 객체 그래프에서)까지 줄어들 수 있습니다.
  • 네트워크 전송 트래픽이 감소합니다.

단점:

  • 압축/해제에 추가 CPU 시간이 필요합니다.

3. 직렬화 속도 최적화

버퍼링: 왜 BufferedOutputStreamBufferedInputStream이 필요한가

문제:
버퍼링이 없으면 write() 또는 read() 호출마다 디스크나 네트워크에 대한 시스템 호출이 발생합니다 — 이는 매우 느립니다!

해결:
버퍼링된 스트림을 사용하세요:

try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
try (ObjectInputStream in = new ObjectInputStream(
         new BufferedInputStream(new FileInputStream("data.bin")))) {
    Object obj = in.readObject();
}

장점:

  • 큰 객체의 쓰기/읽기를 크게 가속합니다.
  • 디스크/네트워크 접근 횟수를 줄입니다.

작동 방식은?
버퍼는 데이터를 메모리에 모아 한 번에 덩어리로 기록하며, 바이트 단위로 쓰지 않습니다.

빠른 복사: FileChannel.transferTo

큰 직렬화 파일을 빠르게 복사해야 한다면 NIO의 transferTo 메서드를 사용하세요:

try (FileChannel src = new FileInputStream("data.bin").getChannel();
     FileChannel dest = new FileOutputStream("copy.bin").getChannel()) {
    src.transferTo(0, src.size(), dest);
}

장점:

  • 복사가 OS 레벨에서 수행되어 Java의 불필요한 버퍼링을 우회 — 큰 파일에서 매우 빠릅니다.

4. 직렬화 프로파일링

간단한 시간 측정: System.nanoTime()

직렬화 성능을 빠르게 가늠하려면 System.nanoTime()을 사용할 수 있습니다:

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
long end = System.nanoTime();
System.out.println("직렬화 시간: " + (end - start) / 1_000_000 + " ms");

장점:

  • 간단하고 빠릅니다.
  • 여러 변형(버퍼 사용/미사용, 압축 유무 등)을 비교할 수 있습니다.

단점:

  • GC와 백그라운드 프로세스의 영향으로 결과가 흔들릴 수 있습니다.
  • 아주 미세한 차이를 정확히 비교하기에는 적합하지 않습니다.

정밀 프로파일링: JMH (Java Microbenchmark Harness)

더 정밀한 측정을 위해서는 마이크로벤치마크 전용 라이브러리인 JMH를 사용하세요.

간단한 벤치마크 예시:

@Benchmark
public void serializeWithBuffer() throws Exception {
    try (ObjectOutputStream out = new ObjectOutputStream(
             new BufferedOutputStream(new FileOutputStream("data.bin")))) {
        out.writeObject(bigObject);
    }
}

장점:

  • JVM 워밍업, GC 영향, OS 노이즈를 고려합니다.
  • 신뢰할 수 있고 재현 가능한 결과를 제공합니다.

단점:

  • JMH 방법론에 대한 설정과 이해가 필요합니다.
  • 대략적인 비교 용도로는 과할 수 있습니다.

5. 실습: 직렬화 시간과 크기 비교

미니 실험을 해봅시다. 큰 객체 그래프(예: 중첩 컬렉션을 포함한 100_000개 객체의 리스트)를 여러 방식으로 직렬화하고, 시간과 파일 크기를 비교합니다.

버퍼링·압축 없이 직렬화

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("버퍼 없음: " + (end - start) / 1_000_000 + " ms, 크기: " +
    new File("data1.bin").length() + " 바이트");

버퍼링 사용 직렬화

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data2.bin")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("버퍼 사용: " + (end - start) / 1_000_000 + " ms, 크기: " +
    new File("data2.bin").length() + " 바이트");

압축(GZIP) 사용 직렬화

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data3.gz")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("압축 사용: " + (end - start) / 1_000_000 + " ms, 크기: " +
    new File("data3.gz").length() + " 바이트");

결과 분석

직렬화를 테스트해 보면 버퍼링과 압축의 영향이 매우 크다는 것을 알 수 있습니다. 압축된 파일은 보통 210배 더 작아집니다(정확한 비율은 데이터 구조에 따라 달라집니다). 버퍼를 사용하면 직렬화가 눈에 띄게 빨라지고, 압축은 과정을 다소 느리게 만들 수 있지만 공간 절약의 가치가 큰 경우가 많습니다.

결론: 대용량 데이터에는 반드시 버퍼링을 사용하고, 크기가 중요하다면 압축을 추가하세요.

6. 직렬화 최적화 시 흔한 실수

오류 1: 버퍼링을 사용하지 않음 — 큰 객체의 직렬화가 몇 배나 느려집니다.

오류 2: 불필요하거나 민감한 데이터(예: 비밀번호, 임시 토큰)를 직렬화함 — 이러한 필드에는 항상 transient를 사용하세요.

오류 3: 압축이 항상 직렬화를 빠르게 만든다고 기대함 — 실제로 압축은 크기를 줄이지만(특히 성능이 낮은 CPU에서) 처리를 다소 느리게 만들 수 있습니다.

오류 4: JVM 워밍업과 GC 영향을 고려하지 않은 시간 측정 — 정확한 벤치마크에는 JMH를 사용하세요.

오류 5: 시간만 또는 크기만 비교함 — 두 지표를 모두 보고, 과제에 맞는 최적의 균형을 선택하세요.

1
설문조사/퀴즈
이진 직렬화 최적화, 레벨 45, 레슨 4
사용 불가능
이진 직렬화 최적화
이진 직렬화 최적화
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION