본문 바로가기
데이터 엔지니어( 실습 정리 )/hadoop

5. 하둡 I/O ( 하둡 기초 )

by 세용용용용 2026. 1. 12.

[ "하둡 완벽 가이드 4판" ] 공부한 후 정리한 내용 입니다

 

5. 하둡 I/O

하둡은 데이터 I/O를 위한 프라미티브( 내장기능 제공 )


1) 데이터 무결성

  • 하둡에서 데이터 무결성은 데이터가 유실되거나 손상되지 않도록 보장하는 것을 의미합니다.
  • 즉, 저장된 데이터와 읽어드린 데이터가 항상 동일함을 보장하는 기능

 

체크섬(Checksum)

  • 체크섬: 데이터를 일정 규칙으로 계산한 검증용 숫자
  • 데이터가 손상되면 체크섬 값이 달라짐 → 손상 판단 가능
  • 하둡에서는 CRC-32 / CRC-32C 사용

체크섬은 오류 검출만 가능하며 데이터 복구는 하지 않습니다. >> 시스템 메모리는 ECC 메모리 사용 권장 ( 오류 정정 기능이 있는 메모리 )


1.1 HDFS의 데이터 무결성

  • HDFS는 데이터를 저장할 때와 읽을 때 자동으로 체크섬(Checksum) 검증을 합니다.
  • 체크섬 설정
    • 기본 512바이트마다 체크섬 4바이트 생성
    • 오버헤드: 전체 공간의 약 1% 미만
  • 데이터 노드 역할
    • 단순 저장만 하는 것이 아니라, 손상 여부 검증도 담당

 

[ 그럼 언제 검증?? ]

  1. 저장 시 : 클라이언트가 데이터 전송 → 데이터 노드가 받는 즉시 체크섬 계산 후 검증
  2. 복제 시 : 다른 노드로 블록 복사 → 수신 데이터 체크섬 검증

 

[ 데이터 전송( 파이프라인 구조 ) ]

클라이언트 → 데이터노드1 → 데이터노드2 → 데이터노드3
  • 마지막 노드에서 체크섬 검증 후 저장
  • 데이터 손상 시 → 클라이언트에 오류 신호
  • 클라이언트는 IOException 처리 → 재처리 가능
  • 읽을 때도 저장된 체크섬과 수신 데이터 체크섬을 비교하여 검증

 

[ 추가 안전 기능 ]

  • 체크섬 검증 로그: 데이터 블록 별 마지막 검증 시간 기록 → 디스크 오류 감지에 활용
  • DataBlockScanner: 백그라운드에서 주기적으로 모든 블록 재검사 → Bit rot 예방 ( 물리적 저장장치 문제 )

 

[ 손상 블록 복구 과정 ]

  • 클라이언트가 데이터 읽다가 손상 블록 발견
  • ChecksumException 발생 → 블록 + 노드 정보 네임노드 보고
  • 네임노드가 손상 블록 표시 → 다른 노드/클라이언트 접근 금지
  • 정상 복제본을 다른 노드에 생성
  • 손상 블록 자동 삭제

 

[ 체크섬 검증 비활성화 ]

  • 코드: FileSystem.setVerifyChecksum(false)
  • 쉘: -get 또는 -copyToLocal 명령에 -ignoreCrc 옵션

손상된 파일 조사나 복구 가능 여부 확인 시 유용

 

 

[ 파일 체크섬 확인 ]

  • 명령: hadoop fs -checksum <파일>
  • 용도: 두 파일 내용 동일 여부 확인 (예: distcp 시)

1.2 LocalFileSystem의 데이터 무결성

  • 하둡은 로컬 디스크에 파일 저장할 떄도 자동으로 체크섬을 계산해 데이터가 손상되지 않도록 관리함

 

[ 파일 저장 시 ]

  • Hadoop은 원본 파일 옆에 숨김 체크섬 파일(.crc) 생성
    • .crc 파일에는 512바이트마다 계산된 체크섬 정보 포함
    • 청크 크기 정보도 저장 → 나중에 청크 크기 변경되어도 파일 정확히 읽을 수 있음
  • 결과: 파일 1개 저장 → 데이터 파일 + 체크섬 → 파일 2개 생성

 

[ 파일 읽기 시 ]

  • 읽을 때 .crc 파일 참고 → 체크섬 검증
  • 파일 손상 발견 → ChecksumException 발생

 

[ 성능 영향 ]

  • 체크섬 계산으로 읽기/쓰기 속도 약간 느려짐
  • 전체 성능에는 거의 영향 없음
  • 데이터 안전성을 위한 납득할 수 있는 비용

 

[ 체크섬 비활성화 ]

체크섬이 필요 없는 경우, LocalFileSystem 대신 RawLocalFileSystem 사용 가능

 

< 전역설정 ( core-site.xml )에 설정 >

<configuration>
    <property>
        <name>fs.file.impl</name>
        <value>org.apache.hadoop.fs.RawLocalFileSystem</value>
    </property>
</configuration>

 

< 코드에서 부분 적용 >

import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.RawLocalFileSystem;
import org.apache.hadoop.conf.Configuration;

Configuration conf = new Configuration();
RawLocalFileSystem rawFs = new RawLocalFileSystem();
rawFs.initialize(null, conf);

1.3 ChecksumFileSystem

  • 체크섬 기능이 없는 파일시스템에도 쉽게 체크섬 기능을 붙일 수 있게 해주는 래퍼 입니다.
## 예시
FileSystem rawFs = ...; // 원래 파일 시스템 (체크섬 없음)
FileSystem checksummedFs = new ChecksumFileSystem(rawFs); // 체크섬 기능 추가

rawFs >> 원시 파일 시스템
checksummedFs >> 체크섬 기능이 추가된 파일 시스템

즉, 파일 읽기/쓰기 자체는 원시 파일 시스템에서 하고, 체크섬 기능만 추가로 제공하는 구조

 

[ 내부 원리 ]

  • 단순 래퍼 역할: 데이터 처리 자체는 원시 파일 시스템에서 수행
  • 필요하면 getRawFileSystem() 메서드로 원시 파일 시스템 접근 가능

 

[ 유용한 메서드 ]

메서드 역할
getChecksumFile(Path file) 해당 파일의 체크섬 파일 경로 반환
reportChecksumFailure(Path file, Path checksumFile) 읽기 중 체크섬 오류 발생 시 호출
  • 기본 구현은 특별히 동작하지 않음
  • 하지만 LocalFileSystem에서는
    • 손상된 파일 + 체크섬 파일 → bad_files 디렉터리로 이동
    • 관리자는 주기적으로 bad_files 확인 후 삭제/복구

 

 

 

2) 압축

  • 압축 목적 : 저장공간 절약, 전송 속도 향상
  • 대용량 데이터를 다루는 하둡 환경에서는 필수!!

2.1 하둡에서 사용 가능한 압축 알고리즘

알고리즘 분할 가능 여부 특징
Gzip 불가 널리 사용, 속도·압축률 균형
Bzip2 가능 압축률 높지만 속도 느림
LZO 기본 불가, 색인(index) 생성 시 가능 압축/해제 속도 빠름
LZ4 / Snappy 불가 속도 매우 빠름, 압축률 낮음

 

분할 가능 의미!!

  • 큰 파일을 여러 조각으로 나눠 동시에 처리 가능
  • MapReduce 작업에 적합

2.2 속도 vs 압축률

  • 속도 빠르면 압축률 낮음
  • 압축률 높으면 속도 느림
  • 즉, 시간 vs 공간의 트레이드오프 관계

 

[ 압축 옵션 ] ( -1 ~ -9 )

  • -1 : 속도 우선
  • -9 : 공간 우선
## 예시
gzip -1 file   # 가장 빠른 방식으로 file.gz 생성

2.3 압축 알고리즘 정리

  • 빠른 순서: LZO / LZ4 / Snappy > Gzip > Bzip2
알고리즘 속도 압축률 분할 가능 비고
LZO 매우 빠름 낮음 ❌ (색인 시 ✅) 실시간 처리에 유리
LZ4 매우 빠름 낮음 최신 시스템에서 자주 사용
Snappy 매우 빠름 낮음 구글에서 개발
Gzip 보통 보통 범용적으로 많이 사용
Bzip2 느림 높음 고압축 필요 시 유용

2.4 코덱

  • 코덱 = 데이터를 압축/해제하는 알고리즘 구현체
  • 하둡에서는 "CompressionCodec" 인터페이스로 제공됨 → 파일 확장자를 보고 자동으로 압축/해제 가능
코덱명 클래스명 확장자 특징
Gzip org.apache.hadoop.io.compress.GzipCodec .gz 압축률 좋음, 속도 보통
Bzip2 org.apache.hadoop.io.compress.BZip2Codec .bz2 압축률 매우 높지만 느림
LZO com.hadoop.compression.lzo.LzopCodec .lzo 압축/해제 매우 빠름, 실시간 처리에 유리

 

 

[ CompressionCodecFactory ]

  • 파일 확장자를 기반으로 적절한 코덱 자동 탐지
    → 예: xxx.gz → GzipCodec 자동 적용

[ 원시 라이브러리 ]

  • 하둡은 C/C++로 구현된 압축 라이브러리 사용 → Java 대비 해제 50% 빠름 / 압축 10% 빠름
  • 자동으로 OS에 맞는 원시 라이브러리 찾아 사용
  • 필요 시 비활성화
# 비활성화 시 >> java로 동작하는 기본 코덱 사용
io.native.lib.available = false

 

[ CodecPool ]

  • 압축기/해제기 객체를 매번 새로 생성하는 것은 비효율적 → Pool 방식으로 재사용
  • 객체 생성 비용 감소지속 압축/해제 시 성능 향상

2.5 압축과 입력 스플릿 ( MR 성능 핵심 포인트!! )

  • MapReduce는 입력 파일을 여러 스플릿으로 분리하여 병렬 처리 수행
    → 따라서 "압축 포맷이 분할 가능한가?" 가 매우 중요
## 이게 무슨 소리냐면!! ##

Gzip + MapReduce 동작 방식
- HDFS는 파일을 블록 단위로 나눠 저장

하지만.. Gzip은 중간에서 분할 위치를 인식할 수 없음..
→ 압축 해제하려면 파일 처음부처 순차적으로 읽어야됨

즉, MapReduce가 map task를 만들떄 블록 단위로 나눠져 있어도
[File 1 block]
[File 2 block]
[File 3 block]

실제 실행은
맵태스크 1개가 전체 gzip 파일을 처음부터 끝까지 처리 ( 순차적으로 처리 )

결론 : 여러 HDFS 블록에 나눠 저장돼 있어도 "쪼개서 병렬 처리할 수 없음!!"

 

상황 결과
분할 가능한 압축 포맷 사용 여러 Map Task 병렬 실행 → 성능 ↑
분할 불가능 압축 포맷 사용 1개의 Map Task가 전체 파일 처리 → 성능 ↓

 

 

  • Gzip 압축 1GB 파일 → HDFS 블록 여러 개로 나뉘어 저장되더라도 Gzip은 분할 불가 → 결국 Map Task 1개가 전체 파일 처리 → 병렬성 손실
  • Bzip2 파일 → 블록 단위 구조 + Marker 존재 → 분할 가능
  • LZO 파일 → 색인 생성 시 분할 가능

즉, 하둡에서는 파일 크기가 클수록 분할 가능한 압축 포맷이 필수!!


2.6 맵리듀스에서 압축 사용하기

  • 하둡은 확장자만 있어도 압축 자동 인식 → 개발자가 별도 압축 코드 작성 X

 

[ 출력 압축 설정 ]

mapreduce.output.fileoutputformat.compress=true
mapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec

## 시퀀스 파일 출력 시 ##
RECORD : 개별 레코드 압축 ( default )
BLOCK : 여러 레코드를 묶어 압축 ( 압축률 / 성능 ↑ )

 

[ 명령줄에서 바로 설정 ]

hadoop jar job.jar MyJob \
 -Dmapreduce.output.fileoutputformat.compress=true \
 -Dmapreduce.output.fileoutputformat.compress.codec=org.apache.hadoop.io.compress.GzipCodec \
 (input) (output)

코드 수정 없이 압축 설정 교체

 

[ 맵 출력 압축 (매우 중요!) ]

  • 맵 출력 데이터는 디스크에 임시 저장 + 네트워크로 리듀스에 전송됨
    → 압축 시 디스크 I/O 감소 + 전송량 감소 → 전체 성능 향상
mapreduce.map.output.compress=true
mapreduce.map.output.compress.codec=org.apache.hadoop.io.compress.SnappyCodec

→  보통 맵 출력은 Bzip2 처럼 느릭 코덱은 비추합니다.. Snappy / LZO / LZ4 사용 (빠르고 CPU 부담 낮음)


 

 

 

3) 직렬화

  • 직렬화 → 객체를 네트워크로 보낼 수 있는 바이트 스트림으로 변환하는 과정
  • 역질렬화 → 바이트 스트림을 다시 원래 객체로 복원하는 과정

[ 왜 직렬화가 필요? ]

  • 분산 시스템은 여러 노드가 네트워크로 데이터를 주고받음 → 이떄, RPC 방식 사용 ( 다른 노드의 메서드 호출 ) → 문제점!.. 객체는 그대로 네트워크 전송 불가
  • 즉, 내부 통신 과정에서 [ 객체 → 직렬화 → 네트워크 전송 → 역직렬화 → 객체 복원 ] 과정을 거침

 

[ RPC 직렬화 포맷이 가져야할 4가지 장점 ]

  1. 간결성
    • 포맷이 간결할수록 전송 데이터 크기 ↓
    • 네트워크 대역폭 절약
  2. 고속화
    • 직렬화 / 역질렬화 빠른 포맷 == 전체 시스템 성능 향상
    • 직렬화 / 역질렬화가 느리면 → 전체 시스템 성능도 느려짐
  3. 확장성
    • 기존 데이터 유지 + 새 필드 추가 가능 == 좋은 직렬화 포맷
    • 버전 변화에 유연해야 함
  4. 상호운영성
    • 클라이언트가 ( java, python, c.. ) 다양한 언어 사용
    • 직렬화 포맷도 여러 언어에서 공통으로 사용 가능해야 함

 

[ RPC 직렬화 vs 영속적 저장소 포맷 차이 ]

구분 RPC 직렬화 영속적 저장소 포맷
수명 짧음 (보통 1초 미만) 매우 김 (수년 보관)
목적 빠른 전송 안전한 저장 & 재사용
중점 속도, 효율 안정성, 호환성
  • 차이는 있지만 → 간결성 / 고속화 / 확장성 / 상호운용성 4가지는 둘 다 중요

 

[ 하둡의 직렬화 방식 - Writable ]

  • Writable은 하둡이 자체적으로 만든 직렬화 인터페이스
[ 장점 ]
- 구조 단순
- 매우 빠름
- 하둡 내부 통신에 최적화 ( 속도 우선 )

[ 단점 ]
- 자바 전용
- 타 언어 호환이 어려움

 

 

[ 요즘 트랜드? ]

  • Writable 대신 범용 직렬화 포맷을 많이 사용 → Avro, Protocol Buffers, Thrift ( 다양한 언어 지원 + 스키마 기반 + 확장성 우수 )
  • 하둡 기본 RPC 통신은 Writalbe이 디폴트 지만, 필요시 범용 직렬화 포맷을 RPC에 쓸 수 있음
# 범용 직렬화 포맷 장단점
[ 장점 ]
- 여러 언어 지원
- 스키마 기반이라 버전 업그레이드 시 호환성 좋음

[ 단점 ]
- Writable 보다 느림
- 구조 복잡하면 직렬화 / 역직렬화 비용 증가

3.1 Writable 인터페이스

[ Writable 기본 개념 ]

  • 하둡 내부 통신에서 데이터를 빠르게 직렬화 / 역직렬화하기 위해 만든 인터페이스
객체 → [직렬화] → 바이트 스트림 → [역직렬화] → 객체

 

  • 메서드
void write(DataOutput out)      // 직렬화: 객체 → 바이트
void readFields(DataInput in)   // 역직렬화: 바이트 → 객체

 

 

[ Writable 사용 예시 ]

  • 직렬화
## 직렬화 ( 객체 → 바이트 )
IntWritable writable = new IntWritable(163);
byte[] bytes = serialize(writable); // 내부적으로 163이 4바이트로 변환

## 직렬화 과정
IntWritable(163)
  ↓ write()
ByteArrayOutputStream → DataOutputStream
  ↓
byte[4] = {00 00 0a 3}   // 실제 바이트 배열

## 확인
assertThat(bytes.length, is(4)); // 4바이트 확인

 

  • 역직렬화
## 역직렬화 ( 바이트 → 객체 )
IntWritable newWritable = new IntWritable();
deserialize(newWritable, bytes);
assertThat(newWritable.get(), is(163)); // 163이 잘 복원됨

## 역질렬화 과정
byte[4] = {00 00 0a 3}
  ↓ readFields()
IntWritable(163)

 

 

[ WritableComparable ]

  • Writable + Comparable( 비교 ) 기능
  • 맵리듀스 키 정렬 떄문에 필요
IntWritable extends WritableComparable<IntWritable>

 

 

[ RawComparator ]

  • 바이트 배열 상태에서 바로 비교
  • 객체 생성 없이 비교 가능 → 속도 빠름
int compare(byte[] b1, int s1, int l1,
            byte[] b2, int s2, int l2);

# b1, b2 → 비교할 바이트 배열
# s1~l1, s2~l2 → 시작 위치와 길이

 

 

[ WritableComparator ]

  • 하둡 기본 사용
  • 용 비교기 + 속도 최적화
  • 두 가지 비교 방법 지원
    1. 객체로 변환 후 비교 ( b1 → 객체1 → 값 추출 → 비교 )
    2. 바이트 상태에서 비교 ( b1, b2 → 바로 값 추출 → 비교 (객체 생성 오버헤드 없음) )
  • 하둡 내부 정렬 단계에서는 자동으로 RawComparator 사용 없으면 compareTo()로 폴백
# MapReduce 정렬 단계: 키 비교

       ┌─────────────┐
       │ RawComparator?│
       └───────┬─────┘
               │ 있으면
               ▼
       [ 바이트 상태로 비교 ] ← 속도 빠름
               │
               │ 없으면
               ▼
       [ 객체 생성 후 compareTo() ] ← 속도 느림

3.2 Writable 클래스

  • 하둡 내부 통신시 반드시 직렬화가 필요
  • 자바 기본 자료형( int, long, double... )는 바로 직렬화 불가 → Writable 래퍼 클래스 제공
기본 자료형 → Writable 래퍼 → 직렬화 → 바이트 스트림

 

 

[ 자바 기본 자료형 용 Writable 래퍼 ]

자바 자료형 Writable 클래스 직렬화 크기
boolean BooleanWritable 1바이트
byte ByteWritable 1바이트
short ShortWritable 2바이트
int IntWritable 4바이트
int VIntWritable 1~5바이트 (가변)
float FloatWritable 4바이트
long LongWritable 8바이트
long VLongWritable 1~9바이트 (가변)
double DoubleWritable 8바이트
  • 고정길이 vs 가변길이
    • 고정길이 : 항상 일정한 바이트 사용 → 연산 빠름
    • 가변길이 : 값이 작으면 바이트 절약 → 저장 공간 효율 ↑

 

[ 문자열용 Writable - Text ]

  • 하둡 문자열용 Writable 클래스
  • UTF-8 인코딩 사용 ( 1~4 바이트 )
  • 바이트 기준으로 인덱스 관리
String s = "가A";  // String 기준 인덱스
Text t = new Text("가A");  // Text 기준 = 바이트 기준
문자 UTF-8 인코딩 바이트 수
EA B0 80 3바이트
A 41 1바이트
  • Text 내부는 ByteBuffer + bytesToCodePoint() 로 문자 접근
  • 재활용 가능 → set()으로 문자열 바꿔도 내부 배열 재사용
    • getBytes() → 내부 배열 전체
    • getLength() → 실제 유효 데이터 길이
Text t = new Text("hadoop");
t.set("pig");
t.getBytes() → [p,i,g,o,o,p] (배열 재사용)
t.getLength() → 3 (실제 유효 데이터)
  • 문자열 조작 필요 시 → toString()로 String 변환
Text text = new Text("hadoop");
String str = text.toString(); // "hadoop"

 

 

[ 바이트 배열용 Writable - BytesWritable ]

  • 이미지, 압축파일 등 바이너리 데이터 전용 Writable 클래스
  • 구조
길이 정보(4바이트) + 데이터
  • set(), getBytes(), getLength() 지원
BytesWritable b = new BytesWritable(new byte[]{3,5});
b.set(new byte[]{1,2});
b.getBytes() → [1,2,7,9] (배열 재활용)
b.getLength() → 2 (유효 데이터)

 

 

[ NullWritable ]

  • 값이 필요 없을 떄 사용하는 빈 데이터
  • 길이 0, 직렬화 가능
값만 필요 → emit(NullWritable.get(), value)
키만 필요 → emit(key, NullWritable.get())

 

 

[ 범용 Writable - ObjectWritable & GenericWritable ]

## ObjectWritable
| ow1 | "org.apache.hadoop.io.Text" + "apple" |
| ow2 | "org.apache.hadoop.io.IntWritable" + 100 |
| ow3 | "org.apache.hadoop.io.BooleanWritable" + true |
| ow4 | "org.apache.hadoop.io.NullWritable" + null |

## GenericWritable
| gw1 | 0 | "banana" |
| gw2 | 1 | 200 |
| gw3 | 2 | false |
구분 ObjectWritable GenericWritable
저장 가능 데이터 모든 타입 미리 정의된 타입만
저장 방식 클래스명 + 데이터 인덱스 번호 + 데이터
유연성 높음 낮음
공간 효율 낮음 높음
  • ObjectWritable → 다양한 타입 썩어서 저장 가능
  • GenericWritable → 저장 공간 절약, 타입 미리 정의 필요

 

[ Writable 컬렉션 ]

클래스 용도
ArrayWritable 1차원 배열
TwoDArrayWritable 2차원 배열
ArrayPrimitiveWritable int[], double[] 등 기본형 배열
MapWritable Map<K,V> 구조
SortedMapWritable 정렬된 Map
EnumSetWritable enum 집합
## ArrayWritable
ArrayWritable arr = new ArrayWritable(Text.class);
arr.set(new Writable[]{new Text("sy"), new Text("jw"), new Text("sj")});

## MapWritable
MapWritable map = new MapWritable();
map.put(new Text("sy"), new IntWritable(28));
map.put(new Text("jw"), new IntWritable(31));

## 다른 타입 썩는 리스트 → ListWritable
public class ListWritable extends MapWritable {
    public void add(Writable value) {
        put(new IntWritable(size()), value);  // index를 key로 사용
    }
}

 

 

이처럼 하둡에서는 모든 데이터가 Writable 형태로 직렬화되어 처리됨!!