"효과적인 Go"의 두 판 사이의 차이

 
(같은 사용자의 중간 판 5개는 보이지 않습니다)
13번째 줄: 13번째 줄:


=== 예시 ===
=== 예시 ===
[https://go.dev/src/ Go 패키지 소스]는 핵심 라이브러리 역할뿐만 아니라 언어 사용 예시로도 활용될 수 있도록 작성되었습니다. 또한, 많은 패키지에는 [https://go.dev/ go.dev] 웹사이트에서 직접 실행할 수 있는 작동가능한 자체 포함 실행 예제가 포함되어 있습니다. (필요한 경우 "Example"이라는 단어를 클릭하여 엽니다.) 문제를 해결하는 방법이나 특정 기능 구현 방법에 대해 궁금한 점이 있다면, 라이브러리의 문서, 코드 및 예제가 답변, 아이디어, 배경을 제공할 수 있습니다.


== 포맷팅 ==
== 포맷팅 ==
33번째 줄: 34번째 줄:
=== Defer ===
=== Defer ===
== 데이터 ==
== 데이터 ==
=== new로 할당 ===
=== <code>new</code>로 할당 ===
Go에는 메모리를 할당하는 두 가지 기본 기능인 <code>new</code>와 <code>make</code>가 있습니다. 이 둘은 서로 다른 일을 수행하며 다른 타입에 적용되기 때문에 헷갈릴 수 있지만, 그 규칙은 간단합니다. 먼저 <code>new</code>에 대해 알아보겠습니다. <code>new</code>는 메모리를 할당하는 빌트인 함수이지만, 다른 언어의 이름과 비슷한 기능들과는 달리 메모리를 초기화하지 않고, 단지 제로로 설정만 합니다. 즉, <code>new(T)</code>는 타입 T의 새 항목을 위한 제로로 채워진 저장 공간을 할당하고 해당 주소를 반환하며, 이는 <code>*T</code> 타입의 값이 됩니다. Go 용어로, 이는 타입 <code>T</code>의 새로 할당된 제로값에 대한 포인터를 반환합니다.
 
<code>new</code>에 의해 반환된 메모리가 제로로 설정되기 때문에, 데이터 구조를 설계할 때 각 타입의 제로값을 추가 초기화 없이 사용할 수 있도록 배열하는 것이 유용합니다. 이렇게 하면 데이터 구조의 사용자가 <code>new</code>를 사용하여 하나를 생성하고 바로 사용할 수 있습니다. 예를 들어, <code>bytes.Buffer</code>의 문서는 "Buffer의 제로값은 사용 가능한 빈 버퍼입니다."라고 설명하고 있습니다. 마찬가지로 <code>sync.Mutex</code>는 명시적인 생성자나 <code>Init</code> 메서드가 없습니다. 대신, <code>sync.Mutex</code>의 제로값은 잠금 해제된 뮤텍스로 정의됩니다.
 
제로값이 유용하다는 특성은 전이적으로 적용됩니다. 다음 타입 선언을 살펴보세요.
 
<syntaxhighlight lang='go'>
type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}
</syntaxhighlight>
 
<code>SyncedBuffer</code> 타입의 값들은 할당되거나 선언된 직후에 바로 사용할 준비가 되어 있습니다. 다음 코드 스니펫에서 <code>p</code>와 <code>v</code> 모두 추가 작업 없이 올바르게 작동합니다.
 
<syntaxhighlight lang='go'>
p := new(SyncedBuffer)  // 타입: *SyncedBuffer
var v SyncedBuffer      // 타입:  SyncedBuffer
</syntaxhighlight>
 
=== 생성자와 복합 리터럴 ===
=== 생성자와 복합 리터럴 ===
때로는 제로값만으로는 충분하지 않아 초기화 생성자가 필요할 때도 있습니다. 다음은 <code>os</code> 패키지에서 파생된 예제입니다.
<syntaxhighlight lang='go'>
func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}
</syntaxhighlight>
위 코드에는 많은 보일러플레이트가 있습니다. 이를 복합 리터럴을 사용하여 간단하게 만들 수 있습니다. 복합 리터럴은 평가될 때마다 새 인스턴스를 생성하는 표현식입니다.
<syntaxhighlight lang='go'>
func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}
</syntaxhighlight>
C 언어와 달리, 지역 변수의 주소를 반환하는 것은 전혀 문제가 없습니다. 해당 변수와 연관된 저장 공간은 함수가 반환된 후에도 유지됩니다. 실제로 복합 리터럴의 주소를 가져오면 평가될 때마다 새 인스턴스를 할당합니다. 따라서 마지막 두 줄을 다음과 같이 결합할 수 있습니다.
<syntaxhighlight lang='go'>
    return &File{fd, name, nil, 0}
</syntaxhighlight>
복합 리터럴의 필드는 순서대로 배치되며 모두 있어야 합니다. 그러나 요소를 '''필드<code>:</code>값''' 쌍으로 명시하면 초기값이 어떤 순서로 나타나도 상관없으며, 누락된 값은 각각의 제로값으로 남습니다. 따라서 다음과 같이 작성할 수 있습니다.
<syntaxhighlight lang='go'>
    return &File{fd: fd, name: name}
</syntaxhighlight>
한계 상황으로, 복합 리터럴에 필드가 전혀 포함되지 않은 경우 해당 타입의 제로값을 생성합니다. <code>new(File)</code>와 <code>&File{}</code> 표현식은 동일합니다.
복합 리터럴은 배열, 슬라이스, 맵에 대해서도 생성할 수 있으며, 필드 레이블은 적절하게 인덱스나 맵 키가 됩니다. 다음 예제에서, 초기화는 <code>Enone</code>, <code>Eio</code>, <code>Einval</code>의 값에 상관없이 작동하며, 이 값들은 서로 구분되어 있기만 하면 됩니다.
<syntaxhighlight lang='go'>
a := [...]string  {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string      {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
</syntaxhighlight>
=== make로 할당 ===
=== make로 할당 ===
=== 배열 ===
=== 배열 ===
71번째 줄: 142번째 줄:
=== 복구 ===
=== 복구 ===
== 웹 서버 ==
== 웹 서버 ==
이제 Go 프로그램을 완성해 봅시다. 이번에는 웹 재서버(web re-server)입니다. Google은 <code>chart.apis.google.com</code>에서 데이터를 차트와 그래프로 자동으로 포맷팅하는 서비스를 제공합니다. 그러나 이 서비스를 상호작용적으로 사용하기는 어렵습니다. 왜냐하면 데이터를 쿼리로 URL에 넣어야 하기 때문입니다. 여기서 제공되는 프로그램은 한 가지 형태의 데이터에 대해 더 나은 인터페이스를 제공합니다. 짧은 텍스트 조각을 입력하면, 이 프로그램은 QR 코드(텍스트를 인코딩하는 상자의 매트릭스)를 생성하기 위해 차트 서버를 호출합니다. 이 이미지는 휴대전화의 카메라로 잡아 URL로 해석할 수 있으며, 이를 통해 휴대전화의 작은 키보드에 URL을 입력하는 수고를 덜 수 있습니다.
이제 Go 프로그램을 완성해 봅시다. 이번에는 웹 리-서버(web re-server)입니다. Google은 <code>chart.apis.google.com</code>에서 데이터를 차트와 그래프로 자동으로 포맷팅하는 서비스를 제공합니다. 그러나 이 서비스를 상호작용적으로 사용하기는 어렵습니다. 왜냐하면 데이터를 쿼리로 URL에 넣어야 하기 때문입니다. 여기서 제공되는 프로그램은 한 가지 형태의 데이터에 대해 더 나은 인터페이스를 제공합니다. 짧은 텍스트 조각을 입력하면, 이 프로그램은 QR 코드(텍스트를 인코딩하는 상자의 매트릭스)를 생성하기 위해 차트 서버를 호출합니다. 이 이미지는 휴대전화의 카메라로 잡아 URL로 해석할 수 있으며, 이를 통해 휴대전화의 작은 키보드에 URL을 입력하는 수고를 덜 수 있습니다.


다음은 Go로 작성된 웹 서버 프로그램입니다:
다음은 Go로 작성된 웹 서버 프로그램입니다:
131번째 줄: 202번째 줄:
<code>QR</code> 함수는 요청을 수신하고, 요청에는 폼 데이터가 포함되며, 폼 값에 있는 데이터를 사용하여 템플릿을 실행합니다.
<code>QR</code> 함수는 요청을 수신하고, 요청에는 폼 데이터가 포함되며, 폼 값에 있는 데이터를 사용하여 템플릿을 실행합니다.


템플릿 패키지인 <code>html/template</code>강력합니다. 이 프로그램은 그 기능의 일부분만 다룹니다. 본질적으로, <code>templ.Execute</code>에 전달된 데이터 항목에서 파생된 요소를 대체함으로써 HTML 텍스트의 일부를 동적으로 재작성합니다. 템플릿 텍스트(<code>templateStr</code>) 내에서 중괄호 두 개로 구분된 부분은 템플릿 동작을 나타냅니다. <code><nowiki>{{if .}}</nowiki></code>에서 <code><nowiki>{{end}}</nowiki></code>까지의 부분은 현재 데이터 항목의 값이 비어 있지 않은 경우에만 실행됩니다. 즉, 문자열이 비어 있을 때는 이 템플릿의 일부가 억제됩니다.
템플릿 패키지인 <code>html/template</code>강력합니다. 이 프로그램은 그 기능의 일부분만 다룹니다. 본질적으로, <code>templ.Execute</code>에 전달된 데이터 항목에서 파생된 요소를 대체함으로써 HTML 텍스트의 일부를 동적으로 재작성합니다. 템플릿 텍스트(<code>templateStr</code>) 내에서 중괄호 두 개로 구분된 부분은 템플릿 동작을 나타냅니다. <code><nowiki>{{if .}}</nowiki></code>에서 <code><nowiki>{{end}}</nowiki></code>까지의 부분은 현재 데이터 항목의 값이 비어 있지 않은 경우에만 실행됩니다. 즉, 문자열이 비어 있을 때는 이 템플릿의 일부가 억제됩니다.


<code><nowiki>{{.}}</nowiki></code>이라는 두 개의 스니핏은 템플릿에 제공된 데이터, 즉 쿼리 문자열을 웹 페이지에 표시하라는 의미입니다. HTML 템플릿 패키지는 자동으로 적절한 이스케이프 처리를 제공하여 텍스트가 안전하게 표시되도록 합니다.
<code><nowiki>{{.}}</nowiki></code>이라는 두 개의 스니핏은 템플릿에 제공된 데이터, 즉 쿼리 문자열을 웹 페이지에 표시하라는 의미입니다. HTML 템플릿 패키지는 자동으로 적절한 이스케이프 처리를 제공하여 텍스트가 안전하게 표시되도록 합니다.

2024년 9월 28일 (토) 16:01 기준 최신판

1 개요[ | ]

Crystal Clear action info.png 작성 중인 문서입니다.
Effective Go
이펙티브 Go, 효과적인 Go

https://go.dev/doc/effective_go

2 소개[ | ]

Go는 새로운 언어입니다. 기존 언어에서 아이디어를 차용했지만, 효과적인 Go 프로그램은 기존 언어로 작성된 프로그램과 성격이 다릅니다. C++ 또는 Java 프로그램을 Go로 직접 번역하는 것은 만족스러운 결과가 되지 못할 가능성이 큽니다. Java 프로그램은 Java로 작성되어야지 Go로 작성되지 않습니다. 반면, Go의 관점에서 문제를 생각하면 성공적이지만 상당히 다른 프로그램을 만들 수 있습니다. 즉, Go를 잘 작성하려면 Go의 속성과 관용구를 이해하는 것이 중요합니다. 또한, 이름 지정, 포맷팅, 프로그램 구성 등 Go 프로그래밍의 기존 규칙을 아는 것도 중요합니다. 그래야 작성한 프로그램이 다른 Go 프로그래머에게 쉽게 이해될 수 있습니다.

이 문서는 명확하고 관용적인 Go 코드를 작성하기 위한 팁을 제공합니다. 언어 사양, Go 투어, Go 코드 작성 방법과 같은 문서를 먼저 읽은 후 보충 자료로 활용하시기 바랍니다.

2022년 1월 추가사항: 이 문서는 2009년 Go의 출시를 위해 작성되었으며, 그 이후로 크게 업데이트되지 않았습니다. 언어 자체를 이해하는 데는 좋은 가이드이지만, 언어의 안정성 덕분에 라이브러리에 대해 거의 언급하지 않으며, 빌드 시스템, 테스트, 모듈, 다형성 등 Go 생태계의 중요한 변화에 대해서는 아무런 언급이 없습니다. 너무 많은 변화가 있었고, 현대 Go 사용법을 잘 설명하는 많은 문서, 블로그, 책들이 존재하기 때문에 이 문서를 업데이트할 계획은 없습니다. 효과적인 Go는 여전히 유용하지만, 독자는 이 문서가 완전한 가이드가 아님을 이해해야 합니다. 자세한 내용은 이슈 28782를 참조하세요.

2.1 예시[ | ]

Go 패키지 소스는 핵심 라이브러리 역할뿐만 아니라 언어 사용 예시로도 활용될 수 있도록 작성되었습니다. 또한, 많은 패키지에는 go.dev 웹사이트에서 직접 실행할 수 있는 작동가능한 자체 포함 실행 예제가 포함되어 있습니다. (필요한 경우 "Example"이라는 단어를 클릭하여 엽니다.) 문제를 해결하는 방법이나 특정 기능 구현 방법에 대해 궁금한 점이 있다면, 라이브러리의 문서, 코드 및 예제가 답변, 아이디어, 배경을 제공할 수 있습니다.

3 포맷팅[ | ]

4 주석[ | ]

5 이름[ | ]

5.1 패키지 이름[ | ]

5.2 게터[ | ]

5.3 인터페이스 이름[ | ]

5.4 MixedCaps[ | ]

6 세미콜론[ | ]

7 제어 구조[ | ]

7.1 If[ | ]

7.2 재선언과 재할당[ | ]

7.3 For[ | ]

7.4 Switch[ | ]

7.5 타입 switch[ | ]

8 함수[ | ]

8.1 멀티 반환 값[ | ]

8.2 명명된 결과 파라미터[ | ]

8.3 Defer[ | ]

9 데이터[ | ]

9.1 new로 할당[ | ]

Go에는 메모리를 할당하는 두 가지 기본 기능인 newmake가 있습니다. 이 둘은 서로 다른 일을 수행하며 다른 타입에 적용되기 때문에 헷갈릴 수 있지만, 그 규칙은 간단합니다. 먼저 new에 대해 알아보겠습니다. new는 메모리를 할당하는 빌트인 함수이지만, 다른 언어의 이름과 비슷한 기능들과는 달리 메모리를 초기화하지 않고, 단지 제로로 설정만 합니다. 즉, new(T)는 타입 T의 새 항목을 위한 제로로 채워진 저장 공간을 할당하고 해당 주소를 반환하며, 이는 *T 타입의 값이 됩니다. Go 용어로, 이는 타입 T의 새로 할당된 제로값에 대한 포인터를 반환합니다.

new에 의해 반환된 메모리가 제로로 설정되기 때문에, 데이터 구조를 설계할 때 각 타입의 제로값을 추가 초기화 없이 사용할 수 있도록 배열하는 것이 유용합니다. 이렇게 하면 데이터 구조의 사용자가 new를 사용하여 하나를 생성하고 바로 사용할 수 있습니다. 예를 들어, bytes.Buffer의 문서는 "Buffer의 제로값은 사용 가능한 빈 버퍼입니다."라고 설명하고 있습니다. 마찬가지로 sync.Mutex는 명시적인 생성자나 Init 메서드가 없습니다. 대신, sync.Mutex의 제로값은 잠금 해제된 뮤텍스로 정의됩니다.

제로값이 유용하다는 특성은 전이적으로 적용됩니다. 다음 타입 선언을 살펴보세요.

type SyncedBuffer struct {
    lock    sync.Mutex
    buffer  bytes.Buffer
}

SyncedBuffer 타입의 값들은 할당되거나 선언된 직후에 바로 사용할 준비가 되어 있습니다. 다음 코드 스니펫에서 pv 모두 추가 작업 없이 올바르게 작동합니다.

p := new(SyncedBuffer)  // 타입: *SyncedBuffer
var v SyncedBuffer      // 타입:  SyncedBuffer

9.2 생성자와 복합 리터럴[ | ]

때로는 제로값만으로는 충분하지 않아 초기화 생성자가 필요할 때도 있습니다. 다음은 os 패키지에서 파생된 예제입니다.

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := new(File)
    f.fd = fd
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

위 코드에는 많은 보일러플레이트가 있습니다. 이를 복합 리터럴을 사용하여 간단하게 만들 수 있습니다. 복합 리터럴은 평가될 때마다 새 인스턴스를 생성하는 표현식입니다.

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    f := File{fd, name, nil, 0}
    return &f
}

C 언어와 달리, 지역 변수의 주소를 반환하는 것은 전혀 문제가 없습니다. 해당 변수와 연관된 저장 공간은 함수가 반환된 후에도 유지됩니다. 실제로 복합 리터럴의 주소를 가져오면 평가될 때마다 새 인스턴스를 할당합니다. 따라서 마지막 두 줄을 다음과 같이 결합할 수 있습니다.

    return &File{fd, name, nil, 0}

복합 리터럴의 필드는 순서대로 배치되며 모두 있어야 합니다. 그러나 요소를 필드: 쌍으로 명시하면 초기값이 어떤 순서로 나타나도 상관없으며, 누락된 값은 각각의 제로값으로 남습니다. 따라서 다음과 같이 작성할 수 있습니다.

    return &File{fd: fd, name: name}

한계 상황으로, 복합 리터럴에 필드가 전혀 포함되지 않은 경우 해당 타입의 제로값을 생성합니다. new(File)&File{} 표현식은 동일합니다.

복합 리터럴은 배열, 슬라이스, 맵에 대해서도 생성할 수 있으며, 필드 레이블은 적절하게 인덱스나 맵 키가 됩니다. 다음 예제에서, 초기화는 Enone, Eio, Einval의 값에 상관없이 작동하며, 이 값들은 서로 구분되어 있기만 하면 됩니다.

a := [...]string   {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string      {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}

9.3 make로 할당[ | ]

9.4 배열[ | ]

9.5 슬라이스[ | ]

9.6 2차원 슬라이스[ | ]

9.7[ | ]

9.8 출력[ | ]

9.9 덧붙이기[ | ]

10 초기화[ | ]

10.1 상수[ | ]

10.2 변수[ | ]

10.3 init 함수[ | ]

11 메소드[ | ]

11.1 포인터 vs 값[ | ]

12 인터페이스와 기타 타입[ | ]

12.1 인터페이스[ | ]

12.2 변환[ | ]

12.3 인터페이스 변환과 타입 어썰션[ | ]

12.4 일반성[ | ]

12.5 인터페이스와 메소드[ | ]

13 빈 식별자[ | ]

13.1 멀티 할당에서 빈 식별자[ | ]

13.2 미사용 임포트와 변수[ | ]

13.3 사이드 이펙트용 임포트[ | ]

13.4 인터페이스 체크[ | ]

14 임베딩[ | ]

15 동시성[ | ]

15.1 통신으로 공유[ | ]

15.2 고루틴[ | ]

15.3 채널[ | ]

15.4 채널의 채널[ | ]

15.5 병렬화[ | ]

15.6 누수 버퍼[ | ]

16 오류[ | ]

16.1 패닉[ | ]

16.2 복구[ | ]

17 웹 서버[ | ]

이제 Go 프로그램을 완성해 봅시다. 이번에는 웹 리-서버(web re-server)입니다. Google은 chart.apis.google.com에서 데이터를 차트와 그래프로 자동으로 포맷팅하는 서비스를 제공합니다. 그러나 이 서비스를 상호작용적으로 사용하기는 어렵습니다. 왜냐하면 데이터를 쿼리로 URL에 넣어야 하기 때문입니다. 여기서 제공되는 프로그램은 한 가지 형태의 데이터에 대해 더 나은 인터페이스를 제공합니다. 짧은 텍스트 조각을 입력하면, 이 프로그램은 QR 코드(텍스트를 인코딩하는 상자의 매트릭스)를 생성하기 위해 차트 서버를 호출합니다. 이 이미지는 휴대전화의 카메라로 잡아 URL로 해석할 수 있으며, 이를 통해 휴대전화의 작은 키보드에 URL을 입력하는 수고를 덜 수 있습니다.

다음은 Go로 작성된 웹 서버 프로그램입니다:

package main

import (
    "flag"
    "html/template"
    "log"
    "net/http"
)

var addr = flag.String("addr", ":1718", "http service address") // Q=17, R=18

var templ = template.Must(template.New("qr").Parse(templateStr))

func main() {
    flag.Parse()
    http.Handle("/", http.HandlerFunc(QR))
    err := http.ListenAndServe(*addr, nil)
    if err != nil {
        log.Fatal("ListenAndServe:", err)
    }
}

func QR(w http.ResponseWriter, req *http.Request) {
    templ.Execute(w, req.FormValue("s"))
}

const templateStr = `
<html>
<head>
<title>QR Link Generator</title>
</head>
<body>
{{if .}}
<img src="http://chart.apis.google.com/chart?chs=300x300&cht=qr&choe=UTF-8&chl={{.}}" />
<br>
{{.}}
<br>
<br>
{{end}}
<form action="/" name=f method="GET">
    <input maxLength=1024 size=70 name=s value="" title="Text to QR Encode">
    <input type=submit value="Show QR" name=qr>
</form>
</body>
</html>
`


main 함수까지는 쉽게 따라올 수 있습니다. 이 플래그는 서버의 기본 HTTP 포트를 설정합니다. 템플릿 변수인 templ부터 재미있어 집니다. 이 변수는 HTML 템플릿을 빌드하여 서버가 페이지를 표시할 때 실행하게 합니다. 이에 대해서는 잠시 후에 더 자세히 설명하겠습니다.

main 함수는 플래그를 해석하고, 앞서 언급한 메커니즘을 사용하여 QR 함수를 서버의 루트 경로에 바인딩합니다. 그런 다음 http.ListenAndServe를 호출하여 서버를 시작합니다. 이 함수는 서버가 실행되는 동안 블록됩니다.

QR 함수는 요청을 수신하고, 요청에는 폼 데이터가 포함되며, 폼 값에 있는 데이터를 사용하여 템플릿을 실행합니다.

템플릿 패키지인 html/template은 강력합니다. 이 프로그램은 그 기능의 일부분만 다룹니다. 본질적으로, templ.Execute에 전달된 데이터 항목에서 파생된 요소를 대체함으로써 HTML 텍스트의 일부를 동적으로 재작성합니다. 템플릿 텍스트(templateStr) 내에서 중괄호 두 개로 구분된 부분은 템플릿 동작을 나타냅니다. {{if .}}에서 {{end}}까지의 부분은 현재 데이터 항목의 값이 비어 있지 않은 경우에만 실행됩니다. 즉, 문자열이 비어 있을 때는 이 템플릿의 일부가 억제됩니다.

{{.}}이라는 두 개의 스니핏은 템플릿에 제공된 데이터, 즉 쿼리 문자열을 웹 페이지에 표시하라는 의미입니다. HTML 템플릿 패키지는 자동으로 적절한 이스케이프 처리를 제공하여 텍스트가 안전하게 표시되도록 합니다.

템플릿 문자열의 나머지 부분은 페이지가 로드될 때 표시할 HTML입니다. 이 설명이 너무 빠르면 template 패키지의 문서를 참조하여 더 자세한 설명을 확인하십시오.

이제 몇 줄의 코드와 일부 데이터 기반 HTML 텍스트로 유용한 웹 서버를 만들 수 있습니다. Go 언어는 몇 줄의 코드로 많은 일을 할 수 있을 만큼 강력합니다.

18 같이 보기[ | ]

19 참고[ | ]

문서 댓글 ({{ doc_comments.length }})
{{ comment.name }} {{ comment.created | snstime }}