본문 바로가기
프로젝트/수수-경조사비 관리 서비스

[수수] Swift 6.0 마이그레이션 후기

by 마라민초닭발로제 2024. 10. 4.

안녕하세요 수수 어플리케이션 iOS개발자 정다함 입니다. 이번 2024-09-17기준으로 Swift6.0이 업데이트 되었습니다. 당장은 Swift 5버전으로 어플리케이션을 배포하고 유지보수해도 상관 없지만, 언젠가는 해야하는 일이라고 생각했습니다. 그래서 이번 기회에 마이그레이션을 진행해 보았습니다. 

 

글은 Swift 6.0 마이그레이션을 하면서 정리했던 생각과, 왜 이런 코드를 작성했는지에 대한 흐름을 보여줄 예정입니다. 

 

1. 싱글톤 객체를 Sendable로

프로젝트에서 사용하는 싱글톤 객체를 컴파일 하면 다음과 같은 에러가 발생합니다. 로컬 캐시나 프로젝트에서 쓰는 모듈을 싱글톤을 활용한 경우가 있는데 이들을 모두 수정해주어야 합니다. 

 

Static property 'shared' is not concurrency-safe because non-'Sendable' type 'MyPageRouterAndPathPublisher' may have shared mutable state

 

처음 이 문제를 접근할 때 싱글톤 객체를 어디서 참조하고 활용할지에 대해서 생각해보았습니다. 그리고, 싱글톤 객체는 Concurrency에 무관해야 한다는 결론을 내렸습니다. 어느Concurrency생성되는 것 그리고 mutable 한 것도 SingleTone의 몫이라고 생각했고, 이것을 특정 Concurrency의 강제한다는 것은 설계의 문제가 된다고 생각했습니다.

실제 예시를 통해서 설명을 드리겠습니다. 

 

import Combine
import Foundation

final class MyPageRouterAndPathPublisher {
  private static let shared = MyPageRouterAndPathPublisher() // ❌ Error occured
  private init() {}
  private var _pathPublisher: PassthroughSubject<MyPageNavigationPath.State, Never> = .init()
  static var pathPublisher: AnyPublisher<MyPageNavigationPath.State, Never> {
    shared._pathPublisher.receive(on: RunLoop.main).eraseToAnyPublisher()
  }
  
  static func push(_ pathState: MyPageNavigationPath.State) {
    shared._pathPublisher.send(pathState)
  }
}

 

위 코드는 실제 SUSU어플리케이션의 Navigation을 담당하는 SIngleTone객체 입니다. 코드에서 보는 것과 같이 단순하게 NaivgationPath.State을 옮김으로서 화면전환을 도와주고 있습니다. 싱글톤 객체가 어디서 활용하고 접근간에 아무 문제가 없습니다. 심지어 MainThread가 아닌 곳에서 다른 쓰레드로 화면전환이 일어나도 publihser를 구독하는쪽에서 MainThread에서 값을 받을 수 있도록 디자인 했습니다. 이처럼 SingleTone객체에서 Concurrency컨트롤이 어색하게 느껴졌고, singletone instance를 nonisolated(unsafe)로 선언하여습니다.

final class MyPageRouterAndPathPublisher {
  private nonisolated(unsafe) static let shared = MyPageRouterAndPathPublisher()
  private init() {}
  // code ...

 

 

2. Identifiable<ID>은 Sendable을 채택하지 않습니다. 

개발하면서 identifiable에 대한 문서를 읽어보지 않았습니다. Identifiable의 associatedtype인 ID는 Hashable입니다. Hashable은 sendable을 채택하지 않습니다. 수수에서는 Protocol과 제너릭 둘을 활용한 경우가 있었습니다. 이럴 경우Property에 Sendable채택이 불가합니다. 저는 Item.ID가 자동으로 Int 타입이 되는줄 알았지만(protoocl에서 정의했기 때문에) 그렇지 않았습니다. 

public struct SingleSelectButtonProperty<Item: SingleSelectButtonItemable>: Equatable, @unchecked Sendable {
  public var items: [Item]
  public let initialSelectedID: Item.ID? // ❌ Cant use sendable 
  //code...
  
public protocol SingleSelectButtonItemable: Identifiable, Equatable, Sendable {
  var id: Int { get }
  var title: String { get set }
}

 

그래서 protocol where절을 활용하여 Item.ID타입의 Int를 명확시 하는 방법을 활용했습니다. 이렇게 만들어준다면, SingleSelectButtonProperty가 자동으로 Sendable을 채택할 수있게 변합니다. 

public protocol SingleSelectButtonItemable: Identifiable, Equatable, Sendable where ID == Int {
  var id: Int { get }
  var title: String { get set }
}

 

 

3. @retroactive

다른 모듈에 있는 코드를 가져다가 Protocol을 추가하여 확장하는 경우들이 있습니다. 범주로서 다른 모듈의 Strcut부터 Foundation의 객체 까지 넓게 생각할 수 있습니다. 만약 extension으로 확장하였고, 버전이 올라가면서 원본 객체가 내가 선언한 protocol을 준수하는 경우에는 protocol을 두번 준수하게 되는 불상사가 발생합니다.

이럴 경우를 방지하기 위해  @retoractive를 활용한 protocol을 선언하여 버전업 이후에 발생하는 에러를 즉각적으로 알아 차릴 수 있게 합니다.

수수에서도 몇몇 객체들이 비슷한 양상을 띄었는데, swift compiler가 이를 warnning으로 제출해서 쉽게 코드를 수정할 수 있었습니다. 

 

extension CreateEnvelopeCategoryProperty: @retroactive SSSelectableItemable {
  public var title: String {
    get { name }
    set { name = newValue }
  }
}

 

리팩토링 완료후 느낀점

Swift6.0을 마이그레이션 하기 위해 공부도 했고, Sendable을 어떻게 채택하는지에 대해서 깊게 공부할 수 있었습니다. Swift라는 언어가 현재는 iOS플래폼에 국한되어 있지만, 점점 더 활용 범위를 넓히기 위해 발돋움 하는 것 처럼 보였습니다. 서버쪽에서도 Swift언어의 지분을 높힐 수 있도록 기대해도 될까 라는 생각을 하면서 글을 마치려고 합니다. 감사합니다.