iOS에서는 멀티쓰레드를 지원해주는가?

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
참고한 블로그


들어가기에 앞서….

파이썬만 공부해봤던 나에게는(파이썬 멀티쓰레드 일반적으로는 지원하지않음) 이 개념이 조금 생소하지만, 일단 결과적으로 iOS에서는 멀티쓰레드를 지원한다!

프로세스(process): 실행중인 프로그램을 의미한다. 프로그램과 프로세스의 차이는 알고 있어야한다.
실행중이라는 의미는 말 그래도 사용자가 작성한 프로그램이 운영체제에 의해 메모리를 할당받았다는 것이고, 이러한 프로세스는 프로그램에 사용되는 메모리와 데이터등의 자원 그리고 스레드로 구성된다.

쓰레드(thread): 프로세스 내에서 실제로 작업이 수행되는 주체를 의미한다. 모든 프로세스는 한개 이상의 스레드가 존재해 작업을 수행한다.
두개 이상의 쓰레드를 가지는 프로세스를 멀티쓰레드 프로세스라고 한다.(multi-thread process)

멀티쓰레딩: 여러개의 쓰레드가 동시에 진행되는 것을 의미한다. 하나의 프로세스에 여러개의 쓰레드가 존재하고 이 스레드들이 프로세스의 자원을 공유하되 실행은 독립적으로 이루어지는 구조를 갖는다.

  • 장점
    • 메모리 공간과 시스템 자원 소모가 줄어든다.
    • 프로세스간 통신방법보다 간단하다 » 별도의 자원공간이 아닌 전역변수의 공간 또는 동적으로 할당된 공간(heap영역)을 이용해 데이터를 주고받음
  • 단점
    • 서로다른 쓰레드가 데이터와 heap영역을 공유하기때문에 한 쓰레드가 다른 쓰레드에서 사용중인 변수나 자료구조에 접근해 엉뚱한 값을 읽어올 수 있다.
    • 병목현상(과도한 lock)이 발생하여 성능이 저하될 가능성이 있다.

iOS에서는 멀티쓰레드를 지원한다!

그러나 멀티쓰레드 프로그래밍을 할 경우 고려해야하는 상황이 있다.

  1. UI업데이트 관한 작업들은 메인쓰레드에서 구현해야 한다.
  2. 쓰레드에 안전하지 않은(Thread-unsafe) 변수는 서로 다른 쓰레드에서 동시 접근하면 위험하기에 해당 작업을 반드시 신경써야한다.

leading, trailing과 left, right 무엇을 사용하는게 더 좋을까?

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


leading, trailing과 left, right 무엇을 사용하는게 더 좋을까?

결론부터 이야기하자면, leading, trailig을 이용하는 것을 더 권장한다.

오토레이아웃을 사용하다보면 우리는 leading, trailing, left, right 개념을 알게될텐데 기본적으로 텍스트를 왼쪽에서 오른쪽으로 읽는 우리에게는 leading을 쓰든 left를 쓰든 별 차이가 없을거라고 생각한다. 우리는 당연하게 글을 왼쪽에서부터 읽으니 텍스트의 시작점을 left로 지정을 해도되지만 만약 우리가 만드는 애플리케이션이 타 국가에도 사용되게끔 만드는데 그때에도 left를 사용하게 된다면 문제가 생기게 될 것이다. 그때 우리가 사용해야하는 것이 leading과 trailing이다.

  • leading: 텍스트의 시작점
  • trailing: 텍스트의 끝
  • left: 왼쪽
  • right: 오른쪽

따라서 애플리케이션의 지역화를 지원해야하는 경우 leading과 trailing은 기본적으로 사용되어야하며, 이런 저런 이유를 떠나서 left, right 딱 왼쪽과 오른쪽을 구분지어 사용하기 보다는 leading, trailing을 사용하여 조금 더 유연하게 코드를 짜는게 옳다고 생각한다. » LTR과 RTL을 모두 원활히 지원하는 코드로 !!

Alamofire란 무엇이고 어떻게 사용하는가?

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
참고한 블로그


간단하게…

iOS HTTP통신을 할 때 필수 라이브러리인 Alamofire에 대해서 아주 간단히 정리해보겠다!

Alamofire는 iOS, macOS를 위한 스위프트 기반 HTTP 네트워킹 라이브러리로 Apple의 Foundation networking 기반으로 인터페이스를 제공하여 일반적인 네트워킹 작업을 단순화한다. Alamofire는 함께 사용가능한(chainable) request/response 매소드들, JSON 파라미터, 응답 직렬화(response serialization), 인증(authentication) 그리고 많은 다른 기능을 제공한다.

  • 연결가능한(Chainable) Request/Response 메서드
  • URL / JSON / plist 파라미터 인코딩
  • 파일 / 데이타 / 스트림 / 멀티파트 폼 데이타 업로드
  • Request 또는 Resume 데이터를 활용한 다운로드
  • NSURLCredential을 통한 인증(Authentication)
  • HTTP 리스폰스 검증(Validation)
  • TLS 인증서와 공개 키 Pinning
  • 진행 상태 클로저와 NSProgress
  • cURL 디버깅 출력
  • 광범위한 단위 테스트 보장
  • 완벽한 문서화

즉, 정리해보자면 아래와 같다.

  • Alamore란 iOS, macOS를 위한 Swift 기반의 HTTP 네트워킹 라이브러리
  • Alamofire는 URLSession 기반이며 URLSession은 네트워킹 호출에서 모호한 부분이 많은데 Alamofire를 사용한다면 데이터를 접근하기 위한 노력을 줄일 수 있으며 코드를 더 깔끔하고 가독성 있게 쓰는 것이 가능해짐

Alamofire는 HTTP 네트워킹을 하는데 자주 사용하게 되는 코드나 함수를 더 쉽게 사용할 수 있도록 모아놓은 것

iOS에서 기본적으로 제공하고 있는 HTTP통신 방법은 여러가지가 있지만 URLSession을 이용한 방법이 있다. 모두가 같은 방법으로 사용하진 않겠지만 기본적으로 이러한 형태로 요청이 가능하다.

var request = URLRequest(url: URL(string: "https://api.github.com/users")!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { (data, response, error) in    
}

만약 여기서 Get요청이 POST요청으로 바뀐다고 한다면 아래와 같 파라미터들을 넘길 수 있습니다.

var request = URLRequest(url: URL(string: "https://api.github.com/users")!)
request.httpMethod = "POST"
let params = ["id":id, "password":password] as Dictionary

do{
  try request.httpBody = JSONSerialization.data(withJSONObject: params, options: [])
} catch {
  return
}

URLSession.shared.dataTask(with: request) { (data, response, error) in }

만약에 여기서 Header를 추가하게 된다면 아래를 추가해줘야한다.

request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")

즉 이렇게 성공, 실패를 또 나눠야한다.

var request = URLRequest(url: URL(string: "https://api.github.com/users")!)
request.httpMethod = "POST"
let params = ["id":id, "password":password] as Dictionary

do {
  try request.httpBody = JSONSerialization.data(withJSONObject: params, options: [])
} catch {
  return
}

request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")

URLSession.shared.dataTask(with: request, completionHandler: {(data, response, error) -> Void in
  if let response = response as? HTTPURLResponse, 200...299 ~= response.statusCode {
    //SUCCESS
  }else{
    //Failure
    }
})

이런식의 코드는 가독성은 물론이며, 조금의 설정만 바꾸면 많은 것이 변경되어야 해서 여러모로 불편한점이 있다. 그래서 이러한 불편한 것들을 개선시키는 라이브러리인 Alamofire를 사용해보자!

Alamofire는 Swift로 작성된 HTTP 네트워킹 라이브러리

설치는 CocoaPod으로 진행할 수 있다.

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '<Your Target Name>' do
    pod 'Alamofire', '~> 4.4'
end

이제 설치까지 완료 하였으니 한번 똑같은 URL로 요청해본다!

Alamofire.request("https://api.github.com/users", method: .get, parameters: [:], encoding: URLEncoding.default, headers: ["Content-Type":"application/json", "Accept":"application/json"])
            .validate(statusCode: 200..<300)
            .responseJSON { (response) in
            if let JSON = response.result.value
            {
                print(JSON)
            }
        }

끝이다!

일단 request메소드의 변수들을 보자 > request메소드를 Command + Mouse Left를 클릭해 정보를 보면

request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)

  • url: 요청할 URL
  • method: 요청 형식 (get, post, put, delete..등)
  • parameters: 요청 시 같이 보낼 파라미터
  • encoding: encoding
  • header: [String:String]형태로 보낼 수 있음

위에서 일일이 추가 하던 것과는 다르게 확연히 다른 차이를 볼 수 있으며, 훨씬 간결해서 보기도 편하다. 뿐만 아니라, request가 성공인지 실패인지를 필터를 하는 validate(statusCode: 200..<300)로 200~299사이의 statusCode결과만 받아올 수 있는 간편한 기능도 지원한다.

저렇게 요청을 하면 아래처럼 쉽게 요청을 할 수 있다.

{
        "avatar_url" = "https://avatars3.githubusercontent.com/u/1?v=3";
        "events_url" = "https://api.github.com/users/mojombo/events{/privacy}";
        "followers_url" = "https://api.github.com/users/mojombo/followers";
        "following_url" = "https://api.github.com/users/mojombo/following{/other_user}";
        "gists_url" = "https://api.github.com/users/mojombo/gists{/gist_id}";
        "gravatar_id" = "";
        "html_url" = "https://github.com/mojombo";
        id = 1;
        login = mojombo;
        "organizations_url" = "https://api.github.com/users/mojombo/orgs";
        "received_events_url" = "https://api.github.com/users/mojombo/received_events";
        "repos_url" = "https://api.github.com/users/mojombo/repos";
        "site_admin" = 0;
        "starred_url" = "https://api.github.com/users/mojombo/starred{/owner}{/repo}";
        "subscriptions_url" = "https://api.github.com/users/mojombo/subscriptions";
        type = User;
        url = "https://api.github.com/users/mojombo";
    },

        {
        "avatar_url" = "https://avatars3.githubusercontent.com/u/2?v=3";
        "events_url" = "https://api.github.com/users/defunkt/events{/privacy}";
        "followers_url" = "https://api.github.com/users/defunkt/followers";
        "following_url" = "https://api.github.com/users/defunkt/following{/other_user}";
        "gists_url" = "https://api.github.com/users/defunkt/gists{/gist_id}";
        "gravatar_id" = "";
        "html_url" = "https://github.com/defunkt";
        id = 2;
        login = defunkt;
        "organizations_url" = "https://api.github.com/users/defunkt/orgs";
        "received_events_url" = "https://api.github.com/users/defunkt/received_events";
        "repos_url" = "https://api.github.com/users/defunkt/repos";
        "site_admin" = 1;
        "starred_url" = "https://api.github.com/users/defunkt/starred{/owner}{/repo}";
        "subscriptions_url" = "https://api.github.com/users/defunkt/subscriptions";
        type = User;
        url = "https://api.github.com/users/defunkt";
    }

구체적으로 파보기

Request 만들기

import Alamofire

Alamofire.request(.GET, "https://httpbin.org/get")

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .responseJSON { response in
             print(response.request)  // original URL request
             print(response.response) // URL response
             print(response.data)     // server data
             print(response.result)   // result of response serialization

             if let JSON = response.result.value {
                 print("JSON: \(JSON)")
             }
         }

Alamofire은 비동기(asynchronously)로 네트워크 연동을 하기 때문에 서버로부터 응답을 받을 때까지 기다리지 않고 콜백을 통해서 응답을 처리해준다. 위의 예에서 처럼 요청에 대한 응답은 이를 처리하는 핸들러 안에서만 유효하므로 수신한 응답이나 데이터에 의존적인 동작들은 반드시 해당 핸들러 내에서 완료 해야한다.

Response 처리

Alamofire.request(.GET, "https://httpbin.org/get", parameters: ["foo": "bar"])
         .response { request, response, data, error in
             print(request)
             print(response)
             print(data)
             print(error)
          }

response 시리얼라이저는 수신한 데이터에 별도의 처리를 하지 않고 URL session 델리게이트로 부터 수신한 모든 정보를 그대로 전달할 뿐이다. Response나 Result 자료형의 장점을 활용할 수 있도록 다른 시리얼라이저를 활용하기를 권장합니다.

Alamofire는 Request뿐만 아니라 Upload, Download등 여러가지를 지원하며 그에 맞는 기능을 최대한 편하게, 간결하게 사용할 수 있도록 제공한다.

코코아 프레임워크, 코코아 터치, 코코아 팟이란?(설치 후 사용까지!)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
참고한 블로그


Framework

소프트웨어의 구체적인 부분에 해당하는 설계와 구현을 재사용이 가능하게끔 일련의 협업화된 형태로 클래스를 제공하는 것

클래스와 그에 속하는 메서드들을 미리 만들어놓고 필요할때마다 사용하는 라이브러리와 비슷하다고 생각이 들었다. 라이브러리는 필요한 사오항에서 원하는 메서드를 뽑아쓸 수 있도록 즉 완성된 메서드들이 구성되어있는 것을 의미힌다. 이에 반해 프레임워크는 개발자가 자신의 애플리케이션에서 틀이 짜여진 미완성의 메서드를 채워넣을 수 있는 형태로 정의되어있다(자바의 추상메서드 implement와 비슷) 또한 프레임워크는 개발을 하기위한 확장성을 가지는 코드를 포함하고 있다. 이것만 봐도 프레임워크의 역할은 프레임워크에서 내 어플리케이션에 메서드를 가져와 원하는 앱 개발 작업을 할 수 있도록 도와주는 것이라고 알 수 있겠다.

즉, 프레임워크는 앱개발을 편리하고 빠르게 할 수 있도록 뼈대를 미리 세워두는것, 살을 붙일 때 필요한 라이브러리 라고 생각할 수 있겠다.

스위프트에서의 약간 프로토콜과 비슷한 개념이라고도 생각했다.

Cocoa Framework

위에서 언급했듯 프레임워크는 앱의 뼈대를 만들어두는 것이고 이런 프레임워크를 여러개 모아 더욱 큰 프레임워크도 구성한 것이 애플의 코코아프레임워크 이다.

이중 터치와 관련된 디바이스의 애플리케이션을 개발할 때 사용하는 도구가 코코아터치 프레임워크인 것이다. 보통 iOS를 개발할 때 이 코코아 터치 프레임워크를 사용하게 된다. 그리고 이 프레임워크에 우리가 iOS 개발할 때 가장 많이 마주치게되는 UIKit, Foundation 프레임워크가 포함되어 있다. 이를 사용할때는 간단하게 import해주면 된다. Foundation은 말그대로 기반을 위한 도구들이 포함되어있다.(데이터타입, 콜렉션, OS 기능 등) UIKit는 iOS 를 위한 기반을 제공하며 인터페이스 그래픽을 구성하는 도구를 포함하고 있다.

CocoaPods

CocoaPods 공식사이트
CocoaPods is a dependency manager for Swift and Objective-C Cocoa projects. It has over 63 thousand libraries and is used in over 3 million apps. CocoaPods can help you scale your projects elegantly.

코코아팟은 Swift와 Objective-C 코코아 프로젝트를 위한 의존적인 매니저다. 굉장히 많은 라이브러리를 가지고 있으며 굉장히 많은 애플리케이션에 사용되고 있다. 당신의 프로젝트가 우아하게 돌아가도록 도와줄 수 있다.

만약 코코아팟 없이 프로젝트를 만들면, 오픈소스 프레임워크중에 원하는 기능이 있을 때 해당 프레임워크를 수동으로 설치해야하며, 프로젝트 안에 로드시키는 과정이 반드시 필요하다. 빌드 설정또한 우리가 직접 건들여줘야하며, 버전이 업데이트되면 위 작업들을 다시 새로해야하는 불편함또한 생긴다. 이런 불편함등을 해소하기 위해 만든것이 코코아팟이다. 코코아팟에 사용하고자 하는 라이브러리/프레임워크의 목록을 텍스트로 작성해두면 알아서 설치와 업데이트를 진행하고 Xcode 프로젝트와의 연결 및 설정도 도와준다.

Installation

코코아팟은 일단 Ruby로 제작되었다. 따라서 mac에서 설치하기 위해 아래의 명령어를 작성해줘야한다.

sudo gem install cocoapods

sudo 명령어는 일단 함부로 쓰면 안되기 떄문에 멈칫- 했지만 해야한다고 하니 일단 해보자.
함부로 쓰면 안되는 가장 기본적인 이유는 sudo를 사용하게 되면 시스템 전역에 설치가 되기 때문이다. 이렇게 함부로 모든 파일들을 sudo를 통해 설치하게 되면 기존 설치된 애들과 의존성문제로 꼬이게 되고 그러다보면 어디서부터 뭐가 잘못된지 모르게 안되는 것들이 하나, 둘 생기게 되는데 이를 해결하는 방법또한 찾을수가 없게된다.(이럴때는 결국 초기화가 답..그래서 정말 어지간한거는 무조건 brew로 설치하는게 가장가장가장!!!!!!!!!좋다!) 이렇게 설치하면 일단 코코아팟 설치가 된것이다.

쓰다보며 확인해봤는데(나는 아직 설치하기 전이었음) brew에도 cocoapods이 있더군요!

brew search cocoapods
brew cask install cocoaPods

Xcode 프로젝트에서 CocoaPods 사용해보기

설치가 끝났으니 이제 프로젝트에서 코코아팟을 사용해보도록 합시다.

터미널을 통해 해당 프로젝트가 있는 폴더로 이동한 뒤 해당 디렉토리에서 아래 명령어를 실행한다.

pod init

해당 명령어를 실행하고 나면 Podfile 파일이 생성된 것을 볼 수 있을 것이다.

이제 Podfile에 사용할 라이브러리 의존성을 추가할 것이다.

vi Podfile

설치가 완료되고나면 아래와 같은 노란글씨가 보일 수 있다.

이건, Podfile을 보면 “# platform :ios, ‘9.0’이라는 문구가 주석 처리되어있는데, Platform Version을 작성해주지 않고 주석처리를 하였기 때문에 CocoaPods에서 자동적으로 12.2 버전으로 assigning 했다는 경고로 직접 버전을 작성해 주셔야 한다면 주석을 풀어주면 된다.

이제 사용하고자 하는 Naver Map에 대한 라이브러리 설치는 완료됐다! (간-단)

자! 지금부터 주의해야하는 점은 아래와 같다.

우리는 지금까지 프로젝트를 열때 .xcodeproj 라는 확장자 파일을 실행했는데 코코아팟을 통해 라이브러리를 설치한 이후부터는 무조건! xcworkspace 라는 확장자 파일을 실행해야만 한다.

open cocoaPodsPractice.xcworkspace

이러고 열려진 xcode를 확인해보면 아래와 같이 기존과 동일한 프로젝트 파일 외에 Pods라는 것이 들어있는 것을 볼 수 있을 것이다.

이제 우리가 적용한 Naver Map을 Import하여 사용해보도록 하자!

import UIKit
import NMapsMap

class ViewController: UIViewController {

    var authState: NMFAuthState!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.

        let nmapView = NMFMapView(frame: view.frame)
        view.addSubview(nmapView)
    }
}

KVO(Key Value Observing)란 무엇인가?

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.


KVO(Key-Value Observing)

특정 키의 값의 변화를 감지하기 위한 기능

모델 객체의 어떤 값이 변경되었을 경우 이를 UI에 반영하기 위해서 컨트롤러는 모델 객체에 Observing을 도입하여 델리게이트에 특정 메시지를 보내 처리할 수 있도록 하는 것

즉, 변수에 코드를 붙여 변수가 변경될 때마다 코드가 실행되도록 하는 방법을 의미한다. property observers(willset , didSet)과 아주 유사한데 KVO는 타입 정의 밖에서 observe를 추가한다는 점이 다르다. KVO는 순수 스위프트 코드로는 그리 좋지 않은데, 그 이유는 Objective-c 런타임에 의존 하고 있기 때문이다. 그래서 NSObject를 상속받기 위해 @objc 를 반드시 붙여줘야 한다. 특히 KVO는 속성 각각에 @objc dynamic 을 붙여줘야 한다.

dynamic 은 objective-c 의 문법의 하나로, 특정 method나 function의 구현을 objective-c 런타임에서 하겠다고 결정하는 것이다.
예를들어, 하위클래스가 상위 클래스의 메서드를 override할 때 dynamic dispatch는 어떤 메서드의 구현해야 하는지 파악한다.

Example

class SomeClass: NSObject {
  @objc dynamic var value: String = ""
}

let someObject = SomeClass()

someObject.observe(\.value) { (object, change) in
  print("SomeClass object value changed to \(object.value)")
}

someObject.value = "test"  // TEST

위 코드는 NSObject를 상속받은 SomeClass클래스를 정의하고 있다. NSObject는 KVO지원을 위해서는 필수인데 KVO 기능이 NSObject에 구현되어 있기 때문이다. 그리고 유일한 프로퍼티인 value앞에는 dynamic이라는 수식어가 붙어있는데 이는 dynamic dispatch를 활성화 시키는 오퍼레이터다. 키패스(KeyPath = /.)이름을 이용해 프로퍼티의 주소를 찾게 해 주도록 하라는 정도라고 할 수 있을 것 같다.

핵심적인 부분은 옵저버를 심기 위한 observe()메소드이다. 이 observe()메소드는 기존 3.x 버전들과는 다르게 클로져를 이용할 수 있게 추가된 메소드다. 즉 해당 오브젝트에서 키패스로 지정된 프로퍼티의 값이 바뀌게 되면 클로져가 호출되게 된다. 귀찮게 오버라이드를 쓸 필요가 없어졌다. 심지어 컨텍스트 같이 몰라도 되는 값은 아예 보이지 않게 되었다. 이 부분은 정말 Swift친화적으로 바뀌었다. 예제의 observe()명령으로 someObject의 value라는 프로퍼티의 변화를 감지하기 시작한다. 결과적으로 마지막 줄에서 someObject.value 값을 바꾸자마자 “SomeClass object value changed to test” 라는 로그가 콘솔에 찍히게 된다.

좀더 이해하기 쉬운 예제로는 아래와 같다.

@objc class Person: NSObject {
    @objc dynamic var name = "Zehye"
}

let zehye = Person

그리고 이제 처음에 말했듯이 상태 변화를 감지하기 위한 코드를 붙여, 변수가 변경될 때마다 코드가 실행되도록 해보자.

zehye.observe(\Person.name, options: .new) { person, change in
    print("I'm now called \(person.name)")
}

이렇게 하면 새로운 변수가 들어오는지 보고 있다가 변수가 set되자마자 person의 이름을 프린트한다.

비록 KVO가 순수한 Swift 코드는 아니지만 Apple의 자체 API로 작업할 때는 더 좋다고한다. 이들이 모두 objective-c로 되어있기 때문에 모두 @objcdynamic 이기 때문이다.


야곰님께 질문

willset, didset과 KVO의 정의 정도는 알았지만, 두개의 쓰임에 있어서는 단순히 observe를 쓰냐 안쓰냐의 차이 정도로만 두개의 차이점을 생각했었는데, 나의 질문에 야곰님이 정말로 무릎을 탁 치게 만드는 답변을 내놓아주셨다. 내 질문은 단순히 위와 같다. willset과 didset 그리고 KVO는 서로 비슷한 역할을 하는 것인것 같은데 그 두개의 차이는 단순히 observe를 쓰냐 안쓰냐의 차이인걸까요?

답변

우리가 타입을 만드는 경우에는 자유롭게 willSet, didSet 등을 구현해줄 수 있겠지만, 다른 사람 혹은 외부 라이브러리에서 정의한 타입이라면 내부 소스를 마음대로 변경할 수 없겠죠.

그럴 때는 그 타입의 프로퍼티 등의 값이 변경되는지 KVO 방식으로 변화를 관찰할 수 있겠습니다.

즉, KVO는 상속 혹은 코드의 변경 없이 옵저빙을 할 수 있는 방법이겠다는 결론이네요 🙂

대………..박!