<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Model in Spring</title>
    <link>https://modelinspring.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Sat, 4 Jul 2026 07:35:43 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>마들브라더</managingEditor>
    <item>
      <title>[Swift/iOS] DispatchQueue와 Task의 차이 &amp;mdash; 두 AI와 대화로 정리한 Swift 동시성</title>
      <link>https://modelinspring.tistory.com/101</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;회사에서 &lt;code&gt;DispatchQueue&lt;/code&gt;와 &lt;code&gt;Task&lt;/code&gt;에 대해 이야기하다가 의문이 생겼다. 신규 코드에서는 &lt;code&gt;DispatchQueue&lt;/code&gt;를 쓸 일이 더 이상 없는 걸까. 처음에는 Claude와 대화하면서 개념을 정리해 보았고, 그 결과를 다시 Gemini에게 리뷰받으며 한 번 더 검증했다. 그 다음 다시 또 Claude와 이야기를 이어갔다. 두 AI가 서로 다른 관점에서 짚어준 부분이 있었고, 그 과정에서 처음에는 보이지 않았던 함정과 미묘한 결론에 도달하게 되었다. 그 모든 과정을 시간 순서대로 정리해 본다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Claude와의 첫 대화: 기본 개념 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 Claude에게 &lt;code&gt;DispatchQueue&lt;/code&gt;와 &lt;code&gt;Task&lt;/code&gt;가 어떻게 다른지 물었다. 답변을 정리하면 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한 줄 요약&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;DispatchQueue&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;등장&lt;/td&gt;
&lt;td&gt;GCD (오래된 방식)&lt;/td&gt;
&lt;td&gt;Swift Concurrency (Swift 5.5+)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;패러다임&lt;/td&gt;
&lt;td&gt;스레드 기반&lt;/td&gt;
&lt;td&gt;구조적 동시성 (async/await)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;DispatchQueue&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GCD(Grand Central Dispatch) 기반으로, 스레드 풀을 직접 관리하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DispatchQueue.global().async {
    let result = heavyWork()

    DispatchQueue.main.async {
        self.label.text = result  // 메인 스레드로 복귀
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식의 특징을 보면, 클로저 기반이라 콜백 중첩이 생기기 쉬워서 흔히 말하는 &quot;콜백 지옥&quot;에 빠질 수 있다. 에러 처리가 자연스럽게 통합되지 않고, 취소를 하려면 &lt;code&gt;DispatchWorkItem&lt;/code&gt;을 따로 만들어야 한다. 또한 어느 스레드에서 실행되는지 컴파일러가 보장해 주지 못한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Task&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Swift Concurrency의 &lt;code&gt;async/await&lt;/code&gt; 기반 비동기 단위다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;Task {
    let result = await heavyWork()
    await MainActor.run {
        self.label.text = result  // 메인 스레드 명시적 보장
    }
}

// 또는 @MainActor가 붙은 컨텍스트라면 자동으로 메인에서 실행&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쪽은 코드가 동기 코드처럼 위에서 아래로 선형으로 읽히고, &lt;code&gt;try await&lt;/code&gt;로 에러 처리가 자연스럽게 통합된다. &lt;code&gt;task.cancel()&lt;/code&gt; 한 줄로 취소가 가능하고, &lt;code&gt;@MainActor&lt;/code&gt;나 &lt;code&gt;actor&lt;/code&gt; 같은 격리 어노테이션을 통해 스레드 안전성을 컴파일 타임에 검증할 수 있다. 그리고 &lt;code&gt;async let&lt;/code&gt;이나 &lt;code&gt;TaskGroup&lt;/code&gt;을 쓰면 부모-자식 관계가 명확한 구조적 동시성을 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 차이점&lt;/h3&gt;
&lt;table data-ke-align=&quot;alignLeft&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;항목&lt;/th&gt;
&lt;th&gt;DispatchQueue&lt;/th&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;취소&lt;/td&gt;
&lt;td&gt;DispatchWorkItem 별도 필요&lt;/td&gt;
&lt;td&gt;&lt;code&gt;task.cancel()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;에러 전파&lt;/td&gt;
&lt;td&gt;불가 (클로저 탈출)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;try await&lt;/code&gt;로 자연스럽게&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스레드 보장&lt;/td&gt;
&lt;td&gt;런타임에만 알 수 있음&lt;/td&gt;
&lt;td&gt;컴파일 타임 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;메모리 관리&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[weak self]&lt;/code&gt; 캡처 필요&lt;/td&gt;
&lt;td&gt;구조적 스코프로 비교적 안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;중단점 (suspension)&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;&lt;code&gt;await&lt;/code&gt;에서 스레드 양보 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 일반적인 비교다. 그런데 이 표만 보면 &quot;그럼 이제 Task만 쓰면 되는 거 아닌가&quot; 싶어진다. 그래서 다음 질문을 던졌다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &quot;Task 안 쓰고 DispatchQueue를 꼭 써야만 하는 상황이 있어?&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에 Claude는 &quot;거의 없지만 몇 가지 예외 상황이 있다&quot;고 답하면서 네 가지 케이스를 제시했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude가 제시한 케이스 1 &amp;mdash; 동기적 직렬 접근이 필요할 때 (Serial Queue)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 스레드에서 동시에 같은 데이터에 접근할 때, &lt;code&gt;DispatchQueue.sync&lt;/code&gt;를 쓰면 동기적으로 직렬을 보장할 수 있다고 했다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;private let queue = DispatchQueue(label: &quot;com.app.myQueue&quot;)

func update(_ value: Int) {
    queue.sync {  // 동기적으로 직렬 보장
        self.data = value
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 &quot;Task로는 &lt;code&gt;actor&lt;/code&gt;로 대체 가능하지만, 동기 컨텍스트에서 동기적으로 직렬 접근해야 한다면 DispatchQueue가 더 직접적&quot;이라고 덧붙였다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude가 제시한 케이스 2 &amp;mdash; DispatchSemaphore / DispatchGroup 활용 시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;async/await&lt;/code&gt;로 전환이 불가능한 서드파티 레거시 콜백이 여럿일 때 유용하다는 설명이었다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;let group = DispatchGroup()

group.enter()
legacyAPI.fetch { group.leave() }

group.enter()
legacyAPI.upload { group.leave() }

group.notify(queue: .main) {
    print(&quot;모두 완료&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude가 제시한 케이스 3 &amp;mdash; asyncAfter로 정밀한 타이밍 지연&lt;/h3&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
    // 딜레이 후 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task로 대체 가능하긴 하다고도 덧붙였다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Task {
    try await Task.sleep(for: .seconds(0.3))
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;code&gt;asyncAfter&lt;/code&gt;는 취소 개념 없이 단순 딜레이가 필요할 때 더 간결하다고 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Claude가 제시한 케이스 4 &amp;mdash; Objective-C / C 기반 API와 연동&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Data의 &lt;code&gt;perform&lt;/code&gt;, AVFoundation의 특정 큐처럼 프레임워크 자체가 특정 큐를 요구하는 경우다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;context.perform {  // NSManagedObjectContext 전용 큐
    // Core Data 작업
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 케이스를 하나씩 따져보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변을 받고 나서 각 케이스가 정말로 DispatchQueue 없이는 안 되는 상황인지 하나씩 반박해 보기로 했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;동기 컨텍스트에서 직렬 접근은 GlobalActor로 불가능해?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;케이스 1에 대해 먼저 의문을 던졌다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 동기 컨텍스트에서 동기적으로 직렬 접근해야하면 GlobalActor로 불가능해?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 &quot;GlobalActor는 &lt;code&gt;async&lt;/code&gt; 컨텍스트에서만 격리 보장이 된다&quot;고 답했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@globalActor
actor MyActor {
    static let shared = MyActor()
}

@MyActor
var data: Int = 0&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;외부 동기 컨텍스트에서 접근하려면 이렇게 된다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;// ❌ 동기 컨텍스트에서 접근 불가 &amp;mdash; 컴파일 에러
func syncFunc() {
    data = 1  // error: expression is 'async' but is not marked with 'await'
}

// ✅ 올바른 패턴 &amp;mdash; 액터로 격리된 함수를 만들어서 await로 호출
@MyActor func setData(_ value: Int) {
    data = value  // 액터 내부이므로 직접 접근 가능
}

func asyncFunc() async {
    await setData(1)  // 함수 호출에 await를 붙이는 것
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;DispatchQueue&lt;/code&gt;는 &lt;code&gt;async&lt;/code&gt; 없이도 동기적으로 직렬을 보장한다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;private let queue = DispatchQueue(label: &quot;serial&quot;)

func syncFunc() {
    queue.sync {   // async 필요 없이 동기적으로 직렬 보장
        data = 1
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;async&lt;/code&gt; 컨텍스트로 진입 자체가 불가능한 동기 코드에서는 GlobalActor가 작동하지 않는다는 게 Claude의 설명이었다. &lt;code&gt;assumeIsolated&lt;/code&gt;라는 방법이 있긴 한데, 실제로 그 액터 위에 있지 않으면 런타임 크래시가 난다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;프레임워크 강제 큐는 뭔소리야?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;케이스 4 설명이 모호해서 다시 물었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 프레임워크 강제 큐는 뭔소리야?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude는 이전에 든 Core Data 예시는 사실 DispatchQueue가 아니라 자체 메커니즘이라 잘못된 예시였다고 인정하면서 더 정확한 예시를 제시했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;// AVCaptureVideoDataOutput &amp;mdash; 특정 큐를 직접 파라미터로 넘겨야 함
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: &quot;camera&quot;))

// NotificationCenter
NotificationCenter.default.addObserver(
    forName: .something,
    object: nil,
    queue: OperationQueue.main
) { _ in }

// DispatchSource
let source = DispatchSource.makeTimerSource(queue: .global())&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 API들은 함수 시그니처 자체가 &lt;code&gt;DispatchQueue&lt;/code&gt;(또는 &lt;code&gt;OperationQueue&lt;/code&gt;)를 인자로 받기 때문에 Task로 대체가 불가능하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;동기 컨텍스트에서 Task 생성하고 GlobalActor에 접근하면 되는 거 아님?&quot;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 동기 컨텍스트에서 Task 생성하고 global actor에 접근하면 되는 거 아님?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude의 답변은 이랬다. &quot;맞습니다. 그렇게 할 수 있어요. 제 설명이 틀렸습니다.&quot;&lt;/p&gt;
&lt;pre class=&quot;autoit&quot;&gt;&lt;code&gt;func syncFunc() {
    Task { @MyActor in
        data = 1  // ✅ 가능
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;code&gt;DispatchQueue.sync&lt;/code&gt;와 결정적인 차이가 하나 있다고 했다.&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;// DispatchQueue.sync &amp;mdash; 호출자가 완료까지 블로킹
func syncFunc() {
    queue.sync {
        data = 1
    }
    print(data)  // ✅ 반드시 1 &amp;mdash; 위 작업이 끝난 뒤 실행됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;// Task &amp;mdash; fire-and-forget, 호출자는 바로 리턴
func syncFunc() {
    Task { @MyActor in
        data = 1
    }
    print(data)  // ⚠️ 아직 0일 수 있음 &amp;mdash; Task는 비동기로 나중에 실행
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &quot;동기 컨텍스트에서 작업 완료를 보장하고 다음 줄로 넘어가야 한다&quot;는 요구사항이 있을 때만 &lt;code&gt;DispatchQueue.sync&lt;/code&gt;가 의미가 있다는 설명이었다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&quot;그냥 print(data)를 Task 안으로 넣으면 되잖아&quot;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 그냥 &lt;code&gt;print(data)&lt;/code&gt;를 Task 안으로 넣으면 되잖아&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Claude의 답변. &quot;맞습니다. 그것도 됩니다.&quot;&lt;/p&gt;
&lt;pre class=&quot;go&quot;&gt;&lt;code&gt;func syncFunc() {
    Task { @MyActor in
        data = 1
        print(data)  // ✅ 반드시 1
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시점에서 Claude는 이렇게 마무리했다. &quot;솔직히 말씀드리면, 제가 처음에 말한 케이스들을 사용자분이 하나씩 다 반박하셨고 전부 맞습니다. 진짜로 DispatchQueue를 써야만 하는 상황은 결국 하나로 좁혀집니다. API 시그니처 자체가 DispatchQueue를 요구하는 경우입니다.&quot;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// API 시그니처 자체가 DispatchQueue를 요구하는 경우
videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: &quot;camera&quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 Claude와의 대화는 일단락되었다. 결론은 &quot;프레임워크 API가 DispatchQueue 타입을 인자로 강제할 때만 진짜로 필요하다&quot;는 것이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Gemini의 등장: &quot;Claude가 너무 쉽게 백기를 들었다&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;대화 내용을 정리해서 Gemini Pro와 이야기를 이어나갔다. 그러자 Gemini가 반례를 들고 나타났다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Gemini:&lt;/b&gt; &quot;Claude가 너무 쉽게 항복했네요. &lt;code&gt;print(data)&lt;/code&gt;를 Task 안으로 넣는 건 해당 함수가 단순히 &lt;b&gt;'값을 출력하고 끝나는 Void 함수'&lt;/b&gt;일 때만 성립합니다. 하지만 실무에서는 &lt;b&gt;동기 함수(Sync)에서 값을 즉시 반환(return)&lt;/b&gt;해야 하는 경우가 많습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반례: 값을 즉시 리턴해야 하는 동기 함수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Task는 비동기로 실행되므로, 동기 함수 내부에서 Task를 돌려 그 값을 즉시 리턴하려고 하면 값을 기다리지 못하고 초기값을 반환해 버린다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;// ❌ Task로는 동기 함수에서 값을 반환할 수 없음
func getSyncData() -&amp;gt; Int {
    var result = 0
    Task { @MyActor in
        result = data  // 비동기로 실행되므로 언제 할당될지 모름
    }
    return result  // ⚠️ 무조건 0이 반환됨. Task가 끝나는 걸 기다릴 수 없음!
}

// ✅ DispatchQueue.sync는 가능함 (호출 스레드를 블로킹하니까)
func getSyncData() -&amp;gt; Int {
    var result = 0
    queue.sync {
        result = data
    }
    return result  // 큐의 작업이 끝나야 다음 줄로 넘어가므로 정확한 값 반환
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 Claude가 놓친 실질적인 케이스였다. 단순히 사이드 이펙트만 일으키는 Void 함수라면 &lt;code&gt;Task { }&lt;/code&gt; 안으로 모든 걸 옮기면 되지만, 함수가 값을 반환해야 한다면 그 트릭이 통하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;추가로 짚어준 표 수정 사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini는 표의 메모리 관리 항목도 짚었다. &lt;code&gt;async let&lt;/code&gt;이나 &lt;code&gt;TaskGroup&lt;/code&gt; 같은 구조적 동시성 안에서는 캡처가 안전하지만, 우리가 예시로 든 &lt;code&gt;Task { ... }&lt;/code&gt;는 비구조적 동시성(Unstructured Concurrency)이라서 내부 클로저가 탈출한다. 따라서 안에서 &lt;code&gt;self&lt;/code&gt;를 강하게 참조하고 그 Task가 오래 걸리는 네트워크 작업을 수행한다면, 뷰 컨트롤러가 닫혀도 메모리에서 해제되지 않는 Memory Leak이 발생할 수 있다는 지적이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;DispatchGroup&lt;/code&gt;의 대안으로 적은 &lt;code&gt;withCheckedContinuation&lt;/code&gt;도 단일 콜백을 감싸는 용도이지, 여러 비동기 작업을 모아 기다리는 용도는 아니라는 설명도 덧붙였다. 여러 개를 모으는 용도라면 &lt;code&gt;TaskGroup&lt;/code&gt; 또는 &lt;code&gt;async let&lt;/code&gt;이 더 정확한 대응이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. &quot;그럼 그냥 저 함수를 async로 바꾸면 되잖아?&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini의 반례를 보고 다시 물었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 저 &lt;code&gt;getSyncData&lt;/code&gt; 함수 자체를 async로 만들어서 await로 리턴하면 되는 것 아닌가요?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini는 이때 &lt;b&gt;비동기 전염(Async Infection)&lt;/b&gt;이라는 개념을 꺼냈다. 코드 한 곳을 async로 바꾸는 순간, 그 함수를 호출하는 모든 곳도 async가 되어야 하고, 그러면 또 그것을 호출하는 곳들도 전부 async가 되어야 한다. 결국 점 하나 찍는 것 같았던 변경이 코드베이스 전체로 전염되어 퍼져 나간다.&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;class NetworkRequest {
    // 기존에 존재하던 동기(Sync) 연산 프로퍼티
    var authHeader: String {
        // ❌ 여기서 에러 폭발!
        // cache가 actor가 되었기 때문에 await를 써야 하는데,
        // 일반 동기 프로퍼티 내부에서는 await를 쓸 수 없음!
        let token = await cache.getValue(forKey: &quot;token&quot;)
        return &quot;Bearer \(token)&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 해결하려면 &lt;code&gt;authHeader&lt;/code&gt;를 &lt;code&gt;var authHeader: String { get async }&lt;/code&gt;로 바꿔야 하고, 그러면 그것을 가져다 쓰던 모든 함수가 async가 되어야 하고, 결국 호출 스택 전체가 도미노처럼 무너진다. 단지 캐시 하나를 actor로 바꿨을 뿐인데 앱 전체의 아키텍처를 뒤집어엎어야 하는 상황이 발생한다는 것이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. &quot;잠깐, cellForRowAt 예시는 안티 패턴이잖아!&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini는 이 비동기 전염을 설명하면서 처음에는 &lt;code&gt;tableView(_:cellForRowAt:)&lt;/code&gt;를 예시로 들었다. UIKit의 데이터소스 메서드는 시그니처가 동기적이라 async로 만들 수 없고, 그 안에서 actor에 접근하려면 결국 &lt;code&gt;DispatchQueue.sync&lt;/code&gt;로 동기적으로 값을 빼와야 한다는 논리였다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath) -&amp;gt; UITableViewCell {
    var cellData: String = &quot;&quot;
    queue.sync {
        cellData = sharedDataManager.readData(at: indexPath.row)
    }
    let cell = UITableViewCell()
    cell.textLabel?.text = cellData
    return cell
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 또 다른 의문이 들었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 야, 저 예시 코드는 애초에 프로덕션 레벨에서는 허용되지 않는 코드잖아. 메인 스레드에서 &lt;code&gt;queue.sync&lt;/code&gt; 걸어버리면 사용자가 스크롤 버벅인다고 느끼게 되잖아. 아예 틀린 예 아니야?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이건 단순한 UX 문제가 아니라 더 근본적인 지적이었다. &lt;code&gt;cellForRowAt&lt;/code&gt; 안에서 무거운 작업을 처리하려는 시도 자체가 아키텍처적 실수다. &lt;code&gt;DispatchQueue.sync&lt;/code&gt;로 블로킹하면 스크롤이 버벅이고, &lt;code&gt;Task { }&lt;/code&gt;로 비동기 처리하면 셀이 반환된 뒤에야 데이터가 들어오니 화면이 깜빡인다. 두 도구 모두 이 자리에서는 쓰면 안 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 패턴은 &lt;code&gt;cellForRowAt&lt;/code&gt;이 호출되기 훨씬 전에 &lt;code&gt;async/await&lt;/code&gt;로 데이터를 미리 로드해두고, &lt;code&gt;cellForRowAt&lt;/code&gt; 안에서는 이미 준비된 배열에서 그냥 꺼내 읽는 것이다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;// ✅ 올바른 구조: cellForRowAt은 이미 준비된 데이터만 읽는다
class MyViewController: UIViewController {
    var items: [Item] = []  // 이미 로드 완료된 데이터

    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            items = await loadItems()  // 여기서 비동기로 로드
            tableView.reloadData()
        }
    }

    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -&amp;gt; UITableViewCell {
        // 여기서는 그냥 읽기만 한다. 동시성 도구 불필요.
        let item = items[indexPath.row]
        // ...
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 &lt;code&gt;cellForRowAt&lt;/code&gt;은 &quot;DispatchQueue냐 Task냐&quot;라는 질문이 성립하는 자리가 아니다. 이건 &lt;b&gt;언제 데이터를 로드할 것인가&lt;/b&gt;라는 설계의 문제이지 동시성 도구를 선택하는 문제가 아니다. Gemini도 이 지적을 받아들이고 &quot;스레드 안전한 인메모리 캐시&quot; 예시로 정정했다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;class ThreadSafeCache {
    private var storage: [String: String] = [:]
    private let queue = DispatchQueue(label: &quot;com.app.cacheQueue&quot;)

    // 무조건 값을 즉시 리턴해야 하는 동기 함수 (프로퍼티 게터도 동일)
    func getValue(forKey key: String) -&amp;gt; String? {
        var result: String?

        // ✅ 큐의 작업이 끝날 때까지 스레드를 아주 잠깐(&amp;micro;s) 블로킹하고 안전하게 값을 빼옴
        queue.sync {
            result = storage[key]
        }

        return result
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이쪽은 메인 스레드에서 호출되는 게 아니라 백그라운드에서 호출되는 가벼운 캐시 조회라, &lt;code&gt;queue.sync&lt;/code&gt;로 짧게 블로킹하는 게 실용적으로 허용될 수 있는 케이스다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. &quot;그럼 그냥 actor로 만들면 되잖아?&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 질문을 던졌다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 아무리 생각해도 이상한데, 이 클래스 자체가 그냥 actor면 다 해결되는 거 아냐?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini의 답. &quot;맞습니다. 새로 짜는 코드라면 actor로 설계하는 것이 100% 정답입니다.&quot;&lt;/p&gt;
&lt;pre class=&quot;processing&quot;&gt;&lt;code&gt;actor ThreadSafeCache {
    private var storage: [String: String] = [:]

    func getValue(forKey key: String) -&amp;gt; String? {
        return storage[key]
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 만들면 호출 측에서는 &lt;code&gt;let token = await cache.getValue(forKey: &quot;token&quot;)&lt;/code&gt; 형태로 쓰면 된다. &lt;code&gt;DispatchQueue.sync&lt;/code&gt;도 필요 없고 락도 필요 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 왜 &lt;code&gt;DispatchQueue.sync&lt;/code&gt;가 필요하다고 했을까. 바로 앞에서 설명한 &lt;b&gt;비동기 전염&lt;/b&gt; 때문이다. 새로 만드는 캐시를 기존 레거시 시스템의 동기 프로퍼티에서 당장 써야 한다면, 그 인터페이스를 async로 못 바꾸는 상황이라면, 어쩔 수 없이 어댑터로 &lt;code&gt;DispatchQueue.sync&lt;/code&gt;(혹은 락)를 끼워 넣을 수밖에 없다는 이야기였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이 케이스도 결국 &quot;신규 코드 안에서 필요한 것&quot;이 아니라 &quot;&lt;b&gt;레거시 동기 인터페이스와의 어댑터&lt;/b&gt;&quot;로서 필요한 것이었다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. 그럼 신규 코드에서는 진짜로 DispatchQueue를 영원히 안 써도 되는 거야?&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;나:&lt;/b&gt; 그러니까 현 시점에서 새로 짜는 코드에서 DispatchQueue를 꼭 써야 하는 순간이 있는 거야 없는 거야?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gemini의 답을 정리하면 이렇다. 내가 통제할 수 있는 비즈니스 로직 안에서는 0%다. 비동기 처리와 네트워킹은 &lt;code&gt;Task&lt;/code&gt;와 &lt;code&gt;async/await&lt;/code&gt;로 완벽히 커버되고, 스레드 안전한 객체는 처음부터 &lt;code&gt;actor&lt;/code&gt;로 설계하면 되고, 어쩔 수 없이 동기 함수에서 락을 걸어야 하더라도 이제는 &lt;code&gt;DispatchQueue.sync&lt;/code&gt; 대신 &lt;code&gt;OSAllocatedUnfairLock&lt;/code&gt;이나 Swift 6의 &lt;code&gt;Mutex&lt;/code&gt;를 쓰는 게 표준이 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오직 남는 건 애플이 제공하는 프레임워크 API의 매개변수 자체가 &lt;code&gt;DispatchQueue&lt;/code&gt; 타입을 강제하는 경우다. 가장 대표적인 게 하드웨어 제어나 미디어 처리(AVFoundation, CoreBluetooth, Network framework 등)다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;let videoOutput = AVCaptureVideoDataOutput()
let cameraQueue = DispatchQueue(label: &quot;com.myApp.cameraQueue&quot;)

// ❌ 시스템 API 시그니처 자체가 DispatchQueue를 요구함. Task로는 죽어도 못 넣음.
videoOutput.setSampleBufferDelegate(self, queue: cameraQueue)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 API들은 내부적으로 C 언어 기반의 로우레벨 콜백을 사용하거나 수십 년 된 레거시 구조로 얽혀 있어서, 애플이 직접 &lt;code&gt;async/await&lt;/code&gt;용 래퍼 API를 만들어주지 않는 이상 우리에게 선택권이 없다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 그런데 다시 한 번 따져보면, 그 &quot;어댑터&quot; 케이스도 사실은&amp;hellip;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 다시 한 번 짚어볼 필요가 있다. Gemini가 말한 두 번째 케이스, 즉 &quot;레거시 동기 인터페이스 안에서 어댑터로 &lt;code&gt;DispatchQueue.sync&lt;/code&gt;를 쓰는&quot; 경우다. 이건 정확히 말하면 신규 코드의 문제가 아니다. &lt;b&gt;기존 레거시가 강제하는 동기 계약(contract)에 신규 코드가 어쩔 수 없이 맞춰주는&lt;/b&gt; 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 정리하려고 했던 질문은 &quot;신규 코드에서 DispatchQueue를 쓸 필요가 있느냐&quot;였다. 그 관점에서 보면 비동기 전염을 막기 위한 어댑터 케이스도 결국 &quot;내가 통제하지 못하는 외부 동기 계약 때문에 어쩔 수 없이&quot;라는 단서가 붙는다. 그리고 이 단서가 붙는 순간, 그건 &quot;신규 코드가 자기 영역 안에서 DispatchQueue가 필요한 경우&quot;가 아니라 &quot;외부와의 경계면에서 어쩔 수 없이 다리를 놓아야 하는 경우&quot;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또 하나 짚을 점은, 그 경계면에서조차도 이제는 &lt;code&gt;DispatchQueue.sync&lt;/code&gt;보다 &lt;code&gt;OSAllocatedUnfairLock&lt;/code&gt;(iOS 16+) 혹은 Swift 6의 &lt;code&gt;Mutex&lt;/code&gt;가 더 적합한 도구라는 것이다. &lt;code&gt;DispatchQueue.sync&lt;/code&gt;는 같은 직렬 큐에서 자기 자신을 호출하면 데드락이 발생하는 함정이 있고, 단순 상호 배제(mutual exclusion) 용도라면 락 프리미티브가 더 가볍고 안전하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. 그래서 최종 결론은&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논의를 다 거치고 나면 이렇게 정리된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 완전히 통제하는 코드, 즉 신규 프로젝트의 비즈니스 로직 안에서는 &lt;code&gt;DispatchQueue&lt;/code&gt;를 더 이상 꺼낼 일이 없다. 비동기 작업은 &lt;code&gt;Task&lt;/code&gt;와 &lt;code&gt;async/await&lt;/code&gt;, 직렬 접근 보호는 &lt;code&gt;actor&lt;/code&gt;, 메인 스레드 보장은 &lt;code&gt;@MainActor&lt;/code&gt;, 딜레이는 &lt;code&gt;Task.sleep&lt;/code&gt;, 여러 비동기 작업 묶기는 &lt;code&gt;TaskGroup&lt;/code&gt;이나 &lt;code&gt;async let&lt;/code&gt;, 외부 콜백 다리 놓기는 &lt;code&gt;withCheckedContinuation&lt;/code&gt;, 단순 상호 배제가 필요하다면 &lt;code&gt;OSAllocatedUnfairLock&lt;/code&gt;이나 Swift 6의 &lt;code&gt;Mutex&lt;/code&gt;. DispatchQueue가 차지했던 자리를 생태계 전체가 메웠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 불구하고 DispatchQueue가 진짜로 필요한 유일한 순간은 &lt;b&gt;애플이 만든 프레임워크 API가 시그니처에서 DispatchQueue 타입을 직접 요구하는 경우&lt;/b&gt;다. AVFoundation의 &lt;code&gt;setSampleBufferDelegate&lt;/code&gt;, &lt;code&gt;DispatchSource&lt;/code&gt; 계열, NotificationCenter의 큐 파라미터 같은 자리들이다. 이건 도구 선택의 문제가 아니라 단순히 통행료다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에 Claude가 백기를 들었던 &quot;API 시그니처 강제 케이스&quot;라는 결론은 사실 거의 정확했다. Gemini가 추가로 제기한 &quot;동기 함수에서 값을 즉시 반환해야 할 때&quot; 같은 케이스도, 끝까지 파고들어 가면 &quot;내가 통제하지 못하는 외부 동기 계약과의 경계면&quot; 이야기였고, 그 경계면조차도 신규 코드 그 자체의 문제는 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 &lt;code&gt;cellForRowAt&lt;/code&gt; 같은 예시는 사실 이 논의의 무대에 올라올 자격조차 없는 케이스였다. 그건 동시성 도구의 문제가 아니라 데이터 로딩 타이밍의 설계 문제다. DispatchQueue를 쓰든 Task를 쓰든 둘 다 답이 아니고, 답은 &quot;그 메서드 안에서 무거운 작업을 하지 않는다&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 AI와 이야기를 해보니 막연했던 Concurrency의 경계가 훨씬 명확해졌다. 무작정 Task만 고집하지도 말고, 그렇다고 DispatchQueue가 아직 필요한 자리가 있다고 막연하게 끌고 가지도 말 것. 상황을 정확히 분해하고, 정말로 그 도구가 아니면 안 되는 자리인지 따져보는 것. 그게 결국 동시성을 제대로 다루는 길이라는 걸 다시 한번 확인했다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 Claude와 Gemini Pro와의 실제 대화를 바탕으로 정리했습니다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>Concurrency</category>
      <category>DispatchQueue</category>
      <category>ios</category>
      <category>SWIFT</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/101</guid>
      <comments>https://modelinspring.tistory.com/101#entry101comment</comments>
      <pubDate>Sat, 23 May 2026 16:12:25 +0900</pubDate>
    </item>
    <item>
      <title>[Swift/iOS] (Base) XML Parser를 만드는 과정, 설계부터 고민까지</title>
      <link>https://modelinspring.tistory.com/100</link>
      <description>&lt;h1&gt;Swift로 XML Parser 만들기, 설계부터 고민까지&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 이야기의 시작&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 XML 응답을 파싱해야하는 일이 생겼습니다. 주로 API로부터 응답은 JSON 포맷으로 받게 되지만 어쩌다 한번씩 XML 포맷으로 응답을 받는 경우가 있습니다.&lt;br /&gt;흔하지 않은 경우이기도 해서 그때마다 필요한 데이터나 정보에 맞게 Parser를 만들었습니다.&lt;br /&gt;하지만 생각보다 XML 포맷의 응답이 많아지고 있었습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Swift의 XMLParser의 불편한 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본으로 제공하는 XMLParser는 실제로 사용하는데 몇가지 불편한 점이 있습니다.&lt;br /&gt;XMLParser는 데이터를 한 줄, 한 줄 읽으면서 파싱을 하게 됩니다.&lt;br /&gt;현재 어느 위치에서 파싱이 진행되는지 알 수 없기 때문에 이 위치를 알기 위해서는 수 많은 추가 작업들이 필요합니다. 특정 태그의 정보를 담는 프로퍼티를 만든다든가, 플래그를 만든다든가 하는 것이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 부모 - 자식 관계나 형제 관계에 대한 정보도 알기 어렵습니다.&lt;br /&gt;그저 파싱 중인 한 태그, 한 태그의 정보만 전달해줄 뿐이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도 몇 가지 불편한 점들이 있지만 이 이유가 Parser를 재사용하기 어렵고, 매번 필요한 정보에 따라 커스텀 Parser를 만들게 되는 이유가 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API의 정보를 Codable을 준수하는 타입으로 바꾸는 작업이 필요하지만 매번 Client의 응답을 하나하나 커스텀해주는 과정은 없습니다.&lt;br /&gt;최소한 다른 API만큼의 작업만 진행하고자 재사용을 위한 Parser를 만들기로 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아, 라이브러리는 고려하지 않았습니다. 이건 저의 개발 환경의 특수성때문입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 어떤 방식으로 만들 수 있을까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. subscript 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML을 파싱하는 라이브러리들을 찾아보았습니다. SWXMLHash가 첫 번째로 눈에 보였습니다. Star의 수도 1.5k로 준수했습니다.&lt;br /&gt;하지만 사용 예시를 보면 아래와 같았습니다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;xml[&quot;user&quot;][&quot;items&quot;][0][&quot;bag&quot;].value&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직관적이지만 하드코딩을 사용하는 방식이었습니다.&lt;br /&gt;바로 패스했습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. &lt;code&gt;[String : Any]&lt;/code&gt; 변환 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 작업들에서 사용하던 방식입니다. 필요한 태그를 key로, 필요한 값들을 value로 변환해서 사용하는 방식입니다.&lt;br /&gt;하지만 여러 구조로 넘어오는 XML을 다 처리하기에는 쉽지 않을 것 같았습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-3. Codable 방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;XML -&amp;gt; JSON -&amp;gt; Codable 구조체로 변환하는 방식입니다.&lt;br /&gt;변환 레이어가 추가되어야 하지만 사용하는 쪽은 JSONDecoder와 거의 동일한 방식으로 사용할 수 있게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 설계하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종 사용 형태를 먼저 정하고, 역으로 설계를 시작했습니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;XMLNode                  &amp;rarr; 파싱 결과를 담는 트리 노드
XMLDocumentParser        &amp;rarr; XMLParserDelegate, 노드 트리 생성
XMLNode+JSON             &amp;rarr; 노드 트리를 JSON으로 변환
XMLDecoder               &amp;rarr; 외부에 노출되는 최종 인터페이스&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;XMLNode &amp;mdash; struct vs class&lt;/h3&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;struct XMLNode {
    let name: String
    var value: String = &quot;&quot;
    var attributes: [String: String] = [:]
    var children: [XMLNode] = []
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음엔 &lt;code&gt;class&lt;/code&gt;로 만들었습닌다. 트리 구조에서 자식을 추가할 때 참조로 접근하면 편하기 때문입니다.&lt;br /&gt;하지만, 파싱이 끝난 이후엔 불변 데이터에 가깝고 외부에서 공유할 필요가 없어서 &lt;code&gt;struct&lt;/code&gt;로 변경했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;code&gt;struct&lt;/code&gt;로 바꾸면서 파서 코드도 수정이 필요.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;// class일 때 - 참조라서 직접 수정 가능
stack.last?.children.append(node) // ❌ struct에선 복사본 반환

// struct일 때 - 인덱스로 직접 접근해야 함
stack[stack.count - 1].children.append(node) // ✅&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;stack.last&lt;/code&gt;는 복사본을 반환하기 때문에 append해도 원본에 반영되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;XMLDocumentParser &amp;mdash; 스택 기반 트리 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;XMLParserDelegate&lt;/code&gt;를 이용해 스택으로 트리를 조립합니다. 핵심 로직은 &lt;code&gt;didEndElement&lt;/code&gt;입니다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;func parser(_ parser: XMLParser, didEndElement elementName: String, ...) {
    guard var node = stack.popLast() else { return }
    node.value = currentValue
    currentValue = &quot;&quot;

    if stack.isEmpty {
        root = node
    } else {
        stack[stack.count - 1].children.append(node)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 트러블슈팅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 구현에서 문제가 있었습니다.&lt;/p&gt;
&lt;pre class=&quot;fsharp&quot;&gt;&lt;code&gt;// 잘못된 코드
let jsonData = try root.toJSONData()
return try jsonDecoder.decode(type, from: jsonData)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;toJSONData()&lt;/code&gt;는 디버깅 용도로 루트 노드 이름을 키로 감싸서 반환합니다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;// &amp;lt;items&amp;gt; XML이라면
{ &quot;items&quot;: { &quot;item&quot;: [...] } }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;items&lt;/code&gt; 구조체는 최상위에서 &lt;code&gt;item&lt;/code&gt; 키를 찾는데 &lt;code&gt;items&lt;/code&gt;만 있으니 디코딩이 항상 실패합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;// 수정 후
private func decode&amp;lt;T: Decodable&amp;gt;(_ type: T.Type, from root: XMLNode) throws -&amp;gt; T {
    let jsonData = try JSONSerialization.data(withJSONObject: root.toJSONObject())
    return try jsonDecoder.decode(type, from: jsonData)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;toJSONObject()&lt;/code&gt;는 루트 이름 없이 내용만 반환하기 때문에 구조체와 정확히 매핑됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 Codable 구조체를 정의할 때도 &lt;code&gt;JSONDecoder&lt;/code&gt;와 동일한 방식으로 사용할 수 있습니다. 루트 태그명은 &lt;code&gt;decode()&lt;/code&gt; 호출 시 타입으로 표현하고, struct는 그 내용물을 정의합니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;INFO&amp;gt;
    &amp;lt;CODE&amp;gt;0&amp;lt;/CODE&amp;gt;
    &amp;lt;MESSAGE&amp;gt;success&amp;lt;/MESSAGE&amp;gt;
&amp;lt;/INFO&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;// INFO 안의 내용물을 바로 정의
struct Info: Decodable {
    let code: String
    let message: String

    enum CodingKeys: String, CodingKey {
        case code = &quot;CODE&quot;
        case message = &quot;MESSAGE&quot;
    }
}

let result = try XMLDecoder().decode(Info.self, from: xmlString)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;태그 속성 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성과 값이 함께 있는 경우도 있습니다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;&amp;lt;price currency=&quot;USD&quot; discount=&quot;10&quot;&amp;gt;39.99&amp;lt;/price&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON으로 변환하면&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;{
  &quot;@attributes&quot;: { &quot;currency&quot;: &quot;USD&quot;, &quot;discount&quot;: &quot;10&quot; },
  &quot;#value&quot;: &quot;39.99&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@&lt;/code&gt;와 &lt;code&gt;#&lt;/code&gt;를 prefix로 쓰는 이유는 XML 태그명으로 사용할 수 없는 문자라 실제 태그와 충돌하지 않기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Codable에서 받으려면 커스텀 &lt;code&gt;init(from:)&lt;/code&gt;이 필요합니다.&lt;/p&gt;
&lt;pre class=&quot;rust&quot;&gt;&lt;code&gt;struct Price: Decodable {
    let currency: String
    let discount: String
    let value: String

    enum CodingKeys: String, CodingKey {
        case attributes = &quot;@attributes&quot;
        case value = &quot;#value&quot;
    }

    struct AttributeKeys: Decodable {
        let currency: String
        let discount: String
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let attrs = try container.decode(AttributeKeys.self, forKey: .attributes)
        currency = attrs.currency
        discount = attrs.discount
        value = try container.decode(String.self, forKey: .value)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;lazy var + struct 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파서를 다른 struct에서 사용하다가 에러가 발생했습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;struct SomeStruct {
    private lazy var xmlDecoder: any XMLDecoding = XMLDecoder()

    func fetch() {
        someFunction {
            try? xmlDecoder.decode(...) // Cannot use mutating getter on immutable value
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;무심코 사용한 &lt;code&gt;lazy var&lt;/code&gt; 때문이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;lazy var&lt;/code&gt;는 &lt;b&gt;처음 접근할 때 값을 초기화하고 self에 저장&lt;/b&gt;한다. 즉 getter가 내부적으로 &lt;code&gt;self&lt;/code&gt;를 변경한다.&lt;/p&gt;
&lt;pre class=&quot;sqf&quot;&gt;&lt;code&gt;// lazy var가 실제로 하는 일
mutating get {
    if _xmlDecoder == nil {
        _xmlDecoder = XMLDecoder() // self를 변경
    }
    return _xmlDecoder!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;func fetch()&lt;/code&gt;는 non-mutating이라 &lt;code&gt;self&lt;/code&gt;가 immutable하고, &lt;code&gt;self&lt;/code&gt;가 immutable하니 &lt;code&gt;lazy var&lt;/code&gt;의 mutating getter를 호출할 수 없는 것 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방법은 별 다른게 없습니다. 생성 비용이 크지 않기 때문에 &lt;code&gt;lazy&lt;/code&gt;를 삭제 했습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// lazy 제거
private let xmlDecoder: any XMLDecoding = XMLDecoder()&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만들면서 가장 많이 고민했던 부분은 구현보다 &lt;b&gt;설계 결정&lt;/b&gt;이었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;subscript vs Codable&lt;/li&gt;
&lt;li&gt;class vs struct&lt;/li&gt;
&lt;li&gt;lazy의 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에도&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자동 타입 변환 여부&lt;/li&gt;
&lt;li&gt;순서 보장에 대한 고민&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이 있었습니다.&lt;br /&gt;단순해 보이는 XML 파서 하나를 만들면서도 Swift의 값 타입 동작 방식, &lt;code&gt;lazy var&lt;/code&gt;의 내부 구조, Dictionary의 순서 보장 여부 같은 개념들을 다시 한번 짚어볼 수 있었습니다.&lt;/p&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>ios</category>
      <category>parser</category>
      <category>SWIFT</category>
      <category>XML</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/100</guid>
      <comments>https://modelinspring.tistory.com/100#entry100comment</comments>
      <pubDate>Sat, 9 May 2026 23:45:00 +0900</pubDate>
    </item>
    <item>
      <title>[Swift/iOS] Jailbreak Detection(탈옥 감지)의 필요성</title>
      <link>https://modelinspring.tistory.com/99</link>
      <description>&lt;h1&gt;Jailbreak Detection, 완벽한 방어는 없지만 그래도 해야 하는 이유&lt;/h1&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 개발을 하다 보면 보안 관련 작업은 왠지 &quot;굳이 하지 않아도 되는 것&quot;처럼 느껴질 때가 있습니다. 애플의 보안성을 믿기도 하고, 요즘도 탈옥하는 사람이 있나? 싶은 마음도 있습니다. 기능 개발도 바쁜데, 탈옥 감지나 앱 무결성 검증까지 신경 써야 하나 싶기도 합니다. 그럼에도 불구하고 왜 해야 하는지, 어떻게 접근해야 하는지를 이야기해보려 합니다.&lt;br /&gt;&lt;br /&gt;이 글에서는 iOS의 기본 보안 구조를 살펴보고, 탈옥이 그 구조를 어떻게 무너뜨리는지, 공격자가 실제로 무엇을 할 수 있는지, 그리고 탈옥 감지와 앱 무결성 검증을 어떻게 구현하는지까지 차근차근 살펴보겠습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. iOS 보안 개념&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;iOS 샌드박스란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS의 모든 앱은 샌드박스(Sandbox) 안에서 실행됩니다. 쉽게 말하면 각 앱이 자기만의 독립된 방 안에 갇혀 있는 것입니다. A 앱은 B 앱의 데이터에 접근할 수 없고, 시스템 파일에도 함부로 접근할 수 없습니다. 이 구조 덕분에 악성 앱이 설치되더라도 다른 앱이나 시스템 전체에 피해를 주기 어렵습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;코드 서명(Code Signing)이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱스토어에 올라가는 모든 앱은 Apple의 인증을 받은 개발자 인증서로 서명되어야 합니다. iOS는 앱을 실행할 때 이 서명을 검증해서, 서명이 유효하지 않으면 실행 자체를 거부합니다. 이 덕분에 출처가 불분명한 앱이 기기에서 실행되기 어렵고, 앱이 배포 이후에 변조됐는지도 확인할 수 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;탈옥이 이 구조를 어떻게 무너뜨리는가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탈옥(Jailbreak)은 iOS의 커널 취약점을 이용해 커널 수준의 제약을 해제하고, 루트 접근과 코드 서명 우회를 가능하게 하는 행위입니다. 과거에는 단순히 루트 권한을 획득하는 방식이 주였지만, palera1n이나 Dopamine 같은 현대의 탈옥 도구들은 더 정교한 방식을 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커널 패치(kernel patch)란 기기의 운영체제 핵심부인 커널을 직접 수정해서, 코드 서명 검증 로직 자체를 비활성화하는 것을 말합니다. 여기에 PPL(Page Protection Layer) 우회라는 개념도 등장하는데, PPL은 Apple이 ARM 하드웨어 수준에서 커널의 핵심 메모리 영역을 보호하는 보안 계층입니다. 쉽게 말하면, 커널조차 함부로 수정할 수 없도록 하드웨어가 한 번 더 지켜주는 자물쇠입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 탈옥 도구들은 이 PPL마저 우회하거나, PPL을 건드리지 않으면서도 코드 서명 제약을 무너뜨리는 방식을 택하고 있습니다. 이른바 &quot;rootless 탈옥&quot;이라 불리는 방식으로, 루트 권한 없이도 코드 서명 제약을 무력화할 수 있게 됩니다. 탈옥이 되면 샌드박스 제한이 해제되고, 공식 앱스토어를 거치지 않은 앱을 설치할 수 있게 되며, 시스템 파일에도 자유롭게 접근할 수 있게 됩니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 탈옥 기기에서 공격자가 할 수 있는 것들&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탈옥을 통해서 어떤 일이 일어날까요? 단순히 &quot;앱 하나더 설치할 수 있는 것&quot; 수준은 아닙니다. 탈옥은 수많은 공격의 시작점일 뿐입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;런타임 조작&lt;/b&gt;은 Frida나 Cydia Substrate 같은 툴을 이용해 앱이 실행 중인 상태에서 함수를 후킹(hooking)하는 것입니다. 예를 들어, 로그인 성공 여부를 판단하는 함수를 후킹해서 어떤 아이디/비밀번호를 입력해도 항상 &quot;인증 성공&quot;을 반환하게 만들 수 있습니다. 게임이라면 체력이나 재화 값을 실시간으로 조작하거나, 프리미엄 기능의 잠금을 해제하는 조건 함수를 통째로 무력화할 수도 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;네트워크 트래픽 가로채기&lt;/b&gt;는 앱과 서버 사이의 통신을 평문으로 들여다보는 공격입니다. SSL Pinning 우회 자체는 탈옥 없이도 가능하지만, 탈옥 환경에서 특히 위험한 이유가 있습니다. 일반 환경에서는 프록시 설정을 통해 트래픽을 가로채는 수준에 그치지만, 탈옥 기기에서는 Frida를 이용해 앱 내부의 TLS 스택 자체를 후킹할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS 스택이란 앱이 HTTPS 통신을 처리할 때 내부적으로 암호화&amp;middot;복호화를 담당하는 함수들의 집합입니다. 이 함수들을 직접 후킹하면 SSL Pinning 로직이 아무리 정교해도 의미가 없어집니다. 암호화되기 전, 혹은 복호화된 직후의 데이터에 바로 접근할 수 있기 때문입니다. 이게 가능해지면 인증 토큰의 구조, API 엔드포인트, 요청 파라미터가 모두 노출되고, 공격자는 앱 없이 서버를 직접 공격할 수 있게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;메모리 덤프&lt;/b&gt;는 앱이 실행 중일 때 메모리를 통째로 읽어오는 공격입니다. 암호화해서 저장한 데이터도 앱이 사용하려면 반드시 복호화해서 메모리에 올려야 합니다. 그 순간을 노리면 JWT 토큰, 세션 키, 개인정보 등을 탈취할 수 있습니다. &quot;나는 저장할 때 암호화하니까 괜찮다&quot;는 생각이 왜 충분하지 않은지 보여주는 사례입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;앱 바이너리 위변조 후 재배포&lt;/b&gt;는 앱을 역공학으로 분석한 뒤, 광고를 제거하거나 유료 기능을 무료로 만들거나 악성코드를 삽입해서 앱스토어 외부(Cydia 등)에 다시 올리는 공격입니다. 사용자 입장에서는 겉으로 보기에 똑같은 앱처럼 보이지만, 실제로는 개인정보가 공격자 서버로 조용히 빠져나가고 있는 상황이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;결제/인앱구매 우회&lt;/b&gt;는 결제 성공 여부를 검증하는 함수를 후킹해서, 실제 결제 없이도 항상 &quot;결제 완료&quot; 상태로 만드는 것입니다. 아이템이나 구독이 활성화되지만 실제 결제는 발생하지 않습니다. 앱 내부에서만 결제를 검증하고 서버 검증이 없다면 특히 취약합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 탈옥 감지 (Jailbreak Detection)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;탈옥 감지가 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 살펴본 공격들은 대부분 탈옥 기기를 전제로 합니다. 탈옥 기기를 감지해서 앱 실행을 제한하거나 민감한 기능을 비활성화하면, 공격자의 진입 장벽을 높일 수 있습니다. 완벽히 막을 수는 없지만, &quot;이 앱은 공격하기 귀찮다&quot;는 인식을 심어주는 것만으로도 의미가 있습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;주요 감지 기법들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;탈옥 감지에는 여러 가지 기법이 사용됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째는 &lt;b&gt;특정 파일 존재 여부 확인&lt;/b&gt;입니다. 탈옥이 되면 탈옥 관련 파일이나 디렉토리가 기기에 생성됩니다. 다만 여기서 중요한 점이 있습니다. 과거의 rooted 탈옥 방식에서는 &lt;code&gt;/Applications/Cydia.app&lt;/code&gt;, &lt;code&gt;/bin/bash&lt;/code&gt;, &lt;code&gt;/etc/apt&lt;/code&gt; 같은 경로에 파일이 생성됐지만, palera1n 기반의 현대적인 rootless 탈옥에서는 이 경로들이 존재하지 않을 수 있습니다. rootless 환경에서는 탈옥 관련 파일들이 &lt;code&gt;/var/jb/&lt;/code&gt; 하위에 위치하기 때문에, 두 경우를 모두 커버하려면 아래처럼 양쪽 경로를 함께 확인해야 합니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;private func checkForJailbreakFiles() -&amp;gt; Bool {
    let paths = [
        // rooted 탈옥 환경 경로
        &quot;/Applications/Cydia.app&quot;,
        &quot;/bin/bash&quot;,
        &quot;/etc/apt&quot;,
        &quot;/private/var/lib/apt&quot;,
        &quot;/usr/sbin/sshd&quot;,
        // rootless 탈옥 환경 경로 (palera1n 등)
        &quot;/var/jb/Applications/Cydia.app&quot;,
        &quot;/var/jb/usr/bin/ssh&quot;,
        &quot;/var/jb/etc/apt&quot;
    ]
    return paths.contains { FileManager.default.fileExists(atPath: $0) }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째는 &lt;b&gt;URL Scheme 확인&lt;/b&gt;입니다. Cydia는 cydia:// 라는 커스텀 URL Scheme을 등록합니다. 이 Scheme으로 앱을 열 수 있는지 확인하면 Cydia 설치 여부를 간접적으로 알 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;private func checkForCydiaURLScheme() -&amp;gt; Bool {
    guard let url = URL(string: &quot;cydia://package/com.example.package&quot;) else { return false }
    return UIApplication.shared.canOpenURL(url)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS 9 이후부터 &lt;code&gt;canOpenURL&lt;/code&gt;은 &lt;code&gt;Info.plist&lt;/code&gt;의 &lt;code&gt;LSApplicationQueriesSchemes&lt;/code&gt;에 등록된 &lt;code&gt;scheme&lt;/code&gt;만 조회할 수 있습니다. cydia를 아래처럼 추가하지 않으면 이 함수는 항상 &lt;code&gt;false&lt;/code&gt;를 반환하고, 탈옥 감지가 전혀 이루어지지 않습니다.&lt;/p&gt;
&lt;pre class=&quot;xml&quot;&gt;&lt;code&gt;&amp;lt;key&amp;gt;LSApplicationQueriesSchemes&amp;lt;/key&amp;gt;
&amp;lt;array&amp;gt;
    &amp;lt;string&amp;gt;cydia&amp;lt;/string&amp;gt;
&amp;lt;/array&amp;gt; &lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 &lt;b&gt;syscall을 이용한 감지&lt;/b&gt;입니다. Swift에서는 C 표준 함수인 &lt;code&gt;syscall&lt;/code&gt;을 직접 호출할 수 없기 때문에, &lt;code&gt;dlopen(nil, RTLD_NOW)&lt;/code&gt;으로 현재 실행 중인 프로세스의 심볼 테이블을 열고 &lt;code&gt;dlsym&lt;/code&gt;으로 &lt;code&gt;syscall&lt;/code&gt; 함수 포인터를 꺼내오는 방식을 사용합니다. 여기서 &lt;code&gt;nil&lt;/code&gt;을 넘기는 것이 &quot;현재 프로세스 자신의 심볼 테이블을 참조한다&quot;는 의미입니다. 이렇게 꺼낸 &lt;code&gt;syscall&lt;/code&gt;로 호출하는 것이 번호 163번의 &lt;code&gt;SYS_csops&lt;/code&gt;인데, 이 시스템 콜은 현재 프로세스의 코드 서명 플래그를 조회합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서 flags &amp;amp; 0x1로 확인하는 것이 &lt;code&gt;CS_VALID&lt;/code&gt; 플래그입니다. 여기서 한 가지 주의할 점이 있습니다. &lt;code&gt;CS_VALID&lt;/code&gt;가 &lt;b&gt;켜져 있으면&lt;/b&gt; &lt;code&gt;true&lt;/code&gt;를 반환하는 이 함수는 사실 &quot;코드 서명이 유효하다 = 정상 기기&quot;라는 의미입니다. 이 함수 단독으로는 탈옥 감지 역할을 하지 않습니다. 실제 사용 의도는 호출부에서 &lt;code&gt;if !checkToFlags()&lt;/code&gt;처럼 반전시켜서, &quot;&lt;code&gt;CS_VALID&lt;/code&gt;가 꺼져 있으면 코드 서명이 훼손된 탈옥 환경일 수 있다&quot;고 판단하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;private func checkToFlags() -&amp;gt; Bool {
    // Swift에서 syscall을 직접 호출할 수 없으므로,
    // dlopen(nil, ...)으로 현재 프로세스의 심볼 테이블을 열어
    // syscall 함수 포인터를 동적으로 꺼내옵니다
    // 참고: nil을 넘긴 경우 현재 프로세스 자신의 핸들이라 실질적 누수는 없지만,
    // 일반적인 dlopen 사용에서는 반드시 dlclose(handle)로 닫는 것이 원칙입니다
    typealias SyscallPtr = @convention(c) (Int32, Int32, UInt32, UnsafeMutablePointer&amp;lt;UInt32&amp;gt;, Int) -&amp;gt; Int32
    var flags: UInt32 = 0
    let handle = dlopen(nil, RTLD_NOW)
    if let sym = dlsym(handle, &quot;sys&quot; + &quot;call&quot;) { // 정적 분석 툴의 문자열 탐지를 피하기 위한 난독화
        let syscall = unsafeBitCast(sym, to: SyscallPtr.self)
        // 163 = SYS_csops: 현재 프로세스의 코드 서명 플래그를 조회하는 시스템 콜
        let result = syscall(50*3+10+3*3, getpid(), 0, &amp;amp;flags, MemoryLayout&amp;lt;UInt32&amp;gt;.size)
        if result == 0 {
            // CS_VALID(0x1)가 켜져 있으면 true(서명 유효 = 정상)를 반환
            // 호출부에서 !checkToFlags()로 반전해서 사용:
            // &quot;CS_VALID가 꺼져 있으면 탈옥 환경으로 의심&quot;
            return (flags &amp;amp; 0x1) != 0
        }
    }
    return false
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 방식도 한계가 있습니다. 일부 구형 탈옥 환경에서는 코드 서명 우회 과정에서 &lt;code&gt;CS_VALID&lt;/code&gt;가 꺼지는 경우가 있었지만, 현대의 탈옥 도구들은 &lt;code&gt;CS_VALID&lt;/code&gt;를 그대로 유지하면서 코드 서명 검증만 선택적으로 우회하는 방향으로 진화했습니다. 이 방법은 현대 탈옥 환경에서 신뢰도가 제한적이므로, 다른 감지 기법과 함께 보조적으로 사용하는 것이 적절합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 &lt;code&gt;&quot;sys&quot; + &quot;call&quot;&lt;/code&gt; 문자열 분리 난독화는 &lt;code&gt;strings&lt;/code&gt; 명령어 같은 정적 분석 수준에서는 효과가 있지만, Frida처럼 런타임에서 동작하는 분석 툴 앞에서는 의미가 없습니다. 난독화는 정적 분석의 장벽을 높이는 것이지, 동적 분석까지 막아주지는 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;한계 &amp;mdash; 왜 우회가 가능한가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 방법들은 모두 우회가 가능합니다. 파일 존재 여부 확인의 경우, &lt;code&gt;FileManager&lt;/code&gt;의 Swift 브리지 레이어를 후킹해서 해당 파일이 없는 것처럼 속일 수 있습니다. &lt;code&gt;URL Scheme&lt;/code&gt; 확인도 마찬가지 입니다. 그래서 고급 구현에서는 &lt;code&gt;FileManager&lt;/code&gt;대신 &lt;code&gt;POSIX&lt;/code&gt;의 &lt;code&gt;stat()&lt;/code&gt; 같은 저수준 API를 직접 사용하기도 하는데, Swift/ObjC 레이어보다 후킹이 어렵기 때문입니다. &lt;code&gt;syscall&lt;/code&gt; 기반 감지도 결국 런타임에서 실행되는 코드이기 때문에, 후킹 툴이 개입할 여지가 있습니다. 탈옥 감지 자체를 우회하는 Cydia 트윅도 이미 많이 존재합니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 앱 무결성 검증 (App Integrity)&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;앱 위변조란 무엇인가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱 위변조는 공격자가 앱 바이너리를 수정한 뒤 다시 서명해서 배포하는 행위입니다. 이렇게 변조된 앱은 원본 앱과 겉으로는 구별이 어렵지만, 내부 동작이 바뀌어 있습니다. 무결성 검증은 &quot;지금 실행 중인 앱이 정말 우리가 서명한 원본인가&quot;를 확인하는 과정입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Team ID 기반 검증 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱의 &lt;code&gt;Bundle ID&lt;/code&gt;와 &lt;code&gt;Team ID&lt;/code&gt;를 &lt;code&gt;SHA256&lt;/code&gt;으로 해싱한 뒤, 미리 하드코딩해둔 값과 비교하는 방법입니다. &lt;code&gt;Team ID&lt;/code&gt;는 &lt;code&gt;Keychain&lt;/code&gt;의 &lt;code&gt;Access Group&lt;/code&gt; 속성에서 가져올 수 있습니다.&lt;/p&gt;
&lt;pre class=&quot;swift&quot;&gt;&lt;code&gt;private func checkToAreIDsNotMatched() -&amp;gt; Bool? {
    guard let bundleID = Bundle.main.bundleIdentifier else { return nil }
    guard let teamID = getTeamID() else { return nil }

    // getSha256의 반환값이 옵셔널이므로 반드시 언래핑 후 비교해야 합니다
    guard let bundleHash = getSha256(bundleID),
          let teamHash = getSha256(teamID) else { return nil }

    // Bundle ID 해시와 Team ID 해시 중 하나라도 원본과 다르면 위변조로 판단
    if bundleHash != &quot;abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz&quot;
        || teamHash != &quot;abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz&quot; {
        return true
    }
    return false
}

private func getSha256(_ text: String) -&amp;gt; String? {
    guard let data = text.data(using: .utf8) else { return nil }
    let digest = SHA256.hash(data: data)
    return digest.compactMap { String(format: &quot;%02x&quot;, $0) }.joined()
}

private func getTeamID() -&amp;gt; String? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: &quot;bundleSeedID&quot;,
        kSecAttrService as String: &quot;&quot;,
        kSecReturnAttributes as String: true
    ]

    var item: CFTypeRef?
    let status = SecItemAdd(query as CFDictionary, &amp;amp;item)

    if status == errSecDuplicateItem {
        SecItemCopyMatching(query as CFDictionary, &amp;amp;item)
    }

    guard let existingItem = item as? [String: Any],
          let accessGroup = existingItem[kSecAttrAccessGroup as String] as? String else {
        return nil
    }

    let components = accessGroup.components(separatedBy: &quot;.&quot;)
    return components.first
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;직접 겪은 한계 &amp;mdash; 클라이언트 검증의 근본적인 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식을 실제로 구현하고 탈옥 환경에서 테스트해봤을 때, 위변조된 앱을 제대로 잡지 못하는 경우가 있었습니다. 처음에는 &lt;code&gt;getTeamID()&lt;/code&gt;의 &lt;code&gt;Keychain API&lt;/code&gt; 후킹 문제를 의심했습니다. 탈옥 환경에서는 Frida로 &lt;code&gt;SecItemAdd&lt;/code&gt;나 &lt;code&gt;SecItemCopyMatching&lt;/code&gt; 같은 &lt;code&gt;Keychain API&lt;/code&gt; 자체를 후킹해서, 항상 정상적인 &lt;code&gt;Team ID&lt;/code&gt;를 반환하도록 조작할 수 있기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 더 근본적인 문제가 있습니다. 하드코딩된 해시값과 비교하는 로직 자체가 바이너리 안에 들어 있다는 것입니다. 공격자는 앱 바이너리를 역공학으로 분석해서 해시 비교 조건을 찾아낸 뒤, 그 분기 자체를 패치해버릴 수 있습니다. &lt;code&gt;getTeamID()&lt;/code&gt;가 뭘 반환하든 상관없이, 비교 로직이 항상 &quot;일치함&quot;을 반환하도록 바이너리를 수정해버리는 것입니다. 즉, 무엇을 어떻게 가져오느냐의 문제보다, &lt;b&gt;검증 로직 자체가 공격 대상의 바이너리 안에 존재한다는 것&lt;/b&gt;이 클라이언트 무결성 검증의 근본적인 한계입니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그렇다면 어떻게 해야 하는가? &amp;mdash; App Attest&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 한계를 극복하기 위해 Apple은 iOS 14부터 &lt;b&gt;App Attest&lt;/b&gt; API를 공식 제공하고 있습니다. App Attest는 클라이언트가 Apple 서버에 앱의 무결성 증명(attestation)을 요청하고, 서버가 그 증명을 Apple 서버를 통해 검증하는 방식입니다. 핵심은 검증 로직이 클라이언트 바이너리가 아닌 &lt;b&gt;서버와 Apple 인프라&lt;/b&gt;에 있다는 것입니다. 공격자가 클라이언트 코드를 아무리 뜯어봐도 검증을 우회할 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 보기 전에 두 가지 개념을 먼저 짚어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫째, &lt;code&gt;challenge&lt;/code&gt;의 역할입니다. 서버에서 일회성 난수(challenge)를 받아서 attestation에 포함시키는 이유는 &lt;b&gt;리플레이 공격(replay attack)&lt;/b&gt; 을 막기 위해서 입니다. 만약 challenge 없이 attestation을 재사용할 수 있다면, 공격자가 정상 기기에서 한 번 통과한 attestation 결과를 녹화해뒀다가 나중에 변조된 앱에서 그대로 제출할 수 있습니다. 매번 새로운 난수를 포함시켜 서명하도록 강제함으로써, 이전에 발급된 attestation은 다시 쓸 수 없게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, &lt;code&gt;generateKey()&lt;/code&gt;와 &lt;code&gt;attestKey()&lt;/code&gt;는 &lt;b&gt;앱 최초 실행 시 딱 한 번씩만 호출&lt;/b&gt;해야 합니다. Apple은 이 두 API의 역할을 명확히 구분합니다. &lt;code&gt;attestKey()&lt;/code&gt;는 &quot;이 키가 정품 Apple 기기의 정품 앱에서 생성됐다&quot;는 것을 Apple 서버에 &lt;b&gt;최초 등록&lt;/b&gt;하는 절차입니다. 이후 매 실행마다 무결성을 증명할 때는 &lt;code&gt;attestKey()&lt;/code&gt;가 아니라 &lt;code&gt;generateAssertion()&lt;/code&gt;을 사용해야 합니다. &lt;code&gt;generateAssertion()&lt;/code&gt;은 등록된 키를 이용해 &quot;지금 이 요청도 같은 정품 앱/기기에서 보낸 것이다&quot;를 매번 증명하는 API입니다. &lt;code&gt;attestKey()&lt;/code&gt;를 반복 호출하면 rate limit을 소진하고 Apple이 의도한 설계에서도 벗어납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;올바른 흐름을 정리하면 이렇습니다. 최초 실행 시 &lt;code&gt;generateKey() &amp;rarr; attestKey()&lt;/code&gt;를 순서대로 호출해서 &lt;code&gt;keyId&lt;/code&gt;를 저장하고, 이후 실행에서는 저장된 &lt;code&gt;keyId&lt;/code&gt;로 &lt;code&gt;generateAssertion()&lt;/code&gt;을 호출하는 것입니다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;import DeviceCheck

// ── 최초 실행 시 한 번만 호출 ──────────────────────────────────────
// generateKey()로 키를 만들고, attestKey()로 Apple 서버에 등록한 뒤 keyId를 저장
func setupAppAttest() async {
    let service = DCAppAttestService.shared
    guard service.isSupported else { return }

    // 이미 저장된 keyId가 있으면 재등록하지 않음
    if UserDefaults.standard.string(forKey: &quot;appAttestKeyId&quot;) != nil { return }
    // keyId 자체는 민감한 값이 아니어서 UserDefaults에 저장해도 무방
    // 다만 보안이 중요한 앱이라면 Keychain에 저장하는 것이 더 일관된 선택

    do {
        // 1. 키 생성 &amp;mdash; 최초 1회만 호출, 매번 호출하면 rate limit 소진
        let keyId = try await service.generateKey()

        // 2. Apple 서버에 키 등록 (최초 1회)
        //    &quot;이 키는 정품 기기의 정품 앱에서 만들어졌다&quot;를 Apple이 서명
        let challenge = try await fetchChallengeFromServer()
        let clientDataHash = Data(SHA256.hash(data: challenge))
        let attestation = try await service.attestKey(keyId, clientDataHash: clientDataHash)

        // 3. 서버에 attestation 전달 &amp;mdash; 서버가 Apple 서버를 통해 최종 검증
        try await sendAttestationToServer(keyId: keyId, attestation: attestation)

        // 4. 등록 완료된 keyId 저장
        UserDefaults.standard.set(keyId, forKey: &quot;appAttestKeyId&quot;)
    } catch {
        print(&quot;App Attest setup failed: \(error)&quot;)
    }
}

// ── 서버에 민감한 요청을 보낼 때마다 호출 ────────────────────────────
// 앱 실행 시점이 아니라, 로그인/결제/개인정보 조회처럼
// 서버가 &quot;이 요청이 정품 앱에서 온 것인지&quot; 확인해야 하는 시점에 호출
// assertion을 요청 본문과 함께 서버로 전송하면, 서버가 무결성을 검증할 수 있음
// attestKey()가 아닌 generateAssertion()을 써야 한다는 점에 주의
func assertAppIntegrity() async {
    let service = DCAppAttestService.shared
    guard service.isSupported else { return }

    guard let keyId = UserDefaults.standard.string(forKey: &quot;appAttestKeyId&quot;) else {
        // keyId가 없으면 setupAppAttest()가 먼저 호출
        return
    }

    do {
        // challenge를 포함해 서명하면 리플레이 공격을 막을 수 있음
        let challenge = try await fetchChallengeFromServer()
        let clientDataHash = Data(SHA256.hash(data: challenge))

        // generateAssertion(): 등록된 키로 &quot;지금 이 요청도 정품 앱에서 보낸 것이다&quot;를 증명
        let assertion = try await service.generateAssertion(keyId, clientData: clientDataHash)

        try await sendAssertionToServer(keyId: keyId, assertion: assertion)
    } catch {
        print(&quot;App integrity assertion failed: \(error)&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트에서의 &lt;code&gt;Team ID&lt;/code&gt; 해시 비교는 공격자가 바이너리를 패치하면 무력화되지만, &lt;code&gt;App Attest&lt;/code&gt;는 Apple 서버가 직접 서명한 증명서를 사용하기 때문에 클라이언트 코드 조작으로는 우회할 수 없습니다. 물론 &lt;code&gt;App Attest&lt;/code&gt;도 완벽하지는 않습니다. 시뮬레이터와 일부 기기에서는 지원하지 않고, 네트워크 요청이 필요하기 때문에 오프라인 상황에서의 처리 정책도 고려해야 합니다. 그럼에도 불구하고, 클라이언트 단독 검증에 비해 훨씬 강건한 방어선입니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 그래도 해야 하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 읽으면 &quot;어차피 다 우회되는데 왜 해?&quot;라는 생각이 들 수도 있습니다. 충분히 합리적인 생각입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서 보안을 바라보는 올바른 시각은 &quot;완벽한 방어&quot;가 아니라 &lt;b&gt;&quot;공격 비용을 높이는 것&quot;&lt;/b&gt; 입니다. 탈옥 감지와 무결성 검증이 있으면, 공격자는 이것들을 먼저 우회해야 합니다. 우회하는 데 시간과 노력이 들고, 그 과정에서 포기하는 공격자도 생깁니다. 자물쇠가 없는 집보다 자물쇠가 있는 집이 더 안전한 것처럼, 완벽하지 않아도 장벽이 존재하는 것 자체로 의미가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안은 단일 레이어로 완성되지 않습니다. 앞서 &lt;code&gt;syscall&lt;/code&gt; 코드에서 봤던 &lt;code&gt;&quot;sys&quot; + &quot;call&quot;&lt;/code&gt; 문자열 분리처럼, 각각의 기법은 하나씩 떼어놓으면 우회 가능하지만 여러 겹으로 쌓이면 공격자의 부담이 기하급수적으로 늘어납니다. 탈옥 감지, 앱 무결성 검증, SSL Pinning, 난독화, 그리고 App Attest와 서버 사이드 검증까지 하나가 뚫려도 다음 레이어가 버텨주는 구조, 이것이 보안을 설계하는 기본 구조인 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 결제/인앱구매 우회 사례에서 &quot;서버 검증이 없으면 취약하다&quot;고 했는데, 이게 바로 다중 레이어의 전형적인 예시입니다. 클라이언트에서 아무리 정교하게 검증해도 서버가 최종 확인을 하지 않으면 그 레이어는 허수가 됩니다. 반대로 서버 검증이 있으면 클라이언트 레이어가 뚫려도 실질적인 피해로 이어지지 않습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서 다룬 내용을 정리하면 이렇습니다. iOS는 샌드박스와 코드 서명이라는 견고한 보안 구조를 갖고 있지만, 탈옥이 되면 그 구조가 무너지게 됩니다. 탈옥 기기에서 공격자는 런타임 조작, 네트워크 가로채기, 메모리 덤프, 앱 위변조 등 다양한 공격을 시도할 수 있습니다. 탈옥 감지는 어느 정도 진입 장벽을 높여주지만, 탈옥 방식의 진화(rootless 탈옥 등)에 맞춰 감지 로직도 계속 업데이트해야 합니다. 클라이언트 단독 무결성 검증은 바이너리 패치로 무력화될 수 있으며, 이를 보완하기 위해 App Attest 같은 서버 연동 검증을 함께 사용하는 것이 바람직합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;완벽한 보안은 없습니다. 하지만 공격자의 비용을 높이고, 방어 레이어를 두껍게 쌓아가는 것. 그것이 우리가 할 수 있는 최선이고, 그래서 해야 하는 이유입니다.&lt;/p&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>ios</category>
      <category>Jailbreak</category>
      <category>탈옥</category>
      <category>탈옥감지</category>
      <category>탈옥방지</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/99</guid>
      <comments>https://modelinspring.tistory.com/99#entry99comment</comments>
      <pubDate>Sat, 25 Apr 2026 22:35:47 +0900</pubDate>
    </item>
    <item>
      <title>[macOS] .pkg(.dmg) 배포를 위한 서명 및 공증(Notarization)</title>
      <link>https://modelinspring.tistory.com/98</link>
      <description>&lt;p&gt;macOS 앱을 배포할 때 가장 까다로운 부분 중 하나가 바로 &lt;strong&gt;공증(Notarization)&lt;/strong&gt; 과정입니다.&lt;br&gt;여러 개의 .dmg 앱이 포함된 .pkg 파일을 만들면서 직접 겪고 해결한 과정들을 정리해 보았습니다. &lt;/p&gt;
&lt;hr&gt;
&lt;h1&gt;[macOS] .pkg 배포를 위한 서명 및 공증(Notarization)&lt;/h1&gt;
&lt;p&gt;macOS 앱을 웹에서 직접 배포(Direct Distribution)하려면, 단순히 빌드하는 것만으로는 부족합니다.&lt;br&gt;별다른 작업 없이 그냥 파일을 전달하면 사용자가 파일을 열었을 때 개발자가 인증되지 않아서 파일을 실행할 수 없다는 내용의 경고를 확인하게 됩니다.&lt;br&gt;설정에 들어가서 경고를 무시하고 실행할 수 있도록 하는 불편한 과정을 감수해야 앱을 실행할 수 있습니다.&lt;br&gt;이런 경고를 없애려면 Apple로부터 &lt;strong&gt;공증&lt;/strong&gt;을 받아야 합니다.&lt;/p&gt;
&lt;h1&gt;앱 서명 및 공증 (Xcode 활용)&lt;/h1&gt;
&lt;p&gt;단일 .dmg 앱을 배포하는 것이든 .pkg 설치 앱을 배포하는 것이든 그것을 이루는 모든 요소들이 공증되어야 합니다.&lt;br&gt;.pkg의 경우에는 그 안에 들어가는 모든 개별 앱이 공증되어야 합니다.&lt;br&gt;개별 앱의 경우에는 모바일 앱 아카이빙 과정과 크게 다르지 않습니다.&lt;br&gt;아카이빙을 통해 공증을 진행할 수 있습니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Archive:&lt;/strong&gt; Xcode 메뉴에서 Product -&amp;gt; Archive를 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distribute App:&lt;/strong&gt; 아카이브 완료 후 Distribute App을 클릭합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom Distribution:&lt;/strong&gt; 배포 방식에서 Direct Distribution을 선택합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Upload:&lt;/strong&gt; 서명 후 Apple 서버에 업로드합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Export:&lt;/strong&gt; 상태가 Ready to distribute로 바뀌면 우측 하단의 &lt;strong&gt;Export Notarized App&lt;/strong&gt;을 눌러 공증된 앱을 다운로드합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h1&gt;앱 전용 암호 생성&lt;/h1&gt;
&lt;p&gt;이제 앱들을 포함하는 .pkg을 공증하는 작업을 진행하도록 하겠습니다.&lt;br&gt;notarytool를 사용해서 공증을 진행해야합니다.&lt;br&gt;그 이전에 Apple ID 비밀번호 대신 &lt;strong&gt;앱 전용 암호&lt;/strong&gt;가 필요합니다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&quot;https://account.apple.com/&quot;&gt;Apple ID 계정 페이지&lt;/a&gt;에 로그인합니다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;로그인 및 보안&lt;/strong&gt; 섹션에서 &lt;strong&gt;앱 암호&lt;/strong&gt;를 선택합니다.&lt;/li&gt;
&lt;li&gt;서비스 이름을 입력하고 생성된 암호(예: xxxx-xxxx-xxxx-xxxx)를 별도로 메모해 둡니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h1&gt;.pkg 공증하기(notarytool)&lt;/h1&gt;
&lt;p&gt;이제 앱들을 포함시킨 .pkg 파일을 공증할 차례입니다.&lt;/p&gt;
&lt;h3&gt;서명 상태 확인&lt;/h3&gt;
&lt;p&gt;공증을 올리기 전, 파일이 제대로 서명되었는지 먼저 확인해야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# pkg 파일 서명 확인
pkgutil --check-signature &amp;quot;패키지파일.pkg&amp;quot;

# 앱 번들 서명 상세 확인
codesign -dvvv &amp;quot;앱파일.app&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;notarytool&lt;/h3&gt;
&lt;p&gt;Apple의 공증 도구인 &lt;code&gt;notarytool&lt;/code&gt;을 사용하는 방법입니다.&lt;br&gt;커맨드에 매번 id와 비밀번호를 입력할 수 있고, 미리 저장해서 사용할 수도 있습니다.&lt;/p&gt;
&lt;h3&gt;자격 증명 등록&lt;/h3&gt;
&lt;p&gt;매번 자격에 대한 명령어를 입력할 필요 없이 profile을 지정해 사용하는 방식입니다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1단계: 자격 증명 저장&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xcrun notarytool store-credentials &amp;quot;my-profile&amp;quot; \
--apple-id &amp;quot;본인의_애플_아이디@email.com&amp;quot; \
--team-id &amp;quot;10자리_팀ID&amp;quot; \
--password &amp;quot;xxxx-xxxx-xxxx-xxxx&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;my-profile&lt;/code&gt;: 앞으로 사용할 프로필 별명입니다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;team-id&lt;/code&gt;: Apple 개발자 포털 우측 상단에서 확인 가능한 10자리 영문/숫자 조합입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2단계: 공증 제출 및 대기&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xcrun notarytool submit &amp;quot;서명된파일.pkg&amp;quot; --keychain-profile &amp;quot;my-profile&amp;quot; --wait&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--wait&lt;/code&gt; 옵션: 검사가 끝날 때까지 터미널이 대기하며, 성공 시 &lt;code&gt;status: Accepted&lt;/code&gt;가 뜹니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;커맨드에 자격 증명 내용 추가&lt;/h3&gt;
&lt;p&gt;등록 없이 한 번에 모든 정보를 입력하는 방식입니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xcrun notarytool submit &amp;quot;파일이름.pkg&amp;quot; \
--apple-id &amp;quot;본인의_애플_아이디@email.com&amp;quot; \
--team-id &amp;quot;10자리_팀ID&amp;quot; \
--password &amp;quot;xxxx-xxxx-xxxx-xxxx&amp;quot; \
--wait&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;실패 시 로그 확인&lt;/h2&gt;
&lt;p&gt;만약 결과가 &lt;code&gt;Invalid&lt;/code&gt;로 나온다면 무엇이 문제인지 로그를 뜯어봐야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;# 키체인 프로필 사용 시
xcrun notarytool log &amp;lt;제출ID&amp;gt; --keychain-profile &amp;quot;my-profile&amp;quot;

# 직접 입력 시
xcrun notarytool log &amp;lt;제출ID&amp;gt; --apple-id &amp;quot;...&amp;quot; --team-id &amp;quot;...&amp;quot; --password &amp;quot;...&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;p&gt;제출 ID는 &lt;code&gt;submit&lt;/code&gt; 명령어 실행 후 결과창에 나오는 &lt;code&gt;id: xxxxx...&lt;/code&gt; 부분을 복사하면 됩니다.&lt;/p&gt;
&lt;/span&gt;&lt;/p&gt;&lt;/blockquote&gt;&lt;hr&gt;
&lt;h2&gt;티켓 부착(Staple) 및 최종 검증&lt;/h2&gt;
&lt;h3&gt;티켓 부착 (Staple)&lt;/h3&gt;
&lt;p&gt;공증이 완료되면 Apple의 &amp;quot;검증 완료 티켓&amp;quot;을 파일에 물리적으로 붙여야 합니다.&lt;br&gt;이 과정을 거쳐야 사용자가 오프라인 상태일 때도 앱을 실행할 수 있습니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;xcrun stapler staple &amp;quot;서명된파일.pkg&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;최종 배포 전 확인&lt;/h3&gt;
&lt;p&gt;사용자 환경에서 문제가 없을지 마지막으로 테스트합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;spctl --assess -vv --type install &amp;quot;서명된파일.pkg&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;결과에 &lt;strong&gt;&lt;code&gt;source=Notarized Developer ID&lt;/code&gt;&lt;/strong&gt;라는 문구가 포함되어 있다면 성공입니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;트러블슈팅: 겪은 에러 상황들&lt;/h1&gt;
&lt;h3&gt;Case 1: &amp;quot;signature does not include a secure timestamp&amp;quot;&lt;/h3&gt;
&lt;p&gt;Xcode 빌드 설정에 따라 타임스탬프가 누락되는 경우가 있습니다. 이럴 땐 터미널에서 &lt;code&gt;runtime&lt;/code&gt; 옵션과 함께 강제로 다시 서명해야 합니다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;codesign --force --options runtime --timestamp --sign &amp;quot;Developer ID Application: 이름 (TeamID)&amp;quot; &amp;quot;경로/파일명&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Case 2: 공증은 성공했는데, 다른 Mac(Intel)에서 실행이 안 됨&lt;/h3&gt;
&lt;p&gt;모든 과정이 완벽한데 특정 환경에서 앱이 작동하지 않는다면 아키텍처 문제입니다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;원인&lt;/strong&gt;: 외부 라이브러리 중 하나가 &lt;code&gt;arm64&lt;/code&gt;(Apple Silicon) 전용으로 빌드되어 인텔 맥에서 충돌 발생.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;해결&lt;/strong&gt;: Xcode 빌드 타겟을 &lt;code&gt;Any Mac (Apple Silicon, Intel)&lt;/code&gt;으로 변경하여 유니버설 바이너리로 빌드했는지 확인해서 해결했습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>dmg</category>
      <category>macOS 배포</category>
      <category>notarization</category>
      <category>pkg</category>
      <category>공증</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/98</guid>
      <comments>https://modelinspring.tistory.com/98#entry98comment</comments>
      <pubDate>Sat, 11 Apr 2026 07:03:48 +0900</pubDate>
    </item>
    <item>
      <title>2026년 상반기 iOS 채용 트렌드 분석</title>
      <link>https://modelinspring.tistory.com/97</link>
      <description>&lt;h1&gt;최근 iOS 채용 트렌드: 기업 규모와 특성별로 달라지는 채용 방식&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;2026년 초 iOS 개발자 채용 공고를 분석했습니다.&lt;/li&gt;
&lt;li&gt;채용하는 기업 규모 또는 특성별로 지원자에게 요구하는 역량을 정리했습니다.&lt;/li&gt;
&lt;li&gt;당근, 안랩, 29cm(무신사), 한국철도공사, 쿠팡, 하이퍼커넥트, KREAM, 카카오페이, 오늘의집, 토스플레이스, 쏘카일레클의 채용공고를 기반으로 작성했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;공통 트렌드&lt;/h1&gt;
&lt;h4&gt;&lt;strong&gt;“iOS 단독”보다 “제품 팀 기여”를 더 강하게 본다&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;iOS 개발은 구현력만으로 차별화가 어려워지면서, &lt;em&gt;문제 정의 → 실험 → 개선&lt;/em&gt; 과정에 참여한 경험을 요구하는 비중이 커졌습니다.&lt;/li&gt;
&lt;li&gt;협업 역량은 단순 커뮤니케이션이 아니라, &lt;strong&gt;요구사항을 설계 언어로 바꾸고 합의하는 능력&lt;/strong&gt;으로 평가됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;채용 포지션이 “기능 개발”에서 “품질/성능/안정성”으로 이동&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;신규 기능이 아니라, &lt;strong&gt;리팩터링, 모듈화, 성능 최적화, 크래시/ANR 대응, CI 안정화&lt;/strong&gt; 같은 운영 역량이 JD에 더 자주 등장합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;Swift Concurrency, 모던 iOS 스택이 사실상 기본 소양화&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;SwiftUI를 “필수”로 두지 않더라도, &lt;strong&gt;비동기 처리, 상태 관리, 테스트 가능한 구조&lt;/strong&gt;는 공통 기대치가 됐습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;과제 전형은 줄고, “라이브/리뷰 기반” 평가가 늘어난다&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;대체로 다음 중 하나로 이동합니다.&lt;/li&gt;
&lt;li&gt;제한된 시간 내 문제 해결(페어/라이브)&lt;/li&gt;
&lt;li&gt;기존 코드/PR 리뷰 기반 토론&lt;/li&gt;
&lt;li&gt;아키텍처 의사결정 질문(트레이드오프 중심)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;기업 규모/특성별 트렌드&lt;/h1&gt;
&lt;h2&gt;대기업/플랫폼&lt;/h2&gt;
&lt;h4&gt;&lt;strong&gt;채용에서 중요하게 보는 것&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;안정성, 관측 가능성(Observability), 장애 대응 경험&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;점진적 개선 경험&lt;/strong&gt;: 큰 코드베이스에서 안전하게 바꾸는 방법&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;협업 프로세스 성숙도&lt;/strong&gt;: 리뷰 문화, 배포/릴리즈 관리&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;자주 나오는 키워드&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;모듈화(Feature/Layer), 의존성 관리(SPM 등)&lt;/li&gt;
&lt;li&gt;Crash/Performance 모니터링, 로그/지표 설계&lt;/li&gt;
&lt;li&gt;테스트 전략(Unit/UI), 회귀 방지&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;의미&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;규모가 커질수록 “빠른 구현”보다 &lt;strong&gt;실수의 비용을 줄이는 역량&lt;/strong&gt;이 중요해집니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;중견/성장기 스타트업(스케일업)&lt;/h2&gt;
&lt;h4&gt;&lt;strong&gt;채용에서 중요하게 보는 것&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;속도와 품질의 균형&lt;/strong&gt;을 잡는 능력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;도메인 이해 기반 설계&lt;/strong&gt;: 단순 MVVM 암기보다, 요구사항 변화를 견디는 구조&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;제품 실험 경험&lt;/strong&gt;: 실험 설계, 데이터 기반 개선, 빠른 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;자주 나오는 키워드&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;아키텍처 의사결정(“왜” 그렇게 했는지)&lt;/li&gt;
&lt;li&gt;피처 플래그, A/B 테스트 연동, 실험→회고&lt;/li&gt;
&lt;li&gt;레거시 정리와 신규 개발 병행&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;의미&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;성장기에는 “구조를 크게 갈아엎는 리뉴얼”보다 &lt;strong&gt;부분 최적화와 기술부채 관리의 우선순위 설정&lt;/strong&gt;이 평가 포인트가 됩니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;초기 스타트업/소규모 팀(0→1)&lt;/h2&gt;
&lt;h4&gt;&lt;strong&gt;채용에서 중요하게 보는 것&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;End-to-End 오너십&lt;/strong&gt;: 기획/디자인/서버와 함께 MVP를 만드는 능력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;우선순위 설정&lt;/strong&gt;: 무엇을 안 할지 결정하는 능력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;범용성&lt;/strong&gt;: iOS 하나만이 아니라 앱 전반 문제(로그인, 결제, 푸시, 분석)를 해결하는 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;자주 나오는 키워드&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;빠른 MVP, 린한 설계, 최소한의 테스트/자동화&lt;/li&gt;
&lt;li&gt;분석 도구 연동(이벤트 설계 포함)&lt;/li&gt;
&lt;li&gt;외부 SDK/서드파티 통합과 안정화&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;의미&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;초기에는 “정답 아키텍처”보다 &lt;strong&gt;사업 가설을 검증할 수 있는 실행력&lt;/strong&gt;을 더 강하게 봅니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;B2B/엔터프라이즈(업무용 앱, 규정/보안 중심)&lt;/h2&gt;
&lt;h4&gt;&lt;strong&gt;채용에서 중요하게 보는 것&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;보안/권한/감사(Audit) 요구사항 대응&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;안정적인 배포/운영&lt;/strong&gt;: MDM, 사내 인증, 기기/OS 버전 제약&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;오프라인/동기화&lt;/strong&gt; 등 복잡한 상태를 다루는 능력&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;자주 나오는 키워드&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;인증/암호화/키체인, 네트워크 보안&lt;/li&gt;
&lt;li&gt;데이터 동기화/충돌 해결&lt;/li&gt;
&lt;li&gt;접근성/가이드라인 준수&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;strong&gt;의미&lt;/strong&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;사용자 경험도 중요하지만, 더 근본적으로는 &lt;strong&gt;리스크를 통제하는 개발 역량&lt;/strong&gt;이 핵심입니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;iOS 개발자(저연차)로서 어떤 역량을 준비해야할까?&lt;/h1&gt;
&lt;h4&gt;공통 기본 역량&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;문제 해결 방식의 설명력&lt;/strong&gt;: 선택의 이유와 트레이드오프를 논리적으로 설명할 수 있는 능력&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;협업을 위한 설계/커뮤니케이션&lt;/strong&gt;: PR 단위 설계, 리뷰 대응, 합의 과정&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;안정성/품질 기본기&lt;/strong&gt;: 크래시 대응, 성능 감, 테스트 가능 구조&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;기업의 개발 환경과 본인 커리에 따른 기술&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Swift Concurrency 실전 적용&lt;/strong&gt;: race condition, cancellation, structured concurrency 이해&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SwiftUI 경험&lt;/strong&gt;: 특히 신규 화면/프로토타이핑에서 선호&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모듈화/의존성 관리&lt;/strong&gt;: 규모가 큰 팀일수록 가산&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;추가로 준비하면 도움이 되는 경험&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;지표 기반 제품 개선 경험&lt;/strong&gt;: 실험 설계, 이벤트 정의, 결과 해석&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;릴리즈/배포 자동화&lt;/strong&gt;: CI 안정화, fastlane, 코드 서명 관리&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;접근성/국제화/다국어&lt;/strong&gt; 같은 제품 완성도 경험&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;실제 취업/이직을 위한 준비과정&lt;/h1&gt;
&lt;h2&gt;“iOS 단독 역량”이 아닌 “제품과 팀 기여에 대한 기여&amp;quot;를 증명하기&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;목표는 ‘기능 구현자’가 아니라 ‘제품 문제를 해결하는 개발자’로서의 나&lt;/strong&gt;를 잘 전달하기&lt;/li&gt;
&lt;li&gt;다음 3가지를 &lt;strong&gt;증명 가능한 형태(프로젝트/PR/글/지표)&lt;/strong&gt;로 서술하기&lt;ul&gt;
&lt;li&gt;문제를 어떻게 정의했고, 어떤 가설을 세웠는지&lt;/li&gt;
&lt;li&gt;트레이드오프를 왜 그렇게 선택했는지&lt;/li&gt;
&lt;li&gt;출시 후 무엇을 관측했고, 어떤 개선을 했는지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;채용 프로세스 대비&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;라이브 코딩/페어 형태 대비&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;제한 시간 안에 “정확한 동작 + 예외 처리 + 설명”까지 하도록 연습&lt;/li&gt;
&lt;li&gt;자료구조/알고리즘보다 &lt;strong&gt;문제 분해, 상태 관리, 테스트 관점&lt;/strong&gt;이 자주 평가됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;코드 리뷰 기반 토론 대비&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;본인 프로젝트의 PR을 3~5개 골라 _의도, 대안, 리스크, 다음 개선_을 말로 정리&lt;/li&gt;
&lt;li&gt;“왜 MVVM인지”가 아니라 &lt;strong&gt;왜 이 화면/도메인에 이 구조가 적합했는지&lt;/strong&gt;가 핵심&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;아키텍처 질문 대비(트레이드오프 중심)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;동시성, 캐시/오프라인, 의존성 분리, 모듈화의 _장단점_을 사례로 답변&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;기술 스택 우선순위&lt;/h2&gt;
&lt;h4&gt;바로 실무 투입을 보여주는 기본기&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Swift Concurrency 실전&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;async/await&lt;/code&gt;, &lt;code&gt;Task&lt;/code&gt;, cancellation, structured concurrency의 기본 흐름&lt;/li&gt;
&lt;li&gt;공통 문제: race condition, 중복 요청, 화면 전환 중 작업 취소, 메모리 누수&lt;/li&gt;
&lt;li&gt;연습 과제: “검색 + 디바운스 + 취소 + 캐시”를 Concurrency로 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;테스트 가능한 구조&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;의존성 주입(DI) 기본, 프로토콜 기반 추상화&lt;/li&gt;
&lt;li&gt;Unit test에서 네트워크/시간/스토리지를 분리해 검증&lt;/li&gt;
&lt;li&gt;UI 테스트는 최소 범위라도 _핵심 플로우 1~2개_는 구축&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;품질/안정성&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Crash 로그 분석(심볼리케이션, 재현/원인 추적)&lt;/li&gt;
&lt;li&gt;성능: 메인 스레드 블로킹, 이미지 처리, 스크롤 성능, 메모리 사용량&lt;/li&gt;
&lt;li&gt;관측: 이벤트/로그/지표를 “왜 필요한지” 설명할 수 있게&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;기업/팀 성격에 따라 준비해야할 부분&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;아키텍처 선택 능력&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;MVVM, Clean, TCA 등을 ‘암기’가 아니라 &lt;strong&gt;상황에 맞게 선택&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;예: 화면 수가 늘 때 테스트/유지보수/속도 간 균형을 어떻게 잡았는지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모듈화/의존성 관리(SPM)&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;Feature/Layer 분리의 목적: 빌드 시간, 팀 분업, 변경 영향 범위 축소&lt;/li&gt;
&lt;li&gt;“작게 시작해서 점진적으로 나누는 방법”을 알고 있으면 강점&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CI/CD 기본&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;테스트 자동 실행, 린트, 빌드/배포 파이프라인의 최소 구성&lt;/li&gt;
&lt;li&gt;fastlane 경험이 있으면 좋지만, &lt;strong&gt;왜 자동화가 필요한지&lt;/strong&gt;가 더 중요&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;차별화 포인트&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SwiftUI&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;신규 화면/프로토타이핑에서 선호&lt;/li&gt;
&lt;li&gt;UIKit 기반 팀이라도 “SwiftUI를 섞을 때의 리스크/장점”을 말할 수 있으면 가점&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;지표 기반 개선 경험&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;이벤트 설계 → 실험 → 결과 해석 → 다음 액션까지 한 사이클을 만들어 보기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;접근성/국제화&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;실제 제품 완성도에 직결되며, 주니어의 차별화에 좋음&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h1&gt;마무리&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;지금 iOS 채용은 “얼마나 많은 기능을 만들었는가”보다 &lt;strong&gt;어떤 문제를 어떻게 풀었고, 그 선택을 어떻게 검증했는가&lt;/strong&gt;를 더 강하게 묻는 흐름으로 정리할 수 있습니다.&lt;/li&gt;
&lt;li&gt;다른 개발 직군도 마찬가지겠지만 1년 전과 2년 전의 공고와 비교한다면, 요구하는 역량의 트렌드가 상당히 빠르게 변하고 있는 것 같습니다.&lt;/li&gt;
&lt;li&gt;AI의 사용으로 인한 개발 생산성 증가가 IT 산업에 전반적으로 영향을 미치고 있습니다.&lt;/li&gt;
&lt;li&gt;단순 아키텍처나 기술 중심의 역량을 벗어나서 기본기에 대한 탄탄한 능력과 거시적으로 도메인과 소프트웨어 공학, 개발 과정 전체를 바라볼 수 있는 사람이 필요하게 되었습니다.&lt;/li&gt;
&lt;li&gt;&amp;#39;iOS 개발자&amp;#39;라는 직무 또는 직군으로 언제까지 채용공고가 게시될 지 솔직히 잘 모르겠습니다.&lt;br&gt;곧 모바일개발자, 더 나아가서 클라이언트 개발자 등으로 확장되지 않을까 하는 생각이 듭니다.&lt;/li&gt;
&lt;li&gt;본인이 담당하는 업무는 정말 깊게 알아야하고 추가로 프론트, 서버, 백, 마케팅, 디자인 등과의 단순한 협업이 아닌 전반적인 이해를 바탕으로 업무를 진행할 수 있는 플레이어가 되어야 합니다.&lt;/li&gt;
&lt;li&gt;이제는 단순히 1명의 개발자를 원하는 시대는 지났습니다.&lt;br&gt;다양한 AI 툴들을 기반으로 본인이 맡은 롤은 완벽히 수행하고, 업무를 수행하면서 만나는 다른 분야의 일도 어느정도 주도적으로 해결해나갈 수 있는 사람이 필요하게 되었습니다.&lt;/li&gt;
&lt;li&gt;개발자는 항상 공부하는 직업이었습니다.&lt;br&gt;AI가 발전하면서 공부가 필요없게 되지 않을까 생각했지만, 역설적으로 더 넓고 많은 것을 알기위해 공부해야하는 상황이 되었습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>programming/Others</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/97</guid>
      <comments>https://modelinspring.tistory.com/97#entry97comment</comments>
      <pubDate>Sat, 28 Mar 2026 11:23:25 +0900</pubDate>
    </item>
    <item>
      <title>[Vapor/Swift] 프로젝트 생성부터 NAS 배포까지: with 트러블슈팅</title>
      <link>https://modelinspring.tistory.com/96</link>
      <description>&lt;h1&gt;[Vapor/Swift] 프로젝트 생성부터 Synology NAS 배포까지: with 트러블슈팅&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 개발자들은 항상 서버, 백, API에 굶주리곤 합니다.&lt;br /&gt;개인적으로 사용할 서비스에서 스케줄링 작업이 필요했습니다.&lt;br /&gt;주 언어인 Swift, Swift에서 사용할 수 있는 웹 프레임워크 Vapor, 집에서 사용중인 NAS를 이용해서 배포한 과정을 공유합니다.&lt;br /&gt;(Swift로 백엔드 개발하는 사람이 여기있네)&lt;br /&gt;자세한 설치 방법은 공식문서를 참고해주세요.&lt;br /&gt;전체적인 배포 흐름과 트러블슈팅을 중심으로 글이 작성되었습니다.&lt;br /&gt;사용하는 프로젝트도 vapor 생성 시 준비되는 기본 todo 프로젝트입니다.&lt;/p&gt;
&lt;h1&gt;1. 프로젝트 시작하기&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;터미널에서 프로젝트를 생성할 디렉토리로 이동한 뒤 아래 명령어를 입력합니다.&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;vapor new sampleVapor&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 ORM(Fluent) 사용이 필요한 경우 y를 입력하세요.&lt;/li&gt;
&lt;li&gt;Leaf가 필요한 경우 y를 입력하세요.&lt;/li&gt;
&lt;li&gt;모든 옵션에 '아니오'로 답하고 싶다면 &lt;code&gt;-n&lt;/code&gt; 플래그를 붙이세요. (&lt;code&gt;vapor new sampleVapor -n&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;저는 DB가 필요해서 ORM(Fluent), Postgres를 사용했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설치가 완료되면 &lt;code&gt;Package.swift&lt;/code&gt;를 실행해 Xcode에서 프로젝트를 엽니다.&lt;br /&gt;처음에는 의존성을 설치하느라 시간이 조금 필요할 수 있습니다.&lt;br /&gt;실행 기기를 &lt;b&gt;My Mac&lt;/b&gt;으로 선택하고 Run을 눌러봅시다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. 로컬 실행 중 마주친 문제들&lt;/h1&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Fatal error: Error raised at top level: bind(descriptor:ptr:bytes:): Address already in use) (errno: 48)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 실행 중인 Vapor 프로세스가 포트(기본 8080)를 점유하고 있을 때 발생합니다.&lt;br /&gt;저는 이미 기존에 사용 중인 프로젝트가 있어서 발생했습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Edit Scheme -&amp;gt; Run -&amp;gt; Arguments&lt;/code&gt;에 &lt;code&gt;serve --port 8081&lt;/code&gt;을 추가하여 포트를 변경합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;No custom working directory set for this scheme&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vapor가 리소스 파일을 찾지 못할 때 발생합니다.&lt;br /&gt;프로젝트가 로컬에서 실행될 때, 임시 폴더가 아닌 프로젝트 폴더에서 실행되도록 해야합니다.&lt;br /&gt;public 폴더 내에 있는 공개 파일에 접근하는 경우에 필요합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;Edit Scheme -&amp;gt; Run -&amp;gt; Options&lt;/code&gt;에서 &lt;b&gt;Use custom working directory&lt;/b&gt;를 체크하고 프로젝트 루트 폴더를 지정합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. Database 연동 및 마이그레이션&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 브라우저에서 &lt;code&gt;/todos&lt;/code&gt; 경로로 접속했을 때 &lt;code&gt;role &quot;유저네임&quot; does not exist&lt;/code&gt; 에러가 난다면 DB 설정이 필요합니다.&lt;br /&gt;vapor내에서 코드로 마이그레이션을 할 수도 있고, docker 이미지를 만들때 마이그레이션 옵션을 지정할 수도 있습니다.&lt;br /&gt;로컬에서는 미리 데이터베이스를 생성해주고 유저 정보도 준비해줘야합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;DB 및 유저 생성 (PostgreSQL)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SQL&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;CREATE DATABASE vapor_database;
CREATE USER vapor_username;
ALTER DATABASE vapor_database OWNER TO vapor_username; -- 권한 부여&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;relation &quot;todos&quot; does not exist&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블이 생성되지 않았을 때 발생합니다. Vapor는 서버 실행 시 자동으로 마이그레이션을 하지 않으므로 플래그를 추가해야 합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 인자에 --auto-migrate를 추가합니다.&lt;/li&gt;
&lt;li&gt;vapor 프로젝트 내에서 코드로 오토 마이그레이션을 사용합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;permission denied for schema public&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저가 테이블 생성 권한이 없을 때 발생합니다. 위에서 언급한 ALTER DATABASE ... OWNER TO 명령어로 소유주를 변경하면 해결됩니다.&lt;br /&gt;정상적으로 마이그레이션이 완료되면 콘솔에 [Migrator] Finished prepare 문구가 뜨며, /todos 접속 시 빈 배열([])을 확인할 수 있습니다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Synology NAS 배포 (Docker 활용)&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로컬에서 잘 돌아가는 프로젝트를 이제 나스로 옮겨봅시다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) 이미지 빌드 및 추출&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나스는 보통 성능이 좋지 않기 때문에 코드를 올린 후 빌드하는 것보다 로컬에서 이미지를 만들어 올리는 게 빠릅니다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;# 1. 앱 이미지만 빌드
docker compose build app

# 2. 빌드된 이미지를 tar 파일로 저장
docker save -o samplevapor.tar samplevapor:latest&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) NAS 이미지 로드&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;samplevapor.tar 파일을 시놀로지 &lt;b&gt;File Station&lt;/b&gt;에 업로드합니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Container Manager&lt;/b&gt; -&amp;gt; &lt;b&gt;이미지&lt;/b&gt; -&amp;gt; &lt;b&gt;가져오기&lt;/b&gt;를 통해 해당 파일을 등록합니다.&lt;/li&gt;
&lt;li&gt;레지스트리에서 postgres 이미지를 다운로드하여 버전을 맞춥니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) 프로젝트 설정 및 트러블슈팅&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Container Manager&lt;/b&gt;에서 프로젝트를 생성할 때 docker-compose.yaml을 사용합니다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;unable to prepare context... Dockerfile: no such file or directory&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미 이미지가 있는데 컴포즈 파일에 build: 명령어가 남아있어 Dockerfile을 찾으려 함&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;yaml 파일에서 &lt;b&gt;build: 섹션을 삭제&lt;/b&gt;하고 image:만 남깁니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;bind: address already in use (5432)&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;나스 자체 DB나 다른 컨테이너가 5432 포트를 이미 사용 중.&lt;/li&gt;
&lt;li&gt;다른 포트를 지정해줍니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. 외부 접속 설정&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 컨테이너가 Exit code: 0 없이 정상 실행되었다면, 외부에서 접속할 차례입니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;공유기 설정(Port Forwarding)에서 외부 포트(예: 8081)를 &lt;b&gt;나스 내부 IP의 8080&lt;/b&gt;으로 연결합니다.&lt;/li&gt;
&lt;li&gt;브라우저에서 http://외부아이피:8081 접속 시 Hello Vapor!가 보이면 성공입니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;6. 그 밖의 에러들&lt;/h1&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Connection request timed out. This might indicate a connection deadlock...&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;AsyncKit.ConnectionPoolTimeoutError.connectionRequestTimeout&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 켜지자마자 DB 연결에서 타임아웃이 발생하면서 에러가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러가지 방법으로 문제 해결을 시도했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Healthcheck 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 서비스 쪽에 pg_isready를 이용한 healthcheck를 넣고, 앱 서비스 쪽에 depends_on: condition: service_healthy를 넣었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;netcat 사용&lt;/li&gt;
&lt;/ol&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;netcat을 사용해서 DB 연결을 직접 확인하고 연결될 때 까지 지연을 시켰습니다.&lt;/li&gt;
&lt;li&gt;Dockerfile 내에 apt-get install -y netcat-openbsd를 추가하고, wait-for-db.sh라는 파일을 도커 이미지 안에 넣었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;7. .env 파일을 사용&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NAS 디렉토리에 .env 파일을 넣었고 컴포즈파일에서도 변수를 잘 읽어오는데, 정작 앱 서버에서는 변수를 읽어오지 못했습니다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;services:
  app:
    # 기존 설정들...
    env_file:
      - .env  # (docker-compose.yml과 같은 폴더에 .env 파일이 있어야 합니다)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컴포즈 파일에서 env_file 옵션을 app에 추가해줍니다.&lt;/p&gt;
&lt;h1&gt;8. 계속 되는 타임아웃&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드, 아이피, 타임아웃 설정을 수정했는데도 계속 연결이 되지 않는 상황이 있었습니다.&lt;br /&gt;Vapor 코드 내에서 DB 환경설정을 할때 &lt;code&gt;tls: .prefer(try .init(configuration: .clientDefault))&lt;/code&gt; 코드가 있었는데 기본 로컬 도커, postgresql18 상황에서 별도의 세팅이 준비되어있지 않아서 문제가 있었습니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;tls: .disable&lt;/code&gt; 로 수정해서 보안 연결을 해제했습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 서버 하나를 띄우는 것이었으나, NAS 환경은 로컬, 클라우드 환경과 달라서 예상치 못한 삽질이 많았습니다.&lt;/p&gt;</description>
      <category>programming/Server</category>
      <category>docker</category>
      <category>docker compose</category>
      <category>Fluent</category>
      <category>Nas</category>
      <category>SWIFT</category>
      <category>synology</category>
      <category>vapor</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/96</guid>
      <comments>https://modelinspring.tistory.com/96#entry96comment</comments>
      <pubDate>Sat, 14 Mar 2026 00:01:21 +0900</pubDate>
    </item>
    <item>
      <title>[Swift/iOS] 토큰 데이터를 KeyChain에 안전하게 저장하기</title>
      <link>https://modelinspring.tistory.com/95</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;앱에서 로그인 후, 서버에서 반환되는 사용자 인증토큰(accessToken과 refreshToken)을 저장하려고 합니다.&lt;br /&gt;CoreData를 사용하기에는 너무 단순한 형태이고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDefaults에는 간단한 설정값을 저장하는데 주로 사용하기 때문에,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증토큰처럼 보안이 중요한 경우에는 다른 방법을 찾아보는게 좋다는 생각이 들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KeyChain은 &lt;b&gt;암호화된 저장소&lt;/b&gt;로 토큰 데이터를 저장하기에 알맞다고 판단한 후 앱에서 사용해보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UserDefaults가 키-값 쌍으로 데이터를 저장하고 불러오는 것처럼, 키체인도 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 KeyChain은 데이터와 속성을 묶고 추가적인 보안 속성을 함께 사용할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키체인 서비스 KeyChain Service&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자를 대신해서 안전하게 작은 데이터 조각을 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Device 안에 암호화된 데이터베이스라고 생각할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KeyChain Service API는 다양한 상황에서 여러 형태의 데이터를 암호화된 데이터베이스에 저장할 수 있는 메커니즘을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호에 한정되지 않고, 신용카드 정보나 짧은 노트 등 사용자가 인식하지 못하지만 필요한 정보도 저장할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서, Key 등을 사용해서 관리하는 암호화 키 등을 저장할 때도 KeyChain을 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;503&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EnQLC/btsd1iAeDlC/vwp4FvraHVAaT3amFKd2v0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EnQLC/btsd1iAeDlC/vwp4FvraHVAaT3amFKd2v0/img.png&quot; data-alt=&quot;https://developer.apple.com/documentation/security/keychain_services&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EnQLC/btsd1iAeDlC/vwp4FvraHVAaT3amFKd2v0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEnQLC%2Fbtsd1iAeDlC%2Fvwp4FvraHVAaT3amFKd2v0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;212&quot; data-origin-width=&quot;951&quot; data-origin-height=&quot;503&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.apple.com/documentation/security/keychain_services&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KeyChain API 구성요소&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;KeyChain Item&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비밀번호나 암호키 같은 정보를 저장할 때, &lt;b&gt;KeyChain Item&lt;/b&gt;으로 패키징합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터와 item의 접근을 관리하고, 검색할 수 있도록 공개 &lt;b&gt;Attributes&lt;/b&gt;을 &lt;b&gt;함께&lt;/b&gt; 패키징합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KeyChain 서비스는 &lt;b&gt;데이터&lt;/b&gt;와 &lt;b&gt;속성&lt;/b&gt;의 저장을 처리하고, 암호화도 처리합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후, 권한이 있는 프로세스는 KeyChain 서비스를 사용해서 아이템을 찾고 데이터를 복호화하는 데 사용합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;319&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIHupP/btsd6t13mks/8xsEAwqM0Ev1kLTkpm7uD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIHupP/btsd6t13mks/8xsEAwqM0Ev1kLTkpm7uD0/img.png&quot; data-alt=&quot;https://developer.apple.com/documentation/security/keychain_services/keychain_items&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIHupP/btsd6t13mks/8xsEAwqM0Ev1kLTkpm7uD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIHupP%2Fbtsd6t13mks%2F8xsEAwqM0Ev1kLTkpm7uD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;121&quot; data-origin-width=&quot;1058&quot; data-origin-height=&quot;319&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.apple.com/documentation/security/keychain_services/keychain_items&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;쉽게 말해서, 저장하고자 하는 &lt;b&gt;데이터&lt;/b&gt;에 데이터 &lt;b&gt;속성&lt;/b&gt;을 더한 것이 KeyChain Item입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;KeyChains&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전체 키체인을 생성하고 관리하는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iOS에서는 앱이 하나의 KeyChain에만 접근할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;iCloud KeyChain을 포함한 하나의 KeyChain이 존재하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱은 앱의 KeyChain 항목 또는 해당 앱이 속한 그룹과 공유된 항목에만 접근할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KeyChain Container 자체를 관리할 수는 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;아이템 클래스의 키와 값 Item Class Keys and Values&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키체인 아이템의 클래스는 여러 종류가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 클래스는 적용되는 속성을 규정하고, 시스템이 암호화 여부를 결정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 비밀번호는 암호화가 필요하고, 인증서는 암호화를 하지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Item Class Values&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;kSecClassGenericPassword&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 비밀번호&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;kSecClassInternetPassword&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터넷 비밀번호&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;kSecClassCertificate&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;kSecClassKey&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화 키&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;kSecClassIdentity&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ID 항목&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이템 클래스마다 속성은 다릅니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;저는 토큰을 저장할 것이기 때문에 kSecClassKey(암호화 키)를 한번 이용해보려고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kSecClassKey의 속성페이지를 보면 굉장히 다양한 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/ksecclasskey&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/security/ksecclasskey&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;몇 가지만 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- kSecAttrLabel : 아이템의 label을 나타내는 문자열 값&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- kSecAttrApplicationLabel : 아이템의 애플리케이션 label을 나타내는 값&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 속성을 사용해서 토큰을 저장해보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키체인 저장하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값 저장을 위해서 데이터를 변수에 임의로 선언했습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683380736799&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let token = &quot;abcd1234&quot;
let label = &quot;accessToken&quot;
let appName = &quot;AppName&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서, 아이템은 데이터와 속성을 합쳐서 패키징한다고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터와 속성을 키와 값 형태인 딕셔너리 형태로 만들어주었습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683380820713&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let saveQuery: NSDictionary = [kSecClass: kSecClassKey,
                           kSecAttrLabel: label,
                kSecAttrApplicationLabel: appName,
                           kSecValueData: token.data(using: .utf8, allowLossyConversion: false)!]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 키-값은 아이템 클래스의 키와 값을 적어주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두세 번째의 키-값은 아이템 클래스의 속성과 값을 적어주고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 데이터를 Data 형태로 저장했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 그림에서 본 것처럼,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키체인 서비스가 이 쿼리(속성값과 데이터)를 받아서 아이템으로 저장을 해주는 것입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장은 메서드 한 줄로 할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683381210695&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let status = SecItemAdd(saveQuery, nil)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecItemAdd 메서드가 OSStatus 타입을 리턴하기 때문에, 이를 변수에 저장해서 결과를 확인할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683381307609&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if status == errSecSuccess {
    print(&quot;저장 성공&quot;)
} else {
    print(status)
    print(&quot;저장 실패&quot;)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;180&quot; data-origin-height=&quot;56&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/1O1t3/btsd3dS0sxw/dY0nz2wzvzoph022S9MDJ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/1O1t3/btsd3dS0sxw/dY0nz2wzvzoph022S9MDJ1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/1O1t3/btsd3dS0sxw/dY0nz2wzvzoph022S9MDJ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F1O1t3%2Fbtsd3dS0sxw%2FdY0nz2wzvzoph022S9MDJ1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;100&quot; height=&quot;31&quot; data-origin-width=&quot;180&quot; data-origin-height=&quot;56&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장에 성공했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;키체인 불러오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장에 성공했으니 불러오는 코드를 살펴보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683381557156&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let searchQuery: NSDictionary = [kSecClass: kSecClassKey,
                             kSecAttrLabel: label,
                      kSecReturnAttributes: true,
                            kSecReturnData: true]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 번째 속성값은 역시 아이템 클래스의 키-값입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 속성값을 하나만 넣어보았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성값을 하나만 넣어도 결과가 돌아올까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1683381658289&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;var searchedResult: CFTypeRef?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 결과를 불러와서 저장할 변수를 먼저 만들어주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 메서드는 역시나 간단한데,&lt;/p&gt;
&lt;pre id=&quot;code_1683381689941&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let searchStatus = SecItemCopyMatching(searchQuery, &amp;amp;searchedResult)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장 메서드와 비슷하게 결과를 리턴해주고, 첫번째 인자로는 쿼리를 전달해주면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째에는 inout파라미터가 있는데, 결과를 저장할 변수를 인자로 넘겨주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683381765728&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;if searchStatus == errSecSuccess {
    guard let checkedItem = searchedResult,
          let token = checkedItem[kSecValueData] as? Data else { return }
    print(&quot;token, &quot;, String(data: token, encoding: String.Encoding.utf8))
} else {
    print(&quot;불러오기 실패, status = &quot;, searchStatus)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;성공적으로 불러왔다면 token을 확인해볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-------&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+ 추가&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;kSecClassKey를 사용할때 -25299(&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;errSecDuplicateItem&lt;/span&gt;)와 -25300(&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;errSecItemNotFound&lt;/span&gt;)에러가 반복해서 나타났습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/forums/thread/77074&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/forums/thread/77074&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위 문제와 비슷한 상황입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;kSecClassGenericPassword를 사용해도 비슷한 상황입니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장하고 싶어! 안돼 -25299&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;그럼 지울게! 안돼 -25300&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;뭔데...?&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;저장할래! 안돼 -25300???&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;아직 해결하지 못했습니다.&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-------&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;+ 추가&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;키체인 아이템 클래스마다 사용법이 조금 다른 것 같은데,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kSecClassGenericPassword을 사용하고, 속성을 간단히 최소화 해서 사용하니 오류 없이 사용할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성값을 어떻게 사용하느냐에 따라 검색, 저장 등에서 차이가 발생하는 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재는 kSecAttrAccount, kSecAttrService 속성만을 사용해서 간단히 사용하고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/security/keychain_services&quot;&gt;https://developer.apple.com/documentation/security/keychain_services&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://applecider2020.tistory.com/48&quot;&gt;https://applecider2020.tistory.com/48&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://pgnt.tistory.com/113&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://pgnt.tistory.com/113&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ios-development.tistory.com/66&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://ios-development.tistory.com/66&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>ios</category>
      <category>Keychain</category>
      <category>SWIFT</category>
      <category>token</category>
      <category>키체인</category>
      <category>토큰저장</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/95</guid>
      <comments>https://modelinspring.tistory.com/95#entry95comment</comments>
      <pubDate>Sat, 6 May 2023 23:04:16 +0900</pubDate>
    </item>
    <item>
      <title>[Swift/iOS] 애플 로그인 후 반환 값 ASAuthorizationAppleIDCredential From 공식문서</title>
      <link>https://modelinspring.tistory.com/94</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;애플 로그인(Apple Authentication Services)를 적용시킨 후 반환되는 값에 대해서 살펴보고자 합니다.&lt;br /&gt;&lt;br /&gt;유저가 첫 로그인에 성공하면, 가입 후 애플 서버로부터 받은 유저 정보를 서비스하는 서버에 전달해야 합니다.&lt;br /&gt;서비스에서도 일부 정보를 사용하고 저장해야하기 때문입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(애플 서버가입 여부는 아이폰 기준, 설정 - Apple ID - 암호 및 보안 - Apple로 로그인 - Apple ID를 사용하는 앱에서 확인 가능)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 쪽에서는 이 전달받은 데이터를 다시 검증하는 것으로 알고 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683310537228&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;authorization.credential as? ASAuthorizationAppleIDCredential&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫 로그인 시 확인할 수 있는 값 중 identityToken과 authorizationCode를 서버로 전달해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 포스팅을 찾아보았는데 userIdentifier, fullname이나 email 정도만을 언급하고 마무리 짓는 경우가 많았습니다.&lt;br /&gt;공식문서를 확인해서 ASAuthorizationAppleIDCredential 클래스에 있는 값들을 추가로 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1683316539286&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
        print(&quot;fullName, &quot;, appleIDCredential.fullName)
        print(&quot;email, &quot;, appleIDCredential.email)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플 로그인에서 로그인이 성공했을 때,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ASAuthorizationControllerDelegate의 didCompleteWithAuthorization 메서드에서 값들을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;얻을 수 있는 값들을 확인해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683316661882&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
    if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {

        print(&quot;// Identifying a User&quot;)
        print(&quot;identityToken, &quot;, appleIDCredential.identityToken)
        print(&quot;authorizationCode, &quot;, appleIDCredential.authorizationCode)
        print(&quot;state, &quot;, appleIDCredential.state)
        print(&quot;user, &quot;, appleIDCredential.user)
        print(&quot;&quot;)

        print(&quot;// Getting Contact Information&quot;)
        print(&quot;authorizedScopes, &quot;, appleIDCredential.authorizedScopes)
        print(&quot;fullName, &quot;, appleIDCredential.fullName)
        print(&quot;email, &quot;, appleIDCredential.email)
        print(&quot;&quot;)

        print(&quot;// Detecting User Characteristics&quot;)
        print(&quot;realUserStatus, &quot;, appleIDCredential.realUserStatus.rawValue)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;애플 로그인 첫 성공 시 반환 값&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플 로그인에 처음 성공한다면 반환 값은 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6K1iM/btsd1izTEsD/J0GfyJkWOeNst503ro8X30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6K1iM/btsd1izTEsD/J0GfyJkWOeNst503ro8X30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6K1iM/btsd1izTEsD/J0GfyJkWOeNst503ro8X30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6K1iM%2Fbtsd1izTEsD%2FJ0GfyJkWOeNst503ro8X30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;228&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1054&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 번째 이후로 로그인한다면 다음과 같습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;472&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dItubt/btsdZN8v8ft/pSG2ZCnSilHilo8KCAT70K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dItubt/btsdZN8v8ft/pSG2ZCnSilHilo8KCAT70K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dItubt/btsdZN8v8ft/pSG2ZCnSilHilo8KCAT70K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdItubt%2FbtsdZN8v8ft%2FpSG2ZCnSilHilo8KCAT70K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;242&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;472&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 하나하나 살펴보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;identityToken&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JSON Web Token으로서, 유저에 관한 정보를 안전하게 앱에 전달하기 위한 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게 무슨 말일까싶지만, 간단하게 설명하면 서버에서 유저 정보를 검증할 때 필요한 토큰이라고 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증할 때마다 다른 값이 반환됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;authorizationCode&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 서버와 상호작용 하는 데 사용되는 토큰.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;identityToken과 같이 서버로 보내준 후, 서버는 애플서버로 유효성 검사를 하게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;짧은 시간만(short-lived) 유효합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;state&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;credential을 생성하는 요청에 앱이 제공하는 임의의 String 값.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증 요청 시 생성되는 리퀘스트에 대한 상태정보를 포함하는 문자열이며, 인증 결과를 확인하고 상태 정보를 비교할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;(근데 nil이 계속 반환되고 있습니다)&lt;/s&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;user&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증된 유저의 고유한 identifier 값, 변경되지 않습니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;authorizedScopes&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱이 접근할 수 있는 사용자가 허락한 연락처 정보.&lt;br /&gt;&lt;s&gt;(빈값이 반환되고 있습니다.)&lt;/s&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fullName&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 이름(Apple ID에서 제공되는, 또는 Sign in with Apple UI에서 유저가 정한).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 로그인 시에만 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후에는 빈값이 반환됩니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;email&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저의 이메일, 처음 로그인할 때 실제 이메일 주소 혹은 애플이 임의로 생성한 가상 이메일 주소.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 로그인 시에만 확인할 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후에는 nil이 반환됩니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;realUserStatus&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Int값이 반환되는데,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2은 likelyReal 유저가 진짜 사람,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1은 unknown 시스템이 유저가 진짜 사람인지 아닌지 결정하지 못함,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0은 unsupported&amp;nbsp; 사용자의 실재여부를 확인할 수 없음 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;(두 번째에는 1이 반환되었습니다.)&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;이후의 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음 로그인했을 때에만 이름과 이메일이 전달되니 이것을 클라이언트 앱에 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;id토큰과 auth코드를 서버로 보낸 후 리턴되는 access토큰과 refresh토큰을 클라이언트 앱에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 부분은 서비스마다 다를 수 있습니다. 자세한 부분은 서버 개발자와 이야기 해야합니다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;user값은 ASAuthorizationAppleIDProvider의 getCredentialState 메서드의 인자로 사용할 수 있으므로, 역시 저장합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앱에서 애플 로그인 상태를 확인할 수 있는 메서드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 사용자가 Apple ID 설정에서 Apple ID 사용 중단을 한다면, 앱에서 새로 로그인할 수 있도록 처리해 주기 위함입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidcredential&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/authenticationservices/asauthorizationappleidcredential&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://labs.brandi.co.kr//2021/04/09/chosh.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://labs.brandi.co.kr//2021/04/09/chosh.html&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hyesunzzang.tistory.com/206&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://hyesunzzang.tistory.com/206&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>ASAuthorizationAppleIDCredential</category>
      <category>authorizationCode</category>
      <category>identity Token</category>
      <category>ios</category>
      <category>SWIFT</category>
      <category>애플 로그인</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/94</guid>
      <comments>https://modelinspring.tistory.com/94#entry94comment</comments>
      <pubDate>Sat, 6 May 2023 06:24:39 +0900</pubDate>
    </item>
    <item>
      <title>[Swift/iOS] CoreData 사용하기(4) - 속성값 유무에 따른 데이터 저장, 속성의 optional 타입</title>
      <link>https://modelinspring.tistory.com/93</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://modelinspring.tistory.com/92&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://modelinspring.tistory.com/92&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739290944686&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Swift/iOS] CoreData 사용하기(3) - 데이터 저장하기, 데이터 불러오기 기초&quot; data-og-description=&quot;https://modelinspring.tistory.com/91&amp;nbsp;[Swift/iOS] CoreData 사용하기(2) - CoreData Stack 설정하기 From 공식문서! 공식문서를 참고한 글입니다.https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack&amp;nbsp;https://mode&quot; data-og-host=&quot;modelinspring.tistory.com&quot; data-og-source-url=&quot;https://modelinspring.tistory.com/92&quot; data-og-url=&quot;https://modelinspring.tistory.com/92&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/YhwqZ/hyYb6uJ9Kn/VhQx3mfaIV4eyfc6NJHOW1/img.png?width=800&amp;amp;height=226&amp;amp;face=0_0_800_226,https://scrap.kakaocdn.net/dn/bkmgjw/hyYfUF9Op2/d3UWlpuUUgGuBqArKEGKFk/img.png?width=800&amp;amp;height=226&amp;amp;face=0_0_800_226,https://scrap.kakaocdn.net/dn/3QseK/hyYf1ZzZJg/RRhpHbynejwKAfTGffdYIk/img.png?width=1438&amp;amp;height=484&amp;amp;face=0_0_1438_484&quot;&gt;&lt;a href=&quot;https://modelinspring.tistory.com/92&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://modelinspring.tistory.com/92&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/YhwqZ/hyYb6uJ9Kn/VhQx3mfaIV4eyfc6NJHOW1/img.png?width=800&amp;amp;height=226&amp;amp;face=0_0_800_226,https://scrap.kakaocdn.net/dn/bkmgjw/hyYfUF9Op2/d3UWlpuUUgGuBqArKEGKFk/img.png?width=800&amp;amp;height=226&amp;amp;face=0_0_800_226,https://scrap.kakaocdn.net/dn/3QseK/hyYf1ZzZJg/RRhpHbynejwKAfTGffdYIk/img.png?width=1438&amp;amp;height=484&amp;amp;face=0_0_1438_484');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Swift/iOS] CoreData 사용하기(3) - 데이터 저장하기, 데이터 불러오기 기초&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;https://modelinspring.tistory.com/91&amp;nbsp;[Swift/iOS] CoreData 사용하기(2) - CoreData Stack 설정하기 From 공식문서! 공식문서를 참고한 글입니다.https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack&amp;nbsp;https://mode&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;modelinspring.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;위 포스트에 이은 CoreData 네 번째, 마지막 포스트입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서는 Object에 값을 채운 후 데이터를 저장했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 값이 채워지지 않은 Object를 저장하면 어떻게 될까요?&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;nil이 포함된 저장&lt;/h3&gt;
&lt;pre id=&quot;code_1683305393432&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;car.setValue(myCar[&quot;brand&quot;], forKey: &quot;brand&quot;)
// car.setValue(myCar[&quot;color&quot;], forKey: &quot;color&quot;)
car.setValue(myCar[&quot;price&quot;], forKey: &quot;price&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같은 코드로, 일부 값을 비워서 저장해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A4uCN/btsd0nIp4uV/BKgjXQIOrqrMlzndBA9by1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A4uCN/btsd0nIp4uV/BKgjXQIOrqrMlzndBA9by1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A4uCN/btsd0nIp4uV/BKgjXQIOrqrMlzndBA9by1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA4uCN%2Fbtsd0nIp4uV%2FBKgjXQIOrqrMlzndBA9by1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;56&quot; data-origin-width=&quot;762&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value를 주지 않고 저장하면, 해당 값이 nil로 들어가게 된 것을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Entity의 속성의 optional 유무&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 Entity의 속성들은 기본적으로 optional일까요?&lt;/p&gt;
&lt;pre id=&quot;code_1683305553663&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;guard let newcar = try? context.fetch(Car.fetchRequest()) else { return }
newcar.forEach {
    print($0.brand, $0.color, $0.price)
    print(type(of: $0.brand))
    print(type(of: $0.color))
    print(type(of: $0.price))
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;142&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cAO3yr/btsdZ4WqpTB/XneX4VFGiNcsISSroSV2EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cAO3yr/btsdZ4WqpTB/XneX4VFGiNcsISSroSV2EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cAO3yr/btsdZ4WqpTB/XneX4VFGiNcsISSroSV2EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcAO3yr%2FbtsdZ4WqpTB%2FXneX4VFGiNcsISSroSV2EK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;81&quot; data-origin-width=&quot;524&quot; data-origin-height=&quot;142&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;brand와 colors는 optional로, price는 Int64로 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이번에는 price 값을 주지 않고 저장해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683305658443&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        car.setValue(myCar[&quot;brand&quot;], forKey: &quot;brand&quot;)
        car.setValue(myCar[&quot;color&quot;], forKey: &quot;color&quot;)
//        car.setValue(myCar[&quot;price&quot;], forKey: &quot;price&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c1rPI9/btsdZ4hQaTO/DVXpS1KKlRgKXZ03C2Qu2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c1rPI9/btsdZ4hQaTO/DVXpS1KKlRgKXZ03C2Qu2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c1rPI9/btsdZ4hQaTO/DVXpS1KKlRgKXZ03C2Qu2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc1rPI9%2FbtsdZ4hQaTO%2FDVXpS1KKlRgKXZ03C2Qu2k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;71&quot; data-origin-width=&quot;706&quot; data-origin-height=&quot;166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;nil이 아니라 0이 들어가있는 것을 알 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무런 설정을 하지 않는다면, 현재 String 속성은 optional로, Int64는 non-optional인 것을 알 수 있습니다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;optional 설정하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실은 Data Model 파일에서 Entity의 Attribute를 클릭한 후, 인스펙터 창을 확인하면 optional 여부를 설정할 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qKmE4/btsd6uzwB0g/0Dy5ktYNasiHPGUo1kp1Ak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qKmE4/btsd6uzwB0g/0Dy5ktYNasiHPGUo1kp1Ak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qKmE4/btsd6uzwB0g/0Dy5ktYNasiHPGUo1kp1Ak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqKmE4%2Fbtsd6uzwB0g%2F0Dy5ktYNasiHPGUo1kp1Ak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;140&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스펙터 창에서는 옵셔널 여부와 Default 값도 설정해줄 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Integral64는 현재 optional로 설정되어있지만 기본 값이 0으로 설정되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본 값을 삭제하고 테스트해보겠습니다.(No Value)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;312&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lwjxw/btsd4d5TTqh/KZpHOE4vOeBCzztYbMzheK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lwjxw/btsd4d5TTqh/KZpHOE4vOeBCzztYbMzheK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lwjxw/btsd4d5TTqh/KZpHOE4vOeBCzztYbMzheK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flwjxw%2Fbtsd4d5TTqh%2FKZpHOE4vOeBCzztYbMzheK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;200&quot; height=&quot;121&quot; data-origin-width=&quot;516&quot; data-origin-height=&quot;312&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;168&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UTbTX/btsd3fJEXPX/60cY7fMsL9E4xE9ZkGXIjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UTbTX/btsd3fJEXPX/60cY7fMsL9E4xE9ZkGXIjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UTbTX/btsd3fJEXPX/60cY7fMsL9E4xE9ZkGXIjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUTbTX%2Fbtsd3fJEXPX%2F60cY7fMsL9E4xE9ZkGXIjk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;73&quot; data-origin-width=&quot;692&quot; data-origin-height=&quot;168&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;0이 기본값으로 들어가고, 여전히 Int64로 타입으로 확인됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;optional도 해제하고 저장해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;514&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z7r3M/btsdZzil93o/eLhQkMcrKraqbDuDhPuoc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z7r3M/btsdZzil93o/eLhQkMcrKraqbDuDhPuoc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z7r3M/btsdZzil93o/eLhQkMcrKraqbDuDhPuoc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz7r3M%2FbtsdZzil93o%2FeLhQkMcrKraqbDuDhPuoc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;186&quot; data-origin-width=&quot;1654&quot; data-origin-height=&quot;514&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;에러가 발생하는 것을 확인할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성값이 non-optional 일때, 값이 전달되지 않으면 오류가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 저장된 데이터들 중에 nil이 있다면, 이후에 속성 값을 non-optional로 설정하면 역시 오류가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;속성 타입을 잘 관리해야 나중에 오류를 피할 수 있습니다.&lt;/p&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>Attributes</category>
      <category>core data</category>
      <category>Entity</category>
      <category>ios</category>
      <category>optional</category>
      <category>SWIFT</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/93</guid>
      <comments>https://modelinspring.tistory.com/93#entry93comment</comments>
      <pubDate>Sat, 6 May 2023 02:13:59 +0900</pubDate>
    </item>
    <item>
      <title>[Swift/iOS] CoreData 사용하기(3) - 데이터 저장하기, 데이터 불러오기 기초</title>
      <link>https://modelinspring.tistory.com/92</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://modelinspring.tistory.com/91&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://modelinspring.tistory.com/91&lt;/a&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1739290562548&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Swift/iOS] CoreData 사용하기(2) - CoreData Stack 설정하기 From 공식문서&quot; data-og-description=&quot;! 공식문서를 참고한 글입니다.https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack&amp;nbsp;https://modelinspring.tistory.com/90&amp;nbsp;[Swift/iOS] CoreData 사용하기(1) - 프로젝트 중간에 추가하기, Entity 만들기Core&quot; data-og-host=&quot;modelinspring.tistory.com&quot; data-og-source-url=&quot;https://modelinspring.tistory.com/91&quot; data-og-url=&quot;https://modelinspring.tistory.com/91&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/sFgqw/hyYf1ZzXzp/VM6aRqGcgAPJmZOZYLhSoK/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/0qLBR/hyYcjnjFuc/MzaqAjmSDBTHfoaNL442ek/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/bfuef3/hyYccPf7fg/aZdlr2vgvAVmJmQL28TRAK/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670&quot;&gt;&lt;a href=&quot;https://modelinspring.tistory.com/91&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://modelinspring.tistory.com/91&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/sFgqw/hyYf1ZzXzp/VM6aRqGcgAPJmZOZYLhSoK/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/0qLBR/hyYcjnjFuc/MzaqAjmSDBTHfoaNL442ek/img.png?width=800&amp;amp;height=418&amp;amp;face=0_0_800_418,https://scrap.kakaocdn.net/dn/bfuef3/hyYccPf7fg/aZdlr2vgvAVmJmQL28TRAK/img.png?width=1280&amp;amp;height=670&amp;amp;face=0_0_1280_670');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Swift/iOS] CoreData 사용하기(2) - CoreData Stack 설정하기 From 공식문서&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;! 공식문서를 참고한 글입니다.https://developer.apple.com/documentation/coredata/setting_up_a_core_data_stack&amp;nbsp;https://modelinspring.tistory.com/90&amp;nbsp;[Swift/iOS] CoreData 사용하기(1) - 프로젝트 중간에 추가하기, Entity 만들기Core&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;modelinspring.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #666666; text-align: start;&quot;&gt;위 포스트에 이은 CoreData 세 번째 포스트입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Core Data 데이터 저장하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스트에서 Car Entity를 만들어놓았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 데이터를 저장해보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brdBNU/btsd5ZM4NtY/1RZoJ313k9zpRyPRody8a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brdBNU/btsd5ZM4NtY/1RZoJ313k9zpRyPRody8a0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brdBNU/btsd5ZM4NtY/1RZoJ313k9zpRyPRody8a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrdBNU%2Fbtsd5ZM4NtY%2F1RZoJ313k9zpRyPRody8a0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;113&quot; data-origin-width=&quot;1074&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1683299778885&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    let myCar: [String: Any] = [&quot;brand&quot;: &quot;KIA&quot;, &quot;color&quot;: &quot;black&quot;, &quot;price&quot;: 3000]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뷰 컨트롤러에서 다음과 같이 딕셔너리 형태로 저장할 값을 만들었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전 포스팅에서 context는 인스턴스들의 변화를 추적한다고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일종의 메서드라고 생각하면 편할 것 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장하는 메서드를 사용하기 위해서 context를 사용해야 하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context 역시 다른 인스턴스들과 함께 container에 포함되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 persistent container는 AppDelegate에 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AppDelegate의 persistent containser에서 context를 불러오기&lt;/h3&gt;
&lt;pre id=&quot;code_1683300414085&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AppDelegate 불러오기
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
// AppDelegate 안에 있는 persistentContainer에서 context 가져오기
let context = appDelegate.persistentContainer.viewContext&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;context를 가져왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장하려는 데이터의 entity도 가져와야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;entity는 데이터 모델 내에서 데이터 유형을 나타낸다고 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 저장할 데이터의 유형을 entity와 맞춰야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Entity 불러오기&lt;/h3&gt;
&lt;pre id=&quot;code_1683301324242&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;guard let entity = NSEntityDescription.entity(forEntityName: &quot;Car&quot;, in: context) else { return }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Car&quot; 이름의 entity를 불러왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Object 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object는 무엇일까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Core Data에서 Object란 모델 객체(Entity)의 인스턴스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스를 만들고 클래스의 인스턴스를 만드는 것처럼, Entity로 Object를 만든다고 하면 이해가 쉬울 것 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683301513995&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let car = NSManagedObject(entity: entity, insertInto: context)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;object를 만들었고, 이제 object에 값도 넣어야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683301541078&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;car.setValue(myCar[&quot;brand&quot;], forKey: &quot;brand&quot;)
car.setValue(myCar[&quot;color&quot;], forKey: &quot;color&quot;)
car.setValue(myCar[&quot;price&quot;], forKey: &quot;price&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장하고자 하는 Object가 준비되었습니다.&lt;br /&gt;context를 통해 저장하는 방법으로 넘어가겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Context를 통해 저장하기&lt;/h3&gt;
&lt;pre id=&quot;code_1683301581163&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try? context.save()&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한줄의 코드로 저장을 할 수 있습니다.&lt;br /&gt;전체 코드를 보면 아래와 같습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683301624029&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;override func viewDidLoad() {
    super.viewDidLoad()

    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
    let context = appDelegate.persistentContainer.viewContext

    guard let entity = NSEntityDescription.entity(forEntityName: &quot;Car&quot;, in: context) else { return }

    let car = NSManagedObject(entity: entity, insertInto: context)
    car.setValue(myCar[&quot;brand&quot;], forKey: &quot;brand&quot;)
    car.setValue(myCar[&quot;color&quot;], forKey: &quot;color&quot;)
    car.setValue(myCar[&quot;price&quot;], forKey: &quot;price&quot;)

    try? context.save()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;데이터 불러오기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Entity 전체의 값을 불러오는 것도 아래와 같은 코드 한 줄로 실행할 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683302266449&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let newcar = try? context.fetch(Car.fetchRequest())
print(type(of: newcar)) // Optional&amp;lt;Array&amp;lt;Car&amp;gt;&amp;gt;
print(newcar)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUaQFz/btsdZPyqgJR/7icykEy0g443pijek6Whyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUaQFz/btsdZPyqgJR/7icykEy0g443pijek6Whyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUaQFz/btsdZPyqgJR/7icykEy0g443pijek6Whyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUaQFz%2FbtsdZPyqgJR%2F7icykEy0g443pijek6Whyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;78&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 저장과 불러오기 성공!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 저장하는 코드를 다 주석 처리한 후,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불러오는 코드만 둔 채 다시 run 하면 불러온 data의 값들이 다 fault로 나오는 걸 볼 수 있습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WOAEj/btsd0oN7yrd/sK2Llk0EAeAKuKM0UIAKs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WOAEj/btsd0oN7yrd/sK2Llk0EAeAKuKM0UIAKs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WOAEj/btsd0oN7yrd/sK2Llk0EAeAKuKM0UIAKs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWOAEj%2Fbtsd0oN7yrd%2FsK2Llk0EAeAKuKM0UIAKs0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;202&quot; data-origin-width=&quot;1438&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;불러오기 코드를 다음과 같이 수정해보겠습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683303515068&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;let request = Car.fetchRequest()
request.returnsObjectsAsFaults = false
let newcar = try? context.fetch(request)
print(newcar)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3bksh/btsdZqMDnOn/OBqexXFLrCPRPiQxIJKOAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3bksh/btsdZqMDnOn/OBqexXFLrCPRPiQxIJKOAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3bksh/btsdZqMDnOn/OBqexXFLrCPRPiQxIJKOAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3bksh%2FbtsdZqMDnOn%2FOBqexXFLrCPRPiQxIJKOAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;190&quot; data-origin-width=&quot;1366&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fault는 메모리에 완전히 올라오지 않은 object를 나타내는 placeHolder라고 합니다.&lt;br /&gt;(faulting이라는 기술을 사용해서 메모리 사용 최소화)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;returnsObjectsAsFaults 값 이 true인 경우, NSFetchRequest를 사용해서 가져온 객체를 fault로 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값은 true인데, Core Data는 Object를 가져와서 row cache를 정보로 채우고, managed object를 faults로 반환합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 값을 사용할 때에는 메모리에 올라오기 때문에 fault로 보이지 않습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1683304652640&quot; class=&quot;swift&quot; data-ke-language=&quot;swift&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;guard let newcar = try? context.fetch(Car.fetchRequest()) else { return }
newcar.forEach {
    print($0.brand, $0.color, $0.price)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqbEPc/btsd4F89STX/DkRaV2D5KkM7hKPn02R9O1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqbEPc/btsd4F89STX/DkRaV2D5KkM7hKPn02R9O1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqbEPc/btsd4F89STX/DkRaV2D5KkM7hKPn02R9O1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqbEPc%2Fbtsd4F89STX%2FDkRaV2D5KkM7hKPn02R9O1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;74&quot; data-origin-width=&quot;758&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://zeddios.tistory.com/987&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://zeddios.tistory.com/987&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@ssionii/Core-Data%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%84%EB%9D%BC%EB%B3%B4%EC%9E%90-3-Fetch-Data&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@ssionii/Core-Data%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%84%EB%9D%BC%EB%B3%B4%EC%9E%90-3-Fetch-Data&lt;/a&gt;&lt;br /&gt;&lt;a href=&quot;https://developer.apple.com/documentation/coredata/nsfetchrequest/1506756-returnsobjectsasfaults&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://developer.apple.com/documentation/coredata/nsfetchrequest/1506756-returnsobjectsasfaults&lt;/a&gt;&lt;/p&gt;</description>
      <category>programming/Swift(iOS)</category>
      <category>context</category>
      <category>CoreData</category>
      <category>Entity</category>
      <category>fetch</category>
      <category>ios</category>
      <category>object</category>
      <category>save</category>
      <category>SWIFT</category>
      <category>불러오기</category>
      <category>저장하기</category>
      <author>마들브라더</author>
      <guid isPermaLink="true">https://modelinspring.tistory.com/92</guid>
      <comments>https://modelinspring.tistory.com/92#entry92comment</comments>
      <pubDate>Sat, 6 May 2023 01:00:37 +0900</pubDate>
    </item>
  </channel>
</rss>