본문 바로가기

학습 노트/iOS (2021)

183 ~ 184. NSCoding and Codable

NSCoding

 

Apple Developer Documentation

 

developer.apple.com

생성자와 encode(with:) 메서드를 필수로 구현해야 하는 NSCoding 프로토콜은
Archiving과 Serialization을 구현하기 위한 필수 프로토콜이다.

  • Decoding
    Binary 데이터를 객체로 변환
  • Encoding
    속성(객체)을 Binary 값으로 변환

프로토콜은 위의 두 가지를 담당하게 된다.

NSCoding은

  • NSObject를 상속해야한다.
  • NSCoding 프로토콜을 채용하거나 구현해야한다.
  • 객체를 저장할 수 있다. 단, 구조체는 불가능하다.

위의 세가지 특징과 한계를 가지고 있다.

init()

 

Apple Developer Documentation

 

developer.apple.com

 

encode()

 

Apple Developer Documentation

 

developer.apple.com

실습에선 이미지와 Double, String의 데이터를 저장하는 인스턴스를 생성하고,
이를 Archive하고, 불러오는 과정을 구현한다.

encodeObject()

   @IBAction func encodeObject(_ sender: Any) {
	   do {
		   guard let url = Bundle.main.url(forResource: "logo", withExtension: "gif") else {
			   return
		   }
		   
		   let data = try Data(contentsOf: url)
		   
		   guard let img = UIImage(data: data) else {
			   return
		   }
		   
		   let obj = Language(name: "Swift", version: 1.0, logo: img)
		   
		   if #available(iOS 11.0, *) {
			   let archivedData = try NSKeyedArchiver.archivedData(withRootObject: obj, requiringSecureCoding: true)
			   try archivedData.write(to: fileUrl)
		   } else {
			   NSKeyedArchiver.archiveRootObject(obj, toFile: fileUrl.path)
		   }
		   
		   print("Done")
	   } catch {
		   print(error)
	   }
   }

이미지 Url 생성

Bundle로 저장돼 있는 이미지의 경로를 저장한다.

Url을 사용해 이미지 인스턴스 생성

생성한 경로를 사용해 인스턴스로 저장한다.
data 변수의 형식은 Data이기 때문에 이를 Image로 인식할 수 있도록 UIImage로 변환해야한다.

Language Class 형식으로 인스턴스 생성

사용할 이미지가 정상적으로 로드되면 실제 사용할 Language 형식으로 객체를 생성한다.

class Language {
   let name: String
   let version: Double
   let logo: UIImage
   
   init(name: String, version: Double, logo: UIImage) {
      self.name = name
      self.version = version
      self.logo = logo
   }
}

해당 클래스의 형식은 위와 같다.
String 형식의 name, Double 형식의 version, UIImage 형식의 logo를 파라미터로 사용하여 초기화한다.

Archive 하기

가장 기초적인 Archive는 위에 표시한 부분이다.

NSKeyedArchiver는 객체를 파일로 저장할 때 사용한다.
NSKeyedArchiver의 archiveRootObject 메서드는
첫번째 파라미터로 저장할 객체를, 두번째 파라미터로 저장할 경로를 전달 받는다.
이때, NSKeyedArchiver를 사용하기 위해서 NSCoding 프로토콜을 채용할 필요가 있다.

NSArchiver

 

Apple Developer Documentation

 

developer.apple.com

위와 같이 Language 클래스가 NSCoding 프로토콜을 채용하도록 하고,
생성자와 encode(coder:)를 정의하면 된다.

encode() 정의

encode 동작 자체는 파라미터로 존재하는 NSCoder가 알아저 처리한다.
블럭 내에는 어떤 것을 어떻게 처리할지만 encode(_:forKey:) 메서드로 정의한다.

encode(_:forKey:)

 

Apple Developer Documentation

 

developer.apple.com

첫번째 파라미터는 오브젝트를, 두번째 파라미터는 변환 후 사용될 Key를 전달한다.
해당 Key는 이후 Decoding 과정에서 사용된다.

init()

생성자는 Dcoding을 정의한다.
각각의 자료형에 맞는 Decoder가 존재한다.
String과 Image는 decodeObject를, Int와 Double은 decodeDouble과 decodeInteger를 사용하는 식이다.
String과 Image 등은 동일한 decodeObject 메서드를 사용하기 때문에 반환값도 동일한 형식이다.
따라서 최종적으로는 각각에 맞는 형식으로 타입캐스팅 해야한다.

이 상태로도 오류가 발생한다.
첫번째 로그의 methodSignatureForSelector 메서드 때문에 생긴 문제로,
해당 메서드를 사용할 수 있도록 조치해야한다.
해당 메서드는 NSObject 클래스에 선언돼 있으므로, 해당 클래스를 상속받도록 변경한다.

이제는 문제 없이 Archive를 진행할 수 있다.

deCodeObject()

   @IBAction func decodeObject(_ sender: Any) {
	   do {
		   let data = try Data(contentsOf: fileUrl)
		   var language: Language?
		   
		   if #available(iOS 11.0, *) {
			   language = try NSKeyedUnarchiver.unarchivedObject(ofClass: Language.self, from: data)
		   } else {
			   language = NSKeyedUnarchiver.unarchiveObject(with: data) as? Language
		   }
		   
		   if let language = language {
			   self.imageView.image = language.logo
			   self.nameLabel.text = language.name
			   self.versionLabel.text = "\(language.version)"
		   }
	   } catch {
		   print(error)
	   }
   }

init()까지 정의했다면 Decoding은 정의를 사용하기만 하면 된다.

사용할 인스턴스 생성

Archive를 불러 올 data 인스턴스와 변환 후 저장할 language 인스턴스를 생성한다.

unArchive

사용하는 클래스는 NSKeyedArchiver의 반대인 NSKeyedUnarchiver다.
unarchiveObject(with:) 메서드를 사용해 디코딩하고, 이를 Language 클래스로 변환한다.
이후의 사용은 일반적인 객체의 사용과 동일한다.

NSSecureCoding

단, 위의 방식은 NSCoding의 반환 방식 때문에 한계를 가지고 있다.
NSCoding의 Decoding은 원하는 방식으로 된다는 보장이 없고,
이에 따라 실패했을 경우 nil등의 잘못된 데이터를 전달하거나
논리적 오류나 충돌을 유발하고,
형식에 제한받지 않기 때문에 보안 취약점이 존재한다.

따라서 iOS 11부터 NSSecureCoding이 채용됐고,
iOS 12부터는 NSCoding을 사용하지 말 것을 권고하고 있다.

사용 방법은 간단하게 NSSecureCoding 프로토콜을 채용하도록 수정한 뒤,
archiveRootObject() 메서드 대신 archivedData(withRootObject:requiringSecureCoding:) 메서드를 사용하고,
decodeObject(forKey:) 메서드 대신 decodeObject(of:forKey:) 메서드를 사용한다.

프로토콜 채용

사용되는 프로토콜은 NSSecureCoding 프로토콜이다.

NSSecureCoding

 

Apple Developer Documentation

 

developer.apple.com

해당 프로토콜은 supportsSecureCoding을 반드시 구현해야 하고,
전달되는 값에 따라 사용 여부가 결정된다.
사용하기 위해서는 true를 전달한다.

decodeObject(of:forKey:)

 

Apple Developer Documentation

 

developer.apple.com

첫번째 파라미터에는 저장된 형식을 전달하고, 두번째 파라미터로는 사용할 Key를 전달한다.

SecureEncode

동일하게 NSKeyedArchiver를 사용하고, archivedData(withRootObject:requiringSecureCoding:) 메서드를 호출한다.

archivedData(withRootObject:requiringSecureCoding:)

 

Apple Developer Documentation

 

developer.apple.com

첫번째 파라미터에는 저장할 형식을 전달하고, 두번째 파라미터로 archive의 SecureCoding의 사용 여부를 전달한다.

이전에 사용하던 메서드와 달리 저장할 경로를 전달하지 않으므로,
인스턴스에서 write(to:) 메서드를 호출해 저장해야한다.

SecureEncode

Decoding 자체는 크게 다를 바 없다.
마찬가지로 unarchivedObject(ofClass:from:)메서드로 변경해
첫번째 파라미터로 변환할 형식을 지정하고, 두번째 파라미터로 데이터를 전달하면 된다.

대신 Language Class의 encode를 수정할 필요가 있다.
실제 encode 방식은 Class 내의 encode 메서드레 존재하기 때문이다.
NSCoding은 구조체를 사용할 수 없는데, 해당 메서드로 전달되는 일부 데이터가 구조체 방식이기 때문이다.

따라서 해당 데이터를 Class 방식으로 캐스팅한다.
String 형식의 name과 Double 형식의 version은 구조체 방식이다.
이들을 NSString과 NSNumber로 캐스팅(Casting)해 클래스 방식으로 변경한다.
이는 생성자도 마찬가지이다.

기존에 구조체 방식을 사용하던 데이터들을 모두 Class 방식으로 변경하면 문제가 해결된다.
이 때 이전의 이미지와 마찬가지로 decodeObject를 사용해 디코딩을 진행하기 때문에 반환값을 타입캐스팅 해야함에 주의하자.

NSSecureCoding은 NSCoding과 비교해 파라미터로 변환할 '형식'을 파라미터로 하나 더 받게 된다.
이는 'Decoding > Key 확인'의 일련의 과정에서 '형식 확인 > Decoding > Key 확인'
으로 한 단계 늘어난 매커니즘을 갖게 되고, 형식을 확인한 뒤 Decoding이 진행되기 때문에
보안상으로도 유리하고, 형식이 맞지 않다면 무의미한 Decoding이 진행되지 않기 때문에 효율적이기도 하다.

Codable

앞서 NSCoding과 NSSecureCoding의 한계점을 명확히 알 수 있었다.
이 둘은 클래스(Class) 전용으로 구조체(Struct)를 사용할 수 없다.
구조체에서는 Codable이라는 다른 프로토콜을 채용해야한다.

Codable 프로토콜은 enCodable과 deCodable이 합쳐진 형태로,
이 둘은 생성자(init)와 encode를 직접 정의해야했던 NSCoding, NSSecureCoding과 달리
기본 생성자와 encode를 제공한다.
대부분의 기본 구조체는 Codable 프로토콜을 채용하고 있고,
만약 모든 인스턴스가 Codable 프로토콜을 해용한 구조체로 이루어져 있다면
기본 생성자와 encode를 그대로 사용할 수 있다.

Codable 프로토콜 채용

(좌 : Codable, 우 : NSSecureCoding)

채용 방법은 단순히 Codable 프로토콜을 채용하면 된다.
해당 프로토콜은 SecureCoding을 지원하기 때문에 NSSecureCoding 프로토콜을 별도로 채용할 필요도 없다.

SecureEncode

이번엔 NSKeyedArchiver의 생성자를 사용했다.
파라미터로 SecureCoding 여부를 선택해 전달할 수 있다.

NSKeyedArchiver(requiringSecureCoding:)

 

Apple Developer Documentation

 

developer.apple.com

encodeEncodable(_:forKey:)

 

Apple Developer Documentation

 

developer.apple.com

마찬가지로 경로를 따로 전달하지 않기 때문에,
encodeEncodable 메서드가 반환한 값이 저장되는 encodedData 속성으로 접근해
write 메서드를 호출해야한다.
또한 지금과 같이 인스턴스 메서드를 사용하는 경우 마지막에 finishEncoding 메서드를 반드시 호출해야함에 주의하자.

SecureDecode

encoding과는 반대로 NSKeyedUnarchiver를 사용한다.

NSKeyedUnarchiver(forReadingFrom:)

 

Apple Developer Documentation

 

developer.apple.com

 

파라미터를 통해 대상을 전달한다.

decodeDecodable(_:forKey:)

 

Apple Developer Documentation

 

developer.apple.com

decodeDecodable 메서드에는 변환할 형식과 Key를 전달한다.
마찬가지로 인스턴스 메서드를 사용했으므로 finishDecoding 메서드를 호출해야한다.

이후 디코딩된 인스턴스를 사용하는 건 기존과 다르지 않다.

Codable은

  • 구조체와 클래스를 모두 지원한다.
  • 기본 생성자와 encode를 제공한다.

위의 두가지의 특징만으로도 그 장점이 뚜렷하다.

NSCoding과 비교했을 때 init과 encode의 정의가 빠지므로 코드 자체가 단순해 지고,
클래스와 구조체 모두에게서 자유롭다보니 타입캐스팅도 필요 없다.

'학습 노트 > iOS (2021)' 카테고리의 다른 글

186. Managed Object and Managed Object Context  (0) 2022.02.12
185. CoreData  (0) 2022.02.05
181 ~ 182. User Defaults and Property List  (0) 2022.01.22
180. File Manager #3  (0) 2022.01.19
179. File Manager #2  (0) 2022.01.19