FileHandle 사용법

NSData 클래스에는 해당 객체의 데이터를 URL에 기록하는 write(to:atomically:) 라는 메소드가 존재한다. 이 때 atomic하게 쓴다는 말은 온전한 데이터를 기록하거나 혹은 아예 기록하지 않거나 둘 중 하나의 결과만 존재하며, 그 중간인 부분적으로 소실되어 잘못된 데이터가 존재하지 않는다는 것을 말한다.

NSDataNSMutableData의 값 시멘틱 타입인 Data에는 해당 메소드가 존재하지 않는다. 대신 write(to:options:) 메소드가 존재하며, NSData.WritingOptions 타입에 .atomic 값이 정의되어 있어서 동일한 동작을 구현하는데에는 문제가 없다.

파일을 atomic하게 기록하는 방법은 임시 파일을 사용하는 것이다. 하나 혹은 그 이상의 임시 파일을 생성하여 데이터를 기록한 후에, 이상 없이 모든 데이터를 기록하게되면 해당 임시 파일을 최종 목적 파일로 교체하거나 복사하는 방식으로 파일 데이터가 중간에 깨지지 않게 하는 것이다.

문제는 앱 번들 외부의 공개된 위치에 파일을 기록할 수 있는 앱을 사용할 때이다. 공개된 위치는 언제나 공격의 대상이 될 수 있기 때문에 이 메소드를 공개된 위치에 파일을 기록할 때 사용하는 것은 위험하다. 일례로 공격자가 임시 디렉토리 내에 특정한 임시 파일을 다른 위치의 파일에 대한 하드링크 혹은 심볼릭 링크로 대체하게 되면, 파일에 기록하는 행위가 시스템을 손상시키는 원인이 될 수 있다.

따라서 이와 같은 경우에는 FileHandle을 사용할 것이 권장된다. FileHandle은 파일 디스크립터를 객체 지향 API로 감싼 얇은 래퍼이다. 특정 경로 및 URL을 기준으로 쓰기/읽기 및 변경용으로 생성할 수 있으며, 동기 및 비동기식 파일 액세스를 지원한다.

파일 핸들 생성하기

파일 핸들을 생성하는 방법은 경로 문자열을 사용하거나 URL을 사용하는 방법이 있다. 특이한 점은 경로 문자열을 사용하는 이니셜라이저는 옵셔널 값을 리턴하며, URL을 사용하는 방법은 예외를 던지는 식으로 동작한다는 점이다.  또 파일 핸들은 쓰기, 읽기, 편집 모드 중 하나로 열 수 있다. 기본적으로 파일 디스크립터를 래핑하는 클래스이므로, 기존에 열려진 파일 디스크립터가 있으면 이를 기반으로 생성할 수 있다. 기본적인 이니셜라이저들은 다음과 같다.

  • init?(forReadingAtPath: String)
  • init(forReadingFrom: URL) throws
  • init?(forWritingAtPath: String)
  • init(forWritingTo: URL) throws
  • init?(forUpdatingAtPath: Stirng)
  • init(forUpdating: URL) throws
  • init(fileDescriptor: Int32

기본 입출력에 대한 파일 디스크립터에 대해서는 클래스가 기본적으로 제공하는 공유 인스턴스가 있다. .standardOutput, .standardInput, .standardError, .nullDevice 등을 사용할 수 있다.

참고로 파일 핸들을 패스나 URL을 통해서 생성할 때에는 저장소 내에 해당 URL이 반드시 존재해야 한다는 것이다. 존재하지 않는 URL에 대해서는 파일 핸들을 생성할 수 없다. 따라서 미리 파일 디스크립터를 생성하면서 파일을 만들거나, FileManager를 통해서 빈 파일을 하나 생성해야 한다.

파일 읽기

기본적으로 파일을 특정 길이만큼 읽거나 파일의 끝까지 읽을 수 있다.

  • readData(ofLength: Int) -> Data
  • readDataToTheEndOfFile() -> Data

이 동작들은 모두 동기식으로 동작한다. 파일 읽기와 관련해서는 .availableData 프로퍼티가 있는데, 해당 프로퍼티를 액세스하는 시점에 readDataToEndOfFile()이 호출된다. 만약 읽기가 모종의 이유에 의해 지연된다면 해당 스레드가 블럭될 수 있다. (단, 읽을 수 없는 이유가 파일의 끝까지 이미 읽었던 경우라면 예외다. 이 경우에는 빈 데이터 객체가 리턴된다.)

파일을 읽어들이는 동작을 수행하는 중에 특정한 코드를 실행하고 싶다면 readablilityHandler 속성을 지정할 수 있다. 이 속성은 기본적으로 nil 이지만, 만약 (FileHandle) -> Void 타입의 클로저가 지정되면 파일에서 데이터 조각을 읽을 수 있을 때마다 해당 핸들러가 실행된다. (다시 nil로 변경하면 스케줄된 핸들러가 제거된다.) 즉 이 방법은 소켓등의 읽기 전 대기가 필요한 핸들러에 사용하기 적합한 방식이다.

그외에 비동기식으로 파일을 액세스하는 방법으로는 비동기 액세스 메소드들을 사용하는 것이다. 이들은 모두 완료 후 노티피케이션을 발송한다. 따라서 적절한 컨트롤러를 관련된 노티피케이션의 옵저버로 미리 등록해두어야 한다.

  • acceptConnectionInBackgroundAndNotify()
  • readInBackgroundAndNotify()
  • readToEndOfFileInBackgroundAndNotify()
  • waitForDataInBackgroundAndNotify()

예를 들어 readInBackgroundAndNotify() 메소드는 파일을 읽은 후에 .readCompletionNotification을 발송하며, 이 노티피케이션의 userInfo 에는 다음과 같은 키-값 정보가 들어있다.

  • NSFileHandleNotificationDataItem : 읽어들인 데이터가 NSData 로 들어있음
  • "NSFileHandleError" : 에러 값

파일 내 탐색

offsetInFile 프로퍼티는 현재 열린 파일 내에서의 포인터의 오프셋 값을 가리킨다. 이 값은 읽기 전용이며, 포인터를 옮기기 위해서는 seek(toFileOffset:) 이나 seekToEndOfFile()을 사용한다.

파일에 쓰기

파일에 쓰는 것은 동기식으로 write(_: Data)를 호출하는 방법이 있다.

닫기와 동기화

파일을 열었으면 반드시 닫아야 한다. closeFile() 메소드가 이 역할을 한다. 만약 메모리 버퍼의 내용과 파일을 동기화할 필요가 있다면, 파일을 닫기 전에 synchrozieFile()을 호출한다. 파이프를 열었던 경우에 flush() 해야 한다면 이 synchronizeFile()을 호출해주면 같은 효과를 볼 수 있다.

읽기 핸들과 쓰기 핸들

파일 디스크립터를 그대로 래핑하는 클래스이기 때문에 읽기모드와 쓰기 모드 중 어느것으로 파일을 열었는지를 프로그래머가 직접 기억하고 있어야 한다. 이를 구분하기 위해서는 각각을 서브클래싱하기보다는 Enum을 사용해서 구분하는 것도 좋은 방법이다.