brunch

You can make anything
by writing

C.S.Lewis

by 이승현 Feb 13. 2017

Threading Performance #02

Memory & Threading

#01 Memory contention problem


Multi-threading 환경에선 다양한 문제 (동기화 문제, 성능 저하 문제 등)가 발생할 수 있습니다.

Thread는 Process와 달리 메모리 영역을 공유하기 때문에, 만약 두 Thread가 동일한 메모리 영역을 동시에 이용한다면 메모리 누수(Memory leak) 문제들이 발생할 수 있습니다.


메모리 누수 문제는 안드로이드에서도 발생할 수 있습니다.





1. UI Object Reference


#02 Throw exceptions by thread


안드로이드 설계 상 UI 객체는 Thread로부터 안전하지 않습니다. 

안전하지 않다는 말은, Main Thread(UI Thread)에서 생성, 사용 및 삭제하는 UI 객체를 다른 Thread에서 수정하거나 참조하려고 하면 exception이 발생하여 crash나 예상치 않은 결과가 발생할 수 있다는 의미입니다.




Explicit references


사실 안드로이드 개발을 하다 보면 Work Thread들를 많이 쓰고 있습니다.

대부분 ANR을 회피하고 Work Thread의 작업 결과를 기반으로 Main Thread의 UI 객체의 속성을 업데이트하기 위해서입니다.


만약 Work Thread에서 Main Thread의 UI 객체를 명시적으로 참조(Explicit references)하고 있다면 문제가 발생할 수 있습니다.


Work Thread가 Main Thread의 UI 객체에 대해 명시적 참조

작업이 완료되기 전에 Main Thread의 UI 객체가 제거됨

작업이 완료되고 명시적으로 참조하고 있는 UI 객체의 속성 업데이트 시도


위와 같은 상황이라면 두 가지 문제가 발생합니다.


1. UI 객체의 속성을 신뢰할 수 없습니다.

Main Thread의 UI 객체가 제거되었기 때문에 Work Thread의 작업이 완료되었어도 이를 화면에 나타낼 방법이 없습니다.


2. Activity의 메모리 영역이 해제되지 않습니다.

UI 객체는 해당 객체를 소유한 Activity instance에 대한 참조를 기본적으로 포함하고 있습니다.

즉 Activity가 Destroy 되더라도 이를 참조하는 Work Thread가 남아있는 경우 garbage collection는 Work Thread의 작업이 완료될 때까지 Activity instance의 메모리 영역을 해제하지 않습니다.




Implicit references

#04 Inner AsyncTask class


위 코드는 흔히 볼 수 있는 AsyncTask를 Actvitiy의 내부 클래스(inner class)로 선언하고 있는 코드입니다.

내부 클래스가 명시적으로 Activity instance에 대해 참조하고 있지 않지만, 암시적으로 참조(Implicit references)를 하게 됩니다.

결국 명시적 참조에서 나타났던 문제처럼 AsyncTask 작업이 완료될 때까지 Activity instance의 메모리 영역이 해제되지 않습니다.


내부 클래스의 암시적 참조로 인한 문제를 해결하기 위해서는 정적 내부 클래스(static nested class)로 선언해 암시적 참조를 제거해야 합니다.

정적 내부 클래스는 내부 클래스와는 달리 Activity instance를 암시적으로 참조하고 있지 않기 때문에, 메서드와 필드에 접근하기 위해서는 아래와 같이 Activity instance를 생성자나 메서드를 통해 별도로 지정해 줘야 합니다. (지정이 있다면 해제해주는 코드도 작성해야 합니다.)

#05 Static nested AsnycTask class



이 외에도 WeakReference를 이용한 방법도 있습니다.

WeakReference는 다른 일반 참조와는 달리 garbage collection의 참조 카운팅 대상이 아니기 때문에 GC가 Heap에서 메모리를 해제시킬 수 있습니다.




2. Don't hold references


즉 UI 객체에 대한 참조를 제거해야 합니다.

#07 Work record


참조 없이 Top level Activity 또는 Fragment만이 UI 객체를 업데이트할 수 있도록 한다면, 이러한 문제들을 해결할 수 있습니다.


view와 이를 업데이트하는 함수(callback)가 쌍을 이루는 WorkRecords를 생성합니다.

Work Thread의 작업이 완료되면 결과를 Activity에 결과를 알려줍니다. (intent...)

Activity는 view를 업데이트하고, 만약 view가 없다면 아무것도 수행하지 않습니다.




Don't reference UI objects from static objects


#06 Life of a staic object


정적(static) 객체는 자신을 생성한 Activity의 수명과 상관없이 앱 전체 process 내내 계속 남아있습니다.

즉 정적 객체에서 UI 객체를 참조하면 Activity가 destory 되더라도 Activity instance의 메모리 영역이 해제되지 않습니다.

예를 들어 이 상황에서 Configuration change 가 발생한다면 기존 Activity instance가 남아있는 채 새로운 Activity instance가 발생하는 문제가 일어납니다.


아래 영상을 보시면 좀 더 자세한 설명들이 있습니다.




작년 말 GDG DevFest Seoul 2016 에 참석했었습니다.

"안드로이드 개발에 유용한 도구들 (안세원)" 세션에서 메모리 누수를 탐지해주는 도구인 LeakCanary를 처음 접하고 당장 개발 중인 앱에 설치해서 실행해보니, 메모리 누수가 엄청 많이 탐지되었던 기억이 나네요ㅠ



메모리 누수가 앱 동작에 큰 영향을 끼치지 않아 보이지만, 메모리 누수가 많아지면 Heap 영역이 부족해지고,

결국 GC가 빈번하게 발생해서 앱 성능에 영향을 끼치게 됩니다.


메모리 누수와 관리에 대해 관심이 있으신 분들은 아래 글들도 읽어 보시기 바랍니다.

이해하는데 많은 도움을 받은 글들입니다.


작가의 이전글 Threading Performance #01
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari