'Flex Builder에서 디버깅&프로파일링 하기'에 해당되는 글 2건

  1. 2014.08.20 [펌] Flex Builder에서 디버깅&프로파일링 하기
  2. 2014.08.20 [펌] Flex Builder에서 디버깅&프로파일링 하기 -1부-
00.Flex,Flash,ActionScript2014. 8. 20. 14:27
반응형
참고 : Adobe Flex3 실전 트레이닝 북, 해외 블로그, 국내 블로그
이 글은 어도비리아에 기술문서로 제출 되었으며, 동일한 내용을 어도비리아에서 볼 수 있습니다.



-2부 프로파일링-
<목차>
1. 프로파일링이란?
2. 가비지 콜렉션
2-1. 레퍼런스 카운팅
2-2. 마크앤스윕
3. Flex Profiling perspective
3-1. 메모리 프로파일링
3-1-1. Profile 뷰
3-1-2. Memory Usage 뷰
3-1-3. Live Objects 뷰
3-1-4. Memory Snapshot 뷰
3-1-5. Object References 뷰
3-1-6. Loitering Objects 뷰
3-1-7. Allocation Trace 뷰
3-1-8. Object Statistics 뷰
3-2. 퍼포먼스 프로파일링
3-2-1. Performance Profile 뷰
3-2-2. Method Statistics 뷰
4. 맺음말


1. 프로파일링 이란?
애플리케이션을 개발하고 실행하게 되면 알 수 없는 원인에 의해 느려지거나 심지어 멈추는 경우가 종종 발생한다. 디버깅만으로는 이런 알 수 없는 원인을 찾아내기란 쉽지 않다. 이런 경우 애플리케이션의 성능측정을 통해 어느 부분에서 어떤 문제가 발생하는지를 찾아낼 수 있다. 성능측정을 전문적인 용어로 프로파일링 이라고 한다.
프로파일링은 디버깅과 더불어 소프트웨어 품질 향상에 도움을 준다. 

우리는 Flex Builder3 Professional(이하 플렉스빌더) 에서 제공하는 프로파일링 기능을 이용할 것이다.
(Flex Builder Standard 버전은 프로파일링 기능을 제공하지 않는다.)
플렉스빌더에서 프로파일링을 하기전 플래시 플레이어의 가비지 콜렉션에 대한 이해가 필요하다.
따라서 먼저 가비지 콜렉션에 대해 알아보겠다.


2. 가비지 콜렉션
가비지 콜렉션이란, 플래시 플레이어의 숨겨진 프로세스인 "가비지 콜렉터"에 의해서 참조 되지 않는 객체들을 메모리에서 해제 하는 것을 말한다. 참조 되지 않는 객체가 있다고 해서 
가비지 콜렉션이 무조건 수행되는건 아니다.가비지 콜렉션이 언제 어느 시점에서 일어나는지 정확하게 알 수는 없지만 짐작할 수 있는 시점은 현재 애플리케이션이 플래시 플레이어가 OS로부터 빌려온 메모리 보다 더 많은 메모리를 요구할 때 이다.

가비지 콜렉터는 객체가 더 이상 참조되지 않는지 확인하기 위해 레퍼런스 카운팅과 마크앤스윕 이라는 두 가지 절차를 따른 후 가비지 콜렉션을 수행한다. 이 두 가지 절차를 이해하는 것은 플렉스 프로파일링 기능을 이용하는데 도움을 줄 것이고 메모리를 효율적으로 이용하는 애플리케이션을 작성하도록 도움을 줄 것이다.

그럼 가비지 콜렉션을 위한 두 가지 절차에 대해 알아보자.


2-1. 레퍼런스 카운팅
레퍼런스 카운팅은 ActionScript 1.0 때 부터 사용되어 왔으며, 객체가 가비지 콜렉션의 대상인지 확인하는 가장 쉽고 빠른 방법이다. 어떤 객체를 생성하고 
참조값(해당 객체의 메모리 주소 값)을 레퍼런스(참조변수)에 할당하면 레퍼런스 카운트가 1 증가한다. 이 객체의 참조값을 또 다른 레퍼런스에 할당하면 레퍼런스 카운트가 1 증가되어 총 2가 된다. 만약 두개의 레퍼런스중 하나의 레퍼런스에 null을 할당하게 되면 레퍼런스 카운트는 1이 감소하며, 남은 하나의 레퍼런스 마저 null을 할당하면 레퍼런스 카운트는 0이 되어 버린다. 가비지 콜렉터는 레퍼런스 카운트가 0인 것들을 가비지 콜렉션의 대상으로 삼는다. 

다음 예제를 보면 쉽게 이해할 수 있을 것이다.



위 예제에서 생성된 Object는 레퍼런스 카운트가 0이기 때문에 가비지 콜렉션의 대상이 된다.
하지만 레퍼런스 카운팅 만으로는 상호참조에 대한 문제를 해결할 수 없다.

상호참조가 무엇인지 다음 예제를 통해 살펴보자.



위 예제에서 생성된 두개의 Object는 obj.foo와 obj2.foo 에서 상호참조 되고 있다.
obj와 obj2에 null을 할당하더라도 이 두개 Object의 레퍼런스 카운트는 결코 0이 될 수 없다.
이런 상황은 매우 빈번하게 일어나며 더욱 복잡해질 수 있다. 플래시 플레이어7 까지는 XML 객체에서 노드들의 상호참조 때문에 이슈가 되기도 했는데, 다행히 플래시 플레이어8에서 마크앤스윕 이라는 새로운 방법이 등장해 이 문제를 해결하고 있다.


2-2. 마크앤스윕
플래시 플레이어(버전 8이상) 는 루트객체(Application 자체 또는 메인MXML) 부터 시작해서 루트객체가 참조하는 객체들과 그 객체들(참조 당하는 객체들)이 참조하는 객체들을 마킹한다. 여기서 마킹의 의미는 객체가 가비지 콜렉션의 대상이 되지 않게 하기 위해 점 찍어두는 것이다. 이 처럼 플래시 플레이어는 애플리케이션의 모든 객체트리를 돌며 참조되고 있는 객체들을 마킹한다. 마킹된 객체들은 레퍼런스 카운트가 무조건 1이상이기 때문에 결코 가비지 컬렉션의 대상이 되지 않는다. 반면 마킹되지 않은 객체들은 가비지 컬렉션의 대상이 된다. 마킹되지 않은 객체는 레퍼런스 카운트가 0일거라고 생각할 수 있지만 그렇지 않다. 플래시 플레이어는 오직 루트객체 까지 연결된 객체만 마킹하므로, 루트객체 까지 연결되지 않은 상호참조 객체들은 마킹되지 않는다. 따라서 상호참조 객체들은 레퍼런스 카운트가 1이상이라도 루트객체에 연결되어 있지 않으면 가비지 콜렉션의 대상이 된다.

다음 그림을 통해 마킹된 객체와 마킹되지 않은 객체들을 파악해보자.



(위 그림은 어도비에서 제공하는 가비지 콜렉터 시뮬레이션이다.http://www.adobe.com/devnet/flashplayer/articles/garbage_collection/example2.html 에서 직접 시뮬레이션 해볼 수 있다.)

위 그림에서 초록색은 루트객체에 연결된 마킹된 객체들이고 빨간색은 상호참조로 인해 레퍼런스 카운트가 각각 1 이지만 루트객체에 연결되지 않았기 때문에 가비지 콜렉션이 수행되면 빨간색 객체들은 메모리에서 제거될 것이다.

지금까지 플래시 플레이어에서 메모리가 어떻게 관리되는지 가비지 콜렉션이 언제 수행되는지 그리고 가비지 콜렉션이 수행될 때 거치는 두가지 과정에 대해 알아봤다. 이러한 밑 바탕이 되는 지식과 함께 본격적으로 플렉스 프로파일링을 시작해 보겠다.


3. Flex Profiling perspective
Flex Profiling perspective 는 플렉스빌더에서 제공하는 프로파일링 관련 도구들을 모아놓은 작업 환경이다.
이 작업 환경에서 실시간으로 애플리케이션이 사용하는 메모리를 비롯해 생성된 객체들의 수를 확인할 수 있는 메모리 프로파일링과, 함수 수행 시간을 확인할 수 있는 퍼포먼스 프로파일링을 할 수 있다.


3-1. 메모리 프로파일링

메모리 프로파일링은 일반적으로 애플리케이션의 메모리가 계속 증가 할 때 이용한다.
메모리가 계속 증가하는 이유는 더 이상 사용되지 않는 객체들이 어디선가 참조되고 있어서 가비지 콜렉터가 제거하지 못하기 때문이다. 이러한 현상을 보다 전문적인 용어로 메모리 누수(memory leak) 라고 부른다.

메모리 프로파일링은 메모리 누수를 찾을수 있을뿐만 아니라, 어디서 어떤 객체가 생성되고 있으며 객체들이 얼마 만큼의 메모리를 차지하고 있는지 알 수 있다. 또한 특정한 두개의 서로 다른 시점의 메모리 상태를 저장하고 이를 비교분석 할 수도 있다. 

먼저 정상적인 애플리케이션을 통해 플렉스빌더의 프로파일링 도구를 살펴보자.
아래의 코드를 프로파일링 모드로 실행한다. (디버그 실행버튼 바로 오른쪽 버튼)



프로파일링 실행버튼을 누르면 다음과 같은 창이 나타난다.



위 창은 프로파일링 옵션을 묻는 창이다. 메모리 프로파일링의 경우 두가지 옵션이 더 존재하는데 Watch live memory data는 실시간으로 현재 활동중인 객체와 메모리 크기를 감지하고 지금까지 생성되었던 객체와 메모리의 크기를 알 수 있는 옵션이며, Generate object allocation stack traces는 특정한 두 시점 사이에서 호출된 메소드와 그 메소드에서 사용했던 메모리 크기를 알 수 있는 옵션이다. 모든 체크박스를 선택하게 될 경우 프로파일링 데이터 수집을 그 만큼 더 많이 하게 되기 때문에 애플리케이션의 속도가 느려진다. 지금은 위 그림처럼 메모리 프로파링일 항목만(하위 두개의 옵션 포함) 체크하고 Resume 버튼으로 프로파일링을 시작해보자.

프로파일링이 시작되면 플렉스빌더는 프로파일링 모드로 전환되고 다음 그림처럼 보일 것이다.



①번 영역은 프로파일링 목록과 프로파일링 실행을 제어할 수 있는 [Profile 뷰] 이다.
②번 영역은 현재 메모리 상황을 그래프로 표현하는 [Memory Usage 뷰] 이다.
③번 영역은 실시간으로 메모리에 존재하는 객체들을 볼 수 있는 [Live Objects 뷰] 이다.

그럼 먼저 프로파일링을 시작했을 때 기본적으로 보여지는 위 세가지 뷰에 대해 자세히 알아보겠다.


3-1-1. Profile 뷰
프로파일링이 시작된 이후에, 프로파일링을 일시중지 하고 다시 시작 하는 등 실행을 제어 하거나, 메모리와 퍼포먼스 분석을 위한 스냅샷 저장을 할 때 [Profile 뷰] 이용한다. [Profile 뷰] 는 프로파일링 실행을 제어할 수 있는 컨트롤 부분과 프로파일링이 진행중 이거나 이미 중단된 애플리케이션과 스냅샷의 목록이 나타나는 부분이 있다. 애플리케이션 목록 중 하나를 선택하고 수행하길 원하는 버튼을 클릭함으로써 프로파일링을 제어하게 된다. 그럼 실행을 제어하는 컨트롤 부분의 버튼들이 어떤 기능을 하는지 알아 보도록 하겠다.

 Resume
일시 정지된 애플리케이션과 함께 프로파일링을 다시 계속한다.

 Suspend
애플리케이션과 함께 프로파일링을 일시 정지한다.

 Terminate
프로파일링을 종료 한다. 애플리케이션(브라우저)는 종료되지 않는다.

 Run Garbage Collector
강제적으로 가비지 콜렉션 수행한다. 이 버튼을 누르면 [Memory Usage 뷰] 에서 가비지 콜렉션이 수행된 것을 알 수 있다.

 Take Memory Snapshot
현재 실행중인 애플리케이션의 메모리 상황을 저장하고, [Profile 뷰]의 선택된 애플리케이션 하위로 메모리 스냅샷 항목이 추가된다. 이 버튼을 누르면 가장 먼저 가비지 콜렉션을 수행한 뒤 메모리 상황을 저장한다. 가비지 콜렉션이 먼저 수행되는 이유는 가비지 객체들이 메모리 상황을 분석하는데 있어서 잘못된 정보로 활용되기 때문이다. 메모리 스냅샷은 1개 이상 저장할 수 있고, 저장된 스냅샷 목록중 하나를 더블클릭 했을 때 열리는 [Memory Snapshot 뷰] 를 이용해 그 당시 메모리 상황을 분석할 수 있다. 또한 저장된 스냅샷 목록 중 두개의 스냅샷을 한 쌍으로하여 앞으로 나오게 될 두 시점 사이의 비교 기능을 이용할 수 있다. 자세한 내용은 
3-1-4. Memory Snapshot 뷰, 3-1-6. Loitering Objects 뷰, 3-1-7. Allocation Trace 뷰 이 세 파트에 걸쳐서 다루겠다.

 Find Loitering Objects
이 버튼은 메모리 스냅샷 목록 중 두개를 선택 했을 때 작동 된다. 이 버튼은 선택된 두 메모리 스냅샷을 비교하여 두 시점 사이에서 생성된 객체들을 확인할 수 있는 [Loitering Objects 뷰]를 연다. 자세한 내용은 
3-1-6. 
Loitering Objects 뷰
 파트에서 다루겠다.

 View Allocation Trace
이 버튼은 프로파일링을 시작할 대 실행 옵션을 묻는 창에서 Generate object allocation stack traces 항목을 체크하고, 메모리 스냅샷 목록 중 두개를 선택 했을 때 작동 된다. 이 버튼은 선택된 두 메모리 스냅샷을 비교하여 두 시점 사이에서 수행된 메소드들을 확인할 수 있는 [Allocation Trace 뷰]를 연다. 자세한 내용   3-1-7. Allocation Trace 뷰 파트에서 다루겠다.

 Reset Performance Data
현재까지 플렉스빌더가 수집한 퍼포먼스 데이터를 초기화 한다. 플렉스빌더는 이 버튼이 눌린 시점으로 하여 퍼포먼스 데이터를 새로 수집하게 된다.

 Capture Performance Profile
프로파일링이 시작된 이후 또는 퍼포먼스 데이터가 초기화된 시점부터 현재까지 수집한 퍼포먼스 데이터를 저장하고, [Profile 뷰]의 선택된 애플리케이션 하위로 퍼포먼스 스냅샷 항목을 추가한다. 추가된 퍼포먼스 스냅샷 항목을 더블클릭 하면 성능에 관련된 정보를 보여주는 [Performance Profile 뷰]가 열린다. 자세한 내용은 3-2. 퍼포먼스 프로파일링 파트에서 다루겠다.

 Delete
선택된 스냅샷 항목 또는 프로파일링이 종료되고 선택된 애플리케이션을 목록에서 제거한다.


3-1-2. Memory Usage 뷰
만약 여러분의 애플리케이션에서 메모리가 정상적인지 확인하고 싶다면 가장 먼저 [Memory Usage 뷰]를 확인하라. [Memory Usage 뷰]를 통해 플래시 플레이어의 메모리 상황을 한눈에 파악할 수 있기 때문에 메모리 누수가 발생하는지 가장 쉽고 빠르게 알 수 있는 도구이다.



위 그림에서 빨간색 라인은 애플리케이션이 시작된 이후로 가장 많은 메모리를 사용했던 양을 나타내고, 파란색 라인은 현재 사용중인 메모리 양을 나타낸다. 위 그래프는 매우 정상적인 흐름의 톱니 패턴을 띄고 있다. 이 톱니 패턴은 앞서 설명 했듯이 애플리케이션이 플래시 플레이어가 OS로 부터 빌려온 메모리 보다 더 많은 메모리를 요구할 때 가비지 콜렉션이 수행된 흔적이다.


3-1-3. Live Objects 뷰
애플리케이션에 존재하는 객체들을 실시간으로 확인하고 싶을 때 [Live Objects 뷰]를 이용한다. 이 뷰는 프로파일링이 진행되는 동안 애플리케이션에서 실시간으로 생성되는 객체와 그 객체가 얼마나 많은 메모리를 사용하는지 보여준다. 또한, 그것들에 대한 누적된 수치도 함께 보여준다.



위 그림에서 알 수 있듯이 [Live Objects 뷰]에는 6개의 컬럼이 존재한다. 이 중 Class, Package 컬럼은 궂이 설명하지 않아도 어떤 컬럼인지 알 수 있을 것이다. 그 두개를 제외한 나머지 컬럼들에 대해 알아보자.

Cumulative Instances
프로파일링이 시작된 이후로 생성된 객체의 누적 합계이다.

Instances
현재 메모리에 존재하는 객체의 수이다.

Cumulative Memory
프로파일링이 시작된 이후로 생성된 객체가 사용했던 메모리 양의 누적 합계이다.

Memory
현재 메모리에 존재하는 객체가 사용중인 메모리 양이다.


위 그림에서 처럼 Package 컬럼 값이 없거나 그 외에 생소한 값을 가진 항목들은 우리가 만든 애플리케이션 단계의 정보가 아니기 때문에 그다지 유용한 정보가 되지 못한다. 지금 우리에게 필요한 정보는 예제 코드에서 1초마다 100개씩 생성되고 있는 DataGrid 객체에 대한 정보일 것이다. 하지만 기본적으로 적용된 필터 때문에 우리에게 필요한 DataGrid 객체에 대한 정보를 볼 수 없기 때문에 필터를 수정해야 한다. 

먼저 위 그림의 우측 상단의 빨간색 마크된 부분의 세번째 아이콘을 누르면 Filters 창이 나타나는데, Inclusion filters와 Exclustion filters를 다음과 같이 변경하자. 그러면 필터의 포함 항목과 제외 항목을 통해 적절하게 필터링된 객체들이 보여질 것이다.



DataGrid 클래스의 패키지인 mx.controls.* 패키지를 포함 시켰다면 다음과 같이 [Live Objects 뷰]에 DataGrid 하나만 나타날 것이다.



위 그림에서 처럼 DataGrid 객체가 지금까지 어마어마하게 생성 되었으며, 현재 메모리에서 존재하는 객체의 수를 비롯해 사용중인 메모리 양도 쉽게 알 수 있다. 이처럼 [Live Objects 뷰]를 통해 실시간으로 생성되거나 소멸되는 객체들 뿐만 아니라, 누적된 수치까지 확인함으로써 애플리케이션의 전반적인 메모리 상황을 알 수 있고, 이러한 정보는 여러분의 애플리케이션을 깊게 이해하는데 도움이 될 것이다.


※코드 변경하기
지금까지 프로파일링이 시작되었을 때 구성되는 기본적인 뷰들에 대해 알아봤다. 이제 예제 코드를 변경한 후 [Memory Usage 뷰]를 통해 메모리 누수를 확인할 것이다. 그리고 앞으로 나오게 될 [Memory Snapshot 뷰] 와 [Loitering Objects 뷰] 그리고 [Allocation Trace 뷰]를 이용해 어디에서 메모리 누수가 발생하는지 확인할 것이다. 변경된 코드는 다음과 같다.


코드를 변경했다면 프로파일링을 시작하고 [Memory Usage 뷰]를 관찰해 보자. 다음 그림처럼 메모리가 계속 상승하는 그래프를 볼 수 있을 것이다. 이렇게 메모리가 계속 상승하는 이유는 가비지 콜렉션이 수행될 때 메모리에서 제거 되어야할 객체들이 어디선가 계속 참조되고 있기 때문이다.



변경된 예제코드에서 직접 메모리 누수 부분을 찾을 수 있지만 우리는 그 사실을 모른다고 가정하고 메모리 프로파일링을 통해 어디에서 메모리 누수가 발생하는데 찾아볼 것이다.

3-1-4. Memory Snapshot 뷰
여러분이 과거 특정한 시점에 저장했던 메모리 상황을 알고 싶을 때 [Memory Snapshot 뷰]를 이용한다. 이 뷰는 특정한 시점의 메모리 상황을 보여 주는 것이기 때문에 [Live Objects 뷰] 처럼 메모리 상황을 업데이트 하지 않고, 오직 해당 시점의 메모리 상황만 보여주는 정적인 뷰이다. [Profile 뷰]에서 저장된 메모리 스냅샷 항목을 더블클릭 하면 이 뷰를 열 수 있다.

이제 위 예제코드의 문제점을 찾아보자. [Memory Usage 뷰] 에서 메모리 누수를 포착 했다면 가장 먼저 [Live Objects 뷰]를 통해서 어떤 객체가 메모리에서 계속 증가하는지 발견해야 한다. 여러분은 이미 [Live Objects 뷰]에 대한 사용법을 알고 있고, 직접 확인해보면 DataGrid 객체가 계속 증가하는 것을 확인할 수 있을 것이다. 확인 했다면 Take Memory Snapshot 버튼으로 메모리 스냅샷을 만들고, 추가된 메모리 스냅샷을 더블클릭 하여 [Memory Snapshot 뷰]를 열어보자. [Memory Snapshot 뷰]를 열었다면 다음 그림처럼 DataGrid 객체만 나오도록 앞서 배운 필터를 설정해보자.



[Memory Snapshot 뷰]에서 제공하는 컬럼들은 앞서 배운 [Live Objects 뷰]에 이미 있는 것들이다. 우리는 현재까지 [Memory Usage 뷰]를 통해 메모리 누수를 발견하고 [Live Objects 뷰]를 통해 DataGrid 가 계속 생성되는 것을 확인했다. 이제는 그 문제의 DataGrid 객체가 과연 어디에서 생성되는지 알아야할 때 이다. 위 그림에서 알 수 있듯이 [Memory Snapshot 뷰] 자체만으로는 DataGrid가 어디서 생성되는지 알 수가 없다. 하지만 이 뷰에서 특정 로우를 더블클릭 하거나, 특정 로우를 선택한 상태에서 우측상단 빨간색 박스 부분의 첫번째 아이콘을 누르면 객체가 어디에서 생성되었는지 알려주는 [Object References 뷰]를 열 수 있다. 자세한 내용은 바로 다음 3-1-5. Object References 뷰 파트에서 다룬다.

3-1-5. Object References 뷰
메모리 프로파일링을 하는 동안 객체가 생성된 위치를 확인 하고자 할 때 [Object References 뷰]를 이용한다. 이 뷰는 객체를 생성시킨 메소드를 보여주며 해당 메소드의 소스코드 위치까지 알려준다.

우리는 이 뷰를 [Memory Snapshot]뷰를 통해 연다는 것을 바로 이전 파트에서 배웠다. 이제 문제의 DataGrid 객체가 어디에서 생성되는지 확인하기 위해 이 뷰를 열어보자.



이 뷰는 객체의 리스트를 보여주는 [Instance] 영역과 해당 객체가 생성된 위치를 보여주는 [Allocation Trace] 영역으로 나뉘어져 있다. 위 그림에서 알 수 있듯이 좌측 [Instance] 영역에서 무수히 많이 생성된 DataGrid 객체를 선택하면 우측 [Allocation Trace]에서 그 객체를 생성한 메소드와 소스 위치까지 파악할 수 있다. 이러한 정보를 이용해 잘못된 애플리케이션을 즉시 수정할 수 있을 것이다.


3-1-6. Loitering Objects 뷰
과거 특정한 시점에 메모리 스냅샷 두개를 저장했고 그 두 시점 사이에서 생성 되었던 객체들을 확인하고 싶을 때 [Loitering Objects 뷰]를 이용한다. 이 뷰는 두 시점 사이에서 생성된 객체와, 그 객체가 사용했던 메모리의 총 합계를 보여준다.

이제 이 뷰를 활용하기 위해 프로파일링을 다시 시작하고, 시작 하자마자 메모리 스냅샷을 저장하고 약 10초뒤 다시 한번 메모리 스냅샷을 저장 해보자. 그리고 [Profile 뷰]에서 두 개의 메모리 스냅샷을 선택하고 Find Loitering Objects 버튼을 누르면 다음 그림처럼 [Loitering Objects 뷰]가 나타날 것이다.



위 그림에서 짧은 시간에 DataGrid 객체가 900개나 생성되고 전체 메모리에서 62%에 해당하는 메모리를 사용하고 있음을 알 수 있다. 이 처럼 이 뷰는 메모리 누수를 발견 하는데 유용하며 [Memory Sanapshot 뷰]와 마찬가지로 특정 객체를 더블클릭 하거나 우측 상단의 빨간색으로 마킹된 아이콘을 누르면 해당 객체가 생성된 위치를 보여주는 [Object References 뷰]를 열 수 있다.


3-1-7. Allocation Trace 뷰
[Allocation Trace 뷰]는 바로 이전 파트에서 배웠던 [Loitering Objects 뷰]와 비슷한데 특정한 두 시점 사이의 메모리 상황을 분석할 때, 호출된 메소드에 대한 정보를 알고 싶을 때 이용한다. 앞서 말했듯이 [Loitering Objects 뷰]가 생성 되었던 객체를 보여줬다면, 이 뷰는 호출된 메소드와 그 메소드가 호출되는 동안 생성한 객체의 수를 비롯하여, 사용했던 메모리 양을 보여준다. 

이제 [Profile 뷰]에서 3-1-6 Loitering Object 뷰 파트에서 저장했던 두 개의 메모리 스냅샷을 선택하고, View Allocation Trace 버튼을 누르면 다음 그림처럼 [Allocation Trace 뷰]가 나타날 것이다.



위 그림과 같이 [Allocation Trace 뷰]에는 6개의 컬럼이 존재한다. 이 중 Method, Package 컬럼은 궂이 설명하지 않아도 어떤 컬럼인지 알 수 있을 것이다. 그 두개를 제외한 나머지 컬럼들에 대해 알아보자.

Cumulative Instances
두 시점 사이에서 이 메소드에 의해서 호출된 메소드와 이 메소드 자체에서 생성된 객체의 수이다.

Self Instances
두 시점 사이에서 이 메소드 자체에서 생성된 객체의 수이다.

Cumulative Memory
두 시점 사이에서 이 메소드에 의해서 호출된 메소드와 이 메소드 자체에서 생성된 객체가 사용한 메모리 양이다.

Self Memory
두 시점 사이에서 이 메소드 자체에서 생성된 객체가 사용한 메모리 양이다.


[Allocation Trace 뷰]를 통해서 짧은 시간동안 timerHandler 라는 메소드에서 많은 객체를 생성하고 많은 메모리를 사용한다는 것을 알 수 있다. 위 그림에서 처럼 timerHandler 라는 메소드가 과연 어떠한 객체를 생성했는지는 알 수 없다. 그러나 해당 메소드를 더블클릭 하거나 우측 상단의 빨간색 마킹된 버튼을 누르면 어떤 객체가 생성 됐는지 보여주는 [Object Statistics 뷰]를 열 수 있다. 바로 다음 파트에서 이 뷰에 대해 자세히 알아보겠다.

3-1-8. Object Statistics 뷰
과거 특정한 두 시점 사이에서 호출 되었던 메소드에 대한 메모리 통계정보를 알고 싶을 때 [Object Statistics 뷰]를 이용한다. 이 뷰는 해당 메소드가 수행 되는 동안 생성한 객체가 무엇인지, 그 객체가 얼마나 많은 메모리를 사용했는지 보여준다. 

이 뷰는 바로 이전 파트에서 [Allocation Trace 뷰]를 통해 연다는 것을 배웠다. 이제 특정 메소드의 통계 정보를 보기 위해 이 뷰를 열어보자.



위 그림처럼 이 뷰는 크게 3가지 영역으로 구분할 수 있다.
번 영역은 호출된 메소드에 대한 요약된 정보로서 [Allocation Trace 뷰]에서 보여지던 정보와 동일하다.
번 영역은 이 메소드 자체에서 생성된 객체와 그 객체가 사용하는 메모리 양을 보여준다.
번 영역은 이 메소드에 의해 호출된 다른 메소드에서 생성던 객체와 그 객체가 사용하는 메모리 양을 보여준다.
이 처럼 [Object Statistics 뷰]는 이제껏 봐왔던 뷰들에 대한 전체적인 정보를 보여주는 통계뷰 라고 할 수 있다. 이러한 통계정보를 바탕으로 보다 종합적인 메모리 분석을 할 수 있을 것이다.

우리는 지금 까지 플렉스빌더에서 메모리 프로파일링을 통해 메모리 누수 감지와 원인 발견 및 애플리케이션 전체적인 메모리 상황에 대한 정보를 알 수 있었다. 이런 정보를 안다면 애플리케이션 설계에 있어서도 도움이 될 것이다. 이제 메모리 상황이 아닌 성능에 초점을 둔 퍼포먼스 프로파일링에 대해 알아 보도록 하자.


3-2. 퍼포먼스 프로파일링
퍼포먼스 프로파일링은 애플리케이션에서 응답이 느린 메소드를 찾아내거나, 성능이 향상될 수 있는 메소드를 찾아 내고자 할 때 이용한다. 퍼포먼스 프로파일링은 이 두가지 타입의 메소드를 최적화 하기 위한 리팩토링 정보를 제공하여 애플리케이션의 전체적인 성능을 향상 시킬 수 있다.

우리는 잘못된 예제코드를 이용하여 수행시간 가장 긴 메소드와 빈번히 호출되는 메소드를 찾아낼 것이다. 예제코드는 다음과 같다.



위 예제코드를 다 적었다면 프로파일링을 시작해보자. 이번에는 퍼포먼스 프로파일링만 하기 때문에 실행 옵션을 묻는 창에서 가장 아래 있는 Enable performance profiling 항목에만 체크를 한다. 그리고 나서 애플리케이션이 시작되면 화면에 보이는 버튼을 10회 누른다. 플렉스빌더는 프로파일링이 시작된 이후로 성능 데이터를 수집 하는데, 성능 데이터는 메소드 호출시간과 횟수이다. 따라서 여러분이 버튼을 10회 눌렀다면 버튼에 의해 실행된 메소드 정보가 수집 되었을 것이다. 이제 플렉스빌더로 돌아가서 [Profile 뷰]에 있는 
Capture Performance Profile 아이콘을 누르면, 다음 그림처럼 플렉스빌더가 아이콘을 누른 시점까지 수집했던 성능 데이터를 저장하게 된다.



퍼포먼스 데이터가 저장되면 위 그림처럼 [Profile 뷰]에 목록으로 추가된다. 또한 하나 이상의 퍼포먼스 데이터를 저장할 수 있으며, 플렉스빌더가 수집한 퍼포먼스 데이터를 초기화 하고 싶다면 
Reset Performance Data 아이콘을 누르면 된다. 저장된 목록중 하나를 더블클릭하면 퍼포먼스 데이터를 볼 수 있는 [Performance Profile 뷰]가 열린다. 다음 파트에서 [Performance Profile 뷰]를 통해 수집된 퍼포먼스 데이터를 확인해 보도록 하겠다.

3-2-1. Performance Profile 뷰
[Performance Profile 뷰]는 퍼포먼스 프로파일링을 하는 동안 사용하는 가장 중요한 뷰 이다. 이 뷰는 메소드 호출에 관련된 통계를 보여주며, 이러한 정보는 애플리케이션의 병목현상을 개선하거나 수행속도를 높이는 정보로 이용될 수 있다.

[Profile 뷰]에서 저장된 퍼포먼스 데이터 항목을 더블클릭 하면 다음 그림처럼 [Performance Profile 뷰]가 열릴 것이다.


위 그림과 같이 [Performance Profile 뷰]에는 6개의 컬럼이 존재한다. 이 중 Method, Package 컬럼은 궂이 설명하지 않아도 어떤 컬럼인지 알 수 있을 것이다. 그 두개를 제외한 나머지 컬럼들에 대해 알아보자.

Calls
퍼포먼스 데이터가 저장된 시점까지 메소드가 호출된 횟수이다.

Cumulative Time

이 메소드에 의해서 호출된 메소드들의 총 수행 시간이다.

Self Time
이 메소드 자체의 수행 시간이다.

AVG. Cumulative Time
이 메소드에 의해서 호출된 메소드들의 평균 수행 시간이다.

AVG. Self Time
이 메소드 자체의 평균 수행 시간이다.


위 그림은 Cumulative Time 컬럼을 눌러서 내림차순 정렬한 것 이다. 정렬을 통해서 가장 응답이 느린 메소드를 확인해 본 결과, 자체적인 수행시간이 가장 느린 메소드는 slow() 임을 확인할 수 있다. 이제 여러분이 직접Calls 컬럼으로 정렬을 하면 가장 많이 호출된 메소드는 fast() 임을 확인할 수 있을 것이다. 이제 여러분은 메소드의 수행 시간을 알게 되었으므로 리팩토링을 통해 성능 향상을 꽤할 수 있을 것이다.

또한 이 뷰에서 특정 메소드 항목을 더블클릭 하거나 위 그림의 우측상단 빨간색 마크된 아이콘을 누르면 해당 메소드의 좀 더 진보된 정보를 볼 수 있는 [Method Statistics 뷰]를 열 수 있다. 다음 파트에서 [Method Statistics 뷰]에 대해 자세히 알아 보겠다.

3-2-2. Method Statistics 뷰
퍼포먼스 프로파일링 하는동안 호출된 메소드에 대한 성능 통계정보를 알고 싶을 때 [Method Statistics 뷰]를 이용한다. 이 뷰는 호출된 메소드에 대한 수행시간과 더불어 메소드 내에서 호출된 다른 메소드나, 자신이 어디서 호출되었는지 정보를 제공한다. 

이제 
[Performance Profile 뷰]에서 Cumulative Time 이 가장 오래 걸린 execute() 항목을 더블클릭해서 이 뷰를 열어보자.



이 뷰는 크게 3가지 영역으로 구분할 수 있다.
번 영역은 이 메소드 자체 수행시간으로써 [Performance Profile 뷰]에서 보여지던 정보와 동일하다.
번 영역은 이 메소드를 호출한 메소드에 대한 수행 시간을 보여준다.
번 영역은 이 메소드에 의해 호출된 다른 메소드들의 수행 시간을 보여준다.
이 처럼 [Method Statistics 뷰]는 수행시간과 더불어 메소드들의 흐름과 같은 최적화 하기 유용한 정보를 제공한다. 애플리케이션의 성능과 흐름을 파악하는 것 만으로도 앞으로 여러분이 무엇을 해야할지 알게 될 것이다.


4. 맺음말
지금까지 프로파일링에 대해서 알아봤다. 이 글에서 소개한 방법으로 메모리와 퍼포먼스를 측정하여 여러분의 애플리케이션의 성능을 향상시켜 보도록 하자. 끝으로 프로파일링 또한 디버깅과 마찬가지로 개발이 끝난 후가 아니라 개발 과정의 일부분으로 행해 진다면 좋은 품질의 애플리케이션을 만들 수 있을 것이다.


출처 : http://jjaeko.tistory.com/104

Posted by 1010
00.Flex,Flash,ActionScript2014. 8. 20. 14:26
반응형
참조 : Adobe Flex3 실전 트레이닝 북, 국내 블로그, 해외 블로그
이 글은 어도비리아에 기술문서로 제출 되었으며, 동일한 내용을 어도비리아에서 볼 수 있습니다.



-1부 디버깅-
<목차>
1. 디버깅이란?
2. 디버깅의 종류
2-1. Trace()
2-2. <mx:TraceTarget />
2-3. Flex Debugging perspective
2-3-1. Debug view
2-3-2. Variables view
2-3-3. Expressions view
2-3-4. Breakpoints view
2-4. 
flash.debugger.enterDebugger()
3. 맺음말


1. 디버깅이란?
프로그래머가 의도하지 않은 소프트웨어의 오작동을 버그라고 한다.
디버깅은 이러한 버그를 찾아내고 수정하는 것을 말하며,
소프트웨어의 품질을 향상 시키기위해 반드시 거쳐야 할 과정이다.

이 글에서 여러분은 Flex Builder에서 효과적으로 디버깅 하는 방법을 배울 것이며,
더 나아가 개발 습관에도 좋은 영향을 줄 것이다.


2. 디버깅의 종류
Flex에서 할 수 있는 디버깅은 크게 3가지가 있다.

- Trace()
- <mx:TraceTarget />
- Flex Debugging perspective

이 3가지 방법 모두 "특정한 시점에 객체가 가지고 있는 값" 을 확인하는 것에 초점을 둔다.
그럼으로써 프로그램이 의도한대로 동작하는지 알게 될 것이다.
그리고 이 중에서 Trace()와 <mx:TraceTarget /> 태그는 디버깅 보다는 로깅에 가깝다.
하지만 "특정한 시점에 객체가 가지고 있는 값" 을 확인하는 용도로써 훌륭한 디버깅 도구가 될 수 있다.

그럼 이제부터 이 3가지 종류에 대해 알아보도록 하겠다.

2-1. Trace()

프로그램의 특정 기능이 수행되는 동안 객체들이 의도한 값을 가지고 있는지 확인하고 싶을때 
이 함수를 이용한다. 이 함수는 객체의 값을 콘솔에 출력해주는 역할을 한다.
프로그램의 흐름속에 Trace() 함수를 이용해 값을 확인 함으로써 프로그램이 정상적으로 작동 되는지 
확인할 수 있을 것이다.

간단한 예제를 통해 사용법을 알아 보겠다.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
    creationComplete="creationComp()">
    <mx:Script>
        <![CDATA[
            import mx.utils.ObjectUtil;
            private function creationComp():void {
                var obj:Object = new Object();
                obj.a = "a";
                obj.b = "b";
                
                trace(ObjectUtil.toString(obj));
            }
        ]]>
    </mx:Script>
</mx:Application>


위 코드를 디버그 모드로 실행(F11 키 또는 벌레모양 아이콘
) 하면 콘솔에서 다음과 같은 결과를 확인할 수 있다.

(Object)#0
  a = "a"
  b = "b"


위 예제에서 사용한 ObjectUtil 클래스는 Trace() 함수와 함께 사용하면 매우 유용하다.
ObjectUtil.toString() 메서드는 객체가 가지고 있는 프로퍼티와 값을 출력결과 처럼 포맷해서 리턴해준다.

2-2. <mx:TraceTarget />
Flex 애플리케이션을 개발 하는데 있어 어려운 부분 중 하나는 서버와 통신하는 경우이다.
어떤 원인에 의해 서버로부터 기대했던 데이터를 받지 못할 경우 원인을 찾아내기가 힘들다.
이런 경우 <mx:TraceTarget /> 태그를 이용하면 원인을 쉽게 파악할 수 있다.
이 태그는 서버와 통신을 할 때 통신상태 및 전송되는 데이터를 콘솔에 출력해주는 역할을 한다.
단지 태그를 MXML 문서에 추가하기만 하면 콘솔에서 그 내용을 확인할 수 있다.

간단한 예제를 통해 사용법을 알아 보겠다.

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml">    

    <mx:TraceTarget />
    
    <mx:HTTPService id="http" 
                               url="http://www.naver.com" 
                               resultFormat="text" />
                    
    <mx:Button  label="click me"
                      click="http.send()" />
                                
</mx:Application>


위 코드를 디버그 모드로 실행(F11 키 또는 벌레모양 아이콘) 하면 콘솔에서 다음과 같은 결과를 확인할 수 있다.

mx.messaging.Producer '1D8D6911-2BBB-B851-5B2B-A2BA93C8F10A' producer set destination to 'DefaultHTTP'.
mx.messaging.Channel 'direct_http_channel' channel endpoint set to http:
mx.messaging.Producer '1D8D6911-2BBB-B851-5B2B-A2BA93C8F10A' producer sending message '54738616-39F6-E6DA-940C-A2BA98DFACB2'
mx.messaging.Channel 'direct_http_channel' channel sending message:
(mx.messaging.messages::HTTPRequestMessage)#0
  body = (Object)#1
  clientId = (null)
  contentType = "application/x-www-form-urlencoded"
  destination = "DefaultHTTP"
  headers = (Object)#2
  httpHeaders = (Object)#3
  messageId = "54738616-39F6-E6DA-940C-A2BA98DFACB2"
  method = "GET"
  recordHeaders = false
  timestamp = 0
  timeToLive = 0
  url = "http://www.naver.com"
mx.messaging.Producer '1D8D6911-2BBB-B851-5B2B-A2BA93C8F10A' producer connected.
mx.messaging.Producer '1D8D6911-2BBB-B851-5B2B-A2BA93C8F10A' producer acknowledge of '54738616-39F6-E6DA-940C-A2BA98DFACB2'.
mx.rpc.http.HTTPService Decoding HTTPService response
mx.rpc.http.HTTPService Processing HTTPService response message:
(mx.messaging.messages::AcknowledgeMessage)#0
  body = "결과 값 생략"
  clientId = "DirectHTTPChannel0"
  correlationId = "54738616-39F6-E6DA-940C-A2BA98DFACB2"
  destination = ""
  headers = (Object)#1
    DSStatusCode = 200
  messageId = "EC0ED47D-ABD2-8671-79ED-A2BA9950521B"
  timestamp = 0
  timeToLive = 0


위 결과에서 알 수 있듯이 전송상태 및 전송 데이터("결과 값 생략" 부분)를 쉽게 확인할 수 있다.
또한 이 태그는 디버깅 보다는 로깅과 매우 밀접한 연관이 있다.
하지만 이 글에서는 오직 디버깅의 용도로만 사용한 것이며 로깅에 관한 부분은 다루지 않는다.


2-3. Flex Debugging perspective
Flex Builder(Eclipse Platform, 이하 빌더) 에서 지원하는 디버깅으로써 이 글의 핵심이라고 할 수 있다.
개발자가 원하는 "특정한 시점"에 객체들의 상세한 값을 확인하고자 할 때 이 기능을 이용한다.
빌더에서 지원하는 디버깅 기능을 이용하면 애플리케이션을 일시 중지하고 중지된 상태의 시점에서
객체들의 상태를 확인할 수 있을 뿐만 아니라 한줄한줄 코드 실행을 제어할 수 있다.

그럼 실습을 통해 자세하게 알아보겠다.

빌더에서 지원하는 디버깅은 "특정한 시점" 으로부터 시작된다.
따라서 여러분은 먼저 객체의 값을 확인하고자 하는 "특정한 시점"을 정해야 한다.
먼저 소스뷰에서 특정 라인번호를 더블클릭 하면 다음 그림처럼 빨간색 박스친 부분의 점이 표시될 것이다.
그 점이 바로 "특정한 시점" 이며 애플리케이션이 중지되고 객체의 상태를 확인할 수 있는 지점이다.
빌더에서는 "특정한 시점"을  보다 전문적인 용어로 "브레이크 포인트" 라고 부른다.



이제 애플리케이션을 디버그 모드
(F11 키 또는 벌레모양 아이콘) 로 실행 시켜보자.
애플리케이션이 시작되면 초기화 과정(creationComplete 이벤트)에서 브레이크 포인트 지점의 코드가 
수행된다. 
브레이크 포인트 지점의 코드가 수행될 때 Flex Builder는 자동으로 디버깅 모드로 전환되고
애플리케이션은 정지상태가 된다. 

디버깅 모드로 전환된 모습은 다음 그림과 같다.



①번 영역은 코드 실행을 제어하거나 세부적인 디버깅을 컨트롤할 때 사용하는 Debug view로 구성되어 있다.
②번 영역은 객체의 상세한 정보를 볼 수 있는 Variables view, 
브레이크 포인트의 목록을 확인할 수 있는 Breakpoints view, 
특정 변수를 주시하고 싶을때 사용하는 Expressions view 로 구성되어 있다.

그럼 이제부터 각각의 view에 대해 자세히 알아보겠다.

2-3-1. Debug view
애플리케이션이 브레이크 포인트를 만나서 디버깅 모드로 진입했을 때 애플리케이션을 디버깅을 중지하고 다시 실행시키거나 한줄한줄 코드를 실행하고자 할 때 Debug view를 사용한다.
Debug view에는 다음과 같이 디버깅을 제어할 수 있는 아이콘이 있다.

Resume

브레이크 포인트에 의해 애플리케이션이 정지상태가 되었을 때, 디버깅을 멈추고 정지된 애플리케이션을 이어서 가동시킨다. 만약 또 다른 브레이크 포인트를 만난다면 다시 디버깅 모드로 진입된다.

Suspend

애플리케이션과 함께 디버깅을 일시 중지한다.

Terminate

애플리케이션과 함께 디버깅을 종료한다.

Disconnect

Flex Builder의 디버거와 연결을 끊는다.
연결이 끊기면 브레이크 포인트를 만나더라도 디버깅 모드로 진입할 수 없다.

Step Into

현재 라인이 함수라면 함수 내부의 첫번째 라인으로 이동한다.
만약 현재 라인이 함수가 아니라면 현재 라인을 실행하고 다음 라인으로 이동한다.

Step Over

현재 라인을 실행하고 다음 라인으로 이동한다.

Step Return

현재 실행중인 함수를 호출한 부모함수로 이동한다.
예를들면, Step Into에서 함수 내부로 이동했을 경우 다시 밖으로 빠져 나오게 된다.


2-3-2. Variables view
애플리케이션이 브레이크 포인트를 만나 정지상태가 되었을 경우 정지된 시점에서의 객체들의 값을 확인할 때 Variables view를 이용한다.
다음 그림은 이번 장의 예제코드를 디버그 모드로 실행했을 때의 Variables view의 모습이다.


위 그림에서 this는 현재 브레이크 포인트가 위치한 클래스(또는 MXML문서) 이다.
트리의 노드 앞에 있는 아이콘은 해당 노드의 가시성을 나타낸다.
따라서 this 하위에 있는 a는 private, b는 protected, c는 public 가시성을 가지는 변수이다.
그리고 회색(L) 아이콘의 obj는 현재 수행중인 함수의 로컬변수임을 뜻한다.

this의 트리를 계속 열면 수백 개의 변수를 확인할 수 있으며, Ctrl+F 키나 마우스 우클릭후 Find Variable 메뉴를 통해 다음 그림처럼 특정 변수를 검색할 수 있다.


위 그림처럼 변수명을 입력하면 실시간으로 원하는 변수를 찾을 수 있다.


2-3-3. Expressions view
Variables view의 수많은 변수 중 특정한 변수를 주시하고 싶다면 Expressions view을 이용하면 된다.
Expressions view 는 특정 변수를 주시할 수 있고 변수들의 값을 원하는 형태의 결과로 볼 수 있는 기능을 제공한다.

먼저 예제 코드를 다음과 같이 변경하고 브레이크 포인트를 함수 종료부분에 지정하자.



코드를 변경 했다면 디버그 모드
(F11 키 또는 벌레모양 아이콘) 로 애플리케이션을 시작한다.
애플리케이션이 브레이크 포인트를 만나 디버깅 모드로 진입하면 Variables view에서  주시하고자 하는 변수에
마우스 우클릭후 Create Watch Expression 메뉴를 클릭한다. 
이제 Expressions view를 확인해보면 다음 그림처럼 선택한 변수가 추가되어 있을 것이다.


이렇게 Expressions view에 변수를 추가해 놓으면 디버깅 하는 동안 Variables 창의 수많은 변수들 중 해당 변수를 찾을 필요 없이 바로 확인할 수 있다.

이번에는 Expressions view를 보다 효과적으로 사용하는 방법에 대해 알아보겠다.
먼저 Expressions 
view에서 마우스 우클릭후 Add Watch Expression 메뉴를 클릭한다.
그리고 다음 그림처럼 변수들을 사칙연산으로 표현해보자.



OK 버튼을 누르고 Expressions 
view를 확인해보면 다음 그림처럼 보일 것이다.



이처럼 Expressions 
view에서는 특정 변수를 주시할 수 있을 뿐만 아니라 변수들의 연산까지 지원해서
변수들의 상호작용에 대한 결과를 바로 알 수 있다.

2-3-4. Breakpoints view
브레이크 포인트를 여기저기 지정하고 해당 문서를 닫았을 경우 지정한 브레이크 포인트들을 찾는 것은 쉬운일이 아니다. 이럴 때 Breakpoints view를 사용하면 지정한 브레이크 포인트들을 쉽게 찾을 수 있다.

다음 그림을 살펴보자.


이처럼 Breakpoints view에는 지정한 브레이크 포인트들의 리스트가 보여진다.
그리고 Delete키 또는 마우스 우클릭후 Remove메뉴를 통해 브레이크 포인트를 바로 제거할 수 있다.

2-4. flash.debugger.enterDebugger()
브레이크 포인트는 빌더에서 관리되기 때문에 브레이크 포인트 자체를 소스코드와 함께 공유할 수는 없다.
그러나 브레이크 포인트와 똑같은 기능을 수행하는 함수가 있다.
바로 flash.debugger.enterDebugger() 함수이다.
이 함수는 브레이크 포인트와 똑같은 기능을 수행하기 때문에 소스코드와 함께 디버깅 지점을 공유할 수 있다.

다음 그림을 보자.


디버깅 지점을 소스코드로 명시했기 때문에 Breakpoints view에 나타나지 않는다.
하지만 그 외에 브레이크 포인트와 다른 점은 없다.


3. 맺음말
지금까지 디버깅에 대해서 알아봤다.
앞으로는 이 글에서 소개한 디버깅 방법으로 소프트웨어 품질을 향상시키는데 노력하자.
마지막으로 디버깅은 개발이 끝난 이후에 하는 것이 아니라 개발과정의 일부분 이라는 것을 명심해야 한다.


출처 : http://jjaeko.tistory.com/103

Posted by 1010