Skip to content

[Feat/#133] Swift6 마이그레이션#135

Open
hooni0918 wants to merge 6 commits intodevelopfrom
feat/#133-swift6migration
Open

[Feat/#133] Swift6 마이그레이션#135
hooni0918 wants to merge 6 commits intodevelopfrom
feat/#133-swift6migration

Conversation

@hooni0918
Copy link
Member

@hooni0918 hooni0918 commented Aug 7, 2025

투어스와 Swift6의 공통점은 첫만남은 너무 어렵다는것이다

🔗 연결된 이슈

📄 작업 내용

Sendable 적용:

  • CalendarRepositoryImpl.swift에서 NetworkService의 thread-safe 처리
  • CalendarUseCase.swift에서 의존성 주입된 repository의 thread-safe 처리
  • ScheduleCategory.swift에서 Enum을 자동으로 Sendable 가능하도록 적용
  • DailySchedule.swift에서 스케줄 배열을 포함한 일일 데이터의 동시성 처리
  • Schedule.swift에서 Swift 6의 데이터 레이스 안전성 요구사항 충족

@mainactor 적용:
CalendarView.swift에서 CalendarStore가 @mainactor로 업데이트되면서 Task 최적화
CalendarStore.swift에서 클래스 레벨에 @mainactor 적용으로 Task 최적화

그외
CalendarModels.swift에서 switch category의 @unknown default 추가로 Swift 6 엄격 모드 요구사항 충족

💻 해결한 오류들

1. Data Race관련

  • 병목점: Sendable 프로토콜 부재로 동시성 안전성 미확보. 액터(Actor) 간 데이터 전달 시 값 타입(struct)은 컴파일 에러를, 참조 타입(class)은 잠재적 데이터 경쟁(Data Race)을 유발하여 앱 안정성을 심각하게 저해.
  • 낭비 요소: 매번 데이터 경쟁을 피하기 위한 방어 코드를 작성하거나, 컴파일 단에서 문제를 잡지 못하고 런타임 크래시로 이어져 디버깅에 막대한 시간을 낭비.

Sendable: Sendable은 동시성 도메인 간에 안전하게 전달될 수 있는 타입을 나타내는 마커 프로토콜.

Swift 6에서는 Task, Actor, async/await 컨텍스트 간 데이터 전달 시 반드시 Sendable 준수를 요구하여 컴파일 타임에 DataRace를 원천 차단.

값 타입(struct, enum)은 모든 저장 프로퍼티가 Sendable이면 자동으로 준수하지만, 참조 타입(class)은 명시적 선언이 필요해졌고. 특히 내부적으로 동기화 메커니즘(NSLock, DispatchQueue 등)을 통해 스레드 안전성을 보장하는 클래스는 @unchecked Sendable을 사용하여 컴파일러에게 안전성을 수동으로 보증해줌

주의사항

  • 클로저 캡처: 비-Sendable 타입을 캡처하는 클로저가 컴파일 오류로 나오는 경우가 생깁니다.
    Actor나 @mainactor를 활용하여 격리된 컨텍스트에서 실행하거나, 캡처할 값을 Sendable 타입으로 변환 후 전달하는 방식으로 해결할수 있음
  • 프로토콜 제약 전파: 프로토콜에 Sendable 제약을 추가하면 모든 구현체가 자동으로 Sendable 준수를 요구받아 일관된 동시성 안전성 확보. 하위 타입까지 연쇄적으로 영향을 미치므로 전체코드를 한번 더 봐봐요~

𝟐. 비효율적인 UI 업데이트 및 불필요한 Actor Hop

  • 병목점: 불필요한 메인 쓰레드 전환(Actor Hop) 남발. UI 업데이트와 무관한 로직까지 Task { @mainactor in ... } 구문으로 감싸거나 개별 메서드에 @mainactor를 붙여 과도한 컨텍스트 스위칭 발생했었습니다
  1. 높은 컨텍스트 스위칭 비용: Actor 경계를 넘는 것은 일반 함수 호출보다 훨씬 비싸다.
  2. 불필요한 재진입: 이미 특정 액터(예: @mainactor)에 있으면서 다시 해당 액터로 전환을 명시하면 불필요한 오버헤드가 발생
  3. UI 성능 저하: 이러한 오버헤드가 누적되면 SwiftUI의 렌더링 루프(View 업데이트)에 영향을 주어 프레임 드랍(버벅임)을 유발

AS-IS

public final class CalendarStore: ObservableObject {
    public func send(_ intent: CalendarIntent) {
//불필요한 Actor Hop. send 메서드는 메인 쓰레드에서 실행될 필요 없음.
        Task { @MainActor in 
            await handle(intent)
        }
    }
    
// 병목점: 개별 메서드마다 @MainActor를 반복 선언하여 코드 가독성 저해.
    @MainActor 
    private func handle(_ intent: CalendarIntent) async { }
}

TO-BE

//클래스 전체를 @MainActor로 격리하여 불필요한 컨텍스트 전환 차단
@MainActor
public final class CalendarStore: ObservableObject {
    public func send(_ intent: CalendarIntent) {

//부모 스코프(@MainActor)를 그대로 사용해 불필요한 Task, @MainActor 선언 제거.
        Task {
            await handle(intent)
        }
    }
    
//클래스 레벨에서 이미 MainActor로 격리되어 어노테이션 불필요.
    private func handle(_ intent: CalendarIntent) async { }
}

최적화 원칙:

  • 클래스 레벨 격리 우선: UI 관련 클래스는 전체를 @mainactor로 격리하여 메서드별 어노테이션 중복 제거. ObservableObject, ViewModel 등이 대표적 대상.
  • nonisolated 활용: 백그라운드 작업이 필요한 특정 메서드만 nonisolated 키워드로 격리 해제. 데이터 페칭, 복잡한 연산 등 UI와 무관한 작업에 적용.
  • Task 생성 최소화: 이미 async 컨텍스트 내부라면 새로운 Task 생성 없이 await만으로 비동기 작업 수행. Task는 새로운 동시성 컨텍스트가 필요할 때만 생성.

4. "nonisolated global shared mutable state" 오류 해결

  • 문제: Static property defaultValue 가 동시성 안전성을 보장하지 못함
    전역 가변 상태(static var)가 여러 스레드에서 동시 접근 시 DataRace 위험으로 컴파일 에러 발생.

Global State Isolation:

Swift 6는 모든 전역 가변 상태에 대해 동시성 안전성을 컴파일 타임에 검증.
static var는 어떤 스레드에서든 접근 가능한 공유 가변 상태로 분류되어 명시적인 동기화 메커니즘 없이는 사용 불가하고 컴파일러는 데이터 경쟁 가능성이 있는 모든 코드를 사전에 차단하여 런타임 크래시 방지.

AS-IS: 동시성 위험이 있는 가변 전역 상태

struct ScrollOffsetPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = 0 // 가변 전역 상태로 데이터 경쟁 위험
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

TO-BE: 불변 상태로 동시성 안전성 확보

struct ScrollOffsetPreferenceKey: PreferenceKey {
    static let defaultValue: CGFloat = 0  // 불변 전역 상태로 스레드 안전 보장
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
  • static let 변환 : PreferenceKey처럼 초기값 설정 후 변경이 없는 경우. SwiftUI 내부에서도 불변값으로 취급하므로 기능상 영향 없음. 가장 간단하고 성능 오버헤드가 전혀 없는 해결책.

📚 참고자료

👀 기타 더 이야기해볼 점

여기 에 남긴것처럼 위 참고자료 내용들 정리해서 마이그레이션 가이드로 남기려 합니다.
생각보다 Concurrency 코드를 나름 잘 짜서인지 규모가 작아서였는지는 모르겟지만 엄청 크리티컬하지않아 다행이네요 다되면 같이 오류 해결해보아요~

✔️ CI Completed

  • ✅ Build: Completed

@hooni0918 hooni0918 requested review from SijongKim93 and k-nh August 7, 2025 14:37
@hooni0918 hooni0918 self-assigned this Aug 7, 2025
@hooni0918 hooni0918 added fix 버그나 오류 해결 시 사용 chore 기타 수정용 infra feature jihoon 나는 지훈 labels Aug 7, 2025
@SijongKim93
Copy link
Collaborator

좋은데?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore 기타 수정용 feature fix 버그나 오류 해결 시 사용 infra jihoon 나는 지훈

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] swift6 마이그레이션

2 participants