[objC] 데이터 관리를 위한 정적 클래스 제작

iOS앱을 개발하다 보면 임의의 데이터를 저장할 일이 꼭 있더군요. (굳이 iOS앱이 아니더라도…) 그럴려면 거창하게 코어 데이터(Core Data)나 SQLite를 쓰는 방법을 생각하게 되는데요. 매번 이런 환경을 만드는게 너무 귀찮았습니다. 단순한 방법으로 데이터를 쓰기/읽기/삭제를 하고 싶었습니다.

예를 들어 다음처럼 쉽게 쓰면 얼마나 좋을까요?
(참고로 모바일에서 코드 가독성을 높이기 위해 줄바꿈 처리 했습니다.)

//특정배열 저장하기 
[bsIO 
   storageSet:@"arr" 
   data:@[@(1),@(2),@(3)]
];

//.... 무엇인가 한 뒤
NSLog(
  @"[storage]arr = %@", 
  [bsIO storageGet:@"arr"]
); //[storage]arr = (1,2,3)

//임의의 이미지 데이터 저장
[bsIO 
   storageSet:@"folder/image.png" 
   data:UIImagePNGRepresentation(image)
];

//.... 무엇인가 한 뒤
NSData *imgData;
imgData = [bsIO 
  storageGet:@"folder/image.png"
];
UIImage *img = 
  [UIImage imageWithData:imgData];

그래서 만들어 봤습니다.

사용한 기술 소개top

iOS앱은 앱 내부의 저장 공간을 크게 3가지로 제공하는 것 같았습니다.

  • 번들 : 런타임 읽기 전용. 컴파일시에 ipa내에 확정된 데이터 입니다.
  • 임시 스토리지(Cache) : 런타임 쓰기/읽기/삭제가 가능. 다만, 캐시용도로만 쓸 수 있으며 언제든 OS사정에 따라 삭제가 될 수 있습니다.
  • 영구 스토리지(Document) : 런타임 쓰기/읽기/삭제가 가능. 앱이 삭제되기 전까지 영구적으로 저장됩니다.(그래도 너무 많이 쓰면 곤란할 듯… 한계가 있는지는 모르겠네요.)

이 공간들은 전부 NSFileManager를 통해 접근이 가능합니다. 각각의 저장공간의 경로를 얻는 방법은 다음과 같습니다.

  • 번들
     [[NSBundle mainBundle] bundlePath];
    

  • 임시 스토리지 :

      [NSSearchPathForDirectoriesInDomains(
         NSCachesDirectory, //여기!
         NSUserDomainMask, YES
      ) objectAtIndex:0];
    

  • 영구 스토리지 :

      [NSSearchPathForDirectoriesInDomains(
         NSDocumentDirectory, //여기!
         NSUserDomainMask, YES
      ) objectAtIndex:0];
    

우리는 이 저장 공간에 특정 데이터를 NSData로 아카이브(archive)한 뒤 NSFileManager로 저장할 수 있습니다(번들 제외). 그리고 반대로 NSData로 읽어와 언아카이브(unarchive)해서 데이터를 참고할 수 있죠.

우선 번들 데이터는 아래와 같은 방법으로 읽어올 수 있습니다.

NSFileManager *fm = 
  [NSFileManager defaultManager];

//번들 데이터 가져오기 
NSData *data = [fm contentsAtPath:
  [[NSBundle mainBundle] bundlePath]
];

영구 스토리지같은 경우는 우선 경로를 해결해야합니다.

NSString *root = //루트(영구) 
  [NSSearchPathForDirectoriesInDomains(
     NSDocumentDirectory, 
     NSUserDomainMask, 
     YES
  ) objectAtIndex:0];
NSString *filePath = //실제 파일 경로 
  [NSString 
     stringWithFormat:@"%@/%@", 
     root, 
     @"myfileName"];

아래와 같은 데이터가 있다고 할 때..

NSData *data = //저장할 데이터....

저장/읽기/삭제는 다음과 같이 사용할 수 있습니다.

NSFileManager *fm = 
  [NSFileManager defaultManager];

//데이터 저장 
[fm createFileAtPath:filePath 
  contents:data 
  attributes:nil];

//데이터 읽기 
data = [fm contentsAtPath:filePath];

//데이터 삭제 
[fm removeItemAtPath:filePath 
    error:nil];

임시 스토리지의 경우는 영구 스토리지에서 root 경로만 빼고 전부 같습니다.

//임시 스토리지에 쓰기/읽기/삭제 
NSString *root = //루트(캐쉬) 
  [NSSearchPathForDirectoriesInDomains(
     NSCachesDirectory, 
     NSUserDomainMask, 
     YES
  ) objectAtIndex:0];

NSString *filePath = //실제 파일 경로 
  [NSString 
     stringWithFormat:@"%@/%@", 
     root, 
    @"myfileName"];

NSFileManager *fm = 
  [NSFileManager defaultManager];

//데이터 저장 
[fm createFileAtPath:filePath 
  contents:data 
  attributes:nil];

//데이터 읽기 
data = [fm contentsAtPath:filePath];

//데이터 삭제 
[fm removeItemAtPath:filePath 
    error:nil];

기술적 설명은 이 정도로 정리할께요.
다음부턴 이 기술을 기초로 본격적으로 만들어 봅니다.

인터페이스 만들기 : bsIO.htop

기초적인 인터페이스는 다음 코드와 같습니다.
잠깐 구경해 볼까요?

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface bsIO : NSObject

#pragma mark - asset
//번들 데이타 읽기 
+(NSData* _Nullable)assetGet:
  (NSString* _Nonnull)key;

//번들에서 JSON 데이타 읽기. 
// JSON형태에 따라 NSArray 또는 
// NSDicationay로 반환 
+(id _Nullable)assetGetJSON:
  (NSString* _Nonnull)key;

//번들에서 이미지 데이타 읽기. 
// UIImage로 반환 
+(UIImage* _Nullable)assetGetImage:
  (NSString* _Nonnull)key;

#pragma mark - storage
//영구 스토리지에서 데이터 읽기 
+(id _Nullable)storageGet:
  (NSString* _Nonnull)key;

//영구 스토리지에 데이터 쓰기 
+(BOOL)storageSet:
  (NSString* _Nonnull)key 
  data:(id _Nonnull)data;

//영구 스토리지의 데이터 삭제 
+(BOOL)storageDel:
  (NSString* _Nonnull)key;

#pragma mark - cache
//임시 스토리지에 데이터 읽기 
+(id _Nullable)cacheGet:
  (NSString* _Nonnull)key;

//임시 스토리지에 데이터 쓰기 
+(BOOL)cacheSet:
  (NSString* _Nonnull)key 
  data:(id _Nonnull)data;

//임시 스토리지의 데이터 삭제 
+(BOOL)cacheDel:
  (NSString* _Nonnull)key;
@end

기본적으로 모든 메소드는 static으로 접근할 수 있게 했어요. 구현체는 singleton으로 만들지만 외부에서 사용할 때는 단순히 [bsIO assetGet:@”key”] 형태로 정적 메소드로 쓰는게 좋을 것 같아서요.

그리고 각 메서드들은 각 저장소 형태에 맞게 접두어로 명명했어요. 가령, 임시 스토리지는 cache접두어를 써서 cacheGet, cacheSet, cacheDel 메소드 명을 썼습니다.

또 영구 스토리지는 storage접두어를 써서 storageGet, storageSet, storageDel를 쓰고요…(지금 생각해보니 doc으로 해야 하나?)

번들의 경우 읽기 전용이므로 assetGet 정도만 써도 될 것 같습니다.

또한 swift와 연동시 optional 연동을 하고 구현시 처음부터 null 분기 코드를 제거하기 위해 _Nonnull과 _Nullable를 지정했습니다.

구현체 만들기 : bsIO.mtop

위에서 정의한 인터페이스에 따라서 동작할 수 있도록 구현체를 만들어 볼께요.

참고로 특별히 번들 저장소는 assets 폴더가 있다고 가정합니다. 아래 그림 처럼요.

assets

초기화 메소드와 싱글톤 메소드 만들기

아래 코드는 실제 구현하기전 초기화와 싱글톤을 다루고 있습니다.

#import "bsIO.h";	 	 

@interface bsIO ()
@property(atomic,strong)
  NSString* assetPath; //번들 경로 	 	 

@property(atomic,strong) 	 	 
  NSString* storagePath;//영구저장 경로 	 	 

@property(atomic,strong) 	 	 
  NSString* cachePath; //임시저장 경로 	 	 

@property(atomic,weak) 	 	 
  NSFileManager *fm; //파일 관리자	 	 

@end	 	 

@implementation bsIO	 	 

//싱글톤 객체 반환(외부 노출 안함)	 	 
+(bsIO* _Nonnull)sharedInstance{	 	 
  static dispatch_once_t pred;	 	 
  static id sharedObject;	 	 
  dispatch_once(&amp;pred, ^{	 	 
    sharedObject = [[bsIO alloc]init];	 	 
  });	 	 
  return sharedObject;	 	 
}	

//초기화 
-(id)init{
  if(self = [super init]){
	//영구저장 경로 
	self.storagePath = 
	  [NSSearchPathForDirectoriesInDomains(
		 NSDocumentDirectory, 
		 NSUserDomainMask, 
		 YES
	  ) objectAtIndex:0];

	//임시저장 경로 
	self.cachePath = 
	  [NSSearchPathForDirectoriesInDomains(
		 NSCachesDirectory, 
		 NSUserDomainMask, 
		 YES
	  ) objectAtIndex:0];

	//번들 경로
	self.assetPath = 
	  [NSString 
		 stringWithFormat:@"%@/assets", 
		 [[NSBundle mainBundle] bundlePath]
	  ];

	//파일 관리자 참조 
	self.fm = 
	  [NSFileManager defaultManager];

	//영구저장 경로가 없으면 생성 
	BOOL isExist = 
	[self.fm 
	   fileExistsAtPath:self.storagePath
	];
	
	if(!isExist){
	  NSError *error = nil;
	  [self.fm 
	    createDirectoryAtPath:
		   self.storagePath 
	    withIntermediateDirectories:YES 
	    attributes:nil 
	    error:&error
	  ];
	  if(error) 
	    [NSException 
		  raise:NSInvalidArgumentException 
		  format:@"%s(%d)%@", 
		  __FUNCTION__, 
		  __LINE__, 
		  @"스토리지 생성 실패"
	    ];
	}
  }
  return self;
}

#pragma mark - asset
//....(번들 구현)

#pragma mark - storage
//....(영구저장 구현)

#pragma mark - cache
//....(임시저장 구현)

@end

위 코드를 보면 먼저 초기화 메소드와 싱글톤을 구현했습니다. 싱글톤 메소드는 외부에 노출하지 않습니다. 초기화 메소드 내부를 보면 각각의 저장소 경로를 셋팅하고 FileMananger를 weak로 참조한 것을 확인할 수 있습니다.

이제 #pragma mark 로 표시한 부분을 구현해야 해보겠습니다.

번들 데이터 가져오기 구현

먼저 번들 데이터를 가져오는 부분을 구현하겠습니다.

#pragma mark - asset

//번들 데이타 읽기
+(NSData* _Nullable)assetGet:
    (NSString* _Nonnull)key {
    return 
      [[bsIO sharedInstance]
        assetGet:key];
}

//번들에서 JSON 데이타 읽기.
+(id _Nullable)assetGetJSON:
  (NSString* _Nonnull)key{
    NSData *jsonData = 
      [bsIO assetGet:key];
    if(!jsonData) return nil;

    NSError *error = nil;
    id json = 
      [NSJSONSerialization 
         JSONObjectWithData:jsonData 
         options:kNilOptions 
         error:&error
      ];
    if(error) return nil;
    return json;
}

//번들에서 이미지 데이타 읽기.
+(UIImage* _Nullable)assetGetImage:
  (NSString* _Nonnull)key{
    NSData *imgData = 
      [bsIO assetGet:key];
    if(!imgData) return nil;
    
    return 
       [UIImage imageWithData:imgData];
}

//번들 데이터 경로 만들기 
-(NSString* _Nonnull)__assetPath:
   (NSString* _Nonnull)key{
    return 
      [NSString 
         stringWithFormat:@"%@/%@", 
         self.assetPath, 
         key
      ];
}

//번들 데이터 가져오기(구현체) 
-(NSData* _Nullable)assetGet:
   (NSString* _Nonnull)key{
    NSString *path = 
      [self __assetPath:key];
    return [self.fm contentsAtPath:path];
}

위 코드는 번들 저장소 접근해서 데이터를 가져오는 메소드들을 정의했습니다. 이미 언급했듯이 +가 붙어 있는 정적 메소드만 해더 파일에 선언했으므로 외부에 노출됩니다. -가 붙어 있는 인스턴스 메소드들은 실제 번들 저장소로부터 데이터를 읽어오는 실제 구현체들입니다.

위 구현 메소드 중 [bsIO assetGet:key]가 기본 메소드로 NSData를 반환합니다. 하지만 NSData는 가장 로우레벨의 데이터로 직접쓰는 경우는 드물죠. 그래서 도우미 메소드들을 추가할 수 있습니다.

도우미 메소드로 [bsIO assetGetJSON:key], [bsIO assetGetImage:key]등이 있습니다. 이 메소드들의 역할은 json문자열을 NSArray나 NSDictionary로 받을 수 있거나 UIImage를 받도록 합니다. 필요할 때 이들 도우미 메소드를 사용하면 호스트 코드가 심플해지겠죠. 이외도 필요하면 계속 추가하면 좋을 겁니다.

영구 데이터 관리 구현

아래 코드는 영구 저장소 관련 구현체 입니다. 영구 저장소는 쓰기/읽기/삭제가 모두 되기 때문에 get/set/del 정적 메소드를 제공합니다.

#pragma mark - storage

//영구 스토리지에서 데이터 읽기 
+(id _Nullable)storageGet:
   (NSString* _Nonnull)key{
    id data = 
      [[bsIO sharedInstance]
         storageGet:key
      ];
    if(data == nil) return nil;
 
    return 
      [NSKeyedUnarchiver 
        unarchiveObjectWithData:data
      ];
}

//영구 스토리지에 데이터 쓰기
+(BOOL)storageSet:
   (NSString* _Nonnull)key 
   data:(id _Nonnull)data{
    NSData *t = 
      [NSKeyedArchiver 
         archivedDataWithRootObject:data
      ];
    return 
      [[bsIO sharedInstance]
         storageSet:key 
         data:t
      ];
}

//영구 스토리지의 데이터 삭제
+(BOOL)storageDel:
    (NSString* _Nonnull)key{
    return 
      [[bsIO sharedInstance] 
        storageDel:key];
}

//영구 스토리지 데이터 경로 만들기 
-(NSString* _Nonnull)__storagePath:
   (NSString* _Nonnull)key{
   return 
     [NSString 
       stringWithFormat:@"%@/%@", 
       self.storagePath, 
       key];
}

//영구 스토리지 폴더 자동 생성 
-(BOOL)__checkStoragePath:
   (NSString* _Nonnull)key{
    NSArray *keys = 
      [key componentsSeparatedByString:@"/"];

    BOOL success = YES;
    if([keys count] > 1){
        NSInteger i = [key length] - 
          [keys[[keys count]-1] length];
        NSString *path = 
          [self __storagePath:
             [key substringToIndex:i]
          ];

        BOOL isExist = 
         [self.fm 
            fileExistsAtPath:path
         ];
        if(!isExist){
            success = 
              [self.fm 
                createDirectoryAtPath:path 
                withIntermediateDirectories:YES 
                attributes:nil 
                error:nil
              ];
        }
    }
    return success;
}

//영구 스토리지에서 데이터 얻기 
-(NSData* _Nullable)storageGet:
    (NSString* _Nonnull)key{
    return 
      [self.fm contentsAtPath:
         [self __storagePath:key]
      ];
}

//영구 스토리지에 데이터 쓰기 
-(BOOL)storageSet:
    (NSString* _Nonnull)key 
    data:(NSData* _Nonnull)data{

    BOOL check = 
      [self 
        __checkStoragePath:key];
    if(NO == check) return NO;

    return 
      [self.fm 
         createFileAtPath:
           [self __storagePath:key] 
         contents:data 
         attributes:nil
      ];
}

//영구 스토리지의 데이터 삭제 
-(BOOL)storageDel: 
    (NSString* _Nonnull)key {
    return 
      [self.fm 
         removeItemAtPath:
           [self __storagePath:key] 
         error:nil
      ];
}

위 코드를 보면 저장소에 임의의 폴더를 자동으로 만들 수 있도록 구현했습니다. __checkStroagePath: 메소드가 그 역할을 하는데요. 결국 아래 코드처럼 사용이 가능합니다.

[bsIO 
  storageSet:
    @"folder1/folder2/filename.txt" 
  data:@"데이터"
];

위처럼 사용하면 __checkStroagePath:를 통해 folder1과 folder2가 자동으로 만들어줍니다. 따로 폴더를 생성할 필요가 없이 자동으로 만들어 주는거죠.

그리고 이렇게 저장된 데이터는 아래 코드로 가져올 수 있죠.

id data =  
  [bsIO 
     storageGet:
       @"folder1/folder2/filename.txt"
  ];

임시 데이터 관리 구현

영구 스토리지를 커버했으니 임시 스토리지는 거의 복사판입니다. 그냥 같다고 봐도 무방할 것 같네요. 중복되는 코드가 많아서 많은 부분 중복을 제거할 수 있을 겁니다. (여기선 생략~ ^^)

#pragma mark - cache

//임시 스토리지에 데이터 읽기
+(id _Nullable)cacheGet:
   (NSString* _Nonnull)key{
    id data = 
      [[bsIO sharedInstance]
        cacheGet:key
      ];
    if(data == nil) return nil;

    return 
      [NSKeyedUnarchiver 
         unarchiveObjectWithData:
         data 
      ];
}

//임시 스토리지에 데이터 쓰기
+(BOOL)cacheSet:
   (NSString* _Nonnull)key 
   data:(id _Nonnull)data{
    NSData *t = 
      [NSKeyedArchiver 
         archivedDataWithRootObject:
         data
      ];
    return 
       [[bsIO sharedInstance]
          cacheSet:key 
          data:t
       ];
}

//임시 스토리지의 데이터 삭제
+(BOOL)cacheDel:
    (NSString* _Nonnull)key{
    return
      [[bsIO sharedInstance]
         cacheDel:key
      ];
}

//임시 스토리지 데이터 경로 만들기
-(NSString* _Nonnull)__cachePath:
    (NSString* _Nonnull)key{
    return
      [NSString 
         stringWithFormat:@"%@/%@", 
         self.cachePath, 
         key
      ];
}

//임시 스토리지 폴더 자동 생성 
-(BOOL)__checkCachePath:
    (NSString* _Nonnull)key{
    NSArray *keys =
      [key 
        componentsSeparatedByString:@"/"
      ];
    
    BOOL success = YES;
    if([keys count] > 1){
        NSInteger i = 
          [key length] - 
          [keys[[keys count]-1] length];
        
        NSString *path = 
          [self __cachePath:
             [key substringToIndex:i]
          ];

        BOOL isExist = 
          [self.fm 
             fileExistsAtPath:path
          ];
        if(!isExist){
            success = 
              [self.fm 
                createDirectoryAtPath:path 
                withIntermediateDirectories:YES 
                attributes:nil 
                error:nil
              ];
        }
    }
    return success;
}

//임시 스토리지에 데이터 읽기
-(NSData* _Nullable)cacheGet:
    (NSString* _Nonnull)key{
    NSString *path = 
      [self __cachePath:key];
    NSData *data = nil;

    BOOL isExist = 
      [self.fm 
         fileExistsAtPath:path
      ];
    if(isExist){
        data = 
          [self.fm 
             contentsAtPath:path
          ];
    }
    return data;
}

//임시 스토리지에 데이터 쓰기
-(BOOL)cacheSet:
    (NSString* _Nonnull)key 
    data:(NSData* _Nonnull)data {
    BOOL check = 
      [self __checkCachePath:key];
    if(NO == check ) return NO;
    return 
      [self.fm 
         createFileAtPath:
           [self __cachePath:key] 
         contents:data 
         attributes:nil
      ];
}

//임시 스토리지의 데이터 삭제
-(BOOL)cacheDel:
    (NSString* _Nonnull)key {
    return 
      [self.fm 
         removeItemAtPath:
           [self __cachePath:key] 
           error:nil
      ];
}

사용해보기top

만들어진 코드를 사용해 볼께요. 다양한 데이터를 저장할 수 있음을 확인할 수 있습니다.

먼저 번들 데이타를 읽어오는 예제입니다.

//json데이터 읽어오기 
NSData *jsonData = 
   [bsIO assetGet:@"test.json"];
   
NSError *error = nil;
NSDictionary *json = 
  [NSJSONSerialization 
     JSONObjectWithData:jsonData 
	 options:kNilOptions 
	 error:&error
  ];

NSLog(
  @"[asset]json = %@",
  json
); //{k1:"테스트",k2:[1,2,3].....}


//json데이터 읽어오기
//(위 보다 아래 코드를 쓰면 한 줄로 할 수 있음) 
NSLog(
  @"[asset]json = %@", 
  [bsIO assetGetJSON:@"test.json"]
); //{k1:"테스트",k2:[1,2,3].....}


//이미지 데이터 읽어오기 
NSData *imgData =  
  [bsIO assetGet:@"img/arrow.png"];

UIImage *img = 
  [UIImage imageWithData:imgData];

NSLog(
  @"[asset]img = %@", 
  img
); // <UIImage: 0x60000009d920>, {54, 88}


//이미지 데이터 읽어오기
//(위 보다 아래 코드를 쓰면 한 줄로 할 수 있음) 
NSLog(
  @"[asset]img = %@", 
  [bsIO assetGetImage:@"img/arrow.png"]
); // <UIImage: 0x60000009d920>, {54, 88}

다음 코드는 영구 스토리지에 쓰기/읽기 등을 하는 예제입니다.

//저장되어 있지 않은 키에 대한 값
id none = 
  [bsIO storageGet:@"none"];

NSLog(
  @"[storage]none = %@",
  none
); //null


//NSArray 저장/읽기
[bsIO 
  storageSet:@"arr" 
  data:@[@(1),@(2),@(3)]
];

NSArray* arr = 
  [bsIO storageGet:@"arr"];

NSLog(
  @"[storage]arr = %@",
  arr
); //(1,2,3)


//NSDictionary 저장/읽기
[bsIO 
  storageSet:@"dic" 
  data:@{
    @"k1":@(1), 
	@"k2":@"abcd", 
	@"k3":@"한글"
  }
];

NSDictionary *dic = 
  [bsIO storageGet:@"dic"];

NSLog(
  @"[storage]dic = %@",
  dic
); //{k1:1, k2:abcd, k3:한글}


//NSString 저장/읽기
[bsIO 
  storageSet:@"str" 
  data:@"test"
];

NSString* str =
  [bsIO storageGet:@"str"];

NSLog(
  @"[storage]str = %@", 
  str
); //test

NSLog(
  @"[cache]dic[k3] = %@",
  dic[@"k3"]
); //한글


//NSData 저장/읽기
NSString * m = 
  @"Save NSData type";

const char *c = 
  [m UTF8String]; // 일반 char type 으로 변환

NSData * data = 
  [NSData 
     dataWithBytes:c 
	 length:strlen(c)
  ]; // 생성자이므로 메모리할당이 필요없음

[bsIO 
  storageSet:@"data" 
  data:data
];

data = 
  [bsIO storageGet:@"data"];

char *bytePtr = 
  (char *)[data bytes];

NSLog(
  @"[storage]char = %s", 
  bytePtr
); //Save NSData type


//하위경로 저장/읽기
[bsIO 
  storageSet:@"folder1/name" 
  data:@"특정폴더에 저장"
];

NSString* s1 = 
  [bsIO storageGet:@"folder1/name"];

NSLog(
  @"[storage]folder1/name = %@", 
  s1
); //특정폴더에 저장

임시 스토리지에 쓰기/읽기 예제는 위 코드와 흡사하므로 생략하겠습니다.

직접 만든 클래스로 생성한 객체를 저장top

직접 만든 클래스로 생성한 객체를 저장하려면 특별한 처리가 필요합니다.
예를 들어 아래와 같은 클래스를 만들었다고 해보죠.

@interface Member : NSObject

@property(nonatomic,strong) 
   NSString* userName; //회원명

@property(nonatomic) 
   NSInteger age; //회원나이 

@end

위 클래스의 객체를 저장해 봅시다.

//회원 생성 
Member *m = [Member new];
m.userName = @"지돌스타";
m.age = 30;

//회원 저장 
[bsIO 
  storageSet:@"member" 
  data:m]; //에러!!

우린 불행히도 바로 다음과 같은 런타임 에러를 만나게 될 겁니다.


*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Member encodeWithCoder:]: unrecognized selector sent to instance

이 에러가 발생하는 이유는 NSKeyedArchiver를 이용해 NSData로 아카이브하려는 객체는 반드시 해당 클래스가 NSCoding을 구현해야 하기 때문입니다.
그래서 에러 메시지에도 [Member encodeWithCoder:]를 찾을 수 없다고 나오는 겁니다.

그럼 Member에 NSCoding을 구현해 볼께요.

//Member.h
#import <Foundation/Foundation.h>
@interface Member 
   : NSObject <NSCoding>

@property(nonatomic,strong) 
   NSString* userName;

@property(nonatomic) 
   NSInteger age;

@end
//Member.m
#import "Member.h"

@implementation Member

- (void)encodeWithCoder:
  (NSCoder *)aCoder{
    [aCoder 
       encodeObject:self.userName 
	   forKey:@"userName"
	];
    [aCoder 
	   encodeInteger:self.age 
	   forKey:@"age"
	];
}

- (instancetype)initWithCoder:
  (NSCoder *)aDecoder{
    if(self = [super init]){
        self.userName = 
		  [aDecoder 
		    decodeObjectForKey:
			  @"userName"
		  ];
        self.age = 
		  [aDecoder 
		    decodeIntegerForKey:
			@"age"
		  ];
    }
    return self;
}

//Log출력하기 위해 description을 구현했습니다.
- (NSString *)description{ 
    return 
	  [NSString 
	     stringWithFormat:
		   @"Member userName=%@, age=%ld", 
		   self.userName, 
		   self.age
      ];
}
@end

클래스 단위로 명시적으로 데이터를 관리하는 것은 좋다만, 모든 데이터를 이렇게 쓴다면 여간 귀찮은게 아닐 것 같네요.
이쯤되면 그냥 Core Data를 써야하지 않을까요?

암튼 NSCoding을 구현한 Member클래스의 객체를 저장하는 것은 문제없이 잘되는 것을 확인할 수 있습니다.

//회원 생성 
Member *m = [Member new];
m.userName = @"지돌스타";
m.age = 30;

//저장 
[bsIO 
   storageSet:@"member" 
   data:m
];

//읽어서 로그에 출력 
NSLog(
   @"member = %@", 
   [bsIO storageGet:@"member"]
); //Member userName=지돌스타, age=30

결론top

이상으로 구현은 귀찮지만 한번 만들어 놓으면 두고두고 써먹을 수 있는 단순한 데이터 관리를 위한 정적 클래스를 구현해 봤습니다. 실제로 실무에 쓸 때 편리했습니다. 단순하고 복잡하지 않은 설정 데이터 관리나 번들 데이터 참조시 사용하면 유용하지 않을까 생각됩니다.

좋은 하루 되세요~