스닥
Playground
스닥
전체 방문자
오늘
어제
  • 분류 전체보기 (125)
    • 개발자 기본기 (1)
    • Swift 아키텍처 (6)
    • iOS 개발 (55)
      • Swift (12)
      • UIKit (17)
      • SwiftUI (9)
      • CoreData (9)
      • MusicKit (4)
      • WebKit (2)
      • 개발 환경 (0)
      • WatchOS (2)
    • 애플 개발자 아카데미 (4)
    • 막 쓰는 개발일지 (0)
    • 운영체제 (4)
    • 네트워크, 서버 (16)
      • Network (9)
      • Server (7)
    • 알고리즘 (8)
    • C언어 (7)
      • 함수 (7)
    • 하루 이야기 (23)

블로그 메뉴

  • GitHub계정
  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • dfs
  • docker
  • BFS
  • 트리
  • ip주소
  • core data
  • 깊이 우선
  • C 언어
  • Core Animation
  • Server
  • swift performance
  • struct class 성능
  • SWIFT
  • 서버
  • 문자열 복사
  • ios rendering
  • 너비 우선
  • 도커
  • 알고리즘
  • 자료구조

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
스닥
iOS 개발/Swift

[ARC] 약한참조(Weak, Unowned)에 대해서

[ARC] 약한참조(Weak, Unowned)에 대해서
iOS 개발/Swift

[ARC] 약한참조(Weak, Unowned)에 대해서

2022. 11. 6. 19:50

인스턴스가 메모리에서 해제되기 위해서는 reference count가 0이 되어야하는데, 0이 되지 않는다면 메모리에 계속 남겨져서 memory leak 이 일어나게 됩니다.

어떤 경우에서 memeory leak이 일어나게 되는 걸까요?

 

대표적인 예시로 순환참조가 있습니다.

 

 

1. 순환 참조 상황 이해하기

 

먼저 순환참조는 두 개 이상의 객체가 서로 강하게 참조하는 상태를 의미합니다.

 

사실 순환참조는 피해야 하는 안좋은 방식입니다! 단순히 memory leak이 어떻게 일어나는지 보여주는 예시일 뿐이지, 우리가 객체를 이렇게 구성하면 안됩니다.

 

순환참조를 사용하게 되면 의존성에 문제가 생기게 됩니다. 컴포넌트 간의 경계가 명확해야 하는데 그러지 못하게 되고 하나가 변경되게 되면 다른 객체가 변경되는 등의 문제가 생기게 되는 것이죠.

 

또한 후에 객체를 서로 분리하기도 어려워지게 됩니다.

 

 

순환참조는 뭐가 문제일까?

 

순환참조는 뭐가 문제일까?

순환참조는 뭐가 문제일까? 개인 프로젝트는 혼자서 진행하기 때문에 내가 만든 설계가 제대로 된 설계인지 확인해보기 어렵다. 이를 확인하기 위해서 한 가지 기준을 두었는데 패키지간의 의

siyoon210.tistory.com

해당 아티클을 읽어보면 왜 순환참조를 하면 안되는지 이해할 수 있을거에요!

 

 

자 근데 지금 우리는 올바른 객체지향 구조에 대해서 공부하는 게 아니라 ARC에 대해서 공부하고 있기 때문에 일단 이런 것들은 넘어가고 서로 참조하고 있는 상황에만 집중해볼게요!

다음 코드를 한번 볼까요?

 

Traveler 와 Account가 서로를 참조하고 있습니다.

 

우리가 공부했던 대로 reference count(이하 ref count) 를 한번 세볼까요?

 

먼저 traveler와 account가 init 되면서 각자 ref count를 1씩 가지게 됩니다.

하지만, account가 생성 될 때, traveler을 참고하기 때문에 Travler는 1이 더 올라가서 2가 됩니다.

Account(traveler: traveler ...) <- 이 부분

 

그리고 traveler의 account에 account를 참조시켜줌으로서 Account의 ref count는 1이 더 올라가서 2가 됩니다.

traveler.account = account <- 이 부분

 

 

이제 account는 더 쓰이지 않기 때문에 ref count가 1 release 되어서 1이 됩니다.

벌써부터 문제가 발생했죠...

 

이제 travler도 printSummary함수를 call 한 후, 다 사용했기 떄문에 ref count가 1 release 되어서 1이 됩니다.

결국 test 함수가 끝나고 나면 해당 함수에서 생성되었던 Traveler와 Account 인스턴스는 각각 ref count가 1이 남아있기 때문에 메모리에서 해제되지 않은 상태로 남아있게 됩니다.

 

memory leak이 일어나게 되는 것이죠!

 

 

2. Weak (약한 참조)

 

객체를 소유한다는 것의 의미는 무엇일까요?

 

뜬금 없는 질문일 수도 있겠지만, 강한 참조와 약한 참조는 객체를 소유한다는 측면에서 가장 큰 차이를 가집니다.

객체를 소유하지 않는다는 것은 해당 참조가 반드시 객체를 가지고 있음을 보장하지 않는다는 것입니다.

 

처음 보면 이해가 잘 안될 거에요. 한번 천천히 볼까요?

 

 

위와 똑같은 코드이지만,

let account = Account(traveler: traveler ...)

해당 부분에서 Travler의 ref count는 retain 되지 않습니다.

그 이유는 Account에서 traveler을 약한 참조를 하겠다고 weak로 선언했기 때문이에요!

즉, Account의 travler 프로퍼티는 이렇게 이야기하는 거죠!

나는 Traveler 객체를 소유하지 않을게! 
그러니까 Traveler 객체가 어디선가 release가 되어서
ref count가 0이 되서 메모리에서 사라지더라도 내가 알아서 처리할게!
(내가 보고 있다가 Travler가 사라지면 나를 nil롤 바꿔줄 계획이야) <- weak
(아 몰라 그냥 터뜨려) <- unowned

나는 그냥 있으면 있는거고 없으면 없는대로 알아서 할테니까 나는 신경쓰지마!
당연히 retain도 해줄 필요 없어!

 

자 이렇게 이야기했으니, 한번 저 코드의 ref count를 세어볼까요?

한 줄씩 읽어볼게요!

 

 

먼저 traveler와 account가 init 되면서 각자 ref count를 1씩 가지게 됩니다.

Account가 traveler을 참조했지만, Traveler의 ref count는 올라가지 않아요.

왜냐하면 Account는 약한 참조로 traveler를 선언했기 때문이죠.

Account(traveler: traveler ...)

 

자 그다음, traveler 의 account에 account를 참조시켰기 때문에 Account의 ref count 는 retain되어서 2가 됩니다.

이제 account는 더 쓰지 않죠? 그럼 release가 되어서 1이 되었습니다.

 

자, 이 다음에 traveler도 print summary 함수를 call 하고 사용을 마쳤기 때문에 Traveler가 release 되게 됩니다.

아까 Traveler의 ref count는 1이었죠? (Account가 약한 참조로 traveler를 선언했기 때문에)

그렇기 때문에 Travler는 ref count가 0이 되어서 메모리에서 해제되게 됩니다!

 

그러면 동시에 Travler가 가지고 있던 account도 사용이 끝나기 때문에 Account도 한번 더 release가 되게 됩니다. 이제 Account의 ref count는 0이 되어서 Account도 마찬가지로 memory에서 해제되게 되는 것이죠!

자 이제 다 끝났습니다. Traveler와 Account 모두 메모리에서 잘 해제되어서 memory leak이 일어나지 않는 상태가 되는 것이죠!

 

 

 

3. Unowned는 Weak와 뭐가 다를까?

 

그럼 Unowned는 Weak와 뭐가 다를까요?

 

둘 다 약한 참조라는 것은 같습니다. 하지만 둘은 해당하는 인스턴스가 메모리에서 해제되었을 때 대처하는 방법이 다릅니다.

Weak는 해당 인스턴스가 메모리에 있는지 없는지를 관찰하다가, 해당 인스턴스가 메모리에서 사라지면 본인을 nil로 바꿔줍니다.

 

반면에 Unowned는 관찰하지 않습니다. 그냥 해당 인스턴스의 주소값을 가지고 있을 뿐이죠.

만약에 인스턴스가 메모리에서 사라지게 되면, 가지고 있는 주소값은 의미없는 주소값이 됩니다.

즉, 댕글링 포인터가 되는 것이죠. 그리고 해당 프로퍼티를 사용하는 순간 앱은 Runtime 오류를 일으키게 됩니다.

그렇다면 이 위험한 Unowned를 왜 쓸까요?

 

 

이에는 대표적으로 두가지 이유가 있습니다.

 

첫번째로 성능적인 측면입니다.

아까 해당 인스턴스가 메모리에 있는지 추적하다가 사라지면 해당 프로퍼티를 nil로 바꿔준다고 했죠?

이렇게 객체가 weak로 선언된 모든 참조를 찾아서 nil로 바꿔주는 작업은 유의미한 오버헤드를 가집니다.

When an object does not have any strong references pointing to it, 
the runtime will start the deallocation process but before this happens,
it will set to nil all the weak references that were pointing to the object.
Because of this behavior, weak references implemented this way are called zeroing weak references.

This implementation has a tangible overhead
if you consider that an additional data structure needs to be maintained
and that we need to guarantee the correctness of all operations 
on these global reference holding structures even in presence of concurrent access.
It should not be possible under any circumstance to access the value pointed
by a weak reference once the deallocation process has started.

참조 : Unowned or Weak? Lifetime and Performance - uraimo.com

 

Unowned or Weak? Lifetime and Performance

While the usual explanation that when dealing with retain cycles you should choose between unowned or weak considering references lifetime is by now well known, sometimes you are still in doubt about which one you should actually use between the two and if

www.uraimo.com

개발자가 해당 인스턴스의 수명을 완벽하게 알고 있어서 굳이 프로퍼티가 해당 인스턴스의 수명을 추적할 필요가 없을 때는 Unowned를 사용하면 불필요한 오버헤드를 방지할 수 있을거에요.

 

 

두번째로는 옵셔널로 선언할 필요가 없어서 코드가 단순해지기 때문입니다.

당연히 마찬가지로 개발자가 해당 인스턴스의 수명을 완벽하게 알고 있을 때 적용되는 이야기이지만,

해당 인스턴스가 메모리에 있는지 추적하다가 사라지면 해당 프로퍼티를 nil로 바꿔줘야 하는 Weak의 경우, 반드시 nil값이 들어갈 수 있는 옵셔널 타입으로 선언되어야 합니다.

weak var traveler: Traveler?

이렇게요!

 

하지만 우리가 옵셔널 타입의 변수를 사용할 때는 if let이나 guard를 이용해서 옵셔널 바인딩을 해줘야 하잖아요?

하지만 Unowned를 사용한다면 해당 참조는 옵셔널로 선언될 필요가 없기 때문에 옵셔널 바인딩도 필요가 없겠죠?

 

12.15
성능적인 측면이 얼마나 차이나는지 궁금해서 탐구 중에 있습니다..! 실험이 끝나면 포스팅할게요!

 

 

 

4. Instance의 수명 관리는 어떻게 해야할까?

 

다음 코드는 문제를 가지고 있습니다.

 

이전 코드와 달라진 부분은 printSummary 메소드가 Account로 들어갔다는 것인데요!

 

아까 위에서 하나씩 ref count를 세줬던 것 처럼, 하나씩 ref count를 세보시면,

Travler의 인스턴스는 traveler.account = account 이 부분을 지나고 나면 메모리에서 해제되게 됩니다.

윗 부분을 꼼꼼하게 읽으셨다면 다 알고 계실거라서 간단하게만 설명하면, travler가 생성됐을 떄 ref count가 1이고, 그 이후에 Account에서 약한 참조로 사용되어서 retain 되지 않고, traveler.account = account 이 코드를 기점으로 더 이상 쓰는 곳이 없기 때문에 release 되어서 ref count가 0이 되었기 때문에 인스턴스가 메모리에서 해제되는 것이죠!

여기까지 이해하셨죠?

 

자 그렇다면 그 다음 줄인 account의 printSummary 함수가 호출되는 상황을 한번 볼까요?

func printSummary() {
    print("\(traveler!.name) has \(points) points")
}

 

해당 함수는 traveler의 name 프로퍼티를 사용하네요.

근데 아까 말씀드렸다싶이 weak로 선언된 프로퍼티는 해당 인스턴스가 메모리에서 사라지게 되면 nil 값이 되어버린다고 했죠?

account의 traveler도 마찬가지로 Traveler 인스턴스가 메모리에서 해제되는 순간부터 nil을 가리키고 있게 됩니다.

저 코드에서는 함수가 traveler을 강제추출하고 있기 때문에 런타임 에러를 발생시키겠네요.

 

만약에 옵셔널 바인딩을 했어도, 우리가 원하는 대로 동작하지 않는 Silent Bug가 될 것입니다.

WWDC 영상에서는 몇가지 해결책을 제시해줍니다.

withExtendedLifetime() 을 이용한 방법, 클래스를 재설계 하는 방법, deinit을 이용하는 방법이 있습니다.

하나씩 살펴봅시다.

 

1) withExtendedLifetime()

사실 이걸 실제 코딩에서 얼마나 쓸지는 모르겠지만! 그래도 알려주니까 말씀을 드리면,

해당 함수는 객체의 수명주기를 연장시켜주는 함수입니다.

 

아까 Traveler 인스턴스가 ref count가 0이 되어서 메모리에서 해제되어버리는 것이 문제였죠?

해당 함수를 사용하면, 컴파일러가 미리 읽고, 저 코드가 끝나기 전까지 해당 객체를 메모리에서 해제시키지 않습니다. 수명을 연장시켜주는 것이죠.

 

그러면 account의 traveler가 nil이 되지 않아서 printSummary 함수는 잘 작동할 수 있습니다.

해당 함수는 다음과 같이도 사용할 수 있습니다. 방금 말씀드린 작동 원리만 알면 다 똑같다는 것을 알 수 있어요!

 

 

근데.. 이건 안쓸 것 같아요!

버그를 땜빵하는 코드는 지양해야하기 때문이죠! 빼먹으면 터지는거니까요.

그리고 굳이 존재해야 할 이유가 없는 코드이기 때문에 다른 방식으로 해결해서 뺄 수 있으면 최대한 빼야 하는 코드인 것 같습니다.

 

2) 클래스 재설계 하기

 

가장 일반적인 방식은 클래스를 재설계하는 방식입니다.

 

지금 보시면 printSummary 메소드가 Traveler로 넘어갔죠?

아까 똑같은 상황을 한번 세어봤기 때문에 아시겠지만, 저 경우에도 문제 없이 작동합니다.

 

그리고 우리가 traveler.account = account 코드 아래로는 Traveler 인스턴스가 메모리에서 해제된다는 것을 알고 있기 때문에, Account의 traveler가 사용되지 않도록 private로 접근 제한을 하였습니다. (nil 값을 가지고 할 일이 딱히 없으니까요!)

 

 

또 다음과 같이 클래스를 개선하는 방법이 있습니다.

 

처음에는 Traveler와 Account가 서로 참조함으로서 관계성을 형성했었는데, 다음과 같이 PersonInfo를 두 객체가 모두 참조함으로서 관계성을 형성했습니다.

Travler에서 필요한 name이라는 항목만 Account도 소유하게 한 것이죠! 이렇게 되면 Traveler와 Account가 상호참조 없이 필요한 정보를 소유할 수 있게 됩니다.

 

'iOS 개발 > Swift' 카테고리의 다른 글

[Swift] Struct와 Class를 메모리 원리부터 자세하게 비교해보자  (2) 2023.02.12
[ARC] 성능을 위해 unowned를 꼭 써야할까?  (0) 2023.02.03
[ARC] Lazy 변수 클로저에서 Unowned 캡처가 항상 안전할까?  (0) 2023.02.02
[Concurrency] Semaphore로 비동기적 이벤트를 동기적으로 발생시키기  (0) 2023.01.20
[Swift] 지정한 For-Loop 탈출하기  (0) 2022.10.03
[Swift] Stride 함수를 사용하자  (0) 2022.10.02
[Swift] appDelegate 참조 안전하게 하기  (0) 2021.11.10
[Swift] lazy 변수란? - 애플개발자 문서가 수정됐다!  (0) 2021.09.06
  • 1. 순환 참조 상황 이해하기
  •  
  •  
  • 2. Weak (약한 참조)
  • 3. Unowned는 Weak와 뭐가 다를까?
  •  
  •  
  • 4. Instance의 수명 관리는 어떻게 해야할까?
  • 1) withExtendedLifetime()
  •  
  • 2) 클래스 재설계 하기
스닥
스닥
https://github.com/feldblume5263

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.