mir.pe (일반/어두운 화면)
최근 수정 시각 : 2024-11-03 19:30:16

프로그램 최적화


파일:나무위키+유도.png  
은(는) 여기로 연결됩니다.
PC최적화와 관련 소프트웨어에 대한 내용은 최적화 프로그램 문서
번 문단을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
, 에 대한 내용은 문서
번 문단을
번 문단을
부분을
부분을
참고하십시오.
1. 개요2. 분야별 특징3. 주 최적화 기법
3.1. 마이크로 튜닝3.2. 컴파일러에서의 최적화
3.2.1. 컴파일러의 한계 예시
3.3. 최적화 예시
4. 단점5. 관련 문서

1. 개요

소프트웨어의 효율성을 높이는 일.

적은 자원으로 높은 효율을 낸다면, 소프트웨어 최적화가 잘 되었다고 말한다. 최적화된 프로그램들은 최적화되지 않은 프로그램들에 비해 메모리를 적게 사용하거나 실행 시간이 줄어드는 이점이 생긴다. 최적화가 제대로 안 되었다면 많은 자원을 먹는데도 영 좋지 않은 효율이 나오고, 발적화나 개적화 등의 표현으로 불리며 까인다.

좁은 범위로는 기기의 성능을 업그레이드 시키는 펌웨어 업그레이드부터, 넓게는 윈도우즈 시리즈의 서비스팩 급과 같은 대규모 업데이트까지도 포함된다.

임베디드 시스템 프로그래밍에서는 속도와 용량 사이의 상반관계 [1]를 감안해야 하기 때문에 어느 쪽을 우선시 하느냐에 따라 최적화의 방향이 달라질 수도 있다. 그래서 윈도우 임베디드 컴팩트를 연구하는 이들에게는 뼈저리게 다가오는 단어다.

사용자 경험에 가장 크게 기여하는 개념이기도 하다. 대표적인 예로 조립 컴퓨터가 있다. 아무리 고성능 부품들만 써도 최적화가 잘 안 되어 있으면 오히려 성능이 떨어지는 모습을 보인다.

단, 어느 쪽이든 하드웨어를 바꾸는 업그레이드는 예외. 한정된 성능에서 최적의 모습을 보여주어야 하기 때문에 별 의미가 없다. 몇몇 회사들은 최적화에 한계가 있는 부분을 하드웨어를 업그레이드하여 커버하기도 한다. 이를 하드웨어로 최적화한다고 표현하기도 하지만 그런 개념은 없다.

프로그램 규모가 커지면 개발중 컴파일 자체에 대해서도 최적화가 필요한데, 소스코드가 많아지고 빌드 규모가 커지면 빌드타임이 수 분을 넘는 경우도 많아 테스트 런을 돌려볼때마다 시간을 잡아먹기 때문이다. 보통 라이브러리 등을 이용한 모듈화, 컴파일용 원시 소스 코드 파일 수 줄이기[2], 멀티코어 컴파일 등을 통해 해결하며, 이 정도로 모자랄 경우에 결국 하드웨어 수를 늘려 분산 컴파일 시스템을 구축하게 된다.

2. 분야별 특징

최적화를 잘 하기로 유명한 기업에는 애플 [3] 심비안 버프의 노키아가 있다. 국내에서는 코원이 유명하다.

2.1. 군사 & 우주용 CPU

이쪽 분야에서는 1980년대부터는 절대로 최신 CPU를 구입하지 않고 오래된 구식 CPU를 쓴다. [4] 시중엔 스레드리퍼 같은 64코어 프로세서까지 나왔는데도 펜티엄급 프로세서를 사용한다. 최신 CPU일수록 성능은 좋은 대신 수명과 내구도는 떨어지는 경향이 있다. 신형일수록 민감도가 심해져 극한의 환경을 버틸 수 없다. 회로 선폭이 좁아질수록 집적도와 속도가 향상되지만 물리적인 내구성은 당연히 취약해질 수 밖에 없다. 이런 이유로 선폭이 굵은 구식 회로 설계를 한 예전 CPU를 쓰는 것이다. 물론 상용 CPU를 그대로 쓰는 것이 아니라, 회로 구조를 유지한 채 웨이퍼의 재료로 실리콘 대신 튼튼한 사파이어( Silicon on Sapphire)를 사용하는 등 여러가지 방식으로 회로의 내구성을 향상시키는 처리를 한다. 이런 처리를 거친( Radiation-Hardened) 제품은 속도를 일부 희생하는 대신 통상 실리콘 소자쯤은 순식간에 망가뜨리는 우주 배경 방사선을 맞아도 정상 작동하는 엄청난 내구성을 자랑한다. 질리도록 오래도록 사용할 수 있는 안정적인 처리 장치만을 요구한다.

2.2. 게임 프로그래머

밸브 코퍼레이션이 최적화로 유명한 편이고, 크라이텍도 2011년부터 최적화로 유명해졌다. 크라이시스 2가 전작과는 달리 높은 그래픽에 비해 최적화가 잘 되어있기 때문. EVE 온라인의 경우 한 때 서버의 발적화로 욕을 많이 먹었다가 발적화를 싹 해결한 프로그래머가 (플레이어 중에 IT 업계 종사자가 많은 덕분에) 슈퍼스타 취급을 받기도 했다. 그리고 락스타 게임즈 Grand Theft Auto IV는 심각한 개적화였지만 후속작인 Grand Theft Auto V는 엄청난 신적화로 환호를 받았다. 또한 프로스트바이트 엔진을 사용한 게임들( 배틀필드 4, 등)도 최적화가 잘 되어있는 편이다. 블리자드 엔터테인먼트 게임들의 경우 들쭉날쭉한 편. 와우, 디아블로 3와 오버워치는 어지간한 컴퓨터에서는 다 돌아가는 이른바 갓적화로 칭송받는다. 그러나 히어로즈 오브 더 스톰과 스타크래프트 2의 경우 엔진 자체의 문제 때문에 컴퓨터를 민감하게 타는 편이라 최적화가 좋다고 하기 어렵고, 하스스톤의 경우에도 PC와 모바일 모두 그다지 뛰어나지 않은 편. 가이진 엔터테이먼트의 워썬더 또한 최적화로 유명하다.

EA DICE 역시 최적화에 있어 상당한 실력을 보여주고 있으며, 특히 스타워즈: 배틀프론트신적화라는 반응까지 나왔다. 배틀필드 1은 몇 가지 논란이 있기는 했지만 전체적으로 보면 훌륭한 최적화가 이루어졌다.

소프트웨어 최적화 전문가로서 가장 유명하고 장수하는 인물로는 마이크로소프트에서 존 카맥 이드 소프트웨어로 스카우트되어 퀘이크 엔진을 최적화했던 전적의 마이클 압래쉬(Michael Abrash)가 있다. 현재까지도 최적화 최고수로 인정받고 있으며, 코드 한 줄 한 줄, 어셈블리어 하나 하나를 극한까지 쥐어 짜내는 것으로 유명하다.

닌텐도의 경우는 조금 특수한 사례인데, 하드웨어를 값싸게 보급하고 게임 판매로 수익을 메꾸는 경쟁사의 방식은 정면으로 거부하고 하드웨어 판매로도 최소한의 수익을 내도록 하는 정책 때문에 닌텐도 DS부터 Wii U에 이르기까지 닌텐도의 하드웨어는 동세대 경쟁사 기기에 비해 성능 상으로 크게 뒤쳐졌었다. 여기서 끝이 아니고 요코이 군페이로부터 내려져오는 '고사한 기술의 수평사고' 철학에 따라 DS의 듀얼 스크린, Wii의 모션 인식, 3DS의 3D 기능 등 닌텐도의 하드웨어에는 항상 타사에는 없는 특수한 기능이 달려서 나왔다. 때문에 닌텐도 하드웨어는 게임 개발자에게 높은 최적화 역량을 요구하였으며, 닌텐도의 정책 자체도 서드 파티 개발사에게 불친절한 편이어서 조악한 개발 툴은 물론 서드 파티에게는 개발에 필요한 최적화 기술조차도 완전히 알려주지 않을 정도였다. 때문에 서드 파티 입장에서는 닌텐도 퍼스트 파티 게임 홀로 판매량을 견인하는 플랫폼에 비집고 들어가기 위해 극단적인 최적화, 경량화는 물론 기기 고유의 기능마저 살리는 것은 도박이나 다름없었고, 특히나 멀티 플랫폼 게임이라면 굳이 퀄리티를 깎아가며 악평을 감수하기보다는 그럴 시간에 PC 버전이라도 준비하는 것이 나았다. 반대로 닌텐도 퍼스트 파티 입장에서는 이미 기존까지 만들어왔던 하드웨어가 다 그런 식이니 베테랑 개발자들에 의해 축적된 최적화 기술도 충분했고, 애초에 닌텐도 게임기에서만 구동할 목적으로 만드는 게임이니만큼 타사처럼 저성능 환경에 맞춰서 스케일을 줄이고 경량화를 할 지 말 지 다툴 여지조차도 없었다. 결과적으로 닌텐도 게임기로 출시되는 타이틀은 닌텐도 퍼스트 파티 타이틀의 경우 수준높은 최적화는 물론 하드웨어의 특징까지도 잘 살리는 최고의 퀄리티를 보여주었지만, 반대로 서드 파티 타이틀의 경우 발적화로 나오기 일쑤였다. 이러한 문제는 닌텐도 스위치가 닌텐도의 개발자조차 '최적화를 위한 어떠한 꼼수도 부리지 않았던 적은 스위치가 처음'이라고 언급할 정도로 기존까지의 '닌텐도 게임기=저성능' 공식을 깨는 준수한 성능으로 나오면서 크게 개선되었다. 무엇보다도 불친절한 개발 환경에서도 탈피해 SDK는 OpenGL Vulkan API를 네이티브로 지원하는 것은 물론 언리얼 엔진, 유니티 엔진 등의 메이저 게임 엔진의 참여를 통해 기존의 PC 게임이나 모바일 게임 개발에 사용했던 대부분의 기술도 그대로 사용할 수 있었다. 덕분에 떠나갔던 서드 파티들도 조금이나마 돌아와서 크게 밀리지 않는 퀄리티의 타이틀을 발매하고 있다.

3. 주 최적화 기법

3.1. 마이크로 튜닝

최적의 알고리즘을 사용하고 주요 병목을 제거했는데도 시원치 않을 경우 군데군데를 손 봐서 성능을 튜닝한다. 이런 최적화는 코드의 가독성이나 유지보수성을 등가 교환해야 하는 경우가 많다. 이 상반 관계를 감수하는 것은 프로그래머의 몫. 일반적으로 병목을 제거하는 것만으로도 충분히 빨라졌다면 튜닝을 굳이 할 필요는 없다. [5] 반면 그래도 좀 더 나은 속도가 필요하다면 다음과 같은 튜닝을 시도하기도 한다.

3.2. 컴파일러에서의 최적화

현대의 컴파일러들은 충분히 똑똑해져서, 적당한 설정을 해 주면 프로그래머를 대신하여 어느 정도 최적화를 해 준다. [7] 다만 최적화된 실행 파일은 프로그래머가 만든 소스 코드와 달라져[8] 이 때문에 DWARF나 PDB같은 디버깅 심볼이 없는 경우 디버깅 난이도가 조금 올라간다는 단점이 있다.

그런데 컴파일러는 프로그램의 구문들을 인식할 수 있지만 인간처럼 프로그램의 흐름을 이해할 수는 없다. 이게 되면 프로그래머들 다 굶어 죽는다. 왜냐하면 컴파일러 스스로가 오류나 최적화를 할 수 있는데 굳이 인간이 직접 해야 할 이유가 없기 때문이다. 프로그래머가 굶어 죽는 것 뿐만 아니라 아예 그 직업이 사라질 수도.. 컴파일러의 최적화는 최적화 테크닉을 적용 가능한 몇몇 구문들을 인식하면서 실행되는데, 요령 있게 짜이지 않은 코드의 경우 컴파일러가 구문을 잘 인식할 수가 없어 자동으로 최적화가 되지 않는다. 이런 식으로 컴파일러의 자동 최적화를 막는 코드들을 '최적화 장애물(optimization blocker)'이라 부른다. 최적화 장애물을 치우는 것은 프로그래머들의 몫이다. 프로그래머들은 최적화 장애물을 치워 컴파일러가 최적화를 잘 할 수 있도록 도와야 한다.

3.2.1. 컴파일러의 한계 예시

다음 코드를 보자.
#!syntax cpp
void func1(int *xp, int *yp)
{
    *xp += *yp;
    *xp += *yp;
}

void func2(int *xp, int *yp)
{
    *xp += 2 * (*yp);
}

func1func2는 동일한 기능을 하는 함수처럼 보이지만 함정이 숨어 있다. 만일 xp == yp[9]라면 두 함수의 결과가 달라진다. xp == yp이고 *xp == 1이었다면 func1 수행 후에는 4가, func2 후에는 3이 *xp의 값이 된다. func1을 불필요하게 대입 연산자와 덧셈 연산자를 한번 더 사용한다고 판단해 func2의 형태로 수정한다면 결과가 달라지는 부분이 존재하며 이는 숨은 버그의 원인이 된다. 또한 펌웨어나 임베디드 혹은 디바이스 드라이버 등을 작성을 할 때에는 시간에 따라 레지스터나 메모리의 값이 변화해야 하는 경우가 있다. 주로 Memory-Mapped I/O를 수행할 때이다. 즉, func1func2와 같은 형태로 변화시킬 경우 개발자가 의도한 것과는 완전히 다른 동작을 하게 된다. 이렇게 컴파일러에 의한 최적화를 사전에 방지해야만 하는 경우에는 해당 변수에 'volatile'를 붙여 선언해야 한다. 또한 위의 예제처럼 인자 두 개가 모두 포인터이고 쓰기 연산을 할 때에는 데이터 위험이 존재할 가능성이 있기 때문에 보통 컴파일러는 이러한 함수에 대해 최적화를 하지 않는다.

3.3. 최적화 예시

다음과 같은 코드가 있다고 하자
#!syntax cpp
extern bool isZero(float x, float y, float z)
{
    return (!x && !y && !z) ? true : false;
}

이 경우 다음과 같이 번역될 수 있다.
    mov r1, 0  // Result

    mov r2, x  // x
    mov r3, y  // y
    mov r4, z  // z
    
    cmp r2, 0  // Compare x
    jne .false // Jump if r2 != 0
    cmp r3, 0  // compare y
    jne .false // jump if r3 != 0
    cmp r4, 0  // compare z
    jne .false // jump if r4 != 0
    jmp .true  // jump true
    
.true
    mov r1, 1  // := true
.false
    ret        // return

프로세서에서 가장 오래 걸리는 처리는 메모리에서 값을 읽어오는 명령이다.
위의 예시에서는 각 값 x, y, z를 레지스터로 이동하는데 가장 오랜 시간이 걸리게 되는데 이는 파이프라인 스톨을 유발시킨다.
jne는 비교 명령인 cmp보다 먼저 실행될 수 없으며, cmp명령은 메모리에서 값을 읽어오는 mov이전에 실행될 수 없다.
cmp r2, 0은 순차적 실행 환경에서는 모든 mov 동작이 끝나기 전에는 실행되지 못한다. 하지만 비순차적 실행이 가능한 환경에서는 r2, r3, r4는 서로 아무런 종속성이 없는 환경이므로 메모리 읽기가 끝났다면 cmp r2, 0의 실행이 가능해지고 이 결과가 참이라면 모든 mov명령이 실행되기 이전에 서브루틴을 빠져 나오는 것이 가능해진다.

이 때문에 일부 컴파일러들의 경우 순차적 실행 프로세서들을 위해 명시적으로 루틴을 최적화 하는 경우가 있다.
    mov r1, 0  // Result

    mov r2, x  // x
    cmp r2, 0  // Compare x
    jne .false // Jump if r2 != 0

    mov r3, y  // y
    cmp r3, 0  // compare y
    jne .false // jump if r3 != 0

    mov r4, z  // z
    cmp r4, 0  // compare z
    jne .false // jump if r4 != 0

    jmp .true  // jump false
    
.true
    mov r1, 1  // := true
.false
    ret        // return

이전 코드는 각 값을 모두 읽은 후 비교를 수행했다면, 최적화가 이루어진 코드는 값을 메모리에서 읽자 마자 비교를 수행하며 파이프라인 스톨을 최소한으로 유지시킨다.

4. 단점

"We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%."

97%의 사소한 효율성은 모두 잊자. 섣부른 최적화가 만악의 근원이다. 하지만 가장 귀중한 그 3%의 기회는 놓치지 말아야 한다.
- 도널드 커누스, <The Art of Computer Programming> 中[10][11]
More computing sins are committed in the name of efficiency (without necessarily achieving it) than for any other single reason - including blind stupidity.

맹목적인 어리석음을 포함해 그 어떤 핑계보다 효율성이라는 이름 아래 행해진 컴퓨팅 죄악이 더 많다.
- 윌리엄 A 울프
The First Rule of Program Optimization: Don't do it.
The Second Rule of Program Optimization (for experts only!): Don't do it yet.

최적화를 할 때는 다음 두 규칙을 따르라. 첫 번째. 하지 마라. 두 번째, (전문가 한정) 아직 하지 마라.
- 미카엘 A 잭슨

일정 이상의 모든 마이크로튜닝은 가독성을 훼손하고, 추상화 계층을 우회하여 추상화 누수(abstraction leak)를 발생시킨다. 그렇지 않은 경우에도 보편적인 최적화가 아닌 튜닝이 들어가므로 원래 인력을 대체하기 힘들어진다. 따라서, 마이크로 튜닝은 코드 베이스를 유지 보수하고 및 변화시키는데 비용과 시간이 더 들어가게 바꾸는 부작용이 있다. 또한 운영적인 관점에서 볼 때 모든 최적화는 잠재적인 버그를 동반한다. 따라서 안정화 과정이 필요한데 이 비용이 성능 개선으로 인한 이득보다 크다고 생각되면 최적화를 하지 않는 경우도 있다.
컴퓨터의 연산 성능은 하루가 다르게 발전하고 있고 소프트웨어 개발에 있어서 동작 시간의 중요성은 과거보다 많이 감소하였다. 어느 상황에서나 비즈니스 요구 사항이 항상 바뀔 수 있고 인력도 바뀔 수 있다. 모든 비즈니스에서 실행 속도만큼 중요한 것은 개발 속도 및 변화 속도를 늦추지 않는 것이다. 발생하지도 않는 병목 현상을 미리 걱정하며 코드 한 줄 한 줄에 신중함을 담는 행위는 개발 프로세스의 병목 현상을 불러오고 비즈니스에 심각한 위협을 초래한다. 프로그래머가 사전에 병목 현상이 발생할만한 소스 코드를 예상하기란 대단히 어렵고 실제 프로그램이 최적화를 하지 않아서 문제가 발생할지 예상하는 것은 더욱 어렵다.
서비스 서버를 Python이 아닌 C++로 짰다면 서버가 과부하로 터질 일이 없었을거야

분명히 완벽히 똑같은 동작을 하는 서버라면 C++이 몇 배 ~ 십 몇 배의 트래픽을 더 버틸 수 있었을 것이다. 이렇게 성능이 향상되면 유저 경험도 좋아진다. 하지만 경영적 관점에서 개발 비용과 기간 [12] 아끼는 데는 Python이 낫다. 스타트업은 자금이 쪼달리고 시간이 없기 때문에 후자를 선호한다.

더구나 개발 기간이 매우 줄어드는 것은 강력한 비즈니스적 메리트가 있는데, 첫 번째 릴리즈를 빨리 내서 빠르게 현 시장의 반응을 확인할 수 있기 때문이다. 모든 것을 최적화해서 한번에 냈다가는 엄청난 비용을 잡아먹고 몇 번이고 연기되다가 출시되었다 시류 변화로 인해 조용히 사라졌을 수도 있다. 특히나 애자일 주도 개발을 한다면 릴리즈 주기는 훨씬 줄어들고 돌아오는 피드백에 대응할 시간도 폭포수 모델에 비해 짧아진다. 이는 역으로 말해 개발 기간 단축이 애자일 프로세스 적용과 생산성 향상에 굉장한 메리트가 될 수 있음을 시사한다.

이외에도 법적인 규제 혹은 어른의 사정으로 인해 반드시 사용해야 하는 라이브러리, DB가 느린 경우가 있다. 이 경우 사용하는 라이브러리가 오픈소스가 아닌 이상 최적화 자체가 다른 회사 손에 달려있다.

5. 관련 문서


[1] Trade-off, 양면성 [2] 비주얼 스튜디오는 이것을 Unity(JUMBO)빌드라는 이름으로 IDE 차원에서 지원한다. [3] 소프트웨어와 하드웨어를 동시에 디자인하기 때문이다. 물론 소프트웨어와 하드웨어를 같이 디자인한다고 무조건 최적화가 잘 되는 건 아니다 그래서 최적화에는 엄청난 강점을 보이는 기업이고 눈으로 보면 하드웨어는 아주 고스펙은 아닌데 거의 최대한의 퍼포먼스를 뽑아낼 때가 많다. 단, 애플에서 내놓은 윈도우 프로그램은 발적화도 모자라서 악성코드급인 경우도 있다. 자세한 것은 iTunes QuickTime Player 항목 참조. [4] '1980년대부터'라고 했기 때문에 나사의 최적화로 가장 유명한 예시인 보이저 호에는 적용되지 않는다. 보이저 호가 발사된 것(1977년)은 인텔에서 8086을 발표(1978년)하기도 전의 일이라는 점을 고려하면 그 크기에 그 정도 스펙이면 당시로서는 상당히 훌륭한 수준이었다. 그래도 부족한 컴퓨팅 파워로 성능과 신뢰도를 얻어내기 위해 높은 수준의 최적화를 한 것은 맞다. [5] 충분히 빠르다면 튜닝하는 게 오히려 해가 된다고 하는 조언이 있다. [6] 가장 심각한 건 이식성의 파괴와 가독성의 포기 [7] 대표적인 C 컴파일러인 GCC 컴파일러의 경우 -O1, -O2 등의 옵션을 통해 최적화된 코드를 작성할 수 있다. [8] 기능상의 차이점은 전혀 없지만, Loop unrolling과 같이 속도 위주의 최적화를 사용하는 경우 컴파일된 코드 사이즈가 늘어나거나 흐름이나 순서 등에서 차이가 생길 수 있다. 컴파일러의 최적화 단계에 따라 결과 값의 차이가 있다면 Undefined behavior를 사용했을 경우가 높다. 즉 코드의 문제. [9] 두 포인터가 가리키는 값이 같다는 뜻이 아니라 두 포인터가 가리키는 메모리 주소가 같음을 의미한다. [10] 최적화 관련 명언 중에서 늘 언급되는 최고의 문장. 보통 '섣부른 최적화가 만악의 근원이다' 부분만 알려져 있지만 전채 맥락은 초보 개발자들이 엉뚱한 장소에서 최적화를 하느라 정작 중요한 부분을 놓친다는 것을 경고하는 의미이다. [11] TACP 책에 등장하는 원본 전체는 다음과 같다: "Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%." [12] 초기 개발 비용, 지속적인 유지 보수 및 디버깅 비용, 인건비



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


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