-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.json
1 lines (1 loc) · 174 KB
/
index.json
1
[{"content":"Rust 프로젝트에서 third-party non-Rust 코드를 함께 컴파일해야 할 때 빌드 스크립트를 이용할 수 있다. 별도의 스크립트를 실행하지 않고 프로젝트에 통합할수 있다는 점이 굉장히 유용한 편!\n이 스크립트를 사용하는 예제로는\nbindgen, cc 등을 이용하여 non-Rust 코드를 컴파일, Rust 코드와 연결해야 할 때 gRPC, jsonschema 등을 사용해 정의된 타입을 변환할 때 등이 있다. 이 스크립트를 사용할 때 주의해야 하는 소소한 점들을 정리했다.\nbuild.rs 예제 Cargo는 컴파일 과정에서 build script를 빌드, 실행한다. 따라서 다음과 같은 빌드 스크립트는 패키지 컴파일 과정에서 hello.rs 파일을 만들게 된다. 패키지의 코드는 hello.rs 내 함수를 사용할 수 있다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // build.rs use std::env; use std::fs; use std::path::Path; fn main() { let out_dir = env::var_os(\u0026#34;OUT_DIR\u0026#34;).unwrap(); let dest_path = Path::new(\u0026amp;out_dir).join(\u0026#34;hello.rs\u0026#34;); fs::write( \u0026amp;dest_path, \u0026#34;pub fn message() -\u0026gt; \u0026amp;\u0026#39;static str { \\\u0026#34;Hello, World!\\\u0026#34; } \u0026#34; ).unwrap(); } Cargo.toml 의 package 옵션에 스크립트를 특정 파일로 지정할 수도 있다.\n1 2 3 4 5 [package] name = \u0026#34;build_example\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; build = \u0026#34;build.rs\u0026#34; # \u0026lt;- 바꿀 수 있음 build.rs dependency 빌드 스크립트를 사용하는 대표적인 경우 중 하나가 proto 파일로 정의된 gRPC를 사용할 때이다. tonic을 사용하면, proto에 정의된 대로 서버, 클라이언트 코드를 생성할 수 있다. 이 과정은 빌드 스크립트에서 일어난다.\n1 2 3 4 5 6 7 8 9 10 11 // build.rs fn main() { tonic_build::configure() .build_server(false) .out_dir(\u0026#34;src/rpc\u0026#34;) .compile( \u0026amp;[\u0026#34;proto/googleapis/google/pubsub/v1/pubsub.proto\u0026#34;], \u0026amp;[\u0026#34;proto/googleapis\u0026#34;], ) .unwrap(); } 이 때, build.rs는 tonic_build를 사용한다. 여기서 쓰는 dependency는 Cargo.toml 파일 build-dependencies 항목에 추가해 줘야 한다. 소소하지만 깜빡하기 쉬운 포인트!\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Cargo.toml [package] name = \u0026#34;build_example\u0026#34; version = \u0026#34;0.1.0\u0026#34; edition = \u0026#34;2021\u0026#34; build = \u0026#34;build.rs\u0026#34; # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] tonic = \u0026#34;0.10.2\u0026#34; tokio = { version = \u0026#34;1.35.1\u0026#34;, features = [\u0026#34;rt\u0026#34;, \u0026#34;rt-multi-thread\u0026#34;, \u0026#34;macros\u0026#34;] } prost = \u0026#34;0.12.3\u0026#34; [build-dependencies] tonic-build = \u0026#34;0.10.2\u0026#34; # \u0026lt;---- 여기 추가해 줘야 함 ! build.rs 재 실행 build.rs는 해당 파일의 변경점이 있을 때 재컴파일 되고, \u0026ldquo;패키지 내의 어떤 파일이 변경되면\u0026rdquo; 재 실행된다.\n위의 gRPC 예제에서 proto 파일이 패키지 내에 있다면, proto 파일이 변경될 때 마다 코드는 새로 생성된다.\n하지만 다음과 같은 구조로, 패키지 외부에 정의된 proto 파일(api.proto)을 참조하는 경우에는 해당 파일이 변경되어도 빌드 스크립트가 재실행되지 않는다.\n1 2 3 4 5 6 7 . ├── frontend ├── api.proto // \u0026lt;-- 참고하는 proto 파일 └── rust_backend // \u0026lt;-- package ├── Cargo.toml ├── build.rs └── main.rs 이 경우에는 다음과 같이 빌드 스크립트가 재실행되는 조건을 명시할 수 있다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 // build.rs fn main() { println!(\u0026#34;cargo:rerun-if-changed=../api.proto\u0026#34;); // \u0026lt;-- 이 명령을 출력! tonic_build::configure() .build_server(false) .out_dir(\u0026#34;src/rpc\u0026#34;) .compile( \u0026amp;[\u0026#34;proto/googleapis/google/pubsub/v1/pubsub.proto\u0026#34;], \u0026amp;[\u0026#34;proto/googleapis\u0026#34;], ) .unwrap(); } 패키지 외부의 파일을 참조할 때, 패키지가 매우 커 모든 변경 시 마다 실행하기 부담스러울 때 등에 활용하면 될 듯 하다.\nbuild.rs에서 src 참조하기 build.rs 내에서 src의 함수/자료구조 등에 접근하는 건 불가능하다. 반대도 마찬가지. stackoverflow나 reddit을 찾아보면 이 경우 crate를 분리하라고 한다. 즉, 참조해야 하는 내용을 별도의 crate로 분리하고 이를 build dependency로 추가하라는 것.\n1 2 3 4 5 6 7 8 9 10 . ├── the_library │ ├── Cargo.toml │ ├── build.rs │ └── src │ └── lib.rs └── the_type ├── Cargo.toml └── src └── lib.rs // \u0026lt;-- 참조하고 싶은 type만 별도의 crate로 분리 1 2 3 4 # the_library/Cargo.toml # add dependency to the library [build-dependencies] the_type = { path = \u0026#34;../the_type\u0026#34; } src 파일과 빌드 스크립트가 서로를 참조하는 경우는 없어야 하는 것 같다.\nReferences Build Scripts rerun-if-changed How can I import a source file from my library into build.rs ","permalink":"http://huijeong-kim.github.io/post/2024-02-02-cargo-build-run-if-changed/","summary":"Rust 프로젝트에서 third-party non-Rust 코드를 함께 컴파일해야 할 때 빌드 스크립트를 이용할 수 있다. 별도의 스크립트를 실행하지 않고 프로젝트에 통합할수 있다는 점이 굉장히 유용한 편!\n이 스크립트를 사용하는 예제로는\nbindgen, cc 등을 이용하여 non-Rust 코드를 컴파일, Rust 코드와 연결해야 할 때 gRPC, jsonschema 등을 사용해 정의된 타입을 변환할 때 등이 있다. 이 스크립트를 사용할 때 주의해야 하는 소소한 점들을 정리했다.\nbuild.rs 예제 Cargo는 컴파일 과정에서 build script를 빌드, 실행한다. 따라서 다음과 같은 빌드 스크립트는 패키지 컴파일 과정에서 hello.","title":"빌드 스크립트 build.rs "},{"content":"지난번에 이어, Andrew Ng 교수님의 \u0026ldquo;ML Strategy\u0026rdquo; 강의 이어서 정리합니다. 이번에는 ML 시스템 디버깅과 프로젝트 디자인 팁들이 소개됩니다.\nError Analysis 사람이 할 수 있는 일에 대한 learning algorithm이 아직 human performance에 미치지 못한다면, machine의 mistake를 수동으로(manually) 분석하여 인사이트를 얻고 추가 개선을 해 볼 수 있습니다. 이 과정을 Error Analysis라고 합니다.\nCarrying Out Error Analysis Error analysis를 통해 시스템의 \u0026ldquo;어떤\u0026rdquo; 성능을 높이는 게 좋을지 찾아볼 수 있습니다. Cat classifier의 예를 들면, 전체적인 성능을 높이는 것이 아니라 \u0026ldquo;dog picture가 cat으로 분류되는 경우 줄이기(false positive 줄이기)\u0026ldquo;와 같은 구체적인 목표를 세울 수 있습니다.\nError analysis를 수행하는 방식은 다음과 같습니다. 우선, dev set example 중에 잘못 판단된 mislabeled examples를 100+개 가량 모읍니다.(Y ≠ Y_hat인 항목들) 그리고 각각의 example들의 mislabel 특징을 분석해 보는 겁니다. 특정 패턴의 오류, 예를 들면 \u0026ldquo;큰 고양이들은 고양이로 인식되지 않는다\u0026rdquo; 와 같은 오류가 전체 오류 중 얼마나 차지하는 지 확인하고, 이를 개선해서 성능을 올릴 수 있을지 가늠해 봅니다. 이 중 가장 큰 비중을 차지는 오류 유형을 개선하는 데 집중한다면 효율적으로 성능을 개선할 수 있습니다.\nCat classifier의 예에서, 성능을 개선할 수 있는 방법들은,\nFix pictures of dogs being recognized as cats Fix great cats(lions, panthers, etc.) being misrecognized as cats Improve performance on blurry images 등이 있을텐데요. Error analysis를 해서 다음과 같은 표를 작성해 보면, 가장 많은 에러가 속한 \u0026ldquo;blurry image\u0026rdquo;, \u0026ldquo;great cats\u0026rdquo; 문제에 집중하는 게 좋은 걸 알 수 있습니다.\n단순하지만 효과적인 방법이라고 하는데, 여기서 error 분류를 뽑아낼 수 있으려면 data의 이해도가 높고 인사이트를 갖고 있어야 할 것 같아요.\nCleaning Up Incorrectly Labeled Data Training에 사용되는 데이터가 깨끗하고 좋은 데이터면 이상적이겠지만, 현실에선 그렇지 않습니다. 간혹 잘못 된 label(dog인데 cat이라고 labeled)을 발견할 수도 있습니다. 그럴 땐 어떻게 해야 할까요?\nTraining set에서 발견되었을 때\nDL algorithms들은 training set의 \u0026ldquo;랜덤\u0026quot;에러에 꽤 robust 합니다 Reasonably random, total data set is big enough라는 가정 하에서 무시할 수 있습니다 하지만 systematic error에는 less robust합니다 ⇒ white dog를 cat으로 일관되게 label 되어 있다면 문제가 될 수 있습니다 Dev/Test set에서 발견되었을 때\nError analysis에 \u0026ldquo;incorrectly labled\u0026rdquo; 항목을 추가해 수행해 봅니다. Incorrectly labeled data를 발견한다면 이 오류를 꼭 수정해야 할까요? 다음와 같이 incorrectly labled data가 에러의 6%를 차지하는 경우엔 이를 개선하는 게 좋을까요? 이에 대한 Andrew Ng의 답은 아래와 같습니다.\nMy advice is, if it makes a significant difference to your ability to evaluate algorithms on your dev set, then go ahead and spend the time to fix incorrect labels. But if it doesn\u0026rsquo;t make a significant difference to your ability to use the dev set to evaluate classifiers, then it might not be the best use of your time.\n이전 글에서 언급한 avoidable bias, variance 분석과 동일하게 접근하면 됩니다.\nOverall dev set error = 10% Errors due incorrect labels = 0.6% Errors due to other causes = 9.4% ⇒ 이 경우에는 9.4%인 다른 에러에 집중하는 게 좋고\nOverall dev set error = 2% Errors due incorrect labels = 0.6% Errors due to other causes = 1.4% ⇒ 이 경우에는 \u0026ldquo;incorrect labels\u0026quot;를 해결하는 게 좋습니다.\nDev set은 learning algorithm 을 고르는 용도임을 명심하고, incorrect label이 algorithm 선택에 영향을 줄 정도의 비중을 차지한다면 이를 고쳐야 합니다.\n**\u0026ldquo;Incorrect examples\u0026rdquo;**를 수정할 때 주의해야 할 것들이 있습니다.\nDev set과 test set은 같은 distribution이어야 합니다. 즉, dev set을 고친다면 test set도 함께 고쳐야 합니다. (그래도 slightly different distributions가 됨) 보통 false positive 위주로 살펴보게 되는데요(상대적으로 양도 적으니..), true negative 또한 살펴보는 게 좋습니다. 이런 분석이 의미 있을까 싶을 수 있겠지만, 현업에선 \u0026ldquo;manually 분석하는 것, human insight를 활용하는 것은 의미 있다.\u0026rdquo; 고 합니다.\nBuild your First System Quickly, then Iterate 위와 같은 error analysis를 통해 성능 개선이 가능하기 때문에, ML System을 만들 때도 agile 하게 움직이는 게 좋은 것 같습니다. ML System 구축 가이드라인은 다음과 같습니다.\nSet up development/ test set and metrics Set up a target Build an initial system quickly Train training set quickly: Fit the parameters Development set: Tune the parameters Test set: Assess the performance Use Bias/Variance analysis \u0026amp; Error analysis to prioritize next steps Mismatched Training and Dev/Test Set 앞에서는 dev set과 test set의 차이에 대해서 이야기 했는데, 이번에는 training set과 dev/test set의 차이입니다.\nTraining and Testing on Different Distributions 데이터가 항상 부족한 현실에서는 ML 기반 서비스를 구축 시 training set과 dev/test set의 분포가 달라지는 경우가 꽤 있다고 합니다. 대표적인 예가, 서비스 초기에 데이터가 부족해서 다른 데이터로 보충하는 경우입니다. 사용자의 이미지를 기반으로 분류하는 실제 시스템과는 다르게, 웹에서 얻은 고해상도, 전문적인 사진, 구매한 사진으로 트레이닝을 하는 겁니다. 이럴 때 취할 수 있는 접근은 다음과 같습니다.\nOption 1: 두 종류의 데이터를 합친 다음 shuffle → 이걸 train/dev/test 로 나눔\n+) 같은 distribution 활용한다는 장점 -) dev set의 대부분이 보충한 이미지(진짜 타겟이 아닌) 가 됨 ⇒ 타겟이 아닌 곳에 optimize 하게 된다 Option 2: training set은 모두 보충한 이미지(웹) + 일부 타겟 이미지, dev/test 는 모두 타겟하는 이미지\n+) aiming the target correctly (dev/test set) -) train 과 dev/test 데이터 간 distribution 차이가 있다 ⇒ 하지만 그래도 long-term으로는 괜찮다 Bias and Variance with Mismatched Data Distributions 지난 글에서 bias/variance 분석을 통해 다음 개선 타겟을 정할 수 있다고 하였는데, \u0026ldquo;mismatched data distribution\u0026quot;을 갖고 있다면 training error와 dev error 차이를 variance problem으로 보기 어려울 수 있습니다. Data set 차이로 인한 성능 차이인지, 알고리즘 문제인지 구분할 수 없기 때문입니다. 이런 경우, 새로운 data set을 정의할 필요가 있습니다. 바로 \u0026ldquo;Training-dev set\u0026rdquo; 입니다.\nTraining-dev set은 training set과 동일한 distribution을 갖나, training 용도가 아닌, dev data set 입니다.\nTraining set과 Training-dev set의 에러 차이가 크다면, 둘은 같은 distribution을 가지므로, variance problem을 판단할 수 있습니다. Training set과 Training-dev set의 에러 차이가 크지 않지만 Training-dev set과 Dev set의 에러 차이가 크다면 data mismatch 문제라 판단할 수 있습니다.\nAddressing Data Mismatch : no systematic solutions… manual insights… (but helps!)\nmanually 특징을 잡아내야 한다 - data 차이의 원인 찾기\nCarry out manual error analysis to try to understand difference between training and dev/test sets e.g., nosiy - car noise, street numbers Make training data more similar; or collect more data similar to dev/test sets e.g., simulate noisy in-car data → artificial data synthesis Artificial data synthesis: 데이터 생성 (조작하는 것)\n(augmented data)\ne.g., 의도적으로 car noise랑 합치는 것 ㅎㅎ 주의) sounds + car noise 합칠 때, 제한된 양의 car noise를 반복(여러 사운드와 합쳐서) 사용한다면, 그 noise에 overfitting 될 수 있는 위험이 있다, 사람이 보기엔 괜찮아 보이더라도.. 주의) car image를 graphic을 만들어서 사용할 때도 동일 → 특정 생성 패턴에 overfitting될 수 있다 (사람 눈에는 동일해 보여도..) Learning from Multiple Tasks ML system을 구성할 때 여러 개의 task(learning algorithm)을 함께 사용하는 경우가 있습니다.\nTransfer Learning (특히 큰 모델에서, 데이터가 많을 땐) training에 상당히 많은 시간이 소요됩니다. 하지만 새로운 어플리케이션을 위해 항상 새로 학습시켜야 하는 건 아닙니다. \u0026ldquo;Transfer learning\u0026quot;을 사용한다면, cat classifier에 사용하던 알고리즘을 가져 와 x-ray scan에서도 활용할 수 있습니다. (예제일 뿐.. 아닐 수도\u0026hellip;)\nTransfer learning이 가능한 이유는 low level features 는 서로 다른 system에서 learning하는 것이 유사하기 때문입니다. 이미지 분류 알고리즘들은 모두 lines, dot, curves와 같은 기본적인 이미지 구성 요소들을 구별하는 feature들을 갖고 있고, 이건 다른 이미지 분류 알고리즘에도 동일하게 적용 가능합니다.\n예를 들어, 다음과 같은 단계로 transfer learning을 할 수 있습니다.\n첫 번째 training: image recognition을 위한 training 진행 = pre-training 웹상에 공유된 pre-training 된 learning algorithm들을 활용할 수 있습니다 두 번째 training: radiology image로 training 진행 =fine-tuning Neural Network의 마지막 몇 개의 layer의 weight만 random initialize 하고 training 합니다 뒤에 몇 layer를 더 추가할 수도 있습니다 (특히 데이터가 충분하지 않은 경우) transfer learning이 의미 있을 때 는 다음과 같습니다.\n첫 번째 분야에는 데이터가 많고, 두 번째 분야에는 데이터가 상대적으로 부족한 경우입니다. 얼굴 인식 사내 출입 시스템을 만들 때, 직원의 사진만으로 충분히 training 시킬 수 없을 겁니다. 일반적인 얼굴 인식 알고리즘으로 pre-training 한 후, 임직원의 얼굴을 추가 training 시킨다면 보다 높은 성능을 얻을 수 있습니다. 반대의 경우엔 transfer learning 이 유효하지 않습니다. 두 분야의 input이 같아야 합니다. (Task A and B have the same input X) 둘 다 image거나 둘 다 audio여야 합니다. Multi-task Learning Transfer learning이 두 개의 training을 sequential 로 수행하는 거라면, multi-task learning은 여러 training을 동시에(simultanously) 수행하는 것 입니다.\n자율주행차의 예를 들면, 자율주행차는 주행 시 많은 사물, 오브젝트를 detect 해야 합니다. pedestrians, cars, stop signs, traffic lights 등등. 이 떄 필요한 \u0026ldquo;object detection algorithm\u0026quot;은 N개가 됩니다.\n이 때, 각각의 object detection을 위한 N개의 Neural Network를 구성하는 것 보다, 앞의 layer들을 공유하는 게 더 성능이 좋다고 합니다. N-dimentional의 Y를 가져야 하고, N개 분류 중 하나를 선택하는 Softmax와는 다르게, 하나의 이미지가 multiple label을 가질 수 있습니다.\nmulti-task learning이 의미있을 때는 다음과 같습니다.\nTraining on a set of tasks that could benefit from having shared low-level features Usually, Amount of data you have for each task is quite similar 단 하나의 task 만 필요하더라도 다른 training set도 같이 training 시키면 도움된다 Can train a big enough neural network to do well on all the tasks 충분히 크지 않다면 multi-task learning이 성능 좋지 않다 보통은 transfer learning이 더 많이 쓰이고, mutli task learning은 computer vision의 object detection에서 많이 쓰인다고 합니다.\nEnd-to-end Deep Learning 마지막으로, ML System 구성 팁입니다.\nWhat is End-to-end Deep Learning E2E DL은 모든 stage를 하나의 Neural Network에 대응시키는 것을 의미합니다. E2E DL의 대표적인 예는 \u0026ldquo;Speech Recognition\u0026rdquo; 입니다.\n예전에는(traditional pipeline) audio의 구성요소를 파악하고(intermediate component = audio → phonemes → transcript) transcript로 바꾸는 과정이 되었는데, 최근에는 이 전체 과정을 하나의 Neural Network 로 구성하는 경우가 많다고 합니다.\ndata가 적을 때(e.g., 고작 3000개)엔 traditional pipeline이 잘 동작하지만, 데이터가 충분히 많다면(e.g., 10,000 ~ 100,000개) E2E가 잘 동작한다고 합니다.\nFace recognition system은 E2E 보다 multi-step approach 를 더 많이 활용하는 예시입니다.\nImage → identity 로 바로 매핑하도록 training 할 수도 있지만, 다음과 같은 multi step approach 활용하면 더 좋은 성능을 얻는다고 합니다.\ndetect person’s face zoom and crop the face → 두 개의 사진을 주고 동일 인물인지 확인, new image가 10,000 개 중 하나와 같은 사람인지 확인하는 training 이 예제에서는 E2E 보다 multi-step으로 쪼개는 것이 더 의미있는 이유는,\n두개로 나누면 훨씬 간단한 문제가 되고 각 sub-task가 많은 데이터를 갖고 있기 때문입니다. face detection 용 데이터, identity 구분 데이터 각각이 많음 두 개를 합친 경우에 대한 데이터는 많지 않다 같은 이유로, machine translation 에서는 (English, French)와 같은 E2E 데이터가 많기 때문에 E2E가 더 잘 작동합니다.\nAutonomous car 또한 모두 Deep Learning만을 사용하지는 않습니다. 일부 X -\u0026gt; Y 매핑만 DL로 하는 경우가 많습니다. DL이 유용한 경우에만 적용하는 것이 좋습니다.\nWhether to use End-to-end Deep Learning 그럼 E2E DL을 정확히 언제 사용해야 될까요? E2E DL의 장, 단점은 아래와 같습니다.\nPros:\nLet the data speak (human preconception 을 강제하지 않는 게 더 좋을 때) e.g., speech recognition의 phoneomes는 사람이 강제로 만든 개념이다 Less hand-designing of components needed Cons:\nMay need large amount of data Excludes potentially useful hand-designed components (manual knowledge를 넣을 기회가 있으면 좋은 경우) data 부족할 땐 hand-design으로 human insight을 넣으면 유용하다 E2E DL을 도입할지 말지 결정하는 key question은,\nDo you have sufficient data to learn a function of the complexity needed to map x to y?\n입니다. 결국 데이터가 키인 것 같네요!\n","permalink":"http://huijeong-kim.github.io/post/2023-11-18-ml-strategy-2/","summary":"지난번에 이어, Andrew Ng 교수님의 \u0026ldquo;ML Strategy\u0026rdquo; 강의 이어서 정리합니다. 이번에는 ML 시스템 디버깅과 프로젝트 디자인 팁들이 소개됩니다.\nError Analysis 사람이 할 수 있는 일에 대한 learning algorithm이 아직 human performance에 미치지 못한다면, machine의 mistake를 수동으로(manually) 분석하여 인사이트를 얻고 추가 개선을 해 볼 수 있습니다. 이 과정을 Error Analysis라고 합니다.\nCarrying Out Error Analysis Error analysis를 통해 시스템의 \u0026ldquo;어떤\u0026rdquo; 성능을 높이는 게 좋을지 찾아볼 수 있습니다. Cat classifier의 예를 들면, 전체적인 성능을 높이는 것이 아니라 \u0026ldquo;dog picture가 cat으로 분류되는 경우 줄이기(false positive 줄이기)\u0026ldquo;와 같은 구체적인 목표를 세울 수 있습니다.","title":"ML Strategy 2"},{"content":"최근 Andrew Ng 교수님의 Deep Learning Specialization(DLS) 과정을 들었습니다. Machine Learning이라는 완전히 다른 패러다임에 적응하지 어렵고 오랫만에 보는 수식들에 정신이 없지만, 그래도 나름 재미있게 공부하고 있어요.\nDLS 강의 중 \u0026ldquo;ML Strategy\u0026rdquo; 강의가 특히 흥미로웠습니다. 이 강의를 통해 \u0026ldquo;ML engineering\u0026quot;을 어떻게 하는지 엿볼 수 있었기 때문입니다. 처음에는 ML System의 general 성능(실행속도 아닌 정확도를 의미)을 측정하는 게 불가능해 보였지만, 이 강의를 통해 ML System을 체계적으로, 전략적으로 분석, 개선하는 방법을 (조금이나마) 배울 수 있었습니다. 개인적으로 기억에 남는 강의여서 이 부분만 따로 정리해 올려봅니다. 총 2편으로 나눠집니다.\nIntroduction to ML Strategy : how to structure your ML project quickly and efficiently\nML의 성능이 원하는 만큼 나오지 않았을 때(e.g., 분류 시스템의 분류정확도가 떨어질 때) 시스템 개선을 위해 할 수 있는 일은 굉장히 많습니다. 더 많은 학습 데이터를 구해볼 수도 있고, ML model을 바꿔볼 수도 있고, hyperparameter들을 tuning해 볼 수도 있습니다. 하지만 아무 방법이나 마구잡이로 시도해 볼 순 없을 겁니다. 요즘의 ML 시스템의 규모와 모델 학습에 소요되는 어마어마한 시간을 고려해 본다면, 다양한 선택지를 전략적으로 접근하는 게 더욱 중요합니다.\n이렇게 많은 선택지 중 전략적인 접근을 하기 위해서는 \u0026ldquo;what to tune in order to try to achieve one effect\u0026rdquo; 를 아는 것이 굉장히 중요합니다. 이 특징을 \u0026ldquo;orthogonalization\u0026rdquo; 이라 하고, 하나의 \u0026ldquo;knob\u0026quot;을 돌려 한 가지의 효과를 얻을 수 있는 것을 의미합니다.\nML System의 성능 목표는 다음과 같은 순서로 타겟팅 되어야 합니다. 그리고 이 목표는 각각 다른 \u0026ldquo;knob\u0026quot;을 사용해서 조절해야 합니다. 각 성능 목표들을 달성시키는 과정이 다른 목표에 영향을 끼치지 않습니다. (=모두 orthogonal 합니다)\nChain of assumptions in ML Fit training set well on cost function knob: bigger network, better optimization algorithms,,, Fit dev set well on cost function knob: regularization, bigger training set, … Fit test set well on cost function knob: bigger dev set, … Performs well in real world knob: Change dev set or cost function 그런 의미에서, \u0026ldquo;early stopping\u0026quot;을 추천하지 않는다고 합니다. Training set, dev set 성능 둘 다에 영향을 미치기 때문입니다.\nSetting Up your Goal Single Number Evaluation Metric 성능을 평가할 때 가장 중요한 것은 \u0026ldquo;평가 기준\u0026ldquo;을 세우는 것 일겁니다. 평가 기준을 세울 때는 현재 시스템의 문제를 \u0026ldquo;single number evaluation metric\u0026ldquo;으로 표현할 수 있어야 합니다. 적절한 single number metric을 가지고 있다면 변경의 효과를 빠르게 파악할 수 있어 효율적인 실험이 가능합니다.\n사용할 수 있는 metric의 예는 다음과 같습니다.\nPrecision: of examples recognized as cat, what % actually are cats? cat으로 recognized된 것 중 몇 %가 진짜 cat인가 Recall: what % of actual cats are correctly recognized? actual cat 중 몇 %가 올바르게 recognized 되었는가? 이 때, Precision-Recall 두 개의 기준을 사용한다면 \u0026ldquo;single number\u0026quot;가 아닙니다. Best 옵션이 무엇인지 알기 어렵습니다. 그럴 땐, 두 지표를 합친 새로운 하나의 지표를 만들고, 이를 기준으로 판단하는 것이 좋습니다.\nPrecision, Recall의 경우 F1 score 를 많이 사용합니다. (harmonic mean of P and R)\n$$ F1score = { 2 \\over {{ 1 \\over P} + {1 \\over R }}} $$\n중요한 건\nwell-defined dev set = how your’e measuring precision and recall single real number evaluation metric = quickly tell which is better 이고 이를 통해 학습 및 실험의 iterating process를 speed-up 할 수 있습니다.\nSatisficing and Optimizing Metric 하지만 단 하나의 지표를 만들어 내기 모호한 경우가 있습니다. 예를 들면, \u0026ldquo;accuracy\u0026quot;와 \u0026ldquo;running time\u0026quot;의 평가 조건이 있을 때, 두 값을 평균을 낸다거나 \u0026ldquo;cost = accuracy - 0.5 * running_time\u0026quot;이라는 수식을 사용하면 원하는 목표를 적절하게 표현하지 못할 수 있습니다.\n이 경우엔 accuracy는 높으면 높을 수록 좋고 running time은 최소 조건만 만족하면 됩니다. accuracy는 optimizing metric으로, running time은 satisficing metric으로 정의하고, satisficing metric을 만족하는 것 중 optimizing metric을 최대화하는 모델을 고르면 됩니다. 이 때, N 개의 metric 중, 1개는 optimizing metrics, (N-1)개는 satisficing metric이 되어야 합니다.\n예를 들어, \u0026ldquo;trigger word\u0026rdquo; 인식 시스템의 평가 기준을 세워 봅시다. \u0026ldquo;trigger word\u0026quot;는 인공지능 스피커에서 사용자 입력 시작을 말합니다. 아이폰의 \u0026ldquo;Hi siri\u0026quot;나 구글의 \u0026ldquo;Okay google\u0026rdquo; 같은 단어입니다. 이 경우, accuracy, 즉 \u0026ldquo;Hi siri\u0026quot;를 인식하는 정확도는 높으면 높을수록 좋은 optimizing metric으로, false positivitiy(e.g., ≤1 false positive every 24 hours)는 satisficing metric으로 잡을 수 있습니다.\nSize of the Dev and Test Sets ML System 구축 시 사용되는 data는 보통 3가지의 data set로 나눠서 사용하게 됩니다.\nTraining set: machine training 용 data set Dev set: used to evaluate different ideas and pick one Test set: 최종 성능을 보는 data set. 이게 만족스러울 때 까지 dev set으로 최적을 찾는다 예전에는 다음과 같은 구성을 많이 사용했다고 하는데요, 이 구성은 data set이 작을 때, 예를 들면 100, 1K, 10K 정도의 데이터가 있을 때는 좋은 구성이라고 합니다.\n70 % training set, 30% test set 60% training set, 20% dev set, 20% test set Modern ML에서는 사용하는 데이터 양이 굉장히 많습니다. 1000K 정도의 데이터를 사용하는 요즘에는, 1%만 사용해도 test set이 10K example을 갖게 됩니다. 그래서 이 경우엔 training set에 최대한 많은 데이터를 사용하는 것이 더 좋다고 합니다. 엄청나게 큰 training set을 쓰는 것이 트렌드라고 하네요.\n98% training set, 1% dev set, 1% test set 여기서 test set의 크기에 대해 좀 더 얘기하자면,\nTest set은 시스템의 성능을 평가할 수 있을 만큼 big enough 해야 합니다. 아주 아주 accurate measure가 필요한 경우가 아니라면 millions 이상의 sample이 필요하지 않을 것, 즉 10K, 100K 면 충분하다고 합니다. 때로 high confidence가 필요하지 않은 경우엔, test set을 사용하지 않거나 dev set을 test set으로 사용할 수도 있습니다 - 추천하는 좋은 방향은 아니지만, 데이터가 절실할 땐 이런 시도도 해볼 수 있는 것 같습니다. Train/Dev/Test Distributions 갖고 있는 data들을 3개의 data set으로 나눌 때 주의해야 할 점들이 있습니다. 바로 \u0026ldquo;data distrubution\u0026rdquo; 입니다.\nDev data set과 test data set의 data distribution은 최대한 유사해야 합니다. 예를 들어, 여러 region에서 얻은 data가 있다면 이걸 어떻게 사용해야 할까요?\n특정 지역에서 나온 데이터를 dev set으로 / 또 다른 지역에서 나온 것 test set으로 사용 좋지 않은 선택입니다. dev/test set의 distribution이 달라져, 타겟 분포가 아닌 분포를 갖고 학습시키기 때문입니다. 학습과 테스트의 타겟 자체가 달라지는 것 입니다.\nrandomly shuffle into dev / test set 이렇게 하면 dev/test set의 distribution이 같아지므로, 좋은 선택입니다.\n수업에서 제시 된 Guideline은 다음과 같습니다.\nChoose a dev set and test set to reflect data you expect to get in the future and consider important to do well on same distribution! 타겟하는 형태로 트레이닝/테스트 해야 한다! 참고로, 이후 다른책에서 \u0026ldquo;훈련/테스트 데이터 분포에 차이가 있을 때\u0026rdquo; 할 수 있는 전략을 알 수 있었는데요. 보통의 ML 과제에서는 data distribution 조절이 그리 어렵지 않으나, 캐글 대회에서는 정해진 training, test set 이 있어서 이런 상황에 대응해야 할 때가 있습니다. 이럴 때 취할 수 있는 전략은 아래와 같습니다.\n억제: dev/test set 분포가 같아질 때가지, 결과에 가장 큰 영향을 미치는 변수를 제거해 본다. test set과 가장 유사한 세트로 훈련: test set과 유사한 분포를 가진 dev set을 sampling 하여 사용한다. test set를 모방한 검증: 모든 데이터를 학습에 사용하되, test set과 유사한 분포의 데이터만 평가에 사용한다. When to Change Dev/Test Sets and Metrics? ML System을 개발하다 보면, 어떤 이유에서든 원하지 않는 혹은 잘못된 결과가 나온다면, 더 나은 알고리즘을 판별하는 기준을 바꿔야 할 수 있습니다. Evaluation metric 혹은 dev/test set을 바꿔야 합니다.\nEvaluation metric을 새로 만드는 예시: 기존의 cost function에 새로운 weight를 추가할 수 있습니다. dev/test set을 바꾸는 예시:\nDev/test set에서는 고화질의 사진을 사용했는데, user images는 low quality,./. 일 경우 데이터셋의 변화가 필요합니다. If doing well on your metric + dev / test set does not correspond to doing well on your application, change your metric and/or dev/test set 중요한 건, 완벽하지 않은 evaluation metric을 갖고 있더라도 일단 이를 타겟으로 빠르게 시작하는 것 입니다.\nComparing to Human-level Performance 최근의 ML의 급격한 발전으로, 그 성능이 human-level 성능과 비교할 만 해졌습니다. 그래서 이제는 성능 목표를 human-level performance, 혹은 그 이상을 잡는 경우가 많은 것 같습니다.\n완벽한 성능은 불가능 할 수도 있습니다. 이미지 분류 시스템에서 사진 혹은 그림이 애매하여 사람조차도, 그 누구도 그게 무엇인지 말할 수 없는 경우가 그 예입니다. 반면 Bayes-optimal performance는 특정 task에 도달할 수 있는 이론 상 최대 성능입니다. Human-level performance보다 높은 정확도로, 우리가 타겟하는 성능이 될 수 있습니다.\n위의 그래프에서 보는 것과 같이, ML System의 성능이 human-level performance에 도달하기 까지는 금방이지만, 그 이후에는 성능 개선이 어려워 집니다. 하지만 사실 human-level performance가 꽤 높습니다(bayes-optimal에 꽤 가깝습니다). Human-level performance보다 낮을 땐 사용할 수 있는 tool이 꽤 많이 있으나, 그 이후 개선에 사용할 수 있는 툴은 많지 않습니다.\n많은 task들에 대해, Human-level performance는 꽤 좋습니다. 그래서, ML System이 human-level performance 이하라면, 다음과 같은 것들을 해 볼 수 있습니다.\nGet labeled data from humans Gain insight from manual error analysis: Why did a person get this right? Better analysis of bias/variance (아래 이어질 내용) Avoidable Bias Bayes-optimal performance의 값을 정확히 알 수 없으니, 우리는 Human level performance를 bayes optimal performance의 proxy로 사용할 수 있습니다. Human level performance에 도달하는 것을 목표로 잡는 것만으로도 충분할 수 있는 거죠.\n풀고자 하는 문제의 최대 성능, human level performance를 dev/training set의 성능과 비교할 때, human level performance와 training set performance 간 차이를 avoidable bias라고 하고 training set 과 dev set 사이의 performance 차이는 variance라고 합니다.\n다음 그림과 같이, 여러 ML 모델의 성능이 다음과 같다고 예를 들면,\n왼쪽: training set error는 8%, Dev set error는 10%인데, human level error는 1%인 경우입니다. 이 경우 training-dev error 차이를 좁히는 것도 의미가 있지만, training set의 성능 자체를 human level 까지 올리는 데 집중하는 것이 좀 더 의미 있습니다. (avoidable bias에 집중하는 것이 좋다)\n오른쪽: training/dev set error는 왼쪽과 동일하나 human level error가 7.5%, 즉 dev set error와 크게 차이가 나지 않는 경우 입니다. 이 경우는 human level performance에 도달하는 것 보다, training-dev set 간 성능 차이를 개선하는 것이 더 의미있습니다. (variance에 집중하는 것이 좋다)\nUnderstanding Human-level Performance 그렇다면, Human level performance은 어떻게 측정할 수 있을까요. 사실 이 성능은 애매할 수 있습니다. \u0026ldquo;어떤 사람\u0026quot;이냐에 따라 그 성능은 천차만별일 수 있기 때문입니다.\nX-ray 이미지 분류 예시에서는, 보통의 사람들은 3%의 에러율로 이미지를 분류하지만, 숙련된 전문가들은 0.7%의 에러율을, 숙련된 전문가들이 토론을 통해 낸 결론에서는 고작 0.5%의 에러율로 이미지를 분류했다고 합니다.\n이 때 human-level performance는 어떤 사람으로 잡아야 할까요? 이 경우 Bayes-optimal performance의 proxy로 사용할 성능은 가장 좋은 성능입니다. 즉, bayes-optimal 성능은 0.5%보다 낮을 것 이기 때문에, 0.5를 목표 성능, human-level performance로 잡아야 하는 것 입니다.\n하지만 용도에 따라 조금씩 선택이 달라질 수 있다곤 합니다. Human level performance를 최대치로 잡으면 집중해야 하는 부분을 잘못 선택할 수도 있습니다. Variance 문제를 풀어야 하는데, bias 에 집중하게 만드는 경우이죠. Human-level 성능과 training-dev set 성능이 충분히 높아 그 차이가 매우 크지 않을 때 이 문제가 두드러진다고 해요. 항상 마지막 행주를 쥐어짜는 성능 개선은 미묘하고 어려운가 봅니다.\nSurpassing Human-level Performance 다음과 같은 분야들에선 ML System이 human-level 성능을 \u0026ldquo;surpass\u0026rdquo; 하곤 합니다.\nOnline advertising Product recommendations Logistics (predicting transit time) Loan approvals Speech recognition Some image recognition Medical tasks 대부분 structured data로부터 학습하는 것, 많은 데이터를 분석하는 것, 그리고 natural perception이 아닌 것들입니다. Natural perception 과 관련된 task는 인간을 이기기가 어렵습니다.\nImproving your Model Performance 지금까지의 내용을 요약하면 다음과 같습니다.\nSupervised learning에서 원하는 성능을 만족시키기 위해선 다음 두가지를 고려해야 합니다. 그리고 두 task는 서로 orthogonal 합니다.\nYou can fit the training set pretty well (low avoidable bias) The training set performance generalizes pretty well to the dev/test set (low variance) ","permalink":"http://huijeong-kim.github.io/post/2023-11-09-ml-strategy-1/","summary":"최근 Andrew Ng 교수님의 Deep Learning Specialization(DLS) 과정을 들었습니다. Machine Learning이라는 완전히 다른 패러다임에 적응하지 어렵고 오랫만에 보는 수식들에 정신이 없지만, 그래도 나름 재미있게 공부하고 있어요.\nDLS 강의 중 \u0026ldquo;ML Strategy\u0026rdquo; 강의가 특히 흥미로웠습니다. 이 강의를 통해 \u0026ldquo;ML engineering\u0026quot;을 어떻게 하는지 엿볼 수 있었기 때문입니다. 처음에는 ML System의 general 성능(실행속도 아닌 정확도를 의미)을 측정하는 게 불가능해 보였지만, 이 강의를 통해 ML System을 체계적으로, 전략적으로 분석, 개선하는 방법을 (조금이나마) 배울 수 있었습니다.","title":"ML Strategy 1"},{"content":"이 블로그는 정적 사이트 생성기를 사용하여 웹페이지를 생성하고, 이를 GitHub에 올려서 hosting하고 있습니다. 그 동안은 Jekyll 을 사용하여 페이지를 생성했는데, 이번에 Hugo로 바꿔봤습니다. 어떻게 바꿨는지, 무엇이 좋았는지를 정리해 봤습니다.\nWhy? Jekyll을 사용한 건 가장 유명한 정적 사이트 생성기이고 GitHub 에서 빌드를 해 주기 때문이었습니다. 적당한 theme 을 찾아 받고 그 위에 글을 올리고, 가끔 약간의 customize를 했습니다.\n근데 사용하기가 조금 불편했습니다. 웹 지식이 부족해서인 것 같긴 하지만, 어떤 것이 생성 결과물인지, 내가 customize 하고자 하는 부분과 관련된 코드는 어디에 있는지 등을 찾기 어려웠습니다.\n기존 theme의 git repo를 fork 해서 사용하는 것도 별로였습니다. Theme repo 의 최신 변경점을 내 repo 에 반영하기도, 내 변경점과 분리 관리하기도 어려운 구조였어요.\n그래서 대안을 찾다 요즘 많이 보이는 Hugo를 사용해 봤습니다. 점점 인기가 많아져 Jekyll 보다 GitHub star 수가 많다는 Hugo는 빠른 것이 강점인 Go 기반의 정적 사이트 생성기 입니다. 일단 마음에 드는 theme 이 있어서 다운받아 본 건데요, 설치하고 잠깐 사용해 보는데 꽤 만족스러워서 바로 옮겼습니다.\nMigration Jekyll에서 Hugo로 옮겨가는 과정은 다음과 같습니다.\n설치, theme 다운로드 등 기본 사용법에 대한 내용은 공식 홈페이지를 참고해 주세요. Hugo 설치하고, hugo new site 'folder-name' 명령어를 사용해 새 블로그를 만들고, theme 을 theme 폴더에 git submodule로 추가하고 셋업하면, 바로 빌드하고 결과를 확인할 수 있습니다.\n이미 Jekyll 블로그를 가지고 있는 경우라면, 다음과 같이 import jekyll 명령어를 사용하여 간편하게 옮길 수 있습니다. huijeong-kim.github.io/ 가 이전에 사용하던 Jekyll 폴더고, migrate 가 Hugo를 사용한 새 블로그의 폴더명 입니다. migrate 폴더는 이 명령어를 통해 생성됩니다.\n$ hugo import jekyll huijeong-kim.github.io/ migrate 저는 처음 Hugo 설치하고 구경하다 글도 옮겨봤는데 잘 되어 그대로 전부 옮겨서, 하나 하나 손으로 옮겼습니다. 그러다 뒤 늦게 이 명령어를 알고 실행해 봤는데 아주 잘 되네요! 다들 이거 쓰세여\u0026hellip;\nTroubleshooting 이전 과정에서 본 아주 소소한(troubleshooting이란 이름을 붙이기도 애매할 정도의) 오류 혹은 이슈들은 다음과 같습니다.\nMarkdown meta 정보 포맷 오류 제 블로그에선 tags 항목에서 오류가 있었습니다. 기존 블로그를 Hugo로 import 한 뒤 빌드하면 다음과 같은 에러 메시지가 나왔습니다. tags 항목을 iterate 하지 못 하고 있었습니다.\nError: error building site: render: failed to render pages: render of \u0026#34;page\u0026#34; failed: \u0026#34;/Users/huijeongkim/Workspace/blog-test/migrate/themes/papermod/layouts/_default/baseof.html:5:8\u0026#34;: execute of template failed: template: _default/single.html:5:8: executing \u0026#34;_default/single.html\u0026#34; at \u0026lt;partial \u0026#34;head.html\u0026#34; .\u0026gt;: error calling partial: \u0026#34;/Users/huijeongkim/Workspace/blog-test/migrate/themes/papermod/layouts/partials/head.html:19:31\u0026#34;: execute of template failed: template: partials/head.html:19:31: executing \u0026#34;partials/head.html\u0026#34; at \u0026lt;.Params.tags\u0026gt;: range can\u0026#39;t iterate over as, rust, type casting 사실 이전에는 다음과 같은 형태로 tags 항목을 작성했는데요. --- date: \u0026#34;2023-05-02T00:00:00Z\u0026#34; tags: rust, as, type casting title: as operator in Rust ---\nHugo(혹은 제가 사용하는 theme) 에서는 이런 형태로 작성해야 했습니다. --- date: \u0026#34;2023-05-02T00:00:00Z\u0026#34; tags: [\u0026#34;rust\u0026#34;, \u0026#34;as\u0026#34;, \u0026#34;type casting\u0026#34;] title: as operator in Rust ---\n이전 블로그에서는 인터넷 어딘가에서 발견한 코드를 복붙해서 조금 이상한 형태였던 것 같긴 해요. 이게 Hugo(혹은 제가 사용한 theme)에서 tags 가져 가는 방식과 다른 것 같고요. 아무튼, 이런 식의 변환 오류가 생길 수 있고, 이 경우 손수 혹은 스크립트 작성해서 변환해야 할 수 있습니다.\nPost permalink Jekyll을 사용할 때 블로그 글의 permalink를 아래와 같이 설정했었습니다. 그래서 글 주소는 https://huijeong-kim.github.io/2023/05/02/rust-as-operator/ 과 같은 형태가 되었어요.\npermalink: /:year/:month/:day/:title/ Hugo로 import 한 뒤에는 글 주소가 https://huijeong-kim.github.io/post/2023-05-02-rust-as-operator/ 로 바뀌었습니다. 달라진 것은 1)post 가 추가 됨, 2) 날짜 형태 입니다.\n먼저, post가 추가된 이유를 보면,\nHugo의 폴더 구조는 다음과 같고, 모든 컨텐츠들은 content 폴더에 추가됩니다. 그리고 공식 사이트 설명에 따르면 content 폴더 내의 top-level 폴더는 \u0026ldquo;content section\u0026quot;을 의미한다고 해요. Content section은 post와 같은 폴더로 하위 글을 갖고 있는 section일 수도 있고, archives.md 파일과 같은 단일 페이지로 이루어진 section일 수도 있습니다.\nSection 지정 없이 새로운 글을 추가한다면, 글 목록이 아닌 메뉴에 항목이 추가될 것 입니다. hugo new 2023/06/15/my-new-page.md로 글을 추가하려 하면, 블로그 글이 추가되는 것이 아니라 2023이란 context section이 추가되는 것 이죠.\n그리고 날짜 형태를 보겠습니다. 만일 yyyy/mm/dd 형태의 글을 다음과 같이 추가한다면,\n1 hugo new post/2023/05/02/rust-as-operator.md 그럼 post/2023/05/02/rust-as-operator.md 파일이 생성됩니다. 즉, content/post 폴더 안에, 2023 폴더, 그 안에 05 폴더, 그 안에 02 폴더, 그 안에 rust-as-operator.md 파일이 생성됩니다.\nyyyy/mm/dd/title 형태의 url을 가져가려면 이런 식의 날짜 hierarchy 를 가질 수 밖에 없더라고요. 공식 사이트 에도 이런 구조가 date-base hierarchy 로 소개되어 있습니다.\nContent 추가 시 title을 2023/06/15/my-new-page.md로 할 순 없지만, 또 다른 방법이 있더라고요! Title은 2023-06-15-my-new-page.md 등으로 설정하고, md 파일에 url을 지정하면 됩니다.\n이 url 항목을 추가한다면 무사히 이전 링크를 그대로 사용할 수 있습니다. 모든 makrdown 파일을 수정하는 게 좀 귀찮은 일이 될 수 있겠지만, 글 url 수정을 원치 않다면 이렇게 할 수 밖에 없을 것 같습니다. 아래에 설명 될 utterance 댓글 연동 관점에서도 url 유지하는 게 좋기도 하니, 고려해 볼만 합니다.\n*2023이라는 section이 추가되고, 동일한 이름의 파일이 이 section에 추가된다면 (2023/06/15/my-new-page.md) 약간의 혼란이 있는 것 같긴 합니다만, 그렇게 쓸 일이 없겠죠.\n--- date: \u0026#34;2023-05-02T00:00:00Z\u0026#34; tags: rust, as, type casting title: as operator in Rust url: \u0026#39;/2023/05/02/rust-as-operator-rust/\u0026#39; --- 보통 하루에 글 한 개 미만인 저에게는 date-based hierarchy가 딱히 필요하지 않고, title과 link 가 다른 것도 마음에 들지 않아, 그냥 title을 yyyy-mm-dd-title 형태로 모두 바꿨습니다. 사실 이전에 yyyy/mm/dd 형태를 쓴게 후회되네요. 글이 몇 개 안 되어서 다행이었어요.\nutterance 댓글 재 연동 Hugo는 Disqus 연동을 지원합니다. 하지만 저는 기존 블로그에서 사용했던 utterance를 사용하고, 이전에 달린 댓글을 그대로 유지하고 싶어서 다음과 같이 수정하였습니다.\n먼저 theme 에 정의된 comments.html 을 오버라이딩 하는 파일을 추가하였습니다. themes/papermod/layouts/partials/comments.html 에 comment 관련 코드가 있는데, layouts/partials/comments.html 파일을 추가하면 theme 파일을 오버라이딩 할 수 있습니다.\n기존 블로그에 삽입했던 utterance script를 나의 comments.html 파일에 붙여넣으면 됩니다. 필요하다면 layouts/_default/single.html 파일을 오버라이딩 하여 comment, navigator, share button간 순서 및 위치 조정을 할 수도 있습니다.\n저는 utterance의 Blog Post \u0026lt;-\u0026gt; Issue Mapping 옵션을 \u0026ldquo;Issue title contains page pathname\u0026rdquo; 옵션으로 사용했습니다. 그 말은 즉, 생성 된 issue들의 이름과 pathname이 같다면 댓글이 연동된다는 의미였습니다.\n기존 글들의 pathname을 모두 바꿨기 때문에, utterance로 인해 생긴 issue들의 title을 2023/05/02/rust-as-operator 에서 post/2023-05-02-rust-as-operator로 바꾸니 기존의 댓글들이 새 블로그에서도 정상적으로 보였습니다.\nDeploy Jekyll을 사용할 때는 따로 빌드하지 않아도, markdown 글만 커밋해도 github가 빌드해 줬습니다. 이게 Jekyll의 가장 큰 장점이었던 것 같은데, 이제는 이 부분을 다른 방식으로 해결해야 합니다.\n많은 사람들이 netlify를 활용하던데요. 저는 가끔 수정되는 정적 사이트를 배포 툴을 사용할 필요가 있을까라는 생각, 내 블로그 URL을 *.netlify.app 로 바꾸고 싶지 않다는 생각에 도입하진 않았습니다.\n대신 제 블로그 repo 인 huijeong-kim.github.io 에 생성 결과된 HTML를 업로드하도록 했습니다. Hugo repo의 결과물이 나오는 폴더인 public 폴더를 제 블로그 repo가 되도록 git submodule로 연결하였습니다. 이 아이디어는 검색하다 여기서 보고 따라해 봤어요. (정말 좋은 아이디어에요. 감사합니다!)\n\u0026lt; 디렉터리 구조와 연결된 git submodule \u0026gt; blog ├── archetypes ├── content ├── hugo.toml ├── layouts ├── public ----\u0026gt; (submodule) huijeong-kim.github.io ├── resources └── themes └── papermod ---\u0026gt; (submodule) hugo-PaperMod\n여기서 두 개의 git repo를 관리하는 것이 조금 귀찮아 지는데요. 위의 링크에 소개된 것과 같이 쉘 스크립트로 자동화 해도 되고, git hook을 걸어서 markdown 파일 commit/push 하면 submodule에도 관련 변경점이 같이 commit/push 되도록 관리해도 좋을 것 같습니다.\n아직은 git commit이 귀찮지 않은데, 조만간 ChatGPT가 작성해 준 아래의 post-commit hook과 push script를 도입, 가능하다면 GitHub Actions에도 추가해 보면 좋을 것 같아요.\npost-commit hook을 통한 sub-module commit 자동화\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/bin/bash # Get the commit message from the initial commit commit_message=$(git log --format=%B -n 1 HEAD) # Path to the submodule directory submodule_dir=\u0026#34;path/to/submodule\u0026#34; # Check if the submodule has any changes cd \u0026#34;$submodule_dir\u0026#34; has_changes=$(git status --porcelain) # Commit the changes in the submodule, if any if [[ -n \u0026#34;$has_changes\u0026#34; ]]; then git add . git commit -m \u0026#34;$commit_message (submodule update)\u0026#34; fi exit 0 Remote commit push 할 때 submodule도 함께 push하는 스크립트\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash # Path to the submodule directory submodule_dir=\u0026#34;path/to/submodule\u0026#34; # Remote name for the submodule submodule_remote=\u0026#34;origin\u0026#34; # Check if the current branch is the branch you want to track current_branch=$(git rev-parse --abbrev-ref HEAD) tracked_branch=\u0026#34;main\u0026#34; # Replace with your desired branch name if [[ \u0026#34;$current_branch\u0026#34; != \u0026#34;$tracked_branch\u0026#34; ]]; then exit 0 fi # Push changes to the main repository remote git push # Push changes to the submodule remote cd \u0026#34;$submodule_dir\u0026#34; git push \u0026#34;$submodule_remote\u0026#34; \u0026#34;$current_branch\u0026#34; exit 0 좋은 점 빠르다 Hugo는 \u0026ldquo;빠른 정적 사이트 생성기\u0026quot;로 유명합니다. 처음에는 블로그 글 쓰는 게 자주 있는 것도 아니고, 이 생성 과정이 빠른 것이 크게 의미 있지 않다고 생각했어요. 그리고 빨라 봤자 큰 차이 없게찌.. 라는 생각도 있었고요.\n근데 막상 사용해 보니 신세계입니다. Jekyll 사용할 때는 조금 느린 것 같아 markdown 문서를 작성한 후 마지막에 퇴고시에만 bundle exec jekyll serve --watch로 결과 확인하면서 마무리 했습니다. 근데 이젠 글 작성 하면서 Ctrl+S 누를 때 마다 바로바로 빠르게 업데이트 된 웹 페이지를 볼 수 있어 매우X10000 편합니다. 블로그 작성 효율이 올라가고 만족도도 올라가, 블로그에 글을 올리는 것이 좀 더 재밌는 일이 되었습니다.\n직관적인 구조, 좋은 공식 문서 어디서 부터 어디까지가 theme 인지, output이 어디로 나오는지, 등등이 직관적으로 구성된 단순한 폴더 구조로 보여집니다. 웹을 모르는 저에게는 이 점이 크게 다가왔습니다. 게다가 공식 문서에 궁금한 대부분의 것들이 적혀 있어서, 추후 customize 하고 공부할 때 많은 도움이 될 것 같아요.\n사용하기 쉬운 툴 hugo 명령어만 있음 어디든 갈 수 있을 것만 같아요. 단순해서 좋아요.\n손쉬운 테마 도입 및 변경 테마 도입이 매우 쉽고 간편합니다. themes 폴더 아래 git submodule을 추가, hugo.toml 파일 내에 theme = \u0026quot;papermod\u0026quot; 를 추가하면 테마 도입 끝! (config 파일도 yml/toml/json 중 원하는 형태 선택 가능한 것도 또 장점) Theme 을 변경할 때도 새로운 submodule을 추가하고 toml 파일만 변경하면 되어서 매우 편합니다. 아래에서 언급할 overriding 된 파일 관리는 좀 필요하겠지만요.\nTheme Overriding 제일 좋았던 점 입니다. Theme 코드들을 직접 수정/변경/삭제 하는 것이 아니라 overriding을 할 수 있습니다!!!!!! Static file, Template file 등을 원하는 파일로 변경할 때 overriding 할 수 있습니다. 간단히 themes/my-theme/{변경하고자 하는파일}의 파일의 수정 버전을 {변경하고자 하는 파일}에 두면 됩니다. 즉, themes/my-theme/layouts/partials/comments.html을 수정하고 싶으면 layouts/partials/comments.html 파일을 생성, 원하는 내용을 넣으면 됩니다.\n그 결과 대 만족! 그 결과 만족스러운 블로그를 얻었습니다. 처음 사용해 보지만 아주 쉽게 소소한 기능을 추가/커스터마이즈도 할 수 있었습니다. 깔끔한 사용성, 직관적인 구조, 디테일한 문서를 보고 있으니 rust 처음 사용할 때 기분이 드네요.\n앞으로 어떻게 더 활용할 수 있을지 궁금한데, 더 공부하고 블로그도 멋지게 꾸며 봐야겠어요!\n","permalink":"http://huijeong-kim.github.io/post/2023-06-15-jekyll-to-hugo/","summary":"이 블로그는 정적 사이트 생성기를 사용하여 웹페이지를 생성하고, 이를 GitHub에 올려서 hosting하고 있습니다. 그 동안은 Jekyll 을 사용하여 페이지를 생성했는데, 이번에 Hugo로 바꿔봤습니다. 어떻게 바꿨는지, 무엇이 좋았는지를 정리해 봤습니다.\nWhy? Jekyll을 사용한 건 가장 유명한 정적 사이트 생성기이고 GitHub 에서 빌드를 해 주기 때문이었습니다. 적당한 theme 을 찾아 받고 그 위에 글을 올리고, 가끔 약간의 customize를 했습니다.\n근데 사용하기가 조금 불편했습니다. 웹 지식이 부족해서인 것 같긴 하지만, 어떤 것이 생성 결과물인지, 내가 customize 하고자 하는 부분과 관련된 코드는 어디에 있는지 등을 찾기 어려웠습니다.","title":"Jekyll to Hugo"},{"content":"안녕하세요.\n오늘은 Rust의 문자열 개념을 간단하게 알아보고, 코드 작성 시 문자열 관련 혼란스러웠던 포인트들을 정리해 보고자 합니다.\n1. String vs. str String 과 str 는 모두 valid UTF-8 문자열을 나타냅니다. Invalid UTF-8 데이터로 String 타입을 생성할 수 없습니다. UTF-8, Unicode 등에 대한 설명은 생략합니다.\nRust에서는 C와 같이 null-terminating string 개념을 사용하지 않습니다. 대신 String 타입은 문자열과 그 길이를 갖고 있는 \u0026ldquo;fat pointer\u0026rdquo; 입니다. \u0026ldquo;fat pointer\u0026quot;는 raw pointer과 additional metadata (e.g., length)를 갖고 있는 포인터를 의미합니다.\nString과 str(string slice) 타입의 주요 차이점은 다음과 같습니다.\n||String|str| |저장 형태|Vec\u0026lt;u8\u0026gt;|[u8]| |할당 위치|Heap|Stack| |Ownership|O|X| |Mutability|growable|immutable|\nas_str()을 사용하면 해당 String을 참조하는 \u0026amp;str을 얻을 수 있습니다. 이 때 String의 lifetime이 \u0026amp;str보다 길어야 합니다.\ninto_string()을 사용하면 \u0026amp;str을 String 형태로 변환할 수 있습니다. 이 때 heap 위에 새로운 메모리가 할당되고 데이터가 복사됩니다. to_owned()를 사용하여 ownership이 없는 \u0026amp;str을 ownership이 있는 String으로 바꿀 수도 있습니다. 이 때도 복사가 일어납니다.\n기본적으로는 mutable, growable string이 필요할 경우 String을, 그렇지 않을 땐 str을 사용하면 될 것 같아요.\n2. str vs. \u0026amp;str str 타입은 단독으로 쓰이는 경우는 거의 없습니다. 주로 \u0026amp;str 과 같은 reference 타입으로 사용됩니다.\n다음과 같이 str 타입을 사용하려 하면 compile error 가 발생합니다.\n1 let str: str = \u0026#34;hello world\u0026#34;; 컴파일 결과\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Compiling playground v0.0.1 (/playground) error[E0308]: mismatched types --\u0026gt; src/main.rs:2:20 | 2 | let str: str = \u0026#34;hello world\u0026#34;; | --- ^^^^^^^^^^^^^ expected `str`, found `\u0026amp;str` | | | expected due to this error[E0277]: the size for values of type `str` cannot be known at compilation time --\u0026gt; src/main.rs:2:9 | 2 | let str: str = \u0026#34;hello world\u0026#34;; | ^^^ doesn\u0026#39;t have a size known at compile-time | = help: the trait `Sized` is not implemented for `str` = note: all local variables must have a statically known size = help: unsized locals are gated as an unstable feature help: consider borrowing here | 2 | let str: \u0026amp;str = \u0026#34;hello world\u0026#34;; | + Some errors have detailed explanations: E0277, E0308. For more information about an error, try `rustc --explain E0277`. error: could not compile `playground` due to 2 previous errors 문제는 두 가지 입니다. 첫 번째는 대입하고자 하는 데이터인 \u0026ldquo;hello world\u0026quot;가 str가 아닌 \u0026amp;str 타입이라는 것 이고, 두 번째는 str 타입의 크기는 compile type에 알 수 없다는 것 입니다.\n\u0026ldquo;hello world\u0026quot;와 같은 string literal은 \u0026amp;str 타입입니다. 실행 파일의 text 영역에 하드코딩 된 문자열을 참조하는 형태기 때문입니다.\nstr 은 string slice로 DST(Dynamic Sized Type) 중 하나 입니다. DST는 compile time에 그 크기를 알 수 없는 타입으로, Slice, Trait 이 대표적인 예 입니다. 해당 타입들은 그냥 사용할 순 없고, Box를 사용하여 heap 에 위치시키거나, reference 로 Sized object를 가리키도록 하는 등의 방식으로 사용해야 합니다. Vec\u0026lt;dyn Trait\u0026gt;을 사용할 때 발생하는 컴파일 에러는 이전 포스트에서 살펴본 적 있습니다.\nstr은 reference 형태로 많이 사용되고, reference 대상은 Heap-allocated String, String literal 등이 됩니다.\n1 2 3 4 5 6 7 8 let heap_allocated_strings = String::from(\u0026#34;Hello from heap\u0026#34;); let str = \u0026#34;Hello from binary\u0026#34;; let ref_to_heap: \u0026amp;str = \u0026amp;heap_allocated_strings; let ref_to_literal: \u0026amp;str = \u0026amp;str; println!(\u0026#34;{}\u0026#34;, ref_to_heap); println!(\u0026#34;{}\u0026#34;, ref_to_literal); 실행결과\n1 2 Hello from heap Hello from binary String literal은 프로그램 실행 시간 전체에서 유효한 값으로, 그 lifetime은 static 입니다. 위의 예시 코드에서 string literal에 다음과 같이 lifetime을 명시할 수도 있습니다.\n1 2 let str = \u0026#34;Hello world from bin\u0026#34;; let ref_to_literal: \u0026amp;\u0026#39;static str = \u0026amp;str; 3. String vs. Box\u0026lt;str\u0026gt; DST인 str을 reference 형태가 아닌 Box 형태로 heap에 할당, 이를 가리키도록 할 수 있습니다. 이 경우, 애초에 heap에 메모리 할당받는 String 타입과 유사해 집니다.\n이를 위해 단순히 Box\u0026lt;str\u0026gt; 타입을 \u0026amp;str 타입으로부터 생성하려 하면 컴파일 에러가 발생합니다.\n1 2 let boxed_str: Box\u0026lt;str\u0026gt; = Box::new(\u0026#34;hello world\u0026#34;); println!(\u0026#34;boxed_str: {}\u0026#34;, boxed_str); 컴파일 결과\n1 2 3 4 5 6 7 8 9 10 11 12 13 error[E0308]: mismatched types --\u0026gt; src/main.rs:15:31 | 15 | let boxed_str: Box\u0026lt;str\u0026gt; = Box::new(\u0026#34;hello world\u0026#34;); | -------- ^^^^^^^^^^^^^^^^^^^^^^^ expected `Box\u0026lt;str\u0026gt;`, found `Box\u0026lt;\u0026amp;str\u0026gt;` | | | expected due to this | = note: expected struct `Box\u0026lt;str\u0026gt;` found struct `Box\u0026lt;\u0026amp;str\u0026gt;` For more information about this error, try `rustc --explain E0308`. error: could not compile `playground` due to previous error 대신 \u0026amp;str을 String 타입으로 변환(heap 영역 할당 후 복사)한 뒤 이를 into_boxed_str()로 변환할 수 있습니다.\n1 2 3 let string: Box\u0026lt;str\u0026gt; = String::from(\u0026#34;Hello\u0026#34;).into_boxed_str(); let string: Box\u0026lt;str\u0026gt; = \u0026#34;Hello\u0026#34;.to_string().into_boxed_str(); let string: Box\u0026lt;str\u0026gt; = Box::from(\u0026#34;Hello\u0026#34;); 세 번째 방식인 Box::from(\u0026amp;str) 타입 또한 내부적으로 heap에 메모리를 할당받고 값을 복사합니다. (참고: docs)\nThis conversion allocates on the heap and performs a copy of s.\nString은 mutable, growable 합니다. 값 변경/추가가 필요한 경우라면 Box\u0026lt;str\u0026gt;가 아닌 String을 사용해야 합니다.\nString과 Box\u0026lt;str\u0026gt;의 또 다른 차이점은 자료구조 형태입니다. String은 Vec\u0026lt;u8\u0026gt; 형태로 문자를 저장하고, Vec 는 dynamic array로 실제 데이터의 크기와 자료구조 크기(capacity)는 다를 수 있습니다. 따라서 String은 실제 데이터보다 많은 메모리를 사용할 수 있고, \u0026ldquo;데이터 포인터, 크기(len), 용량(capacity)\u0026ldquo;를 저장하므로 동일 문자열을 저장하더라도 포인터의 크기가 더 큽니다.\n다음과 같이 두 타입의 크기를 출력해 보면, Box\u0026lt;str\u0026gt;는 문자열 포인터와 문자열 길이를 포함하여 16Bytes, String은 capacity 까지 포함하여 24Bytes 임을 확인할 수 있습니다.\n1 2 3 println!(\u0026#34;{}\u0026#34;, core::mem::size_of::\u0026lt;usize\u0026gt;()); // 8 println!(\u0026#34;{}\u0026#34;, core::mem::size_of::\u0026lt;Box\u0026lt;str\u0026gt;\u0026gt;()); // 16 println!(\u0026#34;{}\u0026#34;, core::mem::size_of::\u0026lt;String\u0026gt;()); // 24 문서에 나와있는 것과 같이, String을 Box\u0026lt;str\u0026gt;로 변환 시 빈 공간 (남아 있는 capacity)는 drop 될 수 있습니다.\n| This will drop any excess capacity.\nString mutability 가 필요하지 않은 상황, memory footage가 중요한 상황에선 Box\u0026lt;str\u0026gt; 사용이 중요할 것 같아요.\n4. CString, CStr, OsString, OsStr 앞서 Rust의 문자열은 \u0026ldquo;null-terminated string\u0026quot;이 아니라고 하였는데, 이는 C와 다른 형태입니다. C와 같은 형태의 문자열은 std::ffi::CString, std::ffi::CStr 을 사용하여 표현할 수 있습니다. Rust 프로그램에서 이런 문자열을 사용할 필요는 없겠지만, C/C++ 코드와 FFI(Foreign Function Interface)로 연결할 때 필요합니다. CString의 into_string(), CStr의 to_str()를 통해 Rust 문자열로 변환할 수 있습니다. 이 때 invalid UTF-8 데이터가 포함되었다면 IntoStringError 발생합니다.\nOsString과 OsStr은 platform-native string 으로, 각 플랫폼(unix, windows 등)에 맞는 문자열 형태입니다. into_string() 함수를 통해 String으로 변환 가능하며, 이 때도 invalid UTF-8 데이터가 포함되어 있다면 변환 실패합니다. 이런 platform-native 문자열 타입이 정의되어 있고 String 변환 함수가 있어 코드 작성하기도 편리하고 안전한 코드를 짤 수 밖에 없게 만드는 것 같아요.\nCString과 OsString은 String의 대응, CStr과 OsStr 은 str의 대응입니다.\nString으로 변환 시 from_utf8_lossy() 함수를 사용하면, invalid UTF-8 글자는 U+FFFD REPLACEMENT CHARACTER (�)로 바꿔서 변환할 수도 있습니다.\n5. Char Rust의 Char 타입은 \u0026ldquo;unicode scalar value\u0026rdquo; 입니다. 이전 포스트에서 as를 통한 char type casting은 u8에 대해서만 가능한 걸 확인했는데, 이 때문이더라고요. 알고보니 너무 당연한 것..\nString literal 의 경우 \u0026quot;hello world\u0026quot; 와 같이 쌍따옴표로 표시하였는데요. 'A'와 같이 따옴표로 character literal를 정의할 수 있습니다. 이 값은 single Unicode character 입니다.\nString 타입과 str 타입은 chars() 함수를 통해 안전하게 문자열 내 Char들을 iterate 할 수 있습니다.\n6. Anti-patterns Rust의 문자열 타입에 대한 크게 생각하지 않고 무작정 사용하다 보면 컴파일 지옥에 빠져 헤매다 대충 이리저리 변환해서 사용하게 되는데요. 이런 것들이 모두 anti-pattern일지는 모르겠지만, 고민되는 내용들을 적어 봤습니다.\nmove, lifetime 이슈를 피하기 위한 clone\n다음과 같이 self 내에 정의된 String 타입을 계속해서 재활용할 때, move가 일어나지 않도록, 혹은 lifetime 이슈가 발생하지 않도록 clone을 할 수도 있습니다.\n1 2 3 4 5 6 fn do_something(\u0026amp;self, path: \u0026amp;String) { let new_dir = self.name.clone(); let path = Path::new(path.clone()); path.push(new_dir); std::fs::create_dir(\u0026amp;path); } 하지만 대부분의 경우 reference 값으로 원하는 결과를 만들 수 있습니다. 필요 이상으로 clone하지 않도록 노력하는 게 좋은 것 같습니다.\n1 2 3 4 5 fn do_something(\u0026amp;self, path: \u0026amp;String) { let path = Path::new(path); let path = path.join(\u0026amp;self.filename); std::fs::create_dir(\u0026amp;path); } \u0026amp;String 타입을 clone\n이 경우는 조금 헷갈리긴 하지만, reference를 clone하는 것은 직관적인 것 같진 않습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn create_something(name: \u0026amp;String) -\u0026gt; SomeStruct { SomeStruct { name: name.clone(), } } fn create_other(name: \u0026amp;String) -\u0026gt; OtherStruct { OtherStruct { name: name.clone(), } } fn main() { let my_name = \u0026#34;hailey\u0026#34;.to_string(); let some = create_something(\u0026amp;my_name); let other = create_other(\u0026amp;my_name); } 이 보다는 소유권을 가질 struct에게 ownership을 가진 변수를 넘겨주면 좋을 것 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fn create_something(name: String) -\u0026gt; SomeStruct { SomeStruct { name, } } fn create_other(name: String) -\u0026gt; OtherStruct { OtherStruct { name, } } fn main() { let my_name = \u0026#34;hailey\u0026#34;.to_string(); let some = create_something(my_name.clone()); let other = create_other(my_name.clone()); } 혹은 애초에 변하지 않는 값인 name은 \u0026amp;str 타입을 사용해도 좋을텐데, 이 경우 주어지는 \u0026amp;str 타입의 lifetime과 두 struct 의 lifetime을 잘 고려해야 합니다. 사용자 요청으로 받은 String에 대한 reference를 전달하고 이를 어떤 struct 멤버로 둔다면, 사용자 요청이 종료될 때 struct도 같이 소멸되는지 잘 확인해 봐야 할 것입니다. 그 전에 컴파일 지옥에 빠지겠지만요..\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 fn create_something(name: \u0026amp;str) -\u0026gt; SomeStruct { SomeStruct { name, // type is \u0026amp;str } } fn create_other(name: \u0026amp;str) -\u0026gt; OtherStruct { OtherStruct { name, // type is \u0026amp;str } } fn name_provided(my_name: \u0026amp;str) { let some = create_something(my_name); let other = create_other(my_name); } 애초에 하나의 값을 여러 struct 가 공유할 일을 만들지 않는 것이 더 좋을 것 같기는 합니다.\nString보다는 \u0026amp;str 사용하기\n그러고 싶은데 이게 잘 안 됩니다. 예를 들어 error message의 타입을 String 보다는 \u0026amp;str으로 정의하고 전달하고 싶은데요. 단순히 Invalid request 와 같은 메시지를 전달하는 거라면 \u0026amp;'static str 으로 정의할 수 있을텐데요. Error message에 여러 값을 포함시키고 싶다면 String으로 만들어야 합니다. 이를 as_str()으로 받은 \u0026amp;str 값을 전달할 순 있겠지만, 현재 함수에 정의한 임시 String변수를 참조하는 \u0026amp;str을 전달하므로 컴파일 되지 않습니다.\n이럴 경우 위에서 언급한 Box\u0026lt;str\u0026gt;을 사용할 수 있을 것 같은데, 어느게 더 좋은 패턴일까요..?\n분명 더 많은 anti-pattern이 있었던 것 같은데 오늘은 여기까지만 생각나는군요. 다음에 또 업데이트 하겠습니다 :)\n마무리하며 Rust 컴파일러를 통과하기 위한 코드를 짜다 보면 as_str(), to_string(), clone() 등을 남발하며 마음 한 켠이 불편했는데요. 이렇게 생각을 안하면 전체적인 코드의 구조, 의도도 불명확해지고, 언젠가는 지나친 memory copy overhead 로 인한 성능 문제로도 고생할 것 같습니다. 변수의 type을 정하는 것은 생각보다 어려운 일인 것 같아요.\n","permalink":"http://huijeong-kim.github.io/post/2023-05-07-exploring-rust-strings/","summary":"안녕하세요.\n오늘은 Rust의 문자열 개념을 간단하게 알아보고, 코드 작성 시 문자열 관련 혼란스러웠던 포인트들을 정리해 보고자 합니다.\n1. String vs. str String 과 str 는 모두 valid UTF-8 문자열을 나타냅니다. Invalid UTF-8 데이터로 String 타입을 생성할 수 없습니다. UTF-8, Unicode 등에 대한 설명은 생략합니다.\nRust에서는 C와 같이 null-terminating string 개념을 사용하지 않습니다. 대신 String 타입은 문자열과 그 길이를 갖고 있는 \u0026ldquo;fat pointer\u0026rdquo; 입니다. \u0026ldquo;fat pointer\u0026quot;는 raw pointer과 additional metadata (e.g., length)를 갖고 있는 포인터를 의미합니다.","title":"Exploring Rust Strings"},{"content":"안녕하세요.\n오늘은 Rust의 as 를 사용한 type casting에 대해 알아보고자 합니다. C++ 프로그램에서는 잘못된 type 사용 으로 인한 오류를 종종 볼 수 있습니다. uint64_t 값을 uint32_t 값에 대입하여 잘못된 값으로 동작하는 경우가 그 예입니다. 조금은 어이 없는 실수이긴 한데, 생각보다 자주 발견됩니다. 이런 류의 버그는 처음 봤을 때 원인을 가늠하기 힘들기도 하지만, 고치기 귀찮거나 어렵기도 합니다. Type 재정의를 사용하지 않는 경우도 많고, 가끔씩은 통일할 필요가 크게 없는 경우도 있고, 의미 상 같은 변수를 모두 찾아내기 힘든 코드들도 종종 있습니다. 테스트를 더 잘 하면 된다고 하지만, 모든 변수에 대해 boundary test를 하는 건 현실적으로 조금 어렵습니다.\nRust에서는 엄격하게 type 검사를 합니다. Rust로 작성할 땐 u64를 u32에 대입할 수 없습니다. into() 혹은 try_into() 등을 통해 type conversion하여야 하고, 적절하지 못한 type 변환 시도는 컴파일 타임에 발견되거나 런타임에 에러 리턴이 됩니다. 그래서 Rust 사용 시에는 type 관련해서는 매우 안심하고 있었는데, 생각치 못한 예외가 있더라고요. 바로 as를 사용한 type casting 입니다.\nas는 Rust의 type cast operator 입니다. Reference book와 std reference를 참고하여 그 용도를 정리하고, type casting과 type conversion 으로 막을 수 있는/없는 invalid casting을 살펴보겠습니다.\nType casting as는 primitive 의 type casting에 사용됩니다. Type을 강제 할당하는 것이기 때문에, 런타임 casting 실패는 존재하지 않습니다. 하지만 casting이 가능한 케이스는 한정되어 있습니다. 그 외의 경우는 compile time에 invalid casting으로 실패합니다.\nNumeric cast 동일 size integer: no-op smaller integer -\u0026gt; larger integer: zero/sign-extend larger integer -\u0026gt; smaller integer: truncate (!!) 제가 최근에 습관적으로 as를 사용하였는데, 이 경우 특별한 경고(compile error/warning 등) 없이 값을 잃을 수 있더라고요. as는 type 변환이 아닌 casting을 해 주는 것 이기 때문에 사용에 주의해야 할 것 같습니다.\n1 2 3 let number: u64 = u64::MAX; let cast_as = number as u32; println!(\u0026#34;Numbers: {:x}, {:x}\u0026#34;, number, cast_as); 실행 결과\n1 Numbers: ffffffffffffffff, ffffffff Numeric casting 하고 싶을 때는 as를 사용한 casting 대신 into() 혹은 try_into()를 사용하여 type conversion을 하는 것이 좋겠습니다. 위와 같은 예제에서 into()를 사용하면, u32에 대한 From\u0026lt;u64\u0026gt;는 구현되어 있지 않다는 이유로 compile error가 발생합니다. 물론 직접 into() 함수를 구현할 수도 있겠지만 굳이 그럴 필욘 없겠지요. try_into()를 사용한다면 결과값은 TryFromIntError 에러가 됩니다.\n1 2 3 4 5 // error[E0277]: the trait bound `u32: From\u0026lt;u64\u0026gt;` is not satisfied //let cast_into: u32 = number.into(); let cast_into: Result\u0026lt;u32, _\u0026gt; = number.try_into(); println!(\u0026#34;Conversion result: {:?}\u0026#34;, cast_into); 실행 결과\n1 Conversion result: Err(TryFromIntError(())) Enum cast Enum의 numeric casting 도 가능합니다. 단,\nUnit-only enums Field-less enums (without explicit discriminants) 만 가능합니다. Field-less enum은 \u0026ldquo;no constructors contain fields\u0026rdquo; 인 enum 입니다. Tuple, Struct 와 같은 enum 값이 있더라도 그 안에 field가 없는 경우를 말하는 것 같습니다. 이 경우에는 explicit discriminants(=3 과 같은 값 지정)가 없어야만 numeric casting 가능합니다.\n1 2 3 4 5 6 7 8 9 10 11 #[derive(Debug)] enum FieldOnlyEnum { Tuple(), Struct{}, Unit, } println!(\u0026#34;FieldOnlyEnum: Tuple {:?}, Struct {:?}, Unit {}\u0026#34;, FieldOnlyEnum::Tuple() as u8, FieldOnlyEnum::Struct{} as u32, FieldOnlyEnum::Unit as u8); 실행 결과\n1 FieldOnlyEnum: Tuple 0, Struct 1, Unit 2 여기에 FieldOnlyEnum::Unit에 Unit = 1,과 같이 명시적으로 값을 지정하면 \u0026ldquo;discriminant value 1 assigned more than once\u0026rdquo; 에러가 발생합니다. 하지만 enum에 #[repr(u8)] 지정할 경우 explicit discriminant가 있는 field-less enum에서도 numeric casting 가능하다고 합니다. 이 때 tuple type이나 struct type에 discriminant value 지정하는 경우에는 *\u0026ldquo;non-primitive cast\u0026rdquo;*로 compile error 발생합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 #[derive(Debug)] #[repr(u8)] enum FieldOnlyEnum2 { OtherUnit = 3, Tuple(), Struct{}, Unit, } println!(\u0026#34;FieldOnlyEnum2: Tuple {:?}, Struct {:?}, Unit {}\u0026#34;, FieldOnlyEnum2::Tuple() as u8, FieldOnlyEnum2::Struct{} as u32, FieldOnlyEnum2::Unit as u8); 실행 결과\n1 FieldOnlyEnum2: Tuple 4, Struct 5, Unit 6 여기서 또 한 가지 신기한 점은, casting 시 FieldOnlyEnum::Struct as u8 와 같이 작성하는 경우(생성자 사용 X)엔 compile error 가 발생하고, FieldOnlyEnum::Tuple as u8 로 작성하면 빌드는 성공하나 값이 이상하게 FieldOnlyEnum: Tuple 224, Struct 1, Unit 2 로 나오더라고요. 혼란스럽지만 이런 enum type casting이 얼마나 필요할 지 모르겠으니 그냥 넘어가 보겠습니다..\nUnit-only enum은 field-less enum 중 unit으로만 이루어진 경우입니다. 이 때는 Field-only enum과 다르게 *\u0026ldquo;explicit discriminant value\u0026rdquo;*를 갖고 있어도 상관 없습니다. 아래 코드에서 주석 처리 된 것 처럼 UnitNum3이 i32란 field를 갖고 있는 경우(unit-only enum이 아닌 경우), 모든 type casting에서 에러가 발생합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #[derive(Debug)] enum UnitOnlyEnum1 { UnitNum, UnitNum2, UnitNum3, //UnitNum3(i32), =\u0026gt; error[E0605]: non-primitive cast: `UnitOnlyEnum1` as `u32` } println!(\u0026#34;UnitOnlyEnum: UnitNum {}, UnitNum2 {} UnitNum3 {}\u0026#34;, UnitOnlyEnum1::UnitNum as u8, UnitOnlyEnum1::UnitNum2 as u16, UnitOnlyEnum1::UnitNum3 as u32, ); enum UnitOnlyEnum2 { UnitNum = 1, UnitNum2 = 4, } println!(\u0026#34;UnitOnlyEnum2: UnitNum {}, UnitNum2 {}\u0026#34;, UnitOnlyEnum2::UnitNum as u32, UnitOnlyEnum2::UnitNum2 as u32, ); 실행 결과\n1 2 UnitOnlyEnum: UnitNum 0, UnitNum2 1, UnitNum3 2 UnitOnlyEnum2: UnitNum 1, UnitNum2 4 Other primitives false -\u0026gt; 0, true -\u0026gt; 1 char -\u0026gt; numerics: 해당 char code numerics -\u0026gt; char: 해당하는 char code의 char numerics -\u0026gt; char 변환은 u8만 가능합니다. 그보다 큰 값을 가진 emoji 들은 try_into나 into를 사용해 type conversion 하면 되겠습니다.\n업데이트 (23.5.7) char 타입은 \u0026ldquo;Unicode scalar value\u0026rdquo; 를 의미하며, 1B로 표현 됩니다. Unicode 글자만이 char로 type casting 가능하다고 보면 될 것 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 println!(\u0026#34;False as u8: {}\u0026#34;, false as u8); println!(\u0026#34;True as u8: {}\u0026#34;, true as u8); println!(\u0026#34;\u0026#39;C\u0026#39; as u8: {}\u0026#34;, \u0026#39;C\u0026#39; as u8); println!(\u0026#34;67 as char: {}\u0026#34;, 67 as char); println!(\u0026#34;\u0026#39;❤️\u0026#39; as u32: {}\u0026#34;, \u0026#39;❤\u0026#39; as u32); // error: only `u8` can be cast into `char` //println!(\u0026#34;10084 as char: {}\u0026#34;, 10084 as char); //error[E0604]: only `u8` can be cast as `char`, not `u32` //println!(\u0026#34;10084 as char: {}\u0026#34;, 10084u32 as char); let result: Result\u0026lt;char, _\u0026gt; = (10084 as u32).try_into(); println!(\u0026#34;Converting result: {:?}\u0026#34;, result); // error[E0277]: the trait bound `char: From\u0026lt;u32\u0026gt;` is not satisfied // let result: char = (10084 as u32).into(); // println!(\u0026#34;Converting result: {:?}\u0026#34;, result); 실행 결과\n1 2 3 4 5 6 False as u8: 0 True as u8: 1 \u0026#39;C\u0026#39; as u8: 67 67 as char: C \u0026#39;❤️\u0026#39; as u32: 10084 Converting result: Ok(\u0026#39;❤\u0026#39;) pointer to address cast의 경우 예상하는 바와 유사한 것 같습니다. casting 되는 type에 따라 값이 truncate 되어 잘못된 주소를 반환할 수 있으니 주의해야 합니다. usize 로 casting 하거나, 다음과 같이 _를 사용하여 컴파일러가 추론하게 만들 수도 있습니다.\n1 2 3 let num = 42; let address = \u0026amp;num as *const _; println!(\u0026#34;The address of num is {:?}\u0026#34;, address); 실행 결과\n1 The address of num is 0x7fffd6002dfc Type coercions Type 을 강제할 때 사용합니다. 흔히 사용하는 literal 에 type 지정하는 경우가 그 예입니다. 이 때 잘못된 casting을 하면 (e.g., 너무 큰 값을 u8로 casting) compile error가 발생합니다.\n1 2 3 4 5 6 let number_coercions = 123 as u8; // error: literal out of range for `u8` //let number_coercions2 = 10002 as u8; println!(\u0026#34;numbers: {}\u0026#34;, number_coercions); 실행 결과\n1 numbers: 123 또 다른 예는 Trait 으로의 변환입니다. 다음과 같이 \u0026amp;dyn Animal trait 으로 casting 하여 해당 trait의 함수를 사용할 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 use std::any::Any; trait Animal { fn speak(\u0026amp;self); } #[derive(Debug)] struct Dog { name: String, } impl Animal for Dog { fn speak(\u0026amp;self) { println!(\u0026#34;{} says woof!\u0026#34;, self.name); } } fn trait_coercions() { let dog = Dog { name: String::from(\u0026#34;Happy\u0026#34;) }; let animal = \u0026amp;dog as \u0026amp;dyn Animal; animal.speak(); } 실행 결과\n1 Happy says woof! 근데 casting 없이 그냥 호출해도 되고, 다음과 같이 함수 인자로 전달하면 자연스럽게(암묵적으로) casting 되는데, 위와 같이 as Trait 을 사용하는 경우가 어떤 경우인지 감이 오질 않네요.\n1 2 3 4 5 6 7 8 fn trait_coercions() { let dog = Dog { name: String::from(\u0026#34;Happy\u0026#34;) }; let_animal_speak(dog); } fn let_animal_speak(animal: \u0026amp;dyn Animal) { animal.speak(); } 실행 결과\n1 Happy says woof! 이 보다는 Trait을 downcasting를 하고 싶은 경우가 더 많을 것 같습니다. 꼭 downcasting을 해야 하겠느냐고 하면 할 말이 없지만(다른 방식으로 해결하는 게 좋은 경우가 더 많지만) 그래도 현실적으로 필요할 때가 종종 있습니다. 이 땐 std::any::Any::downcast_ref를 사용할 수 있습니다(참고). 이 과정에서 animal as \u0026amp;dyn Any와 같이 casting 하면 downcast가 정상 동작하지 않는 등 예상대로 동작하지 않는데요. as_any() 의 신비는 다음에 기회 되면 알아보고 정리해 보겠습니다.\n다음 예제에서 Dog, Cat은 Animal trait을 구현하고 있고, Desk는 구현하고 있지 않습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 trait Animal { fn speak(\u0026amp;self); fn as_any(\u0026amp;self) -\u0026gt; \u0026amp;dyn Any; } // .. 생략 .. impl Animal for Dog { // .. 생략 .. fn as_any(\u0026amp;self) -\u0026gt; \u0026amp;dyn Any { self } } // .. 생략 .. let animal_vec: Vec\u0026lt;Box\u0026lt;dyn Animal\u0026gt;\u0026gt; = vec![ Box::new(Dog { name: String::from(\u0026#34;Fido\u0026#34;) }), Box::new(Cat { name: String::from(\u0026#34;Whiskers\u0026#34;) }), ]; for animal in animal_vec { animal.speak(); if let Some(dog) = animal.as_any().downcast_ref::\u0026lt;Dog\u0026gt;() { println!(\u0026#34;animal\u0026#39;s name is {}\u0026#34;, dog.name); } } let desk = Desk { name: String::from(\u0026#34;birch\u0026#34;) }; let desk_as_any = \u0026amp;desk as \u0026amp;dyn Any; let is_dog = desk_as_any.downcast_ref::\u0026lt;Dog\u0026gt;(); println!(\u0026#34;desk is dog?: {:?}\u0026#34;, is_dog); 실행 결과\n1 2 3 4 Fido says woof! animal\u0026#39;s name is Fido Whiskers says meow! desk is dog?: None Rename imports 마지막으로 C++ 의 namespace renaming과 유사한 rename imports 가 있는데, 이는 type casting이 아니므로 생략합니다.\n결론 Rust 의 type casting은 C++ 대비 매우 제한적이고, 정의되지 않은 casting은 invalid casting으로 처리되는 것 같습니다. 하지만 이 기준을 Reference Book만 보고 파악하기는 조금 어렵더라고요.\n이번 편을 작성하다 보니, Type Casting이 꼭 필요한 경우는 사실 없지 않을까 싶었습니다.(Type Casting이 유용한 예가 있다면 공유해 주시길!!) 오늘도 조금 뻔한 결론, type casting 보다는 type conversion을 사용하여 암묵적인 변환으로 인해 발생할 수 있는 버그를 피하자로 마무리 하겠습니다.\n","permalink":"http://huijeong-kim.github.io/post/2023-05-02-rust-as-operator/","summary":"안녕하세요.\n오늘은 Rust의 as 를 사용한 type casting에 대해 알아보고자 합니다. C++ 프로그램에서는 잘못된 type 사용 으로 인한 오류를 종종 볼 수 있습니다. uint64_t 값을 uint32_t 값에 대입하여 잘못된 값으로 동작하는 경우가 그 예입니다. 조금은 어이 없는 실수이긴 한데, 생각보다 자주 발견됩니다. 이런 류의 버그는 처음 봤을 때 원인을 가늠하기 힘들기도 하지만, 고치기 귀찮거나 어렵기도 합니다. Type 재정의를 사용하지 않는 경우도 많고, 가끔씩은 통일할 필요가 크게 없는 경우도 있고, 의미 상 같은 변수를 모두 찾아내기 힘든 코드들도 종종 있습니다.","title":"as operator in Rust"},{"content":"안녕하세요. 오늘은 rust에서 polymorphism을 활용하는 방법을 정리해 보고자 합니다.\ncpp 코드를 작성하던 습관 대로 Rust 코드를 작성하다 보면 막히는 부분 중 하나가 interface 클래스(pure virtual class)와 이를 통한 객체 전달 부분입니다. 단순히 trait으로 변환하여 코드를 작성하다 보면 쉽게 컴파일 에러 지옥에 빠지곤 하는데요..\n아주 간단한 composite design pattern 예제를 구현해 보면서 rust의 polymorphism 에 대해 알아보도록 하겠습니다. Composite pattern 예제로 많이 사용되는 File, Directory 구조를 표현해 보고자 합니다. File은 이름과 크기를 갖고 있는 객체로, Directory는 이름과 하위 파일 및 디렉토리를 갖는 객체로, 그리고 File, Directory 는 모두 Node 라는 interface를 구현하게 만들고자 합니다.\nFile, Directory가 공통으로 구현하고 있는 Interface의 각 함수는 다음과 같은 의미를 갖고 있습니다.\nget_name: 파일 혹은 디렉토리의 이름 반환. get_size: 파일의 크기를 반환. 디렉토리의 경우, 갖고 있는 모든 파일 혹은 디렉토리 크기의 합을 반환해야 함. Trait을 사용한 Composite Pattern 구현 아주 나이브하게는 interface를 trait으로 1:1 변환하여 다음과 같이 구현할 수 있을 것 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 fn main() { let mut root_node = Directory::new(\u0026#34;root\u0026#34;.to_string()); let new_file = File::new(\u0026#34;new_file\u0026#34;.to_string(), 1024 * 1024); let new_directory = Directory::new(\u0026#34;my_folder\u0026#34;.to_string()); root_node.add_new_node(new_file); root_node.add_new_node(new_directory); assert_eq!(root_node.get_size(), 1024 * 1024); } trait Node { fn get_name(\u0026amp;self) -\u0026gt; String; fn get_size(\u0026amp;self) -\u0026gt; u64; } struct File { name: String, size: u64, } impl Node for File { fn get_name(\u0026amp;self) -\u0026gt; String { self.name.clone() } fn get_size(\u0026amp;self) -\u0026gt; u64 { self.size } } impl File { pub fn new(name: String, size: u64) -\u0026gt; Self { Self { name, size, } } } struct Directory { name: String, childs: Vec\u0026lt;dyn Node\u0026gt;, } impl Node for Directory { fn get_name(\u0026amp;self) -\u0026gt; String { self.name.clone() } fn get_size(\u0026amp;self) -\u0026gt; u64 { self.childs.iter().fold(0, |acc, x| acc + x.get_size()) } } impl Directory { pub fn new(name: String) -\u0026gt; Self { Self { name, childs: Vec::new(), } } pub fn add_new_node(\u0026amp;mut self, node: dyn Node) { self.childs.push(node); } } 이 코드는 사실 빌드 되지는 않습니다. 이유는 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 error[E0277]: the size for values of type `(dyn Node + \u0026#39;static)` cannot be known at compilation time --\u0026gt; src/bin/composite.rs:42:13 | 42 | childs: Vec\u0026lt;dyn Node\u0026gt;, | ^^^^^^^^^^^^^ doesn\u0026#39;t have a size known at compile-time | = help: the trait `Sized` is not implemented for `(dyn Node + \u0026#39;static)` note: required by a bound in `Vec` --\u0026gt; /Users/huijeongkim/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:400:16 | 400 | pub struct Vec\u0026lt;T, #[unstable(feature = \u0026#34;allocator_api\u0026#34;, issue = \u0026#34;32838\u0026#34;)] A: Allocator = Global\u0026gt; { | ^ required by this bound in `Vec` For more information about this error, try `rustc --explain E0277`. dyn Node는 compile time에 크기를 알 수 없기 때문에 Vector에 넣을 수 없다는 것 입니다. 이를 아주 간단하게 해결할 수 있는 방법은, 객체 자체가 아닌 객체를 가리키는 포인터를 Vector에 넣는 것 입니다. dyn Node가 아닌 Heap으로 옮겨 진 Box\u0026lt;dyn Node\u0026gt;의 리스트를 갖도록 수정하면 에러는 사라지고 컴파일 성공하게 됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // main 함수 수정 root_node.add_new_node(Box::new(new_file)); root_node.add_new_node(Box::new(new_directory)); // Directory 수정 struct Directory { name: String, childs: Vec\u0026lt;Box\u0026lt;dyn Node\u0026gt;\u0026gt;, } impl Directory { pub fn add_new_node(\u0026amp;mut self, node: Box\u0026lt;dyn Node\u0026gt;) { self.childs.push(node); } } Trait Clone 여기서 File, Directory 를 clone 하도록 수정해 보겠습니다. #[derive(Clone)] 매크로를 사용하면 손쉽게 Clone trait을 구현할 수 있습니다. 하지만 여기서 다시 한 번 컴파일 에러를 만나게 됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 error[E0277]: the trait bound `dyn Node: Clone` is not satisfied --\u0026gt; src/bin/composite.rs:51:5 | 48 | #[derive(Clone)] | ----- in this derive macro expansion ... 51 | childs: Vec\u0026lt;Box\u0026lt;dyn Node\u0026gt;\u0026gt;, | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Clone` is not implemented for `dyn Node` | = note: required because of the requirements on the impl of `Clone` for `Box\u0026lt;dyn Node\u0026gt;` = note: 1 redundant requirement hidden = note: required because of the requirements on the impl of `Clone` for `Vec\u0026lt;Box\u0026lt;dyn Node\u0026gt;\u0026gt;` = note: this error originates in the derive macro `Clone` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`. dyn Node에 대한 clone 이 구현되어 있지 않다는 것 입니다. Directory가 clone 가능하려면 그 내부의 모든 멤버도 clone 가능해야 합니다. 따라서 dyn Node의 clone을 구현해 줘야 합니다.\nderive 매크로는 struct, enum, union에만 사용 가능하므로 trait Node에는 #[derive(Clone)]을 추가할 수 없습니다. 대신 다음과 같이 구현할 수 있습니다. 참고로 clone 함수의 리턴이 dyn Node가 되면 첫 번째 에러와 동일한 이유, compile time에 size를 알 수 없다는 문제로 에러가 발생하기 때문에 이 또한 Box 로 clone하도록 구현하였고, 이 때문에 Box\u0026lt;dyn Node\u0026gt;에 대한 clone 구현도 추가하였습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // Box\u0026lt;dyn Node\u0026gt; 에 대한 clone을 추가 trait Node : CloneNode { fn get_name(\u0026amp;self) -\u0026gt; String; fn get_size(\u0026amp;self) -\u0026gt; u64; } trait CloneNode { fn clone_box(\u0026amp;self) -\u0026gt; Box\u0026lt;dyn Node\u0026gt;; } impl Clone for Box\u0026lt;dyn Node\u0026gt; { fn clone(\u0026amp;self) -\u0026gt; Self { self.clone_box() } } // File에 CloneNode 추가 #[derive(Clone)] struct File { name: String, size: u64, } impl CloneNode for File { fn clone_box(\u0026amp;self) -\u0026gt; Box\u0026lt;dyn Node\u0026gt; { Box::new(self.clone()) } } // Directory에 CloneNode 추가 #[derive(Clone)] struct Directory { name: String, childs: Vec\u0026lt;Box\u0026lt;dyn Node\u0026gt;\u0026gt;, } impl CloneNode for Directory { fn clone_box(\u0026amp;self) -\u0026gt; Box\u0026lt;dyn Node\u0026gt; { Box::new(self.clone()) } } clone_box 함수를 만드는 패턴은 생각보다 자주 반복 사용되어 boilerplate 코드가 되기도 합니다. 이럴 때 dyn_clone crate를 사용하면 이 코드들을 제거할 수 있습니다.\n1 2 3 4 5 6 use dyn_clone::DynClone; trait Node : DynClone { fn get_name(\u0026amp;self) -\u0026gt; String; fn get_size(\u0026amp;self) -\u0026gt; u64; } dyn_clone::clone_trait_object!(Node); 이렇게 수정하면 위에서 구현했던 CloneNode trait이나 clone_box 구현을 생략할 수 있습니다.\nGeneric을 활용하기 Rust 에서 polymorphism을 활용하는 또 다른 방법은 Generic을 사용하는 것 입니다. Directory에 적용해 보면 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #[derive(Clone)] struct Directory\u0026lt;N: Node\u0026gt; { name: String, childs: Vec\u0026lt;Box\u0026lt;N\u0026gt;\u0026gt;, } impl\u0026lt;N: Node + Clone\u0026gt; Node for Directory\u0026lt;N\u0026gt; { fn get_name(\u0026amp;self) -\u0026gt; String { self.name.clone() } fn get_size(\u0026amp;self) -\u0026gt; u64 { self.childs.iter().fold(0, |acc, x| acc + x.get_size()) } } impl\u0026lt;N: Node\u0026gt; Directory\u0026lt;N\u0026gt; { pub fn new(name: String) -\u0026gt; Self { Self { name, childs: Vec::new(), } } pub fn add_new_node(\u0026amp;mut self, node: Box\u0026lt;N\u0026gt;) { self.childs.push(node); } } 이렇게 작성했을 때의 문제는 N: Node가 File 혹은 Directory 중 하나만 될 수 있다는 것 입니다. main 함수에서 다음과 같이 add_new_node를 File, Directory 에 대해 호출한다면,\n1 2 root_node.add_new_node(Box::new(new_file)); root_node.add_new_node(Box::new(new_directory)); 다음과 같이 컴파일 에러가 발생하게 됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 error[E0308]: mismatched types --\u0026gt; src/bin/composite.rs:10:37 | 10 | root_node.add_new_node(Box::new(new_directory)); | -------- ^^^^^^^^^^^^^ expected struct `File`, found struct `Directory` | | | arguments to this function are incorrect | = note: expected struct `File` found struct `Directory\u0026lt;_\u0026gt;` note: associated function defined here --\u0026gt; /Users/huijeongkim/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/alloc/src/boxed.rs:213:12 | 213 | pub fn new(x: T) -\u0026gt; Self { | ^^^ For more information about this error, try `rustc --explain E0308`. 구체 객체가 run-time에 결정되어야 하는 이런 예제에서는 generic은 적합하지 않은 것이죠. 이 문제를 해결하기 위해서 enum을 사용해 볼 수 있겠습니다.\nEnum으로 감싸기 Rust 의 enum 또한 polymorphism을 활용할 수 있는 방법 중 하나입니다. 다음과 같이 trait의 구현체들을 enum value로 넣어줄 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // enum을 하나 추가하고 #[derive(Clone)] enum NodeType { FileNode(File), DirectoryNode(Directory), } // Directory는 boxed trait 대신 enum을 갖도록 수정, #[derive(Clone)] struct Directory { name: String, childs: Vec\u0026lt;NodeType\u0026gt;, } // main 함수에서는 다음과 같이 node를 추가합니다. root_node.add_new_node(NodeType::FileNode(new_file)); root_node.add_new_node(NodeType::DirectoryNode(new_directory)); 이 떄 enum 내의 value를 꺼내서 trait 함수를 부르기 번거로우므로, enum에도 Node trait을 구현해 줍니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 impl Node for NodeType { fn get_name(\u0026amp;self) -\u0026gt; String { match \u0026amp;self { NodeType::FileNode(f) =\u0026gt; f.get_name(), NodeType::DirectoryNode(d) =\u0026gt; d.get_name(), } } fn get_size(\u0026amp;self) -\u0026gt; u64 { match \u0026amp;self { NodeType::FileNode(f) =\u0026gt; f.get_size(), NodeType::DirectoryNode(d) =\u0026gt; d.get_size(), } } } 이런 패턴 또한 굉장히 많이 사용되는 것 같습니다. 이 코드도 enum_dispatch crate를 사용하면 단순화할 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 use enum_dispatch::enum_dispatch; #[enum_dispatch] trait Node : DynClone { fn get_name(\u0026amp;self) -\u0026gt; String; fn get_size(\u0026amp;self) -\u0026gt; u64; } dyn_clone::clone_trait_object!(Node); #[derive(Clone)] #[enum_dispatch(Node)] enum NodeType { FileNode(File), DirectoryNode(Directory), } 최종 코드 여태까지 수정한 내용을 모두 반영한 최종 코드는 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 fn main() { let mut root_node = Directory::new(\u0026#34;root\u0026#34;.to_string()); let new_file = File::new(\u0026#34;new_file\u0026#34;.to_string(), 1024 * 1024); let new_directory = Directory::new(\u0026#34;my_folder\u0026#34;.to_string()); root_node.add_new_node(NodeType::FileNode(new_file)); root_node.add_new_node(NodeType::DirectoryNode(new_directory)); assert_eq!(root_node.get_size(), 1024 * 1024); } use dyn_clone::DynClone; use enum_dispatch::enum_dispatch; #[enum_dispatch] trait Node : DynClone { fn get_name(\u0026amp;self) -\u0026gt; String; fn get_size(\u0026amp;self) -\u0026gt; u64; } dyn_clone::clone_trait_object!(Node); #[derive(Clone)] #[enum_dispatch(Node)] enum NodeType { FileNode(File), DirectoryNode(Directory), } #[derive(Clone)] struct File { name: String, size: u64, } impl Node for File { fn get_name(\u0026amp;self) -\u0026gt; String { self.name.clone() } fn get_size(\u0026amp;self) -\u0026gt; u64 { self.size } } impl File { pub fn new(name: String, size: u64) -\u0026gt; Self { Self { name, size, } } } #[derive(Clone)] struct Directory { name: String, childs: Vec\u0026lt;NodeType\u0026gt;, } impl Node for Directory { fn get_name(\u0026amp;self) -\u0026gt; String { self.name.clone() } fn get_size(\u0026amp;self) -\u0026gt; u64 { self.childs.iter().fold(0, |acc, x| acc + x.get_size()) } } impl Directory { pub fn new(name: String) -\u0026gt; Self { Self { name, childs: Vec::new(), } } pub fn add_new_node(\u0026amp;mut self, node: NodeType) { self.childs.push(node); } } 이렇게 아주 간단한 composite pattern 예제를 rust로 구현해 봤습니다. 매 번 헷갈리던 거여서 정리해 봤는데 이젠 그만 헷갈리길\u0026hellip;\n","permalink":"http://huijeong-kim.github.io/post/2023-01-29-rust-polymorphism/","summary":"안녕하세요. 오늘은 rust에서 polymorphism을 활용하는 방법을 정리해 보고자 합니다.\ncpp 코드를 작성하던 습관 대로 Rust 코드를 작성하다 보면 막히는 부분 중 하나가 interface 클래스(pure virtual class)와 이를 통한 객체 전달 부분입니다. 단순히 trait으로 변환하여 코드를 작성하다 보면 쉽게 컴파일 에러 지옥에 빠지곤 하는데요..\n아주 간단한 composite design pattern 예제를 구현해 보면서 rust의 polymorphism 에 대해 알아보도록 하겠습니다. Composite pattern 예제로 많이 사용되는 File, Directory 구조를 표현해 보고자 합니다. File은 이름과 크기를 갖고 있는 객체로, Directory는 이름과 하위 파일 및 디렉토리를 갖는 객체로, 그리고 File, Directory 는 모두 Node 라는 interface를 구현하게 만들고자 합니다.","title":"Polymorphism in Rust"},{"content":"오늘은 Rust에서 제공하는 Asynchronous Programming 관련 feature들에 대해 정리하면서, async 관련 포스트를 쓸 때 마다 사용되는 단어들, async, future, runtime, executor에 대해 정리해 보겠습니다. 오늘은 유독 내용이 추상적인 느낌에 부족한 부분이 많은 것 같은데, 틀린 부분이나 부족한 부분이 있다면 코멘트 남겨 주세요 :)\nAsynchronous Programming Async book에는 Asynchronous programming이 다음과 같이 정의되어 있습니다.\nAsynchronous programming, or async for short, is a concurrent programming model supported by an increasing number of programming languages. It lets you run a large number of concurrent tasks on a small number of OS threads, while preserving much of the look and feel of ordinary synchronous programming, through the async/await syntax.\n중요 포인트는 두 가지 일 것 같습니다.\nOS thread 수와 관계 없이 task들을 concurrent하게 실행시킬 수 있게 하는 프로그래밍 모델이다 async/await syntax를 사용하여 기존의 synchronous programming과 비슷한 look \u0026amp; feel을 준다 Concurrent Execution Application의 task를 concurrent하게 실행하는 방법으로 가장 먼저 multi-threading을 떠올릴 수 있습니다. 가장 단순하게는, 병렬 실행할 task 마다 새로운 thread를 생성하고 그 thread에서 task를 실행시켜볼 수 있을 것입니다. std::thread를 사용하면 native OS thread를 생성 및 시작할 수 있습니다.\n하지만 OS thread 를 사용하여 task를 병렬 실행하면 OS 의 제약 내에서 thread를 사용해야 합니다. 단일 process 당 만들 수 있는 최대 thread 수가 제한되어 있고, thread 생성 시 발생하는 overhead, thread pool 관리 등 고려 해야 할 점이 많을 것 입니다.\n많은 프로그래밍 언어에서는 Green Thread와 같은 OS thread 위에서 동작하는 virtual thread와 이 virtual thread를 실행시키는 환경, Runtime을 제공합니다. 이를 사용하면 OS thread의 제약 없이 task parallelism을 구현할 수 있고 개발자가 직접 코드에서 thread를 다루지 않아도 됩니다.\n하지만 Rust의 경우, Green Thread 혹은 Built-in Runtime을 제공하지 않습니다. Async/Await syntax만 제공합니다. 이는 의도적인 디자인으로, 어플리케이션의 workload 에 따라 적합한(성능이 좋은) Runtime은 다를 수 있기 때문 입니다. Rust에는 다양한 Runtime Library들이 존재하고, 개발자는 이 중 application에 적합한 Runtime을 선택하여 async task를 실행시킬 수 있습니다.\nAsync/Await Rust 언어에서 제공하는 Async/await syntax에 대해 간단히 알아보고, 이 것이 Runtime에서 실행 되는 방식을 이어서 알아 보겠습니다.\nThe Book에서 정의한 관련 키워드들은 다음과 같습니다.\nasync - return a Future instead of blocking the current thread await - suspend execution until the result of a Future is ready async는 Future를 리턴하는 asynchronous 동작(현재 thread를 blocking하지 않는 동작)을 의미하고, Future는 Runtime에서 실행될 수 있는 trait 입니다. async 키워드를 사용해 function, block expression, closure를 Future로 만들 수 있습니다. Future trait이 Runtime에서 어떻게 사용 되는지는 뒤에서 간단히 설명하겠습니다.\n다음은 async func, async block, async closure를 만들어 실행시키는 간단한 코드입니다. 기존의 function, block, closure에 async 키워드를 추가하였고, 이를 실행시킬 Runtime으로는 futures crate를 사용했습니다. 각 Future들은 futures::executor::block_on에 의해 실행됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 use futures::executor; async fn async_func() { println!(\u0026#34;async_func\u0026#34;); } fn main() { let future1 = async_func(); let future2 = async { println!(\u0026#34;async block\u0026#34;); }; let async_closure = || async { println!(\u0026#34;async closure\u0026#34;); }; let future3 = async_closure(); executor::block_on(future3); executor::block_on(future2); executor::block_on(future1); } 출력 결과\n1 2 3 async closure async block async_func await는 Future의 동작이 완료되기를 기다릴 때 사용합니다. async 안에서만 사용될 수 있습니다. 다음은 3개의 task, 1) learn_song, 2) sing_song, 3) dance가 주어졌을 때, sing_song과 dance는 learn_song이 완료된 후에 실행되도록 하는 예제 코드입니다. sing_song, dance를 실행시키기 전에 learn_song().await를 사용하여 동작 완료를 기다립니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 struct Song(String); async fn learn_song() -\u0026gt; Song { println!(\u0026#34;Learn song!\u0026#34;); Song(String::from(\u0026#34;Hype boy\u0026#34;)) } async fn dance(song: \u0026amp;Song) { println!(\u0026#34;Dance to {}!!\u0026#34;, song.0); } async fn sing_song(song: \u0026amp;Song) { println!(\u0026#34;Sing a song!! {}\u0026#34;, song.0) } fn main() { futures::executor::block_on(async { let song = learn_song().await; let f1 = sing_song(\u0026amp;song); let f2 = dance(\u0026amp;song); futures::join!(f1, f2); }); } 실행 결과\n1 2 3 Learn song! Sing a song!! Hype boy Dance to Hype boy!! await을 사용하여 \u0026lsquo;기다리는\u0026rsquo; 동작은 현재 thread를 blocking 하지 않고 동작을 yield 하는 방식으로 이루어집니다. 위의 예제에서는 그 동작이 잘 보이지 않을 수 있지만, I/O intensive application을 생각해 보면 이해하기 쉬울 것 입니다.\n다음 예제는 rust의 대표적인 Runtime인 tokio를 사용한 TCP I/O 예시입니다. tokio runtime 생성 시 new_current_thread을 사용하여 현재 thread(단일 thread)만 사용하도록 했습니다. 단일 thread에서 Server 동작과 Client 동작 둘 다를 실행하고 있지만, Server 동작을 하는 async block에서 TcpListner가 accept()를 기다리는(await) 동안 thread를 blocking 하지 않고 동작을 yield 하므로 Client 동작의 async block에서 불리는 connect()가 실행될 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 use tokio::io::{self, AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpStream, TcpListener}; fn main() { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap() .block_on(async { let addr = \u0026#34;127.0.0.1:6142\u0026#34;; let server = tokio::spawn(async move { let listener = TcpListener::bind(\u0026amp;addr).await?; loop { let (mut socket, _) = listener.accept().await?; let mut buf = vec![0; 1024]; match socket.read(\u0026amp;mut buf).await { Ok(n) =\u0026gt; { println!(\u0026#34;{:?}\u0026#34;, buf); } _ =\u0026gt; { return Ok::\u0026lt;_, io::Error\u0026gt;(()); } } } Ok::\u0026lt;_, io::Error\u0026gt;(()) }); let client = tokio::spawn(async move { let socket = TcpStream::connect(\u0026amp;addr).await?; let (mut rd, mut wr) = io::split(socket); wr.write_all(b\u0026#34;hello\\r\\n\u0026#34;).await?; wr.write_all(b\u0026#34;world\\r\\n\u0026#34;).await?; Ok::\u0026lt;_, io::Error\u0026gt;(()) }); tokio::join!(server, client); }); } 이 예제에서 사용한 TcpStream, TcpListener 등은 std crate가 아닌 tokio crate에 구현된 async 버전입니다. 이전 글에서 본 것과 같이, async rust를 사용하는 경우 synchronous하게 구현 된 standard library 대신 async 로 구현 된 library를 사용 해야 합니다.\nRuntime Runtime은 Future를 실행할 수 있는 환경입니다. async를 실행시키는 주체이므로 Async Executor라고도 부르는 것 같습니다. Runtime 혹은 Executor는 async를 실행시킵니다. 만약 async가 완료될 수 없는 상태라면 추후 실행 가능할 때 재 실행하고, 그 동안 다른 실행 가능한 async를 실행시켜 줍니다. 일종의 non-preemptive async task scheduler 인 것 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 // Future trait pub trait Future { type Output; fn poll(self: Pin\u0026lt;\u0026amp;mut Self\u0026gt;, cx: \u0026amp;mut Context\u0026lt;\u0026#39;_\u0026gt;) -\u0026gt; Poll\u0026lt;Self::Output\u0026gt;; } // poll 함수의 결과값 enum pub enum Poll\u0026lt;T\u0026gt; { // Represents that a value is immediately ready. #[lang = \u0026#34;Ready\u0026#34;] #[stable(feature = \u0026#34;futures_api\u0026#34;, since = \u0026#34;1.36.0\u0026#34;)] Ready(#[stable(feature = \u0026#34;futures_api\u0026#34;, since = \u0026#34;1.36.0\u0026#34;)] T), // Represents that a value is not ready yet. // // When a function returns `Pending`, the function *must* also // ensure that the current task is scheduled to be awoken when // progress can be made. #[lang = \u0026#34;Pending\u0026#34;] #[stable(feature = \u0026#34;futures_api\u0026#34;, since = \u0026#34;1.36.0\u0026#34;)] Pending, } async를 실행시키는 것은, async가 구현하고 있는 Future trait의 poll 함수를 호출하는 것을 의미합니다. async function/block/closure는 Future trait, 즉 poll 함수를 구현하고 있습니다. poll 함수가 불리면 async에 구현된 동작을 실행시키는데, async의 모든 라인이 실행 되었다면 Poll::Ready를 리턴하며 동작 완료합니다. 하지만 다른 일을 기다리는 등의 이유(e.g., I/O)로 async가 바로 완료될 수 없다면 Poll::Pending을 리턴하며 추후 재 실행되기를 기대합니다.\n1 2 3 4 5 6 7 8 9 // Future trait 실행 시 주어지는 Context. Waker는 virtual function table을 갖고 있다 pub struct Context\u0026lt;\u0026#39;a\u0026gt; { waker: \u0026amp;\u0026#39;a Waker, // Ensure we future-proof against variance changes by forcing // the lifetime to be invariant (argument-position lifetimes // are contravariant while return-position lifetimes are // covariant). _marker: PhantomData\u0026lt;fn(\u0026amp;\u0026#39;a ()) -\u0026gt; \u0026amp;\u0026#39;a ()\u0026gt;, } poll 함수의 인자로 주어지는 Context에는 Waker가 포함되어 있습니다. Waker는 Poll::Pending 시 기다리던 동작이 완료되면 이를 executor에게 알려주는 함수입니다(함수를 갖고 있습니다). Waker 는 executor-specific 합니다. Context는 async가 어디 까지 실행 되었는지를 알 수 있는 정보 또한 포함 하고 있습니다. Poll::Pending을 리턴하게 된 지점을 기억하고, Waker에 의해 재 실행된 경우 그 지점 부터 재 실행 합니다.\nasync function/block/closure 구현이 어떻게 Future::poll로 변환(?) 되는지 궁금한데 이 부분은 아직 코드 혹은 문서로 확인하지 못했어요. 기존 async 코드에서 바로 완료될 수 없는 함수를 만난 경우 혹은 코드 내에서 호출한 async의 poll도 Poll::Pending을 리턴하는 경우 Poll::Pending 을 리턴할 것 같다는 추측만 해 봅니다 ㅎㅎ\n이러한 polling 기반의 async 동작은 zero-cost futures를 가능하게 한다고 합니다!\nRuntime의 spawn이나 block_on과 같은 함수를 사용하면 async를 실행시킬 수 있습니다. 첫 번째 예제에서 사용한 futures crate의 futures::executor::block_on이 그러한 함수 중 하나 입니다. Runtime이 async를 실행 했을 때(=poll을 호출 했을 때), 그 결과가 Poll::Ready(val)인 경우 async는 실행 완료된 것이고, val를 리턴할 겁니다. 하지만 결과가 Poll::Pending이라면 어딘가에 담아 두었다, Waker가 호출된 후 재 실행할 것 입니다. 간단한 Runtime 구현은 async book의 Build an Executor를 참고하면 되겠습니다.\nRust에서 널리 사용되는 Async Runtime 들은 아래와 같습니다. tokio의 경우, std의 synchronous 동작을 asynchronous 버전(async fn)으로 구현한 버전도 제공합니다.\nTokio: A popular async ecosystem with HTTP, gRPC, and tracing frameworks. async-std: A crate that provides asynchronous counterparts to standard library components. smol: A small, simplified async runtime. Provides the Async trait that can be used to wrap structs like UnixStream or TcpListener. fuchsia-async: An executor for use in the Fuchsia OS. References Rust Async Book std thread std Future tokio io ","permalink":"http://huijeong-kim.github.io/post/2022-09-11-rust-async-programming/","summary":"오늘은 Rust에서 제공하는 Asynchronous Programming 관련 feature들에 대해 정리하면서, async 관련 포스트를 쓸 때 마다 사용되는 단어들, async, future, runtime, executor에 대해 정리해 보겠습니다. 오늘은 유독 내용이 추상적인 느낌에 부족한 부분이 많은 것 같은데, 틀린 부분이나 부족한 부분이 있다면 코멘트 남겨 주세요 :)\nAsynchronous Programming Async book에는 Asynchronous programming이 다음과 같이 정의되어 있습니다.\nAsynchronous programming, or async for short, is a concurrent programming model supported by an increasing number of programming languages.","title":"Rust Async Programming"},{"content":"Unit test를 작성 하다 보면 간단한 함수에 대해서도 꽤나 많은 수의 테스트 케이스를 작성하게 됩니다. Input 값의 조합, 특히나 Invalid Input 값의 조합은 무지 많아질 수 있기 때문 입니다. 그 모든 케이스를 하나 씩 테스트로 작성 하는 것은 꽤 귀찮기도 하고, 테스트 함수 작명 지옥에 빠지면서 테스트 가독성도 떨어지게 됩니다.\n이럴 때 사용할 수 있는 것이 table-driven test, 혹은 parameterised test 입니다. Input-Output 조합을 table로 표현하는 방식입니다.\nRust로 간단한 함수의 parameterised test를 작성해 보고, rust의 test-case, rtest crate를 활용해 이를 더 간편하게 작성하는 방법을 알아보겠습니다.\nParameterised Test 다음과 같은 아주 아주 간단한 함수의 테스트를 작성해 본다고 합시다.(test-case crate repo의 기본 예시 입니다 ^^) 두 개의 i8 값을 받아 두 수의 곱의 절대값을 반환하는 함수입니다.\n1 2 3 pub fn multiplication(x: i8, y: i8) -\u0026gt; i8 { (x * y).abs() } Input의 두 i8 각각이 음수 \u0026amp; 양수일 때 절대값을 돌려주는 지 확인하는 테스트를 작성해 보겠습니다. 너무 간단해서 와닿지 않을 수 있겠지만 예시로만 참고해 주세요.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #[cfg(test)] mod tests { use super::*; #[test] fn multiplication_tests_negative_negative() { let x = -2; let y = -4; let actual = multiplication(x, y); assert_eq!(8, actual); } #[test] fn multiplication_tests_negative_positive() { let x = -2; let y = 4; let actual = multiplication(x, y); assert_eq!(-8, actual); } #[test] fn multiplication_tests_positive_negative() { let x = 2; let y = -4; let actual = multiplication(x, y); assert_eq!(-8, actual); } #[test] fn multiplication_tests_positive_positive() { let x = 2; let y = 4; let actual = multiplication(x, y); assert_eq!(8, actual); } } 실행 결과\n1 2 3 4 5 6 7 running 4 tests test tests::multiplication_tests_negative_negative ... ok test tests::multiplication_tests_negative_positive ... ok test tests::multiplication_tests_positive_negative ... ok test tests::multiplication_tests_positive_positive ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 양수, 음수의 조합 4가지를 테스트로 작성하였습니다. 간단한 함수의 테스트인데 꽤 많은 코드가 필요합니다. 게다가 테스트 fail 발생할 경우 각 테스트를 하나 하나 읽으며 의도를 파악해야 합니다. 이 예제는 간단하여 금방 의도를 파악할 수 있겠으나, 코드가 조금만 더 복잡해진다면 배로 힘들어 질 것 같습니다. 게다가 만약 이 함수가 절대값이 아닌 곱셈 결과를 반환하도록 바뀌어야 한다면, 테스트 케이스 중 바뀌어야 하는 expected 값을 찾는 게 조금 힘들 수 있겠습니다.\n이를 개선하기 위해 여기에 parameterised test를 적용하면 테스트 코드는 다음과 같이 바뀌게 됩니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #[cfg(test)] mod tests { use super::*; #[test] fn multiplication_tests() { let test_cases = vec![ (-2, -4, 8, \u0026#34;when negative-negative value provided\u0026#34;), (-2, 4, 8, \u0026#34;when negative-positive value provided\u0026#34;), (2, -4, 8, \u0026#34;when positive-negative value provided\u0026#34;), (2, 4, 8, \u0026#34;when positive-positive value provided\u0026#34;), ]; for (arg1, arg2, expected, err_msg) in test_cases { let actual = multiplication(arg1, arg2); assert_eq!(expected, actual, \u0026#34;Result is wrong {}\u0026#34;, err_msg); } } } test case table에 input값, 예상 값과 함께 실패했을 경우 출력할 error message를 정의하였습니다. Fail 발생 시 적절한 error message를 출력하기 위해서 입니다. 만약 multiplication 함수에서 abs()를 제거한다면 다음과 같이 적절한 에러 메시지가 출력될 것 입니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 running 1 test test tests::multiplication_tests ... FAILED failures: ---- tests::multiplication_tests stdout ---- thread \u0026#39;tests::multiplication_tests\u0026#39; panicked at \u0026#39;assertion failed: `(left == right)` left: `8`, right: `-8`: Result is wrong when negative-positive value provided\u0026#39;, src/bin/test-cases.rs:21:13 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::multiplication_tests test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass \u0026#39;--bin test-cases\u0026#39; test case table을 만드는 것 만으로도 테스트 코드가 간결해 졌습니다. 다른 모든 unit test code에도 동일하게 적용할 수 있겠습니다. 다만 매 번 table 을 만들고 for loop 를 만드는 수고로움이 있습니다. 이 부분은 test case table 생성을 도와주는 rust crate들의 도움을 받을 수 있습니다!\ntest-case test-case crate를 사용하면 macro로 test case table을 만들 수 있습니다. 위에서 작성한 test case table을 test_case macro를 사용하여 수정하면 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #[cfg(test)] mod tests { use super::*; use test_case::test_case; #[test_case(-2, -4 =\u0026gt; 8; \u0026#34;when negative-negative value provided\u0026#34;)] #[test_case(-2, 4 =\u0026gt; 8; \u0026#34;when negative-positive value provided\u0026#34;)] #[test_case(2, -4 =\u0026gt; 8; \u0026#34;when positive-negative value provided\u0026#34;)] #[test_case(2, 4 =\u0026gt; 8; \u0026#34;when positive-positive value provided\u0026#34;)] fn multiplication_test(x: i8, y: i8) { let actual = multiplication(x, y); assert_eq!(8, actual) } } 실행 결과는 다음과 같습니다. 실행 결과에서 유추할 수 있듯, 각 macro의 마지막에 적은 string 이 곧 test case 이름이 됩니다.\n1 2 3 4 5 6 7 running 4 tests test tests::multiplication_test::when_negative_negative_value_provided ... ok test tests::multiplication_test::when_positive_positive_value_provided ... ok test tests::multiplication_test::when_negative_positive_value_provided ... ok test tests::multiplication_test::when_positive_negative_value_provided ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s test case 이름을 지정하지 않으면 test case name은 input, output 값을 가지고 test case를 생성해 냅니다. 여기서 i8 input의 positive/negative는 구별하지 못하는 것 같더라고요. (-2, 4 =\u0026gt; 8)과 (2, 4 =\u0026gt; 8)이 같은 이름으로 생성되 에러가 발생하니 참고하세요.\n1 2 3 4 5 6 7 #[test_case(-2, -4 =\u0026gt; 8)] #[test_case(-3, 4 =\u0026gt; 12)] #[test_case(1, -4 =\u0026gt; 4)] #[test_case(6, 4 =\u0026gt; 24)] fn multiplication_test(x: i8, y: i8) -\u0026gt; i8{ multiplication(x, y) } 1 2 3 4 5 6 7 running 4 tests test tests::multiplication_test::_1_4_expects_4 ... ok test tests::multiplication_test::_2_4_expects_8 ... ok test tests::multiplication_test::_3_4_expects_12 ... ok test tests::multiplication_test::_6_4_expects_24 ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 그 외에도 expect panic, assert function 지정 등 재미있는 syntax들이 많습니다. 더 자세한 사용법은 link를 참고해 주세요.\nrtest rstest는 test fixture, parameterised test 등을 제공하는 crate 입니다. 앞에서 소개한 test-case 보다 다양한 기능이 있는 것 같습니다. 오늘은 그 중 parameterised test 작성 부분만 살펴보겠습니다.\n위에서 작성한 코드를 그대로 rstest를 사용하여 작성하면 다음과 같습니다. 자동 생성 된 Test case 명이 아쉬운데, 테스트 명을 변경하는 방법은 찾지 못하였어요.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 #[cfg(test)] mod tests { use super::*; use rstest::rstest; #[rstest] #[case(-2, -4, 8)] #[case(2, -4, 8)] #[case(-2, 4, 8)] #[case(2, 4, 8)] fn multiplication_test(#[case] x: i8, #[case] y: i8, #[case] expected: i8) { assert_eq!(multiplication(x, y), expected); } } 1 2 3 4 5 6 7 running 4 tests test tests::multiplication_test::case_1 ... ok test tests::multiplication_test::case_2 ... ok test tests::multiplication_test::case_3 ... ok test tests::multiplication_test::case_4 ... ok test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 사용법도 조금 더 귀찮고 아쉽운 부분이 있긴 한데, rstest는 Test fixture 제공, Test timeout 설정하기 등등 다른 유용한 기능들이 많습니다. 이 기능들은 좀 더 사용해 보고 공유하겠습니다 :)\n","permalink":"http://huijeong-kim.github.io/post/2022-08-28-parameterised-test/","summary":"Unit test를 작성 하다 보면 간단한 함수에 대해서도 꽤나 많은 수의 테스트 케이스를 작성하게 됩니다. Input 값의 조합, 특히나 Invalid Input 값의 조합은 무지 많아질 수 있기 때문 입니다. 그 모든 케이스를 하나 씩 테스트로 작성 하는 것은 꽤 귀찮기도 하고, 테스트 함수 작명 지옥에 빠지면서 테스트 가독성도 떨어지게 됩니다.\n이럴 때 사용할 수 있는 것이 table-driven test, 혹은 parameterised test 입니다. Input-Output 조합을 table로 표현하는 방식입니다.\nRust로 간단한 함수의 parameterised test를 작성해 보고, rust의 test-case, rtest crate를 활용해 이를 더 간편하게 작성하는 방법을 알아보겠습니다.","title":"Rust로 Parameterised Test 작성하기"},{"content":"mpsc(multi produce single consumer) queue는 thread 간 message를 주고받는 channel로 많이 쓰입니다. std mpsc와 crossbeam channel가 많이 쓰이는 mpsc channel이고, async rust에서는 tokio mpsc를 사용할 수 있습니다.\nasync rust에서 mpsc queue를 사용하는 방법을 알아보겠습니다.\n1. std::sync::mpsc 사용하기 가장 먼저 std 라이브러리의 mpsc를 사용해 볼 수 있겠습니다. Async rust에서 mpsc를 사용하려면 여러 future들이 message sender(tx)를 갖고 있고 하나의 future가 message receiver(rx)를 갖고 있어야 합니다. mpsc의 Sender는 clone 가능하므로 다음과 같이 tx를 clone하여 여러 Future가 message channel을 공유할 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 usd std::sync::mpsc; #[tokio::main] async fn main() { let (tx, rx) = mpsc::channel(); for sender_id in 0..10 { let tx_channel = tx.clone(); tokio::spawn(async move { tx_channel.send(sender_id).expect(\u0026#34;Failed to send message\u0026#34;); }); } drop(tx); let handle = tokio::task::spawn(async move { while let Ok(i) = rx.recv() { println!(\u0026#34;got = {}\u0026#34;, i); } }); handle.await; } Sender가 필요한 future를 spawn 할 때 마다 message channel의 Sender를 clone 하였습니다. 중간에 처음 생성한 tx를 drop 하는 것은, 모든 tx 가 drop 되어야 channel이 close 되고 rx.recv()가 Err를 받게 되어 프로그램을 종료할 수 있기 때문입니다.\n실행 결과는 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 got = 0 got = 2 got = 3 got = 1 got = 4 got = 5 got = 6 got = 7 got = 8 got = 9 위의 예시에서 std::sync::recv는 blocking function 입니다. 새로운 메시지를 받거나 Err를 받지 않는 한, spawn 된 future는 executor에서 다음 async task 수행을 blocking 할 수 있습니다. 즉 recv 함수는 runtime executor를 막아 어떤 future들의 실행을 막을 수 있습니다.\n문제가 발생할 만한 예시를 만들기 위해, tokio Runtime을 single-thread (new_current_thread)로 생성하고, message send 하기 전에 3초 sleep 하도록 수정해 보았습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 fn main() { console_subscriber::init(); let rt1 = tokio::runtime::Builder::new_current_thread() .enable_all() .build() .unwrap(); let (tx, rx) = mpsc::channel(); for sender_id in 0..10 { let tx_channel = tx.clone(); rt1.spawn(async move { tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; tx_channel.send(sender_id).expect(\u0026#34;Failed to send message\u0026#34;); }); } drop(tx); let handle = rt1.spawn(async move { while let Ok(i) = rx.recv() { println!(\u0026#34;got = {}\u0026#34;, i); } }); rt1.block_on(async { handle.await; }); } 위 코드에서는 모든 tx들이 sleep하며 await 을 하는 동안 rx.recv()가 시작될 테고, 이 recv() 함수는 blocking 함수이므로 runtime executor를 막고 있어서, tx를 갖고 있는 future들이 sleep 이 끝난 뒤 재 실행 될 기회를 주지 않습니다.\n실행 하면 이전과 다르게 아무런 출력이 나오지 않습니다. 여기서 tokio-console을 사용하여 future들의 상태를 확인하면 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 connection: http://127.0.0.1:6669/ (CONNECTED) views: t = tasks, r = resources controls: left, right or h, l = select column (sort), up, down or k, j = scroll, enter = view details, i = invert sort (highest/lowest), q = quit gg = scroll to top, G = scroll to bottom Warnings /!\\ 1 tasks have lost their waker Tasks (12) BUSY Running (1) IDLE Idle (11) Warn ID State Name Total- Busy Idle Polls Target Location 9 IDLE 244.0032s 118.4160us 244.0031s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 10 IDLE 244.0031s 89.2080us 244.0030s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 6 IDLE 244.0031s 86.3330us 244.0030s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 11 IDLE 244.0030s 85.7080us 244.0029s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 2 IDLE 244.0029s 84.9580us 244.0028s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 3 IDLE 244.0029s 95.5410us 244.0028s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 8 IDLE 244.0028s 84.5830us 244.0027s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 1 IDLE 244.0028s 86.4160us 244.0027s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 7 IDLE 244.0027s 86.0410us 244.0026s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 5 IDLE 244.0026s 86.5830us 244.0025s 1 tokio::task src/bin/tokio-mpsc.rs:15:13 12 BUSY 244.0026s 244.0015s 1.0940ms 1 tokio::task src/bin/tokio-mpsc.rs:23:22 ! 1 4 IDLE 244.0025s 18.0410us 244.0025s 1 tokio::task \u0026lt;cargo\u0026gt;/tokio-1.20.1/src/runtime/mod.rs:477:2 모든 future들은 IDLE 상태인데, BUSY 상태인 future가 하나 있습니다. tokio-mpsc.rs:23에서 시작 한 future 인데, 이는 recv()함수가 포함된 future의 시작점입니다.\n이와 같이 async rust에서 기존의 mpsc queue를 사용할 순 있지만 경우에 따라서 문제가 발생할 수 있습니다. 물론 try_recv를 사용하여 workaround 코드를 작성할 순 있겠으나 조금 복잡해 질 것 같습니다. Async rust에서는 async로 동작하는 mpsc queue를 사용하는 게 좋을 것 같습니다.\n2. tokio::sync::mpsc 사용하기 recv() 동작이 executor를 blocking 하지 않도록 tokio의 async mpsc queue를 사용해 봅시다. 위의 코드에서 mpsc를 tokio 의 mpsc로 바꾸고, await 하도록 수정하면 됩니다. 그 외 API 사용법이 조금씩 다른 부분이 있으니 주의하세요.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 use tokio::sync::mpsc; const BUFFER_SIZE: usize = 128; fn main() { console_subscriber::init(); let rt1 = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap(); let (tx, mut rx) = mpsc::channel(BUFFER_SIZE); for sender_id in 0..10 { let tx_channel = tx.clone(); rt1.spawn(async move { tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; tx_channel.send(sender_id).await.expect(\u0026#34;Failed to send message\u0026#34;); }); } drop(tx); let handle = rt1.spawn(async move { while let Some(i) = rx.recv().await { println!(\u0026#34;got = {}\u0026#34;, i); } }); rt1.block_on(async { handle.await; }); } 실행 시 다음과 같이 정상적인 결과가 나옵니다.\n1 2 3 4 5 6 7 8 9 10 got = 0 got = 1 got = 2 got = 3 got = 4 got = 5 got = 6 got = 7 got = 8 got = 9 3. Tonic server의 async function에서 tx 공유하기 Tokio을 기반으로 하는 Tonic을 사용할 때 mpsc queue가 필요한 경우가 있습니다. gRPC server에서 받은 message를 message queue를 통해 다른 future에게 전달하고 싶을 때가 그 예 중 하나입니다.\nTonic의 hello world 예제에서 mpsc queue를 사용해 봅시다. Hello message를 받을 때 마다 이를 tokio의 다른 future에게 전달하도록 하였습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 use std::sync::mpsc; use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!(\u0026#34;helloworld\u0026#34;); } pub struct MyGreeter { tx: mpsc::Sender\u0026lt;String\u0026gt;, } impl MyGreeter { pub fn new(tx: mpsc::Sender\u0026lt;String\u0026gt;) -\u0026gt; Self { Self { tx, } } } #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( \u0026amp;self, request: Request\u0026lt;HelloRequest\u0026gt;, ) -\u0026gt; Result\u0026lt;Response\u0026lt;HelloReply\u0026gt;, Status\u0026gt; { println!(\u0026#34;Got a request from {:?}\u0026#34;, request.remote_addr()); let name = request.into_inner().name; let reply = hello_world::HelloReply { message: format!(\u0026#34;Hello {}!\u0026#34;, name.clone()), }; self.tx.send(name); Ok(Response::new(reply)) } } #[tokio::main] async fn main() -\u0026gt; Result\u0026lt;(), Box\u0026lt;dyn std::error::Error\u0026gt;\u0026gt; { let (tx, rx) = mpsc::channel(); let addr = \u0026#34;[::1]:50051\u0026#34;.parse().unwrap(); let greeter = MyGreeter::new(tx); println!(\u0026#34;GreeterServer listening on {}\u0026#34;, addr); tokio::spawn(async move { println!(\u0026#34;START RECEIVING MESSAGES\u0026#34;); while let Ok(msg) = rx.recv() { println!(\u0026#34;received a message: {:?}\u0026#34;, msg); } }); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } 우선 std::sync::mpsc를 사용해 봤습니다. 빌드와 실행이 잘 되고, 특정 상황에서만 실행 상 오류가 나던 이전 예제와는 달리, compile error부터 납니다. std::sync::mpsc::Sender가 Sync 를 구현하고 있지 않기 때문입니다. gRPC Server의 message handle 함수들은 여러 thread(tokio runtime이 사용하는 thread)에서 동시에 수행될 수 있으므로(같은 gRPC가 여러 개 들어오면 한 번에 모두 처리 될 수 있음) Sync trait을 가질 것을 강제하고 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 error[E0277]: `std::sync::mpsc::Sender\u0026lt;String\u0026gt;` cannot be shared between threads safely --\u0026gt; src/bin/tokio-mpsc.rs:55:6 | 55 | impl Greeter for MyGreeter { | ^^^^^^^ `std::sync::mpsc::Sender\u0026lt;String\u0026gt;` cannot be shared between threads safely | = help: within `MyGreeter`, the trait `Sync` is not implemented for `std::sync::mpsc::Sender\u0026lt;String\u0026gt;` note: required because it appears within the type `MyGreeter` --\u0026gt; src/bin/tokio-mpsc.rs:44:12 | 44 | pub struct MyGreeter { | ^^^^^^^^^ note: required by a bound in `Greeter` --\u0026gt; /Users/huijeongkim/Workspace/rust_practice/target/debug/build/rust_practice-41089e7fe9e0adbb/out/helloworld.rs:111:31 | 111 | pub trait Greeter: Send + Sync + \u0026#39;static { | ^^^^ required by this bound in `Greeter` For more information about this error, try `rustc --explain E0277`. error: could not compile `rust_practice` due to previous error 이 문제는 tx를 mutex로 보호해서 해결할 수도 있고, 앞에서와 같이 tokio::sync::mpsc를 사용하는 것으로도 해결할 수 있습니다. tokio::sync::mpsc를 사용하도록 수정한 코드는 아래와 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 use tokio::sync::mpsc; const BUFFER_SIZE: usize = 128; use tonic::{transport::Server, Request, Response, Status}; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; pub mod hello_world { tonic::include_proto!(\u0026#34;helloworld\u0026#34;); } pub struct MyGreeter { tx: mpsc::Sender\u0026lt;String\u0026gt;, } impl MyGreeter { pub fn new(tx: mpsc::Sender\u0026lt;String\u0026gt;) -\u0026gt; Self { Self { tx, } } } #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( \u0026amp;self, request: Request\u0026lt;HelloRequest\u0026gt;, ) -\u0026gt; Result\u0026lt;Response\u0026lt;HelloReply\u0026gt;, Status\u0026gt; { println!(\u0026#34;Got a request from {:?}\u0026#34;, request.remote_addr()); let name = request.into_inner().name; let reply = hello_world::HelloReply { message: format!(\u0026#34;Hello {}!\u0026#34;, name.clone()), }; self.tx.send(name).await; Ok(Response::new(reply)) } } #[tokio::main] async fn main() -\u0026gt; Result\u0026lt;(), Box\u0026lt;dyn std::error::Error\u0026gt;\u0026gt; { let (tx, mut rx) = mpsc::channel(BUFFER_SIZE); let addr = \u0026#34;[::1]:50051\u0026#34;.parse().unwrap(); let greeter = MyGreeter::new(tx); println!(\u0026#34;GreeterServer listening on {}\u0026#34;, addr); tokio::spawn(async move { println!(\u0026#34;START RECEIVING MESSAGES\u0026#34;); while let Some(msg) = rx.recv().await { println!(\u0026#34;received a message: {:?}\u0026#34;, msg); } }); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) } tokio::sync::mpsc의 구현을 살펴 보면 그 이유를 알 수 있습니다. tokio mpsc queue의 Sender 구현은 아래와 같이 Arc와 Semaphore로 보호되어 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 // tokio::sync::mpsc 의 Sender pub struct Sender\u0026lt;T\u0026gt; { chan: chan::Tx\u0026lt;T, Semaphore\u0026gt;, } // .. // channel sender pub(crate) struct Tx\u0026lt;T, S\u0026gt; { inner: Arc\u0026lt;Chan\u0026lt;T, S\u0026gt;\u0026gt;, } 4. 결론 돌고 돌아 async rust에서는 async mpsc 를 사용해야 한다는 당연한 얘기로 마무리 합니다\u0026hellip;. 제 삽질기가 도움이 되었길 바랍니다\u0026hellip;.\n","permalink":"http://huijeong-kim.github.io/post/2022-08-27-tokio-mpsc/","summary":"mpsc(multi produce single consumer) queue는 thread 간 message를 주고받는 channel로 많이 쓰입니다. std mpsc와 crossbeam channel가 많이 쓰이는 mpsc channel이고, async rust에서는 tokio mpsc를 사용할 수 있습니다.\nasync rust에서 mpsc queue를 사용하는 방법을 알아보겠습니다.\n1. std::sync::mpsc 사용하기 가장 먼저 std 라이브러리의 mpsc를 사용해 볼 수 있겠습니다. Async rust에서 mpsc를 사용하려면 여러 future들이 message sender(tx)를 갖고 있고 하나의 future가 message receiver(rx)를 갖고 있어야 합니다. mpsc의 Sender는 clone 가능하므로 다음과 같이 tx를 clone하여 여러 Future가 message channel을 공유할 수 있습니다.","title":"Async Rust에서 mpsc queue 사용하기"},{"content":"지난 글에 이어 Actix actor의 AsyncContext 함수를 사용하는 방법을 알아봅니다.\nadd_stream, add_message_stream 1 2 3 4 5 6 7 8 9 10 fn add_stream\u0026lt;S\u0026gt;(\u0026amp;mut self, fut: S) -\u0026gt; SpawnHandle where S: Stream + \u0026#39;static, A: StreamHandler\u0026lt;S::Item\u0026gt;; fn add_message_stream\u0026lt;S\u0026gt;(\u0026amp;mut self, fut: S) where S: Stream + \u0026#39;static, S::Item: Message, A: Handler\u0026lt;S::Item\u0026gt;; Context에 stream을 등록하는 함수입니다. 두 함수는 거의 비슷하나 차이점은 add_message_stream은 stream error를 무시한다는 점입니다.\n다음은 API Doc의 예제인데요. Stream을 만들고 message를 하나 보냅니다. Stream의 사용법과 예제는 다음 글에서 따로 정리해 보겠습니다 :)\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 use actix::prelude::*; use futures_util::stream::once; #[derive(Message)] #[rtype(result = \u0026#34;()\u0026#34;)] struct Ping; struct MyActor; impl StreamHandler\u0026lt;Ping\u0026gt; for MyActor { fn handle(\u0026amp;mut self, item: Ping, ctx: \u0026amp;mut Context\u0026lt;MyActor\u0026gt;) { println!(\u0026#34;PING\u0026#34;); System::current().stop(); } fn finished(\u0026amp;mut self, ctx: \u0026amp;mut Self::Context) { println!(\u0026#34;finished\u0026#34;); } } impl Actor for MyActor { type Context = Context\u0026lt;Self\u0026gt;; fn started(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { // add stream ctx.add_stream(once(async { Ping })); } } fn main() { let mut sys = System::new(); let addr = sys.block_on(async { MyActor.start() }); sys.run(); } 실행 결과는 다음과 같습니다.\n1 2 PING finished notify, notify_later 1 2 3 4 5 6 7 8 9 fn notify\u0026lt;M\u0026gt;(\u0026amp;mut self, msg: M) where A: Handler\u0026lt;M\u0026gt;, M: Message + \u0026#39;static; fn notify_later\u0026lt;M\u0026gt;(\u0026amp;mut self, msg: M, after: Duration) -\u0026gt; SpawnHandle where A: Handler\u0026lt;M\u0026gt;, M: Message + \u0026#39;static; notify는 자기 자신에게 message를 보내는 함수입니다. 이전 글에서 address를 통해 자기 자신의 주소를 찾고, 여기에 message를 보내는 예제를 작성하였는데요. notify 함수로 바로 message handler를 부를 수도 있습니다. 이 함수는 actor의 mailbox를 bypass 하고 항상 message를 전달한다고 합니다. 하지만 actor가 Stopped 상태라면 error 발생합니다.\n이전에 자신의 Addr를 찾아 메시지를 보내는 예제를 notify 함수를 사용하도록 수정하면 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 fn send_message_to_myself_using_address(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { let my_address = ctx.address(); actix::spawn(async move { my_address.send(Message).await; }); } fn send_message_to_myself_using_notify(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { ctx.notify(Message); } notify_later은 notify 함수와 유사하지만, 지정된 시간 이후에 message를 보냅니다. 또 하나의 차이점은 SpawnHandle을 리턴한다는 것 인데요. 실행되기 전에는 cancel_future 통해 동작을 취소할 수 있습니다. 이전 글에서 언급한 바와 같이, cancel_future 함수의 리턴 값은 항상 true 입니다. 취소 성공 여부를 리턴하지 않음을 참고하세요.\n1 2 3 4 fn send_message_to_myself_and_cancel(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { let handle = ctx.notify(Message, Duration::from_secs(3)); let _success = ctx.cancel_future(handle); } run_later, run_interval 1 2 3 4 5 6 7 fn run_later\u0026lt;F\u0026gt;(\u0026amp;mut self, dur: Duration, f: F) -\u0026gt; SpawnHandle where F: FnOnce(\u0026amp;mut A, \u0026amp;mut A::Context) + \u0026#39;static; fn run_interval\u0026lt;F\u0026gt;(\u0026amp;mut self, dur: Duration, f: F) -\u0026gt; SpawnHandle where F: FnMut(\u0026amp;mut A, \u0026amp;mut A::Context) + \u0026#39;static; run_later은 주어진 시간 이후에 주어진 future를 실행시키고, run_interval은 주어진 시간 주기로 future를 실행시킵니다. 사용법은 거의 동일하고, run_interval의 사용 예제는 이 글을 참고하시면 되겠습니다.\nReferences https://docs.rs/actix/0.13.0/actix/trait.AsyncContext.html\n","permalink":"http://huijeong-kim.github.io/post/2022-08-27-actix-actor-async-ctx-2/","summary":"지난 글에 이어 Actix actor의 AsyncContext 함수를 사용하는 방법을 알아봅니다.\nadd_stream, add_message_stream 1 2 3 4 5 6 7 8 9 10 fn add_stream\u0026lt;S\u0026gt;(\u0026amp;mut self, fut: S) -\u0026gt; SpawnHandle where S: Stream + \u0026#39;static, A: StreamHandler\u0026lt;S::Item\u0026gt;; fn add_message_stream\u0026lt;S\u0026gt;(\u0026amp;mut self, fut: S) where S: Stream + \u0026#39;static, S::Item: Message, A: Handler\u0026lt;S::Item\u0026gt;; Context에 stream을 등록하는 함수입니다. 두 함수는 거의 비슷하나 차이점은 add_message_stream은 stream error를 무시한다는 점입니다.\n다음은 API Doc의 예제인데요. Stream을 만들고 message를 하나 보냅니다.","title":"Actix actor의 AsyncContext 활용하기 (2)"},{"content":"Actix에서 actor를 생성하면 해당 actor를 실행시키는 문맥(context)에 해당하는 Context가 생성 되고, Context는 다양한 actor의 동작을 정의합니다. 그 중 Context를 활용해서 actor가 future를 실행, 취소, 기다리는 방법을 알아봅니다. 아래 예제 코드들은 actix 0.13.0 버전을 사용했습니다.\nActor Context 먼저, Actor의 Context의 정의, 생성 시기를 알아보겠습니다.\n사용자가 정의한 struct를 actor로 만들기 위해서 최소로 필요한 것은 Actor trait을 구현(implement) 하고 Context를 정의하는 것 입니다.\n1 2 3 4 5 struct MyActor; impl Actor for MyActor { type Context = Context\u0026lt;Self\u0026gt;; } 위 actor는 다음과 같이 생성되고 시작될 수 있습니다.\n1 2 let actor = MyActor; let addr = actor.start(); actor.start()는 앞에서 정의한 Actor trait에 구현되어 있습니다. 다음은 Actor에 구현된 start 함수입니다. Actor가 시작되면, Actor가 실행 될 Context가 생성되고 실행됩니다.\n1 2 3 4 5 6 fn start(self) -\u0026gt; Addr\u0026lt;Self\u0026gt; where Self: Actor\u0026lt;Context = Context\u0026lt;Self\u0026gt;\u0026gt;, { Context::new().run(self) } Context는 mailbox, future handles 등 actor가 실행되는 문맥을 갖고 있습니다. 그리고 이 Context는 AsyncContext라는 trait를 구현하고 있는데요, 이를 활용하면 actor가 실행해야 하는 async task들을 다룰 수 있습니다.\nAsyncContext가 정의하는 함수들은 아래와 같습니다. 함수 목록만 보기 위해 trait에 구현된 내용은 생략하였습니다. 예제를 통해 각 함수들을 사용하는 방법을 알아보겠습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 pub trait AsyncContext\u0026lt;A\u0026gt;: ActorContext where A: Actor\u0026lt;Context = Self\u0026gt;, { fn address(\u0026amp;self) -\u0026gt; Addr\u0026lt;A\u0026gt;; fn spawn\u0026lt;F\u0026gt;(\u0026amp;mut self, fut: F) -\u0026gt; SpawnHandle where F: ActorFuture\u0026lt;A, Output = ()\u0026gt; + \u0026#39;static; fn wait\u0026lt;F\u0026gt;(\u0026amp;mut self, fut: F) where F: ActorFuture\u0026lt;A, Output = ()\u0026gt; + \u0026#39;static; fn waiting(\u0026amp;self) -\u0026gt; bool; fn cancel_future(\u0026amp;mut self, handle: SpawnHandle) -\u0026gt; bool; fn add_stream\u0026lt;S\u0026gt;(\u0026amp;mut self, fut: S) -\u0026gt; SpawnHandle where S: Stream + \u0026#39;static, A: StreamHandler\u0026lt;S::Item\u0026gt;; fn add_message_stream\u0026lt;S\u0026gt;(\u0026amp;mut self, fut: S) where S: Stream + \u0026#39;static, S::Item: Message, A: Handler\u0026lt;S::Item\u0026gt;; fn notify\u0026lt;M\u0026gt;(\u0026amp;mut self, msg: M) where A: Handler\u0026lt;M\u0026gt;, M: Message + \u0026#39;static; fn notify_later\u0026lt;M\u0026gt;(\u0026amp;mut self, msg: M, after: Duration) -\u0026gt; SpawnHandle where A: Handler\u0026lt;M\u0026gt;, M: Message + \u0026#39;static; fn run_later\u0026lt;F\u0026gt;(\u0026amp;mut self, dur: Duration, f: F) -\u0026gt; SpawnHandle where F: FnOnce(\u0026amp;mut A, \u0026amp;mut A::Context) + \u0026#39;static; fn run_interval\u0026lt;F\u0026gt;(\u0026amp;mut self, dur: Duration, f: F) -\u0026gt; SpawnHandle where F: FnMut(\u0026amp;mut A, \u0026amp;mut A::Context) + \u0026#39;static; } address 1 fn address(\u0026amp;self) -\u0026gt; Addr\u0026lt;A\u0026gt;; 현재 Actor의 Addr를 리턴합니다. Addr는 해당 Actor에게 message 를 보내는 channel과 같습니다. 어떤 경우에 사용 가능할까 고민을 해 본다면,,\n자기 자신에게 message를 보내야 할 때: 아래 예제와 같이 자기 자신에게 message를 보내야 한다면 사용할 수 있겠습니다. 하지만 다음에 설명 될 notify, run_later 등의 함수가 있어 사용하게 될 지 모르겠습니다. 1 2 3 4 5 6 fn send_message_to_myself(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { let my_address = ctx.address(); actix::spawn(async move { my_address.send(Message).await; }); } 자기 자신의 주소가 바뀐 것을 알려줘야 할 때: 예를 들어 message 처리 중 self.clone() 한 뒤 바뀐 Addr를 사용자에게 알려줘야 하는 경우엔 사용할 수 있을 것 같긴 하네요. 자주 쓰일지는 모르겠지만요..\nMessage에 응답 받을 주소를 기재할 때: Message를 보낼 때 상대 actor에게 답장을 받을 주소, 즉 자신의 Addr를 전달할 수도 있겠습니다. 다음은 또 다른 actor framework인 riker의 message handling 함수인데요. Actor는 Message를 받을 때 sender의 주소도 받게 됩니다. 이는 actor가 앞으로 message를 보내야 하는 모든 상대의 Addr를 미리 알 필요 없게 해 주고, 답장 받을 주소를 바꾸기 쉽게 하여 unit test 작성하기 쉽게 해 줍니다.\n1 2 3 4 5 6 7 8 9 10 11 12 // Riker implement the Actor trait impl Actor for MyActor { type Msg = String; fn recv(\u0026amp;mut self, _ctx: \u0026amp;Context\u0026lt;String\u0026gt;, msg: String, _sender: Sender) { println!(\u0026#34;Received: {}\u0026#34;, msg); } } spawn 1 2 3 fn spawn\u0026lt;F\u0026gt;(\u0026amp;mut self, fut: F) -\u0026gt; SpawnHandle where F: ActorFuture\u0026lt;A, Output = ()\u0026gt; + \u0026#39;static; 주어진 future를 현재 Context에 spawn 합니다. 리턴 된 SpawnHandle은 아래에서 설명할 cancel_future 함수 등에 활용될 수 있습니다. Actor가 stopping 상태에 진입되면 spawn 된 함수들은 모두 cancel 됩니다.\n바로 위, 자기 자신에게 message를 보내는 코드에서 message 보내는 future를 actix::spawn()을 사용해 시작했는데요, 이를 Context::spawn()을 사용하여 시작할 수도 있습니다. Actor가 멈추게 된다면 spawn 된 future들도 함께 멈추므로, 이미 멈춘 actor에게 message를 보내는 일이 없도록 Context::spawn()을 사용하는 게 더 안전할 것 같습니다.\n여기서 future의 type은 ActorFuture 입니다. into_actor를 통해 future를 actor의 context를 담은 ActorFuture로 변환할 수 있습니다.\n1 2 3 4 5 6 fn send_message_to_myself(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { let my_address = ctx.address(); ctx.spawn(async { my_address.send(Message).await; }.into_actor(self)); } wait 1 2 3 fn wait\u0026lt;F\u0026gt;(\u0026amp;mut self, fut: F) where F: ActorFuture\u0026lt;A, Output = ()\u0026gt; + \u0026#39;static; future를 spawn 하고 이 것이 \u0026ldquo;resolve\u0026rdquo; 될 때 까지 기다립니다. 즉, async 함수의 await이 끝날 때 까지 기다립니다. 그 동안 actor는 새로운 message를 받을 수 없습니다.\n1 2 3 4 5 6 7 8 9 fn wait_future(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { tracing::info!(\u0026#34;Before wait \u0026#34;); ctx.wait(async { tracing::info!(\u0026#34;async block started...\u0026#34;); tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; tracing::info!(\u0026#34;async block ended...\u0026#34;); }.into_actor(self)); tracing::info!(\u0026#34;After wait \u0026#34;); } 위와 같은 코드를 실행 해 보면 결과는 아래와 같습니다. wait이 호출 되면 async block 이 끝날 때 까지 기다릴 것 같아 보이지만, 이 함수 호출에서 resolve를 기다리는 것은 아니고 함수는 바로 종료됩니다. 다만 context가 wait 합니다. 이 부분은 바로 다음에서 설명하겠습니다.\n1 2 3 4 5 2022-08-20T12:55:48.307811Z INFO actix_context_usage: actor started 2022-08-20T12:55:48.307916Z INFO actix_context_usage: Before wait 2022-08-20T12:55:48.307946Z INFO actix_context_usage: After wait 2022-08-20T12:55:48.307973Z INFO actix_context_usage: async block started... 2022-08-20T12:55:51.310184Z INFO actix_context_usage: async block ended... waiting 1 fn waiting(\u0026amp;self) -\u0026gt; bool; 현재 context가 waiting(paused) 상태인지 알려줍니다. 위에서 설명한 Context::wait() 함수를 통해 시작한 future가 실행되고 있는 경우, context는 해당 future가 끝나기를 기다리며 다음 future를 실행하지 않는 pause 상태가 됩니다. Context::waiting() 함수는 이 상태를 확인하는 용도입니다.\n다음은 바로 위에서 작성한 함수에 waiting을 확인하여 출력하도록 수정한 코드입니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 use actix::{Actor, AsyncContext, Context, Handler, Message, System, WrapFuture}; use tracing_subscriber::FmtSubscriber; struct MyActor; impl Actor for MyActor { type Context = Context\u0026lt;Self\u0026gt;; fn started(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { self.wait_future(ctx); } } impl MyActor { fn wait_future(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { tracing::info!(\u0026#34;Before wait \u0026#34;); tracing::info!(\u0026#34;is waiting: {}\u0026#34;, ctx.waiting()); ctx.wait(async { tracing::info!(\u0026#34;async block started...\u0026#34;); tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; tracing::info!(\u0026#34;async block ended...\u0026#34;); }.into_actor(self)); tracing::info!(\u0026#34;After wait \u0026#34;); tracing::info!(\u0026#34;is waiting: {}\u0026#34;, ctx.waiting()); } } #[derive(Message)] #[rtype(result=\u0026#34;bool\u0026#34;)] struct IsWaiting; impl Handler\u0026lt;IsWaiting\u0026gt; for MyActor { type Result = bool; fn handle(\u0026amp;mut self, _msg: IsWaiting, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) -\u0026gt; Self::Result { ctx.waiting() } } fn main() { let system = System::new(); system.block_on(async { let subscriber = FmtSubscriber::new(); tracing::subscriber::set_global_default(subscriber); let addr = MyActor.start(); tracing::info!(\u0026#34;actor started\u0026#34;); tracing::info!(\u0026#34;check if actor is waiting\u0026#34;); let is_waiting = addr.send(IsWaiting).await.unwrap(); tracing::info!(\u0026#34;is actor waiting: {}\u0026#34;, is_waiting); }); system.run(); } 실행 결과는 다음과 같습니다. Context::wait()이 시작된 이후로 waiting은 false 입니다. Context::wait()이 불린 순간부터 waiting()이 true가 됩니다. waiting 상태가 된 후에는 IsWaiting 메시지는 처리되지 않습니다. 이후 async block(future)이 완료된 후 waiting()이 false가 되고, IsWaiting 메시지에 응답합니다.\n1 2 3 4 5 6 7 8 9 2022-08-20T12:56:07.653870Z INFO actix_context_usage: actor started 2022-08-20T12:56:07.653898Z INFO actix_context_usage: check if actor is waiting 2022-08-20T12:56:07.653915Z INFO actix_context_usage: Before wait 2022-08-20T12:56:07.653922Z INFO actix_context_usage: is waiting: false 2022-08-20T12:56:07.653929Z INFO actix_context_usage: After wait 2022-08-20T12:56:07.653935Z INFO actix_context_usage: is waiting: true 2022-08-20T12:56:07.653942Z INFO actix_context_usage: async block started... 2022-08-20T12:56:10.656440Z INFO actix_context_usage: async block ended... 2022-08-20T12:56:10.656739Z INFO actix_context_usage: is actor waiting: false cancel_future 1 fn cancel_future(\u0026amp;mut self, handle: SpawnHandle) -\u0026gt; bool; spawn 된 future를 취소하는 함수입니다. actix repo의 contxtimpl.rs를 보면, 리턴 값은 항상 true인 것으로 보입니다.\n1 2 3 4 pub fn cancel_future(\u0026amp;mut self, handle: SpawnHandle) -\u0026gt; bool { self.handles.push(handle); true } 다음은 actor가 시작하면 두 개의 future를 spawn 하고, 이후 spawn 된 future들을 취소하는 예제입니다. 아주 단순하게, 두 future는 각각 1초, 5초를 sleep 하고, main 에서는 actor 시작 3초 후에 cancel을 시도합니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 impl MyActor { fn spawn_futures(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { let handle1 = ctx.spawn(async move { // Future 01 tracing::info!(\u0026#34;async block 1 started...\u0026#34;); tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; tracing::info!(\u0026#34;async block 1 ended...\u0026#34;); }.into_actor(self)); let handle2 = ctx.spawn(async move { // Future 02 tracing::info!(\u0026#34;async block 2 started...\u0026#34;); tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; tracing::info!(\u0026#34;async block 2 ended...\u0026#34;); }.into_actor(self)); self.spawned_tasks.push(handle1); self.spawned_tasks.push(handle2); } } #[derive(Message)] #[rtype(result=\u0026#34;()\u0026#34;)] struct CancelAllFutures; impl Handler\u0026lt;CancelAllFutures\u0026gt; for MyActor { type Result = (); fn handle(\u0026amp;mut self, _msg: CancelAllFutures, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) -\u0026gt; Self::Result { for h in \u0026amp;self.spawned_tasks { let success = ctx.cancel_future(*h); tracing::info!(\u0026#34;Cancel handle success: {}\u0026#34;, success); } } } fn main() { let system = System::new(); system.block_on(async { let subscriber = FmtSubscriber::new(); tracing::subscriber::set_global_default(subscriber); let addr = MyActor::default().start(); tracing::info!(\u0026#34;actor started\u0026#34;); tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; addr.send(CancelAllFutures).await.unwrap(); }); system.run(); } 결과는 다음과 같습니다. actor가 시작하면서 두 개의 future가 모두 시작되고, 1초만 sleep 하는 첫 번째 future는 바로 완료 됩니다. 이후 모든 future들을 취소 했고, 취소 함수의 리턴 값은 모두 true입니다. 앞에 설명한 것 처럼, 항상 true로 리턴하는 것으로 보입니다. 하지만 로그에서 볼 수 있듯 첫 번째 future는 이미 끝나 취소되지 않았고 두 번째 future는 sleep 중에 취소된 것으로 보입니다.\n1 2 3 4 5 6 2022-08-21T09:09:08.685024Z INFO actix_context_usage: actor started 2022-08-21T09:09:08.685072Z INFO actix_context_usage: async block 1 started... 2022-08-21T09:09:08.685085Z INFO actix_context_usage: async block 2 started... 2022-08-21T09:09:09.687806Z INFO actix_context_usage: async block 1 ended... 2022-08-21T09:09:11.686872Z INFO actix_context_usage: Cancel handle success: true 2022-08-21T09:09:11.687038Z INFO actix_context_usage: Cancel handle success: true 남은 함수들은 다음 편에서 살펴보겠습니다.\nReferences https:/actix/actix/blob/master/actix/src/actor.rs https:/actix/actix/blob/master/actix/src/context.rs ","permalink":"http://huijeong-kim.github.io/post/2022-08-21-actix-actor-async-ctx-1/","summary":"Actix에서 actor를 생성하면 해당 actor를 실행시키는 문맥(context)에 해당하는 Context가 생성 되고, Context는 다양한 actor의 동작을 정의합니다. 그 중 Context를 활용해서 actor가 future를 실행, 취소, 기다리는 방법을 알아봅니다. 아래 예제 코드들은 actix 0.13.0 버전을 사용했습니다.\nActor Context 먼저, Actor의 Context의 정의, 생성 시기를 알아보겠습니다.\n사용자가 정의한 struct를 actor로 만들기 위해서 최소로 필요한 것은 Actor trait을 구현(implement) 하고 Context를 정의하는 것 입니다.\n1 2 3 4 5 struct MyActor; impl Actor for MyActor { type Context = Context\u0026lt;Self\u0026gt;; } 위 actor는 다음과 같이 생성되고 시작될 수 있습니다.","title":"Actix actor의 AsyncContext 활용하기 (1)"},{"content":"Actix에서 actor를 사용하는 방법은 두 가지로 나눌 수 있을 것 같습니다. 첫 번째는 특정 task를 실행시키는 용도로, client가 message를 보내면 이를 받아서 처리하는 actor를 만드는 것 이고, 두 번째는 주기적으로 task를 실행시키는 actor를 만드는 것 입니다. 두 경우 각각 actor를 사용하는 방법에 대해 알아봅니다.\n1. Message를 처리하는 actor Actor를 시작하면 Addr가 리턴됩니다. Addr는 해당 actor에게 message를 전달할 수 있는 channel 입니다. Client module은 이 addr를 통해서 message를 전달할 수 있습니다.\n1 2 let addr = MyActor::new().start(); addr.send(Ping(10)).await?; 위 예제는 actor를 생성, 시작한 뒤 actor에게 메시지를 보내는 코드입니다. 이 때 Actor는 시작되면서 Started 상태가 됩니다. Started 상태에선 Actor::started()함수가 불린 뒤 Running 상태, 즉 message 를 처리할 수 있는 상태가 됩니다. 이후 Actor는 다음 중 한 가지 조건이 만족되면 Stopping 상태가 되고 종료됩니다.\nActor 자신이 Context::stop 을 불렀을 때 Actor의 모든 Addr가 drop 되었을 때 Context에 등록 된 evented object가 없을 때 특정 Message를 처리하는 용도로 actor를 사용할 땐, 의도적으로 Context::stop을 호출했을 때 혹은 Addr를 사용하는 client가 없을 때 actor는 멈추게 됩니다. 즉 actor를 멈추기 위해선 Context::stop을 호출하거나, 사용 중인 Addr를 모두 drop하면 됩니다. 참고로 Actor를 시작한 후 리턴 된 Addr를 사용하지 않는다면(다른 variable에 bind하지 않는다면) actor는 Actor::started 실행 후 바로 Stopping 상태가 될 수 있습니다.\n세번째 경우는 아래에서 설명됩니다.\n2. 주기적으로 task를 실행하는 actor 주기적인 task를 실행하는 actor의 대표적인 예는 자기 자신 혹은 외부의 상태를 주기적으로 확인 하는 heartbeat 입니다.\n이를 가장 naive하게 구현하면, 특정 주기마다 actor에게 message를 보내 task를 수행하게 할 수 있습니다. 아래는 actix repo의 ping example을 주기적으로 ping을 보내도록 수정한 코드입니다. main 함수에서 특정 주기로 ping message를 보내도록 했습니다.\n여기서 주의할 점은, std::thread::sleep이 아닌 tokio::time::sleep을 사용했다는 점 입니다. std::thread::sleep을 통해 sleep하는 경우, 현재 thread가 sleep 상태에 빠져 Actix System이 다른 future들을 실행시킬 수 없습니다. Single thread를 사용하는 기본 Actix System의 경우, application 전체가 block 상태가 됩니다. tokio::time::sleep은 현재 thread가 아닌 현재 future만 sleep 시켜, 전체 system을 block시키지 않습니다.\n1 2 3 4 5 6 7 8 9 10 11 fn main() { System::new().block_on(async { let addr = MyActor { count: 10 }.start(); loop { let res = addr.send(Ping(10)).await; println!(\u0026#34;RESULT: {}\u0026#34;, res.unwrap()); tokio::time::sleep(tokio::time::Duration::from_secs(3)).await; } }); } 하지만 Actix에는 이 경우 사용할 수 있는 run_interval() 함수가 있습니다. 다음과 같이 Actor의 started()에서 run_interval()을 통해 주기적으로 실행시킬 함수를 등록해 놓는다면, 위와 같은 loop를 작성하지 않아도 됩니다.\n1 2 3 4 5 6 7 fn started(\u0026amp;mut self, ctx: \u0026amp;mut Self::Context) { println!(\u0026#34;Actor started\u0026#34;); ctx.run_interval(std::time::Duration::from_secs(3), |_, _| { MyActor::do_something(); }); } 이 경우 Actor의 Addr가 사용되지 않아도 actor는 멈추지 않습니다. 앞에서 언급한 Actor가 Stopping 상태가 되는 조건 중 세 번째를 충족시키지 않기 때문, 즉 evented object 가 등록된 상태이기 때문입니다.\n1 2 3 4 5 6 fn run_interval\u0026lt;F\u0026gt;(\u0026amp;mut self, dur: Duration, f: F) -\u0026gt; SpawnHandle where F: FnMut(\u0026amp;mut A, \u0026amp;mut A::Context) + \u0026#39;static, { self.spawn(IntervalFunc::new(dur, f).finish()) } run_interval() 함수를 조금 더 살펴봅시다. 함수는 다음과 같이 실행 주기에 해당하는 Duration과 실행시킬 함수, f를 인자로 받습니다.f는 FnMut(\u0026amp;mut A, \u0026amp;mut A::Context) + 'static 을 만족해야 합니다. 여기서 A는 Actor\u0026lt;Context=Self\u0026gt; 입니다. 결국 f는 Actor와 Actor::Context를 인자로 받는 FnMut여야 합니다.\n위의 예제에서 |_, _| {}로 구현된 closure가 이 타입이며, 사용되지 않은 두 인자는 Actor와 Actor::Context 입니다.\n이를 활용하여 다양한 함수를 주기적으로 호출되도록 등록할 수 있습니다. 여기서 만약, 주기적으로 실행되어야 하는 동작이 async 함수라면 어떻게 해야 할까요? 현재 Rust는 async closure를 지원하지 않습니다. run_interval()함수의 두 번째 인자인 f도 Future 타입은 아니죠.\n해결하는 방법은 지난 글에서 사용한 방식과 동일합니다. fn에서 future를 spawn하면 됩니다. 지난 글처럼 해당 context에 spawn 하거나, actix system(현재 arbiter)에 spawn 할 수 있습니다.\n1 2 3 4 5 6 7 let fut = async move { do_something().await; }; let actor = fut.into_actor(fut); ctx.spawn(actor); // 방법 1, 현재 context에 spawn 하기 actix::spawn(fut); // 방법 2, 현재 system에 spawn 하기 아래 코드는 지금까지 설명한 run_interval을 다양한 방식으로 사용하는 코드 예시입니다. 1) 주기적으로 associated func을 호출하는 경우, 2) 주기적으로 struct method를 호출하는 경우, 그리고 3) 주기적으로 async method를 호출하는 경우입니다.\n아래 코드에서 특이한(?) 포인트는 async method를 호출할 때, send_ping 함수에서 self.clone()를 하는 건데요. async block 내에서 self의 async_send_ping를 호출하고 있기 때문 입니다. 여기서 self가 async block 내로 move될 수 없기 때문에 clone 하였습니다. 이 부분은 아래에서 좀 더 설명하겠습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 use actix::{Actor, Addr, AsyncContext, Context, Handler, Message}; /// Define `Ping` message #[derive(Default, Message)] #[rtype(result = \u0026#34;usize\u0026#34;)] struct Ping(usize); /// Actor #[derive(Clone, Debug)] struct MyActor { count: usize, } /// Declare actor and its context impl Actor for MyActor { type Context = Context\u0026lt;Self\u0026gt;; fn started(\u0026amp;mut self, ctx: \u0026amp;mut Self::Context) { println!(\u0026#34;Actor started\u0026#34;); ctx.run_interval(std::time::Duration::from_secs(3), |_act, _ctx| { MyActor::run_associated_func(); }); ctx.run_interval(std::time::Duration::from_secs(1), |act, _ctx| { act.run_func(); }); ctx.run_interval(std::time::Duration::from_secs(1), |act, ctx| { act.send_ping(ctx); }); } fn stopped(\u0026amp;mut self, _ctx: \u0026amp;mut Self::Context) { println!(\u0026#34;Actor stopped\u0026#34;); } } impl MyActor { pub fn new(count: usize) -\u0026gt; Self { Self { count, } } fn run_associated_func() { println!(\u0026#34;CASE 1. run associated func\u0026#34;); } fn run_func(\u0026amp;self) { println!(\u0026#34;CASE 2. run func, count {}\u0026#34;, self.count); } fn send_ping(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { let actor = self.clone(); let address = ctx.address(); let fut = async move { actor.async_send_ping(address).await; }; actix::spawn(fut); } async fn async_send_ping(\u0026amp;self, addr: Addr\u0026lt;MyActor\u0026gt;) { addr.send(Ping(10)).await.unwrap(); println!(\u0026#34;CASE 3. Send ping, now count is {}\u0026#34;, self.count); } } /// Handler for `Ping` message impl Handler\u0026lt;Ping\u0026gt; for MyActor { type Result = usize; fn handle(\u0026amp;mut self, msg: Ping, _: \u0026amp;mut Context\u0026lt;Self\u0026gt;) -\u0026gt; Self::Result { self.count += msg.0; self.count } } fn main() { let system = actix::System::new(); system.block_on(async move { MyActor::new(0).start(); }); system.run().unwrap(); } 실행 결과\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 Actor started CASE 2. run func, count 0 CASE 3. Send ping, now count is 0 CASE 2. run func, count 10 CASE 3. Send ping, now count is 10 CASE 1. run associated func CASE 2. run func, count 20 CASE 3. Send ping, now count is 20 CASE 2. run func, count 30 CASE 3. Send ping, now count is 30 CASE 2. run func, count 40 CASE 3. Send ping, now count is 40 CASE 1. run associated func CASE 2. run func, count 50 CASE 3. Send ping, now count is 50 CASE 2. run func, count 60 CASE 3. Send ping, now count is 60 CASE 2. run func, count 70 CASE 3. Send ping, now count is 70 CASE 1. run associated func CASE 2. run func, count 80 CASE 3. Send ping, now count is 80 CASE 2. run func, count 90 CASE 3. Send ping, now count is 90 (생략) 결과를 보면, send_async_ping 에서 출력한 count가 현재 ping으로 인해 증가된 값이 반영되지 않는 문제가 있습니다. 위의 코드에서 self.clone()을 하면서 의도하지 않은 동작을 하게 만들었습니다. 이 상태를 분석하기 위해 위의 코드에서 send_ping(), send_async_ping() method에 현재 사용하고 있는 actor의 주소를 출력하도록 수정해 봤습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn send_ping(\u0026amp;mut self, ctx: \u0026amp;mut Context\u0026lt;Self\u0026gt;) { println!(\u0026#34;NOW I WILL START SEND PING: {:p}, {:?}\u0026#34;, \u0026amp;self, self); let actor = self.clone(); let address = ctx.address(); let fut = async move { actor.async_send_ping(address).await; }; actix::spawn(fut); } async fn async_send_ping(\u0026amp;self, addr: Addr\u0026lt;MyActor\u0026gt;) { println!(\u0026#34;NOW I WILL SEND PING: {:p}, {:?}\u0026#34;, \u0026amp;self, self); addr.send(Ping(10)).await.unwrap(); println!(\u0026#34;NOW I WILL SEND PING TO SELF COMPLETED: {:p}, {:?}\u0026#34;, \u0026amp;self, self); println!(\u0026#34;Send ping, now count is {}\u0026#34;, self.count); } 실행 시 출력은 다음과 같습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 Actor started NOW I WILL START SEND PING: 0x16fa78608, MyActor { count: 0 } NOW I WILL SEND PING: 0x130605ee0, MyActor { count: 0 } NOW I WILL SEND PING TO SELF COMPLETED: 0x130605ee0, MyActor { count: 0 } Send ping, now count is 0 NOW I WILL START SEND PING: 0x16fa78608, MyActor { count: 10 } NOW I WILL SEND PING: 0x1306062e0, MyActor { count: 10 } NOW I WILL SEND PING TO SELF COMPLETED: 0x1306062e0, MyActor { count: 10 } Send ping, now count is 10 NOW I WILL START SEND PING: 0x16fa78608, MyActor { count: 20 } NOW I WILL SEND PING: 0x130707de0, MyActor { count: 20 } NOW I WILL SEND PING TO SELF COMPLETED: 0x130707de0, MyActor { count: 20 } Send ping, now count is 20 (생략) 0x1318043e0 주소를 가진 actor가 시작되고, 이 actor가 run_interval()을 통해 반복적으로 불립니다. 하지만 send_ping() 함수에서 self.clone() 이 되면 매 번 새로운 주소의 actor가 생성된 후 동작합니다. Ping 요청 시 처음 시작한 actor의 address를 전달하고 있기 때문에, 처음에 0x1318043e0에 만들어 진 actor가 ping message를 처리하고 있긴 합니다.\n이 경우는 Addr를 전달하고 있기 때문에 원하는 대로 주기적으로 count가 올라가는 동작을 구현할 수 있었는데, 주기적으로 자기 자신의 상태를 변경시키고, 그 과정에서 async 동작이 필요한 경우에는 위 방법을 활용하기 어렵겠습니다.\nReferences https:/actix/actix/blob/master/actix/src/actor.rs https://docs.rs/actix/latest/actix/prelude/trait.AsyncContext.html#method.run_interval ","permalink":"http://huijeong-kim.github.io/post/2022-08-17-actix-actor-usage/","summary":"Actix에서 actor를 사용하는 방법은 두 가지로 나눌 수 있을 것 같습니다. 첫 번째는 특정 task를 실행시키는 용도로, client가 message를 보내면 이를 받아서 처리하는 actor를 만드는 것 이고, 두 번째는 주기적으로 task를 실행시키는 actor를 만드는 것 입니다. 두 경우 각각 actor를 사용하는 방법에 대해 알아봅니다.\n1. Message를 처리하는 actor Actor를 시작하면 Addr가 리턴됩니다. Addr는 해당 actor에게 message를 전달할 수 있는 channel 입니다. Client module은 이 addr를 통해서 message를 전달할 수 있습니다.","title":"Actix actor를 사용하는 두 가지 방법"},{"content":"Rust에서 사용 된 macro의 구현이 궁금할 때 cargo expand를 사용할 수 있습니다.\n설치 $ cargo install cargo-expand 사용법 이전 글에서 사용한 코드를 expand해 봅시다. cargo expand 시 expand 된 코드가 stdout으로 출력됩니다. $ cargo expand --bin actix-and-tonic\n이 예제는 하나의 파일로 이루어져 있어 binary를 지정했지만, 특정 module 코드를 expand하기 위해서 module을 지정할 수도 있습니다. $ cargo expand path::to:module\nMessage 구현에 사용했던 Message macro는\n1 2 3 #[derive(Default, Message)] #[rtype(result = \u0026#34;String\u0026#34;)] struct GetName; 이렇게 풀어쓸 수 있습니다.\n1 2 3 4 5 6 7 8 9 impl ::core::default::Default for GetName { #[inline] fn default() -\u0026gt; GetName { GetName {} } } impl ::actix::Message for GetName { type Result = String; } derive(Default)에 의해 Default 구현이 추가되었고, derive(Message)에 의해 Message 구현이 추가 되었습니다. rtype(result=\u0026quot;String\u0026quot;)은 그대로 type Result = String으로 변환됨을 확인할 수 있습니다.\n","permalink":"http://huijeong-kim.github.io/post/2022-08-14-expand-rust-macros/","summary":"Rust에서 사용 된 macro의 구현이 궁금할 때 cargo expand를 사용할 수 있습니다.\n설치 $ cargo install cargo-expand 사용법 이전 글에서 사용한 코드를 expand해 봅시다. cargo expand 시 expand 된 코드가 stdout으로 출력됩니다. $ cargo expand --bin actix-and-tonic\n이 예제는 하나의 파일로 이루어져 있어 binary를 지정했지만, 특정 module 코드를 expand하기 위해서 module을 지정할 수도 있습니다. $ cargo expand path::to:module\nMessage 구현에 사용했던 Message macro는\n1 2 3 #[derive(Default, Message)] #[rtype(result = \u0026#34;String\u0026#34;)] struct GetName; 이렇게 풀어쓸 수 있습니다.","title":"Expand Rust Macros"},{"content":"Rust로 gRPC로 외부와 통신하는 Actix 기반 application을 작성하는 방법을 알아봅니다. gRPC Server는 Tonic을 사용해 구현합니다. Tonic과 Actix는 둘 다 tokio 기반 application으로, tokio Runtime 위에서 동작합니다. 같은 tokio Runtime에 gRPC Server와 Actor System을 실행시켜 보겠습니다.\nTonic을 사용해 gRPC Server 시작하기 Tonic을 사용하면 다음과 같은 간단한 Server 실행 코드를 포함한 Async Block(Future)을 Tokio Runtime에서 실행하여 gRPC Server를 시작할 수 있습니다. 아래 코드는 Tonic repo 내 helloworld exmaple에서 tonic::main 매크로를 풀어 쓴 것입니다.\n1 2 3 4 5 6 7 8 9 10 11 12 fn main() -\u0026gt; Result\u0026lt;(), Box\u0026lt;dyn std::error::Error\u0026gt;\u0026gt; { tokio::runtime::Runtime::new().unwrap().block_on(async { let addr = \u0026#34;[::1]:50051\u0026#34;.parse().unwrap(); let greeter = MyGreeter::default(); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await?; Ok(()) }) } Actix System Actix의 System은 Per-thread Async Runtime Manager입니다. Actix System을 생성하면 Tokio Runtime과 이 위에서 Actor들이 실행될 수 있는 환경이 생성됩니다. 다음은 Actix System을 시작하고 Actor를 시작, 생성한 Actor에게 message를 보내는 코드입니다. Actix repo 내 ping exmaple을 가져왔습니다.\n1 2 3 4 5 6 7 8 9 fn main() { let system = actix::System::new(); system.block_on(async move { let addr = MyActor { count: 10 }.start(); let res = addr.send(Ping(10)).await; println!(\u0026#34;RESULT: {}\u0026#34;, res.unwrap() == 20); }); system.run().unwrap(); } 위 코드는 block_on을 통해 필요한 Actor를 생성하고 메시지를 보냅니다. 이는 바로 실행되고, 실행 완료될 때 까지 이 thread는 block 됩니다.\nsystem.run()은 System의 event loop을 시작하는 코드입니다. 위의 단순한 예제에서는 Actor 를 시작하고 Actor에 메시지를 보내는 동작이 block_on에서 모두 실행되고 있어 event loop 시작이 크게 의미가 없습니다. 하지만 특정 요청에 의해 Actor 동작이 실행되는 경우(Actor가 다른 Actor에게 연속적으로 message를 보내거나 gRPC 와 같은 외부 요청에 응답해야 하는 경우 등)에는 event loop 이 시작 되어야 실행될 수 있습니다.\nTonic Server를 실행시키는 Actix Actor 만들기 이제 위에서 작성한 Greeter Server를 Actix Actor로 만드는 간단한 예제를 작성해 봅니다. MyGreeter가 Hello gRPC 응답 메시지를 보낼 때 MyActor로부터 name을 받아 응답 메시지를 만들도록 합니다.\n다음 코드에는 두 개의 Actor가 있습니다. MyActor는 이름(name)을 갖고 있고, GetName message에 응답합니다. MyServer는 gRPC Service들을 시작합니다.\nActor가 시작 되면 started가 불리므로, MyServer의 started 함수에서 gRPC Service를 시작합니다. 이 started 함수는 async 함수가 아니므로 바로 Server를 시작할 수 없습니다. 따라서 Server를 시작하는 Future를 만들어 spawn 합니다. Spawn 된 Future는 Actix System의 event loop가 시작되면(run) 시작될 수 있습니다.\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 use actix::prelude::*; use hello_world::greeter_server::{Greeter, GreeterServer}; use hello_world::{HelloReply, HelloRequest}; use tonic::{transport::Server, Request, Response, Status}; pub mod hello_world { tonic::include_proto!(\u0026#34;helloworld\u0026#34;); } struct MyServer { addr: String, name: Addr\u0026lt;MyActor\u0026gt;, } impl Actor for MyServer { type Context = Context\u0026lt;Self\u0026gt;; fn started(\u0026amp;mut self, ctx: \u0026amp;mut Self::Context) { let greeter = MyGreeter { name: self.name.clone(), }; let addr = self.addr.parse().unwrap(); let start_server_fut = async move { println!(\u0026#34;Start grpc server: addr {}\u0026#34;, addr); Server::builder() .add_service(GreeterServer::new(greeter)) .serve(addr) .await.unwrap(); }; let start_server_actor = start_server_fut.into_actor(self); ctx.spawn(start_server_actor); } } pub struct MyGreeter { name: Addr\u0026lt;MyActor\u0026gt;, } #[tonic::async_trait] impl Greeter for MyGreeter { async fn say_hello( \u0026amp;self, request: Request\u0026lt;HelloRequest\u0026gt;, ) -\u0026gt; Result\u0026lt;Response\u0026lt;HelloReply\u0026gt;, Status\u0026gt; { println!(\u0026#34;Got a request from {:?}\u0026#34;, request.remote_addr()); let response = self.name.send(GetName::default()).await.unwrap(); let reply = hello_world::HelloReply { message: format!(\u0026#34;Hello {}!, from {:?}\u0026#34;, request.into_inner().name, response), }; Ok(Response::new(reply)) } } #[derive(Default, Message)] #[rtype(result = \u0026#34;String\u0026#34;)] struct GetName; struct MyActor{ name: String, } impl Actor for MyActor { type Context = Context\u0026lt;Self\u0026gt;; } impl Handler\u0026lt;GetName\u0026gt; for MyActor { type Result = String; fn handle(\u0026amp;mut self, msg: GetName, ctx: \u0026amp;mut Self::Context) -\u0026gt; Self::Result { self.name.clone() } } fn main() { let system = System::new(); system.block_on(async { let actor_name = MyActor { name: String::from(\u0026#34;MY_ACTOR\u0026#34;), }.start(); MyServer { addr: \u0026#34;[::1]:50051\u0026#34;.to_string(), name: actor_name, } .start(); }); system.run().unwrap(); } Application을 실행한 후 gRPC message를 보낸 결과는 다음과 같습니다.\nServer 출력 Start grpc server: addr [::1]:50051 Got a request from Some([::1]:50408)\nClient가 받은 응답 RESPONSE=Response { metadata: MetadataMap { headers: {\u0026#34;content-type\u0026#34;: \u0026#34;application/grpc\u0026#34;, \u0026#34;date\u0026#34;: \u0026#34;Sat, 13 Aug 2022 16:46:00 GMT\u0026#34;, \u0026#34;grpc-status\u0026#34;: \u0026#34;0\u0026#34;} }, message: HelloReply { message: \u0026#34;Hello Tonic!, from \\\u0026#34;MY_ACTOR\\\u0026#34;\u0026#34; }, extensions: Extensions }\nReferences https:/hyperium/tonic https://docs.rs/tokio/0.1.22/tokio/runtime https://docs.rs/actix/latest/actix/struct.System.html\n","permalink":"http://huijeong-kim.github.io/post/2022-08-14-run-grpc-server-in-actix/","summary":"Rust로 gRPC로 외부와 통신하는 Actix 기반 application을 작성하는 방법을 알아봅니다. gRPC Server는 Tonic을 사용해 구현합니다. Tonic과 Actix는 둘 다 tokio 기반 application으로, tokio Runtime 위에서 동작합니다. 같은 tokio Runtime에 gRPC Server와 Actor System을 실행시켜 보겠습니다.\nTonic을 사용해 gRPC Server 시작하기 Tonic을 사용하면 다음과 같은 간단한 Server 실행 코드를 포함한 Async Block(Future)을 Tokio Runtime에서 실행하여 gRPC Server를 시작할 수 있습니다. 아래 코드는 Tonic repo 내 helloworld exmaple에서 tonic::main 매크로를 풀어 쓴 것입니다.","title":"Actix에서 gRPC Server 시작하기"}]