성태의 닷넷 이야기
홈 주인
모아 놓은 자료
프로그래밍
질문/답변
사용자 관리
사용자
메뉴
아티클
외부 아티클
유용한 코드
온라인 기능
MathJax 입력기
최근 덧글
[정성태] 아래의 글을 보면, MoveWindow 하면 될 듯한데요. ^^...
[Tom Lee] 안녕하세요 올려주신 글 참고하여 WPF 어플리케이션 안에 Uni...
[정성태] A graphical depiction of the steps ...
[정성태] 질문을 주셔서 출판사 측에 문의를 했습니다. 약 한 달 정도 후...
[Thorondor
] @정성태 개인 블로그인데도 거의 커뮤니티 급 인 것 같아요. 요...
[정성태] Roll A Lisp In C - Reading ; https...
[정성태] Java - How to use the Foreign Funct...
[정성태] 제가 큰 실수를 했군요. ^^; Delegate를 통한 Bein...
[정성태] Working with Rust Libraries from C#...
[정성태] Detecting blocking calls using asyn...
글쓰기
제목
이름
암호
전자우편
HTML
홈페이지
유형
제니퍼 .NET
닷넷
COM 개체 관련
스크립트
VC++
VS.NET IDE
Windows
Team Foundation Server
디버깅 기술
오류 유형
개발 환경 구성
웹
기타
Linux
Java
DDK
Math
Phone
Graphics
사물인터넷
부모글 보이기/감추기
내용
<div style='display: inline'> <h1 style='font-family: Malgun Gothic, Consolas; font-size: 20pt; color: #006699; text-align: center; font-weight: bold'>(번역글) .NET Internals Cookbook Part 6 - Object internals</h1> <p> <br /> 이번에도 .NET Internals Cookbook 시리즈의 6번째 글을 번역한 것입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > .NET Internals Cookbook Part 6 - Object internals ; <a target='tab' href='https://blog.adamfurmanek.pl/2019/03/23/net-internals-cookbook-part-6/'>https://blog.adamfurmanek.pl/2019/03/23/net-internals-cookbook-part-6/</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>36. Shim이란?</div> <br /> 지난 글에서 Native와 .NET EXE의 EntryPoint에 대한 설명을 했는데요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > EXE를 LoadLibrary로 로딩해 PE 헤더에 있는 EntryPoint를 직접 호출하는 방법 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11858'>http://www.sysnet.pe.kr/2/0/11858</a> windbg - .NET x86 CLR2/CLR4 EXE의 EntryPoint ; <a target='tab' href='https://www.sysnet.pe.kr/2/0/11861'>https://www.sysnet.pe.kr/2/0/11861</a> windbg - .NET x64 EXE의 EntryPoint ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11863'>http://www.sysnet.pe.kr/2/0/11863</a> </pre> <br /> 어쨌든 .NET EXE의 실행은 초기에 어셈블리의 메타데이터를 체크해 그에 맞는 .NET 런타임을 로드한 후 _CorExeMain으로 실행을 넘기는 일련의 작업을 합니다. 이러한 mscoree.dll의 작업을 "Shim"을 로딩한다고 합니다.<br /> <br /> 이런 작업은 개발자가 임의로 C/C++ 언어를 이용해 작성할 수 있으며 이에 대해서는 전에 다음의 글로 설명한 적이 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1746'>http://www.sysnet.pe.kr/2/0/1746</a> </pre> <br /> 사실 Shim이란 용어는 유사한 상황에서 자주 사용됩니다. 가령 64비트 운영체제에서 32비트 응용 프로그램을 실행한 경우 32비트 함수 호출을 64비트 API에 투명하게 연결해 주는 WoW64 층도 Shim이라고 불립니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>37. 구조체의 최소 크기는?</div> <br /> 단순히 생각해 보면 필드가 없는 경우라면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Runtime.InteropServices; public class Program { public static void Main() { Console.WriteLine(Marshal.SizeOf(typeof(Foo))); <span style='color: blue; font-weight: bold'>// 출력 결과: 1</span> Console.WriteLine(Marshal.SizeOf(typeof(Bar))); // 출력 결과: 4 } } struct Foo { } struct Bar { int x; } </pre> <br /> 당연히 구조체의 크기는 0이 나와야겠지만, 그렇게 되면 2개의 구조체 인스턴스가 정의된 경우,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Foo a1 = new Foo(); Bar b2 = new Bar(); </pre> <br /> a1의 구조체 크기가 0이므로 b2가 가리키는 변수도 같은 지점이 될 것입니다. 따라서 이러한 상황을 방지하기 위해 필드가 없는 구조체일지라도 최소 1바이트의 크기를 점유합니다.<br /> <br /> 그렇다면 구조체가 아닌 클래스라면 어떨까요? 이에 대해서는 전에 설명한 데로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > windbg에서 확인해 보는 관리 힙의 인스턴스 구조 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1559'>http://www.sysnet.pe.kr/2/0/1559</a> C#에서 확인해 보는 관리 힙의 인스턴스 구조 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1176'>http://www.sysnet.pe.kr/2/0/1176</a> 일반 참조형의 기본 메모리 소비는 얼마나 될까요? ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1174'>http://www.sysnet.pe.kr/2/0/1174</a> </pre> <br /> 다음의 3가지 영역을 필요로 하므로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Object Header + MethodTable + (비어 있어도) Field 1개 영역 </pre> <br /> 32비트에서는 12바이트, 64비트에서는 24바이트가 필요하게 됩니다.<br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>38. sync block이란?</div> <br /> 이 글의 답변은 다음의 글로 대신합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > .NET 참조 개체 인스턴스의 Object Header를 확인하는 방법 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1175'>http://www.sysnet.pe.kr/2/0/1175</a> windbg에서 확인해 보는 관리 힙의 인스턴스 구조 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/1559'>http://www.sysnet.pe.kr/2/0/1559</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>39. 최소 몇 개의 AppDomain이 있을까?</div> <br /> 별도의 AppDomain을 생성하지 않았다면 System Domain, SharedDomain, 기본 Domain까지 총 3개가 생성됩니다. 원 글(<a target='tab' href='https://blog.adamfurmanek.pl/2019/03/23/net-internals-cookbook-part-6/'>.NET Internals Cookbook Part 6 - Object internals</a>)에서는 각각의 도메인에 대해 다음과 같이 설명하고 있는데,<br /> <br /> <ol> <li>System Domain - mscorlib를 로드하고 OutOfMemoryException, StackoverflowException, ExecutionEngineException 예외들이 필요할 때 항상 발생할 수 있도록 미리 생성, 문자열 풀 관리(interning)</li> <li>Shared Domain - mscorlib, System 네임스페이스의 타입 및 그와 관련된 코드</li> <li>Default - 사용자 코드 및 자원을 포함</li> </ol> <br /> 사실, Shared Domain과 Default 간의 역할은 어셈블리 공유 방식에 따라 다르기 때문에 언제나 저렇다고 설명할 수는 없습니다. 이에 대해서는 다음의 글을 참고하세요.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > .NET - 눈으로 확인하는 SharedDomain의 동작 방식 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/10948'>http://www.sysnet.pe.kr/2/0/10948</a> </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>40. 값 형식(value type)도 Method Table이 있을까?</div> <br /> 당연히 값 형식에도 Method Table이 있습니다. 하지만, 값 형식의 경우 반드시 Boxing이나 가상 메서드를 호출해야만 그 시점에 Method Table이 등록됩니다. 예를 들어 다음의 코드를,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; namespace ConsoleApp { class Program { public static void Main() { Foo foo = new Foo(); Console.WriteLine(foo.ToString()); object obj = foo; Bar(obj); } static void Bar(object foo) { Console.WriteLine(foo.ToString()); Console.ReadLine(); } } struct Foo { public override string ToString() { return "Some string"; } } } </pre> <br /> 빌드해 windbg로 붙여보면 다음과 같이 ConsoleApp1.dll에 정의된 MT 목록을 확인할 수 있고, 그 안에는 분명히 Foo struct 항목을 포함하고 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>!name2ee * Program</span> Module: 00007ffb9c1c1000 Assembly: mscorlib.dll -------------------------------------- Module: <span style='color: blue; font-weight: bold'>00007ffb3ea74128</span> Assembly: ConsoleApp1.exe 0:000> <span style='color: blue; font-weight: bold'>!dumpmodule -mt 00007ffb3ea74128</span> Name: C:\temp\cookbook_series6_sample\q40\ConsoleApp1\bin\Debug\ConsoleApp1.exe Attributes: PEFile SupportsUpdateableMethods Assembly: 0000013c26ae9070 LoaderHeap: 0000000000000000 TypeDefToMethodTableMap: 00007ffb3ea70070 TypeRefToMethodTableMap: 00007ffb3ea70090 MethodDefToDescMap: 00007ffb3ea70128 FieldDefToDescMap: 00007ffb3ea70158 MemberRefToDescMap: 0000000000000000 FileReferencesMap: 00007ffb3ea70168 AssemblyReferencesMap: 00007ffb3ea70170 MetaData start address: 0000013c268620d0 (1652 bytes) Types defined in this module MT TypeDef Name ------------------------------------------------------------------------------ 00007ffb3ea75a18 0x02000002 ConsoleApp.Program <span style='color: blue; font-weight: bold'>00007ffb3ea75ac8 0x02000003 ConsoleApp.Foo</span> Types referenced in this module MT TypeRef Name ------------------------------------------------------------------------------ 00007ffb9c8e9d78 0x02000010 System.Object 00007ffb9c8ebb00 0x02000011 System.ValueType 00007ffb9c860928 0x02000012 System.Console </pre> <br /> 당연히 값 형식이 stack에 놓일 때는 Object header 및 MT 값이 없지만 (박싱되어) 힙에 놓일 때는 Object Header와 MT 값이 있습니다. 확인을 위해 clrstack 명령어로 로컬 변숫값을 확인하고,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>!clrstack -a -i</span> Dumping managed stack and managed variables using ICorDebug. ============================================================================= Child SP IP Call Site 000000229e36e968 00007ffbbabdf754 [NativeStackFrame] 000000229e36e9f0 00007ffb9c74e276 000000229e36ea28 (null) [Managed to Unmanaged transition: 000000229e36ea28] 000000229e36ead0 00007ffb9cf7d43a [DEFAULT] I4 System.IO.__ConsoleStream.ReadFileNative(Class Microsoft.Win32.SafeHandles.SafeFileHandle,SZArray UI1,I4,I4,Boolean,Boolean,ByRef I4) (C:\WINDOWS\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll) ...[생략]... PARAMETERS: (none) <span style='color: blue; font-weight: bold'>LOCALS: + ConsoleApp.Foo foo @ 0x229e36ed70 + ConsoleApp.Foo obj @ 0x13c28626038</span> 000000229e36ed90 00007ffb3eb804ae [DEFAULT] Void ConsoleApp.Program.Main() (C:\temp\cookbook_series6_sample\q40\Consol) PARAMETERS: (none) LOCALS: (none) 000000229e36edd0 00007ffb9e186da3 [NativeStackFrame] Stack walk complete. ============================================================================= </pre> <br /> foo, obj 변수가 갖는 메모리를 덤프해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > 0:000> <span style='color: blue; font-weight: bold'>dq 0x229e36ed70-8 L4</span> 00000022`9e36ed68 0000013c`28626030 00000000`00000000 00000022`9e36ed78 00000022`9e36f000 00000022`9e36edc0 0:000> <span style='color: blue; font-weight: bold'>dq 0x13c28626038-8 L4</span> 0000013c`28626030 <span style='color: blue; font-weight: bold'>00007ffb`3ea75ac8</span> 00000000`00000000 0000013c`28626040 00000000`00000000 00007ffb`9c8ea8e8 </pre> <br /> foo의 변수는 구조체 필드의 값 그대로를 가리키고 있는 반면(따라서 MT 값 위치에 이전 스택 값에 불과한, 위에서는 0000013c`28626030이 들어 있지만), obj 변수의 위치 -8에는 정확히 Foo struct에 대한 MT 값(00007ffb`3ea75ac8)이 들어 있습니다. <br /> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>41. delegate 클래스의 부모 클래스</div> <br /> 이 정도는 다들 아시겠지만 ^^ 직접 코드로 확인해 볼 수 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; public class Program { delegate void X(); public static void Main() { X x = Foo; Console.WriteLine(x.GetType().BaseType); Console.WriteLine(x.GetType().BaseType.BaseType); Console.WriteLine(x.GetType().BaseType.BaseType.BaseType); } static void Foo() { } } /* 출력 결과 System.MulticastDelegate System.Delegate System.Object */ </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>42. 배열의 부모 클래스와 구현 인터페이스</div> <br /> 다음의 코드로,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; public class Program { public static void Main() { Foo[] foo = new Foo[5]; Console.WriteLine(foo.GetType().BaseType); foreach (var type in foo.GetType().GetInterfaces()) { Console.WriteLine(type); } } } class Foo { } /* 출력 결과 System.Array System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable System.Collections.Generic.IList`1[Foo] System.Collections.Generic.ICollection`1[Foo] System.Collections.Generic.IEnumerable`1[Foo] System.Collections.Generic.IReadOnlyList`1[Foo] System.Collections.Generic.IReadOnlyCollection`1[Foo] */ </pre> <br /> 1차원 배열의 기반 클래스가 System.Array이고 기타 구현된 인터페이스를 확인할 수 있습니다. 재미있는 것은, 2차원 이상의 배열에 대해서는 제네릭 인터페이스 구현이 누락된다는 점입니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > Foo[,] foo = new Foo[5, 4]; Console.WriteLine(foo.GetType().BaseType); foreach (var type in foo.GetType().GetInterfaces()) { Console.WriteLine(type); } /* 출력 결과 System.Array System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable */ </pre> <br /> 또한, 포인터 배열에 대해서도 제네릭 인터페이스 구현이 없습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > unsafe { int*[] foo = new int*[5]; Console.WriteLine(foo.GetType().BaseType); foreach (var type in foo.GetType().GetInterfaces()) { Console.WriteLine(type); } } /* 출력 결과 System.Array System.ICloneable System.Collections.IList System.Collections.ICollection System.Collections.IEnumerable System.Collections.IStructuralComparable System.Collections.IStructuralEquatable */ </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>43. 인덱스가 0부터 시작하지 않는 배열을 만들 수 있을까?</div> <br /> C#의 배열 생성 문법으로는 무조건 인덱스가 0부터 시작하지만 Array.CreateInstance 메서드의 3번째 인자를 이용하면 인덱스의 시작을 지정할 수 있습니다.<br /> <br /> 예를 들어, 다음의 코드는 첫 번째 요소의 인덱스 시작점을 2로 지정한 배열을 생성합니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; public class Program { public static void Main() { // int 형으로 1차원의 배열을 시작점이 2인 것으로 생성 Array array = Array.CreateInstance(typeof(int), new int[] { 1 }, new int[] { <span style='color: blue; font-weight: bold'>2</span> }); array.SetValue(123, 2); // 첫 번째 요소에 123을 지정 Console.WriteLine(array.GetValue(2)); // 첫 번째 요소의 값을 반환 Console.WriteLine(array.GetValue(0)); // 런타임 예외 발생 - Unhandled Exception: System.IndexOutOfRangeException: Index was outside the bounds of the array. } } </pre> <br /> <hr style='width: 50%' /><br /> <br /> <br /><div style='font-size: 12pt; font-family: Malgun Gothic, Consolas; color: #2211AA; text-align: left; font-weight: bold'>44. string interning이란?</div> <br /> 프로그램 상에는 중복되는 문자열이 사용되는 경우가 많습니다. "string interning"은 그러한 중복 문자열을 관리해 메모리를 효율적으로 관리할 수 있는 방법입니다. 가령 다음의 소스 코드를 실행해 보면,<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > using System; using System.Text; public class Program { public static void Main() { String s1 = "MyTest"; String s2 = new StringBuilder().Append("My").Append("Test").ToString(); String s3 = String.Intern(s2); Console.WriteLine("s1 == '{0}'", s1); Console.WriteLine("s2 == '{0}'", s2); Console.WriteLine("s3 == '{0}'", s3); Console.WriteLine("Is s2 the same reference as s1?: {0}", (Object)s2==(Object)s1); Console.WriteLine("Is s3 the same reference as s1?: {0}", (Object)s3==(Object)s1); } } /* 출력 결과 s1 == 'MyTest' s2 == 'MyTest' s3 == 'MyTest' Is s2 the same reference as s1?: False Is s3 the same reference as s1?: True */ </pre> <br /> s1 == "MyTest"는 C# 소스 코드에 사용된 문자열 리터럴이기 때문에 문자열 Pool에 보관이 되는 반면 s2는 별도의 힙 메모리를 차지하게 됩니다. 하지만 같은 문자열이기 때문에 굳이 힙 메모리를 낭비하기보다는 문자열 Pool에 보관된 문자열이 있다면 그것을 반환하도록 Intern 메서드를 사용해 메모리를 절약할 수 있습니다.<br /> <br /> 참고로 위에서 == 연산자는 참조 주소를 반환한다고 전에 설명한 적이 있습니다.<br /> <br /> <pre style='margin: 10px 0px 10px 10px; padding: 10px 0px 10px 10px; background-color: #fbedbb; overflow: auto; font-family: Consolas, Verdana;' > (번역글) .NET Internals Cookbook Part 4 - Type members 24. Equals 메서드와 == 연산자의 차이점 ; <a target='tab' href='http://www.sysnet.pe.kr/2/0/11872#tag24'>http://www.sysnet.pe.kr/2/0/11872#tag24</a> </pre> <br /> 그런데 왜 굳이 (Object)로 형변환했을까요? 왜냐하면 string 타입은 == 연산자를 재정의해 순수 문자열을 비교하기 때문에 string.Equals 메서드와 역할이 같습니다. 따라서 Object로 형변환함으로써 string.Equals가 아닌 참조 비교를 하도록 의도한 것입니다.<br /> <br /> (<a target='tab' href='https://www.sysnet.pe.kr/bbs/DownloadAttachment.aspx?fid=1440&boardid=331301885'>첨부 파일은 이 글의 예제 코드를 포함</a>합니다.)<br /> </h1><br /> <br /><hr /><span style='color: Maroon'>[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]</span> </div>
첨부파일
스팸 방지용 인증 번호
7711
(왼쪽의 숫자를 입력해야 합니다.)