Delphi Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
델파이 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
FreePascal/Lazarus
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
델마당
볼랜드포럼 광고 모집

델파이 강좌/문서
Delphi Programming Tutorial&Documents
[171] 모바일 개발을 위한 델파이 언어 (3)
박지훈.임프 [cbuilder] 3430 읽음    2013-06-06 15:36
델파이 XE4 버전에서는 iOS 및 ARM 컴파일러 지원을 위해, 기존의 델파이와는 다른 새로운 델파이 컴파일러를 도입했습니다. 기존 델파이 컴파일러와의 호환성을 위해 대부분의 문법들은 하위호환되지만 델파이로 모바일 개발을 하기 위해서는 알아두어야 할 주의해야 할 부분들이 상당히 많습니다.

현재 엠바카데로에서 델파이 프로덕트 매니저를 맡고 있는 마르코 칸투는 문서 "The Delphi Language for Mobile Development"에서 이러한 주의점들을 간략히 설명하고 있습니다. 모바일 개발을 생각하는 델파이 개발자들에게는 아주 중요한 문서이므로, 총 4회로 나누어 이 문서를 번역해서 올립니다. 이번 글은 그중 세번째입니다.


3. ARC: 자동 참조 카운트



“긴 문자열”(AnsiString)이 도입된 델파이 2 버전 이후로 델파이에는 참조 카운트에 기반한 메모리 관리 방식이 있었습니다. 이 문서의 앞에서 설명했던 것처럼 문자열은 참조 카운트를 이용하며 모든 참조가 스코프를 벗어나면 메모리에서 제거됩니다. 델파이 3부터는 객체를 가리키기 위해 인터페이스 타입의 변수를 이용하는 경우에 한해 객체에 대해서도 부분적으로 참조 카운트가 지원되게 되었습니다. 가장 마지막에는 동적 배열도 참조 카운트를 이용하게 되었습니다.

따라서 델파이 업계에서 참조 카운트는 새로운 것은 아닙니다만, 델파이 ARM 컴파일러에서는 처음으로 모든 클래스와 객체에 대해 자동 참조 카운트를 완전하게 지원하게 되었습니다. 자세한 내용으로 들어가기 전에 이 주제에 대한 간단한 소개부터 해보겠습니다.

자동 참조 카운트(Automatic Reference Counting; 이하 ARC)란 무엇일까요? 앞서 LLVM에 대한 섹션에서 링크했던 페이지에서 볼 수 있듯이, ARC란 더 이상 필요하지 않는 객체를 명시적으로 해제할 필요 없이 객체의 생존기간을 관리하는 방법들 중의 하나입니다. 객체(예를 들어 지역 변수)에 대한 참조가 스코프 바깥으로 나가면 해당 객체가 자동으로 파괴됩니다. 델파이는 이미 문자열, 그리고 인터페이스 타입 변수로 참조되는 객체에 대해서는 참조 카운트를 지원해왔습니다. 따라서 객체에 대해 말하자면 윈도우 델파이에서 ARC와 가장 가까운 것은 인터페이스입니다. (하지만 ARC는 클래식 델파이에서 인터페이스 타입 변수를 이용해서는 쉽게 해결하기 어려운 순환 참조 같은 이슈를 해결하기 위한 더 많은 유연성을 가지고 있습니다. 잠시 후에 설명하겠습니다)

가비지 컬렉션(GC)와 달리, ARC는 확정적이고, 객체들은 애플리케이션 흐름 내에서 생성되고 파괴되며, 별도의 백그라운드 쓰레드에 의해 일어나는 것이 아닙니다. 이 방식에는 장점과 단점이 모두 있지만, GC와 ARC를 구체적으로 비교하는 것은 이 문서의 범위를 많이 벗어나므로 여기서는 다루지 않습니다.

노트: 어떤 컴파일러에 ARC가 적용되었나? LLVM 기반의 새로운 컴파일러는 기본적으로 ARC를 이용하지만, 클래식 컴파일러 아키텍처에 기반하고 있는 iOS 시뮬레이터에 사용되는 컴파일러도 ARC를 이용한다는 것을 알아두십시오(기술적으로는 인텔 CPU 및 맥 OSX 운영체제를 위한 컴파일러입니다만). 따라서 시뮬레이터와 디바이스의 메모리 관리는 일치합니다.

3.1: ARC 코딩 스타일



새로운 컴파일러가 참조 카운트를 지원하므로, 한 메소드 내에서 임시 객체를 참조할 때 메모리 관리를 완전히 무시해버림으로써 코드를 크게 단순화시킬 수 있습니다.
class procedure TMySimpleClass.CreateOnly;
var 
  MyObj: TMySimpleClass; 
begin 
  MyObj := TMySimpleClass.Create; 
  MyObj.DoSomething; 
end; 

저는 테스트를 위해 TMySimpleClass에 소멸자를 추가하여 폼에 로그를 남기도록 했습니다(제 데모에서는 메소드 자체에서 로그를 남기도록 했습니다만 여기서는 생략했습니다). 프로그램이 end 문을 만날 때, 즉 MyObj 변수가 스코프를 벗어날 때 객체의 소멸자가 호출됩니다.

어떤 이유로든 메소드의 끝에 이르기 전에 해당 객체를 그만 사용하고 싶을 경우에는, 그 변수를 nil로 설정하면 됩니다.
class procedure TMySimpleClass.SetNil; 
var 
  MyObj: TMySimpleClass; 
begin 
  MyObj := TMySimpleClass.Create; 
  MyObj.DoSomething(False); // True이면 예외 발생
  MyObj := nil;
  // 다른 작업들 진행
end;

이 경우, 해당 객체는 메소드의 끝에 이르기 전에 파괴되며, 정확하게 우리가 해당 변수를 nil로 설정한 지점에서 일어납니다. 하지만 try-finally 블럭이 없는 이 코드의 DoSomething 프로시저 내에서 예외가 발생하면 어떻게 될까요? 그런 경우에는 nil을 대입하는 문장을 건너뛰겠지만, 그래도 해당 객체는 메소드가 종료될 때 파괴됩니다.

요약하자면, 참조 카운트 동작은 객체를 변수에 대입할 때와 변수가 스코프를 벗어날 때 일어나며, 이것은 스택 기반의 지역 변수이든 컴파일러에 의해 추가된 임시 변수이든 다른 객체의 필드이든 무관합니다. 파라미터에 대해서도 마찬가지입니다. 객체를 함수에 파라미터로 넘기면 객체의 참조 카운트는 증가되고 함수가 종료되어 리턴되면 감소됩니다.

노트: 파라미터 전달을 최적화하려면 문자열과 마찬가지로 const를 이용하여 파라미터 전달을 최적화할 수 있습니다. 상수로 전달되는 객체는 참조 카운트 오버헤드를 일으키지 않습니다. 윈도우에서 객체 파라미터에 const를 사용하는 것은 아무런 효과가 없으므로 여러분의 코드에서 객체와 문자열 모두 const 파라미터로 넘기도록 바꾸는 것도 좋은 방법입니다. 다만, 참조 카운트의 오버헤드는 매우 적기 때문에 의미 있는 정도의 속도 향상을 기대할 수는 없습니다.

참조 카운트가 지원되는 플랫폼에서는 아래의 새 속성을 이용하여 객체의 참조 카운트를 알아낼 수 있습니다. (임프 역주: TObject 클래스에서 구현되어 있음)
public 
  property RefCount: Integer read FRefCount; 

노트: 인터락 연산의 속도 쓰레드에 안전(thread safe)하기 위해서는, 객체 참조 카운트의 증가와 감소는 인터락(interlock) 혹은 쓰레드 세이프 연산을 이용해서만 이루어져야 합니다. 인텔 CPU가 LOCK 명령의 실행에서 모든 파이프라인/CPU가 지연되어 느려지는 문제가 있었던 적이 있었습니다. 최근의 인텔 CPU들에서는 적절한 캐시 라인만 락이 걸립니다. 이런 상황은 모바일 플랫폼들에서 사용되는 ARM CPU에서도 비슷합니다. 증가 및 감소 연산이 쓰레드에 안전하다는 사실이 이제 객체 인스턴스가 쓰레드에 안전하다는 것을 의미하지는 않습니다. 이것은 단지 모든 쓰레드들이 변경을 즉시 알게 되며 이전 값을 변경하는 일이 없도록 참조 카운트 인스턴스 변수가 적절히 보호된다는 의미일 뿐입니다.

노트: ARC와 컴파일러의 호환성 라이브러리 개발 등의 경우를 위해 ARC가 지원되는 환경과 지원되지 않는 환경 각각에 대해 최선의 코드를 작성하고 싶다면, 두 경우를 식별하기 위해 {$IFDEF AUTOREFCOUNT} 지시어를 사용할 수 있습니다. 이것은 중요한 지시어로서, 새로운 컴파일러를 식별하는 NEXTGEN과는 달리 클래식 델파이 컴파일러에도 ARC가 구현될 미래에 대해서도 대비할 수 있습니다(iOS 시뮬레이터에서도 이미 사용중입니다).

3.2: ARC에서의 Free 및 DisposeOf 메소드



델파이 개발자들은 Free 메소드와 try-finally 블럭으로 감싸는 코딩 패턴을 사용해왔습니다. 대부분의 개발자들이 이와 같은 패턴의 코드를 대단히 많이 갖고 있고, 또한 여전히 윈도우 델파이와 호환되는 코드가 필요할 수 있기 때문에, ARC 환경에서의 Free의 사용에 대해 짚고 넘어갈 필요가 있습니다. 결론부터 말하자면 여러분의 기존 코드는 여전히 동작하지만, 어떻게 동작하는지 이해하기 위해 읽어둘 필요가 있습니다.

예를 들어, 여러분은 지금까지 위에서 봤던 코드를 보통 다음과 같이 작성해왔을 것입니다.
class procedure TMySimpleClass.TryFinally; 
var
  MyObj: TMySimpleClass; 
begin 
  MyObj := TMySimpleClass.Create; 
  try 
    MyObj.DoSomething; 
  finally 
    MyObj.Free; 
  end; 
end; 

클래식 델파이 컴파일러에서는 Free는 TObject의 메소드로서 현재의 참조가 nil이 아닌지 확인하고 그런 경우 Destroy 파괴자를 호출하여 적절한 파괴자 코드를 실행한 후 메모리에서 객체를 제거하는 동작을 합니다.

차세대 컴파일러에서는 그와 달리 Free 호출은 변수에 nil을 대입하는 동작으로 바뀌었습니다. 해당 객체에 대한 마지막 참조일 경우, 이전과 마찬가지로 파괴자를 호출한 후 메모리에서 제거됩니다. 다른 참조가 남아있을 경우 아무 일도 발생하지 않습니다(물론 참조 카운트는 감소됩니다).

비슷하게, 다음과 같은 호출은,
FreeAndNil(MyObj);

객체에 nil을 대입하고, 그 객체를 참조하는 다른 변수가 없을 경우만 객체를 파괴합니다. 여러분이 프로그램의 다른 곳에서 사용되고 있는 객체를 파괴하려 하지는 않을 것이므로 대부분의 경우 이런 동작은 적절합니다. 하지만 다른 참조가 있을 수 있다는 점을 무시하고 즉시 코드를 즉시 실행하려 하는 경우도 있을 수 있습니다(파일이나 데이터베이스 연결을 닫으려 하는 경우에 그럴 수 있겠지요).

다르게 말하자면, 모바일에서 별 의미가 없기는 하지만, Free나 FreeAndNil 호출은 완벽하게 무해하고 기존의 델파이 프로그램의 이들 호출을 그대로 유지하며 모바일 플랫폼으로 옮길 수 있습니다. 하지만 다른 접근 방법이 필요한 경우가 드물게 있을 수 있지요.

강제로 파괴자를 실행할 수 있게 하기 위해(실제 객체를 메모리에서 소멸시키지 않고) 새 컴파일러는 dispose 패턴을 도입했습니다. 다음과 같이 호출하면,
MyObject.DisposeOf;

다른 참조가 남아있는 경우에도 강제로 파괴자 코드가 호출되게 됩니다. 이 시점에서 객체는 특수한 상태가 되어, 추가로 정리(dispose) 작업의 경우나 참조 카운트가 0이 되어 메모리가 실제로 해제되는 때에도 파괴자가 다시 호출되지 않습니다. 이렇게 “정리된(disposed)” 상태(혹은 “좀비” 상태)는 상당히 중요하므로, 여러분은 Disposed 속성을 이용하여 객체의 상태를 알아낼 수 있습니다.

노트: Win32에서의 DisposeOf 이 새 메소드는 윈도우와 맥 컴파일러에도 존재하지만, 이들 컴파일러에서 DisposeOf 메소드는 단지 Free 메소드를 호출하기만 합니다. 다르게 말하자면, 이 새 메소드는 서로 다른 플랫폼들 사이의 소스코드 호환성을 높이기 위해서 도입되었습니다.

왜 dispose 패턴이 필요할까요? 요소들의 집합의 경우나 다른 컴포넌트에 소유된(owned) 컴포넌트를 가정해봅시다. 일반적으로 사용되는 패턴은 아이템 자체의 해제와 집합으로부터의 제거 양쪽 모두를 위해 집합에서 특정 아이템을 “파괴”하는 것입니다. 다른 흔한 상황은 집합이나 컴포넌트 오너를 파괴하고 모든 소유된 요소나 컴포넌트들을 정리(dispose)하는 것입니다. 이런 경우에는, 소유된 객체에 대한 다른 참조가 존재해도 파괴를 시도하고 최소한 파괴자 코드는 실행하게 됩니다.

정리된(disposed) 후에 파괴된 인스턴스를 사용하면 에러가 발생할 수 있지만, 클래식 델파이 컴파일러에서의 동작과 크게 다르지는 않지요. 클래식 컴파일러에서 인스턴스를 해제한 후에 다른 참조를 사용해도 역시 에러가 발생하니까요. 클래식 델파이 컴파일러들에서는 객체에 대한 참조가 2개 있는 상황에서 그중 하나로 해제(free)를 했을 때 다른 참조가 여전히 유효한지 확인할 방법이 없었습니다. 새 컴파일러에서는 Disposed를 사용하면 객체의 상태를 알아낼 수 있습니다.
  myObj := TMyClass.Create; // 첫번째 객체 참조
  myObj.SomeData := 10; 

  myObj2 := myObj; // 동일 객체에 대한 다른 참조

  myobj.DisposeOf; // 클린업을 강제함

  if myobj2.Disposed then // 다른 참조의 상태를 체크
    Button1.Text := 'disposed'; 

앞에서, 클래식 델파이 컴파일러들에서 사용되는 클래식 try-finally 블럭은 새 컴파일러에서도 역시 잘 동작한다고 언급했습니다. 특정한 경우에 다른 참조의 존재와 무관하게 최대한 빨리 파괴자 코드의 실행을 강제하려 할 때는 dispose 패턴을 사용할 수 있습니다(물론 이전 버전의 델파이에서 코드를 재컴파일하지 않을 경우).
var
MyObj: TMySimpleClass; 
begin 
  MyObj := TMySimpleClass.Create; 
  try 
    MyObj.DoSomething; 
  finally 
    MyObj.DisposeOf; 
  end; 
end; 

클래식 컴파일러들에서는 DisposeOf가 Free를 호출하여 기존과 동일하게 동작합니다. ARC 환경에서는 기대한 시점에서(즉 클래식 컴파일러와 동일한 시점에서) 파괴자를 호출하며, 하지만 메모리는 ARC 메커니즘에 의해 관리됩니다. 이런 방식은 멋지지만 동일한 코드를 델파이의 이전 버전들에서 사용할 수는 없게 됩니다. FreeAndNil 프로시저를 재정의하여 버전에 따라 Free 혹은 DisposeOf를 호출하도록 할 수 있습니다. 아니면 표준 Free 호출을 그대로 둬도 대부분의 경우에는 문제가 없습니다.

노트: Disposed 플래그의 저장소 Disposed 플래그의 실제 저장소를 위해서는 추가로 필드를 사용하지 않고 FRefCount 필드를 이용합니다. FRefCount 필드의 두 번째 비트가 파괴 추적 목적과 연관되어 사용되며, 그런 이유로 참조 카운트의 최대값은 이론적으로 2^30으로 제한되지만, 실제로 그 한계에 도달할 일은 현실적으로는 없겠지요.

Free와 DisposeOf의 차이점을 구별하는 한 가지 방법은, ARC 환경에서 두 동작의 목적을 살펴보는 것입니다(클래식 델파이 컴파일러들에서 일어나는 것과 비교하여). Free를 사용하는 목적은 단순히 특정 참조를 인스턴스로부터 “떼어내는” 것입니다. Free는 어떤 정리나 메모리 해제도 내포하지 않습니다. 이것은 단지 코드 블럭에서 그 참조가 더 이상 필요하지 않다는 것을 의미합니다. 이 동작은 보통 스코프를 벗어나면서 일어나지만 Free를 명시적으로 호출하여 호출할 수도 있습니다.

반대로, DisposeOf는 인스턴스에게 “스스로 정리하라”고 명시적으로 지시하는 프로그래머의 수단입니다. DisposeOf는 반드시 메모리 해제를 수반하지 않으며, 단지 명시적으로 인스턴스의 “정리”를 할 뿐입니다(특정 파괴자 코드를 실행). 해당 인스턴스가 최종적으로 사용중인 메모리를 해제하는 것은 일반적인 참조 카운트 방식에 의존합니다.

다른 말로 하자면, ARC 환경에서는 Free는 “인스턴스 참조에 중점을 두는” 동작이며, 반면 DisposeOf는 “인스턴스에 중점을 두는” 동작입니다. Free는 “인스턴스가 어떻게 되든 난 모르겠고, 어쨌든 난 더 이상 그게 필요하지 않아” 라는 의미입니다. DisposeOf는 “이 인스턴스가 메모리 외의 리소스를 잡고 있다면 해제해야 하니 내부적으로 스스로 정리하라”라는 의미입니다. (여기서 말하는 메모리 외의 리소스에는 파일 핸들, 데이터베이스 핸들, 소켓 등이 있겠지요)

DisposeOf가 필요한 다른 경우는, 복잡한 참조 사이클을 위한 적절한 정리 및 해제를 명시적으로 일으키는 것입니다. 다음 섹션에서 설명하는 약한 참조(weak reference)를 사용하면 더 깔끔하고 명시적이 되기는 하지만, 다른 인스턴스들에게 자신들의 참조를 놓도록 알려주기 위한 명시적인 유발 혹은 알림이 필요한 상황이 있을 수 있습니다.

노트: 포인터와 객체를 섞어쓸 때 주의 예를 들어 객체를 포인터에 대입하고, 객체 변수를 다른 다른 객체에 재활용하고, 다음에 포인터를 다시 그 객체 변수에 대입하면, 그 객체는 더 이상 존재하지 않습니다. 참조 카운트가 0이 되어 파괴되었기 때문입니다(포인터는 참조를 카운트를 하지 않으며 참조 카운트를 증가시키지 않기 때문). RTL의 TStringList.ExchangeItems 메소드가 바로 이런 이유로 변경되었습니다. 이 메소드는 이전에는 새 위치로 이동되는 객체에 대한 임시 “참조”를 저장해두기 위해 포인터를 이용했었습니다.

3.3: 약한 참조(Weak Reference)



ARC에서 또 다른 중요한 개념은 약한 참조(weak reference)의 역할입니다. 두 객체가 각각의 속성으로 서로를 참조하고 있고, 외부 변수가 그 중 첫 번째를 참조하고 있는 경우를 가정해봅시다. 첫 번째 객체의 참조 카운트는 2일 것이며(외부 변수 및 두 번째 객체의 필드), 두 번째 객체의 참조 카운트는 1이겠지요(첫 번째 객체의 필드). 이제, 외부 변수가 스코프를 벗어나면, 두 객체의 참조 카운트는 모두 1이 되고 영원히 메모리에 남게 됩니다.

이런 상황을 해결하려면 순환 참조를 깨야 하는데, 이 작업을 언제 실행할지 모르기 때문에 결코 간단하지 않습니다(마지막 외부 참조가 스코프를 벗어날 때 실행해야 하는데, 객체는 이 시점을 알 수가 없습니다). 이런 상황, 그리고 여러 비슷한 경우들에 대한 해법이 약한 참조를 이용하는 것입니다.

약한 참조는 객체의 참조 카운트를 증가시키지 않는 참조입니다. 앞의 경우에서, 두 번째 객체로부터 첫 번째 객체로의 참조가 약한 참조라면, 외부 변수가 스코프를 벗어나면 두 객체 모두 파괴됩니다.

간단한 경우를 아래의 코드로 살펴봅시다.
type 
  TMyComplexClass = class; 

  TMySimpleClass = class
  private
    [Weak] FOwnedBy: TMyComplexClass;
  public
    constructor Create();
    destructor Destroy(); override;
    procedure DoSomething(bRaise: Boolean = False);
  end; 

  TMyComplexClass = class
  private
    fSimple: TMySimpleClass; 
  public 
    constructor Create(); 
    destructor Destroy(); override; 
    class procedure CreateOnly; 
  end; 

아래는 다른 클래스의 객체를 생성하는 TMyComplexClass 클래스의 생성자입니다.
constructor TMyComplexClass.Create; 
begin 
  inherited Create; 
  FSimple := TMySimpleClass.Create; 
  FSimple.FOwnedBy := self; 
end; 

FOwnedBy 필드가 약한 참조이기 때문에 자신이 가리키는 객체(이 경우에는 현재 객체, 즉 self)의 참조 카운트를 증가시키지 않습니다. 이 클래스 구조에 따라 다음과 같이 코드를 작성할 수 있게 되죠.
class procedure TMyComplexClass.CreateOnly;
var
  MyComplex: TMyComplexClass;
begin
  MyComplex := TMyComplexClass.Create;
  MyComplex.fSimple.DoSomething;
end;

약한 참조가 적절히 사용되었으므로 이렇게 하면 메모리 누수(memory leak)가 발생하지 않습니다.

약한 참조를 더 살펴보기 위해 델파이 RTL의 TComponent 클래스의 선언을 봅시다.
type 
  TComponent = class(TPersistent, IInterface,
    IInterfaceComponentReference)
  private
    [Weak] FOwner: TComponent; 

클래식 델파이 컴파일러에서 weak 어트리뷰트는 무시된다는 사실도 알아둡시다. 하지만 소유자 객체의 파괴자에는 소유된 객체도 함께 해제되도록 하는 적절한 코드를 추가하는 것을 잊지 마십시오. 앞에서 봤듯이, Free 호출은 델파이의 클래식 컴파일러와 ARM 컴파일러 모두 허용됩니다(물론 그 효과는 다르긴 합니다). 또한 두 컴파일러들 모두에서 대부분의 상황에서 적절합니다.

약한 참조를 사용할 때는 위의 예제에서 봤듯이 약한 참조 자체가 nil인지 검사해서는 안됩니다. 그러고 싶다면, 먼저 약한 참조를 강한 참조(strong reference)에 대입하고(내부적으로 특정 확인 작업들을 합니다), 다음으로 strong 참조를 확인하는 것입니다. 예를 들어 위에서 약한 참조인 FOwner의 경우에는 다음과 같이 코드를 작성할 수 있습니다.
var 
  TheOwner: TComponent;	// 강한 참조(strong reference) 변수
begin 
  TheOwner := FOwner;
  if TheOwner <> nil then
    TheOwner.ClassName;	// 안전함
end;


3.4 메모리 문제를 검사하려면



iOS 델파이 ARM 컴파일러에서 메모리 관리가 동작하는 방식 때문에, 모든 것을 제대로 컨트롤하고 있다고 확신하기 위해서는 몇가지 옵션들을 검토해볼 가치가 있습니다. 더 나아가기 전에 알아둘 중요한 것 하나. 윈도우가 아닌 플랫폼에서 델파이는 FastMM 메모리 관리자를 사용하지 않으므로, 프로그램 종료시에 메모리 누수 체크를 위해 전역 플래그 ReportMemoryLeaksOnShutdown을 설정하는 것은 무의미합니다. OS X 및 iOS 플랫폼에서 델파이는 libc 네이티브 라이브러리의 malloc 및 free 함수를 직접 호출합니다.

iOS 플랫폼에서 아주 좋은 해결책은 Apple의 Instruments 툴을 이용하는 것인데, 이것은 물리적인 디바이스에서 동작중인 여러분의 애플리케이션의 모든 측면을 감시하는 완전한 추적 시스템입니다. 이 툴에 대해서는 Daniel Magin와 Daniel Wolf는 델파이의 관점에서 이 툴을 자세히 설명하는 동영상이 아래 링크에 있습니다.

http://www.danielmagin.de/blog/index.php/2013/03/apple-instruments-and-delphi-for-ios-movie/

메모리 누수를 일으키는 잠재적인 이슈들 중 하나는 객체들 사이의 순환 참조이며, 여러분의 애플리케이션이 어떻게 동작하는지 알아내도록 도와줄 수 있는 작은 함수가 있습니다. 이것은 Classes 유닛에 있는 CheckForCycles 입니다.
procedure CheckForCycles(const Obj: TObject; const PostFoundCycle: TPostFoundCycleProc); overload;
procedure CheckForCycles(const Intf: IInterface; const PostFoundCycle: TPostFoundCycleProc); overload;

이것은 여러분이 종료 코드에서 보통 사용하던 함수는 아니고, 개발 및 디버깅 동안에 테스트의 목적입니다. 이 프로시저의 두 번째 파라미터는 익명 메소드(anonymous method)로서, 객체의 클래스, 메모리 주소, 사이클 내 객체의 스택을 파라미터로 받습니다. 아래는 그 사용법에 대한 기본적인 예제로, 이전에 설명했던 클래스에서 약한 참조를 없앤 것입니다(약한 참조에서는 사이클이 없습니다).
var 
  MyComplex: TMyComplexClass;
begin 
  MyComplex := TMyComplexClass.Create;
  MyComplex.fSimple.DoSomething;
  CheckForCycles (myComplex, procedure (const ClassName: string; Reference: IntPtr; const Stack: TStack)
begin
  Log('Object ' + IntToHex (Reference, 8) + 
' of class ' + ClassName + ' has a cycle');
end) 


3.5: Unsafe 어트리뷰트



매우 특수한 상황이 있는데, 함수가 참조 카운트가 0으로 설정된 객체를 리턴할 수도 있는 상황입니다(예를 들면 객체의 생성 동안). 이런 경우에는, 컴파일러가 객체를 곧바로 삭제하지 않도록 하기 위해(변수에 대입되어 참조 카운트가 1이 되기 전에), 해당 객체를 “unsafe”로 표시해두어야 합니다. 이것은 코드를 안전하게 하기 위해 참조 카운트를 일시적으로 무시하기 위한 것입니다.

이런 동작은 새로운 특정한 어트리뷰트, [Unsafe]으로 가능하며, 아주 특수한 상황에서만 필요합니다.
var 
  [Unsafe] Obj1: TObject;

[Result: Unsafe] function GetObject: TObject;

특이한 예로, System 유닛에서는 이에 해당하는 지시어 unsafe가 이 어트리뷰트를 대체하는데, 이것은 단지 동일 유닛에서 어트리뷰트가 정의되기 전에는 그 어트리뷰트를 사용할 수 없기 때문입니다. 예를 들면 TObject 클래스의 저수준 InitInstance 클래스 함수가 이런 경우입니다. 이 함수는 객체에 메모리를 할당하기 위해 사용되며 다음과 같이 선언되어 있습니다.
type 
  TObject = class
public 
  constructor Create; 
  procedure Free;
  class function InitInstance(Instance: Pointer):
    TObject {$IFDEF AUTOREFCOUNT} unsafe {$ENDIF};

unsafe 지시어는 위의 예처럼 System 유닛에서만 사용할 수 있도록 제한되어 있습니다.

3.6 저수준 참조 카운트 조작



대부분의 경우 여러분은 여러분의 코드에 참조 카운트를 적용하고, 필요하다면 약한 참조와 unsafe 어트리뷰트를 추가할 수도 있겠지만, 특정한 상황에는 객체를 위한 메모리를 직접 할당하고 자신만의 방식으로 관리해야 할 수도 있습니다. 비슷한 경우로, 참조 카운트의 방법으로는 제대로 관리되지 않고 실질적인 참조가 없는 객체를 메모리에 유지해야 하는 경우가 있을 수도 있습니다. 그런 경우에는(아주 드물지만) TObject 클래스의 두 개의 퍼블릭 가상 메소드를 호출하여 참조 카운트를 강제로 변경할 수 있습니다.
function __ObjAddRef: Integer; virtual;
function __ObjRelease: Integer; virtual;

두 메소드 모두 동작 완료 후의 참조 카운트를 리턴합니다.

노트: 참조 카운트의 속도 위의 두 함수는 델파이에서 참조 카운트 코드의 핵심을 이루며, 필요할 때마다 컴파일러에 의해 자동으로 호출됩니다. 한 객체를 새 변수에 대입하면, 그 오버헤드는 가상 메소드 테이블(virtual method table)의 함수에 대한 단일 호출이며, 그로 인해 그 객체 자체의 필드 값이 증가됩니다. 이것은 Apple의 ObjectiveC의 현재의 ARC 구현을 포함한 다른 구현 방식들에 비해 훨씬 빠릅니다.

이 메소드들을 사용하는 한 가능성은, 여러분이 객체 타입으로 취급 혹은 캐스트하려 하는 메모리 블럭(외부 API로 할당되었을 수도 있음)을 가지고 있을 경우입니다. RTL에서 볼 수 있는 다른 예는, 한 객체의 데이터를 포인터를 이용하여 복사할 때입니다.
class function TInterlocked.CompareExchange(
  var Target: TObject; Value, Comparand: TObject): TObject;
begin 
{$IFDEF AUTOREFCOUNT}
  if Value <> nil then
    Value.__ObjAddRef;
{$ENDIF AUTOREFCOUNT}

  Result := TObject(CompareExchange(Pointer(Target), Pointer(Value), Pointer(Comparand))); 

{$IFDEF AUTOREFCOUNT}
  if (Value <> nil) and
      (Pointer(Result) <> Pointer(Comparand)) then
    Value.__ObjRelease;
{$ENDIF AUTOREFCOUNT}
end; 


3.7 ARC에서 TObject의 생성 및 파괴 메소드들의 정리



아래는 TObject 클래스에서 생성 및 파괴와 관련된 메소드들의 정리이며, 새로운 메소드들과 클래식 메소드들 모두 나열했습니다(조건 컴파일 지시어들은 생략).
type 
  TObject = class
  public 
    constructor Create; 
    procedure Free; 
    procedure DisposeOf; 
    destructor Destroy; virtual; // ARC에서는 protected 섹션에 있음
    property Disposed: Boolean read GetDisposed;
    property RefCount: Integer read FRefCount; // ARC인 경우만 존재
// 저수준 조작용 메소드들
    class function InitInstance(Instance: Pointer): TObject;
    procedure CleanupInstance;
    classfunction NewInstance: TObject; virtual;
    procedure FreeInstance; virtual;
    function ObjAddRef: Integer; virtual;
    function_ObjRelease: Integer; virtual;

서로 다른 플랫폼들에서 참조 카운트의 경우와 아닌 경우 양쪽에 대해 각 메소드의 구현의 자세한 내용을 파고 들어갈 수도 있겠지만, 그것은 이 새 언어 기능 소개에서는 너무 깊이 들어가는 것이겠지요.

3.8 보너스 기능: 클래스에 대한 연산자 오버로드



메모리 관리에서 ARC를 사용하면 아주 재미있는 부작용이 있는데, 컴파일러가 함수에서 리턴된 임시 객체들의 생존기간을 관리할 수 있다는 것입니다. 그런 한 예는 연산자에서 리턴된 임시 객체입니다. 사실, 새로운 델파이 컴파일러에서 완전히 새로운 기능 하나는 클래스를 위한 연산자를 정의할 수 있다는 것입니다. 이것은 델파이 2006 이후로 레코드에서 사용 가능해졌던 것과 동일한 문법과 모델로 가능합니다.

노트: 클래스에 대한 연산자 오버로드를 지원하는 컴파일러는? 이 언어 기능은 iOS ARM 컴파일러에서 동작하지만, Mac용 iOS 시뮬레이터에서도 동작합니다. 물론 이 코드를 클래식 컴파일러로 윈도우나 Mac으로 컴파일할 수는 없습니다. 클래식 컴파일러에 이 기능을 추가하는 것이 그렇게 끔찍하게 어려운 일은 아니지만, 연산자들은 많은 임시 변수들을 만들어내므로 자동화된 메모리 관리 메커니즘(ARC나 가비지 컬렉션) 없이 이 기능을 구현하는 것은 말이 안되는 것입니다.

예를 들어, 다음과 같은 간단한 클래스를 살펴봅시다.
type 
  TNumber = class
  private 
    FValue: Integer; 
    procedure SetValue(const Value: Integer); 
  public 
    property Value: Integer read FValue write SetValue; 
    class operator Add(a, b: TNumber): TNumber; 
    class operator Implicit(n: TNumber): Integer; 
  end; 

class operator TNumber.Add(a, b: TNumber): TNumber; 
begin 
  Result := TNumber.Create; 
  Result.Value := a.Value + b.Value; 
end; 

class operator TNumber.Implicit(n: TNumber): Integer; 
begin 
  Result := n.Value; 
end; 

이 클래스를 다음과 같이 사용할 수 있습니다.
procedure TForm3.Button1Click(Sender: TObject); 
var 
  a, b, c: TNumber; 
begin 
  a := TNumber.Create; 
  a.Value := 10; 

  b := TNumber.Create; 
  b.Value := 20; 

  c := a + b; 

  ShowMessage(IntToStr(c)); 
end;


3.9: 인터페이스와 클래스를 섞어 쓰기



과거에는, 인터페이스 변수와 표준 객체 변수는 서로 다른 메모리 관리 모델을 이용했기 때문에 일반적으로 두 방식을 섞어 쓰지 않도록 권장했었습니다(인터페이스와 객체 변수 혹은 파라미터가 메모리의 동일 객체를 참조하는 등).

ARC를 갖춘 새로운 ARM 컴파일러에서는 객체와 인터페이스 변수의 참조 카운트가 통합되었기 때문에 두 가지를 간단히 섞어 쓸 수 있게 되었습니다. 이렇게 함으로써 델파이 ARC 플랫폼에서 인터페이스는 ARC가 없는 플랫폼에서보다 더욱 강력하고 유연해졌습니다.

+ -

관련 글 리스트
171 모바일 개발을 위한 델파이 언어 (3) 박지훈.임프 3430 2013-06-06
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.