[Android] java.io.stream 고찰

by DDD

들어가며


안녕하세요! ✋


사이드 프로젝트 동아리 DDD에서 Android 개발자로 활동하고 있는 오세민입니다.


이번 글에서는 Java 개발을 하다 보면 반드시 마주치게 되는 주제,


Stream, InputStream, OutputStream의 설계 철학에 대해 이야기해보려고 해요.


특히,

Stream이라는 개념이 왜 존재하는지

왜 입력과 출력이 분리되어 있는지

flush()와 close()는 왜 필요한지


이런 질문들을 중심으로


“왜 이렇게 설계되었을까?”라는 질문에서 시작해 보려고 합니다.




Stream은 뭘까요


Stream이라는 단어를 처음 들으면 Java 8의 Stream API가 떠오를 수도 있어요.


하지만 여기서 이야기하는 Stream은 그것보다 훨씬 근본적인 개념이에요.


Stream은 “데이터의 흐름(Flow)”을 추상화한 구조예요.


Java의 설계 철학은 이래요.


모든 입출력은 Stream을 통해 이루어진다.


파일이든, 네트워크든, 디바이스든


동일한 방식으로 다룰 수 있도록


데이터를 ‘흐름(Stream)’이라는 개념으로 통일한 거예요.


그래서 Stream을 이렇게 정리할 수 있어요.


Stream = “JVM ↔ OS 간 데이터 통로”


JVM이 OS의 입출력 시스템(파일, 네트워크, 디바이스 등)과 통신하기 위한


통일된 인터페이스라고 보면 돼요.




실제로는 어떤 리소스와 연결될까요


Stream이라는 하나의 추상화 아래에는


실제로 다양한 리소스가 연결되어 있어요.

1*ZY_aaKzGXCVyniynetPtUw.png


파일을 읽든, 소켓으로 데이터를 보내든,


코드에서 다루는 방식은 동일해요.


이게 바로 Stream이라는 추상화가 가져다주는 힘이에요.




왜 InputStream과 OutputStream으로 나뉘어 있을까요


Java는 Stream을 단방향(one-way)으로 설계했어요.

1*B716VkvxVehfM0aukpuazw.png


이렇게 분리한 이유는 명확해요.


데이터 흐름의 책임을 명확히 하고, 예측 가능한 I/O를 보장하기 위해서예요.


“이 Stream은 읽기만 한다” 또는 “이 Stream은 쓰기만 한다”가


코드 레벨에서 명확하게 드러나는 거예요.


양방향 통신이 필요한 Socket 같은 경우에도


내부적으로는 InputStream + OutputStream 쌍으로 처리해요.


“데이터는 한 방향으로만 흐른다”는 철학을 일관되게 지키는 거예요.




왜 추상 클래스로 설계했을까요


InputStreamOutputStream은 인터페이스가 아니라 추상 클래스예요.


그 이유는 이래요.


파일, 네트워크, 메모리 등


다양한 데이터 소스를 동일한 인터페이스로 다루기 위해서예요.


구조를 보면 이렇게 생겼어요.


InputStream / OutputStream ← 추상화 계층 (Closeable)

┌─────────┼───────────┐
│ │ │
FileInputStream SocketInputStream ByteArrayInputStream
(fd 기반) (fd 기반) (메모리 기반)


모든 Stream은 동일한 추상 메서드를 통해 동작하지만,


각 구현체는 자신이 다루는 리소스(fd, socket, memory)에 따라


실제 I/O 방식을 다르게 정의해요.


덕분에 우리는 “이게 파일인지 소켓인지”를 신경 쓰지 않고


동일한 코드로 데이터를 읽고 쓸 수 있어요.




Closeable을 상속한 이유가 뭘까요


Stream이 OS 리소스와 연결된 이상,


그 통로(fd)는 커널에 의해 관리되며 유한한 리소스예요.


Closeable을 상속한 이유는 명확해요.


Stream이 열리면 OS에 리소스가 등록되고, close()는 그 리소스를 OS에 반납해요.


단계별로 보면 이래요.

1*DOmOATTb3nYFhP_iusivbg.png


그래서 close()는 이렇게 이해하면 돼요.


close() = "JVM이 OS에게 이제 이 통로(fd)를 닫아도 좋습니다"라는 신호


close()를 호출하지 않으면


fd가 계속 열린 채로 남아서 리소스 누수(resource leak)가 발생해요.


이건 단순히 메모리 문제가 아니라,


OS 레벨의 리소스 고갈로 이어질 수 있는 심각한 문제예요.




flush는 단순히 “버퍼를 비우는 것”이 아니에요


flush()를 "버퍼를 비운다"고만 알고 계신 분이 많을 거예요.


하지만 조금 더 정확하게 말하면 이래요.


flush() = "지연된 I/O를 즉시 수행하여 논리적 상태와 물리적 상태를 일치시키는 행위"


데이터는 여러 단계의 버퍼를 거쳐요.


[앱 코드] → [JVM 버퍼] → [OS 커널 버퍼] → [디스크/네트워크]
1*fqWeWnBcMsuHQrPnyXfcLw.png


flush()는 "현재 계층의 데이터를 다음 계층으로 강제 전송(commit)"하는 역할을 해요.


쉽게 말하면,


flush() = "이제 진짜로 보내라"


JVM 버퍼를 OS 커널로,


OS 버퍼를 디스크로 내보내는 타이밍 제어 수단이에요.


네트워크 통신에서 flush()를 빼먹으면


데이터가 버퍼에 머물러서 상대방에게 도착하지 않는 경우도 있어요.


그래서 flush()는 데이터 일관성을 보장하는 중요한 메서드예요.




설계 철학을 한눈에 정리하면요

1*Zgmxrj2VBwxKXot8SoBzvw.png


✅ 정리해보면 이렇습니다



Stream은 “JVM과 OS 사이의 통로”예요

InputStream과 OutputStream은 그 통로를 통해 데이터를 “읽고/쓰는 방향”을 정의해요

flush()는 데이터를 다음 계층으로 커밋하는 시점 제어 도구예요

close()는 OS 리소스를 해제하는 명시적 종료 신호예요



결국 Stream은 “I/O를 일관된 추상화로 표현한 계약(Contract)”이며,


Java의 일관성과 안전성 철학을 가장 잘 보여주는 구조예요.


무엇을 쓰느냐보다


왜 이런 구조가 존재하는지 이해하는 게 더 중요해요.


I/O는 조용히 동작하지만,


문제가 생기면 고치기 어렵거든요.


긴 글 읽어주셔서 감사합니다.




레퍼런스


이 글을 정리하면서 실제로 참고한 자료들이에요.


Java InputStream — Oracle Docs

Java OutputStream — Oracle Docs

Closeable — Oracle Docs

Java I/O Tutorial — Oracle