F-LAB JAVA · 3주차 · Phase 8 · Stream 실전
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
FileInputStream은InputStream의 파일 전용 구현으로 디스크의 파일을 1바이트씩 읽는 가장 기본적인 도구다.
read()가 0~255 또는 -1 (EOF) 을 반환하며,byte의 -128~127 범위와의 충돌 회피를 위해int로 표현.
자원 (파일 핸들) 을 사용하므로 try-with-resources 필수, 안 닫으면 OS 의 파일 핸들 누수.
1바이트씩 읽기는 매우 느려서 (1MB 파일 = 100만 번 system call) 실무에서는 byte[] 버퍼 또는 BufferedInputStream 으로 감싸서 사용 (Unit 8.3 ~ 9.2).
NIO.2 의Files.newInputStream(Path)가 현대적 대안 — Path 와 결합 + 명확한 예외.
FileInputStream:
파일이라는 수도꼭지에서 물 받기
- 한 방울 (1바이트) 씩
- 수도꼭지 (FileInputStream) 가 비어있으면 -1
- 사용 후 잠그기 (close) 필수
- 안 잠그면 수도 (파일 핸들) 새는 중
읽기 메서드:
- read(): 1방울 (느림)
- read(byte[]): 여러 방울 한 번에 (효율)
→ FileInputStream = 파일 → JVM 의 가장 기본 통로.
1. FileInputStream 의 정의
2. 5가지 생성자
3. read() 의 정확한 동작
4. 파일 끝 (EOF) 의 -1
5. 예외 처리 (FileNotFoundException, IOException)
6. 자원 관리와 try-with-resources
7. 추가 메서드 (available, skip, mark/reset)
8. NIO.2 의 대안과 실무 패턴
9. 면접 + 자기 점검
package java.io;
public class FileInputStream extends InputStream {
private final FileDescriptor fd;
private final String path;
// 생성자, 메서드, ...
}
핵심:
InputStream 의 자식 (구체 클래스)java.io 패키지InputStream 의 계층:
InputStream (추상)
├── FileInputStream ← 파일
├── ByteArrayInputStream ← 메모리 바이트 배열
├── PipedInputStream ← 파이프
├── FilterInputStream
│ ├── BufferedInputStream
│ ├── DataInputStream
│ ├── PushbackInputStream
│ └── 기타
├── ObjectInputStream ← 직렬화
└── SequenceInputStream ← 연속
FileInputStream 의 위치:
- InputStream 의 직접 자식
- 파일 시스템의 파일을 추상화
public class FileInputStream extends InputStream {
// 생성자 (다음 섹션)
// 핵심 메서드
public int read() throws IOException;
public int read(byte[] b) throws IOException;
public int read(byte[] b, int off, int len) throws IOException;
// 추가 메서드
public long skip(long n) throws IOException;
public int available() throws IOException;
public void close() throws IOException;
// FileInputStream 만의 메서드
public FileChannel getChannel(); // NIO Channel 로 변환
public FileDescriptor getFD() throws IOException;
// 상속 (InputStream)
public byte[] readAllBytes() throws IOException; // Java 9+
public long transferTo(OutputStream out) throws IOException; // Java 9+
}
// 가장 단순한 사용
public class BasicRead {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("data.txt");
try {
int b;
while ((b = fis.read()) != -1) {
System.out.print((char) b);
}
} finally {
fis.close();
}
}
}
// try-with-resources (권장)
public static void main(String[] args) throws IOException {
try (FileInputStream fis = new FileInputStream("data.txt")) {
int b;
while ((b = fis.read()) != -1) {
System.out.print((char) b);
}
}
}
FileInputStream = 파일 핸들 + 읽기 위치
내부:
- FileDescriptor (OS 의 파일 핸들 추상화)
- 파일 내 현재 읽기 위치 (file pointer)
- 매 read() 가 OS 의 read 시스템 호출
OS 자원:
- 각 OS 마다 파일 핸들 한도 (ulimit -n)
- 한도 초과 시 "Too many open files" 에러
- 명시적 close 필수
// InputStream 의 자식 — 다형성 활용
public void process(InputStream in) throws IOException {
int b;
while ((b = in.read()) != -1) {
// 처리
}
}
// 다양한 InputStream 으로 호출 가능
process(new FileInputStream("file.txt")); // 파일
process(new ByteArrayInputStream(bytes)); // 메모리
process(socket.getInputStream()); // 네트워크
process(System.in); // 표준 입력
// 모두 같은 인터페이스
// 1. 단순 파일 읽기
public byte[] readFileContent(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes(); // Java 9+
}
}
// 2. 추상화된 처리
public void processData(InputStream source) throws IOException {
// FileInputStream, ByteArrayInputStream 등 모두 처리 가능
int b;
while ((b = source.read()) != -1) {
// 처리
}
}
// 3. 파일을 InputStream 으로 다른 메서드에 전달
public void uploadShipmentFile(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
uploader.upload(fis); // uploader 는 InputStream 받음
}
}
FileInputStream 의 정의와 위치는?
답:
1. 정의:
본질:
다형성:
자원:
// 1. 경로 문자열
public FileInputStream(String name) throws FileNotFoundException;
// 2. File 객체
public FileInputStream(File file) throws FileNotFoundException;
// 3. FileDescriptor (저수준)
public FileInputStream(FileDescriptor fdObj);
자주 사용되는 3가지 + 변형들.
// 절대 경로
FileInputStream fis1 = new FileInputStream("/var/data/file.txt");
// 상대 경로 (현재 작업 디렉토리 기준)
FileInputStream fis2 = new FileInputStream("data.txt");
// → 보통 프로젝트 루트 기준
// Windows
FileInputStream fis3 = new FileInputStream("C:\\Users\\user\\file.txt");
// 또는 forward slash 도 OK
FileInputStream fis4 = new FileInputStream("C:/Users/user/file.txt");
// File 활용 — 더 유연
File file = new File("/var/data/file.txt");
FileInputStream fis = new FileInputStream(file);
// File 의 메서드 활용
if (file.exists() && file.canRead()) {
FileInputStream fis = new FileInputStream(file);
}
// 부모 + 자식
File dir = new File("/var/data");
File f = new File(dir, "report.txt");
FileInputStream fis = new FileInputStream(f);
// Path → File 변환
Path path = Path.of("/var/data/file.txt");
FileInputStream fis = new FileInputStream(path.toFile());
// FileDescriptor 활용 (드물게)
FileInputStream existing = new FileInputStream("file.txt");
FileDescriptor fd = existing.getFD();
// 같은 fd 로 새 FileInputStream
FileInputStream another = new FileInputStream(fd);
// 같은 파일 핸들 공유
// 표준 입력
FileInputStream stdin = new FileInputStream(FileDescriptor.in);
// 활용:
// - 저수준 파일 핸들 공유
// - 표준 입력의 직접 활용
// - 보통은 사용 안 함
// FileNotFoundException
try {
FileInputStream fis = new FileInputStream("nonexistent.txt");
} catch (FileNotFoundException e) {
// 1. 파일이 없음
// 2. 디렉토리
// 3. 권한 없음 (읽기 불가)
}
// FileNotFoundException 의 부모
// FileNotFoundException extends IOException
// 잡기
try {
FileInputStream fis = new FileInputStream("file.txt");
} catch (FileNotFoundException e) {
// 파일 못 찾음
} catch (IOException e) {
// 기타 I/O 에러 (FileNotFoundException 도 잡힘, 부모니까)
}
// 더 일반적으로
try {
FileInputStream fis = new FileInputStream("file.txt");
} catch (IOException e) {
// 모든 I/O 에러
}
// java.io
FileInputStream fis = new FileInputStream("file.txt");
// 또는
FileInputStream fis = new FileInputStream(new File("file.txt"));
// NIO.2 (Java 7+)
InputStream is = Files.newInputStream(Path.of("file.txt"));
// 또는 옵션 추가
InputStream is = Files.newInputStream(
Path.of("file.txt"),
StandardOpenOption.READ);
// 차이:
// - FileInputStream: 구체 타입
// - Files.newInputStream: InputStream 반환 (구체 타입 숨김)
// - 후자가 더 유연
// 명확한 예외
try {
InputStream is = Files.newInputStream(Path.of("file.txt"));
} catch (NoSuchFileException e) {
// 파일 없음 (FileNotFoundException 보다 명확)
} catch (AccessDeniedException e) {
// 권한 없음 (별도 예외)
} catch (IOException e) {
// 기타
}
| 생성자 | 매개변수 | 특징 |
|---|---|---|
(String) | 경로 문자열 | 가장 단순 |
(File) | File 객체 | File 활용 가능 |
(FileDescriptor) | 저수준 fd | 드물게 사용 |
public class ShipmentFileReader {
private final Path baseDir = Path.of("/var/shipment");
// 1. 단순 경로
public byte[] readSimple(String filename) throws IOException {
try (FileInputStream fis = new FileInputStream(
baseDir.resolve(filename).toString())) {
return fis.readAllBytes();
}
}
// 2. File 활용 (검증)
public byte[] readWithValidation(String filename) throws IOException {
File file = baseDir.resolve(filename).toFile();
if (!file.exists()) {
throw new FileNotFoundException("Not found: " + file);
}
if (!file.canRead()) {
throw new IOException("Cannot read: " + file);
}
if (file.length() > 100_000_000) {
throw new IOException("Too large: " + file);
}
try (FileInputStream fis = new FileInputStream(file)) {
return fis.readAllBytes();
}
}
// 3. NIO.2 권장
public byte[] readModern(String filename) throws IOException {
Path path = baseDir.resolve(filename);
return Files.readAllBytes(path);
// 가장 간결
}
}
FileInputStream 의 생성자 3가지는?
답:
1. (String name):
(File file):
(FileDescriptor fd):
예외:
NIO.2 대안:
Files.newInputStream(Path)public int read() throws IOException;
// 동작:
// 1. 파일에서 1바이트 읽기
// 2. 0~255 의 int 로 반환
// 3. EOF 면 -1 반환
// Blocking:
// - 파일이면 거의 즉시 (디스크 I/O)
// - 네트워크라면 데이터 올 때까지
사용자 코드:
int b = fis.read();
JVM 내부:
1. native 메서드 호출
2. JNI 통해 OS 시스템 호출 (read())
3. user → kernel 전환
4. kernel 이 파일에서 1바이트 읽음
5. kernel → user 전환
6. int 로 반환 (0~255 또는 -1)
파일 위치:
- 매 read() 후 위치 +1
- 다음 read 는 다음 바이트
파일 내용 (5바이트):
[0x48, 0x65, 0x6C, 0x6C, 0x6F]
("Hello" — ASCII)
read 호출:
fis.read(); // 72 (0x48, 'H') 파일 위치: 1
fis.read(); // 101 (0x65, 'e') 파일 위치: 2
fis.read(); // 108 (0x6C, 'l') 파일 위치: 3
fis.read(); // 108 (0x6C, 'l') 파일 위치: 4
fis.read(); // 111 (0x6F, 'o') 파일 위치: 5
fis.read(); // -1 (EOF) 파일 위치: 5 (변화 없음)
fis.read(); // -1 (이후도 계속 -1)
// 패턴 1: while + -1
try (FileInputStream fis = new FileInputStream("file.txt")) {
int b;
while ((b = fis.read()) != -1) {
// 처리
System.out.print((char) b);
}
}
// 패턴 2: 캐스팅 주의
int b = fis.read();
if (b == -1) {
// EOF
} else {
byte data = (byte) b; // -128 ~ 127 (signed)
char c = (char) b; // 0 ~ 65535 (unsigned)
// 영문 (ASCII) 만 정상
// 한글 등은 깨짐
}
// 패턴 3: 카운트
int count = 0;
while (fis.read() != -1) {
count++;
}
// count = 파일 크기 (바이트)
// 하지만 fis.available() 또는 file.length() 가 더 빠름
// 1MB 파일을 1바이트씩
try (FileInputStream fis = new FileInputStream("1MB.dat")) {
int b;
while ((b = fis.read()) != -1) {
// 1,048,576 번 호출
// 각 호출 = 1 system call
// 총 ~1초 이상 (오버헤드)
}
}
// 더 빠른 방법 (Unit 8.3):
// 1. byte[] 활용
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
// 128 번 호출만
// 8000배 빠름
}
// 2. BufferedInputStream (Unit 9.2):
BufferedInputStream bis = new BufferedInputStream(fis);
int b;
while ((b = bis.read()) != -1) {
// 사용자는 1바이트씩 호출하지만
// 내부적으로 8KB 단위로 OS 호출
}
// 함정: byte 로 직접 캐스팅
byte b = (byte) fis.read();
// 0xFF 면 -1
// EOF 와 구분 불가
// 올바른 처리
int b = fis.read();
if (b == -1) {
break;
}
byte data = (byte) b; // 안전
// 또는 int 로 그대로 비교
int b = fis.read();
while (b != -1) {
process(b);
b = fis.read();
}
// 일반 파일: 거의 즉시 (디스크 캐시)
try (FileInputStream fis = new FileInputStream("file.txt")) {
int b = fis.read(); // 빠름 (μs)
}
// 네트워크 파일 (NFS): 느림 가능
// 외부 디스크: 느림 가능
// FIFO 파이프: Blocking
try (FileInputStream fis = new FileInputStream("/tmp/myfifo")) {
int b = fis.read(); // 데이터 올 때까지 대기
}
// /dev/random: 데이터 부족 시 Blocking
try (FileInputStream fis = new FileInputStream("/dev/random")) {
int b = fis.read(); // 엔트로피 부족 시 대기
}
// 1. 작은 파일 처리
public String readSmallFile(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
// 작은 파일 (< 1MB) 만
StringBuilder sb = new StringBuilder();
int b;
while ((b = fis.read()) != -1) {
sb.append((char) b);
}
return sb.toString();
// 영문만 정상, 한글 깨짐
}
}
// 2. 바이트 카운트
public long countBytes(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
long count = 0;
while (fis.read() != -1) {
count++;
}
return count;
}
// 비효율적
// 더 좋은 방법: file.length() 또는 Files.size(path)
}
// 3. 특정 바이트 검색
public long findByte(String path, byte target) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
long pos = 0;
int b;
while ((b = fis.read()) != -1) {
if ((byte) b == target) return pos;
pos++;
}
return -1;
}
}
read() 의 정확한 동작은?
답:
1. 시그니처:
int read() throws IOException반환:
동작:
비효율:
함정:
EOF (End Of File):
파일의 끝, 더 이상 읽을 데이터 없음.
InputStream 의 표시:
- read() 가 -1 반환
- read(byte[]) 가 -1 반환 (한 바이트도 못 읽음)
이유:
- byte (0~255) 와 다른 값 필요
- int 의 -1 활용 (32비트, byte 의 범위 밖)
왜 -1 인가?
byte 의 범위:
signed byte: -128 ~ 127
unsigned byte: 0 ~ 255 (자바엔 없음)
바이너리: 11111111 = 0xFF
byte 로 보면 -1
unsigned 로 보면 255
문제:
byte 로 직접 비교 시
if (b == -1) { // EOF? 또는 0xFF 정상 데이터?
}
→ 구분 불가
해결: int 로 확장
read() 가 int 반환
- 0xFF 데이터: int 의 255
- EOF: int 의 -1
→ 명확히 구분
// 잘못된 비교
byte b = (byte) fis.read();
if (b == -1) { // ❌ 0xFF 와 혼동
// EOF? 아니면 정상 데이터?
}
// 올바른 비교
int b = fis.read();
if (b == -1) { // ✓ EOF 명확
break;
}
// int 로 작업
byte data = (byte) b; // 이제 안전하게 캐스팅
// read(byte[]) 의 반환
byte[] buf = new byte[1024];
int n = fis.read(buf);
// n 의 의미:
// - 양수: 실제 읽은 바이트 수 (1 ~ buf.length)
// - -1: EOF (한 바이트도 못 읽음)
// - 0: 드뭄 (보통 Blocking 에서)
while (n != -1) {
// 0 ~ n-1 처리
process(buf, 0, n);
n = fis.read(buf);
}
// 마지막 읽기 함정:
// 파일 크기 1023 바이트, 버퍼 1024
// 첫 read: n=1023
// 두 번째 read: n=-1 (EOF)
try (FileInputStream fis = new FileInputStream("file.txt")) {
// 파일을 끝까지 읽음
while (fis.read() != -1) { }
// 이후 계속 -1 반환
int b = fis.read(); // -1
b = fis.read(); // -1
// 파일이 변경되어도 (다른 프로세스가)
// 이미 닫힌 위치라 -1 유지
// 다시 열거나 position 변경 필요
}
다른 곳의 "끝" 신호:
InputStream.read(): -1 (EOF)
Reader.read(): -1 (EOF, char 의 -1)
BufferedReader.readLine(): null (없음)
Scanner.hasNext(): false
Iterator.hasNext(): false
DataInputStream.readUTF(): EOFException 예외
차이:
- InputStream: int 의 -1 (반환값)
- Reader: int 의 -1 (char 의 충돌 회피)
- BufferedReader.readLine: null
- 일부 (DataInputStream): 예외
// 패턴 1: while 안에서 변수 할당 (Java 의 관용)
int b;
while ((b = fis.read()) != -1) {
process(b);
}
// 패턴 2: 분리
while (true) {
int b = fis.read();
if (b == -1) break;
process(b);
}
// 패턴 3: do-while
int b;
do {
b = fis.read();
if (b != -1) process(b);
} while (b != -1);
// 일반적으로 패턴 1 권장
// 일반 EOF: -1 반환 (정상)
try (FileInputStream fis = new FileInputStream("file.txt")) {
int b;
while ((b = fis.read()) != -1) { }
// 정상 종료
}
// I/O 에러: 예외
try (FileInputStream fis = new FileInputStream("file.txt")) {
int b = fis.read();
// 디스크 에러, 권한 변경 등
// → IOException
} catch (IOException e) {
// 에러 처리
}
// 즉:
// - 파일 끝까지: -1
// - 에러: 예외
// 표준 패턴
public byte[] readAll(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = is.read()) != -1) {
baos.write(b);
}
return baos.toByteArray();
}
// 더 효율적 (byte[] 활용)
public byte[] readAllEfficient(InputStream is) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int n;
while ((n = is.read(buf)) != -1) {
baos.write(buf, 0, n);
}
return baos.toByteArray();
}
// 더 간단 (Java 9+)
public byte[] readAllSimple(InputStream is) throws IOException {
return is.readAllBytes();
}
파일 끝 (-1) 의 정확한 의미는?
답:
1. -1 의 이유:
반환:
read(): 0~255 정상, -1 EOFread(byte[]): 양수 정상, -1 EOF함정:
byte b = (byte) read() 직접 캐스팅 X지속성:
EOF vs 에러:
Exception
└── IOException
├── FileNotFoundException ← 생성자
├── EOFException ← DataInputStream 등
├── InterruptedIOException
└── ... (기타)
// 생성자가 던질 수 있음
public FileInputStream(String name) throws FileNotFoundException;
// 발생 원인:
// 1. 파일이 존재하지 않음
// 2. 디렉토리 (파일이 아님)
// 3. 권한 없음 (읽기 불가)
try {
FileInputStream fis = new FileInputStream("nonexistent.txt");
} catch (FileNotFoundException e) {
System.err.println("Not found: " + e.getMessage());
}
// read() 등 메서드가 던질 수 있음
public int read() throws IOException;
// 발생 원인:
// 1. 디스크 에러
// 2. 권한 변경 (도중에)
// 3. 닫힌 스트림
// 4. 인터럽트
try (FileInputStream fis = new FileInputStream("file.txt")) {
int b;
while ((b = fis.read()) != -1) {
// ...
}
} catch (IOException e) {
System.err.println("I/O error: " + e.getMessage());
}
// FileNotFoundException 은 IOException 의 자식
// IOException 만 잡으면 둘 다 잡힘
try {
FileInputStream fis = new FileInputStream("file.txt");
fis.read();
fis.close();
} catch (IOException e) {
// FileNotFoundException 도 여기서 잡힘
}
// 구체적으로 잡기 (권장)
try {
FileInputStream fis = new FileInputStream("file.txt");
fis.read();
fis.close();
} catch (FileNotFoundException e) {
// 파일 못 찾음
} catch (IOException e) {
// 기타 I/O 에러
}
// java.io
try {
FileInputStream fis = new FileInputStream("file.txt");
} catch (FileNotFoundException e) {
// 파일 없음? 디렉토리? 권한 없음?
// 정확한 원인 모름
}
// NIO.2 — 명확한 구분
try {
InputStream is = Files.newInputStream(Path.of("file.txt"));
} catch (NoSuchFileException e) {
// 파일 없음
} catch (NotDirectoryException e) {
// 디렉토리 관련
} catch (AccessDeniedException e) {
// 권한 없음
} catch (IOException e) {
// 기타
}
// 패턴 1: try-catch
public byte[] readSafe(String path) {
try {
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes();
}
} catch (FileNotFoundException e) {
log.warn("File not found: {}", path);
return new byte[0];
} catch (IOException e) {
log.error("I/O error reading: {}", path, e);
return new byte[0];
}
}
// 패턴 2: throws (위로 전파)
public byte[] read(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes();
}
}
// 패턴 3: 변환
public byte[] readOrThrow(String path) {
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes();
} catch (IOException e) {
throw new RuntimeException("Failed to read: " + path, e);
}
}
// 패턴 4: Optional
public Optional<byte[]> readOptional(String path) {
try (FileInputStream fis = new FileInputStream(path)) {
return Optional.of(fis.readAllBytes());
} catch (IOException e) {
return Optional.empty();
}
}
// ❌ 자원 누수 가능
public byte[] readBad(String path) {
try {
FileInputStream fis = new FileInputStream(path);
byte[] data = fis.readAllBytes();
fis.close();
return data;
} catch (IOException e) {
// close 안 됨!
return null;
}
}
// ✓ try-finally
public byte[] readOk(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
try {
return fis.readAllBytes();
} finally {
fis.close(); // 예외 무관 항상 실행
}
}
// ✓✓ try-with-resources (권장)
public byte[] readBest(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes();
}
// 자동 close + Suppressed Exception
}
@Service
public class ShipmentFileService {
private final Path baseDir = Path.of("/var/shipment");
// 1. 명확한 예외 처리
public byte[] readShipmentFile(String filename) {
Path path = baseDir.resolve(filename);
try (FileInputStream fis = new FileInputStream(path.toFile())) {
return fis.readAllBytes();
} catch (FileNotFoundException e) {
throw new ShipmentFileNotFoundException(filename);
} catch (IOException e) {
throw new ShipmentFileIOException(filename, e);
}
}
// 2. NIO.2 권장
public byte[] readShipmentFileModern(String filename) {
Path path = baseDir.resolve(filename);
try {
return Files.readAllBytes(path);
} catch (NoSuchFileException e) {
throw new ShipmentFileNotFoundException(filename);
} catch (AccessDeniedException e) {
throw new ShipmentFileAccessException(filename);
} catch (IOException e) {
throw new ShipmentFileIOException(filename, e);
}
}
// 사용자 정의 예외
public static class ShipmentFileNotFoundException extends RuntimeException {
public ShipmentFileNotFoundException(String filename) {
super("Shipment file not found: " + filename);
}
}
public static class ShipmentFileIOException extends RuntimeException {
public ShipmentFileIOException(String filename, Throwable cause) {
super("I/O error: " + filename, cause);
}
}
}
FileInputStream 의 예외 처리는?
답:
1. 두 가지 주요 예외:
계층:
NIO.2 권장:
자원 관리:
파일 핸들 (File Handle):
OS 가 관리하는 파일에 대한 참조.
특징:
- 제한된 자원 (OS 마다 한도)
- 명시적 해제 필요
- 해제 안 하면 누수
OS 한도:
- Linux 기본: 1024 (ulimit -n)
- 일부 OS: 65536
- 한도 초과 시: "Too many open files"
// ❌ 자원 누수
public void readMany(List<String> paths) throws IOException {
for (String path : paths) {
FileInputStream fis = new FileInputStream(path);
// 처리...
// close 안 함!
}
}
// 1만 파일 처리 → 1만 핸들 누수
// 한도 초과 → 에러
// ❌ 예외 시 누수
public byte[] readBad(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
byte[] data = fis.readAllBytes(); // ★ 여기서 예외 가능
fis.close();
return data;
// 예외 시 close 안 됨
}
public byte[] readSafe(String path) throws IOException {
FileInputStream fis = new FileInputStream(path);
try {
return fis.readAllBytes();
} finally {
fis.close(); // 예외 무관 항상 실행
}
}
// 여러 자원
public void copy(String src, String dest) throws IOException {
FileInputStream fis = new FileInputStream(src);
try {
FileOutputStream fos = new FileOutputStream(dest);
try {
// 복사
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
fos.write(buf, 0, n);
}
} finally {
fos.close();
}
} finally {
fis.close();
}
}
// 중첩 → 가독성 ↓
// 단일 자원
public byte[] readBest(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
return fis.readAllBytes();
}
// 자동 close
}
// 여러 자원
public void copy(String src, String dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
fos.write(buf, 0, n);
}
}
// fos, fis 자동 close (역순)
}
// FileInputStream 의 계층
FileInputStream
extends InputStream
implements Closeable ← AutoCloseable 의 자식
// Closeable
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}
// AutoCloseable
public interface AutoCloseable {
void close() throws Exception;
}
// try-with-resources 의 조건:
// AutoCloseable 구현
// FileInputStream 은 자동으로
public void close() throws IOException {
// 1. OS 의 close 시스템 호출
// 2. 파일 핸들 해제
// 3. 버퍼링된 데이터 flush (OutputStream)
// 4. 자원 정리
// 두 번 close 호출 OK (no-op)
// 단, 자원 누수 방지 위해 한 번만 권장
}
// 사용 중인 파일 핸들 확인 (Linux)
// $ lsof -p <PID>
// 또는
// $ ls /proc/<PID>/fd
// Java 코드에서
// java.lang.management.OperatingSystemMXBean (UnixOperatingSystemMXBean)
OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
if (os instanceof UnixOperatingSystemMXBean) {
long open = ((UnixOperatingSystemMXBean) os).getOpenFileDescriptorCount();
long max = ((UnixOperatingSystemMXBean) os).getMaxFileDescriptorCount();
System.out.println("Open FDs: " + open + " / " + max);
}
@Service
public class ShipmentFileService {
// 1. 단일 파일 처리
public byte[] readFile(Path path) throws IOException {
try (FileInputStream fis = new FileInputStream(path.toFile())) {
return fis.readAllBytes();
}
}
// 2. 대량 처리 — 각 파일을 try-with-resources
public void processManyFiles(List<Path> paths) throws IOException {
for (Path path : paths) {
try (FileInputStream fis = new FileInputStream(path.toFile())) {
processStream(fis);
}
// 각 파일 close 보장
}
}
// 3. 파일 복사
public void copyShipmentFile(Path src, Path dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src.toFile());
FileOutputStream fos = new FileOutputStream(dest.toFile())) {
fis.transferTo(fos); // Java 9+
}
// 둘 다 자동 close
}
// 4. 모니터링
public long countOpenFiles() {
OperatingSystemMXBean os = ManagementFactory.getOperatingSystemMXBean();
if (os instanceof UnixOperatingSystemMXBean unix) {
return unix.getOpenFileDescriptorCount();
}
return -1;
}
}
FileInputStream 의 자원 관리는?
답:
1. 자원성:
누수의 위험:
권장 패턴:
다중 자원:
모니터링:
public int available() throws IOException;
// 반환:
// - 즉시 읽기 가능한 바이트 수 (대략)
// - Blocking 없이 read 가능한 양
// 파일에서:
try (FileInputStream fis = new FileInputStream("file.txt")) {
int avail = fis.available();
// 보통 파일 크기 - 현재 위치
byte[] buf = new byte[avail];
fis.read(buf); // Blocking 없이
}
// 함정 1: 정확한 파일 크기 아닐 수 있음
// - 파일이 매우 클 때
// - 네트워크 파일 시스템
// - 파이프
// 함정 2: 0 반환 가능
// - 데이터 없음 (스트림 끝 아님)
// - 0 ≠ EOF
// ❌ 잘못된 사용
byte[] buf = new byte[fis.available()];
fis.read(buf);
// 큰 파일에서 OutOfMemoryError 위험
// 또는 부분만 읽힘
// ✓ 권장: 파일 크기는 별도
long size = Files.size(path);
// 또는 file.length()
public long skip(long n) throws IOException;
// 매개변수: 건너뛸 바이트 수
// 반환: 실제 건너뛴 바이트 수
// 활용
try (FileInputStream fis = new FileInputStream("file.txt")) {
fis.skip(100); // 처음 100바이트 건너뛰기
int b = fis.read(); // 101번째 바이트부터
}
// 함정: 요청한 만큼 다 건너뛸 보장 X
long skipped = fis.skip(1000);
// skipped < 1000 가능
// EOF 도달, 또는 일부만
// 정확히 n 바이트 건너뛰기
public static void skipExactly(InputStream is, long n) throws IOException {
long remaining = n;
while (remaining > 0) {
long skipped = is.skip(remaining);
if (skipped <= 0) {
// skip 못 함 — read 로 대체
if (is.read() == -1) {
throw new EOFException();
}
remaining--;
} else {
remaining -= skipped;
}
}
}
// Java 12+ skipNBytes
fis.skipNBytes(1000); // 정확히 1000, 부족하면 예외
// FileInputStream 은 mark/reset 미지원
FileInputStream fis = new FileInputStream("file.txt");
fis.markSupported(); // false
fis.mark(100); // 효과 없음
fis.reset(); // IOException
// 이유:
// - 파일은 일반적으로 seek 가능
// - 하지만 InputStream API 의 단순성
// - 필요시 RandomAccessFile 또는 FileChannel
// mark/reset 이 필요하면 BufferedInputStream 으로 감싸기
BufferedInputStream bis = new BufferedInputStream(fis);
bis.markSupported(); // true
bis.mark(100); // 가능
// ... read ...
bis.reset(); // 다시 mark 위치로
// FileInputStream → FileChannel (NIO)
try (FileInputStream fis = new FileInputStream("file.txt")) {
FileChannel channel = fis.getChannel();
// position 이동
channel.position(100);
// ByteBuffer 활용
ByteBuffer buf = ByteBuffer.allocate(1024);
channel.read(buf);
}
// 활용:
// - Random Access
// - Memory-mapped file
// - zero-copy (transferTo)
// Java 9+ — InputStream 의 새 메서드
// readAllBytes: 모두 한 번에
try (FileInputStream fis = new FileInputStream("small.txt")) {
byte[] all = fis.readAllBytes();
// 작은 파일만 (큰 파일은 OOM)
}
// transferTo: 다른 OutputStream 으로 전송
try (FileInputStream fis = new FileInputStream("src.txt");
FileOutputStream fos = new FileOutputStream("dest.txt")) {
long copied = fis.transferTo(fos);
// 효율적 복사
}
public class ShipmentBinaryReader {
// 1. 헤더 건너뛰고 본문만 읽기
public byte[] readContent(Path file) throws IOException {
try (FileInputStream fis = new FileInputStream(file.toFile())) {
fis.skip(128); // 헤더 128바이트 건너뛰기
return fis.readAllBytes();
}
}
// 2. 특정 위치의 데이터 읽기
public byte[] readChunk(Path file, long offset, int length) throws IOException {
try (FileInputStream fis = new FileInputStream(file.toFile())) {
fis.skipNBytes(offset); // Java 12+
byte[] buf = new byte[length];
int n = fis.read(buf);
return Arrays.copyOf(buf, n);
}
}
// 3. 효율적 복사 (transferTo)
public void backup(Path src, Path dest) throws IOException {
try (FileInputStream fis = new FileInputStream(src.toFile());
FileOutputStream fos = new FileOutputStream(dest.toFile())) {
fis.transferTo(fos);
}
}
// 4. FileChannel 활용
public byte[] readWithChannel(Path file, long offset, int length) throws IOException {
try (FileInputStream fis = new FileInputStream(file.toFile())) {
FileChannel channel = fis.getChannel();
ByteBuffer buf = ByteBuffer.allocate(length);
channel.read(buf, offset);
buf.flip();
byte[] result = new byte[buf.remaining()];
buf.get(result);
return result;
}
}
}
FileInputStream 의 추가 메서드는?
답:
1. available():
skip(long n):
mark/reset:
getChannel():
Java 9+:
// java.io 의 FileInputStream
FileInputStream fis = new FileInputStream("file.txt");
// NIO.2 의 Files.newInputStream
InputStream is = Files.newInputStream(Path.of("file.txt"));
// 둘 다 InputStream 으로 작동
// 차이:
// - FileInputStream: 구체 클래스 (FileChannel 등 추가 메서드)
// - Files.newInputStream: 인터페이스 반환 (다형성 강조)
// 다양한 읽기 방법
Path path = Path.of("file.txt");
// 1. 모든 바이트
byte[] all = Files.readAllBytes(path);
// 2. 모든 텍스트
String text = Files.readString(path);
String text2 = Files.readString(path, StandardCharsets.UTF_8);
// 3. 모든 줄
List<String> lines = Files.readAllLines(path);
List<String> lines2 = Files.readAllLines(path, StandardCharsets.UTF_8);
// 4. Stream (lazy)
try (Stream<String> lines = Files.lines(path)) {
lines.forEach(System.out::println);
}
// 5. 일반 InputStream
try (InputStream is = Files.newInputStream(path)) {
// 처리
}
// 6. BufferedReader
try (BufferedReader reader = Files.newBufferedReader(path)) {
// 처리
}
// 다양한 옵션
InputStream is = Files.newInputStream(
Path.of("file.txt"),
StandardOpenOption.READ); // 기본
// 추가 옵션
StandardOpenOption.READ // 읽기
StandardOpenOption.WRITE // 쓰기
StandardOpenOption.APPEND // 이어쓰기
StandardOpenOption.TRUNCATE_EXISTING
StandardOpenOption.CREATE
StandardOpenOption.CREATE_NEW
StandardOpenOption.DELETE_ON_CLOSE
StandardOpenOption.SPARSE
StandardOpenOption.SYNC
StandardOpenOption.DSYNC
// java.io
try {
FileInputStream fis = new FileInputStream("file.txt");
} catch (FileNotFoundException e) {
// 모호: 파일 없음? 권한 없음? 디렉토리?
}
// NIO.2
try {
InputStream is = Files.newInputStream(Path.of("file.txt"));
} catch (NoSuchFileException e) {
// 파일 명확히 없음
} catch (AccessDeniedException e) {
// 권한 명확히 없음
} catch (NotDirectoryException e) {
// 디렉토리 관련 명확
} catch (IOException e) {
// 기타
}
실무 권장:
1. 작은 텍스트 파일 (< 10MB):
String content = Files.readString(path, UTF_8);
2. 작은 바이너리:
byte[] data = Files.readAllBytes(path);
3. 큰 텍스트 (Stream):
try (Stream<String> lines = Files.lines(path, UTF_8)) {
lines.forEach(...);
}
4. 큰 바이너리 (chunk):
try (InputStream is = Files.newInputStream(path)) {
byte[] buf = new byte[8192];
int n;
while ((n = is.read(buf)) != -1) {
process(buf, 0, n);
}
}
5. 한 줄씩 (BufferedReader):
try (BufferedReader br = Files.newBufferedReader(path, UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
process(line);
}
}
// 옛 코드 (java.io)
public byte[] readOld(String path) throws IOException {
FileInputStream fis = null;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
fis = new FileInputStream(path);
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
baos.write(buf, 0, n);
}
return baos.toByteArray();
} finally {
if (fis != null) fis.close();
}
}
// 중간 (try-with-resources)
public byte[] readMiddle(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buf = new byte[8192];
int n;
while ((n = fis.read(buf)) != -1) {
baos.write(buf, 0, n);
}
return baos.toByteArray();
}
}
// 새 코드 (NIO.2)
public byte[] readNew(String path) throws IOException {
return Files.readAllBytes(Path.of(path));
}
// 함수가 InputStream 받음 — 다양한 소스 처리
public byte[] processStream(InputStream is) throws IOException {
return is.readAllBytes();
}
// 다양한 호출
// 파일
try (InputStream is = Files.newInputStream(Path.of("file.txt"))) {
processStream(is);
}
// 메모리
processStream(new ByteArrayInputStream("data".getBytes()));
// 네트워크
URL url = new URL("https://example.com");
try (InputStream is = url.openStream()) {
processStream(is);
}
// JAR 리소스
try (InputStream is = MyClass.class.getResourceAsStream("/data.txt")) {
processStream(is);
}
// 모두 InputStream 으로 통일
@Service
public class ShipmentFileService {
private final Path baseDir = Path.of("/var/shipment");
// 1. 작은 파일
public String readSmallFile(String filename) throws IOException {
return Files.readString(baseDir.resolve(filename), StandardCharsets.UTF_8);
}
// 2. 큰 파일 — Stream
public long countLines(String filename) throws IOException {
try (Stream<String> lines = Files.lines(baseDir.resolve(filename))) {
return lines.count();
}
}
// 3. 처리 함수
public <R> R processFile(String filename, Function<InputStream, R> processor)
throws IOException {
try (InputStream is = Files.newInputStream(baseDir.resolve(filename))) {
return processor.apply(is);
}
}
// 4. 명확한 에러 처리
public byte[] readWithErrorHandling(String filename) {
Path path = baseDir.resolve(filename);
try {
return Files.readAllBytes(path);
} catch (NoSuchFileException e) {
log.warn("File not found: {}", filename);
return new byte[0];
} catch (AccessDeniedException e) {
log.error("Access denied: {}", filename);
throw new SecurityException("Cannot read: " + filename);
} catch (IOException e) {
log.error("I/O error: {}", filename, e);
throw new RuntimeException(e);
}
}
// 5. 다중 소스
public byte[] readAnySource(String source) throws IOException {
InputStream is;
if (source.startsWith("http")) {
is = new URL(source).openStream();
} else if (source.startsWith("classpath:")) {
is = getClass().getResourceAsStream(source.substring(10));
} else {
is = Files.newInputStream(Path.of(source));
}
try (is) {
return is.readAllBytes();
}
}
}
NIO.2 의 대안과 실무 패턴은?
답:
1. 대안:
Files.newInputStream(Path)Files.readAllBytes(Path)Files.readString(Path, Charset)Files.lines(Path)장점:
권장 패턴:
추상화:
| Q | 핵심 답변 |
|---|---|
| FileInputStream 정의? | InputStream 의 파일 전용 구현 |
| 5가지 생성자? | String, File, FileDescriptor + 변형 |
| read() 반환? | 0~255 또는 -1 (EOF) |
| -1 이유? | byte 와 충돌 회피 |
| 1바이트씩 비효율? | system call 폭증 |
| FileNotFoundException? | IOException 의 자식, 생성자 |
| 자원 누수? | close 누락, OS 한도 |
| try-with-resources? | 자동 close, AutoCloseable |
| available()? | 즉시 읽기 가능 (대략) |
| skip()? | 바이트 건너뛰기 |
| mark/reset? | FileInputStream 미지원 |
| getChannel()? | FileChannel 변환 (NIO) |
| Files.newInputStream? | NIO.2 대안 |
답:
// Java 9+ 에서 deprecated
@Deprecated(since = "9")
protected void finalize() throws IOException {
// 옛 자동 close 메커니즘
}
답:
// 작은 파일만
if (Files.size(path) < 100_000_000) { // 100MB
return Files.readAllBytes(path);
}
// 큰 파일은 Stream 처리
답:
답:
답:
File dir = new File("/var"); // 디렉토리
FileInputStream fis = new FileInputStream(dir);
// FileNotFoundException
// "Is a directory" (Linux) 또는 비슷한 메시지
1. FileInputStream
2. -1 의 이유
3. 권장 패턴
Files.newInputStream(Path) 또는 Files.readAllBytes(Path)이번 Unit에서 1바이트씩 read 의 비효율을 봤다면, 다음은 byte[] 활용으로 효율화.
🚀 Phase 8 — Stream 실전
✅ Unit 8.1 System.in (한글 안 되는 이유)
✅ Unit 8.2 FileInputStream ← 여기
⏭ Unit 8.3 byte[] 배열로 효율적 읽기
⏭ Unit 8.4 FileOutputStream
⏭ Unit 8.5 한글 처리 (FileReader, InputStreamReader)
⏭ Unit 8.6 FileWriter (한글 쓰기)
✅ Phase 1 ~ 7 완주 (31 Unit)
🚀 Phase 8 — Stream 실전 (2/6 진행)
총: 33/43 Unit (약 77%)