@Mod 클래스와 생성자 패턴
NeoForge 26의 @Mod 어노테이션과 생성자 시그니처(IEventBus, ModContainer)를 이해하고 mod-id 매칭, 이벤트 버스 등록 방법을 학습합니다.
@Mod 클래스와 생성자 패턴
NeoForge 모드의 진입점은 딱 하나입니다. @Mod 어노테이션이 붙은 클래스, 그리고 그 생성자. 이 챕터에서는 ExampleMod.java를 한 줄씩 읽으면서 NeoForge 26이 모드를 어떻게 로드하는지 파악합니다.
1. @Mod 어노테이션
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/ExampleMod.java, @Mod 어노테이션
@Mod(ExampleMod.MODID)
public class ExampleMod {
public static final String MODID = "examplemod";@Mod 어노테이션 하나가 FML(Forge Mod Loader)에게 "이 클래스가 모드 진입점"이라고 알립니다. 괄호 안의 문자열이 mod-id입니다.
mod-id는 세 곳에서 반드시 일치해야 합니다.
| 위치 | 값 |
|---|---|
@Mod("examplemod") | examplemod |
neoforge.mods.toml의 modId | ${mod_id} → 빌드 시 examplemod로 치환 |
DeferredRegister.createItems(MODID) | MODID 상수 참조 |
⚠️ 잘못된 modId 매칭
@Mod("WrongId")와mods.toml의modId = "examplemod"가 불일치하면:Caused by: ModLoadingException: Mod class @Mod annotation does not match modId둘은 반드시 정확히 같은 문자열이어야 합니다.
2. 정적 필드 — DeferredRegister
생성자 위에 선언된 정적 필드들을 먼저 봅니다.
// BLOCKS / ITEMS / CREATIVE_MODE_TABS — 정적 DeferredRegister 선언
public static final DeferredRegister.Blocks BLOCKS =
DeferredRegister.createBlocks(MODID);
public static final DeferredRegister.Items ITEMS =
DeferredRegister.createItems(MODID);
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);DeferredRegister는 "나중에 등록할 것들을 모아두는 바구니"입니다. 클래스가 로드될 때 정적 초기화로 생성되지만, 실제 레지스트리 등록은 생성자에서 register(modEventBus)를 호출할 때까지 일어나지 않습니다.
// EXAMPLE_BLOCK / EXAMPLE_BLOCK_ITEM / EXAMPLE_ITEM — 실제 게임 오브젝트 등록 (지연 방식)
public static final DeferredBlock<Block> EXAMPLE_BLOCK =
BLOCKS.registerSimpleBlock("example_block", p -> p.mapColor(MapColor.STONE));
public static final DeferredItem<BlockItem> EXAMPLE_BLOCK_ITEM =
ITEMS.registerSimpleBlockItem("example_block", EXAMPLE_BLOCK);
public static final DeferredItem<Item> EXAMPLE_ITEM =
ITEMS.registerSimpleItem("example_item", p -> p.food(
new FoodProperties.Builder().alwaysEdible().nutrition(1).saturationModifier(2f).build()));registerSimpleBlock, registerSimpleItem은 팩토리 람다를 받아 DeferredHolder를 반환합니다. 아직 실제 객체가 만들어진 게 아닙니다. 레지스트리 이벤트가 발생할 때 비로소 생성됩니다.
3. 생성자 시그니처 (NeoForge 26)
// NeoForge 26 생성자 시그니처
public ExampleMod(IEventBus modEventBus, ModContainer modContainer) {NeoForge 26에서 @Mod 클래스 생성자는 두 파라미터를 받을 수 있습니다. FML이 자동으로 주입합니다.
IEventBus modEventBus
Mod 이벤트 버스입니다. 모드 라이프사이클 이벤트를 처리합니다.
FMLCommonSetupEvent— 양쪽(서버/클라이언트) 공통 초기화FMLClientSetupEvent— 클라이언트 전용 초기화RegisterEvent— 레지스트리 등록 (DeferredRegister가 내부적으로 사용)BuildCreativeModeTabContentsEvent— 크리에이티브 탭 콘텐츠 구성
ModContainer modContainer
모드의 메타데이터 컨테이너입니다.
modContainer.getModId()— mod-id 문자열modContainer.getModInfo().getVersion()— 버전 정보modContainer.registerConfig(...)— 설정 파일 등록
4. 생성자 내부 — 한 줄씩
// commonSetup 이벤트 리스너 등록
modEventBus.addListener(this::commonSetup);addListener로 메서드 참조를 넘기면 해당 이벤트가 발생할 때 호출됩니다. 람다나 메서드 참조 모두 가능합니다.
// DeferredRegister를 mod 이벤트 버스에 연결
BLOCKS.register(modEventBus);
ITEMS.register(modEventBus);
CREATIVE_MODE_TABS.register(modEventBus);DeferredRegister.register(modEventBus)가 핵심입니다. 이 호출이 없으면 바구니에 담아둔 블록/아이템이 실제 레지스트리에 등록되지 않습니다.
// 게임 이벤트 버스에 this 등록
NeoForge.EVENT_BUS.register(this);NeoForge.EVENT_BUS는 게임 이벤트 버스입니다. modEventBus와 다릅니다. 서버 시작, 플레이어 로그인 같은 런타임 이벤트를 처리합니다. @SubscribeEvent가 붙은 메서드가 이 클래스에 있을 때만 필요합니다.
// 크리에이티브 탭 이벤트 리스너
modEventBus.addListener(this::addCreative);
// 설정 파일 등록
modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC);5. 생성자 안에서 할 일 / 하지 말 것
해야 할 것
// 올바른 패턴
public ExampleMod(IEventBus modEventBus, ModContainer modContainer) {
BLOCKS.register(modEventBus); // DeferredRegister 연결
ITEMS.register(modEventBus);
modEventBus.addListener(this::setup); // 이벤트 리스너 등록
modContainer.registerConfig(...); // 설정 등록
}하지 말 것
// 잘못된 패턴 — 정적 초기화에서 직접 등록
static {
// BuiltInRegistries.BLOCK.register(...) — 절대 금지
// 레지스트리가 아직 준비되지 않은 시점에 접근하면 크래시
}
// 잘못된 패턴 — 생성자에서 무거운 I/O
public ExampleMod(IEventBus modEventBus, ModContainer modContainer) {
Files.readAllBytes(Path.of("huge-data.bin")); // 로딩 시간 증가
}| 패턴 | 이유 |
|---|---|
BuiltInRegistries 직접 접근 (정적 초기화) | 레지스트리 준비 전 접근 → 크래시 |
| 생성자에서 무거운 I/O | 모드 로딩 시간 증가, 다른 모드 로딩 블로킹 |
@SubscribeEvent 없이 NeoForge.EVENT_BUS.register(this) | 불필요한 이벤트 버스 등록 (주석 참고) |
6. @SubscribeEvent 패턴
// onServerStarting — 게임 이벤트 버스 구독
@SubscribeEvent
public void onServerStarting(ServerStartingEvent event) {
LOGGER.info("HELLO from server starting");
}NeoForge.EVENT_BUS.register(this)를 생성자에서 호출했기 때문에 이 메서드가 ServerStartingEvent 발생 시 자동으로 호출됩니다. @SubscribeEvent가 붙은 메서드가 없다면 register(this) 호출 자체가 불필요합니다.
⚠️ 구식 패턴 주의
NeoForge 26 이전 버전에서는
@Mod.EventBusSubscriber어노테이션을 클래스에 붙이는 방식도 있었습니다. 현재 권장 방식은 생성자에서modEventBus.addListener(...)또는NeoForge.EVENT_BUS.register(this)를 명시적으로 호출하는 것입니다.
7. 두 이벤트 버스 정리
modEventBus (IEventBus) | NeoForge.EVENT_BUS | |
|---|---|---|
| 용도 | 모드 라이프사이클 | 게임 런타임 이벤트 |
| 예시 | FMLCommonSetupEvent, RegisterEvent | ServerStartingEvent, PlayerEvent |
| 등록 방법 | modEventBus.addListener(...) | NeoForge.EVENT_BUS.register(this) |
| 주입 시점 | 생성자 파라미터로 자동 주입 | 정적 필드로 접근 |
다음 챕터에서는 이 두 이벤트 버스의 차이를 더 깊이 파고듭니다. 어떤 이벤트가 어느 버스에 속하는지, 잘못 등록하면 어떤 일이 생기는지 살펴봅니다.
요약
@Mod("examplemod")의 문자열은neoforge.mods.toml의modId와 정확히 일치해야 합니다.- NeoForge 26 생성자는
IEventBus와ModContainer를 파라미터로 받습니다. FML이 자동 주입합니다. DeferredRegister.register(modEventBus)호출이 없으면 블록/아이템이 등록되지 않습니다.modEventBus는 라이프사이클 이벤트,NeoForge.EVENT_BUS는 런타임 이벤트입니다.