[Paper 플러그인 실전 제작기 #04] config.yml로 기능 on/off + 메시지 커스터마이징

2026. 2. 25. 21:44Java/마인크래프트

안녕하세요.

1편에서 개발환경 세팅과 첫 플러그인 로딩,
2편에서 명령어 시스템(/heal, 권한, 인자 파싱, 탭완성),
3편에서 이벤트 처리 실전을 다뤘다면,

이번 4편에서는 실전 서버 운영에서 정말 자주 쓰는 기능을 만듭니다.

바로 config.yml을 이용한:

  • 기능 on/off
  • 메시지 커스터마이징
  • 운영 중 설정 변경 반영(리로드)

입니다.

코드를 하드코딩으로만 만들면, 나중에 서버 운영할 때 작은 문구 하나 바꾸려고도 재빌드/재배포를 해야 합니다.
이번 편에서 그 불편함을 줄여보겠습니다.


이번 편 목표

예제로 지난 편의 /heal 명령어를 확장합니다.

  • config.yml에서 /heal 기능 자체를 켜고/끄기
  • 회복량(하트/체력) 설정값으로 관리
  • 안내 메시지(성공/권한 없음/비활성화) 커스터마이징
  • /healreload 명령어로 설정 다시 읽기(관리자용)

즉, 운영자가 코드 수정 없이 서버 설정만 바꿔도 동작이 달라지게 만드는 것이 목표입니다.


먼저 알아둘 용어 (초보자 기준)

  • config.yml: 플러그인 설정 파일(서버 운영자가 수정)
  • saveDefaultConfig(): 기본 config.yml이 없을 때 플러그인 jar 안의 파일을 꺼내 저장
  • getConfig(): 현재 설정값을 읽는 메서드
  • reloadConfig(): 디스크의 config.yml을 다시 읽어서 반영
  • permission node: 권한 문자열 이름 (예: myplugin.heal, myplugin.admin)
  • path: 설정 키 경로 (예: heal.enabled, messages.no-permission)

1) plugin.yml에 명령어와 권한 등록

한 줄 요약: /heal, /healreload를 서버에 알리고 권한 노드도 문서화합니다.

name: my-first-plugin
version: 1.0.0
main: com.example.myplugin.MyPlugin
api-version: '1.21'

commands:
  heal:
    description: Heal yourself or target player
    usage: /heal [player]
    permission: myplugin.heal
  healreload:
    description: Reload plugin config
    usage: /healreload
    permission: myplugin.admin

permissions:
  myplugin.heal:
    description: Use /heal command
    default: op
  myplugin.admin:
    description: Reload plugin config
    default: op

permission:을 써두면 서버 운영자가 명령어 용도를 파악하기 쉽고,
권한 플러그인(LuckPerms 등)과 함께 운영할 때도 관리가 편합니다.


2) 기본 config.yml 만들기

한 줄 요약: 기능 플래그, 수치, 메시지를 분리해두면 운영 중 수정이 쉬워집니다.

src/main/resources/config.yml

heal:
  enabled: true
  amount: 20.0
  allow-target: true

messages:
  prefix: "&8[&aHeal&8] &r"
  no-permission: "&c권한이 없습니다."
  heal-disabled: "&e현재 /heal 기능이 비활성화되어 있습니다."
  player-only: "&e플레이어만 사용할 수 있습니다."
  player-not-found: "&c대상 플레이어를 찾을 수 없습니다."
  healed-self: "&a체력을 회복했습니다."
  healed-target: "&a{player}의 체력을 회복했습니다."
  healed-by-other: "&a관리자에 의해 체력이 회복되었습니다."
  config-reloaded: "&aconfig.yml을 다시 불러왔습니다."

포인트는 3개입니다.

  • heal.enabled: 기능 on/off
  • heal.amount: 회복량 숫자 설정
  • messages.*: 운영자가 문구를 바꿀 수 있게 분리

나중에 다국어 대응이나 서버별 톤 조정할 때도 이 구조가 유리합니다.


3) 메인 클래스에서 기본 config 저장 + 명령어 등록

한 줄 요약: 플러그인 시작 시 기본 설정 파일을 만들어두고, 명령어 실행기를 연결합니다.

package com.example.myplugin;

import org.bukkit.plugin.java.JavaPlugin;

public final class MyPlugin extends JavaPlugin {

    @Override
    public void onEnable() {
        saveDefaultConfig();

        HealCommand healCommand = new HealCommand(this);

        getCommand("heal").setExecutor(healCommand);
        getCommand("healreload").setExecutor(healCommand);

        getLogger().info("MyPlugin enabled.");
    }
}

saveDefaultConfig()가 중요한가?

이 줄이 없으면 서버 plugins/내플러그인/ 폴더에 config.yml이 자동 생성되지 않습니다.
그러면 getConfig()는 동작하더라도 운영자가 수정할 파일이 처음부터 없어서 불편합니다.

setExecutor(...)를 onEnable에서 하나?

플러그인이 켜질 때 명령어와 실행 클래스를 연결해야 서버가 /heal 입력을 해당 코드로 보낼 수 있습니다.
이 줄이 빠지면 명령어가 등록되어 있어도 실제 로직이 실행되지 않습니다.


4) 설정값/메시지를 읽는 /heal 명령어 만들기

한 줄 요약: config 값을 직접 하드코딩 대신 읽어서 기능 on/off와 메시지를 제어합니다.

package com.example.myplugin;

import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.attribute.Attribute;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.entity.Player;

public final class HealCommand implements CommandExecutor {
    private final MyPlugin plugin;

    public HealCommand(MyPlugin plugin) {
        this.plugin = plugin;
    }

    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        String name = command.getName().toLowerCase();

        if (name.equals("healreload")) {
            return handleReload(sender);
        }

        return handleHeal(sender, args);
    }

    private boolean handleReload(CommandSender sender) {
        if (!sender.hasPermission("myplugin.admin")) {
            sender.sendMessage(msg("no-permission"));
            return true;
        }

        plugin.reloadConfig();
        sender.sendMessage(msg("config-reloaded"));
        return true;
    }

    private boolean handleHeal(CommandSender sender, String[] args) {
        if (!sender.hasPermission("myplugin.heal")) {
            sender.sendMessage(msg("no-permission"));
            return true;
        }

        FileConfiguration config = plugin.getConfig();

        if (!config.getBoolean("heal.enabled", true)) {
            sender.sendMessage(msg("heal-disabled"));
            return true;
        }

        if (args.length == 0) {
            if (!(sender instanceof Player player)) {
                sender.sendMessage(msg("player-only"));
                return true;
            }

            healPlayer(player, config.getDouble("heal.amount", 20.0));
            player.sendMessage(msg("healed-self"));
            return true;
        }

        if (!config.getBoolean("heal.allow-target", true)) {
            sender.sendMessage(color("&e대상 지정 heal 기능이 비활성화되어 있습니다."));
            return true;
        }

        Player target = Bukkit.getPlayerExact(args[0]);
        if (target == null) {
            sender.sendMessage(msg("player-not-found"));
            return true;
        }

        healPlayer(target, config.getDouble("heal.amount", 20.0));
        sender.sendMessage(msg("healed-target").replace("{player}", target.getName()));
        target.sendMessage(msg("healed-by-other"));
        return true;
    }

    private void healPlayer(Player player, double amount) {
        double maxHealth = player.getAttribute(Attribute.MAX_HEALTH).getValue();
        double newHealth = Math.min(maxHealth, amount);
        player.setHealth(newHealth);
        player.setFoodLevel(20);
        player.setFireTicks(0);
    }

    private String msg(String path) {
        String prefix = plugin.getConfig().getString("messages.prefix", "");
        String body = plugin.getConfig().getString("messages." + path, path);
        return color(prefix + body);
    }

    private String color(String text) {
        return ChatColor.translateAlternateColorCodes('&', text);
    }
}

코드 설명 (초보자용)

  • plugin.getConfig():

    • 현재 메모리에 로드된 설정을 읽습니다.
    • 매번 파일을 직접 읽는 게 아니라 Paper/Bukkit이 관리하는 설정 객체를 사용합니다.
  • getBoolean("heal.enabled", true):

    • heal.enabled 값이 있으면 그 값을 사용
    • 없으면 기본값 true 사용
    • 설정 누락에도 플러그인이 바로 죽지 않게 만드는 안전장치입니다.
  • args.length == 0:

    • /heal만 입력한 경우(자기 자신 회복)
    • /heal 닉네임과 분기하기 위한 핵심 조건입니다.
  • sender instanceof Player:

    • 콘솔은 Player가 아니므로 자기 자신 회복 로직을 바로 실행하면 안 됩니다.
    • 콘솔에서 /heal만 입력하면 대상이 없으므로 안내 메시지를 보내는 게 맞습니다.
  • plugin.reloadConfig():

    • 디스크의 config.yml을 다시 읽습니다.
    • 서버 재시작 없이 설정 반영이 가능해서 운영 편의성이 크게 올라갑니다.

5) 왜 이 줄들이 중요한가? (실전 포인트)

hasPermission(...)를 왜 명령어 초반에 검사하나?

권한 없는 사용자가 뒤 로직(대상 탐색, 설정값 처리)까지 들어가지 않게 막기 위해서입니다.
실전에서는 "빨리 차단"이 로그/디버깅도 깔끔합니다.

args.length 분기를 왜 먼저 하냐?

args[0] 접근 전에 길이 검사를 하지 않으면 /heal 입력 시 예외가 납니다.
명령어 코드는 항상 인자 개수 검증이 먼저입니다.

getBoolean(..., true)처럼 기본값을 왜 넣나?

운영자가 config를 잘못 수정하거나 일부 키를 지웠을 때도 플러그인이 최대한 계속 동작하도록 만들기 위해서입니다.
실전 서버에서는 "설정 오타로 플러그인 전체가 죽는 상황"을 피하는 게 중요합니다.

reloadConfig()만 하고 끝내도 되나?

이번 예제처럼 getConfig()를 매번 읽는 구조라면 대부분 충분합니다.
반대로 설정값을 필드에 캐싱해두는 구조라면 reload 후 캐시 재로딩 코드가 추가로 필요합니다.


6) 이벤트 처리에도 같은 방식으로 config 적용 가능

한 줄 요약: 3편에서 만든 이벤트 리스너에도 config.yml 플래그를 붙이면 운영 제어가 쉬워집니다.

예를 들어 "특정 아이템 우클릭 효과"를 만들었다면:

if (!plugin.getConfig().getBoolean("features.magic-stick-enabled", true)) {
    return;
}

이렇게 한 줄만 추가해도 운영자는 config에서 기능을 켜고 끌 수 있습니다.

즉, config.yml은 명령어 전용이 아니라
명령어 + 이벤트 + 스케줄러 전체에 공통으로 쓰는 운영 제어판이라고 보면 됩니다.


자주 나오는 실수 (실패 케이스)

1) saveDefaultConfig()를 안 넣음

증상:

  • plugins/플러그인명/config.yml 파일이 안 생김
  • 운영자가 수정할 설정 파일이 없음

원인:

  • onEnable에서 기본 config 저장 누락

2) plugin.yml에 명령어 등록 안 함

증상:

  • /heal 입력 시 Unknown command

원인:

  • 코드에서 setExecutor(...)만 하고 plugin.yml 등록 누락

3) 메시지 경로 오타

증상:

  • 메시지 대신 messages.no-permision 같은 키 문자열이 그대로 출력됨

원인:

  • messages.no-permission 경로 오타
  • 설정 키 이름과 코드 경로가 다름

4) reloadConfig()는 했는데 값이 안 바뀌는 것처럼 보임

증상:

  • /healreload 성공 메시지는 뜨는데 동작이 이전 설정처럼 보임

원인:

  • 설정값을 클래스 필드에 미리 저장(캐싱)해둔 뒤 다시 읽지 않음

수동 테스트 체크리스트 (실전 운영 기준)

  • 서버 시작 후 plugins/플러그인명/config.yml이 생성되는지 확인
  • heal.enabled: true 상태에서 /heal이 정상 동작하는지 확인
  • heal.enabled: false로 바꾸고 /healreload/heal이 차단되는지 확인
  • heal.amount20.0 -> 10.0으로 바꾸고 /healreload 후 체력 회복량이 달라지는지 확인
  • messages.healed-self 문구를 바꾸고 /healreload 후 변경 메시지가 출력되는지 확인
  • 권한 없는 계정에서 /heal, /healreloadno-permission이 출력되는지 확인
  • 콘솔에서 /heal 입력 시 player-only가 출력되는지 확인
  • /heal 없는닉네임 입력 시 player-not-found가 출력되는지 확인

이번 편 정리

이번 편에서는 config.yml을 이용해 플러그인을 "코드 중심"에서 "운영 가능한 형태"로 한 단계 올렸습니다.

핵심은 이 4가지입니다.

  • saveDefaultConfig()로 기본 설정 파일 제공하기
  • getConfig()로 기능 on/off와 수치 설정 읽기
  • messages.* 경로로 문구 하드코딩 줄이기
  • reloadConfig()로 서버 재시작 없이 운영 편의성 확보하기

이 패턴을 익혀두면 다음 편의 데이터 저장(YAML/SQLite)에서도
설정 경로, 기본값, 운영자 친화적인 구조를 훨씬 쉽게 설계할 수 있습니다.

다음 편에서는 YAML을 이용해 유저별 설정을 저장하는 방법을 실전으로 다뤄보겠습니다.