diff --git a/ch05-02-example-structs.html b/ch05-02-example-structs.html index 00fb99000e..7b62c6811e 100644 --- a/ch05-02-example-structs.html +++ b/ch05-02-example-structs.html @@ -450,7 +450,7 @@

부록 C에서 확인할 수 있으니 참고해 주세요. 또한, 여러분만의 트레이트를 직접 만들고, 이런 트레이트의 동작을 커스터마이징해서 구현하는 방법은 10장에서 배울 예정입니다. -또한 device 외에도 여러 가지 속성들이 있습니다; 더 많은 정보는 +또한 derive 외에도 여러 가지 속성들이 있습니다; 더 많은 정보는 러스트 참고 자료의 ‘속성 (attributes)’절을 살펴보세요.

만들어진 area 함수는 사각형의 면적만을 계산합니다. Rectangle 구조체를 제외한 다른 타입으로는 작동하지 않으니 diff --git a/print.html b/print.html index fa2a1589fc..9d14c99e18 100644 --- a/print.html +++ b/print.html @@ -5072,7 +5072,7 @@

부록 C에서 확인할 수 있으니 참고해 주세요. 또한, 여러분만의 트레이트를 직접 만들고, 이런 트레이트의 동작을 커스터마이징해서 구현하는 방법은 10장에서 배울 예정입니다. -또한 device 외에도 여러 가지 속성들이 있습니다; 더 많은 정보는 +또한 derive 외에도 여러 가지 속성들이 있습니다; 더 많은 정보는 러스트 참고 자료의 ‘속성 (attributes)’절을 살펴보세요.

만들어진 area 함수는 사각형의 면적만을 계산합니다. Rectangle 구조체를 제외한 다른 타입으로는 작동하지 않으니 diff --git a/searchindex.js b/searchindex.js index 2f6eb75805..176af82341 100644 --- a/searchindex.js +++ b/searchindex.js @@ -1 +1 @@ -Object.assign(window.search, {"doc_urls":["title-page.html#the-rust-programming-language","foreword.html#들어가기에-앞서","ch00-00-introduction.html#소개","ch00-00-introduction.html#러스트는-누구에게-적합할까요","ch00-00-introduction.html#개발팀","ch00-00-introduction.html#학생","ch00-00-introduction.html#회사","ch00-00-introduction.html#오픈-소스-개발자","ch00-00-introduction.html#속도와-안정성을-중시하는-사람","ch00-00-introduction.html#이-책은-어떤-사람을-위한-책인가요","ch00-00-introduction.html#이-책을-어떻게-읽어야-할까요","ch00-00-introduction.html#소스-코드","ch01-00-getting-started.html#시작해봅시다","ch01-01-installation.html#러스트-설치","ch01-01-installation.html#커맨드-라인-표기","ch01-01-installation.html#rustup-설치---linux-및-macos","ch01-01-installation.html#rustup-설치---windows","ch01-01-installation.html#트러블-슈팅","ch01-01-installation.html#업데이트-및-삭제","ch01-01-installation.html#로컬-문서","ch01-02-hello-world.html#hello-world","ch01-02-hello-world.html#프로젝트-디렉터리-생성하기","ch01-02-hello-world.html#러스트-프로그램-작성하고-실행하기","ch01-02-hello-world.html#러스트-프로그램-뜯어보기","ch01-02-hello-world.html#컴파일과-실행은-별개의-과정입니다","ch01-03-hello-cargo.html#카고를-사용해봅시다","ch01-03-hello-cargo.html#카고로-프로젝트-생성하기","ch01-03-hello-cargo.html#카고로-프로젝트를-빌드하고-실행하기","ch01-03-hello-cargo.html#릴리즈-빌드-생성하기","ch01-03-hello-cargo.html#관례로서의-카고","ch01-03-hello-cargo.html#정리","ch02-00-guessing-game-tutorial.html#추리-게임","ch02-00-guessing-game-tutorial.html#새로운-프로젝트를-준비하기","ch02-00-guessing-game-tutorial.html#추릿값을-처리하기","ch02-00-guessing-game-tutorial.html#변수에-값-저장하기","ch02-00-guessing-game-tutorial.html#사용자-입력받기","ch02-00-guessing-game-tutorial.html#result-타입으로-잠재적-실패-다루기","ch02-00-guessing-game-tutorial.html#println-자리표시자를-이용한-값-출력하기","ch02-00-guessing-game-tutorial.html#첫-번째-부분-테스트하기","ch02-00-guessing-game-tutorial.html#비밀번호를-생성하기","ch02-00-guessing-game-tutorial.html#크레이트를-사용하여-더-많은-기능-가져오기","ch02-00-guessing-game-tutorial.html#임의의-숫자-생성하기","ch02-00-guessing-game-tutorial.html#비밀번호와-추릿값을-비교하기","ch02-00-guessing-game-tutorial.html#반복문을-이용하여-여러-번의-추리-허용하기","ch02-00-guessing-game-tutorial.html#정답을-맞힌-후-종료하기","ch02-00-guessing-game-tutorial.html#잘못된-입력값-처리하기","ch02-00-guessing-game-tutorial.html#정리","ch03-00-common-programming-concepts.html#일반적인-프로그래밍-개념","ch03-01-variables-and-mutability.html#변수와-가변성","ch03-01-variables-and-mutability.html#상수","ch03-01-variables-and-mutability.html#섀도잉","ch03-02-data-types.html#데이터-타입","ch03-02-data-types.html#스칼라-타입","ch03-02-data-types.html#복합-타입","ch03-03-how-functions-work.html#함수","ch03-03-how-functions-work.html#매개변수","ch03-03-how-functions-work.html#구문과-표현식","ch03-03-how-functions-work.html#반환-값을-갖는-함수","ch03-04-comments.html#주석","ch03-05-control-flow.html#제어-흐름문","ch03-05-control-flow.html#if-표현식","ch03-05-control-flow.html#반복문을-이용한-반복","ch03-05-control-flow.html#정리","ch04-00-understanding-ownership.html#소유권-이해하기","ch04-01-what-is-ownership.html#소유권이-뭔가요","ch04-01-what-is-ownership.html#스택-영역과-힙-영역","ch04-01-what-is-ownership.html#소유권-규칙","ch04-01-what-is-ownership.html#변수의-스코프","ch04-01-what-is-ownership.html#string-타입","ch04-01-what-is-ownership.html#메모리와-할당","ch04-01-what-is-ownership.html#소유권과-함수","ch04-01-what-is-ownership.html#반환-값과-스코프","ch04-02-references-and-borrowing.html#참조와-대여","ch04-02-references-and-borrowing.html#가변-참조자","ch04-02-references-and-borrowing.html#댕글링-참조","ch04-02-references-and-borrowing.html#참조자-규칙","ch04-03-slices.html#슬라이스","ch04-03-slices.html#문자열-슬라이스","ch04-03-slices.html#그-외-슬라이스","ch04-03-slices.html#정리","ch05-00-structs.html#구조체로-연관된-데이터를-구조화하기","ch05-01-defining-structs.html#구조체-정의-및-인스턴트화","ch05-01-defining-structs.html#필드-초기화-축약법-사용하기","ch05-01-defining-structs.html#기존-인스턴스를-이용해-새-인스턴스를-만들-때-구조체-업데이트-문법-사용하기","ch05-01-defining-structs.html#명명된-필드-없는-튜플-구조체를-사용하여-다른-타입-만들기","ch05-01-defining-structs.html#필드가-없는-유사-유닛-구조체","ch05-01-defining-structs.html#구조체-데이터의-소유권","ch05-02-example-structs.html#구조체를-사용한-예제-프로그램","ch05-02-example-structs.html#튜플로-리팩터링하기","ch05-02-example-structs.html#구조체로-리팩터링하여-코드에-더-많은-의미를-담기","ch05-02-example-structs.html#트레이트-파생으로-유용한-기능-추가하기","ch05-03-method-syntax.html#메서드-문법","ch05-03-method-syntax.html#메서드-정의하기","ch05-03-method-syntax.html#--연산자는-없나요","ch05-03-method-syntax.html#더-많은-매개변수를-가진-메서드","ch05-03-method-syntax.html#연관-함수","ch05-03-method-syntax.html#여러-개의-impl-블록","ch05-03-method-syntax.html#정리","ch06-00-enums.html#열거형과-패턴-매칭","ch06-01-defining-an-enum.html#열거형-정의하기","ch06-01-defining-an-enum.html#열거형-값","ch06-01-defining-an-enum.html#option-열거형이-널-값보다-좋은-점들","ch06-02-match.html#match-제어-흐름-구조","ch06-02-match.html#값을-바인딩하는-패턴","ch06-02-match.html#option를-이용하는-매칭","ch06-02-match.html#매치는-철저합니다","ch06-02-match.html#포괄-패턴과-_-자리표시자","ch06-03-if-let.html#if-let을-사용한-간결한-제어-흐름","ch06-03-if-let.html#정리","ch07-00-managing-growing-projects-with-packages-crates-and-modules.html#커져-가는-프로젝트를-패키지-크레이트-모듈로-관리하기","ch07-01-packages-and-crates.html#패키지와-크레이트","ch07-02-defining-modules-to-control-scope-and-privacy.html#모듈을-정의하여-스코프-및-공개-여부-제어하기","ch07-02-defining-modules-to-control-scope-and-privacy.html#모듈-치트-시트","ch07-02-defining-modules-to-control-scope-and-privacy.html#모듈로-관련된-코드-묶기","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#경로를-사용하여-모듈-트리의-아이템-참조하기","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#pub-키워드로-경로-노출하기","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#super로-시작하는-상대-경로","ch07-03-paths-for-referring-to-an-item-in-the-module-tree.html#구조체-열거형을-공개하기","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#use-키워드로-경로를-스코프-안으로-가져오기","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#보편적인-use-경로-작성법","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#as-키워드로-새로운-이름-제공하기","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#pub-use로-다시-내보내기","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#외부-패키지-사용하기","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#중첩-경로를-사용하여-대량의-use-나열을-정리하기","ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#글롭-연산자","ch07-05-separating-modules-into-different-files.html#별개의-파일로-모듈-분리하기","ch07-05-separating-modules-into-different-files.html#대체-파일-경로","ch07-05-separating-modules-into-different-files.html#정리","ch08-00-common-collections.html#일반적인-컬렉션","ch08-01-vectors.html#벡터에-여러-값의-목록-저장하기","ch08-01-vectors.html#새-벡터-만들기","ch08-01-vectors.html#벡터-업데이트하기","ch08-01-vectors.html#벡터-요소-읽기","ch08-01-vectors.html#벡터-값에-대해-반복하기","ch08-01-vectors.html#열거형을-이용해-여러-타입-저장하기","ch08-01-vectors.html#벡터가-버려지면-벡터의-요소도-버려집니다","ch08-02-strings.html#문자열에-utf-8-텍스트-저장하기","ch08-02-strings.html#문자열이-뭔가요","ch08-02-strings.html#새로운-문자열-생성하기","ch08-02-strings.html#문자열-업데이트하기","ch08-02-strings.html#문자열-내부의-인덱싱","ch08-02-strings.html#문자열-슬라이싱하기","ch08-02-strings.html#문자열에-대한-반복을-위한-메서드","ch08-02-strings.html#문자열은-그렇게-단순하지-않습니다","ch08-03-hash-maps.html#해시맵에-서로-연관된-키와-값-저장하기","ch08-03-hash-maps.html#새로운-해시맵-생성하기","ch08-03-hash-maps.html#해시맵의-값-접근하기","ch08-03-hash-maps.html#해시맵과-소유권","ch08-03-hash-maps.html#해시맵-업데이트하기","ch08-03-hash-maps.html#해시-함수","ch08-03-hash-maps.html#정리","ch09-00-error-handling.html#에러-처리","ch09-01-unrecoverable-errors-with-panic.html#panic으로-복구-불가능한-에러-처리하기","ch09-01-unrecoverable-errors-with-panic.html#panic에-대응하여-스택을-되감거나-그만두기","ch09-01-unrecoverable-errors-with-panic.html#panic-백트레이스-이용하기","ch09-02-recoverable-errors-with-result.html#result로-복구-가능한-에러-처리하기","ch09-02-recoverable-errors-with-result.html#서로-다른-에러에-대해-매칭하기","ch09-02-recoverable-errors-with-result.html#result와-match-사용에-대한-대안","ch09-02-recoverable-errors-with-result.html#에러-발생-시-패닉을-위한-숏컷-unwrap과-expect","ch09-02-recoverable-errors-with-result.html#에러-전파하기","ch09-02-recoverable-errors-with-result.html#에러를-전파하기-위한-숏컷-","ch09-02-recoverable-errors-with-result.html#-연산자가-사용될-수-있는-곳","ch09-03-to-panic-or-not-to-panic.html#panic이냐-panic이-아니냐-그것이-문제로다","ch09-03-to-panic-or-not-to-panic.html#예제-프로토타입-코드-그리고-테스트","ch09-03-to-panic-or-not-to-panic.html#여러분이-컴파일러보다-더-많은-정보를-가지고-있을-때","ch09-03-to-panic-or-not-to-panic.html#에러-처리를-위한-가이드라인","ch09-03-to-panic-or-not-to-panic.html#유효성을-위한-커스텀-타입-생성하기","ch09-03-to-panic-or-not-to-panic.html#정리","ch10-00-generics.html#제네릭-타입-트레이트-라이프타임","ch10-00-generics.html#함수를-추출하여-중복-없애기","ch10-01-syntax.html#제네릭-데이터-타입","ch10-01-syntax.html#제네릭-함수-정의","ch10-01-syntax.html#제네릭-구조체-정의","ch10-01-syntax.html#제네릭-열거형-정의","ch10-01-syntax.html#제네릭-메서드-정의","ch10-01-syntax.html#제네릭-코드의-성능","ch10-02-traits.html#트레이트로-공통된-동작을-정의하기","ch10-02-traits.html#트레이트-정의하기","ch10-02-traits.html#특정-타입에-트레이트-구현하기","ch10-02-traits.html#기본-구현","ch10-02-traits.html#매개변수로서의-트레이트","ch10-02-traits.html#트레이트를-구현하는-타입을-반환하기","ch10-02-traits.html#트레이트-바운드를-사용해-조건부로-메서드-구현하기","ch10-03-lifetime-syntax.html#라이프타임으로-참조자의-유효성-검증하기","ch10-03-lifetime-syntax.html#라이프타임으로-댕글링-참조-방지하기","ch10-03-lifetime-syntax.html#대여-검사기","ch10-03-lifetime-syntax.html#함수에서의-제네릭-라이프타임","ch10-03-lifetime-syntax.html#라이프타임-명시-문법","ch10-03-lifetime-syntax.html#함수-시그니처에서-라이프타임-명시하기","ch10-03-lifetime-syntax.html#라이프타임의-측면에서-생각하기","ch10-03-lifetime-syntax.html#구조체-정의에서-라이프타임-명시하기","ch10-03-lifetime-syntax.html#라이프타임-생략","ch10-03-lifetime-syntax.html#메서드-정의에서-라이프타임-명시하기","ch10-03-lifetime-syntax.html#정적-라이프타임","ch10-03-lifetime-syntax.html#제네릭-타입-매개변수-트레이트-바운드-라이프타임을-한-곳에-사용해-보기","ch10-03-lifetime-syntax.html#정리","ch11-00-testing.html#자동화-테스트-작성하기","ch11-01-writing-tests.html#테스트-작성-방법","ch11-01-writing-tests.html#테스트-함수-파헤치기","ch11-01-writing-tests.html#assert-매크로로-결과-검사하기","ch11-01-writing-tests.html#assert_eq-assert_ne-매크로를-이용한-동등-테스트","ch11-01-writing-tests.html#커스텀-실패-메시지-추가하기","ch11-01-writing-tests.html#should_panic-매크로로-패닉-발생-검사하기","ch11-01-writing-tests.html#result를-이용한-테스트","ch11-02-running-tests.html#테스트-실행-방법-제어하기","ch11-02-running-tests.html#테스트를-병렬-혹은-순차적으로-실행하기","ch11-02-running-tests.html#함수-출력-표시하기","ch11-02-running-tests.html#이름을-지정해-일부-테스트만-실행하기","ch11-02-running-tests.html#특별-요청이-없다면-일부-테스트-무시하기","ch11-03-test-organization.html#테스트-조직화","ch11-03-test-organization.html#유닛-테스트","ch11-03-test-organization.html#통합-테스트","ch11-03-test-organization.html#정리","ch12-00-an-io-project.html#io-프로젝트-커맨드-라인-프로그램-만들기","ch12-01-accepting-command-line-arguments.html#커맨드-라인-인수-받기","ch12-01-accepting-command-line-arguments.html#인수-값-읽기","ch12-01-accepting-command-line-arguments.html#args-함수와-유효하지-않은-유니코드","ch12-01-accepting-command-line-arguments.html#인수-값들을-변수에-저장하기","ch12-02-reading-a-file.html#파일-읽기","ch12-03-improving-error-handling-and-modularity.html#모듈성과-에러-처리-향상을-위한-리팩터링","ch12-03-improving-error-handling-and-modularity.html#바이너리-프로젝트에-대한-관심사-분리","ch12-03-improving-error-handling-and-modularity.html#clone을-사용한-절충안","ch12-03-improving-error-handling-and-modularity.html#에러-처리-수정","ch12-03-improving-error-handling-and-modularity.html#main으로부터-로직-추출하기","ch12-03-improving-error-handling-and-modularity.html#라이브러리-크레이트로-코드-쪼개기","ch12-04-testing-the-librarys-functionality.html#테스트-주도-개발로-라이브러리-기능-개발하기","ch12-04-testing-the-librarys-functionality.html#실패하는-테스트-작성하기","ch12-04-testing-the-librarys-functionality.html#테스트를-통과하도록-코드-작성하기","ch12-05-working-with-environment-variables.html#환경-변수-사용하기","ch12-05-working-with-environment-variables.html#대소문자를-구분하지-않는-search-함수에-대한-실패하는-테스트-작성하기","ch12-05-working-with-environment-variables.html#search_case_insensitive-함수-구현하기","ch12-06-writing-to-stderr-instead-of-stdout.html#표준-출력-대신-표준-에러로-에러-메시지-작성하기","ch12-06-writing-to-stderr-instead-of-stdout.html#에러가-기록되었는지-검사하기","ch12-06-writing-to-stderr-instead-of-stdout.html#표준-에러로-에러-출력하기","ch12-06-writing-to-stderr-instead-of-stdout.html#정리","ch13-00-functional-features.html#함수형-언어의-특성-반복자와-클로저","ch13-01-closures.html#클로저-자신의-환경을-캡처하는-익명-함수","ch13-01-closures.html#클로저로-환경-캡처하기","ch13-01-closures.html#클로저-타입-추론과-명시","ch13-01-closures.html#참조자를-캡처하거나-소유권-이동하기","ch13-01-closures.html#캡처된-값을-클로저-밖으로-이동하기와-fn-트레이트","ch13-02-iterators.html#반복자로-일련의-아이템들-처리하기","ch13-02-iterators.html#iterator-트레이트와-next-메서드","ch13-02-iterators.html#반복자를-소비하는-메서드","ch13-02-iterators.html#다른-반복자를-생성하는-메서드","ch13-02-iterators.html#환경을-캡처하는-클로저-사용하기","ch13-03-improving-our-io-project.html#io-프로젝트-개선하기","ch13-03-improving-our-io-project.html#반복자를-사용하여-clone-제거하기","ch13-03-improving-our-io-project.html#반복자-어댑터로-더-간결한-코드-만들기","ch13-03-improving-our-io-project.html#루프와-반복자-중-선택하기","ch13-04-performance.html#성능-비교하기-루프-vs-반복자","ch13-04-performance.html#정리","ch14-00-more-about-cargo.html#카고와-cratesio-더-알아보기","ch14-01-release-profiles.html#릴리즈-프로필을-통한-빌드-커스터마이징하기","ch14-02-publishing-to-crates-io.html#cratesio에-크레이트-배포하기","ch14-02-publishing-to-crates-io.html#유용한-문서화-주석-만들기","ch14-02-publishing-to-crates-io.html#pub-use로-편리한-공개-api-내보내기","ch14-02-publishing-to-crates-io.html#cartesio-계정-설정하기","ch14-02-publishing-to-crates-io.html#새-크레이트에-메타데이터-추가하기","ch14-02-publishing-to-crates-io.html#cratesio에-배포하기","ch14-02-publishing-to-crates-io.html#이미-존재하는-크레이트의-새-버전-배포하기","ch14-02-publishing-to-crates-io.html#cargo-yank로-cratesio에서-버전-사용하지-않게-하기","ch14-03-cargo-workspaces.html#카고-작업공간","ch14-03-cargo-workspaces.html#작업공간-생성하기","ch14-03-cargo-workspaces.html#작업공간에-두-번째-패키지-생성하기","ch14-04-installing-binaries.html#cargo-install로-cratesio에-있는-바이너리-설치하기","ch14-05-extending-cargo.html#커스텀-명령어로-카고-확장하기","ch14-05-extending-cargo.html#정리","ch15-00-smart-pointers.html#스마트-포인터","ch15-01-box.html#box를-사용하여-힙에-있는-데이터-가리키기","ch15-01-box.html#box을-사용하여-힙에-데이터-저장하기","ch15-01-box.html#박스로-재귀적-타입-가능하게-하기","ch15-02-deref.html#deref-트레이트로-스마트-포인터를-보통의-참조자처럼-취급하기","ch15-02-deref.html#포인터를-따라가서-값-얻기","ch15-02-deref.html#box를-참조자처럼-사용하기","ch15-02-deref.html#자체-스마트-포인터-정의하기","ch15-02-deref.html#deref-트레이트를-구현하여-임의의-타입을-참조자처럼-다루기","ch15-02-deref.html#함수와-메서드를-이용한-암묵적-역참조-강제","ch15-02-deref.html#역참조-강제가-가변성과-상호작용하는-법","ch15-03-drop.html#drop-트레이트로-메모리-정리-코드-실행하기","ch15-03-drop.html#stdmemdrop으로-값을-일찍-버리기","ch15-04-rc.html#rc-참조-카운트-스마트-포인터","ch15-04-rc.html#rc를-사용하여-데이터-공유하기","ch15-04-rc.html#rc를-클론하는-것은-참조-카운트를-증가시킵니다","ch15-05-interior-mutability.html#refcell와-내부-가변성-패턴","ch15-05-interior-mutability.html#refcell으로-런타임에-대여-규칙-집행하기","ch15-05-interior-mutability.html#내부-가변성-불변값에-대한-가변-대여","ch15-05-interior-mutability.html#rc와-refcell를-조합하여-가변-데이터의-복수-소유자-만들기","ch15-06-reference-cycles.html#순환-참조는-메모리-누수를-발생시킬-수-있습니다","ch15-06-reference-cycles.html#순환-참조-만들기","ch15-06-reference-cycles.html#순환-참조-방지하기-rc를-weak로-바꾸기","ch15-06-reference-cycles.html#정리","ch16-00-concurrency.html#겁-없는-동시성","ch16-01-threads.html#스레드를-이용하여-코드를-동시에-실행하기","ch16-01-threads.html#spawn으로-새로운-스레드-생성하기","ch16-01-threads.html#join-핸들을-사용하여-모든-스레드가-끝날-때까지-기다리기","ch16-01-threads.html#스레드에-move-클로저-사용하기","ch16-02-message-passing.html#메시지-패싱을-사용하여-스레드-간-데이터-전송하기","ch16-02-message-passing.html#채널과-소유권-이동","ch16-02-message-passing.html#여러-값-보내기와-수신자가-기다리는지-알아보기","ch16-02-message-passing.html#송신자를-복제하여-여러-생산자-만들기","ch16-03-shared-state.html#공유-상태-동시성","ch16-03-shared-state.html#뮤텍스를-사용하여-한번에-한-스레드에서의-데이터-접근을-허용하기","ch16-03-shared-state.html#refcellrc와-mutexarc-간의-유사성","ch16-04-extensible-concurrency-sync-and-send.html#sync와-send-트레이트를-이용한-확장-가능한-동시성","ch16-04-extensible-concurrency-sync-and-send.html#send를-사용하여-스레드-사이에-소유권-이동을-허용하기","ch16-04-extensible-concurrency-sync-and-send.html#sync를-사용하여-여러-스레드로부터의-접근을-허용하기","ch16-04-extensible-concurrency-sync-and-send.html#send와-sync를-손수-구현하는-것은-안전하지-않습니다","ch16-04-extensible-concurrency-sync-and-send.html#정리","ch17-00-oop.html#러스트의-객체-지향-프로그래밍-기능들","ch17-01-what-is-oo.html#객체-지향-언어의-특성","ch17-01-what-is-oo.html#객체는-데이터와-동작을-담습니다","ch17-01-what-is-oo.html#상세-구현을-은닉하는-캡슐화","ch17-01-what-is-oo.html#타입-시스템과-코드-공유로서의-상속","ch17-01-what-is-oo.html#다형성","ch17-02-trait-objects.html#트레이트-객체를-사용하여-다른-타입의-값-허용하기","ch17-02-trait-objects.html#공통된-동작을-위한-트레이트-정의하기","ch17-02-trait-objects.html#트레이트-구현하기","ch17-02-trait-objects.html#트레이트-객체는-동적-디스패치를-수행합니다","ch17-03-oo-design-patterns.html#객체-지향-디자인-패턴-구현하기","ch17-03-oo-design-patterns.html#post를-정의하고-초안-상태의-새-인스턴스-생성하기","ch17-03-oo-design-patterns.html#게시물-콘텐츠의-텍스트-저장하기","ch17-03-oo-design-patterns.html#초안-게시물의-내용이-비어있음을-보장하기","ch17-03-oo-design-patterns.html#게시물에-대한-검토-요청이-게시물의-상태를-변경합니다","ch17-03-oo-design-patterns.html#content의-동작을-변경하는-approve-메서드-추가하기","ch17-03-oo-design-patterns.html#상태-패턴의-장단점","ch17-03-oo-design-patterns.html#정리","ch18-00-patterns.html#패턴과-매칭","ch18-01-all-the-places-for-patterns.html#패턴이-사용될-수-있는-모든-곳","ch18-01-all-the-places-for-patterns.html#match-갈래","ch18-01-all-the-places-for-patterns.html#if-let-조건-표현식","ch18-01-all-the-places-for-patterns.html#while-let-조건-루프","ch18-01-all-the-places-for-patterns.html#for-루프","ch18-01-all-the-places-for-patterns.html#let-구문","ch18-01-all-the-places-for-patterns.html#함수-매개변수","ch18-02-refutability.html#반박-가능성-패턴이-매칭에-실패할지의-여부","ch18-03-pattern-syntax.html#패턴-문법","ch18-03-pattern-syntax.html#리터럴-매칭","ch18-03-pattern-syntax.html#명명된-변수-매칭","ch18-03-pattern-syntax.html#다중-패턴","ch18-03-pattern-syntax.html#를-이용한-값의-범위-매칭","ch18-03-pattern-syntax.html#값을-해체하여-분리하기","ch18-03-pattern-syntax.html#패턴에서-값-무시하기","ch18-03-pattern-syntax.html#매치-가드를-사용한-추가-조건","ch18-03-pattern-syntax.html#-바인딩","ch18-03-pattern-syntax.html#정리","ch19-00-advanced-features.html#고급-기능","ch19-01-unsafe-rust.html#안전하지-않은-러스트","ch19-01-unsafe-rust.html#안전하지-않은-슈퍼파워","ch19-01-unsafe-rust.html#원시-포인터-역참조하기","ch19-01-unsafe-rust.html#안전하지-않은-함수-또는-메서드-호출하기","ch19-01-unsafe-rust.html#가변-정적-변수의-접근-혹은-수정하기","ch19-01-unsafe-rust.html#안전하지-않은-트레이트-구현하기","ch19-01-unsafe-rust.html#유니온-필드에-접근하기","ch19-01-unsafe-rust.html#unsafe-코드를-사용하는-경우","ch19-03-advanced-traits.html#고급-트레이트","ch19-03-advanced-traits.html#연관-타입으로-트레이트-정의에서-자리표시자-타입-지정하기","ch19-03-advanced-traits.html#기본-제네릭-타입-매개변수와-연산자-오버로딩","ch19-03-advanced-traits.html#모호성-방지를-위한-완전-정규화-문법-같은-이름의-메서드-호출하기","ch19-03-advanced-traits.html#슈퍼트레이트를-사용하여-한-트레이트에서-다른-트레이트의-기능을-요구하기","ch19-03-advanced-traits.html#뉴타입-패턴을-사용하여-외부-타입에-외부-트레이트-구현하기","ch19-04-advanced-types.html#고급-타입","ch19-04-advanced-types.html#타입-안전성과-추상화를-위한-뉴타입-패턴-사용하기","ch19-04-advanced-types.html#타입-별칭으로-타입의-동의어-만들기","ch19-04-advanced-types.html#절대-반환하지-않는-부정-타입","ch19-04-advanced-types.html#동적-크기-타입과-sized-트레이트","ch19-05-advanced-functions-and-closures.html#고급-함수와-클로저","ch19-05-advanced-functions-and-closures.html#함수-포인터","ch19-05-advanced-functions-and-closures.html#클로저-반환하기","ch19-06-macros.html#매크로","ch19-06-macros.html#매크로와-함수의-차이","ch19-06-macros.html#일반적인-메타프로그래밍을-위한-macro_rules를-사용한-선언적-매크로","ch19-06-macros.html#속성에서-코드를-생성하기-위한-절차적-매크로","ch19-06-macros.html#커스텀-derive-매크로-작성-방법","ch19-06-macros.html#속성형-매크로","ch19-06-macros.html#함수형-매크로","ch19-06-macros.html#정리","ch20-00-final-project-a-web-server.html#최종-프로젝트-멀티스레드-웹-서버-구축하기","ch20-01-single-threaded.html#싱글스레드-웹-서버-구축하기","ch20-01-single-threaded.html#tcp-연결-수신-대기하기","ch20-01-single-threaded.html#요청-읽기","ch20-01-single-threaded.html#http-요청-자세히-살펴보기","ch20-01-single-threaded.html#응답-작성하기","ch20-01-single-threaded.html#실제-html-반환하기","ch20-01-single-threaded.html#요청의-유효성-검사와-선택적-응답","ch20-01-single-threaded.html#리팩터링","ch20-02-multithreaded.html#싱글스레드-서버를-멀티스레드-서버로-바꾸기","ch20-02-multithreaded.html#현재의-서버-구현에서-느린-요청-시뮬레이션","ch20-02-multithreaded.html#스레드-풀로-처리량-개선하기","ch20-03-graceful-shutdown-and-cleanup.html#우아한-종료와-정리","ch20-03-graceful-shutdown-and-cleanup.html#threadpool에-대한-drop-트레이트-구현하기","ch20-03-graceful-shutdown-and-cleanup.html#작업을-기다리는-스레드에게-정지-신호-보내기","ch20-03-graceful-shutdown-and-cleanup.html#정리","appendix-00.html#부록","appendix-01-keywords.html#부록-a-키워드","appendix-01-keywords.html#현재-사용중인-키워드","appendix-01-keywords.html#미래에-사용하기-위해-예약된-키워드","appendix-01-keywords.html#원시-식별자","appendix-02-operators.html#부록-b-연산자와-기호","appendix-02-operators.html#연산자","appendix-02-operators.html#비연산자-기호","appendix-03-derivable-traits.html#부록-c-파생-가능한-트레이트","appendix-03-derivable-traits.html#프로그래머-출력을-위한-debug","appendix-03-derivable-traits.html#동등-비교를-위한-partialeq-및-eq","appendix-03-derivable-traits.html#순서-비교를-위한-partialord-및-ord","appendix-03-derivable-traits.html#값을-복제하기-위한-clone과-copy","appendix-03-derivable-traits.html#어떤-값을-고정-크기의-값으로-매핑하기-위한-hash","appendix-03-derivable-traits.html#기본값을-위한-default","appendix-04-useful-development-tools.html#부록-d---유용한-개발-도구","appendix-04-useful-development-tools.html#rustfmt로-자동-포맷팅하기","appendix-04-useful-development-tools.html#rustfix로-코드-수정하기","appendix-04-useful-development-tools.html#clippy로-더-많은-린트-사용하기","appendix-04-useful-development-tools.html#rust-analyzer를-사용한-ide-통합","appendix-05-editions.html#부록-e---에디션","appendix-06-translation.html#부록-f-번역본","appendix-07-nightly-rust.html#부록-g---러스트가-만들어지는-과정과-nightly-러스트","appendix-07-nightly-rust.html#정체되지-않는-안정성","appendix-07-nightly-rust.html#칙칙폭폭-릴리즈-채널과-기차-타기","appendix-07-nightly-rust.html#불안정한-기능","appendix-07-nightly-rust.html#rustup과-러스트-nightly의-역할","appendix-07-nightly-rust.html#rfc-과정과-팀"],"index":{"documentStore":{"docInfo":{"0":{"body":23,"breadcrumbs":6,"title":3},"1":{"body":6,"breadcrumbs":0,"title":0},"10":{"body":49,"breadcrumbs":0,"title":0},"100":{"body":234,"breadcrumbs":0,"title":0},"101":{"body":142,"breadcrumbs":1,"title":1},"102":{"body":75,"breadcrumbs":2,"title":1},"103":{"body":75,"breadcrumbs":1,"title":0},"104":{"body":114,"breadcrumbs":2,"title":1},"105":{"body":88,"breadcrumbs":1,"title":0},"106":{"body":69,"breadcrumbs":2,"title":1},"107":{"body":110,"breadcrumbs":0,"title":0},"108":{"body":4,"breadcrumbs":0,"title":0},"109":{"body":3,"breadcrumbs":0,"title":0},"11":{"body":1,"breadcrumbs":0,"title":0},"110":{"body":50,"breadcrumbs":0,"title":0},"111":{"body":4,"breadcrumbs":0,"title":0},"112":{"body":77,"breadcrumbs":0,"title":0},"113":{"body":72,"breadcrumbs":0,"title":0},"114":{"body":129,"breadcrumbs":0,"title":0},"115":{"body":158,"breadcrumbs":1,"title":1},"116":{"body":31,"breadcrumbs":1,"title":1},"117":{"body":94,"breadcrumbs":0,"title":0},"118":{"body":126,"breadcrumbs":2,"title":1},"119":{"body":80,"breadcrumbs":2,"title":1},"12":{"body":6,"breadcrumbs":0,"title":0},"120":{"body":31,"breadcrumbs":1,"title":0},"121":{"body":46,"breadcrumbs":3,"title":2},"122":{"body":57,"breadcrumbs":1,"title":0},"123":{"body":128,"breadcrumbs":2,"title":1},"124":{"body":7,"breadcrumbs":1,"title":0},"125":{"body":72,"breadcrumbs":0,"title":0},"126":{"body":16,"breadcrumbs":0,"title":0},"127":{"body":2,"breadcrumbs":0,"title":0},"128":{"body":10,"breadcrumbs":0,"title":0},"129":{"body":1,"breadcrumbs":0,"title":0},"13":{"body":6,"breadcrumbs":0,"title":0},"130":{"body":44,"breadcrumbs":0,"title":0},"131":{"body":20,"breadcrumbs":0,"title":0},"132":{"body":142,"breadcrumbs":0,"title":0},"133":{"body":35,"breadcrumbs":0,"title":0},"134":{"body":25,"breadcrumbs":0,"title":0},"135":{"body":15,"breadcrumbs":0,"title":0},"136":{"body":7,"breadcrumbs":4,"title":2},"137":{"body":15,"breadcrumbs":2,"title":0},"138":{"body":89,"breadcrumbs":2,"title":0},"139":{"body":171,"breadcrumbs":2,"title":0},"14":{"body":1,"breadcrumbs":0,"title":0},"140":{"body":185,"breadcrumbs":2,"title":0},"141":{"body":42,"breadcrumbs":2,"title":0},"142":{"body":15,"breadcrumbs":2,"title":0},"143":{"body":8,"breadcrumbs":2,"title":0},"144":{"body":11,"breadcrumbs":0,"title":0},"145":{"body":24,"breadcrumbs":0,"title":0},"146":{"body":50,"breadcrumbs":0,"title":0},"147":{"body":27,"breadcrumbs":0,"title":0},"148":{"body":100,"breadcrumbs":0,"title":0},"149":{"body":11,"breadcrumbs":0,"title":0},"15":{"body":29,"breadcrumbs":3,"title":3},"150":{"body":21,"breadcrumbs":0,"title":0},"151":{"body":9,"breadcrumbs":0,"title":0},"152":{"body":3,"breadcrumbs":2,"title":1},"153":{"body":52,"breadcrumbs":2,"title":1},"154":{"body":126,"breadcrumbs":2,"title":1},"155":{"body":133,"breadcrumbs":2,"title":1},"156":{"body":62,"breadcrumbs":1,"title":0},"157":{"body":36,"breadcrumbs":4,"title":3},"158":{"body":82,"breadcrumbs":3,"title":2},"159":{"body":92,"breadcrumbs":1,"title":0},"16":{"body":17,"breadcrumbs":2,"title":2},"160":{"body":111,"breadcrumbs":1,"title":0},"161":{"body":215,"breadcrumbs":1,"title":0},"162":{"body":8,"breadcrumbs":4,"title":2},"163":{"body":7,"breadcrumbs":2,"title":0},"164":{"body":36,"breadcrumbs":2,"title":0},"165":{"body":14,"breadcrumbs":2,"title":0},"166":{"body":155,"breadcrumbs":2,"title":0},"167":{"body":7,"breadcrumbs":2,"title":0},"168":{"body":15,"breadcrumbs":0,"title":0},"169":{"body":152,"breadcrumbs":0,"title":0},"17":{"body":23,"breadcrumbs":0,"title":0},"170":{"body":0,"breadcrumbs":0,"title":0},"171":{"body":184,"breadcrumbs":0,"title":0},"172":{"body":149,"breadcrumbs":0,"title":0},"173":{"body":35,"breadcrumbs":0,"title":0},"174":{"body":189,"breadcrumbs":0,"title":0},"175":{"body":30,"breadcrumbs":0,"title":0},"176":{"body":5,"breadcrumbs":0,"title":0},"177":{"body":29,"breadcrumbs":0,"title":0},"178":{"body":133,"breadcrumbs":0,"title":0},"179":{"body":233,"breadcrumbs":0,"title":0},"18":{"body":7,"breadcrumbs":0,"title":0},"180":{"body":177,"breadcrumbs":0,"title":0},"181":{"body":195,"breadcrumbs":0,"title":0},"182":{"body":72,"breadcrumbs":0,"title":0},"183":{"body":2,"breadcrumbs":0,"title":0},"184":{"body":88,"breadcrumbs":0,"title":0},"185":{"body":47,"breadcrumbs":0,"title":0},"186":{"body":128,"breadcrumbs":0,"title":0},"187":{"body":7,"breadcrumbs":0,"title":0},"188":{"body":185,"breadcrumbs":0,"title":0},"189":{"body":90,"breadcrumbs":0,"title":0},"19":{"body":8,"breadcrumbs":0,"title":0},"190":{"body":37,"breadcrumbs":0,"title":0},"191":{"body":122,"breadcrumbs":0,"title":0},"192":{"body":84,"breadcrumbs":0,"title":0},"193":{"body":15,"breadcrumbs":0,"title":0},"194":{"body":45,"breadcrumbs":0,"title":0},"195":{"body":3,"breadcrumbs":0,"title":0},"196":{"body":16,"breadcrumbs":0,"title":0},"197":{"body":3,"breadcrumbs":0,"title":0},"198":{"body":331,"breadcrumbs":0,"title":0},"199":{"body":422,"breadcrumbs":1,"title":1},"2":{"body":14,"breadcrumbs":0,"title":0},"20":{"body":11,"breadcrumbs":4,"title":2},"200":{"body":230,"breadcrumbs":2,"title":2},"201":{"body":202,"breadcrumbs":0,"title":0},"202":{"body":425,"breadcrumbs":1,"title":1},"203":{"body":43,"breadcrumbs":2,"title":2},"204":{"body":19,"breadcrumbs":0,"title":0},"205":{"body":9,"breadcrumbs":0,"title":0},"206":{"body":214,"breadcrumbs":0,"title":0},"207":{"body":198,"breadcrumbs":0,"title":0},"208":{"body":162,"breadcrumbs":0,"title":0},"209":{"body":4,"breadcrumbs":0,"title":0},"21":{"body":28,"breadcrumbs":2,"title":0},"210":{"body":69,"breadcrumbs":0,"title":0},"211":{"body":359,"breadcrumbs":0,"title":0},"212":{"body":1,"breadcrumbs":0,"title":0},"213":{"body":31,"breadcrumbs":2,"title":1},"214":{"body":26,"breadcrumbs":1,"title":0},"215":{"body":30,"breadcrumbs":1,"title":0},"216":{"body":55,"breadcrumbs":2,"title":1},"217":{"body":51,"breadcrumbs":1,"title":0},"218":{"body":112,"breadcrumbs":1,"title":0},"219":{"body":13,"breadcrumbs":1,"title":0},"22":{"body":37,"breadcrumbs":2,"title":0},"220":{"body":145,"breadcrumbs":1,"title":0},"221":{"body":91,"breadcrumbs":2,"title":1},"222":{"body":350,"breadcrumbs":1,"title":0},"223":{"body":332,"breadcrumbs":2,"title":1},"224":{"body":120,"breadcrumbs":1,"title":0},"225":{"body":10,"breadcrumbs":1,"title":0},"226":{"body":317,"breadcrumbs":1,"title":0},"227":{"body":523,"breadcrumbs":1,"title":0},"228":{"body":1,"breadcrumbs":1,"title":0},"229":{"body":131,"breadcrumbs":2,"title":1},"23":{"body":21,"breadcrumbs":2,"title":0},"230":{"body":754,"breadcrumbs":2,"title":1},"231":{"body":8,"breadcrumbs":1,"title":0},"232":{"body":12,"breadcrumbs":1,"title":0},"233":{"body":52,"breadcrumbs":1,"title":0},"234":{"body":1,"breadcrumbs":1,"title":0},"235":{"body":2,"breadcrumbs":0,"title":0},"236":{"body":0,"breadcrumbs":0,"title":0},"237":{"body":143,"breadcrumbs":0,"title":0},"238":{"body":155,"breadcrumbs":0,"title":0},"239":{"body":169,"breadcrumbs":0,"title":0},"24":{"body":55,"breadcrumbs":2,"title":0},"240":{"body":302,"breadcrumbs":1,"title":1},"241":{"body":37,"breadcrumbs":0,"title":0},"242":{"body":66,"breadcrumbs":2,"title":2},"243":{"body":38,"breadcrumbs":0,"title":0},"244":{"body":95,"breadcrumbs":0,"title":0},"245":{"body":81,"breadcrumbs":0,"title":0},"246":{"body":4,"breadcrumbs":2,"title":1},"247":{"body":586,"breadcrumbs":2,"title":1},"248":{"body":265,"breadcrumbs":1,"title":0},"249":{"body":4,"breadcrumbs":1,"title":0},"25":{"body":8,"breadcrumbs":0,"title":0},"250":{"body":70,"breadcrumbs":2,"title":1},"251":{"body":2,"breadcrumbs":1,"title":0},"252":{"body":3,"breadcrumbs":2,"title":1},"253":{"body":74,"breadcrumbs":1,"title":0},"254":{"body":4,"breadcrumbs":3,"title":1},"255":{"body":148,"breadcrumbs":2,"title":0},"256":{"body":251,"breadcrumbs":5,"title":3},"257":{"body":17,"breadcrumbs":3,"title":1},"258":{"body":102,"breadcrumbs":2,"title":0},"259":{"body":34,"breadcrumbs":3,"title":1},"26":{"body":84,"breadcrumbs":0,"title":0},"260":{"body":7,"breadcrumbs":2,"title":0},"261":{"body":28,"breadcrumbs":5,"title":3},"262":{"body":2,"breadcrumbs":1,"title":0},"263":{"body":50,"breadcrumbs":1,"title":0},"264":{"body":411,"breadcrumbs":1,"title":0},"265":{"body":61,"breadcrumbs":7,"title":3},"266":{"body":9,"breadcrumbs":1,"title":0},"267":{"body":2,"breadcrumbs":1,"title":0},"268":{"body":27,"breadcrumbs":0,"title":0},"269":{"body":7,"breadcrumbs":2,"title":1},"27":{"body":104,"breadcrumbs":0,"title":0},"270":{"body":24,"breadcrumbs":2,"title":1},"271":{"body":259,"breadcrumbs":1,"title":0},"272":{"body":12,"breadcrumbs":2,"title":1},"273":{"body":99,"breadcrumbs":1,"title":0},"274":{"body":32,"breadcrumbs":2,"title":1},"275":{"body":96,"breadcrumbs":1,"title":0},"276":{"body":82,"breadcrumbs":2,"title":1},"277":{"body":139,"breadcrumbs":1,"title":0},"278":{"body":22,"breadcrumbs":1,"title":0},"279":{"body":97,"breadcrumbs":2,"title":1},"28":{"body":5,"breadcrumbs":0,"title":0},"280":{"body":186,"breadcrumbs":2,"title":1},"281":{"body":17,"breadcrumbs":2,"title":1},"282":{"body":171,"breadcrumbs":2,"title":1},"283":{"body":115,"breadcrumbs":2,"title":1},"284":{"body":7,"breadcrumbs":2,"title":1},"285":{"body":28,"breadcrumbs":2,"title":1},"286":{"body":735,"breadcrumbs":1,"title":0},"287":{"body":125,"breadcrumbs":3,"title":2},"288":{"body":5,"breadcrumbs":0,"title":0},"289":{"body":245,"breadcrumbs":0,"title":0},"29":{"body":10,"breadcrumbs":0,"title":0},"290":{"body":415,"breadcrumbs":2,"title":2},"291":{"body":6,"breadcrumbs":0,"title":0},"292":{"body":13,"breadcrumbs":0,"title":0},"293":{"body":6,"breadcrumbs":0,"title":0},"294":{"body":74,"breadcrumbs":1,"title":1},"295":{"body":201,"breadcrumbs":1,"title":1},"296":{"body":260,"breadcrumbs":1,"title":1},"297":{"body":106,"breadcrumbs":0,"title":0},"298":{"body":99,"breadcrumbs":0,"title":0},"299":{"body":46,"breadcrumbs":0,"title":0},"3":{"body":0,"breadcrumbs":0,"title":0},"30":{"body":8,"breadcrumbs":0,"title":0},"300":{"body":58,"breadcrumbs":0,"title":0},"301":{"body":3,"breadcrumbs":0,"title":0},"302":{"body":363,"breadcrumbs":0,"title":0},"303":{"body":19,"breadcrumbs":2,"title":2},"304":{"body":3,"breadcrumbs":4,"title":2},"305":{"body":20,"breadcrumbs":3,"title":1},"306":{"body":22,"breadcrumbs":3,"title":1},"307":{"body":8,"breadcrumbs":4,"title":2},"308":{"body":3,"breadcrumbs":2,"title":0},"309":{"body":10,"breadcrumbs":0,"title":0},"31":{"body":6,"breadcrumbs":0,"title":0},"310":{"body":6,"breadcrumbs":0,"title":0},"311":{"body":24,"breadcrumbs":0,"title":0},"312":{"body":112,"breadcrumbs":0,"title":0},"313":{"body":9,"breadcrumbs":0,"title":0},"314":{"body":3,"breadcrumbs":0,"title":0},"315":{"body":29,"breadcrumbs":0,"title":0},"316":{"body":120,"breadcrumbs":0,"title":0},"317":{"body":241,"breadcrumbs":0,"title":0},"318":{"body":10,"breadcrumbs":0,"title":0},"319":{"body":48,"breadcrumbs":0,"title":0},"32":{"body":59,"breadcrumbs":0,"title":0},"320":{"body":66,"breadcrumbs":1,"title":1},"321":{"body":61,"breadcrumbs":0,"title":0},"322":{"body":54,"breadcrumbs":0,"title":0},"323":{"body":132,"breadcrumbs":0,"title":0},"324":{"body":440,"breadcrumbs":2,"title":2},"325":{"body":261,"breadcrumbs":0,"title":0},"326":{"body":0,"breadcrumbs":0,"title":0},"327":{"body":15,"breadcrumbs":0,"title":0},"328":{"body":0,"breadcrumbs":0,"title":0},"329":{"body":34,"breadcrumbs":1,"title":1},"33":{"body":102,"breadcrumbs":0,"title":0},"330":{"body":66,"breadcrumbs":0,"title":0},"331":{"body":27,"breadcrumbs":0,"title":0},"332":{"body":51,"breadcrumbs":0,"title":0},"333":{"body":102,"breadcrumbs":0,"title":0},"334":{"body":48,"breadcrumbs":0,"title":0},"335":{"body":182,"breadcrumbs":0,"title":0},"336":{"body":0,"breadcrumbs":0,"title":0},"337":{"body":18,"breadcrumbs":0,"title":0},"338":{"body":84,"breadcrumbs":0,"title":0},"339":{"body":19,"breadcrumbs":0,"title":0},"34":{"body":62,"breadcrumbs":0,"title":0},"340":{"body":48,"breadcrumbs":0,"title":0},"341":{"body":325,"breadcrumbs":0,"title":0},"342":{"body":283,"breadcrumbs":0,"title":0},"343":{"body":154,"breadcrumbs":0,"title":0},"344":{"body":57,"breadcrumbs":0,"title":0},"345":{"body":1,"breadcrumbs":0,"title":0},"346":{"body":14,"breadcrumbs":0,"title":0},"347":{"body":2,"breadcrumbs":0,"title":0},"348":{"body":14,"breadcrumbs":0,"title":0},"349":{"body":75,"breadcrumbs":0,"title":0},"35":{"body":44,"breadcrumbs":0,"title":0},"350":{"body":372,"breadcrumbs":0,"title":0},"351":{"body":50,"breadcrumbs":0,"title":0},"352":{"body":34,"breadcrumbs":0,"title":0},"353":{"body":5,"breadcrumbs":0,"title":0},"354":{"body":4,"breadcrumbs":1,"title":1},"355":{"body":1,"breadcrumbs":0,"title":0},"356":{"body":91,"breadcrumbs":0,"title":0},"357":{"body":146,"breadcrumbs":0,"title":0},"358":{"body":433,"breadcrumbs":0,"title":0},"359":{"body":236,"breadcrumbs":0,"title":0},"36":{"body":92,"breadcrumbs":1,"title":1},"360":{"body":64,"breadcrumbs":0,"title":0},"361":{"body":5,"breadcrumbs":0,"title":0},"362":{"body":21,"breadcrumbs":0,"title":0},"363":{"body":231,"breadcrumbs":0,"title":0},"364":{"body":146,"breadcrumbs":0,"title":0},"365":{"body":72,"breadcrumbs":1,"title":1},"366":{"body":0,"breadcrumbs":0,"title":0},"367":{"body":110,"breadcrumbs":0,"title":0},"368":{"body":92,"breadcrumbs":0,"title":0},"369":{"body":9,"breadcrumbs":0,"title":0},"37":{"body":38,"breadcrumbs":1,"title":1},"370":{"body":8,"breadcrumbs":0,"title":0},"371":{"body":75,"breadcrumbs":1,"title":1},"372":{"body":24,"breadcrumbs":0,"title":0},"373":{"body":318,"breadcrumbs":1,"title":1},"374":{"body":20,"breadcrumbs":0,"title":0},"375":{"body":19,"breadcrumbs":0,"title":0},"376":{"body":0,"breadcrumbs":0,"title":0},"377":{"body":14,"breadcrumbs":0,"title":0},"378":{"body":23,"breadcrumbs":0,"title":0},"379":{"body":82,"breadcrumbs":1,"title":1},"38":{"body":24,"breadcrumbs":0,"title":0},"380":{"body":135,"breadcrumbs":0,"title":0},"381":{"body":45,"breadcrumbs":1,"title":1},"382":{"body":79,"breadcrumbs":0,"title":0},"383":{"body":99,"breadcrumbs":1,"title":1},"384":{"body":173,"breadcrumbs":0,"title":0},"385":{"body":62,"breadcrumbs":0,"title":0},"386":{"body":0,"breadcrumbs":0,"title":0},"387":{"body":83,"breadcrumbs":0,"title":0},"388":{"body":1521,"breadcrumbs":0,"title":0},"389":{"body":9,"breadcrumbs":0,"title":0},"39":{"body":4,"breadcrumbs":0,"title":0},"390":{"body":628,"breadcrumbs":2,"title":2},"391":{"body":638,"breadcrumbs":0,"title":0},"392":{"body":0,"breadcrumbs":0,"title":0},"393":{"body":0,"breadcrumbs":0,"title":0},"394":{"body":0,"breadcrumbs":0,"title":0},"395":{"body":37,"breadcrumbs":0,"title":0},"396":{"body":12,"breadcrumbs":0,"title":0},"397":{"body":56,"breadcrumbs":0,"title":0},"398":{"body":0,"breadcrumbs":2,"title":1},"399":{"body":148,"breadcrumbs":1,"title":0},"4":{"body":8,"breadcrumbs":0,"title":0},"40":{"body":169,"breadcrumbs":0,"title":0},"400":{"body":141,"breadcrumbs":1,"title":0},"401":{"body":12,"breadcrumbs":2,"title":1},"402":{"body":4,"breadcrumbs":2,"title":1},"403":{"body":17,"breadcrumbs":3,"title":2},"404":{"body":32,"breadcrumbs":3,"title":2},"405":{"body":35,"breadcrumbs":3,"title":2},"406":{"body":10,"breadcrumbs":2,"title":1},"407":{"body":19,"breadcrumbs":2,"title":1},"408":{"body":2,"breadcrumbs":2,"title":1},"409":{"body":15,"breadcrumbs":2,"title":1},"41":{"body":110,"breadcrumbs":0,"title":0},"410":{"body":71,"breadcrumbs":2,"title":1},"411":{"body":69,"breadcrumbs":2,"title":1},"412":{"body":18,"breadcrumbs":4,"title":3},"413":{"body":24,"breadcrumbs":2,"title":1},"414":{"body":16,"breadcrumbs":2,"title":1},"415":{"body":0,"breadcrumbs":4,"title":2},"416":{"body":4,"breadcrumbs":2,"title":0},"417":{"body":68,"breadcrumbs":2,"title":0},"418":{"body":8,"breadcrumbs":2,"title":0},"419":{"body":51,"breadcrumbs":4,"title":2},"42":{"body":311,"breadcrumbs":0,"title":0},"420":{"body":10,"breadcrumbs":3,"title":1},"43":{"body":115,"breadcrumbs":0,"title":0},"44":{"body":54,"breadcrumbs":0,"title":0},"45":{"body":188,"breadcrumbs":0,"title":0},"46":{"body":5,"breadcrumbs":0,"title":0},"47":{"body":0,"breadcrumbs":0,"title":0},"48":{"body":124,"breadcrumbs":0,"title":0},"49":{"body":14,"breadcrumbs":0,"title":0},"5":{"body":0,"breadcrumbs":0,"title":0},"50":{"body":114,"breadcrumbs":0,"title":0},"51":{"body":49,"breadcrumbs":0,"title":0},"52":{"body":209,"breadcrumbs":0,"title":0},"53":{"body":186,"breadcrumbs":0,"title":0},"54":{"body":46,"breadcrumbs":0,"title":0},"55":{"body":85,"breadcrumbs":0,"title":0},"56":{"body":157,"breadcrumbs":0,"title":0},"57":{"body":129,"breadcrumbs":0,"title":0},"58":{"body":17,"breadcrumbs":0,"title":0},"59":{"body":0,"breadcrumbs":0,"title":0},"6":{"body":1,"breadcrumbs":0,"title":0},"60":{"body":266,"breadcrumbs":0,"title":0},"61":{"body":259,"breadcrumbs":0,"title":0},"62":{"body":5,"breadcrumbs":0,"title":0},"63":{"body":3,"breadcrumbs":0,"title":0},"64":{"body":2,"breadcrumbs":0,"title":0},"65":{"body":11,"breadcrumbs":0,"title":0},"66":{"body":2,"breadcrumbs":0,"title":0},"67":{"body":24,"breadcrumbs":0,"title":0},"68":{"body":28,"breadcrumbs":1,"title":1},"69":{"body":256,"breadcrumbs":0,"title":0},"7":{"body":0,"breadcrumbs":0,"title":0},"70":{"body":43,"breadcrumbs":0,"title":0},"71":{"body":70,"breadcrumbs":0,"title":0},"72":{"body":145,"breadcrumbs":0,"title":0},"73":{"body":219,"breadcrumbs":0,"title":0},"74":{"body":107,"breadcrumbs":0,"title":0},"75":{"body":1,"breadcrumbs":0,"title":0},"76":{"body":145,"breadcrumbs":0,"title":0},"77":{"body":294,"breadcrumbs":0,"title":0},"78":{"body":17,"breadcrumbs":0,"title":0},"79":{"body":2,"breadcrumbs":0,"title":0},"8":{"body":1,"breadcrumbs":0,"title":0},"80":{"body":5,"breadcrumbs":0,"title":0},"81":{"body":130,"breadcrumbs":0,"title":0},"82":{"body":49,"breadcrumbs":0,"title":0},"83":{"body":111,"breadcrumbs":0,"title":0},"84":{"body":32,"breadcrumbs":0,"title":0},"85":{"body":17,"breadcrumbs":0,"title":0},"86":{"body":110,"breadcrumbs":0,"title":0},"87":{"body":74,"breadcrumbs":0,"title":0},"88":{"body":27,"breadcrumbs":0,"title":0},"89":{"body":58,"breadcrumbs":0,"title":0},"9":{"body":0,"breadcrumbs":0,"title":0},"90":{"body":233,"breadcrumbs":0,"title":0},"91":{"body":6,"breadcrumbs":0,"title":0},"92":{"body":130,"breadcrumbs":0,"title":0},"93":{"body":53,"breadcrumbs":0,"title":0},"94":{"body":137,"breadcrumbs":0,"title":0},"95":{"body":40,"breadcrumbs":0,"title":0},"96":{"body":69,"breadcrumbs":1,"title":1},"97":{"body":1,"breadcrumbs":0,"title":0},"98":{"body":5,"breadcrumbs":0,"title":0},"99":{"body":41,"breadcrumbs":0,"title":0}},"docs":{"0":{"body":"Steve Klabnik, Carol Nichols 지음. 기여해주신 러스트 커뮤니티 여러분과 한국어 번역에 참여해주신 분들께 감사드립니다. 이 텍스트 버전은 여러분이 (2023년 2월 9일에 출시된) 러스트 1.67.1 혹은 이후 버전을 사용하고 있음을 가정합니다. 러스트를 설치하거나 업데이트하려면 1장의 ‘설치’절 을 보세요. 이 책은 온라인, 오프라인 모두 제공됩니다. 온라인에는 원본(영문) 이외에도 번역본이 존재하며, 각각 https://doc.rust-lang.org/stable/book/ (영문)에서 읽어보실 수 있습니다. 오프라인 본(영문)은 설치되어있는 rustup의 rustup docs --book 명령어로 열어보실 수 있습니다. 커뮤니티 번역본 도 물론 이용 가능하며, 여러분이 읽고 계시는 한국어 번역본은 다음 링크를 통해 읽으실 수 있습니다: https://rust-kr.github.io/doc.rust-kr.org/ . No Starch Press 에서는 영문 원서가 종이책 및 ebook으로 제공됩니다. **🚨 더 상호작용적인 배움의 경험을 원하시나요? 다른 버전의 러스트 책을 시도해 보세요: 퀴즈, 하이라이팅, 시각화 등등의 기능이 있습니다: https://rust-book.cs.brown.edu","breadcrumbs":"The Rust Programming Language » The Rust Programming Language","id":"0","title":"The Rust Programming Language"},"1":{"body":"콕 집어서 말할 순 없지만, 러스트 프로그래밍 언어는 권한 부여 (empowerment) 에 근간을 두고 있습니다: 여러분이 지금 어떤 종류의 코드를 작성하고 있건 간에, 러스트는 여러분에게 더 많은 권한을 부여하여 프로그래머가 다양한 분야에서 이전보다 더 자신감 있게 프로그래밍할 수 있도록 도와줍니다. 예를 들어 메모리 관리, 데이터 표현, 동시성 등 저수준을 세부적으로 다루는 ‘시스템 수준의’ 프로그래밍을 생각해봅시다. 예로부터 이 분야는 악명 높은 함정을 피하기 위해 수 년 동안 관련 지식을 쌓아온 소수 정예만이 다가갈 수 있는 난해한 영역으로 여겨져 왔습니다. 그리고 이런 사람들마저도 코드가 이용당하거나, 망가지거나, 붕괴하지 않도록 심혈을 기울여 작업해야 합니다. 러스트는 이런 오래된 문제를 제거하는 동시에 일반적인 프로그래머에게 친숙하고 세련된 도구를 제공함으로써 이 장벽들을 부숩니다. 저수준 제어에 ‘살짝만 발을 담글’ 필요가 있는 프로그래머들은 까다로운 툴체인의 세세한 특징을 학습할 필요 없이 러스트만으로도 자신의 목적을 달성할 수 있습니다. 더 좋은 점은 이 언어가 속도와 메모리 사용 측면에서 효율적인 신뢰할 수 있는 코드로 자연스럽게 안내하도록 설계되었다는 점입니다. 이전부터 저수준 코드를 작성하던 프로그래머들은 러스트를 사용하여 야망을 키울 수 있습니다. 예를 들면, 러스트에서 병렬화를 도입하는 것은 비교적 위험도가 낮은 작업입니다: 컴파일러가 고전적인 실수를 잡아주거든요. 또한 실수로 인한 충돌이나 취약점을 발생시키지 않을 것이라는 확신을 가지고 코드에 대한 더 공격적인 최적화를 수행할 수 있습니다. 러스트는 저수준 시스템 프로그래밍에만 국한되지 않습니다. CLI 앱, 웹 서버 및 기타 여러 종류의 코드를 작성할 수 있을 정도로 표현력이 풍부하고 개발자 찬화적으로 설계되어 있습니다 — 이 책의 뒷부분에서 두 경우에 대한 간단한 예제를 볼 것입니다. 러스트로 작업하면 한 분야에서 구축한 기술을 다른 분야에도 써먹을 수 있게 해줍니다; 웹 앱을 작성하는 것으로 러스트를 배운 다음, 동일한 기술을 라즈베리 파이를 대상으로 적용해 볼 수 있지요. 이 책은 러스트의 잠재력을 완전히 담아내어 사용자의 역량을 강화할 수 있도록 노력했습니다. 이 책은 러스트에 대한 지식뿐만 아니라 프로그래머로서의 역량과 자신감도 향상시킬 수 있도록 친근하고 접근하기 쉬운 텍스트로 구성되어 있습니다. 그럼, 바로 시작해서 배울 준비를 해보죠—그리고 러스트 커뮤니티에 오신 것을 환영합니다! — Nicholas Matsakis, Aaron Turon","breadcrumbs":"들어가기에 앞서 » 들어가기에 앞서","id":"1","title":"들어가기에 앞서"},"10":{"body":"먼저, 이 책은 앞에서부터 뒤까지 순서대로 읽는 가정하에 작성되었음을 알려드립니다. 따라서 보통 앞 장에서는 기초 내용을 배우고, 뒷장에서는 앞서 나온 내용을 기반으로 한 심화 내용을 배웁니다. 이 책에는 개념 장과 프로젝트 장의 두 가지 종류가 있습니다. 개념 장에서는 러스트에서의 어떤 개념에 대해 알아봅니다. 프로젝트 장에서는 그간 배운 내용을 적용하여 작은 프로그램을 함께 만들어 봅니다. 2장, 12장, 20장은 프로젝트 장이고 나머지는 개념 장입니다. 1장은 러스트를 설치하고 ‘Hello, world!’ 프로그램을 작성하는 방법, 그리고 러스트의 패키지 매니저 및 빌드 도구인 카고의 사용법을 다룹니다. 2장은 숫자 추리 게임을 직접 작성하면서 러스트로 프로그래밍하는 법을 배웁니다. 이후에 깊이 있게 배울 여러 개념을 추상적으로 다뤄볼 수 있습니다. 자기 손으로 직접 실습해 보는 걸 선호하시는 분에게 제격입니다. 3장은 다른 프로그래밍 언어와 유사한 러스트 특성을 다루는 내용이며, 4장은 소유권 시스템을 다루는 내용입니다. 이 부분은 여러 방법으로 읽을 수 있습니다. 3장을 건너뛰고 바로 4장 소유권 시스템부터 배우거나, 하나씩 차근차근 배우는 걸 선호하면 2장을 건너뛰고 3장부터 본 후, 2장으로 돌아와 배운 내용을 프로젝트에 적용해 볼 수도 있지요. 5장은 구조체 및 메서드를 다루며, 6장은 열거형과 match 표현식, if let 제어 흐름문을 다룹니다. 구조체와 열거형은 앞으로 커스텀 타입을 만드는 데 사용할 겁니다. 7장은 공개 API (Application Programming Interface) 를 만들 때, 작성한 코드와 해당 API를 체계화하기 위한 모듈 시스템 및 접근 권한 규칙을 다루며, 8장은 벡터, 문자열, 해시맵 등 표준 라이브러리가 제공하는 일반적인 컬렉션 자료구조를 다룹니다. 9장에서는 러스트의 에러 처리 철학 및 기법을 알아보겠습니다. 10장은 여러 가지 타입에 적용될 수 있는 코드를 정의하도록 해주는 제네릭 (generic), 트레이트 (trait), 라이프타임 (lifetime) 을 다루며 11장에서는 작성한 프로그램 로직이 잘 작동함을 확인하는 데 필요한 테스트 관련 내용을 다룹니다. 12장에서는 이때까지 배운 수많은 개념을 이용해 커맨드 라인 도구 grep의 기능 일부를 직접 구현해 볼 겁니다. 13장은 클로저 및 반복자를 다룹니다: 함수형 프로그래밍 언어에서 유래된 러스트의 기능입니다. 14장은 카고에 대한 심화 내용 및 여러분이 만든 라이브러리를 남들이 쓸 수 있도록 배포하는 방법을 다룹니다. 15장은 표준 라이브러리가 제공하는 스마트 포인터와 스마트 포인터를 구현하는 트레이트를 다룹니다. 16장에서는 여러 동시성 프로그래밍 모델에 대해 돌아보고, 러스트에서는 어째서 두려움 없이 멀티스레드 프로그래밍을 할 수 있는지 이야기하겠습니다. 17장에서는 여러분에게 익숙할 객체 지향 프로그래밍 원칙과 러스트의 표현 양식 간에 차이를 살펴보겠습니다. 18장은 러스트 프로그램 전반에 걸쳐 아이디어를 표현하는 데 강력한 방법인 패턴, 그리고 패턴 매칭을 참고 자료 형식으로 다룹니다. 19장은 안전하지 않은 러스트, 매크로, 라이프타임, 트레이트, 타입, 함수, 클로저 등 다양한 고급 주제를 다룹니다. 20장에서는 저수준 멀티스레드 웹 서버를 직접 구현하는 것으로 프로젝트 실습을 마칠 예정입니다. 마지막으로, 부록에는 러스트 관련 유용한 정보를 참고 자료 형식으로 담아두었습니다. 부록 A에는 러스트에서 사용하는 키워드들을, 부록 B에는 연산자 및 기호를, 부록 C에는 표준 라이브러리가 제공하는 derivable 트레이트를, 부록 D에는 여러 유용한 개발 도구에 대한 내용을, 부록 E에는 러스트 에디션을 각각 설명합니다. 부록 F에서는 이 책의 번역본에 대해서, 부록 G에서는 러스트와 nightly 러스트가 어떻게 만들어지는지 다룹니다. 이 책은 어떻게 읽든 상관없습니다. 일단 넘기고 싶은 부분은 넘긴 뒤, 뒷부분을 읽다가 내용이 헷갈릴 때 다시 앞으로 돌아와 읽으셔도 됩니다. 다만, 자신에게 가장 도움이 되는 방식대로 읽으시길 권합니다. 러스트를 배우는 과정에서 중요한 부분은 컴파일러가 보여주는 에러 메시지를 읽는 법을 배우는 것입니다: 에러 메시지만 잘 읽어도 코드 속 에러를 고칠 수 있기 때문이지요. 따라서, 여러분이 에러 메시지를 읽는 실력을 자연스럽게 늘릴 수 있도록 컴파일되지 않는 예제 코드와 해당 예제에서 발생하는 에러 메시지를 다양하게 보여드릴 겁니다. 그러니 눈에 보이는 아무 예제나 컴파일을 돌렸더니 에러가 나타나더라도, 일부러 에러가 나타나게 만든 예제일 수 있으니 당황하지 마시고 해당 예제 주위의 글을 읽어보세요. 편의를 위해, 오작동하도록 만든 코드에는 페리스 (Ferris) 가 등장하니 구분하는 데 참고하셔도 좋습니다. 페리스 의미 컴파일되지 않는 코드 패닉이 발생하는 코드 의도대로 작동하지 않는 코드 덧붙이자면, 컴파일되지 않는 코드가 등장하는 내용 중 대부분은 해당 코드가 정상 작동하도록 수정해 나가는 내용입니다.","breadcrumbs":"소개 » 이 책을 어떻게 읽어야 할까요?","id":"10","title":"이 책을 어떻게 읽어야 할까요?"},"100":{"body":"아래처럼 IpAddrKind의 두 개의 배리언트에 대한 인스턴스를 만들 수 있습니다: # enum IpAddrKind {\n# V4,\n# V6,\n# }\n# # fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6;\n# # route(IpAddrKind::V4);\n# route(IpAddrKind::V6);\n# }\n# # fn route(ip_kind: IpAddrKind) {} 열거형을 정의할 때의 식별자로 네임스페이스가 만들어져서, 각 배리언트 앞에 이중 콜론(::)을 붙여야 한다는 점을 주의하세요. 이 방식은 IpAddrKind::V4, IpAddrKind::V6가 모두 IpAddrKind 타입이라는 것을 표현할 수 있기 때문에 유용합니다. 이제 IpAddrKind 타입을 인수로 받는 함수를 정의해 봅시다: # enum IpAddrKind {\n# V4,\n# V6,\n# }\n# # fn main() {\n# let four = IpAddrKind::V4;\n# let six = IpAddrKind::V6;\n# # route(IpAddrKind::V4);\n# route(IpAddrKind::V6);\n# }\n# fn route(ip_kind: IpAddrKind) {} 그리고, 배리언트 중 하나를 사용해서 함수를 호출할 수 있습니다: # enum IpAddrKind {\n# V4,\n# V6,\n# }\n# # fn main() {\n# let four = IpAddrKind::V4;\n# let six = IpAddrKind::V6;\n# route(IpAddrKind::V4); route(IpAddrKind::V6);\n# }\n# # fn route(ip_kind: IpAddrKind) {} 열거형을 사용하면 더 많은 이점이 있습니다. IP 주소 타입에 대해 더 생각해 보면, 지금으로서는 실제 IP 주소 데이터 를 저장할 방법이 없고 어떤 종류 인지만 알 수 입니다. 5장에서 구조체에 대해 배웠다면, 이 문제를 예제 6-1처럼 구조체를 사용하여 해결하고 싶을 수 있겠습니다: # fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from(\"127.0.0.1\"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from(\"::1\"), };\n# } 예제 6-1: struct를 사용해서 IP 주소의 데이터와 IpAddrKind 배리언트 저장하기 여기서는 IpAddrKind (이전에 정의한 열거형) 타입인 kind 필드와 String 타입인 address 필드를 갖는 IpAddr를 정의했습니다. 그리고 이 구조체의 인스턴스 두 개를 생성했습니다. 첫 번째 home은 kind의 값으로 IpAddrKind::V4을, 연관된 주소 데이터로 127.0.0.1를 갖습니다. 두 번째 loopback은 IpAddrKind의 다른 배리언트인 V6을 값으로 갖고, 연관된 주소로 ::1를 갖습니다. kind와 address의 값을 함께 사용하기 위해 구조체를 사용했습니다. 그렇게 함으로써 배리언트가 연관된 값을 갖게 되었습니다. 각 열거형 배리언트에 데이터를 직접 넣는 방식을 사용해서 열거형을 구조체의 일부로 사용하는 방식보다 더 간결하게 동일한 개념을 표현할 수 있습니다. IpAddr 열거형의 새로운 정의에서 두 개의 V4와 V6 배리언트는 연관된 String 타입의 값을 갖게 됩니다: # fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from(\"127.0.0.1\")); let loopback = IpAddr::V6(String::from(\"::1\"));\n# } 열거형의 각 배리언트에 직접 데이터를 붙임으로써, 구조체를 사용할 필요가 없어졌습니다. 또한 여기서 열거형의 동작에 대한 다른 세부 사항을 살펴보기가 좀 더 쉬워졌습니다: 각 열거형 배리언트의 이름이 해당 열거형 인스턴스의 생성자 함수처럼 된다는 것이죠. 즉, IpAddr::V4()는 String 인수를 입력받아서 IpAddr 타입의 인스턴스를 결과를 만드는 함수입니다. 열거형을 정의한 결과로써 이러한 생성자 함수가 자동적으로 정의됩니다. 구조체 대신 열거형을 사용하면 또 다른 장점이 있습니다. 각 배리언트는 다른 타입과 다른 양의 연관된 데이터를 가질 수 있습니다. V4 IP 주소는 항상 0 ~ 255 사이의 숫자 4개로 된 구성 요소를 갖게 될 것입니다. V4 주소에 4개의 u8 값을 저장하길 원하지만, v6 주소는 하나의 String 값으로 표현되길 원한다면, 구조체로는 이렇게 할 수 없습니다. 열거형은 이런 경우를 쉽게 처리합니다: # fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from(\"::1\"));\n# } 두 가지 다른 종류의 IP 주소를 저장하기 위해 코드에 열거형을 정의하는 몇 가지 방법을 살펴봤습니다. 그러나, 누구나 알듯이 IP 주소와 그 종류를 저장하는 것은 흔하기 때문에, 표준 라이브러리에 정의된 것을 사용할 수 있습니다! 표준 라이브러리에서 IpAddr를 어떻게 정의하고 있는지 살펴봅시다. 위에서 정의하고 사용했던 것과 동일한 열거형과 배리언트를 갖고 있지만, 배리언트에 포함된 주소 데이터는 두 가지 다른 구조체로 되어 있으며, 각 배리언트마다 다르게 정의하고 있습니다: struct Ipv4Addr { // --생략--\n} struct Ipv6Addr { // --생략--\n} enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr),\n} 이 코드로 알 수 있듯, 열거형 배리언트에는 어떤 종류의 데이터라도 넣을 수 있습니다. 문자열, 숫자 타입, 구조체 등은 물론, 다른 열거형마저도 포함할 수 있죠! 이건 여담이지만, 러스트의 표준 라이브러리 타입은 여러분의 생각보다 단순한 경우가 꽤 있습니다. 현재 스코프에 표준 라이브러리를 가져오지 않았기 때문에, 표준 라이브러리에 IpAddr 정의가 있더라도 동일한 이름의 타입을 만들고 사용할 수 있음을 주의하세요. 타입을 스코프로 가져오는 것에 대해서는 7장에서 더 살펴보겠습니다. 예제 6-2에 있는 열거형의 다른 예제를 살펴봅시다. 이 예제에서는 각 배리언트에 다양한 종류의 타입들이 포함되어 있습니다: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n}\n# # fn main() {} 예제 6-2: Message 열거형은 각 배리언트가 다른 타입과 다른 양의 값을 저장합니다. 이 열거형에는 다른 데이터 타입을 갖는 네 개의 배리언트가 있습니다: Quit은 연관된 데이터가 전혀 없습니다. Move은 구조체처럼 이름이 있는 필드를 갖습니다. Write은 하나의 String을 가집니다. ChangeColor는 세 개의 i32을 가집니다. 예제 6-2에서처럼 배리언트로 열거형을 정의하는 것은 다른 종류의 구조체들을 정의하는 것과 비슷합니다. 열거형과 다른 점은 struct 키워드를 사용하지 않는다는 것과 모든 배리언트가 Message 타입으로 묶인다는 것입니다. 아래 구조체들은 이전 열거형의 배리언트가 갖는 것과 동일한 데이터를 가질 수 있습니다: struct QuitMessage; // 유닛 구조체\nstruct MoveMessage { x: i32, y: i32,\n}\nstruct WriteMessage(String); // 튜플 구조체\nstruct ChangeColorMessage(i32, i32, i32); // 튜플 구조체\n# # fn main() {} 각기 다른 타입을 갖는 여러 개의 구조체를 사용한다면, 이 메시지 중 어떤 한 가지를 인수로 받는 함수를 정의하기 힘들 것입니다. 예제 6-2에 정의한 Message 열거형은 하나의 타입으로 이것이 가능합니다. 열거형과 구조체는 한 가지 더 유사한 점이 있습니다. 구조체에 impl을 사용해서 메서드를 정의한 것처럼, 열거형에도 정의할 수 있습니다. 여기 Message 열거형에 정의한 call이라는 메서드가 있습니다: # fn main() {\n# enum Message {\n# Quit,\n# Move { x: i32, y: i32 },\n# Write(String),\n# ChangeColor(i32, i32, i32),\n# }\n# impl Message { fn call(&self) { // 메서드 본문이 여기 정의될 것입니다 } } let m = Message::Write(String::from(\"hello\")); m.call();\n# } 메서드 본문에서는 self를 사용하여 호출한 열거형의 값을 가져올 것입니다. 이 예제에서 생성한 변수 m은 Message::Write(String::from(\"hello\")) 값을 갖게 되고, 이 값은 m.call()이 실행될 때 call 메서드 안에서 self가 될 것입니다. 이제 표준 라이브러리에 포함된 열거형 중에서 굉장히 유용하고 자주 사용되는 Option 열거형을 살펴봅시다:","breadcrumbs":"열거형과 패턴 매칭 » 열거형 정의하기 » 열거형 값","id":"100","title":"열거형 값"},"101":{"body":"이번 절에서는 표준 라이브러리에서 열거형으로 정의된 또 다른 타입인 Option에 대한 사용 예를 살펴보겠습니다. Option 타입은 값이 있거나 없을 수 있는 아주 흔한 상황을 나타냅니다. 예를 들어 비어있지 않은 리스트의 첫 번째 아이템을 요청한다면 값을 얻을 수 있을 것입니다. 그렇지만 비어있는 리스트로부터 첫 번째 아이템을 요청한다면 아무 값도 얻을 수 없을 것입니다. 이 개념을 타입 시스템으로 표현한다는 것은 처리해야 하는 모든 경우를 처리했는지 컴파일러가 확인할 수 있다는 의미입니다; 이러한 기능은 다른 프로그래밍 언어에서 매우 흔하게 발생하는 버그를 방지해줍니다. 프로그래밍 언어 디자인은 가끔 어떤 기능들이 포함되었는지의 관점에서 생각되기도 하지만, 어떤 기능을 포함하지 않을 것이냐도 중요합니다. 러스트는 다른 언어들에서 흔하게 볼 수 있는 널 (null) 개념이 없습니다. 널 은 값이 없음을 표현하는 하나의 값입니다. 널 개념이 존재하는 언어에서, 변수의 상태는 둘 중 하나입니다. 널인 경우와, 널이 아닌 경우죠. 널을 고안한 토니 호어 (Tony Hoare) 는 그의 2009년 발표 ‘널 참조: 10억 달러짜리 실수 (Null References: The Billion Dollar Mistake)’에서 다음과 같이 말합니다: 저는 그걸 10억 달러짜리 실수라고 부릅니다. 저는 그 당시 객체 지향 언어에서 참조를 위한 첫 포괄적인 타입 시스템을 디자인하고 있었습니다. 제 목표는 컴파일러에 의해 자동으로 수행되는 체크를 통해 모든 참조자의 사용이 절대로 안전함을 보장하는 것이었습니다. 하지만 구현이 무척 간단하다는 단순한 이유로 널 참조를 넣고 싶은 유혹을 참을 수 없었습니다. 이는 수없이 많은 에러와 취약점, 시스템 종료를 유발했고, 아마도 지난 40년간 10억 달러 수준의 고통과 손실을 초래해왔습니다. 널 값으로 발생하는 문제는, 널 값을 널이 아닌 값처럼 사용하려고 할 때 여러 종류의 에러가 발생할 수 있다는 것입니다. 널이나 널이 아닌 속성은 어디에나 있을 수 있고, 너무나도 쉽게 이런 종류의 에러를 만들어 냅니다. 하지만, ‘현재 어떠한 이유로 인해 유효하지 않거나, 존재하지 않는 하나의 값’이라는 널이 표현하려고 하는 개념은 여전히 유용합니다. 널의 문제는 실제 개념에 있기보다, 특정 구현에 있습니다. 이처럼 러스트에는 널이 없지만, 값의 존재 혹은 부재의 개념을 표현할 수 있는 열거형이 있습니다. 그 열거형이 바로 Option이며, 다음과 같이 표준 라이브러리에 정의되어 있습니다 : enum Option { None, Some(T),\n} Option 열거형은 너무나 유용하기 때문에, 러스트에서 기본으로 임포트하는 목록인 프렐루드에도 포함되어 있습니다. 이것의 배리언트 또한 프렐루드에 포함되어 있습니다: 따라서 Some, None 배리언트 앞에 Option::도 붙이지 않아도 됩니다. 하지만 Option는 여전히 그냥 일반적인 열거형이며, Some(T)와 None도 여전히 Option의 배리언트 입니다. 문법은 아직 다루지 않은 러스트의 기능입니다. 이것은 제네릭 타입 매개변수 (generic type parameter) 이며, 제네릭에 대해서는 10장에서 더 자세히 다룰 것입니다. 지금은 라는 것이 Option 열거형의 Some 배리언트가 어떤 타입의 데이터라도 담을 수 있게 한다는 것, 그리고 T의 자리에 구체적인 타입을 집어넣는 것이 전체 Option 타입을 모두 다른 타입으로 만든다는 것만 알아두면 됩니다. 아래에 숫자 타입과 문자열 타입을 갖는 Option 값에 대한 예들이 있습니다: # fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option = None;\n# } some_number의 타입은 Option입니다. some_char의 타입은 Option이고 둘은 서로 다른 타입입니다. Some 배리언트 내에 어떤 값을 명시했기 때문에 러스트는 이 타입들을 추론할 수 있습니다. absent_number에 대해서는 전반적인 Option 타입을 명시하도록 해야 합니다: None 값만 봐서는 동반되는 Some 배리언트가 어떤 타입의 값을 가질지 컴파일러가 추론할 수 없기 때문입니다. 위 예제에서는 absent_number가 Option 타입임을 명시했습니다. Some 값을 얻게 되면, 값이 존재한다는 것과 해당 값이 Some 내에 있다는 것을 알 수 있습니다. None 값을 얻게 되면, 얻은 값이 유효하지 않다는, 어떤 면에서는 널과 같은 의미를 갖습니다. 그렇다면 왜 Option가 널보다 나을까요? 간단하게 말하면, Option와 T(T는 어떤 타입이던 될 수 있음)이 다른 타입이기 때문에, 컴파일러는 Option 값을 명백하게 유효한 값처럼 사용하지 못하도록 합니다. 예를 들면, 아래 코드는 Option에 i8을 더하려고 하고 있으므로 컴파일되지 않습니다: # fn main() { let x: i8 = 5; let y: Option = Some(5); let sum = x + y;\n# } 이 코드를 실행하면, 아래와 같은 에러 메시지가 출력됩니다: $ cargo run Compiling enums v0.1.0 (file:///projects/enums)\nerror[E0277]: cannot add `Option` to `i8` --> src/main.rs:5:17 |\n5 | let sum = x + y; | ^ no implementation for `i8 + Option` | = help: the trait `Add>` is not implemented for `i8` = help: the following other types implement trait `Add`: <&'a i8 as Add> <&i8 as Add<&i8>> > For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `enums` due to previous error 주목하세요! 실제로, 이 에러 메시지는 러스트가 Option 와 i8를 어떻게 더해야 하는지 모른다는 것을 의미하는데, 둘은 다른 타입이기 때문입니다. 러스트에서 i8과 같은 타입의 값을 가질 때, 컴파일러는 항상 유효한 값을 갖고 있다는 것을 보장할 것입니다. 값을 사용하기 전에 널 인지 확인할 필요도 없이 자신 있게 사용할 수 있습니다. 오직 Option(혹은 어떤 타입이건 간에)을 사용할 경우에만 값이 있을지 없을지에 대해 걱정할 필요가 있으며, 컴파일러는 값을 사용하기 전에 이런 경우가 처리되었는지 확인해 줄 것입니다. 바꿔 말하면, T에 대한 연산을 수행하기 전에 Option를 T로 변환해야 합니다. 이런 방식은 널로 인해 발생하는 가장 흔한 문제인, 실제로는 널인데 널이 아니라고 가정하는 상황을 발견하는 데 도움이 됩니다. 널이 아닌 값을 갖는다는 가정을 놓치는 경우에 대한 위험 요소가 제거되면, 코드에 더 확신을 갖게 됩니다. 널일 수 있는 값을 사용하기 위해서는 명시적으로 값의 타입을 Option로 만들어 줘야 합니다. 그다음엔 값을 사용할 때 명시적으로 널인 경우를 처리해야 합니다. 값의 타입이 Option가 아닌 모든 곳은 값이 널이 아니라고 안전하게 가정할 수 있습니다 . 이것은 널을 너무 많이 사용하는 문제를 제한하고 러스트 코드의 안정성을 높이기 위해 의도된 러스트의 디자인 결정 사항입니다. 그래서, Option 타입인 값을 사용할 때 Some 배리언트에서 T 값을 가져오려면 어떻게 해야 하냐고요? Option 열거형이 가진 메서드는 많고, 저마다 다양한 상황에서 유용하게 쓰일 수 있습니다. 그러니 한번 문서에서 여러분에게 필요한 메서드를 찾아보세요. Option의 여러 메서드를 익혀두면 앞으로의 러스트 프로그래밍에 매우 많은 도움이 될 겁니다. 일반적으로, Option 값을 사용하기 위해서는 각 배리언트를 처리할 코드가 필요할 겁니다. Some(T) 값일 때만 실행돼서 내부의 T 값을 사용하는 코드도 필요할 테고, None 값일 때만 실행될, T 값을 쓸 수 없는 코드도 필요할 겁니다. match 표현식은 열거형과 함께 사용할 때 이런 작업을 수행하는 제어 흐름 구조로, 열거형의 배리언트에 따라 다른 코드를 실행하고 매칭되는 값 내부의 데이터를 해당 코드에서 사용할 수 있습니다.","breadcrumbs":"열거형과 패턴 매칭 » 열거형 정의하기 » Option 열거형이 널 값보다 좋은 점들","id":"101","title":"Option 열거형이 널 값보다 좋은 점들"},"102":{"body":"러스트는 match라고 불리는 매우 강력한 제어 흐름 연산자를 가지고 있는데 이는 일련의 패턴에 대해 어떤 값을 비교한 뒤 어떤 패턴에 매칭되었는지를 바탕으로 코드를 수행하도록 해줍니다. 패턴은 리터럴 값, 변수명, 와일드카드 등 다양한 것으로 구성될 수 있으며, 전체 종류 및 각각의 역할은 18장 에서 배울 예정입니다. match의 힘은 패턴의 표현성으로부터 오며 컴파일러는 모든 가능한 경우가 처리되는지 검사합니다. match 표현식을 동전 분류기와 비슷한 종류로 생각해 보세요. 동전들은 다양한 크기의 구멍들이 있는 트랙으로 미끄러져 내려가고, 각 동전은 그것에 맞는 첫 번째 구멍을 만났을 때 떨어집니다. 동일한 방식으로, 값들은 match 내의 각 패턴을 통과하고, 해당 값에 ‘맞는’ 첫 번째 패턴에서, 그 값은 실행 중에 사용될 연관된 코드 블록 안으로 떨어질 것입니다. 동전 이야기가 나왔으니, match를 이용한 예제로 동전들을 이용해 봅시다! 예제 6-3에서 보는 바와 같이, 어떤 모르는 미국 동전을 입력받아서, 동전 계수기와 동일한 방식으로 그 동전이 어떤 것이고 센트로 해당 값을 반환하는 함수를 작성할 수 있습니다. enum Coin { Penny, Nickel, Dime, Quarter,\n} fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }\n}\n# # fn main() {} 예제 6-3: 열거형과 열거형의 배리언트를 패턴으로 사용하는 match 표현식 value_in_cents 함수 내의 match를 쪼개 봅시다. 먼저 match 키워드 뒤에 표현식을 써줬는데, 위의 경우에는 coin 값입니다. 이는 if 에서 사용하는 조건식과 매우 유사하지만, 큰 차이점이 있습니다. if를 사용할 경우에는 조건문에서 부울린 값을 반환해야 하지만, 여기서는 어떤 타입이든 가능합니다. 위 예제에서 coin의 타입은 첫째 줄에서 정의했던 Coin 열거형입니다. 그다음은 match 갈래 (arm) 들입니다. 하나의 갈래는 패턴과 코드 두 부분으로 이루어져 있습니다. 여기서의 첫 번째 갈래에는 값 Coin::Penny로 되어있는 패턴이 있고 그 뒤에 패턴과 실행되는 코드를 구분해 주는 => 연산자가 있습니다. 위의 경우에서 코드는 그냥 값 1입니다. 각 갈래는 그다음 갈래와 쉼표로 구분됩니다. match 표현식이 실행될 때, 결괏값을 각 갈래의 패턴에 대해서 순차적으로 비교합니다. 만일 어떤 패턴이 그 값과 매칭되면, 그 패턴과 연관된 코드가 실행됩니다. 만일 그 패턴이 값과 매칭되지 않는다면, 동전 분류기와 비슷하게 다음 갈래로 실행을 계속합니다. 각 갈래와 연관된 코드는 표현식이고, 이 매칭 갈래에서의 표현식의 결과로써 생기는 값은 전체 match 표현식에 대해 반환되는 값입니다. 각 갈래가 그냥 값을 반환하는 예제 6-3에서처럼 매치 갈래의 코드가 짧다면, 중괄호는 보통 사용하지 않습니다. 만일 매치 갈래 내에서 여러 줄의 코드를 실행시키고 싶다면 중괄호를 사용하고, 그렇게 되면 갈래 뒤에 붙이는 쉼표는 옵션이 됩니다. 예를 들어, 아래의 코드는 Coin::Penny와 함께 메서드가 호출될 때마다 ‘Lucky penny!’를 출력하지만, 여전히 해당 블록의 마지막 값인 1을 반환할 것입니다: # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter,\n# }\n# fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => { println!(\"Lucky penny!\"); 1 } Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }\n}\n# # fn main() {}","breadcrumbs":"열거형과 패턴 매칭 » match 제어 흐름 구조 » match 제어 흐름 구조","id":"102","title":"match 제어 흐름 구조"},"103":{"body":"매치 갈래의 또 다른 유용한 기능은 패턴과 매칭된 값들의 일부분을 바인딩할 수 있다는 것입니다. 이것이 열거형의 배리언트로부터 어떤 값들을 추출할 수 있는 방법입니다. 한 가지 예로, 열거형 배리언트 중 하나가 내부에 값을 들고 있도록 바꿔봅시다. 1999년부터 2008년까지, 미국은 각 50개 주마다 한쪽 면의 디자인이 다른 쿼터 동전을 주조했습니다. 다른 동전들은 주의 디자인을 갖지 않고, 따라서 오직 쿼터 동전들만 이 특별 값을 갖습니다. 이 정보를 Quarter 배리언트 내에 UsState 값을 담도록 enum을 변경하여 추가할 수 있는데, 이는 예제 6-4와 같습니다: #[derive(Debug)] // so we can inspect the state in a minute\nenum UsState { Alabama, Alaska, // --생략--\n} enum Coin { Penny, Nickel, Dime, Quarter(UsState),\n}\n# # fn main() {} 예제 6-4: Quarter 배리언트가 UsState 값도 담고 있는 Coin 열거형 한 친구가 모든 50개 주 쿼터 동전을 모으기를 시도하는 중이라고 상상해 봅시다. 동전의 종류에 따라 동전을 분류하는 동안 각 쿼터 동전에 연관된 주의 이름을 외치기도 해서, 만일 그것이 친구가 가지고 있지 않은 것이라면, 그 친구는 자기 컬렉션에 그 동전을 추가할 수 있겠지요. 이 코드를 위한 매치 표현식 내에서는 배리언트 Coin::Quarter의 값과 매칭되는 패턴에 state라는 이름의 변수를 추가합니다. Coin::Quarter이 매치될 때, state 변수는 그 쿼터 동전의 주에 대한 값에 바인딩될 것입니다. 그러면 우리는 다음과 같이 해당 갈래에서의 코드 내에서 state를 사용할 수 있습니다: # #[derive(Debug)]\n# enum UsState {\n# Alabama,\n# Alaska,\n# // --생략--\n# }\n# # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter(UsState),\n# }\n# fn value_in_cents(coin: Coin) -> u8 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!(\"State quarter from {:?}!\", state); 25 } }\n}\n# # fn main() {\n# value_in_cents(Coin::Quarter(UsState::Alaska));\n# } 만일 우리가 value_in_cents(Coin::Quarter(UsState::Alaska))를 호출했다면, coin은 Coin::Quarter(UsState::Alaska)가 되겠지요. 각각의 매치 갈래와 이 값을 비교하면, Coin::Quarter(state)에 도달할 때까지 아무것에도 매칭되지 않습니다. 이 시점에서, state에 대한 바인딩은 값 UsState::Alaska가 될 것입니다. 그러면 이 바인딩을 println! 표현식에서 사용할 수 있고, 따라서 Quarter에 대한 Coin 열거형 배리언트로부터 주에 대한 내부 값을 얻었습니다.","breadcrumbs":"열거형과 패턴 매칭 » match 제어 흐름 구조 » 값을 바인딩하는 패턴","id":"103","title":"값을 바인딩하는 패턴"},"104":{"body":"이전 절에서 Option 값을 사용하려면 Some일 때 실행돼서, Some 내의 T 값을 얻을 수 있는 코드가 필요하다고 했었죠. 이제 Coin 열거형을 다뤘던 것처럼 Option도 match로 다뤄보도록 하겠습니다. 동전들을 비교하는 대신 Option의 배리언트를 비교하겠지만, match 표현식이 동작하는 방식은 동일합니다. Option를 매개변수로 받아서, 내부에 값이 있으면 그 값에 1을 더하는 함수를 작성하고 싶다고 칩시다. 만일 내부에 값이 없으면, 이 함수는 None 값을 반환하고 다른 어떤 연산도 수행하는 시도를 하지 않아야 합니다. match에 감사하게도, 이 함수는 매우 작성하기 쉽고, 예제 6-5처럼 보일 것입니다: # fn main() { fn plus_one(x: Option) -> Option { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None);\n# } 예제 6-5: Option 상에서 match를 이용하는 함수 plus_one의 첫 번째 실행을 좀 더 자세히 시험해 봅시다. plus_one(five)가 호출될 때, plus_one의 본문 안에 있는 변수 x는 값 Some(5)를 갖게 될 것입니다. 그런 다음 각각의 매치 갈래에 대하여 이 값을 비교합니다: # fn main() {\n# fn plus_one(x: Option) -> Option {\n# match x { None => None,\n# Some(i) => Some(i + 1),\n# }\n# }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } Some(5) 값은 패턴 None과 매칭되지 않으므로, 다음 갈래로 계속 갑니다: # fn main() {\n# fn plus_one(x: Option) -> Option {\n# match x {\n# None => None, Some(i) => Some(i + 1),\n# }\n# }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } Some(5)가 Some(i)랑 매칭되나요? 그렇습니다! 동일한 배리언트를 갖고 있습니다. Some 내부에 담긴 값은 i에 바인딩되므로, i는 값 5를 갖습니다. 그런 다음 매치 갈래 내의 코드가 실행되므로, i의 값에 1을 더한 다음 최종적으로 6을 담은 새로운 Some 값을 생성합니다. 이제 x가 None인 예제 6-5에서의 plus_one의 두 번째 호출을 살펴봅시다. match 안으로 들어와서 첫 번째 갈래와 비교합니다: # fn main() {\n# fn plus_one(x: Option) -> Option {\n# match x { None => None,\n# Some(i) => Some(i + 1),\n# }\n# }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } 매칭되었군요! 더할 값이 없으므로, 프로그램은 멈추고 =>의 우측 편에 있는 None 값을 반환합니다. 첫 번째 갈래에 매칭되었으므로, 다른 갈래와는 비교하지 않습니다. match와 열거형을 조합하는 것은 다양한 경우에 유용합니다. 여러분은 러스트 코드에서 이러한 패턴을 많이 보게 될 겁니다. 열거형에 대한 match, 내부의 데이터에 변수 바인딩, 그런 다음 그에 대한 수행 코드 말이지요. 처음에는 약간 까다롭지만, 일단 익숙해지면 이를 모든 언어에서 쓸 수 있게 되기를 바랄 것입니다. 이것은 꾸준히 사용자들이 가장 좋아하는 기능입니다.","breadcrumbs":"열거형과 패턴 매칭 » match 제어 흐름 구조 » Option를 이용하는 매칭","id":"104","title":"Option를 이용하는 매칭"},"105":{"body":"우리가 논의할 필요가 있는 match의 다른 관점이 있습니다: 갈래의 패턴들은 모든 가능한 경우를 다루어야 합니다. plus_one 함수의 아래 버전을 고려해 봅시다. 버그가 있고 컴파일되지 않지만요: # fn main() { fn plus_one(x: Option) -> Option { match x { Some(i) => Some(i + 1), } }\n# # let five = Some(5);\n# let six = plus_one(five);\n# let none = plus_one(None);\n# } 여기서는 None 케이스를 다루지 않았고, 따라서 이 코드는 버그를 일으킬 것입니다. 다행히도 러스트이 이 버그를 어떻게 잡는지 알고 있습니다. 이 코드의 컴파일을 시도하면, 아래와 같은 에러를 얻게 됩니다: $ cargo run Compiling enums v0.1.0 (file:///projects/enums)\nerror[E0004]: non-exhaustive patterns: `None` not covered --> src/main.rs:3:15 |\n3 | match x { | ^ pattern `None` not covered |\nnote: `Option` defined here --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:518:1 | = note: /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/core/src/option.rs:522:5: not covered = note: the matched value is of type `Option`\nhelp: ensure that all possible cases are being handled by adding a match arm with a wildcard pattern or an explicit pattern as shown |\n4 ~ Some(i) => Some(i + 1),\n5 ~ None => todo!(), | For more information about this error, try `rustc --explain E0004`.\nerror: could not compile `enums` due to previous error 러스트의 매치는 철저합니다 (exhaustive) . 발생할 수 있는 경우 중 놓친 게 있음을 아는 것은 물론, 어떤 패턴을 놓쳤는가도 알고 있죠. 따라서 유효한 코드를 만들려면 모든 가능성을 샅샅이 다루어야 합니다. 이로써 발생하는 장점은 Option 에서도 드러납니다. None 케이스를 다루는 것을 깜박하더라도 러스트가 알아채고 알려주기 때문에, 앞서 말했던 널일지도 모를 값을 가지고 있어서 발생할 수 있는 수십억 달러짜리 실수를 불가능하게 만듭니다.","breadcrumbs":"열거형과 패턴 매칭 » match 제어 흐름 구조 » 매치는 철저합니다","id":"105","title":"매치는 철저합니다"},"106":{"body":"열거형을 사용하면서 특정한 몇 개의 값들에 대해 특별한 동작을 하지만, 그 외의 값들에 대해서는 기본 동작을 취하도록 할 수도 있습니다. 어떤 게임을 구현하는 중인데 주사위를 굴려서 3이 나오면 플레이어는 움직이는 대신 새 멋진 모자를 얻고, 7을 굴리면 플레이어는 그 모자를 잃게 된다고 생각해 봅시다. 그 외의 값들에 대해서는 게임판 위에서 해당 숫자만큼 칸을 움직입니다. 이러한 로직을 구현한 match를 볼 것인데, 실제로 이를 구현하는 것은 이 예제의 범위를 벗어나므로 임의의 값 대신 하드코딩된 주사위 눈 결과를 사용하고, 그 밖의 로직들은 본문 없는 함수로 작성하겠습니다: # fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), other => move_player(other), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn move_player(num_spaces: u8) {}\n# } 처음 두 갈래에서의 패턴은 3과 7 리터럴 값입니다. 나머지 모든 가능한 값을 다루는 마지막 갈래에 대한 패턴은 other라는 이름을 가진 변수입니다. other 갈래 쪽의 코드는 이 변숫값을 move_player 함수에 넘기는 데 사용합니다. u8이 가질 수 있는 모든 값을 나열하지 않았음에도 이 코드는 컴파일 되는데, 그 이유는 특별하게 나열되지 않은 나머지 모든 값에 대해 마지막 패턴이 매칭될 것이기 때문입니다. 이러한 포괄 (catch-all) 패턴은 match의 철저함을 만족시킵니다. 패턴들은 순차적으로 평가되므로 마지막에 포괄적인 갈래를 위치시켜야 한다는 점을 기억해 둡시다. 포괄적인 갈래를 이보다 앞에 두면 그 뒤에 있는 갈래는 결코 실행될 수 없으므로, 만약 포괄 패턴 뒤에 갈래를 추가하면 러스트는 이에 대해 경고를 줍니다! 포괄 패턴이 필요한데 그 포괄 패턴의 값을 사용 할 필요는 없는 경우에 쓸 수 있는 패턴도 있습니다: _는 어떠한 값이라도 매칭되지만, 그 값을 바인딩하지는 않는 특별한 패턴입니다. 이는 러스트에게 해당 값을 사용하지 않겠다는 것을 알려주므로, 러스트는 사용되지 않는 변수에 대한 경고를 띄우지 않을 것입니다. 게임의 규칙을 바꿔봅시다: 이제부터 주사위를 굴려 3 혹은 7 이외의 숫자가 나왔다면 주사위를 다시 굴립니다. 그러면 더 이상 포괄 패턴의 값을 사용할 필요가 없으므로, other라는 이름의 변수 대신 _를 사용하여 코드를 고칠 수 있습니다: # fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => reroll(), } fn add_fancy_hat() {} fn remove_fancy_hat() {} fn reroll() {}\n# } 이 예제 또한 철저함에 대한 요구사항을 충족하는데, 마지막 갈래에서 나머지 모든 값에 대해 명시적으로 무시하기 때문입니다; 우리는 아무것도 잊어버리지 않았습니다. 마지막으로, 게임의 규칙을 한 번 더 바꿔서 3이나 7 이외의 숫자를 굴리게 되면 아무 일도 일어나지 않도록 해보겠습니다. 이는 _ 갈래에 ( ‘튜플 타입’ 에서 다루었던) 유닛 값을 사용하여 표현할 수 있습니다: # fn main() { let dice_roll = 9; match dice_roll { 3 => add_fancy_hat(), 7 => remove_fancy_hat(), _ => (), } fn add_fancy_hat() {} fn remove_fancy_hat() {}\n# } 여기에서는 러스트에게 명시적으로 앞의 갈래에 매칭되지 않은 어떠한 값도 사용하지 않을 것이며, 어떠한 코드도 실행하지 않기를 원한다고 명시적으로 알려준 것입니다. 패턴과 매칭에 관한 더 많은 내용이 18장 에 있습니다. 지금은 match 표현식이 좀 장황할 경우에 유용한 if let 문법으로 넘어가겠습니다.","breadcrumbs":"열거형과 패턴 매칭 » match 제어 흐름 구조 » 포괄 패턴과 _ 자리표시자","id":"106","title":"포괄 패턴과 _ 자리표시자"},"107":{"body":"if let 문법은 if와 let을 조합하여 하나의 패턴만 매칭시키고 나머지 경우는 무시하도록 값을 처리하는 간결한 방법을 제공합니다. 예제 6-6의 프로그램은 config_max 변수의 어떤 Option 값을 매칭하지만 그 값이 Some 배리언트일 경우에만 코드를 실행시키고 싶어 하는 예제를 보여줍니다: # fn main() { let config_max = Some(3u8); match config_max { Some(max) => println!(\"The maximum is configured to be {}\", max), _ => (), }\n# } 예제 6-6: 어떤 값이 Some 일 때에만 코드를 실행하도록 하는 match 이 값이 Some이면 패턴 내에 있는 max에 Some 배리언트의 값을 바인딩하고 출력합니다. None 값에 대해서는 아무 처리도 하지 않으려고 합니다. match 표현식을 만족시키려면 딱 하나의 배리언트 처리 후 _ => ()를 붙여야 하는데, 이는 다소 성가신 보일러 플레이트 코드입니다. 그 대신, if let을 이용하여 이 코드를 더 짧게 쓸 수 있습니다. 아래의 코드는 예제 6-6에서의 match와 동일하게 동작합니다: # fn main() { let config_max = Some(3u8); if let Some(max) = config_max { println!(\"The maximum is configured to be {}\", max); }\n# } if let은 =로 구분된 패턴과 표현식을 입력받습니다. 이는 match와 동일한 방식으로 작동하는데, 여기서 표현식은 match에 주어지는 것이고 패턴은 이 match의 첫 번째 갈래와 같습니다. 위의 경우 패턴은 Some(max)이고 max는 Some 내에 있는 값에 바인딩됩니다. 그렇게 되면 match의 갈래 안에서 max를 사용했던 것과 같은 방식으로 if let 본문 블록 내에서 max를 사용할 수 있습니다. if let을 이용하면 여러분이 덜 타이핑하고, 덜 들여 쓰기 하고, 보일러 플레이트 코드를 덜 쓰게 됩니다. 하지만, match가 강제했던 철저한 검사를 안하게 되었습니다. match와 if let 사이에서 선택하는 것은 여러분의 특정 상황에서 여러분이 하고 있는 것에 따라, 그리고 간결함을 얻는 것이 철저한 검사를 안하게 되는 것에 대한 적절한 거래인지에 따라 달린 문제입니다. 즉, if let은 한 패턴에 매칭될 때만 코드를 실행하고 다른 경우는 무시하는 match 문을 작성할 때 사용하는 문법 설탕 (syntax sugar) 이라고 생각하시면 됩니다. if let과 함께 else를 포함시킬 수 있습니다. else 뒤에 나오는 코드 블록은 match 표현식에서 _ 케이스 뒤에 나오는 코드 블록과 동일합니다. 예제 6-4에서 Quarter 배리언트가 UsState 값도 들고 있었던 Coin 열거형 정의부를 상기해 보세요. 만일 쿼터가 아닌 모든 동전을 세고 싶은 동시에 쿼터 동전일 경우도 알려주고 싶다면, 아래와 같이 match문을 쓸 수도 있을 겁니다: # #[derive(Debug)]\n# enum UsState {\n# Alabama,\n# Alaska,\n# // --생략--\n# }\n# # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter(UsState),\n# }\n# # fn main() {\n# let coin = Coin::Penny; let mut count = 0; match coin { Coin::Quarter(state) => println!(\"State quarter from {:?}!\", state), _ => count += 1, }\n# } 혹은 아래와 같이 if let과 else 표현식을 이용할 수도 있겠지요: # #[derive(Debug)]\n# enum UsState {\n# Alabama,\n# Alaska,\n# // --생략--\n# }\n# # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter(UsState),\n# }\n# # fn main() {\n# let coin = Coin::Penny; let mut count = 0; if let Coin::Quarter(state) = coin { println!(\"State quarter from {:?}!\", state); } else { count += 1; }\n# } 만일 여러분의 프로그램이 match로 표현하기에는 너무 장황한 로직을 가지고 있는 경우라면, 러스트 도구 상자에 if let도 있음을 기억하세요.","breadcrumbs":"열거형과 패턴 매칭 » if let을 사용한 간결한 제어 흐름 » if let을 사용한 간결한 제어 흐름","id":"107","title":"if let을 사용한 간결한 제어 흐름"},"108":{"body":"지금까지 열거형을 사용하여 열거한 값의 집합 중에서 하나가 될 수 있는 커스텀 타입을 만드는 방법에 대해 알아보았습니다. 표준 라이브러리의 Option 타입이 타입 시스템을 사용하여 에러를 방지하는 데 어떻게 도움이 되는지도 살펴봤습니다. 열거형 값에 데이터가 있는 경우, 처리해야 하는 경우의 수에 따라 match나 if let을 사용하여 해당 값을 추출하여 사용할 수 있습니다. 여러분은 이제 구조체와 열거형을 이용해 원하는 개념을 표현할 수 있습니다. 또한, 여러분의 API 내에 커스텀 타입을 만들어서 사용하면, 작성한 함수가 원치 않는 값으로 작동하는 것을 컴파일러가 막아주기 때문에 타입 안정성도 보장받을 수 있습니다. 여러분의 사용자에게 사용하기 직관적이고 필요로 하는 것만 정확하게 노출된, 잘 조직된 API를 제공하기 위해서, 이제 러스트의 모듈로 넘어갑시다.","breadcrumbs":"열거형과 패턴 매칭 » if let을 사용한 간결한 제어 흐름 » 정리","id":"108","title":"정리"},"109":{"body":"거대한 프로그램을 작성할 때는 코드의 구조화가 무척 중요해집니다. 코드에서 연관된 기능을 묶고 서로 다른 기능을 분리해 두면 이후 특정 기능을 구현하는 코드를 찾거나 변경할 때 헤매지 않게 됩니다. 앞서 작성한 프로그램들은 하나의 모듈, 하나의 파일로 이루어져 있었지만, 프로젝트 규모가 커지면 코드를 여러 모듈, 여러 파일로 나누어 관리해야 합니다. 한 패키지에는 여러 개의 바이너리 크레이트와 (원할 경우) 라이브러리 크레이트를 포함될 수 있으므로, 커진 프로젝트의 각 부분을 크레이트로 나눠서 외부 라이브러리처럼 쓸 수 있습니다. 이번 장에서 배워 볼 것은 이러한 기법들입니다. 상호연관된 패키지들로 이루어진 대규모 프로젝트의 경우에는 14장 ‘카고 작업공간’ 절에서 다룰 예정인, 카고에서 제공하는 작업공간 (workspace) 기능을 이용합니다. 또한 세부 구현을 캡슐화하면 더 고수준에서 코드를 재사용할 수 있는 방법에 대해서도 설명합니다. 일단 어떤 연산을 구현하면 그 구현체의 작동 방식을 몰라도 다른 코드에서 공개 인터페이스를 통해 해당 코드를 호출할 수 있습니다. 코드를 작성하는 방식에 따라 다른 코드가 사용할 수 있는 공개 부분과 변경 권한을 작성자에게 남겨두는 비공개 구현 세부 사항이 정의됩니다. 이는 머릿속에 기억해 둬야 하는 세부 사항의 양을 제한하는 또 다른 방법입니다. 스코프 개념도 관련되어 있습니다. 중첩된 컨텍스트에 작성한 코드는 ‘스코프 내에’ 정의된 다양한 이름들이 사용됩니다. 프로그래머나 컴파일러가 코드를 읽고, 쓰고, 컴파일할 때는 특정 위치의 특정 이름이 무엇을 의미하는지 알아야 합니다. 해당 이름이 변수인지, 함수인지, 열거형인지, 모듈인지, 상수인지, 그 외 아이템인지 말이죠. 스코프를 생성하고 스코프 안 혹은 바깥에 있는 이름을 변경할 수 있습니다. 동일한 스코프 내에는 같은 이름을 가진 아이템이 둘 이상 있을 수 없으며, 이름 충돌을 해결하는 도구를 사용할 수 있습니다. 러스트에는 코드 조직화에 필요한 기능이 여럿 있습니다. 어떤 세부 정보를 외부에 노출할지, 비공개로 둘지, 프로그램의 스코프 내 어떤 이름이 있는지 등 다양합니다. 이를 통틀어 모듈 시스템 이라 하며, 다음 기능들이 포함됩니다: 패키지: 크레이트를 빌드하고, 테스트하고, 공유하는 데 사용하는 카고 기능입니다. 크레이트: 라이브러리나 실행 가능한 모듈로 구성된 트리 구조입니다. 모듈 과 use: 구조, 스코프를 제어하고, 조직 세부 경로를 감추는 데 사용합니다. 경로 : 구조체, 함수, 모듈 등의 이름을 지정합니다. 이번 장에서는 이 기능들을 모두 다뤄보면서 이 기능들이 상호작용하는 방식을 논의하고 이를 사용하여 스코프를 관리하는 방법을 설명하겠습니다. 이번 장을 마치고 나면, 모듈 시스템을 확실히 이해하고 프로처럼 스코프를 다룰 수 있을 거랍니다!","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기","id":"109","title":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기"},"11":{"body":"이 책을 만드는 데 사용한 원본 파일은 GitHub (한국어 번역본) 에서 찾아보실 수 있습니다.","breadcrumbs":"소개 » 소스 코드","id":"11","title":"소스 코드"},"110":{"body":"여기서 다룰 모듈 시스템의 첫 부분은 패키지와 크레이트입니다. 크레이트 (crate) 는 러스트가 컴파일 한 차례에 고려하는 가장 작은 코드 단위입니다. (1장의 ‘러스트 프로그램 작성하고 실행하기’절에서 했던 것처럼) cargo 대신 rustc를 실행하여 단일 소스 코드 파일을 넘겨주더라도, 컴파일러는 그 파일이 크레이트라고 생각합니다. 크레이트는 여러 모듈을 담을 수 있고, 다음 절에서 곧 알게 되겠지만 모듈은 이 크레이트와 함께 컴파일되는 다른 파일들에 정의되어 있을 수도 있습니다. 크레이트는 바이너리일 수도 있고, 라이브러리일 수도 있습니다. 바이너리 크레이트 (binary crate) 는 커맨드 라인 프로그램이나 서버처럼 실행 가능한 실행파일로 컴파일할 수 있는 프로그램입니다. 바이너리 크레이트는 실행파일이 실행되면 무슨 일이 일어나는지를 정의한 main 함수를 포함하고 있어야 합니다. 여태껏 만들어 본 모든 크레이트는 바이너리 크레이트였습니다. 라이브러리 크레이트 (library crate) 는 main 함수를 가지고 있지 않고 실행파일 형태로 컴파일되지 않습니다. 그 대신, 여러 프로젝트에서 공용될 의도로 만들어진 기능들이 정의되어 있습니다. 예를 들어, 2장 에서 사용한 rand 크레이트는 난수를 생성하는 기능을 제공합니다. 러스타시안들이 ‘크레이트’라고 말하면 대부분은 이 라이브러리 크레이트를 의미하는 것이고, ‘크레이트’라는 단어는 일반적인 프로그래밍 개념에서의 ‘라이브러리’와 혼용됩니다. 크레이트 루트 (crate root) 는 러스트 컴파일러가 컴파일을 시작하는 소스 파일이고, 크레이트의 루트 모듈을 구성합니다. (모듈은 ‘모듈을 정의하여 스코프 및 공개 여부 제어하기’ 에서 알아볼 예정입니다.) 패키지 (package) 는 일련의 기능을 제공하는 하나 이상의 크레이트로 구성된 번들입니다. 패키지에는 이 크레이트들을 빌드하는 법이 설명된 Cargo.toml 파일이 포함되어 있습니다. 카고는 실제로 코드를 빌드하는 데 사용하는 커맨드 라인 도구의 바이너리 크레이트가 포함된 패키지입니다. 카고 패키지에는 또한 이 바이너리 크레이트가 의존하고 있는 라이브러리 패키지도 포함되어 있습니다. 다른 프로젝트도 카고의 라이브러리 크레이트에 의존하여 카고의 커맨드 라인 도구가 사용하는 것과 동일한 로직을 사용할 수 있습니다. 패키지에는 여러 개의 바이너리 크레이트가 원하는 만큼 포함될 수 있지만, 라이브러리 크레이트는 하나만 넣을 수 있습니다. 패키지에는 적어도 하나 이상의 크레이트가 포함되어야 하며, 이는 라이브러리든 바이너리든 상관없습니다. 패키지를 생성할 때 어떤 일이 일어나는지 살펴봅시다. 먼저 cargo new 명령어를 입력합니다. $ cargo new my-project Created binary (application) `my-project` package\n$ ls my-project\nCargo.toml\nsrc\n$ ls my-project/src\nmain.rs cargo new를 실행한 후 ls 명령을 사용하여 카고가 만든 것들을 살펴봅니다. 프로젝트 디렉터리에는 Cargo.toml 파일이 있는데, 이것이 패키지를 만들어 줍니다. main.rs 파일을 가지고 있는 src 라는 디렉터리도 있습니다. Cargo.toml 을 텍스트 편집기로 열어보면 src/main.rs 가 따로 적시되진 않음을 알 수 있습니다. 카고는 패키지명과 같은 이름의 바이너리 크레이트는 src/main.rs 가 크레이트 루트라는 관례를 준수합니다. 마찬가지로, 패키지 디렉터리에 src/lib.rs 파일이 존재할 경우, 카고는 해당 패키지가 패키지명과 같은 이름의 라이브러리 크레이트를 포함하고 있다고 판단합니다. 그리고 그 라이브러리 크레이트의 크레이트 루트는 src/lib.rs 고요. 카고는 라이브러리 혹은 바이너리를 빌드할 때 이 크레이트 루트 파일을 rustc에게 전달합니다. 현재 패키지는 src/main.rs 만 포함하고 있으므로 이 패키지는 my-project라는 이름의 바이너리 크레이트만으로 구성되어 있습니다. 만약 어떤 패키지가 src/main.rs 와 src/lib.rs 를 가지고 있다면 해당 패키지는 패키지와 같은 이름의 바이너리, 라이브러리 크레이트를 포함하게 됩니다. src/bin 디렉터리 내에 파일을 배치하면 각각의 파일이 바이너리 크레이트가 되어, 여러 바이너리 크레이트를 패키지에 포함할 수 있습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 패키지와 크레이트 » 패키지와 크레이트","id":"110","title":"패키지와 크레이트"},"111":{"body":"이번에는 모듈, 아이템의 이름을 지정하는 경로 (path) , 스코프에 경로를 가져오는 use 키워드, 아이템을 공개하는 데 사용하는 pub 키워드를 알아보겠습니다. as 키워드, 외부 패키지, 글롭 (glob) 연산자 등도 다룰 예정입니다. 우선은 여러분이 미래에 코드를 구조화할 때 쉽게 참조할 수 있는 규칙을 나열하는 것으로 시작해 보겠습니다. 그다음 각각의 규칙에 대한 세부 사항을 설명해 보겠습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 모듈을 정의하여 스코프 및 공개 여부 제어하기 » 모듈을 정의하여 스코프 및 공개 여부 제어하기","id":"111","title":"모듈을 정의하여 스코프 및 공개 여부 제어하기"},"112":{"body":"아래에 모듈, 경로, use, pub 키워드가 컴파일러에서 동작하는 방법과 대부분의 개발자가 코드를 구성하는 방법에 대한 빠른 참고 자료가 있습니다. 이 장을 거치면서 각각의 규칙에 대한 예제를 살펴볼 것이지만, 이곳이 모듈의 작동 방법을 기억하는 데에 참조할 좋은 위치가 되겠습니다. 크레이트 루트부터 시작 : 크레이트를 컴파일할 때 컴파일러는 먼저 크레이트 루트 파일을 봅니다 (보통은 라이브러리 크레이트의 경우 src/lib.rs 혹은 바이너리 크레이트의 경우 src/main.rs 입니다). 모듈 선언 : 크레이트 루트 파일에는 새로운 모듈을 선언할 수 있습니다; mod garden;이라는 코드로 ‘garden’ 모듈을 선언할 수 있습니다. 컴파일러는 아래의 장소에서 이 모듈의 코드가 있는지 살펴볼 것입니다: mod garden 뒤에 세미콜론 대신 중괄호를 써서 안쪽에 코드를 적은 인라인 src/garden.rs 파일 안 src/garden/mod.rs 파일 안 서브모듈 선언 : 크레이트 루트가 아닌 다른 파일에서는 서브모듈 (submodule) 을 선언할 수 있습니다. 예를 들면 src/garden.rs 안에 mod vegetables;를 선언할 수도 있습니다. 컴파일러는 부모 모듈 이름의 디렉터리 안쪽에 위치한 아래의 장소들에서 이 서브모듈의 코드가 있는지 살펴볼 것입니다: mod vegetables 뒤에 세미콜론 대신 중괄호를 써서 안쪽에 코드를 적은 인라인 src/garden/vegetables.rs 파일 안 src/garden/vegetables/mod.rs 파일 안 모듈 내 코드로의 경로 : 일단 모듈이 크레이트의 일부로서 구성되면, 공개 규칙이 허용하는 한도 내에서라면 해당 코드의 경로를 사용하여 동일한 크레이트의 어디에서든 이 모듈의 코드를 참조할 수 있게 됩니다. 예를 들면, garden vegetables 모듈 안에 있는 Asparagus 타입은 crate::garden::vegetables::Asparagus로 찾아 쓸 수 있습니다. 비공개 vs 공개 : 모듈 내의 코드는 기본적으로 부모 모듈에게 비공개 (private) 입니다. 모듈을 공개 (public) 로 만들려면, mod 대신 pub mod를 써서 선언하세요. 공개 모듈의 아이템들을 공개하려면 마찬가지로 그 선언 앞에 pub을 붙이세요. use 키워드 : 어떤 스코프 내에서 use 키워드는 긴 경로의 반복을 줄이기 위한 어떤 아이템으로의 단축경로를 만들어 줍니다. crate::garden::vegetables::Asparagus를 참조할 수 있는 모든 스코프에서 use crate::garden::vegetables::Asparagus;로 단축경로를 만들 수 있으며, 그 이후부터는 스코프에서 이 타입을 사용하려면 Asparagus만 작성해주면 됩니다. 위의 규칙들을 보여주는 backyard라는 이름의 바이너리 크레이트를 만들어 보았습니다. 디렉터리명 또한 backyard로서, 아래의 파일들과 디렉터리들로 구성되어 있습니다. backyard\n├── Cargo.lock\n├── Cargo.toml\n└── src ├── garden │ └── vegetables.rs ├── garden.rs └── main.rs 지금의 경우 크레이트 루트 파일은 src/main.rs 이고, 내용은 아래와 같습니다: 파일명: src/main.rs use crate::garden::vegetables::Asparagus; pub mod garden; fn main() { let plant = Asparagus {}; println!(\"I'm growing {:?}!\", plant);\n} pub mod garden; 라인이 컴파일러에게 src/garden.rs 에 있는 코드를 포함할 것을 알려주고, src/garden.rs 는 아래와 같습니다: 파일명: src/garden.rs pub mod vegetables; 여기 pub mod vegetables;은 src/garden/vegetables.rs 의 코드 또한 포함되어야 함을 의미합니다. 해당 파일의 코드는 아래와 같습니다: #[derive(Debug)]\npub struct Asparagus {} 이제 위 규칙들의 세부 사항으로 넘어가서 실제로 해보면서 확인합시다!","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 모듈을 정의하여 스코프 및 공개 여부 제어하기 » 모듈 치트 시트","id":"112","title":"모듈 치트 시트"},"113":{"body":"모듈 은 크레이트의 코드를 읽기 쉽고 재사용하기도 쉽게끔 구조화를 할 수 있게 해 줍니다. 모듈 내의 코드는 기본적으로 비공개이므로, 모듈은 아이템의 공개 여부 (privacy) 를 제어하도록 해주기도 합니다. 비공개 아이템은 외부에서의 사용이 허용되지 않는 내부의 세부 구현입니다. 모듈과 모듈 내 아이템을 선택적으로 공개할 수 있는데, 이렇게 하여 외부의 코드가 모듈 및 아이템을 의존하고 사용할 수 있도록 노출해 줍니다. 예시로, 레스토랑 기능을 제공하는 라이브러리 크레이트를 작성한다고 가정해 보죠. 코드 구조에 집중할 수 있도록 레스토랑을 실제 코드로 구현하지는 않고, 본문은 비워둔 함수 시그니처만 정의하겠습니다. 레스토랑 업계에서는 레스토랑을 크게 접객 부서 (front of house) 와 지원 부서 (back of house) 로 나눕니다. 접객 부서는 호스트가 고객을 안내하고, 웨이터가 주문 접수 및 결제를 담당하고, 바텐더가 음료를 만들어 주는 곳입니다. 지원 부서는 셰프, 요리사, 주방보조가 일하는 주방과 매니저가 행정 업무를 하는 곳입니다. 중첩 (nested) 모듈 안에 함수를 집어넣어 구성하면 크레이트 구조를 실제 레스토랑이 일하는 방식과 동일하게 구성할 수 있습니다. cargo new --lib restaurant 명령어를 실행하여 restaurant이라는 새 라이브러리를 생성하고, 예제 7-1 코드를 src/lib.rs 에 작성하여 모듈, 함수 시그니처를 정의합시다. 아래는 접객 부서 쪽 코드입니다: 파일명: src/lib.rs mod front_of_house { mod hosting { fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} }\n} 예제 7-1: 함수를 포함하는 별도의 모듈을 포함한 front_of_house 모듈 mod 키워드와 모듈 이름(위의 경우 front_of_house)을 지정하여 모듈을 정의합니다. 모듈의 본문은 중괄호로 감싸져 있습니다. hosting, serving 모듈처럼, 모듈 내에는 다른 모듈을 넣을 수 있습니다. 모듈에는 구조체, 열거형, 상수, 트레이트, 함수(예제 7-1처럼) 등의 아이템 정의 또한 가질 수 있습니다. 모듈을 사용함으로써 관련된 정의들을 하나로 묶고 어떤 연관성이 있는지 이름을 지어줄 수 있습니다. 모듈화된 코드를 사용하는 프로그래머가 자신에게 필요한 어떠한 정의를 찾을 때, 모든 정의를 읽어 내릴 필요 없이 그룹 기반으로 탐색할 수 있으므로 훨씬 쉽게 찾아낼 수 있죠. 코드에 새로운 기능을 추가하려는 프로그래머도 자신이 어디에 코드를 작성해야 프로그램 구조가 그대로 유지되는지 파악할 수 있습니다. 앞서 src/main.rs 와 src/lib.rs 는 크레이트 루트라고 부른다고 언급했습니다. 이 두 파일이 그런 이름을 갖게 된 이유는 모듈 트리 (module tree) 라고 불리는 크레이트 모듈 구조에서 최상위에 crate라는 이름을 갖는 일종의 모듈로 형성되기 때문입니다. 예제 7-2는 예제 7-1의 구조를 모듈 트리로 나타낸 모습입니다. crate └── front_of_house ├── hosting │ ├── add_to_waitlist │ └── seat_at_table └── serving ├── take_order ├── serve_order └── take_payment 예제 7-2: 예제 7-1 코드를 모듈 트리로 나타낸 모습 트리는 모듈이 서로 어떻게 중첩되어 있는지 보여줍니다; 예를 들어 hosting 모듈은 front_of_house 내에 위치합니다. 이 트리는 또한 어떤 모듈이 서로 형제 (sibling) 관계에 있는지 나타내기도 하는데, 이는 동일한 모듈 내에 정의되어 있음을 말합니다; hosting과 serving은 front_of_house 모듈 내에 정의된 형제입니다. 모듈 A가 모듈 B 안에 있으면, 모듈 A는 모듈 B의 자식 이며, 모듈 B는 모듈 A의 부모 라고 말합니다. 전체 모듈 트리 최상위에 crate라는 모듈이 암묵적으로 위치한다는 점을 기억해 두세요. 모듈 트리에서 컴퓨터 파일 시스템의 디렉터리 트리를 연상하셨다면, 적절한 비유입니다! 파일 시스템의 디렉터리처럼, 여러분은 모듈로 코드를 조직화합니다. 또한 디렉터리에서 파일을 찾는 것처럼, 우리는 모듈을 찾아낼 방법이 필요하죠.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 모듈을 정의하여 스코프 및 공개 여부 제어하기 » 모듈로 관련된 코드 묶기","id":"113","title":"모듈로 관련된 코드 묶기"},"114":{"body":"러스트 모듈 트리에서 아이템을 찾는 방법은, 파일 시스템에서 경로를 사용하는 방법과 동일합니다. 함수를 호출하려면 그 함수의 경로를 알아야 합니다. 경로는 두 가지 형태가 존재합니다. 절대 경로 (absolute path) 는 크레이트 루트로부터 시작되는 전체 경로입니다; 외부 크레이트로부터의 코드에 대해서는 해당 크레이트 이름으로 절대 경로가 시작되고 현재의 크레이트로부터의 코드에 대해서는 crate 리터럴로부터 시작됩니다. 상대 경로 (relative path) 는 현재의 모듈을 시작점으로 하여 self, super 혹은 현재 모듈 내의 식별자를 사용합니다. 절대 경로, 상대 경로 뒤에는 ::으로 구분된 식별자가 하나 이상 따라옵니다. 예제 7-1로 돌아와서, add_to_waitlist 함수를 호출하고 싶다고 칩시다. 이는 다음 질문과 같습니다: add_to_waitlist 함수의 경로는 무엇일까요? 예제 7-3은 예제 7-1의 일부 모듈과 함수를 제거한 내용을 담고 있습니다. 예제는 크레이트 루트에 정의된 eat_at_restaurant라는 새로운 함수에서 add_to_waitlist 함수를 호출하는 두 가지 방법을 보여줍니다. 두 경로 모두 맞지만, 이 예제를 이대로 컴파일되지 못하게 하는 다른 문제가 남아있습니다. 무슨 이유인지는 곧 설명하겠습니다. eat_at_restaurant 함수는 우리가 만든 라이브러리 크레이트의 공개 API 중 하나입니다. 따라서 pub 키워드로 지정되어 있습니다. pub에 대해서는 ‘pub 키워드로 경로 노출하기’ 절에서 자세히 알아볼 예정입니다. 파일명: src/lib.rs mod front_of_house { mod hosting { fn add_to_waitlist() {} }\n} pub fn eat_at_restaurant() { // 절대 경로 crate::front_of_house::hosting::add_to_waitlist(); // 상대 경로 front_of_house::hosting::add_to_waitlist();\n} 예제 7-3: 절대 경로와 상대 경로로 add_to_waitlist 함수 호출하기 eat_at_restaurant 함수에서 처음 add_to_waitlist 함수를 호출할 때는 절대 경로를 사용했습니다. add_to_waitlist 함수는 eat_at_restaurant 함수와 동일한 크레이트에 정의되어 있으므로, 절대 경로의 시작점에 crate 키워드를 사용할 수 있습니다. 그 뒤로는 add_to_waitlist 함수에 도달할 때까지의 이어지는 모듈을 포함시켰습니다. 같은 구조의 파일 시스템을 생각해 볼 수 있습니다: /front_of_house/hosting/add_to_waitlist 경로를 써서 add_to_waitlist 프로그램을 실행했군요; crate를 작성해 크레이트 루트를 기준으로 사용하는 것은 셸 (shell) 에서 / 로 파일 시스템의 최상위 디렉터리를 기준으로 사용하는 것과 같습니다. eat_at_restaurant 함수에서 두 번째로 add_to_waitlist 함수를 호출할 때는 상대 경로를 사용했습니다. 경로는 모듈 트리에서 eat_at_restaurant 함수와 동일한 위치에 정의되어 있는 front_of_house 모듈로 시작합니다. 파일 시스템으로 비유하자면 front_of_house/hosting/add_to_waitlist가 되겠네요. 모듈 이름으로 시작한다는 것은 즉 상대 경로를 의미합니다. 상대 경로, 절대 경로 중 무엇을 사용할지는 프로젝트에 따라, 그리고 아이템을 정의하는 코드와 아이템을 사용하는 코드를 분리하고 싶은지, 혹은 같이 두고 싶은지에 따라 여러분이 결정해야 할 사항입니다. 예를 들어, front_of_house 모듈과 eat_at_restaurant 함수를 customer_experience라는 모듈 내부로 이동시켰다고 가정해 보죠. add_to_waitlist 함수를 절대 경로로 작성했다면 코드를 수정해야 하지만, 상대 경로는 수정할 필요가 없습니다. 반면, eat_at_restaurant 함수를 분리하여 dining이라는 모듈 내부로 이동시켰다면, add_to_waitlist 함수를 가리키는 절대 경로는 수정할 필요가 없지만, 상대 경로는 수정해야 합니다. 일반적으로 선호하는 경로는 절대 경로입니다. 아이템을 정의하는 코드와 호출하는 코드는 분리되어 있을 가능성이 높기 때문입니다. 이제 예제 7-3이 컴파일되지 않는 이유를 알아봅시다! 컴파일 시 나타나는 에러는 예제 7-4와 같습니다. $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant)\nerror[E0603]: module `hosting` is private --> src/lib.rs:9:28 |\n9 | crate::front_of_house::hosting::add_to_waitlist(); | ^^^^^^^ private module |\nnote: the module `hosting` is defined here --> src/lib.rs:2:5 |\n2 | mod hosting { | ^^^^^^^^^^^ error[E0603]: module `hosting` is private --> src/lib.rs:12:21 |\n12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^ private module |\nnote: the module `hosting` is defined here --> src/lib.rs:2:5 |\n2 | mod hosting { | ^^^^^^^^^^^ For more information about this error, try `rustc --explain E0603`.\nerror: could not compile `restaurant` due to 2 previous errors 예제 7-4: 예제 7-3 코드 컴파일 시 발생하는 에러 에러 메시지는 hosting 모듈이 비공개 (private) 라는 내용입니다. hosting 모듈과 add_to_waitlist 함수의 경로를 정확히 명시했지만, 해당 영역은 비공개 영역이기 때문에 러스트가 접근을 허용하지 않습니다. 러스트에서는 (함수, 메서드, 구조체, 열거형, 모듈, 그리고 상수 등) 모든 아이템이 기본적으로 부모 모듈에 대해 비공개입니다. 함수나 구조체 같은 아이템을 비공개로 하고 싶다면 모듈에 넣으면 됩니다. 부모 모듈 내 아이템은 자식 모듈 내 비공개 아이템을 사용할 수 없지만, 자식 모듈 내 아이템은 부모 모듈 내 아이템을 사용할 수 있습니다. 이유는, 자식 모듈의 세부 구현은 감싸져서 숨겨져 있지만, 자식 모듈 내에서는 자신이 정의된 컨텍스트를 볼 수 있기 때문입니다. 레스토랑 비유로 돌아와, 비공개 규칙을 레스토랑의 지원 부서로 생각해 보죠. 레스토랑 고객들은 내부에서 진행되는 일을 알 수 없지만, 사무실 관리자는 자신이 운영하는 레스토랑의 모든 것을 보고, 행동할 수 있습니다. 러스트 모듈 시스템은 내부의 세부 구현을 기본적으로 숨기도록 되어 있습니다. 이로써, 여러분은 외부 코드의 동작을 망가뜨릴 걱정 없이 수정할 수 있는 코드가 어느 부분인지 알 수 있죠. 그렇지만 러스트에서는 pub 키워드를 사용하여 자식 모듈의 내부 구성 요소를 공개 (public) 함으로써 외부의 상위 모듈로 노출할 방법을 제공합니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 경로를 사용하여 모듈 트리의 아이템 참조하기 » 경로를 사용하여 모듈 트리의 아이템 참조하기","id":"114","title":"경로를 사용하여 모듈 트리의 아이템 참조하기"},"115":{"body":"hosting 모듈이 비공개라고 했던 예제 7-4 에러로 돌아와 보죠. 부모 모듈 내 eat_at_restaurant 함수가 자식 모듈 내 add_to_waitlist 함수에 접근해야 하니, hosting 모듈에 pub 키워드를 추가했습니다. 작성한 모습은 예제 7-5와 같습니다. 파일명: src/lib.rs mod front_of_house { pub mod hosting { fn add_to_waitlist() {} }\n} pub fn eat_at_restaurant() { // 절대 경로 crate::front_of_house::hosting::add_to_waitlist(); // 상대 경로 front_of_house::hosting::add_to_waitlist();\n} 예제 7-5: eat_at_restaurant 함수에서 hosting 모듈을 사용할 수 있도록 pub으로 선언 안타깝게도, 예제 7-5 코드 또한 예제 7-6과 같은 에러가 발생합니다. $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant)\nerror[E0603]: function `add_to_waitlist` is private --> src/lib.rs:9:37 |\n9 | crate::front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^ private function |\nnote: the function `add_to_waitlist` is defined here --> src/lib.rs:3:9 |\n3 | fn add_to_waitlist() {} | ^^^^^^^^^^^^^^^^^^^^ error[E0603]: function `add_to_waitlist` is private --> src/lib.rs:12:30 |\n12 | front_of_house::hosting::add_to_waitlist(); | ^^^^^^^^^^^^^^^ private function |\nnote: the function `add_to_waitlist` is defined here --> src/lib.rs:3:9 |\n3 | fn add_to_waitlist() {} | ^^^^^^^^^^^^^^^^^^^^ For more information about this error, try `rustc --explain E0603`.\nerror: could not compile `restaurant` due to 2 previous errors 예제 7-6: 예제 7-5 코드 컴파일 시 발생하는 에러 어떻게 된 걸까요? mod hosting 앞에 pub 키워드를 추가하여 모듈이 공개되었습니다. 따라서, front_of_house에 접근할 수 있다면 hosting 모듈에도 접근할 수 있죠. 하지만, hosting 모듈의 내용 은 여전히 비공개입니다. 모듈을 공개했다고 해서 내용까지 공개되지는 않습니다. 모듈의 pub 키워드는 상위 모듈이 해당 모듈을 가리킬 수 있도록 할 뿐, 그 내부 코드에 접근하도록 하는 것은 아닙니다. 모듈은 단순한 컨테이너이기 때문에 모듈을 공개하는 것 만으로 할 수 있는 것은 별로 없으며, 여기에 더해서 모듈이 가지고 있는 아이템도 마찬가지로 공개해야 합니다. 예제 7-6의 에러는 add_to_waitlist 함수가 비공개라는 내용입니다. 비공개 규칙은 구조체, 열거형, 함수, 메서드, 모듈 모두에게 적용됩니다. 예제 7-7처럼 add_to_waitlist 함수도 정의에 pub 키워드를 추가하여 공개해 봅시다. 파일명: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} pub fn eat_at_restaurant() { // 절대 경로 crate::front_of_house::hosting::add_to_waitlist(); // 상대 경로 front_of_house::hosting::add_to_waitlist();\n} 예제 7-7: mod hosting, fn add_to_waitlist 에 pub 키워드를 추가해 eat_at_restaurant 함수에서 호출 가능하도록 만들기 드디어 코드를 컴파일할 수 있습니다! pub 키워드를 추가하는 것이 어째서 비공개 규칙과 관련하여 add_to_waitlist에서 이러한 경로를 사용할 수 있게 하는지 알아보기 위해서, 절대 경로와 상대 경로를 살펴봅시다. 절대 경로는 크레이트 모듈 트리의 최상위인 crate로 시작합니다. front_of_house 모듈은 크레이트 루트 내에 정의되어 있습니다. front_of_house 모듈은 공개가 아니지만, eat_at_restaurant 함수와 front_of_house 모듈은 같은 모듈 내에 정의되어 있으므로 (즉, 서로 형제 관계이므로) eat_at_restaurant 함수에서 front_of_house 모듈을 참조할 수 있습니다. 다음은 pub 키워드가 지정된 hosting 모듈입니다. hosting의 부모 모듈에 접근할 수 있으니, hosting에도 접근할 수 있습니다. 마지막 add_to_waitlist 함수 또한 pub 키워드가 지정되어 있고, 부모 모듈에 접근할 수 있으니, 호출 가능합니다! 상대 경로는 첫 번째 과정을 제외하면 절대 경로와 동일합니다. 상대 경로는 크레이트 루트에서 시작하지 않고, front_of_house로 시작합니다. front_of_house 모듈은 eat_at_restaurant 함수와 동일한 모듈 내에 정의되어 있으므로, eat_at_restaurant 함수가 정의되어 있는 모듈에서 시작하는 상대 경로를 사용할 수 있습니다. 이후 hosting, add_to_waitlist은 pub으로 지정되어 있으므로 나머지 경로도 문제없습니다. 따라서 이 함수 호출도 유효합니다! 다른 프로젝트에서 여러분의 코드를 사용할 수 있도록 라이브러리 크레이트를 공유할 계획이라면, 여러분의 공개 API는 크레이트의 사용자가 코드와 상호 작용하는 방법을 결정하는 계약입니다. 사람들이 여러분의 크레이트에 더 쉽게 의존할 수 있도록 하기 위해서는 공개 API의 변경사항을 관리할 때 고려해야 할 사항이 많습니다. 이러한 고려사항은 이 책의 범위를 벗어납니다; 이 주제에 관심이 있다면 러스트 API 가이드라인 을 참조하세요. 바이너리와 라이브러리가 함께 있는 패키지를 위한 최고의 예제 패키지에는 src/main.rs 바이너리 크레이트 루트뿐만 아니라 src/lib.rs 라이브러리 크레이트 루트도 같이 집어넣을 수 있음을 언급했었고, 두 크레이트 모두 기본적으로 같은 이름을 갖게 됩니다. 통상적으로 이렇게 라이브러리와 바이너리 크레이트 모두를 가지는 패턴의 패키지들은 라이브러리 크레이트에 있는 코드를 호출하여 실행파일을 시작하기 위한 양만큼의 코드가 바이너리 크레이트에 담긴 형태가 됩니다. 라이브러리 크레이트의 코드가 공유될 수 있으므로, 이렇게 하는 것으로 패키지가 제공하는 대부분의 기능을 다른 프로젝트에서 사용할 수 있도록 해줍니다. 모듈 트리는 src/lib.rs 내에 정의되어야 합니다. 그러면 바이너리 크레이트 내에서는 패키지 이름으로 시작하는 경로를 사용함으로써 모든 공개 아이템을 사용할 수 있습니다. 바이너리 크레이트는 완전히 외부에 있는 다른 크레이트가 이 라이브러리 크레이트를 사용하는 식과 동일하게 이 라이브러리 크레이트의 사용자가 됩니다: 즉 공개 API만 사용할 수 있습니다. 이는 여러분이 좋은 API를 설계하는 데 도움을 줍니다; 여러분이 저자일 뿐만 아니라, 고객도 겸하게 되니까요! 12장 에서는 바이너리 크레이트와 라이브러리 크레이트를 모두 가지고 있는 커맨드 라인 프로그램을 작성해 보면서 이와 같은 구조에 대한 예제를 보여 드리겠습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 경로를 사용하여 모듈 트리의 아이템 참조하기 » pub 키워드로 경로 노출하기","id":"115","title":"pub 키워드로 경로 노출하기"},"116":{"body":"super로 시작하면 현재 모듈 혹은 크레이트 루트 대신 자기 부모 모듈부터 시작되는 상대 경로를 만들 수 있습니다. 이는 파일시스템 경로에서 ..로 시작하는 것과 동일합니다. super를 사용하면 부모 모듈에 위치하고 있음을 알고 있는 아이템을 참조하도록 해주고, 이는 모듈이 부모 모듈과 밀접한 관련이 있지만 부모 모듈은 나중에 모듈 트리의 다른 어딘가로 옮겨질지도 모르는 경우 모듈 트리의 재조정을 편하게 만들어 줍니다. 예제 7-8은 셰프가 잘못된 주문을 수정하여 고객에게 직접 전달하는 상황을 묘사한 코드입니다. back_of_house 모듈에 정의된 fix_incorrect_order 함수는 super로 시작하는 deliver_order로의 경로를 특정하는 것으로 부모 모듈에 정의된 deliver_order 함수를 호출합니다: 파일명: src/lib.rs fn deliver_order() {} mod back_of_house { fn fix_incorrect_order() { cook_order(); super::deliver_order(); } fn cook_order() {}\n} 예제 7-8: super로 시작하는 상대 경로를 사용해 함수 호출하기 fix_incorrect_order 함수는 back_of_house 모듈 내에 위치하므로, super는 back_of_house의 부모 모듈, 즉 루트를 의미합니다. 그리고 해당 위치에 deliver_order가 존재하니 호출은 성공합니다. back_of_house 모듈과 deliver_order 함수는 크레이트 모듈 구조 변경 시 서로의 관계를 유지한 채 함께 이동될 가능성이 높습니다. 그러므로 super를 사용하면, 차후에 다른 모듈에 이동시키더라도 수정해야 할 코드를 줄일 수 있습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 경로를 사용하여 모듈 트리의 아이템 참조하기 » super로 시작하는 상대 경로","id":"116","title":"super로 시작하는 상대 경로"},"117":{"body":"pub 키워드로 구조체와 열거형을 공개할 수도 있지만, 이를 활용하기 전에 알아두어야 할 추가사항이 몇 가지 있습니다. 구조체 정의에 pub를 쓰면 구조체는 공개되지만, 구조체의 필드는 비공개로 유지됩니다. 공개 여부는 각 필드마다 정할 수 있습니다. 예제 7-9는 공개 구조체 back_of_house::Breakfast를 정의하고 toast 필드는 공개하지만 seasonal_fruit 필드는 비공개로 둔 예제입니다. 이는 레스토랑에서 고객이 식사와 같이 나올 빵 종류를 선택하고, 셰프가 계절과 재고 상황에 맞춰서 식사에 포함할 과일을 정하는 상황을 묘사한 예제입니다. 과일은 빈번히 변경되므로, 고객은 직접 과일을 선택할 수 없으며 어떤 과일을 받을지도 미리 알 수 없습니다. 파일명: src/lib.rs mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from(\"peaches\"), } } }\n} pub fn eat_at_restaurant() { // 호밀 (Rye) 토스트를 곁들인 여름철 조식 주문하기 let mut meal = back_of_house::Breakfast::summer(\"Rye\"); // 먹고 싶은 빵 바꾸기 meal.toast = String::from(\"Wheat\"); println!(\"I'd like {} toast please\", meal.toast); // 다음 라인의 주석을 해제하면 컴파일되지 않습니다; 식사와 함께 // 제공되는 계절 과일은 조회나 수정이 허용되지 않습니다 // meal.seasonal_fruit = String::from(\"blueberries\");\n} 예제 7-9: 일부 필드는 공개하고, 일부 필드는 비공개인 구조체 back_of_house::Breakfast 구조체의 toast 필드는 공개 필드이기 때문에 eat_at_restaurant 함수에서 점 표기법으로 toast 필드를 읽고 쓸 수 있습니다. 반면, seasonal_fruit 필드는 비공개 필드이기 때문에 eat_at_restaurant 함수에서 사용할 수 없습니다. seasonal_fruit 필드를 수정하는 코드의 주석을 한번 해제하여 어떤 에러가 발생하는지 확인해 보세요! 또한, back_of_house::Breakfast 구조체는 비공개 필드를 갖고 있기 때문에, Breakfast 인스턴스를 생성할 공개 연관 함수(예제에서는 summer 함수입니다)를 반드시 제공해야 합니다. 만약 Breakfast 구조체에 그런 함수가 존재하지 않을 경우, eat_at_restaurant 함수에서 Breakfast 인스턴스를 생성할 수 없습니다. eat_at_restaurant 함수에서는 비공개 필드인 seasonal_fruit 필드의 값을 지정할 방법이 없기 때문입니다. 반대로, 열거형은 공개로 지정할 경우 모든 배리언트가 공개됩니다. 열거형을 공개하는 방법은 enum 키워드 앞에 pub 키워드만 작성하면 됩니다. 작성한 모습은 예제 7-10과 같습니다. 파일명: src/lib.rs mod back_of_house { pub enum Appetizer { Soup, Salad, }\n} pub fn eat_at_restaurant() { let order1 = back_of_house::Appetizer::Soup; let order2 = back_of_house::Appetizer::Salad;\n} 예제 7-10: 열거형과 열거형의 모든 배리언트를 공개로 지정하기 Appetizer 열거형을 공개하였으니, eat_at_restaurant 함수에서 Soup, Salad 배리언트를 사용할 수 있습니다. 열거형은 그 배리언트가 공개되지 않는다면 큰 쓸모가 없습니다; 열거형의 모든 배리언트에 대해 전부 pub을 붙이는 것은 귀찮은 일이 될 것이므로, 열거형의 배리언트는 기본적으로 공개입니다. 구조체의 경우 필드를 공개로 하지 않는 것이 종종 유용하므로, 구조체 필드는 pub을 명시하지 않는 한 기본적으로 모든 것이 비공개라는 일반적인 규칙을 따릅니다. 남은 pub 키워드 관련 내용은 모듈 시스템의 마지막 기능인 use 키워드입니다. 먼저 use 키워드 단독 사용법을 다루고, 그다음 use와 pub을 연계하여 사용하는 방법을 다루겠습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 경로를 사용하여 모듈 트리의 아이템 참조하기 » 구조체, 열거형을 공개하기","id":"117","title":"구조체, 열거형을 공개하기"},"118":{"body":"함수 호출을 위해서 경로를 작성하는 것은 불편하고 반복적인 느낌을 줄 수 있습니다. 예제 7-7에서는 절대 경로를 사용하건 상대 경로를 사용하건, add_to_waitlist 호출할 때마다 front_of_house, hosting 모듈을 매번 지정해줘야 했죠. 다행히도 이 과정을 단축할 방법이 있습니다: use 키워드를 한번 사용하여 어떤 경로의 단축경로 (shortcut) 를 만들 수 있고, 그러면 스코프 안쪽 어디서라도 짧은 이름을 사용할 수 있습니다. 예제 7-11은 crate::front_of_house::hosting 모듈을 eat_at_restaurant 함수가 존재하는 스코프로 가져와, eat_at_restaurant 함수 내에서 add_to_waitlist 함수를 hosting::add_to_waitlist 경로만으로 호출하는 예제입니다. 파일명: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist();\n} 예제 7-11: use 키워드로 모듈을 스코프 안으로 가져오기 스코프에 use 키워드와 경로를 작성하는 건 파일 시스템에서 심볼릭 링크 (symbolic link) 를 생성하는 것과 유사합니다. 크레이트 루트에 use crate::front_of_house::hosting를 작성하면 해당 스코프에서 hosting 모듈을 크레이트 루트에 정의한 것처럼 사용할 수 있습니다. use 키워드로 가져온 경우도 다른 경로와 마찬가지로 비공개 규칙이 적용됩니다. use가 사용된 특정한 스코프에서만 단축경로가 만들어진다는 점을 주의하세요. 예제 7-12에서는 eat_at_restaurant 함수를 새로운 자식 모듈 customer로 옮겼는데, 이러면 use 구문과 다른 스코프가 되므로, 이 함수는 컴파일 되지 않습니다: 파일명: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} use crate::front_of_house::hosting; mod customer { pub fn eat_at_restaurant() { hosting::add_to_waitlist(); }\n} 예제 7-12: use 구문은 사용된 스코프 내에서만 적용됩니다 컴파일러는 customer 모듈 내에 더 이상 단축경로가 적용되지 않음을 알려줍니다: $ cargo build Compiling restaurant v0.1.0 (file:///projects/restaurant)\nwarning: unused import: `crate::front_of_house::hosting` --> src/lib.rs:7:5 |\n7 | use crate::front_of_house::hosting; | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: `#[warn(unused_imports)]` on by default error[E0433]: failed to resolve: use of undeclared crate or module `hosting` --> src/lib.rs:11:9 |\n11 | hosting::add_to_waitlist(); | ^^^^^^^ use of undeclared crate or module `hosting` For more information about this error, try `rustc --explain E0433`.\nwarning: `restaurant` (lib) generated 1 warning\nerror: could not compile `restaurant` due to previous error; 1 warning emitted use가 해당 스코프 안에서 더 이상 사용되지 않는다는 경고도 있음을 주목하세요! 이 문제를 해결하려면 use도 customer 모듈 안쪽으로 옮기거나, customer 모듈 내에서 super::hosting를 써서 부모 모듈로의 단축경로를 참조하면 됩니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » use 키워드로 경로를 스코프 안으로 가져오기 » use 키워드로 경로를 스코프 안으로 가져오기","id":"118","title":"use 키워드로 경로를 스코프 안으로 가져오기"},"119":{"body":"예제 7-11에서 add_to_waitlist 함수까지 경로를 전부 작성하지 않고, use crate::front_of_house::hosting 까지만 작성한 뒤 hosting::add_to_waitlist 코드로 함수를 호출하는 점이 의아하실 수도 있습니다. 예제 7-13처럼 작성하면 안 되는 걸까요? 파일명: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} use crate::front_of_house::hosting::add_to_waitlist; pub fn eat_at_restaurant() { add_to_waitlist();\n} 예제 7-13: use 키워드로 add_to_waitlist 함수를 직접 가져오기 (보편적이지 않은 작성 방식) 예제 7-11과 7-13의 동작은 동일하지만, 예제 7-11 코드가 use 키워드로 스코프에 함수를 가져올 때의 관용적인 코드입니다. 함수의 부모 모듈을 use 키워드로 가져오면 함수를 호출할 때 부모 모듈을 특정해야 합니다. 함수 호출 시 부모 모듈을 특정하면 전체 경로를 반복하는 것을 최소화하면서도 함수가 로컬에 정의되어 있지 않음을 명백히 보여주게 됩니다. 예제 7-13의 코드는 add_to_waitlist가 어디에 정의되어 있는지 불분명합니다. 한편, use 키워드로 구조체나 열거형 등의 타 아이템을 가져올 시에는 전체 경로를 작성하는 것이 보편적입니다. 예제 7-14는 HashMap 표준 라이브러리 구조체를 바이너리 크레이트의 스코프로 가져오는 관용적인 코드 예시입니다. 파일명: src/main.rs use std::collections::HashMap; fn main() { let mut map = HashMap::new(); map.insert(1, 2);\n} 예제 7-14: 보편적인 방식으로 HashMap을 스코프로 가져오기 이러한 관용이 탄생하게 된 명확한 이유는 없습니다. 어쩌다 보니 관습이 생겼고, 사람들이 이 방식대로 러스트 코드를 읽고 쓰는 데에 익숙해졌을 뿐입니다. 하지만, 동일한 이름의 아이템을 여럿 가져오는 경우는 이 방식을 사용하지 않습니다. 러스트가 허용하지 않기 때문이죠. 예제 7-15는 각각 다른 모듈 내에 위치하지만 이름이 같은 두 개의 Result 타입을 스코프로 가져와 사용하는 예시입니다. 파일명: src/lib.rs use std::fmt;\nuse std::io; fn function1() -> fmt::Result { // --생략--\n# Ok(())\n} fn function2() -> io::Result<()> { // --생략--\n# Ok(())\n} 예제 7-15: 이름이 같은 두 개의 타입을 동일한 스코프에 가져오려면 부모 모듈을 반드시 명시해야 합니다. 보시다시피 부모 모듈을 명시하여 두 개의 Result 타입을 구별하고 있습니다. 만약 use std::fmt::Result, use std::io::Result로 작성한다면, 동일한 스코프 내에 두 개의 Result 타입이 존재하므로 러스트는 우리가 어떤 Result 타입을 사용했는지 알 수 없습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » use 키워드로 경로를 스코프 안으로 가져오기 » 보편적인 use 경로 작성법","id":"119","title":"보편적인 use 경로 작성법"},"12":{"body":"앞으로 배울 건 많지만 천 리 길도 한 걸음부터라는 말이 있듯 하나씩 배워보도록 합시다. 이번 장에서 배우는 내용은 다음과 같습니다: 각 운영체제 (Linux, macOS, Windows) 별 러스트 설치법 Hello, world! 프로그램 작성하기 러스트 패키지 매니저 및 빌드 도구인 cargo 사용법","breadcrumbs":"시작해봅시다 » 시작해봅시다","id":"12","title":"시작해봅시다"},"120":{"body":"use 키워드로 동일한 이름의 타입을 스코프로 여러 개 가져올 경우의 또 다른 해결 방법이 있습니다. 경로 뒤에 as 키워드를 작성하고, 새로운 이름이나 타입 별칭을 작성하면 됩니다. 예제 7-16은 as 키워드를 이용해 예제 7-15 코드의 Result 타입 이름 중 하나를 변경한 예제입니다. 파일명: src/lib.rs use std::fmt::Result;\nuse std::io::Result as IoResult; fn function1() -> Result { // --생략--\n# Ok(())\n} fn function2() -> IoResult<()> { // --생략--\n# Ok(())\n} 예제 7-16: 스코프 안으로 가져온 타입의 이름을 as 키워드로 변경하기 두 번째 use 구문에서는, 앞서 스코프 안으로 가져온 std::fmt의 Result와 충돌을 방지하기 위해 std::io::Result 타입의 이름을 IoResult로 새롭게 지정합니다. 예제 7-15, 예제 7-16은 둘 다 관용적인 방식이므로, 원하는 방식을 선택하시면 됩니다!","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » use 키워드로 경로를 스코프 안으로 가져오기 » as 키워드로 새로운 이름 제공하기","id":"120","title":"as 키워드로 새로운 이름 제공하기"},"121":{"body":"use 키워드로 이름을 가져올 경우, 해당 이름은 새 위치의 스코프에서 비공개가 됩니다. pub과 use를 결합하면 우리 코드를 호출하는 코드가 해당 스코프에 정의된 것처럼 해당 이름을 참조할 수 있습니다. 이 기법은 아이템을 스코프로 가져오는 동시에 다른 곳에서 아이템을 가져갈 수 있도록 만들기 때문에, 다시 내보내기 (re-exporting) 라고 합니다. 예제 7-17은 예제 7-11 코드의 use를 pub use로 변경한 예제입니다. 파일명: src/lib.rs mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} }\n} pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist();\n} 예제 7-17: 다른 스코프의 코드에서 사용할 수 있도록 pub use 사용 위와 같이 변경하기 전이라면 외부 코드에서는 add_to_waitlist 함수를 호출하기 위해 restaurant::front_of_house::hosting::add_to_waitlist()라는 경로를 사용해야 할 것입니다. 위의 pub use가 루트 모듈로부터 hosting 모듈을 다시 내보냈으므로, 이제 외부 코드는 restaurant::hosting::add_to_waitlist() 경로를 대신 사용할 수 있습니다. 다시 내보내기 기법은 작성한 코드의 구조 내부와, 그 코드를 사용할 프로그래머들이 예상할법한 해당 분야의 구조가 서로 다를 때 유용합니다. 레스토랑 비유 예제를 예로 들어보죠. 레스토랑을 운영하는 직원들의 머릿속에서는 ‘접객 부서’와 ‘지원 부서’가 나뉘어 있습니다. 하지만 레스토랑을 방문하는 고객들은 레스토랑의 부서를 그런 용어로 나누어 생각하지 않겠죠. pub use를 사용하면 코드를 작성할 때의 구조와, 노출할 때의 구조를 다르게 만들 수 있습니다. 라이브러리를 제작하는 프로그래머와, 라이브러리를 사용하는 프로그래머 모두를 위한 라이브러리를 구성하는 데 큰 도움이 되죠. pub use에 대한 또 다른 예제, 그리고 이것이 여러분의 크레이트 문서에 어떤 영향을 주는지에 대해서는 14장의 ‘pub use를 사용하여 편리한 공개 API 내보내기’ 절에서 살펴보겠습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » use 키워드로 경로를 스코프 안으로 가져오기 » pub use로 다시 내보내기","id":"121","title":"pub use로 다시 내보내기"},"122":{"body":"2장에서는 난수 생성을 위해 rand라는 외부 패키지를 사용하는 추리 게임 프로젝트를 만들었습니다. rand 패키지를 프로젝트에서 사용하기 위해서 Cargo.toml 에 다음 줄을 추가했었죠: 파일명: Cargo.toml rand = \"0.8.5\" Cargo.toml 에 rand를 의존성으로 추가하면 카고가 crates.io 에서 rand 패키지를 비롯한 모든 의존성을 다운로드하고 프로젝트에서 rand 패키지를 사용할 수 있게 됩니다. 그 후, 구현하고 있는 패키지의 스코프로 rand 정의를 가져오기 위해 use 키워드와 크레이트 이름인 rand를 쓰고 가져올 아이템을 나열했습니다. 2장 ‘임의의 숫자 생성하기’ 절을 다시 떠올려 보죠. Rng 트레이트를 스코프로 가져오고 rand::thread_rng 함수를 호출했었습니다. # use std::io;\nuse rand::Rng; fn main() {\n# println!(\"Guess the number!\");\n# let secret_number = rand::thread_rng().gen_range(1..=100);\n# # println!(\"The secret number is: {secret_number}\");\n# # println!(\"Please input your guess.\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"Failed to read line\");\n# # println!(\"You guessed: {guess}\");\n} 러스트 커뮤니티 구성원들이 crates.io 에서 이용 가능한 다양한 패키지를 만들어왔고, 이들 모두 같은 단계를 거쳐서 여러분 패키지에 가져올 수 있습니다: 패키지의 Cargo.toml 파일에 추가하고, use 키워드를 사용해 스코프로 가져오면 됩니다. 알아 두어야 할 것이 있다면 std 표준 라이브러리도 마찬가지로 외부 크레이트라는 겁니다. 러스트 언어에 포함되어 있기 때문에 Cargo.toml 에 추가할 필요는 없지만, 표준 라이브러리에서 우리가 만든 패키지의 스코프로 가져오려면 use 문을 작성해야 합니다. 예를 들어, HashMap을 가져오는 코드는 다음과 같습니다. use std::collections::HashMap; 위는 표준 라이브러리 크레이트의 이름인 std 로 시작하는 절대 경로입니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » use 키워드로 경로를 스코프 안으로 가져오기 » 외부 패키지 사용하기","id":"122","title":"외부 패키지 사용하기"},"123":{"body":"동일한 크레이트나 동일한 모듈 내에 정의된 아이템을 여럿 사용할 경우, 각 아이템 당 한 줄씩 코드를 나열하면 수직 방향으로 너무 많은 영역을 차지합니다. 예시를 살펴봅시다. 추리 게임의 예제 2-4에서 작성했던 다음 두 use 문은 std 내 아이템을 스코프로 가져옵니다. 파일명: src/main.rs # use rand::Rng;\n// --생략--\nuse std::cmp::Ordering;\nuse std::io;\n// --생략--\n# # fn main() {\n# println!(\"Guess the number!\");\n# # let secret_number = rand::thread_rng().gen_range(1..=100);\n# # println!(\"The secret number is: {secret_number}\");\n# # println!(\"Please input your guess.\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"Failed to read line\");\n# # println!(\"You guessed: {guess}\");\n# # match guess.cmp(&secret_number) {\n# Ordering::Less => println!(\"Too small!\"),\n# Ordering::Greater => println!(\"Too big!\"),\n# Ordering::Equal => println!(\"You win!\"),\n# }\n# } 그대신 중첩 경로를 사용하여 동일한 아이템을 한 줄로 가져올 수 있습니다. 경로의 공통된 부분을 작성하고 콜론 두 개를 붙인 다음, 중괄호 내에 경로가 다른 부분을 나열합니다. 예시는 예제 7-18과 같습니다. 파일명: src/main.rs # use rand::Rng;\n// --생략--\nuse std::{cmp::Ordering, io};\n// --생략--\n# # fn main() {\n# println!(\"Guess the number!\");\n# # let secret_number = rand::thread_rng().gen_range(1..=100);\n# # println!(\"The secret number is: {secret_number}\");\n# # println!(\"Please input your guess.\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"Failed to read line\");\n# # let guess: u32 = guess.trim().parse().expect(\"Please type a number!\");\n# # println!(\"You guessed: {guess}\");\n# # match guess.cmp(&secret_number) {\n# Ordering::Less => println!(\"Too small!\"),\n# Ordering::Greater => println!(\"Too big!\"),\n# Ordering::Equal => println!(\"You win!\"),\n# }\n# } 예제 7-18: 중첩 경로를 사용해, 경로의 앞부분이 같은 여러 아이템을 스코프로 가져오기 규모가 큰 프로그램이라면, 동일한 크레이트나 모듈에서 여러 아이템을 가져올 때 중첩 경로를 사용함으로써 많은 use 구문을 줄일 수 있습니다! 중첩 경로는 경로의 아무 단계에서 사용할 수 있으며, 하위 경로가 동일한 use 구문이 많을 때 특히 빛을 발합니다. 다음 예제 7-19는 두 use 구문의 예시입니다. 하나는 std::io를 스코프로 가져오고, 다른 하나는 std::io::Write를 스코프로 가져옵니다. 파일명: src/lib.rs use std::io;\nuse std::io::Write; 예제 7-19: 하위 경로가 같은 두 use 구문 두 경로에서 중복되는 부분은 std::io입니다. 또한 std::io는 첫 번째 경로 그 자체이기도 합니다. 중첩 경로에 self를 작성하면 두 경로를 하나의 use 구문으로 합칠 수 있습니다. 파일명: src/lib.rs use std::io::{self, Write}; 예제 7-20: 예제 7-19의 두 경로를 use 구문 하나로 합치기 이 한 줄로 std::io, std::io::Write 둘 다 스코프로 가져올 수 있습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » use 키워드로 경로를 스코프 안으로 가져오기 » 중첩 경로를 사용하여 대량의 use 나열을 정리하기","id":"123","title":"중첩 경로를 사용하여 대량의 use 나열을 정리하기"},"124":{"body":"경로에 글롭 (glob) 연산자 *를 붙이면 경로 안에 정의된 모든 공개 아이템을 가져올 수 있습니다. use std::collections::*; 이 use 구문은 std::collections 내에 정의된 모든 공개 아이템을 현재 스코프로 가져옵니다. 하지만 글롭 연산자는 코드에 사용된 어떤 이름이 어느 곳에 정의되어 있는지 파악하기 어렵게 만들 수 있으므로, 사용에 주의해야 합니다. 글롭 연산자는 테스트할 모든 아이템을 tests 모듈로 가져오는 용도로 자주 사용됩니다. (11장 ‘테스트 작성 방법’ 에서 다룰 예정입니다.) 또한 프렐루드 패턴의 일부로 사용되기도 하며, 자세한 내용은 표준 라이브러리 문서 를 참고 바랍니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » use 키워드로 경로를 스코프 안으로 가져오기 » 글롭 연산자","id":"124","title":"글롭 연산자"},"125":{"body":"이번 장에서 여태 나온 모든 예제는 하나의 파일에 여러 모듈을 정의했습니다. 큰 모듈이라면, 정의를 여러 파일로 나누어 코드를 쉽게 찾아갈 수 있도록 만들어야겠죠. 예를 들어 여러 개의 레스토랑 관련 모듈을 가지고 있는 예제 7-17 코드로 시작해 봅시다. 크레이트 루트 파일에 모든 모듈이 정의되는 형태 대신 이 모듈들을 파일로 추출해 보겠습니다. 이 경우 크레이트 루트 파일은 src/lib.rs 지만, 이러한 절차는 크레이트 루트 파일이 src/main.rs 인 바이너리 크레이트에서도 작동합니다. 먼저 front_of_house 모듈을 파일로 추출하겠습니다. front_of_house 모듈에 대한 중괄호 내부의 코드를 지우고 mod front_of_house; 선언 부분만 남겨서, src/lib.rs 가 예제 7-21의 코드만 있도록 해봅시다. 예제 7-22의 src/front_of_house.rs 파일을 만들기 전까지는 컴파일되지 않음을 유의하세요. 파일명: src/lib.rs mod front_of_house; pub use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist();\n} 예제 7-21: 본문이 src/front_of_house.rs 에 위치할 front_of_house 모듈 선언하기 다음으로 예제 7-22처럼 src/front_of_house.rs 이라는 새 파일을 만들어 중괄호 안에 있던 코드를 위치시킵니다. 크레이트 루트에 front_of_house라는 이름의 모듈 선언이 나왔으므로 컴파일러는 이 파일을 살펴봐야 한다는 것을 알게 됩니다. 파일명: src/front_of_house.rs pub mod hosting { pub fn add_to_waitlist() {}\n} 예제 7-22: src/front_of_house.rs 파일에 front_of_house 모듈 본문 정의하기 모듈 트리에서 mod 선언을 사일을 로드하는 것은 한 번만 하면 됩니다. 일단 그 파일이 프로젝트의 일부란 것을 컴파일러가 파악하면 (그래서 모듈 트리 내 mod 구문을 집어넣은 곳 옆에 코드가 있음을 알게 되면), ‘경로를 사용하여 모듈 트리의 아이템 참조하기’ 절에서 다루었던 것처럼 프로젝트의 다른 파일들은 선언된 위치의 경로를 사용하여 로드된 파일의 코드를 참조해야 합니다. 즉, mod는 다른 프로그래밍 언어에서 볼 수 있는 ‘포함하기 (include)’ 연산이 아닙니다 . 다음으로 hosting 모듈을 파일로 추출하겠습니다. hosting이 루트 모듈이 아니라 front_of_house의 자식 모듈이기 때문에 과정이 약간 다릅니다. hosting의 파일을 모듈 트리 내 부모의 이름이 될 새 디렉터리, 즉 이 경우에는 *src/front_of_house/*에 위치시키겠습니다. hosting을 옮기는 작업을 시작하기 위하여, src/front_of_house.rs 에는 hosting 모듈의 선언만 있도록 수정합니다: 파일명: src/front_of_house.rs pub mod hosting; 그다음 src/front_of_house 디렉터리를 만들고 그 안에 hosting.rs 파일을 생성한 다음 hosting 모듈 내용을 작성합니다: 파일명: src/front_of_house/hosting.rs pub fn add_to_waitlist() {} hosting.rs 를 src/front_of_house 대신 src 디렉터리에 넣으면 컴파일러는 hosting.rs 코드가 front_of_house 모듈의 하위에 선언되지 않고 크레이트 루트에 선언된 hosting 모듈에 있을 것으로 예상합니다. 어떤 파일에서 어떤 모듈의 코드를 확인할지에 대한 컴파일러의 규칙은 디렉터리와 파일이 모듈 트리와 더 밀접하게 일치한다는 것을 의미합니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 별개의 파일로 모듈 분리하기 » 별개의 파일로 모듈 분리하기","id":"125","title":"별개의 파일로 모듈 분리하기"},"126":{"body":"지금까지는 러스트 컴파일러가 사용하는 가장 관용적인 파일 경로를 다루었지만, 러스트는 예전 스타일의 파일 경로 또한 지원합니다. 크레이트 루트 내에 선언된 front_of_house 모듈에 대하여, 컴파일러는 다음의 위치에서 모듈의 코드를 찾아볼 것입니다: src/front_of_house.rs (우리가 지금 다룬 형태) src/front_of_house/mod.rs (예전 스타일, 여전히 지원되는 경로) front_of_house의 서브모듈인 hosting이라는 모듈에 대해서는 다음의 위치에서 모듈의 코드를 찾아볼 것입니다: src/front_of_house/hosting.rs (우리가 지금 다룬 형태) src/front_of_house/hosting/mod.rs (예전 스타일, 여전히 지원되는 경로) 만약 같은 모듈에 대해 두 스타일 모두를 사용하면 컴파일 에러가 납니다. 같은 프로젝트에서 서로 다른 모듈에 대해 양쪽 스타일을 섞어 사용하는 것은 허용되지만, 프로젝트를 살펴보는 사람들에게 혼란을 줄 가능성이 있습니다. mod.rs 라는 이름의 파일을 사용하는 스타일의 주요 단점은 프로젝트에 여러 파일의 이름이 mod.rs 로 끝나게 되어, 에디터에서 이 파일들을 동시에 열어두었을 때 헷갈릴 수 있다는 점입니다. 각 모듈의 코드를 별도의 파일로 옮겼고, 모듈 트리는 동일한 상태로 남아있습니다. eat_at_restaurant 내의 함수 호출은 그 정의가 다른 파일들에 있다 하더라도 아무 수정 없이 동작할 것입니다. 이러한 기술은 모듈의 크기가 증가했을 때 이를 새로운 파일로 옮기도록 해줍니다. src/lib.rs 파일의 pub use crate::front_of_house::hosting 구문을 변경하지 않았으며, use 구문이 크레이트의 일부로 컴파일되는 파일에 영향을 주지 않는다는 점도 주목해 주세요. mod 키워드는 모듈을 선언하고, 러스트는 모듈과 같은 이름의 파일에서 해당 모듈에 들어가는 코드를 찾습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 별개의 파일로 모듈 분리하기 » 대체 파일 경로","id":"126","title":"대체 파일 경로"},"127":{"body":"러스트에서는 패키지를 여러 크레이트로 나누고, 크레이트를 여러 모듈로 나누어 한 모듈에 정의된 아이템을 다른 모듈에서 참조할 수 있게 해줍니다. 절대 경로나 상대 경로를 지정하면 이를 수행할 수 있습니다. 이러한 경로는 use 구문을 사용해 스코프 안으로 가져올 수 있으므로 해당 스코프에 있는 아이템을 여러 번 사용해야 할 때 더 짧은 경로를 사용할 수 있습니다. 모듈 코드는 기본적으로 비공개지만, pub 키워드를 추가해 정의를 공개할 수 있습니다. 다음 장에서는 이렇게 깔끔하게 구성된 여러분의 코드에서 사용할 수 있는 표준 라이브러리의 컬렉션 자료구조를 몇 가지 살펴보겠습니다.","breadcrumbs":"커져 가는 프로젝트를 패키지, 크레이트, 모듈로 관리하기 » 별개의 파일로 모듈 분리하기 » 정리","id":"127","title":"정리"},"128":{"body":"러스트의 표준 라이브러리에는 컬렉션 (collection) 이라 불리는 매우 유용한 데이터 구조들이 여러 개 포함되어 있습니다. 대부분의 다른 데이터 타입은 단일한 특정 값을 나타내지만, 컬렉션은 다수의 값을 담을 수 있습니다. 내장된 배열 (build-in array) 이나 튜플 타입과는 달리, 이 컬렉션들이 가리키고 있는 데이터들은 힙에 저장되는데, 이는 즉 데이터의 양이 컴파일 타임에 결정되지 않아도 되며 프로그램 실행 중에 늘어나거나 줄어들 수 있다는 의미입니다. 각 컬렉션 종류는 서로 다른 크기와 비용을 가지고 있으며, 현재의 상황에 따라 적절한 컬렉션을 선택하는 것은 장시간에 걸쳐 발전시켜야 하는 기술입니다. 이번 장에서는 러스트 프로그램에서 굉장히 자주 사용되는 세 가지 컬렉션에 대해 다뤄보겠습니다: 벡터 (vector) 는 여러 개의 값을 서로 붙어 있게 저장할 수 있도록 해줍니다. 문자열 (string) 은 문자 (character) 의 모음입니다. String 타입은 전에도 다루었지만, 이번 장에서 더 깊이 있게 이야기해 보겠습니다. 해시맵 (hash map) 은 어떤 값을 특정한 키와 연관지어 주도록 해줍니다. 이는 맵 (map) 이라 일컫는 좀 더 일반적인 데이터 구조의 특정한 구현 형태입니다. 표준 라이브러리가 제공하는 다른 컬렉션에 대해 알고 싶으시면, 문서 를 봐주세요. 이제부터 벡터, 문자열, 해시맵을 만들고 업데이트하는 방법 뿐만 아니라 무엇이 각 컬렉션을 특별하게 해주는 지에 대해 논의해 보겠습니다.","breadcrumbs":"일반적인 컬렉션 » 일반적인 컬렉션","id":"128","title":"일반적인 컬렉션"},"129":{"body":"첫 번째로 살펴볼 컬렉션 타입은 벡터 라고도 하는 Vec입니다. 벡터를 사용하면 메모리에서 모든 값을 서로 이웃하도록 배치하는 단일 데이터 구조에 하나 이상의 값을 저장할 수 있습니다. 벡터는 같은 타입의 값만을 저장할 수 있습니다. 벡터는 파일 내의 텍스트 라인들이나 장바구니의 품목 가격 같은 아이템 목록을 저장하는 상황일 때 유용합니다.","breadcrumbs":"일반적인 컬렉션 » 벡터에 여러 값의 목록 저장하기 » 벡터에 여러 값의 목록 저장하기","id":"129","title":"벡터에 여러 값의 목록 저장하기"},"13":{"body":"우선 러스트를 설치해야겠죠. 설치는 rustup이라는 러스트 버전 및 러스트 관련 도구를 관리하는 커맨드 라인 도구를 이용할 겁니다. 인터넷이 연결되어 있어야 하니 미리 인터넷 연결을 확인해 주세요. Note: rustup 이외에 다른 방법으로 설치하길 원하신다면 기타 러스트 설치 방법 페이지 를 참고하시기 바랍니다. 다음은 러스트 컴파일러 최신 stable 버전을 설치하는 내용입니다. 혹여나 이 책을 읽는 시점에, 이 책에서 사용한 버전이 낮아서 걱정되시는 분들을 위해 말씀드리자면, 러스트에는 안정성 보증 (stability guarantees) 이 적용되어 있습니다. 간혹 에러나 경고 메시지가 변경되는 일이 있기에 출력은 버전마다 조금씩 다를 수 있으나, 이 책에 등장하는 모든 예제는 향후 버전에서도 책 내용에서 설명하는 대로 동작할 겁니다.","breadcrumbs":"시작해봅시다 » 러스트 설치 » 러스트 설치","id":"13","title":"러스트 설치"},"130":{"body":"비어있는 새 벡터를 만들려면 다음 예제 8-1과 같이 Vec::new 함수를 호출합니다: # fn main() { let v: Vec = Vec::new();\n# } 예제 8-1: i32 타입의 값을 가질 수 있는 비어있는 새 벡터 생성 위에서 타입 명시 (type annotation) 가 추가된 것에 주목하세요. 이 벡터에 어떠한 값도 집어넣지 않았기 때문에, 러스트는 저장하고자 하는 요소가 어떤 타입인지 알지 못합니다. 이는 중요한 지점입니다. 벡터는 제네릭 (generic) 을 이용하여 구현됐습니다; 제네릭을 이용하여 여러분만의 타입을 만드는 방법은 10장에서 다룰 것입니다. 지금 당장은 표준 라이브러리가 제공하는 Vec 타입은 어떠한 타입의 값이라도 저장할 수 있다는 것만 기억해 둡시다. 특정한 타입의 값을 저장할 벡터를 만들 때는 꺾쇠괄호(<>) 안에 해당 타입을 지정합니다. 예제 8-1에서는 러스트에게 v의 Vec이 i32 타입의 요소를 갖는다고 알려주었습니다. 대부분의 경우는 초깃값들과 함께 Vec를 생성하고 러스트는 저장하고자 하는 값의 타입을 대부분 유추할 수 있으므로, 이런 타입 명시를 할 필요가 거의 없습니다. 러스트는 편의를 위해 vec! 매크로를 제공하는데, 이 매크로는 제공된 값들을 저장한 새로운 Vec을 생성합니다. 예제 8-2는 1, 2, 3을 저장한 새로운 Vec을 생성할 것입니다. 3장의 ‘데이터 타입’ 절에서 본 것처럼, 기본 정수형이 i32기 때문에 여기서도 타입은 i32입니다. # fn main() { let v = vec![1, 2, 3];\n# } 예제 8-2: 값을 저장하고 있는 새로운 벡터 생성하기 러스트는 i32 값이 초깃값으로 설정된 것을 이용해, v의 타입을 Vec로 추론할 수 있습니다. 따라서 타입 명시는 필요 없습니다. 다음으로는 벡터를 수정하는 방법을 살펴보겠습니다.","breadcrumbs":"일반적인 컬렉션 » 벡터에 여러 값의 목록 저장하기 » 새 벡터 만들기","id":"130","title":"새 벡터 만들기"},"131":{"body":"벡터를 만들고 여기에 요소를 추가하기 위해서는 다음 예제 8-3처럼 push 메서드를 사용할 수 있습니다: # fn main() { let mut v = Vec::new(); v.push(5); v.push(6); v.push(7); v.push(8);\n# } 예제 8-3: push 메서드를 사용하여 벡터에 값을 추가하기 3장에서 설명한 것처럼, 어떤 변수의 값을 변경하려면 mut 키워드를 사용하여 해당 변수를 가변으로 만들어야 합니다. 또한 Vec 타입 명시를 붙이지 않아도 되는 이유는, 집어넣은 숫자가 모두 i32 타입인 점을 통하여 러스트가 v의 타입을 추론하기 때문입니다.","breadcrumbs":"일반적인 컬렉션 » 벡터에 여러 값의 목록 저장하기 » 벡터 업데이트하기","id":"131","title":"벡터 업데이트하기"},"132":{"body":"벡터에 저장된 값을 참조하는 방법은 인덱싱과 get 메서드 두 가지가 있습니다. 다음 예제에서는 명료한 전달을 위해 각 함수들이 반환하는 값의 타입을 명시했습니다. 예제 8-4는 인덱스 문법과 get 메서드를 가지고 벡터의 값에 접근하는 두 방법을 모두 보여줍니다: # fn main() { let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2]; println!(\"The third element is {third}\"); let third: Option<&i32> = v.get(2); match third { Some(third) => println!(\"The third element is {third}\"), None => println!(\"There is no third element.\"), }\n# } 예제 8-4: 인덱스 문법 혹은 get 메서드를 사용하여 벡터 내의 아이템에 접근하기 여기서 주의할 세부 사항이 몇 가지 있습니다. 벡터의 인덱스는 0부터 시작하므로, 세 번째 값을 얻어오기 위해서는 인덱스 값 2를 사용합니다. &와 []를 사용하면 인덱스 값에 위치한 요소의 참조자를 얻게 됩니다. get 함수에 인덱스를 매개변수로 넘기면, match를 통해 처리할 수 있는 Option<&T>를 얻게 됩니다. 러스트가 벡터 요소를 참조하는 방법을 두 가지 제공하는 이유는 벡터에 없는 인덱스 값을 사용하고자 했을 때 프로그램이 어떻게 동작할 것인지 선택할 수 있도록 하기 위해서입니다. 예를 들어, 아래의 예제 8-5와 같이 5개의 요소를 가지고 있는 벡터가 있고 100 인덱스에 있는 요소에 접근을 시도하는 경우 어떤 일이 생기는지 확인해 봅시다: # fn main() { let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100]; let does_not_exist = v.get(100);\n# } 예제 8-5: 5개의 요소를 가진 벡터에 100 인덱스에 있는 요소에 접근하기 이 프로그램을 실행하면, 첫 번째의 [] 메서드는 패닉을 일으키는데, 이는 존재하지 않는 요소를 참조하기 때문입니다. 이 방법은 프로그램이 벡터의 끝을 넘어서는 요소에 접근하는 시도를 하면 프로그램이 죽게 만들고 싶은 경우 가장 좋습니다. get 함수에 벡터 범위를 벗어난 인덱스가 주어지면 패닉 없이 None이 반환됩니다. 일반적인 상황에서 벡터의 범위 밖에 있는 요소에 접근하는 일이 종종 발생할 수도 있다면 이 방법을 사용할 만합니다. 이 방법을 사용한다면 6장에서 본 것처럼 Some(&element) 혹은 None에 대해 처리하는 로직이 있어야 합니다. 예를 들어 인덱스는 사람이 직접 번호를 입력하는 것으로 들어올 수도 있습니다. 만일 사용자가 잘못하여 너무 큰 숫자를 입력하여 프로그램이 None 값을 받았을 경우라면 사용자에게 현재 Vec에 몇 개의 아이템이 있으며 유효한 값을 입력할 기회를 다시 한 번 줄 수도 있습니다. 이렇게 하는 편이 오타 때문에 프로그램이 죽는 것보다는 더 사용자 친화적이겠죠? 프로그램에 유효한 참조자가 있다면, 대여 검사기 (borrow checker) 가 (4장에서 다루었던) 소유권 및 대여 규칙을 집행하여 이 참조자와 벡터의 내용물로부터 얻은 다른 참조자들이 계속 유효하게 남아있도록 보장합니다. 같은 스코프에서는 가변 참조자와 불변 참조자를 가질 수 없다는 규칙을 상기하세요. 이 규칙은 아래 예제에서도 적용되는데, 예제 8-6에서는 벡터의 첫 번째 요소에 대한 불변 참조자를 얻은 뒤 벡터의 끝에 요소를 추가하는 시도를 합니다. 함수 끝에서 해당 요소에 대한 참조까지 시도한다면 이 프로그램은 동작하지 않을 것입니다: # fn main() { let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); println!(\"The first element is: {first}\");\n# } 예제 8-6: 아이템의 참조자를 가지고 있는 상태에서 벡터에 새로운 요소 추가 시도하기 이 예제를 컴파일하면 아래와 같은 에러가 발생합니다: $ cargo run Compiling collections v0.1.0 (file:///projects/collections)\nerror[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable --> src/main.rs:6:5 |\n4 | let first = &v[0]; | - immutable borrow occurs here\n5 |\n6 | v.push(6); | ^^^^^^^^^ mutable borrow occurs here\n7 |\n8 | println!(\"The first element is: {first}\"); | ----- immutable borrow later used here For more information about this error, try `rustc --explain E0502`.\nerror: could not compile `collections` due to previous error 예제 8-6의 코드는 동작해야 할 것처럼 보일 수도 있겠습니다: 첫 번째 요소의 참조자가 벡터 끝부분의 변경이랑 무슨 상관일까요? 이를 이해하기 위해, 잠시 벡터의 동작 방법을 알아보도록 하겠습니다. 벡터는 모든 요소가 서로 붙어서 메모리에 저장됩니다. 그리고 새로운 요소를 벡터 끝에 추가할 경우, 현재 벡터 메모리 위치에 새로운 요소를 추가할 공간이 없다면, 다른 넉넉한 곳에 메모리를 새로 할당하고 기존 요소를 새로 할당한 공간에 복사합니다. 이 경우, 기존 요소의 참조자는 해제된 메모리를 가리키게 되기 때문에, 이러한 상황을 대여 규칙으로 막아둔 것이죠. Note: Vec 타입의 구현 세부 사항에 대한 그 밖의 것에 대해서는 ‘러스토노미콘 (The Rustonomicon)’ 을 보세요:","breadcrumbs":"일반적인 컬렉션 » 벡터에 여러 값의 목록 저장하기 » 벡터 요소 읽기","id":"132","title":"벡터 요소 읽기"},"133":{"body":"벡터 내의 각 요소를 차례대로 접근하기 위해서는 인덱스를 사용하여 한 번에 하나의 값에 접근하기보다는 모든 요소에 대한 반복 처리를 합니다. 예제 8-7은 for 루프를 사용하여 i32의 벡터에 있는 각 요소에 대한 불변 참조자를 얻어서 이를 출력하는 방법을 보여줍니다: # fn main() { let v = vec![100, 32, 57]; for i in &v { println!(\"{i}\"); }\n# } 예제 8-7: for 루프로 벡터의 요소들에 대해 반복하여 각 요소를 출력하기 모든 요소를 변경하기 위해서는 가변 벡터의 각 요소에 대한 가변 참조자로 반복 작업을 할 수도 있습니다. 예제 8-8의 for 루프는 각 요소에 50을 더할 것입니다: # fn main() { let mut v = vec![100, 32, 57]; for i in &mut v { *i += 50; }\n# } 예제 8-8: 벡터의 요소에 대한 가변 참조자로 반복하기 가변 참조자가 가리키는 값을 수정하려면, += 연산자를 쓰기 전에 * 역참조 연산자로 i의 값을 얻어야 합니다. 역참조 연산자는 15장 ‘포인터를 따라가서 값 얻기’ 에서 자세히 알아볼 예정입니다. 벡터에 대한 반복 처리는 불변이든 가변이든 상관없이 대여 검사 규칙에 의해 안전합니다. 만일 예제 8-7과 예제 8-8의 for 루프 본문에서 아이템을 추가하거나 지우는 시도를 했다면 예제 8-6의 코드에서 본 것과 유사한 컴파일 에러가 발생하게 됩니다. for 루프가 가지고 있는 벡터에 대한 참조자는 전체 벡터에의 동시다발적 수정을 막습니다.","breadcrumbs":"일반적인 컬렉션 » 벡터에 여러 값의 목록 저장하기 » 벡터 값에 대해 반복하기","id":"133","title":"벡터 값에 대해 반복하기"},"134":{"body":"벡터는 같은 타입을 가진 값들만 저장할 수 있습니다. 이는 불편할 수 있습니다; 다른 타입의 아이템들에 대한 리스트를 저장해야 하는 상황도 분명히 있으니까요. 다행히도, 열거형의 배리언트는 같은 열거형 타입 내에 정의가 되므로, 벡터 내에 다른 타입의 값들을 저장할 필요가 있다면 열거형을 정의하여 사용할 수 있습니다! 예를 들어, 스프레드시트의 행으로부터 값들을 가져오고 싶은데, 여기서 어떤 열은 정수를, 어떤 열은 실수를, 어떤 열은 문자열을 갖고 있다고 해봅시다. 다양한 타입의 값을 갖는 배리언트를 보유한 열거형을 정의할 수 있고, 모든 열거형 배리언트들은 해당 열거형 타입과 같은 타입으로 간주됩니다. 그러면 해당 열거형을 담을 벡터를 생성하여 궁극적으로 다양한 타입을 담을 수 있습니다. 예제 8-9에서 이를 보여주고 있습니다: # fn main() { enum SpreadsheetCell { Int(i32), Float(f64), Text(String), } let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from(\"blue\")), SpreadsheetCell::Float(10.12), ];\n# } 예제 8-9: 열거형을 정의하여 벡터 내에 다른 타입의 데이터를 담을 수 있도록 하기 러스트가 컴파일 타임에 벡터 내에 저장될 타입이 무엇인지 알아야 하는 이유는 각 요소를 저장하기 위해 얼마만큼의 힙 메모리가 필요한지 알아야 하기 때문입니다. 또한 이 벡터가 담을 수 있는 타입을 명시적으로 보여줘야 합니다. 만일 러스트가 어떠한 타입이든 담을 수 있는 벡터를 허용한다면, 벡터의 각 요소마다 수행되는 연산에 대해 하나 혹은 그 이상의 타입이 에러를 발생시킬 수도 있습니다. 열거형과 match 표현식을 사용한다는 것은 6장에서 설명한 것처럼 러스트가 컴파일 타임에 가능한 모든 경우를 처리함을 보장해 준다는 뜻입니다. 런타임에 프로그램이 벡터에 저장할 모든 타입 집합을 알지 못하면 열거형을 이용한 방식은 사용할 수 없을 것입니다. 대신 트레이트 객체 (trait object) 를 이용할 수 있는데, 이건 17장에서 다룰 예정입니다. 지금까지 벡터를 이용하는 가장 일반적인 방식 몇 가지를 논의했는데, 표준 라이브러리의 Vec에 정의된 유용한 메서드들이 많이 있으니 API 문서 를 꼭 살펴봐 주시기 바랍니다. 예를 들면, push에 더해서, pop 메서드는 제일 마지막 요소를 반환하고 지워줍니다.","breadcrumbs":"일반적인 컬렉션 » 벡터에 여러 값의 목록 저장하기 » 열거형을 이용해 여러 타입 저장하기","id":"134","title":"열거형을 이용해 여러 타입 저장하기"},"135":{"body":"struct와 마찬가지로, 예제 8-10에 주석으로 표시된 것처럼 벡터는 스코프를 벗어날 때 해제됩니다. # fn main() { { let v = vec![1, 2, 3, 4]; // v를 가지고 작업하기 } // <- 여기서 v가 스코프 밖으로 벗어나고 해제됩니다\n# } 예제 8-10: 벡터와 요소들이 버려지는 위치를 표시 벡터가 버려질 때 벡터의 내용물도 전부 버려집니다. 즉, 벡터가 가지고 있던 정수들의 메모리도 정리됩니다. 대여 검사기는 벡터의 내용물에 대한 참조자의 사용이 해당 벡터가 유효할 때만 발생했는지 확인합니다. 이제 다음 컬렉션 타입인 String으로 넘어갑시다!","breadcrumbs":"일반적인 컬렉션 » 벡터에 여러 값의 목록 저장하기 » 벡터가 버려지면 벡터의 요소도 버려집니다","id":"135","title":"벡터가 버려지면 벡터의 요소도 버려집니다"},"136":{"body":"4장에서도 문자열을 다뤄봤지만, 이번에는 좀 더 깊이 살펴보겠습니다. 갓 입문한 러스타시안은 보통 세 가지 이유의 조합에 의해 문자열 부분에서 막힙니다: 발생할 수 있는 에러를 최대한 표시하는 러스트의 성향, 많은 프로그래머의 예상보다 문자열이 복잡한 자료구조라는 점, 그리고 UTF-8이 그 이유입니다. 이 때문에 다른 언어를 사용하다 넘어오면 러스트의 문자열은 어려워 보이죠. 문자열이 컬렉션 장에 있는 이유는 문자열이 바이트의 컬렉션으로 구현되어 있고, 이 바이트들을 텍스트로 통역할 때 유용한 기능을 제공하는 여러 메서드들을 구현해 두었기 때문입니다. 이번 절에서는 생성, 업데이트, 값 읽기와 같은 모든 컬렉션 타입이 가지고 있는, String에서의 연산에 대해 이야기해 보겠습니다. 또한 String을 다른 컬렉션들과 다르게 만드는 부분, 즉 사람과 컴퓨터가 String 데이터를 통역하는 방식 간의 차이로 인해 생기는 String 인덱싱의 복잡함을 논의해 보겠습니다.","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 문자열에 UTF-8 텍스트 저장하기","id":"136","title":"문자열에 UTF-8 텍스트 저장하기"},"137":{"body":"먼저 문자열 (string) 이라는 용어가 정확히 무엇을 뜻하는 것인지 정의해 보겠습니다. 러스트 언어의 핵심 기능에서는 딱 한 가지 문자열 타입만 제공하는데, 그것은 바로 참조자 형태인 &str는 많이 봤던 문자열 슬라이스 str입니다. 4장에서는 문자열 슬라이스 에 대해 얘기했고, 이는 UTF-8으로 인코딩되어 다른 어딘가에 저장된 문자열 데이터의 참조자입니다. 예를 들어, 문자열 리터럴은 프로그램의 바이너리 결과물 안에 저장되어 있으며, 그러므로 문자열 슬라이스입니다. String 타입은 언어의 핵심 기능에 구현된 것이 아니고 러스트의 표준 라이브러리를 통해 제공되며, 커질 수 있고, 가변적이며, 소유권을 갖고 있고, UTF-8으로 인코딩된 문자열 타입입니다. 러스타시안들이 ‘문자열’에 대해 이야기할 때는 보통 String과 문자열 슬라이스 &str 타입 둘 중 무언가를 이야기하는 것이지, 특정한 하나를 뜻하는 것은 아닙니다. 이번 절은 대부분 String에 관한 것이지만, 두 타입 모두 러스트 표준 라이브러리에서 매우 많이 사용되며 String과 문자열 슬라이스 모두 UTF-8으로 인코딩되어 있습니다.","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 문자열이 뭔가요?","id":"137","title":"문자열이 뭔가요?"},"138":{"body":"Vec에서 쓸 수 있는 연산 다수가 String에서도 똑같이 쓸 수 있는데, 이는 String이 실제로 바이트 벡터에 더하여 몇 가지 보장, 제한, 기능들을 추가한 래퍼 (wrapper) 로 구현되어 있기 때문입니다. Vec와 String이 같은 방식으로 동작한다는 함수의 예시로 예제 8-11과 같이 새 인스턴스를 생성하는 new 함수가 있습니다. # fn main() { let mut s = String::new();\n# } 예제 8-11: 비어있는 새로운 String 생성하기 이 라인은 어떤 데이터를 담을 수 있는 s라는 빈 문자열을 만들어 줍니다. 종종 시작 지점에서 저장해 둘 문자열의 초깃값을 가지고 있을 것입니다. 그럴 때는 to_string 메서드를 이용하는데, 이는 Display 트레이트가 구현된 어떤 타입이든 사용 가능하며, 문자열 리터럴도 이 트레이트를 구현하고 있습니다. 예제 8-12에서 두 가지 예제를 보여주고 있습니다: # fn main() { let data = \"initial contents\"; let s = data.to_string(); // 이 메서드는 리터럴에서도 바로 작동합니다: let s = \"initial contents\".to_string();\n# } 예제 8-12: to_string 메서드를 사용하여 문자열 리터럴로부터 String 생성하기 이 코드는 initial contents를 담고 있는 문자열을 생성합니다. 또한 문자열 리터럴로부터 String을 생성하기 위해서 String::from 함수를 이용할 수도 있습니다. 예제 8-13의 코드는 to_string을 사용하는 예제 8-12의 코드와 동일합니다: # fn main() { let s = String::from(\"initial contents\");\n# } 예제 8-13: String::from 함수를 사용하여 문자열 리터럴로부터 String 생성하기 문자열이 매우 다양한 용도로 사용되기 때문에, 문자열에 다양한 제네릭 API들을 사용할 수 있으며, 이를 통해 다양한 옵션들을 제공할 수 있습니다. 몇몇은 중복되어 보일 수 있지만, 다 사용할 곳이 있습니다! 지금의 경우 String::from과 to_string은 동일한 작업을 수행하므로, 따라서 어떤 것을 사용하는가는 스타일과 가독성의 문제입니다. 문자열이 UTF-8으로 인코딩되었음을 기억하세요. 즉, 아래의 예제 8-14처럼 적합하게 인코딩된 모든 데이터를 집어넣을 수 있습니다: # fn main() { let hello = String::from(\"السلام عليكم\"); let hello = String::from(\"Dobrý den\"); let hello = String::from(\"Hello\"); let hello = String::from(\"שָׁלוֹם\"); let hello = String::from(\"नमस्ते\"); let hello = String::from(\"こんにちは\"); let hello = String::from(\"안녕하세요\"); let hello = String::from(\"你好\"); let hello = String::from(\"Olá\"); let hello = String::from(\"Здравствуйте\"); let hello = String::from(\"Hola\");\n# } 예제 8-14: 문자열에 다양한 언어로 인사말 저장하기 위의 모두가 유효한 String 값입니다.","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 새로운 문자열 생성하기","id":"138","title":"새로운 문자열 생성하기"},"139":{"body":"String은 Vec의 내용물처럼 더 많은 데이터를 집어넣음으면 크기가 커지고 내용물은 변경될 수 있습니다. 또한 + 연산자나 format! 매크로를 사용하여 편리하게 String 값들을 이어붙일 수 있습니다. push_str과 push를 이용하여 문자열 추가하기 예제 8-15처럼 push_str 메서드를 사용하여 문자열 슬라이스를 추가하는 것으로 String을 키울 수 있습니다: # fn main() { let mut s = String::from(\"foo\"); s.push_str(\"bar\");\n# } 예제 8-15: push_str 메서드를 사용하여 String에 문자열 슬라이스 추가하기 위의 두 줄이 실행된 후 s에는 foobar가 들어있을 것입니다. push_str 메서드는 문자열 슬라이스를 매개변수로 갖는데 이는 매개변수의 소유권을 가져올 필요가 없기 때문입니다. 예를 들어, 예제 8-16의 코드에서는 s2의 내용물을 s1에 추가한 후 s2를 쓰려고 합니다. # fn main() { let mut s1 = String::from(\"foo\"); let s2 = \"bar\"; s1.push_str(s2); println!(\"s2 is {s2}\");\n# } 예제 8-16: 문자열 슬라이스를 String에 붙인 이후에 문자열 슬라이스를 사용하기 만일 push_str 함수가 s2의 소유권을 가져갔다면, 마지막 줄에서 이 값을 출력할 수 없었을 것입니다. 하지만 이 코드는 기대했던 대로 작동합니다! push 메서드는 한 개의 글자를 매개변수로 받아서 String에 추가합니다. 예제 8-17은 push 메서드를 사용하여 String에 ‘l’을 추가하고 있습니다: # fn main() { let mut s = String::from(\"lo\"); s.push('l');\n# } 예제 8-17: push를 사용하여 String 값에 한 글자 추가하기 위의 코드를 실행한 결과로 s는 lol을 담고 있을 것입니다. + 연산자나 format! 매크로를 이용한 접합 가지고 있는 두 개의 문자열을 조합하고 싶은 경우도 종종 있습니다. 예제 8-18에 표시된 것처럼 + 연산자를 사용하는 것이 한 가지 방법입니다: # fn main() { let s1 = String::from(\"Hello, \"); let s2 = String::from(\"world!\"); let s3 = s1 + &s2; // s1은 여기로 이동되어 더 이상 사용할 수 없음을 주의하세요\n# } 예제 8-18: + 연산자를 사용하여 두 String 값을 하나의 새로운 String 값으로 조합하기 문자열 s3는 Hello, world!를 담게 될 것입니다. s1이 더하기 연산 이후에 더 이상 유효하지 않은 이유와 s2의 참조자가 사용되는 이유는 + 연산자를 사용했을 때 호출되는 함수의 시그니처와 맞춰야 하기 때문입니다. + 연산자는 add 메서드를 사용하는데, 이 메서드의 시그니처는 아래처럼 생겼습니다: fn add(self, s: &str) -> String { 표준 라이브러리에는 add가 제네릭과 연관 타입을 사용하여 정의되어 있습니다. 여기서는 제네릭에 구체 타입 (concrete type) 을 대입하였고, 이는 String 값으로 이 메서드를 호출했을 때 발생합니다. 제네릭에 대한 내용은 10장에서 다룰 것입니다. 이 시그니처는 + 연산자의 까다로운 부분을 이해하는 데 필요한 단서를 줍니다. 먼저 s2에는 &가 있는데, 즉 첫 번째 문자열에 두 번째 문자열의 참조자 를 더하고 있음을 뜻합니다. 이는 add 함수의 s 매개변수 때문입니다: String에는 &str만 더할 수 있고, 두 String끼리는 더하지 못합니다. 아니, 잠깐만요. &s2의 타입은 &String이지, add의 두 번째 매개변수에 지정된 &str은 아니죠. 어째서 예제 8-18가 컴파일되는 걸까요? &s2를 add 호출에 사용할 수 있는 이유는 &String 인수가 &str로 강제 될 수 있기 때문입니다. add 함수가 호출되면, 러스트는 역참조 강제 (deref coercion) 를 사용하는데, 이것이 add 함수 내에서 사용되는 &s2를 &s2[..]로 바꿉니다. 역참조 강제는 15장에서 더 자세히 다루겠습니다. add가 매개변수의 소유권을 가져가지는 않으므로, s2는 이 연산 이후에도 계속 유효한 String일 것입니다. 두 번째로, 시그니처에서 add가 self의 소유권을 가져가는 것을 볼 수 있는데, 이는 self가 &를 안 가지고 있기 때문입니다. 즉 예제 8-18에서 s1이 add 호출로 이동되어 이후에는 더 이상 유효하지 않을 것이라는 의미입니다. 따라서 let s3 = s1 + &s2;가 마치 두 문자열을 복사하여 새로운 문자열을 만들 것처럼 보일지라도, 실제로 이 구문은 s1의 소유권을 가져다가 s2의 내용물의 복사본을 추가한 다음, 결과물의 소유권을 반환합니다. 바꿔 말하면, 이 구문은 여러 복사본을 만드는 것처럼 보여도 그렇지 않습니다: 이러한 구현은 복사보다 더 효율적입니다. 만일 여러 문자열을 접하고자 한다면, +의 동작은 다루기 불편해 집니다: # fn main() { let s1 = String::from(\"tic\"); let s2 = String::from(\"tac\"); let s3 = String::from(\"toe\"); let s = s1 + \"-\" + &s2 + \"-\" + &s3;\n# } 이 시점에서 s는 tic-tac-toe가 될 것입니다. +와 \" 문자가 많으면 어떤 결과가 나올지 확인이 어렵습니다. 더 복잡한 문자열 조합에는 대신 format! 매크로를 사용할 수 있습니다: # fn main() { let s1 = String::from(\"tic\"); let s2 = String::from(\"tac\"); let s3 = String::from(\"toe\"); let s = format!(\"{s1}-{s2}-{s3}\");\n# } 이 코드 또한 s에 tic-tac-toe를 설정합니다. format! 매크로는 println!처럼 작동하지만, 화면에 결과를 출력하는 대신 결과가 담긴 String을 반환해 줍니다. format!을 이용한 버전이 훨씬 읽기 쉽고, format! 매크로로 만들어진 코드는 참조자를 이용하므로 이 호출은 아무 매개변수의 소유권도 가져가지 않습니다.","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 문자열 업데이트하기","id":"139","title":"문자열 업데이트하기"},"14":{"body":"이번 장을 비롯해 터미널에 명령어를 입력할 일이 많습니다. 입력할 명령어와 출력을 구분하실 수 있도록, 명령어에는 각 행 앞에 $가 붙습니다. $가 붙지 않은 행은 보통 앞선 명령어의 결과를 나타낸다고 보시면 됩니다. 예외적으로, $ 대신 >가 붙은 예제는 PowerShell 한정 예제입니다.","breadcrumbs":"시작해봅시다 » 러스트 설치 » 커맨드 라인 표기","id":"14","title":"커맨드 라인 표기"},"140":{"body":"다른 많은 프로그래밍 언어에서, 인덱스를 이용한 참조를 통해 문자열 내부의 개별 문자에 접근하는 것은 유효하고 범용적인 연산에 속합니다. 그러나 러스트에서 인덱싱 문법을 이용하여 String의 부분에 접근하고자 하면 에러를 얻게 됩니다. 아래 예제 8-19와 같은 코드를 생각해 봅시다: # fn main() { let s1 = String::from(\"hello\"); let h = s1[0];\n# } 예제 8-19: 문자열에 인덱싱 문법을 사용하는 시도 이 코드는 아래와 같은 에러를 출력합니다: $ cargo run Compiling collections v0.1.0 (file:///projects/collections)\nerror[E0277]: the type `String` cannot be indexed by `{integer}` --> src/main.rs:3:13 |\n3 | let h = s1[0]; | ^^^^^ `String` cannot be indexed by `{integer}` | = help: the trait `Index<{integer}>` is not implemented for `String` = help: the following other types implement trait `Index`: >> > >> >> >> >> For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `collections` due to previous error 에러와 노트 부분이 이야기해 줍니다: 러스트 문자열은 인덱싱을 지원하지 않는다고 하는군요. 그런데 왜 안 되는 걸까요? 이 질문에 답하기 위해서는 러스트가 문자열을 메모리에 저장하는 방법에 대해 설명해야 합니다. 내부적 표현 String은 Vec을 감싼 것입니다. 예제 8-14에서 보았던 적합하게 인코딩된 UTF-8 예제 문자을 몇 가지를 살펴봅시다. 첫 번째로, 이것입니다: # fn main() {\n# let hello = String::from(\"السلام عليكم\");\n# let hello = String::from(\"Dobrý den\");\n# let hello = String::from(\"Hello\");\n# let hello = String::from(\"שָׁלוֹם\");\n# let hello = String::from(\"नमस्ते\");\n# let hello = String::from(\"こんにちは\");\n# let hello = String::from(\"안녕하세요\");\n# let hello = String::from(\"你好\");\n# let hello = String::from(\"Olá\");\n# let hello = String::from(\"Здравствуйте\"); let hello = String::from(\"Hola\");\n# } 이 경우 len은 4가 되는데, 이는 문자열 ‘Hola’를 저장하고 있는 Vec이 4바이트 길이라는 뜻입니다. UTF-8으로 인코딩되면 각각의 글자들이 1바이트씩 차지한다는 것이죠. 그러나 다음 줄은 아마도 여러분을 놀라게 할 수도 있습니다. (맨 앞의 문자는 아라비아 숫자 3이 아닌, 키릴 문자 Ze입니다.) # fn main() {\n# let hello = String::from(\"السلام عليكم\");\n# let hello = String::from(\"Dobrý den\");\n# let hello = String::from(\"Hello\");\n# let hello = String::from(\"שָׁלוֹם\");\n# let hello = String::from(\"नमस्ते\");\n# let hello = String::from(\"こんにちは\");\n# let hello = String::from(\"안녕하세요\");\n# let hello = String::from(\"你好\");\n# let hello = String::from(\"Olá\"); let hello = String::from(\"Здравствуйте\");\n# let hello = String::from(\"Hola\");\n# } 이 문자열의 길이가 얼마인지 묻는다면, 여러분은 12라고 답할지도 모릅니다. 실제 러스트의 대답은 24입니다. 이는 \"Здравствуйте\"를 UTF-8으로 인코딩된 바이트들의 크기인데, 각각의 유니코드 스칼라 값이 저장소의 2바이트를 차지하기 때문입니다. 따라서, 문자열의 바이트 안의 인덱스는 유효한 유니코드 스칼라 값과 항상 대응되지는 않을 것입니다. 이를 설명하기 위해 다음과 같은 유효하지 않은 러스트 코드를 고려해 보겠습니다: let hello = \"Здравствуйте\";\nlet answer = &hello[0]; 여러분은 이미 answer가 첫 번째 글자인 З이 아닐 것이란 점을 알고 있습니다. UTF-8으로 인코딩될 때, З의 첫 번째 바이트는 208이고, 두 번째는 151이므로, answer는 사실 208이 되어야 하지만, 208은 그 자체로는 유효한 문자가 아닙니다. 208을 반환하는 것은 이 문자열의 첫 번째 글자를 요청했을 때 예상한 것이 아닙니다. 하지만 그게 러스트가 인덱스 0에 가지고 있는 유일한 데이터죠. 라틴 글자들만 있는 경우일지라도, 일반적으로 바이트 값의 반환이 사용자들이 원하는 것은 아닐 겁니다. : &\"hello\"[0]는 h가 아니라 104를 반환합니다. 따라서 예상치 못한 값을 반환하고 즉시 발견되지 않을 수 있는 버그를 방지하기 위해서, 러스트는 이러한 코드를 전혀 컴파일하지 않고 이러한 오해들을 개발 과정 내에서 일찌감치 방지한다는 것이 정답입니다. 바이트와 스칼라 값과 문자소 클러스터! 이런! UTF-8에 대한 또 다른 요점은, 실제로는 러스트의 관점에서 문자열을 보는 세 가지 관련 방식이 있다는 것입니다: 바이트, 스칼라 값, 그리고 문자소 클러스터 (grapheme cluster, 우리가 글자 라고 부르는 것과 가장 근접한 것) 입니다. 데바나가리 (Devanagari) 글자로 쓰인 힌디어 ‘नमस्ते’를 보면, 이것은 궁극적으로 아래와 같은 u8 값들의 Vec으로 저장됩니다: [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,\n224, 165, 135] 이건 18바이트이고 컴퓨터가 이 데이터를 궁극적으로 저장하는 방법입니다. 만일 이를 유니코드 스칼라 값, 즉 러스트의 char 타입인 형태로 본다면, 아래와 같이 보이게 됩니다: ['न', 'म', 'स', '्', 'त', 'े'] 여섯 개의 char 값이 있지만, 네 번째와 여섯 번째는 글자가 아닙니다: 그 자체로는 이해할 수 없는 발음 구별 부호입니다. 마지막으로, 이 문자열을 문자소 클러스터로 본다면, 이 힌디 단어를 구성하는 네 글자를 알아낼 수 있습니다: [\"न\", \"म\", \"स्\", \"ते\"] 러스트는 컴퓨터가 저장하는 원시 문자열 (raw string) 을 번역하는 다양한 방법을 제공하여, 데이터가 담고 있는 것이 무슨 언어든 상관없이 각 프로그램이 필요로 하는 통역방식을 선택할 수 있도록 합니다. 러스트가 String을 인덱스로 접근하여 문자를 얻지 못하도록 하는 마지막 이유는 인덱스 연산이 언제나 상수 시간(O(1))에 실행될 것으로 기대받기 때문입니다. 그러나 String을 가지고 그러한 성능을 보장하는 것은 불가능한데, 그 이유는 러스트가 문자열 내에 유효한 문자가 몇 개 있는지 알아내기 위해 내용물을 시작 지점부터 인덱스로 지정된 곳까지 훑어야 하기 때문입니다.","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 문자열 내부의 인덱싱","id":"140","title":"문자열 내부의 인덱싱"},"141":{"body":"문자열 인덱싱의 반환 타입이 어떤 것이 (바이트 값인지, 캐릭터인지, 문자소 클러스터인지, 혹은 문자열 슬라이스인지) 되어야 하는지 명확하지 않기 때문에 문자열의 인덱싱은 종종 좋지 않은 생각이 됩니다. 따라서 문자열 슬라이스를 만들기 위해 정말로 인덱스를 사용하고자 한다면 러스트는 좀 더 구체적인 지정을 요청합니다. []에 숫자 하나를 사용하는 인덱싱이 아니라 []와 범위를 사용하여 특정 바이트들이 담고 있는 문자열 슬라이스를 만들 수 있습니다: let hello = \"Здравствуйте\"; let s = &hello[0..4]; 여기서 s는 문자열의 첫 4바이트를 담고 있는 &str가 됩니다. 앞서 우리는 이 글자들이 각각 2바이트를 차지한다고 언급했으므로, 이는 s가 ‘Зд’이 될 것이란 뜻입니다. 만약에 &hello[0..1]처럼 문자 바이트의 일부를 슬라이스를 얻으려고 한다면, 러스트는 벡터 내에 유효하지 않은 인덱스에 접근했을 때와 동일한 방식으로 런타임에 패닉을 발생시킬 것입니다. $ cargo run Compiling collections v0.1.0 (file:///projects/collections) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/collections`\nthread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З' (bytes 0..2) of `Здравствуйте`', src/main.rs:4:14\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 범위를 지정하여 문자열 슬라이스를 생성하는 것은 프로그램을 죽게 만들 수도 있기 때문에 주의깊게 사용되어야 합니다.","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 문자열 슬라이싱하기","id":"141","title":"문자열 슬라이싱하기"},"142":{"body":"문자열 조각에 대한 연산을 하는 가장 좋은 방법은 명시적으로 문자를 원하는 것인지 아니면 바이트를 원하는 것인지 지정하는 것입니다. 개별적인 유니코드 스칼라 값에 대해서는 chars 메서드를 사용하세요. ‘Зд’에 대해 chars 함수를 호출하면 각각을 분리하여 char 타입의 두 개의 값을 반환하고, 이 결과에 대한 반복을 통하여 각 요소에 접근할 수 있습니다: for c in \"Зд\".chars() { println!(\"{c}\");\n} 이 코드는 다음을 출력할 것입니다: З\nд 다른 방법으로 bytes 메서드는 각 원시 바이트를 반환하는데, 문제의 도메인이 무엇인가에 따라 적절할 수도 있습니다: for b in \"Зд\".bytes() { println!(\"{b}\");\n} 위의 코드는 이 문자열을 구성하는 네 개의 바이트를 출력합니다: 208\n151\n208\n180 하지만 유효한 유니코드 스칼라 값이 하나 이상의 바이트로 구성될지도 모른다는 것을 확실히 기억해 주세요. 데바나가리 문서와 같은 문자열로부터 문자소 클러스터를 얻는 방법은 복잡해서, 이 기능은 표준 라이브러리를 통해 제공되지 않습니다. 여러분이 원하는 기능이 이것이라면 crates.io 에 사용 가능한 크레이트가 있습니다.","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 문자열에 대한 반복을 위한 메서드","id":"142","title":"문자열에 대한 반복을 위한 메서드"},"143":{"body":"요약하자면, 문자열은 복잡합니다. 프로그래밍 언어마다 이러한 복잡성을 프로그래머에게 표현하는 방법에 대해 다른 선택을 합니다. 러스트는 String 데이터의 올바른 처리가 모든 러스트 프로그램의 기본 동작으로 선택했는데, 이는 프로그래머가 UTF-8 데이터를 처리할 때 미리 더 많은 생각을 해야 함을 의미합니다. 이러한 절충안은 다른 프로그래밍 언어보다 문자열의 복잡성을 더 많이 노출시키지만, 한편으로는 여러분의 개발 수명 주기 후반에 ASCII 아닌 문자와 관련된 에러를 처리해야 할 필요가 없도록 해줍니다. 좋은 소식은 표준 라이브러리에 이런 복잡한 상황을 올바르게 처리하는 데 도움이 될 String 및 &str 타입 기반의 기능을 다양하게 제공한다는 점입니다. 문자열 검색을 위한 contains와 문자열 일부를 다른 문자열로 바꾸는 replace 같은 유용한 메서드들에 대해 알아보려면 꼭 문서를 확인해 보세요. 이것보다 살짝 덜 복잡한 것으로 옮겨 갑시다: 해시맵이요!","breadcrumbs":"일반적인 컬렉션 » 문자열에 UTF-8 텍스트 저장하기 » 문자열은 그렇게 단순하지 않습니다","id":"143","title":"문자열은 그렇게 단순하지 않습니다"},"144":{"body":"마지막으로 볼 일반적인 컬렉션은 해시맵 (hash map) 입니다. HashMap 타입은 K 타입의 키와 V 타입의 값에 대해 해시 함수 (hashing function) 를 사용하여 매핑한 것을 저장하는데, 이 해시 함수는 이 키와 값을 메모리 어디에 저장할지 결정합니다. 수많은 다른 프로그래밍 언어도 이러한 종류의 데이터 구조를 지원하지만, 종종 해시, 맵, 오브젝트, 해시 테이블, 혹은 연관 배열 (associative) 등과 같이 이름만 다르게 사용됩니다. 해시맵은 벡터에서처럼 인덱스를 이용하는 것이 아니라 임의의 타입으로 된 키를 이용하여 데이터를 찾고 싶을 때 유용합니다. 예를 들면, 게임에서 각 팀의 점수를 해시맵에 유지할 수 있는데, 여기서 키는 팀의 이름이고 값은 팀의 점수가 됩니다. 팀의 이름을 제공하면 그 팀의 점수를 조회할 수 있습니다. 이번 절에서는 해시맵의 기본 API를 다룰 것이지만, 표준 라이브러리의 HashMap에 정의되어 있는 함수 중에는 더 많은 좋은 것들이 숨어있습니다. 항상 말했듯이, 더 많은 정보를 원하신다면 표준 라이브러리 문서를 확인하세요.","breadcrumbs":"일반적인 컬렉션 » 해시맵에 서로 연관된 키와 값 저장하기 » 해시맵에 서로 연관된 키와 값 저장하기","id":"144","title":"해시맵에 서로 연관된 키와 값 저장하기"},"145":{"body":"빈 해시맵 생성하는 한 가지 방법으로 new를 사용한 뒤 insert를 이용하여 요소를 추가하는 것이 있습니다. 예제 8-20에서는 팀 이름이 각각 블루 와 옐로 인 두 팀의 점수를 관리하고 있습니다. 블루 팀은 10점, 옐로 팀은 50점으로 시작할 것입니다: # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10); scores.insert(String::from(\"Yellow\"), 50);\n# } 예제 8-20: 새로운 해시맵을 생성하여 몇 개의 키와 값을 집어넣기 먼저 표준 라이브러리의 컬렉션 부분으로부터 HashMap을 use로 가져와야 할 필요가 있음을 주목하세요. 이 장에서 보고 있는 세 가지 일반적인 컬렉션 중 해시맵이 제일 덜 자주 사용되는 것이기 때문에, 프렐루드의 자동으로 가져오는 기능에는 포함되어 있지 않습니다. 또한 해시맵은 표준 라이브러리로부터의 지원을 덜 받습니다; 예를 들면 해시맵을 생성하는 기본 제공 매크로가 없습니다. 벡터와 마찬가지로, 해시맵도 데이터를 힙에 저장합니다. 이 HashMap은 String 타입의 키와 i32 타입의 값을 갖습니다. 벡터와 비슷하게 해시맵도 동질적입니다: 모든 키는 서로 같은 타입이어야 하고, 모든 값도 같은 타입이여야 합니다.","breadcrumbs":"일반적인 컬렉션 » 해시맵에 서로 연관된 키와 값 저장하기 » 새로운 해시맵 생성하기","id":"145","title":"새로운 해시맵 생성하기"},"146":{"body":"예제 8-21처럼 get 메서드에 키를 제공하여 해시맵으로부터 값을 얻어올 수 있습니다. # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10); scores.insert(String::from(\"Yellow\"), 50); let team_name = String::from(\"Blue\"); let score = scores.get(&team_name).copied().unwrap_or(0);\n# } 예제 8-23: 해시맵 내에 저장된 블루 팀의 점수 접근하기 여기서 score는 블루 팀과 연관된 값을 갖게 될 것이고, 결괏값은 10일 것입니다. get 메서드는 Option<&V>를 반환합니다; 만일 이 해시맵에 해당 키에 대한 값이 없다면 get은 None을 반환할 것입니다. 이 프로그램에서는 copied를 호출하여 Option<&i32>가 아닌 Option를 얻어온 다음, unwrap_or를 써서 scores가 해당 키에 대한 아이템을 가지고 있지 않을 경우 score에 0을 설정하도록 처리합니다. 벡터에서와 유사한 방식으로 for 루프를 사용하여 해시맵 내의 키/값 쌍에 대한 반복 작업을 수행할 수 있습니다: # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10); scores.insert(String::from(\"Yellow\"), 50); for (key, value) in &scores { println!(\"{key}: {value}\"); }\n# } 이 코드는 각각의 쌍을 임의의 순서로 출력할 것입니다: Yellow: 50\nBlue: 10","breadcrumbs":"일반적인 컬렉션 » 해시맵에 서로 연관된 키와 값 저장하기 » 해시맵의 값 접근하기","id":"146","title":"해시맵의 값 접근하기"},"147":{"body":"i32처럼 Copy 트레이트를 구현한 타입의 값은 해시맵 안으로 복사됩니다. String처럼 소유권이 있는 값의 경우, 아래의 예제 8-22와 같이 값들이 이동되어 해시맵이 그 값의 소유자가 됩니다: # fn main() { use std::collections::HashMap; let field_name = String::from(\"Favorite color\"); let field_value = String::from(\"Blue\"); let mut map = HashMap::new(); map.insert(field_name, field_value); // field_name과 field_value는 이 시점부터 유효하지 않습니다. // 사용을 시도해보고 무슨 컴파일러 에러가 발생하는 알아보세요!\n# } 예제 8-22: 키와 값이 삽입되는 순간 이들이 해시맵의 소유가 되는 것을 보여주는 예 insert를 호출하여 field_name과 field_value를 해시맵으로 이동시킨 후에는 더 이상 이 둘을 사용할 수 없습니다. 해시맵에 값들의 참조자들을 삽입한다면, 이 값들은 해시맵으로 이동되지 않을 것입니다. 하지만 참조자가 가리키고 있는 값은 해시맵이 유효할 때까지 계속 유효해야 합니다. 이와 관련하여 10장의 ‘라이프타임으로 참조자의 유효성 검증하기’ 절에서 더 자세히 이야기할 것입니다.","breadcrumbs":"일반적인 컬렉션 » 해시맵에 서로 연관된 키와 값 저장하기 » 해시맵과 소유권","id":"147","title":"해시맵과 소유권"},"148":{"body":"키와 값 쌍의 개수는 늘어날 수 있을지라도, 각각의 유일한 키는 연관된 값을 딱 하나만 가질 수 있습니다. (그 역은 성립하지 않습니다: 예를 들면 블루 팀과 옐로 팀 모두 scores 해시맵에 10점을 저장할 수도 있습니다.) 해시맵의 데이터를 변경하고 싶을 때는 키에 이미 값이 할당되어 있을 경우에 대한 처리 방법을 결정해야 합니다. 예전 값을 완전히 무시하면서 새 값으로 대신할 수도 있습니다. 혹은 예전 값을 계속 유지하면서 새 값은 무시하고, 해당 키에 값이 할당되어 있지 않을 경우에만 새 값을 추가하는 방법을 선택할 수도 있습니다. 또는 예전 값과 새 값을 조합할 수도 있습니다. 각각의 경우를 어떻게 할지 살펴봅시다! 값을 덮어쓰기 해시맵에 어떤 키와 값을 삽입하고, 그 후 똑같은 키에 다른 값을 삽입하면, 해당 키에 연관된 값은 새 값으로 대신될 것입니다. 아래 예제 8-23의 코드가 insert를 두 번 호출함에도, 해시맵은 딱 하나의 키/값 쌍을 담게 되는데 그 이유는 두 번 모두 블루 팀의 키에 대한 값을 삽입하고 있기 때문입니다: # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10); scores.insert(String::from(\"Blue\"), 25); println!(\"{:?}\", scores);\n# } 예제 8-23: 특정한 키로 저장된 값을 덮어쓰기 이 코드는 {\"Blue\": 25}를 출력할 것입니다. 원래의 값 10은 덮어써졌습니다. 키가 없을 때만 키와 값 추가하기 해시맵 내에 특정 키가 이미 있는지 검사한 뒤, 다음과 같은 동작을 하는 경우는 흔합니다: 만일 키가 해시맵 내에 존재하면, 해당 값은 그대로 둬야 합니다. 만일 키가 없다면, 키와 그에 대한 값을 추가합니다. 해시맵은 이를 위해 entry라고 하는 특별한 API를 가지고 있는데, 이는 검사하려는 키를 매개변수로 받습니다. entry 함수의 반환 값은 열거형 Entry인데, 해당 키가 있는지 혹은 없는지를 나타냅니다. 옐로 팀에 대한 키에 대한 값이 있는지 검사하고 싶다고 해봅시다. 만일 없다면 값 50을 삽입하고, 블루 팀에 대해서도 똑같이 하려고 합니다. entry API를 사용한 코드는 아래의 예제 8-24와 같습니다. # fn main() { use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10); scores.entry(String::from(\"Yellow\")).or_insert(50); scores.entry(String::from(\"Blue\")).or_insert(50); println!(\"{:?}\", scores);\n# } 예제 8-24: entry 메서드를 이용하여 어떤 키가 값을 이미 갖고 있지 않을 경우에만 추가하기 Entry의 or_insert 메서드는 해당 키가 존재할 경우 Entry 키에 대한 연관된 값을 반환하도록 정의되어 있고, 그렇지 않은 경우 매개변수로 제공된 값을 해당 키에 대한 새 값으로 삽입하고 수정된 Entry에 대한 값을 반환합니다. 이 방법은 직접 로직을 작성하는 것보다 훨씬 깔끔하고, 게다가 대여 검사기와 잘 어울려 동작합니다. 예제 8-24의 코드를 실행하면 {\"Yellow\": 50, \"Blue\": 10}를 출력할 것입니다. 첫 번째 entry 호출은 옐로 팀에 대한 키에 대하여 값 50을 삽입하는데, 이는 옐로 팀이 값을 가지고 있지 않기 때문입니다. 두 번째 entry 호출은 해시맵을 변경하지 않는데, 왜냐하면 블루 팀은 이미 값 10을 가지고 있기 때문입니다. 예전 값에 기초하여 값을 업데이트하기 해시맵에 대한 또 다른 일반적인 사용 방식은 키에 대한 값을 찾아서 예전 값에 기초하여 값을 업데이트하는 것입니다. 예를 들어, 예제 8-25는 어떤 텍스트 내에 각 단어가 몇 번이나 나왔는지를 세는 코드를 보여줍니다. 단어를 키로 사용하는 해시맵을 이용하여 해당 단어가 몇 번이나 나왔는지 추적하기 위해 값을 증가시켜 줍니다. 처음 본 단어라면, 값 0을 삽입할 것입니다. # fn main() { use std::collections::HashMap; let text = \"hello world wonderful world\"; let mut map = HashMap::new(); for word in text.split_whitespace() { let count = map.entry(word).or_insert(0); *count += 1; } println!(\"{:?}\", map);\n# } 예제 8-25: 단어와 횟수를 저장하는 해시맵을 사용하여 단어의 등장 횟수 세기 이 코드는 {\"world\": 2, \"hello\": 1, \"wonderful\": 1}를 출력할 것입니다. 이러한 키/값 쌍의 출력 순서가 다를 수도 있습니다: ‘해시맵의 값 접근하기’ 절에서 해시맵에 대한 반복 처리가 임의의 순서로 일어난다고 한 것을 상기해 봅시다. split_whitespace 메서드는 text의 값을 공백문자로 나눈 서브 슬라이스에 대한 반복자를 반환합니다. or_insert 메서드는 실제로는 해당 키에 대한 값의 가변 참조자(&mut V)를 반환합니다. 여기서는 count 변수에 가변 참조자를 저장하였고, 여기에 값을 할당하기 위해 먼저 애스터리스크(*)를 사용하여 count를 역참조해야 합니다. 가변 참조자는 for 루프의 끝에서 스코프 밖으로 벗어나고, 따라서 모든 값의 변경은 안전하며 대여 규칙에 위배되지 않습니다.","breadcrumbs":"일반적인 컬렉션 » 해시맵에 서로 연관된 키와 값 저장하기 » 해시맵 업데이트하기","id":"148","title":"해시맵 업데이트하기"},"149":{"body":"기본적으로 HashMap은 해시 테이블과 관련된 서비스 거부 공격 (Denial of Service(DoS) attack) 에 저항 기능을 제공할 수 있는 SipHash 라 불리는 해시 함수를 사용합니다 [1] . 이는 사용할 수 있는 가장 빠른 해시 알고리즘은 아니지만, 성능을 떨어트리면서 더 나은 보안을 취하는 거래는 가치가 있습니다. 만일 여러분의 코드를 프로파일링해보니 기본 해시 함수가 여러분의 목적에 사용되기엔 너무 느리다면, 다른 해시어를 지정하여 다른 함수로 바꿀 수 있습니다. 해시어 (hasher) 는 BuildHasher 트레이트를 구현한 타입을 말합니다. 트레이트와 이를 구현하는 방법에 대해서는 10장에서 다룰 것입니다. 여러분의 해시어를 바닥부터 새로 구현해야 할 필요는 없습니다; crates.io 에는 수많은 범용적인 해시 알고리즘을 구현한 해시어를 제공하는 공유 라이브러리가 있습니다. https://en.wikipedia.org/wiki/SipHash","breadcrumbs":"일반적인 컬렉션 » 해시맵에 서로 연관된 키와 값 저장하기 » 해시 함수","id":"149","title":"해시 함수"},"15":{"body":"Linux 나 macOS 사용자는 터미널을 열고 다음 명령어를 입력해 주세요: $ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh 최신 stable 버전 러스트를 설치하는 데 사용할 rustup 도구를 설치하는 명령어입니다. (설치할 때 여러분 비밀번호를 묻는 메시지가 나타날 수 있습니다.) 설치가 완료되면 다음 문장이 나타납니다: Rust is installed now. Great! 링커는 기본으로 설치되나, 러스트 컴파일 시에 링커를 실행할 수 없다는 에러가 나타나면 따로 설치하셔야 합니다. 이 에러는 C 컴파일러를 설치할 때 같이 설치되는 링커로 해결되므로 플랫폼에 맞는 C 컴파일러를 찾아서 설치하시기 바랍니다. 몇 가지 흔히 사용되는 러스트 패키지들이 C 코드를 이용하고 있기 때문에 C 컴파일러가 필요할 수도 있습니다. macOS에서는 아래와 같이 실행하여 C 컴파일러를 설치할 수 있습니다: $ xcode-select --install Linux 사용자의 경우 배포판의 문서에 의하면 일반적으로 GCC나 Clang이 설치되어 있습니다. 예를 들어 우분투 사용자라면 build-essential 패키지를 설치할 수 있습니다.","breadcrumbs":"시작해봅시다 » 러스트 설치 » rustup 설치 - Linux 및 macOS","id":"15","title":"rustup 설치 - Linux 및 macOS"},"150":{"body":"벡터, 문자열, 해시맵은 프로그램에서 여러분이 데이터를 저장하고, 접근하고, 수정하고 싶은 곳에 필요한 수많은 기능들을 제공해 줄 것입니다. 이제 여러분이 풀 준비가 되어 있어야 할 만한 몇 가지 연습문제를 소개합니다: 정수 리스트가 주어졌을 때, 벡터를 이용하여 이 리스트의 중간값 (median, 정렬했을 때 가장 가운데 위치한 값), 그리고 최빈값 (mode, 가장 많이 발생한 값; 해시맵이 여기서 도움이 될 것입니다) 을 반환해 보세요. 문자열을 피그 라틴 (pig Latin) 으로 변경해 보세요. 각 단어의 첫 번째 자음은 단어의 끝으로 이동하고 ‘ay’를 붙이므로, ‘first’는 ‘irst-fay’가 됩니다. 모음으로 시작하는 단어는 대신 끝에 ‘hay’를 붙입니다. (‘apple’은 ‘apple-hay’가 됩니다.) UTF-8 인코딩에 대한 세부 사항을 명심하세요! 해시맵과 벡터를 이용하여 사용자가 회사 부서의 직원 이름을 추가할 수 있도록 하는 텍스트 인터페이스를 만들어 보세요. 예를 들어 ‘Add Sally to Engineering’이나 ‘Add Amir to Sales’ 같은 식으로요. 그 후 사용자가 모든 사람에 대해 알파벳 순으로 정렬된 목록이나 부서별 모든 사람에 대한 목록을 조회할 수 있도록 해보세요. 표준 라이브러리 API 문서는 이 연습문제들에 도움될 만한 벡터, 문자열, 해시맵의 메서드를 설명해 줍니다! 연산이 실패할 수 있는 더 복잡한 프로그램이 등장하고 있는 상황입니다; 따라서, 다음은 에러 처리에 대해 다룰 완벽한 시간이란 뜻이죠!","breadcrumbs":"일반적인 컬렉션 » 해시맵에 서로 연관된 키와 값 저장하기 » 정리","id":"150","title":"정리"},"151":{"body":"소프트웨어에서 에러는 삶의 일부이므로, 러스트는 뭔가 잘못되는 상황을 처리하기 위한 기능을 몇 가지 갖추고 있습니다. 대부분의 경우 러스트에서는 코드가 컴파일 되기 전에 에러의 가능성을 인지하고 조치를 취해야 합니다. 이러한 요구사항은 여러분의 코드를 프로덕션 환경에 배포하기 전에 에러를 발견하고 적절히 조치할 것을 보장하여 여러분의 프로그램을 더 견고하게 해 줍니다! 러스트는 에러를 복구 가능한 (recoverable) 에러와 복구 불가능한 (unrecoverable) 에러 두 가지 범주로 묶습니다. 파일을 찾을 수 없음 에러 같은 복구 가능한 에러에 대해서는 대부분의 경우 그저 사용자에게 문제를 보고하고 명령을 재시도하도록 하길 원합니다. 복구 불가능한 에러는 배열 끝을 넘어선 위치에 접근하는 경우처럼 언제나 버그 증상이 나타나는 에러이며, 따라서 프로그램을 즉시 멈추기를 원합니다. 대부분의 언어는 예외 처리 (exception) 와 같은 메커니즘을 이용하여 이 두 종류의 에러를 구분하지 않고 같은 방식으로 처리합니다. 러스트에는 예외 처리 기능이 없습니다. 대신, 복구 가능한 에러를 위한 Result 타입과 복구 불가능한 에러가 발생했을 때 프로그램을 종료하는 panic! 매크로가 있습니다. 이번 장에서는 panic!을 호출하는 것을 먼저 다룬 뒤, Result 값을 반환하는 것에 대해 이야기하겠습니다. 또한 에러로부터 복구를 시도할지 아니면 실행을 멈출지를 결정할 때의 고려 사항을 탐구해 보겠습니다.","breadcrumbs":"에러 처리 » 에러 처리","id":"151","title":"에러 처리"},"152":{"body":"가끔은 코드에서 나쁜 일이 일어나고, 이에 대해 여러분이 할 수 있는 것이 없을 수도 있습니다. 이런 경우를 위해 러스트에는 panic! 매크로가 있습니다. 실제로 패닉을 일으키는 두 가지 방법이 있습니다: (배열 끝부분을 넘어선 접근과 같이) 코드가 패닉을 일으킬 동작을 하는 것 혹은 panic! 매크로를 명시적으로 호출하는 것이죠. 두 경우 모두 프로그램에 패닉을 일으킵니다. 기본적으로 이러한 패닉은 실패 메시지를 출력하고, 되감고 (unwind), 스택을 청소하고, 종료합니다. 패닉이 발생했을 때 그 패닉의 근원을 쉽게 추적하기 위해 환경 변수를 통하여 러스트가 호출 스택을 보여주도록 할 수 있습니다.","breadcrumbs":"에러 처리 » panic!으로 복구 불가능한 에러 처리하기 » panic!으로 복구 불가능한 에러 처리하기","id":"152","title":"panic!으로 복구 불가능한 에러 처리하기"},"153":{"body":"기본적으로, panic!이 발생하면, 프로그램은 되감기 (unwinding) 를 시작하는데, 이는 러스트가 패닉을 발생시킨 각 함수로부터 스택을 거꾸로 훑어가면서 데이터를 청소한다는 뜻입니다. 하지만 이 되감기와 청소 작업은 간단한 작업이 아닙니다. 그래서 러스트에서는 프로그램이 데이터 정리 작업 없이 즉각 종료되는 대안인 그만두기 (aborting) 를 선택할 수도 있습니다. 프로그램이 사용하고 있던 메모리는 운영체제가 청소해 주어야 합니다. 프로젝트 내에서 결과 바이너리를 가능한 한 작게 만들고 싶다면, Cargo.toml 내에서 적합한 [profile] 섹션에 panic = 'abort'를 추가하여 되감기를 그만두기로 바꿀 수 있습니다. 예를 들어, 여러분이 릴리즈 모드에서는 패닉 시 그만두기 방식을 쓰고 싶다면, 다음을 추가하세요: [profile.release]\npanic = 'abort' 간단한 프로그램에서 panic!을 호출해 봅시다: 파일명: src/main.rs fn main() { panic!(\"crash and burn\");\n} 프로그램을 실행하면, 다음과 같은 내용이 나타납니다: $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.25s Running `target/debug/panic`\nthread 'main' panicked at 'crash and burn', src/main.rs:2:5\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace panic!의 호출이 마지막 두 줄의 에러 메시지를 일으킵니다. 첫 번째 줄은 작성해둔 패닉 메시지와 패닉이 발생한 소스 코드 지점을 보여줍니다. src/main.rs:2:5 는 src/main.rs 파일의 두 번째 줄 다섯 번째 문자를 나타냅니다. 이 예제에서는 표시된 줄이 직접 작성한 코드 부분이고, 해당 라인에서 panic! 매크로 호출을 눈으로 직접 볼 수 있습니다. 그 외의 경우, 우리가 호출한 외부 코드에서 panic! 호출이 있을 수도 있습니다. 에러 메시지에 의해 보고되는 파일 이름과 라인 번호는 panic! 매크로가 호출된 다른 누군가의 코드일 것이며, 궁극적으로 panic!을 발생시킨 것이 우리 코드 라인이 아닐 것입니다. 문제를 일으킨 코드 조각을 발견하기 위해서 panic! 호출이 발생한 함수에 대한 백트레이스 (backtrace) 를 사용할 수 있습니다. 백트레이스에 대해서는 뒤에 더 자세히 다룰 것입니다.","breadcrumbs":"에러 처리 » panic!으로 복구 불가능한 에러 처리하기 » panic!에 대응하여 스택을 되감거나 그만두기","id":"153","title":"panic!에 대응하여 스택을 되감거나 그만두기"},"154":{"body":"직접 매크로를 호출하는 대신 우리 코드의 버그 때문에 라이브러리로부터 panic! 호출이 발생할 때는 어떻게 되는지 다른 예제를 통해서 살펴봅시다. 예제 9-1은 유효한 범위를 넘어서는 인덱스로 벡터에 접근을 시도하는 코드입니다. 파일명: src/main.rs fn main() { let v = vec![1, 2, 3]; v[99];\n} 예제 9-1: panic!을 일으키는 벡터의 끝을 넘어서는 요소에 대한 접근 시도 여기서는 벡터의 100번째 요소(0부터 시작하므로 99입니다)에 접근하기를 시도하고 있지만, 이 벡터는 단 3개의 요소만 가지고 있습니다. 이 경우 러스트는 패닉을 일으킬 것입니다. []의 사용은 어떤 요소의 반환을 가정하지만, 유효하지 않은 인덱스를 넘기게 되면 러스트가 반환할 올바른 요소가 없습니다. C에서 데이터 구조의 끝을 넘어서 읽는 시도는 정의되지 않은 동작입니다. 메모리가 해당 데이터 구조의 소유가 아닐지라도, 그 데이터 구조의 해당 요소에 상응하는 메모리 위치에 있는 모든 값을 가져올 수 있습니다. 이러한 것을 버퍼 초과 읽기 (buffer overread) 라 하며, 접근이 허용되어서는 안 되는 데이터를 읽기 위해 어떤 공격자가 배열 뒤에 저장된 데이터를 읽어낼 요량으로 인덱스를 다루게 된다면, 이는 보안 취약점으로 이어질 수 있습니다. 프로그램을 이러한 취약점으로부터 보호하기 위해서, 존재하지 않는 인덱스에서의 요소를 읽으려 시도한다면, 러스트는 실행을 멈추고 계속하기를 거부할 것입니다. 한번 시도해 봅시다: $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/panic`\nthread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 위 에러는 99 인덱스로 접근을 시도한 main.rs 4번째 줄을 가리키고 있습니다. 그다음 줄은 RUST_BACKTRACE 환경 변수를 설정하여 에러의 원인이 무엇인지 정확하게 백트레이스할 수 있다고 말해주고 있습니다. 백트레이스 (backtrace) 란 어떤 지점에 도달하기까지 호출한 모든 함수의 목록을 말합니다. 러스트의 백트레이스는 다른 언어들과 마찬가지로 동작합니다: 백트레이스를 읽는 요령은 위에서부터 시작하여 여러분이 작성한 파일이 보일 때까지 읽는 것입니다. 그곳이 바로 문제를 일으킨 지점입니다. 여러분의 파일이 나타난 줄보다 위에 있는 줄은 여러분의 코드가 호출한 코드이고, 아래의 코드는 여러분의 코드를 호출한 코드입니다. 이 전후의 줄에는 핵심 러스트 코드, 표준 라이브러리, 여러분이 이용하고 있는 크레이트가 포함될 수 있습니다. 한번 RUST_BACKTRACE 환경변수를 0이 아닌 값으로 설정하여 백트레이스를 얻어봅시다. 예제 9-2는 여러분이 보게 될 것과 유사한 출력을 나타냅니다. $ RUST_BACKTRACE=1 cargo run\nthread 'main' panicked at 'index out of bounds: the len is 3 but the index is 99', src/main.rs:4:5\nstack backtrace: 0: rust_begin_unwind at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/std/src/panicking.rs:584:5 1: core::panicking::panic_fmt at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:142:14 2: core::panicking::panic_bounds_check at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/panicking.rs:84:5 3: >::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:242:10 4: core::slice::index:: for [T]>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/slice/index.rs:18:9 5: as core::ops::index::Index>::index at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/alloc/src/vec/mod.rs:2591:9 6: panic::main at ./src/main.rs:4:5 7: core::ops::function::FnOnce::call_once at /rustc/e092d0b6b43f2de967af0887873151bb1c0b18d3/library/core/src/ops/function.rs:248:5\nnote: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace. 예제 9-2: 환경 변수 RUST_BACKTRACE가 설정되었을 때 panic!의 호출에 의해 발생하는 백트레이스 출력 출력이 엄청 많군요! 여러분이 보는 실제 출력값은 운영 체제 및 러스트 버전에 따라 다를 수 있습니다. 이러한 정보로 백트레이스를 얻기 위해서는 디버그 심볼이 활성화되어 있어야 합니다. 디버그 심볼은 여기서처럼 여러분이 cargo build나 cargo run을 --release 플래그 없이 실행했을 때 기본적으로 활성화됩니다. 예제 9-2 출력 내용에서는 백트레이스의 6번 라인이 문제를 일으킨 이 프로젝트 src/main.rs 의 4번 줄을 가리키고 있습니다. 프로그램이 패닉에 빠지지 않도록 하려면 직접 작성한 파일이 언급된 첫 줄부터 조사해야 합니다. 고의로 패닉을 일으키도록 코드를 작성한 예제 9-1에서 패닉을 고칠 방법은 범위를 벗어난 벡터 인덱스로 요소를 요청하지 않도록 하는 것입니다. 추후 여러분의 코드에서 패닉이 발생할 때는 어떤 코드가 패닉을 일으키는지, 코드를 어떻게 고쳐야 하는지 알아야 합니다. 다음은 에러가 발생했을 때 Result를 이용하여 복구하는 방법을 살펴보겠습니다. 언제 panic!을 써야 하는지, 혹은 쓰지 말아야 하는지에 대해서는 그다음에 나올 ‘panic!이냐, panic!이 아니냐, 그것이 문제로다’ 절에서 알아볼 예정입니다.","breadcrumbs":"에러 처리 » panic!으로 복구 불가능한 에러 처리하기 » panic! 백트레이스 이용하기","id":"154","title":"panic! 백트레이스 이용하기"},"155":{"body":"대부분 에러는 프로그램을 전부 중단해야 할 정도로 심각하진 않습니다. 때때로 어떤 함수가 실패할 경우는 쉽게 해석하고 대응할 수 있는 원인 때문입니다. 예를 들어 어떤 파일을 열려고 했는데 해당 파일이 존재하지 않아서 실패했다면, 프로세스를 종료해 버리는 대신 파일을 생성하는 것을 원할지도 모르죠. 2장의 ‘Result 타입으로 잠재적 실패 다루기’ 절에서 Result 열거형은 다음과 같이 Ok와 Err라는 두 개의 배리언트를 갖도록 정의되어 있음을 상기해 봅시다: enum Result { Ok(T), Err(E),\n} T와 E는 제네릭 타입 매개변수입니다: 제네릭은 10장에서 자세히 다루겠습니다. 지금 당장은 T는 성공한 경우에 Ok 배리언트 안에 반환될 값의 타입을 나타내고 E는 실패한 경우에 Err 배리언트 안에 반환될 에러의 타입을 나타낸다는 점만 알아둡시다. Result가 이러한 제네릭 타입 매개변수를 갖기 때문에, 반환하고자 하는 성공적인 값과 에러 값이 달라질 수 있는 다양한 상황에서 Result 타입 및 이에 정의된 함수들을 사용할 수 있습니다. 실패할 가능성이 있어서 Result 값을 반환하는 함수를 한번 호출해 봅시다. 예제 9-3은 파일을 열어보는 코드입니다. 파일명: src/main.rs use std::fs::File; fn main() { let greeting_file_result = File::open(\"hello.txt\");\n} 예제 9-3: 파일 열기 File::open의 반환 타입은 Result입니다. 제네릭 매개변수 T는 File::open의 구현부에 성공 값인 파일 핸들 std::fs::File로 채워져 있습니다. 에러 값에 사용된 E의 타입은 std::io::Error입니다. 이 반환 타입은 File::open의 호출이 성공하여 읽거나 쓸 수 있는 파일 핸들을 반환할 수도 있음을 뜻합니다. 이 함수 호출은 실패할 수도 있습니다: 예를 들면 해당 파일이 존재하지 않거나, 파일 접근을 위한 권한이 없을지도 모릅니다. File::open 함수는 함수가 성공하거나 실패할 수 있음을 알려주면서도 파일 핸들 혹은 에러 정보를 제공할 방법이 필요합니다. 이러한 정보는 정확하게 Result 열거형이 전달하는 것입니다. File::open이 성공한 경우에는 greeting_file_result 변수의 값이 파일 핸들을 가지고 있는 Ok 인스턴스가 될 것입니다. 실패한 경우 greeting_file_result는 발생한 에러의 종류에 관한 더 자세한 정보가 담긴 Err 인스턴스가 될 것입니다. 예제 9-3 코드에 File::open 반환 값에 따라 다르게 작동하는 코드를 추가해 봅시다. 예제 9-4는 6장에서 다뤘던 match 표현식을 이용하여 Result를 처리하는 한 가지 방법을 보여줍니다: 파일명: src/main.rs use std::fs::File; fn main() { let greeting_file_result = File::open(\"hello.txt\"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!(\"Problem opening the file: {:?}\", error), };\n} 예제 9-4: match 표현식을 사용하여 반환 가능한 Result 배리언트들을 처리하기 Option 열거형과 같이 Result 열거형과 배리언트들은 프렐루드로부터 가져와진다는 점을 주의하세요. 따라서 match 갈래의 Ok와 Err 앞에 Result::라고 지정하지 않아도 됩니다. 결과가 Ok일 때 이 코드는 Ok 배리언트 내부의 file 값을 반환하고, 그 후 이 파일 핸들 값을 변수 greeting_file에 대입합니다. match 후에는 이 파일 핸들을 읽거나 쓰는 데에 사용할 수 있습니다. match의 다른 갈래는 File::open으로부터 Err를 받은 경우를 처리합니다. 이 예제에서는 panic! 매크로를 호출하는 방법을 택했습니다. 디렉터리 내에 hello.txt 라는 이름의 파일이 없는 경우 이 코드를 실행하면, panic! 매크로로부터 다음과 같은 출력을 보게 될 것입니다: $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/error-handling`\nthread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: \"No such file or directory\" }', src/main.rs:8:23\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 여태 그래왔듯, 이 출력은 어떤 것이 잘못되었는지 정확하게 알려줍니다.","breadcrumbs":"에러 처리 » Result로 복구 가능한 에러 처리하기 » Result로 복구 가능한 에러 처리하기","id":"155","title":"Result로 복구 가능한 에러 처리하기"},"156":{"body":"예제 9-4의 코드는 File::open이 실패한 원인이 무엇이든 간에 panic!을 일으킵니다. 하지만 우리는 어떠한 이유로 실패했느냐에 따라 다른 조치를 취하도록 하려고 합니다: 파일이 없어서 File::open이 실패했다면 새로운 파일을 만들어서 핸들을 반환하겠습니다. 그 밖의 이유로 (예를 들어 파일을 열 권한이 없다거나 하는 이유로) 실패했다면 예제 9-4처럼 panic!을 일으키고요. match에 내용을 추가한 예제 9-5를 살펴봅시다. 파일명: src/main.rs use std::fs::File;\nuse std::io::ErrorKind; fn main() { let greeting_file_result = File::open(\"hello.txt\"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => match error.kind() { ErrorKind::NotFound => match File::create(\"hello.txt\") { Ok(fc) => fc, Err(e) => panic!(\"Problem creating the file: {:?}\", e), }, other_error => { panic!(\"Problem opening the file: {:?}\", other_error); } }, };\n} 예제 9-5: 다른 종류의 에러를 다른 방식으로 처리하기 File::open이 반환하는 Err 배리언트 값의 타입은 io::Error인데, 이는 표준 라이브러리에서 제공하는 구조체입니다. 이 구조체가 제공하는 kind 메서드를 호출하여 io::ErrorKind값을 얻을 수 있습니다. 표준 라이브러리가 제공하는 io::ErrorKind는 io 연산으로부터 발생할 수 있는 다양한 종류의 에러를 나타내는 배리언트가 있는 열거형입니다. 여기서 사용하고자 하는 배리언트는 ErrorKind::NotFound이며, 열고자 하는 파일이 아직 존재하지 않음을 나타냅니다. 따라서 greeting_file_result 매칭 안에 error.kind()에 대한 내부 매칭이 하나 더 생겼습니다. 내부 매치에서는 error.kind()가 반환한 값이 ErrorKind 열거형의 NotFound 배리언트가 맞는지 확인하고, 맞다면 File::create로 파일을 생성합니다. 하지만 File::create도 실패할 수 있으니, 내부 match 표현식의 두 번째 갈래 또한 작성해야 합니다. 파일을 생성하지 못한 경우에는 별도의 에러 메시지가 출력됩니다. 외부 match의 두 번째 갈래 또한 동일하므로, 파일을 찾을 수 없는 에러인 경우 외에는 모두 패닉이 발생합니다.","breadcrumbs":"에러 처리 » Result로 복구 가능한 에러 처리하기 » 서로 다른 에러에 대해 매칭하기","id":"156","title":"서로 다른 에러에 대해 매칭하기"},"157":{"body":"match가 정말 많군요! match 표현식은 매우 유용하지만 굉장히 원시적이기도 합니다. 13장에서는 클로저에 대해서 배워볼 텐데, Result 타입에는 클로저를 사용하는 여러 메서드가 있습니다. 이 메서드들로 Result 값들을 처리하면 match보다 더 간결하게 만들 수 있습니다. 예를 들면, 예제 9-5와 동일한 로직을 작성한 다른 방법이 아래 있는데, 이번에는 unwrap_or_else 메서드와 클로저를 사용했습니다: use std::fs::File;\nuse std::io::ErrorKind; fn main() { let greeting_file = File::open(\"hello.txt\").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create(\"hello.txt\").unwrap_or_else(|error| { panic!(\"Problem creating the file: {:?}\", error); }) } else { panic!(\"Problem opening the file: {:?}\", error); } });\n} 이 코드는 예제 9-5와 완벽하게 똑같이 작동하지만, match 표현식을 전혀 사용하지 않았으며 더 깔끔하게 읽힙니다. 13장을 읽고 이 예제로 돌아와서, 표준 라이브러리 문서에서 unwrap_or_else 메서드를 찾아보세요. 에러를 다룰 때 이런 메서드를 사용하면 거대하게 중첩된 match 표현식 덩어리를 제거할 수 있습니다.","breadcrumbs":"에러 처리 » Result로 복구 가능한 에러 처리하기 » Result와 match 사용에 대한 대안","id":"157","title":"Result와 match 사용에 대한 대안"},"158":{"body":"match의 사용은 충분히 잘 동작하지만, 살짝 장황하기도 하고 의도를 항상 잘 전달하는 것도 아닙니다. Result 타입은 다양한 특정 작업을 수행하기 위해 정의된 수많은 도우미 메서드를 가지고 있습니다. unwrap 메서드는 예제 9-4에서 작성한 match 구문과 비슷한 구현을 한 숏컷 메서드입니다. 만일 Result 값이 Ok 배리언트라면, unwrap은 Ok 내의 값을 반환할 것입니다. 만일 Result가 Err 배리언트라면 unwrap은 panic! 매크로를 호출해줄 것입니다. 아래에 unwrap이 동작하는 예가 있습니다: 파일명: src/main.rs use std::fs::File; fn main() { let greeting_file = File::open(\"hello.txt\").unwrap();\n} hello.txt 파일이 없는 상태에서 이 코드를 실행시키면, unwrap 메서드에 의해 호출된 panic!으로부터의 에러 메시지를 보게 될 것입니다: thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {\ncode: 2, kind: NotFound, message: \"No such file or directory\" }',\nsrc/main.rs:4:49 이와 비슷한 expect는 panic! 에러 메시지도 선택할 수 있도록 해 줍니다. unwrap 대신 expect를 이용하고 좋은 에러 메시지를 제공하면 여러분의 의도를 전달하면서 패닉의 근원을 추적하는 걸 쉽게 해줍니다. expect의 문법은 아래와 같이 생겼습니다: 파일명: src/main.rs use std::fs::File; fn main() { let greeting_file = File::open(\"hello.txt\") .expect(\"hello.txt should be included in this project\");\n} unwrap과 똑같이 파일 핸들을 반환하거나 panic! 매크로를 호출하도록 하는 데에 expect를 사용했습니다. unwrap은 panic!의 기본 메시지가 출력되지만, expect는 매개변수로 전달한 에러 메시지를 출력합니다. 다음과 같은 형태로 나타납니다: thread 'main' panicked at 'hello.txt should be included in this project: Os {\ncode: 2, kind: NotFound, message: \"No such file or directory\" }',\nsrc/main.rs:5:10 프로덕션급 품질의 코드에서 대부분의 러스타시안은 unwrap보다 expect를 선택하여 해당 연산이 항시 성공한다고 기대하는 이유에 대한 더 많은 맥락을 제공합니다. 이렇게 하면 가정이 틀렸다는 것이 입증될 경우 디버깅에 사용할 더 많은 정보를 확보할 수 있습니다.","breadcrumbs":"에러 처리 » Result로 복구 가능한 에러 처리하기 » 에러 발생 시 패닉을 위한 숏컷: unwrap과 expect","id":"158","title":"에러 발생 시 패닉을 위한 숏컷: unwrap과 expect"},"159":{"body":"함수의 구현체에서 실패할 수도 있는 무언가를 호출할 때, 이 함수에서 에러를 처리하는 대신 이 함수를 호출하는 코드 쪽으로 에러를 반환하여 그쪽에서 수행할 작업을 결정하도록 할 수 있습니다. 이를 에러 전파하기 (propagating) 라고 하며 호출하는 코드 쪽에 더 많은 제어권을 주는 것인데, 호출하는 코드 쪽에는 에러를 어떻게 처리해야 하는지 결정하는 정보와 로직이 여러분의 코드 컨텍스트 내에서 활용할 수 있는 것보다 더 많이 있을 수도 있기 때문입니다. 예를 들면, 예제 9-6은 파일로부터 사용자 이름을 읽는 함수를 작성한 것입니다. 만일 파일이 존재하지 않거나 읽을 수 없다면, 이 함수는 호출하는 코드 쪽으로 해당 에러를 반환할 것입니다: 파일명: src/main.rs use std::fs::File;\nuse std::io::{self, Read}; fn read_username_from_file() -> Result { let username_file_result = File::open(\"hello.txt\"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), }\n} 예제 9-6: match를 이용하여 호출 코드 쪽으로 에러를 반환하는 함수 이 함수는 더 간결하게 작성할 수 있지만, 에러 처리를 배우기 위해 과정을 하나씩 직접 작성해 보고, 간결한 버전은 마지막에 살펴보도록 하겠습니다. 함수의 반환 타입인 Result부터 먼저 살펴봅시다. 함수가 Result 타입의 값을 반환하는데 제네릭 매개변수 T는 구체 타입 (concrete type) 인 String으로 채워져 있고, 제네릭 타입 E는 구체 타입인 io::Error로 채워져 있다는 뜻입니다. 만일 이 함수가 문제없이 성공하면, 함수를 호출한 코드는 String(이 함수가 파일로부터 읽어 들인 사용자 이름이겠지요)을 담은 Ok 값을 받을 것입니다. 만일 어떤 문제가 발생한다면, 이 함수를 호출한 코드는 문제가 뭐였는지에 대한 더 많은 정보를 담고 있는 io::Error의 인스턴스를 담은 Err 값을 받을 것입니다. 이 함수의 반환 타입으로 io::Error를 선택했는데, 그 이유는 이 함수 내부에서 호출하는 실패할 수 있는 연산 File::open 함수와 read_to_string 메서드 두 가지가 모두 io::Error 타입의 에러 값을 반환하기 때문입니다. 함수의 본문은 File::open 함수를 호출하면서 시작합니다. 그다음에는 예제 9-4에서 본 match와 유사하게 match를 이용하여 Result 값을 처리합니다. 만약 File::open이 성공하면 패턴 변수 file의 파일 핸들은 가변 변수 username_file의 값이 되고 함수는 계속됩니다. Err의 경우에는 panic!을 호출하는 대신 return 키워드를 사용하여 함수 전체를 일찍 끝내고 호출한 코드 쪽에 File::open으로부터 얻은 (지금의 경우 패턴 변수 e에 있는) 에러 값을 이 함수의 에러 값처럼 넘깁니다. 그래서 username_file에 파일 핸들을 얻게 되면, 함수는 username 변수에 새로운 String을 생성하고 username_file의 파일 핸들에 대해 read_to_string 메서드를 호출하여 파일의 내용물을 username으로 읽어 들입니다. File::open이 성공하더라도 read_to_string 메서드가 실패할 수도 있으므로 Result를 반환합니다. 따라서 이 Result를 처리하기 위한 또 다른 match가 필요합니다: read_to_string이 성공하면, 이 함수는 성공한 것이고, 이제는 username에 있는 파일로부터 읽은 사용자 이름을 Ok로 감싸서 반환합니다. read_to_string이 실패하면, File::open의 반환 값을 처리했던 match에서의 에러 값 반환과 똑같은 방식으로 에러 값을 반환합니다. 하지만 이 함수의 마지막 표현식이기 때문에 명시적으로 return이라고 적을 필요는 없습니다. 그러면 이 코드를 호출하는 코드는 사용자 이름이 있는 Ok 값 혹은 io::Error를 담은 Err 값을 처리하게 될 것입니다. 이 값을 가지고 어떤 일을 할지에 대한 결정은 호출하는 코드 쪽에 달려 있습니다. 만일 그쪽에서 Err 값을 얻었다면, 이를테면 panic!을 호출하여 프로그램을 종료시키는 선택을 할 수도 있고, 기본 사용자 이름을 사용할 수도 있으며, 혹은 파일이 아닌 다른 어딘가에서 사용자 이름을 찾을 수도 있습니다. 호출하는 코드가 정확히 어떤 것을 시도하려 하는지에 대한 충분한 정보가 없기 때문에, 모든 성공 혹은 에러 정보를 위로 전파하여 호출하는 코드가 적절하게 처리하도록 합니다. 러스트에서는 에러를 전파하는 패턴이 너무 흔하여 이를 더 쉽게 해주는 물음표 연산자 ?를 제공합니다.","breadcrumbs":"에러 처리 » Result로 복구 가능한 에러 처리하기 » 에러 전파하기","id":"159","title":"에러 전파하기"},"16":{"body":"Windows 사용자는 https://www.rust-lang.org/tools/install 에서 안내를 따라주시기 바랍니다. 설치 과정에서 Visual Studio 2013 버전 이상의 MSVC 빌드 도구가 필요하다는 메시지가 나타날 것입니다. 빌드 도구를 설치하려면 Visual Studio 2022 를 설치할 필요가 있습니다. 구체적으로는 아래와 같은 패키지가 필요합니다: ‘C++ 데스크톱 개발’ Windows 10 혹은 11 SDK 영어 언어팩과 여러분이 선택하고 싶은 다른 언어팩 이후부터는 cmd.exe 와 PowerShell에서 혼용되는 명령어만 사용할 예정이며, 서로 다른 부분이 있을 경우엔 따로 명시하겠습니다.","breadcrumbs":"시작해봅시다 » 러스트 설치 » rustup 설치 - Windows","id":"16","title":"rustup 설치 - Windows"},"160":{"body":"예제 9-7은 예제 9-6과 같은 기능을 가진 read_username_from_file의 구현체인데, 이번 구현에서는 ? 연산자를 이용합니다: 파일명: src/main.rs use std::fs::File;\nuse std::io::{self, Read}; fn read_username_from_file() -> Result { let mut username_file = File::open(\"hello.txt\")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username)\n} 예제 9-7: ? 연산자를 이용하여 에러를 호출 코드 쪽으로 반환하는 함수 Result 값 뒤의 ?는 예제 9-6에서 Result 값을 다루기 위해 정의했던 match 표현식과 거의 같은 방식으로 동작하게끔 정의되어 있습니다. 만일 Result의 값이 Ok라면, Ok 안의 값이 얻어지고 프로그램이 계속됩니다. 만일 값이 Err라면, return 키워드로 에러 값을 호출하는 코드에게 전파하는 것처럼 Err의 값이 반환될 것입니다. 예제 9-6의 match 표현식과 ? 연산자의 차이점은, ? 연산자를 사용할 때의 에러 값들은 from 함수를 거친다는 것입니다. from 함수는 표준 라이브러리 내의 From 트레이트에 정의되어 있으며 어떤 값의 타입을 다른 타입으로 변환하는 데에 사용합니다. ? 연산자가 from 함수를 호출하면, ? 연산자가 얻게 되는 에러를 ? 연산자가 사용된 현재 함수의 반환 타입에 정의된 에러 타입으로 변환합니다. 이는 어떤 함수가 다양한 종류의 에러로 인해 실패할 수 있지만, 모든 에러를 하나의 에러 타입으로 반환할 때 유용합니다. 예를 들면 예제 9-7의 read_username_from_file 함수가 직접 정의한 OurError라는 이름의 커스텀 에러 타입을 반환하도록 고칠 수 있겠습니다. impl From for OurError도 정의하여 io::Error로부터 OurError를 생성하도록 한다면, read_username_from_file 본문에 있는 ? 연산자 호출은 다른 코드를 추가할 필요 없이 from을 호출하여 에러 타입을 변환할 것입니다. 예제 9-7의 컨텍스트에서, File::open 호출 부분의 끝에 있는 ?는 Ok의 값을 변수 username_file에게 반환해 줄 것입니다. 만일 에러가 발생하면 ?는 함수로부터 일찍 빠져나와 호출하는 코드에게 어떤 Err 값을 줄 것입니다. read_to_string 호출의 끝부분에 있는 ?도 같은 방식이 적용됩니다. ?는 많은 양의 보일러 플레이트를 제거해 주고 함수의 구현을 더 단순하게 만들어 줍니다. 심지어는 예제 9-8과 같이 ? 뒤에 바로 메서드 호출을 연결하는 식으로 이 코드를 더 줄일 수도 있습니다: 파일명: src/main.rs use std::fs::File;\nuse std::io::{self, Read}; fn read_username_from_file() -> Result { let mut username = String::new(); File::open(\"hello.txt\")?.read_to_string(&mut username)?; Ok(username)\n} 예제 9-8: ? 연산자 뒤에 메서드 호출을 연결하기 새로운 String을 만들어 username에 넣는 부분을 함수의 시작 부분으로 옮겼습니다. 이 부분은 달라진 것이 없습니다. username_file 변수를 만드는 대신, File::open(\"hello.txt\")?의 결과 바로 뒤에 read_to_string의 호출을 연결했습니다. read_to_string 호출의 끝에는 여전히 ?가 남아있고, File::open과 read_to_string이 모두 에러를 반환하지 않고 성공했을 때 username 안의 사용자 이름을 담은 Ok를 반환하는 것도 여전합니다. 함수의 기능 또한 예제 9-6과 예제 9-7의 것과 동일하고, 다만 더 인체공학적인 작성 방법이라는 차이만 있을 뿐입니다. 예제 9-9에는 fs::read_to_string을 사용하여 더 짧게 만든 예시가 있습니다. 파일명: src/main.rs use std::fs;\nuse std::io; fn read_username_from_file() -> Result { fs::read_to_string(\"hello.txt\")\n} 예제 9-9: 파일을 열고, 읽는 대신 fs::read_to_string을 사용하기 파일에서 문자열을 읽는 코드는 굉장히 흔하게 사용되기 때문에, 표준 라이브러리에서는 파일을 열고, 새 String을 생성하고, 파일 내용을 읽고, 내용을 String에 집어넣고 반환하는 fs::read_to_string라는 편리한 함수를 제공합니다. 다만 fs::read_to_string을 사용해 버리면 에러를 다루는 법을 자세히 설명할 수 없으니 긴 코드로 먼저 설명했습니다.","breadcrumbs":"에러 처리 » Result로 복구 가능한 에러 처리하기 » 에러를 전파하기 위한 숏컷: ?","id":"160","title":"에러를 전파하기 위한 숏컷: ?"},"161":{"body":"?는 ?이 사용된 값과 호환 가능한 반환 타입을 가진 함수에서만 사용될 수 있습니다. 이는 ? 연산자가 예제 9-6에서 정의한 match 표현식과 동일한 방식으로 함수를 일찍 끝내면서 값을 반환하는 동작을 수행하도록 정의되어 있기 때문입니다. 예제 9-6에서 match는 Result 값을 사용하고 있었고, 빠른 반환 갈래는 Err(e) 값을 반환했습니다. 이 함수의 반환 타입이 Result여야 이 return과 호환 가능합니다. 만일 ?가 사용된 값의 타입과 호환되지 않는 반환 타입을 가진 main 함수에서 ? 연산자를 사용하면 어떤 에러를 얻게 되는지 예제 9-10에서 살펴보도록 합시다: 파일명: src/main.rs use std::fs::File; fn main() { let greeting_file = File::open(\"hello.txt\")?;\n} 예제 9-10: ()를 반환하는 main에서의 ? 사용 시도는 컴파일되지 않습니다 이 코드는 파일을 열고, 이는 실패할 수도 있습니다. ? 연산자는 File::open에 의해 반환되는 Result 값을 따르지만, main 함수는 반환 타입이 Result가 아니라 ()입니다. 이 코드를 컴파일하면 다음과 같은 에러 메시지를 얻게 됩니다: $ cargo run Compiling error-handling v0.1.0 (file:///projects/error-handling)\nerror[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`) --> src/main.rs:4:48 |\n3 | fn main() { | --------- this function should return `Result` or `Option` to accept `?`\n4 | let greeting_file = File::open(\"hello.txt\")?; | ^ cannot use the `?` operator in a function that returns `()` | = help: the trait `FromResidual>` is not implemented for `()` For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `error-handling` due to previous error 이 에러는 ? 연산자가 Result, Option 혹은 FromResidual을 구현한 타입을 반환하는 함수에서만 사용될 수 있음을 지적하고 있습니다. 이 에러를 고치기 위해서는 두 가지 선택지가 있습니다. 첫 번째는 ? 연산자가 사용되는 곳의 값과 호환되게 함수의 반환 타입을 수정하는 것인데, 이러한 수정을 막는 제약 사항이 없는 한에서 가능합니다. 다른 방법은 Result를 적절한 식으로 처리하기 위해 match 혹은 Result의 메서드 중 하나를 사용하는 것입니다. 에러 메시지는 또한 ?가 Option 값에 대해서도 사용될 수 있음을 알려주었습니다. Result에 ?를 사용할 때와 마찬가지로, 함수가 Option를 반환하는 경우에는 Option에서만 ?를 사용할 수 있습니다. Option에서 호출되었을 때의 ? 연산자 동작은 Result에서 호출되었을 때의 동작과 비슷합니다: None 값인 경우 그 함수의 해당 지점으로부터 None 값을 일찍 반환할 것입니다. Some 값이라면 Some 안에 있는 값이 이 표현식의 결괏값이 되면서 함수가 계속됩니다. 예제 9-11은 주어진 텍스트에서 첫 번째 줄의 마지막 문자를 찾는 함수의 예제를 보여줍니다: fn last_char_of_first_line(text: &str) -> Option { text.lines().next()?.chars().last()\n}\n# # fn main() {\n# assert_eq!(\n# last_char_of_first_line(\"Hello, world\\nHow are you today?\"),\n# Some('d')\n# );\n# # assert_eq!(last_char_of_first_line(\"\"), None);\n# assert_eq!(last_char_of_first_line(\"\\nhi\"), None);\n# } 예제 9-11: Option 값에 대한 ? 연산자의 사용 이 함수는 문자가 있을 수도, 없을 수도 있기 때문에 Option를 반환합니다. 이 코드는 text 문자열 슬라이스 인수를 가져와서 lines 메서드를 호출하는데, 이는 해당 문자열의 라인에 대한 반복자를 반환합니다. 첫 번째 줄의 검사가 필요하므로, 반복자의 next를 호출하여 첫 번째 값을 얻어옵니다. 만일 text가 빈 문자열이라면 next 호출은 None을 반환하는데, 여기서 ?를 사용하여 last_char_of_first_line의 실행을 멈추고 None을 반환합니다. 만약 text가 빈 문자열이 아니라면 next는 text의 첫 번째 줄의 문자열 슬라이스를 담고 있는 Some의 값을 반환합니다. ?이 문자열 슬라이스를 추출하고, 이 문자열 슬라이스의 chars를 호출하여 문자들에 대한 반복자를 얻어올 수 있습니다. 이 첫 번째 라인의 마지막 문자에 관심이 있으므로, last를 호출하여 이 반복자의 마지막 아이템을 얻어옵니다. \"\\nhi\"처럼 빈 줄로 시작하지만 다른 줄에는 문자가 담겨있는 경우처럼, 첫 번째 라인이 빈 문자열일 수 있으므로 반복자의 결과는 Option입니다. 만약 첫 번째 라인에 마지막 문자가 있다면 Some 배리언트를 반환할 것입니다. 가운데의 ? 연산자가 이러한 로직을 표현할 간단한 방식을 제공하여 이 함수를 한 줄로 작성할 수 있도록 해 줍니다. 만일 Option에 대하여 ? 연산자를 이용할 수 없었다면 더 많은 메서드 호출 혹은 match 표현식을 사용하여 이 로직을 구현했어야 할 것입니다. Result를 반환하는 함수에서는 Result에서 ? 연산자를 사용할 수 있고, Option을 반환하는 함수에서는 Option에 대해 ? 연산자를 사용할 수 있지만, 이를 섞어서 사용할 수는 없음을 주목하세요. ? 연산자는 자동으로 Result를 Option으로 변환하거나 혹은 그 반대를 할 수 없습니다; 그러한 경우에는 Result의 ok 메서드 혹은 Option의 ok_or 메서드 같은 것을 통해 명시적으로 변환을 할 수 있습니다. 여태껏 다뤄본 main 함수는 모두 ()를 반환했습니다. main 함수는 실행 프로그램의 시작점이자 종료점이기 때문에 특별하며, 프로그램이 기대한 대로 동작려면 반환 타입의 종류에 대한 제약사항이 있습니다. 다행히도 main은 Result<(), E>도 반환할 수 있습니다. 예제 9-12는 예제 9-10의 코드에서 main의 반환 타입을 Result<(), Box>로 변경하고 함수 마지막에 반환 값 Ok(())를 추가한 것입니다. 이 코드는 이제 컴파일될 것입니다: use std::error::Error;\nuse std::fs::File; fn main() -> Result<(), Box> { let greeting_file = File::open(\"hello.txt\")?; Ok(())\n} 예제 9-12: main이 Result<(), E>를 반환하도록 하여 Result 값에 대한 ? 사용 가능하게 하기 Box 타입은 트레이트 객체 (trait object) 인데, 17장의 ‘트레이트 객체를 사용하여 다른 타입의 값 허용하기’ 절에서 다룰 예정입니다. 지금은 Box가 ‘어떠한 종류의 에러’를 의미한다고 읽으면 됩니다. 반환할 에러 타입이 Box인 Result이면 어떠한 Err 값을 일찍 반환할 수 있으므로 main에서의 ? 사용이 허용됩니다. main 함수의 구현 내용이 std::io::Error 타입의 에러만 반환하겠지만, 이 함수 시그니처에 Box라고 명시하면 이후 main의 구현체에 다른 에러들을 반환하는 코드가 추가되더라도 계속 올바르게 작동할 것입니다. main 함수가 Result<(), E>를 반환하게 되면, 실행 파일은 main이 Ok(())를 반환할 경우 0 값으로 종료되고, main이 Err 값을 반환할 경우 0이 아닌 값으로 종료됩니다. C로 작성된 실행파일은 종료될 때 정숫값을 반환합니다: 성공적으로 종료된 프로그램은 정수 0을 반환하고, 에러가 발생한 프로그램은 0이 아닌 어떤 정숫값을 반환합니다. 러스트 또한 이러한 규칙과 호환될 목적으로 실행파일이 정숫값을 반환합니다. main 함수가 std::process::Termination 트레이트 를 구현한 타입을 반환할 수도 있는데, 이는 ExitCode를 반환하는 report라는 함수를 가지고 있습니다. 여러분이 만든 타입에 대해 Termination 트레이트를 구현하려면 표준 라이브러리 문서에서 더 많은 정보를 찾아보세요. panic! 호출하기와 Result 반환하기에 대한 세부 내용을 논의했으니, 어떤 경우에 어떤 방법을 사용하는 것이 적절한지 결정하는 방법에 대한 주제로 돌아갑시다.","breadcrumbs":"에러 처리 » Result로 복구 가능한 에러 처리하기 » ? 연산자가 사용될 수 있는 곳","id":"161","title":"? 연산자가 사용될 수 있는 곳"},"162":{"body":"그러면 언제 panic!을 써야 하고 언제 Result를 반환할지는 어떻게 결정해야 할까요? 코드가 패닉을 일으킬 때는 복구할 방법이 없습니다. 복구 가능한 방법이 있든 없든 간에 에러 상황에 대해 panic!을 호출할 수 있지만, 그렇게 되면 호출하는 코드를 대신하여 현 상황은 복구 불가능한 것이라고 결정을 내리는 꼴이 됩니다. Result 값을 반환하는 선택을 한다면 호출하는 쪽에게 옵션을 제공하는 것입니다. 호출하는 코드 쪽에서는 상황에 적합한 방식으로 복구를 시도할 수도 있고, 혹은 현재 상황의 Err은 복구 불가능하다고 결론을 내리고 panic!을 호출하여 복구 가능한 에러를 복구 불가능한 것으로 바꿔놓을 수도 있습니다. 그러므로 실패할지도 모르는 함수를 정의할 때는 기본적으로 Result를 반환하는 것이 좋은 선택입니다. 예제, 프로토타입, 테스트 같은 상황에서는 Result를 반환하는 대신 패닉을 일으키는 코드가 더 적절합니다. 왜 그런지 탐구해 보고, 사람으로서의 여러분이라면 실패할 리 없는 코드라는 것을 알 수 있지만, 컴파일러는 이유를 파악할 수 없는 경우에 대해서도 논의해 봅시다. 그리고 라이브러리 코드에 패닉을 추가해야 할지 말지를 어떻게 결정할까에 대한 일반적인 가이드라인을 내림으로써 결론짓겠습니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » panic!이냐, panic!이 아니냐, 그것이 문제로다","id":"162","title":"panic!이냐, panic!이 아니냐, 그것이 문제로다"},"163":{"body":"어떤 개념을 묘사하기 위한 예제를 작성 중이라면, 견고한 에러 처리 코드를 포함시키는 것이 오히려 예제의 명확성을 떨어트릴 수도 있습니다. 예제 코드 내에서는 panic!을 일으킬 수 있는 unwrap 같은 메서드의 호출이 애플리케이션의 에러 처리가 필요한 곳을 뜻하는 방식으로 해석될 수 있는데, 이러한 에러 처리는 코드의 나머지 부분이 하는 일에 따라 달라질 수 있습니다. 비슷한 상황으로 에러를 어떻게 처리할지 결정할 준비가 되기 전이라면, unwrap과 expect 메서드가 프로토타이핑할 때 매우 편리합니다. 이 함수들은 코드를 더 견고하게 만들 준비가 되었을 때를 위해서 명확한 표시를 남겨 둡니다. 만일 테스트 내에서 메서드 호출이 실패한다면, 해당 메서드가 테스트 중인 기능이 아니더라도 전체 테스트를 실패시키도록 합니다. panic!이 테스트의 실패를 표시하는 방식이므로, unwrap이나 expect의 호출이 정확히 그렇게 만들어줍니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 예제, 프로토타입 코드, 그리고 테스트","id":"163","title":"예제, 프로토타입 코드, 그리고 테스트"},"164":{"body":"Result가 Ok 값을 가지고 있을 거라 확신할만한 논리적 근거가 있지만, 컴파일러가 그 논리를 이해할 수 없는 경우라면, unwrap 혹은 expect를 호출하는 것이 적절할 수 있습니다. 어떤 연산이든 간에 특정한 상황에서는 논리적으로 불가능할지라도 기본적으로는 실패할 가능성을 가지고 있는 코드를 호출하는 것이므로, 처리가 필요한 Result 값이 나오게 됩니다. 손수 코드를 조사하여 Err 배리언트가 나올리 없음을 확신할 수 있다면 unwrap을 호출해도 아무런 문제가 없으며, expect의 문구에 Err 배리언트가 있으면 안 될 이유를 적어주는 것이 더 좋을 것입니다. 아래에 예제가 있습니다: # fn main() { use std::net::IpAddr; let home: IpAddr = \"127.0.0.1\" .parse() .expect(\"Hardcoded IP address should be valid\");\n# } 여기서는 하드코딩된 문자열을 파싱하여 IpAddr 인스턴스를 만드는 중입니다. 127.0.0.1이 유효한 IP 주소라는 사실을 알 수 있으므로, 여기서는 expect의 사용이 허용됩니다. 하지만 하드코딩된 유효한 문자열이라는 사실이 parse 메서드의 반환 타입을 변경해 주지는 않습니다: 여전히 Result 값이 나오고, 컴파일러는 마치 Err 배리언트가 나올 가능성이 여전히 있는 것처럼 Result를 처리하도록 요청할 것인데, 그 이유는 이 문자열이 항상 유효한 IP 주소라는 사실을 알 수 있을 만큼 컴파일러가 똑똑하지 않기 때문입니다. 만일 IP 주소 문자열이 프로그램에 하드코딩된 것이 아니라 사용자로부터 입력되었다면, 그래서 실패할 가능성이 생겼다면 , 더 견고한 방식으로 Result를 처리할 필요가 분명히 있습니다. expect에 이 IP 주소가 하드코딩 되었다는 가정을 언급하는 것은 향후에 IP 주소가 다른 곳으로부터 가져올 필요가 생길 경우 expect를 더 나은 에러 처리 코드로 수정하도록 재촉할 것입니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 여러분이 컴파일러보다 더 많은 정보를 가지고 있을 때","id":"164","title":"여러분이 컴파일러보다 더 많은 정보를 가지고 있을 때"},"165":{"body":"코드가 결국 나쁜 상태에 처하게 될 가능성이 있을 때는 코드에 panic!을 넣는 것이 바람직합니다. 이 글에서 말하는 나쁜 상태 란 어떤 가정, 보장, 계약, 혹은 불변성이 깨질 때를 뜻하는 것으로, 이를테면 유효하지 않은 값이나 모순되는 값, 혹은 찾을 수 없는 값이 코드에 전달되는 경우를 말합니다 - 아래에 쓰인 상황 중 하나 혹은 그 이상일 경우라면 말이죠: 이 나쁜 상태란 것은 예기치 못한 무언가이며, 이는 사용자가 입력한 데이터가 잘못된 형식이라던가 하는 흔히 발생할 수 있는 것과는 반대되는 것입니다. 그 시점 이후의 코드는 매번 해당 문제에 대한 검사를 하는 것이 아니라, 이 나쁜 상태에 있지 않아야만 할 필요가 있습니다. 여러분이 사용하고 있는 타입 내에 이 정보를 집어넣을만한 뾰족한 수가 없습니다. 이러한 것의 의미에 대한 예제를 17장의 ‘상태와 동작을 타입으로 인코딩하기’ 절에서 살펴볼 것입니다. 만일 어떤 사람이 여러분의 코드를 호출하고 타당하지 않은 값을 집어넣었다면, 가능한 에러를 반환하여 라이브러리의 사용자들이 이러한 경우에 대해 어떤 동작을 원하는지 결정할 수 있도록 하는 것이 가장 좋습니다. 그러나 계속 실행하는 것이 보안상 좋지 않거나 해를 끼치는 경우라면 panic!을 써서 여러분의 라이브러리를 사용하고 있는 사람에게 자신의 코드에 있는 버그를 알려줘서 개발 중에 이를 고칠 수 있게끔 하는 것이 최선책일 수도 있습니다. 비슷한 식으로, 여러분의 제어권에서 벗어난 외부 코드를 호출하고 있고, 이것이 고칠 방법이 없는 유효하지 않은 상태를 반환한다면, panic!이 종종 적절합니다. 하지만 실패가 충분히 예상되는 경우라면 panic!을 호출하는 것보다 Result를 반환하는 것이 여전히 더 적절합니다. 이에 대한 예는 잘못된 데이터가 제공된 파서나, 속도 제한에 도달했음을 나타내는 상태를 반환하는 HTTP 요청 등이 있습니다. 이러한 경우, Result를 반환하면 호출자가 처리 방법을 결정해야 하는 실패 가능성이 예상된다는 것을 나타냅니다. 코드가 유효하지 않은 값에 대해 호출되면 사용자를 위험에 빠뜨릴 수 있는 연산을 수행할 때, 그 코드는 해당 값이 유효한지를 먼저 검사하고, 만일 그렇지 않다면 panic!을 호출해야 합니다. 이는 주로 보안상의 이유입니다: 유효하지 않은 데이터에 어떤 연산을 시도하는 것은 코드를 취약점에 노출시킬 수 있습니다. 이것이 범위를 벗어난 메모리 접근을 시도했을 경우 표준 라이브러리가 panic!을 호출하는 주된 이유입니다: 현재 사용하는 데이터 구조가 소유하지 않은 메모리에 접근 시도하는 것은 흔한 보안 문제입니다. 종종 함수에는 입력이 특정 요구사항을 만족시킬 경우에만 함수의 행동이 보장되는 계약 이 있습니다. 이 계약을 위반했을 때는 패닉을 발생시키는 것이 이치에 맞는데, 그 이유는 계약 위반이 항상 호출자 쪽의 버그임을 나타내고, 이는 호출하는 코드가 명시적으로 처리해야 하는 종류의 버그가 아니기 때문입니다. 사실 호출하는 쪽의 코드가 복구시킬 합리적인 방법은 존재하지 않고, 호출하는 프로그래머 가 그 코드를 고칠 필요가 있습니다. 함수에 대한 계약은, 특히 계약 위반이 패닉의 원인이 될 때는, 그 함수에 대한 API 문서에 설명되어야 합니다. 하지만 모든 함수 내에서 수많은 에러 검사를 한다는 것은 장황하고 짜증나는 일일 것입니다. 다행히도 러스트의 타입 시스템이 (그리고 컴파일러에 의한 타입 검사 기능이) 여러분을 위해 수많은 검사를 해줄 수 있습니다. 함수에 특정한 타입의 매개변수가 있는 경우 컴파일러가 이미 유효한 값을 확인했으므로 코드 로직을 계속 진행할 수 있습니다. 예를 들면, 만약 Option이 아닌 어떤 타입을 갖고 있다면, 여러분의 프로그램은 아무것도 아닌 것 이 아닌 무언가 를 갖고 있음을 예측합니다. 그러면 코드는 Some과 None 배리언트에 대한 두 경우를 처리하지 않아도 됩니다: 분명히 값을 가지고 있는 하나의 경우만 있을 것입니다. 함수에 아무것도 넘기지 않는 시도를 하는 코드는 컴파일조차 되지 않을 것이므로, 그 함수에서는 그런 경우에 대한 런타임 검사가 필요 없습니다. 또 다른 예로는 u32와 같은 부호 없는 정수의 사용이 있는데, 이는 매개변수가 절대 음수가 아님을 보장합니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 에러 처리를 위한 가이드라인","id":"165","title":"에러 처리를 위한 가이드라인"},"166":{"body":"러스트의 타입 시스템을 사용해 유효한 값을 보장하는 아이디어에서 한 발 더 나가서, 유효성 검사를 위한 커스텀 타입을 생성하는 방법을 살펴봅시다. 2장의 추리 게임을 상기해 보시면, 사용자에게 1부터 100 사이의 숫자를 추측하도록 요청했었죠. 사용자의 추릿값을 비밀 번호와 비교하기 전에 추릿값이 양수인지만 확인했을 뿐, 해당 값이 유효한지는 확인하지 않았습니다. 이 경우에는 결과가 그렇게 끔찍하지는 않았습니다: ‘Too high’나 ‘Too low’라고 표시했던 출력이 여전히 정확했기 때문입니다. 하지만 사용자가 올바른 추측을 할 수 있도록 안내하고, 사용자가 범위를 벗어난 숫자를 입력했을 때와 사용자가 숫자가 아닌 문자 등을 입력했을 때 다른 동작을 하는 건 꽤 괜찮은 개선일 겁니다. 이를 위한 한 가지 방법은 u32 대신 i32로 추릿값을 파싱하여 음수가 입력될 가능성을 허용하고, 그리고서 숫자가 범위 내에 있는지에 대한 검사를 아래와 같이 추가하는 것입니다: # use rand::Rng;\n# use std::cmp::Ordering;\n# use std::io;\n# # fn main() {\n# println!(\"Guess the number!\");\n# # let secret_number = rand::thread_rng().gen_range(1..=100);\n# loop { // --생략-- # println!(\"Please input your guess.\");\n# # let mut guess = String::new();\n# # io::stdin()\n# .read_line(&mut guess)\n# .expect(\"Failed to read line\");\n# let guess: i32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, }; if guess < 1 || guess > 100 { println!(\"The secret number will be between 1 and 100.\"); continue; } match guess.cmp(&secret_number) { // --생략--\n# Ordering::Less => println!(\"Too small!\"),\n# Ordering::Greater => println!(\"Too big!\"),\n# Ordering::Equal => {\n# println!(\"You win!\");\n# break;\n# }\n# } }\n# } if 표현식은 값이 범위 밖에 있는지 혹은 그렇지 않은지 검사하고, 사용자에게 문제점을 말해주고, continue를 호출하여 루프의 다음 반복을 시작하고 다른 추릿값을 요청해 줍니다. if 표현식 이후에는 guess가 1과 100 사이의 값임을 확인한 상태에서 guess와 비밀 숫자의 비교를 진행할 수 있습니다. 하지만 이는 이상적인 해결책이 아닙니다. 만약 프로그램이 오직 1과 100 사이의 값에서만 동작한다는 점이 굉장히 중요한 사항이고 많은 함수가 동일한 요구사항을 가지고 있다면, 모든 함수 내에서 이런 검사를 하는 것은 지루한 일일 겁니다. (게다가 성능에 영향을 줄지도 모릅니다.) 그대신 새로운 타입을 만들어서 그 타입의 인스턴스를 생성하는 함수에서 유효성을 확인하는 방식으로 유효성 확인을 모든 곳에서 반복하지 않게 할 수 있습니다. 이렇게 하면 함수가 새로운 타입을 시그니처에 사용하여 받은 값을 자신있게 사용할 수 있어 안전합니다. 예제 9-13은 new 함수가 1과 100 사이의 값을 받았을 때만 인스턴스를 생성하는 Guess 타입을 정의하는 한 가지 방법을 보여줍니다: pub struct Guess { value: i32,\n} impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!(\"Guess value must be between 1 and 100, got {}.\", value); } Guess { value } } pub fn value(&self) -> i32 { self.value }\n} 예제 9-13: 1과 100 사이의 값일 때만 실행을 계속하는 Guess 타입 먼저 i32를 갖는 value라는 이름의 필드를 가진 Guess라는 이름의 구조체를 선언하였습니다. 이것이 숫자가 저장될 곳입니다. 그다음 Guess 값의 인스턴스를 생성하는 new라는 이름의 연관 함수를 구현하였습니다. new 함수는 i32 타입의 값인 value를 매개변수를 받아서 Guess를 반환하도록 정의되었습니다. new 함수의 본문에 있는 코드는 value가 1부터 100 사이의 값인지 확인하는 테스트를 합니다. 만일 value가 이 테스트에 통과하지 못하면 panic!을 호출하며, 이는 이 코드를 호출하는 프로그래머에게 고쳐야 할 버그가 있음을 알려주는데, 범위 밖의 value로 Guess를 생성하는 것은 Guess::new가 요구하는 계약을 위반하기 때문입니다. Guess::new가 패닉을 일으킬 수 있는 조건은 공개 API 문서에서 다뤄져야 합니다. 여러분이 만드는 API 문서에서 panic!의 가능성을 가리키는 것에 대한 문서 관례는 14장에서 다룰 것입니다. 만일 value가 테스트를 통과한다면, value 매개변수로 value 필드를 설정한 새로운 Guess를 생성하여 이를 반환합니다. 다음으로, self를 빌리고, 매개변수를 갖지 않으며, i32를 반환하는 value라는 이름의 메서드를 구현했습니다. 이러한 종류의 메서드를 종종 게터 (getter) 라고 부르는데, 그 이유는 이런 함수의 목적이 객체의 필드로부터 어떤 데이터를 가져와서 반환하는 것이기 때문입니다. 이 공개 메서드가 필요한 이유는 Guess 구조체의 value 필드가 비공개이기 때문입니다. value 필드를 비공개이기 때문에 Guess 구조체를 사용하는 코드는 value를 직접 설정할 수 없다는 것은 중요합니다. 모듈 밖의 코드는 반드시 Guess::new 함수로 새로운 Guess의 인스턴스를 생성해야 하며, 이를 통해 Guess가 Guess::new 함수의 조건에 의해 확인되지 않은 value를 가질 수 없음을 보장합니다. 이제 1에서 100 사이의 숫자를 매개변수로 쓰거나 반환하는 함수에서 i32 대신 Guess를 사용하면 함수의 본문에서 추가로 확인할 필요가 없습니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 유효성을 위한 커스텀 타입 생성하기","id":"166","title":"유효성을 위한 커스텀 타입 생성하기"},"167":{"body":"러스트의 에러 처리 기능은 여러분이 더 견고한 코드를 작성하는 데 도움을 주도록 설계되었습니다. panic! 매크로는 프로그램이 처리 불가능한 상태에 놓여 있음에 대한 신호를 주고 유효하지 않거나 잘못된 값으로 계속 진행을 시도하는 대신 실행을 멈추게끔 해줍니다. Result 열거형은 러스트의 타입 시스템을 이용하여 복구할 수 있는 방법으로 코드의 연산이 실패할 수도 있음을 알려줍니다. 또한 Result를 이용하면 여러분의 코드를 호출하는 코드에게 잠재적인 성공이나 실패를 처리해야 할 필요가 있음을 알려줄 수 있습니다. panic!과 Result를 적절한 상황에서 사용하는 것은 여러분의 코드가 불가피한 문제에 직면했을 때도 더 신뢰할 수 있도록 해줄 것입니다. 이제 Option과 Result 열거형을 가지고 표준 라이브러리의 유용한 제네릭 사용 방식들을 보았으니, 제네릭이 어떤 식으로 동작하고 여러분의 코드에 어떻게 이용할 수 있는지에 대해 이야기해 보겠습니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 정리","id":"167","title":"정리"},"168":{"body":"모든 프로그래밍 언어는 중복되는 개념을 효율적으로 처리하기 위한 도구를 가지고 있습니다. 러스트에서는 제네릭 (generic) 이 그 역할을 맡습니다: 제네릭은 구체 (concrete) 타입 혹은 기타 속성에 대한 추상화된 대역입니다. 컴파일과 실행 시점에 제네릭들이 실제로 무슨 타입으로 채워지는지 알 필요 없이 제네릭의 동작이나 다른 제네릭과의 관계를 표현할 수 있습니다. 함수가 어떤 값이 들어있을지 모르는 매개변수를 전달받아서 동일한 코드를 다양한 구체적 값으로 실행되는 것처럼, 함수는 i32, String 같은 구체 타입 대신 제네릭 타입의 매개변수를 전달받을 수 있습니다. 사실은 이미 여러 제네릭을 사용해 봤었습니다. 6장에서는 Option, 8장에서는 Vec와 HashMap, 9장에서는 Result 제네릭을 사용했죠. 이번 장에서는 제네릭을 사용해 자체 타입, 함수, 메서드를 정의하는 방법을 살펴보겠습니다. 우선, 함수를 추출하여 중복되는 코드를 제거하는 방법을 살펴볼 겁니다. 그다음 매개변수의 타입만 다른 두 함수가 생기면 제네릭 함수를 사용해 코드 중복을 한 번 더 줄여보겠습니다. 또한, 제네릭 타입을 구조체 및 열거형 정의에 사용하는 방법도 살펴보겠습니다. 다음으로는 트레이트 (trait) 를 이용해 동작을 제네릭한 방식으로 정의하는 법을 배워보겠습니다. 트레이트를 제네릭 타입과 함께 사용하면, 아무 타입이나 허용하는 것이 아니라 특정 동작을 하는 타입만 허용할 수 있습니다. 마지막으로는 라이프타임 (lifetime) 을 살펴보겠습니다: 라이프타임은 제네릭의 일종이며, 컴파일러에게 참조자들이 서로 어떤 관계에 있는지를 알려주는 데에 사용합니다. 라이프타임은 빌린 값들에 대한 정보를 컴파일러에게 충분히 제공하여 작성자의 추가적인 도움 없이도 참조자의 여러 가지 상황에 대한 유효성 검증을 할 수 있게 해 줍니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 제네릭 타입, 트레이트, 라이프타임","id":"168","title":"제네릭 타입, 트레이트, 라이프타임"},"169":{"body":"제네릭은 여러 가지 타입을 나타내는 자리표시자의 위치에 특정 타입을 집어넣는 것으로 코드 중복을 제거할 수 있게 해 줍니다. 제네릭 문법을 배우기 전에, 먼저 제네릭 타입을 이용하지 않고 여러 가지 값을 나타내는 자리표시자로 특정 값을 대체하는 함수를 추출하는 방식으로 중복되는 코드를 없애는 요령을 알아보겠습니다. 그다음 동일한 기법을 이용하여 제네릭 함수를 추출해 보겠습니다! 함수로 추출할 수 있는 중복되는 코드를 알아내는 방법을 보는 것으로 제네릭을 사용할 수 있는 중복되는 코드들이 인식되기 시작할 것입니다. 예제 10-1과 같이 리스트에서 가장 큰 숫자를 찾아내는 간단한 프로그램부터 시작하겠습니다. 파일명: src/main.rs fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!(\"The largest number is {}\", largest);\n# assert_eq!(*largest, 100);\n} 예제 10-1: 숫자 리스트에서 가장 큰 수 찾기 number_list 변수에는 정수 리스트를 저장하고, largest 변수에 리스트의 첫 번째 숫자에 대한 참조자를 집어넣습니다. 그리고 리스트 내 모든 숫자를 순회하는데, 만약 현재 값이 largest에 저장된 값보다 크다면 largest의 값을 현재 값으로 변경합니다. 현재 값이 여태까지 본 가장 큰 값보다 작다면 largest의 값은 바뀌지 않습니다. 리스트 내 모든 숫자를 돌아보고 나면 largest는 가장 큰 값을 갖게 되며, 위의 경우에는 100이 됩니다. 이번에는 두 개의 다른 숫자 리스트에서 가장 큰 숫자를 찾으라는 일감을 받았습니다. 그렇게 하려면 예제 10-2처럼 예제 10-1의 코드를 프로그램 내 다른 곳에 복사하여 동일한 로직을 이용할 수도 있습니다. 파일명: src/main.rs fn main() { let number_list = vec![34, 50, 25, 100, 65]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!(\"The largest number is {}\", largest); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = &number_list[0]; for number in &number_list { if number > largest { largest = number; } } println!(\"The largest number is {}\", largest);\n} 예제 10-2: 두 개의 숫자 리스트에서 가장 큰 숫자를 찾는 코드 이 코드는 잘 동작하지만, 중복된 코드를 생성하는 일은 지루하고 에러가 발생할 가능성도 커집니다. 또한, 로직을 바꾸고 싶을 때 수정해야 할 부분이 여러 군데임을 기억해야 한다는 의미이기도 합니다. 이러한 중복을 제거하기 위해서, 정수 리스트를 매개변수로 전달받아 동작하는 함수를 정의하여 추상화할 것입니다. 이렇게 하면 코드가 더 명확해지고 목록에서 가장 큰 숫자를 찾는다는 개념을 추상적으로 표현할 수 있습니다. 예제 10-3에서는 가장 큰 수를 찾는 코드를 largest라는 이름의 함수로 추출합니다. 그다음 예제 10-2에 있는 두 리스트에서 가장 큰 수를 찾기 위해 이 함수를 호출합니다. 나중에 있을지 모를 다른 어떤 i32 값의 리스트에 대해서라도 이 함수를 사용할 수 있겠습니다. 파일명: src/main.rs fn largest(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!(\"The largest number is {}\", result);\n# assert_eq!(*result, 100); let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&number_list); println!(\"The largest number is {}\", result);\n# assert_eq!(*result, 6000);\n} 예제 10-3: 두 리스트에서 가장 큰 수를 찾는 추상화된 코드 largest 함수는 list 매개변수를 갖는데, 이는 함수로 전달될 임의의 i32 값 슬라이스를 나타냅니다. 실제로 largest 함수가 호출될 때는 전달받은 구체적인 값으로 실행됩니다. 예제 10-2에서부터 예제 10-3까지 거친 과정을 요약하면 다음과 같습니다: 중복된 코드를 식별합니다. 중복된 코드를 함수의 본문으로 분리하고, 함수의 시그니처 내에 해당 코드의 입력값 및 반환 값을 명시합니다. 중복됐었던 두 지점의 코드를 함수 호출로 변경합니다. 다음에는 제네릭으로 이 과정을 그대로 진행하여 중복된 코드를 제거해 보겠습니다. 함수 본문이 특정한 값 대신 추상화된 list로 동작하는 것처럼, 제네릭을 이용한 코드는 추상화된 타입으로 동작합니다. 만약 i32 슬라이스에서 최댓값을 찾는 함수와 char 슬라이스에서 최댓값을 찾는 함수를 따로 가지고 있다면 어떨까요? 이런 중복은 어떻게 제거해야 할지 한번 알아봅시다!","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 함수를 추출하여 중복 없애기","id":"169","title":"함수를 추출하여 중복 없애기"},"17":{"body":"러스트가 제대로 설치되었는지 확인하는 방법은 다음과 같습니다: $ rustc --version 최신 릴리즈된 stable 버전 정보가 다음 포맷대로 나타나며, 나타난 정보는 순서대로 버전 숫자, 커밋 해시 (hash), 커밋 날짜입니다: rustc x.y.z (abcabcabc yyyy-mm-dd) 위의 정보가 보이면 러스트가 성공적으로 설치된 것입니다! 정보가 보이지 않는다면 여러분의 %PATH% 시스템 변수에 러스트가 포함되어 있는지 확인해 주세요. Windows CMD에서는 다음과 같이 확인합니다: > echo %PATH% PowerShell에서는 다음과 같이 확인합니다: > echo $env:Path Linux와 macOS에서는 다음과 같이 확인합니다: $ echo $PATH 잘못된 것을 찾을 수 없는데 계속 작동하지 않으면 한국 러스트 사용자 그룹 디스코드 에 질문해 주세요. 영어가 능숙한 분들은 커뮤니티 페이지 에서 다른 러스타시안 (Rustacean, 러스트 사용자들 스스로를 부르는 웃긴 별명입니다) 들을 만나볼 수 있을 겁니다.","breadcrumbs":"시작해봅시다 » 러스트 설치 » 트러블 슈팅","id":"17","title":"트러블 슈팅"},"170":{"body":"제네릭을 사용하면 함수 시그니처나 구조체의 아이템에 다양한 구체적 데이터 타입을 사용할 수 있도록 정의할 수 있습니다. 함수, 구조체, 열거형, 메서드를 제네릭으로 정의하는 방법을 알아보고, 제네릭이 코드 성능에 미치는 영향을 알아보겠습니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 제네릭 데이터 타입 » 제네릭 데이터 타입","id":"170","title":"제네릭 데이터 타입"},"171":{"body":"제네릭 함수를 정의할 때는, 함수 시그니처 내 매개변수와 반환 값의 데이터 타입 위치에 제네릭을 사용합니다. 이렇게 작성된 코드는 더 유연해지고, 이 함수를 호출하는 쪽에서 더 많은 기능을 사용할 수 있도록 하며 코드 중복 또한 방지합니다. largest 함수를 이용해 계속해 보겠습니다. 예제 10-4는 슬라이스에서 가장 큰 값을 찾는 두 함수를 보여줍니다. 제네릭 사용하여 이 함수들을 하나의 함수로 묶어보겠습니다. 파일명: src/main.rs fn largest_i32(list: &[i32]) -> &i32 { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn largest_char(list: &[char]) -> &char { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest_i32(&number_list); println!(\"The largest number is {}\", result);\n# assert_eq!(*result, 100); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest_char(&char_list); println!(\"The largest char is {}\", result);\n# assert_eq!(*result, 'y');\n} 예제 10-4: 이름과 타입 시그니처만 다른 두 함수 함수 largest_i32는 예제 10-3에서 봤던 슬라이스에서 가장 큰 i32를 찾는 함수이고, largest_char 함수는 슬라이스에서 가장 큰 char를 찾는 함수입니다. 이 두 함수의 본문은 완벽히 동일하니, 제네릭을 이용해 이 두 함수를 하나로 만들어서 코드 중복을 제거해 보겠습니다. 새 단일 함수의 시그니처 내 타입을 매개변수화하려면 타입 매개변수의 이름을 지어줄 필요가 있습니다. 방법은 함수 매개변수와 비슷합니다. 타입 매개변수의 이름에는 아무 식별자나 사용할 수 있지만, 여기서는 T를 사용하겠습니다. 러스트에서는 타입 이름을 지어줄 때는 대문자로 시작하는 낙타 표기법 (UpperCamelCase) 을 따르고, 타입 매개변수의 이름은 짧게 (한 글자로만 된 경우도 종종 있습니다) 짓는 것이 관례이기 때문에, 대부분의 러스트 프로그래머는 'type'을 줄인 T를 사용합니다. 함수 본문에서 매개변수를 사용하려면 함수 시그니처에 매개변수의 이름을 선언하여 컴파일러에게 해당 이름이 무엇을 의미하는지 알려주어야 해야 하는 것처럼, 타입 매개변수를 사용하기 전에도 타입 매개변수의 이름을 선언해야 합니다. 예를 들어, 제네릭 largest 함수를 정의하려면 아래와 같이 함수명과 매개변수 목록 사이의 꺾쇠괄호(<>)에 타입 매개변수 이름을 선언해야 합니다: fn largest(list: &[T]) -> &T { 이 정의는 ‘largest 함수는 어떤 타입 T에 대한 제네릭 함수’라고 읽힙니다. 이 함수는 T 타입 값의 슬라이스인 list 매개변수를 가지고 있고, 동일한 T 타입의 값에 대한 참조자를 반환합니다. 예제 10-5는 제네릭 데이터 타입을 사용해 하나로 통합한 largest 함수 정의를 나타냅니다. 코드에서 볼 수 있듯, 이 함수를 i32 값들의 슬라이스로 호출할 수도 있고 char 값들의 슬라이스로도 호출할 수 있습니다. 이 코드는 아직 컴파일되지 않음을 주의해 주시고, 나중에 고치도록 하겠습니다. 파일명: src/main.rs fn largest(list: &[T]) -> &T { let mut largest = &list[0]; for item in list { if item > largest { largest = item; } } largest\n} fn main() { let number_list = vec![34, 50, 25, 100, 65]; let result = largest(&number_list); println!(\"The largest number is {}\", result); let char_list = vec!['y', 'm', 'a', 'q']; let result = largest(&char_list); println!(\"The largest char is {}\", result);\n} 예제 10-5: 제네릭 타입 매개변수를 이용한 largest 함수; 아직 컴파일되지는 않습니다 이 코드를 지금 바로 컴파일해 보면 다음과 같은 에러가 발생합니다: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0369]: binary operation `>` cannot be applied to type `&T` --> src/main.rs:5:17 |\n5 | if item > largest { | ---- ^ ------- &T | | | &T |\nhelp: consider restricting type parameter `T` |\n1 | fn largest(list: &[T]) -> &T { | ++++++++++++++++++++++ For more information about this error, try `rustc --explain E0369`.\nerror: could not compile `chapter10` due to previous error 도움말에서 트레이트 (trait) std::cmp::PartialOrd가 언급되는데, 트레이트는 다음 절에서 살펴볼 것입니다. 지금은 이 에러가 ‘largest의 본문이 T가 될 수 있는 모든 타입에 대해 동작할 수 없음’을 뜻한다는 정도만 알아둡시다. 함수 본문에서 T 타입 값들에 대한 비교가 필요하므로, 여기에는 값을 정렬할 수 있는 타입에 대해서만 동작할 수 있습니다. 비교가 가능하도록 하기 위해, 표준 라이브러리는 임의의 타입에 대해 구현 가능한 std::cmp::PartialOrd 트레이트를 제공합니다. (이 트레이트에 대한 더 자세한 사항은 부록 C를 보세요.) 도움말의 제안을 따라서 T가 PartialOrd를 구현한 것일 때만 유효하도록 제한을 두면 이 예제는 컴파일되는데, 이는 표준 라이브러리가 i32와 char 둘 모두에 대한 PartialOrd를 구현하고 있기 때문입니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 제네릭 데이터 타입 » 제네릭 함수 정의","id":"171","title":"제네릭 함수 정의"},"172":{"body":"<> 문법으로 구조체 필드에서 제네릭 타입 매개변수를 사용하도록 구조체를 정의할 수도 있습니다. 예제 10-6은 임의의 타입으로 된 x, y를 갖는 Point 구조체를 정의합니다. 파일명: src/main.rs struct Point { x: T, y: T,\n} fn main() { let integer = Point { x: 5, y: 10 }; let float = Point { x: 1.0, y: 4.0 };\n} 예제 10-6: T 타입의 값 x, y를 갖는 Point 구조체 문법은 함수 정의에서 제네릭을 사용하는 것과 유사합니다. 먼저 구조체 이름 바로 뒤 꺾쇠괄호에 타입 매개변수 이름을 선언하고, 구조체 정의 내 구체적 데이터 타입을 지정하던 곳에 제네릭 타입을 대신 사용합니다. Point 선언에 하나의 제네릭 타입만 사용했으므로, 이 선언은 Point가 어떤 타입 T에 대한 제네릭이며 x, y 필드는 실제 타입이 무엇이건 간에 둘 다 동일한 타입이라는 것을 의미합니다. 만약 예제 10-7처럼 서로 다른 타입의 값을 갖는 Point 인스턴스를 생성하려고 할 경우, 코드는 컴파일되지 않습니다. 파일명: src/main.rs struct Point { x: T, y: T,\n} fn main() { let wont_work = Point { x: 5, y: 4.0 };\n} 예제 10-7: x와 y 필드는 둘 다 동일한 제네릭 데이터 타입 T이므로 같은 타입이어야 합니다 컴파일러는 x에 정숫값 5를 대입할 때 Point 인스턴스의 제네릭 타입 T를 정수 타입으로 인지합니다. 그다음에는 y에 4.0을 지정했는데, y는 x와 동일한 타입을 갖도록 정의되었으므로 컴파일러는 타입 불일치 에러를 발생시킵니다: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0308]: mismatched types --> src/main.rs:7:38 |\n7 | let wont_work = Point { x: 5, y: 4.0 }; | ^^^ expected integer, found floating-point number For more information about this error, try `rustc --explain E0308`.\nerror: could not compile `chapter10` due to previous error 제네릭 Point 구조체의 x, y가 서로 다른 타입일 수 있도록 정의하고 싶다면 여러 개의 제네릭 타입 매개변수를 사용해야 합니다. 예제 10-8에서는 x는 T 타입으로, y는 U 타입으로 정의한 제네릭 Point 정의를 나타냅니다. 파일명: src/main.rs struct Point { x: T, y: U,\n} fn main() { let both_integer = Point { x: 5, y: 10 }; let both_float = Point { x: 1.0, y: 4.0 }; let integer_and_float = Point { x: 5, y: 4.0 };\n} 예제 10-8: 두 타입의 제네릭을 사용하여, x와 y가 서로 다른 타입의 값이 될 수 있는 Point 이제 위와 같이 모든 Point 인스턴스를 생성할 수 있습니다! 제네릭 타입 매개변수는 원하는 만큼 여러 개를 정의할 수 있지만, 많으면 많아질수록 코드 가독성은 떨어집니다. 만약 코드에서 많은 수의 제네릭 타입이 필요함을 알게 되었다면, 코드를 리팩터링해서 작은 부분들로 나누는 것을 고려해야 할 수도 있겠습니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 제네릭 데이터 타입 » 제네릭 구조체 정의","id":"172","title":"제네릭 구조체 정의"},"173":{"body":"구조체처럼, 열거형도 배리언트에 제네릭 데이터 타입을 갖도록 정의할 수 있습니다. 6장에서 사용했었던 표준 라이브러리의 Option 열거형을 다시 살펴봅시다: enum Option { Some(T), None,\n} 이제는 이 코드를 이해할 수 있습니다. 보시다시피 Option 열거형은 T 타입에 대한 제네릭이며, T 타입을 들고 있는 Some 배리언트와 아무런 값도 들고 있지 않은 None 배리언트를 갖습니다. Option 열거형을 사용함으로써 옵션 값에 대한 추상화된 개념을 표현할 수 있고, Option 열거형이 제네릭으로 되어있는 덕분에 옵션 값이 어떤 타입이건 상관없이 추상화하여 사용할 수 있죠. 열거형에서도 여러 개의 제네릭 타입을 이용할 수 있습니다. 9장에서 사용했던 Result 열거형의 정의가 대표적인 예시입니다: enum Result { Ok(T), Err(E),\n} Result 열거형은 T, E 두 타입을 이용한 제네릭이며, T 타입 값을 갖는 Ok와 E 타입 값을 갖는 Err 배리언트를 갖습니다. 제네릭으로 정의되어 있는 덕분에, 연산이 성공할지 (따라서 T 타입 값을 반환할지) 실패할지 (E 타입 값을 반환할지) 알 수 없는 어느 곳이든 Result 열거형을 편리하게 사용할 수 있습니다. 예제 9-3에서 파일을 열 때도 사용했었죠. 이때는 파일을 여는 데 성공하면 T는 std::fs::File 타입이 되고, 파일을 열다가 문제가 생기면 E는 std::io::Error 타입이 됐었습니다. 작성한 코드에서 보유하는 값의 타입만 다른 구조체나 열거형이 여러 개 있음을 발견했을 때는 제네릭 타입을 사용해 코드 중복을 제거할 수 있습니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 제네릭 데이터 타입 » 제네릭 열거형 정의","id":"173","title":"제네릭 열거형 정의"},"174":{"body":"5장에서 했던 것처럼 구조체나 열거형에 메서드를 구현할 때도 제네릭 타입을 이용해 정의할 수 있습니다. 예제 10-9는 예제 10-6에서 정의했던 Point 구조체에 x 메서드를 구현한 모습입니다. 파일명: src/main.rs struct Point { x: T, y: T,\n} impl Point { fn x(&self) -> &T { &self.x }\n} fn main() { let p = Point { x: 5, y: 10 }; println!(\"p.x = {}\", p.x());\n} 예제 10-9: T 타입의 x 필드에 대한 참조자를 반환하는 x 메서드를 Point에 정의 x 필드 데이터의 참조자를 반환하는 x 메서드를 Point에 정의해 보았습니다. impl 바로 뒤에 T를 선언하여 Point 타입에 메서드를 구현한다고 명시했음을 주의하세요. 이렇게 하면 러스트는 Point의 꺾쇠괄호 내 타입이 구체적인 타입이 아닌 제네릭 타입임을 인지합니다. 구조체 정의에 선언된 제네릭 매개변수와는 다른 제네릭 매개변수를 선택할 수도 있었겠지만, 같은 이름을 사용하는 것이 관례입니다. 제네릭 타입이 선언된 impl 안에 작성된 메서드는 이 제네릭 타입에 어떤 구체 타입을 집어넣을지와는 상관없이 어떠한 타입의 인스턴스에라도 정의될 것입니다. 이 타입의 메서드를 정의할 때 제네릭 타입에 대한 제약을 지정할 수도 있습니다. 예를 들면, 임의의 제네릭 타입 Point 인스턴스가 아닌 Point 인스턴스에 대한 메서드만을 정의할 수 있습니다. 예제 10-10에서는 구체적 타입 f32을 사용하였는데, impl 뒤에는 어떤 타입도 선언하지 않았습니다. 파일명: src/main.rs # struct Point {\n# x: T,\n# y: T,\n# }\n# # impl Point {\n# fn x(&self) -> &T {\n# &self.x\n# }\n# }\n# impl Point { fn distance_from_origin(&self) -> f32 { (self.x.powi(2) + self.y.powi(2)).sqrt() }\n}\n# # fn main() {\n# let p = Point { x: 5, y: 10 };\n# # println!(\"p.x = {}\", p.x());\n# } 예제 10-10: 구조체의 제네릭 타입 매개변수 T가 특정 구체적 타입인 경우에만 적용되는 impl 블록 이 코드에서 Point 타입 인스턴스는 distance_from_origin 메서드를 갖게 될 것입니다; T가 f32 타입이 아닌 Point 인스턴스는 이 메서드가 정의되지 않습니다. 이 메서드는 생성된 점이 원점 (0.0, 0.0)으로부터 떨어진 거리를 측정하며 부동 소수점 타입에서만 사용 가능한 수학적 연산을 이용합니다. 구조체 정의에서 사용한 제네릭 타입 매개변수와, 구조체의 메서드 시그니처 내에서 사용하는 제네릭 타입 매개변수가 항상 같은 것은 아닙니다. 예제 10-11을 보면 예제를 명료하게 만들기 위해 Point 구조체에 대해서는 X1와 Y1이라는 제네릭 타입을, 그리고 mixup 메서드에 대해서는 X2와 Y2라는 제네릭 타입을 사용했습니다. 이 메서드는 self Point의 (X1 타입인) x값과 매개변수로 넘겨받은 Point의 (Y2 타입인) y값으로 새로운 Point 인스턴스를 생성합니다. 파일명: src/main.rs struct Point { x: X1, y: Y1,\n} impl Point { fn mixup(self, other: Point) -> Point { Point { x: self.x, y: other.y, } }\n} fn main() { let p1 = Point { x: 5, y: 10.4 }; let p2 = Point { x: \"Hello\", y: 'c' }; let p3 = p1.mixup(p2); println!(\"p3.x = {}, p3.y = {}\", p3.x, p3.y);\n} 예제 10-11: 구조체 정의와 다른 제네릭 타입을 사용하는 메서드 main에서는 i32 타입 x(5)와 f64 타입 y(10.4)를 갖는 Point를 정의했습니다. p2는 문자열 슬라이스 타입 x(\"Hello\")와 char 타입 y(c)를 갖는 Point입니다. p3는 p1에서 mixup 메서드를 p2를 인수로 호출하여 반환된 값입니다. p3의 x는 p1에서 온 i32 타입이며, y는 p2에서 온 char 타입입니다. println! 매크로는 p3.x = 5, p3.y = c를 출력합니다. 이 예제는 제네릭 매개변수 중 일부가 impl 에 선언되고 일부는 메서드 정의에 선언되는 경우를 보여주기 위한 예제입니다. 여기서 제네릭 매개변수 X1, Y1는 구조체 정의와 한 묶음이니 impl 뒤에 선언했지만, 제네릭 매개변수 X2, Y2는 mixup 메서드에만 연관되어 있으므로 fn mixup 뒤에 선언합니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 제네릭 데이터 타입 » 제네릭 메서드 정의","id":"174","title":"제네릭 메서드 정의"},"175":{"body":"제네릭 타입 매개변수를 사용하면 런타임 비용이 발생하는지 궁금해 할지도 모르겠습니다. 좋은 소식은, 제네릭 타입의 사용이 구체적인 타입을 사용했을 때와 비교해서 전혀 느려지지 않는다는 것입니다. 러스트는 컴파일 타임에 제네릭을 사용하는 코드를 단형성화 (monomorphization) 합니다. 단형성화란 제네릭 코드를 실제 구체 타입으로 채워진 특정한 코드로 바꾸는 과정을 말합니다. 이 과정에서, 컴파일러는 예제 10-5에서 제네릭 함수를 만들 때 거친 과정을 정반대로 수행합니다: 즉 컴파일러는 제네릭 코드가 호출된 곳을 전부 찾고, 제네릭 코드가 호출할 때 사용된 구체 타입으로 코드를 생성합니다. 표준 라이브러리의 Option 열거형을 사용하는 예제를 통해 알아봅시다: let integer = Some(5);\nlet float = Some(5.0); 러스트는 이 코드를 컴파일할 때 단형성화를 수행합니다. 이 과정 중 컴파일러는 Option 인스턴스에 사용된 값을 읽고, i32, f64 두 종류의 Option가 있다는 것을 인지합니다. 그리고 제네릭 정의를 i32와 f64에 대해 특성화시킨 정의로 확장함으로써, 제네릭 정의를 이 구체적인 것들로 대체합니다. 단형성화된 코드는 다음과 비슷합니다. (여기 사용된 이름은 예시를 위한 것이며 컴파일러에 의해 생성되는 이름은 다릅니다): 파일명: src/main.rs enum Option_i32 { Some(i32), None,\n} enum Option_f64 { Some(f64), None,\n} fn main() { let integer = Option_i32::Some(5); let float = Option_f64::Some(5.0);\n} 제네릭 Option가 컴파일러에 의해 특정한 정의들로 대체되었습니다. 러스트 컴파일러가 제네릭 코드를 각 인스턴스의 명시적인 타입으로 변경해 주는 덕분에, 굳이 런타임 비용을 줄이기 위해 수동으로 직접 각 타입마다 중복된 코드를 작성할 필요가 없습니다. 단형성화 과정은 러스트 제네릭을 런타임에 극도로 효율적으로 만들어줍니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 제네릭 데이터 타입 » 제네릭 코드의 성능","id":"175","title":"제네릭 코드의 성능"},"176":{"body":"트레이트 (trait) 는 특정한 타입이 가지고 있으면서 다른 타입과 공유할 수 있는 기능을 정의합니다. 트레이트를 사용하면 공통된 기능을 추상적으로 정의할 수 있습니다. 트레이트 바운드 (trait bound) 를 이용하면 어떤 제네릭 타입 자리에 특정한 동작을 갖춘 타입이 올 수 있음을 명시할 수 있습니다. Note: 약간의 차이는 있으나, 트레이트는 다른 언어에서 흔히 인터페이스 (interface) 라고 부르는 기능과 유사합니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 트레이트로 공통된 동작을 정의하기 » 트레이트로 공통된 동작을 정의하기","id":"176","title":"트레이트로 공통된 동작을 정의하기"},"177":{"body":"타입의 동작은 해당 타입에서 호출할 수 있는 메서드로 구성됩니다. 만약 다양한 타입에서 동일한 메서드를 호출할 수 있다면, 이 타입들은 동일한 동작을 공유한다고 표현할 수 있을 겁니다. 트레이트 정의는 메서드 시그니처를 그룹화하여 특정 목적을 달성하는 데 필요한 일련의 동작을 정의하는 것입니다. 예를 들어 다양한 종류 및 분량의 텍스트를 갖는 여러 가지 구조체가 있다고 칩시다: NewsArticle 구조체는 특정 지역에서 등록된 뉴스 기사를 저장하고, Tweet 구조체는 최대 280자의 콘텐츠와 해당 트윗이 새 트윗인지, 리트윗인지, 다른 트윗의 대답인지를 나타내는 메타데이터를 저장합니다. NewsArticle이나 Tweet 인스턴스에 저장된 데이터를 종합해 보여주는 종합 미디어 라이브러리 크레이트 aggregator를 만든다고 가정합시다. 이를 위해서는 각 타입의 요약 정보를 얻어와야 하는데, 인스턴스에서 summarize 메서드를 호출하여 이 요약 정보를 가져오려고 합니다. 예제 10-12는 이 동작을 공개 Summary 트레이트 정의로 표현합니다. 파일명: src/lib.rs pub trait Summary { fn summarize(&self) -> String;\n} 예제 10-12: summarize 메서드가 제공하는 동작으로 구성된 Summary 트레이트 trait 키워드 다음 트레이트의 이름 Summary를 작성해 트레이트를 선언했습니다. 또한 몇몇 예제에서 보게 될 것처럼 트레이트를 pub으로 선언하여 이 크레이트에 의존하는 다른 크레이트가 이 트레이트를 사용할 수 있도록 하였습니다. 중괄호 안에는 이 트레이트를 구현할 타입의 동작을 묘사하는 메서드 시그니처를 선언했는데, 위의 경우는 fn summarize(&self) -> String입니다. 메서드 시그니처 뒤에는 중괄호로 시작하여 메서드를 구현하는 대신 세미콜론을 집어넣었습니다. 이 트레이트를 구현하는 각 타입이 메서드에 맞는 동작을 직접 제공해야 합니다. 컴파일러는 Summary 트레이트가 있는 모든 타입에 정확히 이와 같은 시그니처의 summarize 메서드를 가지고 있도록 강재할 것입니다. 트레이트는 본문에 여러 메서드를 가질 수 있습니다: 메서드 시그니처는 한 줄에 하나씩 나열되며, 각 줄은 세미콜론으로 끝납니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 트레이트로 공통된 동작을 정의하기 » 트레이트 정의하기","id":"177","title":"트레이트 정의하기"},"178":{"body":"Summary 트레이트의 메서드 시그니처를 원하는 대로 정의했으니, 종합 미디어 크레이트의 각 타입에 Summary 트레이트를 구현해 봅시다. 예제 10-13은 NewsArticle 구조체에 헤드라인, 저자, 지역 정보를 사용하여 summarize의 반환 값을 만드는 Summary 트레이트를 구현한 모습입니다. Tweet 구조체에는 트윗 내용이 이미 280자로 제한되어 있음을 가정하고, 사용자명과 해당 트윗의 전체 텍스트를 가져오도록 summarize를 정의했습니다. 파일명: src/lib.rs # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# pub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,\n} impl Summary for NewsArticle { fn summarize(&self) -> String { format!(\"{}, by {} ({})\", self.headline, self.author, self.location) }\n} pub struct Tweet { pub username: String, pub content: String, pub reply: bool, pub retweet: bool,\n} impl Summary for Tweet { fn summarize(&self) -> String { format!(\"{}: {}\", self.username, self.content) }\n} 예제 10-13: NewsArticle과 Tweet 타입에 Summary 트레이트 구현하기 어떤 타입에 대한 트레이트를 구현하는 것은 평범한 메서드를 구현하는 것과 비슷합니다. 다른 점은 impl 뒤에 구현하고자 하는 트레이트 이름을 적고, 그다음 for 키워드와 트레이트를 구현할 타입명을 명시한다는 점입니다. impl 블록 안에는 트레이트 정의에서 정의된 메서드 시그니처를 집어넣되, 세미콜론 대신 중괄호를 사용하여 메서드 본문에 원하는 특정한 동작을 채워 넣습니다. 라이브러리가 NewsArticle과 Tweet에 대한 Summary 트레이트를 구현했으니, 크레이트 사용자는 NewsArticle과 Tweet 인스턴스에 대하여 보통의 메서드를 호출하는 것과 같은 방식으로 트레이트 메서드를 호출할 수 있습니다. 유일한 차이점은 크레이트 사용자가 타입 뿐만 아니라 트레이트도 스코프로 가져와야 한다는 점입니다. 바이너리 크레이트가 aggregator 라이브러리 크레이트를 사용하는 방법에 대한 예제가 아래에 있습니다: use aggregator::{Summary, Tweet}; fn main() { let tweet = Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, }; println!(\"1 new tweet: {}\", tweet.summarize());\n} 이 코드는 1 new tweet: horse_ebooks: of course, as you probably already know, people를 출력합니다. aggregator 크레이트에 의존적인 다른 크레이트들 또한 Summary 트레이트를 스코프로 가져와서 자신들의 타입에 대해 Summary를 구현할 수 있습니다. 트레이트 구현에는 한 가지 제약사항이 있는데, 이는 트레이트가나 트레이트를 구현할 타입 둘 중 하나는 반드시 자신의 크레이트 것이어야 해당 타입에 대한 트레이트를 구현할 수 있다는 점입니다. 예를 들어, 우리가 만든 aggregator 크레이트의 일부 기능으로 Tweet 타입에 표준 라이브러리 트레이트인 Display 등을 구현할 수 있습니다. Tweet 타입이 우리가 만든 aggregator 크레이트의 타입이기 때문입니다. 또한 aggregator 크레이트에서 Vec 타입에 Summary 트레이트를 구현할 수도 있습니다. 마찬가지로 Summary 트레이트가 우리가 만든 aggregator 크레이트의 트레이트가기 때문입니다. 하지만 외부 타입에 외부 트레이트를 구현할 수는 없습니다. 예를 들어, 우리가 만든 aggregator 크레이트에서는 Vec에 대한 Display 트레이트를 구현할 수 없습니다. Vec, Display 둘 다 우리가 만든 크레이트가 아닌 표준 라이브러리에 정의되어 있기 때문입니다. 이 제약은 프로그램의 특성 중 하나인 일관성 (coherence) , 보다 자세히는 고아 규칙 (orphan rule) 에서 나옵니다. (부모 타입이 존재하지 않기 때문에 고아 규칙이라고 부릅니다.) 이 규칙으로 인해 다른 사람의 코드가 여러분의 코드를 망가뜨릴 수 없으며 반대의 경우도 마찬가지입니다. 이 규칙이 없다면 두 크레이트가 동일한 타입에 동일한 트레이트를 구현할 수 있게 되고, 러스트는 어떤 구현체를 이용해야 할지 알 수 없게 됩니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 트레이트로 공통된 동작을 정의하기 » 특정 타입에 트레이트 구현하기","id":"178","title":"특정 타입에 트레이트 구현하기"},"179":{"body":"타입에 트레이트를 구현할 때마다 모든 메서드를 구현할 필요는 없도록 트레이트의 메서드에 기본 동작을 제공할 수도 있습니다. 이러면 특정한 타입에 트레이트를 구현할 때 기본 동작을 유지할지 혹은 오버라이드 (override) 할지 선택할 수 있습니다. 예제 10-14는 예제 10-12에서 Summary 트레이트에 메서드 시그니처만 정의했던 것과는 달리 summarize 메서드에 기본 문자열을 명시하였습니다. 파일명: src/lib.rs pub trait Summary { fn summarize(&self) -> String { String::from(\"(Read more...)\") }\n}\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {}\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# } 예제 10-14: summarize 메서드의 기본 구현이 있는 Summary 트레이트 정의하기 NewsArticle 인스턴스에 기본 구현을 사용하려면 impl Summary for NewsArticle {}처럼 비어있는 impl 블록을 명시합니다. NewsArticle 에 summarize 메서드를 직접적으로 정의하지는 않았지만, NewsArticle은 Summary 트레이트를 구현하도록 지정되어 있으며, Summary 트레이트는 summarize 메서드의 기본 구현을 제공합니다. 결과적으로 아래처럼 NewsArticle 인스턴스에서 summarize 메서드를 여전히 호출할 수 있습니다: # use aggregator::{self, NewsArticle, Summary};\n# # fn main() { let article = NewsArticle { headline: String::from(\"Penguins win the Stanley Cup Championship!\"), location: String::from(\"Pittsburgh, PA, USA\"), author: String::from(\"Iceburgh\"), content: String::from( \"The Pittsburgh Penguins once again are the best \\ hockey team in the NHL.\", ), }; println!(\"New article available! {}\", article.summarize());\n# } 이 코드는 New article available! (Read more...)를 출력합니다. 기본 구현을 생성한다고 해서 예제 10-13 코드의 Tweet 의 Summary 구현을 변경할 필요는 없습니다. 기본 구현을 오버라이딩하는 문법과 기본 구현이 없는 트레이트 메서드를 구현하는 문법은 동일하기 때문입니다. 기본 구현 안쪽에서 트레이트의 다른 메서드를 호출할 수도 있습니다. 호출할 다른 메서드가 기본 구현을 제공하지 않는 메서드여도 상관없습니다. 이런 방식으로 트레이트는 구현자에게 작은 부분만 구현을 요구하면서 유용한 기능을 많이 제공할 수 있습니다. 예시로 알아봅시다. Summary 트레이트에 summarize_author 메서드를 추가하고, summarize 메서드의 기본 구현 내에서 summarize_author 메서드를 호출하도록 만들어 보았습니다: pub trait Summary { fn summarize_author(&self) -> String; fn summarize(&self) -> String { format!(\"(Read more from {}...)\", self.summarize_author()) }\n}\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize_author(&self) -> String {\n# format!(\"@{}\", self.username)\n# }\n# } 이 Summary를 어떤 타입에 구현할 때는 summarize_author만 정의하면 됩니다: # pub trait Summary {\n# fn summarize_author(&self) -> String;\n# # fn summarize(&self) -> String {\n# format!(\"(Read more from {}...)\", self.summarize_author())\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# impl Summary for Tweet { fn summarize_author(&self) -> String { format!(\"@{}\", self.username) }\n} summarize_author를 정의하고 나면 Tweet 인스턴스에서 summarize를 호출할 수 있습니다. 이러면 summarize 기본 구현이 직접 정의한 summarize_author 메서드를 호출할 겁니다. summarize_author만 구현하고 추가적인 코드를 전혀 작성하지 않았지만, Summary 트레이트는 summarize 메서드의 기능도 제공해 주는 것을 알 수 있습니다. # use aggregator::{self, Summary, Tweet};\n# # fn main() { let tweet = Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, }; println!(\"1 new tweet: {}\", tweet.summarize());\n# } 이 코드는 1 new tweet: (Read more from @horse_ebooks...)를 출력합니다. 어떤 메서드를 오버라이딩하는 구현을 하면 해당 메서드의 기본 구현을 호출할 수는 없다는 점을 주의하세요.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 트레이트로 공통된 동작을 정의하기 » 기본 구현","id":"179","title":"기본 구현"},"18":{"body":"rustup으로 러스트를 설치했다면 최신 버전 업데이트도 간편합니다. 셸에 다음 명령어를 입력해 주세요: $ rustup update rustup과 러스트를 삭제하는 방법은 다음과 같습니다: $ rustup self uninstall","breadcrumbs":"시작해봅시다 » 러스트 설치 » 업데이트 및 삭제","id":"18","title":"업데이트 및 삭제"},"180":{"body":"트레이트를 정의하고 구현하는 방법을 알아보았으니, 트레이트를 이용하여 어떤 함수가 다양한 타입으로 작동하게 만드는 법을 알아봅시다. 예제 10-13에서 NewsArticle, Tweet 타입에 구현한 Summary 트레이트를 사용하여, Summary 트레이트를 구현하는 어떤 타입의 item 매개변수에서 summarize 메서드를 호출하는 notify 함수를 정의하겠습니다. 이렇게 하려면 아래와 같이 impl Trait 문법을 사용합니다: # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {\n# fn summarize(&self) -> String {\n# format!(\"{}, by {} ({})\", self.headline, self.author, self.location)\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# }\n# pub fn notify(item: &impl Summary) { println!(\"Breaking news! {}\", item.summarize());\n} item 매개변수의 구체적 타입을 명시하는 대신 impl 키워드와 트레이트 이름을 명시했습니다. 이 매개변수에는 지정된 트레이트를 구현하는 타입이라면 어떤 타입이든 전달받을 수 있습니다. notify 본문 내에서는 item에서 summarize와 같은 Summary 트레이트의 모든 메서드를 호출할 수 있습니다. notify는 NewsArticle 인스턴스로도, Tweet 인스턴스로도 호출할 수 있습니다. 만약 Summary 트레이트를 구현하지 않는 String, i32 등의 타입으로 notify 함수를 호출하는 코드를 작성한다면 컴파일 에러가 발생합니다. 트레이트 바운드 문법 impl Trait 문법은 간단하지만, 이는 트레이트 바운드 (trait bound) 로 알려진, 좀 더 기다란 형식의 문법 설탕입니다; 트레이트 바운드는 다음과 같이 생겼습니다: pub fn notify(item: &T) { println!(\"Breaking news! {}\", item.summarize());\n} 앞서 본 예시와 동일한 코드지만, 더 장황합니다. 트레이트 바운드는 꺾쇠괄호 안의 제네릭 타입 매개변수 선언에 붙은 콜론(:) 뒤에 위치합니다. impl Trait 문법이 단순한 상황에서는 편리하고 코드를 더 간결하게 만들어 주는 반면, 트레이트 바운드 문법은 더 복잡한 상황을 표현할 수 있습니다. 예를 들어, Summary를 구현하는 두 매개변수를 전달받는 함수를 구현할 때, impl Trait 문법으로 표현하면 다음과 같은 모양이 됩니다: pub fn notify(item1: &impl Summary, item2: &impl Summary) { item1 과 item2가 (둘 다 Summary를 구현하는 타입이되) 서로 다른 타입이어도 상관없다면 impl Trait 문법 사용도 적절합니다. 하지만 만약 두 매개변수가 같은 타입으로 강제되어야 한다면, 이는 아래와 같이 트레이트 바운드를 사용해야 합니다: pub fn notify(item1: &T, item2: &T) { item1 및 item2 매개변수의 타입으로 지정된 제네릭 타입 T는 함수를 호출할 때 item1, item2 인수 값의 구체적인 타입이 반드시 동일하도록 제한합니다. + 구문으로 트레이트 바운드를 여럿 지정하기 트레이트 바운드는 여러 개 지정될 수도 있습니다. notify에서 item의 summarize 메서드뿐만 아니라 출력 포맷팅까지 사용하고 싶다고 가정해 봅시다: 즉 notify의 정의를 할때 item이 Display, Summary를 모두 구현해야 하도록 지정해야 합니다. + 문법을 사용하면 트레이트를 여러 개 지정할 수 있습니다: pub fn notify(item: &(impl Summary + Display)) { + 구문은 제네릭 타입의 트레이트 바운드에도 사용할 수 있습니다: pub fn notify(item: &T) { 두 개의 트레이트 바운드가 지정됐으니, notify 본문에서는 item의 summarize 메서드를 호출할 수도 있고 item을 {}로 포맷팅할 수도 있습니다. where 조항으로 트레이트 바운드 정리하기 트레이트 바운드가 너무 많아지면 문제가 생깁니다. 제네릭마다 트레이트 바운드를 갖게 되면, 여러 제네릭 타입 매개변수를 사용하는 함수는 함수명과 매개변수 사이에 너무 많은 트레이트 바운드 정보를 담게 될 가능성이 있습니다. 이는 가독성을 해치기 때문에, 러스트는 트레이트 바운드를 함수 시그니처 뒤의 where 조항에 명시하는 대안을 제공합니다. 즉, 다음과 같이 작성하는 대신: fn some_function(t: &T, u: &U) -> i32 { 다음과 같이 where 조항을 사용할 수 있습니다: fn some_function(t: &T, u: &U) -> i32\nwhere T: Display + Clone, U: Clone + Debug,\n{\n# unimplemented!()\n# } 트레이트 바운드로 도배되지 않고, 평범한 함수처럼 함수명과 매개변수 목록, 반환 타입이 붙어 있으니, 함수 시그니처를 읽기 쉬워집니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 트레이트로 공통된 동작을 정의하기 » 매개변수로서의 트레이트","id":"180","title":"매개변수로서의 트레이트"},"181":{"body":"아래처럼 impl Trait 문법을 반환 타입 위치에 써서 어떤 트레이트를 구현한 타입의 값을 반환시키는 데에도 사용할 수 있습니다: # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {\n# fn summarize(&self) -> String {\n# format!(\"{}, by {} ({})\", self.headline, self.author, self.location)\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# }\n# fn returns_summarizable() -> impl Summary { Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, }\n} 반환 타입에 구체적인 타입명이 아닌 impl Summary를 작성하여 returns_summarizable 함수는 Summary 트레이트를 구현하는 타입을 반환한다고 명시했습니다. 위의 경우 returns_summarizable는 Tweet을 반환하지만, 이 함수를 호출하는 쪽의 코드에서는 구체적인 타입을 알 필요가 없습니다. 구현되는 트레이트로 반환 타입을 명시하는 기능은 13장에서 다룰 클로저 및 반복자의 컨텍스트에서 굉장히 유용합니다. 클로저와 반복자는 컴파일러만 아는 타입이나, 직접 명시하기에는 굉장히 긴 타입을 생성합니다. impl Trait 문법을 사용하면 굉장히 긴 타입을 직접 작성할 필요 없이 Iterator 트레이트를 구현하는 어떤 타입이라고 간결하게 지정할 수 있습니다. 하지만, impl Trait 문법을 쓴다고 해서 다양한 타입을 반환할 수는 없습니다. 다음은 반환형을 impl Summary로 지정하고 NewsArticle, Tweet 중 하나를 반환하는 코드 예시입니다. 이 코드는 컴파일할 수 없습니다: # pub trait Summary {\n# fn summarize(&self) -> String;\n# }\n# # pub struct NewsArticle {\n# pub headline: String,\n# pub location: String,\n# pub author: String,\n# pub content: String,\n# }\n# # impl Summary for NewsArticle {\n# fn summarize(&self) -> String {\n# format!(\"{}, by {} ({})\", self.headline, self.author, self.location)\n# }\n# }\n# # pub struct Tweet {\n# pub username: String,\n# pub content: String,\n# pub reply: bool,\n# pub retweet: bool,\n# }\n# # impl Summary for Tweet {\n# fn summarize(&self) -> String {\n# format!(\"{}: {}\", self.username, self.content)\n# }\n# }\n# fn returns_summarizable(switch: bool) -> impl Summary { if switch { NewsArticle { headline: String::from( \"Penguins win the Stanley Cup Championship!\", ), location: String::from(\"Pittsburgh, PA, USA\"), author: String::from(\"Iceburgh\"), content: String::from( \"The Pittsburgh Penguins once again are the best \\ hockey team in the NHL.\", ), } } else { Tweet { username: String::from(\"horse_ebooks\"), content: String::from( \"of course, as you probably already know, people\", ), reply: false, retweet: false, } }\n} NewsArticle, Tweet 중 하나를 반환하는 행위는 impl Trait 문법이 컴파일러 내에 구현된 방식으로 인한 제약 때문에 허용되지 않습니다. 함수가 이런 식으로 동작하도록 만드는 방법은 17장의 ‘트레이트 객체를 사용하여 다른 타입의 값 허용하기’ 절에서 알아볼 예정입니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 트레이트로 공통된 동작을 정의하기 » 트레이트를 구현하는 타입을 반환하기","id":"181","title":"트레이트를 구현하는 타입을 반환하기"},"182":{"body":"제네릭 타입 매개변수를 사용하는 impl 블록에 트레이트 바운드를 이용하면, 지정된 트레이트를 구현하는 타입에 대해서만 메서드를 구현할 수도 있습니다. 예를 들어, 예제 10-15의 Pair 타입은 언제나 새로운 Pair 인스턴스를 반환하는 new 함수를 구현합니다. (5장의 ‘메서드 정의하기’ 절에서 다룬 것처럼 Self는 impl 블록에 대한 타입의 별칭이고, 지금의 경우에는 Pair라는 점을 상기합시다.) 하지만 그다음의 impl 블록에서는 어떤 T 타입이 비교를 가능하게 해주는 PartialOrd 트레이트와 출력을 가능하게 만드는 Display 트레이트를 모두 구현한 타입인 경우에 대해서만 cmp_display 메서드를 구현하고 있습니다. 파일명: src/lib.rs use std::fmt::Display; struct Pair { x: T, y: T,\n} impl Pair { fn new(x: T, y: T) -> Self { Self { x, y } }\n} impl Pair { fn cmp_display(&self) { if self.x >= self.y { println!(\"The largest member is x = {}\", self.x); } else { println!(\"The largest member is y = {}\", self.y); } }\n} 예제 10-15: 트레이트 바운드를 이용해 제네릭 타입에 조건부로 메서드 구현하기 타입이 특정 트레이트를 구현하는 경우에만 해당 타입에 트레이트를 구현할 수도 있습니다. 트레이트 바운드를 만족하는 모든 타입에 대해 트레이트를 구현하는 것을 포괄 구현 (blanket implementations) 이라 하며, 이는 러스트 표준 라이브러리 내에서 광범위하게 사용됩니다. 예를 들어, 표준 라이브러리는 Display 트레이트를 구현하는 모든 타입에 ToString 트레이트도 구현합니다. 표준 라이브러리의 impl 블록은 다음과 비슷하게 생겼습니다: impl ToString for T { // --생략--\n} Display 트레이트가 구현된 모든 타입에서 (ToString 트레이트에 정의된) to_string() 메서드를 호출할 수 있는 건 표준 라이브러리의 이 포괄 구현 덕분입니다. 예를 들어, 정수는 Display를 구현하므로 String 값으로 변환할 수 있습니다: let s = 3.to_string(); 포괄 구현은 트레이트 문서 페이지의 ‘구현자 (Implementors)’ 절에 있습니다. 트레이트와 트레이트 바운드를 사용하면 제네릭 타입 매개변수로 코드 중복을 제거하면서 특정 동작을 하는 제네릭 타입이 필요하다는 사실을 컴파일러에게 전달할 수 있습니다. 컴파일러는 트레이트 바운드를 이용하여 코드에 사용된 구체적인 타입들이 올바른 동작을 제공하는지 검사합니다. 동적 타입 언어에서는 해당 타입이 정의하지 않은 메서드를 호출하면 런타임에 에러가 발생합니다. 하지만 러스트는 컴파일 시점에 에러를 발생시켜 코드를 실행하기도 전에 문제를 해결하도록 강제합니다. 따라서 런타임에 해당 동작을 구현하는지 검사하는 코드를 작성할 필요가 없습니다. 컴파일 시점에 이미 다 확인했기 때문이죠. 러스트는 제네릭의 유연성과 성능 둘 다 놓치지 않습니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 트레이트로 공통된 동작을 정의하기 » 트레이트 바운드를 사용해 조건부로 메서드 구현하기","id":"182","title":"트레이트 바운드를 사용해 조건부로 메서드 구현하기"},"183":{"body":"라이프타임 (lifetime) 은 이미 사용해 본 적 있는 또 다른 종류의 제네릭입니다. 라이프타임은 어떤 타입이 원하는 동작이 구현되어 있음을 보장하기 것이 아니라, 어떤 참조자가 필요한 기간 동안 유효함을 보장하도록 합니다. 4장 ‘참조와 대여’ 절에서 다루지 않은 내용이 있습니다. 러스트의 모든 참조자는 라이프타임 이라는 참조자의 유효성을 보장하는 범위를 갖습니다. 대부분의 상황에서 타입이 암묵적으로 추론되듯, 라이프타임도 암묵적으로 추론됩니다. 하지만 여러 타입이 될 가능성이 있는 상황에서는 타입을 명시해 주어야 하듯, 참조자의 수명이 여러 방식으로 서로 연관될 수 있는 경우에는 라이프타임을 명시해 주어야 합니다. 러스트에서 런타임에 사용되는 실제 참조자가 반드시 유효할 것임을 보장하려면 제네릭 라이프타임 매개변수로 이 관계를 명시해야 합니다. 라이프타임을 명시하는 것은 다른 프로그래밍 언어에서는 찾아보기 어려운 개념이며, 따라서 친숙하지 않은 느낌이 들 것입니다. 이번 장에서 라이프타임의 모든 것을 다루지는 않겠지만, 라이프타임이라는 개념에 익숙해질 수 있도록 여러분이 접하게 될 일반적인 방식의 라이프타임 문법만 다루겠습니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 라이프타임으로 참조자의 유효성 검증하기","id":"183","title":"라이프타임으로 참조자의 유효성 검증하기"},"184":{"body":"라이프타임의 주목적은 댕글링 참조 (dangling reference) 방지입니다. 댕글링 참조는 프로그램이 참조하려고 한 데이터가 아닌 엉뚱한 데이터를 참조하게 되는 원인입니다. 예제 10-16처럼 안쪽 스코프와 바깥쪽 스코프를 갖는 프로그램을 생각해 봅시다: fn main() { let r; { let x = 5; r = &x; } println!(\"r: {}\", r);\n} 예제 10-16: 스코프 밖으로 벗어난 값을 참조하는 코드 Note: 예제 10-16, 10-17, 10-23 예제는 변수를 초깃값 없이 선언하여, 스코프 밖에 변수명을 위치시킵니다. 널 값을 갖지 않는 러스트가 이런 형태의 코드를 허용하는 게 이상하다고 생각하실 수도 있지만, 만약 값을 넣기 전에 변수를 사용하는 코드를 실제로 작성할 경우에는 러스트가 컴파일 에러를 발생시킵니다. 널 값이 허용되는 것은 아닙니다. 바깥쪽 스코프에서는 r 변수를 초깃값 없이 선언하고 안쪽 스코프에서는 x 변수를 초깃값 5로 선언합니다. 안쪽 스코프에서는 r 값에 x 참조자를 대입합니다. 안쪽 스코프가 끝나면 r 값을 출력합니다. 이 코드는 컴파일되지 않습니다. r이 참조하는 값이 사용하려는 시점에 이미 자신의 스코프를 벗어났기 때문입니다. 에러 메시지는 다음과 같습니다: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0597]: `x` does not live long enough --> src/main.rs:6:13 |\n6 | r = &x; | ^^ borrowed value does not live long enough\n7 | } | - `x` dropped here while still borrowed\n8 |\n9 | println!(\"r: {}\", r); | - borrow later used here For more information about this error, try `rustc --explain E0597`.\nerror: could not compile `chapter10` due to previous error 변수 x가 ‘충분히 오래 살지 못했습니다 (does not live long enough).’ x는 안쪽 스코프가 끝나는 7번째 줄에서 스코프를 벗어나지만 r은 바깥쪽 스코프에서 유효하기 때문입니다. 스코프가 더 클수록 ‘더 오래 산다 (lives longer)’고 표현합니다. 만약 러스트가 이 코드의 작동을 허용하면 r은 x가 스코프를 벗어날 때 할당 해제된 메모리를 참조할 테고, r을 이용하는 모든 작업은 제대로 작동하지 않을 것입니다. 그렇다면 러스트는 이 코드가 유효한지를 어떻게 검사할까요? 정답은 대여 검사기입니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 라이프타임으로 댕글링 참조 방지하기","id":"184","title":"라이프타임으로 댕글링 참조 방지하기"},"185":{"body":"러스트 컴파일러는 대여 검사기 (borrow checker) 로 스코프를 비교하여 대여의 유효성을 판단합니다. 예제 10-17은 예제 10-16 코드의 변수 라이프타임을 주석으로 표시한 모습입니다: fn main() { let r; // ---------+-- 'a // | { // | let x = 5; // -+-- 'b | r = &x; // | | } // -+ | // | println!(\"r: {}\", r); // |\n} // ---------+ 예제 10-17: r, x의 라이프타임을 각각 'a, 'b로 표현한 주석 r의 라이프타임은 'a, x의 라이프타임은 'b로 표현했습니다. 보시다시피 안쪽 'b 블록은 바깥쪽 'a 라이프타임 블록보다 작습니다. 러스트는 컴파일 타임에 두 라이프타임의 크기를 비교하고, 'a 라이프타임을 갖는 r이 'b 라이프타임을 갖는 메모리를 참조하고 있음을 인지합니다. 하지만 'b가 'a보다 짧으니, 즉 참조 대상이 참조자보다 오래 살지 못하니 러스트 컴파일러는 이 프로그램을 컴파일하지 않습니다. 예제 10-18은 댕글링 참조를 만들지 않고 정상적으로 컴파일되도록 수정한 코드입니다. fn main() { let x = 5; // ----------+-- 'b // | let r = &x; // --+-- 'a | // | | println!(\"r: {}\", r); // | | // --+ |\n} // ----------+ 예제 10-18: 데이터의 라이프타임이 참조자의 라이프타임보다 길어서 문제없는 코드 여기서 x의 라이프타임 'b는 'a보다 더 깁니다. 러스트는 참조자 r이 유효한 동안에는 x 도 유효하다는 것을 알고 있으므로, r은 x를 참조할 수 있습니다. 참조자의 라이프타임이 무엇인지, 러스트가 어떻게 라이프타임을 분석하여 참조자의 유효성을 보장하는지 알아보았습니다. 이제 함수 매개변수와 반환 값에 대한 제네릭 라이프타임을 알아봅시다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 대여 검사기","id":"185","title":"대여 검사기"},"186":{"body":"두 문자열 슬라이스 중 긴 쪽을 반환하는 함수를 작성해 보겠습니다. 이 함수는 두 문자열 슬라이스를 전달받고 하나의 문자열 슬라이스를 반환합니다. longest 함수를 구현하고 나면 예제 10-19 코드로 The longest string is abcd가 출력되어야 합니다. 파일명: src/main.rs fn main() { let string1 = String::from(\"abcd\"); let string2 = \"xyz\"; let result = longest(string1.as_str(), string2); println!(\"The longest string is {}\", result);\n} 예제 10-19: 두 문자열 슬라이스 중 긴 쪽을 찾기 위해 longest 함수를 호출하는 main 함수 longest 함수가 매개변수의 소유권을 얻지 않도록, 문자열 대신 참조자인 문자열 슬라이스를 전달한다는 점을 주목하세요. 어째서 예제 10-19처럼 문자열을 매개변수로 전달하는지는 4장의 ‘문자열 슬라이스를 매개변수로 사용하기’ 절을 참고해 주세요. 예제 10-20처럼 longest 함수를 구현할 경우, 컴파일 에러가 발생합니다. 파일명: src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"The longest string is {}\", result);\n# }\n# fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y }\n} 예제 10-20: 두 문자열 슬라이스 중 긴 쪽을 반환하는 longest 함수 (컴파일되지 않음) 나타나는 에러는 라이프타임과 관련되어 있습니다: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0106]: missing lifetime specifier --> src/main.rs:9:33 |\n9 | fn longest(x: &str, y: &str) -> &str { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`\nhelp: consider introducing a named lifetime parameter |\n9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`.\nerror: could not compile `chapter10` due to previous error 이 도움말은 반환 타입에 제네릭 라이프타임 매개변수가 필요하다는 내용입니다. 반환할 참조자가 x인지, y인지 러스트가 알 수 없기 때문입니다. 사실 우리도 알 수 없죠. if 블록에서는 x 참조자를 반환하고 else 블록에서는 y 참조자를 반환하니까요. 이 함수를 정의하는 시점에서는 함수가 전달받을 구체적인 값을 알 수 없으니, if의 경우가 실행될지 else의 경우가 실행될지 알 수 없습니다. 전달받은 참조자의 구체적인 라이프타임도 알 수 없습니다. 그러니 예제 10-17, 예제 10-18에서처럼 스코프를 살펴보는 것만으로는 반환할 참조자의 유효성을 보장할 수 없습니다. 대여 검사기도 x, y 라이프타임이 반환 값의 라이프타임과 어떤 연관이 있는지 알지 못하니 마찬가지입니다. 따라서, 참조자 간의 관계를 제네릭 라이프타임 매개변수로 정의하여 대여 검사기가 분석할 수 있도록 해야 합니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 함수에서의 제네릭 라이프타임","id":"186","title":"함수에서의 제네릭 라이프타임"},"187":{"body":"라이프타임을 명시한다고 해서 참조자의 수명이 바뀌진 않습니다. 그보다는 여러 참조자에 대한 수명에 영향을 주지 않으면서 서로 간 수명의 관계가 어떻게 되는지에 대해 기술하는 것입니다. 함수 시그니처에 제네릭 타입 매개변수를 작성하면 어떤 타입이든 전달할 수 있는 것처럼, 함수에 제네릭 라이프타임 매개변수를 명시하면 어떠한 라이프타임을 갖는 참조자라도 전달할 수 있습니다. 라이프타임 명시 문법은 약간 독특합니다. 라이프타임 매개변수의 이름은 어퍼스트로피(')로 시작해야 하며, 보통은 제네릭 타입처럼 매우 짧은 소문자로 정합니다. 대부분의 사람들은 첫 번째 라이프타임을 명시할 때 'a를 사용합니다. 라이프타임 매개변수는 참조자의 & 뒤에 위치하며, 공백을 한 칸 입력하여 참조자의 타입과 분리합니다. 다음은 순서대로 라이프타임 매개변수가 없는 i32 참조자, 라이프타임 매개변수 'a가 있는 i32 참조자, 마찬가지로 라이프타임 매개변수 'a가 있는 가변 참조자에 대한 예시입니다. &i32 // 참조자\n&'a i32 // 명시적인 라이프타임이 있는 참조자\n&'a mut i32 // 명시적인 라이프타임이 있는 가변 참조자 자신의 라이프타임 명시 하나만 있는 것으로는 큰 의미가 없습니다. 라이프타임 명시는 러스트에게 여러 참조자의 제네릭 라이프타임 매개변수가 서로 어떻게 연관되어 있는지 알려주는 용도이기 때문입니다. longest 함수의 컨텍스트에서 라이프타임 명시가 서로에게 어떤 식으로 연관 짓는지 실험해 봅시다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 라이프타임 명시 문법","id":"187","title":"라이프타임 명시 문법"},"188":{"body":"라이프타임 명시를 함수 시그니처에서 사용하기 위해서는 제네릭 타입 매개변수를 사용할 때처럼 함수명과 매개변수 목록 사이의 꺾쇠괄호 안에 제네릭 라이프타임 매개변수를 선언할 필요가 있습니다. 시그니처에서는 다음과 같은 제약사항을 표현하려고 합니다: 두 매개변수의 참조자 모두가 유효한 동안에는 반환된 참조자도 유효할 것이라는 점이지요. 이는 매개변수들과 반환 값 간의 라이프타임 관계입니다. 예제 10-21과 같이 이 라이프타임에 'a라는 이름을 붙여 각 참조자에 추가하겠습니다. 파일명: src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"The longest string is {}\", result);\n# }\n# fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }\n} 예제 10-21: 시그니처 내 모든 참조자가 동일한 라이프타임 'a를 가져야 함을 나타낸 longest 함수 정의 이 코드는 정상적으로 컴파일되며, 예제 10-19의 main 코드로 실행하면 우리가 원했던 결과가 나옵니다. 이 함수 시그니처는 러스트에게, 함수는 두 매개변수를 갖고 둘 다 적어도 라이프타임 'a만큼 살아있는 문자열 슬라이스이며, 반환하는 문자열 슬라이스도 라이프타임 'a만큼 살아있다는 정보를 알려줍니다. 이것의 실제 의미는, longest 함수가 반환하는 참조자의 라이프타임은 함수 인수로서 참조된 값들의 라이프타임 중 작은 것과 동일하다는 의미입니다. 이러한 관계가 바로 러스트로 하여금 이 코드를 분석할 때 사용하도록 만들고 싶었던 것입니다. 함수 시그니처에 라이프타임 매개변수를 지정한다고 해서, 전달되는 값이나 반환 값의 라이프타임이 변경되는 건 아니라는 점을 기억해 두세요. 어떤 값이 제약 조건을 지키지 않았을 때 대여 검사기가 불합격 판정을 내릴 수 있도록 명시할 뿐입니다. longest 함수는 x와 y가 얼마나 오래 살지 정확히 알 필요는 없고, 이 시그니처를 만족하는 어떤 스코프를 'a로 대체할 수 있다는 점만 알면 됩니다. 라이프타임을 함수에 명시할 때는 함수 본문이 아닌, 함수 시그니처에 적습니다. 라이프타임 명시는 함수 시그니처의 타입들과 마찬가지로 함수에 대한 계약서의 일부가 됩니다. 함수 시그니처가 라이프타임 계약을 가지고 있다는 것은 러스트 컴파일러가 수행하는 분석이 좀 더 단순해질 수 있음을 의미합니다. 만일 함수가 명시된 방법이나 함수가 호출된 방법에 문제가 있다면, 컴파일러 에러가 해당 코드의 지점과 제약사항을 좀 더 정밀하게 짚어낼 수 있습니다. 그렇게 하는 대신 러스트 컴파일러가 라이프타임 간의 관계에 대해 개발자가 의도한 바를 더 많이 추론했다면, 컴파일러는 문제의 원인에서 몇 단계 떨어진 코드의 사용만을 짚어내는 것밖에는 할 수 없을지도 모릅니다. longest 함수에 구체적인 참조자들이 넘겨질 때 'a에 대응되는 구체적인 라이프타임은 x 스코프와 y 스코프가 겹치는 부분입니다. 바꿔 말하면, x 라이프타임과 y 라이프타임 중 더 작은 쪽이 제네릭 라이프타임 'a의 구체적인 라이프타임이 됩니다. 반환하는 참조자도 동일한 라이프타임 매개변수 'a를 명시했으므로, x, y 중 더 작은 라이프타임 내에서는 longest가 반환한 참조자의 유효함을 보장할 수 있습니다. 서로 다른 구체적인 라이프타임을 가진 참조자를 longest 함수에 넘겨보면서, 라이프타임 명시가 어떤 효과를 내는지 알아봅시다. 예제 10-22에 간단한 예제가 있습니다. 파일명: src/main.rs fn main() { let string1 = String::from(\"long string is long\"); { let string2 = String::from(\"xyz\"); let result = longest(string1.as_str(), string2.as_str()); println!(\"The longest string is {}\", result); }\n}\n# # fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n# if x.len() > y.len() {\n# x\n# } else {\n# y\n# }\n# } 예제 10-22: 서로 다른 구체적인 라이프타임을 가진 String 값의 참조자로 longest 함수 호출하기 string1은 바깥쪽 스코프가 끝나기 전까지, string2는 안쪽 스코프가 끝나기 전까지 유효합니다. result는 안쪽 스코프가 끝나기 전까지 유효한 무언가를 참조합니다. 대여 검사기는 이 코드를 문제 삼지 않습니다. 실행하면 The longest string is long string is long이 출력됩니다. 다음은 두 인수 중 하나의 라이프타임이 result 참조자의 라이프타임보다 작을 경우입니다. result 변수의 선언을 안쪽 스코프에서 밖으로 옮기고, 값의 대입은 string2가 있는 안쪽 스코프에 남겨보겠습니다. 그리고 result를 사용하는 println! 구문을 안쪽 스코프가 끝나고 난 이후의 바깥쪽 스코프로 옮겨보겠습니다. 이렇게 수정한 예제 10-23 코드는 컴파일할 수 없습니다. 파일명: src/main.rs fn main() { let string1 = String::from(\"long string is long\"); let result; { let string2 = String::from(\"xyz\"); result = longest(string1.as_str(), string2.as_str()); } println!(\"The longest string is {}\", result);\n}\n# # fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {\n# if x.len() > y.len() {\n# x\n# } else {\n# y\n# }\n# } 예제 10-24: string2가 스코프 밖으로 벗어나고 나서 result 사용해 보기 컴파일하면 다음과 같은 에러가 발생합니다: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0597]: `string2` does not live long enough --> src/main.rs:6:44 |\n6 | result = longest(string1.as_str(), string2.as_str()); | ^^^^^^^^^^^^^^^^ borrowed value does not live long enough\n7 | } | - `string2` dropped here while still borrowed\n8 | println!(\"The longest string is {}\", result); | ------ borrow later used here For more information about this error, try `rustc --explain E0597`.\nerror: could not compile `chapter10` due to previous error 이 에러는 println! 구문에서 result가 유효하려면 string2 가 바깥쪽 스코프가 끝나기 전까지 유효해야 한다는 내용입니다. 함수 매개변수와 반환 값에 모두 동일한 라이프타임 매개변수 'a를 명시했으므로, 러스트는 문제를 정확히 파악할 수 있습니다. 사실 우리 눈으로 보기에는 코드에 문제가 없어 보입니다. string1의 문자열이 string2 보다 더 기니까 result는 string1을 참조하게 될 테고, println! 구문을 사용하는 시점에 string1의 참조자는 유효하니까요. 하지만 컴파일러는 이 점을 알아챌 수 없습니다. 러스트가 전달받은 것은 ‘longest 함수가 반환할 참조자의 라이프타임은 매개변수의 라이프타임 중 작은 것과 동일하다’라는 내용이었으니, 대여 검사기는 예제 10-23 코드가 잠재적으로 유효하지 않은 참조자를 가질 수도 있다고 판단합니다. longest 함수에 다양한 값, 다양한 라이프타임의 참조자를 넘겨보고, 반환한 참조자를 여러 방식으로 사용해 보세요. 컴파일하기 전에 코드가 대여 검사기를 통과할 수 있을지 혹은 없을지 예상해 보고, 여러분의 생각이 맞았는지 확인해 보세요!","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 함수 시그니처에서 라이프타임 명시하기","id":"188","title":"함수 시그니처에서 라이프타임 명시하기"},"189":{"body":"라이프타임 매개변수 명시의 필요성은 함수가 어떻게 동작하는지에 따라서 달라집니다. 예를 들어, longest 함수를 제일 긴 문자열 슬라이스를 반환하는 게 아니라, 항상 첫 번째 매개변수를 반환하도록 바꾸었다고 가정해 봅시다. 그러면 이제 y 매개변수에는 라이프타임을 지정할 필요가 없습니다. 다음 코드는 정상적으로 컴파일됩니다: 파일명: src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"efghijklmnopqrstuvwxyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"The longest string is {}\", result);\n# }\n# fn longest<'a>(x: &'a str, y: &str) -> &'a str { x\n} 매개변수 x와 반환 타입에만 라이프타임 매개변수 'a가 지정되어 있습니다. y의 라이프타임은 x나 반환 값의 라이프타임과 전혀 관계없으므로, 매개변수 y에는 'a를 지정하지 않았습니다. 참조자를 반환하는 함수를 작성할 때는 반환 타입의 라이프타임 매개변수가 함수 매개변수 중 하나와 일치해야 합니다. 반환할 참조자가 함수 매개변수중 하나를 참조하지 않을 유일한 가능성은 함수 내부에서 만들어진 값의 참조자를 반환하는 경우입니다. 하지만 이 값은 함수가 끝나는 시점에 스코프를 벗어나므로 댕글링 참조가 될 것입니다. 다음과 같이 longest 함수를 구현하면 컴파일할 수 없습니다: 파일명: src/main.rs # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest(string1.as_str(), string2);\n# println!(\"The longest string is {}\", result);\n# }\n# fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from(\"really long string\"); result.as_str()\n} 반환 타입에 'a를 지정했지만, 반환 값의 라이프타임이 그 어떤 매개변수와도 관련 없으므로 컴파일할 수 없습니다. 나타나는 에러 메시지는 다음과 같습니다: $ cargo run Compiling chapter10 v0.1.0 (file:///projects/chapter10)\nerror[E0515]: cannot return reference to local variable `result` --> src/main.rs:11:5 |\n11 | result.as_str() | ^^^^^^^^^^^^^^^ returns a reference to data owned by the current function For more information about this error, try `rustc --explain E0515`.\nerror: could not compile `chapter10` due to previous error result는 longest 함수가 끝나면서 스코프를 벗어나 정리되는데, 함수에서 result의 참조자를 반환하려고 하니 문제가 발생합니다. 여기서 댕글링 참조가 발생하지 않도록 라이프타임 매개변수를 지정할 방법은 없습니다. 그리고 러스트는 댕글링 참조를 생성하는 코드를 눈감아주지 않죠. 이런 상황을 해결하는 가장 좋은 방법은 참조자 대신 값의 소유권을 갖는 데이터 타입을 반환하여 함수를 호출한 함수 측에서 값을 정리하도록 하는 것입니다. 라이프타임 문법의 근본적인 역할은 함수의 다양한 매개변수와 반환 값의 라이프타임을 연결하는 데에 있습니다. 한번 라이프타임을 연결해 주고 나면, 러스트는 해당 정보를 이용해 댕글링 포인터 생성을 방지하고, 메모리 안전 규칙을 위배하는 연산을 배제합니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 라이프타임의 측면에서 생각하기","id":"189","title":"라이프타임의 측면에서 생각하기"},"19":{"body":"러스트 설치 시 로컬 문서 (local documentation) 도 같이 설치됩니다. 오프라인 상태로도 이용할 수 있으며, rustup doc 명령어로 여러분의 브라우저에서 열어볼 수 있습니다. 표준 라이브러리에서 제공하는 타입이나 함수 중 이게 무슨 기능을 하는지나 사용하는 법을 모르겠다면 API (Application Programming Language) 문서에서 모르는 내용을 찾아볼 수도 있겠죠?","breadcrumbs":"시작해봅시다 » 러스트 설치 » 로컬 문서","id":"19","title":"로컬 문서"},"190":{"body":"여태껏 정의해 본 구조체들은 모두 소유권이 있는 타입을 들고 있었습니다. 구조체가 참조자를 들고 있도록 할 수도 있지만, 이 경우 구조체 정의 내 모든 참조자에 라이프타임을 명시해야합니다. 예제 10-24는 문자열 슬라이스를 보유하는 ImportantExcerpt 구조체를 나타냅니다: 파일명: src/main.rs struct ImportantExcerpt<'a> { part: &'a str,\n} fn main() { let novel = String::from(\"Call me Ishmael. Some years ago...\"); let first_sentence = novel.split('.').next().expect(\"Could not find a '.'\"); let i = ImportantExcerpt { part: first_sentence, };\n} 예제 10-24: 참조자를 보유하여 라이프타임 명시가 필요한 구조체 이 구조체에는 문자열 슬라이스를 보관하는 part 참조자 필드가 하나 있습니다. 구조체의 제네릭 라이프타임 매개변수의 선언 방법은 제네릭 데이터 타입과 마찬가지로, 제네릭 라이프타임 매개변수의 이름을 구조체 이름 뒤 꺾쇠괄호 내에 선언하고 구조체 정의 본문에서 라이프타임 매개변수를 이용합니다. 예제 10-25의 라이프타임 명시는 ‘ImportantExcerpt 인스턴스는 part 필드가 보관하는 참조자의 라이프타임보다 오래 살 수 없다’라는 의미입니다. main 함수에서는 novel 변수가 소유하는 String의 첫 문장에 대한 참조자로 ImportantExcerpt 구조체를 생성합니다. novel 데이터는 ImportantExcerpt 인스턴스가 생성되기 전부터 존재하며, ImportantExcerpt 인스턴스가 스코프를 벗어나기 전에는 novel이 스코프를 벗어나지도 않으니, ImportantExcerpt 인스턴스는 유효합니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 구조체 정의에서 라이프타임 명시하기","id":"190","title":"구조체 정의에서 라이프타임 명시하기"},"191":{"body":"모든 참조자는 라이프타임을 가지며, 참조자를 사용하는 함수나 구조체는 라이프타임 매개변수를 명시해야 함을 배웠습니다. 하지만 4장에서 본 예제 4-9의 함수는, 예제 10-25에서 다시 보여 드리겠지만, 라이프타임 명시가 없었는데도 컴파일 할 수 있었습니다. 파일명: src/lib.rs fn first_word(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[0..i]; } } &s[..]\n}\n# # fn main() {\n# let my_string = String::from(\"hello world\");\n# # // first_word works on slices of `String`s\n# let word = first_word(&my_string[..]);\n# # let my_string_literal = \"hello world\";\n# # // first_word works on slices of string literals\n# let word = first_word(&my_string_literal[..]);\n# # // Because string literals *are* string slices already,\n# // this works too, without the slice syntax!\n# let word = first_word(my_string_literal);\n# } 예제 10-25: 4장에서 정의했던, 매개변수, 반환 타입이 참조자인데도 라이프타임 명시 없이 컴파일 가능한 함수 이 함수에 라이프타임을 명시하지 않아도 컴파일 할 수 있는 이유는 러스트의 역사에서 찾아볼 수 있습니다. 초기 버전(1.0 이전) 러스트에서는 이 코드를 컴파일할 수 없었습니다. 모든 참조자는 명시적인 라이프타임이 필요했었죠. 그 당시 함수 시그니처는 다음과 같이 작성했습니다: fn first_word<'a>(s: &'a str) -> &'a str { 수많은 러스트 코드를 작성하고 난 후, 러스트 팀은 러스트 프로그래머들이 특정한 상황에서 똑같은 라이프타임 명시를 계속 똑같이 작성하고 있다는 걸 알아냈습니다. 이 상황들은 예측 가능한 상황들이었으며, 몇 가지 결정론적인 (deterministic) 패턴을 따르고 있었습니다. 따라서 러스트 팀은 컴파일러 내에 이 패턴들을 프로그래밍하여, 이러한 상황들에서는 라이프타임을 명시하지 않아도 대여 검사기가 추론할 수 있도록 하였습니다. 앞으로 더 많은 결정론적 패턴이 컴파일러에 추가될 가능성이 있다는 사실은 이러한 러스트의 역사와 관련되어 있습니다. 나중에는 라이프타임 명시가 필요한 상황이 더욱 적어질지도 모르지요. 러스트의 참조자 분석 기능에 프로그래밍 된 이 패턴들을 라이프타임 생략 규칙 (lifetime elision rules) 이라고 부릅니다. 이 규칙은 프로그래머가 따라야 하는 규칙이 아닙니다. 그저 컴파일러가 고려하는 특정한 사례의 모음이며, 여러분의 코드가 이에 해당할 경우 라이프타임을 명시하지 않아도 될 따름입니다. 생략 규칙이 완전한 추론 기능을 제공하는 것은 아닙니다. 만약 러스트가 이 규칙들을 적용했는데도 라이프타임이 모호한 참조자가 있다면, 컴파일러는 이 참조자의 라이프타임을 추측하지 않습니다. 컴파일러는 추측 대신 에러를 발생시켜서, 여러분이 라이프타임 명시를 추가하여 문제를 해결하도록 할 것입니다. 먼저 몇 가지를 정의하겠습니다. 함수나 메서드 매개변수의 라이프타임은 입력 라이프타임 (input lifetime) 이라 하며, 반환 값의 라이프타임은 출력 라이프타임 (output lifetime) 이라 합니다. 라이프타임 명시가 없을 때 컴파일러가 참조자의 라이프타임을 알아내는 데 사용하는 규칙은 세 개입니다. 첫 번째 규칙은 입력 라이프타임에 적용되고, 두 번째 및 세 번째 규칙은 출력 라이프타임에 적용됩니다. 세 가지 규칙을 모두 적용했음에도 라이프타임을 알 수 없는 참조자가 있다면 컴파일러는 에러와 함께 작동을 멈춥니다. 이 규칙은 fn 정의는 물론 impl 블록에도 적용됩니다. 첫 번째 규칙은, 컴파일러가 참조자인 매개변수 각각에게 라이프타임 매개변수를 할당한다는 것입니다. fn foo<'a>(x: &'a i32)처럼 매개변수가 하나인 함수는 하나의 라이프타임 매개변수를 갖고, fn foo<'a, 'b>(x: &'a i32, y: &'b i32)처럼 매개변수가 두 개인 함수는 두 개의 개별 라이프타임 매개변수를 갖는 식입니다. 두 번째 규칙은, 만약 입력 라이프타임 매개변수가 딱 하나라면, 해당 라이프타임이 모든 출력 라이프타임에 대입된다는 것입니다: fn foo<'a>(x: &'a i32) -> &'a i32처럼 말이지요. 세 번째 규칙은, 입력 라이프타임 매개변수가 여러 개인데, 그중 하나가 &self나 &mut self라면, 즉 메서드라면 self의 라이프타임이 모든 출력 라이프타임 매개변수에 대입됩니다. 이 규칙은 메서드 코드를 깔끔하게 만드는 데 기여합니다. 한번 우리가 컴파일러라고 가정해 보고, 예제 10-25의 first_word 함수 시그니처 속 참조자의 라이프타임을 이 규칙들로 알아내 봅시다. 시그니처는 참조자에 관련된 어떤 라이프타임 명시도 없이 시작됩니다: fn first_word(s: &str) -> &str { 첫 번째 규칙을 적용해, 각각의 매개변수에 라이프타임을 지정해 봅시다. 평범하게 'a라고 해보죠. 시그니처는 이제 다음과 같습니다: fn first_word<'a>(s: &'a str) -> &str { 입력 라이프타임이 딱 하나밖에 없으니 두 번째 규칙을 적용합니다. 두 번째 규칙대로 출력 라이프타임에 입력 매개변수의 라이프타임을 대입하고 나면, 시그니처는 다음과 같습니다: fn first_word<'a>(s: &'a str) -> &'a str { 함수 시그니처의 모든 참조자가 라이프타임을 갖게 됐으니, 컴파일러는 프로그래머에게 이 함수의 라이프타임 명시를 요구하지 않고도 계속 코드를 분석할 수 있습니다. 이번엔 다른 예제로 해보죠. 예제 10-20에서의 아무런 라이프타임 매개변수 없는 longest 함수를 이용해 보겠습니다: fn longest(x: &str, y: &str) -> &str { 첫 번째 규칙을 적용해, 각각의 매개변수에 라이프타임을 지정해 봅시다. 이번에는 매개변수가 두 개니, 두 개의 라이프타임이 생깁니다. fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { 입력 라이프타임이 하나가 아니므로 두 번째 규칙은 적용하지 않습니다. longest 함수는 메서드가 아니니, 세 번째 규칙도 적용할 수 없습니다. 세 가지 규칙을 모두 적용했는데도 반환 타입의 라이프타임을 알아내지 못했습니다. 예제 10-20의 코드를 컴파일하면 에러가 발생하는 이유가 바로 이 때문입니다. 컴파일러가 라이프타임 생략 규칙을 적용해 보았지만, 이 시그니처 안에 있는 모든 참조자의 라이프타임을 알아내지 못했습니다. 세 번째 규칙은 메서드 시그니처에만 적용되니, 메서드에서의 라이프타임을 살펴보고, 왜 세 번째 규칙 덕분에 메서드 시그니처의 라이프타임을 자주 생략할 수 있는지 알아봅시다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 라이프타임 생략","id":"191","title":"라이프타임 생략"},"192":{"body":"라이프타임을 갖는 메서드를 구조체에 구현하는 문법은 예제 10-11에서 본 제네릭 타입 매개변수 문법과 같습니다. 라이프타임 매개변수의 선언 및 사용 위치는 구조체 필드나 메서드 매개변수 및 반환 값과 연관이 있느냐 없느냐에 따라 달라집니다. 라이프타임이 구조체 타입의 일부가 되기 때문에, 구조체 필드의 라이프타임 이름은 impl 키워드 뒤에 선언한 다음 구조체 이름 뒤에 사용해야 합니다. impl 블록 안에 있는 메서드 시그니처의 참조자들은 구조체 필드에 있는 참조자의 라이프타임과 관련되어 있을 수도 있고, 독립적일 수도 있습니다. 또한 라이프타임 생략 규칙으로 인해 메서드 시그니처에 라이프타임을 명시하지 않아도 되는 경우도 있습니다. 예제 10-24의 ImportantExcerpt 구조체로 예시를 들어보겠습니다. 먼저 level이라는 메서드가 있습니다. 이 메서드의 매개변수는 self 참조자 하나뿐이며, 반환 값은 참조자가 아닌 그냥 i32 값입니다. # struct ImportantExcerpt<'a> {\n# part: &'a str,\n# }\n# impl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 }\n}\n# # impl<'a> ImportantExcerpt<'a> {\n# fn announce_and_return_part(&self, announcement: &str) -> &str {\n# println!(\"Attention please: {}\", announcement);\n# self.part\n# }\n# }\n# # fn main() {\n# let novel = String::from(\"Call me Ishmael. Some years ago...\");\n# let first_sentence = novel.split('.').next().expect(\"Could not find a '.'\");\n# let i = ImportantExcerpt {\n# part: first_sentence,\n# };\n# } impl 뒤에서 라이프타임 매개변수를 선언하고 타입명 뒤에서 사용하는 과정은 필수적이지만, 첫 번째 생략 규칙으로 인해 self 참조자의 라이프타임을 명시할 필요는 없습니다. 다음은 세 번째 라이프타임 생략 규칙이 적용되는 예시입니다: # struct ImportantExcerpt<'a> {\n# part: &'a str,\n# }\n# # impl<'a> ImportantExcerpt<'a> {\n# fn level(&self) -> i32 {\n# 3\n# }\n# }\n# impl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!(\"Attention please: {}\", announcement); self.part }\n}\n# # fn main() {\n# let novel = String::from(\"Call me Ishmael. Some years ago...\");\n# let first_sentence = novel.split('.').next().expect(\"Could not find a '.'\");\n# let i = ImportantExcerpt {\n# part: first_sentence,\n# };\n# } 두 개의 입력 라이프타임이 있으니, 러스트는 첫 번째 라이프타임 생략 규칙대로 &self, announcement에 각각의 라이프타임을 부여합니다. 그다음, 매개변수 중 하나가 &self이니 반환 타입에 &self의 라이프타임을 부여합니다. 이제 모든 라이프타임이 추론되었네요.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 메서드 정의에서 라이프타임 명시하기","id":"192","title":"메서드 정의에서 라이프타임 명시하기"},"193":{"body":"정적 라이프타임 (static lifetime), 즉 'static이라는 특별한 라이프타임을 다뤄봅시다. 'static 라이프타임은 해당 참조자가 프로그램의 전체 생애주기 동안 살아있음을 의미합니다. 모든 문자열 리터럴은 'static 라이프타임을 가지며, 다음과 같이 명시할 수 있습니다. let s: &'static str = \"I have a static lifetime.\"; 이 문자열의 텍스트는 프로그램의 바이너리 내에 직접 저장되기 때문에 언제나 이용할 수 있습니다. 따라서 모든 문자열 리터럴의 라이프타임은 'static입니다. 'static 라이프타임을 이용하라는 제안이 담긴 에러 메시지를 보시게 될 수도 있습니다. 하지만 어떤 참조자를 'static으로 지정하기 전에 해당 참조자가 반드시 프로그램의 전체 라이프타임동안 유지되어야만 하는 참조자인지, 그리고 그것이 진정 원하는 것인지 고민해 보라고 당부하고 싶습니다. 'static 라이프타임을 제안하는 에러 메시지는 대부분의 경우 댕글링 참조를 만들다가 발생하거나, 사용 가능한 라이프타임이 잘못 짝지어져서 발생합니다. 이러한 경우 바람직한 해결책은 그런 문제를 고치는 것이지, 'static 라이프타임이 아닙니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 정적 라이프타임","id":"193","title":"정적 라이프타임"},"194":{"body":"제네릭 타입 매개변수, 트레이트 바운드, 라이프타임이 문법이 함수 하나에 전부 들어간 모습을 살펴봅시다! # fn main() {\n# let string1 = String::from(\"abcd\");\n# let string2 = \"xyz\";\n# # let result = longest_with_an_announcement(\n# string1.as_str(),\n# string2,\n# \"Today is someone's birthday!\",\n# );\n# println!(\"The longest string is {}\", result);\n# }\n# use std::fmt::Display; fn longest_with_an_announcement<'a, T>( x: &'a str, y: &'a str, ann: T,\n) -> &'a str\nwhere T: Display,\n{ println!(\"Announcement! {}\", ann); if x.len() > y.len() { x } else { y }\n} 예제 10-21에서 본 두 개의 문자열 슬라이스 중 긴 쪽을 반환하는 longest 함수입니다. 하지만 이번에는 where 구문에 명시한 바와 같이 Display 트레이트를 구현하는 제네릭 타입 T에 해당하는 ann 매개변수를 추가했습니다. 이 추가 매개변수는 {}를 사용하여 출력될 것인데, 이 때문에 Display 트레이트 바운드가 필요합니다. 라이프타임은 제네릭의 일종이므로, 함수명 뒤의 꺾쇠괄호 안에는 라이프타임 매개변수 'a 선언과 제네릭 타입 매개변수 T가 함께 나열되어 있습니다.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 제네릭 타입 매개변수, 트레이트 바운드, 라이프타임을 한 곳에 사용해 보기","id":"194","title":"제네릭 타입 매개변수, 트레이트 바운드, 라이프타임을 한 곳에 사용해 보기"},"195":{"body":"이번 장에서는 정말 많은 내용을 배웠네요! 여러분은 제네릭 타입 매개변수, 트레이트, 트레이트 바운드, 제네릭 라이프타임 매개변수를 배웠습니다. 이제 다양한 상황에 맞게 작동하는 코드를 중복 없이 작성할 수 있겠군요. 제네릭 타입 매개변수로는 다양한 타입으로 작동하는 코드를 작성할 수 있고, 트레이트와 트레이트 바운드로는 제네릭 타입을 다루면서도 코드에서 필요한 특정 동작을 보장할 수 있습니다. 라이프타임을 명시하면 이런 유연한 코드를 작성하면서도 댕글링 참조가 발생할 일이 없습니다. 그리고, 이 모든 것들은 컴파일 타임에 분석되어 런타임 성능에 전혀 영향을 주지 않습니다! 이번 장에서 다룬 주제들에서 더 배울 내용이 남았다고 하면 믿어지시나요? 17장에서는 트레이트를 사용하는 또 다른 방법인 트레이트 객체 (trait object) 를 다룰 예정입니다. 매우 고급 시나리오 상에서만 필요하게 될, 라이프타임 명시에 관한 더 복잡한 시나리오도 있습니다. 이와 관련해서는 러스트 참고 자료 문서 를 읽으셔야 합니다. 하지만 일단 다음 장에서는 러스트에서 여러분의 코드가 원하는 대로 작동함을 보장할 수 있도록 해주는 코드 테스트 작성 방법을 배워보도록 하죠.","breadcrumbs":"제네릭 타입, 트레이트, 라이프타임 » 라이프타임으로 참조자의 유효성 검증하기 » 정리","id":"195","title":"정리"},"196":{"body":"에츠허르 다익스트라 (Edsger W. Dijkstra) 는 1972년 자신의 에세이 ‘겸손한 프로그래머 (The Humble Programmer)’에서 ‘프로그램 테스트는 버그의 존재를 보여주는 데에는 매우 효율적인 방법일 수 있지만, 버그의 부재를 보여주기에는 절망적으로 부적절하다’라고 말했습니다. 그렇다고 해서 가능한 많은 테스트를 시도하지 말아야 한다는 뜻은 아닙니다! 프로그램의 정확성은 곧 ‘프로그램이 얼마나 의도한 대로 작동하는가’와 같습니다. 러스트는 프로그램의 정확성에 굉장히 신경을 써서 설계된 언어지만, 정확성을 증명하기란 어렵고 복잡합니다. 러스트 타입 시스템이 이 역할의 큰 부담을 해소해 주고 있으나 타입 시스템만으로 모든 문제를 잡아내지는 못합니다. 따라서 러스트는 언어 자체적으로 자동화된 소프트웨어 테스트 작성을 지원합니다. 전달받은 숫자에 2를 더하는 add_two 함수를 작성한다고 칩시다. 함수 시그니처는 매개변수로 정수를 전달받고, 결과로 정수를 반환합니다. 이 함수를 구현하고 컴파일할 때 러스트는 앞서 배운 타입 검사 및 대여 검사를 수행합니다. 함수에 String 값이나 유효하지 않은 참조자가 전달될 일이 없도록 보장해 주죠. 하지만 러스트는 함수가 의도대로 작동하는지에 대해서는 검사할 수 없습니다. 함수가 매개변수에 2를 더하지 않고, 10을 더하거나 50을 빼서 반환해도 모를 일입니다! 이런 경우에 테스트를 도입합니다. 예를 들어 add_two 함수에 3을 전달하면 5가 반환될 것임을 단언 (assert) 하는 테스트를 작성하고 코드를 수정할 때마다 테스트를 실행하면, 제대로 작동하던 기존 코드에 문제가 생기지 않았을지 걱정할 필요가 없습니다. 테스트는 복잡한 기술입니다. 이번 장에서 좋은 테스트를 작성하는 방법에 대한 모든 것을 전부 다룰 수는 없습니다. 이번 장은 러스트의 테스트 메커니즘을 설명합니다. 테스트 작성 시 사용하는 어노테이션, 매크로를 배우고, 테스트 실행 시의 기본 동작과 실행 옵션, 유닛 테스트와 통합 테스트를 조직화하는 방법을 배워보도록 하죠.","breadcrumbs":"자동화 테스트 작성하기 » 자동화 테스트 작성하기","id":"196","title":"자동화 테스트 작성하기"},"197":{"body":"테스트란, 테스트할 코드가 의도대로 기능하는지 검증하는 함수입니다. 테스트 함수는 보통 본문에서 세 가지 동작을 수행합니다. 필요한 데이터나 상태 설정 테스트할 코드 실행 의도한 결과가 나오는지 확인 test 속성 (attribute), 몇 가지 매크로, should_panic 속성을 포함하여 위 세 가지 동작을 수행하는 테스트를 위해 러스트가 특별히 제공하는 기능을 살펴봅시다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 작성 방법 » 테스트 작성 방법","id":"197","title":"테스트 작성 방법"},"198":{"body":"간단히 말해서, 러스트에서 테스트란 test 속성이 어노테이션된 함수입니다. 속성은 러스트 코드 조각에 대한 메타데이터입니다. 앞서 5장에서 구조체에 사용했던 derive도 속성 중 하나입니다. 함수의 fn 이전 줄에 #[test]를 추가하면 테스트 함수로 변경됩니다. 테스트는 cargo test 명령어로 실행되며, 이 명령을 실행하면 러스트는 속성이 표시된 함수를 실행하고 결과를 보고하는 테스트 실행 바이너리를 빌드합니다. 카고로 새 라이브러리 프로젝트를 생성할 때마다 테스트 함수가 포함된 테스트 모듈이 자동 생성됩니다. 이 모듈이 테스트 작성을 위한 템플릿을 제공하므로, 새 프로젝트를 시작할 때마다 정확한 구조 및 테스트 함수 문법을 찾아볼 필요는 없습니다. 테스트 모듈과 테스트 함수는 여러분이 원하는 만큼 추가할 수 있습니다! 어떤 코드를 실제로 테스트해 보기 전에, 먼저 이 템플릿 테스트를 가지고 실험해 보면서 테스트가 어떻게 작동하는지 알아보겠습니다. 그다음 실제로 우리가 작성한 코드가 제대로 작동하는지 확인하는 테스트를 직접 작성해 보겠습니다. 두 숫자를 더하는 adder라는 라이브러리 프로젝트를 생성해 봅시다: $ cargo new adder --lib Created library `adder` project\n$ cd adder adder 라이브러리의 src/lib.rs 파일 내용은 다음과 같습니다. 파일명: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); }\n} 예제 11-1: cargo new 명령어로 자동 생성된 테스트 모듈과 함수 맨 위 두 줄은 무시하고 함수에 집중합시다. #[test] 어노테이션을 주목해 주세요: 이 속성은 해당 함수가 테스트 함수임을 표시하며, 테스트 실행기는 이 표시를 보고 해당 함수를 테스트로 다룰 수 있게 됩니다. tests 모듈 내에는 테스트 함수뿐만 아니라, 일반적인 시나리오를 설정하거나 자주 쓰이는 연산을 수행하는 일반 함수도 작성하기도 하므로, 어떤 함수가 테스트 함수인지 항상 표시해 줘야 합니다. 예제 함수 본문에서는 assert_eq! 매크로를 사용하여 result에 대한 단언 (assert) 을 했는데, 이 변수의 내용물이 2와 2를 더한 결과인 4와 같다는 것입니다. 이 단언 코드는 일반적인 테스트 형식 예제로써 제공됩니다. 한번 테스트를 실행해 이 테스트가 통과되는지 확인해 보죠. cargo test 명령어는 프로젝트 내 모든 테스트를 실행합니다. 결과는 예제 11-2처럼 나타납니다. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.57s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 예제 11-2: 자동 생성된 테스트 실행 결과 카고가 테스트를 컴파일하고 실행했습니다. running 1 test 줄이 보입니다. 그다음 줄에는 생성된 테스트 함수의 이름 it_works와 테스트 실행 결과 ok가 표시됩니다. 전체 요약 test result: ok.는 모든 테스트가 통과됐다는 뜻이고, 1 passed; 0 failed라는 부분은 통과하거나 실패한 테스트 개수를 종합합니다. 어떤 테스트를 무시하도록 표시하여 특정 인스턴스에서는 실행되지 않도록 할 수도 있습니다; 이에 대해서는 이 장의 ‘특별 요청이 없다면 일부 테스트 무시하기’ 절에서 다루겠습니다. 이번 예제에는 그런 게 없었으므로, 요약에는 0 ignored가 표시됩니다. 또한 cargo test에 인수를 넘겨서 어떤 문자열과 이름이 일치하는 테스트만 실행하도록 할 수도 있습니다; 이것을 필터링 (filtering) 이라고 하고, ‘이름을 지정해 일부 테스트만 실행하기’ 절에서 다룰 예정입니다. 지금의 테스트에서는 필터링도 없었으므로, 요약의 끝부분에 0 filtered out이 표시됩니다. 0 measured 통계는 성능 측정 벤치마크 테스트용입니다. 이 내용이 작성된 시점을 기준으로, 벤치마크 테스트는 러스트 나이틀리 (nightly) 에서만 사용 가능합니다. 자세한 내용은 벤치마크 테스트 문서 를 참고해 주세요. 테스트 출력 결과 중 Doc-tests adder로 시작하는 부분은 문서 테스트 결과를 나타냅니다. 아직 문서 테스트를 작성해 보진 않았지만, 러스트는 API 문서에 작성해 놓은 예제 코드도 컴파일 할 수 있습니다. 러스트의 이 기능은 작성한 코드와 문서의 내용이 달라지지 않도록 유지보수하는 데에 매우 유용하답니다! 문서 테스트 작성 방법은 14장의 ‘테스트로서의 문서화 주석’ 절에서 배울 예정입니다. 지금은 일단 Doc-tests 출력을 무시하겠습니다. 현재의 요구사항에 맞게 테스트의 커스터마이징을 시작해 봅시다. 먼저 다음과 같이 it_works 함수의 이름을 exploration 같은 다른 이름으로 변경해 봅시다: 파일명: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); }\n} cargo test를 다시 실행하면 출력 결과에 it_works 대신 exploration이 나타납니다. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.59s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::exploration ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 이제 다른 테스트를 추가하는데, 이번엔 테스트가 실패하도록 만들어 보죠! 테스트 함수 내에서 패닉이 발생하면 테스트는 실패합니다. 각각의 테스트는 새로운 스레드에서 실행되며, 메인 스레드에서 테스트 스레드가 죽은 것을 알게 되면 해당 테스트는 실패한 것으로 처리됩니다. 9장에서, 가장 쉽게 패닉을 일으키는 방법은 panic 매크로를 호출하는 것이라고 이야기했습니다. 예제 11-3처럼 src/lib.rs 파일에 another라는 테스트를 새로 추가해 봅시다. 파일명: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { panic!(\"Make this test fail\"); }\n} 예제 11-3: panic! 매크로를 호출하여 실패하도록 만든 테스트 추가 cargo test를 다시 실행해 보죠. 출력 결과는 예제 11-4처럼 exploration 테스트는 통과하고 another 테스트는 실패했다고 나타날 겁니다. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.72s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests\ntest tests::another ... FAILED\ntest tests::exploration ... ok failures: ---- tests::another stdout ----\nthread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::another test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 예제 11-4: 테스트 하나는 통과하고 다른 하나는 실패했을 때의 테스트 결과 test tests::another 줄은 ok가 아니라 FAILED로 표시됩니다. 개별 결과와 요약 사이에 새로운 절이 두 개 나타났네요: 첫 번째 절은 테스트가 실패한 자세한 이유를 보여줍니다. 위의 경우 another 테스트는 panicked at 'Make this test fail'라는 이유로 실패했으며, src/lib.rs 파일 10번째 줄에서 발생했다는 세부 사항을 알게 되었습니다. 다음 절은 실패한 테스트의 이름을 목록으로 보여줍니다. 이는 테스트가 많아지고 테스트 실패 사유 출력량도 많아졌을 때 유용합니다. 실패한 테스트의 이름을 이용해 해당 테스트만 실행하면 쉽게 디버깅할 수 있습니다. 테스트를 실행하는 각종 방식은 ‘테스트 실행 방법 제어하기’ 절에서 다룰 예정입니다. 요약 줄은 마지막에 출력됩니다. 종합적인 테스트 결과는 FAILED군요. 테스트 하나는 통과했지만, 테스트 하나가 실패했습니다. 각 상황에서 테스트 실행 결과가 어떻게 나타나는지 살펴봤으니, panic! 이외에 테스트에서 유용하게 쓰이는 매크로를 알아봅시다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 작성 방법 » 테스트 함수 파헤치기","id":"198","title":"테스트 함수 파헤치기"},"199":{"body":"어떤 조건이 true임을 보장하는 테스트를 작성할 땐 표준 라이브러리가 제공하는 assert! 매크로가 유용합니다. assert! 매크로는 부울린 값으로 평가되는 인수를 전달받습니다. true 값일 경우, 아무 일도 일어나지 않고 테스트는 통과합니다. false 값일 경우, assert! 매크로는 panic! 매크로를 호출하여 테스트를 실패하도록 만듭니다. assert! 매크로를 사용하면 작성한 코드가 의도대로 기능하는지 검사하는 데에 유용합니다. 5장 예제 5-15에서 Rectangle 구조체랑 can_hold 메서드를 사용했었죠. (예제 11-5로 다시 보여드립니다.) 이 코드를 src/lib.rs 파일에 작성하고, 그다음 assert! 매크로로 테스트를 작성해 봅시다. 파일명: src/lib.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }\n} 예제 11-5: 5장 Rectangle 구조체와 can_hold 메서드 can_hold 메서드는 부울린 값을 반환하니 assert 매크로 사용 예시로 쓰기에 딱 알맞습니다. 예제 11-6은 can_hold 메서드를 시험하는 테스트를 작성한 모습입니다. 너비 8, 높이 7 Rectangle 인스턴스를 생성하고, 이 인스턴스는 너비 5, 높이 1 Rectangle 인스턴스를 포함할 수 있음을 단언합니다. 파일명: src/lib.rs # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n# # impl Rectangle {\n# fn can_hold(&self, other: &Rectangle) -> bool {\n# self.width > other.width && self.height > other.height\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(larger.can_hold(&smaller)); }\n} 예제 11-6: 큰 사각형이 작은 사각형을 정말로 포함할 수 있는지 검사하는 can_hold 메서드 테스트 tests 모듈에 use super::*; 줄이 추가되었습니다. tests 모듈 또한 7장 ‘경로를 사용하여 모듈 트리의 아이템 참조하기’ 절에서 다룬 가시성 규칙을 따르는 평범한 모듈입니다. 따라서, 내부 모듈인 tests 모듈에서 외부 모듈의 코드를 테스트하려면 먼저 내부 스코프로 가져와야 합니다. tests 모듈에서는 글롭 (*) 을 사용해 외부 모듈에 정의된 걸 전부 사용할 수 있도록 하였습니다. 테스트 이름은 larger_can_hold_smaller로 정하고, 필요한 Rectangle 인스턴스를 두 개 생성하고, larger.can_hold(&smaller) 호출 결과를 전달하여 assert! 매크로를 호출하였습니다. larger.can_hold(&smaller) 표현식은 true를 반환할 테니 테스트는 성공하겠죠. 확인해 봅시다! $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 1 test\ntest tests::larger_can_hold_smaller ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 통과됐네요! 이번에는 작은 사각형이 큰 사각형을 포함할 수 없음을 단언하는 테스트를 추가해 봅시다. 파일명: src/lib.rs # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n# # impl Rectangle {\n# fn can_hold(&self, other: &Rectangle) -> bool {\n# self.width > other.width && self.height > other.height\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn larger_can_hold_smaller() { // --생략--\n# let larger = Rectangle {\n# width: 8,\n# height: 7,\n# };\n# let smaller = Rectangle {\n# width: 5,\n# height: 1,\n# };\n# # assert!(larger.can_hold(&smaller)); } #[test] fn smaller_cannot_hold_larger() { let larger = Rectangle { width: 8, height: 7, }; let smaller = Rectangle { width: 5, height: 1, }; assert!(!smaller.can_hold(&larger)); }\n} 이번에는 can_hold 함수가 false를 반환해야 하니, assert! 매크로에 전달하기 전에 논리 부정 연산자를 사용했습니다. 결과적으로, 이 테스트는 can_hold 함수에서 false 값을 반환하면 성공합니다. $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests\ntest tests::larger_can_hold_smaller ... ok\ntest tests::smaller_cannot_hold_larger ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests rectangle running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 두 테스트를 모두 통과했습니다! 그러면 이제 코드에 버그가 있으면 테스트 결과가 어떻게 되는지 알아보죠. can_hold 메서드 구현부 중 너비 비교 부분의 큰 부등호를 작은 부등호로 바꿔보겠습니다: # #[derive(Debug)]\n# struct Rectangle {\n# width: u32,\n# height: u32,\n# }\n# // --생략--\nimpl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width < other.width && self.height > other.height }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn larger_can_hold_smaller() {\n# let larger = Rectangle {\n# width: 8,\n# height: 7,\n# };\n# let smaller = Rectangle {\n# width: 5,\n# height: 1,\n# };\n# # assert!(larger.can_hold(&smaller));\n# }\n# # #[test]\n# fn smaller_cannot_hold_larger() {\n# let larger = Rectangle {\n# width: 8,\n# height: 7,\n# };\n# let smaller = Rectangle {\n# width: 5,\n# height: 1,\n# };\n# # assert!(!smaller.can_hold(&larger));\n# }\n# } 테스트 실행 결과는 다음과 같습니다. $ cargo test Compiling rectangle v0.1.0 (file:///projects/rectangle) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/rectangle-6584c4561e48942e) running 2 tests\ntest tests::larger_can_hold_smaller ... FAILED\ntest tests::smaller_cannot_hold_larger ... ok failures: ---- tests::larger_can_hold_smaller stdout ----\nthread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::larger_can_hold_smaller test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 테스트로 버그를 찾아냈네요! larger.width는 8이고 smaller.width는 5인데 can_hold의 너비 비교 결과는 false(larger.width가 smaller.width 보다 작음)를 반환합니다. 8이 5보다 작진 않죠.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 작성 방법 » assert! 매크로로 결과 검사하기","id":"199","title":"assert! 매크로로 결과 검사하기"},"2":{"body":"Note: 이 책은 No Starch Press 에서 ebook 형태로 무료 제공되는 The Rust Programming Language 와 동일한 내용을 원작자의 동의하에 번역한 것입니다. 러스트 입문서, The Rust Programming Language 에 오신 것을 환영합니다. 러스트 프로그래밍 언어는 더 빠르고 안정적인 소프트웨어를 작성하는 데 도움을 줍니다. 여태 프로그래밍 언어 디자인에 있어 저수준 (low-level) 제어와 고수준 (high-level) 문법은 양립하기 어려웠지만, 러스트는 이러한 충돌에 도전합니다. 강력한 기술적 능력과 뛰어난 개발자 경험 간의 균형을 유지함으로써, 러스트는 (메모리 사용과 같은) 저수준 제어에 전통적으로 동반되는 귀찮은 것들 없이 이를 제어할 수 있게 해 줍니다.","breadcrumbs":"소개 » 소개","id":"2","title":"소개"},"20":{"body":"설치도 마쳤으니, 러스트 프로그램을 만들 시간입니다. 새 언어를 배울 때면 늘 그렇듯, 만들어 볼 프로그램은 화면에 Hello, world! 문자를 출력하는 간단한 프로그램입니다. Note: 이 책은 커맨드 라인 위주로 설명하고 있습니다. 하지만 러스트에는 코드 작성 및 개발 도구 사용환경에 따로 정해진 규정이 없으므로 커맨드 라인 대신 IDE (통합 개발 환경) 를 사용하실 분은 애용하는 IDE를 사용하셔도 좋습니다. (요즘은 IDE 대부분이 러스트를 어느 정도 지원하니 세부 사항은 각 IDE 문서를 참고 바랍니다) 러스트 팀은 rust-analyzer를 통하여 IDE 지원 수준을 높이는 데 집중하고 있습니다. 더 자세한 사항은 부록 D 를 참고하세요.","breadcrumbs":"시작해봅시다 » Hello, World! » Hello, World!","id":"20","title":"Hello, World!"},"200":{"body":"기능성 검증의 일반적인 방법은 테스트 코드의 결괏값이 예상한 값과 같은지 확인하는 것입니다. 이는 assert! 매크로에 == 연산자를 사용한 표현식을 전달하는 식으로도 가능하지만, 러스트는 이런 테스트에 더 알맞은 매크로를 따로 제공합니다. assert_eq!, assert_ne! 매크로는 각각 두 인수를 비교하고 동등한지 (equality) 그렇지 않은지 (inequality) 판단합니다. 단언 코드가 실패하면 두 값을 출력하여 테스트의 실패 사유 를 더 알기 쉽게 보여줍니다. assert! 매크로는 == 표현식이 false 값임을 알려줄 뿐, 어떤 값으로 인해 false 값이 나왔는지 출력하지는 않습니다. 예제 11-7은 매개변수에 2를 더하는 add_two 함수를 작성한 다음, assert_eq! 매크로를 이용해 테스트하는 예제입니다. 파일명: src/lib.rs pub fn add_two(a: i32) -> i32 { a + 2\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn it_adds_two() { assert_eq!(4, add_two(2)); }\n} 예제 11-7: assert_eq! 매크로를 이용한 add_two 함수 테스트 테스트를 통과하는지 확인해 봅시다! $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s assert_eq!에 4를 인수로 넘겼는데, 이는 add_two(2) 호출 결과와 같습니다. 출력 중 테스트에 해당하는 줄은 test tests::it_adds_two ... ok이고, ok는 테스트가 통과했다는 뜻이죠! 코드에 버그를 집어넣어서 assert_eq!가 실패했을 때는 어떤 식으로 보이는지 확인해 봅시다. add_two 함수가 3을 더하도록 구현을 변경해 봅시다: pub fn add_two(a: i32) -> i32 { a + 3\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn it_adds_two() {\n# assert_eq!(4, add_two(2));\n# }\n# } 테스트를 다시 실행해 보죠. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::it_adds_two ... FAILED failures: ---- tests::it_adds_two stdout ----\nthread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)` left: `4`, right: `5`', src/lib.rs:11:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_adds_two test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 테스트가 버그를 찾아냈습니다! it_adds_two 테스트가 실패하고, 메시지는 assertion failed: `(left == right)` 메시지와 left, right가 각각 4, 5 였다는 것을 알려줍니다. 이 메시지로 assert_eq!의 left 인수는 4였는데 right 인수(add_two(2))는 5였다는 내용을 알 수 있기 때문에, 디버깅을 시작하는 데 도움이 됩니다. 수많은 테스트가 있을 때라면 특히 유용할 것임을 짐작할 수 있습니다. 몇몇 프로그래밍 언어, 프레임워크에서는 동등 단언 함수의 매개변수를 expected, actual라고 지칭하며, 코드를 작성할 때 인수의 순서를 지켜야 합니다. 하지만 러스트에서는 left, right라고 지칭할 뿐, 예상값과 테스트 코드로 만들어진 값의 순서는 상관없습니다. 테스트 코드를 assert_eq!(add_two(2), 4) 로 작성할 수도 있습니다. 이 경우에도 실패 메시지는 똑같이 assertion failed: `(left == right)`라고 나타납니다. assert_ne! 매크로는 전달한 두 값이 서로 같지 않으면 통과하고, 동등하면 실패합니다. 어떤 값이 될지 는 확신할 수 없지만, 적어도 이 값은 되지 않아야 함 을 알고 있는 경우에 유용합니다. 예를 들어, 테스트할 함수가 입력값을 어떤 방식으로든 변경한다는 것은 확실하지만, 테스트를 실행하는 요일에 따라 함수의 입력값이 달라진다면, 입력값과 함수 출력이 동일하면 안 된다고 테스트를 작성하는 게 가장 좋을 겁니다. 내부적으로 assert_eq!, assert_ne 매크로는 각각 ==, != 연산자를 사용합니다. 단언에 실패할 경우, 매크로는 인수를 디버그 형식으로 출력하는데, 즉 assert_eq!, assert_ne 매크로로 비교할 값은 PartialEq, Debug 트레이트를 구현해야 합니다. 모든 기본 타입 및 대부분의 표준 라이브러리 타입은 이 두 트레이트를 구현합니다. 직접 정의한 구조체나 열거형의 경우에는 PartialEq 트레이트를 구현하여 해당 타입의 값이 같음을 단언할 수 있도록 할 필요가 있습니다. 또한 단언 실패 시 값이 출력될 수 있도록 Debug 트레이트도 구현해야 합니다. 5장 예제 5-12에서 설명했듯 두 트레이트 모두 파생 가능한 트레이트가기 때문에, 구조체, 열거형 정의에 #[derive(PartialEq, Debug)]를 어노테이션하는 것이 일반적입니다. 이에 대한 추가 내용 및 파생 가능한 나머지 트레이트는 부록 C ‘파생 가능한 트레이트’ 를 참고해 주세요.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 작성 방법 » assert_eq!, assert_ne! 매크로를 이용한 동등 테스트","id":"200","title":"assert_eq!, assert_ne! 매크로를 이용한 동등 테스트"},"201":{"body":"assert!, assert_eq!, assert_ne! 매크로에 추가 인수로 실패 메시지에 출력될 내용을 추가할 수 있습니다. 필수적인 인수들 이후의 인수는 format! 매크로로 전달됩니다. (format! 매크로는 8장의 ‘+ 연산자나 format! 매크로를 이용한 접합’ 절에서 다루었습니다.) 따라서 {} 자리표시자가 들어있는 포맷 문자열과 자리표시자에 들어갈 값을 전달할 수 있습니다. 커스텀 메시지는 테스트 단언의 의미를 서술하는 데에 유용합니다; 테스트가 실패할 경우 코드의 문제점이 무엇인지 알아내기 더 수월해지죠. 예를 들어 이름을 불러 사람을 환영하는 함수가 있고, 함수에게 전달한 이름이 결과에 나타나는지 확인하는 테스트를 작성한다고 칩시다. 파일명: src/lib.rs pub fn greeting(name: &str) -> String { format!(\"Hello {}!\", name)\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn greeting_contains_name() { let result = greeting(\"Carol\"); assert!(result.contains(\"Carol\")); }\n} 아직 프로그램의 요구 사항이 정해지지 않아서, 분명히 Hello 텍스트 부분이 나중에 변경될 거라고 치죠. 프로그램 요구 사항이 바뀔 때 테스트 코드도 고치고 싶지는 않으니 greeting 함수의 정확한 반환 값을 검사하는 대신, 출력 값에 입력 매개변수로 전달한 텍스트가 포함되어 있는지만 확인하려고 합니다. 이제 기본 테스트 실패 시 출력을 살펴보기 위해, greeting 함수 결괏값에서 name이 빠지도록 변경하여 버그를 만들어 보았습니다: pub fn greeting(name: &str) -> String { String::from(\"Hello!\")\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn greeting_contains_name() {\n# let result = greeting(\"Carol\");\n# assert!(result.contains(\"Carol\"));\n# }\n# } 테스트 결과는 다음과 같습니다. $ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test\ntest tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ----\nthread 'tests::greeting_contains_name' panicked at 'assertion failed: result.contains(\\\"Carol\\\")', src/lib.rs:12:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 출력 결과는 단언이 실패했다는 것과 몇 번째 줄에서 실패했는지만 표시합니다. 실패 메시지에서 greeting 함수의 반환 값을 출력해 주면 더 유용하겠죠. 테스트 함수에 커스텀 실패 메시지를 추가해 봅시다. greeting 함수가 반환하는 실제 값으로 채워지게 될 자리표시자가 들어있는 포맷 문자열을 작성해 보죠. # pub fn greeting(name: &str) -> String {\n# String::from(\"Hello!\")\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# #[test] fn greeting_contains_name() { let result = greeting(\"Carol\"); assert!( result.contains(\"Carol\"), \"Greeting did not contain name, value was `{}`\", result ); }\n# } 이제 에러 메시지를 보고 더 많은 정보를 얻을 수 있습니다. 테스트를 다시 실행해 보죠. $ cargo test Compiling greeter v0.1.0 (file:///projects/greeter) Finished test [unoptimized + debuginfo] target(s) in 0.93s Running unittests src/lib.rs (target/debug/deps/greeter-170b942eb5bf5e3a) running 1 test\ntest tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ----\nthread 'tests::greeting_contains_name' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::greeting_contains_name test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 실제 테스트 결괏값을 볼 수 있으니 의도했던 것과 무엇이 다른지 알 수 있어, 디버깅하는 데 도움이 됩니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 작성 방법 » 커스텀 실패 메시지 추가하기","id":"201","title":"커스텀 실패 메시지 추가하기"},"202":{"body":"코드의 반환 값을 검사하는 것에 더하여, 예상한대로 에러 조건을 잘 처리하는지 검사하는 것도 중요합니다. 예를 들어 9장의 예제 9-10에서 만들었던 Guess 타입을 생각해 보세요. Guess 타입을 사용하는 다른 코드는 Guess 인스턴스가 1에서 100 사잇값임을 보장하는 기능에 의존합니다. 이런 경우, 범위를 벗어난 값으로 Guess 인스턴스를 만들면 패닉이 발생하는지 검사하는 테스트를 작성하면 이를 확실하게 보장할 수 있습니다. 패닉 검사 테스트 함수에는 should_panic 속성을 추가합니다. 이 테스트는 내부에서 패닉이 발생해야 통과되고, 패닉이 발생하지 않으면 실패합니다. 예제 11-8은 Guess::new의 에러 조건이 의도대로 작동하는지 검사하는 테스트를 보여줍니다. 파일명: src/lib.rs pub struct Guess { value: i32,\n} impl Guess { pub fn new(value: i32) -> Guess { if value < 1 || value > 100 { panic!(\"Guess value must be between 1 and 100, got {}.\", value); } Guess { value } }\n} #[cfg(test)]\nmod tests { use super::*; #[test] #[should_panic] fn greater_than_100() { Guess::new(200); }\n} 예제 11-8: panic! 발생 테스트 #[should_panic] 속성은 #[test] 속성과 적용할 함수 사이에 위치시켰습니다. 테스트 성공 시 결과를 살펴봅시다. $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test\ntest tests::greater_than_100 - should panic ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests guessing_game running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 괜찮아 보이네요! 이제 new 함수의 패닉 발생 조건 중 100보다 큰 값일 때의 조건을 지워서 버그를 만들어 보죠. # pub struct Guess {\n# value: i32,\n# }\n# // --생략--\nimpl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!(\"Guess value must be between 1 and 100, got {}.\", value); } Guess { value } }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# #[should_panic]\n# fn greater_than_100() {\n# Guess::new(200);\n# }\n# } 예제 11-8 테스트를 실행하면 다음과 같이 실패합니다. $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test\ntest tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ----\nnote: test did not panic as expected failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 에러 메시지는 그다지 유용하지 않지만, 테스트 함수를 살펴보면 #[should_panic]으로 어노테이션된 함수라는 걸 알 수 있습니다. 즉, 테스트 함수에서 패닉이 발생하지 않아서 실패했다는 뜻이죠. should_panic을 사용하는 테스트는 정확하지 않을 수 있습니다. 의도한 것과는 다른 이유로 패닉이 발생하더라도 should_panic 테스트는 통과할 것입니다. should_panic 속성에 expected 매개변수를 추가해, 포함되어야 하는 실패 메시지를 지정하면 더 꼼꼼한 should_panic 테스트를 작성할 수 있습니다. 예제 11-9는 new 함수에서 값이 너무 작은 경우와 큰 경우에 서로 다른 메시지로 panic!을 발생시키도록 수정한 Guess 코드입니다. 파일명: src/lib.rs # pub struct Guess {\n# value: i32,\n# }\n# // --생략-- impl Guess { pub fn new(value: i32) -> Guess { if value < 1 { panic!( \"Guess value must be greater than or equal to 1, got {}.\", value ); } else if value > 100 { panic!( \"Guess value must be less than or equal to 100, got {}.\", value ); } Guess { value } }\n} #[cfg(test)]\nmod tests { use super::*; #[test] #[should_panic(expected = \"less than or equal to 100\")] fn greater_than_100() { Guess::new(200); }\n} 예제 11-9: 특정한 부분 문자열을 포함하는 패닉 메시지를 사용한 panic!에 대한 테스트 should_panic 속성의 expected 매개변숫값이 Guess::new 함수에서 발생한 패닉 메시지 문자열의 일부이므로 테스트는 통과합니다. 발생해야 하는 패닉 메시지 전체를 명시할 수도 있습니다. 이 경우 Guess value must be less than or equal to 100, got 200.이 되겠죠. expected 매개변수에 명시할 내용은 패닉 메시지가 얼마나 고유한지 혹은 동적인지, 그리고 테스트에 요구되는 정확성에 따라 달라집니다. 이번 경우에는, 패닉 메시지 문자열 일부만으로도 실행된 함수 코드가 else if value > 100 상황에 해당함을 확신할 수 있으니 충분합니다. expected 메시지를 지정한 should_panic 테스트가 실패하면 어떻게 되는지 알아보죠. if value < 1 코드 단락과 else if value > 100 코드 단락을 서로 바꾸어 버그를 만들어 보았습니다. # pub struct Guess {\n# value: i32,\n# }\n# # impl Guess {\n# pub fn new(value: i32) -> Guess { if value < 1 { panic!( \"Guess value must be less than or equal to 100, got {}.\", value ); } else if value > 100 { panic!( \"Guess value must be greater than or equal to 1, got {}.\", value ); }\n# # Guess { value }\n# }\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# #[should_panic(expected = \"less than or equal to 100\")]\n# fn greater_than_100() {\n# Guess::new(200);\n# }\n# } 이번에는 should_panic 테스트가 실패합니다. $ cargo test Compiling guessing_game v0.1.0 (file:///projects/guessing_game) Finished test [unoptimized + debuginfo] target(s) in 0.66s Running unittests src/lib.rs (target/debug/deps/guessing_game-57d70c3acb738f4d) running 1 test\ntest tests::greater_than_100 - should panic ... FAILED failures: ---- tests::greater_than_100 stdout ----\nthread 'tests::greater_than_100' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:13:13\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace\nnote: panic did not contain expected string panic message: `\"Guess value must be greater than or equal to 1, got 200.\"`, expected substring: `\"less than or equal to 100\"` failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 테스트에서 패닉이 발생하긴 했지만, 지정한 \"less than or equal to 100\" 문자열이 패닉 메시지에 포함되어 있지 않다는 것을 알려줍니다. 실제로 발생한 패닉 메시지는 Guess value must be greater than or equal to 1, got 200.입니다. 이제 이 메시지를 단서로 버그를 찾아낼 수 있습니다!","breadcrumbs":"자동화 테스트 작성하기 » 테스트 작성 방법 » should_panic 매크로로 패닉 발생 검사하기","id":"202","title":"should_panic 매크로로 패닉 발생 검사하기"},"203":{"body":"지금까지는 실패 시 패닉을 발생시키는 테스트만 작성했습니다. 테스트는 Result를 사용해 작성할 수도 있습니다. 다음은 예제 11-1 테스트를 Result를 사용하도록 수정한 예시입니다. 패닉을 발생시키는 대신 Err을 반환합니다. #[cfg(test)]\nmod tests { #[test] fn it_works() -> Result<(), String> { if 2 + 2 == 4 { Ok(()) } else { Err(String::from(\"two plus two does not equal four\")) } }\n} 이제 it_works 함수는 Result<(), String> 타입을 반환합니다. 함수 본문에서는 assert_eq! 매크로를 호출하는 대신, 테스트 성공 시에는 Ok(())를 반환하고 실패 시에는 String을 갖는 Err을 반환합니다. Result를 반환하는 테스트에서는 ? 연산자를 사용할 수 있기 때문에, 내부 작업이 Err를 반환할 경우 실패해야 하는 테스트를 작성하기 편리합니다. Result 테스트에서는 #[should_panic] 어노테이션을 사용할 수 없습니다. 연산이 Err 배리언트를 반환하는 것을 단언하기 위해서는 Result 값에 물음표 연산자를 사용하지 마세요 . 대신 assert!(value.is_err())를 사용하세요. 여러 테스트 작성 방법을 배웠으니, 테스트를 실행할 때 어떤 일들이 일어나는지 알아보고 cargo test 명령어 옵션을 살펴봅시다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 작성 방법 » Result를 이용한 테스트","id":"203","title":"Result를 이용한 테스트"},"204":{"body":"cargo run 명령어가 코드를 컴파일하고 생성된 바이너리를 실행하는 것과 마찬가지로, cargo test 명령어는 코드를 테스트 모드에서 컴파일하고 생성된 바이너리를 실행합니다. cargo test에 의해 생성된 바이너리의 기본 동작은 모든 테스트를 병렬로 실행하고 테스트가 수행되는 동안 발생된 출력을 캡처하는 것으로, 출력이 표시되는 것을 막고 테스트 결과와 관련된 출력을 읽기 편하게 해 줍니다. 하지만 커맨드 라인 옵션을 지정하여 이러한 기본 동작을 변경할 수 있습니다. 명령어 옵션은 cargo test에 전달되는 것도 있고, 테스트 바이너리에 전달되는 것도 있습니다. 이 둘을 구분하기 위해 cargo test에 전달할 인수를 먼저 나열하고, -- 구분자 (separator) 를 쓰고, 그 뒤에 테스트 바이너리에게 전달할 인수를 나열합니다. cargo test --help 명령어는 cargo test 명령어에 사용 가능한 옵션을 표시하고, cargo test -- --help 명령어는 구분자 이후에 사용 가능한 옵션을 표시합니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 실행 방법 제어하기 » 테스트 실행 방법 제어하기","id":"204","title":"테스트 실행 방법 제어하기"},"205":{"body":"여러 테스트를 실행할 때는 기본적으로 스레드를 사용해 병렬 실행되는데, 이는 테스트를 더 빨리 끝내서 피드백을 더 빠르게 얻기 위함입니다. 여러 테스트가 동시에 실행되므로, 각 테스트가 공유 상태(공유 자원, 현재 작업 디렉터리, 환경 변수 등)를 갖거나 다른 테스트에 의존해서는 안 됩니다. 예시를 생각해 보죠. 각 테스트가 test-output.txt 파일을 생성하고 그 파일에 어떤 데이터를 작성하는 코드를 실행하도록 만들었습니다. 각 테스트는 파일의 데이터를 읽고, 파일이 특정 값을 포함하고 있는지 확인하며, 특정 값은 테스트마다 다릅니다. 여러 테스트가 동시에 실행되므로, 어떤 테스트가 파일에 작성하고 읽는 사이에 다른 테스트가 파일의 내용을 덮어쓸 수도 있습니다. 이 경우 방해받은 테스트는 실패할 겁니다. 코드에 문제가 있어서가 아니라, 병렬 실행되는 도중 방해받아서 말이죠. 한 가지 해결책은 각 테스트가 서로 다른 파일에 작성하도록 만드는 것일 테고, 다른 해결책은 테스트를 한 번에 하나씩 실행하는 것입니다. 테스트를 병렬로 실행하고 싶지 않거나, 사용할 스레드의 개수에 대한 미세 조정이 필요한 경우에는 --test-threads 플래그와 함께 테스트 바이너리에서 사용할 스레드 개수를 지정할 수 있습니다. 다음과 같이 사용합니다. $ cargo test -- --test-threads=1 스레드 개수를 1로 설정하여 프로그램이 어떠한 병렬 처리도 사용하지 않도록 하였습니다. 스레드 하나만 사용해 테스트를 실행하면 병렬 실행에 비해 더 느려지겠지만, 서로 상태를 공유하는 테스트가 방해받을 일이 사라집니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 실행 방법 제어하기 » 테스트를 병렬 혹은 순차적으로 실행하기","id":"205","title":"테스트를 병렬 혹은 순차적으로 실행하기"},"206":{"body":"기본적으로, 러스트 테스트 라이브러리는 성공한 테스트의 모든 표준 출력 (standard output) 을 캡처합니다. 테스트에서 println! 매크로를 호출해도, 해당 테스트가 성공하면 터미널에서 println!의 출력을 찾아볼 수 없습니다. 해당 테스트가 성공했다고 표시된 줄만 볼 수 있죠. 테스트가 실패하면 표준 출력으로 출력됐던 모든 내용이 실패 메시지 아래에 표시됩니다. 예제 11-10은 매개변수를 출력하고 10을 반환하는 단순한 함수와, 성공하는 테스트와 실패하는 테스트를 작성한 예시입니다. 파일명: src/lib.rs fn prints_and_returns_10(a: i32) -> i32 { println!(\"I got the value {}\", a); 10\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn this_test_will_pass() { let value = prints_and_returns_10(4); assert_eq!(10, value); } #[test] fn this_test_will_fail() { let value = prints_and_returns_10(8); assert_eq!(5, value); }\n} 예제 11-10: println!을 호출하는 함수 테스트 cargo test 명령어를 실행하면 다음 결과가 나타납니다. $ cargo test Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.58s Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166) running 2 tests\ntest tests::this_test_will_fail ... FAILED\ntest tests::this_test_will_pass ... ok failures: ---- tests::this_test_will_fail stdout ----\nI got the value 8\nthread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 성공한 테스트에서 출력했던 I got the value 4은 캡처되었으므로 찾아볼 수 없습니다. 실패한 테스트에서 출력한 I got the value 8는 테스트 실패 원인과 함께 테스트 출력 요약 절에 나타납니다. 성공한 테스트에서 출력한 내용도 보고 싶다면, 러스트에게 --show-output 옵션을 전달하여 성공한 테스트의 출력도 표시하도록 할 수 있습니다. $ cargo test -- --show-output 예제 11-10의 테스트를 --show-output 플래그로 실행한 결과는 다음과 같습니다. $ cargo test -- --show-output Compiling silly-function v0.1.0 (file:///projects/silly-function) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166) running 2 tests\ntest tests::this_test_will_fail ... FAILED\ntest tests::this_test_will_pass ... ok successes: ---- tests::this_test_will_pass stdout ----\nI got the value 4 successes: tests::this_test_will_pass failures: ---- tests::this_test_will_fail stdout ----\nI got the value 8\nthread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib`","breadcrumbs":"자동화 테스트 작성하기 » 테스트 실행 방법 제어하기 » 함수 출력 표시하기","id":"206","title":"함수 출력 표시하기"},"207":{"body":"간혹 테스트 모음을 전부 실행하는 데 시간이 오래 걸리기도 합니다. 코드의 특정한 부분에 대한 작업 중이라면 해당 부분의 코드에 관련된 테스트만 실행하고 싶을 수도 있습니다. cargo test 명령어에 테스트의 이름을 인수로 넘겨 어떤 테스트를 실행할지 선택할 수 있습니다. 일부 테스트만 실행하는 법을 알아보기 위해, 먼저 예제 11-11처럼 add_two 함수에 대한 세 가지 테스트를 작성하고 하나만 골라 실행해 보겠습니다. 파일명: src/lib.rs pub fn add_two(a: i32) -> i32 { a + 2\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn add_two_and_two() { assert_eq!(4, add_two(2)); } #[test] fn add_three_and_two() { assert_eq!(5, add_two(3)); } #[test] fn one_hundred() { assert_eq!(102, add_two(100)); }\n} 예제 11-11: 세 가지 서로 다른 이름의 테스트 앞서 살펴본 것처럼, 테스트를 아무 인수도 없이 실행하면 모든 테스트가 병렬로 실행됩니다. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.62s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 3 tests\ntest tests::add_three_and_two ... ok\ntest tests::add_two_and_two ... ok\ntest tests::one_hundred ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 테스트 하나만 실행하기 cargo test 명령어에 테스트 함수 이름을 전달하여 해당 테스트만 실행할 수 있습니다. $ cargo test one_hundred Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.69s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::one_hundred ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s one_hundred 테스트만 실행되었습니다. 나머지 두 테스트는 이름이 맞지 않았습니다. 테스트 결과는 마지막 요약 라인에서 2 filtered out을 표시하여, 실행한 테스트 이외에도 다른 테스트가 존재함을 알려줍니다. 이 방법으로 테스트 이름을 여러 개 지정할 수는 없습니다. cargo test 명령어는 첫 번째 값만 사용합니다. 하지만 여러 테스트를 실행하는 방법이 없지는 않습니다. 테스트를 필터링하여 여러 테스트 실행하기 테스트 이름의 일부만 지정하면 해당 값에 맞는 모든 테스트가 실행됩니다. 예를 들어, cargo test add 명령어를 실행하면 우리가 작성한 세 개의 테스트 중 add가 포함된 두 개가 실행됩니다. $ cargo test add Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests\ntest tests::add_three_and_two ... ok\ntest tests::add_two_and_two ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s 이 명령어는 add가 이름에 포함된 모든 테스트를 실행하고, one_hundred 테스트를 필터링했습니다. 테스트가 위치한 모듈도 테스트 이름의 일부로 나타나는 점을 기억해 두세요. 모듈 이름으로 필터링하면 해당 모듈 내 모든 테스트를 실행할 수 있습니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 실행 방법 제어하기 » 이름을 지정해 일부 테스트만 실행하기","id":"207","title":"이름을 지정해 일부 테스트만 실행하기"},"208":{"body":"간혹 몇몇 특정 테스트는 실행하는 데 굉장히 오랜 시간이 걸려서, cargo test 실행 시 이런 테스트는 제외하고 싶을 수도 있습니다. 그럴 때는 실행할 모든 테스트를 인수로 열거할 필요 없이 시간이 오래 걸리는 테스트에 ignore 속성을 어노테이션하면 됩니다. 파일명: src/lib.rs #[test]\nfn it_works() { assert_eq!(2 + 2, 4);\n} #[test]\n#[ignore]\nfn expensive_test() { // code that takes an hour to run\n} 제외할 테스트의 #[test] 다음 줄에 #[ignore] 줄을 추가했습니다. 이제 테스트를 실행하면 it_works 테스트는 실행되지만, expensive_test 테스트는 실행되지 않습니다. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.60s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 2 tests\ntest expensive_test ... ignored\ntest it_works ... ok test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s expensive_test 테스트는 ignored로 표시되었습니다. cargo test -- --ignored 명령어를 사용하면 무시된 테스트만 실행할 수 있습니다. $ cargo test -- --ignored Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.61s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest expensive_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 실행할 테스트를 선별하여 cargo test 결과는 빨리 확인할 수 있습니다. 무시한 테스트의 결과를 확인해야 할 때가 되었고 그 결과를 기다릴 시간이 있다면, cargo test -- --ignored 명령어를 실행합니다. 무시되었건 말건 간에 모든 테스트를 실행하고 싶다면 cargo test -- --include-ignored를 실행할 수 있습니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 실행 방법 제어하기 » 특별 요청이 없다면 일부 테스트 무시하기","id":"208","title":"특별 요청이 없다면 일부 테스트 무시하기"},"209":{"body":"이번 장의 시작 부분에서 언급했듯, 테스트는 복잡한 분야입니다. 사람들은 저마다 다른 용어와 구조를 사용합니다. 러스트 커뮤니티는 테스트를 크게 유닛 테스트 (unit test, 단위 테스트라고도 함), 통합 테스트 (integration test) 두 종류로 나눕니다. 유닛 테스트 는 작고 더 집중적입니다. 한 번에 하나의 모듈만 테스트하며, 모듈의 비공개 인터페이스도 테스트할 수 있습니다. 통합 테스트 는 완전히 라이브러리 외부에 위치하며, 따라서 여러분이 작성한 라이브러리를 외부 코드에서 사용할 때와 똑같은 방식을 사용합니다. 하나의 테스트에서 잠재적으로 여러 모듈이 사용되기도 합니다. 여러분의 라이브러리의 각 부분이 따로 사용될 때와 함께 사용될 때의 모든 경우에서 예상한대로 작동할 것을 보장하려면 두 종류의 테스트 모두 작성해야 합니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 조직화 » 테스트 조직화","id":"209","title":"테스트 조직화"},"21":{"body":"작성할 러스트 코드를 저장해 둘 디렉터리가 필요하겠죠. 러스트 코드 자체는 어디에 저장하건 실행하는 데 문제는 없습니다만, 이 책을 보며 연습하시는 분들은 편의를 위해 홈 디렉터리 내 projects 디렉터리를 생성해 각종 프로젝트를 보관하는 것을 권장해 드립니다. 터미널을 열고 다음 명령어를 입력해 projects 디렉터리를 생성한 후, projects 내에 ‘Hello, world!’ 프로젝트용 디렉터리를 만들어 봅시다. Linux, macOS, Windows PowerShell에서는 다음 명령어를 입력해 주세요: $ mkdir ~/projects\n$ cd ~/projects\n$ mkdir hello_world\n$ cd hello_world Windows CMD 사용자는 다음 명령어를 입력해 주세요: > mkdir \"%USERPROFILE%\\projects\"\n> cd /d \"%USERPROFILE%\\projects\"\n> mkdir hello_world\n> cd hello_world","breadcrumbs":"시작해봅시다 » Hello, World! » 프로젝트 디렉터리 생성하기","id":"21","title":"프로젝트 디렉터리 생성하기"},"210":{"body":"유닛 테스트의 목적은 각 코드 단위를 나머지 코드와 분리하여, 제대로 작동하지 않는 코드가 어느 부분인지 빠르게 파악하는 것입니다. 유닛 테스트는 src 디렉터리 내의 각 파일에 테스트 대상이 될 코드와 함께 작성합니다. 각 파일에 tests 모듈을 만들고 cfg(test)를 어노테이션하는 게 일반적인 관례입니다. 테스트 모듈과 #[cfg(test)] 테스트 모듈에 어노테이션하는 #[cfg(test)]은 이 코드가 cargo build 명령어가 아닌 cargo test 명령어 실행 시에만 컴파일 및 실행될 것을 러스트에게 전달합니다. 라이브러리 빌드 시 테스트 코드는 제외되므로, 컴파일 소요 시간이 짧아지고, 컴파일 결과물 크기도 줄어듭니다. 이후에 알게 되겠지만, 통합 테스트는 별도의 디렉터리에 위치하기 때문에 #[cfg(test)] 어노테이션이 필요 없습니다. 하지만 유닛 테스트는 일반 코드와 같은 파일에 위치하기 때문에, #[cfg(test)] 어노테이션을 작성해 컴파일 결과물에 포함되지 않도록 명시해야 합니다. 이번 장 첫 번째 절에서 adder 프로젝트를 생성했을 때 카고가 생성했던 코드를 다시 살펴봅시다. 파일명: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); }\n} 이 코드는 자동으로 생성된 테스트 모듈입니다. cfg 속성은 설정 (configuration) 을 의미하며, 러스트는 이 아이템을 특정 설정 옵션 적용 시에만 포함합니다. 이 경우 옵션 값은 러스트에서 테스트를 컴파일, 실행하기 위해 제공하는 test입니다. cfg 속성을 사용하면 카고는 cargo test 명령어를 실행할 때만 테스트 코드를 컴파일합니다. 여기에는 #[test] 어노테이션된 함수뿐만 아니라 모듈 내 도우미 함수도 포함됩니다. 비공개 함수 테스트하기 비공개 함수도 직접 테스트해야 하는지에 대해서는 많은 논쟁이 있습니다. 다른 언어에서는 비공개 함수를 테스트하기 어렵거나, 불가능하게 만들어 두었습니다. 여러분의 테스트 철학이 어떤지는 모르겠지만, 러스트의 비공개 규칙은 비공개 함수를 테스트하도록 허용합니다. 예제 11-12는 비공개 함수 internal_adder를 보여줍니다. 파일명: src/lib.rs pub fn add_two(a: i32) -> i32 { internal_adder(a, 2)\n} fn internal_adder(a: i32, b: i32) -> i32 { a + b\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn internal() { assert_eq!(4, internal_adder(2, 2)); }\n} 예제 11-12: 비공개 함수 테스트하기 internal_adder 함수는 pub으로 표시되지 않았습니다. 테스트는 그냥 러스트 코드이며 tests 모듈도 그저 또 다른 모듈일 뿐입니다. ‘경로를 사용하여 모듈 트리의 아이템 참조하기’ 절에서 논의한 바와 같이, 자식 모듈 내의 아이템은 자기 조상 모듈에 있는 아이템을 사용할 수 있습니다. 이 테스트에서는 use super::*를 사용하여 test 모듈의 부모에 있는 아이템을 모두 스코프 안으로 가져오고 있고, 따라서 테스트가 internal_adder를 호출할 수 있습니다. 혹시 여러분이 비공개 함수를 테스트해서는 안 된다는 주의라면, 러스트가 이를 강요하지는 않습니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 조직화 » 유닛 테스트","id":"210","title":"유닛 테스트"},"211":{"body":"통합 테스트는 여러분이 만든 라이브러리와 완전히 분리되어 있습니다. 통합 테스트는 외부 코드와 마찬가지로, 여러분이 만든 라이브러리의 공개 API만 호출 가능합니다. 통합 테스트의 목적은 라이브러리의 여러 부분을 함께 사용했을 때 제대로 작동하는지 확인하는 것입니다. 각각 따로 사용했을 때 잘 작동하는 코드도 함께 사용할 때는 문제가 발생할 수 있기 때문에 통합 테스트도 중요합니다. 통합 테스트를 작성하려면 먼저 tests 디렉터리를 만들어야 합니다. tests 디렉터리 프로젝트 디렉터리 최상위, 다시 말해 src 옆에 tests 디렉터리를 생성합니다. 카고는 디렉터리 내 통합 테스트 파일을 자동으로 인식합니다. 그런 다음에는 원하는 만큼 통합 테스트 파일을 만들 수 있고, 카고는 각 파일을 개별 크레이트로 컴파일합니다. 통합 테스트를 직접 만들어 봅시다. 예제 11-12 코드를 src/lib.rs 에 작성한 채로 tests 디렉터리를 만들고, tests/integration_test.rs 파일을 생성합니다. 디렉터리 구조는 다음과 같이 보일 것입니다: adder\n├── Cargo.lock\n├── Cargo.toml\n├── src\n│ └── lib.rs\n└── tests └── integration_test.rs tests/integration_test.rs 파일에 예제 11-13의 코드를 입력합시다: 파일명: tests/integration_test.rs use adder; #[test]\nfn it_adds_two() { assert_eq!(4, adder::add_two(2));\n} 예제 11-13: adder 크레이트 내 함수를 테스트하는 통합 테스트 tests 디렉터리의 각 파일은 별개의 크레이트이므로, 각각의 테스트 크레이트의 스코프로 우리가 만든 라이브러리를 가져올 필요가 있습니다. 이러한 이유로 코드 최상단에 use adder를 추가했는데, 이는 유닛 테스트에서는 필요 없던 것이지요. tests/integration_test.rs 내 코드는 #[cfg(test)]가 필요 없습니다. 카고는 tests 디렉터리를 특별 취급하여, 디렉터리 내 파일을 cargo test 시에만 컴파일합니다. cargo test를 다시 실행시켜 보죠. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 1.31s Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6) running 1 test\ntest tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6) running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 출력에 유닛 테스트, 통합 테스트, 문서 테스트 세 가지 절이 만들어졌네요. 어떤 절 안에 어떠한 테스트라도 실패하면, 그다음 절은 실행되지 않음을 유의하세요. 예를 들어 유닛 테스트가 실패하면, 통합 테스트와 문서 테스트는 모든 유닛 테스트가 통과되어야만 실행되기 때문에 이와 관련한 어떠한 출력도 없을 것입니다. 첫 번째 절인 유닛 테스트는 앞에서 본 것과 같습니다: 유닛 테스트가 한 줄씩 (internal은 예제 11-12에서 추가했었습니다) 출력되고, 유닛 테스트 결과 요약 줄이 출력됩니다. 통합 테스트 절은 Running tests/integration_test.rs줄로 시작합니다. 그다음 통합 테스트 내 각각의 테스트 함수가 한 줄씩 출력되고, 통합 테스트 결과 요약은 Doc-tests adder 절이 시작하기 직전에 출력됩니다. 각각의 통합 테스트 파일은 별도의 출력 절을 생성하므로, tests 디렉터리에 파일을 추가하면 통합 테스트 절이 더 만들어질 것입니다. 통합 테스트도 마찬가지로 cargo test 명령어에 테스트 함수명을 인수로 전달해 특정 통합 테스트 함수를 실행할 수 있습니다. 특정 통합 테스트 파일의 모든 테스트를 실행하려면, cargo test 명령어에 --test 인수로 파일명을 전달하면 됩니다. $ cargo test --test integration_test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.64s Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298) running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 이 명령어는 tests/integration_test.rs 파일 내의 테스트만 실행합니다. 통합 테스트 내 서브 모듈 통합 테스트를 추가하다 보면, 조직화를 위해 tests 디렉터리에 더 많은 파일이 필요할 수도 있습니다; 예를 들어, 테스트 함수가 테스트하는 기능별로 그룹화할 수도 있죠. 앞서 말했듯, tests 내 각 파일은 각각의 크레이트로 컴파일되는데, 이는 각 통합 테스트 파일이 각각의 크레이트로 취급된다는 점 때문에 여러분이 만든 크레이트를 사용할 실제 사용자처럼 분리된 스코프를 만들어 내는 데에는 유용합니다. 하지만 이는 7장에서 배운 것처럼 src 디렉터리에서 코드를 모듈과 파일로 분리하여 동일한 동작을 공유하는 것을 tests 디렉터리 내 파일에서는 할 수 없음을 의미합니다. 여러 통합 테스트 파일에서 유용하게 사용할 도우미 함수 묶음을 7장 ‘별개의 파일로 모듈 분리하기’ 절의 과정대로 공통 모듈로 분리하려 할 때, tests 디렉터리 파일의 동작 방식은 걸림돌이 됩니다. 예를 들어 tests/common.rs 파일을 생성하고, 여러 테스트 파일의 테스트 함수에서 호출하려는 setup 함수를 작성한다고 가정해 봅시다: 파일명: tests/common.rs pub fn setup() { // 여기에 라이브러리 테스트와 관련된 설정 코드를 작성하려고 합니다\n} 이제 테스트를 실행하면, 결과 출력에 새로운 절이 common.rs 파일 때문에 생성된 모습을 볼 수 있습니다. common.rs 파일은 어떤 테스트 함수도 담고 있지 않고, 다른 곳에서 setup 함수를 호출하지도 않았는데 말이죠. $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished test [unoptimized + debuginfo] target(s) in 0.89s Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4) running 1 test\ntest tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/common.rs (target/debug/deps/common-92948b65e88960b4) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4) running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 원하던 것은 다른 통합 테스트 파일에서 사용할 코드를 공유하는 것이지, 테스트 출력 결과에 common 과 running 0 tests이 출력되는 게 아니었죠. 테스트 출력 결과에서 common을 제외하려면 tests/common.rs 파일 대신 tests/common/mod.rs 파일을 생성해야 합니다. 프로젝트 디렉터리는 이제 아래와 같은 모양이 됩니다: ├── Cargo.lock\n├── Cargo.toml\n├── src\n│ └── lib.rs\n└── tests ├── common │ └── mod.rs └── integration_test.rs 이는 7장의 ‘대체 파일 경로’ 절에서 언급했던 러스트에서 사용 가능한 예전 명명 규칙입니다. 이러한 방식의 파일명 규칙을 따르는 파일은 통합 테스트 파일로 취급하지 않습니다. setup 함수를 tests/common/mod.rs 파일로 옮기고 tests/common.rs 파일을 삭제하면 더 이상 테스트 결과 출력에 common이 나타나지 않습니다. tests 디렉터리의 서브 디렉터리 내 파일은 별도 크레이트로 컴파일되지 않고, 테스트 결과 출력에서 별도의 출력 절이 생성되지도 않습니다. tests/common/mod.rs 파일을 생성하고 나면 다른 통합 테스트 파일에서 모듈처럼 사용할 수 있습니다. 다음은 tests/integration_test.rs 파일 내 it_adds_two 테스트에서 setup 함수를 호출하는 예시입니다. 파일명: tests/integration_test.rs use adder; mod common; #[test]\nfn it_adds_two() { common::setup(); assert_eq!(4, adder::add_two(2));\n} 예제 7-21에서 배운 모듈 선언대로 mod common;를 선언했습니다. 선언하고 나면 common::setup() 함수를 호출할 수 있습니다. 바이너리 크레이트에서의 통합 테스트 src/lib.rs 파일이 없고 src/main.rs 파일만 있는 바이너리 크레이트라면, tests 디렉터리에 통합 테스트를 만들어서 src/main.rs 파일에 정의된 함수를 use 구문으로 가져올 수 없습니다. 다른 크레이트에서 사용할 수 있도록 함수를 노출하는 건 라이브러리 크레이트 뿐입니다. 바이너리 크레이트는 자체적으로 실행되게 되어있습니다. 바이너리를 제공하는 러스트 프로젝트들이 src/main.rs 파일은 간단하게 작성하고, 로직은 src/lib.rs 파일에 위치시키는 이유 중 하나가 이 때문입니다. 이런 구조로 작성하면 중요 기능을 통합 테스트에서 use 구문으로 가져와 테스트할 수 있습니다. 중요 기능이 제대로 작동하면 src/main.rs 파일 내 소량의 코드도 작동할 테니, 이 소량의 코드는 테스트하지 않아도 됩니다.","breadcrumbs":"자동화 테스트 작성하기 » 테스트 조직화 » 통합 테스트","id":"211","title":"통합 테스트"},"212":{"body":"러스트의 테스트 기능을 사용하면 코드가 어떻게 작동해야 하는지 명시하여, 코드를 변경하더라도 계속하여 의도대로 작동함을 보장할 수 있습니다. 유닛 테스트는 비공개 세부 구현을 포함한 라이브러리의 각 부분이 별도로 잘 작동하는지 확인합니다. 통합 테스트는 외부 코드가 라이브러리를 사용하는 것과 동일한 방식으로 라이브러리 공개 API를 이용하여 라이브러리의 여러 부분이 함께 사용될 때 제대로 작동하는지 확인합니다. 러스트의 타입 시스템과 소유권 규칙이 일부 버그를 방지해 주긴 하지만, 여러분이 작성한 코드가 의도대로 작동하지 않는 논리 버그를 제거하려면 테스트도 마찬가지로 중요합니다. 이번 장과 이전 장에서 배운 지식을 결합하여 프로젝트를 진행해 봅시다!","breadcrumbs":"자동화 테스트 작성하기 » 테스트 조직화 » 정리","id":"212","title":"정리"},"213":{"body":"이번 장에서는 여러분이 지금까지 배운 여러 기술들을 요약하고 표준 라이브러리의 기능을 몇 가지 더 탐색해 보겠습니다. 파일 및 커맨드 입출력을 통해 상호작용하는 커맨드 라인 도구를 만들면서 이제는 여러분이 이해하고 있을 러스트 개념 몇 가지를 연습해 볼 것입니다. 러스트의 속도, 안정성, 단일 바이너리 출력, 그리고 크로스 플랫폼 지원은 커맨드 라인 도구를 만들기 위한 이상적인 언어가 되게끔 하므로, 프로젝트를 위해 고전적인 커맨드 라인 검색 도구인 grep( g lobally search a r egular e xpression and p rint)의 직접 구현한 버전을 만들어 보려고 합니다. 가장 단순한 사용례에서 grep은 어떤 특정한 파일에서 특정한 문자열을 검색합니다. 이를 위해 grep은 파일 경로와 문자열을 인수로 받습니다. 그다음 파일을 읽고, 그 파일에서 중 문자열 인수를 포함하고 있는 라인을 찾고, 그 라인들을 출력합니다. 그러는 와중에 수많은 다른 커맨드 라인 도구들이 사용하는 터미널의 기능을 우리의 커맨드 라인 도구도 사용할 수 있게 하는 방법을 알아보겠습니다. 먼저 환경 변수의 값을 읽어서 사용자가 커맨드 라인 도구의 동작을 설정하도록 할 것입니다. 또한 표준 출력 콘솔 스트림 (stdout) 대신 표준 에러 콘솔 스트림 (stderr) 에 에러 메시지를 출력하여, 예를 들자면 사용자가 화면을 통해 에러 메시지를 보는 동안에도 성공적인 출력을 파일로 리디렉션할 수 있게끔 할 것입니다. 러스트 커뮤니티 멤버 일원인 앤드루 갈란트 (Andrew Gallant) 가 이미 ripgrep이라는 이름의, 모든 기능을 가진 grep의 매우 빠른 버전을 만들었습니다. 그에 비해서 지금 만들어 볼 버전은 꽤 단순할 예정이지만, 이 장은 여러분에게 ripgrep과 같은 실제 프로젝트를 이해하는 데 필요한 배경 지식을 제공할 것입니다. 이 grep 프로젝트는 지금까지 배운 여러 개념을 조합할 것입니다: 코드 조직화하기 ( 7장 에서 모듈에 대해 배운 것들을 사용) 벡터와 문자열 사용하기 ( 8장 의 컬렉션) 에러 처리하기 ( 9장 ) 적절한 곳에 트레이트와 라이프타임 사용하기 ( 10장 ) 테스트 작성하기 ( 11장 ) 아울러 13장 과 17장 에서 자세히 다루게 될 클로저 (closure), 반복자 (iterator), 그리고 트레이트 객체 (trait object) 에 대해서도 간략히 소개하겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » I/O 프로젝트: 커맨드 라인 프로그램 만들기","id":"213","title":"I/O 프로젝트: 커맨드 라인 프로그램 만들기"},"214":{"body":"언제나처럼 cargo new로 새 프로젝트를 만들어 봅시다. 여러분의 시스템에 이미 설치되어 있을지도 모를 grep 도구와 구분하기 위하여, 우리 프로젝트 이름은 minigrep으로 하겠습니다. $ cargo new minigrep Created binary (application) `minigrep` project\n$ cd minigrep minigrep을 만들기 위한 첫 과제는 두 개의 커맨드 라인 인수를 받는 것입니다: 바로 검색할 파일 경로와 문자열이지요. 그 말은즉슨, 다음과 같이 프로그램을 실행하기 위해 cargo run, cargo 대신 우리 프로그램을 위한 인수가 나올 것임을 알려주는 두 개의 하이픈, 검색을 위한 문자열, 그리고 검색하길 원하는 파일을 사용할 수 있도록 하고 싶다는 것입니다: $ cargo run -- searchstring example-filename.txt 현재 cargo new로 생성된 프로그램은 입력된 인수를 처리할 수 없습니다. crates.io 에 있는 몇 가지 라이브러리가 커맨드 라인 인수를 받는 프로그램 작성에 도움 되겠지만, 지금은 이 개념을 막 배우는 중이므로 직접 이 기능을 구현해 봅시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 커맨드 라인 인수 받기 » 커맨드 라인 인수 받기","id":"214","title":"커맨드 라인 인수 받기"},"215":{"body":"minigrep이 커맨드 라인 인수로 전달된 값들을 읽을 수 있도록 하기 위해서는 러스트의 표준 라이브러리가 제공하는 std::env::args 함수를 사용할 필요가 있겠습니다. 이 함수는 minigrep으로 넘겨진 커맨드 라인 인수의 반복자 (iterator) 를 반환합니다. 반복자에 대한 모든 것은 13장 에서 다룰 예정입니다. 지금은 반복자에 대한 두 가지 세부 사항만 알면 됩니다: 반복자는 일련의 값들을 생성하고, 반복자의 collect 메서드를 호출하여 반복자가 생성하는 모든 요소를 담고 있는 벡터 같은 컬렉션으로 바꿀 수 있다는 것입니다. 예제 12-1의 코드는 minigrep 프로그램이 넘겨진 어떤 커맨드 라인 인수들을 읽은 후, 그 값들을 벡터로 모아주도록 해 줍니다. 파일명: src/main.rs use std::env; fn main() { let args: Vec = env::args().collect(); dbg!(args);\n} 예제 12-1: 커맨드 라인 인수들을 벡터로 모으고 출력하기 먼저 use를 사용하여 std::env 모듈을 스코프로 가져와서 args 함수를 사용할 수 있게 합니다. std::env::args 함수는 두 단계로 중첩된 모듈에 있는 점을 주목하세요. 7장 에서 논의한 것처럼, 하나 이상의 모듈로 중첩된 곳에 원하는 함수가 있는 경우에는, 함수가 아닌 그 부모 모듈을 스코프로 가져오는 선택을 했습니다. 이렇게 하면 std::env의 다른 함수들도 쉽게 사용할 수 있습니다. 또한 이렇게 하는 것이 use std::env::args를 추가하고 args 만으로 함수를 호출하는 덜 모호한데, 이는 args가 현재의 모듈 내에 정의된 다른 함수로 쉽게 오해받을 수 있기 때문입니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 커맨드 라인 인수 받기 » 인수 값 읽기","id":"215","title":"인수 값 읽기"},"216":{"body":"어떤 인수에라도 유효하지 않은 유니코드가 들어있다면 std::env::args가 패닉을 일으킨다는 점을 주의하세요. 만일 프로그램이 유효하지 않은 유니코드를 포함하는 인수들을 받을 필요가 있다면, std::env::args_os를 대신 사용하세요. 이 함수는 String 대신 OsString 값을 생성하는 반복자를 반환합니다. 여기서는 단순함을 위해 std::env::args을 사용했는데, 이는 OsString 값이 플랫폼 별로 다르고 String 값을 가지고 작업하는 것보다 더 복잡하기 때문입니다. main의 첫째 줄에서는 env::args를 호출한 즉시 collect를 사용하여 반복자에 의해 만들어지는 모든 값을 담고 있는 벡터로 바꿉니다. collect 함수를 사용하여 다양한 종류의 컬렉션을 만들 수 있으므로, 문자열의 벡터가 필요하다는 것을 명시하기 위해 args의 타입을 명시적으로 표기하였습니다. 러스트에서는 타입을 명시할 필요가 거의 없지만, 러스트가 여러분이 원하는 종류의 컬렉션을 추론할 수는 없으므로 collect는 타입 표기가 자주 필요한 함수 중 하나입니다. 마지막으로 디버그 매크로를 사용하여 벡터를 출력합니다. 먼저 인수 없이 코드를 실행해 보고, 그다음 인수 두 개를 넣어 실행해 봅시다: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.61s Running `target/debug/minigrep`\n[src/main.rs:5] args = [ \"target/debug/minigrep\",\n] $ cargo run -- needle haystack Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 1.57s Running `target/debug/minigrep needle haystack`\n[src/main.rs:5] args = [ \"target/debug/minigrep\", \"needle\", \"haystack\",\n] 벡터의 첫 번째 값이 \"target/debug/minigrep\", 즉 이 바이너리 파일의 이름인 점을 주목하세요. 이는 C에서의 인수 리스트의 동작과 일치하며, 프로그램이 실행될 때 호출된 이름을 사용할 수 있게 해 줍니다. 프로그램의 이름에 접근할 수 있는 것은 메시지에 이름을 출력하고 싶을 때라던가 프로그램을 호출할 때 사용된 커맨드 라인 별칭이 무엇이었는지에 기반하여 프로그램의 동작을 바꾸고 싶을 때 종종 편리하게 이용됩니다. 하지만 이 장의 목적을 위해서 지금은 이를 무시하고 현재 필요한 두 인수만 저장하겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 커맨드 라인 인수 받기 » args 함수와 유효하지 않은 유니코드","id":"216","title":"args 함수와 유효하지 않은 유니코드"},"217":{"body":"이제 프로그램은 커맨드 라인 인수로 지정된 값들에 접근할 수 있습니다. 이제는 두 인수의 값을 변수에 저장할 필요가 있는데, 그렇게 하면 프로그램의 나머지 부분에서 이 값들을 사용할 수 있겠습니다. 예제 12-2에서 이 동작을 수행합니다. 파일명: src/main.rs use std::env; fn main() { let args: Vec = env::args().collect(); let query = &args[1]; let file_path = &args[2]; println!(\"Searching for {}\", query); println!(\"In file {}\", file_path);\n} 예제 12-2: 질의 (query) 인수와 파일 경로 인수를 담은 변수 생성하기 벡터를 출력할 때 본 것처럼 프로그램의 이름이 벡터의 첫 번째 값 args[0]을 사용하므로, 인덱스 1에 있는 인수부터 시작하고 있습니다. minigrep이 취하는 첫 번째 인수는 검색하고자 하는 문자열이므로, 첫 번째 인수의 참조자를 query 변수에 집어넣습니다. 두 번째 인수는 파일 경로가 될 것이므로, 두 번째 인수의 참조자를 file_path에 집어넣습니다. 우리 의도대로 코드가 동작하는지 검증하기 위해 이 변수의 값들을 임시로 출력하겠습니다. test와 sample.txt를 인수로 하여 이 프로그램을 다시 실행해 봅시다: $ cargo run -- test sample.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep test sample.txt`\nSearching for test\nIn file sample.txt 프로그램이 훌륭하게 동작하네요! 필요로 하는 인수 값들이 올바른 변수에 저장되고 있습니다. 나중에는 사용자가 아무런 인수를 제공했을 때처럼 에러가 발생할 수 있는 특정한 경우를 처리하기 위한 에러 처리 기능을 몇 가지 추가할 것입니다; 지금은 그런 경우를 무시하고 파일 읽기 기능을 추가하는 작업으로 넘어가겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 커맨드 라인 인수 받기 » 인수 값들을 변수에 저장하기","id":"217","title":"인수 값들을 변수에 저장하기"},"218":{"body":"이제는 file_path 인수에 명시된 파일을 읽는 기능을 추가해 보겠습니다. 우선 테스트에 사용할 샘플 파일이 필요합니다: 여러 줄의 몇 개의 반복된 단어들로 구성된 작은 양의 텍스트로 된 파일을 사용하겠습니다. 예제 12-3은 딱 맞게 사용될 에밀리 딕킨슨 (Emily Dickinson) 의 시가 있습니다! 프로젝트의 루트 레벨에 poem.txt 이라는 이름의 파일을 만들고, 시 ‘I’m Nobody! Who are you?’를 입력하세요. 파일명: poem.txt I'm nobody! Who are you?\nAre you nobody, too?\nThen there's a pair of us - don't tell!\nThey'd banish us, you know. How dreary to be somebody!\nHow public, like a frog\nTo tell your name the livelong day\nTo an admiring bog! 예제 12-3: 에밀리 딕킨슨의 시는 좋은 테스트 케이스를 만들어 줍니다 텍스트를 채워 넣었다면 예제 12-4처럼 src/main.rs 에 파일을 읽는 코드를 추가하세요. 파일명: src/main.rs use std::env;\nuse std::fs; fn main() { // --생략--\n# let args: Vec = env::args().collect();\n# # let query = &args[1];\n# let file_path = &args[2];\n# # println!(\"Searching for {}\", query); println!(\"In file {}\", file_path); let contents = fs::read_to_string(file_path) .expect(\"Should have been able to read the file\"); println!(\"With text:\\n{contents}\");\n} 예제 12-4: 두 번째 인수로 명시된 파일의 내용물 읽기 먼저 use 구문을 사용하여 표준 라이브러리의 연관된 부분을 가져옵니다: 파일을 다루기 위해서는 std::fs가 필요하죠. main에서 새로운 구문 fs::read_to_string이 file_path를 받아서 그 파일을 열고, 파일 내용물의 std::io::Result을 반환합니다. 그다음 다시 한번 임시로 println! 구문을 추가하여 파일을 읽은 후 contents의 값을 출력하는 것으로 현재까지의 프로그램이 잘 작동하는지 확인합니다. 첫 번째 커맨드 라인 인수에는 아무 문자열이나 넣고 (아직 검색 부분은 구현하지 않았으므로) 두 번째 인수에는 poem.txt 파일을 넣어서 이 코드를 실행해 봅시다: $ cargo run -- the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep the poem.txt`\nSearching for the\nIn file poem.txt\nWith text:\nI'm nobody! Who are you?\nAre you nobody, too?\nThen there's a pair of us - don't tell!\nThey'd banish us, you know. How dreary to be somebody!\nHow public, like a frog\nTo tell your name the livelong day\nTo an admiring bog! 훌륭해요! 코드가 파일의 내용물을 읽은 뒤 출력했습니다. 하지만 이 코드에는 몇 가지 결점이 있습니다. 현재 main 함수에는 여러 가지 기능이 있습니다: 일반적으로 함수 하나당 단 하나의 아이디어에 대한 기능을 구현할 때 함수가 더 명료해지고 관리하기 쉬워집니다. 또 한 가지 문제는 처리 가능한 수준의 에러 처리를 안 하고 있다는 점입니다. 프로그램은 아직 작고, 따라서 이러한 결점이 큰 문제는 아니지만, 프로그램이 커지면 이 문제들을 깔끔하게 고치기 어려워질 것입니다. 작은 양의 코드를 리팩터링하는 것이 훨씬 쉽기 때문에, 프로그램을 개발할 때 일찍 리팩터링하는 것은 좋은 관행입니다. 이걸 바로 다음에 하겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 파일 읽기 » 파일 읽기","id":"218","title":"파일 읽기"},"219":{"body":"프로그램을 개선하기 위해서 프로그램의 구조 및 잠재적 에러를 처리하는 방식과 관련된 네 가지 문제를 고치려고 합니다. 첫 번째로는 main 함수가 지금 두 가지 일을 수행한다는 것입니다: 인수 파싱, 파일을 읽는 작업 말입니다. 이 프로그램이 커짐에 따라 main 함수에서 처리하는 개별 작업의 개수는 증가할 것입니다. 어떤 함수가 책임 소재를 계속 늘려나가면, 이 함수는 어떤 기능인지 추론하기 어려워지고, 테스트하기도 힘들어지고, 기능 일부분을 깨트리지 않으면서 고치기도 어려워집니다. 기능을 나누어 각각의 함수가 하나의 작업에 대한 책임만 지는 것이 최선입니다. 이 주제는 두 번째 문제와도 엮입니다: query와 file_path가 프로그램의 설정 변수이지만, contents 같은 변수는 프로그램 로직을 수행하기 위해 사용됩니다. main이 점점 길어질수록 필요한 변수들이 더 많이 스코프 안에 있게 되고, 스코프 안에 더 많은 변수가 있을수록 각 변수의 목적을 추적하는 것이 더 어려워집니다. 설정 변수들을 하나의 구조체로 묶어서 목적을 분명히 하는 것이 가장 좋습니다. 세 번째 문제는 파일 읽기 실패 시 에러 메시지 출력을 위해서 expect를 사용했는데, 이 에러 메시지가 겨우 Should have been able to read the file이나 출력한다는 것입니다. 파일을 읽는 작업은 여러 가지 방식으로 실패할 수 있습니다: 이를테면 파일을 못 찾았거나, 파일을 열 권한이 없었다든가 하는 식이죠. 현재로서는 상황과는 관계없이 모든 에러에 대해 동일한 에러 메시지를 출력하고 있는데, 이는 사용자에게 어떠한 정보도 제공할 수 없을 것입니다! 네 번째로, expect가 서로 다른 에러를 처리하기 위해 반복적으로 사용되는데, 만일 사용자가 실행되기 충분한 인수를 지정하지 않고 프로그램을 실행한다면, 사용자는 러스트의 index out of bounds 에러를 얻게 될 것이고 이 에러는 문제를 명확하게 설명하지 못합니다. 모든 에러 처리 코드가 한 곳에 있어서 미래에 코드를 유지보수할 사람이 에러 처리 로직을 변경하기를 원할 경우 찾아봐야 하는 코드가 한 군데에만 있는 것이 가장 좋을 것입니다. 모든 에러 처리 코드를 한 곳에 모아두면 최종 사용자에게 의미 있는 메시지를 출력할 수 있습니다. 이 프로젝트를 리팩터링하여 위의 네 가지 문제를 해결해 봅시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러 처리 향상을 위한 리팩터링 » 모듈성과 에러 처리 향상을 위한 리팩터링","id":"219","title":"모듈성과 에러 처리 향상을 위한 리팩터링"},"22":{"body":"다음으로 main.rs 소스 파일을 만들어 봅시다. 러스트 파일은 항상 .rs 확장자로 끝납니다. 파일명을 지을 때는 두 단어 이상으로 이루어질 경우에는 helloworld.rs 와 같이 붙여서 쓰지 않고 hello_world.rs 처럼 단어 사이에 밑줄 (_) 을 넣는 것이 관례입니다. main.rs 파일에 예제 1-1 코드를 입력합시다. 파일명: main.rs fn main() { println!(\"Hello, world!\");\n} 예제 1-1: Hello, world!를 출력하는 프로그램 파일을 저장하고 터미널 창으로 돌아가 ~/projects/hello_world 디렉터리로 갑니다. Linux, macOS 사용자는 다음 명령어를 입력하여 컴파일하고 실행할 수 있습니다: $ rustc main.rs\n$ ./main\nHello, world! Windows에서는 ./main을 .\\main.exe로 바꿔주시면 됩니다: > rustc main.rs\n> .\\main.exe\nHello, world! 사용하시는 운영체제와 상관없이 터미널에 Hello, world!가 출력되면 정상입니다. 출력되지 않으면 ‘트러블 슈팅’ 내용을 참고해 도움을 얻을 방법을 찾아보세요. 문제없이 Hello, world!가 출력됐다면, 축하드립니다! 여러분은 공식적으로 러스트 프로그램을 작성했으니 이제 어엿한 러스트 프로그래머입니다!","breadcrumbs":"시작해봅시다 » Hello, World! » 러스트 프로그램 작성하고 실행하기","id":"22","title":"러스트 프로그램 작성하고 실행하기"},"220":{"body":"여러 작업에 대한 책임을 main 함수에 떠넘기는 조직화 문제는 많은 바이너리 프로젝트에서 흔한 일입니다. 이에 따라 러스트 커뮤니티는 main이 커지기 시작할 때 이 바이너리 프로그램의 별도 관심사를 나누기 위한 가이드라인을 개발했습니다. 이 프로세스는 다음의 단계로 구성되어 있습니다: 프로그램을 main.rs 와 lib.rs 로 분리하고 프로그램 로직을 lib.rs 로 옮기세요. 커맨드 라인 파싱 로직이 작은 동안에는 main.rs 에 남을 수 있습니다. 커맨드 라인 파싱 로직이 복잡해지기 시작하면, main.rs 로부터 추출하여 lib.rs 로 옮기세요. 이 과정을 거친 후 main 함수에 남아있는 책임소재는 다음으로 한정되어야 합니다: 인수 값을 가지고 커맨드 라인 파싱 로직 호출하기 그 밖의 설정 lib.rs 의 run 함수 호출 run이 에러를 반환할 때 에러 처리하기 이 패턴은 관심사 분리에 관한 것입니다: main.rs 는 프로그램의 실행을 다루고, lib.rs 는 당면한 작업의 모든 로직을 처리합니다. main 함수를 직접 테스트할 수 없으므로, 이 구조는 lib.rs 내의 함수 형태로 테스트를 옮기게 하여 여러분의 모든 프로그램 로직을 테스트하게끔 합니다. main.rs 에 남겨진 코드는 정확한지 검증할 때 읽는 것만으로도 충분할 정도로 작아질 것입니다. 이 프로세스를 따르는 것으로 프로그램 작업을 다시 해 봅시다. 인수 파서 추출 커맨드 라인 파싱 로직을 src/lib.rs 로 옮기기 위한 준비 단계로 인수를 파싱하기 위한 기능을 main이 호출할 함수로 추출하겠습니다. 예제 12-5는 새로 시작하는 main과 호출되는 새로운 함수 parse_config를 보여주는데, 여기서는 잠깐 src/main.rs 에 정의하겠습니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\n# fn main() { let args: Vec = env::args().collect(); let (query, file_path) = parse_config(&args); // --생략--\n# # println!(\"Searching for {}\", query);\n# println!(\"In file {}\", file_path);\n# # let contents = fs::read_to_string(file_path)\n# .expect(\"Should have been able to read the file\");\n# # println!(\"With text:\\n{contents}\");\n} fn parse_config(args: &[String]) -> (&str, &str) { let query = &args[1]; let file_path = &args[2]; (query, file_path)\n} 예제 12-5: main으로부터 parse_config 함수 추출 여전히 커맨드 라인 인수는 벡터로 모으지만, main 함수 내에서 인덱스 1번의 인수 값을 query 변수에 할당하고 인덱스 2번의 인수 값을 file_path 변수에 할당하는 대신, 전체 벡터를 parse_config 함수에 넘깁니다. 그러면 parse_config 함수는 어떤 인수 값이 어떤 변수에 들어갈지 정하는 로직을 담고 있고 이 값들을 main에게 다시 넘겨줍니다. 여전히 query와 file_path 변수는 main 안에서 만들지만, main은 더 이상 커맨드 라인 인수와 변수들이 어떻게 대응되는지를 결정할 책임이 없습니다. 이러한 재작업은 우리의 작은 프로그램에 대해서는 지나쳐 보일지도 모르겠으나, 우리는 작게, 점진적인 단계로 리팩터링을 하는 중입니다. 이 변경 후에 프로그램을 다시 실행하여 인수 파싱이 여전히 동작하는지 검증하세요. 진행률을 자주 체크하는 것은 좋은 일이며, 문제가 발생했을 때 그 원인을 식별하는 데 도움이 됩니다. 설정 값 묶기 parse_config 함수를 더욱 개선하기 위해 작은 단계를 하나 더 진행할 수 있습니다. 현재는 튜플을 반환하는 중인데, 그런 다음 이 튜플을 개별 부분으로 즉시 다시 쪼개고 있습니다. 이는 아직 적절한 추상화가 이루어지지 않았다는 신호일 수 있습니다. 개선의 여지가 남아있음을 보여주는 또 다른 지표는 parse_config의 config 부분인데, 이는 반환하는 두 값이 연관되어 있고 둘 모두 하나의 설정 값을 이루는 부분임을 의미합니다. 현재 두 값을 튜플로 묶는 것 말고는 데이터의 구조에서 이러한 의미를 전달하지 못하고 있습니다; 그래서 이 두 값을 하나의 구조체에 넣고 구조체 필드에 각각 의미가 있는 이름을 부여하려고 합니다. 그렇게 하는 것이 미래에 이 코드를 유지보수하는 사람에게 이 서로 다른 값들이 어떻게 연관되어 있고 이 값들의 목적은 무엇인지를 더 쉽게 이해하도록 만들어 줄 것입니다. 예제 12-6은 parse_config 함수에 대한 개선을 보여줍니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\n# fn main() { let args: Vec = env::args().collect(); let config = parse_config(&args); println!(\"Searching for {}\", config.query); println!(\"In file {}\", config.file_path); let contents = fs::read_to_string(config.file_path) .expect(\"Should have been able to read the file\"); // --생략--\n# # println!(\"With text:\\n{contents}\");\n} struct Config { query: String, file_path: String,\n} fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path }\n} 예제 12-6: Config 구조체의 인스턴스를 반환하도록 하는 parse_config 리팩터링 query와 file_path라는 이름의 필드를 갖도록 정의된 Config라는 이름의 구조체를 추가했습니다. parse_config의 시그니처는 이제 Config 값을 반환함을 나타냅니다. parse_config 본문에서는 원래 args의 String 값들을 참조하는 문자열 슬라이스를 반환했는데, 이제는 String 값을 소유한 Config를 정의했습니다. main 안에 있는 args 변수는 인수 값들의 소유자이고 parse_config 함수에게는 이 값을 빌려주고 있을 뿐인데, 이는 즉 Config가 args의 값에 대한 소유권을 가져가려고 하면 러스트의 대여 규칙을 위반하게 된다는 의미입니다. String 데이터를 관리하는 방법은 다양하며, 가장 쉬운 방법은 (다소 비효율적이지만) 그 값에서 clone 메서드를 호출하는 것입니다. 이는 데이터의 전체 복사본을 만들어 Config 인스턴스가 소유할 수 있게 해주는데, 이는 문자열 데이터에 대한 참조자를 저장하는 것에 비해 더 많은 시간과 메모리를 소비합니다. 그러나 값의 복제는 참조자의 라이프타임을 관리할 필요가 없어지기 때문에 코드를 매우 직관적으로 만들어 주기도 하므로, 이러한 환경에서 약간의 성능을 포기하고 단순함을 얻는 것은 가치 있는 절충안입니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러 처리 향상을 위한 리팩터링 » 바이너리 프로젝트에 대한 관심사 분리","id":"220","title":"바이너리 프로젝트에 대한 관심사 분리"},"221":{"body":"러스타시안들 중에서 많은 이들이 런타임 비용의 이유로 clone을 사용한 소유권 문제 해결을 회피하는 경향을 가지고 있습니다. 13장 에서 이러한 종류의 상황에서 더 효율적인 메서드를 사용하는 방법을 배울 것입니다. 하지만 프로젝트를 계속 진행하기 위해 지금으로서는 약간의 문자열을 복사하는 정도는 괜찮은데, 이 복사가 딱 한 번만 일어나고 파일 경로와 질의 문자열이 매우 작기 때문입니다. 한 번에 매우 최적화된 코드 작성을 시도하기보다는 다소 비효율적이라도 동작하는 프로그램을 만드는 편이 좋습니다. 여러분이 러스트에 더 경험을 쌓게 되면 가장 효율적인 해답을 가지고 시작하기 더 쉽겠으나, 지금으로선 clone을 호출하는 것도 충분히 허용될만 합니다. main을 업데이트하여 parse_config가 반환한 Config 인스턴스를 config라는 이름의 변수에 위치시켰고, 이전에 별개로 사용된 query와 file_path 대신 이제는 Config 구조체의 필드를 이용합니다. 이제 코드가 query와 file_path가 서로 연관되어 있고 이들의 목적이 프로그램의 동작 방법을 설정하기 위함임을 더 명료하게 전달합니다. 이러한 값을 사용하는 모든 코드는 config 인스턴스에서 목적에 맞게 명명된 필드 값을 찾을 수 있습니다. Config를 위한 생성자 만들기 여기까지 해서 main으로부터 커맨드 라인 인수 파싱을 책임지는 로직을 추출하여 parse_config 함수에 위치시켰습니다. 그렇게 하면 query와 file_path 값이 연관되어 있고 이 관계가 코드로부터 전달된다는 것을 알기 쉽게 해주었습니다. 그다음 query와 file_path의 목적에 연관된 이름을 갖고 parse_config 함수로부터 반환되는 값을 구조체 필드 값이 되도록 하기 위해 Config 구조체를 추가하였습니다. 따라서 이제 parse_config 함수의 목적이 Config 인스턴스를 생성하는 것이 되었으므로, parse_config를 일반 함수에서 Config 구조체와 연관된 new라는 이름의 함수로 바꿀 수 있겠습니다. 이러한 변경이 코드를 더 자연스럽게 만들어 줄 것입니다. String 같은 표준 라이브러리 타입의 인스턴스 생성은 String::new를 호출하는 것으로 할 수 있습니다. 비슷하게 parse_config를 Config와 연관된 함수 new로 변경함으로써 Config 인스턴스의 생성을 Config::new의 호출로 할 수 있을 것입니다. 예제 12-7은 이를 위한 변경점을 보여줍니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\n# fn main() { let args: Vec = env::args().collect(); let config = Config::new(&args);\n# # println!(\"Searching for {}\", config.query);\n# println!(\"In file {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"Should have been able to read the file\");\n# # println!(\"With text:\\n{contents}\"); // --생략--\n} // --생략-- # struct Config {\n# query: String,\n# file_path: String,\n# }\n# impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let file_path = args[2].clone(); Config { query, file_path } }\n} 예제 12-7: parse_config를 Config::new로 변경하기 원래 parse_config를 호출하고 있던 main 부분을 Config::new 호출로 바꿨습니다. parse_config의 이름은 new로 변경되었고 impl 블록에 옮겨졌는데, 이것이 Config와 new 함수를 연관시켜 줍니다. 이 코드를 다시 한번 컴파일하여 잘 동작하는지 확인하세요.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러 처리 향상을 위한 리팩터링 » clone을 사용한 절충안","id":"221","title":"clone을 사용한 절충안"},"222":{"body":"이제부터는 에러 처리 기능을 수정할 겁니다. args 벡터에 3개보다 적은 아이템이 들어있는 경우에는 인덱스 1이나 2의 값에 접근을 시도하는 것이 프로그램의 패닉을 일으킬 것이라는 점을 상기합시다. 아무런 인수 없이 프로그램을 실행해 보세요; 아래처럼 나올 것입니다: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep`\nthread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace index out of bounds: the len is 1 but the index is 1 줄은 프로그래머를 위한 에러 메시지입니다. 최종 사용자들에게는 무엇을 대신 해야 하는지 이해시키는 데 도움이 안 될 것입니다. 이제 수정해 봅시다. 에러 메시지 개선 예제 12-8에서는 인덱스 1과 2에 접근하기 전에 슬라이스의 길이가 충분한지 검증하는 기능을 new 함수에 추가했습니다. 만일 슬라이스가 충분히 길지 않다면, 프로그램은 패닉을 일으키고 더 나은 에러 메시지를 보여줍니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\n# # fn main() {\n# let args: Vec = env::args().collect();\n# # let config = Config::new(&args);\n# # println!(\"Searching for {}\", config.query);\n# println!(\"In file {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"Should have been able to read the file\");\n# # println!(\"With text:\\n{contents}\");\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config { // --생략-- fn new(args: &[String]) -> Config { if args.len() < 3 { panic!(\"not enough arguments\"); } // --생략--\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Config { query, file_path }\n# }\n# } 예제 12-8: 인수의 개수 검사 추가 이 코드는 예제 9-13에서 작성했었던 Guess::new 함수 와 비슷한데, 거기서는 value 인수가 유효한 값의 범위 밖인 경우 panic!을 호출했었지요. 여기서는 값의 범위를 검사하는 대신, args의 길이가 최소 3이고 이 조건을 만족하는 가정 아래에서 함수의 나머지 부분이 동작할 수 있음을 검사하고 있습니다. 만일 args가 아이템을 세 개보다 적게 가지고 있다면 이 조건은 참이 되고, panic! 매크로를 호출하여 프로그램을 즉시 종료시킵니다. new에 이렇게 몇 줄을 추가한 다음, 다시 한번 아무 인수 없이 프로그램을 실행하여 이제 에러가 어떤 식으로 보이는지 살펴봅시다: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep`\nthread 'main' panicked at 'not enough arguments', src/main.rs:26:13\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace 이번 출력이 더 좋습니다: 이제는 적절한 에러 메시지가 되었습니다. 하지만 사용자들에게 제공할 필요 없는 추가적인 정보도 제공하고 있습니다. 어쩌면 예제 9-13에서 사용했던 기술을 여기에 써먹는 것이 최선은 아닌가 봅니다: 9장에서 얘기한 것처럼 panic!을 호출하는 것은 사용의 문제보다는 프로그램의 문제에 더 적합합니다. 대신에 여러분이 9장에서 배웠던 다른 기술, 즉 성공인지 혹은 에러인지를 나타내는 Result를 반환하는 기술을 사용해 보겠습니다. panic! 호출 대신 Result 반환하기 성공한 경우에는 Config를 담고 있고 에러가 난 경우에는 문제를 설명해줄 Result 값을 반환시킬 수 있습니다. 또한 new라는 함수 이름은 build로 변경할 것인데, 이는 많은 프로그래머가 new 함수가 절대 실패하지 않으리라 예상하기 때문입니다. Config::build가 main과 소통하고 있을 때 Result 타입을 사용하여 문제에 대한 신호를 줄 수 있습니다. 그러면 main을 수정하여 Err 배리언트를 사용자에게 더 실용적인 에러 메시지로 변경할 수 있고, 이는 panic!의 호출로 인한 thread 'main'과 RUST_BACKTRACE에 대해 감싸져 있는 텍스트를 없앨 수 있겠습니다. 예제 12-9는 이제 Config::build라고 하는 함수의 반환 값과 Result를 반환할 필요가 있는 함수 본문을 위해서 필요한 변경점을 보여줍니다. main도 마찬가지로 수정하지 않으면 컴파일 되지 않는다는 점을 유의하세요. 이건 다음에 하겠습니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\n# # fn main() {\n# let args: Vec = env::args().collect();\n# # let config = Config::new(&args);\n# # println!(\"Searching for {}\", config.query);\n# println!(\"In file {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"Should have been able to read the file\");\n# # println!(\"With text:\\n{contents}\");\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# impl Config { fn build(args: &[String]) -> Result { if args.len() < 3 { return Err(\"not enough arguments\"); } let query = args[1].clone(); let file_path = args[2].clone(); Ok(Config { query, file_path }) }\n} 예제 12-9: Config::build로부터 Result 반환하기 우리의 build 함수는 성공한 경우 Config를, 에러가 난 경우 &'static str을 갖는 Result를 반환합니다. 에러 값은 언제나 'static 라이프타임을 갖는 문자열 리터럴일 것입니다. 함수 본문에는 두 가지 변경점이 있었습니다: 사용자가 충분한 인수를 넘기지 않았을 때 panic!을 호출하는 대신 이제 Err 값을 반환하며, 반환 값 Config를 Ok로 감쌌습니다. 이러한 변경점이 함수의 새로운 타입 시그니처에 맞도록 합니다. Config::build로부터 Err 값을 반환하는 것은 main 함수가 build 함수로부터 반환된 Result 값을 처리하여 에러가 난 경우 프로세스를 더 깔끔하게 종료하도록 해줍니다. Config::build 호출과 에러 처리 에러가 발생한 경우를 처리하여 사용자 친화적인 메시지를 출력하기 위해서는, 예제 12-10처럼 main을 수정하여 Config::build에 의해 반환되는 Result를 처리할 필요가 있습니다. 또한 panic!으로부터 벗어나서 직접 0이 아닌 에러 코드로 커맨드 라인 도구를 종료하도록 구현할 것입니다. 0이 아닌 종료 상태값은 프로그램을 호출한 프로세스에게 에러 상태값과 함께 종료되었음을 알려주는 관례입니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\nuse std::process; fn main() { let args: Vec = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { println!(\"Problem parsing arguments: {err}\"); process::exit(1); }); // --생략--\n# # println!(\"Searching for {}\", config.query);\n# println!(\"In file {}\", config.file_path);\n# # let contents = fs::read_to_string(config.file_path)\n# .expect(\"Should have been able to read the file\");\n# # println!(\"With text:\\n{contents}\");\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } 예제 12-10: Config 생성이 실패했을 경우 에러 코드와 함께 종료하기 위의 코드에서는 아직 상세히 다루지 않은 unwrap_or_else 메서드를 사용했는데, 이는 표준 라이브러리의 Result에 구현되어 있습니다. unwrap_or_else을 사용하면 커스터마이징된 panic!이 아닌 에러 처리를 정의할 수 있습니다. 만일 Result가 Ok 값이라면 이 메서드의 동작은 unwrap과 유사합니다: 즉 Ok가 감싸고 있는 안쪽 값을 반환합니다. 하지만 값이 Err 값이라면, 이 메서드는 클로저 (closure) 안의 코드를 호출하는데, 이는 unwrap_or_else의 인수로 넘겨준 우리가 정의한 익명 함수입니다. 클로저에 대해서는 13장 에서 더 자세히 다루겠습니다. 지금은 그저 unwrap_or_else가 Err의 내부 값을 클로저의 세로 파이프 (|) 사이에 있는 err 인수로 넘겨주는데, 이번 경우 그 값은 예제 12-9에 추가한 정적 문자열 \"not enough arguments\"이라는 정도만 알면 됩니다. 그러면 실행했을 때 클로저 내의 코드가 err 값을 사용할 수 있게 됩니다. 새로 추가된 use 줄은 표준 라이브러리로부터 process를 스코프 안으로 가져옵니다. 에러가 난 경우 실행될 클로저 내의 코드는 딱 두 줄입니다: err 값을 출력한 다음 process::exit를 호출하는 것이지요. process::exit 함수는 프로그램을 즉시 멈추고 넘겨진 숫자를 종료 상태 코드로서 반환하게 될 것입니다. 이는 예제 12-8에서 사용했던 panic! 기반의 처리와 비슷하지만, 이제는 추가 출력문들이 사라지게 됩니다. 한번 시도해 봅시다: $ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.48s Running `target/debug/minigrep`\nProblem parsing arguments: not enough arguments 훌륭하군요! 이 출력문이 사용자들에게 훨씬 친숙합니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러 처리 향상을 위한 리팩터링 » 에러 처리 수정","id":"222","title":"에러 처리 수정"},"223":{"body":"이제 설정 값 파싱의 리팩터링을 끝냈으니, 프로그램 로직으로 돌아와 봅시다. ‘바이너리 프로젝트에 대한 관심사 분리’ 절에서 기술한 바와 같이, 현재 main 함수에 있는 로직 중 설정 값이나 에러 처리와는 관련되지 않은 모든 로직을 run이라는 함수로 추출하도록 하겠습니다. 그렇게 하고 나면 main은 간결하고 검사하기 쉬워질 것이며, 나머지 모든 로직에 대한 테스트를 작성할 수 있게 될 것입니다. 예제 12-11은 추출된 run 함수를 보여줍니다. 지금은 함수 추출에 대한 작고 점진적인 개선만 하고 있습니다. 여전히 함수는 src/main.rs 에 정의되어 있습니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\n# use std::process;\n# fn main() { // --생략-- # let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"Problem parsing arguments: {err}\");\n# process::exit(1);\n# });\n# println!(\"Searching for {}\", config.query); println!(\"In file {}\", config.file_path); run(config);\n} fn run(config: Config) { let contents = fs::read_to_string(config.file_path) .expect(\"Should have been able to read the file\"); println!(\"With text:\\n{contents}\");\n} // --생략--\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } 예제 12-11: 나머지 프로그램 로직을 담는 run 함수 추출 run 함수는 이제 이는 파일을 읽는 부분부터 시작되는, main으로부터 남은 모든 로직을 담고 있습니다. run 함수는 Config 인스턴스를 인수로 취합니다. run 함수로부터 에러 반환하기 run 함수로 분리된 남은 프로그램 로직에 대하여, 예제 12-9에서 Config::build에 했던 것처럼 에러 처리 기능을 개선할 수 있습니다. run 함수는 뭔가 잘못되면 expect를 호출하여 프로그램이 패닉이 되도록 하는 대신 Result를 반환할 것입니다. 이를 통해 에러 처리에 관한 로직을 사용자 친화적인 방식으로 main 안에 더욱 통합시킬 수 있습니다. 예제 12-12는 run의 시그니처와 본문에 필요한 변경점을 보여줍니다. 파일명: src/main.rs # use std::env;\n# use std::fs;\n# use std::process;\nuse std::error::Error; // --생략-- # # fn main() {\n# let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"Problem parsing arguments: {err}\");\n# process::exit(1);\n# });\n# # println!(\"Searching for {}\", config.query);\n# println!(\"In file {}\", config.file_path);\n# # run(config);\n# }\n# fn run(config: Config) -> Result<(), Box> { let contents = fs::read_to_string(config.file_path)?; println!(\"With text:\\n{contents}\"); Ok(())\n}\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } 예제 12-12: run 함수가 Result를 반환하도록 변경하기 여기서는 세 가지 중요한 변경점이 있습니다. 첫 번째로, run 함수의 반환 타입이 Result<(), Box>으로 변경되었습니다. 이 함수는 원래 유닛 타입 ()를 반환했었는데, Ok인 경우에 반환될 값으로써 계속 유지하고 있습니다. 에러 타입에 대해서는 트레이트 객체 Box를 사용했습니다 (그리고 상단에 use 구문을 사용하여 std::error::Error를 스코프로 가져 왔습니다). 트레이트 객체에 대해서는 17장 에서 다룰 예정입니다. 지금은 그저 Box는 이 함수가 Error 트레이트를 구현한 어떤 타입을 반환하는데, 그 반환 값이 구체적으로 어떤 타입인지는 특정하지 않아도 된다는 것을 의미한다는 정도만 알면 됩니다. 이는 서로 다른 에러의 경우에서 서로 다른 타입이 될지도 모를 에러값을 반환하는 유연성을 제공합니다. dyn 키워드는 ‘동적 (dynamic)’의 줄임말입니다. 두 번째로 9장 에서 이야기했던 ? 연산자를 활용하여 expect의 호출을 제거했습니다. ?은 에러 상황에서 panic! 대신 호출하는 쪽이 처리할 수 있도록 현재의 함수로부터 에러 값을 반환할 것입니다. 세 번째로 run 함수는 이제부터 성공한 경우 Ok 값을 반환합니다. run 함수의 성공 타입은 시그니처 상에서 ()로 선언되었는데, 이는 유닛 타입 값을 Ok 값으로 감쌀 필요가 있다는 의미입니다. 이 Ok(()) 문법은 처음엔 좀 이상해 보일런지도 모릅니다만, 이렇게 ()를 사용하는 것은 run의 호출하여 부작용에 대해서만 처리하겠다는 것을 가리키는 자연스러운 방식입니다; 즉 반환 값이 필요 없는 경우입니다. 이 코드를 실행시키면, 컴파일은 되지만 다음과 같은 경고가 나타날 것입니다: $ cargo run the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep)\nwarning: unused `Result` that must be used --> src/main.rs:19:5 |\n19 | run(config); | ^^^^^^^^^^^ | = note: this `Result` may be an `Err` variant, which should be handled = note: `#[warn(unused_must_use)]` on by default warning: `minigrep` (bin \"minigrep\") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.71s Running `target/debug/minigrep the poem.txt`\nSearching for the\nIn file poem.txt\nWith text:\nI'm nobody! Who are you?\nAre you nobody, too?\nThen there's a pair of us - don't tell!\nThey'd banish us, you know. How dreary to be somebody!\nHow public, like a frog\nTo tell your name the livelong day\nTo an admiring bog! 러스트가 우리에게 Result 값이 무시되고 있으며 Result 값이 에러가 발생했음을 나타낼지도 모른다고 알려주는군요. 그렇지만 에러가 있는지 없는지 알아보는 검사를 하지 않고 있고, 그래서 어떤 에러 처리 코드를 의도했었던 것은 아닌지를 상기시켜 줍니다! 이제 이 문제를 바로잡아 봅시다. main에서 run으로부터 반환된 에러 처리하기 이제 예제 12-10의 Config::build에 사용했던 것과 비슷한 기술을 사용하여 에러를 검사하고 이를 처리해 볼 것인데, 약간 다른 점이 있습니다: 파일명: src/main.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# use std::process;\n# fn main() { // --생략-- # let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"Problem parsing arguments: {err}\");\n# process::exit(1);\n# });\n# println!(\"Searching for {}\", config.query); println!(\"In file {}\", config.file_path); if let Err(e) = run(config) { println!(\"Application error: {e}\"); process::exit(1); }\n}\n# # fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # println!(\"With text:\\n{contents}\");\n# # Ok(())\n# }\n# # struct Config {\n# query: String,\n# file_path: String,\n# }\n# # impl Config {\n# fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# } run이 Err 값을 반환했는지 검사하고 만일 그렇다면 process::exit(1)를 호출하기 위해 사용한 unwrap_or_else 대신 if let이 사용되었습니다. run 함수가 반환한 값은 Config 인스턴스를 반환하는 Config::build과 동일한 방식대로 unwrap을 하지 않아도 됩니다. run이 성공한 경우 ()를 반환하기 때문에 에러를 찾는 것만 신경 쓰면 되므로, 고작 ()나 들어있을 값을 반환하기 위해 unwrap_or_else를 쓸 필요는 없어집니다. if let과 unwrap_or_eulse 함수의 본문은 둘 모두 동일합니다: 즉, 에러를 출력하고 종료합니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러 처리 향상을 위한 리팩터링 » main으로부터 로직 추출하기","id":"223","title":"main으로부터 로직 추출하기"},"224":{"body":"여기까지의 minigrep 프로젝트는 괜찮아 보이는군요! 이제 src/main.rs 파일을 쪼개어 코드 일부를 src/lib.rs 파일에 넣을 것입니다. 그렇게 하여 코드를 테스트할 수 있고 src/main.rs 파일의 책임 소재를 더 적게 할 수 있습니다. main 함수가 아닌 모든 코드를 src/main.rs 에서 src/lib.rs 로 옮깁시다: run 함수 정의 부분 이와 관련된 use 구문들 Config 정의 부분 Config::build 함수 정의 부분 src/lib.rs 의 내용은 예제 12-13과 같은 시그니처를 가지고 있어야 합니다. (간결성을 위해 함수의 본문은 생략하였습니다.) src/main.rs 를 예제 12-14처럼 수정하기 전까지는 컴파일이 되지 않음을 유의하세요. 파일명: src/lib.rs use std::error::Error;\nuse std::fs; pub struct Config { pub query: String, pub file_path: String,\n} impl Config { pub fn build(args: &[String]) -> Result { // --생략--\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path }) }\n} pub fn run(config: Config) -> Result<(), Box> { // --생략--\n# let contents = fs::read_to_string(config.file_path)?;\n# # println!(\"With text:\\n{contents}\");\n# # Ok(())\n} 예제 12-13: Config와 run을 src/lib.rs 안으로 옮기기 pub 키워드를 자유롭게 사용했습니다: Config와 이 구조체의 각 필드 및 build 메서드, 그리고 run 함수에 대해 사용했지요. 이제 우리는 테스트해 볼 수 있는 공개 API를 갖춘 라이브러리 크레이트를 가지게 되었습니다! 이제는 예제 12-14처럼 src/lib.rs 로 옮겨진 코드를 src/main.rs 내의 바이너리 크레이트 스코프 쪽으로 가져올 필요가 생겼습니다. 파일명: src/main.rs use std::env;\nuse std::process; use minigrep::Config; fn main() { // --생략--\n# let args: Vec = env::args().collect();\n# # let config = Config::build(&args).unwrap_or_else(|err| {\n# println!(\"Problem parsing arguments: {err}\");\n# process::exit(1);\n# });\n# # println!(\"Searching for {}\", config.query);\n# println!(\"In file {}\", config.file_path);\n# if let Err(e) = minigrep::run(config) { // --생략--\n# println!(\"Application error: {e}\");\n# process::exit(1); }\n} 예제 12-14: src/main.rs 에서 minigrep 라이브러리 크레이트 사용하기 use minigrep::Config 줄을 추가하여 라이브러리 크레이트로부터 바이너리 크레이트 스코프로 Config 타입을 가져오고, run 함수 앞에는 크레이트 이름을 붙였습니다. 이제 모든 기능이 연결되어 동작해야 합니다. cargo run으로 프로그램을 실행하여 모든 것이 정상적으로 동작하는지 확인하세요. 휴우! 작업이 참 많았습니다만, 우리는 미래의 성공을 위한 기반을 닦았습니다. 이제 에러를 처리하기도 훨씬 쉽고, 코드도 훨씬 모듈화되었습니다. 이제부터는 거의 모든 작업이 src/lib.rs 내에서 완료될 것입니다. 이전 코드에서는 어려웠지만 새 코드에서는 쉬운 작업을 수행하여 이 새로운 모듈성의 이점을 활용해 봅시다: 테스트를 작성해 보겠습니다!","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러 처리 향상을 위한 리팩터링 » 라이브러리 크레이트로 코드 쪼개기","id":"224","title":"라이브러리 크레이트로 코드 쪼개기"},"225":{"body":"로직을 src/lib.rs 로 추출하고 인수 모으기와 에러 처리는 src/main.rs 에 남겨두었으니, 이제는 코드의 핵심 기능에 대한 테스트를 작성하기 무척 쉽습니다. 커맨드 라인에서 바이너리를 호출할 필요 없이 다양한 인수 값으로 함수를 직접 호출하여 반환 값을 검사해 볼 수 있습니다. 이 절에서는 아래의 단계를 따르는 테스트 주도 개발 (Test-Driven Development, TDD) 프로세스를 사용하여 minigrep 프로그램의 검색 로직을 추가해 보도록 하겠습니다: 실패하는 테스트를 작성하고 실행하여, 여러분이 예상한 이유대로 실패하는지 확인합니다. 이 새로운 테스트를 통과하기 충분한 정도의 코드만 작성하거나 수정하세요. 추가하거나 변경한 코드를 리팩터링하고 테스트가 계속 통과하는지 확인하세요. 1단계로 돌아가세요! 그저 소프트웨어 작성의 수많은 방식 중 하나일 뿐이지만, TDD는 코드 설계를 주도하는데 도움이 됩니다. 테스트를 통과하도록 해줄 코드를 작성하기 전에 테스트 먼저 작성하는 것은 프로세스 전체에 걸쳐 높은 테스트 범위를 유지하는 데 도움을 줍니다. 실제로 파일 내용에서 질의 문자열을 찾아보고 질의와 일치하는 라인의 목록을 생성하는 기능의 구현을 테스트 주도적으로 해볼 것입니다. 이 기능을 search라는 이름의 함수에 추가해 보겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 테스트 주도 개발로 라이브러리 기능 개발하기 » 테스트 주도 개발로 라이브러리 기능 개발하기","id":"225","title":"테스트 주도 개발로 라이브러리 기능 개발하기"},"226":{"body":"프로그램 동작을 확인하기 위해 사용되었던 src/lib.rs 와 src/main.rs 의 println! 구문들은 이제 더 이상 필요가 없으므로 제거합시다. 그런 다음 11장 에서처럼 src/lib.rs 에 test 모듈과 함께 테스트 함수를 추가하세요. 테스트 함수는 search 함수가 가져야 할 동작을 지정합니다: 즉 질의 값과 검색할 텍스트를 입력받아서 텍스트로부터 질의 값을 담고 있는 라인들만 반환하는 것이죠. 예제 12-15는 이러한 테스트를 보여주는데, 아직 컴파일되진 않을 것입니다. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn one_result() { let query = \"duct\"; let contents = \"\\\nRust:\nsafe, fast, productive.\nPick three.\"; assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents)); }\n} 예제 12-15: 구현하고자 하는 search 함수를 위한 실패하는 테스트 만들기 이 테스트는 문자열 \"duct\"를 검색합니다. 검색하는 텍스트는 세 줄인데, 그중 한 줄만이 \"duct\"를 가지고 있습니다 (앞의 큰 따옴표 뒤에 붙은 역슬래시는 이 문자열 리터럴 내용의 앞에 줄 바꿈 문자를 집어넣지 않도록 러스트에게 알려주는 것임을 유의하세요). search 함수가 반환하는 값은 우리가 예상하는 라인만 가지고 있을 것이라고 단언해 두었습니다. 이 테스트는 아직 컴파일도 되지 않을 것이므로 테스트를 실행시켜서 실패하는 걸 지켜볼 수는 없습니다: 아직 search 함수가 없으니까요! TDD 원칙에 따라서, 예제 12-16과 같이 항상 빈 벡터를 반환하는 search 함수 정의부를 추가하는 것으로 컴파일과 테스트가 동작하기에 딱 충분한 코드만 집어넣어 보겠습니다. 그러면 테스트는 컴파일되고, 반환된 빈 벡터가 \"safe, fast, productive.\" 라인을 가지고 있는 벡터와 일치하지 않으므로 실패해야 합니다. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![]\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 예제 12-16: 테스트가 딱 컴파일만 될 정도의 search 함수 정의하기 search의 시그니처에는 명시적 라이프타임 'a가 정의될 필요가 있고 이 라이프타임이 contents 인수와 반환 값에 사용되고 있음을 주목하세요. 10장 에서 본 것처럼 라이프타임 매개변수는 어떤 인수의 라이프타임이 반환 값의 라이프타임과 연결되는지를 특정한다는 점을 상기해 봅시다. 위의 경우에는 반환된 벡터에 (인수 query 쪽이 아니라) 인수 contents의 슬라이스를 참조하는 문자열 슬라이스가 들어있음을 나타내고 있습니다. 바꿔 말하면, 지금 러스트에게 search 함수에 의해 반환된 데이터가 search 함수의 contents 인수로 전달된 데이터만큼 오래 살 것이라는 것을 말해준 것입니다. 이것이 중요합니다! 슬라이스에 의해 참조된 데이터는 그 참조자가 유효한 동안 유효할 필요가 있습니다; 만일 컴파일러가 contents 대신 query의 문자열 슬라이스를 만들고 있다고 가정하면, 안전성 검사는 정확하지 않게 될 것입니다. 라이프타임 명시를 잊어먹고 이 함수의 컴파일을 시도하면, 다음과 같은 에러를 얻게 됩니다: $ cargo build Compiling minigrep v0.1.0 (file:///projects/minigrep)\nerror[E0106]: missing lifetime specifier --> src/lib.rs:28:51 |\n28 | pub fn search(query: &str, contents: &str) -> Vec<&str> { | ---- ---- ^ expected named lifetime parameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`\nhelp: consider introducing a named lifetime parameter |\n28 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> { | ++++ ++ ++ ++ For more information about this error, try `rustc --explain E0106`.\nerror: could not compile `minigrep` due to previous error 러스트는 두 인수 중 어떤 쪽이 필요한지 알 가능성이 없고, 따라서 이를 명시적으로 말해줄 필요가 있습니다. contents가 모든 텍스트를 가지고 있는 인수이고 이 텍스트에서 일치하는 부분을 반환하고 싶은 것이므로, 라이프타임 문법을 사용해 반환 값과 연결되어야 할 인수는 contents라는 사실을 알고 있습니다. 다른 프로그래밍 언어들은 시그니처에 인수와 반환 값을 연결하도록 요구하지 않습니다만, 이 연습은 시간이 지날수록 더 쉬워질 것입니다. 어쩌면 이 예제를 10장의 ‘라이프타임으로 참조자의 유효성 검증하기’ 절에 있는 예제와 비교하고 싶을지도 모르겠습니다. 이제 테스트를 실행해 봅시다: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 0.97s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test\ntest tests::one_result ... FAILED failures: ---- tests::one_result stdout ----\nthread 'tests::one_result' panicked at 'assertion failed: `(left == right)` left: `[\"safe, fast, productive.\"]`, right: `[]`', src/lib.rs:44:9\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 좋습니다. 예상대로 테스트는 실패했습니다. 이제 테스트가 통과되도록 해봅시다!","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 테스트 주도 개발로 라이브러리 기능 개발하기 » 실패하는 테스트 작성하기","id":"226","title":"실패하는 테스트 작성하기"},"227":{"body":"현재는 언제나 빈 벡터가 반환되고 있으므로 테스트가 실패하고 있습니다. 이를 고치고 search를 구현하려면 프로그램에서 아래의 단계를 따라야 합니다: 내용물의 각 라인에 대해 반복합니다. 해당 라인이 질의 문자열을 담고 있는지 검사합니다. 만일 그렇다면, 반환하고자 하는 값의 리스트에 추가합니다. 아니라면 아무것도 안 합니다. 매칭된 결과 리스트를 반환합니다. 라인들에 대한 반복을 시작으로 각 단계 별로 작업해 봅시다. lines 메서드로 라인들에 대해 반복하기 러스트는 문자열의 라인별 반복을 처리하기 위한 유용한 메서드를 제공하는데, 편리하게도 lines라는 이름이고 예제 12-17에서 보는 바와 같이 동작합니다. 아직 컴파일되지 않음을 주의하세요. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // do something with line }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 예제 12-17: contents의 각 줄에 대한 반복 lines 메서드는 반복자를 반환합니다. 반복자에 대해서는 13장 에서 더 깊이 다루겠습니다만, 예제 3-5 에서 이런 방식의 반복자 사용을 봤었음을 상기해 봅시다. 그때는 어떤 컬렉션 안의 각 아이템에 대해 어떤 코드를 실행시키기 위해 for과 함께 반복자를 사용했었지요. 각 라인에서 질의값 검색하기 다음으로는 현재의 라인에 질의 문자열이 들어있는지 검사해 보겠습니다. 다행히도 이걸 해주는 contains라는 이름의 유용한 메서드가 문자열에 있습니다! 예제 12-18처럼 search 함수에 contains 메서드 호출을 추가하세요. 아직 컴파일되지는 않음을 주의하세요. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { if line.contains(query) { // do something with line } }\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 예제 12-18: 라인이 query의 문자열을 포함하는지 알아보기 위한 기능 추가하기 이 시점에서는 아직 기능을 구축하는 중입니다. 컴파일되기 위해서는 함수 시그니처에 명시한 대로 함수 본문에서 어떤 값을 반환할 필요가 있습니다. 매칭된 라인 저장하기 이 함수를 완성하기 위해서는 반환하고자 하는 매칭된 라인들을 저장할 방법이 필요합니다. 이를 위해서 for 루프 전에 가변 벡터를 만들고 line을 이 벡터에 저장하기 위해 push 메서드를 호출할 수 있겠습니다. for 루프 뒤에는 예제 12-19와 같이 이 벡터를 반환합니다. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 예제 12-19: 매칭된 라인들을 저장하여 반환될 수 있게 하기 이제 search 함수는 query를 담고 있는 라인들만 반환해야 하고 테스트는 통과되어야 합니다. 테스트를 실행해 봅시다: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.22s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 1 test\ntest tests::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 테스트가 통과되었으므로, 함수가 동작한다는 사실을 알았습니다! 이 시점에서, 동일한 기능을 유지하여 테스트가 계속 통과되도록 유지하면서 이 검색 함수의 구현을 리팩터링할 기회를 고려해 볼 수 있겠습니다. 이 검색함수의 코드는 그렇게 나쁘진 않습니다만, 반복자의 몇몇 유용한 기능을 활용하고 있지는 않군요. 13장 에서 이 예제로 돌아올 건데, 거기서 반복자에 대해 더 자세히 탐구하고 어떻게 개선할 수 있는지 알아볼 것입니다. run 함수에서 search 함수 사용하기 이제 search 함수가 작동하고 테스트도 되었으니, run 함수에서 search를 호출할 필요가 있겠습니다. search 함수에 config.query 값과 run이 읽어 들인 contents를 넘겨줘야 합니다. 그러면 run은 search가 반환한 각 라인을 출력할 것입니다: 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# pub fn run(config: Config) -> Result<(), Box> { let contents = fs::read_to_string(config.file_path)?; for line in search(&config.query, &contents) { println!(\"{line}\"); } Ok(())\n}\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } search로부터 반환된 각 라인에 대해 여전히 for를 사용하여 출력하고 있습니다. 이제 전체 프로그램이 동작해야 합니다! 먼저 에밀리 딕킨슨의 시에서 딱 한 줄만 반환되도록 ‘frog’라는 단어를 넣어 시도해 봅시다: $ cargo run -- frog poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.38s Running `target/debug/minigrep frog poem.txt`\nHow public, like a frog 멋지군요! 이제 여러 줄과 매칭될 ‘body’ 같은 단어를 시도해 봅시다: $ cargo run -- body poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep body poem.txt`\nI'm nobody! Who are you?\nAre you nobody, too?\nHow dreary to be somebody! 마지막으로, ‘monomorphization’ 같이 이 시의 어디에도 없는 단어를 검색하는 경우 아무 줄도 안 나오는지 확인해 봅시다: $ cargo run -- monomorphization poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep monomorphization poem.txt` 훌륭하군요! 고전적인 도구에 대한 여러분만의 미니 버전을 만들었고 애플리케이션을 구조화하는 방법에 대해 많이 배웠습니다. 또한 파일 입출력과 라이프타임, 테스트, 커맨드 라인 파싱에 대해서도 약간씩 배웠습니다. 이 프로젝트를 정리하기 위해서, 환경 변수를 가지고 동작시키는 방법과 표준 에러로 출력하는 방법을 간략하게 보려고 하는데, 둘 모두 커맨드 라인 프로그램을 작성할 때 유용합니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 테스트 주도 개발로 라이브러리 기능 개발하기 » 테스트를 통과하도록 코드 작성하기","id":"227","title":"테스트를 통과하도록 코드 작성하기"},"228":{"body":"minigrep에 추가 기능을 넣어서 개선시켜 보겠습니다: 바로 환경 변수를 통해 사용자가 켤 수 있는 대소문자를 구분하지 않는 검색 옵션입니다. 이 기능을 커맨드 라인 옵션으로 만들어서 필요한 경우 사용자가 매번 입력하도록 요구할 수도 있겠으나, 환경 변수로 만듦으로써 사용자는 이 환경 변수를 한 번만 설정하고 난 다음 그 터미널 세션 동안에는 모든 검색을 대소문자 구분 없이 할 수 있게 됩니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 환경 변수 사용하기 » 환경 변수 사용하기","id":"228","title":"환경 변수 사용하기"},"229":{"body":"먼저 환경 변숫값이 있을 때 호출될 새로운 함수 search_case_insensitive를 추가하겠습니다. 계속하여 TDD 프로세스를 따를 것이므로, 첫 번째 단계는 다시 한번 실패하는 테스트를 작성하는 것입니다. 새로운 함수 search_case_insensitive를 추가하고 이전 테스트 이름은 예제 12-20처럼 두 테스트 간의 차이를 명확하게 하기 위해 one_result에서 case_sensitive로 바꾸겠습니다. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # for line in search(&config.query, &contents) {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# #[cfg(test)]\nmod tests { use super::*; #[test] fn case_sensitive() { let query = \"duct\"; let contents = \"\\\nRust:\nsafe, fast, productive.\nPick three.\nDuct tape.\"; assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents)); } #[test] fn case_insensitive() { let query = \"rUsT\"; let contents = \"\\\nRust:\nsafe, fast, productive.\nPick three.\nTrust me.\"; assert_eq!( vec![\"Rust:\", \"Trust me.\"], search_case_insensitive(query, contents) ); }\n} 예제 12-20: 추가하려는 대소문자 구분 없는 함수를 위한 새로운 실패 테스트 추가하기 예전 테스트의 contents도 수정되었음을 유의하세요. 대문자 D를 사용한 \"Duct tape.\" 라인을 추가였고 이는 대소문자를 구분하는 방식으로 검색할 때는 질의어 \"duct\"에 매칭되지 않아야 합니다. 이렇게 예전 테스트를 변경하는 것은 이미 구현된 대소문자를 구분하는 검색을 우발적으로 깨트리지 않도록 확인하는 데 도움을 줍니다. 이 테스트는 지금 통과되어야 하며 대소문자를 구분하지 않는 검색에 대해 작업을 하는 중에도 계속해서 통과되어야 합니다. 대소문자를 구분하지 않는 검색을 위한 새로운 테스트에서는 질의어로 \"rUsT\"를 사용합니다. 추가하려는 search_case_insensitive 함수에서 질의어 \"rUsT\"는 대소문자 구분이 질의어와 다르더라도 대문자 R로 시작하는 \"Rust:\"를 포함하는 라인 및 \"Trust me.\" 라인과 매칭되어야 합니다. 이것이 실패하는 테스트고, 아직 search_case_insensitive 함수를 정의하지 않았으므로 컴파일에 실패할 것입니다. 예제 12-16에서 테스트가 컴파일되고 실패하는 것을 지켜보기 위해 했었던 것과 마찬가지로, 간편하게 언제나 빈 벡터를 반환하는 뼈대 구현을 추가해 봅시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 환경 변수 사용하기 » 대소문자를 구분하지 않는 search 함수에 대한 실패하는 테스트 작성하기","id":"229","title":"대소문자를 구분하지 않는 search 함수에 대한 실패하는 테스트 작성하기"},"23":{"body":"방금 만든 ‘Hello, world!’ 프로그램을 자세히 살펴봅시다. 우선 첫 부분은 다음과 같습니다: fn main() { } 이 라인은 러스트에서 main이라는 이름의 함수를 정의합니다. main 함수는 특별한 함수로, 러스트 실행 프로그램에서 항상 가장 먼저 실행되는 함수입니다. 여기서는 매개변수를 받지 않고 아무것도 반환하지 않는 main이라는 함수를 선언합니다. 함수에 매개변수가 있을 때는 () 안쪽에 이를 작성해야 합니다. 함수 본문은 {}로 감싸집니다. 러스트에서는 모든 함수에 대해 본문을 감싸는 중괄호({})가 필수입니다. 겁니다. 중괄호는 함수 정의와 같은 줄에 작성하고 그 사이에 공백을 한 칸 넣으면 보기 좋으니 참고하세요. Note: 여러분이 러스트 프로젝트의 코드를 표준 스타일로 통일시키고 싶다면, 코드를 특정 스타일로 포맷팅해주는 rustfmt라는 이름의 자동 포맷팅 도구를 사용할 수 있습니다 (더 자세한 사항은 부록 D 에 있습니다.) 러스트 팀은 이 도구를 rustc처럼 기본 러스트 배포에 포함시켰으므로, 이미 여러분의 컴퓨터에 설치되어 있습니다! main 함수 내 코드를 살펴봅시다. println!(\"Hello, world!\"); 화면에 텍스트를 출력하는 코드로, 이 한 라인이 이 자그마한 프로그램의 전부입니다. 하지만 이 단순한 코드에도 눈여겨볼 것이 네 가지 들어있습니다. 첫 번째로, 러스트에서는 탭 대신 스페이스 4칸을 사용합니다. 두 번째로, println!는 러스트의 매크로 호출 코드입니다. 이 코드가 함수 호출 코드였다면 ! 없이 println이라고 되어 있었을 것입니다. 매크로는 19장에서 자세히 다루며, 지금은 !가 붙으면 함수가 아니라 매크로 호출 코드이고, 매크로는 함수와 항상 같은 규칙을 따르지는 않는다는 것만 알아두시면 됩니다. 세 번째는 println!의 인수로 넘겨준 \"Hello, world!\" 문자열이 그대로 화면에 나타난 점입니다. 마지막으로, 이 라인은 세미콜론(;)으로 끝납니다. 이 표현식이 끝났으며 다음 표현식이 시작될 준비가 됐다는 표시지요. 러스트 코드의 거의 모든 라인이 세미콜론으로 끝납니다.","breadcrumbs":"시작해봅시다 » Hello, World! » 러스트 프로그램 뜯어보기","id":"23","title":"러스트 프로그램 뜯어보기"},"230":{"body":"예제 12-21에서 보시는 search_case_insensitive 함수는 search 함수와 거의 똑같이 생겼을 것입니다. 유일한 차이점은 query와 각 line을 소문자로 만들어서 입력된 인수의 대소문자가 어떻든 간에 질의어가 라인에 포함되어 있는지 확인할 때는 언제나 같은 소문자일 것이란 점입니다. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # for line in search(&config.query, &contents) {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# pub fn search_case_insensitive<'a>( query: &str, contents: &'a str,\n) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 예제 12-21: 질의어와 라인을 비교하기 전에 소문자로 만드는 search_case_insensitive 함수 정의하기 먼저 query 문자열을 소문자로 만들어서 같은 이름의 변수를 가리는 방식으로 저장합니다. 질의어에 대해 to_lowercase가 호출되므로 사용자의 질의어가 \"rust\", \"RUST\", \"Rust\", 혹은 \"rUsT\"이든 상관없이 이 질의어를 \"rust\"로 취급하여 대소문자를 구분하지 않게 될 것입니다. to_lowercase가 기본적인 유니코드를 처리하겠지만, 100% 정확하지는 않을 것입니다. 실제 애플리케이션을 작성하는 중이었다면 여기에 약간의 작업을 추가할 필요가 있겠지만, 이 절은 유니코드가 아니라 환경변수에 대한 것이므로, 여기서는 그대로 두겠습니다. to_lowercase의 호출이 존재하는 데이터를 참조하지 않고 새로운 데이터를 만들기 때문에, query가 이제 문자열 슬라이스가 아니라 String이 되었음을 주의하세요. 예를 들어 질의어가 \"rUsT\"라고 해봅시다: 이 문자열 슬라이스는 우리가 사용하려는 소문자 u나 t가 들어있지 않으므로, \"rust\"를 담고 있는 새로운 String을 할당해야 합니다. 이제 query를 contains의 인수로 넘길 때는 앰퍼센드를 붙여줄 필요가 있는데 이는 contains의 시그니처가 문자열 슬라이스를 받도록 정의되어 있기 때문입니다. 다음으로 line의 모든 글자를 소문자로 만들기 위해 to_lowercase 호출을 추가합니다. 이제 line과 query를 소문자로 변환했으니 질의어의 대소문자에 상관없이 매칭된 라인들을 찾아낼 것입니다. 이 구현이 테스트를 통과하는지 살펴봅시다: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.33s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 2 tests\ntest tests::case_insensitive ... ok\ntest tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 훌륭하군요! 테스트가 통과되었습니다. 이제 search_case_insensitive 함수를 run 함수에서 호출해 봅시다. 먼저 대소분자 구분 여부를 전환하기 위한 옵션을 Config 구조체에 추가하겠습니다. 아직 이 필드를 어디서도 초기화하고 있지 않기 때문에 필드를 추가하는 것만으로는 컴파일 에러가 날 것입니다: 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool,\n}\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 부울린 값을 갖는 ignore_case 필드를 추가했습니다. 다음으로, 예제 12-22에서 보시는 것처럼 run 함수가 ignore_case 필드의 값을 검사하여 search 함수 혹은 search_case_insensitive 함수 중 어느 쪽을 호출할 지 결정하는 것이 필요합니다. 아직은 컴파일되지 않을 것입니다. 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# pub fn run(config: Config) -> Result<(), Box> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!(\"{line}\"); } Ok(())\n}\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 예제 12-22: config.ignore_case의 값에 기초하여 search나 search_case_insensitive를 호출하기 마지막으로 환경 변수의 검사가 필요합니다. 환경 변수 사용을 위한 함수는 표준 라이브러리의 env 모듈에 있으므로, src/lib.rs 상단에서 이 모듈을 스코프로 가져옵니다. 그런 다음 예제 12-23처럼 env 모듈의 var 함수를 사용하여 IGNORE_CASE라는 이름의 환경 변수에 어떤 값이 설정되었는지 확인해 보겠습니다. 파일명: src/lib.rs use std::env;\n// --생략-- # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# impl Config { pub fn build(args: &[String]) -> Result { if args.len() < 3 { return Err(\"not enough arguments\"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var(\"IGNORE_CASE\").is_ok(); Ok(Config { query, file_path, ignore_case, }) }\n}\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 예제 12-23: IGNORE_CASE라는 이름의 환경 변수의 값을 검사하기 여기서는 ignore_case라는 새 변수를 만들었습니다. 이 값을 설정하기 위해서 env::var 함수를 호출하고 환경 변수의 이름 INGORE_CASE를 넘겼습니다. env::var 함수는 Result를 반환하는데 여기에는 해당 환경 변수에 어떤 값이 설정되어 있을 경우 그 값을 담은 Ok 배리언트가 될 것입니다. 만일 환경 변수가 설정되어 있지 않다면 Err 배리언트가 반환될 것입니다. 환경 변수가 설정되었는지 확인하기 위해서 Result의 is_ok 메서드를 사용 중인데, 이는 프로그램이 대소문자를 구분하지 않는 검색을 해야 함을 뜻합니다. 만일 IGNORE_CASE 환경 변수가 아무 값도 설정되어 있지 않다면, is_ok는 거짓값을 반환하고 프로그램은 대소문자를 구분하는 검색을 수행할 것입니다. 이 환경 변수의 값 에 대해서는 고려하지 않고 그저 값이 설정되어 있는지 아닌지만 고려하므로, 여기서는 unwrap이나 expect 혹은 Result에서 사용했던 다른 메서드들 대신 is_ok를 사용하고 있습니다. 이 ignore_case 변수의 값을 Config 인스턴스에게 전달했으므로, run 함수는 예제 12-22에 구현된 것처럼 이 값을 읽어서 search_case_insensitive 혹은 search의 호출 여부를 결정할 수 있습니다. 한번 시도해 봅시다! 먼저 환경 변수 설정 없이 질의어 to를 넣어 프로그램을 실행시킬 것인데, 이는 모두 소문자인 단어 ‘to’가 포함된 어떤 라인과 매칭되어야 합니다: $ cargo run -- to poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep to poem.txt`\nAre you nobody, too?\nHow dreary to be somebody! 아직 잘 동작하는 것처럼 보이는군요! 이제 IGNORE_CASE를 1로 설정하고 동일한 질의어 to를 넣어서 프로그램을 실행해 봅시다. $ IGNORE_CASE=1 cargo run -- to poem.txt 여러분이 PowerShell을 사용 중이라면, 별도의 커맨드로 환경 변수 설정과 프로그램 실행을 할 필요가 있을 것입니다: PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt 이는 남은 셸 세션에 대해 IGNORE_CASE가 영구적으로 설정되게 할 것입니다. Remove_Item cmdlet으로 설정을 해제할 수 있습니다: PS> Remove-Item Env:IGNORE_CASE 이제 대문자일 수도 있는 ‘to’를 담고 있는 라인들을 얻어야 합니다: Are you nobody, too?\nHow dreary to be somebody!\nTo tell your name the livelong day\nTo an admiring bog! ‘To’를 담고 있는 라인도 얻었으니, 훌륭합니다! minigrep 프로그램은 지금부터 환경 변수에 의해 제어되는 대소문자 구별 없는 검색기능을 사용할 수 있게 되었습니다. 이제 여러분은 커맨드 라인 인수 혹은 환경 변수를 통한 옵션 설정을 관리하는 방법을 알게 되었습니다. 어떤 프로그램들은 같은 환경값에 대해 인수와 환경 변수 모두를 사용할 수 있게 합니다. 그러한 경우에는 보통 한쪽이 다른 쪽에 대해 우선순위를 갖도록 결정합니다. 연습용으로 대소문자 구분 옵션을 커맨드 라인 혹은 환경 변수로 제어하는 시도를 직접 해보세요. 한쪽은 대소문자를 구분하도록 하고 다른 쪽은 대소문자 구분을 무시하도록 설정되어 실행되었을 경우에는 커맨드 라인 인수 쪽 혹은 환경 변수 쪽이 우선권을 갖도록 하는 결정이 필요합니다. std::env 모듈에는 환경 변수를 다루기 위한 더 유용한 기능들을 많이 가지고 있습니다: 어떤 것들이 가능한지는 문서를 확인해 보세요.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 환경 변수 사용하기 » search_case_insensitive 함수 구현하기","id":"230","title":"search_case_insensitive 함수 구현하기"},"231":{"body":"이 시점에서는 터미널로 출력되는 모든 것이 println! 매크로를 사용하여 작성되고 있는 상태입니다. 대부분의 터미널에는 두 종류의 출력이 있습니다: 범용적인 정보를 위한 표준 출력 (standard output) (stdout)과 에러 메시지를 위한 표준 에러 (standard error) (stderr) 두 가지죠. 이러한 구분은 사용자로 하여금 성공한 프로그램의 출력값을 파일로 향하게끔 하지만 에러 메시지는 여전히 화면에 나타나도록 해줄 수 있습니다. println! 매크로는 표준 출력으로의 출력 기능만 있으므로, 표준 에러로 출력하기 위해서는 다른 무언가를 사용해야 합니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준 출력 대신 표준 에러로 에러 메시지 작성하기 » 표준 출력 대신 표준 에러로 에러 메시지 작성하기","id":"231","title":"표준 출력 대신 표준 에러로 에러 메시지 작성하기"},"232":{"body":"먼저 minigrep이 출력하는 내용들이 현재 표준 에러 쪽에 출력하고 싶은 에러 메시지를 포함하여 어떤 식으로 표준 출력에 기록되는지 관찰해 봅시다. 의도적으로 에러를 발생시키면서 표준 출력 스트림을 파일 쪽으로 리디렉션하여 이를 확인할 것입니다. 표준 에러 스트림은 리디렉션하지 않을 것이므로 표준 에러 쪽으로 보내진 내용들은 계속 화면에 나타날 것입니다. 커맨드 라인 프로그램은 표준 에러 스트림 쪽으로 에러 메시지를 보내야 하므로 표준 출력 스트림이 파일로 리디렉션되더라도 여전히 에러 메시지는 화면에서 볼 수 있습니다. 이 프로그램은 현재 잘 제대로 동작하지 않습니다: 에러 메시지가 대신 파일 쪽에 저장되는 것을 막 보려는 참입니다! 이 동작을 확인해 보기 위해서 프로그램을 >과 파일 경로 output.txt 과 함께 실행해 보려 하는데, 이 파일 경로는 표준 출력 스트림이 리디렉션될 곳입니다. 아무런 인수를 넣지 않을 것인데, 이는 에러를 발생시켜야 합니다: $ cargo run > output.txt > 문법은 셸에게 표준 출력의 내용을 화면 대신 output.txt 에 작성하라고 알려줍니다. 화면 출력되리라 기대되었던 에러 메시지는 보이지 않으므로, 이는 결국 파일 안으로 들어갔음에 틀림없다는 의미입니다. 아래가 output.txt 이 담고 있는 내용입니다: Problem parsing arguments: not enough arguments 네, 에러 메시지가 표준 출력에 기록되고 있네요. 이런 종류의 에러 메시지는 표준 에러로 출력되게 함으로써 성공적인 실행으로부터 나온 데이터만 파일로 향하게 만드는 것이 훨씬 유용합니다. 그렇게 바꿔보겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준 출력 대신 표준 에러로 에러 메시지 작성하기 » 에러가 기록되었는지 검사하기","id":"232","title":"에러가 기록되었는지 검사하기"},"233":{"body":"에러 메시지 출력 방식을 변경하기 위해 예제 12-24의 코드를 사용해 보겠습니다. 이 장에서 앞서 했던 리팩터링 덕분에 에러 메시지를 출력하는 모든 코드는 단 하나의 함수 main 안에 있습니다. 표준 라이브러리는 표준 에러 스트림으로 출력하는 eprintln! 매크로를 제공하므로, 에러 출력을 위해 println!을 호출하고 있는 두 군데를 eprintln!로 바꿔봅시다. 파일명: src/main.rs # use std::env;\n# use std::process;\n# # use minigrep::Config;\n# fn main() { let args: Vec = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(\"Problem parsing arguments: {err}\"); process::exit(1); }); if let Err(e) = minigrep::run(config) { eprintln!(\"Application error: {e}\"); process::exit(1); }\n} 예제 12-24: eprintln!를 사용하여 표준 출력 대신 표준 에러로 에러 메시지 작성하기 이제 동일한 방식, 즉 아무런 인수 없이 >로 표준 출력을 리디렉션하여 프로그램을 다시 실행해 봅시다: $ cargo run > output.txt\nProblem parsing arguments: not enough arguments 이제는 에러가 화면에 보여지고 output.txt 에는 아무것도 없는데, 이것이 커맨드 라인 프로그램에 대해 기대한 동작입니다. 다시 한번 프로그램을 시키는데 이번에는 다음과 같이 에러를 내지 않는 인수를 사용하고 표준 출력을 파일로 리디렉션 시켜봅시다: $ cargo run -- to poem.txt > output.txt 터미널에는 아무런 출력을 볼 수 없고, output.txt 에는 결과물이 담겨 있을 것입니다: 파일명: output.txt Are you nobody, too?\nHow dreary to be somebody! 이는 이제 성공적인 출력에 대해서는 표준 출력을, 에러 출력에 대해서는 표준 에러를 적절히 사용하고 있음을 입증합니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준 출력 대신 표준 에러로 에러 메시지 작성하기 » 표준 에러로 에러 출력하기","id":"233","title":"표준 에러로 에러 출력하기"},"234":{"body":"이번 장에서는 여태껏 배운 몇몇 주요 개념들을 재점검하고 러스트에서의 통상적인 입출력 연산이 수행되는 방식을 다루었습니다. 커맨드 라인 인수, 파일, 환경 변수, 그리고 에러 출력을 위한 eprintln! 매크로를 사용함으로써, 여러분은 이제 커맨드 라인 애플리케이션을 작성할 준비가 되었습니다. 이전 장의 개념들과의 조합을 통하여 여러분의 코드는 잘 조직되고, 적절한 데이터 구조에 효율적으로 데이터를 저장하고, 에러를 잘 처리하고, 잘 테스트하게 될 것입니다. 다음으로는 함수형 언어로부터 영향을 받은 러스트의 기능 몇 가지를 탐구해 보겠습니다: 바로 클로저와 반복자죠.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준 출력 대신 표준 에러로 에러 메시지 작성하기 » 정리","id":"234","title":"정리"},"235":{"body":"러스트의 디자인은 기존의 많은 언어와 기술에서 영감을 얻었으며, 영향받은 중요한 것 중 하나에는 함수형 프로그래밍 이 있습니다. 함수형 스타일의 프로그래밍은 대개 함수를 값처럼 인수로 넘기는 것, 다른 함수들에서 결괏값으로 함수들을 반환하는 것, 나중에 실행하기 위해 함수를 변수에 할당하는 것 등을 포함합니다. 이번 장에서는, 무엇이 함수형 프로그래밍이고 그렇지 않은지에 대해 논의하는 대신, 다른 언어에서 자주 함수형으로 언급되는 특성들과 유사한 러스트의 특성들에 대해 논의할 것입니다. 더 구체적으로는 다음을 다룹니다: 클로저 , 변수에 저장할 수 있는 함수와 유사한 구조. 반복자 , 일련의 요소들을 처리할 수 있는 방법. 클로저와 반복자를 사용해서 12장의 I/O 프로젝트를 개선할 수 있는 방법. 클로저와 반복자의 성능 (스포일러 있음: 생각보다 빠릅니다!) 이미 다뤄 본 패턴 매칭이나 열거형과 같은 기능들 역시 함수형 스타일의 영향을 받았습니다. 클로저들과 반복자들을 정복하는 것은 자연스러우면서도 빠른 러스트 코드를 작성하는 데 중요한 부분이기 때문에 이번 장을 통으로 할애했습니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 함수형 언어의 특성: 반복자와 클로저","id":"235","title":"함수형 언어의 특성: 반복자와 클로저"},"236":{"body":"러스트의 클로저 는 변수에 저장하거나 다른 함수에 인수로 전달할 수 있는 익명 함수입니다. 한 곳에서 클로저를 만들고 다른 컨텍스트의 다른 곳에서 이를 호출하여 평가할 수 있습니다. 함수와 다르게 클로저는 정의된 스코프에서 값을 캡처할 수 있습니다. 앞으로 클로저의 이러한 기능이 어떻게 코드 재사용과 동작 커스터마이징을 가능하게 하는지 살펴볼 것입니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 클로저: 자신의 환경을 캡처하는 익명 함수 » 클로저: 자신의 환경을 캡처하는 익명 함수","id":"236","title":"클로저: 자신의 환경을 캡처하는 익명 함수"},"237":{"body":"먼저 클로저가 정의된 환경으로부터 나중에 사용할 목적으로 값을 캡처하는 방법을 시험해 보겠습니다. 여기 시나리오가 있습니다: 종종 우리 티셔츠 회사는 프로모션으로 메일링 리스트에 있는 사람들에게 독점 공급하는 한정판 티셔츠를 증정합니다. 메일링 리스트에 있는 사람들은 추가로 자신의 프로파일에 제일 좋아하는 색상을 추가할 수 있습니다. 만일 무료 티셔츠에 추첨된 사람이 좋아하는 색상을 설정해 두었다면, 그 색상의 티셔츠를 받게 됩니다. 만일 그 사람이 좋아하는 색상을 특정하지 않았다면 회사가 현재 제일 많이 가지고 있는 색상을 받게 됩니다. 이를 구현하는 방법은 여러 가지가 있습니다. 이번 예제에서는 Red와 Blue 배리언트가 있는 ShirtColor라는 열거형을 이용해 보겠습니다. (단순한 예제를 위해 가능한 색상을 제한했습니다.) 회사의 재고는 Inventory 구조체로 표현하는데 여기에는 shirts라는 이름의 필드가 있고, 이 필드는 현재 재고에 있는 셔츠 색상을 나타내는 Vec 타입입니다. Inventory에 정의된 giveaway 메서드는 무료 티셔츠를 타게 된 사람의 추가 색상 설정값을 얻어와서 그 사람이 받게 될 셔츠 색상을 반환합니다. 이러한 설정이 예제 13-1에 있습니다: 파일명: src/main.rs #[derive(Debug, PartialEq, Copy, Clone)]\nenum ShirtColor { Red, Blue,\n} struct Inventory { shirts: Vec,\n} impl Inventory { fn giveaway(&self, user_preference: Option) -> ShirtColor { user_preference.unwrap_or_else(|| self.most_stocked()) } fn most_stocked(&self) -> ShirtColor { let mut num_red = 0; let mut num_blue = 0; for color in &self.shirts { match color { ShirtColor::Red => num_red += 1, ShirtColor::Blue => num_blue += 1, } } if num_red > num_blue { ShirtColor::Red } else { ShirtColor::Blue } }\n} fn main() { let store = Inventory { shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue], }; let user_pref1 = Some(ShirtColor::Red); let giveaway1 = store.giveaway(user_pref1); println!( \"The user with preference {:?} gets {:?}\", user_pref1, giveaway1 ); let user_pref2 = None; let giveaway2 = store.giveaway(user_pref2); println!( \"The user with preference {:?} gets {:?}\", user_pref2, giveaway2 );\n} 예제 13-1: 셔츠 회사 증정 상황 main에 정의된 store에는 이 한정판 프로모션 배포를 위해 남은 두 개의 파란색 셔츠와 하나의 빨간색 셔츠가 있습니다. 여기서 빨간색 셔츠로 설정한 고객과 색상 설정이 없는 고객에 대하여 giveaway 메서드를 호출하였습니다. 다시 한번 말하지만, 이 코드는 여러 가지 방법으로 구현될 수 있고, 여기서는 클로저에 초점을 맞추기 위해서 클로저가 사용된 giveaway 메서드 본문을 제외하고는 이미 배운 개념만 사용했습니다. giveaway 메서드에서는 고객의 설정을 Option 타입의 매개변수 user_preference로 unwrap_or_else 메서드를 호출합니다. Option의 unwrap_or_else 메서드 Option 는 표준 라이브러리에 정의되어 있습니다. 이것은 하나의 인수를 받습니다: 바로 아무런 인수도 없고 T 값을 반환하는 클로저 입니다. (이때 T는 Option의 Some 배리언트에 저장되는 타입과 동일하며, 지금의 경우 ShirtColor입니다.) 만일 Option가 Some 배리언트라면, unwrap_or_else는 그 Some 안에 들어있는 값을 반환합니다. 만일 Option가 None 배리언트라면, unwrap_or_else는 이 클로저를 호출하여 클로저가 반환한 값을 반환해 줍니다. unwrap_or_else의 인수로는 || self.most_stocked()이라는 클로저 표현식을 지정했습니다. 이는 아무런 매개변수를 가지지 않는 클로저입니다. (만일 클로저가 매개변수를 갖고 있다면 두 개의 세로 막대 사이에 매개변수가 나올 것입니다.) 클로저의 본문은 self.most_stocked()를 호출합니다. 여기서는 클로저가 정의되어 있고, 결괏값이 필요해진 경우 unwrap_or_else의 구현부가 이 클로저를 나중에 평가할 것입니다. 이 코드를 실행하면 다음이 출력됩니다: $ cargo run Compiling shirt-company v0.1.0 (file:///projects/shirt-company) Finished dev [unoptimized + debuginfo] target(s) in 0.27s Running `target/debug/shirt-company`\nThe user with preference Some(Red) gets Red\nThe user with preference None gets Blue 여기서 한 가지 흥미로운 점은 현재의 Inventory 인스턴스에서 self.most_stocked()를 호출하는 클로저를 넘겼다는 것입니다. 표준 라이브러리는 우리가 정의한 Inventory나 ShirtColor 타입이나, 혹은 이 시나리오에서 우리가 사용하고자 하는 로직에 대해 전혀 알 필요가 없습니다. 이 클로저는 self Inventory 인스턴스의 불변 참조자를 캡처하여 우리가 지정한 코드와 함께 이 값을 unwrap_or_else 메서드에 넘겨줍니다. 반면에 함수는 이런 방식으로 자신의 환경을 캡처할 수 없습니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 클로저: 자신의 환경을 캡처하는 익명 함수 » 클로저로 환경 캡처하기","id":"237","title":"클로저로 환경 캡처하기"},"238":{"body":"함수와 클로저 간의 차이점은 더 있습니다. 클로저는 보통 fn 함수에서처럼 매개변수 혹은 반환 값의 타입을 명시하도록 요구하지 않습니다. 함수의 타입 명시는 그 타입이 사용자들에게 노출되는 명시적인 인터페이스의 일부분이기 때문에 요구됩니다. 이러한 인터페이스를 엄격하게 정의하는 것은 함수가 어떤 타입의 값을 사용하고 반환하는지에 대해 모두가 납득하는 것을 보증하는 데에 중요합니다. 반면에 클로저는 함수처럼 노출된 인터페이스로 사용되지 않습니다: 클로저는 이름이 지어지거나 라이브러리의 사용자들에게 노출되지 않은 채로 변수에 저장되고 사용됩니다. 클로저는 통상적으로 짧고, 임의의 시나리오가 아니라 짧은 컨텍스트 내에서만 관련됩니다. 이러한 한정된 컨텍스트 내에서, 컴파일러는 대부분의 변수에 대한 타입을 추론하는 방법과 비슷한 식으로 클로저의 매개변수와 반환 타입을 추론합니다. (컴파일러가 클로저 타입을 명시하도록 요구하는 경우도 드물게는 있습니다.) 변수와 마찬가지로, 꼭 필요한 것보다 더 장황해지더라도 명시성과 명확성을 올리고 싶다면 타입 명시를 추가할 수 있습니다. 클로저에 대한 타입 명시를 추가하면 예제 13-2의 정의와 비슷해집니다. 이 예제에서는 예제 13-1에서처럼 인수로 전달하는 위치에서 클로저를 정의하기보다는, 클로저를 정의하여 변수에 저장하고 있습니다. 파일명: src/main.rs # use std::thread;\n# use std::time::Duration;\n# # fn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num: u32| -> u32 { println!(\"calculating slowly...\"); thread::sleep(Duration::from_secs(2)); num };\n# # if intensity < 25 {\n# println!(\"Today, do {} pushups!\", expensive_closure(intensity));\n# println!(\"Next, do {} situps!\", expensive_closure(intensity));\n# } else {\n# if random_number == 3 {\n# println!(\"Take a break today! Remember to stay hydrated!\");\n# } else {\n# println!(\n# \"Today, run for {} minutes!\",\n# expensive_closure(intensity)\n# );\n# }\n# }\n# }\n# # fn main() {\n# let simulated_user_specified_value = 10;\n# let simulated_random_number = 7;\n# # generate_workout(simulated_user_specified_value, simulated_random_number);\n# } 예제 13-2: 클로저에 매개변수와 반환 값의 타입을 추가적으로 명시하기 타입 명시가 추가되면 클로저 문법은 함수 문법과 더욱 유사해 보입니다. 아래는 매개변수의 값에 1을 더하는 함수와, 그와 동일한 동작을 수행하는 클로저를 비교하기 위해 정의해 본 것입니다. 관련된 부분들의 열을 맞추기 위해 공백을 좀 추가했습니다. 아래는 파이프의 사용과 부차적인 문법들을 제외하면 클로저의 문법이 함수 문법과 얼마나 비슷한지를 보여줍니다: fn add_one_v1 (x: u32) -> u32 { x + 1 }\nlet add_one_v2 = |x: u32| -> u32 { x + 1 };\nlet add_one_v3 = |x| { x + 1 };\nlet add_one_v4 = |x| x + 1 ; 첫 번째 줄은 함수 정의고, 두 번째 줄은 모든 것이 명시된 클로저 정의입니다. 세 번째 줄에서는 타입 명시를 제거했습니다. 네 번째 줄에서는 중괄호를 제거했는데, 이 클로저의 본문이 딱 하나의 표현식이기 때문에 가능합니다. 위의 방식 모두 호출시에 동일한 동작을 수행하는 유효한 정의법입니다. add_one_v3와 add_one_v4 줄을 컴파일하기 위해서는 이 클로저들이 평가되는 곳이 필요한데, 그 이유는 이 클로저들이 사용된 곳에서 타입이 추론될 것이기 때문입니다. 이는 let v = Vec::new();가 러스트에 의해 타입이 추론되기 위해서 타입 명시 혹은 Vec 안에 집어넣을 어떤 타입의 값이 필요한 것과 유사합니다. 클로저 정의에 대하여, 컴파일러는 각각의 매개변수와 반환 값마다 하나의 고정 타입을 추론할 것입니다. 예를 들면 예제 13-3은 자신이 매개변수로 받은 값을 그냥 반환하는 짧은 클로저의 정의를 보여주고 있습니다. 이 클로저는 이 예제 용도 말고는 그다지 유용하진 않습니다. 정의에 아무런 타입 명시를 하지 않았음을 주의하세요. 아무런 타입 명시도 없으므로 아무 타입에 대해서나 이 클로저를 호출할 수 있는데, 여기서는 처음에 String에 대해 호출했습니다. 그런 다음 정수에 대해 example_closure의 호출을 시도한다면, 에러를 얻게 됩니다. 파일명: src/main.rs # fn main() { let example_closure = |x| x; let s = example_closure(String::from(\"hello\")); let n = example_closure(5);\n# } 예제 13-3: 두 개의 다른 타입에 대해 타입이 추론되는 클로저 호출 시도하기 컴파일러는 아래와 같은 에러를 냅니다: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example)\nerror[E0308]: mismatched types --> src/main.rs:5:29 |\n5 | let n = example_closure(5); | --------------- ^- help: try using a conversion method: `.to_string()` | | | | | expected struct `String`, found integer | arguments to this function are incorrect |\nnote: closure parameter defined here --> src/main.rs:2:28 |\n2 | let example_closure = |x| x; | ^ For more information about this error, try `rustc --explain E0308`.\nerror: could not compile `closure-example` due to previous error 처음 String을 가지고 example_closure를 호출하면, 컴파일러는 클로저의 x 타입과 반환 타입이 String이라고 추론합니다. 그러면 이 타입이 example_closure 클로저에 고정되고, 그다음 동일한 클로저를 가지고 다른 타입에 대해 사용 시도했을 때 타입 에러를 얻게 됩니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 클로저: 자신의 환경을 캡처하는 익명 함수 » 클로저 타입 추론과 명시","id":"238","title":"클로저 타입 추론과 명시"},"239":{"body":"클로저는 세 가지 방식으로 자신의 환경으로부터 값을 캡처할 수 있는데, 이는 함수가 매개변수를 취하는 세 가지 방식과 직접적으로 대응됩니다: 불변으로 빌려오기, 가변으로 빌려오기, 그리고 소유권 이동이죠. 클로저는 캡처된 값이 쓰이는 방식에 기초하여 캡처할 방법을 결정할 것입니다. 예제 13-4에서 정의한 클로저는 list라는 이름의 벡터에 대한 불변 참조자를 캡처하는데, 이는 그저 값을 출력하기 위한 불변 참조자가 필요한 상태이기 때문입니다: 파일명: src/main.rs fn main() { let list = vec![1, 2, 3]; println!(\"Before defining closure: {:?}\", list); let only_borrows = || println!(\"From closure: {:?}\", list); println!(\"Before calling closure: {:?}\", list); only_borrows(); println!(\"After calling closure: {:?}\", list);\n} 예제 13-4: 불변 참조자를 캡처하는 클로저의 정의와 호출 또한 이 예제는 어떤 변수가 클로저의 정의에 바인딩될 수 있고, 이 클로저는 나중에 마치 변수 이름이 함수 이름인 것처럼 변수 이름과 괄호를 사용하여 호출될 수 있음을 보여줍니다. list에 대한 여러 개의 불변 참조자를 동시에 가질 수 있기 때문에, list에는 클로저 정의 전이나 후 뿐만 아니라 클로저의 호출 전과 후에도 여전히 접근이 가능합니다. 이 코드는 컴파일 및 실행이 되고, 다음을 출력합니다: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example`\nBefore defining closure: [1, 2, 3]\nBefore calling closure: [1, 2, 3]\nFrom closure: [1, 2, 3]\nAfter calling closure: [1, 2, 3] 다음으로 예제 13-5에서는 클로저의 본문을 바꾸어 list 벡터에 요소를 추가하도록 했습니다. 클로저는 이제 가변 참조자를 캡처합니다: 파일명: src/main.rs fn main() { let mut list = vec![1, 2, 3]; println!(\"Before defining closure: {:?}\", list); let mut borrows_mutably = || list.push(7); borrows_mutably(); println!(\"After calling closure: {:?}\", list);\n} 예제 13-5: 가변 참조자를 캡처하는 클로저의 정의와 호출 이 코드는 컴파일되고, 실행되고, 다음을 출력합니다: $ cargo run Compiling closure-example v0.1.0 (file:///projects/closure-example) Finished dev [unoptimized + debuginfo] target(s) in 0.43s Running `target/debug/closure-example`\nBefore defining closure: [1, 2, 3]\nAfter calling closure: [1, 2, 3, 7] borrows_mutably 클로저의 정의와 호출 사이에 더 이상 println!이 없음을 주목하세요: borrows_mutably가 정의된 시점에, 이 클로저가 list에 대한 가변 참조자를 캡처합니다. 클로저가 호출된 이후로 다시 클로저를 사용하고 있지 않으므로, 가변 대여가 그 시점에서 끝납니다. 클로저 정의와 호출 사이에는 출력을 위한 불변 대여가 허용되지 않는데, 이는 가변 대여가 있을 때는 다른 대여가 허용되지 않기 때문입니다. println!을 추가해서 어떤 에러가 나오는지 시도해 보세요! 엄밀하게는 클로저의 본문에서 사용하고 있는 값의 소유권이 필요하진 않더라도 만약 여러분이 클로저가 소유권을 갖도록 만들고 싶다면, 매개변수 리스트 전에 move 키워드를 사용할 수 있습니다. 이 기법은 대체로 클로저를 새 스레드에 넘길 때 데이터를 이동시켜서 새로운 스레드가 이 데이터를 소유하게 하는 경우 유용합니다. 스레드가 무엇이고 왜 이를 사용하게 되는지에 대한 자세한 내용은 16장에서 동시성에 대해 이야기할 때 다루기로 하고, 지금은 move 키워드가 필요한 클로저를 사용하는 새 스레드의 생성을 살짝 보겠습니다. 예제 13-6은 예제 13-4를 수정하여 메인 스레드가 아닌 새 스레드에서 벡터를 출력하는 코드를 보여줍니다: 파일명: src/main.rs use std::thread; fn main() { let list = vec![1, 2, 3]; println!(\"Before defining closure: {:?}\", list); thread::spawn(move || println!(\"From thread: {:?}\", list)) .join() .unwrap();\n} 예제 13-6: 스레드에 대한 클로저가 list의 소유권을 갖도록 move 사용하기 여기서는 새 스레드를 생성하여 여기에 인수로 실행될 클로저를 제공합니다. 클로저의 본문에서는 리스트를 출력합니다. 예제 13-4에서는 클로저가 불변 참조자만 사용하여 list를 캡처했는데, 이것이 list를 출력하기 위해 필요한 최소한의 접근 수준이기 때문입니다. 이 예제에서는 클로저 본문이 여전히 불변 참조자만 필요할지라도, 클로저 정의의 앞부분에 move 키워드를 집어넣어 list가 이동되어야 함을 명시할 필요가 있습니다. 새로운 스레드가 메인 스레드의 나머지 부분이 끝나기 전에 끝날 수도 있고, 혹은 메인 스레드가 먼저 끝날 수도 있습니다. 만일 메인 스레드가 list의 소유권을 유지하고 있는데 새 스레드가 끝나기 전에 끝나버려서 list를 제거한다면, 새 스레드의 불변 참조자는 유효하지 않게 될 것입니다. 따라서 컴파일러는 list를 새 스레드에 제공될 클로저로 이동시켜 참조자가 유효하도록 요구합니다. move 키워드를 제거하거나 클로저가 정의된 이후 메인 스레드에서 list를 사용하면 어떤 컴파일러 에러를 얻게 되는지 시도해 보세요!","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 클로저: 자신의 환경을 캡처하는 익명 함수 » 참조자를 캡처하거나 소유권 이동하기","id":"239","title":"참조자를 캡처하거나 소유권 이동하기"},"24":{"body":"앞서 새 프로그램을 만들고 실행한 과정을 세세한 단계로 나누어 검토해 봅시다. 러스트 프로그램을 실행하기 전에, 아래와 같이 rustc 명령어에 소스 파일명을 넘겨주어 컴파일해야 하는 과정이 있었습니다: $ rustc main.rs C나 C++ 을 다뤄보셨다면 gcc나 clang 사용 방법과 비슷하다는 걸 눈치채셨을지도 모르겠네요. 러스트는 소스 파일 컴파일에 성공하면 실행 가능한 바이너리를 만들어 냅니다. Linux, macOS, Windows PowerShell 상에서는 ls 명령어로 실행 파일을 확인할 수 있습니다. $ ls\nmain main.rs Linux와 macOS에서는 두 개의 파일이 보일 것이고, PowerShell의 경우에는 CMD와 같이 세 개의 파일이 보일 것입니다. Windows CMD 는 다음 명령어를 입력해야 합니다: > dir /B %= `/B`는 파일명만 출력하는 옵션입니다 =%\nmain.exe\nmain.pdb\nmain.rs .rs 확장자를 갖는 소스 파일과 실행 파일 (타 플랫폼에서는 main , Windows에서는 main.exe 입니다)을 확인할 수 있습니다. Windows에서는 디버깅 정보가 포함된 pdb 확장자 파일도 볼 수 있네요. 여기서 main 이나 main.exe 를 실행하는 방법은 다음과 같습니다: $ ./main # Windows에서는 .\\main.exe main.rs 가 여러분의 ‘Hello, world!’ 프로그램이라면 터미널에 Hello, world!가 출력될 겁니다. Ruby, Python, JavaScript 등 명령어 한 줄로 프로그램을 컴파일하고 실행할 수 있는 동적 프로그래밍 언어에 익숙한 분들은 컴파일과 실행이 별개의 과정으로 진행되는 게 낯설 겁니다. 하지만 이 언어들은 .rb , .py , .js 파일을 다른 곳에서 실행하려면 해당 언어의 구현체를 설치해야만 합니다. 반면 러스트는 AOT(ahead-of-time-compiled) 언어로, 컴파일과 실행이 별개인 대신 여러분의 프로그래밍을 컴파일하여 만든 실행 파일을 러스트가 설치되지 않은 곳에서도 실행할 수 있습니다. 저마다 장단점이 있는 법이죠. 간단한 프로그램에는 rustc를 사용하는 것도 좋습니다. 다만 프로젝트가 커질수록 관리할 옵션이 많아지고, 코드 배포도 점점 번거로워지겠죠. 다음 내용에서 소개할 카고 (Cargo) 가 바로 이러한 문제를 해결하는, 여러분이 앞으로 rustc 대신 사용할 도구입니다.","breadcrumbs":"시작해봅시다 » Hello, World! » 컴파일과 실행은 별개의 과정입니다","id":"24","title":"컴파일과 실행은 별개의 과정입니다"},"240":{"body":"어떤 클로저가 자신이 정의된 환경으로부터 값의 참조자 혹은 소유권을 캡처하면 (그래서 클로저의 안으로 이동되는 것에 영향을 준다면), 클로저 본문의 코드는 이 클로저가 나중에 평가될 때 그 참조자나 값에 어떤 일이 발생하는지 정의합니다. (그래서 클로저의 밖으로 무언가 이동되는 것에 영향을 줍니다.) 클로저 본문은 다음의 것들을 할 수 있습니다: 캡처된 값을 클로저 밖으로 이동시키기, 캡처된 값을 변형하기, 이동시키지도 변형시키지도 않기, 혹은 시작 단계에서부터 환경으로부터 아무 값도 캡처하지 않기 세 가지 입니다. 클로저가 환경으로부터 값을 캡처하고 다루는 방식은 이 클로저가 구현하는 트레이트에 영향을 주고, 트레이트는 함수와 구조체가 사용할 수 있는 클로저의 종류를 명시할 수 있는 방법입니다. 클로저는 클로저의 본문이 값을 처리하는 방식에 따라서 이 Fn 트레이트들 중 하나, 둘, 혹은 셋 모두를 추가하는 방식으로 자동으로 구현할 것입니다: FnOnce는 한 번만 호출될 수 있는 클로저에게 적용됩니다. 모든 클로저들은 호출될 수 있으므로, 최소한 이 트레이트는 구현해 둡니다. 캡처된 값을 본문 밖으로 이동시키는 클로저에 대해서는 FnOnce만 구현되며 나머지 Fn 트레이트는 구현되지 않는데, 이는 이 클로저가 딱 한 번만 호출될 수 있기 때문입니다. FnMut은 본문 밖으로 캡처된 값을 이동시키지는 않지만 값을 변경할 수는 있는 클로저에 대해 적용됩니다. 이러한 클로저는 한 번 이상 호출될 수 있습니다. Fn은 캡처된 값을 본문 밖으로 이동시키지 않고 캡처된 값을 변경하지도 않는 클로저는 물론, 환경으로부터 아무런 값도 캡처하지 않는 클로저에 적용됩니다. 이러한 클로저는 자신의 환경을 변경시키지 않으면서 한번 이상 호출될 수 있는데, 이는 클로저가 동시에 여러 번 호출되는 등의 경우에서 중요합니다. 예제 13-1에서 사용했던 Option의 unwrap_or_else 메서드 정의를 살펴봅시다: impl Option { pub fn unwrap_or_else(self, f: F) -> T where F: FnOnce() -> T { match self { Some(x) => x, None => f(), } }\n} T가 Option의 Some 배리언트 내 값의 타입을 나타내는 제네릭 타입임을 상기합시다. 이 타입 T는 또한 unwrap_or_else 함수의 반환 타입이기도 합니다: 예를 들어 Option 상에서 unwrap_or_else를 호출하면 String을 얻을 것입니다. 다음으로, unwrap_or_else 함수가 추가로 제네릭 타입 매개변수 F를 갖고 있음을 주목하세요. F 타입은 f라는 이름의 매개변수의 타입인데, 이것이 unwrap_or_else를 호출할 때 제공하는 클로저입니다. 제네릭 타입 F에 명시된 트레이트 바운드는 FnOnce() -> T인데, 이는 F가 한 번만 호출될 수 있어야 하고, 인수가 없고, T를 반환함을 의미합니다. 트레이트 바운드에 FnOnce를 사용하는 것은 unwrap_or_else가 f를 아무리 많아야 한 번만 호출할 것이라는 제약 사항을 표현해 줍니다. unwrap_or_else의 본문을 보면 Option이 Some일 때 f가 호출되지 않을 것임을 알 수 있습니다. 만일 Option이 None라면 f가 한 번만 호출될 것입니다. 모든 클로저가 FnOnce를 구현하므로 unwrap_or_else는 가장 다양한 종류의 클로저를 허용하며 될 수 있는 한 유연하게 동작합니다. Note: 함수도 이 세 종류의 Fn 트레이트를 모두 구현할 수 있습니다. 만일 하고자 하는 것이 환경으로부터 값을 캡처할 필요가 없다면, Fn 트레이트 중 하나를 구현한 무언가가 필요한 곳에 클로저 대신 함수 이름을 사용할 수 있습니다. 예를 들면 Option>의 값 상에서 unwrap_or_else(Vec::new)를 호출하여 이 값이 None일 경우 비어있는 새 벡터를 얻을 수 있습니다. 이제 표준 라이브러리에서 슬라이스 상에 정의되어 있는 메서드인 sort_by_key를 살펴보면서 unwrap_or_else와는 어떻게 다르고 sort_by_key의 트레이트 바운드는 왜 FnOnce 대신 FnMut인지를 알아봅시다. 이 클로저는 처리하려는 슬라이스에서 현재 아이템에 대한 참조자를 하나의 인수로 받아서, 순서를 매길 수 있는 K 타입의 값을 반환합니다. 이 함수는 각 아이템의 특정 속성을 이용하여 슬라이스를 정렬하고 싶을 때 유용합니다. 예제 13-7에는 Rectangle 인스턴스의 리스트가 있고 sort_by_key를 사용하여 width 속성을 낮은 것부터 높은 순으로 정렬합니다: 파일명: src/main.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; list.sort_by_key(|r| r.width); println!(\"{:#?}\", list);\n} 예제 13-7: sort_by_key를 사용하여 너비로 사각형 정렬하기 이 코드는 다음을 출력합니다: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles) Finished dev [unoptimized + debuginfo] target(s) in 0.41s Running `target/debug/rectangles`\n[ Rectangle { width: 3, height: 5, }, Rectangle { width: 7, height: 12, }, Rectangle { width: 10, height: 1, },\n] sort_by_key가 FnMut 클로저를 갖도록 정의된 이유는 이 함수가 클로저를 여러 번 호출하기 때문입니다: 슬라이스 내 각 아이템마다 한 번씩요. 클로저 |r| r.width는 자신의 환경으로부터 어떤 것도 캡처나 변형, 혹은 이동을 시키지 않으므로, 트레이트 바운드 요건을 충족합니다. 반면 예제 13-8은 FnOnce 트레이트만 구현한 클로저의 예를 보여주는데, 이 클로저는 환경으로부터 값을 이동시키고 있습니다. 컴파일러는 이 클로저를 sort_by_key에 사용할 수 없게 할 것입니다: 파일명: src/main.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut sort_operations = vec![]; let value = String::from(\"by key called\"); list.sort_by_key(|r| { sort_operations.push(value); r.width }); println!(\"{:#?}\", list);\n} 예제 13-8: FnOnce 클로저를 sort_by_key에 사용 시도하기 이는 list를 정렬할 때 sort_by_key가 클로저를 호출하는 횟수를 세려고 시도하는 부자연스럽고 대단히 난해한 (동작하지 않는) 방식입니다. 이 코드는 클로저 환경의 String인 value를 sort_operations 벡터로 밀어 넣는 형태로 횟수 세기를 시도하고 있습니다. 클로저는 value를 캡처한 다음 value의 소유권을 sort_operations 벡터로 보내서 value를 클로저 밖으로 이동시킵니다. 이 클로저는 한 번만 호출될 수 있습니다; 두 번째 호출 시도에서는 value가 더 이상 이 환경에 남아있지 않은데 sort_operations로 밀어 넣으려고 하므로 동작하지 않을 것입니다! 따라서, 이 클로저는 오직 FnOnce만 구현하고 있습니다. 이 코드를 컴파일 시도하면, 클로저가 FnMut를 구현해야 하기 때문에 value가 클로저 밖으로 이동될 수 없음을 지적하는 에러를 얻게 됩니다: $ cargo run Compiling rectangles v0.1.0 (file:///projects/rectangles)\nerror[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure --> src/main.rs:18:30 |\n15 | let value = String::from(\"by key called\"); | ----- captured outer variable\n16 |\n17 | list.sort_by_key(|r| { | --- captured by this `FnMut` closure\n18 | sort_operations.push(value); | ^^^^^ move occurs because `value` has type `String`, which does not implement the `Copy` trait For more information about this error, try `rustc --explain E0507`.\nerror: could not compile `rectangles` due to previous error 이 에러는 환경에서 value 값을 빼내는 클로저 본문의 라인을 지적합니다. 이를 고치기 위해서는 클로저 본문을 수정하여 환경에서 값을 이동시키지 않도록 할 필요가 있습니다. sort_by_key가 호출되는 횟수를 세기 위해서는 환경 쪽에 카운터를 유지하면서 클로저 본문에서 이 값을 증가시키는 것이 더 직관적으로 계산하는 방법이겠습니다. 예제 13-9의 클로저는 sort_by_key에서 동작하는데, 이는 num_sort_operation 카운터에 대한 가변 참조자를 캡처할 뿐이라서 한 번 이상 호출이 가능하기 때문입니다: 파일명: src/main.rs #[derive(Debug)]\nstruct Rectangle { width: u32, height: u32,\n} fn main() { let mut list = [ Rectangle { width: 10, height: 1 }, Rectangle { width: 3, height: 5 }, Rectangle { width: 7, height: 12 }, ]; let mut num_sort_operations = 0; list.sort_by_key(|r| { num_sort_operations += 1; r.width }); println!(\"{:#?}, sorted in {num_sort_operations} operations\", list);\n} 예제 13-9: FnMut 클로저를 sort_by_key에 사용하는 것은 허용됩니다 Fn 트레이트는 클로저를 사용하는 함수 혹은 타입을 정의하고 사용할 때 중요합니다. 다음 절에서는 반복자를 다루려고 합니다. 많은 반복자들이 클로저 인수를 받으니, 계속 진행하면서 이러한 클로저 세부 내용을 새겨둡시다!","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 클로저: 자신의 환경을 캡처하는 익명 함수 » 캡처된 값을 클로저 밖으로 이동하기와 Fn 트레이트","id":"240","title":"캡처된 값을 클로저 밖으로 이동하기와 Fn 트레이트"},"241":{"body":"반복자 패턴은 일련의 아이템들에 대해 순서대로 어떤 작업을 수행할 수 있도록 해줍니다. 반복자는 각 아이템을 순회하고 언제 시퀀스가 종료될지 결정하는 로직을 담당합니다. 반복자를 사용하면, 그런 로직을 다시 구현할 필요가 없습니다. 러스트에서의 반복자는 게으른데 , 이는 반복자를 사용하는 메서드를 호출하여 반복자를 소비하기 전까지는 동작을 하지 않는다는 의미입니다. 예를 들면, 예제 13-10의 코드는 Vec 에 정의된 iter 메서드를 호출함으로써 벡터 v1에 있는 아이템들에 대한 반복자를 생성합니다. 이 코드 자체로는 어떤 유용한 동작도 하지 않습니다. # fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter();\n# } 예제 13-10: 반복자 생성하기 반복자는 v1_iter 변수에 저장됩니다. 일단 반복자를 만들면, 다양한 방법으로 사용할 수 있습니다. 3장의 예제 3-5에서는 각 아이템에 대해 어떤 코드를 실행하기 위해 for 루프를 사용하여 어떤 배열에 대한 반복을 수행했습니다. 내부적으로는 암묵적으로 반복자를 생성한 다음 소비하는 것이었지만, 지금까지는 이게 정확히 어떻게 동작하는지에 대해서 대충 넘겼습니다. 예제 13-11의 예제에서는 for 루프에서 반복자를 사용하는 부분으로부터 반복자 생성을 분리했습니다. v1_iter에 있는 반복자를 사용하여 for 루프가 호출되면, 반복자의 각 요소가 루프의 한 순번마다 사용되는데, 여기서는 각각의 값을 출력합니다. # fn main() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!(\"Got: {}\", val); }\n# } 예제 13-11: for 루프에서 반복자 사용하기 표준 라이브러리에서 반복자를 제공하지 않는 언어에서는, 아마도 변수를 인덱스 0으로 시작하고, 그 변수를 인덱스로 사용하여 벡터에서 값을 꺼내오고, 루프 안에서 벡터가 가진 아이템의 전체 개수에 다다를 때까지 그 변숫값을 증가시키는 것으로 동일한 기능을 작성할 것입니다. 반복자는 그러한 모든 로직을 대신 처리하여 잠재적으로 엉망이 될 수 있는 반복적인 코드를 줄여 줍니다. 반복자는 벡터처럼 인덱스를 사용할 수 있는 자료구조 뿐만 아니라, 많은 다른 종류의 시퀀스에 대해 동일한 로직을 사용할 수 있도록 더 많은 유연성을 제공합니다. 반복자가 어떻게 그런 작동을 하는지 살펴봅시다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 반복자로 일련의 아이템들 처리하기 » 반복자로 일련의 아이템들 처리하기","id":"241","title":"반복자로 일련의 아이템들 처리하기"},"242":{"body":"모든 반복자는 표준 라이브러리에 정의된 Iterator라는 이름의 트레이트를 구현합니다. 트레이트의 정의는 아래처럼 생겼습니다: pub trait Iterator { type Item; fn next(&mut self) -> Option; // 기본 구현이 있는 메서드는 생략했습니다\n} 이 정의에 새로운 문법 몇 가지가 사용된 것에 주목하세요: type Item과 Self::Item은 이 트레이트에 대한 연관 타입 (associated type) 을 정의합니다. 연관 타입에 대해서는 19장에서 더 자세히 이야기하겠습니다. 현재로서는 이 코드에서 Iterator 트레이트를 구현하려면 Item 타입도 함께 정의되어야 하며, 이 Item 타입이 next 메서드의 반환 타입으로 사용된다는 것만 알면 됩니다. 바꿔 말하면, Item 타입은 반복자로부터 반환되는 타입이 되겠습니다. Iterator 트레이트는 구현하려는 이에게 딱 하나의 메서드 정의를 요구합니다: 바로 next 메서드인데, 이 메서드는 Some으로 감싼 반복자의 아이템을 하나씩 반환하고, 반복자가 종료될 때는 None을 반환합니다. 반복자의 next 메서드를 직접 호출할 수 있습니다; 예제 13-12는 벡터로부터 생성된 반복자에 대하여 next를 반복적으로 호출했을 때 어떤 값들이 반환되는지 보여줍니다. 파일명: src/lib.rs # #[cfg(test)]\n# mod tests { #[test] fn iterator_demonstration() { let v1 = vec![1, 2, 3]; let mut v1_iter = v1.iter(); assert_eq!(v1_iter.next(), Some(&1)); assert_eq!(v1_iter.next(), Some(&2)); assert_eq!(v1_iter.next(), Some(&3)); assert_eq!(v1_iter.next(), None); }\n# } 예제 13-12: 반복자의 next 메서드 호출하기 v1_iter를 가변으로 만들 필요가 있음을 주의하세요: 반복자에 대한 next 메서드 호출은 반복자 내부의 상태를 변경하여 반복자가 현재 시퀀스의 어디에 있는지 추적합니다. 바꿔 말하면, 이 코드는 반복자를 소비 (consume) , 즉 다 써 버립니다. next에 대한 각 호출은 반복자로부터 하나의 아이템을 소비합니다. for 루프를 사용할 때는 v1_iter를 가변으로 만들 필요가 없는데, 루프가 v1_iter의 소유권을 갖고 내부적으로 가변으로 만들기 때문입니다. 또한 next 호출로 얻어온 값들은 벡터 내의 값들에 대한 불변 참조자라는 점도 주의하세요. iter 메서드는 불변 참조자에 대한 반복자를 생성합니다. 만약 v1의 소유권을 얻어서 소유한 값을 반환하도록 하고 싶다면, iter 대신 into_iter를 호출할 수 있습니다. 비슷하게, 가변 참조자에 대한 반복자가 필요하면, iter 대신 iter_mut을 호출할 수 있습니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 반복자로 일련의 아이템들 처리하기 » Iterator 트레이트와 next 메서드","id":"242","title":"Iterator 트레이트와 next 메서드"},"243":{"body":"Iterator 트레이트에는 표준 라이브러리에서 기본 구현을 제공하는 여러 가지 메서드가 있습니다; 이 메서드들은 표준 라이브러리 API 문서의 Iterator 트레이트에 대한 부분을 살펴보면 찾을 수 있습니다. 이 메서드들 중 일부는 정의 부분에서 next 메서드를 호출하는데, 이것이 Iterator 트레이트를 구현할 때 next 메서드를 구현해야만 하는 이유입니다. next를 호출하는 메서드들을 소비 어댑터 (consuming adaptor) 라고 하는데, 호출하면 반복자를 소비하기 때문에 그렇습니다. 한 가지 예로 sum 메서드가 있는데, 이는 반복자의 소유권을 가져온 다음 반복적으로 next를 호출하는 방식으로 순회하며, 따라서 반복자를 소비합니다. 전체를 순회하면서 현재의 합계값에 각 아이템을 더하고 순회가 완료되면 합계를 반환합니다. 예제 13-13은 sum 메서드 사용 방식을 보여주는 테스트입니다: 파일명: src/lib.rs # #[cfg(test)]\n# mod tests { #[test] fn iterator_sum() { let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); let total: i32 = v1_iter.sum(); assert_eq!(total, 6); }\n# } 예제 13-13: sum 메서드를 호출하여 반복자의 모든 아이템에 대한 합계 구하기 sum은 반복자를 소유하여 호출하므로, sum을 호출한 이후에는 v1_iter의 사용이 허용되지 않습니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 반복자로 일련의 아이템들 처리하기 » 반복자를 소비하는 메서드","id":"243","title":"반복자를 소비하는 메서드"},"244":{"body":"반복자 어댑터 (iterator adaptor) 는 Iterator 트레이트에 정의된 메서드로 반복자를 소비하지 않습니다. 대신 원본 반복자의 어떤 측면을 바꿔서 다른 반복자를 제공합니다. 예제 13-14는 반복자 어댑터 메서드인 map을 호출하는 예를 보여주는데, 클로저를 인수로 받아서 각 아이템에 대해 호출하여 아이템 전체를 순회합니다. map 메서드는 수정된 아이템들을 생성하는 새로운 반복자를 반환합니다. 여기에서의 클로저는 벡터의 각 아이템에서 1이 증가한 새로운 반복자를 만듭니다: 파일명: src/main.rs # fn main() { let v1: Vec = vec![1, 2, 3]; v1.iter().map(|x| x + 1);\n# } 예제 13-14: 반복자 어댑터 map을 호출하여 새로운 반복자 생성하기 하지만 이 코드는 다음과 같은 경고를 발생시킵니다: $ cargo run Compiling iterators v0.1.0 (file:///projects/iterators)\nwarning: unused `Map` that must be used --> src/main.rs:4:5 |\n4 | v1.iter().map(|x| x + 1); | ^^^^^^^^^^^^^^^^^^^^^^^^ | = note: iterators are lazy and do nothing unless consumed = note: `#[warn(unused_must_use)]` on by default warning: `iterators` (bin \"iterators\") generated 1 warning Finished dev [unoptimized + debuginfo] target(s) in 0.47s Running `target/debug/iterators` 예제 13-14의 코드는 아무것도 하지 않습니다; 넘겨진 클로저는 결코 호출되지 않습니다. 위 경고는 이유가 무엇인지 상기시켜 줍니다: 반복자 어댑터는 게으르고, 반복자를 여기서 소비할 필요가 있다는 것을요. 이 경고를 수정하고 반복자를 소비하기 위해서 collect 메서드를 사용할 것인데, 12장의 예제 12-1에서 env::args와 함께 사용했었지요. 이 메서드는 반복자를 소비하고 결괏값을 모아서 컬렉션 데이터 타입으로 만들어 줍니다. 예제 13-15에서는 벡터에 map을 호출하여 얻은 반복자를 순회하면서 결과를 모읍니다. 이 벡터는 원본 벡터로부터 1씩 증가한 아이템들을 담고 있는 상태가 될 것입니다. 파일명: src/main.rs # fn main() { let v1: Vec = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]);\n# } 예제 13-15: map을 호출하여 새로운 반복자를 생성한 다음 collect 메서드를 호출하여 이 반복자를 소비하고 새로운 벡터 생성하기 map이 클로저를 인수로 받기 때문에, 각 아이템에 대해 수행하고자 하는 어떤 연산이라도 지정할 수 있습니다. 이는 Iterator 트레이트가 제공하는 반복 동작을 재사용하면서 클로저로 동작의 일부를 커스터마이징할 수 있게 해주는 방법을 보여주는 훌륭한 예입니다. 반복자 어댑터의 호출을 연결시키면 복잡한 동작을 읽기 쉬운 방식으로 수행할 수 있습니다. 하지만 모든 반복자는 게으르므로, 반복자 어댑터를 호출한 결과를 얻기 위해서는 소비 어댑터 중 하나를 호출해야만 합니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 반복자로 일련의 아이템들 처리하기 » 다른 반복자를 생성하는 메서드","id":"244","title":"다른 반복자를 생성하는 메서드"},"245":{"body":"많은 반복자 어댑터는 클로저를 인수로 사용하고, 보통 반복자 어댑터의 인수에 명시되는 클로저는 자신의 환경을 캡처하는 클로저일 것입니다. 이러한 예를 들기 위해 클로저 인수를 사용하는 filter 메서드를 사용해 보겠습니다. 이 클로저는 반복자로부터 아이템을 받아서 bool을 반환합니다. 만일 클로저가 true를 반환하면, 그 값을 filter에 의해 생성된 반복자에 포함시키게 됩니다. 클로저가 false를 반환하면 해당 값은 포함시키지 않습니다. 리스트 13-13에서는 환경으로부터 shoe_size를 캡처하는 클로저를 가지고 filter를 사용하여 Shoe 구조체 인스턴스의 컬렉션을 순회합니다. 이는 지정된 크기의 신발만 반환해 줄 것입니다. 파일명: src/lib.rs #[derive(PartialEq, Debug)]\nstruct Shoe { size: u32, style: String,\n} fn shoes_in_size(shoes: Vec, shoe_size: u32) -> Vec { shoes.into_iter().filter(|s| s.size == shoe_size).collect()\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn filters_by_size() { let shoes = vec![ Shoe { size: 10, style: String::from(\"sneaker\"), }, Shoe { size: 13, style: String::from(\"sandal\"), }, Shoe { size: 10, style: String::from(\"boot\"), }, ]; let in_my_size = shoes_in_size(shoes, 10); assert_eq!( in_my_size, vec![ Shoe { size: 10, style: String::from(\"sneaker\") }, Shoe { size: 10, style: String::from(\"boot\") }, ] ); }\n} 예제 13-16: shoe_size를 캡처하는 클로저로 filter 메서드 사용하기 shoes_in_size 함수는 매개변수로 신발들의 벡터에 대한 소유권과 신발 크기를 받습니다. 이 함수는 지정된 크기의 신발들만을 담고 있는 벡터를 반환합니다. shoes_in_size의 본문에서는 into_iter를 호출하여 이 벡터의 소유권을 갖는 반복자를 생성합니다. 그다음 filter를 호출하여 앞의 반복자를 새로운 반복자로 바꾸는데, 새로운 반복자에는 클로저가 true를 반환하는 요소들만 담겨있게 됩니다. 클로저는 환경에서 shoe_size 매개변수를 캡처하고 각 신발의 크기와 값을 비교하여 지정된 크기의 신발만 유지하도록 합니다. 마지막으로, collect를 호출하면 적용된 반복자에 의해 반환된 값을 벡터로 모으고, 이 벡터가 함수에 의해 반환됩니다. 이 테스트는 shoes_in_size를 호출했을 때 지정된 값과 동일한 크기인 신발들만 돌려받는다는 것을 보여 줍니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 반복자로 일련의 아이템들 처리하기 » 환경을 캡처하는 클로저 사용하기","id":"245","title":"환경을 캡처하는 클로저 사용하기"},"246":{"body":"반복자에 대한 새로운 지식을 가지고 12장의 I/O 프로젝트에 반복자를 사용하여 코드들을 더 명확하고 간결하게 개선할 수 있습니다. 반복자가 어떻게 Config::build 함수와 search 함수의 구현을 개선할 수 있는지 살펴봅시다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » I/O 프로젝트 개선하기 » I/O 프로젝트 개선하기","id":"246","title":"I/O 프로젝트 개선하기"},"247":{"body":"예제 12-6에서는 String 값의 슬라이스를 받아서 슬라이스에 인덱스로 접근하고 복사하는 방식으로 Config 구조체의 인스턴스를 생성하는 코드를 넣었고, Config 구조체가 이 값들을 소유하도록 했습니다. 예제 13-17은 예제 12-23에 있던 Config::build 함수의 구현체를 재현한 것입니다: 파일명: src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# impl Config { pub fn build(args: &[String]) -> Result { if args.len() < 3 { return Err(\"not enough arguments\"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var(\"IGNORE_CASE\").is_ok(); Ok(Config { query, file_path, ignore_case, }) }\n}\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 예제 13-17: 예제 12-23의 Config::build 함수 재현 그때는 비효율적인 clone 호출에 대해서, 나중에 제거할 테니 걱정하지 말라고 이야기했었지요. 자, 그때가 되었습니다! String 요소들의 슬라이스를 args 매개변수로 받았지만, build 함수는 args를 소유하지 않기 때문에 clone이 필요했습니다. Config 인스턴스의 소유권을 반환하기 위해서는 Config의 query와 file_path 필드로 값을 복제하는 것으로 Config 인스턴스가 그 값들을 소유하게 할 필요가 있었습니다. 반복자에 대한 새로운 지식을 사용하면, 인수로써 슬라이스를 빌리는 대신 반복자의 소유권을 갖도록 build 함수를 변경할 수 있습니다. 슬라이스의 길이를 체크하고 특정 위치로 인덱싱하는 코드 대신 반복자의 기능을 사용할 것입니다. 이렇게 하면 반복자가 값에 접근하기 때문에 Config::build 함수가 수행하는 작업이 명확해집니다. Config::build가 반복자의 소유권을 가져오고 빌린 값에 대한 인덱싱 연산을 사용하지 않게 되면, clone을 호출하여 새로 할당하는 대신 반복자의 String 값을 Config로 이동할 수 있습니다. 반환된 반복자를 직접 사용하기 여러분의 I/O 프로젝트에 있는 src/main.rs 파일을 열어보면, 아래와 같이 생겼을 것입니다: 파일명: src/main.rs # use std::env;\n# use std::process;\n# # use minigrep::Config;\n# fn main() { let args: Vec = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!(\"Problem parsing arguments: {err}\"); process::exit(1); }); // --생략--\n# # if let Err(e) = minigrep::run(config) {\n# eprintln!(\"Application error: {e}\");\n# process::exit(1);\n# }\n} 먼저 예제 12-24에 있던 main 함수의 시작점을 수정하여 예제 13-18의 코드로 바꾸려고 하는데, 이번에는 반복자를 사용합니다. Config::build도 마찬가지로 업데이트하기 전에는 컴파일 되지 않습니다. 파일명: src/main.rs # use std::env;\n# use std::process;\n# # use minigrep::Config;\n# fn main() { let config = Config::build(env::args()).unwrap_or_else(|err| { eprintln!(\"Problem parsing arguments: {err}\"); process::exit(1); }); // --생략--\n# # if let Err(e) = minigrep::run(config) {\n# eprintln!(\"Application error: {e}\");\n# process::exit(1);\n# }\n} 예제 13-18: env::args의 반환 값을 Config::build로 넘기기 env::args 함수는 반복자를 반환합니다! 반복자의 값들을 벡터로 모아서 Config::build에 슬라이스를 넘기는 대신, 이번에는 env::args로부터 반환된 반복자의 소유권을 Config::build로 직접 전달하고 있습니다. 다음으로는 Config::build의 정의를 업데이트할 필요가 있습니다. 여러분의 I/O 프로젝트에 있는 src/lib.rs 파일에서, 예제 13-19와 같이 Config::build의 시그니처를 변경합시다. 함수 본문을 업데이트해야 하기 때문이 여전히 컴파일 되지 않습니다. 파일명: src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# impl Config { pub fn build( mut args: impl Iterator, ) -> Result { // --생략--\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # let ignore_case = env::var(\"IGNORE_CASE\").is_ok();\n# # Ok(Config {\n# query,\n# file_path,\n# ignore_case,\n# })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 예제 13-19: 반복자를 받도록 Config::build의 시그니처 업데이트하기 env::args 함수에 대한 표준 라이브러리 문서에는 반환되는 반복자의 타입이 std::env::Args이며, 이 타입은 Iterator 트레이트를 구현하고 String 값을 반환함을 명시하고 있습니다. Config::build 함수의 시그니처를 업데이트해서 args 매개변수가 &[String] 대신 트레이트 바운드 impl Iterator를 갖는 제네릭 타입이 되도록 하였습니다. 10장의 ‘매개변수로서의 트레이트’ 절에서 논의했었던 이러한 impl Trait 문법을 사용하면 args가 Iterator 타입을 구현하면서 String 아이템을 반환하는 모든 종류의 타입을 사용할 수 있습니다. args의 소유권을 가져와서 이를 순회하면서 args를 변경할 것이기 때문에, args 매개변수의 명세 부분에 mut 키워드를 추가하여 가변이 되도록 합니다. 인덱싱 대신 Iterator 트레이트 메서드 사용하기 다음으로 Config::build의 본문을 수정하겠습니다. args가 Iterator 트레이트를 구현하고 있으므로, 여기에 next 메서드를 호출할 수 있다는 것을 알고 있지요! 예제 13-20은 예제 12-23의 코드를 next 메서드를 사용하여 업데이트한 것입니다: 파일명: src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# impl Config { pub fn build( mut args: impl Iterator, ) -> Result { args.next(); let query = match args.next() { Some(arg) => arg, None => return Err(\"Didn't get a query string\"), }; let file_path = match args.next() { Some(arg) => arg, None => return Err(\"Didn't get a file path\"), }; let ignore_case = env::var(\"IGNORE_CASE\").is_ok(); Ok(Config { query, file_path, ignore_case, }) }\n}\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# # pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.contains(query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 예제 13-20: 반복자 메서드를 사용하여 Config::build의 본문 변경하기 env::args 반환 값의 첫 번째 값이 프로그램의 이름이라는 점을 기억해 둡시다. 이 첫 번째 값은 무시하고 그다음 값을 얻고자 하므로, 우선 next를 호출한 뒤 그 반환 값으로 아무것도 하지 않았습니다. 두 번째로, next를 호출하여 Config의 query 필드에 원하는 값을 집어넣었습니다. next가 Some을 반환하면, match를 사용하여 값을 추출합니다. 만약 None을 반환한다면, 이는 충분한 인수가 넘어오지 않았음을 의미하고, Err 값과 함께 일찍 반환합니다. file_path 값도 동일하게 처리합니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » I/O 프로젝트 개선하기 » 반복자를 사용하여 clone 제거하기","id":"247","title":"반복자를 사용하여 clone 제거하기"},"248":{"body":"I/O 프로젝트의 search 함수에도 반복자의 장점을 활용할 수 있는데, 예제 12-19의 코드가 예제 13-21에 재현되어 있습니다: 파일명: src/lib.rs # use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# }\n# # impl Config {\n# pub fn build(args: &[String]) -> Result {\n# if args.len() < 3 {\n# return Err(\"not enough arguments\");\n# }\n# # let query = args[1].clone();\n# let file_path = args[2].clone();\n# # Ok(Config { query, file_path })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { let mut results = Vec::new(); for line in contents.lines() { if line.contains(query) { results.push(line); } } results\n}\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn one_result() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# } 예제 13-21: 예제 12-19의 search 함수 구현 반복자 어댑터 메서드를 사용하면 이 코드를 더 간결한 방식으로 작성할 수 있습니다. 이렇게 하면 중간에 가변 results 벡터를 만들지 않아도 됩니다. 함수형 프로그래밍 스타일은 더 명확한 코드를 만들기 위해 변경 가능한 상태의 양을 최소화하는 편을 선호합니다. 가변 상태를 제거하면 results 벡터에 대한 동시 접근을 관리하지 않아도 되기 때문에, 차후에 검색을 병렬로 수행하도록 하는 향상이 가능해집니다. 예제 13-22는 이러한 변경을 보여줍니다: 파일명: src/lib.rs # use std::env;\n# use std::error::Error;\n# use std::fs;\n# # pub struct Config {\n# pub query: String,\n# pub file_path: String,\n# pub ignore_case: bool,\n# }\n# # impl Config {\n# pub fn build(\n# mut args: impl Iterator,\n# ) -> Result {\n# args.next();\n# # let query = match args.next() {\n# Some(arg) => arg,\n# None => return Err(\"Didn't get a query string\"),\n# };\n# # let file_path = match args.next() {\n# Some(arg) => arg,\n# None => return Err(\"Didn't get a file path\"),\n# };\n# # let ignore_case = env::var(\"IGNORE_CASE\").is_ok();\n# # Ok(Config {\n# query,\n# file_path,\n# ignore_case,\n# })\n# }\n# }\n# # pub fn run(config: Config) -> Result<(), Box> {\n# let contents = fs::read_to_string(config.file_path)?;\n# # let results = if config.ignore_case {\n# search_case_insensitive(&config.query, &contents)\n# } else {\n# search(&config.query, &contents)\n# };\n# # for line in results {\n# println!(\"{line}\");\n# }\n# # Ok(())\n# }\n# pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents .lines() .filter(|line| line.contains(query)) .collect()\n}\n# # pub fn search_case_insensitive<'a>(\n# query: &str,\n# contents: &'a str,\n# ) -> Vec<&'a str> {\n# let query = query.to_lowercase();\n# let mut results = Vec::new();\n# # for line in contents.lines() {\n# if line.to_lowercase().contains(&query) {\n# results.push(line);\n# }\n# }\n# # results\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# # #[test]\n# fn case_sensitive() {\n# let query = \"duct\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Duct tape.\";\n# # assert_eq!(vec![\"safe, fast, productive.\"], search(query, contents));\n# }\n# # #[test]\n# fn case_insensitive() {\n# let query = \"rUsT\";\n# let contents = \"\\\n# Rust:\n# safe, fast, productive.\n# Pick three.\n# Trust me.\";\n# # assert_eq!(\n# vec![\"Rust:\", \"Trust me.\"],\n# search_case_insensitive(query, contents)\n# );\n# }\n# } 예제 13-22: search 함수 구현에서 반복자 어댑터 메서드 사용하기 search 함수의 목적은 query를 포함하는 contents의 모든 라인을 반환하는 것임을 상기합시다. 예제 13-16의 filter 예제와 유사하게, 이 코드는 line.contains(query)이 true를 반환하는 라인들만 유지하기 위해서 filter 어댑터를 사용합니다. 그런 다음 collect를 사용하여 매칭된 라인들을 모아 새로운 벡터로 만듭니다. 훨씬 단순하군요! 마찬가지로 search_case_insensitive도 반복자 메서드들을 사용하도록 동일한 변경을 해보셔도 좋습니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » I/O 프로젝트 개선하기 » 반복자 어댑터로 더 간결한 코드 만들기","id":"248","title":"반복자 어댑터로 더 간결한 코드 만들기"},"249":{"body":"그렇다면 여러분의 코드에서 어떤 스타일을 선택하는 것이 좋은지와 그 이유에 대한 질문이 논리적으로 뒤따르겠지요: 예제 13-21에 있는 원래 구현과 예제 13-29에 있는 반복자를 사용하는 버전 중 어떤 것이 좋을까요? 대부분의 러스트 프로그래머는 반복자 스타일을 선호합니다. 처음 사용하기는 다소 어렵습니다만, 다양한 반복자 어댑터와 어떤 일을 하는지에 대해 일단 감을 잡으면 반복자들을 이해하기 쉬워질 것입니다. 루프를 만들고 새 벡터를 만드는 등 다양한 것들을 만지작거리는 대신, 이 코드는 루프의 고수준의 목표에 집중합니다. 이는 몇몇 아주 흔한 코드를 추상화해서 제거하므로, 반복자의 각 요소가 반드시 통과해야 하는 필터링 조건과 같이 이 코드에 유일한 개념을 더 알기 쉽게끔 합니다. 그런데 이 두 가지 구현은 정말 동일할까요? 직관적으로는 더 저수준의 루프가 더 빨라 보입니다. 그러면 성능에 대해서 얘기해 봅시다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » I/O 프로젝트 개선하기 » 루프와 반복자 중 선택하기","id":"249","title":"루프와 반복자 중 선택하기"},"25":{"body":"카고 (Cargo) 는 러스타시안이라면 대부분 사용하는 러스트 빌드 시스템 및 패키지 매니저입니다. 이 도구는 코드 빌드나, 코드 작성에 필요한 외부 라이브러리를 다운로드할 때나, 라이브러리를 제작할 때 겪는 귀찮은 일들을 상당수 줄여주는 편리한 도구입니다. (앞으로 외부 라이브러리는 의존성 (dependency) 이라고 지칭하겠습니다.) 여태 우리가 작성해 본 간단한 러스트 프로그램에는 의존성을 추가하지 않았습니다. 카고를 가지고 ‘Hello, world!’ 프로젝트를 만들었다면, 코드 빌드를 처리하는 카고의 기는 일부만을 사용했을 것입니다. 훗날 복잡한 프로그램을 작성하게 되면 의존성을 추가하게 될 것이고, 카고를 사용하여 프로젝트를 시작하면 의존성을 추가하는 일이 훨씬 더 쉬워질 것입니다. 러스트 프로젝트 대부분이 카고를 사용하고 있기 때문에, 이 책의 이후 내용도 여러분이 카고를 사용한다는 전제로 작성했습니다. ‘러스트 설치’ 절을 따라 하셨다면 이미 카고가 설치되어 있을 테니 따로 설치하실 필요는 없으나, 다른 방법을 이용하신 경우엔 다음 명령어로 카고가 설치되어 있는지 확인하시기 바랍니다: $ cargo --version 버전 숫자가 나타나면 정상입니다. command not found 등 에러가 나타날 경우 여러분이 설치하면서 참고한 문서에서 카고를 따로 설치하는 방법을 찾아보세요.","breadcrumbs":"시작해봅시다 » 카고를 사용해봅시다 » 카고를 사용해봅시다","id":"25","title":"카고를 사용해봅시다"},"250":{"body":"루프와 반복자 중 무엇을 사용할지 결정하기 위해서는 어떤 쪽에 더 빠른지 알 필요가 있겠습니다: 명시적으로 for 루프를 사용한 search 함수 버전과 반복자 버전 중 말이지요. 여기서는 아서 코난 도일이 쓴 셜록 홈스의 모험 의 전체 내용을 로딩하고 내용 중에 the 를 찾는 벤치마크를 돌렸습니다. 아래에 루프를 사용한 search 버전과 반복자를 사용한 버전에 대한 벤치마크 결과가 있습니다: test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)\ntest bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200) 반복자 버전이 약간 더 빨랐군요! 여기서는 벤치마크 코드에 대해 설명하진 않을 것인데, 왜냐하면 여기에서의 핵심은 두 버전이 동등하다는 것을 증명하는 것이 아니고, 두 구현이 성능 측면에서 얼마나 비교되는지에 대한 일반적인 감을 얻는 것이기 때문입니다. 더 종합적인 벤치마크를 위해서는 다양한 크기의 다양한 텍스트를 contents로 사용하고, 서로 다른 길이의 다양한 단어들을 query로 사용하여 모든 종류의 다른 조합으로 확인해야 합니다. 요점은 이렇습니다: 반복자는 비록 고수준의 추상화지만, 컴파일되면 대략 직접 작성한 저수준의 코드와 같은 코드 수준으로 내려갑니다. 반복자는 러스트의 비용 없는 추상화 (zero-cost abstraction) 중 하나이며, 그 추상을 사용하는 것은 추가적인 런타임 오버헤드가 없다는 것을 의미합니다. 최초의 C++ 디자이너이자 구현자인 비야네 스트롭스트룹 (Bjarne Stroustrup) 이 ‘C++ 기초 (Foundations of C++, 2012)’에서 제로 오버헤드 (zero-overhead) 를 정의한 것과 유사합니다: 일반적으로 C++ 구현은 제로 오버헤드 원칙을 준수합니다: 사용하지 않는 것에 대해서는 비용을 지불하지 않습니다. 그리고 더 나아가서, 사용한다면 이보다 더 나은 코드를 수작업으로 만들 수 없습니다. 또 다른 예로, 다음 코드는 오디오 디코더에서 가져왔습니다. 디코딩 알고리즘은 선형 예측이라는 수학적 연산을 사용하여 이전 샘플의 선형 함수에 기반해서 미래의 값을 추정합니다. 이 코드는 반복자 체인을 사용해서 스코프에 있는 세 개의 변수로 수학 연산을 합니다: 데이터의 buffer 슬라이스, 12개의 coefficients 배열, 그리고 데이터를 쉬프트 하기 위한 qlp_shift 값으로 말이죠. 이 예제에서는 변수를 선언했지만 값을 주지는 않았습니다; 비록 이 코드가 컨텍스트 밖에서는 큰 의미 없지만, 러스트가 어떻게 고수준의 개념을 저수준의 코드로 변환하는지에 대한 간결하고 실질적인 예제입니다. let buffer: &mut [i32];\nlet coefficients: [i64; 12];\nlet qlp_shift: i16; for i in 12..buffer.len() { let prediction = coefficients.iter() .zip(&buffer[i - 12..i]) .map(|(&c, &s)| c * s as i64) .sum::() >> qlp_shift; let delta = buffer[i]; buffer[i] = prediction as i32 + delta;\n} prediction의 값을 계산하기 위해서, 이 코드는 coefficients에 있는 12개의 값을 순회하면서 zip 메서드를 사용하여 각 계수와 buffer의 이전 12개의 값 간의 쌍을 만듭니다. 그런 다음, 각 쌍의 값을 서로 곱하고, 모든 결과를 더한 다음, 더한 값의 비트를 qlp_shift 비트만큼 우측으로 쉬프트합니다. 오디오 디코더와 같은 애플리케이션에서의 계산은 종종 성능에 가장 높은 우선순위를 둡니다. 여기서는 반복자를 만들고, 두 개의 어댑터를 사용하고, 값을 소비하고 있습니다. 이 러스트 코드가 컴파일되면 어떤 어셈블리 코드가 될까요? 글쎄요, 이 글을 쓰는 시점에서는 직접 손으로 작성한 것과 같은 어셈블리 코드로 컴파일됩니다. coefficients의 값들을 순회하기 위해 동반되는 어떠한 루프도 없습니다: 러스트는 12번의 반복이 있다는 것을 알고 있으므로, 루프를 ‘풀어 (unrolls)’ 놓습니다. 언롤링 (unrolling) 은 루프 제어 코드의 오버헤드를 제거하고 대신 루프의 각 순회에 해당하는 반복되는 코드를 생성하는 최적화 방법입니다. 모든 계수는 레지스터에 저장되어 값에 대한 접근 속도가 매우 빠릅니다. 런타임에 배열 접근에 대한 경계 검사가 없습니다. 러스트가 적용할 수 있는 이러한 모든 최적화들은 결과적으로 코드를 매우 효율적으로 만듭니다. 이제 이 사실을 알게 되었으니, 반복자와 클로저를 무서워하지 않고 사용할 수 있겠습니다! 반복자와 클로저는 코드를 좀 더 고수준으로 보이도록 하지만, 런타임 성능에 불이익을 주지 않습니다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 성능 비교하기: 루프 vs. 반복자 » 성능 비교하기: 루프 vs. 반복자","id":"250","title":"성능 비교하기: 루프 vs. 반복자"},"251":{"body":"클로저와 반복자는 함수형 프로그래밍 아이디어에서 영감을 받은 러스트의 기능들입니다. 이들은 고수준의 개념을 저수준의 성능으로 명확하게 표현해 주는 러스트의 능력에 기여하고 있습니다. 클로저와 반복자의 구현은 런타임 성능에 영향을 미치지 않도록 설계 되었습니다. 이는 비용 없는 추상화를 제공하기 위해 노력하는 러스트의 목표 중 하나입니다. 이제 I/O 프로젝트의 표현력을 개선했으니, 이런 프로젝트를 세상과 공유하는 데 도움을 줄 cargo의 기능들을 몇 가지 살펴봅시다.","breadcrumbs":"함수형 언어의 특성: 반복자와 클로저 » 성능 비교하기: 루프 vs. 반복자 » 정리","id":"251","title":"정리"},"252":{"body":"지금까지는 빌드, 실행, 코드 테스트 등 카고의 가장 기본적인 기능만 사용하였지만, 카고는 훨씬 더 많은 일을 할 수 있습니다. 이번 장에서는 아래 목록의 기능을 수행하는 몇 가지 고급 기능들을 알아보도록 하겠습니다: 릴리즈 프로필을 통한 빌드 커스터마이징하기 crates.io 에 라이브러리 배포하기 대규모 작업을 위한 작업공간 (workspace) 구성하기 crates.io 로부터 바이너리 설치하기 커스텀 명령어로 카고 확장하기 카고는 이번 장에서 다루는 것보다 더 많은 일을 할 수 있으니, 카고의 모든 기능에 대한 설명을 보고 싶다면 공식 문서 를 참고하세요.","breadcrumbs":"카고와 Crates.io 더 알아보기 » 카고와 Crates.io 더 알아보기","id":"252","title":"카고와 Crates.io 더 알아보기"},"253":{"body":"러스트에서의 릴리즈 프로필 (release profile) 이란 설정값을 가지고 있는 미리 정의된, 커스터마이징 가능한 프로필인데, 이 설정값으로 프로그래머는 코드 컴파일을 위한 다양한 옵션을 제어할 수 있습니다. 각 프로필은 다른 프로필과 독립적으로 설정됩니다. 카고는 두 개의 주요 프로필을 가지고 있습니다: cargo build를 실행할 때 쓰는 dev 프로필과 cargo build --release를 실행할 때 쓰는 release 프로필이 바로 이 둘입니다. dev 프로필은 개발에 적합한 기본값으로 정의되었고, release 프로필은 릴리즈 빌드용 설정을 기본값으로 가집니다. 이 프로필 이름이 빌드 출력에 나와서 익숙할 수도 있겠습니다: $ cargo build Finished dev [unoptimized + debuginfo] target(s) in 0.0s\n$ cargo build --release Finished release [optimized] target(s) in 0.0s 여기에서의 dev와 release가 바로 컴파일러에 의해 사용된 이 두 개의 프로필입니다. 카고에는 프로젝트의 Cargo.toml 파일에 [profile.*] 섹션을 명시적으로 추가하지 않았을 경우 적용되는 각 프로필의 기본 설정이 있습니다. 커스터마이징을 원하는 프로필에 대해 [profile.*] 섹션을 추가하면 이 기본 설정을 덮어씌울 수 있습니다. 여기 예시로 opt-level 설정에 대한 dev 와 release 프로필의 기본 설정값을 보여드리겠습니다: 파일명: Cargo.toml [profile.dev]\nopt-level = 0 [profile.release]\nopt-level = 3 opt-level 설정은 러스트가 코드에 적용할 최적화 수치이며, 0에서 3 사이의 값을 가집니다. 높은 최적화 수치를 적용할수록 컴파일 시간이 늘어나므로, 개발 중 코드를 자주 컴파일하는 상황이라면 코드의 실행 속도가 조금 느려지더라도 컴파일이 빨리 되도록 덜 최적화하길 원할 것입니다. 따라서 dev의 opt-level 기본값은 0 으로 되어 있습니다. 코드를 출시할 준비가 됐을 때라면 더 많은 시간을 컴파일에 쓰는 게 최상책입니다. 릴리즈 모드에서의 컴파일은 한 번이지만, 실행 횟수는 여러 번이니까요. 따라서 릴리즈 모드에서는 긴 컴파일 시간과 빠른 코드 실행 속도를 맞바꿉니다. release 프로필의 opt-level 기본값이 3으로 되어 있는 이유는 이 때문입니다. Cargo.toml 에 기본 설정과 다른 값을 넣어서 기본 설정을 덮어씌울 수 있습니다. 예를 들어 개발용 프로필에 최적화 단계 1을 사용하고 싶다면, 프로젝트의 Cargo.toml 에 아래의 두 줄을 추가하면 됩니다: 파일명: Cargo.toml [profile.dev]\nopt-level = 1 이 코드는 기본 설정인 0을 덮어씌웁니다. 이제부터 cargo build를 실행할 때는 카고가 dev 프로필의 기본값과 커스터마이징된 opt-level을 사용하게 될 것입니다. opt-level을 1로 설정했으므로 카고는 릴리즈 빌드만큼은 아니지만 기본값보다 많은 최적화를 적용할 것입니다. 각 프로필의 설정 옵션 및 기본값의 전체 목록을 보시려면 카고 공식 문서 를 참고해 주시기 바랍니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » 릴리즈 프로필을 통한 빌드 커스터마이징하기 » 릴리즈 프로필을 통한 빌드 커스터마이징하기","id":"253","title":"릴리즈 프로필을 통한 빌드 커스터마이징하기"},"254":{"body":"여지까지 프로젝트의 의존성으로서 crates.io 의 패키지를 이용해 왔지만, 여러분도 자신만의 패키지를 배포 (publish) 하여 다른 사람들과 코드를 공유할 수 있습니다. crates.io 에 있는 크레이트 등기소 (registry) 는 여러분 패키지의 소스 코드를 공개하므로, 이는 주로 오픈 소스인 코드를 호스팅 합니다. 러스트와 카고는 배포한 패키지를 사람들이 더 쉽게 찾고 사용할 수 있도록 도와주는 기능이 있습니다. 이 기능들 몇 가지에 대해 바로 다음에 이야기한 후 패키지를 배포하는 방법을 설명하겠습니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » Crates.io에 크레이트 배포하기","id":"254","title":"Crates.io에 크레이트 배포하기"},"255":{"body":"패키지에 대한 상세한 문서화는 다른 사용자들이 패키지를 어떻게, 언제 사용해야 하는지 알게 해 주므로, 문서 작성에 시간을 투자하는 것은 가치 있는 일입니다. 3장에서 러스트 코드에 두 개의 슬래시 //를 이용하여 주석을 다는 법을 이야기했습니다. 러스트에는 문서화 주석 (documentation comment) 이라고 불리는 문서화를 위한 특별한 종류의 주석도 있는데, 이 주석이 HTML 문서를 생성할 겁니다. 이 HTML에는 여러분의 크레이트가 어떻게 구현되었는지 가 아닌 어떻게 사용하는지 에 관심 있는 프로그래머들을 위하여 공개 API 아이템들에 대한 문서화 주석 내용을 보여줍니다. 문서화 주석은 슬래시 두 개가 아니라 세 개 ///를 이용하며 텍스트 서식을 위한 마크다운 표기법을 지원합니다. 문서화할 아이템 바로 앞에 문서화 주석을 배치하세요. 예제 14-1은 my_crate라는 이름의 크레이트에 있는 add_one 함수에 대한 문서화 주석을 보여줍니다. 파일명: src/lib.rs /// Adds one to the number given.\n///\n/// # Examples\n///\n/// ```\n/// let arg = 5;\n/// let answer = my_crate::add_one(arg);\n///\n/// assert_eq!(6, answer);\n/// ```\npub fn add_one(x: i32) -> i32 { x + 1\n} 예제 14-1: 함수에 대한 문서화 주석 여기서 add_one 함수가 무슨 일을 하는지에 대한 설명을 적었고, 제목 Example로 절을 시작한 다음, add_one 함수의 사용법을 보여주는 코드를 제공했습니다. cargo doc을 실행하면 이 문서화 주석으로부터 HTML 문서를 생성할 수 있습니다. 이 명령어는 러스트와 함께 배포되는 rustdoc 도구를 실행하여 생성된 HTML 문서를 target/doc 디렉터리에 넣습니다. 편의성의 위하여 cargo doc --open을 실행시키면 여러분의 현재 크레이트의 문서에 대해 (심지어 여러분의 크레이트가 가진 모든 의존성의 문서까지) HTML을 생성하고 그 결과를 웹 브라우저에 띄워줄 겁니다. 이제 add_one 함수를 찾아보면 그림 14-1에 보시는 것처럼 문서화 주석의 텍스트가 어떤 식으로 렌더링 되는지 알 수 있을 겁니다: 그림 14-1: add_one 함수에 대한 HTML 문서 자주 사용되는 절 예제 14-1에서는 HTML에 \"Examples\" 제목을 가진 절을 만들기 위해 # Examples 마크다운 제목을 사용했습니다. 이외에 크레이트 저자가 문서에서 자주 사용하는 절은 다음과 같습니다: Panics : 문서화된 함수가 패닉을 일으킬 수 있는 시나리오입니다. 함수를 호출하는 쪽에서 자신의 프로그램이 패닉을 일으키는 것을 원치 않는다면 이러한 상황에서 함수를 호출하지 않음을 확실히 해야 합니다. Errors : 해당 함수가 Result를 반환하는 경우에는 발생할 수 있는 에러의 종류와 해당 에러들이 발생하는 조건을 설명해 준다면 호출하는 사람이 다양한 종류의 에러를 여러 방법으로 처리할 수 있도록 코드를 작성하는 데 도움을 줄 수 있습니다. Safety : 함수가 호출하기에 unsafe한 경우라면 (불안전성에 대해서는 19장에서 다룹니다), 이 함수가 안전하지 않은 이유와 호출자가 이 함수를 호출할 때 지켜야 할 불변성 (invariant) 에 대해 설명하는 절이 있어야 합니다. 대부분의 문서화 주석에 이 절들이 모두 필요하진 않습니다만, 여러분의 코드를 사용하는 사람들이 알고 싶어 하는 것에 대한 측면을 상기하는데 좋은 체크리스트입니다. 테스트로서의 문서화 주석 문서화 주석에 예시 코드를 추가하는 건 라이브러리의 사용 방법을 보여주는데 도움이 될뿐더러 추가적인 보너스도 가질 수 있습니다: 무려 cargo test를 실행하면 여러분의 문서에 들어있던 예시 코드들이 테스트로서 실행됩니다! 예시를 포함한 문서보다 좋은 문서는 없습니다. 하지만 문서가 작성된 이후 코드가 변경되어 작동하지 않게 되어버린 예제보다 나쁜 것도 없습니다. 예제 14-1의 add_one 함수에 대한 문서를 가지고 cargo test를 실행하면 다음과 같이 테스트 결과 절을 볼 수 있습니다: Doc-tests my_crate running 1 test\ntest src/lib.rs - add_one (line 5) ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s 이제 함수나 예제를 변경하여 예시 코드의 assert_eq!가 패닉을 발생시키는 상태로 cargo test를 다시 실행하면, 문서 테스트 기능이 해당 예제를 찾아내어 이 코드가 더 이상 기능하지 못함을 알려줄 것입니다! 주석이 포함된 아이템 문서화 주석 스타일 //!은 주석 뒤에 오는 아이템을 문서화하는 것이 아닌 주석을 담고 있는 아이템을 문서화합니다. 이러한 문서화 주석은 일반적으로 크레이트 루트 파일 (관례상 src/lib.rs ) 혹은 모듈에 사용하여 크레이트 혹은 모듈 전체에 대한 문서를 작성하는 데 씁니다. 예를 들어 add_one 함수를 담고 있는 my_crate 크레이트의 목적을 설명하는 문서를 추가하려면 예제 14-2와 같이 src/lib.rs 파일의 시작 지점에 //!로 시작하는 문서화 주석을 추가합니다: 파일명: src/lib.rs //! # My Crate\n//!\n//! `my_crate` is a collection of utilities to make performing certain\n//! calculations more convenient. /// Adds one to the number given.\n// --생략--\n# ///\n# /// # Examples\n# ///\n# /// ```\n# /// let arg = 5;\n# /// let answer = my_crate::add_one(arg);\n# ///\n# /// assert_eq!(6, answer);\n# /// ```\n# pub fn add_one(x: i32) -> i32 {\n# x + 1\n# } 예제 14-2: my_crate 크레이트 전체에 대한 문서 //!로 시작하는 라인 중 마지막 라인 이후에 아무 코드도 없음을 주목하세요. /// 대신 //!로 주석을 시작하였기 때문에, 이 주석 뒤에 나오는 아이템이 아닌 이 주석을 포함하고 있는 아이템에 대한 문서화를 하는 중입니다. 위의 경우 그 아이템은 크레이트 루트인 src/lib.rs 파일이며, 크레이트 전체를 설명합니다 cargo doc --open을 실행하면 그림 14-2와 같이 문서 첫 페이지 내용 중 크레이트의 공개 아이템 목록 상단에 이 주석의 내용이 나타날 것입니다: 그림 14-2: 전체 크레이트를 설명하는 주석이 포함된 my_crate의 렌더링 된 문서 아이템 내 문서화 주석은 특히 크레이트와 모듈에 대해 기술할 때 유용합니다. 이를 이용해 주석이 담긴 것의 전체 목적을 설명해서 사용자들이 크레이트 구조를 이해할 수 있도록 해보세요.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » 유용한 문서화 주석 만들기","id":"255","title":"유용한 문서화 주석 만들기"},"256":{"body":"크레이트를 배포할 때는 공개 API의 구조가 주요 고려사항입니다. 여러분의 크레이트를 사용하는 사람들은 여러분보다 그 구조에 덜 익숙하고, 크레이트가 커다란 모듈 계층 구조를 이루고 있다면 사용하고자 하는 조각들을 찾아내는 데 어려움이 있을 수도 있습니다. 7장에서는 pub 키워드를 사용하여 아이템을 공개하는 법, 그리고 use 키워드를 가지고 스코프 안으로 아이템을 가져오는 법을 다루었습니다. 하지만 크레이트를 개발하는 동안 여러분에게 익숙해진 구조가 사용자들에게는 마냥 편리하지 않을지도 모릅니다. 구조체들을 여러 단계로 구성된 계층 구조로 조직화하고 싶을 수도 있지만, 그러면 계층 구조 깊숙히 정의된 타입을 이용하고 싶어 하는 사람들은 해당 타입의 존재를 발견하는 데 어려움을 겪을 수도 있습니다. 또한 사용자들은 use my_crate::UsefulType;이 아니라 use my_crate::some_module::another_module::UsefulType;라고 입력해야 하는 데에 짜증을 낼지도 모릅니다. 좋은 소식은 지금의 구조가 다른 사람들이 다른 라이브러리에서 사용하는 데 편리하지 않더라도 굳이 내부 구조를 뒤엎을 필요는 없다는 겁니다. 대신에 pub use를 이용하여 내부 아이템을 다시 내보내서 ( re-export ) 기존의 비공개 구조와 다른 공개 구조를 만들 수 있습니다. 다시 내보내기는 어떤 위치에서 공개 아이템 (public item) 을 가져와서 이를 마치 다른 위치에 정의된 것처럼 해당 위치의 공개 아이템으로 만듭니다. 예를 들어, 예술적인 개념을 모델링하기 위해 art라는 라이브러리를 만들었다고 가정해 봅시다. 이 라이브러리에는 두 모듈이 들어 있습니다: 예제 14-3과 같이 kinds 모듈에는 PrimaryColor와 SecondaryColor 열거형이 있고, utils 모듈에는 mix라는 이름의 함수가 있습니다: 파일명: src/lib.rs //! # Art\n//!\n//! A library for modeling artistic concepts. pub mod kinds { /// The primary colors according to the RYB color model. pub enum PrimaryColor { Red, Yellow, Blue, } /// The secondary colors according to the RYB color model. pub enum SecondaryColor { Orange, Green, Purple, }\n} pub mod utils { use crate::kinds::*; /// Combines two primary colors in equal amounts to create /// a secondary color. pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { // --생략--\n# unimplemented!(); }\n} 예제 14-3: kinds와 utils 모듈에 아이템을 구성한 art 라이브러리 그림 14-3은 이 크레이트에 대하여 cargo doc으로 생성시킨 문서의 첫 화면입니다: 그림 14-3: kinds와 utils 모듈이 목록에 나타난 art의 문서 첫 화면 PrimaryColor와 SecondaryColor 타입도, mix 함수도 목록에 나타나지 않았음을 주목하세요. 이들을 보려면 각각 kinds와 utils를 클릭해야 합니다. 이 라이브러리에 의존하는 다른 크레이트에서는 art의 아이템을 스코프 안으로 가져오는 use를 사용해야 하는데, 현재 정의된 모듈의 구조대로 지정해야 합니다. 예제 14-4는 어떤 크레이트에서 art 크레이트의 PrimaryColor와 mix를 이용하는 예시를 보여줍니다: 파일명: src/main.rs use art::kinds::PrimaryColor;\nuse art::utils::mix; fn main() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow);\n} 예제 14-4: art 크레이트의 내부 구조에서 내보내진 아이템을 이용하는 크레이트 예제 14-4 코드의 저자, 즉 art 크레이트를 사용하는 사람은 PrimaryColor가 kinds 모듈에 들어있고 mix가 utils 모듈에 들어있다는 사실을 알아내야 합니다. art 크레이트의 구조는 크레이트를 사용하는 사람보다 크레이트를 개발하는 사람에게 더 적합합니다. 내부 구조는 art 크레이트를 사용하고자 하는 사람에게는 전혀 필요 없는 정보이며, 오히려 이를 사용하는 개발자가 어디를 찾아봐야 하는지 파악하고 use 구문에 모듈 이름들을 지정해야 하기 때문에 혼란만 야기할 뿐입니다. 공개 API로부터 내부 구조를 제거하기 위해서는 예제 14-5와 같이 예제 14-3의 art 크레이트 코드에 pub use 구문을 추가하여 아이템들을 최상위 단계로 다시 내보내야 합니다: 파일명: src/lib.rs //! # Art\n//!\n//! A library for modeling artistic concepts. pub use self::kinds::PrimaryColor;\npub use self::kinds::SecondaryColor;\npub use self::utils::mix; pub mod kinds { // --생략--\n# /// The primary colors according to the RYB color model.\n# pub enum PrimaryColor {\n# Red,\n# Yellow,\n# Blue,\n# }\n# # /// The secondary colors according to the RYB color model.\n# pub enum SecondaryColor {\n# Orange,\n# Green,\n# Purple,\n# }\n} pub mod utils { // --생략--\n# use crate::kinds::*;\n# # /// Combines two primary colors in equal amounts to create\n# /// a secondary color.\n# pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {\n# SecondaryColor::Orange\n# }\n} 예제 14-5: pub use 구문을 추가하여 아이템을 다시 내보내기 cargo doc이 생성한 이 크레이트의 API 문서는 이제 그림 14-4와 같이 다시 내보내진 아이템을 첫 화면의 목록에 보여주고 링크를 걸어줄 것이며, 이로써 PrimaryColor와 SecondaryColor 타입과 mix 함수를 더 쉽게 찾도록 만들어 줍니다. 그림 14-4: 다시 내보내진 아이템이 목록에 있는 art 문서 첫 화면 art 크레이트 사용자는 예제 14-4에서 봤던 것처럼 예제 14-3의 내부 구조를 여전히 보고 이용할 수 있고, 혹은 예제 14-6과 같이 예제 14-5의 더 편리해진 구조를 사용할 수도 있습니다: 파일명: src/main.rs use art::mix;\nuse art::PrimaryColor; fn main() { // --생략--\n# let red = PrimaryColor::Red;\n# let yellow = PrimaryColor::Yellow;\n# mix(red, yellow);\n} 예제 14-6: art 크레이트의 다시 내보내진 아이템을 사용하는 프로그램 중첩된 모듈이 많이 있는 경우, pub use를 사용하여 최상위 단계로 타입들을 다시 내보내는 것은 크레이트를 사용하는 사람들의 경험을 크게 바꿀 수 있습니다. pub use의 또 다른 일반적인 사용법은 현재 크레이트가 의존하고 있는 크레이트에 정의된 것을 다시 내보내서 그 크레이트의 정의를 여러분 크레이트의 공개 API의 일부분으로 만드는 것입니다. 유용한 공개 API를 만드는 것은 기술보단 예술에 가깝고, 여러분은 반복적으로 사용자들에게 가장 잘 맞는 API를 찾아갈 수 있습니다. pub use를 사용하는 것은 크레이트를 내부적으로 구조화하는 데 유연성을 제공하면서 이 내부 구조와 사용자에게 제공하는 것을 분리해 줍니다. 여러분이 설치한 크레이트 코드 몇 개를 열어서 내부 구조와 공개 API가 얼마나 다른지 살펴보세요.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » pub use로 편리한 공개 API 내보내기","id":"256","title":"pub use로 편리한 공개 API 내보내기"},"257":{"body":"어떤 크레이트를 배포하기에 앞서 crates.io 에서 계정을 만들고 API 토큰을 얻을 필요가 있습니다. 그러려면 crates.io 홈페이지에 방문해서 GitHub 계정으로 로그인하세요. (현재는 GitHub 계정이 필수지만, 나중에는 다른 계정 생성 방법을 지원할 수도 있습니다.) 일단 로그인되었다면 https://crates.io/me/ 에 있는 계정 설정으로 가서 API 키를 얻으세요. 그런 다음 아래와 같이 여러분의 API 키로 cargo login 명령어를 실행하세요: $ cargo login abcdefghijklmnopqrstuvwxyz012345 이 명령어는 카고에게 여러분의 API 토큰을 알려주고 로컬의 ~/.cargo/credentials 에 저장하도록 합니다. 이 토큰은 비밀키 (secret) 임을 주의하세요: 아무와도 공유하지 마세요. 어떤 이유에서든 누군가와 공유했다면, 이 토큰을 무효화시키고 crates.io 에서 새 토큰을 생성해야 합니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » Cartes.io 계정 설정하기","id":"257","title":"Cartes.io 계정 설정하기"},"258":{"body":"이제 배포하고자 하는 크레이트가 있다고 칩시다. 배포하기 전, 크레이트의 Cargo.toml 파일의 [package] 절 안에 메타데이터 몇 가지를 추가할 필요가 있을 것입니다. 여러분의 크레이트는 고유한 이름이 필요할 것입니다. 로컬에서 어떤 크레이트를 작업하는 중이라면 이 크레이트의 이름을 뭐라고 짓든 상관없습니다. 하지만 crates.io 에 올라오는 크레이트의 이름은 선착순으로 배정됩니다. 일단 크레이트 이름이 사용되고 나면, 그 이름으로는 다른 누구도 크레이트를 배포할 수 없습니다. 크레이트를 배포하기 전에 사용하려는 이름을 검색해 보세요. 해당 크레이트명이 사용되었다면, 다른 이름을 찾아서 Cargo.toml 파일 안의 [package] 절 아래에 다음과 같이 name 필드를 수정하여 배포를 위한 새로운 이름을 사용해야 합니다: 파일명: Cargo.toml [package]\nname = \"guessing_game\" 고유한 이름을 선택했더라도 이 시점에서 cargo publish를 실행시켜 크레이트를 배포해 보면 다음과 같은 경고 후 에러를 보게 될 것입니다: $ cargo publish Updating crates.io index\nwarning: manifest has no description, license, license-file, documentation, homepage or repository.\nSee https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.\n--snip--\nerror: failed to publish to registry at https://crates.io Caused by: the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata 이 에러는 몇 가지 중요한 정보가 없기 때문에 발생된 것입니다: 설명과 라이선스는 필수로서 이 크레이트가 무엇을 하는지와 어떤 조건으로 사용할 수 있는지 사람들이 알게끔 할 것입니다. Cargo.toml 안에 한두 문장 정도만 설명을 추가해 주세요. 이 설명은 여러분 크레이트의 검색 결과에 함께 나타나게 될 것입니다. license 필드에는 라이선스 식별자 값 (license identifier value) 이 필요합니다. Linux 재단의 Software Package Data Exchange (SPDX) 에 이 값으로 사용할 수 있는 식별자 목록이 있습니다. 예를 들어 여러분의 크레이트에 MIT 라이선스를 적용하고 싶다면, MIT 식별자를 추가합니다: 파일명: Cargo.toml [package]\nname = \"guessing_game\"\nlicense = \"MIT\" SPDX에 없는 라이선스를 사용하고 싶다면, 그 라이선스에 대한 텍스트를 파일에 넣어서 프로젝트 내에 포함시킨 다음, license 키 대신 license-file을 사용하여 해당 파일의 이름을 지정해야 합니다. 여러분의 프로젝트에 어떤 라이선스가 적합한지에 대한 안내는 이 책의 범위를 벗어납니다. 러스트 커뮤니티의 많은 이들은 자신의 프로젝트에 러스트가 쓰는 라이선스인 MIT OR Apache-2.0 듀얼 라이선스를 사용합니다. 이러한 실제 예는 여러분도 OR로 구분된 여러 라이선스 식별자를 지정하여 프로젝트에 여러 개의 라이선스를 적용할 수 있음을 보여줍니다. 고유한 이름, 버전, 설명, 그리고 라이선스가 추가된 상태에서 배포할 준비가 된 프로젝트의 Cargo.toml 파일은 아래처럼 생겼습니다: 파일명: Cargo.toml [package]\nname = \"guessing_game\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"A fun game where you guess what number the computer has chosen.\"\nlicense = \"MIT OR Apache-2.0\" [dependencies] 카고 공식 문서 에는 다른 사람들이 여러분의 크레이트를 더 쉽게 발견하고 사용하도록 해주기 위해 지정할 수 있는 다른 메타데이터에 대해 설명되어 있습니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » 새 크레이트에 메타데이터 추가하기","id":"258","title":"새 크레이트에 메타데이터 추가하기"},"259":{"body":"이제 계정을 만들었고, API 토큰을 저장했고, 크레이트의 이름도 정했고, 필요한 메타데이터도 지정되었다면, 배포할 준비가 된 것입니다! 크레이트 배포는 다른 사람들이 사용할 특정 버전을 crates.io 에 올리는 것입니다. 배포는 영구적이므로 주의하세요. 버전은 덮어씌워질 수 없고, 코드는 삭제될 수 없습니다. crates.io 의 주요 목표 한 가지는 영구적인 코드 보관소로서 동작하여 crates.io 의 크레이트에 의존하는 모든 프로젝트의 빌드가 계속 동작하도록 하는 것입니다. 버전 삭제를 서용하면 이 목표의 이행을 불가능하게 할 것입니다. 하지만 배포할 수 있는 크레이트 버전의 숫자에 제한은 없습니다. 다시 한번 cargo publish 명령어를 수행해 보세요. 이제 성공해야 합니다: $ cargo publish Updating crates.io index Packaging guessing_game v0.1.0 (file:///projects/guessing_game) Verifying guessing_game v0.1.0 (file:///projects/guessing_game) Compiling guessing_game v0.1.0\n(file:///projects/guessing_game/target/package/guessing_game-0.1.0) Finished dev [unoptimized + debuginfo] target(s) in 0.19s Uploading guessing_game v0.1.0 (file:///projects/guessing_game) 축하합니다! 여러분은 이제 코드를 러스트 커뮤니티에 공유하였고, 다른 사람들이 자신의 프로젝트에 여러분의 크레이트를 의존성으로 쉽게 추가할 수 있습니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » Crates.io에 배포하기","id":"259","title":"Crates.io에 배포하기"},"26":{"body":"카고로 프로젝트를 생성해 보고 앞서 만들었던 ‘Hello, world!’ 프로젝트와 비교해 봅시다. projects 디렉터리로 (다른 곳에 코드를 만드신 분은 해당 위치로) 돌아가 다음 명령어를 실행해 보세요. $ cargo new hello_cargo\n$ cd hello_cargo 첫 번째 명령어는 hello_cargo 라는 디렉터리를 생성합니다. 우리는 프로젝트의 이름을 hello_cargo 로 지정했고 카고는 동일한 이름의 디렉터리 안에 파일들을 생성합니다. hello_cargo 디렉터리로 이동해 파일을 살펴보면 Cargo.toml 파일과 src 디렉터리를 확인할 수 있으며, src 디렉터리 내에는 main.rs 파일이 있는 것도 볼 수 있습니다. 그 외에도 .gitignore 파일과 함께 새 Git 저장소가 초기화됩니다. 여러분이 이미 Git 저장소가 있는 디렉터리에서 cargo new를 실행시킨다면 Git 파일들은 생성되지 않을 것입니다. 이 동작은 cargo new --vcs=git 명령을 통해 덮어쓸 수 있습니다. Note: Git은 일반적으로 사용하는 버전 관리 시스템입니다. 따라서 기본 설정되어 있으며, 이 설정은 cargo new 명령어의 --vcs 플래그로 변경할 수 있습니다. 그 외의 다른 옵션들은 cargo new --help로 확인할 수 있습니다. 이제 텍스트 에디터로 Cargo.toml 을 열어보세요. 예제 1-2처럼 나오면 정상입니다. 파일명: Cargo.toml [package]\nname = \"hello_cargo\"\nversion = \"0.1.0\"\nedition = \"2021\" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] 예제 1-2: cargo new로 생성한 Cargo.toml 파일의 내용 이 파일은 TOML ( Tom’s Obvious, Minimal Language ) 포맷으로 되어있고, 이 포맷은 카고 설정에서 사용하는 포맷입니다. [package]라고 적힌 첫 번째 라인은 섹션 헤더로, 뒤에 패키지 설정 구문들이 따라오는 걸 보실 수 있습니다. 나중에 우리가 이 파일에 내용을 추가하며 새로운 섹션을 만들어 볼 겁니다. 다음 세 라인은 카고가 코드를 컴파일하는 데 필요한 설정 정보로, 각각 패키지명, 버전, 작성자, 사용하는 러스트 에디션을 나타냅니다. edition 키에 대한 설명은 부록 E 에서 다룹니다. 마지막 라인의 [dependencies]는 프로젝트에서 사용하는 의존성 목록입니다. 러스트에서는 코드 패키지를 크레이트 (crate) 라고 부릅니다. 이 프로젝트에는 크레이트가 필요 없지만, 2장 첫 프로젝트에서는 필요하므로 그때 사용해 보겠습니다. 이제 src/main.rs 를 열어 살펴봅시다: 파일명: src/main.rs fn main() { println!(\"Hello, world!\");\n} 카고가 ‘Hello, world!’ 프로그램을 만들어 놨네요. 예제 1-1에서 만든 프로젝트와 다른 점은 이번엔 코드 위치가 src 디렉터리라는 점과 최상위 디렉터리에 Cargo.toml 설정 파일이 존재한다는 점입니다. 카고는 소스 파일이 src 내에 있다고 예상합니다. 최상위 프로젝트 디렉터리를 README, 라이선스, 설정 파일 등 코드 자체와는 관련 없는 파일들을 저장하는 데 사용됩니다. 이처럼 카고는 각각의 파일을 알맞은 위치에 배치하여 여러분이 프로젝트를 조직화하는 걸 돕습니다. ‘Hello, world!’ 프로젝트에서처럼 프로젝트 생성 시 카고를 사용하지 않았어도, Cargo.toml 파일을 알맞게 작성하고 프로젝트 코드를 src 디렉터리로 옮기면, 카고를 사용하는 프로젝트로 변경이 가능합니다.","breadcrumbs":"시작해봅시다 » 카고를 사용해봅시다 » 카고로 프로젝트 생성하기","id":"26","title":"카고로 프로젝트 생성하기"},"260":{"body":"크레이트를 변경하여 새 버전을 배포할 준비가 되었다면, Cargo.toml 파일에 명시된 version 값을 바꿔 다시 배포하면 됩니다. 변경 사항의 종류에 기반하여 적절한 버전 숫자를 결정하려면 유의적 버전 규칙 (Semantic Versioning Rules) 을 사용하세요. 그다음 cargo publish를 실행하여 새 버전을 올립니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » 이미 존재하는 크레이트의 새 버전 배포하기","id":"260","title":"이미 존재하는 크레이트의 새 버전 배포하기"},"261":{"body":"크레이트의 이전 버전을 제거할 수는 없지만, 향후의 프로젝트들이 이를 새로운 의존성으로 추가하는 것을 방지할 수는 있습니다. 이는 어떤 크레이트 버전이 어떤 이유에서인가 깨졌을 때 유용합니다. 그런 상황에서 카고는 어떤 크레이트 버전의 끌어내기 (yanking) 를 지원합니다. 버전 끌어내기는 이 버전에 의존하는 중인 존재하는 모든 프로젝트들을 계속 지원하면서 새 프로젝트가 이 버전에 의존하는 것을 방지합니다. 근본적으로 끌어내기란 Cargo.lock 이 있는 모든 프로젝트가 깨지지 않으면서, 이후에 생성되는 Cargo.lock 파일에는 끌어내려진 버전을 사용하지 않을 것임을 의미합니다. 크레이트의 버전을 끌어내리려면 이전에 배포했던 크레이트 디렉터리에서 cargo yank를 실행하여 끌어내리고자 하는 버전을 지정하세요. 예를 들어 guessing_game이라는 이름의 크레이트 버전 1.0.1을 배포했었고 이를 끌어내리고자 한다면, guessing_game의 프로젝트 디렉터리에서 다음과 같이 실행합니다: $ cargo yank --vers 1.0.1 Updating crates.io index Yank guessing_game@1.0.1 명령어에 --undo를 추가하면 끌어내기를 되돌려 다른 프로젝트들이 다시 이 버전에 대한 의존을 허용할 수 있습니다: $ cargo yank --vers 1.0.1 --undo Updating crates.io index Unyank guessing_game@1.0.1 끌어내기는 어떤 코드도 삭제하지 않습니다 . 예를 들어 실수로 업로드된 비밀키 같은걸 삭제할 수는 없습니다. 그런 일이 벌어졌다면 즉시 해당 비밀키를 리셋해야 합니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » cargo yank로 Crates.io에서 버전 사용하지 않게 하기","id":"261","title":"cargo yank로 Crates.io에서 버전 사용하지 않게 하기"},"262":{"body":"12장에서 바이너리 크레이트와 라이브러리 크레이트를 포함하는 패키지를 만들어 봤습니다. 하지만 프로젝트를 개발하다 보면, 라이브러리 크레이트가 점점 거대해져서 패키지를 여러 개의 라이브러리 크레이트로 분리하고 싶을 겁니다. 카고는 작업공간 (workspace) 이라는 기능을 제공하여 나란히 개발되는 여러 관련 패키지를 관리하는 데 도움을 줄 수 있습니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » 카고 작업공간 » 카고 작업공간","id":"262","title":"카고 작업공간"},"263":{"body":"작업공간은 동일한 Cargo.lock 과 출력 디렉터리를 공유하는 패키지들의 집합입니다. 작업공간을 이용하여 프로젝트를 만들어 봅시다- 여기서는 간단한 코드만 사용하여 작업공간의 구조에 집중하겠습니다. 작업공간을 구성하는 방법은 여러 가지가 있으므로, 그중 일반적인 방법 하나를 보겠습니다. 우리의 작업공간은 하나의 바이너리와 두 개의 라이브러리를 담을 것입니다. 주요 기능을 제공할 바이너리는 두 라이브러리를 의존성으로 가지게 될 것입니다. 첫 번째 라이브러리는 add_one 함수를 제공하고, 두 번째 라이브러리는 add_two 함수를 제공할 것입니다. 이 세 크레이트는 같은 작업공간의 일부가 될 겁니다. 작업공간을 위한 새 디렉터리를 만드는 것부터 시작하겠습니다: $ mkdir add\n$ cd add 다음으로 add 디렉터리 내에 Cargo.toml 을 생성하여 전체 작업공간에 대한 설정을 합니다. 이 파일은 [package] 절이 없습니다. 대신 [workspace] 절로 시작하여 바이너리 크레이트 패키지에 대한 경로를 명시하는 방식으로 이 작업공간에 멤버를 추가할 것입니다; 지금의 경우 해당 경로는 adder 입니다: 파일명: Cargo.toml [workspace] members = [ \"adder\",\n] 다음엔 add 디렉터리 내에서 cargo new를 실행하여 adder 바이너리 크레이트를 생성하겠습니다: $ cargo new adder Created binary (application) `adder` package 이 시점에서 작업 공간을 cargo build로 빌드할 수 있습니다. add 디렉터리 내의 파일들은 아래와 같은 형태여야 합니다: ├── Cargo.lock\n├── Cargo.toml\n├── adder\n│ ├── Cargo.toml\n│ └── src\n│ └── main.rs\n└── target 작업공간은 컴파일된 결과가 위치할 하나의 target 디렉터리를 최상위 디렉터리에 가집니다; adder 크레이트는 자신의 target 디렉터리를 갖지 않습니다. adder 디렉터리 내에서 cargo build 명령어를 실행하더라도 컴파일 결과는 add/adder/target 이 아닌 add/target 에 위치하게 될 겁니다. 카고가 이처럼 target 디렉터리를 작업공간 내에 구성하는 이유는, 작업공간 내의 크레이트들이 서로 의존하기로 되어있기 때문입니다. 만약 각 크레이트가 각자의 target 디렉터리를 갖는다면, 각 크레이트는 작업공간 내의 다른 크레이트들을 다시 컴파일하여 그 결과물을 자신의 target 디렉터리에 넣어야 합니다. 하나의 target 디렉터리를 공유하면 크레이트들의 불필요한 재빌드를 피할 수 있습니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » 카고 작업공간 » 작업공간 생성하기","id":"263","title":"작업공간 생성하기"},"264":{"body":"다음으로 다른 멤버 패키지를 작업공간에 생성하여 add_one이라고 이름을 붙입시다. 최상위 Cargo.toml 을 수정하여 members 목록에 add_one 경로를 지정하세요: 파일명: Cargo.toml [workspace] members = [ \"adder\", \"add_one\",\n] 그런 다음 add_one이라는 이름의 새 라이브러리 크레이트를 생성하세요: $ cargo new add_one --lib Created library `add_one` package add 디렉터리는 이제 다음과 같은 디렉터리와 파일을 갖추어야 합니다: ├── Cargo.lock\n├── Cargo.toml\n├── add_one\n│ ├── Cargo.toml\n│ └── src\n│ └── lib.rs\n├── adder\n│ ├── Cargo.toml\n│ └── src\n│ └── main.rs\n└── target add_one/src/lib.rs 파일에 add_one 함수를 추가합시다: 파일명: add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1\n} 이제 바이너리를 가지고 있는 adder 패키지와 이것이 의존하는 라이브러리를 갖고 있는 add_one 패키지를 갖추었습니다. 먼저 adder/Cargo.toml 에 add_one의 경로 의존성을 추가할 필요가 있겠습니다. 파일명: adder/Cargo.toml [dependencies]\nadd_one = { path = \"../add_one\" } 카고는 작업 공간 내의 크레이트들이 서로 의존할 것이라고 가정하지 않으므로, 의존성 관계에 대해 명시할 필요가 있습니다. 다음으로 adder 크레이트에서 (add_one 크레이트에 있는) add_one 함수를 사용해 봅시다. adder/src/main.rs 파일을 열어서 제일 윗줄에 use를 추가하여 스코프로 새로운 add_one 라이브러리를 가져옵시다. 그런 다음 예제 14-7과 같이 main 함수를 수정하여 add_one 함수를 호출하세요. 파일명: adder/src/main.rs use add_one; fn main() { let num = 10; println!(\"Hello, world! {num} plus one is {}!\", add_one::add_one(num));\n} 예제 14-7: adder 크레이트에서 add_one 라이브러리 크레이트 사용하기 최상위 add 디렉터리에서 cargo build를 실행하여 작업공간을 빌드해 봅시다! $ cargo build Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 0.68s add 디렉터리에서 바이너리 크레이트를 실행하기 위해서는 cargo run에 -p 인수와 패키지명을 써서 작업공간 내의 어떤 패키지를 실행하고 싶은지 지정해야 합니다: $ cargo run -p adder Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/adder`\nHello, world! 10 plus one is 11! 이 명령은 adder/src/main.rs 의 코드를 실행시키고, 이는 add_one 크레이트에 의존하고 있습니다. 작업공간에서 외부 패키지 의존하기 작업공간에는 각 크레이트 디렉터리마다 Cargo.lock 이 생기지 않고, 최상위에 하나의 Cargo.lock 이 생긴다는 점을 주목하세요. 이는 모든 크레이트가 모든 의존성에 대해 같은 버전을 사용함을 보증합니다. adder/Cargo.toml 과 add_one/Cargo.toml 에 rand 패키지를 추가하면, 카고는 이 둘을 하나의 rand 버전으로 결정하여 하나의 Cargo.lock 에 기록합니다. 작업공간 내 모든 크레이트가 동일한 의존성을 사용하도록 만드는 것은 이 크레이트들이 항상 서로 호환될 것임을 뜻합니다. * add_one/Cargo.toml 파일의 [dependencies] 절에 rand 크레이트를 추가하여 add_one 크레이트에서 rand 크레이트를 사용해 봅시다: 파일명: add_one/Cargo.toml [dependencies]\nrand = \"0.8.5\" 이제 add_one/src/lib.rs 파일에 use rand;를 추가할 수 있으며, add 디렉터리에서 cargo build를 실행하여 전체 작업공간을 빌드하면 rand 크레이트를 가져와 컴파일할 것입니다. 아직 스코프로 가져온 rand를 참조하지 않았으므로 경고 하나를 받을 겁니다: $ cargo build Updating crates.io index Downloaded rand v0.8.5 --snip-- Compiling rand v0.8.5 Compiling add_one v0.1.0 (file:///projects/add/add_one)\nwarning: unused import: `rand` --> add_one/src/lib.rs:1:5 |\n1 | use rand; | ^^^^ | = note: `#[warn(unused_imports)]` on by default warning: `add_one` (lib) generated 1 warning Compiling adder v0.1.0 (file:///projects/add/adder) Finished dev [unoptimized + debuginfo] target(s) in 10.18s 최상위의 Cargo.lock 에는 이제 add_one의 rand 의존성에 대한 정보가 포함됩니다. 하지만 작업공간의 어딘가에서 rand가 사용되더라도 작업공간의 다른 크레이트의 Cargo.toml 파일에 마찬가지로 rand를 추가하지 않으면 이를 사용할 수 없습니다. 예를 들어 use rand;를 adder 패키지의 adder/src/main.rs 파일에 추가하면 다음과 같은 에러가 납니다: $ cargo build --snip-- Compiling adder v0.1.0 (file:///projects/add/adder)\nerror[E0432]: unresolved import `rand` --> adder/src/main.rs:2:5 |\n2 | use rand; | ^^^^ no external crate `rand` 이를 수정하려면 adder 패키지의 Cargo.toml 을 고쳐서 이 패키지도 rand에 의존함을 알려주세요. adder 패키지를 빌드하면 Cargo.lock 에 있는 adder에 대한 의존성 목록에 rand를 추가하지만, rand의 추가 복제본을 내려받지는 않을 것입니다. 카고는 작업공간 내에서 rand 패키지를 사용하는 모든 패키지의 모든 크레이트가 동일한 버전을 사용할 것임을 보증하여 저장공간을 아끼고 작업공간 내의 크레이트들이 확실히 서로 호환되도록 합니다. 작업공간에 테스트 추가하기 또 다른 개선 사항으로, add_one::add_one 함수의 테스트를 add_one 크레이트 내에 추가해 봅시다: 파일명: add_one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1\n} #[cfg(test)]\nmod tests { use super::*; #[test] fn it_works() { assert_eq!(3, add_one(2)); }\n} 이제 최상위 add 디렉터리에서 cargo test를 실행해 보세요. 이런 구조의 작업공간에서 cargo test를 실행하면 작업공간의 모든 크레이트에 대한 테스트를 실행할 것입니다: $ cargo test Compiling add_one v0.1.0 (file:///projects/add/add_one) Compiling adder v0.1.0 (file:///projects/add/adder) Finished test [unoptimized + debuginfo] target(s) in 0.27s Running unittests src/lib.rs (target/debug/deps/add_one-f0253159197f7841) running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/adder-49979ff40686fa8e) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 출력의 첫 번째 절은 add_one 크레이트의 it_works 테스트가 통과되었음을 보여줍니다. 다음 절은 adder 크레이트에서 아무 테스트도 발견하지 못했음을 보여주고, 마지막 절에서는 add_one 크레이트 내에서 아무런 문서 테스트도 발견하지 못했음을 보여줍니다. -p 플래그와 테스트하고자 하는 크레이트의 이름을 명시하면 최상위 디렉터리에서 작업공간에 있는 특정 크레이트에 대한 테스트를 실행할 수도 있습니다: $ cargo test -p add_one Finished test [unoptimized + debuginfo] target(s) in 0.00s Running unittests src/lib.rs (target/debug/deps/add_one-b3235fea9a156f74) running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests add_one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s 이 출력은 cargo test가 add_one 크레이트에 대한 테스트만 실행했으며 adder 크레이트의 테스트는 실행하지 않았음을 보여줍니다. 작업공간의 크레이트를 crates.io 에 배포한다면, 작업공간 내 각 크레이트를 별도로 배포할 필요가 있습니다. cargo test처럼 -p 플래그와 배포하고자 하는 크레이트의 이름을 지정하여 작업공간 내의 특정 크레이트를 배포할 수 있습니다. 추가 연습을 위해 이 작업공간에 add_one 크레이트와 비슷한 방식으로 add_two 크레이트를 추가해 보세요! 프로젝트가 커지면 작업공간 사용을 고려해 보세요: 하나의 커다란 코드 덩어리보다는 작고 개별적인 구성 요소들을 이해하는 것이 쉬우니까요. 게다가 작업공간에 크레이트들을 유지하는 것은 이 크레이트들이 동시에 자주 변경될 경우 크레이트 간의 조정을 더 쉽게 해 줄 수 있습니다.","breadcrumbs":"카고와 Crates.io 더 알아보기 » 카고 작업공간 » 작업공간에 두 번째 패키지 생성하기","id":"264","title":"작업공간에 두 번째 패키지 생성하기"},"265":{"body":"cargo install 명령어는 로컬 환경에 바이너리 크레이트를 설치하고 사용할 수 있도록 해줍니다. 이는 시스템 패키지를 대체할 의도는 아닙니다; 러스트 개발자들이 crates.io 에서 공유하고 있는 도구를 편리하게 설치할 수 있도록 하기 위함입니다. 바이너리 타겟 (binary target) 을 가진 패키지만 설치할 수 있음을 주의하세요. 바이너리 타겟 이란 src/main.rs 파일 혹은 따로 바이너리로 지정된 파일을 가진 크레이트가 생성해 낸 실행 가능한 프로그램을 말하는 것으로, 혼자서 실행될 수 없지만 다른 프로그램에 포함되기에 적합한 라이브러리 타겟과는 반대되는 의미입니다. 보통은 크레이트의 README 파일에 해당 크레이트가 라이브러리인지, 바이너리 타겟을 갖는지, 혹은 둘 다인지에 대한 정보가 담겨있습니다. cargo install을 이용해 설치된 모든 바이너리는 설치 루트의 bin 디렉터리에 저장됩니다. 만약 여러분이 rustup.rs 를 이용해 러스트를 설치했고 별도의 설정값 수정이 없었다면, 이 디렉터리는 $HOME/.cargo/bin 일 것입니다. cargo install로 설치한 프로그램을 실행하려면 $PATH 환경변수에 해당 디렉터리가 등록되어 있는지 확인하세요. 예를 들면, 12장에서 파일 검색용 grep 도구의 러스트 구현체인 ripgrep이라는 게 있다고 언급했었지요. ripgrep을 설치하려면 다음과 같이 하면 됩니다: $ cargo install ripgrep Updating crates.io index Downloaded ripgrep v13.0.0 Downloaded 1 crate (243.3 KB) in 0.88s Installing ripgrep v13.0.0\n--snip-- Compiling ripgrep v13.0.0 Finished release [optimized + debuginfo] target(s) in 3m 10s Installing ~/.cargo/bin/rg Installed package `ripgrep v13.0.0` (executable `rg`) 출력의 마지막 두 줄은 설치된 바이너리의 경로와 이름을 보여주는데, ripgrep의 경우에는 rg로군요. 방금 전에 언급했듯 여러분의 $PATH 환경변수에 설치된 디렉터리가 등록되어 있다면 명령창에서 rg --help를 실행할 수 있고, 파일을 찾을 때 더 빠르고 러스트다운 도구를 사용할 수 있습니다!","breadcrumbs":"카고와 Crates.io 더 알아보기 » cargo install로 Crates.io에 있는 바이너리 설치하기 » cargo install로 Crates.io에 있는 바이너리 설치하기","id":"265","title":"cargo install로 Crates.io에 있는 바이너리 설치하기"},"266":{"body":"카고는 직접 카고를 수정하지 않고도 새로운 보조 명령어로 확장할 수 있게끔 설계되어 있습니다. 만약 $PATH에 있는 어떤 바이너리의 이름이 cargo-something라면, cargo something이라는 명령어로 마치 카고의 보조 명령어인 것처럼 실행할 수 있습니다. 이와 같은 커스텀 명령어들은 cargo --list를 실행할 때의 목록에도 포함됩니다. cargo install을 이용해 확장 모듈을 설치한 다음 카고의 기본 제공 도구처럼 이용할 수 있다는 점은 카고 설계에서 무척 편리한 장점입니다!","breadcrumbs":"카고와 Crates.io 더 알아보기 » 커스텀 명령어로 카고 확장하기 » 커스텀 명령어로 카고 확장하기","id":"266","title":"커스텀 명령어로 카고 확장하기"},"267":{"body":"카고와 crates.io 를 통해 코드를 공유하는 것은 러스트 생태계가 다양한 일에 유용하도록 만들어 주는 부분입니다. 러스트의 기본 라이브러리는 작고 고정되어 있지만, 크레이트들은 쉽게 공유될 수 있고, 쉽게 사용될 수 있으며 러스트 언어 자체보다 훨씬 빠른 속도로 발전합니다. 여러분에게 유용한 코드가 있다면 주저 말고 crates.io 에 공유하세요; 분명 다른 누군가에게도 도움이 될 테니까요!","breadcrumbs":"카고와 Crates.io 더 알아보기 » 커스텀 명령어로 카고 확장하기 » 정리","id":"267","title":"정리"},"268":{"body":"포인터 (pointer) 는 메모리의 주솟값을 담고 있는 변수에 대한 일반적인 개념입니다. 이 주솟값은 어떤 다른 데이터를 참조합니다. 혹은 바꿔 말하면, ‘가리킵니다.’ 러스트에서 가장 흔한 종류의 포인터는 4장에서 배웠던 참조자입니다. 참조자는 & 심볼로 표시하고 이들이 가리키고 있는 값을 빌려옵니다. 이들은 값을 참조하는 것 외에 다른 어떤 특별한 능력은 없으며, 오버헤드도 없습니다. 한편, 스마트 포인터 (smart pointer) 는 포인터처럼 작동할 뿐만 아니라 추가적인 메타데이터와 능력들도 가지고 있는 데이터 구조입니다. 스마트 포인터의 개념은 러스트 고유의 것이 아닙니다: 스마트 포인터는 C++로부터 유래되었고 다른 언어들에도 존재합니다. 러스트의 표준 라이브러리에는 다양한 종류의 스마트 포인터들이 정의되어 있는데 이를 통해 참조자가 제공하는 것 이상의 기능을 제공합니다. 일반적인 개념을 탐구하기 위하여 몇 가지 스마트 포인터 예제를 살펴보려고 하는데, 그중에는 참조 카운팅 (reference counting) 스마트 포인터 타입이 있습니다. 이 포인터는 소유자의 개수를 계속 추적하고, 더 이상 소유자가 없으면 데이터를 정리하는 방식으로, 어떤 데이터에 대한 여러 소유자를 만들 수 있게 해 줍니다. 소유권과 대여의 개념을 가지고 있는 러스트에서, 참조자와 스마트 포인터 사이에는 추가적인 차이점이 있습니다: 참조자가 데이터를 빌리기만 하는 반면, 대부분의 경우 스마트 포인터는 가리킨 데이터를 소유합니다. 우리는 이미 이 책에서 8장의 String과 Vec와 같은 몇 가지 스마트 포인터들을 마주쳤습니다. 비록 그때는 이것들을 스마트 포인터라고 부르지 않았지만요. 이 두 타입 모두 스마트 포인터로 치는데 그 이유는 이들이 어느 정도의 메모리를 소유하고 이를 다룰 수 있게 해 주기 때문입니다. 그들은 또한 메타데이터와 추가 능력 또는 보장성을 갖고 있습니다. 예를 들어 String은 자신의 용량을 메타데이터로 저장하고 자신의 데이터가 언제나 유효한 UTF-8 임을 보증하는 추가 능력도 가지고 있습니다. 스마트 포인터는 보통 구조체를 이용하여 구현되어 있습니다. 보통의 구조체와는 달리 스마트 포인터는 Deref와 Drop 트레이트를 구현합니다. Deref 트레이트는 스마트 포인터 구조체의 인스턴스가 참조자처럼 동작하도록 하여 참조자 혹은 스마트 포인터와 함께 작동하는 코드를 작성할 수 있도록 해줍니다. Drop 트레이트는 스마트 포인터의 인스턴스가 스코프 밖으로 벗어났을 때 실행되는 코드를 커스터마이징 가능하도록 해 줍니다. 이번 장에서는 이 두 개의 트레이트 모두를 다루고 이들이 왜 스마트 포인터에게 중요한지 보여줄 것입니다. 스마트 포인터 패턴이 러스트에서 자주 사용되는 일반적인 디자인 패턴임을 생각하면, 존재하는 모든 스마트 포인터를 이번 장에서 다루지는 못할 것입니다. 많은 라이브러리가 자신만의 스마트 포인터를 가지고 있고, 심지어 여러분도 자신만의 것을 작성할 수 있습니다. 여기서는 표준 라이브러리에 있는 가장 일반적인 스마트 포인터들을 다루겠습니다: 값을 힙에 할당하기 위한 Box 복수 소유권을 가능하게 하는 참조 카운팅 타입인 Rc 대여 규칙을 컴파일 타임 대신 런타임에 강제하는 타입인, RefCell를 통해 접근 가능한 Ref와 RefMut 추가로 불변 타입이 내부 값을 변경하기 위하여 API를 노출하는 내부 가변성 (interior mutability) 패턴에 대해서 다루겠습니다. 또한 순환 참조 (reference cycles) 가 어떤 식으로 메모리가 새어나가게 할 수 있으며, 이를 어떻게 방지하는지에 대해서도 논의해 보겠습니다. 함께 뛰어들어 볼까요!","breadcrumbs":"스마트 포인터 » 스마트 포인터","id":"268","title":"스마트 포인터"},"269":{"body":"가장 직관적인 스마트 포인터는 박스 (box) 인데, Box로 쓰이는 타입입니다. 박스는 스택이 아니라 힙에 데이터를 저장할 수 있도록 해줍니다. 스택에 남는 것은 힙 데이터를 가리키는 포인터입니다. 스택과 힙의 차이에 대해 다시 보고 싶다면 4장을 참조하세요. 박스는 스택 대신 힙에 데이터를 저장한다는 점 외에는, 성능 측면에서의 오버헤드가 없습니다. 하지만 여러 추가 기능도 없습니다. 박스는 아래와 같은 상황에서 가장 자주 쓰이게 됩니다: 컴파일 타임에는 크기를 알 수 없는 타입이 있는데, 정확한 크기를 요구하는 컨텍스트 내에서 그 타입의 값을 사용하고 싶을 때 커다란 데이터를 가지고 있고 소유권을 옮기고 싶지만 그렇게 했을 때 데이터가 복사되지 않을 것을 보장하고 싶을 때 어떤 값을 소유하고 이 값의 구체화된 타입보다는 특정 트레이트를 구현한 타입이라는 점만 신경 쓰고 싶을 때 첫 번째 상황은 ‘박스로 재귀적 타입 가능하게 하기’ 절에서 보여주겠습니다. 두 번째 경우, 방대한 양의 데이터의 소유권 옮기기는 긴 시간이 소요될 수 있는데 이는 그 데이터가 스택 상에서 복사되기 때문입니다. 이러한 상황에서 성능을 향상시킬 목적으로 박스 안의 힙에 그 방대한 양의 데이터를 저장할 수 있습니다. 그러면 작은 양의 포인터 데이터만 스택 상에서 복사되고, 이 포인터가 참조하는 데이터는 힙의 한 곳에 머물게 됩니다. 세 번째 경우는 트레이트 객체 (trait object) 라고 알려진 것이고, 17장의 ‘트레이트 객체를 사용하여 다른 타입의 값 허용하기’ 절 전체가 이 주제만으로 채워져 있습니다. 그러니 여기서 배운 것을 17장에서 다시 적용하게 될 것입니다!","breadcrumbs":"스마트 포인터 » Box를 사용하여 힙에 있는 데이터 가리키기 » Box를 사용하여 힙에 있는 데이터 가리키기","id":"269","title":"Box를 사용하여 힙에 있는 데이터 가리키기"},"27":{"body":"이제 카고로 생성한 ‘Hello, world!’ 프로그램은 빌드하고 실행했을 때 어떤 점이 다른지 확인해 봅시다! hello_cargo 디렉터리에서 다음 명령어를 이용해 프로젝트를 빌드해주세요: $ cargo build Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs 이 명령어는 현재 디렉터리가 아닌 target/debug/hello_cargo (Windows에서는 target\\debug\\hello_cargo.exe )에 실행 파일을 생성합니다. 기본 빌드가 디버그 빌드기 때문에, 카고는 debug 라는 디렉터리에 바이너리를 생성합니다. 실행 파일은 다음 명령어로 실행할 수 있습니다: $ ./target/debug/hello_cargo # or .\\target\\debug\\hello_cargo.exe on Windows\nHello, world! 터미널에 Hello, world!가 출력되면 제대로 진행된 겁니다. 처음 cargo build 명령어를 실행하면 최상위 디렉터리에 Cargo.lock 파일이 생성될 텐데, 이 파일은 프로젝트에서 사용하는 의존성의 정확한 버전을 자동으로 기록해 두는 파일이니 여러분이 직접 수정할 필요는 없습니다. 물론 이번 프로젝트는 의존성을 갖지 않으므로 현재는 파일에 특별한 내용이 없습니다. 방금은 cargo build로 빌드한 후 ./target/debug/hello_cargo 명령어로 실행했지만, 컴파일과 실행을 한 번에 진행하는 cargo run 명령어도 있습니다: $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/hello_cargo`\nHello, world! cargo run을 사용하면 cargo build 실행 후 바이너리 경로를 입력해서 실행하는 것보다 편리하므로, 대부분의 개발자들이 cargo run을 이용합니다. 출력 내용에 hello_cargo를 컴파일 중이라는 내용이 없는 걸 눈치채셨나요? 이는 카고가 파일 변경 사항이 없음을 알아채고 기존 바이너리를 그대로 실행했기 때문입니다. 소스 코드를 수정한 뒤 명령어를 다시 실행해 보면 다음과 같이 프로젝트를 다시 빌드한 후에 바이너리를 실행함을 알 수 있습니다. $ cargo run Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.33 secs Running `target/debug/hello_cargo`\nHello, world! 카고에는 cargo check라는 명령어도 있는데, 이는 실행 파일은 생성하지 않고 작성한 소스가 문제없이 컴파일되는지만 빠르게 확인하는 명령어입니다. $ cargo check Checking hello_cargo v0.1.0 (file:///projects/hello_cargo) Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs 실행 파일도 생성하지 않는 명령어가 왜 필요할까요? cargo check는 실행 파일을 생성하는 단계를 건너뛰기 때문에 cargo build보다 훨씬 빠릅니다. 코드를 작성하는 동안 여러분의 프로젝트가 컴파일되는지 지속적으로 검사하려면, cargo check 사용이 코드가 계속 컴파일되는지 확인하는 과정을 빠르게 해줄 것입니다! 러스타시안은 대부분 주기적으로 이 명령어를 실행해 코드에서 컴파일 문제가 발생하지 않는지 확인하고, 실행 파일이 필요할 경우에만 cargo build를 사용합니다. 여태까지 카고에 대해 배운 내용을 복습해 봅시다: cargo new로 새 프로젝트를 생성할 수 있습니다. cargo build 명령으로 프로젝트를 빌드할 수 있습니다. cargo run 명령어는 한 번에 프로젝트를 빌드하고 실행할 수 있습니다. cargo check 명령으로 바이너리를 생성하지 않고 프로젝트의 에러를 체크할 수 있습니다. 빌드로 만들어진 파일은 작성한 소스 코드와 뒤섞이지 않도록 target/debug 디렉터리에 저장됩니다. 운영체제에 상관없이 같은 명령어를 사용한다는 것도 카고 사용으로 얻는 추가적인 장점입니다. 따라서 이 시점부터는 운영체제별로 명령어를 따로 알려드리지 않겠습니다.","breadcrumbs":"시작해봅시다 » 카고를 사용해봅시다 » 카고로 프로젝트를 빌드하고 실행하기","id":"27","title":"카고로 프로젝트를 빌드하고 실행하기"},"270":{"body":"Box에 대한 사용례를 논의하기 전에, 먼저 문법 및 Box 안에 저장된 값의 사용법을 다루겠습니다. 예제 15-1은 박스를 사용하여 힙에 i32 값을 저장하는 방법을 보여줍니다: 파일명: src/main.rs fn main() { let b = Box::new(5); println!(\"b = {}\", b);\n} 예제 15-1: 박스를 사용하여 i32 값을 힙에 저장하기 변수 b를 정의하여 5라는 값을 가리키는 Box 값을 갖도록 했는데, 여기서 5는 힙에 할당됩니다. 이 프로그램은 b = 5를 출력할 것입니다; 이 경우, 박스 안에 있는 데이터는 마치 이 데이터가 스택에 있는 것처럼 접근 가능합니다. b가 main의 끝에 도달하는 것처럼 어떤 박스가 스코프를 벗어날 때, 다른 어떤 소유된 값과 마찬가지로 할당은 해제될 것입니다. 할당 해제는 (스택에 저장된) 박스와 이것이 가리키고 있는 (힙에 저장된) 데이터 모두에게 일어납니다. 단일 값을 힙에 집어넣는 것은 그다지 유용하지는 않으므로, 이 같은 방식의 박스 사용은 자주 쓰이지 않을 것입니다. 단일 i32의 저장 공간은 기본적으로 스택이고, 이러한 값은 스택에 저장하는 것이 대부분의 경우에 더 적합합니다. 박스를 쓰지 않으면 허용되지 않을 타입을 박스로 정의하는 경우를 살펴봅시다.","breadcrumbs":"스마트 포인터 » Box를 사용하여 힙에 있는 데이터 가리키기 » Box을 사용하여 힙에 데이터 저장하기","id":"270","title":"Box을 사용하여 힙에 데이터 저장하기"},"271":{"body":"재귀적 타입 (recursive type) 의 값은 자신 안에 동일한 타입의 또 다른 값을 담을 수 있습니다. 러스트는 컴파일 타임에 어떤 타입이 얼마만큼의 공간을 차지하는지 알아야 하기 때문에 재귀적 타입은 문제를 일으킵니다. 재귀적 타입의 값 중첩은 이론적으로 무한히 계속될 수 있으므로, 러스트는 이 값에 얼마만큼의 공간이 필요한지 알 수 없습니다. 박스는 알려진 크기를 갖고 있으므로, 재귀적 타입의 정의에 박스를 집어넣어서 재귀적 타입을 가능하게 할 수 있습니다. 재귀적 타입의 예제로, 콘스 리스트 (cons list) 를 탐구해 봅시다. 이것은 함수형 프로그램 언어에서 흔히 발견되는 데이터 타입입니다. 여기서 정의할 콘스 리스트 타입은 재귀를 제외하면 직관적입니다; 따라서 여기서 작업할 예제의 개념은 재귀적 타입을 포함하는 더 복잡한 경우에 직면하더라도 유용할 것입니다. 콘스 리스트에 대한 더 많은 정보 콘스 리스트 는 Lisp 프로그래밍 언어 및 그의 파생 언어들로부터 유래된 데이터 구조로서 중첩된 쌍으로 구성되며, 연결 리스트 (linked list) 의 Lisp 버전입니다. 이 이름은 Lisp의 (‘생성 함수 (construct function)’의 줄임말인) cons 함수에서 유래되었는데, 이 함수는 두 개의 인수로부터 새로운 쌍을 생성합니다. cons에 어떤 값과 다른 쌍으로 구성된 쌍을 넣어 호출함으로써 재귀적인 쌍으로 이루어진 콘스 리스트를 구성할 수 있습니다. 예를 들어, 다음은 1, 2, 3 리스트를 담고 있는 콘스 리스트를 각각의 쌍을 괄호로 묶어서 표현한 의사 코드입니다: (1, (2, (3, Nil))) 콘스 리스트의 각 아이템은 두 개의 요소를 담고 있습니다: 현재 아이템의 값과 다음 아이템이지요. 리스트의 마지막 아이템은 다음 아이템 없이 Nil 이라 불리는 값을 담고 있습니다. 콘스 리스트는 cons 함수를 재귀적으로 호출함으로써 만들어집니다. 재귀의 기본 케이스를 의미하는 표준 이름이 바로 Nil입니다. 6장의 ‘널 (null)’ 혹은 ‘닐 (nil)’ 개념과 동일하지 않다는 점을 주의하세요. 이들은 값이 유효하지 않거나 없음을 말합니다. 콘스 리스트는 러스트에서 흔히 사용되는 데이터 구조는 아닙니다. 러스트에서 아이템 리스트를 쓰는 대부분의 경우에는 Vec가 더 나은 선택입니다. 그와는 다른, 더 복잡한 재귀적 데이터 타입들은 다양한 상황에서 유용 하기는 하지만, 이 장에서는 콘스 리스트로 시작하여 박스가 어떤 식으로 별로 주의를 기울이지 않고도 재귀적 데이터 타입을 정의하도록 하는지 탐구하겠습니다. 예제 15-2는 콘스 리스트를 위한 열거형 정의를 담고 있습니다. List 타입이 알려진 크기를 가지고 있지 않고 있기 때문에 이 코드는 아직 컴파일이 안된다는 점을 유의하세요. 여기서 보여주려는 것이 바로 그 점입니다. 파일명: src/main.rs enum List { Cons(i32, List), Nil,\n}\n# # fn main() {} 예제 15-2: i32 값의 콘스 리스트 데이터 구조를 표현하는 열거형 정의에 대한 첫 번째 시도 Note: 이 예제의 목적을 위해 오직 i32 값만 담는 콘스 리스트를 구현하는 중입니다. 10장에서 논의했던 것처럼, 제네릭을 이용하면 임의의 타입 값을 저장할 수 있는 콘스 리스트 타입을 정의할 수도 있습니다. List 타입을 이용하여 리스트 1, 2, 3을 저장하면 예제 15-3의 코드처럼 보일 것입니다: 파일명: src/main.rs # enum List {\n# Cons(i32, List),\n# Nil,\n# }\n# use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Cons(2, Cons(3, Nil)));\n} 예제 15-3: List 열거형을 이용하여 리스트 1, 2, 3 저장하기 첫 번째 Cons 값은 1과 또 다른 List 값을 갖습니다. 이 List 값은 2와 또 다른 List 값을 갖는 Cons 값입니다. 그 안의 List 값에는 3과 List 값을 갖는 Cons가 하나 더 있는데, 여기서 마지막의 List는 Nil로써, 리스트의 끝을 알리는 비재귀적인 배리언트입니다. 예제 15-3 코드의 컴파일을 시도하면, 예제 15-4과 같은 에러를 얻습니다: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list)\nerror[E0072]: recursive type `List` has infinite size --> src/main.rs:1:1 |\n1 | enum List { | ^^^^^^^^^\n2 | Cons(i32, List), | ---- recursive without indirection |\nhelp: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle |\n2 | Cons(i32, Box), | ++++ + For more information about this error, try `rustc --explain E0072`.\nerror: could not compile `cons-list` due to previous error 예제 15-4: 재귀적 열거형을 정의하는 시도를 했을 때 얻게 되는 에러 이 에러는 이 타입이 ‘무한한 크기다 (infinite size)’라고 말해줍니다. 그 원인은 재귀적인 배리언트를 이용하여 List를 정의했기 때문입니다: 즉 이것은 자신의 또 다른 값을 직접 갖습니다. 결과적으로, 러스트는 List 값을 저장하는 데 필요한 크기가 얼마나 되는지 알아낼 수 없습니다. 왜 이런 에러가 생기는지 쪼개서 봅시다. 먼저, 러스트가 비재귀적인 타입의 값을 저장하는 데 필요한 용량이 얼마나 되는지 결정하는 방법을 살펴봅시다. 비재귀적 타입의 크기 계산하기 6장에서 열거형 정의에 대해 논의할 때 예제 6-2에서 정의했던 Message 열거형을 상기해 봅시다: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n}\n# # fn main() {} Message 값을 할당하기 위해 필요한 공간의 양을 결정하기 위해서, 러스트는 각 배리언트들의 내부를 보면서 어떤 배리언트가 가장 많은 공간을 필요로 하는지를 알아봅니다. 러스트는 Message::Quit가 어떠한 공간도 필요 없음을, Message::Move는 두 개의 i32 값을 저장하기에 충분한 공간이 필요함을 알게 되고, 그런 식으로 진행됩니다. 하나의 배리언트만 사용될 것이기 때문에, Message 값이 필요로 하는 가장 큰 공간은 배리언트 중에서 가장 큰 것을 저장하는 데 필요한 공간입니다. 러스트가 예제 15-2의 List 열거형과 같은 재귀적 타입이 필요로 하는 공간을 결정하는 시도를 할 때는 어떤 일이 일어날지 위의 경우와 대조해 보세요. 컴파일러는 Cons 배리언트를 살펴보기 시작하는데, 이는 i32 타입의 값과 List 타입의 값을 갖습니다. 그러므로 Cons는 i32의 크기에 List 크기를 더한 만큼의 공간을 필요로 합니다. List 타입이 얼마나 많은 메모리를 차지하는지 알아내기 위해서, 컴파일러는 그것의 배리언트들을 살펴보는데, 이는 Cons 배리언트로 시작됩니다. Cons 배리언트는 i32 타입의 값과 List 타입의 값을 갖고, 이 과정은 Figure 15-1에서 보는 바와 같이 무한히 계속됩니다. 그림 15-1: 무한한 Cons 배리언트를 가지고 있는 무한한 List Box를 이용하여 알려진 크기를 가진 재귀적 타입 만들기 러스트는 재귀적으로 정의된 타입을 위하여 얼마만큼의 공간을 할당하는지 알아낼 수 없으므로, 컴파일러는 에러와 함께 아래와 같은 유용한 제안을 제공합니다: help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List` representable |\n2 | Cons(i32, Box), | ++++ + 이 제안에서 ‘간접 (indirection)’이란, 값을 직접 저장하는 대신 데이터 구조를 바꿔 값을 가리키는 포인터를 저장하는 식으로 값을 간접적으로 저장해야 함을 의미합니다. Box가 포인터이기 때문에, 러스트는 언제나 Box가 필요로 하는 공간이 얼마인지 알고 있습니다: 포인터의 크기는 그것이 가리키고 있는 데이터의 양에 따라 변경되지 않습니다. 이는 Cons 배리언트 내에 또 다른 List 값을 직접 넣는 대신 Box를 넣을 수 있음을 의미합니다. Box는 Cons 배리언트 안이 아니라 힙에 있을 다음의 List 값을 가리킬 것입니다. 개념적으로는 여전히 다른 리스트들을 담은 리스트로 만들어진 리스트지만, 이 구현은 이제 아이템을 다른 것 안쪽에 넣는 것이 아니라 그다음 위치에 놓는 형태에 더 가깝습니다. 예제 15-2의 List 열거형의 정의와 예제 15-3의 List 사용법을 예제 15-5의 코드로 바꿀 수 있고, 이것은 컴파일될 것입니다: 파일명: src/main.rs enum List { Cons(i32, Box), Nil,\n} use crate::List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));\n} 예제 15-5: 알려진 크기를 갖도록 하기 위해 Box를 이용한 List 정의 Cons 배리언트에는 i32와 박스의 포인터 데이터를 저장할 공간을 더한 크기가 필요합니다. Nil 배리언트는 아무런 값도 저장하지 않으므로, Cons 배리언트에 비해 공간을 덜 필요로 합니다. 이제는 어떠한 List 값이라도 i32의 크기와 박스의 포인터 데이터 크기를 더한 값만큼만 차지한다는 것을 알게 되었습니다. 박스를 이용하는 것으로 무한한 재귀적 연결을 깨뜨렸고, 따라서 컴파일러는 List 값을 저장하는 데 필요한 크기를 알아낼 수 있습니다. 그림 15-2는 Cons 배리언트가 이제 어떻게 생겼는지를 보여주고 있습니다: 그림 15-2: Cons가 Box를 들고 있기 때문에 무한한 크기가 아니게 된 List 박스는 그저 간접 및 힙 할당만을 제공할 뿐입니다; 이들은 다른 어떤 특별한 능력들, 다른 스마트 포인터 타입들에서 보게 될 능력 같은 것들은 없습니다. 또한 이들은 이러한 특별한 능력들이 초래하는 성능적인 오버헤드도 가지고 있지 않으므로, 필요한 기능이 간접 하나인 콘스 리스트와 같은 경우에는 유용할 수 있습니다. 17장에서 박스에 대한 더 많은 사용례도 살펴볼 예정입니다. Box 타입은 Deref 트레이트를 구현하고 있기 때문에 스마트 포인터이며, 이는 Box 값이 참조자와 같이 취급되도록 허용해 줍니다. Box 값이 스코프 밖으로 벗어날 때, 박스가 가리키고 있는 힙 데이터도 마찬가지로 정리되는데 이는 Drop 트레이트의 구현 때문에 그렇습니다. 이 두 트레이트가 이 장의 나머지에서 다루고자 하는 다른 스마트 포인터 타입에 의해 제공되는 기능들보다도 심지어 더 중요할 것입니다. 이 두 트레이트에 대하여 더 자세히 탐구해 봅시다.","breadcrumbs":"스마트 포인터 » Box를 사용하여 힙에 있는 데이터 가리키기 » 박스로 재귀적 타입 가능하게 하기","id":"271","title":"박스로 재귀적 타입 가능하게 하기"},"272":{"body":"Deref 트레이트를 구현하면 역참조 연산자 (dereference operator) * 동작의 커스터마이징을 가능하게 해 줍니다. (곱하기 혹은 글롭 연산자와 헷갈리지 마세요.) 스마트 포인터가 보통의 참조자처럼 취급될 수 있도록 Deref를 구현함으로써, 참조자에 작동하도록 작성된 코드가 스마트 포인터에도 사용되게 할 수 있습니다. 먼저 역참조 연산자가 보통의 참조자에 대해 동작하는 방식을 살펴보고, 그런 다음 Box처럼 동작하는 커스텀 타입의 정의를 시도해 보면서, 역참조 연산자가 새로 정의한 타입에서는 참조자처럼 동작하지 않는 이유를 알아보겠습니다. Deref 트레이트를 구현하는 것이 스마트 포인터가 참조자와 유사한 방식으로 동작하도록 하는 원리를 탐구해 볼 것입니다. 그리고서 러스트의 역참조 강제 (deref corecion) 기능과 이 기능이 참조자 혹은 스마트 포인터와 함께 동작하도록 하는 방식을 살펴보겠습니다. Note: 이제부터 만들려고 하는 MyBox 타입과 실제 Box 간에는 한 가지 큰 차이점이 있습니다: 우리 버전은 데이터를 힙에 저장하지 않습니다. 이 예제는 Deref에 초점을 맞추고 있으므로, 데이터가 어디에 저장되는가 하는 것은 포인터 같은 동작에 비해 덜 중요합니다.","breadcrumbs":"스마트 포인터 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기","id":"272","title":"Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기"},"273":{"body":"보통의 참조자는 포인터의 한 종류이고, 포인터에 대해 생각하는 방법 하나는 어딘가에 저장된 값을 가리키는 화살표처럼 생각하는 것입니다. 예제 15-6에서는 i32 값의 참조자를 생성하고는 역참조 연산자를 사용하여 참조자를 따라가서 값을 얻어냅니다: 파일명: src/main.rs fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y);\n} 예제 15-6: 역참조 연산자를 사용하여 i32 값에 대한 참조자 따라가기 변수 x는 i32 값 5를 가지고 있습니다. y에는 x의 참조자를 설정했습니다. x는 5와 같음을 단언할 수 있습니다. 하지만 만일 y 안의 값에 대하여 단언하고 싶다면, *y를 사용하여 참조자를 따라가서 이 참조자가 가리키고 있는 값을 얻어 내어 (그래서 역참조 라고 합니다) 컴파일러가 실제 값을 비교할 수 있도록 해야 합니다. 일단 y를 역참조하면, 5와 비교 가능한 y가 가리키고 있는 정숫값에 접근하게 됩니다. 대신 assert_eq!(5, y);이라고 작성을 시도했다면, 다음과 같은 컴파일 에러를 얻게 됩니다: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example)\nerror[E0277]: can't compare `{integer}` with `&{integer}` --> src/main.rs:6:5 |\n6 | assert_eq!(5, y); | ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}` | = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}` = help: the following other types implement trait `PartialEq`: f32 f64 i128 i16 i32 i64 i8 isize and 6 others = note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `deref-example` due to previous error 숫자와 숫자에 대한 참조자를 비교하는 것은 이 둘이 서로 다른 타입이므로 허용되지 않습니다. *를 사용하여 해당 참조자를 따라가서 그것이 가리키고 있는 값을 얻어내야 합니다.","breadcrumbs":"스마트 포인터 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 » 포인터를 따라가서 값 얻기","id":"273","title":"포인터를 따라가서 값 얻기"},"274":{"body":"예제 15-6의 코드는 참조자 대신 Box를 사용하여 다시 작성할 수 있습니다; 예제 15-7의 Box에 사용된 역참조 연산자는 예제 15-6의 참조자에 사용된 역참조 연산자와 동일한 방식으로 기능합니다: 파일명: src/main.rs fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y);\n} 예제 15-7: Box에 역참조 연산자 사용하기 여기서 예제 15-7과 예제 15-6 간의 주요 차이점은 y에 x의 값을 가리키는 참조자가 아닌 x의 복제된 값을 가리키는 Box의 인스턴스를 설정했다는 것입니다. 마지막 단언문에서 y가 참조자일 때 했던 것과 동일한 방식으로 박스 포인터 앞에 역참조 연산자를 사용할 수 있습니다. 다음으로, 자체 박스 타입을 정의함으로써 Box가 역참조 연산자의 사용을 가능하게끔 해주는 특별함이 무엇인지 탐구해 보겠습니다.","breadcrumbs":"스마트 포인터 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 » Box를 참조자처럼 사용하기","id":"274","title":"Box를 참조자처럼 사용하기"},"275":{"body":"표준 라이브러리가 제공하는 Box와 유사한 스마트 포인터를 만들어 보면서 스마트 포인터는 어떻게 기본적으로 참조자와는 다르게 동작하는지 경험해 봅시다. 그다음 역참조 연산자의 사용 기능을 추가하는 방법을 살펴보겠습니다. Box 타입은 궁극적으로 하나의 요소를 가진 튜플 구조체로 정의되므로, 예제 15-8에서 MyBox 타입을 동일한 방식으로 정의했습니다. 또한 Box에 정의된 new 함수와 짝을 이루는 new 함수도 정의하겠습니다. 파일명: src/main.rs struct MyBox(T); impl MyBox { fn new(x: T) -> MyBox { MyBox(x) }\n}\n# # fn main() {} 예제 15-8: MyBox 타입 정의하기 MyBox라는 이름의 구조체를 정의하고 제네릭 매개변수 T를 선언했는데, 이는 모든 타입의 값을 가질 수 있도록 하고 싶기 때문입니다. MyBox 타입은 T 타입의 요소 하나를 가진 튜플 구조체입니다. MyBox::new 함수는 T 타입의 매개변수 하나를 받아서 그 값을 들고 있는 MyBox 인스턴스를 반환합니다. 예제 15-7의 main 함수를 예제 15-8에 추가하고 Box 대신 우리가 정의한 MyBox 타입을 사용하도록 고쳐봅시다. 러스트는 MyBox를 역참조하는 방법을 모르기 때문에 예제 15-9의 코드는 컴파일되지 않을 것입니다. 파일명: src/main.rs # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y);\n} 예제 15-9: 참조자와 Box에 사용되었던 방식 그대로 MyBox 사용 시도하기 아래는 그 결과 발생한 컴파일 에러입니다: $ cargo run Compiling deref-example v0.1.0 (file:///projects/deref-example)\nerror[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 |\n14 | assert_eq!(5, *y); | ^^ For more information about this error, try `rustc --explain E0614`.\nerror: could not compile `deref-example` due to previous error MyBox 타입은 역참조 될 수 없는데, 그 이유는 이 타입에 그런 기능을 구현한 적이 없기 때문입니다. * 연산자로 역참조를 할 수 있게 하려면 Deref 트레이트를 구현해야 합니다.","breadcrumbs":"스마트 포인터 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 » 자체 스마트 포인터 정의하기","id":"275","title":"자체 스마트 포인터 정의하기"},"276":{"body":"10장의 ‘특정 타입에 트레이트 구현하기’ 절에서 논의한 바와 같이, 어떤 트레이트를 구현하기 위해서는 그 트레이트가 요구하는 메서드에 대한 구현체를 제공해야 합니다. 표준 라이브러리가 제공하는 Deref 트레이트는 deref라는 이름의 메서드 하나를 구현하도록 요구하는데, 이 함수는 self를 빌려와서 내부 데이터의 참조자를 반환합니다. 예제 15-10은 MyBox의 정의에 덧붙여 Deref의 구현체를 담고 있습니다: 파일명: src/main.rs use std::ops::Deref; impl Deref for MyBox { type Target = T; fn deref(&self) -> &Self::Target { &self.0 }\n}\n# # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# # fn main() {\n# let x = 5;\n# let y = MyBox::new(x);\n# # assert_eq!(5, x);\n# assert_eq!(5, *y);\n# } 예제 15-10: MyBox에 대한 Deref 구현하기 type Target = T; 문법은 Deref 트레이트가 사용할 연관 타입 (associated type) 을 정의합니다. 연관 타입은 제네릭 매개변수를 선언하는 약간 다른 방식이지만, 지금은 여기에 신경 쓰지 않아도 됩니다; 이에 대해서는 19장에서 더 자세히 다룰 예정입니다. deref 메서드의 본문은 &self.0으로 채워졌으므로 deref는 * 연산자를 이용하여 접근하려는 값의 참조자를 반환합니다; 5장의 ‘명명된 필드 없는 튜플 구조체를 사용하여 다른 타입 만들기’ 절에서 다룬 것처럼 .0이 튜플 구조체의 첫 번째 값에 접근한다는 것을 상기하세요. 예제 15-9에서 MyBox 값에 대해 *을 호출하는 main 함수는 이제 컴파일되고 단언문은 통과됩니다! Deref 트레이트가 없으면 컴파일러는 오직 & 참조자들만 역참조할 수 있습니다. deref 메서드는 컴파일러가 Deref를 구현한 어떤 타입의 값에 대해 deref 메서드를 호출하여, 자신이 역참조하는 방법을 알고 있는 & 참조자를 가져올 수 있는 기능을 제공합니다. 예제 15-9의 *y에 들어서면 러스트 뒤편에서는 실제로 아래와 같은 코드가 동작합니다: *(y.deref()) 러스트는 * 연산자에 deref 메서드 호출과 보통의 역참조를 대입하므로 deref 메서드를 호출할 필요가 있는지 혹은 없는지에 대해서는 생각하지 않아도 됩니다. 러스트의 이 기능은 일반적인 참조자의 경우든 혹은 Deref를 구현한 타입의 경우든 간에 동일한 기능을 하는 코드를 작성하도록 해 줍니다. deref 메서드가 값의 참조자를 반환하고, *(y.deref())에서의 괄호 바깥의 일반 역참조가 여전히 필요한 이유는 소유권 시스템과 함께 작동시키기 위해서입니다. 만일 deref 메서드가 값의 참조자 대신 값을 직접 반환했다면, 그 값은 self 바깥으로 이동할 것입니다. 위의 경우 혹은 역참조 연산자를 사용하는 대부분의 경우에서는 MyBox 내부의 값에 대한 소유권을 얻으려는 것이 아닙니다. 코드에 *를 쓸 때마다 이 * 연산자가 deref 함수의 호출 후 *를 한 번만 호출하는 것으로 대치된다는 점을 주의하세요. * 연산자의 대입이 무한히 재귀적으로 실행되지 않기 때문에, 결국 i32 타입의 데이터를 얻게 되는데, 이는 예제 15-9의 assert_eq! 내의 5와 일치합니다.","breadcrumbs":"스마트 포인터 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 » Deref 트레이트를 구현하여 임의의 타입을 참조자처럼 다루기","id":"276","title":"Deref 트레이트를 구현하여 임의의 타입을 참조자처럼 다루기"},"277":{"body":"역참조 강제 (deref coercion) 는 Deref를 구현한 어떤 타입의 참조자를 다른 타입의 참조자로 바꿔줍니다. 예를 들어, 역참조 강제는 &String을 &str로 바꿔줄 수 있는데, 이는 String의 Deref 트레이트 구현이 그렇게 &str을 반환하도록 했기 때문입니다. 역참조 강제는 러스트가 함수와 메서드의 인수에 대해 수행해 주는 편의성 기능이고, Deref 트레이트를 구현한 타입에 대해서만 동작합니다. 이는 어떤 특정한 타입값에 대한 참조자를 함수 혹은 메서드의 인수로 전달하는데 이 함수나 메서드의 정의에는 그 매개변수 타입이 맞지 않을 때 자동으로 발생합니다. 일련의 deref 메서드 호출이 인수로 제공한 타입을 매개변수로서 필요한 타입으로 변경해 줍니다. 역참조 강제는 함수와 메서드 호출을 작성하는 프로그래머들이 &와 *를 사용하여 수많은 명시적인 참조 및 역참조를 추가할 필요가 없도록 하기 위해 도입되었습니다. 또한 역참조 강제 기능은 참조자나 스마트 포인터 둘 중 어느 경우라도 작동되는 코드를 더 많이 작성할 수 있도록 해 줍니다. 역참조 강제가 실제 작동하는 것을 보기 위해서, 예제 15-8에서 정의했던 MyBox와 예제 15-10에서 추가했던 Deref의 구현체를 이용해 봅시다. 예제 15-11은 문자열 슬라이스 매개변수를 갖는 함수의 정의를 보여줍니다: 파일명: src/main.rs fn hello(name: &str) { println!(\"Hello, {name}!\");\n}\n# # fn main() {} 예제 15-11: &str 타입의 name 매개변수를 갖는 hello 함수 hello 함수는 이를테면 hello(\"Rust\");와 같이 문자열 슬라이스를 인수로 호출될 수 있습니다. 예제 15-12에서 보는 바와 같이, 역참조 강제는 MyBox 타입 값에 대한 참조자로 hello의 호출을 가능하게 만들어 줍니다: 파일명: src/main.rs # use std::ops::Deref;\n# # impl Deref for MyBox {\n# type Target = T;\n# # fn deref(&self) -> &T {\n# &self.0\n# }\n# }\n# # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# # fn hello(name: &str) {\n# println!(\"Hello, {name}!\");\n# }\n# fn main() { let m = MyBox::new(String::from(\"Rust\")); hello(&m);\n} 예제 15-12: 역참조 강제에 의해 작동되는, MyBox 값에 대한 참조자로 hello 호출하기 여기서는 hello 함수에 &m 인수를 넣어 호출하고 있는데, 이것이 MyBox 값에 대한 참조자입니다. 예제 15-10에서 MyBox에 대한 Deref 트레이트를 구현했으므로, 러스트는 deref를 호출하여 &MyBox을 &String으로 바꿀 수 있습니다. Deref에 대한 API 문서에도 나와 있듯이, 표준 라이브러리에 구현되어 있는 String의 Deref가 문자열 슬라이스를 반환합니다. 러스트는 다시 한번 deref를 호출하여 &String을 &str로 바꾸는데, 이것이 hello 함수의 정의와 일치하게 됩니다. 만일 러스트에 역참조 강제가 구현되어 있지 않았다면, &MyBox 타입의 값으로 hello를 호출하기 위해서는 예제 15-12의 코드 대신 예제 15-13의 코드를 작성했어야 할 것입니다: 파일명: src/main.rs # use std::ops::Deref;\n# # impl Deref for MyBox {\n# type Target = T;\n# # fn deref(&self) -> &T {\n# &self.0\n# }\n# }\n# # struct MyBox(T);\n# # impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n# # fn hello(name: &str) {\n# println!(\"Hello, {name}!\");\n# }\n# fn main() { let m = MyBox::new(String::from(\"Rust\")); hello(&(*m)[..]);\n} 예제 15-13: 러스트에 역참조 강제가 없었을 경우 작성했어야 할 코드 (*m)은 MyBox을 String으로 역참조해 줍니다. 그런 다음 &과 [..]가 전체 문자열과 동일한 String의 문자열 슬라이스를 얻어와서 hello 시그니처와 일치되도록 합니다. 역참조 강제가 없는 코드는 이 모든 기호가 수반된 상태가 되어 읽기도, 쓰기도, 이해하기도 더 힘들어집니다. 역참조 강제는 러스트가 프로그래머 대신 이러한 변환을 자동으로 다룰 수 있도록 해 줍니다. 인수로 넣어진 타입에 대해 Deref 트레이트가 정의되어 있다면, 러스트는 해당 타입을 분석하고 Deref::deref를 필요한 만큼 사용하여 매개변수 타입과 일치하는 참조자를 얻을 것입니다. Deref::deref가 추가되어야 하는 횟수는 컴파일 타임에 분석되므로, 역참조 강제의 이점을 얻는 데에 관해서 어떠한 런타임 페널티도 없습니다!","breadcrumbs":"스마트 포인터 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 » 함수와 메서드를 이용한 암묵적 역참조 강제","id":"277","title":"함수와 메서드를 이용한 암묵적 역참조 강제"},"278":{"body":"Deref 트레이트를 사용하여 불변 참조자에 대한 *를 오버라이딩하는 방법과 비슷한 방식으로, DerefMut 트레이트를 사용하여 가변 참조자에 대한 * 연산자를 오버라이딩할 수 있습니다. 러스트는 다음의 세 가지 경우에 해당하는 타입과 트레이트 구현을 찾았을 때 역참조 강제를 수행합니다: T: Deref일 때 &T에서 &U로 T: DerefMut일 때 &mut T에서 &mut U로 T: Deref일 때 &mut T에서 &U로 처음 두 가지 경우는 두 번째가 가변성을 구현했다는 점을 제외하면 동일합니다. 첫 번째 경우는 어떤 &T가 있는데, T가 어떤 타입 U에 대한 Deref를 구현했다면, 명료하게 &U를 얻을 수 있음을 기술하고 있습니다. 두 번째 경우는 동일한 역참조 강제가 가변 참조자에 대해서도 발생함을 기술합니다. 세 번째 경우는 좀 더 까다로운데, 러스트는 가변 참조자를 불변 참조자로 강제할 수도 있습니다. 하지만 그 역은 불가능하며 , 불변 참조자는 가변 참조자로 결코 강제되지 않을 것입니다. 대여 규칙에 의거하여, 가변 참조자가 있을 경우에는 그 가변 참조자가 해당 데이터에 대한 유일한 참조자여야 합니다. (그렇지 않다면, 그 프로그램은 컴파일되지 않을 것입니다.) 가변 참조자를 불변 참조자로 변경하는 것은 결코 대여 규칙을 깨트리지 않을 것입니다. 불변 참조자를 가변 참조자로 변경하는 것은 초기 불변 참조자가 해당 데이터에 대한 단 하나의 불변 참조자여야 함을 요구할 것인데, 대여 규칙으로는 이를 보장해 줄 수 없습니다. 따라서, 러스트는 불변 참조자의 가변 참조자로의 변경 가능성을 가정할 수 없습니다.","breadcrumbs":"스마트 포인터 » Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 » 역참조 강제가 가변성과 상호작용하는 법","id":"278","title":"역참조 강제가 가변성과 상호작용하는 법"},"279":{"body":"스마트 포인터 패턴에서 중요한 트레이트 그 두 번째는 Drop인데, 이는 어떤 값이 스코프 밖으로 벗어나려고 할 때 무슨 일을 할지 커스터마이징하게끔 해줍니다. 어떠한 타입이든 Drop 트레이트를 구현할 수 있고, 이 코드가 파일이나 네트워크 연결 같은 자원 해제에 사용되게 할 수 있습니다. 스마트 포인터에 대한 맥락에서 Drop을 소개하는 이유는 Drop 트레이트의 기능이 스마트 포인터를 구현할 때 거의 항상 이용되기 때문입니다. 예를 들어 Box가 버려질 때는 이 박스가 가리키고 있는 힙 공간의 할당을 해제할 것입니다. 몇몇 언어들에서는 어떤 타입의 인스턴스 사용을 끝낼 때마다 프로그래머가 직접 메모리 혹은 자원을 해제하는 코드를 호출해 줘야 합니다. 그 예에는 파일 핸들, 소켓, 또는 락이 포함됩니다. 해제를 잊어버리면 시스템은 과부하에 걸리고 멈출 수도 있습니다. 러스트에서는 값이 스코프 밖으로 벗어날 때마다 실행되는 특정 코드를 지정할 수 있고, 컴파일러가 이 코드를 자동으로 삽입해 줄 것입니다. 결과적으로, 프로그램 내에서 특정 타입의 인스턴스 사용이 끝나는 지점마다 메모리 정리 코드를 집어넣는 것에 관한 걱정하지 않아도 됩니다. 여전히 자원 누수는 발생하지 않을 테니까요! Drop 트레이트를 구현하여 어떤 값이 스코프 밖으로 벗어났을 때 실행되는 코드를 지정합니다. Drop 트레이트는 drop이라는 이름의 메서드 하나를 구현해야 하는데 이 메서드는 self에 대한 가변 참조자를 매개변수로 갖습니다. 러스트가 언제 drop을 호출하는지 알아보기 위해서, 지금은 println! 구문을 써서 drop을 구현해 봅시다. 예제 15-14는 러스트가 drop 함수를 호출하는 시점을 보여주기 위해서, 인스턴스가 스코프 밖으로 벗어났을 때 Dropping CustomSmartPointer!를 출력하는 커스텀 기능만을 갖춘 CustomSmartPointer 구조체를 보여줍니다. 파일명: src/main.rs struct CustomSmartPointer { data: String,\n} impl Drop for CustomSmartPointer { fn drop(&mut self) { println!(\"Dropping CustomSmartPointer with data `{}`!\", self.data); }\n} fn main() { let c = CustomSmartPointer { data: String::from(\"my stuff\"), }; let d = CustomSmartPointer { data: String::from(\"other stuff\"), }; println!(\"CustomSmartPointers created.\");\n} 예제 15-14: 메모리 정리 코드를 집어넣게 될 Drop 트레이트를 구현한 CustomSmartPointer 구조체 Drop 트레이트는 프렐루드에 포함되어 있으므로, 이를 스코프로 가져올 필요는 없습니다. CustomSmartPointer에는 Drop 트레이트가 구현되어 여기에 println!을 호출하는 drop 메서드 구현체를 제공하였습니다. drop 함수의 본문 부분에는 해당 타입의 인스턴스가 스코프 밖으로 벗어났을 때 실행시키고 싶은 어떠한 로직이라도 집어넣을 수 있습니다. 여기서는 러스트가 drop을 호출하게 될 때를 보여주기 위해서 어떤 텍스트를 출력하는 중입니다. main에서는 두 개의 CustomSmartPointer 인스턴스를 만든 다음 CustomSmartPointers created.를 출력합니다. main의 끝부분에서, CustomSmartPointer 인스턴스들은 스코프 밖으로 벗어날 것이고, 러스트는 drop 메서드에 집어넣은 코드를 호출할 것이고, 이는 마지막 메시지를 출력합니다. drop 메서드를 명시적으로 호출할 필요가 없다는 점을 주목하세요. 이 프로그램을 실행시키면 다음과 같은 출력을 보게 될 것입니다: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.60s Running `target/debug/drop-example`\nCustomSmartPointers created.\nDropping CustomSmartPointer with data `other stuff`!\nDropping CustomSmartPointer with data `my stuff`! 러스트는 인스턴스가 스코프 밖으로 벗어났을 때 drop을 호출했고, 이것이 지정해 두었던 코드를 실행시켰습니다. 변수들은 만들어진 순서의 역순으로 버려지므로, d가 c보다 먼저 버려집니다. 이 예제의 목적은 여러분에게 drop 메서드가 어떻게 동작하는지에 대한 시각적인 가이드를 제공하는 것입니다; 보통은 메시지 출력이 아니라 여러분의 타입에 대해 실행해야 하는 메모리 정리 코드를 지정하게 될 것입니다.","breadcrumbs":"스마트 포인터 » Drop 트레이트로 메모리 정리 코드 실행하기 » Drop 트레이트로 메모리 정리 코드 실행하기","id":"279","title":"Drop 트레이트로 메모리 정리 코드 실행하기"},"28":{"body":"프로젝트를 완성해서 배포(릴리즈)할 준비가 끝났다면, cargo build --release 명령어를 사용해 릴리즈 빌드를 생성할 수 있습니다. 일반 빌드와 차이점은 target/debug 가 아닌 target/release 에 실행 파일이 생성된다는 점, 그리고 컴파일 시 최적화를 진행하여 컴파일이 오래 걸리는 대신 러스트 코드가 더 빠르게 작동하는 점입니다. 릴리즈 빌드가 더 빠르게 작동한다면, 왜 일반 빌드시에는 최적화를 진행하지 않을까요? 이에 대한 해답은 빌드가 두 종류로 나뉘게 된 이유이기도 한데, 개발 중에는 빌드가 잦으며 작업의 흐름을 끊지 않기 위해 빌드 속도 또한 빠를수록 좋지만, 배포용 프로그램은 잦은 빌드가 필요 없으며 빌드 속도보단 프로그램의 작동 속도가 더 중요하기 때문입니다. 이와 같은 이유로, 작성한 코드 작동 속도를 벤치마킹할 시에는 릴리즈 빌드를 기준으로 해야 한다는 것도 알아두시기 바랍니다.","breadcrumbs":"시작해봅시다 » 카고를 사용해봅시다 » 릴리즈 빌드 생성하기","id":"28","title":"릴리즈 빌드 생성하기"},"280":{"body":"불행하게도 자동적인 drop 기능을 비활성화하는 일은 직관적이지 않습니다. drop 비활성화는 보통 필요가 없습니다; Drop 트레이트의 요점은 이것이 자동으로 이루어진다는 것이니까요. 하지만 가끔은 어떤 값을 일찍 정리하고 싶을 때도 있습니다. 한 가지 예는 락을 관리하는 스마트 포인터를 이용할 때입니다: 강제로 drop 메서드를 실행하여 락을 해제해서 같은 스코프의 다른 코드에서 해당 락을 얻도록 하고 싶을 수도 있지요. 러스트는 수동으로 Drop 트레이트의 drop 메서드를 호출하게 해주지는 않는 대신, 표준 라이브러리가 제공하는 std::mem::drop 함수를 호출하여 스코프가 끝나기 전에 강제로 값을 버리도록 할 수 있습니다. 예제 15-14의 main 함수를 예제 15-15처럼 수정하여 Drop 트레이트의 drop 메서드를 수동으로 호출하려고 하면 컴파일 에러가 납니다: 파일명: src/main.rs # struct CustomSmartPointer {\n# data: String,\n# }\n# # impl Drop for CustomSmartPointer {\n# fn drop(&mut self) {\n# println!(\"Dropping CustomSmartPointer with data `{}`!\", self.data);\n# }\n# }\n# fn main() { let c = CustomSmartPointer { data: String::from(\"some data\"), }; println!(\"CustomSmartPointer created.\"); c.drop(); println!(\"CustomSmartPointer dropped before the end of main.\");\n} 예제 15-15: 메모리를 일찍 정리하기 위한 Drop 트레이트의 drop 메서드의 수동 호출 시도하기 이 코드의 컴파일을 시도하면 다음과 같은 에러를 얻게 됩니다: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example)\nerror[E0040]: explicit use of destructor method --> src/main.rs:16:7 |\n16 | c.drop(); | --^^^^-- | | | | | explicit destructor calls not allowed | help: consider using `drop` function: `drop(c)` For more information about this error, try `rustc --explain E0040`.\nerror: could not compile `drop-example` due to previous error 이 에러 메시지는 drop을 명시적으로 호출하는 것이 허용되지 않음을 기술하고 있습니다. 에러 메시지에서 소멸자 (destructor) 라는 용어가 사용되었는데, 이는 인스턴스를 정리하는 함수에 대한 일반적인 프로그래밍 용어입니다. 소멸자 는 인스턴스를 생성하는 생성자 (constructor) 와 유사한 용어입니다. 러스트의 drop 함수는 특정한 형태의 소멸자입니다. 러스트는 drop을 명시적으로 호출하도록 해주지 않는데 이는 러스트가 여전히 main의 끝부분에서 그 값에 대한 drop 호출을 자동으로 할 것이기 때문입니다. 이는 러스트가 동일한 값에 대해 두 번 메모리 정리를 시도할 것이므로 중복 해제 (double free) 에러가 될 수 있습니다. 어떤 값이 스코프 밖으로 벗어났을 때의 자동적인 drop 호출을 비활성화할 수 없고, drop 메서드를 명시적으로 호출할 수도 없습니다. 따라서, 어떤 값에 대한 메모리 정리를 강제로 일찍 하기 원할 때는 std::mem::drop 함수를 이용합니다. std::mem::drop 함수는 Drop 트레이트에 있는 drop 메서드와는 다릅니다. 이 함수에 일찍 버리려고 하는 값을 인수로 넘겨 호출합니다. 이 함수는 프렐루드에 포함되어 있어서, 예제 15-14의 main을 예제 15-16처럼 수정할 수 있습니다: 파일명: src/main.rs # struct CustomSmartPointer {\n# data: String,\n# }\n# # impl Drop for CustomSmartPointer {\n# fn drop(&mut self) {\n# println!(\"Dropping CustomSmartPointer with data `{}`!\", self.data);\n# }\n# }\n# fn main() { let c = CustomSmartPointer { data: String::from(\"some data\"), }; println!(\"CustomSmartPointer created.\"); drop(c); println!(\"CustomSmartPointer dropped before the end of main.\");\n} 예제 15-16: std::mem::drop을 호출하여 값이 스코프를 벗어나기 전에 명시적으로 버리기 이 코드를 실행하면 아래와 같이 출력할 것입니다: $ cargo run Compiling drop-example v0.1.0 (file:///projects/drop-example) Finished dev [unoptimized + debuginfo] target(s) in 0.73s Running `target/debug/drop-example`\nCustomSmartPointer created.\nDropping CustomSmartPointer with data `some data`!\nCustomSmartPointer dropped before the end of main. Dropping CustomSmartPointer with data `some data`!라는 텍스트가 CustomSmartPointer created.와 CustomSmartPointer dropped before the end of main. 사이에 출력되는데, 이는 c를 버리는 drop 메서드가 그 지점에서 호출되었음을 보여줍니다. Drop 트레이트 구현체에 지정되는 코드를 다양한 방식으로 사용하여 메모리 정리를 편리하고 안전하게 할 수 있습니다: 예를 들면, 이것을 사용하여 여러분만의 고유한 메모리 할당자를 만들 수 있습니다! Drop 트레이트와 러스트의 소유권 시스템을 이용하면 러스트가 메모리 정리를 자동으로 수행하기 때문에 메모리 정리를 기억해 두지 않아도 됩니다. 또한 아직 사용 중인 값이 뜻하지 않게 정리되면서 발생하는 문제도 걱정할 필요 없습니다: 참조자가 항상 유효하도록 보장해 주는 소유권 시스템은 그 값이 더 이상 사용되지 않을 때 drop이 한 번만 호출되는 것도 보장합니다. 지금까지 Box와 스마트 포인터의 몇 가지 특성을 시험해 보았으니, 표준 라이브러리에 정의되어 있는 몇 가지 다른 스마트 포인터를 살펴봅시다.","breadcrumbs":"스마트 포인터 » Drop 트레이트로 메모리 정리 코드 실행하기 » std::mem::drop으로 값을 일찍 버리기","id":"280","title":"std::mem::drop으로 값을 일찍 버리기"},"281":{"body":"대부분의 경우에서 소유권은 명확합니다: 즉 어떤 변수가 주어진 값을 소유하는지 정확히 압니다. 그러나 하나의 값이 여러 개의 소유자를 가질 수 있는 경우도 있습니다. 예를 들어, 그래프 데이터 구조에서 여러 에지가 동일한 노드를 가리킬 수도 있고, 그 노드는 개념적으로 해당 노드를 가리키는 모든 에지에 의해 소유됩니다. 노드는 어떠한 에지도 이를 가리키지 않아 소유자가 하나도 없는 상태가 아니라면 메모리 정리가 되어서는 안 됩니다. 명시적으로 복수 소유권을 가능하게 하려면 러스트의 Rc 타입을 이용해야 하는데, 이는 참조 카운팅 (reference counting) 의 약자입니다. Rc 타입은 어떤 값의 참조자 개수를 계속 추적하여 해당 값이 계속 사용 중인지를 판단합니다. 만일 어떤 값에 대한 참조자가 0개라면 이 값의 메모리 정리를 하더라도 유효하지 않은 참조자가 발생하지 않을 수 있습니다. Rc를 거실의 TV라고 상상해 봅시다. 한 사람이 TV를 보러 들어올 때 TV를 켭니다. 다른 사람들은 거실로 들어와서 TV를 볼 수 있습니다. 마지막 사람이 거실을 나선다면, TV는 더 이상 사용되고 있지 않으므로 끕니다. 만일 누군가 계속 TV를 보고 있는 중에 어떤 이가 꺼버리면, 남아있던 TV 시청자들로부터 엄청난 소란이 있겠죠! Rc 타입은 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶은데 컴파일 타임에는 어떤 부분이 그 데이터를 마지막에 이용하게 될지 알 수 없는 경우 사용됩니다. 만일 어떤 부분이 마지막으로 사용하는지 알았다면, 그냥 그 해당 부분을 데이터의 소유자로 만들면 되고, 보통의 소유권 규칙이 컴파일 타임에 수행되어 효력을 발생시킬 겁니다. Rc는 오직 싱글스레드 시나리오용이라는 점을 주의하세요. 16장에서 동시성 (cuncurrency) 에 대한 논의를 할 때, 멀티스레드 프로그램에서 참조 카운팅을 하는 방법을 다루겠습니다.","breadcrumbs":"스마트 포인터 » Rc, 참조 카운트 스마트 포인터 » Rc, 참조 카운트 스마트 포인터","id":"281","title":"Rc, 참조 카운트 스마트 포인터"},"282":{"body":"예제 15-5의 콘스 리스트 예제로 돌아가 봅시다. Box를 이용해서 이를 정의했던 것을 상기합시다. 이번에는 두 개의 리스트를 만들고 이 둘이 모두 세 번째 리스트의 소유권을 공유하도록 하겠습니다. 개념적으로는 그림 15-3처럼 생겼습니다: 그림 15-3: 세 번째 리스트 a의 소유권을 공유하는 두 리스트 b와 c 먼저 5와 10을 담은 리스트 a를 만들겠습니다. 그런 다음 두 개의 리스트를 더 만들 것입니다: 3으로 시작하는 b와 4로 시작하는 c를 말이죠. 그리고서 b와 c 리스트 둘 모두 5와 10을 가지고 있는 첫 번째 a 리스트로 계속되도록 하겠습니다. 바꿔 말하면, 두 리스트 모두 5와 10을 담고 있는 첫 리스트를 공유하게 될 것입니다. 예제 15-17과 같이 Box를 가지고 정의한 List를 이용하여 이 시나리오의 구현을 시도하면 작동하지 않을 것입니다: 파일명: src/main.rs enum List { Cons(i32, Box), Nil,\n} use crate::List::{Cons, Nil}; fn main() { let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); let b = Cons(3, Box::new(a)); let c = Cons(4, Box::new(a));\n} 예제 15-17: Box를 이용한 두 리스트가 세 번째 리스트에 대한 소유권을 공유하는 시도는 허용되지 않음을 보이는 예 이 코드를 컴파일하면 다음과 같은 에러를 얻습니다: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list)\nerror[E0382]: use of moved value: `a` --> src/main.rs:11:30 |\n9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil)))); | - move occurs because `a` has type `List`, which does not implement the `Copy` trait\n10 | let b = Cons(3, Box::new(a)); | - value moved here\n11 | let c = Cons(4, Box::new(a)); | ^ value used here after move For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `cons-list` due to previous error Cons 배리언트는 자신이 들고 있는 데이터를 소유하므로, b 리스트를 만들 때 a는 b 안으로 이동되어 b의 소유가 됩니다. 그다음 c를 생성할 때 a를 다시 사용하려 할 경우는 허용되지 않는데, 이미 a가 이동되었기 때문입니다. Cons의 정의를 변경하여 참조자를 대신 들고 있도록 할 수도 있지만, 그러면 라이프타임 매개변수를 명시해야 할 것입니다. 라이프타임 매개변수를 명시함으로써, 리스트 내의 모든 요소가 최소한 전체 리스트만큼 오래 살아있도록 지정할 것입니다. 이는 예제 15-17의 요소와 리스트에 대한 경우지, 모든 시나리오에 맞는 것은 아닙니다. 그 대신 예제 15-18과 같이 Box의 자리에 Rc를 이용하는 형태로 List의 정의를 바꾸겠습니다. 각각의 Cons 배리언트는 이제 어떤 값과 List를 가리키는 Rc를 갖게 될 것입니다. b를 만들 때는 a의 소유권을 얻는 대신, a를 가지고 있는 Rc를 클론할 것인데, 이는 참조자의 개수를 하나에서 둘로 증가시키고 a와 b가 Rc 안에 있는 데이터의 소유권을 공유하도록 해줍니다. 또한 c를 만들 때도 a를 클론할 것인데, 이로써 참조자의 개수가 둘에서 셋으로 늘어납니다. Rc::clone가 호출될 때마다 그 Rc가 가지고 있는 데이터에 대한 참조 카운트는 증가할 것이고, 그 데이터는 참조자가 0개가 되지 않으면 메모리가 정리되지 않을 것입니다. 파일명: src/main.rs enum List { Cons(i32, Rc), Nil,\n} use crate::List::{Cons, Nil};\nuse std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a));\n} 예제 15-18: Rc를 이용하는 List 정의 Rc는 프렐루드에 포함되어 있지 않으므로 이를 스코프로 가져오려면 use 구문을 추가해야 합니다. main 안에서 5와 10을 가지고 있는 리스트가 만들어지고 이것이 a의 새로운 Rc에 저장됩니다. 그다음 b와 c를 만들 때는 Rc::clone 함수를 호출하고 a의 Rc에 대한 참조자를 인수로서 넘깁니다. Rc::clone(&a) 대신 a.clone()을 호출할 수도 있지만, 위의 경우 러스트의 관례는 Rc::clone를 이용하는 것입니다. Rc::clone의 구현체는 대부분의 타입들에 대한 clone 구현체들이 그러하듯 모든 데이터에 대한 깊은 복사 (deep copy) 를 하지 않습니다. Rc::clone의 호출은 오직 참조 카운트만 증가시키는데, 이는 시간이 얼마 걸리지 않습니다. 데이터의 깊은 복사는 많은 시간이 걸릴 수 있습니다. 참조 카운팅을 위해 Rc::clone을 사용함으로써 깊은 복사 종류의 클론과 참조 카운트를 증가시키는 종류의 클론을 시각적으로 구별할 수 있습니다. 코드에서 성능 문제를 찾는 중이라면 깊은 복사 클론만 고려할 필요가 있고 Rc::clone 호출은 무시할 수 있습니다.","breadcrumbs":"스마트 포인터 » Rc, 참조 카운트 스마트 포인터 » Rc를 사용하여 데이터 공유하기","id":"282","title":"Rc를 사용하여 데이터 공유하기"},"283":{"body":"예제 15-18의 작동하는 예제를 변경하여 a 내부의 Rc에 대한 참조자가 생성되고 버려질 때 참조 카운트가 변하는 것을 볼 수 있도록 해봅시다. 예제 15-19에서는 main을 변경하여 안쪽의 스코프가 리스트 c를 감싸도록 하겠습니다; 그러면 c가 스코프 밖으로 벗어났을 때 참조 카운트가 어떻게 바뀌는지 볼 수 있습니다. 파일명: src/main.rs # enum List {\n# Cons(i32, Rc),\n# Nil,\n# }\n# # use crate::List::{Cons, Nil};\n# use std::rc::Rc;\n# fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!(\"count after creating a = {}\", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!(\"count after creating b = {}\", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!(\"count after creating c = {}\", Rc::strong_count(&a)); } println!(\"count after c goes out of scope = {}\", Rc::strong_count(&a));\n} 예제 15-19: Printing the reference count 프로그램 내 참조 카운트가 변하는 각 지점에서 Rc::strong_count 함수를 호출하여 얻은 참조 카운트 값을 출력합니다. 이 함수가 count가 아니고 strong_count라는 이름이 된 이유는 Rc 타입이 weak_count도 갖고 있기 때문입니다; weak_count가 뭘 위해서 사용되는지는 ‘순환 참조 방지하기: Rc를 Weak로 바꾸기’ 절에서 알아보겠습니다. 이 코드는 다음을 출력합니다: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.45s Running `target/debug/cons-list`\ncount after creating a = 1\ncount after creating b = 2\ncount after creating c = 3\ncount after c goes out of scope = 2 a의 Rc는 초기 참조 카운트 1을 갖고 있음을 볼 수 있습니다; 그 후 clone을 호출할 때마다 카운트가 1씩 증가합니다. c가 스코프 밖으로 벗어날 때는 카운트가 1 감소합니다. Rc::clone를 호출하여 참조 카운트를 증가시켜야 했던 것과 달리 참조 카운트를 감소시키기 위해 어떤 함수를 호출할 필요는 없습니다: Rc 값이 스코프 밖으로 벗어나면 Drop 트레이트의 구현체가 자동으로 참조 카운트를 감소시킵니다. main의 끝부분에서 b와 그다음 a가 스코프 밖을 벗어나서, 카운트가 0이 되고, 그 시점에서 Rc가 완전히 메모리 정리되는 것은 이 예제에서 볼 수 없습니다. Rc를 이용하면 단일 값이 복수 소유자를 갖도록 할 수 있고, 그 개수는 소유자 중 누구라도 계속 존재하는 한 해당 값이 계속 유효하도록 보장해 줍니다. Rc는 불변 참조자를 통하여 읽기 전용으로 프로그램의 여러 부분에서 데이터를 공유하도록 해줍니다. 만일 Rc가 여러 개의 가변 참조자도 만들도록 해준다면, 4장에서 논의했던 대여 규칙 중 하나를 위반할지도 모릅니다: 동일한 위치에서 여러 개의 가변 대여는 데이터 경합 및 데이터 불일치를 야기할 수 있습니다. 하지만 데이터의 변형을 가능하게 하는 것은 매우 유용하죠! 다음 절에서는 이러한 불변성 제약과 함께 동작하도록 하기 위한 내부 가변성 (interior mutability) 패턴 및 Rc와 같이 결합하여 사용할 수 있는 RefCell 타입에 대해 논의하겠습니다.","breadcrumbs":"스마트 포인터 » Rc, 참조 카운트 스마트 포인터 » Rc를 클론하는 것은 참조 카운트를 증가시킵니다","id":"283","title":"Rc를 클론하는 것은 참조 카운트를 증가시킵니다"},"284":{"body":"내부 가변성 (interior mutability) 은 어떤 데이터에 대한 불변 참조자가 있을 때라도 데이터를 변경할 수 있게 해주는 러스트의 디자인 패턴입니다; 보통 이러한 동작은 대여 규칙에 의해 허용되지 않습니다. 데이터를 변경하기 위해서, 이 패턴은 데이터 구조 내에서 unsafe (안전하지 않은) 코드를 사용하여 변경과 대여를 지배하는 러스트의 일반적인 규칙을 우회합니다. 안전하지 않은 코드는 이 규칙들을 지키고 있는지에 대한 검사를 컴파일러에게 맡기는 대신 수동으로 하는 중임을 컴파일러에게 알립니다; 안전하지 않은 코드에 대해서는 19장에서 더 알아보겠습니다. 컴파일러는 대여 규칙을 준수함을 보장할 수 없을지라도, 우리가 이를 런타임에 보장할 수 있는 경우라면 내부 가변성 패턴을 쓰는 타입을 사용할 수 있습니다. 여기에 포함된 unsafe 코드는 안전한 API로 감싸져 있고, 바깥쪽 타입은 여전히 불변입니다. 내부 가변성 패턴을 따르는 RefCell 타입을 살펴보면서 이 개념을 탐구해 봅시다.","breadcrumbs":"스마트 포인터 » RefCell와 내부 가변성 패턴 » RefCell와 내부 가변성 패턴","id":"284","title":"RefCell와 내부 가변성 패턴"},"285":{"body":"Rc와는 다르게, RefCell 타입은 가지고 있는 데이터에 대한 단일 소유권을 나타냅니다. 그렇다면, Box와 같은 타입과 RefCell의 다른 부분은 무엇일까요? 4장에서 배웠던 대여 규칙을 상기해 봅시다: 어떠한 경우이든 간에, 하나의 가변 참조자 혹은 여러 개의 불변 참조자 중 (둘 다가 아니고) 하나 만 가질 수 있습니다. 참조자는 항상 유효해야 합니다. 참조자와 Box를 이용할 때, 대여 규칙의 불변성은 컴파일 타임에 집행됩니다. RefCell를 이용할 때, 이 불변성은 런타임에 집행됩니다. 참조자를 가지고서 이 규칙을 어기면 컴파일러 에러를 얻게 될 것입니다. RefCell를 가지고서 여러분이 이 규칙을 어기면, 프로그램은 panic!을 일으키고 종료될 것입니다. 컴파일 타임의 대여 규칙 검사는 개발 과정에서 에러를 더 일찍 잡을 수 있다는 점, 그리고 이 모든 분석이 사전에 완료되기 때문에 런타임 성능에 영향이 없다는 장점이 있습니다. 이러한 이유로 컴파일 타임의 대여 규칙을 검사하는 것이 대부분의 경우에서 가장 좋은 선택이고, 이것이 러스트의 기본 설정인 이유이기도 합니다. 런타임의 대여 규칙 검사를 하면 컴파일 타임 검사에 의해서는 허용되지 않을 특정 메모리 안정성 시나리오가 허용된다는 장점이 있습니다. 러스트 컴파일러와 같은 정적 분석은 태생적으로 보수적입니다. 어떤 코드 속성은 코드 분석으로는 발견이 불가능합니다: 가장 유명한 예제로 정지 문제 (halting problem) 가 있는데, 이는 이 책의 범위를 벗어나지만 연구하기에 흥미로운 주제입니다. 몇몇 분석이 불가능하기 때문에, 러스트 컴파일러가 어떤 코드의 소유권 규칙 준수를 확신할 수 없다면, 올바른 프로그램을 거부할지도 모릅니다; 이런 식으로 컴파일러는 보수적입니다. 러스트가 올바르지 않은 프로그램을 수용한다면, 사용자들은 러스트가 보장하는 것을 신뢰할 수 없을 것입니다. 하지만, 만일 러스트가 올바른 프로그램을 거부한다면, 프로그래머는 불편하겠지만 어떠한 재앙도 일어나지 않을 수 있습니다. RefCell 타입은 여러분의 코드가 대여 규칙을 준수한다는 것을 컴파일러는 이해하거나 보장할 수 없지만 여러분이 확신하는 경우 유용합니다. Rc와 유사하게, RefCell은 싱글스레드 시나리오 내에서만 사용 가능하고, 멀티스레드 컨텍스트에서 사용을 시도할 경우에는 컴파일 타임 에러를 낼 것입니다. RefCell의 기능을 멀티스레드 프로그램에서 사용하는 방법에 대해서는 16장에서 이야기하겠습니다. Box, Rc, 혹은 RefCell을 선택하는 이유의 요점을 정리하면 다음과 같습니다: Rc는 동일한 데이터에 대해 복수 소유자를 가능하게 합니다; Box와 RefCell은 단일 소유자만 갖습니다. Box는 컴파일 타임에 검사 되는 불변 혹은 가변 대여를 허용합니다; Rc는 오직 컴파일 타임에 검사 되는 불변 대여만 허용합니다; RefCell는 런타임에 검사되는 불변 혹은 가변 대여를 허용합니다. RefCell이 런타임에 검사 되는 가변 대여를 허용하기 때문에, RefCell이 불변일 때라도 RefCell 내부의 값을 변경할 수 있습니다. 불변값 내부의 값을 변경하는 것이 내부 가변성 패턴입니다. 내부 가변성이 유용한 경우를 살펴보고 이것이 어떻게 가능한지 조사해 봅시다.","breadcrumbs":"스마트 포인터 » RefCell와 내부 가변성 패턴 » RefCell으로 런타임에 대여 규칙 집행하기","id":"285","title":"RefCell으로 런타임에 대여 규칙 집행하기"},"286":{"body":"대여 규칙의 결과로 불변값을 가지고 있을 때 이걸 가변으로 빌려올 수는 없습니다. 예를 들면, 다음 코드는 컴파일되지 않을 것입니다: fn main() { let x = 5; let y = &mut x;\n} 이 코드를 컴파일 시도하면, 다음과 같은 에러를 얻게 됩니다: $ cargo run Compiling borrowing v0.1.0 (file:///projects/borrowing)\nerror[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable --> src/main.rs:3:13 |\n2 | let x = 5; | - help: consider changing this to be mutable: `mut x`\n3 | let y = &mut x; | ^^^^^^ cannot borrow as mutable For more information about this error, try `rustc --explain E0596`.\nerror: could not compile `borrowing` due to previous error 하지만, 어떤 값이 자신의 메서드 내부에서는 변경되지만 다른 코드에서는 불변으로 보이게 하는 것이 유용한 경우가 있습니다. 그 값의 메서드 바깥쪽 코드에서는 값을 변경할 수 없을 것입니다. RefCell을 이용하는 것이 내부 가변성의 기능을 얻는 한 가지 방법이지만, RefCell이 대여 규칙을 완벽하게 피하는 것은 아닙니다: 컴파일러의 대여 검사기는 이러한 내부 가변성을 허용하고, 대신 대여 규칙은 런타임에 검사 됩니다. 만일 이 규칙을 위반하면, 컴파일러 에러 대신 panic!을 얻을 것입니다. RefCell를 이용하여 불변값을 변경할 수 있는 실질적인 예제를 실습해보고 이것이 왜 유용한지를 알아봅시다. 내부 가변성에 대한 용례: 목 객체 테스트 중 종종 프로그래머는 어떤 타입 대신 다른 타입을 사용하게 되는데, 이를 통해 특정 동작을 관측하고 정확하게 구현되었음을 단언하기 위한 것입니다. 이러한 자리 표시형 타입을 테스트 더블 (test double) 이라고 합니다. 영화 제작에서 ‘스턴트 더블 (stunt double)’이라고 부르는, 어떤 사람이 나서서 배우를 대신해 특정한 어려운 장면을 수행하는 것과 같은 의미로 생각하시면 됩니다. 테스트 더블은 테스트를 수행할 때 다른 타입 대신 나서는 것이죠. 목 객체 (mock object) 는 테스트 더블의 특정한 형태로서 테스트 중 어떤 일이 일어났는지 기록하여 정확한 동작이 일어났음을 단언할 수 있도록 해줍니다. 러스트에는 다른 언어들에서의 객체와 동일한 의미의 객체가 없고, 러스트에는 몇몇 다른 언어들처럼 표준 라이브러리에 미리 만들어진 목 객체 기능이 없습니다. 하지만, 당연하게도 목 객체로서 동일한 목적을 제공할 구조체를 만들 수 있습니다. 여기서 테스트하려는 시나리오는 다음과 같습니다: 최댓값을 기준으로 어떤 값을 추적하여 현재 값이 최댓값에 얼마나 근접했는지에 대한 메시지를 전송하는 라이브러리를 만들려고 합니다. 이를테면 이 라이브러리는 한 명의 사용자에게 허용되고 있는 API 호출 수의 허용량을 추적하는 데 사용될 수 있습니다. 우리의 라이브러리는 어떤 값이 최댓값에 얼마나 근접했는지를 추적하고 어떤 메시지를 언제 보내야 할지에 대한 기능만 제공할 것입니다. 이 라이브러리를 사용하는 애플리케이션이 메시지를 전송하는 것에 대한 메커니즘을 제공할 예정입니다: 이 애플리케이션은 메시지를 애플리케이션 내에 집어넣거나, 이메일을 보내거나, 문자 메시지를 보내거나, 혹은 그 밖의 것들을 할 수 있습니다. 라이브러리는 그런 자세한 사항을 알 필요가 없습니다. 필요한 모든 것은 우리가 제공하게 될 Messenger라는 이름의 트레이트를 구현하는 것입니다. 예제 15-20은 라이브러리 코드를 보여줍니다: 파일명: src/lib.rs pub trait Messenger { fn send(&self, msg: &str);\n} pub struct LimitTracker<'a, T: Messenger> { messenger: &'a T, value: usize, max: usize,\n} impl<'a, T> LimitTracker<'a, T>\nwhere T: Messenger,\n{ pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> { LimitTracker { messenger, value: 0, max, } } pub fn set_value(&mut self, value: usize) { self.value = value; let percentage_of_max = self.value as f64 / self.max as f64; if percentage_of_max >= 1.0 { self.messenger.send(\"Error: You are over your quota!\"); } else if percentage_of_max >= 0.9 { self.messenger .send(\"Urgent warning: You've used up over 90% of your quota!\"); } else if percentage_of_max >= 0.75 { self.messenger .send(\"Warning: You've used up over 75% of your quota!\"); } }\n} 예제 15-20: 어떤 값이 최댓값에 얼마나 근접하는지를 추적하고 특정 수준에 값이 있으면 경고해 주는 라이브러리 이 코드에서 한 가지 중요한 부분은 Messenger 트레이트가 self에 대한 불변 참조자와 메시지의 텍스트를 인수로 갖는 send라는 메서드 하나를 갖고 있다는 것입니다. 이 트레이트는 목 객체가 실제 오브젝트와 동일한 방식으로 사용될 수 있도록 하기 위해 구현해야 하는 인터페이스입니다. 그 외에 중요한 부분은 LimitTracker 상의 set_value 메서드의 동작을 테스트가 필요하다는 점입니다. value 매개변수에 어떤 것을 넘길지 바꿀 수는 있지만, set_value는 단언에 필요한 어떤 것도 반환하지 않습니다. Messenger 트레이트를 구현한 어떤 것과 max에 대한 특정 값과 함께 LimitTracker를 만든다면, value에 대해 다른 숫자들을 넘겼을 때 메신저가 적합한 메시지를 보냈다고 말할 수 있길 원하는 것이죠. send를 호출했을 때 메일이나 텍스트 메시지를 보내는 대신, 보냈다고 언급하는 메시지만 추적할 목 객체가 필요합니다. 목 객체의 새 인스턴스를 생성하고, 이 목 객체를 사용하는 LimitTracker를 만들고, LimitTracker의 set_value 메서드를 호출한 다음, 목 객체가 예상한 메시지를 가지고 있는지 검사할 수 있겠습니다. 예제 15-21이 바로 이런 일을 하기 위한 목 객체 구현 시도이지만, 대여 검사기가 이를 허용하지 않을 것입니다: 파일명: src/lib.rs # pub trait Messenger {\n# fn send(&self, msg: &str);\n# }\n# # pub struct LimitTracker<'a, T: Messenger> {\n# messenger: &'a T,\n# value: usize,\n# max: usize,\n# }\n# # impl<'a, T> LimitTracker<'a, T>\n# where\n# T: Messenger,\n# {\n# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {\n# LimitTracker {\n# messenger,\n# value: 0,\n# max,\n# }\n# }\n# # pub fn set_value(&mut self, value: usize) {\n# self.value = value;\n# # let percentage_of_max = self.value as f64 / self.max as f64;\n# # if percentage_of_max >= 1.0 {\n# self.messenger.send(\"Error: You are over your quota!\");\n# } else if percentage_of_max >= 0.9 {\n# self.messenger\n# .send(\"Urgent warning: You've used up over 90% of your quota!\");\n# } else if percentage_of_max >= 0.75 {\n# self.messenger\n# .send(\"Warning: You've used up over 75% of your quota!\");\n# }\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; struct MockMessenger { sent_messages: Vec, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: vec![], } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_messages.len(), 1); }\n} 예제 15-21: 대여 검사기가 허용하지 않는 MockMessenger 구현 시도 이 테스트 코드는 보냈다고 알려주는 메시지를 추적하기 위한 String 값의 Vec인 sent_messages 필드를 갖는 MockMessenger 구조체를 정의합니다. 또한 연관 함수 new를 정의하여 편리하게 빈 메시지 리스트로 시작하는 새로운 MockMessenger 값을 생성할 수 있도록 합니다. 그런 다음에는 MockMessenger에 대한 Messenger 트레이트를 구현하여 MockMessenger를 LimitTracker에 넘겨줄 수 있도록 하였습니다. send 메서드의 정의 부분에서는 매개변수로 넘겨진 메시지를 가져와서 MockMessenger 내의 sent_messages 리스트에 저장합니다. 테스트 내에서는 LimitTracker의 value에 max 값의 75퍼센트 이상인 어떤 값이 설정되었다 했을 때 무슨 일이 일어나는지 테스트하고 있습니다. 먼저 새로운 MockMessenger를 만드는데, 이는 빈 메시지 리스트로 시작될 것입니다. 그 다음 새로운 LimitTracker를 만들고 여기에 새로운 MockMessenger의 참조자와 max 값 100을 매개변수로 넘깁니다. LimitTracker의 set_value 메서드를 80 값으로 호출하였는데, 이는 75퍼센트 이상입니다. 그다음 MockMessenger가 추적하고 있는 메시지 리스트가 이제 한 개의 메시지를 가지고 있는지를 검사합니다. 하지만, 이 테스트에는 아래와 같이 한 가지 문제점이 있습니다: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)\nerror[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference --> src/lib.rs:58:13 |\n2 | fn send(&self, msg: &str); | ----- help: consider changing that to be a mutable reference: `&mut self`\n...\n58 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable For more information about this error, try `rustc --explain E0596`.\nerror: could not compile `limit-tracker` due to previous error\nwarning: build failed, waiting for other jobs to finish... 메시지를 추적하기 위해서 MockMessenger를 수정할 수가 없는데, 그 이유는 send 메서드가 self의 불변 참조자를 가져오기 때문입니다. 또한 에러 메시지가 제안하는 &mut self를 대신 사용하라는 것도 받아들일 수 없는데, 그렇게 되면 send의 시그니처가 Messenger 트레이트의 정의에 있는 시그니처와 맞지 않게 될 것이기 때문입니다. (편하게 한번 시도해 보고 어떤 에러가 나오는지 보세요.) 지금이 내부 가변성의 도움을 받을 수 있는 상황입니다! sent_messages가 RefCell 내에 저장되게 하면, send 메서드는 sent_message를 수정하여 우리에게 보이는 메시지를 저장할 수 있게 될 것입니다. 예제 15-22는 이것이 어떤 형태인지를 보여줍니다: 파일명: src/lib.rs # pub trait Messenger {\n# fn send(&self, msg: &str);\n# }\n# # pub struct LimitTracker<'a, T: Messenger> {\n# messenger: &'a T,\n# value: usize,\n# max: usize,\n# }\n# # impl<'a, T> LimitTracker<'a, T>\n# where\n# T: Messenger,\n# {\n# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {\n# LimitTracker {\n# messenger,\n# value: 0,\n# max,\n# }\n# }\n# # pub fn set_value(&mut self, value: usize) {\n# self.value = value;\n# # let percentage_of_max = self.value as f64 / self.max as f64;\n# # if percentage_of_max >= 1.0 {\n# self.messenger.send(\"Error: You are over your quota!\");\n# } else if percentage_of_max >= 0.9 {\n# self.messenger\n# .send(\"Urgent warning: You've used up over 90% of your quota!\");\n# } else if percentage_of_max >= 0.75 {\n# self.messenger\n# .send(\"Warning: You've used up over 75% of your quota!\");\n# }\n# }\n# }\n# #[cfg(test)]\nmod tests { use super::*; use std::cell::RefCell; struct MockMessenger { sent_messages: RefCell>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_messages: RefCell::new(vec![]), } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_messages.borrow_mut().push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { // --생략--\n# let mock_messenger = MockMessenger::new();\n# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);\n# # limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); }\n} 예제 15-22: RefCell를 사용하여 바깥쪽에서는 불변으로 간주되는 한편 내부 값 변경하기 sent_message 필드는 이제 Vec 대신 RefCell> 타입입니다. new 함수에서는 빈 벡터를 감싼 새로운 RefCell> 인스턴스를 생성합니다. send 메서드의 구현부에서 첫 번째 매개변수는 여전히 self의 불변 대여 형태인데, 이는 트레이트의 정의와 일치합니다. self.sent_messages의 RefCell>에 있는 borrow_mut를 호출하여 RefCell> 내부 값, 즉 벡터에 대한 가변 참조자를 얻습니다. 그런 다음에는 그 벡터에 대한 가변 참조자의 push를 호출하여 테스트하는 동안 보내진 메시지를 추적할 수 있습니다. 변경할 필요가 있는 마지막 부분은 단언 부분 안에 있습니다: 내부 벡터 안에 몇 개의 아이템이 있는지 보기 위해서 RefCell>의 borrow를 호출하여 벡터에 대한 불변 참조자를 얻습니다. 이제 RefCell가 어떻게 동작하는지 보았으니, 어떻게 동작하는지 파봅시다! RefCell로 런타임에 대여 추적하기 불변 및 가변 참조자를 만들 때는 각각 & 및 &mut 문법을 사용합니다. RefCell로는 borrow와 borrow_mut 메서드를 사용하는데, 이들은 RefCell가 보유한 안전한 API 중 일부입니다. borrow 메서드는 스마트 포인터 타입인 Ref를 반환하고, borrow_mut는 스마트 포인터 타입 RefMut를 반환합니다. 두 타입 모두 Deref를 구현하였기 때문에, 이들을 보통의 참조자처럼 다룰 수 있습니다. RefCell는 현재 활성화된 Ref와 RefMut 스마트 포인터들이 몇 개나 있는지 추적합니다. borrow를 호출할 때마다, RefCell는 불변 참조자가 활성화된 개수를 증가시킵니다. Ref 값이 스코프 밖으로 벗어날 때는 불변 대여의 개수가 하나 감소합니다. 컴파일 타임에서의 대여 규칙과 똑같이, RefCell는 어떤 시점에서든 여러 개의 불변 대여 혹은 하나의 가변 대여를 가질 수 있도록 만들어 줍니다. 만일 이 규칙들을 위반한다면, RefCell의 구현체는 참조자에 대해 그렇게 했을 때처럼 컴파일 에러를 내는 것이 아니라, 런타임에 panic!을 일으킬 것입니다. 예제 15-23은 예제 15-22의 send 구현을 수정한 것입니다. 고의로 같은 스코프에서 두 개의 가변 대여를 만드는 시도를 하여 RefCell가 이렇게 하는 것을 런타임에 방지한다는 것을 보여주고 있습니다. 파일명: src/lib.rs # pub trait Messenger {\n# fn send(&self, msg: &str);\n# }\n# # pub struct LimitTracker<'a, T: Messenger> {\n# messenger: &'a T,\n# value: usize,\n# max: usize,\n# }\n# # impl<'a, T> LimitTracker<'a, T>\n# where\n# T: Messenger,\n# {\n# pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, T> {\n# LimitTracker {\n# messenger,\n# value: 0,\n# max,\n# }\n# }\n# # pub fn set_value(&mut self, value: usize) {\n# self.value = value;\n# # let percentage_of_max = self.value as f64 / self.max as f64;\n# # if percentage_of_max >= 1.0 {\n# self.messenger.send(\"Error: You are over your quota!\");\n# } else if percentage_of_max >= 0.9 {\n# self.messenger\n# .send(\"Urgent warning: You've used up over 90% of your quota!\");\n# } else if percentage_of_max >= 0.75 {\n# self.messenger\n# .send(\"Warning: You've used up over 75% of your quota!\");\n# }\n# }\n# }\n# # #[cfg(test)]\n# mod tests {\n# use super::*;\n# use std::cell::RefCell;\n# # struct MockMessenger {\n# sent_messages: RefCell>,\n# }\n# # impl MockMessenger {\n# fn new() -> MockMessenger {\n# MockMessenger {\n# sent_messages: RefCell::new(vec![]),\n# }\n# }\n# }\n# impl Messenger for MockMessenger { fn send(&self, message: &str) { let mut one_borrow = self.sent_messages.borrow_mut(); let mut two_borrow = self.sent_messages.borrow_mut(); one_borrow.push(String::from(message)); two_borrow.push(String::from(message)); } }\n# # #[test]\n# fn it_sends_an_over_75_percent_warning_message() {\n# let mock_messenger = MockMessenger::new();\n# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);\n# # limit_tracker.set_value(80);\n# # assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);\n# }\n# } 예제 15-23: 두 개의 가변 참조자를 같은 스코프에서 만들어서 RefCell이 패닉을 일으키는 것을 보기 borrow_mut로부터 반환되는 RefMut 스마트 포인터를 위한 one_borrow 변수가 만들어졌습니다. 그런 다음 또 다른 가변 대여를 같은 방식으로 two_borrow 변수에 만들어 넣었습니다. 이는 같은 스코프에 두 개의 가변 참조자를 만드는 것이고, 허용되지 않습니다. 라이브러리를 위한 테스트를 실행하면 예제 15-23의 코드는 어떠한 에러 없이 컴파일되겠지만, 테스트는 실패할 것입니다: $ cargo test Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker) Finished test [unoptimized + debuginfo] target(s) in 0.91s Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde) running 1 test\ntest tests::it_sends_an_over_75_percent_warning_message ... FAILED failures: ---- tests::it_sends_an_over_75_percent_warning_message stdout ----\nthread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53\nnote: run with `RUST_BACKTRACE=1` environment variable to display a backtrace failures: tests::it_sends_an_over_75_percent_warning_message test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s error: test failed, to rerun pass `--lib` 이 코드가 already borrowed: BorrowMutError라는 메시지와 함께 패닉을 일으켰음을 주목하세요. 이것이 바로 RefCell가 런타임에 대여 규칙 위반을 다루는 방법입니다. 여기서처럼 대여 에러를 컴파일 타임이 아닌 런타임에 잡기로 선택하는 것은 개발 과정 이후에 여러분의 코드에서 실수를 발견할 가능성이 있음을 의미합니다: 여러분의 코드가 프로덕션으로 배포될 때까지 발견되지 않을 수도 있습니다. 또한, 여러분의 코드는 컴파일 타임이 아닌 런타임에 대여를 추적하는 결과로 약간의 런타임 성능 페널티를 초래할 것입니다. 하지만 RefCell를 이용하는 것은 오직 불변값만 허용된 컨텍스트 안에서 사용하는 중에 본 메시지를 추적하기 위해서 스스로를 변경할 수 있는 목 객체 작성을 가능하게 해 줍니다. 트레이드오프가 있더라도 RefCell를 사용하여 일반적인 참조자가 제공하는 것보다 더 많은 기능을 얻을 수 있습니다.","breadcrumbs":"스마트 포인터 » RefCell와 내부 가변성 패턴 » 내부 가변성: 불변값에 대한 가변 대여","id":"286","title":"내부 가변성: 불변값에 대한 가변 대여"},"287":{"body":"RefCell를 사용하는 일반적인 방법은 Rc와 조합하는 것입니다. Rc가 어떤 데이터에 대해 복수의 소유자를 허용하지만, 그 데이터에 대한 불변 접근만 제공하는 것을 상기하세요. 만일 RefCell를 들고 있는 Rc를 가지게 되면, 가변이면서 동시에 복수의 소유자를 갖는 값을 얻을 수 있는 것이죠! 예를 들면, 예제 15-18에서 Rc를 사용하여 여러 개의 리스트가 어떤 리스트의 소유권을 공유하도록 해준 콘스 리스트 예제를 상기해 보세요. Rc가 오직 불변의 값만을 가질 수 있기 때문에, 일단 이것들을 만들면 리스트 안의 값들을 변경하는 것은 불가능했습니다. RefCell를 추가하여 이 리스트 안의 값을 변경하는 능력을 얻어봅시다. 예제 15-24는 Cons 정의 내에 RefCell를 사용하여 모든 리스트 내에 저장된 값이 변경될 수 있음을 보여줍니다: 파일명: src/main.rs #[derive(Debug)]\nenum List { Cons(Rc>, Rc), Nil,\n} use crate::List::{Cons, Nil};\nuse std::cell::RefCell;\nuse std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(3)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a)); *value.borrow_mut() += 10; println!(\"a after = {:?}\", a); println!(\"b after = {:?}\", b); println!(\"c after = {:?}\", c);\n} 예제 15-24: Rc>를 사용하여 변경 가능한 List 생성하기 먼저 Rc>의 인스턴스 값을 생성하고 value라는 이름의 변수 안에 저장하여 나중에 이를 직접 접근할 수 있게 했습니다. 그다음 value를 가지고 있는 Cons 배리언트로 List를 생성하여 a에 넣었습니다. value는 클론되어 value가 가진 내부의 값 5 값에 대한 소유권이 a로 이동되거나 a가 value로부터 빌려오는 것이 아니라 a와 value 모두가 이 값에 대한 소유권을 갖도록 할 필요가 있습니다. 리스트 a는 Rc로 감싸져서, b와 c 리스트를 만들 때는 둘 다 a를 참조할 수 있는데, 이는 예제 15-18에서 해본 것입니다. a, b와 c 리스트가 생성된 이후, value의 값에 10을 더하려고 합니다. 이는 value의 borrow_mut를 호출하는 식으로 수행되었는데, 여기서 5장에서 논의했던 자동 역참조 기능이 사용되어 Rc를 역참조하여 안에 있는 RefCell 값을 얻어옵니다 ( ‘-> 연산자는 없나요?’ 절을 보세요). borrow_mut 메서드는 RefMut 스마트 포인터를 반환하고, 여기에 역참조 연산자를 사용한 다음 내부 값을 변경합니다. a, b와 c를 출력하면 이 리스트들이 모두 5가 아니라 변경된 값 15를 가지고 있는 것을 볼 수 있습니다: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.63s Running `target/debug/cons-list`\na after = Cons(RefCell { value: 15 }, Nil)\nb after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))\nc after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil)) 이 기술은 꽤 근사합니다! RefCell를 이용하면 표면상으로는 불변인 List를 갖게 됩니다. 하지만 데이터를 변경할 필요가 생기면 내부 가변성 접근 기능을 제공하는 RefCell의 메서드를 사용하여 그렇게 할 수 있습니다. 대여 규칙의 런타임 검사는 데이터 경합으로부터 우리를 지켜주고, 데이터 구조에 대한 이런 유연성을 위해서 약간의 속도를 맞바꾸는 것이 때로는 가치가 있습니다. RefCell가 멀티스레드 코드에서는 동작하지 않음을 주의하세요! Mutex가 RefCell의 스레드 안전 버전이고, 이는 16장에서 다루겠습니다.","breadcrumbs":"스마트 포인터 » RefCell와 내부 가변성 패턴 » Rc와 RefCell를 조합하여 가변 데이터의 복수 소유자 만들기","id":"287","title":"Rc와 RefCell를 조합하여 가변 데이터의 복수 소유자 만들기"},"288":{"body":"러스트의 메모리 안정성 보장은 ( 메모리 누수 (memory leak) 라고도 알려져 있는) 뜻하지 않게 해제되지 않는 메모리를 생성하기 어렵게 만들지만, 불가능하게 만드는 것은 아닙니다. 메모리 누수를 완전히 방지하는 것은 러스트가 보장하는 것 중 하나가 아닌데, 이는 메모리 누수도 러스트에서는 메모리 안정성에 포함됨을 의미합니다. Rc 및 RefCell를 사용하면 러스트에서 메모리 누수가 허용되는 것을 알 수 있습니다: 즉 아이템들이 서로를 순환 참조하는 참조자를 만드는 것이 가능합니다. 이는 메모리 누수를 발생시키는데, 그 이유는 순환 고리 안의 각 아이템의 참조 카운트는 결코 0이 되지 않을 것이고, 그러므로 값들은 버려지지 않을 것이기 때문입니다.","breadcrumbs":"스마트 포인터 » 순환 참조는 메모리 누수를 발생시킬 수 있습니다 » 순환 참조는 메모리 누수를 발생시킬 수 있습니다","id":"288","title":"순환 참조는 메모리 누수를 발생시킬 수 있습니다"},"289":{"body":"예제 15-25의 List 열거형과 tail 메서드 정의를 시작으로 어떻게 순환 참조가 생길 수 있고, 이를 어떻게 방지하는지 알아봅시다: 파일명: src/main.rs use crate::List::{Cons, Nil};\nuse std::cell::RefCell;\nuse std::rc::Rc; #[derive(Debug)]\nenum List { Cons(i32, RefCell>), Nil,\n} impl List { fn tail(&self) -> Option<&RefCell>> { match self { Cons(_, item) => Some(item), Nil => None, } }\n} fn main() {} 예제 15-25: RefCell를 가지고 있어서 Cons 배리언트가 참조하는 것을 변경할 수 있는 cons 리스트 정의 예제 15-5의 List 정의의 또 다른 변형이 이용되고 있습니다. 이제 Cons 배리언트 내의 두 번째 요소는 RefCell>인데, 이는 예제 15-24에서 했던 것처럼 i32 값을 변경하는 능력을 가진 대신, Cons 배리언트가 가리키고 있는 List 값을 변경하길 원한다는 의미입니다. 또한 tail 메서드를 추가하여 Cons 배리언트를 갖고 있다면 두 번째 아이템에 접근하기 편하게 만들었습니다. 예제 15-26에서는 예제 15-25에서 사용한 main 함수를 추가하고 있습니다. 이 코드는 a에 리스트를 만들고 b에는 a의 리스트를 가리키고 있는 리스트를 만들어 넣었습니다. 그다음 a의 리스트가 b를 가리키도록 수정하는데, 이것이 순환 참조를 생성합니다. 이 과정에서 참조 카운트가 얼마인지 여러 곳에서 확인하기 위해 곳곳에 println! 구문들을 넣었습니다. 파일명: src/main.rs # use crate::List::{Cons, Nil};\n# use std::cell::RefCell;\n# use std::rc::Rc;\n# # #[derive(Debug)]\n# enum List {\n# Cons(i32, RefCell>),\n# Nil,\n# }\n# # impl List {\n# fn tail(&self) -> Option<&RefCell>> {\n# match self {\n# Cons(_, item) => Some(item),\n# Nil => None,\n# }\n# }\n# }\n# fn main() { let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil)))); println!(\"a initial rc count = {}\", Rc::strong_count(&a)); println!(\"a next item = {:?}\", a.tail()); let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a)))); println!(\"a rc count after b creation = {}\", Rc::strong_count(&a)); println!(\"b initial rc count = {}\", Rc::strong_count(&b)); println!(\"b next item = {:?}\", b.tail()); if let Some(link) = a.tail() { *link.borrow_mut() = Rc::clone(&b); } println!(\"b rc count after changing a = {}\", Rc::strong_count(&b)); println!(\"a rc count after changing a = {}\", Rc::strong_count(&a)); // Uncomment the next line to see that we have a cycle; // it will overflow the stack // println!(\"a next item = {:?}\", a.tail());\n} 예제 15-26: 두 개의 List 값이 서로를 가리키는 순환 참조 생성하기 초깃값 리스트 5, Nil을 가진 List 값을 갖는 Rc 인스턴스를 만들어 a 변수에 넣었습니다. 그다음 Rc 인스턴스에 만들어서 b에 넣었는데, 여기에는 10과 a의 리스트를 가리키고 있는 또 다른 List 값이 있습니다. a를 수정하여 이것이 Nil 대신 b를 가리키도록 하였는데, 이렇게 순환이 만들어집니다. 이는 tail 메서드를 사용하여 a에 있는 RefCell>로부터 참조자를 얻어오는 식으로 이루어졌는데, 이것을 link라는 변수에 넣었습니다. 그다음 RefCell>의 borrow_mut 메서드를 사용하여 Nil 값을 가지고 있는 Rc 내부의 값을 b의 Rc로 바꾸었습니다. 잠시 마지막 println! 문이 실행되지 않도록 주석 처리하고서 이 코드를 실행시키면 아래와 같은 출력을 얻게 됩니다: $ cargo run Compiling cons-list v0.1.0 (file:///projects/cons-list) Finished dev [unoptimized + debuginfo] target(s) in 0.53s Running `target/debug/cons-list`\na initial rc count = 1\na next item = Some(RefCell { value: Nil })\na rc count after b creation = 2\nb initial rc count = 1\nb next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })\nb rc count after changing a = 2\na rc count after changing a = 2 a의 리스트가 b를 가리키도록 변경한 이후 a와 b의 Rc 인스턴스의 참조 카운트는 둘 다 2입니다. main의 끝에서 러스트는 b를 버리는데, 이는 b의 Rc 참조 카운트를 2에서 1로 줄입니다. Rc가 힙에 보유한 메모리는 이 시점에서 해제되지 않을 것인데, 그 이유는 참조 카운트가 0이 아닌 1이기 때문입니다. 그런 다음 러스트는 a를 버리고, 이는 마찬가지로 a의 Rc 인스턴스가 가진 참조 카운트를 2에서 1로 줄입니다. 이 인스턴스의 메모리 또한 버려질 수 없는데, 왜냐하면 이쪽의 Rc 인스턴스도 여전히 무언가를 참조하기 때문입니다. 리스트에 할당된 메모리는 정리되지 않은 채 영원히 남을 것입니다. 이러한 순환 참조를 시각화하기 위해 그림 15-4의 다이어그램을 만들었습니다. 그림 15-4: 리스트 a와 b가 서로를 가리키고 있는 순환 참조 만일 여러분이 마지막 println!의 주석을 해제하고 프로그램을 실행해 보면, 러스트는 a를 가리키고 있는 b를 가리키고 있는 a를 가리키고 있는 등등 스택 오버플로우가 날 때까지 이 순환을 출력하려 할 것입니다. 실제 프로그램과 비교했을 때 이 예제에서의 순환 참조 생성 결과는 그렇게까지 심각하진 않습니다: 순환 참조가 생성된 직후 프로그램이 종료되니까요. 하지만, 더 복잡한 프로그램이 많은 양의 메모리를 순환 참조하여 할당하고 오랜 시간 동안 이걸 가지고 있게 되면, 프로그램은 필요한 양보다 더 많은 메모리를 사용하게 되고 사용 가능한 메모리를 다 써버리게 되어 시스템을 멈추게 할지도 모릅니다. 순환 참조를 만드는 것은 쉽게 이루어지지는 않지만, 불가능한 것도 아닙니다. 만일 여러분이 Rc 값을 가지고 있는 RefCell 혹은 그와 유사하게 내부 가변성 및 참조 카운팅 기능이 있는 타입들의 중첩된 조합을 사용한다면, 여러분이 직접 순환을 만들지 않음을 보장해야 합니다; 이 순환을 찾아내는 것을 러스트에 의지할 수는 없습니다. 순환 참조를 만드는 것은 프로그램의 논리적 버그로서, 자동화된 테스트, 코드 리뷰, 그 외 소프트웨어 개발 연습 등을 통해 최소화해야 할 것입니다. 순환 참조를 피하는 또 다른 해결책은 데이터 구조를 재구성하여 어떤 참조자는 소유권을 갖고 어떤 참조자는 그렇지 않도록 하는 것입니다. 결과적으로 몇 개의 소유권 관계와 몇 개의 소유권 없는 관계로 이루어진 순환을 만들 수 있으며, 소유권 관계들만이 값을 버릴지 말지에 관해 영향을 주게 됩니다. 예제 15-25에서는 Cons 배리언트가 언제나 리스트를 소유하기를 원하므로, 데이터 구조를 재구성하는 것은 불가능합니다. 부모 노드와 자식 노드로 구성된 그래프를 이용한 예제를 살펴보면서 소유권 없는 관계가 순환 참조를 방지하는 적절한 방법이 되는 때가 언제인지 알아봅시다.","breadcrumbs":"스마트 포인터 » 순환 참조는 메모리 누수를 발생시킬 수 있습니다 » 순환 참조 만들기","id":"289","title":"순환 참조 만들기"},"29":{"body":"카고는 단순한 프로젝트에서는 그냥 rustc만 사용할 때와 비교하여 큰 값어치를 못하지만, 프로그램이 더욱 복잡해지면 그 가치를 증명할 것입니다. 여러 개의 파일 혹은 의존성을 필요로 하는 복잡한 프로젝트에서는 카고가 빌드를 조정하게 하는 것이 훨씬 쉽습니다. hello_cargo 프로젝트는 단순하지만, 이미 여러분은 앞으로 러스트를 사용하며 쓰게 될 카고 명령어 중 대부분을 써본 것과 다름없습니다. 실제로 기존에 있던 러스트 프로젝트를 Git으로 가져와서, 해당 디렉터리로 이동하고, 빌드하는 과정은 다음과 같은 명령을 이용하면 됩니다. $ git clone someurl.com/someproject\n$ cd someproject\n$ cargo build 더 자세한 내용은 카고 문서 를 확인하세요.","breadcrumbs":"시작해봅시다 » 카고를 사용해봅시다 » 관례로서의 카고","id":"29","title":"관례로서의 카고"},"290":{"body":"지금까지 Rc::clone을 호출하는 것은 Rc 인스턴스의 strong_count를 증가시키고, Rc 인스턴스는 자신의 strong_count가 0이 된 경우에만 제거되는 것을 보았습니다. Rc::downgrade에 Rc의 참조자를 넣어서 호출하면 Rc 인스턴스 내의 값을 가리키는 약한 참조 (weak reference) 를 만드는 것도 가능합니다. 강한 참조는 Rc 인스턴스의 소유권을 공유할 수 있는 방법입니다. 약한 참조는 소유권 관계를 표현하지 않고, 약한 참조의 개수는 Rc 인스턴스가 제거되는 경우에 영향을 주지 않습니다. 약한 참조가 포함된 순환 참조는 그 값의 강한 참조 개수를 0으로 만드는 순간 깨지게 되기 때문에, 순환 참조를 일으키지 않게 될 것입니다. Rc::downgrade를 호출하면 Weak 타입의 스마트 포인터를 얻게 됩니다. Rc::downgrade는 Rc 인스턴스의 strong_count를 1 증가시키는 대신 weak_count를 1 증가시킵니다. Rc 타입은 strong_count와 유사한 방식으로 weak_count를 사용하여 Weak 참조가 몇 개 있는지 추적합니다. 차이점은 Rc 인스턴스가 제거되기 위해 weak_count가 0일 필요는 없다는 것입니다. Weak가 참조하고 있는 값이 이미 버려졌을지도 모르기 때문에, Weak가 가리키고 있는 값으로 어떤 일을 하기 위해서는 그 값이 여전히 존재하는지를 반드시 확인해야 합니다. 이를 위해 Weak의 upgrade 메서드를 호출하는데, 이 메서드는 Option>를 반환할 것입니다. 만일 Rc 값이 아직 버려지지 않았다면 Some 결과를 얻게 될 것이고 Rc 값이 버려졌다면 None 결괏값을 얻게 될 것입니다. upgrade가 Option를 반환하기 때문에, 러스트는 Some의 경우와 None의 경우가 반드시 처리되도록 할 것이고, 따라서 유효하지 않은 포인터는 없을 것입니다. 예제로 리스트처럼 어떤 아이템이 오직 다음 아이템에 대해서만 알고 있는 데이터 구조 말고, 자식 아이템 그리고 부모 아이템에 대해 모두 알고 있는 아이템을 갖는 트리를 만들어 보겠습니다. 트리 데이터 구조 만들기: 자식 노드를 가진 Node 자기 자식 노드에 대해 알고 있는 노드로 이루어진 트리를 만드는 것으로 시작해 보겠습니다. i32값과 함께 자식 Node 값들의 참조자들도 가지고 있는 Node라는 이름의 구조체를 만들겠습니다: 파일명: src/main.rs use std::cell::RefCell;\nuse std::rc::Rc; #[derive(Debug)]\nstruct Node { value: i32, children: RefCell>>,\n}\n# # fn main() {\n# let leaf = Rc::new(Node {\n# value: 3,\n# children: RefCell::new(vec![]),\n# });\n# # let branch = Rc::new(Node {\n# value: 5,\n# children: RefCell::new(vec![Rc::clone(&leaf)]),\n# });\n# } Node가 자기 자식들을 소유하도록 하고, 이 소유권을 공유하여 트리의 각 Node에 직접 접근할 수 있도록 하고 싶습니다. 이렇게 하기 위해 Vec 아이템이 Rc 타입의 값이 되도록 정의하였습니다. 또한 어떤 노드가 다른 노드의 자식이 되도록 수정하려고 Vec>를 RefCell로 감싼 children을 갖도록 하였습니다. 그다음 예제 15-27처럼 이 구조체 정의를 이용하여 3 값과 자식 노드가 없는 leaf라는 이름의 Node 인스턴스, 그리고 5 값과 leaf를 자식으로 갖는 branch라는 이름의 인스턴스를 만들도록 하겠습니다: 파일명: src/main.rs # use std::cell::RefCell;\n# use std::rc::Rc;\n# # #[derive(Debug)]\n# struct Node {\n# value: i32,\n# children: RefCell>>,\n# }\n# fn main() { let leaf = Rc::new(Node { value: 3, children: RefCell::new(vec![]), }); let branch = Rc::new(Node { value: 5, children: RefCell::new(vec![Rc::clone(&leaf)]), });\n} 예제 15-27: 자식이 없는 leaf 노드와 이 leaf를 자식으로 갖는 branch 노드 만들기 leaf의 Rc를 복제하여 이를 branch 내에 저장했는데, 이는 leaf에 있는 Node의 소유자가 이제 둘이 되었다는 뜻입니다. branch로부터 branch.children를 통하여 leaf까지 접근할 수 있게 되었지만, leaf에서부터 branch로 접근할 방법은 없습니다. 그 원인은 leaf가 branch에 대한 참조자를 가지고 있지 않고 이들 간의 연관성을 알지 못하기 때문입니다. leaf에게 branch가 자신의 부모임을 알려주고 싶습니다. 이걸 다음에 해보겠습니다. 자식에서 부모로 가는 참조자 추가하기 자식 노드가 그의 부모를 알도록 하기 위해서는 parent 필드를 Node 구조체 정의에 추가할 필요가 있겠습니다. 문제는 parent의 타입을 결정하는 데에 있습니다. 여기에 Rc를 넣게 되면 branch를 가리키고 있는 leaf.parent와 leaf를 가리키고 있는 branch.children으로 이루어진 순환 참조를 만들게 되며, 이들의 strong_count값을 결코 0이 되지 않게 하는 원인을 제공할 것이기 때문에, 여기에 Rc를 사용할 수 없음을 알고 있습니다. 이 관계들을 다른 방식으로 생각해 보면, 부모 노드는 그의 자식들을 소유해야 합니다: 즉 만일 부모 노드가 버려지게 되면, 그의 자식 노드들도 또한 버려져야 합니다. 하지만, 자식은 그의 부모를 소유해서는 안 됩니다: 즉 자식 노드가 버려지더라도 그 부모는 여전히 존재해야 합니다. 이것이 바로 약한 참조를 위한 경우입니다! 따라서 Rc 대신 Weak를 이용하여, 특별히 RefCell>를 이용하여 parent의 타입을 만들겠습니다. 이제 Node 구조체 정의는 아래와 같이 생겼습니다: 파일명: src/main.rs use std::cell::RefCell;\nuse std::rc::{Rc, Weak}; #[derive(Debug)]\nstruct Node { value: i32, parent: RefCell>, children: RefCell>>,\n}\n# # fn main() {\n# let leaf = Rc::new(Node {\n# value: 3,\n# parent: RefCell::new(Weak::new()),\n# children: RefCell::new(vec![]),\n# });\n# # println!(\"leaf parent = {:?}\", leaf.parent.borrow().upgrade());\n# # let branch = Rc::new(Node {\n# value: 5,\n# parent: RefCell::new(Weak::new()),\n# children: RefCell::new(vec![Rc::clone(&leaf)]),\n# });\n# # *leaf.parent.borrow_mut() = Rc::downgrade(&branch);\n# # println!(\"leaf parent = {:?}\", leaf.parent.borrow().upgrade());\n# } 노드는 자신의 부모 노드를 참조할 수 있게 되겠지만 그 부모를 소유하지는 않습니다. 예제 15-28에서는 main을 업데이트하여 이 새로운 정의를 사용하도록 해서 leaf 노드가 자기 부모인 branch를 참조할 수 있는 방법을 갖도록 합니다: 파일명: src/main.rs # use std::cell::RefCell;\n# use std::rc::{Rc, Weak};\n# # #[derive(Debug)]\n# struct Node {\n# value: i32,\n# parent: RefCell>,\n# children: RefCell>>,\n# }\n# fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!(\"leaf parent = {:?}\", leaf.parent.borrow().upgrade()); let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!(\"leaf parent = {:?}\", leaf.parent.borrow().upgrade());\n} 예제 15-28: 부모 노드 branch의 약한 참조를 갖는 leaf 노드 leaf 노드를 만드는 것이 parent 필드를 제외하고는 예제 15-27과 비슷해 보입니다: leaf는 부모 없이 시작돼서, 새 비어있는 Weak 참조자 인스턴스를 생성하였습니다. 이 시점에서 upgrade 메서드를 사용하여 leaf의 부모에 대한 참조자를 얻는 시도를 하면 None 값을 얻습니다. 첫 번째 println! 구문에서는 아래와 같은 출력을 보게 됩니다: leaf parent = None branch 노드를 생성할 때도 parent 필드에 새로운 Weak 참조자를 넣었는데, 이는 branch에게는 부모 노드가 없기 때문입니다. leaf는 여전히 branch의 자식 중 하나입니다. 일단 branch의 Node 인스턴스를 갖게 되면, leaf를 수정하여 자기 부모에 대한 Weak 참조자를 갖도록 할 수 있습니다. leaf의 parent 필드의 RefCell>에 있는 borrow_mut 메서드를 사용하고, 그런 다음 Rc::downgrade 함수를 사용하여 branch의 Rc로부터 branch에 대한 Weak 참조자를 생성하였습니다. leaf의 부모를 다시 한번 출력할 때는 branch를 가지고 있는 Some 배리언트를 얻게 될 것입니다: 이제 leaf는 자기 부모에 접근할 수 있습니다! leaf를 출력할 때 예제 15-26에서와 같이 궁극적으로 스택 오버플로우로 끝나버리는 그 순환 문제도 피하게 되었습니다; Weak 참조자는 (Weak)로 출력됩니다: leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },\nchildren: RefCell { value: [Node { value: 3, parent: RefCell { value: (Weak) },\nchildren: RefCell { value: [] } }] } }) 무한 출력이 없다는 것은 이 코드가 순환 참조를 생성하지 않았음을 나타냅니다. 또한 Rc::strong_count와 Rc::weak_count를 호출하여 얻은 값을 살펴보는 것으로도 알 수 있습니다. strong_count와 weak_count의 변화를 시각화하기 새로운 내부 스코프를 만들고 branch의 생성 과정을 이 스코프로 옮겨서 Rc 인스턴스의 strong_count와 weak_count 값이 어떻게 변하는지 살펴봅시다. 그렇게 하면 branch가 만들어질 때와, 그 후 스코프 밖으로 벗어났을 때 어떤 일이 생기는지 알 수 있습니다. 수정본은 예제 15-29와 같습니다: 파일명: src/main.rs # use std::cell::RefCell;\n# use std::rc::{Rc, Weak};\n# # #[derive(Debug)]\n# struct Node {\n# value: i32,\n# parent: RefCell>,\n# children: RefCell>>,\n# }\n# fn main() { let leaf = Rc::new(Node { value: 3, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![]), }); println!( \"leaf strong = {}, weak = {}\", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); { let branch = Rc::new(Node { value: 5, parent: RefCell::new(Weak::new()), children: RefCell::new(vec![Rc::clone(&leaf)]), }); *leaf.parent.borrow_mut() = Rc::downgrade(&branch); println!( \"branch strong = {}, weak = {}\", Rc::strong_count(&branch), Rc::weak_count(&branch), ); println!( \"leaf strong = {}, weak = {}\", Rc::strong_count(&leaf), Rc::weak_count(&leaf), ); } println!(\"leaf parent = {:?}\", leaf.parent.borrow().upgrade()); println!( \"leaf strong = {}, weak = {}\", Rc::strong_count(&leaf), Rc::weak_count(&leaf), );\n} 예제 15-29: 내부 스코프에서 branch를 만들고 강한 참조 카운트와 약한 참조 카운트 시험하기 leaf가 생성된 다음, 이것의 Rc는 강한 참조 카운트 1개와 약한 참조 카운트 0개를 갖습니다. 내부 스코프에서 branch를 만들고 leaf와 연관짓게 되는데, 이때 카운트를 출력하면 branch의 Rc는 강한 참조 카운트 1개와 (Weak로 branch를 가리키는 leaf.parent에 대한) 약한 참조 카운트 1개를 갖고 있을 것입니다. leaf 내의 카운트를 출력하면 강한 참조 카운트 2개를 갖고 있음을 보게 될 것인데, 이는 branch가 이제 branch.children에 저장된 leaf의 Rc에 대한 클론을 가지게 되었지만, 약한 참조는 여전히 0개이기 때문입니다. 내부 스코프가 끝나게 되면, branch는 스코프 밖으로 벗어나게 되고 Rc의 강한 참조 카운트는 0으로 줄어들게 되므로, 이것의 Node는 버려집니다. leaf.parent의 약한 참조 카운트 1개는 Node가 버려질지 말지와는 아무런 관계가 없으므로, 아무런 메모리 누수도 발생하지 않습니다! 만일 이 스코프의 끝부분 뒤에 leaf의 부모에 접근 시도를 한다면 다시 None을 얻게 될 것입니다. 프로그램의 끝부분에서, leaf의 Rc는 강한 참조 카운트 1개와 약한 참조 카운트 0개를 갖고 있는데, 이제 leaf 변수가 다시 Rc에 대한 유일한 참조자이기 때문입니다. 참조 카운트와 값 버리기를 관리하는 모든 로직은 Rc와 Weak, 그리고 이들의 Drop 트레이트에 대한 구현부에 만들어져 있습니다. 자식과 부모의 관계가 Weak 참조자로 있어야 함을 Node의 정의에 특정함으로써, 여러분은 순환 참조와 메모리 누수를 만들지 않으면서 자식 노드를 가리키는 부모 노드 혹은 그 반대의 것을 만들 수 있습니다.","breadcrumbs":"스마트 포인터 » 순환 참조는 메모리 누수를 발생시킬 수 있습니다 » 순환 참조 방지하기: Rc를 Weak로 바꾸기","id":"290","title":"순환 참조 방지하기: Rc를 Weak로 바꾸기"},"291":{"body":"이번 장에서는 러스트가 일반적인 참조자를 가지고 기본적으로 보장하는 것들과는 다른 보장과 절충안을 만들어 내기 위해 스마트 포인터를 사용하는 방법을 다루었습니다. Box 타입은 알려진 크기를 갖고 있고 힙에 할당된 데이터를 가리킵니다. Rc 타입은 힙에 있는 데이터에 대한 참조자의 개수를 추적하여 그 데이터가 여러 개의 소유자를 가질 수 있도록 합니다. 내부 가변성을 갖춘 RefCell 타입은 불변 타입이 필요하지만 그 타입의 내부 값을 변경할 필요가 있을 때 사용할 수 있습니다; 이것은 또한 컴파일 타임 대신 런타임에 대여 규칙을 따르도록 강제합니다. 또한 Deref 및 Drop 트레이트를 다루었는데, 이는 스마트 포인터의 수많은 기능을 활성화해 줍니다. 메모리 누수를 발생시킬 수 있는 순환 참조와, Weak을 이용하여 이를 방지하는 방법도 탐구하였습니다. 이번 장이 여러분의 흥미를 자극하여 직접 여러분만의 스마트 포인터를 구현하고 싶어졌다면, ‘러스토노미콘’ 에서 더 유용한 정보를 확인하세요. 다음에는 러스트의 동시성에 대해 이야기해 보겠습니다. 심지어 몇 가지 새로운 스마트 포인터에 대해서도 배우게 될 것입니다.","breadcrumbs":"스마트 포인터 » 순환 참조는 메모리 누수를 발생시킬 수 있습니다 » 정리","id":"291","title":"정리"},"292":{"body":"안전하고 효율적으로 동시성 프로그래밍을 다루는 것은 러스트의 또 다른 주요 목표 중 하나입니다. 동시성 프로그래밍 (concurrent programming) , 즉 프로그램의 서로 다른 부분이 독립적으로 실행되는 것과, 병렬 프로그래밍 (parallel programming) , 즉 프로그램의 서로 다른 부분이 동시에 실행되는 것은 더 많은 컴퓨터가 여러 개의 프로세서를 활용함에 따라 그 중요성이 커지고 있습니다. 역사적으로, 동시성 및 병렬 컨텍스트에서의 프로그래밍은 어렵고 에러를 내기 쉬웠습니다: 러스트는 이를 바꾸기를 희망합니다. 초기에 러스트 팀은 메모리 안전을 보장하는 것과 동시성 문제를 방지하는 것은 다른 방법으로 해결돼야 하는 별개의 도전 과제라고 생각했습니다. 시간이 흘러 러스트 팀은 소유권과 타입 시스템이 메모리 안전성 및 동시성 문제를 관리하는 것을 돕기 위한 강력한 도구들의 집합이라는 사실을 발견했습니다! 소유권과 타입 검사를 지렛대 삼음으로써, 많은 동시성 에러는 런타임 에러가 아닌 컴파일 타임 에러가 됩니다. 따라서 런타임 동시성 버그가 발생하는 정확한 환경을 재현하는 시도에 많은 시간을 쓰게 되는 것이 아니라, 부정확한 코드가 컴파일되지 않고 문제점을 설명하는 에러가 나타납니다. 결과적으로 잠재적으로 프로덕션에 배포된 이후가 아니라 작업을 하는 동안에 여러분의 코드를 고칠 수 있습니다. 우리는 러스트의 이러한 측면에 겁 없는 동시성 (fearless concurrency) 이란 별명을 지어 주었습니다. 겁 없는 동시성은 미묘한 버그가 없으면서 새로운 버그 생성 없이 리팩터링하기 쉬운 코드를 작성하도록 해줍니다. 노트: 간결성을 위해서 동시성 및/또는 병렬성 이라는 정확한 표현 대신 많은 문제를 동시성 이라고 지칭하겠습니다. 만일 이 책이 동시성 및/또는 병렬성에 대한 것이었다면 더 정확하게 말했을 것입니다. 이번 장에서는 동시성 이라는 단어가 사용될 때마다 마음속으로 동시성 및/또는 병렬성 을 대입해 주세요. 많은 언어가 동시성 문제를 다루기 위해 제공하는 해결책에 대해 독단적입니다. 예를 들어, 얼랭 (Erlang) 은 메시지-패싱 (message-passing) 동시성을 위한 우아한 기능을 가지고 있지만 스레드 간에 상태를 공유하는 방법은 모호합니다. 고수준 언어의 경우 추상화를 위해 일부 제어권을 포기함으로써 얻을 수 있는 이점이 있기 때문에 가능한 해결책 중 일부만을 제공하는 것은 고수준 언어의 합리적인 전략입니다. 하지만 저수준 언어는 주어진 상황 내에서 최고의 성능을 갖는 해결책을 제공하고 하드웨어 대한 추상화가 적을 것으로 기대됩니다. 따라서 러스트는 여러분의 상황과 요구사항에 적합한 방법으로 문제를 모델링하기 위한 다양한 도구들을 제공합니다. 이번 장에서 다루게 될 주제들입니다: 스레드를 생성하여 여러 조각의 코드를 동시에 실행시키는 방법 채널들이 스레드 간에 메시지를 보내는 메시지-패싱 동시성 여러 스레드가 어떤 동일한 데이터에 접근할 수 있는 상태-공유 (shared-state) 동시성 러스트의 동시성 보장을 표준 라이브러리가 제공하는 타입은 물론 사용자 정의 타입으로 확장하는 Sync와 Send 트레이트","breadcrumbs":"겁 없는 동시성 » 겁 없는 동시성","id":"292","title":"겁 없는 동시성"},"293":{"body":"대부분의 최신 운영 체제에서, 실행된 프로그램의 코드는 프로세스 내에서 실행되고, 운영 체제는 한 번에 여러 개의 프로세스를 관리하게 됩니다. 프로그램 내에서도 동시에 실행되는 독립적인 부분들을 가질 수 있습니다. 이러한 독립적인 부분들을 실행하는 기능을 스레드 라 합니다. 예를 들어 웹 서버는 여러 스레드를 가지고 동시에 하나 이상의 요청에 대한 응답을 할 수 있습니다. 여러분의 프로그램 내의 연산을 여러 스레드로 쪼개서 동시에 여러 일을 수행하게 하면 성능을 향상시킬 수 있지만, 프로그램을 복잡하게 만들기도 합니다. 스레드가 동시에 실행될 수 있기 때문에, 서로 다른 스레드에서 실행될 코드 조각들의 실행 순서에 대한 본질적인 보장이 없습니다. 이는 다음과 같은 문제들을 야기할 수 있습니다: 여러 스레드가 일관성 없는 순서로 데이터 혹은 리소스에 접근하게 되는, 경합 조건 (race condition) 두 스레드가 서로를 기다려서 양쪽 스레드 모두 계속 실행되는 것을 막아버리는, 데드록 (deadlock) 특정한 상황에서만 발생하여 안정적으로 재현하고 수정하기 힘든 버그들 러스트는 스레드 사용의 부정적인 효과를 완화하는 시도를 하지만, 멀티스레드 컨텍스트에서의 프로그래밍은 여전히 신중하게 생각해야 하고 싱글스레드로 실행되는 프로그램의 것과는 다른 코드 구조가 필요합니다. 프로그래밍 언어들은 몇 가지 다른 방식으로 스레드를 구현하고, 많은 운영 체제들이 새로운 스레드를 생성하기 위해 해당 언어가 호출할 수 있는 API를 제공합니다. 러스트 표준 라이브러리는 스레드 구현에 대해 1:1 모델을 사용하는데, 이에 따라 프로그램은 하나의 언어 스레드당 하나의 운영 체제 스레드를 사용합니다. 1:1 모델과는 다른 절충안이 있는 그 밖의 스레드 모델을 구현한 크레이트도 있습니다.","breadcrumbs":"겁 없는 동시성 » 스레드를 이용하여 코드를 동시에 실행하기 » 스레드를 이용하여 코드를 동시에 실행하기","id":"293","title":"스레드를 이용하여 코드를 동시에 실행하기"},"294":{"body":"새로운 스레드를 생성하기 위해서는 thread::spawn 함수를 호출하고 여기에 새로운 스레드에서 실행하고 싶은 코드가 담긴 클로저를 넘깁니다. (클로저에 대해서는 13장에서 다뤘습니다.) 예제 16-1의 예제는 메인 스레드에서 어떤 텍스트를 출력하고 새로운 스레드에서는 다른 텍스트를 출력합니다: 파일명: src/main.rs use std::thread;\nuse std::time::Duration; fn main() { thread::spawn(|| { for i in 1..10 { println!(\"hi number {} from the spawned thread!\", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!(\"hi number {} from the main thread!\", i); thread::sleep(Duration::from_millis(1)); }\n} 예제 16-1: 메인 스레드에서 무언가를 출력하는 동안 다른 것을 출력하는 새로운 스레드 생성하기 러스트 프로그램의 메인 스레드가 완료되면 생성된 모든 스레드는 실행이 종료되었든 혹은 그렇지 않든 멈추게 될 것이라는 점을 주의하세요. 이 프로그램의 출력은 매번 약간씩 다를 수 있으나, 아래와 비슷하게 보일 것입니다: hi number 1 from the main thread!\nhi number 1 from the spawned thread!\nhi number 2 from the main thread!\nhi number 2 from the spawned thread!\nhi number 3 from the main thread!\nhi number 3 from the spawned thread!\nhi number 4 from the main thread!\nhi number 4 from the spawned thread!\nhi number 5 from the spawned thread! thread::sleep의 호출은 스레드의 실행을 강제로 잠깐 멈추게 하는데, 다른 스레드는 실행될 수 있도록 합니다. 스레드들은 아마도 교대로 실행될 것이지만, 그게 보장되지는 않습니다: 여러분의 운영 체제가 스레드를 어떻게 스케줄링하는지에 따라 다른 문제입니다. 위의 실행 예에서는 생성된 스레드로부터의 출력 구문이 먼저 나왔음에도 불구하고 메인 스레드가 먼저 출력하였습니다. 그리고 생성된 스레드에게 i가 9일 때까지 출력하라고 했음에도 불구하고, 메인 스레드가 멈추기 전까지 고작 5에 도달했습니다. 만일 이 코드를 실행하고 메인 스레드의 출력만 보았다면, 혹은 어떠한 겹침도 보지 못했다면, 운영 체제에게 스레드 간 전환에 대한 더 많은 기회를 주도록 범위 값을 늘려서 시도해 보세요.","breadcrumbs":"겁 없는 동시성 » 스레드를 이용하여 코드를 동시에 실행하기 » spawn으로 새로운 스레드 생성하기","id":"294","title":"spawn으로 새로운 스레드 생성하기"},"295":{"body":"예제 16-1의 코드는 메인 스레드의 종료 때문에 대체로 생성된 스레드를 조기에 멈출게 할 뿐만 아니라, 스레드들이 실행되는 순서에 대한 보장이 없기 때문에 생성된 스레드가 모든 코드를 실행할 것임을 보장해 줄 수도 없습니다! 생성된 스레드가 실행되지 않거나, 전부 실행되지 않는 문제는 thread::spawn의 반환 값을 변수에 저장함으로써 해결할 수 있습니다. thread::spawn의 반환 타입은 JoinHandle입니다. JoinHandle은 자신의 join 메서드를 호출했을 때 그 스레드가 끝날 때까지 기다리는 소윳값입니다. 예제 16-2는 예제 16-1에서 만들었던 스레드의 JoinHandle을 이용해서 join을 호출하여 main이 끝나기 전에 생성된 스레드가 종료됨을 보장하는 방법을 보여줍니다: 파일명: src/main.rs use std::thread;\nuse std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!(\"hi number {} from the spawned thread!\", i); thread::sleep(Duration::from_millis(1)); } }); for i in 1..5 { println!(\"hi number {} from the main thread!\", i); thread::sleep(Duration::from_millis(1)); } handle.join().unwrap();\n} 예제 16-2: thread::spawn으로부터 JoinHandle을 저장하여 스레드가 완전히 실행되는 것을 보장하기 핸들에 대해 join을 호출하면 핸들에 대한 스레드가 종료될 때까지 현재 실행 중인 스레드를 블록합니다. 스레드를 블록 (Block) 한다는 것은 그 스레드의 작업을 수행하거나 종료되는 것이 방지된다는 뜻입니다. 메인 스레드의 for 루프 이후에 join의 호출을 넣었으므로, 예제 16-2의 실행은 아래와 비슷한 출력을 만들어야 합니다: hi number 1 from the main thread!\nhi number 2 from the main thread!\nhi number 1 from the spawned thread!\nhi number 3 from the main thread!\nhi number 2 from the spawned thread!\nhi number 4 from the main thread!\nhi number 3 from the spawned thread!\nhi number 4 from the spawned thread!\nhi number 5 from the spawned thread!\nhi number 6 from the spawned thread!\nhi number 7 from the spawned thread!\nhi number 8 from the spawned thread!\nhi number 9 from the spawned thread! 두 스레드가 계속하여 교차하지만, handle.join()의 호출로 인하여 메인 스레드는 기다리고 생성된 스레드가 종료되기 전까지 끝나지 않습니다. 그런데 만일 아래와 같이 main의 for 루프 이전으로 handle.join()을 이동시키면 어떤 일이 생기는지 봅시다: 파일명: src/main.rs use std::thread;\nuse std::time::Duration; fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!(\"hi number {} from the spawned thread!\", i); thread::sleep(Duration::from_millis(1)); } }); handle.join().unwrap(); for i in 1..5 { println!(\"hi number {} from the main thread!\", i); thread::sleep(Duration::from_millis(1)); }\n} 메인 스레드는 생성된 스레드가 종료될 때까지 기다릴 것이고 그다음 자신의 for 루프를 실행하게 되어, 아래처럼 출력값이 더 이상 교차하지 않을 것입니다: hi number 1 from the spawned thread!\nhi number 2 from the spawned thread!\nhi number 3 from the spawned thread!\nhi number 4 from the spawned thread!\nhi number 5 from the spawned thread!\nhi number 6 from the spawned thread!\nhi number 7 from the spawned thread!\nhi number 8 from the spawned thread!\nhi number 9 from the spawned thread!\nhi number 1 from the main thread!\nhi number 2 from the main thread!\nhi number 3 from the main thread!\nhi number 4 from the main thread! join이 호출되는 위치처럼 작은 세부 사항도 스레드가 동시에 실행되는지의 여부에 영향을 미칠 수 있습니다.","breadcrumbs":"겁 없는 동시성 » 스레드를 이용하여 코드를 동시에 실행하기 » join 핸들을 사용하여 모든 스레드가 끝날 때까지 기다리기","id":"295","title":"join 핸들을 사용하여 모든 스레드가 끝날 때까지 기다리기"},"296":{"body":"move 클로저는 thread::spawn에 넘겨지는 클로저와 함께 자주 사용되는데, 그렇게 하면 클로저가 환경으로부터 사용하는 값의 소유권을 갖게 되어 한 스레드에서 다른 스레드로 소유권이 이동될 것이기 때문입니다. 13장의 ‘참조자를 캡처하거나 소유권 이동하기’ 절에서 클로저의 컨텍스트에서의 move에 대해 다루었습니다. 지금은 move와 thread::spawn 사이의 상호작용에 더 집중해 보겠습니다. 예제 16-1에서 'thread::spawn'에 전달된 클로저에는 어떤 인수도 없음을 주목하세요: 생성된 스레드의 코드에서는 메인 스레드로부터 온 어떤 데이터도 이용하고 있지 않습니다. 메인 스레드의 데이터를 생성된 스레드에서 사용하기 위해, 생성된 스레드의 클로저는 자신이 필요로 하는 값을 캡처해야 합니다. 예제 16-3은 메인 스레드에서 벡터를 생성하여 이를 생성된 스레드 내에서 사용하는 시도를 보여주고 있습니다. 그러나 잠시 후에 보시게 될 것처럼 아직은 동작하지 않습니다. 파일명: src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(|| { println!(\"Here's a vector: {:?}\", v); }); handle.join().unwrap();\n} 예제 16-3: 메인 스레드에서 생성된 벡터에 대한 다른 스레드에서의 사용 시도 클로저가 v를 사용하므로, v는 캡처되어 클로저 환경의 일부가 됩니다. thread::spawn이 이 클로저를 새로운 스레드에서 실행하므로, v는 새로운 스레드 내에서 접근 가능해야 합니다. 하지만 이 예제를 컴파일하면 아래와 같은 에러를 얻게 됩니다: $ cargo run Compiling threads v0.1.0 (file:///projects/threads)\nerror[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function --> src/main.rs:6:32 |\n6 | let handle = thread::spawn(|| { | ^^ may outlive borrowed value `v`\n7 | println!(\"Here's a vector: {:?}\", v); | - `v` is borrowed here |\nnote: function requires argument type to outlive `'static` --> src/main.rs:6:18 |\n6 | let handle = thread::spawn(|| { | __________________^\n7 | | println!(\"Here's a vector: {:?}\", v);\n8 | | }); | |______^\nhelp: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword |\n6 | let handle = thread::spawn(move || { | ++++ For more information about this error, try `rustc --explain E0373`.\nerror: could not compile `threads` due to previous error 러스트는 v를 어떻게 캡처할지 추론하고 , println!이 v의 참조자만 필요로 하기 때문에, 클로저는 v를 빌리려고 합니다. 하지만 문제가 있습니다: 러스트는 생성된 스레드가 얼마나 오랫동안 실행될지 알 수 없으므로, v에 대한 참조자가 항상 유효할 것인지 알지 못합니다. 예제 16-4는 유효하지 않은 v의 참조자가 있을 가능성이 더 높은 시나리오를 제공합니다: 파일명: src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(|| { println!(\"Here's a vector: {:?}\", v); }); drop(v); // 오, 이런! handle.join().unwrap();\n} 예제 16-4: v를 버리는 메인 스레드로부터 v에 대한 참조자를 캡처하려 하는 클로저를 갖는 스레드 만약 러스트가 이 코드의 실행을 허용했다면, 생성된 스레드가 전혀 실행되지 않고 즉시 백그라운드에 들어갔을 가능성이 있습니다. 생성된 스레드는 내부에 v의 참조자를 가지고 있지만, 메인 스레드는 15장에서 다루었던 drop 함수를 사용하여 v를 즉시 버립니다. 그러면 생성된 스레드가 실행되기 시작할 때 v가 더 이상 유효하지 않게 되어, 그에 대한 참조자 또한 유효하지 않게 됩니다. 이런! 예제 16-3의 컴파일 에러를 고치기 위해서 에러 메시지의 조언을 이용할 수 있습니다: help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword |\n6 | let handle = thread::spawn(move || { | ++++ move 키워드를 클로저 앞에 추가함으로써 러스트가 값을 빌려와야 된다고 추론하도록 하는 것이 아니라 사용 중인 값의 소유권을 강제로 가지도록 합니다. 예제 16-3을 예제 16-5처럼 수정하면 컴파일되어 의도한 대로 실행됩니다: 파일명: src/main.rs use std::thread; fn main() { let v = vec![1, 2, 3]; let handle = thread::spawn(move || { println!(\"Here's a vector: {:?}\", v); }); handle.join().unwrap();\n} 예제 16-5: move 키워드를 이용하여 클로저가 사용하는 값의 소유권을 갖도록 강제하기 move 클로저를 사용하여 메인 스레드에서 drop을 호출하는 예제 16-4의 코드를 고치려고 시도해 보고 싶을 수도 있습니다. 하지만 이 수정은 동작하지 않는데, 그 이유는 예제 16-4가 시도하고자 하는 것이 다른 이유로 허용되지 않기 때문입니다. 만일 클로저에 move를 추가하면, v를 클로저의 환경으로 이동시킬 것이고, 더 이상 메인 스레드에서 이것에 대한 drop 호출을 할 수 없게 됩니다. 대신 아래와 같은 컴파일 에러를 얻게 됩니다: $ cargo run Compiling threads v0.1.0 (file:///projects/threads)\nerror[E0382]: use of moved value: `v` --> src/main.rs:10:10 |\n4 | let v = vec![1, 2, 3]; | - move occurs because `v` has type `Vec`, which does not implement the `Copy` trait\n5 |\n6 | let handle = thread::spawn(move || { | ------- value moved into closure here\n7 | println!(\"Here's a vector: {:?}\", v); | - variable moved due to use in closure\n...\n10 | drop(v); // 오, 이런! | ^ value used here after move For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `threads` due to previous error 러스트의 소유권 규칙이 우리를 또다시 구해주었습니다! 예제 16-3의 코드에서 에러가 발생한 이유는 러스트가 보수적으로 스레드에 대해 v만 빌려왔기 때문이었는데, 이는 메인 스레드가 이론적으로 생성된 스레드의 참조자를 무효화할 수 있음을 의미합니다. 러스트에게 v의 소유권을 생성된 스레드로 이동시키라고 함으로써, 메인 스레드가 v를 더 이상 이용하지 않음을 러스트에게 보장하고 있습니다. 만일 예제 16-4를 같은 방식으로 바꾸면, v를 메인 스레드에서 사용하려고 할 때 소유권 규칙을 위반하게 됩니다. move 키워드는 러스트의 대여에 대한 보수적인 기본 기준을 무효화합니다; 즉 소유권 규칙을 위반하지 않도록 해줍니다. 스레드와 스레드 API에 대한 기본적인 이해를 바탕으로, 스레드로 할 수 있는 것들을 살펴봅시다.","breadcrumbs":"겁 없는 동시성 » 스레드를 이용하여 코드를 동시에 실행하기 » 스레드에 move 클로저 사용하기","id":"296","title":"스레드에 move 클로저 사용하기"},"297":{"body":"안전한 동시성을 보장하기 위해 인기가 오르고 있는 접근법 중에는 메시지 패싱 (message passing) 이 있는데, 이는 스레드들 혹은 액터들이 서로 데이터를 담은 메시지를 보내서 통신하는 것입니다. Go 언어 문서 의 슬로건에 있는 아이디어는 다음과 같습니다: ‘메모리를 공유하여 통신하지 마세요; 그 대신, 통신하여 메모리를 공유하세요.’ 메시지 보내기 동시성을 달성하기 위해서 러스트 표준 라이브러리는 채널 (channel) 구현체를 제공합니다. 채널은 한 스레드에서 다른 쪽으로 데이터를 보내기 위한 일반적인 프로그래밍 개념입니다. 프로그래밍에서의 채널은 개울이나 강처럼 방향이 정해져 있는 물줄기와 비슷하다고 상상할 수 있겠습니다. 강에 고무 오리 같은 것을 띄우면, 물길의 끝까지 하류로 여행하게 되겠지요. 채널은 둘로 나뉘어 있습니다: 바로 송신자 (transmitter) 와 수신자 (receiver) 입니다. 송신자 측은 여러분이 강에 고무 오리를 띄우는 상류 위치이고, 수신자 측은 하류에 고무 오리가 도달하는 곳입니다. 코드의 어떤 곳에서 보내고자 하는 데이터와 함께 송신자의 메서드를 호출하면, 다른 곳에서는 도달한 메시지에 대한 수신 종료를 검사합니다. 송신자 혹은 송신자가 버려지면 채널이 닫혔다 (closed) 라고 말합니다. 여기서는 값을 생성하여 채널로 내려보내는 한 스레드와, 값을 받아서 이를 출력하는 또 다른 스레드로 이루어진 프로그램을 만들어 보겠습니다. 기능을 설명하기 위해서 채널을 사용하여 스레드 간에 단순한 값들을 보내려고 합니다. 이 기법에 익숙해지고 나면, 채팅 시스템이나 다수의 스레드가 계산의 일부분을 수행하여 그 결과를 종합하는 스레드에 보내는 시스템과 같이 서로 통신이 필요한 스레드에 채널을 이용할 수 있습니다. 먼저 예제 16-6에서는 채널을 만들지만 이걸로 아무것도 하지 않을 것입니다. 채널을 통해 보내려는 값의 타입이 무엇인지 러스트가 알지 못하므로 이 코드가 아직 컴파일되지 않는다는 점을 주의하세요. 파일명: src/main.rs use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel();\n} 예제 16-6: 채널을 생성하여 각 절반을 tx와 rx에 할당하기 mpsc::channel 함수를 사용하여 새로운 채널을 생성합니다; mpsc는 복수 생산자, 단일 소비자 (multiple producer, single consumer) 를 나타냅니다. 짧게 줄이면, 러스트의 표준 라이브러리가 채널을 구현한 방법은 한 채널이 값을 생산하는 송신 단말을 여러 개 가질 수 있지만 값을 소비하는 수신 단말은 단 하나만 가질 수 있음을 의미합니다. 하나의 큰 강으로 함께 흐르는 여러 개울을 상상해 보세요: 아무 개울에나 흘려보낸 모든 것은 끝내 하나의 강에서 끝날 것입니다. 지금은 단일 생산자를 가지고 시작하겠지만, 이 예제가 동작하기 시작하면 여러 생산자를 추가할 것입니다. mpsc::channel 함수는 튜플을 반환하는데, 첫 번째 요소는 송신 단말이고 두 번째 요소는 수신 단말입니다. tx와 rx라는 약어는 많은 분야에서 각각 송신자 (transmitter) 와 수신자 (receiver) 에 사용되므로, 각각의 단말을 나타내기 위해 그렇게 변수명을 지었습니다. 튜플을 해체하는 패턴과 함께 let 구문이 사용되고 있습니다; let 구문 내에서의 패턴의 사용과 해체에 대해서는 18장에서 다룰 것입니다. 지금은 이런 방식으로 let 구문을 사용하는 것이 mpsc::channel에 의해 반환된 튜플의 조각들을 추출하는 데 편리한 접근법이라고만 알아둡시다. 예제 16-7과 같이 송신 단말을 생성된 스레드로 이동시키고 하나의 문자열을 전송하게 하여 생성된 스레드가 메인 스레드와 통신하도록 해봅시다. 이는 강 상류에 고무 오리를 띄우는 것 혹은 한 스레드에서 다른 스레드로 채팅 메시지를 보내는 것과 비슷합니다. 파일명: src/main.rs use std::sync::mpsc;\nuse std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from(\"hi\"); tx.send(val).unwrap(); });\n} 예제 16-7: tx를 생성된 스레드로 이동시키고 ‘hi’를 보내기 다시 한번 thread::spawn을 이용하여 새로운 스레드를 생성한 뒤 move를 사용하여 tx를 클로저로 이동시켜 생성된 스레드가 tx를 소유하도록 합니다. 생성된 스레드는 채널을 통해 메시지를 보낼 수 있도록 하기 위해 채널의 송신 단말을 소유할 필요가 있습니다. 송신 단말에는 보내려는 값을 입력받는 send 메서드가 있습니다. send 메서드는 Result 타입을 반환하므로, 수신 단말이 이미 버려져 값을 보낼 곳이 없을 경우 송신 연산은 에러를 반환할 것입니다. 이 예제에서는 unwrap을 호출하여 에러가 나는 경우 패닉을 일으키고 중입니다. 그러나 실제 애플리케이션에서는 이를 적절히 처리해야 할 것입니다: 적절한 에러 처리를 위한 전략을 다시 보려면 9장으로 돌아가세요. 예제 16-8에서는 메인 스레드에 있는 채널의 수신 단말로부터 값을 받을 것입니다. 이는 강의 끝물에서 고무 오리를 건져 올리는 것 혹은 채팅 메시지를 받는 것과 비슷합니다. 파일명: src/main.rs use std::sync::mpsc;\nuse std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from(\"hi\"); tx.send(val).unwrap(); }); let received = rx.recv().unwrap(); println!(\"Got: {}\", received);\n} 예제 16-8: 메인 스레드에서 ‘hi’ 값을 받아 출력하기 수신자는 두 개의 유용한 메서드를 가지고 있습니다: recv와 try_recv입니다. 여기서는 수신 (receive) 의 줄임말인 recv를 사용하고 있는데, 이는 메인 스레드의 실행을 블록시키고 채널로부터 값을 받을 때까지 기다릴 것입니다. 일단 값을 받으면, recv는 이것을 Result로 반환할 것입니다. 채널의 송신 단말이 닫히면, recv는 더 이상 어떤 값도 오지 않을 것이란 신호를 주기 위해 에러를 반환하게 됩니다. try_recv 메서드는 블록하지 않는 대신 즉시 Result를 반환할 것입니다: 전달받은 메시지가 있다면 이를 담고 있는 Ok 값을, 지금 시점에서 메시지가 없다면 Err 값을 반환합니다. try_recv의 사용은 메시지를 기다리는 동안 다른 작업을 해야 할 때 유용합니다: try_recv를 매번 호출하는 루프를 작성하여 메시지가 있으면 이를 처리하고, 그렇지 않으면 다음번 검사 때까지 잠시 다른 일을 할 수 있습니다. 이 예제에서는 간소화를 위해 recv를 이용했습니다; 이 메인 스레드에서는 메시지를 기다리는 동안 해야 할 다른 일이 없으므로, 메인 스레드를 블록시키는 것이 적절합니다. 예제 16-8의 코드를 실행하면, 메인 스레드로부터 출력된 값을 보게 될 것입니다: Got: hi 완벽하군요!","breadcrumbs":"겁 없는 동시성 » 메시지 패싱을 사용하여 스레드 간 데이터 전송하기 » 메시지 패싱을 사용하여 스레드 간 데이터 전송하기","id":"297","title":"메시지 패싱을 사용하여 스레드 간 데이터 전송하기"},"298":{"body":"소유권 규칙은 메시지 전송에서 안전하면서 동시적인 코드를 작성하는데 중요한 역할을 합니다. 동시성 프로그래밍 내에서의 에러 방지는 러스트 프로그램 전체에서 소유권을 고려할 경우 얻을 수 있는 이점입니다. 실험을 통해 채널과 소유권이 함께 동작하는 것이 어떤 식으로 문제를 방지하는지 알아봅시다: 채널로 val 값을 보낸 이후에 생성된 스레드에서 이 값을 사용하는 시도를 해보겠습니다. 예제 16-9의 코드를 컴파일하여 이 코드가 왜 허용되지 않는지를 보세요: 파일명: src/main.rs use std::sync::mpsc;\nuse std::thread; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from(\"hi\"); tx.send(val).unwrap(); println!(\"val is {}\", val); }); let received = rx.recv().unwrap(); println!(\"Got: {}\", received);\n} 예제 16-9: val을 채널로 보낸 뒤 이에 대한 사용 시도 여기서는 tx.send를 통하여 채널에 val을 보낸 뒤 이를 출력하는 시도를 하였습니다. 이를 허용하는 것은 나쁜 생각입니다: 일단 값이 다른 스레드로 보내지고 나면, 그 값을 다시 사용하려고 하기 전에 값을 받은 스레드에서 수정되거나 버려질 수 있습니다. 잠재적으로, 다른 스레드에서의 수정은 불일치하거나 존재하지 않는 데이터로 인하여 에러 혹은 예상치 못한 결과를 야기할 수 있습니다. 하지만 러스트에서는 예제 16-9 코드의 컴파일 시도를 하면 에러가 납니다: $ cargo run Compiling message-passing v0.1.0 (file:///projects/message-passing)\nerror[E0382]: borrow of moved value: `val` --> src/main.rs:10:31 |\n8 | let val = String::from(\"hi\"); | --- move occurs because `val` has type `String`, which does not implement the `Copy` trait\n9 | tx.send(val).unwrap(); | --- value moved here\n10 | println!(\"val is {}\", val); | ^^^ value borrowed here after move | = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `message-passing` due to previous error 동시성에 관한 실수가 컴파일 타임 에러를 만들었습니다. send 함수가 그 매개변수의 소유권을 가져가고, 이 값이 이동되면, 수신자가 이에 대한 소유권을 얻습니다. 이는 값을 보낸 이후에 우발적으로 이 값을 다시 사용하는 것을 방지합니다; 소유권 시스템은 모든 것이 정상인지 확인합니다.","breadcrumbs":"겁 없는 동시성 » 메시지 패싱을 사용하여 스레드 간 데이터 전송하기 » 채널과 소유권 이동","id":"298","title":"채널과 소유권 이동"},"299":{"body":"예제 16-8의 코드는 컴파일되고 실행도 되지만, 두 개의 분리된 스레드가 채널을 통해 서로 대화를 했는지 우리에게 명확히 보여주진 못했습니다. 예제 16-10에서는 예제 16-8의 코드가 동시에 실행됨을 입증해 줄 수정본을 만들었습니다: 이제 생성된 스레드가 여러 메시지를 보내면서 각 메시지 사이에 1초씩 잠깐 멈출 것입니다. 파일명: src/main.rs use std::sync::mpsc;\nuse std::thread;\nuse std::time::Duration; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let vals = vec![ String::from(\"hi\"), String::from(\"from\"), String::from(\"the\"), String::from(\"thread\"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!(\"Got: {}\", received); }\n} 예제 16-10: 여러 메시지를 보내고 각각마다 멈추기 이번에는 생성된 스레드가 메인 스레드로 보내고 싶은 문자열의 벡터를 가지고 있습니다. 문자열마다 반복하여 각각의 값을 개별적으로 보내고, Duration 값에 1초를 넣어서 thread::sleep 함수를 호출하는 것으로 각각의 사이에 멈춥니다. 메인 스레드에서는 더 이상 recv 함수를 명시적으로 호출하지 않고 있습니다: 대신 rx를 반복자처럼 다루고 있습니다. 각각 수신된 값에 대해서 이를 출력합니다. 채널이 닫힐 때는 반복이 종료될 것입니다. 예제 16-10의 코드를 실행시키면 다음과 같은 출력이 각 줄마다 1초씩 멈추면서 보일 것입니다: Got: hi\nGot: from\nGot: the\nGot: thread 메인 스레드의 for 루프 내에는 어떠한 멈춤 혹은 지연 코드를 넣지 않았으므로, 메인 스레드가 생성된 스레드로부터 값을 전달받는 것을 기다리는 중임을 알 수 있습니다.","breadcrumbs":"겁 없는 동시성 » 메시지 패싱을 사용하여 스레드 간 데이터 전송하기 » 여러 값 보내기와 수신자가 기다리는지 알아보기","id":"299","title":"여러 값 보내기와 수신자가 기다리는지 알아보기"},"3":{"body":"러스트는 다양한 사람들에게 이상적입니다. 이유도 각각 다양하나, 대표적인 몇 가지 경우를 살펴보도록 하겠습니다.","breadcrumbs":"소개 » 러스트는 누구에게 적합할까요?","id":"3","title":"러스트는 누구에게 적합할까요?"},"30":{"body":"여러분은 이미 러스트 여정의 위대한 시작을 한 발 내디뎠습니다! 이번 장에서 배운 내용은 다음과 같습니다: rustup 으로 최신 stable 버전 러스트를 설치하기 러스트를 새 버전으로 업데이트하기 로컬 설치된 문서 열어보기 직접 rustc를 사용해 ‘Hello, world!’ 프로그램을 작성하고 실행해 보기 일반적인 카고의 사용법으로 프로젝트를 생성하고 실행하기 지금이 좀 더 실질적인 프로그램을 만들어 코드를 읽고 쓰는 데 익숙해지기 좋은 타이밍입니다. 그리하여 2장은 추리 게임 프로그램을 만들어 보겠습니다. 러스트에서 사용되는 보편적인 프로그래밍 개념부터 살펴보실 분들은 3장부터 읽고 2장을 읽는 것도 나쁘지 않습니다.","breadcrumbs":"시작해봅시다 » 카고를 사용해봅시다 » 정리","id":"30","title":"정리"},"300":{"body":"이전에 mpsc가 복수 생산자, 단일 소비자 (multiple producer, single consumer) 의 약어라는 것을 언급했었지요. mpsc를 예제 16-10의 코드에 적용하여 모두 동일한 수신자로 값들을 보내는 여러 스레드를 만들도록 코드를 확장해 봅시다. 예제 16-11처럼 채널의 송신자를 복제하면 그렇게 할 수 있습니다: 파일명: src/main.rs # use std::sync::mpsc;\n# use std::thread;\n# use std::time::Duration;\n# # fn main() { // --생략-- let (tx, rx) = mpsc::channel(); let tx1 = tx.clone(); thread::spawn(move || { let vals = vec![ String::from(\"hi\"), String::from(\"from\"), String::from(\"the\"), String::from(\"thread\"), ]; for val in vals { tx1.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); thread::spawn(move || { let vals = vec![ String::from(\"more\"), String::from(\"messages\"), String::from(\"for\"), String::from(\"you\"), ]; for val in vals { tx.send(val).unwrap(); thread::sleep(Duration::from_secs(1)); } }); for received in rx { println!(\"Got: {}\", received); } // --생략--\n# } 예제 16-11: 여러 개의 생산자로부터 여러 메시지 보내기 이번에는 첫 번째로 생성된 스레드를 생성하기 전에, 채널의 송신 단말에 대해 clone을 호출했습니다. 이는 첫 번째로 생성된 스레드로 전달할 수 있는 새로운 송신 핸들을 제공해 줄 것입니다. 두 번째로 생성된 스레드에게는 원래의 채널 송신 단말을 전달합니다. 이렇게 다른 메시지를 하나의 수신 단말로 보내주는 두 스레드를 만듭니다. 이 코드를 실행시키면, 다음과 같은 출력과 비슷하게 보여야 합니다: Got: hi\nGot: more\nGot: from\nGot: messages\nGot: for\nGot: the\nGot: thread\nGot: you 값들의 순서는 여러분의 시스템에 따라 다르게 보일 수도 있습니다. 이것이 바로 동시성을 흥미롭게 만들 뿐만 아니라 어렵게 만드는 것입니다. 만일 여러분이 thread::sleep을 가지고 실험하면서 서로 다른 스레드마다 다양한 값을 썼다면, 매번의 실행이 더욱 비결정적이고 매번 다른 출력을 생성할 것입니다. 이제 채널이 동작하는 방식을 알아봤으니, 동시성의 다른 방법을 알아봅시다.","breadcrumbs":"겁 없는 동시성 » 메시지 패싱을 사용하여 스레드 간 데이터 전송하기 » 송신자를 복제하여 여러 생산자 만들기","id":"300","title":"송신자를 복제하여 여러 생산자 만들기"},"301":{"body":"메시지 패싱은 동시성을 다루는 좋은 방법이지만, 유일한 수단은 아닙니다. 또다른 방법은 여러 스레드가 동일한 공유 데이터에 접근하는 것입니다. 고 (Go) 언어 문서로부터 나온 슬로건의 일부를 다시 한번 생각해 보세요: ‘메모리를 공유하여 통신하지 마세요.’ 메모리를 공유하는 통신은 어떻게 생겼을까요? 더불어서 메시지 패싱 애호가들은 왜 메모리 공유를 쓰지 말라고 경고할까요? 어떤 면에서, 모든 프로그래밍 언어의 채널들은 단일 소유권과 유사한데, 이는 값이 채널로 송신되면, 그 값은 더 이상 쓸 수 없게 되기 때문입니다. 공유 메모리 동시성은 복수 소유권과 유사합니다: 여러 스레드들이 동시에 동일한 메모리 위치를 접근할 수 있지요. 스마트 포인터가 복수 소유권을 가능하게 하는 내용을 담은 15장에서 보셨듯이, 복수 소유권은 서로 다른 소유자들에 대한 관리가 필요하기 때문에 더 복잡할 수 있습니다. 러스트의 타입 시스템과 소유권 규칙은 이러한 관리가 올바르도록 훌륭히 도와줍니다. 예를 들면, 공유 메모리를 위한 더 일반적인 동시성 기초 재료 중 하나인 뮤텍스 (mutex) 를 살펴봅시다.","breadcrumbs":"겁 없는 동시성 » 공유 상태 동시성 » 공유 상태 동시성","id":"301","title":"공유 상태 동시성"},"302":{"body":"뮤텍스 는 상호 배제 (mutual exclusion) 의 줄임말로, 뮤텍스에서는 한번에 하나의 스레드만 데이터 접근을 허용합니다. 뮤텍스 내부의 데이터에 접근하려면 스레드는 먼저 뮤텍스의 락 (lock) 을 얻는 요청을 해서 접근을 희망하는 신호를 보내야 합니다. 락은 누가 현재 배타적으로 데이터에 접근하는지 추적하는 뮤텍스의 일부에 해당하는 데이터 구조입니다. 그러므로, 뮤텍스는 잠금 시스템을 통해 가지고 있는 데이터를 보호하는 (guard) 것으로 묘사됩니다. 뮤텍스는 사용하기 어렵다는 평판이 있는데 이는 다음 두 가지 규칙을 기억해야 하기 때문입니다: 데이터를 사용하기 전에는 반드시 락을 얻는 시도를 해야 합니다. 만일 뮤텍스가 보호하는 데이터의 사용이 끝났다면, 반드시 언락을 해야 다른 스레드들이 락을 얻을 수 있습니다. 뮤텍스에 대한 실제 세계에서의 비유를 위해서, 마이크가 딱 하나만 있는 컨퍼런스 패널 토의를 상상해보세요. 패널 참가자들이 말하기 전, 그들은 마이크 사용을 원한다고 요청하거나 신호를 줘야 합니다. 마이크를 얻었을 때는 원하는 만큼 길게 말한 다음, 말하기를 원하는 다음 패널 참가자에게 마이크를 건네줍니다. 만일 패널 참여자가 마이크 사용을 끝냈을 때 이를 건네주는 것을 잊어먹는다면, 그 외 아무도 말할 수 없게 됩니다. 공유하는 마이크의 관리가 잘못되면, 패널 토의는 계획대로 진행되지 않을 겁니다! 뮤텍스의 관리를 올바르게 하려면 믿을 수 없을만큼 까다로울 수 있는데, 이것이 바로 많은 사람들이 채널 애호가가 되는 이유입니다. 하지만, 러스트의 타입 시스템과 소유권 규칙에 덕분에 락과 언락이 잘못 될 수는 없습니다. Mutex의 API 뮤텍스 사용 방법에 대한 예제로, 예제 16-12처럼 싱글스레드 컨텍스트에서 뮤텍스를 사용하는 것으로 시작해봅시다: 파일명: src/main.rs use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut num = m.lock().unwrap(); *num = 6; } println!(\"m = {:?}\", m);\n} 예제 16-12: 간소화를 위해 싱글스레드 컨텍스트에서 Mutex의 API 탐색하기 많은 타입이 그렇듯 Mutex는 연관 함수 new를 사용하여 만들어집니다. 뮤텍스 내의 데이터에 접근하기 위해서는 lock 메서드를 사용하여 락을 얻습니다. 이 호출은 현재의 스레드를 블록할 것이므로, 락을 얻을 차례가 될 때까지 아무런 작업도 할 수 없습니다. lock의 호출은 락을 가진 다른 스레드가 패닉 상태인 경우 실패할 것입니다. 그런 경우 아무도 락을 얻을 수 없게 되므로, unwrap을 택하여 그런 상황일 경우 이 스레드에 패닉을 일으킵니다. 락을 얻고난 후에는 그 반환 값, 지금의 경우 num이라는 이름의 값을 내부 데이터에 대한 가변 참조자로 취급할 수 있습니다. 타입 시스템은 m 내부의 값을 사용하기 전에 락을 얻도록 보장합니다. Mutex는 i32가 아니므로 i32 값을 사용하기 위해서는 반드시 락을 얻어야 합니다. 잊어먹을 수가 없습니다; 잊어버린다면 타입 시스템이 내부의 i32에 접근할 수 없게 할 것입니다. 짐작하셨을지 모르겠지만 Mutex는 스마트 포인터입니다. 더 정확하게는 lock의 호출이 MutexGuard라는 스마트 포인터를 반환하는데, unwrap 호출을 통해 처리되는 LockResult로 감싸져 있습니다. MutexGuard 스마트 포인터는 내부 데이터를 가리키도록 Deref가 구현되어 있습니다; 또한 MutexGuard 스마트 포인터에는 Drop 구현체가 있는데, 이것으로 내부 스코프의 끝에서 스코프 밖으로 벗어났을 때 자동으로 락을 해제하는 일이 벌어집니다. 결과적으로 락이 자동으로 해제되기 때문에, 락을 해제하는 것을 잊어버려 다른 스레드에서 뮤텍스가 사용되지 못하게 차단될 위험이 없습니다. 락이 버려진 후에는 뮤텍스 값을 출력하여 내부의 i32를 6으로 바꿀 수 있음을 확인할 수 있습니다. 여러 스레드 사이에서 Mutex 공유하기 이제 Mutex를 사용하여 여러 스레드 사이에서 값을 공유하는 시도를 해봅시다. 10개의 스레드를 생성하고 각자 카운터 값을 1씩 증가시켜서 카운터가 0에서 10으로 가도록 할 것입니다. 다음 예제 16-13는 컴파일 에러가 날 것이고, 이 에러를 이용하여 Mutex를 사용하는 방법과 러스트가 이를 고치는 것을 돕는 방법에 대해 학습하겠습니다. 파일명: src/main.rs use std::sync::Mutex;\nuse std::thread; fn main() { let counter = Mutex::new(0); let mut handles = vec![]; for _ in 0..10 { let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!(\"Result: {}\", *counter.lock().unwrap());\n} 예제 16-13: Mutex에 의해 보호되는 카운터를 각자 증가시키는 10개의 스레드 예제 16-12에서 했던 것처럼 Mutex 내부에 i32를 담고 있는 counter 변수를 만듭니다. 다음으로 숫자 범위만큼 반복하여 10개의 스레드를 만듭니다. thread::spawn을 사용하고 모든 스레드에게 동일한 클로저를 주었습니다: 이 클로저는 카운터를 스레드로 이동시키고, lock 메서드를 호출하여 Mutex의 락을 얻은 다음, 뮤텍스 내의 값을 1만큼 증가시킵니다. 스레드가 자신의 클로저 실행을 끝냈을 때, num은 스코프 밖으로 벗어내고 락이 해제되어 다른 스레드가 이를 얻을 수 있습니다. 메인 스레드에서는 조인 핸들을 전부 모읍니다. 그리고나서 예제 16-2에서처럼 각 핸들에 join을 호출하여 모든 스레드가 종료되는 것을 확실히 합니다. 그 시점에서 메인 스레드는 락을 얻고 이 프로그램의 결과를 출력합니다. 이 예제가 컴파일되지 않을 것이라고 암시했었죠. 이제 왜 그런지 알아봅시다! $ cargo run Compiling shared-state v0.1.0 (file:///projects/shared-state)\nerror[E0382]: use of moved value: `counter` --> src/main.rs:9:36 |\n5 | let counter = Mutex::new(0); | ------- move occurs because `counter` has type `Mutex`, which does not implement the `Copy` trait\n...\n9 | let handle = thread::spawn(move || { | ^^^^^^^ value moved into closure here, in previous iteration of loop\n10 | let mut num = counter.lock().unwrap(); | ------- use occurs due to use in closure For more information about this error, try `rustc --explain E0382`.\nerror: could not compile `shared-state` due to previous error 이 에러 메시지는 counter 값이 루프의 이전 반복에서 이동되었다고 설명합니다. 러스트는 락 counter의 소유권을 여러 스레드로 옮길 수 없음을 말하고 있습니다. 15장에서 설명했던 복수 소유자 메서드를 가지고 이 컴파일 에러를 고쳐봅시다. 복수 스레드와 함께하는 복수 소유권 15장에서는 스마트 포인터 Rc을 사용하여 참조 카운팅 값을 만들어 값에 여러 소유자를 부여했습니다. 여기서도 똑같이 해서 어떻게 되는지 봅시다. 예제 16-14의 Mutex를 Rc로 감싸서 스레드로 소유권을 넘기기 전에 그 Rc을 복제하겠습니다. 파일명: src/main.rs use std::rc::Rc;\nuse std::sync::Mutex;\nuse std::thread; fn main() { let counter = Rc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Rc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!(\"Result: {}\", *counter.lock().unwrap());\n} 예제 16-14: Rc를 사용하여 여러 스레드가 Mutex를 소유할 수 있도록 하는 시도 다시 한번 컴파일을 하고 그 결과가... 다른 에러들이네요! 컴파일러는 많은 것을 가르쳐 주고 있습니다. $ cargo run Compiling shared-state v0.1.0 (file:///projects/shared-state)\nerror[E0277]: `Rc>` cannot be sent between threads safely --> src/main.rs:11:36 |\n11 | let handle = thread::spawn(move || { | ------------- ^------ | | | | ______________________|_____________within this `[closure@src/main.rs:11:36: 11:43]` | | | | | required by a bound introduced by this call\n12 | | let mut num = counter.lock().unwrap();\n13 | |\n14 | | *num += 1;\n15 | | }); | |_________^ `Rc>` cannot be sent between threads safely | = help: within `[closure@src/main.rs:11:36: 11:43]`, the trait `Send` is not implemented for `Rc>`\nnote: required because it's used within this closure --> src/main.rs:11:36 |\n11 | let handle = thread::spawn(move || { | ^^^^^^^\nnote: required by a bound in `spawn` --> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:704:8 | = note: required by this bound in `spawn` For more information about this error, try `rustc --explain E0277`.\nerror: could not compile `shared-state` due to previous error 와우, 이 에러는 정말 장황하네요! 여기 집중할 중요한 부분이 있습니다: `Rc>` cannot be sent between threads safely (`Rc>`는 스레드간에 안전하게 보낼 수 없습니다). 또한 컴파일러는 그 이유를 말해주고 있습니다: the trait `Send` is not implemented for `Rc>` (트레이트 `Send`가 `Rc>` 에 대해 구현되지 않았습니다). Send에 대해서는 다음 절에서 이야기하겠습니다: 이것은 스레드와 함께 사용하는 타입들이 동시적 상황에서 쓰이기 위한 것임을 보장하는 트레이트 중 하나입니다. 안타깝게도, Rc는 스레드를 교차하면서 공유하기에는 안전하지 않습니다. Rc가 참조 카운트를 관리할 때, 각 clone 호출마다 카운트에 더하고 각 클론이 버려질 때 카운트에서 제합니다. 하지만 그것은 다른 스레드에 의해 카운트를 변경하는 것을 방해할 수 없음을 보장하는 어떠한 동시성 기초 재료도 이용하지 않습니다. 이는 잘못된 카운트를 야기할 수 있습니다-결과적으로 메모리 누수를 발생시키거나 아직 다 쓰기 전에 값이 버려질 수 있는 미세한 버그를 낳겠죠. 우리가 원하는 것은 정확히 Rc와 비슷하지만 스레드-안전한 방식으로 참조 카운트를 바꾸는 녀석입니다. Arc를 이용한 아토믹 참조 카운팅 다행히도, Arc가 바로 동시적 상황에서 안전하게 사용할 수 있는 Rc 같은 타입입니다. a 는 아토믹 (atomic) 을 의미하는데, 즉 이것이 원자적으로 참조자를 세는 (atomically reference counted) 타입임을 뜻합니다. 아토믹은 추가적인 종류의 동시성 기초 재료로서, 여기서는 자세히 다루지 않을 겁니다: 더 자세히 알고 싶으면 std::sync::atomic 에 대한 표준 라이브러리 문서를 보세요. 이 시점에서는 아토믹이 기초 타입처럼 동작하지만 스레드를 교차하며 공유해도 안전하다는 것만 알면 됩니다. 그렇다면 여러분은 왜 모든 기초 타입이 아토믹하지 않은지, 그리고 표준 라이브러리 타입은 왜 기본적으로 Arc을 구현에 이용하지 않는지를 궁금해 할지도 모르겠습니다. 그 이유는 스레드 안전성이란 것이 정말로 필요할 때만 감내하고 싶을 성능 저하를 일으키기 때문입니다. 싱글스레드 내에서만 값을 연산하는 경우, 아토믹이 제공하는 보장을 강제할 필요없이 코드는 더 빠르게 실행될 수 있습니다. 예제로 다시 돌아갑시다: Arc와 Rc는 같은 API를 가지고 있으므로, use 라인과 new 호출, 그리고 clone 호출 부분을 바꾸는 것으로 프로그램을 수정합니다. 예제 16-15의 코드는 마침내 컴파일 및 실행이 될 것입니다: 파일명: src/main.rs use std::sync::{Arc, Mutex};\nuse std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!(\"Result: {}\", *counter.lock().unwrap());\n} 예제 16-15: Arc를 사용하여 Mutex를 감싸서 여러 스레드 사이에서 소유권을 공유할 수 있도록 하기 이 코드는 다음을 출력할 것입니다: Result: 10 해냈군요! 크게 인상적인 것처럼 보이지 않을지도 모르겠지만 0부터 10까지 세었고, Mutex와 스레드 안전성에 대하여 많은 것을 알게 주었습니다. 또한 이 프로그램의 구조를 사용하여 카운터만 증가시키는 것 보다 더 복잡한 연산을 할 수도 있겠습니다. 이 전략을 사용하여 계산을 독립적인 부분들로 나누고, 해당 부분들을 스레드로 쪼갠 다음, Mutex를 사용하여 각 스레드가 해당 부분의 최종 결과를 업데이트하도록 할 수 있습니다. 단순한 산술 연산을 하는 중이라면 표준 라이브러리의 std::sync::atomic 모듈 이 제공하는 Mutex 타입보다 단순한 타입이 있습니다. 이 타입은 기초 타입에 대한 안전하고, 동시적이며, 원자적인 접근을 제공합니다. 이 예제에서는 기초 타입에 대해 Mutex를 사용하여 Mutex가 동작하는 방식에 집중하였습니다.","breadcrumbs":"겁 없는 동시성 » 공유 상태 동시성 » 뮤텍스를 사용하여 한번에 한 스레드에서의 데이터 접근을 허용하기","id":"302","title":"뮤텍스를 사용하여 한번에 한 스레드에서의 데이터 접근을 허용하기"},"303":{"body":"counter가 불변이지만 내부값에 대한 가변 참조자를 가지고 올 수 있었음을 알아채셨을 수도 있겠습니다; 이는 Cell 가족이 그러하듯 Mutex가 내부 가변성을 제공한다는 의미입니다. 15장에서 Rc의 내용물을 변경할 수 있도록 하기 위해 RefCell을 사용한 것과 같은 방식으로, Arc 내부의 값을 변경하기 위해 Mutex를 이용합니다. 주목할만한 또다른 세부 사항은 Mutex를 사용할 때 러스트가 모든 종류의 논리 에러로부터 보호해줄 수 없다는 것입니다. 15장에서 Rc를 사용하는 것은 두 Rc 값들이 서로를 참조하여 메모리 누수를 야기하는 순환 참조자를 만들 위험성이 따라오는 것이었음을 상기해 봅시다. 이와 유사하게, Mutex에는 데드락 (deadlock) 을 생성할 위험성이 따라옵니다. 이것은 어떤 연산이 두 개의 리소스에 대한 락을 얻을 필요가 있고 두 개의 스레드가 락을 하나씩 얻는다면, 서로가 서로를 영원히 기다리는 형태로 발생됩니다. 데드락에 흥미가 있다면, 데드락이 있는 러스트 프로그램 만들기를 시도해보세요; 그다음 아무 언어에 있는 뮤텍스를 위한 데드락 완화 전략를 연구해보고 이를 러스트에서 구현해보세요. Mutex와 MutexGuard에 대한 표준 라이브러리 API 문서가 유용한 정보를 제공합니다. 이제 Send와 Sync 트레이트와 이를 커스텀 타입과 함께 사용하는 방법을 이야기하는 것으로 이 장을 마무리 하겠습니다.","breadcrumbs":"겁 없는 동시성 » 공유 상태 동시성 » RefCell/Rc와 Mutex/Arc 간의 유사성","id":"303","title":"RefCell/Rc와 Mutex/Arc 간의 유사성"},"304":{"body":"흥미롭게도, 러스트 언어는 매우 적은 숫자의 동시성 기능을 갖고 있습니다. 이 장에서 여태껏 이야기한 거의 모든 동시성 기능이 언어의 부분이 아닌 표준 라이브러리의 영역이었습니다. 동시성 처리를 위한 옵션은 언어 혹은 표준 라이브러리에만 국한되지 않습니다; 여러분만의 동시성 기능을 작성하거나 다른 이들이 작성한 것을 이용할 수 있습니다. 그러나, 두 개의 동시성 개념은 언어에 내재되어 있습니다: 바로 std::marker 트레이트인 Sync와 Send입니다.","breadcrumbs":"겁 없는 동시성 » Sync와 Send 트레이트를 이용한 확장 가능한 동시성 » Sync와 Send 트레이트를 이용한 확장 가능한 동시성","id":"304","title":"Sync와 Send 트레이트를 이용한 확장 가능한 동시성"},"305":{"body":"Send 마커 트레이트는 Send가 구현된 타입의 소유권이 스레드 사이에서 이동될 수 있음을 나타냅니다. 대부분의 러스트 타입이 Send이지만, 몇 개의 예외가 있는데, 그 중 Rc도 있습니다: 이것은 Send가 될 수 없는데 그 이유는 여러분이 Rc 값을 복제하여 다른 스레드로 복제본의 소유권 전송을 시도한다면, 두 스레드 모두 동시에 참조 카운트 값을 업데이트할지도 모르기 때문입니다. 이러한 이유로, Rc는 여러분이 스레드-안전성 성능 저하를 지불하지 않아도 되는 싱글스레드의 경우에 사용되도록 구현되었습니다. 따라서 러스트의 타입 시스템과 트레이트 바운드는 우발적으로 스레드 간에 Rc 값을 불안전하게 보내질 수 없도록 보장해 줍니다. 예제 16-14를 시도할 때는 트레이트 `Send`가 `Rc>`에 대해 구현되지 않았습니다 라는 에러를 얻었습니다. Send가 구현된 Arc로 바꿨을 때는 코드가 컴파일 되었습니다. 또한 전체가 Send 타입으로 구성된 모든 타입은 자동으로 Send로 마킹됩니다. 원시 포인터 (raw pointer) 를 빼고 거의 모든 기초 타입이 Send인데, 이는 19장에서 다루겠습니다.","breadcrumbs":"겁 없는 동시성 » Sync와 Send 트레이트를 이용한 확장 가능한 동시성 » Send를 사용하여 스레드 사이에 소유권 이동을 허용하기","id":"305","title":"Send를 사용하여 스레드 사이에 소유권 이동을 허용하기"},"306":{"body":"Sync 마커 트레이트는 Sync가 구현된 타입이 여러 스레드로부터 안전하게 참조 가능함을 나타냅니다. 바꿔 말하면, 만일 &T (T의 불변 참조자) 가 Send이면, 즉 참조자가 다른 스레드로 안전하게 보내질 수 있다면, T는 Sync합니다. Send와 유사하게, 기초 타입들은 Sync하고, 또한 전체가 Sync한 타입들로 구성된 타입 또한 Sync합니다. 스마트 포인터 Rc는 또한 Send가 아닌 이유와 동일한 이유로 Sync하지도 않습니다. (15장에서 이야기한) RefCell 타입과 연관된 Cell 타입의 가족들도 Sync하지 않습니다. RefCell가 런타임에 수행하는 대여 검사 구현은 스레드-안전하지 않습니다. 스마트 포인터 Mutex는 Sync하고 여러분이 ‘여러 스레드 사이에서 Mutex 공유하기’ 절에서 본 것처럼 여러 스레드에서 접근을 공유하는 데 사용될 수 있습니다.","breadcrumbs":"겁 없는 동시성 » Sync와 Send 트레이트를 이용한 확장 가능한 동시성 » Sync를 사용하여 여러 스레드로부터의 접근을 허용하기","id":"306","title":"Sync를 사용하여 여러 스레드로부터의 접근을 허용하기"},"307":{"body":"Send와 Sync 트레이트들로 구성된 타입들이 자동으로 Send 될 수 있고 Sync하기 때문에, 이 트레이트들은 손수 구현하지 않아도 됩니다. 이들은 심지어 마커 트레이트로서 구현할 어떠한 메서드도 없습니다. 이들은 그저 동시성과 관련된 불변성을 강제하는 데 유용할 따름입니다. 이 트레이트들을 손수 구현하는 것은 안전하지 않은 (unsafe) 러스트 코드 구현을 수반합니다. 19장에서 안전하지 않은 러스트 코드에 대하여 이야기하겠습니다; 지금으로서 중요한 정보는 Send와 Sync하지 않은 구성 요소들로 구성된 새로운 동시적 타입을 만드는 것이 안전성 보장을 유지하기 위해 신중한 고려가 필요하다는 점입니다. ‘러스토노미콘’ 에 이러한 보장과 유지하는 방법에 대한 더 많은 정보가 있습니다.","breadcrumbs":"겁 없는 동시성 » Sync와 Send 트레이트를 이용한 확장 가능한 동시성 » Send와 Sync를 손수 구현하는 것은 안전하지 않습니다","id":"307","title":"Send와 Sync를 손수 구현하는 것은 안전하지 않습니다"},"308":{"body":"지금 부분이 이 책에서 동시성에 대해 보게 될 마지막은 아닙니다: 20장의 프로젝트에서는 이번 장에서 다룬 개념들을 조금 전 다루었던 작은 예제보다 더 실질적인 상황에서 이용하게 될 것입니다. 일찍이 언급한 것처럼, 러스트가 동시성을 처리하는 방법이 언어의 매우 작은 부분이기 때문에, 많은 동시성 솔루션이 크레이트로 구현됩니다. 이들은 표준 라이브러리보다 더 빠르게 진화하므로, 현재 가장 최신 기술의 크레이트를 온라인으로 검색해서 멀티스레드 상황에 사용해 보세요. 러스트 표준 라이브러리는 메시지 패싱을 위한 채널을 제공하고, 동시적 컨텍스트에서 사용하기 안전한 Mutex와 Arc 같은 스마트 포인터 타입들을 제공합니다. 타입 시스템과 대여 검사기는 이 솔루션을 이용하는 코드가 데이터 경합 혹은 유효하지 않은 참조자로 끝나지 않을 것을 보장합니다. 일단 코드가 컴파일된다면, 다른 언어에서는 흔하게 발생하는 추적하기 어려운 버그 없이 여러 스레드 상에서 행복하게 동작하므로 안심할 수 있습니다. 동시성 프로그래밍은 더 이상 두려워할 개념이 아닙니다: 앞으로 나아가 겁 없이 여러분의 프로그램을 동시적으로 만드세요! 다음으로는 러스트 프로그램이 점차 커짐에 따라서 문제를 모델링하고 솔루션을 구조화하는 자연스러운 방법에 대해 이야기할 것입니다. 더불어 객체 지향 프로그래밍으로부터 친숙할 수 있을 개념들과 러스트의 관용구가 어떻게 연관되어 있는지 다루겠습니다.","breadcrumbs":"겁 없는 동시성 » Sync와 Send 트레이트를 이용한 확장 가능한 동시성 » 정리","id":"308","title":"정리"},"309":{"body":"객체 지향 프로그래밍 (object-oriented programming, OOP) 은 프로그램을 모델링하는 방식입니다. 프로그래밍 개념으로서의 객체는 1960년대에 프로그래밍 언어 시뮬라 (Simula) 에 도입되었습니다. 이 객체들은 임의의 객체들이 서로 메시지를 전달하는 앨런 케이 (Alan Kay) 의 프로그래밍 아키텍처에 영향을 끼쳤습니다. 이 아키텍처를 설명하기 위해 그는 1967년 객체 지향 프로그래밍 이라는 용어를 만들었습니다. 다수의 정의가 경쟁적으로 OOP이 무엇인지 설명하고 있으며, 그중 일부에 따르면 러스트는 객체 지향이지만 다른 정의에 따르면 그렇지 않습니다. 이 장에서는 일반적으로 객체 지향이라고 간주하는 특성들을 알아보고 이런 특성들이 러스트다운 표현들로 어떻게 변환되는지 알아보겠습니다. 그런 후에 객체 지향적 디자인 패턴을 러스트에서 구현하는 방법을 보여주고, 그렇게 했을 때와 러스트가 가진 강점 중 일부를 사용하여 구현했을 경우의 장단점에 대해 논의해 보겠습니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 러스트의 객체 지향 프로그래밍 기능들","id":"309","title":"러스트의 객체 지향 프로그래밍 기능들"},"31":{"body":"실습 프로젝트를 통해 러스트를 사용해 봅시다. 이번 장은 실제 프로젝트에서 몇몇 일반적인 러스트의 개념이 어떻게 활용되는지를 소개하려 합니다. 이 과정에서 let, match, 메서드, 연관 함수 (associated functions), 외부 크레이트 (external crates) 등의 활용 방법을 배울 수 있습니다. 이런 개념들은 다음 장들에서 더 자세히 다뤄질 것입니다. 이번 장에서는 여러분이 직접 기초적인 내용을 실습합니다. 여기서는 고전적인 입문자용 프로그래밍 문제인 추리 게임을 구현해 보려 합니다. 먼저 프로그램은 1~100 사이에 있는 임의의 정수를 생성합니다. 다음으로 플레이어가 프로그램에 추리한 정수를 입력합니다. 프로그램은 입력받은 추릿값이 정답보다 높거나 낮음을 알려줍니다. 추릿값이 정답이라면 축하 메시지를 보여주고 종료됩니다.","breadcrumbs":"추리 게임 » 추리 게임","id":"31","title":"추리 게임"},"310":{"body":"프로그래밍 커뮤니티에서는 어떤 언어가 객체 지향으로 간주되기 위해 반드시 갖춰야 하는 기능에 대한 합의가 이루어지지 않았습니다. 러스트는 OOP를 포함한 많은 프로그래밍 패러다임의 영향을 받았습니다; 예를 들어, 13장에서는 함수형 프로그래밍에서 나온 기능들을 탐구해봤습니다. OOP 언어라면 거의 틀림없이 몇가지 공통된 특성을 공유하는데, 여기에는 객체 (object), 캡슐화 (encapsulation), 그리고 상속 (inheritance) 이 있습니다. 각 특성이 무엇을 의미하는지와 이를 러스트가 지원하는지를 살펴봅시다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 객체 지향 언어의 특성","id":"310","title":"객체 지향 언어의 특성"},"311":{"body":"속칭 4인조 책이라고도 불리는 에리히 감마 (Erich Gamma), 리처드 헬름 (Richard Helm), 랄프 존슨 (Ralph Johnson), 그리고 존 블리시데스 (John Vlissides) 의 책 디자인 패턴 (Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley Professional, 1994) 은 객체 지향 디자인 패턴에 대한 카탈로그입니다. 이 책에서는 OOP를 다음과 같이 정의합니다: 객체 지향 프로그램은 객체로 구성됩니다. 객체 는 데이터 및 이 데이터를 활용하는 프로시저를 묶습니다. 이 프로시저들을 보통 메서드 혹은 연산 (operation) 이라고 부릅니다. 이 정의에 따르면, 러스트는 객체 지향적입니다: 구조체와 열거형에는 데이터가 있고, impl 블록은 그 구조체와 열거형에 대한 메서드를 제공하죠. 설령 메서드가 있는 구조체와 열거형이 객체라고 호칭 되지는 않더라도, 4인조의 객체에 대한 정의에 따르면 이들은 동일한 기능을 제공합니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 객체는 데이터와 동작을 담습니다","id":"311","title":"객체는 데이터와 동작을 담습니다"},"312":{"body":"일반적으로 OOP와 연관된 또 다른 측면은 캡슐화 (encapsulation) 라는 개념으로, 그 의미는 객체를 이용하는 코드에서 그 객체의 상세 구현에 접근할 수 없게 한다는 것입니다. 따라서, 객체와 상호작용하는 유일한 방법은 해당 객체의 공개 API를 통하는 것입니다; 객체를 사용하는 코드는 직접 객체의 내부에 접근하여 데이터나 동작을 직접 변경시켜서는 안 됩니다. 이는 프로그래머가 객체를 사용하는 코드의 변경 없이 이 객체 내부를 변경하거나 리팩터링할 수 있도록 해줍니다. 7장에서 어떻게 캡슐화를 제어하는지에 대해 논의했습니다: pub 키워드를 사용하여 어떤 모듈, 타입, 함수, 그리고 메서드가 공개될 것인가를 결정할 수 있으며, 기본적으로 다른 모든 것들은 비공개입니다. 예를 들면, i32 값의 벡터를 필드로 가지고 있는 AveragedCollection 구조체를 정의할 수 있습니다. 또한 이 구조체는 벡터의 값에 대한 평균값을 담는 필드도 가질 수 있으므로, 평균값이 필요한 순간마다 매번 이를 계산할 필요는 없습니다. 바꿔 말하면, AveragedCollection은 계산된 평균값을 캐시할 것입니다. 예제 17-1은 이 AveragedCollection 구조체에 대한 정의를 나타냅니다: 파일명: src/lib.rs pub struct AveragedCollection { list: Vec, average: f64,\n} 예제 17-1: 컬렉션 내의 정수 아이템들과 그의 평균값을 관리하는 AveragedCollection 구조체 구조체는 pub으로 표시되어 다른 코드가 이를 사용할 수 있지만, 구조체 안에 존재하는 필드들은 여전히 비공개입니다. 이는 이번 사례에 매우 중요한데, 그 이유는 하나의 값이 리스트에 추가되거나 제거될 때마다 평균도 업데이트되는 것을 보장하고 싶기 때문입니다. 예제 17-2와 같이 구조체에 add, remove, 그리고 average 메서드를 구현하여 이를 수행합니다: 파일명: src/lib.rs # pub struct AveragedCollection {\n# list: Vec,\n# average: f64,\n# }\n# impl AveragedCollection { pub fn add(&mut self, value: i32) { self.list.push(value); self.update_average(); } pub fn remove(&mut self) -> Option { let result = self.list.pop(); match result { Some(value) => { self.update_average(); Some(value) } None => None, } } pub fn average(&self) -> f64 { self.average } fn update_average(&mut self) { let total: i32 = self.list.iter().sum(); self.average = total as f64 / self.list.len() as f64; }\n} 예제 17-2: AveragedCollection의 공개 메서드 add, remove, 그리고 average의 구현 공개 메서드 add, remove, 그리고 average는 AveragedCollection 인스턴스의 데이터에 접근하거나 수정할 수 있는 유일한 방법입니다. add 메서드를 사용하여 list에 아이템을 추가하거나 remove 메서드를 사용하여 제거하면, 각 구현에서는 average 필드의 업데이트을 처리하는 비공개 메서드 update_average도 호출합니다. list와 average 필드는 비공개로 하였으므로 외부 코드가 list 필드에 직접 아이템을 추가하거나 제거할 방법은 없습니다; 그렇게 하면 average 필드는 list가 변경될 때 동기화되지 않을 수 있습니다. average 메서드는 average 필드의 값을 반환하므로, 외부 코드가 average를 읽을 수 있도록 하지만 변경할 수는 없습니다. AveragedCollection의 세부 구현은 캡슐화되었기 때문에, 향후에 데이터 구조와 같은 측면을 쉽게 변경할 수 있습니다. 예를 들면, list 필드에 대해서 Vec가 아닌 HashSet를 사용할 수 있습니다. add, remove, average 공개 메서드의 시그니처가 그대로 유지되는 한, AveragedCollection를 사용하는 코드들은 변경될 필요가 없습니다. 대신 list를 공개했다면 반드시 그렇지는 않았을 것입니다: HashSet와 Vec는 아이템들을 추가하거나 제거하기 위한 메서드들이 다르므로, 만약 외부 코드가 list에 직접 접근하여 변경했더라면 모두 변경되어야 할 가능성이 높겠지요. 캡슐화가 객체 지향 언어로 간주하기 위해 필요한 측면이라면, 러스트는 해당 요구 사항을 충족합니다. 코드의 서로 다른 부분들에 대해 pub을 사용할지 여부를 선택하는 옵션을 통해 구현 세부 사항을 캡슐화할 수 있습니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 상세 구현을 은닉하는 캡슐화","id":"312","title":"상세 구현을 은닉하는 캡슐화"},"313":{"body":"상속 은 어떤 객체가 다른 객체의 정의로부터 요소를 상속받을 수 있는 메커니즘으로, 이를 통해 객체를 다시 정의하지 않고도 부모 객체의 데이터와 동작을 가져올 수 있습니다. 만약 객체 지향 언어가 반드시 상속을 제공해야 한다면, 러스트는 그렇지 않은 쪽입니다. 매크로를 사용하지 않고 부모 구조체의 필드와 메서드 구현을 상속받는 구조체를 정의할 방법은 없습니다. 하지만 여러분이 상속에 익숙하다면, 애초에 이를 사용하고자 하는 이유에 따라 러스트의 다른 솔루션들을 이용할 수 있습니다. 상속을 선택하는 이유는 크게 두 가지입니다. 하나는 코드를 재사용하는 것입니다: 어떤 타입의 특정한 동작을 구현할 수 있고, 상속을 통하여 다른 타입에 대해 그 구현을 재사용할 수 있습니다. 러스트 코드에서는 대신 기본 트레이트 메서드의 구현을 이용하여 제한적으로 공유할 수 있는데, 이는 예제 10-14에서 Summary 트레이트에 summarize 메서드의 기본 구현을 추가할 때 봤던 것입니다. Summary 트레이트를 구현하는 모든 타입은 추가 코드 없이 summarize 메서드를 사용할 수 있습니다. 이는 어떤 메서드의 구현체를 갖는 부모 클래스와 그를 상속받는 자식 클래스 또한 그 메서드의 해당 구현체를 갖는 것과 유사합니다. 또한 Summary 트레이트를 구현할 때 summarize의 기본 구현을 오버라이딩할 수 있고, 이는 자식 클래스가 부모 클래스에서 상속받는 메서드를 오버라이딩하는 것과 유사합니다. 상속을 사용하는 또 다른 이유는 타입 시스템과 관련된 것입니다: 자식 타입을 부모 타입과 같은 위치에서 사용할 수 있게 하기 위함입니다. 이를 다형성 (polymorphism) 이라고도 부르는데, 이는 여러 객체가 일정한 특성을 공유한다면 이들을 런타임에 서로 대체하여 사용할 수 있음을 의미합니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 타입 시스템과 코드 공유로서의 상속","id":"313","title":"타입 시스템과 코드 공유로서의 상속"},"314":{"body":"많은 사람이 다형성을 상속과 동일시합니다. 하지만 다형성은 여러 타입의 데이터로 작업할 수 있는 코드를 나타내는 더 범용적인 개념입니다. 상속에서는 이런 타입들이 일반적으로 하위클래스에 해당합니다. 러스트는 대신 제네릭을 사용하여 호환 가능한 타입을 추상화하고 트레이트 바운드를 이용하여 해당 타입들이 반드시 제공해야 하는 제약사항을 부과합니다. 이것을 종종 범주 내 매개변수형 다형성 (bounded parametric polymophism) 이라고 부릅니다. 최근 많은 프로그래밍 언어에서는 상속이 프로그래밍 디자인 솔루션으로써 선호되지 않고 있는데 그 이유는 필요 이상으로 많은 코드를 공유할 수 있는 위험이 있기 때문입니다. 하위클래스가 늘 그들의 부모 클래스의 모든 특성을 공유할 필요가 없어도 상속한다면 그렇게 됩니다. 이는 프로그램 설계의 유연성을 저하시킬 수 있습니다. 또한 하위클래스에서는 타당하지 않거나 적용될 수 없어서 에러를 유발하는 메서드들이 호출될 수 있는 가능성을 만듭니다. 게다가, 어떤 언어들은 단일 상속 (하위클래스가 하나의 클래스로부터만 상속받을 수 있음을 의미) 만을 허용하기 때문에 프로그램 디자인의 유연성을 더욱 제한하게 됩니다. 이러한 이유로, 러스트는 상속 대신에 트레이트 객체를 사용하는 다른 접근법을 택합니다. 트레이트 객체가 러스트에서 어떻게 다형성을 가능하게 하는지 살펴봅시다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 다형성","id":"314","title":"다형성"},"315":{"body":"8장에서 벡터의 제약사항 중 하나는 딱 하나의 타입에 대한 요소만 보관할 수 있다는 것임을 언급했습니다. 예제 8-10에서 정수, 부동 소수점, 그리고 문자를 보관하기 위한 배리언트들을 가지고 있는 SpreadsheetCell 열거형을 정의하는 해결 방안을 만들었습니다. 즉, 각 칸마다 다른 타입의 데이터를 저장할 수 있으면서도 여전히 그 칸들의 한 묶음을 대표하는 벡터를 가질 수 있었습니다. 이는 교환 가능한 아이템들이 코드를 컴파일할 때 알 수 있는 고정된 타입의 집합인 경우 완벽한 해결책입니다. 하지만, 때로는 우리의 라이브러리 사용자가 특정 상황에서 유효한 타입의 집합을 확장할 수 있도록 하길 원할 때가 있습니다. 이를 어떻게 달성할 수 있는지 보이기 위해, 예제로 아이템들의 리스트에 대해 반복하고 각 아이템에 대해 draw 메서드를 호출하여 이를 화면에 그리는 그래픽 사용자 인터페이스 (GUI) 도구를 만들어 보겠습니다 - GUI 도구들에게 있어서는 흔한 방식이죠. 우리가 만들 라이브러리 크레이트는 gui라고 호명되고 GUI 라이브러리 구조를 포괄합니다. 이 크레이트는 사용자들이 사용할 수 있는 몇 가지 타입들, Button이나 TextField 들을 포함하게 될 수 있습니다. 또한 gui 사용자들은 자신만의 그릴 수 있는 타입을 만들고자 할 것입니다: 일례로, 어떤 프로그래머는 Image를 추가할지도, 또 다른 누군가는 SelectBox를 추가할지도 모릅니다. 이번 예제에서 완전한 GUI 라이브러리를 구현하지는 않겠지만 이 조각들이 어떻게 결합하는지 보여주고자 합니다. 라이브러리를 작성하는 시점에서는 다른 프로그래머들이 만들고자 하는 모든 타입들을 알 수 없죠. 하지만 gui가 다양한 타입들의 많은 값을 추적해야 하고, draw 메서드가 각각의 다양한 타입의 값들에 대해 호출되어야 한다는 것은 알고 있습니다. draw 메서드를 호출했을 때 벌어지는 일에 대해서 정확히 알 필요는 없고, 그저 그 값에 호출할 수 있는 해당 메서드가 있음을 알면 됩니다. 상속이 있는 언어로 이 작업을 하기 위해서는 draw라는 이름의 메서드를 갖고 있는 Component라는 클래스를 정의할 수 있습니다. 다른 클래스들, 이를테면 Button, Image, 그리고 SelectBox 같은 것들은 Component를 상속받고 따라서 draw 메서드를 물려받게 됩니다. 이들은 각각 draw 메서드를 오버라이딩하여 그들의 고유 동작을 정의할 수 있으나, 프레임워크는 모든 타입을 마치 Component인 것처럼 다룰 수 있고 draw를 호출할 수 있습니다. 하지만 러스트에는 상속이 없는 관계로, 사용자들이 새로운 타입을 정의하고 확장할 수 있도록 gui 라이브러리를 구조화하는 다른 방법이 필요합니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 트레이트 객체를 사용하여 다른 타입의 값 허용하기 » 트레이트 객체를 사용하여 다른 타입의 값 허용하기","id":"315","title":"트레이트 객체를 사용하여 다른 타입의 값 허용하기"},"316":{"body":"gui에 필요한 동작을 구현하기 위해, draw라는 이름의 메서드가 하나 있는 Draw라는 이름의 트레이트를 정의하겠습니다. 그러면 트레이트 객체 (trait object) 를 담는 벡터를 정의할 수 있습니다. 트레이트 객체는 특정 트레이트를 구현한 타입의 인스턴스와 런타임에 해당 타입의 트레이트 메서드를 조회하는 데 사용되는 테이블 모두를 가리킵니다. & 참조자나 Box 스마트 포인터 같은 포인터 종류로 지정한 다음 dyn 키워드를 붙이고, 그 뒤에 관련된 트레이트를 특정하면 트레이트 객체를 생성할 수 있습니다. (트레이트 객체에 포인터를 사용해야 하는 이유는 19장의 ‘동적 크기 타입과 Sized 트레이트’ 절에서 설명하겠습니다.) 제네릭 타입이나 구체 타입 대신 트레이트 객체를 사용할 수 있습니다. 트레이트 객체를 사용하는 곳이 어디든, 러스트의 타입 시스템은 컴파일 타임에 해당 컨텍스트에서 사용된 모든 값이 트레이트 객체의 트레이트를 구현할 것을 보장합니다. 결론적으로 컴파일 타임에 모든 가능한 타입을 알 필요가 없습니다. 앞서 언급했듯 러스트에서는 다른 언어의 객체와 구분하기 위해 구조체와 열거형을 ‘객체’라고 부르는 것을 자제합니다. 구조체나 열거형에서는 구조체 필드의 데이터와 impl 블록의 동작이 분리되는 반면, 다른 언어에서는 데이터와 동작이 하나의 개념으로 결합한 것을 객체라고 명명하는 경우가 많으니까요. 트레이트 객체들은 데이터와 동작을 결합한다는 의미에서 다른 언어의 객체와 더 비슷합니다 . 하지만 트레이트 객체는 트레이트 객체에 데이터를 추가할 수 없다는 점에서 전통적인 객체와 다릅니다. 트레이트 객체는 다른 언어들의 객체만큼 범용적으로 유용하지는 않습니다: 그들의 명확한 목적은 공통된 동작에 대한 추상화를 가능하도록 하는 것이죠. 예제 17-3은 draw라는 이름의 메서드를 갖는 Draw라는 트레이트를 정의하는 방법을 보여줍니다: 파일명: src/lib.rs pub trait Draw { fn draw(&self);\n} 예제 17-3: Draw 트레이트의 정의 이 문법은 10장에 있는 트레이트를 정의하는 방법에서 다뤘으니 익숙하실 겁니다. 다음에 새로운 문법이 등장합니다: 예제 17-4는 components라는 벡터를 보유하고 있는 Screen이라는 구조체를 정의합니다. Box 타입의 벡터인데, 이것이 트레이트 객체입니다; 이것은 Draw 트레이트를 구현한 Box 안의 모든 타입에 대한 대역입니다. 파일명: src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n# pub struct Screen { pub components: Vec>,\n} 예제 17-4: Draw 트레이트를 구현하는 트레이트 객체들의 벡터 components를 필드로 가지고 있는 Screen 구조체의 정의 Screen 구조체에서는 예제 17-5와 같이 components의 각 요소마다 draw 메서드를 호출하는 run 메서드를 정의합니다: 파일명: src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n# # pub struct Screen {\n# pub components: Vec>,\n# }\n# impl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } }\n} 예제 17-5: 각 컴포넌트에 대해 draw 메서드를 호출하는 Screen의 run 메서드 이는 트레이트 바운드가 있는 제네릭 타입 매개변수를 사용하는 구조체를 정의하는 것과는 다르게 작동합니다. 제네릭 타입 매개변수는 한 번에 하나의 구체 타입으로만 대입될 수 있는 반면, 트레이트 객체를 사용하면 런타임에 트레이트 객체에 대해 여러 구체 타입을 채워 넣을 수 있습니다. 예를 들면, 예제 17-6처럼 제네릭 타입과 트레이트 바운드를 사용하여 Screen 구조체를 정의할 수도 있을 겁니다: 파일명: src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n# pub struct Screen { pub components: Vec,\n} impl Screen\nwhere T: Draw,\n{ pub fn run(&self) { for component in self.components.iter() { component.draw(); } }\n} 예제 17-6: 제네릭과 트레이트 바운드를 사용한 Screen 구조체와 run 메서드의 대체 구현 이렇게 하면 전부 Button 타입이거나 전부 TextField 타입인 컴포넌트의 목록을 가진 Screen 인스턴스로 제한됩니다. 동일 타입의 컬렉션만 사용한다면 제네릭과 트레이트 바운드를 사용하는 것이 바람직한데, 왜냐하면 그 정의들은 컴파일 타임에 단형성화 (monomorphize) 되어 구체 타입으로 사용되기 때문입니다. 반면 트레이트 객체를 사용하는 메서드를 이용할 경우, 하나의 Screen 인스턴스가 Box