1. 도입
.NET에는 파일과 폴더를 다루기 위한 철학적으로 다른 두 그룹의 클래스가 있습니다:
정적 클래스: File, Directory — 유틸리티 함수 집합처럼 동작합니다: 메서드를 호출하고 경로를 전달하면 결과를 얻습니다.
인스턴스 클래스: FileInfo, DirectoryInfo — 특정 파일과 폴더를 자신의 프로퍼티와 메서드를 가진 객체로 표현합니다.
왜 둘 다 있냐고요? 이건 사용의 단순성(정적 메서드)과 객체지향적 유연성(인스턴스 클래스) 사이의 절충안입니다. 각 방식은 특정 시나리오에서 편리합니다.
2. 정적 클래스 철학
File 및 Directory 클래스는 "그냥 해" 원칙을 따릅니다. 중간 객체를 만들 필요 없이 파일 시스템 작업을 수행하는 직관적인 인터페이스를 제공합니다.
// 파일 존재 확인
if (File.Exists("document.txt"))
{
// 전체 텍스트 읽기
string content = File.ReadAllText("document.txt");
// 백업 생성
File.Copy("document.txt", "document_backup.txt");
// 원본 삭제
File.Delete("document.txt");
}
단발성 간단한 작업에 편리합니다: 객체나 수명주기, 상태를 고민할 필요 없이 경로만 넘겨서 메서드를 호출하면 됩니다.
Directory도 마찬가지입니다:
// 폴더 생성
Directory.CreateDirectory(@"C:\MyProject\Data");
// 모든 텍스트 파일 얻기
string[] textFiles = Directory.GetFiles(@"C:\MyProject", "*.txt");
// 폴더 존재 확인
if (Directory.Exists(@"C:\Temp"))
{
Directory.Delete(@"C:\Temp", recursive: true);
}
3. 실전 예제 비교
예제 1: 간단한 파일 복사
정적 접근:
string sourcePath = "original.txt";
string destinationPath = "copy.txt";
if (File.Exists(sourcePath))
{
File.Copy(sourcePath, destinationPath, overwrite: true);
Console.WriteLine("파일이 복사되었습니다");
}
else
{
Console.WriteLine("원본 파일을 찾을 수 없습니다");
}
인스턴스 접근:
var sourceFile = new FileInfo("original.txt");
var destinationFile = new FileInfo("copy.txt");
if (sourceFile.Exists)
{
sourceFile.CopyTo(destinationFile.FullName, overwrite: true);
Console.WriteLine("파일이 복사되었습니다");
}
else
{
Console.WriteLine("원본 파일을 찾을 수 없습니다");
}
여기서는 정적 접근이 더 간결합니다. 하지만 추가 정보가 필요하면?
예제 2: 크기 체크를 동반한 복사
정적 접근:
string sourcePath = "largefile.zip";
string destinationPath = "backup.zip";
if (File.Exists(sourcePath))
{
var fileInfo = new FileInfo(sourcePath); // 결국 객체를 만들어야 함!
if (fileInfo.Length > 100 * 1024 * 1024) // 100MB 초과
{
Console.WriteLine($"주의: 큰 파일을 복사합니다 ({fileInfo.Length / 1024 / 1024} MB)");
}
File.Copy(sourcePath, destinationPath, overwrite: true);
}
인스턴스 접근:
var sourceFile = new FileInfo("largefile.zip");
if (sourceFile.Exists)
{
if (sourceFile.Length > 100 * 1024 * 1024) // 100MB 초과
{
Console.WriteLine($"주의: 큰 파일을 복사합니다 ({sourceFile.Length / 1024 / 1024} MB)");
}
sourceFile.CopyTo("backup.zip", overwrite: true);
}
여기선 객체 지향 접근이 자연스럽습니다: 파일을 객체로 다루며 그 프로퍼티를 사용합니다.
예제 3: 폴더 내용 분석
정적 접근:
string folderPath = @"C:\Documents";
if (Directory.Exists(folderPath))
{
string[] files = Directory.GetFiles(folderPath);
string[] subdirs = Directory.GetDirectories(folderPath);
Console.WriteLine($"파일 수: {files.Length}, 폴더 수: {subdirs.Length}");
// 크기 정보를 얻으려면 어쨌든 FileInfo 객체가 필요함
long totalSize = 0;
foreach (string filePath in files)
{
var fileInfo = new FileInfo(filePath);
totalSize += fileInfo.Length;
}
Console.WriteLine($"총 크기: {totalSize / 1024} KB");
}
인스턴스 접근:
var folder = new DirectoryInfo(@"C:\Documents");
if (folder.Exists)
{
var files = folder.GetFiles();
var subdirs = folder.GetDirectories();
Console.WriteLine($"파일 수: {files.Length}, 폴더 수: {subdirs.Length}");
long totalSize = files.Sum(f => f.Length); // 정보가 이미 있음!
Console.WriteLine($"총 크기: {totalSize / 1024} KB");
}
여기서는 DirectoryInfo가 유리합니다: FileInfo[] 컬렉션에 메타데이터가 이미 포함되어 있습니다.
4. 접근 방식 선택 기준
다음 경우 정적 클래스(File/Directory)를 사용하세요:
- 작고 일회성인 작업. 파일 존재 확인, 삭제, 내용 읽기 등 빠른 작업에 정적 메서드가 이상적입니다.
// 간단한 설정 읽기
if (File.Exists("config.json"))
{
string config = File.ReadAllText("config.json");
// 설정 처리...
}
- 파일 프로퍼티 정보가 필요하지 않을 때. 크기, 날짜, 속성이 중요하지 않으면 정적 호출이 더 짧고 적합합니다.
- 경로를 다루고 파일을 객체로 다루지 않을 때. 로직이 문자열 경로를 중심으로 돌아가면 정적 메서드가 자연스럽습니다.
다음 경우 인스턴스 클래스(FileInfo/DirectoryInfo)를 사용하세요:
- 하나의 파일/폴더에 대해 많은 정보가 필요할 때. Length, CreationTime, Attributes 같은 프로퍼티를 직접 사용할 수 있습니다.
var logFile = new FileInfo("application.log");
Console.WriteLine($"로그 크기: {logFile.Length / 1024} KB");
Console.WriteLine($"마지막 수정: {logFile.LastWriteTime}");
Console.WriteLine($"위치: {logFile.Directory.FullName}");
- 하나의 파일에 대해 여러 작업을 수행할 때. 객체를 한 번 만들어 여러 작업에 재사용합니다.
var document = new FileInfo("report.docx");
if (document.Exists)
{
var backup = document.CopyTo($"report_backup_{DateTime.Now:yyyyMMdd}.docx");
document.MoveTo("archive/report.docx");
Console.WriteLine($"문서가 보관되었고, 복사본 {backup.Name}이 생성되었습니다");
}
- 파일 컬렉션을 다룰 때. GetFiles()와 GetDirectories()는 완전한 정보를 가진 객체를 반환합니다.
- 객체지향 아키텍처가 필요할 때. 파일/폴더를 객체로 다루면 LINQ나 전달, 저장이 더 쉬워집니다.
5. 성능 및 캐싱 특성
인스턴스 클래스의 캐싱
FileInfo/DirectoryInfo의 핵심 특성 중 하나는 메타데이터 캐싱입니다. 처음 프로퍼티(예: Length 또는 CreationTime)에 접근할 때 .NET은 시스템 호출을 해서 모든 정보를 불러오고 그 객체 내부에 캐시합니다.
var file = new FileInfo("document.txt");
// 첫 접근 - 메타데이터를 불러오기 위한 시스템 호출
long size = file.Length;
// 이후 접근은 캐시 사용 - 매우 빠름
DateTime created = file.CreationTime;
DateTime modified = file.LastWriteTime;
bool readOnly = file.IsReadOnly;
하나의 파일에서 여러 프로퍼티가 필요하면 객체 접근이 효율적입니다: 여러 번의 시스템 호출 대신 한 번으로 해결됩니다.
캐시의 오래됨 문제
단점은 파일이 외부에서 변경되면 정보가 오래된 상태가 될 수 있다는 점입니다. 이때는 Refresh()를 사용하세요:
var file = new FileInfo("data.txt");
Console.WriteLine($"크기: {file.Length}"); // 예: 1000 바이트
// 이 사이에 다른 프로그램이 파일을 변경...
Console.WriteLine($"크기: {file.Length}"); // 여전히 캐시된 1000 바이트!
// 강제 갱신
file.Refresh();
Console.WriteLine($"크기: {file.Length}"); // 이제 최신 크기
정적 호출은 항상 파일 시스템에 직접 접근하므로 이런 문제는 없습니다.
대량 작업
많은 파일에 대해 작업할 때 API 선택이 성능에 영향을 줍니다:
// 정적 접근 — 많은 시스템 호출
string[] files = Directory.GetFiles(@"C:\Photos");
foreach (string filePath in files)
{
var info = new FileInfo(filePath); // 파일별로 시스템 호출
if (info.Length > 10 * 1024 * 1024) // 10MB 초과
{
Console.WriteLine($"큰 사진: {info.Name}");
}
}
// 인스턴스 접근 — 폴더당 한 번의 시스템 호출
var photosDir = new DirectoryInfo(@"C:\Photos");
foreach (var file in photosDir.GetFiles()) // 정보가 한 번에 로드됨
{
if (file.Length > 10 * 1024 * 1024)
{
Console.WriteLine($"큰 사진: {file.Name}");
}
}
6. API와 기능 차이
인스턴스 클래스의 고유 기능
var file = new FileInfo(@"C:\Projects\MyApp\source\Program.cs");
// 폴더 계층 탐색
DirectoryInfo projectDir = file.Directory.Parent; // MyApp
DirectoryInfo sourceDir = file.Directory; // source
// 파일 상세 정보
Console.WriteLine($"확장자: {file.Extension}");
Console.WriteLine($"읽기 전용: {file.IsReadOnly}");
Console.WriteLine($"속성: {file.Attributes}");
// 디렉토리를 객체로 다루기
var dir = new DirectoryInfo(@"C:\Projects");
DirectoryInfo parent = dir.Parent; // C:\
DirectoryInfo root = dir.Root; // C:\
정적 클래스의 고유 기능
// 한 줄로 텍스트 읽기/쓰기
string content = File.ReadAllText("config.txt");
File.WriteAllText("output.txt", "Hello World");
// 문자열 작업
string[] lines = File.ReadAllLines("data.txt");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });
// 파일에 텍스트 추가
File.AppendAllText("log.txt", $"{DateTime.Now}: Application started\n");
// 바이트 작업
byte[] data = File.ReadAllBytes("image.jpg");
File.WriteAllBytes("copy.jpg", data);
7. 실용적인 권장사항
시나리오 1: 백업 유틸리티
크기와 날짜를 확인하며 복사할 때는 인스턴스 클래스가 더 편합니다:
public void BackupDirectory(string sourcePath, string backupPath)
{
var sourceDir = new DirectoryInfo(sourcePath);
var backupDir = new DirectoryInfo(backupPath);
if (!backupDir.Exists)
backupDir.Create();
foreach (var file in sourceDir.GetFiles())
{
var backupFile = new FileInfo(Path.Combine(backupPath, file.Name));
// 파일이 더 최신이거나 존재하지 않을 때만 복사
if (!backupFile.Exists || file.LastWriteTime > backupFile.LastWriteTime)
{
file.CopyTo(backupFile.FullName, overwrite: true);
Console.WriteLine($"복사됨: {file.Name} ({file.Length / 1024} KB)");
}
}
}
시나리오 2: 텍스트 파일 간단 처리
단순한 데이터 로드/저장은 정적 메서드로 충분합니다:
public void SaveUserPreferences(string username, string theme, bool notifications)
{
string configPath = "user.config";
string[] settings = {
$"Username={username}",
$"Theme={theme}",
$"Notifications={notifications}"
};
File.WriteAllLines(configPath, settings);
}
public Dictionary<string, string> LoadUserPreferences()
{
string configPath = "user.config";
var preferences = new Dictionary<string, string>();
if (!File.Exists(configPath))
{
// 기본 설정 생성
SaveUserPreferences("User", "Light", true);
return LoadUserPreferences();
}
string[] lines = File.ReadAllLines(configPath);
foreach (string line in lines)
{
if (line.Contains('='))
{
string[] parts = line.Split('=', 2);
preferences[parts[0]] = parts[1];
}
}
return preferences;
}
시나리오 3: 파일 시스템 분석
디스크 사용량 리포트를 만들 때는 인스턴스 클래스가 최적입니다:
public void AnalyzeDiskUsage(string path)
{
var directory = new DirectoryInfo(path);
var report = new Dictionary<string, long>();
foreach (var file in directory.GetFiles("*", SearchOption.AllDirectories))
{
string extension = file.Extension.ToLower();
if (string.IsNullOrEmpty(extension))
extension = "(확장자 없음)";
if (!report.ContainsKey(extension))
report[extension] = 0;
report[extension] += file.Length;
}
var sortedReport = report.OrderByDescending(kvp => kvp.Value);
foreach (var item in sortedReport.Take(10))
{
Console.WriteLine($"{item.Key}: {item.Value / 1024 / 1024} MB");
}
}
8. 흔한 실수와 함정
실수: 불필요한 방식 혼합
가끔 정적 메서드로 시작한 뒤 메타데이터 때문에 객체를 만들면 불필요한 중복 호출이 생깁니다:
// 비효율적 - 파일 시스템에 두 번 접근
if (File.Exists("document.txt"))
{
var fileInfo = new FileInfo("document.txt"); // 존재 여부 확인 중복
Console.WriteLine($"크기: {fileInfo.Length}");
}
// 객체 접근으로 바로 하는 것이 낫다
var fileInfo = new FileInfo("document.txt");
if (fileInfo.Exists)
{
Console.WriteLine($"크기: {fileInfo.Length}");
}
실수: 캐시 갱신 무시
외부에서 변경될 수 있는 파일을 장시간 다룰 때는 Refresh()로 캐시를 갱신하세요:
var logFile = new FileInfo("application.log");
while (true)
{
logFile.Refresh(); // 파일 정보 갱신
if (logFile.Length > 100 * 1024 * 1024) // 100MB
{
// 로그 보관
logFile.MoveTo($"logs/archived_{DateTime.Now:yyyyMMdd_HHmmss}.log");
break;
}
Thread.Sleep(60000); // 1분마다 확인
}
실수: 대량 작업에 대한 부적절한 선택
수천 개 파일을 처리할 때는 API 선택이 성능을 좌우합니다:
// 느림 - 파일 시스템에 작은 호출이 많음
string[] allFiles = Directory.GetFiles(@"C:\BigFolder", "*", SearchOption.AllDirectories);
var largeFiles = allFiles.Where(path => new FileInfo(path).Length > 1024 * 1024).ToList();
// 빠름 - 이미 준비된 정보를 사용
var folder = new DirectoryInfo(@"C:\BigFolder");
var largeFiles = folder.GetFiles("*", SearchOption.AllDirectories)
.Where(file => file.Length > 1024 * 1024)
.ToList();
GO TO FULL VERSION