NSLog
Objective-C에서 로그를 찍어보기 위해서는 NSLog를 사용한다.(print 디버깅 신공)
NSLog는 Foundation 프레임워크의 일부로 전역함수라 어디서든 호출이 가능하다.
형식지정자를 통해 문자열 포메팅이 가능하고, 해당 문자열을 표준 에러 출력으로 보낸다.
다만 불필요한 로그가 너무 많이 찍히면 성능이 떨어질 수 있고,
실기기나 배포버전에서 로그가 찍히는 것은 바람직하지 않기 때문에
아래와 같이 디버깅 모드에서만 로그가 출력되도록 할 수 있다.
(기본은 디버깅 모드, Release모드로 변경하려면 아래 경로 참고)
xCode 상단 탭 product - scheme - Edit scheme - Run- Build-configuration (release 선택)
#if DEBUG == 0
#define DebugLog(...)
#elif DEBUG == 1
#define DebugLog(...) NSLog(__VA_ARGS__)
#endif
#import <Foundation/Foundation.h>
int main(void) {
NSLog(@"OBJC");
DebugLog(@"print when debug only");
NSLog(@"print always");
return 0;
}
NSError
Objective-C에서 에러 핸들링은 Foundation 프레임워크의 NSError class를 활용한다.
NSError 클래스의 구성요소
Domain
에러 도메인은 오류의 범주를 나타내는 문자열이다. (어느 부분의 에러인가?)
(미리 정의된 NSError 도메인이거나 커스텀 도메인이며 nil이어서는 안된다.)
Code
도매인 내에서 구체적인 에러의 유형을 나타내는 정수이다.
User Info
오류에 대한 추가 정보를 제공하는 딕셔러니
오류 메세지나 관련 객체에 대한 설명이 포함될 수 있으며 nil일 수 있다.
*nil: 객체 포인터가 유효한 객체를 가리키고 있지 않음을 나타내는 값. (null 포인터와 유사하다.)
에러 생성은 아래와 같이 수행할 수 있다.
#import <Foundation/Foundation.h>
int main(void) {
NSString *domain = @"com.ITCompany.MyApp.ErrorDomain";
NSString *desc = NSLocalizedString(@"failed to complete", @"retry");
NSDictionary *userInfo = @{NSLocalizedDescriptionKey: desc};
NSError *error = [NSError errorWithDomain:domain code:-101 userInfo:userInfo];
}
싱글 포인터와 이중 포인터
에러를 사용하는 예시코드를 살펴보기 전에, 싱글 포인터와 더블 포인터에 대해 정리하고 넘어가려고 한다.
특히나 함수에 파라미터로 포인터를 전달하는 경우 동작방식에 대해 이해하는 것이 중요하다고 느껴졌기 때문이다.
#import <Foundation/Foundation.h>
//싱글 포인터
void changeValueWithSinglePtr(NSString *str) {
str = @"New Value";
NSLog(@"이거는?? %@",str);
}
//더블 포인터
void changeValuewithDoublePtr(NSString **str) {
*str = @"New Value";
}
int main(void) {
@autoreleasepool {
NSString *original = @"Original Value";
changeValueWithSinglePtr(original);
NSLog(@"%@",original);
changeValuewithDoublePtr(&original);
NSLog(@"%@",original);
}
}
위 코드를 보자. Original Value라는 문자열 상수를 만들고, 해당 메모리 주소를 original이라는 포인터변수가 가리키고 있다.
(문자열 상수가 저장된 메모리 주소를 1001번지라고 가정한다_ read_only 영역인 data segment에 위치할 것이다.)
싱글 포인터 함수
먼저 싱글 포인터 함수를 호출하면, original이 가리키는 1001번지 메모리 주소가 '복사' 돼서 함수의 매개변수로 전달된다.
함수 내부에서 str = @"New Value"가 실행되면, str에 새로운 문자열 상수 New Value(1002번지라 가정하자)의 메모주소가 할당된다.
하지만 이는 복사된 메모리 주소에 대한 변경일 뿐, 원본 original 변수의 메모리 주소인 1001에는 영향을 미치지 않는다.
즉, 원본 original은 여전히 1001번지를 가리키고 있다는 것이다.
NSLog(@"이거는?? %@",str);이 실행되면 str이 가리키는 새로운 문자열 상수인 "New Value"를 출력하지만,
함수의 호출이 종료되면 '복사된 메모리 주소는 버려진다'.
사실 위 코드가 의도하는 바는 original이 가리키는 문자열 자체를 "Original Value"에서 "New Value"로 변경하고 싶은 것이다.
그래서 아예 original 변수가 가리키는 대상을 바꾸기 위해서 이중 포인터가 필요한 것이다.
더블 포인터 함수
changeValuewithDoublePtr을 보면 파라미터로 이중 포인터를 받는다.
original 변수는 이미 포인터 변수이기 때문에, 해당 변수에 주소연산자를 사용하면 이중 포인터가 된다. (포인터의 포인터(주소))
싱글포인터 함수에서는 1001번지라는 메모리 주소가 복사돼서 함수의 매개변수로 전달됐다면,
이번에는 original변수의 메모리 주소(3000번지라고 가정하자)가 복사돼서 전달된다.
함수 내부에서 *연산자를 통해 *str = @"New Value"를 실행하게 되면
*str은 원본 original 포인터를 말하고, 즉 original 포인터가 기존의 1001번지가 아닌,
@"New Value"의 메모리 주소인 1002번지를 가리키게 하는 것이다.
따라서 함수 호출이 끝나고 다시 original 포인터에 접근해보면 "New Value"가 출력되게 된다.
요약하자면 싱글 포인터 함수에서는 값에 대한 메모리 주소가 복사돼서 함수에 전달되었기 때문에 원본에 변화를 끼치지 못하지만,
더블 포인터 함수에서는 원본 변수의 메모리 주소가 복사돼서 함수에 전달됐기 때문에 원본 변수의 값 자체를 변경한 것이다.
그래서 싱글 포인터와 더블 포인터 중 어떤 것을 어떻게 사용해야 하는가?라는 질문은 무의미할 것이다.
상황에 따라서 원본 배열의 변경 없이 값을 사용하고 싶으면 싱글포인터를 사용하면 될 것이고,
원본의 변경이 필요한 경우에는 더블 포인터를 사용할 수 있다는 인사이트가 중요한 것 같다.
위 내용을 바탕으로 함수 파라미터로 에러를 함께 전달하는 예시를 살펴보자.
#import <Foundation/Foundation.h>
@interface MyClass : NSObject
-(NSString *) getCharacterNameForID: (int) id withError: (NSError **) errorPtr;
@end
@implementation MyClass
-(NSString *)getCharacterNameForID:(int)id withError:(NSError **)errorPtr {
if (id == 1) {
return @"Choonsik";
} else {
NSString *domain = @"com.ITCompany.MyApp.ErrorDomain";
NSString *desc = NSLocalizedString(@"failed to complete", @"retry");
NSDictionary *userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:@"NSLocalizedDescriptionKey", NULL];
*errorPtr = [NSError errorWithDomain:domain code:-101 userInfo:userInfo];
return @"";
}
}
@end
정수형 id와 이중 포인터 타입의 에러도 함께 전달하는 메서드가 포함된 클래스 예시이다.
id가 1인경우는 문자열을 잘 반환하지만, 1이 아닌 경우에는 새로운 에러를 만들어서 원본 변수에 할당한다.
여기서 에러를 바로 던지는 것이 아니라, 이중 포인터를 받아서 외부의 에러 변수에 새로운 에러를 할당해주는 것이다.
int main(void) {
@autoreleasepool {
MyClass *myClass = [[MyClass alloc] init];
NSError *error = nil;
//No error
NSString *name1 = [myClass getCharacterNameForID:1 withError:&error];
//if error not nil
if (error) {
NSLog(@"ERROR! : %@",error);
} else {
NSLog(@"character name: %@",name1);
}
error = nil;
NSString *name2 = [myClass getCharacterNameForID:2 withError:&error];
if (error) {
NSLog(@"ERROR! : %@",error);
} else {
NSLog(@"character name: %@",name1);
}
}
return 0;
}
name2를 초기화 해주는 과정에서 id가 1이 아니기 때문에 else 구문을 타면서 error변수는 기존의 nil이 아니라 새로 생성된 error 객체의 메모리 주소를 가리키게 된다. 따라서 에러를 던지게 된다.
역시 포인터는 꽤나 복잡하고 사용하기가 까다로운 것 같다. 메모리 누수 문제고 있고 안정성 문제도 있고..
그래서 Swift를 포함한 최신 언어들이 내부적 추상화를 통해 포인터의 직접 사용을 지양하는 것도 이해가 된다.
사실 iOS계열에서 이제는 objective-c로 프로젝트를 시작하는 일은 없겠지만 레거시 코드는 전부 옵젝씨이기 때문에
read-only라도 되려면 포인터의 개념을 이해하는 것이 필요하다고 생각한다.
'iOS Developer > Objective-C' 카테고리의 다른 글
18. Objective-C Inheritance 상속, encapsulation(캡슐화) (0) | 2024.05.21 |
---|---|
17. Objective-C 클래스와 객체 (메모리), 접근제어 (0) | 2024.05.20 |
15. Objective-C typedef, type casting (0) | 2024.05.17 |
14. Objective-C Preprocessor 전처리기 (0) | 2024.05.16 |
13. Objective-C Structure 구조체 (0) | 2024.05.16 |