핵심 요약
왜 이 문서가 필요한가? .NET Core 환경에서 AppDomain이 제거되면서 기존의 코드 격리 방식이 사라졌습니다. 보안이 필수적인 엔터프라이즈 환경에서 신뢰할 수 없는 코드를 안전하게 실행해야 하는 상황이 매우 많은데, 이를 위한 새로운 솔루션이 바로 Isolator 프레임워크입니다. 플러그인 아키텍처, 동적 코드 실행, 멀티테넌트 애플리케이션 등에서 실제로 필요한 기술입니다.
상세 요약
1. .NET에서의 코드 격리 문제
1.1 AppDomain의 제거와 그 이유
- .NET Framework에서의 AppDomain: 이전 .NET Framework 환경에서는 Custom AppDomain을 통해 코드를 격리된 환경에서 실행할 수 있었습니다.
- 제거된 이유: .NET Core 도입 이후 AppDomain은 공식적으로 제거되었습니다. 그 이유는 무거운 리소스 사용과 복잡한 유지보수였습니다.
- 현재 상황: 현재도
AppDomain클래스와AppDomain.CurrentDomain속성이 존재하지만,AppDomain.CreateDomain메서드는 모든 플랫폼에서 예외를 발생시킵니다.
1.2 기존 AppDomain의 장점
AppDomain이 훌륭했던 이유는 다음과 같습니다:
- 세분화된 권한 관리: 새로운 AppDomain을 생성할 때 커스텀 권한을 설정할 수 있었습니다.
- 동적 로드 및 언로드: 코드를 동적으로 로드한 후 사용이 끝나면 언로드할 수 있었습니다.
- 격리된 메모리 공간: 각 AppDomain은 독립된 실행 환경을 제공했습니다.
2. 현재 가능한 대안들
2.1 프로세스 기반 격리 (Process Isolation)
- 방식: 새로운 프로세스를 생성하여 코드를 실행합니다.
- 장점: 완전한 격리, 시스템 리소스 수준의 분리
- 단점:
- 프로세스 간 통신(IPC)의 복잡성
- 성능 오버헤드가 큼
- 권한 세분화가 제한적
2.2 Assembly Load Context (ALC)
- 방식: 권장되는 방식으로, 어셈블리를 특정 컨텍스트에 로드합니다.
- 장점:
- 상대적으로 가볍고 효율적
- .NET의 표준 방식
- 단점: 권한을 세분화하기 어려움
3. Isolator 프레임워크 소개
Isolator는 위의 두 대안을 모두 구현한 통합 프레임워크로, 플러그인 기반의 코드 격리를 제공합니다.
3.1 핵심 인터페이스
IPlugin 인터페이스
public interface IPlugin
{
object? Execute(IsolationContext context);
}
- 모든 플러그인이 구현해야 하는 기본 인터페이스입니다.
Execute메서드에서 실제 작업을 수행합니다.
IIsolationHost 인터페이스
public interface IIsolationHost : IDisposable
{
Task<PluginExecutionResult> ExecutePluginAsync<TPlugin>(
TPlugin plugin,
IsolationContext context,
CancellationToken cancellationToken = default)
where TPlugin : IPlugin, new();
}
- 플러그인을 실행하는 호스트 역할을 합니다.
IDisposable구현: 리소스 정리를 위해 반드시 using 블록 또는 using 문으로 감싸야 합니다.- 비동기 실행:
ExecutePluginAsync메서드로 비동기 처리를 지원합니다. - 제네릭 매개변수: 구체적인 플러그인 클래스를 제네릭 타입으로 지정해야 합니다.
PluginExecutionResult 레코드
public record PluginExecutionResult(
string StandardOutput,
string StandardError,
object? Result);
- StandardOutput: 플러그인이 출력한 표준 출력 메시지
- StandardError: 플러그인이 출력한 표준 오류 메시지
- Result: 플러그인 실행 결과값
IsolationContext 클래스
public class IsolationContext
{
public Dictionary<string, object> Properties { get; set; } = [];
public string[] Arguments { get; set; } = [];
}
- Properties: 키-값 쌍으로 임의의 데이터를 전달합니다.
- Arguments: 문자열 배열로 추가 인자를 전달합니다.
- 중요: Properties에서 수정한 내용은 호출자에게 반환됩니다.
- Arguments와 Properties는 선택적으로 사용 가능하며, 프로세스 명령줄 인자와는 무관합니다.
3.2 플러그인 구현 예제
public class HelloWorldPlugin : IPlugin
{
public object? Execute(IsolationContext ctx)
{
System.Console.WriteLine(ctx.Properties["Greeting"]);
System.Console.WriteLine(string.Join(", ", ctx.Arguments));
ctx.Properties["ExecutedAt"] = DateTime.UtcNow;
return DateTime.UtcNow;
}
}
이 예제에서 보여주는 패턴:
- 표준 출력 사용:
System.Console.WriteLine으로 텍스트 출력 - Properties에서 값 읽기:
ctx.Properties["Greeting"]- 호스트에서 전달한 값 접근 - Arguments 배열 접근: 전달된 인자들을 순회
- Properties에 값 쓰기:
ctx.Properties["ExecutedAt"]- 호스트에게 반환될 값 설정
4. 구현 방식
Isolator는 두 가지 구체적인 격리 호스트 구현을 제공합니다:
4.1 ProcessIsolationHost - 프로세스 기반 격리
기본 사용
using var host = new ProcessIsolationHost();
var plugin = new HelloWorldPlugin();
var context = new IsolationContext
{
Properties = new Dictionary<string, object>
{
["Greeting"] = "Hello, World!"
},
Arguments = ["This", "is", "a", "test"]
};
var result = await host.ExecutePluginAsync(plugin, context);
// CancellationToken 매개변수는 생략 가능
보안 옵션을 포함한 사용
using var host = new ProcessIsolationHost(
userName: "Joe",
password: "Black",
domain: "Movies",
loadUserProfile: true);
이 매개변수들은 ProcessStartInfo에 전달되며:
- userName: 프로세스를 실행할 사용자 계정
- password: 해당 사용자의 비밀번호
- domain: 사용자가 속한 도메인
- loadUserProfile: 사용자 프로필 로드 여부
- 주의: 이 기능은 Windows에서만 작동합니다.
이를 통해 제한된 권한을 가진 계정에서 코드를 실행할 수 있으므로, 신뢰할 수 없는 코드 실행 시 매우 유용합니다.
4.2 AssemblyLoadContextIsolationHost - 어셈블리 로드 컨텍스트 기반 격리
using var host = new AssemblyLoadContextIsolationHost();
- 현재 상태: 아직 추가 매개변수가 없습니다.
- 특징: 프로세스보다 가볍고 빠른 격리 방식입니다.
4.3 두 구현의 공통점
- 모두 **코드 생성(Code Generation)**에 의존합니다.
ExecutePluginAsync메서드의 구현 방식은 각 구현체에서 자유롭게 결정할 수 있습니다.
5. 직렬화 요구사항
매우 중요: Isolator 사용 시 다음 요소들이 모두 System.Text.Json으로 직렬화 가능해야 합니다:
- IPlugin 구현 클래스의 모든 매개변수
- IsolationContext의 Properties 딕셔너리에 저장된 모든 값
- Execute 메서드에서 반환되는 결과값
이러한 제약이 있는 이유는 호스트와 플러그인 간의 데이터 전달을 위해 직렬화가 필수적이기 때문입니다. 향후에는 직렬화 방식을 플러그인화하여 확장할 계획이 있습니다.
6. 구현자를 위한 주의사항
6.1 비동기 처리 자유도
- 각 구현체의
ExecutePluginAsync메서드는 실제로 비동기일 필요가 없습니다. - 구현 전략은 자유롭게 선택할 수 있습니다.
6.2 리소스 정리
- Dispose 메서드: 모든 IIsolationHost 구현은 IDisposable을 구현합니다.
- 매번 using 블록으로 감싸서 리소스가 제대로 정리되도록 해야 합니다.
7. 현재 상태 및 로드맵
7.1 현재 상태
- Proof of Concept (PoC): 많은 기능을 수행할 수 있지만, 완전하지는 않습니다.
- 공개된 이유: 피드백을 받고 커뮤니티의 관심도를 파악하기 위함입니다.
7.2 향후 계획 로드맵
Isolator 개발자가 고려 중인 기능들:
- 더 많은 보안 옵션: 두 구현 모두에 대해 추가 보안 기능 추가
- 직렬화 플러그인화: System.Text.Json 외의 직렬화 방식 지원
- WebAssembly 구현: WebAssembly 샌드박스를 활용한 격리 (DotNetIsolator 활용 가능)
- 더 많은 옵션 및 검사: 추가 구성 옵션과 유효성 검사
8. 대안 프로젝트
8.1 DotNetIsolator
- 개발자: Steve Sanderson (Microsoft, Knockout.js, Blazor 창시자)
- 방식: WebAssembly 샌드박스 사용
- 특징: 플랫폼 독립적, 보안 강화
- GitHub: GitHub - SteveSandersonMS/DotNetIsolator: A library for running isolated .NET runtimes inside .NET
8.2 AppDomainAlternative
- 개발자: Cy Scott
- 방식: .NET Remoting 유사 방식 사용
- 특징: 분산 실행 환경 구성 가능
- GitHub: GitHub - CyAScott/AppDomainAlternative: A .Net Core AppDomain isolation alternative.
9. 실무 적용 시나리오
플러그인 아키텍처가 필요한 경우:
- 사용자 정의 비즈니스 로직을 런타임에 로드 및 실행
- 플러그인이 메인 애플리케이션을 방해하지 않도록 격리
멀티테넌트 애플리케이션:
- 각 테넌트의 커스텀 코드를 독립된 환경에서 실행
- 한 테넌트의 오류가 다른 테넌트에 영향을 주지 않도록 보호
동적 코드 실행:
- 컴파일되지 않은 코드를 런타임에 실행
- 신뢰할 수 없는 제3자 코드의 안전한 실행
보안이 중요한 상황:
- 제한된 권한으로 코드 실행 (ProcessIsolationHost 활용)
- 코드 실행의 영향 범위를 제한
10. 리소스 및 참고 링크
GitHub 저장소
NuGet 패키지
블로그 원문
관련 .NET 개념
- AppDomain: Using Application Domains - .NET Framework | Microsoft Learn
- AssemblyLoadContext: About AssemblyLoadContext - .NET | Microsoft Learn
- IDisposable 패턴: Implement a Dispose method - .NET | Microsoft Learn
- System.Text.Json: How to serialize JSON in C# - .NET | Microsoft Learn
11. 핵심 정리
Isolator 프레임워크는 AppDomain이 제거된 .NET Core 환경에서 안전한 코드 격리를 구현하기 위한 솔루션입니다. 프로세스 기반 격리와 AssemblyLoadContext 기반 격리 두 가지 방식을 제공하여 다양한 보안 요구사항에 대응할 수 있습니다. 플러그인 아키텍처나 멀티테넌트 애플리케이션에서 신뢰할 수 없는 코드를 안전하게 실행해야 할 때 특히 유용하며, 현재는 PoC 단계이지만 지속적인 개발이 진행 중입니다.