본문 바로가기
IOS App Programming/Swift

weak self 를 사용하지 않아도 되는 경우

by B_Tori 2024. 2. 7.

결론적으로 클로저에서 weak self를 사용하지 않아도 되는 경우는 다음과 같은 두 가지가 있다.

1. non-escaping 클로저인 경우
2. DispatchQueue.main.asyncAfter 함수를 사용할 경우

 

escaping 클로저와 non-escaping 클로저

함수 파라미터로 전달된 클로저를 함수 실행이 종료된 후 실행되는 것을 함수를 탈출(escape)한다고 한다.

즉 escaping클로저는 파라미터로 전달된 클로저가 탈출하여 함수 종료 후에 실행되는 클로저를 의미한다.

  escaping non-escaping
실행 시점 함수 종료 후 함수 내에서 코드 호출 즉시
외부 저장 여부 가능 불가능

 

두 함수의 차이는 외부 저장 여부에 대해서도 다르다.

non - escaping 클로저는 함수 외부 변수(or 상수)에 저장하려고 하면 컴파일 에러가 발생한다.

함수 내부에서만 사용되고 말기 때문에 외부 변수로 탈출시킬 수 없기 때문이다.

 

반대로 escaping 함수는 함수 밖으로 탈출 가능하기 때문에 함수 외부의 변수(or 상수)에 저장이 가능하다.

 

escaping과 weak self의 상관관계

그렇다면 escaping이 weak self와 무슨 관련일까?

non-escaping은 함수 밖으로 탈출하지 못한다는 특징을 가지고 있다.

그렇기 때문에 함수 호출이 끝나면 해당 클로저는 더 이상 사용되지 않음을 보장한다.

따라서 자동으로 클로저를 메모리에서 해제시킨다

 

다음과 같은 상황이 있다 하자.

인스턴스 내부에 있는 클로저에서 self를 참조하고 있다.

non-escaping 클로저는 클로저가 함수범위를 벗어날 일이 없음을 보장하기 때문에 클로저 실행 시 자동으로 메모리에서 해제되어 self의 strong reference를 증가시키지 않는다.

하지만 escaping클로저의 경우 함수 밖으로 벗어날 수 있기 때문에 reference count를 증가시킬 수 있다.

만약에 위의 그림처럼 서로의 RC를 증가시킨 경우

 

다음과 같은 RC 구조를 가지게 된다.

만약 인스턴스 변수에 nil을 할당하게 될 경우

  1. 인스턴스 변수가 참조하고 있던 RC 가 사라져 -1 된다 → 인스턴스의 RC = 1
  2. 클로저에서는 self의 강한 참조로 인해 self(인스턴스)가 메모리에서 해제될 때까지 기다림
  3. 인스턴스는 클로저에서 참조하는 RC가 남아있어 메모리에서 해제되지 못함

이처럼 순환참조를 발생시키게 된다.

예제 코드

// non - escaping 클로저 사용
class Class1 {

    func format(_ value: Int) -> String { return String(value) }

    func test() {
				// 대표적인 고차함수들은 non-escaping
        let formatted = [1,2,3,4,5].map { num in
            self.format(num)
        }
        print(formatted)
    }
    
    deinit {
        print("Class1 deinit")
    }
}

var class1: Class1? = Class1()
class1?.test()
class1 = nil

// escaping 클로저 사용
class Class2 {
    var handler: ((Int) -> Void)? = nil
    func format(_ value: Int) -> String { return String(value) }
    
    func test() {
        handler = { value in
                    let formatted = self.format(value)
                    print(formatted as Any)
                }
    }
    
    deinit {
        print("Class2 deinit")
    }
}

var class2: Class2? = Class2()
class2?.test()
class2 = nil

class2는 deinit 되지 않은 모습을 볼 수 있다.

handler = { [weak self] value in
                    let formatted = self?.format(value)
                    print(formatted as Any)
                }

하지만 Class2에 weak self를 붙이게 되면 deinit 되는 걸 확인할 수 있다.

DispatchQueue.main.asyncAfter

해당 함수는 escaping 클로저로 전달해 강한 참조를 발생하지만

deadline이 존재하여 deadline동안만 메모리에서 유지함

즉 deadline이 끝나면 클로저가 메모리에서 해제됨으로 순환참조가 발생하지 않는다.

그런데 여기서 주의할 점이 있다.

순환참조가 발생하지 않아 weak self를 사용하지 않아도 되는 데 사용할 경우

생각한 동작과 다르게 동작할 수 있다.

class Class2 {

    func format(_ value: Int) -> String { return String(value) }

    func test() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            let formatted = self.format(10)
            print("formatted: \\(formatted as Any)")
        }
    }
    
    deinit {
        print("Class2 deinit")
    }
}

class Class1 {
    let class2: Class2

    init() {
        class2 = Class2()
    }

    func someEvent() {
        class2.test()
    }

    deinit {
        print("Class1 deinit")
    }
}

var class1: Class1? = Class1()
class1?.someEvent()
class1 = nil

Weak self에 따른 실행 차이

왼: weak self 사용 X, 오: weak self 사용 O

  • weak self 사용 : class1이 deinit 되고 1초 뒤 format함수가 실행된 뒤 자동으로 class2가 deinit 된다.
  • weak self 사용 X: 바로 1,2 둘 다 deinit 되고 1초 뒤 test함수가 실행은 되지만 nil을 반환한다.

weak self를 쓰지 않으면 이와 같은 메모리 상황에서

class 1이 메모리에서 사라져도 class2의 rc = 1 이기 때문에 사라지지 않아

DispatchQueue.main.asyncAfter에서 self를 참조할 수 있다.

그리고 deadline 후 클로저는 자동으로 메모리에서 해제되면서 self 참조도 사라진다.

즉 클로저 참조가 사라지면서 class 2의 RC도 0이 되므로 class도 메모리에서 해제된다.

 

반대로 여기서 클로저의 참조가 weak가 될 경우 class2의 RC를 증가시키지 않는다.

즉 처음부터 RC = 1이고 class1이 nil이 되면 RC가 0이 되어 class2가 메모리에서 사라진다. (deinit 호출)

즉 DispatchQueue.main.asyncAfter 안의 내용이 실행될 때는 이미 self가 nil 이기 때문에 출력이 이상하게 나왔던 것이다.

 

func test() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
            print(self)
            let formatted = self?.format(10)
            print("formatted: \\(formatted as Any)")
        }
    }

이와 같이 weak self를 쓰고 self를 프린트해 보면 nil이 찍히는 모습을 알 수 있다.

3번줄이 self 출력 줄

 

 

이번 공부로 인해 단순히 클로저를 쓴다고 해서 weak self를 사용하면 안 된다는  사실을 알 수 있었다.

특히 DispatchQueue.main.asyncAfter의 경우는 실행 결과가 다르게 나올 수 있다는 사실에 놀랐다.

클로저들의 특성 및 생명주기를 잘 이해하고 weak self를 써야 한다는 사실을 알 수 있었다.

댓글