3주차 Unit 7.1 — I/O 란 무엇인가

Psj·2026년 5월 19일

F-lab

목록 보기
101/230

Unit 7.1 — I/O 란 무엇인가

F-LAB JAVA · 3주차 · Phase 7 · I/O 시스템 큰 그림
🚀 Phase 7 시작 — I/O 정복


📌 학습 목표

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

  • I/O 의 정의 — Input, Output 의 정확한 의미는?
  • "JVM 기준" 이라는 표현 이 왜 중요한가?
  • 콘솔 출력은 Input 인가 Output 인가? DB 조회는?
  • 자바 I/O 의 4가지 종류 (콘솔, 파일, 네트워크, 메모리) 는?
  • 자바 I/O 의 진화 (Java 1.0 → 1.4 NIO → 7 NIO.2) 는?
  • I/O bound 와 CPU bound 의 차이는?
  • I/O 모델 4가지 (Blocking, Non-blocking, Sync, Async) 의 정확한 분류는?
  • 자바 I/O 의 패키지 구조 (java.io, java.nio, java.nio.file) 는?
  • ILIC 같은 시스템에서 I/O 흐름 은 어떻게 발생하나?

🎯 핵심 한 문장

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 의 관점.


🧭 9개 섹션 로드맵

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. 면접 + 자기 점검

1️⃣ I/O 의 정의 — Input, Output

1.1 I/O 의 기본 정의

I/O (Input/Output):

  프로그램과 외부 세계 사이의 데이터 흐름.

  - Input (입력): 외부 → 프로그램
  - Output (출력): 프로그램 → 외부

핵심:
  "외부" 의 범위가 곧 I/O 의 범위.

1.2 "외부" 의 정의

프로그램의 "외부" 란?

  프로그램이 직접 제어 못 하는 모든 것:
  - 파일 (디스크)
  - 네트워크 (다른 컴퓨터)
  - 콘솔 (사용자)
  - 데이터베이스
  - 키보드, 마우스
  - 메모리도 일부 (다른 프로세스의 메모리 등)

내부 vs 외부:
  - 변수 = 내부 (메모리, JVM 안)
  - 객체 = 내부
  - 함수 호출 = 내부
  - 파일 읽기 = 외부 I/O
  - DB 조회 = 외부 I/O

1.3 Input 의 예

// 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.4 Output 의 예

// 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

1.5 Input + 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();
        }
    }
}

1.6 자기 점검 답변

Input 과 Output 의 정의와 헷갈리는 케이스는?

:

  • Input: 외부 → JVM
  • Output: JVM → 외부

헷갈리는 케이스:

  • 콘솔 출력 (System.out.println) → "사람이 보니까 Input?" → Output (JVM 기준)
  • DB 조회 → "내가 요청하니까 Output?" → Input (데이터가 JVM 로)
  • 표준 입력 (System.in) → Input (키보드 → JVM)

판단 기준:

  • 데이터 흐름의 방향
  • "JVM 입장에서 데이터가 들어오는가 나가는가"

2️⃣ JVM 기준의 중요성

2.1 왜 JVM 기준인가?

이유:

1. 자바 프로그램의 시각:
   - 코드는 JVM 안에서 동작
   - 외부와의 모든 데이터 흐름이 I/O

2. 클래스명도 JVM 기준:
   - InputStream: JVM 이 데이터를 받는 스트림
   - OutputStream: JVM 이 데이터를 보내는 스트림
   - System.in: JVM 의 입력 (사람이 입력하는 게 아님)
   - System.out: JVM 의 출력 (사람이 보는 게 출력)

3. 일관된 사고:
   - "외부 → 안" vs "안 → 외부"
   - 사람의 시각으로는 매 시나리오마다 헷갈림

2.2 클래스명의 일관성

// 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 기준으로 명명됨

2.3 헷갈리는 시나리오 분석

시나리오 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

2.4 양방향 채널의 분석

// 소켓 — 양방향
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)

2.5 자바의 일관된 명명

원칙: "JVM 기준의 데이터 흐름"

자바 표준 클래스명:
  XxxInputStream  → JVM 이 데이터 받음
  XxxOutputStream → JVM 이 데이터 보냄
  
  XxxReader  → JVM 이 문자 받음 (텍스트)
  XxxWriter  → JVM 이 문자 보냄

모든 I/O 클래스가 일관:
  - File, ByteArray, Pipe, Object, ...
  - 모두 In = 받음, Out = 보냄

2.6 자기 점검 답변

"JVM 기준" 이 중요한 이유는?

:
1. 클래스명 일관성:

  • InputStream = JVM 이 받음
  • OutputStream = JVM 이 보냄
  • System.out = 출력 (JVM 의 출력)
  1. 헷갈리는 케이스 해결:

    • 콘솔 출력 → Output (사람 시각이 아니라 JVM 시각)
    • 키보드 입력 → Input
    • DB 조회 → Input
    • DB 저장 → Output
  2. 사고의 일관성:

    • 매번 "사람 vs JVM" 비교 불필요
    • "데이터가 JVM 으로 들어오는가 나가는가" 만 보면 됨

3️⃣ I/O 의 종류 (콘솔, 파일, 네트워크, 메모리)

3.1 I/O 의 4가지 분류

I/O 의 종류 (대상 기준):

1. 콘솔 I/O — 사용자와의 상호작용
2. 파일 I/O — 디스크
3. 네트워크 I/O — 다른 컴퓨터
4. 메모리 I/O — 메모리 (특수)

각각 다른 특성:
  - 속도
  - 안정성
  - 사용 시나리오

3.2 콘솔 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 프로그램, 디버깅

3.3 파일 I/O

// 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);
}

// 특성:
// - 영구 저장
// - 디스크 속도 (메모리보다 느림)
// - 큰 데이터 가능
// - 데이터베이스, 로그, 설정

3.4 네트워크 I/O

// 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, 메시지 큐, 마이크로서비스

3.5 메모리 I/O (특수)

// 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 인터페이스를 메모리에 적용 (편의)

3.6 종류별 속도 비교

대략적 속도 (1KB 처리):

콘솔:       수십 마이크로초 (μs)
메모리:     수 나노초 (ns)
파일 (SSD): 수십~수백 마이크로초 (μs)
파일 (HDD): 밀리초 (ms)
네트워크:   밀리초 ~ 초 (ms~s)

빠른 순:
  메모리 >> 파일 > 콘솔 >> 네트워크

I/O 의 병목:
  - 네트워크가 가장 큰 병목
  - 그래서 Non-blocking, Async 가 발달

3.7 ILIC 시스템의 I/O 활용

// 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)
}

3.8 자기 점검 답변

I/O 의 4가지 종류와 각 특성은?

:
1. 콘솔 I/O:

  • 사용자와 직접 (System.in, System.out)
  • 작은 데이터, 동기적
  1. 파일 I/O:

    • 디스크
    • 영구 저장, 큰 데이터 가능
    • HDD/SSD 속도
  2. 네트워크 I/O:

    • 다른 컴퓨터
    • 가변 속도, 신뢰성 이슈
    • HTTP, TCP/UDP
  3. 메모리 I/O (특수):

    • 메모리 안의 배열
    • 매우 빠름, 테스트용
    • ByteArrayInputStream 등

속도 순: 메모리 >> 파일 > 콘솔 >> 네트워크


4️⃣ 자바 I/O 의 진화 (1.0 → 1.4 → 7)

4.1 자바 I/O 의 3 시대

자바 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 시스템 추상화

4.2 Java 1.0 — java.io

// 전통 스트림 기반
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

4.3 Java 1.4 — java.nio

// 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 강화

4.4 Java 7 — java.nio.file (NIO.2)

// 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);

4.5 3 시대 비교

항목Java 1.0 (java.io)Java 1.4 (java.nio)Java 7 (NIO.2)
단위스트림 (바이트/문자)채널 + 버퍼채널 + 버퍼 + Path
방향단방향양방향양방향
Blocking항상Non-blocking 가능Non-blocking + 비동기
파일 APIFile 클래스FileChannelPath + Files (static)
패키지java.iojava.nio, java.nio.channelsjava.nio.file
Stream 통합XX
가독성중첩 많음복잡간결

4.6 어떤 것을 언제 쓰나?

실무 가이드:

파일 작업 → 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 패턴

4.7 ILIC 의 I/O 활용

// 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 활용 가능

4.8 자기 점검 답변

자바 I/O 의 3 시대와 각 특징은?

:
1. Java 1.0 (java.io):

  • 스트림 기반
  • 단방향
  • Blocking only
  • Decorator 패턴
  1. Java 1.4 (java.nio):

    • 채널 + 버퍼
    • 양방향
    • Non-blocking 가능 (Selector)
    • 네트워크 I/O 강화
  2. Java 7 (NIO.2):

    • Path, Files API
    • Stream 통합
    • 비동기 I/O
    • WatchService
    • 가장 간결한 파일 작업

실무: 파일은 NIO.2, 대규모 네트워크는 NIO, 단순한 건 java.io.


5️⃣ I/O 성능 — I/O bound vs CPU bound

5.1 작업의 분류

프로그램 작업의 두 종류:

1. CPU bound (CPU 집약):
   - 계산이 병목
   - CPU 가 항상 100% 활용
   - 예: 이미지 처리, 암호화, 머신러닝 학습

2. I/O bound (I/O 집약):
   - 데이터 전송이 병목
   - CPU 가 대기 시간 많음
   - 예: 웹 서버, DB 처리, 파일 다운로드

5.2 I/O 의 속도 한계

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 보다 수천~수억배 느림.

5.3 I/O bound 시스템의 특성

대부분의 실무 시스템 = 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

5.4 Blocking I/O 의 문제

// 단순 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개 필요
// - 메모리 부담 ↑
// - 컨텍스트 스위칭 비용

5.5 Non-blocking I/O 의 효과

// 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만 요청 처리 가능
// - 메모리 효율 ↑
// - 컨텍스트 스위칭 ↓

5.6 실제 시스템의 분석

일반 웹 서비스의 분포:

전체 응답 시간 (예: 200ms):
  - 네트워크 (요청): 10ms
  - 컨트롤러 로직: 1ms
  - DB 조회: 100ms (5번 호출)
  - 캐시 조회: 5ms
  - 외부 API: 50ms
  - 네트워크 (응답): 10ms
  
  CPU 사용: ~3ms
  I/O 대기: ~197ms
  
  I/O bound: 98.5%!

5.7 ILIC 시스템의 I/O bound 분석

@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%)
    }
}

5.8 자기 점검 답변

시스템이 I/O bound 인지 어떻게 판단하나?

:
1. CPU 사용률: 낮으면 (< 30%) I/O bound 가능성
2. 응답 시간 분석:

  • 대기 시간 vs 실제 처리 시간
  • 대부분이 대기 → I/O bound
  1. 시나리오:

    • 웹 서버, API: 대부분 I/O bound
    • DB 처리: I/O bound
    • 이미지/영상 처리: CPU bound
    • 머신러닝 학습: CPU bound
  2. 해결책:

    • I/O bound → Non-blocking, Async
    • CPU bound → 멀티스레드, 병렬화

6️⃣ I/O 모델 분류 (Blocking, Non-blocking, Sync, Async)

6.1 4가지 I/O 모델

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 (현대)

6.2 Blocking vs Non-blocking

Blocking:
  - read() 호출 → 데이터 준비 될 때까지 스레드 정지
  - 함수가 결과와 함께 리턴
  - 단순하지만 스레드 낭비

Non-blocking:
  - read() 호출 → 즉시 리턴 (데이터 있으면 가져오고 없으면 0/null)
  - 결과를 받기 위해 다시 호출 필요
  - 스레드 효율적

6.3 Sync vs Async

Sync (동기):
  - 작업 완료를 호출자가 직접 확인
  - 결과를 기다리거나 폴링

Async (비동기):
  - 작업 완료를 시스템이 콜백으로 알림
  - 호출자는 다른 일 가능
  - 콜백, Future, Promise 등 활용

6.4 4가지 조합

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);
   // 즉시 리턴, 완료 시 핸들러 호출

6.5 자바의 모델별 API

// 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 -> {
    // 완료 시
});

6.6 비유 — 음식점

시나리오: 음식 주문 후 받기

1. Blocking + Sync (전통 식당):
   - 주문 후 카운터에서 대기
   - 음식 준비될 때까지 못 떠남
   - 다른 일 못 함

2. Non-blocking + Sync (포장 카운터):
   - 주문 후 "10분 후 오라" 안내
   - 10분 후 가서 "준비됐어?" 물어봄 (폴링)
   - 다른 일 가능

3. Non-blocking + Async (전화 + 콜백):
   - 주문 + 전화번호 남김
   - 음식 준비되면 식당에서 전화
   - 다른 일 + 알림 받음

6.7 모델 선택 가이드

선택 기준:

Blocking + Sync (java.io):
  ✓ 단순한 시나리오
  ✓ 적은 동시 연결
  ✓ 코드 가독성 ↑
  ✗ 동시성 ↓

Non-blocking + Sync (java.nio):
  ✓ 중간 규모 서버
  ✓ Selector 멀티플렉싱
  ✓ 직접 제어 필요
  ✗ 코드 복잡

Non-blocking + Async (CompletableFuture, Reactive):
  ✓ 대규모 동시 처리
  ✓ 마이크로서비스
  ✓ 함수형 스타일
  ✗ 학습 곡선 가파름
  ✗ 디버깅 어려움

6.8 자기 점검 답변

4가지 I/O 모델의 차이는?

:
1. Blocking + Sync:

  • read() 가 데이터 올 때까지 정지
  • 자바 표준 java.io
  • 단순하지만 스레드 낭비
  1. Non-blocking + Sync:

    • read() 즉시 리턴
    • Selector 로 폴링
    • 자바 java.nio
  2. Blocking + Async:

    • 드물게 사용
    • 모순적 조합
  3. Non-blocking + Async:

    • 콜백 또는 Future
    • 자바 AsynchronousFileChannel
    • 현대 표준 (Reactive)

Unit 7.4 (마스터 깊이) 에서 Blocking vs Non-blocking 정밀.


7️⃣ 자바 I/O 의 패키지 구조

7.1 자바 I/O 의 4가지 패키지

자바 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

7.2 java.io 의 주요 클래스

// 바이트 스트림
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

7.3 java.nio 의 주요 클래스

// 버퍼
ByteBuffer / CharBuffer
IntBuffer / LongBuffer
FloatBuffer / DoubleBuffer
ShortBuffer

// 문자셋
Charset
CharsetEncoder / CharsetDecoder

7.4 java.nio.channels

// 채널
Channel (인터페이스)
ReadableByteChannel / WritableByteChannel
FileChannel
SocketChannel / ServerSocketChannel
DatagramChannel

// 비동기
AsynchronousChannel
AsynchronousFileChannel
AsynchronousSocketChannel

// 멀티플렉싱
Selector
SelectionKey

7.5 java.nio.file (NIO.2)

// 경로
Path / Paths

// 파일 작업
Files

// 파일 시스템
FileSystem / FileSystems
FileStore

// 감시
WatchService
WatchKey
WatchEvent

// 속성
BasicFileAttributes
PosixFileAttributes
PosixFilePermission

// 디렉토리
DirectoryStream

7.6 패키지 관계

사용 패턴:

전통 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 채널 기반
  - 한 시스템에서 함께 사용 가능

7.7 import 패턴

// 파일 작업 (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;

7.8 ILIC 의 import 패턴

// 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);
    }
}

7.9 자기 점검 답변

자바 I/O 의 4가지 패키지와 용도는?

:
1. java.io (Java 1.0+):

  • 스트림 (InputStream, OutputStream)
  • Reader/Writer
  • 직렬화 (ObjectInputStream)
  1. java.nio (Java 1.4+):

    • 버퍼 (ByteBuffer, CharBuffer)
    • 문자셋 (Charset)
  2. java.nio.channels (Java 1.4+):

    • 채널 (FileChannel, SocketChannel)
    • Selector (멀티플렉싱)
    • 비동기 채널 (Java 7+)
  3. java.nio.file (Java 7+):

    • Path, Files
    • WatchService
    • 파일 시스템 추상화

조합 가능: NIO.2 의 Files 가 내부적으로 채널 활용.


8️⃣ ILIC 시스템의 I/O 흐름

8.1 ILIC 의 I/O 구조 종합

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.)

8.2 단일 요청의 I/O 흐름

// 한 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
}

8.3 I/O 타입별 빈도

ILIC 의 일반 API 요청에서:

1. 네트워크 I/O (HTTP, DB, 외부 API):
   - 가장 빈번
   - 가장 느림 (병목)
   - Non-blocking 효과 큼

2. 파일 I/O (로그, export):
   - 가끔 (export 시)
   - 로그는 항상 (비동기)

3. 콘솔 I/O (개발/디버깅):
   - 개발 환경만
   - 운영은 파일 로그

8.4 ILIC 의 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 에서 설정

8.5 동시 사용자의 I/O 영향

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 요청 처리 가능
  - 메모리 절약
  - 처리량 ↑

8.6 ILIC 의 I/O 모니터링

// 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 활용

8.7 I/O bound 시스템의 운영

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 등

8.8 자기 점검 답변

ILIC 같은 시스템에서 I/O 의 역할은?

:
1. 모든 데이터 흐름이 I/O:

  • HTTP 요청/응답
  • DB 조회/저장
  • 외부 API 호출
  • 로그 기록
  • 파일 export
  1. I/O bound 가 일반적:

    • CPU 사용 < 5%
    • I/O 대기 > 95%
  2. 최적화 패턴:

    • Connection Pool
    • 캐싱
    • 비동기 처리
    • Bulk 처리
  3. 운영 포인트:

    • 모니터링 (응답 시간, 쿼리)
    • 슬로우 쿼리 추적
    • Circuit Breaker

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

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 의 멀티플렉서

9.2 자기 점검 체크리스트

기본 이해

  • I/O 의 정의
  • JVM 기준의 중요성
  • Input/Output 헷갈리는 케이스
  • I/O 의 4가지 종류
  • 종류별 속도 차이

진화

  • Java 1.0 java.io
  • Java 1.4 java.nio
  • Java 7 NIO.2
  • 각 시대의 한계와 개선

성능

  • I/O bound 정의
  • CPU bound 와 차이
  • CPU vs I/O 속도 차이
  • Blocking 의 문제

모델

  • Blocking vs Non-blocking
  • Sync vs Async
  • 4가지 조합
  • 자바의 모델별 API

패키지

  • java.io
  • java.nio
  • java.nio.channels
  • java.nio.file

실무

  • ILIC 의 I/O 흐름
  • Connection Pooling
  • 캐싱
  • 비동기 처리

9.3 추가 심화 질문

Q1: System.out 의 정확한 타입은?

답:

  • PrintStream 의 인스턴스
  • OutputStream 의 자식 (Output 임을 명확히)
  • static final PrintStream out 으로 정의
  • 콘솔로 출력하는 스트림

Q2: 콘솔 출력이 왜 Output 인가? 사람이 보는데?

답:

  • JVM 기준:
    • JVM 의 데이터가 외부 (콘솔) 로 나감
    • "사람 시각" 이 아님
  • 클래스명 일관성:
    • PrintStream (Output 의 자식)
    • System.out
  • 모든 자바 I/O 가 JVM 기준

Q3: 자바에서 표준 입력은 어떻게 받나?

답:

// 방법 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();   // 비밀번호 입력

Q4: Java I/O 의 가장 큰 단점은?

답:

  • java.io 의 Blocking:

    • 동시 처리 ↓
    • 스레드 낭비
  • Decorator 패턴의 복잡함:

    • 중첩이 많음
    • 예: new BufferedReader(new InputStreamReader(new FileInputStream(...)))
  • NIO 가 일부 해결:

    • Non-blocking
    • NIO.2 의 간결한 Files API

Q5: 한 시스템에서 java.io 와 java.nio 를 함께 쓸 수 있나?

답:

  • 가능
  • 실제로 자주 함께 사용
  • 예: Files.newBufferedReader 는 NIO.2 의 메서드지만 반환은 BufferedReader (java.io)
// NIO.2 의 Files
BufferedReader reader = Files.newBufferedReader(Path.of("file.txt"));
// reader 는 java.io 의 BufferedReader
String line = reader.readLine();   // java.io 메서드

🎯 핵심 요약 — 3줄 정리

1. I/O 의 정의

  • JVM 기준의 데이터 흐름
  • Input: 외부 → JVM
  • Output: JVM → 외부

2. 자바 I/O 3 시대

  • Java 1.0: java.io (스트림, Blocking)
  • Java 1.4: java.nio (채널, Non-blocking)
  • Java 7: NIO.2 (Path, Files, 비동기)

3. I/O 모델

  • I/O bound 가 일반적 (CPU 보다 수천~수억배 느림)
  • 4가지 모델 (Blocking/Non-blocking × Sync/Async)
  • 현대: Non-blocking + Async

📚 다음으로...

Unit 7.2 — IO vs NIO (역사적 진화)

이번 Unit에서 I/O 의 큰 그림을 봤다면, 다음은 IO 와 NIO 의 정밀한 비교.

  • File 클래스의 한계
  • NIO 의 채널 + 버퍼
  • Files (NIO.2) 의 현대적 API
  • 각 시대별 정확한 차이

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 시스템 큰 그림 (1/5 진행, 7.1 재작성)

총: 27/43 Unit 작성 (약 63%)
profile
Software Developer

0개의 댓글