Swift

클로저에서 [weak self] 사용할 때 주의할 점

Phililip
728x90

안녕하세요.

 

클로저 안에서 [weak self]를 쓰는 Swift 코드를 적지 않게 볼 수 있어요.

 

그 이유는 메모리 누수(memory leak)를 막기 위해서인데요,

 

 

만약, 클로저 안에서 [weak self]와 함께 guard-let 구문을 사용해서 접근할 인스턴스를 non-optional로 바꿨을 때, 이건 strong reference일까요 weak reference일까요??

 

그리고 클로저를 중첩(nested)시킨다면???

 

 

이번 글에서는 요런 궁금한 점들을 알아보는 시간을 가져볼게요.

 

 

이후 아래 글도 같이 봐주세요~^^

클로저에서 [weak self] 사용할 때 주의할 점2

 

클로저에서 [weak self] 사용할 때 주의할 점2

안녕하세요. 저번 글에서 [weak self]를 쓸 때 주의할 점에 대해서 알아봤는데요. 클로저에서 [weak self] 사용할 때 주의할 점 클로저에서 [weak self] 사용할 때 주의할 점 안녕하세요. 클로저 안에서 [we

phillip5094.tistory.com

 


 

아래처럼 doSomething method와 doSomethingElse method를 가지는 클래스를 만들어줄게요.

class MyClass {
    var didSomething: Bool = false
    var didSomethingElse: Bool = false
    
    func doSomething(_ completion: (() -> Void)?) {
        completion?()
    }
    
    func doSomethingElse(_ completion: (() -> Void)?) {
        completion?()
    }
    
    deinit {
        print("Deinit")
    }
}

 

그리고 doEverything이란 method를 구현해줄게요.

(이 method는 doSomething method와 doSomeThingElse method를 중첩해서 호출합니다.)

(각 줄마다 Retain Count도 출력해줄게요.)

class MyClass {
	...
    func doEverything() {
        print("start")
        printCounter()
        self.doSomething {
            self.didSomething = true		// strong reference
            print("did something")
            printCounter()	
            
            self.doSomethingElse {
                self.didSomethingElse = true	// strong reference
                print("did something else")
                printCounter()	
            }
        }
        print("Finish")
        printCounter()
    }
    
    func printCounter() {
        print(CFGetRetainCount(self))
    }
}

 

doSomething 클로저와 doSomethingElse 클로저 안에서 인스턴스 프로퍼티에 접근하기 때문에 strong reference가 걸릴 걸로 예상됩니다.

 

 

그럼 MyClass 인스턴스를 만들고 doEverything method를 호출해볼까요??

(아래는 전체 코드입니다.)

class MyClass {
    var didSomething: Bool = false
    var didSomethingElse: Bool = false
    
    func doSomething(_ completion: (() -> Void)?) {
        completion?()
    }
    
    func doSomethingElse(_ completion: (() -> Void)?) {
        completion?()
    }
    
    func doEverything() {
        print("start")
        printCounter()
        self.doSomething {
            self.didSomething = true		// strong reference
            print("did something")
            self.printCounter()	
            
            self.doSomethingElse {
                self.didSomethingElse = true	// strong reference
                print("did something else")
                self.printCounter()	
            }
        }
        print("Finish")
        printCounter()
    }
    
    func printCounter() {
        print(CFGetRetainCount(self))
    }
    
    deinit {
        print("Deinit")
    }
}

do {
    let model = MyClass()
    model.doEverything()
}

(scope를 지정해주기 위해서 do로 감싸줬어요. scope를 벗어났을 때 de-initializer가 호출되야지 메모리 관리가 잘된 것이겠죠??ㅎㅎ)

 

 

출력된 로그를 볼까요???

// output
start
2			// MARK 1
did something
4			// MARK 2
did something else
6			// MARK 3
Finish
2			// MARK 4
Deinit			// MARK 5

 

 

 

로그를 토대로 생각해보면 아래처럼 해석이 가능합니다.

MARK 1:  인스턴스 생성 후 첫 retain count 값 = 2
MARK 2:  didSomething 프로퍼티에 접근하자 retain count가 2 -> 4로 증가 (strong reference 걸림)
MARK 3:  didSomethingElse 프로퍼티에 접근하자 retain count가 4 -> 6으로 증가 (strong reference 걸림)
MARK 4:  모든 클로저가 종료되니 strong reference가 끊어지고 초기값 2로 돌아옴
MARK 5:  do scope를 벗어나서 인스턴스를 참조하는 곳이 없어졌기 때문에 de-initializer 호출됨.

 

여기서 한 가지 의문점이 있는데,

클로저 안에서 strong reference가 걸리면 2 -> 3으로 증가해야 하는 것 아닌가요........????
왜 2 -> 4로 증가했는지는 잘 모르겠어요... 아시는 분은 알려주세요ㅠㅠㅠ

CFGetRetainCount(self)로 인해 내부적으로 인스턴스에 참조가 걸려서 2 -> 3 -> 4로 증가한 것인가 싶기도 하고.....;;;
일단 retain count가 증가했다는 것 자체에 의의를 가질게요.. 허헣...😢 😢

 

클로저가 끝나고 scope를 벗어나니 메모리 해제가 된 것을 볼 수 있어요.

 

 

 

[weak self]를 활용하면 strong reference 대신 weak reference를 걸 수 있습니다.

func doEverything() {
    print("start")
    printCounter()
    self.doSomething { [weak self] in  ✅
        self?.didSomething = true
        print("did something")
        self?.printCounter()

        self?.doSomethingElse { [weak self] in  ✅
            self?.didSomethingElse = true
            print("did something else")
            self?.printCounter()
        }
    }
    print("Finish")
    printCounter()
}
// output
start
2
did something
3
did something else
4
Finish
2
Deinit

(weak reference 임에도 retain count가 2-> 3으로 증가한 건 CFGetRetainCount(self) 때문인 것 같아요.... 틀렸다면 알려주세요ㅠㅠ)

 

 

 

근데 지금은 클로저가 중첩되어 있죠??

 

이런 경우에는 가장 바깥 클로저에서만 [weak self]를 선언해주면 내부 클로저에서도 공통으로 사용할 수 있습니다ㅎㅎ

func doEverything() {
    print("start")
    printCounter()
    self.doSomething { [weak self] in
        self?.didSomething = true
        print("did something")
        self?.printCounter()

        self?.doSomethingElse {
            self?.didSomethingElse = true
            print("did something else")
            self?.printCounter()
        }
    }
    print("Finish")
    printCounter()
}
// output
start
2
did something
3
did something else
4
Finish
2
Deinit

(중복으로 선언 안 해도 결과는 동일합니다ㅎㅎ)

 

 

 

만약 [weak self]와 함께 guard-let 구문도 같이 사용한다면??

func doEverything() {
    print("start")
    printCounter()
    self.doSomething { [weak self] in
        guard let self = self else { return }  ✅
        self.didSomething = true
        print("did something")
        self.printCounter()

        self.doSomethingElse {
            self.didSomethingElse = true
            print("did something else")
            self.printCounter()
        }
    }
    printCounter()
}
// output
start
2
did something
3
did something else
5 		🤔
2
Deinit

 

[weak self]만 사용했을 때와 guard-let 구문도 같이 사용했을 때의 차이점은 doSomethingElse 클로저에 있습니다.

 

didSomethingElse 프로퍼티에 접근하니 retain count가 4 -> 5로 증가했죠??

 

이 말은,doSomething 클로저에서는 weak reference가 걸렸지만, doSomethingElse 클로저에서는 strong reference가 걸렸다는 의미입니다.

 

 

 

만약 doSomething 클로저에서 약한 참조가 걸렸으니 내부 클로저인 doSomethingElse에서도 약한 참조가 걸릴 것이다!! 라고 오해를 한다면 strong reference cycle이 걸릴 수 있는 부분이니 주의해주세요!!

 

 

 

 

코드 중에 중첩된 클로저가 많다면 리팩토링하는 것을 권장하고,

 

만약 [weak self]와 guard-let 구문을 같이 사용해야겠다!! 라고 한다면, 내부 클로저에서는 약한 참조가 걸린 self?를 사용하는 것이 안전합니다.

func doEveryThing() {
    print("start")
    printCounter()
    self.doSomething { [weak self] in
        guard let strongSelf = self else { return }  ✅
        strongSelf.didSomething = true
        print("did something")
        strongSelf.printCounter()

        strongSelf.doSomethingElse {
            self?.didSomethingElse = true  ✅
            print("did something else")
            self?.printCounter()
        }
    }
    printCounter()
}

 

 

 

## 참고

- https://benoitpasquier.com/weak-self-story-memory-management-closure-swift/

 

Weak self, a story about memory management and closure in Swift

Memory management is a big topic in Swift and iOS development. If there are plenty of tutorials explaining when to use weak self with closure, here is a short story when memory leaks can still happen with it.

benoitpasquier.com

 

 


 

이번 글은 여기서 마무리.

 

 

 

반응형

'Swift' 카테고리의 다른 글

클로저에서 [weak self] 사용할 때 주의할 점3  (0) 2022.05.21
클로저에서 [weak self] 사용할 때 주의할 점2  (0) 2022.04.24
Automatic Reference Counting (ARC)  (0) 2022.04.10
subscript  (0) 2022.04.03
Struct와 Class  (0) 2022.04.02