9.1 선택적 매개변수와 명명된 인수

 

단 하나의 메서드만 정의하는 선택적 매개변수

Person p = new();
p.Output("Anderson");
p.Output("Winnie", 32);
p.Output("Tom", 28, "Taiwan");
class Person
{
    public void Output(string name, int age = 0, string address = "Korea")
    {
        Console.WriteLine(string.Format($"{name}: {age} in {address}"));
    }
}

# 출력
Anderson: 0 in Korea
Winnie: 32 in Korea
Tom: 28 in Taiwan

 

특징

- 선택적 매개변수는 ref, out 예약어와 함께 사용 불가

- 선택적 매개변수가 시작되면 그 사이에 필수 매개변수를 사용할 수 없다.

- 선택적 매개변수가 시작돼도 마지막에는 params 유형의 매개변수를 정의할 수 있다.

더보기

params 키워드는 가변 인자 (variable number of arguments) 를 받을 수 있도록 해주는 매개변수 배열을 의미한다. 이 키워드를 사용하면 메서드에 0개 이상의 인수를 배열처럼 전달할 수 있다.

PrintNumbers();                   // 아무 값도 전달하지 않음
PrintNumbers(1);                 // 하나 전달
PrintNumbers(1, 2, 3, 4, 5);     // 여러 개 전달

void PrintNumbers(params int[] numbers)
{
    foreach (int number in numbers)
    {
        Console.WriteLine(number);
    }
}

- params 유형의 매개변수는 선택적 매개변수가 될 수 없다. 즉 기본값을 지정할 수 없다.

- 선택적 매개변수의 기본값은 반드시 상수 표현식이어야함.

- 선택적 매개변수에 전달되는 인자는 차례대로 대응되며, 중간에 생략돼 전달될 수 없다. 

=> 다만, 명명된 인수를 이용하면 원하는 값을 골라서 전달 할수 있기에 중간에 생략 가능한 경우다.

p.Output("Tom", "Taiwan"); // 컴파일에러, age생략하고, address 인자 전달할 수 없다.

p.Output("Tom", address: "Taiwan");  // address 매개변수를 지정해서 값을 전달, 컴파일에러 x

명명된 인수는 순서를 지킬 필요도 없음.

 

 

9.2 dynamic 예약어

ms는 정적언어 말고도 동적언어까지도 닷넷과 호환 목적으로 DLR(Dynamic Langugage Runtime)라이브러리를 제공하며, C# 4.0에서는 동적 언어와 연동을 쉽게 할 수 있도록 dynamic 예약어를 추가했다.

 

예제 9.3 dynamuc 사용 예

dynamic d = 5;
int sum = d + 10;
Console.WriteLine(sum);

#출력
15

var의 경우 c# 컴파일러가 빌드 시점에 초깃값과 대응되는 타입으로 치환하는 반면, dynamic 변수는 컴파일 시점에 타입을 결정하지 않고 해당 프로그램이 실행되는 런타임 시점에 타입을 결정한다.

var d = 5;
d = "test";  // 컴파일 에러  , d == System.Int32로 이미 결정했기에 문자열 x
d.CallFunc();  // 컴파일 에러 System.Int32 타입엔 CallFunc 메서드 없음
dynamic d = 5;
d = "test"; // d는 형식 결정되지 않았기에 다시 문자열로 초기화 가능.
d.CallFunc(); // 실행 시 d2 변수 타입으로 CallFunc 호출하기에 컴파일 시에는 에러 발생x, 실행시 에러 발생

 

dynamic d = 5;

d.CallTest(); // 정수타입에는 CallTest 메서드가 없으나 컴파일에러는 발생 x, 그러나 런타임 에러 발생함

 

 

예제 9.4 C# 컴파일러가 dynamic 예약어를 위해 자동 생성하는 코드

using System.Runtime.CompilerServices;
using Microsoft.CSharp.RuntimeBinder;
class Program
{
    public static CallSite<Action<CallSite, object>> p_Site1;

    //dynamic 예약어를 사용하기 싫으면 다음과 같이 직접 코딩해도 동작한다.
    //하지만 dynamic 예약어를 쓰는 것이 코드 가독성 측면에서 낫다.

    static void Main(string[] args)
    {
        object d = 5;

        if(p_Site1 == null)
        {
            p_Site1 = CallSite<Action<CallSite, object>>.Create(
                Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded,
                "CallTest", null, typeof(Program),
                new CSharpArgumentInfo[]
                {
                    CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
                }));
        }
        p_Site1.Target(p_Site1, d);
    }
}

#출력
Unhandled exception. Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'int' does not contain a definition for 'CallTest'
   at CallSite.Target(Closure, CallSite, Object)
   at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid1[T0] (CallSite site, T0 arg0)
   at Program.Main(String[] args) in C: \Users\asd57\source\repos\ConsoleApp5\ConsoleApp5\Program.cs:line 24

CallTest 메서드가 존재하지 않음에도 컴파일에러는 발생하진 않지만 결과적으로 System.Int32 타입의 인스턴스에 CallTest메서드가 없기에 런타임시 예외가 발생됨.

 

9.2.1 리플렉션 개선

 

string txt = "Test Func";
bool result = txt.Contains("Test");
Console.WriteLine(result);

#출력
True

Contains 메서드를 리플렉션으로 호출해보자.

using System.Reflection;

string txt = "Test Func";

Type type = txt.GetType(); // System.String
MethodInfo containsMethodInfo = type.GetMethod("Contains"); // 런타임 에러 발생.
//Contains 메서드가 string.Contains(string) 외에도 오버로드가 있기 때문이다.
//type.GetMethod("Contains")는 기본적으로 파라미터가 없는 메서드를 찾으려고 하다가 실패하거나 잘못된 오버로드를 찾을 수 있다.

if(containsMethodInfo != null)
{
    object returnValue = containsMethodInfo.Invoke(txt, new object[] { "Test" });
    bool callResult = (bool)returnValue;
    Console.WriteLine(callResult);
}

상기 코드에 대한 런타임에러를 방지하고자 하기와 같이 Contains메서드의 적절한 오버로드를 직접 지정해주었다.

using System;
using System.Reflection;

string txt = "Test Func";

Type type = txt.GetType(); // System.String

// string.Contains(string) 메서드를 정확히 지정
MethodInfo containsMethodInfo = type.GetMethod("Contains", new Type[] { typeof(string) });

if (containsMethodInfo != null)
{
    object returnValue = containsMethodInfo.Invoke(txt, new object[] { "Test" });
    bool callResult = (bool)returnValue;
    Console.WriteLine(callResult); // True
}

 

type.GetMethod를 호출 시 Contains 메서드 명이 들어갔다. 이는 dynamic 예약어로 자동생성된 예제 9.4에 포함된 Binder.InvokeMember의 "CallTest" 이름이 들어간 것과 유사하다. 

리플렉션 기술은 dynamic의 근간을 이루기에 dynamic 예약어를 리플렉션의 간편 표기법으로 여겨도 된다.

dynamic txt = "Test Func";
bool result = txt.Contains("Test");

이와 같은 특성은 확장 모듈(Plug-in)을 사용하기 쉽게 만들어준다.

기존 Assembly.LoadFrom 등으로 직접 로드한 어셈블리 안에 있는 타입의 메서드를 호출하려면 리플렉션을 이용해야만 했지만 dynamic을 사용하면 확장 모듈로부터 생성된 객체를 dynamic 변수에 담아 사전에 정의된 메서드 이름으로만 호출하면 된다.

 

더보기

1. 리플렉션(Reflection)이란?

  • 런타임에 타입의 정보(Type, Method, Property 등)를 조사하거나, 객체를 생성하고 메서드를 호출할 수 있는 기술
  • 예: 어떤 객체가 어떤 메서드를 갖고 있는지 모를 때도 이름만으로 찾아서 실행 가능
Type t = obj.GetType();
MethodInfo mi = t.GetMethod("Run");
mi.Invoke(obj, null);

2. dynamic은 리플렉션의 간편 문법이다?

yes

dynamic obj = ...;
obj.Run();  // 컴파일러는 타입 체크 안 하고, 런타임에 "Run" 메서드를 찾아 실행

이건 내부적으로 사실상 리플렉션을 이용해서 "Run"이라는 메서드를 찾아서 실행하는 것과 거의 같다. 즉, dynamic은 "컴파일 시 타입 모름 → 런타임에 알아서 찾아 호출"이라는 점에서 리플렉션과 유사한 역할을 한다.

3. 확장 모듈(Plug-in)에 유용하다는 말은?

Assembly.LoadFrom() 등으로 외부 DLL을 동적으로 로드한 경우, 보통 이렇게 해야 한다:

Assembly asm = Assembly.LoadFrom("plugin.dll");
Type type = asm.GetType("Plugin.MyClass");
object instance = Activator.CreateInstance(type);
MethodInfo method = type.GetMethod("Run");
method.Invoke(instance, null);

하지만 dynamic을 쓰면 이렇게 간단해진다:

Assembly asm = Assembly.LoadFrom("plugin.dll");
Type type = asm.GetType("Plugin.MyClass");
dynamic instance = Activator.CreateInstance(type);
instance.Run();  // 리플렉션 없이도 가능!

→ 이처럼 dynamic은 확장 모듈의 객체를 더 쉽게 사용할 수 있게 해준다. 단, 호출하려는 메서드 이름이 정확히 존재해야 하며, 컴파일 타임에는 오류를 못 잡는다는 단점이 있다.

✅ 요약

  • dynamic은 런타임 타입 바인딩을 제공해서 리플렉션 없이도 유사한 동작이 가능하다.
  • 특히 플러그인처럼 외부에서 가져온 객체를 사용할 때, 메서드 이름만 알아도 호출이 가능하니 코드가 간결해진다.
  • 단점은 오타나 존재하지 않는 메서드도 컴파일 시점에 검출되지 않는다는 점이다.

 

 

9.2.2 덕 타이핑

"덕 타이핑(duck typing)"은 객체 지향 프로그래밍에서 사용하는 개념으로, 객체의 실제 타입보다는 그 객체가 어떤 메서드나 속성을 가지고 있는지에 따라 그 객체를 사용하는 방식이.

이 개념은 다음과 같은 표현에서 유래했다:

"If it walks like a duck and quacks like a duck, it must be a duck."
(걷는 모습이 오리 같고, 우는 소리가 오리 같다면, 그것은 오리다.)

즉, 어떤 객체가 특정한 메서드나 속성을 가지고 있다면, 우리는 그 객체가 어떤 타입인지 따지지 않고 해당 기능을 사용하는 것이다.

 

둘 이상의 타입에서 '동일한 이름'으로 제공되는 속성 또는 메서드가 있을 때 여기에 접근해야 하는 코드 작성 시 다음과 같이 인터페이스나 부모 클래스를 공유하고 상속관계를 이용해 호출할 수 있게 만든다.

StrongTypeCall(new Duck());
StrongTypeCall(new Goose());
#출력
Duck Fly.
Goose Fly.

void StrongTypeCall(IBird bird)
{
    bird.Fly();
}
interface IBird
{
    void Fly();
}

class Duck : IBird
{
    public void Fly()
    {
        Console.WriteLine("Duck Fly.");
    }
}
class Goose: IBird
{
    public void Fly()
    {
        Console.WriteLine("Goose Fly.");
    }
}

 

하지만 모든 타입이 이런 식의 구조적인 호출관계를 따르지 않는다. 예로는, string 타입과 List<T> 타입은 동일하게 IndexOf 메서드를 제공하나, 두 타입은 IndexOf 메서드를 위한 기반 타입을 상속받은 것은 아니기 때문에, 공통 타입이 없다.

 

이럴 경우 dynamic을 사용하면 문제없이 처리된다.

예제 9.5 덕타이핑

string txt = "Test func";
List<int> list = new List<int> { 1, 2, 3, 4, 5 };

Console.WriteLine(DuckTypingCall(txt, "func"));
Console.WriteLine(DuckTypingCall(list, 4));
int DuckTypingCall(dynamic target, dynamic item)
{
    return target.IndexOf(item);
}

#출력
5
3

 

덕 타이핑(duck typing)의 타이핑은 '형식 적용'을 의미함.

일반적으로 객체 지향에서는 강력한 형식이 적용돼 있어 특정한 속성이나 메서드를 호출하고 싶다면, 반드시 그 형식을 기반으로 동작한다.

하지만 동적언어에서 널리 사용되는 덕 타이핑은 강력한 형식을 기반으로 하지 않고, 단지 같은 이름의 속성이나 메서드가 공통으로 제공된다면, 그것을 기능적인 관점에서 동일한 객체로 본다.

 예제 9.5 또한 string과 List<T>가 객체지향관점에서 완전 별개의 객체이지만, IndexOf 메서드 기능이 동일하게 제공된다는 점에서 같은 객체라 볼 수 있다.

 

dynamic 예약어는 동적언어에서만 가능한 덕타이핑을 정적언어인 c#에서 이용가능하게 만들었다.

 

9.2.3 동적언어와의 타입연동(중요 x)

c# 코드 환경(visualstudio)에서 python을 사용할 수 있게 IronPython 누겟 패키지 다운받아서 실습.

 

9.3 동시성 컬렉션(Concurrent Collections)

C# 4.0과 함께 배포한 닷넷 BCL에 추가된 타입 중 스레드와 연관해 알아둘 필요가 있는 내용을 다뤄보자.

 

6.6.2 절에서 System.Threading.Monitor를 이용한 공유 리소스 접근은 단 하나의 변수 대상으로 설명했다. 문제는 공유 리소스가 컬렉션인 경우다.

internal class Program
{
    static List<int> list = new();
    static void Main(String[] args)
    {
        list.AddRange(Enumerable.Range(1, 100));

        ChangeList();
        EnumerateList();
    }

    private static void EnumerateList()
    {
        foreach(var item in list)
        {
            Console.WriteLine(item);
        }
    }

    private static void ChangeList()
    {
        for(int idx = 1; idx <= 10; ++idx)
        {
            list.Add(100 + idx);
            Thread.Sleep(16);
        }
    }
}

#출력
1~110 까지 나옴.

 

단일스레드에선 문제가 안되나 멀티스레드에서 ChangeList와 EnmerateList 메서드 사용이 문제 될 수 있다.

 

internal class Program
{
    static List<int> list = new();
    static void Main(String[] args)
    {
        list.AddRange(Enumerable.Range(1, 100));
        ThreadPool.QueueUserWorkItem((arg) =>
        {
            ChangeList();
        });
        ThreadPool.QueueUserWorkItem((arg) =>
        {
            EnumerateList();
        });
        Console.ReadLine();
    }

    private static void EnumerateList()
    {
        foreach(var item in list)
        {
            Console.WriteLine(item);
        }
    }

    private static void ChangeList()
    {
        for(int idx = 1; idx <= 10; ++idx)
        {
            list.Add(100 + idx);
            Thread.Sleep(16);
        }
    }
}


//#출력 1~ 101 까지만 나오거나
/*
 * 예외 발생
Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at Program.EnumerateList() in C:\Users\asd57\source\repos\ConsoleApp5\ConsoleApp5\Program.cs:line 20
   at Program.<>c.<Main>b__1_1(Object arg) in C:\Users\asd57\source\repos\ConsoleApp5\ConsoleApp5\Program.cs:line 13
   at System.Threading.ThreadPoolWorkQueue.Dispatch()
   at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
 */

 

일반적으로 멀티(다중) 스레드가 공유 리소스에 접근하면 버그는 발생하나, 상기처럼 예외까지는 발생하지 않는다.

예제에서 사용한 List<T> 타입은 foreach로 열거하는 동안 목록의 수가 바뀌면 'Collection was modified'라는 예외를 발생시키도록 만들어졌기 때문에 나타난 현상이다.

** System.Collections와 System.Collections.Generic 네임스페이스에서 제공하는 모든 컬렉션은 foreach로 열거하는 중에 요소 수 가 변경되는 것을 허용하지 않는다.

EnumerateList 메서드가 실행 중에 changeList 메서드를 실행하는 스레드에 제어권이 넘어가서 changeList 메서드에서 list의 요소가 추가됨에 따라 다시 EnumerateList 스레드로 제어권 넘어갈 경우 foreach 대상인 list의 열거 목록 수 가 바뀜에 따라 일어난 예외다. 

 

상기 문제 해결을 위한 공유 자원인 collection에 대한 동기화 적용 코드는 다음과 같다.

internal class Program
{
    static List<int> list = new();
    static void Main(String[] args)
    {
        list.AddRange(Enumerable.Range(1, 100));
        ThreadPool.QueueUserWorkItem((arg) =>
        {
            ChangeList();
        });
        
        ThreadPool.QueueUserWorkItem((arg) =>
        {
            EnumerateList();
        });
        Console.ReadLine();
    }

    private static void EnumerateList()
    {
        lock (list)
        {
            foreach (var item in list)
            {
                Console.WriteLine(item);
            }
        }
    }

    private static void ChangeList()
    {
        for(int idx = 1; idx <= 10; ++idx)
        {
            lock (list)
            {
                list.Add(100 + idx);
                Thread.Sleep(16);
            }
        }
    }
}

 

lock을 통해 공유자원인 컬렉션을 동기화 가능하다. 예외가 발생안되나 문제는 110까지 출력이 안되는 경우가 있다. 이는 ChangeList가 작업을 끝내기 전 EnumerateList메서드가 호출되어 먼저 끝낸 후 메인스레드가 끝나는 경우가 있기 떄문이다.

이를 방지하고자 다음과 같은 코드를 통해 ChangeList가 끝날 때까지 기다린 다음 EnumerateList 메서드를 호출한다.

lock을 걸었음에도 불구하고 EnumerateList()가 리스트의 모든 값(1~110)을 출력하지 않는 이유인데, 이는 lock과는 별개로 두 쓰레드의 실행 타이밍 차이 때문이다.


🔍 핵심 요약

lock은 데이터 경쟁(race condition)을 방지하는 도구이지,
쓰레드 실행 순서를 제어하거나 동기화하지 않는다.


📌 실행 흐름 설명

상기 코드 흐름을 시간 순으로 보면:

  1. list에 1~100까지 미리 넣음.
  2. ThreadPool.QueueUserWorkItem을 통해 두 개의 작업을 큐에 등록함:
    • ChangeList: 리스트에 10개를 추가함 (Thread.Sleep(16) 포함)
    • EnumerateList: 리스트를 lock한 후 한 번 열거하여 출력함
  3. 어느 쓰레드가 먼저 실행될지는 보장되지 않음.

만약 EnumerateList가 먼저 실행되어 리스트를 lock하고 열거하면,

  • 그 시점엔 리스트에 1~100만 있음
  • 열거 후 출력하고 끝

그 뒤 ChangeList가 실행되면서 10개를 추가하더라도,

  • 이미 열거는 끝났기 때문에
  • 추가된 항목은 출력되지 않음

✅ lock이 하는 일

lock(list)은 list 객체에 대한 동기화를 보장한다:

  • 여러 쓰레드가 동시에 리스트에 접근할 때 데이터 무결성을 유지
  • 하지만 쓰레드 실행 시점이나 순서까지 조정해주진 않음

즉, EnumerateList()가 리스트를 일찍 lock하고 열거를 시작했다면,

  • 그 시점에는 아직 ChangeList()가 리스트에 아무것도 추가하지 않았기 때문에
  • 출력은 1~100까지만 되고 끝난다.

✅ 해결 방법 (타이밍 제어)

 ChangeList()가 끝난 후 EnumerateList() 호출

internal class Program
{
    static List<int> list = new();
    static void Main(String[] args)
    {
        list.AddRange(Enumerable.Range(1, 100));

        // ManualResetEvent로 동기화
        ManualResetEvent done = new(false);

        ThreadPool.QueueUserWorkItem((arg) =>
        {
            ChangeList();
            done.Set(); // 완료 신호
        });

        // ChangeList가 끝날 때까지 대기
        done.WaitOne();

        // 리스트를 출력
        EnumerateList();

        Console.ReadLine();
    }


    private static void EnumerateList()
    {
        lock (list)
        {
            foreach (var item in list)
            {
                Console.WriteLine(item);
            }
        }
    }

    private static void ChangeList()
    {
        for(int idx = 1; idx <= 10; ++idx)
        {
            lock (list)
            {
                list.Add(100 + idx);
                Thread.Sleep(16);
            }
        }
    }
}


//#출력 1~110까지 출력됨

 

개발자가 일일이 동기화 코드(lock)를 추가하는 번거로운 일을 대신해 닷넷 BCL에 System.Collections.Concurrent 네임스페이스로 묶어 네임스페이스 내부 전용 컬렉션을 제공한다.

BlockingCollection<T> Producer/Consumer 패턴에서 사용하기 좋은 컬렉션
ConcurrentBag<T> List<T>의 순서가 지켜지지 않는 동시성 버전
ConcurrentDictionary<TKey, TValue> Dictionary<TKey, TValue>의 동시성 버전
ConcurrentQueue<T> Queue<T>의 동시성 버전
ConcurrentStack<T> Stack<T>의 동시성 버전

상기 컬렉션들은 Safe-Thread(스레드에 안전)하므로 다중(멀티) 스레드 환경에서 개발자가 별도 동기화 코드를 작성할 필요 없다.

 

상기 코드에서 List<T> 사용을 ConcurrentBag<T>로 바꿈으로써 불필요한 동기화 코드를 제거하였다.

실행 또한 정상적이다.

using System.Collections.Concurrent;

internal class Program
{
    static ConcurrentBag<int> list = new();
    static void Main(String[] args)
    {
        foreach(var item in Enumerable.Range(1,100))
        {
            list.Add(item);
        }

        // ManualResetEvent로 동기화
        ManualResetEvent done = new(false);

        ThreadPool.QueueUserWorkItem((arg) =>
        {
            ChangeList();
            done.Set(); // 완료 신호
        });

        // ChangeList가 끝날 때까지 대기
        done.WaitOne();

        // 리스트를 출력
        EnumerateList();

        Console.ReadLine();
    }


    private static void EnumerateList()
    {
        foreach (var item in list)
        {
            Console.WriteLine(item);
        }
    }

    private static void ChangeList()
    {
        for (int idx = 1; idx <= 10; ++idx)
        {
            list.Add(100 + idx);
            Thread.Sleep(16);
        }
    }
}


//#출력 110->1 까지 순차적으로 출력

 

ConcurrentBag<T>는 다음과 같은 특징이 있다:

  • 내부적으로 스레드마다 별도의 스토리지(bucket) 를 사용하여 동시성을 최적화한다.
  • 입력 순서를 보장하지 않는다.
  • 열거 시에는 가장 최근에 추가된 항목이 먼저 나올 가능성이 크다 (stack-like behavior).

상기 특징 중 LIFO가 있어 110부터 출력됨.

 

 

+ Recent posts