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문법 마스터 스쿨 (온라인 BootCamp - 2개월과정)
- 앨런 Swift문법 마스터 스쿨 (온라인 BootCamp - 2개월과정)
- 앨런 Swift문법 마스터 스쿨 (온라인 BootCamp - 2개월과정)
게시글
질문&답변
참고 코드 자료 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
- 18
질문&답변
기초 앱 4강 Type Any -> UIButton
네 기연 님.디테일하게 적어주셔서 감사합니다 😁 Any타입은 추상적인 타입이기 때문에, 만약 구체적인 타입인 UIButton으로 직접 지정해서 사용하지 않는 경우는 타입캐스팅을 해서 UIButton들의 속성에 접근할 수 있고,직접 UIButton을 설정하는 경우, 굳이 타입캐스팅을 하지 않아도 되니.. 조금 더 편하게 사용하실 수 있습니다. :)
- 0
- 2
- 21
질문&답변
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
- 27
질문&답변
10번 강의 관련하여 질문드립니다.
네 안녕하세요 개발자 님. 네 맞습니다. 엄밀하게 따져보면, 개발자 님이 말씀하신게 맞습니다. 제 교재 29 페이지에도 보시면, 함수가 재개가 될때는 다른 쓰레드에서 재개가 될 수 있다고 그림까지 그려서 설명드리고 있기도 하고요. 다만, 여기서 그렇게 설명드리면, 이해할 수 없을 정도로 엄청 복잡해 지지 않을까요? 그래서 해당 설명 드리고 있는 부분에서는 Task 자체가 특정한 쓰레드에서만 실행된다고 가정을 해야, 이론적으로 이해하는 측면에서 훨씬 "직관적으로" 이해가 쉬워지기 때문에, 그렇게 가정하고 말씀드리고 있는 부분이니.. 강의 내용에서 그런 부분을 참고 부탁드립니다. (실제로는 다른 쓰레드에서 재개(resume)되어 실행될 수 있다는 부분을 당연히 아신다는 (앞 부분의 강의 내용을 다 이해하시고 있다는) 가정 하에 최대한 직관적인 설명을 위한 가정일 뿐입니다.) 감사합니다. :)
- 0
- 2
- 30
질문&답변
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
- 31
질문&답변
비동기 반복문은 하나의 thread에서만 동작하게 되나요??
네, 안녕하세요 요니 님! 제가 이해한 바로는 'Swift Concurrency는 thread관점에서 벗어나서, Task라는 작업의 단위를 기준으로 비동기 관리를 한다' 라고 이해하고 있습니다.==> 네, Task라는 작업의 단위를 기준으로 관리한다는 말은 맞지만, 결국 그 내부 메커니즘을 파고 들어가보면 Task 내부에서는 1개의 쓰레드(1개의 CPU)를 사용하는 것입니다. (수업에서 그려드린 그림을 잘 참고해보시면 좋고요.) 그리고 Swift Concurrency 자체는 내부적으로 (운영체제에서.. 현재 사용하고 있지 않은 쓰레드를 양보하는) 협력적 Thread Pool 방식으로 동작합니다. (기존의 GCD에서 Swift Concurrency가 도입된 것은.. 이 협력적 Thread Pool 방식(쓰레드를 양보하는)이 훨씬 내부적으로 효율적으로 동작하게 만들어주는 부분이 있기 때문에 이 방식이 도입된 것입니다. 이 부분을 조금 더 자세하게 설명드리려면.. 내용이 너무 길어지기 때문에.. Part2강의의 CS부분을 참고하시면 많은 도움이 되실 것 같긴 합니다. 쓰레드는 편하게 CPU 1개라고 생각하셔도 됩니다.) 그래서 Task는 말 그대로.. "일"의 단위 정도로 생각하시면 됩니다. 예를 들어 작업(Task)은 언제든지 생성, 소멸 같은 일이 발생할 수 있는 것이고, 그 안에서 결국 운영체제가 생성한 Thread를 사용하게 되는 것입니다. 최대한 직관적으로, TaskGroup이라는 자체가 도입된 이유를 무엇일까?를 생각해보시면.. 결국 이러한 여러개의 Task가 (여러개의 쓰레드(CPU)를 사용해) 동시에 일을 시키도록 만들고, 마지막에는.. 그러한 작업들을 한데 모아서 처리하려는 목적을 가지고 설계된 것이 TaskGroup이라고 보시면 됩니다. (쉬운 병렬 작업과, 마지막에는 Thread-safe한 직렬이 필요한 것이죠.) 만약 하나의 thread에 고정되어 있다면, group을 통해서 결과가 넘어오게 될 때, 자식 Task중 과도하게 오래걸리는 작업이 있다고 가정하면 비동기 반복문이 실행되는 특정 thread가 계속 blocking되는건가? 하는 의문이 들어서요==> 결론적으로는 Non-Blocking방식이기 때문에 오래걸리는 작업이 있다고 하더라도, blocking되지 않습니다. 사실 이 부분이 Non-Blocking 방식의 장점인데, (추후의 Part2의 CS부분까지 학습하게되시면 내부적인 메커니즘을 더 정확하게 알게 되실텐데..) 무튼 최대한 직관적으로 설명드리면, 제가 Non-blocking을 설명드리면서 그려드렸던.. 아래와 같은 그림을 떠올려보시면 좋습니다. 오래걸리는 작업이 있다고 하더라도 (일을 안하는 동안에) 쓰레드를 양보하기 때문에 Blocking이 일어날 일이 없습니다. (그게 사용하지 않는 동안에 쓰레드를 양보하는 협력적 Thread Pool이 동작하는 방식이자 Swift Concurrency의 장점이죠.)(사진) 그리고 이 부분을 최대한 직관적으로 이해하시기 좋게.. 제가 이걸 TaskGroup에 적용해서 (오래 걸리는 작업이 있다고 가정하고 그림을 그려서 보여드리면..)(사진) 이런식으로 하위 자식 작업이 오래 걸리는 작업이 있다고 하더라도 결국 부모작업 조차도 자기가 동작하지 않는 동안에 쓰레드를 양보하게 됩니다. (Non-Blocking 방식) 그렇기 때문에 Thread에서 Blocking이 일어날 수 없습니다. 뭐 그렇다면 한가지 의문이 드실 수도 있는데요.. 그렇다면, 부모작업이 반드시 2번 쓰레드에서만 동작하는 것이냐? 이렇게 의문을 가지실 수도 있는데.. 물론 (엄밀하게 따지면) 그렇지는 않습니다. 부모작업 내부적으로 (작업 재개를 할 때) 2번 쓰레드를 사용할 수도 있고, 3번 쓰레드를 사용할 수도 있는데.. 사실, 그건 우리에게 중요한 관심사는 아닙니다. 왜냐면, (2번 쓰레드이던, 3번 쓰레드이던, 4번쓰레드이던..) (동시에 동작하는 일은 절대 발생하지 않고) 순차적으로(무조건 차례 차례) 1개의 특정 쓰레드에서만 (직렬방식으로) 일을 하면서 Thread-safe하게 일을 하게 되기 때문에.. 그게 (내부적으로 꼭 정해진 특정 쓰레드인) 2, 3, 4번 이던 간에 그게 중요한 것은 아니라는 것이죠. 다만, 그렇기 때문에 저는 최대한 직관적으로 생각하시기 편하게.. 부모 작업은 "특정" 2번 쓰레드에서만 동작하게 된다라고 설명드린 것이라고 보시면 됩니다. 추가적으로 궁금하신 부분 있으시면 또 질문 주세요. 감사합니다. :)
- 0
- 2
- 48
질문&답변
34강 유용한 앱 추천에 나오는 UIKitViewer에 대해서 질문이 있습니다
아 네,UIKit Box 라는 앱이 비슷합니다 🙂 UIKit Lab이라는 앱도 도움이 되실 수 있고요 !
- 0
- 1
- 37
질문&답변
10강 내용 문의드립니다.
네 안녕하세요!지금 일단 질문주신 내용에서는, 왜 1번쓰레드라고 찍히는 지를 정확하게 이해하고 계십니다.제가 말씀드린... Task 가 여러 개일 경우 '동일 시점에 여러개의 쓰레드에서 접근/사용 가능성이 있기 때문에' Race Condition이 발생할 수 있다고 설명드린 것은, Task로 비동기 처리를 하게 되는 일반적인 상황을 말씀드린 것이예요. 즉, 아래 코드와 같이 액터 내부의 메서드가 아닌, 일반 함수 또는 클래스, 구조체 등의 메서드로 구현하신 다면.. 예를 들어, 2번 쓰레드 / 3번 쓰레드로 동작하게 되니.. 각각의 Task 내부에서 데이터에 접근하게 되면 당연히 동시 접근이 일어날 수 있습니다.func test() { Task { // 2번 쓰레드 } Task { // 3번 쓰레드 } } 다만, 지금 윤종 님께서 만드신 코드의 경우, 스위프트UI 뷰 내부의 메서드로 만드셨기 때문에.. 지금 만드신 test( ) 메서드 자체가 @MainActor를 채택하게 됩니다. https://developer.apple.com/documentation/swiftui/view View프로토콜 공식문서에 들어가보시면 프로토콜 앞에 @MainActor 라고 붙어있고, 그 프로토콜을 채택하고 있기 때문에 ContentView 내부의 메서드들이 모두 @MainActor(1번 쓰레드)에서 실행되게 됩니다. (쉽게 직관적으로 생각해보자면.. 스위프트UI에서 뷰 관련 코드이니, 1번 쓰레드에서 실행되는 것이예요.)즉, 윤종 님께서 말씀하신 대로.. Task 는 액터내부에서 실행된다면, 해당 (실행)액터의 메타 데이터를 상속 받게 되기 때문에, 지금 현재 ContentView 안에서 test( ) 메서드를 구현하시면 결국 @MainActor가 붙어있는 메서드를 구현하신 것과 완전히 동일한 것이라고 보시면 됩니다.@MainActor func test() { Task { // 1번 쓰레드 (메타 데이터 상속) } Task { // 1번 쓰레드 (메타 데이터 상속) } } 그래서 당연히 결과도 1번 쓰레드라고 찍히고 있는 것이고요. 그래서 액터 내부에 메서드로 구현하시고 그 안에서 Task를 사용하시면, 그건 당연히 액터의 실행 컨텍스트를 물려 받아서(쉽게 말해 쓰레드를 물려 받기 때문에) 그렇게 하시면 안되고.. 액터 내부의 메서드가 아닌 일반적인 메서드나 함수로 구현하시면.. 당연히 일반적인 비동기 상황이기 때문에 여러쓰레드에서 동시에 접근하는 상황이 됩니다. (물론 당연히 알고 계시겠지만, async/await을 사용하는 일반적인 비동기 상황은.. 대부분 액터 내부의 메서드가 아닐껍니다.) 아, 그리고 프로토콜 채택 규칙과 관련해서는.. 교재 p.285 내용 및 강의 60강에서 설명드리고 있습니다. 참고하시면 될 것 같아요 ! 궁금하신 내용이 해결되셨길 바라요! 감사합니다. :)
- 0
- 2
- 43
질문&답변
깃에 코드는 올려도 된다고 하셨는데
네 Jiho 님!수업에서 사용하는 이미지들의 경우, 제가 직접 제작한 것이기 때문에 올리셔도 됩니다 :) 감사합니다 !
- 0
- 1
- 49
질문&답변
수강기간 연장신청 드려도 될까요?
네 연장해드렸어요! 죄송하지만, 질문 삭제 부탁드립니다 🙏🏻
- 0
- 1
- 39