강좌2011/03/07 23:48

안녕하세요 문씨입니다


* 이 강좌는 먼저 올렸던 IB있든말든 SDK버전 문제 해결하기(http://cafe.naver.com/mcbugi/61663)편을 수정하여 내용을 보강 한것입니다.



이번 강좌에서는 매번 SDK가 버전업 하면 가지는 문제를 해결하기위한 해법을 제시합니다.




최신 SDK를 설치하게 되면 기존 버전들은 다 지워집니다

*3.x대의 SDK경우 계속 추가되는 방식이었으나 4.0부터 필요한 버전 외에는 제거됩니다.



그리고 실제로 빌드 가능한 버전은 버전으로 기기를 구별하는 좀 이상한 경우라서 아이패드용인 3.2와 아이폰용인 4.0이렇게 두개가 됩니다.

*나중에 아이패드가 4.0으로 나오면 버전 통합이 있을것으로 보입니다.


4.0 SDK 업데이트를 통해 애플은 xCode에 있었던 버전 선택 부분을 없에 버렸습니다.


이제는 최신 버전으로만 빌드하도록 유도하는 애플입니다.


당연히 최신 SDK를 설치한 상태에서 하위 버전을 사용하려고 해도 SDK가 없다고 뜹니다.




기존의 개발하던 프로젝트를 열면 Missing SDK가 뜹니다.





그렇다면 할수있는 방향은 네가지입니다.


  1. 최신 버전 전용으로 앱을 수정한다. 
    1. 개발자 편의용입니다. 빌드를 최신버전에 맞추고 그냥 맞추어 사용하는겁니다. 가장 편하고 몇가지 버려진 API만 수정해 주면 됩니다.


  1. 최신 SDK의 사용을 포기하고 현재 개발중인 버전을 유지한다. 
    1. 최신 버전을 포기하는 방법인데 별로 추천하지 못합니다. 최신 버전이 설치된 기기에서 테스트 해봐야 하는데 이 방법으로는 xCode에서 기기를 인식하지 못하고 기기에서 앱에 어떤 에러를 보여줄지 테스트 해보지 않고는 모릅니다. 그리고 계속 바뀌어 나가는 애플에 열심히 배우고 따라가는 취지에는 맞지 않습니다. 


  1. 최신 SDK를 설치하고 직접 구 SDK에서 SDK데이터를 가져온다. 
    1. /Developer/Platforms/iPhoneOS.platform/Developer/SDKs 위치에 있는 폴더들이 SDK 버전별로 있는것인데 구 SDK를 따로 설치한다음 위 폴더로 폴더들을 복사해 넣고 따로 설치한 구 SDK 는 지우는겁니다. (용량낭비니) 이 방법은 좀 변칙적인 방식입니다. 애플에서 구버전을 제공하지 않으니 직접 넣어주는 방법인데 시뮬레이터에서 실행 할수가 없습니다.시뮬레이터용 SDK를 복사해 넣어도 문제의 시뮬레이터가 SDK 정보가 없어서 에러납니다. 2번과 같은이유로 별로 추천하지는 않습니다. 


  1. 최신 SDK를 지원하면서 구 버전에도  설치할수 있도록 만든다.
    1. 애플에서 유도하고 있는 방법입니다. 이미 해외 앱들은 당연한듯 개발하고 있고 이방법을 사용하는게 최고인듯 하지만 위 세가지 방법보다 제일 어렵고 번거롭습니다. 


* 4.0기기에 설치하면 멀티테스킹(이라 쓰고 FAS라 읽는다)도 지원되고 3.0기기에 설치하면 지원 안되지만 실행은 됩니다.


버전 호환 문제가 있는 이유는 호환시키는 방법을 잘 모르기 때문입니다.

구버전으로 빌드하면 별다른 설정 없이도 최신 버전에서 작동합니다. 문제는 그 반대 입니다. 최신 버전으로 빌드한 앱을 올리면 그 버전을 사용하는 유저만 설치가 가능해집니다. 그래서 이전부터 모르고 최신으로 올렸다가 유저들의 원성으로 다시 하위 버전으로 빌드해서 올리는 경우가 허다했습니다.



최신버전으로 빌드하면서 구버전도 지원하려면 두가지가 필요합니다.


프로젝트 설정과 코딩 규칙입니다.







  1. 설정

프로젝트 설정

*이 스샷은 SDK4.0입니다 내용을 보완하는 시점에는 4.1 베타 3까지 나와있습니다. 그리고 선택가능한 버전은 아이패드용인 3.2와 아이폰용인 4.1입니다.



프로젝트 설정에서 위 스샷 처럼 Base SDK는 최신 버전의 Device를 선택합니다

그리고 Deployment Target을 지원할수 있는 최하 버전까지 선택합니다

프리뷰로 나와있는 xCode 4.0은 (알아)보기좋게 바뀌어 있습니다



슬라이더로 구성되서 어느 버전부터 어느 버전까지 설치가능한지 한눈에 보입니다.


프로젝트 설정은 이정도입니다.


기존의 Missing SDK를 표시하는 프로젝트가 있다면 위와같이 수정하면 됩니다.


그리고 이렇게 수정하면 구버전의 기기 테스트에서도 특별히 바뀐 API가 없다면 잘 돌아갑니다.




프레임워크 설정


    다음은 프레임워크 설정입니다.


    프레임워크 중에는 3.x부터 추가된것도 있고 4.x부터 추가된것도 있습니다

    4.0부터 추가된 프레임워크의 예로 iAd가 있습니다.


    문제는 이 프레임워크를 넣어두면 3.x의 기기에서 튕긴다는 겁니다.


    프레임워크를 못찾았다는 이유입니다.

    앱은 로드될때 연결된 프레임워크를 확인합니다.


    그래서 로그도 못찍어보고 바로 튕기는 겁니다.


    앱에서 프레임워크에 대해 반드시 필요하다고 설정해 놓았기 때문에 일어나는 문제인겁니다.


     Target의 항목의 정보를 엽니다

    연결된 라이브러리로 프레임워크들이 있습니다


    종류(Type)을 보면 Required가 보입니다.


    이걸 있어도 그만 없어도 그만이라는 뜻의 Week으로 바꿔줍니다.

    이제 설정은 끝났습니다.

    2. 코딩


    컴파일러에서 구분


      컴파일러는 #if문을 사용하는 방법으로

      빌드하는 SDK버전을 인식해서 


      상황에 맞는 코드를 사용하도록 구분하는것입니다.


      보통 컴파일러 조건문인 #if는 여러 버전에서 같이 사용되는 프레임워크나 직접 만든 공용 클래스에서 많이 사용됩니다.


      예) 

      같은 프레임워크도 여러 버전에서 빌드되는 일이 있기때문에


      #if 문을 사용하여 빌드 당시 버전을 체크합니다

      위 스샷의 경우 

      #if __IPHONE_4_0 <= __IPHONE_OS_VERSION_MAX_ALLOWED

      를 사용했는데 __IPHONE_4_0의 경우 상수라 보면 됩니다. 고정된 값입니다.


      __IPHONE_OS_VERSION_MAX_ALLOWED는 빌드하는 버전에 맞춘 수치로


      빌드버전마다 선언되는 버전이 다릅니다.


      아이패드용 3.2의 경우

      아이폰용 4.1의 경우

      즉 해당 빌드 버전을 구별해서 사용할 코드는 사용하고 사용해서 안되는 코드는 빌드에 사용되지 않도록 할수 있습니다.


      즉 위 예시는 빌드가 4.0 이상 일때만 저 #if안에 들어있는 코드를 컴파일 할때 사용하도록 한것입니다.



      제가 사용중인 공용 클래스중 하나가 멀티다운로더인데

      3.x대의 빌드의 경우 NSThread를 사용하였고 4.x이상의 경우 GCD를 사용하도록 구성되어 있습니다.



      이 컴파일 조건문은 빌드시 적용되는 것으로 앱 실행중에 버전에 맞추어서 작동하는 것과는 다른겁니다.


      주의하세요







      런타임상에서 구분


        컴파일러에서 조건문을 사용하는 경우는 빌드할때 조건에 맞는 코드가 생성되는겁니다..


        예를 들어 NSLog는 디버깅할때만 사용하는것이기에 시뮬레이터에서 돌릴때는 사용하고 기기에서 돌릴때는 사용하지 않도록 하는 경우도 있습니다.


        즉 컴파일시 분간하는 것은 실제 앱이 모든 기기에서 모든 버전에서 호환되도록 하는것과는 거리가 있습니다.


        즉 코드로 기기의 상태를 확인해서 대응할 필요가 있습니다.



        보통 버전별로 다르기 때문에 시스템의 버전을 찾아다 비교하는 방식을 사용하기도 합니다만

        하지만 이 방법은 별로 추천하지는 않습니다.


        같은 버전이라도 상황에 따라 지원안하는 경우도 있기 때문이죠.


        그저 이 버전부터는 지원되니 그냥 사용한다는 사고방식은 문제가 있습니다.



        추천하는 방식은 기능별로 나누어서 확인 하는 방법입니다.

        단순히 iOS버전별로 구분하는 것이 아니라 기기 종류에 따라 구분도 해야 하기 때문이죠.


        기기는 점점 종류가 많아지고 있습니다.


        애플조차 처음에 앱의 기능 구분을 전화와 GPS, 카메라가 있는 아이폰과 없는 터치로 구분했었습니다만, 나중에는 이 앱이 어디까지 기능이 필요한지 정의 하도록 하였습니다. 그러면 스토어에서 해당앱이 어느 기기까지 설치 가능한지 분석하여 설치 유무를 알려주었습니다.




        그럼 어떻게 iOS버전에 맞추는지 보겠습니다.



        • C함수는 심볼 체크를 한다.

        아무리 Objective-C만 한다 하더라도 나중에 좀더 심도있는 기능을 구현하려면 C함수의 사용은 필연적입니다. 그리고 C 함수는 프레임워크에 따라 아직 추가가 안된경우도 있을수 있습니다.


        정의되지 않은 C함수를 사용하면 당연히 심볼에러를 내면서 튕기게 됩니다.

        그 예중 하나가 GCD의 Dispatch함수입니다.

        이 함수들은 iOS4.0부터 추가된것이라 그냥 하위버전에서 사용되면 당연히 문제가 됩니다.


        심볼체크하는 방법은 간단합니다 그 함수명을 쓰는 겁니다.

        단 실행이 아닙니다.


        예를 들어서 queue = dispatch_queue_create(“test”, NULL); 함수가 있으면

        이 코드는 실행한겁니다


        심볼 체크하려면 if (dispatch_queue_create) 를 합니다.


        C함수는 기본적으로 끝에 인자가 있던 없던 ()가 붙습니다. 


        그래서 foo()는 실행하는 코드고 foo면 함수의 포인터 주소를 돌려주기 때문에 그 함수가 존재하는지 확인할수 있습니다.


        실행 -> foo();

        심볼 체크 -> if (foo != NULL) { ...



        • Objective-C함수에서는 respondsToSelector

        Objective-C의 객체의 경우 respondsToSelector함수로 확인합니다. 객체에 해당함수가 있으면 YES가 돌려집니다. 

        이 함수는 보통 델리게이트 선언중 @optional을 사용하는 객체에서 단골 코드입니다.


        그 예를 들면

        4.0부터 로컬푸시 기능이 제공됩니다. 그렇다면 시스템의 버전이 4.0이상인지 확인하는 것보다는


        이렇게 UIApplication객체를 불러와서 respondsToSelector함수로 scheduleLocalNotification:함수가 있는지 확인하는 겁니다.


        함수가 있으면 지원한다고 판단하고 기능을 사용하는 것입니다.


        보통 이런식으로 판단하지만 무작정 함수가 있다고 사용하는 것도 금물입니다.

        그 기능에 대해 확실히 조건을 알아두어야 한다는 겁니다.


        그 예로 백그라운드의 경우 UIApplication에 백그라운드 관련 함수가 있다고 해도 기기에 따라 백그라운드를 지원안하는 경우가 있기 때문에 UIDevice에 multitaskingSupported속성이 YES인지 확인해봐야 합니다.



        • 클래스를 찾는 NSClassFromString

        1, 2까지 이해를 했어도 이 함수의 존재 없이는 호환성 성립 못하는 것이 있습니다.


        이 NSClassFromString함수는 문자열을 그대로 클래스 명으로 인식해서 클래스를 돌려주는 역활을 합니다.


        이것이 없어서는 안되는 이유가 있습니다.


        먼저 예문을 보겠습니다.

        로컬푸시를 하기 위해 처음 객체를 만드는 부분입니다.


        이 로컬푸시는 iOS4.0부터 추가되었는데 

        이 코드 한줄 때문에 하위버전 기기에서는 로그 한번 못 찍어보고 튕겨버립니다. 


        원인은 프레임워크 존재 체크랑 비슷한 이유인데 한마디로 UILocalNotification을 못찾아서 그런겁니다.


        그렇다면 2번 방법으로 기능 유무를 확인 하더라도 코드 실행도 안했는데 단지 코드가 있다는 이유로 튕긴다면 방법이 없다고 볼수 있습니다.


        심볼에러가 나는 부분은 정확히 보자면 alloc앞의 클래스 부분입니다.



        그래서 코드를 이렇게 바꾸면 튕기지 않습니다.


        직접 선언한 부분을 Class변수로 교체했기 때문에 심볼 체크가 일어나지 않습니다.


        클래스라는것은 알지만 어떤 클래스인지는 모르기 때문이죠. 


        하지만 Class로 선언한 className은 NULL입니다.


        이대로 사용해도 문제가 됩니다.


        생성이 안되기 때문이죠.


        그렇다고 Class className = UILocalNotification;으로 바꿀수도 없는 노릇입니다.


        그래서 사용하는 것이 NSClassFromString입니다.

        문자열을 넣으면 Class로 돌려주지만 그 클래스가 없으면 NULL이 돌려집니다.


        물론 위 코드 방법도 안전한 방법은 아닙니다.

        확실히 하자면 이렇게 하는게 정답일겁니다.




        그렇다면 UILocalNotification *push부분은 어떤것일까요?

        클래스로 사용한 부분은 문제가 되고 변수 부분은 문제가 안된다는 것은 이상하게 보일수도 있습니다.

        당연히 문제 없습니다. 객체 타입 선언은 어디까지나 컴파일러에서 인식하기 위한 것일뿐 빌드후에는 id push로 인식됩니다. 어떤 Objective-C객체든 그것을 잡고있는 변수는 그저 메모리 주소를 가지는 변수일 뿐입니다.


        실제로 다른 객체 선언을 해놓고 선언되지 않은 함수를 사용하면 경고는 뜨지만 객체에 함수만 제대로 있다면 별 문제 없이 실행됩니다.






        이렇게 버전업을 할때마다 추가되는것이 있고 제거되는 것도 있습니다.


        호환테크닉에 정말 중요한것은 이해입니다. 


        사용하고자 하는 기능을 이해하고 버전의 변화를 거치면서 무엇이 바뀌었는지 확실히 알아둘 필요가 있습니다. 



        여기까지 버전별 호환 테크닉이었습니다. 


        감사합니다.

        저작자 표시 비영리 동일 조건 변경 허락
        Posted by 문씨
        강좌2011/03/07 23:24

        안녕하세요 문씨입니다.


        이번 강좌에서는 멀티스레딩의 3탄 Grand Central Dispatch에 대해 다루어 보겠습니다.


        제가 Grand Central Dispatch(이하 GCD)라는 단어를 처음 접한것은 2009년에 열린 WWDC09였습니다.


        당시 키노트에서 iPhone 3GS를 발표했고 참가자들에게는 지금은 자연히 쓰고있는 스노우레오파드 첫 개발자 프리뷰 버전 설치 디스크를 나누어 주었습니다.



        그리고 키노트 후에 있는 Kickoff에서 스노우레오파드에 새로 들어간 기술로 GCD가 소개되었었습니다.



        당시는 오로지 아이폰 개발만 집중하였기에 새로운 OS에 새로운 기술이 들어갔구나 싶은 정도였습니다. 어차피 아이폰에는 안들어가던 기술이니.


        그저 OpenGL같은 새로운 기술중 하나라고만 생각했습니다. OpenGL도 그래픽 처리에 관련된 기술이라는 것만 알뿐 어떻게 쓰는지는 모르는것과 같은거죠.



        GCD의 진수를 보여준 부분은 지구를 돌고있는 엄청난양의 우주쓰레기와 인공위성을 시뮬레이션 하는 부분이었습니다.



        엄청나게 많은 파티클은 FPS를 떨어트리면서 화면에 버벅임을 보여줍니다.



        그러나 GCD를 사용하자 화면이 부드럽게 돌아갑니다.

        • 위 영상은 애플 아이튠즈의 WWDC2009 세션 700 Kickoff영상으로 볼수있습니다.

        •  해당 부분만 뽑은 영상

        보다시피 GCD기술은 멀티프로세싱에 사용되는 기술입니다.

        • 멀티스레딩보다 멀티프로세싱이라 한 이유는 멀티스레딩 보다 시연에 사용된 맥프로가 16코어 시스템으로 멀티 CPU를 활용하는 데모이기 때문입니다.



        우주를 시뮬레이션 한다는 방대한 스케일은 신선한 충격을 주었습니다. 


        하지만 이 GCD는 아이폰과는 무관한 기술이었습니다.


        그래서 한편으로는 잊고있었습니다.


        4.0베타가 나오면서 그 단어가 언급되었지만 인지하지 못하고 있었고


        WWDC2010에서 삼일째에 아이폰을 위한 GCD라는 세션이 참석자 폭주로 들어가지도 못한 사람들이 있다는 소식을 듣기 전까지는 전혀 모르고 애플 개발자와 당면한 문제를 해결하느라 논의 중이었습니다.

        나중에 이일을 들으니 이 GCD는 그냥 넘어갈 부분이 아니라고 판단했고 이미 놓쳐버린 세션이라 급하게 애플개발자에게 직접 물어보고 배웠습니다.


        물론 배운건 기본개념이지만 그것만이라도 충분했다고 봅니다.


        나중에 마지막날인 금요일에 세션에 참석못한 사람들의 요청에 따라 다시한번 재방을 했습니다.

        2009년에 보았던 GCD, 그리고 2010 전세계에서 모인 참석자들을 난리치게 만든 GCD.


        GCD에서 느껴지는 개발자에 대한 배려.


        하나하나 살펴보겠습니다.

        애플은 예전부터 LLVM이라는 오픈소스 프로젝트에 투자를 해왔습니다. 애플이 밀고있는 새로운 컴파일러로 GCC를 대체할 물건이죠. WWDC09에서도 해당 세션이 있긴했습니다.


        애플이 더이상 GCC를 개발하지 않고 LLVM으로 가려고 하는 이유를 다음과 같이 묘사했습니다.


        결론은 하도 뜯어 고쳐서 새로운 기술에 적용하기 어렵다는 겁니다.



        이제 LLVM도 2.0까지 개발되고 있고 최근에 나올 준비중인 xCode4 프리뷰2는 GCC를 기본 프로젝트 설정으로 되어있는 xCode3와는 달리 LLVM2.0을 기본으로 지원하고 있습니다.


        일단 당장 눈에 띄는 GCC와 LLVM의 차이로는 빌드 속도입니다.


        그외에도 Objective-C++을 빌드할때 걸리는 시간 차이도 있지만 여기서는 대략 넘어가겠습니다.




        아무튼 이 LLVM덕분에 가능해진 기술이 Block과 GCD입니다.



        * Block과 GCD는 iOS4.0부터 지원됩니다.


        iOS4.0으로 업데이트 하면서 멀티테스킹이 지웠되었습니다.

        덕분에 앱을 종료하지 않고 다른 앱에 갔다가 올수있게 되었습니다.

        하지만 동시에 불편해진 부분도 있습니다.


        바로 메모리 문제입니다. 여러앱이 동시에 실행되기 때문에 메모리 여유는 없을수밖에 없습니다. 그만큼 앱의 활동영역이 줄어들기 때문에 쾌적하지 못한 반응을 보여주기도 합니다.


        하지만 애플은 이 문제에 대해 다른 안을 제시했습니다. GCD기술을 제공함으로써 메모리 부족의 문제를 로직적으로 개선된 성능으로 커버하도록 한겁니다. 어떤 의미로는 병주고 약주고라 할수있습니다.




        그럼 GCD를 사용하려면 우선 Block코딩을 알아야 합니다.


        Block코딩 없이는 GCD가 성립되지 않기 때문이죠.

        WWDC10에서 세션113을 보면 Block에 대한 내용이 있습니다.

        기존에 C를 하던분들은 알수도 있지만 보통 closure라고 부르는 코딩 기법입니다.

        Objective-C에서는 Block이라고 부르는거죠.


        위 슬라이드로 눈치챈 분도 있겠지만, 블럭은 간단히 말하면 코드를 객체화 할수 있다는 겁니다.


        블럭은 우선 “ ^ ”로 시작합니다. ^ 블럭의 시작임을 알리는 구분자인것이죠.


        먼저 C에서의 함수 구조를 볼까요?


        int foo( int arg1, bool arg2) {


             ...


             return 0;

        }



        먼저 돌려주는 변수형이 있고 (없는경우 void) : int


        함수명이 있습니다 : foo


        그리고 ‘ ( ‘ 열고 ‘ )’ 닫으면서 파라메터(arguments)들이 들어가게 되죠. 


        그리고 마지막으로 스코프 ‘ { ‘를 열고 내부에 코드를 넣은후 돌려줄 변수형이 있으면 return한후 스코프 ‘ } ‘를 닫습니다.


        이것이 C함수의 기본이죠. *Objective-C가 아님


        블럭에서는 단순히 이 구조에 함수명만 없는것입니다.

        물론 앞에는 구분자(^)를 넣구요.


        ^ int (int arg1, bool arg2) {


             ....

           

              return 0;

        }



        그래서 가장 기본의 블럭 코딩의 형태는


        ^ <리턴변수형> ( <파라메터들> ) { ...<코드>... }


        즉 돌려줄 변수타입, 파라메터, 본문 코드로 구성됩니다.

        그리고 리턴 변수형과 파라메터는 없을경우 안 쓰고 무시 할수도 있습니다.



        파라메터가 없는경우

        ^ int { ... } 


        리턴이 없는경우

        ^ (int arg1) { ... }


        둘다 없는경우

        ^ { ... }




        물론 블럭 내부에 들어가는 코드는 Objective-C로 사용하면 됩니다.


        블럭코딩의 응용기법은 많지만 너무 많은 관계로 가장 기본만 설명하겠습니다.


        이부분은 애플도 강의를 포기한 부분입니다. 나머지는 알아서 공부해서 필요한걸 찾아 쓰라는 겁니다.

        블럭코딩이 들어간 예를 하나 들겠습니다.



        UIView에는 애니메이션 효과를 구현해주는 기능이 있습니다.


        예를 들어 뷰를 이동시킨후 투명도를 조절해서 사라지게 하는 효과를 구현한다 치겠습니다.


        view.center = CGPointMake(x, y);

        [UIView beginAnimations:nil context:nil];

        [UIView setAnimationDuration:2.0];

        [UIView setAnimationDelegate:self];

        [UIView setAnimationDidStopSelector:@selector(viewHide)];

        view.center = CGPointMake(x2, y2);

        [UIView commitAnimations];


        먼저 해당 뷰를 이동시킵니다.

        이동하는 애니메이션이 종료되면 viewHide라는 함수를 실행합니다.


        그럼 그 함수에서


        투명도설정으로 사라지도록 합니다.


        - (void)viewHide {

        [UIView beginAnimations:nil context:nil];

        [UIView setAnimationDuration:2.0];

        view.alpha = 0.0

        [UIView commitAnimations];

        }


        이러식으로 애니메이션을 선언하고 재생 시간을 정해준후 설정하는 방법으로 은근히 코드가 길어집니다.


        그리고 문제는 첫 애니메이션이 끝나면 viewHide라는 함수를 호출함으로써 다음 애니메이션을 호출하게 되는 점입니다. 함수라는 점과 조작하는 view가 계속 살아있는지 봐줘야 하는 부분도 문제입니다.

        view가 전역이 아니라 처음 애니메이션을 실행하는 곳에서 사용되는 객체 변수라면  viewHide에서 사용하기 위해 전역으로 올리거나 beginAnimations함수에 인자로 객체를 같이 보내줄수도 있습니다.


        이방법이나 저방법이나 번거롭기는 마찬가지죠



        그래서 이번에 추가된 새로운 함수로 

        + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations

        가 있습니다.


        간단하게 duration은 시간이고 animations에는 블럭코딩이 들어갑니다.



        하지만 위에서 했던 것은 이중 애니메이션입니다. 


        그럼 다른 함수로는

        + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion

        가 있습니다.


        completion인자에 블럭 코딩을 하나더 받는걸로 애니메이션에 끝났을때 호출되도록 한것입니다.


        코딩을 하다보면 줄이 너무 길어져서 줄넘겨본적이 있을겁니다.

        인자가 너무 많아서 넘기기도 하고 배열같은건 아예 한줄한줄 넘기는것이 알아보기 쉽기도 합니다.


        블럭코딩을 사용한 함수들은 자연스럽게 줄을 넘겨서 코딩 할수밖에 없게 됩니다.



        앞서 했던 코딩을 블럭코딩 방식으로 구현해 보겠습니다.

        view.center = CGPointMake(x, y);

        [UIView animateWithDuration:2.0

         animations:^{

         view.center = CGPointMake(x2, y2);

         }

         completion:^(BOOL finished){

         [UIView animateWithDuration:2.0

          animations:^{

          view.alpha = 0.0;

          }];

         

         }];



        어떤가요 이해가 가나요?


        2초짜리 애니메이션을 animations에 넣고 작동 시키고 애니메이션이 끝나면 completion인자에 다른 블럭코딩을 넣어서 새로운 애니메이션을 실행했습니다.



        블럭코딩의 특징은 함수내에서 선언된 객체도 유지된다는 겁니다.


        시간차 때문에 코드가 실행되기 전에 객체가 해제되지 않을까 걱정할수도 있습니다만 그 코드 내부의 객체는 전부 retain올려서 관리됩니다.



        블럭코딩을 받는 인자의 경우 API마다 받는 조건이 있습니다.

        블럭코딩은 함수와 비슷한 구조라서 리턴이 있고 인자가 있습니다.


        앞서 본 애니메이션 함수에 animations인자의 경우

        (void (^)(void))animations

        라고 선언되어 있습니다.


        이경우 첫 단어는 void이므로 리턴할것이 없고, 

        (^) 이후의 가로의 내용이 (void)이므로 인자도 없습니다.


        그래서 animations인자에는  ^{ 으로 시작하면 되는겁니다


        그리고 애니메이션 종료후 실행되는 블럭인 completion은 

        (void (^)(BOOL finished))completion

        라고 선언되어 있습니다.


        리턴은 void이므로 당연히 없습니다 근데 이번에는 BOOL finished가 하나 있습니다


        그러니 당연히 ^ (BOOL finished) { 로 만들어서 사용해야 합니다

        이 finished인자는 애니메이션이 재생하다가 도중에 멈추는 경우를 비교하기 위한 겁니다. 재생 도중에 다른 곳에서 애니메이션 취소를 할수도 있기 때문이죠.

        보통 YES입니다.

        이제 블럭코딩에 대해 어느정도 이해했으면


        본격적으로 GCD에 들어가보겠습니다.


        GCD는 C언어로 되어있는 스레드 관리 기술입니다.

        계층으로는 UIKit보다 하위로 


        Application층과 UIKit층이 대부분 Objective-C를 사용하고 libSystem(예: SQLite)등이 C로 되어 있습니다.


        커널층은.... 넘어가도록하죠.


        즉 깊을 수록 어려워집니다.



        GCD를 사용하려면 먼저 #include선언을 해주어야 합니다.

        #include <dispatch/dispatch.h>


        기왕이면 전역에 선언해 주고 사용하는게 편하겠죠.



        먼저 블럭 코딩을 다룬것은 GCD는 블럭코딩을 이용했기 때문입니다.

        • GCD는 Block-based API입니다.



        멀티 스레딩 강좌 1탄 NSThread편과 2탄 NSOperation편에서 스레드의 기초와 스레드 응용기술을 다루어 보았습니다.


        하지만 이 두 클래스의 스레드를 다루는 공통된 방식은 스레드용 함수를 만든다는 점입니다.


        객체 지향방식인 Objective-C라서 이런 형태가 된것이지만 오히려 구현하기 힘들게 만드는 문제도 있습니다. 너무 객체지향을 추구하다보니 독이 된 셈이죠.


        NSOperation덕에 스레드를 작업단위로 구분해서 할수 있게 되었지만 몇가지 간단한 작업을 위해서 클래스를 따로 만들어야 한다는 점은 여전히 불편합니다.


        그에 비해 블럭코딩 기반의 GCD는 좀더 편히 코딩을 할수 있고 무엇보다 이미 해둔 코딩에 어느정도 쉽게 적용 가능하다는 장점이 있습니다.





        멀티 스레딩 강좌로 원래 2편에 GCD를 할 예정이었습니다만, 개념 설명이 필요 하기 때문에 NSOperation을 다루었습니다.


        큐 방식을 사용하는 NSOperation은 GCD와 비슷합니다.


        WWDC에서 만난 애플 개발자에게서 GCD의 실체를 알고나서 물어본 질문은 Objective-C로 된것은 없는가? 였습니다.


        답은 NSOperation이라고 합니다.


        사실상 NSOperation은 GCD의 객체형이라고 볼수있다는 것이죠.



        NSOperationQueue가 있듯이 GCD에는 dispatch_queue_t가 있습니다.


        비교해 볼까요?


        큐를 만드는 부분입니다.


        NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];


        dispatch_queue_t dQueue = dispatch_queue_create(“com.apple.testQueue”, NULL);




        NSOperationQueue의 경우 오토릴리즈가 걸려있어서 이후 일이 다 끝나면 알아서 해제 됩니다


        하지만 C언어의 GCD는 생성했으면 해제를 해주어야 합니다


        dispatch_release(dQueue);


        물론 리테인 카운터 개념을 따르고 있기 때문에 dispatch_retain이라는 리테인 거는 함수도 있습니다.



        GCD의 첫 함수 입니다. 


        dispatch_queue_create


        큐를 생성하는 함수이고 인자로는 char배열의 label과 attr이 있습니다.

        label의 경우 큐에 이름을 지어주는 격으로 그다지 없어도 그만입니다. (NULL처리 해도 문제 없음)

        attr경우 큐의 속성으로 설정하는 인자로 추정되지만 현재 API문서에는 안쓰고 있다고 합니다. 그냥 NULL


        dispatch_queue_t는 변수 형으로 큐를 선언할때 사용됩니다.





        이제 큐를 만들었으면 작업을 넣어보겠습니다.


        NSOperationQueue의 경우 작업을 NSOperation으로 생성해서 넣었지만


        GCD는 블럭을 작업 단위로 판단 합니다.


        dispatch_async(dQueue, ^{

                 ...

        });



        dispatch_async는 큐에 블럭을 넣는 일을 하는 함수입니다.


        당연히 인자는 큐와 블럭을 받습니다.




        다른 함수로 dispatch_sync도 있습니다.


        이 두함수의 차이는 코드를 실행하는 동안 기다리는 유무입니다.



        async의 경우 큐에 블럭을 넣는 일을 한후 바로 다음 코드로 넘어가지만

        sync는 큐에 넣고 그 일이 끝날때까지 기다립니다.




        기본 기능을 봤으니 이제 응용해보겠습니다.



        멀티스레딩 NSOperation편에서 이미지를 다운 받아서 메인스레드에 영향을 안주고 이미지뷰에 띄우는 기능을 구현해 보았습니다.


        이미지뷰에 카테고리로 함수를 만들고 내부에서 NSOperation으로 작업을 했었습니다.




        순차적으로 본다면


        우선 이미지뷰(UIImageView)를 준비하고


        데이터를 다운받고


        이미지로 로딩한다음


        이미지뷰에 넣는것입니다.




        이걸 코드로 구현해보겠습니다.



        일단 이미지뷰는 화면에 이미 만들어져서 넣어져있다고 가정하고


        UIImageView *imageView = ...




        1. NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@”주소”]];


        2. UIImage *image = [UIImage imageWithData:data];


        3. imageView.image = image;



        1과 2는 다운받는 시간과 순간적 로딩하는 시간이 있기때문에 NSOperation에서는 이부분을 스레드로 돌린뒤 3번을 메인스레드로 돌아와서 지정하도록 했습니다.


        근데 이미 코드가 상당합니다 아무리 간단해서 클래스를 만들고 지정하는 함수등 해야할 부분이 은근히 있습니다.



        그런데 위 코드를 GCD에서는 어떻게 하는지 보겠습니다.


        dispatch_async(dQueue, ^{

            

                NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@”주소”]];

                UIImage *image = [UIImage imageWithData:data];


                dispatch_sync(dispatch_get_main_queue(), ^{

                        imageView.image = image;

                });

        });


        함수가 하나더 나왔군요 

        dispatch_get_main_queue();


        단어뜻 그대로 메인 스레드용 큐를 돌려주는 함수입니다.



        즉 위의 코드는 먼저 전체 블럭을 dQueue에 넣습니다.

        dQueue에서 실행되는 블럭은 다른 멀티 스레드로 돌아갑니다.


        블럭이 스레드에서 실행되면서


        먼저 데이터를 다운 받는 코드를 실행합니다.


        그다음 이미지를 불러옵니다.



        그리고 안에 구현된 두번째 블럭을 이번에는 메인큐(메인스레드)에 넣습니다.


        이렇게 함으로서 화면에 관련된 코드는 메인스레드 상에서 돌렸습니다.


        NSOperation으로 구현했을때와 사실상 같은 구조입니다.


        다만 더 코드가 짧습니다.



        NSOperation이나 NSThread를 사용하는 경우 NSAutoreleasePool을 사용해야 하지만

        블럭코딩의 경우 필요가 없습니다.



        NSOperationQueue의 경우 최대동시 작업 진행 설정이 있습니다.

        기본값은 무한대라 큐에 작업을 넣는데로 바로 스레드가 생성됩니다.


        물론 스레드가 너무 많아서는 안되기 때문에 보통 5개정도로 제한해둡니다.



        하지만 GCD의 큐의 경우 무조건 한번에 하나만 실행됩니다.



        큐에는 직접 만든 큐와 글로벌큐, 그리고 메인큐가 있습니다.


        메인큐는 메인스레드상에서 돌리게 되어 있고

                dispatch_get_main_queue()


        직접 만든 큐는 하나의 스레드를 할당 받고 사용합니다.

                dispatch_queue_create


        글로벌 큐는 큐에 넣는대로 바로 스레드를 만들어서 붙여줍니다.

                dispatch_get_global_queue();


        즉 NSOperationQueue에서 본다면 동시 실행 가능 작업수를 무한대로 하는것이 글로벌큐와 같은 역활이라고 보면 되고 직접만든 큐의 경우는 하나로 설정했다고 보면됩니다.



        그렇다면 보통큐는 한 블럭이 끝나야 다음 블럭을 진행할수 있는데 어떻게 해야 동시 진행 구현할수 있을까요?



        바로 글로벌 큐를 사용하는 방법입니다.


        하지만 글로벌큐는 넣는 즉시 스레드로 만들기 때문에 반대로 제한을 줄수 없습니다.



        그렇다면 만든큐에서 제한을 걸어서 글로벌큐에 필요한 수 만큼만 넣는겁니다.



        카운트를 사용해서 루프 돌리며 기다리게 하는 방법은 그다지 추천하지 않습니다.

        기다리는 동안 스레드를 돌리기 때문이죠.

         

        일단 간단한 GCD예문을 만들어 보겠습니다.


        변수 앞에 __block선언은 블럭들 안에 공통으로 사용하기 위한 선언입니다.


        이렇게 하고 돌려보면

        [Session started at 2010-08-17 00:49:44 +0900.]

        2010-08-17 00:49:46.449 ThreadSample[21942:207] end

        2010-08-17 00:49:46.449 ThreadSample[21942:1903] GCD: 0

        2010-08-17 00:49:48.451 ThreadSample[21942:1903] GCD: 0 END

        2010-08-17 00:49:48.452 ThreadSample[21942:1903] GCD: 1

        2010-08-17 00:49:50.453 ThreadSample[21942:1903] GCD: 1 END

        2010-08-17 00:49:50.454 ThreadSample[21942:1903] GCD: 2

        2010-08-17 00:49:52.454 ThreadSample[21942:1903] GCD: 2 END

        2010-08-17 00:49:52.455 ThreadSample[21942:1903] GCD: 3

        2010-08-17 00:49:54.456 ThreadSample[21942:1903] GCD: 3 END

        2010-08-17 00:49:54.457 ThreadSample[21942:1903] GCD: 4

        2010-08-17 00:49:56.457 ThreadSample[21942:1903] GCD: 4 END

        라고 뜹니다.

        순차적으로 실행되긴하지만 결국 하나씩 진행되었습니다.



        하지만 필요한건 동시 진행입니다.



        여기서 등장하는것이 semaphore(시그널)입니다.


        먼저 시그널또한 하나의 객체로 취급됩니다


        dispatch_semaphore_t


        큐를 만드는 법과 비슷하게 시그널을 만듭니다


        dispatch_semaphore_t execSemaphore = dispatch_semaphore_create(5);


        dispatch_semaphore_create함수는 시그널을 만들어 주는 함수입니다

        인자로 갯수를 받습니다. 대충 5개로 하면 스레드를 5개까지만 한다고 보면됩니다.



        이렇게 만들어진 시그널은 해제 할때 dispatch_release로 해제해주면됩니다.



        이 시그널에 관련된 함수는 3개입니다.


        생성용인 

        dispatch_semaphore_create

        신호를 날리는 

        dispatch_semaphore_signal

        그리고 신호를 받는 

        dispatch_semaphore_wait


        이 시그널의 작동원리는 이렇습니다



        먼저 하나의 블럭이 실행되면

        wait을 호출합니다. 


        그러면 시그널은 내부 카운트를 1 올립니다.


        그리고 내부 카운트가 먼저 생성할때 설정한 5보다 적으면 바로 돌아가고 많을 경우 스레드가 다음 코드로 넘어가지 못하게 Sleep을 걸어버립니다.


        그리고 다른 블럭에서 작업이 끝날때 signal을 날려주면 내부 카운트가 하나 줄게됩니다.


        내부 카운트가 줄었을때 설정한 5보다 적으면 재우던 스레드를 깨워서 진행 시켜주는 것이죠


        먼저 기존의 블럭을 블럭을 실행할때 내부에서 글로벌큐에 새로운 블럭을 넣도록 이중구조로 변경합니다.





        그리고 실행해보면 넣는 즉식 전부 글로벌 큐에 넣기 때문에 실행 순서를 알아보기 힘듭니다.

        [Session started at 2010-08-17 01:23:02 +0900.]

        2010-08-17 01:23:03.775 ThreadSample[22365:207] end

        2010-08-17 01:23:03.776 ThreadSample[22365:1903] GCD: 0

        2010-08-17 01:23:03.776 ThreadSample[22365:5d03] GCD: 1

        2010-08-17 01:23:03.778 ThreadSample[22365:6203] GCD: 3

        2010-08-17 01:23:03.777 ThreadSample[22365:6103] GCD: 2

        2010-08-17 01:23:05.776 ThreadSample[22365:1903] GCD: 0 END

        2010-08-17 01:23:05.777 ThreadSample[22365:5d03] GCD: 1 END

        2010-08-17 01:23:05.779 ThreadSample[22365:6203] GCD: 3 END

        2010-08-17 01:23:05.782 ThreadSample[22365:6103] GCD: 2 END

        3초에 동시에 전부 실행되고 2초후인 5초에 전부 정지된것을 볼수있습니다.

        이제 시그널을 사용해 보겠습니다.


        코드가 길어보이는건 여러개를 돌려보기 위해서 그냥 코드를 복사한것뿐입니다.


        돌려보면

        [Session started at 2010-08-17 01:31:08 +0900.]

        2010-08-17 01:31:09.172 ThreadSample[23337:207] end

        2010-08-17 01:31:09.173 ThreadSample[23337:5e03] GCD: 0

        2010-08-17 01:31:09.174 ThreadSample[23337:5f03] GCD: 1

        2010-08-17 01:31:11.175 ThreadSample[23337:5e03] GCD: 0 END

        2010-08-17 01:31:11.176 ThreadSample[23337:5e03] GCD: 2

        2010-08-17 01:31:11.177 ThreadSample[23337:5f03] GCD: 1 END

        2010-08-17 01:31:11.178 ThreadSample[23337:6503] GCD: 3

        2010-08-17 01:31:13.176 ThreadSample[23337:5e03] GCD: 2 END

        2010-08-17 01:31:13.177 ThreadSample[23337:5e03] GCD: 4

        2010-08-17 01:31:13.179 ThreadSample[23337:6503] GCD: 3 END

        2010-08-17 01:31:15.177 ThreadSample[23337:5e03] GCD: 4 END

        이렇게 출력되었습니다.

        시간기준으로 보면 0, 1이 동시에 진행하고 2, 3이 진행되었으며 4가 마지막으로 진행될걸 알수있습니다.


        원하던 데로 두개씩 돌린겁니다.


        그럼 작동 원리를 자세히 보겠습니다.


        반복된 코드중 하나만 보면


        먼저 블럭으로 dqueue에 넣었습니다.


        그리고 이 만든큐(dqueue)는 순차적으로 하나씩 실행할수 있습니다.


        이 반복된 블럭이 5개 정도 큐에 들어있습니다.



        구분을 위해 각각의 블럭에 들어간 순서대로 0, 1, 2, 3, 4라합니다

        실제로 로그로 찍히는 번호도 같습니다.


        먼저 0번 블럭이 dqueue에서 실행됩니다.


        그럼 먼저 시그날의 wait을 실행합니다. 이때 시그날을 내부 카운트를 하나 올립니다.


        처음 실행된것이므로 1이됩니다.


        그리고 처음 만들때 설정한 2보다 작으므로 그대로 스레드를 진행합니다.


        다음 코드는 글로벌큐에 해당 블럭을 추가합니다.

        물론 추가되는 대로 바로 스레드를 진행합니다.


        이시점에서 로그 0이 찍힙니다.


        그리고 결국 글로벌에 넣어진 0번 블럭은 첫 로그를 찍고 sleep문으로 잠들어 있는동안


        dqueue에서는 1번 블럭을 실행합니다.


        마찬가지로 시그널의 wait을 실행하고 시그널은 이제 2가 되었으므로 다음 코드를 실행합니다.


        그래서 글로벌로 0번과 1번 블럭을 실행하고 있는 두개의 스레드가 작동합니다.


        1번블럭도 sleep문으로 자고있는사이 dqueue는 2번 블럭을 실행합니다.


        그리고 시그널의 wait 실행합니다. 이때 내부카운트가 이미 2이므로 이것이 줄때까지 스레드를 잠재웁니다.


        그래서 2번 블럭은 진행 하다말고 정지한 상태고 dqueue는 아직 2번 블럭이 종료되지 않았으니 3번을 실행하지 않고 기다립니다.


        그리고 0번과 1번은 2초후 종료 로그를 찍고 시그널의 signal을 실행합니다.


        어느쪽이든 한순간 먼저 끝날때 signal에 의해 wait에서 얼어있던 스레드가 진행 됩니다. 시그널은 2에서 signal로 1이 되었다가 wait진행되면서 2번 블럭에 의해 다시 2가 됩니다. 그리고 1번 블럭이 끝날때 또한 signal을 날리기 때문에 마찬가지로 wait에서 걸려있던 3번이 다시 진행되는 겁니다.






        이렇게 간단히 GCD를 다루어 보았지만 여기서 직접 스레드를 다루는 내용은 나오지 않았습니다.


        작업을 큐에 넣기만 할뿐 직접 스레드를 만들어서 붙여주는 작업은 GCD가 알아서 해주기 때문이죠.


        GCD는 NSOperation의 근본 개념으로 스레드를 사용하면서 작업을 분산해서 효율적으로 진행하도록 하는데 목적이 있습니다.


        아마도 같은 GCD라해도 iOS는 멀티스레딩에서 끝나는 정도지만 데스크탑인 OSX에서는 멀티코어 대책으로 나온 기술이라 한 CPU에 작업이 몰리지 않도록 효율적인 제어를 하는 기술이라고 이해됩니다.



        이상으로 Grand Central Dispatch강좌를 마치겠습니다.


        저작자 표시 비영리 동일 조건 변경 허락
        Posted by 문씨
        강좌2011/03/07 22:04

        안녕하세요 문씨입니다.


        멀티스레딩 1편에서 스레딩 개념을 다루었기 때문에 기본 개념 설명은 넘어가겠습니다.

        스레드 개념을 이해 못하는분은 1편을 필독해주세요.



        지난 강좌에서 NSThread로 스레드의 기초를 다루는 보았는데요. 

        이번에는 스레드를 응용하는 객체인 NSOperation에 대해 다루어 보겠습니다.



        스레드를 사용하다보면 어떤 문제에 접할수 있습니다.

        전역이나 프로퍼티로 가지고 있는 객체를 두군데 이상의 스레드에서 동시에 접근하는 경우 문제를 야기할수 있습니다.


        애플의 프레임워크에서는 이부분을 해결하기위해 화면에 관련된 부분은 무조건 메인 스레드에서 접근하지 않으면 에러(NSException)를 날리도록 하기도 했습니다.

        예) UIWebView를 release하는 곳이 다른 스레드고 동시에 이 release가 마지막 retainCount를 내려서 직접 해제되는 원인이라면 스레드 에러를 냅니다.


        그래서 애플문서에는 양 스레드에서 동시에 접근해도 문제없는 객체를 thread-safe되었다 라고 설명합니다. 


        멀티스레딩 문서에서 Thread-Safe클래스 리스트를 보면 대부분 NSArray, NSString, NSNumber등 한번 생성하면 변경되지 않는 클래스입니다.


        반대로 Thread-Unsafe클래스 리스트는 NSMutableArray, NSMutableString 등 변경이 가능한 클래스들입니다.


        예) 한 스레드에서 전역 객체로 배열 객체를 사용하고 있을때 다른 스레드에서 내용을 바꿔버리는 경우 문제!




        하지만 직접 만들어 쓰는 클래스는 그런 처리를 하기도 번거롭고 제일 좋은 방법은 그런일이 일어나지 않도록 구조를 짜는겁니다.






        예를 들어 이미지를 다운받아서 보여주는 스레드용 함수를 하나 만들었다고 가정합니다.


        하나의 이미지만으로 다운받고 끝나는 경우는 별 문제 없겠지만, 상황에 따라서 한번에 많은 이미지를 받을수도 있고 그냥 한두개만 받을수도 있는 경우도 있습니다 (예: 트위터의 프로파일 이미지들, 스크롤을 움직이면 중복은 빼더라도 유저당 하나씩 이미지 로드 이벤트가 발생). 


        이경우 만든 스레드 함수를 그냥 요청할때마다 알아서 다운 받도록 한다면 반드시 문제가 됩니다. 스레드가 너무 많아지기 때문이죠. 예를 들어 30개의 이미지를 다운로드 하도록 요청한다면 동시에 스레드가 30개가 되버리는데 제 경험상 7개정도 넘어도 위험해 집니다. 즉 스레드 강좌 1편에서 언급 했던데로 많아도 문제라는 점이죠.



        로딩한다고 유저가 만지지 못하도록 화면을 막는다면 그것은 최악의 사용성이 됩니다. 

        조금 보려고 하면 툭하면 화면을 정지시켜 버릴테니까요. 


        모바일 앱의 이슈는 대용량의 데이터를 어떻게 모바일이라는 좁은 환경에서도 제대로 표현할까입니다.


        WWDC(전세계 개발자 회의)2010에서 효율적인 메모리 관리를 주제로 다루는 세션에서는 보통 데스크탑 어플리케이션에서의 내용을 그대로 아이패드로 똑같이 구현했는데 데스크탑에서 대략 500메가 메모리를 잡던 것을 아이패드 앱에서는 10메가로 줄였습니다. 모바일다운 테크닉을 사용한 것이죠. 모든것을 전부 올려놓고 쉽게 쓰던 데스크탑과는 다르게 필요할때만 불러다 쓰는 방식의 모바일... 여기에는 멀티스레딩은 필수입니다. 이론만 보면 매우 당연하다고 생각하게 되는데 직접 보여주니 세삼스레 놀라게 합니다. (그렇다면 저는 이 모바일 방식을 데스크탑에 적용하면 어떨까 싶기도 합니다. 매우 가벼워 지겠죠...)





        그렇다면 앱에서 다운로드 요청을 동시에 수십개 이상도 할수도 있으면서 부담이 없도록 하려면 어떻게 해야 할까요? 




        먼저 구현해본 방법으로는 직접 큐를 만들어서 NSThread의 갯수를 직접 제어하는 방식입니다. 


        NSMutableArray로 큐용 배열을 하나 준비해두고 어느곳에서든 다운을 요청하면 일단 큐에 넣습니다. 그리고 현재 돌고있는 스레드수를 확인한후 설정한 최대 스레드수를 안넘으면 스레드를 만들어서 진행하고 전부 사용중일때는 하나가 끝날때까지 기다리는 겁니다.


        이런식으로 NSThread로 직접 스레드 수를 제어하는 방법이 있습니다. 하지만 구현하기도 힘들고 상황에 따라 제어하기도 힘듭니다. 



        하나의 작업을 하나의 스레드로 보고 그것을 관리해 주는 것이 없을까? 하는 질문에 있는것이 NSOperation과 NSOperationQueue입니다. 



        앞서 설명에서는 직접 관리해보았지만 대신 해주는것이 있으니 해맬 필요가 없겠죠.



        Queue(큐)라는 단어를 보고 바로 감 잡은 분도 있겠지만 이 강좌는 최대한 쉽게 풀어쓰는것이 목적인지라 자세히 설명합니다.


        NSOperation의 Operation은 작업을 뜻합니다.


        그리고 NSOperationQueue는 작업큐입니다.

        C나 C++을 배울때 배우는 데이터구조 중 하나가 큐입니다.


        보통 스텍과 큐를 같이 배웁니다. 서로 비슷한 데이터 구조지만 서로 대조되기 때문이죠.

        스텍은 LIFO라 하고 큐는 FIFO이라고 합니다.


        프로그래밍 배울때 배우는 기초중의 하나입니다.


        LIFO는 Last In, First Out이라는 뜻의 약자입니다.

        즉 스텍 구조를 본다면 가장 마지막에 들어간 것이 가장 먼저 나오게되는 거죠.


        FIFO는 마찬가지로 First In, First Out이라는 뜻입니다.

        먼저 들어간것이 먼저 나온다는 것이죠.

        말그대로 큐는 선착순 구조입니다.


        NSOperationQueue는 NSOperation을 담는 큐이며 FIFO방식으로 들어간 순서대로 NSOperation을 실행 시켜주는 기능을 합니다.




        NSOperation은 결국 일의 단위이면서 하나의 스레드 단위로 보기도 합니다.


        NSThread에서는 스레드 관리자로서 함수하나를 연결해서 스레드로 돌렸지만 이럴경우 재사용하기에는 좀 힘든 감이 있습니다.


        예를 들어 앞서 예문을 들었던것과 같이



        NSOperation에서는 어떤 방법으로 하는지 자세히 알아 보겠습니다.



        먼저 헤더를 보겠습니다.

        이것저것 많지만 NSThread의 헤더에서 봤던 같은 이름의 함수도 있습니다.



        NSOperation은 스스로는 아무것도 하지않는 객체입니다. NSObject같은 존재입니다.


        그래서 NSThread같이 함수를 호출해주는 역활을 하는 (NSOperation을 상속받은)자식 클래스도 있지만 여기서는 간단히 기본기만 설명합니다.

        먼저 헤더를 보면 뭔가 많아 보이지만 일단 사용해 보겠습니다.


        앞서 말했듯 NSOperation은 NSObject같은 존재라 이것을 상속받은 새로운 객체가 필요합니다.


        먼저 NSOperation을 상속받은 새 클래스를 만듭니다.

        아직 만든게 없으니 내용이 복잡해질 이유가 없습니다.


        이제 NSOperation을 상속받은 TESTOperation클래스를 만들었습니다.


        먼저 앞서 봤던 NSOperation헤더에서 봐둬야 할 함수가있습니다.


        main이라는 함수인데 보통 main은 C에서 프로그램의 시작에 사용되는 함수입니다.


        main.m에서 main함수가 앱의 시작점으로 되어있는것도 당연한것이죠.


        NSOperation에서 - (void)main;은 한 작업(스레드)의 시작함수로 보고있는것입니다.


        즉 NSOperation으로 스레드를 시작할때 main함수를 실행한다는 겁니다. NSThread를 만들때 직접 함수와 그 함수가 들어있는 객체(target)를 지정해주는것과 대조됩니다. NSOperation은 실행 함수도 직접 자기자신으로 지정해두는것이 NSThread와 다른점입니다.

        NSThread는 스레드만 관리하는 객체이고 NSOperation은 작업을 관리하는 객체이기 때문입니다. 구조적 개념의 차이라고 보면됩니다.


        NSOperation은 작업을 진행할때 main함수를 호출하기 때문에 먼저 main함수를 오버라이드(override)합니다.


        오버라이드(override)는 부모 클래스에 선언된 함수를 그대로 자식 클래스에도 만드는것을 말합니다. 이럴경우 자식클래스의 함수가 우선됩니다. 자세한 부분은 다른 강좌에서...





        속성인 i에는 순서대로 0, 1, 2, 3, 4를 넣어주었습니다.


        이제 어떻게 출력되는지 보겠습니다.


        2010-08-15 23:21:28.887 ThreadSample[3273:207] End Adding Queue

        2010-08-15 23:21:28.889 ThreadSample[3273:1903] Start: 1

        2010-08-15 23:21:28.889 ThreadSample[3273:5c03] Start: 0

        2010-08-15 23:21:28.891 ThreadSample[3273:6103] Start: 2

        2010-08-15 23:21:28.892 ThreadSample[3273:6303] Start: 3

        2010-08-15 23:21:28.894 ThreadSample[3273:6503] Start: 4

        2010-08-15 23:21:30.890 ThreadSample[3273:1903] End: 1

        2010-08-15 23:21:30.890 ThreadSample[3273:5c03] End: 0

        2010-08-15 23:21:30.891 ThreadSample[3273:6103] End: 2

        2010-08-15 23:21:30.893 ThreadSample[3273:6303] End: 3

        2010-08-15 23:21:30.895 ThreadSample[3273:6503] End: 4


        로그를 보면 큐에 넣는 작업이 끝난후 작업들이 거의 동시에 실행된것을 알수 있습니다.

        근데 문제는 순차적으로 실행될것이라고 본것과는 다르게 약간 순서가 섞여 보입니다.


        NSOperationQueue는 특별한 설정없이는 큐에 넣어지는 즉시 실행합니다.

        물론 순차적으로 들어오는 순서대로 실행은 했지만 

        실행되는 함수인 main에서 로그 찍는 순간은 스레드 마다 다를수 있죠.


        그래도 위 로그는 0과 1만 순서가 바뀌었을뿐 나머지는 거의 비슷합니다.


        전부 거의 동시에 실행되고 2초후 동시에 완료 된것을 알수있습니다.



        하지만 이래서는 스레드를 무작정 만드는거와 같습니다.


        이번에는 NSOperationQueue의 헤더를 보겠습니다.


        함수중에 setMaxConcurrentOperationCount: 가 있습니다.


        API문서를 보면 maxConcurrentOperationCount의 기본값이 NSOperationQueueDefaultMaxConcurrentOperationCount라고합니다. 

        헤더에 보면 이 상수가 선언되어 있는데 -1로 되어있습니다.


        동시 실행 제한이 무한대로 되어 있는 셈이죠.



        그러면 이걸 1로 설정해보겠습니다.

        [Session started at 2010-08-15 23:44:21 +0900.]

        2010-08-15 23:44:23.442 ThreadSample[3371:207] End Adding Queue

        2010-08-15 23:44:23.442 ThreadSample[3371:1903] Start: 0

        2010-08-15 23:44:25.448 ThreadSample[3371:1903] End: 0

        2010-08-15 23:44:25.449 ThreadSample[3371:6103] Start: 1

        2010-08-15 23:44:27.450 ThreadSample[3371:6103] End: 1

        2010-08-15 23:44:27.451 ThreadSample[3371:6203] Start: 2

        2010-08-15 23:44:29.452 ThreadSample[3371:6203] End: 2

        2010-08-15 23:44:29.453 ThreadSample[3371:6103] Start: 3

        2010-08-15 23:44:31.453 ThreadSample[3371:6103] End: 3

        2010-08-15 23:44:31.454 ThreadSample[3371:6103] Start: 4

        2010-08-15 23:44:33.455 ThreadSample[3371:6103] End: 4


        착실하게 하나씩 진행합니다.


        이런식으로 동시 실행 수에 제한을 걸면 그만큼 되는겁니다.


        이번에는 2로 설정해 보겠습니다.


        [Session started at 2010-08-15 23:46:55 +0900.]

        2010-08-15 23:46:57.047 ThreadSample[3401:207] End Adding Queue

        2010-08-15 23:46:57.049 ThreadSample[3401:1a03] Start: 0

        2010-08-15 23:46:57.049 ThreadSample[3401:5c03] Start: 1

        2010-08-15 23:46:59.051 ThreadSample[3401:5c03] End: 1

        2010-08-15 23:46:59.051 ThreadSample[3401:1a03] End: 0

        2010-08-15 23:46:59.051 ThreadSample[3401:6303] Start: 2

        2010-08-15 23:46:59.052 ThreadSample[3401:6203] Start: 3

        2010-08-15 23:47:01.053 ThreadSample[3401:6303] End: 2

        2010-08-15 23:47:01.053 ThreadSample[3401:6203] End: 3

        2010-08-15 23:47:01.054 ThreadSample[3401:6303] Start: 4

        2010-08-15 23:47:03.055 ThreadSample[3401:6303] End: 4


        두개씩 실행된것을 볼수있습니다.


        NSOperationQueue에는 setSuspended함수도 있습니다 YES설정해주면 큐의 실행을 일시 정지하고 NO하면 다시 진행합니다.


        단 주의할점은 NSOperationQueue는 아직 실행하지 않은 것들에 대해서 대기하고 기다릴뿐 이미 실행에 들어간 것은 일시정지 시킬수 없다는 겁니다.


        스레드에 관련된 함수는 어딜 봐도 진행 도중에 멈추는 방법은 없습니다.



        하던일을 전부 취소하는 경우도 있습니다.

        cancelAllOperations라는 함수인데

        호출할 경우 큐에 있는 모든 NSOperation에 cancel을 날립니다.

        즉 실행되기전에 쌓인 작업들이 취소처리되는것이죠.


        NSOperationQueue는 NSMutableArray에서 넣은 객체 지우기 같아 보이지만 취소명령은 제거 명령이 아닙니다. 그래서 전부취소를 해도 큐에는 작업들이 남아있습니다.


        큐는 들어있는 작업(NSOperation)을 순차적으로 실행할뿐입니다. 취소된 작업은 실행 안하고 넘길뿐이죠.


        그렇다면 이미 실행중인것들은 어떻게 될까요?



        NSThread에서 취소되면 플래그(flag)만 바뀌고 스래드 내부에서 알아서 처리해줘야 한다는  점은 같습니다.


        결국 전체 취소를 호출하면 NSOperationQueue는 내부에 실행중이거나 대기중인 NSOperation에 취소를 호출합니다. 실행중인 NSOperation은 스레드 내부에 취소 처리 코드가 있다면 하던 작업을 중단하고 끝낼 것이고 없다면 끝날때까지 기다립니다. 


        그리고 취소후에 다시 다른 작업을 넣는다면 그 작업들은 물론 진행됩니다.







        이번에는 이 NSOperation을 사용한 실전을 해보겠습니다.



        음 간단한 예제를 들자면


        인터넷상에서 이미지를 받아다 보여주는 것을 구현한다 합시다.


         NSThread를 이용할 경우 UIImageView가 있는 뷰 컨트롤러에서 전용 스레드 함수를 만들어 두고 이미지를 다운로드한후 메인스레드로 UIImageView에 띄우는 작업을 하게됩니다.



        그런데 매번 일일이 처리해 주는것도 귀찬군요.



        그렇다고 이렇게 할수도 없습니다.


        UIImageView *iv = [[[UIImageView allocinitWithImage:[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://이미지주소"]]]] autorelease];

        뭔가 복잡해 보이지만 풀어보면

        먼저 NSURL객체로 이미지의 주소를 URL객체화합니다.

        그리고 NSData를 인터넷 주소로 데이터를 받아와서 데이터 객체를 생성하게 합니다.

        그리고 데이터를 받아오면 UIImage객체를 데이터로 불러다 생성합니다. 

        즉 바이너리데이터에서 이미지로 불러오는것이죠.

        그다음은 이미지뷰에 만든 이미지객체를 넣고 뷰를 만드는 겁니다.


        간단한 한줄이지만 많은 작업을 하죠

        문자을 주소로, 주소를 데이터로, 데이터를 이미지로, 이미지를 이미지뷰로...

        문제는 이 코드를 메인스레드에서 돌리면 로딩하는동안 화면이 멈춘다는 겁니다.



        이미지 로딩을 하기 위해 멀티스레딩은 필수입니다.


        NSURLConnection을 이용해서 할수도 있지만 이부분은 코드만 복잡해지니 넘어갑니다.



        귀찬은것은 싫어하니 위에 사용한 한줄 코드를 최대한 변경없이 구현하는 것이 조건입니다.



        먼저 작업(NSOperation)을 구현합니다.


        하는 일은 인터넷 주소와 이미지뷰을 지정받으면 그 주소로 이미지로 받아다가 이미지뷰에 지정하는겁니다.

        다음 본문을 구현합니다.

        init함수에서 주소와 이미지뷰를 받아서 retain올려둡니다. 그래서 작업중이나 전에 객체가 해제되서 엑세스 에러를 방지합니다.


        그리고 main함수에서 

        1. NSAutoreleasePool을 열고
        2. 주소(NSURL)로 부터 데이터(NSData)를 받아오고
        3. 데이터(NSData)로 부터 이미지(UIImage)를 만듭니다.
        4. 그리고 _setImage:함수를 메인스레드로 돌려서 만든 이미지(UIImage)를 이미지뷰에 지정하도록 합니다.
        5. 이미지를 받기전에는 이미지의 크기를 모르므로 받은후에 뷰 크기를 재설정 해줍니다.
        6. 그리고 NSOperation객체(ImageOperation)가 해제되면서 retain올려둔 주소(_url)와 이미지뷰(_imageView)를 해제해줍니다.



        이제 NSOperation작업은 되었고


        UIImageView에 새로운 함수를 카테고리로 만들어줍니다.

        즉 앞서 사용했던 

        UIImageView *iv = [[[UIImageView allocinitWithImage:[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://이미지주소"]]]] autorelease];


        코드를 다른 추가적인 처리 없이 그저 이 한줄을 수정하면 되도록 하는겁니다.


        UIImageView *iv = [[[UIImageView allocinitWithURL:[NSURL URLWithString:@"http://이미지주소"]] autorelease];


        이제 본문에서는 알아서 작업을 만들고 넣게 되겠죠.


        물론 여기에는 카테고리를 사용하고 싱글톤 기법을 사용합니다. (싱글톤은 여러모로 유용하죠)


        싱글톤으로 NSOperationQueue를 만들어 영구 사용하고 동시 진행 가능 스레드 수를 5개까지 정합니다.


        그리고 초기화 함수인 initWithURL은 실제로는 initWithFrame으로 초기화하고

        앞서 만든 ImageOperation을 만들어서 주소와 자신(UIImageView)을 지정하고 큐에 넣어줍니다.




        실제 이미지뷰에 사용해보면 메인스레드에 영향 없이 나중에 이미지가 뜨는것을 알수 있습니다.





        여기까지 NSOperation의 기본 개념 및 사용기법을 다루어보았습니다.


        저작자 표시 비영리 동일 조건 변경 허락
        Posted by 문씨