새 에디션에서 !Leak 자동 트레이트를 도입하려 할 때, 에디션 간 “방화벽”만으로는 안전을 보장하기 어렵다는 점을 네 개의 크레이트 예제로 보여 주며, 실제로 어디에서 오류로 잡아야 하는지와 그 규칙을 어떻게 사용자에게 설명할지라는 난제를 제기한다.
2023년 9월 18일
이전 글에서, 새로운 자동 트레이트를 도입하기 위해 에디션 메커니즘을 사용하는 아이디어를 설명했다. 컴파일러가, 모든 타입의 값이 누수될 수 있다고 가정하는 구(舊) 에디션의 코드와 새 에디션의 !Leak 타입을 함께 사용하는 것을 막기 위해 “깨지지 않는 방화벽”을 만들어야 한다고 썼다.
이에 대해 가능할 것이라는 낙관적인 반응이 많았는데, 글에서 내가 스스로 그 난이도에 “절망한다”라고 썼음에도 말이다. Ariel Ben‑Yehuda로부터 훌륭한 예시를 받았는데, 이 문제가 당신이 생각하는 것보다 훨씬 해결하기 어렵다는 점을 잘 보여준다.
이 예시는 서로 다른 네 개의 크레이트를 포함한다. 첫 번째 크레이트 foo는 2024 에디션이며, 단 하나의 제네릭 메서드를 가진 트레이트를 정의한다:
// 크레이트 foo; 에디션 = 2024
trait Foo {
fn foo<T>(input: T);
}
두 번째 크레이트 bar는 2021 에디션으로, foo에 의존하며 자기 타입에 대해 그 트레이트를 구현한다. 이 구현은 그 메서드의 인자를 잊어버린다(forget):
// 크레이트 bar; 에디션 = 2021
pub struct Bar;
impl foo::Foo for Bar {
fn foo<T>(input: T) {
std::mem::forget(input);
}
}
이 코드는 지난번에 나열한 방화벽 규칙하에서도 합법적이다!
세 번째 크레이트 baz는 2024 에디션이다. bar에는 의존하지 않고 foo에만 의존하며, 자기만의 비공개 !Leak 타입을 사용해 새로운 함수에서 그 트레이트를 사용한다:
// 크레이트 baz; 에디션 = 2024
struct Baz;
impl !Leak for Baz { }
pub fn baz<T: foo::Foo>() {
T::foo(Baz);
}
foo와 baz의 모든 코드는 2024 에디션이며 모두 유효하다. 왜냐하면 그 어떤 것도 Leak 바운드나 2021 에디션 코드를 포함하지 않기 때문이다. 이제 대충 어디로 흘러가는지 감이 올 것이다..
네 번째 크레이트 quux는 bar와 baz 모두에 의존한다:
// 크레이트 quux;
pub fn quux() {
baz::baz::<bar::Bar>()
}
이제 이 코드는 Leak을 구현하지 않은 타입을 누수시킨다! baz::baz는 자신의 !Leak 타입을 bar::Bar::foo에 전달하고, 그곳에서 그것을 누수시킨다. 어딘가에는 오류가 있어야 한다. 하지만 어디에?
답을 적기 전에 다음을 생각해 보자:
quux가 어떤 에디션으로 컴파일되는지 일부러 말하지 않았다. 중요하지 않다. foo, bar, baz가 모두 컴파일되었다면, 어떤 에디션으로 컴파일하든 컴파일되지 않아야 하는 쪽은 반드시 quux다.foo에 있을 수 없다. 제네릭 메서드를 가진 트레이트가 오류일 리 없고, 2024에서 오류가 될 일도 없기 때문이다.baz에도 있을 수 없다. baz에는 전혀 잘못된 것이 없기 때문이다. 해당 인터페이스의 바운드를 만족하는 타입으로 제네릭 인터페이스를 인스턴스화했을 뿐이다.bar에 있다고 말한다면, 그것은 foo를 2021에서 2024로 올리는 것이 호환성 깨짐이라는 함의를 갖는다. 왜냐하면 오늘날 2021 에디션에서도 foo와 bar를 글에 적힌 그대로 구현할 수 있기 때문이다. 따라서 오류는 bar에도 있을 수 없다.겉보기엔 오류가 오로지 quux에 있는 듯하지만, quux에 관련된 타입들 가운데 그 무엇도 Leak과 관련이 없다. 그렇다면 quux에 오류를 내게끔 당신이 강제하려는 규칙은 대체 무엇인가? 그리고 실제 상황에서 이 문제를 마주한 사용자가 무슨 일이 벌어지는지 반이라도 짐작할 수 있도록, 그 규칙을 어떻게 설명할 것인가? 행운을 빈다!