From 9da1557a1bee04cc49dba625126639a44595da78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Jan 2024 01:14:39 +0000 Subject: [PATCH] Auto Deploy (fix: typo (#311)) 448eeac6c1262c9ece9a19de3452f11d6714c94d --- ch11-01-writing-tests.html | 2 +- print.html | 2 +- searchindex.js | 2 +- searchindex.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ch11-01-writing-tests.html b/ch11-01-writing-tests.html index e027339b6a..406ac7309c 100644 --- a/ch11-01-writing-tests.html +++ b/ch11-01-writing-tests.html @@ -479,7 +479,7 @@

이며, 다음과 같이 표준 라이브러리에 정의되어 있습니다: enum Option { Some(T), None,\n} Option 열거형은 매우 유용하며 기본적으로 포함되어 있기 때문에, 명시적으로 가져오지 않아도 사용할 수 있습니다. 또한 variants 도 마찬가지입니다: Option:: 를 앞에 붙이지 않고, Some 과 None 을 바로 사용할 수 있습니다. Option 는 여전히 일반적인 열거형이고, Some(T) 과 None 도 여전히 Option 의 variants 입니다. 는 러스트의 문법이며 아직 다루지 않았습니다. 제너릭 타입 파라미터이며, 제너릭에 대해서는 10 장에서 더 자세히 다룰 것입니다. 지금은 단지 가 Option 열거형의 Some variant 가 어떤 타입의 데이터라도 가질 수 있다는 것을 의미한다는 것을 알고 있으면 됩니다. 여기 숫자 타입과 문자열 타입을 갖는 Option 값에 대한 예들이 있습니다: let some_number = Some(5);\nlet some_string = Some(\"a string\"); let absent_number: Option = None; Some 이 아닌 None 을 사용한다면, Option 이 어떤 타입을 가질지 러스트에게 알려줄 필요가 있습니다. 컴파일러는 None 만 보고는 Some variant 가 어떤 타입인지 추론할 수 없습니다. Some 값을 얻게 되면, 값이 있다는 것과 Some 이 갖고 있는 값에 대해 알 수 있습니다. None 값을 사용하면, 어떤 면에서는 null 과 같은 의미를 갖게 됩니다: 유효한 값을 갖지 않습니다. 그렇다면 왜 Option 가 null 을 갖는 것보다 나을까요? 간단하게 말하면, Option 와 T (T 는 어떤 타입이던 될 수 있음)는 다른 타입이며, 컴파일러는 Option 값을 명확하게 유효한 값처럼 사용하지 못하도록 합니다. 예를 들면, 아래 코드는 Option 에 i8 을 더하려고 하기 때문에 컴파일되지 않습니다: let x: i8 = 5;\nlet y: Option = Some(5); let sum = x + y; 이 코드를 실행하면, 아래와 같은 에러 메시지가 출력됩니다: error[E0277]: the trait bound `i8: std::ops::Add>` is\nnot satisfied --> |\n7 | let sum = x + y; | ^^^^^ | 주목하세요! 실제로, 이 에러 메시지는 러스트가 Option 와 i8 를 어떻게 더해야 하는지 모른다는 것을 의미하는데, 둘은 다른 타입이기 때문입니다. 러스트에서 i8 과 같은 타입의 값을 가질 때, 컴파일러는 항상 유효한 값을 갖고 있다는 것을 보장할 것입니다. 값을 사용하기 전에 null 인지 확인할 필요도 없이 자신 있게 사용할 수 있습니다. 단지 Option 을 사용할 경우엔 (혹은 어떤 타입 이건 간에) 값이 있을지 없을지에 대해 걱정할 필요가 있으며, 컴파일러는 값을 사용하기 전에 이런 케이스가 처리되었는지 확인해 줄 것입니다. 다르게 얘기하자면, T 에 대한 연산을 수행하기 전에 Option 를 T 로 변환해야 합니다. 일반적으로, 이런 방식은 null 과 관련된 가장 흔한 이슈 중 하나를 발견하는데 도움을 줍니다: 실제로 null 일 때, null 이 아니라고 가정하는 경우입니다. null 이 아닌 값을 갖는다는 가정을 놓치는 경우에 대해 걱정할 필요가 없게 되면, 코드에 더 확신을 갖게 됩니다. null 일 수 있는 값을 사용하기 위해서, 명시적으로 값의 타입을 Option 로 만들어 줘야 합니다. 그다음엔 값을 사용할 때 명시적으로 null 인 경우를 처리해야 합니다. 값의 타입이 Option 가 아닌 모든 곳은 값이 null 아 아니라고 안전하게 가정할 수 있습니다 . 이것은 null을 너무 많이 사용하는 문제를 제한하고 러스트 코드의 안정성을 높이기 위한 러스트의 의도된 디자인 결정사항입니다. 그럼 Option 타입인 값을 사용할 때, Some variant 에서 T 값을 어떻게 가져와서 사용할 수 있을까요? Option 열거형에서 다양한 상황에서 유용하게 사용할 수 있는 많은 메소드들이 있습니다; 문서에서 확인할 수 있습니다. Option 의 메소드들에 익숙해지는 것은 러스트를 사용하는데 매우 유용할 것입니다. 일반적으로, Option 값을 사용하기 위해서는 각 variant 를 처리할 코드가 필요할 것입니다. Some(T) 값일 경우만 실행되는 코드가 필요하고, 이 코드는 안에 있는 T 를 사용할 수 있습니다. 다른 코드에서는 None 값일 때 실행되는 코드가 필요가 하기도 하며, 이 코드에서는 사용할 수 있는 T 값이 없습니다. match 표현식은 제어 흐름을 위한 구분으로, 열거형과 함께 사용하면 이런 일들을 할 수 있습니다: 열거형이 갖는 variant 에 따라 다른 코드를 실행할 것이고, 그 코드는 매칭 된 값에 있는 데이터를 사용할 수 있습니다.","breadcrumbs":"열거형과 패턴 매칭 » 열거형 정의하기 » Option 열거형과 Null 값 보다 좋은 점들.","id":"101","title":"Option 열거형과 Null 값 보다 좋은 점들."},"102":{"body":"러스트는 match라고 불리는 극도로 강력한 흐름 제어 연산자를 가지고 있는데 이는 우리에게 일련의 패턴에 대해 어떤 값을 비교한 뒤 어떤 패턴에 매치되었는지를 바탕으로 코드를 수행하도록 해줍니다. 패턴은 리터럴 값, 변수명, 와일드카드, 그리고 많은 다른 것들로 구성될 수 있습니다; 18장에서 다른 모든 종류의 패턴들과 이것들로 할 수 있는 것에 대해 다룰 것입니다. match의 힘은 패턴의 표현성으로부터 오며 컴파일러는 모든 가능한 경우가 다루어지는지를 검사합니다. match 표현식을 동전 분류기와 비슷한 종류로 생각해보세요: 동전들은 다양한 크기의 구멍들이 있는 트랙으로 미끄러져 내려가고, 각 동전은 그것에 맞는 첫 번째 구멍을 만났을 때 떨어집니다. 동일한 방식으로, 값들은 match 내의 각 패턴을 통과하고, 해당 값에 “맞는” 첫 번째 패턴에서, 그 값은 실행 중에 사용될 연관된 코드 블록 안으로 떨어질 것입니다. 우리가 방금 동전들을 언급했으니, match를 이용한 예제로 동전들을 이용해봅시다! Listing 6-3에서 보는 바와 같이, 우리는 익명의 미국 동전을 입력받아서, 동전 계수기와 동일한 방식으로 그 동전이 어떤 것이고 센트로 해당 값을 반환하는 함수를 작성할 수 있습니다. enum Coin { Penny, Nickel, Dime, Quarter,\n} fn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }\n} Listing 6-3: 열거형과 열거형의 variant를 패턴으로서 사용하는 match 표현식 value_in_cents 함수 내의 match를 쪼개 봅시다. 먼저, match 키워드 뒤에 표현식을 써줬는데, 위의 경우에는 coin 값입니다. 이는 if를 사용한 표현식과 매우 유사하지만, 큰 차이점이 있습니다: if를 사용하는 경우, 해당 표현식은 부울린 값을 반환할 필요가 있습니다. 여기서는 어떤 타입이든 가능합니다. 위 예제에서 coin의 타입은 Listing 6-3에서 정의했던 Coin 열거형입니다. 다음은 match 갈래(arm)들입니다. 하나의 갈래는 두 부분을 갖고 있습니다: 패턴과 어떤 코드로 되어 있죠. 여기서의 첫 번째 갈래는 값 Coin::Penny로 되어있는 패턴을 가지고 있고 그 후에 패턴과 실행되는 코드를 구분해주는 => 연산자가 있습니다. 위의 경우에서 코드는 그냥 값 1입니다. 각 갈래는 그다음 갈래와 쉼표로 구분됩니다. match 표현식이 실행될 때, 결과 값을 각 갈래의 패턴에 대해서 순차적으로 비교합니다. 만일 어떤 패턴이 그 값과 매치되면, 그 패턴과 연관된 코드가 실행됩니다. 만일 그 패턴이 값과 매치되지 않는다면, 동전 분류기와 비슷하게 다음 갈래로 실행을 계속합니다. 각 갈래와 연관된 코드는 표현식이고, 이 매칭 갈래에서의 표현식의 결과 값은 전체 match 표현식에 대해 반환되는 값입니다. 각 갈래가 그냥 값을 리턴하는 Listing 6-3에서처럼 매치 갈래의 코드가 짧다면, 중괄호는 보통 사용하지 않습니다. 만일 매치 갈래 내에서 여러 줄의 코드를 실행시키고 싶다면, 중괄호를 이용할 수 있습니다. 예를 들어, 아래의 코드는 Coin::Penny와 함께 메소드가 호출될 때마다 “Lucky penny!”를 출력하지만 여전히 해당 블록의 마지막 값인 1을 반환할 것입니다: # enum Coin {\n# Penny,\n# Nickel,\n# Dime,\n# Quarter,\n# }\n#\nfn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => { println!(\"Lucky penny!\"); 1 }, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter => 25, }\n}","breadcrumbs":"열거형과 패턴 매칭 » match 흐름 제어 연산자 » match 흐름 제어 연산자","id":"102","title":"match 흐름 제어 연산자"},"103":{"body":"매치 갈래의 또 다른 유용한 기능은 패턴과 매치된 값들의 부분을 바인딩할 수 있다는 것입니다. 이것이 열거형 variant로부터 어떤 값들을 추출할 수 있는 방법입니다. 한 가지 예로서, 우리의 열거형 variant 중 하나를 내부에 값을 들고 있도록 바꿔봅시다. 1999년부터 2008년까지, 미국은 각 50개 주마다 한쪽 면의 디자인이 다른 쿼터 동전을 주조했습니다. 다른 동전들은 주의 디자인을 갖지 않고, 따라서 오직 쿼터 동전들만 이 특별 값을 갖습니다. 우리는 이 정보를 Quarter variant 내에 UsState 값을 포함하도록 우리의 enum을 변경함으로써 추가할 수 있는데, 이는 Listing 6-4에서 한 바와 같습니다: #[derive(Debug)] // So we can inspect the state in a minute\nenum UsState { Alabama, Alaska, // ... etc\n} enum Coin { Penny, Nickel, Dime, Quarter(UsState),\n} Listing 6-4: Quarter variant가 UsSate 값 또한 들고 있는 Coin 열거형 우리의 친구가 모든 50개 주 쿼터 동전을 모으기를 시도하는 중이라고 상상해봅시다. 동전의 종류에 따라 동전을 분류하는 동안, 우리는 또한 각 쿼터 동전에 연관된 주의 이름을 외쳐서, 만일 그것이 우리 친구가 가지고 있지 않은 것이라면, 그 친구는 자기 컬렉션에 그 동전을 추가할 수 있겠지요. 이 코드를 위한 매치 표현식 내에서는 variant 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#\nfn value_in_cents(coin: Coin) -> u32 { match coin { Coin::Penny => 1, Coin::Nickel => 5, Coin::Dime => 10, Coin::Quarter(state) => { println!(\"State quarter from {:?}!\", state); 25 }, }\n} 만일 우리가 value_in_cents(Coin::Quarter(UsState::Alaska))를 호출했다면, coin은 Coin::Quarter(UsState::Alaska)가 될 테지요. 각각의 매치 갈래들과 이 값을 비교할 때, Coin::Quarter(state)에 도달할 때까지 아무것도 매치되지 않습니다. 이 시점에서, state에 대한 바인딩은 값 UsState::Alaska가 될 것입니다. 그러면 이 바인딩을 println! 표현식 내에서 사용할 수 있고, 따라서 Quarter에 대한 Coin 열거형 variant로부터 내부의 주에 대한 값을 얻었습니다.","breadcrumbs":"열거형과 패턴 매칭 » match 흐름 제어 연산자 » 값들을 바인딩하는 패턴들","id":"103","title":"값들을 바인딩하는 패턴들"},"104":{"body":"이전 절에서 Option을 사용할 때 Some 케이스로부터 내부의 T 값을 얻을 필요가 있었습니다; 우리는 Coin 열거형을 가지고 했던 것처럼 match를 이용하여 Option를 다룰 수 있습니다! 동전들을 비교하는 대신, Option의 variant를 비교할 것이지만, match 표현식이 동작하는 방법은 동일하게 남아있습니다. Option를 파라미터로 받아서, 내부에 값이 있으면, 그 값에 1을 더하는 함수를 작성하고 싶다고 칩시다. 만일 내부에 값이 없으면, 이 함수는 None 값을 반환하고 다른 어떤 연산도 수행하는 시도를 하지 않아야 합니다. match에 감사하게도, 이 함수는 매우 작성하기 쉽고, Listing 6-5와 같이 보일 것입니다: fn plus_one(x: Option) -> Option { match x { None => None, Some(i) => Some(i + 1), }\n} let five = Some(5);\nlet six = plus_one(five);\nlet none = plus_one(None); Listing 6-5: Option 상에서 match를 이용하는 함수 Some(T) 매칭 하기 plus_one의 첫 번째 실행을 좀 더 자세히 시험해봅시다. plus_one(five)가 호출될 때, plus_one의 본체 내의 변수 x는 값 Some(5)를 갖게 될 것입니다. 그런 다음 각각의 매치 갈래에 대하여 이 값을 비교합니다. None => None, Some(5) 값은 패턴 None과 매칭 되지 않으므로, 다음 갈래로 계속 갑니다. Some(i) => Some(i + 1), Some(5)가 Some(i)랑 매칭 되나요? 예, 바로 그렇습니다! 동일한 variant를 갖고 있습니다. Some 내부에 담긴 값은 i에 바인드 되므로, i는 값 5를 갖습니다. 그런 다음 매치 갈래 내의 코드가 실행되므로, i의 값에 1을 더한 다음 최종적으로 6을 담은 새로운 Some 값을 생성합니다. None 매칭 하기 이제 x가 None인 Listing 6-5에서의 plus_one의 두 번째 호출을 살펴봅시다. match 안으로 들어와서 첫 번째 갈래와 비교합니다. None => None, 매칭 되었군요! 더할 값은 없으므로, 프로그램은 멈추고 =>의 우측 편에 있는 None 값을 반환합니다. 첫 번째 갈래에 매칭 되었으므로, 다른 갈래와는 비교하지 않습니다. match와 열거형을 조합하는 것은 다양한 경우에 유용합니다. 여러분은 러스트 코드 내에서 이러한 패턴을 많이 보게 될 것입니다: 열거형에 대한 match, 내부의 데이터에 변수 바인딩, 그런 다음 그에 대한 수행 코드 말이지요. 처음에는 약간 까다롭지만, 여러분이 일단 익숙해지면, 이를 모든 언어에서 쓸 수 있게 되기를 바랄 것입니다. 이것은 꾸준히 사용자들이 가장 좋아하는 기능입니다.","breadcrumbs":"열거형과 패턴 매칭 » match 흐름 제어 연산자 » Option를 이용하는 매칭","id":"104","title":"Option를 이용하는 매칭"},"105":{"body":"우리가 논의할 필요가 있는 match의 다른 관점이 있습니다. plus_one 함수의 아래 버전을 고려해 보세요: fn plus_one(x: Option) -> Option { match x { Some(i) => Some(i + 1), }\n} 여기서는 None 케이스를 다루지 않았고, 따라서 이 코드는 버그를 일으킬 것입니다. 다행히도, 이는 러스트가 어떻게 잡는지 알고 있는 버그입니다. 이 코드를 컴파일하고자 시도하면, 아래와 같은 에러를 얻게 됩니다: error[E0004]: non-exhaustive patterns: `None` not covered --> |\n6 | match x { | ^ pattern `None` not covered 러스트는 우리가 다루지 않은 모든 가능한 경우를 알고 있고, 심지어 우리가 어떤 패턴을 잊어먹었는지도 알고 있습니다! 러스트에서 매치는 하나도 빠뜨리지 않습니다(exhaustive): 코드가 유효해지기 위해서는 모든 마지막 가능성까지 샅샅이 다루어야 합니다. 특히 Option의 경우, 즉 러스트가 우리로 하여금 None 케이스를 명시적으로 다루는 일을 잊는 것을 방지하는 경우에는, Null 일지도 모를 값을 가지고 있음을 가정하여, 앞서 논의했던 수십억 달러짜리 실수를 하는 일을 방지해줍니다.","breadcrumbs":"열거형과 패턴 매칭 » match 흐름 제어 연산자 » 매치는 하나도 빠뜨리지 않습니다","id":"105","title":"매치는 하나도 빠뜨리지 않습니다"},"106":{"body":"러스트는 또한 우리가 모든 가능한 값을 나열하고 싶지 않을 경우에 사용할 수 있는 패턴을 가지고 있습니다. 예를 들어, u8은 0에서부터 255까지 유효한 값을 가질 수 있습니다. 만일 우리가 1, 3, 5, 그리고 7 값에 대해서만 신경 쓰고자 한다면, 나머지 0, 2, 4, 6, 8, 그리고 9부터 255까지를 모두 나열하고 싶진 않을 겁니다. 다행히도, 그럴 필요 없습니다: 대신 특별 패턴인 _를 이용할 수 있습니다. let some_u8_value = 0u8;\nmatch some_u8_value { 1 => println!(\"one\"), 3 => println!(\"three\"), 5 => println!(\"five\"), 7 => println!(\"seven\"), _ => (),\n} _ 패턴은 어떠한 값과도 매칭 될 것입니다. 우리의 다른 갈래 뒤에 이를 집어넣음으로써, _는 그전에 명시하지 않은 모든 가능한 경우에 대해 매칭 될 것입니다. ()는 단지 단위 값이므로, _ 케이스에서는 어떤 일도 일어나지 않을 것입니다. 결과적으로, 우리가 _ 변경자 이전에 나열하지 않은 모든 가능한 값들에 대해서는 아무것도 하고 싶지 않다는 것을 말해줄 수 있습니다. 하지만 match 표현식은 우리가 단 한 가지 경우에 대해 고려하는 상황에서는 다소 장황할 수 있습니다. 이러한 상황을 위하여, 러스트는 if let을 제공합니다.","breadcrumbs":"열거형과 패턴 매칭 » match 흐름 제어 연산자 » _ 변경자(placeholder)","id":"106","title":"_ 변경자(placeholder)"},"107":{"body":"if let 문법은 if와 let을 조합하여 하나의 패턴만 매칭 시키고 나머지 경우는 무시하는 값을 다루는 덜 수다스러운 방법을 제공합니다. 어떤 Option 값을 매칭 하지만 그 값이 3일 경우에만 코드를 실행시키고 싶어 하는 Listing 6-6에서의 프로그램을 고려해 보세요: let some_u8_value = Some(0u8);\nmatch some_u8_value { Some(3) => println!(\"three\"), _ => (),\n} Listing 6-6: 어떤 값이 Some(3) 일 때에만 코드를 실행하도록 하는 match 우리는 Some(3)에 매칭 되는 경우에만 뭔가를 하지만 다른 Some 값 혹은 None 값인 경우에는 아무것도 하지 않고 싶습니다. 이러한 match 표현식을 만족시키기 위해, _ => ()을 단 하나의 variant를 처리한 다음에 추가해야 하는데, 이는 추가하기에 너무 많은 보일러 플레이트 코드입니다. 그 대신, if let을 이용하여 이 코드를 더 짧게 쓸 수 있습니다. 아래의 코드는 Listing 6-6에서의 match와 동일하게 동작합니다: # let some_u8_value = Some(0u8);\nif let Some(3) = some_u8_value { println!(\"three\");\n} if let은 =로 구분된 패턴과 표현식을 입력받습니다. 이는 match와 동일한 방식으로 작동하는데, 여기서 표현식은 match에 주어지는 것이고 패턴은 이 match의 첫 번째 갈래와 같습니다. if let을 이용하는 것은 여러분이 덜 타이핑하고, 덜 들여 쓰기 하고, 보일러 플레이트 코드를 덜 쓰게 된다는 뜻입니다. 하지만, match가 강제했던 하나도 빠짐없는 검사를 잃게 되었습니다. match와 if let 사이에서 선택하는 것은 여러분의 특정 상황에서 여러분이 하고 있는 것에 따라, 그리고 간결함을 얻는 것이 전수 조사를 잃는 것에 대한 적절한 거래인지에 따라 달린 문제입니다. 바꿔 말하면, 여러분은 if let를 어떤 값이 하나 패턴에 매칭 되었을 때 코드를 실행하고 다른 값들에 대해서는 무시하는 match 문을 위한 문법적 설탕(syntax sugar)으로 생각할 수 있습니다. if let과 함께 else를 포함시킬 수 있습니다. else 뒤에 나오는 코드 블록은 match 표현식에서 _ 케이스 뒤에 나오는 코드 블록과 동일합니다. Listing 6-4에서 Quarter variant가 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# let coin = Coin::Penny;\nlet mut count = 0;\nmatch 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# let coin = Coin::Penny;\nlet mut count = 0;\nif let Coin::Quarter(state) = coin { println!(\"State quarter from {:?}!\", state);\n} 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":"여러분이 러스트로 프로그램을 작성하기 시작했을 때, 여러분의 코드는 오로지 main 함수 안에만 있을지도 모르겠습니다. 코드가 커짐에 따라서, 여러분은 재사용 및 더 나은 조직화를 위하여 결국 어떤 기능을 다른 함수로 이동시킬 것입니다. 코드를 더 작은 덩어리로 쪼갬으로서, 각각의 덩어리들은 개별적으로 이해하기 더 수월해집니다. 하지만 함수가 너무 많으면 어떤 일이 벌어질까요? 러스트는 조직화된 방식으로 코드의 재사용을 할 수 있게 해주는 모듈(module) 시스템을 갖추고 있습니다. 코드 몇줄을 함수로 추출하는 것과 같은 방식으로, 여러분은 함수 (혹은 구조체나 열거형 같은 다른 코드들)를 다른 모듈로 뽑아낼 수 있으며, 여러분은 이것들의 정의가 모듈의 바깥쪽에서 볼 수 있도록 하거나(public) 혹은 보이지 않게 하도록 (private) 선택할 수 있습니다. 모듈이 어떤 식으로 동작하는지에 대한 개요를 봅시다: mod 키워드는 새로운 모듈을 선언합니다. 모듈 내의 코드는 이 선언 바로 뒤에 중괄호 로 묶여서 따라오거나 다른 파일에 놓일 수 있습니다. 기본적으로, 함수, 타입, 상수, 그리고 모듈은 private입니다. pub 키워드가 어떤 아이템을 public하게 만들어줘서 이것의 네임스페이스 바깥쪽에서도 볼 수 있도록 합니다. use 키워드는 모듈이나 모듈 내의 정의들을 스코프 안으로 가져와서 이들을 더 쉽게 참조할 수 있도록 합니다. 각각의 부분들을 살펴보면서 이것들이 전체적으로 어떻게 맞물리는지 살펴봅시다.","breadcrumbs":"모듈 » 모듈을 사용하여 코드를 재사용하고 조직화하기","id":"109","title":"모듈을 사용하여 코드를 재사용하고 조직화하기"},"11":{"body":"여러분의 러스트 여정을 시작해봅시다! 이 장에서는 다음을 다룰 것입니다: Linux, macOS, Windows에 러스트 설치하기 \"Hello, world!\"를 출력하는 프로그램 작성하기 러스트의 패키지 매니저이자 빌드 시스템인 cargo 사용하기","breadcrumbs":"시작하기 » 시작하기","id":"11","title":"시작하기"},"110":{"body":"먼저 카고를 이용해서 새로운 프로젝트를 만드는 것으로 모듈 예제를 시작하려고 하는데, 바이너리 크레이트(crate)을 만드는 대신에 라이브러리 크레이트을 만들 것입니다. 여기서 라이브러리 크레이트이란 다른 사람들이 자신들의 프로젝트에 디펜던시(dependency)로 추가할 수 있는 프로젝트를 말합니다. 예를 들어, 2장의 rand 크레이트은 우리가 추리 게임 프로젝트에서 디펜던시로 사용했던 라이브러리 크레이트입니다. 우리는 몇가지 일반적인 네트워크 기능을 제공하는 라이브러리의 뼈대를 만들 것입니다; 여기서는 모듈들과 함수들의 조직화에 집중할 것이고, 함수의 본체에 어떤 코드가 들어가야 하는지는 신경쓰지 않겠습니다. 이 라이브러리를 communicator라고 부르겠습니다. 라이브러리를 만들기 위해서는 --bin 대신 --lib 옵션을 넘기세요: $ cargo new communicator --lib\n$ cd communicator 카고가 src/main.rs 대신 src/lib.rs 을 생성했음을 주목하세요. src/lib.rs 내부를 보면 다음과 같은 코드를 찾을 수 있습니다: Filename: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { }\n} 카고는 우리가 만든 라이브러리의 작성 시작을 돕기 위해 빈 테스트를 만드는데, 이는 --bin 옵션을 사용했을때 “Hello, world!” 바이너리를 만들어준 것과 사뭇 다릅니다. #[]와 mod tests 문법은 이 장의 “super를 이용하여 부모 모듈에 접근하기”절에서 더 자세히 다룰 것이지만, 당장은 src/lib.rs 의 아래쪽에 이 코드를 남겨두겠습니다. src/main.rs 파일이 없기 때문에, cargo run 커맨드로 카고가 실행할 것이 없습니다. 따라서, 여기서는 라이브러리 크레이트의 코드를 컴파일하기 위해 cargo build를 사용할 것입니다. 이제 여러분이 작성하는 코드의 의도에 따라 만들어지는 다양한 상황에 알맞도록 라이브러리 코드를 조직화하는 다양한 옵션들을 살펴보겠습니다.","breadcrumbs":"모듈 » mod와 파일 시스템 » mod와 파일 시스템","id":"110","title":"mod와 파일 시스템"},"111":{"body":"우리의 communicator 네트워크 라이브러리를 위해서, 먼저 connect라는 이름의 함수가 정의되어 있는 network라는 이름의 모듈을 정의하겠습니다. 러스트 내 모듈 정의는 모두 mod로 시작됩니다. 이 코드를 src/lib.rs 의 시작 부분, 즉 테스트 코드의 윗 쪽에 추가해봅시다: Filename: src/lib.rs mod network { fn connect() { }\n} mod 키워드 뒤에, 모듈의 이름 network가 쓰여지고 중괄호 안에 코드 블록이 옵니다. 이 블록 안의 모든 것은 이름공간 network 안에 있습니다. 위의 경우 connect라는 이름의 함수 하나가 있습니다. 이 함수를 network 모듈 바깥의 스크립트에서 호출하고자 한다면, 우리는 모듈을 특정할 필요가 있으므로 이름공간 문법 ::를 이용해야 합니다: connect() 이렇게만 하지 않고 network::connect() 이런 식으로요. 또한 같은 src/lib.rs 파일 내에 여러 개의 모듈을 나란히 정의할 수도 있습니다. 예를 들어, connect라는 이름의 함수를 갖고 있는 client 모듈을 정의하려면, Listing 7-1에 보시는 바와 같이 이를 추가할 수 있습니다: Filename: src/lib.rs mod network { fn connect() { }\n} mod client { fn connect() { }\n} Listing 7-1: src/lib.rs 내에 나란히 정의된 network 모듈과 client 모듈 이제 우리는 network::connect 함수와 client::connect 함수를 갖게 되었습니다. 이들은 완전히 다른 기능을 갖고 있을 수 있고, 서로 다른 모듈에 정의되어 있기 때문에 함수 이름이 서로 부딪힐 일은 없습니다. 이 경우, 우리가 라이브러리를 만드는 중이기 때문에, 라이브러리의 시작 지점으로서 제공되는 파일은 src/lib.rs 입니다. 하지만 모듈을 만드는 것에 관하여 src/lib.rs 는 특별할 것이 없습니다. 우리는 라이브러리 크레이트의 src/lib.rs 내에 모듈을 만드는 것과 똑같은 방식으로 바이너리 크레이트의 src/main.rs 내에도 모듈을 만들 수 있습니다. 사실 모듈 안에 다른 모듈을 집어넣는 것도 가능한데, 이는 여러분의 모듈이 커짐에 따라 관련된 기능이 잘 조직화 되도록 하는 한편 각각의 기능을 잘 나누도록 하는데 유용할 수 있습니다. 여러분의 코드를 어떻게 조직화 할 것인가에 대한 선택은 여러분이 코드의 각 부분 간의 관계에 대해 어떻게 생각하고 있는지에 따라 달라집니다. 예를 들어, Listing 7-2와 같이 client 모듈과 connect 함수가 network 이름공간 내에 있다면 우리의 라이브러리 사용자가 더 쉽게 이해할지도 모릅니다: Filename: src/lib.rs mod network { fn connect() { } mod client { fn connect() { } }\n} Listing 7-2: client 모듈을 network 모듈 안으로 이동 src/lib.rs 파일에서 Listing 7-2와 같이 client 모듈이 network 모듈의 내부 모듈이 되도록 mod network와 mod client의 위치를 바꿔 봅시다. 이제 우리는 network::connect와 network::client::connect 함수를 갖게 되었습니다: 다시 말하지만, connect라는 이름의 두 함수는 서로 다른 이름공간에 있으므로 부딪힐 일이 없습니다. 이런 식으로 모듈들은 계층을 구성하게 됩니다. src/lib.rs 의 내용은 가장 위의 층을 이루고, 서브 모듈들은 그보다 낮은 층에 있습니다. Listing 7-1 예제에서의 조직화가 계층 구조를 생각했을 때 어떻게 보일지 살펴봅시다: communicator ├── network └── client 그리고 Listing 7-2 예제에 대응되는 계층 구조는 이렇습니다: communicator └── network └── client Listing 7-2에서 계층 구조는 client가 network의 형제이기 보다는 자식임을 보여줍니다. 더 복잡한 프로젝트는 많은 수의 모듈을 갖고 있을 수 있고, 이들은 지속적인 트래킹을 위해 논리적으로 잘 조직화될 필요가 있을 것입니다. 여러분의 프로젝트 내에서 “논리적으로”가 의미하는 것은 여러분에게 달려 있는 것이며, 여러분과 여러분의 라이브러리 사용자들이 프로젝트 도메인에 대해 어떻게 생각하는지에 따라 달라집니다. 여러분이 선호하는 어떤 형태의 구조이건 간에 여기서 보여준 나란한 모듈 및 중첩된(nested) 모듈을 만드는 테크닉을 이용해 보세요.","breadcrumbs":"모듈 » mod와 파일 시스템 » 모듈 정의","id":"111","title":"모듈 정의"},"112":{"body":"모듈은 계층적인 구조를 형성하는데, 여러분이 익숙하게 사용하고 있는 다른 구조와 매우 닮았습니다: 바로 파일 시스템이죠! 러스트에서는 프로젝트를 잘게 나누기 위해 여러 개의 파일 상에서 모듈 시스템을 사용할 수 있어, 모든 것들이 src/lib.rs 나 src/main.rs 안에 존재하지 않게할 수 있습니다. 이러한 예를 위해서, Listing 7-3에 있는 코드를 시작해봅시다: Filename: src/lib.rs mod client { fn connect() { }\n} mod network { fn connect() { } mod server { fn connect() { } }\n} Listing 7-3: 세 개의 모듈 client, network, network::server가 모두 src/lib.rs 에 정의되어 있음 파일 src/lib.rs 는 아래와 같은 모듈 계층을 갖고 있습니다: communicator ├── client └── network └── server 만일 이 모듈들이 여러 개의 함수들을 갖고 있고, 이 함수들이 길어지고 있다면, 우리가 작업하고자 하는 코드를 찾으려고 이 파일을 스크롤 하기가 까다로워질 것입니다. 함수들은 하나 혹은 그 이상의 mod 블록 안에 포함되어 있기 때문에, 함수 내의 코드 라인들 또한 길어지기 시작할 것입니다. 이는 client, network, 그리고 server 모듈을 src/lib.rs 로부터 떼어내어 각자를 위한 파일들에 위치시키기 좋은 이유가 되겠습니다. 먼저 client 모듈의 코드를 client 모듈의 선언 부분만 남겨두는 것으로 바꾸세요. 그러니까 여러분의 src/lib.rs 는 아래와 같이 될 것입니다: Filename: src/lib.rs mod client; mod network { fn connect() { } mod server { fn connect() { } }\n} 여기서는 여전히 client 모듈을 선언 하고 있지만, 코드 블록을 세미콜론으로 대체함으로써, 우리는 러스트에게 client 모듈의 스코프 내에 정의된 코드를 다른 위치에서 찾으라고 말하는 것입니다. 달리 말하면, mod client;라는 라인의 뜻은 이렇습니다: mod client { // contents of client.rs\n} 이제 모듈의 이름과 같은 이름을 가진 외부 파일을 만들 필요가 있습니다. client.rs 파일을 여러분의 src/ 디렉토리에 생성하고 여세요. 그런 뒤 아래와 같이 앞 단계에서 제거했던 client 모듈내의 connect 함수를 입력해세요: Filename: src/client.rs fn connect() {\n} 이미 src/lib.rs 안에다 client 모듈을 mod를 이용하여 선언을 했기 때문에, 이 파일 안에는 mod 선언이 필요없다는 점을 기억하세요. 이 파일은 단지 client 모듈의 내용물 만 제공할 뿐입니다. 만일 mod client를 여기에 또 집어넣는다면, 이는 client 모듈 내에 서브모듈 client를 만들게 됩니다! 러스트는 기본적으로 src/lib.rs 만 찾아볼줄 압니다. 만약에 더 많은 파일을 프로젝트에 추가하고 싶다면, src/lib.rs 내에서 다른 파일을 찾아보라고 러스트에게 말해줄 필요가 있습니다; 이는 mod client라는 코드가 왜 src/lib.rs 내에 정의될 필요가 있는지, 그리고 src/client.rs 내에는 정의될 수 없는지에 대한 이유입니다. 이제 몇 개의 컴파일 경고가 생기지만, 프로젝트는 성공적으로 컴파일 되어야 합니다. 우리가 바이너리 크레이트 대신 라이브러리 크레이트를 만드는 중이므로 cargo run 대신 cargo build를 이용해야 한다는 점을 기억해두세요: $ cargo build Compiling communicator v0.1.0 (file:///projects/communicator) warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/client.rs:1:1 |\n1 | fn connect() { | ^ warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/lib.rs:4:5 |\n4 | fn connect() { | ^ warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/lib.rs:8:9 |\n8 | fn connect() { | ^ 이 경고들은 사용된 적이 없는 함수가 있음을 우리에게 알려줍니다. 지금은 이 경고들을 너무 걱정하지 마세요: 이 장의 뒤에 나오는 “pub을 이용하여 가시성 제어하기”절에서 이 문제에 대해 알아볼 것입니다. 좋은 소식은 이들이 그냥 경고일 뿐이란 것입니다; 우리 프로젝트는 성공적으로 빌드됐습니다! 다음으로 같은 방식을 이용하여 network 모듈을 개별 파일로 추출해봅시다. src/lib.rs 안에서, 아래와 같이 network 모듈의 몸체를 지우고 선언부의 끝부분에 세미콜론을 붙이세요: Filename: src/lib.rs mod client; mod network; 그리고나서 새로운 src/network.rs 파일을 만들어서 아래를 입력하세요: Filename: src/network.rs fn connect() {\n} mod server { fn connect() { }\n} 이 모듈 파일 내에는 mod 선언이 여전히 있음을 주목하세요; 이는 server가 network의 서브모듈로서 여전히 필요하기 때문입니다. cargo build를 다시 실행시키세요. 성공! 여기 또 추출할만한 모듈이 하나 더 있습니다: server 말이죠. 이것이 서브모듈(즉, 모듈 내의 모듈)이기 때문에, 모듈을 파일로 추출해서 파일 이름을 모듈 이름으로 사용하는 전략은 사용하기 힘듭니다. 어쨌든 시도해서 에러를 확인해보겠습니다. 먼저, src/network.rs 내에서 server 모듈의 내용물 대신에 mod server을 쓰세요: Filename: src/network.rs fn connect() {\n} mod server; 그후 src/server.rs 파일을 만들고 추출해둔 server 모듈의 내용물을 입력하세요: Filename: src/server.rs fn connect() {\n} cargo build를 실행해보면, Listing 7-4와 같은 에러를 얻게 됩니다: $ cargo build Compiling communicator v0.1.0 (file:///projects/communicator)\nerror: cannot declare a new module at this location --> src/network.rs:4:5 |\n4 | mod server; | ^^^^^^ |\nnote: maybe move this module `network` to its own directory via `network/mod.rs` --> src/network.rs:4:5 |\n4 | mod server; | ^^^^^^\nnote: ... or maybe `use` the module `server` instead of possibly redeclaring it --> src/network.rs:4:5 |\n4 | mod server; | ^^^^^^ Listing 7-4: server 서브모듈을 src/server.rs 로 추출을 시도했을 때 발생하는 에러 에러는 이 위치에 새로운 모듈을 선언할수 없다고 말해주며 src/network.rs 의 mod server; 라인을 지적하고 있습니다. src/network.rs 는 src/lib.rs 와는 다소 다릅니다: 왜 그런지 이해하려면 계속 읽어주세요. Listing 7-4의 중간의 노트는 실질적으로 매우 도움이 되는데, 그 이유는 우리가 아직 설명하지 않은 무언가를 지적하고 있기 때문입니다: note: maybe move this module `network` to its own directory via\n`network/mod.rs` 전에 사용했던 똑같은 파일 이름 쓰기 패턴을 계속해서 따르는 대신, 아래 노트에서 제안하는 것을 해볼 수 있습니다: 부모 모듈의 이름에 해당하는, network 라는 이름의 새로운 디렉토리 를 만드세요. src/network.rs 파일을 이 새로운 network 디렉토리 안으로 옮기고, 파일 이름을 src/network/mod.rs 로 고치세요. 서브모듈 파일 src/server.rs 를 network 디렉토리 안으로 옮기세요. 위의 단계들을 실행하기 위한 명령들입니다: $ mkdir src/network\n$ mv src/network.rs src/network/mod.rs\n$ mv src/server.rs src/network 이제 cargo build를 다시 실행하면, 컴파일은 작동할 것입니다 (여전히 경고는 좀 있지만요). 우리의 모듈 레이아웃은 여전히 아래와 같이 되는데, 이는 Listing 7-3의 src/lib.rs 내의 코드에서 만든 것과 정확하게 동일합니다: communicator ├── client └── network └── server 이에 대응하는 파일 레이아웃는 아래와 같이 생겼습니다: ├── src\n│ ├── client.rs\n│ ├── lib.rs\n│ └── network\n│ ├── mod.rs\n│ └── server.rs 그러니까 우리가 network::server 모듈을 추출하고자 할 때, 왜 network::server 모듈을 src/server.rs 로 추출하는 대신, src/network.rs 파일을 src/network/mod.rs 로 바꾸고 network::server 코드를 network 디렉토리 안에 있는 src/network/server.rs 에 넣었을까요? 그 이유는 src 디렉토리 안에 server.rs 파일이 있으면, 러스트는 server가 network의 서브모듈이라고 인식할 수 없기 때문입니다. 러스트가 동작하는 방식을 명확하게 알기 위해서, 아래와 같은 모듈 계층 구조를 가진, src/lib.rs 내에 모든 정의가 다 들어있는 다른 예제를 봅시다: communicator ├── client └── network └── client 이 예제에는 또다시 client, network, 그리고 network::client라는 세 개의 모듈이 있습니다. 모듈을 파일로 추출하기 위해 앞서 했던 단계를 따르면, client 모듈을 위한 src/client.rs 을 만들게 될 것입니다. network 모듈을 위해서는 src/network.rs 파일을 만들게 될 것입니다. 하지만 network::client 모듈을 src/client.rs 로 추출하는 것은 불가능한데, 그 이유는 최상위 층에 client 모듈이 이미 있기 때문이죠! 만일 client와 network::client 모듈 둘다 src/client.rs 파일에 집어넣는다면, 러스트는 이 코드가 client를 위한 것인지, 아니면 network::client를 위한 것인지 알아낼 방법이 없을 것입니다. 따라서, network 모듈의 network::client 서브모듈을 위한 파일을 추출하기 위해서는 src/network.rs 파일 대신 network 모듈을 위한 디렉토리를 만들 필요가 있습니다. network 모듈 내의 코드는 그후 src/network/mod.rs 파일로 가고, 서브모듈 network::client은 src/network/client.rs 파일을 갖게할 수 있습니다. 이제 최상위 층의 src/client.rs 는 모호하지 않게 client 모듈이 소유한 코드가 됩니다.","breadcrumbs":"모듈 » mod와 파일 시스템 » 모듈을 다른 파일로 옮기기","id":"112","title":"모듈을 다른 파일로 옮기기"},"113":{"body":"파일에 관한 모듈의 규칙을 정리해봅시다: 만일 foo라는 이름의 모듈이 서브모듈을 가지고 있지 않다면, foo.rs 라는 이름의 파일 내에 foo에 대한 선언을 집어넣어야 합니다. 만일 foo가 서브모듈을 가지고 있다면, foo/mod.rs 라는 이름의 파일에 foo에 대한 선언을 집어넣어야 합니다. 이 규칙들은 재귀적으로 적용되므로, foo라는 이름의 모듈이 bar라는 이름의 서브모듈을 갖고 있고 `bar는 서브모듈이 없다면, 여러분의 src 디렉토리 안에는 아래와 같은 파일들이 있어야 합니다: ├── foo\n│ ├── bar.rs (contains the declarations in `foo::bar`)\n│ └── mod.rs (contains the declarations in `foo`, including `mod bar`) 이 모듈들은 부모 모듈의 파일에 mod 키워드를 사용하여 선언되어 있어야 합니다. 다음으로, pub 키워드에 대해 알아보고 앞의 그 경고들을 없애봅시다!","breadcrumbs":"모듈 » mod와 파일 시스템 » 모듈 파일 시스템의 규칙","id":"113","title":"모듈 파일 시스템의 규칙"},"114":{"body":"우리는 network와 network::server 코드를 각각 src/network/mod.rs 와 src/network/server.rs 파일 안으로 이동시켜서 Listing 7-4에 나온 에러 메세지를 해결했습니다. 이 지점에서 cargo build로 프로젝트를 빌드할 수 있긴 했지만, 사용하지 않고 있는 client::connect, network::connect, 그리고 network::server::connect 함수에 대한 경고 메세지를 보게 됩니다: warning: function is never used: `connect`, #[warn(dead_code)] on by default\nsrc/client.rs:1:1 |\n1 | fn connect() { | ^ warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/network/mod.rs:1:1 |\n1 | fn connect() { | ^ warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/network/server.rs:1:1 |\n1 | fn connect() { | ^ 그럼 이런 경고들은 왜 나오는 걸까요? 결국, 우리는 우리 자신의 프로젝트 내에서 사용할 필요가 있는 것이 아닌, 사용자 가 사용할 수 있도록 만들어진 함수들의 라이브러리를 만드는 중이므로, 이런 connect 함수 등이 사용되지 않는 것은 큰 문제가 아닙니다. 이 함수들을 만든 의도는 함수들이 우리의 지금 이 프로젝트가 아닌 또다른 프로젝트에 사용될 것이란 점입니다. 이 프로그램이 이러한 경고들을 들먹이는 이유를 이해하기 위해, connect 라이브러리 를 다른 프로젝트에서 사용하기를 시도해 봅시다. 이를 위해서, 아래의 코드를 담은 src/main.rs 파일을 만듦으로서 같은 디렉토리에 라이브러리 크레이트와 마찬가지로 바이너리 크레이트를 만들겠습니다: Filename: src/main.rs extern crate communicator; fn main() { communicator::client::connect();\n} communicator 라이브러리 크레이트를 가져오기 위해 extern crate 명령어를 사용합니다. 우리의 패키지는 이제 두 개의 크레이트를 담고 있습니다. 카고는 src/main.rs 를 바이너리 크레이트의 루트 파일로 취급하는데, 이 바이너리 크레이트는 src/lib.rs 가 루트 파일인 이미 있던 라이브러리 크레이트는 별개입니다. 이러한 패턴은 실행 가능한 프로젝트에서 꽤 흔합니다: 대부분의 기능은 라이브러리 크레이트 안에 있고, 바이너리 크레이트는 이 라이브러리 크레이트를 이용합니다. 결과적으로, 다른 프로그램 또한 그 라이브러리 크레이트를 이용할 수 있고, 이는 멋지게 근심을 덜어줍니다. communicator 라이브러리 밖의 크레이트가 안을 들여다 보는 시점에서, 우리가 만들어왔던 모든 모듈들은 communicator라는 이름을 갖는 모듈 내에 있습니다. 크레이트의 최상위 모듈을 루트 모듈 (root module) 이라 부릅니다. 또한. 비록 우리의 프로젝트의 서브모듈 내에서 외부 크레이트를 이용하고 있을지라도, extern crate이 루트 모듈에 와 있어야 한다는 점(즉 src/main.rs 혹은 src/lib.rs )을 기억하세요. 그러면 서브모듈 안에서 마치 최상위 모듈의 아이템을 참조하듯 외부 크레이트로부터 아이템들을 참조할 수 있습니다. 현시점에서 우리의 바이너리 크레이트는 고작 라이브러리의 client 모듈로부터 connect 함수를 호출할 뿐입니다. 하지만 cargo build을 실행하면 경고들 이후에 에러를 표시할 것입니다: error: module `client` is private --> src/main.rs:4:5 |\n4 | communicator::client::connect(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 아하! 이 에러는 client 모듈이 비공개(private) 임을 알려주고 있는데, 이는 그 경고들의 요점입니다. 또한 러스트의 내용 중에서 공개(public) 그리고 비공개(private) 에 대한 개념에 대해 알아보게 될 첫번째 시간입니다. 러스트의 모든 코드의 기본 상태는 비공개입니다: 즉, 다른 사람은 이 코드를 사용할 수 없습니다. 만일 여러분의 프로그램 내에서 비공개 함수를 이용하지 않는다면, 여러분의 프로그램이 그 함수를 이용할 수 있는 유일한 곳이기 때문에, 러스트는 그 함수가 사용된 적이 없다며 경고해줄 것입니다. client::connect와 같은 함수를 공개로 지정한 뒤에는 우리의 바이너리 크레이트 상에서 이 함수를 호출하는 것이 가능해질 뿐만 아니라, 그 함수가 사용된 적이 없다는 경고 또한 사라질 것입니다. 함수를 공개로 표시하는 것은 러스트로 하여금 그 함수가 우리 프로그램 외부의 코드에 의해 사용될 것이라는 점을 알게끔 해줍니다. 러스트는 이제부터 가능하게 된 이론적인 외부 사용에 대해 이 함수가 “사용되었다”라고 간주합니다. 따라서, 어떤 것이 공개로 표시될 때, 러스트는 그것이 우리 프로그램 내에서 이용되는 것을 요구하지 않으며 해당 아이템이 미사용에 대한 경고를 멈출 것입니다.","breadcrumbs":"모듈 » pub으로 가시성 제어하기 » pub으로 가시성(visibility) 제어하기","id":"114","title":"pub으로 가시성(visibility) 제어하기"},"115":{"body":"러스트에게 어떤 것을 공개하도록 말하기 위해서는, 공개하길 원하는 아이템의 선언 시작 부분에 pub 키워드를 추가합니다. 지금은 client::connect가 사용된 적 없음을 알리는 경고와 바이너리 크레이트에서 나온 module `client` is private 에러를 제거하는데 집중하겠습니다. 아래와 같이 src/lib.rs 을 수정하여 client 모듈을 공개로 만드세요: Filename: src/lib.rs pub mod client; mod network; pub 키워드는 mod 바로 전에 위치합니다. 다시 빌드를 시도해봅시다: error: function `connect` is private --> src/main.rs:4:5 |\n4 | communicator::client::connect(); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 만세! 다른 에러가 나왔습니다! 네, 다른 에러 메세지라는건 축하할만한 이유죠. 새로운 에러는 function `connect` is private라고 하고 있으므로, src/client.rs 를 수정해서 client::connect도 공개로 만듭시다: Filename: src/client.rs pub fn connect() {\n} 이제 cargo build를 다시 실행하면: warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/network/mod.rs:1:1 |\n1 | fn connect() { | ^ warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/network/server.rs:1:1 |\n1 | fn connect() { | ^ 코드가 컴파일되었고, client::connect가 사용된 적 없다는 것에 대한 경고도 사라집니다! 미사용 코드 경고가 항상 여러분의 코드에 있는 아이템이 공개로 만들 필요가 있음을 나타내는 것은 아닙니다: 이 함수들이 여러분의 공개 API의 일부분으로서 들어가길 원하지 않는다면 , 미사용 코드 경고는 여러분에게 해당 코드가 더이상 필요 없고 안전하게 지울 수 있음을 알려줄 수 있습니다. 또한 이 경고는 여러분의 라이브러리 내에서 해당 함수가 호출된 모든 곳을 실수로 지웠을 경우 발생할 수 있는 버그를 알려줄 수도 있습니다. 하지만 지금의 경우, 우리는 다른 두 함수들이 우리 크레이트의 공개 API의 일부분이 되길 원하고 있으므로, 이들에게 pub를 표시해줘서 남은 경고들을 제거합시다. src/network/mod.rs 를 아래와 같이 수정하세요: Filename: src/network/mod.rs pub fn connect() {\n} mod server; 그리고 컴파일하면: warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/network/mod.rs:1:1 |\n1 | pub fn connect() { | ^ warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/network/server.rs:1:1 |\n1 | fn connect() { | ^ 흠, network::connect가 pub으로 설정되어 있음에도, 여전히 미사용 함수 경고가 나옵니다. 그 이유는 함수가 모듈 내에서 공개지만, 함수가 상주해 있는 network 모듈은 공개가 아니기 때문입니다. 이번에는 모듈의 안쪽에서 작업하고 있지만, client::connect에서는 바깥쪽에서 작업을 했었죠. src/lib.rs 을 수정하여 network가 공개가 되도록 할 필요가 있습니다. 이렇게요: Filename: src/lib.rs pub mod client; pub mod network; 이제 컴파일하면, 그 경고는 사라집니다: warning: function is never used: `connect`, #[warn(dead_code)] on by default --> src/network/server.rs:1:1 |\n1 | fn connect() { | ^ 경고 딱 하나 남았네요! 여러분이 직접 고쳐보세요!","breadcrumbs":"모듈 » pub으로 가시성 제어하기 » 함수를 공개로 만들기","id":"115","title":"함수를 공개로 만들기"},"116":{"body":"종합해보면, 아이템 가시성에 관한 규칙은 다음과 같습니다: 만일 어떤 아이템이 공개라면, 이는 부모 모듈의 어디에서건 접근 가능합니다. 만일 어떤 아이템이 비공개라면, 같은 파일 내에 있는 부모 모듈 및 이 부모의 자식 모듈에서만 접근 가능합니다.","breadcrumbs":"모듈 » pub으로 가시성 제어하기 » 비공개 규칙(Privacy Rules)","id":"116","title":"비공개 규칙(Privacy Rules)"},"117":{"body":"연습을 위해 몇 가지 비공개에 관한 예제를 봅시다. 새로운 라이브러리 프로젝트를 만들고 이 새로운 프로젝트의 src/lib.rs 에 Listing 7-5와 같이 코드를 넣으세요: Filename: src/lib.rs mod outermost { pub fn middle_function() {} fn middle_secret_function() {} mod inside { pub fn inner_function() {} fn secret_function() {} }\n} fn try_me() { outermost::middle_function(); outermost::middle_secret_function(); outermost::inside::inner_function(); outermost::inside::secret_function();\n} Listing 7-5: 비공개 및 공개 함수 예제. 몇 가지는 잘못되었음. 이 코드를 컴파일하기 전에, try_me 함수의 어떤 라인이 에러를 발생시킬지 추측해보세요. 그리고나서 컴파일을 하여 여러분이 맞았는지 확인하고, 에러에 대한 논의를 위해 계속 읽어주세요! 에러 보기 try_me 함수는 우리 프로젝트의 루트 모듈 내에 있습니다. outermost 모듈은 비공개지만, 두 번째 비공개 규칙은 try_me함수가 outermost 모듈에 접근하는 것이 허용됨을 알려주는데, 이는 outermost가 try_me 함수와 마찬가지로 현재의 (루트) 모듈 내에 있기 때문입니다. middle_function이 공개이므로 outermost::middle_function 호출은 작동할 것이며, try_me는 middle_function의 부모 모듈인 outermost를 통해 middle_function에 접근하고 있습니다. 이 모듈에 접근 가능하다는 것은 이전 문단에서 알아냈죠. outermost::middle_secret_function 호출은 컴파일 에러를 일으킬 것입니다. middle_secret_function는 비공개이므로, 두번째 규칙이 적용됩니다. 루트 모듈은 middle_secret_function의 현재 모듈도 아니고 (outermost가 현재 모듈입니다), middle_secret_function의 현재 모듈의 자식 모듈도 아닙니다. inside 모듈은 비공개고 자식 모듈이 없으므로, 이것의 현재 모듈인 outermost에 의해서만 접근될 수 있습니다. 이는 즉 try_me 함수는 outermost::inside::inner_function나 outermost::inside::secret_function를 호출할 수 없음을 의미합니다. 에러 고치기 여기 이 에러들을 고치기 위해 코드를 수정하는것에 관한 몇 가지 제안이 있습니다. 각각을 시도해보기 전에, 이 시도가 에러를 고칠지 그렇지 않을지 추측해 보고, 컴파일을 해서 여러분이 맞췄는지 그렇지 않은지 확인하고, 왜 그랬는지 이해하기 위해 비공개 규칙을 이용해보세요. inside 모듈이 공개라면 어떨까요? outermost가 공개고 inside가 비공개면 어떨까요? inner_function의 내부에서 ::outermost::middle_secret_function()을 호출한다면 어떨까요? (시작 부분의 콜론 두개는 루트 모듈로부터 시작하여 모듈을 참조하고 싶음을 나타냅니다) 자유롭게 더 많은 실험을 설계하고 시도해 보세요! 다음으로, use 키워드를 사용하여 아이템을 스코프 내로 가져오는 것에 대해 이야기해 봅시다.","breadcrumbs":"모듈 » pub으로 가시성 제어하기 » 비공개 예제(Privacy Examples)","id":"117","title":"비공개 예제(Privacy Examples)"},"118":{"body":"우리는 Listing 7-6에서 보시는 것과 같이 nested_modules 함수를 호출하는 것처럼, 모듈 이름을 호출 구문의 일부분으로 사용하여 해당 모듈 내에 정의된 함수를 호출하는 방법을 다룬바 있습니다: Filename: src/main.rs pub mod a { pub mod series { pub mod of { pub fn nested_modules() {} } }\n} fn main() { a::series::of::nested_modules();\n} Listing 7-6: 함수에 인접한 모듈 경로를 완전히 특정한 함수 호출하기 보시다시피 완전하게 경로를 지정한 이름을 참조하는 것은 너무 길어질 수 있습니다. 다행히도 러스트는 이러한 호출을 더 간결하게 만들어주는 키워드를 가지고 있습니다.","breadcrumbs":"모듈 » use로 이름 가져오기 » 이름 가져오기 (Importing Names)","id":"118","title":"이름 가져오기 (Importing Names)"},"119":{"body":"러스트의 use 키워드는 여러분이 스코프 내에서 호출하고 싶어하는 함수의 모듈을 가져옴으로써 긴 함수 호출을 줄여줍니다. a::series::of 모듈을 바이너리 크레이트의 루트 스코프로 가져온 예제입니다: Filename: src/main.rs pub mod a { pub mod series { pub mod of { pub fn nested_modules() {} } }\n} use a::series::of; fn main() { of::nested_modules();\n} use a::series::of; 줄은 of 모듈을 참조하고 싶은 곳마다 a::series::of 전부를 사용하기 보다는 of를 사용할 수 있다는 뜻입니다. use 키워드는 우리가 명시한 것만 스코프 내로 가져옵니다: 즉 모듈의 자식들을 스코프 내로 가져오지는 않습니다. 이는 nested_modules 함수를 호출하고자 할 때 여전히 of::nested_modules를 사용해야 하는 이유입니다. 다음과 같이 use 구문 안에서 모듈 대신 함수를 명시하여 스코프 내에서 함수를 가져올 수도 있습니다: pub mod a { pub mod series { pub mod of { pub fn nested_modules() {} } }\n} use a::series::of::nested_modules; fn main() { nested_modules();\n} 이렇게 하면 모든 모듈을 안 써주고 함수를 직접 참조하도록 해줍니다. 열거형 또한 모듈과 비슷한 일종의 이름공간을 형성하고 있기 때문에, 열거형의 variant 또한 use를 이용하여 가져올 수 있습니다. 어떠한 use 구문이건 하나의 이름공간으로부터 여러 개의 아이템을 가져오려 한다면, 여러분은 아래와 같이 중괄호와 쉼표를 구문의 마지막 위치에 사용하여 이 아이템들을 나열할 수 있습니다: enum TrafficLight { Red, Yellow, Green,\n} use TrafficLight::{Red, Yellow}; fn main() { let red = Red; let yellow = Yellow; let green = TrafficLight::Green;\n} Green variant에 대해서는 여전히 TrafficLight 이름공간을 명시하고 있는데, 이는 use 구문 내에 Green를 포함하지 않았기 때문입니다.","breadcrumbs":"모듈 » use로 이름 가져오기 » use를 이용한 간결한 가져오기","id":"119","title":"use를 이용한 간결한 가져오기"},"12":{"body":"첫 번째 단계는 러스트를 설치하는 것입니다. 우리는 rustup이라고 하는 러스트 버전 및 관련 도구들을 관리하기 위한 커멘드 라인 도구를 통하여 러스트를 다운로드할 것입니다. 다운로드를 위해서는 인터넷 연결이 필요할 것입니다. 다음 단계들이 러스트 컴파일러의 최신 안정 버전을 설치합니다. 이 책에 나오는 모든 예제들과 출력들은 안정화된 러스트 1.21.0을 사용했습니다. 러스트의 안정성에 대한 보장은 책에 나오는 모든 예제들이 새로운 러스트 버전에서도 계속해서 잘 컴파일 되도록 해줍니다. 버전마다 출력이 약간씩 다를 수도 있는데, 이는 러스트가 종종 에러 메시지와 경고들을 개선하기 때문입니다. 바꿔 말하면, 이 단계들을 이용하여 여러분이 설치한 러스트가 어떤 새로운 안정화 버전이라도 이 책의 내용에 기대하는 수준으로 동작해야 합니다.","breadcrumbs":"시작하기 » 설치하기 » 설치하기","id":"12","title":"설치하기"},"120":{"body":"이름공간 내의 모든 아이템을 가져오기 위해서는 * 문법을 이용할 수 있습니다. 예를 들면: enum TrafficLight { Red, Yellow, Green,\n} use TrafficLight::*; fn main() { let red = Red; let yellow = Yellow; let green = Green;\n} *는 글롭(glob) 이라고 부르며, 이는 이름공간 내에 공개된 모든 아이템을 가져올 것입니다. 여러분은 글롭을 아껴가며 써야 합니다: 글롭은 편리하지만, 여러분이 예상한 것보다 더 많은 아이템을 끌어와서 이름 간의 충돌(naming conflict)의 원인이 될수도 있습니다.","breadcrumbs":"모듈 » use로 이름 가져오기 » *를 이용한 모두(glob) 가져오기","id":"120","title":"*를 이용한 모두(glob) 가져오기"},"121":{"body":"이 장의 시작 부분에서 보셨듯이, 여러분이 라이브러리 크레이트를 만들때, 카고는 여러분들을 위해 tests 모듈을 만들어줍니다. 지금부터 이에 대한 구체적인 부분들을 봅시다. 여러분의 communicator 프로젝트 내에 있는 src/lib.rs 을 여세요: Filename: src/lib.rs pub mod client; pub mod network; #[cfg(test)]\nmod tests { #[test] fn it_works() { }\n} 11장에서 테스트에 관한 더 많은걸 설명하고 있습니다만, 이 예제는 지금도 이해가 되시리라 봅니다: tests라는 이름의 모듈이 우리의 다른 모듈들 옆에 있고, it_works라는 이름의 함수 하나를 담고 있지요. 좀 특별한 주석(annotation)이 있지만, tests 모듈은 그냥 또다른 모듈일 뿐입니다! 따라서 우리의 모듈 계층 구조는 아래와 같이 생겼습니다: communicator ├── client ├── network | └── client └── tests 테스트는 우리 라이브러리 내에 있는 코드를 연습하기 위한 것이므로, 현재로서는 어떠한 기능도 확인할 게 없긴 하지만, it_works 함수 안에서 우리의 client::connect 함수를 호출해 봅시다: Filename: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { client::connect(); }\n} cargo test 명령을 써서 테스트를 실행하면: $ cargo test Compiling communicator v0.1.0 (file:///projects/communicator)\nerror[E0433]: failed to resolve. Use of undeclared type or module `client` --> src/lib.rs:9:9 |\n9 | client::connect(); | ^^^^^^^^^^^^^^^ Use of undeclared type or module `client` 컴파일이 실패했습니다, 하지만 대체 왜일까요? 우리는 src/main.rs 에서 했었던 것과 마찬가지로 함수 앞에 communicator::를 붙일 필요가 없는데, 왜냐하면 이 코드가 분명히 communicator 라이브러리 크레이트 안에 있기 때문입니다. 원인은 경로가 항상 현재 모듈을 기준으로 상대적인데, 여기는 test이기 때문입니다. 딱 하나의 예외는 use 구문인데, 이는 기본적으로 크레이트 루트에 대한 상대적인 경로로 인식됩니다. 우리의 tests 모듈은 이 스코프 내에서 client 모듈이 필요합니다! 그러면 어떻게 모듈 계층 구조 내에서 한 모듈 위로 거슬러 올라가 tests 모듈 안에서 client::connect 함수를 호출할 수 있을까요? 아래와 같이 앞에 콜론 두개를 사용하여 러스트에게 우리가 루트부터 시작하여 전체 경로를 나열하겠다고 알려주는 방법이 있습니다: ::client::connect(); 혹은, 아래와 같이 super를 사용하여 계층 구조 상에서 현재 모듈로부터 한 모듈 거슬러 올라갈 수도 있습니다: super::client::connect(); 이 두 가지 옵션은 이번 예제에서는 차이가 없는 것처럼 보이지만, 여러분의 모듈 계층 구조가 깊어진다면, 매번 루트에서부터 경로를 시작하는 것은 여러분의 코드를 길게 만들 것입니다. 그런 경우에는 super를 이용하여 현재 모듈에서 형제 모듈을 가져오는 것이 좋은 지름길이 됩니다. 여기에 더해서, 만약 여러분이 여러 군데에 루트로부터 시작되는 경로를 명시한 뒤에 서브트리를 다른 곳으로 옮기는 식으로 여러분의 모듈을 재정리한다면, 여러분은 여러 군데의 경로를 갱신하도록 요구받는 처지가 될 것이고, 이는 지루한 작업이 될 것입니다. 각각의 테스트에 super::를 타이핑해야 하는 것이 짜증날수 있겠지만, 여러분은 이미 여기에 대한 해답이 될 도구를 보셨습니다: use 말이죠! super::의 기능은 use에 제공한 경로를 변경시켜서 이제 루트 모듈 대신 부모 모듈에 상대적인 경로가 되게 해줍니다. 이러한 이유로, 특히 tests 모듈 내에서는 보통 use super::something이 가장 좋은 해결책이 됩니다. 따라서 이제 우리의 테스트는 이렇게 됩니다: Filename: src/lib.rs #[cfg(test)]\nmod tests { use super::client; #[test] fn it_works() { client::connect(); }\n} cargo test를 다시 실행시키면, 테스트가 통과되고 테스트 결과 출력의 첫번째 부분이 아래와 같이 나타날 것입니다: $ cargo test Compiling communicator v0.1.0 (file:///projects/communicator) Running target/debug/communicator-92007ddb5330fa5a running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured","breadcrumbs":"모듈 » use로 이름 가져오기 » super를 사용하여 부모 모듈에 접근하기","id":"121","title":"super를 사용하여 부모 모듈에 접근하기"},"122":{"body":"이제 여러분은 코드를 조직화하기 위한 몇가지 새로운 기술을 알게 되었습니다! 관련된 기능들을 함께 묶여주는 이 기술들을 사용하고, 파일들이 너무 길어지지 않게 하고, 여러분의 라이브러리 사용자들에게 깔끔한 공개 API를 제공해 보세요. 다음으로 여러분의 멋지고 깔끔한 코드에 사용할 수 있는 표준 라이브러리 내의 몇가지 컬렉션 데이터 구조를 보겠습니다!","breadcrumbs":"모듈 » use로 이름 가져오기 » 정리","id":"122","title":"정리"},"123":{"body":"러스트의 표준 라이브러리에는 컬렉션 이라 불리는 여러 개의 매우 유용한 데이터 구조들이 포함되어 있습니다. 대부분의 다른 데이터 타입들은 하나의 특정한 값을 나타내지만, 컬렉션은 다수의 값을 담을 수 있습니다. 내장된 배열(build-in array)와 튜플 타입과는 달리, 이 컬렉션들이 가리키고 있는 데이터들은 힙에 저장되는데, 이는 즉 데이터량이 컴파일 타임에 결정되지 않아도 되며 프로그램이 실행될 때 늘어나거나 줄어들 수 있다는 의미입니다. 각각의 컬렉션 종류는 서로 다른 용량과 비용을 가지고 있으며, 여러분의 현재 상황에 따라 적절한 컬렉션을 선택하는 것은 시간이 지남에 따라 발전시켜야 할 기술입니다. 이번 장에서는 러스트 프로그램에서 굉장히 자주 사용되는 세 가지 컬렉션에 대해 논의해 보겠습니다: 벡터(vector) 는 여러 개의 값을 서로 붙어 있게 저장할 수 있도록 해줍니다. 스트링(string) 은 문자(character)의 모음입니다. String 타입은 이전에 다루었지만, 이번 장에서는 더 깊이 있게 이야기해 보겠습니다. 해쉬맵(hash map 은 어떤 값을 특정한 키와 연관지어 주도록 해줍니다. 이는 맵(map) 이라 일컫는 좀더 일반적인 데이터 구조의 특정한 구현 형태입니다. 표준 라이브러리가 제공해주는 다른 종류의 컬렉션에 대해 알고 싶으시면, the documentation 를 봐 주세요. 이제부터 어떻게 벡터, 스트링, 해쉬맵을 만들고 업데이트하는지 뿐만 아니라 어떤 것이 각각의 컬렉션을 특별하게 해주는지에 대해 논의해 보겠습니다.","breadcrumbs":"일반적인 컬렉션 » 일반적인 컬렉션","id":"123","title":"일반적인 컬렉션"},"124":{"body":"우리가 보게될 첫번째 콜렉션은 벡터 라고도 알려진 Vec입니다. 벡터는 메모리 상에 서로 이웃하도록 모든 값을 집어넣는 단일 데이터 구조 안에 하나 이상의 값을 저장하도록 해줍니다. 벡터는 같은 타입의 값만을 저장할 수 있습니다. 이는 여러분이 파일 내의 텍스트의 라인들이라던가 장바구니의 아이템 가격들 같은 아이템 리스트를 저장하는 상황일 경우 유용합니다.","breadcrumbs":"일반적인 컬렉션 » 벡터 » 벡터","id":"124","title":"벡터"},"125":{"body":"비어있는 새 벡터를 만들기 위해서는, 아래의 Listing 8-1과 같이 Vec::new 함수를 호출해 줍니다: let v: Vec = Vec::new(); Listing 8-1: i32 타입의 값을 가질 수 있는 비어있는 새 벡터 생성 여기에 타입 명시(type annotation)를 추가한 것을 주목하세요. 이 벡터에 어떠한 값도 집어넣지 않았기 때문에, 러스트는 우리가 저장하고자 하는 요소의 종류가 어떤 것인지 알지 못합니다. 이는 중요한 지점입니다. 벡터는 제네릭(generic)을 이용하여 구현되었습니다; 제네릭을 이용하여 여러분만의 타입을 만드는 방법은 10장에서 다룰 것입니다. 지금 당장은, 표준 라이브러리가 제공하는 Vec타입은 어떠한 종류의 값이라도 저장할 수 있다는 것, 그리고 특정한 벡터는 특정한 타입의 값을 저장할 때, 이 타입은 꺾쇠 괄호(<>) 안에 적는다는 것만 알아두세요. Listing 8-1에서는 러스트에게 v 안의 Vec가 i32 타입의 요소를 가질 것이고 알려주었습니다. 일단 우리가 값을 집어넣으면 러스트는 우리가 저장하고자 하는 값의 타입을 대부분 유추할 수 있으므로, 좀 더 현실적인 코드에서는 이러한 타입 명시를 할 필요가 거의 없습니다. 초기값들을 갖고 있는 Vec을 생성하는 것이 더 일반적이며, 러스트는 편의를 위해 vec! 매크로를 제공합니다. 이 매크로는 우리가 준 값들을 저장하고 있는 새로운 Vec을 생성합니다. Listing 8-2는 1, 2, 3을 저장하고 있는 새로운 Vec을 생성할 것입니다: let v = vec![1, 2, 3]; Listing 8-2: 값을 저장하고 있는 새로운 벡터 생성하기 초기 i32 값들을 제공했기 때문에, 러스트는 v가 `Vec 타입이라는 것을 유추할 수 있으며, 그래서 타입 명시는 필요치 않습니다. 다음은, 벡터를 어떻게 수정하는지를 살펴보겠습니다.","breadcrumbs":"일반적인 컬렉션 » 벡터 » 새 벡터 만들기","id":"125","title":"새 벡터 만들기"},"126":{"body":"벡터를 만들고 여기에 요소들을 추가하기 위해서는 아래 Listing 8-3과 같이 push 메소드를 사용할 수 있습니다: let mut v = Vec::new(); v.push(5);\nv.push(6);\nv.push(7);\nv.push(8); Listing 8-3: push 메소드를 사용하여 벡터에 값을 추가하기 3장에서 설명한 바와 같이, 어떤 변수에 대해 그 변수가 담고 있는 값이 변경될 수 있도록 하려면, mut 키워드를 사용하여 해당 변수를 가변으로 만들어 줄 필요가 있습니다. 우리가 집어넣는 숫자는 모두 i32 타입이며, 러스트는 데이터로부터 이 타입을 추론하므로, 우리는 Vec 명시를 붙일 필요가 없습니다.","breadcrumbs":"일반적인 컬렉션 » 벡터 » 벡터 갱신하기","id":"126","title":"벡터 갱신하기"},"127":{"body":"struct와 마찬가지로, Listing 8-4에 달려있는 주석처럼 벡터도 스코프 밖으로 벗어났을 때 해제됩니다: { let v = vec![1, 2, 3, 4]; // v를 가지고 뭔가 합니다 } // <- v가 스코프 밖으로 벗어났고, 여기서 해제됩니다 Listing 8-4: 벡터와 벡터의 요소들이 드롭되는 곳을 보여주기 벡터가 드롭될 때 벡터의 내용물 또한 전부 드롭되는데, 이는 벡터가 가지고 있는 정수들이 모두 제거된다는 의미입니다. 이는 직관적인 것처럼 보일 수도 있겠지만 벡터의 요소들에 대한 참조자를 만들때는 좀 더 복잡해 질 수 있습니다. 다음으로 이런 상황을 파해쳐 봅시다!","breadcrumbs":"일반적인 컬렉션 » 벡터 » 벡터를 드롭하는 것은 벡터의 요소들을 드롭시킵니다","id":"127","title":"벡터를 드롭하는 것은 벡터의 요소들을 드롭시킵니다"},"128":{"body":"지금까지 벡터를 만들고, 갱신하고, 없애는 방법에 대해 알아보았으니, 벡터의 내용물을 읽어들이는 방법을 알아보는 것이 다음 단계로 좋아보입니다. 벡터 내에 저장된 값을 참조하는 두 가지 방법이 있습니다. 예제에서는 특별히 더 명료하게 하기 위해 함수들이 반환하는 값의 타입을 명시했습니다. Listing 8-5는 인덱스 문법이나 get 메소드를 가지고 벡터의 값에 접근하는 두 방법 모두를 보여주고 있습니다: let v = vec![1, 2, 3, 4, 5]; let third: &i32 = &v[2];\nlet third: Option<&i32> = v.get(2); Listing 8-5: 인덱스 문법 혹은 get 메소드를 사용하여 벡터 내의 아이템에 접근하기 두가지 세부사항을 주목하세요. 첫번째로, 인덱스값 2를 사용하면 세번째 값이 얻어집니다: 벡터는 0부터 시작하는 숫자로 인덱스됩니다. 두번째로, 세번째 요소를 얻기 위해 두 가지 다른 방법이 사용되었습니다: &와 []를 이용하여 참조자를 얻은 것과, get 함수에 인덱스를 파라미터로 넘겨서 Option<&T>를 얻은 것입니다. 러스트가 벡터 요소를 참조하는 두가지 방법을 제공하는 이유는 여러분이 벡터가 가지고 있지 않은 인덱스값을 사용하고자 했을 때 프로그램이 어떻게 동작할 것인지 여러분이 선택할 수 있도록 하기 위해서입니다. 예를 들어, 아래의 Listing 8-6과 같이 5개의 요소를 가지고 있는 벡터가 있고 100 인덱스에 있는 요소에 접근하려고 시도한 경우 프로그램은 어떻게 동작해야 할까요: let v = vec![1, 2, 3, 4, 5]; let does_not_exist = &v[100];\nlet does_not_exist = v.get(100); Listing 8-6: 5개의 요소를 가진 벡터에 100 인덱스에 있는 요소에 접근하기 이 프로그램을 실행하면, 첫번째의 [] 메소드는 panic!을 일으키는데, 이는 존재하지 않는 요소를 참조하기 때문입니다. 이 방법은 여러분의 프로그램이 벡터의 끝을 넘어서는 요소에 접근하는 시도를 하면 프로그램이 죽게끔 하는 치명적 에러를 발생하도록 하기를 고려하는 경우 가장 좋습니다. get 함수에 벡터 범위를 벗어난 인덱스가 주어졌을 때는 패닉 없이 None이 반환됩니다. 보통의 환경에서 벡터의 범위 밖에 있는 요소에 접근하는 것이 종종 발생한다면 이 방법을 사용할만 합니다. 여러분의 코드는 우리가 6장에서 본 것과 같이 Some(&element) 혹은 None에 대해 다루는 로직을 갖추어야 합니다. 예를 들어 인덱스는 사람이 직접 번호를 입력하는 것으로 들어올 수도 있습니다. 만일 사용자가 잘못하여 너무 큰 숫자를 입력하여 프로그램이 None 값을 받았을 경우, 여러분은 사용자에게 현재 Vec에 몇개의 아이템이 있으며 유효한 값을 입력할 또한번의 기회를 줄 수도 있습니다. 이런 편이 오타 때문에 프로그램이 죽는 것 보다는 더 사용자 친화적이겠죠! 유효하지 않은 참조자 프로그램이 유효한 참조자를 얻을 때, 빌림 검사기(borrow checker)가 (4장에서 다루었던) 소유권 및 빌림 규칙을 집행하여 이 참조자와 벡터의 내용물로부터 얻은 다른 참조자들이 계속 유효하게 남아있도록 확실히 해줍니다. 같은 스코프 내에서 가변 참조자와 불변 참조자를 가질 수 없다는 규칙을 상기하세요. 이 규칙은 아래 예제에서도 적용되는데, Listing 8-7에서는 벡터의 첫번째 요소에 대한 불변 참조자를 얻은 뒤 벡터의 끝에 요소를 추가하고자 했습니다: let mut v = vec![1, 2, 3, 4, 5]; let first = &v[0]; v.push(6); Listing 8-7: 아이템에 대한 참조자를 가지는 동안 벡터에 요소 추가 시도하기 이 예제를 컴파일하면 아래와 같은 에러가 발생합니다: error[E0502]: cannot borrow `v` as mutable because it is also borrowed as\nimmutable |\n4 | let first = &v[0]; | - immutable borrow occurs here\n5 |\n6 | v.push(6); | ^ mutable borrow occurs here\n7 | } | - immutable borrow ends here Listing 8-7의 코드는 동작을 해야만 할것처럼 보일 수도 있습니다: 왜 첫번째 요소에 대한 참조자가 벡터 끝에 대한 변경을 걱정해야 하죠? 이 에러에 대한 내막은 벡터가 동작하는 방법 때문입니다: 새로운 요소를 벡터의 끝에 추가하는 것은 새로 메모리를 할당하여 예전 요소를 새 공간에 복사하는 일을 필요로 할 수 있는데, 이는 벡터가 모든 요소들을 붙여서 저장할 공간이 충분치 않는 환경에서 일어날 수 있습니다. 이러한 경우, 첫번째 요소에 대한 참조자는 할당이 해제된 메모리를 가리키게 될 것입니다. 빌림 규칙은 프로그램이 이러한 상황에 빠지지 않도록 해줍니다. 노트: Vec 타입의 구현 세부사항에 대한 그밖의 것에 대해서는 https://doc.rust-lang.org/stable/nomicon/vec.html 에 있는 노미콘(The Nomicon)을 보세요:","breadcrumbs":"일반적인 컬렉션 » 벡터 » 벡터의 요소들 읽기","id":"128","title":"벡터의 요소들 읽기"},"129":{"body":"만일 벡터 내의 각 요소들을 차례대로 접근하고 싶다면, 하나의 값에 접근하기 위해 인덱스를 사용하는것 보다는, 모든 요소들에 대해 반복처리를 할 수 있습니다. Listing 8-8은 for 루프를 사용하여 i32의 벡터 내에 있는 각 요소들에 대한 불변 참조자를 얻어서 이를 출력하는 방법을 보여줍니다: let v = vec![100, 32, 57];\nfor i in &v { println!(\"{}\", i);\n} Listing 8-8: for 루프를 이용한 요소들에 대한 반복작업을 통해 각 요소들을 출력하기 만일 모든 요소들을 변형시키길 원한다면 가변 벡터 내의 각 요소에 대한 가변 참조자로 반복작업을 할 수도 있습니다. Listing 8-9의 for 루프는 각 요소에 50을 더할 것입니다: let mut v = vec![100, 32, 57];\nfor i in &mut v { *i += 50;\n} Listing 8-9: 벡터 내의 요소에 대한 가변 참조자로 반복하기 가변 참조자가 참고하고 있는 값을 바꾸기 위해서, i에 += 연산자를 이용하기 전에 역참조 연산자 (*)를 사용하여 값을 얻어야 합니다.","breadcrumbs":"일반적인 컬렉션 » 벡터 » 벡터 내의 값들에 대한 반복처리","id":"129","title":"벡터 내의 값들에 대한 반복처리"},"13":{"body":"이 장 및 책 곳곳에서, 우리는 터미널에서 사용되는 몇몇 커맨드를 보여줄 것입니다. 여러분이 터미널에 입력해야 하는 라인들은 모두 $로 시작합니다. 여러분은 $ 문자를 입력할 필요가 없습니다; 이는 각 커맨드의 시작을 나타냅니다. 여러분이 일반 사용자로서 실행할 커맨드를 위해 $를 그리고 여러분이 관리자로서 실행할 커맨드를 위해 #를 쓰는 관례는 많은 튜토리얼들이 사용합니다. $로 시작하지 않는 라인들은 보통 이전 커맨드의 출력을 나타냅니다. 추가적으로, 파워쉘 한정 예제는 $ 대신 >를 이용할 것입니다.","breadcrumbs":"시작하기 » 설치하기 » 커맨드 라인 표기법","id":"13","title":"커맨드 라인 표기법"},"130":{"body":"이 장의 시작 부분에서, 벡터는 같은 타입을 가진 값들만 저장할 수 있다고 이야기했습니다. 이는 불편할 수 있습니다; 다른 타입의 값들에 대한 리스트를 저장할 필요가 있는 상황이 분명히 있지요. 다행히도, 열거형의 variant는 같은 열거형 타입 내에 정의가 되므로, 백터 내에 다른 타입의 값들을 저장할 필요가 있다면 열거형을 정의하여 사용할 수 있습니다! 예를 들어, 스프레드시트의 행으로부터 값들을 가져오고 싶은데, 여기서 어떤 열은 정수를, 어떤 열은 실수를, 어떤 열은 스트링을 갖고 있다고 해봅시다. 우리는 다른 타입의 값을 가지는 variant가 포함된 열거형을 정의할 수 있고, 모든 열거형 variant들은 해당 열거형 타입, 즉 같은 타입으로 취급될 것입니다. 따라서 우리는 궁극적으로 다른 타입을 담은 열거형 값에 대한 벡터를 생성할 수 있습니다. Listing 8-10에서 이를 보여주고 있습니다: enum SpreadsheetCell { Int(i32), Float(f64), Text(String),\n} let row = vec![ SpreadsheetCell::Int(3), SpreadsheetCell::Text(String::from(\"blue\")), SpreadsheetCell::Float(10.12),\n]; Listing 8-10: 열거형을 정의하여 벡터 내에 다른 타입의 데이터를 담을 수 있도록 하기 러스트가 컴파일 타임에 벡터 내에 저장될 타입이 어떤 것인지 알아야할 필요가 있는 이유는 각 요소를 저장하기 위해 얼만큼의 힙 메모리가 필요한지 알기 위함입니다. 부차적인 이점은 이 백터에 허용되는 타입에 대해 명시적일 수 있다는 점입니다. 만일 러스트가 어떠한 타입이든 담을수 있는 벡터를 허용한다면, 벡터 내의 각 요소마다 수행되는 연산에 대해 하나 혹은 그 이상의 타입이 에러를 야기할 수도 있습니다. 열거형과 match 표현식을 사용한다는 것은 6장에서 설명한 바와 같이 러스트가 컴파일 타임에 모든 가능한 경우에 대해 처리한다는 것을 보장해준다는 의미입니다. 만약 프로그램을 작성할 때 여러분의 프로그램이 런타임에 벡터에 저장하게 될 타입의 모든 경우를 알지 못한다면, 열거형을 이용한 방식은 사용할 수 없을 것입니다. 대신 트레잇 객체(trait object)를 이용할 수 있는데, 이건 17장에서 다루게 될 것입니다. 지금까지 벡터를 이용하는 가장 일반적인 방식 중 몇가지에 대해 논의했는데, 표준 라이브러리의 Vec에 정의된 수많은 유용한 메소드들이 있으니 API 문서를 꼭 살펴봐 주시기 바랍니다. 예를 들면, push에 더해서, pop 메소드는 제일 마지막 요소를 반환하고 지워줍니다. 다음 콜렉션 타입인 String으로 넘어갑시다!","breadcrumbs":"일반적인 컬렉션 » 벡터 » 열거형을 사용하여 여러 타입을 저장하기","id":"130","title":"열거형을 사용하여 여러 타입을 저장하기"},"131":{"body":"4장에서 스트링에 관한 이야기를 했습니다만, 지금은 좀 더 깊이 살펴보겠습니다. 새로운 러스트인들은 흔히들 스트링 부분에서 막히는데 이는 세 가지 개념의 조합으로 인한 것입니다: 가능한 에러를 꼭 노출하도록 하는 러스트의 성향, 많은 프로그래머의 예상보다 더 복잡한 데이터 구조인 스트링, 그리고 UTF-8입니다. 다른 언어들을 사용하다 왔을 때 이 개념들의 조합이 러스트의 스트링을 어려운 것처럼 보이게 합니다. 스트링이 컬렉션 장에 있는 이유는 스트링이 바이트의 컬렉션 및 이 바이트들을 텍스트로 통역할때 유용한 기능을 제공하는 몇몇 메소드로 구현되어 있기 때문입니다. 이번 절에서는 생성, 갱신, 값 읽기와 같은 모든 컬렉션 타입이 가지고 있는, String에서의 연산에 대해 이야기 해보겠습니다. 또한 String을 다른 컬렉션들과 다르게 만드는 부분, 즉 사람과 컴퓨터가 String 데이터를 통역하는 방식 간의 차이로 인해 생기는 String 인덱싱의 복잡함을 논의해보겠습니다.","breadcrumbs":"일반적인 컬렉션 » 스트링 » 스트링","id":"131","title":"스트링"},"132":{"body":"먼저 스트링 이라는 용어가 정확히 무엇을 뜻하는 것인지 정의해보겠습니다. 러스트는 핵심 언어 기능 내에서 딱 한가지 스트링 타입만 제공하는데, 이는 바로 스트링 슬라이스인 str이고, 이것의 참조자 형태인 &str을 많이 봤죠. 4장에서는 스트링 슬라이스 에 대해 얘기했고, 이는 다른 어딘가에 저장된 UTF-8로 인코딩된 스트링 데이터의 참조자입니다. 예를 들어, 스트링 리터럴은 프로그램의 바이너리 출력물 내에 저장되어 있으며, 그러므로 스트링 슬라이스입니다. String 타입은 핵심 언어 기능 내에 구현된 것이 아니고 러스트의 표준 라이브러리를 통해 제공되며, 커질 수 있고, 가변적이며, 소유권을 갖고 있고, UTF-8로 인코딩된 스트링 타입입니다. 러스트인들이 “스트링”에 대해 이야기할 때, 그들은 보통 String과 스트링 슬라이스 &str 타입 둘 모두를 이야기한 것이지, 이들 중 하나를 뜻한 것은 아닙니다. 이번 절은 대부분 String에 관한 것이지만, 두 타입 모두 러스트 표준 라이브러리에서 매우 많이 사용되며 String과 스트링 슬라이스 모두 UTF-8로 인코딩되어 있습니다. 또한 러스트 표준 라이브러리는 OsString, OsStr, CString, 그리고 CStr과 같은 몇가지 다른 스트링 타입도 제공합니다. 심지어 어떤 라이브러리 크레이트들은 스트링 데이터를 저장하기 위해 더 많은 옵션을 제공할 수 있습니다. *String/*Str이라는 작명과 유사하게, 이들은 종종 소유권이 있는 타입과 이를 빌린 변형 타입을 제공하는데, 이는 String/&str과 비슷합니다. 이러한 스트링 타입들은, 예를 들면 다른 종류의 인코딩이 된 텍스트를 저장하거나 다른 방식으로 메모리에 저장될 수 있습니다. 여기서는 이러한 다른 스트링 타입은 다루지 않겠습니다; 이것들을 어떻게 쓰고 어떤 경우에 적합한지에 대해 알고 싶다면 각각의 API 문서를 확인하시기 바랍니다.","breadcrumbs":"일반적인 컬렉션 » 스트링 » 스트링이 뭔가요?","id":"132","title":"스트링이 뭔가요?"},"133":{"body":"Vec에서 쓸 수 있는 많은 연산들이 String에서도 마찬가지로 똑같이 쓰일 수 있는데, new 함수를 이용하여 스트링을 생성하는 것으로 아래의 Listing 8-11과 같이 시작해봅시다: let mut s = String::new(); Listing 8-11: 비어있는 새로운 String 생성하기 이 라인은 우리가 어떤 데이터를 담아둘 수 있는 s라는 빈 스트링을 만들어 줍니다. 종종 우리는 스트링에 담아두고 시작할 초기값을 가지고 있을 것입니다. 그런 경우, to_string 메소드를 이용하는데, 이는 Display 트레잇이 구현된 어떤 타입이든 사용 가능하며, 스트링 리터럴도 이 트레잇을 구현하고 있습니다. Listing 8-12에서 두 가지 예제를 보여주고 있습니다: let data = \"initial contents\"; let s = data.to_string(); // the method also works on a literal directly:\nlet s = \"initial contents\".to_string(); Listing 8-12: to_string 메소드를 사용하여 스트링 리터럴로부터 String 생성하기 이 코드는 initial contents를 담고 있는 스트링을 생성합니다. 또한 스트링 리터럴로부터 String을 생성하기 위해서 String::from 함수를 이용할 수도 있습니다. Listing 8-13의 코드는 to_string을 사용하는 Listing 8-12의 코드와 동일합니다: let s = String::from(\"initial contents\"); Listing 8-13: String::from 함수를 사용하여 스트링 리터럴로부터 String 생성하기 스트링이 너무나 많은 것들에 사용되기 때문에, 스트링을 위해 다양한 제네릭 API들을 사용할 수 있으며, 다양한 옵션들을 제공합니다. 몇몇은 쓸모없는 것처럼 느껴질 수도 있지만, 다 사용할 곳이 있습니다! 지금의 경우, String::from과 .to_string은 정확히 똑같은 일을 하며, 따라서 어떤 것을 사용하는가는 여러분의 스타일에 따라 달린 문제입니다. 스트링이 UTF-8로 인코딩되었음을 기억하세요. 즉, 아래의 Listing 8-14에서 보는 것처럼 우리는 인코딩된 어떤 데이터라도 포함시킬 수 있습니다: let hello = String::from(\"السلام عليكم\");\nlet hello = String::from(\"Dobrý den\");\nlet hello = String::from(\"Hello\");\nlet hello = String::from(\"שָׁלוֹם\");\nlet hello = String::from(\"नमस्ते\");\nlet hello = String::from(\"こんにちは\");\nlet hello = String::from(\"안녕하세요\");\nlet hello = String::from(\"你好\");\nlet hello = String::from(\"Olá\");\nlet hello = String::from(\"Здравствуйте\");\nlet hello = String::from(\"Hola\"); Listing 8-14: 스트링에 다양한 언어로 인삿말 저장하기 위의 모두가 유효한 String 값입니다.","breadcrumbs":"일반적인 컬렉션 » 스트링 » 새로운 스트링 생성하기","id":"133","title":"새로운 스트링 생성하기"},"134":{"body":"String은 크기가 커질 수 있으며 이것이 담고 있는 내용물은 Vec의 내용물과 마찬가지로 더 많은 데이터를 집어넣음으로써 변경될 수 있습니다. 추가적으로, + 연산자나 format! 매크로를 사용하여 편리하게 String 값들을 서로 접합(concatenation)할 수 있습니다. push_str과 push를 이용하여 스트링 추가하기 Listing 8-15와 같이 스트링 슬라이스를 추가하기 위해 push_str 메소드를 이용하여 String을 키울 수 있습니다: let mut s = String::from(\"foo\");\ns.push_str(\"bar\"); Listing 8-15: push_str 메소드를 사용하여 String에 스트링 슬라이스 추가하기 s는 위의 두 라인 뒤에 “foobar”를 담게 될 것입니다. push_str 메소드는 스트링 슬라이스를 파라미터로 갖는데 이는 파라미터의 소유권을 가져올 필요가 없기 때문입니다. 예를 들어, Listing 8-16의 코드는 s1에 s2의 내용물을 추가한 뒤 s2를 더 이상 쓸 수 없게 되었다면 불행했을 경우를 보여주고 있습니다: let mut s1 = String::from(\"foo\");\nlet s2 = \"bar\";\ns1.push_str(&s2);\nprintln!(\"s2 is {}\", s2); Listing 8-16: 스트링 슬라이스를 String에 붙인 이후에 스트링 슬라이스를 사용하기 만일 push_str 함수가 s2의 소유권을 가져갔다면, 마지막 줄에서 그 값을 출력할 수 없었을 것입니다. 하지만, 이 코드는 우리가 기대했던 대로 작동합니다! push 메소드는 한 개의 글자를 파라미터로 받아서 String에 추가합니다. Listing 8-17은 push 메소드를 사용하여 String에 l을 추가하는 코드를 보여주고 있습니다: let mut s = String::from(\"lo\");\ns.push('l'); Listing 8-17: push를 사용하여 String 값에 한 글자 추가하기 위의 코드를 실행한 결과로 s는 lol을 담고 있을 것입니다. + 연산자나 format! 매크로를 이용한 접합 종종 우리는 가지고 있는 두 개의 스트링을 조합하고 싶어합니다. 한 가지 방법은 아래 Listing 8-18와 같이 + 연산자를 사용하는 것입니다: let s1 = String::from(\"Hello, \");\nlet s2 = String::from(\"world!\");\nlet s3 = s1 + &s2; // s1은 여기서 이동되어 더이상 쓸 수 없음을 유의하세요 Listing 8-18: + 연산자를 사용하여 두 String 값을 하나의 새로운 String 값으로 조합하기 위의 코드 실행 결과로서, 스트링 s3는 Hello, world!를 담게 될 것입니다. s1이 더하기 연산 이후에 더이상 유효하지 않은 이유와 s2의 참조자가 사용되는 이유는 + 연산자를 사용했을 때 호출되는 함수의 시그니처와 맞춰야 하기 때문입니다 + 연산자는 add 메소드를 사용하는데, 이 메소드의 시그니처는 아래처럼 생겼습니다: fn add(self, s: &str) -> String { 이는 표준 라이브러리에 있는 정확한 시그니처는 아닙니다: 표준 라이브러리 내에서 add는 제네릭을 이용하여 정의되어 있습니다. 여기서는 제네릭에 구체 타입(concrete type)을 대입한 add의 시그니처를 보는 중인데, 이는 우리가 String 값으로 이 메소드를 호출했을때 생깁니다. 제네릭에 대한 내용은 10장에서 다룰 것입니다. 이 시그니처는 교묘한 + 연산자를 이해하는데 필요한 단서를 줍니다. 첫번째로, s2는 &를 가지고 있는데, 이는 add 함수의 s 파라미터 때문에 첫번째 스트링에 두번째 스트링의 참조자 를 더하고 있음을 뜻합니다: 우리는 String에 &str만 더할 수 있고, 두 String을 더하지는 못합니다. 하지만, 잠깐만요 - &s2의 타입은 &String이지, add의 두번째 파라미터에 명시한것처럼 &str은 아니죠. 왜 Listing 8-18의 예제가 컴파일될까요? &s2를 add 호출에 사용할 수 있는 이유는 &String 인자가 &str로 강제 될 수 있기 때문입니다 - add 함수가 호출되면, 러스트는 역참조 강제(deref coercion) 라 불리는 무언가를 사용하는데, 이는 add 함수내에서 사용되는 &s2가 &s2[..]로 바뀌는 것으로 생각할 수 있도록 해줍니다. 역참조 강제에 대한 것은 15장에서 다룰 것입니다. add가 파라미터의 소유권을 가져가지는 않으므로, s2는 이 연산 이후에도 여전히 유효한 String일 것입니다. 두번째로, 시그니처에서 add가 self의 소유권을 가져가는 것을 볼 수 있는데, 이는 self가 &를 안 가지고 있기 때문입니다. 즉 Listing 8-18의 예제에서 s1이 add 호출로 이동되어 이후에는 더 이상 유효하지 않을 것이라는 의미입니다. 따라서 let s3 = s1 + &s2;가 마치 두 스트링을 복사하여 새로운 스트링을 만들 것처럼 보일지라도, 실제로 이 구문은 s1의 소유권을 가져다가 s2의 내용물의 복사본을 추가한 다음, 결과물의 소유권을 반환합니다. 달리 말하면, 이 구문은 여러 복사본을 만드는 것처럼 보여도 그렇지 않습니다: 이러한 구현은 복사보다 더 효율적입니다. 만일 여러 스트링을 접하고자 한다면, +의 동작은 다루기 불편해 집니다.: let s1 = String::from(\"tic\");\nlet s2 = String::from(\"tac\");\nlet s3 = String::from(\"toe\"); let s = s1 + \"-\" + &s2 + \"-\" + &s3; 이 지점에서 s는 tic-tac-toe가 될 것입니다. 모든 +와 \" 문자들과 함께 보면 어떤 결과가 나올지 알기 어렵습니다. 더 복잡한 스트링 조합을 위해서는 format! 매크로를 사용할 수 있습니다: let s1 = String::from(\"tic\");\nlet s2 = String::from(\"tac\");\nlet s3 = String::from(\"toe\"); let s = format!(\"{}-{}-{}\", s1, s2, s3); 이 코드 또한 s에 tic-tac-toe을 설정합니다. format! 매크로는 println!과 똑같은 방식으로 작동하지만, 스크린에 결과를 출력하는 대신 결과를 담은 String을 반환해줍니다. format!을 이용한 버전이 훨씬 읽기 쉽고, 또한 어떠한 파라미터들의 소유권도 가져가지 않습니다.","breadcrumbs":"일반적인 컬렉션 » 스트링 » 스트링 갱신하기","id":"134","title":"스트링 갱신하기"},"135":{"body":"다른 많은 프로그래밍 언어들에서, 인덱스를 이용한 참조를 통해 스트링 내부의 개별 문자들에 접근하는 것은 유효하고 범용적인 연산에 속합니다. 그러나 러스트에서 인덱싱 문법을 이용하여 String의 부분에 접근하고자 하면 에러를 얻게 됩니다. 아래 Listing 8-19와 같은 코드를 생각해봅시다: let s1 = String::from(\"hello\");\nlet h = s1[0]; Listing 8-19: 스트링에 인덱싱 문법을 사용하는 시도 이 코드는 아래와 같은 에러를 출력합니다: error: the trait bound `std::string::String: std::ops::Index<_>` is not\nsatisfied [--explain E0277] |> |> let h = s1[0]; |> ^^^^^\nnote: the type `std::string::String` cannot be indexed by `_` 에러와 노트 부분이 이야기해 줍니다: 러스트 스트링은 인덱싱을 지원하지 않는다고. 그렇지만 왜 안되는 걸까요? 이 질문에 답하기 위해서는 러스트가 어떻게 스트링을 메모리에 저장하는지에 관하여 살짝 이야기해야 합니다. 내부적 표현 String은 Vec을 감싼 것입니다(wrapper). Listing 8-14에서 보았던 몇가지 적절히 인코딩된 UTF-8 예제 스트링을 살펴봅시다. 첫번째로, 이것입니다: let len = String::from(\"Hola\").len(); 이 경우, len은 4가 되는데, 이는 스트링 “Hola”를 저장하고 있는 Vec이 4바이트 길이라는 뜻입니다. UTF-8로 인코딩되면 각각의 글자들이 1바이트씩 차지한다는 것이죠. 그런데 아래 예제는 어떨까요? let len = String::from(\"Здравствуйте\").len(); 이 스트링의 길이가 얼마인지 묻는다면, 여러분은 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를 반환합니다. 기대치 않은 값을 반환하고 즉시 발견하기 힘들지도 모를 버그를 야기하는 것을 방지하기 위해, 러스트는 이러한 코드를 전혀 컴파일하지 않고 이러한 오해들을 개발 과정 내에서 일찌감치 방지합니다. 바이트와 스칼라 값과 문자소 클러스터(Grapheme cluster)! 이런! UTF-8에 대한 또다른 지점은, 실제로는 러스트의 관점에서 문자열을 보는 세 가지의 의미있는 방식이 있다는 것입니다: 바이트, 스칼라 값, 그리고 문자소 클러스터(우리가 글자 라고 부르는 것과 가장 근접한 것)입니다. 데바가나리 글자로 쓰여진 힌디어 “नमस्ते”를 보면, 이것은 궁극적으로 아래와 같이 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을 인덱스로 접근하여 문자를 얻지 못하도록 하는 마지막 이유는 인덱스 연산이 언제나 상수 시간(O(1))에 실행될 것으로 기대받기 때문입니다. 그러나 String을 가지고 그러한 성능을 보장하는 것은 불가능한데, 그 이유는 러스트가 스트링 내에 얼마나 많은 유효 문자가 있는지 알아내기 위해 내용물의 시작 지점부터 인덱스로 지정된 곳까지 훑어야 하기 때문입니다.","breadcrumbs":"일반적인 컬렉션 » 스트링 » 스트링 내부의 인덱싱","id":"135","title":"스트링 내부의 인덱싱"},"136":{"body":"스트링 인덱싱의 리턴 타입이 어떤 것이 (바이트 값인지, 캐릭터인지, 문자소 클러스터인지, 혹은 스트링 슬라이스인지) 되어야 하는지 명확하지 않기 때문에 스트링의 인덱싱은 종종 나쁜 아이디어가 됩니다. 따라서, 여러분이 스트링 슬라이스를 만들기 위해 정말로 인덱스를 사용하고자 한다면 러스트는 좀 더 구체적으로 지정하도록 요청합니다. 여러분의 인덱싱을 더 구체적으로 하고 스트링 슬라이스를 원한다는 것을 가리키기 위해서, []에 숫자 하나를 사용하는 인덱싱보다, []와 범위를 사용하여 특정 바이트들이 담고 있는 스트링 슬라이스를 만들 수 있습니다: let hello = \"Здравствуйте\"; let s = &hello[0..4]; 여기서 s는 스트링의 첫 4바이트를 담고 있는 &str가 될 것입니다. 앞서 우리는 이 글자들이 각각 2바이트를 차지한다고 언급했으므로, 이는 s가 “Зд”이 될 것이란 뜻입니다. 만약에 &hello[0..1]라고 했다면 어떻게 될까요? 답은 다음과 같습니다: 러스트는 벡터 내에 유효하지 않은 인덱스에 접근했을 때와 동일한 방식으로 런타임에 패닉을 발생시킬 것입니다. thread 'main' panicked at 'index 0 and/or 1 in `Здравствуйте` do not lie on\ncharacter boundary', ../src/libcore/str/mod.rs:1694 여러분은 스트링 슬라이스를 만들기 위하여 범위를 이용하는 방법을 조심스럽게 사용해야 하는데, 이는 여러분의 프로그램을 죽게 만들 수도 있기 때문입니다.","breadcrumbs":"일반적인 컬렉션 » 스트링 » 스트링 슬라이싱하기","id":"136","title":"스트링 슬라이싱하기"},"137":{"body":"다행히도, 스트링의 요소에 접근하는 다른 방법이 있습니다. 만일 개별적인 유니코드 스칼라 값에 대한 연산을 수행하길 원한다면, 가장 좋은 방법은 chars 메소드를 이용하는 것입니다. chars를 “नमस्ते”에 대해 호출하면 char타입의 6개의 값으로 나누어 반환하며, 여러분은 각각의 요소에 접근하기 위해 이 결과값에 대해 반복(iterate)할 수 있습니다: for c in \"नमस्ते\".chars() { println!(\"{}\", c);\n} 이 코드는 다음을 출력할 것입니다: न\nम\nस\n्\nत\nे bytes 메소드는 가공되지 않은 각각의 바이트를 반환하는데, 여러분의 문제 범위에 따라 적절할 수도 있습니다: for b in \"नमस्ते\".bytes() { println!(\"{}\", b);\n} 이 코드는 이 String을 구성하는 아래처럼 시작되는 18 바이트를 출력합니다: 224\n164\n168\n224\n// ... etc 하지만 유효한 유니코드 스칼라 값이 하나 이상의 바이트로 구성될지도 모른다는 것을 확실히 기억해주세요. 스트링으로부터 문자소 클러스터를 얻는 방법은 복잡해서, 이 기능은 표준 라이브러리를 통해 제공되지 않습니다. 여러분이 원하는 기능이 이것이라면 crates.io 에서 사용 가능한 크레이트가 있습니다.","breadcrumbs":"일반적인 컬렉션 » 스트링 » 스트링 내에서 반복적으로 실행되는 메소드","id":"137","title":"스트링 내에서 반복적으로 실행되는 메소드"},"138":{"body":"종합하면, 스트링은 복잡합니다. 다른 프로그래밍 언어들은 이러한 복잡성을 프로그래머에게 어떻게 보여줄지에 대해 각기 다른 선택을 합니다. 러스트는 String 데이터의 올바른 처리가 모든 러스트 프로그램에 대한 기본적인 동작이 되도록 선택했는데, 이는 솔직히 프로그래머들이 UTF-8 데이터를 처리하는데 있어 더 많은 생각을 해야한다는 의미입니다. 이러한 거래는 다른 프로그래밍 언어들에 비해 더 복잡한 스트링을 노출시키지만, 한편으로는 여러분의 개발 생활 주기 후반에 비 ASCII 캐릭터를 포함하는 에러를 처리해야 하는 것을 막아줄 것입니다. 이것보다 살짝 덜 복잡한 것으로 옮겨 갑시다: 해쉬맵이요!","breadcrumbs":"일반적인 컬렉션 » 스트링 » 스트링은 그렇게 단순하지 않습니다","id":"138","title":"스트링은 그렇게 단순하지 않습니다"},"139":{"body":"마지막으로 볼 일반적인 컬렉션은 해쉬맵 입니다. HashMap 타입은 K 타입의 키에 V 타입의 값을 매핑한 것을 저장합니다. 이 매핑은 해쉬 함수(hashing function) 을 통해 동작하는데, 해쉬 함수는 이 키와 값을 메모리 어디에 저장할지 결정합니다. 많은 다른 프로그래밍 언어들도 이러한 종류의 데이터 구조를 지원하지만, 종종 해쉬, 맵, 오브젝트, 해쉬 테이블, 혹은 연관 배열 (associative) 등과 같은 그저 몇몇 다른 이름으로 이용됩니다. 해쉬맵은 여러분이 벡터를 이용하듯 인덱스를 이용하는 것이 아니라 임의의 타입으로 된 키를 이용하여 데이터를 찾기를 원할때 유용합니다. 예를 들면, 게임 상에서는 각 팀의 점수를 해쉬맵에 유지할 수 있는데, 여기서 키는 팀의 이름이고 값은 팀의 점수가 될 수 있습니다. 팀의 이름을 주면, 여러분은 그 팀의 점수를 찾을 수 있습니다. 이 장에서는 해쉬맵의 기본 API를 다룰 것이지만, 표준 라이브러리의 HashMap에 정의되어 있는 함수 중에는 더 좋은 것들이 숨어있습니다. 항상 말했듯이, 더 많은 정보를 원하신다면 표준 라이브러리 문서를 확인하세요.","breadcrumbs":"일반적인 컬렉션 » 해쉬맵 » 해쉬맵(hash map)","id":"139","title":"해쉬맵(hash map)"},"14":{"body":"만일 여러분들이 Linux 혹은 macOS를 사용중이라면, 터미널을 열고 다음 커멘드를 입력하세요: $ curl https://sh.rustup.rs -sSf | sh 이 커맨드는 스크립트를 다운로드하고 rustup 도구의 설치를 시작하는데, 이 도구는 가장 최신의 러스트 안정화 버전을 설치해줍니다. 여러분의 패스워드를 입력하라는 프롬프트가 나올 수도 있습니다. 설치가 성공적이면, 다음과 같은 라인이 나타날 것입니다: Rust is installed now. Great! 물론 여러분이 어떤 소프트웨어를 설치하기 위해 curl URL | sh를 사용하는 것을 신용하지 않는다면, 여러분이 원하는 어떤 방식으로든 이 스크립트를 다운로드하고, 검사하고, 실행할 수 있습니다. 설치 스크립트는 여러분의 다음 로그인 이후에 러스트를 자동적으로 여러분의 시스템 패스에 추가합니다. 만일 여러분이 터미널을 재시작하지 않고 러스트를 바로 사용하기를 원한다면, 다음과 같은 커멘트를 쉘에서 실행하여 수동적으로 러스트를 시스템 패스에 추가하세요: $ source $HOME/.cargo/env 혹은 그 대신에, 여러분의 ~/.bash_profile 에 다음과 같은 라인을 추가할 수 있습니다: $ export PATH=\"$HOME/.cargo/bin:$PATH\" 추가적으로, 여러분은 어떤 종류의 링커가 필요할 것입니다. 이미 설치되어 있을 것 같지만, 여러분이 러스트 프로그램을 컴파일하다가 링커를 실행할 수 없음을 나타내는 에러를 보게 되면, 링커를 설치해야 합니다. 여러분은 C 컴파일러를 설치할 수 있는데, 이것이 보통 올바른 링커와 함께 설치되기 때문입니다. C 컴파일러를 인스톨하는 방법을 위해서는 여러분의 플랫폼 문서를 확인하세요. 몇몇의 일반적인 러스트 패키지는 C 코드에 의존적이고 C 컴파일러 또한 사용할 것이므로, 지금 상황에 상관없이 하나 설치하는것이 좋을 수도 있습니다.","breadcrumbs":"시작하기 » 설치하기 » Linux와 macOS에서 Rustup 설치하기","id":"14","title":"Linux와 macOS에서 Rustup 설치하기"},"140":{"body":"우리는 빈 해쉬맵을 new로 생성할 수 있고, insert를 이용하여 요소를 추가할 수 있습니다. Listing 8-20에서, 우리는 팀 이름이 각각 블루(Blue)와 옐로우(Yellow)인 두 팀의 점수를 유지하고 있습니다. 블루 팀은 10점, 옐로우 팀은 50점으로 시작할 것입니다: use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10);\nscores.insert(String::from(\"Yellow\"), 50); Listing 8-20: 새로운 해쉬맵을 생성하여 몇 개의 키와 값을 집어넣기 먼저 표준 라이브러리의 컬렉션 부분으로부터 HashMap을 use로 가져와야할 필요가 있음을 주목하세요. 우리가 보고 있는 세 가지 일반적인 컬렉션 중에서 이 해쉬맵이 제일 덜 자주 사용되는 것이기 때문에, 프렐루드(prelude) 내에 자동으로 가져와지는 기능에 포함되어 있지 않습니다. 또한 해쉬맵은 표준 라이브러리로부터 덜 지원을 받습니다; 예를 들면 해쉬맵을 생성하는 빌트인 매크로가 없습니다. 벡터와 마찬가지로, 해쉬맵도 데이터를 힙에 저장합니다. 이 HashMap은 String 타입의 키와 i32 타입의 값을 갖습니다. 벡터와 비슷하게 해쉬맵도 동질적입니다: 모든 키는 같은 타입이어야 하고, 모든 값도 같은 타입이여야 합니다. 해쉬맵을 생성하는 또다른 방법은 튜플의 벡터에 대해 collect 메소드를 사용하는 것인데, 이 벡터의 각 튜플은 키와 키에 대한 값으로 구성되어 있습니다. collect 메소드는 데이터를 모아서 HashMap을 포함한 여러 컬렉션 타입으로 만들어줍니다. 예를 들면, 만약 두 개의 분리된 벡터에 각각 팀 이름과 초기 점수를 갖고 있다면, 우리는 zip 메소드를 이용하여 “Blue”와 10이 한 쌍이 되는 식으로 튜플의 벡터를 생성할 수 있습니다. 그 다음 Listing 8-21과 같이 collect 메소드를 사용하여 튜플의 벡터를 HashMap으로 바꿀 수 있습니다: use std::collections::HashMap; let teams = vec![String::from(\"Blue\"), String::from(\"Yellow\")];\nlet initial_scores = vec![10, 50]; let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect(); Listing 8-21: 팀의 리스트와 점수의 리스트로부터 해쉬맵 생성하기 타입 명시 HashMap<_, _>이 필요한데 이는 collect가 다른 많은 데이터 구조로 바뀔 수 있고, 러스트는 여러분이 특정하지 않으면 어떤 것을 원하는지 모르기 때문입니다. 그러나 키와 값의 타입에 대한 타입 파라미터에 대해서는 밑줄을 쓸 수 있으며 러스트는 벡터에 담긴 데이터의 타입에 기초하여 해쉬에 담길 타입을 추론할 수 있습니다.","breadcrumbs":"일반적인 컬렉션 » 해쉬맵 » 새로운 해쉬맵 생성하기","id":"140","title":"새로운 해쉬맵 생성하기"},"141":{"body":"i32와 같이 Copy 트레잇을 구현한 타입에 대하여, 그 값들은 해쉬맵 안으로 복사됩니다. String과 같이 소유된 값들에 대해서는, 아래의 Listing 8-22와 같이 값들이 이동되어 해쉬맵이 그 값들에 대한 소유자가 될 것입니다: use std::collections::HashMap; let field_name = String::from(\"Favorite color\");\nlet field_value = String::from(\"Blue\"); let mut map = HashMap::new();\nmap.insert(field_name, field_value);\n// field_name과 field_value은 이 지점부터 유효하지 않습니다.\n// 이들을 이용하는 시도를 해보고 어떤 컴파일러 에러가 나오는지 보세요! Listing 8-22: 키와 값이 삽입되는 순간 이들이 해쉬맵의 소유가 되는 것을 보여주는 예 insert를 호출하여 field_name과 field_value를 해쉬맵으로 이동시킨 후에는 더 이상 이 둘을 사용할 수 없습니다. 만일 우리가 해쉬맵에 값들의 참조자들을 삽입한다면, 이 값들은 해쉬맵으로 이동되지 않을 것입니다. 하지만 참조자가 가리키고 있는 값은 해쉬맵이 유효할 때까지 계속 유효해야합니다. 이것과 관련하여 10장의 “라이프타임을 이용한 참조자 유효화”절에서 더 자세히 이야기할 것입니다.","breadcrumbs":"일반적인 컬렉션 » 해쉬맵 » 해쉬맵과 소유권","id":"141","title":"해쉬맵과 소유권"},"142":{"body":"Listing 8-23과 같이 해쉬맵의 get 메소드에 키를 제공하여 해쉬맵으로부터 값을 얻어올 수 있습니다: use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10);\nscores.insert(String::from(\"Yellow\"), 50); let team_name = String::from(\"Blue\");\nlet score = scores.get(&team_name); Listing 8-23: 해쉬맵 내에 저장된 블루 팀의 점수 접근하기 여기서 score는 블루 팀과 연관된 값을 가지고 있을 것이고, 결과값은 Some(&10)일 것입니다. 결과값은 Some으로 감싸져 있는데 왜냐하면 get이 Option<&V>를 반환하기 때문입니다; 만일 해쉬맵 내에 해당 키에 대한 값이 없다면 get은 None을 반환합니다. 프로그램은 우리가 6장에서 다루었던 방법 중 하나로 Option을 처리해야 할 것입니다. 우리는 벡터에서 했던 방법과 유사한 식으로 for 루프를 이용하여 해쉬맵에서도 각각의 키/값 쌍에 대한 반복작업을 할 수 있습니다: use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10);\nscores.insert(String::from(\"Yellow\"), 50); for (key, value) in &scores { println!(\"{}: {}\", key, value);\n} 이 코드는 각각의 쌍을 임의의 순서로 출력할 것입니다: Yellow: 50\nBlue: 10","breadcrumbs":"일반적인 컬렉션 » 해쉬맵 » 해쉬맵 내의 값 접근하기","id":"142","title":"해쉬맵 내의 값 접근하기"},"143":{"body":"키와 값의 개수가 증가할 수 있을지라도, 각각의 개별적인 키는 한번에 연관된 값 하나만을 가질 수 있습니다. 해쉬맵 내의 데이터를 변경하길 원한다면, 키에 이미 값이 할당되어 있을 경우에 대한 처리를 어떻게 할지 결정해야 합니다. 예전 값을 완전히 무시하면서 예전 값을 새 값으로 대신할 수도 있습니다. 혹은 예전 값을 계속 유지하면서 새 값은 무시하고, 해당 키에 값이 할당되지 않을 경우에만 새 값을 추가하는 방법을 선택할 수도 있습니다. 또는 예전 값과 새 값을 조합할 수도 있습니다. 각각의 경우를 어떻게 할지 살펴봅시다! 값을 덮어쓰기 만일 해쉬맵에 키와 값을 삽입하고, 그 후 똑같은 키에 다른 값을 삽입하면, 키에 연관지어진 값은 새 값으로 대신될 것입니다. 아래 Listing 8-24의 코드가 insert를 두 번 호출함에도, 해쉬맵은 딱 하나의 키/값 쌍을 담게 될 것인데 그 이유는 두 번 모두 블루 팀의 키에 대한 값을 삽입하고 있기 때문입니다: use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(String::from(\"Blue\"), 10);\nscores.insert(String::from(\"Blue\"), 25); println!(\"{:?}\", scores); Listing 8-24: 특정한 키로 저장된 값을 덮어쓰기 이 코드는 {\"Blue\": 25}를 출력할 것입니다. 원래의 값 10은 덮어써졌습니다. 키에 할당된 값이 없을 경우에만 삽입하기 특정 키가 값을 가지고 있는지 검사하고, 만일 가지고 있지 않다면 이 키에 대한 값을 삽입하고자 하는 경우는 흔히 발생합니다. 해쉬맵은 이를 위하여 entry라고 하는 특별한 API를 가지고 있는데, 이는 우리가 검사하고자 하는 키를 파라미터로 받습니다. entry 함수의 리턴값은 열거형 Entry인데, 해당 키가 있는지 혹은 없는지를 나타냅니다. 우리가 옐로우 팀에 대한 키가 연관된 값을 가지고 있는지 검사하고 싶어한다고 해봅시다. 만일 없다면, 값 50을 삽입하고, 블루팀에 대해서도 똑같이 하고 싶습니다. 엔트리 API를 사용한 코드는 아래의 Listing 8-25와 같습니다: use std::collections::HashMap; let mut scores = HashMap::new();\nscores.insert(String::from(\"Blue\"), 10); scores.entry(String::from(\"Yellow\")).or_insert(50);\nscores.entry(String::from(\"Blue\")).or_insert(50); println!(\"{:?}\", scores); Listing 8-25: entry 메소드를 이용하여 어떤 키가 값을 이미 갖고 있지 않을 경우에만 추가하기 Entry에 대한 or_insert 메소드는 해당 키가 존재할 경우 관련된 Entry 키에 대한 값을 반환하도록 정의되어 있고, 그렇지 않을 경우에는 파라미터로 주어진 값을 해당 키에 대한 새 값을 삽입하고 수정된 Entry에 대한 값을 반환합니다. 이 방법은 우리가 직접 로직을 작성하는 것보다 훨씬 깔끔하고, 게다가 빌림 검사기와 잘 어울려 동작합니다. Listing 8-25의 코드를 실행하면 {\"Yellow\": 50, \"Blue\": 10}를 출력할 것입니다. 첫번째 entry 호출은 옐로우 팀에 대한 키에 대하여 값 50을 삽입하는데, 이는 옐로우 팀이 값을 가지고 있지 않기 때문입니다. 두번째 entry 호출은 해쉬맵을 변경하지 않는데, 왜냐하면 블루 팀은 이미 값 10을 가지고 있기 때문입니다. 예전 값을 기초로 값을 갱신하기 해쉬맵에 대한 또다른 흔한 사용 방식은 키에 대한 값을 찾아서 예전 값에 기초하여 값을 갱신하는 것입니다. 예를 들어, Listing 8-26은 어떤 텍스트 내에 각 단어가 몇번이나 나왔는지를 세는 코드를 보여줍니다. 단어를 키로 사용하는 해쉬맵을 이용하여 해당 단어가 몇번이나 나왔는지를 유지하기 위해 값을 증가시켜 줍니다. 만일 어떤 단어를 처음 본 것이라면, 값 0을 삽입할 것입니다. 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;\n} println!(\"{:?}\", map); Listing 8-26: 단어와 횟수를 저장하는 해쉬맵을 사용하여 단어의 등장 횟수 세기 이 코드는 {\"world\": 2, \"hello\": 1, \"wonderful\": 1}를 출력할 것입니다. or_insert 메소드는 실제로는 해당 키에 대한 값의 가변 참조자 (&mut V)를 반환합니다. 여기서는 count 변수에 가변 참조자를 저장하였고, 여기에 값을 할당하기 위해 먼저 애스터리스크 (*)를 사용하여 count를 역참조해야 합니다. 가변 참조자는 for 루프의 끝에서 스코프 밖으로 벗어나고, 따라서 모든 값들의 변경은 안전하며 빌림 규칙에 위배되지 않습니다.","breadcrumbs":"일반적인 컬렉션 » 해쉬맵 » 해쉬맵 갱신하기","id":"143","title":"해쉬맵 갱신하기"},"144":{"body":"기본적으로, HashMap은 서비스 거부 공격(Denial of Service(DoS) attack)에 저항 기능을 제공할 수 있는 암호학적으로 보안되는 해쉬 함수를 사용합니다. 이는 사용 가능한 가장 빠른 해쉬 알고리즘은 아니지만, 성능을 떨어트리면서 더 나은 보안을 취하는 거래는 가치가 있습니다. 만일 여러분이 여러분의 코드를 프로파일하여 기본 해쉬 함수가 여러분의 목표에 관해서는 너무 느리다면, 다른 해쉬어(hasher) 를 특정하여 다른 함수로 바꿀 수 있습니다. 해쉬어는 BuildHasher 트레잇을 구현한 타입을 말합니다. 트레잇과 이를 어떻게 구현하는지에 대해서는 10장에서 다룰 것입니다. 여러분의 해쉬어를 바닥부터 새로 구현해야할 필요는 없습니다; crates.io 에서는 많은 수의 범용적인 해쉬 알고리즘을 구현한 해쉬어를 제공하는 공유 라이브러리를 제공합니다.","breadcrumbs":"일반적인 컬렉션 » 해쉬맵 » 해쉬 함수","id":"144","title":"해쉬 함수"},"145":{"body":"벡터, 스트링, 그리고 해쉬맵은 프로그램 내에서 여러분이 데이터를 저장하고, 접근하고, 수정하고 싶어하는 곳마다 필요한 수많은 기능들을 제공해줄 것입니다. 이제 여러분이 풀 준비가 되어있어야 할만한 몇가지 연습문제를 소개합니다: 정수 리스트가 주어졌을 때, 벡터를 이용하여 이 리스트의 평균값(mean, average), 중간값(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":"145","title":"정리"},"146":{"body":"러스트의 신뢰성에 대한 약속은 에러 처리에도 확장되어 있습니다. 에러는 소프트웨어에서 피할 수 없는 현실이며, 따라서 러스트는 무언가 잘못되었을 경우에 대한 처리를 위한 몇 가지 기능을 갖추고 있습니다. 많은 경우, 러스트는 여러분이 에러가 발생할 가능성을 인정하고 여러분의 코드가 컴파일되기 전에 어떤 행동을 취하기를 요구할 것입니다. 이러한 요구사항은 여러분의 코드를 제품으로서 배포하기 전에 에러를 발견하고 적절히 조치할 것이라고 보장함으로써 여러분의 프로그램을 더 강건하게 해줍니다! 러스트는 에러를 두 가지 범주로 묶습니다: 복구 가능한(recoverable) 에러와 복구 불가능한(unrecoverable) 에러입니다. 복구 가능한 에러는 사용자에게 문제를 보고하고 연산을 재시도하는 것이 보통 합리적인 경우인데, 이를테면 파일을 찾지 못하는 에러가 그렇습니다. 복구 불가능한 에러는 언제나 버그의 증상이 나타나는데, 예를 들면 배열의 끝을 넘어선 위치의 값에 접근하려고 시도하는 경우가 그렇습니다. 대부분의 언어들은 이 두 종류의 에러를 분간하지 않으며 예외 처리(exception)와 같은 메카니즘을 이용하여 같은 방식으로 둘 다 처리합니다. 러스트는 예외 처리 기능이 없습니다. 대신, 복구 가능한 에러를 위한 Result 값과 복구 불가능한 에러가 발생했을 때 실행을 멈추는 panic! 매크로를 가지고 있습니다. 이번 장에서는 panic!을 호출하는 것을 먼저 다룬 뒤, Result 값을 반환하는 것에 대해 이야기 하겠습니다. 추가로, 에러로부터 복구을 시도할지 아니면 실행을 멈출지를 결정할 때 고려할 것에 대해 탐구해 보겠습니다.","breadcrumbs":"에러 처리 » 에러 처리","id":"146","title":"에러 처리"},"147":{"body":"가끔씩 여러분의 코드에서 나쁜 일이 일어나고, 이에 대해 여러분이 할 수 있는 것이 없을 수도 있습니다. 이러한 경우를 위하여 러스트는 panic! 매크로를 가지고 있습니다. 이 매크로가 실행되면, 여러분의 프로그램은 실패 메세지를 출력하고, 스택을 되감고 청소하고, 그 후 종료됩니다. 이런 일이 발생하는 가장 흔한 상황은 어떤 종류의 버그가 발견되었고 프로그래머가 이 에러를 어떻게 처리할지가 명확하지 않을 때 입니다.","breadcrumbs":"에러 처리 » panic!과 함께하는 복구 불가능한 에러 » panic!과 함께하는 복구 불가능한 에러","id":"147","title":"panic!과 함께하는 복구 불가능한 에러"},"148":{"body":"기본적으로, panic!이 발생하면, 프로그램은 되감기(unwinding) 를 시작하는데, 이는 러스트가 패닉을 마주친 각 함수로부터 스택을 거꾸로 훑어가면서 데이터를 제거한다는 뜻이지만, 이 훑어가기 및 제거는 일이 많습니다. 다른 대안으로는 즉시 그만두기(abort) 가 있는데, 이는 데이터 제거 없이 프로그램을 끝내는 것입니다. 프로그램이 사용하고 있던 메모리는 운영체제에 의해 청소될 필요가 있을 것입니다. 여러분의 프로젝트 내에서 결과 바이너리가 가능한 작아지기를 원한다면, 여러분의 Cargo.toml 내에서 적합한 [profile] 섹션에 panic = 'abort'를 추가함으로써 되감기를 그만두기로 바꿀 수 있습니다. 예를 들면, 여러분이 릴리즈 모드 내에서는 패닉 상에서 그만두기를 쓰고 싶다면, 다음을 추가하세요: [profile.release]\npanic = 'abort' 단순한 프로그램 내에서 panic! 호출을 시도해 봅시다: Filename: 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.25 secs Running `target/debug/panic`\nthread 'main' panicked at 'crash and burn', src/main.rs:2\nnote: Run with `RUST_BACKTRACE=1` for a backtrace.\nerror: Process didn't exit successfully: `target/debug/panic` (exit code: 101) panic!의 호출이 마지막 세 줄의 에러 메세지를 야기합니다. 첫 번째 줄은 우리의 패닉 메세지와 소스 코드에서 패닉이 발생한 지점을 보여줍니다: src/main.rs:2 는 src/main.rs 파일의 두 번째 줄을 가리킵니다. 위 예제의 경우, 가리키고 있는 줄은 우리 코드 부분이고, 해당 줄로 가면 panic! 매크로 호출을 보게 됩니다. 그 외의 경우들에서는, panic! 호출이 우리가 호출한 코드 내에 있을 수도 있습니다. 에러 메세지에 의해 보고되는 파일 이름과 라인 번호는 panic! 매크로가 호출된 다른 누군가의 코드일 것이며, 궁극적으로 panic!을 이끌어낸 것이 우리 코드 라인이 아닐 것입니다. 문제를 일으킨 코드 부분을 발견하기 위해서 panic! 호출이 발생된 함수에 대한 백트레이스(backtrace)를 사용할 수 있습니다. 백트레이스가 무엇인가에 대해서는 뒤에 더 자세히 다를 것입니다.","breadcrumbs":"에러 처리 » panic!과 함께하는 복구 불가능한 에러 » panic!에 응하여 스택을 되감거나 그만두기","id":"148","title":"panic!에 응하여 스택을 되감거나 그만두기"},"149":{"body":"다른 예를 통해서, 우리 코드가 직접 매크로를 호출하는 대신 우리 코드의 버그 때문에 panic! 호출이 라이브러리로부터 발생될 때는 어떻게 되는지 살펴봅시다. Listing 9-1은 벡터 내의 요소를 인덱스로 접근 시도하는 코드입니다: Filename: src/main.rs fn main() { let v = vec![1, 2, 3]; v[99];\n} Listing 9-1: panic!을 일으키는 벡터의 끝을 넘어선 요소에 대한 접근 시도 여기서 우리는 벡터의 100번째 요소(0부터 시작하여 100번째)에 접근하기를 시도하고 있지만, 벡터는 오직 3개의 요소만 가지고 있습니다. 이러한 상황이면 러스트는 패닉을 일으킬 것입니다. []를 사용하는 것은 어떤 요소를 반환하기를 가정하지만, 유효하지 않은 인덱스를 넘기게 되면 러스트가 반환할 올바른 요소는 없습니다. 이러한 상황에서 C와 같은 다른 언어들은 여러분이 원하는 것이 아닐지라도, 여러분이 요청한 것을 정확히 주려고 시도할 것입니다: 여러분은 벡터 내에 해당 요소와 상응하는 위치의 메모리에 들어 있는 무언가를 얻을 것입니다. 설령 그 메모리 영역이 벡터 소유가 아닐지라도 말이죠. 이러한 것을 버퍼 오버리드(buffer overread) 라고 부르며, 만일 어떤 공격자가 읽도록 허용되어선 안 되지만 배열 뒤에 저장된 데이터를 읽어낼 방법으로서 인덱스를 다룰 수 있게 된다면, 이는 보안 취약점을 발생시킬 수 있습니다. 여러분의 프로그램을 이러한 종류의 취약점으로부터 보호하기 위해서, 여러분이 존재하지 않는 인덱스 상의 요소를 읽으려 시도한다면, 러스트는 실행을 멈추고 계속하기를 거부할 것입니다. 한번 시도해 봅시다: $ cargo run Compiling panic v0.1.0 (file:///projects/panic) Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs Running `target/debug/panic`\nthread 'main' panicked at 'index out of bounds: the len is 3 but the index is\n100', /stable-dist-rustc/build/src/libcollections/vec.rs:1362\nnote: Run with `RUST_BACKTRACE=1` for a backtrace.\nerror: Process didn't exit successfully: `target/debug/panic` (exit code: 101) 위 에러는 우리가 작성하지 않은 파일인 libcollections/vec.rs 를 가리키고 있습니다. 이는 표준 라이브러리 내에 있는 Vec의 구현 부분입니다. 우리가 벡터 v에 []를 사용할 때 실행되는 코드는 libcollections/vec.rs 안에 있으며, 그곳이 바로 panic!이 실제 발생한 곳입니다. 그 다음 노트는 RUST_BACKTRACE 환경 변수를 설정하여 에러의 원인이 된 것이 무엇인지 정확하게 백트레이스할 수 있다고 말해주고 있습니다. 백트레이스 (backtrace) 란 어떤 지점에 도달하기까지 호출해온 모든 함수의 리스트를 말합니다. 러스트의 백트레이스는 다른 언어들에서와 마찬가지로 동작합니다: 백트레이스를 읽는 요령은 위에서부터 시작하여 여러분이 작성한 파일이 보일 때까지 읽는 것입니다. 그곳이 바로 문제를 일으킨 지점입니다. 여러분의 파일을 언급한 줄보다 위에 있는 줄들은 여러분의 코드가 호출한 코드입니다; 밑의 코드는 여러분의 코드를 호출한 코드입니다. 이 줄들은 핵심(core) 러스트 코드, 표준 라이브러리, 혹은 여러분이 이용하고 있는 크레이트를 포함하고 있을지도 모릅니다. 백트레이스를 얻어내는 시도를 해봅시다: Listing 9-2는 여러분이 보게 될 것과 유사한 출력을 보여줍니다: $ RUST_BACKTRACE=1 cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/panic`\nthread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', /stable-dist-rustc/build/src/libcollections/vec.rs:1392\nstack backtrace: 1: 0x560ed90ec04c - std::sys::imp::backtrace::tracing::imp::write::hf33ae72d0baa11ed at /stable-dist-rustc/build/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:42 2: 0x560ed90ee03e - std::panicking::default_hook::{{closure}}::h59672b733cc6a455 at /stable-dist-rustc/build/src/libstd/panicking.rs:351 3: 0x560ed90edc44 - std::panicking::default_hook::h1670459d2f3f8843 at /stable-dist-rustc/build/src/libstd/panicking.rs:367 4: 0x560ed90ee41b - std::panicking::rust_panic_with_hook::hcf0ddb069e7abcd7 at /stable-dist-rustc/build/src/libstd/panicking.rs:555 5: 0x560ed90ee2b4 - std::panicking::begin_panic::hd6eb68e27bdf6140 at /stable-dist-rustc/build/src/libstd/panicking.rs:517 6: 0x560ed90ee1d9 - std::panicking::begin_panic_fmt::abcd5965948b877f8 at /stable-dist-rustc/build/src/libstd/panicking.rs:501 7: 0x560ed90ee167 - rust_begin_unwind at /stable-dist-rustc/build/src/libstd/panicking.rs:477 8: 0x560ed911401d - core::panicking::panic_fmt::hc0f6d7b2c300cdd9 at /stable-dist-rustc/build/src/libcore/panicking.rs:69 9: 0x560ed9113fc8 - core::panicking::panic_bounds_check::h02a4af86d01b3e96 at /stable-dist-rustc/build/src/libcore/panicking.rs:56 10: 0x560ed90e71c5 - as core::ops::Index>::index::h98abcd4e2a74c41 at /stable-dist-rustc/build/src/libcollections/vec.rs:1392 11: 0x560ed90e727a - panic::main::h5d6b77c20526bc35 at /home/you/projects/panic/src/main.rs:4 12: 0x560ed90f5d6a - __rust_maybe_catch_panic at /stable-dist-rustc/build/src/libpanic_unwind/lib.rs:98 13: 0x560ed90ee926 - std::rt::lang_start::hd7c880a37a646e81 at /stable-dist-rustc/build/src/libstd/panicking.rs:436 at /stable-dist-rustc/build/src/libstd/panic.rs:361 at /stable-dist-rustc/build/src/libstd/rt.rs:57 14: 0x560ed90e7302 - main 15: 0x7f0d53f16400 - __libc_start_main 16: 0x560ed90e6659 - _start 17: 0x0 - Listing 9-2: 환경 변수 RUST_BACKTRACE가 설정되었을 때 panic!의 호출에 의해 발생되는 백트레이스 출력 출력이 엄청 많군요! 여러분이 보는 실제 출력값은 여러분의 운영 체제 및 러스트 버전에 따라 다를 수 있습니다. 이러한 정보들과 함께 백트레이스를 얻기 위해서는 디버그 심볼이 활성화되어 있어야 합니다. 디버그 심볼은 여기서와 마찬가지로 여러분이 cargo build나 cargo run을 --release 플래그 없이 실행했을 때 기본적으로 활성화됩니다. Listing 9-2의 출력값 내에서, 백트레이스의 11번 라인이 문제를 일으킨 우리 프로젝트의 라인을 가리키고 있습니다: 바로 src/main.rs , 4번 라인입니다. 만일 프로그램이 패닉에 빠지지 않도록 하고 싶다면, 우리가 작성한 파일이 언급된 첫 라인으로 지적된 위치가 바로 패닉을 일으킨 값을 가지고 있는 위치를 찾아내기 위해 수사하기 시작할 지점입니다. 백트레이스를 어떻게 사용하는지 시범을 보이기 위해 고의로 패닉을 일으키는 코드를 작성한 우리의 예제에서, 패닉을 고칠 방법은 고작 3개의 아이템을 가진 벡터로부터 인덱스 100에서의 요소를 요청하지 않도록 하는 것입니다. 여러분의 코드가 추후 패닉에 빠졌을 때, 여러분의 특정한 경우에 대하여 어떤 코드가 패닉을 일으키는 값을 만드는지와 코드는 대신 어떻게 되어야 할지를 알아낼 필요가 있을 것입니다. 우리는 panic!으로 다시 돌아올 것이며 언제 panic!을 써야 하는지, 혹은 쓰지 말아야 하는지에 대해 이 장의 뒷부분에서 알아보겠습니다. 다음으로 Result를 이용하여 에러로부터 어떻게 복구하는지를 보겠습니다.","breadcrumbs":"에러 처리 » panic!과 함께하는 복구 불가능한 에러 » panic! 백트레이스 사용하기","id":"149","title":"panic! 백트레이스 사용하기"},"15":{"body":"Windows에서는 https://www.rust-lang.org/en-US/install.html 페이지로 가서 러스트 설치를 위한 지시를 따르세요. 설치의 몇몇 지점에서, 여러분이 Visual Studio 2013이나 이후 버전용 C++ 빌드 도구 또한 설치할 필요가 있음을 설명하는 메세지를 받을 것입니다. 이 빌드 도구를 얻는 가장 쉬운 방법은 Visual Studio 2017용 빌드 도구 를 설치하는 것입니다. 이 도구들은 다른 도구 및 프레임워크 섹션 내에 있습니다. 이 책의 나머지 부분에서는 cmd.exe 및 파워쉘 모두에서 동작하는 커멘드를 사용합니다. 만일 특별히 다른 부분이 있다면, 어떤 것을 이용하는지 설명할 것입니다.","breadcrumbs":"시작하기 » 설치하기 » Windows에서 Rustup 설치하기","id":"15","title":"Windows에서 Rustup 설치하기"},"150":{"body":"대부분의 에러는 프로그램을 전부 멈추도록 요구될 정도로 심각하지는 않습니다. 종종 어떤 함수가 실패할 때는, 우리가 쉽게 해석하고 대응할 수 있는 이유에 대한 것입니다. 예를 들어, 만일 우리가 어떤 파일을 여는데 해당 파일이 존재하지 않아서 연산에 실패했다면, 프로세스를 멈추는 대신 파일을 새로 만드는 것을 원할지도 모릅니다. 2장의 “ Result 타입으로 잠재된 실패 다루기 ” 절에서 Result 열거형은 다음과 같이 Ok와 Err라는 두 개의 variant를 갖도록 정의되어 있음을 상기하세요: enum Result { Ok(T), Err(E),\n} T와 E는 제네릭 타입 파라미터입니다; 10장에서 제네릭에 대해 더 자세히 다룰 것입니다. 지금으로서 여러분이 알아둘 필요가 있는 것은, T는 성공한 경우에 Ok variant 내에 반환될 값의 타입을 나타내고 E는 실패한 경우에 Err variant 내에 반환될 에러의 타입을 나타내는 것이라는 점입니다. Result가 이러한 제네릭 타입 파라미터를 갖기 때문에, 우리가 반환하고자 하는 성공적인 값과 에러 값이 다를 수 있는 다양한 상황 내에서 표준 라이브러리에 정의된 Result 타입과 함수들을 사용할 수 있습니다. 실패할 수도 있기 때문에 Result 값을 반환하는 함수를 호출해 봅시다: Listing 9-3에서는 파일 열기를 시도합니다: Filename: src/main.rs use std::fs::File; fn main() { let f = File::open(\"hello.txt\");\n} Listing 9-3: 파일 열기 File::open이 Result를 반환하는지 어떻게 알까요? 표준 라이브러리 API 문서를 찾아보거나, 컴파일러에게 물어볼 수 있습니다! 만일 f에게 우리가 알고 있고 그 함수의 반환 타입은 아닐 어떤 타입에 대한 타입 명시를 주고 그 코드의 컴파일을 시도한다면, 컴파일러는 우리에게 타입이 맞지 않는다고 알려줄 것입니다. 그 후 에러 메세지는 f의 타입이 무엇인지 알려줄 것입니다. 한번 해봅시다: 우리는 File::open의 반환 타입이 u32는 아니라는 것을 알고 있으니, let f 구문을 이렇게 바꿔봅시다: let f: u32 = File::open(\"hello.txt\"); 이제 컴파일을 시도하면 다음 메세지가 나타납니다: error[E0308]: mismatched types --> src/main.rs:4:18 |\n4 | let f: u32 = File::open(\"hello.txt\"); | ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum\n`std::result::Result` | = note: expected type `u32` = note: found type `std::result::Result` 이 메세지는 File::open 함수의 반환 타입이 Result라는 것을 알려줍니다. 여기서 제네릭 파라미터 T는 성공값의 타입인 std::fs::File로 채워져 있는데, 이것은 파일 핸들입니다. 에러에 사용되는 E의 타입은 std::io::Error입니다. 이 반환 타입은 File::open을 호출하는 것이 성공하여 우리가 읽거나 쓸 수 있는 파일 핸들을 반환해 줄 수도 있다는 뜻입니다. 함수 호출은 또한 실패할 수도 있습니다: 예를 들면 파일이 존재하지 않거나 파일에 접근할 권한이 없을지도 모릅니다. File::open 함수는 우리에게 성공했는지 혹은 실패했는지를 알려주면서 동시에 파일 핸들이나 에러 정보 둘 중 하나를 우리에게 제공할 방법을 가질 필요가 있습니다. 바로 이러한 정보가 Result 열거형이 전달하는 것과 정확히 일치합니다. File::open이 성공한 경우, 변수 f가 가지게 될 값은 파일 핸들을 담고 있는 Ok 인스턴스가 될 것입니다. 실패한 경우, f의 값은 발생한 에러의 종류에 대한 더 많은 정보를 가지고 있는 Err의 인스턴스가 될 것입니다. 우리는 Listing 9-3의 코드에 File::open이 반환하는 값에 따라 다른 행동을 취하는 코드를 추가할 필요가 있습니다. Listing 9-4은 우리가 6장에서 다뤘던 기초 도구 match 표현식을 이용하여 Result를 처리하는 한 가지 방법을 보여줍니다: Filename: src/main.rs use std::fs::File; fn main() { let f = File::open(\"hello.txt\"); let f = match f { Ok(file) => file, Err(error) => { panic!(\"There was a problem opening the file: {:?}\", error) }, };\n} Listing 9-4: match 표현식을 사용하여 발생 가능한 Result variant들을 처리하기 Option 열거형과 같이 Result 열거형과 variant들은 프렐루드(prelude)로부터 가져와진다는 점을 기억하세요. 따라서 match의 각 경우에 대해서 Ok와 Err 앞에 Result::를 특정하지 않아도 됩니다. 여기서 우리는 러스트에게 결과가 Ok일 때에는 Ok variant로부터 내부의 file 값을 반환하고, 이 파일 핸들 값을 변수 f에 대입한다고 말해주고 있습니다. match 이후에는 읽거나 쓰기 위해 이 파일 핸들을 사용할 수 있습니다. match의 다른 경우는 File::open으로부터 Err를 얻은 경우를 처리합니다. 이 예제에서는 panic! 매크로를 호출하는 방법을 택했습니다. 우리의 현재 디렉토리 내에 hello.txt 라는 이름의 파일이 없는데 이 코드를 실행하게 되면, panic! 매크로로부터 다음과 같은 출력을 보게 될 것입니다: thread 'main' panicked at 'There was a problem opening the file: Error { repr:\nOs { code: 2, message: \"No such file or directory\" } }', src/main.rs:9:12 늘 그렇듯이, 이 출력은 어떤 것이 잘못되었는지 정확히 알려줍니다.","breadcrumbs":"에러 처리 » Result와 함께하는 복구 가능한 에러 » Result와 함께하는 복구 가능한 에러","id":"150","title":"Result와 함께하는 복구 가능한 에러"},"151":{"body":"Listing 9-3의 코드는 File::open이 실패한 이유가 무엇이든 간에 panic!을 일으킬 것입니다. 대신 우리가 원하는 것은 실패 이유에 따라 다른 행동을 취하는 것입니다: 파일이 없어서 File::open이 실패한 것이라면, 새로운 파일을 만들어서 핸들을 반환하고 싶습니다. 만일 그 밖의 이유로 File::open이 실패한 거라면, 예를 들어 파일을 열 권한이 없어서라면, Listing 9-4에서 했던 것과 마찬가지로 panic!을 일으키고 싶습니다. match에 새로운 경우를 추가한 Listing 9-5를 봅시다: Filename: src/main.rs use std::fs::File;\nuse std::io::ErrorKind; fn main() { let f = File::open(\"hello.txt\"); let f = match f { Ok(file) => file, Err(ref error) if error.kind() == ErrorKind::NotFound => { match File::create(\"hello.txt\") { Ok(fc) => fc, Err(e) => { panic!( \"Tried to create file but there was a problem: {:?}\", e ) }, } }, Err(error) => { panic!( \"There was a problem opening the file: {:?}\", error ) }, };\n} Listing 9-5: 다른 종류의 에러를 다른 방식으로 처리하기 Err variant 내에 있는 File::open이 반환하는 값의 타입은 io::Error인데, 이는 표준 라이브러리에서 제공하는 구조체입니다. 이 구조체는 kind 메소드를 제공하는데 이를 호출하여 io::ErrorKind값을 얻을 수 있습니다. io::ErrorKind는 io 연산으로부터 발생할 수 있는 여러 종류의 에러를 표현하는 variant를 가진, 표준 라이브러리에서 제공하는 열거형입니다. 우리가 사용하고자 하는 variant는 ErrorKind::NotFound인데, 이는 열고자 하는 파일이 아직 존재하지 않음을 나타냅니다. 조건문 if error.kind() == ErrorKind::NotFound는 매치 가드(match guard) 라고 부릅니다: 이는 match 줄기 상에서 줄기의 패턴을 좀 더 정제해주는 추가 조건문입니다. 그 줄기의 코드가 실행되기 위해서는 이 조건문이 참이어야 합니다; 그렇지 않다면, 패턴 매칭은 match의 다음 줄기에 맞춰보기 위해 이동할 것입니다. 패턴에는 ref가 필요하며 그럼으로써 error가 가드 조건문으로 소유권 이동이 되지 않고 그저 참조만 됩니다. 패턴 내에서 참조자를 얻기 위해 &대신 ref이 사용되는 이유는 18장에서 자세히 다룰 것입니다. 짧게 설명하면, &는 참조자를 매치하고 그 값을 제공하지만, ref는 값을 매치하여 그 참조자를 제공합니다. 매치 가드 내에서 확인하고자 하는 조건문은 error.kind()에 의해 반환된 값이 ErrorKind 열거형의 NotFound variant인가 하는 것입니다. 만일 그렇다면, File::create로 파일 생성을 시도합니다. 그러나, File::create 또한 실패할 수 있기 때문에, 안쪽에 match 구문을 바깥쪽과 마찬가지로 추가할 필요가 있습니다. 파일이 열 수 없을 때, 다른 에러 메세지가 출력될 것입니다. 바깥쪽 match의 마지막 갈래는 똑같이 남아서, 파일을 못 찾는 에러 외에 다른 어떤 에러에 대해서도 패닉을 일으킵니다.","breadcrumbs":"에러 처리 » Result와 함께하는 복구 가능한 에러 » 서로 다른 에러에 대해 매칭하기","id":"151","title":"서로 다른 에러에 대해 매칭하기"},"152":{"body":"match의 사용은 충분히 잘 동작하지만, 살짝 장황하기도 하고 의도를 항상 잘 전달하는 것도 아닙니다. Result 타입은 다양한 작업을 하기 위해 정의된 수많은 헬퍼 메소드를 가지고 있습니다. 그 중 하나인 unwrap 이라 부르는 메소드는 Listing 9-4에서 작성한 match 구문과 비슷한 구현을 한 숏컷 메소드입니다. 만일 Result 값이 Ok variant라면, unwrap은 Ok 내의 값을 반환할 것입니다. 만일 Result가 Err variant라면, unwrap은 우리를 위해 panic! 매크로를 호출할 것입니다. 아래에 unwrap이 작동하는 예가 있습니다: Filename: src/main.rs use std::fs::File; fn main() { let f = File::open(\"hello.txt\").unwrap();\n} hello.txt 파일이 없는 상태에서 이 코드를 실행시키면, unwrap 메소드에 의한 panic! 호출로부터의 에러 메세지를 보게 될 것입니다: thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {\nrepr: Os { code: 2, message: \"No such file or directory\" } }',\n/stable-dist-rustc/build/src/libcore/result.rs:868 또 다른 메소드인 expect는 unwrap과 유사한데, 우리가 panic! 에러 메세지를 선택할 수 있게 해줍니다. unwrap대신 expect를 이용하고 좋은 에러 메세지를 제공하는 것은 여러분의 의도를 전달해주고 패닉의 근원을 추적하는 걸 쉽게 해 줄 수 있습니다. expect의 문법은 아래와 같이 생겼습니다: Filename: src/main.rs use std::fs::File; fn main() { let f = File::open(\"hello.txt\").expect(\"Failed to open hello.txt\");\n} expect는 unwrap과 같은 식으로 사용됩니다: 파일 핸들을 리턴하거나 panic! 매크로를 호출하는 것이죠. expect가 panic! 호출에 사용하는 에러 메세지는 unwrap이 사용하는 기본 panic! 메세지보다는 expect에 넘기는 파라미터로 설정될 것입니다. 아래에 어떻게 생겼는지에 대한 예가 있습니다: thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:\n2, message: \"No such file or directory\" } }',\n/stable-dist-rustc/build/src/libcore/result.rs:868 이 에러 메세지는 우리가 특정한 텍스트인 Failed to open hello.txt로 시작하기 때문에, 이 에러 메세지가 어디서부터 왔는지를 코드 내에서 찾기가 더 수월해질 것입니다. 만일 우리가 여러 군데에 unwrap을 사용하면, 정확히 어떤 unwrap이 패닉을 일으켰는지 찾기에 좀 더 많은 시간이 걸릴 수 있는데, 그 이유는 패닉을 호출하는 모든 unwrap이 동일한 메세지를 출력하기 때문입니다.","breadcrumbs":"에러 처리 » Result와 함께하는 복구 가능한 에러 » 에러가 났을 때 패닉을 위한 숏컷: unwrap과 expect","id":"152","title":"에러가 났을 때 패닉을 위한 숏컷: unwrap과 expect"},"153":{"body":"실패할지도 모르는 무언가를 호출하는 구현을 가진 함수를 작성할 때, 이 함수 내에서 에러를 처리하는 대신, 에러를 호출하는 코드 쪽으로 반환하여 그쪽에서 어떻게 할지 결정하도록 할 수 있습니다. 이는 에러 전파하기 로 알려져 있으며, 에러가 어떻게 처리해야 좋을지 좌우해야 할 상황에서, 여러분의 코드 내용 내에서 이용 가능한 것들보다 더 많은 정보와 로직을 가지고 있을 수도 있는 호출하는 코드 쪽에 더 많은 제어권을 줍니다. 예를 들면, Listing 9-6은 파일로부터 사용자 이름을 읽는 함수를 작성한 것입니다. 만일 파일이 존재하지 않거나 읽을 수 없다면, 이 함수는 호출하는 코드 쪽으로 해당 에러를 반환할 것입니다: Filename: src/main.rs use std::io;\nuse std::io::Read;\nuse std::fs::File; fn read_username_from_file() -> Result { let f = File::open(\"hello.txt\"); let mut f = match f { Ok(file) => file, Err(e) => return Err(e), }; let mut s = String::new(); match f.read_to_string(&mut s) { Ok(_) => Ok(s), Err(e) => Err(e), }\n} Listing 9-6: match를 이용하여 호출 코드 쪽으로 에러를 반환하는 함수 함수의 반환 타입부터 먼저 살펴봅시다: Result. 이는 함수가 Result 타입의 값을 반환하는데 제네릭 파라미터 T는 구체적 타입(concrete type)인 String로 채워져 있고, 제네릭 타입 E는 구체적 타입인 io::Error로 채워져 있습니다. 만일 이 함수가 어떤 문제 없이 성공하면, 함수를 호출한 코드는 String을 담은 값을 받을 것입니다 - 이 함수가 파일로부터 읽어들인 사용자 이름이겠지요. 만일 어떤 문제가 발생한다면, 이 함수를 호출한 코드는 문제가 무엇이었는지에 대한 더 많은 정보를 담고 있는 io::Error의 인스턴스를 담은 Err 값을 받을 것입니다. 이 함수의 반환 타입으로서 io::Error를 선택했는데, 그 이유는 우리가 이 함수 내부에서 호출하고 있는 실패 가능한 연산 두 가지가 모두 이 타입의 에러 값을 반환하기 때문입니다: File::open 함수와 read_to_string 메소드 말이죠. 함수의 본체는 File::open 함수를 호출하면서 시작합니다. 그다음에는 Listing 9-4에서 본 match와 유사한 식으로 match을 이용해서 Result 값을 처리하는데, Err 경우에 panic!을 호출하는 대신 이 함수를 일찍 끝내고 File::open으로부터의 에러 값을 마치 이 함수의 에러 값인 것처럼 호출하는 쪽의 코드에게 전달합니다. 만일 File::open이 성공하면, 파일 핸들을 f에 저장하고 계속합니다. 그 뒤 변수 s에 새로운 String을 생성하고 파일의 콘텐츠를 읽어 s에 넣기 위해 f에 있는 파일 핸들의 read_to_string 메소드를 호출합니다. File::open가 성공하더라도 read_to_string 메소드가 실패할 수 있기 때문에 이 함수 또한 Result를 반환합니다. 따라서 이 Result를 처리하기 위해서 또 다른 match가 필요합니다: 만일 read_to_string이 성공하면, 우리의 함수가 성공한 것이고, 이제 s 안에 있는 파일로부터 읽어들인 사용자 이름을 Ok에 싸서 반환합니다. 만일 read_to_string이 실패하면, File::open의 반환값을 처리했던 match에서 에러값을 반환하는 것과 같은 방식으로 에러 값을 반환합니다. 하지만 여기서는 명시적으로 return이라 말할 필요는 없는데, 그 이유는 이 함수의 마지막 표현식이기 때문입니다. 그러면 이 코드를 호출하는 코드는 사용자 이름을 담은 Ok 값 혹은 io::Error를 담은 Err 값을 얻는 처리를 하게 될 것입니다. 호출하는 코드가 이 값을 가지고 어떤 일을 할 것인지 우리는 알지 못합니다. 만일 그쪽에서 Err 값을 얻었다면, 예를 들면 panic!을 호출하여 프로그램을 종료시키는 선택을 할 수도 있고, 기본 사용자 이름을 사용할 수도 있으며, 혹은 파일이 아닌 다른 어딘가에서 사용자 이름을 찾을 수도 있습니다. 호출하는 코드가 정확히 어떤 것을 시도하려 하는지에 대한 충분한 정보가 없기 때문에, 우리는 모든 성공 혹은 에러 정보를 위로 전파하여 호출하는 코드가 적절하게 처리를 하도록 합니다. 러스트에서 에러를 전파하는 패턴은 너무 흔하여 러스트에서는 이를 더 쉽게 해주는 물음표 연산자 ?를 제공합니다.","breadcrumbs":"에러 처리 » Result와 함께하는 복구 가능한 에러 » 에러 전파하기","id":"153","title":"에러 전파하기"},"154":{"body":"Listing 9-7은 Listing 9-6과 같은 기능을 가진 read_username_from_file의 구현을 보여주는데, 다만 이 구현은 물음표 연산자를 이용하고 있습니다: Filename: src/main.rs use std::io;\nuse std::io::Read;\nuse std::fs::File; fn read_username_from_file() -> Result { let mut f = File::open(\"hello.txt\")?; let mut s = String::new(); f.read_to_string(&mut s)?; Ok(s)\n} Listing 9-7: ?를 이용하여 에러를 호출하는 코드 쪽으로 반환하는 함수 Result 값 뒤의 ?는 Listing 9-6에서 Result 값을 다루기 위해 정의했던 match 표현식과 거의 같은 방식으로 동작하게끔 정의되어 있습니다. 만일 Result의 값이 Ok라면, Ok 내의 값이 이 표현식으로부터 얻어지고 프로그램이 계속됩니다. 만일 값이 Err라면, 우리가 return 키워드를 사용하여 에러 값을 호출하는 코드에게 전파하는 것과 같이 전체 함수로부터 Err 내의 값이 반환될 것입니다. Listing 9-6에 있는 match 표현식과 물음표 연산자가 수행하는 한 가지 차이점은 물음표 연산자를 사용할 때 에러 값들이 표준 라이브러리 내에 있는 From 트레잇에 정의된 from 함수를 친다는 것입니다. 많은 에러 타입들이 어떤 타입의 에러를 다음 타입의 에러로 변환하기 위해 from 함수를 구현하였습니다. 물음표 연산자가 사용되면, from 함수의 호출이 물음표 연산자가 얻게 되는 에러 타입을 ?이 사용되고 있는 현재 함수의 반환 타입에 정의된 에러 타입으로 변환합니다. 이는 어떤 함수의 부분들이 수많은 다른 이유로 인해 실패할 수 있지만 이 함수는 실패하는 모든 방식을 하나의 에러 타입으로 반환할 때 유용합니다. 각각의 에러 타입이 그 자신을 반환되는 에러 타입으로 변경할 방법을 정의하기 위해 from 함수를 구현하기만 한다면, 물음표 연산자는 이 변환을 자동적으로 다룹니다. Listing 9-7의 내용에서, File::open 호출 부분의 끝에 있는 ?는 Ok내의 값을 변수 f에게 반환해줄 것입니다. 만일 에러가 발생하면 ?는 전체 함수로부터 일찍 빠져나와 호출하는 코드에게 어떤 Err 값을 줄 것입니다. read_to_string 호출의 끝부분에 있는 ?도 같은 것이 적용되어 있습니다. ?는 많은 수의 보일러플레이트(boilerplate)를 제거해주고 이 함수의 구현을 더 단순하게 만들어 줍니다. 심지어는 Listing 9-8과 같이 ? 뒤에 바로 메소드 호출을 연결하는 식으로 (chaining) 이 코드를 더 줄일 수도 있습니다: Filename: src/main.rs use std::io;\nuse std::io::Read;\nuse std::fs::File; fn read_username_from_file() -> Result { let mut s = String::new(); File::open(\"hello.txt\")?.read_to_string(&mut s)?; Ok(s)\n} Listing 9-8: 물음표 연산자 뒤에 메소드 호출을 연결하기 새로운 String을 만들어 s에 넣는 부분을 함수의 시작 부분으로 옮겼습니다; 이 부분은 달라진 것이 없습니다. f 변수를 만드는 대신, File::open(\"hello.txt\")?의 결과 바로 뒤에 read_to_string의 호출을 연결시켰습니다. read_to_string 호출의 끝에는 여전히 ?가 남아있고, File::open과 read_to_string이 모두 에러를 반환하지 않고 성공할 때 s 안의 사용자 이름을 담은 Ok를 여전히 반환합니다. 함수의 기능 또한 Lsting 9-6과 Listing 9-7의 것과 동일하고, 다만 작성하기에 더 인체공학적인 방법이라는 차이만 있을 뿐입니다.","breadcrumbs":"에러 처리 » Result와 함께하는 복구 가능한 에러 » 에러를 전파하기 위한 숏컷: ?","id":"154","title":"에러를 전파하기 위한 숏컷: ?"},"155":{"body":"?는 Result 타입을 반환하는 함수에서만 사용이 가능한데, 이것이 Listing 9-6에 정의된 match 표현식과 동일한 방식으로 동작하도록 정의되어 있기 때문입니다. Result 반환 타입을 요구하는 match 부분은 return Err(e)이며, 따라서 함수의 반환 타입은 반드시 이 return과 호환 가능한 Result가 되어야 합니다. main의 반환 타입이 ()라는 것을 상기하면서, 만약 main 함수 내에서 ?를 사용하면 어떤일이 생길지 살펴봅시다: use std::fs::File; fn main() { let f = File::open(\"hello.txt\")?;\n} 이걸 컴파일하면, 아래와 같은 에러 메세지가 뜹니다: error[E0277]: the `?` operator can only be used in a function that returns\n`Result` (or another type that implements `std::ops::Try`) --> src/main.rs:4:13 |\n4 | let f = File::open(\"hello.txt\")?; | ------------------------ | | | cannot use the `?` operator in a function that returns `()` | in this macro invocation = help: the trait `std::ops::Try` is not implemented for `()` = note: required by `std::ops::Try::from_error` 이 에러는 오직 Result를 반환하는 함수 내에서만 물음표 연산자를 사용할 수 있음을 지적합니다. Result를 반환하지 않는 함수 내에서, 여러분이 Result를 반환하는 다른 함수를 호출했을 때, 여러분은 ?를 사용하여 호출하는 코드에게 잠재적으로 에러를 전파하는 대신 match나 Result에서 제공하는 메소드들 중 하나를 사용하여 이를 처리할 필요가 있을 것입니다. panic!을 호출하거나 Result를 반환하는 것의 자세한 부분을 논의했으니, 어떤 경우에 어떤 방법을 사용하는 것이 적합할지를 어떻게 결정하는가에 대한 주제로 돌아갑시다.","breadcrumbs":"에러 처리 » Result와 함께하는 복구 가능한 에러 » ?는 Result를 반환하는 함수에서만 사용될 수 있습니다","id":"155","title":"?는 Result를 반환하는 함수에서만 사용될 수 있습니다"},"156":{"body":"그러면 언제 panic!을 써야 하고 언제 Result를 반환할지 어떻게 결정해야 할까요? 코드가 패닉을 일으킬 때는 복구할 방법이 없습니다. 복구 가능한 방법이 있든 혹은 그렇지 않든 여러분은 어떤 에러 상황에 대해 panic!을 호출할 수 있지만, 그렇다면 여러분은 여러분의 코드를 호출하는 코드를 대신하여 현 상황은 복구 불가능한 것이라고 결정을 내리는 겁니다. 여러분이 Result 값을 반환하는 선택을 한다면, 호출하는 코드에게 결단을 내려주기보다는 옵션을 제공하는 것입니다. 그들은 그들의 상황에 적합한 방식으로 복구를 시도할 수도 있고, 혹은 현재 상황의 Err은 복구 불가능하다고 사실상 결론을 내려서 panic!을 호출하여 여러분이 만든 복구 가능한 에러를 복구 불가능한 것으로 바꿔놓을 수도 있습니다. 그러므로, 여러분이 실패할지도 모르는 함수를 정의할 때는 Result을 반환하는 것이 기본적으로 좋은 선택입니다. 몇 가지 상황에서는 Result를 반환하는 대신 패닉을 일으키는 코드를 작성하는 것이 더 적합하지만, 덜 일반적입니다. 예제, 프로토타입 코드 및 테스트의 경우에는 왜 패닉이 더 좋은지를 탐구합시다; 그다음, 사람으로서의 여러분이라면 실패할 리 없는 메소드라는 것을 알 수 있지만 컴파일러는 이유를 파악할 수 없는 경우도 봅시다; 그리고 라이브러리 코드에 패닉을 추가해야 할지 말지를 어떻게 결정할까에 대한 일반적인 가이드라인을 내림으로서 결론지어 봅시다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » panic!이냐, panic!이 아니냐, 그것이 문제로다","id":"156","title":"panic!이냐, panic!이 아니냐, 그것이 문제로다"},"157":{"body":"여러분이 어떤 개념을 그려내기 위한 예제를 작성 중이라면, 강건한 에러 처리 코드를 예제 안에 넣는 것은 또한 예제를 덜 깨끗하게 만들 수 있습니다. 예제 코드 내에서는 panic!을 일으킬 수 있는 unwrap 같은 메소드를 호출하는 것이 여러분의 어플리케이션이 에러를 처리하고자 하는 방법에 대한 플레이스홀더로서의 의미를 갖는데, 이는 여러분의 코드의 나머지 부분이 어떤 것을 하는지에 따라 달라질 수 있습니다. 비슷한 상황에서, 여러분이 에러를 어떻게 처리할지 결정할 준비가 되기 전에는, unwrap과 expect 메소드가 프로토타이핑을 할 때 매우 편리합니다. 이 함수들은 여러분의 코드를 더 강건하게 만들 준비가 되었을 때를 위해서 명확한 표시를 남겨 둡니다. 만일 테스트 내에서 메소드 호출이 실패한다면, 해당 메소드가 테스트 중인 기능이 아니더라도 전체 테스트가 실패하는 게 좋을 것입니다. panic!이 테스트를 실패시키는 방법이기 때문에, unwrap이나 expect를 호출하는 것은 정확하게 하고자 하는 일과 일치합니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 예제, 프로토타입 코드, 그리고 테스트는 전부 패닉을 일으켜도 완전 괜찮은 곳입니다","id":"157","title":"예제, 프로토타입 코드, 그리고 테스트는 전부 패닉을 일으켜도 완전 괜찮은 곳입니다"},"158":{"body":"Result가 Ok 값을 가지고 있을 거라 확신할 다른 논리를 가지고 있지만, 그 논리가 컴파일러에 의해 이해할 수 있는 것이 아닐 때라면, unwrap을 호출하는 것이 또한 적절할 수 있습니다. 여러분은 여전히 처리할 필요가 있는 Result 값을 가지고 있습니다: 여러분의 특정한 상황에서 논리적으로 불가능할지라도, 여러분이 호출하고 있는 연산이 무엇이든 간에 일반적으로는 여전히 실패할 가능성이 있습니다. 만일 여러분이 수동적으로 Err variant를 결코 발생시키지 않는 코드를 조사하여 확신할 수 있다면, unwrap을 호출하는 것이 완벽히 허용됩니다. 여기 예제가 있습니다: use std::net::IpAddr; let home = \"127.0.0.1\".parse::().unwrap(); 여기서는 하드코딩된 스트링을 파싱하여 IpAddr 인스턴스를 만드는 중입니다. 우리는 127.0.0.1이 유효한 IP 주소임을 볼 수 있으므로, 여기서 unwrap을 사용하는 것은 허용됩니다. 그러나, 하드코딩된 유효한 스트링을 갖고 있다는 것이 parse 메소드의 반환 타입을 변경해주지는 않습니다: 우리는 여전히 Result 값을 갖게 되고, 컴파일러는 마치 Err variant가 나올 가능성이 여전히 있는 것처럼 우리가 Result를 처리하도록 할 것인데, 그 이유는 이 스트링이 항상 유효한 IP 주소라는 것을 알 수 있을 만큼 컴파일러가 똑똑하지는 않기 때문입니다. 만일 IP 주소 스트링이 프로그램 내에 하드코딩된 것이 아니라 사용자로부터 입력되었다면, 그래서 실패할 가능성이 생겼다면 , 우리는 대신 더 강건한 방식으로 Result를 처리할 필요가 분명히 있습니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 컴파일러보다 여러분이 더 많은 정보를 가지고 있을 때","id":"158","title":"컴파일러보다 여러분이 더 많은 정보를 가지고 있을 때"},"159":{"body":"여러분의 코드가 결국 나쁜 상태에 처하게 될 가능성이 있을 때는 여러분의 코드에 panic!을 넣는 것이 바람직합니다. 이 글에서 말하는 나쁜 상태란 어떤 가정, 보장, 계약, 혹은 불변성이 깨질 때를 뜻하는 것으로, 이를테면 유효하지 않은 값이나 모순되는 값, 혹은 찾을 수 없는 값이 여러분의 코드를 통과할 경우를 말합니다 - 아래에 쓰여진 상황 중 하나 혹은 그 이상일 경우라면 말이죠: 이 나쁜 상태란 것이 가끔 벌어질 것으로 예상되는 무언가가 아닙니다. 그 시점 이후의 코드는 이 나쁜 상태에 있지 않아야만 할 필요가 있습니다. 여러분이 사용하고 있는 타입 내에 이 정보를 집어 넣을만한 뾰족한 수가 없습니다. 만일 어떤 사람이 여러분의 코드를 호출하고 타당하지 않은 값을 집어넣었다면, panic!을 써서 여러분의 라이브러리를 사용하고 있는 사람에게 그들의 코드 내의 버그를 알려서 개발하는 동안 이를 고칠 수 있게끔 하는 것이 최선책일 수도 있습니다. 비슷한 식으로, 만일 여러분의 제어권을 벗어난 외부 코드를 호출하고 있고, 이것이 여러분이 고칠 방법이 없는 유효하지 않은 상태를 반환한다면, panic!이 종종 적합합니다. 나쁜 상태에 도달했지만, 여러분이 얼마나 코드를 잘 작성했든 간에 일어날 것으로 예상될 때라면 panic!을 호출하는 것보다 Result를 반환하는 것이 여전히 더 적합합니다. 이에 대한 예는 기형적인 데이터가 주어지는 파서나, 속도 제한에 달했음을 나타내는 상태를 반환하는 HTTP 요청 등을 포함합니다. 이러한 경우, 여러분은 이러한 나쁜 상태를 위로 전파하기 위해 호출자가 그 문제를 어떻게 처리할지를 결정할 수 있도록 하기 위해서 Result를 반환하는 방식으로 실패가 예상 가능한 것임을 알려줘야 합니다. panic!에 빠지는 것은 이러한 경우를 처리하는 최선의 방식이 아닐 것입니다. 여러분의 코드가 어떤 값에 대해 연산을 수행할 때, 여러분의 코드는 해당 값이 유효한지를 먼저 검사하고, 만일 그렇지 않다면 panic!을 호출해야 합니다. 이는 주로 안전상의 이유를 위한 것입니다: 유효하지 않은 데이터 상에서 어떤 연산을 시도하는 것은 여러분의 코드를 취약점에 노출시킬 수 있습니다. 이는 여러분이 범위를 벗어난 메모리 접근을 시도했을 경우 표준 라이브러리가 panic!을 호출하는 주된 이유입니다: 현재의 데이터 구조가 소유하지 않은 메모리를 접근 시도하는 것은 흔한 보안 문제입니다. 함수는 종종 계약 을 갖고 있습니다: 입력이 특정 요구사항을 만족시킬 경우에만 함수의 행동이 보장됩니다. 이 계약을 위반했을 때 패닉에 빠지는 것은 사리에 맞는데, 그 이유는 계약 위반이 언제나 호출자 쪽의 버그임을 나타내고, 이는 호출하는 코드가 명시적으로 처리하도록 하는 종류의 버그가 아니기 때문입니다. 사실, 호출하는 쪽의 코드가 복구시킬 합리적인 방법은 없습니다: 호출하는 프로그래머 는 그 코드를 고칠 필요가 있습니다. 함수에 대한 계약은, 특히 계약 위반이 패닉의 원인이 될 때는, 그 함수에 대한 API 문서에 설명되어야 합니다. 하지만 여러분의 모든 함수 내에서 수많은 에러 검사를 한다는 것은 장황하고 짜증 날 것입니다. 다행스럽게도, 러스트의 타입 시스템이 (그리고 컴파일러가 하는 타입 검사 기능이) 여러분을 위해 수많은 검사를 해줄 수 있습니다. 여러분의 함수가 특정한 타입을 파라미터로 갖고 있다면, 여러분이 유효한 값을 갖는다는 것을 컴파일러가 이미 보장했음을 아는 상태로 여러분의 코드 로직을 진행할 수 있습니다. 예를 들면, 만약 여러분이 Option이 아닌 어떤 타입을 갖고 있다면, 여러분의 프로그램은 아무것도 아닌 것 이 아닌 무언가 를 갖고 있음을 예측합니다. 그러면 여러분의 코드는 Some과 None variant에 대한 두 경우를 처리하지 않아도 됩니다: 이는 분명히 값을 가지고 있는 하나의 경우만 있을 것입니다. 여러분의 함수에 아무것도 넘기지 않는 시도를 하는 코드는 컴파일조차 되지 않을 것이고, 따라서 여러분의 함수는 그러한 경우에 대해서 런타임에 검사하지 않아도 됩니다. 또 다른 예로는 u32와 같은 부호 없는 정수를 이용하는 것이 있는데, 이는 파라미터가 절대 음수가 아님을 보장합니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 에러 처리를 위한 가이드라인","id":"159","title":"에러 처리를 위한 가이드라인"},"16":{"body":"만일 여러분이 어떤 이유로 rustup를 쓰지 않기를 선호한다면, the Rust installation page 페이지에서 다른 옵션을 확인하세요.","breadcrumbs":"시작하기 » 설치하기 » Rustup 없이 커스텀 설치하기","id":"16","title":"Rustup 없이 커스텀 설치하기"},"160":{"body":"러스트의 타입 시스템을 이용하여 유효한 값을 보장하는 아이디어에서 한 발 더 나가서, 유효성을 위한 커스텀 타입을 생성하는 것을 살펴봅시다. 2장의 추리 게임을 상기해 보시면, 우리의 코드는 사용자에게 1부터 100 사이의 숫자를 추측하도록 요청했었죠. 우리는 실제로는 사용자의 추측 값이 우리의 비밀 숫자와 비교하기 전에 해당 값이 유효한지 결코 확인하지 않았습니다; 우리는 추측값이 양수인지 만을 확인했습니다. 이 경우, 결과는 매우 끔찍하지는 않았습니다: “Too high”나 “Too low”라고 표시했던 출력은 여전히 맞을 것입니다. 사용자에게 유효한 추측 값을 안내해주고, 사용자가 예를 들어 글자를 입력했을 때에 비해 사용자가 범위 밖의 값을 추측했을 때 다른 동작을 하는 것은 쓸모 있는 향상일 것입니다. 이를 위한 한 가지 방법은 u32 대신 i32로서 추측 값을 파싱하여 음수가 입력될 가능성을 허용하고, 그리고나서 아래와 같이 숫자가 범위 내에 있는지에 대한 검사를 추가하는 것입니다: loop { // snip 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) { // snip\n} if 표현식은 우리의 값이 범위 밖에 있는지 혹은 그렇지 않은지 검사하고, 사용자에게 문제점을 말해주고, continue를 호출하여 루프의 다음 반복을 시작하고 다른 추측값을 요청해줍니다. if 표현식 이후에는, guess가 1과 100 사이의 값이라는 것을 아는 상태에서 guess와 비밀 숫자의 비교를 진행할 수 있습니다. 하지만, 이는 이상적인 해결책이 아닙니다: 만일 프로그램이 오직 1과 100 사이의 값에서만 동작하는 것이 전적으로 중요하고, 많은 함수가 이러한 요구사항을 가지고 있다면, 모든 함수 내에서 이렇게 검사를 하는 것은 지루할 것입니다. (그리고 잠재적으로 성능에 영향을 줄 것입니다.) 대신, 우리는 새로운 타입을 만들어서, 유효성 확인을 모든 곳에서 반복하는 것보다는 차라리 그 타입의 인스턴스를 생성하는 함수 내에 유효성 확인을 넣을 수 있습니다. 이 방식에서, 함수가 그 시그니처 내에서 새로운 타입을 이용하고 받은 값을 자신 있게 사용하는 것은 안전합니다. Listing 9-9는 new 함수가 1과 100 사이의 값을 받았을 때에만 인스턴스를 생성하는 Guess 타입을 정의하는 한 가지 방법을 보여줍니다: pub struct Guess { value: u32,\n} impl Guess { pub fn new(value: u32) -> Guess { if value < 1 || value > 100 { panic!(\"Guess value must be between 1 and 100, got {}.\", value); } Guess { value } } pub fn value(&self) -> u32 { self.value }\n} Listing 9-9: 1과 100 사이의 값일 때만 계속되는 Guess 타입 먼저 u32를 갖는 value라는 이름의 항목을 가진 Guess라는 이름의 구조체를 선언하였습니다. 이것이 숫자가 저장될 곳입니다. 그런 뒤 Guess 값의 인스턴스를 생성하는 new라는 이름의 연관 함수를 구현하였습니다. new 함수는 u32 타입의 값인 value를 파라미터를 갖고 Guess를 반환하도록 정의 되었습니다. new 함수의 본체에 있는 코드는 value가 1부터 100 사이의 값인지 확인하는 테스트를 합니다. 만일 value가 이 테스트에 통과하지 못하면 panic!을 호출하며, 이는 이 코드를 호출하는 프로그래머에게 고쳐야 할 버그가 있음을 알려주는데, 범위 밖의 value를 가지고 Guess를 생성하는 것은 Guess::new가 필요로 하는 계약을 위반하기 때문입니다. Guess::new가 패닉을 일으킬 수도 있는 조건은 공개된 API 문서 내에 다뤄져야 합니다; 여러분이 만드는 API 문서 내에서 panic!의 가능성을 가리키는 것에 대한 문서 관례는 14장에서 다룰 것입니다. 만일 value가 테스트를 통과한다면, value 항목을 value 파라미터로 설정한 새로운 Guess를 생성하여 이 Guess를 반환합니다. 다음으로, self를 빌리고, 파라미터를 갖지 않으며, u32를 반환하는 value라는 이름의 메소드를 구현했습니다. 이러한 종류 메소드를 종종 게터(getter) 라고 부르는데, 그 이유는 이런 함수의 목적이 객체의 항목으로부터 어떤 데이터를 가져와서 이를 반환하는 것이기 때문입니다. 이 공개 메소드는 Guess 구조체의 value 항목이 비공개이기 때문에 필요합니다. value 항목이 비공개라서 Guess 구조체를 이용하는 코드가 value를 직접 설정하지 못하도록 하는 것은 중요합니다: 모듈 밖의 코드는 반드시 Guess::new 함수를 이용하여 새로운 Guess의 인스턴스를 만들어야 하는데, 이는 Guess가 Guess::new 함수의 조건들을 확인한 적이 없는 value를 갖는 방법이 없음을 보장합니다. 그러면 파라미터를 가지고 있거나 오직 1에서 100 사이의 숫자를 반환하는 함수는 u32 보다는 Guess를 얻거나 반환하는 시그니처로 선언되고 더 이상의 확인이 필요치 않을 것입니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 유효성을 위한 커스텀 타입 생성하기","id":"160","title":"유효성을 위한 커스텀 타입 생성하기"},"161":{"body":"러스트의 에러 처리 기능은 여러분이 더 강건한 코드를 작성하는 데 도움을 주도록 설계되었습니다. panic! 매크로는 여러분의 프로그램이 처리 불가능한 상태에 놓여 있음에 대한 신호를 주고 여러분이 유효하지 않거나 잘못된 값으로 계속 진행하는 시도를 하는 대신 실행을 멈추게끔 해줍니다. Result 열거형은 러스트의 타입 시스템을 이용하여 여러분의 코드가 복구할 수 있는 방법으로 연산이 실패할 수도 있음을 알려줍니다. 또한 Result를 이용하면 여러분의 코드를 호출하는 코드에게 잠재적인 성공이나 실패를 처리해야 할 필요가 있음을 알려줄 수 있습니다. panic!과 Result를 적합한 상황에서 사용하는 것은 여러분의 코드가 불가피한 문제에 직면했을 때도 더 신뢰할 수 있도록 해줄 것입니다. 이제 표준 라이브러리가 Option과 Result 열거형을 가지고 제네릭을 사용하는 유용한 방식들을 보았으니, 제네릭이 어떤 식으로 동작하고 여러분의 코드에 어떻게 이용할 수 있는지에 대해 다음 장에서 이야기해 보겠습니다.","breadcrumbs":"에러 처리 » panic!이냐, panic!이 아니냐, 그것이 문제로다 » 정리","id":"161","title":"정리"},"162":{"body":"모든 프로그래밍 언어는 컨셉의 복제를 효율적으로 다루기 위한 도구를 가지고 있습니다; 러스트에서, 그러한 도구 중 하나가 바로 제네릭(generic) 입니다. 제네릭은 구체화된 타입이나 다른 속성들에 대하여 추상화된 대리인입니다. 코드를 작성하고 컴파일할 때, 우리는 제네릭들이 실제로 어떻게 완성되는지 알 필요 없이, 제네릭의 동작 혹은 다른 제네릭과 어떻게 연관되는지와 같은 제네릭에 대한 속성을 표현할 수 있습니다. 여러 개의 구체화된 값들에 대해 실행될 코드를 작성하기 위해서 함수가 어떤 값을 담을지 알 수 없는 파라미터를 갖는 것과 동일한 방식으로, i32나 String과 같은 구체화된 타입 대신 몇몇 제네릭 타입의 파라미터를 갖는 함수를 작성할 수 있습니다. 우리는 6장의 Option, 8장의 Vec와 HashMap, 그리고 9장의 Result에서 이미 제네릭을 사용해 보았습니다. 이 장에서는, 어떤 식으로 우리만의 타입, 함수, 그리고 메소드를 제네릭으로 정의하는지 탐험해 볼 것입니다! 우선, 우리는 코드 중복을 제거하는 함수의 추출하는 원리에 대해 돌아볼 것입니다. 그러고 나서 두 함수가 오직 파라미터의 타입만 다른 경우에 대하여 이들을 하나의 제네릭 함수로 만들기 위해 동일한 원리를 사용할 것입니다. 또한 제네릭 타입을 구조체와 열거형의 정의에 사용하는 것을 살펴볼 것입니다. 그리고 난 후 트레잇(trait) 에 대하여 논의할 것인데, 이는 동작을 제네릭 한 방식으로 정의하는 방법을 말합니다. 트레잇은 제네릭 타입과 결합되어 제네릭 타입에 대해 아무 타입이나 허용하지 않고, 특정 동작을 하는 타입으로 제한할 수 있습니다. 마지막으로, 우리는 라이프타임(lifetime) 에 대해 다룰 것인데, 이는 제네릭의 일종으로서 우리가 컴파일러에게 참조자들이 서로에게 어떤 연관이 있는지에 대한 정보를 줄 수 있도록 해줍니다. 라이프타임은 수많은 상황에서 값을 빌릴 수 있도록 허용해 주고도 여전히 참조자들이 유효할지를 컴파일러가 검증하도록 해주는 러스트의 지능입니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 제네릭 타입, 트레잇, 그리고 라이프타임","id":"162","title":"제네릭 타입, 트레잇, 그리고 라이프타임"},"163":{"body":"제네릭 문법을 들어가기 전에, 먼저 제네릭 타입을 이용하지 않는 중복 코드 다루기 기술을 훑어봅시다: 바로 함수 추출하기죠. 이를 한번 우리 마음속에서 생생하게 상기시키고 나면, 우리는 제네릭 함수를 추출하기 위해 제네릭을 가지고 똑같은 수법을 이용할 것입니다! 여러분이 함수로 추출할 중복된 코드를 인식하는 것과 똑같은 방식으로, 여러분은 제네릭을 이용할 수 있는 중복된 코드를 인식하기 시작할 것입니다. Listing 10-1과 같이 리스트에서 가장 큰 숫자를 찾아내는 작은 프로그램이 있다고 칩시다: Filename: src/main.rs fn main() { let numbers = vec![34, 50, 25, 100, 65]; let mut largest = numbers[0]; for number in numbers { if number > largest { largest = number; } } println!(\"The largest number is {}\", largest);\n# assert_eq!(largest, 100);\n} Listing 10-1: 숫자 리스트 중에서 가장 큰 수를 찾는 코드 이 코드는 정수의 리스트를 얻는데, 여기서는 변수 numbers에 저장되어 있습니다. 리스트의 첫 번째 아이템을 largest라는 이름의 변수에 우선 집어넣습니다. 그러고 나서 리스트 내의 모든 숫자들에 대해 반복 접근을 하는데, 만일 현재 숫자가 largest 내에 저장된 숫자보다 더 크다면, 이 숫자로 largest 내의 값을 변경합니다. 만일 현재 숫자가 여태까지 본 가장 큰 값보다 작다면, largest는 바뀌지 않습니다. 리스트 내의 모든 아이템을 다 처리했을 때, largest는 가장 큰 값을 가지고 있을 것인데, 위 코드의 경우에는 100이 될 것입니다. 만일 두 개의 서로 다른 숫자 리스트로부터 가장 큰 숫자를 찾기를 원한다면, Listing 10-1의 코드를 복사하여, Listing 10-2에서처럼 한 프로그램 내에 동일한 로직이 두 군데 있게 할 수도 있습니다: Filename: src/main.rs fn main() { let numbers = vec![34, 50, 25, 100, 65]; let mut largest = numbers[0]; for number in numbers { if number > largest { largest = number; } } println!(\"The largest number is {}\", largest); let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let mut largest = numbers[0]; for number in numbers { if number > largest { largest = number; } } println!(\"The largest number is {}\", largest);\n} Listing 10-2: 두 개의 숫자 리스트에서 가장 큰 숫자를 찾는 코드 이 코드는 잘 동작하지만, 코드를 중복 적용하는 일은 지루하고 오류가 발생하기도 쉬우며, 또한 로직을 바꾸고 싶다면 이 로직을 갱신할 곳이 여러 군데가 된다는 의미이기도 합니다. 이러한 중복을 제거하기 위해서 우리는 추상화를 쓸 수 있는데, 이 경우에는 어떠한 정수 리스트가 함수의 파라미터로 주어졌을 때 동작하는 함수의 형태가 될 것입니다. 이는 우리 코드의 명료성을 증가시켜주고 리스트 내에서 가장 큰 수를 찾는 컨셉을 사용하는 특정한 위치와 상관없이 이러한 컨셉을 전달하고 추론하도록 해줍니다. Listing 10-3의 프로그램에서는 가장 큰 수를 찾는 코드를 largest라는 이름의 함수로 추출했습니다. 이 프로그램은 두 개의 서로 다른 숫자 리스트에서 가장 큰 수를 찾을 수 있지만, Listing 10-1에서의 코드는 한 군데에서만 나타납니다: Filename: src/main.rs fn largest(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest\n} fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest(&numbers); println!(\"The largest number is {}\", result);\n# assert_eq!(result, 100); let numbers = vec![102, 34, 6000, 89, 54, 2, 43, 8]; let result = largest(&numbers); println!(\"The largest number is {}\", result);\n# assert_eq!(result, 6000);\n} Listing 10-3: 두 리스트에서 가장 큰 수를 찾는 추상화된 코드 이 함수는 list라는 파라미터를 갖고 있는데, 이것이 함수로 넘겨질 구체적인 임의 i32 값들의 슬라이스를 나타냅니다. 함수 정의 내의 코드는 임의의 &[i32]의 list 표현에 대해 동작합니다. largest 함수를 호출할 때, 이 코드는 실제로 우리가 넘겨준 구체적인 값에 대해 실행됩니다. Listing 10-2에서부터 Listing 10-3까지 우리가 살펴본 원리는 아래와 같은 단계로 진행되었습니다: 중복된 코드가 있음을 알아챘습니다. 중복된 코드를 함수의 본체로 추출하고, 함수의 시그니처 내에 해당 코드의 입력값 및 반환 값을 명시했습니다. 두 군데의 코드가 중복되었던 구체적인 지점에 함수 호출을 대신 집어넣었습니다. 우리는 다른 시나리오 상에서 다른 방식으로 제네릭을 가지고 중복된 코드를 제거하기 위해 같은 단계를 밟을 수 있습니다. 함수의 본체가 현재 구체적인 값 대신 추상화된 list에 대해 동작하고 있는 것과 같이, 제네릭을 이용한 코드는 추상화된 타입에 대해 작동할 것입니다. 제네릭으로 강화되는 컨셉은 여러분이 이미 알고 있는 함수로 강화되는 컨셉과 동일하며, 다만 다른 방식으로 적용될 뿐입니다. 만일 우리가 두 개의 함수를 가지고 있는데, 하나는 i32의 슬라이스에서 최댓값을 찾는 것이고 다른 하나는 char 값의 슬라이스에서 최댓값을 찾는 것이라면 어떨까요? 어떻게 하면 이런 중복을 제거할 수 있을까요? 한번 알아봅시다!","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 함수를 추출하여 중복 없애기","id":"163","title":"함수를 추출하여 중복 없애기"},"164":{"body":"함수 시그니처나 구조체에서와 같은 방식으로, 우리가 일반적으로 타입을 쓰는 곳에다 제네릭을 이용하는 것은 여러 다른 종류의 구체적인 데이터 타입에 대해 사용할 수 있는 정의를 생성하도록 해줍니다. 제네릭을 이용하여 함수, 구조체, 열거형, 그리고 메소드를 정의하는 방법을 살펴본 뒤, 이 절의 끝에서 제네릭을 이용한 코드의 성능에 대해 논의하겠습니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 제네릭 데이터 타입 » 제네릭 데이터 타입","id":"164","title":"제네릭 데이터 타입"},"165":{"body":"우리는 함수의 시그니처 내에서 파라미터의 데이터 타입과 반환 값이 올 자리에 제네릭을 사용하는 함수를 정의할 수 있습니다. 이러한 방식으로 작성된 코드는 더 유연해지고 우리 함수를 호출하는 쪽에서 더 많은 기능을 제공할 수 있는 한편, 코드 중복을 야기하지도 않습니다. 우리의 largest 함수로 계속 진행하면, Listing 10-4는 슬라이스 내에서 가장 큰 값을 찾는 동일한 기능을 제공하는 두 함수를 보여주고 있습니다. 첫 번째 함수는 Listing 10-3에서 추출한 슬라이스에서 가장 큰 i32를 찾는 함수입니다. 두 번째 함수는 슬라이스에서 가장 큰 char를 찾습니다: Filename: src/main.rs fn largest_i32(list: &[i32]) -> i32 { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest\n} fn largest_char(list: &[char]) -> char { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest\n} fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest_i32(&numbers); println!(\"The largest number is {}\", result);\n# assert_eq!(result, 100); let chars = vec!['y', 'm', 'a', 'q']; let result = largest_char(&chars); println!(\"The largest char is {}\", result);\n# assert_eq!(result, 'y');\n} Listing 10-4: 이름과 시그니처만 다른 두 함수들 여기서 함수 largest_i32와 largest_char는 정확히 똑같은 본체를 가지고 있으므로, 만일 우리가 이 두 함수를 하나로 바꿔서 중복을 제거할 수 있다면 좋을 것입니다. 운 좋게도, 제네릭 타입 파라미터를 도입해서 그렇게 할 수 있습니다! 우리가 정의하고자 하는 함수의 시그니처 내에 있는 타입들을 파라미터화 하기 위해서, 타입 파라미터를 위한 이름을 만들 필요가 있는데, 이는 값 파라미터들의 이름을 함수에 제공하는 방법과 유사합니다. 우리는 T라는 이름을 선택할 겁니다. 어떤 식별자(identifier)든지 타입 파라미터의 이름으로 사용될 수 있지만, 러스트의 타입 이름에 대한 관례가 낙타 표기법(CamelCase)이기 때문에 T를 사용하려고 합니다. 제네릭 타입 파라미터의 이름은 또한 관례상 짧은 경향이 있는데, 종종 그냥 한 글자로 되어 있습니다. \"type\"을 줄인 것으로서, T가 대부분의 러스트 프로그래머의 기본 선택입니다. 함수의 본체에 파라미터를 이용할 때는, 시그니처 내에 그 파라미터를 선언하여 해당 이름이 함수 본체 내에서 무엇을 의미하는지 컴파일러가 할 수 있도록 합니다. 비슷하게, 함수 시그니처 내에서 타입 파라미터 이름을 사용할 때는, 사용 전에 그 타입 파라미터 이름을 선언해야 합니다. 타입 이름 선언은 함수의 이름과 파라미터 리스트 사이에 꺾쇠괄호를 쓰고 그 안에 넣습니다. 우리가 정의하고자 하는 제네릭 largest 함수의 함수 시그니처는 아래와 같이 생겼습니다: fn largest(list: &[T]) -> T { 이를 다음과 같이 읽습니다: 함수 largest는 어떤 타입 T을 이용한 제네릭입니다. 이것은 list라는 이름을 가진 하나의 파라미터를 가지고 있고, list의 타입은 T 타입 값들의 슬라이스입니다. largest 함수는 동일한 타입 T 값을 반환할 것입니다. Listing 10-5는 함수 시그니처 내에 제네릭 데이터 타입을 이용한 통합된 형태의 largest 함수 정의를 보여주며, 또한 i32 값들의 슬라이스 혹은 char 값들의 슬라이스를 가지고 어떻게 largest를 호출할 수 있을지를 보여줍니다. 이 코드가 아직 컴파일되지 않는다는 점을 주의하세요! Filename: src/main.rs fn largest(list: &[T]) -> T { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest\n} fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest(&numbers); println!(\"The largest number is {}\", result); let chars = vec!['y', 'm', 'a', 'q']; let result = largest(&chars); println!(\"The largest char is {}\", result);\n} Listing 10-5: 제네릭 타입 파라미터를 이용하지만 아직 컴파일되지 않는 largest 함수의 정의 이 코드를 지금 컴파일하고자 시도하면, 다음과 같은 에러를 얻게 될 것입니다: error[E0369]: binary operation `>` cannot be applied to type `T` |\n5 | if item > largest { | ^^^^ |\nnote: an implementation of `std::cmp::PartialOrd` might be missing for `T` 위 노트는 std::cmp::PartialOrd를 언급하는데, 이는 트레잇(trait) 입니다. 트레잇에 대해서는 다음 절에서 살펴볼 것이지만, 간략하게 설명하자면, 이 에러가 말하고 있는 것은 T가 될 수 있는 모든 가능한 타입에 대해서 동작하지 않으리라는 것입니다: 함수 본체 내에서 T 타입의 값을 비교하고자 하기 때문에, 어떻게 순서대로 정렬하는지 알고 있는 타입만 사용할 수 있는 것입니다. 표준 라이브러리는 어떤 타입에 대해 비교 연산이 가능하도록 구현할 수 있는 트레잇인 std::cmp::PartialOrd을 정의해뒀습니다. 다음 절에서 트레잇, 그리고 어떤 제네릭 타입이 특정 트레잇을 갖도록 명시하는 방법을 알아보기 위해 돌아올 것이지만, 이 예제는 잠시 옆으로 치워두고 제네릭 타입 파라미터를 이용할 수 있는 다른 곳을 먼저 돌아봅시다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 제네릭 데이터 타입 » 함수 정의 내에서 제네릭 데이터 타입을 이용하기","id":"165","title":"함수 정의 내에서 제네릭 데이터 타입을 이용하기"},"166":{"body":"우리는 또한 하나 혹은 그 이상의 구조체 필드 내에 제네릭 타입 파라미터를 사용하여 구조체를 정의할 수 있습니다. Listing 10-6은 임의의 타입으로 된 x와 y 좌표값을 가질 수 있는 Point 구조체의 정의 및 사용법을 보여주고 있습니다: Filename: 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} Listing 10-6: T 타입의 값 x와 y를 갖는 Point 구조체 문법은 함수 정의 내에서의 제네릭을 사용하는 것과 유사합니다. 먼저, 구조체 이름 바로 뒤에 꺾쇠괄호를 쓰고 그 안에 타입 파라미터의 이름을 선언해야 합니다. 그러면 구조체 정의부 내에서 구체적인 데이터 타입을 명시하는 곳에 제네릭 타입을 이용할 수 있습니다. Point의 정의 내에서 단 하나의 제네릭 타입을 사용했기 때문에, Point 구조체는 어떤 타입 T를 이용한 제네릭이고 x와 y가 이게 결국 무엇이 되든 간에 둘 다 동일한 타입을 가지고 있다고 말할 수 있음을 주목하세요. 만일 Listing 10-7에서와 같이 다른 타입의 값을 갖는 Point의 인스턴스를 만들고자 한다면, 컴파일이 되지 않을 것입니다: Filename: src/main.rs struct Point { x: T, y: T,\n} fn main() { let wont_work = Point { x: 5, y: 4.0 };\n} Listing 10-7: x와 y 필드는 둘 모두 동일한 제네릭 데이터 타입 T를 가지고 있기 때문에 동일한 타입이어야 합니다 이 코드를 컴파일하고자 하면, 다음과 같은 에러를 얻게 될 것입니다: error[E0308]: mismatched types --> |\n7 | let wont_work = Point { x: 5, y: 4.0 }; | ^^^ expected integral variable, found floating-point variable | = note: expected type `{integer}` = note: found type `{float}` x에 정수 5를 대입할 때, 컴파일러는 이 Point의 인스턴스에 대해 제네릭 타입 T가 정수일 것이고 알게 됩니다. 그다음 y에 대해 4.0을 지정했는데, 이 y는 x와 동일한 타입을 갖도록 정의되었으므로, 타입 불일치 에러를 얻게 됩니다. 만일 x와 y가 서로 다른 타입을 가지지만 해당 타입들이 여전히 제네릭인 Point 구조체를 정의하길 원한다면, 여러 개의 제네릭 타입 파라미터를 이용할 수 있습니다. Listing 10-8에서는 Point의 정의를 T와 U를 이용한 제네릭이 되도록 변경했습니다. 필드 x의 타입은 T이고, 필드 y의 타입은 U입니다: Filename: 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} Listing 10-8: 두 타입을 이용한 제네릭이어서 x와 y가 다른 타입의 값일 수도 있는 Point 이제 위와 같은 모든 Point 인스턴스가 허용됩니다! 정의 부분에 여러분이 원하는 만큼 많은 수의 제네릭 타입 파라미터를 이용할 수 있지만, 몇몇 개보다 더 많이 이용하는 것은 읽고 이해하는 것을 어렵게 만듭니다. 여러분이 많은 수의 제네릭 타입을 필요로 하는 지점에 다다랐다면, 이는 아마도 여러분의 코드가 좀 더 작은 조각들로 나뉘는 재구조화가 필요할지도 모른다는 징조입니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 제네릭 데이터 타입 » 구조체 정의 내에서 제네릭 데이터 타입 사용하기","id":"166","title":"구조체 정의 내에서 제네릭 데이터 타입 사용하기"},"167":{"body":"구조체와 유사하게, 열거형도 그 variant 내에서 제네릭 데이터 타입을 갖도록 정의될 수 있습니다. 6장에서 표준 라이브러리가 제공하는 Option 열거형을 이용해봤는데, 이제는 그 정의를 좀 더 잘 이해할 수 있겠지요. 다시 한번 봅시다: enum Option { Some(T), None,\n} 달리 말하면, Option는 T 타입에 제네릭인 열거형입니다. 이것은 두 개의 variant를 가지고 있습니다: 타입 T 값 하나를 들고 있는 Some, 그리고 어떠한 값도 들고 있지 않는 None variant 입니다. 표준 라이브러리는 구체적인 타입을 가진 이 열거형에 대한 값의 생성을 지원하기 위해서 딱 이 한 가지 정의만 가지고 있으면 됩니다. \"옵션 값\"의 아이디어는 하나의 명시적인 타입에 비해 더 추상화된 개념이고, 러스트는 이 추상화 개념을 수많은 중복 없이 표현할 수 있도록 해줍니다. 열거형은 또한 여러 개의 제네릭 타입을 이용할 수 있습니다. 우리가 9장에서 사용해본 Result 열거형의 정의가 한 가지 예입니다: enum Result { Ok(T), Err(E),\n} Result 열거형은 T와 E, 두 개의 타입을 이용한 제네릭입니다. Result는 두 개의 variant를 가지고 있습니다: 타입 T의 값을 들고 있는 Ok, 그리고 타입 E의 값을 들고 있는 Err입니다. 이 정의는 성공하거나 (그래서 어떤 T 값을 반환하거나) 혹은 실패하는 (그래서 E 타입으로 된 에러를 반환하는) 연산이 필요한 어디에서든 편리하게 Result 열거형을 이용하도록 해줍니다. Listing 9-2에 우리가 파일을 열 때를 상기해보세요: 이 경우, 파일이 성공적으로 열렸을 때는 T에 std::fs::File 타입의 값이 채워지고 파일을 여는데 문제가 생겼을 때는 E에 std::io::Error 타입으로 된 값이 채워졌습니다. 여러분의 코드에서 단지 들고 있는 값의 타입만 다른 여러 개의 구조체나 열거형이 있는 상황을 인지했다면, 우리가 함수 정의에서 제네릭 타입을 대신 도입하여 사용했던 것과 똑같은 절차를 통해 그러한 중복을 제거할 수 있습니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 제네릭 데이터 타입 » 열거형 정의 내에서 제네릭 데이터 타입 사용하기","id":"167","title":"열거형 정의 내에서 제네릭 데이터 타입 사용하기"},"168":{"body":"5장에서 했던 것과 유사하게, 정의부에 제네릭 타입을 갖는 구조체와 열거형 상의 메소드를 구현할 수도 있습니다. Listing 10-9는 우리가 Listing 10-6에서 정의했던 Point 구조체를 보여주고 있습니다. 그러고 나서 필드 x의 값에 대한 참조자를 반환하는 x라는 이름의 메소드를 Point 상에 정의했습니다: Filename: 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} Listing 10-9: T 타입의 x 필드에 대한 참조자를 반환하는 Point 구조체 상에 x라는 이름의 메소드 정의 impl 바로 뒤에 T를 정의해야만 타입 Point 메소드를 구현하는 중에 이를 사용할 수 있음을 주목하세요. 구조체 정의 내에서의 제네릭 타입 파라미터는 여러분이 구조체의 메소드 시그니처 내에서 사용하고 싶어하는 제네릭 타입 파라미터와 항상 같지 않습니다. Listing 10-10에서는 Listing 10-8에서의 Point 구조체 상에 mixup 이라는 메소드를 정의했습니다. 이 메소드는 또다른 Point를 파라미터로 갖는데, 이는 우리가 호출하는 mixup 상의 self의 Point와 다른 타입을 가지고 있을 수도 있습니다. 이 메소드는 새로운 Point를 생성하는데 self Point로부터 (T 타입인) x 값을 가져오고, 파라미터로 넘겨받은 Point로부터 (W 타입인) y 값을 가져온 것입니다: Filename: src/main.rs struct Point { x: T, y: U,\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} Listing 10-10: 구조체 정의에서와는 다른 제네릭 타입을 사용하는 메소드 main에서, 우리는 (5 값을 갖는) x에 대해 i32를, (10.4 값을 갖는) y에 대해 f64를 사용하는 Point를 정의했습니다. p2는 (\"Hello\" 값을 갖는) x에 대해 스트링 슬라이스를, (c 값을 갖는) y에 대해 char를 사용하는 Point입니다. p1상에서 인자로 p2를 넘기는 mixup 호출은 p3을 반환하는데, 이는 x가 p1으로부터 오기 때문에 x는 i32 타입을 갖게 될 것입니다. 또한 y는 p2로부터 오기 때문에 p3은 y에 대해 char 타입을 가지게 될 것입니다. println!은 p3.x = 5, p3.y = c를 출력하겠지요. 제네릭 파라미터 T와 U는 impl 뒤에 선언되었는데, 이는 구조체 정의와 함께 사용되기 때문임을 주목하세요. 제네릭 파라미터 V와 W는 fn mixup 뒤에 선언되었는데, 이는 이들이 오직 해당 메소드에 대해서만 관련이 있기 때문입니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 제네릭 데이터 타입 » 메소드 정의 내에서 제네릭 데이터 타입 사용하기","id":"168","title":"메소드 정의 내에서 제네릭 데이터 타입 사용하기"},"169":{"body":"여러분이 이 절을 읽으면서 제네릭 타입 파라미터를 이용한 런타임 비용이 있는지 궁금해하고 있을런지도 모르겠습니다. 좋은 소식을 알려드리죠: 러스트가 제네릭을 구현한 방식이 의미하는 바는 여러분이 제네릭 파라미터 대신 구체적인 타입을 명시했을 때와 비교해 전혀 느려지지 않을 것이란 점입니다! 러스트는 컴파일 타임에 제네릭을 사용하는 코드에 대해 단형성화(monomorphization) 를 수행함으로써 이러한 성능을 이루어 냈습니다. 단형성화란 제네릭 코드를 실제로 채워질 구체적인 타입으로 된 특정 코드로 바꾸는 과정을 말합니다. 컴파일러가 하는 일은 Listing 10-5에서 우리가 제네릭 함수를 만들 때 수행한 단계들을 반대로 한 것입니다. 컴파일러는 제네릭 코드가 호출되는 모든 곳을 살펴보고 제네릭 코드가 호출될 때 사용된 구체적인 타입에 대한 코드를 생성합니다. 표준 라이브러리의 Option 열거형을 사용하는 예제를 통해 알아봅시다: let integer = Some(5);\nlet float = Some(5.0); 러스트가 이 코드를 컴파일할 때, 단형성화를 수행할 것입니다. 컴파일러는 Option에 넘겨진 값들을 읽고 두 종류의 Option를 가지고 있다는 사실을 알게 됩니다: 하나는 i32이고 나머지 하나는 f64 이지요. 그리하여 컴파일러는 제네릭 정의를 명시적인 것들로 교체함으로써 Option에 대한 제네릭 정의를 Option_i32와 Option_f64로 확장시킬 것입니다. 컴파일러가 생성한 우리의 단형성화된 버전의 코드는 아래와 같이 보이게 되는데, 컴파일러에 의해 생성된 구체화된 정의로 교체된 제네릭 Option이 사용되었습니다: Filename: 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} 우리는 제네릭을 사용하여 중복 없는 코드를 작성할 수 있고, 러스트는 이를 각 인스턴스에 대해 구체적인 타입을 갖는 코드로 컴파일할 것입니다. 이는 우리가 제네릭을 사용하는 데에 어떠한 런타임 비용도 없음을 의미합니다; 코드가 실행될 때, 손으로 각각 특정 정의를 중복시킨 것과 같이 실행될 것입니다. 단형성화의 과정은 러스트의 제네릭이 런타임에 극도로 효율적 이도록 만들어 주는 것입니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 제네릭 데이터 타입 » 제네릭을 이용한 코드의 성능","id":"169","title":"제네릭을 이용한 코드의 성능"},"17":{"body":"rustup을 통해 러스트를 설치한 뒤라면, 최신 버전을 업데이트하는 것은 쉽습니다. 여러분의 쉘에서 다음과 같은 업데이트 스크립트를 실행하세요: $ rustup update 러스트와 rustup을 제거하려면 다음과 같은 설치 제거용 스크립트를 쉘에서 실행하세요: $ rustup self uninstall","breadcrumbs":"시작하기 » 설치하기 » 업데이트 및 설치 제거하기","id":"17","title":"업데이트 및 설치 제거하기"},"170":{"body":"트레잇은 다른 종류의 추상화를 사용할 수 있도록 해줍니다: 이는 타입들이 공통적으로 갖는 동작에 대하여 추상화하도록 해줍니다. 트레잇(trait) 이란 러스트 컴파일러에게 특정한 타입이 갖고 다른 타입들과 함께 공유할 수도 있는 기능에 대해 말해줍니다. 우리가 제네릭 타입 파라미터를 사용하는 상황에서는, 컴파일 타임에 해당 제네릭 타입이 어떤 트레잇을 구현한 타입이어야 함을 명시하여, 그러한 상황에서 우리가 사용하길 원하는 동작을 갖도록 하기 위해 트레잇 바운드(trait bounds) 를 사용할 수 있습니다. 노트: 트레잇 은 다른 언어들에서 '인터페이스(interface)'라고 부르는 기능과 유사하지만, 몇 가지 다른 점이 있습니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 트레잇: 공유 동작을 정의하기 » 트레잇: 공유 동작을 정의하기","id":"170","title":"트레잇: 공유 동작을 정의하기"},"171":{"body":"어떤 타입의 동작은 우리가 해당 타입 상에서 호출할 수 있는 메소드들로 구성되어 있습니다. 만일 우리가 서로 다른 타입에 대해 모두 동일한 메소드를 호출할 수 있다면 이 타입들은 동일한 동작을 공유하는 것입니다. 트레잇의 정의는 어떠한 목적을 달성하기 위해 필요한 동작의 집합을 정의하기 위해 메소드 시그니처들을 함께 묶는 방법입니다. 예를 들면, 다양한 종류와 양의 텍스트를 갖는 여러 가지의 구조체를 가지고 있다고 칩시다: NewsArticle 구조체는 세계의 특정한 곳에서 줄지어 들어오는 뉴스 이야기를 들고 있고, Tweet은 최대 140글자의 콘텐츠와 함께 해당 트윗이 리트윗인지 혹은 다른 트윗에 대한 답변인지와 같은 메타데이터를 가지고 있습니다. 우리는 NewsArticle 혹은 Tweet 인스턴스에 저장되어 있을 데이터에 대한 종합 정리를 보여줄 수 있는 미디어 종합기 라이브러리를 만들고 싶어 합니다. 각각의 구조체들이 가질 필요가 있는 동작은 정리해주기가 되어야 하며, 그래서 각 인스턴스 상에서 summary 메소드를 호출함으로써 해당 정리를 얻어낼 수 있어야 한다는 것입니다. Listing 10-11은 이러한 개념을 표현한 Summarizable 트레잇의 정의를 나타냅니다: Filename: lib.rs pub trait Summarizable { fn summary(&self) -> String;\n} Listing 10-11: summary 메소드에 의해 제공되는 동작으로 구성된 Summarizable 트레잇의 정의 trait 키워드 다음 트레잇의 이름, 위의 경우 Summarizable을 써서 트레잇을 선언했습니다. 중괄호 내에서는 이 트레잇을 구현하는 타입들이 가질 필요가 있는 동작들을 묘사한 메소드 시그니처들을 정의했는데, 위의 경우에는 fn summary(&self) -> String입니다. 메소드 시그니처 뒤에, 중괄호 내의 정의부를 제공하는 대신, 세미콜론을 집어넣었습니다. 그러면 이 트레잇을 구현하는 각 타입은 이 메소드의 본체에 대한 해당 타입 고유의 커스텀 동작을 제공해야 하는데, 컴파일러는 Summarizable 트레잇을 갖는 어떠한 타입이든 그에 대한 메소드 summary를 정확히 동일한 시그니처로 정의되도록 강제할 것입니다. 트레잇은 한 줄 당 하나의 메소드 시그니처와 각 줄의 끝에 세미콜론을 갖도록 함으로써, 본체 내에 여러 개의 메소드를 가질 수 있습니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 트레잇: 공유 동작을 정의하기 » 트레잇 정의하기","id":"171","title":"트레잇 정의하기"},"172":{"body":"Summarizable 트레잇을 정의하였으니, 이제 우리의 미디어 종합기 내에서 이 동작을 갖길 원했던 타입들 상에 이 트레잇을 구현할 수 있습니다. Listing 10-12는 summary의 반환 값을 만들기 위해 헤드라인, 저자, 위치를 사용하는 NewsArticle 구조체 상의 Summarizable 트레잇 구현을 보여줍니다. Tweet 구조체에 대해서는, 트윗 내용이 이미 140자로 제한되어 있음을 가정하고, summary를 정의하는 데 있어 사용자 이름과 해당 트윗의 전체 텍스트를 가지고 오는 선택을 했습니다. Filename: lib.rs # pub trait Summarizable {\n# fn summary(&self) -> String;\n# }\n#\npub struct NewsArticle { pub headline: String, pub location: String, pub author: String, pub content: String,\n} impl Summarizable for NewsArticle { fn summary(&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 Summarizable for Tweet { fn summary(&self) -> String { format!(\"{}: {}\", self.username, self.content) }\n} Listing 10-12: NewsArticle과 Tweet 타입 상에서의 Summarizable 트레잇 구현 어떤 타입 상에서의 트레잇 구현은 트레잇과 관련이 없는 메소드를 구현하는 것과 유사합니다. 다른 점은 impl 뒤에 우리가 구현하고자 하는 트레잇 이름을 넣고, 그다음 for와 우리가 트레잇을 구현하고자 하는 타입의 이름을 쓴다는 것입니다. impl 블록 내에서는 트레잇 정의부가 정의한 바 있는 메소드 시그니처를 집어넣지만, 각 시그니처의 끝에 세미콜론을 집어넣는 대신 중괄호를 넣고 우리가 트레잇의 메소드가 특정한 타입에 대해서 갖기를 원하는 특정한 동작으로 메소드의 본체를 채웁니다. 트레잇을 한번 구현했다면, 트레잇의 일부가 아닌 메소드들을 호출했던 것과 동일한 방식으로 NewsArticle과 Tweet의 인스턴스 상에서 해당 메소드들을 호출할 수 있습니다: let tweet = Tweet { username: String::from(\"horse_ebooks\"), content: String::from(\"of course, as you probably already know, people\"), reply: false, retweet: false,\n}; println!(\"1 new tweet: {}\", tweet.summary()); 이 코드는 1 new tweet: horse_ebooks: of course, as you probably already know, people를 출력할 것입니다. Listing 10-12에서 Summarizable 트레잇과 NewsArticle 및 Tweet 타입을 동일한 lib.rs 내에 정의했기 때문에, 이들이 모두 동일한 스코프 내에 있다는 점을 주목하세요. 만일 이 lib.rs가 aggregator라고 불리는 크레이트에 대한 것이고 누군가가 우리의 크레이트 기능에 더해 그들의 WeatherForecast 구조체에 대하여 Summarizable을 구현하기를 원한다면, 그들의 코드는 Listing 10-13과 같이 이를 구현하기 전에 먼저 Summarizable 트레잇을 그들의 스코프로 가져올 필요가 있습니다: Filename: lib.rs extern crate aggregator; use aggregator::Summarizable; struct WeatherForecast { high_temp: f64, low_temp: f64, chance_of_precipitation: f64,\n} impl Summarizable for WeatherForecast { fn summary(&self) -> String { format!(\"The high will be {}, and the low will be {}. The chance of precipitation is {}%.\", self.high_temp, self.low_temp, self.chance_of_precipitation) }\n} Listing 10-13: 우리의 aggregator 크레이트로부터 다른 크레이트 내의 스코프로 Summarizable 트레잇을 가져오기 이 코드는 또한 Summarizable이 공개 트레잇임을 가정하는데, 이는 Listing 10-11에서 trait 전에 pub 키워드를 집어넣었기 때문입니다. 트레잇 구현과 함께 기억할 한 가지 제한사항이 있습니다: 트레잇 혹은 타입이 우리의 크레이트 내의 것일 경우에만 해당 타입에서의 트레잇을 정의할 수 있습니다. 바꿔 말하면, 외부의 타입에 대한 외부 트레잇을 구현하는 것은 허용되지 않습니다. 예를 들어, Vec에 대한 Display 트레잇은 구현이 불가능한데, Display와 Vec 모두 표준 라이브러리 내에 정의되어 있기 때문입니다. 우리의 aggregator 크레이트 기능의 일부로서 Tweet과 같은 커스텀 타입에 대한 Display와 같은 표준 라이브러리 트레잇을 구현하는 것은 허용됩니다. 또한 우리의 aggregator 크레이트 내에서 Vec에 대한 Summarizable을 구현하는 것도 가능한데, 이는 우리 크레이트 내에 Summarizable이 정의되어 있기 때문입니다. 이러한 제한은 고아 규칙(orphan rule) 이라고 불리는 것의 일부인데, 이는 타입 이론에 흥미가 있다면 찾아볼 수 있습니다. 간단하게 말하면, 부모 타입이 존재하지 않기 때문에 고아 규칙이라고 부릅니다. 이 규칙이 없다면, 두 크레이트는 동일한 타입에 대해 동일한 트레잇을 구현할 수 있게 되고, 이 두 구현체가 충돌을 일으킬 것입니다: 러스트는 어떤 구현을 이용할 것인지 알지 못할 것입니다. 러스트가 고아 규칙을 강제하기 때문에, 다른 사람의 코드는 여러분의 코드를 망가뜨리지 못하고 반대의 경우도 마찬가지입니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 트레잇: 공유 동작을 정의하기 » 특정 타입에 대한 트레잇 구현하기","id":"172","title":"특정 타입에 대한 트레잇 구현하기"},"173":{"body":"종종 모든 타입 상에서의 모든 구현체가 커스텀 동작을 정의하도록 하는 대신, 트레잇의 몇몇 혹은 모든 메소드들에 대한 기본 동작을 갖추는 것이 유용할 수 있습니다. 특정한 타입에 대한 트레잇을 구현할 때, 각 메소드의 기본 동작을 유지하거나 오버라이드(override)하도록 선택할 수 있습니다. Listing 10-14는 우리가 Listing 10-11에서 한 것과 같이 메소드 시그니처를 정의만 하는 선택 대신 Summarizable 트레잇의 summary 메소드에 대한 기본 스트링을 명시하는 선택을 하는 방법을 보여줍니다: Filename: lib.rs pub trait Summarizable { fn summary(&self) -> String { String::from(\"(Read more...)\") }\n} Listing 10-14: summary 메소드의 기본 구현을 포함한 Summarizable 트레잇의 정의 만일 우리가 Listing 10-12에서 한 것과 같은 커스텀 구현을 정의하는 대신 NewsArticle의 인스턴스를 정리하기 위해 이 기본 구현을 사용하고자 한다면, 빈 impl 블록을 명시하면 됩니다: impl Summarizable for NewsArticle {} 비록 NewsArticle에 대한 summary 메소드를 직접 정의하는 선택을 더 이상 하지 않았더라도, summary 메소드가 기본 구현을 갖고 있고 NewsArticle이 Summarizable 트레잇을 구현하도록 명시했기 때문에, 우리는 여전히 newsArticle의 인스턴스 상에서 summary 메소드를 호출할 수 있습니다: 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.\"),\n}; println!(\"New article available! {}\", article.summary()); 위의 코드는 New article available! (Read more...)를 출력합니다. Summarizable 트레잇이 summary 에대한 기본 구현을 갖도록 변경하는 것은 Listing 10-12의 Tweet이나 Listing 10-13의 WeatherForecast 상에서의 Summarizable 구현에 대한 어떤 것도 바꾸도록 요구하지 않습니다: 기본 구현을 오버라이딩 하기 위한 문법은 기본 구현이 없는 트레잇 메소드를 구현하기 위한 문법과 정확히 동일합니다. 기본 구현은 동일한 트레잇 내의 다른 메소드들을 호출하는 것이 허용되어 있는데, 심지어 그 다른 메소드들이 기본 구현을 갖고 있지 않아도 됩니다. 이러한 방식으로, 트레잇은 수많은 유용한 기능을 제공하면서도 다른 구현자들이 해당 트레잇의 작은 일부분만 구현하도록 요구할 수 있습니다. 우리는 Summarizable 트레잇이 구현이 필요한 author_summary 메소드도 갖도록 하여, summary 메소드가 author_summary 메소드를 호출하는 기본 구현을 갖는 형태를 선택할 수도 있습니다: pub trait Summarizable { fn author_summary(&self) -> String; fn summary(&self) -> String { format!(\"(Read more from {}...)\", self.author_summary()) }\n} 이 버전의 Summarizable을 사용하기 위해서는, 어떤 타입에 대한 이 트레잇을 구현할 때 author_summary만 정의하면 됩니다: impl Summarizable for Tweet { fn author_summary(&self) -> String { format!(\"@{}\", self.username) }\n} 일단 author_summary를 정의하면, Tweet 구조체의 인스턴스 상에서 summary를 호출할 수 있으며, summary의 기본 구현이 우리가 제공한 author_summary의 정의부를 호출할 것입니다. let tweet = Tweet { username: String::from(\"horse_ebooks\"), content: String::from(\"of course, as you probably already know, people\"), reply: false, retweet: false,\n}; println!(\"1 new tweet: {}\", tweet.summary()); 위의 코드는 1 new tweet: (Read more from @horse_ebooks...)를 출력할 것입니다. 오버라이딩된 구현으로부터 기본 구현을 호출하는 것은 불가능하다는 점을 기억해주세요.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 트레잇: 공유 동작을 정의하기 » 기본 구현","id":"173","title":"기본 구현"},"174":{"body":"이제 트레잇을 정의하고 어떤 타입들에 대해 이 트레잇을 구현해봤으니, 제네릭 타입 파라미터를 이용하는 트레잇을 사용할 수 있습니다. 우리는 제네릭 타입에 제약을 가하여 이 제네릭 타입이 어떠한 타입이든 되기 보다는, 이 제네릭 타입이 특정한 트레잇을 구현하여 이 타입들이 가지고 있을 필요가 있는 동작을 갖고 있도록 타입들로 제한함을 컴파일러가 확신하도록 할 수 있습니다. 예를 들면, Listing 10-12에서는 NewsArticle과 Tweet 타입에 대하여 Summarizable 트레잇을 구현했습니다. 우리는 파라미터 item 상에서 summary 메소드를 호출하는 함수 notify를 정의할 수 있는데, 이 item은 제네릭 타입 T의 값입니다. 에러없이 item 상에서 summary를 호출하기 위해서는, T에 대한 트레잇 바운드를 사용하여 item이 Summarizable 트레잇을 반드시 구현한 타입이어야 함을 특정할 수 있습니다: pub fn notify(item: T) { println!(\"Breaking news! {}\", item.summary());\n} 트레잇 바운드는 제네릭 타입 파라미터의 선언부와 함께, 꺾쇠 괄호 내에 콜론 뒤에 옵니다. T 상에서의 트레잇 바운드이므로, 우리는 notify를 호출하여 NewsArticle이나 Tweet의 어떠한 인스턴스라도 넘길 수 있습니다. 우리의 aggregator 크레이트를 사용하는 Listing 10-13의 외부 코드도 우리의 notify 함수를 호출하여 WeatherForecast의 인스턴스를 넘길 수 있는데, 이는 WeatherForecast 또한 Summarizable을 구현하였기 때문입니다. String이나 i32 같은 어떠한 다른 타입을 가지고 notify를 호출하는 코드는 컴파일되지 않을 것인데, 그 이유는 그러한 타입들이 Summarizable을 구현하지 않았기 때문입니다. +를 이용하면 하나의 제네릭 타입에 대해 여러 개의 트레잇 바운드를 특정할 수 있습니다. 만일 함수 내에서 타입 T에 대해 summary 메소드 뿐만 아니라 형식화된 출력을 사용하길 원한다면, 트레잇 바운드 T: Summarizable + Display를 이용할 수 있습니다. 이는 T가 Summarizable과 Display 둘다 구현한 어떤 타입이어야 함을 의미합니다. 여러 개의 제네릭 타입 파라미터를 가진 함수들에 대하여, 각 제네릭은 고유의 트레잇 바운드를 가집니다. 함수 이름과 파라미터 리스트 사이의 꺾쇠 괄호 내에 많은 수의 트레잇 바운드 정보를 특정하는 것은 코드를 읽기 힘들게 만들 수 있으므로, 함수 시그니처 뒤에 where 절 뒤로 트레잇 바운드를 옮겨서 특정하도록 해주는 대안 문법이 있습니다. 따라서 아래와 같은 코드 대신: fn some_function(t: T, u: U) -> i32 { where 절을 이용하여 아래와 같이 작성할 수 있습니다: fn some_function(t: T, u: U) -> i32 where T: Display + Clone, U: Clone + Debug\n{ 함수 이름, 파라미터 리스트, 그리고 반환 타입이 서로 가까이 있도록 하여, 이쪽이 덜 어수선하고 이 함수의 시그니처를 많은 트레잇 바운드를 가지고 있지 않은 함수처럼 보이도록 만들어 줍니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 트레잇: 공유 동작을 정의하기 » 트레잇 바운드","id":"174","title":"트레잇 바운드"},"175":{"body":"따라서 여러분이 어떤 제네릭 상에서 어떤 트레잇으로 정의된 동작을 이용하기를 원하는 어떤 경우이든, 여러분은 해당 제네릭 타입 파라미터의 타입내에 트레잇 바운드를 명시할 필요가 있습니다. 이제 우리는 Listing 10-5에서 제네릭 타입 파라미터를 사용하는 largest 함수의 정의를 고칠 수 있습니다! 우리가 그 코드를 치워뒀을 때, 아래와 같은 에러를 봤었지요: error[E0369]: binary operation `>` cannot be applied to type `T` |\n5 | if item > largest { | ^^^^ |\nnote: an implementation of `std::cmp::PartialOrd` might be missing for `T` largest의 본체 내에서 큰 부등호 연산자를 사용하여 타입 T의 두 값을 비교할 수 있길 원했습니다. 이 연산자는 표준 라이브러리 트레잇인 std::cmp::PartialOrd 상에서 기본 메소드로 정의되어 있습니다. 따라서 큰 부등호 연산자를 사용할 수 있도록 하기 위해서는, T에 대한 트레잇 바운드 내에 PartialOrd를 특정하여 largest 함수가 비교 가능한 어떤 타입의 슬라이스에 대해 작동하도록 할 필요가 있습니다. PartialOrd는 프렐루드(prelude)에 포함되어 있기 때문에 따로 스코프 내로 가져올 필요는 없습니다. fn largest(list: &[T]) -> T { 이 코드를 컴파일하면, 다른 에러를 얻게 됩니다: error[E0508]: cannot move out of type `[T]`, a non-copy array --> src/main.rs:4:23 |\n4 | let mut largest = list[0]; | ----------- ^^^^^^^ cannot move out of here | | | hint: to prevent move, use `ref largest` or `ref mut largest` error[E0507]: cannot move out of borrowed content --> src/main.rs:6:9 |\n6 | for &item in list.iter() { | ^---- | || | |hint: to prevent move, use `ref item` or `ref mut item` | cannot move out of borrowed content 이 에러에 대한 열쇠는 cannot move out of type [T], a non-copy array에 있습니다. largest 함수의 제네릭 없는 버전에서, 우리는 고작 가장 큰 i32 혹은 char를 찾는 시도만 했습니다. 4장에서 논의한 바와 같이, 고정된 크기를 갖는 i32와 char와 같은 타입들은 스택에 저장될 수 있으며, 따라서 이 타입들은 Copy 트레잇을 구현하고 있습니다. 우리가 largest 함수를 제네릭으로 바꿨을 때, 이제는 list 파라미터가 Copy 트레잇을 구현하지 않은 타입을 가질 가능성도 생기는데, 이는 곧 list[0]의 값을 largest 변수로 소유권을 옮기지 못할 것이라는 의미입니다. 만약 이 코드를 오직 Copy가 구현된 타입들을 가지고 호출하도록 하는 것만 원한다면, T의 트레잇 바운드에 Copy를 추가할 수 있습니다! Listing 10-15는 largest로 넘겨지는 슬라이스 내의 값의 타입이 i32와 char처럼 PartialOrd 및 Copy 트레잇 모두를 구현했을 때에 한하여 컴파일되는 제네릭 largest 함수의 완전체 코드를 보여줍니다: Filename: src/main.rs use std::cmp::PartialOrd; fn largest(list: &[T]) -> T { let mut largest = list[0]; for &item in list.iter() { if item > largest { largest = item; } } largest\n} fn main() { let numbers = vec![34, 50, 25, 100, 65]; let result = largest(&numbers); println!(\"The largest number is {}\", result); let chars = vec!['y', 'm', 'a', 'q']; let result = largest(&chars); println!(\"The largest char is {}\", result);\n} Listing 10-15: PartialOrd와 Copy 트레잇을 구현한 어떠한 제네릭 타입 상에서 동작하는 largest 함수의 동작 가능한 정의 만일 우리의 largest 함수를 Copy 트레잇을 구현한 타입에 대한 것으로만 제한하길 원치 않는다면, T가 Copy 대신 Clone 트레잇 바운드를 갖도록 명시하여 largest 함수가 소유권을 갖길 원하는 경우 슬라이스의 각 값이 복제되도록 할 수도 있습니다. 그러나 clone 함수를 이용한다는 것은 더 많은 힙 할당을 할 수 있다는 것이고, 힙 할당은 많은 양의 데이터에 대해서 동작할 경우 느릴 수 있습니다. largest를 구현하는 또다는 방법은 함수가 슬라이스 내의 T 값에 대한 참조자를 반환하도록 하는 것입니다. 만약 반환 타입을 T 대신 &T로 바꾸고 함수의 본체가 참조자를 반환하도록 바꾼다면, Clone이나 Copy 트레잇 바운드도 필요치 않으며 어떠한 힙 할당도 하지 않게 될 것입니다. 여러분이 직접 이 대안 해결책을 구현해보세요! 트레잇과 트레잇 바운드는 중복을 제거하기 위하여 제네릭 타입 파라미터를 사용하는 코드를 작성할 수 있도록 해주지만, 여전히 컴파일러에게 해당 제네릭 타입이 어떤 동작을 할 필요가 있는지를 정확히 명시하도록 해줍니다. 컴파일러에게 트레잇 바운드를 제공하기 때문에, 우리 코드와 함께 이용되는 모든 구체적인 타입들이 정확한 동작을 제공하는지를 확인할 수 있습니다. 동적 타입 언어에서는, 어떤 타입에 대해 어떤 메소드를 호출하는 시도를 했는데 해당 타입이 그 메소드를 구현하지 않았다면, 런타임에 에러를 얻게 됩니다. 러스트는 이러한 에러들을 컴파일 타임으로 옮겨서 우리의 코드가 실행 가능하기 전에 그 문제들을 해결하도록 우리를 강제합니다. 이에 더해서, 우리는 런타임에 해당 동작에 대한 검사를 하는 코드를 작성할 필요가 없는데, 우리는 이미 컴파일 타임에 이를 확인했기 때문이며, 이는 제네릭의 유연성을 포기하지 않고도 다른 언어들에 비해 성능을 향상시킵니다. 우리가 심지어 아직 알아채지도 못한 라이프타임(lifetime) 이라 불리는 또다른 종류의 제네릭이 있습니다. 라이프타임은 어떤 타임이 우리가 원하는 동작을 갖도록 확신하는데 도움을 주기 보다는, 참조자들이 우리가 원하는 만큼 오랫동안 유효한지를 확신하도록 도와줍니다. 라이프타임이 어떤 식으로 그렇게 하는지를 배워봅시다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 트레잇: 공유 동작을 정의하기 » 트레잇 바운드를 사용하여 largest 함수 고치기","id":"175","title":"트레잇 바운드를 사용하여 largest 함수 고치기"},"176":{"body":"4장에서 참조자에 대한 이야기를 할 때, 중요한 디테일을 한 가지 남겨두었습니다: 러스트에서 모든 참조자는 라이프타임(lifetime) 을 갖는데, 이는 해당 참조자가 유효한 스코프입니다. 대부분의 경우에서 타입들이 추론되는 것과 마찬가지로, 대부분의 경우에서 라이프타임 또한 암묵적이며 추론됩니다. 여러 가지 타입이 가능하기 때문에 우리가 타입을 명시해야 하는 때와 비슷하게, 참조자의 라이프타임이 몇몇 다른 방식으로 연관될 수 있는 경우들이 있으므로, 러스트는 우리에게 제네릭 라이프타임 파라미터를 이용하여 이 관계들을 명시하길 요구하여 런타임에 실제 참조자가 확실히 유효하도록 확신할 수 있도록 합니다. 네 그렇습니다. 이러한 개념은 다소 흔치 않으며, 여러분들이 다른 프로그래밍 언어에서 사용해온 도구들과는 다른 것입니다. 몇 가지 측면에서, 라이프타임은 러스트의 가장 독특한 기능입니다. 라이프타임은 이 장에서 전체를 다룰 수 없는 큰 주제이므로, 이 장에서는 여러분이 이 개념에 친숙해질 수 있도록 여러분이 라이프타임 문법을 맞닥뜨릴 흔한 경우에 대해 다룰 것입니다. 19장에서는 라이프타임이 할 수 있는 좀 더 상급 정보를 다룰 것입니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 라이프타임을 이용한 참조자 유효화","id":"176","title":"라이프타임을 이용한 참조자 유효화"},"177":{"body":"라이프타임의 주목적은 댕글링 참조자(dangling reference)를 방지하는 것인데, 댕글링 참조자는 프로그램이 우리가 참조하기로 의도한 데이터가 아닌 다른 데이터를 참조하는 원인이 됩니다. Listing 10-16의 프로그램과 같이 외부 스코프와 내부 스코프를 가진 프로그램을 생각해봅니다. 외부 스코프는 r이라는 이름의 변수를 초기값 없이 선언하였고, 내부 스코프는 x라는 이름의 변수를 초기값 5와 함께 선언했습니다. 내부 스코프 내에서, x의 참조자를 r에 대입하도록 시도했습니다. 그 후 내부 스코프는 끝났고, r의 값을 출력하도록 시도했습니다: { let r; { let x = 5; r = &x; } println!(\"r: {}\", r);\n} Listing 10-16: 스코프 밖으로 벗어난 값에 대한 참조자를 사용하는 시도 초기화되지 않은 변수는 사용할 수 없습니다 다음에 나올 몇 가지 예제는 초기값을 주지 않고 변수를 선언하고 있으며, 따라서 해당 변수의 이름이 외부 스코프에 존재하고 있습니다. 이는 러스트가 널(null) 값을 갖지 않는다는 개념과 충돌을 일으키는 것처럼 보일지도 모릅니다. 그러나, 우리가 값을 제공하기 전에 변수를 사용하고자 시도하면, 컴파일 에러가 나올 것입니다. 시도해 보세요! 이 코드를 컴파일하면, 다음과 같은 에러가 나타날 것입니다: error: `x` does not live long enough |\n6 | r = &x; | - borrow occurs here\n7 | } | ^ `x` dropped here while still borrowed\n...\n10 | } | - borrowed value needs to live until here 변수 x는 \"충분히 오래 살지 못한다(does not live long enough)\"고 합니다. 왜 안될까요? x는 7번 라인의 닫는 중괄호 기호에 도달했을 때 내부 스코프가 끝나면서 스코프 밖으로 벗어날 것입니다. 그러나 r은 외부 스코프에 대해 유효합니다; 이쪽의 스코프가 더 크고 우리는 이쪽이 \"더 오래 산다\"라고 말합니다. 만일 러스트가 이 코드를 작동하도록 허용한다면, r은 x가 스코프 밖으로 벗어났을 때 할당이 해제되는 메모리를 참조하게 될 것이고, r을 가지고 시도하려 했던 어떤 것이든 정확히 동작하지 않게 될 것입니다. 그렇다면 러스트는 이 코드가 허용되어서는 안 된다는 것을 어떻게 결정할까요? 빌림 검사기(Borrow checker) 빌림 검사기(borrow checker) 라고 불리는 컴파일러의 부분이 모든 빌림이 유효한지를 결정하기 위해 스코프를 비교합니다. Listing 10-17은 변수들의 라이프타임을 보여주는 주석과 함께 Listing 10-16과 동일한 예제를 보여줍니다: { let r; // -------+-- 'a // | { // | let x = 5; // -+-----+-- 'b r = &x; // | | } // -+ | // | println!(\"r: {}\", r); // | // | // -------+\n} Listing 10-17: 각각 'a과 'b로 명명된 r과 x의 라이프타임에 대한 주석 우리는 r의 라이프타임을 'a라고 명명하였고, x의 라이프타임을 'b라고 명명하였습니다. 보시다시피, 내부의 'b 블록은 외부의 'a 라이프타임 블록에 비해 훨씬 작습니다. 컴파일 타임에서, 러스트는 두 라이프타임의 크기를 비교하고 r이 'a 라이프타임을 가지고 있지만, 'b 라이프타임을 가지고 있는 어떤 오브젝트를 참조하고 있음을 보게 됩니다. 'b 라이프타임이 'a 라이프타임에 비해 작기 때문에 러스트 컴파일러는 이 프로그램을 거부합니다: 참조자의 주체가 참조자만큼 오래 살지 못하고 있으니까요. 댕글링 참조자를 만드는 시도가 없고 에러 없이 컴파일되는 Listing 10-18의 예제를 살펴봅시다: { let x = 5; // -----+-- 'b // | let r = &x; // --+--+-- 'a // | | println!(\"r: {}\", r); // | | // --+ |\n} // -----+ Listing 10-18: 데이터가 참조자에 비해 더 긴 라이프타임을 갖고 있기 때문에 유효한 참조자 여기서 x는 라이프타임 'b를 갖고 있는데, 위의 경우 'a에 비해 더 큽니다. 이는 r이 x를 참고할 수 있음을 의미합니다: 러스트는 r의 참조자가 x가 유효한 동안 언제나 유효할 것이라는 점을 알고 있습니다. 지금까지 참조자의 라이프타임이 구체적인 예제 어디에 나오는지를 보았고 러스트가 어떻게 라이프타임을 분석하여 참조자가 항상 유효하도록 확신시키는지를 논의했으니, 이제 함수의 내용물 내에 있는 파라미터와 반환 값에 대한 제네릭 라이프타임에 대하여 이야기해 봅시다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 라이프타임은 댕글링 참조자를 방지합니다","id":"177","title":"라이프타임은 댕글링 참조자를 방지합니다"},"178":{"body":"두 스트링 슬라이스 중에서 긴 쪽을 반환하는 함수를 작성해 봅시다. 이 함수에 두 개의 스트링 슬라이스를 넘겨서 호출할 수 있기를 원하고, 스트링 슬라이스를 반환하기를 원합니다. Listing 10-19의 코드는 longest 함수를 구현하면 The longest string is abcd를 출력해야 합니다: Filename: 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} Listing 10-19: 두 스트링 슬라이스 중 긴 쪽을 찾기 위해 longest 함수를 호출하는 main 함수 longest 함수가 인자의 소유권을 얻는 것을 원치 않기 때문에 스트링 슬라이스들을 (4장에서 이야기했던 것처럼 이들은 참조자입니다) 파라미터로서 갖는 함수를 원한다는 점을 주목하세요. 우리는 함수가 String의 슬라이스 (이는 변수 string1의 타입입니다)는 물론 스트링 리터럴 (이는 변수 string2가 담고 있는 것이지요) 또한 받아들일 수 있기를 원하고 있습니다. 왜 이들이 우리가 원하는 인자 들인 지에 대한 더 많은 논의에 대해서는 4장의 \"인자로서의 스트링 슬라이스\"를 참조하세요. 만일 Listing 10-20에서 보는 바와 같이 longest 함수를 구현하는 시도를 한다면, 이는 컴파일되지 않을 것입니다: Filename: src/main.rs fn longest(x: &str, y: &str) -> &str { if x.len() > y.len() { x } else { y }\n} Listing 10-20: 두 스트링 슬라이스 중 긴 쪽을 반환하는 longest 함수의 구현체, 그러나 아직 컴파일되지 않음 대신 우리는 라이프타임에 대해 이야기하는 다음과 같은 에러를 얻습니다: error[E0106]: missing lifetime specifier |\n1 | fn longest(x: &str, y: &str) -> &str { | ^ expected 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` 이 도움말은 반환 타입에 대하여 제네릭 라이프타임 파라미터가 필요하다는 것을 말해주고 있는데, 왜냐하면 반환되는 참조자가 x를 참조하는지 혹은 y를 참조하는지를 러스트가 말할 수 없기 때문입니다. 사실, 우리 또한 모르는데, 이 함수의 본체 내의 if 블록은 x의 참조자를 반환하고 else 블록은 y의 참조자를 반환하기 때문입니다! 우리가 이 함수를 정의하고 있는 시점에서, 우리는 이 함수에 넘겨지게 될 구체적인 값을 모르므로, if 케이스가 실행될지 혹은 else 케이스가 실행될지는 알 수 없습니다. 또한 함수에 넘겨지게 될 참조자의 구체적인 라이프타임을 알지 못하므로, 우리가 반환하는 참조자가 항상 유효한지를 결정하기 위해서 Listing 10-17과 10-18에서 했던 것과 같이 스코프를 살펴볼 수도 없습니다. 빌림 검사기 또한 이를 결정할 수 없는데, 그 이유는 x와 y의 라이프타임이 반환 값의 라이프타임과 어떻게 연관되어 있는지 알지 못하기 때문입니다. 우리는 참조자들 간의 관계를 정의하는 제네릭 라이프타임 파라미터를 추가하여 빌림 검사기가 분석을 수행할 수 있도록 할 것입니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 함수에서의 제네릭 라이프타임","id":"178","title":"함수에서의 제네릭 라이프타임"},"179":{"body":"라이프타임 명시는 연관된 참조자가 얼마나 오랫동안 살게 되는지를 바꾸지는 않습니다. 함수의 시그니처가 제네릭 타입 파라미터를 특정할 때 이 함수가 어떠한 타입이든 허용할 수 있는 것과 같은 방식으로, 함수의 시그니처가 제네릭 라이프타임 파라미터를 특정할 때라면 이 함수는 어떠한 라이프타임을 가진 참조자라도 허용할 수 있습니다. 라이프타임 명시가 하는 것은 여러 개의 참조자에 대한 라이프타임들을 서로 연관 짓도록 하는 것입니다. 라이프타임 명시는 약간 독특한 문법을 갖고 있습니다: 라이프타임 파라미터의 이름은 어퍼스트로피 '로 시작해야 합니다. 라이프타임 파라미터의 이름은 보통 모두 소문자이며, 제네릭 타입과 비슷하게 그들의 이름은 보통 매우 짧습니다. 'a는 대부분의 사람들이 기본적으로 사용하는 이름입니다. 라이프타임 파라미터 명시는 참조자의 & 뒤에 오며, 공백 문자가 라이프타임 명시와 참조자의 타입을 구분해줍니다. 여기 몇 가지 예제가 있습니다: 라이프타임 파라미터가 없는 i32에 대한 참조자, 'a라고 명명된 라이프타임 파라미터를 가지고 있는 i32에 대한 참조자, 그리고 역시 라이프타임 'a를 갖고 있는 i32에 대한 가변 참조자입니다: &i32 // a reference\n&'a i32 // a reference with an explicit lifetime\n&'a mut i32 // a mutable reference with an explicit lifetime 스스로에 대한 하나의 라이프타임 명시는 큰 의미를 가지고 있지 않습니다: 라이프타임 명시는 러스트에게 여러 개의 참조자에 대한 제네릭 라이프타임 파라미터가 서로 어떻게 연관되는지를 말해줍니다. 만일 라이프타임 'a를 가지고 있는 i32에 대한 참조자인 first를 파라미터로, 그리고 또한 라이프타임 'a를 가지고 있는 i32에 대한 또 다른 참조자인 second를 또 다른 파라미터로 가진 함수가 있다면, 이 두 개의 같은 이름을 가진 라이프타임 명시는 참조자 first와 second가 돌다 동일한 제네릭 라이프타임만큼 살아야 한다는 것을 가리킵니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 라이프타임 명시 문법","id":"179","title":"라이프타임 명시 문법"},"18":{"body":"러스트가 올바르게 설치되었는지를 확인하기 위해서는, 쉘을 열고 다음 라인을 입력하세요: $ rustc --version 버전 번호, 커밋 해쉬, 그리고 배포된 최신 안정 버전에 대한 커밋 일자가 다음과 같은 형식으로 보여야 합니다: rustc x.y.z (abcabcabc yyyy-mm-dd) 이 정보가 보인다면, 여러분은 러스트를 성공적으로 설치한 것입니다! 만일 이 정보가 보이지 않고 Windows를 이용중이라면, %PATH% 시스템 변수 내에 러스트가 있는지 확인해주세요. 만일 이 설정이 모두 정확하고 러스트가 여전히 동작하지 않는다면, 여러분이 도움을 구할 수 있는 몇 군데의 장소가 있습니다. 가장 쉬운 방법은 irc.mozilla.org 안에 있는 #rust IRC 채널 인데, 이는 Mibbit 을 통해 접속할 수 있습니다. 이 주소에서 여러분을 도와줄 수 있는 다른 러스티시안(Rustacean, 우리가 스스로를 부르는 우스운 별명입니다)들과 채팅을 할 수 있습니다. 다른 훌륭한 리소스들에는 유저 포럼 과 Stack Overflow 가 있습니다.","breadcrumbs":"시작하기 » 설치하기 » 문제 해결하기","id":"18","title":"문제 해결하기"},"180":{"body":"우리가 작업하고 있던 longest 함수의 내용 중에서 라이프타임 명시 부분을 살펴봅시다. 제네릭 타입 파라미터와 마찬가지로, 제네릭 라이프타임 파라미터도 함수 이름과 파라미터 리스트 사이에 꺾쇠괄호를 쓰고 그 안에 정의가 되어야 합니다. 우리가 파라미터들과 반환 값에서의 참조자들에 대해 러스트에게 말해주고 싶은 제약사항은 그들이 모두 동일한 라이프타임을 갖고 있어야 한다는 것인데, 이는 Listing 10-21에서 보는 바와 같이 우리가 'a라고 명명하여 각각의 참조자에 추가할 것입니다: Filename: src/main.rs fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y }\n} Listing 10-21: 시그니처 내의 모든 참조자들이 동일한 라이프타임 'a를 가지고 있어야 함을 특정한 longest 함수 정의 이는 컴파일될 것이고 Listing 10-19에 있는 main 함수에서 사용되었을 때 우리가 원하는 결과를 만들어줄 것입니다. 이 함수 시그니처는 이제 어떤 라이프타임 'a에 대하여, 이 함수는 두 개의 파라미터를 갖게 될 것인데, 두 개 모두 적어도 라이프타임 'a만큼 살아있는 스트링 슬라이스임을 말해줍니다. 이 함수는 또한 적어도 라이프타임 'a만큼 살아있는 스트링 슬라이스를 반환할 것입니다. 이는 러스트에게 우리가 강제하고 싶은 것을 말해주는 계약입니다. 이 함수 시그니처 내에 라이프타임 파라미터를 특정함으로써, 우리는 함수에 넘겨지거나 반환되는 어떠한 값들의 라이프타임도 바꾸지 않지만, 이 계약에 부합하지 않는 어떠한 값들도 빌림 검사기에 의해 거부되어야 함을 말해주는 것입니다. 이 함수는 x와 y가 정확히 얼마나 오래 살게 될지 알지 못하지만 (혹은 알 필요가 없지만), 다만 이 시그니처를 만족시킬 'a에 대입될 수 있는 어떤 스코프가 있음을 알아야 할 필요가 있을 뿐입니다. 함수 안에 라이프타임을 명시할 때, 이 명시는 함수 시그니처에 붙어 있으며, 함수의 본체 내에의 어떠한 코드에도 붙어있지 않습니다. 이는 러스트가 다른 도움 없이 함수 내의 코드를 분석할 수 있지만, 함수가 그 함수 밖의 코드에서의 참조자를 가지고 있을 때, 인자들 혹은 반환 값들의 라이프타임이 함수가 호출될 때마다 달라질 가능성이 있기 때문입니다. 이는 러스트가 발견해내기에는 너무나 비용이 크고 종종 불가능할 것입니다. 이 경우, 우리는 스스로 라이프타임을 명시할 필요가 있습니다. 구체적인 참조자들이 longest로 넘겨질 때, 'a에 대입되게 되는 구체적인 라이프타임은 y의 스코프와 겹치는 x 스코프의 부분입니다. 스코프는 언제나 중첩되기 때문에, 이것이 제네릭 라이프타임 'a이다라고 말하는 또 다른 방법은 x와 y의 라이프타임 중에서 더 작은 쪽과 동일한 구체적인 라이프타임을 구하는 것일 겁니다. 반환되는 참조자에 대해서도 같은 라이프타임 파라미터인 'a를 명시했으므로, 반환되는 참조자도 x 와 y의 라이프타임 중 짧은 쪽만큼은 길게 유효함을 보장할 것입니다. 서로 다른 구체적인 라이프타임을 가진 참조자들을 넘김으로써 이것이 longest 함수의 사용을 어떻게 제한하는지 봅시다. Listing 10-22는 아무 언어에서나 여러분의 직관에 부합될 간단한 예제입니다: string1은 외부 스코프가 끝날 때까지 유효하고 string2는 내부 스코프가 끝날 때까지 유효하며, result는 내부 스코프가 끝날 때까지 유효한 무언가를 참조합니다. 빌림 검사기는 이 코드를 승인합니다; 이는 컴파일되며 실행했을 때 The longest string is long string is long를 출력합니다: Filename: src/main.rs # 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# }\n#\nfn 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} Listing 10-22: 서로 다른 구체적인 라이프타임을 가진 String 값의 참조자들을 이용한 longest 함수의 사용 다음으로, result의 참조자의 라이프타임이 두 인자들의 라이프타임보다 작아야 함을 보여줄 예제를 시도해봅시다. 우리는 result의 선언부를 내부 스코프 밖으로 옮길 것이지만, result 변수에 대한 값의 대입은 string2가 있는 스코프 내에 남겨둘 것입니다. 다음으로, result를 이용하는 println! 구문을 내부 스코프 바깥에, 내부 스코프가 끝나는 시점으로 옮기겠습니다. 이렇게 수정한 Listing 10-23의 코드는 컴파일되지 않을 것입니다: Filename: 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} Listing 10-23: string2가 스코프 밖으로 벗어난 후에 result를 사용하고자 하는 시도는 컴파일되지 않습니다 만일 이를 컴파일하고자 시도하면, 다음과 같은 에러를 얻습니다: error: `string2` does not live long enough |\n6 | result = longest(string1.as_str(), string2.as_str()); | ------- borrow occurs here\n7 | } | ^ `string2` dropped here while still borrowed\n8 | println!(\"The longest string is {}\", result);\n9 | } | - borrowed value needs to live until here 이 에러는 result가 println!에서 유효하기 위해서는. string2가 외부 스코프의 끝까지 유효할 필요가 있음을 말해줍니다. 러스트는 이를 알고 있는데, 그 이유는 우리가 함수의 파라미터들과 반환 값에 대해 동일한 라이프타임 파라미터 'a를 명시했기 때문입니다. 우리는 인간으로서 이 코드를 살펴볼 수 있고 string1이 더 길기 때문에 result는 string1의 참조자를 담게 될 것이라는 점을 알 수 있습니다. string1이 스코프 밖으로 아직 벗어나지 않았기 때문에, string1의 참조자는 println! 구문에서 여전히 유효할 것입니다. 그렇지만, 우리가 러스트에게 라이프타임 파라미터를 가지고 말해준 것은 longest 함수에 의해 반환되는 참조자의 라이프타임이 인자로 넘겨준 라이프타임들 중 작은 쪽과 동일하다는 것이었지요. 따라서, 빌림 검사기는 잠재적으로 유효하지 않은 참조자를 가질 수 있는 문제로 인해 Listing 10-23의 코드를 허용하지 않습니다. longest 함수에 넘겨질 참조자들의 값과 라이프타임들, 그리고 반환된 참조자를 어떻게 이용하는지를 다양화하여 더 많은 실험들을 디자인해 시도해보세요. 컴파일하기 전에 여러분의 실험이 빌림 검사기를 통과할지 안 할지에 대한 가설을 세워보고, 여러분이 맞았는지 확인해보세요!","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 함수 시그니처 내의 라이프타임 명시","id":"180","title":"함수 시그니처 내의 라이프타임 명시"},"181":{"body":"라이프타임 파라미터를 특정하는 정확한 방법은 여러분의 함수가 어떤 일을 하고 있는가에 따라 달린 문제입니다. 예를 들면, longest 함수의 구현을 제일 긴 스트링 슬라이스 대신 항상 첫 번째 인자를 반환하도록 바꾸었다면, y 파라미터에 대한 라이프타임을 특정할 필요는 없을 것입니다. 아래 코드는 컴파일됩니다: Filename: src/main.rs fn longest<'a>(x: &'a str, y: &str) -> &'a str { x\n} 이 예제에서, 파라미터 x와 반환 값에 대한 라이프타임 파라미터 'a는 특정하였지만, 파라미터 y는 특정하지 않았는데, 그 이유는 y의 라이프타임이 x 혹은 반환 값의 라이프타임과 어떠한 관련도 없기 때문입니다. 함수로부터 참조자를 반환할 때, 반환 타입에 대한 라이프타임 파라미터는 인자 중 하나의 라이프타임 파라미터와 일치할 필요가 있습니다. 만일 반환되는 참조가 인자들 중 하나를 참조하지 않는다면 , 다른 유일한 가능성은 이 함수 내에서 생성된 값을 참조하는 경우인데, 이 값은 함수가 끝나는 시점에서 스코프 밖으로 벗어나기 때문에 댕글링 참조자가 될 것입니다. longest 함수에 대한 아래와 같은 구현 시도는 컴파일되지 않습니다: Filename: src/main.rs fn longest<'a>(x: &str, y: &str) -> &'a str { let result = String::from(\"really long string\"); result.as_str()\n} 우리가 반환 타입에 대해 라이프타임 파라미터 'a를 특정했을지라도, 이러한 구현은 컴파일에 실패하게 되는데 이는 반환되는 값의 라이프타임이 파라미터의 라이프타임과 아무런 관련이 없기 때문입니다. 여기 우리가 얻게 되는 에러 메시지를 보시죠: error: `result` does not live long enough |\n3 | result.as_str() | ^^^^^^ does not live long enough\n4 | } | - borrowed value only lives until here |\nnote: borrowed value must be valid for the lifetime 'a as defined on the block\nat 1:44... |\n1 | fn longest<'a>(x: &str, y: &str) -> &'a str { | ^ 문제는 result가 longest 함수가 끝나는 지점에서 스코프 밖으로 벗어나게 되어 메모리 해제가 일어나게 되는데, 이 함수로부터 result의 참조자를 반환하려는 시도를 한다는 점입니다. 이 댕글링 참조자를 변경시킬 라이프타임 파라미터를 특정할 방법은 없으며, 러스트는 우리가 댕글링 참조자를 만들게끔 놔두지 않습니다. 이 경우, 가장 좋은 수정 방법은 참조자보다는 차라리 값을 소유한 데이터 타입을 리턴하도록 하여 호출하는 함수가 값을 할당 해제하도록 하는 것입니다. 궁극적으로, 라이프타임 문법은 함수들의 다양한 인자들과 반환 값 사이를 연결하는 것에 대한 것입니다. 이들이 일단 연결되고 나면, 러스트는 메모리에 안전한 연산들을 허용하고 댕글링 포인터를 생성하거나 그렇지 않은 경우 메모리 안전을 위배하게 될 연산들을 배제하기에 충분한 정보를 갖게 됩니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 라이프타임의 측면에서 생각하기","id":"181","title":"라이프타임의 측면에서 생각하기"},"182":{"body":"현재까지 우리는 소유권 있는 타입만 들고 있는 구조체들만 정의해왔습니다. 구조체가 참조자를 들고 있도록 할 수 있지만, 구조체 정의 내의 모든 참조자들에 대하여 라이프타임을 표시할 필요가 있습니다. Listing 10-24에 스트링 슬라이스를 들고 있는 ImportantExcerpt라고 명명된 구조체가 있습니다: Filename: 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} Listing 10-24: 참조자를 들고 있는 구조체, 따라서 정의 부분에 라이프타임 명시가 필요합니다 이 구조체는 스트링 슬라이스를 담을 수 있는 part라는 하나의 필드를 갖고 있는데, 이것이 참조자입니다. 제네릭 데이터 타입과 마찬가지로, 제네릭 라이프타임 파라미터의 이름을 구조체의 이름 뒤편에 꺾쇠괄호 안에다 선언하여 구조체 정의의 본체 내에서 이 라이프타임 파라미터를 이용할 수 있도록 해야 합니다. 여기 이 main 함수는 변수 novel이 소유하고 있는 String의 첫 문장에 대한 참조자를 들고 있는 ImportantExcerpt 구조체의 인스턴스를 생성합니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 구조체 정의 상에서의 라이프타임 명시","id":"182","title":"구조체 정의 상에서의 라이프타임 명시"},"183":{"body":"이 절에서, 우리는 모든 참조자가 라이프타임을 가지고 있으며, 참조자를 사용하는 함수나 구조체에 대하여 라이프타임 파라미터를 특정할 필요가 있다고 배웠습니다. 하지만, Listing 10-25에서 다시 보여주듯이, 4장의 \"스트링 슬라이스\"절의 함수는 라이프타임 명시 없이도 컴파일이 됐었지요: Filename: 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} Listing 10-25: 파라미터와 반환 값의 타입이 참조자임에도 불구하고 라이프타임 명시 없이 컴파일되었던, 4장에서 정의한 바 있는 함수 이 함수가 라이프타임 없이 컴파일되는 이유는 역사가 있습니다: 1.0 이전 시절의 러스트에서는 이 코드가 실제로 컴파일되지 않았습니다. 모든 참조자들은 명시적인 라이프타임이 필요했지요. 그 시절, 함수 시그니처는 아래와 같이 작성되었습니다: fn first_word<'a>(s: &'a str) -> &'a str { 수많은 러스트 코드를 작성하고 난 후, 러스트 팀은 러스트 프로그래머들이 특정한 상황에서 똑같은 라이프타임 명시를 계속하여 타이핑하고 있다는 사실을 발견하게 되었습니다. 이 상황들은 예측 가능하며 몇 가지 결정론적인 패턴을 따르고 있었습니다. 그리하여 러스트 팀은 러스트 컴파일러 코드 내에 이 패턴들을 프로그래밍하여 이러한 상황 내에서는 프로그래머가 명시적으로 라이프타임 명시를 추가하도록 강제하지 않고 빌림 검사기가 라이프타임을 추론할 수 있도록 하였습니다. 더 많은 결정론적인 패턴들이 출현하여 컴파일러 내에 추가될 가능성이 충분하기에 이러한 러스트의 역사에 대해 언급하였습니다. 나중에는 더욱 적은 라이프타임 명시만이 필요할지도 모르지요. 참조자에 대한 러스트의 분석 기능 내에 프로그래밍된 패턴들을 일컬어 라이프타임 생략 규칙(lifetime elision rules) 이라고 합니다. 이들은 프로그래머가 따라야 하는 규칙들이 아닙니다; 이 규칙들은 컴파일러가 고려할 특정한 경우의 집합이고, 여러분의 코드가 이러한 경우에 들어맞으면, 여러분은 명시적으로 라이프타임을 작성할 필요가 없어집니다. 생략 규칙들은 모든 추론을 제공하지는 않습니다: 만일 러스트가 결정론적으로 이 규칙들을 적용했지만 여전히 참조자들이 어떤 라이프타임을 가지고 있는지에 대하여 모호하다면, 해당하는 남은 참조자들의 라이프타임이 어떻게 되어야 하는지에 대해 추측하지 않을 것입니다. 이러한 경우, 컴파일러는 여러분에게 이 참조자들이 서로 어떻게 연관되는지에 대하여 여러분의 의도에 맞게끔 라이프타임을 추가함으로써 해결 가능한 에러를 표시할 것입니다. 먼저 몇 가지 정의들을 봅시다: 함수나 메소드의 파라미터에 대한 라이프타임을 입력 라이프타임(input lifetime) 이라고 하며, 반환 값에 대한 라이프타임을 출력 라이프타임(output lifetime) 이라고 합니다. 이제 명시적인 라이프타임이 없을 때 참조자가 어떤 라이프타임을 가져야 하는지 알아내기 위해서 컴파일러가 사용하는 규칙들을 봅시다. 첫 번째 규칙은 입력 라이프타임에 적용되고, 다음의 두 규칙들은 출력 라이프타임에 적용됩니다. 만일 컴파일러가 이 세 가지 규칙의 끝에 도달하고 여전히 라이프타임을 알아낼 수 없는 참조자가 있다면, 컴파일러는 에러와 함께 멈출 것입니다. 참조자인 각각의 파라미터는 고유한 라이프타임 파라미터를 갖습니다. 바꿔 말하면, 하나의 파라미터를 갖는 함수는 하나의 라이프타임 파라미터를 갖고: 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의 라이프타임이 모든 출력 라이프타임 파라미터에 대입됩니다. 이는 메소드의 작성을 더욱 멋지게 만들어줍니다. 우리가 직접 컴파일러가 된 척하여 Listing 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 { 이제 이 함수 시그니처의 모든 참조자들이 라이프타임을 갖게 되었고, 컴파일러는 프로그래머에게 이 함수 시그니처 내의 라이프타임을 명시하도록 요구하지 않고도 분석을 계속할 수 있게 되었습니다. 또 다른 예제를 해보려는데, 이번에는 Listing 10-20에서와 같이 우리가 처음 시작할 때의 아무런 라이프타임 파라미터도 가지고 있지 않은 longest 함수를 가지고 해 봅시다: fn longest(x: &str, y: &str) -> &str { 다시 한번 우리가 컴파일러가 된 척하여, 첫 번째 규칙을 적용해봅시다: 각각의 파라미터는 고유의 라이프타임을 갖습니다. 이번에는 두 개의 파라미터들이 있으므로, 두 개의 라이프타임을 갖게 됩니다: fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { 두 번째 규칙을 살펴봤을 때, 하나 이상의 입력 라이프타임이 있으므로 적용되지 않습니다. 세번째 규칙을 살펴봤을 때, 이 또한 적용되지 않는데 이는 이것이 메소드가 아니라 함수이고, 따라서 어떠한 파라미터도 self가 아니기 때문입니다. 따라서 규칙이 더 이상 남아있지 않은데, 우리는 아직 반환 타입의 라이프타임이 무엇인지 알아내지 못했습니다. 이것이 바로 Listing 10-20의 코드를 컴파일하려 시도했을 때 에러가 발생한 이유입니다: 컴파일러는 자신이 알고 있는 라이프타임 생략 규칙들을 통해 작업을 수행했지만, 여전히 이 시그니처의 참조자들에 대한 모든 라이프타임을 알아낼 수 없으니까요. 세번째 규칙이 오직 메소드 시그니처에 대해서만 실제로 적용되므로, 이제 그러한 경우에서의 라이프타임을 살펴보고, 어째서 세번서 규칙이 메소드 시그니처의 라이프타임을 매우 흔하게 생략해도 된다는 것을 의미하는지 알아봅시다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 라이프타임 생략","id":"183","title":"라이프타임 생략"},"184":{"body":"라이프타임을 가진 구조체에 대한 메소드를 구현할 때, 문법은 또다시 Listing 10-10에서 보신 바와 같이 제네릭 타입 파라미터의 그것과 같습니다: 라이프타임 파라미터가 선언되고 사용되는 곳은 라이프타임 파라미터가 구조체의 필드들 혹은 메소드 인자와 반환 값과 연관이 있는지 없는지에 따라 달린 문제입니다. 구조체 필드를 위한 라이프타임 이름은 언제나 impl 키워드 뒤에 선언되어야 하며, 그러고 나서 구조체의 이름 뒤에 사용되어야 하는데, 이 라이프타임들은 구조체 타입의 일부이기 때문입니다. impl 블록 안에 있는 메소드 시그니처에서, 참조자들이 구조체 필드에 있는 참조자들의 라이프타임과 묶일 수도 있고, 혹은 서로 독립적일 수도 있습니다. 여기에 더해, 라이프타임 생략 규칙이 종종 적용되어 메소드 시그니처 내에 라이프타임 명시를 할 필요가 없습니다. Listing 10-24에서 정의했던 ImportantExcerpt라는 이름의 구조체를 이용한 몇 가지 예제를 봅시다. 먼저, 여기 level라는 이름의 메소드가 있습니다. 파라미터는 오직 self에 대한 참조자이며, 반환 값은 무언가에 대한 참조자가 아닌, 그냥 i32입니다: # struct ImportantExcerpt<'a> {\n# part: &'a str,\n# }\n#\nimpl<'a> ImportantExcerpt<'a> { fn level(&self) -> i32 { 3 }\n} impl뒤의 라이프타임 파라미터 선언부와 타입 이름 뒤에서 이를 사용하는 것이 필요하지만, 첫 번째 생략 규칙때문에 self로의 참조자의 라이프타임을 명시할 필요는 없습니다. 아래는 세번째 라이프타임 생략 규칙이 적용되는 예제입니다: # struct ImportantExcerpt<'a> {\n# part: &'a str,\n# }\n#\nimpl<'a> ImportantExcerpt<'a> { fn announce_and_return_part(&self, announcement: &str) -> &str { println!(\"Attention please: {}\", announcement); self.part }\n} 두 개의 입력 라이프타임이 있으므로, 러스트는 첫 번째 라이프타임 생략 규칙을 적용하여 &self와 announcement에게 각각 라이프타임을 부여합니다. 그다음, 파라미터 중 하나가 &self이므로, 반환 타입은 &self의 라이프타임을 얻고, 모든 라이프타임들이 추론되었습니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 메소드 정의 내에서의 라이프타임 명시","id":"184","title":"메소드 정의 내에서의 라이프타임 명시"},"185":{"body":"우리가 논의할 필요가 있는 특별한 라이프타임이 딱 하나 있습니다: 바로 'static입니다. 'static 라이프타임은 프로그램의 전체 생애주기를 가리킵니다. 모든 스트링 리터럴은 'static 라이프타임을 가지고 있는데, 아래와 같이 명시하는 쪽을 선택할 수 있습니다: let s: &'static str = \"I have a static lifetime.\"; 이 스트링의 텍스트는 여러분의 프로그램의 바이너리 내에 직접 저장되며 여러분 프로그램의 바이너리는 항상 이용이 가능하지요. 따라서, 모든 스트링 리터럴의 라이프타임은 'static입니다. 여러분은 어쩌면 에러 메시지 도움말에서 'static 라이프타임을 이용하라는 제안을 보셨을지도 모릅니다만, 참조자의 라이프타임으로서 'static으로 특정하기 전에, 여러분이 가지고 있는 참조자가 실제로 여러분 프로그램의 전체 라이프타임 동안 사는 것인지 대해 생각해보세요 (혹은 가능하다면 그렇게 오래 살게끔 하고 싶어 할지라도 말이죠). 대부분의 경우, 코드 내의 문제는 댕글링 참조자를 만드는 시도 혹은 사용 가능한 라이프타임들의 불일치이며, 해결책은 이 문제들을 해결하는 것이지 'static 라이프타임으로 특정하는 것이 아닙니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 정적 라이프타임(Static lifetime)","id":"185","title":"정적 라이프타임(Static lifetime)"},"186":{"body":"그럼 제네릭 타입 파라미터, 트레잇 바운드, 그리고 라이프타임이 하나의 함수에 모두 특정된 문법을 간단하게 살펴봅시다! use std::fmt::Display; fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str where T: Display\n{ println!(\"Announcement! {}\", ann); if x.len() > y.len() { x } else { y }\n} 이것은 Listing 10-21에 나온 바 있는 두 스트링 슬라이스 중 긴 쪽을 반환하는 longest 함수지만, ann이라는 이름의 추가 인자를 가지고 있습니다. ann의 타입은 제네릭 타입 T인데, where 절을 가지고 특정한 바와 같이 Display 트레잇을 구현한 어떤 타입으로도 채워질 수 있습니다. 이 추가 인자는 함수가 스트링 슬라이스들의 길이를 비교하기 전 출력될 것인데, 이것이 Display 트레잇 바운드가 필요한 이유지요. 라이프타임이 제네릭의 한 종류이므로, 라이프타임 파라미터 'a와 제네릭 타입 파라미터 T 둘 모두에 대한 선언이 함수 이름 뒤 꺾쇠괄호 내에 나열되어 있습니다.","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 제네릭 타입 파라미터, 트레잇 바운드, 라이프타임을 함께 써보기","id":"186","title":"제네릭 타입 파라미터, 트레잇 바운드, 라이프타임을 함께 써보기"},"187":{"body":"이번 절에서 참 많은 것을 다루었습니다! 이제 여러분은 제네릭 타입 파라미터, 트레잇과 트레잇 바운드, 그리고 제네릭 라이프타임 파라미터에 대해 알게되었으니, 여러분은 중복되지 않지만 많은 서로 다른 상황들에서 사용 가능한 코드를 작성할 준비가 되었습니다. 제네릭 타입 파라미터는 코드가 서로 다른 타입에 대해서 적용될 수 있음을 의미합니다. 트레잇과 트레잇 바운드는 그 타입이 제네릭일지라도 해당 타입들이 코드에 필요한 동작을 할 수 있음을 보장합니다. 라이프타임 명시에 의해 특정된 참조자들의 라이프타임 간의 관계는 이 유연한 코드가 어떠한 댕글링 참조자도 만들지 않을 것임을 확신시켜줍니다. 그리고 이 모든 것들이 컴파일 타임에 이루어지므로 런타임 성능에는 영향을 주지 않지요! 믿을진 모르겠지만, 이 부분에 대해 배울 것이 심지어 더 있습니다: 17장에서는 트레잇 객체(trait object)에 대해 다룰 예정인데, 이는 트레잇을 사용하는 또 다른 방법입니다. 19장에서는 라이프타임 명시를 포함하는 더 복잡한 시나리오를 다룰 것입니다. 20장에서는 더 고급 수준의 타입 시스템 특성을 다룰 것입니다. 하지만, 다음 절에서는 러스트에서 어떻게 테스트를 작성하여 우리의 코드가 우리가 원했던 방식대로 모든 기능들을 작동시킨다는 것을 확신할 수 있도록 하는 방법에 대해 이야기해봅시다!","breadcrumbs":"제네릭 타입, 트레잇, 그리고 라이프타임 » 라이프타임을 이용한 참조자 유효화 » 정리","id":"187","title":"정리"},"188":{"body":"프로그램 테스팅은 버그의 존재를 보여주는 매우 효율적인 방법일 수 있지만, 버그의 부재를 보여주기에는 절망적으로 불충분하다. 에츠허르 W. 데이크스트라(Edsger W. Dijkstra), \"겸손한 프로그래머(The Humble Programmer)\" (1972) 에츠허르 W. 데이크스트라(Edsger W. Dijkstra)는 그의 1972년 에세이 “겸손한 프로그램 (The Humble Programmer)”에서 “프로그램 테스팅은 버그의 존재를 보여주는 매우 효율적인 방법일 수 있지만, 버그의 부재를 보여주기에는 절망적으로 불충분하다”라고 말했습니다. 이는 우리가 할 수 있는 한 많은 테스트를 시도하지 않아도 된다는 의미가 아닙니다! 우리 프로그램이 정확하다는 것은 즉 우리가 의도한 바를 그대로 우리가 작성한 코드가 수행한다는 뜻입니다. 러스트는 정확성에 매우 많이 신경 쓴 프로그래밍 언어이지만, 정확성이란 복잡한 주제이며 증명하기 쉽지 않습니다. 러스트의 타입 시스템은 이 짐의 큰 부분을 짊어지고 있지만, 타입 시스템이 모든 종류의 부정확성을 잡아낼 수는 없습니다. 러스트에는 보통 말하는 그런 자동화된 소프트웨어 테스트를 작성하기 위한 지원이 언어 내부에 포함되어 있습니다. 예를 들어 어떤 숫자든 입력되면 2를 더하는 add_two라는 함수를 작성한다 칩시다. 이 함수의 시그니처는 정수를 파라미터로 받아들여서 정수를 결과로 반환합니다. 이 함수를 구현하여 컴파일할 때, 러스트는 우리가 이제껏 봐온 모든 종류의 타입 검사 및 빌림 검사를 할 것입니다. 이러한 검사는, 이를테면 String 값이나 유효하지 않은 참조자를 이 함수로 넘기지 않음을 보장해 줄 것입니다. 그러나 러스트는 우리가 정확히 의도한 것을 이 함수가 수행하는가에 대해서는 검사할 수 없는데 , 말하자면 파라미터 더하기 10 혹은 파라미터 빼기 50이 아니라 파라미터 더하기 2여야 합니다! 이러한 지점이 바로 테스트가 필요해지는 부분입니다. 예를 들면 우리가 3을 add_two 함수에 넘겼을 때, 반환 값은 5임을 단언하는(assert) 테스트를 작성할 수 있습니다. 우리는 어떤 종류의 코드 변경이라도 있을 때마다 기존의 정확히 동작하던 부분에 어떠한 변화도 없음을 확신할 수 있도록 이 테스트들을 실행할 수 있습니다. 테스팅은 복잡한 기술입니다: 하나의 장 내에서 어떻게 좋은 테스트를 작성하는지에 대한 모든 상세한 부분을 다룰 수는 없을지라도, 러스트의 테스팅 설비의 역학을 논의할 것입니다. 우리는 여러분이 테스트를 작성할 때 이용 가능한 어노테이션(annotation)과 매크로, 여러분의 테스트를 실행하기 위해 제공되는 기본 동작 및 옵션, 그리고 테스트들을 유닛(unit) 테스트와 통합(integration) 테스트로 조직화하는 방법에 대해 이야기할 것입니다.","breadcrumbs":"테스팅 » 자동화된 테스트 작성하기","id":"188","title":"자동화된 테스트 작성하기"},"189":{"body":"테스트는 테스트 아닌 코드가 프로그램 내에서 기대했던 대로 기능을 하는지 검증하는 러스트 함수입니다. 테스트 함수의 본체는 통상적으로 다음의 세 가지 동작을 수행합니다: 필요한 데이터 혹은 상태를 설정하기 우리가 테스트하고 싶은 코드를 실행하기 그 결과가 우리 예상대로인지 단언하기(assert) 이러한 동작을 하는 테스트 작성을 위해 러스트가 특별히 제공하는 기능들을 살펴봅시다. test 속성, 몇 가지 매크로, 그리고 should_panic 속성들을 포함해서 말이죠.","breadcrumbs":"테스팅 » 테스트 작성하기 » 테스트를 작성하는 방법","id":"189","title":"테스트를 작성하는 방법"},"19":{"body":"인스톨러에는 또한 문서 복사본이 로컬에 포함되어 있으므로, 여러분은 이를 오프라인으로 읽을 수 있습니다. 여러분의 브라우저에서 로컬 문서를 열려면 rustup doc을 실행하세요. 표준 라이브러리가 제공하는 타입이나 함수가 무엇을 하는지 혹은 어떻게 사용하는지 확신이 들지 않는다면 언제라도 API (application programming interface) 문서를 이용하여 알아보세요!","breadcrumbs":"시작하기 » 설치하기 » 로컬 문서","id":"19","title":"로컬 문서"},"190":{"body":"가장 단순하게 말하면, 러스트 내의 테스트란 test 속성(attribute)이 주석으로 달려진 (annotated) 함수입니다. 속성은 러스트 코드 조각에 대한 메타데이터입니다: 한 가지 예로 5장에서 우리가 구조체와 함께 사용했던 derive 속성이 있습니다. 함수를 테스트 함수로 변경하기 위해서는, fn 전 라인에 #[test]를 추가합니다. cargo test 커맨드를 사용하여 테스트를 실행시키면, 러스트는 test 속성이 달려있는 함수들을 실행하고 각 테스트 함수가 성공 혹은 실패했는지를 보고하는 테스트 실행용 바이너리를 빌드할 것입니다. 7장에서 여러분이 카고를 통해 새로운 라이브러리 프로젝트를 만들었을 때, 테스트 함수를 갖고 있는 테스트 모듈이 자동으로 생성되는 것을 보았습니다. 이 모듈은 우리의 테스트를 작성하기 시작하도록 도움을 주는데, 즉 우리가 새로운 프로젝트를 시작할 때마다 매번 테스트 함수를 위한 추가적인 구조 및 문법을 찾아보지 않아도 되게 해 줍니다. 우리는 원하는 만큼 추가적인 테스트 함수들과 테스트 모듈들을 추가할 수 있습니다! 우리는 실제 코드를 테스팅하지는 않으면서 자동으로 만들어진 템플릿 테스트를 가지고 실험하는 식으로 테스트가 어떻게 동작하는지를 몇 가지 관점에서 탐구할 것입니다. 그러고 나서 우리가 작성한 몇몇 코드를 호출하고 동작이 정확한지를 확고히 하는 실제의 테스트를 작성해 볼 것입니다. adder라고 하는 새로운 라이브러리 프로젝트를 만듭시다: $ cargo new adder --lib Created library `adder` project\n$ cd adder 여러분의 adder 라이브러리 내에 있는 src/lib.rs 파일의 내용물은 Listing 11-1과 같아야 합니다: Filename: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); }\n} Listing 11-1: cargo new를 이용하여 자동으로 생성된 테스트 모듈과 함수 지금은 제일 위의 두 줄은 무시하고 함수가 어떻게 작동하는지 알아보는데 집중합시다. fn 라인 전의 #[test] 어노테이션을 주목하세요: 이 속성이 바로 이것이 테스트 함수임을 나타내므로, 테스트 실행기는 이 함수를 테스트로 다루어야 한다는 것을 알게 됩니다. 또한 우리는 tests 모듈 내에 일반적인 시나리오를 셋업 하거나 일반적인 연산을 수행하는 것을 돕기 위한 테스트 아닌 함수를 넣을 수 있으므로, 어떤 함수가 테스트 함수인지 #[test]를 이용하여 나타낼 필요가 있습니다. 이 함수의 본체는 2 + 2가 4와 같음을 단언하기 위해 assert_eq! 매크로를 사용합니다. 이 단언은 통상적인 테스트에 대한 형식 예제로서 제공됩니다. 실행하여 이 테스트가 통과되는지 확인해봅시다. cargo test 커맨드는 Listing 11-2에서 보는 바와 같이 우리 프로젝트에 있는 모든 테스트를 실행합니다: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished dev [unoptimized + debuginfo] target(s) in 0.22 secs Running target/debug/deps/adder-ce99bcc2479f4607 running 1 test\ntest tests::it_works ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Listing 11-2: 자동으로 생성된 테스트를 실행한 결과 카고는 테스트를 컴파일하고 실행했습니다. Compiling, Finished, 그리고 Running 라인 이후에는 running 1 test 라인이 있습니다. 그다음 라인에는 생성된 테스트 함수의 이름인 it_works가 나타나고, 테스트의 실행 결과 ok가 나타납니다. 그러고 나서 테스트 실행의 전체 요약이 나타납니다. test result: ok.는 모든 테스트가 통과했다는 뜻입니다. 1 passed; 0 failed는 통과하거나 실패한 테스트의 개수를 추가적으로 보여줍니다. 우리가 무시하라고 표시한 테스트가 없기 때문에, 요약문에 0 ignored라고 표시됩니다. 다음 절인 \"테스트의 실행방식 제어하기\"에서 테스트를 무시하는 것에 대해 다룰 것입니다. 0 measured 통계는 성능을 측정하는 벤치마크 테스트를 위한 것입니다. 벤치마크 테스트는 이 글이 쓰인 시점에서는 오직 나이틀리(nightly) 러스트에서만 사용 가능합니다. 나이틀리 러스트에 대한 더 많은 정보는 1장을 보세요. Doc-tests adder로 시작하는 테스트 출력의 다음 부분은 문서 테스트의 결과를 보여주기 위한 것입니다. 아직 어떠한 문서 테스트도 없긴 하지만, 러스트는 우리의 API 문서 내에 나타난 어떠한 코드 예제라도 컴파일할 수 있습니다. 이 기능은 우리의 문서와 코드가 동기화를 유지하도록 돕습니다! 우리는 14장의 \"문서 주석\"절에서 문서 테스트를 작성하는 방법에 대해 이야기할 것입니다. 지금은 Doc-tests 출력을 무시할 것입니다. 우리의 테스트의 이름을 변경하고 테스트 출력이 어떻게 변하는지를 살펴봅시다. 다음과 같이 it_works 함수의 이름을 exploration으로 변경하세요: Filename: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); }\n} 그러고 나서 cargo test를 다시 실행시킵니다. 이제 출력 부분에서 it_works 대신 exploration을 볼 수 있을 것입니다: running 1 test\ntest tests::exploration ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 다른 테스트를 추가해봅시다. 하지만 이번에는 실패하는 테스트를 만들 것입니다! 테스트 함수 내의 무언가가 패닉을 일으키면 테스트는 실패합니다. 각 테스트는 새로운 스레드 내에서 실행되며, 테스트 스레드가 죽은 것을 메인 스레드가 알게 되면, 테스트는 실패한 것으로 표시됩니다. 9장에서 패닉을 유발하는 가장 단순한 방법에 대해 이야기했었습니다: 바로 panic! 매크로를 호출하는 것이죠! 새로운 테스트를 입력하여 여러분의 src/lib.rs가 Listing 11-3과 같은 모양이 되게 해 보세요: Filename: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn exploration() { assert_eq!(2 + 2, 4); } #[test] fn another() { panic!(\"Make this test fail\"); }\n} Listing 11-3: panic! 매크로를 호출하기 때문에 실패하게 될 두번째 테스트 추가 cargo test를 이용하여 다시 한번 테스트를 실행시키세요. 결과 출력은 Listing 11-4와 같이 나올 것인데, 이는 exploration 테스트는 통과하고 another는 실패했음을 보여줍니다: running 2 tests\ntest tests::exploration ... ok\ntest tests::another ... FAILED failures: ---- tests::another stdout ---- thread 'tests::another' panicked at 'Make this test fail', src/lib.rs:10:8\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. failures: tests::another test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out error: test failed Listing 11-4: 한 테스트는 통과하고 다른 한 테스트는 실패할 때의 테스트 결과 test tests::another 라인은 ok 대신 FAILED을 보여줍니다. 개별 결과 부분과 요약 부분 사이에 새로운 두 개의 섹션이 나타납니다: 첫번째 섹션은 테스트 실패에 대한 구체적인 이유를 표시합니다. 이 경우, another는 panicked at 'Make this test fail' 때문에 실패했는데, 이는 src/lib.rs 의 9번 라인에서 발생했습니다. 다음 섹션은 실패한 모든 테스트의 이름만 목록화한 것인데, 이는 테스트들이 많이 있고 구체적인 테스트 실패 출력이 많을 때 유용합니다. 실패하는 테스트의 이름은 이를 더 쉽게 디버깅하기 위해서 해당 테스트만을 실행시키는데 사용될 수 있습니다; \"테스트의 실행방식 제어하기\" 절에서 테스트를 실행시키는 방법에 대한 더 많은 내용을 이야기할 것입니다. 요약 라인이 가장 마지막에 표시됩니다: 전체적으로, 우리의 테스트 결과는 FAILED입니다. 우리는 하나의 테스트에 통과했고 하나의 테스트에 실패했습니다. 이제 서로 다른 시나리오에서 테스트 결과가 어떻게 보이는지를 알았으니, panic! 외에 테스트 내에서 유용하게 쓰일 수 있는 몇 가지 매크로를 봅시다.","breadcrumbs":"테스팅 » 테스트 작성하기 » 테스트 함수의 해부","id":"190","title":"테스트 함수의 해부"},"191":{"body":"표준 라이브러리에서 제공하는 assert! 매크로는 여러분이 테스트가 어떤 조건이 true임을 보장하기를 원하는 경우 유용합니다. assert! 매크로에는 부울린 타입으로 계산되는 인자가 제공됩니다. 만일 값이 true라면 assert!는 아무일도 하지 않고 테스트는 통과됩니다. 만일 값이 false라면, assert!는 panic! 매크로를 호출하는데, 이것이 테스트를 실패하게 합니다. 이는 우리의 코드가 우리 의도대로 기능하고 있는지를 체크하는 것을 도와주는 매크로 중 하나입니다. 5장에 있는 Listing 5-9에서, Rectangle 구조체와 can_hold 메소드를 다루었는데, 여기 Listing 11-5에 다시 나왔습니다. 이 코드를 src/lib.rs 에 넣고, assert! 매크로를 사용하여 테스트를 작성해봅시다. Filename: src/lib.rs #[derive(Debug)]\npub struct Rectangle { length: u32, width: u32,\n} impl Rectangle { pub fn can_hold(&self, other: &Rectangle) -> bool { self.length > other.length && self.width > other.width }\n} Listing 11-5: 5장의 Rectangle 구조체와 can_hold 메소드 이용하기 can_hold 메소드는 부울린 값을 반환하는데, 이는 assert! 매크로를 위한 완벽한 사용 사례라는 의미입니다! Listing 11-6에서는 길이 8에 너비 7인 Rectangle 인스턴스를 만들고, 이것이 길이 5에 너비 1인 다른 Rectangle 인스턴스를 포함할 수 있는지 단언(assert)해보는 것으로 can_hold 메소드를 시험하는 테스트를 작성합니다: Filename: src/lib.rs #[cfg(test)]\nmod tests { use super::*; #[test] fn larger_can_hold_smaller() { let larger = Rectangle { length: 8, width: 7 }; let smaller = Rectangle { length: 5, width: 1 }; assert!(larger.can_hold(&smaller)); }\n} Listing 11-6: 큰 사각형이 작은 사각형을 정말로 담을 수 있는지 검사하는 can_hold를 위한 테스트 tests 모듈 내에 새로운 라인이 추가된 것을 주목하세요: use super::*;. tests 모듈은 우리가 7장에서 다루었던 보통의 가시성 규칙을 따르는 일반적인 모듈입니다. 우리가 내부 모듈 내에 있기 때문에, 외부 모듈에 있는 코드를 내부 모듈의 스코프로 가져올 필요가 있습니다. 여기서는 글롭(*)을 사용하기로 선택했고 따라서 우리가 외부 모듈에 정의한 어떠한 것이듯 이 tests모듈에서 사용 가능합니다. 우리의 테스트는 larger_can_hold_smaller로 명명되었고, 요구된 바와 같이 Rectangle 인스턴스를 두 개 생성했습니다. 그 뒤 assert! 매크로를 호출하고 larger.can_hold(&smaller) 호출의 결과값을 인자로서 넘겼습니다. 이 표현식은 true를 반환할 예정이므로, 우리의 테스트는 통과해야 합니다. 자, 이제 알아봅시다! running 1 test\ntest tests::larger_can_hold_smaller ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 통과되었군요! 이번에는 작은 사각형이 큰 사각형을 포함시킬수 없음을 단언하는 또 다른 테스트를 추가합시다: Filename: src/lib.rs #[cfg(test)]\nmod tests { use super::*; #[test] fn larger_can_hold_smaller() { // --snip-- } #[test] fn smaller_cannot_hold_larger() { let larger = Rectangle { length: 8, width: 7 }; let smaller = Rectangle { length: 5, width: 1 }; assert!(!smaller.can_hold(&larger)); }\n} 이 경우 can_hold 함수의 올바른 결과값은 false이므로, assert! 매크로에게 넘기기 전에 이 결과를 반대로 만들 필요가 있습니다. 결과적으로, 우리의 테스트는 can_hold가 false를 반환할 경우에만 통과할 것입니다: running 2 tests\ntest tests::smaller_cannot_hold_larger ... ok\ntest tests::larger_can_hold_smaller ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 통과하는 테스트가 두 개가 되었습니다! 이제는 만약 우리의 코드에 버그가 있을 때는 테스트 결과가 어찌되는지 봅시다. can_hold 메소드의 구현 부분 중 큰(>) 부등호를 이용해 길이를 비교하는 부분을 작은(<) 부등호로 바꿔봅시다: # #[derive(Debug)]\n# pub struct Rectangle {\n# length: u32,\n# width: u32,\n# }\n// --snip-- impl Rectangle { pub fn can_hold(&self, other: &Rectangle) -> bool { self.length < other.length && self.width > other.width }\n} 테스트를 실행시키면 이제 아래와 같이 출력됩니다: running 2 tests\ntest tests::smaller_cannot_hold_larger ... ok\ntest tests::larger_can_hold_smaller ... FAILED failures: ---- tests::larger_can_hold_smaller stdout ---- thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:22:8\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. failures: tests::larger_can_hold_smaller test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out 우리의 테스트가 버그를 찾았습니다! larger.length는 8이고 smaller.length는 5이므로, can_hold의 길이 부분에 대한 비교값은 이제 false를 반환합니다: 8이 5보다 작지 않으니까요.","breadcrumbs":"테스팅 » 테스트 작성하기 » assert! 매크로를 이용하여 결과 확인하기","id":"191","title":"assert! 매크로를 이용하여 결과 확인하기"},"192":{"body":"기능성을 테스트하는 일반적인 방법은 테스트 내의 코드의 결과값과 우리가 기대하는 값을 비교하여 둘이 서로 같은지를 확실히 하는 것입니다. 이를 assert! 매크로에 ==를 이용한 표현식을 넘기는 식으로 할 수도 있습니다. 그러나 이러한 테스트를 더 편리하게 수행해주는 표준 라이브러리가 제공하는 한 쌍의 매크로 - assert_eq!와 assert_ne! - 가 있습니다. 이 매크로들은 각각 동치(equality)와 부동(inequality)을 위해 두 인자를 비교합니다. 또한 이들은 만일 단언에 실패한다면 두 값을 출력해 주는데, 이는 왜 테스트가 실패했는지를 포기 더 쉬워집니다; 반면, assert!는 == 표현식에 대해 false 값을 얻었음을 가리킬 뿐, 어떤 값이 false값을 야기했는지는 알려주지 않습니다. Listing 11-7와 같이, 파라미터에 2를 더하여 결과를 반환하는 add_two 함수를 작성합시다. 그 후 assert_eq! 매크로를 이용하여 이 함수를 테스트하겠습니다. Filename: 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} Listing 11-7: assert_eq! 매크로를 이용하는 add_two 함수 테스트 이게 통과하는지 확인해 봅시다! running 1 test\ntest tests::it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 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} 테스트를 다시 실행해 보세요: running 1 test\ntest tests::it_adds_two ... FAILED failures: ---- tests::it_adds_two stdout ---- thread 'tests::it_adds_two' panicked at 'assertion failed: `(left == right)` left: `4`, right: `5`', src/lib.rs:11:8 failures: tests::it_adds_two test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out 우리의 테스트가 버그를 잡았어요! it_adds_two 테스트는 assertion failed: `(left == right)`라는 메세지와 left는 4였고 right는 5였다는 것으로 보여줌과 함께 실패했습니다. 이 메세지는 우리가 디버깅을 시작하는데 유용한 도움을 줍니다: assert_eq!의 left 인자는 4였는데, add_two(2)를 넣은 right` 인자는 5라고 말해주고 있습니다. 몇몇 언어와 테스트 프레임워크 내에서는, 두 값이 같은지를 단언하는 함수의 파라미터를 expected와 actual로 부르며, 우리가 인자를 넣는 순서가 중요하다는 점을 기억하세요. 하지만 러스트에서는 그 대신 left와 right라고 불리며 우리가 기대한 값과 테스트 내의 코드가 생성하는 값을 지정하는 순서는 중요치 않습니다. 이 테스트의 단언을 assert_eq!(add_two(2), 4)로 작성할 수도 있는데, 이는 assertion failed: `(left == right)`와 left는 5고 right는 4라는 실패 메세지를 만들어낼 것입니다. assert_ne! 매크로는 우리가 제공한 두 개의 값이 서로 갖지 않으면 통과하고 동일하면 실패할 것입니다. 이 매크로는 어떤 값이 될 것인지 는 정확히 확신하지 못하지만, 어떤 값이라면 절대로 될 수 없는지 는 알고 있을 경우에 가장 유용합니다. 예를 들면, 만일 어떤 함수가 입력값을 어떤 방식으로든 변경한다는 것을 보장하지만, 그 입력값이 우리가 테스트를 실행한 요일에 따라 달라지는 형태라면, 단언을 하는 가장 좋은 방법은 함수의 결괏값이 입력값과 같지 않다는 것일지도 모릅니다. 표면 아래에서, assert_eq!와 assert_ne! 매크로는 각각 ==과 != 연산자를 이용합니다. 단언에 실패하면, 이 매크로들은 디버그 포맷팅을 사용하여 인자들을 출력하는데, 이는 비교되는 값들이 PartialEq와 Debug 트레잇을 구현해야 한다는 의미입니다. 모든 기본 타입과 표준 라이브러리가 제공하는 대부분의 타입들은 이 트레잇들을 구현하고 있습니다. 여러분이 정의한 구조체나 열거형에 대해서, 해당 타입의 값이 서로 같은지 혹은 다른지를 단언하기 위해서는 PartialEq를 구현할 필요가 있습니다. 단언에 실패할 경우에 값을 출력하기 위해서는 Debug를 구현해야 합니다. 5장에서 설명한 바와 같이 두 트레잇 모두 추론 가능한(derivable) 트레잇이기 때문에, 이 트레잇의 구현은 보통 #[derive(PartialEq, Debug)] 어노테이션을 여러분의 구조체나 열거형 정의부에 추가하는 정도로 간단합니다. 이에 대한 것과 다른 추론 가능한 트레잇에 대한 더 자세한 내용은 부록 C를 참고하세요.","breadcrumbs":"테스팅 » 테스트 작성하기 » aseert_eq!와 assert_ne!를 이용한 동치(equality) 테스트","id":"192","title":"aseert_eq!와 assert_ne!를 이용한 동치(equality) 테스트"},"193":{"body":"또한 우리는 assert!, assert_eq! 및 assert_ne! 매크로의 추가 인자로서 커스텀 메세지를 입력하여 실패 메세지와 함께 출력되도록 할 수 있습니다. assert!가 요구하는 하나의 인자 후에 지정된 인자들이나 assert_eq!와 assert_ne!가 요구하는 두 개의 인자 후에 지정된 인자들은 우리가 8장의 “+ 연산자나 format! 매크로를 이용한 접합”절에서 다루었던 format! 매크로에 넘겨지므로, 여러분은 {} 변경자 (placeholder)를 갖는 포맷 스트링과 이 변경자에 입력될 값들을 넘길 수 있습니다. 커스텀 메세지는 해당 단언의 의미를 문서화하기 위한 용도로서 유용하므로, 테스트가 실패했을 때, 코드에 어떤 문제가 있는지에 대해 더 좋은 생각을 가질 수 있습니다. 예를 들어, 이름을 부르며 사람들을 환영하는 함수가 있고, 이 함수에 넘겨주는 이름이 출력 내에 있는지 테스트하고 싶다고 칩시다: Filename: 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} 이 테스트를 수행하면 다음을 출력합니다: running 1 test\ntest tests::greeting_contains_name ... FAILED failures: ---- tests::greeting_contains_name stdout ---- thread 'tests::greeting_contains_name' panicked at 'assertion failed: result.contains(\"Carol\")', src/lib.rs:12:8\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. failures: tests::greeting_contains_name 이 결과는 그저 단언이 실패했으며 몇 번째 줄의 단언이 실패했는지만을 나타냅니다. 이 경우에서 더 유용한 실패 메세지는 greeting 함수로부터 얻은 값을 출력하는 것일 테지요. 테스트 함수를 바꿔서 greeting 함수로부터 얻은 실제 값으로 채워질 변경자를 이용한 포맷 스트링으로부터 만들어지는 커스텀 실패 메세지를 줄 수 있도록 해봅시다: #[test]\nfn greeting_contains_name() { let result = greeting(\"Carol\"); assert!( result.contains(\"Carol\"), \"Greeting did not contain name, value was `{}`\", result );\n} 이제 테스트를 다시 실행시키면, 더 많은 정보를 가진 에러 메세지를 얻을 것입니다: ---- tests::greeting_contains_name stdout ---- thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain name, value was `Hello!`', src/lib.rs:12:8\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. 이제 실제로 테스트 출력에서 얻어진 값을 볼 수 있고, 이는 우리가 기대했던 일 대신 실제 어떤 일이 일어났는지 디버깅하는데 도움을 줄 것입니다.","breadcrumbs":"테스팅 » 테스트 작성하기 » 커스텀 실패 메세지 추가하기","id":"193","title":"커스텀 실패 메세지 추가하기"},"194":{"body":"우리의 코드가 우리가 기대한 정확한 값을 반환하는 것을 체크하는 것에 더하여, 우리의 코드가 우리가 기대한 대로 에러가 나는 경우를 처리할 수 있는지 체크하는 것 또한 중요합니다. 예를 들어, 9장의 Listing 9-9에서 우리가 만들었던 Guess 타입을 떠올려보세요. Guess를 이용하는 다른 코드는 Guess 인스턴스가 1과 100 사이의 값만 가질 것이라는 보장에 의존적입니다. 우리는 범위 밖의 값으로 Guess 인스턴스를 만드는 시도가 패닉을 일으킨다는 것을 확실히 하는 테스트를 작성할 수 있습니다. 이는 또 다른 속성인 should_panic를 테스트 함수에 추가함으로써 할 수 있습니다. 이 속성은 함수 내의 코드가 패닉을 일으키면 테스트가 통과하도록 만들어줍니다; 함수 내의 코드가 패닉을 일으키지 않는다면 테스트는 실패할 것입니다. Listing 11-8은 Guess::new의 에러 조건이 우리 예상대로 발동되는지를 검사하는 테스트를 보여줍니다: Filename: src/lib.rs pub struct Guess { value: u32,\n} impl Guess { pub fn new(value: u32) -> 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} Listing 11-8: 어떤 조건이 panic!을 일으키는지에 대한 테스트 #[should_panic] 속성이 #[test] 속성 뒤, 그리고 적용될 테스트 함수 앞에 붙었습니다. 이 테스트가 통과될 때의 결과를 봅시다: running 1 test\ntest tests::greater_than_100 ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 좋아 보이는군요! 이제 new 함수가 100 이상의 값일 때 패닉을 발생시키는 조건을 제거함으로써 코드에 버그를 넣어봅시다: # pub struct Guess {\n# value: u32,\n# }\n#\nimpl Guess { pub fn new(value: u32) -> Guess { if value < 1 { panic!(\"Guess value must be between 1 and 100, got {}.\", value); } Guess { value } }\n} Listing 11-8의 테스트를 실행시키면, 아래와 같이 실패할 것입니다: running 1 test\ntest tests::greater_than_100 ... FAILED failures: failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out 이 경우에는 그다지 쓸모 있는 메세지를 얻지 못하지만, 한번 테스트 함수를 살펴보게 되면, 함수가 #[should_panic]으로 어노테이션 되었다는 것을 볼 수 있습니다. 우리가 얻은 실패는 함수 내의 코드가 패닉을 일으키지 않았다는 의미가 됩니다. should_panic 테스트는 애매할 수 있는데, 그 이유는 이 속성이 단지 코드에서 어떤 패닉이 유발되었음만을 알려줄 뿐이기 때문입니다. should_panic 테스트는 일어날 것으로 예상한 것 외의 다른 이유로 인한 패닉이 일어날 지라도 통과할 것입니다. should_panic 테스트를 더 엄밀하게 만들기 위해서, should_panic 속성에 expected 파라미터를 추가할 수 있습니다. 이 테스트 도구는 실패 메세지가 제공된 텍스트를 담고 있는지 확실히 할 것입니다. 예를 들면, Listing 11-9와 같이 입력된 값이 너무 작거나 혹은 너무 클 경우에 대해 서로 다른 메세지를 가진 패닉을 일으키는 new 함수를 갖고 있는 수정된 Guess 코드를 고려해봅시다: Filename: src/lib.rs # pub struct Guess {\n# value: u32,\n# }\n# // --snip impl Guess { pub fn new(value: u32) -> 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 = \"Guess value must be less than or equal to 100\")] fn greater_than_100() { Guess::new(200); }\n} Listing 11-9: 어떤 조건이 특정 패닉 메세지를 가진 panic!을 일으키는 테스트 이 테스트는 통과할 것인데, 그 이유는 should_panic 속성에 추가한 expected 파라미터 값이 Guess::new 함수가 패닉을 일으킬 때의 메세지의 서브 스트링이기 때문입니다. 우리가 예상하는 전체 패닉 메세지로 특정할 수도 있는데, 그러한 경우에는 Guess value must be less than or equal to 100, got 200.이 되겠지요. 여러분이 should_panic에 대한 기대하는 파라미터를 특정하는 것은 패닉 메세지가 얼마나 유일한지 혹은 유동적인지, 그리고 여러분의 테스트가 얼마나 정확하기를 원하는지에 따라서 달라집니다. 위의 경우, 패닉 메세지의 서브 스트링은 실행된 함수의 코드가 else if value > 100 경우에 해당함을 확신하기에 충분합니다. expect 메세지를 가진 should_panic 테스트가 실패하면 어떻게 되는지 보기 위해서, 다시 한번 if value < 1 아래 코드 블록과 else if value > 100 아래 코드 블록을 바꿔서 버그를 만들어봅시다: if value < 1 { panic!(\"Guess value must be less than or equal to 100, got {}.\", value);\n} else if value > 100 { panic!(\"Guess value must be greater than or equal to 1, got {}.\", value);\n} 이번에는 should_panic 테스트를 실행하면, 아래와 같이 실패합니다: running 1 test\ntest tests::greater_than_100 ... FAILED failures: ---- tests::greater_than_100 stdout ---- thread 'tests::greater_than_100' panicked at 'Guess value must be greater than or equal to 1, got 200.', src/lib.rs:11:12\nnote: Run with `RUST_BACKTRACE=1` for a backtrace.\nnote: Panic did not include expected string 'Guess value must be less than or\nequal to 100' failures: tests::greater_than_100 test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out 실패 메세지는 이 테스트가 우리 예상에 맞게 실제로 패닉에 빠지기는 했으나, 패닉 메세지가 예상하는 스트링을 포함하지 않고 있다고 말하고 있습니다 (did not include expected string 'Guess value must be less than or equal to 100'.) 우리가 얻어낸 패닉 메세지를 볼 수 이는데, 이 경우에는 Guess value must be greater than or equal to 1, got 200. 이었습니다. 그러면 우리는 어디에 우리의 버그가 있는지를 찾아내기 시작할 수 있습니다! 이제까지 테스트를 작성하는 몇 가지 방법을 알게 되었으니, 우리의 테스트를 실행할 때 어떤 일이 벌어지는지를 살펴보고 cargo test와 함께 사용할 수 있는 어려가지 옵션들에 대해서 탐구해봅시다.","breadcrumbs":"테스팅 » 테스트 작성하기 » should_panic을 이용한 패닉에 대한 체크","id":"194","title":"should_panic을 이용한 패닉에 대한 체크"},"195":{"body":"cargo run이 여러분의 코드를 컴파일하고 난 뒤 그 결과인 바이너리를 실행하는 것과 마찬가지로, cargo test는 여러분의 코드를 테스트 모드에서 컴파일하고 결과로 발생한 테스트 바이너리를 실행합니다. 여러분은 커맨드 라인 옵션을 지정하여 cargo test의 기본 동작을 변경할 수 있습니다. 예를 들어, cargo test를 통해 생성된 바이너리의 기본 동작은 모든 테스트를 병렬적으로 수행하고 테스트가 실행되는 동안 생성된 결과를 캡처하는 것으로, 테스트 결과와 연관된 출력을 읽기 쉽도록 화면에 표시되는 것을 막아버립니다. 어떤 커맨드 라인 옵션은 cargo test에 입력되고 어떤 옵션은 결과 테스트 바이너리에 입력됩니다. 이 두 가지 타입의 인자를 구분하기 위해서, cargo test에 주어질 인자를 먼저 나열하고, 그다음 구분자(separator)로 --를 넣고, 그 뒤 테스트 바이너리에 입력될 인자를 나열합니다. cargo test --help를 실행하는 것은 cargo test에서 사용할 수 있는 옵션을 표시하고, cargo test -- --help를 실행하는 것은 구분자 -- 이후에 나올 수 있는 옵션을 표시합니다.","breadcrumbs":"테스팅 » 테스트 실행하기 » 테스트의 실행 방식 제어하기","id":"195","title":"테스트의 실행 방식 제어하기"},"196":{"body":"여러 개의 테스트를 실행할 때는, 기본적으로 스레드를 이용하여 병렬적으로 수행됩니다. 이는 테스트가 더 빠르게 실행되어 끝낼 수 있다는 의미이므로, 우리의 코드가 잘 동작하는지 혹은 그렇지 않은지에 대한 피드백을 더 빨리 얻을 수 있습니다. 테스트가 동시에 실행되므로, 여러분의 테스트가 서로 다른 테스트 혹은 공유 상태 값에 의존하지 않는지 주의해야 하는데, 이는 이를테면 현재 작업 디렉토리나 환경 변수와 같은 공유 환경 값을 포함합니다. 예를 들면, 여러분이 작성한 테스트 각각이 test-output.txt 라는 파일을 디스크에 만들고 이 파일에 어떤 데이터를 쓰는 코드를 실행한다고 가정해봅시다. 그런 다음 각 테스트는 그 파일로부터 데이터를 읽고, 이 파일이 특정한 값을 담고 있는지 단언하는데, 이 값들은 테스트마다 다릅니다. 모든 테스트들이 동시에 실행되기 때문에, 어떤 테스트가 파일을 쓰고 읽는 동안 다른 테스트가 파일을 덮어쓸지도 모릅니다. 두 번째 테스트는 실패할 것인데, 이는 코드가 정확히 않아서가 아니라 테스트들이 병렬적으로 실행하는 동안 서로에게 간섭을 일으켰기 때문입니다. 한 가지 해결책은 각 테스트가 서로 다른 파일을 쓰도록 확실히 하는 것일 겁니다; 또 다른 해결책은 테스트를 한 번에 하나씩만 실행하는 것입니다. 만일 여러분이 테스트들을 병렬적으로 실행하고 싶지 않을 경우, 혹은 여러분이 사용되는 스레드의 개수에 대한 더 정밀한 제어를 하고 싶을 경우, 여러분은 --test-threads 플리그와 테스트 바이너리에서 사용하고 싶은 스레드 개수를 넘길 수 있습니다. 다음 예제를 봅시다: $ cargo test -- --test-threads=1 여기서는 테스트 스레드의 개수에 1을 지정했는데, 이는 프로그램이 어떠한 병렬 처리도 사용하지 않음을 얘기해줍니다. 테스트를 하나의 스레드에서 실행하는 것은 병렬로 수행하는 것에 비해 시간이 더 오래 걸리겠지만, 테스트들이 어떤 상태를 공유할 경우 서로가 간섭할 가능성이 없어질 것입니다.","breadcrumbs":"테스팅 » 테스트 실행하기 » 테스트를 병렬 혹은 연속으로 실행하기","id":"196","title":"테스트를 병렬 혹은 연속으로 실행하기"},"197":{"body":"기본적으로 어떤 테스트가 통과하면, 러스트의 테스트 라이브러리는 표준 출력(standard output)으로 출력되는 어떤 것이든 캡처합니다. 예를 들면, 우리가 테스트 내에서 println!을 호출하고 이 테스트가 통과하면, println! 출력을 터미널에서 볼 수 없습니다: 우리는 오직 그 테스트가 통과되었다고 표시된 라인만 볼 뿐입니다. 만일 테스트가 실패하면, 실패 메세지 아래에 표준 출력으로 출력되었던 어떤 것이든 보게 될 것입니다. 예를 들어, Listing 11-10은 파라미터의 값을 출력한 뒤 10을 반환하는 바보 같은 함수를 보여주고 있습니다. 그리고 통과하는 테스트와 실패하는 테스트를 갖추고 있습니다: Filename: 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} Listing 11-10: println!을 호출하는 함수를 위한 테스트 cargo test를 이용하여 이 테스트를 실행했을 때 보게 될 출력은 다음과 같습니다: running 2 tests\ntest tests::this_test_will_pass ... ok\ntest tests::this_test_will_fail ... FAILED failures: ---- tests::this_test_will_fail stdout ---- I got the value 8\nthread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:8\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out I got the value 4라는 메세지를 어디에서도 볼 수 없는데, 이는 성공하는 테스트가 실행시키는 출력이라는 점을 주목하세요. 이 출력 메세지는 캡처되었습니다. 실패한 테스트로부터 얻어진 출력 메세지인 I got the value 8은 테스트 정리 출력 부분에 나타나는데, 이는 테스트 실패 원인 또한 함께 보여줍니다. 만일 성공하는 테스트에 대한 출력 값 또한 볼 수 있기를 원한다면, --nocapture 플래그를 이용하여 출력 캡처 동작을 비활성화시킬 수 있습니다: $ cargo test -- --nocapture Listing 11-10의 테스트를 --nocapture 플래그와 함께 실행시키면 다음과 같이 나옵니다: running 2 tests\nI got the value 4\nI got the value 8\ntest tests::this_test_will_pass ... ok\nthread 'tests::this_test_will_fail' panicked at 'assertion failed: `(left == right)` left: `5`, right: `10`', src/lib.rs:19:8\nnote: Run with `RUST_BACKTRACE=1` for a backtrace.\ntest tests::this_test_will_fail ... FAILED failures: failures: tests::this_test_will_fail test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out 테스트에서의 출력과 테스트 결과 출력이 분리된 점을 주목하세요; 이는 우리가 이전 절에서 다룬 내용처럼 테스트가 병렬적으로 수행되기 때문입니다. --test-threads=1 옵션과 --nocapture 기능을 동시에 시도하고 출력이 어떻게 바뀌는지를 확인해 보세요!","breadcrumbs":"테스팅 » 테스트 실행하기 » 함수 결과 보여주기","id":"197","title":"함수 결과 보여주기"},"198":{"body":"가끔, 모든 테스트 셋을 실행하는 것은 시간이 오래 걸릴 수 있습니다. 만일 여러분이 특정 영역의 코드에 대해서 작업하고 있다면, 그 코드와 연관된 테스트만 실행시키고 싶어 할 수도 있습니다. 여러분은 cargo test에 여러분이 실행시키고 싶어 하는 테스트(들)의 이름들을 인자로 넘김으로써 어떤 테스트들을 실행시킬지 고를 수 있습니다. 테스트의 일부분만을 실행시키는 법을 보여드리기 위해서, Listing 11-11에서 보시는 바와 같이 add_two 함수를 위한 세 개의 테스트를 만들어서 하나만 골라 실행해보겠습니다: Filename: 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} Listing 11-11: 여러 이름으로 된 세 가지 테스트 만일 테스트를 어떠한 인자 없이 실행시키면, 전에 본 것과 같이 모든 테스트가 병렬적으로 수행될 것입니다: running 3 tests\ntest tests::add_two_and_two ... ok\ntest tests::add_three_and_two ... ok\ntest tests::one_hundred ... ok test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 단일 테스트 실행하기 단 하나의 테스트만 실행시키기 위해 cargo test에 그 테스트 함수의 이름을 넘길 수 있습니다: $ cargo test one_hundred Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running target/debug/deps/adder-06a75b4a1f2515e9 running 1 test\ntest tests::one_hundred ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out one_hundred라는 이름의 테스트만 실행되었습니다; 다른 두 개의 테스트는 이 이름에 맞지 않습니다. 테스트 출력은 정리 라인의 끝에 2 filtered out이라고 표시함으로써 이 커맨드로 지정한 것보다 많은 테스트를 가지고 있음을 우리에게 알려줍니다. 이 방법으로는 여러 테스트의 이름들을 특정할 수는 없고, cargo test에 주어진 제일 첫 번째 값만 이용될 것입니다. 여러 개의 테스트를 실행시키기 위한 필터링 우리는 테스트 이름의 일부분을 특정할 수 있고, 해당 값과 일치하는 이름의 테스트가 실행될 것입니다. 예를 들면, 우리의 테스트 이름들 중에서 두 개가 add를 포함하므로, cargo test add라고 실행하여 이 두 개의 테스트를 실행시킬 수 있습니다: $ cargo test add Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running target/debug/deps/adder-06a75b4a1f2515e9 running 2 tests\ntest tests::add_two_and_two ... ok\ntest tests::add_three_and_two ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out 이는 add가 이름에 포함된 모든 테스트를 실행시켰고 one_hundred라는 이름의 테스트를 걸러냈습니다. 또한 테스트가 있는 모듈이 테스트의 이름의 일부가 되어 있으므로, 모듈의 이름으로 필터링하여 그 모듈 내의 모든 테스트를 실행시킬 수 있다는 점도 주목하세요.","breadcrumbs":"테스팅 » 테스트 실행하기 » 이름으로 테스트의 일부분만 실행하기","id":"198","title":"이름으로 테스트의 일부분만 실행하기"},"199":{"body":"이따금씩 몇몇 특정 테스트들은 실행하는데 너무나 시간이 많이 소모될 수 있어서, 여러분은 cargo test의 실행 시 이 테스트들을 배제하고 싶어 할지도 모릅니다. 여러분이 실행시키고자 하는 모든 테스트들을 인자로서 열거하는 것 대신, 다음과 같이 시간이 많이 걸리는 테스트들에 ignore 속성을 어노테이션하여 이들을 배제시킬 수 있습니다: Filename: 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 dev [unoptimized + debuginfo] target(s) in 0.24 secs Running target/debug/deps/adder-ce99bcc2479f4607 running 2 tests\ntest expensive_test ... ignored test it_works ... ok test result: ok. 1 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out expensive_test는 ignored 리스트에 포함되었습니다. 만일 무시된 테스트들만 실행시키고 싶다면, cargo test -- --ignored라고 실행함으로써 이를 요청할 수 있습니다. $ cargo test -- --ignored Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running target/debug/deps/adder-ce99bcc2479f4607 running 1 test\ntest expensive_test ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out 어떠한 테스트를 실행시킬지를 제어함으로써, 여러분은 cargo test의 결과가 빠르게 나오도록 확실히 할 수 있습니다. ignored 테스트들의 결과를 확인하기에 타당한 시점에 있고 해당 결과를 기다릴 시간을 가지고 있을 때, 여러분은 대신 cargo test -- --ignored를 실행시킬 수 있습니다.","breadcrumbs":"테스팅 » 테스트 실행하기 » 특별한 요청이 없는 한 몇몇 테스트들 무시하기","id":"199","title":"특별한 요청이 없는 한 몇몇 테스트들 무시하기"},"2":{"body":"러스트는 다양한 이유로 수많은 사람들에게 이상적입니다. 가장 중요한 그룹 중 일부를 살펴봅시다.","breadcrumbs":"소개 » 러스트는 누구를 위한 것인가요?","id":"2","title":"러스트는 누구를 위한 것인가요?"},"20":{"body":"여러분이 러스트를 설치했으니, 이제 여러분의 첫번째 러스트 프로그램을 작성해봅시다. 새로운 언어를 배울 때면 “Hello, world!”라는 텍스트를 스크린에 출력하는 짧은 프로그램을 작성하는 것이 전통이니, 우리도 여기서 그렇게 할 것입니다! 노트: 이 책은 커맨드 라인에 대한 기본적인 친숙성을 가정하고 있습니다. 러스트는 여러분의 코드 수정, 도구 사용, 혹은 어디에 여러분의 코드가 있는지에 대한 어떠한 특별 요구도 없으므로, 커맨드 라인 대신 IDE (Integrated Development Environment, 통합 개발 환경)를 이용하는 것은 선호한다면, 여러분이 좋아하는 IDE를 편히 이용하세요. 이제 많은 IDE들이 어느 정도 수준의 러스트 지원을 해줍니다; 자세한 사항은 해당 IDE의 문서를 확인하세요. 최근에는 러스트 팀이 훌륭한 IDE 지원을 활성화하는데 집중해왔으며, 매우 급격한 진전이 이루어지고 있습니다!","breadcrumbs":"시작하기 » Hello, World! » Hello, World!","id":"20","title":"Hello, World!"},"200":{"body":"이 장의 시작 부분에서 언급했듯이, 테스팅은 복잡한 분야이고, 여러 사람들이 서로 다른 용어와 조직화 방식을 이용합니다. 러스트 커뮤니티에서는 테스트에 대해서 두 개의 주요한 카테고리로 나눠 생각합니다: 단위 테스트(unit test) 그리고 *통합 테스트(integration test)*입니다. 단위 테스트는 작고 하나에 더 집중하며, 한 번에 하나의 모듈만 분리하여 테스트하고, 비공개 인터페이스 (private interface)를 테스트합니다. 통합 테스트는 완전히 여러분의 라이브러리 외부에 있으며, 공개 인터페이스 (public interface)를 이용하고 테스트마다 여러 개의 모듈을 잠재적으로 실험함으로써, 다른 외부의 코드가 하는 방식과 동일한 형태로 여러분의 코드를 이용합니다. 두 종류의 테스트 작성 모두가 여러분의 라이브러리 코드 조각들이 따로따로 혹은 함께 사용되었을 때 여러분이 기대하는 바와 같이 작동하는 지를 확신시키는데 중요합니다.","breadcrumbs":"테스팅 » 테스트 조직화 » 테스트 조직화","id":"200","title":"테스트 조직화"},"201":{"body":"단위 테스트의 목적은 각 코드의 단위를 나머지 부분과 분리하여 테스트하는 것인데, 이는 코드가 어디 있고 어느 부분이 기대한 대로 동작하지 않는지를 빠르게 정확히 찾아낼 수 있도록 하기 위함입니다. 단위 테스트는 src 디렉토리 내에 넣는데, 각 파일마다 테스트하는 코드를 담고 있습니다. 관례는 각 파일마다 테스트 함수를 담고 있는 tests라는 이름의 모듈을 만들고, 이 모듈에 cfg(test)라고 어노테이션 하는 것입니다. 테스트 모듈과 #[cfg(test)] 테스트 모듈 상의 #[cfg(test)] 어노테이션은 러스트에게 우리가 cargo build를 실행시킬 때가 아니라 cargo test를 실행시킬 때에만 컴파일하고 실행시키라고 말해줍니다. 이는 우리가 오직 라이브러리만 빌드하고 싶을 때 컴파일 시간을 절약시켜주고, 테스트가 포함되어 있지 않으므로 컴파일 결과물의 크기를 줄여줍니다. 통합 테스트는 다른 디렉토리에 위치하기 때문에, 여기에는 #[cfg(test)] 어노테이션이 필요치 않음을 앞으로 보게 될 것입니다. 하지만, 단위 테스트가 해당 코드와 동일한 파일에 위치하기 때문에, #[cfg(test)]를 사용하여 컴파일 결과물에 이들이 포함되지 않아야 함을 특정합니다. 이 장의 첫 번째 절에서 새로운 adder 프로젝트를 생성했을 때, 카고가 우리를 위하여 아래와 같은 코드를 생성했던 것을 상기하세요: Filename: src/lib.rs #[cfg(test)]\nmod tests { #[test] fn it_works() { assert_eq!(2 + 2, 4); }\n} 이 코드는 자동으로 생성되는 테스트 모듈입니다. cfg 속성은 환경 설정(configuration) 을 의미하며, 러스트에게 뒤따르는 아이템이 특정한 환경 값에 대해서만 포함되어야 함을 말해줍니다. 위의 경우, 환경 값이 test인데, 테스트를 컴파일하고 실행하기 위해 러스트로부터 제공되는 것입니다. 이 속성을 이용함으로써, 카고는 우리가 능동적으로 cargo test를 이용해서 테스트를 실행시킬 경우에만 우리의 테스트 코드를 컴파일합니다. 이는 이 모듈 내에 있을지도 모를 어떠한 헬퍼 함수들, 추가적으로 #[test]라고 어노테이션 된 함수들을 포함합니다. 비공개 함수 테스트하기 테스팅 커뮤니티 내에서 비공개 함수가 직접적으로 테스트되어야 하는지 혹은 그렇지 않은지에 대한 논쟁이 있었고, 다른 언어들은 비공개 함수를 테스트하는 것이 어렵거나 불가능하게 만들어두었습니다. 여러분이 어떤 테스트 이데올로기를 고수하는지와는 상관없이, 러스트의 비공개 규칙은 여러분이 비공개 함수를 테스트하도록 허용해줍니다. 비공개 함수 internal_adder가 있는 Listing 11-12 내의 코드를 고려해 보시죠: Filename: 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} Listing 11-12: 비공개 함수 테스트하기 internal_adder 함수는 pub으로 표시되어 있지 않지만, 테스트가 그저 러스트 코드일 뿐이고 tests 모듈도 그냥 또 다른 모듈이기 때문에, internal_adder를 불러들여 호출하는 것이 그냥 되는 것을 주목하세요. 만약 여러분이 비공개 함수를 테스트해야 한다고 생각하지 않는다면, 러스트에서는 여러분이 그렇게 하도록 강제할 일은 없습니다.","breadcrumbs":"테스팅 » 테스트 조직화 » 단위 테스트","id":"201","title":"단위 테스트"},"202":{"body":"러스트에서 통합 테스트들은 완전히 여러분의 라이브러리 외부에 있습니다. 이들은 여러분의 라이브러리를 다른 코드들과 동일한 방식으로 이용하는데, 이는 이 외부 테스트들이 오직 여러분의 라이브러리의 공개 API 부분에 속하는 함수들만 호출할 수 있다는 의미입니다. 이들의 목적은 여러분의 라이브러리의 수많은 파트들이 함께 올바르게 동작하는지를 시험하는 것입니다. 그 자체로서는 올바르게 동작하는 코드의 단위들도 통합되었을 때는 문제를 일으킬 수 있으므로, 통합된 코드의 테스트 커버율 또한 중요합니다. 통합 테스트를 만들기 위해서는 tests 디렉토리를 먼저 만들 필요가 있습니다. tests 디렉토리 프로젝트 디렉토리의 최상위, 그러니까 src 옆에 tests 디렉토리를 만듭니다. 카고는 이 디렉토리 내의 통합 테스트 파일들을 찾을 줄 압니다. 그런 후에는 이 디렉토리에 원하는 만큼 많은 테스트 파일을 만들 수 있으며, 카고는 각각의 파일들을 개별적인 크레이트처럼 컴파일할 것입니다. 한 번 통합 테스트를 만들어봅시다. Listing 11-12의 src/lib.rs 코드를 그대로 유지한 채로, tests 디렉토리를 만들고, tests/integration_test.rs 라는 이름의 새 파일을 만든 다음, Listing 11-13의 코드를 집어넣으세요. Filename: tests/integration_test.rs extern crate adder; #[test]\nfn it_adds_two() { assert_eq!(4, adder::add_two(2));\n} Listing 11-13: adder 크레이트 내의 함수에 대한 통합 테스트 코드의 상단에 extern crate adder를 추가했는데, 이는 단위 테스트에서는 필요 없었지요. 이는 tests 디렉토리 내의 각 테스트가 모두 개별적인 크레이트이라서, 우리의 라이브러리를 각각에 가져올 필요가 있기 때문입니다. tests/integration_test.rs 에는 #[cfg(test)]를 이용한 어노테이션을 해줄 필요가 없습니다. 카고는 test 디렉토리를 특별 취급하여 cargo test를 실행시켰을 때에만 이 디렉토리 내의 파일들을 컴파일합니다. 이제 cargo test 실행을 시도해봅시다: $ cargo test Compiling adder v0.1.0 (file:///projects/adder) Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs Running target/debug/deps/adder-abcabcabc running 1 test\ntest tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running target/debug/deps/integration_test-ce99bcc2479f4607 running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 출력에 세 개의 섹션이 생겼습니다: 단위 테스트, 통합 테스트, 그리고 문서 테스트입니다. 단위 테스트를 위한 첫 번째 섹션은 우리가 봐오던 것과 동일합니다: 각각의 단위 테스트마다 한 라인 (Listing 11-12에서 우리가 추가한 intenal이라는 이름의 것이 있었죠), 그다음 단위 테스트들의 정리 라인이 있습니다. 통합 테스트 섹션은 Running target/debug/deps/integration-test-ce99bcc2479f4607 이라고 말하는 라인과 함께 시작합니다 (여러분의 출력 값 끝의 해쉬값은 다를 것입니다). 그다음 이 통합 테스트 안의 각 테스트 함수를 위한 라인이 있고, Doc-tests adder 섹션이 시작되기 직전에 통합 테스트의 결과를 위한 정리 라인이 있습니다. 어떠한 src 파일에 단위 테스트 함수를 더 추가하는 것이 단위 테스트 섹션의 테스트 결과 라인을 더 늘린다는 점을 상기하세요. 통합 테스트 파일에 테스트 함수를 더 추가하는 것은 그 파일의 섹션의 라인을 더 늘릴 것입니다. 각 통합 테스트 파일은 고유의 섹션을 가지고 있으므로, 만일 우리가 tests 디렉토리에 파일을 더 추가하면, 통합 테스트 섹션이 더 생길 것입니다. cargo test의 인자로서 테스트 함수의 이름을 명시하는 식으로 특정 통합 테스트 함수를 실행시키는 것도 여전히 가능합니다. 특정한 통합 테스트 파일 내의 모든 테스트를 실행시키기 위해서는, cargo test에 파일 이름을 뒤에 붙인 --test 인자를 사용하세요: $ cargo test --test integration_test Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running target/debug/integration_test-952a27e0126bb565 running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 이 커맨드는 tests/integration_test.rs 내의 테스트만 실행합니다. 통합 테스트 내의 서브모듈 더 많은 통합 테스트를 추가하게 되면, 이들을 조직화하기 쉽도록 tests 디렉토리 내에 하나 이상의 파일을 만들고 싶어 할지도 모릅니다; 예를 들면, 여러분은 이들이 테스트하는 기능별로 테스트 함수들을 묶을 수 있습니다. 앞서 언급했듯이, tests 디렉토리 내의 각 파일은 고유의 개별적인 크레이트인 것처럼 컴파일됩니다. 각 통합 테스트 파일을 고유한 크레이트인 것 처럼 다루는 것은 여러분의 크레이트를 이용하게 될 사용자들의 방식과 더 유사하게 분리된 스코프를 만들어 내기에 유용합니다. 하지만, 이는 src 내의 파일들이 동일한 동작을 공유하는 것을 tests 디렉토리 내의 파일들에서는 할 수 없음을 의미하는데, 이는 여러분이 7장에서 코드를 모듈과 파일로 나누는 법에 대해 배웠던 것입니다. 만일 여러분이 여러 개의 통합 테스트 파일들 내에서 유용하게 사용될 헬퍼 함수들 묶음을 가지고 있으며, 이들을 공통 모듈로 추출하기 위해 7장의 \"모듈을 다른 파일로 옮기기\"절에 있는 단계를 따르는 시도를 한다면, 이러한 tests 디렉토리 내의 파일에 대한 이색적인 동작 방식은 가장 주목할 만 점입니다. 이를테면, 만일 우리가 tests/common.rs 이라는 파일을 만들어서 그 안에 아래와 같이 setup이라는 이름의 함수를 위치시키고, 여기에 여러 테스트 파일들 내의 여러 테스트 함수로부터 호출될 수 있기를 원하는 어떤 코드를 집어넣는다면: Filename: tests/common.rs pub fn setup() { // 여러분의 라이브러리 테스트에 특화된 셋업 코드가 여기 올 것입니다\n} 만약 테스트를 다시 실행시키면, 비록 이 코드가 어떠한 테스트 함수도 담고 있지 않고, setup 함수를 다른 어딘가에서 호출하고 있지 않을지라도, common.rs 파일을 위한 테스트 출력 내의 새로운 섹션을 보게 될 것입니다: running 1 test\ntest tests::internal ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running target/debug/deps/common-b8b07b6f1be2db70 running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Running target/debug/deps/integration_test-d993c68b431d39df running 1 test\ntest it_adds_two ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests adder running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out running 0 tests이 표시되는 테스트 출력이 보이는 common을 만드는 건 우리가 원하던 것이 아닙니다. 우리는 그저 다른 통합 테스트 파일들에서 어떤 코드를 공유할 수 있기를 원했지요. common이 테스트 출력에 나타나는 것을 막기 위해서는, tests/common.rs 을 만드는 대신, tests/common/mod.rs 를 만듭니다. 7장의 \"모듈 파일 시스템의 규칙\"절에서 서브모듈을 가지고 있는 모듈의 파일들을 위해 module_name/mod.rs 라는 이름 규칙을 이용했었고, 여기서 common에 대한 서브모듈을 가지고 있지는 않지만, 이러한 방식으로 파일명을 정하는 것이 러스트에게 common 모듈을 통합 테스트 파일로 취급하지 않게끔 전달해줍니다. setup 함수 코드를 tests/common/mod.rs 로 옮기고 tests/common.rs 파일을 제거하면, 테스트 출력에서 해당 섹션이 더 이상 나타나지 않을 것입니다. tests 디렉토리의 서브 디렉토리 내의 파일들은 개별적인 크레이트처럼 컴파일되지도, 테스트 출력의 섹션을 갖지도 않습니다. tests/common/mod.rs 를 만든 뒤에는, 어떤 통합 테스트 파일에서라도 이를 모듈처럼 쓸 수 있습니다. 아래에 tests/integration_test.rs 내에 it_adds_two 테스트로부터 setup 함수를 호출하는 예제가 있습니다: Filename: tests/integration_test.rs extern crate adder; mod common; #[test]\nfn it_adds_two() { common::setup(); assert_eq!(4, adder::add_two(2));\n} mod common; 선언은 Listing 7-4에서 보여주었던 모듈 선언과 동일한 점을 주목하세요. 그런 다음 테스트 함수 내에서 common::setup() 함수를 호출 할 수 있습니다. 바이너리 크레이트를 위한 통합 테스트 만약 우리의 프로젝트가 src/lib.rs 파일이 없고 src/main.rs 파일만 갖고 있는 바이너리 프로젝트라면, tests 디렉토리 내에 통합 테스트를 만들어서 src/main.rs 에 정의된 함수를 가져오기 위하여 extern crate를 이용할 수 없습니다. 오직 라이브러리 크레이트만 다른 크레이트에서 호출하고 사용할 수 있는 함수들을 노출시킵니다; 바이너리 크레이트는 그 스스로 실행될 것으로 여겨집니다. 이는 바이너리를 제공하는 러스트 프로젝트들이 src/lib.rs 에 위치한 로직을 호출하는 간단한 형태의 src/main.rs 를 가지고 있는 이유 중 하나입니다. 이러한 구조와 함께라면, extern crate를 이용하여 중요한 기능들을 커버하도록 하기 위해 통합 테스트가 라이브러리 크레이트를 테스트할 수 있습니다 . 만일 중요 기능이 작동한다면, src/main.rs 내의 소량의 코드 또한 동작할 것이고, 이 소량의 코드는 테스트할 필요가 없습니다.","breadcrumbs":"테스팅 » 테스트 조직화 » 통합 테스트","id":"202","title":"통합 테스트"},"203":{"body":"러스트의 테스트 기능은 코드를 변경하더라도 계속하여 우리가 기대한 대로 동작할 것이라는 확신을 주기 위하여 코드가 어떻게 기능하는지 명시하는 방법을 제공합니다. 단위 테스트는 라이브러리의 서로 다른 부분을 개별적으로 시험하며 비공개된 구현 세부사항을 테스트할 수 있습니다. 통합 테스트는 라이브러리의 많은 부분이 함께 작동하는 사용 상황을 다루며, 외부 코드가 사용하게 될 똑같은 방식대로 테스트하기 위해 그 라이브러리의 공개 API를 이용합니다. 비록 러스트의 타입 시스템과 소유권 규칙이 몇 가지 종류의 버그를 방지하는데 도움을 줄지라도, 테스트는 여러분의 코드가 어떻게 동작하기를 기대하는지와 함께 해야 하는 논리 버그를 잡는 일을 도와주는 데에 있어 여전히 중요합니다. 이 장과 이전 장들의 지식을 합쳐서 다음 장의 프로젝트 작업을 해봅시다!","breadcrumbs":"테스팅 » 테스트 조직화 » 정리","id":"203","title":"정리"},"204":{"body":"이 장에서 우리는 지금까지 배운 많은 내용을 요약 정리하고 몇 가지 표준 라이브러리 기능을 탐색하고자 합니다. 현재 우리가 보유한 러스트 실력을 연습하기 위한 커맨드 라인 툴을 만들고 파일, 커맨드 라인 입출력 작업을 해보게 될 것 입니다. 러스트는 성능, 안전성, '단일 바이너리'로 출력, 그리고 교차 플랫폼 지원으로 커맨드 라인 툴을 제작하기 좋은 언어입니다. 그러니 우리는 고전적인 커맨드 라인 툴 grep을 우리 자체 버전으로 만들어 볼 것입니다. Grep은 \"정규 표현식 검색 및 인쇄\"의 약어 입니다. grep의 간단한 사용 예로 다음의 단계를 거쳐 지정된 파일에서 지정된 문자를 검색합니다. 인자로 파일 이름과 문자를 취합니다. 파일을 읽어들입니다. 문자 인자를 포함하는 파일의 행들을 찾습니다. 해당 라인들을 표시합니다. 우리는 또한 환경 변수를 사용하는 방법과 표준 출력 대신 표준 에러로 표시하는 방법을 다루고자 합니다. 이러한 기법들은 일반적으로 커맨드 라인 도구들에서 사용됩니다. 한 러스트 커뮤니티 멤버인 Andrew Gallant가 이미 grep의 전체 기능이 구현됐으면서도 월등히 빠른 ripgrep을 만들었습니다. 이에 비해 우리의 grep은 훨씬 간단하게 만들 것 입니다, 이번 장에서 ripgrep과 같은 실제 프로젝트를 이해하는데 필요한 배경지식을 제공합니다. 이 프로젝트는 우리가 지금까지 학습한 다양한 개념을 종합하게 될 겁니다: 조직화된 코드 (7장 모듈 편에서 배운 내용) 벡터와 문자열의 사용 (8장 콜렉션) 에러 처리 (9장) 특성과 생명주기를 적절히 사용하기 (10장) 테스트 작성 (11장) 또한 우리는 클로저, 반복자, 특성 개체를 간단히 소개하고자 합니다. 이는 13장과 17장에서 상세히 다룰 겁니다. 언제나처럼 cargo new를 통해 새로운 프로젝트를 생성합시다. 새 프로젝트의 이름을 greprs로 이름 지어서 시스템에 이미 존재하는 grep와 구분짓도록 하겠습니다: $ cargo new --bin greprs Created binary (application) `greprs` project\n$ cd greprs","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » I/O 프로젝트: 커맨드 라인 프로그램 만들기","id":"204","title":"I/O 프로젝트: 커맨드 라인 프로그램 만들기"},"205":{"body":"우리의 첫 번째 작업은 greprs가 두 개의 커맨드라인 인자를 받을 수 있도록 하는 것 입니다: 파일이름과 검색할 문자. 즉, cargo run을 통해 함께 우리의 프로그램을 수행시킬 때, 검색할 문자와 검색할 파일의 경로를 사용할 수 있도록 하고자 합니다, 다음처럼 말이죠: $ cargo run searchstring example-filename.txt 현재로서는, cargo new를 통해 생성된 프로그램은 우리가 입력한 인자를 모두 무시합니다. crates.io 라이브러리에 커맨드라인 인자들을 받아들이도록 도와줄 라이브러리가 이미 존재하지만, 우리 스스로 이를 구현해봅시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 커맨드 라인 인자 허용하기 » 커맨드라인 인자 허용하기","id":"205","title":"커맨드라인 인자 허용하기"},"206":{"body":"우리 프로그램에 전달된 커맨드라인 인자의 값을 얻으려면 Rust의 표준 라이브러리에서 제공되는 함수를 호출해야합니다: std::env::args. 이 함수는 반복자(iterator) 형식으로 커맨드라인 인자들을 우리 프로그램에 전달해줍니다. 우리는 아직 반복자에 대해 다루지 않았고, 13장에서 다룰 예정이니, 지금은 반복자에 대해서 두 가지 성질만 알고 갑시다. 반복자는 하나의 연속된 값을 생성합니다. 반복자에 collect 함수 호출을 통해 반복자가 생성하는 일련의 값을 벡터로 변환할 수 있습니다. 한번 해볼까요? 항목 12-1의 코드를 사용하여 모든 커맨드라인 인자들을 벡터 형태로 greprs로 전달해봅시다. Filename: src/main.rs use std::env; fn main() { let args: Vec = env::args().collect(); println!(\"{:?}\", args);\n} 항목 12-1: 커맨드라인 인자를 벡터 형태로 모으고 그들을 출력하기. 가장 먼저, 우리는 std::env 모듈을 use를 통해 모듈 범위 내로 가져와서 그 안의 args 함수를 호출할 수 있도록 합니다. std::env::args 함수는 두 단계 모듈들로 중첩된 호출임을 주지하세요. 7장에서 우리가 이야기 나눴듯이, 원하는 함수가 두 개 이상의 중첩된 모듈에 있는 경우에는 함수 자체가 아닌 부모 모듈을 범위로 가져오는게 일반적입니다. 이런 방식은 우리가 std::env의 다른 함수를 사용하기 용이하도록하며 덜 모호합니다. use std::env::args;를 사용하여 args처럼 함수를 호출하면 현재 모듈에 이 함수가 정의된 것처럼 착각할 수 있습니다. 참고: 어떤 인자가 잘못된 유니코드를 포함하고 있으면 std::env::args는 패닉을 발생합니다. 유효하지 않은 유니코드를 포함한 인자를 허용해야 하는 경우에는 std::env::args_os를 대신 사용하도록 하세요. 이 함수는 String대신 OsString 값을 반환합니다. OsString 값은 플랫폼마다 다르며 String 값보다 다루기가 더 복잡하기 때문에 여기서는 std::env::args를 사용하여 좀더 단순화 했습니다. main의 첫 번째 줄에서, 우리가 호출한 env::args, 그리고 동시에 사용한 collect는 반복자가 가진 모든 값들을 벡터 형태로 변환하여 반환합니다. collect 함수는 많은 종류의 콜렉션들과 사용될 수 있기 때문에, 우리가 원하는 타입이 문자열 벡터라고 args의 타입을 명시합니다. Rust에서 타입 명시를 할 필요는 거의 없지만, Rust는 우리가 원하는 콜렉션의 타입을 추론 할 수 없기 때문에 collect는 타입을 명시할 필요가있는 함수 중 하나입니다. 마지막으로, 우리는 디버그 형식자인 :?으로 벡터를 출력합니다. 인자 없이, 그리고 두 개의 인자들로 우리의 코드를 실행시켜 봅시다. $ cargo run\n[\"target/debug/greprs\"] $ cargo run needle haystack\n...snip...\n[\"target/debug/greprs\", \"needle\", \"haystack\"] 벡터의 첫 번째 값은 바이너리의 이름 인 \"target / debug / minigrep\"입니다. 이것은 C에서 인수 목록의 동작을 일치시키고 프로그램은 실행시 호출 된 이름을 사용하게합니다. 메시지를 인쇄하거나 프로그램을 호출하는 데 사용 된 명령 줄 별칭을 기반으로 프로그램의 동작을 변경하려는 경우 프로그램 이름에 액세스하는 것이 편리하지만이 장의 목적을 위해 무시할 것입니다 우리가 필요로하는 두 개의 인자만 저장하면됩니다. 벡터의 첫 값이 \"target/debug/greprs\"으로 바이너리의 이름임을 알 수 있습니다. 왜 그런지에 대한 내용은 이번 장을 넘어가니, 우리가 필요한 두 인자를 저장하였음을 기억하면 되겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 커맨드 라인 인자 허용하기 » 인자값 읽어들이기","id":"206","title":"인자값 읽어들이기"},"207":{"body":"인자값들이 들어있는 벡터의 값들을 출력하는 것을 통해 우리의 프로그램에서 커맨드라인 인자의 원하는 값에 접근하는 것이 가능하다는 것을 상상할 수 있습니다. 다음은 정확히 우리가 원하는 방식이 아니지만, 두개의 인자값을 변수로 저장하여 그 값들을 우리의 프로그램에서 사용할 수 있도록 합니다. 항목 12-2대로 해봅시다: Filename: src/main.rs use std::env; fn main() { let args: Vec = env::args().collect(); let query = &args[1]; let filename = &args[2]; println!(\"Searching for {}\", query); println!(\"In file {}\", filename);\n} 항목 12-2: 쿼리와 파일이름 인자를 보관하는 두 변수를 만듭니다. 우리가 벡터를 출력했을 때 봤듯이, 프로그램의 이름이 벡터의 첫 번째 값으로 args[0]에 저장되어 있으니, 우리는 1번째 색인부터 접근하면 됩니다. greprs의 첫 번째 인자는 검색하고자 하는 문자열이므로, 우리는 첫 번째 인자의 참조자를 query에 저장합니다. 두 번째 인자는 파일이름이니, 두 번째 인자의 참조자를 변수 filename에 저장합니다. 임시적으로 우리는 이 값들을 단순 출력하고 있으니, 우리의 코드가 우리가 원하는 방식으로 동작하고 있다는 것을 증명하기 위해, 이 프로그램을 test와 sample.txt를 인자로 주고 다시 실행해봅시다: $ cargo run test sample.txt Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/greprs test sample.txt`\nSearching for test\nIn file sample.txt 훌륭하게, 동작하네요! 우리는 인자 값들을 우리가 원하는 변수에 정확히 저장했습니다. 후에 사용자가 아무런 인자를 넣지 않은 상황을 다루기 위해 오류처리를 추가해볼 겁니다. 하지만 당장은 그것보다 파일 읽기 기능을 추가해봅시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 커맨드 라인 인자 허용하기 » 변수에 인자 값들 저장하기","id":"207","title":"변수에 인자 값들 저장하기"},"208":{"body":"다음으로, 우리는 커맨드 라인 인자 파일이름으로 지정된 파일을 읽어볼 겁니다. 먼저, 함께 테스트 할 샘플 파일이 필요합니다. 'greprs'가 동작하는 것을 확신할 수 있기 위해 가장 좋은 종류의 파일은 몇 개의 반복되는 단어의 다수의 줄에 걸쳐 존재하는 작은 양의 텍스트입니다. 항목 12-3의 에밀리 딕킨스 시는 잘 작동할 겁니다. poem.txt로 명명된 파일을 당신의 프로젝트 최상위에 생성하고 시를 입력합시다 \"I'm nobody! Who are you?\": Filename: 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: 테스트 용으로 적합한 에밀리 딕킨슨의 시 \"I'm nobody! Who are you?\" 언급된 위치에 위의 파일을 생성한 후, src/main.rs 파일을 아래 항목 12-4의 내용을 참고하여 편집합니다. Filename: src/main.rs use std::env;\nuse std::fs::File;\nuse std::io::prelude::*; fn main() { let args: Vec = env::args().collect(); let query = &args[1]; let filename = &args[2]; println!(\"Searching for {}\", query); println!(\"In file {}\", filename); let mut f = File::open(filename).expect(\"file not found\"); let mut contents = String::new(); f.read_to_string(&mut contents).expect(\"Something went wrong reading the file\"); println!(\"With text:\\n{}\", contents);\n} 항목 12-4: 두 번째 인자로 특정된 파일의 내용 읽어들이기 먼저, 우리는 use문 몇 개를 추가하여 표준 라이브러리에서 관련 있는 부분을 가져옵니다: 우리는 파일 관련하여 std::fs::File과, 파일 I/O를 포함한 I/O 작업을 위해 유용한 다양한 특성이 있는 std::io::prelude::*이 필요합니다. Rust가 가진 지정된 것들을 영역 내로 가져오는 일반적인 도입부와 동일하게, std::io 모듈은 당신이 I/O 작업을 할 때 필요할만한 일반적인 것들에 대한 그 자신만의 도입부를 갖습니다. 기본적인 도입부와는 다르게, 우리는 반드시 std::io의 도입부를 명시적으로 use해야 합니다. main에서, 우리는 다음 세 가지를 추가했습니다: 첫 째, File::open함수를 호출하고 filename값을 전달하여 파일을 변경할 수 있는 핸들을 얻습니다. 두 번째로, contents라는 이름의 빈 String 가변 변수를 만들었습니다. 이 변수는 우리가 읽어들인 내용을 보관하기 위한 용도로 사용될 겁니다. 셋 째, 우리가 만들어 둔 파일 핸들에 read_to_string을 호출하여 가변 참조를 contents의 인자로 전달합니다. 이후, 임시로 println!을 추가하여 contents의 값을 출력함으로서 파일을 읽어들인 이후 우리 프로그램이 제대로 동작했는지 확인할 수 있습니다. 아무 문자나 첫 번째 커맨드라인 인자로 입력하고(우리가 아직 검색 부분을 구현하지 않았기 때문에) 두 번째는 우리가 만들어 둔 poem.txt 파일로 입력하여 이 코드를 실행해봅시다. $ cargo run the poem.txt Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/greprs 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":"208","title":"파일 읽기"},"209":{"body":"우리 프로그램을 향상시키기 위해 네 가지 수정하고 싶은 문제가 있는데, 이들은 프로그램을 구조화하고 발생가능한 에러를 처리하는 방식과 관련있습니다. 첫 번째, 우리 main 함수는 현재 두 가지 작업을 수행합니다: 인자들을 분석하고 파일을 열지요. 이런 작은 함수에서, 이건 큰 문제가 안됩니다. 하지만 우리가 계속해서 main함수 안에 프로그램을 작성하여 커지게 되면, main 함수가 처리하는 작업의 수도 늘어나게 될 겁니다. 함수가 갖게되는 책임들만큼, 근원을 파악하기도, 테스트 하기에도, 부분 별로 나누지 않고는 수정하기도 어려워 집니다. 함수는 나뉘어 하나의 작업에 대해서만 책임을 지는 것이 더 좋은 구조입니다. 이 문제는 우리의 두 번째 문제와도 관련이 있습니다: query 와 filename 은 프로그램의 설정을 저장하는 변수이고 f 와 contents 같은 변수는 프로그램의 논리 수행에 사용됩니다. main이 길어질수록 범위 내에 더 많은 변수가 생깁니다. 범위 내에 더 많은 변수가 존재할수록, 각각의 변수를 추적하기 힘들어집니다. 목적을 분명히 하기 위해 설정 변수를 그룹화하여 하나의 구조로 결합시키는 것이 좋습니다. 세 번째 문제는 파일 열기가 실패 할 경우expect를 사용하여 오류 메시지를 출력해주는데, 에러 메시지가 Something went wrong reading the file 밖에 없습니다. 파일이 존재하지 않는 경우 외에도 파일 열기가 실패하는 경우들이 있습니다. 예를 들어 파일은 존재하지만 파일을 열 수있는 권한이 없을 수 있습니다. 현재는 이런 상황에도 Something went wrong reading the file 이란 오류 메시지를 출력하여 사용자에게 잘못된 조언을 해주게 됩니다. 넷째, 우리는 서로 다른 오류를 다루기 위해 expect를 반복적으로 사용하고 있습니다. 헌데 만약 사용자가 충분한 인수를 지정하지 않고 프로그램을 실행하면 Rust의 \"index out of bounds\" 오류가 발생하는데 이는 문제를 명확하게 설명하지 않습니다. 우리가 모든 오류처리 코드를 한 군데 모아놓으면 후에 관리자는 오류처리 로직을 변경해야 할 때 오직 이 곳의 코드만 참고하면 되니 더 좋죠. 또한, 모든 오류 처리 코드를 한 곳에 저장하면 우리가 최종 사용자에게 도움이 되는 메시지를 출력하고 있는지 확신하는데도 도움이 됩니다. 이런 문제들을 우리 프로젝트를 리팩토링하여 해결해보도록 하겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러처리의 향상을 위한 리팩토링 » 모듈성과 에러처리의 향상을 위한 리팩토링","id":"209","title":"모듈성과 에러처리의 향상을 위한 리팩토링"},"21":{"body":"여러분의 러스트 코드를 저장하기 위한 디렉토리를 만드는 것으로 시작할 것입니다. 여러분의 코드가 어디에 있는지는 러스트에게 문제가 되지 않습니다만, 이 책의 예제 및 프로젝트들을 위해서, 우리는 여러분의 홈 디렉토리에 projects 디렉토리를 만들고 모든 프로젝트를 그곳에 유지하는 것을 제안합니다. 터미널을 열고 다음 커맨드를 입력하여 projects 디렉토리를 만들고 projects 디렉토리 내에 “Hello, world!” 프로젝트를 위한 디렉토리를 만드세요. Linux와 macOS에서는 다음을 입력하세요: $ 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 Windows 파워쉘에서는 다음을 입력하세요: > mkdir $env:USERPROFILE\\projects\n> cd $env:USERPROFILE\\projects\n> mkdir hello_world\n> cd hello_world","breadcrumbs":"시작하기 » Hello, World! » 프로젝트 디렉토리 만들기","id":"21","title":"프로젝트 디렉토리 만들기"},"210":{"body":"main 함수가 여러 작업에 책임을 갖게 되는 구조적 문제는 많은 바이너리 프로젝트에서 공통적입니다. 그래서 Rust 커뮤니티는 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 에는 읽어서 옳바른지 여부를 검증할 수 있을 정도로 적은 코드만을 남겨두도록 합니다. 다음의 과정을 거치며 재작업을 해봅시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러처리의 향상을 위한 리팩토링 » 바이너리 프로젝트를 위한 관심사의 분리","id":"210","title":"바이너리 프로젝트를 위한 관심사의 분리"},"211":{"body":"먼저 우리는 커맨드라인 인자를 분석하는 기능을 추출할 겁니다. 항목 12-5에서 main의 시작 부분이 새로운 함수 parse_config를 호출하는 것을 볼 수 있을텐데, 이는 아직은 src/main.rs 에 정의되어 있을 겁니다. Filename: src/main.rs fn main() { let args: Vec = env::args().collect(); let (query, filename) = parse_config(&args); // ...snip...\n} fn parse_config(args: &[String]) -> (&str, &str) { let query = &args[1]; let filename = &args[2]; (query, filename)\n} Listing 12-5: Extract a parse_config function from main 우리는 아직 커맨드라인 인자들을 벡터로 수집하고 있는데, 인덱스 1의 인수 값을 변수 query 에, 인덱스 2의 인수 값을 main 함수 내의 변수 filename에 할당하는 대신에 전체 벡터를 parse_config 함수로 전달합니다. parse_config 함수는 어디에 위치한 인자가 어떤 변수에 대입되는지에 대한 로직을 보유하고, 그 값들을 main으로 되돌려 줍니다. 우리는 여전히 query와 filename변수를 main에 생성하지만, main은 더 이상 커맨드라인 인자와 변수간의 상관 관계를 책임지지도 알아야 할 필요도 없죠. 이것이 우리가 작은 프로그램을 유지하기 위한 과도한 행동으로 보일수도 있지만, 우리는 조금씩 점진적으로 리팩토링을 진행하고 있습니다. 이런 변화를 준 뒤에는, 프로그램을 다시 실행해 인자의 파싱이 정상적으로 동작하고 있는지 확인해보십시오. 진행 상황을 자주 확인하면 문제가 생겼을 때 원인을 파악하는데 도움이 됩니다. 설정 변수들을 그룹짓기 우리는 이 함수의 기능을 더 향상시키기 위해 또 다른 작은 행동을 할 수 있습니다. 현재 우리는 튜플을 반환하고 있는데, 그 시점에 즉시 튜플을 개별된 부분으로 나눌 수가 없습니다. 이는 우리가 아직은 제대로 된 추상화를 하지 못하고 있다는 신호일 수 있습니다. 또 다른 의미로는 config의 부분인 parse_config에 향상시킬 지점이 있다는 것으로, 우리가 반환하는 두 개의 값은 관련되어 있으며 모두 하나의 설정 값에 대한 부분이죠. 우리는 현재 두 값을 튜플로 그룹화하는 것 이외의 다른 의미를 전달하지 않습니다. 두 값을 하나의 구조체에 넣고 각 구조체 필드에 의미있는 이름을 지정할 수 있습니다. 이렇게 하면 이 코드의 향후 유지 보수 담당자가 서로 다른 값이 서로 어떻게 관련되어 있고 그 목적이 무엇인지 쉽게 이해할 수 있습니다. 주의: 어떤 사람들은 복합 타입(complex type)이 더 적절할 경우에도 기본 타입(primitive type)을 사용하는데 이러한 안티 패턴을 강박적 기본타입 사용(primitive obsession) 이라 부릅니다 항목 12-6에서 query와 filename을 필드로 갖는 Config란 구조체 정의가 추가된 것을 볼 수 있습니다. 우리는 또한 parse_config 함수를 변경하여 Config 구조체의 객체를 반환하게 변경하였으며, main에서 별개의 변수가 아닌 구조체의 필드를 사용하도록 변경했습니다. Filename: src/main.rs # use std::env;\n# use std::fs::File;\n#\nfn main() { let args: Vec = env::args().collect(); let config = parse_config(&args); println!(\"Searching for {}\", config.query); println!(\"In file {}\", config.filename); let mut f = File::open(config.filename).expect(\"file not found\"); // ...snip...\n} struct Config { query: String, filename: String,\n} fn parse_config(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename }\n} Listing 12-6: Refactoring parse_config to return an instance of a Config struct 이제 parse_config의 선언은 Config 값을 반환한다는 것을 알려줍니다. parse_config 의 내부에서는 args의 String값을 참조하는 문자열 조각을 반환했었지만, 이제는 Config를 정의하고 자체 String의 값을 포함하도록 선택했습니다. main의 args변수는 인자 값들의 소유주로 parse_config에는 그들을 대여해줄 뿐 입니다. 그렇기에 만약 Config가 args의 값들에 대한 소유권을 가지려고 시도하면 Rust의 대여 규칙을 위반하게 됩니다. 우리가 String 데이터를 관리하는 방식은 여러가지가 있겠습니다만, 가장 쉽고 약간 비효율적인 방법은 clone 메소드를 호출하는 겁니다. 이 방식은 Config 객체에서 소유하게 할 data 전체에 대한 복사본을 만들 것이며, 이런 방식은 참조만 보관하는 것에 비해 약간 더 많은 비용과 메모리가 소비됩니다. 하지만 데이터의 복제본을 만드는 방식은 우리가 참조의 생명주기를 관리하지 않아도 되기 때문에 우리의 코드를 매우 직관적이게 합니다. 그래서 이런 상황에서는 약간의 성능을 포기하고 간소함을 유지하는 것이 매우 가치있는 거래입니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러처리의 향상을 위한 리팩토링 » 인자 파서의 추출","id":"211","title":"인자 파서의 추출"},"212":{"body":"많은 Rust 사용자들은 런타임 비용 때문에 소유권 문제를 수정하기 위해 clone을 사용하지 않는 경향이 있습니다. 13장 이터레이터에서, 이런 상황에서보다 효율적인 메소드를 사용하는 법을 배우겠지만, 지금은 한 번만 clone하며 query와 filename이 매우 작기 때문에 몇 개의 문자열을 clone하여 진행하는 것이 좋습니다. 첫 번째 단계에서는 코드를 최대한 최적화하는 것보다 약간 비효율적이더라도 넘어가는게 좋습니다. Rust에 대한 경험이 많을수록 바람직한 방법으로 곧장 진행할 수 있을 겁니다. 지금은 clone을 호출하는 것이 완벽한 선택입니다. parse_config에 의해 반환된 Config의 객체를 config라는 변수에 넣고 이전에 별도로 query와 filename이란 이름으로 나뉘어 있던 변수 대신 Config 구조체의 필드를 사용하도록 main을 업데이트했습니다. 우리의 코드는 이제 보다 분명하게 query와 filename이 연관되어 있으며 이들의 목적이 프로그램이 어떻게 동작할지에 대한 설정이라는 의도를 전달할 수 있습니다. 이 값을 사용하는 모든 코드는 그들의 의도에 맞게 지정된 필드를 config 객체에서 찾을 수 있습니다. Config를 위한 생성자 만들기. 지금까지 우리는 main에서 parse_config함수로 커맨드라인 인자를 파싱하는 로직을 추출했습니다. 이를 통해 우리 코드에서 query와 filename값이 연관되어 있고 그 연결성이 전달되어야 한다는 것을 알았습니다. 그래서 우리는 Config 구조체를 추가하고 그 의도와 목적에 맞게 query와 filename을 명명했으며 parse_config 함수에서 변수의 이름을 구조체 필드 이름으로 반환 할 수 있게 했습니다. 그래서 이제 parse_config 함수의 목적은 Config 객체를 생성하는 것인데, 우리는 parse_config라는 평범한 함수를 Config 구조체와 관련된 new라는 함수로 변경 할 수 있습니다. 이런 변경은 우리의 코드를 보다 자연스럽게 만들어 줍니다:String::new를 호출하여 String형의 객체를 생성하는 것처럼 표준 라이브러리들의 객체를 생성할 수 있습니다. 그리고 parse_config를 Config와 연관된 new함수로 변경하게 되면, 우리는 Config의 객체를 Config::new를 호출하여 생성할 수 있게 됩니다. 항목 12-7는 우리가 해야할 변동사항 보여줍니다. Filename: src/main.rs # use std::env;\n#\nfn main() { let args: Vec = env::args().collect(); let config = Config::new(&args); // ...snip...\n} # struct Config {\n# query: String,\n# filename: String,\n# }\n#\n// ...snip... impl Config { fn new(args: &[String]) -> Config { let query = args[1].clone(); let filename = args[2].clone(); Config { query, filename } }\n} Listing 12-7: Changing parse_config into Config::new 우리는 main을 갱신하여 parse_config를 호출하는 대신 Config::new를 호출하게 되었습니다. 우리는 parse_config의 이름을 new로 바꾸고 그를 impl블록 안으로 옮겼는데, 이를 통해 new함수가 Config와 연결되게 됩니다. 다시 컴파일을 하고 제대로 동작하는지 확인해보도록 합시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러처리의 향상을 위한 리팩토링 » clone 사용의 기회비용","id":"212","title":"clone 사용의 기회비용"},"213":{"body":"이번에는 우리의 에러 처리를 수정해 볼 겁니다. 만일 args 벡터가 3개 미만의 아이템을 가지고 있을 때 인덱스 2 혹은 3의 값에 접근하려는 시도를 하면 프로그램은 패닉을 일으키게 된다고 했던 것을 상기시켜 드립니다. 프로그램을 인자 없이 실행해보시면; 다음같이 될 겁니다. $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/greprs`\nthread 'main' panicked at 'index out of bounds: the len is 1\nbut the index is 1', /stable-dist-rustc/build/src/libcollections/vec.rs:1307\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. index out of bounds: the len is 1 but the index is 1 줄은 프로그래머를 위해 의도된 에러 메시지이지, 최종 사용자에게는 무슨 일이 있었는지 무엇을 해야 하는지 이해하는데 아무런 도움이 되지 않습니다. 당장 한번 고쳐보겠습니다. 에러 메시지 향상시키기 항목 12-8에서 new함수에 검사를 추가하여 인덱스 1과 2에 접근하기 전에 조각이 충분한 길이인지를 확인합니다. 조각이 충분히 길지 않다면, 프로그램은 더 좋은 에러메시지 index out of bounds를 보여주고 패닉을 일으킵니다: Filename: src/main.rs // ...snip...\nfn new(args: &[String]) -> Config { if args.len() < 3 { panic!(\"not enough arguments\"); } // ...snip... 항목 12-8: 인자의 숫자가 몇 개인지 검증 추가 이것은 항목 9-8에서 작성한 Guess::new함수와 유사합니다. 이 함수는 value인수가 유효한 값의 범위를 벗어난 경우 panic!을 호출했습니다. 값의 범위를 검사하는 대신에, 우리는args의 길이가 적어도 3개인지 검사하면, 함수의 나머지 부분은 이 조건이 이미 충족되었다는 가정 하에서 동작할 수 있습니다. args가 3개 보다 적은 아이템을 가진다면, 이 조건은 true가 되고 우리는 panic! 매크로를 호출해 프로그램을 즉시 종료 시킬겁니다. 이런 몇 줄의 추가 코드들을 new상에 추가하고, 우리 프로그램을 아무 인자도 없이 다시 실행시키면 다음과 같은 에러를 볼 수 있을 겁니다. $ cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/greprs`\nthread 'main' panicked at 'not enough arguments', src/main.rs:29\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. 이 결과 더 합리적인 좋은 오류 메시지가 표시됩니다. 그러나 사용자에게 제공하고 싶지 않은 추가 정보가 있습니다. 따라서 항목 9-8에서 사용한 기술을 사용하는 것은 여기선 최선의 방법은 아닙니다. panic!에 대한 호출은 9장에서 논의했던 것처럼 사용 방법에 대한 문제가 아닌 아니라 프로그래밍 관련 문제에 더 적합합니다. 대신, 우리는 9장에서 배운 다른 기법으로 Result를 반환하는 것을 성공이나 오류를 나타낼 수 있습니다. new에서 panic!을 호출하는 대신 Result를 반환하기. 우리는 Result를 반환 값으로 선택하여 성공인 경우에는 Config 객체를 포함시키고 에러가 발생한 경우에는 문제가 무엇인지 설명할 수 있게 만들 수 있습니다. Config::new가 main과 상호작용할 시에, 우리는 Result를 사용하여 문제가 있다고 신호할 수 있습니다. 그리고main에선 Err의 값을 사용자들에게 보다 실용적인 방식으로 변환하여 보여줄 수 있습니다. thread 'main' 으로 시작하는 문자들과 panic!을 사용해서 보여지는 RUST_BACKTRACE관련 메시지 없이. 항목 12-9에서 당신이 변경해야 할 Config::new의 반환 값과 Result를 반환하기 위한 함수 본문을 보여줍니다: Filename: src/main.rs impl Config { fn new(args: &[String]) -> Result { if args.len() < 3 { return Err(\"not enough arguments\"); } let query = args[1].clone(); let filename = args[2].clone(); Ok(Config { query, filename }) }\n} 항목 12-9: Config::new에서 Result반환 우리의 new 함수는 이제 성공 시에는 Config객체가 에러 시에는 &'static str가 포함된 Result를 반환하게 됩니다. 10장의 \"The Static Lifetime\"에서 `&'static str'이 문자열 리터럴이라고 다뤘는데, 이게 현재 우리의 에러 타입입니다. 우리는 new함수의 본문에서 두 가지 변경을했습니다 : 사용자가 충분한 인수를 전달하지 않을 때 panic!을 호출하는 대신 Err값을 반환하고 Config를 반환할 때는 Ok로 포장하여 반환 합니다. 이런 변경으로 인해 함수는 새로운 타입 선언을 갖게 됩니다. Config::new가 Err값을 반환하게 함으로써, main함수는 new함수로부터 반환된 Result값을 처리하고 에러 상황에 프로세스를 더 깨끗하게 종료 할 수 있습니다. Config::new를 호출하고 에러 처리하기 에러 케이스를 처리하고 사용자-친화적인 메시지를 출력하기 위해서, 항목 12-10에서처럼 Config::new가 리턴하는 Result를 처리하기 위해 main을 갱신해야 합니다. 그리고 우리 커맨드라인 프로그램을 panic!으로 0이 아닌 값을 발생시킬 때에는 종료시켜야 하므로 직접 구현해보도록 합시다. 0이 아닌 종료 값은 우리 프로그램을 호출한 프로그램에게 우리의 프로그램이 에러 상태로 종료되었음을 알리는 규칙입니다. Filename: src/main.rs use std::process; fn main() { let args: Vec = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!(\"Problem parsing arguments: {}\", err); process::exit(1); }); // ...snip... 항목 12-10: new Config가 실패했을 때 에러 코드와 함께 종료시키기 이 목록에서 우리는 이전에 다루지 않았던 메소드를 사용하고 있습니다: unwrap_or_else는 표준 라이브러리에 의해 Result 에 정의되어 있습니다. unwrap_or_else를 사용하면 panic!이 아닌 에러 처리를 직접 정의 할 수 있습니다. Result가 Ok 값이면, 이 메소드의 동작은 unwrap과 유사합니다 : 그것은 Ok로 포장한 내부 값을 반환합니다. 그러나 Err값이면 메소드는 closure 의 코드를 호출합니다. closure 는 익명의 함수로 unwrap_or_else에 인수로 전달됩니다. 13장에서 클로저에 대해 더 자세히 다룰 것입니다. 여기서 알아 두어야 할 것은 unwrap_or_else가 Err의 내부 값, 이번 경우에는 항목 12-9에서 우리가 추가한 정적 문자열인 not enough arguments을, 수직파이프 사이에 위치하는 err로 인자로서 우리의 클로저로 전달한다는 겁니다. 클로저에 있는 코드는 이런 과정을 거쳐 실행 시에 err값을 사용할 수 있습니다. 우리는 새 use줄을 추가하여 process를 공유 라이브러리에서 import했습니다. 에러 상황에 실행될 클로저의 코드는 단 두 줄 입니다. 에러 값을 출력해주고 process::exit를 호출합니다. process::exit함수는 프로그래을 즉시 중단시키고 종료 상태 코드로 전달받은 값을 반환합니다. 이것은 항목 12-8에서 사용한 panic!기반의 처리 방식과 유사해 보이지만, 더이상 필요하지 않은 출력을 하지 않죠. 해볼까요? $ cargo run Compiling greprs v0.1.0 (file:///projects/greprs) Finished dev [unoptimized + debuginfo] target(s) in 0.48 secs Running `target/debug/greprs`\nProblem parsing arguments: not enough arguments 훌륭하네요! 이 출력은 우리 사용자들에게 훨씬 친화적입니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러처리의 향상을 위한 리팩토링 » 에러 처리 수정하기","id":"213","title":"에러 처리 수정하기"},"214":{"body":"이제 환경 설정 파싱 리팩토링을 마무리 했습니다. 우리 프로그램의 로직으로 돌아갑시다. 우리가 \"바이너리 프로젝트에서 핵심 기능의 분리\"절에서 논의한 과정에 따라, 우리는 main함수에 구성 설정 또는 오류 처리와 관계 없는 남아있는 모든 로직들을 담고있는 run함수를 추출 할 겁니다. 이 과정이 종료되면, main은 간결해져 쉽게 검증할 수 있어지고, 우리는 다른 모든 로직에 대한 테스트를 작성할 수 있을 겁니다. 항목 12-11 추출된 run 함수를 보여줍니다. 현재 우리는 함수를 추출하여 src/main.rs 에 함수를 정의하는 작고 점진적 개선만 수행하고 있습니다. Filename: src/main.rs fn main() { // ...snip... println!(\"Searching for {}\", config.query); println!(\"In file {}\", config.filename); run(config);\n} fn run(config: Config) { let mut f = File::open(config.filename).expect(\"file not found\"); let mut contents = String::new(); f.read_to_string(&mut contents).expect(\"something went wrong reading the file\"); println!(\"With text:\\n{}\", contents);\n} // ...snip... 항목 12-11: 남은 프로그램 로직을 run 함수로 추출하기 이제 run함수에는 main에 잔존하는 파일을 읽는 것부터 나머지 모든 로직이 포함됩니다. run 함수는 Config 객체를 인수로 취합니다. run 함수에서 에러 반환하기 나머지 프로그램 로직을 main이 아닌run 함수로 분리하면, Listing 12-9의 Config::new처럼 에러 처리를 향상시킬 수 있습니다. expect를 호출하여 프로그램을 패닉 상태로 만드는 대신, run함수는 무언가가 잘못되었을 때 Result 를 리턴 할 것입니다. 이러면 사용자 친화적인 방법으로 오류를 처리하는 로직을 main으로 통합 할 수 있습니다. 항목 12-12는 run의 선언부와 본문의 변경 사항을 보여줍니다. Filename: src/main.rs use std::error::Error; // ...snip... fn run(config: Config) -> Result<(), Box> { let mut f = File::open(config.filename)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; println!(\"With text:\\n{}\", contents); Ok(())\n} 항목 12-12: run 함수가 Result를 반환하게 바꾸기 우리는 여기서 세 가지 큰 변화를 만들었습니다. 먼저, run 함수의 리턴 타입을 Result <(), Box >로 바꿨습니다. 이 함수는 이전에 유닛 타입 ()을 반환했으며, 우리는 Ok의 경우 반환할 값으로 이 타입을 유지합니다. 우리의 에러 타입으로, 특성 오브젝트 Box 를 사용합니다 (그리고 상단에 use문으로 std::error::Error를 범위 내로 임포트 해왔습니다). 우리는 특성 오브젝트들을 17장에서 다룰 것입니다. 지금 당장은, Box는 함수가 Error 특성을 구현하는 타입을 반환한다는 것만 알면 되고, 특별히 어떤 타입이 반환될지에 대해서는 알 필요 없습니다. 이런 방식은 다양한 에러 상황에 다른 타입의 오류 값을 반환 할 수 있는 유연성을 확보할 수 있습니다. dyn은 \"dynamic\"의 약자입니다. 우리가 만든 두 번째 변화는 우리가 9 장에서 이야기했듯이, ?에 대한 expect에 대한 호출을 제거한 것입니다. 에러 시에 panic!을 호출하는 것보다 현재 함수에서 에러 값을 반환하며 호출자가 처리 할 수 ​​있도록 하였습니다. 셋째, 이 함수는 성공 사례에서 Ok값을 반환합니다. 우리는 run 함수의 성공 타입을 선언부에서 ()로 선언했습니다, 이것은 우리가 유닛 타입 값을 Ok 값으로 감쌀 필요가 있음을 의미합니다. 이 Ok (())구문은 조금 이상하게 보일 수 있지만, ()를 사용하는 것과 마찬가지로 이는 사이드이펙트 없이 run을 호출하는 것을 나타내는 관용적인 방법입니다. 우리가 필요로 하는 값을 반환하지 않습니다. 실행시키면, 컴파일 될텐데, 경고를 보여줍니다: warning: unused result which must be used, #[warn(unused_must_use)] on by default --> src/main.rs:39:5 |\n39 | run(config); | ^^^^^^^^^^^^ Rust는 우리 코드가 오류가 있음을 나타내는 Result 값을 무시한다는 것을 알려줍니다. 우리는 에러가 있는지 아닌지를 확인하지 않고 있고, 컴파일러는 우리에게 아마도 여기에 에러 처리 코드를 작성해야 한다는 것을 상기 시켜줄 것입니다! 당장 바로잡아 봅시다. main안의 run에서 반환되는 에러 처리하기 우리는 항목 12-10의 Config::new를 사용하여 오류를 처리하는 방식과 비슷한 방법을 사용하여 오류를 검사하고 멋지게 처리합니다. 그러나 약간의 차이점이 있습니다. Filename: src/main.rs fn main() { // ...snip... println!(\"Searching for {}\", config.query); println!(\"In file {}\", config.filename); if let Err(e) = run(config) { println!(\"Application error: {}\", e); process::exit(1); }\n} 우리는 unwrap_or_else를 호출하기 보다 if let을 사용하여 run이 Err값을 반환하는지 검사하고 만약 그렇다면 process::exit(1)을 호출합니다. run은 Config::new가 Config객체를 반환하는 것처럼 우리가 unwrap하기를 원하는 값을 반환하지 않습니다. 왜냐하면 run은 성공하면 ()를 반환하기 때문에, 우리는 에러가 발생한 경우만 신경쓰면 됩니다. 그래서 우리는 unwrap_or_else을 통해 포장을 벗길 필요가 없죠, 값은 무조건 ()일테니까요. if let과 unwrap_or_else 함수의 내용은 동일한 경우에 동일한 동작을 합니다, 오류를 출력하고 종료하죠.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러처리의 향상을 위한 리팩토링 » run 함수 추출하기","id":"214","title":"run 함수 추출하기"},"215":{"body":"지금까지 꽤 좋아 보인다! 이제 우리는 src/main.rs 파일을 나눠서 src/lib.rs 에 몇 개의 코드를 넣어서 테스트 할 수 있고 작은 src/main.rs 파일을 갖게 될 것입니다. src/main.rs 에 파편으로 존재하는 다음 코드들을 새 파일로 옮겨봅시다. src/lib.rs : run 함수 정의 관련있는use 문들 Config의 정의 Config::new 함수와 정의 src/lib.rs 의 내용은 항목 12-13에서 보이는 것과 같을겁니다. Filename: src/lib.rs use std::error::Error;\nuse std::fs::File;\nuse std::io::prelude::*; pub struct Config { pub query: String, pub filename: String,\n} impl Config { pub fn new(args: &[String]) -> Result { if args.len() < 3 { return Err(\"not enough arguments\"); } let query = args[1].clone(); let filename = args[2].clone(); Ok(Config { query, filename }) }\n} pub fn run(config: Config) -> Result<(), Box>{ let mut f = File::open(config.filename)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; println!(\"With text:\\n{}\", contents); Ok(())\n} 항목 12-13: Config과 run을 src/lib.rs 로 옮기기 우리는 Config의 필드 및 new 메소드와 run 함수에 대해 pub을 자유롭게 사용했습니다. 이제 우리가 테스트 할 수있는 공개 API를 가진 라이브러리 크레이트가 생겼습니다. 바이너리 크레이트에서 라이브러리 크레이트 호출하기 이제 우리는 src/main.rs 에 있는 바이너리 크레이트의 범위에 src/lib.rs 로 옮긴 코드를 extern crate greprs를 사용하여 가져와야 합니다. 이후 use greprs::Config 행을 추가하여 Config 타입을 범위로 가져오고 항목 12-14와 같이 크레이트 이름으로run 함수 앞에 접두사를 붙입니다. Filename: src/main.rs extern crate greprs; use std::env;\nuse std::process; use greprs::Config; fn main() { let args: Vec = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { println!(\"Problem parsing arguments: {}\", err); process::exit(1); }); println!(\"Searching for {}\", config.query); println!(\"In file {}\", config.filename); if let Err(e) = greprs::run(config) { println!(\"Application error: {}\", e); process::exit(1); }\n} 항목 12-14: greprs크레이트를 src/main.rs 범위로 연결하기 라이브러리 크레이트를 바이너리 크레이트에 가져 오려면 extern crate greprs을 사용합니다. 그런 다음 greprs::Config줄을 추가하여 Config타입을 범위로 가져오고 run 함수 접두어에 크레이트 이름을 붙입니다. 이를 통해 모든 기능이 연결되어 있어야 하며 작동해야 합니다. cargo run을 실행하여 모든 것이 올바르게 연결되어 있는지 확인하십시오. 아오! 빡시게 작업했네요, 우리의 미래를 우리 스스로가 성공의 방향으로 설정했습니다. 이제 에러를 처리가 훨씬 쉬워졌고, 우리의 코드를 보다 모듈화하였습니다. 거의 모든 작업은 여기 src/lib.rs 에서 수행될 겁니다. 새롭게 확보한 모듈성을 통해 이전의 코드로는 하지 못했을 무언가를 쉽게 할 수 있는 이점을 확보했습니다:몇 개의 테스트를 작성해봅시다!","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 모듈성과 에러처리의 향상을 위한 리팩토링 » 라이브러리 크레이트로 코드를 나누기","id":"215","title":"라이브러리 크레이트로 코드를 나누기"},"216":{"body":"src/lib.rs 으로 로직을 추출하고 src/main.rs 에 인수 수집 및 에러 처리를 남겨 두었으므로 우리의 핵심 기능 코드에 대한 테스트를 작성하는 것이 훨씬 쉬워졌습니다. 커맨드라인에서 바이너리를 실행할 필요없이 다양한 인수를 사용하여 함수를 직접 호출하고 반환 값을 확인할 수 있습니다. 자신이 만든 Config::new와 run함수의 기능에 대해 몇 가지 테스트를 작성하면서 자유도를 느껴보세요. 이 섹션에서는 TDD(Test Driven Development) 프로세스에 따라 minigrep에 검색 로직을 추가합니다. 해당 소프트웨어 개발 기법은 다음의 단계를 따릅니다: 실패할 테스트를 작성하고, 의도한 대로 실패하는지 실행해보세요. 새 테스트를 통과하기 충분할 정도로 코드를 작성하거나 수정하세요. 추가하거나 수정하는 정도의 리팩토링을 해보고, 여전히 테스트를 통과하는지 확인해보세요. 1단계로 반복! 이것은 소프트웨어를 작성하는 여러 가지 방법 중 하나지만 TDD는 코드 설계를 좋은 상태로 유지시켜 줍니다. 코드를 작성하기 전에 테스트를 작성하고 테스트를 통과시키면 높은 테스트 범위를 유지하는데 도움이 됩니다. 테스트 패스를 작성하는 코드를 작성하기 전에 테스트를 작성하면 프로세스 전체에서 높은 테스트 적용 범위를 유지하는 데 도움이 됩니다. 우리는 실제로 파일 내용에서 쿼리 문자열을 검색하고 쿼리와 일치하는 줄의 목록을 생성하는 기능의 구현을 테스트 주도로 개발해 볼 겁니다. 이 기능을 search라는 함수에 추가 할 것입니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 테스트 주도 개발로 라이브러리의 기능 개발하기 » 테스트 주도 개발로 라이브러리의 기능 개발하기","id":"216","title":"테스트 주도 개발로 라이브러리의 기능 개발하기"},"217":{"body":"더 이상 필요하지 않으므로 프로그램의 동작을 확인하는 데 사용했던 src/lib.rs 및 *src/main.rs *에서 println!문을 제거해 봅시다. 그런 다음 src/lib.rs 에 11 장에서 했던 것처럼 test 함수가 있는 test 모듈을 추가 할 것입니다. test 함수는 search 함수에 필요한 동작을 지정합니다. 쿼리와 텍스트를 가져 와서 쿼리를 검색하고 쿼리를 포함하는 텍스트의 줄만 반환합니다. 항목 12-15는 아직 컴파일되지 않는 이 테스트를 보여줍니다. Filename: src/lib.rs # fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# vec![]\n# }\n#\n#[cfg(test)]\nmod test { 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} Listing 12-15: Creating a failing test for the search function we wish we had 이 테스트는 “duct.”라는 문자열을 검색합니다. 우리가 검색하는 텍스트는 세 줄로, 한 줄은 “duct.”를 포함합니다. 우리는 search 함수에서 반환하는 값이 우리가 예상한 줄이어야 한다고 단정했습니다(assert). 테스트가 컴파일되지 않기 때문에 우리는 이 테스트를 실행할 수 없으며 search 함수가 아직 존재하지 않습니다! 이제 우리는 항목 12-16에서 보듯이 항상 빈 벡터를 반환하는 search 함수의 정의를 추가하여 컴파일과 실행하기에 충분한 코드를 추가 할 것입니다. 빈 벡터가 \"safe, fast, productive.\"줄을 포함하는 벡터와 일치하지 않기 때문에 테스트는 컴파일되지만 실패해야 합니다. Filename: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { vec![]\n} 항목 12-16: 우리 테스트를 컴파일 하기 위해 필요한 search 정의. search의 선언부에는 필요한 명시적인 라이프타임 'a가 contents 인자, 그리고 반환 값과 함께 사용됩니다. 10 장에서 인자의 라이프타임으로 라이프타임 값이 매개변수로 명시된 경우 반환되는 값의 라이프타임도 연결된다고 했던 점을 상기하십시오. 이 경우 반환된 벡터는 인자로 받은 contents를 참조하는 문자열 조각들이 포함되어 있어야 합니다. (query 인자가 아니라) 다른 말로 하자면, search함수로 반환되는 데이터는 search함수로 전달된 contents인자만큼 오래 유지될 것이라고 Rust에게 말해주는 겁니다. 이것이 중요합니다! 조각들에 의해 참조되는 데이터는 참조가 유효한 동안 유효해야 하기 때문이죠; 만일 컴파일러가 우리가 만든 문자열 조각이 contents에서가 아니라 query에서 만들었다고 추측하면 그에 대한 안전성 검사가 제대로 수행되지 않을 겁니다. 만약 우리가 라이프타임 어노테이션을 깜빡하고 이 함수를 컴파일하려고 시도하면, 이런 에러를 얻게 될겁니다: error[E0106]: missing lifetime specifier --> src/lib.rs:5:51 |\n5 | pub fn search(query: &str, contents: &str) -> Vec<&str> { | ^ expected lifetime\nparameter | = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents` Rust는 두 인자 중에 우리가 필요한 쪽이 어느건지 알 수 없기 때문에, 우리가 알려줘야 합니다. contents가 우리의 문자들을 모두 가지고 있고 우리가 원하는 것은 그 중 일치하는 부분이기 때문에, contents가 라이프타임 문법을 사용하여 반환 값과 연결되어야 한다는걸 압니다. 다른 프로그래밍 언어는 인자와 반환 값을 선언부에서 연결시키라고 요구하지 않으니, 아마 이게 낯설거고, 전체적으로 좀더 쉬울겁니다. 아마 여러분은 이 예제와 10장에서 다룬 “Validating References with Lifetimes” 장의 내용을 비교하고 싶을지도 모르겠습니다. 이제 테스트를 실행해봅시다: $ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep)\n--warnings-- Finished dev [unoptimized + debuginfo] target(s) in 0.43 secs Running target/debug/deps/minigrep-abcabcabc running 1 test\ntest test::one_result ... FAILED failures: ---- test::one_result stdout ---- thread 'test::one_result' panicked at 'assertion failed: `(left ==\nright)`\nleft: `[\"safe, fast, productive.\"]`,\nright: `[]`)', src/lib.rs:48:8\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. failures: test::one_result test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out error: test failed, to rerun pass '--lib' 훌륭하게, 우리가 예상했던 예상대로 테스트가 실패했습니다. 테스트를 통과하게 만들어봅시다!","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 테스트 주도 개발로 라이브러리의 기능 개발하기 » 실패 테스트 작성하기","id":"217","title":"실패 테스트 작성하기"},"218":{"body":"현재는, 우리가 늘 빈 벡터를 반환하니까 테스트가 실패하게 됩니다. 이를 수정하고 search를 구현하기 위해, 우리의 프로그램은 다음 단계를 따를 필요가 있습니다. contents의 각 줄에 대한 반복작업 해당 줄에 우리의 쿼리 문자열이 포함되어 있는지 검사 그렇다면, 우리가 반환할 값 목록에 추가 그렇지 않다면, 통과 일치하는 결과 목록을 반환 각 단계를 밟아가기 위해, 줄들에 대한 반복작업부터 시작합시다! lines 메소드를 사용하여 줄들에 대한 반복 작업 Rust는 문자열의 줄-단위로 반복 작업을 할 수 있는 유용한 메소드가 있는데, 편리하게 이름이 lines이고, 항목 12-17처럼 보여주는 것처럼 동작합니다. 아직 컴파일되지 않는다는 점에 유의하세요: Filename: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { for line in contents.lines() { // do something with line }\n} 항목 12-17: contents의 각 줄마다 반복작업 lines 메소드는 반복자를 리턴합니다. 우리는 13장에서 반복자에 대해서 다루게 될 겁니다만, 항목 3-4에서 반복자를 사용하는 방법을 봤었다는걸 상기시켜 드립니다. 항목 3-4에서는 반복자와 함께 for반복문을 사용하여 컬렉션의 각 항목에 대해 임의의 코드를 수행했었습니다. Query로 각 줄을 검색하기 다음으로 현재 줄에 쿼리 문자열이 포함되어 있는지 확인합니다. 다행스럽게도 문자열에는 유용한 'contains'라는 메소드가 있습니다. 항목 12-18과 같이 search 함수에서 contains 메소드에 대한 호출을 추가하십시오. 이 코드는 여전히 컴파일되지 않으니 주의하세요. Filename: src/lib.rs 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} 항목 12-18: 어느 줄이 query 문자열을 포함하고 있는지 보기 위한 기능 추가 일치하는 줄 보관하기 또한 쿼리 문자열이 포함된 줄을 저장할 방법이 필요합니다. 이를 위해 우리는 for반복문 전에 가변 벡터를 만들고 push 메소드를 호출하여 벡터에 line을 저장합니다. 항목 12-19처럼 for반복문이 끝난 다음에 벡터를 반환합니다. Filename: src/lib.rs 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} 항목 12-19: 일치하는 라인들을 저장하여 반환할 수 있게 만들기. 이제 search함수는 query를 포함하는 줄들만 반환하게 되었으니 우리의 테스트는 통과되야 할 겁니다. 테스트를 실행해 봅시다: $ cargo test\n--snip--\nrunning 1 test\ntest test::one_result ... ok test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 우리 테스트가 통과되었으니, 제대로 동작한다는 것을 알게 되죠! 이 시점에서, 우리는 동일한 기능을 유지하기 위해 테스트를 통과시키면서 search 함수를 리팩토링할 기회를 고려해 볼 수 있게 됐습니다. search 함수가 많이 나쁘지는 않지만, 반복자의 기능들이 주는 유용함을 충분히 활용하지 못하고 있습니다. 우리는 13장에서 이 예제로 돌아와 반복자에 대해서 자세히 알아보고 어떻게 개선할 수 있는지 알아볼 겁니다. run함수에서 search함수를 사용하기 Using the search Function in the run Function 이제 search 함수는 실행되고 테스트 되었지만, 우리의 run함수에서 search를 호출하게 해야 합니다. 우리는 config.query 값과 run으로 파일에서 읽어온 contents를 search함수에 전달해야 합니다. 그 이후 run은 search로부터 반환된 각 줄을 출력합니다: Filename: src/lib.rs pub fn run(config: Config) -> Result<(), Box> { let mut f = File::open(config.filename)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; for line in search(&config.query, &contents) { println!(\"{}\", line); } Ok(())\n} 우리는 아직 search에서 for반복문을 사용해 각 줄을 반환하고 출력하고 있습니다. 이제 우리의 프로그램 전체가 동작하는 것 같습니다! 확신하기 위해, 첫째로 “frog” 단어로 Emily Dickinson의 시에서 정확히 한 줄이 반환되야 합니다: $ cargo run frog poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.38 secs Running `target/debug/minigrep frog poem.txt`\nHow public, like a frog 좋군요! 다음으 여러 줄에 일치할 “body” 같은 단어를 해봅시다: $ cargo run body poem.txt Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs 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 Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/minigrep monomorphization poem.txt` 훌륭해! 우리는 어플리케이션의 구조화를 어떻게 수행하는지에 대해 많은 것을 배우며 고전적인 도구를 우리 자체 미니 버전으로 만들어봤습니다. 또한 우리는 파일의 입력, 출력, 라이프타임, 테스팅과 커맨드라인 파싱에 대해서도 좀 알게 되었네요. 이 프로젝트를 완벽하게 하기 위해, 환경 변수를 다루고 표준 에러를 출력하는 방법을 간단히 시연하려고 하는데, 모두 커맨드라인 프로그램을 작성하는데 유용할 겁니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 테스트 주도 개발로 라이브러리의 기능 개발하기 » 테스트를 통과하는 코드 작성","id":"218","title":"테스트를 통과하는 코드 작성"},"219":{"body":"우리는 추가 기능을 구현하여 minigrep을 향상시키려고 합니다. 대소문자를 구분하여 검색할지를 선택할 수 있는 기능인데, 사용자가 환경 변수를 사용하여 키고 끌 수 있게 할 수 있도록 하려 합니다. 우리는 해당 기능을 명령줄 옵션으로 구현하고 사용자가 원할때마다 해당 옵션을 기입하게 만들 수도 있지만, 대신 환경 변수를 사용하게 할 수도 있습니다. 이를 통해 사용자가 한번 환경변수를 설정하는 것을 통해 현재 터미널 세션에서 하는 모든 검색이 대소문자를 구분하게 만듭니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 환경 변수들을 활용하기 » 환경 변수들을 활용하기","id":"219","title":"환경 변수들을 활용하기"},"22":{"body":"다음으로, main.rs 이라 불리우는 새로운 소스 파일을 만드세요. 러스트 파일들은 언제나 .rs 확장자로 끝납니다. 만일 여러분이 한 단어 이상을 여러분의 파일에 사용하겠다면, 단어 구분을 위해서 언더스코어(_)를 사용하세요. 예를 들면, helloworld.rs 보다는 hello_world.rs 를 사용하세요. 이제 여러분이 방금 만든 main.rs 을 열고 Listing 1-1의 코드를 입력하세요. Filename: main.rs fn main() { println!(\"Hello, world!\");\n} Listing 1-1: “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":"우리는 새로운 search_case_insensitive 함수를 추가하고, 환경 변수가 적용되어 있으면 호출하고자 합니다. 우리는 TDD 절차를 따르고자 하니, 우리는 먼저 실패 테스트를 작성해야 합니다. 우리는 새 테스트를 새 search_case_insensitive를 위해 작성하고 예전에 작성한 테스트 one_result를 case_sensitive로 이름을 바꿔 두 테스트 간의 차이점을 명확하게 합니다. 항목 12-20에서는 이를 보여줍니다. Filename: src/lib.rs #[cfg(test)]\nmod test { 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도 바꿨음을 주의하세요. 우리는 “Duct tape”라는 대문자 D로 시작되는 새로운 문자를 추가해 대소문자 구분 시에 쿼리 “duct”으로는 검색되지 않도록 하였습니다. 이러한 방식으로 이전 테스트를 변경하면 이미 구현한 대소문자 구분 검색 기능을 실수로 손상시키지 않게됩니다. 이 테스트는 지금 통과해야하며 우리가 작업을 마친 이후에도 대소문자를 구분하지 않는 검색 시에 통과되어야 합니다. 대소문자를 구분하지 않는 검색을 위해 새로 추가된 테스트는 “rUsT”를 쿼리로 사용합니다. 우리가 추가할 함수 search_case_insensitive는 “rUsT”가 대문자 R이 포함된 “Rust:”에 그리고 “Trust me.”처럼 쿼리와 다른 경우에도 일치될 겁니다. 이건 우리가 만든 search_case_insensitive 함수의 실패 테스트이고, 우리가 아직 함수를 추가하지 않았기 때문에 컴파일은 실패할 겁니다. 우리는 search` 함수를 추가할 때와 비슷한 방식으로 빈 벡터를 반환하는 뼈대를 자유롭게 추가하면 됩니다. 항목 12-16에서 테스트의 컴파일과 실패를 볼 수 있습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 환경 변수들을 활용하기 » 대소문자를 구분하는 search 함수의 실패 케이스 작성하기","id":"220","title":"대소문자를 구분하는 search 함수의 실패 케이스 작성하기"},"221":{"body":"항목 12-21에서 보여주는 search_case_insensitive는 search 함수와 거의 같습니다. 유일하게 다른 점은 query와 각 line을 소문자로 만들어 인자의 대소문자 여부와 무관하게 동일한 문자가 각 라인에 존재하는지 검사할 수 있게 만든겁니다: Filename: src/lib.rs fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> 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} 항목 12-21: search_case_insensitive 함수를 정의해 query와 line을 query와 line을 비교하기 전에 소문자로 변경. 첫 째, 소문자화 한 query 문자열을 동일한 이름을 가진 그림자 변수에 보관합니다. to_lowercase를 쿼리에서 호출하면 사용자의 쿼리가 “rust”, “RUST”, “Rust”, 혹은 “rUsT”인지 구분할 필요가 없어지고, 우리는 사용자 쿼리가 “rust” 로 간주하고 대소문자 구문을 하지 않을 겁니다. to_lowercase 호출은 기존 데이터를 참조하는 것이 아니라 새로운 데이터를 생성기 때문에 query는 문자열 슬라이스가 아닌 String입니다. 예로 들었던 쿼리 “rUsT” 문자열 slice에는 우리가 사용할 “u” 또는 “t” 소문자가 없으므로 “rust”가 포함 된 새 String을 할당해야 합니다. 우리가 contains 메소드에 인자로 query를 전달할 때 contains의 선언이 문자열 slice를 인자로 받게 정의되어 있으니 앰퍼샌드(&)를 추가해야합니다. 다음으로, 우리는 각 line에 모두 소문자로 이뤄진 query가 존재하는지 검사하기 전에 to_lowercase를 호출합니다. 이제 line과 query를 모두 소문자로 변경했으니, 대소문자 구분없이 매치되는 문자열을 검색할 수 있습니다. 해당 구현이 테스트들을 통과하는지 한번 보시죠. running 2 tests\ntest test::case_insensitive ... ok\ntest test::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 시원하게 통과했습니다. 이제 run 함수에서 신상 search_case_insensitive를 호출해보자구요. 먼저 Config 구조체에 검색을 시에 대소문자를 구분할지 설정 옵션을 추가부터 하구요. 근데 이 필드를 추가하면 컴파일러가 필드 값을 초기화 하지 않았다고 에러를 내게 되요. Filename: src/lib.rs pub struct Config { pub query: String, pub filename: String, pub case_sensitive: bool,\n} 우리는 불린 값을 갖는 case_sensitive를 추가했어요. 다음으로, 우리는 run 함수를 실행해서 case_sensitive 필드의 값을 확인한 뒤에 search 함수와 search_case_insensitive 함수 중에 어느 쪽을 호출 할 것인지 결정하면 되요, 항목 12-22처럼 말이죠. 아직도 컴파일은 안되욧! Filename: src/lib.rs # use std::error::Error;\n# use std::fs::File;\n# use std::io::prelude::*;\n#\n# fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# vec![]\n# }\n#\n# fn search_case_insensitive<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {\n# vec![]\n# }\n#\n# struct Config {\n# query: String,\n# filename: String,\n# case_sensitive: bool,\n# }\n#\npub fn run(config: Config) -> Result<(), Box> { let mut f = File::open(config.filename)?; let mut contents = String::new(); f.read_to_string(&mut contents)?; let results = if config.case_sensitive { search(&config.query, &contents) } else { search_case_insensitive(&config.query, &contents) }; for line in results { println!(\"{}\", line); } Ok(())\n} 항목 12-22: config.case_sensitive의 값을 기준으로 search 혹은 search_case_insensitive이 호출됩니다. 마지막으로, 우리는 환경 변수를 검사해야 해요. 환경 변수를 다루기 위한 함수들은 env모듈이 있는 표준 라이브러리에 있어요, 그래서 우리는 use std::env;을 src/lib.rs 의 최상단에 추가해서 현재 범위로 끌어오려고 해요. 그러면 우리는 env에 있는 var메소드를 사용하여 CASE_INSENSITIVE란 이름의 환경변수를 검사할 수 있죠. 항목 12-23에서 보이듯 말이에요. Filename: src/lib.rs use std::env;\n# struct Config {\n# query: String,\n# filename: String,\n# case_sensitive: bool,\n# } // --snip-- impl Config { pub fn new(args: &[String]) -> Result { if args.len() < 3 { return Err(\"not enough arguments\"); } let query = args[1].clone(); let filename = args[2].clone(); let case_sensitive = env::var(\"CASE_INSENSITIVE\").is_err(); Ok(Config { query, filename, case_sensitive }) }\n} 항목 12-23: CASE_INSENSITIVE란 이름의 환경변수 검사하기 여기서 우리는 case_sensitive라는 새 변수를 만들어요. 그의 값을 설정하려고, env::var 함수를 호출하고 CASE_INSENSITIVE란 환경변수의 이름을 전달하죠. env::var 메소드는 Result를 반환하는데, 만약 환경변수가 설정된 상태라면 환경 변수의 값을 포함한 성공한 Ok 변형체가, 만약 설정되지 않았다면 Err 변형체를 반환하게 됩니다. 우리는 Result의 is_err 메소드를 에러이며 설정되지 않은 상태라서 대소문자를 구분하는 검색을 해야하는지 확인하고자 사용합니다. 만약 CASE_INSENSITIVE 환경 변수에 뭐라도 설정이 되었으면, is_err는 false를 반환하고 대소문자 구분 검색을 수행하게 될겁니다. 우리는 환경변수의 내용 은 신경쓰지 않고, 그저 그게 설정이 되어있는지만을, is_err로 검사하며 unwrap, expect나 Result에 존재하는 다른 메소드는 사용하지 않았어요. 항목 12-22에서 구현했던 것처럼 case_sensitive 변수의 값을 Config 인스턴스에 전달하여 run 함수가 해당 값을 읽고 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.0 secs Running `target/debug/minigrep to poem.txt`\nAre you nobody, too?\nHow dreary to be somebody! 잘 동작하고 있네요! 이제, 프로그램을 CASE_INSENSITIVE를 1로 설정하지만 쿼리는 동일한 “to”로 실행해볼까요. PowerShell을 사용하는 경우 환경 변수를 설정하고 둘로 나눈 명령으로 프로그램을 실행해야합니다. $ $env:CASE_INSENSITIVE=1\n$ cargo run to poem.txt 대소문자 “to” 가 포함된 줄을 가져와야 합니다. $ CASE_INSENSITIVE=1 cargo run to poem.txt Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/minigrep to poem.txt`\nAre 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":"221","title":"search_case_insensitive 함수 구현하기"},"222":{"body":"지금까지 우리는 모든 출력을 println!을 사용하여 터미널에 출력했습니다. 대부분의 터미널은 두 가지 방식의 출력 을 지원합니다: 표준 출력 (stdout)은 일반적인 정보전달용이고 표준 에러 (stderr)는 에러 메시지용 입니다. 이렇게 구분지음으로 인해 사용자는 프로그램의 출력을 직접 파일에 작성하면서도 여전히 에러메시지를 화면에 출력할 수 있습니다. println! 함수는 오직 표준출력만 사용할 수 있으므로, 우리는 표준에러에 출력을 위한 다른 것을 알아보겠습니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준출력 대신 표준에러로 에러메시지 출력하기 » 표준출력 대신 표준에러로 에러메시지 출력하기","id":"222","title":"표준출력 대신 표준에러로 에러메시지 출력하기"},"223":{"body":"먼저, minigrep의 출력 내용이 어떻게 표준출력에 작성되는지를 후에 우리가 표준에러로 바꾸려는 에러메시지를 염두하며 살펴봅시다. 에러가 발생할 것을 인지한채로 우리는 표준출력 스트림을 파일로 변경하고자 합니다. 표준에러 스트림은 변경하지 않을 것이므로, 표준에러로 보내진 모든 출력내용은 화면에 표시될 겁니다. 커맨드라인 프로그램들은 에러메시지들이 표준에러로 전달되는 것을 상정하고 있기 때문에 표준출력 스트림을 파일로 변경하더라도 우리는 에러메시지가 출력되는 것을 여전히 볼 수 있습니다. 우리 프로그램은 정상 동작하고 있지 않습니다 : 오류메시지 출력이 파일로 저장되고 있거든요! 이런 동작을 시연하는 방법은 프로그램의 실행시킬때 >과 표준출력 스트림을 향하게 할 파일이름을 주면 됩니다. 에러가 발생할 여지가 있는 인자는 주지 않습니다. $ cargo run > output.txt > 문법은 쉘에게 표준출력의 내용을 화면이 아닌 *output.txt`에 출력하게끔 하는 것입니다. 우리가 기대했던 에러에시지의 화면 출력은 보지 못했으니 이것은 파일 마지막에 기록됐을 겁니다. 다음인 output.txt 의 내용입 니다. Problem parsing arguments: not enough arguments 역시, 우리의 에러메시지는 표준출력으로 출력되었네요. 이런 에러메시지가 표준에러로 출력된다면 훨씬 유용하고 우리가 같은 방법으로 표준출력을 변경했을때 오직 성공적 실행에 관련된 데이터만 저장할 수 있게 될 겁니다. 지금 바꿔봅시다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준출력 대신 표준에러로 에러메시지 출력하기 » 에러가 어디에 출력될지 검사","id":"223","title":"에러가 어디에 출력될지 검사"},"224":{"body":"우리는 항목 12-24의 코드를 출력되는 에러메시지들을 변경하는데 사용하고자 합니다. 이번 장 진입부에서 리팩토링한 결과 모든 에러메시지는 하나의 함수 main에서 출력되고 있습니다. 표준라이브러리에 존재하는 표준에러에 출력해주는 매크로 eprintln!를 사용하여 println!을 사용하여 에러를 출력하던 두 부분을 eprintln!`을 사용하도록 변경 해봅시다. Filename: src/main.rs fn main() { let args: Vec = env::args().collect(); let config = Config::new(&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!을 사용하여 표준에러로 변경하기 println!을 eprintln!으로 변경한 후에, 같은 방식으로 >을 사용해 표준출력을 변경하는 것 외에 다른 인자를 주지 않고 프로그램을 다시 실행시켜 봅시다. $ cargo run > output.txt\nProblem parsing arguments: not enough arguments 이제 우리는 에러를 화면에서 볼 수 있고, 우리가 커맨드라인 프로그램에서 기대한 대로 output.txt 는 비어있습니다. 이번에는 에러를 발생시키지 않게 인자와 함께 프로그램을 실행시키면서 표준출력을 파일로 변경해봅시다. $ cargo run to poem.txt > output.txt 터미널에는 아무것도 출력되지 않고, output.txt 가 보관하게 됩니다. Filename: output.txt Are you nobody, too?\nHow dreary to be somebody! 이번 시연은 우리가 표준출력에 성공적출력을 표준에러에 에러출력을 의도한 대로 수행하고 있음을 보여줍니다.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준출력 대신 표준에러로 에러메시지 출력하기 » 에러를 표준에러로 출력하기","id":"224","title":"에러를 표준에러로 출력하기"},"225":{"body":"이번 장에서는 지금까지 우리가 배웠던 몇 가지 주요 개념을 되짚어보고 Rust 문법에서 범용 I/O 작업수행을 하는 방법을 알아봤습니다. 커맨드라인 인자, 파일, 환경변수, 그리고 eprintln!매크로로 에러출력를 사용하여 당신은 이제 커맨드라인 응용프로그램을 작성할 준비가 됐습니다. 이전 장들의 개념을 활용하여, 당신의 코드는 잘 구조화되고, 적합한 데이터 구조를 사용하여 효율적으로 데이터를 저장하며, 에러를 보기좋게 관리하며, 잘 테스트 할 수 있게 됐습니다. 다음으로, 우리는 함수형 언어의 영향을 받은 Rust의 기능 몇가지를 알아보겠습니다 : 클로저와 반복자.","breadcrumbs":"I/O 프로젝트: 커맨드 라인 프로그램 만들기 » 표준출력 대신 표준에러로 에러메시지 출력하기 » 종합","id":"225","title":"종합"},"226":{"body":"러스트의 디자인은 많은 기존 언어들과 기술들에서 영감을 얻었으며, 중요한 영향 중에 하나는 함수형 프로그래밍 입니다. 함수형 스타일의 프로그래밍은 자주 함수를 값처럼 인자로 넘기는 것, 다른 함수들에 서 결괏값으로 함수들을 돌려주는 것, 나중에 실행하기 위해 함수를 변수에 할당하는 것 등을 포함합니다. 이번 장에서는, 무엇이 함수형 프로그래밍이고 그렇지 않은지에 대해 논의하는 대신, 다른 언어에서 자주 함수형으로 언급되는 특성들과 유사한 러스트의 특성들에 대해 논의할 것입니다. 더 구체적으로, 이것들을 다룹니다: 클로저들 , 변수에 저장할 수 있는 함수와 유사한 구조. 반복자들 , 일련의 요소들을 처리할 수 있는 방법. 이 두가지 특성들을 사용해서 12장의 I/O 프로젝트를 향샹시킬 수 있는 방법. 이 두 특성들의 성능 (스포일러 있음: 생각보다 빠릅니다!) 다른 장에서 다룬 패턴 매칭이나 열거형과 같은 다른 러스트의 특성들도 역시 함수형 스타일의 영향을 받았습니다. 클로저들과 반복자들을 정복하는 것은 자연스러우면서도 빠른 러스트 코드를 작성하는데 중요한 부분 입니다, 그래서 이번 장 전체에서 이것들을 다룹니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 함수형 언어의 특성들: 반복자들과 클로저들","id":"226","title":"함수형 언어의 특성들: 반복자들과 클로저들"},"227":{"body":"러스트의 클로저 는 변수에 저장하거나 다른 함수에 인자로 넘길 수 있는 익명 함수입니다. 한 곳에서 클로저를 만들고 다른 문맥에서 그것을 평가하기 위해 호출할 수 있습니다. 함수와 다르게 클로저는 그들이 호출되는 스코프로부터 변수들을 캡처할 수 있습니다. 이 클로저 특성이 코드 재사용과 동작 사용자 정의를 어떤 식으로 허용하는지 예를 들어 보여줄 것입니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 클로저: 환경을 캡쳐할 수 있는 익명 함수 » 클로저: 환경을 캡처할 수 있는 익명 함수","id":"227","title":"클로저: 환경을 캡처할 수 있는 익명 함수"},"228":{"body":"클로저를 나중에 실행하기 위해 저장하는 것이 유용한 상황에 대한 예제로 작업해 봅시다. 따라가다 보면, 클로저 문법과 타입 추론, 트레잇에 대해 이야기할 것입니다. 이런 가상의 상황을 생각해 봅시다: 우리는 맞춤 운동 계획을 생성하는 앱을 만드는 스타트업에서 일합니다. 백엔드는 러스트로 작성되어 있고, 운동 계획을 생성하는 알고리즘은 앱 사용자의 나이, 체질량 지수, 선호도, 최근 운동들과 그들이 지정한 강도 숫자와 같은 많은 다른 요소들을 고려합니다. 이 예제에서 사용되는 실제 알고리즘은 중요하지 않습니다; 중요한 것은 이 알고리즘이 몇 초가 걸린다는 것입니다. 이 알고리즘을 우리가 필요할 때 한 번만 호출하기를 원하고, 그래서 사용 자가 필요 이상으로 기다리지 않게 만들고 싶습니다. 우리는 리스트 13-1 에 보이는 simulated_expensive_calculation 함수를 사용해서 이 가상의 알고리즘 호출을 실험할 것입니다. 이 함수는 calculating slowly... 을 출력하고, 2초를 기다린 다음, 인자로 넘어온 어떤 값이든 돌려줍니다: 파일명: src/main.rs use std::thread;\nuse std::time::Duration; fn simulated_expensive_calculation(intensity: u32) -> u32 { println!(\"calculating slowly...\"); thread::sleep(Duration::from_secs(2)); intensity\n} 리스트 13-1: 실행시간이 2초 걸리는 가상의 계산을 대신하는 함수 다음은 이 예제에서 중요한 운동 앱의 일부를 담고 있는 main 함수 입니다. 이 함수는 사용자가 운동 계획을 물어볼 때 앱이 호출 할 코드를 나타냅니다. 앱의 프론트엔드와의 상호작용은 클로저를 사용하기에 적합하지 않기 때문에, 우리 프로 그램에 대한 입력을 나타내는 값을 코드상에 넣어두고 결과를 출력 할 것 입니다. 필요한 입력들은: 사용자로 부터의 강도 숫자 , 이것은 그들이 운동을 요청할 때 지정되며, 낮은 강도 운동을 원하는지 혹은 고강도 운동을 원하는지를 나타냅니다. 임의의 숫자 는 몇 가지 다양한 운동 계획들을 생성할 것입니다. 결과는 추천 운동 계획이 될 것입니다. 리스트 13-2 에 우리가 사용할 main 함수 가 있습니다: 파일이름: src/main.rs fn main() { let simulated_user_specified_value = 10; let simulated_random_number = 7; generate_workout( simulated_user_specified_value, simulated_random_number );\n}\n# fn generate_workout(intensity: u32, random_number: u32) {} 리스트 13-2:사용자 입력과 임의의 숫자 생성을 시뮬레이션 하기 위한 main 함수와 하드코딩된 값 단순함을 위해서 simulated_user_specified_value 변수의 값을 10 으로하고 simulated_random_number 변수의 값을 7로 하드코딩 했습니다; 실제 프로그램에서, 강도 숫자를 앱 프론트엔드에서 얻고 2장의 추리게임에서 그랬던 것 처럼, 임의의 숫자 생성을 위해 rand 크레이트를 사용합니다. main 함수는 generate_workout 함수를 모의의 입력값으로 호출 합니다. 이제 상황이 만들어 졌으니, 알고리즘으로 넘어가겠습니다. 리스트 13-3 에 있는 generate_workout 함수는 이 예제에서 가장 신경써야 할 앱의 비즈니스 로직을 포함하고 있습니다. 이 예제에서 나머지 코드를 변경 사항은 이 함수에 적용 됩니다: 파일이름: src/main.rs # use std::thread;\n# use std::time::Duration;\n#\n# fn simulated_expensive_calculation(num: u32) -> u32 {\n# println!(\"calculating slowly...\");\n# thread::sleep(Duration::from_secs(2));\n# num\n# }\n#\nfn generate_workout(intensity: u32, random_number: u32) { if intensity < 25 { println!( \"Today, do {} pushups!\", simulated_expensive_calculation(intensity) ); println!( \"Next, do {} situps!\", simulated_expensive_calculation(intensity) ); } else { if random_number == 3 { println!(\"Take a break today! Remember to stay hydrated!\"); } else { println!( \"Today, run for {} minutes!\", simulated_expensive_calculation(intensity) ); } }\n} 리스트 13-3: 입력값과 simulated_expensive_calculation 함 수 호출에 근거해서 운동 계획을 출력하는 비즈니스 로직 리스트 13-3 의 코드는 느린 계산 함수에 대해 여려번 호출을 합니다. 첫번째 if 블럭은 simulated_expensive_calculation 함수를 두번 호출하고, 바깥 else 의 안쪽에 있는 if 문에서는 전혀 호출하지 않으며, 두번째 else 문 의 경우는 한번 호출 합니다. generate_workout 함수의 바람직한 행위는 먼저 사용자가 저강도 운동(25보다 작은 수로 표시) 혹은 고강도 운동(25 혹은 더 큰수)을 원하는지 체크하는 것입니다. 저강도 운동 계획은 우리가 시뮬레이션 하는 복잡한 알고리즘에 근거에서 푸쉬업과 싯업의 수를 추천 할 것입니다. 사용자가 고강도 운동을 원한다면, 약간의 추가 로직이 있습니다: 앱에 의해 생성된 임의의 숫자가 3이면, 앱은 휴식과 수분 섭취를 추천합니다. 그렇지 않다면, 사용자는 복잡한 알고리즘을 기반으로 몇 분의 달리기를 안내 받을 것입니다. 데이터 과학팀은 앞으로 알고리즘 호출 방식을 일부 변경해야 한다고 알렸습니다. 이러한 변경이 발생 했을 때 업데이트를 단순화 하기 위해서, 이 코드를 리팩토링 하여 simulated_expensive_calculation 함수를 단지 한 번만 호출 하도록 하려고 합니다. 또한 현재 프로세스에서 해당 함수에 대한 다른 호출을 추가하지 않고 불필요하게 함수를 두 번 호출하는 위치를 없애고 싶습니다. 즉, 결과가 필요없다면 함수를 호출하고 싶지 않고, 여전히 그것을 한 번만 호출하고 싶습니다. 함수를 사용해서 리팩토링 하기 우리는 여러 방향으로 운동 프로그램을 다시 구조화 할 수 있습니다. 우선, 리스트 13-4 에 보이는 것처럼, 중복된 expensive_calculation 함수 호출을 하나의 변수로 추출 해볼 것입니다: 파일이름: src/main.rs # use std::thread;\n# use std::time::Duration;\n#\n# fn simulated_expensive_calculation(num: u32) -> u32 {\n# println!(\"calculating slowly...\");\n# thread::sleep(Duration::from_secs(2));\n# num\n# }\n#\nfn generate_workout(intensity: u32, random_number: u32) { let expensive_result = simulated_expensive_calculation(intensity); if intensity < 25 { println!( \"Today, do {} pushups!\", expensive_result ); println!( \"Next, do {} situps!\", expensive_result ); } else { if random_number == 3 { println!(\"Take a break today! Remember to stay hydrated!\"); } else { println!( \"Today, run for {} minutes!\", expensive_result ); } }\n} 리스트 13-4: simulated_expensive_calculation 에 대한 호출들을 한 곳으로 추출하고 결과를 expensive_result 변수에 저장하기. 이 변경은 simulated_expensive_calculation 에 대한 모든 호출들을 하나로 합치고 첫번째 if 문에서 불필요하게 이 함수를 여러번 호출하던 문제를 해결 합니다. 불행하게도, 이제 모든 경우에 대해서 이 함수를 호출하고 결과를 기다리며, 이 결과를 전혀 사용하지 않는 안쪽 if 블럭도 해당됩니다. 우리는 프로그램에서 한곳에서 코드를 정의하고, 실제로 결과가 필요한 곳에서만 그 코드를 실행하고 싶습니다. 이것이 클로저의 유스 케이스 입니다. 코드를 저장하기 위해 클로저를 사용해서 리팩토링 하기. if 블럭 전에 항상 simulated_expensive_calculation 함수를 호출하는 대신, 리스트 13-5에 보이는 것 처럼, 클로저를 정의하고 변수에 결과를 저장하기 보단 클로저 를 변수에 저장 할 수 있습니다. 여기서 소개하는 것처럼 실제로 클로저 안에 simulated_expensive_calculation 의 전체 내용을 옮길 수 있습니다. Filename: src/main.rs # use std::thread;\n# use std::time::Duration;\n#\nlet expensive_closure = |num| { println!(\"calculating slowly...\"); thread::sleep(Duration::from_secs(2)); num\n};\n# expensive_closure(5); 리스트 13-5: 클로저를 정의하고 expensive_closure 변수에 저장하기 클로저 정의는 변수 expensive_closure 에 그것을 할당하기 위해 = 다음에 옵니다. 클로저를 정의하기 위해, 수직의 파이프 (|) 한쌍으로 시작하며, 그 사이에 클로저에 대한 파라미터를 기술합니다; 이 문법은 스몰토크와 루비에서 클로저 정의와의 유사성 때문에 선택 되었습니다. 이 클로저는 num 이라는 하나의 파라미터를 갖습니다: 하나 이상의 파라미터를 갖는다면, |param1, param2| 와 같이 콤마로 구분합니다. 파라미터들 다음에, 클로저의 바디를 포함하는 중괄호를 넣습니다—클로저 바디가 하나의 표현식이라면 이것은 선택적 입니다. 중괄호 다음에 클로저의 끝에는 let 문을 완성하기 위해 세미콜론이 필요합니다. 클로저 바디에서 마지막 줄로부터 반환되는 값인 (num) 은 그것이 호출되었을 때 클로저로 부터 반환되는 값이 될 것입니다, 왜냐하면 그 줄은 함수 본문 처럼 세미콜론으로 끝나지 않기 때문 입니다. let 문은 expensive_closure 가 익명함수의 정의 를 포함하며, 익명함수를 호출한 결과 값 을 포함하지 않는다는 것에 유의 하세요. 우리가 클로저를 사용하는 이유는 호출할 코드를 한 곳에서 정의하고, 그 코드를 저장하며, 이후 다른 곳에서 그것을 호출하길 원하기 때문이라는 것을 상기하세요; 우리가 호출하고자 하는 코드가 이제 expensive_closure 에 저장되었습니다. 클로저를 정의하면서, 저장된 코드를 실행하고 결과값을 얻기 위하여 if 블록 안의 코드를 클로저 호출 방식으로 변경할 수 있습니다. 우리는 함수를 호출하는 것 처럼 클로저를 호출 합니다: 리스트 13-6 에 보이는 것처럼, 클로저 정의를 갖고 있는 변수명을 쓰고 다음엔 사용할 인자값을 포함하는 괄호가 따라 옵니다: 파일명: src/main.rs # use std::thread;\n# use std::time::Duration;\n#\nfn generate_workout(intensity: u32, random_number: u32) { let expensive_closure = |num| { println!(\"calculating slowly...\"); thread::sleep(Duration::from_secs(2)); num }; if intensity < 25 { println!( \"Today, do {} pushups!\", expensive_closure(intensity) ); println!( \"Next, do {} situps!\", expensive_closure(intensity) ); } else { if random_number == 3 { println!(\"Take a break today! Remember to stay hydrated!\"); } else { println!( \"Today, run for {} minutes!\", expensive_closure(intensity) ); } }\n} 리스트 13-6: 우리가 정의한 expensive_closure 호출하기 이제 비용이 큰 계산은 단 한곳에서만 호출 되고, 우리가 결과가 필요한 곳에서만 그 코드를 실행 합니다. 그러나, 리스트 13-3 에 있는 문제중 하나를 다시 소개합니다: 우리는 여전히 첫번째 if 블럭에서 클로저를 두번 호출 하는데, 이는 비용이 큰 코드를 두번 호출하고 사용자가 실행시간 만큼 긴시간을 두번 기다리게 합니다. 우리는 그 if 블럭안에 클로저 호출의 결과를 저장하는 로컬 변수를 만들어서 그 문제를 해결할 수 있지만, 클로저는 다른 해결책을 제공합니다. 우리는 그 해결책에 대해 조금 이야기할 것입니다. 그러나 우선 클로저 정의에 타입 어노테이션이 없는 이유와 클로저와 연관된 트레잇에 대해 이야기 합시다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 클로저: 환경을 캡쳐할 수 있는 익명 함수 » 클로저로 행위를 추상화 하기","id":"228","title":"클로저로 행위를 추상화 하기"},"229":{"body":"클로저는 fn 함수처럼 파라미터나 반환값의 타입을 명시할 것을 요구하지 않습니다. 타입 어노테이션은 사용자에게 노출되는 명시적인 인터페이스의 일부이기 때문에 함수에 필요 합니다. 이 인터페이스를 엄격하게 정의하는 것은 함수가 어떤 타입의 값을 사용하고 반환하는지에 대해 모두가 합의 한다는 것을 보장하는데 중요 합니다. 그러나 클로저는 이와 같이 노출된 인터페이스에 사용되지 않습니다: 변수에 저장되고 이름없이 우리의 라이브러리 사용자들에게 노출되지 않고 사용 됩니다. 추가적으로, 클로저는 보통 짧고 임의의 시나리오 보다 좁은 문맥 안에서만 관련이 있습니다. 이런 제한된 문맥 안에서만, 컴파일러는 안정적으로 파라미터와 리턴타입을 추론할 수 있으며, 이는 대부분의 변수 타입을 추론 할 수 있는 방법과 비슷 합니다. 프로그래머들에게 이런 작고 익명의 함수들에 타입을 달도록하는 것은 짜증나고 컴파일러가 이미 사용할수 있는 정보와 대게는 중복 됩니다. 변수처럼, 엄밀하게 필요한 것 이상으로 자세히 표현하는 비용을 지불하고서라도 명확성과 명료성을 높이고 싶다면 타입 어노테이션(혹은 타입 명시)를 추가할 수 있습니다; 리스트 13-4 에 정의한 클로저에 타입을 명시하는 것은 리스트 13-7 에 보이는 것과 같을 것입니다: 파일명: src/main.rs # use std::thread;\n# use std::time::Duration;\n#\nlet expensive_closure = |num: u32| -> u32 { println!(\"calculating slowly...\"); thread::sleep(Duration::from_secs(2)); num\n}; 리스트 13-7: 클로저에 파라미터와 반환값 타입에 대한 선택적 인 타입 어노테이션 추가하기 타입 어노테이션이 있으면 클로저와 함수의 문법은 더 비슷해 보입니다. 다음은 파라미터에 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 ; 첫번째 줄은 함수 정의를 보여주고, 두번째 줄은 타입을 모두 명기한 클로저 정의를 보여 줍니다. 세번째 줄은 클로저 정의에서 타입 어노테이션을 지웠고, 네번째 줄은 선택적인 중괄호를 지웠는데, 클로저 보디가 단 하나의 표현식을 갖기 때문 입니다. 이것은 모두 호출 했을 때 동일한 행위를 수행하는 유효한 정의들 입니다. 클로저 정의는 각 파라미터들과 그들의 반환값에 대해 단 하나의 추론된 구체적인 타입을 갖을 것입니다. 예를 들면, 리스트 13-8 은 파라미터로 받은 값을 그대로 반환하는 짧은 클로저의 정의를 보여줍니다. 이 클로저는 이 예제의 목적 이에외는 유용하지 않습니다. 정의에 타입 어노테이션을 추가하지 않았다는 것에 유의하세요: 클로저를 두번 호출하는데, 첫번째는 String 을 인자로 사용하고 두번째는 u32 을 사용한다면 에러가 발생합니다: 파일명: src/main.rs let example_closure = |x| x; let s = example_closure(String::from(\"hello\"));\nlet n = example_closure(5); 리스트 13-8: 두개의 다른 타입으로 추론된 타입을 갖는 클로저 호출 해보기 컴파일러는 이런 에러를 줍니다: error[E0308]: mismatched types --> src/main.rs | | let n = example_closure(5); | ^ expected struct `std::string::String`, found integral variable | = note: expected type `std::string::String` found type `{integer}` 처음 String 값으로 example_closure 을 호출하면, 컴파일러는 x 의 타입과 클로저의 반환 타입을 String 으로 추론합니다. 이 타입들은 그다음에는 example_closure 에 있는 클로저에 고정되고, 같은 클로저를 다른 타입으로 사용하려고 할 때 타입 에러를 얻게 됩니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 클로저: 환경을 캡쳐할 수 있는 익명 함수 » 클로저 타입 추론과 어노테이션","id":"229","title":"클로저 타입 추론과 어노테이션"},"23":{"body":"여러분의 “Hello, world!” 프로그램에서 어떤 일이 벌어졌는지를 상세하게 짚어보겠습니다. 여기 첫번째 퍼즐 조각이 있습니다: fn main() { } 이 라인들은 러스트의 *함수(function)*를 정의합니다. main 함수는 특별합니다: 이것은 모든 실행가능한 러스트 프로그램 내에서 첫번째로 실행되는 코드입니다. 첫번째 라인은 파라미터가 없고 아무것도 반환하지 않는 main이라는 이름의 함수를 정의합니다. 만일 파라미터가 있었다면, 파라미터들이 괄호 기호 (와 ) 내에 위치했을 것입니다. 또한 함수의 본체가 중괄호 기호 {와 }로 감싸져 있음을 주목하세요. 러스트는 모든 함수 본체들 주위에 이것들을 요구합니다. 여는 중괄호 기호를 함수 정의부와 같은 줄에 한 칸 띄워서 위치시키는 것은 좋은 스타일입니다. 이 글을 쓰는 시점에서 rustfmt라 불리우는 자동 포맷팅 도구가 개발중에 있습니다. 만일 여러분이 러스트 프로젝트를 가로지르는 표준 스타일을 고수하길 원한다면, rustfmt가 여러분의 코드를 특정한 스타일로 포매팅해줄 것입니다. 러스트 팀은 궁극적으로 이 도구가 rustc처럼 표준 러스트 배포에 포함되기를 계획하고 있습니다. 따라서 여러분이 이 책을 언제 읽는가에 따라써, 이 툴이 여러분의 컴퓨터에 이미 설치되어 있을지도 모릅니다! 더 자세한 사항에 대해서는 온라인 문서를 참고하세요. main 함수 내부에는 다음과 같은 코드가 있습니다: println!(\"Hello, world!\"); 이 라인이 이 짧은 프로그램 내의 모든 일을 합니다: 스크린에 텍스트를 출력합니다. 여기에 주목할만 한 네 가지의 중요한 디테일이 있습니다. 첫째로, 러스트 스타일은 탭이 아닌 네 개의 스페이스로 들여쓰기를 합니다. 둘째로, println!은 러스트 매크로 (macro) 라고 불립니다. 만일 대신에 함수라고 불리려면, (! 없이) println으로 입력되었어야 할 것입니다. 러스트 매크로에 대한 자세한 사항은 부록 D에서 다룰 것입니다. 지금은 !이 보통의 함수 대신 매크로를 호출하고 있음을 의미한다는 것만 알아두면 됩니다. 셋째로, 여러분은 \"Hello, world!\" *스트링 (string)*을 볼 수 있습니다. 우리는 이 스트링을 println!의 인자로 넘기고, 이 스트링이 화면에 출력됩니다. 넷째로, 우리는 이 라인을 세미콜론 ;으로 끝내는데, 이는 이 표현식이 끝났고 다음 것이 시작될 준비가 되었음을 나타냅니다. 대다수의 러스트 코드 라인들이 세미콜론으로 끝납니다.","breadcrumbs":"시작하기 » Hello, World! » 러스트 프로그램 해부하기","id":"23","title":"러스트 프로그램 해부하기"},"230":{"body":"운동 생성 앱으로 돌아갑시다. 리스트 13-6 에서, 우리의 코드는 아직도 비용이 큰 계산을 하는 클로저를 필요한 것 보다 더 많이 호출 합니다. 이 문제를 풀기위한 한가지 옵션은 비싼 비용의 클로저 결과를 재활용을 위해 변수에 저장하고 결과가 필요한 부분에서 클로저를 다시 호출하는 대신 그 변수를 사용하는 것입니다. 그러나, 이 방법은 많은 반복된 코드를 만들 수 있습니다. 운 좋게도, 다른 해결책이 있습니다. 우리는 클로저와 클로저를 호출한 결과값을 갖고 있는 구조체를 만들 수 있습니다. 그 구조체는 결과값을 필요로 할 때만 클로저를 호출 할 것이며, 결과값을 캐시에 저장해 두어 우리의 나머지 코드에서 결과를 저장하고 재사용 하지 않아도 되도록 할 것입니다. 이 패턴을 메모이제이션(memoization) 혹은 *지연 평가(lazy evaluation)*로 알고 있을 것 입니다. 구조체에서 클로저를 갖고 있도록 하기 위해, 클로저 타입을 기술 할 필요가 있는데, 구조체 정의는 각 필드의 타입을 알 필요가 있기 때문 입니다. 각 클로저 인스턴스는 자신의 유일한 익명 타입을 갖습니다: 즉, 두 클로저가 동일한 타입 서명을 갖더라도 그들의 타입은 여전히 다른 것으로 간주 됩니다. 클로저를 사용하는 구조체, 열거형, 함수 파라미터를 정의하기 위해, 10장에서 설명한 것 처럼 제네릭과 트레잇 바운드를 사용합니다. Fn 트레잇은 표준 라이브러리에서 제공 합니다. 모든 클로저들은 다음 트레잇 중 하나를 구현 합니다: Fn, FnMut, 혹은 FnOnce. 환경을 캡처하는 것에 대한 다음 절에서 이 트레잇들의 차이점들에 대해 설명할 것입니다; 이 예제에서, Fn 트레잇 을 사용할 수 있습니다. 클로저가 이 트레잇 바운드에 맞춰야 하는 파라미터와 반환값의 타입을 표현하기 위해 Fn 트레잇 바운드에 타입을 추가 합니다. 이 경우, 클로저는 파라미터 타입이 u32 이고 u32 타입을 번환하므로, 명시하는 트레잇 바운드는 Fn(u32) -> u32 입니다. 리스트 13-9 는 Cacher 구조체의 정의를 보여주는데 클로저와 선택적인 반환값을 갖고 있습니다: 파일명: src/main.rs struct Cacher where T: Fn(u32) -> u32\n{ calculation: T, value: Option,\n} 리스트 13-9: calculation 에 클로저를 담고, 선택적인 결과 를 value 에 담는 Cacher 구조체 정의하기 Cacher 구조체는 제너릭 타입 T 의 calculation 필드를 갖습니다. T 에 대한 트레잇 바운드는 Fn 트레잇을 사용하여 그것이 클로저라는 것을 기술 합니다. calculation 필드에 저장하고자 하는 클로저는 하나의 u32 타입 파라미터 (Fn 다음에 괄호안에 명시됨)를 갖고 u32 (-> 다음에 명시됨) 타입의 값을 반환해야 합니다. 노트: 함수는 세개의 Fn 트레잇도 모두 구현 합니다. 환경에서 값을 캡처할 필요 가 없다면, Fn 트레잇을 구현한 어떤것을 필요로 하는 곳에 클로저 대신 함수를 사용할 수 있습니다. value 필드는 Option 타입 입니다. 클로저를 실행하기 전에는 value 는 None 일 것입니다. Cacher 를 사용하는 코드에서 클로저의 결과 를 요청할 경 우, Cacher 는 그 때 클로저를 실행하고 결과를 Some variant 에 넣어서 value 필드에 저장 할 것입니다. 그 다음에는 코드에서 클로저의 결과를 다시 요청하면 클로저를 다시 실행하는 대신, Cacher 는 Some variant 안에 있는 결과를 돌려줄 것입니다. 방금 설명한 value 필드에 대한 로직은 리스트 13-10 에 정의되어 있습니다: 파일명: src/main.rs # struct Cacher\n# where T: Fn(u32) -> u32\n# {\n# calculation: T,\n# value: Option,\n# }\n#\nimpl Cacher where T: Fn(u32) -> u32\n{ fn new(calculation: T) -> Cacher { Cacher { calculation, value: None, } } fn value(&mut self, arg: u32) -> u32 { match self.value { Some(v) => v, None => { let v = (self.calculation)(arg); self.value = Some(v); v }, } }\n} 리스트 13-10: Cacher 의 캐싱 로직 우리는 이 필드에 있는 값을 호출하는 코드에서 잠재적으로 변경하도록 두기 보다 Cacher 가 구조체 필드의 값을 관리하도록 하고 싶기 때문에, 이 필드는 비공개 (private) 입니다. Cacher::new 함수는 제네릭 파라미터 T 를 받는데, Cacher 구조체와 동일한 트레잇 바운드를 갖도록 정의 되었습니다. 그 다음 Cacher::new 는 calculation 필드에 명시된 클로저를 포함하고 클로저를 아직 실행한적이 없기 때문에 value 필드가 None 값을 갖는 Cacher 인스턴스를 반환 합니다. 호출하는 코드에서 클로저를 평가한 결과값을 원할때, 클로저를 직접 호출하기 보다, value 메서드를 호출 할 것입니다. 이 메서드는 이미 self.value 에 결과값을 Some 으로 갖고 있는지 체크 합니다; 만약 그렇다면 클로저를 다시 실행하는 대신 Some 안에 있는 값을 반환 합니다. 만약 self.value 가 None 이라면, self.calculation 에 저장된 클로저를 호출 하고, 나중에 재사용 하기 위해 결과를 self.value 에 저장한 다음 그 값을 반환 합니다. 리스트 13-11 는 리스트 13-6 에 있는 generate_workout 함수에서 이 Cacher 구조 체를 사용하는 방법을 보여줍니다: 파일명: src/main.rs # use std::thread;\n# use std::time::Duration;\n#\n# struct Cacher\n# where T: Fn(u32) -> u32\n# {\n# calculation: T,\n# value: Option,\n# }\n#\n# impl Cacher\n# where T: Fn(u32) -> u32\n# {\n# fn new(calculation: T) -> Cacher {\n# Cacher {\n# calculation,\n# value: None,\n# }\n# }\n#\n# fn value(&mut self, arg: u32) -> u32 {\n# match self.value {\n# Some(v) => v,\n# None => {\n# let v = (self.calculation)(arg);\n# self.value = Some(v);\n# v\n# },\n# }\n# }\n# }\n#\nfn generate_workout(intensity: u32, random_number: u32) { let mut expensive_result = Cacher::new(|num| { println!(\"calculating slowly...\"); thread::sleep(Duration::from_secs(2)); num }); if intensity < 25 { println!( \"Today, do {} pushups!\", expensive_result.value(intensity) ); println!( \"Next, do {} situps!\", expensive_result.value(intensity) ); } else { if random_number == 3 { println!(\"Take a break today! Remember to stay hydrated!\"); } else { println!( \"Today, run for {} minutes!\", expensive_result.value(intensity) ); } }\n} 리스트 13-11: 캐싱 로직을 추상화 하기 위해 generate_workout 함수 안에서 Cacher 사용하기 클로저를 변수에 직접 저장하는 대신, 클로저를 갖는 Cacher 의 새 인스턴스를 저장 했습니다. 그러고는, 결과가 필요한 각 위치에 Cacher 인스턴스의 value 메소드를 호출 했습니다. 우리는 value 메소드를 원하는 만큼 많이 호출할 수 있고, 전혀 호출하지 않을 수도 있으며, 비싼 비용의 게산은 최대 한 번만 수행 될 것입니다. 리스트 13-2 의 main 함수로 이 프로그램을 실행해 보세요. 다양한 if 와 else 블럭에 있는 모든 케이스들을 검증하기 위해 simulated_user_specified_value 와 simulated_random_number 변수들을 변경해 보면, calculating slowly... 메세지는 필요할 때 단지 한 번만 나타 납니다. Cacher 는 필요한것 보다 더 많이 비싼 비용의 계산을 호출하지 않도록 보장하는 필요한 로직을 처리해서, generate_workout 가 비즈니스 로직에 집중하도록 해줍니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 클로저: 환경을 캡쳐할 수 있는 익명 함수 » 제너릭 파라미터와 Fn 트레잇을 사용하여 클로저 저장하기","id":"230","title":"제너릭 파라미터와 Fn 트레잇을 사용하여 클로저 저장하기"},"231":{"body":"값을 캐싱하는 것은 일반적으로 유용한 동작이기 때문에 이와는 다른 클로저를 사용 해서 우리 코드의 다른 부분에서 적용하고 싶을 수도 있습니다. 그러나 현재 Cacher 구현은 다른 문맥에서 다르게 재사용 하기에는 두 가지 문제가 있습니다. 첫 번째 문제는 Cacher 인스턴스가 value 메소드의 arg 파라미터에 대해 항상 같은 값을 얻는다는 가정을 한다는 것입니다. 즉, 이 Cacher 테스트는 실패 할 것 입니다: #[test]\nfn call_with_different_values() { let mut c = Cacher::new(|a| a); let v1 = c.value(1); let v2 = c.value(2); assert_eq!(v2, 2);\n} 이 테스트는 인자로 받은 값을 그대로 돌려주는 클로저가 포함된 새로운 Cacher 인스턴스를 생성 합니다. arg 값을 1로 그리고 arg 값을 2로 해서 이 Cacher 인스턴스의 value 메소드를 호출하고, arg 값을 2로 value 를 호출 했을 때 2를 반환 할 것으로 기대 합니다. 리스트 13-9 와 13-10 에 있는 Cacher 구현에 대해 이 테스트를 돌리면, 테스트는 이 메세지와 함께 assert_eq! 에서 실패 할 것입니다: thread 'call_with_different_values' panicked at 'assertion failed: `(left == right)` left: `1`, right: `2`', src/main.rs 문제는 처음 c.value 을 1로 호출 했을 때, Cacher 인스턴스는 self.value 에 Some(1) 을 저장 합니다. 그 후에, value 값으로 무엇을 넘기던, 항상 1을 반환 할 것입니다. Cacher 이 하나의 값보다 해시맵을 사용하도록 수정해 봅시다. 해시맵의 키는 넘겨받은 arg 값이 될 것이고, 해시맵의 값은 그 키로 클로저를 호출한 결과가 될 것입니다. self.value 가 Some 혹은 None 값인지 직접 살펴보는 대신, value 함수는 해시맵의 arg 값을 살펴보고 값이 있으면 반환 할 것입니다. 값이 없으면, Cacher 는 클로저를 호출해서 해당 arg 값과 연관된 해시맵에 결과값을 저장 할 것입니다. 현재 Cacher 구현의 두 번째 문제는 u32 타입 파라미터 한 개만 받고 하나의 u32 을 반환한다는 것입니다. 예를 들면, 문자열 슬라이스를 넘겨주고 usize 값을 반환하는 클로저의 결과를 캐시에 저장하고 싶을 수도 있습니다. 이 이슈를 수정 하기 위해, Cacher 기능에 유연성을 높여주도록 더 중립적인 파라미터를 사용해 봅시다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 클로저: 환경을 캡쳐할 수 있는 익명 함수 » Cacher 구현의 제약사항","id":"231","title":"Cacher 구현의 제약사항"},"232":{"body":"운동 생성 예제에서, 우리는 클로저를 단지 인라인 익명 함수로 사용 했습니다. 그러나 클로저는 함수에 없는 추가적인 능력을 갖고 있습니다: 환경을 캡처해서 클로저가 정의된 스코프의 변수들을 접근할 수 있습니다. equal_to_x 변수에 저장된 클로저가 클로저를 둘러싼 환경에 있는 x 변수를 사용하는 예제가 리스트 13-12 에 있습니다: 파일명: src/main.rs fn main() { let x = 4; let equal_to_x = |z| z == x; let y = 4; assert!(equal_to_x(y));\n} 리스트 13-12: 둘러싼 범위에 있는 변수를 참조하는 클로저의 예 비록 x 가 equal_to_x 의 파라미터 중에 하나가 아니더라도, equal_to_x 는 equal_to_x 가 정의된 동일한 스코프에 정의된 x 변수를 사용하는 것이 허용 됩니다. 함수로는 이와 동일하게 할 수 없습니다; 다음 예제로 시도해 보면, 코드는 컴파일 되지 않습니다: Filename: src/main.rs fn main() { let x = 4; fn equal_to_x(z: i32) -> bool { z == x } let y = 4; assert!(equal_to_x(y));\n} 에러가 발생 합니다: error[E0434]: can't capture dynamic environment in a fn item; use the || { ...\n} closure form instead --> src/main.rs |\n4 | fn equal_to_x(z: i32) -> bool { z == x } | ^ 컴파일러는 이것은 클로저에서만 동작한다고 상기시켜 주기까지 합니다! 클로저가 그것의 환경에서 값을 캡처할 때, 클로저 바디에서 사용하기 위해 그 값을 저장하기 위한 메모리를 사용 합니다. 이 메모리 사용은 환경을 캡처하지 않는 코드를 실행하길 원하는 더 흔한 상황에서는 지불하기 싶지 않은 오버헤드 입니다. 왜냐하면 함수는 그들의 환경을 캡처할 수 없기 때문에, 함수를 정의하고 사용하는데 결코 이런 오버헤드는 발생하지 않을 것이기 때문 입니다. 클로저는 세가지 방식으로 그들의 환경에서 값을 캡처 할 수 있는데, 함수가 파라미터 를 받는 세가지 방식과 직접 연결 됩니다: 소유권 받기, 불변으로 빌려오기, 가변으로 빌려오기. 이것들은 다음과 같이 세개의 Fn 트레잇으로 표현 합니다: FnOnce 는 클로저의 환경 으로 알고 있는, 그것을 둘러싼 환경에서 캡처한 변수 들을 소비합니다. 캡처한 변수를 소비하기 위해, 클로저는 이 변수의 소유권을 가져야 하고 그것이 정의될 때 클로저 안으로 그것들을 옮겨와야 합니다. 이름의 일부인 Once 는 그 클로저가 동일한 변수들에 대해 한번이상 소유권을 얻을수 없다는 사실을 의미하며, 그래서 한 번만 호출 될 수 있습니다. Fn 은 그 환경으로 부터 값들을 불변으로 빌려 옵니다. FnMut 값들을 가변으로 빌려오기 때문에 그 환경을 변경할 수 있습니다. 우리가 클로저를 만들때, 러스트는 클로저가 환경에 있는 값을 어떻게 사용하는지에 근거 해서 어떤 트레잇을 사용할지 추론 합니다. 리스트 13-12 에서, equal_to_x 클로저의 바디에서는 x 에 있는 값을 읽기만 하면 되기 때문에 클로저는 x 를 불변으로 빌려 옵니다. (그래서 equal_to_x 은 Fn 트래잇 입니다) 만약 클로저가 환경으로부터 사용하는 값에 대해 소유권을 갖도록 강제하고 싶다면, 파라미터 리스트 앞에 move 키워드를 사용할 수 있습니다. 이 기법은 클로저를 다른 쓰레드로 넘길때 데이터를 이동시켜 새로운 쓰레드가 소유하도록 할때 대부분 유용 합니다. 16장에 병렬성에 대해 이야기 하는 부분에서 더 많은 move 클로저에 대한 예제가 있습니다. 지금은 리스트 13-12 의 코드에서 클로저에 move 키워드를 추가하고 정수 대신 벡터를 사용하도록 했는데, 정수는 이동되지 않고 복사되기 때문 입니다; 이 코드는 아직 컴파일 되지 않습니다: 파일명: src/main.rs fn main() { let x = vec![1, 2, 3]; let equal_to_x = move |z| z == x; println!(\"can't use x here: {:?}\", x); let y = vec![1, 2, 3]; assert!(equal_to_x(y));\n} 아래와 같은 에러가 발생합니다: error[E0382]: use of moved value: `x` --> src/main.rs:6:40 |\n4 | let equal_to_x = move |z| z == x; | -------- value moved (into closure) here\n5 |\n6 | println!(\"can't use x here: {:?}\", x); | ^ value used here after move | = note: move occurs because `x` has type `std::vec::Vec`, which does not implement the `Copy` trait move 키워드를 추가했기 때문에 클로저가 정의될 때 x 값은 클로저 안으로 이동됩니다. x 의 소유권은 클로저가 갖게 되었고, main 은 더 이상 println! 문에서 x 사용하도록 허용되지 않습니다. println! 를 삭제하면 이 예제는 수정 됩니다. Fn 트래잇 바운드 중 하나를 기술할 때 대부분의 경우, Fn 으로 시작해보면 컴파일러는 클로저 바디에서 무슨일을 하는지에 근거해서 FnMut 혹은 FnOnce 이 필요한지 말해 줍니다. 클로저가 그들의 환경을 캡처할 수 있는 상황을 표현하는 것은 함수 파라미터로써 유용 합니다. 다음 주제로 넘어가 봅시다: 반복자.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 클로저: 환경을 캡쳐할 수 있는 익명 함수 » 클로저로 환경 캡처 하기","id":"232","title":"클로저로 환경 캡처 하기"},"233":{"body":"반복자 패턴은 일련의 항목들에 대해 순서대로 어떤 작업을 수행할 수 있도록 해줍 니다. 반복자는 각 항목들을 순회하고 언제 시퀀스가 종료될지 결정하는 로직을 담당 합니다. 반복자를 사용하면, 저런 로직을 다시 구현할 필요가 없습니다. 러스트에서, 반복자는 게으른데 , 항목들을 사용하기위해 반복자를 소비하는 메서드를 호출하기 전까지 반복자는 아무런 동작을 하지 않습니다. 예를 들면, 리스트 13-13 의 코드는 Vec 에 정의된 iter 메서드를 호출함으로써, 벡터 v1 에 있는 항목들에 대한 반복자를 생성 합니다. 이 코드 자체로는 어떤 유용한 동작을 하진 않습니다. let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); 리스트 13-13: 반복자 생성하기 일단 반복자를 만들면, 다양한 방법으로 사용할 수 있습니다. 3장의 리스트 3-5 에서, 각 항목에 대해 어떤 코드를 수행하기 위해 for 루프에서 반복자를 사용 했습니다만, 지금까지 iter 에 대한 호출이 무엇을 했는지 대충 넘어 갔었습니다. 리스트 13-14 의 예제는 for 루프에서 반복자를 사용하는 부분에서 반복자 생성을 분리 했습니다. 반복자는 v1_iter 변수에 저장되고, 그 시점에 순회는 발생하지 않습니다. v1_iter 에 있는 반복자를 사용하는 for 루프가 호출되면, 루프 순회 마다 반복자의 각 요소가 사용되는데, 각각의 값을 출력 합니다. let v1 = vec![1, 2, 3]; let v1_iter = v1.iter(); for val in v1_iter { println!(\"Got: {}\", val);\n} 리스트 13-14: for 루프에서 반복자 사용하기 표준 라이브러리에서 반복자를 제공하지 않는 언어에서는, 변수를 인덱스 0으로 시작해서, 그 변수로 벡터를 색인해서 값을 가져오는데 사용하며, 루프안에서 벡터에 있는 아이템의 총 갯수까지 그 변수를 증가시키는 방식으로 동일한 기능을 작성할 수 있습니다. 반복자는 그러한 모든 로직을 대신 처리 하며, 잠재적으로 엉망이 될 수 있는 반복적인 코드를 줄여 줍니다. 반복자는 벡터처럼 색인할 수 있는 자료구조 뿐만 아니라, 많은 다른 종류의 시퀀스에 대해 동일한 로직을 사용할 수 있도록 더 많은 유연성을 제공 합니다. 반복자가 어떻게 그렇게 하는지 살펴 봅시다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 반복자로 일련의 항목들 처리하기 » 반복자로 일련의 항목들 처리하기","id":"233","title":"반복자로 일련의 항목들 처리하기"},"234":{"body":"모든 반복자는 표준 라이브러리에 정의된 Iterator 라는 이름의 트레잇을 구현 합니 다. 트레잇의 정의는 아래와 같습니다: trait Iterator { type Item; fn next(&mut self) -> Option; // methods with default implementations elided\n} 이 정의는 몇 개의 새로운 문법을 사용하는 것에 유의하세요: type Item 과 Self::Item 은 이 트레잇과 연관 타입 을 정의 합니다. 우리는 19장에서 연관 타입에 대해 자세히 이야기 할 것 입니다. 지금 당장 알아야 할 것은 이 코드가 Iterator 트레잇을 구현하는 것은 Item 타입을 정의하는 것 또한 요구하며, 이 Item 타입이 next 메서드의 리턴 타입으로 사용된다는 것을 나타낸다는 것 입니 다. 다른 말로, Item 타입은 반복자로 부터 반환되는 타입이 될 것 입니다. Iterator 트레잇은 단지 구현자가 하나의 메서드를 정의하도록 요구 합니다: next 메서드 입니다. 이 메서드는 반복자의 하나의 항목을 Some 에 넣어서 반환 하고, 반복자가 종료되면 None 을 반환 합니다. 반복자의 next 메서드를 직접 호출할 수 있습니다; 리스트 13-15 는 벡터로 부터 생성된 반복자에 대해 반복된 next 호출이 어떤 값들을 반환하는지 보여줍니다: Filename: src/lib.rs #[test]\nfn 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-15: 반복자의 next 메서드 호출하기 v1_iter 가 변경 가능하도록 만들 필요가 있다는 것에 유의 하세요: 반복자에 대해 next 메서드를 호출하면 시퀀스의 어디에 있는지 추적하기 위해 반복자가 사용하는 내부 상태를 변경합니다. 다른 말로, 이 코드는 반복자를 소비 합니다 , 혹은 다 써 버립니다. next 에 대한 각 호출은 반복자로 부터 하나의 항목을 소비 합니다. for 루프를 사용할 때는 v1_iter 를 변경할 수 있도록 만들 필요가 없는데, 루프가 v1_iter 의 소유권을 갖고 내부적으로 변경 가능하도록 만들기 때문 입니다. next 호출로 얻어온 값들은 벡터 안에 있는 값들에 대한 불변 참조라는 점 역시 유의 하세요. iter 메서드는 불변 참조에 대한 반복자를 만듭니다. 만약 v1 의 소유권을 갖고 소유된 값들을 반환하도록 하고 싶다면, iter 대신 into_iter 를 호출해야 합니다. 비슷하게, 가변 참조에 대한 반복자를 원한다면, iter 대신 iter_mut 을 호출할 수 있습니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 반복자로 일련의 항목들 처리하기 » Iterator트레잇과 next 메서드","id":"234","title":"Iterator트레잇과 next 메서드"},"235":{"body":"Iterator 트레잇에는 표준 라이브러리에서 기본 구현을 제공하는 다수의 다른 메서드들이 있습니다; Iterator 트레잇에 대한 표준 라이브러리 API 문서를 살펴 보면, 이 메서드들을 찾을 수 있습니다. 이 메서드들 중 일부는 그들의 구현에서 next 메서드를 호출하는데, 이것이 Iterator 트레잇을 구현할 때 next 메서드를 구현해야만 하는 이유 입니다. next 를 호출하는 메서드들을 소비하는 어댑터들 이라고 하는데, 그들을 호출하면 반복자를 써버리기 때문 입니다. sum 메서드가 하나의 예인데, 반복자의 소유권을 가져오고 반복적으로 next 를 호출해서 순회함으로써 반복자를 소비 합니다. 순회해 나가면서 누적합계에 각 아이템을 더하고 순회가 완료되면 합계를 반환 합니다. 리스트 13-16 은 sum 메서드의 사용을 보여주는 테스트 입니다: Filename: src/lib.rs #[test]\nfn 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-16: 반복자의 모든 항목에 대한 합계를 얻기 위해 sum 메서드 호출 하기 sum 은 호출한 반복자의 소유권을 갖기 때문에, sum 을 호출한 후 v1_iter 은 사용할 수 없습니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 반복자로 일련의 항목들 처리하기 » 반복자를 소비하는 메서드들","id":"235","title":"반복자를 소비하는 메서드들"},"236":{"body":"Iterator 트레잇에 정의된 다른 메서드들 중에 반복자 어댑터들 로 알려진 메서드 들은 반복자를 다른 종류의 반복자로 변경하도록 허용 합니다. 복잡한 행위를 수행하 기 위해 읽기 쉬운 방법으로 반복자 어댑터에 대한 여러개의 호출을 연결할 수 있습 니다. 하지만 모든 반복자는 게으르기 때문에, 반복자 어댑터들로 부터 결과를 얻기 위해 소비하는 메서드들 중 하나를 호출 해야 합니다. 리스트 13-17 은 반복자 어댑터 메서드인 map 을 호출하는 예를 보여주는데, 새로운 반복자를 생성하기 위해 각 항목에 대해 호출할 클로저를 인자로 받습니다. 여기서 클로저는 벡터의 각 항목에서 1이 증가된 새로운 반복자를 만듭니다. 그러나, 이 코드는 경고를 발생 합니다: Filename: src/main.rs let v1: Vec = vec![1, 2, 3]; v1.iter().map(|x| x + 1); 리스트 13-17: 새로운 반복자를 만들기 위해 반복자 어댑터 map 호출 하기 경고 메세지는 이것 입니다: warning: unused `std::iter::Map` which must be used: iterator adaptors are lazy\nand do nothing unless consumed --> src/main.rs:4:5 |\n4 | v1.iter().map(|x| x + 1); | ^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: #[warn(unused_must_use)] on by default 리스트 13-17 의 코드는 아무것도 하지 않습니다; 인자로 넘긴 클로저는 결코 호출 되지 않습니다. 경고는 이유를 알도록 해주니다: 반복자 어댑터는 게으르고, 반복자를 여기서 소비할 필요가 있다. 이것을 고치고 반복자를 소비하기 위해, collect 메서드를 사용할 것인데, 12장의 리스트 12-1 에서 env::args 와 함께 사용했습니다. 이 메서드는 반복자를 소비하고 결과값을 수집 데이터 타입으로 모읍니다. 리스트 13-18 에서, 벡터에 대한 map 호출로 부터 반환된 반복자를 순회하면서 결과를 모읍니다. 이 벡터는 각 항목이 원본 벡터로 부터 1씩 증가된 상태로 될 것 입니다. Filename: src/main.rs let v1: Vec = vec![1, 2, 3]; let v2: Vec<_> = v1.iter().map(|x| x + 1).collect(); assert_eq!(v2, vec![2, 3, 4]); 리스트 13-18: 새로운 반복자를 만들기 위해 map 메서드를 호출하고, 새로운 반복자를 소비하고 벡터를 생성하기 위해 collect 메서드 호출 하기 map 은 클로저를 인자로 받기 때문에, 각 항목에 대해 수행하기를 원하는 어떤 연산도 기술할 수 있습니다. 이것은 Iterator 트레잇이 제공하는 반복자 행위를 재사용 하면서 클로저가 어떻게 일부 행위를 맞춤 조작할 수 있는지를 보여주는 굉장한 예제 입니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 반복자로 일련의 항목들 처리하기 » 다른 반복자를 생성하는 메서드들","id":"236","title":"다른 반복자를 생성하는 메서드들"},"237":{"body":"이제 반복자를 소개했으니, filter 반복자 어댑터를 사용해서 환경을 캡쳐하는 클로저의 일반적인 사용을 보여줄 수 있습니다. 반복자의 filter 메서드는 반복자로 부터 각 항목을 받아 Boolean 을 반환하는 클로저를 인자로 받습니다. 만약 클로저가 true 를 반환하면, 그 값은 filter 에 의해 생성되는 반복자에 포함될 것 입니다. 클로저가 false 를 반환하면, 결과로 나오는 반복자에 포함되지 않을 것 입니다. 리스트 13-19 에서, Shoe 구조체 인스턴스들의 컬렉션을 순회하기 위해 filter 와 그 환경으로 부터 shoe_size 변수를 캡쳐하는 클로저를 사용 합니다. 그것은 기술된 크기의 신발들만 반환 할 것 입니다. Filename: src/lib.rs #[derive(PartialEq, Debug)]\nstruct Shoe { size: u32, style: String,\n} fn shoes_in_my_size(shoes: Vec, shoe_size: u32) -> Vec { shoes.into_iter() .filter(|s| s.size == shoe_size) .collect()\n} #[test]\nfn 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_my_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-19: shoe_size 를 캡쳐하는 클로저와 filter 메서드 사용하기 shoes_in_my_size 함수는 파라미터로 신발들의 벡터에 대한 소유권과 신발 크기를 받습니다. 그것은 지정된 크기의 신발들만을 포함하는 벡터를 반환 합니다. shoes_in_my_size 의 구현부에서, 벡터의 소유권을 갖는 반복자를 생성하기 위해 into_iter 를 호출 합니다. 그 다음 그 반복자를 클로저가 true 를 반환한 요소들만 포함하는 새로운 반복자로 바꾸기 위해 filter 를 호출 합니다. 클로저는 환경에서 shoe_size 매개 변수를 캡처하고, 지정된 크기의 신발만 유지하면서 각 신발의 크기와 값을 비교합니다. 마지막으로,collect를 호출하면 적용된 반복자에 의해 리턴된 값을 함수가 리턴한 벡터로 모으게됩니다. 테스트는 shoes_in_my_size 를 호출 했을 때, 지정된 값과 동일한 사이즈를 갖는 신발들만 돌려받는 다는 것을 보여 줍니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 반복자로 일련의 항목들 처리하기 » 환경을 캡쳐하는 클로저 사용하기","id":"237","title":"환경을 캡쳐하는 클로저 사용하기"},"238":{"body":"벡터에 대해 iter, into_iter 혹은 iter_mut 을 호출해서 반복자를 생성할 수 있다는 것을 보았습니다. 해시맵과 같은 표준 라이브러리에 있는 다른 컬렉션 타입으로 부터 반복자를 생성할 수 있습니다. 자신만의 타입에 대해 Iterator 트레잇을 구현함으로써 원하는 동작을하는 반복자를 생성하는것 역시 가능 합니다. 이전에 언급했던 것 처럼, 정의를 제공해야 하는 유일한 메서드는 next 메서드 입 니다. 그러고 나면, Iterator 트레잇에서 제공하는 기본구현을 갖는 다른 모든 메서드를 사용할 수 있습니다! 이것을 보여주기 위해 1부터 5까지 셀 수있는 반복자를 만듭니다. 우선, 어떤 값들을 유지하는 구조체를 만들 것 입니다. 그 다음 Iterator 트레잇을 구현하고 그 구현에서 값들을 사용함으로써 이 구조체를 반복자로 만들 것 입니다. 리스트 13-20 에는 Counter 구조체의 정의와 Counter 인스턴스를 생성하는 연관된 new 함수가 있습니다: Filename: src/lib.rs struct Counter { count: u32,\n} impl Counter { fn new() -> Counter { Counter { count: 0 } }\n} 리스트 13-20: Counter 구조체와 count 의 초기값 0 으로 Counter 의 인스턴스를 생성하는 new 함수 정의하기 Counter 구조체는 count 라는 이름의 하나의 필드를 갖습니다. 이 필드는 u32 타입의 값을 갖는데 1부터 5까지 순회하는데 어디까지 진행했는지를 추적할 것 입니다. count 필드는 Counter 구현이 그 값을 관리하길 원하기 때문에 외부로 노출되지 않습니다. new 함수는 항상 새로운 인스턴스가 count 필드에 0을 담은 채로 시작하도록 강제합니다. 다음으로, 이 반복자가 사용될 때 우리가 원하는 것을 지정하기 위해 next 메소드의 본문을 정의함으로써 Counter 타입에 대한 Iterator 특성을 구현할 것입니다, 리스트 13-21 참조: Filename: src/lib.rs # struct Counter {\n# count: u32,\n# }\n#\nimpl Iterator for Counter { type Item = u32; fn next(&mut self) -> Option { self.count += 1; if self.count < 6 { Some(self.count) } else { None } }\n} 리스트 13-21: Counter 구조체에 대해 Iterator 트레잇 구현하기 우리의 반복자를 위해 연관된 Item 타입을 u32 로 지정했는데, 이는 반복자가 u32 값을 반환한다는 것을 의미 합니다. 다시, 아직 연관 타입에 대해 걱정하시 마세요, 19장에서 다룰 것입니다. 우리는 우리의 반복자가 현재 상태에 1을 더하길 원합니다, 그래서 count 를 0 으로 초기화 했고 처음엔 1을 반환할 것 입니다. count 의 값이 6 보다 작다면, next 는 Some 으로 포장된 현재 값을 리턴할 것이며, count 가 6 이거나 더 크다면, 우리의 반복자는 None 을 반환할 것 입니다. Counter 반복자의 next 메서드 사용하기 Iterator 트레잇을 구현 했다면, 반복자를 갖게 됩니다! 리스트 13-22 는 리스트 13-15 에서 벡터로 부터 생셩된 반복자에 했던 것 처럼, Counter 구조체에 직접 next 메서드를 호출 함으로써 반복자 기능을 사용할 수 있다는 것을 보여주는 테스트를 보여 줍니다. Filename: src/lib.rs # struct Counter {\n# count: u32,\n# }\n#\n# impl Iterator for Counter {\n# type Item = u32;\n#\n# fn next(&mut self) -> Option {\n# self.count += 1;\n#\n# if self.count < 6 {\n# Some(self.count)\n# } else {\n# None\n# }\n# }\n# }\n#\n#[test]\nfn calling_next_directly() { let mut counter = Counter::new(); assert_eq!(counter.next(), Some(1)); assert_eq!(counter.next(), Some(2)); assert_eq!(counter.next(), Some(3)); assert_eq!(counter.next(), Some(4)); assert_eq!(counter.next(), Some(5)); assert_eq!(counter.next(), None);\n} 리스트 13-22: next 메서드 구현의 기능 테스트 이 테스트는 counter 변수에 새로운 Counter 인스턴스를 생성하고 next 를 반복적으로 호출하면서, 이 반복자가 우리가 원하는 행위를 구현했다는 것을 검증 합니다: 1 부터 5까지의 값을 반환함. 다른 Iterator 메서드들 사용하기 우리는 next 메서드를 정의함으로써 Iterator 트레잇을 구현했습니다, 그래서 표준 라이브러리에 정의된 Iterator 트레잇 메서드들의 기본 구현을 사용할 수 있 는데, 그들은 모두 next 메서드의 기능을 사용하기 때문 입니다. 예를 들면, 만약 어떤 이유에서든 Counter 인스턴스에 의해 생성된 값들을 얻고, 다른 Counter 인스턴스에 의해 생성된 값과 쌍을 이루며, 각 쌍을 함께 곱하고, 3으로 나눠지는 값들만 유지하며, 모든 결과 값을 함께 더하고 싶다면, 리스트 12-23 의 테스트에서 보여지는 것처럼, 그렇게 할 수 있습니다: Filename: src/lib.rs # struct Counter {\n# count: u32,\n# }\n#\n# impl Counter {\n# fn new() -> Counter {\n# Counter { count: 0 }\n# }\n# }\n#\n# impl Iterator for Counter {\n# // Our iterator will produce u32s\n# type Item = u32;\n#\n# fn next(&mut self) -> Option {\n# // increment our count. This is why we started at zero.\n# self.count += 1;\n#\n# // check to see if we've finished counting or not.\n# if self.count < 6 {\n# Some(self.count)\n# } else {\n# None\n# }\n# }\n# }\n#\n#[test]\nfn using_other_iterator_trait_methods() { let sum: u32 = Counter::new().zip(Counter::new().skip(1)) .map(|(a, b)| a * b) .filter(|x| x % 3 == 0) .sum(); assert_eq!(18, sum);\n} 리스트 13-23: Counter 반복자에 대해 Iterator 트레잇의 다양햔 메서드 사용하기 zip 은 단지 네 개의 쌍을 생성한다는데 유의 하세요; 이론적으로 다섯번째 쌍인 (5, None) 은 결코 생성되지 않는데, zip 은 입력 반복자 중 하나라도 None 을 반환하면 None 을 반환하기 때문 입니다. 우리가 next 메서드가 어떻게 동작하는지에 대해 기술했기 때문에 이 모든 메서드 호출이 가능하며, 표준 라이브러리는 next 를 호출하는 다른 메서드들의 기본 구현 을 제공 합니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 반복자로 일련의 항목들 처리하기 » Iterator 트레잇으로 자신만의 반복자 만들기","id":"238","title":"Iterator 트레잇으로 자신만의 반복자 만들기"},"239":{"body":"반복자에 대한 새로운 지식을 사용하여 12장의 I/O 프로젝트의 코드들을 더 깔끔하고 간결하게 개선할 수 있습니다. 반복자를 사용하여 어떻게 Config::new 함수와 search 함수의 구현을 개선할 수 있는지 살펴봅시다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » I/O 프로젝트 개선하기 » I/O 프로젝트 개선하기","id":"239","title":"I/O 프로젝트 개선하기"},"24":{"body":"여러분이 이제 막 새로 만든 프로그램을 실행했으므로, 이 과정의 각 단계를 검토해 봅시다. 러스트 프로그램을 실행하기 전에, 여러분은 아래와 같이 rustc 커맨드를 입력하고 여기에 여러분의 소스코드를 넘기는 식으로 러스트 컴파일러를 사용하여 이를 컴파일해야 합니다: $ rustc main.rs 만일 여러분이 C 혹은 C++ 배경지식을 갖고 있다면, 이것이 gcc 혹은 clang과 유사하다는 것을 눈치챘을 것입니다. 컴파일을 성공적으로 한 뒤, 러스트는 실행가능한 바이너리를 출력합니다. Linux, macOS, 그리고 Windows의 파워쉘 상에서는 여러분의 쉘에 다음과 같이 ls 커맨드를 입력하여 이 실행 파일을 볼 수 있습니다: $ ls\nmain main.rs Windows의 CMD 환경에서는 다음과 같이 입력해야 합니다: > dir /B %= the /B option says to only show the file names =%\nmain.exe\nmain.pdb\nmain.rs 이 커맨드는 .rs 확장자를 가진 소스 코드 파일, 실행 파일 (Windows에서는 main.exe , 다른 모든 플랫폼에서는 main ), 그리고 만일 CMD를 이용하는 중이라면, .pdb 확장자를 가지고 있는 디버깅 정보를 담고 있는 파일을 보여줍니다. 여기서 여러분은 아래와 같이 main 혹은 main.exe 파일을 실행합니다: $ ./main # or .\\main.exe on Windows 만일 main.rs 가 여러분의 “Hello, world!” 프로그램이었다면, 위의 라인이 여러분의 터미널에 Hello, world!라고 출력해줄 것입니다. 여러분이 루비, 파이썬, 자바스크립트와 같은 동적 언어에 더 친숙하다면, 아마도 프로그램의 컴파일과 실행을 개별적인 단계로 이용하지 않았을지도 모릅니다. 러스트는 ahead-of-time compiled 언어인데, 이는 여러분이 프로그램을 컴파일하고, 그 실행파일을 다른 이들에게 주면, 그들은 러스트를 설치하지 않고도 이를 실행할 수 있다는 의미입니다. 만일 여러분이 누군가에게 .rb , .py 혹은 .js 파일을 준다면, 그는 (각각) 루비, 파이썬, 혹은 자바스크립트 구현체가 설치되어 있어야 합니다. 하지만 그러한 언어들에서는 하나의 커맨드로 여러분의 프로그램을 컴파일하고 실행할 수 있습니다. 언어 디자인에서는 모든 것이 트레이드 오프입니다. 간단한 프로그램에 대해 그낭 rustc만으로 컴파일하는 것은 괜찮지만, 여러분의 프로젝트가 커지면서, 여러분은 모든 옵션을 관리하고 여러분의 코드를 공유하기 쉽도록 하길 원할 것입니다. 다음 절에서 우리는 여러분에게 Cargo 도구를 소개할 것인데, 이것이 여러분의 실생활 러스트 프로그램 작성을 도와줄 것입니다.","breadcrumbs":"시작하기 » Hello, World! » 컴파일과 실행은 개별적인 단계입니다","id":"24","title":"컴파일과 실행은 개별적인 단계입니다"},"240":{"body":"리스트 12-6 에서, String 값의 슬라이스를 받고 슬라이스를 인덱싱하고 복사 함으로써 Config 구조체의 인스턴스를 생성하였고, Config 구조체가 이 값들을 소유하도록 했습니다. 리스트 13-24 에서는 리스트 12-23 에 있던 것 처럼 Config::new 함수의 구현을 다시 재현 했습니다: 파일명: src/lib.rs impl Config { pub fn new(args: &[String]) -> Result { if args.len() < 3 { return Err(\"not enough arguments\"); } let query = args[1].clone(); let filename = args[2].clone(); let case_sensitive = env::var(\"CASE_INSENSITIVE\").is_err(); Ok(Config { query, filename, case_sensitive }) }\n} 리스트 13-24: 리스트 12-23 의 Config::new 함수 재현 그 당시, 비효율적인 clone 호출에 대해 걱정하지 말라고 얘기 했으며 미래에 없앨 것이라고 했습니다. 자, 그때가 되었습니다! String 요소들의 슬라이스를 args 파라미터로 받았지만 new 함수는 args 를 소유하지 않기 때문에 clone 이 필요했습니다. Config 인스턴스의 소유권을 반환하기 위해, Config 의 query 와 filename 필드로 값을 복제 함으로써 Config 인스턴스는 그 값들을 소유할 수 있습니다. 반복자에 대한 새로운 지식으로, 인자로써 슬라이스를 빌리는 대신 반복자의 소유권을 갖도록 new 함수를 변경할 수 있습니다. 슬라이스의 길이를 체크하고 특정 위치로 인덱싱을 하는 코드 대신 반복자의 기능을 사용할 것 입니다. 이것은 반복자가 값에 접근 할 것이기 때문에 Config::new 함수가 무엇을 하는지를 명확하게 해줄 것 입니다. Config::new 가 반복자의 소유권을 갖고 빌린 값에 대한 인덱싱을 사용하지 않게 된다면, clone 을 호출하고 새로운 할당을 만드는 대신 String 값들을 반복자에서 Config 로 이동할 수 있습니다. 반환된 반복자를 직접 사용하기 I/O 프로젝트의 src/main.rs 파일을 열어보면, 아래와 같을 것 입니다: 파일명: src/main.rs fn main() { let args: Vec = env::args().collect(); let config = Config::new(&args).unwrap_or_else(|err| { eprintln!(\"Problem parsing arguments: {}\", err); process::exit(1); }); // --snip--\n} 우리는 리스트 12-24 에 있는 main 함수의 시작점을 리스트 13-25 에 있는 코드로 바꿀 것 입니다. 이것은 Config::new 도 업데이트 해야 컴파일 됩니다. 파일명: src/main.rs fn main() { let config = Config::new(env::args()).unwrap_or_else(|err| { eprintln!(\"Problem parsing arguments: {}\", err); process::exit(1); }); // --snip--\n} 리스트 13-25: Config::new 로 env::args 의 반환값 넘기기 env::args 함수는 반복자를 반환 합니다! 반복자의 값들을 벡터로 모아서 Config::new 에 슬라이스를 넘기는 대신, env::args 에서 반환된 반복자의 소유권 을 Config::new 로 직접 전달 합니다. 그 다음, Config::new 정의를 업데이트 할 필요가 있습니다. I/O 프로젝트의 src/lib.rs 파일에서, 리스트 13-26 처럼 Config::new 의 시그니처를 변경 합시다. 함수 본문을 업데이트 해야 하기 때문이 아직 컴파일 되지 않습니다. 파일명: src/lib.rs impl Config { pub fn new(mut args: std::env::Args) -> Result { // --snip-- 리스트 13-26: 반복자를 받도록 Config::new 의 시그니처 업데이트 하기 env::args 함수에 대한 표준 라이브러리 문서에는 반환하는 반복자의 타입이 std::env::Args 라고 명시되어 있습니다. Config::new 함수의 시그니처를 업데이 트 해서 args 파라미터가 &[String] 대신 std::env::Args 타입을 갖도록 했습니다. args 의 소유권을 갖고 그것을 순회하면서 args 를 변경할 것이기 때문에, 변경 가능하도록 하기 위해 args 파라미터의 명세에 mut 키워드를 추가 할 수 있습니다. 인덱싱 대신 Iterator 트레잇 메서드 사용하기 다음으로, Config::new 의 본문을 수정 할 것입니다. 표준 라이브러리 문서에는 std::env::Args 이 Iterator 트레잇을 구현하고 있다는 것 역시 언급하고 있으 므로, next 메서드를 호출 할 수 있다는 것을 알 수 있습니다! 리스트 13-27 은 리스트 12-23 의 코드에서 next 메서드를 사용하도록 변경 합니다: Filename: src/lib.rs # fn main() {}\n# use std::env;\n#\n# struct Config {\n# query: String,\n# filename: String,\n# case_sensitive: bool,\n# }\n#\nimpl Config { pub fn new(mut args: std::env::Args) -> Result { args.next(); let query = match args.next() { Some(arg) => arg, None => return Err(\"Didn't get a query string\"), }; let filename = match args.next() { Some(arg) => arg, None => return Err(\"Didn't get a file name\"), }; let case_sensitive = env::var(\"CASE_INSENSITIVE\").is_err(); Ok(Config { query, filename, case_sensitive }) }\n} 리스트 13-27: 반복자 메서드들을 사용하도록 Config::new 의 본문 변경하기 env::args 반환값의 첫번째 값은 프로그램 이름이라는 것을 명심하세요. 우리는 첫번째 값을 무시하고 그 다음 값을 얻기 위해 우선 next 를 호출한 다음, 그 반환값으로 아무것도 하지 않았습니다. 두번째로, Config 의 query 에 원하는 값을 넣기 위해 next 를 호출 했습니다. next 가 Some 을 반환하면, 값을 추출하기 위해 match 를 사용 합니다. 만약 None 을 반환하면, 이것은 충분한 인자가 넘어오지 않았다는 것을 의미하고, Err 값과 함께 조기 반환을 합니다. filename 값도 동일하게 처리 합니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » I/O 프로젝트 개선하기 » 반복자를 사용하여 clone 제거하기","id":"240","title":"반복자를 사용하여 clone 제거하기"},"241":{"body":"I/O 프로젝트의 search 함수에도 반복자의 장점을 활용할 수 있습니다. 리스트 12-19 의 코드가 리스트 13-28 에 재현되어 있습니다: 파일명: src/lib.rs 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} 리스트 13-28: 리스트 12-19 의 search 함수 구현 우리는 반복자 어댑터 메서드를 사용해서 이 코드를 더 간결한 방식으로 작성할 수 있습니다. 이렇게 함으로써 results 벡터가 변경 가능한 중간 상태를 갖는 것을 피할 수 있습니다. 함수형 프로그래밍 스타일은 더 깔끔한 코드를 만들기 위해 변경 가능한 상태의 양을 최소화 하는 것을 선호 합니다. 가변 상태를 제거하면 results 벡터에 대한 동시 접근을 관리 할 필요가 없기 때문에, 추후에 검색을 병렬로 수행하는 것과 같은 향상이 가능해 집니다. 리스트 13-29 는 이 변경을 보여줍니다: 파일명: src/lib.rs pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> { contents.lines() .filter(|line| line.contains(query)) .collect()\n} 리스트 13-29: search 함수 구현에서 반복자 어댑터 메서드 사용하기 search 함수의 목적은 query 를 포함하는 contents 의 모든 줄을 반환하는 것임 을 기억하세요. 리스트 13-19 의 filter 예제와 유사하게, 이 코드는 line.contains(query) 이 true 를 반환하는 줄들만 유지하기 위해 filter 어댑 터를 사용 합니다. 그러고나서 collect 를 통해서 일치하는 줄들을 모아 새로운 벡터로 만듭니다. 훨씬 단순합니다! search_case_insensitive 도 역시 반복자 메서드들을 사용하도록 같은 변경을 자유롭게 만들어 보세요. 다음 논리적 질문은 당신의 코드에서 어떤 스타일을 선택하는 것이 좋은지와 그 이유 입니다: 리스트 13-28 의 최초 구현 혹은 리스트 13-29 의 반복자를 사용하는 버전. 대부분의 러스트 프로그래머는 반복자 스타일을 선호 합니다. 처음 사용하기는 다소 어렵습니다만, 다양한 반복자 어댑터와 어떤 일을 하는지에 대해 한번 감이 온다면, 반복자들은 이해하기 쉬워질 것 입니다. 루핑과 새로운 벡터 생성과 같은 다양한 작업을 수행하는 대신, 코드는 루프의 고차원적 목표에 집중 합니다. 이것은 아주 흔한 코드의 일부를 추상화해서 제거함으로써 반복자의 각 요소가 반드시 통과 해야하는 필터링 조건과 같이 이 코드에 유일한 개념을 더 쉽게 볼 수 있도록 합니다. 그러나 두 구현은 정말 동일 할까요? 직관적으로 저수준의 루프가 더 빠르다고 가정할 수도 있습니다. 그럼 성능에 대해서 얘기해 봅시다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » I/O 프로젝트 개선하기 » 반복자 어댑터로 더 간결한 코드 만들기","id":"241","title":"반복자 어댑터로 더 간결한 코드 만들기"},"242":{"body":"루프와 반복자 중에 어떤것을 사용할지 결정하기 위해, 어떤 버전의 search 함수가 더 빠른지 알 필요가 있습니다: 명시적으로 for 루프를 사용한 버전과 반복자를 사용한 버전. 우리는 아서 코난 도일이 쓴 셜록 홈즈의 모험 의 전체 내용을 로딩하고 내용중에 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) 반복자 버전이 약간더 빠릅니다! 여기서 벤치마크 코드에 대해 설명하진 않을 것 입니 다. 왜냐하면 핵심은 두 버전이 동등하다는 것을 증명하는 것이 아니고, 이 두 구현 방법이 성능 측면에서 어떻게 다른지에 대한 상식적인 이해를 얻는 것이기 때문 입니 다. 더 포괄적인 벤치마크를 위해, 다양한 크기의 다양한 텍스트를 내용 으로 사용하고, 다른 길이의 다른 단어들을 질의어 로 사용해서 모든 종류의 다른 조합을 확인 하는 것이 좋습니다. 핵심은 이렇습니다: 반복자는 비록 고수준의 추상이지만, 컴파일 되면 대략 직접 작성한 저수준의 코드와 같은 코드 수준으로 내려갑니다. 반복자는 러스트의 제로 비용 추상화 중 하나이며, 그 추상을 사용하는 것은 추가 적인 실행시간 오버헤드가 없다는 것을 의미 합니다. 최초의 C++ 디자이너 이자 구현자인 비야네 스트롭스트룹이 “Foundations of C++” (2012) 에서 제로 오버헤드 를 정의한 것과 유사 합니다: 일반적으로, 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개의 값을 순회하면서 각각의 계수와 buffer 의 이전 12개의 값의 쌍을 만들기 위해 zip 메서드를 사용 합니다. 그런 다음, 각 쌍에 대해 값들을 모두 곱하고 모든 결과를 더한 후 더한 값을 qlp_shift 비트 만큼 우측으로 쉬프트 합니다. 오디오 디코더와 같은 어플리케이션에서의 계산은 종종 성능에 가장 높은 우선순위를 둡니다. 여기서 우리는 두 개의 어댑터를 사용하는 반복자를 생성하고 값을 소비 했습니다. 이 러스트 코드가 컴파일 되면 어떤 어셈블리 코드가 될 까요? 글쎄요, 이 글을 쓰는 시점에선 그것은 직접 손으로 작성한 것과 같은 어셈블리 코드로 컴파일 됩니다. 거기엔 coefficients 의 값들을 순회하기 위한 어떤 루프도 없습니다: 러스트는 12개의 순회가 있다는 것을 알고 있으며, 루프를 \"풀어(unrolls)\" 놓습니다. 언롤링(Unrolling) 은 루프 제어 코드의 오버헤드를 제거하고 대신 루프의 각 순회에 해당하는 반복적인 코드를 생성하는 최적화 방법 입니다. 모든 계수들은 레지스터에 저장되는데 값에 대한 접근이 매우 빠르다는 것을 뜻합 니다. 실행시간에 배열 접근에 대한 경계 체크가 없습니다. 러스트가 적용할 수 있는 이런 모든 최적화들은 결과 코드를 아주 효율적으로 만듭니다. 이제 이것을 알게 되었으니, 반복자와 클로저를 공포없이 사용할 수 있습니다! 이것들은 코드를 고수준 으로 보이도록 하지만, 그렇게 하기 위해 실행시간 성능 저하를 만들지 않습니다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 성능 비교하기: 루프 vs. 반복자 » 성능 비교하기: 루프 vs. 반복자","id":"242","title":"성능 비교하기: 루프 vs. 반복자"},"243":{"body":"클로저와 반복자는 함수형 프로그래밍 아이디어에서 영감을 받은 러스트의 특징들 입니다. 이것들은 고수준의 개념을 저수준의 성능으로 명확하게 표현할 수 있는 러스트의 능력에 기여하고 있습니다. 클로저와 반복자의 구현들은 런타임 성능에 영향을 미치지 않습니다. 이것은 제로-비용 추상을 제공하기 위해 노력하는 러스트의 목표 중의 일부 입니다. 이제 I/O 프로젝트의 표현력을 개선 했으니, 프로젝트를 세상과 공유하는데 도움을 줄 cargo 의 몇몇 특징들을 살펴 봅시다.","breadcrumbs":"함수형 언어의 특성들: 반복자들과 클로저들 » 성능 비교하기: 루프 vs. 반복자 » 요약","id":"243","title":"요약"},"244":{"body":"지금까지 우린 빌드, 실행, 코드 테스트등 Cargo 의 가장 기본적인 기능만 사용하였지만, Cargo 는 훨씬 더 많은 일을 할 수 있습니다. 이번 장에서 다음 목록의 기능을 수행하는 고급 기능 몇가지를 알아보도록 하겠습니다. 릴리즈 프로필을 이용해 빌드 커스터마이징하기 crates.io 에 라이브러리 배포하기 대규모 작업을 위한 작업공간 구성하기 crates.io 에서 바이너리 설치하기 커스텀 명령어로 Cargo 확장하기 Cargo 는 이번 장에서 다루는 것보다 더 많은 일을 할 수 있습니다. 만약 Cargo 의 모든 기능에 대한 설명을 보고 싶으시다면 Cargo 공식 문서 를 참고하세요.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Cargo 와 Crates.io 더 알아보기","id":"244","title":"Cargo 와 Crates.io 더 알아보기"},"245":{"body":"러스트에서 릴리즈 프로필(release profiles) 은 프로그래머가 코드 컴파일에 관련된 여러가지 옵션을 제어할 수 있도록 다양한 구성으로 사전 정의되고 커스텀 가능한 프로필입니다. 각 프로필은 다른 프로필과 독립적으로 설정됩니다. Cargo 는 두 메인 프로필을 가집니다: 여러분이 cargo build 를 실행할때 쓰는dev 프로필과 cargo build --release 를 실행할때 쓰는 release 프로필 입니다. dev 프로필은 개발에 적합한 설정을 기본값으로 갖고, release 프로필은 릴리즈 빌드용 설정을 기본값으로 가집니다. 여러분은 빌드 출력에서 이 프로필들의 이름을 몇 번 보셨을 수도 있습니다. $ cargo build Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs\n$ cargo build --release Finished release [optimized] target(s) in 0.0 secs 위 출력의 dev 와 release 는 컴파일러가 다른 프로필을 사용한다는 것을 나타냅니다. Cargo 는 프로젝트의 Cargo.toml 파일에 [profile.*] 구획이 따로 없을때 적용되는 각 프로필의 기본 설정을 가지고 있습니다. 이때 여러분은 원하는 프로필에 [profile.*] 구획을 추가하여 기본 설정을 덮어 씌울 수 있습니다. 여기 예시로 dev 와 release 프로필 각각의 opt-level 기본 설정 값을 보여드리겠습니다. Filename: 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 에 다른 값을 넣어서 기본 설정을 덮어 씌울 수 있습니다. 예를 들어 만약 우리가 개발용 프로필에 0 이 아닌 1 의 최적화 수치를 적용하고 싶다면 우리 프로젝트의 Cargo.toml 에 다음 두 줄을 추가하면 됩니다: Filename: Cargo.toml [profile.dev]\nopt-level = 1 이 코드는 기본 설정인 0 을 덮어 씌웁니다. 이후에 우리가 cargo build 를 실행하면 Cargo 는 dev 프로필의 기본값과 우리가 커스텀 한 opt-level 을 사용합니다. 우리가 opt-level 을 1 로 설정 했기 때문에 Cargo 는 릴리즈 빌드 만큼은 아니지만 기본 설정 보다 많은 최적화를 진행할 겁니다. 각 프로필의 설정 옵션 및 기본값의 전체 목록을 보시려면 Cargo 공식 문서 를 참고해 주시기 바랍니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » 릴리즈 프로필을 이용해 빌드 커스터마이징하기 » 릴리즈 프로필을 이용해 빌드 커스터마이징하기","id":"245","title":"릴리즈 프로필을 이용해 빌드 커스터마이징하기"},"246":{"body":"우린 crates.io 의 패키지를 프로젝트의 의존성으로만 사용했지만 여러분이 직접 여러분의 패키지를 배포(publish)해서 코드를 다른 사람들과 공유 할 수도 있습니다. crates.io 의 크레이트 등기소 (registry)는 여러분이 만든 패키지의 소스코드를 배포하므로, crates.io 는 주로 오픈 소스인 코드를 관리합니다. 러스트와 Cargo 는 여러분이 배포한 패키지를 사람들이 더 쉽게 찾고 사용할 수 있도록 도와주는 기능이 있습니다. 다음 내용이 바로 이런 기능들 몇개에 대한 설명과 패키지를 배포하는 방법에 대한 설명입니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » Crates.io 에 크레이트 배포하기","id":"246","title":"Crates.io 에 크레이트 배포하기"},"247":{"body":"여러분의 패키지를 시간을 들여서 자세하게 문서화하는 작업은 굉장히 가치있는 일 입니다. 문서는 다른 사람들이 그 패키지를 언제, 어떻게 써야할지 알게 해주는데 굉장히 도움이 되거든요. 3장에서 우린 슬래시 두 개(//) 를 이용해 러스트 코드에 주석을 남기는 법을 배웠습니다만, 러스트에는 문서화 주석(documentation comment) 이라고 불리는 문서화를 위한 특별한 주석이 존재합니다. 이 주석은 HTML 문서를 생성할 수 있는데, 이 HTML 에는 여러분의 크레이트가 어떻게 구현되었는지 가 아닌 어떻게 사용하는지 에 관심 있는 프로그래머들을 위한 공개 API의 문서화 주석이 보여집니다. 문서화 주석은 슬래시 두 개가 아니라 세 개(///) 를 이용하며 텍스트 서식을 위한 마크다운 표기법을 지원합니다. 문서화 주석은 문서화할 대상 바로 이전에 배치하면 됩니다. Listing 14-1 은 my_crate 크레이트의 add_one 함수에 대한' 문서화 주석의 예시를 보여줍니다: Filename: src/lib.rs /// Adds one to the number given.\n///\n/// # Examples\n///\n/// ```\n/// let five = 5;\n///\n/// assert_eq!(6, my_crate::add_one(5));\n/// ```\npub fn add_one(x: i32) -> i32 { x + 1\n} Listing 14-1: 함수에 대한 문서화 주석 자, add_one 함수가 무슨 일을 하는지 설명을 적었고 Example 절에서 add_one 함수를 어떻게 사용하는지에 대한 예시 코드를 제공 했습니다. 이제 우린 cargo doc 을 이용해 이 문서화 주석으로부터 HTML 문서를 생성할 수 있습니다. 이 명령어는 러스트에 들어있는 rustdoc 툴을 실행시키고 생성된 HTML 문서를 target/doc 디렉토리에 저장합니다. 좀더 편리하게, cargo doc --open 을 실행시키면 여러분의 현재 크레이트의 문서에 대해 (심지어 여러분의 크레이트가 가진 모든 디펜던시의 문서까지) HTML 을 생성하고 웹 브라우저에 띄워줄 겁니다. 이제 add_one 함수를 찾아보면 여러분은 문서화 주석의 내용이 어떻게 나타나는지 보실 수 있습니다. Figure 14-1 처럼요: Figure 14-1: add_one 함수에 대한 HTML 문서화 자주 사용되는 구절 우린 Listing 14-1 에서 HTML 에 \"Examples.\" 제목을 가진 구절을 만들기 위해 # Examples 마크다운 헤더를 사용했습니다. 이외에 크레이트의 제작자가 일반적으로 문서에 사용하는 구절은 다음과 같습니다. Panics : 문서화된 기능이 패닉을 일으킬 수 있는 시나리오입니다. 함수를 호출하는 사람들에게 \"프로그램이 패닉을 일으키지 않게 하려면 이러한 상황에서는 이 함수를 호출하지 않아야 합니다\" 라는 내용을 알려줍니다. Errors : 해당 함수가 Result 를 반환할 경우에는 발생할 수 있는 에러의 종류와 해당 에러들이 발생하는 조건을 설명해 주어서 호출하는 사람이 여러 에러를 여러 방법으로 처리할 수 있도록 해야합니다. Safety : 함수가 안전하지 않을(unsafe) 경우에 (19장에서 다루는 내용입니다) 왜 이 함수가 안전하지 않은지와 이 함수가 호출하는 사람에게 지키길 기대하는 불변성에 대해 알려주는 구절이 있어야 합니다. 대부분의 문서화 주석은 이 구절들이 모두 필요하진 않습니다. 하지만 여러분의 코드를 사용하는 사람들이 관심을 가지고 알아보게 될 측면에 대해 곱씹어 보게 만드는 좋은 체크리스트가 될 수 있습니다. 테스트로서의 문서화 주석 여러분의 문서화 주석에 예시 코드를 추가하는 건 여러분의 라이브러리를 어떻게 사용하는지 알려줄 수 있을뿐더러 또 다른 효과도 있습니다: 무려 cargo test 를 실행하면 여러분의 문서에 들어있던 예시 코드들이 테스트로서 실행됩니다! 백문이 불여일견이라는 말이 있듯이, 예시를 포함한 문서보다 좋은 문서는 없습니다. 다만, 코드를 변경하고 문서를 업데이트하지 않아서 예시 코드가 작동하지 않는 일은 절대 있어선 안되니 주의하세요. 우리가 Listing 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 이제 우리가 함수나 예제를 변경하고 예시 코드에서 패닉이 발생하는 상태로 cargo test 를 실행하면, 문서 테스트 기능이 더이상 예시 코드가 기능하지 못한다고 알려줄 겁니다. 주석을 포함하는 항목을 문서화 하기 문서화 주석의 또 다른 스타일로 //! 가 있습니다. 이는 주석 뒤에 오는 항목을 문서화 하는게 아닌 주석을 포함하는 항목을 문서화 합니다. 일반적으로 크레이트의 루트 파일 (관례적으로 src/lib.rs 입니다) 이나 크레이트 혹은 모듈 전체를 문서화하는 모듈 내부에 이 문서화 주석을 작성합니다. 예시로, 만약 add_one 함수를 포함한 my_crate 크레이트를 설명하기 위한 목적으로 문서화를 진행한다면, Listing 14-2 처럼 src/lib.rs 에 //! 로 시작하는 문서화 주석을 추가할 수 있습니다. Filename: 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// --snip-- Listing 14-2: my_crate 크레이트 전체를 위한 문서화 //! 로 시작하는 줄 중 마지막 줄에 코드가 뒤따르지 않는다는 점을 주목하세요. 우린 주석 뒤에 따라오는 항목이 아닌, 주석을 포함하는 항목을 문서화 할 것이기에 /// 가 아니라 //! 로 시작하는 주석을 사용했습니다. 이 경우, 주석을 포함하는 항목은 크레이트의 루트 파일인 src/lib.rs 이며 주석은 전체 크레이트를 설명하게 됩니다. cargo doc --open 을 실행하면, Figure 14-2 처럼 my_crate 문서 첫 페이지 내용 중 크레이트의 공개 아이템들 상단에 이 주석의 내용이 표시될 것입니다. Figure 14-2: 전체 크레이트를 설명하는 주석이 포함된 my_crate 의 문서가 렌더링된 모습 항목 내 문서화 주석은 크레이트나 모듈을 설명하는데 유용합니다. 이를 이용해 사용자들이 크레이트의 구조를 이해할 수 있도록 크레이트의 중심 목적을 설명하세요.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » 유용한 문서화 주석 만들기","id":"247","title":"유용한 문서화 주석 만들기"},"248":{"body":"7 장에서 우린 mod 키워드를 이용해 우리 코드를 체계화 하는 법과, pub 키워드로 공개 아이템을 만드는 법, use 를 이용해 스코프 내로 가져오는 법을 다뤘습니다. 다만 여러분이 크레이트를 개발할때 만들어놓은 구조는 여러분의 크레이트를 사용할 사용자들에게는 그다지 편리하지 않을 수 있습니다. 여러분은 여러 단계의 계층 구조를 이용해 크레이트를 구성하고 싶으시겠지만, 여러분이 계층 구조상에서 깊은곳에 정의한 타입을 다른 사람들이 사용하기에는 상당히 어려움을 겪을 수 있습니다. 애초에 그런 타입이 존재하는지 알아내는 것 조차 힘들테니까요. 또한 알아내더라도 use my_crate::UsefulType; 가 아니라 use my_crate::some_module::another_module::UsefulType; 를 입력 하는 일은 꽤나 짜증이 날 테죠. 공개 API 의 구조는 크레이트를 배포하는데 있어서 중요한 고려사항 중 하나입니다. 여러분의 크레이트를 이용할 사람들은 해당 구조에 있어서 여러분보다 이해도가 떨어질 것이고, 만약 여러분의 크레이트가 거대한 구조로 되어 있다면 자신들이 원하는 부분을 찾기조차 힘들 겁니다. 좋은 소식은 여러분이 만든 구조가 다른 라이브러리에서 이용하는데 편리하지 않다고 해서 굳이 내부 구조를 뒤엎을 필요는 없다는 겁니다. 대신에 여러분은 pub use 를 이용해 내부 항목을 다시 export( re-export ) 하여 기존의 private 구조와 다른 public 구조를 만들 수 있다는 겁니다. 다시 export 한다는 것은 한 위치에서 공개 항목(public item)을 가져오고 이것을 마치 다른 위치에서 정의한 것처럼 공개 항목으로 만드는 것을 의미합니다. 예를 들어, 우리가 예술적인 개념을 모델링 하기 위해 art 라는 라이브러리를 만들었다고 가정해 봅시다. 해당 라이브러리에는 두 모듈이 들어 있습니다: kinds 모듈은 PrimaryColor 과 SecondaryColor 열거형를 포함하고, utils 모듈은 mix 라는 이름의 함수를 포함합니다. Listing 14-3 처럼요. Filename: 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 kinds::*; /// Combines two primary colors in equal amounts to create /// a secondary color. pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor { // --생략-- }\n} Listing 14-3: kinds 모듈과 utils 모듈로 이루어진 art 라이브러리 Figure 14-3 은 cargo doc 으로 생성된 이 크레이트 문서의 첫 화면입니다: Figure 14-3: kinds 와 utils 모듈을 포함한 art 크레이트의 문서가 렌더링된 모습 PrimaryColor, SecondaryColor 타입들과 mix 함수가 첫 화면에 나오지 않는 걸 주목하세요. 이들을 보려면 각각 kinds 와 utils 를 클릭하셔야 합니다. 이 라이브러리를 의존성으로 가지고 있는 다른 크레이트에서 use 를 이용해 art 의 항목을 가져오기 위해선, 현재 정의된 art 모듈의 구조대로 일일이 입력해야 합니다. Listing 14-4 에서 다른 크레이트에서 art 크레이트의 PrimaryColor 과 mix 를 이용하는 예시를 볼 수 있습니다. Filename: src/main.rs extern crate art; use art::kinds::PrimaryColor;\nuse art::utils::mix; fn main() { let red = PrimaryColor::Red; let yellow = PrimaryColor::Yellow; mix(red, yellow);\n} Listing 14-4: art 크레이트의 내부 구조에 정의된 항목을 이용하는 또 다른 크레이트 Listing 14-4 의 코드를 작성한, 즉 art 크레이트를 사용하는 사람은 PrimaryColor 이 kinds 모듈에 들어있고 mix 가 utils 모듈에 들어 있단 걸 알아내야 합니다. 이처럼 현재 art 크레이트의 구조는 크레이트를 사용하는 사람보다 크레이트를 개발하는 사람에게 적합한 구조로 되어 있습니다. 내부 구조상에서의 kinds 와 utils 모듈의 위치 같은 정보는 art 크레이트를 사용하는 입장에서는 전혀 필요 없는 정보이며, 또한 직접 구조상에서 자신이 찾는 것의 위치를 알아내야 하고 use 뒤에 모듈의 이름을 일일이 입력해야 한다는 건 혼란스럽고 불편한 일 이니까요. 공개 API 로부터 내부 구조의 흔적를 제거하려면 Listing 14-3 처럼 맨 위에서 pub use 를 이용해 다시 export 하도록 art 크레이트의 코드를 수정해야 합니다: Filename: src/lib.rs //! # Art\n//!\n//! A library for modeling artistic concepts. pub use kinds::PrimaryColor;\npub use kinds::SecondaryColor;\npub use utils::mix; pub mod kinds { // --snip--\n} pub mod utils { // --snip--\n} Listing 14-5: Re-export 를 위해 pub use 추가 cargo doc 를 이용해 현재 크레이트에 대한 API 문서를 생성하면 Figure 14-4 처럼 Re-exports 목록과 링크가 첫 페이지에 나타날 겁니다. 이로써 PrimaryColor, Secondary 타입과 mix 함수를 훨씬 더 쉽게 찾을 수 있게 되었네요. Figure 14-4: Re-exports 목록이 포함된 art 크레이트 문서의 첫 페이지 art 크레이트의 사용자는 기존의 Listing 14-3 의 내부 구조를 이용하여 Listing 14-4 처럼 사용하거나, 혹은 좀 더 편한 방식으로 Listing 14-5 의 구조를 이용하여 Listing 14-6 과 같이 사용할 수 있습니다: Filename: src/main.rs extern crate art; use art::PrimaryColor;\nuse art::mix; fn main() { // --생략--\n} Listing 14-6: art 크레이트의 Re-export 된 항목들을 사용하는 프로그램 만약 특정 부분에서 중첩된 모듈이 많을 경우, 모듈의 상위 계층에서 pub use 를 이용해 타입을 다시 export 함으로써 크레이트의 사용자들에게 더 뛰어난 경험을 제공할 수 있습니다. 쓰기 좋고 편한 형태의 공개 API 를 만드는 일은 기술보단 예술에 가까운 일입니다. 따라서 한번에 완벽한 형태를 만들려고 하기보다는 계속해서 사용자들을 위한 최적의 구조를 찾아 개선해 나가야 합니다. 이럴때 pub use 를 이용하면 크레이트 내부를 보다 유연하게 구조화 할 수 있고, 사용자에게 제공하는 것에서 내부 구조의 흔적을 없앨 수 있습니다. 한번 여러분이 설치한 크레이트 중에 아무거나 코드를 열어서 그의 공개 API 구조와 내부 구조를 비교해 보세요. 아마 상당히 다를걸요?","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » pub use 를 이용해 공개 API 를 편리한 형태로 export 하기","id":"248","title":"pub use 를 이용해 공개 API 를 편리한 형태로 export 하기"},"249":{"body":"여러분은 첫 크레이트를 배포하기에 앞서, crates.io 에 계정을 만들고 API 토큰을 얻어야 합니다. crates.io 홈페이지에 방문하고 GitHub 계정을 통해 로그인 해주세요. (현재는 GitHub 계정이 필수지만, 추후에 사이트에서 다른 방법을 통한 계정 생성을 지원하게 될 수 있습니다) 로그인 하셨다면 계정 설정 페이지인 https://crates.io/me/ 로 들어가 주세요. 그리고 페이지에서 API 키를 얻어온 후에, 여러분의 API 키를 이용해 cargo login 명령어를 실행해 주세요. 이런식으로요: $ cargo login abcdefghijklmnopqrstuvwxyz012345 이 명령어는 Cargo 에게 여러분의 API 토큰을 알려주고 내부 ( ~/.cargo/credentials ) 에 저장하도록 합니다. 미리 말하지만 여러분의 토큰은 남들에겐 비밀 입니다: 어떤 이유로 남들에게 알려졌다면, (그 사람을 처리하거나, 혹은) crates.io 에서 기존의 토큰을 무효화하고 새 토큰을 발급받으세요.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » Cartes.io 계정 설정하기","id":"249","title":"Cartes.io 계정 설정하기"},"25":{"body":"Cargo(카고)는 러스트의 빌드 시스템 및 패키지 매니저입니다. 대부분의 러스트인들이 이 도구를 이용하여 그들의 러스트 프로젝트를 관리하는데, 그 이유는 Cargo가 여러분의 코드를 빌드하고, 여러분의 코드가 의존하고 있는 라이브러리를 다운로드해주고, 그 라이브러리들을 빌드하는 등 여러분을 위한 많은 작업들을 다루기 때문입니다. (여러분의 코드가 필요로 하는 라이브러리를 의존성 (dependency) 이라고 부릅니다) 여러분이 이제껏 작성한 것과 같은 가장 단순한 러스트 프로그램은 어떠한 의존성도 없습니다. 따라서 만일 Cargo를 가지고 “Hello, world!” 프로젝트를 빌드했다면, 여러분의 코드를 빌드하는 것을 다루는 카고의 일부분만을 이용하게 되었을 것입니다. 여러분이 더 복잡한 러스트 프로그램을 작성할 때면, 여러분은 의존성을 추가할 것이고, 여러분이 Cargo를 이용하여 프로젝트를 시작한다면, 의존성 추가가 훨씬 더 하기 쉬워질 것입니다. 압도적인 숫자의 러스트 프로젝트가 Cargo를 이용하기 때문에, 이 책의 나머지 부분에서는 여러분 또한 Cargo를 이용하고 있음을 가정합니다. 만일 여러분이 “설치하기” 절에서 다룬대로 공식 인스톨러를 이용했다면 Cargo는 러스트와 함께 설치되어 있습니다. 만일 여러분이 다른 수단을 통해 러스트를 설치했다면, Cargo가 설치되어 있는지 확인하기 위해서 여러분의 터미널에 다음을 입력해보세요: $ cargo --version 버전 숫자가 보인다면, 가지고 있는 것입니다! command not found 같은 에러를 보게 된다면, 여러분이 설치한 방법에 대한 문서에서 Cargo를 개별적으로 어떻게 설치하는지 찾아보세요.","breadcrumbs":"시작하기 » Hello, Cargo! » Hello, Cargo!","id":"25","title":"Hello, Cargo!"},"250":{"body":"계정을 만들었으니, 여러분이 크레이트를 배포하려고 한다고 가정합시다. 여러분은 배포하기 전에 Cargo.toml 파일에 [package] 구절을 추가하여 메타데이터(metadata) 를 추가해야합니다. 여러분의 크레이트명은 고유해야 합니다. 여러분이 로컬에서 작업 할 땐 문제 없지만, crates.io 에 올라갈 크레이트의 이름은 선착순으로 배정되기에, 여러분이 정한 크레이트명을 누군가 이미 쓰고 있다면 해당 크레이트명으로는 크레이트를 배포할 수 없습니다. 크레이트를 배포하기 전에 사이트에서 여러분이 사용하려는 이름을 검색해보고 해당 크레이트명이 이미 사용중인지 확인하세요. 만약 아직 사용중이지 않다면 다음과 같이 Cargo.toml 파일 내 [package] 절 아래의 이름을 수정하세요: Filename: Cargo.toml [package]\nname = \"guessing_game\" 고유한 이름을 선택하고, 크레이트를 배포하기 위해 cargo publish 를 실행하면 다음과 같은 경고와 에러가 나타날 겁니다. $ cargo publish Updating registry `https://github.com/rust-lang/crates.io-index`\nwarning: manifest has no description, license, license-file, documentation,\nhomepage or repository.\n--snip--\nerror: api errors: missing or empty metadata fields: description, license. 이 에러는 중요한 정보를 몇개 입력하지 않았다는 의미입니다: 설명(description) 과 라이센스(license) 는 필수적인데, 이들은 각각 사람들에게 해당 크레이트가 어떤 작업을 하는지와 해당 크레이트를 이용할 수 있는 조건을 알려줍니다. 이 에러를 고치려면 이 정보들을 Cargo.toml 에 포함시켜야 합니다. 설명은 한 문장이나 두 문장정도면 충분합니다. 크레이트를 검색 했을때의 결과에 여러분의 크레이트명과 같이 표시되거든요. license 필드엔 라이센스 식별자 값(license identifier value) 을 부여해야 합니다. Linux Foundation’s Software Package Data Exchange (SPDX) 에 여러분이 사용할 수 있는 식별자가 나열되어 있으니 참고 바랍니다. 예를 들어, 만약 여러분의 크레이트에 MIT 라이센스를 적용하고 싶으시다면, 다음과 같이 MIT 식별자를 추가하시면 됩니다. Filename: Cargo.toml [package]\nname = \"guessing_game\"\nlicense = \"MIT\" SPDX 에 없는 라이센스를 사용하고 싶으실 경우엔 해당 라이센스의 텍스트를 파일로 만들고 자신의 프로젝트에 해당 파일을 포함시킨 뒤, license 대신 license-file 을 추가해 해당 파일의 이름을 넣으시면 됩니다. 여러분의 프로젝트에 어떤 라이센스가 적합한지에 대해 알아보는 내용은 이 책 범위 이상의 내용입니다. 다만 알아두실 건 러스트 커뮤니티의 많은 이들은 자신의 프로젝트에 러스트 자체가 쓰는 라이센스인 MIT OR Apache-2.0 이중 라이센스를 사용한다는 겁니다, 즉 여러분은 프로젝트의 라이선스에 OR 을 이용해 여러 라이센스 식별자를 명시할 수 있습니다. 고유한 프로젝트명, 버전, cargo new 로 크레이트를 생성할때 추가된 작성자 정보, 설명, 라이센스를 모두 추가하셨다면 배포할 준비가 끝났습니다. 이때 Cargo.toml 파일의 모습은 다음과 같은 형태일 겁니다: Filename: Cargo.toml [package]\nname = \"guessing_game\"\nversion = \"0.1.0\"\nauthors = [\"Your Name \"]\ndescription = \"A fun game where you guess what number the computer has chosen.\"\nlicense = \"MIT OR Apache-2.0\" [dependencies] Cargo 공식 문서 에 다른 사람들이 여러분의 크레이트를 좀 더 찾기 쉽게 해주고, 쓰기 편하게 해주는 나머지 메타데이터들이 설명 되어 있으니, 참고 바랍니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » 새 크레이트에 Metadata 추가하기","id":"250","title":"새 크레이트에 Metadata 추가하기"},"251":{"body":"계정도 만들었고, API 토큰도 얻었고, 크레이트명도 정했고, 메타데이터도 작성했으니 이제 여러분은 크레이트를 배포할 준비 만전이에요! 크레이트를 배포한다는 것은 다른 사람이 사용할 특정 버전을 crates.io 에 올리는 것입니다. 크레이트를 배포할땐 주의하시기 바랍니다. 기본적으로 낙장불입이거든요. 버전은 중복될 수 없으며, 한번 올라간 코드는 수정할 수 없습니다. crates.io 의 원대한 목표중 하나는 crates.io 에 등록된 크레이트들에 의존하는 모든 프로젝트의 빌드가 계속 작동할 수 있도록 영구적인 코드 보관소의 역할을 맡는 것이기 때문에, 버전을 삭제하거나 수정하는 행위는 용납하지 않습니다. 만약 용납한다면 목표를 이룰 수 없으니까요. 대신 버전의 개수에 대한 제한은 없으니 버전을 올리는 것 자체는 얼마든지 가능합니다. cargo publish 명령어를 재실행 해보면 이번엔 성공할 겁니다: $ cargo publish Updating registry `https://github.com/rust-lang/crates.io-index`\nPackaging guessing_game v0.1.0 (file:///projects/guessing_game)\nVerifying guessing_game v0.1.0 (file:///projects/guessing_game)\nCompiling 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.19 secs\nUploading guessing_game v0.1.0 (file:///projects/guessing_game) 축하합니다! 이제 여러분의 코드는 러스트 커뮤니티와 공유되고, 아무나 여러분의 크레이트를 자신들의 프로젝트 의존성 목록에 쉽게 추가할 수 있을 겁니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » Crates.io 에 배포하기","id":"251","title":"Crates.io 에 배포하기"},"252":{"body":"여러분의 크레이트에 변경사항을 적용하고 새 버전을 릴리즈하려면 Cargo.toml 파일의 version 값을 새 버전으로 변경하면 됩니다. 이때 변경사항의 종류에 맞춰서 적절한 버전을 결정하는 방법은 유의적 버전 규칙(Semantic Versioning rules) 을 참고하시기 바랍니다. 버전을 변경하고 나면 cargo publish 를 실행해 새 버전을 배포합시다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » 이미 배포한 크레이트의 버전 업데이트하기","id":"252","title":"이미 배포한 크레이트의 버전 업데이트하기"},"253":{"body":"크레이트의 이전 버전을 제거할 순 없지만, Cargo 는 크레이트의 버전을 yanking(끌어내리는) 기능을 지원합니다. 이는 특정 크레이트의 버전이 어떤 이유에선가 문제가 생긴 등의 경우에 새롭게 만들어지는 프로젝트들이 해당 버전을 종속성으로 추가할 수 없도록 막아주는 주는 기능입니다. (역주: yank 의 사전적 의미는 홱 당기다 입니다) 버전을 끌어내려도 해당 버전에 의존하던 기존의 프로젝트들은 계속해서 그 버전에 의존성을 가질 수 있고 해당 버전을 다운로드 받을 수도 있지만, 새로운 프로젝트들이 끌어내려진 버전을 의존성으로 가지는 시작하는것은 불가능합니다. 근본적인 yank 의 의미는 Cargo.lock 을 가진 모든 프로젝트는 문제가 없을 것이며, 추후에 새로 생성될 Cargo.lock 파일은 끌어내려진 버전을 사용하지 않을 것이란 의미입니다. 크레이트의 버전을 yank 하기 위해서는 cargo yank 에 yank 하고자 하는 버전을 명시하고 실행하시면 됩니다: $ cargo yank --vers 1.0.1 또한 여러분은 --undo 를 붙여서 yank 를 취소하고 다시 새 프로젝트들이 해당 버전을 의존성으로 갖는 것을 허용할 수 있습니다: $ cargo yank --vers 1.0.1 --undo yank 는 어떤 코드도 삭제하지 않습니다 . 예를 들어, 여러분이 실수로 자신의 비밀 정보를 업로드한 상황에 대한 해결책으로 yank 기능을 사용하셨다면, 이는 잘못된 방법입니다. 만약 그런 일이 일어나면 비밀 정보를 재설정하셔야 합니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Crates.io에 크레이트 배포하기 » cargo yank 를 이용해 Crates.io 에서 버전 제거하기","id":"253","title":"cargo yank 를 이용해 Crates.io 에서 버전 제거하기"},"254":{"body":"12 장에서 바이너리 크레이트와 라이브러리 크레이트를 포함하는 패키지를 만들어 봤습니다. 하지만 여러분이 프로젝트를 개발하다 보면, 라이브러리 크레이트가 점점 거대해져서 여러분의 패키지를 여러개의 라이브러리 크레이트로 분리하고 싶으실 겁니다. Cargo 는 이런 상황에서 사용할 수 있는 작업공간(workspace) 이라는 기능을 제공하며, 이 기능은 함께 개발된 여러개의 관련된 패키지를 관리하는데 도움이 됩니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Cargo 작업공간 » Cargo 작업공간","id":"254","title":"Cargo 작업공간"},"255":{"body":"작업공간(workspace) 은 동일한 Cargo.lock 과 출력 디렉토리를 공유하는 패키지들의 집합입니다. 한번 이 작업공간을 이용한 프로젝트를 만들어 봅시다. 다만 작업공간의 구조에 집중할 수 있도록 간단한 코드만 사용할 겁니다. 작업공간을 구성하는 방법은 여러가지가 있지만, 일반적인 방법중 하나를 사용하도록 하겠습니다; 작업 공간은 바이너리 하나와 두 라이브러리를 포함하도록 할 것입니다. 주요 기능을 제공할 바이너리는 두 라이브러리를 의존성으로 가지게 될 것인데, 하나는 add_one 함수를 제공할 것이고, 또 하나는 add_two 함수를 제공할 것입니다. 이 세 크레이트는 같은 작업 공간의 일부가 될 겁니다. 그럼 작업공간을 위한 새 디렉토리를 만드는 것 부터 시작합시다. $ mkdir add\n$ cd add 다음은 add 디렉토리 내에서 전체 작업공간을 구성 할 Cargo.toml 파일을 생성합시다. 이 파일은 우리가 여태 다른곳에서 봐온 Cargo.toml 파일들과는 달리, [package] 절이나 메타데이터를 가지지 않습니다. 대신 [workspace] 로 시작하는 구절을 갖는데, 이걸 이용해 작업공간에 members 를 추가할 수 있습니다; 추가하는 법은 우리의 바이너리 크레이트 경로를 명시하는 것이며, 이 경우 해당 경로는 adder 입니다: Filename: Cargo.toml [workspace] members = [ \"adder\",\n] 다음으로, add 디렉토리 안에서 cargo new 를 실행하여 adder 바이너리 크레이트를 생성합시다: $ cargo new --bin adder Created binary (application) `adder` project 이 시점에서 우린 작업 공간을 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 에 위치하게 될 겁니다. Cargo 가 작업공간 내에 이와 같이 target 디렉토리를 구성한 이유는, 작업공간 내의 크레이트들이 서로 의존하기로 되어있기 때문입니다. 만약 각 크레이트가 각각의 target 디렉토리를 갖게 된다면, 각각의 크레이트를 컴파일 할때마다 자신의 target 디렉토리에 컴파일 결과를 넣기 위해 다른 크레이트들을 매번 재컴파일 하게 될 겁니다. 이와 같은 불필요한 재빌드를 피하기 위해, 하나의 크레이트들은 target 디렉토리를 공유하도록 되어 있습니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Cargo 작업공간 » 작업공간 생성","id":"255","title":"작업공간 생성"},"256":{"body":"다음은 작업공간에 add-one 이라고 부를 새로운 멤버 크레이트를 생성해 봅시다. members 목록에 add-one 경로를 지정하기 위해 최상위의 Cargo.toml 파일을 수정합시다. Filename: Cargo.toml [workspace] members = [ \"adder\", \"add-one\",\n] 그리고 add-one 이라는 새 라이브러리 크레이트를 생성합시다. $ cargo new add-one Created library `add-one` project 이제 여러분의 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 함수를 추가합시다: Filename: add-one/src/lib.rs pub fn add_one(x: i32) -> i32 { x + 1\n} 이제 우린 작업공간 내에 라이브러리 크레이트를 가졌으니, adder 바이너리 크레이트를 add-one 라이브러리 크레이트에 의존하도록 만들 수 있습니다. 먼저, adder/Cargo.toml 에 add-one 에 대한 의존성 경로를 추가합시다. Filename: adder/Cargo.toml [dependencies] add-one = { path = \"../add-one\" } Cargo 는 작업공간 내 크레이트들이 서로 의존하고 있을 것이라고 추정하지 않기 때문에, 우리가 크레이트간의 의존 관계에 대해 명시해 주어야 합니다. 다음으로 adder 크레이트에서 add-one 크레이트의 add_one 함수를 사용해보도록 합시다. adder/src/main.rs 파일을 열고 상단에 extern crate 행을 추가해 스코프 내로 add-one 라이브러리를 가져오도록 한 뒤, main 함수를 add_one 함수를 호출하도록 변경합니다. Listing 14-7 처럼요: Filename: adder/src/main.rs extern crate add_one; fn main() { let num = 10; println!(\"Hello, world! {} plus one is {}!\", num, add_one::add_one(num));\n} Listing 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.68 secs add 디렉토리에서 바이너리 크레이트를 실행하기 위해선 cargo run 에 -p 옵션과 패키지 이름을 사용하여 우리가 작업공간 내에서 사용할 패키지를 명시해야 합니다: $ cargo run -p adder Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running `target/debug/adder`\nHello, world! 10 plus one is 11! 이는 add-one 크레이트에 의존성을 가진 adder/src/main.rs 코드를 실행시킵니다. 작업공간의 외부 크레이트에 의존성 갖기 작업공간은 작업공간에 있는 각각의 크레이트의 디렉토리에 Cargo.lock 파일을 갖는게 아닌, 작업공간의 최상위에만 단 하나의 Cargo.lock 파일을 갖는다는 걸 기억하세요. 이는 모든 크레이트들이 모든 의존성의 같은 버전을 사용함을 보증합니다. 만약 우리가 rand 크레이트를 adder/Cargo.toml 과 add-one/Cargo.toml 에 추가하면 Cargo 는 둘을 모두 같은 버전을 쓰도록 결정하고 하나의 Cargo.lock 에 기록합니다. 작업공간의 모든 크레이트들이 같은 의존성을 갖도록 한다는 의미는 작업공간 내의 크레이트들이 항상 서로 조화를 이룬다는 의미입니다. 한번 add-one 크레이트에서 rand 크레이트를 사용할 수 있도록 add-one/Cargo.toml 파일의 [dependencies] 절에 rand 를 추가해 봅시다: Filename: add-one/Cargo.toml [dependencies] rand = \"0.3.14\" 이제 우린 add-one/src/lib.rs 파일에 extern crate rand; 를 추가할 수 있으며, add 디렉토리에서 cargo build 를 이용해 전체 작업공간을 빌드하면 rand 크레이트를 가져오고 컴파일 할 것입니다: $ cargo build Updating registry `https://github.com/rust-lang/crates.io-index` Downloading rand v0.3.14 --snip-- Compiling rand v0.3.14 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 10.18 secs 이제 최상위 Cargo.lock 엔 add-one 의 rand 로의 종속성 정보가 포함되어 있습니다. 하지만, 작업공간의 어딘가에서 rand 를 사용하였다고 해도, 작업공간의 다른 크레이트에선 rand 를 자신의 Cargo.toml 파일에 추가하지 않는 한 사용이 불가능합니다. 예를 들어, 만약 adder 크레이트에서 rand 를 그냥 사용하기 위해 adder/src/main.rs 파일에 extern crate rand; 를 추가하면 에러가 나타납니다: $ cargo build Compiling adder v0.1.0 (file:///projects/add/adder)\nerror: use of unstable library feature 'rand': use `rand` from crates.io (see\nissue #27703) --> adder/src/main.rs:1:1 |\n1 | extern crate rand; 이 에러를 해결하려면 adder 크레이트의 Cargo.toml 파일을 수정하여 rand 를 해당 크레이트의 의존성으로 나타내야합니다. 그 후 adder 크레이트를 빌드하면 Cargo.lock 의 adder 을 위한 의존성 목록에 rand 가 추가될 테지만, rand 가 다시 다운로드 되진 않을 겁니다. Cargo 는 rand 를 사용하는 작업공간 내의 크레이트는 모두 같은 버전의 rand 크레이트를 사용할 것임을 보장하기 때문에 같은 크레이트를 여러개의 버전으로 다운로드 받을 필요 없고, 따라서 그만큼 공간은 절약되며, 작업공간 내의 각 크레이트는 조화를 이룰 수 있습니다. 작업공간에 테스트 추가하기 또 다른 향상을 위해, add_one 크레이트의 add_one::add_one 함수에 대한 테스트를 추가해 봅시다. Filename: 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 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.27 secs Running 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 Running target/debug/deps/adder-f88af9d2cc175a5e running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out Doc-tests add-one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 출력의 첫번째 절 은 add-one 크레이트의 it_works 테스트가 통과했다는 의미이고, 다음 절 은 adder 크레이트에서 테스트를 찾지 못했다는 의미이며, 마지막 절 은 add-one 크레이트에서 문서화 테스트를 찾지 못했다는 의미입니다. 이처럼 작업공간 구조 내에서 cargo test 를 실행하면 작업공간 내의 모든 크레이트에 대한 테스트들이 실행됩니다. 우린 작업공간 내의 하나의 특정한 크레이트에 대한 테스트도 실행할 수 있습니다. 최상위 디렉토리에서 -p 플래그와 테스트 하고자 하는 크레이트명을 명시해줌으로써 말이죠: $ cargo test -p add-one Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs Running 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 Doc-tests add-one running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out 이 출력은 cargo test 가 adder 크레이트는 테스트하지 않고 add-one 크레이트에 대해서만 테스트를 실행 했음을 보여줍니다. 만약 여러분이 https://crates.io/ 에 작업공간 내의 크레이트를 배포하시려면, 각 크레이트는 분리돼서 배포되어야 합니다. cargo publish 명령어엔 --all 이나 -p 같은 플래그가 없어요. 따라서 여러분은 각 크레이트 디렉토리를 수정하고 cargo publish 를 실행해야 합니다. 추가 과제로는, 한번 이 작업공간에 add-two 크레이트를 추가해 보세요! add-one 크레이트를 추가할때와 비슷한 방법으로 하시면 됩니다. 언젠가 여러분의 프로젝트가 커지면 작업공간을 사용하는 걸 고려해보세요: 하나의 거대한 코드보다 작은 개별 요소를 이해하는 일이 훨씬 쉽고, 작업공간에서 크레이트를 관리한다면 각 크레이트가 동시에 변경되는 경우도 쉽게 조정할 수 있습니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » Cargo 작업공간 » 작업공간에 두번째 크레이트 만들기","id":"256","title":"작업공간에 두번째 크레이트 만들기"},"257":{"body":"cargo install 명령어는 여러분이 로컬에서 바이너리 크레이트를 설치하고 사용할 수 있도록 해줍니다. 이는 시스템 패키지를 대체하기 위한 것이 아닌, 러스트 개발자들이 crates.io 에서 공유하고 있는 툴을 편리하게 설치할 수 있도록 하기 위함입니다. 여러분은 바이너리 타겟(binary target) 을 가진 패키지만 설치할 수 있다는 걸 알아두셔야 하는데, 이 바이너리 타겟 이란 혼자서 실행될 수 없고 다른 프로그램에 포함되는 용도인 라이브러리 타겟과는 반대되는 의미로, src/main.rs 파일 혹은 따로 바이너리로 지정된 파일을 가진 크레이트가 생성해낸 실행 가능한 프로그램을 말합니다. 보통 해당 크레이트가 라이브러리인지, 바이너리 타겟을 갖는지, 혹은 둘 다인지에 대한 정보를 README 파일에 작성해둡니다. cargo install 을 이용해 설치한 모든 바이너리들은 Cargo가 설치된 폴더의 bin 폴더에 저장됩니다. 만약 여러분이 rustup.rs 를 이용해 러스트를 설치하셨고, 따로 설정을 건들지 않으셨다면 $HOME/.cargo/bin 폴더입니다. cargo install 로 설치한 프로그램을 실행하시려면 여러분의 $PATH 환경변수에 해당 디렉토리가 등록되어 있는지 확인하세요. 12 장에서 언급한 grep 을 러스트로 구현한 파일 검색 툴인 ripgrep 을 예로 들어봅시다. ripgrep 을 설치하려면 다음과 같이 하면 됩니다: $ cargo install ripgrep\nUpdating registry `https://github.com/rust-lang/crates.io-index` Downloading ripgrep v0.3.2 --snip-- Compiling ripgrep v0.3.2 Finished release [optimized + debuginfo] target(s) in 97.91 secs Installing ~/.cargo/bin/rg 출력의 마지막 줄은 설치된 바이너리의 경로와 이름을 보여줍니다. ripgrep 의 이름은 rg 네요. 방금 앞에서 말했던 것처럼 여러분의 $PATH 환경변수에 설치된 폴더가 등록되어 있는 한, 여러분은 명령창에서 rg --help 를 실행할 수 있고, 앞으로 파일을 찾을때 더 빠르고 러스트다운 툴을 사용할 수 있습니다!","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » cargo install을 이용해 Crates.io에서 바이너리 설치하기 » cargo install 을 이용해 Crates.io 에서 바이너리 설치하기","id":"257","title":"cargo install 을 이용해 Crates.io 에서 바이너리 설치하기"},"258":{"body":"Cargo 는 여러분이 직접 Cargo 를 수정하지 않고도 새 보조 명령어를 추가할 수 있도록 되어 있습니다. 만약 여러분의 $PATH 내 어떤 바이너리의 이름이 cargo-something 이고, 해당 바이너리가 Cargo 의 보조 명령어 바이너리일 경우 cargo something 라는 명령어를 이용해 실행할 수 있습니다. 이와 같은 커스텀 명령어들은 cargo --list 를 실행 할 때의 목록에도 포함됩니다. 이런식으로 cargo install 을 이용해 확장 모듈을 설치하고 Cargo 의 자체 툴처럼 이용할 수 있다는 점은 Cargo 의 무척 편리한 점 중 하나입니다.","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » 커스텀 명령어로 Cargo 확장하기 » 커스텀 명령어로 Cargo 확장하기","id":"258","title":"커스텀 명령어로 Cargo 확장하기"},"259":{"body":"Cargo 와 crates.io 를 통해 코드를 공유하는 행위는 러스트 생태계를 발전시키고, 러스트가 많은 방면에서 활약하도록 만드는데 주축이 되는 행위입니다. 러스트의 기본 라이브러리는 작고 고정되어 있지만, 크레이트들은 쉽게 공유될 수 있고, 쉽게 사용될 수 있으며 러스트 언어 자체보다 훨씬 빠른 속도로 발전합니다. 여러분에게 유용한 코드가 있다면 주저말고 crates.io 에 공유하세요; 분명 다른 누군가에게도 도움이 될 테니까요!","breadcrumbs":"Cargo와 Crates.io 더 알아보기 » 커스텀 명령어로 Cargo 확장하기 » 정리","id":"259","title":"정리"},"26":{"body":"Cargo를 사용하여 새 프로젝트를 만들고 우리의 원래 “Hello, world!” 프로젝트와 얼마나 차이가 나는지 살펴봅시다. 여러분의 projects 디렉토리로 (혹은 여러분의 코드를 저장하기로 결정한 어느 곳이든) 이동하세요. 그 다음, 어떤 운영체제이든 상관없이 다음을 실행하세요: $ cargo new hello_cargo --bin\n$ cd hello_cargo 첫번째 커맨드는 hello_cargo 라고 불리우는 새로운 실행 가능한 바이너리를 생성합니다. cargo new에게 넘겨지는 --bin 인자가 라이브러리가 아닌 실행 가능한 애플리케이션으로 만들어줍니다 (흔히들 그냥 바이너리 (binary) 라고 부릅니다). 우리의 프로젝트는 hello_cargo 라고 이름지었고, Cargo는 동일한 이름의 디렉토리에 이 프로젝트의 파일들을 생성합니다. hello_cargo 디렉토리로 가서 파일 리스트를 보세요. 여러분은 Cargo가 우리를 위해 두 개의 파일과 하나의 디렉토리를 생성한 것을 볼 수 있을 것입니다: Cargo.toml 파일 및 안에 main.rs 파일을 담고 있는 src 디렉토리가 그것입니다. 안에는 또한 .gitignore 과 함께 새로운 Git 저장소도 초기화되어 있습니다. 노트: Git은 보편적인 버전 관리 시스템입니다. 여러분은 --vcs 플래그를 사용하여 cargo new가 다른 버전 관리 시스템을 사용하거나 혹은 버전 관리 시스템을 사용하지 않도록 변경할 수 있습니다. 사용 가능한 옵션을 보려면 cargo new --help를 실행하세요. Cargo.toml 을 여러분이 원하는 텍스트 에디터로 여세요. 이 파일은 Listing 1-2의 코드와 유사하게 보여야 합니다. Filename: Cargo.toml [package]\nname = \"hello_cargo\"\nversion = \"0.1.0\"\nauthors = [\"Your Name \"] [dependencies] Listing 1-2: cargo new가 생성한 Cargo.toml 내용 이 파일은 TOML (Tom’s Obvious, Minimal Language) 포맷으로 작성되었는데, 이것이 Cargo의 환경설정 포맷입니다. 첫번째 라인 [package]은 이후의 문장들이 패키지 환경설정이라는 것을 나타내는 섹션의 시작지점입니다. 우리가 이 파일에 더 많은 정보를 추가하기 위해, 다른 섹션들을 추가할 것입니다. 그 다음 세 라인들은 Cargo가 여러분의 프로그램을 컴파일하기 위해 필요로 하는 정보에 대한 설정을 합니다: 이름, 버전, 그리고 누가 작성했는가 입니다. Cargo는 여러분의 환경으로부터 여러분의 이름과 이메일 정보를 얻어내므로, 만일 그 정보가 정확하지 않다면, 지금 수정하고 파일을 저장하세요. 마지막 라인 [dependencies]은 여러분 프로젝트의 의존성들의 리스트를 적을 수 있는 섹션의 시작점입니다. 러스트에서는 코드의 패키지를 크레이트 (crate) 라고 부릅니다. 이 프로젝트를 위해서는 어떤 다른 크레이트도 필요없지만, 2장의 첫 프로젝트에서는 필요할 것이므로, 그때 이 의존성 섹션을 사용하겠습니다. 이제 src/main.rs 을 열어서 살펴봅시다: Filename: src/main.rs fn main() { println!(\"Hello, world!\");\n} Cargo는 우리가 Listing 1-1에서 작성했던 것과 똑같이 여러분을 위해 “Hello, world!” 프로그램을 작성해놨습니다! 여기까지, 우리의 이전 프로젝트와 Cargo가 만든 프로젝트 간의 차이점은 Cargo가 코드를 src 디렉토리 안에 위치시킨다는 점, 그리고 최상위 디렉토리에 Cargo.toml 환경 파일을 가지게 해준다는 점입니다. Cargo는 여러분의 소스 파일들이 src 디렉토리 안에 있을 것으로 예상합니다. 최상위 프로젝트 디렉토리는 그저 README 파일들, 라이센스 정보, 환경 파일들, 그리고 여러분의 코드와는 관련이 없는 다른 것들 뿐입니다. Cargo를 이용하는 것은 여러분이 프로젝트를 조직화하는 데에 도움을 줍니다. 모든 것을 위한 공간이 있고, 모든 것은 자신의 공간 안에 있습니다. 만일 여러분이 Hello, world! 프로젝트에서 했던 것처럼 Cargo를 사용하지 않은 프로젝트를 시작했다면, Cargo를 사용한 프로젝트로 이를 바꿀 수 있습니다. 프로젝트 코드를 src 디렉토리로 옮기고 적합한 Cargo.toml 파일을 생성하세요.","breadcrumbs":"시작하기 » Hello, Cargo! » Cargo를 사용하여 프로젝트 생성하기","id":"26","title":"Cargo를 사용하여 프로젝트 생성하기"},"260":{"body":"포인터 (pointer) 는 메모리의 주소 값을 담고 있는 변수에 대한 일반적인 개념입니다. 이 주소 값은 어떤 다른 데이터를 참조합니다. 혹은 바꿔 말하면, “가리킵니다”. 러스트에서 가장 흔한 종류의 포인터는 참조자인데, 이는 여러분들이 3장에서 배웠던 것입니다. 참조자는 & 심볼에 의해 나타내지고 이들이 가리키고 있는 값을 빌립니다. 이들은 값을 참조하는 것 외에 다른 어떤 특별한 능력도 없습니다. 또한, 이들은 어떠한 오버헤드도 발생하지 않으며 우리가 가장 자주 사용하는 포인터의 한 종류입니다. 한편, 스마트 포인터 (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":"260","title":"스마트 포인터"},"261":{"body":"가장 직관적인 스마트 포인터는 박스 (box) 인데, 이 타입은 Box 라고 쓰입니다. 박스는 여러분이 데이터를 스택이 아니라 힙에 저장할 수 있도록 해줍니다. 스택에 남는 것은 힙 데이터를 가리키는 포인터입니다. 스택과 힙의 차이를 상기하려면 4장을 참조하세요. 박스는 스택 대신 힙에 데이터를 저장한다는 점 외에는, 성능적인 오버헤드가 없습니다. 하지만 여러 가지의 추가 기능 또한 가지고 있지 않습니다. 여러분은 이를 아래와 같은 상황에서 가장 자주 쓰게 될 것입니다: 컴파일 타임에 크기를 알 수 없는 타입을 갖고 있고, 정확한 사이즈를 알 필요가 있는 맥락 안에서 해당 타입의 값을 이용하고 싶을 때 커다란 데이터를 가지고 있고 소유권을 옮기고 싶지만 그렇게 했을 때 데이터가 복사되지 않을 것이라고 보장하기를 원할 때 어떤 값을 소유하고 이 값의 구체화된 타입을 알고 있기보다는 특정 트레잇을 구현한 타입이라는 점만 신경 쓰고 싶을 때 이 장에서는 첫 번째 상황을 보여줄 것입니다. 그러나 보여주기 전에, 나머지 두 상황에 대해 약간 더 자세히 말하겠습니다: 두 번째 경우, 방대한 양의 데이터의 소유권 옮기기는 긴 시간이 소요될 수 있는데 이는 그 데이터가 스택 상에서 복사되기 때문입니다. 이러한 상황에서 성능을 향상하기 위해서, 박스 안의 힙에 그 방대한 양의 데이터를 저장할 수 있습니다. 그러면, 작은 양의 포인터 데이터만 스택 상에서 복사되고, 데이터는 힙 상에서 한 곳에 머물게 됩니다. 세 번째 경우는 트레잇 객체 (trait object) 라고 알려진 것이고, 17장이 이 주제만으로 전체를 쏟아부었습니다. 그러니 여러분이 여기서 배운 것을 17장에서 다시 적용하게 될 것입니다!","breadcrumbs":"스마트 포인터 » Box는 힙에 있는 데이터를 가리키고 알려진 크기를 갖습니다 » Box는 힙에 있는 데이터를 가리키고 알려진 크기를 갖습니다","id":"261","title":"Box는 힙에 있는 데이터를 가리키고 알려진 크기를 갖습니다"},"262":{"body":"Box에 대한 사용례를 논의하기 전에, 먼저 문법 및 Box 내에 저장된 값과 어떻게 상호작용 하는지 다루겠습니다. Listing 15-1은 힙에 i32 값을 저장하기 위해 박스를 사용하는 법을 보여줍니다: Filename: src/main.rs fn main() { let b = Box::new(5); println!(\"b = {}\", b);\n} Listing 15-1: 박스를 사용하여 i32 값을 힙에 저장하기 5라는 값을 가리키는 Box의 값을 갖는 변수 b를 선언했는데, 여기서 5는 힙에 할당됩니다. 이 프로그램은 b = 5를 출력할 것입니다; 이 경우, 우리는 마치 이 데이터가 스택에 있었던 것과 유사한 방식으로 박스 내의 데이터에 접근할 수 있습니다. 다른 어떤 소유한 값과 마찬가지로, b가 main의 끝에 도달하는 것처럼 어떤 박스가 스코프를 벗어날 때, 할당은 해제될 것입니다. 할당 해제는 (스택에 저장된) 박스와 이것이 가리키고 있는 (힙에 저장된) 데이터 모두에게 일어납니다. 단일 값을 힙에 집어넣는 것은 그다지 유용하지는 않으므로, 이 방식처럼 박스를 이용하는 것은 자주 쓰지 않을 것입니다. 단일한 i32 같은 값을 스택에 갖는 것은, 스택이 해당 값이 기본적으로 저장되는 곳이기도 하고, 대부분의 경우에서 더 적절합니다. 만일 우리가 박스를 쓰지 않는다면 허용되지 않았을 타입을 정의하도록 해주는 경우를 살펴봅시다.","breadcrumbs":"스마트 포인터 » Box는 힙에 있는 데이터를 가리키고 알려진 크기를 갖습니다 » Box을 사용하여 힙에 데이터를 저장하기","id":"262","title":"Box을 사용하여 힙에 데이터를 저장하기"},"263":{"body":"컴파일 타임에서, 러스트는 어떤 타입이 얼마나 많은 공간을 차지하는지를 알 필요가 있습니다. 컴파일 타임에는 크기를 알 수 없는 한 가지 타입이 바로 재귀적 타입 (recursive type) 인데, 이는 어떤 값이 그 일부로서 동일한 타입의 다른 값을 가질 수 있는 것을 말합니다. 이러한 값의 내포가 이론적으로는 무한하게 계속될 수 있으므로, 러스트는 재귀적 타입의 값이 얼마큼의 공간을 필요로 하는지 알지 못합니다. 하지만, 박스는 알려진 크기를 갖고 있으므로, 재귀적 타입 정의 내에 박스를 넣음으로써 이를 쓸 수 있습니다. 재귀적 타입의 예제로서, 함수형 프로그래밍 언어에서 일반적인 데이터 타입인 cons list 를 탐험해 봅시다. 우리가 정의할 cons list 타입은 재귀를 제외하면 직관적입니다; 그러므로, 우리가 작업할 예제에서의 개념은 여러분이 재귀적 타입을 포함하는 더 복잡한 어떠한 경우에 처하더라도 유용할 것입니다. Cons List에 대한 더 많은 정보 cons list 는 Lisp 프로그래밍 언어 및 그의 파생 언어들로부터 유래된 데이터 구조입니다. Lisp에서, (“생성 함수 (construct function)”의 줄임말인) cons 함수는 두 개의 인자를 받아 새로운 한 쌍을 생성하는데, 이 인자는 보통 단일 값과 또 다른 쌍입니다. 이러한 쌍들을 담고 있는 쌍들이 리스트를 형성합니다. cons 함수 개념은 더 일반적인 함수형 프로그래밍 용어로 나아갑니다: “to cons x onto y”는 약식으로 요소 x를 새로운 컨테이너에 집어넣고, 그다음 컨테이너 y를 넣는 식으로 새로운 컨테이너 인스턴스를 생성하는 것을 의미합니다. cons list 내의 각 아이템은 두 개의 요소를 담고 있습니다: 현재 아이템의 값과 다음 아이템이지요. 리스트의 마지막 아이템은 다음 아이템 없이 Nil 이라 불리는 값을 담고 있습니다. cons list는 cons 함수를 재귀적으로 호출함으로써 만들어집니다. 재귀의 기본 케이스를 의미하는 표준 이름이 바로 Nil 입니다. 유효하지 않은 값 혹은 값이 없는 것을 말하는 6장의 “null” 혹은 “nil” 개념과 동일하지 않다는 점을 주의하세요. 비록 함수형 프로그래밍 언어들이 cons list를 자주 사용할지라도, 러스트에서는 흔히 사용되는 데이터 구조가 아닙니다. 러스트에서 아이템의 리스트를 갖는 대부분의 경우에는, Vec이 사용하기에 더 나은 선택입니다. 그와는 다른, 더 복잡한 재귀적 데이터 타입들은 다양한 상황들에서 유용 하기는 하지만, cons list를 가지고 시작함으로써, 박스가 어떻게 재귀적 데이터 타입을 정의하도록 해주는지 우리의 집중을 방해하는 것들 없이 탐구할 수 있습니다. Listing 15-2는 cons list를 위한 열거형 정의를 담고 있습니다. 우리가 보여주고자 하는 것인데, List 타입이 알려진 크기를 가지고 있지 않고 있기 때문에 이 코드는 아직 컴파일이 안된다는 점을 유의하세요: Filename: src/main.rs enum List { Cons(i32, List), Nil,\n} Listing 15-2: i32 값의 cons list 데이터 구조를 표현하는 열거형 정의에 대한 첫 번째 시도 노트: 이 예제의 목적을 위해 오직 i32 값만 담는 cons list를 구현하고 있습니다. 우리가 10장에서 논의한 것처럼, 임의의 타입 값을 저장할 수 있는 cons list 타입을 정의하기 위해서는 제네릭을 이용해 이를 구현할 수도 있습니다. List 타입을 이용하여 리스트 1, 2, 3을 저장하는 것은 Listing 15-3의 코드와 같이 보일 것입니다: Filename: src/main.rs use List::{Cons, Nil}; fn main() { let list = Cons(1, Cons(2, Cons(3, Nil)));\n} Listing 15-3: List 열거형을 이용하여 리스트 1, 2, 3 저장하기 첫 번째 Cons 값은 1과 List 값을 갖습니다. 이 List 값은 2와 또 다른 List 값을 갖는 Cons 값입니다. 그 안의 List 값은 3과 List 값을 갖는 추가적인 Cons인데, 여기서 마지막의 List은 Nil로서, 리스트의 끝을 알리는 비재귀적인 variant입니다. 만일 Listing 15-3의 코드를 컴파일하고자 시도하면, Listing 15-4에 보이는 에러를 얻습니다: error[E0072]: recursive type `List` has infinite size --> src/main.rs:1:1 |\n1 | enum List { | ^^^^^^^^^ recursive type has infinite size\n2 | Cons(i32, List), | ----- recursive without indirection | = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable Listing 15-4: 재귀적 열거형을 정의하고자 시도했을 때 얻게 되는 에러 이 에러는 이 타입이 “무한한 크기를 갖는다”라고 말해줍니다. 그 원인은 우리가 재귀적인 variant를 이용하여 List를 정의했기 때문입니다: 즉 이것은 또 다른 자신을 직접 값으로 갖습니다. 결과적으로, 러스트는 List 값을 저장하는데 필요한 크기가 얼마나 되는지 알아낼 수 없습니다. 왜 우리가 이런 에러를 얻게 되는지 좀 더 쪼개어 봅시다: 먼저, 러스트가 비재귀적인 타입의 값을 저장하는데 필요한 용량이 얼마나 되는지 결정하는 방법을 살펴봅시다. 비재귀적 타입의 크기 계산하기 6장에서 열거형 정의에 대해 논의할 때 우리가 Listing 6-2에서 정의했던 Message 열거형을 상기해봅시다: enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32),\n} Message 값을 할당하기 위해 얼마나 많은 공간이 필요한지를 결정하기 위해서, 러스트는 어떤 variant가 가장 많은 공간을 필요로 하는지를 알기 위해 각각의 variant들 내부를 봅니다. 러스트는 Message::Quit가 어떠한 공간도 필요 없음을 알게 되고, Message::Move는 두 개의 i32 값을 저장하기에 충분한 공간이 필요함을 알게 되고, 그렇게 진행됩니다. 단 하나의 variant만 사용될 것이기 때문에, Message 값이 필요로 하는 가장 큰 공간은 그것의 varient 중 가장 큰 것을 저장하는데 필요한 공간입니다. 러스트가 Listing 15-2의 List 열거형과 같은 재귀적 타입이 필요로 하는 공간을 결정하고자 시도할 때 어떤 일이 일어나는지를 이와 대조해보세요. 컴파일러는 Cons variant를 살펴보는 것을 시작하는데, 이는 i32 타입의 값과 List 타입의 값을 갖습니다. 그러므로, Cons는 i32의 크기에 List 크기를 더한 만큼의 공간을 필요로 합니다. List 타입이 얼마나 많은 메모리를 차지하는지 알아내기 위해서, 컴파일러는 그것의 variants를 살펴보는데, 이는 Cons variant로 시작됩니다. Cons variant는 i32 타입의 값과 List 타입의 값을 갖고, 이 과정은 Figure 15-1에서 보는 바와 같이 무한히 계속됩니다: Figure 15-1: 무한한 Cons variant를 가지고 있는 무한한 List Box를 이용하여 알려진 크기를 가진 재귀적 타입 만들기 러스트는 재귀적으로 정의된 타입을 위하여 얼마큼의 공간을 할당하는지 알아낼 수 없으므로, 컴파일러는 Listing 15-4의 에러를 내줍니다. 하지만 이 에러는 아래와 같은 유용한 제안을 포함하고 있습니다: = help: insert indirection (e.g., a `Box`, `Rc`, or `&`) at some point to make `List` representable 이 제안에서, “간접 (indirection)”은 값을 직접 저장하는 대신, 간접적으로 값의 포인터를 저장하기 위하여 데이터 구조를 바꿀 수 있음을 의미합니다. Box가 포인터이기 때문에, 러스트는 언제나 Box가 필요로 하는 공간이 얼마인지 알고 있습니다: 포인터의 크기는 그것이 가리키고 있는 데이터의 양에 기반하여 변경되지 않습니다. 이는 우리가 Cons variant 내에 또 다른 List 값을 직접 넣는 대신 Box를 넣을 수 있다는 뜻입니다. Box는 Cons variant 안에 있기보다는 힙에 있을 다음의 List 값을 가리킬 것입니다. 개념적으로, 우리는 다른 리스트들을 “담은” 리스트들로 만들어진 리스트를 여전히 갖게 되지만, 이 구현은 이제 또 다른 것 안의 아이템들이 아니라 또 다른 것 옆에 있는 아이템들에 더 가깝습니다. 우리는 Listing 15-2의 List 열거형의 정의와 Listing 15-3의 List 사용법을 Listing 15-5의 코드로 바꿀 수 있는데, 이는 컴파일될 것입니다: Filename: src/main.rs enum List { Cons(i32, Box), Nil,\n} use List::{Cons, Nil}; fn main() { let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));\n} Listing 15-5: 알려진 크기를 갖도록 하기 위해 Box를 이용하는 List의 정의 Cons variant는 i32와 박스의 포인터 데이터를 저장할 공간을 더한 크기를 요구할 것입니다. Nil variant는 아무런 값도 저장하지 않으므로, Cons variant에 비해 공간을 덜 필요로 합니다. 우리는 이제 어떠한 List 값이 i32의 크기 더하기 박스의 포인터 데이터의 크기만큼을 차지할 것인 점을 알게 되었습니다. 박스를 이용함으로써, 우리는 무한하고, 재귀적인 연결을 부수었고, 따라서 컴파일러는 List 값을 저장하는데 필요한 크기를 알아낼 수 있습니다. Figure 15-2는 Cons variant가 이제 어떻게 생겼는지를 보여주고 있습니다: Figure 15-2: Cons가 Box를 들고 있기 때문에 무한한 크기가 아니게 된 List 박스는 단지 간접 및 힙 할당만을 제공할 뿐입니다; 이들은 다른 어떤 특별한 능력들, 우리가 다른 스마트 포인터 타입들에서 보게 될 것 같은 능력들이 없습니다. 또한 이들은 이러한 특별한 능력들이 초래하는 성능적인 오버헤드도 가지고 있지 않으므로, 우리가 필요로 하는 기능이 딱 간접 하나인 cons list와 같은 경우에 유용할 수 있습니다. 우리는 또한 17장에서 박스에 대하여 더 많은 사용례를 살펴볼 것입니다. Box 타입은 스마트 포인터인데 그 이유는 이것이 Deref 트레잇을 구현하고 있기 때문이며, 이는 Box 값이 참조자와 같이 취급되도록 허용해줍니다. Box 값이 스코프 밖으로 벗어날 때, 박스가 가리키고 있는 힙 데이터도 마찬가지로 정리되는데 이는 Drop 트레잇의 구현 때문에 그렇습니다. 이 두 가지 트레잇에 대하여 더 자세히 탐구해 봅시다. 이 두 트레잇이 이 장의 나머지에서 다루게 될 다른 스마트 포인터 타입에 의해 제공되는 기능들보다 심지어 더 중요할 것입니다.","breadcrumbs":"스마트 포인터 » Box는 힙에 있는 데이터를 가리키고 알려진 크기를 갖습니다 » 박스는 재귀적 타입을 가능하게 합니다","id":"263","title":"박스는 재귀적 타입을 가능하게 합니다"},"264":{"body":"Deref 트레잇을 구현하는 것은 우리가 (곱하기 혹은 글롭 연산자와는 반대 측에 있는) 역참조 연산자 (dereference operator) * 의 동작을 커스터마이징 하는 것을 허용합니다. 스마트 포인터가 평범한 참조자처럼 취급될 수 있는 방식으로 Deref를 구현함으로써, 우리는 참조자에 대해 작동하는 코드를 작성하고 이 코드를 또한 스마트 포인터에도 사용할 수 있습니다. 먼저 *가 보통의 참조자와 어떤 식으로 동작하는지를 살펴보고, 그런 다음 Box와 비슷한 우리만의 타입을 정의하는 시도를 해서 왜 *가 우리의 새로 정의된 타입에서는 참조자처럼 작동하지 않는지를 봅시다. 우리는 Deref 트레잇을 구현하는 것이 어떻게 스마트 포인터가 참조자와 유사한 방식으로 동작하는 것을 가능하게 해 주는지를 탐구할 것입니다. 그런 뒤 러스트의 역참조 강제 (deref coercion) 기능과 이 기능이 어떻게 참조자 혹은 스마트 포인터와 함께 동작하도록 하는지 살펴보겠습니다.","breadcrumbs":"스마트 포인터 » Deref 트레잇은 참조자를 통하여 데이터로의 접근을 허용합니다 » Deref 트레잇을 가지고 스마트 포인터를 평범한 참조자와 같이 취급하기","id":"264","title":"Deref 트레잇을 가지고 스마트 포인터를 평범한 참조자와 같이 취급하기"},"265":{"body":"보통의 참조자는 포인터 타입이며, 포인터를 생각하는 한 가지 방법은 다른 어딘가에 저장된 값을 가리키는 화살표로서 생각하는 것입니다. Listing 15-6에서는 i32 값의 참조자를 생성하고는 참조자를 따라가서 값을 얻기 위해 역참조 연산자를 사용합니다: Filename: src/main.rs fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y);\n} Listing 15-6: 역참조 연산자를 사용하여 i32 값에 대한 참조자를 따라가기 변수 x는 i32 값을 가지고 있습니다. y에는 x의 참조자를 설정했습니다. 우리는 x가 5와 동일함을 단언할 수 있습니다. 하지만, 만일 y 안의 값에 대한 단언을 만들고 싶다면, 참조자를 따라가서 이 참조자가 가리키고 있는 값을 얻기 위해 *y를 사용해야 합니다 (그래서 역참조 라 합니다). 일단 y를 역참조하면, 5와 비교 가능한 y가 가리키고 있는 정수 값에 접근하게 됩니다. 대신 assert_eq!(5, y);라고 작성하길 시도했다면, 아래와 같은 컴파일 에러를 얻을 것입니다: error[E0277]: the trait bound `{integer}: std::cmp::PartialEq<&{integer}>` is\nnot satisfied --> src/main.rs:6:5 |\n6 | assert_eq!(5, y); | ^^^^^^^^^^^^^^^^^ can't compare `{integer}` with `&{integer}` | = help: the trait `std::cmp::PartialEq<&{integer}>` is not implemented for `{integer}` 숫자와 숫자에 대한 참조자를 비교하는 것은 허용되지 않는데 그 이유는 이들이 서로 다른 타입이기 때문입니다. *를 사용하여 해당 참조자를 따라가서 그것이 가리키고 있는 값을 얻어야 합니다.","breadcrumbs":"스마트 포인터 » Deref 트레잇은 참조자를 통하여 데이터로의 접근을 허용합니다 » *와 함께 포인터를 따라가서 값을 얻기","id":"265","title":"*와 함께 포인터를 따라가서 값을 얻기"},"266":{"body":"Listing 15-7에서 보는 바와 같이, Listing 15-6의 코드는 참조자 대신 Box를 이용하여 재작성될 수 있으며, 역참조 연산자는 동일한 방식으로 작동될 것입니다: Filename: src/main.rs fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y);\n} Listing 15-7: Box 상에 역참조 연산자 사용하기 Listing 15-7와 Listing 15-6 사이의 차이점은 오직 x의 값을 가리키는 참조자보다는 x를 가리키는 박스의 인스턴스로 y를 설정했다는 것입니다. 마지막 단언문에서, 우리는 y가 참조자일 때 했던 것과 동일한 방식으로 박스 포인터 앞에 역참조 연산자를 사용할 수 있습니다. 다음으로, 우리만의 박스 타입을 정의함으로써 Box가 우리에게 역참조 연산자를 사용 가능하게끔 해주는 특별함이 무엇인지 탐구해 보겠습니다.","breadcrumbs":"스마트 포인터 » Deref 트레잇은 참조자를 통하여 데이터로의 접근을 허용합니다 » Box를 참조자처럼 사용하기","id":"266","title":"Box를 참조자처럼 사용하기"},"267":{"body":"어떤 식으로 스마트 포인터가 기본적으로 참조자와는 다르게 동작하는지를 경험하기 위해서, 표준 라이브러리가 제공하는 Box 타입과 유사한 스마트 포인터를 만들어 봅시다. 그런 다음 어떻게 역참조 연산자를 사용할 수 있는 기능을 추가하는지 살펴보겠습니다. Box 타입은 궁극적으로 하나의 요소를 가진 튜플 구조체로 정의되므로, Listing 15-8은 MyBox 타입을 동일한 방식으로 정의하였습니다. 또한 Box에 정의되어 있는 new 함수에 맞추기 위해 new 함수도 정의하겠습니다: Filename: src/main.rs struct MyBox(T); impl MyBox { fn new(x: T) -> MyBox { MyBox(x) }\n} Listing 15-8: MyBox 타입 정의하기 우리는 MyBox라는 이름의 구조체를 정의하고 제네릭 파라미터 T를 선언했는데, 이는 우리의 타입이 어떠한 종류의 타입 값이든 가질 수 있길 원하기 때문입니다. MyBox 타입은 T 타입의 하나의 요소를 가진 튜플 구조체입니다. MyBox::new 함수는 T 타입인 하나의 파라미터를 받아서 그 값을 갖는 MyBox 인스턴스를 반환합니다. Listing 15-7의 main 함수를 Listing 15-8에 추가하고 Box 대신 우리가 정의한 MyBox를 이용하도록 수정해봅시다. Listing 15-9는 컴파일되지 않을 것인데 그 이유는 러스트가 MyBox를 어떻게 역참조 하는지 모르기 때문입니다: Filename: src/main.rs fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y);\n} Listing 15-9: 참조자와 Box를 사용한 것과 동일한 방식으로 MyBox 사용 시도하기 아래는 그 결과 발생한 컴파일 에러입니다: error[E0614]: type `MyBox<{integer}>` cannot be dereferenced --> src/main.rs:14:19 |\n14 | assert_eq!(5, *y); | ^^ 우리의 MyBox 타입은 역참조 될 수 없는데 그 이유는 우리의 타입에 대해 해당 기능을 아직 구현하지 않았기 때문입니다. * 연산자로 역참조를 가능케 하기 위해서, 우리는 Deref 트레잇을 구현합니다.","breadcrumbs":"스마트 포인터 » Deref 트레잇은 참조자를 통하여 데이터로의 접근을 허용합니다 » 우리만의 스마트 포인터 정의하기","id":"267","title":"우리만의 스마트 포인터 정의하기"},"268":{"body":"10장에서 논의한 바와 같이, 트레잇을 구현하기 위해서는 트레잇의 요구 메소드들에 대한 구현체를 제공할 필요가 있습니다. 표준 라이브러리가 제공하는 Deref 트레잇은 우리에게 self를 빌려서 내부 데이터에 대한 참조자를 반환하는 deref라는 이름의 메소드 하나를 구현하도록 요구합니다. Listing 15-10은 MyBox의 정의에 덧붙여 Deref의 구현을 담고 있습니다: Filename: src/main.rs use std::ops::Deref; # struct MyBox(T);\nimpl Deref for MyBox { type Target = T; fn deref(&self) -> &T { &self.0 }\n} Listing 15-10: MyBox 상의 Deref 구현 type Target = T; 문법은 Deref 트레잇이 사용할 연관 타입 (associated type) 을 정의합니다. 연관 타입은 제네릭 파라미터를 정의하는 것과 약간 다른 방식이지만, 여러분은 지금 이를 걱정할 필요는 없습니다; 우리는 이를 19장에서 더 자세히 다룰 것입니다. 우리는 deref 메소드의 본체를 &self.0로 채웠으므로 deref는 우리가 * 연산자를 이용해 접근하고자 하는 값의 참조자를 반환합니다. MyBox 값에 대하여 *을 호출하는 Listing 15-9의 main 함수는 이제 컴파일되고 단언문은 통과됩니다! Deref 트레잇 없이, 컴파일러는 오직 & 참조자들만 역참조 할 수 있습니다. deref 메소드는 컴파일러에게 Deref를 구현한 어떠한 타입의 값을 가지고 & 참조자를 가져오기 위해서 어떻게 역참조 하는지 알고 있는 deref 메소드를 호출하는 기능을 부여합니다. Listing 15-9의 *y에 들어설 때, 무대 뒤에서 러스트는 실제로 아래의 코드를 실행했습니다: *(y.deref()) 러스트는 * 연산자에 deref 메소드 호출 후 보통의 역참조를 대입하므로 프로그래머로서 우리는 deref 메소드를 호출할 필요가 있는지 혹은 없는지를 생각하지 않아도 됩니다. 이 러스트의 기능은 우리가 보통의 참조자를 가지고 있는 경우 혹은 Deref를 구현한 타입을 가지고 있는 경우에 대하여 동일하게 기능하는 코드를 작성하도록 해 줍니다. deref 메소드가 값의 참조자를 반환하고 *(y.deref())에서의 괄호 바깥의 평범한 역참조가 여전히 필요한 이유는 소유권 시스템 때문입니다. 만일 deref 메소드가 값의 참조자 대신 값을 직접 반환했다면, 그 값은 self 바깥으로 이동될 것입니다. 위의 경우 및 우리가 역참조 연산자를 사용하는 대부분의 경우에서 우리는 MyBox 내부의 값에 대한 소유권을 얻길 원치 않습니다. 우리의 코드에 *를 한번 타이핑할 때마다, *는 deref 함수의 호출 후 *를 한번 호출하는 것으로 대치된다는 점을 기억하세요. *의 대입이 무한히 재귀적으로 실행되지 않기 때문에, 우리는 결국 i32 타입의 데이터를 얻는데, 이는 Listing 15-9의 assert_eq! 내의 5와 일치합니다.","breadcrumbs":"스마트 포인터 » Deref 트레잇은 참조자를 통하여 데이터로의 접근을 허용합니다 » Deref 트레잇을 구현하여 임의의 타입을 참조자처럼 다루기","id":"268","title":"Deref 트레잇을 구현하여 임의의 타입을 참조자처럼 다루기"},"269":{"body":"역참조 강제(deref coercion) 는 러스트가 함수 및 메소드의 인자에 수행하는 편의성 기능입니다. 역참조 강제는 Deref를 구현한 어떤 타입의 참조자를 Deref가 본래의 타입으로부터 바꿀 수 있는 타입의 참조자로 바꿔줍니다. 역참조 강제는 우리가 특정 타입의 값에 대한 참조자를 함수 혹은 메소드의 인자로 넘기는 중 정의된 파라미터 타입에는 맞지 않을 때 자동적으로 발생합니다. 일련의 deref 메소드 호출은 우리가 제공한 타입을 파라미터가 요구하는 타입으로 변경해 줍니다. 역참조 강제가 러스트에 도입되어서 함수와 메소드 호출을 작성하는 프로그래머들은 &와 *를 이용한 많은 수의 명시적 참조 및 역참조를 추가하지 않아도 됩니다. 역참조 강제 기능은 또한 우리가 참조자나 스마트 포인터 둘 중 어느 경우라도 작동할 수 있는 코드를 더 많이 작성할 수 있도록 해 줍니다. 역참조 강제가 실제 작동하는 것을 보기 위해서, 우리가 Listing 15-8에서 정의했던 MyBox과 Listing 15-10에서 추가했던 Deref의 구현체를 이용합시다. Listing 15-11은 스트링 슬라이스 파라미터를 갖는 함수의 정의를 보여줍니다: Filename: src/main.rs fn hello(name: &str) { println!(\"Hello, {}!\", name);\n} Listing 15-11: 타입 &str의 name이라는 파라미터를 갖는 hello 함수 우리는 예를 들면 hello(\"Rust\");와 같이 스트링 슬라이스를 인자로 하여 hello 함수를 호출할 수 있습니다. Listing 15-12에서 보는 바와 같이, 역참조 강제는 MyBox 타입의 값에 대한 참조자를 이용하여 hello를 호출하는 것을 가능하게 해 줍니다: Filename: src/main.rs # use std::ops::Deref;\n#\n# struct MyBox(T);\n#\n# impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n#\n# impl Deref for MyBox {\n# type Target = T;\n#\n# fn deref(&self) -> &T {\n# &self.0\n# }\n# }\n#\n# fn hello(name: &str) {\n# println!(\"Hello, {}!\", name);\n# }\n#\nfn main() { let m = MyBox::new(String::from(\"Rust\")); hello(&m);\n} Listing 15-12: 역참조 강제 때문에 작동되는, MyBox 값에 대한 참조자로 hello 호출하기 여기서 우리는 hello 함수를 호출하는 인자로서 &m를 이용했는데, 이는 MyBox의 참조자입니다. 우리가 Listing 15-10에서 MyBox의 Deref 트레잇을 구현했기 때문에, 러스트는 deref를 호출하여 &MyBox을 &String으로 바꿀 수 있습니다. 표준 라이브러리는 스트링 슬라이스를 반환하는 String의 Deref 구현체를 제공하는데, 이는 Deref에 대한 API 문서에도 있습니다. 러스트는 deref를 다시 한번 호출하여 &String을 &str로 변환하고, 이는 hello 함수의 정의와 일치하게 됩니다. 만일 러스트가 역참조 강제 기능을 구현하지 않았다면, 우리는 &MyBox 타입의 값을 가지고 hello 함수를 호출하는 데 있어 Listing 15-12의 코드 대신 Listing 15-13의 코드를 작성해야 했을 것입니다: Filename: src/main.rs # use std::ops::Deref;\n#\n# struct MyBox(T);\n#\n# impl MyBox {\n# fn new(x: T) -> MyBox {\n# MyBox(x)\n# }\n# }\n#\n# impl Deref for MyBox {\n# type Target = T;\n#\n# fn deref(&self) -> &T {\n# &self.0\n# }\n# }\n#\n# fn hello(name: &str) {\n# println!(\"Hello, {}!\", name);\n# }\n#\nfn main() { let m = MyBox::new(String::from(\"Rust\")); hello(&(*m)[..]);\n} Listing 15-13: 만일 러스트에 역참조 강제가 없었다면 우리가 작성했어야 했을 코드 (*m)은 MyBox을 String로 역참조해 줍니다. 그런 다음 &과 [..]은 hello 시그니처와 일치되도록 전체 스트링과 동일한 String의 스트링 슬라이스를 얻습니다. 역참조 강제가 없는 코드는 이러한 모든 기호들이 수반된 상태에서 읽기도, 쓰기도, 이해하기도 더 힘들어집니다. 역참조 강제는 러스트가 우리를 위해 이러한 변환을 자동적으로 다룰 수 있도록 해 줍니다. Deref 트레잇이 관련된 타입에 대해 정의될 때, 러스트는 해당 타입을 분석하여 파라미터의 타입에 맞는 참조자를 얻기 위해 필요한 수만큼의 Deref::deref를 사용할 것입니다. Deref::deref가 삽입될 필요가 있는 횟수는 컴파일 타임에 분석되므로, 역참조 강제의 이점을 얻는 데에 관해서 어떠한 런타임 페널티도 없습니다!","breadcrumbs":"스마트 포인터 » Deref 트레잇은 참조자를 통하여 데이터로의 접근을 허용합니다 » 함수와 메소드를 이용한 암묵적 역참조 강제","id":"269","title":"함수와 메소드를 이용한 암묵적 역참조 강제"},"27":{"body":"이제 Cargo로 만든 “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 에) 실행 파일을 생성합니다. 여러분은 아래 커맨드를 통해 이 실행 파일을 실행할 수 있습니다: $ ./target/debug/hello_cargo # or .\\target\\debug\\hello_cargo.exe on Windows\nHello, world! 만일 모든 것이 잘 진행되었다면, 터미널에 Hello, world!가 출력되어야 합니다. 처음으로 cargo build를 실행하는 것은 또한 Cargo가 최상위 디렉토리에 Cargo.lock 이라는 새로운 파일을 생성하도록 합니다. 이 프로젝트는 어떠한 의존성도 가지고 있지 않으므로, 파일의 내용이 얼마 없습니다. 여러분이 이 파일을 손수 변경할 필요는 전혀 없습니다; Cargo가 여러분을 위해 이 파일의 내용을 관리합니다. 우리는 그저 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가 hello_cargo를 컴파일하는 중이었다는 것을 나타내는 출력을 볼 수 없음을 주목하세요. Cargo는 파일들이 변경된 적이 없음을 알아내고, 따라서 해당 바이너리를 그저 실행했을 뿐입니다. 만일 여러분이 여러분의 코드를 수정한 적 있다면, 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는 또한 cargo check라고 하는 커맨드를 제공합니다. 이 커맨드는 여러분의 코드가 컴파일되는지를 빠르게 확인해주지만 실행파일을 생성하지는 않습니다: $ cargo check Compiling 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 check을 실행합니다. 그런 다음 실행파일을 사용할 준비가 되었을 때 cargo build를 실행합니다. 여태까지 Cargo에 대하여 우리가 배운 것들을 정리하자면: 우리는 cargo build나 cargo check를 사용하여 프로젝트를 빌드할 수 있습니다. 우리는 cargo run를 사용하여 단숨에 프로젝트를 빌드하고 실행할 수 있습니다. 우리 코드가 있는 동일한 디렉토리에 빌드의 결과물이 저장되는 대신, Cargo는 이를 target/debug 디렉토리에 저장합니다. Cargo를 사용하면 생기는 추가적인 장점은 여러분이 어떠한 운영체제로 작업을 하든 상관없이 커맨드들이 동일하다는 점입니다. 따라서 이러한 점 때문에 우리는 더 이상 Linux와 macOS 및 Windows를 위한 특정 명령을 제공하지 않을 것입니다.","breadcrumbs":"시작하기 » Hello, Cargo! » Cargo 프로젝트를 빌드하고 실행하기","id":"27","title":"Cargo 프로젝트를 빌드하고 실행하기"},"270":{"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":"270","title":"역참조 강제가 가변성과 상호작용 하는 법"},"271":{"body":"스마트 포인터 패턴에서 중요한 두 번째 트레잇은 Drop인데, 이는 값이 스코프 밖으로 벗어나려고 할 때 어떤 일이 발생될지를 커스터마이징하게끔 해줍니다. 우리는 어떠한 타입이 든 간에 Drop 트레잇을 위한 구현을 제공할 수 있고, 우리가 특정한 코드는 파일이나 네트워크 연결 같은 자원을 해제하는 데에 사용될 수 있습니다. 우리는 스마트 포인터의 맥락 안에서 Drop을 소개하고 있는데 그 이유는 Drop 트레잇의 기능이 언제나 대부분 스마트 포인터를 구현할 때에 사용되기 때문입니다. 예를 들면, Box는 박스가 가리키고 있는 힙 상의 공간을 할당 해제하기 위해 Drop을 커스터마이징 합니다. 몇몇 언어들에서, 프로그래머는 스마트 포인터의 인스턴스 사용을 종료하는 매번 마다 메모리 혹은 자원을 해제하기 위해 코드를 호출해야 합니다. 만일 이를 잊어버리면, 그 시스템은 과부하가 걸리거나 멈출지도 모릅니다. 러스트에서는 값이 스코프 밖으로 벗어날 때마다 실행되어야 하는 특정한 코드 조각을 특정할 수 있고, 컴파일러는 이 코드를 자동으로 삽입해줄 것입니다. 결과적으로, 우리는 프로그램 내에서 특정한 타입의 인스턴스가 종료되는 곳마다 정리 코드를 집어넣는 것에 관한 걱정을 할 필요가 없지만, 여전히 자원 누수는 발생하지 않을 것입니다! Drop 트레잇을 구현함으로써 값이 스코프 밖으로 벗어났을 때 실행될 코드를 특정합니다. Drop 트레잇은 self에 대한 가변 참조자를 파라미터로 갖는 drop 이라는 이름의 하나의 메소드를 구현하도록 우리에게 요구합니다. 러스트가 언제 drop을 호출하는지 보기 위해서, 지금은 println! 구문과 함께 drop을 구현해봅시다. Listing 15-4는 인스턴스가 스코프 밖으로 벗어났을 때 Dropping CustomSmartPointer!를 출력하는 커스텀 기능만을 갖춘 CustomSmartPointer 구조체를 보여주고 있습니다. 이 예제는 러스트가 drop 함수를 실행시키는 때를 보여줍니다: Filename: 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} Listing 15-14: 우리의 정리 코드를 넣을 수 있는 Drop 트레잇을 구현한 CustomSmartPointer 구조체 Drop 트레잇은 프렐루드에 포함되어 있으므로, 이를 가져오지 않아도 됩니다. 우리는 CustomSmartPointer 상에 Drop 트레잇을 구현하였고, println!을 호출하는 drop 메소드 구현을 제공했습니다. drop 함수의 본체는 여러분이 만든 타입의 인스턴스가 스코프 밖으로 벗어났을 때 실행시키고자 하는 어떠한 로직이라도 위치시킬 수 있는 곳입니다. 우리는 여기서 러스트가 drop을 호출하게 될 때를 보여주기 위해서 어떤 텍스트를 출력하는 중입니다. main에서는 두 개의 CustomSmartPointer 인스턴스를 만든 다음 CustomSmartPointers created.를 출력합니다. main의 끝에서, 우리의 CustomSmartPointer 인스턴스는 스코프 밖으로 벗어날 것이고, 러스트는 우리가 drop 메소드 내에 집어넣은 코드, 즉 우리의 마지막 메시지를 출력하는 코드를 호출할 것입니다. 우리가 drop 메소드를 명시적으로 호출할 필요가 없다는 점을 주의하세요. 이 프로그램을 실행시켰을 때, 다음과 같은 출력을 보게 될 것입니다: CustomSmartPointers created.\nDropping CustomSmartPointer with data `other stuff`!\nDropping CustomSmartPointer with data `my stuff`! 러스트는 우리의 인스턴스가 스코프 밖으로 벗어났을 때 우리를 위하여 drop를 호출했고, 우리가 특정한 그 코드를 호출하게 됩니다. 변수들은 만들어진 순서의 역순으로 버려지므로, d는 c 전에 버려집니다. 이 예제는 여러분에게 drop 메소드가 어떻게 동작하는지에 대한 시각적인 가이드만을 제공하지만, 여러분은 보통 메시지 출력보다는 여러분의 타입이 실행할 필요가 있는 정리 코드를 특정할 것입니다.","breadcrumbs":"스마트 포인터 » Drop 트레잇은 메모리 정리 코드를 실행시킵니다 » Drop 트레잇은 메모리 정리 코드를 실행시킵니다","id":"271","title":"Drop 트레잇은 메모리 정리 코드를 실행시킵니다"},"272":{"body":"불행하게도, 자동적인 drop 기능을 비활성화하는 것은 직관적이지 않습니다. drop 비활성화는 보통 필요가 없습니다; Drop 트레잇의 전체적 관점은 자동적으로 다루어진다는 것입니다. 가끔, 여러분은 값을 일찍 정리하기를 원할 지도 모릅니다. 한 가지 예는 락을 관리하는 스마트 포인터를 이용할 때입니다: 여러분은 실행할 락을 해제하는 drop 메소드를 강제로 실행시켜서 같은 스코프 내의 다른 코드가 락을 얻을 수 있길 원할지도 모릅니다. 러스트는 우리가 수동으로 Drop 트레잇의 drop 메소드를 호출하도록 해주지 않습니다; 대신 우리가 스코프 밖으로 벗어나기 전에 값이 강제로 버려질 원한다면 표준 라이브러리에서 제공하는 std::mem::drop 함수를 호출해야 합니다. Listing 15-14의 main 함수를 Listing 15-15 처럼 수정하여 Drop 트레잇의 drop 메소드를 호출하려고 하면 어떤 일이 벌어지는지 봅시다: Filename: src/main.rs fn main() { let c = CustomSmartPointer { data: String::from(\"some data\") }; println!(\"CustomSmartPointer created.\"); c.drop(); println!(\"CustomSmartPointer dropped before the end of main.\");\n} Listing 15-15: 메모리 정리를 일찍 하기 위해 Drop 트레잇으로부터 drop 메소드를 호출 시도하기 이 코드의 컴파일을 시도하면, 다음과 같은 에러를 얻게 됩니다: error[E0040]: explicit use of destructor method --> src/main.rs:14:7 |\n14 | c.drop(); | ^^^^ explicit destructor calls not allowed 이 에러 메시지는 우리가 drop를 명시적으로 호출하는 것이 허용되지 않음을 기술하고 있습니다. 에러 메시지는 소멸자 (destructor) 라는 용어를 사용하는데, 이는 인스턴스를 정리하는 함수에 대한 일반적인 프로그래밍 용어입니다. 소멸자 는 인스턴스를 생성하는 생성자 (constructor) 와 비슷합니다. 러스트 내의 drop 함수는 특정한 형태의 소멸자입니다. 러스트는 우리가 drop을 명시적으로 호출하도록 해주지 않는데 이는 러스트가 main의 끝에서 값에 대한 drop 호출을 여전히 자동적으로 할 것이기 때문입니다. 이는 러스트가 동일한 값을 두 번 메모리 정리를 시도할 수 있기 때문에 중복 해제 (double free) 에러가 될 수 있습니다. 우리는 값이 스코프 밖으로 벗어났을 때 자동적인 drop 추가를 비활성화할 수 없고, drop 메소드를 명시적으로 호출할 수도 없습니다. 따라서, 값이 일찍 메모리 정리되도록 강제하길 원한다면, std::mem::drop 함수를 이용할 수 있습니다. std::mem::drop 함수는 Drop 트레잇 내에 있는 drop 메소드와 다릅니다. 우리가 일찍 버리도록 강제하길 원하는 값을 인자로 넘김으로써 이를 호출할 수 있습니다. 이 함수는 프렐루드에 포함되어 있으므로, 우리는 Listing 15-15의 main을 Listing 15-16에서 보는 것처럼 수정할 수 있습니다: Filename: src/main.rs # struct CustomSmartPointer {\n# data: String,\n# }\n#\n# impl Drop for CustomSmartPointer {\n# fn drop(&mut self) {\n# println!(\"Dropping CustomSmartPointer!\");\n# }\n# }\n#\nfn main() { let c = CustomSmartPointer { data: String::from(\"some data\") }; println!(\"CustomSmartPointer created.\"); drop(c); println!(\"CustomSmartPointer dropped before the end of main.\");\n} Listing 15-16: 값이 스코프 밖으로 벗어나기 전에 명시적으로 버리기 위한 std::mem::drop 호출하기 이 코드의 실행은 다음을 출력할 것입니다: CustomSmartPointer 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":"272","title":"std::mem::drop을 이용하여 값을 일찍 버리기"},"273":{"body":"대부분의 경우에서, 소유권은 명확합니다: 여러분은 어떤 변수가 주어진 값을 소유하는지 정확히 압니다. 그러나, 하나의 값이 여러 개의 소유자를 가질 수도 있는 경우가 있습니다. 예를 들면, 그래프 데이터 구조에서, 여러 에지가 동일한 노드를 가리킬 수도 있고, 그 노드는 개념적으로 해당 노드를 가리키는 모든 에지들에 의해 소유됩니다. 노드는 어떠한 에지도 이를 가리키지 않을 때까지는 메모리 정리가 되어서는 안됩니다. 복수 소유권을 가능하게 하기 위해서, 러스트는 Rc라 불리우는 타입을 가지고 있습니다. 이 이름은 참조 카운팅 (reference counting) 의 약자인데, 이는 어떤 값이 계속 사용되는지 혹은 그렇지 않은지를 알기 위해 해당 값에 대한 참조자의 갯수를 계속 추적하는 것입니다. 만일 값에 대한 참조자가 0개라면, 그 값은 어떠한 참조자도 무효화하지 않고 메모리 정리될 수 있습니다. Rc를 거실의 TV로 상상해보세요. 만일 한 사람이 TV를 보러 들어온다면, TV를 킵니다. 다른 사람들은 거실로 들어와서 TV를 볼 수 있습니다. 마지막 사람이 거실을 나선다면, TV는 더 이상 사용되지 않으므로 이를 끕니다. 만일 다른 사람들이 여전히 TV를 보고 있는 중에 누군가가 이를 끈다면, 남은 TV 시청자들로부터 엄청난 소란이 있을 것입니다! 우리 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶고, 어떤 부분이 그 데이터를 마지막에 이용하게 될지 컴파일 타임에는 알 수 없는 경우 Rc 타입을 사용합니다. 만일 어떤 부분이 마지막으로 사용하는지 알 수 있다면, 우리는 그냥 그 해당 부분을 데이터의 소유자로 만들면 되고, 컴파일 타임에 집행되는 보통의 소유권 규칙이 효과를 발생시킬 것입니다. Rc가 오직 단일 스레드 시나리오 상에서만 사용 가능하다는 점을 주의하세요. 16장에서 동시성 (concurrency) 을 논의할 때, 다중 스레드 프로그램에서는 어떻게 참조 카운팅을 하는지 다루겠습니다.","breadcrumbs":"스마트 포인터 » Rc, 참조 카운팅 스마트 포인터 » Rc, 참조 카운팅 스마트 포인터","id":"273","title":"Rc, 참조 카운팅 스마트 포인터"},"274":{"body":"Listing 15-5의 cons list 예제로 돌아가 봅시다. 우리는 Box를 이용해서 이것을 정의했던 것을 상기하세요. 이번에는 세 번째 리스트의 소유권을 둘 다 공유하는 두 개의 리스트를 만들 것인데, 이는 개념적으로 Figure 15-3과 유사하게 보일 것입니다: Figure 15-3: 세 번째 리스트 a의 소유권을 공유하는 두 리스트 b와 c 우리는 5와 10을 담은 리스트 a를 만들 것입니다. 그런 다음 두 개의 리스트를 더 만들 것입니다: 3으로 시작하는 b와 4로 시작하는 c입니다. 그리고 나서 b와 c 리스트 둘 모두 5와 10을 담고 있는 첫번째 a 리스트로 계속되게 할 것입니다. 바꿔 말하면, 두 리스트 모두 5와 10을 담은 첫 리스트를 공유하게 될 것입니다. Listing 15-17에서 보시는 것처럼, 우리가 Box를 가지고 정의한 List를 이용하여 이 시나리오를 구현하는 시도는 작동하지 않을 것입니다: Filename: src/main.rs enum List { Cons(i32, Box), Nil,\n} use 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} Listing 15-17: Box를 이용한 두 리스트가 세 번째 리스트의 소유권을 공유하는 시도는 허용되지 않음을 보이는 예 이 코드를 컴파일하면, 다음과 같은 에러를 얻습니다: error[E0382]: use of moved value: `a` --> src/main.rs:13:30 |\n12 | let b = Cons(3, Box::new(a)); | - value moved here\n13 | let c = Cons(4, Box::new(a)); | ^ value used here after move | = note: move occurs because `a` has type `List`, which does not implement the `Copy` trait Cons variant는 이것이 가지고 있는 데이터를 소유하므로, 우리가 b리스트를 만들때, a는 b 안으로 이동되고 b는 a를 소유합니다. 그 뒤, c를 생성할 때 a를 다시 이용하는 시도를 할 경우, 이는 a가 이동되었으므로 허용되지 않습니다. 우리는 Cons가 대신 참조자를 갖도록 정의를 변경할 수도 있지만, 그러면 라이프타임 파라미터를 명시해야 할 것입니다. 라이프타임 파라미터를 명시함으로써, 리스트 내의 모든 요소들이 최소한 전체 리스트만큼 오래 살아있을 것입니다. 예를 들어 빌림 검사기는 라이프타임 파라미터를 명시함으로써, let a = Cons(10, &Nil);의 &Nil과 같은 임시 값에 대한 참조를 사용할 수 있게 해줍니다. 그러나 경우에 따라 적합한 라이프타임 매개변수를 지정하는 것이 어렵거나 비실용적일 수 있습니다. 대신, 우리는 Listing 15-18과 같이 Box의 자리에 Rc를 이용하여 List의 정의를 바꿀 것입니다. 각각의 Cons variant는 이제 어떤 값과 List를 가리키는 Rc를 갖게 될 것입니다. b를 만들때는 a의 소유권을 얻는 대신, a를 가지고 있는 Rc를 클론할 것인데, 이는 참조자의 갯수를 하나에서 둘로 증가시키고 a와 b가 Rc 안에 있는 값을 공유하게 해줍니다. 우리는 또한 c를 만들때도 a를 클론할 것인데, 이는 참조자의 갯수를 둘에서 셋으로 늘립니다. 우리가 Rc::clone을 호출하는 매번마다, 해당 Rc가 가지고 있는 데이터에 대한 참조 카운트는 증가할 것이고, 그 데이터는 참조자가 0개가 되지 않으면 메모리 정리되지 않을 것입니다: Filename: src/main.rs enum List { Cons(i32, Rc), Nil,\n} use 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} Listing 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":"274","title":"Rc를 사용하여 데이터 공유하기"},"275":{"body":"Listing 15-18의 동작 예제를 변경하여 a 내부의 Rc에 대한 참조자가 생성되고 드롭될 때 참조 카운트의 변화를 볼 수 있도록 해봅시다. Listing 15-19에서는 main을 변경하여 리스트 c를 감싸고 있는 내부 스코프를 갖도록 하겠습니다; 그런 다음 우리는 c가 스코프 밖으로 벗어났을 때 참조 카운트가 어떻게 변하는지 볼 수 있습니다. 프로그램 내 참조 카운트가 변하는 각 지점에서, 우리는 참조 카운트 값을 출력할텐데, 이는 Rc::strong_count 함수를 호출함으로써 얻을 수 있습니다. 이 함수는 count 보다는 strong_count라는 이름을 갖고 있는데 이는 Rc 타입이 weak_count도 갖고 있기 때문입니다; weak_count가 무엇을 위해 사용되는지는 “참조 순환 (reference cycles) 방지하기”절에서 볼 것입니다. Filename: src/main.rs # enum List {\n# Cons(i32, Rc),\n# Nil,\n# }\n#\n# use List::{Cons, Nil};\n# use std::rc::Rc;\n#\nfn 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} Listing 15-19: 참조 카운트 출력하기 이 코드는 다음을 출력합니다: count 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":"275","title":"Rc의 클론 생성은 참조 카운트를 증가시킵니다"},"276":{"body":"내부 가변성 (interior mutability) 은 어떤 데이터에 대한 불변 참조자가 있을 때라도 여러분이 데이터를 변형할 수 있게 해주는 러스트의 디자인 패턴입니다: 보통 이러한 동작은 빌림 규칙에 의해 허용되지 않습니다. 그렇게 하기 위해서, 이 패턴은 변형과 빌림을 지배하는 러스트의 통상적인 규칙을 구부리기 위하여 데이터 구조 내에서 unsafe (안전하지 않은) 코드를 사용합니다. 우리는 아직 안전하지 않은 코드를 다루지 않았습니다; 이는 19장에서 다룰 것입니다. 우리가 런타임에 빌림 규칙을 따를 것임을 보장할 수 있을 때라면, 심지어 컴파일러가 이를 보장하지 못하더라도 내부 가변성 패턴을 이용하는 타입을 사용할 수 있습니다. 포함되어 있는 unsafe 코드는 안전한 API로 감싸져 있고, 외부 타입은 여전히 불변입니다. 내부 가변성 패턴을 따르는 RefCell 타입을 살펴보는 것으로 이 개념을 탐구해 봅시다.","breadcrumbs":"스마트 포인터 » RefCell와 내부 가변성 패턴 » RefCell와 내부 가변성 패턴","id":"276","title":"RefCell와 내부 가변성 패턴"},"277":{"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":"277","title":"RefCell을 가지고 런타임에 빌림 규칙을 집행하기"},"278":{"body":"빌림 규칙의 결과로 인해 우리는 불변값을 가지고 있을 때 이를 변경 가능하게 빌릴 수 없습니다. 예를 들면, 다음 코드는 컴파일되지 않을 것입니다: fn main() { let x = 5; let y = &mut x;\n} 이 코드의 컴파일을 시도하면, 다음과 같은 에러를 얻을 것입니다: error[E0596]: cannot borrow immutable local variable `x` as mutable --> src/main.rs:3:18 |\n2 | let x = 5; | - consider changing this to `mut x`\n3 | let y = &mut x; | ^ cannot borrow mutably 하지만, 값이 자신의 메소드 내부에서 변경되지만 다른 코드에서는 불변인 것으로 보이는 것이 유용할 수 있는 경우가 있습니다. 그 값의 메소드 바깥의 코드는 값을 변경할 수 없을 것입니다. RefCell을 이용하는 것은 내부 가변성의 기능을 얻는 한가지 방법입니다. 그러나 RefCell은 빌림 규칙을 완벽하게 피하는 것은 아닙니다: 컴파일러 내의 빌림 검사기는 이러한 내부 가변성을 허용하고, 빌림 규칙은 대신 런타임에 검사됩니다. 만일 이 규칙을 위반하면, 우리는 컴파일러 에러 대신 panic!을 얻을 것입니다. 불변 값을 변경하기 위해 RefCell를 이용할 수 있는 실질적인 예제를 살펴보고 이것이 왜 유용한지를 알아봅시다. 내부 가변성에 대한 용례: 목(mock) 객체 테스트 더블 (test double) 은 테스트하는 동안 또다른 타입을 대신하여 사용되는 타입을 위한 일반적인 프로그래밍 개념입니다. 목 객체 (mock object) 는 테스트 중 어떤 일이 일어났는지 기록하여 정확한 동작이 일어났음을 단언할 수 있도록 하는 테스트 더블의 특정한 타입입니다. 러스트는 다른 언어들이 객체를 가지는 것과 동일한 의미의 객체를 가지고 있지 않고, 러스트는 몇몇 다른 언어들이 제공하는 것 같은 표준 라이브러리에 미리 만들어진 목 객체 기능이 없습니다. 하지만, 우리는 목 객체와 동일한 목적을 제공할 구조체를 당연히 만들 수 있습니다. 다음은 우리가 테스트할 시나리오입니다: 우리는 최대값에 맞서 값을 추적하고 현재 값이 최대값에 얼마나 근접한지를 기반으로 메세지를 전송하는 라이브러리를 만들 것입니다. 이 라이브러리는 예를 들면 한 명의 유저에게 허용되고 있는 API 호출수의 허용량을 추적하는데 사용될 수 있습니다. 우리의 라이브러리는 오직 어떤 값이 최대값에 얼마나 근접한지를 추적하고 어떤 시간에 어떤 메세지를 보내야 할지 정하는 기능만을 제공할 것입니다. 우리의 라이브러리를 사용하는 어플리케이션이 메세지를 전송하는 것에 대한 메카니즘을 제공할 예정입니다: 그 어플리케이션은 메세지를 어플리케이션 내에 집어넣거나, 이메일을 보내거나, 문자 메세지를 보내거나, 혹은 기타 다른 것을 할 수 있습니다. 라이브러리는 그런 자세한 사항을 알 필요가 없습니다. 필요한 모든 것은 우리가 제공할 Messenger라는 이름의 트레잇을 구현하는 것입니다. Listing 15-20는 라이브러리 코드를 보여줍니다: Filename: src/lib.rs pub trait Messenger { fn send(&self, msg: &str);\n} pub struct LimitTracker<'a, T: 'a + Messenger> { messenger: &'a T, value: usize, max: usize,\n} impl<'a, T> LimitTracker<'a, T> where T: Messenger { pub fn new(messenger: &T, max: usize) -> LimitTracker { 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 >= 0.75 && percentage_of_max < 0.9 { self.messenger.send(\"Warning: You've used up over 75% of your quota!\"); } else if percentage_of_max >= 0.9 && percentage_of_max < 1.0 { self.messenger.send(\"Urgent warning: You've used up over 90% of your quota!\"); } else if percentage_of_max >= 1.0 { self.messenger.send(\"Error: You are over your quota!\"); } }\n} Listing 15-20: 어떤 값이 최대값에 얼마나 근접하는지를 추적하고 특정 수준에 값이 있으면 경고해주는 라이브러리 이 코드에서 한가지 중요한 부분은 Messenger 트레잇이 self에 대한 불변 참조자와 메세지의 텍스트를 인자로 갖는 send라는 이름의 하나의 메소드를 갖고 있다는 것입니다. 이는 우리의 목 객체가 가질 필요가 있는 인터페이스입니다. 그 외에 중요한 부분은 우리가 LimitTracker 상의 set_value 메소드의 동작을 테스트하고 싶어한다는 점입니다. 우리는 value 파라미터에 대해에 어떤 것을 넘길지 바꿀 수 있지만, set_value는 우리가 단언을 하기 위한 어떤 것도 반환하지 않습니다. 우리는 Messenger 트레잇을 구현한 무언가와 max에 대한 특정값과 함께 LimitTracker를 만든다면, value에 대해 다른 숫자들을 넘겼을 때 메신저가 적합한 메세지를 보낸다고 말하고 싶습니다. 우리는 send를 호출했을 때 메일이나 텍스트 메세지를 보내는 대신 보냈다고 언급하는 메세지만 추적할 목 객체가 필요합니다. 우리는 목 객체의 새로운 인스턴스를 만들고, 이 목 객체를 사용하는 LimitTracker를 만들고, LimitTracker 상의 set_value 메소드를 호출하고, 그 다음 목 객체는 우리가 기대했던 메세지를 가지고 있는지를 검사할 수 있습니다. Listing 15-21은 바로 이런 일을 하지만 빌림 검사기가 허용하지는 않을 목 객체 구현 시도를 보여주고 있습니다: Filename: src/lib.rs #[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} Listing 15-21: 빌림 검사기가 허용하지 않는 MockMessenger 구현 시도 이 테스트 코드는 보내질 메세지들을 추적하기 위한 String 값의 Vec인 sent_messages 필드를 갖는 MockMessenger 구조체를 정의하고 있습니다. 우리는 또한 빈 메세지 리스트로 시작하는 새로운 MockMessenger 값을 생성하기 쉽도록 하기 위해 연관 함수 new를 정의하였습니다. 그런 다음에는 MockMessenger를 LimitTracker에 넘겨줄 수 있도록 MockMessenger를 위한 Messenger 트레잇을 구현하였습니다. send 메소드의 정의 부분에서는 파라미터로서 넘겨진 메세지를 가져와서 MockMessenger 내의 sent_messages 리스트에 저장합니다. 테스트 내에서는 max 값의 75 퍼센트 이상의 무언가가 value로 설정되었을 때 LimitTracker는 어떤 메세지를 듣는지를 테스트하고 있습니다. 먼저 우리는 새로운 MockMessenger를 만드는데, 이는 비어있는 메시지 리스트로 시작할 것입니다. 그 다음에는 새로운 LimitTracker를 만들고 여기에 새로운 MockMessenger의 참조자와 max값 100을 파라미터로 넘깁니다. 우리는 LimitTracker 상의 set_value 메소드를 80 값으로 호출하였습니다. 그 다음 우리는 MockMessenger가 추적하고 있는 메세지 리스트가 이제 한 개의 메세지를 가지고 있는지를 검사합니다. 하지만, 아래에서 보는 것과 같이 이 테스트에 한가지 문제점이 있습니다: error[E0596]: cannot borrow immutable field `self.sent_messages` as mutable --> src/lib.rs:52:13 |\n51 | fn send(&self, message: &str) { | ----- use `&mut self` here to make mutable\n52 | self.sent_messages.push(String::from(message)); | ^^^^^^^^^^^^^^^^^^ cannot mutably borrow immutable field 우리는 메세지를 추적하기 위해 MockMessenger를 수정할 수 없는데 그 이유는 send 메소드가 self의 불변 참조자를 파라미터로 갖기 때문입니다. 우리는 또한 에러 메세지로부터 &mut self를 대신 사용하라는 제안도 얻을 수 없는데, 그렇게 되면 send의 시그니처가 Messenger 트레잇의 정의에 있는 시그니처와 일치하지 않을 것이지 때문입니다 (마음 편하게 한번 시도해보고 어떤 에러가 나오는지 보세요). 이는 내부 가변성이 도움을 줄 수 있는 상황입니다! 우리는 sent_messages를 RefCell 내에 저장할 것이고, 그러면 send 메소드는 우리가 보게 되는 메세지를 저장하기 위해 sent_message를 수정할 수 있을 것입니다. Listing 15-22는 이것이 어떤 형태인지를 보여줍니다: Filename: src/lib.rs #[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() { // --snip--\n# let mock_messenger = MockMessenger::new();\n# let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);\n# limit_tracker.set_value(75); assert_eq!(mock_messenger.sent_messages.borrow().len(), 1); }\n} Listing 15-22: RefCell를 사용하여 바깥쪽에서는 불변으로 간주되는 동안 내부의 값을 변경하기 sent_messages 필드는 이제 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!을 일으킬 것입니다. Listing 15-23은 Listing 15-22의 send 구현의 수정을 보여줍니다. 우리는 RefCell가 런타임에 두개의 활성화된 가변 빌림을 같은 스코프에 만드는 일을 하는 것을 막아주는 것을 보여주기 위해서 의도적으로 그런 시도를 하는 중입니다: Filename: src/lib.rs 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} Listing 15-23: RefCell이 패닉을 일으킬 것을 보기 위한 같은 스코프 내에 두 개의 가변 참조자 만들기 우리는 borrow_mut로부터 반환된 RefMut 스마트 포인터를 위한 one_borrow 변수를 만들었습니다. 그런 다음 또다른 가변 빌림을 같은 방식으로 two_borrow 변수에 만들어 넣었습니다. 이는 같은 스코프에 두개의 가변 참조자를 만드는데, 이는 허용되지 않습니다. 우리가 우리의 라이브러리를 위한 테스트를 실행할 때, Listing 15-23의 코드는 어떠한 에러 없이 컴파일될 것이지만, 테스트는 실패할 것입니다: ---- tests::it_sends_an_over_75_percent_warning_message stdout ---- thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at 'already borrowed: BorrowMutError', src/libcore/result.rs:906:4\nnote: Run with `RUST_BACKTRACE=1` for a backtrace. 코드가 already borrowed: BorrowMutError라는 메세지와 함께 패닉을 일으켰음을 주목하세요. 이것이 바로 RefCell가 런타임에 빌림 규칙의 위반을 다루는 방법입니다. 빌림 에러를 컴파일 타임보다 런타임에 잡는다는 것은 개발 과정 이후에 우리 코드의 실수를 발견할 것이란 의미이고, 심지어는 우리 코드가 프로덕션으로 배포될 때 까지도 발견되지 않을 가능성도 있습니다. 또한, 우리 코드는 컴파일 타임 대신 런타임에 빌림을 추적하는 결과로서 약간의 런타임 성능 페널티를 초래할 것입니다. 그러나, RefCell를 이용하는 것은 우리가 오직 불변 값만 허용하는 콘텍스트 내에서 그것이 본 메세지를 추적하기 위해서 스스로를 변경할 수 있는 목 객체를 작성하도록 해줍니다. 우리는 일반적인 참조자가 우리에게 제공하는 것보다 더 많은 기능을 얻기 위해서 트레이드 오프에도 불구하고 RefCell를 이용할 수 있습니다.","breadcrumbs":"스마트 포인터 » RefCell와 내부 가변성 패턴 » 내부 가변성: 불변값에 대한 가변 빌림","id":"278","title":"내부 가변성: 불변값에 대한 가변 빌림"},"279":{"body":"RefCell를 사용하는 일반적인 방법은 Rc와 함께 조합하는 것입니다. Rc이 어떤 데이터에 대해 복수의 소유자를 허용하지만, 그 데이터에 대한 불변 접근만 제공하는 것을 상기하세요. 만일 우리가 RefCell을 들고 있는 Rc를 갖는다면, 우리가 변경 가능 하면서 복수의 소유자를 갖는 값을 가질 수 있습니다! 예를 들면, Listing 15-18에서 우리가 어떤 리스트의 소유권을 공유하는 여러 개의 리스트를 가질 수 있도록 하기 위해 Rc를 사용했던 cons 리스트 예제를 상기해보세요. Rc가 오직 불변의 값만을 가질 수 있기 때문에, 우리가 이들을 일단 만들면 리스트 안의 값들을 변경하는 것은 불가능했습니다. 이 리스트 안의 값을 변경하는 능력을 얻기 위해서 RefCell을 추가해 봅시다. Listing 15-24는 Cons 정의 내에 RefCell를 사용함으로써 우리가 모든 리스트 내에 저장된 값을 변경할 수 있음을 보여줍니다: Filename: src/main.rs #[derive(Debug)]\nenum List { Cons(Rc>, Rc), Nil,\n} use List::{Cons, Nil};\nuse std::rc::Rc;\nuse std::cell::RefCell; 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(6)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a)); *value.borrow_mut() += 10; println!(\"a after = {:?}\", a); println!(\"b after = {:?}\", b); println!(\"c after = {:?}\", c);\n} Listing 15-24: Rc>을 사용하여 변경 가능한 List 생성하기 우리는 Rc>의 인스턴스인 값을 생성하고 value라는 이름의 변수 안에 이를 저장하여 나중에 이를 직접 접근할 수 있게 했습니다. 그런 다음 우리는 value를 가지고 있는 Cons variant와 함께 a에다 List를 생성하였습니다. value에서 a로 소유권이 이전되거나 value로부터 빌린 a 보다는 a와 value 둘다 내부의 5 값에 대한 소유권을 얻기 위해서는 value를 클론할 필요가 있습니다. 리스트 a는 Rc로 감싸져서 우리가 b와 c 리스트를 만들때, 이 리스트들은 둘다 a를 참조할 수 있는데, 이는 Listing 15-18에서 해본 것입니다. a, b, 그리고 c 리스트를 생성한 이후, 우리는 value 내의 값에 10을 더했습니다. value 상의 borrow_mut를 호출함으로써 수행되었는데, 이는 내부의 RefCell값을 가리키는 Rc를 역참조하기 위해서 우리가 5장에서 논의했던 자동 역참조 기능을 사용한 것입니다 (“-> 연산자는 어디로 갔나요?”절을 보세요). borrow_mut 메소드는 RefMut 스마트 포인터를 반환하고, 우리는 여기에 역참조 연산자를 사용한 다음 내부 값을 변경합니다. a, b, 그리고 c를 출력할때, 우리는 이 리스트들이 모두 5가 아니라 변경된 값 15를 가지고 있는 것을 볼 수 있습니다: a after = Cons(RefCell { value: 15 }, Nil)\nb after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))\nc after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil)) 이 기술은 매우 깔끔합니다! RefCell을 이용함으로써, 우리는 표면상으로는 불변인 List를 갖고 있습니다. 하지만 우리는 내부 가변성 접근을 제공하여 우리가 원할때 데이터를 변경시킬 수 있는 RefCell 내의 메소드를 사용할 수 있습니다. 빌림 규칙의 런타임 검사는 데이터 레이스로부터 우리를 지켜주고, 우리 데이터 구조의 이러한 유연성을 위해서 약간의 속도를 맞거래하는 것이 때때로 가치있습니다. 표준 라이브러리는 내부 가변성을 제공하는 다른 타입을 가지고 있는데, 이를 테면 Cell는 내부 값의 참조자를 주는 대신 값이 복사되어 Cell 밖으로 나오는 점만 제외하면 비슷합니다. 또한 Mutex가 있는데, 이는 스레드들을 건너가며 사용해도 안전한 내부 가변성을 제공합니다; 이것의 사용법은 16장에서 다룰 것입니다. 이 타입들의 차이점에 대해 더 자세히 알고 싶다면 표준 라이브러리 문서를 참조하세요.","breadcrumbs":"스마트 포인터 » RefCell와 내부 가변성 패턴 » Rc와 RefCell를 조합하여 가변 데이터의 복수 소유자 만들기","id":"279","title":"Rc와 RefCell를 조합하여 가변 데이터의 복수 소유자 만들기"},"28":{"body":"여러분의 프로젝트가 마침내 배포(릴리즈)를 위한 준비가 되었다면, cargo build --release를 사용하여 최적화와 함께 이를 컴파일할 수 있습니다. 이 커맨드는 target/debug 대신 target/release 에 실행파일을 생성할 것입니다. 최적화는 여러분의 러스트 코드를 더 빠르게 만들어주지만, 최적화를 켜는 것은 여러분의 프로그램을 컴파일하는데 드는 시간을 길게 할 것입니다: 이것이 바로 두 개의 서로 다른 프로파일이 있는 이유입니다: 하나는 여러분이 빠르게 그리고 자주 다시 빌드하기를 원하는 개발용, 그리고 다른 하나는 반복적으로 다시 빌드를 할 필요 없고 가능한 빠르게 실행되어 여러분이 사용자들에게 제공할 최종 프로그램을 빌드하기 위한 용도입니다. 만일 여러분이 코드의 실행 시간을 벤치마킹 중이라면, cargo build --release를 실행하고 target/release 의 실행파일을 가지고 밴치마킹하고 있음을 확인하세요.","breadcrumbs":"시작하기 » Hello, Cargo! » 릴리즈 빌드","id":"28","title":"릴리즈 빌드"},"280":{"body":"러스트의 메모리 안정성 보장은 ( 메모리 릭 (memory leak) 이라고도 알려져 있는) 뜻하지 않게 해제되지 않는 메모리를 생성하기 힘들게 하지만, 그게 불가능한 것은 아닙니다. 메모리 릭을 완전히 방지하는 것은 컴파일 타임에 데이터 레이스를 허용하지 않는 것과 마찬가지로 러스트가 보장하는 것들 중 하나가 아닌데, 이는 메모리 릭도 러스트에서는 메모리 안정성에 포함됨을 의미합니다. 러스트가 Rc 및 RefCell를 사용하여 메모리 릭을 허용하는 것을 우리는 알 수 있습니다: 즉 아이템들이 서로를 순환 참조하는 참조자를 만드는 것이 가능합니다. 이는 메모리 릭을 발생시키는데, 그 이유는 순환 고리 안의 각 아이템들의 참조 카운트는 결코 0이 되지 않을 것이고, 그러므로 값들은 버려지지 않을 것이기 때문입니다.","breadcrumbs":"스마트 포인터 » 순환 참조를 만드는 것과 메모리 누수는 안전한 것에 해당됩니다 » 순환 참조는 메모리 릭을 발생시킬 수 있습니다","id":"280","title":"순환 참조는 메모리 릭을 발생시킬 수 있습니다"},"281":{"body":"Listing 15-25의 List 열거형과 tail 메소드 정의를 가지고서 어떻게 순환 참조가 생길 수 있고, 이를 어떻게 방지하는지 알아봅시다: Filename: src/main.rs # fn main() {}\nuse std::rc::Rc;\nuse std::cell::RefCell;\nuse List::{Cons, Nil}; #[derive(Debug)]\nenum List { Cons(i32, RefCell>), Nil,\n} impl List { fn tail(&self) -> Option<&RefCell>> { match *self { Cons(_, ref item) => Some(item), Nil => None, } }\n} Listing 15-25: RefCell를 가지고 있어서 Cons variant가 참조하는 것을 변경할 수 있는 cons 리스트 정의 우리는 Listing 15-5의 List 정의의 또다른 변형을 이용하고 있습니다. 이제 Cons variant 내의 두번째 요소는 RefCell>인데, 이는 Listing 15-24에서 우리가 했던 것처럼 i32 값을 변경하는 능력을 갖는 대신, Cons variant가 가리키고 있는 List 값을 변경하길 원한다는 의미입니다. 또한 Cons variant를 갖고 있다면 두번째 아이템에 접근하기 편하도록 tail 메소드를 추가하고 있습니다. Listing 15-26에서 우리는 Listing 15-25의 정의를 사용하는 main 함수를 추가하고 있습니다. 이 코드는 a에 리스트를 만들고 b에는 a의 리스트를 가리키고 있는 리스트를 만들어 넣었습니다. 그 다음 a의 리스트가 b를 가리키도록 수정하는데, 이것이 순환 참조를 생성합니다. 이 과정 내의 다양한 지점에서 참조 카운트가 얼마인지를 보기 위해 곳곳에 println! 구문들이 있습니다. Filename: src/main.rs # use List::{Cons, Nil};\n# use std::rc::Rc;\n# use std::cell::RefCell;\n# #[derive(Debug)]\n# enum List {\n# Cons(i32, RefCell>),\n# Nil,\n# }\n#\n# impl List {\n# fn tail(&self) -> Option<&RefCell>> {\n# match *self {\n# Cons(_, ref item) => Some(item),\n# Nil => None,\n# }\n# }\n# }\n#\nfn 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} Listing 15-26: 두 개의 List 값이 서로를 가리키는 순환 참조 생성하기 우리는 초기값 리스트 5, Nil를 가진 List 값을 갖는 Rc 인스턴스를 만들어 a 변수에 넣었습니다. 그런 다음 값 10과 a의 리스트를 가리키는 또다른 List 값을 갖는 Rc 인스턴스를 만들어서 b 변수에 넣었습니다. 우리는 a를 수정하여 이것이 Nil 대신 b를 가리키도록 하였습니다. a 내의 RefCell>에 대한 참조자를 얻어오기 위해 tail 메소드를 사용했는데, 이 참조자는 link 변수에 집어넣습니다. 그런 다음 RefCell>의 borrow_mut 메소드를 사용하여 Nil 값을 가지고 있는Rc 내부의 값을 b의 Rc로 바꾸었습니다. 지금 잠깐동안 마지막 println! 문이 들어가지 않도록 주석처리하고 이 코드를 실행시킬 때, 아래와 같은 출력을 얻을 것입니다: a 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를 먼저 버리는 시도를 할 것인데, 이는 a와 b의 각각의 Rc 인스턴스 내의 카운트를 1로 줄일 것입니다. 하지만 a가 여전히 b 내에 있는 Rc를 참조하는 상태기 때문에, 이 Rc는 0이 아니라 1의 카운트를 갖게 되고, 따라서 Rc가 힙에 가지고 있는 메모리는 버려지지 않을 것입니다. 그 메모리는 참조 카운트 1을 가진 채로 영원히 그 자리에 그냥 있을 것입니다. 이러한 순환 참조를 시각화하기 위해 Figure 15-4의 다이어그램을 만들었습니다. Figure 15-4: 리스트 a와 b가 서로를 가리키고 있는 순환 참조 만일 여러분이 마지막 println!의 주석을 해제하고 프로그램을 실행해보면, 러스트는 a를 가리키고 있는 b를 가리키고 있는 a를 가리키고 있는... 과 같은 식으로 스택 오버플로우가 날 때까지 이 순환을 출력하려 할 것입니다. 이 경우, 우리가 순환 참조를 만든 직후, 프로그램은 종료됩니다. 위의 순환의 결과는 그렇게까지 심각하지는 않습니다. 하지만, 만일 좀더 복잡한 프로그램이 많은 매모리를 순환 형태로 할당했고 오랫동안 이를 유지했더라면, 프로그램은 필요한 것보다 더 많은 메모리를 사용하게 되고, 사용 가능한 메모리를 동나게 하여 시스템을 멈추게 했을런지도 모릅니다. 순환 참조를 만드는 것은 쉽게 이루어지지는 않지만, 불가능한 것도 아닙니다. 만일 여러분이 Rc 값을 가지고 있는 RefCell 혹은 내부 가변성 및 참조 카운팅 기능이 있는 타입들로 유사한 조합을 사용한다면, 여러분은 순환을 만들지 않음을 보장해야 합니다; 이 순환들을 찾아내는 것을 러스트에 의지할 수는 없습니다. 순환 참조를 만드는 것은 여러분이 자동화된 테스트, 코드 리뷰, 그 외 소프트웨어 개발 연습 등을 이용하여 최소화해야 할 프로그램 내의 논리적 버그입니다. 순환 참조를 피하는 또다른 해결책은 여러분의 데이터 구조를 재구성하여 어떤 참조자는 소유권을 갖고 어떤 참조자는 그렇지 않도록 하는 것입니다. 결과적으로 여러분은 몇 개의 소유권 관계와 몇 개의 소유권 없는 관계로 이루어진 순환을 가질 수 있으며, 소유권 관계들만이 값을 버릴지 말지에 관해 영향을 주게 됩니다. Listing 15-25에서 우리는 Cons variant가 언제나 리스트를 소유하기를 원하므로, 데이터 구조를 재구성하는 것은 불가능합니다. 언제 소유권 없는 관계가 순환 참조를 방지하는 적절한 방법이 되는 때인지를 알기 위해서 부모 노드와 자식 노드로 구성된 그래프를 이용하는 예제를 살펴봅시다.","breadcrumbs":"스마트 포인터 » 순환 참조를 만드는 것과 메모리 누수는 안전한 것에 해당됩니다 » 순환 참조 만들기","id":"281","title":"순환 참조 만들기"},"282":{"body":"이제까지 우리는 Rc::clone을 호출하는 것이 Rc 인스턴스의 strong_count를 증가시키고, Rc 인스턴스는 이것의 strong_count가 0이 된 경우에만 제거되는 것을 보았습니다. 여러분은 또한 Rc::downgrade를 호출하고 여기에 Rc에 대한 참조자를 넘겨서 Rc 인스턴스 내의 값을 가리키는 약한 참조 (weak reference) 를 만들 수 있습니다. 여러분이 Rc::downgrade를 호출하면, 여러분은 Weak 타입의 스마트 포인터를 얻게 됩니다. Rc 인스턴스의 strong_count를 1 증가시키는 대신, Rc::downgrade는 weak_count를 1 증가시킵니다. Rc 타입은 몇 개의 Weak 참조가 있는지 추적하기 위해서 strong_count와 유사한 방식으로 weak_count를 사용합니다. 차이점은 Rc인스턴스가 제거되기 위해서 weak_count가 0일 필요는 없다는 것입니다. 강한 참조는 여러분이 Rc 인스턴스의 소유권을 공유할 수 있는 방법입니다. 약한 참조는 소유권 관계를 표현하지 않습니다. 이것은 순환 참조를 야기하지 않는데 그 이유는 몇몇의 약한 참조를 포함하는 순환이라도 강한 참조의 카운트가 0이 되고 나면 깨지게 될 것이기 때문입니다. Weak가 참조하고 있는 값이 이미 버려졌을지도 모르기 때문에, Weak가 가리키고 있는 값을 가지고 어떤 일을 하기 위해서는 그 값이 여전히 존재하는지를 반드시 확인해야 합니다. 이를 위해 Weak의 upgrade 메소드를 호출하는데, 이 메소드는 Option>를 반환할 것입니다. 만일 Rc 값이 아직 버려지지 않았다면 여러분은 Some 결과를 얻게 될 것이고 Rc 값이 버려졌다면 None 결과값을 얻게 될 것입니다. upgrade가 Option를 반환하기 때문에, 러스트는 Some의 경우와 None의 경우가 반드시 처리되도록 할 것이고, 따라서 유효하지 않은 포인터는 없을 것입니다. 예제로서 어떤 아이템이 오직 다음 아이템에 대해서만 알고 있는 리스트를 이용하는 것보다는 자식 아이템 그리고 부모 아이템에 대해 모두 알고 있는 아이템을 갖는 트리를 만들어 보겠습니다. 트리 데이터 구조 만들기: 자식 노드를 가진 Node 자신의 자식 노드에 대해 알고 있는 노드를 갖는 트리를 만드는 것으로 시작해 보겠습니다. 우리는 i32값은 물론 자식 Node들의 참조자들 또한 가지고 있는 Node라는 이름의 구조체를 만들 것입니다: Filename: src/main.rs use std::rc::Rc;\nuse std::cell::RefCell; #[derive(Debug)]\nstruct Node { value: i32, children: RefCell>>,\n} 우리는 Node가 자신의 자식들을 소유하기를 원하고, 이 소유권을 공유하여 트리의 각 Node에 직접 접근할 수 있도록 하기를 원합니다. 이를 하기 위해서 Vec 아이템이 Rc 타입의 값이 되도록 정의하였습니다. 또한 우리는 어떤 노드가 다른 노드의 자식이 되도록 수정하기를 원하므로, Vec>를 RefCell로 감싼 children을 갖도록 하였습니다. 그 다음, Listing 15-27에서 보시는 것처럼 이 구조체 정의를 이용하여 3의 값과 자식 노드가 없는 leaf라는 이름의 Node 인스턴스, 그리고 5의 값과 leaf를 자식으로 갖는 branch라는 이름의 인스턴스를 만들도록 하겠습니다: Filename: src/main.rs # use std::rc::Rc;\n# use std::cell::RefCell;\n#\n# #[derive(Debug)]\n# struct Node {\n# value: i32,\n# children: RefCell>>,\n# }\n#\nfn 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} Listing 15-27: 자식이 없는 leaf 노드와 이 leaf를 자식 중 하나로 갖는 branch 노드 만들기 leaf 내의 Rc를 클론하여 이를 branch 내에 저장했는데, 이는 leaf 내의 Node가 이제 두 소유권자를 가지게 되었다는 의미입니다. 우리는 branch.children를 통하여 branch에서부터 leaf까지 접근할 수 있게 되었지만, leaf에서부터 branch로 접근할 방법은 없습니다. 그 이유는 leaf가 branch에 대한 참조자를 가지고 있지 않아서 이들간의 연관성을 알지 못하기 때문입니다. 우리는 leaf로 하여금 branch가 그의 부모임을 알도록 하기를 원합니다. 이걸 다음에 해보겠습니다. 자식으로부터 부모로 가는 참조자 추가하기 자식 노드가 그의 부모를 알도록 만들기 위하여, parent 필드를 우리의 Node 구조체 정의에 추가할 필요가 있습니다. 문제는 parent의 타입이 어떤 것이 되어야 하는지를 결정하는 중에 발생합니다. 이것이 Rc를 담을 수 없음을 우리는 알고 있는데, 그렇게 하게 되면 branch를 가리키고 있는 leaf.parent와 leaf를 가리키고 있는 branch.children을 가지고 있는 순환 참조를 만들게 되며, 이것들의 strong_count값을 결코 0이 안되도록 하는 일을 야기하기 때문입니다. 이 관계들을 다른 방식으로 생각해보면, 부모 노드는 그의 자식들을 소유해야 합니다: 만일 부모 노드가 버려지게 되면, 그의 자식 노드들도 또한 버려져야 합니다. 하지만, 자식은 그의 부모를 소유해서는 안됩니다: 만일 우리가 자식 노드를 버리면, 그 부모는 여전히 존재해야 합니다. 이것이 바로 약한 참조를 위한 경우에 해당됩니다! 따라서 Rc 대신 Weak를 이용하여, 특별히 RefCell>를 이용하여 parent의 타입을 만들겠습니다. 이제 우리의 Node 구조체 정의는 아래와 같이 생기게 되었습니다: Filename: src/main.rs use std::rc::{Rc, Weak};\nuse std::cell::RefCell; #[derive(Debug)]\nstruct Node { value: i32, parent: RefCell>, children: RefCell>>,\n} 노드는 그의 부모 노드를 참조할 수 있게 되겠지만 그 부모를 소유하지는 않습니다. Listing 15-28에서, 우리는 이 새로운 정의를 사용하도록 main을 업데이트하여 leaf 노드가 그의 부모인 branch를 참조할 수 있는 방법을 갖도록 할 것입니다: Filename: src/main.rs # use std::rc::{Rc, Weak};\n# use std::cell::RefCell;\n#\n# #[derive(Debug)]\n# struct Node {\n# value: i32,\n# parent: RefCell>,\n# children: RefCell>>,\n# }\n#\nfn 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} Listing 15-28: 부모 노드 branch의 약한 참조를 갖는 leaf 노드 leaf 노드를 만드는 것은 parent 필드를 제외하고는 Listing 15-27에서 leaf 노드를 만드는 방법과 비슷해 보입니다: 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 variant를 얻게될 것입니다: 이제 leaf는 그의 부모에 접근할 수 있습니다! leaf를 출력할 때, 우리는 또한 Listing 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가 만들어질 때와 그 다음 스코프 밖으로 벗어났을 때 어떤일이 생기는지 알 수 있습니다. 수정본은 Listing 15-29와 같습니다: Filename: src/main.rs # use std::rc::{Rc, Weak};\n# use std::cell::RefCell;\n#\n# #[derive(Debug)]\n# struct Node {\n# value: i32,\n# parent: RefCell>,\n# children: RefCell>>,\n# }\n#\nfn 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} Listing 15-29: branch를 내부 스코프에서 만들고 강한 참조 및 약한 참조 카운트를 시험하기 leaf가 생성된 다음, 이것의 Rc는 강한 참조 카운트 1개와 약한 참조 카운트 0개를 갖습니다. 내부 스코프에서 branch를 만들고 leaf와 연관짓게 되는데, 이때 우리가 카운트를 출력하면 branch의 Rc는 강한 참조 카운트 1개와 (Weak 클론을 branch.children에 저장해 두었으므로, leaf의 카운트를 출력해보면 강한 참조 카운트는 2개가 되지만, 약한 참조 카운트는 여전히 0개일 것입니다. 내부 스코프가 끝나게 되면, branch는 스코프 밖으로 벗어나게 되며 Rc의 강한 참조 카운트는 0으로 줄어들게 되므로, 이것의 Node는 버려지게 됩니다. leaf.parent로부터 발생된 1개의 약한 참조 카운트는 Node가 버려질지 말지에 대한 어떠한 영향도 주지 않으므로, 아무런 메모리 릭도 발생하지 않았습니다! 만일 우리가 이 스코프의 끝 이후에 leaf의 부모에 접근하기를 시도한다면, 우리는 다시 None을 얻게 될 것입니다. 프로그램의 끝 부분에서, leaf의 Rc는 강한 참조 카운트 1개와 약한 참조 카운트 0개를 갖고 있는데, 그 이유는 leaf 변수가 이제 다시 Rc에 대한 유일한 참조자이기 때문입니다. 참조 카운트들과 버리는 값들을 관리하는 모든 로직은 Rc와 Weak, 그리고 이들의 Drop 트레잇에 대한 구현부에 만들어져 있습니다. 자식으로부터 부모로의 관계가 Node의 정의 내에서 Weak 참조자로 되어야 함을 특정함으로서, 여러분은 순환 참조와 메모리 릭을 만들지 않고도 자식 노드를 가리키는 부모 노드 혹은 그 반대의 것을 가지게 될 수 있습니다.","breadcrumbs":"스마트 포인터 » 순환 참조를 만드는 것과 메모리 누수는 안전한 것에 해당됩니다 » 참조 순환 방지하기: Rc를 Weak로 바꾸기","id":"282","title":"참조 순환 방지하기: Rc를 Weak로 바꾸기"},"283":{"body":"이번 장에서는 러스트가 일반적인 참조자를 가지고 기본적으로 보장하는 것들과는 다른 보장 및 트레이드 오프를 만들어내기 위해 스마트 포인터를 사용하는 방법을 다루었습니다. Box 타입은 알려진 크기를 갖고 있고 힙에 할당된 데이터를 가리킵니다. Rc 타입은 힙에 있는 데이터에 대한 참조자의 개수를 추적하여 그 데이터가 여러 개의 소유자들을 가질 수 있도록 합니다. 내부 가변성을 갖춘 RefCell 타입은 불변 타입을 원하지만 그 타입의 내부 값을 변경하기를 원할 때 사용할 수 있는 타입을 제공합니다; 이는 또한 컴파일 타임 대신 런타임에 빌림 규칙을 따르도록 강제합니다. 또한 Deref 및 Drop 트레잇을 다루었는데, 이는 스마트 포인터의 수많은 기능을 활성화해줍니다. 우리는 메모리 릭을 발생시킬 수 있는 순환 참조에 대한 것과 Weak을 이용하여 이들을 방지하는 방법도 탐구하였습니다. 만일 이번 장이 여러분의 흥미를 언짢게 하고 여러분이 직접 여러분만의 스마트 포인터를 구현하기를 원한다면, “러스토노미콘” 에서 더 유용한 정보를 확인하세요. 다음으로 우리는 러스트의 동시성에 대해 이야기해볼 것입니다. 여러분은 심지어 몇 개의 새로운 스마트 포인터에 대해서도 배우게 될 것입니다.","breadcrumbs":"스마트 포인터 » 순환 참조를 만드는 것과 메모리 누수는 안전한 것에 해당됩니다 » 정리","id":"283","title":"정리"},"284":{"body":"안전하고 효율적으로 동시성 프로그래밍을 다루는 것은 러스트의 또다른 주요 목표들 중 하나입니다. 동시성 프로그래밍 (concurrent programming) , 즉 프로그램의 서로 다른 부분이 독립적으로 실행되는 것과, 병렬 프로그래밍 (parallel programming) , 즉 프로그램의 서로 다른 부분이 동시에 실행되는 것은 더 많은 컴퓨터들이 여러 개의 프로세서로 이점을 얻음에 따라 그 중요성이 증가하고 있습니다. 역사적으로, 이러한 맥락에서 프로그래밍하는 것은 어렵고 에러를 내기 쉬웠습니다: 러스트는 이를 바꾸기를 바라고 있습니다. 초기에 러스트 팀은 메모리 안전을 보장하는 것과 동시성 문제를 방지하는 것은 다른 방법으로 해결되야 하는 별개의 도전 과제라고 생각했습니다. 시간이 흘러 러스트 팀은 소유권과 타입 시스템이 메모리 안전성 및 동시성 문제를 관리하는 것을 돕기 위한 강력한 도구들의 집합이라는 사실을 발견했습니다! 소유권과 타입 검사를 지렛대 삼아서 많은 동시성 에러들이 러스트 내에서 런타임 에러가 아닌 컴파일 타임 에러가 되었습니다. 그러므로, 런타임 동시성 버그가 발생하는 정확한 환경을 재현하는 시도를 하는데 여러분이 수많은 시간을 소비하도록 만들지 않고, 부정확한 코드는 컴파일 되기를 거부하고 문제점을 설명하는 에러를 보여줄 것입니다. 결과적으로 여러분은 잠재적으로 프로덕션에 배포된 이후가 아니라 작업을 하는 동안에 여러분의 코드를 고칠 수 있습니다. 우리는 러스트의 이러한 측면을 겁없는 동시성 (fearless concurrency) 이라고 별명지어 주었습니다. 겁없는 동시성은 여러분이 감지하기 힘든 버그 없고 새로운 버그 생성 없이 리팩토링하기 쉬운 코드를 작성하도록 해줍니다. 노트: 단순함을 목적으로 우리는 많은 수의 문제들을 더 정교하게 동시성 및/또는 병렬성 이라고 말하기 보다는 그냥 동시성 에 대한 문제로서 참고할 것입니다. 만일 이 책이 동시성 및/또는 병렬성에 대한 것이었다면, 우리는 더 정확하게 말했을 것입니다. 이번 장에서는 우리가 동시성 이라고 말할 때마다 마음속으로 동시성 및/또는 병렬성 을 대입해 주세요.. 많은 언어들은 동시성 문제를 다루기 위해 그들이 제공하는 해결책에 대해 독단적입니다. 예를 들어, Erlang은 메세지-패싱 (message-passing) 동시성을 위한 우아한 기능을 가지고 있지만 스레드 간에 상태를 공유하기 위한 이해하기 힘든 방법만을 가지고 있습니다. 가능한 해결책 중 일부만을 제공하는 것은 고수준의 언어를 위한 타당한 전략인데, 이는 고수준의 언어가 추상화를 얻기 위해 몇몇의 제어권을 포기함으로써 얻는 이득을 약속하기 때문입니다. 하지만 저수준의 언어는 어떠한 주어진 상황 내에서 최고의 성능을 갖는 해결책을 제공하도록 기대받고 있고 하드웨어에 대하여 더 적은 추상화를 갖습니다. 그러므로, 러스트는 여러분의 상황과 요구사항에 적합한 방법이 무엇이든간에 문제를 모델링하기 위한 다양한 도구들을 제시합니다. 이번 장에서 다루게 될 주제들입니다: 여러 조각의 코드를 동시에 실행시키기 위해 스레드를 생성하는 방법 체널들이 스레드 간에 메세지를 보내는 메세지-패싱 동시성 여러 스레드가 어떤 동일한 데이터를 접근할 수 있는 상태-공유 (shared-state) 동시성 표준 라이브러리가 제공하는 타입 뿐만 아니라 러스트의 동시성 보장을 사용자 정의 타입으로 확장하는 Sync와 Send 트레잇","breadcrumbs":"겁없는 동시성 » 겁없는 동시성","id":"284","title":"겁없는 동시성"},"285":{"body":"대부분의 요즘 운영 체제에서, 실행되는 프로그램의 코드는 프로세스 내에서 실행되고, 운영 체제는 한번에 여러 개의 프로세스들을 관리합니다. 여러분의 프로그램 내에서도 동시에 실행되는 독립적인 부분들을 가질 수 있습니다. 이러한 독립적인 부분들을 실행하는 기능을 스레드 라고 부릅니다. 여러분의 프로그램 내에 계산 부분을 여러 개의 스레드로 쪼개는 것은 프로그램이 동시에 여러 개의 일을 할 수 있기 때문에 성능을 향상시킬 수 있지만, 프로그램을 복잡하게 만들기도 합니다. 스레드가 동시에 실행될 수 있기 때문에, 다른 스레드 상에서 실행될 여러분의 코드 조각들의 실행 순서에 대한 내재적인 보장이 없습니다. 이는 다음과 같은 문제들을 야기할 수 있습니다: 여러 스레드들이 일관성 없는 순서로 데이터 혹은 리소스에 접근하게 되는, 경쟁 조건 (race condition) 두 스레드가 서로 상대방 스레드가 가지고 있는 리소스의 사용을 끝내길 기다려서 양쪽 스레드 모두 계속 실행되는 것을 막아버리는, 데드록 (deadlock) 특정한 상황에서만 발생되어 재현하기와 안정적으로 수정하기가 힘든 버그들 러스트는 스레드 사용의 부정적인 효과를 줄이려는 시도를 하지만, 다중 스레드 콘텍스트 내에서의 프로그래밍은 여전히 신중하게 생각해야 하고 단일 스레드 내에서 실행되는 프로그램의 것과는 다른 코드 구조가 필요합니다. 프로그래밍 언어들은 몇가지 다른 방식으로 스레드를 구현합니다. 많은 운영 체제들이 새로운 스레드를 만들기 위한 API를 제공합니다. 언어가 운영 체제의 API를 호출하여 스레드를 만드는 이러한 구조는 때때로 1:1 이라 불리는데, 이는 하나의 운영 체제 스레드가 하나의 언어 스레드에 대응된다는 의미입니다. 많은 프로그래밍 언어들은 그들만의 특별한 스레드 구현을 제공합니다. 프로그래밍 언어가 제공하는 스레드는 그린 (green) 스레드라고 알려져 있으며, 이러한 그린 스레드를 사용하는 언어들은 다른 숫자의 운영 체제 스레드로 구성된 콘텍스트 내에서 그린 스레드들을 실행할 것입니다. 이러한 이유로 인하여 그린 스레드 구조는 M:N 이라고 불립니다: M 개의 그린 스레드가 N 개의 시스템 스레드에 대응되는데, 여기서 M과 N은 굳이 동일한 숫자가 아니어도 됩니다. 각각의 구조는 고유한 장점과 트레이드 오프를 가지고 있으며, 러스트에게 있어 가장 중요한 트레이드 오프는 런타임 지원입니다. 런타임 은 혼동하기 쉬운 용어이고 맥락에 따라 다른 의미를 가질 수 있습니다. 이 글의 맥락에서 런타임 이라 하는 것은 언어에 의해 모든 바이너리 내에 포함되는 코드를 의미합니다. 이 코드는 언어에 따라 크거나 작을 수 있지만, 모든 어셈블리 아닌 언어들은 어느 정도 크기의 런타임 코드를 가지게 될 것입니다. 이러한 이유로 인하여, 흔히 사람들이 “런타임이 없다”라고 말할 때는, 종종 “런타임이 작다”는 것을 의미하는 것입니다. 런타임이 작을 수록 더 적은 기능을 갖지만 더 작아진 바이너리로 인해 얻어지는 장점을 갖는데, 이는 더 큰 콘텍스트 내에서 다른 언어들과 조합하기 쉬워진다는 점입니다. 비록 많은 언어들이 더 많은 기능을 위하여 런타임 크기를 늘리는 거래를 수락하더라도, 러스트는 거의 런타임이 없을 필요가 있고 성능을 관리하기 위해 C를 호출하는 것에 대해 타협할 수 없습니다. 그린 스레드 M:N 구조는 스레드들을 관리하기 위해 더 큰 언어 런타임이 필요합니다. 그런 이유로 러스트 표준 라이브러리는 오직 1:1 스레드 구현만 제공합니다. 러스트가 이러한 저수준 언어이기 때문에, 여러분이 예를 들어 어떤 스레드를 언제 실행시킬지에 대한 더 많은 제어권과 콘텍스트 교환(context switching)의 더 저렴한 비용 같은 관점을 위해 오버헤드와 맞바꾸겠다면 M:N 스레드를 구현한 크레이트도 있습니다. 이제 러스트에서의 스레드를 정의했으니, 표준 라이브러리가 제공하는 스레드 관련 API를 어떻게 사용하는지를 탐구해봅시다.","breadcrumbs":"겁없는 동시성 » 스레드 » 스레드를 이용하여 코드를 동시에 실행하기","id":"285","title":"스레드를 이용하여 코드를 동시에 실행하기"},"286":{"body":"새로운 스레드를 생성하기 위해서는 thread::spawn 함수를 호출하고 여기에 우리가 새로운 스레드 내에서 실행하기를 원하는 코드가 담겨 있는 클로저를 넘깁니다 (클로저에 대해서는 13장에서 다뤘습니다). Listing 16-1의 예제는 메인 스레드에서 어떤 텍스트를 출력하고 새로운 스레드에서는 다른 텍스트를 출력합니다: Filename: 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} Listing 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":"286","title":"spawn으로 새로운 스레드 생성하기"},"287":{"body":"Listing 16-1의 코드는 대개의 경우 메인 스레드가 종료되는 이유로 생성된 스레드가 조기에 멈출 뿐만 아니라, 생성된 스레드가 모든 코드를 실행할 것임을 보장해 줄수도 없습니다. 그 이유는 스레드들이 실행되는 순서에 대한 보장이 없기 때문입니다! 생성된 스레드가 실행되지 않거나, 전부 실행되지 않는 문제는 thread::spawn의 반환값을 변수에 저장함으로서 해결할 수 있습니다. thread::spawn의 반환 타입은 JoinHandle입니다. JoinHandle은 이것이 가지고 있는 join 메소드를 호출했을 때 그 스레드가 끝날때까지 기다리는 소유된 값입니다. Listing 16-2는 어떤식으로 우리가 Listing 16-1에서 만들었던 스레드의 JoinHandle을 사용하고 join을 호출하여 main이 끝나기 전에 생성된 스레드가 종료되는 것을 확실하게 하는지를 보여줍니다: Filename: 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} Listing 16-2: 스레드가 완전시 실행되는 것을 보장하기 위해 thread::spawn으로부터 JoinHandle을 저장하기 핸들에 대해 join을 호출하는 것은 핸들에 대한 스레드가 종료될 때까지 현재 실행중인 스레드를 블록합니다. 스레드를 블록 (Block) 한다는 것은 그 스레드의 작업을 수행하거나 종료되는 것이 방지된다는 의미입니다. 우리가 메인 스레드의 for 루프 이후에 join의 호출을 넣었으므로, Listing 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()을 이동시키면 어떤 일이 생기는지 봅시다: Filename: 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":"287","title":"join 핸들을 사용하여 모든 스레드들이 끝날때까지 기다리기"},"288":{"body":"move 클로저는 thread::spawn와 함께 자주 사용되는데 그 이유는 이것이 여러분으로 하여금 어떤 스레드의 데이터를 다른 스레드 내에서 사용하도록 해주기 때문입니다. 13장에서는 클로저의 파라미터 목록 앞에 move 키워드를 이용하여 클로저가 그 환경에서 사용하는 값의 소유권을 강제로 갖게 한다고 언급했습니다. 이 기술은 값의 소유권을 한 스레드에서 다른 스레드로 이전하기 위해 새로운 스레드를 생성할 때 특히 유용합니다. Listing 16-1에서 우리가 thread::spawn에 넘기는 클로저는 아무런 인자도 갖지 갖지 않는다는 점을 주목하세요: 생성된 스레드의 코드 내에서는 메인 스레드로부터 온 어떤 데이터도 이용하고 있지 않습니다. 메인 스레드로부터의 데이터를 생성된 스레드 내에서 사용하기 위해서는 생성된 스레드의 클로저가 필요로 하는 값을 캡처해야 합니다. Listing 16-3은 메인 스레드에서 백터 생성하여 이를 생성된 스레드 내에서 사용하는 시도를 보여주고 있습니다. 그러나 잠시 후에 보시게 될 것처럼 아직은 동작하지 않습니다. Filename: 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} Listing 16-3: 메인 스레드에서 생성된 벡터를 다른 스레드 내에서 사용하는 시도 클로저는 v를 사용하므로, v는 캡처되어 클로저의 환경의 일부가 됩니다. thread::spawn이 이 클로저를 새로운 스레드 내에서 실행하므로, v는 새로운 스레드 내에서 접근 가능해야 합니다. 하지만 이 예제를 컴파일하면 아래와 같은 에러를 얻게 됩니다: error[E0373]: closure may outlive the current function, but it borrows `v`,\nwhich 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 |\nhelp: to force the closure to take ownership of `v` (and any other referenced\nvariables), use the `move` keyword |\n6 | let handle = thread::spawn(move || { | ^^^^^^^ 러스트는 v를 어떻게 캡처하는지 추론하고 , println!이 v의 참조자만 필요로 하기 때문에, 클로저는 v를 빌리는 시도를 합니다. 하지만 문제가 있습니다: 러스트는 생성된 스레드가 얼마나 오랫동안 실행될지 말해줄 수 없으므로, v에 대한 참조자가 항상 유효할 것인지를 알지 못합니다. Listing 16-4는 유효하지 않게 된 v의 참조자를 갖게 될 가능성이 더 높은 시나리오를 제공합니다: Filename: 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); // oh no! handle.join().unwrap();\n} Listing 16-4: v를 드롭하는 메인 스레드로부터 v에 대한 참조자를 캡처하는 시도를 하는 클로저를 갖는 스레드 만약 우리가 이 코드를 실행할 수 있다면, 생성된 스레드가 전혀 실행되지 않고 즉시 백그라운드에 들어갈 가능성이 있습니다. 생성된 스레드는 내부에 v의 참조자를 가지고 있지만, 메인 스레드는 우리가 15장에서 다루었던 drop 함수를 사용하여 v를 즉시 드롭시킵니다. 그러면 생성된 스레드가 실행되기 시작할 때 v가 더 이상 유효하지 않게 되어, 참조자 또한 유효하지 않게 됩니다. 이런! Listing 16-3의 컴파일 에러를 고치기 위해서는 에러 메세지의 조언을 이용할 수 있습니다: help: to force the closure to take ownership of `v` (and any other referenced\nvariables), use the `move` keyword |\n6 | let handle = thread::spawn(move || { | ^^^^^^^ move 키워드를 클로저 앞에 추가함으로서 우리는 러스트가 값을 빌려와야 된다고 추론하도록 하는 것이 아니라 사용하는 값의 소유권을 강제로 가지도록 합니다. Listing 16-3을 Listing 16-5에서 보이는 것처럼 수정하면 컴파일되어 우리가 원하는 대로 실행됩니다: Filename: 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} Listing 16-5: move 키워드를 사용하여 사용하는 값의 소유권을 클로저가 갖도록 강제하기 메인 스레드에서 drop을 호출하는 Listing 16-4의 코드에서 move 클로저를 이용한다면 어떤 일이 벌어질까요? move가 이 경우도 고칠 수 있을까요? 불행하게도, 아닙니다; Listing 16-4이 시도하고자 하는 것이 다른 이유로 허용되지 않기 때문에 우리는 다음 에러를 얻게 됩니다. 만일 클로저에 move를 추가하면, v를 클로저의 환경으로 이동시킬 것이고, 더이상 메인 스레드에서 이것에 대한 drop 호출을 할 수 없게 됩니다. 대신 우리는 아래와 같은 컴파일 에러를 얻게 됩니다: error[E0382]: use of moved value: `v` --> src/main.rs:10:10 |\n6 | let handle = thread::spawn(move || { | ------- value moved (into closure) here\n...\n10 | drop(v); // oh no! | ^ value used here after move | = note: move occurs because `v` has type `std::vec::Vec`, which does not implement the `Copy` trait 러스트의 소유권 규칙이 다시 한번 우리를 구해주었습니다! Listing 16-3의 코드로부터 에러를 받은 이유는 러스트가 보수적이고 스레드를 위해 v를 단지 빌리려고만 했기 때문이었는데, 이는 메인스레드가 이론적으로 생성된 스레드의 참조자를 무효화할 수 있음을 의미합니다. 러스트에게 v의 소유권을 생성된 스레드로 이동시키라 말해줌으로서, 우리는 러스트에게 메인 스레드가 v를 더 이상 이용하지 않음을 보장하고 있습니다. 만일 우리가 Listing 16-4를 같은 방식으로 바꾸면, 우리가 v를 메인스레드 상에서 사용하고자 할 때 소유권 규칙을 위반하게 됩니다. move 키워드는 러스트의 빌림에 대한 보수적인 기본 기준을 무효화합니다; 즉 우리가 소유권 규칙을 위반하지 않도록 해줍니다. 스레드와 스레드 API에 대한 기본적인 이해를 하고서, 우리가 스레드를 가지고 어떤 것을 할 수 있는지 살펴봅시다.","breadcrumbs":"겁없는 동시성 » 스레드 » 스레드에 move 클로저 사용하기","id":"288","title":"스레드에 move 클로저 사용하기"},"289":{"body":"안전한 동시성을 보장하는 인기 상승중인 접근법 하나는 메세지 패싱 (message passing) 인데, 이는 스레드들 혹은 액터들이 데이터를 담고 있는 메세지를 서로 주고받는 것입니다. Go 언어 문서 의 슬로건에 있는 아이디어는 다음과 같습니다: \"메모리를 공유하는 것으로 통신하지 마세요; 대신, 통신해서 메모리를 공유하세요\" 러스트가 메세지 보내기 방식의 동시성을 달성하기 위해 갖춘 한가지 주요 도구는 채널 (channel) 인데, 이는 러스트의 표준 라이브러리가 구현체를 제공하는 프로그래밍 개념입니다. 프로그래밍에서의 채널은 개울이나 강 같은 물의 통로와 비슷하다고 상상할 수 있습니다. 만일 여러분이 고무 오리나 배 같은 것을 개울에 띄우면, 물길의 끝까지 하류로 여행하게 될 것입니다. 프로그래밍에서의 채널은 둘로 나뉘어져 있습니다: 바로 송신자(transmitter)와 수신자(receiver)입니다. 송신자 측은 여러분이 강에 고무 오리를 띄우는 상류 위치이고, 수신자 측은 하류에 고무 오리가 도달하는 곳입니다. 여러분 코드 중 한 곳에서 여러분이 보내고자 하는 데이터와 함께 송신자의 메소드를 호출하면, 다른 곳에서는 도달한 메세지에 대한 수신 종료를 검사합니다. 송신자 혹은 송신자가 드롭되면 채널이 닫혔다 (closed) 라고 말합니다. 여기서 우리는 값을 생성하여 채널로 내려보내는 한 스레드와, 값을 받아서 이를 출력하는 또다른 스레드를 가지고 있는 프로그램을 만들어볼 것입니다. 우리는 기능을 설명하기 위해서 채널을 사용해 스레드 간에 단순한 값들을 보내게 될 것입니다. 여러분이 이 기술에 익숙해지고 나면, 여러분은 채팅 시스템이나 다수의 스레드가 계산의 일부분을 수행하여 결과를 종합하는 하나의 스레드에 이를 보내는 시스템을 구현하기 위해 채널을 이용할 수 있습니다. 먼저 Listing 16-6에서는 채널을 만들지만 이걸 가지고 아무것도 하지 않을 것입니다. 우리가 채널을 통해 어떤 타입의 값을 보내는지에 대해 러스트에게 말하지 않았기 때문에 아직 컴파일되지 않는다는 점을 주의하세요. Filename: src/main.rs use std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel();\n# tx.send(()).unwrap();\n} Listing 16-6: 채널을 생성하여 두 결과값을 tx와 rx에 할당하기 우리는 mpsc::channel 함수를 사용하여 새로운 채널을 생성합니다; mpsc는 복수 생성자, 단수 소비자 (multiple producer, single consumer) 를 나타냅니다. 짧게 줄이면, 러스트의 표준 라이브러리가 채널을 구현한 방법은 한 채널이 값을 생성하는 복수개의 송신 단말을 가질 수 있지만 값을 소비하는 단 하나의 수신 단말을 가질 수 있음을 의미합니다. 하나의 큰 강으로 함께 흐르는 여러 개울들을 상상해 보세요: 개울 중 어떤 쪽에라도 흘려보낸 모든 것은 끝에 하나의 강에서 끝날 것입니다. 지금은 단일 생성자를 가지고 시작하겠지만, 이 예제가 동작하기 시작하면 여러 생성자를 추가할 것입니다. mpsc::channel 함수는 튜플을 반환하는데, 첫번째 요소는 송신 단말이고 두번째 요소는 수신 단말입니다. tx와 rx라는 약어는 많은 분야에서 각각 송신자 (transmitter) 와 수신자 (receiver) 를 위해 사용하므로, 각각의 단말을 가리키기 위해 그렇게 변수명을 지었습니다. 우리는 튜플을 해체하는 패턴과 함께 let 구문을 사용하는 중입니다; let 구문 내에서의 패턴의 사용과 해체에 대해서는 18장에서 다룰 것입니다. 이런 방식으로 let 구문을 사용하는 것은 mpsc::channel이 반환하는 튜플의 조각들을 추출하는데 편리한 접근법입니다. Listing 16-7에서 보는 바와 같이 송신 단말을 생성된 스레드로 이동시키고 하나의 스트링을 전송하게 하여 생성된 스레드가 메인 스레드와 통신하도록 해봅시다. 이는 강 상류에 고무 오리를 띄우는 것 혹은 한 스레드에서 다른 스레드로 채팅 메세지를 보내는 것과 비슷합니다. Filename: src/main.rs use std::thread;\nuse std::sync::mpsc; fn main() { let (tx, rx) = mpsc::channel(); thread::spawn(move || { let val = String::from(\"hi\"); tx.send(val).unwrap(); });\n} Listing 16-7: tx를 생성된 스레드로 이동시키고 “hi”를 보내기 다시 한번 thread::spawn을 이용하여 새로운 스레드를 생성한 뒤 move를 사용하여 tx를 클로저로 이동시켜 생성된 스레드가 tx를 소유하도록 합니다. 생성된 스레드는 채널을 통해 메세지를 보낼 수 있도록 하기 위해 채널의 송신 단말을 소유할 필요가 있습니다. 송신 단말은 우리가 보내고 싶어하는 값을 취하는 send 메소드를 가집니다. send 메소드는 Result 타입을 반환하므로, 만일 수신 단말이 이미 드롭되어 있고 값을 보내는 곳이 없다면, 송신 연산은 에러를 반환할 것입니다. 이 예제에서는 에러가 나는 경우 패닉을 일으키기 위해 unwrap을 호출하는 중입니다. 그러나 실제 애플리케이션에서는 이를 적절히 다뤄야 할 것입니다: 적절한 에러 처리를 위한 전략을 다시 보려면 9장으로 돌아가세요. Listing 16-8에서 우리는 메인 스레드에 있는 채널의 수신 단말로부터 값을 받을 것입니다. 이는 강의 끝물에서 고무 오리를 건져올리는 것 혹은 채팅 메세지를 받는 것과 비슷합니다. Filename: src/main.rs use std::thread;\nuse std::sync::mpsc; 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} Listing 16-8: 메인 스레드에서 “hi” 값을 받아 출력하기 채널의 수신 단말은 두 개의 유용한 메소드를 가지고 있습니다: recv와 try_recv 입니다. 우리는 수신 (receive) 의 줄임말인 recv를 사용하는 중인데, 이는 메인 스레드의 실행을 블록시키고 채널로부터 값이 보내질 때까지 기다릴 것입니다. 값이 일단 전달되면, recv는 Result 형태로 이를 반환할 것입니다. 채널의 송신 단말이 닫히면, recv는 더 이상 어떤 값도 오지 않을 것이란 신호를 하는 에러를 반환할 것입니다. try_recv 메소드는 블록하지 않는 대신 즉시 Result를 반환합니다: 전달 받은 메세지가 있다면 이를 담고 있는 Ok 값을, 이 시점에서 메세지가 없다면 Err 값을 반환합니다. try_recv를 사용하는 것은 메세지를 기다리는 동안 해야 하는 다른 작업이 있을 때 유용합니다: try_recv을 매번마다 호출하여, 가능한 메세지가 있으면 이를 처리하고, 그렇지 않으면 다음번 검사때까지 잠시동안 다른 일을 하는 루프를 만들 수 있습니다. 이 예제에서는 단순함을 위해 recv를 이용했습니다; 이 메인 스레드에서는 메세지를 기다리는 동안 해야 할 다른 일이 없으므로, 메인 스레드를 블록시키는 것이 적절합니다. Listing 16-8의 코드를 실행하면, 메인 스레드로부터 출력된 값을 보게 될 것입니다: Got: hi 완벽하군요!","breadcrumbs":"겁없는 동시성 » 메세지 패싱 » 메세지 패싱을 사용하여 스레드 간에 데이터 전송하기","id":"289","title":"메세지 패싱을 사용하여 스레드 간에 데이터 전송하기"},"29":{"body":"단순한 프로젝트와 함께 Cargo를 사용하는 것은 그냥 rustc을 이용하는 것에 비해 큰 가치를 제공해주지는 못합니다만, 여러분의 프로그램이 점점 더 복잡해질수록 Cargo는 자신의 가치를 증명할 것입니다. 여러 개의 크레이트들로 구성된 복잡한 프로젝트와 함께라면 Cargo가 빌드를 조직화하도록 하는것이 훨씬 쉽습니다. 비록 hello_cargo 프로젝트가 단순했을지라도, 이 프로젝트는 이제 여러분의 남은 러스트 경력 생활 내에 사용하게될 진짜배기 도구를 사용하였습니다. 사실, 어떤 기존 프로젝트들 상에서 작업을 하기 위해서, 여러분은 Git을 사용하여 코드를 체크 아웃하고 그 프로젝트 디렉토리로 가서 빌드하기 위해 다음 커맨드를 사용할 수 있습니다: $ git clone someurl.com/someproject\n$ cd someproject\n$ cargo build Cargo에 대해 더 많은 정보를 보려면 문서 를 참고하세요.","breadcrumbs":"시작하기 » Hello, Cargo! » 관례로서의 Cargo","id":"29","title":"관례로서의 Cargo"},"290":{"body":"소유권 규칙은 여러분들이 안전하고 동시적인 코드를 작성하는 것을 돕기 때문에 메세지 보내기 방식 내에서 강건한 역할을 합니다. 동시성 프로그래밍 내에서 에러를 방지하는 것은 여러분의 러스트 프로그램 전체에 걸친 소유권에 대한 생각해볼 수 있는 장점이 있습니다. 어떤 식으로 채널과 소유권이 문제를 방지하기 위해 함께 동작하는지를 보기 위한 실험을 해봅시다: 우리가 채널로 val 값을 내려보낸 이후에 생성된 스레드에서 이 값을 사용하는 시도를 해볼 것입니다. Listing 16-9의 코드를 컴파일하여 이 코드가 왜 허용되지 않는지를 보세요: Filename: src/main.rs use std::thread;\nuse std::sync::mpsc; 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} Listing 16-9: val을 채널로 내려보낸 뒤 이에 대한 사용 시도 여기서는 tx.send를 통하여 채널에 val을 내려보낸 뒤 이를 출력하는 시도를 하였습니다. 이 코드를 허용하는 것은 나쁜 생각입니다: 일단 값이 다른 스레드로 보내지고 나면, 우리가 값을 다시 사용해보기 전에 그 스레드에서 수정되거나 버려질 수 있습니다. 잠재적으로, 다른 스레드에서의 수정은 불일치하거나 존재하지 않는 데이터로 인한 에러를 일으킬 수 있습니다. 그러나, 우리가 Listing 16-9의 코드를 컴파일 시도하면 러스트는 에러를 내놓습니다: error[E0382]: use of moved value: `val` --> src/main.rs:10:31 |\n9 | tx.send(val).unwrap(); | --- value moved here\n10 | println!(\"val is {}\", val); | ^^^ value used here after move | = note: move occurs because `val` has type `std::string::String`, which does\nnot implement the `Copy` trait 우리의 동시성에 관한 실수가 컴파일 타임 에러를 야기했습니다. send 함수가 그 파라미터의 소유권을 가져가고, 이 값이 이동될 때, 수신자가 이에 대한 소유권을 얻습니다. 이는 우리가 값을 보낸 이후에 우발적으로 이 값을 다시 사용하는 것을 방지합니다; 소유권 시스템은 모든게 정상인지 확인합니다.","breadcrumbs":"겁없는 동시성 » 메세지 패싱 » 채널과 소유권 전달","id":"290","title":"채널과 소유권 전달"},"291":{"body":"Listing 16-8의 코드는 컴파일되고 실행도 되지만, 두개의 분리된 스레드가 채널을 통해 서로 대화를 했는지를 우리에게 명확히 보여주진 못했습니다. Listing 16-10에서는 Listing 16-8의 코드가 동시에 실행된다는 것을 입증해 중 수정본을 만들었습니다: 이제 생성된 스레드가 여러 메세지를 보내면서 각 메세지 사이에 1초씩 잠깐 멈출 것입니다. Filename: src/main.rs use std::thread;\nuse std::sync::mpsc;\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} Listing 16-10: 여러 메세지를 보내고 각 사이마다 멈추기 이번에 생성된 스레드는 우리가 메인 스레드로 보내고 싶어하는 스트링의 벡터를 가지고 있습니다. 스트링마다 반복하여 각각의 값을 개별적으로 보내고, Duration 값에 1을 넣어서 thread::sleep 함수를 호출하는 것으로 각각의 사이에 멈춥니다. 메인 스레드에서는 더 이상 recv 함수를 명시적으로 호출하지 않고 있습니다: 대신 rx를 반복자처럼 다루고 있습니다. 각각의 수신된 값에 대해서 이를 출력합니다. 채널이 닫힐 때는 반복이 종료될 것입니다. Listing 16-10의 코드를 실행시키면 다음과 같은 출력이 각 줄마다 1초씩 멈추면서 보일 것입니다: Got: hi\nGot: from\nGot: the\nGot: thread 메인 스레드의 for 루프 내에는 어떠한 멈춤 혹은 지연 코드를 넣지 않았으므로, 우리는 메인 스레드가 생성된 스레드로부터 값을 전달받는 것을 기다리는 중이라고 말할 수 있습니다.","breadcrumbs":"겁없는 동시성 » 메세지 패싱 » 복수의 값들을 보내고 수신자가 기다리는지 보기","id":"291","title":"복수의 값들을 보내고 수신자가 기다리는지 보기"},"292":{"body":"이전에 mpsc가 복수 생성자 단일 소비자 (multiple producer, single consumer) 의 약어라는 것을 언급했었지요. mpsc를 Listing 16-10의 코드에 넣어 모두 동일한 수신자로 값들을 보내는 여러 스레드들을 만들도록 코드를 확장해봅시다. Listing 16-11에서 보시는 것처럼 채널의 송신자를 복제하는 것으로 그렇게 할 수 있습니다: Filename: src/main.rs # use std::thread;\n# use std::sync::mpsc;\n# use std::time::Duration;\n#\n# fn main() {\n// --snip-- let (tx, rx) = mpsc::channel(); let tx1 = mpsc::Sender::clone(&tx);\nthread::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)); }\n}); 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)); }\n}); for received in rx { println!(\"Got: {}\", received);\n} // --snip--\n# } Listing 16-11: 여러 개의 생성자로부터 여러 메세지 보내기 이번에는 우리가 첫번째 스레드를 생성하기 전에, 채널의 송신 단말에 대해 clone을 호출했습니다. 이는 우리에게 첫번째 생성된 스레드로 값을 보낼 수 있는 새로운 송신 핸들을 제공해줄 것입니다. 두번째 생성된 스레드에게는 원래의 채널 송신 단말을 넘깁니다. 이렇게 함으로써 각각이 다른 메세지를 채널의 수신 단말로 보내주는 두 스레드를 만듭니다. 여러분이 이 코드를 실행시키면, 다음과 같은 출력과 비슷하게 보여야 합니다: Got: hi\nGot: more\nGot: from\nGot: messages\nGot: for\nGot: the\nGot: thread\nGot: you 값들의 순서가 다르게 보일 수도 있습니다; 이는 여러분의 시스템에 따라 다릅니다. 이것이 바로 동시성을 흥미로울 뿐만 아니라 어렵게 만드는 것입니다. 만일 여러분이 thread::sleep을 가지고 실험하면서 서로 다른 스레드마다 다양한 값을 썼다면, 각각의 실행이 더욱 비결정적이고 매번 다른 출력을 생성할 것입니다. 이제 채널이 어떤 식으로 동작하는지 보았으니, 동시성을 위한 다른 방법을 알아봅시다.","breadcrumbs":"겁없는 동시성 » 메세지 패싱 » 송신자를 복제하여 여러 생성자 만들기","id":"292","title":"송신자를 복제하여 여러 생성자 만들기"},"293":{"body":"메세지 패싱은 동시성을 다루는 좋은 방법이지만, 유일한 수단은 아닙니다. Go 언어 문서로부터 나온 슬로건의 일부를 다시한번 고려해보죠: “메모리를 공유함으로써 소통하세요.” 메모리를 공유하는 통신은 어떤 형태로 보일까요? 더불어서 메세지 패싱의 열광적인 지지자들은 왜 이걸 안쓰고 대신 반대편의 것을 쓸까요? 어떤 면에서, 프로그래밍 언어의 채널들은 단일 소유권과 유사한데, 이는 여러분이 채널로 값을 송신하면, 그 값을 더이상 쓸 수 없게되기 때문입니다. 공유 메모리 동시성은 복수 소유권과 유사합니다: 복수개의 스레드들이 동시에 동일한 메모리 위치를 접근할 수 있지요. 스마트 포인터들이 복수 소유권을 가능하게 만드는 내용을 담은 15장에서 보셨듯이, 복수 소유권은 이 서로 다른 소유자들의 관리가 필요하기 때문에 복잡성을 더할 수 있습니다. 러스트의 타입 시스템과 소유권 규칙은 이러한 관리를 올바르도록 훌륭히 유도합니다. 예를 들면, 공유 메모리를 위한 더 일반적인 동시성의 기초 재료 중 하나인 뮤텍스 (mutex)를 살펴 봅시다.","breadcrumbs":"겁없는 동시성 » 공유 상태 » 공유 상태 동시성","id":"293","title":"공유 상태 동시성"},"294":{"body":"뮤텍스 는 상호 배제 (mutual exclusion) 의 줄임말로서, 내부에서 뮤텍스는 주어진 시간에 오직 하나의 스레드만 데이터 접근을 허용합니다. 뮤텍스 내부의 데이터에 접근하기 위해서 스레드는 먼저 뮤텍스의 락 (lock) 을 얻기를 요청함으로써 접근을 윈한다는 신호를 보내야 합니다. 락은 누가 배타적으로 데이터에 접근하는지를 추적하는 뮤텍스의 부분인 데이터 구조입니다. 그러므로, 뮤텍스는 잠금 시스템을 통해 가지고 있는 데이터를 보호하는 것으로 묘사됩니다. 뮤텍스는 사용하기 어렵다는 평판을 가지고 있는데 이는 여러분이 다음 두 가지 규칙을 기억해야 하기 때문입니다: 여러분은 데이터를 사용하기 전에 반드시 락을 얻는 시도를 해야 합니다. 만일 뮤텍스가 보호하는 데이터의 사용이 끝났다면, 다른 스레드들이 락을 얻을 수 있도록 반드시 언락해야 합니다. 뮤텍스에 대한 실세계 은유를 위해서, 마이크가 딱 하나만 있는 컨퍼런스 패널 토의를 상상해보세요. 패널 참가자들이 말하기 전, 그들은 마이크 사용을 원한다고 요청하거나 신호를 줘야 합니다. 마이크를 얻었을 때는 원하는 만큼 길게 말을 한 다음 말하기를 원하는 다음 패널 참가자에게 마이크를 건네줍니다. 만일 패널 참여자가 마이크 사용을 끝냈을 때 이를 건네주는 것을 잊어먹는다면, 그 외 아무도 말할 수 없게 됩니다. 공유된 마이크의 관리가 잘못되면, 패널은 계획된데로 되지 않을겁니다! 뮤텍스의 관리는 바로잡기 위해 믿을 수 없으리만치 교묘해질 수 있는데, 이것이 바로 많은 사람들이 채널의 열성 지지자가 되는 이유입니다. 하지만, 러스트의 타입 시스템과 소유권 규칙에 감사하게도, 여러분은 잘못 락을 얻거나 언락 할 수가 없습니다. Mutex의 API 어떻게 뮤텍스를 이용하는지에 대한 예제로서, Listing 16-12와 같이 단일 스레드 맥락 내에서 뮤텍스를 사용하는 것으로 시작해봅시다: Filename: 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} Listing 16-12: 단순함을 위해 단일 스레드 맥락 내에서 Mutex의 API 탐색하기 많은 타입들처럼 Mutex는 연관함수 new를 사용하여 만들어집니다. 뮤텍스 내의 데이터에 접근하기 위해서는 lock 메소드를 사용하여 락을 얻습니다. 이 호출은 현재의 스레드를 막아설 것이므로, 락을 얻는 차례가 될 때까지 아무런 작업도 할 수 없습니다. lock의 호출은 다른 스레드가 패닉 상태의 락을 가지고 있을 경우 실패할 수 있습니다. 그런 경우 아무도 락을 얻을 수 없게 되므로, unwrap을 택하여 그런 상황일 경우 이 스레드에 패닉을 일으킵니다. 락을 얻고난 다음에는 그 반환값 (위의 경우에는 num이라는 이름의 값) 을 내부의 데이터에 대한 가변 참조자처럼 다룰 수 있습니다. 타입 시스템은 m 내부의 값을 사용하기 전에 우리가 락을 얻는 것을 확실히 해줍니다: Mutex는 i32가 아니므로 우리는 반드시 i32 값을 사용하기 위해 락을 얻어야 합니다. 우리는 이를 잊어버릴 수 없습니다; 잊어버린다면 타입 시스템이 내부의 i32에 접근할 수 없게 할 것입니다. 여러분이 의심한 것처럼, Mutex는 스마트 포인터입니다. 더 정확하게는, lock의 호출은 MutexGuard라고 불리우는 스마트 포인터를 반환합니다. 이 스마트 포인터는 우리의 내부 데이터를 가리키도록 Deref가 구현되어 있습니다; 이 스마트 포인터는 또한 MutexGuard가 스코프 밖으로 벗어났을 때 자동으로 락을 해제하는 Drop 구현체를 가지고 있는데, 이는 Listing 16-12의 내부 스코프의 끝에서 일어나는 일입니다. 결과적으로 락이 자동으로 해제되기 때문에, 우리는 락을 해제하는 것을 잊어버리고 다른 스레드에 의해 뮤텍스가 사용되는 것을 막는 위험을 짊어지지 않아도 됩니다. 락이 버려진 후, 뮤텍스 값을 출력하여 내부의 i32를 6으로 바꿀 수 있음을 확인할 수 있습니다. 여러 스레드들 사이에서 Mutex 공유하기 이제 Mutex를 사용하여 여러 스레드들 사이에서 값을 공유하는 시도를 해봅시다. 우리는 10개의 스레드를 돌리고 이들이 카운터 값을 1만큼씩 증가 시켜서, 카운터가 0에서 10으로 가도록 할 것입니다. 다음 몇 개의 예제가 컴파일 에러가 날 것이고, 우리가 이 에러를 사용하여 Mutex를 사용하는 방법과 러스트가 이를 고치는 것을 어떻게 돕는지에 대해 학습할 것임을 주의하세요. Listing 16-13이 시작 예제입니다: Filename: 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} Listing 16-13: Mutex에 의해 보호되는 카운터를 각자 증가시키는 10개의 스레드 Listing 16-12에서 했던 것처럼 Mutex 내부에 i32를 담는 counter 변수를 만듭니다. 그 다음, 숫자 범위로 반복하여 10개의 스레드를 만듭니다. 우리는 thread::spawn을 사용하여 동일한 클로저를 모든 스레드에게 주었는데, 이 클로저는 스레드로 카운터를 이동시키고, lock 메소드를 호출함으로써 Mutex의 락을 얻은 다음, 뮤텍스 내의 값을 1만큼 증가시킵니다. 스레드가 자신의 클로저 실행을 끝냈을 때, num은 스코프 밖으로 벗어내고 락이 해제되어 다른 스레드가 이를 얻을 수 있습니다. 메인 스레드에서 우리는 조인 핸들을 전부 모읍니다. 그리고나서 Listing 16-2에서 했던 것처럼, 각 핸들에 join을 호출하여 모든 스레드가 종료되는 것을 확실히 합니다. 이 시점에서 메인 스레드는 락을 얻고 이 프로그램의 결과를 출력합니다. 이 예제가 컴파일되지 않는다는 힌트를 줬었죠. 이제 왜 그런지 알아봅시다! error[E0382]: capture of moved value: `counter` --> src/main.rs:10:27 |\n9 | let handle = thread::spawn(move || { | ------- value moved (into closure) here\n10 | let mut num = counter.lock().unwrap(); | ^^^^^^^ value captured here after move | = note: move occurs because `counter` has type `std::sync::Mutex`, which does not implement the `Copy` trait error[E0382]: use of moved value: `counter` --> src/main.rs:21:29 |\n9 | let handle = thread::spawn(move || { | ------- value moved (into closure) here\n...\n21 | println!(\"Result: {}\", *counter.lock().unwrap()); | ^^^^^^^ value used here after move | = note: move occurs because `counter` has type `std::sync::Mutex`, which does not implement the `Copy` trait error: aborting due to 2 previous errors 이 에러 메세지는 counter 값이 클로저 내부로 이동되어서 우리가 lock을 호출할 때 캡처되었다고 설명합니다. 이 설명은 우리가 원하는 것처럼 들리지만, 허용되지 않습니다! 프로그램을 단순화하여 이를 알아내봅시다. 10개의 스레드를 for 루프 내에서 만드는 대신, 루프 없이 두 개의 스레드만 만들어서 어떤 일이 일어나는지 봅시다. Listing 16-13의 첫번째 for 루프를 아래 코드로 바꿔 넣으세요: use std::sync::Mutex;\nuse std::thread; fn main() { let counter = Mutex::new(0); let mut handles = vec![]; let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); let handle2 = thread::spawn(move || { let mut num2 = counter.lock().unwrap(); *num2 += 1; }); handles.push(handle2); for handle in handles { handle.join().unwrap(); } println!(\"Result: {}\", *counter.lock().unwrap());\n} 우리는 두 개의 스레드를 만들고 두번째 스레드에서 사용되는 변수 이름을 handle2와 num2로 바꿨습니다. 이제 이 코드를 실행하면, 컴파일러가 우리에게 다음 에러 메세지를 줍니다: error[E0382]: capture of moved value: `counter` --> src/main.rs:16:24 |\n8 | let handle = thread::spawn(move || { | ------- value moved (into closure) here\n...\n16 | let mut num2 = counter.lock().unwrap(); | ^^^^^^^ value captured here after move | = note: move occurs because `counter` has type `std::sync::Mutex`, which does not implement the `Copy` trait error[E0382]: use of moved value: `counter` --> src/main.rs:26:29 |\n8 | let handle = thread::spawn(move || { | ------- value moved (into closure) here\n...\n26 | println!(\"Result: {}\", *counter.lock().unwrap()); | ^^^^^^^ value used here after move | = note: move occurs because `counter` has type `std::sync::Mutex`, which does not implement the `Copy` trait error: aborting due to 2 previous errors 아하! 첫번째 에러 메세지는 counter가 handle과 연관된 스레드에 대한 클로저 내부로 이동되었음을 나타냅니다. 이 이동이 우리가 두번째 스레드에서 lock의 호출을 시도하고 num2에 결과를 저장할 때 counter를 캡처하는 것을 방지합니다! 따라서 러스트는 우리가 counter의 소유권을 여러 스레드로 이동시킬 수 없음을 말하는 중입니다. 이는 더 일찍 발견하기 어려운데 그 이유는 우리의 스레드들이 루프 내에 있었고, 러스트는 루프의 다른 반복 회차 내의 다른 스레드를 지적할 수 없기 때문입니다. 우리가 15장에서 다루었던 복수 소유자 메소드를 이용하여 이 컴파일에러를 고쳐봅시다. 여러 스레드들과 함께하는 복수 소유권 15장에서 우리는 참조 카운팅 값을 만들기 위해 스마트 포인터 Rc을 사용함으로써 값에게 복수의 소유권자를 주었습니다. 동일한 일을 여기서도 해서 어떻게 되는지 봅시다. Listing 16-14에에서 Mutex를 Rc로 감싸서 스레드로 소유권을 이동시키기 전에 이 Rc를 복제하겠습니다. 이제는 우리가 에러를 봤으므로, for 루프를 이용하도록 다시 전환하고 클로저와 함께 쓴 move 키워드를 유지하겠습니다. Filename: 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} Listing 16-14: 여러 스레드가 Mutex를 소유할 수 있도록 Rc를 사용하는 시도 다시 한번 컴파일을 하고 그 결과가... 다른 에러들이네요! 컴파일러는 우리에게 많은 것을 가르치고 있습니다. error[E0277]: the trait bound `std::rc::Rc>:\nstd::marker::Send` is not satisfied in `[closure@src/main.rs:11:36:\n15:10 counter:std::rc::Rc>]` --> src/main.rs:11:22 |\n11 | let handle = thread::spawn(move || { | ^^^^^^^^^^^^^ `std::rc::Rc>`\ncannot be sent between threads safely | = help: within `[closure@src/main.rs:11:36: 15:10\ncounter:std::rc::Rc>]`, the trait `std::marker::Send` is\nnot implemented for `std::rc::Rc>` = note: required because it appears within the type\n`[closure@src/main.rs:11:36: 15:10 counter:std::rc::Rc>]` = note: required by `std::thread::spawn` 와우, 이 에러는 정말 장황하네요! 여기 초점을 맞출 몇몇 중요한 부분이 있습니다: 첫번째 인라인 에러는 `std::rc::Rc>`는 스레드 사이에 안전하게 보내질 수 없다 라고 말합니다. 이에 대한 이유는 초점을 맞출 그 다음 중요한 부분인 에러 메세지 내에 있습니다. 정제된 에러 메세지는 트레잇 바운드 `Send`가 만족되지 않았다 라고 말합니다. Send는 다음 절에서 얘기할 것입니다: 이것은 우리가 스레드와 함께 사용하는 타입들이 동시적 상황들 내에서 쓰이기 위한 것임을 확실히 하는 트레잇 중 하나입니다. 안타깝게도, Rc는 스레드를 교차하면서 공유하기에는 안전하지 않습니다. Rc가 참조 카운트를 관리할 때, 각각의 clone 호출마다 카운트에 더하고 각 클론이 버려질 때마다 카운트에서 제합니다. 하지만 그것은 다른 스레드에 의해 카운트를 변경하는 것을 방해할 수 없도록 확실히 하는 어떠한 동시성 기초 재료도 이용하지 않습니다. 이는 잘못된 카운트를 야기할 수 있습니다-결과적으로 메모리 누수를 발생시키거나 아직 다 쓰기 전에 값이 버려질 수 있는 교묘한 버그를 낳겠죠. 우리가 원하는 것은 정확히 Rc와 비슷하지만 스레드-안전한 방식으로 참조 카운트를 바꾸는 녀석입니다. Atomic Reference Counting with Arc Arc을 이용하는 아토믹 (atomic) 참조 카운팅 다행히도, Arc가 바로 동시적 상황에서 안전하게 사용할 수 있는 Rc 타입입니다. a 는 아토믹 (atomic) 을 의미하는데, 즉 이것이 원자적으로 참조자를 세는 (atomically reference counted) 타입임을 의미합니다. 아토믹은 우리가 여기서 자세히 다루지 않을 추가적인 동시성 기초 재료 종류입니다: 더 자세히 알고 싶으면 std::sync::atomic에 대한 표준 라이브러리 문서를 보세요. 이 시점에서 여러분은 아토믹이 기초 타입처럼 동작하지만 스레드를 교차하며 공유해도 안전하다는 것만 알면 됩니다. 그렇다면 여러분은 왜 모든 기초 타입이 아토믹하지 않은지, 그리고 표준 라이브러리 타입은 왜 기본적으로 Arc을 구현에 이용하지 않는지를 궁금해 할런지도 모르겠습니다. 그 이유는 스레드 안전성이란 것이 여러분이 정말로 원할 때만 지불하고 싶을 성능 저하를 일으키기 때문입니다. 만일 여러분이 단일 스레드 내의 값에 대한 연산만 수행하는 중이라면, 아토믹이 제공하는 보장을 강제하지 않아도 된다면 여러분의 코드는 더 빠르게 실행될 수 있습니다. 우리의 예제로 다시 돌아갑시다: Arc와 Rc는 같은 API를 가지고 있으므로, 우리는 use을 사용하는 라인과 new 호출, 그리고 clone 호출 부분을 바꾸는 것으로 프로그램을 수정합니다. Listing 16-15의 코드는 마침내 컴파일 및 실행이 될 것입니다: Filename: src/main.rs use std::sync::{Mutex, Arc};\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} Listing 16-15: Arc를 사용하여 Mutex를 감싸서 여러 스레드 사이에서 소유권을 공유할 수 있도록 하기 이 코드는 다음을 출력할 것입니다: Result: 10 해냈군요! 우리는 0부터 10까지 세었고, 이는 그렇게 크게 인상적인 것 같지 않을런지도 모르겠지만, 우리에게 Mutex와 스레드 안전성에 대하여 많은 것을 가르쳐 주었습니다. 여러분은 또한 이 프로그램의 구조를 사용하여 단지 카운터를 증가시키는 것 보다 더 복잡한 연산을 할 수도 있습니다. 이 전략을 사용하여, 여러분은 계산할 것을 독립적인 부분들로 나누고, 해당 부분들을 스레드로 쪼갠 다음, Mutex를 사용하여 각 스레드가 해당 부분의 최종 결과를 갱신하도록 할 수 있습니다.","breadcrumbs":"겁없는 동시성 » 공유 상태 » 뮤텍스를 사용하여 한번에 한 스레드에서의 데이터 접근을 허용하기","id":"294","title":"뮤텍스를 사용하여 한번에 한 스레드에서의 데이터 접근을 허용하기"},"295":{"body":"여러분은 counter이 불변적이지만 이것 내부의 값에 대한 가변 참조자를 가지고 올 수 있었음을 알아챘을런지 모르겠습니다; 이는 Mutex가 Cell 가족이 그러하듯 내부 가변성을 제공한다는 의미입니다. 우리가 15장에서 Rc의 내용물을 변경할 수 있도록 하기 위해 RefCell을 사용한 것과 같은 방식으로, Arc 내부의 값을 변경하기 위해 Mutex를 이용합니다. 주목할만한 또다른 세부 사항은 여러분이 Mutex를 사용할 때 러스트가 여러분을 모든 종류의 논리적 에러로부터 보호해줄 수없다는 것입니다. 15장에서 Rc를 사용하는 것은 두 Rc 값들이 서로를 참조하여 메모리 누수를 야기하는 순환 참조자를 만들 위험성이 따라오는 것이었음을 상기하세요. 이와 유사하게, Mutex는 데드락 (deadlock) 을 생성할 위험성이 따라옵니다. 이것은 어떤 연산이 두 개의 리소스에 대한 락을 얻을 필요가 있고 두 개의 스레드가 하나씩의 락을 얻는다면, 서로가 서로를 영원히 기다리는 식으로 발생됩니다. 여러분이 데드락에 흥미가 있다면, 데드락이 있는 러스트 프로그램 만들기를 시도해보세요; 그리고나서 어떤 언어에 있는 뮤텍스를 위한 데드락 완화 전략를 연구해보고 이를 러스트에서 구현해보세요. Mutex와 MutexGuard에 대한 표준 라이브러리 API 문서가 유용한 정보를 제공합니다. 이제 Send와 Sync 트레잇에 대해 얘기하고 커스텀 타입과 함께 어떻게 이용할 수 있는지에 대해 얘기하는 것으로 이 장을 마무리 하겠습니다.","breadcrumbs":"겁없는 동시성 » 공유 상태 » RefCell/Rc와 Mutex/Arc 간의 유사성","id":"295","title":"RefCell/Rc와 Mutex/Arc 간의 유사성"},"296":{"body":"흥미롭게도, 러스트 언어는 매우 적은 숫자의 동시성 기능을 갖고 있습니다. 우리가 이 장에서 여지껏 얘기해 온 거의 모든 동시성 기능들이 언어의 부분이 아니라 표준 라이브러리의 영역이었습니다. 동시성 제어를 위한 여러분의 옵션들은 언어 혹은 표준 라이브러리에 제한되지 않습니다; 여러분은 여러분만의 동시성 기능을 작성하거나 다른 이들이 작성한 것을 이용할 수 있습니다. 그러나, 두 개의 동시성 개념이 이 언어에 내재되어 있습니다: 바로 std::marker 트레잇인 Sync와 Send입니다.","breadcrumbs":"겁없는 동시성 » 확장 가능한 동시성: Sync와 Send » Sync와 Send 트레잇을 이용한 확장 가능한 동시성","id":"296","title":"Sync와 Send 트레잇을 이용한 확장 가능한 동시성"},"297":{"body":"Send 마커 트레잇은 Send가 구현된 타입의 소유권이 스레드 사이에서 이전될 수 있음을 나타냅니다. 거의 대부분의 러스트 타입이 Send이지만, 몇 개의 예외가 있는데, 그 중 Rc도 있습니다: 이것은 Send가 될 수 없는데 그 이유는 여러분이 Rc 값을 클론하여 다른 스레드로 복제본의 소유권 전송을 시도한다면, 두 스레드 모두 동시에 참조 카운트 값을 갱신할지도 모르기 때문입니다. 이러한 이유로, Rc는 여러분이 스레드-안전성 성능 저하를 지불하지 않아도 되는 단일 스레드의 경우에 사용되도록 구현되었습니다. 따라서, 러스트의 타입 시스템과 트레잇 바운드는 여러분들이 결코 우연히라도 스레드 사이로 Rc 값을 불안전하게 보낼 수 없도록 확실히 해줍니다. Listing 16-14의 것을 시도할 때, 우리는 the trait Send is not implemented for Rc> 라는 에러를 얻었습니다. Send가 구현된 Arc로 바꿨을 때는 코드가 컴파일 되었습니다. 전체적으로 Send 타입으로 구성된 어떤 타입은 또한 자동적으로 Send로 마킹됩니다. 로우 포인터 (raw pointer)를 빼고 거의 모든 기초 타입이 Send인데, 이는 19장에서 다루겠습니다.","breadcrumbs":"겁없는 동시성 » 확장 가능한 동시성: Sync와 Send » Send를 사용하여 스레드 사이에 소유권 이전을 허용하기","id":"297","title":"Send를 사용하여 스레드 사이에 소유권 이전을 허용하기"},"298":{"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":"298","title":"Sync를 사용하여 여러 스레드로부터의 접근을 허용하기"},"299":{"body":"Send와 Sync 트레잇들로 구성된 타입들이 자동적으로 Send 될 수 있고 Sync하기 때문에, 우리가 이 트레잇들을 손수 구현치 않아도 됩니다. 마커 트레잇으로서, 이들은 심지어 구현할 어떠한 메소드도 없습니다. 이들은 그저 동시성과 관련된 불변성을 강제하는데 유용할 따름입니다. 이 트레잇 들을 손수 구현하는 것은 안전하지 않은 러스트 코드 구현을 수반합니다. 19장에서 안전하지 않은 러스트 코드에 대하여 이야기 하겠습니다; 지금으로서 중요한 정보는 Send되고 Sync하지 않은 요소들로 구성된 새로운 동시적 타입을 만드는 것이 안전성 보장을 유지하기 위해 조심스러운 생각을 요구한다는 점입니다. 러스토노미콘 이 이러한 보장과 이를 어떻게 유지하는지에 대한 더 많은 정보를 갖고 있습니다.","breadcrumbs":"겁없는 동시성 » 확장 가능한 동시성: Sync와 Send » Send와 Sync를 손수 구현하는 것은 안전하지 않습니다","id":"299","title":"Send와 Sync를 손수 구현하는 것은 안전하지 않습니다"},"3":{"body":"러스트는 시스템 프로그래밍 지식에 대한 다양한 수준을 가진 큰 개발자 팀들 사이에서 협업을 하기 위한 생산적인 도구라는 것이 밝혀지고 있습니다. 저수준 코드는 다양한 감지하기 힘든 버그들에 노출되기 쉬운데, 이는 다른 대부분의 언어들에서는 경험 있는 개발자들에 의한 대규모의 테스트 및 세심한 코드 리뷰를 통해 잡을 수 있습니다. 러스트에서는, 컴파일러가 동시성 버그를 포함하여 이러한 찾기 어려운 버그를 가진 코드의 컴파일을 거부함으로써 문지기 역할을 수행합니다. 이 컴파일러와 나란히 작업을 함으로써, 팀은 버그를 추적하는 것보다는 프로그램의 로직에 집중하는데 더 많은 시간을 쓸 수 있습니다. 또한 러스트는 시스템 프로그램 세계로 현대적인 개발자 도구들을 가져옵니다: Cargo라고 불리는 기본 구성에 포함된 의존성(dependency) 관리자 및 빌드 도구는, 러스트 생태계 상에서 고통 없고 일관되게 의존성을 추가하고, 컴파일하고, 관리하도록 해줍니다. Rustfmt는 개발자들 사이에서 일관된 코딩 스타일을 반드시 따르도록 해줍니다. 러스트 언어 서버(Rust Language Server)는 코드 자동완성(code completion) 및 인라인 에러 메시지를 위한 통합 개발 환경(IDE)으로의 결합에 힘을 제공합니다. 이들 및 러스트 생태계의 다른 툴들을 이용함으로서, 개발자들은 시스템 수준의 코드를 작성하면서도 생산적일 수 있습니다.","breadcrumbs":"소개 » 개발자 팀","id":"3","title":"개발자 팀"},"30":{"body":"여러분은 이미 여러분의 러스트 여정에서 아주 좋은 출발을 하고 있습니다! 이 장에서는 아래 항목들을 어떻게 하는지에 대해 배웠습니다: rustup을 사용하여 최신의 안정화된 러스트 버전 설치하기 더 최근에 나온 러스트 버전으로 업데이트하기 로컬에 설치된 문서 열기 rustc를 직접 사용하여 “Hello, world!” 프로그램을 작성하고 실행하기 Cargo의 관례를 사용하여 새로운 프로젝트를 만들고 실행하기 이제 러스트 코드를 읽고 쓰는데 익숙해지기 위해서 좀더 상당한 프로그램을 빌드하기 좋은 시간입니다. 따라서 다음 장에서는 추리 게임 프로그램을 빌드해 볼 것입니다. 만약 그보다 러스트에서 어떻게 보편적인 프로그래밍 개념이 동작하는지를 배우는 것으로 시작하길 원한다면, 3장을 먼저 보시고 2장으로 돌아오세요.","breadcrumbs":"시작하기 » Hello, Cargo! » 정리","id":"30","title":"정리"},"300":{"body":"여기가 이 책에서 동시성에 대해 보게될 마지막은 아닙니다: 20장의 프로젝트에서는 여기서 다룬 작은 예제보다 더 실제와 같은 상황에서 이번 장에서 다룬 개념들을 이용하게 될 것입니다. 일찍이 언급한 것처럼, 러스트가 동시성을 제어하는 방법이 언어의 매우 작은 부분이기 때문에, 많은 동시성 솔루션이 크레이트로 구현됩니다. 이들은 표준 라이브러리보다 더 빠르게 진화하므로, 멀티스레드 상황에서 사용하기 위하여 현재 가장 최신 기술의 크레이트를 온라인으로 검색해보세요. 러스트 표준 라이브러리는 메세지 패싱을 위해 채널을 제공하고, 동시적 맥락에서 사용하기에 안전한 Mutex와 Arc 같은 스마트 포인터 타입들을 제공합니다. 타입 시스템과 빌림 검사기는 이러한 솔루션을 이용하는 코드가 데이터 레이스 혹은 유효하지 않은 참조자로 끝나지 않을 것을 보장합니다. 여러분의 코드가 컴파일된다면, 여러분은 다른 언어에서는 흔한 추적하기 어려운 버그 종류들 없이 여러 스레드 상에서 행복하게 동작할 것이라고 자신감 있게 쉴 수 있습니다. 동시성 프로그래밍은 더 이상 두려워할 개념이 아닙니다: 앞으로 나아가 두려움없이 여러분의 프로그램을 동시적으로 만드세요! 다음으로, 우리는 여러분의 러스트 프로그램이 점차 커짐에 따라서 문제를 모델링하고 솔루션을 구조화하는 자연스러운 방법에 대해 이야기할 것입니다. 더불어서 여러분이 객체 지향 프로그램으로부터 친숙할지도 모를 개념들과 러스트의 관용구가 어떻게 연관되어 있는지 다루겠습니다.","breadcrumbs":"겁없는 동시성 » 확장 가능한 동시성: Sync와 Send » 정리","id":"300","title":"정리"},"301":{"body":"객체 지향 프로그래밍(OOP)는 프로그램을 모델링하는 방식입니다. 객체는 1960년대 Simula에서 유래됐습니다. 이 객체들은 임의의 객체들이 서로에게 메세지를 전달하는 Alan Kay의 프로그래밍 아키텍처에 영향을 끼쳤습니다. 1967년 그는 객체 지향 프로그래밍 이라는 용어를 이 아키텍처를 설명하기 위해 사용했습니다. 다수의 정의가 경쟁적으로 OOP이 무엇인지 설명합니다; 그 중 일부는 Rust를 객체 지향이라고 분류하지만 다른 정의는 그렇지 않습니다. 이번 장에서 우리는, 일반적인 객체 지향이 가진 특성들과 어떻게 이런 특성들이 러스트다운 표현들로 번역되었는지 알아볼 것입니다. 그런 후에 객체 지향적 디자인 패턴을 Rust에서 어떻게 구현하는지 보여주고 이를 Rust가 가진 강점을 사용하여 구현했을 경우의 기회비용에 대해 토의합니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 러스트의 객체 지향 프로그래밍 기능들","id":"301","title":"러스트의 객체 지향 프로그래밍 기능들"},"302":{"body":"객체 지향적인 언어가 반드시 갖춰야 할 기능에 대해 프로그래밍 커뮤니티들은 의견 일치를 보지 못하고 있습니다. 러스트는 OOP도 포함하여 많은 프로그래밍 패러다임에 영향을 받았습니다; 예를 들면, 우리가 13장에서 살펴본 기능인 함수형 프로그래밍에서 온 기능들 말이지요. OOP 언어라면 거의 틀림없이 몇가지 공통적인 특성을 공유하는데, 객체, 캡슐화 및 상속이 있습니다. 이 특성들이 각각 뜻하는 것과 러스트가 이를 지원하는지에 대해 살펴봅시다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 객체 지향 언어의 특성","id":"302","title":"객체 지향 언어의 특성"},"303":{"body":"흔히 The Gang of Four라고도 불리우는 Erich Gamma, Richard Helm, Ralph Johnson, 그리고 John Vlissides (Addison-Wesley Professional, 1994)의 책 Design Patterns: Elements of Reusable Object-Oriented Software 은 객체 지향 디자인 패턴의 편람입니다. 이 책에서는 OOP를 다음과 같이 정의합니다. 객체-지향 프로그램은 객체로 구성된다. 객체 는 데이터 및 이 데이터를 활용하는 프로시저를 묶는다. 이 프로시저들은 보통 메소드 혹은 연산 (operation) 으로 불린다. 이 정의에 따르면, 러스트는 객체 지향적입니다: 구조체와 열거형은 데이터를 갖고, impl 블럭은 그 구조체와 열거형에 대한 메소드를 제공하죠. 설령 메소드를 갖는 구조체와 열거형을 객체라고 호칭 하지 않더라도, 그들은 동일한 기능을 수행하며, 이는 Gang of Four의 객체에 대한 정의를 따릅니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 객체는 데이터와 동작을 담습니다","id":"303","title":"객체는 데이터와 동작을 담습니다"},"304":{"body":"일반적으로 OOP와 관련된 또다른 면은 캡슐화로, 그 의미는 객체를 이용하는 코드에서 그 객체의 상세 구현에 접근할 수 없게 한다는 것입니다. 따라서, 유일하게 객체와 상호작용하는 방법은 이것의 공개 API를 통하는 것입니다; 객체를 사용하는 코드는 직접 객체의 내부에 접근하여 데이터나 동작을 변경해서는 안됩니다. 이는 프로그래머가 객체를 사용하는 코드의 변경없이 이 객체 내부를 변경하거나 리팩토링할 수 있도록 해줍니다. 우리는 7장에서 어떻게 캡슐화를 제어하는지에 대해 논의했습니다: 우리는 pub 키워드를 사용하여 어떤 모듈들, 타입들, 함수들, 그리고 메소드들이 공개될 것인가를 결정할 수 있으며, 기본적으로는 모든 것들이 비공개입니다. 예를 들면, 우리는 i32 값의 벡터 항목을 가지고 있는 AveragedCollection 구조체를 정의할 수 있습니다. 또한 이 구조체는 벡터의 값에 대한 평균값을 담는 항목도 갖는데, 이는 누구든 평균값이 필요한 순간마다 매번 이를 계산할 필요는 없음을 의미합니다. 바꿔 말하면, AveragedCollection은 우리를 위해 계산된 평균값을 캐쉬할 것입니다. Listing 17-1가 이 AveragedCollection 구조체에 대한 정의입니다. Filename: src/lib.rs pub struct AveragedCollection { list: Vec, average: f64,\n} Listing 17-1: 콜렉션 내의 정수 항목들과 그의 평균을 관리하는 AveragedCollection 구조체 구조체가 pub으로 표기되면 다른 코드가 이를 사용할 수 있게 되지만, 구조체 안에 존재하는 항목들은 여전히 비공개입니다. 이는 이번 사례에 매우 중요한데, 그 이유는 하나의 값이 리스트에서 더해지거나 제거될 때마다 평균 또한 갱신되는 것을 확신하길 원하기 때문입니다. 우리는 add, remove, 그리고 average 메소드를 구조체에 구현하여 이를 달성하고자 하며, 이는 Listing 17-2과 같습니다: Filename: src/lib.rs # pub struct AveragedCollection {\n# list: Vec,\n# average: f64,\n# }\nimpl 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} Listing 17-2: AveragedCollection의 공개 메소드 add, remove, 그리고 average 공개 메소드들 add, remove, 그리고 average는 AveragedCollection의 인스턴스를 수정하는 유일한 방법입니다. 아이템이 list에 add 메소드를 통해 추가되거나 remove 메소드를 통해 제거될 때, 각각의 호출은 비공개 update_average 메소드를 호출하여 average 필드를 변경하도록 하는 역할 또한 수행합니다. 우리가 list와 average 필드를 비공개로 두었으므로 외부 코드가 list 필드에 직접 아이템들을 추가하거나 제거할 방법은 없습니다; 그렇지 않으면, average 필드는 list가 변경될 때 동기화되지 않을지도 모릅니다. average 메소드는 average 필드의 값을 반환하여, 외부 코드가 average를 읽을 수 있도록 하지만, 변경은 안됩니다. 우리가 AveragedCollection의 내부 구현을 캡슐화했기 때문에, 차후에 데이터 구조 등을 쉽게 변경할 수 있습니다. 예를 들면, 우리는 list 필드에 대해서 Vec가 아닌 HashSet를 사용할 수 있습니다. add, remove 그리고 average 공개 메소드들의 선언이 그대로 유지되는 한, AveragedCollection를 사용하는 코드들은 변경될 필요가 없습니다. 대신 우리가 list를 공개했다면 꼭 그런 상황이 될 수는 없을 것입니다: HashSet와 Vec는 아이템들을 추가하거나 제거하기 위한 메소드들이 다르므로, 만약 list에 직접 접근하여 변경하는 방식의 외부 코드들이 있다면 모두 변경되어야겠죠. 만약 캡슐화가 객체 지향을 염두하는 언어를 위한 필요 요소라면, 러스트는 이를 만족합니다. 코드의 서로 다른 부분들에 대해 pub을 사용하거나 사용하지 않는 옵션이 구현 세부 사항의 캡슐화를 가능케 합니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 상세 구현을 은닉하는 캡슐화","id":"304","title":"상세 구현을 은닉하는 캡슐화"},"305":{"body":"상속 은 어떤 객체가 다른 객체의 정의를 상속받아서, 이를 통해 부모 객체의 데이터와 동작들을 다시 정의하지 않고도 얻을 수 있게 해주는 메커니즘입니다. 만약 객체 지향 언어가 반드시 상속을 제공해야 한다면, 러스트는 그렇지 않은 쪽입니다. 부모 구조체의 필드와 메소드 구현을 상속받는 구조체를 정의할 방법은 없습니다. 하지만 여러분이 상속에 익숙하다면, 우선 이를 사용하고자 하는 이유에 따라 러스트의 다른 솔루션들을 이용할 수 있습니다. 여러분은 두가지 주요한 이유에 의해 상속을 택합니다. 하나는 코드를 재사용하는 것입니다: 여러분은 어떤 타입의 특정한 행위를 구현할 수 있고, 상속은 당신이 다른 타입을 위해 그 구현을 재사용할 수 있도록 만들어줍니다. 여러분은 대신 기본 트레잇 메소드의 구현을 이용하여 러스트 코드를 공유할 수 있는데, 이는 Listing 10-14에서 우리가 Summary 트레잇에 summarize 메소드의 기본 구현을 추가할 때 봤던 것입니다. Summary 트레잇을 구현하는 어떤 타입이든, summarize 메소드를 별도로 작성하지 않더라도 사용 가능합니다. 이는 어떤 메소드의 구현체를 갖는 부모 클래스와 그를 상속받는 자식 클래스 또한 그 메소드의 해당 구현체를 갖는 것과 유사합니다. 우리는 또한 Summary 트레잇을 구현할 때 summarize의 기본 구현을 오버라이딩할 수 있고, 이는 자식 클래스가 부모 클래스에서 상속받는 메소드를 오버라이딩하는 것과 유사합니다. 상속을 사용하는 다른 이유는 타입 시스템과 관련있습니다: 자식 타입을 같은 위치에서 부모 타입처럼 사용할 수 있게 하기 위함입니다. 이를 또한 다형성 (polymorphism) 이라고도 부르는데, 이는 여러 객체들이 일정한 특성을 공유한다면 이들을 런타임에 서로 바꿔 대입하여 사용할 수 있음을 의미합니다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 타입 시스템과 코드 공유로서의 상속","id":"305","title":"타입 시스템과 코드 공유로서의 상속"},"306":{"body":"많은 사람들이 다형성을 상속과 동일시 합니다. 하지만 다형성은 다수의 타입들의 데이터에 대해 동작 가능한 코드를 나타내는 더 범용적인 개념입니다. 상속에서는 이런 타입들이 일반적으로 하위클래스에 해당합니다. 러스트는 대신 제네릭을 사용하여 호환 가능한 타입을 추상화하고 트레잇 바운드를 이용하여 해당 타입들이 반드시 제공해야 하는 제약사항을 부과합니다. 이것을 종종 범주내 매개변수형 다형성 (bounded parametric polymophism) 이라고 부릅니다. 최근에는 상속이 많은 프로그래밍 언어에서 프로그래밍 디자인 솔루션으로서의 인기가 떨어지고 있는데 그 이유는 필요한 것보다 더 많은 코드를 공유할 수 있는 위험이 있기 때문입니다. 하위 클래스가 늘 그들의 부모 클래스의 모든 특성을 공유해서는 안되지만 상속한다면 그렇게 됩니다. 이는 프로그램의 유연성을 저하시킬 수 있습니다. 또한, 하위 클래스에서는 타당하지 않거나 적용될 수 없어서 에러를 유발하는 메소드들이 호출될 수 있는 가능성을 만듭니다. 게다가, 어떤 언어들은 하나의 클래스에 대한 상속만을 허용하기 때문에 프로그램 디자인의 유연성을 더욱 제한하게 됩니다. 이런 이유로, 러스트는 다른 방식을 취하여, 상속 대신에 트레잇 객체를 사용합니다. 러스트에서 어떤 식으로 트레잇 객체가 다형성을 가능케 하는지 살펴봅시다.","breadcrumbs":"러스트의 객체 지향 프로그래밍 기능들 » 객체 지향 언어의 특성 » 다형성","id":"306","title":"다형성"},"307":{"body":"8장에서는 벡터가 한 번에 하나의 타입만 보관할 수 있다는 제약사향이 있다고 언급했습니다. 우리가 만들었던 Listing 8-10의 작업내역에서는 정수, 부동소수점, 그리고 문자를 보관하기 위한 variant들을 가지고 있는 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":"307","title":"트레잇 객체를 사용하여 다른 타입 간의 값 허용하기"},"308":{"body":"gui가 갖길 원하는 동작을 구현하기 위해, 우리는 draw라는 이름의 메소드 하나를 갖는 Draw라는 이름의 트레잇을 정의할 것입니다. 그러면 트레잇 객체 (trait object) 를 취하는 벡터를 정의할 수 있습니다. 트레잇 객체는 특정 트레잇을 구현한 타입의 인스턴스를 가리킵니다. 우리는 & 참조자나 Box 스마트 포인터 같은 포인터 종류로 명시함으로서 트레잇 객체를 만들고, 그런 다음 관련된 트레잇을 특정합니다. (우리가 트레잇 객체에 포인터를 사용해야 하는 이유는 19장의 “동적인 크기의 타입과 Sized” 절에서 다룰 겁니다.) 우리는 제네릭 타입이나 구체 타입 대신 트레잇 객체를 사용할 수 있습니다. 트레잇 객체를 사용하는 곳이 어디든, 러스트의 타입 시스템은 컴파일 타임에 해당 문맥 안에 사용된 값이 트레잇 객체의 트레잇을 구현할 것을 보장합니다. 결론적으로, 우리는 컴파일 타임에 모든 가능한 타입을 알 필요가 없습니다. 러스트에서는 구조체와 열거형을 다른 언어의 객체와 구분하기 위해 “객체”라고 부르는 것을 자제한다고 언급했었습니다. 구조체 또는 열거형에서는 구조체 필드의 데이터와 impl 블록의 동작이 분리되는 반면, 다른 언어에서는 데이터와 동작이 결합되어 객체로 명명됩니다. 그러나 트레잇 객체들은 데이터와 동작을 결합한다는 의미에서 다른 언어의 객체와 비슷합니다 . 하지만 트레잇 객체는 트레잇 객체에 데이터를 추가 할 수 없다는 점에서 전통적인 객체들과 다릅니다. 트레잇 객체는 다른 언어들의 객체만큼 범용적으로 유용하지는 않습니다: 그들의 명확한 목적은 공통된 동작들에 걸친 추상화를 가능하도록 하는 것이죠. Listing 17-3은 draw라는 이름의 메소드를 갖는 Draw 라는 트레잇을 정의하는 방법을 보여줍니다: Filename: src/lib.rs pub trait Draw { fn draw(&self);\n} Listing 17-3: Draw 트레잇의 정의 이 문법은 10장에 있는 트레잇을 정의하는 방법에서 다뤘으니 익숙하실 겁니다. 다음에 새로운 문법이 등장합니다: Listing 17-4는 components 라는 벡터를 보유하고 있는 Screen이라는 구조체를 정의합니다. Box 타입의 벡터인데, 이것이 트레잇 객체입니다; 이것은 Draw 트레잇을 구현한 Box에 담긴 임의의 타입에 대한 대역입니다. Filename: src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n#\npub struct Screen { pub components: Vec>,\n} Listing 17-4: Draw 트레잇을 구현하는 트레잇 객체들의 벡터 항목 components를 소유한 구조체 Screen Screen 구조체에서는 Listing 17-5와 같이 각 components 마다 draw메소드를 호출하는 run 메소드를 정의합니다: Filename: src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n#\n# pub struct Screen {\n# pub components: Vec>,\n# }\n#\nimpl Screen { pub fn run(&self) { for component in self.components.iter() { component.draw(); } }\n} Listing 17-5: 각 컴포넌트에 대해 draw 메소드를 호출하는 Screen의 run 메소드 이것은 트레잇 바운드와 함께 제네릭 타입 파라미터를 사용하는 구조체를 정의하는 것과는 다르게 작동합니다. 제네릭 타입 파라미터는 한 번에 하나의 구체 타입으로만 대입될 수 있는 반면, 트레잇 객체를 사용하면 런타임에 여러 구체 타입을 트레잇 객체에 대해 채워넣을 수 있습니다. 예를 들면, Listing 17-6처럼 제네릭 타입과 트레잇 바운드를 사용하여 Screen 구조체를 정의할 수도 있을 겁니다. Filename: src/lib.rs # pub trait Draw {\n# fn draw(&self);\n# }\n#\npub struct Screen { pub components: Vec,\n} impl Screen where T: Draw { pub fn run(&self) { for component in self.components.iter() { component.draw(); } }\n} Listing 17-6: 제네릭과 트레잇 바운드를 사용한 Screen 구조체와 run 메소드의 대체 구현 이렇게하면 전부 Button 타입 혹은 전부 TextField 타입인 컴포넌트 리스트를 가지는 Screen 인스턴스로 제한됩니다. 동일 유형의 콜렉션만 사용한다면 제네릭과 특성 범위를 사용하는 것이 바람직한데, 왜냐하면 그 정의들은 구체 타입을 사용하기 위해 컴파일 타임에 단형성화 (monomorphize) 되기 때문입니다. 반면에 트레잇 객체를 사용하는 메소드를 이용할때는 하나의 Screen 인스턴스가 Box