본문 바로가기
프로그래밍/iOS

Objective-C에서 NSFileManger와 NSFileHandle을 이용하여 문자열을 파일로 저장하는 방법

by 백룡화검 2012. 9. 4.

출처 : http://blog.saltfactory.net/117


iPhone이나 Cocoa 애플리케이션을 개발하면 데이터 저장 방법에 대해서 고민을 하게 된다.  애플리케이션이 종 료했다가 다시 실행되었을 때 이전의 데이터를 다시 사용하고 싶기 때문이다. plist나 SQLite와 같은 저장 방법은 매우 효과적으로 구조화된 데이터를 저장하거나 읽어 올 수 있다. 애플리케이션이 크기 않거나 단순한 데이터를 저장하고 싶을 경우는 SQLite를 사용하지 않고 텍스트를 저장하거나 읽어올 수 있게 만들 수도 있다.

이 때 사용할 수 있는 가장 기본적인 방법이 바로 파일을 이용하는 방법이다. plist도 파일로 저장하는 것이지만 plist 특징상 XML 형태로 저장이 된다. 이 포스트에서 설명하는 텍스트 저장 방법은 단순하게 파일에 저장하는 방법이다.

예 제를 만들게 된 상황은 다른 개발자(B 개발자)에게 텍스트를 저장할 수 있는 모듈을 만들어 주는 상황이라고 가정하자. B 개발자는 UI 개발자로 단순하게 텍스트를 넘겨주면 알아서 저장할 수 있는 객체가 필요하고 저장이 완료되면 자동으로 딜리케이트 메소드를 호출할 수 있는 모듈이 필요하다고 A 개발자에게 요청을 한 상태이다.

A 개발자는 단순하게 텍스트를 저장할 수 있는 인터페이스를 가진 모듈을 만들어주기로 했다. iOS 나 Cocoa 개발을 할 때 파일을 관리할 수 있는 객체가 NSFileManager와 NSFileHandle이다. 이 두가지를 가지고 새로운 SFFileManager라는 객체를 만들고 딜리케이트 메소드를 사용할 수 있는 protocol을 정의했다. SFFileManager가 가지고 있는 메소드는 저장기능 (단순 덮어쓰기, 이어 쓰기)을 하는 -saveText:append:와 읽기 기능을 하는 -readText 그리고 빈파일로 만드는 clearFile 메소드를 가지고 있다. @protocol은 각각 메소드가 일을 처리한 후 동작할 수 있는 -fileManagerFailWithErorr:, -fileManagerDidSaveText:, -fileManagerDidReatText:, -fileManagerDidClearFile 을 정의하였다.

//

//  SFFileManager.h

//  SaltfactoryiOSLib

//

//  Created by SungKwang Song on 4/4/12.

//  Copyright (c) 2012 saltfactory@me.com. All rights reserved.

//


#import <Foundation/Foundation.h>



@protocol SFFileManagerDelegate;

@interface SFFileManager : NSObject

{

    id<SFFileManagerDelegate> delegate;

    NSURL *filePathURL;

}

@property (nonatomic, readonly) NSString *filePath;

- (id)initWithDelegate:(id<SFFileManagerDelegate>)aDelegate filename:(NSString *)aFilename;

- (void)saveText:(NSString *)text append:(BOOL)append;

- (void)readText;

- (void)clearFile;

@end


@protocol SFFileManagerDelegate <NSObject>

@required

- (void)fileManagerFailWithError:(NSError *)error;


@optional

- (void)fileManagerDidSaveText:(NSString *)text;

- (void)fileManagerDidReadText:(NSString *)text;

- (void)fileManagerDidClearFile;

@end

SFFileManager는 최초 생성될때 delegate 메소드를 처리할 수 있는 delegat 대상을 지정하게 하고 저장될 파일의 이름을 지정한다. iCloud가 적용되면서부터 NSDocumentDirectory에 저장하지말고 NSCacheDirectory로 저장하라고 권고하고 있기 때문에 저장 경로는 NSCacheDirectory로 한다. 이때, NSURL *filePathURL에 따로 저장을 하는데, 이유는 NSFileHandle에서 처리한 결과가 에러가 있는지 없는지 판단하기 위해서 NSString 형태의 path만 사용하는 것이 아니라 NSURL를 이용하려고 하기 때문이다. 뒤에 다른 메소드에서 이유에 대해서 다시 한번 설명하겠다.

- (id)initWithDelegate:(id<SFFileManagerDelegate>)aDelegate filename:(NSString *)aFilename

{

    self = [self init];

    if (self) {

        delegate = aDelegate;

        

        NSArray *pathList = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);

        filePath = [[pathList objectAtIndex:0] stringByAppendingPathComponent:aFilename];

        filePathURL = [NSURL fileURLWithPath:filePath];

        

        return self;

    } else {

        return nil;

    }

}

첫번째 텍스트를 파일에 저장하는 메소드를 살펴보자. NSFileManager를 이용해서 만약에 filePath에 파일이 없으면 파일을 하나 만든다. 그리고 이후에 인자로 받은 text를 파일에 저장하는데 이 때 NSFileHandle의 -fileHandleForUpdatingURL:error: 를 사용한다. -fileHandleForUpdatingPath: 를 사용할 수 도 있지만 파일 저장 작업에서 에러가 발생하면 -fileManagerFailWithErorr: 딜리게이트 메소드를 호출하고 에러가 없으면 저장후에 - fileManagerDidSaveText: 딜리게이트 메소드를 호출하게끔 하려고 하기 때문이다. -saveText:append: 메소드는 덮어쓰기와 기존의 텍스트 뒤에 추가하여 이어서 저장하기 기능을 지원하기 위해서 append 인자 값을 받는다. 이 값이 YES가 되면, 즉 이어서 저장하기는 NSFileHandle의 -seekToEndOfFile 메소드를 이용하여 파일 끝을 찾아서 저장하기를 한다. 만약 덮어 쓰기를 원하면 NSFileHandle 의 -truncateFileAtOffset: 메소드를 이용하여 어디서부터 truncate 시킬지를 정하는데 최초부터 모두 하겠다고 하고 다시 text를 저장하게 한다.

- (void)saveText:(NSString *)text append:(BOOL)append

{

    NSError *error = nil;


    NSData *data = [text dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];


    NSFileManager *fileManager = [NSFileManager defaultManager];


    if (![fileManager fileExistsAtPath:filePath]) {

        [fileManager createFileAtPath:filePath contents:nil attributes:nil];

    }

    

    NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingURL:filePathURL error:&error];

    

    if (error) {

        [fileHandle closeFile];

        [delegate fileManagerFailWithError:error];

    } else {

        if (append) {

            [fileHandle seekToEndOfFile];        

        } else {

            [fileHandle truncateFileAtOffset:0];    

        }

        [fileHandle writeData:data];

        [fileHandle closeFile];       

        

        [delegate fileManagerDidSaveText:text];

    }

    

}

다음은 파일에서 텍스트를 읽어오는 -readText 메소드를 살펴보자. 

NSFileHandle 의 -readDataToEndOfFile 메소드를 이용하여 파일의 끝까지 읽은 NSData를 NSUTF8StringEncoding으로 문자열로 변환해서 -fileManagerdidReadText: 딜리게이트 메소드를 문자열과 가지고 호출하게 한다.

- (void)readText

{

    NSError *error = nil;

    NSFileHandle *fileHandle = nil;


    fileHandle = [NSFileHandle fileHandleForUpdatingURL:filePathURL error:&error];

   

    if (error) {

        [fileHandle closeFile];

        [delegate fileManagerFailWithError:error];

    } else {

        NSData *data = [fileHandle readDataToEndOfFile];

        NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];    

        [fileHandle closeFile];

        [delegate fileManagerDidReadText:text];

    }

}

마지막으로 파일 내용을 모두 삭제하는 -clearText 메소드를 살펴보자. -saveText:append:에서 덮어서 저장하기에 사용했던 NSFileHandle의 -truncateFileAtOffset: 메소들 이용해서 파일의 내용을 truncate 시키고 파일을 닫는 것을 확인할 수 있다.

- (void)clearFile

{

    NSError *error = nil;

    NSFileHandle *fileHandle = nil;

    

    fileHandle = [NSFileHandle fileHandleForUpdatingURL:filePathURL error:&error];

    

    if (error) {

        [fileHandle closeFile];

        [delegate fileManagerFailWithError:error];

    } else {

        [fileHandle truncateFileAtOffset:0];

        [fileHandle closeFile];

        [delegate fileManagerDidClearFile];

    }

    

}


이제 간단한 FileManger를 만들었는데 실제 이상없이 동작하는지 단위테스트를 해보자. SFFileManger는 SFFileManagerDelegate 프로토콜을 사용하기 때문에 SenTestCase를 상속받은 테스트 객체에 <SFFileManagerDelegate> 프로토콜을 정의한다. 그리고 단위테스트는 delegate 메소드가 호출하는 메세지 콜을 처리하기 전에 끝나버리기 때문에 delegate 메소드 안에서 done이 YES가 될 때까지 RunLoop를 실행하도록 하는 특별한 변수를 하나 추가한다.

//

//  SaltfactoryiOSLibTests.h

//  SaltfactoryiOSLibTests

//

//  Created by SungKwang Song on 4/4/12.

//  Copyright (c) 2012 saltfactory@me.com. All rights reserved.

//


#import <SenTestingKit/SenTestingKit.h>

#import "SFFileManager.h"

@interface SaltfactoryiOSLibTests : SenTestCase<SFFileManagerDelegate>

{

    BOOL done;

    SFFileManager *fileManager;

    

}

@end

테 스트 메소드는 SFFileManager에 가지고 있는 세가지 메소드, -saveText:append:, -readText, -clearText 를 테스트하는 단위 테스트 메소드들과 <SFFileManagerDelegaet> 프로토콜이 가지고 있는 delegate 메소드들이 존재하고, 단위테스트 객체가 delegate 메소드에게 메세지를 보내어 정상적으로 처리가 완료될 때까지 기다리게하는 -waitForCompletion: 메소드가 존재한다. 각각의 단위테스트가 끝나고 확인해보면 NSCacheDirectory에 파일이 저장되어 진것을 확인하고 내용을 확인할 수 있을 것이다.

//

//  SaltfactoryiOSLibTests.m

//  SaltfactoryiOSLibTests

//

//  Created by SungKwang Song on 4/4/12.

//  Copyright (c) 2012 saltfactory@me.com. All rights reserved.

//


#import "SaltfactoryiOSLibTests.h"


@implementation SaltfactoryiOSLibTests


- (void)setUp

{

    [super setUp];

    

    // Set-up code here.

    fileManager = [[SFFileManager alloc] initWithDelegate:self filename:@"test.txt"];

    done = NO;

}


- (void)tearDown

{

    // Tear-down code here.

    

    [super tearDown];

}

#pragma mark - SFFileManager delegate methods

- (void)fileManagerFailWithError:(NSError *)error

{

    NSLog(@"error : %@", [error localizedDescription]);

    done = YES;

}


- (void)fileManagerDidSaveText:(NSString *)text

{

    NSLog(@"save success : %@", [fileManager filePath]);

    done = YES;

}


- (void)fileManagerDidReadText:(NSString *)text

{

    NSLog(@"read success : %@", text);

    done = YES;

}


- (void)fileManagerDidClearFile

{

    NSLog(@"clear success");

    done = YES;    

}


#pragma mark - SFFileManager test methods

- (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs {

    NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];

    

    do {

        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];

        if([timeoutDate timeIntervalSinceNow] < 0.0)

            break;

    } while (!done);

    

    return done;

}


- (void)testSaveText

{

    NSString *message = @"Hello, Saltfactory?\n";

    [fileManager saveText:message append:YES];

    [self waitForCompletion:1];

}


- (void)testReadText

{

    [fileManager readText];

    [self waitForCompletion:1];

}


- (void)testClearFile

{

    [fileManager clearFile];

    [self waitForCompletion:1];

}


@end


참고 

http://stackoverflow.com/questions/1077737/ocunit-test-for-protocols-callbacks-delegate-in-objective-c