← 홈으로 돌아가기

📝 Daily Learning Log (2026-06-01 ~ 06-07)

주간 학습일지 · 2026-06-01 ~ 06-07

2026-06-03 (화)

📚 학습한 문서

💡 중요한 포인트

POP 핵심 개념:
  • Protocol Extension을 통한 기본 구현
  • Associated Type과 Generic의 차이
  • some/any 타입의 활용

📕 오답노트

async/await가 코드 누락을 막아주는 이유

  • async/await로 동시성을 관리하면 completion 핸들러처럼 결과를 직접 호출해 넘길 필요가 없다 → completion 호출을 깜빡하는 "처리 누락"이 사라진다.
  • 모든 분기에서 return을 강제한다. 빠진 경로가 있으면 컴파일 자체가 안 된다.
  • 에러는 throw로 자동 전파되고, 호출부는 try를 반드시 붙여야 한다 → 에러 처리 누락도 컴파일 단계에서 차단된다.

🔗 복습 정리 (Q&A)

2026-06-04 (수)

📚 학습한 문서

💡 중요한 포인트

Swift Concurrency 핵심:
  • MainActor를 통한 UI 스레드 안전성
  • actor isolation의 의미와 경계
  • Thread Sanitizer로 data race 감지

📕 오답노트

any가 작은 값에선 inline boxing이라 박싱 비용 ≈ 0이지만, dispatch 비용은 항상 있다

  • 처음엔 "any는 항상 힙 박싱이라 느리다"고 단순화했지만 틀린 일반화였다.
  • Existential container의 value buffer는 보통 3-word(24 byte). 그보다 작은 값(Int, Double 등)은 inline으로 buffer에 직접 담겨 힙 할당이 일어나지 않는다.
  • 그러나 메서드 호출 시 PWT(Protocol Witness Table)를 거치는 간접 호출 비용은 값 크기와 무관하게 항상 발생한다.
  • 그래서 진짜 zero-cost가 되려면 any가 아니라 <T: P> generic + specialization 조합이어야 한다.

Swift는 모듈 단위로 컴파일된다

  • 모듈(Module) = 함께 컴파일되는 코드 단위. SPM 패키지, Framework, Xcode target, main app target이 각각 하나의 모듈.
  • 모듈을 빌드하면 두 결과물이 나온다 — .swiftmodule(시그니처만), .dylib/.o(본문 기계어).
  • 다른 모듈에서 import할 때 컴파일러는 .swiftmodule시그니처만 본다. 본문은 이미 컴파일되어 바이너리에 굳어 있다.
  • 그래서 모듈 경계를 넘는 generic 호출은 specialization이 막힌다. 본문 소스가 없으니 T별 전용 함수를 새로 만들 수 없기 때문.
  • 해결: @inlinable로 본문 소스를 .swiftmodule에 노출하거나, @_specialize로 특정 타입용 specialized 버전을 미리 빌드해둔다.
  • 표준 라이브러리의 Array, Dictionary 등은 거의 모든 메서드에 @inlinable이 붙어 있어 zero-cost가 유지된다.

🔗 복습 정리 (Q&A)

2026-06-05 (목)

📚 읽은 자료 (진행중)

💡 중요한 포인트

Tuist 적용기에서 배운 점:
  • Xcode 프로젝트 파일 충돌 문제를 Tuist로 해결
  • 모듈화된 아키텍처 구성 시 의존성 관리의 중요성
  • 빌드 속도 개선 효과
Static Library Linking 동작 방식 (핵심 개념):
  • 같은 앱 타겟 내에서 여러 framework가 같은 static library를 의존하는 경우:
    • 각 framework는 컴파일 시점에 static library의 심볼을 참조만
    • 최종 링킹 단계에서 링커가 앱 바이너리에 static library 코드를 한 번만 포함
    • 링커가 중복을 자동으로 제거하므로 코드 중복 없음
  • 여러 타겟(App + Extension + Widget)이 같은 static library를 의존하는 경우:
    • 각 타겟은 독립적인 실행 파일/번들
    • 각 타겟의 바이너리에 static library가 각각 포함됨 (중복 발생)
    • 전체 IPA 크기 증가 → 이런 경우 Dynamic Framework 사용 권장

참고: Swift Forums - Static Library Duplication, Emerge Tools - Dynamic Frameworks, Advanced Static vs Dynamic

Swift Concurrency의 핵심 장점 (LINE 블로그):
  • 개발자 실수 방지: 컴파일러가 data race를 미리 감지하여 런타임 크래시를 사전에 차단
  • 가독성 향상: async/await로 비동기 코드가 동기 코드처럼 읽힘 (completion handler 콜백 지옥 해결)
  • GCD 대비: DispatchQueue.sync, NSLock, Semaphore 등 수동 동기화 불필요 → Actor가 자동 처리
  • 기존 방식은 개발자가 직접 "어디에 락을 걸지" 결정해야 했지만, Swift Concurrency는 언어 레벨에서 보장

참고: About Swift Concurrency (LINE Engineering)

📕 오답노트

Computed property로 동기화하면 += 연산이 안전하지 않다

  • 처음엔 "computed property의 get/set에 queue.sync를 넣으면 thread-safe하다"고 생각했지만 틀렸다.
  • finishedImages += 1은 실제로 get → 계산 → set 3단계로 나뉨
  • computed property는 get과 set을 각각 따로 보호하므로, 두 호출 사이에 다른 스레드가 끼어들 수 있음
  • 올바른 방법: queue.sync { _finishedImages += 1 }전체를 하나의 원자적 연산으로 묶어야 함
  • 또는 func incrementFinishedImages() 같은 전용 메서드를 만들어 내부에서 동기화

Thread Explosion은 블로킹이지 데드락이 아니다

  • 블로킹: 느리지만 시간이 지나면 결국 진행됨
  • 데드락: 순환 대기, 영원히 진행 불가

🔄 다음에 할 일

  • LINE Engineering Blog - Swift Concurrency 글 완독하기
  • 학습한 개념 정리하기 (Static Library Linking, Swift Concurrency)

2026-06-06 (토)

📚 읽은 자료 (진행중)

✅ 학습 완료

💡 Deadlock과 큐 vs 스레드 개념 정리

핵심 개념:
  • Deadlock vs Self-Deadlock vs Blocking 구분
  • 큐 = 태스크 대기열, 스레드 = 실제 일꾼 (Serial 큐 ≠ 전용 스레드)
  • enqueue는 항상 가능, 직렬화는 실행만
  • async ≠ 논블로킹 (어떤 함수를 부르냐가 결정)
  • Thread explosion = Blocking (Deadlock 아님)

📄 상세 Q&A 문서 보기 (10개 질문)

📕 오답노트

데드락 vs self-deadlock — 등장 스레드 수가 다르다

  • 고전 데드락: 스레드 2개 이상이 서로의 자원을 쥔 채 순환 대기
  • Self-deadlock: 스레드 1개가 자기 자신을 기다림 (main.sync on main)

Task.yield()는 동기→비동기 변환이 아니다

  • yield는 스레드 양보일 뿐, 동기 코드는 여전히 동기로 실행됨.
  • 진짜 비동기 변환은 Task.detached로 백그라운드 스레드에서 실행.
  • await Task.yield()는 "잠깐 다른 Task 먼저 해"라는 의미.

뒤로가기 눌러도 for문이 자동으로 안 멈춘다

  • Cooperative Cancellation: Task 취소되어도 명시적 체크 전까지는 계속 실행됨.
  • try Task.checkCancellation() 또는 if Task.isCancelled로 확인 필수.
  • URLSession 같은 일부 API는 내부적으로 취소 체크하지만, 동기 코드는 안 함.

defer는 Task 취소 시 즉시 호출 안 됨

  • Task 취소 = 플래그만 설정, 함수는 계속 실행 중.
  • defer는 함수가 return 또는 throw할 때만 호출됨.
  • await 지점에서 throw → 함수 종료 → defer 호출.

Task.detached는 Parent Task와 독립적

  • try Task.checkCancellation()은 Parent Task 취소를 체크 (detached와 무관).
  • detached 안에서 체크하면 detached 자신의 취소 상태 확인.
  • 부모 취소되어도 detached는 계속 실행 → defer { detached.cancel() } 필수.

캔슬 체크 없으면 무작정 기다린다

  • try await task.value만 하면 Parent 취소되어도 task 끝날 때까지 기다림.
  • 루프 안에서 try Task.checkCancellation()로 주기적으로 체크 필요.
  • 또는 withThrowingTaskGroup 사용하면 자동 취소됨 (Structured).

함수는 Task가 아니다

  • 함수 안에 Task 만들어도 함수가 Parent Task가 아님
  • Parent Task = "이 함수를 실행하는 Task"
  • Task { }는 함수 안에 있어도 독립적으로 실행됨

SwiftUI .task { }는 특별하다

  • 일반 Task { }는 생명주기 독립 (VC 닫혀도 계속 실행)
  • SwiftUI .task { }는 View와 생명주기 연동 (View 사라지면 자동 취소)
  • SwiftUI가 내부적으로 onDisappear에서 .cancel() 호출

Task vs Task.detached는 생명주기 관리가 동일하다

  • 차이점: Actor context 상속, Priority 상속만 다름
  • 공통점: 둘 다 Unstructured, 부모 취소 전파 안 됨, defer 필요
  • 95%는 Task { } 사용 (context 유지), 5%만 Task.detached { } 사용

🔄 다음에 할 일

  • Protocol Oriented Programming 문서 읽기 (아직 안 읽음) — POP 문서
  • 카카오페이 블로그 "문제 해결을 위해 우리가 한 일" 섹션 완독
  • LINE Engineering Blog - Swift Concurrency 글 완독하기 (이월)