아주 좋은 글이다.



Spring Interceptor(혹은 Servlet Filter)에서 POST 방식으로 전달된 JSON 데이터 처리하기

https://meetup.toast.com/posts/44

'Java > Spring' 카테고리의 다른 글

Spring OSIV 작동 방식 (OpenEntityManagerInViewFilter)  (0) 2022.05.19
스프링의 트랜잭션 관리  (0) 2017.06.30

이클립스 메이븐 플러그인 m2e 는 의존성 설정된 프로젝트가 로컬에 존재하는 경우 로컬 프로젝트를 참조하고, 존재하지 않는 경우 원격 저장소(넥서스) 에서 jar 를 받아와 참조한다.


그런데 로컬에 존재하는 프로젝트의 jar 파일이 톰캣의 server path lib 경로로 배포(복사)되지 않아서 서버 동작 시 ClassNotFoundException 이 던져지는 경우가 있다.


이 경우 다음과 같은 확인 절차를 따른다.


1. 먼저 web 프로젝트의 pom.xml 재확인

-> 의존성 설정이 정확히 되어있는지 다시 확인한다.



2. 의존 프로젝트의 의존성 트리 확인

-> web 프로젝트 -> 의존 프로젝트가 간접 의존 관계인 경우 그 연결고리가 제대로 맺어져 있는지 확인한다.



3. web 프로젝트의 Java Build path 확인

-> 프로젝트 properties 의 Java Build Path 에서 Maven Dependencies 에 의존 프로젝트가 제대로 떠 있는지 확인한다. 로컬 프로젝트의 경우 로컬 프로젝트 아이콘으로 존재해야 한다.



4. 의존 프로젝트의 Deployment Assembly 확인

-> 여기가 관건이다. 위 1,2,3 은 보통 제대로 설정이 되어 있다. 그럼에도 불구하고 이 문제가 발생하는 경우, 의존 프로젝트의 properties 의 Deployment Assembly 를 본다. 여기의 설정이 다른 정상적인 프로젝트의 설정과 일치하는지 확인한다. 프로젝트 설정이 틀어진 경우 이 메뉴를 클릭하면

...contains invalid values 라는 오류 안내를 내뱉고 아예 메뉴가 열리지 않을 수 있는데, 이 경우 다른 정상적인 프로젝트의 다음 경로의 파일들을 가져와 덮어 쓴다:


  • .settings
  • .classpath
  • .project


그리고 Deployment Assembly 메뉴가 정상적으로 열리는지 확인한다.


확인이 되었으면 프로젝트를 clean-install 후 web 프로젝트를 clean-install, 톰캣 clean 을 한 번 해주자.


com.ibm.db2.jcc.am.SqlSyntaxErrorException:

[jcc][10145][10844][3.62.56] Invalid parameter 1: Parameter

index is out of range. ERRORCODE=-4461, SQLSTATE=42815

com.ibm.db2.jcc.am.SqlSyntaxErrorException:

[jcc][10145][10844][3.62.123] 유효하지 않은 매개변수 1: 매개변수 인덱스가 범위를 벗어납니다.

ERRORCODE=-4461, SQLSTATE=42815



이 오류는 기본적으로 db2 jdbc type 4 드라이버가 named parameter setting 처리 도중 파라미터의 타입과 실제 컬럼의 데이터 타입이 맞지 않는 데서 발생한다. 그런데 2대 이상의 호스트에서 컨테이너를 띄울 때, 간혹 정확히 같은 request 내용을 처리 시 어떤 서버에서는 오류가 발생하고, 어떤 서버에서는 오류가 발생하지 않는 현상이 있다.


이는 db2 jdbc 드라이버 버전의 차이에서 발생하는 현상으로, 제우스의 datasource path 에서 db2 드라이버 버전을 맞춰 주어 해결할 수 있다.


아래는 내 호스트의 jdbc 드라이버 jar 위치이다. 제우스의 webinf-first 설정을 false 로 한다면 이 곳의 드라이버 클래스를 로드 할태니 참고.


.../jeus/lib/datasource/



아래는 ibm 의 관련 내용. JCC driver version 을 3.64.141 이상으로 올릴 것을 권하고 있다.


http://www-01.ibm.com/support/docview.wss?uid=swg1IC86612

MyClass.class.getProtectionDomain().getCodeSource().getLocation().getPath();


현재 JVM에 로드된 클래스의 물리적 파일 위치를 반환.

러스트의 클로저


원문: http://huonw.github.io/blog/2015/05/finding-closure-in-rust/ 




클로저란?


클로저를 한 문장으로 표현하자면: 클로저란, 자신이 정의된 스코프의 변수를 직접 사용할 수 있는 함수이다. 클로저의 이 특성은 흔히 closing over 또는 capturing variables 이라 불리며, 이렇게 접근되는 변수를 environment 라 한다. 

※ 이하 capturing 을 캡처링, environment 를 변수라 번역.


문법적으로, 러스트의 클로저는 루비에서의 것과 유사하게 파이프('|') 로 정의되는 익명 함수이다. |arguments...| body. 예로, |a, b| a + b 는 두 아규먼트를 받아 둘의 합계를 리턴하는 클로저이다. 이건 평범한 함수 정의와 비슷하면서 추론 기능이 더해진 형태다.


// 함수:
fn foo(a: i32, b:i32) -> i32 {a + b}
// 클로저:
|a, ,b| a + b


일반 함수와 마찬가지로, 클로저도 괄호와 함께 호출된다: closure(arguments...).


아래 코드는 캡처링을 표현하기 위해 Option<i32>map 을 호출한다. mapi32 값에 대해 클로저를 호출하고, 새 Option 을 생성하여 여기에 클로저의 결과값을 담아 리턴한다.


fn main() {
let option = Some(2);

let x = 3;
// 명시적 타입:
let new: Option<i32> = option.map(|val: i32| -> i32 { val + x });
println!("{:?}", new); // Some(5)

let y = 10;
// 추론형:
let new2 = option.map(|val| val * y);
println!("{:?}", new2); // Some(20)
}


클로저는 변수 xy 를 캡처하고 이 변수들을 매핑에 사용할 수 있도록 한다. (이 코드에 하드코딩된 값은 실제로는 런타임에서만 알 수 있는 값이라 생각하자.)




근본으로 돌아가서...


이제 우리는 클로저의 의미를 알았다. 이제 그 근본으로 들어가보자: 러스트에 클로저가 없다면, 제너릭 map 을 어떻게 구현해야 할까?


Option::map 의 기능을 동등하게 구현해보자면:


fn map<X, Y>(option: Option<X>, transformer: ...) -> Option<Y> {
match option {
Some(x) => Some(transformer(x)), // 일단은 클로저 호출 형태이다.
None => None,
}
}


우리는 ...XY 로 변환하는 무언가를 넣어야 한다. Option::map 을 완벽히 대체하기에 가장 큰 제약사항은, 여기에 어떤 형태로든 제너릭이 필요하다는 점이다. 그래야만 우리가 원하는대로 유연하게 작동할 수 있다. 러스트에서 이 제너릭 바운딩은 트레잇을 통해 이루어진다.


fn map<X, Y, T>(option: Option<X>, transform: T) -> Option<Y>
where T: /* 트레잇 */
{


이 트레잇에 특정 데이터를 다른 데이터로 변환하는 메서드가 있어야 한다. 그래서 여기에 map 과 같은 제너릭 바운드에서 특정 데이터의 정확한 타입을 지정할 수 있는 타입 파라미터가 필요하다. 두 선택지가 있다: 트레잇 정의의 제너릭 ("인풋 타입 파라미터"), 연관(associated) 타입 ("아웃풋 타입 파라미터"). 쌍따옴표로 표시된 부분이 우리 선택의 힌트가 된다: 데이터 변환의 인풋 타입은 제너릭 정의에, 그리고 아웃풋 타입은 연관 타입에 있어야 한다.


이렇게 하여 나온 트레잇은 다음과 같다:


trait Transform<Input> {
type Output;

fn transform(/* self?? */, input: Input) -> Self::Output;
}


이제 마지막 남은 질문은 어떤 형태의 self 를 취하느냐이다.


이 데이터 변환은 Input 이 가진 값 이상의 임의의 정보를 통합할 수 있어야 한다. self 아규먼트 없이는 이 메서드는 fn transform(input: Input) -> Self::Output 의 형태를 가지게 되며, 메서드의 기능은 전적으로 Input 과 글로벌 변수에 의존하게 된다. 때문에 우리는 self 가 필요하다.


가장 명쾌한 선택은 레퍼런스에 의한 &self, 뮤터플 레퍼런스에 의한 &mut self, 값에 의한 self 이다. 우리는 map 에 타입 체킹을 두면서도, map 의 사용자에게 가능한대로 강력한 기능을 제공하고 싶다. self 는 구현자(implementers: 이 트레잇을 구현하는 타입)에게 가장 강력한 유연성을 주고, &mut self&self 는 최소한의 유연성을 준다. 반대로, &self 는 이 트레잇의 소비자(comsumers: 이 트레잇을 통해 제너릭 바운딩되는 함수)에게 가장 강력한 유연성을 주고, self 는 최소한의 유연성을 준다.


 

 구현자(implementers: 이 트레잇을 구현하는 타입)

 소비자(comsumers: 이 트레잇을 통해 제너릭 바운딩되는 함수)

self

 이동할 수 있고, 변경할 수 있음

 메서드를 오직 한 번만 호출할 수 있음

&mut self

 이동할 수 없고, 변경할 수 있음

 메서드를 여러 번 호출할 수 있고, 자신만 단독으로 접근할 수 있음

&self

 이동할 수 없고, 변경할 수 없음

 메서드를 여러 번 호출할 수 있고, 접근에 제한이 없음

(여기서 '이동' 과 '변경' 은 self 에 저장되는 데이터를 의미한다.)


이들 중 어느 것을 선택하느냐는 균형의 문제이다. 우리는 보통 선택지에서 가장 강력한 것을 원한다. 소비자에게 원하는 것을 제약 없이 가능하게 하고, 동시에 구현자에게도 최대한 많은 것을 가능하게 하고 싶다.


선택지의 첫번째부터 시작해보자: 우리는 self 를 시도한다: fn transform(self, input: Input) -> Self::Output. 값에 의한 self 는 오너십을 소비한다. 때문에 transform 은 오직 한 번만 호출 가능하다. 다행히도 map 은 데이터 변환을 한 번만 하면 되기 때문에, self 를 사용할 수 있다.


간단히 표현해서, map 과 트레잇은 이제 다음과 같다:


trait Transform<Input> {
type Output;

fn transform(self, input: Input) -> Self::Output;
}

fn map<X, Y, T>(option: Option<X>, transform: T) -> Option<Y>
where T: Transform<X, Output = Y>
{
match option {
Some(x) => Some(transform.transform(x)),
None => none,
}
}


이제 이 트레잇을 구현하는 적절한 구조체를 만든다:


// |val| val + x 을 대체.
struct Adder { x: i32 }

impl Transform<i32> for Adder {
type Output = i32;

// `fn ... self` 은 무시한다. 이것은 |val| val + x 와 유사하다.
fn transform(self, val: i32) -> i32 {
val + self.x
}
}

// |val| val * y 을 대체.
struct Multiplier { y: i32 }

impl Transform<i32> for Multiplier {
type Output = i32;

// |val| val * y 와 유사하다.
fn transform(self, val: i32) -> i32 {
val * self.y
}
}

fn main() {
let option = Some(2);

let x = 3;
let new: Option<i32> = map(option, Adder { x: x });
println!("{:?}", new); // Some(5)

let y = 10;
let new2 = map(option, Multiplier { y: y });
println!("{:?}", new2); // Some(20)
}


우리는 대강 러스트 클로저와 의미적으로 동일한 것을 구현했다. 트레잇과 구조체를 사용해서 스코프의 변수를 다뤘다. 사실 이 구조체에는 클로저의 스코프(environment)와 묘한 유사성이 있다: transform 의 바디에서 사용할 변수를 구조체 자신 안에 저장한다는 것이다.




진짜 클로저는 어떻게 동작하는가?


클로저는 위에서 구현한 것에 유연성과 문법적 편의성(syntactic sugar)을 더한 것이다. Option::map 의 진짜 정의는 다음과 같다:


impl<X> Option<X> {
pub fn map<Y, F: FnOnce(X) -> Y>(self, f: F) -> Option<Y> {
match self {
Some(x) => Some(f(x)),
None => None
}
}
}


FnOnce(X) -> Y 는 우리의 Transform<X, Output = Y> 바운드와, 그리고 f(x)transform.transform(x) 와의 대칭이다.


클로저에는 세 가지 트레잇이 있다. 셋 모두 ...(...) 의 호출 형태를 제공한다. 이 트레잇들의 차이는 호출 메서드의 self 의 타입에 있다. 그리고 이들은 위에서 언급한, self 의 모든 유형을 다룬다.

  • &self : Fn
  • &mut self : FnMut
  • self : FnOnce
이 트레잇들은 러스트의 데이터 핸들링의 세 가지 핵심적 방식을 정확히 커버한다. 각 트레잇은 러스트의 타임 시스템과 완벽하게 어우러진다.

|args...| code... 코드를 작성하면, 컴파일러는 암시적으로 새로운 구조체 타입을 정의하고, 이 구조체 안에 캡처된 변수를 저장하고, 클로저의 바디를 이용하여 위 트레잇 중 하나를 구현하고, 클로저의 스코프를 통해 캡처된 변수를 다시 작성한다. 이 구조체 타입의 이름은 사용자에게 보여지지 않으며, 순수히 컴파일러에게만 내부적으로 존재한다. 런타임에 프로그램이 클로저 정의 코드에 닿으면(실행할 때가 되면), 클로저 정의는 컴파일러가 만든 구조체의 인스턴스를 생성하여 이 인스턴스를 클로저의 자리에 전달한다. (우리가 위에서 mapAdderMultiplier 를 생성해서 전달했던 것처럼)

여기엔 두 가지 의문이 있다:
  1. 변수는 어떻게 캡처되는가? (클로저 구조체 필드의 타입은 무엇?)
  2. 어떤 트레잇이 사용되는가? (self 의 유형은 무엇?)
컴파일러는 위 질문에 몇가지 로컬 규칙을 사용하여 답을 내린다. 이 규칙은 가장 강력한 유연성을 제공하는 옵션을 선택하기 위한 것이다. 로컬 규칙은 오직 클로저의 정의와 클로저가 캡처랑 변수의 타입만을 가지고 체크하도록 구현되어 있다.

여기서 '유연성' 이란, 컴파일러의 시각에서 컴파일 가능하면서도 프로그래머에게 최소한의 책임을 부과하는 옵션을 의미한다.



구조체와 캡처

당신이 C++11 의 클로저에 익숙하다면, [=][&] 캡처를 떠올릴 수 있다: 값에 의한 변수 캡처, 레퍼런스에 의한 변수 캡처. 러스트는 비슷한 기능을 제공한다: 값으로 캡처할 수 있고(클로저 스코프로 변수를 이동시키기), 레퍼런스로 캡처할 수도 있다(클로저 스코프로 변수의 레퍼런스를 저장하기).

기본적으로, 컴파일러는 클로저 바디 코드 안에서 캡처 대상 변수가 어떤식으로 사용되는지 체크하여 이 변수의 캡처 방식을 추론한다:
  • 캡처된 변수가 오직 공유된 레퍼런스를 통해서만 사용되면, 이 변수는 & 레퍼런스로 캡처된다.
  • 캡처된 변수가 뮤터블 레퍼런스로 사용되면 (값 할당 포함), 이 변수는 &mut 레퍼런스로 캡처된다.
  • 캡처된 변수가 이동한다면, 이 변수는 값에 의한 이동으로 캡처된다. (주의: Copy 타입의 캡처는 & 레퍼런스만을 필요로 한다. 때문에 이 규칙은 오직 non-Copy 타입에만 적용된다.)
이 알고리즘은 기술적으로 다소 복잡해 보이지만, 오너십/대여를 정확히 사용하는, 경험 있는 러스트 프로그래머의 멘탈 모델에 정확히 들어맞는다. 사실 클로저가 "non-escaping" 이라면, 즉 클로저가 자신이 정의된 스택 프레임을 절대 떠나지 않는다면, 이 알고리즘은 완벽하다고 생각된다: 코드 컴파일에는 캡처에 대한 어떠한 주석도 필요하지 않다.

요약해서, 컴파일러는 클로저의 변수 캡처에 있어 클로저 안에서 변수를 원하는 만큼, 의도한대로 사용하면서도, 클로저 밖에서도 변수를 계속 사용한다는 관점으로, 최소한의 제약을 두는 방식을 취한다(& 가 가장 선호되고, 다음은 &mut, 그리고 마지막이 값에 의한 캡처). 그리고 이 분석은 각 변수에 변수 단위로 이루어진다(per-variable basis):

struct T { ... }

fn by_value(_: T) {}
fn by_mut(_: &mut T) {}
fn by_ref(_: &T) {}

let x: T = ...;
let mut y: T = ...;
let mut z: T = ...;

let closure = || {
by_ref(&x);
by_ref(&y);
by_ref(&z);

// 'y' 와 'z' 가 적어도 '&mut' 레퍼런스로 캡처되도록 한다.
by_mut(&mut y);
by_mut(&mut z);

// 'z' 가 값에 의해 캡처되도록 한다.
by_value(z);
};

유연성에 초점을 두자: x 는 공유 레퍼런스로 캡처되기 때문에 이 클로저가 존재하는(유효한) 동안에도 사용할 수 있다. 그리고 y 는 뮤터블 레퍼런스로 대여되었기 때문에 이 클로저가 스코프를 벗어나 사라져야만 사용할 수 있다. z 값으로 캡처되기 때문에 이 클로저가 사라진 뒤에도 어디에서도 다시 사용할 수 없다.


컴파일러는 대강 다음과 같은 코드를 생성한다:


struct Environment<'x, 'y> {
x: &'x T,
y: &'y mut T,
z: T
}

/* impl of FnOnce for Environment */

let closure = Environment {
x: &x,
y: &mut y,
z: z
};


이 구조체는 러스트의 타입 시스템의 모든 기능을 사용하여, 실수로 댕글링 레퍼런스를 생성하거나 이미 해제된 메모리를 사용하거나 클로저를 오용하여 메모리의 안전을 침해하는 등의 현상을 방지한다. 문제가 될 수 있는 코드가 있다면 컴파일러가 잡아낼 것이다.




move 와 이스케이프


위에서, 추론 알고리즘이 non-escaping 클로저에 대해서는 완벽하다고 했다. 이는 다른 한편으로, escaping 클로저에 대해서는 완벽하지 않다는 의미이다.


클로저가 escaping 이면, 즉 클로저가 자신이 생성된 스택 프레임을 떠나게 된다면, 이 클로저는 자신이 떠나는 스택 프레임 안의 어떠한 레퍼런스도 가지고 있지 않아야 한다. 레퍼런스를 가지고 있게 된다면 클로저가 스택 프레임을 벗어날 때 이는 댕글링 레퍼런스가 될 것이며, 이건 매우 나쁜 일이다. 다행히도 컴파일러는 이런 현상이 발생할 여지가 있으면 에러를 일으킨다. 그런데, 클로저를 리턴하는 일은 유용하면서도 가능해야 하는 일이다. 예로:


/// `x` 를 더하는 클로저를 리턴한다.
fn make_adder(x: i32) -> Box<Fn(i32) -> i32> {
Box::new(|y| x + y)
}

fn main() {
let f = make_adder(3);

println!("{}", f(1)); // 4
println!("{}", f(10)); // 13
}


상당히 괜찮다... 컴파일이 안된다는 것만 빼면:


...:3:14: 3:23 error: closure may outlive the current function, but it borrows `x`, which is owned by the current function [E0373]

...:3     Box::new(|y| x + y)

                   ^~~~~~~~~

...:3:18: 3:19 note: `x` is borrowed here

...:3     Box::new(|y| x + y)

                       ^

 

모든걸 명시적 구조체로 작성하면 문제는 명확해진다: x 가 필요한 부분은 + 연산으로, 레퍼런스에 의한 캡처이면 족하다. 그래서 컴파일러는 이를 다음과 같은 코드로 추론해낸다:


struct Closure<'a> {
x: &'a i32
}

/* impl of Fn for Closure */

fn make_adder(x: i32) -> Box<Fn(i32) -> i32> {
Box::new(Closure { x: &x })
}


xmake_adder 의 끝에서 스코프를 벗어나게 되기 때문에, 이 레퍼런스를 가진 요소를 make_adder 밖으로 리턴할 수 없다.


그럼 이 문제를 어떻게 해결할 것인가? 컴파일러가 방법을 제시해주면 좋을 것이다.


사실, 위에서 나는 에러 메시지의 마지막 두 줄을 생략했었다:


...:3:14: 3:23 help: to force the closure to take ownership of `x` (and any other referenced variables), use the `move` keyword, as shown:

...:      Box::new(move |y| x + y)


새로운 키워드의 등장이다. move 는 클로저 정의의 앞에 위치하여 클로저에서 캡처하는 모든 변수를 값에 의해 캡처되도록 한다. 이전 섹션으로 돌아가서, 만약 코드가 let closure = move || { /* 같은 코드 */ } 형태였으면, 클로저 환경 구조체는 이렇게 달라졌을 것이다:


struct Environment {
x: T,
y: T,
z: T
}


모든 변수를 값으로 캡처하는 일은 엄밀히 말해서 레퍼런스에 의한 캡처보다 일반적인 것이다: 러스트에서 레퍼런스 타입은 일급(first-class)으로 취급된다. 때문에, '레퍼런스에 의한 캡처' 는 사실 '레퍼런스를 값으로 캡처' 와 동일한 말이다. 그래서, C++ 과는 달리, 여기에는 레퍼런스에 의한 캡처와 값에 의한 캡처의 근본적인 구별이 있다. 그리고 러스트의 분석은 필수가 아니다: 이 사실은 프로그래머을 편하게 해준다.


다음 코드는 move 를 사용하면서 레퍼런스를 캡처하여 첫 번째 버전과 동일한 동작, 동일한 환경을 가진다:


let x: T = ...;
let mut y: T = ...;
let mut z: T = ...;

let x_ref: &T = &x;
let y_mut: &mut T = &mut y;

let closure = move || {
by_ref(x_ref);
by_ref(&*y_mut);
by_ref(&z);

by_mut(y_mut);
by_mut(&mut z);

by_value(z);
};


캡처된 변수들은 클로저 몸체에서 사용되는 변수와 똑같다. C++ 11 에서와 같은 세분화된 캡처 목록은 존재하지 않는다. [=] 캡처 목록은 move 키워드로 존재하지만, 이게 전부다.


이제 우린 make_adder 에서의 문제를 해결할 수 있다. move 를 사용함으로써 컴파일러의 암시적 레퍼런스 추가를 막고, 클로저가 스택 프레임에 묶이지 않도록 한다. 컴파일러의 권고에 따라 Box::new(move |y| x + y) 로 코드를 수정한다:


struct Closure {
x: i32
}

/* impl of Fn for Closure */

fn make_adder(x: i32) -> Box<Fn(i32) -> i32> {
Box::new(Closure { x: x })
}


컴파일러는 move 가 필요한 때를 인지하지 못한다는 건 명백하다. 그런데 help 메시지를 보면 컴파일러는 경우에 따라 move 가 언제 필요한지 충분히 인지할 수 있음을 암시한다. 불행하게도, 컴파일러의 move 추론을 기본 기능에 포함시키는 일은 클로저 바디의 내부를 분석하는 것 이상의 작업을 필요로 한다: 클로저 값이 어디에서/어떻게 사용되느냐를 고려하는 일은 기술적으로 더 복잡한 일일 것이다. 결국 help 메시지가 move 추론에 대한 최선의 노력의 결과이고, 추론 기능이 러스트의 기본 기능에는 들어갈 수 없다.




트레잇


클로저의 실제 '함수' 는 위에서 언급한 트레잇에 의해 다루어진다. 암시적 구조체 타입은 트레잇을 암시적으로 구현하며, 이렇게 구현된 트레잇이 실제 클로저 타입으로 동작한다.


예제를 보자: make_adder 예제에서 Fn 트레잇이 암시적 클로저 구조체에 대해 구현되었다.


impl Fn(i32) -> i32 for Closure {
fn call(&self, y: i32) -> i32 {
// |y| x + y
self.x + y
}
}


실제로는 여기에 Closure 에 대한 FnMut, FnOnce 의 암시적 구현이 존재하지만, Fn 은 이 클로저에 대한 '기본적인' 것이다.


트레잇은 세 가지가 있고, 여기서 7개의 트레잇 세트가 구현될 수 있다. 그런데 여기서는 그들 중 가장 중요한 세 가지를 본다:

  • Fn, FnMut, FnOnce
  • FnMut, FnOnce
  • FnOnce
왜 이런 세트가 나올까? 세 개의 클로저 트레잇은 실제로 세 가지 중첩된 세트이다: Fn 을 구현하는 모든 클로저는 FnMut 를 구현할 수 있다(&self 가 가능하다면 &mut self 역시 가능하다; &*self). 그리고 같은 맥락에서 FnMut 를 구현하는 모든 클로저는 FnOnce 를 구현할 수 있다. 이런 계층 구조는 타입 레벨에서 강제된다. FnMut 의 정의는 다음과 같다:


pub trait FnMut<Args>: FnOnce<Args> {
...
}


한마디로: FnMut 를 구현하는 어떤 것이든 FnOnce 를 반드시 구현한다.


컴파일러가 어떤 트레잇을 구현하느냐를 추론하는 데에 있어 어떤 미묘한 점은 없다. 컴파일러는 구현할 수 있는 한 모든 트레잇을 구현한다. 이런 성질은 '최대한의 유연성 제공' 규칙으로 유지된다. 이 규칙은 캡처 타입의 추론에 적용되는데, 구현하는 트레잇이 많다는 것은 그만큼 많은 옵션이 존재할 수 있음을 의미하기 때문이다. Fn* 트레잇의 집합이 의미하는 것은 이 규칙은 언제나 위에 나열된 세 가지 트레잇 집합 목록 중 하나를 구현하게 한다는 것이다.


예제로, 아래 코드는 Fn 구현이 불가능하지만 FnMutFnOnce 는 가능한 경우를 보여준다:


let mut v = vec![];

// nice form
let closure = || v.push(1);

// explicit form
struct Environment<'v> {
v: &'v mut Vec<i32>
}

// let's try implementing `Fn`
impl<'v> Fn() for Environment<'v> {
fn call(&self) {
self.v.push(1) // error: cannot borrow data mutably
}
}
let closure = Environment { v: &mut v };


& &mut ... 를 변경하는 일은 불가능하다. 그리고 &self 는 그 외부 공유 영역을 참조한다. &mut self 혹은 self 였으면 아무 문제가 없을 것이다: 전자 쪽이 더 유연성이 좋으니, 컴파일러는 FnMut 를 구현한다. (물론 FnOnce 도)


이와 유사하게, closure|| drop(v); 라면, 즉 v 를 밖으로 던진다면, 이 경우는 FnFnMut 이 불가능할 것이다. 왜냐하면 &self&mut self 로 이런 동작을 수행하는 일은 대여된 데이터의 오너십을 훔치는 일이니까.




유연성


러스트의 목표 중 하나는 프로그래머에게 선택권을 남겨두면서도, 그들의 코드를 효율적이게 하고, 추상화를 컴파일하여 빠른 머신 코드를 생성하는 일이다. 클로저의 유니트 구조체 타입와 트레잇/제너릭 디자인이 여기의 핵심이 된다.


각 클로저는 저마다의 타입을 가지기 때문에, 클로저를 사용할 때 의무적인 힙 할당은 필요하지 않다: 위에서 보여졌듯, 클로저의 변수 캡처는 변수를 구조체 안으로 곧장 위치시킨다. 이는 러스트가 C++ 11 과 공유하는 성질이다. 클로저는 기본적으로 bare-mental 환경을 포함한 어떤 환경에서든 사용할 수 있도록 허용된다.


유니크 타입이란 서로 다른 클로저를 함께 사용할 수 없음을 의미한다. 예를 들어, 각각 다른 클로저를 담고 있는 벡터를 만들 수는 없다. 클로저들은 각각 서로 사이즈가 다르며, 호출 방식도 다르다. (각각의 클로저들은 내부 코드가 서로 다르기에, 호출하는 함수도 서로 다르다.) 다행히도 트레잇을 사용하여 클로저 유형을 추상화할 수 있다. 트레잇 객체를 통해 이런 기능과 이점을 '온 디맨드로' 선택할 수 있다: Box<Fn(i32) -> i32> 를 리턴한다.


let mut closures: Vec<Box<Fn()>> = vec![];

let text = "second";

closures.push(Box::new(|| println!("first")));
closures.push(Box::new(|| println!("{}", text)));
closures.push(Box::new(|| println!("third")));

for f in &closures {
f(); // first / second / third
}


유니크한 타입과 제너릭의 또다른 이점은, 기본적으로 컴파일러는 클로저가 호출되는 위치에서 무엇을 하는지 전부 알고 있기 때문에, 인라이닝과 같은 최적화를 수행할 수 있다는 것이다. 예를 들어, 아래 스니펫은 동일한 코드로 컴파일된다:


x.map(|z| z + 3)

match x {
Some(z) => Some(z + 3),
None => None
}


(내가 위의 두 코드를 각각 서로 다른 함수 안에 두고, 각 함수를 하나의 파일에 두어 컴파일 해보았다. 컴파일러는 두 번째 함수에서 첫 번째 함수를 호출하는 방식으로 최적화를 수행했다.)


이건 러스트가 monomorphisation 을 통해 제너릭을 구현하는 방법 때문이다. 제너릭 함수는 주어진 타입 파라미터마다 각각의 호출 형태에 맞는 코드를 생성하는 방식으로 컴파일된다. 불행히도, 이 최적화가 언제나 좋은 것은 아니다. 이 방식은 하나의 함수를 두고 비슷한 함수 여러개를 복제해내면서 거대한 코드를 낳게 된다. 이것을 막는 방법으로는 트레잇 객체가 있다. 트레잇 객체를 이용하면 제너릭 타입에 여러 클로저가 함께 사용되더라도 동적으로 디스패치된 클로저를 사용하여 함수의 사본이 하나만 존재하는지 확인할 수 있다.


fn generic_closure<F: Fn(i32)>(f: F) {
f(0);
f(1);
}

generic_closure(|x| println!("{}", x)); // A
generic_closure(|x| { // B
let y = x + 2;
println!("{}", y);
});


fn closure_object(f: &Fn(i32)) {
f(0);
f(1);
}

closure_object(&|x| println!("{}", x));
closure_object(&|x| {
let y = x + 2;
println!("{}", y);
});


마지막 바이너리는 generic_clocure 의 두 가지 복사본을 가지게 된다. 하나는 A 에 대해서, 다른 하나는 B 에 대해서. 그런데 closure_object 의 복사본은 하나이다. 사실 포인터에 대한 Fn* 트레잇 구현체가 존재하기 때문에 트레잇 객체를 generic_closure 에 바로 사용할 수도 있다. 예로, generic_closure((&|x| {... }) as &Fn(_)) 으로 사용하는 것이 가능하다. 고차원 함수의 사용자는 스스로 원하는 방식을 택하여 사용할 수 있다.


클로저의 강력함은 사용자로 하여금 세부 사항을 직접 작성하는 것과 비교하여 성능 면에서 손실이 없으면서도 높은 수준의 "fluent" 한 API 를 작성하도록 해준다는 것이다. 이것의 좋은 예는 iterators 이다: 사용자는 C 수준의 효율성으로 최적화되는 mapfilter 같은 어댑터 함수를 체이닝 방식으로 호출할 수 있다. 







'Rust' 카테고리의 다른 글

클로저(심화)  (0) 2018.03.07
라이프타임  (0) 2018.02.28
트레잇(Traits)  (0) 2018.02.28
제너릭 타입  (0) 2018.02.27
에러 핸들링(Result)  (0) 2018.02.26

클로저



클로저의 트레잇에 관하여 매우 혼동되는 부분이 있어, 스택오버플로 검색 결과 좋은 답변 발견. 그대로 옮겨 적어둔다.



주소: https://stackoverflow.com/questions/30177395/when-does-a-closure-implement-fn-fnmut-and-fnonce

원문:


The traits each represent more and more restrictive properties about closures/functions, indicated by the signatures of their call_... method, and particularly the type of self:

  • FnOnce (self) are functions that can be called once,
  • FnMut (&mut self) are functions that can be called if they have &mut access to their environment
  • Fn (&self) are functions that can still be called if they only have & access to their environment.

A closure |...| ... will automatically implement as many of those as it can.

  • All closures implement FnOnce: a closure that can't be called once doesn't deserve the name. Note that if a closure only implements FnOnce, it can be called only once.
  • Closures that don't move out of their captures implement FnMut, allowing them to be called more than once (if there is unaliased access to the function object).
  • Closures that don't need unique/mutable access to their captures implement Fn, allowing them to be called essentially everywhere.

These restrictions follow directly from the type of self and the "desugaring" of closures into structs (described in Finding Closure in Rust).



번역:

각 트레잇은 자신의 call_... 메서드 시그니처와 특정한 self 의 타입에 의해 나타나는, 클로저/함수의 제한적인 성질을 표현한다.

  • FnOnce (self) 는 한 번 호출 가능한 함수이다.

  • FnMut (&mut self) 는 스코프의 아이템에 &mut 으로 접근하는 함수이다. (스코프 변수에 값을 변경하는 동작이 있는 경우를 의미하는 듯)

  • Fn (&self) 는 스코프의 아이템에 오직 & 으로만 접근하는 함수이다. (스코프 변수에 값을 변경하는 동작이 없는 경우를 의미하는 듯)

클로저는 위 트레잇 중 가능한 많은 트레잇을 구현한다 (조건만 만족한다면 하나의 클로저가 다수의 트레잇의 구현체가 될 수 있음)
  • 모든 클로저는 FnOnce 를 구현한다: 한 번 호출할 수 없는 클로저는 이름을 가질 필요가 없다. 클로저가 오직 FnOnce 트레잇만을 구현한 경우, 이 클로저는 오직 한 번만 호출 가능하다.
  • 스코프의 아이템을 이동시키지 않는 클로저는 FnMut 를 구현한며, 한 번 이상 호출 가능하다. (함수 객체에 별칭이 아닌 접근 방법이 있는 경우)
  • 스코프의 아이템에 유니크/뮤터블 접근이 필요하지 않은 클로저는 Fn 을 구현하며, 기본적으로 어디서든 호출 가능하다.
이러한 제한 사항은 self 의 타입과 구조체에 대한 클로저의 'desugaring' 을 직접적으로 따른다.




'Rust' 카테고리의 다른 글

클로저(심화)  (0) 2018.03.07
라이프타임  (0) 2018.02.28
트레잇(Traits)  (0) 2018.02.28
제너릭 타입  (0) 2018.02.27
에러 핸들링(Result)  (0) 2018.02.26

라이프타임



라이프타임은 어쩌면 러스트에서 가장, 적어도 지금까지 다룬 내용 중 개념적으로 가장 이해하기 어려운 파트일 수 있음. 이해한 것을 정리하는 느낌으로 라이프타임이 무엇이고, 왜 필요한지 적어본다. (단, 라이프타임 파라미터의 문법에 대해 세세하게 다루지는 않는다.)


라이프타임은 프로그래밍 언어에서 자주 등장하는, 변수 혹은 레퍼런스의 '유효 범위(이하 스코프)' 라고 생각하면 된다. 


차근차근 살펴보자.


먼저, 러스트의 모든 변수와 레퍼런스는 각자의 스코프를 가진다. 변수의 스코프는 자신이 선언/생성된 스코프와 그 하위 스코프 까지이고, 그 범위를 벗어난 위치에서의 접근은 컴파일 에러를 일으킨다. (레퍼런스의 경우 자신이 생성된 스코프에서 다른 스코프로 '이동' 할 가능성이 있다.)


fn main() {                                 // 메인 스코프

{ // 또 다른 스코프가 열림
let s1 = "my string".to_string(); // 이 스코프에서 변수와 스트링 생성.
} // 스코프가 끝나면서 변수 s1 은 폐기되고, 참조하던 스트링 메모리도 해제됨.
println!("{}", s1); // 변수 s1 의 유효 스코프 밖에서의 접근 -에러!
}


에러 메시지는 다음과 같다.


error[E0425]: cannot find value `s1` in this scope



그렇다면 아래 코드는 어떨까?


fn main() {
let s1;

{
let s2 = "my string".to_string();
s1 = s2;
}
println!("{}", s1);
}


위 코드의 아웃풋은 "my string" 이다. 스트링이 생성된 스코프가 끝난 뒤 스코프 밖에서 스트링에 접근했는데 컴파일-실행에 아무런 문제가 없다. 이것은 왜냐하면 변수 s2 이 폐기되기 전에 s1 에 레퍼런스를 '이동' 시켰기 때문이다. s2 은 폐기되어도 바깥 스코프의 s1 에 스트링 레퍼런스를 넘겼으니 스트링은 살아있고, 변수 s1 을 통한 접근에 문제가 없다.



이제 아래 코드를 본다.


fn main() {

let s1;

{
let s2 = "my string".to_string();
s1 = &s2;
}
println!("{}", s1);
}


위 코드는 컴파일 에러를 일으킨다. 에러 메시지는 다음과 같다.


error[E0597]: `s2` does not live long enough


이게 무슨 말인가. 여기서부터 라이프타임의 개념이 시작된다.


라이프타임은 레퍼런스의 '이동' 이 아닌, '대여' 에 관한 것이다. 원본 레퍼런스가 존재하고, 이 레퍼런스를 다른 변수에 빌려주었는데, 이 상태에서 원본 레퍼런스가 사라질 '가능성' 이 있는 코드는 러스트에서 컴파일 에러를 일으킨다. 위 코드에서는 변수 s1 의 스코프가 s2 의 스코프보다 넓다. s2 이 스코프가 끝나 사라져도 s1 은 여전히, '더 오래' 존재한다. 그런데 s1 은 s2 의 레퍼런스를 '대여' 한 상황이다. 이 상태에서 원본 레퍼런스를 가진 s2 이 먼저 사라진다면? s1 이 대여한 레퍼런스도 무효화된다. 변수 s1 의 사용자는 여전히 s1 이 스트링을 참조하고 있다고 생각하지만, 이 참조는 쓰레기 참조이다. 전형적인 댕글링 레퍼런스(댕글링 포인터)의 발생이며, use-after-free 케이스이다. 러스트는 컴파일 타임에 댕글링 레퍼런스 현상을 막고자 한다.


라이프타임은 댕글링 레퍼런스를 어떻게 막을 것인가? 에 대한 러스트의 해답이다.


처음에, 모든 변수와 레퍼런스는 자신의 스코프를 가진다고 했다. 이 스코프를 라이프타임이라 생각하자. 모든 변수와 레퍼런스는 생성되면서 자신의 라이프타임을 가진다.


{
let r; // -------+-- 'a
// |
{ // |
let x = 5; // -+-----+-- 'b
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
// |
// -------+
}


위 코드에서, 변수 r 의 라이프타임은 'a 이다. 변수 x 의 라이프타임은 'b 이다. 라이프타임 'a 의 범위가 'b 의 범위보다 넓다. 이 말은 즉, 라이프타임 'a 의 변수는 라이프타임 'b 보다 오래 존재한다. 때문에 라이프타임 'b 의 변수는 라이프타임 'a 의 레퍼런스를 대여해도 괜찮지만, 반대로 라이프타임 'a 의 변수는 라이프타임 'b 의 레퍼런스를 대여할 수 없다. (대여된 레퍼런스가 대여한 레퍼런스보다 먼저 사리지면 대여한 레퍼런스가 가진 것은 무효한 참조(댕글링 레퍼런스)가 되기 때문에.)


간단히 말해서, 대여되는 쪽이 대여 해가는 쪽보다 오래(or 동일하게) 살아 있어야 한다.


{
let x = 5; // -----+-- 'b
// |
let r = &x; // --+--+-- 'a
// | |
println!("r: {}", r); // | |
// --+ |
}


위 코드에서, 변수 x 의 라이프타임은 'b 이다. 변수 r 의 라이프타임은 'a 이다. 'a 보다 'b 이 더 길다. 고로, 변수 x 가 r 보다 오래(사실 여기서는 똑같이) 살고, r 은 x 의 레퍼런스를 대여할 수 있다.


간단하지 않은가? 사실 다른 언어에서도 마찬가지인 이야기이고, 특별할 게 없다. 그럼 넘어가보자.



함수에서의 라이프타임



fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}


위 코드는 아래와 같은 컴파일 에러를 일으킨다.


error[E0106]: missing lifetime specifier

 --> src/main.rs:6:33

  |

6 | 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 가 될 수도 있다. 둘 중 무엇이 될지는 런타임에 파라미터 값에 따라 결정된다. 즉 컴파일 타임에 미리 알 수는 없다. 당연하다. 그런데 이게 왜 문제가 될까?

아래 코드를 보면 답이 나온다. (코드에서 하드코딩된 스트링 값은 실제 런타임에는 알 수 없는 값이라고 생각하고 봐야 한다.)

fn main() {

let s1 = "value of s1".to_string();
let result;
{
let s2 = "value of s2".to_string();
result = longest(&s1, &s2);
}

println!("{}", result);
}


감이 오는가? 코드의 스코프 구조에 집중해서 보도록 하자.


함수 longest 의 입장에서는 파라미터로 어떤 라이프타임을 가진 레퍼런스가 전달될지 모른다. 그리고 리턴값을 받는 주체도 어떤 라이프타임을 가졌을지 알 수 없다. 위 코드에서 파라미터가 된 s1, s2 의 라이프타임은 명백히 다르다. longest 는 두 값의 길이를 비교하여 큰 쪽을 레퍼런스 그대로 리턴하는데, 그 리턴값은 받는 변수 result 의 라이프타임은 s1 과 동일하다. 앞서 언급한 규칙을 생각해보자. 이 상황에서, 함수 longest(&s1, &s2); 의 실행 결과로 &s1 이 리턴된다면 어떨까? 리턴값(레퍼런스)은 result 가 받는데, result 와 s1 의 라이프타임이 동일하기 때문에 문제가 없다. 그럼 &s2 이 리턴된다면 어떨까? 이걸 result 가 받는다면? 규칙에서 대여되는 쪽이 대여해가는 쪽보다 오래(or 동일하게) 살아야 한다고 했는데, 대여되는 쪽인 &s2 의 라이프타임이 대여해가는 쪽인 result 보다 짧게 된다. 이 코드를 컴파일 허용한다면, 댕글링 레퍼런스가 생길 여지를 허용하는 셈이 되는 것이다.


러스트는 이렇게 댕글링 레퍼런스를 야기할 여지가 있는 코드를 컴파일에서 걸러낸다. 그리고 하나의 함수는 런타임에 여러 랜덤한 값을 파라미터로 받게 될 수 있고, 특히 다양한 스코프 구조에서 실행될 수 있다. 때문에 댕글링 레퍼런스가 생길 수 있는 코드인지 체크하는 작업은 하나의 독립된 함수 단위에서부터 이루어진다. 간단히 말해, 정작 당장 함수를 호출하는 코드가 전혀 없더라도, 그 함수의 생김새 만으로도 이 함수가 댕글링 레퍼런스를 야기할 수 있다고 판단하면 러스트는 컴파일 에러를 던진다.


그럼 함수 longest 는 어떻게 해야 컴파일 할 수 있나?


아래와 같이 라이프타임 파라미터를 정의하면 된다. (처음에 언급했듯, 라이프타임 파라미터의 문법에 대해서는 다루지 않는다.)


fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}


위 라이프타임 파라미터의 의미는, "함수 longest 에 'a 라는 라이프타임이 있고, 레퍼런스 아규먼트 s1, s2, 그리고 리턴 레퍼런스는 같은 라이프타임을 가진다." 라는 것이다. 설명했듯이 컴파일러는 longest 함수의 리턴값이 s1 이 될지 s2 이 될지 알 수 없으므로, 이에 대한 조언을 해주는 셈이다. 컴파일러는 이를 이해하고 longest 를 컴파일한다. 


여기서 절대 착각해서는 안된다. 라이프타임 파라미터는 절대로 라이프타임을 변경하는 것이 아니다. 파라미터의 라이프타임은 호출 코드의 문맥에서 결정되는 것이지, 함수 안에서 변경 가능한 것이 아니다. 라이프타임 파라미터는 컴파일러에게 컴파일을 위한 힌트를 줄 뿐, 프로그램의 런타임 동작에 어떠한 영향도 주지 않는다.


이렇게 라이프타임 파라미터를 정의해도 아래와 같은 코드는 여전히 컴파일 에러를 일으킨다.


fn main() {

let s1 = "value of s1".to_string();
let result;
{
let s2 = "value of s2".to_string();
result = longest(&s1, &s2);
}

println!("{}", result);
}

fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str {
if s1.len() > s2.len() {
s1
} else {
s2
}
}


그런데 컴파일 에러 메시지가 아까와 좀 다르다.


error[E0597]: `s2` does not live long enough

  --> src/main.rs:7:32

   |

7  |         result = longest(&s1, &s2);   

   |                                ^^ borrowed value does not live long enough

8  |     }

   |     - `s2` dropped here while still borrowed

...

11 | }

   | - borrowed value needs to live until here


그렇다, 함수 longest 는 컴파일이 된 것이다! 동시에, 라이프타임 파라미터를 정의했어도 s2 의 라이프타임을 지적하는 에러는 발생한다는 점에 주목해야 한다. 다시 한 번 말하건데, 라이프타임 파라미터를 똑같이 정의한다고 해서 레퍼런스의 실제 라이프타임이 절대로 같아지지 않는다.


그렇다면 라이프타임 파라미터가 어떤 역할을 했길래 함수 longest 가 컴파일 됐을까? 라이프타임 생략 규칙




Elision Rules (생략 규칙)



라이프타임 생략 규칙이라는 것이 있다. 라이프타임을 항상 명시적으로 정의하는 일은 번거로운 일이기에, 몇 가지 아주 명백한 상황에서는 라이프타임 파라미터를 명시하지 않아도 러스트가 알아서 라이프타임을 추론한다. 이 '명백한 상황' 을 판단하는 규칙이 elision rules(이하 생략 규칙) 이며, 이 규칙에 해당되지 않으면 라이프타임 파라미터를 명시적으로 정의해주어야 한다. 그렇지 않으면 컴파일 에러가 발생한다.


규칙은 다음과 같다.


1. 레퍼런스 타입의 파라미터는 모두 각자의 독립된 라이프타임을 가진다. 

-> 예를 들어, 함수 fn foo(x: &i32) 는 fn foo<'a>(x: &'a i32) 가 되고, fn foo(x: &i32, y: &i32) 는 fn foo<'a, 'b>(x: &'a i32, y: &'b i32) 가 된다.


2. 파라미터가 단 하나일 경우, 리턴 레퍼런스는 동일한 라이프타임 파라미터를 가진다. 

-> 예를 들어, fn foo(x: &i32) -> &i32 는 fn foo<'a>(x: &'a i32) -> &'a i32 가 된다.


3. 다수의 파라미터가 있는데 그 중 하나가 &self 혹은 &mut self 인 경우(즉 메서드인 경우), self 의 라이프타임은 리턴 레퍼런스의 라이프타임이 된다.


몇 가지 케이스로 규칙을 이해해보자. 


일단 아래 코드가 있다. 컴파일러가 어떤 방식으로 여기에 규칙을 적용하여 라이프타임을 추론할까?


fn first_word(s: &str) -> &str {


첫 번째 규칙에 따라, 파라미터 s 는 라이프타임을 가진다.


fn first_word<'a>(s: &'a str) -> &str {


그리고 이 코드는 두 번째 규칙도 적용된다. 컴파일러는 함수 first_word 의 라이프타임을 아래와 같이 추론하여 컴파일한다.


fn first_word<'a>(s: &'a str) -> &'a str {



파라미터가 두 개인 경우는 어떨까?


fn longest(x: &str, y: &str) -> &str {


첫 번째 규칙에 따라, 각 파라미터는 각자의 독립된 라이프타임을 가진다.


fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {


파라미터 수가 하나를 초과하기 때문에, 두 번째 규칙은 적용되지 못한다. 그리고 파라미터에 self 가 없기 때문에, 세 번째 규칙도 적용되지 못한다. 규칙을 모두 대입해도 리턴 파라미터의 라이프타임을 알 수 없다. 이제 컴파일 에러가 발생한다. 


알겠는가? 처음에 함수 longest 에 컴파일 에러가 발생한 이유는 이 때문이다. 그리고 후에 명시적 파라미터 정의를 통해서 파라미터의 라이프타임과 리턴값의 라이프타임을 연결해 주었기 때문에 정상적으로 컴파일 될 수 있었다. 함수에서의 라이프타임 파라미터의 역할은, 궁극적으로 다양한 레퍼런스 파라미터와 리턴 레퍼런스의 라이프타임을 연결해 주는 것이다. 


논리적으로, 레퍼런스가 리턴값인 함수에서 레퍼런스 파라미터 말고는 리턴할 수 있는 레퍼런스가 없다. 


fn return_invaild_reference(r1: &i32, r2: &i32) -> &i32 {


위와 같은 함수가 있다고 할 때, 리턴값은 반드시 r1 혹은 r2 가 오게 된다는 말이다. 왜냐하면 둘 말고 다른 레퍼런스, 즉 함수 안에서 생성된 또다른 레퍼런스를 리턴한다는 것은 그 시도 자체만으로도 이미 러스트의 스코프 범위 규칙을 위반하는 일이다. 함수가 종료되면서 그 안에서 생성된 레퍼런스는 모두 해제될태니까. 


때문에 레퍼런스 리턴은 파라미터를 통해 이루어져야 하고, 이를 위해서 파라미터 레퍼런스의 라이프타임과 리턴 레퍼런스의 라이프타임이 동일해야 한다.



구조체에서의 라이프타임


구조체에서의 경우는 더 단순하다. 구조체가 레퍼런스 타입 필드를 가지고 있다면, 무조건 라이프타임 파라미터가 명시되어야 한다.


struct ImportantExcerpt<'a> {
part: &'a str,
}

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 };
}


구조체 ImportantExcerpt 에 왜 라이프타임이 필요한지는 지금까지 알아본 내용에 비추어 보면 당연한 일이다. 구조체가 자신이 참조하는 메모리보다 오래 살아있으면 안되니까.



메서드 정의에서의 라이프타임


메서드를 정의할 때, 메서드가 속한 구조체의 필드 또는 메서드의 아규먼트 와 리턴값이 라이프타임 파라미터가 필요한 경우라면 impl 에도 라이프타임 파라미터가 필요하다. 실제로 구현되는 메서드가 라이프타임 파라미터를 필요로 하지 않더라고, 그 구조체가 라이프타임 파라미터를 가진다면 impl 에도 라이프타임 파라미터는 필요하다.


impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
3
}
}


위 코드의 경우, 메서드 level 은 생략 규칙에 따라 라이프타임 파라미터가 필요하지 않다. 하지만 구조체 ImportantExcerpt 가 라이프타임 파라미터를 필요로 하기에, impl 에도 똑같이 이를 지정해 주어야 한다.


impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}


위 코드의 메서드는 파라미터가 둘이다. 첫 번째 생략 규칙에 따라 두 파라미터는 각각 독립된 라이프타임 파라미터를 가진다. 그리고 세 번째 규칙에 따라 &self 파라미터의 라이프타임이 리턴 레퍼런스의 라이프타임이 된다.



스태틱 라이프타임


스태틱 라이프타임은 좀 특별하다. 스태틱 라이프타임은 프로그램 전체에 걸쳐 유효하다. 스태틱 라이프타임이 적용된 대표적인 예는 스트링이다. 모든 스트링 리터럴은 스태틱 라이프타임을 가지고 있다.



제너릭 타입, 트레잇 바운드, 라이프타임을 함께 사용하기


말이 필요없고, 아래 코드처럼 사용 가능하다.


use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where T: Display
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}




'Rust' 카테고리의 다른 글

클로저(심화)  (0) 2018.03.07
클로저(심화)  (0) 2018.03.07
트레잇(Traits)  (0) 2018.02.28
제너릭 타입  (0) 2018.02.27
에러 핸들링(Result)  (0) 2018.02.26

트레잇(Traits)



러스트의 트레잇은 굳이 다른 언어로 말하자면 인터페이스라 할 수 있음.



트레잇의 규칙 및 성질


  1. 트레잇은 하나의 타입으로 취급됨. 같은 트레잇을 구현한 타입은 트레잇으로 같은 타입으로 동작할 수 있음. (제너릭)
  2. 트레잇은 추상 메서드, 디폴트 메서드를 가질 수 있음.
  3. 트레잇을 구현하는 타입은 반드시 트레잇의 모든 추상 메서드를 구현해야 함. (디폴트 메서드는 반드시 구현할 필요 없음)
  4. 한 타입이 트레잇의 디폴트 메서드를 구현하면 오버라이딩 되어 구현된 메서드가 실행됨.
  5. 한 타입은 여러 트레잇을 구현할 수 있음.
  6. 한 타입이 여러 트레잇을 구현할 때, 메서드 시그니처가 겹치면 컴파일 에러 발생.
  7. 외부 타입으로 로컬 트레잇을 구현할 수 있음.
  8. 로컬 타입이 외부 트레잇을 구현할 수 있음.
  9. 외부 타입으로 외부 트레잇을 구현할 수 없음. (orphan rule)



트레잇 정의 및 구현


트레잇 정의.


pub trait Summarizable {
fn summary(&self) -> String;

fn summary_nicer(&self) { // 디폴트 메서드(구현할 필요 없음)
// do something..
}
}


그리고 아래와 같이 구현. summary_nicer 메서드는 디폴트라 구현하지 않아도 됨.


pub struct NewsArticle {

}

impl Summarizable for NewsArticle {
fn summary(&self) -> {
// do something..
}
}



트레잇 제한


아래는 트레잇을 타입으로 하여 메서드 파라미터 제한.


pub fn notify<T: Summarizable>(item: T) {
println!("Breaking news! {}", item.summary());
}



아래처럼 and 조건을 걸듯, 여러 트레잇을 구현한 타입으로 제한할 수도 있음.


fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {



위처럼 파라미터가 많고 제한 트레잇이 많은 경우 보기가 불편함. 아래와 같이 where 키워드를 사용하여 트레잇을 예쁘게 명시할 수 있음.


fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{



아래와 같이 impl 에 트레잇의 제너릭 타입과 타입의 제너릭 타입을 지정하여 제한된 메서드 구현을 할 수 있음.


trait movable<T> {
fn goto(&self);
}

struct Location<T> {
X: i32,
y: i32,
loc_type: T,
}

impl movable<i32> for Location<i32> {
fn goto(&self) {
// do something..
}
}


위의 경우, 같은 Location 인스턴스라도 Location<i32> 타입만 movable 을, 그것도 movable<i32> 를 구현하여 movable<i32> 타입으로 동작할 수 있음. Location<String> 타입 인스턴스는 goto 메서드를 호출할 수 없음.


impl 에 다중 트레잇 타입을 한정하는 경우 아래와 같이 하면 됨.


struct Location<T> {
X: i32,
y: i32,
loc_type: T,
}

impl<T: Display + PartialOrd> Location<T> {
fn do_something(&self) {
// do something..
}
}


Display + PartialOrd 트레잇을 모두 구현한 타입의 Location 만이 do_something 메서드를 구현.




'Rust' 카테고리의 다른 글

클로저(심화)  (0) 2018.03.07
라이프타임  (0) 2018.02.28
제너릭 타입  (0) 2018.02.27
에러 핸들링(Result)  (0) 2018.02.26
에러 핸들링(panic)  (0) 2018.02.26

제너릭 타입



제너릭 타입은 자바에서의 것과 크게 다르지 않음.


fn largest_i32(list: &[i32]) -> i32 {
let mut largest = list[0];

for &item in list.iter() {
if item > largest {
largest =item;
}
}
largest
}

fn largest_char(list: &[char]) -> char {
let mut largest = list[0];

for &item in list.iter() {
if item > largest {
largest =item;
}
}
largest
}


위 두 함수를 제너릭으로 바꾸자면,


fn largest<T>(list: &[T]) -> T {


이렇게 쓰면 됨.


그런데 위와 같이 바꾸고 컴파일 시도하면 에러가 남. 이유는 타입 T 가 어떻게 값을 비교해야 하는지 정의되지 않았기 때문에 item > largest 코드가 에러를 일으킨 것. 이에 관하여는 후에 자세히 다룸.



구조체에 제너릭 타입 사용


특별할 것 없음.


struct Point<T> {
x: T,
y: T,
}

struct AnotherPoint<T, U> {
x: T,
y: U,
}



메서드에 제너릭 타입 사용


아래와 같이 사용.


struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}


아래는 조금 다름.


impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}


위의 경우, Point<T> 에서도 Point<f32> 타입에만 메서드를 구현한 것. Point<i32> 인스턴스를 생성한다면 그 인스턴스는 위의 메서드를 사용할 수 없음. 아래와 같이 일부 제너릭 타입만 한정하는 것도 가능.


struct Point<T, U> {
x: T,
y: U,
}

impl<T, i32> Point<T, i32> {
fn x(&self) -> &T {
&self.x
}
}


또, 아래와 같이 메서드에 다른 제너릭 타입을 부여하여 섞어 사용하는 것도 가능(사실 이건 자바에서도 가능..).


struct Point<T, U> {
x: T,
y: U,
}

impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}



제너릭의 성능 문제?


러스트의 제너릭은 런타임 비용이 없음. 컴파일 타임에 코드의 monorphization(단일 형태화) 을 수행함. 이는 제너릭 코드를 구체적인 타입 코드로 변환하는 일. 컴파일러는 사용자가 제너릭 코드를 작성하는 과정과 반대의 절차를 거쳐서 구체적인 코드를 생성해냄.


아래와 같은 코드가 있다면,


let integer = Some(5);
let float = Some(5.0);


컴파일러는 다음과 같은 코드를 만들어냄



enum Option_i32 {
Some(i32),
None,
}

enum Option_f64 {
Some(f64),
None,
}

fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}


이런 과정을 컴파일 타임에 모두 거치기 때문에 제너릭 타입에 런타임 시 타입 추론 등의 동작이 아예 발생하지 않음. 때문에 성능상의 비용은 없음.

'Rust' 카테고리의 다른 글

라이프타임  (0) 2018.02.28
트레잇(Traits)  (0) 2018.02.28
에러 핸들링(Result)  (0) 2018.02.26
에러 핸들링(panic)  (0) 2018.02.26
컬렉션(해시맵)  (0) 2018.02.25

Result 를 이용한 에러 핸들링



Result<T, E> 는 enum 타입이며, 주로 에러가 발생할 수 있는 함수에서 리턴 타입으로 사용됨. 


enum Result<T, E> {
Ok(T),
Err(E),
}

보다시피 제너릭 타입임.


Result<T, E> 타입에는 Ok, Err 이라는 요소가 있음. Option<T> 때와 비슷하게 생각하면 됨. Ok 인 경우는 함수(또는 메서드)의 동작이 정상적으로 수행된 경우, Err 인 경우는 비정상적인 상황을 만나 함수가 정상적으로 수행되지 못했음을 알리는 것.


Ok 는 안에 정상적인 함수의 리턴값을 담고, Err 은 에러값을 담음.


예제로 보자면,


use std::fs::File;

fn main() {
let f = File::open("hello.txt");
}


위 코드는 hello.txt 라는 파일을 찾아서 열기를 시도함. 그런데 hello.txt 파일이 존재하지 않아도 실행에 아무런 에러가 없음. 왜냐하면 File::open 함수는 Result<T, E> 를 리턴하기 때문. 바로 에러를 내고 프로그램이 죽어버리는 것이 아니라, 파일이 있을 경우 Ok 에 파일을 담아서 리턴하고, 파일이 없으면 Err 에 에러를 담아서 리턴하는 것. 사용자는 이걸 알고 경우에 맞는 처리를 하면 됨.


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);
},
};
}


위 코드는 File::open 함수가 파일을 정상적으로 찾고 열었으면 변수 f 에 파일을 넣고, panic! 을 던지고 프로그램을 종료함.


실제로 파일이 없으므로 실행 결과는 아래와 같음.




에러 종류에 따른 처리


에러는 다양한 종류가 있을 수 있고, 에러의 종류에 따라 대응하는 코드가 달라질 수 있음. 긴 말이 필요없고, 아래 코드를 보면 됨.


use std::fs::File;
use std::io::ErrorKind;

fn main() {
let f = File::open("hello.txt"); // 파일 오픈 시도

let f = match f {
Ok(file) => file, // 파일이 존재하고 읽는 데 문제가 없을 경우 file 리턴
Err(ref error) if error.kind() == ErrorKind::NotFound => { // 에러가 발생했는데 그 에러가 ErrorFind::NotFound 일 경우
match File::create("hello.txt") { // 새로운 파일을 만들기
Ok(fc) => fc, // 파일이 정상적으로 만들어졌으면 그 새 파일을 리턴
Err(e) => { // 파일 생성 중 에러가 발생했으면
panic!("Tried to create file but there was a problem: {:?}", e); // panic!
}
}
},
Err(e) => { // 파일 열기 중 발생한 에러가 ErrorKind::NotFound 가 아닌 다른 에러라면
panic!("There was a problem opening the file: {:?}", e); // panic!
},
};
}


읽기 시도하는 파일이 없을 경우 File::open 함수가 전달하는 에러는 std::io::ErrorKind 라는 enum 타입의 NotFound 요소임. 위와 같은 방법으로 에러의 종류에 따라 처리를 달리 할 수 있음. 


if error.kind() == ErrorKind::NotFound 코드를 match guard 라 함. 그리고 Err(ref error) 의 ref 는 error 가 match guard 에 사용되면서도 오너십을 잃지 않게 하기 위함임. 이에 대해서는 후에 자세히 다룸.



에러 처리 코드 줄이기


자주 사용되어 반복되는 에러 처리 코드를 줄이는 방법이 있음. unwrap/expect 를 사용하는 것.


unwrap 은 Result 가 Ok 이면 Ok 안의 값을 리턴하고, Err 이면 알아서 panic! 을 호출함.


fn main() {
let f = File::open("hello.txt").unwrap();
}


expect 는 unwrap 과 동일하게 동작하되, panic! 의 메시지를 다르게 지정할 수 있다는 차이만 있음.


let f = File::open("hello.txt").expect("Failed to open hello.txt");



에러 전파(propagating)


코드에서 에러가 발생할 경우 때로는 에러를 직접 처리하지 않고 그 코드를 호출한 코드에게 넘겨버리는게 좋을 때도 있음(자바에서 익셉션을 throw 할 때와 같이). 이럴 땐 다음과 같이 처리함.


fn read_file() -> Result<String, io::Error> {
let f = File::open("hello.txt").unwrap();

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);
}
}

읽을 파일이 없으면 더 이상 함수를 수행할 수 없으므로 에러를 그냥 던짐


보다시피, 당연히, 함수의 리턴 타입은 Result 가 와야 함. 위 함수는 에러가 발생하면 함수 호출 코드에게 에러 처리를 넘기고, 정상적으로 실행되면 파일의 내용을 스트링으로 리턴함(Ok(s))



에러 전파 코드 줄이기


에러 전파 코드는 자주 반복될 수 있음. '?' 를 사용해서 코드를 줄일 수 있음.


앞의 read_file 함수를 아래와 같이 줄일 수 있음.


fn read_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt");
let mut s = String::new(0;
f.read_to_string(&mut s)?;
Ok(s);
}


위의 read_file 함수는 이전의 함수와 거의 똑같이 동작함. 차이점이 있다면, '?' 의 동작 방식임. '?' 는 std 라이브러리의 From 트레잇에 정의된 from 함수를 사용함. 


코드 실행 중 에러가 발생하고 -> '?' 에 의해 from 함수가 호출되면 -> from 함수는 발생한 에러를 현재 함수의 리턴 타입에 맞는 에러로(예제에서는 io::Error로) 변환해줌. 이로 인해 한 함수가 여러가지 이유로 다른 에러가 발생해도 하나의 에러 타입으로 에러를 던질 수 있게 됨. 아무 에러나 이런 처리가 되는 것은 아니고, 에러 타입이 from 함수를 구현하여 이 에러가 어떻게 변환되어야 하는지 정의되어 있어야 함. 이 조건만 충족되면 '?' 는 알아서 에러를 변환해서 던져줌.


그리고 '?' 의 좋은 기능이 더 있음. 


fn read_file() -> Result<String, io:Error> {
let mut s = String::new();

File::open("hello.txt")?.read_to_string(*mut s)?;
Ok(s);
}


이전의 함수를 위와 같이 줄일 수 있음. File::open("hello.txt") 에서 에러가 발생하면 '?' 가 알아서 함수 실행을 중단하고 에러를 던지고, 정상적으로 파일이 열리면 파일을 리턴함. 그리고 그 파일에 바로 read_to_string 함수를 사용하는 것임. 제이쿼리나 여러가지 빌더 패턴에서 보는 체이닝 코드임.


'?' 는 반드시 리턴 타입이 Result 인 함수를 호출할 때만 사용할 수 있음. 왜냐하면 '?' 는 Result 타입을 다룰 때의 match 와 같은 방식으로 동작하기 때문임. match 를 Result 에 사용하되, Err 를 만나면 return Err(e) 를 수행하는 것이 '?' 의 동작 방식임. 때문에 리턴 타입이 Result 여야만 '?' 를 사용 가능. 아니면 컴파일 에러 발생.




'Rust' 카테고리의 다른 글

트레잇(Traits)  (0) 2018.02.28
제너릭 타입  (0) 2018.02.27
에러 핸들링(panic)  (0) 2018.02.26
컬렉션(해시맵)  (0) 2018.02.25
컬렉션(스트링)  (0) 2018.02.24

+ Recent posts