프로그래밍 언어가 변수 및 함수의 그림자 / 숨기기를 허용하는 이유는 무엇입니까?


31

가장 많이 사용되는 프로그래밍 언어 (예 : C ++, Java, Python 등)는 변수 또는 함수 를 숨기 거나 그림자 라는 개념을 가지고 있습니다. 숨기거나 그림자가 생기면 버그를 찾기가 어려웠으며 언어의 이러한 기능을 사용해야하는 경우를 본 적이 없습니다.

나에게 숨기와 그림자를 허용하지 않는 것이 나을 것 같습니다.

이 개념을 잘 사용하는 사람이 있습니까?

업데이트 :
나는 클래스 멤버 (개인 / 보호 멤버)의 캡슐화를 말하는 것이 아닙니다.


그래서 모든 필드 이름이 F로 시작합니다.
Pieter B

7
에릭 리퍼 트 ​​(Eric Lippert)가 이것에 관해 좋은 기사를 가지고 있다고 생각합니다. 아 잠깐만, 여기있다 : blogs.msdn.com/b/ericlippert/archive/2008/05/21/…
Lescai Ionel

1
질문을 명확히하십시오. 일반적으로 정보 숨기기 또는 파생 클래스가 기본 클래스의 함수를 숨기는 Lippert 기사에 설명 된 특정 사례에 대해 묻고 있습니까?
Aaron Kurtzhals

중요 사항 : 숨기기 / 그림자에 의해 발생하는 많은 버그에는 돌연변이가 있습니다 (예 : 잘못된 변수를 설정하고 왜 "변화가 발생하지 않는지"궁금합니다). 변경 불가능한 참조로 주로 작업 할 때 숨기기 / 그림자 사용은 훨씬 적은 문제를 야기하며 버그를 일으킬 가능성은 훨씬 적습니다.
Jack

답변:


26

숨기와 그림자를 허용하지 않으면 모든 변수가 전역 인 언어가 있습니다.

전역 변수 또는 함수를 숨길 수있는 로컬 변수 또는 함수를 허용하는 것보다 분명히 나쁩니다.

당신이 숨어과 음영을 허용, 경우 당신이 시도하는 특정 전역 변수, 당신은 컴파일러가 프로그래머 "미안 해요, 데이브,하지만 당신은 그 이름을 사용할 수 없습니다 알려주는 상황을 만들고"보호 ", 그것은 이미 사용 중입니다 " COBOL에 대한 경험은 프로그래머가이 상황에서 거의 즉시 욕설에 의존한다는 것을 보여줍니다.

근본적인 문제는 숨기고 그림자가 아니라 전역 변수입니다.


19
섀도 잉을 금지하는 또 다른 단점은 전역 변수를 추가하면 로컬 블록에서 변수가 이미 사용 되었기 때문에 코드가 손상 될 수 있다는 것입니다.
Giorgio

19
"숨김과 그림자를 허용하지 않으면 모든 변수가 전역 인 언어가 있습니다." -반드시 그런 것은 아닙니다 : 그림자없이 범위 변수를 가질 수 있으며 설명했습니다.
Thiago Silva

@ThiagoSilva은 : 그리고 당신의 언어는이 모듈이 있음을 컴파일러에게 할 수있는 방법이하는 IS 액세스에 해당 모듈의 "frammis"변수를 허용합니다. 자신도 모르는 물건을 숨기거나 가리는 것을 허용합니까, 아니면 그 이름을 사용할 수없는 이유를 알려주는 것이 있습니까?
John R. Strohm 2009 년

9
@ 필, 당신의 의견에 동의하지 않아서 실례 합니다만, OP는 "변수 또는 함수의 숨기기 / 섀도 잉"에 대해 물었고 "부모", "자식", "클래스"및 "멤버"라는 단어는 그의 질문에 아무 곳에도 나타나지 않습니다. 그것은 이름 범위에 대한 일반적인 질문으로 보일 것입니다.
John R. Strohm

3
@dylnmc, 나는 명백한 "2001 : A Space Odyssey"참조를 얻지 못할만큼 어리석은 족제비를 겪을만큼 오래 살기를 기대하지 않았다.
John R. Strohm

15

이 개념을 잘 사용하는 사람이 있습니까?

정확하고 설명적인 식별자를 사용하는 것이 항상 좋습니다.

변수 숨기기가 동일한 버그 / 유사한 유형 (변수 숨기기가 허용되지 않은 경우 수행 할 작업)의 이름이 비슷한 두 개의 변수가있을 경우 버그 및 / 또는 버그가 발생할 가능성이 높기 때문에 변수 숨기기로 인해 많은 버그가 발생하지 않는다고 주장 할 수 있습니다. 심각한 버그. 그 주장이 올바른지 모르겠지만 적어도 그럴듯하게 논란의 여지가 있습니다.

필드와 지역 변수를 구별하기 위해 일종의 헝가리 표기법을 사용하면이 문제가 발생하지만 유지 관리 (및 프로그래머 정신)에 영향을 미칩니다.

(아마도 개념이 처음에 알려진 이유 일 것입니다.) 언어가 숨기거나 그림자를 적용하는 것이 허용하지 않는 것보다 훨씬 쉽습니다. 구현이 쉬워지면 컴파일러에 버그가 발생할 가능성이 줄어 듭니다. 구현이 쉬워 짐에 따라 컴파일러는 작성 시간이 단축되어 플랫폼을보다 광범위하고 빠르게 채택 할 수 있습니다.


3
사실, 아니요, 숨기기 및 그림자를 구현하는 것은 쉽지 않습니다. 실제로는 "모든 변수는 전역"이라고 구현하는 것이 더 쉽습니다. 네임 스페이스는 하나만 필요하며 네임 스페이스가 여러 개 있고 이름을 내보낼 지 여부를 결정해야하는 것과 달리 항상 이름을 내 보냅니다.
John R. Strohm

5
@ JohnR.Strohm-물론, 어떤 종류의 범위 지정 (읽기 : 클래스)이 있으면 범위가 더 낮은 범위를 숨기면 무료로 제공됩니다.
Telastyn

범위와 수업은 다릅니다. 베이직을 제외하고 내가 프로그래밍 한 모든 언어의 범위가 있지만 모든 클래스 또는 객체 개념이있는 것은 아닙니다.
Michael Shaw

@ michaelshaw-물론 더 분명했습니다.
Telastyn

7

우리가 같은 페이지에 있는지 확인하기 위해 메소드 "숨김"은 파생 클래스가 기본 클래스의 멤버와 동일한 이름의 멤버를 정의 할 때입니다 (메소드 / 프로퍼티 인 경우 가상 / 재정의 가능한 것으로 표시되지 않음) ), 파생 된 클래스의 인스턴스에서 "파생 된 컨텍스트"를 호출하면 파생 된 멤버가 사용되는 반면 기본 클래스의 컨텍스트에서 동일한 인스턴스를 호출하면 기본 클래스 멤버가 사용됩니다. 이것은 기본 클래스 멤버가 파생 클래스가 대체를 정의 할 것으로 예상하는 멤버 추상화 / 재정의와, 원하는 범위 밖의 소비자로부터 멤버를 "숨기는"범위 / 가시성 수정 자와 다릅니다.

허용되는 이유에 대한 짧은 대답은 그렇게하지 않으면 개발자가 객체 지향 디자인의 몇 가지 주요 원칙을 위반하게된다는 것입니다.

더 긴 대답은 다음과 같습니다. 먼저 C #에서 멤버 숨기기를 허용하지 않는 대체 유니버스에서 다음 클래스 구조를 고려하십시오.

public interface IFoo
{
   string MyFooString {get;}
   int FooMethod();
}

public class Foo:IFoo
{
   public string MyFooString {get{return "Foo";}}
   public int FooMethod() {//incredibly useful code here};
}

public class Bar:Foo
{
   //public new string MyFooString {get{return "Bar";}}
}

Bar에서 멤버의 주석을 해제하고이를 통해 Bar가 다른 MyFooString을 제공하도록 허용하십시오. 그러나 멤버 숨기기에 대한 대체 현실 금지를 위반하기 때문에 그렇게 할 수 없습니다. 이 특정 예는 버그가 많지 않으며이를 금지하려는 주요 예입니다. 예를 들어, 다음을 수행하면 어떤 콘솔 출력을 얻을 수 있습니까?

Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;

Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);

내 머리 꼭대기에서 실제로 마지막 줄에 "Foo"또는 "Bar"가 표시되는지 확실하지 않습니다. 세 변수가 모두 정확히 동일한 상태의 동일한 인스턴스를 참조하더라도 첫 번째 줄에는 "Foo", 두 번째 줄에는 "Bar"가 표시됩니다.

따라서 대체 우주에서 언어 디자이너는 속성 숨기기를 방지하여이 잘못된 코드를 사용하지 않는 것이 좋습니다. 이제 여러분은 코더로서 정확히이 일을해야합니다. 한계를 어떻게 극복합니까? 음, 한 가지 방법은 Bar의 속성 이름을 다르게 지정하는 것입니다.

public class Bar:Foo
{
   public string MyBarString {get{return "Bar";}}       
}

완벽하게 합법적이지만 우리가 원하는 행동은 아닙니다. Bar의 인스턴스는 "Bar"를 생성 할 때 MyFooString 속성에 대해 항상 "Foo"를 생성합니다. IFoo가 특별히 Bar라는 것을 알아야 할뿐만 아니라 다른 접근자를 사용해야한다는 것도 알아야합니다.

또한 부모-자식 관계를 잊고 인터페이스를 직접 구현할 수도 있습니다.

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   public int FooMethod() {...}
}

이 간단한 예를 들어 그것은 완벽한 대답 한으로 만 관심을 푸와 바 모두 IFoos이라는 것을. Bar가 Foo가 아니므로 할당 할 수 없으므로 몇 가지 예제를 사용하는 사용 코드가 컴파일되지 않습니다. 그러나 Foo에 Bar가 필요로하는 유용한 "FooMethod"메소드가있는 경우 해당 메소드를 상속 할 수 없습니다. Bar에서 코드를 복제하거나 창의성을 발휘해야합니다.

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   private readonly theFoo = new Foo();

   public int FooMethod(){return theFoo.FooMethod();}
}

이것은 명백한 해킹이며, OO 언어 사양의 일부 구현은 이것보다 약간 더 중요하지만 개념적으로 잘못되었습니다. 바 필요의 소비자 푸의 기능을 노출하는 경우, 바한다 없습니다하는 푸 푸.

물론 Foo를 제어하면 가상으로 만든 다음 재정의 할 수 있습니다. 이것은 멤버가 재정의 될 것으로 예상 될 때 현재 유니버스에서 개념적인 모범 사례이며 숨기기를 허용하지 않는 대체 유니버스에서 유지됩니다.

public class Foo:IFoo
{
   public virtual string MyFooString {get{return "Foo";}}
   //...
}

public class Bar:Foo
{
   public override string MyFooString {get{return "Bar";}}
}

이것의 문제점은 가상 멤버 액세스가 수행 비용이 상대적으로 비싸기 때문에 일반적으로 필요할 때만 수행한다는 것입니다. 그러나 숨기지 않으면 소스 코드를 제어하지 않는 다른 코더가 다시 구현하기를 원할 수있는 멤버에 대해 비관적이어야합니다. 봉인되지 않은 클래스의 "모범 사례"는 특별히 원하지 않는 한 모든 것을 가상으로 만드는 것입니다. 또한 여전히 숨어있는 정확한 행동을 제공하지는 않습니다. 인스턴스가 Bar 인 경우 문자열은 항상 "Bar"입니다. 때로는 작업중인 상속 수준에 따라 숨겨진 상태 데이터 계층을 활용하는 것이 진정으로 유용한 경우가 있습니다.

요약하면, 멤버 숨기기를 허용하는 것이 이러한 악의 적은 것입니다. 그것을 갖지 않으면 일반적으로 그것을 허용하는 것보다 객체 지향적 원칙에 반하는 더 나쁜 잔학 행위로 이어질 것입니다.


실제 질문을 해결하기 위해 +1. 실제 멤버 숨기기 사용의 좋은 예 는 주제에 대한 Eric Libbert의 블로그 게시물 에 설명 된 IEnumerableIEnumerable<T>인터페이스 입니다.
Phil

재정의는 숨겨져 있지 않습니다 . 나는 이것이 문제를 해결한다는 @Phil의 의견에 동의하지 않습니다.
Jan Hudec

내 요점은 숨기기가 옵션이 아닌 경우 재정의가 숨기기를 대신 할 것이라는 점이었습니다. 나는 그것이 숨어 있지 않다는 데 동의하며 첫 번째 단락에서 많이 말합니다. C #에서 숨기지 않는 대체 현실 시나리오에 대한 해결 방법은 숨기지 않습니다. 그게 요점입니다.
KeithS

나는 당신의 그림자 / 숨김 사용을 좋아하지 않습니다. 내가 보는 주된 좋은 사용법은 (1) 새 버전의 기본 클래스가 이전 버전을 중심으로 설계된 소비자 코드와 충돌하는 멤버를 포함하는 상황을 극복하는 것입니다. (2) 리턴 형 공분산과 같은 가짜; (3) 특정 하위 유형에서 기본 클래스 메소드를 호출 할 수 있지만 유용 하지 않은 경우를 처리 합니다 . LSP는 전자를 필요로하지만, 기본 계급 계약이 일부 조건에서 일부 방법이 유용하지 않을 수 있다고 명시한 경우 후자를 필요로하지 않습니다.
supercat

2

솔직히, 에릭 Lippert의, C # 컴파일러 팀의 원칙 개발자는, 꽤 잘 설명 (링크에 대한 감사 Lescai Ionel을). .NET IEnumerableIEnumerable<T>인터페이스는 멤버 숨기기가 유용한 경우의 좋은 예입니다.

.NET 초기에는 제네릭이 없었습니다. 따라서 IEnumerable인터페이스는 다음과 같습니다.

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

이 인터페이스 foreach를 사용하면 객체 컬렉션 을 오버 할 수 있었지만 제대로 사용하려면 모든 객체를 캐스팅해야했습니다.

그런 다음 제네릭이 나왔습니다. 제네릭을 얻었을 때 새로운 인터페이스도 얻었습니다.

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

이제 반복하는 동안 객체를 캐스트 할 필요가 없습니다! 우와! 이제 멤버 숨기기가 허용되지 않으면 인터페이스는 다음과 같아야합니다.

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumeratorGeneric();
}

때문에, 바보의 종류 것 GetEnumerator()GetEnumeratorGeneric()거의 정확하게 할 두 경우 모두에서 같은 일을 하지만, 그들은 약간 다른 반환 값을 가지고있다. 실제로는 너무 비슷 하여 제네릭이 .NET에 도입되기 전에 작성된 레거시 코드로 작업하지 않는 한 항상 항상 제네릭 형식의 기본값을 사용하려고합니다 GetEnumerator.

때때로 회원 숨어 않는 불쾌한 코드 찾기 어려운 버그에 대한 더 많은 공간을 허용합니다. 그러나 레거시 코드를 손상시키지 않고 반환 유형을 변경하려는 경우와 같이 때로는 유용합니다. 이는 언어 설계자가 내려야 할 결정 중 하나 일뿐입니다.이 기능을 합법적으로 필요로하는 개발자에게 불편을 줍니까? 아니면이 기능을 언어에 포함시키고 오용의 대상이되는 사람들의 실수를 포착합니까?


형식적 IEnumerable<T>.GetEnumerator()으로은을 숨기는 반면 IEnumerable.GetEnumerator()이는 재정의 할 때 C #에 공변량 반환 유형이 없기 때문입니다. 논리적으로 이것은 LSP와 완전히 일치하는 재정의입니다. 숨기는 map것은 using namespace std(C ++에서) 하는 파일의 함수에 로컬 변수가있을 때 입니다.
Jan Hudec

2

귀하의 질문은 두 가지 방법으로 읽을 수 있습니다 : 일반적으로 변수 / 함수 범위에 대해 묻거나 상속 계층 구조의 범위에 대해보다 구체적인 질문을합니다. 상속을 구체적으로 언급하지는 않았지만 버그를 찾기가 어렵다고 언급했습니다. 버그는 일반 범위보다 상속 컨텍스트에서 범위와 더 비슷하게 들리므로 두 가지 질문에 모두 대답하겠습니다.

일반적으로 범위는 좋은 생각입니다. 프로그램의 특정 부분에 집중할 수 있기 때문입니다. 로컬 이름이 항상 이길 수 있기 때문에 주어진 범위에있는 프로그램의 일부만 읽으면 로컬에 정의 된 부분과 다른 곳에 정의 된 부분을 정확히 알 수 있습니다. 이름은 로컬을 의미하며,이 경우 해당 코드를 정의하는 코드가 바로 앞에 있거나 로컬 범위 외부에있는 참조입니다. 로컬 변수가 아닌 다른 참조가 없으면 (특히 전역 변수, 어디서나 변경 될 수 있음) 로컬 범위의 프로그램 부분이 참조 하지 않고 올바른지 여부를 평가할 수 있습니다 모두에서 프로그램의 나머지의 부분 .

때로는 몇 가지 버그로 이어질 수도 있지만, 가능한 많은 버그를 막아서 보상하는 것 이상입니다. 라이브러리 함수와 같은 이름으로 로컬 정의를 만드는 것 외에는 (그렇지 마십시오) 로컬 범위로 버그를 도입하는 쉬운 방법을 볼 수는 없지만 로컬 범위는 동일한 프로그램의 많은 부분을 사용할 수있게합니다 i는 서로를 방해하지 않고 루프의 인덱스 카운터로 사용하고 Fred는 복도에서 같은 이름의 문자열을 방해하지 않는 str이라는 문자열을 사용하는 함수를 작성합니다.

Bertrand Meyer가 상속의 맥락에서 오버로드를 논의 하는 흥미로운 기사 를 발견 했습니다 . 그는 구문 적 오버로드 (동일한 이름을 가진 두 개의 다른 것들이 있음을 의미)와 의미 론적 오버로드 (동일한 아이디어의 두 가지 다른 구현이 있음을 의미) 사이에 흥미로운 차이점을 제시합니다. 서브 클래스에서 다르게 구현하려고했기 때문에 시맨틱 오버로딩이 좋습니다. 구문 과부하는 우발적 인 이름 충돌로 인해 버그가 발생했습니다.

의도되고 버그 인 상속 상황에서의 오버로드의 차이는 의미론 (의미)이므로 컴파일러는 사용자가 한 일이 옳고 그른지 알 방법이 없습니다. 명확한 범위의 상황에서 올바른 답은 항상 로컬 문제이므로 컴파일러는 올바른 것이 무엇인지 파악할 수 있습니다.

Bertrand Meyer의 제안은 Eiffel과 같은 언어를 사용하는 것인데,이 같은 이름 충돌을 허용하지 않으며 프로그래머가 하나 또는 둘 다의 이름을 바꾸도록하여 문제를 완전히 피할 수 있습니다. 내 제안은 상속을 완전히 사용하지 말고 문제를 완전히 피하는 것입니다. 그중 하나를 수행 할 수 없거나 원하지 않는 경우 상속에 문제가 발생할 가능성을 줄이기 위해 여전히 할 수있는 일이 있습니다 .LSP (Liskov Substitution Principle)를 따르고 상속보다 구성을 선호하고 유지하십시오. 상속 계층 구조가 얕고 상속 계층 구조의 클래스를 작게 유지하십시오. 또한 일부 언어는 에펠과 같은 언어와 같이 오류가 발생하지 않더라도 경고를 표시 할 수 있습니다.


2

여기 내 센트가 있습니다.

프로그램은 자체 포함 된 프로그램 논리 단위 인 블록 (함수, 절차)으로 구성 될 수 있습니다. 각 블록은 이름 / 식별자를 사용하여 "사물"(변수, 기능, 절차)을 참조 할 수 있습니다. 이름에서 사물로의 이러한 매핑을 바인딩 이라고 합니다.

블록이 사용하는 이름은 세 가지 범주로 나뉩니다.

  1. 블록 내부에서만 알려진 로컬 정의 이름 (예 : 로컬 변수)
  2. 블록이 호출 될 때 값에 바인딩되고 호출자가 블록의 입력 / 출력 매개 변수를 지정하는 데 사용할 수있는 인수입니다.
  3. 블록이 포함되어 있고 블록 내에있는 환경에 정의 된 외부 이름 / 바인딩.

예를 들어 다음 C 프로그램을 고려하십시오.

#include<stdio.h>

void print_double_int(int n)
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4);
}

이 함수 print_double_int에는 로컬 이름 (local variable) d및 argument 가 있으며 범위에 있지만 로컬로 정의되지 않은 n외부 전역 이름을 사용합니다 printf.

공지 사항 printf또한 인수로 전달 될 수있다 :

#include<stdio.h>

void print_double_int(int n, int printf(const char *, ...))
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4, printf);
}

일반적으로 인수는 함수 (프로 시저, 블록)의 입력 / 출력 매개 변수를 지정하는 데 사용되는 반면 전역 이름은 "환경에 존재하는"라이브러리 함수와 같은 것을 나타내는 데 사용되므로 언급하는 것이 더 편리합니다. 필요할 때만. 전역 이름 대신 인수를 사용 하는 것이 의존성 주입 의 주요 아이디어이며 , 컨텍스트를 보면서 의존성을 해결하지 않고 명시 적으로 만들어야 할 때 사용됩니다.

외부 적으로 정의 된 이름의 다른 유사한 사용은 클로저에서 찾을 수 있습니다. 이 경우 블록의 어휘 컨텍스트에 정의 된 이름을 블록 내에서 사용할 수 있으며 해당 이름에 바인딩 된 값은 블록이 참조하는 한 (일반적으로) 계속 존재합니다.

이 스칼라 코드를 예로 들어 보겠습니다.

object ClosureExample
{
  def createMultiplier(n: Int) = (m: Int) => m * n

  def main(args: Array[String])
  {
    val multiplier3 = createMultiplier(3)
    val multiplier5 = createMultiplier(5)

    // Prints 6.
    println(multiplier3(2))

    // Prints 10.
    println(multiplier5(2))
  }
}

함수의 반환 값은 createMultiplier클로저 (m: Int) => m * n이며, 여기에는 인수 m와 외부 이름이 포함 n됩니다. n클로저가 정의 된 컨텍스트를 보면 이름 이 결정됩니다. 이름은 nfunction 인수 에 바인딩됩니다 createMultiplier. 이 바인딩은 클로저가 생성 될 때, 즉 createMultiplier호출 될 때 생성 됩니다. 따라서 이름 n은 함수의 특정 호출에 대한 인수의 실제 값에 바인딩됩니다. printf프로그램의 실행 파일이 빌드 될 때 링커가 해결하는와 같은 라이브러리 함수의 경우와 대조하십시오 .

요약하면 로컬 코드 블록 내에서 외부 이름을 참조하면 유용합니다.

  • 외부 적으로 정의 된 이름을 인수로 명시 적으로 전달할 필요가 없습니다.
  • 블록이 생성 될 때 런타임에 바인딩을 고정한 다음 나중에 블록이 호출 될 때 바인딩에 액세스 할 수 있습니다.

블록에서 환경에 정의 된 관련 이름 (예 : printf사용하려는 기능) 에만 관심이 있다고 생각하면 섀도 잉이 발생합니다 . 우연히 로컬 이름을 사용하려면 ( getc, putc, scanf, ...) 이미 환경에서 사용되어왔다, 당신은 간단한 희망은 전역 이름을 무시 (그림자)합니다. 따라서 로컬에서 생각할 때 전체 (아마도 매우 큰) 컨텍스트를 고려하고 싶지 않습니다.

다른 방향으로, 전 세계적으로 생각할 때 로컬 컨텍스트의 내부 세부 사항 (캡슐화)을 무시하려고합니다. 따라서 섀도 잉이 필요합니다. 그렇지 않으면 전역 이름을 추가하면 해당 이름을 이미 사용하고 있던 모든 로컬 블록이 손상 될 수 있습니다.

결론적으로, 코드 블록이 외부 적으로 정의 된 바인딩을 참조하도록하려면 전역 이름으로부터 로컬 이름을 보호하기 위해 섀도 잉이 필요합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.