Tunko Development Diary

Swift ARC [weak self] 란? 본문

Development/iOS 개발

Swift ARC [weak self] 란?

Tunko 2022. 6. 15. 20:48

ARC(Automatic Reference Counting) 을 통해서 대부분의 문제를 해결했지만 Velue Type 이 아닌 Reference type 에서는 Retain Count를 관리해야 합니다.

Swift 코딩중에 클로저와 같은 것을 사용하게 되면 종종 [weak self] 가필요합니다. 이게 왜 필요한지 차근차근 알아보려고 합니다.

ARC란?

Automatic Reference Counting - The Swift Programming Language (Swift 5.7)

Swift에서 우리는 코드의 관계 사이에 필요한 정보를 ARC에서 제공하기 위해 weak self 와 unowned self를 사용해야 합니다. weak self 와 unowned self를 사용하지 않으면 기본적으로 strong reference (강한 참조) 상태로 메모리상에 유지되며 ARC를 통해 자동적으로 할당된 메모리가 해지되지 않게 됩니다. 즉, 메모리 누수가 발생합니다.

참조 카운트는 클래스 인스턴스에서만 적용됩니다. 구조 및 열거형등 값타입에는 적용되지 않습니다.

weak self 는 언제 사용해야 될까?

우선 약한 참조는 nil 참조가 할당 해제될 때 ARC에 의해 자동으로 설정될 수 있으므로 항상 선택적 변수로 선언됩니다. 다음은 예시입니다.

class Blog {
    let name: String
    let url: URL
    var owner: Blogger?

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \\(name) is being deinitialized")
    }
}

class Blogger {
    let name: String
    var blog: Blog?

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

    deinit {
        print("Blogger \\(name) is being deinitialized")
    }
}

deinit을 통해 메모리가 해지되면 print메시지가 출력됩니다.

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog = nil
blogger = nil

// Nothing is printed

위 코드를 보면 서로가 서로를 가지고있습니다. nil을 할당했다고해도 계속해서 참조카운트가 남아있게되고 메모리를 잡고있습니다. 따라서 약한 참조를 도입합니다.

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \\(name) is being deinitialized")
    }
}

class Blogger {
    let name: String
    var blog: Blog?

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

    deinit {
        print("Blogger \\(name) is being deinitialized")
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog = nil
blogger = nil

// Blogger Antoine van der Lee is being deinitialized
// Blog SwiftLee is being deinitialized

weak var owner: Blogger?

이렇게 하므로서 blog 와 blogger에 nil 을 넣어주면 deinit이 되는것을 확인할 수 있습니다.

다음은 blog에 Post 구조체를 도입하겠습니다

struct Post {
    let title: String
    var isPublished: Bool = false

    init(title: String) { self.title = title }
}

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    var publishedPosts: [Post] = []

    init(name: String, url: URL) { self.name = name; self.url = url }

    deinit {
        print("Blog \\(name) is being deinitialized")
    }

    func publish(post: Post) {
        /// Faking a network request with this delay:
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.publishedPosts.append(post)
            print("Published post count is now: \\(self.publishedPosts.count)")
        }
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil

출력

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1
// Blog SwiftLee is being deinitialized

publish메소드에 DispatchQueue.main.asyncAfter 를 통해 약간의 딜레이를 주었습니다. 네트워크 요청을 가정한 딜레이 입니다.

출력된 로그를 보면 blog가 deinit되기전에 DispatchQueue.main.asyncAfter 내부 코드가 실행된것을 알 수 있습니다. 즉, publishedPosts 배열에 post가 하나 들어가게 됩니다. blog에는 nil을 할당하여 메모리 해지를 했는데도 말이죠.

그럼 이를 수정해보겠습니다 weak self 를 통해서 말이죠.

func publish(post: Post) {
    /// Faking a network request with this delay:
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
        self?.publishedPosts.append(post)
        print("Published post count is now: \\(self?.publishedPosts.count)")
    }
}

출력

// Blogger Antoine van der Lee is being deinitialized
// Blog SwiftLee is being deinitialized
// Published post count is now: nil

publish 요청이 완료되기 전에 blog에 nil이 할당되어 메모리해지 되었습니다.

게시된 게시물의 로컬 상태를 업데이트 할 수 없습니다. “Published post count is now: nil” 이 나온 이유입니다.

결론

클로저가 실행되는 즉시 참조 인스턴스르 수행할 작업이 있는 경우weak self를 사용하지 않도록 해야 합니다.

클로저에서 Weak references 유지 주기

class Blog {
    let name: String
    let url: URL
    weak var owner: Blogger?

    var publishedPosts: [Post] = []
    var onPublish: ((_ post: Post) -> Void)?

    init(name: String, url: URL) {
        self.name = name
        self.url = url

        // Adding a closure instead to handle published posts
        onPublish = { post in
            self.publishedPosts.append(post)
            print("Published post count is now: \\(self.publishedPosts.count)")
        }
    }

    deinit {
        print("Blog \\(name) is being deinitialized")
    }

    func publish(post: Post) {
        /// Faking a network request with this delay:
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.onPublish?(post)
        }
    }
}

var blog: Blog? = Blog(name: "SwiftLee", url: URL(string: "www.avanderlee.com")!)
var blogger: Blogger? = Blogger(name: "Antoine van der Lee")

blog!.owner = blogger
blogger!.blog = blog

blog!.publish(post: Post(title: "Explaining weak and unowned self"))
blog = nil
blogger = nil

출력

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: 1

blog 에 nil 이 할당되었습니다.

따라서 print("Blog \(name) is being deinitialized") 문구가 출력되어야 하지만 나오지 않습니다.

하지만 blogger에 nil 할당했고 정상적으로 메모리 해지 된것을 확인할 수 있습니다.

이 상황에서 weak self 를 도입하면 참조 유지되는 현상이 해결됩니다.

onPublish = { [weak self] post in
    self?.publishedPosts.append(post)
    print("Published post count is now: \\(self?.publishedPosts.count)")
}

출력

// Blogger Antoine van der Lee is being deinitialized
// Published post count is now: Optional(1)
// Blog SwiftLee is being deinitialized

참고 :

Weak self and unowned self explained in Swift

반응형
Comments