1. FileStream 클래스: 조각조각 작업하기
FileStream은 마치 네 파일에 직접 연결된 수도관 같은 거야. 이 관을 통해서 데이터를 컨트롤하면서 보낼 수 있어: 파일에 바이트를 보내거나(쓰기), 파일에서 바이트를 받아올 수 있지(읽기). 더 고수준 메서드들은 그냥 "물 한 컵"을 주는 거라면, FileStream은 네가 직접 "수도꼭지"를 조절해서 물(바이트) 흐름을 원하는 대로 조절할 수 있게 해줘.
FileStream은 바이트 단위로 동작해. 이 말은, 그게 텍스트든 이미지든 신경 안 써; 그냥 모든 게 바이트 시퀀스일 뿐이야. 만약 FileStream으로 텍스트 파일을 다루고 있다면, 직접 문자열을 바이트로 변환(예를 들어 인코딩, UTF-8 같은 거)해서 써야 하고, 읽을 때도 다시 바이트를 문자열로 바꿔야 해.
언제 FileStream이 꼭 필요할까?
- 엄청 큰 파일 다룰 때: 파일이 너무 커서 한 번에 메모리에 올릴 수 없거나(예: 기가바이트 로그, 동영상 파일), 그럴 필요가 없을 때. FileStream은 데이터를 조각(청크) 단위로 읽고 쓸 수 있어서 메모리 효율이 좋아.
- 바이너리 데이터: 그냥 텍스트가 아닌 파일(이미지, 오디오, 비디오, 직렬화된 객체, DB 파일 등) 다룰 때 FileStream이 필수야. 바이트에 직접 접근할 수 있으니까.
- 접근 모드 세밀하게 컨트롤할 때: 파일을 읽기*랑*쓰기 동시에 열고 싶다거나, 다른 프로그램이 접근 못 하게 하고 싶다거나, 반대로 여러 프로세스가 동시에 읽게 하고 싶을 때. FileStream이 이런 모드 전부 컨트롤해줘.
- 비동기 작업: 요즘 고성능 앱(예: 웹서버)에서는 파일 작업이 메인 스레드를 막으면 안 돼. FileStream은 비동기 메서드(ReadAsync, WriteAsync)도 지원해서 프로그램이 계속 반응할 수 있어.
- 부분 읽기/쓰기 또는 랜덤 액세스: 파일의 특정 위치(예: 500번째 바이트)에서 읽거나, 파일 중간에 데이터를 써야 할 때, FileStream으로 파일 포인터 위치를 직접 조절할 수 있어.
초보자들이 자주 하는 실수: FileStream을 그냥 간단히 한 줄 텍스트를 작은 파일에 쓸 때 쓰는 거. 이런 경우엔 오버킬이야. FileStream은 좀 더 복잡하고 특별한 상황에서 쓰는 도구야.
2. FileStream 만들기와 열기
파일을 FileStream으로 다루려면, 먼저 인스턴스를 만들어야 해. 이건 생성자를 쓰는데, 파일을 어떻게 다룰지 정하는 중요한 파라미터들을 넣어줘야 해.
using System;
using System.IO; // FileStream 쓸 때 꼭 필요
using System.Text; // 인코딩(문자열 <-> 바이트 변환) 쓸 때 필요
FileStream으로 텍스트 파일 기본 읽기 예제:
먼저 읽을 파일을 하나 만들어보자.
// 데모용으로 간단한 텍스트 파일 하나 만들기
string path = "example_filestream.txt";
// 읽기용 스트림 열기
FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
// 읽은 데이터를 임시로 저장할 버퍼(바이트 배열) 만들기
byte[] buffer = new byte[fs.Length];
// 스트림에서 버퍼로 바이트 읽기
int bytesRead = fs.Read(buffer, 0, buffer.Length);
// 읽은 바이트를 다시 문자열로 변환(예: UTF-8 인코딩 사용)
string content = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine("FileStream으로 파일에서 읽은 내용: " + content);
fs.Close(); //스트림 닫기
주요 파라미터
- FileMode: 파일을 어떻게 열지 (
Open,Create,AppendOpenOrCreate등) - FileAccess: 파일로 뭘 할지 (
Read,Write,ReadWrite) - FileShare: 다른 프로세스가 동시에 이 파일을 쓸 수 있는지(보통 간단한 작업엔 필요 없음)
자주 하는 실수: FileStream을 닫는 걸 까먹으면, 파일이 "잠겨서" 나중에 삭제나 재오픈이 안 될 수 있어!
3. FileStream 생성자 파라미터
FileStream 생성자는 파일을 어떻게 열지 아주 세밀하게 설정할 수 있어. 주요 파라미터는 이래:
new FileStream(
string path, // 파일 경로(절대/상대)
FileMode mode, // 파일을 어떻게 열지(생성, 열기, 덮어쓰기 등)
FileAccess access, // 어떤 접근 권한(읽기만, 쓰기만, 읽기+쓰기)
FileShare share // 파일이 열려있는 동안 다른 프로세스가 어떻게 접근할 수 있는지(옵션)
);
FileMode랑 FileAccess 열거형 값들을 살펴보자:
FileMode (파일 열기 모드):
파일을 열 때 OS가 어떻게 처리할지 정해.
FileMode.Open: 이미 있는 파일을 연다. 경로에 파일이 없으면 FileNotFoundException이 나와.FileMode.Create: 새 파일을 만든다. 같은 이름의 파일이 이미 있으면 완전히 덮어써서 내용이 다 사라져.
FileAccess (파일 접근 권한):
열린 파일로 어떤 작업이 가능한지 정해.
FileAccess.Read: 파일을 읽기만 가능. 쓰기는 못 해.FileAccess.Write: 파일을 쓰기만 가능. 읽기는 못 해.FileAccess.ReadWrite: 읽기랑 쓰기 둘 다 가능. 이 모드는 유연하지만, 스트림 위치 관리도 네가 직접 해야 해서 좀 더 신경 써야 해.
FileShare (공유 모드):
네 프로그램이 파일을 열고 있는 동안, 다른 프로세스가 그 파일을 어떻게 열 수 있는지 정해. 락 문제 방지에 중요해.
FileShare.Read: 다른 프로세스가 읽기만 가능, FileStream이 열려있는 동안 쓰기는 못 해.FileShare.Write: 다른 프로세스가 쓰기만 가능, 읽기는 못 해.FileShare.ReadWrite: 다른 프로세스가 읽기랑 쓰기 둘 다 가능. 이건 제일 자유로운 모드지만, 여러 프로그램이 동시에 파일을 바꾸면 충돌 날 수 있어.
모드별 자세한 내용은 다음 강의에서 다룰게.
4. FileStream으로 데이터 읽고 쓰기
FileStream을 쓸 때는 바이트 단위로 조작해. 그래서 텍스트 데이터를 다룰 땐 문자열과 바이트 배열 사이 변환을 직접 해줘야 해. (인코딩 클래스, 예: System.Text.Encoding.UTF8 사용)
FileStream으로 파일에 데이터 쓰기
쓰기할 때는 데이터를(예: 문자열) 바이트 배열로 변환해서 스트림에 써.
string outputPath = "user_data.txt";
string userName = "이반 페트로프";
// 문자열을 UTF-8 인코딩으로 바이트 배열로 변환
byte[] userNameBytes = Encoding.UTF8.GetBytes(userName);
// FileStream을 쓰기용으로 열기
FileStream fsWrite = new FileStream(outputPath, FileMode.Create, FileAccess.Write);
// 바이트 배열을 스트림에 쓰기
fsWrite.Write(userNameBytes, 0, userNameBytes.Length);
Console.WriteLine($"이름 '{userName}'이(가) 파일 '{outputPath}'에 성공적으로 저장됨.");
fsWrite.Close(); //스트림 닫기
FileStream으로 파일에서 데이터 읽기
읽기할 때는 스트림에서 바이트를 버퍼로 읽고, 그걸 다시 원하는 형식(예: 문자열)로 변환해.
string inputPath = "user_data.txt";
// 읽기용 FileStream 열기
FileStream fsRead = new FileStream(inputPath, FileMode.Open, FileAccess.Read);
// 파일 크기만큼 버퍼 만들기
byte[] buffer = new byte[fsRead.Length];
// 스트림에서 버퍼로 바이트 읽기
int bytesRead = fsRead.Read(buffer, 0, buffer.Length);
// 읽은 바이트를 문자열로 변환
string loadedUserName = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"FileStream으로 파일에서 읽은 이름: {loadedUserName}");
fsRead.Close(); //스트림 닫기
5. 큰 파일 다루기
FileStream의 가장 큰 장점은 File.ReadAll... 메서드랑 달리, 조각조각 읽고 쓸 수 있다는 거야. 이건 파일이 너무 커서 한 번에 메모리에 못 올릴 때 필수야.
큰 파일을 조각조각 읽기
파일을 조각조각 읽으려면, 고정 크기 버퍼(바이트 배열)를 만들어서, 파일 끝까지 계속 읽어오면 돼.
string bigFilePath = "bigfile.bin"; //큰 파일
int bufferSize = 4096; // 버퍼 크기, 예: 4KB
byte[] buffer = new byte[bufferSize]; // 버퍼 만들기
int bytesRead; // 실제로 읽은 바이트 수
long totalBytesRead = 0; // 전체 읽은 바이트 수
// 스트림 열기
FileStream fs = new FileStream(bigFilePath, FileMode.Open, FileAccess.Read);
int chunkNumber = 1;
// fs.Read()가 양수(읽은 바이트 수) 반환하는 동안 반복
while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0)
{
totalBytesRead += bytesRead;
Console.WriteLine($"조각 {chunkNumber++}: {bytesRead} 바이트 읽음. 전체: {totalBytesRead} 바이트.");
// !!! 여기서 읽은 "조각" 데이터를 처리하면 됨 !!!
}
fs.Close(); //스트림 닫기
중요한 팁: fs.Read(buffer, offset, count)는 항상 buffer를 다 채운다는 보장이 없어. 실제로 읽은 바이트 수를 반환하는데, 이게 count보다 작을 수도 있어(특히 파일 끝에서). 항상 bytesRead 값을 써서 데이터 처리해야 해.
큰 데이터 조각조각 쓰기
읽기랑 마찬가지로, 큰 데이터를 파일에 조각조각 쓸 수도 있어.
string bigOutputPath = "big_output.txt";
string longText = new string('A', 1_000_000); // 'A' 백만 개로 된 문자열
byte[] longTextBytes = Encoding.UTF8.GetBytes(longText);
int writeBufferSize = 1024; // 한 번에 1KB씩 쓰기
// FileMode.Create: 파일 생성
FileStream fsWrite = new FileStream(bigOutputPath, FileMode.Create, FileAccess.Write);
for (int i = 0; i < longTextBytes.Length; i += writeBufferSize)
{
// 이번에 쓸 바이트 수 계산
int bytesToWrite = Math.Min(writeBufferSize, longTextBytes.Length - i);
// longTextBytes에서 'i' 오프셋부터 bytesToWrite만큼 쓰기
fsWrite.Write(longTextBytes, i, bytesToWrite);
Console.WriteLine($"{bytesToWrite} 바이트 저장됨");
}
fsWrite.Close(); //스트림 닫기
6. 잘 안 보이는 함정과 흔한 실수들
FileStream처럼 강력한 도구도, 몇 가지 주의할 점이 있어:
버퍼링과 Flush(): 이미 말했듯이, 데이터가 메모리 버퍼에 남아있고 디스크에 안 써질 수도 있어. using을 안 쓰거나 Close()/Dispose()를 안 부르고 그냥 프로그램 끝내면, 데이터가 날아갈 수 있음. 항상 Flush()를 부르거나(using/Close()에 맡기거나) 해서 데이터가 확실히 저장되게 해.
예외 처리: 입출력 에러(예: IOException - 디스크 꽉 찼을 때, UnauthorizedAccessException - 권한 없을 때, FileNotFoundException - Open 모드로 없는 파일 열 때 등)는 흔해. FileStream 작업은 항상 try-catch로 감싸자.
인코딩: FileStream으로 텍스트 다룰 때(바이트 단위로 동작하니까), 인코딩을 네가 직접 잘 선택해야 해 (Encoding.UTF8, Encoding.ASCII, Encoding.Unicode 등). 잘못 고르면 "깨진 글자"가 나올 수 있어.
포지션 관리: Seek() 쓸 때는 offset이랑 origin 파라미터를 잘못 주면 파일 범위 밖이나 엉뚱한 위치로 갈 수 있으니 조심!
파일 락(FileShare): FileShare.None으로 파일을 열면, 다른 프로세스가 접근 못 해. 만약 다른 프로그램(혹은 네 프로그램의 다른 스레드)이 같은 파일을 쓰려고 하면 문제 생길 수 있어. 항상 상황에 맞는 FileShare 모드를 골라야 해.
GO TO FULL VERSION