# 문자열과 텍스트

> Reduce the memory allocations that string and text manipulation cause in your project.

문자열과 텍스트 처리는 Unity 프로젝트에서 흔히 발생하는 성능 문제입니다. C#에서 모든 문자열은 변경할 수 없습니다. 문자열을 조작하면 전체 새 문자열이 할당됩니다. 이렇게 되면 상대적으로 많은 리소스를 소모하며, 반복된 문자열 연결로 인해 대규모 문자열, 대규모 데이터 집합 또는 빠른 루프에서 수행될 때 성능 문제가 발생할 수 있습니다.

또한 N개의 문자열 연결은 N-1개의 중간 문자열을 할당해야 하므로, 연쇄적인 연결은 관리되는 메모리에 큰 부담을 줄 수 있습니다.

문자열이 빠르게 반복하는 루프나 매 프레임에서 연결되어야 할 경우 실제 연결 작업을 하기 위해 StringBuilder를 사용합니다. StringBuilder 인스턴스를 재사용하여 불필요한 메모리 할당을 더욱 최소화할 수도 있습니다.

Microsoft는 C#에서 문자열 작업에 대한 베스트 프랙티스 목록을 제공하고 있습니다. MSDN 웹사이트인 [msdn.microsoft.com](https://msdn.microsoft.com/en-us/library/dd465121\(v=vs.110\).aspx)을 참조하십시오.

## 로케일 강제 변환 및 서수 비교

문자열 관련 코드에서 자주 발생하는 주요 성능 문제 중 하나는 의도치 않게 느린 기본 문자열 API를 사용하는 것입니다. 이러한 API는 비즈니스 애플리케이션용으로 제작되었으며 텍스트에 있는 문자와 관련하여 다양한 문화적 및 언어 규칙의 문자열을 처리하려고 합니다.

예를 들어, 다음의 예제 코드는 미국식 영어 로케일에서 실행된 경우 true를 반환하지만, 다수의 유럽어 로케일에서는 false를 반환합니다.

**참고**: Unity 5.3 및 5.4의 경우 Unity의 스크립팅 런타임은 항상 미국식 영어(en-US) 로케일에서 실행됩니다.

```
    String.Equals("encyclopedia", “encyclopædia”);
```

대부분의 Unity 프로젝트에서 이런 방식은 전혀 필요하지 않습니다. C 및 C++ 프로그래머들이 친숙한 방식으로 문자열을 비교하는 서수 비교 방식을 사용하는 것이 대략 열 배는 빠릅니다. 이는 바이트로 표현되는 문자를 고려하지 않고 문자열의 각 바이트를 단순히 순차 비교하는 방식입니다.

서수 문자열 비교 방식으로 전환하는 것은 `String.Equals`에 최종 인자로 `StringComparison.Ordinal`을 제공하는 것만큼 간단합니다.

```
myString.Equals(otherString, StringComparison.Ordinal);
```

## 비효율적인 빌트인 문자열 API

서수 비교로 전환하는 것 외에도 특정 C# `String` API는 매우 비효율적인 것으로 알려져 있습니다. 이들 중에는 `String.Format`, `String.StartsWith`, `String.EndsWith`가 있습니다. `String.Format`은 교체하기 어렵지만 비효율적인 문자열 비교 방식은 아주 쉽게 최적화됩니다.

Microsoft는 현지화를 위해 조정될 필요가 없는 모든 문자열 비교에 `StringComparison.Ordinal`을 전달하는 것을 권장하지만, Unity 벤치마크에 의하면 사용자 구현 방식과 비교했을 때 그 영향이 상대적으로 미미한 것으로 나타났습니다.

| 메서드                      | 십만 개의 짧은 문자열 처리 시간(ms) |
| ------------------------ | ---------------------- |
| `String.StartsWith`, 기본값 | 137                    |
| `String.EndsWith`, 기본값   | 542                    |
| `String.StartsWith`, 서수  | 115                    |
| `String.EndsWith`, 서수    | 34                     |
| 커스텀 `StartsWith` 교체      | 4.5                    |
| 커스텀 `EndsWith` 교체        | 4.5                    |

다음 예시와 같이 `String.StartsWith` 및 `String.EndsWith` 둘 다 핸드 코딩된 간단한 버전으로 교체할 수 있습니다.

```

    public static bool CustomEndsWith(this string a, string b)
    {
    	int ap = a.Length - 1;
    	int bp = b.Length - 1;
    
    	while (ap >= 0 && bp >= 0 && a [ap] == b [bp])
    	{
    		ap--;
    		bp--;
    	}
    
    	return (bp < 0);
    }

    public static bool CustomStartsWith(this string a, string b)
    {
    	int aLen = a.Length;
    	int bLen = b.Length;
    
    	int ap = 0; int bp = 0;
    
    	while (ap < aLen && bp < bLen && a [ap] == b [bp])
    	{
    		ap++;
    		bp++;
    	}
    
    	return (bp == bLen);
    }
```

## 정규 표현식

정규 표현식은 문자열을 매칭하고 조작하기 위한 최상의 방법이지만 성능이 매우 많이 소모될 수 있습니다. 또한 C# 라이브러리의 정규 표현식 구현으로 인해 심지어 단순한 부울 `IsMatch` 쿼리도 대용량 임시 데이터 구조를 ‘내부적으로’ 할당합니다. 이 일시적인 관리되는 메모리의 불필요한 활동은 초기화 시점을 제외하고는 허용하지 않아야 합니다.

정규 표현식이 필요한 경우 정규 표현식을 문자열 파라미터로 허용하는 정적 `Regex.Match` 또는 `Regex.Replace` 메서드를 사용하지 않는 것이 좋습니다. 이러한 메서드는 정규 표현식을 실시간으로 컴파일하고 생성된 오브젝트를 캐싱하지 않습니다.

이 예제는 무해해 보이는 한 줄 코드입니다.

```

Regex.Match(myString, "foo");
```

하지만 실행될 때마다 5KB의 가비지를 생성합니다. 간단한 리팩터링을 통해 이러한 가비지를 상당 부분 제거할 수 있습니다.

```

var myRegExp = new Regex("foo");

myRegExp.Match(myString);
```

이 예시에서는 `myRegExp.Match`를 호출할 때마다 가비지가 320바이트만 생성됩니다. 간단한 매치 작업이라는 것을 감안할 때 여전히 비용이 많이 들지만 이전 예시보다 상당히 개선된 결과입니다.

따라서 정규 표현식이 불변의 문자열 리터럴인 경우 이를 Regex 오브젝트 생성자의 첫 파라미터로 전달하여 미리 컴파일하는 것이 상당히 효율적입니다. 그런 다음 이러한 사전 컴파일된 Regex를 재사용해야 합니다.

## XML, JSON과 그 외 긴 형식의 텍스트 파싱

텍스트 파싱은 로드 시간에 발생하는 복잡한 작업 중 하나입니다. 경우에 따라 텍스트를 파싱하는 데 소요되는 시간이 에셋을 로드하고 인스턴스화하는 시간보다 더 길어질 수도 있습니다.

그 이유는 사용되는 특정 파서에 따라 달라집니다. C# 빌트인 XML 파서는 대단히 유연하지만 결과적으로 특정 데이터 레이아웃에는 최적화할 수 없습니다.

많은 타사 파서는 반사를 기반으로 빌드됩니다. 반사는 개발 중에는 탁월한 선택이지만(데이터 레이아웃의 변경에도 파서가 빠르게 적응할 수 있기 때문), 느린 것으로 악명이 높습니다.

Unity는 JSON을 읽고 방출하는 Unity 직렬화 시스템에 대한 인터페이스를 제공하는 빌트인 [JSONUtility](/engine/6000.0/script-reference/unity-engine/json-utility.md) API를 통해 부분적인 솔루션을 도입했습니다. 대부분의 벤치마크에서는 순수 C# JSON 파서보다 빠르지만, Unity의 직렬화 시스템에 대한 다른 인터페이스와 동일한 제약이 있습니다. 그 제약은 추가 코드 없이 딕셔너리 같은 복잡한 데이터 유형을 직렬화할 수 없다는 것입니다.

**참고**: Unity 직렬화 과정 중에 복잡한 데이터 유형을 양방향으로 전환하는 데 필요한 과정을 추가하는 방법은 [ISerializationCallbackReceiver](/engine/6000.0/script-reference/unity-engine/i-serialization-callback-receiver.md) 인터페이스를 참조하십시오.

텍스트 데이터 구문 분석으로 인하여 성능 문제를 겪는 경우, 다음의 세 가지 대안을 고려하십시오.

### 옵션 1: 빌드 시 파싱

텍스트 파싱 비용을 줄이는 가장 좋은 방법은 런타임 시 텍스트 파싱을 완전히 제거하는 것입니다. 일반적으로 이는 일종의 빌드 단계를 통해 텍스트 데이터를 바이너리 포맷으로 ‘베이크’하는 것을 의미합니다.

이 경로를 선택하는 대부분의 개발자는 데이터를 일종의 ScriptableObject 파생 클래스 계층 구조로 이동한 후 에셋 번들을 통해 데이터를 배포합니다. ScriptableObject 사용에 대한 자세한 내용은 YouTube에서 [리처드 파인의 유나이트 2016 강연](https://www.youtube.com/watch?v=VBA1QCoEAX4)(영어)을 참조하십시오.

이 방법으로 최상의 성능을 얻을 수 있지만, 동적으로 생성되어야 할 필요가 없는 데이터에만 적합하며 게임 디자인 파라미터나 기타 콘텐츠에 가장 적합합니다.

### 옵션 2: 분할 및 지연 로드

두 번째 방법은 파싱해야 하는 데이터를 더 작은 청크로 분할하는 것입니다. 분할하면 데이터 파싱 비용을 여러 프레임으로 분할할 수 있습니다. 이상적인 경우 사용자에게 원하는 경험을 제공하는 데 필요한 특정 데이터 부분을 파악하고 해당 부분만 로드합니다.

간단한 예시: 프로젝트가 플랫폼 게임이라면 모든 레벨의 데이터를 하나의 거대한 블롭(blob)으로 직렬화할 필요가 없습니다. 데이터를 각 레벨에 개별 에셋으로 분할하고, 특히 레벨을 영역으로 세분화하면 플레이어가 접근할 때 데이터를 파싱할 수 있습니다.

언뜻 쉬워 보이는 방법이지만 현실적으로는 툴 코드에 대한 상당한 투자를 필요로 하며, 데이터 구조를 재구성해야 할 수도 있습니다.

### 옵션 3: 스레드

순수 C# 오브젝트에 전부 파싱되고 Unity API와 상호 작용을 필요로 하지 않는 데이터의 경우, 해당 데이터의 파싱 작업을 워커 스레드가 담당하도록 할 수 있습니다.

이 방법으로 멀티 코어를 가진 플랫폼에서 매우 강력한 성능을 발휘할 수 있습니다. 하지만 신중하게 프로그래밍하여 교착 상태와 경쟁 상태를 방지해야 합니다.

**참고**: iOS 기기에는 최대 2개의 코어가 있습니다. 대부분의 Android 기기에는 2-4개가 있습니다. 이 기법은 스탠드얼론 및 콘솔 빌드 타겟용으로 빌드할 때 더 적합합니다.

스레딩을 구현하려는 프로젝트는 빌트인 C# [Thread](https://msdn.microsoft.com/en-us/library/system.threading.thread\(v=vs.110\).aspx) 및 [ThreadPool](https://msdn.microsoft.com/en-us/library/system.threading.threadpool\(v=vs.110\).aspx) 클래스([msdn.microsoft.com](https://msdn.microsoft.com/en-us/library/system.threading.thread\(v=vs.110\).aspx) 참조)를 사용하여 표준 C# 동기화 클래스와 함께 워커 스레드를 관리합니다.
