Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Objective-C

22. Objective-C 메모리 관리 MMR, ARC

by Toughie 2024. 5. 23.

KAKAO-Choonsik

앱 개발을 하다보면 메모리 누수 메모리 누수 하는 얘기를 들어봤을 것이다.

예전에 비하면 스마트폰의 성능과 램 용량이 넉넉해져서 좀 덜하지만.. 그래도 앱의 성능을 위해서는 메모리 관리의 개념을 알고 있는 것은 굉장히 중요하다.


MRR (Manual Retain-Release)

최근 많은 언어들이 메모리 관리의 많은 부분을 자동으로 해주지만, Objective C와 같은 C계열 언어는 메모리 관리를 직접 해줘야 했다. 필요한 경우에 메모리에 객체를 올리고, 객체가 더 이상 필요 없어지면 메모리에서 직접 해제해 주는 것이다.

깜빡하거나 실수로 필요없는 객체를 메모리에서 해제하지 않으면 쓸데없이 메모리를 잡아먹고 있는 녀석들이 많아지는데 이걸 메모리 누수라고 한다.

 

MRR은 이름 그대로 수동으로 메모리 할당 및 해제를 해주는 것이다.

명시적으로 객체의 메모리 사이클을 개발자가 관리해줘야 한다. 여기서 객체가 메모리에 잔존해도 되는지 판단하기 위한 

기준은 참조 카운팅(Reference Count)이다. 이건 런타임에서 Foundation class에 있는 NSObject에서 제공되는 기능이다.

 

객체의 인스턴스를 몇군데에서 참조하는지를 체크하고 있다가 카운트가 0이 됐을 때 객체를 메모리에서 해제하고

이 메모리 공간은 다른 객체가 사용할 수 있는 상태가 된다.


객체를 생성하는 메서드

alloc, new, copy, mutable copy 

위 메서드를 통하면 rc값은 1이 된다.

(copy의 경우 원본의 rc값을 늘리는 대신, 따로 관리하게 된다.)

 

객체의 참조카운트를 늘리는 retain

참조하려는 객체가 작업이 끝날 때 까지 안전하게 메모리에 존재하도록 하기 위해서 retain을 통해 참조카운트를 올려준다.

 

객체의 참조카운트를 줄이는 release

객체가 필요없어졌을 때 rc값을 줄이기 위해 release를 사용한다. (카운트 - 1)


객체의 생성과 해제 예시

#import <Foundation/Foundation.h>

@interface MyClass : NSObject
-(void) myMethod;
@end

@implementation MyClass
-(void) myMethod {
  NSLog(@"Hell World");
}

-(id) init {
  self = [super init];
  if (self) {
    NSLog(@"Object initialized");
  }
  return self;
}

-(void) dealloc {
  NSLog(@"Object deallocated");
  [super dealloc];
}

@end

int main(void) {
  MyClass *MyClass = [[MyClass alloc] init];
  [myMethod myMethod];

  NSLog(@"RC: %lu", [MyClass retainCount]); // 1

  [MyClass retain];
  NSLog(@"RC: %lu", [MyClass retainCount]); // 2

  [MyClass release];
  NSLog(@"RC: %lu", [MyClass retainCount]); // 1

  [MyClass release]; // 0

  //EXC_BAD_ACCESS
  //접근할 수 없는 메모리 주소에 접근하려고 할 때 (이미 deallocated 됨)
  // [MyClass release];
  // [myClass myMethod]

  myClass = nil;
    //아무런 동작도 하지 않고 nil이 반환된다. 런타임 에러도 안난다.
  [myClass myMethod];

  return 0;
}

 

alloc

우선 alloc같은 경우는 객체를 위한 메모리 공간말 할당하는 메서드로 직접 오버라이딩은 하지 못한다

내부적으로 allocWithZone이라는 메서드를 오버라이딩 할 수는 있지만, 메모리 할당 시점에 다른 작업을 같이 하는 것은 지양하는 것이 좋다. 객체 생성 시 alloc, 그리고 init을 호출하게 된다.

 

dealloc

dealloc의 경우 객체의 rc가 0이 되었을 때 메모리에서 해지되기 전에

자동으로 호출되는 메서드이다. (Swift에서 deinit과 동일하다.)

사라지기전에 cleanUp 작업을 추가할 수 있는데, 반드시 [super dealloc]을 호출해줘야 한다.

 

retain과 release, retainCount

alloc init을 통해 객체를 생성하면 RC는 1이다.

이후 retain을 하면 RC의 값이 1 늘어나고, release를 하면 1이 줄어든다.

RC는 retainCount 메서드를 통해 직접 확인할 수 있다.

 

EXC_BAD_ACCESS

접근할 수 없는 메모리 주소에 접근하려고 할 때 발생하는 예외이다.

런타임에 발생하기 때문에 굉장히 주의해야 한다.

 

위 예시에서는 alloc -> retain -> release ->release로 rc를 0으로 만들어 dealloc 시켜주었다.

그런데 그 이후에 또 release를 하려 하거나 객체에 접근하려고 하면 예외가 발생하는 것이다.

 

위와 같은 간단한 예시면 다행이지만 ,실제 개발환경에서는 엄청나게 많은 객체들이 메세지를 주고 받기 때문에

메모리 해제를 제대로 안하면 메모리 누수가 나고, 해제된 메모리인데 접근하려 하면 예외가 터질 수 있다.

(이래서 자동 메모리 관리가 등장하게 된 것이다.)

 

런타임 에러가 나면 앱이 죽어버리기 때문에, 꼭 dealloc된 객체를 참조하던 변수는 nil을 할당해주는 것이 좋다.

nil을 할당한 변수를 통해 메서드를 호출해보면 런타임 에러가 나지 않고, 아무런 동작 없이 nil을 반환한다.

이는 안전한 동작을 위해 빠뜨려서는 안되는 작업이다.


ARC (Automatic Reference Counting)

MMR과 같이 참조 카운트를 확인하는 방식이지만 retain과 release를 개발자가 명시적으로 하지 않는다. (못한다.)

컴파일러가 알아서 도와주기 때문이다. (내부적으로 retain과 release를 통해 동작하는 것은 동일하다.)

(자바에서 GC랑 동일하다고 볼 수 있다.)

 

최근의 프로젝트에서는 다 ARC를 사용하는 것이 권장되지만, Objective-C로 진행된 플젝의 경우 

ARC 설정이 꺼져있을 수도 있기 때문에 확인하고 필요에 따라 변경해줘야 한다. (현재는 ARC 사용이 기본값이다.)

 

ARC가 retain과 release를 호출해주면서 객체의 생명주기를 관리하긴 하지만,

모든 메모리 누수가 일어나지 않거나 예외가 발생하지 않는다는 것은 아니다. 

 

강한 순환 참조 문제

객체가 서로를 strong하게 참조할 때 참조 카운트가 0이 되지 않아 메모리 누수가 발생

한쪽 또는 양쪽의 참조를 약한 참조(weak) 또는 미소유 참조(unsafe_unretained)로 변경해서 해결할 수 있다.

@interface Person : NSObject
@property (nonatomic, strong) Dog *dog;
@end

@interface Dog : NSObject
@property (nonatomic, weak) Person *owner; // weak로 순환 참조 방지
@end

 

EXC_BAD_ACCESS 확인

강한참조를 쓰면 메모리에서 객체가 내려가지 않아 예외는 안나겠지만 메모리 누수의 위험이 있다.

따라서 메모리에 안전하게 접근할 수 있는가를 사전에 확인하는 작업이 필요하다.

특히 약한 참조를 사용할 때 객체가 존재하는지 확인하고 필요한 경우 강한 참조로 일시적으로 유지해야 한다.

__weak MyClass *weakObj = strongObj;
//nil이 아니면
if (weakObj) {
    [weakObj someMethod];
}

 

ARC는 객체의 참조카운트를 더욱 편리하게 관리해주기는 하지만, 다른 파일 매니저나 데이터베이스 등을 활용하다보면

해당 객체들은 여전히 개발자가 직접 관리해줘야 한다. 


함수의 스택 메모리는 작업이 끝나면 알아서 비워지지만, 힙 영역은 MRR 혹은 ARC 방식으로 관리된다는 것을 복습했다.

무거운 앱일수록 메모리의 누수가 성능에 영향을 크게 미칠 수도 있기 때문에

코드를 작성할 때 항상 안전하게 참조가 이루어지고 있는지,

누수가 나지 않는지 확인하면서 필요에 따라 leak detector등을 사용하면 더 쾌적한 프로그램을 작성할 수 있을 것이다.