이벤트 핸들러와 @SubscribeEvent
NeoForge 26의 이벤트 핸들러 3가지 패턴(@EventBusSubscriber, addListener, register)과 올바른 핸들러 시그니처 규칙을 설명합니다.
NeoForge의 이벤트 시스템은 모드가 게임 로직에 끼어들 수 있는 핵심 통로입니다. 플레이어가 로그인할 때, 블록이 파괴될 때, 엔티티가 스폰될 때 — 이 모든 순간에 이벤트가 발생하고, 핸들러가 그 이벤트를 받아 처리합니다.
이 챕터에서는 핸들러를 등록하는 세 가지 방법과 각각의 적합한 사용 시나리오를 다룹니다.
이벤트 버스 두 가지
핸들러를 작성하기 전에 버스 구분부터 짚고 넘어갑니다. NeoForge에는 이벤트 버스가 두 개 있습니다.
| 버스 | 접근 방법 | 용도 |
|---|---|---|
| MOD 버스 | 생성자 인자 IEventBus modEventBus | 모드 초기화 라이프사이클 (레지스트리, 설정, 클라이언트 셋업) |
| GAME 버스 | NeoForge.EVENT_BUS | 게임 플레이 이벤트 (플레이어, 블록, 엔티티, 월드) |
NeoForge 26.1.2부터 @EventBusSubscriber로 등록할 때는 핸들러가 받는 이벤트 타입을 보고 어느 버스인지 자동으로 결정됩니다 (bus 속성과 EventBusSubscriber.Bus enum은 삭제됨). 직접 등록할 때는 위 표의 접근 방법을 사용하며, 잘못된 버스에 핸들러를 등록하면 이벤트가 아예 발생하지 않습니다. 에러도 없이 조용히 무시됩니다. 버스 선택은 항상 이벤트 클래스의 Javadoc을 확인하세요.
패턴 A: @EventBusSubscriber + static 메소드 (권장)
가장 간결하고 NeoForge 공식 예제에서 가장 많이 쓰는 방식입니다. 클래스에 @EventBusSubscriber를 붙이면 NeoForge가 모드 로딩 시 자동으로 해당 클래스를 스캔해 @SubscribeEvent가 붙은 static 메소드를 등록합니다.
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/events/GameEvents.java
package com.example.examplemod.events;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
@EventBusSubscriber(modid = "examplemod")
public class GameEvents {
@SubscribeEvent
public static void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
Player player = event.getEntity();
player.sendSystemMessage(Component.literal("환영합니다, " + player.getName().getString() + "!"));
}
}이 방식의 장점:
- 별도 등록 코드가 필요 없습니다. 어노테이션만으로 완결됩니다.
- 클래스 단위로 이벤트를 묶어 관리하기 좋습니다.
- 테스트 시 클래스를 직접 참조하기 쉽습니다.
주의: @EventBusSubscriber를 붙인 클래스의 핸들러 메소드는 반드시 static이어야 합니다. 인스턴스 메소드를 쓰면 NeoForge가 등록을 거부합니다.
패턴 B: addListener로 람다 등록
모드 메인 클래스 생성자에서 직접 리스너를 추가하는 방식입니다. 람다나 메소드 참조를 쓸 수 있어 간단한 핸들러에 적합합니다.
// examplemod-template-26.1.2/src/main/java/com/example/examplemod/ExampleMod.java
package com.example.examplemod;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.common.Mod;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
@Mod("examplemod")
public class ExampleMod {
public ExampleMod(IEventBus modEventBus) {
// GAME 버스에 인스턴스 메소드 참조로 등록
NeoForge.EVENT_BUS.addListener(this::onPlayerLogin);
}
private void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
// 핸들러 로직
}
}이 방식의 장점:
- 핸들러가 인스턴스 상태에 접근해야 할 때 유용합니다.
- 등록 시점이 명확합니다 (생성자 실행 순서대로).
- 람다로 짧게 쓸 수 있습니다.
단점: 핸들러가 많아지면 생성자가 길어집니다. 이 경우 패턴 A나 C로 분리하는 게 낫습니다.
패턴 C: register(instance)로 인스턴스 등록
핸들러 클래스를 별도로 만들고 인스턴스를 버스에 등록하는 방식입니다. 패턴 A와 달리 static이 아닌 인스턴스 메소드를 쓸 수 있습니다.
// 핸들러 클래스 (static 불필요)
package com.example.examplemod.events;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Player;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.event.entity.player.PlayerEvent;
public class GameEventHandler {
@SubscribeEvent
public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) {
Player player = event.getEntity();
player.sendSystemMessage(Component.literal("환영합니다!"));
}
}// 모드 메인 클래스에서 등록
@Mod("examplemod")
public class ExampleMod {
public ExampleMod(IEventBus modEventBus) {
NeoForge.EVENT_BUS.register(new GameEventHandler());
}
}이 방식의 장점:
- 핸들러 클래스가 생성자 인자를 받을 수 있습니다 (의존성 주입 친화적).
- 인스턴스 상태를 유지하는 복잡한 핸들러에 적합합니다.
단점: 패턴 A보다 코드가 많습니다. 단순한 경우엔 과한 구조입니다.
세 패턴 비교
| 패턴 A | 패턴 B | 패턴 C | |
|---|---|---|---|
| 등록 방식 | 어노테이션 자동 | addListener 수동 | register 수동 |
| 메소드 타입 | static 필수 | 인스턴스 가능 | 인스턴스 가능 |
| 코드량 | 최소 | 중간 | 중간 |
| 추천 상황 | 대부분의 경우 | 간단한 핸들러 1~2개 | 상태 있는 핸들러 |
핸들러 시그니처 규칙
NeoForge는 핸들러 메소드 시그니처를 엄격하게 검사합니다. 규칙은 세 가지입니다.
- 반환 타입은
void— 다른 타입은 허용하지 않습니다. - 인자는 정확히 1개 — 이벤트 타입 하나만 받습니다.
- 패턴에 맞는 static/instance —
@EventBusSubscriber에서는 static,register(instance)에서는 instance.
⚠️ 잘못된 핸들러 시그니처
// ❌ 반환 타입이 void가 아님 @SubscribeEvent public static int onLogin(PlayerEvent.PlayerLoggedInEvent e) { return 0; } // ❌ 인자가 2개 @SubscribeEvent public static void onLogin(PlayerEvent.PlayerLoggedInEvent e, Player p) { } // ❌ @EventBusSubscriber 클래스에서 static 누락 @EventBusSubscriber(modid = "examplemod") public class Events { @SubscribeEvent public void onLogin(PlayerEvent.PlayerLoggedInEvent e) { } // static 빠짐 }위 경우 NeoForge가
"Method @SubscribeEvent is invalid"에러를 던지며 모드 로딩이 실패합니다.
이벤트 우선순위
같은 이벤트를 여러 핸들러가 받을 때 실행 순서를 제어할 수 있습니다. @SubscribeEvent의 priority 속성을 씁니다.
@SubscribeEvent(priority = EventPriority.HIGH)
public static void onPlayerLoginFirst(PlayerEvent.PlayerLoggedInEvent event) {
// 다른 핸들러보다 먼저 실행
}
@SubscribeEvent(priority = EventPriority.LOW)
public static void onPlayerLoginLast(PlayerEvent.PlayerLoggedInEvent event) {
// 다른 핸들러보다 나중에 실행
}우선순위 순서: HIGHEST > HIGH > NORMAL (기본값) > LOW > LOWEST
대부분의 경우 기본값(NORMAL)으로 충분합니다. 다른 모드와 상호작용이 필요한 경우에만 우선순위를 조정하세요.
취소 가능한 이벤트
일부 이벤트는 취소할 수 있습니다. Cancelable 어노테이션이 붙은 이벤트 클래스가 대상입니다.
@SubscribeEvent
public static void onBlockBreak(BlockEvent.BreakEvent event) {
// 특정 블록은 파괴 불가
if (event.getState().is(Blocks.BEDROCK)) {
event.setCanceled(true);
}
}취소 불가능한 이벤트에 setCanceled(true)를 호출하면 런타임 예외가 발생합니다. 이벤트 클래스 Javadoc에서 @Cancelable 여부를 먼저 확인하세요.
정리
- 패턴 A (
@EventBusSubscriber+ static): 대부분의 경우 이걸 씁니다. - 패턴 B (
addListener): 간단한 핸들러를 생성자에서 바로 등록할 때. - 패턴 C (
register(instance)): 상태를 가진 핸들러 클래스가 필요할 때. - 핸들러 시그니처:
void반환, 인자 1개, static/instance 규칙 준수. - 버스 선택: MOD 버스는 초기화, GAME 버스는 게임플레이.
다음 챕터에서는 자주 쓰는 게임 이벤트들을 실제로 다뤄봅니다.