-
14장. 문자, 문자열, 텍스트 사용하기CLR Via C# 2022. 2. 21. 21:28
System.String 타입
String 타입은 변경할 수 없는 문자들의 순서 열이다. String 타입은 Object 타입을 직접 상속하기 때문에 참조 타입으로 분류되며, 따라서 String 타입과 그 내부의 배열은 항상 힙에 할당되고 절대 스레드의 스택 영역에는 할당되지 않는다.
C#을 포함한 많은 프로그래밍 언어들이 String을 기본 타입으로 간주하는데, 컴파일러가 문자열 리터럴을 소스 코드상에서 직접 표현할 수 있도록 한다. 컴파일러는 리터럴 문자열을 모듈의 메타데이터 영역에 배치하고, 실행 시점에 이 메타데이터를 메모리에 로드한 후 참조하게 된다.
// String s = "Hi\nthere."; String s = "Hi" + Environment.NewLine +"there." // 위에 코드는 환경에 따라 바뀔 수 있음. 아래의 코드는 어떤 환경에서든 가능!##
문자열은 변경 불가능하다.
String 객체에 관하여 알아두어야 할 가장 중요한 성질은 변경이 불가능하다는 것이다. 변경 불가능한 문자열은 몇 가지 이점을 제공한다.
첫째, 문자열에 대해서 실제로 문자열을 변경하지 않고도 작업을 수행할 수 있도록 해준다. 변경 불가능한 문자열은 달리 말하면 문자열에 대한 스레드 동기화 문제가 발생하지 않는다. 또한 같은 String 객체가 하나의 문자열을 공유해 메모리 사용량을 최소화할 수 있다. 하지만 문자열에 대한 연산이 많이질수록 힙에는 수많은 String 임시객체가 형성되어, 더 많은 가비지 수집을 필요로 하게 되고, 당연히 응용프로그램의 성능에 부정적인 영향을 끼치게 될 것이다.
문자열 비교하기
문자열 비교는 String클래스가 정의하고 있는 다양한 메서드들을 사용하는 것이 좋다.
Equls, Compare, StartWith, EndWith등 다양한 인스턴스 혹은 정적 메서드들이 있고StringComparison, CompareOptions, CultureInfo의 옵션을 통해 특정 언어의 특징을 고려하거나 무시해 문자열을 비교할 수 있다.일본어 문자 비교 등 문자열의 동일성 또는 정렬을 위한 비교를 수행할 때 좀 더 세밀하게 작업을 수행해야 할 수도 있다. 추가적인 제어를 수행하려면 CultureInfo 객체의 CompareInfo속성을 이용하면 된다.
문자열 인터닝
메모리에 동일한 문자열이 여럿 있을 경우 문자열은 변경 불가능한 타입이므로 메모리 낭비가 된다. 메모리에 문자열을 단 하나만 유지하고 모든 변수가 이 문자열을 참조하도록 한다면 메모리를 좀 더 효율적으로 사용할 수 있을 것이다. 이것이 바로 인터닝 메커니즘이다. CLR은 초기화 시에 내부적으로 하나의 해시 테이블을 생성한다. 이 테이블의 키로는 문자열을 사용하고, 값으로는 관리 힙에 만들어진 String 객체의 참조를 저장한다. 다음의 두 개의 정적 메서드를 이용하면 내부 해시 테이블에 접근할 수 있다.
public static String Intern(String str); public static String IsInterned(String str);첫 번째 메서드인 Intern은 String 매개변수를 받아 해시 코드 값을 계산한 후, 내부 해시 테이블에 일치하는 항목이 있는지 확인한다. 만약 일치하는 항목이 있을 경우, 기존의 String 객체가 반환된다. 만약 일치하는 항목이 없는 경우, String 객체의 사본을 만들고 내부 해시 테이블에 그 참초를 추가한 후, 이 사본을 반환한다.
두 번째 메서드인 IsInterned는 String 매개변수를 받아 해시 코드 값을 계산한 후, 내부 테이블에 일치하는 항목 검사하는 과정은 같다. 일치하는 항목이 있을 경우, 기존의 String 객체를 반환하는 것은 같지만, 일치하는 항목이 없는 경우 문자열을 추가하지 않고 대신 null을 반환한다.
기본적으로, CLR이 어떤 어셈블리를 로드하면, 로드하는 어셈블리 내의 메타데이터상에 기재된 모든 리터럴 문자열들을 이와 같은 메커니즘을 사용하여 보관한다. 이런 메커니즘은 추후 해시 테이블을 검색할 때 성능에 나쁜 영향을 미칠 수 있어 C# 컴파일러의 경우 기본적으로 사용하지 않는다. 하지만 CLR은 필요에 따라 이 특성을 무시하고 문자열을 인터닝할 수도 있다. 그래서 실제로 String의 Intern 메서드를 명시적으로 호출하는 경우를 제외하고는 문자열이 인터닝 되었는지의 여부에 의존적인 코드를 작성해서는 안 된다.
인터닝의 성능 향상을 살펴보자. 문자열의 동일성을 검사할 때, 인터닝을 사용하면 그저 같은 참조를 가리키는가를 검사한다. 하지만 인터닝을 사용하지 않으면 문자열을 구성하고 있는 글자들을 모두 비교해야하고 동일한 문자열을 포함하고 있는 서로 다른 String 객체를 참조하고 있을 수 있다. 이것만 보면 인터닝이 무조건 좋아 보이지만, 인터닝하는 과정 자체가 시간이 많이 소요된다. 그래서 문자열 배열을 여러번 사용해야 하는 프로그램에서는 성능과 메모리 사용량 감소 효과를 분명하게 보여줄 것이다.
문자열 풀링
같은 리터럴 문자열이 소스 코드에서 여러 번 등장한다면, 메타데이터의 크기를 불필요하게 늘어날 수도 있을 것이다. 이러한 문제점을 해결하기 위하여, 동일 리터럴 문자열을 모듈의 메타데이터에 단 한 번만 기록한다. 해당 문자열을 참조하는 모든 코드는 메타데이터상의 동일 문자열을 참조하도록 수정된다. 이를 문자열 풀링(pooling) 이라고 한다. 풀링 기법은 게임 제작시에도 오브젝트 풀링 등 중요한 기법이다.
StringBuilder 객체 생성하기
StringBuilder sb = new StringBuilder();
StringBuilder 타입은 여러 형태의 생성자들을 제공한다. 모든 생성자들은 공통적으로 StringBuilder 객체가 사용할 상태를 초기화하고 메모리를 할당하는 작업을 수행한다. 생성자에서 문자열 값, 용량, 최대 용량을 설정할 수 있다.
StringBuilder는 변경 가능한 문자열을 나타내기 위한 객체다. 따라서 거의 모든 멤버들은 매번 관리 힙상에 새로운 객체를 생성하지 않고, 기존 문자 배열의 내용을 수정하게 된다. 실제로 StringBuilder는 딱 두 가지 이유에 의해서만 새로운 객체를 생성한다.
초기에 지정한 용량보다 큰 문자열을 동적으로 만들어야 하는 경우
StringBuilder의 ToString 메서드를 호출하는 경우
##
객체에 대한 문자열 표현을 얻어오기: ToString
public이면서 가상 메서드인 매개변수 없는 ToString 메서드는 System.Object에 정의되어 있으며, 모든 타입에서 호출할 수 있는 메서드다. 의미적으로 현재 객체의 값을 나나내는 문자열을 반환해야 한다. 또한 이 문자열은 반드시 현재 스레드의 문화권 정보를 기준으로 서식이 적용되어야 한다.
System.Object의 ToString 메서드는 단순히 객체의 전체 타입 이름을 반환하도록 구현되어 있다.
객체의 현재 상태를 문자열로 표현하려는 모든 타입에 대해서 가장 적당한 구현 방법은 ToString 메서드를 재정의하는 것이다. FCL 안에 내장되어있는 상당수의 핵심 타입들은 모두 ToString 메서드를 재정의하여 스레드의 문화권에 부합하는 문자열을 반환하도록 구현하고 있다. Visual Studio 디버거에서 특정 변수 위로 마우스 커서를 가져갔을 때 표시되는 데이터 팁 안에 표시되는 문자열은 객체의 ToString 메서드를 호출해서 얻은 문자열이 표시된다.
따라서, 어떤 클래스를 정의할 떄에는 가능한 항상 ToString 메서드를 재정의하여 디버깅에 대한 지원을 강화하는 것이 좋다.특성 서식과 문화권
매개변수 없는 ToString 메서드는 서식을 지정하거나 문화권을 설정할 수 없다. 그래서 이런 기능이 가능한 ToString 메서드가 필요하다.
System.IFormattable 인터페이스를 구현하고 있는 타입들은 서식이나 문화권을 설정할 수 있는 ToString을 구현하고 있다.
public interface IFormattable {
String ToString( String format, IFormatProvider formatProvider );
}
FCL에서 모든 기본 타입들은 이 인터페이스를 구현하고 있으며, Guid와 같은 일부 타입들도 이 인터페이스를 구현하고 있다. 마지막으로, 모든 열거 타입들은 자동으로 IFormattable 인터페이스 구현하므로, 열거 타입의 인스턴스에 대해서도 이 메서드를 사용하여 의미 있는 결과를 얻어낼 수 있다.
첫 번째 매개변수에는 문자열 타입으로 문자열의 서식을 지정한다. 두 번째 매개변수에는 System.IFormatProvider 인터페이스를 구현하고 있는 타입의 인스턴스를 전달해 문화권 정보를 지정한다. null 이면 System.Globalization.CultureInfo.CurrentCulture 속성을 확인하여 사용한다. 이 속성은 System.Globalization.CultureInfo 타입의 인스턴스를 반환한다. 이건 현재 사용중인 스레드의 문화권 정보이다.
사용자 정의 서식 지정 구현하기
String.Format 이나 StringBuilder.AppendFormat 메서드를 사용할 때 첫 매개변수로 IFormatProvider 객체를 넣어주면 이 객체에서 구현한 사용자 정의 서식을 적용할 수 있다. 또한 이 객체에 ICustomFormatter 인터페이스를 구현한다면 쉽게 제어 가능하다.
public interface ICustomFormatter {
String Format(String format, Object arg, IFormatProvider formatProvider);
}
이 인터페이스의 Format 메서드는 StringBuilder의 AppendFormat 메서드가 객체에 대한 문자열 표현을 얻어오려고 할 때마다 매번 호출되도록 되어있다. 따라서 이 메서드를 구현할 때 문자열 서식을 간편하고 쉽게 제어하는 코드를 추가할 수 있다.
문자열을 객체로 분석하기 : Parse
문자열로부터 객체를 생성할 수 있는 타입들은 public 정적 메서드인 Parse 메서드를 가지고 있으며 이를 통해 문자열을 매개변수로 받아 해당 타입의 인스턴스를 반환한다. 이 동작은 팩토리 패턴과 유사하다. FCL에서는 모든 숫자 타입, DateTime, TimeSpan, SQL 데이터 타입과 같은 일부 타입에 Parse 메서드가 포함되어 있다. 올바르지 않은 사용자 입력으로 인해 Parse 메서드가 자주 예외를 유발하게 되면 응용프로그램의 성능이 급감하는 이유로 TryParse라는 메서드도 추가되었다.
인코딩 : 문자 배열과 바이트 배열 사이의 변환
문자를 인코딩하거나 디코딩하려면 먼저 System.Text.Encoding 클래스를 상속한 클래스의 인스턴스를 얻어와야 한다. System.Text.Encoding은 추상 클래스이며 다양한 readonly 속성을 가지고 있는데 그 각각은 System.Encoding을 상속한 클래스의 인스턴스를 반환한다.
String s = "Hi there."; System.Text.Encoding encodingUTF8 = System.Text.Encoding.UTF8; Byte[] encodedBytes = encodingUTF8.GetBytes(s); String decodedString = encodingUTF8.GetString(encodedBytes);문자열과 바이트 스트림의 인코딩과 디코딩
만약 데이터 송신시 일정하지 않은 크기의 데이터 묶음 단위로 인코딩이나 디코딩을 수행해야 한다면, 데이터를 잃어버리지 않도록 상태를 관리하는 작업을 반드시 수행해야 한다. 그래서 스트림으로부터 디코딩하는 과정이나 스트림으로 인코딩 하는 과정은 상태 관리가 필요하다. 이럴 때는 System.Text.Encoding를 상속한 객체에서 GetDecoder 나 GetEncoder 메서드를 호출해 System.Text.Decoder 나 System.Text.Encoder를 상속한 클래스의 인스턴스를 받아와 사용해야 한다.
안전한 문자열
String 객체에 들어있는 문자의 배열은 메모리상에 정보를 저장하며, 몇몇 안전하지 않거나 관리되지 않는 코드가 수행될 수 있다면, 이 코드들이 프로세스의 주소 공간에 침입하여 민감한 정보를 검색하여 빼내가는 것이 가능하다. 그래서 좀 더 보안상 안전한 문자열 클래스 System.Security.SecureString 클래스가 추가됐다. SecureString 객체를 만들면, 내부적으로 비관리 메모리 영역에 문자의 배열을 할당한다. 이 비관리 메모리 영역은 가비지 컬렉터가 관리하지 않는다.
이 문자열들은 암호화되기 때문에, 안전하지 않고/관리되지 않은 코드로부터 보안에 민감한 정보를 보호할 수 있다. 이 객체의 AppendChar, InsertAt, RemoveAt, SetAt 메서드를 이용하면 문자를 추가 - 삭제 - 설정할 수 있다. 이 메서드들을 호출하면, 내부적으로는 문자열을 복호화하고 요청한 작업을 수행한 후, 다시 암호화하는 과정을 거치게 된다. 이러한 특성으로 인해 각각의 작업 성능이 현저히 느리므로 가능한 적게 사용하는 것이 좋다.
SecureString 클래스는 IDisposable 인터페이스를 구현하고 있기 때문에, 정확한 시점에 문자열이 가지고 있는 암호화된 내용을 쉽게 제거할 수 있다. String 객체와는 다르게 소멸시 암호화된 문자열의 내용이 완벽하게 0으로 덮어 씌워져 메모리에 남지 않는다.
secureString의 내용은 절대로 String으로 담아서는 안 된다. 이 경우 String 객체가 힙에 남아 가비지 컬렉터에 의하여 수집될 때까지 메모리상에 남게 되고, 내용을 0으로 초기화할 수도 없는 상태가 된다.
다음은 SecureString을 어떻게 초기화하고 사용할 수 있는지를 보여주는 예제 코드이며, C# 컴파일러에 /unsafe 스위치를 지정해야 다음의 코드를 컴파일할 수 있다.
using System; using System.Security; using System.Runtime.InteropServices; public static class Program { public static void Main(){ using (SecureString ss = new SecureString()){ Console.Write("Please enter password: "); while (ture){ ConsoleKeyInfo cki = Console.ReadKey(true); if (cki.Key == ConsoleKey.Enter) break; // SecureString에 비밀번호 문자들을 더한다. ss.AppendChar(cki.KeyChar); Console.Write("*"); } Console.WriteLine(); // 비밀번호 입력이 끝나면, 그 안의 내용을 보여주는 시연용 메서드 호출 DisplaySecureString(ss); } // using 블록을 벗어나면 SecureString의 Dispose // 메서드가 호출되고 메모리상에 관련된 데이터는 모두 제거된다. } // 이 메서드는 관리되지 않는 메모리르 다루기 때문에 안전하지 않은 코드로 분류됨. private unsafe static void DisplaySecureString(SecureString ss) { Char* pc = null; try{ // SecureString을 관리되지 않는 메모리 버퍼로 복원한다. pc = (char*) Marshal.SecureStringToCoTaskMemUnicode(ss); // 복원된 SecureString의 내용을 보관하는 관리되지 않는 메모리에 접근 for(int index = 0; pc[index] != 0; index++) Console.Write(pc[index]); } finally{ // 관리되지 않는 메모리 버퍼의 내용을 0으로 초기화하여 해제하여 // 복원된 문자열을 삭제한다. if (pc != null) Marshal.ZeroFreeCoTaskMemUnicode((IntPtr) pc); } } }System.Runtime.InteropServices.Marshal 클래스는 SecureString의 문자들을 관리되지 않는 메모리 버퍼상으로 복원을 도와주는 다섯 가지 메서드를 제공한다. 이 메서드들은 모두 정적 메서드이고, SecureString을 매개변수로 받고, IntPtr 타입의 인스턴스를 반환한다. 각 메서드들은 각각 대응되는 제거 함수를 호출하여 정확하게 내부 버퍼를 0으로 초기화하고 제거해야 한다.
SecureString을 버퍼로 복원하는 메서드 버퍼 내용을 지우고, 반환하는 메서드 SecureStringToBSTR ZeroFreeBSTR SecureStringToCoTaskMemAnsi ZeroFreeCoTaskMemAnsi SecureStringToCoTaskMemUnicode ZeroFreeCoTaskMemUnicode SecureStringToGlobalAllocAnsi ZeroFreeGlobalAllocAnsi SecureStringToGlobalAllocUnicode ZeroFreeGlobalAllocUnicode 'CLR Via C#' 카테고리의 다른 글
16장. 배열 (0) 2022.02.28 15장. 열거 타입과 비트 플래그 (0) 2022.02.23 13장. 인터페이스 (0) 2022.02.16 12장. 제네릭(Generics) (0) 2022.02.12 11장. 이벤트 (0) 2022.02.07