본문 바로가기

진리는어디에/C#

[C#] AppDomain.GetAssemblies 메소드 사용하기

들어가며

서버를 만드는데 메시지를 핸들링하는 클래스를 만들때 마다 디스패칭을 해줄 수 있는 어딘가에 매번 등록하는게 귀찮았다.

'C#에는 리플렉션도 있는데 특정 클래스를 상속 받으면 해당 클래스들을 자동으로 찾아서 디스패처에 등록하는 방법은 없을까?'

있더라... GetAssemblies() 메소드를 이용해 현재 로드된 어셈블리의 목록을 가져 오고, 그 어셈블리에서 내가 필요한 타입만을 쏙쏙 뽑아 올 수 있었다.

아래는 한줄 한줄 설명하며 진행 된다. 만일 설명 필요 없고 그냥 예제 코드가 필요하다 하시는 분이나 나는 전체 예제를 봐야 윤곽이 쉽게 잡힌다 하시는 분들은 [여기]를 클릭하도록 하자.

코드 살펴 보기

먼저 베이스가 될 BaseMsgHandler 클래스가 있다고 가정하자. 우리는 이 클래스를 상속 받은 모든 클래스들을 어셈블리에서 추출 해낼 것이다. 그럼 추출 대상이 될 BaseMsgHandler 클래스를 상속 받는 ConcreteMsgHandler 클래스들도 만들자. 그리고 다른 클래스들은 추출이 되지 않는 다는 것을 보여 주기 위해 아무런 관련 없는 클래스들도 몇개 만들어 주자.

class BaseMsgHandler                        // 최상위 클래스. 이 클래스를 상속 받는 모든 클래스를 찾을 것이다.
{
}

class ConcreteMsgHandler_1 : BaseMsgHandler // 찾을 대상 클래스 1
{
}

class ConcreteMsgHandler_2 : BaseMsgHandler // 찾을 대상 클래스 2
{
}

class ConcreteMsgHandler_3 : BaseMsgHandler // 찾을 대상 클래스 3
{
}

class Other_1                               // 찾을 대상이 아닌 클래스 1
{
}

class Other_2                               // 찾을 대상이 아닌 클래스 2
{
}

클래스들이 준비 되었으므로 이제 로드 된 어셈블리에서 BaseMsgHandler 클래스를 부모 클래스로 가지는 클래스들의 목록을 뽑아 보도록하자. 가장 먼저 우리가 클래스를 작성한 어셈블리를 찾아야 한다.

AppDomain currentDomain = AppDomain.CurrentDomain; // 현재 어플리케이션 도메인
Assembly[] assems = currentDomain.GetAssemblies(); // 현재 도메인에서 로드 된 모든 어셈블리를 가져온다.

위 코드를 실행하고 assems변수에 담기는 내용을 보면 아래와 같다. 기본적으로 필요한 System 네임스페이스의 어셈블리들이 로드되고, 추가로 아래 이미지에서 노란 박스로 표시된 어셈블리가 하나 더 있다. 

예제 프로젝트 이름이 'helloworld_cs'였고 프로젝트의 이름과 동일하게 어셈블리가 만들어 진다. 우리는 이 어셈블리 안에 MsgHandler 클래스들을 작성했으므로 이 어셈블리를 가져 와야 할 필요가 있다.

IEnumerable<Assembly> currentAssembly = assems.Where(a => a.GetName().Name.Equals("helloworld_cs"));
IEnumerable<Type> childrenTypes = currentAssembly.SelectMany(s => s.GetTypes()).Where(p => typeof(BaseMsgHandler).IsAssignableFrom(p) && p.IsClass);

Linq를 이용해 어셈블리중 이름이 'helloworld_cs'인 어셈블리만을 따로 가져왔다(타입은 enumerable이지만 실제 결과는 하나만 들어 있을 것이다). 그리고 currentAssembly에서 역시 Linq를 이용해 BaseMsgHandler타입에 할당이 되며, 클래스타입인 타입들을 모두 childrenTypes에 담았다.

static void Main(string[] args)
{
    AppDomain currentDomain = AppDomain.CurrentDomain; // 현재 어플리케이션 도메인
    Assembly[] assems = currentDomain.GetAssemblies(); // 현재 어플리케이션 도메인에 로드 된 모든 어셈블리를 가져온다.

    IEnumerable<Assembly> currentAssembly = assems.Where(a => a.GetName().Name.Equals("helloworld_cs"));
    IEnumerable<Type> childrenTypes = currentAssembly.SelectMany(s => s.GetTypes()).Where(p => typeof(BaseMsgHandler).IsAssignableFrom(p) && p.IsClass);

    foreach (var type in childrenTypes)
    {
        Console.WriteLine(type.ToString());
    }
}

// OUTPUT :
//   BaseMsgHandler
//   ConcreteMsgHandler_1
//   ConcreteMsgHandler_2
//   ConcreteMsgHandler_3

코드를 실행 시키면 Other_1, 2 클래스는 리턴되지 않고 BaseMsgHandler를 상속 받은 클래스들만 출력되는 것을 확인 할 수 있다. 여기서 문제가 생긴다. 우리는 BaseMsgHandler를 상속 받은 클래스들을 찾고 싶은거지 BaseMsgHandler를 찾고 싶은 것이 아니다. 이 아래와 같이 BaseMsgHandler를 클래스에서 인터페이스로 변경하면 간단히 해결 된다. 어차피 BaseMsgHandler를 직접 사용하는 것은 막고 싶었으므로 오히려 더 잘 됐다.

/*
class BaseMsgHandler
{
}
*/
interface BaseMsgHandler
{
}

지금까지 BaseMsgHandler를 상속 받는 클래스 타입들을 찾았다면 이제 자식 클래스들의 인스턴스를 만들어 디스패처에 등록할 차례다. Activator.CreateInstance 메소드를 사용하면 타입을 이용해 객체를 생성 할 수 있다.

List<BaseMsgHandler> dispatcher = new List<BaseMsgHandler>();

BaseMsgHandler handler = Activator.CreateInstance(type) as BaseMsgHandler;
dispatcher.Add(handler);

보통 디스패쳐는 딕셔너리 구조를 많이 사용하지만 여기서는 흉내만 내기 위해 List를 사용해다. 이해 바란다. 

전체 코드

이제 앞에서 살펴본 코드 조각들을 모은 전체 코드를 보면서 마무리 하도록 하자.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

interface BaseMsgHandler                    // 최상위 클래스. 이 클래스를 상속 받는 모든 클래스를 찾을 것이다.
{
}

class ConcreteMsgHandler_1 : BaseMsgHandler // 찾을 대상 클래스 1
{
}

class ConcreteMsgHandler_2 : BaseMsgHandler // 찾을 대상 클래스 2
{
}

class ConcreteMsgHandler_3 : BaseMsgHandler // 찾을 대상 클래스 3
{
}

class Other_1                               // 찾을 대상이 아닌 클래스 1
{
}

class Other_2                               // 찾을 대상이 아닌 클래스 2
{
}


class Program
{
    static List<BaseMsgHandler> dispatcher = new List<BaseMsgHandler>();
    static void Main(string[] args)
    {
        AppDomain currentDomain = AppDomain.CurrentDomain; // 현재 어플리케이션 도메인
        Assembly[] assems = currentDomain.GetAssemblies(); // 현재 어플리케이션 도메인에 로드 된 모든 어셈블리를 가져온다.

        IEnumerable<Assembly> currentAssembly 
            = assems.Where(a => a.GetName().Name.Equals("helloworld_cs"));

        IEnumerable<Type> childrenTypes 
            = currentAssembly.SelectMany(s => s.GetTypes()).Where(p => typeof(BaseMsgHandler).IsAssignableFrom(p) && p.IsClass);

        foreach (var type in childrenTypes)
        {
            BaseMsgHandler handler = Activator.CreateInstance(type) as BaseMsgHandler;
            dispatcher.Add(handler); // 디스패처에 핸들러 등록
        }
    }
}

 

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!