클라이언트/서버 분리와 Dist
Minecraft의 물리적/논리적 사이드 개념과 NeoForge의 Dist enum, @OnlyIn 어노테이션, ExampleModClient 분리 패턴을 학습합니다.
클라이언트/서버 분리와 Dist
Minecraft 모딩에서 가장 흔한 크래시 원인 중 하나는 "서버에서 클라이언트 코드를 호출하는 것"입니다. NoClassDefFoundError, NullPointerException, 서버 시작 실패. 이 챕터에서는 왜 이런 일이 생기는지, 그리고 NeoForge가 어떻게 이 문제를 구조적으로 해결하는지 배웁니다.
1. 물리적 사이드 vs 논리적 사이드
"사이드(side)"라는 단어가 Minecraft 문서에서 두 가지 의미로 쓰입니다. 혼동하기 쉬우니 먼저 정리합니다.
물리적 사이드
실제로 실행되는 프로세스 기준입니다.
| 물리적 사이드 | 실행 파일 | 포함 내용 |
|---|---|---|
| 물리적 클라이언트 | minecraft.exe / java -jar minecraft.jar | 렌더링 엔진 + 게임 로직 |
| 물리적 서버 | server.jar | 게임 로직만 (렌더링 없음) |
물리적 서버 JAR에는 Minecraft 클래스 자체가 없습니다. net.minecraft.client.* 패키지 전체가 빠져 있습니다. 그래서 서버에서 Minecraft.getInstance()를 호출하면 NoClassDefFoundError가 납니다.
논리적 사이드
단일 플레이어(싱글플레이)를 생각해 보세요. 프로세스는 하나지만 내부적으로는 논리 서버와 논리 클라이언트가 동시에 돌아갑니다.
단일 플레이어 프로세스 (물리적 클라이언트)
├── 논리 클라이언트 ← 렌더링, 입력 처리
└── 논리 서버 ← 게임 로직, 엔티티 틱
멀티플레이에서는 논리 서버가 별도 프로세스(물리적 서버)로 분리됩니다.
ℹ️ 왜 구분이 중요한가?
모드 코드는 양쪽에서 로드됩니다. 물리적 클라이언트에서는 클라이언트 코드가 안전하지만, 물리적 서버에서는 클라이언트 전용 클래스가 아예 없습니다. 이 차이를 무시하면 서버 크래시가 납니다.
2. Dist enum
NeoForge는 물리적 사이드를 Dist enum으로 표현합니다.
// net.neoforged.api.distmarker.Dist
public enum Dist {
CLIENT, // 물리적 클라이언트 (minecraft.exe)
DEDICATED_SERVER // 물리적 서버 (server.jar)
}현재 실행 환경을 확인하려면:
import net.neoforged.fml.loading.FMLEnvironment;
if (FMLEnvironment.dist.isClient()) {
// 클라이언트 전용 코드
}
if (FMLEnvironment.dist.isDedicatedServer()) {
// 서버 전용 코드
}3. ExampleModClient.java 분리 패턴
NeoForge 템플릿이 제시하는 정석 패턴입니다. 클라이언트 전용 코드를 별도 클래스로 완전히 분리합니다.
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/ExampleModClient.java
package com.example.examplemod;
import net.minecraft.client.Minecraft;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.neoforged.neoforge.client.gui.ConfigurationScreen;
import net.neoforged.neoforge.client.gui.IConfigScreenFactory;
// 이 클래스는 Dedicated Server에서 로드되지 않습니다.
@Mod(value = ExampleMod.MODID, dist = Dist.CLIENT)
@EventBusSubscriber(modid = ExampleMod.MODID, value = Dist.CLIENT)
public class ExampleModClient {
public ExampleModClient(ModContainer container) {
// 설정 화면 등록 — 클라이언트 전용
container.registerExtensionPoint(IConfigScreenFactory.class, ConfigurationScreen::new);
}
@SubscribeEvent
static void onClientSetup(FMLClientSetupEvent event) {
// 클라이언트 전용 초기화
ExampleMod.LOGGER.info("HELLO FROM CLIENT SETUP");
ExampleMod.LOGGER.info("MINECRAFT NAME >> {}", Minecraft.getInstance().getUser().getName());
}
}핵심은 두 어노테이션입니다.
| 어노테이션 | 역할 |
|---|---|
@Mod(value = MODID, dist = Dist.CLIENT) | FML이 이 클래스를 클라이언트에서만 인스턴스화 |
@EventBusSubscriber(value = Dist.CLIENT) | 이벤트 핸들러를 클라이언트 이벤트 버스에만 등록 |
4. 코드 분리 기준
어떤 코드를 어디에 두어야 할지 판단 기준입니다.
클라이언트 전용 (ExampleModClient.java)
// 이 코드들은 ExampleModClient.java 안에서만 안전합니다
Minecraft.getInstance() // 클라이언트 싱글톤
BlockEntityRenderer // 블록 엔티티 렌더러 등록
EntityRenderer // 엔티티 렌더러 등록
Screen // GUI 화면 클래스
SoundEngine // 사운드 재생
KeyMapping // 키 바인딩 등록양쪽 모두 안전 (ExampleMod.java)
// 이 코드들은 ExampleMod.java에서 안전합니다
DeferredRegister // 아이템/블록 등록
IEventBus.addListener() // 공통 이벤트 등록
ServerStartingEvent // 서버 이벤트
FMLCommonSetupEvent // 공통 초기화5. 코드 흐름 다이어그램
물리적 서버에서는 ExampleModClient.java 자체가 로드되지 않습니다. @Mod(dist = Dist.CLIENT) 덕분에 FML이 서버 환경에서 이 클래스를 완전히 건너뜁니다.
6. @OnlyIn vs DistExecutor vs FMLEnvironment
세 가지 방법이 있지만 권장 순서가 다릅니다.
@OnlyIn(Dist.CLIENT) — 비권장
@OnlyIn(Dist.CLIENT)
public void clientOnlyMethod() {
// 컴파일 타임 힌트일 뿐, 런타임 보호 없음
}@OnlyIn은 Mixin 환경에서 주로 쓰이는 어노테이션입니다. 모드 코드에서는 런타임 보호를 제공하지 않으므로 사용을 피하세요.
DistExecutor — 레거시
// 구식 패턴 — 현재는 @Mod(dist=) 방식이 더 명확
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
// 클라이언트 전용 코드
});NeoForge 26에서는 @Mod(dist = Dist.CLIENT) 클래스 분리 패턴이 더 권장됩니다.
FMLEnvironment.dist — 런타임 조건 분기
if (FMLEnvironment.dist.isClient()) {
// 클라이언트 전용 코드
}간단한 조건 분기에는 쓸 수 있지만, 클라이언트 전용 클래스를 참조하는 코드는 이 방법으로도 보호되지 않습니다. 클래스 로더가 조건 분기와 무관하게 클래스를 로드하려 시도하기 때문입니다.
✅ 권장: 클라이언트 전용 코드는
ExampleModClient.java처럼 별도 클래스로 분리하고@Mod(dist = Dist.CLIENT)를 붙이세요.
7. 안티패턴: 서버에서 Minecraft.getInstance() 호출
⚠️ 서버에서 Minecraft.getInstance() 호출
// ❌ 잘못됨 — Dedicated Server에서 NoClassDefFoundError public void onServerStarting(ServerStartingEvent e) { Minecraft mc = Minecraft.getInstance(); // 서버에 Minecraft 클래스 없음 }클라이언트 전용 코드는 반드시
ExampleModClient.java(Dist.CLIENT) 안에 분리해야 합니다.ExampleMod.java의onServerStarting은 서버에서도 실행되므로 여기서Minecraft를 참조하면 크래시가 납니다.
정리
| 개념 | 핵심 |
|---|---|
| 물리적 클라이언트 | minecraft.exe — 렌더링 + 게임 로직 |
| 물리적 서버 | server.jar — 게임 로직만, net.minecraft.client.* 없음 |
Dist.CLIENT | 물리적 클라이언트 환경 |
Dist.DEDICATED_SERVER | 물리적 서버 환경 |
@Mod(dist = Dist.CLIENT) | 해당 클래스를 클라이언트에서만 로드 |
ExampleModClient.java | 클라이언트 전용 코드의 정석 분리 위치 |
다음 챕터에서는 NeoForge의 레지스트리 시스템과 DeferredRegister를 자세히 살펴봅니다.