Deref와 역참조 강제 변환
Deref를 직접 구현해 스마트 포인터를 참조처럼 역참조하고 함수 호출에서 적용되는 암묵적 역참조 변환을 이해합니다.
Deref 트레이트를 구현하면 역참조 연산자(dereference operator) *의 동작을 커스터마이징할 수 있습니다. (곱하기 혹은 글롭 연산자와 헷갈리지 마세요.)
스마트 포인터가 보통의 참조자처럼 취급될 수 있도록 Deref를 구현함으로써, 참조자에 작동하도록 작성된 코드가 스마트 포인터에도 사용되게 할 수 있습니다.
먼저 역참조 연산자가 보통의 참조자에 대해 동작하는 방식을 살펴보고, 그런 다음 Box<T>처럼 동작하는 커스텀 타입의 정의를 시도해 보면서, 역참조 연산자가 새로 정의한 타입에서는 참조자처럼 동작하지 않는 이유를 알아보겠습니다.
Deref 트레이트를 구현하는 것이 스마트 포인터가 참조자와 유사한 방식으로 동작하도록 하는 원리를 탐구해 볼 것입니다.
그리고서 러스트의 역참조 강제 변환(deref coercion) 기능과 이 기능이 참조자 혹은 스마트 포인터와 함께 동작하도록 하는 방식을 살펴보겠습니다.
Note: 이제부터 만들려고 하는
MyBox<T>타입과 실제Box<T>간에는 한 가지 큰 차이점이 있습니다. 우리 버전은 데이터를 힙에 저장하지 않습니다. 이 예제는Deref에 초점을 맞추고 있으므로, 데이터가 어디에 저장되는가 하는 것은 포인터 같은 동작에 비해 덜 중요합니다.
이 절은 소유권과 이동, 참조자와 대여, 트레이트 구현을 알고 있다는 전제에서 시작합니다. &value는 값을 빌리고, *reference는 빌린 값에 접근한다는 차이를 먼저 확인하세요. 역참조할 수 있다는 사실만으로 내부 값의 소유권을 가져올 수 있는 것은 아닙니다.
포인터를 따라가서 값 얻기
보통의 참조자는 포인터의 한 종류이고, 포인터에 대해 생각하는 방법 하나는 어딘가에 저장된 값을 가리키는 화살표처럼 생각하는 것입니다.
예제 14-6에서는 i32 값의 참조자를 생성하고는 역참조 연산자를 사용하여 참조자를 따라가서 값을 얻어냅니다.
i32 값에 대한 참조자
따라가기
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
변수 x는 i32 값 5를 가지고 있습니다.
y에는 x의 참조자를 설정했습니다.
x는 5와 같음을 단언할 수 있습니다.
하지만 만일 y 안의 값에 대하여 단언하고 싶다면, *y를 사용하여 참조자를 따라가서 이 참조자가 가리키고 있는 값을 얻어 내어(그래서 역참조라고 합니다) 컴파일러가 실제 값을 비교할 수 있도록 해야 합니다.
일단 y를 역참조하면, 5와 비교 가능한 y가 가리키고 있는 정숫값에 접근하게 됩니다.
대신 assert_eq!(5, y);이라고 작성을 시도했다면, 다음과 같은 컴파일 에러를
얻게 됩니다.
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= help: the following other types implement trait `PartialEq<Rhs>`.
f32
f64
i128
i16
i32
i64
i8
isize
and 6 others
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` due to previous error
숫자와 숫자에 대한 참조자를 비교하는 것은 이 둘이 서로 다른 타입이므로 허용되지 않습니다.
*를 사용하여 해당 참조자를 따라가서 그것이 가리키고 있는 값을 얻어내야 합니다.
Box<T>를 참조자처럼 사용하기
예제 14-6의 코드는 참조자 대신 Box<T>를 사용하여 다시 작성할
수 있습니다; 예제 14-7의 Box<T>에 사용된 역참조 연산자는
예제 14-6의 참조자에 사용된 역참조 연산자와 동일한 방식으로
기능합니다.
Box<i32>에 역참조 연산자
사용하기
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
여기서 예제 14-7과 예제 14-6 간의 주요 차이점은 y에 x의 값을 가리키는 참조자가 아닌 x의 복제된 값을 가리키는 Box<T>의 인스턴스를 설정했다는 것입니다.
여기서 복사가 일어나는 이유는 x의 타입인 i32가 Copy이기 때문입니다. Box::new가 모든 값을 자동 복제하는 것은 아닙니다. 예를 들어 String을 Box::new(x)에 넘기면 그 문자열의 소유권이 이동하므로 이후에 원래 x를 사용할 수 없습니다.
마지막 단언문에서 y가 참조자일 때 했던 것과 동일한 방식으로 박스 포인터 앞에 역참조 연산자를 사용할 수 있습니다.
다음으로, 자체 박스 타입을 정의함으로써 Box<T>가 역참조 연산자의 사용을 가능하게끔 해주는 특별함이 무엇인지 탐구해보겠습니다.
자체 스마트 포인터 정의하기
표준 라이브러리가 제공하는 Box<T>와 유사한 스마트 포인터를 만들어 보면서 스마트 포인터는 어떻게 기본적으로 참조자와는 다르게 동작하는지 경험해봅시다.
그다음 역참조 연산자의 사용 기능을 추가하는 방법을 살펴보겠습니다.
예제 14-8에서는 MyBox<T>를 하나의 요소를 가진 튜플 구조체로 정의합니다. 이는 Deref 학습용 래퍼이며 실제 Box<T>의 내부 구현이나 힙 할당을 그대로 재현하지 않습니다.
또한 Box<T>에 정의된 new 함수와 짝을 이루는 new 함수도 정의하겠습니다.
MyBox<T> 타입 정의하기
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
MyBox라는 이름의 구조체를 정의하고 제네릭 매개변수 T를 선언했는데, 이는 모든 타입의 값을 가질 수 있도록 하고 싶기 때문입니다.
MyBox 타입은 T 타입의 요소 하나를 가진 튜플 구조체입니다.
MyBox::new 함수는 T 타입의 매개변수 하나를 받아서 그 값을 들고 있는 MyBox 인스턴스를 반환합니다.
예제 14-7의 main 함수를 예제 14-8에 추가하고 Box<T> 대신 우리가 정의한 MyBox<T> 타입을 사용하도록 고쳐봅시다.
러스트는 MyBox를 역참조하는 방법을 모르기 때문에 예제 14-9의 코드는 컴파일되지 않을 것입니다.
Box<T>에 사용되었던 방식
그대로 MyBox<T> 사용 시도하기
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
아래는 그 결과 발생한 컴파일 에러입니다.
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` due to previous error
MyBox<T> 타입은 역참조될 수 없는데, 그 이유는 이 타입에 그런 기능을 구현한 적이 없기 때문입니다.
* 연산자로 역참조를 할 수 있게 하려면 Deref 트레이트를 구현해야 합니다.
Deref 트레이트를 구현하여 임의의 타입을 참조자처럼 다루기
9장의 ‘특정 타입에 트레이트 구현하기’ 절에서 논의한 바와 같이, 어떤 트레이트를 구현하기 위해서는 그 트레이트가 요구하는 메서드에 대한 구현체를 제공해야 합니다.
표준 라이브러리가 제공하는 Deref 트레이트는 deref라는 이름의 메서드 하나를 구현하도록 요구하는데, 이 함수는 self를 빌려와서 내부 데이터의 참조자를 반환합니다.
예제 14-10은 MyBox의 정의에 덧붙여 Deref의 구현체를 담고 있습니다.
MyBox<T>에 대한 Deref 구현하기
use std::ops::Deref;
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
type Target = T; 문법은 Deref 트레이트가 사용할 연관 타입(associated type)을 정의합니다.
여기서는 MyBox<T>의 Deref 구현이 역참조 대상의 타입을 T 하나로 정한다고 읽으면 됩니다. 예를 들어 MyBox<String>의 Self::Target은 String이고, deref의 반환 타입은 &String입니다. 함수 호출자가 Target을 새로 선택하는 것은 아닙니다. 연관 타입과 제네릭 매개변수의 차이는 18장에서 더 자세히 다룹니다.
deref 메서드의 본문은 &self.0으로 채워졌으므로 deref는 * 연산자를 이용하여 접근하려는 값의 참조자를 반환합니다; 4장의 ‘명명된 필드 없는 튜플 구조체를 사용하여 다른 타입 만들기’ 절에서 다룬 것처럼 .0이 튜플 구조체의 첫 번째 값에 접근한다는 것을 상기하세요.
예제 14-9에서 MyBox<T> 값에 대해 *을 호출하는 main 함수는 이제 컴파일되고 단언문은 통과됩니다!
여기서 새로 만든 MyBox<T>는 Deref 구현으로 역참조 동작을 제공해야 합니다. 언어가 이미 아는 & 참조자·Box<T>의 역참조와 사용자 정의 타입의 트레이트 구현을 구분하세요. 원시 포인터의 역참조는 unsafe를 요구하는 별도 주제입니다.
deref 메서드는 컴파일러가 Deref를 구현한 어떤 타입의 값에 대해 deref 메서드를 호출하여, 자신이 역참조하는 방법을 알고 있는 & 참조자를 가져올 수 있는 기능을 제공합니다.
예제 14-9의 *y에 들어서면 러스트 뒤편에서는 실제로 아래와 같은 코드가
동작합니다.
*(y.deref())러스트는 * 연산자에 deref 메서드 호출과 보통의 역참조를 대입하므로 deref 메서드를 호출할 필요가 있는지 혹은 없는지에 대해서는 생각하지 않아도 됩니다.
러스트의 이 기능은 일반적인 참조자의 경우든 혹은 Deref를 구현한 타입의 경우든 간에 동일한 기능을 하는 코드를 작성하도록 해 줍니다.
deref 메서드가 값의 참조자를 반환하고, *(y.deref())에서의 괄호 바깥의 일반 역참조가 여전히 필요한 이유는 소유권 시스템과 함께 작동시키기 위해서입니다.
만일 deref 메서드가 값의 참조자 대신 값을 직접 반환했다면, 그 값은 self 바깥으로 이동할 것입니다.
위의 경우 혹은 역참조 연산자를 사용하는 대부분의 경우에서는 MyBox<T> 내부의 값에 대한 소유권을 얻으려는 것이 아닙니다.
코드에 *를 쓸 때마다 이 * 연산자가 deref 함수의 호출 후 *를 한 번만 호출하는 것으로 대치된다는 점을 주의하세요.
* 연산자의 대입이 무한히 재귀적으로 실행되지 않기 때문에, 결국 i32 타입의 데이터를 얻게 되는데, 이는 예제 14-9의 assert_eq! 내의 5와 일치합니다.
함수와 메서드를 이용한 암묵적 역참조 강제 변환
RUST · TWO DEREF TARGETS
hello(&m)는 두 Deref를 거쳐 &str을 빌린다
요청된 참조 타입이 변환의 목표입니다. 본문의 MyBox<String>에서 String, str로 이어지는 실제 Target 관계를 따라갑니다.
&MyBox<String> → &String
본문 MyBox의 Deref::Target은 String이며 deref는 &self.0을 반환합니다.
&String → &str
String의 Target은 str입니다. hello의 요청 타입에 도달하면 추가 변환 없이 빌려 호출합니다.
본문의 hello(&m) · 결과는 Hello, Rust!
let m = MyBox::new(
String::from("Rust")
);
hello(&m);MyBox와 hello는 앞의 본문 정의를 사용합니다. MyBox<T>(T)는 이 수업의 튜플 래퍼이며 데이터를 힙에 저장하지 않습니다. m의 String 소유권을 hello에 넘기지 않습니다.
변환 단계는 컴파일 때 결정됩니다. 사용자 정의 deref의 실행 비용까지 없어지는 것은 아니며, 암시적 호출에 적합하도록 저렴하고 예상 가능한 동작으로 설계합니다. Target은 ?Sized일 수 있습니다.
역참조 강제 변환(deref coercion)은 Deref를 구현한 어떤 타입의 참조자를 다른 타입의 참조자로 바꿔줍니다.
예를 들어, 역참조 강제 변환은 &String을 &str로 바꿔줄 수 있는데, 이는 String의 Deref 트레이트 구현이 그렇게 &str을 반환하도록 했기 때문입니다.
역참조 강제 변환은 러스트가 함수와 메서드의 인수에 대해 수행해 주는 편의성 기능이고, Deref 트레이트를 구현한 타입에 대해서만 동작합니다.
이는 어떤 특정한 타입의 값에 대한 참조자를 함수 혹은 메서드의 인수로 전달하는데 이 함수나 메서드의 정의에는 그 매개변수 타입이 맞지 않을 때 자동으로 발생합니다.
일련의 deref 메서드 호출이 인수로 제공한 타입을 매개변수로서 필요한 타입으로 변경해 줍니다.
역참조 강제 변환은 함수와 메서드 호출을 작성하는 프로그래머들이 &와 *를 사용하여 수많은 명시적인 참조 및 역참조를 추가할 필요가 없도록 하기 위해 도입되었습니다.
또한 역참조 강제 변환 기능은 참조자나 스마트 포인터 둘 중 어느 경우라도 작동되는 코드를 더 많이 작성할 수 있도록 해 줍니다.
역참조 강제 변환이 실제 작동하는 것을 보기 위해서, 예제 14-8에서 정의했던 MyBox<T>와 예제 14-10에서 추가했던 Deref의 구현체를 이용해봅시다.
예제 14-11은 문자열 슬라이스 매개변수를 갖는 함수의 정의를 보여줍니다.
예제 14-11:&str 타입의 name 매개변수를 갖는
hello 함수
fn hello(name: &str) {
println!("Hello, {name}!");
}
hello 함수는 이를테면 hello("Rust");와 같이 문자열 슬라이스를 인수로 받아 호출될 수 있습니다.
예제 14-12에서 보는 바와 같이, 역참조 강제 변환은 MyBox<String> 타입 값에 대한 참조자로 hello의 호출을 가능하게 만들어 줍니다.
MyBox<String> 값에 대한 참조자로 hello 호출하기
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&m);
}
여기서는 hello 함수에 &m 인수를 넣어 호출하고 있는데, 이것이 MyBox<String> 값에 대한 참조자입니다.
예제 14-10에서 MyBox<T>에 대한 Deref 트레이트를 구현했으므로, 러스트는 deref를 호출하여 &MyBox<String>을 &String으로 바꿀 수 있습니다.
Deref에 대한 API 문서에도 나와 있듯이, 표준 라이브러리에 구현되어 있는 String의 Deref가 문자열 슬라이스를 반환합니다.
러스트는 다시 한번 deref를 호출하여 &String을 &str로 바꾸는데, 이것이 hello 함수의 정의와 일치하게 됩니다.
만일 러스트에 역참조 강제 변환이 구현되어 있지 않았다면, &MyBox<String>
타입의 값으로 hello를 호출하기 위해서는 예제 14-12의 코드 대신
예제 14-13의 코드를 작성했어야 할 것입니다.
fn main() {
let m = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
(*m)은 MyBox<String>을 String으로 역참조해 줍니다.
그런 다음 &와 [..]가 전체 문자열과 동일한 String의 문자열 슬라이스를 얻어와서 hello 시그니처와 일치되도록 합니다.
역참조 강제 변환이 없는 코드는 이 모든 기호가 수반된 상태가 되어 읽기도, 쓰기도, 이해하기도 더 힘들어집니다.
역참조 강제 변환은 러스트가 프로그래머 대신 이러한 변환을 자동으로 다룰 수 있도록 해 줍니다.
인수로 전달된 타입에 대해 Deref 트레이트가 정의되어 있다면, 러스트는 해당 타입을 분석하고 Deref::deref를 필요한 만큼 사용하여 매개변수 타입과 일치하는 참조자를 얻을 것입니다.
Deref::deref가 추가되어야 하는 단계는 컴파일 타임에 결정됩니다. 이 수업처럼 단순한 참조를 반환하는 구현은 최적화할 수 있지만, 사용자 정의 deref의 실행 비용까지 자동으로 사라지는 것은 아닙니다.
역참조 강제 변환이 가변성과 상호작용하는 법
Deref 트레이트를 사용하여 불변 참조자에 대한 *를 오버라이딩하는 방법과
비슷한 방식으로, DerefMut 트레이트를 사용하여 가변 참조자에 대한 * 연산자를
오버라이딩할 수 있습니다.
러스트는 다음의 세 가지 경우에 해당하는 타입과 트레이트 구현을 찾았을 때 역참조 강제 변환을 수행합니다.
T: Deref<Target=U>일 때&T에서&U로T: DerefMut<Target=U>일 때&mut T에서&mut U로T: Deref<Target=U>일 때&mut T에서&U로
DerefMut은 Deref를 함께 요구하며 deref_mut(&mut self) -> &mut Self::Target을 제공합니다. 따라서 읽기용 대상 타입과 가변 접근의 대상 타입은 같은 Target입니다. 아래에서 쓰는 MyBox<T>는 Deref만 구현했으므로 첫 번째와 세 번째 경로만 사용할 수 있습니다.
처음 두 가지 경우는 두 번째가 가변성을 구현했다는 점을 제외하면 동일합니다.
첫 번째 경우는 어떤 &T가 있는데, T가 어떤 타입 U에 대한 Deref를 구현했다면, 명료하게 &U를 얻을 수 있음을 기술하고 있습니다.
두 번째 경우는 동일한 역참조 강제 변환이 가변 참조자에 대해서도 발생함을 기술합니다.
세 번째 경우는 좀 더 까다로운데, 러스트는 가변 참조자를 불변 참조자로 강제할 수도 있습니다.
하지만 그 역은 불가능하며, 불변 참조자는 가변 참조자로 결코 강제되지 않을 것입니다.
대여 규칙에 의거하여, 가변 참조자가 있을 경우에는 그 가변 참조자가 해당 데이터에 대한 유일한 참조자여야 합니다. (그렇지 않다면, 그 프로그램은 컴파일되지 않을 것입니다.)
가변 참조자를 불변 참조자로 변경하는 것은 결코 대여 규칙을 깨트리지 않을 것입니다.
불변 참조자를 가변 참조자로 변경하는 것은 초기 불변 참조자가 해당 데이터에 대한 단 하나의 불변 참조자여야 함을 요구할 것인데, 대여 규칙으로는 이를 보장해 줄 수 없습니다.
따라서, 러스트는 불변 참조자의 가변 참조자로의 변경 가능성을 가정할 수 없습니다.
아래 다이어그램은 Deref, DerefMut, 그리고 역참조 강제 변환이 허용하는
방향을 사용 시점별로 정리합니다.
RUST · BORROW DIRECTION CONTRACTS
Deref는 공유 빌림, DerefMut는 가변 Target 빌림을 제공한다
입력과 출력의 가변성을 함께 비교합니다. 아래 네 관계는 역참조 강제 변환의 방향이며, 명시적 trait 구현이 없는 임의 타입 변환을 뜻하지 않습니다.
공유 → 공유 · Deref
&T → &U
T: Deref<Target = U>입력의 공유 빌림을 유지하며 Target을 빌립니다. 본문의 &MyBox<String> → &String → &str이 이 방향입니다.
가변 → 가변 · DerefMut
&mut T → &mut U
T: DerefMut<Target = U>가변 Target을 빌리는 구현이 필요합니다. 본문의 MyBox는 Deref만 구현하므로 이 가변 경로는 아직 제공하지 않습니다.
가변 → 공유 · Deref
&mut T → &U
T: Deref<Target = U>가변 빌림을 읽기용 공유 빌림으로 낮출 수 있습니다. 빌림이 유효한 동안 기존 소유자의 접근은 계속 대여 규칙을 따릅니다.
공유 → 가변 · 허용되지 않음
&T ⇏ &mut UDeref coercion은 공유 빌림에서 새로운 배타 빌림을 만들지 않습니다. 내부 가변성은 Cell/RefCell/Mutex 같은 별도의 API 계약입니다.
DerefMut는 Deref를 함께 요구합니다. 공유에서 가변으로 강제 변환하는 경로는 없습니다.
Deref를 이해할 때는 스마트 포인터가 소유한 값과 함수가 빌려 가는 참조 타입이 어떻게 이어지는지 함께 보아야 합니다.
예시
앞의 발췌들은 하나의 파일에서 함께 사용하는 정의입니다. 다음 전체 프로그램은 이를 조립하고 공유 호출과 가변 참조에서 읽기 호출을 나란히 확인합니다.
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(value: T) -> Self {
Self(value)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
fn hello(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let mut name = MyBox::new(String::from("Cloud"));
hello(&name);
hello(&mut name);
assert_eq!(name.len(), 5);
}예상 출력은 Hello, Cloud! 두 줄입니다. 두 호출 모두 &str만 요구하여 공유 Target을 빌립니다. let mut과 &mut name이 있다는 사실만으로 MyBox에 DerefMut 구현이 생기는 것은 아닙니다. 참조 변환은 새 String 생성이나 소유권 이전도 아닙니다.
마지막의 name.len()은 Deref 덕분에 대상 String의 읽기 메서드를 찾을 수 있는 예입니다. 메서드 호출에서는 컴파일러가 수신자를 자동으로 역참조하고 빌리는 과정도 수행합니다. 이를 함수 인수의 &MyBox<String>을 &str로 맞추는 과정과 구분하되, 두 경우 모두 내부 문자열을 빌려 읽는다는 점을 연결해서 보세요.
스스로 타입 경로 고르기
각 시도는 위와 같은 새 MyBox<String>에서 시작합니다. A는 hello(&name), B는 hello(&mut name), C는 name.push('!'), D는 let owned: String = *name입니다. 지금의 구현만으로 허용되는 시도를 고르고, C·D에 필요한 능력이 읽기용 Deref와 어떻게 다른지 설명하세요. D의 String은 Copy가 아닙니다.
허용 경로 확인
A와 B는 공유 문자열 슬라이스를 얻어 호출할 수 있습니다. C는 String의 가변 수신 메서드를 호출할 Target 가변 접근이 필요하지만 현재 래퍼는 DerefMut을 구현하지 않았습니다. D는 공유 대여 뒤의 String을 소유 값으로 빼내려 하므로 허용되지 않습니다. Deref를 구현했다고 소유권을 내보내는 변환이 된 것은 아닙니다. 실제 Box<T>는 언어의 특별한 이동 역참조 규칙도 가지므로, 이 학습용 래퍼와 모든 동작이 같다고 일반화하지 마세요.
변환과 수신 메서드는 Deref 계약, Box의 이동 역참조는 Rust Reference의 역참조 연산을 함께 읽어 확인할 수 있습니다.
Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기 학습 판단 기준표
hello(&name)에서 시작 타입 &MyBox<String>, 첫 대상 &String, 최종 매개변수 &str을 순서대로 설명할 수 있는지 확인하세요. 그다음 name.push('!')가 요구하는 가변 대상 접근과 let owned: String = *name이 요구하는 소유권 이동을 각각 구별하면, 읽기용 Deref가 제공하는 범위와 제공하지 않는 범위를 판단할 수 있습니다.