주의! 애플 코드에 조심해야할 강한 참조가 있어요.

whitelips
a day of a programmer
11 min readJul 3, 2024

Swift 학습을 할 때에 빠지지 않는 주제였던 강한 참조와 약한 참조가 있을텐데요. delegate 패턴은 약한 참조의 대표 사례입니다. 그런데 애플 코드에 delegate 이지만 강한 참조가 있습니다.

created by DALL-E

오늘은 가볍고 재미있는 Swift 이야기 입니다. Swift 로 앱을 개발할 때에 유명하고 편리한 네트워크 오픈소스가 많은데요. 따라서 다음 문제를 직접 겪거나 고민해본 경험은 적을 것이에요.

😱 URLSessionDelegate

먼저, URLSession 의 헤더를 열어봅시다.

@available(iOS 7.0, *)
open class URLSession : NSObject, @unchecked Sendable {


open class var shared: URLSession { get }


public /*not inherited*/ init(configuration: URLSessionConfiguration)

public /*not inherited*/ init(configuration: URLSessionConfiguration, delegate: (any URLSessionDelegate)?, delegateQueue queue: OperationQueue?)


open var delegateQueue: OperationQueue { get }

open var delegate: (any URLSessionDelegate)? { get }

@NSCopying open var configuration: URLSessionConfiguration { get }

// blahbla
}

찾으셨나요? delegate 선언에 weak이 없습니다. 어라? 우리가 잘못보고 있는 것일까요? Objective-C 코드의 헤더도 함께 살펴봅시다.

@interface NSURLSession : NSObject

/*
* The shared session uses the currently set global NSURLCache,
* NSHTTPCookieStorage and NSURLCredentialStorage objects.
*/
@property (class, readonly, strong) NSURLSession *sharedSession;

/*
* Customization of NSURLSession occurs during creation of a new session.
* If you only need to use the convenience routines with custom
* configuration options it is not necessary to specify a delegate.
* If you do specify a delegate, the delegate will be retained until after
* the delegate has been sent the URLSession:didBecomeInvalidWithError: message.
*/
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;

@property (readonly, retain) NSOperationQueue *delegateQueue;
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
@property (readonly, copy) NSURLSessionConfiguration *configuration;

// blahblah

여기에도 delegate 프로퍼티의 속성에 retain이 있습니다.

애플이 왜?

Swift 언어를 개발하고 배포하는 회사인 애플이 틀린 걸까요? 혹시 실수를 했을까요? 그렇지는 않습니다.

사실 델리게이트에 대한 quick help를 보아도 설명이 되어있고, 개발자 문서에도 자세히 나와있습니다.

The session object keeps a strong reference to this delegate until your app exits or explicitly invalidates the session. If you do not invalidate the session, your app leaks memory until it exits.

특별한 목적을 가지고 만든 코드입니다. 그런데 자세히 살펴보지 않으면 초보자가 아니어도 쉽게 실수할만한 내용입니다. 늘 하던대로 delegate 코드를 구현하고 assign 한다면요. 가이드에는 invalidate 호출을 이야기하지만, 이 사례처럼 개발자가 고칠 수 없는 부분에 강한 참조 코드를 약한 참조로 사용하고 싶다면 어떻게 해야할까요? 간단합니다.

struct WeakContainer<Object: AnyObject> {
weak var object: Object?
}

예시는 struct 을 사용했지만, 필요에 따라 class 로 바꾸어써도 좋습니다. URLSession의 예시로 돌아가서 코드를 더 만들어볼게요.

class WeakURLSessionDelegate: NSObject, URLSessionDelegate {
weak var delegate: URLSessionDelegate?

func urlSession(_ session: URLSession, didBecomeInvalidWithError error: (any Error)?) {
delegate?.urlSession(session, didBecomeInvalidWithError: error)
}

// blahblah
}

class Foo: URLSessionDelegate {

let session: URLSession

func foo() {
session.delegate = WeakURLSessionDelegate(delegate: self)
}
}

짜잔! 해결했습니다. 이제 session.delegate 에 약한 참조를 전달했어요.

그런데 웹뷰 말입니다.

이렇게 조심해야할 내용이 하나가 아닙니다. WKWebView 에서 웹과 앱이 통신을 위해 사용하는 WKUserContentController 에도 놓치기 쉬운 강한 참조 사용이 있습니다.

func add(_ scriptMessageHandler: any WKScriptMessageHandler, name: String)

바로 위에 보이는 함수인데요. 여기는 개발자 문서의 설명에도 특별한 참조에 대한 내용이 없습니다.

하지만, 여러분이 일하는 팀에서 WKScriptMessageHandler 개발을 해본 사람은 경험적으로 알고 있습니다. 그래서 앞서 설명한 weak property를 가지는 class 나 struct 를 작성해서 해결해왔을 겁니다.

왜 이런지 궁금하지 않은가요? 해결책이 있으니 괜찮다고요? 그래도 한번 열어보겠습니다.

먼저 WebKit 오픈 소스에서 WKUserContentController.mm을 열어봅니다.

- (void)_addScriptMessageHandler:(WebKit::WebScriptMessageHandler&)scriptMessageHandler
{
if (!_userContentControllerProxy->addUserScriptMessageHandler(scriptMessageHandler))
[NSException raise:NSInvalidArgumentException format:@"Attempt to add script message handler with name '%@' when one already exists.", (NSString *)scriptMessageHandler.name()];
}

- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name
{
auto handler = WebKit::WebScriptMessageHandler::create(makeUnique<ScriptMessageHandlerDelegate>(self, scriptMessageHandler, name), name, API::ContentWorld::pageContentWorld());
[self _addScriptMessageHandler:handler.get()];
}

함수 이름을 알고 있으니 쉽게 찾을 수 있습니다. 다음을 따라가볼게요. WebUserContentControllerProxy.cpp 를 열어보아요.

bool WebUserContentControllerProxy::addUserScriptMessageHandler(WebScriptMessageHandler& handler)
{
auto& world = handler.world();

for (auto& existingHandler : m_scriptMessageHandlers.values()) {
if (existingHandler->name() == handler.name() && existingHandler->world().identifier() == world.identifier())
return false;
}

addContentWorld(world);

m_scriptMessageHandlers.add(handler.identifier(), &handler);

for (auto& process : m_processes)
process.send(Messages::WebUserContentController::AddUserScriptMessageHandlers({ { handler.identifier(), world.identifier(), handler.name() } }), identifier());

return true;
}

주목해야할 코드 라인은 m_scriptMessageHandlers.add(handler.identifier(), &handler); 입니다. HashMap<uint64_t, RefPtr<WebScriptMessageHandler>> m_scriptMessageHandlers; 형태로 해시맵으로 구현되어 있습니다. 흔히 아는 key-value 저장 형태이며, value에 RefPtr로 선언되어 강한 참조 형태로 사용됩니다. 약한 참조의 해시맵은 WeakHashMap 으로 별도로 구현되어 있습니다. 잠시 c++ 의 세계였는데요. Swift 세계로 빗대어 설명하면 Dictionary를 사용하고 있는 셈입니다.

마치며

어떤가요? 오늘은 애플이 제공하는 API여도 세심하게 사용하지 않으면 메모리 누수를 만들 수 있는 코드를 살펴보았습니다. 익숙한 API 여도 자세히 살펴보지 않으면 문제 상황을 만나기 이전까지는 모를 수 있는 내용들입니다. 개발 의도를 알게 되어 해결하는 경우도 있겠지만, 웹뷰 API 처럼 내부 구현이 만들어내는 한계점일 때도 있습니다.

iOS 면접 준비나 질문 사례에서 자주 볼 수 있는 강한 참조와 약한 참조에 대한 내용이 아쉬웠다면, 이렇게 변경할 수 없는 코드의 강한 참조를 해결할 방법에 대한 고민을 더해보세요. 조금 더 이야기할 거리가 많은 재미난 과정이 될 수 있습니다.

--

--

whitelips
a day of a programmer

Software Engineer with 10+ years in iOS, focusing on performance optimization, modularization, and innovative solutions. Proven leader in major tech projects.