3주차 Unit 7.3 — Stream vs Channel

Psj·2026년 5월 19일

F-lab

목록 보기
103/197

Unit 7.3 — Stream vs Channel

F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림


📌 학습 목표

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

  • Stream (IO) 의 정밀한 구조와 동작은?
  • Channel (NIO) 의 정밀한 구조와 동작은?
  • Buffer 의 4가지 속성 (capacity, position, limit, mark) 의 정확한 의미와 변화는?
  • Buffer 의 쓰기/읽기 모드 전환 (flip, clear, rewind, compact) 은?
  • Heap Buffer vs Direct Buffer 의 결정적 차이는?
  • "Channel 은 반드시 Buffer 통해서" 의 정확한 의미는?
  • Stream 과 Channel 의 동일 작업 코드 비교는?
  • zero-copy 와 transferTo 의 원리는?
  • 실무에서 Stream/Channel 선택 기준은?

🎯 핵심 한 문장

Stream (IO) 은 "1바이트씩 흐르는 단방향 데이터 흐름", Channel (NIO) 은 "Buffer 를 통해 블록 단위로 양방향 입출력하는 통로" 다.
Stream 은 비유적으로 "수도꼭지에서 물 한 방울씩", Channel + Buffer 는 "양동이로 물 퍼서 옮기기".
Channel 은 항상 Buffer 통해서만 데이터를 주고받으며 (channel.read(buffer), channel.write(buffer)),
Buffer 의 position, limit, capacity 3가지 속성과 flip/clear 모드 전환을 정확히 이해해야 NIO 를 제대로 쓸 수 있다.
Direct Buffer + transferTo 로 OS 의 zero-copy 를 활용하면 일반 IO 보다 수십~수백 배 빠르다.

비유 — 수도꼭지 vs 양동이

Stream (IO):
  수도꼭지에서 물 받기
  - 한 방울 (1바이트) 씩 흐름
  - 단방향 (수도꼭지 → 컵, 또는 반대)
  - 보조 도구 (Buffered) 로 양 늘리기 가능
  - Blocking — 물이 안 나오면 대기

Channel + Buffer (NIO):
  양동이로 물 퍼서 옮기기
  - 양동이 (Buffer) 만큼 한 번에
  - 양방향 (한 양동이로 퍼고 붓기)
  - 양동이 크기 (capacity) 미리 정함
  - Non-blocking 가능 — 물 없으면 즉시 리턴

→ Stream = 흐름, Channel + Buffer = 양동이.


🧭 9개 섹션 로드맵

1. Stream (IO) 의 정밀 구조
2. Channel (NIO) 의 정밀 구조
3. Buffer 의 4가지 속성
4. Buffer 의 모드 전환 (flip, clear, rewind, compact)
5. Heap Buffer vs Direct Buffer
6. Stream vs Channel 동작 비교
7. 성능과 zero-copy
8. 실무 패턴과 선택 기준
9. 면접 + 자기 점검

1️⃣ Stream (IO) 의 정밀 구조

1.1 Stream 의 정의

Stream (java.io):

  데이터가 흐르는 일방향 통로.
  - 1바이트 (또는 1문자) 단위
  - 단방향 (Input 또는 Output)
  - 시작점과 끝점이 정해진 흐름

1.2 Stream 의 분류

java.io 의 스트림 4계열:

1. InputStream (바이트 입력)
   - 자식: FileInputStream, ByteArrayInputStream, ...
   - 메서드: read(), read(byte[]), read(byte[], int, int)

2. OutputStream (바이트 출력)
   - 자식: FileOutputStream, ByteArrayOutputStream, ...
   - 메서드: write(int), write(byte[]), write(byte[], int, int)

3. Reader (문자 입력)
   - 자식: FileReader, StringReader, ...
   - 메서드: read(), read(char[])

4. Writer (문자 출력)
   - 자식: FileWriter, StringWriter, ...
   - 메서드: write(int), write(char[]), write(String)

1.3 InputStream 의 핵심 메서드

public abstract class InputStream implements Closeable {
    
    // 핵심 메서드 (추상)
    public abstract int read() throws IOException;
    // - 1바이트 읽음
    // - 0~255 반환 (int 로 확장)
    // - 스트림 끝 = -1
    
    // 편의 메서드
    public int read(byte[] b) throws IOException {
        return read(b, 0, b.length);
    }
    
    public int read(byte[] b, int off, int len) throws IOException {
        // 여러 바이트 읽기
        // 실제 읽은 바이트 수 반환
        // 끝이면 -1
    }
    
    // 추가 메서드
    public int available() throws IOException;
    public long skip(long n) throws IOException;
    public void close() throws IOException;
    public boolean markSupported();
    public void mark(int readlimit);
    public void reset() throws IOException;
    
    // Java 9+
    public byte[] readAllBytes() throws IOException;
    public long transferTo(OutputStream out) throws IOException;
}

1.4 read() 의 반환값 정밀

// read() 가 int 를 반환하는 이유

byte b = (byte) -1;   // -1 = 1바이트로 1111_1111
// 정상 데이터 0xFF (= -1 as byte) 도 -1
// 그러면 "끝" 과 구분 못 함!

// 해결: int (32비트) 로 확장
public abstract int read() throws IOException;
// 0~255: 정상 데이터
// -1: 스트림 끝 (정상 데이터에 못 옴)

// 변환 시 주의
int b = in.read();
if (b == -1) {
    // 끝
} else {
    byte data = (byte) b;   // 명시적 캐스트 필요
    // 또는
    char ch = (char) b;
}

1.5 OutputStream 의 핵심 메서드

public abstract class OutputStream implements Closeable, Flushable {
    
    // 핵심 (추상)
    public abstract void write(int b) throws IOException;
    // - 1바이트 쓰기
    // - int 의 하위 8비트만 사용
    
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }
    
    public void write(byte[] b, int off, int len) throws IOException;
    
    // 추가
    public void flush() throws IOException;
    // 버퍼링된 데이터 강제 출력
    
    public void close() throws IOException;
    
    // Java 11+
    public static OutputStream nullOutputStream();
}

1.6 Stream 의 흐름 — 시각화

InputStream:

   외부 (파일)         스트림           프로그램
  [data..............] → → → → → → →  read()
                                         ↓
                                       byte b
                                       
   한 방향 (외부 → 프로그램)
   1바이트씩
   끝이 -1


OutputStream:

   프로그램            스트림           외부 (파일)
   write(b) → → → → → → → → → → → →  [data...]
                                       
   한 방향 (프로그램 → 외부)
   1바이트씩
   close() 또는 flush() 필요

1.7 Buffered 의 효과

// 일반 FileInputStream
FileInputStream fis = new FileInputStream("file.txt");
int b;
while ((b = fis.read()) != -1) {
    // 매 read() 가 OS system call
    // 1MB 파일 = 1,048,576번 호출
}

// Buffered 추가
BufferedInputStream bis = new BufferedInputStream(fis);   // 기본 8KB
int b;
while ((b = bis.read()) != -1) {
    // 사용자는 1바이트씩 호출하지만
    // 내부적으로 8KB 단위로 OS 호출
    // 1MB / 8KB = 128번 호출
}

// 또는 byte[] 활용
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
    // 한 번에 8192 바이트
    // OS 호출 횟수 ↓
}

1.8 Stream 의 핵심 특징 종합

Stream 의 5가지 특징:

1. 1바이트 (또는 1문자) 단위
   - byte / char 가 기본 단위
   - 보조 스트림으로 늘릴 수 있음

2. 단방향
   - InputStream OR OutputStream
   - 둘이 함께면 두 객체

3. 순차 접근
   - 처음부터 끝까지 순서대로
   - Random Access 어려움 (RandomAccessFile 예외)

4. Blocking
   - 데이터 올 때까지 대기
   - 다른 일 못 함

5. Decorator 패턴
   - 기본 + 보조 스트림 중첩
   - 기능 확장 (Buffered, Data, Object 등)

1.9 ILIC 의 Stream 활용

// CSV 한 줄씩 읽기 — Stream 의 자연스러운 활용
public class ShipmentCsvReader {
    
    public List<Shipment> read(Path path) throws IOException {
        List<Shipment> shipments = new ArrayList<>();
        
        try (BufferedReader reader = Files.newBufferedReader(path)) {
            // BufferedReader = Reader + 버퍼링
            // 한 줄씩 효율적으로 읽음
            String line;
            reader.readLine();   // 헤더 건너뛰기
            while ((line = reader.readLine()) != null) {
                shipments.add(parseShipment(line));
            }
        }
        
        return shipments;
    }
    
    private Shipment parseShipment(String line) {
        String[] parts = line.split(",");
        return Shipment.builder()
            .blNo(parts[0])
            .weight(new BigDecimal(parts[1]))
            .build();
    }
}

1.10 자기 점검 답변

Stream (IO) 의 핵심 특징 5가지는?

:
1. 1바이트/1문자 단위: 가장 작은 단위
2. 단방향: InputStream OR OutputStream
3. 순차 접근: 처음부터 끝까지
4. Blocking: 항상 대기
5. Decorator 패턴: 보조 스트림으로 확장

read() 의 반환이 int 인 이유:

  • byte (-128~127) 로는 "끝" 표현 불가
  • int 로 확장: 0~255 정상, -1 끝

2️⃣ Channel (NIO) 의 정밀 구조

2.1 Channel 의 정의

Channel (java.nio.channels):

  외부 자원과 데이터를 주고받는 양방향 통로.
  - 파일, 소켓, 파이프 등을 추상화
  - Buffer 를 통해서만 데이터 입출력
  - 블록 단위 처리
  - Non-blocking 가능

2.2 Channel 의 계층 구조

java.nio.channels 의 Channel 계층:

Channel (인터페이스)
  └── ReadableByteChannel       — read(ByteBuffer)
  └── WritableByteChannel       — write(ByteBuffer)
  
ByteChannel extends ReadableByteChannel, WritableByteChannel
  └── 양방향

GatheringByteChannel    — 여러 Buffer 한 번에 쓰기
ScatteringByteChannel   — 여러 Buffer 한 번에 읽기

자바 표준 구현:
  ├── FileChannel
  ├── SocketChannel        (TCP 클라이언트)
  ├── ServerSocketChannel  (TCP 서버)
  ├── DatagramChannel      (UDP)
  └── Pipe.SinkChannel / SourceChannel
  
비동기 채널 (Java 7+):
  ├── AsynchronousFileChannel
  ├── AsynchronousSocketChannel
  └── AsynchronousServerSocketChannel

2.3 ReadableByteChannel 의 read

public interface ReadableByteChannel extends Channel {
    
    int read(ByteBuffer dst) throws IOException;
    // - Buffer 에 데이터 읽기
    // - 실제 읽은 바이트 수 반환
    // - 스트림 끝 = -1
    // - Non-blocking 채널: 데이터 없으면 0
}

특징:

  • 반드시 ByteBuffer 를 인자로
  • 단순 int b 가 아니라 Buffer 에 채움
  • 읽는 위치는 Buffer 의 position 부터

2.4 WritableByteChannel 의 write

public interface WritableByteChannel extends Channel {
    
    int write(ByteBuffer src) throws IOException;
    // - Buffer 의 데이터 쓰기
    // - 실제 쓴 바이트 수 반환
    // - Non-blocking: 일부만 쓸 수도
}

2.5 FileChannel 의 정밀 메서드

public abstract class FileChannel extends AbstractInterruptibleChannel
        implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
    
    // 기본 read/write
    public abstract int read(ByteBuffer dst) throws IOException;
    public abstract int write(ByteBuffer src) throws IOException;
    
    // Random Access (Stream 못 함)
    public abstract long position() throws IOException;
    public abstract FileChannel position(long newPosition) throws IOException;
    public abstract long size() throws IOException;
    public abstract FileChannel truncate(long size) throws IOException;
    
    // 특정 위치에서 read/write
    public abstract int read(ByteBuffer dst, long position) throws IOException;
    public abstract int write(ByteBuffer src, long position) throws IOException;
    
    // zero-copy
    public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;
    public abstract long transferFrom(ReadableByteChannel src, long position, long count) throws IOException;
    
    // 메모리 매핑
    public abstract MappedByteBuffer map(MapMode mode, long position, long size) throws IOException;
    
    // 강제 동기
    public abstract void force(boolean metaData) throws IOException;
    
    // 락
    public abstract FileLock lock(long position, long size, boolean shared) throws IOException;
    public final FileLock lock() throws IOException;
    public abstract FileLock tryLock(long position, long size, boolean shared) throws IOException;
}

2.6 Channel 의 생성

// FileChannel
FileChannel ch1 = FileChannel.open(
    Path.of("file.txt"),
    StandardOpenOption.READ);

FileChannel ch2 = FileChannel.open(
    Path.of("file.txt"),
    StandardOpenOption.WRITE, StandardOpenOption.CREATE);

FileChannel ch3 = FileChannel.open(
    Path.of("file.txt"),
    StandardOpenOption.READ, StandardOpenOption.WRITE);

// 옵션
StandardOpenOption.READ
StandardOpenOption.WRITE
StandardOpenOption.APPEND
StandardOpenOption.CREATE
StandardOpenOption.CREATE_NEW
StandardOpenOption.TRUNCATE_EXISTING
StandardOpenOption.DELETE_ON_CLOSE

// SocketChannel
SocketChannel sc = SocketChannel.open(new InetSocketAddress("host", 8080));

// ServerSocketChannel
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8080));

// FileChannel from FileInputStream (legacy)
FileInputStream fis = new FileInputStream("file.txt");
FileChannel fch = fis.getChannel();

2.7 Channel 의 양방향 활용

// 양방향 — 같은 채널로 read + write
try (FileChannel channel = FileChannel.open(
        Path.of("file.txt"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    
    // 1. 처음부터 읽기
    ByteBuffer readBuf = ByteBuffer.allocate(1024);
    channel.read(readBuf);
    
    // 2. position 이동 + 쓰기
    channel.position(0);
    ByteBuffer writeBuf = ByteBuffer.wrap("Hello".getBytes());
    channel.write(writeBuf);
    
    // 3. 다시 읽기
    channel.position(0);
    readBuf.clear();
    channel.read(readBuf);
    
    // 한 채널로 모든 작업
}

2.8 Channel 의 핵심 특징

Channel 의 5가지 특징:

1. 양방향
   - 한 채널로 read + write
   - SocketChannel 도 양방향

2. 블록 단위
   - Buffer 의 크기만큼
   - 1바이트씩 X

3. Random Access
   - position() 으로 위치 이동
   - Stream 못 함 (RandomAccessFile 외)

4. Non-blocking 가능
   - configureBlocking(false)
   - Selector 와 결합

5. Buffer 필수
   - read/write 가 항상 ByteBuffer 매개변수
   - Buffer 없이 데이터 입출력 불가

2.9 ILIC 의 Channel 활용

public class ShipmentChannelService {
    
    // 양방향 파일 처리
    public void updateInPlace(Path file, long offset, byte[] newData) throws IOException {
        try (FileChannel channel = FileChannel.open(
                file,
                StandardOpenOption.READ, StandardOpenOption.WRITE)) {
            
            // 1. 특정 위치 읽기
            ByteBuffer readBuf = ByteBuffer.allocate(newData.length);
            channel.read(readBuf, offset);
            
            // 2. 같은 위치에 쓰기
            ByteBuffer writeBuf = ByteBuffer.wrap(newData);
            channel.write(writeBuf, offset);
        }
    }
    
    // 큰 파일의 일부 빠른 접근
    public byte[] readChunk(Path file, long offset, int length) throws IOException {
        try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(length);
            int read = channel.read(buffer, offset);
            
            buffer.flip();
            byte[] result = new byte[buffer.remaining()];
            buffer.get(result);
            return result;
        }
    }
}

2.10 자기 점검 답변

Channel 의 핵심 특징 5가지는?

:
1. 양방향: 한 채널로 read + write
2. 블록 단위: Buffer 크기만큼
3. Random Access: position() 이동
4. Non-blocking 가능: configureBlocking(false)
5. Buffer 필수: 모든 I/O 가 ByteBuffer 통해

Stream 과 차이:

  • Stream: 1바이트, 단방향, 순차, Blocking, byte 매개변수
  • Channel: 블록, 양방향, Random, Non-blocking 가능, Buffer 매개변수

3️⃣ Buffer 의 4가지 속성

3.1 Buffer 의 정의

Buffer (java.nio):

  데이터의 컨테이너.
  - 특정 타입 (byte, char, int 등) 의 배열
  - Channel 과 데이터 교환의 매개체
  - 4가지 속성으로 상태 관리

3.2 Buffer 의 4가지 속성

Buffer 의 4가지 속성:

capacity (용량):
  - 버퍼의 최대 크기
  - 생성 시 결정, 불변
  - 예: ByteBuffer.allocate(1024) 면 capacity = 1024

position (위치):
  - 다음 read/write 위치
  - 0 ≤ position ≤ limit

limit (한계):
  - read/write 가능한 마지막 위치 + 1
  - 0 ≤ limit ≤ capacity

mark (북마크, 선택):
  - position 의 임시 저장
  - reset() 으로 복귀
  - 기본 -1 (설정 안 됨)

불변식:
  0 ≤ mark ≤ position ≤ limit ≤ capacity

3.3 시각적 표현

capacity = 10 (생성 시)

쓰기 모드 (초기):
  [_, _, _, _, _, _, _, _, _, _]
   ↑ position = 0           ↑ limit = capacity = 10
  
  - position 부터 limit 까지 쓰기 가능
  - 처음엔 비어있음

데이터 5개 쓴 후:
  [A, B, C, D, E, _, _, _, _, _]
                  ↑ position = 5     ↑ limit = 10

flip() 호출 (읽기 모드로):
  [A, B, C, D, E, _, _, _, _, _]
   ↑ position = 0
                  ↑ limit = 5 (이전 position)

  - 0 ~ 5 사이가 읽을 데이터
  - position 부터 limit 까지 읽기

읽기 후 (3개 읽음):
  [A, B, C, D, E, _, _, _, _, _]
              ↑ position = 3
                  ↑ limit = 5
                  
  - 아직 D, E 남음

3.4 Buffer 생성

// 기본 생성 — capacity 만 지정
ByteBuffer buf1 = ByteBuffer.allocate(1024);
// capacity = 1024
// position = 0
// limit = 1024
// mark = -1

// 배열로부터 (wrap)
byte[] bytes = "Hello".getBytes();
ByteBuffer buf2 = ByteBuffer.wrap(bytes);
// 배열 공유 (변경이 서로 영향)
// capacity = 5
// position = 0
// limit = 5

// 직접 메모리
ByteBuffer buf3 = ByteBuffer.allocateDirect(1024);
// JVM heap 밖
// 다음 섹션 (Direct Buffer) 참고

3.5 position, limit, capacity 의 동작

ByteBuffer buf = ByteBuffer.allocate(10);

// 초기
buf.capacity();   // 10
buf.position();   // 0
buf.limit();      // 10
buf.remaining();  // limit - position = 10
buf.hasRemaining();   // true

// 데이터 5개 쓰기
buf.put((byte) 'A');
buf.put((byte) 'B');
buf.put((byte) 'C');
buf.put((byte) 'D');
buf.put((byte) 'E');

// 상태:
buf.capacity();   // 10
buf.position();   // 5
buf.limit();      // 10
buf.remaining();  // 5

// flip() — 쓰기 → 읽기
buf.flip();
buf.capacity();   // 10
buf.position();   // 0
buf.limit();      // 5
buf.remaining();  // 5

// 데이터 3개 읽기
byte b1 = buf.get();   // A
byte b2 = buf.get();   // B
byte b3 = buf.get();   // C

buf.position();   // 3
buf.remaining();  // 2

// 남은 D, E 도 읽기
while (buf.hasRemaining()) {
    System.out.print((char) buf.get());
}
// DE 출력

3.6 mark 의 활용

ByteBuffer buf = ByteBuffer.allocate(10);
buf.put("Hello".getBytes());
buf.flip();

buf.get();   // H
buf.mark();   // position 표시 (1)

buf.get();   // e
buf.get();   // l

buf.reset();   // position = mark = 1

buf.get();   // e (다시)

// 활용: 임시 위치 저장 + 복귀

3.7 Buffer 의 종류

// 8가지 기본 타입에 대한 Buffer
ByteBuffer bb = ByteBuffer.allocate(1024);
CharBuffer cb = CharBuffer.allocate(1024);
ShortBuffer sb = ShortBuffer.allocate(1024);
IntBuffer ib = IntBuffer.allocate(1024);
LongBuffer lb = LongBuffer.allocate(1024);
FloatBuffer fb = FloatBuffer.allocate(1024);
DoubleBuffer db = DoubleBuffer.allocate(1024);
// boolean 은 없음

// ByteBuffer 가 가장 자주 사용
// 다른 타입은 ByteBuffer 로부터 변환 가능
ByteBuffer bb = ByteBuffer.allocate(1024);
IntBuffer ib = bb.asIntBuffer();   // 같은 데이터를 int 로 해석

3.8 ByteBuffer 의 put / get

ByteBuffer buf = ByteBuffer.allocate(1024);

// put — 다양한 타입
buf.put((byte) 65);                // 1바이트
buf.putChar('A');                   // 2바이트
buf.putShort((short) 100);          // 2바이트
buf.putInt(42);                     // 4바이트
buf.putLong(123L);                  // 8바이트
buf.putFloat(3.14f);                // 4바이트
buf.putDouble(2.718);               // 8바이트
buf.put("Hello".getBytes());        // 5바이트

// get — 동일하게
buf.flip();
byte b = buf.get();
char c = buf.getChar();
short s = buf.getShort();
int i = buf.getInt();
long l = buf.getLong();
float f = buf.getFloat();
double d = buf.getDouble();

// 인덱스 지정 (position 변경 없음)
buf.put(0, (byte) 'A');
byte b = buf.get(0);

// 배열로
byte[] dst = new byte[10];
buf.get(dst);   // dst 에 채움
buf.get(dst, 0, 5);

3.9 자기 점검 답변

Buffer 의 4가지 속성과 불변식은?

:
1. capacity: 버퍼 최대 크기 (불변)
2. position: 다음 read/write 위치
3. limit: read/write 가능한 마지막 위치 + 1
4. mark: position 의 임시 저장 (선택)

불변식:

0 ≤ mark ≤ position ≤ limit ≤ capacity

활용:

  • 쓰기 시 position 이 증가
  • flip() 으로 limit = position, position = 0
  • 읽기 시 position 이 증가
  • 다 읽으면 position = limit

4️⃣ Buffer 의 모드 전환 (flip, clear, rewind, compact)

4.1 모드 전환 메서드

public abstract class Buffer {
    
    // flip — 쓰기 → 읽기
    public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
    
    // clear — 처음 상태로 (초기화, 비우기)
    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
    
    // rewind — position 만 0 으로
    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
    
    // compact — 안 읽은 데이터를 앞으로
    // (구체 클래스에 정의)
}

4.2 flip — 쓰기 → 읽기 전환

ByteBuffer buf = ByteBuffer.allocate(10);

// 쓰기 모드
buf.put("Hello".getBytes());   // 5바이트
// position = 5, limit = 10

// flip — 읽기 준비
buf.flip();
// position = 0, limit = 5

// 읽기
while (buf.hasRemaining()) {
    System.out.print((char) buf.get());
}
// Hello 출력

4.3 clear — 처음 상태로

ByteBuffer buf = ByteBuffer.allocate(10);
buf.put("Hello".getBytes());
buf.flip();

// 일부 읽음
buf.get();   // H
buf.get();   // e

// clear — 처음 상태로 (데이터는 그대로 남지만 무시)
buf.clear();
// position = 0, limit = 10

// 새 데이터 쓰기 가능
buf.put("World".getBytes());
// 기존 'l', 'l', 'o' 부분은 덮어쓰일 수도

// 주의: clear 가 데이터를 지우지 않음
// 단지 position/limit 만 리셋

4.4 rewind — position 만 0

ByteBuffer buf = ByteBuffer.allocate(10);
buf.put("Hello".getBytes());
buf.flip();

// 읽기
buf.get();   // H
buf.get();   // e

// rewind — 처음으로
buf.rewind();
// position = 0, limit = 그대로 (5)

// 처음부터 다시 읽기
while (buf.hasRemaining()) {
    System.out.print((char) buf.get());
}
// Hello 출력 (전체)

4.5 compact — 안 읽은 데이터 앞으로

ByteBuffer buf = ByteBuffer.allocate(10);
buf.put("Hello".getBytes());
buf.flip();

// 일부 읽음
buf.get();   // H
buf.get();   // e

// 상태: [H, e, l, l, o, _, _, _, _, _]
//                   ↑ position = 2
//                          ↑ limit = 5

// compact — 안 읽은 (l, l, o) 를 앞으로
buf.compact();
// 결과: [l, l, o, _, _, _, _, _, _, _]
//                ↑ position = 3
//                                ↑ limit = 10 (capacity)

// 이제 추가 쓰기 가능 (쓰기 모드)
buf.put("!".getBytes());
// 결과: [l, l, o, !, ...]

4.6 4가지 메서드 비교

메서드효과positionlimit데이터
flip쓰기 → 읽기0이전 position그대로
clear비우기 (재사용)0capacity그대로 (무시)
rewind처음으로 (다시 읽기)0그대로그대로
compact안 읽은 거 앞으로안 읽은 개수capacity이동됨

4.7 일반적 사용 흐름

// 패턴 1: 쓰기 → 읽기 → 비우기 → 다시 쓰기

ByteBuffer buf = ByteBuffer.allocate(1024);

while (channel.read(buf) > 0) {   // 쓰기 (Channel → Buffer)
    buf.flip();                    // 읽기 모드
    while (buf.hasRemaining()) {
        process(buf.get());         // 읽기
    }
    buf.clear();                    // 비우기, 다시 쓸 준비
}

// 패턴 2: 쓰기 → 읽기 → 일부 처리 후 compact → 더 쓰기

ByteBuffer buf = ByteBuffer.allocate(1024);
channel.read(buf);

buf.flip();
// 일부 처리
processSomeData(buf);

buf.compact();   // 안 읽은 데이터 앞으로, 새 데이터 받을 준비
channel.read(buf);   // 추가 데이터 읽기 (이어서)

4.8 흔한 실수

// 실수 1: flip 안 함
ByteBuffer buf = ByteBuffer.allocate(10);
buf.put("Hello".getBytes());

// 바로 읽기 시도
while (buf.hasRemaining()) {
    System.out.print((char) buf.get());
}
// 잘못된 결과 — position = 5, limit = 10
// 빈 데이터 (0x00) 5개 출력

// 해결
buf.flip();   // 읽기 모드
while (buf.hasRemaining()) {
    System.out.print((char) buf.get());
}
// Hello

// 실수 2: clear 대신 flip
ByteBuffer buf = ByteBuffer.allocate(1024);
while (channel.read(buf) > 0) {
    buf.flip();
    process(buf);
    buf.flip();   // ❌ 잘못
    // 두 번 flip 하면: limit = 0, position = 0
}

// 해결
buf.clear();   // 다음 쓰기 준비

4.9 ILIC 활용 패턴

public class ShipmentBufferReader {
    
    // 큰 파일을 Channel + Buffer 로 효율적으로 읽기
    public List<String> readLines(Path file) throws IOException {
        List<String> lines = new ArrayList<>();
        ByteBuffer buffer = ByteBuffer.allocate(8192);   // 8KB
        StringBuilder line = new StringBuilder();
        
        try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
            while (channel.read(buffer) > 0) {
                buffer.flip();   // 읽기 모드
                
                while (buffer.hasRemaining()) {
                    char c = (char) buffer.get();
                    if (c == '\n') {
                        lines.add(line.toString());
                        line.setLength(0);
                    } else {
                        line.append(c);
                    }
                }
                
                buffer.clear();   // 다음 읽기 준비
            }
            
            if (line.length() > 0) {
                lines.add(line.toString());
            }
        }
        
        return lines;
    }
}

4.10 자기 점검 답변

flip, clear, rewind, compact 의 차이는?

:
1. flip: 쓰기 → 읽기 (limit = position, position = 0)
2. clear: 비우기 (position = 0, limit = capacity, 데이터는 안 지움)
3. rewind: 처음으로 (position = 0, limit 유지)
4. compact: 안 읽은 데이터 앞으로 (재사용)

언제?:

  • 쓰기 끝나고 읽기 → flip
  • 모두 읽고 새 데이터 받기 → clear
  • 다시 처음부터 읽기 → rewind
  • 일부만 읽고 새 데이터 받기 → compact

5️⃣ Heap Buffer vs Direct Buffer

5.1 두 종류의 Buffer

Heap Buffer:
  ByteBuffer.allocate(1024)
  - JVM heap 안의 메모리
  - byte[] 배열 기반
  - GC 가 관리
  - 할당/해제 빠름

Direct Buffer:
  ByteBuffer.allocateDirect(1024)
  - JVM heap 밖의 메모리 (off-heap)
  - OS 의 native 메모리
  - GC 가 직접 관리 X
  - I/O 시 복사 불필요

5.2 Heap Buffer 의 동작

Heap Buffer 의 흐름 (파일 읽기):

  [OS 파일]
     ↓
  [OS 의 native 버퍼]
     ↓ (복사 1)
  [JVM 의 Direct 버퍼] (임시)
     ↓ (복사 2)
  [JVM Heap 의 Heap Buffer (byte[])]
     ↓
  [사용자 코드]

→ 복사 2번 발생
→ 작은 데이터는 OK, 큰 데이터는 비효율

5.3 Direct Buffer 의 동작

Direct Buffer 의 흐름 (파일 읽기):

  [OS 파일]
     ↓
  [OS 의 native 버퍼]
     ↓ (복사 1)
  [Direct Buffer (off-heap)]
     ↓
  [사용자 코드]

→ 복사 1번
→ 큰 데이터에 효율적
→ JVM heap 우회

5.4 코드 비교

// Heap Buffer
ByteBuffer heap = ByteBuffer.allocate(1024);
// JVM heap 안
// byte[] 기반

// Direct Buffer
ByteBuffer direct = ByteBuffer.allocateDirect(1024);
// JVM heap 밖
// native 메모리

// 사용은 동일
heap.put("Hello".getBytes());
direct.put("Hello".getBytes());

// 검사
heap.isDirect();    // false
direct.isDirect();  // true

5.5 성능 비교

시나리오: 1GB 파일 복사

Heap Buffer (1MB 단위):
  - 1024번 read/write
  - 매번 2번 복사
  - 총 복사: 2 * 1GB = 2GB 데이터 이동

Direct Buffer (1MB 단위):
  - 1024번 read/write
  - 매번 1번 복사
  - 총 복사: 1GB 데이터 이동

성능 차이:
  - 작은 데이터 (< 1MB): Heap 이 빠를 수도 (할당 비용)
  - 큰 데이터 (> 1MB): Direct 가 빠름
  - 자주 재사용: Direct 가 매우 유리

5.6 Direct Buffer 의 단점

Direct Buffer 의 단점:

1. 할당/해제 비용 큼
   - native 메모리 할당 = system call
   - 자주 만들면 오히려 느림

2. GC 가 직접 정리 안 함
   - Cleaner (PhantomReference 기반) 으로 정리
   - 정리 시점 예측 불가
   - 메모리 누수 가능

3. 메모리 사용 추적 어려움
   - JVM heap 통계에 안 잡힘
   - -XX:MaxDirectMemorySize 로 제한 가능

4. OutOfMemoryError 가능
   - Direct memory 한도 초과 시
   - 일반 OOM 과 다른 메시지

5.7 언제 어느 것을?

Heap Buffer 권장:
  ✓ 작은 데이터 (< 1MB)
  ✓ 일회성 사용
  ✓ 자주 새로 만듦
  ✓ JVM heap 안에서 작업

Direct Buffer 권장:
  ✓ 큰 데이터 (> 1MB)
  ✓ 자주 재사용
  ✓ 네트워크 I/O 빈번
  ✓ JNI 와 통신

실무 패턴:
  - 풀링 (BufferPool) 으로 재사용
  - Netty 같은 라이브러리가 자동 관리

5.8 메모리 매핑 (MappedByteBuffer)

// 파일을 메모리에 직접 매핑
try (FileChannel channel = FileChannel.open(
        Path.of("large.dat"),
        StandardOpenOption.READ, StandardOpenOption.WRITE)) {
    
    MappedByteBuffer mapped = channel.map(
        FileChannel.MapMode.READ_WRITE,
        0,                  // 시작 위치
        channel.size());    // 매핑할 크기
    
    // mapped 는 파일과 직접 연결된 메모리
    // 읽기 빠름 (페이지 교체)
    // 쓰기는 OS 가 비동기로 디스크 반영
    
    byte b = mapped.get(0);
    mapped.put(0, (byte) 'A');
    
    // force() 로 강제 디스크 쓰기
    mapped.force();
}

// 활용:
// - 매우 큰 파일의 일부만 자주 접근
// - DB 엔진의 페이지 캐시
// - 고성능 IPC

5.9 ILIC 의 Direct Buffer 활용

public class HighPerformanceLogger {
    
    // 고빈도 로깅을 위한 재사용 Direct Buffer
    private static final ByteBuffer LOG_BUFFER = 
        ByteBuffer.allocateDirect(64 * 1024);   // 64KB
    
    private static final Object LOCK = new Object();
    
    public void log(String message, FileChannel channel) throws IOException {
        byte[] data = (message + "\n").getBytes(StandardCharsets.UTF_8);
        
        synchronized (LOCK) {
            // Buffer 재사용
            if (LOG_BUFFER.remaining() < data.length) {
                // 버퍼 가득 → flush
                LOG_BUFFER.flip();
                channel.write(LOG_BUFFER);
                LOG_BUFFER.clear();
            }
            LOG_BUFFER.put(data);
        }
    }
    
    public void flush(FileChannel channel) throws IOException {
        synchronized (LOCK) {
            LOG_BUFFER.flip();
            channel.write(LOG_BUFFER);
            LOG_BUFFER.clear();
        }
    }
}

5.10 자기 점검 답변

Heap Buffer 와 Direct Buffer 의 차이와 선택은?

:
1. 위치:

  • Heap: JVM heap 안 (byte[])
  • Direct: heap 밖 (native 메모리)
  1. I/O 시 복사:

    • Heap: 2번 (OS → Direct 임시 → Heap)
    • Direct: 1번 (OS → Direct)
  2. 할당 비용:

    • Heap: 빠름
    • Direct: 비쌈 (system call)
  3. GC:

    • Heap: GC 가 관리
    • Direct: Cleaner 로 정리 (예측 어려움)
  4. 선택:

    • 작은 데이터, 일회성 → Heap
    • 큰 데이터, 재사용 → Direct

6️⃣ Stream vs Channel 동작 비교

6.1 동일 작업의 비교

// 작업: 파일을 1024 바이트씩 읽어서 처리

// Stream (IO) 방식
try (FileInputStream fis = new FileInputStream("file.txt");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    
    byte[] buf = new byte[1024];
    int n;
    while ((n = bis.read(buf)) != -1) {
        process(buf, 0, n);   // n 바이트만 처리
    }
}

// Channel (NIO) 방식
try (FileChannel channel = FileChannel.open(
        Path.of("file.txt"), StandardOpenOption.READ)) {
    
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while (channel.read(buffer) > 0) {
        buffer.flip();
        process(buffer);   // Buffer 자체를 처리
        buffer.clear();
    }
}

6.2 차이점 분석

Stream 방식:
  - byte[] 배열 직접 다룸
  - read() 가 채운 바이트 수 반환
  - n 만큼만 처리 (마지막 read 에서 부분 채움 주의)
  - 가독성 ↑ (직관적)

Channel 방식:
  - ByteBuffer 다룸
  - position/limit 자동 관리
  - flip/clear 모드 전환 필요
  - 가독성 ↓ (Buffer 학습 필요)
  - 양방향, Non-blocking 등 추가 기능

6.3 Stream 의 효율 패턴

// 한 번에 충분히 크게
byte[] buf = new byte[8192];   // 8KB
int n;
while ((n = in.read(buf)) != -1) {
    out.write(buf, 0, n);
}
// BufferedInputStream 도 비슷한 효과

// 더 효율: Java 9+ transferTo
in.transferTo(out);
// 내부적으로 최적화

6.4 Channel 의 효율 패턴

// 1. 일반 Channel 복사
try (FileChannel src = FileChannel.open(srcPath, READ);
     FileChannel dest = FileChannel.open(destPath, WRITE, CREATE)) {
    
    ByteBuffer buffer = ByteBuffer.allocate(8192);
    while (src.read(buffer) > 0) {
        buffer.flip();
        dest.write(buffer);
        buffer.clear();
    }
}

// 2. zero-copy (가장 빠름)
try (FileChannel src = FileChannel.open(srcPath, READ);
     FileChannel dest = FileChannel.open(destPath, WRITE, CREATE)) {
    
    src.transferTo(0, src.size(), dest);
    // OS 의 sendfile 시스템 호출 활용 (Linux)
    // 커널 ↔ 유저 공간 복사 회피
}

6.5 성능 측정 결과 (예시)

1GB 파일 복사 성능 (예시):

방식 1: Stream (1바이트씩)
  - 너무 느림 (사용 안 함)

방식 2: Stream + BufferedInputStream
  - ~3000ms

방식 3: Stream + byte[8192]
  - ~1500ms

방식 4: Channel + Heap Buffer
  - ~1200ms

방식 5: Channel + Direct Buffer
  - ~900ms

방식 6: Channel.transferTo (zero-copy)
  - ~400ms

→ zero-copy 가 가장 빠름
→ 작은 파일은 차이 미미

6.6 함수형 패턴 (Java 8+)

// Stream + Files.lines (실제로는 java.io 의 BufferedReader 기반)
try (Stream<String> lines = Files.lines(Path.of("file.txt"))) {
    lines.filter(l -> !l.isEmpty())
        .map(this::process)
        .forEach(System.out::println);
}

// Channel + ByteBuffer 는 함수형과 직접 결합 어려움
// 보통 Stream API 와 함께 사용

6.7 Stream vs Channel 종합

항목Stream (IO)Channel (NIO)
데이터 단위byte / byte[]ByteBuffer
방향단방향양방향
위치 관리순차Random Access
Blocking항상Non-blocking 가능
Buffer보조 스트림 (Buffered)필수
가독성↓ (학습 곡선)
성능 (단순 작업)OK약간 빠름
성능 (zero-copy)불가가능
메모리 매핑XMappedByteBuffer
직렬화✓ (ObjectStream)X (직접 안 됨)
네트워크 대규모어려움권장

6.8 자기 점검 답변

같은 작업을 Stream/Channel 로 할 때 차이는?

:
1. 데이터 단위:

  • Stream: byte[] 배열
  • Channel: ByteBuffer
  1. 위치 관리:

    • Stream: 순차, n 만큼만 처리
    • Channel: position/limit, flip/clear
  2. 성능:

    • 단순 작업: 비슷
    • zero-copy: Channel 만
  3. 가독성:

    • Stream: 직관적
    • Channel: 학습 필요
  4. 확장:

    • Stream: Decorator
    • Channel: Non-blocking, Selector

7️⃣ 성능과 zero-copy

7.1 일반 I/O 의 데이터 흐름

일반 파일 → 네트워크 복사:

1. 사용자가 read() 호출
2. context switch (user → kernel)
3. 디스크 → kernel buffer (DMA)
4. kernel buffer → user buffer (CPU 복사)
5. context switch (kernel → user)
6. 사용자가 write() 호출
7. context switch (user → kernel)
8. user buffer → socket buffer (CPU 복사)
9. socket buffer → 네트워크 (DMA)
10. context switch (kernel → user)

총: 4번 복사, 4번 context switch

7.2 zero-copy 의 데이터 흐름

zero-copy (transferTo, Linux sendfile):

1. 사용자가 transferTo() 호출
2. context switch (user → kernel)
3. 디스크 → kernel buffer (DMA)
4. kernel buffer → socket buffer (DMA, 또는 직접)
5. socket buffer → 네트워크 (DMA)
6. context switch (kernel → user)

총: 2번 복사 (DMA), 2번 context switch
→ CPU 복사 제거
→ context switch 절반

7.3 zero-copy 활용

// FileChannel.transferTo (가장 권장)
try (FileChannel src = FileChannel.open(srcPath, READ);
     FileChannel dest = FileChannel.open(destPath, WRITE, CREATE)) {
    
    src.transferTo(0, src.size(), dest);
    // 내부적으로 OS 의 sendfile() 시스템 호출
    // Linux: sendfile, Windows: TransmitFile
}

// FileChannel.transferFrom — 반대 방향
dest.transferFrom(src, 0, src.size());

// 파일 → 네트워크
try (FileChannel file = FileChannel.open(filePath, READ);
     SocketChannel socket = SocketChannel.open(addr)) {
    
    file.transferTo(0, file.size(), socket);
    // 매우 효율적
    // 웹 서버의 파일 전송 (Nginx 등) 의 핵심
}

7.4 zero-copy 의 한계

zero-copy 의 제약:

1. OS 의 지원 필요
   - Linux: sendfile ✓
   - Windows: TransmitFile (소켓만)
   - 다른 OS 는 fallback

2. 변환 작업 불가
   - 데이터를 그대로 전달
   - 압축, 암호화 등 처리 X
   - 처리 필요하면 일반 I/O

3. 크기 제한
   - Linux sendfile: 2GB 제한 (구버전)
   - 큰 파일은 분할

4. 일부 채널만
   - FileChannel.transferTo 가 가장 효과적
   - SocketChannel 도 가능

7.5 MappedByteBuffer — 다른 고성능 방식

// 메모리 매핑 — 파일을 메모리처럼
try (FileChannel channel = FileChannel.open(
        path, READ, WRITE)) {
    
    MappedByteBuffer mapped = channel.map(
        FileChannel.MapMode.READ_WRITE,
        0, channel.size());
    
    // 파일을 직접 메모리처럼 다룸
    byte b = mapped.get(0);
    mapped.put(1000, (byte) 'A');
    
    // OS 의 페이지 교체 활용
    // 큰 파일의 랜덤 접근에 빠름
    
    mapped.force();   // 디스크 동기화
}

// 활용:
// - DB 엔진 (페이지 캐시)
// - 큰 인덱스 파일
// - IPC (공유 메모리)

7.6 Direct Buffer + Channel 의 효과

// 큰 파일 처리 — Direct Buffer + Channel
try (FileChannel src = FileChannel.open(srcPath, READ);
     FileChannel dest = FileChannel.open(destPath, WRITE, CREATE)) {
    
    ByteBuffer buffer = ByteBuffer.allocateDirect(64 * 1024);   // 64KB
    
    while (src.read(buffer) > 0) {
        buffer.flip();
        dest.write(buffer);
        buffer.clear();
    }
}

// Direct Buffer 의 이점:
// - OS 와 직접 통신
// - 추가 복사 회피
// - 큰 파일 처리에 효과적

7.7 성능 비교 (정리)

파일 복사 성능 (1GB):

1. FileInputStream.read() 1바이트:
   매우 느림 (사용 X)
   ~수십 초

2. FileInputStream.read(byte[]) 8KB:
   ~3 초

3. FileChannel + Heap Buffer:
   ~2.5 초

4. FileChannel + Direct Buffer:
   ~2 초

5. FileChannel.transferTo (zero-copy):
   ~1 초

6. MappedByteBuffer:
   ~1.5 초 (랜덤 접근 빠름)

→ 큰 파일은 zero-copy 또는 메모리 매핑
→ 작은 파일은 Stream 도 충분

7.8 ILIC 의 고성능 파일 처리

public class HighPerformanceFileService {
    
    // 1. zero-copy 파일 복사
    public void copyLargeFile(Path src, Path dest) throws IOException {
        try (FileChannel srcCh = FileChannel.open(src, StandardOpenOption.READ);
             FileChannel destCh = FileChannel.open(dest,
                StandardOpenOption.CREATE, StandardOpenOption.WRITE)) {
            
            long size = srcCh.size();
            long transferred = 0;
            
            while (transferred < size) {
                long n = srcCh.transferTo(transferred, size - transferred, destCh);
                transferred += n;
            }
        }
    }
    
    // 2. 메모리 매핑으로 큰 파일 분석
    public BigDecimal sumColumnFromLargeFile(Path file, int columnIndex) throws IOException {
        BigDecimal sum = BigDecimal.ZERO;
        
        try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) {
            MappedByteBuffer buffer = channel.map(
                FileChannel.MapMode.READ_ONLY, 0, channel.size());
            
            StringBuilder line = new StringBuilder();
            while (buffer.hasRemaining()) {
                char c = (char) buffer.get();
                if (c == '\n') {
                    String[] parts = line.toString().split(",");
                    sum = sum.add(new BigDecimal(parts[columnIndex]));
                    line.setLength(0);
                } else {
                    line.append(c);
                }
            }
        }
        
        return sum;
    }
    
    // 3. 파일 → 네트워크 직접 전송
    public void streamToClient(Path file, SocketChannel client) throws IOException {
        try (FileChannel ch = FileChannel.open(file, StandardOpenOption.READ)) {
            long size = ch.size();
            long sent = 0;
            while (sent < size) {
                long n = ch.transferTo(sent, size - sent, client);
                sent += n;
            }
            // 웹 서버의 파일 다운로드 핵심
        }
    }
}

7.9 자기 점검 답변

zero-copy 의 원리와 활용은?

:
1. 일반 I/O:

  • 4번 복사 (디스크 → kernel → user → kernel → socket)
  • 4번 context switch
  1. zero-copy:

    • 2번 복사 (DMA 만)
    • 2번 context switch
    • CPU 복사 제거
  2. 활용:

    • FileChannel.transferTo
    • 파일 → 네트워크 전송
    • 큰 파일 복사
  3. 한계:

    • OS 의존
    • 변환 작업 불가 (압축/암호화 등)
    • 크기 제한
  4. 자바 표준 활용:

    • Tomcat, Nginx 의 파일 전송
    • 다운로드 서버

8️⃣ 실무 패턴과 선택 기준

8.1 어느 것을 언제?

실무 선택 가이드:

Stream (java.io) 선택:
  ✓ 텍스트 파일 읽기/쓰기
  ✓ 직렬화 (ObjectStream)
  ✓ 작은 파일
  ✓ 간단한 코드 우선
  ✓ Files.lines, Files.readString 활용

Channel (java.nio) 선택:
  ✓ 큰 파일 (>1MB)
  ✓ zero-copy 필요
  ✓ 메모리 매핑
  ✓ 네트워크 (대규모 동시 연결)
  ✓ Random Access
  ✓ 비동기 I/O

8.2 구체적 시나리오

시나리오 1: CSV 파일 처리 (10MB)
  → Stream + BufferedReader
  → Files.lines + Stream API
  → 충분히 빠름, 가독성 ↑

시나리오 2: 대용량 로그 분석 (10GB)
  → Channel + MappedByteBuffer
  → 랜덤 접근 빠름
  → 또는 Stream API 의 라인 처리

시나리오 3: 파일 다운로드 서버
  → FileChannel.transferTo (zero-copy)
  → 매우 효율적
  → Nginx, Tomcat 의 기본

시나리오 4: 객체 직렬화
  → ObjectOutputStream (IO)
  → NIO 와 결합 가능
  → Files.newOutputStream + ObjectOutputStream

시나리오 5: 실시간 동시 연결 (수천)
  → SocketChannel + Selector
  → 또는 Netty 같은 라이브러리

8.3 라이브러리 의존

실무 라이브러리:

Apache Commons IO:
  - IOUtils.copy(in, out)
  - IOUtils.toByteArray(in)
  - Stream 시대의 편의

Google Guava:
  - ByteStreams, CharStreams
  - Stream 기반

Netty:
  - Non-blocking 네트워크 표준
  - Direct Buffer 풀링
  - zero-copy 최적화

Project Reactor:
  - Reactive Streams
  - Spring WebFlux

Spring:
  - Resource 추상화
  - 내부적으로 NIO 활용

8.4 Spring Boot 의 I/O

// Spring Boot 의 파일 업로드
@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) throws IOException {
    Path dest = Path.of("/uploads", file.getOriginalFilename());
    
    // 옵션 1: Spring 의 transferTo (내부적으로 NIO)
    file.transferTo(dest);
    
    // 옵션 2: 명시적 NIO
    try (InputStream is = file.getInputStream();
         OutputStream os = Files.newOutputStream(dest)) {
        is.transferTo(os);   // Java 9+ Stream 의 transferTo
    }
    
    return "uploaded";
}

// Spring Boot 의 파일 다운로드
@GetMapping("/download/{name}")
public ResponseEntity<Resource> download(@PathVariable String name) {
    Path path = Path.of("/uploads", name);
    Resource resource = new FileSystemResource(path);
    
    return ResponseEntity.ok()
        .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + name)
        .body(resource);
    // Spring 이 내부적으로 zero-copy 활용 가능
}

8.5 Buffer 재사용 패턴

// 잘못된 패턴 — 매번 새 Buffer
public void copy(Path src, Path dest) throws IOException {
    try (FileChannel s = FileChannel.open(src, READ);
         FileChannel d = FileChannel.open(dest, WRITE)) {
        
        ByteBuffer buf = ByteBuffer.allocateDirect(64 * 1024);
        // 매 호출마다 Direct Buffer 할당 (비싼 비용)
        // ...
    }
}

// 권장 패턴 — Buffer Pool
public class BufferPool {
    private final Queue<ByteBuffer> pool = new ConcurrentLinkedQueue<>();
    private final int bufferSize;
    
    public BufferPool(int bufferSize) {
        this.bufferSize = bufferSize;
    }
    
    public ByteBuffer acquire() {
        ByteBuffer buf = pool.poll();
        if (buf == null) {
            buf = ByteBuffer.allocateDirect(bufferSize);
        }
        return buf;
    }
    
    public void release(ByteBuffer buf) {
        buf.clear();
        pool.offer(buf);
    }
}

// 사용
BufferPool pool = new BufferPool(64 * 1024);
ByteBuffer buf = pool.acquire();
try {
    // 사용
} finally {
    pool.release(buf);
}

8.6 ILIC 의 종합 패턴

@Service
public class ShipmentFileIoService {
    
    private final BufferPool bufferPool = new BufferPool(64 * 1024);
    
    // 1. 일반 CSV — Stream
    public List<Shipment> importCsv(Path src) throws IOException {
        try (Stream<String> lines = Files.lines(src)) {
            return lines
                .skip(1)
                .map(this::parseShipment)
                .toList();
        }
    }
    
    // 2. 큰 파일 export — Stream + Buffered
    public void exportLargeCsv(List<Shipment> shipments, Path dest) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(dest)) {
            writer.write("id,blNo,weight,createdAt\n");
            for (Shipment s : shipments) {
                writer.write(s.toCsvLine());
                writer.newLine();
            }
        }
    }
    
    // 3. 매우 큰 파일 복사 — zero-copy
    public void copyLargeBackup(Path src, Path dest) throws IOException {
        try (FileChannel srcCh = FileChannel.open(src, READ);
             FileChannel destCh = FileChannel.open(dest, CREATE, WRITE)) {
            
            long size = srcCh.size();
            long transferred = 0;
            while (transferred < size) {
                transferred += srcCh.transferTo(transferred, size - transferred, destCh);
            }
        }
    }
    
    // 4. 큰 파일 부분 읽기 — Channel + position
    public byte[] readShipmentBlock(Path file, long offset, int length) throws IOException {
        try (FileChannel channel = FileChannel.open(file, READ)) {
            ByteBuffer buffer = bufferPool.acquire();
            try {
                buffer.limit(length);
                channel.read(buffer, offset);
                buffer.flip();
                byte[] result = new byte[buffer.remaining()];
                buffer.get(result);
                return result;
            } finally {
                bufferPool.release(buffer);
            }
        }
    }
    
    // 5. 직렬화 — ObjectStream (IO) + Files (NIO.2)
    public void saveBinary(Shipment s, Path dest) throws IOException {
        try (OutputStream os = Files.newOutputStream(dest);
             ObjectOutputStream oos = new ObjectOutputStream(os)) {
            oos.writeObject(s);
        }
    }
}

8.7 결합 활용

실무에서 IO + NIO 결합:

1. NIO.2 의 Files 가 IO 의 클래스 반환
   - Files.newBufferedReader → BufferedReader (IO)
   - Files.newInputStream → InputStream (IO)

2. IO 의 ObjectStream + NIO.2 의 Files
   - 직렬화는 ObjectStream
   - 파일은 NIO.2

3. Stream API + Files
   - Files.lines (Stream + IO 결합)
   - 함수형 + 효율

4. Channel + Stream
   - Channels.newInputStream(channel)
   - Channel 을 InputStream 으로
   - 두 API 호환

8.8 자기 점검 답변

실무에서 Stream/Channel 선택의 기준은?

:
1. 데이터 크기:

  • 작음 (< 1MB): Stream OK
  • 큼 (> 1MB): Channel 권장
  1. 작업 종류:

    • 순차 읽기/쓰기: Stream
    • Random Access: Channel
    • zero-copy: Channel
    • 메모리 매핑: Channel
  2. 가독성 vs 성능:

    • 가독성: Stream
    • 성능: Channel
  3. 결합:

    • NIO.2 Files + IO BufferedReader
    • 한 시스템에서 함께
  4. 라이브러리:

    • Spring: 내부 NIO
    • Netty: NIO + 풀링
    • Files.lines: Stream + IO 결합

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Stream vs Channel?1바이트/단방향 vs 블록/양방향
Channel 이 양방향인 이점?한 객체로 read+write
Buffer 의 4가지 속성?capacity, position, limit, mark
불변식?0 ≤ mark ≤ position ≤ limit ≤ capacity
flip 의 동작?limit=position, position=0
clear의 동작?position=0, limit=capacity
compact 의 동작?안 읽은 데이터 앞으로
Heap vs Direct?heap 안 vs heap 밖, 복사 횟수
Direct Buffer 단점?할당 비용, GC 어려움
zero-copy?커널 ↔ 유저 복사 회피
transferTo?FileChannel 의 zero-copy 메서드
MappedByteBuffer?파일을 메모리처럼
Stream 이 항상 필요한 곳?직렬화, Reader/Writer 텍스트

9.2 자기 점검 체크리스트

Stream

  • InputStream/OutputStream 정의
  • read() 의 반환 int 이유
  • Decorator 패턴
  • Buffered 의 효과

Channel

  • Channel 의 정의
  • FileChannel/SocketChannel
  • 양방향성
  • position()

Buffer

  • 4가지 속성 정확히
  • 불변식
  • flip 의 정확한 동작
  • clear vs rewind vs compact

Direct Buffer

  • Heap vs Direct 차이
  • 복사 횟수
  • 단점 (할당 비용 등)
  • 풀링 패턴

성능

  • zero-copy 원리
  • transferTo
  • MappedByteBuffer
  • 성능 비교

실무

  • 선택 기준
  • 결합 활용
  • Spring 의 활용
  • 라이브러리

9.3 추가 심화 질문

Q1: 왜 Channel 은 항상 Buffer 통해서만 데이터를 입출력하나?

답:

  • 설계 결정:

    • 블록 단위 처리를 강제
    • 효율성 (1바이트씩이 아님)
    • 위치 관리 (position/limit)
  • 장점:

    • 일관된 API
    • 다양한 Buffer 종류 (Heap, Direct)
    • Random Access (position)
  • 비교:

    • Stream 은 직접 byte[] — 단순하지만 위치 관리 없음
    • Channel + Buffer 는 추상화 — 유연

Q2: Buffer 의 mark 가 자주 사용되지 않는 이유?

답:

  • 대부분의 경우 position 직접 관리로 충분
  • 복잡한 파싱에서는 유용 (다음에 분기 시 돌아오기)
  • 실무에서 거의 사용 X
  • Stream API 가 더 선호됨

Q3: Direct Buffer 가 GC 에 잡히지 않는다?

답:

  • Direct Buffer 의 메모리는 native (off-heap)
  • 하지만 ByteBuffer 객체 자체는 JVM heap
  • ByteBuffer 객체가 GC 되면 Cleaner 가 native 메모리 정리
  • 문제: GC 가 늦으면 native 메모리 부족
  • 해결: 명시적 cleanup 또는 Buffer Pool
// 명시적 cleanup (비표준)
if (buffer instanceof DirectBuffer) {
    ((DirectBuffer) buffer).cleaner().clean();
}

Q4: MappedByteBuffer 의 장단점?

답:

  • 장점:

    • 매우 빠른 랜덤 접근
    • OS 의 페이지 캐시 활용
    • 큰 파일 효율
    • 메모리처럼 사용 가능
  • 단점:

    • 매핑 비용
    • 동기화 복잡 (force())
    • 32비트 JVM 에서 2GB 제한
    • 매핑 해제가 까다로움

Q5: Java NIO 와 Netty 의 관계?

답:

  • NIO: 자바 표준 (Java 1.4+)
  • Netty: NIO 위에 구축된 라이브러리
  • Netty 의 장점:
    • NIO 의 복잡성 추상화
    • Direct Buffer 풀링
    • 이벤트 기반 모델
    • 다양한 프로토콜 지원 (HTTP, WebSocket 등)
  • 사용:
    • Spring WebFlux 가 Netty 기반
    • gRPC 도 Netty 활용

🎯 핵심 요약 — 3줄 정리

1. Stream vs Channel

  • Stream: 1바이트, 단방향, Blocking, 직렬화 강함
  • Channel: 블록 (Buffer), 양방향, Non-blocking 가능, 대규모 강함
  • 둘 다 살아있고 결합 가능

2. Buffer 정밀

  • 4속성: capacity, position, limit, mark
  • 불변식: 0 ≤ mark ≤ position ≤ limit ≤ capacity
  • 모드 전환: flip (쓰기→읽기), clear, rewind, compact

3. 고성능 패턴

  • Heap vs Direct Buffer (복사 횟수)
  • zero-copy: FileChannel.transferTo
  • MappedByteBuffer: 큰 파일 랜덤 접근
  • Buffer Pool: 재사용

📚 다음으로...

Unit 7.4 — Blocking vs Non-blocking (★ 마스터 깊이)

이번 Unit에서 Stream/Channel 의 정밀을 봤다면, 다음은 Blocking vs Non-blocking 의 마스터.

  • Blocking 의 정확한 동작
  • Non-blocking 의 메커니즘
  • Selector 의 멀티플렉싱
  • 1만 동시 연결 시나리오
  • Async + CompletableFuture
  • Project Loom (가상 스레드)
  • Reactive Programming 기초

Phase 7 진행 상황

🚀 Phase 7 — I/O 시스템 큰 그림
  ✅ Unit 7.1 I/O 란 무엇인가
  ✅ Unit 7.2 IO vs NIO (역사적 진화)
  ✅ Unit 7.3 Stream vs Channel ← 여기
  ⏭ Unit 7.4 Blocking vs Non-blocking (★ 마스터 깊이)
  ⏭ Unit 7.5 오버헤드와 File 객체

3주차 누적 진행

✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
✅ Phase 5 — 제네릭과 와일드카드 (5.1 ~ 5.5 완주)
✅ Phase 6 — 객체 비교 (6.1 ~ 6.4 완주)
🚀 Phase 7 — I/O 시스템 큰 그림 (3/5 진행)

총: 29/43 Unit 작성 (약 67%)
profile
Software Developer

0개의 댓글