C#으로 KoNLPy 활용하기 (feat. Embeddable Python)

시작하기

요즈음 생성형 AI와 검색 증강 생성 (RAG)에 대한 관심이 높다보니, 자연스레 한국어 처리의 필요성이 많이 거론되는 것 같습니다.

그간 오랜 시간동안 꾸준히 한글/한국어 자연어 처리에 대한 연구와 개발이 학계는 물론 커뮤니티의 주도로 이루어져 왔고, 이제는 상당한 성숙도를 갖추게 되었습니다. 잘 알려진 산출물이 많지만, 그 중에서도 많은 분들이 찾아보는 라이브러리로 KoNLPy (파이썬 한국어 NLP) 라이브러리가 대표적일 것입니다.

C#은 전통적으로 상호 운용성에 대한 개발과 투자를 많이 해왔던 언어로, 파이썬과 같이 닷넷 런타임 밖의 매우 다른 환경과의 상호 운용성 역시 충실히 지원합니다.

이 아티클에서는 최신 파이썬 기술을 C#에 직접 연동할 수 있는 방법을 KoNLPy를 호출하는 방법을 살펴보려 합니다.

3개의 좋아요

Embeddable Python이란?

기본적으로 Python은 시스템에 설치해서 사용하는 패키지 형태로 널리 배포되고 있습니다. 원할 때 언제든 .py 확장자를 가진 스크립트를 복잡한 명령줄이나 실행 절차 없이 바로 실행할 수 있도록 지원하기 위함이지요.

이렇게 설치되는 Python 실행 환경은 보통 프로그램이 기준이 아니라 사용자를 기준으로 환경을 만들어가기 마련입니다. 그러나 Python 기술을 사용하려는 애플리케이션의 입장에서는 사용자가 힘들게 구축해 놓은 환경을 임의로 바꿀 수 없는 경우가 많습니다. 반대로, 사용자는 Python에 의존하는 애플리케이션의 구성을 맞추기 위해 Virtual Environment를 따로 구축해서 연결해주는 수고스러움을 들여야 하는 불편함도 있습니다.

그래서 애플리케이션 입장에서 확실히 격리된 Python 실행 환경을 보장하고, 문제가 발생했을 때 언제든 재설치를 통해 복구를 시도할 수 있는 수단이 필요한데, Python 공식 홈페이지에서는 이를 고려하여 Embeddable Package 버전의 Python을 따로 제공합니다. 애플리케이션은 이렇게 배포되는 패키지를 손쉽게 활용할 수 있습니다.

이렇게 분리된 독립적인 Python 환경은 사용자 기준이 아닌 애플리케이션 기준으로 따로 관리되므로 시스템/사용자 단위의 Python 환경에 영향을 전혀 끼치지 않습니다.

3개의 좋아요

Embeddable Python 다운로드

노트: Embeddable Python은 현재 Windows용으로만 제공되며, Linux의 경우에는 따로 Embeddable Python 패키지 없이 일반 패키지를 임베딩하는 형태로 쓸 수 있고, macOS의 경우에는 유니버설 인스톨러만 제공하기 때문에 이 아티클의 내용을 적용하는데 제한이 있습니다.

Windows 버전 다운로드 페이지에 가면, 표준 인스톨러 대신 Embeddable Python 패키지를 다운로드받을 수 있습니다. ZIP 파일의 압축을 해제한 다음, python3xx._pth 파일을 찾아, 파일 제일 끝에 다음 줄을 추가해줍니다.

import site

3개의 좋아요

PIP 설치하기

이제 필요한 소프트웨어 패키지를 설치할 수 있는 환경을 조성하기 위해 PIP 인스톨러를 설치해야 합니다.

Embeddable Python의 압축을 해제한 위치로 명령 프롬프트를 열고, get-pip.py 스크립트를 다운로드합니다.

curl.exe -L https://bootstrap.pypa.io/get-pip.py -o get-pip.py

그 다음, python get-pip.py 명령어를 실행하여 PIP를 Embeddable Python 패키지를 설치합니다.

설치가 잘 되었는지 확인하기 위해 이어서 python -m pip --version 명령어를 실행합니다. 버전 정보와 함께 모듈이 로드된 위치가 Embeddable Python 측의 경로로 나타나면 제대로 설치가 된 것입니다.

2개의 좋아요

KoNLPy 설치 + Microsoft Build of OpenJDK 준비하기

python -m pip install konlpy 명령어를 실행하면 KoNLPy 실행에 필요한 모든 종속성과 함께 패키지 설치가 자동으로 진행됩니다.

그리고 KoNLPy를 사용하기 위해서 준비해야 할 것이 하나 더 있는데, 바로 JDK입니다. KoNLPy 안에는 여러 한국어 형태소 분석기가 있고, 그 중에서 가장 많이 쓰이는 것이 서울대학교 지능형 데이터 시스템 (IDS) 연구실에서 개발한 꼬꼬마 한국어 형태로 분석기로, Java로 개발되어있습니다. 그래서 꼬꼬마 형태소 분석기 사용을 전제로 KoNLPy 설치 가이드에서 JDK 설치까지 다루고 있습니다.

하지만 JDK 역시 Embeddable Python을 따로 설치한 것처럼 분리된 환경에서 관리할 수 있다면 좋겠죠?

그래서 JDK 역시 ZIP 패키지 형태로 되어있는 버전을 다운로드받아 별도 디렉터리에 압축을 풀어두려 합니다. 한 가지 좋은 부분은, Python 패키지 안에 꼬꼬마 형태소 분석기 실행에 필요한 JAR 패키지가 내장되어있고, JDK만 정확히 찾으면 Python 코드 측에서 상호 운용에 관련된 동작을 구현하고 있어 특별히 다룰 것이 없다는 점입니다.

여기서는 Microsoft가 빌드하고 공급하는 OpenJDK 최신 버전을 사용해보려 합니다. Download the Microsoft Build of OpenJDK 에서 다운로드하실 수 있습니다.

적절한 위치에 JDK 패키지를 압축 해제하고 경로만 메모해둔 상태로 다음으로 넘어갑니다.

1개의 좋아요

본론: 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 { }
1개의 좋아요

마무리

이렇게 해서 Embeddable Python과 PythonNet을 이용하여 한국어 자연어 처리 프레임워크인 KoNLPy를 C#을 이용하여 상호운용하는 예시를 살펴보았습니다.

여기서 살펴본 내용은 C#에서 Python 코드를 호출하는 관점으로 살펴보았지만, 반대로 Python에서 .NET Framework나 .NET Core를 호출하는 시나리오도 PythonNet에서 지원하고 있으니 살펴보시면 유용할 것 같습니다.

4개의 좋아요