[Swift] ARC가 그래서 뭔데요 ?

·2025년 6월 23일
0

iOS-posting

목록 보기
4/4

Swift를 처음 접하면 반드시 마주치는 개념, 바로 ARC(Automatic Reference Counting)
저는 처음에 아 이건 고급용어니까 나중에 더 잘해졌을 때 공부해야지! 지금 옵셔널도 헷갈리는데 뭔 ARC냐.. 했는데, 전혀 어렵지 않은 친구였습니다..!
일단 풀어서 설명하면 그냥 자동 참조 카운팅인거라서, 이게 메모리를 관리해주는 기법이다! 정도만 이해하시면 될 것 같네요!

이 글에서는 간단하게나마 이제 ARC를 파악하고 갈 수 있게 한 번 짜보도록 하겠습니다.


다시 - ARC란?!

Automatic Reference Counting - 말 그대로, Swift가 객체의 메모리 관리를 자동으로 해주는 시스템입니다. swift가 알아서 객체의 ‘참조 횟수’를 세고, 필요 없어지면 메모리에서 제거합니다.

그럼 가비지 컬렉터랑 똑같은 친구인건가요?

  • 이번 포스팅은 이친구(GC)가 주인공은 아니라 정말 간단하게 넘어갈게요..!

    사진 출처

그럼 가비지 컬렉터랑 똑같은 친구인건가요?

둘 다 메모리를 자동으로 관리해 주는 건 맞지만, 방식이 좀 다르다.

가비지 컬렉터

  • 런타임에 주기적으로 프로그램 전체를 검사해서 더 이상 사용하지 않는 객체를 찾아내고 한꺼번에 메모리를 정리한다.
  • 이 과정에서 프로그램이 잠깐 멈추는 Stop-The-World 현상이 발생할 수 있다.

ARC

  • 컴파일 시점에 컴파일러가 retain/release 코드를 자동으로 삽입해준다.
  • 그래서 런타임에는 객체의 참조 횟수가 0이 되는 즉시 메모리가 해제된다.
  • 별도의 백그라운드 검사 과정이나 프로그램 멈춤 없이 예측 가능한 방식으로 동작한다.

즉, 목적은 비슷하지만 언제(컴파일 vs 런타임)와 어떻게(정적 분석 vs 동적 검사) 작동하는지가 근본적으로 다르다.

비유로 설명하면

  • ARC: 설거지거리 나오자마자 바로 치워버리는 야무진 친구
  • GC: 좀 모아서 한 번에 설거지하는 친구

왜 ARC가 필요한가?

앱에서 객체들이 계속 만들어지고 사라지는데, 안 쓰는 객체가 메모리에 계속 남아있으면 앱이 느려지거나 꺼지는데, Swift는 이걸 자동화해서 안쓰면 바로바로 치워주는 야무진 친구를 만든거다!

--

ARC의 동작 원리

이름에서 봤듯이 레퍼런스 카운트 - 참조 카운트가 핵심이다.

그럼 이 참조 카운트라는 친구를 어떻게 올려주는데?

엄청 간단하다 !

  • 변수나 상수가 인스턴스를 참조하면 +1
  • 참조를 해제하면 -1
  • 이 숫자가 0이 되는 순간, ARC가 즉시 메모리를 해제
let obj1 = MyClass()  // 참조 카운트: 1 (obj1이 참조)
let obj2 = obj1       // 참조 카운트: 2 (obj1, obj2가 참조)
// obj1 = nil         // 참조 카운트: 1 (obj2만 참조)
// obj2 = nil         // 참조 카운트: 0 → 메모리 해제!

ARC의 한계와 주의점

이렇게만 보면 정말 간단하고 문제될게 없을 것 같은데, 야무진 친구가 일을 잘해주는 대신에 허점이 있다..! 그 중 하나가 순환참조라는 것이다.

근데 이 순환참조에 대해서 설명하기 전에 참조의 종류에 대해서 알아볼 필요가 있다..

참조의 종류

Swift에서는 참조하는 방식이 크게 3가지가 있다.

1. Strong Reference (강한 참조)

  • 기본적으로 우리가 별다른 키워드를 안붙여줬으면 그게 전부 강한참조다.
  • 참조 카운트를 +1 올려줌

2. Weak Reference (약한 참조)

  • 참조 카운트를 올리지 않음
  • 참조하는 객체가 사라지면 자동으로 nil이 됨
  • weak 키워드 사용

3. Unowned Reference (미소유 참조)

  • 참조 카운트를 올리지 않음
  • 참조하는 객체가 항상 존재한다고 가정
  • unowned 키워드 사용
class Person {
   let name: String
   weak var bestFriend: Person?     // 약한 참조
   unowned let birthPlace: City     // 미소유 참조
   
   init(name: String, birthPlace: City) {
       self.name = name
       self.birthPlace = birthPlace
   }
}

순환참조 오류

그래서 이제 강한참조를 알게 되셨으니!! 순환참조 오류에 대해서 설명을 해보겠습니다.

간단한 예시

class Person {
    var friend: Person?
}

var a: Person? = Person()
var b: Person? = Person()

a?.friend = b
b?.friend = a

여기서 뭔 일이 일어났는지 차근차근 보자.

  1. a라는 변수가 Person 인스턴스를 참조 -> 참조 카운트: 1
  2. b라는 변수가 또 다른 Person 인스턴스를 참조 -> 참조 카운트: 1
  3. a?.friend = b -> b의 참조 카운트: 2 (b변수 + a의 friend 프로퍼티)
  4. b?.friend = a -> a의 참조 카운트: 2 (a변수 + b의 friend 프로퍼티)

이제 문제가 생긴다.

a = nil  // a 변수는 사라졌는데, b의 friend는 여전히 a를 물고있다.
b = nil  // b도 마찬가지, b가 죽었는데, a의 친구로서 남아있다..

이렇게 두 인스턴스 모두 참조 카운트가 1로 남아서 영원히 메모리에 갇혀있게 된다. 이게 바로 순환참조다.

서로가 서로를 꽉 잡고 있어서 ARC가 "어? 이거 아직 쓰고 있네?"라고 착각하는 거다. 마치 두 사람이 서로의 손을 잡고 "너 먼저 놔" "아니야 너 먼저 놔" 하면서 영원히 못 헤어지는 상황 같은 거다.

물론 영화 코코에서 봤듯이 이러면 죽어서도 영원히 이승에 남아있을 수 있긴하겠다만... Swift가 원하는 방향은 아닌것같다.

그래서 이거 상황을 어떻게 해결해줄 것인가??

먼저 일단은 둘 다 동일하게 Person 클래스의 인스턴스다 보니까 크게 와닿지 않는 것 같아서.. 좀 더 와닿는 시나리오를 넣은 다른 예시를 들어보자!

주인과 강아지의 순환 참조

예시코드

class Dog {
    var name: String
    var owner: Person? // 강한 참조
    
    init(_ name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 강아지 해제됨")
    }
}

class Person {
    var name: String
    var dog: Dog? // 강한 참조
    
    init(_ name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 해제됨")
    }
}

var 철수: Person? = Person("철수")
var 멍멍이: Dog? = Dog("멍멍이")

철수?.dog = 멍멍이   // 철수 → 멍멍이 (강한 참조)
멍멍이?.owner = 철수 // 멍멍이 → 철수 (강한 참조)

철수 = nil  // 철수는 죽음을 맞이했고,
멍멍이 = nil  // 멍멍이도 죽음을 맞이했지만...

// 실제로는 둘 다 메모리에 아직 살아 숨쉬는중!! 
// (deinit 호출 안됨 = 메모리 누수 발생)

순환 참조(Retain Cycle) 문제

이게 순환참조다. 철수는 멍멍이를, 멍멍이는 철수를 놓지 않고 있어서 둘 다 메모리에서 해제되지 않는다.

철수 객체 <-> 멍멍이 객체
   |            |
변수는 nil    변수는 nil

객체들만 서로를 붙잡고 떠다니는 유령 상태다... ㄷㄷ

해결책1 - 명시적으로 관계 끊어주기

🃏 저승사자 등장

// 저승사자가 돼서 둘의 인연을 끊어준다
철수?.dog = nil  // 또는 멍멍이?.owner = nil

// 기존 코드
철수 = nil  // 이제 철수가 평안히 떠난다
멍멍이 = nil  // 멍멍이도 평안히 떠난다

// "철수 해제됨"
// "멍멍이 강아지 해제됨"

왜 이렇게 되나?

1. 철수?.dog = nil 실행
		멍멍이 객체의 참조 카운트: 2 -> 1

2. 철수 = nil 실행  
		철수 객체의 참조 카운트: 2 -> 1

3. 멍멍이 = nil 실행
		멍멍이 객체의 참조 카운트: 1 -> 0 (해제!)
		연쇄적으로 철수 객체도 해제!

이렇게 철수를 nil로 만들기전에 철수.dog의 참조를 풀어주면
해제를 할 수가 있다.

하지만 인간은 실수의 동물이지않는가? 이런거 한줄이라도 빼먹는 순간 나도 모르는 사이에 메모리 누수가 발생할 것이다..!

특히 앱에서는 다음과 같이 사소한 코드에서도 발생할 수 있다.

// 만약 유저 관련된 프로퍼티 관계를 설정할 때
user.profile = profile
profile.owner = user

//... 여러 로직들을 지나고.. 하단에
user = nil
profile = nil
// 어? 관계 정리를 깜빡했네? -> 즉 시 메모리 누수!

그래서 이런 고민에서 등장한게 참조 카운트를 늘리지 않는 weakunowned 키워드다!

해결책2 - weakunowned 키워드

이런 고민에서 등장한 것이 참조 카운트를 늘리지 않는 weakunowned 키워드다!

weak 참조로 해결

class Dog {
    var name: String
    weak var owner: Person? // 약한 참조로 변경!
    
    init(_ name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 강아지 해제됨")
    }
}

// 이제 순환 참조가 없음 약한 참조기 때문 ! 
var 철수: Person? = Person("철수")
var 멍멍이: Dog? = Dog("멍멍이")

철수?.dog = 멍멍이   // 강한 참조
멍멍이?.owner = 철수 // 약한 참조 (RC 증가 안함)

철수 = nil  // 철수 해제됨 -> 멍멍이.owner 자동으로 nil
멍멍이 = nil // 멍멍이도 해제됨


// "철수 해제됨"  
// "멍멍이 강아지 해제됨"

핵심 차이점

강한 참조: 나는 이 객체를 절대 놓지 않겠다! -> 참조 카운트 +1
약한 참조: 있으면 쓰고, 없으면 말고~ -> 참조 카운트 그대로

순환 참조 해결 방법

  1. 수동 해결: 명시적으로 관계 끊기 (실수하기 쉬움)
  2. 자동 해결: weak/unowned 사용 (추천되는 방향)

그래서 언제, 어디에 weak을 쓰면 됩니까!

- 부모 <-> 자식 관계: 자식 -> 부모 방향을 weak
- 소유자 <-> 델리게이트: 델리게이트를 weak  
- 순환 참조가 의심되는 모든 곳

결론

어느 쪽을 weak로 만드느냐에 따라 누가 누구를 소유하는지가 결정된다.

  • Dog의 owner를 weak로 → 주인이 강아지를 소유 (일반적)
  • Person의 dog를 weak로 → 강아지가 주인을 선택

이렇게 소유 관계를 명확히 구분해서 한쪽은 강한 참조, 다른 쪽은 약한 참조로 만들어주면 순환참조를 해결할 수 있다.


잠깐 뭔가 빠졌는데??

unowned에 대해서 설명을 안했다!

weak과 함께 순환 참조를 해결하는 또 다른 키워드가 바로 unowned다.
weak과 unowned의 차이점이라.. 쉽게 설명하면 옵셔널이랑 강제언래핑이랑 비슷하다!!

이렇게 말하면 느낌이 오실 수도 있을 것 같은데, 얘도 참조카운트를 늘리지는 않지만 만약 참조하던애가 nil이되면 그냥 그대로 프로그램을 폭파시켜버린다.(크래시)

더 제대로 설명을 하자면..

weak vs unowned 차이점

weak var owner: Person?     // Optional - 안전하지만 nil 체크 필요
unowned var owner: Person   // Non-optional - 편하지만 위험할 수 있음

unowned사용 코드 예시

import Foundation

// Person 클래스를 정의해야 함
class Person {
    var name: String
    var dog: Dog?

    init(_ name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 해제됨")
    }
}

class Dog {
    var name: String
    unowned var owner: Person // unowned 사용! 일단 옵셔널도 아닌걸보니 벌써부터 심상치않다..

    init(_ name: String, owner: Person) {
        self.name = name
        self.owner = owner
    }

    func greet() {
        print("\(owner.name)님 안녕하세요!")
    }

    deinit {
        print("\(name) 강아지 해제됨")
    }
}

var 철수: Person? = .init("철수")
var 멍멍이: Dog? = Dog("멍멍이", owner: 철수!)

철수?.dog = 멍멍이
멍멍이?.greet() // "철수님 안녕하세요!"

철수 = nil // 철수 해제됨
멍멍이?.greet() // 펑! 크래시! (해제된 객체 접근)

//출력 결과

철수님 안녕하세요!
철수 해제됨
Fatal error: Attempted to read an unowned reference but object 0x1003bba90 was already deallocated

에러메세지를 보면
소유되지 않은 참조를 읽으려고 했지만 개체 0x1003bba90이 이미 할당 해제되었습니다.

이 에러 메시지를 통해 weak와 unowned의 근본적인 차이를 알 수 있다.
그니까 이걸로 하나 알 수 있는 것은 weak는 nil값이 된걸보면 알아서 할당을 해제하지만, unowned는 포인터처럼 힙주소를 가리키던 손가락을 거두지않고, 그냥 그대로 굳어버리게 되는데, 그때 이미 할당이 해제된 주소값에 접근하니까 터지는것이다.

weak vs unowned

weak - 똑똑이

weak var owner: Person?
  • 객체 해제 : Swift 런타임이 자동으로 nil 설정
  • 접근 시: 안전하게 nil 체크 가능
  • 가리키던 대상이 사라지면 자동으로 손을 거둠

unowned - 고집쎈친구

unowned var owner: Person
  • 객체 해제 시: 여전히 해제된 메모리 주소를 가리킴
  • 접근 시: 해제된 메모리에 접근 시도 -> 크래시
  • 가리키던 대상이 사라져도 그 자리 , 무덤을 계속 가리킴 👉 🪦

그럼 대체 언제 unowned를 사용할까?

현실적인 조언: 99% 경우에는 weak를 쓰자

메모리 누수는 앱이 느려질 뿐이지만, 크래시는 앱이 아예 꺼져버린다. 그래서 웬만하면 weak가 안전하다.

unowned 사용하는 극단적인 경우

철저한 종속 관계에서만 사용하자!

class Customer {
    var card: CreditCard?
}

class CreditCard {
    unowned let customer: Customer  // 고객 없는 카드는 존재 불가!
}
  • 고객이 사라지면 신용카드도 반드시 함께 사라져야 함
  • 신용카드가 고객을 잃어버릴 상황은 절대 있으면 안됨
  • 이런 논리적으로 불가능한 상황에서만 사용

핵심 원칙

100% 확신이 있을 때만 unowned
조금이라도 의심스러우면 weak

결론

절대적 종속 관계가 확실할 때만 unowned, 나머지는 모두 weak을 사용해주자!

핵심 요약

weak - 안전하지만 Optional이라 nil 체크 필요
unowned - 편하지만 잘못 쓰면 크래시

결론: 순환 참조 해결이 목적이라면 대부분 weak을 사용하는 것이 안전하다. unowned는 성능이 중요하고 생명주기가 확실한 특별한 경우에만 사용하자!

막판 ARC 요약

  • 자동 메모리 관리: 객체 참조만 관리하면, 해제는 ARC가 자동으로 처리
  • 참조 카운트 기반: 참조가 0 되면 메모리 해제
  • 순환 참조 주의: 강한 참조가 서로 얽히면 메모리 누수 발생 → weak/unowned로 해결

다음엔 또 weak 공부하다가 델리게이트 패턴에 대해서도 좀 살펴봤는데 그 부분에 대해서 포스팅을 해보면 좋을 것 같네요!!


참고

profile
기억보단 기록을

0개의 댓글