Microsoft MVP성태의 닷넷 이야기
.NET Framework: 743. C# 언어의 공변성과 반공변성 [링크 복사], [링크+제목 복사]
조회: 7685
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일

C# 언어의 공변성과 반공변성

좋은 번역 글이 있군요. ^^

공변성과 반공변성은 무엇인가?
; https://www.haruair.com/blog/4458

[원글] What are covariance and contravariance? 
; https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance

본문의 내용을 C# 소스 코드로 옮겨 보면 다음과 같이 테스트할 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        Program pg = new Program();

        //pg.f(pg.g1); // Argument 1: cannot convert from 'method group' to 'Program.g'
        //pg.f(pg.g2); // Argument 1: cannot convert from 'method group' to 'Program.g'
        //pg.f(pg.g3); // 'Animal Program.g3(Animal)' has the wrong return type

        pg.f(pg.g4);
    }

    public delegate Dog g(Dog dog);

    string f(g func)
    {
        return "";
    }

    Greyhound g1(Greyhound arg)
    {
        return null;
    }

    Animal g2(Greyhound arg)
    {
        return null;
    }

    Animal g3(Animal arg)
    {
        return null;
    }

    Greyhound g4(Animal arg)
    {
        return null;
    }
}

class Greyhound : Dog
{
}

class Dog : Animal
{
}

class Animal
{
}

따라서, "공변성과 반공변성은 무엇인가?" 글의 본문에 따르면 C#에서도 Animal -> Greyhound는 Dog -> Dog의 서브타입입니다.




본문의 내용을 정리해 보면,

A ≼ B는 A가 B의 서브타입이고,
A -> B는 함수 타입으로 함수의 인자 타입은 A며 반환 타입은 B라는 의미일 때,

(Animal -> Greyhound) ≼ (Dog -> Dog)이다.

다소 말이 어려운데요. 쉽게 생각해서 인자 타입과 반환 타입의 권고 사항인 다음의 2가지를 비교해 기억하시면 됩니다.

  • 인자 타입은 최대한 추상적으로,
  • 반환 타입은 최대한 구체적으로,

예를 들어 다음의 메서드보다는,

Animal MyMethod(Greyhoud arg);

다음의 메서드가 더 좋은 시그니처입니다.

// 물론, MyMethod 안에서 사용하는 멤버가 Animal에 국한하는 것으로 가정!
// 다시 말해, 메서드 내에서 사용하는 멤버들을 만족하는 최소의 추상화 타입을 인자의 타입으로 명시
Greyhoud MyMethod(Animal arg);

그 이유는 메서드를 사용할 때를 생각하면 금방 이해가 됩니다. 가령 인자 타입을 가능한 추상화한 타입을 받기 때문에 다음과 같은 것들이 가능합니다.

MyMethod(new Dog());
MyMethod(new Animal());
MyMethod(new Greyhoud());
MyMethod(new Poodle());  // Poodle ≼ Dog

반대로 반환 타입의 경우에는 가능한 구체적인 타입이 좋습니다. 이것은 추상화한 타입을 반환할 경우와 비교하면 그 이유를 쉽게 알 수 있습니다. 가령, string 타입을 반환하는 메서드를 IEnumerable로 할 수는 있지만 해당 메서드를 사용하는 측에서 보면 이후 원하는 메서드를 사용하는데 제약이 많습니다.

IEnumerable GetText()
{
    return "test";
}

/*
GetText 메서드의 반환 값으로 호출할 수 있는 멤버가 제약이 됨.

GetText().Equals
GetText().GetEnumerator
GetText().GetHashCode
GetText().GetType
GetText().ToString
*/

반면, 최대한 구체적인(concrete) 타입을 반환하면,

string GetText()
{
    return "test";
}

/*
GetText 메서드의 반환 값으로 string의 모든 멤버를 호출 가능
*/

/*
느슨한 결합을 위해 concrete 타입보다는 interface가 선호되기도 하지만,
그런 경우에도 최대한 구체적인 interface를 선택합니다.
*/

위의 규칙을 염두에 두고 다음의 기호로 된 정의를 다시 보면,

(Animal -> Greyhound) ≼ (Dog -> Dog)

반환 타입의 경우는 최대한 구체적으로, 인자 타입은 최대한 추상화하는 것과 일치합니다.




또다시 정리해 보면, 공변/반공변은 각각 인자/반환 타입에 대해 다음과 같이 서술할 수 있습니다.

[A ≼ B일 때 인자 타입에 대해]
    반공변이면,
        (B -> T) ≼ (A -> T)
    공변이면,
        (A -> T) ≼ (B -> T)

[A ≼ B일 때 반환 타입에 대해]
    반공변이면,
        (T -> B) ≼ (T -> A)
    공변이면,
        (T -> A) ≼ (T -> B)

물론, C# 코드를 통해 알아본 것처럼 위의 4가지 경우 중 안전한 가변 규칙은 다음의 2가지로 압축됩니다.

[A ≼ B일 때 인자 타입에 대해 반공변]
        (B -> T) ≼ (A -> T)

[A ≼ B일 때 반환 타입에 대해 공변]
        (T -> A) ≼ (T -> B)

그런데, 타입스크립트(TypeScript) 언어의 경우 인자 타입이 이변적(bivariant)이라고 하면서 공변성과 반공변성을 동시에 지녔다고 하는데 이것은 다음의 규칙이 성립한다는 것입니다.

[A ≼ B일 때 인자 타입에 대해]
    반공변이면서,
        (B -> T) ≼ (A -> T)
    공변,
        (A -> T) ≼ (B -> T)

[A ≼ B일 때 반환 타입에 대해 공변]
        (T -> A) ≼ (T -> B)

물론 인자 타입에 대해 공변인 것은 안전하지 않기 때문에 이 문제를 해결하기 위해 TypeScript 2.6부터 컴파일 옵션을 통해 공변이면 컴파일 오류가 발생하도록 만들었다고 설명하고 있습니다.

반면, Eiffel이라는 언어는 인자 타입에 대해 다음과 같은 규칙이 성립한다는 것이고.

[A ≼ B일 때 인자 타입에 대해 공변]
        (A -> T) ≼ (B -> T)

[A ≼ B일 때 반환 타입에 대해 공변]
        (T -> A) ≼ (T -> B)




"공변성과 반공변성은 무엇인가?" 글에서 자바의 배열은 mutable인데 공변적이므로 부적절하다고 언급하고 있습니다. 이 말을 본문의 기호로 옮겨 보면 이렇습니다.

[A ≼ B일 때]
        (A []) ≼ (B [])

혹은 이렇게 풀어쓸 수 있습니다.

[A가 B를 상속받았을 때]
    A []는  B []의 서브타입이다.

사실 이것은 자바뿐만 아니라 C#도 마찬가지입니다. C#도 배열은 공변이므로,

Greyhound[] greyhoundArr = new Greyhound[] { new Greyhound(), new Greyhound() };

Animal[] animalArr = greyhoundArr;  // 공변, 즉 Greyhound []은 Animal []의 서브타입이므로 컴파일 에러 없음
AnimalArrayCovariant(greyhoundArr); // 에러 없음

static void AnimalArrayCovariant(Animal[] args)
{
}

2가지 모두 컴파일 에러가 발생하지 않고, 심지어 이 때문에 다음과 같은 코딩이 가능합니다.

Greyhound[] greyhoundArr = new Greyhound[] { new Greyhound(), new Greyhound() };
Animal[] animalArr = greyhoundArr;

// 아래의 2개 모두 런타임 에러: System.ArrayTypeMismatchException: 'Attempted to access an element as a type incompatible with the array.' 
animalArr[0] = new Animal(); 
animalArr[0] = new Dog(); 

AnimalArrayCovariant(greyhoundArr);          

static void AnimalArrayCovariant(Animal[] args)
{
    // 아래의 2개 모두 런타임 에러: System.ArrayTypeMismatchException: 'Attempted to access an element as a type incompatible with the array.'
    args[0] = new Animal(); 
    args[0] = new Dog(); 
}

이런 문제 때문에 "공변성과 반공변성은 무엇인가?" 글에서 배열은 mutable인데 공변적이므로 부적절하다고 언급한 것입니다. 게다가 형변환 연산자를 사용하면 반공변적이기도 하다는 문제가 있습니다.

Greyhound[] greyhoundArr = new Greyhound[] { new Greyhound(), new Greyhound() };
Animal[] animalArr = greyhoundArr;
Greyhound[] greyhoundArr2 = (Greyhound[])animalArr;

배열의 이러한 공변성은 말 그대로 Greyhound[]가 Animal[]의 "서브타입"임을 의미합니다. 제가 혼란스러운 것은, 여기서의 "서브타입"은 상속 관계와는 무관하다는 점입니다. 실제로 다음과 같이 테스트를 해보면,

Type type1 = typeof(Greyhound[]);
Type type2 = typeof(Animal[]);

Console.WriteLine(type1.IsSubclassOf(type2)); // False
Console.WriteLine(type2.IsSubclassOf(type1)); // False

Console.WriteLine(type1.IsInstanceOfType(type2)); // False
Console.WriteLine(type2.IsInstanceOfType(type1)); // False

Console.WriteLine(type1.IsAssignableFrom(type2)); // False
Console.WriteLine(type2.IsAssignableFrom(type1)); // True

Greyhound[]가 Animal[] 간에는 상속 관계가 전혀 없으며 단지 "Assignable"하다는 정도로만 설명이 됩니다. 게다가 더욱 혼란스러운 것은 이 글의 처음에 제시했던 예제들도 delegate 차원에서 살펴보면 다음과 같이 Assignable한 관계도 아니라는 점입니다.

public delegate Greyhound GreyhoundAnimalDelegate(Animal arg);

GreyhoundAnimalDelegate greyhoundAnimal = pg.g4;
DogDogDelegate func = greyhoundAnimal; // 컴파일 오류: Cannot implicitly convert type 'Program.GreyhoundAnimalDelegate' to 'Program.g'

Type type1 = typeof(g);
Type type2 = typeof(GreyhoundAnimalDelegate);

Console.WriteLine(type1.IsAssignableFrom(type2)); // False
Console.WriteLine(type2.IsAssignableFrom(type1)); // False

전체적인 맥락에서 봤을 때 "서브타입"을 클래스 간의 물리적인 상속 관계가 아닌, 그와 유사한 논리적인 상속 관계로 이해할 수는 있겠는데 정확히 이것을 콕 집어서 설명할 수가 없으니 좀 답답한 감이 있습니다. 혹시 여기서의 "서브타입"이란 의미에 대해 학문적으로 잘 아시는 분은 덧글 부탁드립니다. ^^




C#의 경우, 공변/반공변이 대두가 된 것은 제네릭이 나오면서부터입니다.

자바와 닷넷의 제네릭 차이점 - 중간 언어 및 공변/반공변 처리
; https://www.sysnet.pe.kr/2/0/1581

"공변성과 반공변성은 무엇인가?" 글에서 나온 질문을 여기서도 해보겠습니다.

C#의 제네릭에서 List<Dog>이 List<Animal>의 서브타입일까요? 

이것은 C#의 제네릭이 공변이냐는 것으로 가령 List<T> 제네릭 타입을 예로 들면 다음과 같은 규칙이 적용되느냐에 해당합니다.

[A ≼ B일 때]
    공변이면,
        (List<A>) ≼ (List<B>)

[A가 B를 상속받았을 때]
    List<A>는 List<B>의 서브타입이다.

이미 아시겠지만, C#의 제네릭은 위의 규칙을 만족하지 않아 공변적이지 않습니다. 따라서 다음과 같은 처리가 불가능합니다.

List<Greyhound> greyhoundArr = new List<Greyhound>(new Greyhound [] { new Greyhound(), new Greyhound() });

List<Animal> animalArr = greyhoundArr; // 컴파일 에러: Cannot implicitly convert type 'List<Greyhound>' to 'List<Animal>'

ListAnimalArrayCovariant(greyhoundArr); // 컴파일 에러: Argument 1: cannot convert from 'List<Greyhound>' to 'List<Animal>'

static void ListAnimalArrayCovariant(List<Animal> args)
{}

즉, C#의 제네릭은 공변적이지도, 반공변적이지도 않기 때문에 invariant(고정, 불변, 무공변)입니다. 따라서 이를 이용하면 배열의 공변 문제를 다음과 같이 해결할 수 있습니다. (Covariance and contravariance (computer science) 글에서 "With the addition of generics, Java and C# now offer ways to write this kind of polymorphic function without relying on covariance."라고 설명하는 그 내용입니다.)

List<Greyhound> greyhoundArr = new List<Greyhound>(new Greyhound[] { new Greyhound(), new Greyhound() });

List<Animal> animalArr = greyhoundArr;       // 컴파일 오류: 제네릭은 불변이므로.
ListAnimalArrayCovariant(greyhoundArr);      // 컴파일 오류: 제네릭은 불변이므로.

// 배열을 제네릭으로 구현
class GenericArray<T>
{
    T[] _elems;

    public GenericArray(T [] elems)
    {
        if (elems == null || elems.Length == 0)
        {
            return;
        }

        _elems = new T[elems.Length];
        Array.Copy(elems, _elems, elems.Length);
    }

    public T this[int index]
    {
        get
        {
            return _elems[index];
        }
        set
        {
            _elems[index] = value;
        }
    }
}




제네릭의 불변이 꼭 좋은 것만은 아닙니다. 왜냐하면, 이 글의 처음에 예시한 것처럼 인자 타입에서 반공변, 반환 타입에서 공변이어야 할 경우가 있기 때문입니다. C#에서는, 인터페이스와 델리게이트에 한해 제네릭이 사용되는 경우 공변/반공변을 허용하도록 C# 4 컴파일러부터 제네릭 타입 인자에 다음의 예약어 지원을 추가했습니다.

out: 공변
in: 반공변

각각의 가변성(variance)에 대해 예약어 지원을 추가하긴 했지만, 아무 위치에서나 사용하도록 허용하지는 않습니다. 즉, "인자 타입을 위한 반공변", "반환 타입을 위한 공변"인 경우에만 허용을 합니다. 따라서 in을 사용해 인자로 사용되는 반공변에 대해서는 다음과 같이 사용 가능하지만,

public interface IMyListContra<in TArg>
{
    void MyGet(TArg arg);
}

delegate void MyFuncCo<in TArg>(TArg arg);

반환에 대해 반공변으로 사용하려고 하면 컴파일 오류가 발생합니다.

// 컴파일 오류: Partial declarations of 'IMyList<TResult>' must have the same type parameter names and variance modifiers in the same order
public interface IMyList<in TResult>
{
    TResult MyGet();
}

// 컴파일 오류: Invalid variance: The type parameter 'TResult' must be covariantly valid on 'IMyList<TResult>.MyGet()'. 'TResult' is contravariant.
delegate TResult MyFuncCo<in TResult>();

마찬가지로, 반환 타입을 위한 out 인자는 가능하지만,

public interface IMyListCo<out TResult>
{
    TResult MyGet();
}

delegate TResult MyFuncContra<out TResult>();

인자 타입을 위한 공변을 하려는 경우 역시 컴파일 오류가 발생합니다.

// 컴파일 오류: Invalid variance: The type parameter 'TArg' must be contravariantly valid on 'IMyList<TArg>.MyGet(TArg)'. 'TArg' is covariant.
public interface IMyList<out TArg>
{
    void MyGet(TArg arg);
}

// 컴파일 오류: Invalid variance: The type parameter 'TArg' must be contravariantly valid on 'MyFuncContra<TArg>.Invoke(TArg)'. 'TResult' is covariant.
delegate void MyFuncContra<out TArg>(TArg arg);

물론, 2개 모두 개별 형식 인자에 지정할 수 있습니다.

// TArg: 반공변 형식 인자
// TResult: 공변 형식 인자
public interface IMyList<in TArg, out TResult>
{
    TResult MyGet(TArg arg);
}

이 정도면 지난 글(자바와 닷넷의 제네릭 차이점 - 중간 언어 및 공변/반공변 처리)과 함께 대충 공변/반공변에 대해서는 정리가 된 것 같습니다. ^^

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




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

[연관 글]


donaricano-btn



[최초 등록일: ]
[최종 수정일: 6/18/2021

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

비밀번호

댓글 쓴 사람
 



2018-05-04 12시21분
[용균] 리퍼러 링크가 있어서 오게 되었는데 너무 놀랐네요. 안그래도 C# 보다가 쉽게 와닿지 않아서 번역한 글이었는데 C#으로 설명해주셔서 제가 다 감사할 따름입니다. 늘 느끼지만 용어가 어려우면 실제 코드, 관념을 연결해서 생각하는게 쉽지 않은 것 같습니다.

중간에 배열 설명하시는 부분에서 서브타입은 상속 관계를 의미하는게 맞다고 생각합니다. 대신 명제의 역은 참이 아니기 때문에 공변성이 있다고 해서 관계가 서브타입이라고 추론할 수 없는 것이라고 보고 있습니다. 배열은 제네릭 이전부터 존재했기 때문에 mutable 하면서도 공변적으로 구현되어 있다고 생각하는데요. 제네릭으로 구현되어 있다고, 즉 System.Array<out TEntity> 식으로 생각하면 타입 관계가 좀 더 와닿는 것 같습니다.

IsAssignableFrom 메소드가 궁금해서 찾아보니 공변성, 반공변성 모두 확인해주네요. 설명에 또 많이 배워 갑니다.
[손님]
2018-05-04 04시21분
근데, 배열의 경우 상속 관계를 의미한다고 해버리면 그걸 증명할 길이 없습니다. 왜냐하면 닷넷의 Type 시스템에서는 명확히 그것이 상속 관계라는 걸 밝힐 수 있는 어떠한 방법도 제공하지 않고 있으며, 오히려 상속 관계를 알아내는 메서드 등에서는 전부 상속이 아니라고 나오기 때문입니다.

언급하신 "공변성이 있다고 해서 관계가 서브타입이라고 추론할수 없는 것"에는 수긍이 갑니다. 그런데, 사실 위와 같은 이유로 인해 현재 공변성이 있다는 것 외에는 서브타입을 판단할 수 있는 방법이 달리 없습니다. 아직 제가 미숙하다 보니 원글에서의 서브타입이라는 용어를 정확히 이해하기까지는 이 부분에 대한 결론은 좀 유보하려고 합니다. 어쨌든 한 걸음씩 나가다 보면 언젠가 명확해질 날이 있겠죠. ^^

용균님 덕분에 이 정도까지만이라도 정리할 수 있었습니다. ^^
정성태
2018-05-04 04시36분
[용균] 정성태님 댓글을 보니 제 덧글은 잘못된 내용이네요. 좀 더 공부해보고 제대로 정리해야겠습니다. 말씀 감사드립니다!
[손님]
2018-05-24 03시12분
[spowner] https://blogs.msdn.microsoft.com/ericlippert/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance/
이 글에 의하면 배열이 컴파일 타임이 아니라 런타임에서 예외로 처리하는 이뉴는 Java를 CLR에서 수용하기 위해 규칙이 깨졌다고 이해가 됩니다. (한마디로 만가진 T_T)
하위 호환성을 유지하는 목적은 대중적으로 사용하는 언어에 필수적인 경우라, 이후 수정없이 유지된게 아닌가 해요

용균님과 성태님 덕분에 왜 C#에서 제네릭의 공변/반공변을 인터페이스와 딜리게이트에만 제한적으로 허용했는지를 이해하게 됩니다.
[손님]
2018-05-24 09시52분
제가 인용한 글에서의 "서브타입"이라는 것을, spowner 님이 소개한 글에서는,

Covariance and Contravariance in C#, Part One
; https://docs.microsoft.com/en-us/archive/blogs/ericlippert/covariance-and-contravariance-in-c-part-one

Covariance and Contravariance in C#, Part Two: Array Covariance
; https://docs.microsoft.com/en-us/archive/blogs/ericlippert/covariance-and-contravariance-in-c-part-two-array-covariance

다음과 같은 기준으로,

T is bigger than U.
T is smaller than U.

by “bigger than” and “smaller than” I explicitly do NOT mean “is a supertype of” and “is a subtype of”.

설명하고 있군요. 즉, 명시적으로 서브타입이라고 쓰지 않고 "bigger than", "smaller than"이라고 바꿔 설명하고 있는 걸로 봤을 때 제가 인용한 글에서도 "서브타입"이라고 쓴 것을 다른 용어로 바꾸는 것이 더 좋을 듯 합니다. 암튼, 어려운 이야기들입니다. ^^

그나저나, Java와 맞춰 주기 위해서였다고는 하지만 Java가 없었다고 해도 충분한 논란이 있었을 것 같습니다. 개발의 편이성으로 봤을 때 현재 그 구조가 좋은 것은 사실이고, 반면 런타임에 체크해야 하는 오버헤드가 추가되었으니 성능 면에서는 손해지만... 휴... 이것도 역시 정답이 안 나오는 이야기같습니다. ^^
정성태
2021-05-14 12시31분
C#을 다루는 기술
; http://www.yes24.com/Product/Goods/101511486

위의 책에서, 다시 한번 "Covariance and Contravariance in C#, Part Two: Array Covariance" 링크와 함께 배열의 공변에 관한 이야기가 나오는군요. ^^

"
참조 타입의 배열은 배열의 공변 특성 때문에 값을 저장할 때는 대체로 안전하다. 이 책의 범위를 벗어나는 주제이기는 하지만, 배열에 공변의 특성을 부여한 것은 초기 설계의 실수라고 생각한다. 에릭 리퍼트가 공변과 반공변 특성에 대해서 쓴 블로그의 글을 살펴보기 바란다.
"
정성태
2021-06-19 11시47분
[한예지] 선생님 읽다가 이해되지 않는 부분이 있어서 질문드립니다...

Grayhound <: Dog // Grayhound는 Dog의 서브타입
Grayhoud → Dog // 함수의 인자 타입은 Grayhoud, 반환 타입은 Dog
라고 이해했습니다.

그럼 밑에는 다음과 같이 해석하는 것이 맞나요?
(Animal → Greyhound) <: (Dog → Dog)
// 질문 ① (Animal → Grayhound)는 함수 인자가 Dog이고,
반환 타입이Dog인 놈에 서브타입인데
서브타입은 함수 인자를 Animal, 반환타입을 Grayhound로 받는다는 뜻인가요??

// 질문 ②
인자 타입은 최대한 추상적으로,
반환 타입은 최대한 구체적이라면
(Animal → Greyhound) <: (Dog → Dog)보다는
(Dog → Greyhound) <: (Dog → Dog)이 좋은 표현 아닌가요??
[손님]
2021-06-19 05시41분
답변 1) 넵 맞습니다. 좀 더 극명한 예제로 아래의 코드를 살펴 보시면 도움이 될 것입니다.

// C# 2.0부터 컴파일 가능
delegate Dog BaseFunc(Dog dog);

class Program
{
    static void Main(string[] args)
    {
        BaseType b = new SubType();
        BaseFunc f = SubFunc;
    }

    static Greyhound SubFunc(Animal arg)
    {
        return null;
    }
}

class BaseType
{
}

class SubType : BaseType
{
}

class Greyhound : Dog
{
}

class Dog : Animal
{
}

class Animal
{
}

답변 2) (Dog -> Greyhound) <: (Dog -> Dog)가 왜 좋은 표현이라는 거죠? 그것은 뭐랄까요, byte의 범위가 [0, 255]인데 [0, 127]도 바이트 범위가 맞으니 그게 더 좋은 표현이라고... 하는 것처럼 보입니다.
정성태
2021-06-21 07시01분
[한예지] 답변 감사드립니다!
[손님]

1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
12714정성태7/16/202167오류 유형: 735. VCRUNTIME140.dll, MSVCP140.dll, VCRUNTIME140.dll, VCRUNTIME140_1.dll이 없어 exe 실행이 안 되는 경우
12713정성태7/16/2021146.NET Framework: 1077. C# - 동기 방식이면서 비동기 규약을 따르게 만드는 Task.FromResult파일 다운로드1
12712정성태7/15/202191개발 환경 구성: 579. Azure - 리눅스 호스팅의 Site Extension 제작 방법
12711정성태7/15/202176개발 환경 구성: 578. Azure - Java Web App Service를 위한 Site Extension 제작 방법
12710정성태7/15/2021131개발 환경 구성: 577. MQTT - emqx.io 서비스 소개
12709정성태7/14/202165Linux: 42. 실행 중인 docker 컨테이너에 대한 구동 시점의 docker run 명령어를 확인하는 방법
12708정성태7/14/202196Linux: 41. 리눅스 환경에서 디스크 용량 부족 시 원인 분석 방법
12707정성태7/14/202164오류 유형: 734. MySQL - Authentication method 'caching_sha2_password' not supported by any of the available plugins.
12706정성태7/14/2021177.NET Framework: 1076. C# - AsyncLocal 기능을 CallContext만으로 구현하는 방법 [1]파일 다운로드1
12705정성태7/13/2021146VS.NET IDE: 168. x64 DLL 프로젝트의 컨트롤이 Visual Studio의 Designer에서 보이지 않는 문제 - 두 번째 이야기
12704정성태7/12/2021123개발 환경 구성: 576. Azure VM의 서비스를 Azure Web App Service에서만 접근하도록 NSG 설정을 제한하는 방법
12703정성태7/11/2021106개발 환경 구성: 575. Azure VM에 (ICMP) ping을 허용하는 방법
12702정성태7/11/202175오류 유형: 733. TaskScheduler에 등록된 wacs.exe의 Let's Encrypt 인증서 업데이트 문제
12701정성태7/9/2021141.NET Framework: 1075. C# - ThreadPool의 스레드는 반환 시 ThreadStatic과 AsyncLocal 값이 초기화 될까요?파일 다운로드1
12700정성태7/8/2021212.NET Framework: 1074. RuntimeType의 메모리 누수? [1]
12699정성태7/8/2021166VS.NET IDE: 167. Visual Studio 디버깅 중 GC Heap 상태를 보여주는 "Show Diagnostic Tools" 메뉴 사용법
12698정성태7/7/2021236오류 유형: 732. Windows 11 업데이트 시 3% 또는 0%에서 다운로드가 멈춘 경우
12697정성태7/7/2021185개발 환경 구성: 574. Windows 11 (Insider Preview) 설치하는 방법
12696정성태7/6/2021231VC++: 146. 운영체제의 스레드 문맥 교환(Context Switch)을 유사하게 구현하는 방법파일 다운로드2
12695정성태7/3/2021251VC++: 145. C 언어의 setjmp/longjmp 기능을 Thread Context를 이용해 유사하게 구현하는 방법파일 다운로드1
12694정성태7/2/2021113Java: 24. Azure - Spring Boot 앱을 Java SE(Embedded Web Server)로 호스팅 시 로그 파일 남기는 방법
12693정성태6/30/202198오류 유형: 731. Azure Web App Site Extension - Failed to install web app extension [...]. {1}
12692정성태6/30/2021243디버깅 기술: 180. Azure - Web App의 비정상 종료 시 남겨지는 로그 확인
12691정성태6/30/2021152개발 환경 구성: 573. 테스트 용도이지만 테스트에 적합하지 않은 Azure D1 공유(shared) 요금제
12690정성태6/28/2021171Java: 23. Azure - 자바(Java)로 만드는 Web App Service - Tomcat 호스팅
12689정성태6/25/2021155오류 유형: 730. Windows Forms 디자이너 - The class Form1 can be designed, but is not the first class in the file.
1  [2]  3  4  5  6  7  8  9  10  11  12  13  14  15  ...