Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1113. C# 10 - (13) 문자열 보간 성능 개선 [링크 복사], [링크+제목 복사]
조회: 361
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# 10 - (13) 문자열 보간 성능 개선

휴~~~ 이번에도 역시, 이를 설명하기 위해 지난 2개의 글을 먼저 설명할 필요가 있었습니다. ^^;

C# - FormattableString 타입
; https://www.sysnet.pe.kr/2/0/12819

C# - .NET 6부터 공개된 ISpanFormattable 사용법
; https://www.sysnet.pe.kr/2/0/12821

그리고, 이번 문법에 대해서는 spec 문서보다는 다음의 블로그 글을 읽는 것이 더 이해가 잘 될 것입니다.

String Interpolation in C# 10 and .NET 6
; https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/

사실 이번 글은 위의 내용을 정리한 것에 불과합니다.




자, 그럼 왜 문자열 보간에 대한 개선을 해야만 했는지, 기존 문자열 보간이 가진 문제점 - 즉, string.Format에 대한 문제점들을 다음과 같이 나열할 수 있습니다.

1) string.Format은 꽤나 무거운 작업을 동반합니다. 즉, 서식이나 정렬에 관계되고 심지어 자체적인 문자열 파싱도 해야 합니다. 그런데, 여기서 재미있는 것은 C# 컴파일러의 경우 string interpolation 표현이 있을 때 이미 한번 파싱을 해 string.Format으로 변경하는데요, 이것을 다시 런타임에 string.Format 메서드 내에서 다시 파싱을 하고 있었다는 점입니다.

2) string.Format은 object 인자를 받는데, 이로 인해 값 타입의 인스턴스인 경우 반드시 박싱 연산을 필요로 하게 됩니다. 이와 함께, 그동안 값 타입의 성능을 높이기 위한 모든 다양한 방법들, 가령 Span<char> 등의 인스턴스는 string.Format의 Object 타입 제한으로 인해 사용할 수 없는 문제점이 있습니다.

3) string.Format은 3개까지 인자를 받는 오버로드를 제공하지만, 그 이후부터는 params Object[]로 처리하기 때문에 인자 수가 4개 이상인 경우부터 무조건 배열을 위한 힙 메모리 할당이 발생합니다.

4) 인자로 넘어온 값은 문자열 변환을 위해 반드시 ToString 호출을 필요로 하며, 당연히 이때 임시 문자열이 생성됩니다.

이런 유의 문제점들을 C# 10에서 해결했다는데, 사실 어떻게 저런 것들을 개선할 수 있었을지 잘 상상이 안 됩니다. 한번 볼까요? ^^




C# 컴파일러의 경우, foreach에 배열을 전달해 열거를 하게 되면,

int[] array = ...;
foreach (int i in array)
{
    Use(i);
}

원래의 IEnumerator/IEnumerable을 활용한 코드로 번역하지 않고,

int[] array = ...;
using (IEnumerator<int> e = array.GetEnumerator())
{
    while (e.MoveNext())
    {
        Use(e.Current);
    }
}

좀 더 빠른 성능을 내도록 일부러 indexer를 활용하는 식으로 바꿔서 번역한다고 합니다.

int[] array = ...;
for (int i = 0; i < array.Length; i++)
{
    Use(array[i]);
}

그리고, 이런 식의 전처리를 컴파일러 쪽의 용어로 "Lowering"이라고 일컫는다는데,,, ^^ Lowering을 한글로 뭐라고 표현하면 좋을까요? ^^ 의미상으로 보면 "구문 변환"이면서 다소 최적화를 하므로 "최적 구문 변환" 정도로 하면 될까요? ^^; (혹시 이에 해당하는 번역을 아시는 분은 덧글 부탁드립니다.)

그러니까, C# 10 컴파일러는 문자열 보간에 대해서도 단순히 이전처럼 string.Concat나 string.Format으로 번역하지 않고, 좀 더 성능이 좋은 코드로 바꾸게 되었는데요, 바로 그 역할을 위해 .NET 6에 추가된 타입이 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler ref struct입니다.

생각했던 것보다 의외로 해결책은 간단합니다. DefaultInterpolatedStringHandler는 문자열 처리를 StringBuilder처럼 하는데, Append 시킬 문자열에 대해 제네릭 인자로 처리하기 때문에 박싱 문제에서 자유롭게 된 것입니다.

namespace System.Runtime.CompilerServices
{
    [InterpolatedStringHandler]
    public ref struct DefaultInterpolatedStringHandler
    {
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount);
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider);
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span<char> initialBuffer);

        public void AppendLiteral(string value);

        public void AppendFormatted<T>(T value);
        public void AppendFormatted<T>(T value, string? format);
        public void AppendFormatted<T>(T value, int alignment);
        public void AppendFormatted<T>(T value, int alignment, string? format);

        public void AppendFormatted(ReadOnlySpan<char> value);
        public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

        public void AppendFormatted(string? value);
        public void AppendFormatted(string? value, int alignment = 0, string? format = null);
        public void AppendFormatted(object? value, int alignment = 0, string? format = null);

        public string ToStringAndClear();
    }
}

따라서, C# 9 이전에는 다음과 같은 식의 문자열 보간을 하면,

public static string FormatVersion(int major, int minor, int build, int revision) =>
    $"{major}.{minor}.{build}.{revision}";

이렇게 번역을 했지만,

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var array = new object[4];
    array[0] = major; // 박싱 발생
    array[1] = minor; // 박싱 발생
    array[2] = build; // 박싱 발생
    array[3] = revision; // 박싱 발생
    return string.Format("{0}.{1}.{2}.{3}", array); // 내부에서 각각 ToString을 호출해 문자열 힙 할당 발생
}

C# 10부터는 이렇게 바뀌게 됩니다.

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var handler = new DefaultInterpolatedStringHandler(literalLength: 3, formattedCount: 4);
    handler.AppendFormatted(major); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(minor); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(build); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(revision); // 박싱 및 문자열 힙 할당 없음
    return handler.ToStringAndClear();
}

박싱은 물론이고 개별 힙 할당도 없어지고 마지막의 ToStringAndClear에서 최종 문자열을 만들어 단 한 번의 힙 할당만 하고 있습니다.

또한, StringBuilder와는 달리 DefaultInterpolatedStringHandler는 AppendFormatted를 public으로 공개했기 때문에, 사용자 정의 구조체 타입에도 ISpanFormattable만 구현되어 있다면 그대로 사용할 수 있습니다. 일례로 아래의 예제 코드는 실행해 보면 Console.WriteLine에서 Person 타입의 object.ToString 메서드가 실행되지 않고 TryFormat 메서드가 실행되는 것을 확인할 수 있습니다.

using System.Diagnostics;
using System.Runtime.InteropServices;

Console.WriteLine(Format.FormatVersion(1, 5, 1, 2<span style='color: blue; font-weight: bold'>,
                new Person { Age = 25 }</span>));

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision, Person person) =>
        $"{major}.{minor}.{build}.{revision}.{person}";
}

// "C# - .NET 6부터 공개된 ISpanFormattable 사용법" 글의 예제 코드
public struct Person : ISpanFormattable
{
    public int Age;

    public override string ToString()
    {
        return ToString(null, null);
    }

    // ...[생략]...

    public unsafe bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
    {
        return TryFormatInt32(this.Age, -1, format, provider, destination, out charsWritten);
    }

    // ...[생략]...
}




자, 그런데 과거 FormattableString이 나온 이유를 상기시켜 보면, 이제 저렇게 바뀐 문자열 보간 처리 방식도 여전히 Localization에 대한 문제가 남아 있다는 것을 알 수 있습니다. 역시나 마이크로소프트는 이 문제도 함께 해결해야만 했을 것이고, 이를 위해 문자열 보간 처리를 위임하는 DefaultInterpolatedStringHandler에게 사용자가 넘겨주는 Localization 정보를 활용할 수 있는 메서드를 제공하는 방식으로 우회하고 있습니다.

즉, 위의 예제 코드에서 만약 사용자가 Localization을 제공해야 한다면 우선 다음과 같은 식의 메서드를 하나 정의합니다.

public static string Create(IFormatProvider provider, 
    [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler) =>
        handler.ToStringAndClear();

해당 메서드에는 InterpolatedStringHandlerArgument 특성이 DefaultInterpolatedStringHandler 매개 변수에 적용되었고, 그 인자로 "provider"라는 문자열로 IFormatProvider를 전달할 매개 변수 이름을 지정했습니다. 그리고 이렇게 만든 메서드를 이전의 FormatVersion 메서드에 다음과 같이 적용시켜 주면,

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string Create(IFormatProvider provider, 
        [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler) =>
            handler.ToStringAndClear();
}

C# 10 컴파일러는 이를 인지하고 FormatVersion 메서드의 내부 코드를 다음과 같이 생성해 줍니다.

public static string FormatVersion(int major, int minor, int build, int revision)
{
    IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
    IFormatProvider provider = invariantCulture;
    DefaultInterpolatedStringHandler defaultInterpolatedStringHandler;
    defaultInterpolatedStringHandler..ctor(3, 4, invariantCulture);
    defaultInterpolatedStringHandler.AppendFormatted<int>(major);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(minor);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(build);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.Create(provider, ref defaultInterpolatedStringHandler);
}

이뿐만이 아닙니다. 사실 위의 코드를 자세히 들여다보면 결국 AppendFormatted로 들어갈 문자열들이 어딘가에는 버퍼로 있어야 한다는 것을 알 수 있으며 결국 그 버퍼들이 생성되는 문제가 있다는 것도 짐작할 수 있습니다. 마이크로소프트는 일단 이 문제를 ArrayPool을,

C# - ArrayPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12478

C# - ArrayPool<T>와 MemoryPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12480

이용하는 방식으로 기본 구현을 제공합니다. 하지만, 이것조차도 사용하고 싶지 않을 때, 즉 사용자 측에서 TryFormat으로 인해 필요한 버퍼의 크기를 미리 알 수 있다면 직접 stackalloc 등의 버퍼로 대체할 수 있도록 또 다른 인자를 제공합니다. 아래는 그렇게 해서 바뀐 Create 메서드와 그것을 사용해 FormatVersion2 메서드에 반영한 예제를 보여줍니다.

public static string Create(IFormatProvider provider, Span<char> myBuffer,
    [InterpolatedStringHandlerArgument("provider", "myBuffer")] ref DefaultInterpolatedStringHandler handler) =>
        handler.ToStringAndClear();

public static string FormatVersion2(int major, int minor, int build, int revision) =>
    Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

그럼, C# 10 컴파일러는 ArrayPool로부터 버퍼를 받아오지 않고 사용자가 넘겨준 버퍼를 활용하도록 다음과 같은 코드를 생성합니다.

public unsafe static string FormatVersion2(int major, int minor, int build, int revision)
{
    IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
    IFormatProvider formatProvider = invariantCulture;
    Span<char> span = new Span<char>(stackalloc byte[(UIntPtr)128], 64);
    Span<char> span2 = span;
    IFormatProvider provider = formatProvider;
    Span<char> myBuffer = span2;
    DefaultInterpolatedStringHandler defaultInterpolatedStringHandler;
    defaultInterpolatedStringHandler..ctor(3, 4, invariantCulture, span2);
    defaultInterpolatedStringHandler.AppendFormatted<int>(major);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(minor);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(build);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.Create(provider, myBuffer, ref defaultInterpolatedStringHandler);
}

그리고, 사용자의 편의를 위해 Create 메서드는 고정적으로 사용할 수 있는 유형인 까닭에 미리 string 타입에 포함시켰으므로 최종적으로는 위의 코드를 다음과 같이 간단하게 변경할 수 있습니다.

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion2(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");
}

복잡하게 설명했지만, Localization이나 사용자 버퍼를 전달하고 싶다면 string.Create를 사용하면 되고, 그 외의 경우라면 그냥 일반적인 보간 문자열을 예전처럼 사용하면 됩니다. (어쩌면, 원래부터 DefaultInterpolatedStringHandler가 관여됐다면 애당초 FormattableString은 나오지 않았을 것입니다.)




재미있는 것은 C# 10 컴파일러에게 DefaultInterpolatedStringHandler와 같은 타입을 사용자 정의해 인식시킬 수 있다는 점입니다. 이를 위해 필요한 것은 일정한 포맷을 가진 AppendLiteral, AppendFormatted와 같은 메서드를 담고 있을 것과 InterpolatedStringHandlerAttribute 특성만 부여돼 있으면 됩니다.

일례로 다음과 같은 타입을 하나 임의로 만들 수 있고,

[InterpolatedStringHandler]
public ref struct MyInterpolatedStringHandler
{
    public MyInterpolatedStringHandler(int literalLength, int formattedCount)
    {
    }

    public MyInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider provider)
    {
    }

    public MyInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider provider, Span<char> initialBuffer)
    {
    }

    public string MakeText()
    {
        return null;
    }

    public void AppendLiteral(string value)
    {
    }

    public void AppendFormatted<T>(T value)
    {
    }
}

이것을 사용하는 FormatVersion 메서드를 자유롭게 정의할 수 있습니다.

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion2(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion3(int major, int minor, int build, int revision) =>
        Format.UseMyHandler($"{major}.{minor}.{build}.{revision}");

    public static string UseMyHandler([InterpolatedStringHandlerArgument()] ref MyInterpolatedStringHandler handler) =>
        handler.MakeText();
}

그럼 C# 10 컴파일러는 여러분들이 정의한 타입을 이용해 문자열 보간 구문을 처리하는 코드를 생성합니다.

// Format
public static string FormatVersion3(int major, int minor, int build, int revision)
{
    MyInterpolatedStringHandler myInterpolatedStringHandler = new MyInterpolatedStringHandler(3, 4);
    myInterpolatedStringHandler.AppendFormatted<int>(major);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(minor);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(build);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.UseMyHandler(ref myInterpolatedStringHandler);
}




이와 관련한 변화들을 좀 볼까요? ^^

새로운 DefaultInterpolatedStringHandler도 사실 ref struct 타입은 처리할 수 없습니다. 왜냐하면, 위와 같은 처리 자체가 대상 값 타입이 ISpanFormattable을 구현하고 있는 경우에 한해 힙 할당을 없애는 방향으로 동작하기 때문에 인터페이스조차 상속할 수 없는 ref struct는 그에 대한 혜택을 받지 못합니다. 하지만, ref struct 유형 중 가장 많이 사용되는 ReadOnlySpan<char>에 대해서는 오버로드된 메서드를 가지고 있기 때문에,

public void AppendFormatted(ReadOnlySpan<char> value);
public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

문자열에 대한 처리에 한해서는 다음과 같이 직접 문자열 보간에 사용하는 것이 가능해졌습니다.

ReadOnlySpan<char> span = "Hello World!"[0..2].AsSpan();
string text = $"{span,4}"; // C# 9 이하에서는 컴파일 오류 - error CS0029: Cannot implicitly convert type 'System.ReadOnlySpan<char>' to 'object'

또한 StringBuilder의 경우 Append 메서드에 AppendInterpolatedStringHandler가 적용된 오버로드를 추가해,

; https://docs.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendinterpolatedstringhandler

문자열 보간으로 Append하는 경우,

StringBuilder sb = new StringBuilder();
int value = 5;
sb.Append($"{value}");

AppendInterpolatedStringHandler를 사용하는 코드로 C# 10 컴파일러는 번역을 합니다.

StringBuilder.AppendInterpolatedStringHandler appendInterpolatedStringHandler;
appendInterpolatedStringHandler..ctor(0, 1, stringBuilder);
appendInterpolatedStringHandler.AppendFormatted<int>(value);
stringBuilder2.Append(ref appendInterpolatedStringHandler);

위의 번역이 재미있는 것이, 만약 여러분들이 기존에 StrinBuilder와 함께 사용하던 문자열 보간 코드가 있다면 C# 10 컴파일러로 다시 빌드하는 것만으로도 성능 향상의 기회를 가질 수 있다는 점입니다. ^^

마지막으로 AssertInterpolatedStringHandler를 제공해,

Debug.AssertInterpolatedStringHandler Struct
; https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.assertinterpolatedstringhandler

Debug.Assert에서의 문자열 보간 사용 시 힙 메모리 할당을 최적화할 수 있게 되었고, TryWriteInterpolatedStringHandler를 제공해,

MemoryExtensions.TryWriteInterpolatedStringHandler Struct
; https://docs.microsoft.com/en-us/dotnet/api/system.memoryextensions.trywriteinterpolatedstringhandler

제공해야 할 문자열 크기에 비해 버퍼가 작다면 Append 관련 코드를 빠르게 벗어날 수 있는 InterpolatedStringHandler가 제공되고 있습니다. 아마도, 이런 식으로 사용자 정의된 handler는 점점 더 늘어날 것이고 이로 인해 BCL은 계속해서 힙 메모리 사용을 줄이는 쪽으로 가게 될 것입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




C# 10 - (1) 구조체를 생성하는 record struct (공식 문서, Static Abstract Members In Interfaces C# 10 Preview)
; https://www.sysnet.pe.kr/2/0/12790

C# 10 - (2) 전역 네임스페이스 선언 (공식 문서, Global Using Directive)
; https://www.sysnet.pe.kr/2/0/12792

C# 10 - (3) 개선된 변수 초기화 판정 (공식 문서, Improved Definite Assignment)
; https://www.sysnet.pe.kr/2/0/12793

C# 10 - (4) 상수 문자열에 포맷 식 사용 가능 (공식 문서, Constant Interpolated Strings)
; https://www.sysnet.pe.kr/2/0/12796

C# 10 - (5) 속성 패턴의 개선 (공식 문서, Extended property patterns)
; https://www.sysnet.pe.kr/2/0/12799

C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용 (공식 문서, Sealed record ToString)
; https://www.sysnet.pe.kr/2/0/12801

C# 10 - (7) Source Generator V2 APIs (공식 문서, Source Generator V2 APIs)
; (예약) https://www.sysnet.pe.kr/2/0/12804

C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능 (공식 문서, Mix declarations and variables in deconstruction)
; https://www.sysnet.pe.kr/2/0/12805

C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능 (공식 문서, Async method builder override); 
; https://www.sysnet.pe.kr/2/0/12807

C# 10 - (10) 개선된 #line 지시자 (공식 문서, Enhanced #line directive)
; https://www.sysnet.pe.kr/2/0/12812

C# 10 - (11) Lambda 개선 (공식 문서 1, 공식 문서 2, Lambda improvements) 
; https://www.sysnet.pe.kr/2/0/12813

C# 10 - (12) 인터페이스 내에 정적 추상 메서드 정의 가능(공식 문서, Static Abstract Members In Interfaces C# 10 Preview)
; https://www.sysnet.pe.kr/2/0/12814

C# 10 - (13) 문자열 보간 성능 개선 (공식 문서, Interpolated string improvements)
; https://www.sysnet.pe.kr/2/0/12826

C# 10 - (14) 단일 파일 내에 적용되는 namespace 선언 (공식 문서, File-scoped namespace)
; https://www.sysnet.pe.kr/2/0/12828

C# 10 - (15) 구조체 타입에 기본 생성자 정의 가능 (공식 문서, Parameterless struct constructors)
; https://www.sysnet.pe.kr/2/0/12829

C# 10 - (16) CallerArgumentExpression 특성 추가 (공식 문서, Caller expression attribute)
; https://www.sysnet.pe.kr/2/0/12835

C# 10 - (17) 제네릭 유형의 특성 허용 (공식 문서, Generic attributes)
; https://www.sysnet.pe.kr/2/0/12839

Language Feature Status
; https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 10/1/2021]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 쓴 사람
 




[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12850정성태10/27/202116오류 유형: 765. 우분투에서 pip install mysqlclient 실행 시 "OSError: mysql_config not found" 오류
12849정성태10/17/2021221스크립트: 33. JavaScript와 C#의 시간 변환
12848정성태10/17/2021161스크립트: 32. 파이썬 - sqlite3 기본 예제 코드
12847정성태10/14/2021138스크립트: 31. 파이썬 gunicorn - WORKER TIMEOUT 오류 발생
12846정성태10/7/2021283스크립트: 30. 파이썬 __debug__ 플래그 변수에 따른 코드 실행 제어
12845정성태10/6/2021477.NET Framework: 1120. C# - BufferBlock<T> 사용 예제 [4]파일 다운로드1
12844정성태10/3/2021219오류 유형: 764. MSI 설치 시 "... is accessible and not read-only." 오류 메시지
12843정성태10/3/2021234스크립트: 29. 파이썬 - fork 시 기존 클라이언트 소켓 및 스레드의 동작파일 다운로드1
12842정성태10/1/2021230오류 유형: 763. 파이썬 오류 - AttributeError: type object '...' has no attribute '...'
12841정성태10/1/2021305스크립트: 28. 모든 파이썬 프로세스에 올라오는 특별한 파일 - sitecustomize.py
12840정성태9/30/2021330.NET Framework: 1119. Entity Framework의 Join 사용 시 다중 칼럼에 대한 OR 조건 쿼리파일 다운로드1
12839정성태9/15/2021557.NET Framework: 1118. C# 10 - (17) 제네릭 타입의 특성 적용파일 다운로드1
12838정성태9/13/2021525.NET Framework: 1117. C# - Task에 전달한 Action, Func 유형에 따라 달라지는 async/await 비동기 처리 [2]파일 다운로드1
12837정성태9/11/2021311VC++: 151. Golang - fmt.Errorf, errors.Is, errors.As 설명
12836정성태9/10/2021305Linux: 45. 리눅스 - 실행 중인 다른 프로그램의 출력을 확인하는 방법
12835정성태9/7/2021314.NET Framework: 1116. C# 10 - (16) CallerArgumentExpression 특성 추가파일 다운로드1
12834정성태9/7/2021270오류 유형: 762. Visual Studio 2019 Build Tools - 'C:\Program' is not recognized as an internal or external command, operable program or batch file.
12833정성태9/6/2021388VC++: 150. Golang - TCP client/server echo 예제 코드파일 다운로드1
12832정성태9/6/2021273VC++: 149. Golang - 인터페이스 포인터가 의미 있을까요?
12831정성태9/6/2021261VC++: 148. Golang - 채널에 따른 다중 작업 처리파일 다운로드1
12830정성태9/6/2021265오류 유형: 761. Internet Explorer에서 파일 다운로드 시 "Your current security settings do not allow this file to be downloaded." 오류
12829정성태9/5/2021361.NET Framework: 1115. C# 10 - (15) 구조체 타입에 기본 생성자 정의 가능파일 다운로드1
12828정성태9/4/2021317.NET Framework: 1114. C# 10 - (14) 단일 파일 내에 적용되는 namespace 선언파일 다운로드1
12827정성태9/4/2021260스크립트: 27. 파이썬 - 웹 페이지 데이터 수집을 위한 scrapy Crawler 사용법 요약
12826정성태9/3/2021361.NET Framework: 1113. C# 10 - (13) 문자열 보간 성능 개선파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...