F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림
🚀 Phase 7 시작 — I/O 정복
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
java.io, java.nio, java.nio.file) 는?I/O 는 "JVM 의 시각에서 외부와의 데이터 흐름" 을 의미한다 — 들어오면 Input, 나가면 Output.
헷갈리는 이유는 사람의 시각 (콘솔 출력은 내가 "보는" 것이라 Input 으로 착각) 과 JVM 의 시각이 반대일 수 있어서.
자바는 I/O 를 3단계로 진화시켰다 — Java 1.0 의java.io(스트림), Java 1.4 의java.nio(채널 + 버퍼), Java 7 의java.nio.file(NIO.2, Path).
현대 애플리케이션의 병목은 대부분 I/O bound 이며, 이를 해결하기 위해 Non-blocking + Async 같은 고급 I/O 모델이 등장.
JVM = 우체국
사람의 관점:
"내가 편지 받음 (받은 편지함을 본다)"
우체국 관점:
"우체국에서 손님 (=사람) 에게 편지 전달 = Output"
"우체국으로 편지 도착 = Input"
콘솔 출력:
사람 관점: "내가 글자를 본다"
JVM 관점: "JVM 이 콘솔로 데이터를 내보냄 = Output"
DB 조회:
사람 관점: "DB 에 명령 보냄"
JVM 관점: "DB 에서 결과 데이터 받음 = Input"
→ I/O 는 항상 JVM 의 관점.
1. I/O 의 정의 — Input, Output
2. JVM 기준의 중요성
3. I/O 의 종류 (콘솔, 파일, 네트워크, 메모리)
4. 자바 I/O 의 진화 (1.0 → 1.4 → 7)
5. I/O 성능 — I/O bound vs CPU bound
6. I/O 모델 분류 (Blocking, Non-blocking, Sync, Async)
7. 자바 I/O 의 패키지 구조
8. ILIC 시스템의 I/O 흐름
9. 면접 + 자기 점검
I/O (Input/Output):
프로그램과 외부 세계 사이의 데이터 흐름.
- Input (입력): 외부 → 프로그램
- Output (출력): 프로그램 → 외부
핵심:
"외부" 의 범위가 곧 I/O 의 범위.
프로그램의 "외부" 란?
프로그램이 직접 제어 못 하는 모든 것:
- 파일 (디스크)
- 네트워크 (다른 컴퓨터)
- 콘솔 (사용자)
- 데이터베이스
- 키보드, 마우스
- 메모리도 일부 (다른 프로세스의 메모리 등)
내부 vs 외부:
- 변수 = 내부 (메모리, JVM 안)
- 객체 = 내부
- 함수 호출 = 내부
- 파일 읽기 = 외부 I/O
- DB 조회 = 외부 I/O
// 1. 파일 읽기 — Input
String content = Files.readString(Path.of("data.txt"));
// 디스크의 파일 → JVM 의 String 변수
// 외부 → JVM = Input
// 2. 키보드 입력 — Input
Scanner scanner = new Scanner(System.in);
String name = scanner.nextLine();
// 키보드 → JVM
// Input
// 3. DB 조회 — Input
ResultSet rs = ps.executeQuery();
String value = rs.getString("col");
// DB → JVM
// Input
// 4. HTTP 응답 받기 — Input
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
String body = response.body();
// 외부 서버 → JVM
// Input
// 5. 표준 입력 — Input
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// System.in 자체가 InputStream
// 1. 파일 쓰기 — Output
Files.writeString(Path.of("output.txt"), "Hello");
// JVM 의 데이터 → 디스크의 파일
// JVM → 외부 = Output
// 2. 콘솔 출력 — Output
System.out.println("Hello");
// JVM → 콘솔 (사용자가 보는 화면)
// Output! (헷갈리지만)
// 3. DB 저장 — Output
ps.setString(1, "value");
ps.executeUpdate();
// JVM → DB
// Output
// 4. HTTP 요청 보내기 — Output
HttpRequest request = HttpRequest.newBuilder()
.POST(BodyPublishers.ofString("data"))
.build();
client.send(request, ...);
// JVM → 외부 서버
// Output
// 5. 로그 기록 — Output
logger.info("processed");
// JVM → 로그 파일/콘솔
// Output
// 파일 복사 — Input 과 Output 모두
public void copyFile(Path src, Path dest) throws IOException {
try (InputStream in = Files.newInputStream(src);
OutputStream out = Files.newOutputStream(dest)) {
in.transferTo(out);
// in: 디스크 → JVM = Input
// out: JVM → 디스크 = Output
}
}
// DB → 파일 export
public void export(Path dest) throws Exception {
try (
Connection conn = ds.getConnection(); // Input 채널 (조회 시)
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery(); // Input
BufferedWriter writer = Files.newBufferedWriter(dest) // Output 채널
) {
while (rs.next()) {
writer.write(rs.getString(1)); // Output
writer.newLine();
}
}
}
Input 과 Output 의 정의와 헷갈리는 케이스는?
답:
헷갈리는 케이스:
판단 기준:
이유:
1. 자바 프로그램의 시각:
- 코드는 JVM 안에서 동작
- 외부와의 모든 데이터 흐름이 I/O
2. 클래스명도 JVM 기준:
- InputStream: JVM 이 데이터를 받는 스트림
- OutputStream: JVM 이 데이터를 보내는 스트림
- System.in: JVM 의 입력 (사람이 입력하는 게 아님)
- System.out: JVM 의 출력 (사람이 보는 게 출력)
3. 일관된 사고:
- "외부 → 안" vs "안 → 외부"
- 사람의 시각으로는 매 시나리오마다 헷갈림
// java.io 패키지
InputStream // JVM 이 받음
OutputStream // JVM 이 보냄
Reader // 문자 받음
Writer // 문자 보냄
FileInputStream // 파일 → JVM
FileOutputStream // JVM → 파일
System.in // InputStream (JVM 으로 입력)
System.out // PrintStream (JVM 의 출력)
System.err // PrintStream (JVM 의 에러 출력)
// 모두 JVM 기준으로 명명됨
시나리오 1: 콘솔 출력
System.out.println("Hello")
사람의 시각: "내가 글자를 본다 → Input?"
JVM 의 시각: "JVM 이 콘솔로 데이터를 내보낸다 → Output"
클래스명 확인: System.out 은 PrintStream
→ Print + Stream = 출력
→ JVM 기준이 정답
시나리오 2: 키보드 입력
Scanner sc = new Scanner(System.in);
사람의 시각: "내가 입력한다 → Output?"
JVM 의 시각: "JVM 이 키보드로부터 받는다 → Input"
클래스명: System.in 은 InputStream
→ JVM 기준이 정답
시나리오 3: DB 조회 (SELECT)
ResultSet rs = ps.executeQuery();
사람: "내가 명령 보내니까 Output?"
JVM: "DB 에서 결과 데이터를 받는다 → Input"
→ Input (데이터 흐름 기준)
시나리오 4: DB 저장 (INSERT)
ps.setString(1, value);
ps.executeUpdate();
JVM: "JVM 의 데이터를 DB 로 보낸다 → Output"
→ Output
// 소켓 — 양방향
Socket socket = new Socket(host, port);
// 받기 위한 스트림 — Input
InputStream in = socket.getInputStream();
in.read(buffer); // 서버 → JVM (Input)
// 보내기 위한 스트림 — Output
OutputStream out = socket.getOutputStream();
out.write(data); // JVM → 서버 (Output)
// 같은 소켓 = 양방향
// 단, 스트림 자체는 단방향
// Channel 은 양방향 (NIO)
원칙: "JVM 기준의 데이터 흐름"
자바 표준 클래스명:
XxxInputStream → JVM 이 데이터 받음
XxxOutputStream → JVM 이 데이터 보냄
XxxReader → JVM 이 문자 받음 (텍스트)
XxxWriter → JVM 이 문자 보냄
모든 I/O 클래스가 일관:
- File, ByteArray, Pipe, Object, ...
- 모두 In = 받음, Out = 보냄
"JVM 기준" 이 중요한 이유는?
답:
1. 클래스명 일관성:
헷갈리는 케이스 해결:
사고의 일관성:
I/O 의 종류 (대상 기준):
1. 콘솔 I/O — 사용자와의 상호작용
2. 파일 I/O — 디스크
3. 네트워크 I/O — 다른 컴퓨터
4. 메모리 I/O — 메모리 (특수)
각각 다른 특성:
- 속도
- 안정성
- 사용 시나리오
// 표준 입력 (System.in)
Scanner scanner = new Scanner(System.in);
String input = scanner.nextLine();
// 표준 출력 (System.out)
System.out.println("Hello");
// 표준 에러 (System.err)
System.err.println("Error!");
// 특성:
// - 사용자와의 직접 소통
// - 일반적으로 작은 데이터
// - 동기적
// - CLI 프로그램, 디버깅
// Java 7+ NIO.2 — Files
String content = Files.readString(Path.of("data.txt"));
Files.writeString(Path.of("output.txt"), "Hello");
// 큰 파일 — Stream
try (Stream<String> lines = Files.lines(Path.of("big.csv"))) {
lines.forEach(System.out::println);
}
// Channel (NIO)
try (FileChannel channel = FileChannel.open(path)) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
}
// 특성:
// - 영구 저장
// - 디스크 속도 (메모리보다 느림)
// - 큰 데이터 가능
// - 데이터베이스, 로그, 설정
// HTTP 클라이언트 (Java 11+)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create("https://api.example.com/data"))
.GET()
.build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
// 소켓
try (Socket socket = new Socket("example.com", 80)) {
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
// ...
}
// 특성:
// - 다른 컴퓨터와 통신
// - 매우 가변적 속도 (네트워크 상태)
// - 신뢰성 이슈 (timeout, disconnection)
// - HTTP API, 메시지 큐, 마이크로서비스
// ByteArrayInputStream / ByteArrayOutputStream
ByteArrayInputStream bis = new ByteArrayInputStream("hello".getBytes());
int b = bis.read(); // 메모리의 배열에서 읽음 — 일종의 Input
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bos.write("hello".getBytes());
byte[] result = bos.toByteArray();
// 특성:
// - 메모리 안에서 동작
// - 매우 빠름
// - 테스트, 임시 버퍼
// - 직렬화 결과 보관
// - I/O 인터페이스를 메모리에 적용 (편의)
대략적 속도 (1KB 처리):
콘솔: 수십 마이크로초 (μs)
메모리: 수 나노초 (ns)
파일 (SSD): 수십~수백 마이크로초 (μs)
파일 (HDD): 밀리초 (ms)
네트워크: 밀리초 ~ 초 (ms~s)
빠른 순:
메모리 >> 파일 > 콘솔 >> 네트워크
I/O 의 병목:
- 네트워크가 가장 큰 병목
- 그래서 Non-blocking, Async 가 발달
// 1. 네트워크 I/O — HTTP API
@RestController
public class ShipmentController {
@PostMapping("/api/shipments")
public ShipmentResponse create(@RequestBody ShipmentRequest req) {
// 클라이언트 → 서버 (Input)
Shipment created = service.create(req.toEntity());
return ShipmentResponse.from(created);
// 서버 → 클라이언트 (Output)
}
}
// 2. DB I/O — JDBC
@Service
public class ShipmentService {
public Shipment findById(Long id) {
return repository.findById(id).orElseThrow();
// DB → JVM (Input)
}
public void save(Shipment s) {
repository.save(s);
// JVM → DB (Output)
}
}
// 3. 파일 I/O — Export
public void exportToFile(Path path) throws IOException {
List<Shipment> all = service.findAll();
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
for (Shipment s : all) {
writer.write(s.toCsvLine());
writer.newLine();
}
// JVM → 파일 (Output)
}
}
// 4. 콘솔 I/O — 로깅
private static final Logger log = LoggerFactory.getLogger(ShipmentService.class);
public void process() {
log.info("processing started");
// JVM → 콘솔/파일 (Output)
}
I/O 의 4가지 종류와 각 특성은?
답:
1. 콘솔 I/O:
파일 I/O:
네트워크 I/O:
메모리 I/O (특수):
속도 순: 메모리 >> 파일 > 콘솔 >> 네트워크
자바 I/O 진화:
1. Java 1.0 (1996) — java.io
- 전통 I/O (스트림 기반)
- 1바이트씩 처리
- Blocking only
2. Java 1.4 (2002) — java.nio
- NIO (New I/O)
- 채널 + 버퍼
- Non-blocking 가능
- 네트워크 I/O 강화
3. Java 7 (2011) — java.nio.file (NIO.2)
- 현대적 파일 API
- Path, Files, WatchService
- 비동기 I/O
- File 시스템 추상화
// 전통 스트림 기반
import java.io.*;
// 파일 읽기
try (FileInputStream fis = new FileInputStream("file.txt");
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// 파일 쓰기
try (FileOutputStream fos = new FileOutputStream("out.txt");
OutputStreamWriter osw = new OutputStreamWriter(fos);
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write("Hello");
}
// 특성:
// - 스트림 기반 (단방향)
// - 바이트 (InputStream/OutputStream)
// - 문자 (Reader/Writer)
// - Decorator 패턴 (Buffered, Data, Object 등)
// - 모두 Blocking
// NIO — 채널 + 버퍼
import java.nio.*;
import java.nio.channels.*;
// FileChannel 사용
try (FileChannel channel = FileChannel.open(Path.of("file.txt"))) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
buffer.flip(); // 읽기 모드로 전환
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
}
// Non-blocking 소켓
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(8080));
Selector selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
selector.select(); // 준비된 채널 대기
Set<SelectionKey> keys = selector.selectedKeys();
// ...
}
// 특성:
// - 채널 (양방향)
// - 버퍼 사용 (직접 메모리 가능)
// - Non-blocking 가능
// - Selector 로 멀티플렉싱
// - 네트워크 I/O 강화
// NIO.2 — 현대 파일 API
import java.nio.file.*;
// 파일 읽기 — 매우 간결
String content = Files.readString(Path.of("file.txt"));
List<String> lines = Files.readAllLines(Path.of("file.txt"));
// Stream 통합
try (Stream<String> lines = Files.lines(Path.of("big.csv"))) {
lines.filter(l -> !l.isEmpty())
.forEach(System.out::println);
}
// 디렉토리 순회
try (Stream<Path> paths = Files.walk(Path.of("/some/dir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// WatchService — 파일 변경 감지
WatchService watcher = FileSystems.getDefault().newWatchService();
Path.of("/some/dir").register(watcher, StandardWatchEventKinds.ENTRY_CREATE);
// 비동기 I/O
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
Future<Integer> future = channel.read(buffer, 0);
| 항목 | Java 1.0 (java.io) | Java 1.4 (java.nio) | Java 7 (NIO.2) |
|---|---|---|---|
| 단위 | 스트림 (바이트/문자) | 채널 + 버퍼 | 채널 + 버퍼 + Path |
| 방향 | 단방향 | 양방향 | 양방향 |
| Blocking | 항상 | Non-blocking 가능 | Non-blocking + 비동기 |
| 파일 API | File 클래스 | FileChannel | Path + Files (static) |
| 패키지 | java.io | java.nio, java.nio.channels | java.nio.file |
| Stream 통합 | X | X | ✓ |
| 가독성 | 중첩 많음 | 복잡 | 간결 |
실무 가이드:
파일 작업 → NIO.2 (Files, Path)
- Java 7+ 표준
- 간결, Stream 통합
네트워크 (단순) → java.io 또는 라이브러리
- 간단한 소켓
- 또는 HttpClient (Java 11+)
네트워크 (대규모) → java.nio
- Non-blocking, Selector
- 또는 Netty 같은 라이브러리
파일 + 네트워크 결합 → NIO.2 + 라이브러리
- Spring WebFlux, Vert.x 등
- Reactive I/O
레거시 호환 → java.io
- 옛 코드와 호환
- Reader/Writer 패턴
// 1. NIO.2 — 파일 export
public void export(Path dest) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(dest)) {
// NIO.2 의 Files.newBufferedWriter
}
}
// 2. Spring Boot — 내부적으로 NIO 사용
@RestController
public class ApiController {
// Spring 의 HTTP 처리는 NIO 기반 (Netty/Tomcat NIO connector)
}
// 3. JDBC — java.io 와 비슷한 추상화
@Repository
public class ShipmentRepository {
public List<Shipment> findAll() {
// 내부적으로 JDBC 가 소켓 I/O
}
}
// 4. Logback — 파일 로깅
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
// Logback 이 NIO.2 의 FileChannel 활용 가능
자바 I/O 의 3 시대와 각 특징은?
답:
1. Java 1.0 (java.io):
Java 1.4 (java.nio):
Java 7 (NIO.2):
실무: 파일은 NIO.2, 대규모 네트워크는 NIO, 단순한 건 java.io.
프로그램 작업의 두 종류:
1. CPU bound (CPU 집약):
- 계산이 병목
- CPU 가 항상 100% 활용
- 예: 이미지 처리, 암호화, 머신러닝 학습
2. I/O bound (I/O 집약):
- 데이터 전송이 병목
- CPU 가 대기 시간 많음
- 예: 웹 서버, DB 처리, 파일 다운로드
CPU 와 I/O 의 속도 차이:
CPU 명령: 나노초 (ns) — 1 GHz = 1ns per cycle
L1 캐시: ~1ns
L2 캐시: ~10ns
메모리 (RAM): ~100ns
SSD 디스크: ~100μs (100,000ns)
HDD 디스크: ~10ms (10,000,000ns)
네트워크 (LAN): ~0.5ms
네트워크 (WAN): ~100ms
비유 (1초로 환산):
CPU 1 cycle = 1초
L1 캐시 = 1초
메모리 = 100초
SSD = 100,000초 (28시간)
HDD = 10,000,000초 (4달!)
네트워크 WAN = 100,000,000초 (3년!)
→ I/O 가 CPU 보다 수천~수억배 느림.
대부분의 실무 시스템 = I/O bound
예: 웹 서버
- 요청 받음 (네트워크 I/O)
- DB 조회 (DB I/O = 네트워크)
- 응답 (네트워크 I/O)
- CPU 사용 시간 < 5%
- 나머지 95% 가 I/O 대기
문제:
- CPU 가 놀고 있음
- 더 많은 요청 처리 가능
- 하지만 스레드가 I/O 에 묶임
해결책:
- Non-blocking I/O
- Async I/O
- Reactive Programming
// 단순 Blocking 코드
public void handleRequest(Request req) {
String data = db.query(req.getId()); // ★ DB 응답 대기 (10ms)
String enriched = enrichWithRemoteApi(data); // ★ API 응답 대기 (100ms)
response.send(processedData); // ★ 네트워크 전송 (1ms)
}
// 한 요청당 시간:
// - CPU: 1ms (실제 처리)
// - I/O 대기: 110ms
// - 총: 111ms
// 동시 요청 100개 가능?
// - 스레드 100개 필요
// - 메모리 부담 ↑
// - 컨텍스트 스위칭 비용
// Non-blocking 또는 Reactive
public Mono<Response> handleRequest(Request req) {
return db.queryAsync(req.getId())
.flatMap(data -> remoteApi.fetchAsync(data))
.flatMap(enriched -> sendAsync(enriched));
}
// 한 스레드가 수많은 요청 처리
// - 한 요청 대기 중 → 다른 요청 처리
// - 스레드 8개로 1만 요청 처리 가능
// - 메모리 효율 ↑
// - 컨텍스트 스위칭 ↓
일반 웹 서비스의 분포:
전체 응답 시간 (예: 200ms):
- 네트워크 (요청): 10ms
- 컨트롤러 로직: 1ms
- DB 조회: 100ms (5번 호출)
- 캐시 조회: 5ms
- 외부 API: 50ms
- 네트워크 (응답): 10ms
CPU 사용: ~3ms
I/O 대기: ~197ms
I/O bound: 98.5%!
@Service
public class ShipmentService {
// 일반 API 호출 — I/O bound
public ShipmentResponse getDetails(Long id) {
// 1. DB 조회 (I/O)
Shipment shipment = repository.findById(id).orElseThrow();
// ~10ms
// 2. 캐시 조회 (I/O, 비교적 빠름)
ShippingRate rate = cache.get(shipment.getRouteCode());
// ~1ms
// 3. 외부 API (I/O)
TrackingInfo tracking = trackingApi.fetch(shipment.getTrackingNumber());
// ~50ms
// 4. DB 추가 조회 (I/O)
List<Cargo> cargos = cargoRepository.findByShipmentId(id);
// ~20ms
// 5. CPU 작업 (계산)
BigDecimal totalFare = calculateFare(shipment, cargos, rate);
// ~0.1ms
return ShipmentResponse.builder()
.shipment(shipment)
.cargos(cargos)
.tracking(tracking)
.totalFare(totalFare)
.build();
// 총: ~81.1ms (I/O 99.8%)
}
}
시스템이 I/O bound 인지 어떻게 판단하나?
답:
1. CPU 사용률: 낮으면 (< 30%) I/O bound 가능성
2. 응답 시간 분석:
시나리오:
해결책:
I/O 모델의 2가지 축:
축 1: Blocking vs Non-blocking
- 호출이 결과를 기다리는가?
축 2: Sync vs Async
- 작업 완료를 누가 알려주는가?
4가지 조합:
1. Blocking + Sync (전통)
2. Non-blocking + Sync (NIO)
3. Blocking + Async (드물게)
4. Non-blocking + Async (현대)
Blocking:
- read() 호출 → 데이터 준비 될 때까지 스레드 정지
- 함수가 결과와 함께 리턴
- 단순하지만 스레드 낭비
Non-blocking:
- read() 호출 → 즉시 리턴 (데이터 있으면 가져오고 없으면 0/null)
- 결과를 받기 위해 다시 호출 필요
- 스레드 효율적
Sync (동기):
- 작업 완료를 호출자가 직접 확인
- 결과를 기다리거나 폴링
Async (비동기):
- 작업 완료를 시스템이 콜백으로 알림
- 호출자는 다른 일 가능
- 콜백, Future, Promise 등 활용
1. Blocking + Sync (전통 java.io):
String data = file.read(); // 대기 + 직접 결과 받음
2. Non-blocking + Sync (NIO Selector):
int n = channel.read(buffer); // 즉시 리턴
if (n == 0) { ... } // 직접 폴링
3. Blocking + Async (드뭄, 모순적):
// 거의 사용 X
4. Non-blocking + Async (현대):
channel.read(buffer, attachment, completionHandler);
// 즉시 리턴, 완료 시 핸들러 호출
// 1. Blocking + Sync — java.io
try (InputStream in = new FileInputStream("file.txt")) {
int b;
while ((b = in.read()) != -1) { // ★ 블로킹
// 처리
}
}
// 2. Non-blocking + Sync — java.nio (Selector)
Selector selector = Selector.open();
channel.configureBlocking(false);
channel.register(selector, SelectionKey.OP_READ);
while (true) {
selector.select(); // 준비된 채널 대기
Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (key.isReadable()) {
channel.read(buffer); // 즉시
}
}
}
// 3. Non-blocking + Async — AsynchronousFileChannel (Java 7+)
AsynchronousFileChannel channel = AsynchronousFileChannel.open(path);
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer, 0, null, new CompletionHandler<Integer, Void>() {
@Override
public void completed(Integer result, Void attachment) {
// 콜백 — 완료 시 호출
}
@Override
public void failed(Throwable exc, Void attachment) {
// 실패 시
}
});
// 또는 CompletableFuture (현대)
CompletableFuture<HttpResponse<String>> future =
httpClient.sendAsync(request, BodyHandlers.ofString());
future.thenAccept(response -> {
// 완료 시
});
시나리오: 음식 주문 후 받기
1. Blocking + Sync (전통 식당):
- 주문 후 카운터에서 대기
- 음식 준비될 때까지 못 떠남
- 다른 일 못 함
2. Non-blocking + Sync (포장 카운터):
- 주문 후 "10분 후 오라" 안내
- 10분 후 가서 "준비됐어?" 물어봄 (폴링)
- 다른 일 가능
3. Non-blocking + Async (전화 + 콜백):
- 주문 + 전화번호 남김
- 음식 준비되면 식당에서 전화
- 다른 일 + 알림 받음
선택 기준:
Blocking + Sync (java.io):
✓ 단순한 시나리오
✓ 적은 동시 연결
✓ 코드 가독성 ↑
✗ 동시성 ↓
Non-blocking + Sync (java.nio):
✓ 중간 규모 서버
✓ Selector 멀티플렉싱
✓ 직접 제어 필요
✗ 코드 복잡
Non-blocking + Async (CompletableFuture, Reactive):
✓ 대규모 동시 처리
✓ 마이크로서비스
✓ 함수형 스타일
✗ 학습 곡선 가파름
✗ 디버깅 어려움
4가지 I/O 모델의 차이는?
답:
1. Blocking + Sync:
Non-blocking + Sync:
Blocking + Async:
Non-blocking + Async:
Unit 7.4 (마스터 깊이) 에서 Blocking vs Non-blocking 정밀.
자바 I/O 패키지:
1. java.io — Java 1.0+ 전통 I/O
2. java.nio — Java 1.4+ NIO (버퍼, 채널 등)
3. java.nio.file — Java 7+ NIO.2 파일 시스템
4. java.nio.channels — Java 1.4+ 채널
세부:
java.io
└── 파일, 스트림, Reader/Writer
java.nio
└── ByteBuffer, IntBuffer 등 버퍼들
java.nio.channels
└── FileChannel, SocketChannel, Selector
java.nio.file
└── Path, Paths, Files, WatchService
// 바이트 스트림
InputStream / OutputStream
FileInputStream / FileOutputStream
BufferedInputStream / BufferedOutputStream
DataInputStream / DataOutputStream
ObjectInputStream / ObjectOutputStream
ByteArrayInputStream / ByteArrayOutputStream
PipedInputStream / PipedOutputStream
// 문자 스트림
Reader / Writer
FileReader / FileWriter
BufferedReader / BufferedWriter
InputStreamReader / OutputStreamWriter
PrintWriter
// 기타
File
RandomAccessFile
PrintStream (System.out)
StreamTokenizer
// 버퍼
ByteBuffer / CharBuffer
IntBuffer / LongBuffer
FloatBuffer / DoubleBuffer
ShortBuffer
// 문자셋
Charset
CharsetEncoder / CharsetDecoder
// 채널
Channel (인터페이스)
ReadableByteChannel / WritableByteChannel
FileChannel
SocketChannel / ServerSocketChannel
DatagramChannel
// 비동기
AsynchronousChannel
AsynchronousFileChannel
AsynchronousSocketChannel
// 멀티플렉싱
Selector
SelectionKey
// 경로
Path / Paths
// 파일 작업
Files
// 파일 시스템
FileSystem / FileSystems
FileStore
// 감시
WatchService
WatchKey
WatchEvent
// 속성
BasicFileAttributes
PosixFileAttributes
PosixFilePermission
// 디렉토리
DirectoryStream
사용 패턴:
전통 I/O (java.io):
- 단순한 파일 처리
- 콘솔 I/O
- 직렬화
NIO 채널 (java.nio.channels):
- 대규모 네트워크
- 비동기 I/O
- 메모리 매핑
NIO.2 (java.nio.file):
- 현대 파일 API
- Stream 통합
- 파일 시스템 추상화
조합 예:
- NIO.2 의 Files 가 내부적으로 채널 사용
- Files.newBufferedReader 는 NIO 채널 기반
- 한 시스템에서 함께 사용 가능
// 파일 작업 (NIO.2)
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
// 버퍼 (NIO)
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
// 채널 (NIO)
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
// 전통 (Java 1.0 IO)
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedReader;
import java.io.IOException;
// ShipmentExporter — 파일 + 스트림
import java.nio.file.Files;
import java.nio.file.Path;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Stream;
public class ShipmentExporter {
public void export(Path dest, List<Shipment> shipments) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(
dest, StandardCharsets.UTF_8)) {
for (Shipment s : shipments) {
writer.write(s.toCsvLine());
writer.newLine();
}
}
}
public Stream<Shipment> read(Path src) throws IOException {
return Files.lines(src, StandardCharsets.UTF_8)
.map(this::parseShipment);
}
}
자바 I/O 의 4가지 패키지와 용도는?
답:
1. java.io (Java 1.0+):
java.nio (Java 1.4+):
java.nio.channels (Java 1.4+):
java.nio.file (Java 7+):
조합 가능: NIO.2 의 Files 가 내부적으로 채널 활용.
ILIC 시스템의 I/O 구성:
Client (Browser/Mobile)
↕ HTTP (네트워크 I/O)
Spring Boot Application (JVM)
↕ JDBC (DB 네트워크 I/O)
PostgreSQL Database
↕
Local File System (export, logs)
↕ HTTP (외부 API)
External Services (Tracking API, etc.)
// 한 API 요청의 I/O 흐름
// 1. HTTP 요청 받음 (Input)
@PostMapping("/api/shipments")
public ShipmentResponse create(@RequestBody ShipmentRequest req) {
// 네트워크 → JVM = Input
// 2. DB 트랜잭션 시작 (소켓 I/O)
Shipment shipment = shipmentService.create(req.toEntity());
// 3. 외부 API 호출 (네트워크 I/O)
TrackingInfo tracking = trackingApiClient.create(shipment);
// 4. 캐시 저장 (네트워크 I/O — Redis)
cache.put(shipment.getId(), shipment);
// 5. 로그 기록 (파일 I/O)
log.info("Shipment created: {}", shipment.getId());
// 6. HTTP 응답 (Output)
return ShipmentResponse.from(shipment);
// JVM → 네트워크 = Output
}
ILIC 의 일반 API 요청에서:
1. 네트워크 I/O (HTTP, DB, 외부 API):
- 가장 빈번
- 가장 느림 (병목)
- Non-blocking 효과 큼
2. 파일 I/O (로그, export):
- 가끔 (export 시)
- 로그는 항상 (비동기)
3. 콘솔 I/O (개발/디버깅):
- 개발 환경만
- 운영은 파일 로그
// 1. Connection Pooling — DB I/O 최적화
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(20); // 연결 풀
// 매 요청마다 새 연결 X
return new HikariDataSource(config);
}
}
// 2. Cache — DB I/O 회피
@Cacheable("shipments")
public Shipment findById(Long id) {
return repository.findById(id).orElseThrow();
// 캐시 hit 시 DB I/O 회피
}
// 3. Async — Non-blocking
@Async
public CompletableFuture<TrackingInfo> fetchTrackingAsync(Long shipmentId) {
TrackingInfo info = trackingApi.fetch(shipmentId);
return CompletableFuture.completedFuture(info);
}
// 4. Bulk Export — 대량 파일 I/O
public void exportBulk(Path dest) throws IOException {
try (Stream<Shipment> stream = repository.streamAll();
BufferedWriter writer = Files.newBufferedWriter(dest)) {
stream.forEach(s -> {
try {
writer.write(s.toCsvLine());
writer.newLine();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
// 5. 로그 — 비동기 로깅 (Logback AsyncAppender)
// logback.xml 에서 설정
ILIC 시나리오: 동시 1,000 사용자
각 요청의 I/O:
- DB 조회 5번 × 10ms = 50ms
- 외부 API 1번 × 100ms = 100ms
- 캐시 조회 2번 × 1ms = 2ms
- CPU 처리: 5ms
총: ~157ms per request
Blocking 방식:
- 1,000 동시 요청
- 1,000 스레드 필요
- 메모리: 1MB * 1,000 = 1GB (스레드 스택)
- 컨텍스트 스위칭 비용 ↑
Non-blocking + Async 방식:
- 50-100 스레드로 1,000 요청 처리 가능
- 메모리 절약
- 처리량 ↑
// 1. 응답 시간 측정
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long elapsed = System.currentTimeMillis() - start;
log.info("{} took {}ms", joinPoint.getSignature(), elapsed);
}
}
// 2. DB 쿼리 로깅
// application.yml
// spring:
// jpa:
// show-sql: true
// properties:
// hibernate:
// format_sql: true
// generate_statistics: true
// 3. Connection Pool 모니터링
// HikariCP metrics 활용
ILIC 의 운영 포인트:
1. DB 최적화
- 인덱스 활용
- N+1 쿼리 회피
- Connection Pool 튜닝
2. 캐시 활용
- Redis 또는 Caffeine
- 빈번한 조회 캐싱
3. 비동기 처리
- @Async 활용
- 메시지 큐 (Kafka, RabbitMQ)
4. 모니터링
- APM (Application Performance Monitoring)
- 슬로우 쿼리 추적
- 외부 API 응답 시간
5. 회로 차단 (Circuit Breaker)
- 외부 API 실패 시 차단
- Resilience4j 등
ILIC 같은 시스템에서 I/O 의 역할은?
답:
1. 모든 데이터 흐름이 I/O:
I/O bound 가 일반적:
최적화 패턴:
운영 포인트:
| Q | 핵심 답변 |
|---|---|
| I/O 정의? | JVM 기준 외부와의 데이터 흐름 |
| Input vs Output? | 외부 → JVM vs JVM → 외부 |
| 콘솔 출력은? | Output (JVM → 콘솔) |
| DB 조회는? | Input (DB → JVM) |
| 자바 I/O 3 시대? | java.io / java.nio / NIO.2 |
| java.io 특징? | 스트림, 단방향, Blocking |
| java.nio 특징? | 채널 + 버퍼, 양방향, Non-blocking |
| NIO.2 추가? | Path, Files, WatchService |
| I/O bound vs CPU bound? | I/O 대기 vs 계산 |
| 4가지 I/O 모델? | Blocking/Non-blocking × Sync/Async |
| Non-blocking 이점? | 한 스레드가 다수 처리 |
| Selector? | NIO 의 멀티플렉서 |
답:
PrintStream 의 인스턴스OutputStream 의 자식 (Output 임을 명확히)static final PrintStream out 으로 정의답:
답:
// 방법 1: Scanner
Scanner sc = new Scanner(System.in);
String line = sc.nextLine();
// 방법 2: BufferedReader
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line = br.readLine();
// 방법 3: Console (Java 6+)
Console console = System.console();
String line = console.readLine();
char[] password = console.readPassword(); // 비밀번호 입력
답:
java.io 의 Blocking:
Decorator 패턴의 복잡함:
new BufferedReader(new InputStreamReader(new FileInputStream(...)))NIO 가 일부 해결:
답:
// NIO.2 의 Files
BufferedReader reader = Files.newBufferedReader(Path.of("file.txt"));
// reader 는 java.io 의 BufferedReader
String line = reader.readLine(); // java.io 메서드
1. I/O 의 정의
2. 자바 I/O 3 시대
3. I/O 모델
이번 Unit에서 I/O 의 큰 그림을 봤다면, 다음은 IO 와 NIO 의 정밀한 비교.
🚀 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 객체
✅ 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 시스템 큰 그림 (1/5 진행, 7.1 재작성)
총: 27/43 Unit 작성 (약 63%)