Languages/Objective-C2011.02.08 11:02
뷰에 그려지는 내용은, 사실 CALayer 객체 위에 그려집니다. 뷰에 대해서 animation을 적용할 수 있듯이, CALayer 객체에 대해서도 animation을 적용할 수 있습니다.

CALayer에 대한 reference를 찾아보시면 아시겠습니다만, CALayer 객체에는 여러 개의 애니메이션 가능한 프라퍼티들이 있습니다. CALayer는 key-value coding을 지원하기 때문에, 키 값을 사용해 이들 프라퍼티에 애니메이션을 지정할 수 있습니다. 

이런 애니메이션은 적용하기도 간단합니다. 물론 복잡한 애니메이션을 구현하려면 좀 심각한 코딩을 해야 하겠습니다만, 레이어 하나 위에 이미지를 그리고 그 이미지가 깜빡거리게 만드는 정도는 대단히 간단하게 처리할 수 있습니다. 아래의 코드를 보시죠.

UIImage* img = [UIImage imageNamed: @"testimg.png"];

CALayer* sub = [CALayer layer];

sub.contents = img.CGImage;

sub.frame = ...

[self.layer addSublayer:sub];

CABasicAnimation* ani = [CABasicAnimation animationWithKeyPath:@"opacity"];

ani.duration = 2.0;

ani.repeatCount = HUGE_VALF;

ani.autoreverses = YES;

ani.fromValue = [NSNumber numberWithFloat:0.0];

ani.toValue = [NSNumber numberWithFloat:1.0];

ani.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];

[sub addAnimation:ani forKey:@"opacity"];


우선 새 레이어를 하나 만들구요. 그 위에 이미지를 얹습니다. 그런 다음 뷰의 backing layer 위에 해당 레이어를 얹습니다. 그런 다음 애니메이션 객체를 하나 만들어서 해당 애니메이션이 어떻게 실행될 지 지정한 다음에 해당 애니메이션을 새로 만든 레이어에 붙입니다.

유의해서 볼 부분은, 해당 에니메이션이 CALayer의 어떤 프라퍼티에 적용될 것인지를 적용하는 부분입니다. CALayer에는 opacity라는 프라퍼티가 있습니다. 이 프라퍼티는 레이어가 얼마나 투명한지를 결정하는 프라퍼티입니다. 위의 애니메이션은 그 지속 시간(duration)이 2초이며, 영원히(HUGE_VALF) 반복됩니다. opacity 값은 0.0에서 1.0으로 변했다가 다시 원래 상태로 돌아갑니다(autoreverse = YES). 

따라서 위에서 만들어 붙인 레이어에 그려진 이미지는 주기적으로 나타났다 사라지는 것 처럼 보이게 됩니다. 간단하죠?

레이어를 실제로 조작하는 것은 UIView를 가지고 프로그래밍하는 것에 비해 저수준(low-level) 연산처럼 보이기도 합니다만, 잘만 쓰면 뷰를 새로 만들지 않고도 프로그래밍할 수 있기 때문에, 성능 문제를 개선하는 데도 도움을 줄 수 있습니다.

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 나그네

    sub.contents = img.CGImage 부분에서 Cannpt convert CGImage to objc_object in argument passing 이 떠요 ㅠㅠ

    2011.04.22 20:54 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. 부따부따

    제가 깜박이는 애니메이션이 특정 버튼을 누르면 시작하고,

    이 버튼에 대한 프로세스가 끝나면 원래 상태로 돌아가는 것을 구현 하려고 하는데요

    실시간 영상을 처리하는 가운데 overlay에 대해 해당 애니메이션을 지정해줬는데

    좀처럼 쉽게되질 않네요 도움 주시면 감사하겠습니다.

    2011.07.15 17:36 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2011.02.08 09:34
보통 비동기적으로 프로그래밍 한다고 하면 많은 분들이 쓰레드 프로그래밍을 떠올립니다. 네. pThread같이 비교적 알기쉬운 쓰레드 메커니즘을 사용하면 아주 간단하게 비동기적 프로그래밍을 할 수 있죠. 그런데 iPhone이나 iPad는 좀 더 간단한 메커니즘을 제공합니다. 쓰레드 프로그래밍을 단 한줄도 하지 않고서도, 시스템이 제공하는 쓰레드의 도움을 받아 비동기 프로그래밍을 할 수 있죠. 그리고 이렇게 프로그래밍 하면 프로그램을 구성하는 클래스 사이에 의존 관계를 꽤 간단히 끊어버릴 수 있습니다.

결론부터 이야기하자면 NSNotification을 이용하자는 것인데요. 프로그램 내의 한 모듈에서 다른 모듈로 어떤 사건의 발생을 통지하여, 보내는 쪽 클래스의 실행 궤적과 받는 쪽의 실행 궤적이 비동기적으로 동시 수행될 수 있도록 하자는 것이죠. 

NSNotification을 이용할 때 많은 분들이 [NSNotificationCenter defaultCenter]로 얻어낸 NSNotificationCenter 객체의 postNotification 메소드를 호출하는 것을 떠올리시는데, NSNotificationCenter의 클래스 레퍼런스에도 나와 있습니다만, NSNotificationCenter를 직접 이용하면 동기적인 처리밖에 안됩니다. 

A notification center delivers notifications to observers synchronously.

무슨 이야긴가 하면, postNotification을 호출하면 해당 notification에 observer로 등록한 객체의 callback 루틴이 수행 종료될 때 까지 그 자리에서 가만히 대기하고 있게 된다는 것입니다. 그러니, 비동기적으로 프로그래밍해서 어느 정도의 병행성(concurrency)을 얻고자 한다면 이렇게 프로그래밍을 하면 안되죠. 

따라서, NSNotificationQueue를 이용해야 합니다.

Notification을 날리는 쪽에서는 다음과 같이 하면 됩니다.

[[NSNotificationQueue defaultQueue] enqueueNotification:[NSNotification notificationWithName:@"dataLoad" object:nilpostingStyle:NSPostWhenIdle];


위의 코드는 NSNotificationQueue에 "dataLoad"라는 이름의 NSNotification 객체를 넣는 코드입니다. 이 객체는 언제 notification observer에게 전달될까요? manual에 따르면, 'run loop가 idle할 때'로 되어 있습니다. NSPostWhenIdle로 지정했으니까요. 

Notification을 받을 쪽 코드는 NSNotificationCenter를 사용할 때와 같습니다. 다음과 같이 하면, "dataLoad"라는 이름의 Notification이 떴을 때 _dataLoad 메소드가 callback으로 수행되도록 할 수 있습니다.

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_loadData) name:@"dataLoad" object:nil];


한가지 주의할 것은 Notification을 queue에 집어넣을 때 NSPostNow라고 하면 비동기적으로 notification이 날아가는 게 아니라 동기적으로 처리된다는 점이에요. reference에 실린 아래의 글을 인용하는 것으로, 이번 글은 마치도록 하죠.

A notification queued with NSPostNow is posted immediately after coalescing to the notification center. You queue a notification with NSPostNow (or post one with postNotification:) when you do not require asynchronous calling behavior. For many programming situations, synchronous behavior is not only allowable but desirable: You want the notification center to return after dispatching so you can be sure that observing objects have received and processed the notification. Of course, you should use enqueueNotification... with NSPostNow rather than use postNotification: when there are similar notifications in the queue that you want to remove through coalescing.


물론, 이런 질문도 있을 수 있겠어요. 그럼 그냥 NSPostNow를 써서 큐에 넣는 대신, NSNotificationCenter의 postNotification 메소드를 부르면 되지 않느냐? 그런데 그렇게 하면 유사 이벤트 병합기능을 써먹을 수 없게 되죠. NSNotificationQueue는 (위에도 적혀 있습니다만) 같은 이름의 이벤트가 큐에 들어오면 합쳐버리거든요.

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 좋은글 잘 읽었습니다.
    그런데, nsthread 에서는 NSPostNow 외에는 호출이 안되네요..
    혹시 가능한 방법을 찾을 수 있겠습니까?

    2011.02.21 18:27 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2011.01.20 17:35
UIScrollView를 사용하여 Zooming, 페이지 단위 스크롤링을 고려하려면 주의해야 할 것이 있습니다. 그건 바로 "하나의 UIScrollView 하위 클래스에 Zooming 기능과 스크롤링 관련 기능을 동시에 우겨넣으면 안된다"는 것이죠.

같이 우겨넣어 프로그래밍을 할 수 있을지도 모르겠습니다만, 아마 "굉장히" 프로그래밍하기 어려울 겁니다.

그럼 확대/축소도 되고 좌우로 스크롤링도 되는 PDF 뷰어 같은걸 구현하려면 어떻게 해야 하나요?

가장 간단한 방법은 스크롤 뷰를 중첩(nesting)하는 겁니다. 안쪽 스크롤 뷰에는 확대/축소 기능을 넣고, 바깥쪽 스크롤 뷰에는 좌우 스크롤 기능을 구현하는 것이죠.

확대 축소 기능에 대해서는 지난번 글에 잠깐 다루었는데, 좌우 스크롤 기능과 함께 사용하려면 지난번 글처럼 기능을 무조건 단순화 하면 곤란합니다. 좌우 스크롤하려면 다음과 같은 요구사항을 만족해야 하기 때문이죠.

(1) 끊김없는 좌우 스크롤
(2) 다음번에 나타날 화면의 caching
(3) 페이지 단위 좌우 스크롤

(3)번은 만족시키기가 간단합니다. 스크롤 뷰의 pagingMode를 YES로 설정하면 되거든요. 그렇게 하면 화면 크기를 (엄밀하게 말하면 스크롤 뷰의 bounds 속성 값) 페이지 크기로 인식하고 스크롤을 합니다.

(1)을 할떄는 생각할 것이 좀 많습니다. '끊김 없는'이라는 것은 '좌우 스크롤을 할 때 멈칫 거리는 일이 없어야 함'을 의미하거든요. 모든 페이지를 하나의 스크롤 뷰의 서브 뷰로 몽땅 다 로딩해 버린다음에 스크롤 하면 별문제겠습니다만 그렇게 하는 건 오버헤드가 너무 큽니다. 그러니 보통은 좌우 스크롤을 구현할 때 이렇게 해야 하죠.

a. 스크롤 뷰의 contentSize는 스크롤 뷰 안에 '모든 페이지'를 우겨넣는다고 가정하고 잡습니다.

그래야 하는 이유는? 그래야 스크롤 뷰가 '어디서부터 어디까지 스크롤 해야 하는지' 제대로 인식할 수 있기 때문이죠.

b. 스크롤 뷰 안에 컨텐트를 (그러니까 서브 뷰들을) 추가할 때에는 딱 세 개 (혹은 다섯 개?) 만 합니다.

화면에 보이는 것은 그 중 가운데고, 왼쪽 오른쪽에 있는 것들은 각각 손가락을 오른쪽으로 긁었을 때, 왼쪽으로 긁었을 때 화면에 나타나야 하는 것들이죠.

c. 스크롤 뷰가 실제로 스크롤 되면, 위의 세 개 뷰 중에 왼쪽에 있는 것을 오른쪽으로 옮기고, 가운데 있는 것은 오른쪽, 맨 오른쪽에 있는 것은 폐기처분하거나 (release) 아니면 맨 왼쪽으로 돌려서 재활용합니다. (재활용 할 때에는, 재활용 한 뷰에 '그려졌던' 내용이 잠시라도 화면에 다시 표시되는 일이 없도록 주의해야 합니다.)

이렇게만 하면 서브뷰 딱 세 개만 가지고도 가로 방향 스크롤을 구현할 수 있습니다. (세로 방향도 마찬가지.) 굉장히 간단하게 들리는데, 무슨 문제가 있나요?

보통 '왼쪽으로 스크롤이 일어났다' 아니면 '오른쪽으로 스크롤이 일어났다'는 뭘 가지고 판단하나요? 스크롤 뷰의 delegate 함수 중에 scrollViewDidEndDecelerating 함수를 구현해서 할 수도 있겠지만, 테스트 해 본 바에 따르면 굉장히 페이지를 빠른 속도로 넘기는 경우에 문제가 발생할 가능성이 있습니다.

그래서 보통은 스크롤 뷰의 contentOffset.x 값이 오른쪽이나 왼쪽 페이지의 중간 지점을 넘어갔을 때 '스크롤이 일어났다'고 판단하게 되는데, 그러면 그 때 무슨 일을 해야 하느냐 하면 앞서 말한 대로 페이지들의 위치를 옮기고 재배치하는 등의 작업을 해 줘야 하거든요.

근데 재배치 될 뷰 안의 drawRect나 drawLayer 함수 안에서 뭔가 굉장히 많은 일을 하게 되면 바로 그 순간에 스크롤이 멈칫거리는 현상이 발생하게 됩니다. (Apple의 이미지 스크롤 관련 예제에서도 동일한 문제가 발생해요.) PDF 페이지를 렌더링 하는 것도 PDF 파일에 따라서는 막대한 오버헤드를 유발하기 때문에 마찬가지죠.

이 문제를 극복하기 위해서는 drawRect는 가급적 피하고 drawLayer를 쓰며, layer 클래스로는 CATiledLayer를 쓰는 것이 바람직합니다. CATiledLayer는 렌더링을 할 때 쓰레드를 만들어서 백그라운드에서 렌더링을 하기 때문에, drawLayer가 스크롤을 멈칫거리게 한다던가 하는 일이 적어요.

물론 화면에 뜨는 스피드는 좀 느려지긴 합니다. 이런 trade-off에 대해서는 개발자가 적절히 생각하고 선택을 해야겠죠. (저는 못해 드립니다. ㅎㅎ)
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.17 16:18
보통 Zoom 효과를 구현할 때 많이 사용하는 것이 UIScrollView입니다. 특히 PDF의 확대 효과에 관해서라면 Apple Developer Center에서 제공하는 ZoomingPDFViewer 예제를 많이들 참고하시죠.

이 예제에서는 세 개의 뷰를 사용해서 Zoom 효과를 구현합니다. 일단 백그라운드에 pdf를 먼저 그리구요. 그 앞에 zooming을 구현할 pdf view 두 개를 둡니다. 하나는 확대 이전 뷰, 하나는 확대 다음 뷰. (간단히 설명하자면 그렇습니다) zooming이 진행중인 동안에는 확대 이전 뷰를 사용해서 rendering하다가, 확대가 끝나고 나면 확대 다음 뷰를 확대된 크기만큼 새로 만들어서 (그러기 위해서는 얼마나 확대되었는지를 기억하고 있어야 합니다) 뷰가 보이는 계층 맨 위쪽에 붙여 버리는 것이죠. 

그런데 이 세 개의 뷰가 정말 필요한 걸까요? 이건 ZoomingPDFViewer 예제를 처음 봤을때 제 스스로 던진 질문이었습니다. 이 문제를 풀기 위해서, 과연 zooming이 진행 중일때 scroll view의 content view에 해당하는 뷰 (그러니까 확대 대상인 뷰)에 어떤 일이 벌어지는 지를 살펴봤습니다.

결론부터 이야기하자면, content view의 frame 속성(CGRect 값입니다)이 끊임없이 변합니다. 하지만 content view의 bounds 속성 값은 전혀 변하지 않습니다. 그러니, 다음과 같이 했다고 해 봅시다.

- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView

{

    return pdfView;

}


위의 코드는 확대 대상 content view를 scroll view에게 반환하기 위해 구현하는, UIScrollViewDelegate 프로토콜 함수의 코드입니다. pdfView는 self.bounds를 사용해 자기 뷰 내부를 렌더링합니다. 그런데 frame 객체의 크기는 zooming을 할수록 자꾸 커진단 말입니다. 그래도 self.bounds에는 변함이 없으니, 뷰가 커지면 커질수록 뷰 안에 그려진 내용은 해상도가 떨어져 보이게 됩니다. 

그럼 대체 scroll view는 self.bounds는 가만 냅두면서 self.frame은 어떻게 계속 뻥튀기하는 걸까요?

답은 pdfView의 (그러니까 UIView를 상속한 뷰 클래스 객체라면 누구나 갖고 있는) transform 프라퍼티에 숨어 있습니다. 이 프라퍼티의 값이 CGAffineTransformIdentity가 아닐 경우, frame 프라퍼티의 값은 계산된 값입니다. 보통은 frame이나 bounds나 값이 동일하지만, transform 프라퍼티의 값이 CGAffineTransformIdentity가 아닌 경우에는 frame은 bounds 프라퍼티에 transform을 적용해서 구해진 CGRect 값이 되는 것이죠.

그래서 다음과 같은 코드 안에서 frame의 값과 bounds의 값을 찍어보면 그 값이 서로 다르게 나옵니다.

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale

{

// ...

NSLog(@"scale = %f, pdfScale = %f", scale, pdfScale);

NSLog(@"frame = %f, %f, %f, %f", view.frame.origin.x, view.frame.origin.y, view.frame.size.width, view.frame.size.height);

NSLog(@"bounds = %f, %f, %f, %f", view.bounds.origin.x, view.bounds.origin.y, view.bounds.size.width, view.bounds.size.height);

// ...

}


그러니까 결국 UIScrollView는 content view의 transform 프라퍼티의 값을 적절히 변경해서, content view의 확대/축소를 구현한다는 결론을 내릴 수 있는 것이죠. (거기다 contentOffset 프라퍼티까지 함께 수정/적용해서 말이죠.)

자. 그럼 여벌의 view 없이도 확대 동작을 구현하려면, ZoomingPDFViewer의 코드를 어떻게 개선해야 하는 걸까요? transform 프라퍼티를 적당히 매만지도록 코드를 변경하면 되겠군요. 지금부터는 여러분께서 ZoomingPDFViewer의 코드를 같이 보고 계신다고 가정하고 설명을 진행하겠습니다. 우선, backgroundImageView를 scroll view의 서브 뷰로 붙이는 부분을 아예 없애 버립니다. 필요 없으니까요.

그런 다음, oldPDFView를 참조하는 코드도 전부 없애 버립니다. 역시 필요 없습니다. (그러면 아마 scrollViewWillBeginZooming 안의 코드가 전부 날아가 버릴겁니다.) 

자. 그런 다음에는 pdfView만 필요하니까, PDFScrollView의 initWithFrame 메소드 안에서 pdfView를 생성하는 부분의 코드를 다음과 같이 고칩니다.

pdfView = [[TiledPDFView alloc] initWithFrame:pageRect andScale:pdfScale];

[pdfView setPage:page];

[self addSubview:pdfView];


이렇게 한 다음에, scrollViewDidEndZooming의 코드를 다음과 같이 고칩니다.

- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView 

         withView:(UIView *)view atScale:(float)scale

{

// set the new scale factor for the TiledPDFView

pdfScale *=scale;

pdfView.bounds = CGRectMake(0,0,

                      view.frame.size.width, view.frame.size.height);

        // make the frame size to the bounds size

view.transform = CGAffineTransformIdentity

       

[pdfView setNeedsDisplay];

}


그런 다음, TiledPDFView의 initWithFrame의 코드를 다음과 같이 수정합니다.

tiledLayer.levelsOfDetail = 1;

tiledLayer.levelsOfDetailBias = 1;

tiledLayer.tileSize = CGSizeMake(512, 512);


detail 레벨을 1로 주어야 줌이 된 다음에 해상도가 상승되는 것이 보이게 되더군요. 너무 높으면 마치 줌이 되는 동안에서 해상도가 상승되다가, 마지막에 뷰가 쓸데없이 다시 그려지는 것 처럼 보입니다.

이렇게 한 다음에 실행시켜 보면, 이제 뷰 하나만 가지고도 PDF 문서의 zooming, 그러니까 확대/축소가 정상적으로 처리됩니다. scrollViewDidZooming 안에서 bounds의 값이 frame과 같도록, transform 프라퍼티의 값을 강제로 CGAffineTransformIdentity로 변경했기 때문입니다. 

자. 그런데 한가지 문제가 남았습니다. 뭔가요?

네. 아마 이 예제를 한번이라도 실행해 본 분이라면 아실텐데요. 이 문제는 이전 버전이나 현 버전이나 똑같습니다. 이 문제는 content view의 크기가 계속 변하기 때문에 발생하는 문제입니다. content view가 예전과 동일한 instance를 가리키고 있고 bounds의 값에도 변화가 없는 경우에는 scrollViewDidEndZooming 메소드의 atScale:(float)scale 인자값 'scale'의 값이 항상 'content view의 원래 크기 대비, 현재 크기의 비율'을 나타내게 되는데, content view instance가 확대할 때 마다 계속 달라지거나 (종전 예제의 경우) bounds의 값이 똑같이 유지되지 않는 경우(현 예제의 경우)에는 인자값 'scale'의 값이 '종전 크기 대비, 현재 크기의 비율'을 나타내게 된다는 것이죠. 

따라서 scroll view의 maximumZoomScale 프라퍼티나 minimumZoomScale 프라퍼티 값을 제한하는 것으로는 '어디까지 확대하고 어디까지 축소할 것인지'를 결정할 수 없게 됩니다. 

그러니 scrollViewWillBeginZooming과 scrollViewDidEndZooming 메소드 안에서 pdfScale 값을 가지고 minimumZoomScale 프라퍼티와 maximumZoomScale 프라퍼티를 가지고 꽁수를 좀 부려야 됩니다. 

이에 관해서는 각자 알아서 해보시는 것으로 하고, (연습문제라고 해도 좋겠습니다. ㅎㅎ) ZoomingPDFViewer 예제 분석과 개선에 관한 이번 글은 마치도록 하죠. 

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 띵호

    좋은 글 잘 읽었습니다.

    님이 남겨주신 문제가 딱 제가 가지고 있는 문제네요 ㅋㅋㅋㅋ
    근데 pdfScale를 가지고 계속 씨름 중인데. 혹시..아닌가요??ㅜㅜ

    2012.05.29 02:27 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 띵호

      와우 거의 한시간 동안 씨름해봤는데....
      문제를 해결했습니다.
      머가 실마리인지 몰랐는데 minimumzommScale 과 maximumZoomScale 를 가지고 노니 해결되네요 ㅋㅋ

      감사합니다 수고하세요~~

      2012.05.29 03:00 신고 [ ADDR : EDIT/ DEL ]

Languages/Objective-C2011.01.11 15:18
아이패드 보급이 늘어가면서 아이패드 위에 PDF 뷰어같은걸 구현하시는 분들도 많이 늘어가는 것 같군요. 그런 소프트웨어 구현할 때 가장 많이 사용하게 되는 것이 CGContextDrawPDFPage라는 함수죠. 이 함수는 현재의 드로잉 컨텍스트 위에 PDF 페이지를 뿌립니다.

이 함수의 한가지 문제는 호출할 때 마다 굉장히 많은 메모리를 계속 잡아먹는다는 점이죠. 그래서 PDF 파일을 열어놓은 상태로 모든 페이지를 전부 다 보게 되는 경우, 메모리 관련 경고가 뜨게 될 수 있습니다.

이 문제를 해결하는 한가지 방법은 메모리 점유율이 증가하거나 메모리 관련 경고가 뜨는 경우 (그러니까 다시 말해 메인 View Controller의 didReceiveMemoryWarning이 호출되는 경우) PDF 파일을 닫았다가 다시 여는 것이 되겠습니다. 

그런데 그런 코드를 작성하려면 PDF 파일에 대한 레퍼런스 (CGPDFDocumentRef)를 들고 있는 클래스를 어디 한 군데로 제한하는 것이 바람직하죠. 그럴 때에는 Singleton 패턴을 써야 한다는 것은 아마 많은 분들이 알고 계시겠습니다만...

그런데 그냥 CGPDFDocumentRef만 singleton 패턴으로 관리하면 땡인걸까요?

그렇지 않습니다. 한 쪽에서 이 패턴 객체를 통해 레퍼런스를 가져가서 열심히 뭔가 작업을 하고 있는데, 다른 한쪽에서 이 레퍼런스를 닫아버린다거나 하게 되면 프로그램이 당장 돌아가시게 되죠. 

그러니까 결국은 Singleton 패턴을 통해 CGPDFDocumentRef에 대한 접근 자체를 통제해야 합니다. 몇 가지 방법이 있겠습니다. 레퍼런스 카운터를 통해서 모두가 document ref를 전부 다 썼다는 것이 보장되기 전까지는 닫지 않는다거나 하는 것도 한가지 방법이죠.

저는 메모리를 얼마 이상 쓰지 않아야 한다는 요구조건이 있어서, 다음과 같은 싱글턴 클래스를 만들고 block을 통해서 해당 PDF 문서에 대한 작업을 구현하도록 하는 클래스를 만들어서 쓰고 있습니다. 소스코드를 보시면...

//

//  PDFSingleton.h

//  Muine

//

//  Created by bjlee on 11. 1. 11..

//  Copyright 2011 buggymind.com. All rights reserved.

//


#import <Foundation/Foundation.h>



@interface PDFSingleton : NSObject {


}


+ (NSUInteger) do:(NSUInteger(^)(CGPDFDocumentRef))blk;


@end



//

//  PDFSingleton.m

//  Muine

//

//  Created by bjlee on 11. 1. 11..

//  Copyright 2011 buggymind.com. All rights reserved.

//


#import "PDFSingleton.h"



@implementation PDFSingleton


static CGPDFDocumentRef _pdf = NULL;

static int requestCount = 0;


+ (NSUInteger) do:(NSUInteger(^)(CGPDFDocumentRef))blk {


@synchronized ( self ) {

if ( ++requestCount == 6 ) {

CGPDFDocumentRelease(_pdf);

_pdf = NULL;

requestCount = 1;

}

if ( _pdf == NULL ) {

CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("xxx.pdf"), NULL, NULL);

_pdf = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL);

CFRelease(pdfURL);

}


return blk(_pdf);

}

}



@end


PDF 문서 페이지가 여섯번 이상 요청되면 강제로 닫았다가 다시 열도록 했구요. 해당 pdf 문서에 대한 사용자 작업은 block으로 처리했습니다. 사용 예를 보시면...

// ctx의 선언은 이 위 어딘가에 있음 ㅎ

[PDFSingleton do:^(CGPDFDocumentRef pdf) {

CGPDFPageRef page = CGPDFDocumentGetPage(pdf, index);

// ...

CGContextDrawPDFPage(ctx, page);

return (NSUInteger)0;

}];


그런데 이 코드에는 문제가 있습니다. 뭘까요?

네. 어떤 한 쓰레드에서 해당 PDF 파일을 사용해 뭔가 심각하고 오래 걸리는 일을 하고 있으면, 다른쪽에서는 그 일이 끝날 때 까지 무작정 기다려야 합니다.

그러니 동시성을 활용해 캐싱이라던가 프리패칭(pre-fetching)같은 것을 하고 싶다면, 이렇게 하면 곤란하겠죠. 연습문제 삼아, 각자 이 문제를 고민해보시기 바랍니다. :-)
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.04 18:46
이건 비단 MPMoviePlayerController에 국한된 이야기는 아닐 수 있습니다. 다만 이 문제가 '비동기적'인 오류를 유발할 수 있으며, 디버깅이 굉장히 까다로운 문제를 야기할 수 있다는 점을 지적하고 싶습니다. 생각해보면 매우 단순한 문제인데, 깨닫기가 어렵죠. 거기다 iOS 3.2버전과 iOS 4.0 버전에서 동작 방식이 다릅니다. (특히 시뮬레이터에서는 더더욱요.)

간단히 문제를 살펴보죠. 제가 MoviePlayer라는 UIView 클래스의 하위 클래스를 만들었다고 치겠습니다. 이 클래스의 생성자 안에서 MPMoviePlayerController의 객체를 하나 만든 다음에, 이 객체를 사용해서 영화를 재생하기 전에 다음과 같은 짓을 했다고 해 보죠.

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(movieStopped) 
                                                              name:MPMoviePlayerPlaybackDidFinishNotification 
                                                             object:moviePlayer];

이렇게 하면, 영화 재생이 끝나면 self, 그러니까 MoviePlayer 객체의 movieStopped 메소드가 호출됩니다. Notification 메커니즘 덕분이죠. 여기서 moviePlayer는 MPMoviePlayerController 객체입니다.

그런데, iOS 3.2에서는 영화가 정상적으로 재생이 끝난 경우에만 notification이 발생하고, 재생 도중에 moviePlayer에 대해서 [moviePlayer stop]을 날린 경우에 대해서는 Notification이 발생하지 않았었어요.

하지만 iOS 4.0에서는 이야기가 좀 다릅니다. [moviePlayer stop]한 경우에도 Notification이 발생하거든요. (실제 장비에서는 모르겠지만 적어도 시뮬레이터에서는 발생합니다.)

그러니, [moviePlayer stop]을 코드에 넣으려고 하고 있다면, 다음과 같이 해 주어야 합니다. 아니면 movieStopped 메소드 안에 removeObserver를 호출하는 부분을 두거나요. 그래야 안전합니다.

[[NSNotificationCenter defaultCenter] removeObserver:self];

[moviePlayer stop];


왜 그럴까요? 만약에 [moviePlayer stop]을 날린 이후에 MoviePlayer 객체를 dealloc했다고 해 보죠. 그러면 아까 addObserver 할 때 인자로 넘겼던 self가 온데간데 없이 메모리에서 사라지게 되거든요. 그러니 removeObserver를 해주지 않으면 사라진 객체에게 NSNotification이 날아가게 되는 것이죠.

그러면 어떻게 되나요?

(잠시 침묵)

네. 사라진 객체가 메시지를 받기 때문에 프로그램이 쭈욱 뻗습니다. (죽는단 소리죠.)

생각해보면 간단하기 짝이 없는데, 몇 시간의 삽질 과정이 없으면 깨닫기 힘든 경우도 많습니다. 이 글이 여러분의 삽질을 조금이라도 줄여주었으면 좋겠군요. :-)


신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.04 16:19
[이전 글에 이어서..]

그럼 새로운 UIView 애니메이션은 어떻게 써먹느냐. Ruby 같은 프로그래밍 언어를 보면 함수를 실행할 때 그 함수에 다른 함수 바디를 동적으로 만들어서 넘길 수도 있도록 허용하고 있는데요. Objective-C에서도 이제 그런 식의 프로그래밍이 가능합니다. 우선 developer.apple.com에서 퍼온 API 설명부터 한번 보죠.

animateWithDuration:animations:completion:

Animate changes to one or more views using the specified duration and completion handler.

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

The total duration of the animations, measured in seconds. If you specify a negative value or 0, the changes are made without animating them.

animations

A block object containing the changes to commit to the views. This is where you programmatically change any animatable properties of the views in your view hierarchy. This block takes no parameters and has no return value. This parameter must not be NULL.

completion

A block object to be executed when the animation sequence ends. This block has no return value and takes a single Boolean argument that indicates whether or not the animations actually finished before the completion handler was called. If the duration of the animation is 0, this block is performed at the beginning of the next run loop cycle. This parameter may be NULL.

Discussion

This method performs the specified animations immediately using the UIViewAnimationOptionCurveEaseInOut and UIViewAnimationOptionTransitionNone animation options.

For example, if you want to fade a view until it is totally transparent and then remove it from your view hierarchy, you could use code similar to the following:


이 함수는 static method이고, 첫 번째 인자로는 Animation 지속시간, 두 번째 인자로는 애니매이션이 수행되는 동안 행해질 작업에 대한 코드 블럭, 그리고 세 번째 인자로는 애니메이션이 끝나면 실행될 코드 블럭이 넘어갑니다.

대충 어떻게 써먹느냐 하면...

[UIView animatedWithDuration:0.2
            animations:^{view.alpha = 0.0;}
            completion:^(BOOL finished){ 
                               if ( finished)
                                  [view removeFromSuperView];
                             }];

위의 코드는 0.2 초 에 걸쳐, 어떤 뷰를 서서히 투명하게 만들고, 투명화 작업이 끝나면 그 뷰를 부모 뷰에서 떼 버리는 코드입니다. 블럭의 시작은 ^으로 표시하고, 인자의 목록은 () 안에, 블럭 코드 바디는 {} 안에 둔다는 것을 알 수 있습니다.

이렇게 코딩하면 앞서 봤던 [UIView beginAnimations], [UIView commitAnimations]를 통해 구현한 경우보다 코드 사이즈가 줄어듭니다. 애니메이션 종료시 호출될 메소드를 따로 구현해 둘 필요가 없거든요. 블럭 문법에 익숙한 분들에게는 코드도 훨씬 더 깔끔해 보이죠.



신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2011.01.04 14:19

[UIView beginAnimation]

이거 써서 애니메이션 효과 넣으신 분들 많으시죠. iOS 4.0 부터는 블럭 기반 문법을 사용하는 API를 사용하도록 하라는 권고안이 날아오는 바람에 앞으로는 사용하기 좀 껄끄러워 질 것 같긴 합니다만, 그럼에도 여전히 많이 쓰이고 있습니다.
 
기본적으로 이 계열 API는 다음과 같이 사용하는데요.

gameCenter.frame = before;
[self.view addSubview:gameCenter];
[UIView beginAnimations:nil context:nil];
gameCenter.frame = after;
[UIView commitAnimations];


이렇게 하면 gameCenter의 프레임 위치가 움직이는 것이 애니메이션 형태로 표현되죠.

그런데 이렇게 프로그래밍 해 본 분들이면 대충 아시겠습니다만, commitAnimations 한 다음에 [gameCenter removeFromSuperview] 한다거나 [gameCenter release] 하면 애니메이션 효과가 제대로 먹질 않는 경우가 많아요. 애니메이션이 끝나기도 전에 뷰가 화면에서 사라지기 때문이죠.

이를 방지하기 위해서는 다음과 같이 해야 합니다.
(아마 대충 다 아시겠습니다만... ㅎ)

[UIView beginAnimations:nil context:nil];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(clearGameCenter)];
gameCenter.frame = after;
[UIView commitAnimations];
 
 
그러니까 애니메이션이 끝난 다음에 호출될 메소드와 그 메소드가 정의되어 있는 객체를 넘기는 것이죠. 후자의 작업은 setAnimationDelegate를 통해서 하고, 전자의 작업은 setAnimationDidStopSelector를 통해서 합니다.
 
그러니까 애니메이션이 끝난 다음에 gameCenter UIView를 화면에서 지워버리고 싶다면, clearGameCenter 메소드 안에서 다음과 같이 하면 됩니다.
 
- (void) clearGameCenter {
    [gameCenter removeFromSuperview];
    [gameCenter release];
    gameCenter = nil;
}
 
간단하죠. 이런 일들이 iOS 4.0 이상에서는 좀 더 간단해 질 터인데, 그에 관해서는 제가 XCode를 업그레이드 한 다음에 살펴보도록 하겠습니다. ㅎㅎ
 

[다음에 계속]


신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2010.02.07 12:21

This program is basically bluetooth-based messenger.

All the participants of a messenger session can share their opinion and contact info with this simple program during their coffee break :-) or any kind of meeting held within the reach of bluetooth network.

The program enables meeting participant to communicate easily, and saves all the conversations as local history. The saved conversations can be sent by email, which enables users to let others know about the meeting result.

The program is useful in almost every situation where text-based communication is needed. But especially effective if you do stand-up meeting frequently.

As all the participants of the meeting can share their opinion instantly, the burdens of taking notes about the meeting is simply reduced.

In the near future, this program will be enhanced to support photo taking & sharing. The enhancement will satisfy everyone who wants to leave the visual record of their meeting. :-)

Have fun! APPSTORE LINK

PS. If you have any question, pleas leave comments.

이 프로그램은 블루투스 메신저 프로그램입니다.

이 프로그램으로 매일 매일 진행하는 모든 형태의 미팅 기록을 남기고, 메일로 전송할 수 있으며, 연락처 정보를 주고받을 수 있습니다. (연락처 정보는 config 메뉴를 통해 설정할 수 있습니다.)

가령 매일 매일 stand-up meeting을 진행하는 개발자라면, 이 응용 프로그램을 통해 자신의 의견을 미팅 참여자에게 전달하고, 그 기록을 자동으로 자신의 iphone에 보관할 수 있습니다.

저장된 기록을 email로 전송할 수도 있기 때문에, 회의록을 남기기 위해 따로 문서 작업을 할 필요도 없습니다. 연락처를 주고받을 수 있는 기능은 명함이 없는 상황에서 유용합니다. 학회가 열리는 장소나 전시회가 열리는 장소에서 미팅 방 이름만 공개하면, 다수에게 자신의 연락처를 즉시 전송할 수 있습니다.

이 프로그램으로 여러 행사에 참여하여 다른 사람들과 의견을 나누는 과정이나, 매일 매일 일상적으로 일어나는 회의의 기록을 남기는 과정의 생산성이 조금이나마 올라가기를 기대합니다. 재미있게 사용해 주세요. 감사합니다.



신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2009.12.30 20:05
Objective-C에서는 클래스 변수의 개념을 명시적으로 지원하지 않습니다. Java 같으면 다음과 같이 할 수 있었겠죠.

public class MyClass {
  static int INSTANCE_COUNT = 0;
  //...
}

하지만 Objective-C에서는 그렇게 할 수 없기 때문에, static 변수를 사용한 편법을 써야 합니다. C/C++에서 static 키워드는 다음과 같은 효과를 갖습니다.

1. 함수 안에서 static으로 선언된 변수의 lifetime은 그 함수가 최초로 사용된 시점부터 프로그램이 종료될 때 까지로 연장된다.

2. 함수 외부에서 static으로 선언된 변수의 scope는 그 변수가 사용된 파일 내부로 한정된다.

따라서 2번 규칙을 사용하면 비슷한 효과를 얻을 수 있습니다.

다음 예제를 한번 보시죠.

// MyClass.h

@interface MyClass : NSObject {
}

+ (MyClass*) myClassFactory;

+ (int) count;

- (id) init;

@end

// MyClass.m

@implementation MyClass

static int INSTANCE_COUNT = 0;

+ (MyClass*) myClassFactory {
  ++INSTANCE_COUNT;
  return [[[MyClass alloc] init] autorelease];
}

+ (int) count {
  return INSTANCE_COUNT;
}

- (id) init {
  if ( self = [super init] ) {
    // ...
  }
  return self;
}

@end

- - -

삽질의 길은 언제나 멀고도 험하다는...
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 비밀댓글입니다

    2010.01.04 21:19 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2009.12.24 10:01


심심해서 만들어봤다던 아이폰 앱 10000 hours가 앱스토어에 등록되었다. 링크는 http://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=347168390&mt=8 이다. 소프트웨어가 허접해서 리뷰가 빨리 끝난듯.. -_-;

가만 보고 있으니 어서 빨리 업데이트나 해야 겠다는 생각이 밀려든다. 그런데 벌써 두 번째 소프트웨어 "CoffeeBreak"를 만들고 있는 상태라 그거 끝날때까지는 아마도 첫 번째 소프트웨어의 다음 버전은 내놓지 못할 듯... 

아이디어는 좀 있는데 시장조사할 여력도 없고 UI 구현도 하도 오랫만에 해보는 거라 진도가 더디다. 취미삼아 시작한 건데 이러다가 중독되는 건 아닌지...

"CoffeeBreak" 이야기하니까 아침부터 커피가 무지 땡긴다. 어제 너무 마셔서 잠을 설쳤는데, 오늘은 적당히 좀 마셔야 할듯... 하지만 앱스토어 등록 자축 의미로 따아악 한잔만!


신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 강성희

    방금 다운받았습니다. 감사히 쓰겠습니다. :D

    2009.12.24 17:15 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2009.12.17 14:11
연말이 되었는데 이런 저런 실험 탓에 기다려야 하는 시간이 많아서 시간도 때울 겸(?) 아이폰 앱을 하나 만들어 보았다. "아웃라이어"에 보면 10,000시간의 법칙이 나오는데, 요지는 10,000시간의 의도적 수련(deliberate practice)이 전문가를 만들어 준다는 것이다. 이 시간을 관리할 수 있게 하는 앱이다. (이런 거나 하고 있으니 한가해 보이는 걸까...)
 
As pointed out in the book "Outliers", a professional is made by 10,000 hours deliberate practice. This iPhone app manages time spent on your deliberate practices.

First, you define "Areas" where you want to be a professional.


Next, you define "activities" which comprise an area. The time spent on each activity is summed and displayed in the area view page.


By clicking activity and press "Start" button, you can count the time spent on the acvitivity. Following screen shows that I've spent 8 minutes on 'coding' activity :-)


To stop the counting, you can press "stop" button or "back" button at the top of the screen. That's it.

개인적인 용도로 써먹을려고 만들었는데, 시험삼아 공개해보는 것도 나쁘지 않을 것 같아서 앱 스토어에 등록을 신청해 둔 상태다. 어제 먹은 술이 덜 깨서 등록 절차를 제대로 밟았는지도 잘 모르겠다. 

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 깔끔한데요! 아이폰 사면 함 다운받아보겠습니다 ^^
    그나저나, 10,000시간 채우기 전에 배터리 수명이 다하지 않을런지요.. ㅋ

    2009.12.17 18:37 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. 괜찮네요 어플등록되면 다운받겠습니다.^^

    2009.12.18 09:27 신고 [ ADDR : EDIT/ DEL : REPLY ]
  3. 왠지 대박 얘감!!!

    2009.12.18 10:01 신고 [ ADDR : EDIT/ DEL : REPLY ]
  4. 남미영

    팀장님..역쉬..^^
    아이폰 언제 살지 모르겠으나..저도 다운받아서 사용해봐야겠습니다.
    10000시간 투자하면 아웃라이어가 될 수 있을까요?? 감사합니다.

    2009.12.18 10:49 신고 [ ADDR : EDIT/ DEL : REPLY ]

Languages/Objective-C2009.12.16 06:42
얼마전에 Programming in Objective-C 2.0을 받았다. 아이폰 어플을 개발해보려고 하던 차라, 마침 필요하던 책이었다.

Objective-C를 입문할 때 C++이나 Java 프로그래머가 가장 헷갈리는 부분을 꼽으라면?

1. 메모리 관리 (retain, copy, release, autorelease...)
2. [와 ]로 표현되는, 다소 생뚱맞은 메시지 전달 방식
3. C++과는 달리, 대충 대충 해도 일단은 넘어가주는 클래스 선언 방식

C++이나 Java 프로그래머에게 있어서 Objective-C의 진입장벽은 다소 낮은 편이다. 뭐 어차피 다 같은 객체지향 언어이므로, 사실 거기서 거기다. 그러니 저런 부분만 파악하면 쉽게 쉽게 코딩을 할 수 있지 않을까 생각해서 애플에서 제공하는 Objective-C 2.0 가이드를 대충 본 다음에 바로 코딩에 들어갔었다.

그런데 아니나 다를까... 역시 대충 대충 공부한 탓인지 결정적인 부분에 가면 꼭 막혔다. -_-;

- - -

C++을 한 8년 가까이 한 이후, Java The Programming Language를 한 일주일 정도 보고 나서 바로 Java 프로그래밍을 할 수 있었다. 객체지향 언어라는 것에 대해 익숙해진 탓도 있었겠지만, 책이 워낙 잘 씌여진 탓도 있었다. 좋은 교과서가 있으면 실패할 확률이 줄어든다.

Objective-C의 실질적인 교과서를 꼽으라면, 이 책일 것이다. 크게는 2부로 나뉘어 있다. 언어적인 특성을 설명한 1부. 그리고 C++의 STL에 해당하는 Foundation Framework에 대해서 설명한 2부.

다른 프로그래밍 언어 서적들과 크게 다른 구성을 가지고 있지 않고, Java나 C++과의 차이점을 비교설명하고 있지 않으므로 그런 부분만을 재빨리 파악한 다음에 바로 프로그래밍을 시작하려는 사람에게는 그다지 적당하지는 않다. 하지만 책이 평이하고 쉬운 어조로 서술되어 있으므로, 일주일 정도 투자하면 완독할 수 있을 것이라고 생각한다.



정 시간이 없는 사람은 부록 B만 읽고 넘어가도 된다. -_-; 언어를 전반적으로 요약해두었으니 Objective-C에 대한 기본적인 사항을 이미 알고 있는 사람이라면 그래도 될 것이다.

그러나 아쉬운점은, 부록 B에 적힌 내용을 좀 더 자세히 알고 싶을 경우 본문의 어디를 보아야 하는지를 전혀 알 수가 없다는 점이다. (C++ The Programming Language의 경우, 관련된 부분에 대한 링크가 일일이 달려 있는 것과는 대조적이다.) 가령, property를 사용할 경우 retain, copy 등의 속성을 property를 선언할 때 지정해 주게 되고 이것이 메모리 관리와 연관이 있다는 내용이 부록 B에는 친절하게 정리되어 있는데, 이게 메모리 관리와 실질적으로 어떤 관계를 맺고 있는지를 파악하려면 어디를 보아야 하는지 알 수가 없다. 목차를 보면 17장 "메모리 관리"를 보면 되겠구나 싶겠지만, 아니다. 관련된 부분은 책 여기 저기에 흩어져 있다. (가령 nonatomic과 copy에 대한 내용은 482페이지에 가서야 설명된다.)
 
그런 사소한 불편함을 빼면, Objective-C를 공부하는 사람은 가지고 있어야 할 책이라고 생각한다. 객체지향 언어에 이미 익숙한 사람에게는 설명이 너무 많아 장황하게 느껴질 수도 있겠지만, 나처럼 나중에 개고생하지 않으려면 이런 책을 꼼꼼하게 읽어두는 것이 좋다. 원래 교과서라는 게 다 처음에는 재미가 없다. -_-;
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Languages/Objective-C2008.11.06 14:43
이 글의 원문은 http://en.wikipedia.org/wiki/Objective-C 입니다. 한국 위키피디아에 번역글을 올리고 싶었는데, 올리는 방법을 찾기가 귀찮아서 그냥 여기다 올립니다. 누군가 퍼주실 의향이 있으시다면 올려주시면 고맙겠습니다. :-) 번역에 이상한 부분이 있다면, 역시 지적해주세요. 서너시간만에 낼름한거라 틀린 부분이 있을수도 있습니다. 감사합니다.

- - -

Objective-C는 reflective 객체지향 프로그래밍 언어로서, Smalltalk 스타일의 메시징 메커니즘을 C에 추가한 것이다.

오늘날 이 언어는 Mac OS X와 GNUStep이라는, OpenStep 표준에 기반한 두 환경에서 주로 사용되고 있으며, NextSTEP, OPENSTEP, Cocoa 응용 프레임워크의 주 언어로 쓰인다. 이런 프레임워크가 제공하는 라이브러리를 사용하지 않는 일반적인 Objective-C 프로그램은 gcc가 설치된 시스템이라면 어떤 시스템에서도 컴파일될 수 있다. gcc에는 Objective-C 컴파일러가 포함되어 있는 까닭이다.

역사

1980년대 초, Software Engineering의 일반적 관행은 structured programming의 관습을 따르는 것이었다. 구조화된 프로그래밍 기법은 프로그램을 잘개 쪼개어 작업하기 쉽도록 만드는 것을 돕기 위한 기법이었다. 하지만 풀어야 할 문제 규모가 커짐에 따라 구조화된 프로그래밍 기법의 유용성은 감소하였는데, 더 많은 프로시저가 작성되어야 하다 보니 재사용성이 떨어지고 프로그램 제어 흐름도 복잡해지기 때문이었다.

많은 사람들은 객체지향 프로그래밍 기법이 이 문제에 대한 해답이 될 수 있지 않을까 하고 생각하였다. 사실, Smalltalk라는 프로그래밍 언어는 이미 이러한 Engineering 이슈들을 섭렵하고 있었다. 세상에서 가장 복잡한 시스템들 중 몇몇이 Smalltalk 환경에서 작성되었던 것이다. 하지만 이 언어에도 한가지 문제는 있었으니 가상 머신virtual machine을 사용한다는 것이 그것이었다. Smalltalk의 가상 머신은 이미지image라고 불리는 객체 메모리를 두고 여기에 모든 개발 툴들을 저장해두었다. 그러다보니 메모리 요구량이 그 당시로는 비합리적일 정도로 늘어났고, 느리게 동작할 수 밖에 없었다. 이는 부분적으로는 vm/container 개념을 쓸만하게 지원하는 하드웨어가 없었기 때문이기도 했다.

Objective-C는 Stepstone이라는 회사에서 일하고 있던 Brad Cox와 Tom Love라는 두 연구원이 만들었다. 역시 1980년대 초의 일이다. 이 두 사람은 1981년 ITT의 Programming Technology Center에서 일하던 시절 Smalltalk를 처음 접했다. Cox는 소프트웨어 설계와 프로그래밍에 있어서 재사용성의 문제에 흥미를 느끼게 되었고, Smalltalk같은 프로그래밍 언어가 ITT 시스템 개발자들이 개발을 진행할 강력한 환경을 만드는 데 필요불가결한 요소임을 깨닫게 되었다. Cox는 C 컴파일러를 고쳐 Smalltalk의 기능 일부를 추가하기 시작했고, 곧 그 스스로 "OOPC라고 불렀던 C의 객체지향 버전을 내놓게 되었다. 한편 Love는 1982년에 Schlumberger Research에 채용되었고, Smalltalk-80의 최초 상업적 버전을 써 볼 기회를 가지게 되었다. Smalltalk-80은 이후 그들이 낳은 정신적 자식의 성장에 큰 영향을 끼쳤다.

새로운 언어가 가질 실질적인 파급력을 증명하기 위해, Cox는 기존 툴들에 몇 가지 사소한 변경만 가하면 재사용이 가능한 소프트웨어 컴포넌트를 만들 수 있음을 시연해보였다. 특히 컴포넌트들은 객체를 유연하게 지원해야 했고, 라이브러리들과 함꼐 제공되어야 했으며, 코드 (그리고 그 코드가 필요로 하는 자원들)는 하나의 단일한 플랫폼-중립적 포멧으로 저장될 수 있어야 했다.

결국 Cox와 Love는 Productivity Products International(PPI)이라는 새로운 벤처 회사를 설립한다. 목적은 Objective-C 컴파일러와 강력한 클래스 라이브러리를 결합한 상업 제품을 만들어 파는 것이었다.

1986년에 Cox는 Objective-C의 명세를 담은 책 Object-Oriented Programming, An Evolutionary Approach를 출간한다. 이 책에서 그는 재사용성의 문제가 단순히 언어 차원의 문제가 아님을 지적하였으나, 이후 Objective-C는 종종 다른 언어와 기능적으로 비교되고는 했다.

NeXT를 통해 인기를 얻다

1988년 Steve Jobs는 NeXT라는 회사를 설립한다. NeXT는 StepStone으로부터 Objective-C를 라이센스받았고 NeXTstep 사용자 인터페이스와 그 빌더를 구축하는 데 쓰일 Objective-C 컴파일러와 라이브러리들을 스스로 만들어 내놓았다. NeXT가 내놓은 워크스테이션들이 시장에 강력한 영향력을 행사하지는 못했지만, 그 툴들만큼은 업계에 널리 퍼지게 된다. 결국 NeXT는 하드웨어 제작을 포기하고 소프트웨어 툴들에 집중하기로 방향을 선회하여 NeXTstep (그리고 OpenStep)을 독립적인 사용자 프로그래밍 환경으로 판매하기 시작했다.

이 때 NeXTstep의 free clone을 구축하려는 프로젝트가 GNU에서 시작되었는데, 그 이름은 GNUstep이었다. OpenStep 표준에 바탕을 둔 프로젝트였다. 1992년, Dennis Glatting이 gnu-objc의 런타임을 처음으로 만들었다. 1993년 이후로는 Kresten Krab Thorup이 Denmark에서 대학생시절에 만든 런타임이 널리 쓰였다. Kresten은 1993년부터 1996년까지 NeXT에서 일했다.

1996년에 NeXT는 Apple에 합병된다. Apple은 OpenStep을 기반으로 Mac OS X라는 새로운 운영체제를 내놓는다. 이 안에는 Objective-C와, NeXT의 Objective-C 기반 개발 툴들이 탑재되어 있었다. Project Builder(나중에 Xcode라는 이름으로 바뀐다)라는 개발환경이 제공되었고, Interface Builder라는 인터페이스 설계 툴이 제공되었다. 오늘날 Apple의 Cocoa API 대부분은 OpenStep 인터페이스 객체에 기반을 둔 것이며, 아마 이것이 현존하는 Objective-C 개발 환경 중 가장 널리 쓰이면서도 중요한 것일 것이다.

문법

Objective-C는 C위에 올라가는 아주 얇은 레이어이며, 엄격한(Strict) C 상위집합(superset)이다. 어떤 C 프로그램도 Objective-C 컴파일러로 컴파일할 수 있다는 뜻이다.Objective-C의 문법은 Smalltalk와 C의 문법을 계승하고 있다. 대부분의 문법은 C로부터 가져온 것이지만, 객체지향 기능에 관계된 것들은 Smalltalk-스타일의 메시징 메커니즘을 고려한 것이다.

메시지

Object-C의 문법은 이제 좀 낡아보이는 C 문법에 대안적 요소들을 추가하는 것에 그치지 않고, 객체지향 프로그래밍을 지원한다. Objective-C의 객체지향 프로그래밍 모델은 자족적인 객체에 '메시지'를 보내는 것에 그 근간을 두고 있다. C++이 차용하고 있는 Simula 프로그래밍 모델과는 다른 부분이다. 그리고 이런 차이는 의미적으로 중요하다. 가장 기본적인 차이는, Objective-C는 '메소드를 호출하지 않'으며, '메시지를 보낸다'는 것에 있다. Objective-C에서 메시지의 수신자는 그 메시지를 거부할 수 있다. 둘 중 어느 스타일을 취하건, 장단점은 있다. Simula 스타일의 OOP는 다중 상속을 지원하고 가능한한 컴파일 시간에 바인딩을 하려고 하기 때문에 더 빨리 수행될 수 있다. 또한 virtual 로 선언되지 않은 모든 메소드는 반드시 구현되어 있어야 하며, 설사 virtual로 선언되어 있더라도 구현이 되지 않은 메소드는 호출될 수 없다. Smalltalk 스타일의 OOP는 메시지가 구현되지 않은 상대에게 전송되는 것을 허용하며, 기본적으로 동적 바인딩에 의해 처리된다. 하지만 그렇기 떄문에 때로 느리게 동작하며, 어떤 프로그래머들은 (특히 Simula 진영에 속한 프로그래머들) 디버깅하기가 좀 더 까다롭다고 느낀다. 그런 이유로 Simula 진영 프로그머들은 때로 Objective-C 를 싫어하고, Objective-C 프로그래머들은 Simula 스타일의 언어를 싫어하며, 스스로 상대 진영의 OO 언어는 진정한 OO 언어가 아니며, 심각한 결함이 있다고 느끼기도 한다. (특히 C++은 그런 점에서 가장 자주 공격받곤 하는 언어가 될 것이다. 이에 관해서는 C++ FAQ Lite를 참고하기 바란다.)

메소드 method를 가지고 있는 객체 obj는 메시지 method에 '반응한다'고 일컫는다. C++이라면, obj에 method라는 메시지를 전송하는 코드를 다음과 같이 작성했을 것이다.

obj.method(parameter);

Objective-C에서는 다음과 같이 한다.

[obj method:parameter];

이 메커니즘은 정의된 객체에 메시지가 실행 시간에 전달되도록 한다 - C++과 같은 정적 타입 언어는 현행 표준에 따르면 그런 식으로 동작할 수는 없다. 하지만 C++도 Boost Library가 표준화된다면 ANSI 표준에 따르는 메시징을 지원할 수 있게 될 것이다. Qt는 이 기능을 C++을 비롯한 다른 언어들에게 제공한다(Objective-C 지원은 빈약한 수준이다). 이 기능을 지원하는 많은 클래스들 뿐 아니라 'connect' 함수를 제공하기 때문이다. (동적 타입의 장점에 대해서는 아래의 '동적 타입' 절을 참고하기 바란다.)

Objective-C 에는 메시지 전달과 관계된 몇몇 기능들이 들어 있다. Objective-C 메시지는 반드시 실행될 필요는 없는데, 동적으로 바인딩되기 때문이다. 메시지를 받은 객체가 그 메시지를 구현한 경우 해당 메시지는 실행될 것이지만 그렇지 않은 경우에는 실행되지 않을 것이다. 그렇다 하더라도 코드는 문제없이 컴파일되고 실행된다. 예를 들어, 모든 객체는 awakeFromNib이라는 메시지를 받는다.. 하지만 모든 객체가 이 메시지를 처리하도록 구현되어야만 컴파일이 되는 것은 아니다. 객체에 awakeFromNibg이라는 메시지를 처리하는 부분이 구현되어 있다면 메시지를 받았을 때 그 코드가 실행되겠지만, 그렇지 않은 경우에는 해당 메시지는 무시된다. 또한 메시지는 메시지를 구현한 객체에 보내질 수도 있고, 그 객체가 상속한 상위 클래스에 보내질 수도 있다. 이 메커니즘은 self와 super라는 객체 포인터를 통해 지원된다. 메시지는 nil 객체에도 전달될 수 있다.

인터페이스와 구현

Objective-C는 인터페이스와 클래스 구현이 별도로 선언된 코드 블록에 놓이도록 한다. 관습적으로, 인터페이스는 헤더 파일에 놓이며 구현은 코드 파일에 놓인다. 헤더 파일의 확장자는  .h이고, 구현 파일의 확장자는 통상 .m인데, 통상적인 C 파일들과 유사하다.

인터페이스

클래스의 인터페이스는 보통 헤더 파일 안에 정의된다. 통상적인 관습은 클래스의 이름과 헤더 파일의 이름을 같게 두는 것이다. Class라는 이름의 클래스에 대한 인터페이스는 Class.h에 두도록 한다는 것.

인터페이스 선언은 다음과 같은 형태를 띤다.

@interface classname : superclassname {
    // instance variables
}
+classMethod1;
+(return_type)classMethod2;
+(return_type)classMethod3:(param1_type)parameter_varName;
 
-(return_type)instanceMethod1:(param1_type)param1_varName :(param2_type)param2_varName;
-(return_type)instanceMethod2WithParameter:(param1_type)param1_varName 
andOtherParameter:(param2_type)param2_varName; @end
플러스 기호 +는 클래스 메소드라는 뜻이고, 마이너스 기호 -는 인스턴스 메소드라는 뜻이다. 클래스 메소드는 인스턴스 변수를 접근할 수 없다.

반환값 타입으로는 표준 C 타입, Objective-C 객체에 대한 포인터, 그리고 특별한 종류의 객체에 대한 포인터(NSArray*, NSImage*, NSString* 등)을 사용할 수 있다. 내정(default) 반환값 타입은 Objective-C 타입인 id이다.

메소드의 인자는 콜론 기호 뒤에 인자 타입과 그 이름을 두는 식으로 명시하는데, 타입 이름에는 괄호를 두른다. 어떤 경우에는 (system API를 작성한다거나 하는 경우) 각 인자 이름 앞에 그 의미를 설명하는(descriptive) 텍스트를 두기도 한다.

-(void) setRange:(int)start :(int)end;
-(void) importDocumentWithName:(NSString *)name 
withSpecifiedPreferences:(Preferences *)prefs
beforePage:(int)insertPage;

구현

인터페이스는 클래스의 인터페이스만을 선언하며, 메소드가 어떻게 구현될지는 명시하지 않는다. 실제 코드는 구현(implementation) 안에 들어간다. 구현 파일은 통상 .m의 확장자를 갖는다.

@implementation classname
+classMethod {
    // implementation
}
-instanceMethod {
    // implementation
}
@end

메소드는 인터페이스에 선언된 대로 구현된다. C와 Objective-C의 경우를 비교해 보자.

int function(int i) {
    return square_root(i);
}









-(int)method:(int)i {
    return [self square_root: i];
}

Objective-C는 문법적으로 인자에 대한 pseudo-naming을 지원한다.

-(int)changeColorToRed:(float)red green:(float)green blue:(float)blue
 
[myColor changeColorToRed:5.0 green:2.0 blue:6.0];

이 메소드의 실제표현은 Objective-C 구현체에 따라 달라질 수 있다. myColor가 클래스 Color에 의해 만들어진 것이라면, 인스턴스 메소드 -changeColorWIthRed:green:blue는 내부적으로는 _i_Color_changeColorWIthRed_green_blue와 같은 이름을 갖게 될 수 있다. i는 인스턴스 메소드임을 표시하기 위한 것이고, 그 뒤에 클래스 이름과 메소드 이름을 두었다. 콜론은 전부 _ 기호로 변환되었다. 이 이름을 보면, 인자가 전달되는 순서 또한 메소드 이름의 일부임을 알 수 있다., 메소드에 전달되는 실인자의 순서를 바꿀 수는 없는 것이다. pseudo-naming이라고 한 것은 그래서이다.

하지만 이러한 내부 표현명을 직접적으로 사용할 경우는 거의 없다. 일반적으로 메시지는 Objective-C 런타임 라이브러리에 정의된 함수에 대한 호출로 변환된다. 그러니 링킹 시점에 어떤 메시지가 어떤 메소드를 호출하게 되는지를 알 필요는 없다. 실행 시간까지는 메시지의 리시버 클래스를 알 필요가 없다는 것이다.

객체 생성

일단 Objective-C 클래스가 작성되고 나면, 그 클래스로 객체를 만들 수 있다. 그 과정은 새로운 객체에 대한 메모리를 잡고, 그 메모리를 초기화하는 과정이다. 이 두 과정이 완전히 끝나기 전에는 객체는 완전하게 기능하지 못한다. 통상, 이 과정들은 다음과 같은 한 줄의 코드에 의해 처리된다.

MyObject * o = [[MyObject alloc] init];

alloc 호출은 객체의 인스턴스 변수들을 담기에 충분한 양의  메모리를 잡으며, init 호출은 그 변수들의 값을 특정한 값으로 초가화한다. init 메소드는 통상 다음과 같이 작성된다.

-(id) init {
    self = [super init];
    if (self) {
        ivar1 = value1;
        ivar2 = value2;
        .
        .
        .
    }
    return self;
}

프로토콜

Objective-C는 나중에 NeXT에 의해 확장되어 다중 상속의 개념을 스펙에 포함시키게 되는데, '프로토콜'이 바로 그것이다. 다중 상속은 C++에서처럼 여러 개의 상위 클래스를 상속받는 형태로 제공되기도 하고, Java나 C#에서처럼 '인터페이스'를 계승하는 형태로 지원되기도 한다(이 쪽이 좀 더 인기가 있다). Objective-C는 비정형 프로토콜(informal protocol)이라고 불리는 ad-hoc 프로토콜과, 정형 프로토콜이라고 불리는 컴파일러가 강제하는 프토토콜을 통해 다중 상속을 지원한다.

비정형 프로토콜은 클래스가 구현할 수 있는 메소드의 리스트이다. 그 리스트는 문서(documentation)에 명세되는데, 언어 스펙에는 그 리스트를 표현하기 위한 문법이 없기 때문이다. 비정형 프로토콜은 종종 선택적 메소드(optional method)를 포함하는데, 이 메소드를 구현하면 클래스의 동작이 바뀔 수 있다. 예를 들어, 텍스트 필드 클래스는 그 동작을 다른 객체에 위임(delegation)할 수 있는데, 그 객체는 autocomplete라는 선택가능 메소드를 포함하는 비정형 프로토콜을 구현해야 한다고 해 보자. 텍스트 필드 클래스는 리플렉션을 통해 그 메소드가 해당 객체에 구현되어 있는 것이 분명해지면 해당 메소드를 호출하여 자동 완성 기능이 제공되도록 할 수 있을 것이다.

Object-C에서의 프로토콜 개념은 Java나 C#의 인터페이스와는 좀 다르다. 클래스를 구현할 때, 특정한 프로토콜을 구현하도록 선언하지 않아도 그 프로토콜을 구현해 버릴 수 있기 때문이다. 그 차이는 외부 코드에서는 알아챌 수 없다. 한편 형식 프로토콜은 어떤 구현도 제공하지 않으며, 단순히 특정한 프로토콜을 만족하는 클래스는 해당 프로토콜에 속한 모든 메소드를 구현해야 한다는 것을 강제하기 위해 쓰인다. NeXT/Apple 라이브러리의 경우, 분삭 객체 시스템에 속한 객체가 원격 시스템에서 실행될 수 있는지를 명하하기 위해 프로토콜 개념을 자주 사용하고 있다.

문법은 다음과 같다.

@protocol Locking
- (void)lock;
- (void)unlock;
@end

락을 걸고 푼다는 추상화된 프로토콜이 존재한다는 사실을 명시하고 있다. 클래스를 정의할 때에는 다음과 같이 쓴다.

@interface SomeClass : SomeSuperClass <Locking>
@end

SomeClass에 의해 만들어진 객체들은 Locking 프로토콜에 명시된 두 개의 인스턴스 메소드의 구현을 제공할 것임을 명시하고 있다. 일례로 이런 추상화된 명세법은 구현 계층(hierarchy)이 어떻게 만들어져야하는지를 지정하지 않더라도 플러그인에 요구되는 행위 형태를 기술할 수 있어서 특히 효과적이다.

동적 타입(dynamic typing)

Smalltalk에서와 마찬가지로 Object-C에서는 동적 타입을 사용할 수 있다. 객체는 그 인터페이스에 명시되지 않은 메시지를 수신할 수 있다. 그 결과로 유연성이 향상되는데, 해당 메시지를 "캡처"한 후 또다른 객체에 그 메시지를 보내버릴 수도 있는 것이다. (메시지 수신 객체는 그 메시지에 올바르게 응답을 할 수도 있을 것이고, 아니면 또다른 객체에 메시지를 다시 전송해 버릴 수도 있을 것이다.) 이런 형태의 행위 패턴을 흔히 메시지 전달(message forwarding) 혹은 위임(delegation)이라고 부른다. (위임에 대해서는 아래에 나온다.) 메시지를 포워드(전달)할 수 없을 경우에는 오류 핸들러(error handler)를 통해 처리할 수도 있다. 객체가 메시지를 포워드하지도 않고, 오류 처리를 하지도 않고, 응답하지도 않는 경우에는 런타임 오류가 발생하게 된다.

선택적으로 변수에 정적으로 타입 정보를 부가할수도 있다. 이 정보는 컴파일 시에 검사된다. 아래의 문장들을 보자. 아래쪽으로 갈수록, 더 많은 정적 타입 정보가 부가되어 있다. 실행시간에 이들 문장들은 전부 동일하다. 하지만 부가 정보는 컴파일러로 하여금 전달한 인자의 타입이 맞지 않을 경우 경고를 내 보내줄수 있도록 한다. 첫 번째 문장의 경우, 객체는 어떤 타입이라도 될 수 있다. 두 번째 문장에서는 객체는 반드시 aProtocol 프로토콜을 준수해야만한다. 마지막 문장의 경우에는, 해당 객체는 반드시 NSNumber 클래스의 멤버이어야 한다.

- setMyValue:(id) foo;
- setMyValue:(id <aProtocol>) foo;
- setMyValue:(NSNumber*)foo;

동적 타입은 강력한 기능일 수 있다. 1.5이전의 자바처럼 지네릭(generic) 기능이 빠져있는 정적 타입 언어를 가지고 컨테이너 클래스를 구현하게 되면, 프로그래머는 일반적인 타입의 클래스(자바의  경우에는 Object)에 대한 컨테이너를 만든 다음에 나중에 실제 타입으로 이리저리 캐스팅하는 코드를 작성해야만 한다. 하지만 캐스팅을 사용한 코드는 정적 타입의 원칙을 깨버린다 - Integer 객체를 컨테이너에 저장한다음에 읽을때는 String으로 읽으면, 오류가 발생하게 되는 것이다. 이 문제를 경감하는 한가지 방법은 제네릭 프로그래밍 기법에 의존하는 것이다(C++이라면 템플릿 프로그래밍 기법 같은 것). 하지만 그 경우에는 컨테이너 클래스가 타입 중립적으로 작성되어야만 한다. 동적 타입을 지원하는 언어의 경우에는, 그럴 필요는 없다.

전달(Forwarding)

Objective-C가 아무 객체에나 메시지를 보낼 수 있도록 허용하고 있기 때문에, 객체가 메시지를 받았을 경우 할 수 있는 일도 다양하다. 그 중 하나는 메시지를 다른 객체로 전달하는 것이다. 이 기법을 사용해 옵저버 패턴이나 프록시 패턴 같은 디자인 패턴을 간단히 구현할 수 있다.

Objective-C 런타임은 Object 클래스에 몇 가지 메소드들을 정의해놓고 있다.

전달 메소드들:

- (retval_t) forward:(SEL) sel :(arglist_t) args; // with GCC
- (id) forward:(SEL) sel :(marg_list) args; // with NeXT/Apple systems

액션 메소드들:

- (retval_t) performv:(SEL) sel :(arglist_t) args;  // with GCC
- (id) performv:(SEL) sel :(marg_list) args; // with NeXT/Apple systems

메시지를 전달하고 싶은 객체는 전달 메소드를 오버라이드 하여 구현하면 된다. 액션 메소드 peformv::는 오버라이드 할 필요 없는데, 이 메소드는 단순히 셀렉터(selector)와 인자를 가지고 메소드를 실행하는 역할만 하기 때문이다.

예제
다음은 전달(forwarding)의 기본기를 보여주는 예제 프로그램이다.

Forwarder.h
#import <objc/Object.h>
 
@interface Forwarder : Object
{
    id recipient; //The object we want to forward the message to. 
}
 
//Accessor methods
- (id) recipient;
- (id) setRecipient:(id) _recipient; 
 
@end
Forwarder.m
#import "Forwarder.h"
 
@implementation Forwarder
 
- (retval_t) forward: (SEL) sel : (arglist_t) args
{
    /*
     * Check whether the recipient actually responds to the message. 
     * This may or may not be desirable, for example, if a recipient
     * in turn does not respond to the message, it might do forwarding
     * itself.
     */
    if([recipient respondsTo:sel]) 
       return [recipient performv: sel : args];
    else
       return [self error:"Recipient does not respond"];
}
 
- (id) setRecipient: (id) _recipient
{
    recipient = _recipient;
    return self;
}
 
- (id) recipient
{
    return recipient;
}
 @end
Recipient.h
#import <objc/Object.h>
 
// A simple Recipient object.
@interface Recipient : Object
- (id) hello;
@end
Recipient.m
#import "Recipient.h"
 
@implementation Recipient
 
- (id) hello
{
    printf("Recipient says hello!\n");
 
    return self;
}
 
@end
main.m
#import "Forwarder.h"
#import "Recipient.h"
 
int
main(void)
{
    Forwarder *forwarder = [Forwarder new];
    Recipient *recipient = [Recipient new];
 
    [forwarder setRecipient:recipient]; //Set the recipient. 
    /* 
     * Observe forwarder does not respond to a hello message! It will
     * be forwarded. All unrecognized methods will be forwarded to
     * the recipient 
     * (if the recipient responds to them, as written in the Forwarder)
     */
    [forwarder hello]; 
 
    return 0;
}

노트
이 프로그램을 컴파일하면 컴파일러가 다음과 같은 메시지를 보여줄 것이다.

$ gcc -x objective-c -Wno-import Forwarder.m Recipient.m main.m -lobjc
main.m: In function `main':
main.m:12: warning: `Forwarder' does not respond to `hello'
$

Forwarder가 hello 메시지에 응답하지 않는다는 경고 메시지이다. 어떤 환경에서는 저런 경고 메시지를 통해 좀 더 쉽게 오류를 찾아낼 수 있다. 하지만 지금 이 환경에서는 이 경고 메시지는 무시해도 좋다. 전달 기능을 구현했기 때문이다. 그러니 프로그램을 실행해 보면 다음과 같은 메시지를 보게 될 것이다.

$ ./a.out
Recipient says hello!

[다음 글에 계속]
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^