C# 6.0 완벽 가이드/ 스트림과 입출력

스트림 구조

  • .NET 스트림 구조는 배경 저장소(backing store), 장식자(decorator), 적응자(adapter)라는 세 가지 개념으로 구성된다. 아래 그림에 이들이 나와 있다.

  • 배경 저장소는 입출력 연산이 실제로 효과를 발휘하는 종점(endpoint)이다. 이를테면 파일이나 네트워크 연결이 배경 저장소에 해당한다. 좀 더 정확히 말하면 배경 저장소는 다음 둘 중 하나 또는 둘 다이다.
    • 바이트들을 차례로(순차적) 읽을 수 있는 원본(source)
    • 바이트들을 차례로 기록할 수 있는 대상(destination)
  • 그런데 배경 저장소가 쓸모가 있으려면 프로그래머가 접근할 수 있어야 한다. 그런 용도로 쓰이는 표준 .NET 클래스가 바로 Stream이다.
    • 이 클래스에는 읽기, 쓰기, 위치 지정을 위한 일단의 메서드를 제공한다. 모든 지원 자료가 메모리에 들어 있는 배열과는 달리, 스트림은 자료를 직렬로(serially) 다룬다. 스트림에서는 한 번에 1바이트씩 또는 관리 가능한 크기의 블록 하나씩만 읽거나 쓸 수 있다.
    • 따라서 배경 저장소가 아무리 커도 스트림을 사용하는데는 아주 적은 양의 메모리만 필요하다.
  • 스트림은 크게 두 종류로 나뉜다.
    • 배경 저장소 스트림
      • 배경 저장소의 구체적인 종류에 맞게 특화된 스트림이다. 이를테면 FileStream이나 NetworkStream이 이 종류에 해당한다.
    • 장식자 스트림
      • 장식자 스트림은 다른 스트림의 자료를 적절히 변환하는 기능을 제공한다. 이를테면 DeflateStream나 CryptoStream이 장식자 스트림이다.
  • 장식자 스트림에는 다음과 같은 구조적인 장점들이 있다.
    • 압축이나 암호화 같은 기능을 배경 저장소 스트림마다 따로 구현할 필요가 없다.
    • 스트림의 자료를 변환(‘장식’)하기 위해 스트림의 인터페이스를 변경할 필요가 없다.
    • 장식자를 실행시점에서 스트림에 연결할 수 있다.
    • 여러 장식자를 사슬처럼 이을 수 있다(이를테면 압축한 후 암호화하는 등)
  • 배경 저장소 스트림과 장식자 스트림은 바이트만 다룬다. 이것이 유연하고 효율적인 방식이긴 하지만, 응용 프로그램은 텍스트나 XML 같은 좀 더 높은 수준의 자료를 다루는 경우가 많다. 이런 간극을 메우는 것이 적응자(adapter)이다.
    • 적응자는 특정 서식에 대응되는 형식으로 특화된 메서드들ㅇ르 가진 클래스로 스트림을 감싼다. 예컨대 텍스트 읽기 적응자는 ReadLine이라는 메서드를 제공하고 XML 쓰기 적응자는 WriteAttributes라는 메서드를 제공한다.
  • 장식자처럼 적응자도 스트림을 감싸는 래퍼(wrapper) 클래스이다. 그러나 장식자와는 달리, 적응자 자체는 스트림이 아니다. 일반적으로 적응자는 바이트 지향적 메서드들을 완전히 감춘다.
  • 정리하자면 배경 저장소 스트림은 미가공 자료(raw data)를 제공한다.
    • 장식자 스트림은 암호화 같은 변환을 수행하되, 역시 바이트 수준에서 작동한다.
    • 적응자는 문자열이나 XML 같은 고수준 형식을 다루기 위한 형식있는 메서드들을 제공한다.
    • 이들을 하나의 사슬로 이으려면 그냥 한 객체를 다른 객체의 생성자에 넣으면 된다.

스트림 사용

  • 추상 Steam 클래스는 모든 스트림 클래스의 기반 클래스이다. Stream은 읽기(reading), 쓰기(writing), 탐색(seeking)이라는 세 가지 근본 연산을 위한 메서드들과 속성들을 정의하며 스트림 닫기와 배출, 시간 만료 설정 같은 관리 작업을 위한 메서드들과 속성들도 정의한다.
범주 멤버
읽기 public abstract bool CanRead { get; }
public abstract int Read(byte[] buffer, int offset, int count)
public virtual int ReadByte();
쓰기 public abstract bool CanWrite { get; }
public abstract void Write(byte[] buffer, int offset, int count)
public virtual int WriteByte(byte value);
탐색 public abstract bool CanSeek { get; }
public abstract long Position { get; set; }
public abstract void SetLength(long value)
public abstract void Length { get; }
public abstract long Seek (long offset, SeekOrigin origin);
닫기/배출 public virtual void Close();
public void Dispose();
public abstract void Flush();
시간만료 public virtual bool CanTimeout { get; }
public virtual int ReadTimeout { get; set; }
public virtual int WriteTimeout { get; set; }
기타 public static readonly Stream Null; // ‘널’ 스트림
public static Stream Synchronized (Stream stream);

 

  • .NET Framework 4.5부터는 Read/Write 메서드들의 비동기 버전들도 생겼다. 비동기 메서드들은 모두 Task를 돌려주며, 선택적 인수로 취소 토큰을 받는다.
  • 다음은 파일 스트림을 이용해서 파일을 읽고, 쓰고, 탐색하는 예이다.
using System;
using System.IO;

class Program
{
  static void Main()
  {
    using (Stream s = new FileStream("test.txt", FileMode.Create))
    {
      Console.WriteLine(s.CanRead);  // true
      Console.WriteLine(s.CanWrite);  // true
      Console.WriteLine(s.CanSeek);  // true

      s.WriteByte(101);
      s.WriteByte(102);
      byte[] block = { 1, 2, 3, 4, 5, };
      s.Write(block, 0, block.Length);  // 5바이트 블록을 기록한다.

      Console.WriteLine(s.Length);  // 7
      Console.WriteLine(s.Position);  // 7
      s.Position = 0;  // 시작 위치로 돌아간다.

      Console.WriteLine(s.ReadByte());  // 101
      Console.WriteLine(s.ReadByte());  // 102

      // 스트림을 읽어서 block 배열을 다시 채운다.
      Console.WriteLine(s.Read(block, 0, block.Length));  // 5

      // 위의 Read 호출이 5를 돌려주어다고 가정하면 현재 위치느느 파일의 끝이므로 다음 Read는 0을 돌려준다.
      Console.WriteLine(s.Read(block, 0, block.Length));  // 0
    }
  }
}
  • 비동기 읽기/쓰기는 그냥 Read/Write 대신 ReadAsync/WriteAsync를 호출하고 해당 호출문에 await를 적용하며녀 된다.
async static void AsyncDemo()
{
  using (Stream s = new FileStream("test.txt", FileMode.Create))
  {
      byte[] block = { 1, 2, 3, 4, 5, };
      await s.WriteAsync(block, 0, block.Length);  // 비동기 쓰기

      s.Position = 0;  // 시작 위치로 돌아간다.2

      // 스트림을 읽어서 block 배열을 다시 채운다.
      Console.WriteLine(await s.ReadAsync(block, 0, block.Length));  // 5
  }
}
  • 잠재적으로 느린 스트림(특히 네트워크 스트림)을 다루는 응용 프로그램을 작성할 때 비동기 메서드들을 이용하면 스레드를 점유하지 않고도 응용 프로그램의 반응성과 규모가변성을 높일 수 있다.
    • 간결함을 위해 이번 장의 예제들은 대부분 동기적 메서드들을 사용했지만 네트워크 입출력이 관여하는 대부분의 시나리오에서는 비동기 Read/Write 연산이 더 나은 선택임을 기억하기 바란다.

스트림 읽기와 쓰기

  • 하나의 스트림은 읽기와 쓰기 중 하나만 지원할 수도 있고 둘 다 지원할 수도 있다. CanWrite 속성이 false인 스트림은 읽기 전용이고, CanRead가 false면 쓰기 전용이다.
  • Read 메서드는 스트림에서 한 블록의 자료를 읽어서 바이트 배열에 넣는다. 이 메서드는 읽은(수신한) 바이트 개수를 돌려주는데, 그 개수는 항상 count 인수와 같거나 더 작다.
    • count 보다 작다는 것은 스트림의 끝에 도달했거나 스트림이 count 인수로 지정한 것보다 더 작은 단위로 자료를 제공한다는 뜻이다(네트워크 스트림에서는 그런 경우가 많다.)
    • 어떤 경우든 배열에 있는 여분의 바이트들 즉 요청한 개수와 실제 개수의 차이만큼의 바이트들은 덮어 쓰이지 않는다(따라서 이전 값을 유지한다.)
  • Read 메서드에서 스트림의 끝에 도달했음을 확신할 수 있는 유일한 기준은 Read가 0을 돌려주는 것 뿐이다. 따라서 만일 스트림의 전체 크기가 1,000 바이트라 할 때 다음 코드는 1,000 바이트 전체를 메모리에 읽어 들이지 못할 수도 있다.
    • 이 Read 호출은 1바이트에서 1,000바이트 사이의 임의의 크기를 읽을 수 있으며, 스트림의 여분의 바이트들은 읽히지 않은 상태로 남는다.
// s가 스트림이라고 가정
byte[] data = new byte[1000];
s.Read(data, 0, data.Length);
  • 다음은 1,000 바이트 스트림을 모두 읽는 올바른 방법을 보여주는 예이다.
byte[] data = new byte[1000];

// 스트림의 크기가 1000 미만이라고 해도 결국에는 bytesRead가 1000에 도달한다.
int bytesRead = 0;
int chunkSize = 1;

while(bytesRead < data.Length && chunkSize > 0)
  bytesRead += 
    chunkSize = s.Read(data, bytesRead, data.Length - bytesRead);
  • 다행히 BinaryReader 클래스를 이용하면 같은 결과를 좀 더 간단하게 얻을 수 있다.
    • 스트림 크기(길이)가 1000 바이트 미만인 경우 반환된 바이트 배열은 실제 스트림 크기를 반영한다. 스트림 탐색 가능이면 1000 대신 (int)s.Length를 사용해서 스트림의 내용 전체를 읽을 수 있다.
byte[] data = new BinaryReader(s).ReadBytes(1000);
  • 더 간단한 메서드로 ReadByte가 있다. 이 메서드는 바이트 하나를 읽어서 돌려주며, 만일 스트림의 끝에 도달했으면 -1을 돌려준다.
    • ReadByte의 반환 형식은 byte가 아니라 int인데, 이는  byte 형식으로는 -1을 표현할 수 없기 때문이다.
  • Write/WriteByte 메서드는 자료를 스트림에 기록(전송)한다. 지정된 개수만큼의 바이트들을 보낼 수 없으면 예외를 던진다.
  • Read 메서드와 Write 메서드의 둘째 매개변수 offset은 buffer 배열 안에서 읽기/쓰기가 시작되는 위치(색인)을 뜻한다. 스트림 안의 위치가 아님을 주의해야 한다.

탐색

  • CanSeek 속성이 true인 스트림은 탐색이 가능하다. 탐색 가능 스트림(파일 스트림 등)에서는 Length 속성을 이용해서 스트림의 길이를 조회할 수 있으며, SetLength 메서드로 길이를 변경할 수도 있다. 또한 언제라도 Position 속성르 이용해서 읽기, 쓰기 위치를 조회 또는 변경할 수 있다.
    • Position 속성은 스트림의 시작에 상대적이다. 반면 Seek 메서드를 이용하면 현재 위치나 스트림의 끝을 기준으로 한 위치를 지정할 수 있다.
  • FileStream의 Position 변경에는 일반적으로 수 마이크로초가 걸린다. 이를 루프에서 수백만 버너 수행해야 한다면 FileStream보다 MemoryMappedFile 클래스가 나은 선택일 수 있다.
  • 탐색 불가 스트림(암호화 장식자 스트림 등)에서 스트림의 길이를 알아내는 유일한 방법은 스트림을 끝까지 읽는 것이다. 더 나아가서 이전 위치를 읽으려면 스트림을 닫고 새로 시작해야 한다.

스트림 닫기와 배출

  • 스트림을 다 사용한 다음에는 반드시 처분(disposal)해 주어야 한다. 그래야 파일이나 소켓 핸들 같은 바탕 자원들이 해제된다. 이를 보장하는 간단한 방법은 스트림을 using 블록 안에서 인스턴스화 하는 것이다. ㅇ리반적으로 스트림들은 다음과 같은 표준적인 처분 의미론을 따른다.
    • Dispose와 Close가 같은 기능을 수행한다.
    • 스트림을 여러 번 처분하거나 닫아도 오류가 발생하지 않는다.
  • 장식자 스트림을 닫으면 장식자와 배경 저장소 스트림이 모두 닫힌다. 장식자들의 사슬에서 가장 바깥쪽 장식자(사슬의 시작에 있는)을 닫으면 사슬의 모든 장식자가 닫힌다.
  • 어떤 스트림은 왕복 통신 횟수를 줄임으로써 성능을 향상하기 위해 배경 저장소와 주고받는 자료를 일시적으로 내부 버퍼를 담아둔다(파일 스트림이 좋은 예이다). 이는 스트림에 자료를 기록해도 그 즉시 배경 저장소가 갱신되지는 않을 수도 있음을 뜻한다. 버퍼가 다 차야 실제로 배경 저장소에 자료가 기록된다.
    • Flush 메서드를 호출하면 내부 버퍼의 내용이 즉시 배경 저장소에 기록(배출) 된다. 스트림을 닫으면 Flush가 자동으로 호출되므로 굳이 다음처럼 할 필요는 없다.
s.Flush(); s.Close();

시간 만료

  • 읽기나 쓰기 연산의 시간 만료를 지원하는 스트림은 CanTimeout 속성이 true이다. 네트워크 스트림은 시간 만료를 지원하지만 파일 스트림과 메모리 스트림은 시간 만료를 지원하지 않는다.
    • 시간 만료를 지원하는 스트림에서 읽기 또는 쓰기 만료시간을 지정하려면 ReadTimeout 속성이나 WriteTimeout 속성에 밀리초 단위의 시간을 설정하면 된다. 0은 시간 만료가 없다는 뜻이다.
    • 지정된 시간이 만료된 경우 Read, Write 메서드들은 예외를 던짐으로써 그 사실을 알린다.

스레드 안전성

  • 애초에 스트림은 스레드에 안전하지 않다. 이는 하나의 규칙이다. 따라서 두 스레드가 같은 스트림을 동시에 읽거나 쓰면 오류가 발생할 여지가 있다. 이에 대한 해결책으로 Stream 클래스는 Synchronized라는 정적 메서드를 제공한다.
    • 이 메서드는 임의의 형식의 스트림을 받아서 스레드에 안전한 래퍼를 돌려준다. 그 래퍼는 각각의 읽기, 쓰기, 탐색 연산에 대해 독점 자물쇠를 걸어서 그러한 연산을 오직 한 번에 한 스레드만 수행할 수 있게 한다. 이를 이용하면 여러 스레드가 같은 스트림에 자료를 동시에 추가할 수 있다.
    • 그러나 그 외의 활동을 위해서는 추가적인 보호 장치가 필요할 수 있다. 예컨대 여러 스레드가 동시에 스트림을 읽어야 한다면, 추가적인 잠금을 통해서 각 스레드가 스트림의 원하는 부분에 접근할 수 있게 해야 한다.

배경 저장소 스트림

  • .NET Framework가 제공하는 주요 배경 저장소 스트림들이 아래 그림에 나와 있다. 그림에 나온 것들 외에 Stream의 정적 Null 필드로 제공되는 ‘널 스트림’도 있다.

FileStream

  • Windows 스토어 앱에서는 FileStream을 사용할 수 없다. 대신 Windows.Storage에 있는 WinRT 형식들을 사용해야 한다.

FileStream 객체 생성

  • FileStream을 인스턴스화하는 가장 간단한 방법은 File 클래스에 있는 다음과 같은 정적 퍼사드(facade) 메서드 중 하나를 사용하는 것이다.
FileStream fs1 = File.OpenRead("readme.bin");  // 읽기 전용
FileStream fs2 = File.OpenWrite(@"c:\temp\writeme.tmp");  // 쓰기 전용
FileStream fs3 = File.OpenCreate(@"c\temp\writeme.tmp");  // 읽기/쓰기
  • OpenWrite와 Create 차이는 파일이 이미 존재할 때 나타난다.
    • Create는 기존 파일의 내용을 폐기해서 파일의 길이를 0으로 만들지만(‘절단’)
    • OpenWrite는 기존 내용을 보존하고 스트림 접근 위치를 시작 위치(0)로 옮긴다. 그 상태에서 기존 파일이 있던 것보다 더 적은 개수의 바이튿을을 기록하면 파일은 기존 내용과 새 내용이 섞여 있는 상태가 된다.
  • FileStream 인스턴스를 직접 생성할 수도 있다. FileStream은 다양한 생성자들을 제공한다. 이들을 통해서 파일 이름 대신 저수준 파일 핸들을 지정할 수 있으며, 모든 파일 생성 및 접근 모드를 선택할 수 있고, 공유나 버퍼링, 보안 관련 옵션들도 세밀하게 지정할 수 있다.
    • 다음은 기존 파일을 읽기/쓰기용으로 열되 덮어쓰지는 않도록 하는 예이다.
var fs = new FileStream("readwrite.tmp", FileMode.Open); // 읽기/쓰기

File 클래스의 단축 메서드들

  • 다음은 파일 전체를 한번에 메모리에 읽어 들이는 정적 메서드들이다.
    • File.ReadAllText(문자열을 돌려줌)
    • File.ReadAllLines(문자열 배열을 돌려줌)
    • File.ReadAllBytes(바이트 배열을 돌려줌)
  • 다음은 파일 전체를 한번에 기록하는 메서드들이다.
    • File.WriteAllText
    • File.WriteAllLines
    • File.WriteAllBytes
    • File.AppendAllText(로그 파일에 항목을 추가하는데 아주 적합하다)
  • 또한 File.ReadLines라는 정적 메서드도 있다. 이 메서드는 ReadAllLines와 비슷하되, 지연 평가 방식의 IEnumerable<string>을 돌려준다는 점이 다르다.
    • 파일 전체를 한 번에 메모리에 적재하지 않는다는 점에서 이 메서드가 더 효율적이다.
    • 읽은 자료를 소비하는데는 LINQ가 이상적이다. 다음은 텍스트 파일에서 길이가 80자를 넘는 행의 수를 세는 예이다.
int longLines = File.ReadLines("파일 경로").Count(l => l.Length > 80);

파일 이름 지정

  • 파일 이름은 절대 경로일 수도 있고 현재 디렉터리에 상대적인 경로일 수도 있다. 현재 디렉터리는 정적 Environment.CurrentDirectory 속성으로 조회하거나 변경할 수 있다.
    • 프로그램 시작 시 현재 디렉터리가 반드시 프로그램의 실행 파일이 있는 디렉터리라는 보장은 없다. 따라서 프로그램의 실행 파일과 함께 설치된 자원 파일들을 찾을 때 현재 디렉터리에 의존하는 것은 위함한 일이다.
  • AppDomain.CurrentDomain.BaseDirectory는 응용 프로그램 기준 디렉터리를 돌려준다. 보통의 경우 이 디렉터리는 프로그램의 실행 파일이 담긴 폴더에 해당한다.
    • 이 디렉터리에 상대적인 파일 이름을 지정하려면 다음과 같이 Path.Combine 메서드를 사용하며녀 된다.
string baseFolder = AppDomain.CurrentDomain.BaseDirectory;
string logoPath = Path.Combine(baseFolder, "logo.jpg");

FileMode 지정

  • 파일 이름을 받는 FileStream의 모든 생성자는 FileMode 열거형 인수도 받는다. 아래 그림은 용도에 맞는 FileMode 값을 선택하는 과정을 나타낸 것이다. 같은 결과를 내는 File의 정적 메서드도 제시되어 있다.

  • 숨겨진 파일에 대해 File.Create나 FileMode.Create를 호출하면 예외가 발생한다. 숨겨진 파일을 덮어쓰려면 먼저 삭제한 후 다시 생성해야 한다.
if (File.Exists("hidden.txt")) File.Delete("hidden.txt");
  • 파일 이름과 FileMode만 지정해서 FileStream 인스턴스를 생성하면 한 가지 경우만 제외하고 읽기와 쓰기가 모두 가능한 스트림이 만들어진다.
    • 읽기나 쓰기 중 하나만 가능하게 하려면 FileAccess 인수를 지정하면 된다.
[Flags]
public enum FileAccess { Read = 1, Write = 2, ReadWrite = 3 }
  • 다음은 읽기 전용 스트림을 돌려준다. File.OpenRead를 호출해도 같은 결과가 나온다.
using (var fs = new FileStream("x.bin", FileMode.Open, FileAccess.Read))
   ...
  • 앞서 말한 한 가지 경우만 제외에 해당하는 옵션은 FileMode.Append이다. 이 추가 모드를 지정하면 쓰기 전용 스트림이 된다.
    • 읽기/쓰기용 추가 모드로 파일을 열려면 FileMode.Open이나 FileMode.OpenOrCreate 모드로 파일을 연 후 접근 위치를 스트림의 끝으로 옮겨야 한다.
using (var fs = new FileStream("x.bin", FileMode.Open, FileAccess.Read))
{
  fs.Seek(0, SeekOrigin.End);
   ...
}

FileStream의 고급 기능

  • 다음은 FileStream 생성시 지정할 수 있는 기타 선택적 인수들이다.
    • 현재 프로세스가 파일 스트림을 사용하는 동안 다른 프로세스가 그 스트림에 어떤 모드로 접근할 수 있는지를 결정하는 FileShare 열거형(가능한 값은 None, Read(기본), ReadWrite, Write)
    • 내부 버퍼의 크기(바이트 단위; 현재 기본은 4KB)
    • 비동기 입출력을 운영체제에 맡길 것인지를 나타내는 플래그
    • 새 파일에 부여할 사용자 및 역할(role) 권한들을 서술하는 FileSecurity 객체
    • 운영체제 암호화 요청(Encrypted), 스트림 종료 시 임시 파일 자동 삭제 여부(DeleteOnClose), 최적화 힌트(RandomAccess와 SequentialScan) 등을 지정하는 FileOptions 플래그 열거형, 또한 쓰기 지연 캐싱(write-behind caching)을 비활성화하라고 운영체제에 요청하는 WriteThrough 플래그도 있다. 이는 트랜잭션 파일이나 로그를 위한 것이다.
  • FileShare.ReadWrite를 지정해서 파일을 열면 다른 프로세스나 사용자가 같은 파일을 동시에 읽고 쓸 수 있다.
    • 혼란을 피하려면 스트림에 접근하는 모든 프로세스는 읽기 또는 쓰기 연산을 시작하기 전에 자신이 접근하고자 하는 부분을 다음 메서드들을 이용해서 잠가야 한다.
// 이들은 FileStream 클래스에 정의되어 있다.
public virtual void Lock(long position, long length);
public virtual void Unlock(long position, long length);
  • 만일 파일의 해당 부분이 이미 잠겨 있으면 Lock은 예외를 던진다. Access나 FoxPro 같은 파일 기반 데이터베이스들이 사용하는 것이 바로 이 시스템이다.

MemoryStream

  • MemorySteam은 배열을 배경 저장소로 사용한다. 따라서 스트림의 장점 중 하나인 배경 저장소 전체를 메모리에 담아 둘 필요가 없다는 점이 무의미해진다. 그래도 MemoryStream은 쓸모가 있다. 한 가지 용도는 탐색 불가 스트림에 대한 임의 접근을 가능하게 하는 것이다.
    • 원본 스트림의 크기가 적당하다는 것을 알고 있는 경우 다음처럼 스트림 전체를 MemoryStream으로 복사하고 나면 얼마든지 임의의 위치에 접근할 수 있다.
var ms = new MemoryStream();
sourceStream.CopyTo(ms);
  • ToArray 메서드를 이용해서 MemoryStream을 바이트 배열로 변환할 수도 있다. 비슷한 용도로 GetBuffer 메서드도 있는데, 이 메서드는 배경 저장소의 바탕 배열에 대한 직접적인 참조를 돌려준다는 점에서 더 효율적이다. 단점은 일반적으로 그 배열이 스트림의 실제 길이보다 더 길다는 것이다.
  • MemoryStream은 굳이 닫거나 배출할 필요가 없다. MemoryStream을 닫으면 더 이상 스트림을 읽고 쓸 수 없게 되지만, ToArray를 호출해서 바탕 자료를 얻는 것은 여전히 가능하다. 메모리 스트림에 대한 Flush는 아무 일도 하지 않는다.

PipeStream

  • PipeStream은 .NET Framework 3.5에서 도입되었다. PipeStream은 현재 프로세스가 Windows의 파이프 프로토콜을 이용해서 다른 프로세스와 통신하는 과정을 단순화 해준다. 사용할 수 있는 파이프는 두 종류이다.
    • 익명 파이프
      • 같은 컴퓨터에 있는 부모 프로세스와 자식 프로세스 사이의 단방향 통신에 쓰인다.
    • 명명된 파이프
      • 같은 컴퓨터 또는 Windows 네트워크의 서로 다른 컴퓨터에 있는 임의의 두 프로세스 사이의 양방향 통신이 가능하다.
  • 파이프는 한 컴퓨터 안에서의 프로세스간 통신(interprocess communication, IPC)에 적합하다. 파이프는 네트워크 전송에 의존하지 않으므로 성능이 좋고, 방화벽 관련 문제도 없다.
    • Windows 스토어 앱에서는 파이프를 사용할 수 없다.
  • 파이프는 스트림 기반이므로 쓰기 프로세스가 일련의 바이트들을 전송하는 동안 읽기 프로세스는 전송이 끝나길 기다려야 한다. 파이프 대신 공유 메모리를 프로세스간 통신에 사용할 수도 있다.
  • PipeStream는 추상 클래스이다. .NET Framework는 이를 구현하는 네 가지 구체 클래스를 제공하는데, 둘은 익명 파이프용이고 다른 둘은 명명된 파이프용이다.
    • 익명 파이프
      • AnonymousPipeServerStream과 AnonymousPipeClientStream
    • 명명된 파이프
      • NamedPipeServerStream과 NamedPipeClientStream
  • 파이프는 바이트들(또는 일련의 바이트들로 이루어진 ‘메시지’)만 주고받을 수 있는 저수준 수단이다. WCF와 Remoting API는 좀 더 고수준 메시징 프레임워크를 제공하는데 원한다면 IPC 채널을 통신에 사용할 수도 있다.

명명된 파이프

  • 명명된 파이프에서 두 통신 단위는 같은 이름의 파이프 하나를 이용해서 통신한다. 명명된 파이프 프로토콜은 명확히 구분되는 두 가지 역할을 정의한다. 바로 클라이언트와 서버이다. 클라이언트와 서버 사이의 통신이 벌어지는 과정은 다음과 같다.
    • 서버가 NamePipeServerStream 인스턴스를 생성해서 WaitForConnection을 호출한다.
    • 클라이언트가 NamedPipeClientStream 인스턴스를 생성해서 Connect를 호출한다. (이때 만료시간을 지정할 수도 있다.)
    • 각자 스트림 인스턴스에 자료를 쓰거나 읽음으로써 통신을 진행한다.
  • 다음은 서버에서 바이트 하나(100)를 보낸 후 바이트 하나가 오길 기다렸다가 출력하는 예이다.
using (var s = new NamedPipeServerStream("pipedream"))
{
  s.WaitForConnection();
  s.WriteByte(100);
  Console.WriteLine(s.ReadByte());
}
  • 다음은 그에 대응되는 클라이언트 쪽의 코드이다.
using (var s = new NamedPipeClientStream("pipedream"))
{
  s.Connect();
  Console.WriteLine(s.ReadByte());
  s.WriteByte(200);  // 200이라는 값을 돌려보낸다.
}
  • 명명된 파이프 스트림은 기본적으로 양방향이다. 즉, 두 당사자 모두 자신의 스트림을 읽거나 쓸 수 있다. 따라서 두 당사자가 동시에 뭔가를 보내거나 받는 혼란을 피하려면 클라이언트와 서버가 자료를 주고 받는 절차를 합의해서 지켜야 한다.
  • 또한 각 전송의 길이도 합의해야 한다. 앞의 예제는 한 바이트를 읽고 쓸 뿐이었지만 여러 바이트로 이루어진 메시지를 주고 받으려면 좀 더 정교한 방식이 필요한데, 이를 돕기 위해 파이프 스트림은 메시지 전송 모드라는 것을 지원한다.
    • 이 모드를 활성화한 경우, Read를 호출하는 쪽에서는 하나의 메시지를 구성하는 바이트들이 모두 도착했는지를 IsMessageComplete 속성으로 알아낼 수 있다.
  • 그럼 구체적인 예제를 보자. 우선 다음은 메시지 전송 모드가 활성화된 PipeStreamdㅔ서 하나의 메시지 전체를 읽는 보조 메서드이다. 이 메서드가 하는 일은 IsMessageComplete가 true가 될 때까지 바이트들을 거듭 읽는 것일 뿐이다.
static byte[] ReadMessage(PipeStream s)
{
  MemoryStream ms = new MemoryStream();
  byte[] buffer = new byte[0x1000]; // 4kb 블록 단위로 읽어 들인다.

  do { ms.Write(buffer, 0, s.Read(buffer, 0, buffer.Length)); }
  while (!s.IsMessageComplete);

  return ms.ToArray();
}
  • 이를 비동기화하려면 s.Read를 await s.ReadAsync로 대체하면 된다.
  • PipeStream에서 메시지 하나를 완전히 읽었는지를 Read의 반환값이 0인지로 판단해서는 안된다. 다른 대부분의 스트림과는 달리 파이프 스트림과 네트워크 스트림에는 명확한 ‘끝’이 없기 때문이다.
    • 파이프 스트림과 네트워크 스트림은 끝나지 않는다. 이런 스트림들은 메시지 전송이 없는 동안 그냥 ‘말라붙을’ 뿐이다. (가뭄에 개울(stream)이 말라붙듯이)
  • 다음으로 메시지 전송 모드를 켜는 방법을 보자. 서버에서는 스트림 인스턴스를 생성할 때 PipeTransmissionMode.Message를 지정하면 된다.
using (var s = new NamedPipeServerStream("pipedream", PipeDirection.InOut, 1, PipeTransmissionMode.Message))
{
  s.WaitForConnection();

  byte[] msg = Encoding.UTF8.GetBytes("Hello");
  s.WriteByte(msg, 0, msg.Length);

  Console.WriteLine(Encoding.UTF8.GetString(ReadMessage(s)));
}
  • 클라이언트에서는 Connect 호출 후 ReadMode를 설정하면 된다.
using (var s = new NamedPipeClientStream("pipedream"))
{
  s.Connect();
  s.ReadMode = PipeTransmissionMode.Message;

  Console.WriteLine(Encoding.UTF8.GetString(s)));

  byte[] msg = Encoding.UTF8.GetBytes("Hello right back!");
  s.Write(msg, 0, msg.Length); 
}

익명 파이프

  • 익명 파이프는 부모 프로세스와 자식 프로세스 사이의 양방향 통신 기능을 제공한다. 익명 파이프를 사용할 때는 시스템 전역에 알려진 이름이 아니라 전용(private) 핸들을 통해서 통신 채널을 확립한다.
  • 명명된 파이프처럼 익명 파이프에서도 두 당사자는 클라이언트와 서버의 역할을 나누어 맡는다. 그러나 통신 체계는 조금 다르다. 익명 파이프를 이용한 통신 과정은 다음과 같다.
    1. 부모 프로세스는 PipeDirection 인수를 In 또는 Out으로 지정해서 AnonymousPipeServerStream을 인스턴스화 한다.
    2. 부모 프로세스는 GetClientHandleAsString을 호출해서 파이프 식별자를 얻고 그것을 클라이언트에 넘겨준다.(보통은 자식 프로세스를 띄울 때 이 식별자를 명령줄 인수로 지정한다)
    3. 자식 프로세스는 부모와는 반대 방향의 PipeDirection 인수를 지정해서 AnonymousPipeClientStream을 인스턴스화 한다.
    4. 부모는 DisposeLocalCopyOfClientHandle을 호출해서 지역 핸들(단계 2에서 얻은)을 해제한다.
    5. 부모와 자식이 파이프에 자료를 쓰거나 읽으면서 통신한다.
  • 익명 파이프는 단방향이므로 양방향 통신을 위해서는 부모가 파이프를 두 개 생성해야 한다. 다음은 부모 프로세스가 자식 프로세스에게 바이트 하나를 보내고 그에 대한 응답으로 자식 프로세스가 보낸 바이트를 읽는 예이다.
string clientExe = @"d:\PipeDemo\ClientDemo.exe";

HandleInheritability inherit = HandleInheritability.Inheritable;

using (var tx = new AnonymousPipeServerStream (PipeDirection.Out, inherit))
using (var rx = new AnonymoutPipeServerStream (PipeDirection.In, inherit))
{
  string txID = tx.GetClientHandleAsString();
  string rxID = rx.GetClientHandleAsString();

  var startInfo = new ProcessStartInfo(clientExe, txID + " " + rxID);
  startInfo.UseShellExcute = false;  // 자식 프로세스에 필요한 설정
  Process p = Process.Start(startInfo);

  tx.DisposeLocalCopyOfClientHandle();  // 비관리 핸들 자원들을 해제한다.
  rx.DisposeLocalCopyOfClientHandle();

  tx.WriteByte(100);
  Console.WriteLine("Server received: " + rx.ReadByte());

  p.WaitForExit();
}
  • 다음은 자식 프로세스 쪽의 코드 이다.
string rxID = args[0];
string txID = args[1];

using (var rx = new AnonymousPipeClientStream (PipeDirection.In, rxID))
using (var tx = new AnonymousPipeClientStream (PipeDirection.Out, txID))
{
  Console.WriteLine("Client received: " + rx.ReadByte());
  tx.WriteByte(200);
}
  • 명명된 파이프에서처럼 부모 프로세스와 자식 프로세스는 자료를 주고받는 절차와 각 전송의 크기를 합의해야 한다. 안타깝게도 익명 파이프는 메시지 전송 모드를 지원하지 않으므로 메시지의 길이에 관한 규약을 직접 만들어서 구현해야 한다.
    • 한 가지 해법은 메시지를 전송할 때 먼저 메시지의 길이를 4바이트에 담아서 보내는 것이다. 4바이트 배열과 정수 사이의 변환은 BitConverter 클래스의 메서드들을 이용하면 된다.

BufferedStream

  • BufferedStream은 기존의 스트림을 감싸서 버퍼링 기능을 부여하는 장식자이다. 이 형식은 핵심 .NET Framework의 여러 장식자 스트림 형식(아래 그림) 중 하나 이다.

  • 버퍼링은 배경 저장소와의 왕복 통신 횟수를 줄여서 성능을 향상한다. 다음은 FileStream을 20KB짜리 버퍼를 가진 BufferedStream으로 감싸는 예이다.
// 파일에 100KB의 자료를 기록한다.
File.WriteAllBytes("myFile.bin", new byte[100000]);

using (FileStream fs = File.OpenRead("myFile.bin"))
using (BufferedStream bs = new BufferedStream(fs, 20000)) // 20kb의 버퍼
{
  bs.ReadByte();
  Console.WriteLine(fs.Position);  // 20000
}
  • 이 예제에서 장식된 스트림에서 바이트 하나만 읽어도 원본 스트림에서는 20,000 바이트가 읽힌다. 이는 BufferedStream의 미리 읽기(read-ahead) 버퍼링 때문이다. 이후 ReadByte를 19,999회 더 호출해야 FileStream에 대한 읽기가 다시 실행된다.
    • 사실 FileStream 자체에 이미 버퍼링 기능이 있으므로, FileStream을 BufferedStream으로 감싸는 것은 그리 쓸모가 없다. 이 조합은 FileStream의 내부 버퍼보다 더 큰 버퍼를 적용하려는 경우에나 의미가 있다.
  • BufferedStream을 닫으면 바탕 배경 저장소 스트림도 자동으로 닫힌다.

스트림 적응자

  • Stream은 바이트들만 다룬다. 문자열이나 정수, XML 요소 같은 자료를 읽고 쓰려면 스트림에 적응자(adapter)를 끼워 넣어야 한다. 다음은 .NET Framework가 제공하는 스트림 적응자들이다.
    • 텍스트 적응자(문자열과 문자 자료를 위한)
      • TextReader/ TextWriter
      • StreamReader/ StreaWriter
      • StringReader/ StringWriter
    • 이진 적응자(int, bool, string, float 같은 기본 형식들을 위한)
      • BinaryReader/ BinaryWriter
    • XML 적응자
      • XmlReader/ XmlWriter
  • 이 형식들 사이의 관계가 아래 그림에 나와 있다.

텍스트 적응자

  • TextReader와 TextWriter는 문자 또는 문자열 형식의 자료만을 다루는 적응자들을 위한 추상 기반 클래스이다. .NET Framework는 이 두 형식에 대해 각각 두 종류의 구현 클래스를 제공한다.
    • StreamReader/ StreamWriter
      • Stream을 미가공 자료 저장소로 사용해서, 스트림의 바이트들을 문자들 또는 문자열로 변환한다.
    • StringReader/ StringWriter
      • 메모리 내부 문자열을 이용해서 TextReader/ TextWriter를 구현한다.
  • 아래 표에 TextReader의 멤버들이 범주별로 나열되어 있다.
    • Peek는 스트림의 다음 문자를 돌려주되, 스트림 내부의 접근 위치는 변경하지 않는다. Peek와 매개변수가 없는 Read는 스트림의 끝에 도달했을 때 -1을 돌려주며, 그 외의 경우에는 정수를 돌려준다. 그 정수를 char로 직접 캐스팅하면 해당 문자가 된다.
    • char[] 배열을 받도록 중복적재된 Read는 ReadBlock 메서드와 기능이 같다.
    • ReadLine은 새 줄(줄 바꿈) 표시를 만날 떄까지 문자들을 읽어서 새 줄 표시를 제외한 문자들로 일워진 문자열을 돌려준다. 이메서드는 CR(문자부호 13)이나 LF(문자부호 10) 또는 CR+LF 쌍을 새 줄 표시로 인식한다.
범주 멤버
문자 하나 읽기 public virtual int Peek();  // 반환값을 char로 캐스팅해야 함
public virtual int Read(); // 반환값을 char로 캐스팅해야 함
여러 문자 읽기 public virtual int Read(char[] buffer, int index, int count);
public virtual int ReadBlock(char[] buffer, int index, int count);
public virtual string ReadLine();
public virtual string ReadToEnd();
닫기 public virtual void Close();
public void Dispose();  // Close와 같음
기타 public static readonly TextReader Null;
public static TextReader Synchronized(TextReader reader);

 

  • Windows의 기본 새 줄 표시인 CR+LF는 기계식 타자기의 줄 바꿈 과정과 비슷하다. CR(carriage return)은 타자기의 나르개(carriage)를 왼쪽으로 되돌리는(return) 것에 해당하고, LF(line feed)는 타자 용지를 한 행(line)만큼 위로 공급(feed)하는 것에 해당한다. 이에 해당하는 C# 문자열은 “\r\n”이다
  • TextWriter에도 이와 비슷한 메서드들이 있다.(아래 표)
    • Write 메서드와 WriteLine 메서드에는 또한 모든 기본 형식과 object 형식을 위한 중복적재 버전들도 있다. 이 메서드들은 주어진 인수에 대해 그냥 ToString 메서드를 호출한다.(메서드 호출 시 또는 TextWriter 생성 시 IFormatProvider를 지정했다면 그것도 적용한다)
범주 멤버
문자 하나 쓰기 public virtual void Write (char value);
여러 문자 쓰기 public virtual void Write (string value);
public virtual void Write (char[] buffer, int index, int count);
public virtual void Write (string format, params object[] arg);
public virtual void WriteLine (string value);
닫기와 배출 public virtual void Close();
public void Dispose();  // Close와 같음
public virtual void Flush();
서식화와 부호화 public virtual IFormatProvider FormatProvider { get; }
public virtual string NewLine { get; set; }
public abstract Encoding Encoding { get; }
기타 public static readonly TextWriter Null;
public static TextWriter Synchronized (TextWriter writer);

 

  • WriteLine은 그냥 주어진 텍스트에 CR+LF를 추가해서 출력하기만 한다. 다른 새 줄 표시를 사용하고 싶다면 NewLine 속성을 변경하면 된다.(이는 Unix 파일 형식들과의 상호운용성에 유용하다)
  • Stream처럼 TextReader와 TextWriter는 작업 기반 비동기 방식으리 읽기/쓰기 메서드들도 제공한다.

StreamReader와 StreamWriter

  • 다음은 StreamWriter를 이용해서 파일에 두 행의 텍스트를 기록하고 StreamReader를 이용해서 그것들을 다시 읽어 들이는 예이다.
using (FileStream fs = File.Create("text.txt"))
using (TextWriter writer = new StreamWriter(fs))
{
  writer.WriteLine("Line1");
  writer.WriteLine("Line2");
}

using (FileStream fs = File.OpenRead("text.txt"))
using (TextReader reader = new StreamReader(fs))
{
  Console.WriteLine(reader.ReadLine());  // Line1;
  Console.WriteLine(reader.ReadLine());  // Line2;
}
  • 텍스트 적응자를 파일 입출력에 사용하는 경우가 많다. 그래서 File 클래스는 적절한 텍스트 적응자를 돌려주는 편리한 정적 메서드들을 제공한다. CreateText와 AppendText, OpenText가 바로 그것이다.
using (TextWriter writer = File.CreateText("test.txt"))
{
  writer.WriteLine("Line1");
  writer.WriteLine("Line2");
}

using (TextWriter writer = File.AppendText("test.txt"))
{
  writer.WriteLine("Line3");
}

using (TextReader reader = File.OpenText("test.txt"))
{
  while (reader.Peek() > -1)
    Console.WriteLine(reader.ReadLine());
}
  • 이 예제는 파일의 끝을 판정하는 방법도 보여준다(reader.Peek() 부분). 이 방법 대신 reader.ReadLine이 null을 돌려줄 때까지 읽을 수도 있다.
  • 정수 같은 다른 형식들은 읽거나 쓰는 것도 가능하나, TextWriter는 해당 형식에 대해 ToString을 호출하므로 그것을 다시 읽을 때는 문자열을 해당 형식으로 파싱해야 한다.
using (TextWriter writer = File.CreateText("data.txt"))
{
  writer.WriteLine(123);  // "123"을 기록한다.
  writer.WriteLine(true);  // 단어 "True"를 기록한다.
}

using (TextReader reader = File.OpenText("data.txt"))
{
  int myInt = int.Parse(r.ReadLine());
  bool yes = bool.Parse(r.ReadLine());
}

문자 부호화

  • TextReader와 TextWriter 자체는 그냥 추상 클래스일 뿐이므로 특정 스트림이나 배경 저장소와 직접 연결되지는 않는다. 그러나 StreamREader 형식이나 StreamWriter 형식 객체는 구체적인 바이트 지향적 스트림과 실제로 연결되므로 문자와 바이트 사이의 변환을 수행해야 한다.
    • 이를 위해 이 형식들은 System.Text 이름공간의 Encoding 클래스를 이용한다. StreamReader나 StreamWriter를 생성할 때 문자 부호화 방식을 지정할 수 있으며, 부호화 방식을 지정하지 않으면 기본 부호화인 UTF-8이 쓰인다.
  • 부호화 방식을 명시적으로 지정하면 기본적으로 StreamWriter는 부호화 방식을 나타내는 접두어를 스트림의 시작에 기록한다. 보통은 그런 접두어가 없는 것이 바람직하다. 접두어를 기록하지 않게 하려면 부호화 객체를 다음과 같이 생성하면 된다.
    • 둘째 인수는 주어진 부호화 방식으로 직접 변환할 수 없는 바이트들을 만났을 때 StreamWriter(또는 StreamReader)가 예외를 던지게 한다. 부호화 방식을 아예 지정하지 않았을 때에도 이 옵션이 적용된다.
var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier:false, throwOnInvalidBytes:true);
  • 가장 간단한 부호화 방식은 ASCII이다. ASCII 부호화에서는 각 문자가 한 바이트로 표현되기 때문이다.
    • ASCII 부호화는 유니코드 문자 집합의 처음 127자를 각각 하나의 바이트에 대응시킨다. 여기에는 북미 스타일의 키보드에 보이는 모든 문자가 포함된다. 그 외의 대부분의 문자, 특히 특수 기호들과 비영어 문자들은 ASCII로 표현할 수 없다. 그런 문자들은 □자로 변환된다.
    • 스트림의 기본 부호화 방식인 UTF-8은 할당된 모든 유니코드 문자를 적절한 부호에 대응시키는데, 그 대응 방식이 다소 복잡하다.
    • ASCII와의 호환성을 위해, 유니코드 문자집합의 처음 127자는 각각 하나의 바이트에 대응된다. 그 나머지 문자들은 가변길이 다중 바이트열로 부호화된다(대부분은 2바이트 또는 3바이트)
    • 다음 예를 생각해 보자.
using (TextWriter writer = File.CreateText("but.txt"))  // 기본 UTF-8 부호화를 사용한다.
{
  writer.WriteLine("but-");  
}

using (TextReader reader = File.OpenText("but.txt"))
{
  for (int b; (b = s.ReadByte()) > -1;)
    Console.WriteLine(b);
}
  • 단어 “but” 다음의 기호는 보통의 하이픈 기호가 아니라 그보다 긴 엠대시 문자로 유니코드 부호는 U+2104이다. 이 코드의 출력을 살펴보자.
98  // b
117  // u
116  // t
226  // 엠 대시 바이트 1  - 이런 다중 바이트열의 모든 바이트가 128보다 큰 값임을 주목할 것
128  // 엠 대시 바이트 2
148  // 엠 대시 바이트 3
13  // <CR>
10  // <LF>
  • 엠 대시처럼 유니코드 문자 집합의 처음 127자에 속하지 않는 문자는 UTF-8에서 하나의 바이트로 표현되지는 않는다(엠 대시는 정확히 3바이트로 표현된다).
    • 서구의 알파벳 체계에서는 자주 쓰이는 대부분의 문자가 1바이트만 소비하므로 UTF-8이 효율적이다. 또한 그냥 127 이상의 바이트를 모두 무시하기만 하면 간단하게 ASCII로 변환(하향)된다.
    • 단점은 스트림 안에서 문자의 위치가 바이트 위치와 일치하지 않으므로 스트림 안에서 위치를 지정하기 어렵다는 점이다. 대안은 UTF-16이다(Encoding에서는 그냥 Unicode라고 칭한다)
    • 다음은 앞의 예제와 같은 문자열을 UTF-16으로 기록하는 예이다.
using (Stream s = File.Create("but.txt"))
using (TextWriter writer = new StreamWriter(s, Encoding.Unicode))
  writer.WriteLine("but-");  

foreach(byte b in File.ReadAllBytes("but.txt"))
    Console.WriteLine(b);
  • 출력은 다음과 같다.
255  // 바이트 순서 표시(BOM) 1
254  // 바이트 순서 표시 2
98  // b 바이트 1
0  // b 바이트 2
117  // u 바이트 1
0  // u 바이트 2
116  // t 바이트 1
0  // t 바이트 2
20  // - 바이트 1
32 // - 바이트 2
13  // <CR> 바이트 1
0  // <CR> 바이트 2
10  // <LF> 바이트 1
0  // <LF> 바이트 2
  • 기술적으로 UTF-16은 문자당 2바이트 또는 4바이트를 사용한다(유니코드에는 1백만 개에 가까운 문자가 할당 또는 예약되어 있기 때문에, 2바이트로는 표현할 수 없는 문자들이 존재한다)
    • 그런데 C#의 char 형식 자체는 너비가 16비트 밖에 되지 않으므로, .NET Framework의 UTF-16 부호화는 항상 .NET char 문자 하나당 정확히 2바이트를 사용한다. 이 덕분에 스트림 안에서 색인을 이용해서 특정 문자에 접근하기가 아주 쉽다.
  • UTF-16은 하나의 문자를 나타내는 바이트쌍이 리틀엔디안(little-endian) 순서(최하위 바이트가 먼저)인지 아니면 빅엔디안(big-endian) 순서(최상위 바이트가 먼저)인지를 나타내는 2바이트 접두어를 사용한다. Windows 기반 시스템에서는 리틀엔디언 순서가 표준이다.

StringReader와  StringWriter

  • StringReader 적응자와 StringWriter 적응자는 스트림을 감싸지 않는다. 이들은 문자열이나 StringBuilder를 바탕 자료원으로 사용한다. 따라서 이들은 바이트 수준의 변환을 수행하지 않는다.
    • 사실 이들로 할 수 있는 모든 일은 문자열/StringBuilder와 색인 변수의 조합으로도 할 수 있다. 이들의 장점은 StreamReader/StreamWriter와 공통의 기반 클래스를 상속한다는 점에서 비롯된다.
    • 예컨대 XML 자료를 담은 문자열 XmlReader로 파싱한다고 하자. XmlReader.Create는 다음 중 하나를 받는다.
      • URI 객체
      • Stream 객체
      • TextReader 객체
    • 그렇다면 String 형식의 문자열을 이 메서드로 파싱하려면 어떻게 해야 할까? 다행히 StringReader는 TextReader의 파생 클래스이므로, 다음과 같이 StringReader 인스턴스를 만들어서 넘겨주면 된다.
XmlReader r = XmlReader.Create(new StringReader(myString));

이진 적응자

  • 이진 적응자 BinaryReader와 BinaryWriter는 bool, byte, char, decimal, float, double, short, int, long, sbyte, ushort, uint, ulong 같은 기본 자료 형식들과 string 형식, 그리고 이들의 배열을 읽고 쓴다.
  • StreamReader나 StreamWriter와 달리 이진 적응자들은 기본 자료 형식들을 매모리에 표현되는 그대로 저장하므로 저장 효율성이 좋다.
    • 예컨대 int 하나를 저장하는데 4바이트, double 하나를 저장하는데 8바이트를 사용한다.
    • 문자열은 텍스트 부호화를 거쳐서 저장하되(StreamReader나 StreamWriter처럼) 제일 앞의 문자열의 길이를 첨부한다. 이 덕분에 특별한 구분 기호 없이도 일련의 문자열들을 읽어 들일 수 있다.
    • 다음과 같은 간단한 형식이 있다고 하자.
public class Person
{
  public string Name;
  public int Age;
  public double Height;
}
  • 다음은 이러한 Person 형식의 객체의 자료를 이진 적응자를 이용해서 스트림에 저장하거나 스트림에서 불러오는 메서드들이다.
public void SaveData(Stream s)
{
  var w = new BinaryWriter (s);
  w.Write(Name);
  w.Write(Age);
  w.Write(Height);
  w.Flush();  // BinaryWriter의 버퍼를 확실히 비운다. BinaryWriter 자체가 닫히거나 처분되지는 않으므로 계속해서 스트림에 자료를 쓸 수 있다.
}

public void LoadData(Stream s)
{
  var r = new BinaryReader(s);
  Name = r.ReadString();
  Age = r.ReadInt32();
  Height = r.ReadDouble();
}
  • BinaryReader는 스트림의 내용을 바이트 배열로 읽어 들이는 메서드도 제공한다. 다음은 탐색 가능 스트림의 내용 전체를 읽어 들이는 예이다.
byte[] data = new BinaryReader(s).ReadBYtes((int)s.Length);
  • 모든 자료가 읽혔는지 점검하는 루프가 필요 없다는 점에서 스트림을 직접 읽는 것보다 이처럼 이진 적응자를 사용하는 것이 더 편리하다.

스트림 적응자 닫기와 처분

  • 다 사용한 스트림 적응자를 정리하는 방법은 네 가지이다.
    1. 적응자만 닫는다.
    2. 적응자를 닫은 후 스트림을 닫는다.
    3. (쓰기 적응자의 경우) 적응자를 배출한 후 스트림을 닫는다.
    4. (읽기 적응자의 경우) 그냥 스트림만 닫는다.
  • 스트림과 마찬가지로 적응자에서는 Close와 Dispose가 동의어이다.
  • 1번과 2번은 같은 의미이다. 어차피 적응자를 닫으면 바탕 스트림도 자동으로 닫히기 때문이다. using 문을 중첩해서 사용하는 것은 2번에 해당한다.
using (FileStream fs = File.Create("test.txt"))
using (TextWriter writer = new StreamWriter(fs))
  writer.WriteLine("Line");
  • 중첩된 using 문에서 객체들의 처분은 안쪽에서 바깥쪽으로 진행되므로, 이 예의 경우 적응자가 먼저 닫히고 스트림이 닫힌다.
    • 더 나아가서 만일 적응자의 생성자에서 예외가 발생해도 스트림은 여전히 닫힌다. 이처럼 중첩된 using 문을 사용하면 뭔가 잘못될 여지가 별로 없다.
  • 쓰기 적응자를 닫거나 배출하기 전에 바탕 스트림을 닫아서는 절대 안 된다. 그러면 적응자의 버퍼에 남아 있는 자료가 소실된다.
  • 3번과 4번이 문제가 되지 않는 이유는, 적응자가 선택적 처분 가능 객체라는 흔치 않은 범주에 속하기 때문이다.
    • 예컨대 적응자를 다 사용한 후 적응자의 바탕 스트림을 더 사용해야 하는 경우에는 적응자를 처분하지 말아야 한다. 다음이 그러한 예이다.
using (FileStream fs = new FileStream("test.txt", FileMode.Create))
{
  StreamWriter writer = new StreamWriter(fs);
  writer.WriteLine("Hello");
  writer.Flush();
  fs.Position = 0;
  Console.WriteLine(fs.ReadByte());
}
  • 이 예제는 파일에 문자열을 기록한 후 스트림을 다시 시작 위치로 되돌려서 첫 바이트를 읽는다. 그런 다음 using 문에 의해 스트림이 닫힌다.
    • 만일 StreamWriter를 처분했다면 바탕 FileStream도 닫히므로 이후의 스트림 읽기가 실패할 것이다.
    • 이후의 읽기가 의도대로 작동하려면 Flush를 호출해서 StreamWriter의 버퍼가 바탕 스트림에 확실히 기록되게 해야 한다는 점도 주목하기 바란다.
  • 스트림 적응자는 선택적 처분 의미론을 사용하며, 그런 만큼 확장된 처분 패턴(종료자에서 Dispose를 호출하는)은 구현하지 않는다. 그래서 다 쓴 적응자가 쓰레기 수거기에 의해 자동으로 처분되지 않는다.
  • .NET Framework 4.5에서는 처분 이후에도 스트림을 계속 열어 두게 하는 새로운 생성자가 StreamReader와 StreamWriter에 추가되었다. 다음은 그 생성자를 이용해서 앞의 예제를 다시 작성한 것이다.
using (FileStream fs = new FileStream("test.txt", FileMode.Create))
{
  using (StreamWriter writer = new StreamWriter(fs, new UTF8Encoding(false, true), 0x400, true);
    writer.WriteLine("Hello");
  fs.Position = 0;
  Console.WriteLine(fs.ReadByte());
  Console.WriteLIne(fs.Length);
}

압축 스트림

  • System.IO.Compression 이름공간에는 두 범용 압축 스트림 DeflateStream과 GZipStream이 있다. 둘 다 Zip 형식에 쓰이는 것과 비슷한 유명한 압축 알고리즘을 사용한다.
    • 둘의 차이는 GZipStream은 스트림의 시작과 끝에 추가적인 정보(오류 검출을 위한 CRC를 포함한다)를 기록한다는 것이다. 또한 GZipStream은 다른 소프트웨어들이 인식하는 표준을 준수한다.
  • 두 스트림 모두 읽기와 쓰기를 허용한다. 단, 다음과 같은 제약이 있다.
    • 압축 시에는 스트림이 쓰기 전용으로 작동한다.
    • 압축을 풀 때는 스트림이 읽기 전용으로 작동한다.
  • DeflateStream과 GZipStream은 생성 시 지정한 스트림의 자료를 압축하거나 푸는 장식자이다. 다음 예제는 FileStream을 배경 저장소로 사용해서 일련의 바이트들을 압축하고 푼다.
using (Stream s = File.Create("compressed.bin"))
using (Stream ds = new DeflateStream(s, CompressionMode.Compress))
{
  for (byte i = 0; i < 100; i++)
    ds.WriteByte(i);
}

using (Stream s = File.OpenRead("compressed.bin"))
using (Stream ds = new DeflateStream(s, CompressionMode.Decompress))
  for (byte i = 0; i < 100; i++)
    Console.WriteLine(ds.ReadByte());
  • (이하 텍스트 압축 내용 생략)

메모리 내 압축

  • 압축을 전적으로 메모리 안에서 수행해야 할 때도 종종 있다. 다음은 이를 위해 MemoryStream을 활용하는 방법을 보여준다.
byte[] data = new byte[1000];

var ms = new MemoryStream();
using (Stream ds = new DeflateStream(ms, CompressionMode.Compress))
  ds.Write(data, 0, data.Length);

byte[] compressed = ms.ToArray();
Console.WriteLine(compressed.Length);  // 11

// 압축을 해제해서 다시 data 배열에 넣는다.
ms = new MemoryStream(compressed);
using (Stream ds = new DeflateStream(ms, COmpressionMode.Decompress))
  for (int i = 0; i < 1000; i += ds.Read(data, i, 1000-i));
  • DeflateStream을 포함하는 using 문은 DeflateStream을 교과서적으로 닫는다. 그 괒어에서 아직 기록되지 않은 버퍼도 배출한다. 또한 DeflateStream이 감싼 MemoryStream도 자동으로 닫힌다. 따라서 압축 결과를 얻으려면 ToArray를 호출해야 한다.
  • 다음 예제는 MemoryStream이 자동으로 닫히지 않게 만드는 방법을 보여준다. 또한 비동기 읽기, 쓰기 메서드들을 사용하는 방법도 보여준다.
byte[] data = new byte[1000];

var ms = new MemoryStream();
using (Stream ds = new DeflateStream(ms, CompressionMode.Compress, true))
  await ds.WriteAsync(data, 0, data.Length);

Console.WriteLine(ms.Length);  // 113
ms.Position - 0;

using (Stream ds = new DeflateStream(ms, COmpressionMode.Decompress))
  for (int i = 0; i < 1000; i += ds.ReadAsync(data, i, 1000-i));
  • DeflateStream 생성자에 추가로 지정한 true는 처분시 바탕 스트림을 닫지 말라는 뜻이다. 이 때문에 using 문이 끝나도 MemoryStream은 열린 상태를 유지한다. 그 스트림의 위치를 0으로 되돌린 후 자료를 읽을 수 있는 것은 그 덕분이다.

ZIP 파일 다루기

  • .NET Framework 4.5에는 널리 쓰이는 압축 파일 형식인 ZIP을 읽고 쓸 수 있는 ZipArchive 클래스와 ZipFile 클래스가 추가되었다. 이들이 속한 이름공간은 System.IO.Compression이다.
    • DeflateStream이나 GZipStream이 사용하는 압축 형식에 비한 ZIP 형식의 장점은 다수의 파일을 담는 컨테이너 역할을 한다는 점과 Windows 탐색기나 기타 압축 유틸리티로 생성한 ZIP 파일들과 호환된다는 점이다.
  • ZipArchive는 스트림을 압축하거나 해제하는데 쓰이고, ZipFile은 좀 더 흔한 경우인 파일들을 압축하거나 해제하는데 쓰인다(ZipFile은 ZipArchive의 기능을 좀 더 쉽게 사용하기 위한 정적 보조 클래스이다)
  • ZipFile의 CreateFromDirectory 메서드는 지정된 디렉터리의 모든 파일을 하나의 ZIP 파일에 추가한다.
ZipFile.CreateFromDirectory(@"d:\MyFolder", @"d:\compressed.zip");
  • 반대로 ExtractToDirectory 메서드는 ZIP 파일의 내용을 추출해서 디렉터리에 넣는다.
ZipFile.ExtractToDirectory(@"d:\compressed.zip", @"d:\MyFolder");
  • 압축 시 최적화 방식(파일 크기 또는 속도)이나 압축 파일에 원본 디렉터리 이름을 포함시킬 것인지의 여부 등을 지정할 수 있다.
    • 앞의 예제에서 원본 디렉터리 이름을 포함시킨다면, 압축 파일 안에 MyFolder라는 하위 디렉터리가 만들어진다.
  • 압축 파일의 개별 항목을 읽거나 쓸 때는 ZipFile의 Open 메서드를 사용한다. 이 메서드는 ZipArchive 객체를 돌려준다(Stream 객체로 ZipArchive를 직접 인스턴스화 하는 것도 가능하다)
    • Open 호출 시 반드시 파일 이름을 지정해야 하며, 파일을 읽을 것인지(Read), 새로 만들 것인지(Create), 갱신할 것인지(Update)도 지정해야 한다.
    • 일단 ZipArchive 객체를 얻었으면, Entries 속성을 이용해서 기존 항목들을 열거하거나 GetEntry 메서드로 특정 파일을 찾을 수 있다.
using (ZipArchive zip = ZipFile.Open(@"d:\zz.zip", ZipArchiveMode.Read))
  foreach (ZipArchiveEntry entry in zip.Entries)
    Console.WriteLine(entry.FullName + " " + entry.Length);
  • ZipArchiveEntry에는 또한 Delete 메서드와 ExtractToFile 메서드(사실 이는 ZipFileExtensions 클래스에 정의된 확장 메서드이다) 그리고 읽기가능/ 쓰기가능 Stream을 돌려주는 Open 메서드가 있다.
    • 새 항목을 만들 때는 ZipArchive에 대해 CreateEntry를(또는 확장 메서드 CreateEntryFromFile을) 호출한다.
    • 다음은 d:\zz.zip 이라는 압축 파일을 생성한 후, foo.dll 파일을 그 압축 파일 안의 bin\X86이라는 디렉터리 구조 안에 추가하는 예이다.
byte[] data = File.ReadAllBytes(@"d:\foo.dll");
using (ZipArchive zip = ZipFile.Open(@"d:\zz.zip", ZipArchiveMode.Update))
  zip.CreateEntry(@"bin\X64\foo.dll).Open().Write(data, 0, data.Length);
  • 이러한 연산들을 전적으로 메모리 안에서 수행할 수도 있다. MemoryStream으로 ZipArchive 객체를 생성하면 된다.

파일과 디렉터리 연산

  • System.IO 이름공간은 복사, 이동, 디렉터리 생성, 파일 특성과 권한 설정 같은 일장적인 파일 및 디렉터리 연산들을 수행하는 일단의 형식들을 제공한다. 대부분의 기능에 두 가지 선택이 존재한다. 주어진 기능을 정적 클래스의 정적 메서드로 수행할 수도 있고, 적절한 인스턴스를 생성한 후 인스턴스 메서드로 수행할 수도 있다.
    • 정적 클래스 – File과 Directory
    • 인스턴스 메서드 클래스 – FileInfo와 DirectoryInfo
  • 또한 Path라는 정적 클래스도 있다. 이 클래스가 파일이나 디렉터리를 직접 다루지는 않는다. 이 클래스는 파일 이름과 디렉터리 경로에 특화된 문자열 조작 메서드들을 제공한다. Path는 또한 임시 파일 이름을 위한 수단도 제공한다.
    • Windows 스토어 앱에서는 File과 Directory, Path 클래스를 사용할 수 없다.

File 클래스

  • File은 정적 메서드들로 이루어진 정적 클래스이다. 이 클래스의 메서드들은 모두 파일 이름을 인수로 받는다. 파일 이름은 현재 디렉터리에 상대적인 경로일 수도 있고 모든 디렉터리가 지정된 전체 경로일 수도 있다.
    • (클래스 메서드 소개 생략)
  • Move와 Replace는 파일 이름을 변경하거나 파일을 다른 디렉터리로 옮기는데 쓰인다. 만일 대상 파일이 이미 존재하면 Move는 예외를 던지지만, Replace는 예외를 던지지 않는다.
  • Delete는 파일을 삭제한다. 만일 파일이 읽기 전용이면 UnauthorizedAccessException을 던진다.
    • 파일이 읽기 전용인지는 GetAttributes로 얻은 파일 특성들을 점검해서 알아낼 수 있다. 다음은 GetAttributes가 돌려주는 FileAttrubute 열거형의 멤버들이다.
    • (멤버들 소개 생략)
  • 이 열거형 멤버들은 조합이 가능하다. 다음은 다른 파일 특성들은 그대로 두고 특정 특성 하나만 변경하는 방법을 보여주는 예이다.
string filePath = @"c:\temp\test.txt";

FileAttributes fa = File.GetAttributes(filePath);
if ((fa & FileAttributes.ReadOnly) != 0)
{
  fa ^= FileAttributes.ReadOnly;
  File.SetAttributes(filePath, fa);
}

// 이제 파일이 읽기 전용이 아니므로 삭제할 수 있다.
File.Delete(filePath);
  • 파일의 읽기 전용 특성은 다음과 같이 FileInfo를 이용해서 변경하는 것이 더 쉽다.
new FileInfo(@"c:\temp\test.txt").IsReadOnly = false;

압축 특성과 암호화 특성

  • FileAttribute의 Compressed와 Encrypted는 Windows 탐색기에서 파일 또는 디렉터리의 속성 대화상자를 통해 접근할 수 있는 ‘압축 또는 암호화 특성’ 체크상자에 해당하는 속성들이다.
    • 이런 종류의 압축과 암호화는 투명하게 작동한다. 다른 말로 하면 이런 압축과 암호화에 관련된 모든 작업은 운영체제가 배경에서 수행하며, 사용자는 그냥 보통의 파일을 읽고 쓸 때와 같은 방법으로 파일을 읽고 쓸 수 있다.
  • 파일의 Compressed나 Encrypted 특성은 SetAttributes 메서드로 변경할 수 없다. 변경하려 하면 오류 없이 실패한다.
    • 암호와 특성(Encrypted)에 대한 해결책은 간단하다. 그냥 File 클래스의 Encrypt 메서드나 Decrypt 메서드를 사용하면 된다.
    • 그러나 압축 특성을 변경하는 것은 좀 더 복잡하다. 한 가지 해결책은 System.Management에 있는 WMI(Windows Management Instrumentation) API를 사용하는 것이다.
    • 다음 메서드는 지정된 디렉터리의 압축을 시도해서 성공시 0을 돌려준다. (실패 시에는 WMI 오류 부호를 돌려준다)
static uint CompressFolder (string folder, bool recursive)
{
  string path = "Win32_Directory.Name='" + folder +"'";

  using (ManagementObject dir = new ManagementObject(path))
  using (ManagementBaseObject p = dir.GetMethodParameters("CompressEx"))
  {
    p["Recursive"] = recursive;
    using (ManagementBaseObject result = dir.InvokeMethod("CompressEx", p, null))
      return (uint) result.Properties["ReturnValue"].Value;
  }
}
  • 압축 특성을 해제하려면 CompressEx 대신 UncompressEx를 사용하면 된다.
  • Windows의 투명한 파일 암호화 시스템은 로그인한 사용자의 패스워드에 기초한 키를 사용한다. 이 시스템은 인증된 사용자가 패스워드를 변경해도 안정적으로 작동한다. 그러나 관리자가 패스워드를 재설정하면 암호화된 파일의 자료를 복원하는 것이 불가능하다.
  • 투명한 암호화와 압축을 위해서는 파일 시스템의 특별한 지원이 필요하다. NTFS(하드 드라이브에서 주로 쓰이는 파일 시스템)는 이 기능들을 지원하지만 CDFS(CD-ROM)와 FAT(이동식 매체)은 지원하지 않는다.
  • 주어진 디스크 볼륨이 압축과 암호화를 지원하는지는 Win32 상호운용 기능(Interop)으로 알아낼 수 있다.
using System;
using System.IO;
using System.Text;
using System.ComponentModel;
using System.Runtime.InteropServices;

class SupportsCompressionEncryption
{
  const int SupportsCompression = 0x10;
  const int SupportsEncryption = 0x20000;

  [DllImport("Kernel32.dll", SetLastError = true)]
  extern static bool GetVolumeInformation (string vol, StringBuilder name, int nameSize, out uint serialNum, out uint maxNameLen, out uint flags, StringBuilder fileSysName, int fileSysNameSize);

  static void Main()
  {
    uint serialNum, maxNameLen, flags;
    bool ok = GetVolumnInformation(@"C:\", null, 0, out serialNum, out maxNameLen, out flags, null, 0);

    if (!ok)
      throw new Win32Exception();

    bool canCompress = (flags & SupportsCompression) != 0;
    bool canEncryp = (flags & SupportsEncryption) != 0;
  }
}

파일 보안

  • GetAccessControl 메서드와 SetAccessControl 메서드를 이용하면 사용자와 역할(role)에 배정된 운영체제 권한들을 조회하거나 변경할 수 있다. 두 메서드 모두 FileSecurity 객체를 사용한다.
    • 또한 새 파일을 생성할 때 FileStream의 생성자에 FileSecurity 객체를 지정해서 새 파일의 권한들을 설정할 수도 있다.
  • 다음은 파일의 기존 권한들을 나열하고 ‘Users’ 그룹에 실행 권한을 부여하는 예이다.
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;

...
FileSecurity sec = File.GetAccessControl(@"d:\test.txt");
AuthorizationRuleCollection rules = sec.GetAccessRules(true, true, typeof(NTAccount));

foreach (FileSystemAccessRule rule in rules)
{
  Console.WriteLine(rule.AccessControlType);  // Allow 또는 Deny
  Console.WriteLine(rule.FileSystemRights);  // 이를테면 FullControl
  Console.WriteLine(rule.IdentityReference.Value);  // 이를테면 MyDoamin/Joe
}

var sid = new SecurityIdentifier (WellKnownSidtype.BuiltinUsersSid, null);
string usersAccount = sid.Translate(typeof(NTAccount)).ToString();

FIleSystemAccessRule newRule = new FileSystemAccessRule(usersAccount, FileSystemRights.ExecuteFile, AccessControlType.Allow);

sec.AddAccessRule(newRule);
File.SetAccessControl(@"d:\test\txt", sec);

Directory 클래스

  • 정적 Directory 클래스는 File 클래스의 메서드들에 대응되는 일단의 메서드들을 제공한다. 디렉터리 존재 여부를 점검하는 Exist, 디렉터리의 이동과 삭제를 위한 Move와 Delete가 잇고, 디렉터리 생성 시간과 최종 접근 시간을 조회, 설정하는 메서드들과 보안 권한들을 조회, 설장하는 메서드들이 있다.
    • (이하 Directory 메서드 소개 생략)
  • IEnumerable<string> EnumerateFiles, IEnumerable<string> EnumerateDirectories, IEnumerable<string> EnumerateFileSystemEntries는 .NET Framework 4.0에서 추가되었다. 이들은 게으르게 평가되므로 즉 실제로 순차열을 열거해야 비로소 파일 시스템에서 자료를 가져오므로 Get* 메서드들보다 더 효율적일 수 있다. 이들은 특히 LINQ 질의에 잘 맞는다.
  • Enumerate* 메서드들과 Get* 메서드들에는 searchPattern(문자열)과 searchOption(열거형)을 받도록 중복적재된 버전들도 있다. SearchOptoin.SearchAllSubDirectories를 지정하면 재귀적인 하위 디렉터리 검색이 수행된다.
  • *FileSystemEntries 메서드들은 *Files의 결과들과 *Directories의 결과들을 합친다.

FileInfo와 DirectoryInfo

  • File과 Directory의 정적 메서드들은 하나의 파일 또는 디렉터리 연산을 수행하는데 편리하다. 그러나 여러 메서드를 연달아 호출해야 한다면 FileInfo 클래스와 DirectoryInfo 클래스가 제공하는 객체 모형을 사용하는 것이 더 편리하다.
  • FileInfo에는 File의 정적 메서드들 대부분에 대응되는 인스턴스 메서드들이 있다.
    • FileInfo에는 또한 Extension이나 Length, IsReadOnly, Directory(DirectoryInfo 객체를 돌려준다) 같은 추가적인 속성들도 제공한다.
    • (사용 예 생략)
  • 다음 예제는 DirectoryInfo를 이용해서 파일들과 하위 디렉터리들을 열거하는 방법을 보여준다.
DirectoryInfo di = new DirectoryInfo(@"e\photos");

foreach(FileInfo fi in di.GetFiles("*.jpg"))
  Console.WriteLine(fi.Name);

foreach(DirectoryInfo subDir in di.GetDirectories())
  Console.WriteLine(subDir.FullName);

Path 클래스

  • 정적 Path 클래스는 경로와 파일 이름을 다루는 메서드들과 필드들을 제공한다. 다음과 같은 설정 코드가 실행되었다고 할 때 아래 표는 Path의 메서드들과 필드들의 결과를 보여준다.
string dir = @"c:\mydir";
string file = "myfile.txt";
string path = @"c:\mydir\myfile.txt";

Directory.SetCurrentDirectory(@"k:\demo");

 

표현식 결과
Directory.GetCurrentDirectory() k:\demo\
Path.IsPathRooted(file) False
Path.IsPathRooted(path) True
Path.GetPathRoot(path) c:\
Path.GetDirectoryName(path) c:\mydir
Path.GetFileName(path) myfile.txt
Path.GetFullPath(file) k:\demo\myfile.txt
Path.Combine(dir, file) c:\mydir\myfile.txt
파일 확장자
Path.HasExtension(file) True
Path.GetExtention(file) .txt
Path.GetFileNameWithoutExtesion(file) myfile
Path.ChangeExtension(file, “.log”) myfile.log
구분자와 문자
Path.AltDirectorySeparatorChar /
Path.PathSeparator ;
Path.VolumeSeparatorChar :
Path.GetInvalidPathChars() 문자 부호 0에서 31까지의 문자들과 “<>/
Path.GetInvalidFileNameChars() 문자 부호 0에서 31까지의 문자들과 “<>/:*?\/
임시 파일
Path.GetTempPath() <지역 사용자 폴더>\Temp
Path.GetRandomFileName() d2dwuzjf.dnp
Path.GetTempFileName() <지역 사용자 폴더>\Temp\tmp14b.tmp

 

  • Combine 메서드는 디렉터리 이름과 파일 이름 또는 두 디렉터리의 이름을 결합한 결과를 돌려주는데, 이름 끝의 역슬래시를 자동으로 점검 또는 추가해주기 때문에 편리하다.
  • GetFullPath 메서드는 현재 디렉터리를 기준으로 한 상대 경로를 절대 경로로 변환한다.
    • 이 메서드는 이를테면 ..\..\file.txt 같은 값을 받아들인다.
  • GetRandomFileName 메서드는 고유함이 보장되는 8.3자 파일 이름을 돌려준다. 그 이름의 파일을 실제로 생성하지는 않는다.
  • GetTempFileName 메서드는 파일 65,000개 마다 순환되는 자동 증가 카운터를 이용해서 만든 임시 파일 이름을 돌려준다. 이 메서드는 지역 임시 디렉터리에 실제로 그 이름의 파일(길이가 0바이트인)을 생성한다.
    • GetTempFileName으로 생성한 파일을 다 사용했으면 반드시 삭제해야 한다. 삭제하지 않으면 언젠가는 (GetTempFileName을 65,000회 호출한 후에) 예외가 발생한다.
    • 그것이 문제가 된다면 대신 GetTempPath의 결과와 GetRandomFileName의 결과를 Combine으로 합친 임시 파일 이름을 사용하면 된다.

특수 폴더

  • Path와 Directory에 없는 기능 하나는 My Documents나 Program Files, Application Data 같은 특수 폴더들의 실제 경로를 알려주는 기능이다. 이 기능은 System.Environment 클래스의 GetFolderPath 메서드가 제공한다.
string myDocPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
  • Environment.SpeicalFolder는 하나의 열거형으로 Windows의 모든 특수 폴더에 대응되는 다음과 같은 멤버들로 구성되어 있다.
AdminTools CommonVideos Personal
ApplicationData Cookies PrinterShortcuts
CDBurning Desktop ProgramFiles
CommonAdminTools DesktopDirectory ProgramFilesX86
CommonApplicationData Favorites Programs
CommonDesktopDirectory Fonts Recent
CommonDocuments History Resources
CommonMusic InternetCache Sendto
CommonOemLinks LocalApplicationData StartMenu
CommonPictures LocalizedResources Startup
CommonProgramFiles MyComputer System
CommonProgramFilesX86 MyDocuments SystemX86
CommonPrograms MyMusic Template
CommonStartMenu MyPictures UserProfile
CommonStartup MyVideos Windows
CommonTemplates NetworkShortcuts

 

  • 아쉽게도 .NET Framework 디렉터리는 이 메서드가 알려주지 않는다. 그 디렉터리는 다음 메서드로 알아낼 수 있다.
System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory()
  • 추가 설명이 필요한 값이 몇 개 있다. ApplicatoinData에 해당하는 디렉터리에 응용 프로그램의 자료를 저장하면 그 자료는 같은 네트워크에서 사용자를 따라 다닌다(사용자가 네트워크 도메인에 대해 로밍(raming) 프로파일을 활성화한 경우) 비로밍자료(현재 로그인한 사용자에게만 국한된)는 LocalApplicationData 디렉터리에 저장하면 된다.
    • CommonApplicationData 디렉터리는 현재 컴퓨터의 모든 사용자가 공유하는 자료를 저장하도록 마련된 장소이다.
    • 응용 프로그램의 자료를 Windows의 레지스트리보다는 이 폴더들에 저장하는 것이 권장된다. 이 폴더들에 자료를 저장할 떄는 다음 예처럼 응용 프로그램과 같은 이름의 하위 디렉터리를 만들어서 저장하는 것이 관례이다.
string localAppDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Application), "MyCoolApplication");

if (!Directory.Exists(localAppDataPath))
  Directory.CreateDirectory(localAppDataPath);
  • 아주 제한된 모래상자 안에서 실행되는 프로그램(Silverlight 응용 프로그램)은 이 폴더들에 접근할 수 없다. 그런 프로그램은 격리된 저장소를 사용한다. Windows 스토어 앱의 경우에는 WinRT 라이브러리를 사용한다.
  • CommonApplicatoinData와 관련해서 아주 고약한 함정이 하나 있다. 사용자가 프로그램을 관리자 권한으로 실행한 상태에서 프로그램이 CommonApplicationData에 폴더와 파일을 생성한 경우, 나중에 제한된 Windows 로그인 세션에서 프로그램을 실행하면 프로그램은 CommonApplicatoinData의 해당 폴더들과 파일들을 갱신할 수 없다.
    • 권한이 제한된 다른 사용자 계정으로 전환해서 프로그램을 실행하는 경우도 마찬가지다.
    • 한 가지 해결책은 그런 폴더를 프로그램 설치 시에 생성해 두는 것이다(모든 사용자에게 배정된 권한들을 적용해서)
    • 아니면 CommonApplicationData에 폴더를 생성한 즉시(다른 파일들을 기록하기 전에) 다음 코드를 실행하는 방법도 있다. 이 코드는 ‘Users’ 그룹의 모든 계정이 해당 폴더에 제한 없이 접근할 수 있게 한다.
public void AssignUseresFullControlToFolder(string path)
{
  try
  {
    var sec = Directory.GetAccessControl(path);
    if (UsersHaveFullControl (esc)) return;

    var rule = new FileSystemAccessRule(
      GetUsersAccount().ToString(),
      FileSystemRights.FullControl,
      InheritanceFlags.ContainerInherit | IneritanceFlags.ObjectInherit,
      PropagationFlags.None,
      AccessControlType.Allow);

      sec.AddAccessRule(rule);
      Directory.SetAccessControl(path, sec);
  }
  catch (UnauthorizedAccessException)
  {
    // 해당 폴더를 다른 사용자가 이미 생성햇음
  }
}

bool UsersHaveFullControl(FileSystemSecurity sec)
{
  var usersAccount = GetUsersAccount();
  var rules = sec.GetAccessRules(true, true, typeof(NTAccount)).OfType<FileSystemAccessRule>();
  return rules.Any(r =>
    r.FileSystemRights == FileSystemRights.FullControl &&
    r.AccessControlType == AccessControltype.Allow &&
    r.InheritanceFlags == (InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit) && 
    r.IdentityReference == usersAccount);
}

NtAccount GetUsersAccount()
{
  var sid = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null);
  return (NTAccount)sid.Translate(typeof(NTAccount));
}
  • 응용 프로그램의 구성 파일이나 로그 파일을 응용 프로그램의 기준 디렉터리에 저장하기도 한다. 기준 디렉터리는 AppDomain.CurrentDomain.BaseDirectory로 얻을 수 잇다. 그러나 이 방법은 권자오디지 않는다. 최초 설치 이후에는 응용 프로그램이 그 폴더에 접근하는 것을 운영체제가 허용하지 않을 가능성이 크기 때문이다. (응용 프로그램을 관리자 권한으로 상승해서 실행하지 않는 한)

볼륨 정보 얻기

  • 컴퓨터에 설치된 드라이브에 대한 정보는 DriveInfo 클래스를 이용해서 얻을 수 잇다.
DriveInfo c = new DriveInfo("C");  // C: 드라이브를 질의한다.

long totalSize = c.TotalSize;  // 크기 (바이트 단위)
long freeBytes = c.TotalFreeSpace;  // 디스크 할당량(쿼터)은 무시한 가용 용량
long freeToMe = c.AvailableFreeSpace;  // 디스크 할당량도 포함한 가용 용량

foreach (DriveInfo d in DriveInfo.GetDrives())  // 정의된 모든 드라이브를 나열
{
  Console.WriteLine(d.Name);  // C:\
  Console.WriteLine(d.DriveType);  // Fixed
  Console.WriteLine(d.RootDirectory);  // C:\

  if (d.IsReady)  // 드라이브라 준비되지 않은 상태이면 다음 속성들이 예외를 던진다.
  {
    Console.WriteLine(d.VolumeLabel);  // The Sea Drive
    Console.WriteLine(d.DriveFormat);  // NTFS
  }
}
  • 정적 GetDrives 메서드는 마운팅된 모든 드라이브(CD-ROM, 매체 카드, 네트워크 연결 포함)를 돌려준다. DriveType은 다음과 같은 값들을 가진 열거형이다.
Unknown, NoRootDirectory, Removable, Fixed, Network, CDRom, Ram

파일 시스템 활동 감시

  • FileSystemWatcher 클래스를 이용하면 한 디렉터리의(그리고 필요하다면 모든 하위 디렉터리의) 활동을 감시할 수 잇다.
    • FileSystemWatcher는 파일 또는 하위 디렉터리 생성, 수정, 이름 변경, 삭제나 파일 또는 디렉터리의 특성 변경 같은 사건에 대한 이벤트들을 제공한다.
    • 이 이벤트들은 해당 변경을 어떤 사용자 또는 프로세스가 수행했는지와는 무관하게 항상 발동한다.
    • 다음은 이 클래스를 사용하는 예이다.
static void Main() { Watch(@"c:\temp", "*.txt", true); }

static void Watch(string path, string filter, bool includeSubDirs)
{
  using (var watcher = new FileSYstemWatcher(path, filter))
  {
    watcher.Created += FileCreatedChangedDeleted;
    watcher.Changed += FileCreatedChangedDeleted;
    watcher.Deleted += FileCreatedChangedDeleted;
    watcher.Renamed += FileRenamed;
    watcher.Error += FileError;

    watcher.IncludeSubdirectories = includeSubDirs;
    watcher.EnableRaisingEvents = true;

    Console.WriteLine("이벤트 청취 중 - 종료하려면 Enter 키를 누르세요");
    Console.ReadLine();
  }
  // FileSystemWatcher를 처분하면 이벤트가 더 이상 발동되지 않는다.
}

static void FileCreatedChangedDeleted(object o, FileSystemEventArgs e) => Console.WriteLine("파일 {0}, {1}", e.FullPath, e.ChangeType);

static void FileRenamed(object o, RenamedEventArgs e) => Console.WriteLine("이름 바뀜: {0} -> {1}", e.OldFullPath, e.FullPath);

static void FileError(object o, ErrorEventArgs e) => Console.WriteLine("오류: " + e.GetException().Message);
  • FileSystemWatcher는 개별 스레드에서 이벤트를 발동하기 떄문에 이벤트 처리 코드를 예외 처리 블록으로 감싸지 않으면 응용 프로그램이 강제로 종료될 수 있다.
  • Error 이벤트는 파일 시스템의 오류에 관한 것이 아니다. 이 이벤트는 Changed나 Created, Deleted, Renamed 이벤트가 너무 많이 발생해서 FileSystemWatcher의 내부 이벤트 버퍼가 넘쳤음을 뜻한다. 그 버퍼의 크기는 InternalBufferSize 속성을 이용해서 변경할 수 있다.
  • IncludeSubdirectories는 재귀적으로 적용된다. 따라서 만일 C:\에 대해 FileSystemWatcher를 생성해서 IncludeSubdirectories를 true로 설정하면 C 드라이브의 모든 파일과 디렉터리의 변화에 대해 이벤트가 발동한다.
  • FileSystemWatcher를 사용할 때 한 가지 조심할 점은, 파일을 새로 생성하거나 갱신할 때 파일의 내용이 완전히 채워지거나 갱신되기 전에 변경 이벤트가 발동될 수 있다는 것이다.
    • 파일을 생성하는 다른 소프트웨어와 연동해서 작동하는 프로그램을 만들 떄는 감시 되지 않는 확장자로 파일을 생성하고 내용을 모두 기록한 후에 확장자를 변경하는 등의 전략을 사용해서 이 문제를 완화할 필요가 있다.

WinRT의 파일 입출력

  • (생략)

메모리 대응 파일

  • 메모리 대응 파일(memory-mapped file)의 핵심 특징은 다음 두 가지이다.
    • 파일 자료에 대한 임의 접근이 효율적이다.
    • 같은 컴퓨터의 서로 다른 프로세스가 자료를 공유하는 매개체로 유용하다.
  • 메모리 대응 파일을 위한 형식들은 System.IO.MemoryMappedFiles 이름공간에 있다.
    • 이들은 .NET Framework 4.0에서 도입되었다. 내부적으로 이들은 메모리 대응 파일 관련 Win32 API를 사용한다.
    • Windows 스토어 앱에서는 이 형식들을 사용할 수 없다.

메모리 대응 파일과 임의 접근 파일 입출력

  • 보통의 FileStream에서도 임의 접근 파일 입출력이 가능하지만(스트림의 Position 속성을 설정함으로써), FileStream은 기본적으로 순차적인 입출력에 최적화 되어 있다. 대략적인 수치를 제시하자면 다음과 같다.
    • 순차적인 입출력에서는 FileStream이 메모리 대응 파일보다 10배 빠르다.
    • 입의 접근 입출력에서는 메모리 대응 파일이 FileStream보다 10배 빠르다.
  • FileStream의 Position 변경에는 수 마이크로초가 소비된다. 루프 안에서 Position을 변경하면 시간이 꽤 많이 지연될 수 있다. 또한 스트림을 읽거나 쓸 때마다 접근 위치가 바뀌므로, FileStream은 다중 스레드에 적합하지 않다.
  • 메모리 대응 파일을 생성하는 과정은 다음과 같다.
    1. 보통의 방식으로 FileStream 객체를 얻는다.
    2. 파일 스트림(FileStream 객체)을 인수로 지정해서 MemoryMappedFile 인스턴스를 생성한다.
    3. 그 인스턴스에 대해 CreateViewAccessor를 호출한다.
  • 3번의 호출은 MemoryMappedViewAccessor 객체를 반환한다.
    • MemoryMappedViewAccessor 형식은 단순 형식들과 구조체, 배열의 임의 접근 읽기/쓰기를 위한 메서드들을 제공한다.
  • 다음은 1백만 바이트짜리 파일을 하나 생성한 후 메모리 대응 파일 API를 이용해서 500,000번 위치에 있는 바이트 하나를 읽고 쓰는 예제이다.
File.WriteAllBytes("long.bin", new byte[1000000]);

using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile("long.bin"))
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
  accessor.Write(500000, (byte)77);
  Console.WriteLine(accessor.ReadByte(5000000);  // 77
}
  • CreateFromFile을 호출할 때 메모리 대응 파일의 이름을 지정할 수도 있다. 널이 아닌 문자열 이름을 지정해서 생성한 맵을 이용하면 다른 프로세스와 메모리 블록을 공유할 수 있다. 용량도 지정할 수 있는데, 그러면 메모리 대응 파일이 그 크기만큼 자동으로 확장된다. 다음은 1,000 바이트짜리 메모리 대응 파일을 생성하는 예이다.
using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile("long.bin"), FileMode.Create, null, 1000))
...

메모리 대응 파일과 공유 메모리

  • 앞서 얘기했듯, 메모리 대응 파일을 같은 컴퓨터에 있는 프로세스들이 메모리를 공유하는 수단으로 사용할 수 있다.
    • 이 경우 한 프로세스가 적절한 이름으로 MemoryMappedFile.CreateNew를 호출해서 메모리 대응 파일을 생성하고, 다른 프로세스는 같은 이름으로 MemoryMappedFile.OpenExisting을 호출해서 그 메모리 대응 파일을 얻는다.
    • 그런 다음에는 두 프로세스가 메모리 대응 파일을 하나의 공유 메모리 블록으로 사용해서 자료를 주고 받는다.
    • 이런 식으로 스이는 메모리 대응 파일은 비록 이름에 ‘파일’이 있긴 하지만 전적으로 메모리 안에만 존재하며 디스크에 잇는 그 어떤 것과도 무관하다.
  • 다음 예제는 500바이트짜리 공유 메모리 대응 파일을 생성해서 0번 위치에 정수 12345를 기록한다.
using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("Demo", 500))
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
  accessor.Write(0, 12345);
  Console.ReadLine();  // 사용자가 엔터키를 누를 때까지 공유 메모리를 살려 둔다.
}
  • 다음은 같은 메모리 대응 파일을 열어서 그 정수를 읽는 코드이다.
// 다음을 개별적인 실행 파일(exe)에서 실행할 수도 있다.
using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("Demo"))
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
  Console.WriteLine(accessor.ReadInt32(0));  // 12345
}

뷰 접근자 다루기

  • MemoryMappedFile에 대해 CreateViewAccessor를 호출하면 임의의 위치에서 값들을 읽고 쓸 수 있는 하나의 뷰 접근자(view accessor)가 반환된다.
  • 뷰 접근자는 수치 형식들과 bool, char 형식을 받는, 그리고 그러한 값 형식의 원소 또는 필드로 구성된 배열 또는 구조체를 받는 Read*/Write* 메서드들을 제공한다.
    • 참조 형식들이나 참조 형식의 원소/필드를 담은 배열 또는 구조체는 지원하지 않는데, 그런 형식들은 관리되지 않는 메모리에 대응시킬 수 없기 때문이다.
    • 따라서 문자열을 기록하려면 먼저 바이트 배열로 부호화 해야 한다. 다음이 그러한 예이다.
byte[] data = Encoding.UTF8.GetBytes("시험용 텍스트");
accessor.Write(0, data.Length);
accessor.WriteArray(4, data, 0, data.Length);
  • 제일 먼저 길이를 기록했음을 주목하기 바란다. 이는 나중에 이 문자열을 읽을 때 바이트를 몇 개나 읽어야 할지 알 수 있게 하기 위한 것이다.
byte[] data = new byte[accessor.ReadInt32(0)]
accessor.ReadArray(4, data, 0, data.Length);
Console.WriteLine(Encoding.UTF8.GetString(data));  // 시험용 텍스트
  • 다음은 구조체를 읽고 쓰는 예이다.
struct Data { public int X, Y; }
...
var data = new Data { X = 123, Y = 456 };
accessor.Write(0, ref data);
accessor.Read(0, out data);
Console.WriteLine(data.X + " " + data.Y);  // 123 456
  • Read/Write 메서드들은 놀랄 만큼 느리다. 포인터를 이용해서 바탕 비관리 메모리에 직접 접근하는 것이 성능이 훨씬 낫다. 다음은 앞의 예제의 비관리 메모리 접근 버전이다.
unsafe
{
  byte* pointer = null;
  try
  {
    accessor.SafeMomoryMappedViewHandle.AcquirePointer(ref pointer);
    int* intPointer = (int*) pointer;
    Console.WriteLine(*intPointer);  // 123
  }
  finally
  {
    if (pointer != null)
      accessor.SafeMemoryMappedViewHandle.ReleasePointer();
  }
}
  • 포인터가 주는 성능상의 이점은 필드가 많은 구조체를 다룰 때 더욱 커진다. Read/Write를 이용해서 관리되는 메모리와 비관리 메모리 사이에서 필드들을 일일이 복사하는 것보다. 포인터를 이용해서 비관리 메모리의 미가공 자료를 직접 다루는 것이 훨씬 빠르다.

격리된 저장소

  • 모든 .NET 프로그램에는 프로그램이 접근할 수 있는 지역 저장소가 주어진다. 그러한 저장소를 격리된 저장소(isolated storage)라고 부른다.
    • 격리된 저장소는 표준 파일 시스템에 접근할 수 없는, 그래서 ApplicationData, LocalApplicatoinData, CommonApplicationData, MyDocuments 같은 특수 폴더에 파일을 만들어서 자료를 저장할 수 없는 프로그램에 아주 유용하다. 제한된 ‘인터넷’ 권한으로 설치된 Silverlight 응용 프로그램과 ClickOnce 응용 프로그램이 바로 그러한 프로그램의 예이다.
  • 격리된 저장소는 다음과 같은 단점을 가지고 있다.
    • API가 사용하기 까다롭다.
    • IsolatedStorageStream을 통해서만 자료를 읽고 쓸 수 있다. 파일이나 디렉터리 객체를 얻어서 보통의 파일 입출력 수단을 사용할 수는 없다.
    • 제한된 OS 권한을 가진 사용자는 컴퓨터의 공용 저장소(CommonApplicationData에 해당)에 있는 다른 사용자가 생성한 파일을 삭제하거나 덮어쓸 수 없다.(수정은 가능) 이는 사실상 버그이다.
  • 보안의 관점에서 격리된 저장소는 외부로부터 독자의 프로그램을 보호하기 위한 울타리라기보다는 독자의 프로그램을 가두는(외부에 해를 입히지 않도록) 울타리에 가깝다.
    • 아주 제한된 권한을 가진(즉, ‘인터넷’ 영역에 속하는) 다른 .NET 응용 프로그램의 침범은 확실하게 막아주지만, 그 외의 경우에는 보안이 강하지 않다.
    • 필요하다면 다른 프로그램의 격리된 저장소에 접근하는 코드를 작성하는 것이 얼마든지 가능하다.
    • 격리된 저장소는 의도적인 침범을 막기 위한 보안 장치가 아니라 실수 또는 부주의로 다른 응용 프로그램의 영역에 침범하지 않게 하기 위한 보호 장치라 할 수 있다.
  • 일반적으로 모래상자 안에서 실행되는 응용 프로그램에 주어지는 격리된 저장소의 용량은 제한적이다. 그 용량은 권한에 따라 다른데, 인터넷 영역에 속하는 응용 프로그램과 Silverlight 응용 프로그램에 주어지는 격리된 저장소의 기본 용량은 1MB이다.
    • Silverlight처럼 다른 런타임에 호스팅되는 UI 기반 응용 프로그램은 사용자에게 격리된 저장소의 용량을 증가할 권한을 요청할 수 있다. IsolatedStorageFile 객체의 IncreaseQuotaTo 메서드를 호출하면 된다. 이 메서드는 반드시 버튼 클릭처럼 사용자가 주도한 이벤트에서 호출해야 한다. 사용자가 동의하면 이 메서드는 true를 돌려준다.
    • 격리된 저장소의 현재 최대 용량은 Quota 속성으로 알아낼 수 있다.

격리 저장소 종류

  • 격리된 저장소는 프로그램에 따라 그리고 사용자에 따라 여러 구획으로 나뉜다. 격리된 저장소는 기본적으로 다음 세 가지 구획(compartment)으로 구성된다.
    • 지역 사용자 구획들
      • 사용자당, 프로그램당, 컴퓨터당 하나씩
    • 로밍 사용자 구획들
      • 사용자당, 프로그램당 하나씩
    • 컴퓨터 구획들
      • 프로그램당, 컴퓨터당 하나씩(한 프로그램의 모든 사용자가 한 구획을 공유)
  • 적절한 운영체제와 도메인의 지원이 있는 경우, 로밍(roaming) 사용자 구획의 자료는 한 네트워크 안에서 사용자를 따라 다닌다. 그러한 지원이 없으면 로밍 사용자 구획은 그냥 지역 사용자 구획처럼 작동한다.
  • 그런데 격리된 저장소의 구획과 관련해서 ‘프로그램’은 격리 모드에 따라 다음 둘 중 하나를 의미한다.
    • 어셈블리
    • 특정 응용 프로그램의 문맥 안에서 실행되는 어셈블리
  • 전자는 어셈블리 격리 모드의 경우이고 후자는 도메인 격리 모드의 경우이다. 도메인 격리가 어셈블리 격리보다 더 흔하게 쓰인다.
    • 도메인 격리 모드는 현재 실행 중인 어셈블리와 애초에 그 어셈블리를 실행한 실행 파일 또는 웹 응용 프로그램이라는 두 가지 기준으로 저장소를 격리한다.
    • 반면 어셈블리 격리 모드는 현재 실행 중인 어셈블리만 고려한다. 따라서 서로 다른 응용 프로그램들이 같은 어셈블리를 호출한 경우 그 응용 프로그램들은 모두 같은 저장소를 공유한다.
  • 어셈블리와 응용 프로그램은 ‘강력한 이름(strong name; 또는 강한 이름)’으로 식별된다. 강력한 이름이 없는 어셈블리(약한 이름 어셈블리)는 전체 파일 경로 또는 URI로 식별한다. 따라서 약한 이름 어셈블리의 위치를 옮기거나 이름을 변경하면 해당 격리된 저장소가 초기화 된다.
  • 이 둘을 앞의 세 종류와 조합하면 총 여섯 종류가 된다. 아래 표는 각 종류가 제공하는 격리 기능을 비교한 것이다.
종류 컴퓨터당? 응용 프로그램당? 어셈블리당? 사용자당? 저장소 획득용 메서드
도메인/사용자(기본) O O O O GetUserStoreForDomain
도메인/로밍 O O O GetMachineStoreForDomain
도메인/컴퓨터 O O O
어셈블리/사용자 O O O GetUserStoreForAssembly
어셈블리/로밍 O O
어셈블리/컴퓨터 O O GetMachineStoreForAssembly

 

  • 도메인만 고려하는 격리 저장소 구획은 없다. 그러나 한 응용 프로그램의 모든 어셈블리가 하나의 격리된 저장소를 공유하게 하고 싶다면, 간단한 해결책이 존재한다. 그냥 어셈블리 중에서 IsolatedStorageFileStream 객체를 인스턴스화해서 돌려주는 공용 메서드를 노출하면 된다.
    • 그 어떤 어셈블리라도, 일단 IsolatedStorageFile 객체를 얻기만 하면 그 객체를 이용해서 해당 격리된 저장소 파일에 접근할 수 잇다. 격리의 제약은 생성 시점에서 적용될 뿐, 이후의 사용에는 적용되지 않는다.
  • 마찬가지로 컴퓨터만 고려한느 격리 저장소 구획도 없다. 한 컴퓨터의 여러 응용 프로그램이 하나의 격리된 저장소를 공유하게 하고 싶다면, 어셈블리별로 격리되는 IsolatedStorageFileStream 객체를 돌려주는 공용 메서드를 노출하는 어셈블리를 만들어서 여러 응용 프로그램이 그것을 참조하게 하는 수 밖에 없다. 이 방식이 작동하려면 그 공용 어셈블리에 반드시 강력한 이름을 부여해야 한다.

격리된 저장소 읽기/쓰기

  • 격리된 저장소를 읽고 쓸 때는 스트림을 사용한다. 작동 방식은 보통의 파일 스트림과 별로 다르지 않다.
    • 격리된 저장소 스트림을 얻으려면 IsolatedStorageFile의 정적 메서드 중 하나를 호출해서 원하는 종류의 격리 저장소 구획 객체를 얻어야 한다.
    • 그런 다음에는 그 객체와 파일 이름, 그리고 파일 모드(FileMode 열거형)를 지정해서 IsolatedStorageFileStream 인스턴스를 생성한다.
// IsolatedStorage 클래스는 System.IO.IsolatedStorage에 있다.
using (IsolatedStorageFile f = IsolatedStorageFile.GetMachineStoreForDomain())
using (var s = new IsolatedStorageFileStream("hi.txt", FileMode.Create, f))
using (var writer = new StreamWriter(s))
  writer.WriteLine("Hello, World");

// 다시 읽어 들인다.
using (IsolatedStorageFile f = IsolatedStorageFile.GetMachineStoreForDomain())
using (var s = new IsolatedStorageFileStream("hi.txt", FileMode.Open, f))
using (var reader = new StreamReader(s))
  Console.WriteLine(reader.ReadToEnd());  // Hello, World
  • IsolatedStorageFile의 이름은 그리 적절하지 않다. 이 클래스는 파일이 아니라 파일들을 담는 컨테이너(기본적으로는 디렉터리)를 대표한다.
  • IsolatedStorageFile 객체를 얻는 더 나은 (그러나 좀 더 장황한) 방법은 적절한 IsolatedStorageScope 플래그들의 조합을 지정해서 IsolatedStorageFile.GetStore를 호출하는 것이다.
var flags = IsolatedStorageScope.Machine | IsolatedStorageScope.Application | IsolatedStorageScope.Assembly;

using (IsolatedStorageFile f = IsolatedStorageFile.GetStore(flags, typeof(StrongName), typeof(stringName)))
{
  ...
  • 이 방법의 장점은 CLR이 적절한 격리된 저장소를 선택하기 위해 현재 프로그램을 식별하는 과정에서 고려할 증거들의 종류를 IsolatedStorageScope 플래그들을 통해서 명시적으로 지정할 수 있다는 점이다.
    • 일반적으로는 프로그램 어셈블리들의 강력한 이름을 사용하는 것이 바람직하다. 강력한 이름은 고유하며, 어셈블리의 버전이 바뀌어도 일관되게 유지할 수 있기 때문이다.
  • 증거들을 명시적으로 지정하지 않고 자동으로 선택하게 하면, CLR이 Authenticode 서명까지 고려 할 수도 있다.
    • Authenticode 관련 사항이 바뀌면 응용 프로그램의 신원까지 바뀐다는 점에서 대체로 이는 바람직하지 않은 일이다.
    • 특히 처음에는 Authenticode 없이 응용 프로그램을 만들었다가 나중에 추가한 경우 CLR은 새 응용 프로그램을 이전 버전과는 다른 것으로 간주할 수 있으며, 그러면 이전 버전에서 저장한 자료에 접근할 수 없는 사태가 벌어질 수 있다.
  • IsolatedStorageScope는 플래그 열거형이다. 이 열거형의 플래그들을 제대로 조합해서 지정하지 않으면 유효한 저장소를 얻지 못한다.
    • 아래 표에 올바른 조합들이 나와 있다. 로밍 저장소에 대한 접근이 허용됨을 주목하기 바란다.(로밍 저장소는 지역 저장소와 비슷하되 Windows 로밍 프로파일을 통해서 사용자를 따라다니는 ‘로밍’ 능력이 있다)
어셈블리 어셈블리 및 도메인
지역 사용자 Assembly | User Assembly | Domain | User
로밍 사용자 Assembly | User | Roaming Assembly | Domain | User | Roaming
컴퓨터 Assembly | Machine Assembly | Domain | Machine

 

  • 다음은 어셈블리 및 로밍 사용자별 격리된 저장소에 자료를 기록하는 예이다.
var flags = IsolatedStorageScope.Assembly | IsolatedStorageScope.User | IsolatedStorageScope.Roaming;

using (IsolatedStorageFile f = IsolatedStorageFile.GetStore(flags, null, null))
using (var s = new IsolatedStorageFileSystem("a.txt", FileMode.Create, f))
using (var writer = new StreamWriter(s))
  writer.WriteLine("Hello, World");

저장소 위치

  • .NET이 격리된 저장소 파일들을 실제로 기록하는 디렉터리들은 다음과 같다.
범위 위치
지역 사용자 [LocalApplicationData]\IsolatedStorage
로밍 사용자 [ApplicationData]\IsolatedStorage
컴퓨터 [CommonApplicationData]\IsolatedStorage

 

  • 대괄호 쌍으로 표시된 폴더의 실제 위치는 Environment.GetFolderPath 메서드로 알아낼 수 있다. 다음은 Windows Vista 이상에서의 기본 위치들이다.
범위 위치
지역 사용자 \Users\<사용자>\AppData\Local\IsolatedStorage
로밍 사용자 \Users\<사용자>\AppData\Roaming\IsolatedStorage
컴퓨터 \ProgramData\IsolatedStorage#

 

  • Windows XP에서는 다음과 같다.
범위 위치
지역 사용자 \Documents and Settings\<사용자>\Local Settings\Application Data\IsolatedStorage
로밍 사용자 \Documents and Settings\<사용자>\Application Data\IsolatedStorage
컴퓨터 \Documents and Settings\All Users\Application Data\IsolatedStorage

 

  • 이들은 단지 기준 폴더들일 뿐이다. 실제 자료 파일은 난해한 이름(어셈블리 이름의 해시 코드로 만든)을 가진 하위 디렉터리들의 미궁 속 깊은 곳에 들어 있다. 이는 격리된 저장소를 사용하는 것이 좋은 이유이자 좋지 않은 이유이다.
    • 한편으로는 불순한 응용 프로그램이 다른 응용 프로그램의 격리된 저장소에 접근하려는 시도를 그냥 상위 디렉터리 내용의 나열을 막기만 하면 효과적으로 차단할 수 있다는 (구체적인 디렉터리 이름을 추측하는 것이 어려우므로) 장점이 있지만, 또 한편으로는 응용 프로그램 외부에서 사용자가 응용 프로그램의 자료를 관리하기가 어렵다는 단점이 있다.
    • 응용 프로그램이 제대로 시동되게 하려면 XML 구성 파일을 메모장으로 편집하는 것이 편리한(또는 필수적인) 경우가 종종 있는데, 격리된 저장소 때문에 그렇게 하기가 불가능하거나 몹시 어렵다.

격리된 저장소 열거

  • IsolatedStorageFile 객체는 저장소에 있는 파일들을 나열하는 메서드들도 제공한다.
using (IsolatedStorageFile f = IsolatedStorageFile.GetUserStoreForDomain())
{
  using (var s = new IsolatedStorageFileStream("f1.x", FileMode.Create, f))
    s.WriteByte(123);

  using (var s = new IsolatedStorageFileStream("f2.x", FileMode.Create, f))
    s.WriteByte(123);

  foreach (string s in f.GetFileNames("*.*"))
    Console.Write(s + " ");  // f1.x f2.x
}
  • 파일은 물론이고 하위 디렉터리도 생성 또는 제거할 수 있다.
using (IsolatedStorageFile f = IsolatedStorageFile.GetUserStoreForDomain())
{
  f.CreateDirectory("subfolder");

  foreach (string s in f.GetFileNames("*.*"))
    Console.Write(s); 

  using (var s = new IsolatedStorageFileStream(@"subfolder\sub1.txt", FileMode.Create, f))
    s.WriteByte(100);

  f.DeleteFile(@"subfolder\sub1.txt");
  f.DeleteDirectory("subfolder");
}
  • 적절한 권한이 있다면 현재 사용자가 생성한 모든 격리된 저장소와 현재 컴퓨터의 모든 격리된 저장소를 나열하는 것도 가능하다. 이를 통해서 다른 프로그램의 고유한 자료에 접근할 수 있지만 그렇다고 이것이 사용자의 개인정보를 침해하는 것은 아니다. 다음은 이 기능을 사용하는 예이다.
System.Collections.IEnumerator rator = IsolatedStorageFile.GetEnumerator(IsolatedStorageScope.User);

while (rator.MoveNext())
{
  var isf = (IsolatedStorageFile) rator.Current;

  Console.WriteLine(isf.AssemblyIdentity);  // 강력한 이름 또는 URI
  Console.WriteLine(isf.CurrentSize);
  Console.WriteLine(isf.Scope);  // User 등
}
  • 다른 여러 GetEnmuerator 메서드들과는 달리, IsolatedStorageFile 클래스의 GetEnumerator는 인수를 하나 받는다(이 때문에 IsolatedStorageFile은 foreach와 호환되지 않는다). GetEnumerator의 인수로 사용할 수 있는 값은 다음 세 가지이다.
    • IsolatedStorageScope.User
      • 현재 사용자에 속한 모든 지역 저장소 구획을 열거한다.
    • IsolatedStorageScope.User | IsolatedStorageScope.Roaming
      • 현재 사용자에 속한 모든 로밍 저장소 구획을 열거한다.
    • IsolatedStorageScope.Machine
      • 현재 컴퓨터의 모든 컴퓨터 저장소 구획을 열거한다.
  • 일단 IsolatedStorageFile 객체를 얻고 나면 GetFiles와 GetDirectories를 호출해서 저장소의 파일들과 디렉터리들을 나열할 수 있다.
[ssba]

The author

지성을 추구하는 디자이너/ suyeongpark@abyne.com

댓글 남기기

This site uses Akismet to reduce spam. Learn how your comment data is processed.