광고 자리 모집 중 · 제휴 문의

유니티(Unity) 모딩 아키텍처: BepInEx의 Mono와 IL2CPP 구동 차이, Harmony 동적 후킹 및 어셈블리 패치 분석

365Finds Editorial
Wiki 문서 11,430편 수록

내 AI에서 365 Wiki 사용하기

아래 프롬프트를 복사해 평소 사용하는 AI에 붙여넣고 바로 질문하세요.

전체 Skill 다운로드

먼저 프롬프트로 체험해 보세요. 365 Wiki를 자주 쓴다면 전체 365Wiki Skill을 설치해 매번 붙여넣는 수고를 줄일 수 있습니다. 평소 쓰는 AI가 없다면 Gemini나 Microsoft Copilot 웹 버전도 사용할 수 있습니다.

서론: 유니티(Unity) 엔진 모딩의 동작 원리

유니티(Unity) 엔진은 현대 PC 인디 및 중견 게임 생태계의 상당수를 지탱하고 있습니다. 《발헤임(Valheim)》이나 《리설 컴퍼니(Lethal Company)》 같은 협동 생존 게임부터 《시티즈: 스카이라인 2(Cities: Skylines II)》, 《다이슨 스피어 프로그램(Dyson Sphere Program)》과 같은 복잡한 시뮬레이션에 이르기까지 수많은 명작들이 유니티로 제작되었습니다. 독자적인 바이너리 파일이나 베데스다 스타일의 선언적 플러그인 데이터 구조를 사용하는 다른 게임 엔진들과 달리, 유니티 프로젝트는 .NET 및 C# 소프트웨어 아키텍처를 기반으로 구축됩니다.

유니티 게임을 모딩할 때 디스크 상의 원본 애셋 아카이브를 직접 수정하거나 덮어쓰는 방식은 거의 사용되지 않습니다. 대신 현대 유니티 모딩은 런타임 메모리 상에서의 동적 어셈블리 주입 및 메서드 가로채기(인터셉션) 기법을 핵심으로 작동합니다. 이 생태계의 중심에는 오픈 소스 모드 인젝터이자 플러그인 부트스트래퍼인 BepInEx(Bepis Injector Extensible)와 안드레아스 파르데이케(Andreas Pardeike)가 개발한 Harmony 라이브러리가 자리 잡고 있습니다.

그러나 모드 개발자와 게이머들은 게임에 따라 모드가 전혀 실행되지 않거나 충돌하는 호환성 문제를 자주 겪습니다. 그 근본 원인은 유니티의 스크립트 컴파일 구조에 있습니다. 바로 전통적인 Mono 런타임 백엔드와 사전 컴파일 방식인 IL2CPP(Intermediate Language to C++) 백엔드 사이의 근본적인 아키텍처 격차 때문입니다. BepInEx와 Harmony가 이 두 백엔드를 처리하는 내부 메커니즘을 파악해야만 충돌, 비정상 후킹, 로드 순서 오류를 정확히 진단하고 해결할 수 있습니다.

스크립팅 백엔드의 양대 산맥: Mono와 IL2CPP 구조 비교

게임 개발자가 유니티 프로젝트를 빌드하여 배포할 때, 다음 두 가지 스크립팅 백엔드 중 하나를 반드시 선택해야 합니다.

[C# 소스 코드: 게임 로직 스크립트]
               │
               ▼
[Roslyn / Unity C# 컴파일러] ---> 공통 중간 언어 (CIL 바이트코드)
               │
               ├─────────────────────────────────────────┐
               ▼                                         ▼
        [Mono 백엔드 경로]                         [IL2CPP 백엔드 경로]
- 관리형 어셈블리에 CIL 바이트코드 패키징         - IL2CPP 트랜스파일러가 CIL을 C++로 변환
- 대표 파일: Managed/Assembly-CSharp.dll      - 플랫폼 네이티브 C++ 컴파일러가 GameAssembly.dll 생성
- Mono JIT 가상 머신에 의해 런타임 실행         - 메타데이터를 global-metadata.dat 파일로 분리
- 리플렉션 완전 지원, 소스 코드 역컴파일 용이    - 순수 기계어 바이너리, 런타임 CLR/JIT 부재

Mono 스크립팅 백엔드

Mono 방식으로 빌드된 게임에서는 모든 게임플레이 규칙, AI 제어 로직, 캐릭터 상태 머신이 관리형 .NET 어셈블리(주로 Managed/Assembly-CSharp.dll)로 패키징됩니다.

IL2CPP 스크립팅 백엔드

반면 IL2CPP 방식으로 빌드된 게임은 완전히 다른 컴파일 파이프라인을 거칩니다.

BepInEx 부트스트래퍼 아키텍처

외부 코드를 실행할 공식 진입점이 없는 닫힌 유니티 바이너리에 모드를 로드하기 위해, BepInEx는 동적 링크 라이브러리(DLL) 프록시 하이재킹 기법을 사용합니다.

Doorstop / 프록시 하이재킹 체인

윈도우 운영체제는 실행 파일이 필요로 하는 동적 라이브러리를 찾을 때, 시스템 디렉터리(C:\Windows\System32)를 조회하기 전에 먼저 실행 파일이 위치한 루트 디렉터리를 우선 탐색합니다. BepInEx는 이 순서를 활용합니다.

  1. 프록시 DLL 배치: 게임 실행 파일이 있는 폴더에 윈도우 기본 시스템 라이브러리와 이름이 같은 프록시 DLL(winhttp.dll, version.dll 등)을 배치합니다.
  2. 실행 흐름 가로채기: 사용자가 게임을 실행하면 윈도우 로더는 시스템 폴더 대신 게임 폴더의 winhttp.dll을 우선적으로 메모리에 로드합니다.
  3. 런타임 환경 부트스트랩: 로드된 프록시 DLL은 Unity Doorstop을 구동하고, 유니티 엔진의 본체 로직이 초기화되기 전에 BepInEx.Preloader.dll과 BepInEx.Core.dll을 프로세스 메모리 공간에 신속하게 주입합니다.
  4. 플러그인 탐색 및 마운트: 환경 구축이 완료되면 BepInEx는 디버그 콘솔을 활성화하고 로깅 시스템을 시작한 뒤, BepInEx/plugins/ 디렉터리에 존재하는 모드 파일들을 검색하여 메모리에 마운트합니다.

BepInEx 5 대 BepInEx 6: IL2CPP 상호 운용 브리지

Harmony: 메모리 상에서의 동적 메서드 패칭 기술

BepInEx가 모드 코드를 메모리에 올려놓으면, 실제 게임 기능 변경은 Harmony 라이브러리가 담당합니다. Harmony는 디스크 상의 원본 파일을 단 1바이트도 건드리지 않고, JIT 컴파일된 메모리 상의 메서드 프롤로그 코드를 동적으로 재작성하여 작동합니다.

Harmony의 4대 핵심 패치 유형

[호출되는 원본 메서드: 예: PlayerController.TakeDamage(float amount)]
           │
           ▼
[Harmony Prefix 패치] ---> true 반환(계속 진행) 또는 false 반환(원본 스킵)
           │
           ▼
[원본 게임 메서드 실행: 체력 차감 및 판정 연산]
           │
           ▼
[Harmony Postfix 패치] ---> 반환값(__result) 검사 및 수정, 추가 로직 실행
           │
           ▼
[Harmony Finalizer 패치] ---> 예외 처리(try/finally 역할), 게임 강제 종료 방지
  1. 전위 패치 (Prefix, [HarmonyPrefix]):
    • 원본 메서드가 실행되기 직전에 가장 먼저 가로채 실행됩니다.
    • 메서드로 전달되는 인자 값을 실시간으로 읽거나 수정할 수 있습니다(예: 피격 데미지 float amount 수치 변경).
    • 실행 흐름을 완전히 차단할 수 있습니다. Prefix가 false를 반환하면 원본 게임 메서드의 실행 자체가 건너뛰어집니다.
  2. 후위 패치 (Postfix, [HarmonyPostfix]):
    • 원본 메서드의 실행이 정상적으로 끝난 직후에 실행됩니다.
    • 특수 매개변수인 ref __result를 통해 원본 메서드의 최종 반환값을 가로채거나 덮어쓸 수 있습니다.
    • 아이템 제작 완료 시 업적을 달성하게 하는 등 파생 이벤트를 추가할 때 가장 안정적인 선택지입니다.
  3. 트랜스파일러 패치 (Transpiler, [HarmonyTranspiler]):
    • 가장 정밀하고 강력한 패치 방식입니다. 대상 메서드의 CIL 바이트코드를 JIT 컴파일 전에 명령어 열(IEnumerable 형태의 CodeInstruction)로 가로챕니다.
    • 바이트코드 단계에서 특정 연산자(OpCode)를 삭제, 삽입, 치환할 수 있어 매우 미세한 로직 수정이 가능합니다.
  4. 종결자 패치 (Finalizer, [HarmonyFinalizer]):
    • 패치된 메서드를 감싸는 동적 try/finally 블록 역할을 수행합니다. 메서드 내부에서 발생한 치명적인 예외를 안전하게 포착하고 무마하여, 모드 오류로 인해 게임이 바탕화면으로 튕기는 현상을 방지합니다.

고장 진단 매트릭스: 유니티 모딩 오류 해결 가이드

유니티 모드가 정상 로드되지 않거나 실행 중 강제 종료될 경우, 아래 진단 매트릭스를 통해 아키텍처 수준의 결함을 점검할 수 있습니다.

문제 현상근본적인 원인진단 확인 방법해결 조치
콘솔 미표시 / 모드 완전 무반응프록시 DLL이 OS 로더에 의해 차단되거나 누락됨BepInEx/LogOutput.log 파일이 생성되지 않음루트의 winhttp.dll 파일 확인 및 폴더 쓰기 권한 점검
게임 실행 즉시 바탕화면 튕김 (IL2CPP)IL2CPP 게임에 구형 BepInEx 5를 설치함루트에 GameAssembly.dll이 존재하나 BepInEx 5 구동기존 BepInEx 삭제 후 BepInEx 6 x64 IL2CPP 전용 빌드 설치
상호 운용(Interop) 캐시 충돌게임이 업데이트되었으나 interop 래퍼가 미갱신됨로그에 TypeLoadException 또는 누락 메서드 에러 발생BepInEx/interop/ 폴더를 완전히 삭제하고 게임 재실행하여 재생성
Prefix 후크 충돌여러 모드가 동일 메서드에서 false를 반환함특정 모드만 동작하고 다른 모드 기능이 먹통이 됨플러그인 로드 우선순위 재조정 및 파괴적 Prefix 우회 자제
안티치트 부트스트랩 차단EAC 또는 배틀아이가 프록시 DLL 주입을 감지함게임이 즉시 종료되거나 신뢰할 수 없는 시스템 파일 경고공식 오프라인 모드로 실행하거나 개발자 전용 모딩 실행 플래그 적용

다양한 게임 엔진 전반의 모드 로드 순서 정렬, 어셈블리 충돌 디버깅 및 커스텀 C# 확장 도구 개발에 관한 심층적인 기술 분석은 365Finds의 전문 지식 베이스를 통해 확인할 수 있습니다. 메모리 상주 원리, 프록시 주입 및 비파괴적 바이트코드 패칭을 올바르게 이해하면 대규모 게임 업데이트 후에도 안정적인 모딩 환경을 유지할 수 있습니다.

참고 자료