GS SHOP 서비스에서 모바일 앱 개발 및 메시지 시스템 운영을 맡고 있습니다.
현재 단말의 성능은 대부분의 앱들이 소비하는 자원을 충분히 감당할 수 있는 성능을 가지고 있지만,
앞으로 VR, AR 등 많은 자원을 소모하는 앱 서비스들이 많아지면, 예전 단말처럼 자원이 부족하게 될 가능성이 높습니다.
이로 인해 GSRetail App들의 서비스 안정성이 떨어질 수 있기 때문에,
예전에 적용했던 GSSHOP App에서의 메모리 확보 방법에 대해 이야기하려고 합니다.
iOS (iPhone OS) 기준으로 이야기를 할 것이니 이점 참고 바랍니다.
GS SHOP App 분석
제가 이 회사에 처음 입사하여 가장 먼저 개발해서 추가했던 것이 바로 App crash 정보 수집 기능입니다.
운영 중인 App을 개선하기 위해서 문제를 발생시키는 원인 파악이 선행되어야 했기 때문입니다.
게다가, iOS는 C, C++기반으로 짜여 있었고, 개발언어인 Objective-C도 유사한 부분이 많아 접근에 좀 더 용이했습니다.
관련 정보를 얻을 수 있는 부분을 찾아보다가 시스템이 앱을 종료할 때, 호출하는 콜백 함수를 찾을 수 있었죠.
바로 해당 함수를 이용하여 App crash 정보 수집 기능을 추가하였고,
몇 가지 정보를 더 추가하여 앱 종료를 야기하는 원인을 수집하게 되었습니다.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
...
// nami0342 - Collect a app crash log in global.
NSSetUncaughtExceptionHandler(&logingAppCrash);
...
}
// nami0342 - 2016.03 App 크래시 로그를 수집하고 Shop server로 전달.
static void logingAppCrash(id exception)
{
//NSLog(@"XXX Statck trace : %@", [exception callStackSymbols]);
#if DEBUG
return;
#endif
#if APPSTORE
...
NSString *strSideMenuTop = (ApplicationDelegate.isSideMenuOnTop == YES)?@"sideMenuTop":@"";
NSString *strDeathNote = [NSString stringWithFormat:@"%@ %@",strLastViewController,strSideMenuTop];
NSMutableArray *arCrashSymbols = [NSMutableArray arrayWithArray:[exception callStackSymbols]];
// Add exception desctription - 콜 스택만 나오면 해당 함수에서 왜 크래쉬 났는지 안 보여서 추가
[arCrashSymbols insertObject:[NSString stringWithFormat:@"%@ DESC : %@",strDeathNote, [exception description]] atIndex:0];
NSString *strNetwork;
if([NetworkManager.shared currentReachabilityStatus] == NetworkReachabilityStatusViaWiFi)
{
strNetwork = @"wifi";
}
else if([NetworkManager.shared currentReachabilityStatus] == NetworkReachabilityStatusViaWWAN)
{
strNetwork = @"lte";
}
// To JSON string
NSString *strJson = [arCrashSymbols description];
NSString *strDevice = [UIDevice currentDevice].deviceModelName;
if([[strDevice lowercaseString] hasPrefix:@"simulator"] == YES)
return;
// Send a log to GSShop server if possible.
NSString *strURL = [NSString stringWithFormat:@"%@?seq=%@", SERVER_CRASH_LOG, DEVICEUUID];
NSURL *url = [NSURL URLWithString:strURL];
NSMutableURLRequest *urlRequest = [NSMutableURLRequest requestWithURL:url];
//2017.11.23 모든 NSMutableURLRequest User-Agent 일괄추가
NSString *strUserAgent = [[NSUserDefaults standardUserDefaults] objectForKey:@"UserAgent"];
[urlRequest setValue:strUserAgent forHTTPHeaderField:@"User-Agent"];
[urlRequest setHTTPMethod: @"POST"];
NSMutableString *mstrPostDatas = [[NSMutableString alloc] init];
[mstrPostDatas appendFormat:@"%@=%@&", @"app_version", CURRENTAPPVERSION];
[mstrPostDatas appendFormat:@"%@=ios_crash_%@&", @"error_name", [exception name]];
[mstrPostDatas appendFormat:@"%@=%@&", @"error_description", strJson];
[mstrPostDatas appendFormat:@"%@=%@&", @"platform", @"iOS"];
[mstrPostDatas appendFormat:@"%@=%@&", @"device_model", [UIDevice currentDevice].deviceModelName];
[mstrPostDatas appendFormat:@"%@=%@&", @"os_version", [UIDevice currentDevice].systemVersion];
[mstrPostDatas appendFormat:@"%@=%@&", @"network_type", strNetwork];
NSData *postData = [mstrPostDatas dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
[urlRequest setHTTPBody: postData];
SyncResponseModel *responseModel = [NetworkManager.shared sendSessionSynchronousRequest:urlRequest];
if (responseModel.error != nil) {
NSLog(@"Server Error.... %@",[NSString stringWithFormat:@"Submission error: %@", [responseModel.error localizedDescription]]);
} else {
NSLog(@"!!!!!Crash log send finished");
}
#endif
}
이렇게 수집된 정보를 분석하여 점차 App의 안정성은 향상되었지만,
여러 앱을 동시 구동하여 단말의 가용 메모리가 부족한 상황일 경우에는
GS SHOP App의 모든 서비스를 고객에게 안정적으로 제공하기란 너무나도 어려운 상황이었습니다.
다른 앱에 접근할 수 없는 상황에서 다른 앱이 차지한 메모리를 반환시키기 위해 모바일 OS의 메모리 관리 방식의 이해가 필요했습니다.
모바일 환경에서의 메모리
모바일 단말에서는 장치의 소형화로 인해 시스템이 활용할 수 있는 가용 자원이 작습니다.
그 자원 중에 메모리는 앱의 안정적인 서비스에 가장 많은 영향을 미칩니다.
물론 CPU나 Disk, Network도 중요한 자원임에는 틀림없지만,
CPU는 클럭이 낮더라도 동작은 되고, Disk 용량은 파일 삭제를 통해 확보 가능합니다.
메모리는 다양한 프로세스들이 실시간적으로 영향을 미치는 자원이기 때문에 선제적인 대응이 쉽지 않습니다.
따라서, 모든 OS는 메모리를 잘 활용하기 위해 연속 메모리 할당, Paging, Swapping 방법들을 사용하고 있죠.
게다가, 메모리는 한 프로그램이 영역을 할당받아 점유해 버리면,
해당 영역이 해제되기전까지 다른 프로그램이 해당 영역을 사용할 수 없습니다.
보안을 위해서 일반 프로그램들은 다른 프로그램이 확보한 메모리 영역에 접근할 수 없는 것이죠.
메모리의 활용
예를 들어 프로그램에서 이미지 하나를 화면에 표시한다고 하면,
Disk 메모리에서 데이터를 읽어 Data buffer에 담아 처리한 후,
Image buffer에 담은 후 Renderer가 화면에 표시하게 됩니다.
10MB짜리 이미지라면, Data buffer, Image buffer만 생각해도 최소 20MB의 메모리를 사용하게 되는 것이죠.
요즘은 대부분의 이미지를 로컬이 아닌 CDN에서 네트워크를 통해 다운로드하여 표시합니다.
이는, 네트워크로부터 받은 데이터를 임시 저장하기 위해 메모리를 추가적으로 사용한다는 의미입니다.
단순하게만 생각해도 벌써 최소 원래 이미지 크기의 3배에 해당하는 메모리 공간을 사용하게 되는 것입니다.
이 처럼 프로그램에서는 단순히 이미지를 화면에 노출하기 위해서 원본보다 더 큰 크기의 메모리 공간을 사용합니다.
당연하게도, 많은 프로그램이 구동되는 환경에서는 더 많은 메모리를 사용하게 되겠죠.
이 처럼 프로그램이 지속적으로 메모리를 사용하게 되면, 시스템이 운영할 수 있는 가용(여유) 메모리가 부족하게 됩니다.
이렇게 가용 메모리가 부족하게 되면, 일반적인 OS는 새로운 프로그램을 메모리 부족을 이유로 구동시키지 않습니다.
하지만, 모바일 OS에서는 다른 방법을 선택했습니다.
모바일 OS의 선택
초창기 모바일 단말이 기억나나요?
작은 장치 사이즈로 인해 공간부족과 기술의 한계로 인해
모바일 OS 개발자들은 좀 더 가용 메모리 확보를 위해 여러 가지 방안을 고심하였습니다.
다행히도(?) 화면이 작기 때문에 하나의 GUI App만 화면 가득 렌더링 할 수 있게 설계했습니다.
문제는 다중 코어 CPU를 사용하기 때문에 멀티 프로세스가 기본인 환경이었다는 것입니다.
다시 말해 여러 개의 App이 동시에 구동되는 환경이라는 이야기입니다.
이렇기 때문에 전면에서 렌더링 되어 사용되어 사용자에게 보이는 App과
보이지 않는 App을 구분해서 관리하게 됩니다.
기존 OS에서도 멀티 GUI 프로그램에 대한 처리가 이미 있어서 관련 처리 기능도 이미 있었습니다.
예를 들어
화면에 이미지를 표시하는 프로그램의 창을 최소화한다면,
Data buffer -> Image Buffer -> Renderer 순서로 이어지는 리소스 중에서
Renderer 리소스는 즉시 회수되고, 그 이후 역순으로 Image Buffer, Data buffer의 자원까지도 회수하여 메모리 관리를 합니다.
당연히 모바일 OS에서도 이 Achitecture는 재 활용합니다.
App의 상태 변화와 메모리 관리
아래 그림처럼 모바일 OS는 App을 여러 가지 상태로 구분하지만,
여기에선 간단하게 2가지 상태만을 이야기하겠습니다.
(Foreground) Active (눈에 보이는 - 활성화된 App 상태)
Backgroud (눈에 안 보이는 - 백그라운드 상태)
예를 들어볼까요,
GS SHOP App을 실행시켜서 보고 있다가 GS Fresh App을 실행시킨다면,
각 App 들은 아래와 같은 상태를 가지게 됩니다.
GS SHOP App : Active -> Backgroud 상태 (현재)
GS Fresh App : Active 상태 (현재)
모바일 환경에서도 새로운 App이 구동되려면 가용 메모리가 있어야 하는데,
백 그라운드 상태의 App들이 많은 memory를 점유하고 있으면, 가용 메모리 확보가 안되어,
새로운 앱을 구동할 수 없겠지만, 모바일 OS 개발자들은 다른 방법을 고안합니다.
가용 메모리가 부족할 경우 신규 App 구동을 막기 전에
백그라운드 앱을 죽여 메모리를 회수하는 방식으로 말이죠.
사용자들이 백그라운드 앱을 잘 종료하지 않아 메모리가 낭비될 거란 상황을 예측했던 걸까요?
Background 상태에서의 메모리 관리
모바일 OS는 App이 Backgroud 상태로 떨어지면, 아래를 판단하여 메모리 회수 절차를 진행합니다.
- 가용 메모리가 여유롭지 않다면,
- 가장 많은 메모리를 사용하는 백그라운드 App의 메모리 회수
- Background에서 가장 오랫동안 머물렀던 App의 메모리 회수
- 사용 빈도가 가장 떨어지는 App의 메모리 회수 등.
요즘엔 단말 메모리 용량이 커져 경험할 확률이 낮아졌지만, 예전 저 사양 단말에서 빈번하게 발생했던 상황에 대해 이야기 해볼까요?
모바일 게임을 한 후, 쇼핑 앱을 사용하다가, 잠깐 스마트 뱅킹을 위해 은행 앱을 띄웠다가 쇼핑 앱으로 되돌아오니
- 앱이 처음부터 구동되었다?
- 화면이 깜빡하더니, 내용이 초기화되었다?
혹시, 한 번도 이런 경험을 해본 적이 없다면, 몇 가지의 앱 위주로만 사용하셨을 확률이...
이 현상은 뱅킹 앱이 활성화되면서 쇼핑 앱이 백그라운드 상태로 떨어져 관리 대상이 되었고,
시스템은 가용 메모리가 부족하다고 판단하여
백그라운드 앱들을 대상으로 메모리 회수 절차를 시행했기 때문에 발생한 것입니다.
그렇다면, 앱이 백그라운드 상태로 안 떨어지게 만들면 메모리 회수 대상이 안될까요?
그에 관련된 내용이 이전 아티클에 나와 있으니 안 읽어보신 분들은 한번 읽어보시기 바랍니다.
읽을 시간이 없으시다고요? 간단하게 설명하면,
앱을 Backgroud 상태로 전환되지 않는 것처럼 시스템에게 알려
메모리 관리 대상으로 넣지 않게 하는 방법입니다.
안타깝게도 이 좋은 기능은 AOS(안드로이드 OS)에서만 제공하고 있죠.
그렇다면 iOS에서는 뭐가 있을까요?
IOS에서는 didReceiveMemoryWarning()이라는 Callback 함수 (시스템이 호출하는 함수)를 제공하고 있습니다.
시스템에서 가용 메모리가 모자라다고 판단하면 Backgroud 상태의 앱들은 회수 절차대로 진행하면서
Active상태인 앱에게는 2~3회 정도 호출하며 경고를 합니다.
물론 애플답게 경고뿐만 아니라 현재 로드된 view에 대해서 ViewUnload()를 강제 호출 해 버립니다.
만약, 대응을 제대로 안 했다면, 하얀 화면을 만나게 되어 앱을 강제로 종료해야 합니다.
그래서 예전에는 해당 콜백함수에 메모리를 해제하는 코드를 추가해 놓기도 했었습니다.
하지만, ViewUnload() 같은 강제조치를 한 후에도 개발자의 추가적인 메모리 해제 로직을 통해서도
여유 메모리가 부족하다면, 시스템은 해당 앱을 강제 종료 시킵니다.
메모리 확보 방안
일반 단말 사용자 입장에서 저런 상황을 최대한 피할 수 있는 방안이 있습니다.
사용하지 않는 앱들 중 빈번하게 사용하지 않는 앱들은 백그라운드에 놓지 마시고, 그냥 종료하시고,
적어도 3개월에 한 번 정도는 스마트폰을 껐다 켜주세요.
아니라면 메모리가 빠방 한 스마트폰을… 구매한다던지요.
당연하게도 저는 저 방식을 일반 고객들에게 이해시킬 수 없는 입장이었기에,
저 상황을 최대한 피할 수 있는 방안을 고민했습니다.
결국, 관리자 권한이나 다른 앱의 메모리 영역을 손댈 수 없는 상태에서 생각해 낸 것은
시스템에게 Background 앱들의 메모리 회수를 다그치는 방식이었습니다.
방법이 너무 간단하여 실망할 수 도 있는데.. 효과는 꽤 좋았습니다.
앱이 Active 상태였을 때,
가용 메모리 사이즈를 확인한 다음.
적당한 사이즈 이하일 때 !! (너무 적은 상태에서 진행할 경우 Active 상태 앱이 Kill 명령을 받음)
가용 메모리를 체크하여 적당량의 동적 메모리 할당을 요청하고
잠시 후 이를 해제하는 로직을 추가했습니다.
시스템은 메모리 할당 요청을 받자마자 다른 백그라운드 상태의 앱들의 메모리를 회수하여 요청한 가용 메모리를 확보하려고 했고,
앱에서도 할당받은 동적 메모리를 해제 하니, 다른 앱들을 죽여서 좀 더 안정적으로 자원을 사용했었습니다.
기존 버전의 앱보다 수정된 버전에서 좀 더 안정성이 향상되었습니다
int *iBuffer = malloc(500 * 10240);
...
free(iBuffer);
관리하는 앱의 안정성 확보를 위해 사용했었지만, 여러 이유로 꽤 제약된 상황에서만 활용되었습니다.
현재는 불필요하여 해당 코드는 제거된 상태입니다.
너무 간단한 코드라 의심이 된다구요?
맞습니다. 의심해야 개발자죠.
하지만, 데이터는 거짓말하지 않습니다.
아래 그래프는 당시, 메모리 관련으로 Crash 나는 앱의 건수를 수집했던 자료입니다.
해당 코드를 추가하기 전에는
하루에 100여 건의 App crash가 발생했지만, 적용 후 하루 10여 건으로 지속적으로 줄었음을 확인하실 수 있습니다.
그리고 1개월 뒤의 지표에서도 안정적인 상태를 유지함을 보실 수 있을 겁니다.
기술의 발전은 고용량의 메모리를 만들어 냈고,
OS 버전이 상승하면서 앱 배포 마켓에서는
너무 낮은 버전에서는 앱 설치의 제약을 거는 방식을 적용하는 분리 정책을 통해 안정성을 향상하고 있습니다.
OS의 메모리 관리기술의 향상과 낮은 단말 성능을 가진 고객들의 수는 줄어들었고,
그로 인해 위의 간단한 코드는 현재는 불 필요해졌습니다.
하지만, 나중에 고용량의 메모리를 이용하는 서비스들이 많이 나오면
시스템을 졸라대는 저 방식이나,
또 다른 이용할 수 있는 허점처럼 보이는 괜찮은 OS Architecture를 확인한다면
슬그머니 적용시켜볼 생각입니다.
임성남 | 디지털서비스본부 > GSSHOP 개발팀
GSSHOP App 개발/ GSSHOP 메시징 시스템 운영을 담당
경험 언어 : C, C++, C#, Object Pascal, Objective-C, Swift, Dart(ing)
경험 프로젝트 : ITAM(시계열 예측기반), SMS(Server Management System), NMS, IPS, API, Windows service, IOCP Server, 2D Image filter engine, iPhone OS 기반 여러 App 개발 등.
'APP' 카테고리의 다른 글
우리동네GS BFF 구현기 Step 1 - 도입 배경과 설계 (1) | 2023.07.28 |
---|---|
Flutter App 실시간 CDN 이미지 변경 상태 적용 방안 (1) | 2023.06.20 |
엔터프라이즈 MSA 이야기 2탄-RateLimit 적용으로 시스템장애 예방하기 (0) | 2023.06.20 |
안드로이드 포그라운드 서비스를 활용한 메모리부족으로 앱 종료되는 현상 개선 (4) | 2023.04.17 |
엔터프라이즈 MSA 이야기 1탄 - 주문서비스(Milestone1) (2) | 2023.01.02 |