최근 수정 시각 : 2024-04-18 19:54:01

Rust(프로그래밍 언어)/비판


파일:상위 문서 아이콘.svg   상위 문서: Rust(프로그래밍 언어)
1. 개요2. 언어 비판
2.1. 높은 학습 난이도2.2. 잦은 언어 스펙 변경
2.2.1. 반론
2.3. 객체 상속 부재
2.3.1. 반론
2.4. 미비한 심볼 기능
3. 컴파일러 비판
3.1. 컴파일 속도3.2. 부동소수점 최적화
4. 커뮤니티 비판
4.1. 배타적 커뮤니티4.2. 강한 정치적, 사회활동 참여적 성향

1. 개요

Rust의 단점 또는 문제점이나 그 반론 등에 대해 서술하는 문서이다.

이전에 C++에만 존재하는 개념이나 용어를 사용하여 잘못 설명하거나 이해하는 경우가 자주 있다. 따라서 C++와 비교할 때에는 주의를 요한다.

2. 언어 비판

2.1. 높은 학습 난이도

여러 최신 최고급 프로그래밍 언어기능을 풍부하게 가지고 있다보니 언어가 배우기 어렵다. 보안과 안전을 위해 기존의 C++ 나 Java C# 등의 전통적 프로그래밍 언어와는 다른 새로운 개념과 제한이 존재하기 때문에 난이도 곡선이 가파르다. 이는 Go 언어가 언어 기능 확장을 매우 억제하고 언어 스펙의 규모를 제한해 비교적 쉽게 배울 수 있는 것과 대비된다. C/C++ 사용자 대상이 아니라면 상당히 진입곡선이 있는 편이다.
Rust의 개발자인 그레이던 호어도 Rust의 문제점으로 학습의 어려움이 자주 선정된 점을 언급하며 새로운 기능의 추가에 우려를 표했다.

2.2. 잦은 언어 스펙 변경

2015년 첫 발표 이후에 여러차례 언어 스펙에 변경이 있어서 아직 언어 스펙이 안정화 되지 않고 있다. 공식 언어교육 책자에 있는 예제도 최신 컴파일러에선 경고를 주루룩 뱉는다. 컴파일러 뿐 아니라 표준 라이브러리나 여러 핵심 라이브러리도 기능 변경이 잦아 장기적으로 안정적 개발이 어렵다. 2023년 현재도 아직도 발전이나 변화의 속도가 느려지지 않고 있어서 과거 ANSI C 같이 진정한 안정된 언어 표준의 출현은 요원하다.

2.2.1. 반론

Rust와 그 컴파일러의 안정 버전은 개발과정에서 하위호환을 상당히 고려하고 있고 기존 사양에 새로운 기능을 추가하는 방향으로 발전해왔다. 그 과정에서 많은 시범 기능들이 도입되었는데 이들은 명확히 분리되어 있고 시범 버전 컴파일러에서 설정해야 사용할 수 있기 때문에 이 자체가 언어 스펙의 변경이라고 보기에는 무리가 있다.[1] 언어의 하위 호환에 영향을 미칠 정도의 변경 사항은 매 에디션마다 적용되었고 하나의 컴파일러에서 여러 에디션의 상호 호환이 가능하다는 점을 고려한다면 소스 코드 레벨에서 안정적인 개발이 불가능하다고 보기는 어렵다.[2]

사용자 입장에서는 차라리 Rust 컴파일러의 ABI에 대한 비판이 더 와닿을 것이다. Rust 컴파일러의 ABI는 안정성이 보증되지 않기 때문에 컴파일러 버전을 섞어서 쓰기 어렵다.

2.3. 객체 상속 부재

현대 프로그래밍 언어로는 특이하게 객체 상속이 없다. 이는 설계사상의 일부로 객체 상속이 메모리 관리나 바인딩의 관리를 어렵게 하기 때문에 일부러 제외시킨 것. 이는 사용자에 따라 호불호가 엇갈리는데 Rust 가 주된 목표로 하는 극도의 효율과 안전성을 중시하는 시스템 프로그래밍에는 C++ 도 아닌 객체지향을 지원하지않는 ANSI C 언어로 대부분 구현하기 때문에 이런 객체지향 패러다임이 잘 쓰이지 않기 때문이다. 반면 객체지향 패러다임이 보편적으로 쓰이는 게임이나 시뮬레이션 등 일부 응용 영역에서는 이는 큰 약점이 될 수 있다. 굳이 계층적 타이프 구조가 필요하다면 코드의 중복을 감수하고 비슷한 효과를 가지도록 흉내 낼 수 있기는 하다.

2.3.1. 반론

이러한 설계 사상은 유지 관리의 측면을 고려한 것이다. 비록 약간의 코드를 더 작성해야 할 수 있으나 객체의 상속으로 인해 객체 구조가 경직되어 프로그램의 수정이 어려워지는 경우를 예방할 수 있다. Rust에서는 코드의 재사용성을 위해 트레이트를 사용하는 합성(composition)이란 개념이 대체제로 존재한다. 트레이트는 일종의 인터페이스로 다형성의 구현이 가능하며 표준 라이브러리에서부터 적극적으로 사용되고 있다. 덕분에 대부분의 상황에서 객체의 상속 부재로 인한 큰 문제는 발견되고 있지 않다.

전통적인 C 계열 언어에서의 다형성 구현이 상속을 통한 수직계열적 직렬구현이라면, 러스트의 다형성은 트레잇 바운드를 통한 수평적 병렬구현이라고 할 수 있다.

또한 객체 상속이라는 것 자체가 자바의 아버지 고슬링 조차 자바를 재설계한다면 빼 버리고 싶다고 벼르고 있을 정도로 현대에는 이미 부정적으로 여겨지고 있다. 이미 데코레이터와 합성 패턴으로 대체 하는 것이 적극적으로 권장되는 상황에서 이 점은 오히려 관성적으로 안티패턴을 양산하는 것을 방지하고 있다고 볼 수 있다.

2.4. 미비한 심볼 기능

임베디드 개발에 필요한 심볼 기능들이 아직 미비하며 시범 채널인 nightly에서만 이용 가능한 경우가 많다. GCC나 Clang의 __attribute__((weak))[3]__attribute__((alias()))[4]는 임베디드 개발에 있어 필수적이나 이에 대응되는 Rust의 기능은 갖가지 이유로 인해 수년간 안정화되지 못하고 있다.

현재 임베디드 Rust 개발자들은 안정 버전에서 이러한 기능의 부재로 인한 사용자들의 수고를 줄이기 위해 cortex-m-rt처럼 linker script를 통해 weak linkage를 사용하고 procedural macro를 통해 이 과정이 쉽게 이루어지도록 한다.[5] 다만 이런 구조는 약간 복잡하고 생소하여 C로 시작한 임베디드 개발자들에게 혼란을 준다.[6][7]

3. 컴파일러 비판

3.1. 컴파일 속도

러스트는 컴파일이 상당히 느린 편이며 주요 원인은 다음과 같다:
  1. 복잡한 타입 시스템 및 desugaring
  2. LLVM의 느린 머신 코드 생성 속도
  3. 컴파일해야 하는 수많은 의존성 패키지[8]

우선 러스트는 언어 설계상 가능한 한 많은 작업을 개발자가 아닌 컴파일러가 대신 처리하도록 설계되어 있다. 따라서 컴파일러가 최대한 코드를 최적화할 수 있도록 강한 정적 타입 시스템(strong and static type system)을 사용하되 동시에 개발자가 가능한 쉽게 작성할 수 있도록 여러 타입 추론 테크닉이 동원되는데, 예를 들어
#!syntax rust
let mut v = vec![]; // v는 컴파일 타임에 Vec<&str>로 추론된다.
v.push("namuwiki");
위와 같은 경우도 타입 체킹 과정에서 빠짐없이 추론된다. 이는 rustc가 힌들리-밀러 타입 시스템(Hindley–Milner Type systems)[9]을 사용하기 때문인데 이런 추론 과정이 다른 명시적(explicit) 타입 언어에 비해 시간을 잡아먹는다.

분석(analyzing) 단계에는 type checking만 있는 게 아니라 오직 러스트에만 존재하는 borrow checking 과정이 또 필요하다.

무비용 추상화(zero cost abstraction)를 지향하는 언어인 만큼 여러 문법을 더 낮은 수준으로 만들고 최적화하고 더 낮게 만들고 최적화하는 과정이 여러 차례 반복된다. 먼저 macro expansion 과정을 거치는데, MBE가 아닌 프로시저 매크로(procedural macros)의 경우 빌드 시간을 잡아먹는 굉장히 주된 원인이다. 일단 현재 크레이트 빌드하는 것만 해도 느린데 각 매크로별 크레이트를 먼저 별도로 컴파일하고 dylib으로 로드해 실행하는데다, 코드베이스에서 해당 매크로를 자주 호출하면 호출할수록 해당 매크로의 실행(execution) 횟수가 비례해서 증가한다. 특히 러스트 생태계 대부분의 proc macro가 거대한 syn 파서를 사용하기 때문에 이 크레이트 하나 컴파일하는 데도 한 세월이 걸린다.[10] 하지만 그렇다고 serde, clap, thiserror 등 러스트 개발에 사실상 필수적이 되어버린 크레이트들을 안 쓸 수는 없기 때문에 마땅한 해결책은 나오지 않고 있다.
파일:1683833583527909253-scrot.png
Cargo 빌드 타임 프로파일.
빌드에 약 6.44초가 걸리는 syn 크레이트를 확인할 수 있다.

rustc는 기본적으로 백엔드로 LLVM을 사용하는데, 이 덕분에 C에 가까운 빠른 속도나 여러 플랫폼[11]으로의 크로스 컴파일을 지원할 수 있다. 하지만 이는 반대로 말하면 rustc가 아무리 최적화를 해도 LLVM을 쓰는 이상 결코 똑같이 LLVM을 쓰는 clang의 컴파일 속도보다 빨라지기는 어렵다는 뜻이기도 하다. 특히나 LLVM은 점점 발전하며 여러 언어에서 백엔드로 쓰이고 있다고는 하지만 기본적으로는 C를 위해 만들어졌다. 결국 Rust의 언어 디자인을 100% 염두에 두고 설계된 것이 아니니만큼 C에 비해 차별점이 있을 수 있다.[12] 2020년경에도 LLVM측의 패치로 인해 러스트 컴파일 시간에 상당한 저하가 생겼던 적이 있다. # LLVM보다 빠른 Cranelift를 백앤드로 추가하기 위해 계속 개발중이고 빌드 시간이 30% 감소했다는 벤치마크 결과도 있지만 기능이 완전하지 않아서 아직은 갈 길이 멀다. #

여담으로, 링커 설정만 잘 해도 빌드 시간을 대폭 줄일 수 있다. 러스트의 경우 바이너리 크레이트를 빌드하면 반드시 단 하나의 파일로 static linking하지만[13] 몇몇 크레이트는 dylib으로 불러오고 mold, lld 등을 쓰도록 설정해 보자.

물론 경쟁 언어중에 하나인 애플 생태계의 Swift 등 현대의 신세대 고급 언어는 대체로 컴파일이 느리다는 점을 감안할 필요는 있다[14]. 하지만 러스트의 비교 대상으로 자주 등장하는 Go만은 예외인데, 구글의 천재들이직접 C[15]로 컴파일러를 다 짜서 중간에 번거롭게 IR을 만들지 않아 컴파일이 굉장히 빠르고, 현재까지도 이런 번개같은 빌드 속도가 Go의 최대 장점 중 하나이기도 하다. 사실 웬만큼 저수준 영역인 것이 아니라면 포인터 단위의 정확성보단 GC좀 사용하더라도 빠르고 정확힌 피드백 사이클이 더 중요해진다는 점으로 미루어 볼 때, 이러한 느린 컴파일 시간이 러스트 개발자의 생산성에 적잖이 부정적인 영향을 끼칠 수 있다고 볼 수 있다.

다만, C++와의 직접적인 비교에서는 컴파일 시간이 엎치락뒤치락하는 결과를 보여주었다. #
파일:rust-survey.svg
실제로도 Rust 2019 survey 결과 '컴파일 속도 개선'이 무려 위시리스트 4위에 올랐고, 이 때에도 Go와의 컴파일 시간 차이를 Rust의 고려 요소로 꼽았다.[16]

3.2. 부동소수점 최적화

여타 다른 언어와 달리 부동소수점 최적화가 적용되지 않는 경우가 적지 않다. 이는 부동소수점 타입이 IEEE 754 표준을 엄격히 준수하여 플랫폼 간 일관된 동작을 가지도록 하기 위한 것으로 보인다. 이에 대한 예로는 FMA 명령어가 있는데 곱셈과 덧셈을 한꺼번에 수행하면서 라운딩 에러가 한번만 발생하다보니 따로 수행했을 때와 결과가 다르게 나타날 수 있다. # 이러한 문제를 완화하기 위해 mul_add와 같이 특정 연산을 명시할 수 있는 메서드를 추가하였고[17], 약간의 오차가 허용되는 타입을 도입하는 등의 방안이 논의되고 있다. #

4. 커뮤니티 비판

4.1. 배타적 커뮤니티

아직은 소수의 언어이고 역량이 매우 높은 프로그래밍 전문가들에게 관심받고 주로 쓰는 언어이다보니 관련 커뮤니티의 분위기가 Rust 언어를 강력하게 밀어주는 분위기가 감돌아 초보자들이나 외부의 비판자에게는 다소 배타적이다. 과거 Lisp 언어 커뮤니티의 분위기가 생각날 정도이다. 어찌 보면 이것은 순환 상승효과인데, Rust가 커뮤니티 위주로 개발자가 집중되어 있고 이들의 분위기에 의해 전체 방향성이 정해지다보니 이들의 집단적 성향에 질린 사람이 일명 '까' 가 되고, 언어의 '까' 라는 것부터가 프로그래밍 계에서는 그리 좋게 받아들여지지 않는 존재다 보니 커뮤니티 내부에서는 이들을 외부의 탄압으로 간주하여 더 안으로 뭉치고 배타성을 강화하는 강화 작용이 일어난다. 스택 오버플로우 포럼 등에서 '나도 러스트 좋아하는데 러스트 공식 커뮤니티가 너무 공격적이다' 라는 글에 '나한테는 너무 친근하고 우호적인데?' 처럼 러스트 유저 간의 긍정적 경험으로 이견을 부정하는 답글이 달리는 식으로, 내부 동조적으로는 우호적이고 외부에는 반발하는 패턴이 일반적이다.

이런 부류 중에는 소위 RIIR(Rewrite It In Rust) 또는 RESF(Rust Evangelism Strike Force) 라는 신규 시스템 언어 풀에 비해 극성인 부류가 존재하는데. 시도때도 없이 OSS개발자들에게 기여도 없이 Rust로 다시 짜라는 요구를 하다 보니 기존 프로젝트 개발자들에게 언어 자체와는 별개로 러스트 커뮤니티를 좋게 보지 않는 경우가 있다. 어차피 요구를 받을지 말지는 개발자 자유이며 어그로를 끌며 방해하지 않는 한 과민반응할 필요가 없다.

보통은 규모가 커지면서 자연스럽게 이러한 성향이 희석되지만, 러스트의 경우 그 '설계사상' 자체에 의한 팬덤도 상당하기 때문에 아직까지 이런 집단적 배척의식이 강하게 남아 있는 편이다.

4.2. 강한 정치적, 사회활동 참여적 성향

먼저 우크라이나 사람들과 연대하고 이 분쟁의 영향을 받는 모든 사람들에 대한 지원을 표명하고 싶습니다. (중략) 팀과 정치적 견해가 다른 커뮤니티 구성원을 소외시키는 것은 언어에 상처를 줍니다. 커뮤니티의 90%가 특정 상황에서 선택한 편에 동의하더라도. 여전히 부정적입니다.
러스트 커뮤니티가 대부분 진보적이라는 것은 인정하지만. 기본적으로 "전쟁은 나쁘다"라는 의미의 발언들입니다. 이게 정말 정치적인가요?
현 시점에서 우크라이나를 지지하는 것은 정치적 의견이 아닙니다. 당신이 우크라이나와 함께 하지 않는다면 그것은 당신에 대해 많은 것을 말해주는 것이고, 나는 당신이 Rust 커뮤니티나 다른 커뮤니티에 있는 것을 원하지 않을 것입니다.
누군가가 피해자와 연대하는데 이견이 있다면 나는 그들이 소외감을 느끼는 것에 대해 전적으로 찬성합니다. 그 외에 아무것도 바라지 않습니다.
https://www.reddit.com/r/rust/comments/t1x787/rust_making_political_statements/

원칙적으로 기술 관련 문서에서는 정치사회적 특성이 기술될 필요가 없으나, Rust의 경우 "Rust believes that tech is and always will be political" 이라고 공식적으로 천명할 정도로 강한 사회참여적 성향을 띠고 있으며, 언어 내부구조에서도 '성 중립적 단어로의 문서 개정', '비 차별적 단어로의 용어 변경' 등 정치적 올바름을 언어 자체에 녹여내기 위한 노력을 지속적으로 진행하고 있으므로 일부 사례가 언급된다.

사실 논쟁 자체는 어느 언어에나 있으므로 키배가 벌어지는 것 자체가 비판의 대상이라고 하는 것에는 문제가 있다. 다른 언어나 개발커뮤니티와 달리 가지는 실체적인 논점은 이러한 사상적 갈등이 위에 있는 커뮤니티의 배타성과 연결점을 가진다는 것이다.

예를 들어, 명칭이나 로고의 디자인 변형에 민감하게 반응하거나, 'bad'라는 도움말 용어가 너무 나쁜말 같으니 'non standard' 로 순화시키자고 하거나, 마찬가지로 'stupid', 'crazy' 같은 단어를 지적하는 등의 발언이 발생하는 것 자체가 부정적이라고 볼 수는 없다.

이것이 위에서 언급된 '배타성' 까지 확장되는 경계선은 이러한 하나하나의 여론이 개발자 커뮤니티 전반에서 '사회적 동조를 밝히지 않고 중립을 주장하자고 하는' 견해를 의심의 눈으로 보고 '반 정의적' 행동을 찾으려고 하는 지점이다. 정치적인 논쟁이 등장하면, 위에서도 언급되었듯이 필연적으로 갈등이 갈등을 낳고 반대 의견이 더 강한 반대의견을 끌어오는 현상을 일으킨다. 이 문단 초기에 인용된 게시물에서는 러시아 우크라이나 전쟁에 대한 화제가 친 러시아 게시물 작성자를 낳고, 그를 성토하는 과정에서 '정치적으로 과열된 듯한데 프로그래밍 언어 커뮤니티에서 정치화제를 너무 깊게 다루지 말자' 라는 의견까지 '불의를 내버려두자고 하는 것은 불의의 편이나 마찬가지고, 잘못된 생각을 가진 사람을 배척하는 것은 정치논쟁거리라고 할수도 없다. 잘못된 사상을 가진 사람은 순수 기술적 기여로만 판단할 것이 아니라 러스트 커뮤니티에서 배제하여야 한다' 라는 의견이 여럿 나오면서 점점 진흙탕과 순수주의 싸움의 전형적인 형태로 흘러가고 있다.

앞서도 말했지만 포럼에서 온갖 화제로 난투가 벌어지는 것 자체는 지극히 흔한 현상이다. 특이점이라면 Rust의 경우 그 핵심집단이 정치사회적 화제에 대해 매우 적극적인 용인을 하고 있기 때문에 좀 더 많은 갈등과 반발이 일어나고, 그 '트롤'에 대처하면서 점점 더 '순수주의적이고 교조적인 동질적 집단' 으로서의 성향을 갖는 데 거리낌이 없다는 것이다.

이 때문에, 보통 신생언어의 경우 언어를 외부에서 기술적으로 '활용'하는 사람과 '언어 커뮤니티에서 활동하는' 사람이 많이 겹치는 데 반해, Rust의 경우에는 선을 긋고 순수하게 활용만 하거나 이슈를 전혀 모르는 사람과 '언어 커뮤니티' 의 경계가 깊은 편이다.

[1] C/C++도 새로운 표준의 초안을 컴파일러에서 시범적으로 도입하고 지원하지만 이를 공식 표준이라고 하지 않는 것과 같은 맥락이다.[2] C/C++도 필요에 따라 하위 호환에 영항을 미칠 정도의 변경 사항이 매 표준마다 적용되었다.[3] weak symbol로 함수를 정의하는 기능이다. 이는 미리 함수 하나를 정의하나, 만약 동일한 함수가 재선언되면, 오류가 아닌 재선언된 함수로 심볼을 교체한다. 이는 interrupt vector table 구성 시 기본 핸들러를 선언하는데 필요하다.[4] 별칭으로 함수를 정의하는 기능이다. 똑같은 코드를 작성하는 수고를 줄이기 위해 사용한다.[5] 실질적으로는 함수가 선언된 코드에 unsafe extern \"C\"가 추가된게 끝이다.(...)[6] Linker Script의 PROVIDE 키워드를 사용하여 weak와 alias 기능이 합쳐진 기능을 사용할 수 있다! 물론 최대 단점은 alias 기능도 합쳐져있는 만큼 Default Function 선언이 필수이다.[7] 현재 상태에서는 최선이지만, 무조건 바꿔야할 기능이라고 말할 수 있다. 일단 매우 비효율적이다. cortex-m-rt처럼 Core, Peripheral Interrupt가 동일한 Default Function으로 선언 가능한 경우에는 매우 최선이다. 대표적으로 임베디드에서의 C의 표준 라이브러리 사용을 예시로 들 수 있다. C 표준 라이브러리같은 경우에는 라이브러리에서 기본적으로 기능을 구현하고, 필요에 따라, 기능을 수정해야하는 경우가 있다. 예를 들면 _write와 같은 출력, _time과 같은 시간 관련 함수들이 있다. 만약 Rust에서 비슷하게 std crate에서 일부 기능에 대한 재정의를 위해, Weak Linkage가 필요하다면, Linker Script에서는 Weak Linkage 해야하는 함수를 본래 기능이 정의된 Default Function를 사용하고자 선언하고, 코드에서는 Provide를 사용하기 위해서는 Default Function 선언이 필수이기에, 본래 기능이 정의된 Default Function 선언도 해야한다... 또한 Default Function 정의로 끝이 아닌, Weak Linkage하고자 하는 원래 함수들에 대해서도 프로토타입 선언을 해야 Linking이 가능하다. 즉, Interrupt나 Default Function이 비고, 공통적인 경우가 아니라면, 한계가 뚜렷하다.[8] 현재로서는 Cargo에서 미리 컴파일된 패키지를 제공할 수 없다. Rust ABI가 안정화되지 않았기 때문. 현재로써는 모든 종속성 패키지의 소스를 그대로 가져와서 한번에 컴파일하는 것이 최선이다. #[9] 해당 타입 시스템을 사용하는 언어는 이 분야의 영원한 빌런Haskell, OCaml 등으로 별로 많지 않다. TypeScript조차 타입 추론에 HM을 쓰지 않는다.[10] the reason proc macros are slow is that the (excellent) proc macro infrastructure - syn and friends - are slow to compile. Using proc macros themselves does not have a huge impact on compile times." - @ManishEarth(러스트의 코어 개발자 중 한 명)[11] 러스트에서는 Target이라고 한다.[12] 실제로도 noalias 최적화 기능은 안정화에 몇 년이 걸렸고, C에서는 noalias를 쓸 일이 드물기 때문에 llvm쪽에서 진행이 느렸던 점이 있다. #[13] 이 링킹 과정에서 일반적인 빌드 타임의 약 11%가량이 소요된다.[14] 스위프트 역시 같은 LLVM 기반이다[15] 나중에 Go로 부트스트랩되었다.[16] "Improve compile times. Compiling development builds at least as fast as Go would be table stakes for us to consider Rust. (Release builds can be slow.)"[17] 다만, FMA가 지원되지 않는 프로세서에서는 소프트웨어로 처리하기 때문에 오히려 더 느려질 수 있다.


파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 문서의 r790에서 가져왔습니다. 이전 역사 보러 가기
파일:CC-white.svg 이 문서의 내용 중 전체 또는 일부는 다른 문서에서 가져왔습니다.
[ 펼치기 · 접기 ]
문서의 r790 (이전 역사)
문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)

문서의 r (이전 역사)