iOS개발자 앨런입니다.
https://www.youtube.com/@allen_ios
제가 공부하면서 겪었던 시행착오를 쉽게 풀어내어
지식을 공유할 수있는 개발자가 되고싶습니다.
쉬운 주제로 빠른 시간 안에 겉핥기 식으로 쉽게만 가르치는 강의를 만드는 것에는 관심이 없습니다.
그런 강의는 얼마든지 빠르게 찍어내듯 만들 수 있겠지만, 결국 "좋은 개발자로 성장"하는 것은 그만큼 이론적인 기반의 밑거름이 탄탄해야 한다고 믿고 있기 때문입니다.
쉬운 강의보다는, 좋은 개발자(끝임없이 성장할 수 있는 개발자)가 되기 위해
반드시 알아야 하는 어려운 내용까지를 최대한 쉽게, 그리고 직관적으로 알려드리는 것.
그래서 제가 아닌 여러분 스스로 고민/생각할 수 있는 밑거름을 만들어 드리는 것을 저의 강의 목표로 삼고 있습니다.
저 스스로도 내일은 더 좋은 개발자가 되자는 모토를 가지고 있는 만큼
제가 고민 했던 내용들을 깊이있게 전달 드리고 싶습니다.
👇🏻문의는 아래의 이메일로 주시면 됩니다.
we.love.code.allen@gmail.com
언어: Swift(스위프트), Python, Java, C#
강의
수강평
- 앨런 Swift Concurrency for Swift 6 (Part-1)
- 앨런 Swift Concurrency for Swift 6 (Part-1)
- 앨런 Swift Concurrency for Swift 6 (Part-2)
- 앨런 Swift Concurrency for Swift 6 (Part-1)
- 앨런 Swift Concurrency for Swift 6 (Part-1)
게시글
질문&답변
Task 클로저 내 `non-Sendable` 값 타입 접근 시, 캡처 리스트가 정의된 Task 순서에 따른 컴파일러 에러 차이
안녕하세요! sujinnaljin 님!질문주신 내용이현재 강의의 48강 ~ 52강 내용하고 관련된 내용인데, 이번에 Xcode가 업데이트가 되면서 내부 메커니즘이 이번에 제대로 구현이 되었나 보네요.(Region Based Isolation 관련 내용이예요, https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md )뒤에 강의에서 내용을 자세히 다루고 있으니.. 강의 내용을 보시면 되고, 그 전에 최대한 직관적이고 간단하게만 말씀드려보면, ValueCounter타입은 값타입이긴 하지만, Task의 클로저 자체는 변수 주소를 캡처해서 사용하기 때문에 valueCounter 변수 주소가 한번 전달(sending)이 되어 버리면, 그 이후의 코드에서는 사용할 수가 없습니다. (그게 Swift Concurrency에서 새롭게 도입한 Shared Mutable State를 다루기 위한 내부 메커니즘이라고 보시면 됩니다.)(즉, 위의 Task를 "1번 Task"라고 이름 붙이기로 하고, 아래 Task를 "2번 Task"라고 이름 붙이기로 하면.. 이미 1번 Task에서 valueCounter를 차지하고 써버리기로 된 것이기 때문에 2번 Task에서는 더이상 valueCounter의 변수에 조차 접근할 수 없어진 것입니다.)func test() { var valueCounter = ValueCounter() Task {// valueCounter의 주소가 Task 내부로 완전 전달 (더이상 아래 코드에서 사용 불가) print(valueCounter.increment()) print(valueCounter.value) } Task { [valueCounter] in // valueCounter 주소에 접근 불가 var newValueCounter = valueCounter print(newValueCounter.increment()) print(newValueCounter.value) } }다만, Task와 캡처리스트 순서가 바뀌면 가능한 것입니다. 왜냐면 이번에는 캡처리스트를 먼저 사용했기 때문에, valueCounter변수의 주소가 Task 내부로 완전 전달(sending)이 된 것이 아니기 때문에.. 그 이후의 코드인 Task에서 valueCounter 변수에 접근이 가능한 것입니다.func test() { var valueCounter = ValueCounter() Task { [valueCounter] in // valueCounter 주소가 완전 전달이 된 것이 아님 var newValueCounter = valueCounter print(newValueCounter.increment()) print(newValueCounter.value) } Task {// valueCounter 주소에 접근 가능 print(valueCounter.increment()) print(valueCounter.value) } } 네, 그래서 위와 같은 내용을 확인하시면 될 것 같고..강의 후반부 48강 ~ 52강 내용과 sending키워드 내용까지를 잘 보시면 질문 주셨던 내용에 대해 명확하게 이해하실 수 있으실 것 같습니다.(제가 강의 촬영 당시에는.. 컴파일러 내부에 sending키워드 관련 부분이 아마 정확하게 구현이 안되어 있어 크래시가 나지 않았는데, 정확하게 말씀드리면 해당 12강 강의 내용 중에 캡처리스트 부분에 대해 설명드리는 내용은 정확하게 컴파일러 업데이트가 되기 전 내용이라, 제대로 동작하고 있지 않은 것으로 보시면 될 것 같습니다. 즉, 지금 설명드린 내용이 Evolution문서와 정확하게 일치하게 컴파일러가 업데이트 된 내용이라고 보시면 됩니다.) 말씀드린 강의 후반부 내용 확인해보시고도 이해가 안되시는 부분이 있으시면 다시 질문주세요 ! 감사합니다. :)
- 0
- 1
- 34
질문&답변
18강 NSCache 예시 질문
네, sujinnaljin 님첫번째 질문)아, 네네 단순 예시라서 제가 꼼꼼하게 작성하지는 않았었네요! 말씀하신대로 정확하게 작성하신다면.. 아래와 같은 형태로 작성하시는 것이 당연히 맞습니다. (삭제도 딕셔너리에 접근하는 것이니까요.)func clearAll() { lock.lock() cache.removeAll() lock.unlock() } 두번째 질문)아 네, NSCache 자체는 내부적으로 일단 Thread-safe하기 때문에 @unchecked Sendable 만 사용하셔도 됩니다. (아래와 같은 처리를 안해도 된다는 것이겠죠.)func setValue(_ value: Value, for key: Key) { semaphore.wait() cache[key] = value semaphore.signal() }func setValue(_ value: Value, for key: Key) { lock.lock() cache[key] = value lock.unlock() } 그리고.. 일단 공식문서에 Thread-safe하다고 나와있으니, (저라면) 아래처럼 그냥 직접적으로 사용할 것 같고..func setValue(_ value: Value, for key: Key) { cache[key] = value }만약에 그 전에 (1) NSCache를 사용해본 적이 없거나, (2) 공식문서를 미리 확인하지 않았다는 가정을 해보면, 당연히 @unchecked Sendable + 안전하게 위(semaphore or lock)와 같은 처리를 사용할 것 같습니다. (물론 예를 들어, 위와 같은 메서드 로직에서 NSCache만 사용하는 것이 아니라, 다른 딕셔너리 등도 속성으로 같이 처리해주는 등이 있다면, 아무리 NSCache를 사용한다더라도, semaphore 나 lock 을 사용해야겠지만요.)답변이 되셨으면 좋겠습니다. :) 감사합니다...!
- 0
- 2
- 28
질문&답변
Task 의 default 우선순위 문의 (utility vs medium)
네, sujinnaljin 님 강의 내용에서도 말씀드리고 있지만.. 크게 중요한 내용은 아닌 것 같아서.. ^^; 제가 굳이 다시 찾아보지는 않았었네요. Task 의 default 우선순위가 medium 인 것이 맞습니다. (제가 알기로는 Swift버전이 업데이트 되면서 중간에 바뀐 것 같네요. 초반에 자료를 만들기 시작할때는 utility 였던 것으로 기억하고 있어서요.) 불편을 드렸다면 죄송합니다. 강의는.. 전반적인 내용 큰틀을 이해하시는 데는 도움이 되실 수 있으나, 아주 디테일한 내용에서는 직접 공식문서 등을 통해서 찾아보신 내용이 당연히 우선시 되리라고 생각됩니다. 감사합니다 :)
- 0
- 2
- 21
질문&답변
18강 자식 작업의 메타데이터 상속 관련 강의 자료 문의
아 네, 큰 의미가 있는 것은 아니고그냥 단순하게 두가지 의미 때문에 취소선으로 표시해 놓은 것이기는 한데,"메타데이터"라는 것에는 (1) 첫번째로는 현재 실행 중인 액터가 있을 수도 있고 아닐 수도 있어서 취소선으로 표시해놓은 것 입니다. 즉, 구조적 동시성이 액터 내부에서 실행하는 경우 - 액터 상속하게 되고, 액터 내부가 아닌 곳에서 실행하는 경우 - 상속할 액터가 없어서... 상속할 액터가 있는 경우/없는 경우 때문에 그렇습니다.(2) (다른 측면에서는) 구조적 동시성 자체가 병렬처리를 목적으로 하는 것이라.. 부모 작업의 경우는 액터에서 실행될 수 있지만, 하위(자식) 작업들은 병렬로 처리되어 (액터와 상관없이) 실행되기 때문에 그런 의미에서 살짝(?) 표시를 해둔 것이긴 합니다. 감사합니다. :)
- 0
- 1
- 20
질문&답변
withCheckedContinuation 에서 resume 호출의 안정성 보장 질문
아 네네, sujinnaljin 님.강의 내용이 잘못된 것 같네요! 공식 문서 내용을 저도 다시 확인해보니, 해당 부분에 대한 내용을 제가 자세하게 체크하지 못한 부분이 있네요! 말씀하신 내용이 맞습니다 ^^;(아마 저도 초반 학습 할 때의 내용에 대해 추후에 자세하게 살펴보진 않아서 더블 체크를 못했던 부분이네요.) 강의 내용, 교재 내용도 수정해 놓아야 겠네요!알려주셔서 감사합니다 :)
- 0
- 1
- 29
질문&답변
참고 코드 자료 7-StructuredConcurrency(102, 103줄) 오타?
아, 네 맞네요ㅠㅠ오타가 있었네요! 네 A코드의 형태는 잘못되었고, (아래와 같은) 사용하신 B의 형태가 정확히 맞습니다.(자료도 수정해 놓겠습니다.) /// 구조적 동시성 작업의 생성 (하위 작업의 생성) async let image1 = fetchImage(num: 1) async let image2 = fetchImage(num: 2)질문에 정확한 답변을 드리고 싶지만..제 생각에는 잘못된 코드를 분석한다는게 크게 의미가 없을 것 같습니다..ㅠㅠ 어차피 A코드와 같은 형태로 사용하는 것이 아니라, 잘못된 코드일뿐인 것이죠. (잘못된 코드인데, 자체적으로 컴파일러가 오류를 잡아주지 않는 것일뿐이라고 보시면 될 것 같습니다.) 그냥 단순히, 진짜 제 개인적인 뇌피셜로는 내부적으로 B코드와 완전히 동일하게 동작할 것 같긴합니다. /// 구조적 동시성 작업의 생성 (하위 작업의 생성) async let image1 = try await fetchImage(num: 1) async let image2 = try await fetchImage(num: 2)왜냐면, 위와 같은 코드에서 컴파일 될때, 왼쪽의 코드인.. async let의 코드를 먼저 컴파일 시킬 것이기 때문에, 실제 async let image1 코드에서 일단 비동기 함수를 호출 시키고 다음 줄의 코드로 넘어가는 동작으로 async let image2를 컴파일 시킬 것 같습니다.(다만, 측정하신 시간이 다르게 나온 이유는 정말 단순하게 플레이그라운드 실행 환경의 문제이지 않을까 싶습니다. 수백번의 실험을 해보면 얼추 비슷하게 나올 것 같습니다.) 코드 내용을 주의 깊게 봐주셔서 감사합니다. :)
- 0
- 3
- 35
질문&답변
기초 앱 4강 Type Any -> UIButton
네 기연 님.디테일하게 적어주셔서 감사합니다 😁 Any타입은 추상적인 타입이기 때문에, 만약 구체적인 타입인 UIButton으로 직접 지정해서 사용하지 않는 경우는 타입캐스팅을 해서 UIButton들의 속성에 접근할 수 있고,직접 UIButton을 설정하는 경우, 굳이 타입캐스팅을 하지 않아도 되니.. 조금 더 편하게 사용하실 수 있습니다. :)
- 0
- 2
- 34
질문&답변
3강 스레드 제어권 관리 질문드립니다
네 안녕하세요 미뇽 님!1. 네 맞습니다. 재개(resume)를 하게 되면, 양보했던 쓰레드 제어권을 다시 돌려받아 일처리를 하게 됩니다. (쓰레드 제어권이란 것은, "지금 실행되는 함수"가 가지고 있을 수 밖에 없다고 보시면 됩니다. 왜냐면.. 쓰레드 제어권은 쉽게 말하자면, "CPU를 내가 차지하고 사용할께"의 그 내부 컨트롤을 관리하는 개념이기 때문입니다. 그러니까, 여기서 func2는 다시 쓰레드 제어권을 돌려받아서, 함수를 재개시킨 것이다라는 의미로 설명드리고 있는 것입니다.)2. 네 맞습니다. func1에서 func2를 호출한 상황이라고 가정하고 설명드렸고, (func2가 CPU를 차지하고 사용하다가) func2가 일처리가 다 끝나서 리턴하게 되면 ("func1아, 이제 CPU 니가 차지하고 사용해도 돼"..이런 것처럼) 쓰레드 제어권을 이제 func1에 넘기게 됩니다.3. 네 맞습니다. func1 (Caller)에서 func2 (Callee)를 호출하게 되면, func1이 가지고 있던 쓰레드 제어권을 func2에게 넘겼다가 실행이 다 끝나면 func1이 돌려받게 되는데, 이 개념은 GCD나 Swift Concurrency나 동일합니다. 다만, 차이는.. GCD에서는 (func2입장에서 보면) func2는 쓰레드 제어권을 운영체제에게 양보할 수 있는 개념이 없는데, (그래서 func2는 한번 일을 시작하면 무조건 끝날때까지 동작할 수 밖에 없는데)Swift Concurrency는 func2가 운영체제에게 잠깐 쓰레드 제어권을 양보해서, (중간에) 운영체제가 다른 일처리를 해도 될 수 있게 되는 개념입니다. (운영체제는 그 양보 받은 쓰레드 제어권을 또 다른 어떤 함수에게 잠깐 빌려줘서 일을 시키겠죠.)4강에서 메모리 구조적으로 어떻게 함수가 잠깐 멈췄다가 실행될 수 있는지의 내용을 참고하셔서 생각해 보시면 많은 도움이 되실꺼예요! 이해가 안되시는 부분이 있으시면 추가적으로 질문주세요 :) 감사합니다. :)
- 0
- 1
- 44
질문&답변
10번 강의 관련하여 질문드립니다.
네 안녕하세요 개발자 님. 네 맞습니다. 엄밀하게 따져보면, 개발자 님이 말씀하신게 맞습니다. 제 교재 29 페이지에도 보시면, 함수가 재개가 될때는 다른 쓰레드에서 재개가 될 수 있다고 그림까지 그려서 설명드리고 있기도 하고요. 다만, 여기서 그렇게 설명드리면, 이해할 수 없을 정도로 엄청 복잡해 지지 않을까요? 그래서 해당 설명 드리고 있는 부분에서는 Task 자체가 특정한 쓰레드에서만 실행된다고 가정을 해야, 이론적으로 이해하는 측면에서 훨씬 "직관적으로" 이해가 쉬워지기 때문에, 그렇게 가정하고 말씀드리고 있는 부분이니.. 강의 내용에서 그런 부분을 참고 부탁드립니다. (실제로는 다른 쓰레드에서 재개(resume)되어 실행될 수 있다는 부분을 당연히 아신다는 (앞 부분의 강의 내용을 다 이해하시고 있다는) 가정 하에 최대한 직관적인 설명을 위한 가정일 뿐입니다.) 감사합니다. :)
- 0
- 2
- 39
질문&답변
ImageProject 관련 문의
네 안녕하세요 ㅎㅇ 님.결국, 해당 코드에서는.. 다른 객체(imageDownloader)가 가지고 있는 캐시에 URL을 가지고 있는 여부의 확인을 하려고 하는 것인데, Swift Concurrency에서는 내부적으로 await 코드의 시점에 재진입 문제가 발생할 수 있다는 것이 문제죠.결국 "요청 시작" 시점부터 ===> "포함 여부 확인 완료" 시점까지 중간에 await 코드가 포함되어 있는지의 여부가 중요합니다.1) 번의 경우let keys = await imageDownloader.cache.keys if keys.contains(url) { return try await imageDownloader.image(from: url) }요청 시점 ==== await (O) ====> 캐시 정보(keys) 가져옴 ==== await (X) ====> 포함 여부 요청 시점에서 캐시 정보를 가져 올때까지는 await 코드가 있긴 하지만, 실제 캐시 정보를 확인한 시점 이후에는 그 캐시 정보가 바뀌지 않는다는 것을 가정하는 코드라고 보시면 되고요. (따라서, 일단 캐시 정보의 스냅샷을 확인한 시점이후 캐시 데이터는 변할 틈이 없다고 보면 됨) 2) 번의 경우if await imageDownloader.cache.keys.contains(url) { return try await imageDownloader.image(from: url) }요청 시점 ==== await (O) ====> contains호출 시점 ==== await (O) ====> 포함 여부 2번의 경우는.. 어떻게 보면 요청 시점부터 확인 시점까지 전 구간에 걸쳐서, await코드가 있는 것이기 때문에.. 해당 객체(imageDownloader)에 여러 요청(메서드 호출 등)이 있다면, 실제 contains메서드를 호출한 시점부터도 재진입 문제가 발생할 수 있다는 것이 문제라고 보시면 됩니다. (쉽게 말씀드리면.. (실제 코드가 그렇다는게 아니라) 논리적으로 아래 코드와 비슷하다는 뜻입니다.)let keys = await imageDownloader.cache.keys let isCashed = await keys.contains(url) if isCashed { return try await imageDownloader.image(from: url) }그렇기 때문에, 재진입 문제와 관련해서는 1번 코드가 더 안정적인 코드라고 말씀드리고 있는 겁니다. 네네, lazy var로 선언하시더라도 (글로벌 액터로 격리된) DiskStorage 클래스의 경우, DiskStorage타입의 존재 자체가 ImageDatabase에 의존하고 있기 때문에 발생하는 문제라고 보시면 됩니다.ImageDatabase가 먼저 초기화 되어야 ==> DiskStorage 존재 가능 ==> 존재하려면 ImageDatabase 필요 이런식으로 순환해서 의존(필요)하고 있기 때문이라고 보시면 될 것 같습니다. lazy로 선언했다고 해서 어차피 컴파일러 자체가 논리적인 모든 순서를 파악할 수 있는 것은 아니기 때문에, 컴파일러는 DiskStorage 타입 자체의 초기화 문제로 바라보고 있는 것입니다. 감사합니다. :)
- 0
- 1
- 38






