Objective-C · 기본
Objective-C 문법과 Cocoa 기초
오류와 파일 - NSError 패턴
NSError ** 출력 매개변수 관례, 반환값 먼저 확인하기, NSException 은 프로그래머 실수용, NSFileManager, 파일 읽고 쓰기(stringWithContentsOfFile·writeToFile), 임시 폴더 경로
개발자KR · 원고 갱신
이 장에서 배우는 것
지금까지 만든 예제는 메모리 안에서만 돌았다. 프로그램이 끝나면 도서 데이터도 함께 사라진다. 실제 도구는 파일을 읽고 쓰고, 그 과정에서 파일이 없거나 내용이 깨져 있는 상황을 만난다. 이 장은 Objective-C가 이런 실패를 알리는 관례인 NSError ** 출력 매개변수를 익히고, 파일을 읽고 쓰는 기본 메서드와 임시 폴더 경로 다루는 법을 다룬다.
NSError **매개변수가 왜 포인터의 포인터인지 설명하고, 같은 모양의 메서드와 함수를 직접 만들 수 있다.- 실패 여부를 반환값으로 먼저 판단하고, 그다음에 오류 객체를 읽는 순서를 지킨다.
NSError와NSException이 각각 어떤 상황을 위한 것인지 구분한다.NSFileManager로 파일·폴더의 존재를 확인하고 폴더를 만들고 지우고 목록을 읽는다.stringWithContentsOfFile:encoding:error:와writeToFile:atomically:encoding:error:로 텍스트 파일을 읽고 쓴다.
문제 상황
사내 도서 대출 관리 도구에 장부 파일 기능을 붙인다고 하자. 장부는 한 줄에 책 한 권씩, "제목,재고" 형식의 텍스트 파일이다. 이 파일을 읽는 함수를 처음 만들면 보통 이런 모양이 된다.
NSDictionary *LoadLedger(NSString *path); // 실패하면 nil
실패하면 nil을 돌려준다는 약속만으로는 부족하다. 호출한 쪽은 nil을 받았을 때 "파일이 없다", "읽기 권한이 없다", "3번째 줄이 깨져 있다" 중 무엇인지 알 수 없다. 사용자에게 보여 줄 안내문도 달라지고, 프로그램이 이어서 할 일도 달라진다. 파일이 없으면 빈 장부로 시작하면 되지만 내용이 깨져 있으면 덮어쓰지 말고 멈춰야 한다.
그렇다고 실패마다 예외를 던져 프로그램 흐름을 끊는 방식은 Objective-C의 관례가 아니다. Cocoa는 "반환값으로 성공 여부를 알리고, 이유는 오류 객체에 담아 출력 매개변수로 돌려준다"는 방식을 쓴다. 이 장에서 그 방식을 익힌다.
NSError 출력 매개변수 관례
C의 출력 매개변수 다시 보기
C에서 함수는 값을 하나만 돌려줄 수 있다. 값을 하나 더 돌려주고 싶으면 호출자 변수의 주소를 넘기고, 함수가 그 주소에 값을 써 준다.
void SetTwo(int *out) {
*out = 2;
}
int n = 0;
SetTwo(&n); // n 은 2 가 된다
이렇게 함수가 값을 밖으로 내보내는 데 쓰는 매개변수를 출력 매개변수(out parameter)라고 한다. NSError **도 같은 원리다. 돌려줄 값이 "NSError 객체를 가리키는 포인터"이므로, 그 포인터 변수의 주소를 받으려고 별이 두 개 붙는다. 호출자는 NSError *error = nil;로 변수를 만들고 &error를 넘긴다. ARC 아래에서 이 매개변수는 컴파일러가 자동 해제 풀에 맞춰 다루므로, 호출자는 평범한 지역 변수 하나만 선언하면 된다.
반환값 먼저 확인하기
이 관례에서 가장 중요한 규칙은 순서다. 성공했는지는 항상 반환값으로 판단한다. 반환 타입이 객체이면 nil인지, BOOL이면 NO인지 본다. 반환값이 실패를 가리킬 때만 error를 읽는다. 성공했을 때 error 변수에 무엇이 들어 있는지는 보장되지 않기 때문이다. 앞 호출이 남긴 오류 객체가 그대로 있을 수도 있다.
호출하는 쪽의 기본 모양은 다음과 같다.
NSError *error = nil;
NSString *text = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:&error];
if (text == nil) {
// 여기서만 error 를 읽는다
}
NSError가 담고 있는 것
NSError 객체는 실패의 이유를 구조화해 담는다. 문자열 한 줄이 아니라 "어느 영역에서 어떤 번호의 실패인가"를 함께 담기 때문에 코드가 오류 종류를 가려 처리할 수 있다.
| 속성 | 타입 | 뜻 |
|---|---|---|
domain | NSString * | 오류가 속한 영역을 나타내는 이름 |
code | NSInteger | 그 영역 안에서 오류를 구분하는 번호 |
userInfo | NSDictionary * | 설명문 등 부가 정보를 담은 딕셔너리 |
localizedDescription | NSString * | 사람이 읽을 설명문 |
코드 번호는 영역이 다르면 같은 숫자도 다른 뜻이다. 그래서 code만 비교하지 말고 domain과 함께 본다. 시스템이 만드는 파일 오류는 보통 Cocoa 영역에 속한다. 번호의 구체적인 값은 NSError 공식 문서에서 확인할 수 있다.
직접 오류를 만들고 전달하기
우리가 만드는 함수도 같은 관례를 따르게 할 수 있다. 영역 이름은 문자열 상수로 정하고, 오류 번호는 열거형으로 정한다. 오류 객체는 errorWithDomain:code:userInfo:로 만든다. 설명문은 NSLocalizedDescriptionKey 키에 넣는다.
if (error != NULL) {
*error = [NSError errorWithDomain:@"com.example.loantool"
code:1
userInfo:@{NSLocalizedDescriptionKey: @"설명"}];
}
return nil;
여기서 error != NULL 검사를 빼면 안 된다. 호출자가 오류 이유에 관심이 없으면 error:NULL을 넘길 수 있고, 그때 *error에 대입하면 프로그램이 죽는다. 또 다른 함수를 호출하다 실패했을 때는 받은 error 포인터를 그대로 넘겨 주면 된다. 아래 완성 코드에서 LoadLedger가 ParseLedger에, ParseLedger가 ParseCount에 같은 error를 넘기는 부분이 그 예다. 안쪽에서 만든 오류 객체가 바깥 호출자의 변수에 바로 들어간다.
NSException은 프로그래머 실수용
Objective-C에는 @try, @catch, @throw로 다루는 예외(NSException)도 있다. 하지만 Cocoa에서 예외는 "코드가 잘못 짜였다"는 신호로 쓰인다. 배열 범위 밖 인덱스에 접근하거나, 딕셔너리에 nil을 넣거나, 존재하지 않는 메서드를 보내면 예외가 난다. 이런 상황은 사용자가 상황을 바꿔서 해결할 수 없다. 코드를 고쳐야 한다.
| 구분 | NSError | NSException |
|---|---|---|
| 원인 | 파일 없음, 잘못된 입력 같은 실행 환경의 실패 | 범위 밖 인덱스, nil 삽입 같은 코드 실수 |
| 전달 방법 | 반환값과 출력 매개변수 | 던져서 호출 스택을 거슬러 올라감 |
| 호출자의 대응 | 반환값을 보고 분기해서 계속 진행 | 코드를 고친다 |
따라서 파일이 없다고 @try로 감싸는 것은 맞지 않다. 파일 API는 예외를 던지지 않고 NSError로 알린다. 반대로 인덱스를 확인하지 않아 난 예외를 @catch로 덮는 것도 문제를 숨길 뿐이다. 이 장의 완성 코드에는 예외가 어떤 모양으로 나타나는지 한 번 보여 주려고 일부러 범위 밖 인덱스를 쓰는 부분이 있다. 이것은 관찰용이고, 실제 코드에서는 index < array.count를 먼저 확인한다. 또 ARC는 예외가 던져지는 경로에서 객체를 정리해 주지 않을 수 있으므로, 예외를 잡고 계속 실행하는 구조는 설계 단계부터 피한다.
파일과 폴더 다루기
NSFileManager
NSFileManager는 파일 시스템 작업을 맡는 클래스다. 보통 공유 인스턴스를 [NSFileManager defaultManager]로 얻어 쓴다. 이 장에서 쓰는 메서드는 다음과 같다.
| 메서드 | 반환 | 하는 일 |
|---|---|---|
fileExistsAtPath: | BOOL | 해당 경로에 무언가 있는지 확인 |
fileExistsAtPath:isDirectory: | BOOL | 있는지와 폴더인지를 함께 확인 |
createDirectoryAtPath:withIntermediateDirectories:attributes:error: | BOOL | 폴더 생성 |
contentsOfDirectoryAtPath:error: | NSArray * | 폴더 안 이름 목록 |
removeItemAtPath:error: | BOOL | 파일이나 폴더 삭제 |
fileExistsAtPath:isDirectory:도 출력 매개변수를 쓴다. 이쪽은 BOOL *이다. 반환값이 NO이면 폴더 여부 값은 의미가 없다. 같은 규칙, 곧 반환값 먼저 확인하는 규칙이 여기에도 적용된다. 한편 contentsOfDirectoryAtPath:error:가 돌려주는 이름 순서는 정해져 있지 않다. 출력이 항상 같아야 하는 프로그램에서는 정렬한 뒤 쓴다.
텍스트 파일 읽고 쓰기
작은 텍스트 파일은 NSString의 메서드로 한 번에 읽고 쓴다.
NSString *text = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:&error];
BOOL ok = [text writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:&error];
인코딩(encoding)은 글자를 바이트로 바꾸는 규칙이다. 읽을 때와 쓸 때 같은 규칙을 써야 글자가 깨지지 않는다. 한국어가 들어가는 텍스트에는 NSUTF8StringEncoding을 쓴다. atomically:YES는 먼저 임시 파일에 전부 쓴 뒤 이름을 바꿔 최종 위치에 놓는다는 뜻이다. 쓰는 도중 문제가 생겨도 원래 파일이 반쯤 덮어쓰인 상태로 남지 않는다. 인코딩 인자와 error: 인자가 없는 옛 형태의 메서드(stringWithContentsOfFile:, 오류를 받지 않는 writeToFile:atomically:)는 더 이상 쓰지 않는 것으로 분류되어 컴파일 경고를 낸다. 이 책에서는 항상 error:가 있는 형태를 쓴다.
이 방식은 파일 전체를 메모리에 올린다. 장부처럼 작은 파일에는 적당하고, 아주 큰 파일을 줄 단위로 처리하는 방법은 이 책의 범위를 넘는다.
임시 폴더 경로
연습용 파일을 /tmp/ledger.txt처럼 고정 경로에 쓰면 다른 운영체제에서 안 되거나 다른 사용자의 파일과 겹칠 수 있다. 시스템이 사용자별로 정해 둔 임시 폴더는 NSTemporaryDirectory()가 문자열로 돌려준다. 이 경로는 실행 환경마다 다르고 끝에 슬래시가 붙어 있을 수도 있으므로 문자열을 직접 이어 붙이지 않는다. 대신 stringByAppendingPathComponent:를 쓰면 구분자를 알아서 맞춰 준다.
NSString *dir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"objc-ch11-ledger"];
NSString *path = [dir stringByAppendingPathComponent:@"ledger.txt"];
임시 경로는 실행할 때마다 값이 다를 수 있으므로 결과로 출력하지 않는다. 출력이 항상 같아야 하는 예제에서는 파일 이름만 보여 준다.
결과는 표준 출력으로, NSLog는 표준 오류로
이 책은 결과를 printf("%s\n", [문자열 UTF8String])으로 출력한다. NSLog는 출력 앞에 시각과 프로세스 이름, 번호를 붙이고 표준 오류(stderr)로 내보낸다. 실행할 때마다 앞부분이 달라지고, ./app > result.txt로 결과를 파일에 저장할 때도 섞이지 않는다. 그래서 프로그램의 결과는 표준 출력으로, 진단 메시지는 NSLog로 나눈다. 아래 코드에서 NSLog는 예상치 못한 실패로 프로그램이 멈추는 경로에만 쓴다.
완성 코드
임시 폴더에 장부 파일 세 개를 만들어 읽고, 실패 세 가지(숫자가 아닌 재고, 필드 수가 틀린 줄, 없는 파일)와 예외 하나를 관찰한 뒤 폴더를 정리한다. 파일은 main.m 하나다. 리눅스에서는 GNUstep의 컴파일 옵션이 필요하고, 아래 명령은 macOS 기준이다.
#import <Foundation/Foundation.h>
static NSString *const LoanErrorDomain = @"com.example.loantool";
typedef NS_ENUM(NSInteger, LoanErrorCode) {
LoanErrorBadCount = 1,
LoanErrorBadLine = 2
};
static void Say(NSString *text) {
printf("%s\n", [text UTF8String]);
}
static NSError *MakeLoanError(LoanErrorCode code, NSString *message) {
return [NSError errorWithDomain:LoanErrorDomain
code:code
userInfo:@{NSLocalizedDescriptionKey: message}];
}
static void ReportError(NSString *label, NSError *error) {
Say([NSString stringWithFormat:@"%@: %@ / 코드 %ld / %@",
label, error.domain, (long)error.code, error.localizedDescription]);
}
static int Abort(NSString *what, NSError *error) {
NSLog(@"%@ 실패: %@", what, error);
return 1;
}
static NSNumber *ParseCount(NSString *text, NSError **error) {
NSCharacterSet *notDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet];
if (text.length == 0 || [text rangeOfCharacterFromSet:notDigits].location != NSNotFound) {
if (error != NULL) {
*error = MakeLoanError(LoanErrorBadCount,
[NSString stringWithFormat:@"재고 수량이 숫자가 아니다: %@", text]);
}
return nil;
}
return @([text integerValue]);
}
static NSDictionary<NSString *, NSNumber *> *ParseLedger(NSString *text, NSError **error) {
NSMutableDictionary<NSString *, NSNumber *> *books = [NSMutableDictionary dictionary];
NSArray<NSString *> *lines = [text componentsSeparatedByString:@"\n"];
for (NSUInteger i = 0; i < lines.count; i++) {
NSString *line = lines[i];
if (line.length == 0) {
continue;
}
NSArray<NSString *> *parts = [line componentsSeparatedByString:@","];
if (parts.count != 2) {
if (error != NULL) {
*error = MakeLoanError(LoanErrorBadLine,
[NSString stringWithFormat:@"%lu번째 줄: 필드가 %lu개",
(unsigned long)(i + 1), (unsigned long)parts.count]);
}
return nil;
}
NSNumber *count = ParseCount(parts[1], error);
if (count == nil) {
return nil;
}
books[parts[0]] = count;
}
return books;
}
static NSDictionary<NSString *, NSNumber *> *LoadLedger(NSString *path, NSError **error) {
NSString *text = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:error];
if (text == nil) {
return nil;
}
return ParseLedger(text, error);
}
static BOOL SaveText(NSString *text, NSString *path, NSError **error) {
return [text writeToFile:path
atomically:YES
encoding:NSUTF8StringEncoding
error:error];
}
int main(void) {
@autoreleasepool {
NSFileManager *fm = [NSFileManager defaultManager];
NSString *dir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"objc-ch11-ledger"];
NSError *error = nil;
if ([fm fileExistsAtPath:dir]) {
if (![fm removeItemAtPath:dir error:&error]) {
return Abort(@"이전 폴더 삭제", error);
}
}
if (![fm createDirectoryAtPath:dir
withIntermediateDirectories:YES
attributes:nil
error:&error]) {
return Abort(@"폴더 생성", error);
}
Say(@"폴더 준비 완료");
NSString *path = [dir stringByAppendingPathComponent:@"ledger.txt"];
NSString *badCountPath = [dir stringByAppendingPathComponent:@"bad-count.txt"];
NSString *badLinePath = [dir stringByAppendingPathComponent:@"bad-line.txt"];
if (!SaveText(@"Cocoa 입문,3\nC 언어 노트,0\n블록 정복,2\n", path, &error)) {
return Abort(@"장부 저장", error);
}
if (!SaveText(@"Cocoa 입문,3\n블록 정복,많음\n", badCountPath, &error)) {
return Abort(@"재고 오류 파일 저장", error);
}
if (!SaveText(@"Cocoa 입문,3\n제목만 있음\n", badLinePath, &error)) {
return Abort(@"형식 오류 파일 저장", error);
}
Say(@"저장 성공: ledger.txt");
NSError *loadError = nil;
NSDictionary<NSString *, NSNumber *> *books = LoadLedger(path, &loadError);
if (books == nil) {
return Abort(@"장부 읽기", loadError);
}
Say([NSString stringWithFormat:@"도서 %lu종을 읽었다", (unsigned long)books.count]);
for (NSString *title in [books.allKeys sortedArrayUsingSelector:@selector(compare:)]) {
Say([NSString stringWithFormat:@" %@: %@권", title, books[title]]);
}
NSError *countError = nil;
if (LoadLedger(badCountPath, &countError) == nil) {
ReportError(@"재고 오류", countError);
}
NSError *lineError = nil;
if (LoadLedger(badLinePath, &lineError) == nil) {
ReportError(@"형식 오류", lineError);
}
NSString *missingPath = [dir stringByAppendingPathComponent:@"missing.txt"];
NSError *missingError = nil;
NSDictionary<NSString *, NSNumber *> *none = LoadLedger(missingPath, &missingError);
Say([NSString stringWithFormat:@"없는 파일: 반환값 %@, 오류 객체 %@",
none == nil ? @"nil" : @"있음", missingError != nil ? @"있음" : @"없음"]);
Say([NSString stringWithFormat:@"NULL 전달: 반환값 %@",
LoadLedger(missingPath, NULL) == nil ? @"nil" : @"있음"]);
NSArray *shelf = @[@"A", @"B"];
NSUInteger index = shelf.count + 3;
@try {
Say(shelf[index]);
} @catch (NSException *exception) {
Say([NSString stringWithFormat:@"예외 이름: %@", exception.name]);
}
BOOL isDir = NO;
BOOL exists = [fm fileExistsAtPath:dir isDirectory:&isDir];
Say([NSString stringWithFormat:@"폴더 존재: %@, 폴더 여부: %@",
exists ? @"예" : @"아니오", isDir ? @"예" : @"아니오"]);
NSArray<NSString *> *names = [fm contentsOfDirectoryAtPath:dir error:&error];
if (names == nil) {
return Abort(@"목록 읽기", error);
}
Say([NSString stringWithFormat:@"폴더 내용: %@",
[[names sortedArrayUsingSelector:@selector(compare:)] componentsJoinedByString:@" "]]);
if (![fm removeItemAtPath:dir error:&error]) {
return Abort(@"정리", error);
}
Say([NSString stringWithFormat:@"정리 후 폴더 존재: %@",
[fm fileExistsAtPath:dir] ? @"예" : @"아니오"]);
}
return 0;
}
줄별 해설
상수와 열거형. LoanErrorDomain은 우리 도구의 오류 영역 이름이다. 오류 번호는 숫자를 그대로 쓰지 않고 LoanErrorCode 열거형에 이름을 붙였다. 호출자가 error.code == LoanErrorBadLine처럼 읽을 수 있다.
Say, ReportError, Abort. Say는 표준 출력으로 한 줄을 내보내는 도우미다. ReportError는 영역, 번호, 설명문을 한 줄로 만든다. Abort는 예상치 못한 실패일 때 NSLog로 표준 오류에 기록하고 종료 코드 1을 돌려준다. main에서는 return Abort(...) 형태로 쓴다.
ParseCount. 숫자가 아닌 글자가 하나라도 있거나 빈 문자열이면 실패다. 실패하면 error != NULL일 때만 *error에 오류 객체를 넣고 nil을 돌려준다. 반환 타입이 NSInteger가 아니라 NSNumber *인 까닭은, 재고 0이 정상 값이라 숫자 자체로는 "실패"를 표현할 수 없기 때문이다. 객체 반환이면 nil이 실패 표시가 된다.
ParseLedger. 텍스트를 줄로 나누고 빈 줄은 건너뛴다. 쉼표로 나눈 조각이 두 개가 아니면 LoanErrorBadLine 오류를 만든다. 재고 변환이 실패하면 ParseCount에 넘긴 error에 이미 오류가 들어 있으므로 새로 만들지 않고 nil만 돌려준다. 이것이 오류 전파다.
LoadLedger. 파일 읽기 메서드에 받은 error를 그대로 넘긴다. 읽기에 실패하면 그 메서드가 직접 오류 객체를 채운다. 성공하면 같은 error를 파싱 단계로 넘긴다.
SaveText. writeToFile:...error:의 BOOL 결과를 그대로 돌려주는 얇은 함수다. 같은 관례를 가진 함수로 감싸도 모양이 유지된다는 점을 보여 준다.
main의 준비 단계. 같은 이름의 폴더가 남아 있으면 지우고 새로 만든다. 폴더 생성의 withIntermediateDirectories:YES는 중간 폴더가 없으면 함께 만들겠다는 뜻이다. 모든 호출은 반환값이 NO일 때만 error를 쓴다.
저장과 읽기. 정상 장부 하나와 일부러 깨뜨린 파일 둘을 저장한다. 정상 장부를 읽은 뒤에는 키를 정렬해 출력한다. 딕셔너리는 순서가 정해져 있지 않기 때문이다.
실패 관찰. bad-count.txt는 두 번째 줄 재고가 "많음"이라 재고 오류가 나고, bad-line.txt는 두 번째 줄에 쉼표가 없어 형식 오류가 난다. 없는 파일은 반환값이 nil이고 오류 객체가 채워진다. 오류 번호와 설명문은 운영체제가 정하는 값이라 출력하지 않고 "객체가 있다"만 확인한다. NULL을 넘기면 오류 객체를 받지 않고도 안전하게 nil만 돌려받는다.
예외 관찰. 길이 2인 배열에 인덱스 5로 접근해 NSRangeException을 일으킨다. 인덱스를 변수로 둔 것은 컴파일러가 상수 범위 오류를 미리 경고하지 않게 하기 위해서다. 이것은 예외의 모양을 보기 위한 장치일 뿐이다.
폴더 확인과 정리. fileExistsAtPath:isDirectory:로 폴더임을 확인하고, 이름 목록을 정렬해 출력한다. 마지막에 폴더를 지우고 사라졌는지 다시 확인한다.
실행 결과
$ clang -fobjc-arc -Wall -framework Foundation main.m -o app
$ ./app
폴더 준비 완료
저장 성공: ledger.txt
도서 3종을 읽었다
C 언어 노트: 0권
Cocoa 입문: 3권
블록 정복: 2권
재고 오류: com.example.loantool / 코드 1 / 재고 수량이 숫자가 아니다: 많음
형식 오류: com.example.loantool / 코드 2 / 2번째 줄: 필드가 1개
없는 파일: 반환값 nil, 오류 객체 있음
NULL 전달: 반환값 nil
예외 이름: NSRangeException
폴더 존재: 예, 폴더 여부: 예
폴더 내용: bad-count.txt bad-line.txt ledger.txt
정리 후 폴더 존재: 아니오
실무에서 자주 틀리는 것
오류 객체만 보고 성공 여부를 판단한다
틀린 코드:
NSError *error = nil;
NSString *text = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:&error];
if (error == nil) {
Say(text); // 성공했다고 가정
}
성공 시 error가 nil로 유지된다는 보장은 없다. 반대로 성공했는데 값이 남아 있을 수도 있다. 판단은 반환값으로 한다.
고친 코드:
if (text != nil) {
Say(text);
} else {
ReportError(@"읽기", error);
}
*error에 대입하기 전에 NULL을 확인하지 않는다
틀린 코드:
static NSNumber *Parse(NSString *s, NSError **error) {
*error = MakeLoanError(LoanErrorBadCount, @"숫자 아님"); // error 가 NULL 이면 비정상 종료
return nil;
}
호출자가 오류에 관심이 없어 NULL을 넘기는 것은 정상 사용이다. 그때 NULL 주소에 쓰면 프로그램이 멈춘다.
고친 코드:
if (error != NULL) {
*error = MakeLoanError(LoanErrorBadCount, @"숫자 아님");
}
return nil;
파일 오류를 예외로 처리하려 한다
틀린 코드:
@try {
NSString *text = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:NULL];
Say(text); // 파일이 없으면 nil, 예외는 나지 않는다
} @catch (NSException *e) {
Say(@"읽기 실패"); // 실행되지 않는다
}
파일 API는 실패를 반환값과 NSError로 알리며 예외를 던지지 않는다. @catch는 한 번도 실행되지 않고, nil은 그대로 Say로 넘어간다.
고친 코드:
NSError *error = nil;
NSString *text = [NSString stringWithContentsOfFile:path
encoding:NSUTF8StringEncoding
error:&error];
if (text == nil) {
ReportError(@"읽기", error);
return 1;
}
Say(text);
임시 경로를 문자열로 이어 붙이거나 고정 경로를 쓴다
틀린 코드:
NSString *path = [NSTemporaryDirectory() stringByAppendingString:@"ledger.txt"];
NSString *other = @"/tmp/ledger.txt";
임시 폴더 경로 끝에 슬래시가 없으면 첫 줄은 ...Tledger.txt처럼 이름이 붙어 버리고, 둘째 줄은 환경에 따라 쓸 수 없거나 다른 파일과 겹친다.
고친 코드:
NSString *dir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"objc-ch11-ledger"];
NSString *path = [dir stringByAppendingPathComponent:@"ledger.txt"];
한눈에 보기
| API | 성공 판단 | 실패 시 | 쓰임 |
|---|---|---|---|
stringWithContentsOfFile:encoding:error: | 반환값이 nil이 아님 | nil 반환, error 채움 | 텍스트 파일 읽기 |
writeToFile:atomically:encoding:error: | 반환값이 YES | NO 반환, error 채움 | 텍스트 파일 쓰기 |
createDirectoryAtPath:...error: | 반환값이 YES | NO 반환, error 채움 | 폴더 만들기 |
removeItemAtPath:error: | 반환값이 YES | NO 반환, error 채움 | 파일·폴더 지우기 |
NSTemporaryDirectory() | 항상 문자열 | 없음 | 임시 폴더 경로 얻기 |
| 규칙 | 이유 |
|---|---|
| 반환값 먼저 확인한다 | 성공 시 error 값은 보장되지 않는다 |
*error 대입 전 NULL을 확인한다 | 호출자가 NULL을 넘길 수 있다 |
받은 error는 안쪽 호출에 그대로 넘긴다 | 안쪽 오류가 바깥 변수에 바로 담긴다 |
| 예외는 잡아서 계속 실행하지 않고 코드를 고친다 | 예외는 프로그래머 실수의 신호다 |
결과는 printf, 진단은 NSLog | NSLog는 부가 정보가 붙고 표준 오류로 나간다 |
연습 문제
static BOOL RemoveIfExists(NSString *path, NSError **error)함수를 작성하라. 경로에 아무것도 없으면 아무 일도 하지 않고YES를 돌려주고, 있으면 지운 결과를 돌려준다.- 다음 함수의 문제를 두 가지 찾아 고쳐라.
static NSString *ReadTitle(NSString *path, NSError **error) { NSString *text = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:NULL]; if (text.length == 0) { *error = [NSError errorWithDomain:@"x" code:1 userInfo:nil]; } return text; } ParseCount를 고쳐서 재고가 1000 이상이면 코드 3(LoanErrorTooMany)의 오류를 돌려주게 하라. 열거형에 값을 추가하는 부분도 보여라.- 다음 세 상황을
NSError로 처리할 것과 코드를 고쳐야 할 것으로 나누고 이유를 한 줄씩 써라. (가) 사용자가 지정한 장부 파일이 없다. (나)NSMutableArray의removeObjectAtIndex:에 길이보다 큰 번호를 넘겼다. (다) 장부 파일에 쉼표가 없는 줄이 있다.
정답과 해설
- 존재 확인을 먼저 하고, 삭제 결과를 그대로 넘긴다.
받은static BOOL RemoveIfExists(NSString *path, NSError **error) { NSFileManager *fm = [NSFileManager defaultManager]; if (![fm fileExistsAtPath:path]) { return YES; } return [fm removeItemAtPath:path error:error]; }error를 그대로 넘기므로NULL검사를 직접 할 필요가 없다. 직접 오류를 만들 때만 검사가 필요하다. - 첫째, 읽기 메서드에
NULL을 넘겨서 읽기 실패의 이유가 사라진다. 받은error를 넘겨야 한다. 둘째,*error대입 전에NULL확인이 없고, 읽기 자체가 실패해text가nil일 때와 빈 파일일 때를 구분하지 않는다.static NSString *ReadTitle(NSString *path, NSError **error) { NSString *text = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:error]; if (text == nil) { return nil; } if (text.length == 0) { if (error != NULL) { *error = [NSError errorWithDomain:@"x" code:1 userInfo:nil]; } return nil; } return text; } - 열거형에
LoanErrorTooMany = 3을 추가하고, 숫자 검사를 통과한 뒤 값을 확인한다.
숫자 검사 이후에 변환하므로NSInteger value = [text integerValue]; if (value >= 1000) { if (error != NULL) { *error = MakeLoanError(LoanErrorTooMany, [NSString stringWithFormat:@"재고 수량이 너무 크다: %@", text]); } return nil; } return @(value);integerValue가 의미 있는 값을 준다. - (가)
NSError: 사용자가 경로를 고치면 해결되는 실행 환경의 실패다. (나) 코드 수정: 인덱스가 범위 안인지 먼저 확인하지 않은 프로그래머 실수이고 예외가 난다. (다)NSError: 입력 데이터의 문제이고, 어느 줄이 틀렸는지 알려 주고 계속 진행하거나 멈출 수 있다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.