본론: C# + PythonNet으로 KoNLPy 호출해서 결과 얻어오기
이제 본론입니다. C#으로 PythonNet을 활용하여 KoNLPy와 상호작용하는 방법을 코드로 살펴보겠습니다.
우선 PythonNet을 초기화해야 하는데, 초기화에 앞서 Python DLL의 위치를 지정해주어야 합니다. Python DLL의 경로를 지정하는 방법은 여럿 있지만, 여기서는 Runtime.PythonDLL
정적 프로퍼티에 경로를 지정하는 방법을 사용해보겠습니다.
try { Runtime.PythonDLL = @"C:\Users\rkttu\Downloads\python-3.13.2-embed-amd64\python313.dll"; }
catch (InvalidOperationException) { Console.Error.WriteLine($"Cannot change python DLL in the current process. (Current: {Runtime.PythonDLL})"); }
string javaHome = @"C:\Users\rkttu\Downloads\microsoft-jdk-21.0.6-windows-x64\jdk-21.0.6+7";
여기서 InvalidOperationException
에 대한 예외 처리를 넣은 이유는, 보통은 발생하지 않는 상황이나 이미 PythonNet을 초기화한 후에 경로 설정을 다시 진행하게 되는 경우에 대한 예외 처리입니다. 저는 이 샘플 코드를 LINQPad에서 테스트했기 때문에 이런 예외 처리를 넣었지만, 필요에 따라 생략할 수도 있으니 참고하시면 좋겠습니다.
그리고 PythonEngine.Initialize()
정적 메서드를 호출해줍니다.
if (!PythonEngine.IsInitialized)
PythonEngine.Initialize();
else
Console.Error.WriteLine("Python engine already initialized.");
이렇게 초기화를 마친 후, Python 인터프리터를 시작해야 합니다. 여기서 중요한 것이 하나 있는데, Python, 정확히는 여기서 사용하는 CPython은 파이썬 실행 환경을 여러 스레드가 동시에 액세스할 수 없도록 보호하는 장치인 Global Interpreter Lock을 두고 있습니다. (자세한 내용은 python 동시성 관리 (2) - GIL(Global Interpreter Lock) 아티클과 같이 GIL에 대한 내용을 소개하는 아티클을 읽으시면 좋습니다.)
PythonNet에서도 이 부분을 반영하고 있습니다. 아래 코드처럼 using (Py.GIL()) { ... }
코드 블록을 사용하면, Python 실행 환경 전체에 Lock을 거는 것과 비슷한 효과가 나타나게 됩니다.
이어서, 파이썬 런타임에서 실행 구역을 나누어 반복적으로 인터프리터를 재사용할 수 있도록 하기 위하여 스코프를 만들어 이 안에서 Python 작업을 수행할 목적으로 Py.CreateScope()
API를 이용하여 분리된 스코프를 형성합니다.
이렇게 모든 환경이 준비가 되면, konlpy.tag
모듈의 Kkma
를 사용하기 위해 Python 프로세스 측의 환경 변수에 JAVA_HOME
환경 변수를 설정해줍니다. 그 다음은 한국어 형태소 분석기를 이용하여 문장 단위 분리를 실행하고, 결과를 string[]
으로 마샬링해서 가져오는 것까지가 하나의 동작이 됩니다.
using (Py.GIL())
{
using (var scope = Py.CreateScope())
{
// import os
var osModule = scope.Import("os");
// os.environ['JAVA_HOME'] = '...'
var environ = osModule.GetAttr("environ");
environ.SetItem("JAVA_HOME", javaHome.ToPython());
// import Kkma from knlpy.tag
var tagModule = scope.Import("konlpy.tag");
var kkmaType = tagModule.GetAttr("Kkma");
// kkma = Kkma()
var kkma = kkmaType.Invoke();
// result = kkma.sentences(u'...')
var result = kkma.InvokeMethod("sentences", "네, 안녕하세요. 반갑습니다.".ToPython()).As<string[]>();
string.Join('/', result);
}
}
마지막으로 엔진을 종료하고 상호 운용 세션을 종료하기 위해 PythonEngine.Shutdown()
API를 호출해야 합니다. 그런데 PythonNet 내부에서는 .NET Runtime Serialization을 사용하는 부분이 있다보니, .NET 8 이후로는 이런 동작에 의존하는 API를 호출할 경우 PlatformNotSupportedException
을 발생시킵니다.
이 부분에 대한 예외 처리를 위해, CSPROJ를 편집하여 EnableUnsafeBinaryFormatterSerialization 태그를 추가하고 값을 true로 지정해야 Shutdown 호출 시 오류가 발생하지 않습니다.
try { PythonEngine.Shutdown(); }
catch { }