생명주기와 스레드
모드 코드가 언제, 어느 스레드에서 호출되는지 정리한 문서입니다. 내용은 각 타입의 Javadoc과 Poly 모드 로더 런타임의 실제 동작을 따릅니다.
한눈에 보기
JVM 시작 ─ 메인 스레드
│
├─ 모드 탐색 · poly.mod.json 검사 · 의존성 해결 POLY-METADATA-*, POLY-DEPENDENCY-*
├─ 시스템 교체 충돌 해결 (모드 코드 실행 전) POLY-SYSTEM-001/002/004/005
├─ 변환 계획 확정 POLY-TRANSFORM-*
├─ SystemProvider.create (시스템마다 1회) POLY-SYSTEM-003
├─ common 엔트리포인트 (모든 모드, 의존성 순서) POLY-ENTRYPOINT-*
├─ client 또는 server 엔트리포인트 (의존성 순서)
├─ 게임 계층 시작 POLY-BOOTSTRAP-005
│
└─ 게임 main
├─ Minecraft 부트스트랩 뒤: 다른 로더의 모드 초기화(Fabric), 그다음
│ LifecycleEvents.MODS_INITIALIZED
├─ 레지스트리 동결 직전: 선언을 게임 객체로 만들고 등록을 닫음
│ ── 이후 선언은 POLY-*-002 ──
├─ 서버 스레드: 이벤트, 틱, 명령어, AI, 예약 작업, 서버 쪽 페이로드
├─ 클라이언트(렌더) 스레드: HUD, 화면, 그리기, 클라이언트 쪽 페이로드
├─ 월드 생성 스레드: 코드 배치 기능, density function, 구조물, 지형 생성기
└─ 네트워크 스레드: 페이로드 코덱
│
JVM 종료 ─ "Poly 모드 로더 Shutdown" 스레드
└─ onShutdown 작업, 등록 역순 POLY-SHUTDOWN-001
시작 순서
| 순서 | 실행되는 것 | 스레드 | 실패 |
|---|---|---|---|
| 1 | 모드 탐색, 메타데이터 검사, 의존성 해결, 시스템 교체 충돌 해결 | 메인 | POLY-METADATA-*, POLY-DEPENDENCY-*, POLY-SYSTEM-*. 모드 코드는 아직 실행되지 않습니다 |
| 2 | 교체할 시스템의 SystemProvider.create | 메인, 시스템마다 1회 | POLY-SYSTEM-003으로 시작 중단 |
| 3 | 모든 모드의 CommonModInitializer | 메인 | POLY-ENTRYPOINT-003으로 시작 중단 |
| 4 | 클라이언트면 모든 모드의 ClientModInitializer, 전용 서버면 DedicatedServerModInitializer | 메인 | 위와 같음 |
| 5 | 게임 계층 시작 | 메인 | POLY-BOOTSTRAP-005 |
| 6 | 게임 main | 메인 | — |
| 7 | 게임 시작 안, Minecraft 부트스트랩 뒤: 다른 로더의 모드가 자기 로더에서와 같은 자리에서 초기화되고(Fabric: main, 그다음 client 또는 server), 이어서 LifecycleEvents.MODS_INITIALIZED | 메인 | 예외를 던진 리스너는 꺼집니다(POLY-EVENT-001) |
- 3, 4단계는 각각 모든 모드를 의존성 순서로 순회합니다. 의존하는 모드가 먼저 실행되고, 순서가 같으면 모드 ID 순으로 실행합니다. 그래서 모든 common 엔트리포인트가 끝난 뒤에야 첫 client(또는 server) 엔트리포인트가 실행됩니다.
- 싱글 플레이의 내장 서버는 클라이언트 프로세스 안에서 동작하며, 이때
server엔트리포인트는 호출되지 않습니다.server엔트리포인트는 전용 서버에서만 실행됩니다. - 엔트리포인트가 실행되는 시점에는 Minecraft 레지스트리와 게임 객체가 아직 없으므로, 엔트리포인트에서는 선언만 하세요.
- 다른 로더의 모드(Fabric)는 7단계에서, 자기 로더가 부르는 자리에서 초기화됩니다. 다른 로더의 모드가 초기화된 뒤에 할 일이 있는 Poly 모드는
LifecycleEvents.MODS_INITIALIZED를 듣습니다. 모든 모드가 끝난 뒤 한 번 오고, 다른 로더의 모드가 없어도 같은 자리에서 옵니다. 자세한 내용은 호환성을 참고하세요.
context.events().register(LifecycleEvents.MODS_INITIALIZED, () -> {
// 모든 로더의 모든 모드가 초기화를 마쳤습니다.
});
엔트리포인트 종류와 poly.mod.json 키는 엔트리포인트에서 설명합니다.
게임 계층과 등록 종료
게임 계층(poly-game-v26_2)은 모든 엔트리포인트가 끝난 뒤 게임 main이 시작되기 전에 시작합니다. 시작하지 못하면 POLY-BOOTSTRAP-005와 함께 멈춥니다.
등록은 게임이 레지스트리를 동결(freeze)하기 직전에 닫힙니다.
- 엔트리포인트의 선언은 Poly 모드 로더가 그 자리에서 검증합니다. 이름이 잘못되었거나 중복이면 곧바로 예외가 발생합니다.
- 게임이 레지스트리를 동결하기 직전에, 게임 계층이 모든 선언을 실제 게임 객체로 만듭니다.
- 그 직후 등록이 닫힙니다. 이후의 선언은 모두
IllegalStateException을 일으키며, 메시지는<코드> … must be declared in a mod entrypoint; registration is closed입니다.
선언은 언제나 엔트리포인트 안에서 끝내세요. 이벤트 리스너, 예약 작업, 다른 스레드에서는 선언하지 마세요.
콜백별 스레드
| 콜백 | 스레드 | 지켜야 할 것 |
|---|---|---|
CommonModInitializer, ClientModInitializer, DedicatedServerModInitializer | 메인 | 선언만 합니다 |
SystemProvider.create | 메인, 엔트리포인트 전에 1회 | 인수 없는 public 생성자가 필요합니다 |
GameBrandSystem.brand | 아무 스레드, 여러 번 | 상태를 두지 말고 빠르게 처리합니다 |
EventRegistry.register | 아무 스레드, 아무 때나 | 스레드 안전합니다 |
서버 이벤트(ServerEvents, PlayerEvents, BlockEvents, EntityEvents, WorldEvents, InteractionEvents) | 서버 | ServerEvents.TICK은 매 틱 호출되므로 빠르게 처리합니다 |
ClientEvents | 클라이언트 | 클라이언트에서만 발생합니다. ITEM_TOOLTIP은 툴팁이 보이는 동안 매 프레임 불리므로 빠르게 처리합니다 |
BlockBehavior, ItemBehavior | 해당 월드의 스레드, 서버와 클라이언트 양쪽 | world.isClientSide()를 확인하고 서버에서만 상태를 바꿉니다 |
BlockEntityTicker, AiGoal, ProjectileBehavior | 서버 | 받은 뷰는 호출되는 동안에만 씁니다 |
CommandAction, PermissionProvider | 서버 | 권한 검사는 명령어 제안 때도 자주 호출되므로 빠르게 처리합니다 |
WorldView.schedule의 작업 | 서버, 지정한 틱 뒤 | — |
ServerPayloadHandler / ClientPayloadHandler | 서버 / 클라이언트 | — |
PayloadCodec | 네트워크 스레드 | 게임 상태에 접근하지 않습니다 |
ConfigurationExchange | 서버 쪽은 서버, 클라이언트 답은 클라이언트 | 서버 쪽은 common 엔트리포인트에서, 클라이언트 답은 client 엔트리포인트에서 정합니다 |
HudLayer, BlockEntityPainter, WorldPainter | 클라이언트(렌더), 매 프레임 | 빠르게 처리합니다. 블록 엔티티 그리기는 클라이언트 사본을 읽습니다 |
ModScreen의 모든 메서드, 화면 버튼 | 클라이언트(렌더) | — |
CodeFeature, StructureBuilder | 월드 생성 스레드, 여러 청크에서 동시에 | 상태는 지역 변수에만 둡니다. 배치 기능의 무작위 값은 FeatureContext.random()으로만 얻습니다 |
NoiseFunction | 월드 생성 스레드, 동시에 매우 자주 | 상태를 두지 말고 빠르게 처리하며, 같은 위치에는 언제나 같은 값을 돌려줍니다 |
TerrainGenerator | 월드 생성 스레드, 여러 열에서 동시에 | 스레드 안전해야 하며, 같은 위치와 시드에는 같은 열을 만듭니다 |
onDataReload, onDataFiles | 서버 | 서버를 시작할 때와 /reload 때 호출됩니다 |
onAssetReload, onAssetFiles | 클라이언트 | 클라이언트를 시작할 때와 F3+T 때 호출됩니다. 전용 서버에서는 호출되지 않습니다 |
onShutdown의 작업 | Poly 모드 로더 종료 스레드(Poly 모드 로더 Shutdown) | 아래 종료를 참고하세요 |
뷰는 넘겨받은 스레드에서, 호출 동안만
WorldView, EntityView, PlayerView 같은 뷰는 그 뷰를 넘겨준 스레드(서버 또는 클라이언트)에서, 콜백이 실행되는 동안에만 유효합니다. 필드에 저장하거나 다른 스레드로 넘기지 마세요. 월드 생성 콜백에서는 뷰 대신 전달받은 context만 사용하세요.
콜백이 예외를 던지면
| 콜백 | 결과 |
|---|---|
| 엔트리포인트 | POLY-ENTRYPOINT-003. 시작이 중단됩니다 |
SystemProvider, 시스템 검증 | POLY-SYSTEM-003. 시작이 중단됩니다 |
| 이벤트 리스너 | POLY-EVENT-001. 해당 리스너만 끄고, 다른 리스너와 게임은 계속 동작합니다. 예외를 던지거나 null을 돌려준(POLY-EVENT-003) 결정 리스너는 PASS로 칩니다 |
HudLayer, BlockEntityPainter, WorldPainter | POLY-CLIENT-003. 해당 레이어나 그리기만 끕니다 |
CodeFeature, NoiseFunction, StructureBuilder | POLY-WORLDGEN-005. 한 번만 기록하고 실패한 자리는 건너뜁니다 |
WorldView.schedule의 작업 | POLY-SCHEDULE-001. 기록하고 계속 진행합니다 |
PayloadCodec.decode | POLY-NETWORK-004. 해당 페이로드를 버립니다 |
ConfigurationExchange의 모드 코드 | POLY-NETWORK-005 |
BlockBehavior, ItemBehavior | 게임 자체 블록에서 난 예외처럼 게임으로 전달됩니다 |
| 종료 작업 | POLY-SHUTDOWN-001. 기록하고 남은 작업을 계속 실행합니다 |
너무 늦게, 또는 잘못된 쪽에서 등록하면
등록이 닫힌 뒤(예: 이벤트 리스너 안)에 선언하면 영역별 코드와 함께 IllegalStateException이 발생합니다.
| 등록 | 닫힌 뒤 오류 |
|---|---|
| 블록, 아이템, 엔티티, 투사체, 효과, 사운드, 파티클, 게임 규칙, 아이템 컴포넌트, 엔티티 데이터, 발전 과제 트리거 | POLY-CONTENT-002 |
| 명령어 | POLY-COMMAND-002 |
| 페이로드 채널, 접속 준비 교환 | POLY-NETWORK-002 |
| 배치 기능, 코드 배치 기능, density function, 구조물, 바이옴, 지형 생성기 | POLY-WORLDGEN-002 |
| 권한 노드, 권한 공급자 | POLY-PERMISSION-002 |
| 리로드 리스너 | POLY-RESOURCE-002 |
| 게임 테스트 | POLY-GAMETEST-002 |
| 키 바인딩, HUD 레이어, 블록 엔티티 그리기, 동적 텍스처, 월드 그리기, 셰이더 파이프라인 | POLY-CLIENT-002 |
onShutdown (종료가 시작된 뒤) | POLY-SHUTDOWN-002 |
서버와 클라이언트 중 잘못된 쪽에서 호출한 경우:
| 상황 | 결과 |
|---|---|
전용 서버에서 mod.client() 호출 | IllegalStateException, POLY-CLIENT-001. 클라이언트 등록은 client 엔트리포인트로 옮기세요 |
| 연결되기 전에 페이로드 전송 | IllegalStateException, POLY-NETWORK-002 |
전용 서버에서 sendToServer 호출, 또는 서버 쪽 플레이어가 아닌 대상에 sendToPlayer 호출 | IllegalStateException |
| 클라이언트에서 월드를 바꾸는 메서드 호출 | IllegalStateException |
| 클라이언트 스레드가 아닌 곳에서 동적 텍스처 업로드 | POLY-CLIENT-005 |
| 콘텐츠를 client 엔트리포인트에서만 선언 | 별도의 오류 코드는 없지만 서버와 콘텐츠 목록이 달라져 동기화가 어긋납니다. 콘텐츠는 common 엔트리포인트에서 선언하세요 |
종료
mod.onShutdown("close database", () -> database.close());
- 종료 작업은 JVM이 종료될 때 Poly 모드 로더 종료 스레드(
Poly 모드 로더 Shutdown)에서 한 번씩 실행합니다. - 모든 모드의 작업을 등록한 역순으로 실행하므로, 의존하는 모드가 의존 대상보다 먼저 정리됩니다.
- 작업은 짧게 끝내고, 새 스레드를 만들지 말고, 게임 상태를 건드리지 마세요. 게임 상태는 이미 사라졌을 수 있습니다.
- 한 작업이 예외를 던지면 모드 ID와 함께
POLY-SHUTDOWN-001로 기록하고 남은 작업은 계속 실행합니다. - 종료가 시작된 뒤에
onShutdown을 호출하면POLY-SHUTDOWN-002오류가 납니다.
서버가 멈추는 순간에 할 일은 종료 작업이 아니라 서버 스레드의 서버 이벤트에서 처리하세요. 월드를 저장하기 전이라면 ServerEvents.STOPPING, 차원마다 저장을 마친 뒤라면 WorldEvents.UNLOADED, 월드를 모두 닫은 뒤라면 ServerEvents.STOPPED입니다.