싱글톤 패턴에서의 무한 재귀 발생상황

이재웅 (Curry)·2024년 7월 17일
0
post-thumbnail

싱글톤 패턴에서의 무한 재귀 발생상황

  1. 업로드 기능의 API를 싱글톤 패턴으로 간단히 만들고 사용하려 함
  2. 싱글톤 패턴 객체 UploadApi를 구현하기 위해, URLSession 인스턴스 초기화 시 self 를 참조하려 했지만 컴파일 에러 발생
  3. 따라서 URLSession 인스턴스를 생성할 때 self 가 아닌 UploadApi.shared 변수를 참조하도록 구현
class UploadApi: NSObject {
    static let shared = UploadApi()
		
    private init() { 
        super.init()
    }
		
    // self 대신 shared 변수 참조
    private let session: URLSession = URLSession(configuration: .default, delegate: UploadApi.shared, delegateQueue: .current)
		
    func upload(_ data: Data) {
        session.uploadTask....
    }		
}

extension UploadApi: URLSessionTaskDelegate {
    func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
        let uploadProgress = Float(totalBytesSent) / Float(totalBytesExpectedToSend)
        print(uploadProgress)
    }
}
  1. API를 호출했는데 호출시점에서 무한 재귀 발생
func upload(_ data: Data) {
		// Error!! 실행 직후 에러 발생
		UploadApi.shared.upload(data)
}

image

image2

무한 재귀 발생과정

  1. static 변수인 UploadApi.shared 를 사용하는 시점에서 UploadApi 객체 초기화 시작
  2. UploadApi 객체를 초기화 하기 위해 내부 프로퍼티인 session 변수의 초기화 시작
  3. URLSession의 initializer가 실행되면서 init 파라미터로 UploadApi.shared 가 참조됨
  4. 하지만 UploadApi.shared 변수는 현재 초기화 과정중에 있어 해당 변수를 사용하기 위해 다시 URLSession 인스턴스를 참조하게 됨
  5. 그렇게 UploadApi.shared의 init을 위해 URLSession을 참조하고, URLSession의 init은 UploadApi.shared을 참조하게 됨
  6. 무한재귀 발생

Swift에서 static 변수 초기화 방식

Swift는 전역변수 초기화를 싱글톤 패턴과 같은 복잡한 초기화 방식과 퍼포먼스를 위해, lazy한 방식으로 초기화 한다.
(변수를 사용하는 시점에 초기화 하는 방식을 lazy하다고 표현)

lazy하게 초기화 하는 방식은 여러 쓰레드가 동시에 전역변수에 접근했을 때 race-condition을 발생시킬 수 있는데, 이 문제점을 dispatch_once를 통해 atomic하게 실행되어 한번만 초기화 되는 것을 보장한다.

이러한 개념을 통해 나는 static 변수는 반드시 thread-safe하게 초기화를 실시한다고 단편적으로 생각하였고, 전역변수를 초기화할 때 다시 초기화를 위해 대기중인 전역변수를 참조하여 데드락이 발생하게 되었다.

static 변수 초기화 방식과 무한 재귀 발생과 무관하여 제거되었습니다.

해결방안

1. lazy 키워드 사용

class UploadApi: NSObject {
  	static let shared = SomeApi()
	
    private init() {
        super.init()
    }

	// lazy 키워드로 초기화 시점을 미루고 self를 참조함
  	private lazy var session: URLSession = URLSession(configuration: .default, delegate: self, delegateQueue: .current)
	
    func upload(_ data: Data) {
        session.uploadTask....
    }	
}
  • lazy 키워드는 해당 변수가 호출될 때 시점에 초기화를 하도록 변수의 초기화 시점을 미루는 키워드
  • 따라서 UploadApi의 초기화 시점에는 URLSession 인스턴스가 초기화 되지 않기 때문에 shared 변수가 문제없이 초기화됨
  • UploadApi.shared를 참조한 후 UploadApi 인스턴스가 초기화 되고, upload 메소드를 실행시키는 시점에 URLSession 인스턴스가 초기화 되어서 무한 재귀 상황이 발생하지 않음

2. init 내부에서 초기화 시점 조절

class UploadApi: NSObject {
  	static let shared = UploadApi()
	
  	private init() {
  	  	super.init()
		  	
  	  	// super.init이 완료된 이후에 session 초기화
  	  	self.session = URLSession(configuration: .default, delegate: self, delegateQueue: .current)
  	}

  	private var session: URLSession!
	
  	func upload(_ data: Data) {
  	  	session.uploadTask....
  	}	
}
  • 내부 프로퍼티인 URLSession의 타입을 옵셔널 타입으로 선언
  • UploadApi의 super.init()이 실행되기 전에 session 변수가 강제로 초기화 되야 하는 것을 방지하고, super.init()이 완료되어 self를 참조할 수 있게 되었을 때 URLSession 인스턴스를 생성해줌

마무리

위에서 발생했던 상황 이외에도, 여러개의 싱글톤이 각각의 초기화 시점에 다른 싱글톤 객체를 의존하고 있으면 동일하게 무한 재귀 상황이 발생한다.

‘static 변수 == thread-safe’ 라고 단순히 생각하며 개발하기 보단, static 변수가 어떤 시점에 thread-safe한지 정확하게 이해하고 개발하는 것이 중요하다고 생각된다.


참고

  • 유사한 방식의 싱글톤 패턴 무한 재귀 발생 상황

Circular singletons, EXC_BREAKPOINT, shared.unsafeMutableAddressor - Swift Forums

  • 참고문서

Files and Initialization - Swift Blog

profile
iOS 개발자 이재웅입니다.

3개의 댓글

comment-user-thumbnail
2024년 7월 18일

lazy var 없이는 살 수 없게 되어 버렸습니다..

1개의 답글
comment-user-thumbnail
2024년 8월 6일

ts에서는

class Singleton {
    static instance: Singleton
    private constructor() {
         Singleton.instance ??= new Singleton()
    }

    public static create(): Singleton {
         return new Singleton()
     }
}

이렇게 구현하는데 swift는 생성자가 혹시 없나용?

답글 달기