iOS개발자 앨런입니다.
https://www.youtube.com/@allen_ios
"정확하게 아는 지식은, 쉽게 자신의 말로 설명할 수 있어야 한다."가 제 모토입니다.
그렇기에 제가 공부하면서 겪었던 시행착오를 쉽게 풀어내어
누구든 이해할 수 있는 언어로 지식을 공유할 수있는 개발자가 되고싶습니다.
저는 쉬운 주제로 빠른 시간 안에 겉핥기 식으로 쉽게만 가르치는 강의를 만드는 것에는 관심이 없습니다.
그런 강의는 얼마든지 빠르게 찍어내듯 만들 수 있겠지만, 결국 "좋은 개발자로 성장"하는 것은 그만큼 이론적인 기반의 밑거름이 탄탄해야 한다고 믿고 있기 때문입니다.
쉬운 강의보다는, 좋은 개발자(끊임없이 성장할 수 있는 개발자)가 되기 위해
반드시 알아야 하는 어려운 내용까지를 최대한 쉽게, 그리고 직관적으로 알려드리는 것.
그래서 제가 아닌 여러분 스스로 WHY(왜)를 고민/생각할 수 있는 밑거름을 만들어 드리는 것을 저의 강의 목표로 삼고 있습니다.
저 스스로도 내일은 더 좋은 개발자가 되자고 노력하는 만큼
제가 고민 했던 내용들을 깊이있게 전달 드리고 싶습니다.
면접에서 자꾸 떨어지거나, 쉬운 내용도 자신의 언어로 잘 표현하지 못하겠다는 것은 정확하게 알고 있지 않다는 의미라고도 생각합니다. 오히려 기본적인 내용, CS에 대해 "탄탄한 기본기"가 있어야, 빠르게 성장 가능하다고 믿습니다.
👇🏻문의는 아래의 이메일로 주시면 됩니다.
we.love.code.allen@gmail.com
언어: Swift(스위프트), Python, Java, C#
Courses
Reviews
- Alan Swift Grammar Master School (Online BootCamp - 2 Month Course)
- Alan iOS Concurrency - Understanding Dispatch Queues and Operation Queues
- Alan Swift Concurrency for Swift 6 (Part-1)
- Alan Swift Concurrency for Swift 6 (Part-1)
- Alan Swift Concurrency for Swift 6 (Part-2)
Posts
Q&A
Actor에서 Task vs Task.detached 사용 시 재진입 문제 질문
네 안녕하세요! 질문 내용에 대해, 2가지 포인트를 말씀드릴 수 있을 것 같은데요.(1) 여기서 이렇게 Task.detached로 구현되나 그냥 Task로 구현되나, 전혀 상관없이 downloadImage 함수 자체는 액터가 아닌 외부 쓰레드에서 실행되게 됩니다.let task = Task.detached { let image = try await downloadImage(from: url) return image }let task = Task { let image = try await downloadImage(from: url) return image }조금 의아하실 수도 있지만, 왜 그런지 설명드리면.. Task로 구현하시면 actor의 context를 상속받는 것은 맞지만, 실제 downloadImage(from: url) 함수 자체가 액터 내부의 메서드가 아닌 외부의 메서드이고, 해당 함수를 호출하면서, 앞에 await 이라는 키워드가 붙어있다는 것이, 결국엔 나의 쓰레드가 아닌 외부의 쓰레드를 사용한다는 의미라고 보시면 됩니다.(쉽게 한마디로 정리해서 말씀드리면, 비동기 메서드(함수)로 구현되어 있으면, (액터에서 호출하더라도) 액터가 아닌 외부 쓰레드에서 동작합니다.)그래서, 사실 Task 나 Task.detached나 구현하고, 실제 동작하는 것에는 전혀 차이가 없지만.. 여기서 Task.detached를 사용했다는 것은 "외부 쓰레드에서 실행된다는 의미를 조금 더 명확하게 해주기 위함"이라고 보시면 될 것 같습니다.(네 그래서, 위의 내용을 정확하게 이해하시는 것이 먼저 중요한 것 같고요.) (2) 만약에, 위의 (1)번 내용이 아니라고 가정하고, Task를 사용하는 것이 serial executor에서 실행이 된다고 가정을 해서 말씀을 드려도, 재진입 문제가 발생하지 않습니다.let task = Task { let image = try await downloadImage(from: url) return image } /// 일단 (완료되지 않은) 작업 상태를 보관 cache[url] = .loading(task)왜냐하면, 바로 아래에 있는 코드인, cache[url] = .loading(task) 때문에 그렇습니다. 쉽게 말씀을 드리면 위의 코드는 바로 아래의 코드와 동일합니다. (Task 안의 비동기적인 작업을 제외하고 생각하시면 쉬워요.)let task = Task{ } cache[url] = .loading(task)작업을 생성하자마자, 동기적으로 일단 cache에 상태를 .loading라고 저장해 놓습니다. 작업 자체를 생성하자마자 저장해놓는 (동기적인) 방식이기 때문에 (물론 작업 안에서는 비동기적인 일이 일어나지만..) 일단 캐시 자체를 바꿔놓았기 때문에 await을 통해 외부에서 접근하더라도 재진입 문제가 발생하지 않습니다.(그래서, "작업(Task)"자체를 생성하고 저장하는(중간에 await키워드가 전혀 들어가지 않는) 동기적인 방식으로 재진입 문제를 해결하는 것이라고 보시면 됩니다.) 위의 내용들을 잘 이해해 보시면 좋을 것 같고, 추가적으로 질문이 생기시면 또 질문주세요!감사합니다. :)
- 0
- 2
- 24
Q&A
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
- 57
Q&A
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
- 46
Q&A
Task 의 default 우선순위 문의 (utility vs medium)
네, sujinnaljin 님 강의 내용에서도 말씀드리고 있지만.. 크게 중요한 내용은 아닌 것 같아서.. ^^; 제가 굳이 다시 찾아보지는 않았었네요. Task 의 default 우선순위가 medium 인 것이 맞습니다. (제가 알기로는 Swift버전이 업데이트 되면서 중간에 바뀐 것 같네요. 초반에 자료를 만들기 시작할때는 utility 였던 것으로 기억하고 있어서요.) 불편을 드렸다면 죄송합니다. 강의는.. 전반적인 내용 큰틀을 이해하시는 데는 도움이 되실 수 있으나, 아주 디테일한 내용에서는 직접 공식문서 등을 통해서 찾아보신 내용이 당연히 우선시 되리라고 생각됩니다. 감사합니다 :)
- 0
- 2
- 33
Q&A
18강 자식 작업의 메타데이터 상속 관련 강의 자료 문의
아 네, 큰 의미가 있는 것은 아니고그냥 단순하게 두가지 의미 때문에 취소선으로 표시해 놓은 것이기는 한데,"메타데이터"라는 것에는 (1) 첫번째로는 현재 실행 중인 액터가 있을 수도 있고 아닐 수도 있어서 취소선으로 표시해놓은 것 입니다. 즉, 구조적 동시성이 액터 내부에서 실행하는 경우 - 액터 상속하게 되고, 액터 내부가 아닌 곳에서 실행하는 경우 - 상속할 액터가 없어서... 상속할 액터가 있는 경우/없는 경우 때문에 그렇습니다.(2) (다른 측면에서는) 구조적 동시성 자체가 병렬처리를 목적으로 하는 것이라.. 부모 작업의 경우는 액터에서 실행될 수 있지만, 하위(자식) 작업들은 병렬로 처리되어 (액터와 상관없이) 실행되기 때문에 그런 의미에서 살짝(?) 표시를 해둔 것이긴 합니다. 감사합니다. :)
- 0
- 1
- 29
Q&A
withCheckedContinuation 에서 resume 호출의 안정성 보장 질문
아 네네, sujinnaljin 님.강의 내용이 잘못된 것 같네요! 공식 문서 내용을 저도 다시 확인해보니, 해당 부분에 대한 내용을 제가 자세하게 체크하지 못한 부분이 있네요! 말씀하신 내용이 맞습니다 ^^;(아마 저도 초반 학습 할 때의 내용에 대해 추후에 자세하게 살펴보진 않아서 더블 체크를 못했던 부분이네요.) 강의 내용, 교재 내용도 수정해 놓아야 겠네요!알려주셔서 감사합니다 :)
- 0
- 1
- 42
Q&A
참고 코드 자료 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
- 39
Q&A
기초 앱 4강 Type Any -> UIButton
네 기연 님.디테일하게 적어주셔서 감사합니다 😁 Any타입은 추상적인 타입이기 때문에, 만약 구체적인 타입인 UIButton으로 직접 지정해서 사용하지 않는 경우는 타입캐스팅을 해서 UIButton들의 속성에 접근할 수 있고,직접 UIButton을 설정하는 경우, 굳이 타입캐스팅을 하지 않아도 되니.. 조금 더 편하게 사용하실 수 있습니다. :)
- 0
- 2
- 37
Q&A
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
- 49
Q&A
10번 강의 관련하여 질문드립니다.
네 안녕하세요 개발자 님. 네 맞습니다. 엄밀하게 따져보면, 개발자 님이 말씀하신게 맞습니다. 제 교재 29 페이지에도 보시면, 함수가 재개가 될때는 다른 쓰레드에서 재개가 될 수 있다고 그림까지 그려서 설명드리고 있기도 하고요. 다만, 여기서 그렇게 설명드리면, 이해할 수 없을 정도로 엄청 복잡해 지지 않을까요? 그래서 해당 설명 드리고 있는 부분에서는 Task 자체가 특정한 쓰레드에서만 실행된다고 가정을 해야, 이론적으로 이해하는 측면에서 훨씬 "직관적으로" 이해가 쉬워지기 때문에, 그렇게 가정하고 말씀드리고 있는 부분이니.. 강의 내용에서 그런 부분을 참고 부탁드립니다. (실제로는 다른 쓰레드에서 재개(resume)되어 실행될 수 있다는 부분을 당연히 아신다는 (앞 부분의 강의 내용을 다 이해하시고 있다는) 가정 하에 최대한 직관적인 설명을 위한 가정일 뿐입니다.) 감사합니다. :)
- 0
- 2
- 47








