본문 바로가기
마인크래프트/플러그인 제작 강좌(자바)

[자바로 마크 Paper 플러그인 만들기]6. 주기적으로 공지사항 띄우기

by Zepelown 2024. 9. 1.

*본 강의는 1.20.2 Paper 기준으로 제작되었습니다*

 

이전 강의

[자바로 마크 Paper 플러그인 만들기]5. 유저 데이터 관리하기(4) - Config로 데이터 저장하기(Config 만들기, 직렬화 사용) (tistory.com)

 

[자바로 마크 Paper 플러그인 만들기]5. 유저 데이터 관리하기(4) - Config로 데이터 저장하기(Config 만

*본 강의는 1.20.2 Paper 기준으로 제작되었습니다* 이전 강의[자바로 마크 Paper 플러그인 만들기]4. 유저 데이터 관리하기(3) - 다른 플레이어 조회하기 (tistory.com) [자바로 마크 Paper 플러그인 만들

zepelown.tistory.com

 

강의에 사용한 전체 코드

Zepelown/PaperBlogPosting: Minecraft Paper API Blog Posting Repository (github.com)

 

GitHub - Zepelown/PaperBlogPosting: Minecraft Paper API Blog Posting Repository

Minecraft Paper API Blog Posting Repository. Contribute to Zepelown/PaperBlogPosting development by creating an account on GitHub.

github.com

 


 

미리 보는 결과물

그림 1. 10초마다 공지사항을 띄우는 모습


개요

 

이전 강의까지 유저 데이터를 기본적으로 관리하는 방법에 대해 알아봤습니다.

 

이번엔 조금 쉬어가는 편으로

 

이전 시리즈와 크게 상관없이 따라 할 수 있는 내용을 준비해 보았습니다.

 

그 주제가 바로 "주기적으로 공지사항 띄우기" 입니다.

 

아주 기초적인 내용만 다루고 있으니 금방 따라 하실 수 있으실 겁니다!

 

바로 시작해 보죠!

 


패키지(폴더) 구조 예시

그림 2. 패키지 구조

 


기능 구현 전 이론 이해 단계

 

먼저, 마크에서 시간이 어떻게 정의가 되어 있는지에 대해 알고 계셔야 합니다.

 

마인크래프트에선 틱(Tick) 이라는 시간 단위를 사용합니다.

 

약 20 틱 = 1초,

약 1200틱 = 1분

...

 

으로 정의하고 있습니다.

 

이 틱을 편하게 구분하기 위해 자바에 정의되어 있는 시간을 이용하여 표현할 수 있습니다.

TimeUnit.MINUTES.toSeconds(5) * 20 // 5분을 틱으로 변경
TimeUnit.SECONDS.toMinutes(ticks / 20) // 주어진 틱을 분으로 변경

 

이 시간을 재고 있는 주체인 스레드에 대한 이해도 필요합니다.

 

 

그림 3. Executor Service 구성 요소

 

스레드란?

 

프로세스 내에서 실행되는 작업 단위로 이는 스레드 풀에서 스레드를 가지고 있습니다.

 

그리고 이를 전체적으로 관리하는 것이 자바에선 Executor Service가 되는 겁니다.

 

너무 이론적인 내용으로 접근하면 머리가 터지니 

 

간단하게 예시와 비유를 들어보겠습니다.

 

지금 저희가 서버를 돌리게 되면

 

기본적으로 "메인 스레드(Main Thread)"라고 이름을 가진 스레드이자 Exceutor Service에서 모든 활동이 진행됩니다.

(이 개념 자체가 매우 모호한 느낌이 있어, 실제로 사용하는 사람들도 단어를 쉽게 혼용합니다..)

 

이 모든 활동이라는 건, 서버(PaperAPI) 개발자가 따로 정의하지 않은 작업을 의미하며,

 

월드 내에서 플레이어의  블럭 파괴, 이동, 설치 등의 모든 일련의 활동을 말합니다.

 

이렇게 생각하시면 됩니다.

 

대도시 중심에 있는 영화관(Executor Service)이 있습니다.

 

사람(일, Task)이 너무 많아서 맨날 티켓 발매기(Thread) 앞에 줄을 서고 있어요.

 

여기서 티켓 발매기에 줄을 선 사람들을 서버가 처리하는 일로

 

티켓 발매기를 스레드라고 생각하시면 편합니다.

 

자, 여기서 줄을 많이 서고 있던 건 당연하게도 이유가 있었습니다.

 

영화 티켓 발매기가 하나밖에 없어서인데, 이를 해결하기 위해선 크게 세 가지 방법이 있을 수 있습니다.

 

첫 번째, 엄청 과정을 간략화시킨 고성능 티켓 발매기로 교체한다.

 

두 번째, 티켓 발매기의 개수를 늘린다.

 

세 번째, 영화관을 늘린다.

 

두 번째든, 세 번째든 목적은 똑같습니다.

 

사람(일)을 분리하는 겁니다.

 

"일의 분리", 이게 스레드 또는 Executor Service 를 만들게 된 배경입니다.

 

다시 돌아와서, Paper API에선 메인 스레드(Executor Service) 하나가

 

기본적으로 모든 서버의 작업을 처리하고 있습니다.

 

Executor Service 하나가 하나의 스레드를 가지고 있습니다.

(경우에 따라 즉석에서 다른 스레드를 만들었다가 일 끝나면 지우는 상황도 존재)

 

이 작업엔 마크 월드 내용도 포함되어 있습니다.

 

여기서 만약, 공지사항도 메인 스레드에 일을 맡긴다면 서버에 쉽게 부하가 일어날 수 있습니다.

(지금 강의에선 규모가 작지만, 커지면 커질수록 서버에 부하는 쉽게 일어나요!)

 

그렇기 때문에 다른 스레드(풀)를 만들어, 이를 분리시켜 주는 겁니다.

 

여기서 정말 중요한 것은 다른 스레드에게 지시하는 일은 절대로 월드에 영향을 주지 않는 작업들이어야만 합니다!!

(메인 스레드가 월드를 담당하고 있기 때문에 동기화의 문제가 발생할 수 있어요)

 

공지사항을 개시하는 것은 여기에 자유로우니 웬만하면 분리해서 사용하면 좋겠죠

 

어쨌든 스레드는 작업을 처리하는 영화 티켓 발매기 같은 겁니다.

 

여기서 나오는 일의 분리에 관한 이야기가 바로 비동기, 동기 작업에 대한 이야기입니다.

 

일은 처리하는 방식에 대한 용어라고 보시면 돼요

그림 4. 동기와 비동기

 

동기 작업(위 그림 왼쪽)은 하나의 작업이 끝나야만, 다음 작업을 실행하는 것으로

 

비동기 작업은 이에 반대에 속합니다.

 

즉, 메인 스레드 하나에서 모든 일을 처리하는 방식은 동기 작업

 

새로운 스레드를 만들어 일을 분리하는 것을 비동기 작업이라고 말할 수 있습니다.

 

 

근데 여기까지 와서 뭔가 의문이 들지 않으신가요??

 

시간마다 공지사항을 띄우는 걸 하는데 스레드도 알아야 하고, 비동기, 동기 작업을 알아야 하는지 말이에요.

 

이는, 시간마다 공지를 띄운다는 자체가 일을 처리하는 주체인 스레드에게 주기적으로 일을 던져줘야 함을 의미하기 때문입니다.

 

결국엔 서버의 일을 처리하는 주체는 스레드이니까요.

 

따라서 이런 복잡하고 어려운 이론적 배경에 대해 간략하게라도 이해가 필요한 겁니다.

 

차근차근 코드 예시 보시면 더 이해가 쉽게 되실 겁니다.

 

 


기능 구현 단계

그림 5. 패키지 구조

 

위와 같이 패키지 구조와 클래스를 만들어줍니다.

 

여기서 Runnable이 바로 스레드에 들어갈 작업이자, 공지사항의 내용을 가지고 있는 객체입니다.

 

/ (참고) Runnable에 대하여 /

server.management.model.AnnouncementRunnable

public class AnnouncementRunnable implements Runnable{
    @Override
    public void run() {
        PaperEdu.getServerInstance().getServer().broadcast(Component.text("공지사항 테스트"));
    }
}

 

"공지사항 테스트" 라는 문구를 공지사항으로 출력하는 매우 간단한 구문입니다.

 

이 Runnable 구현체를 스레드에 일로 던져주면 이제 공지를 하게 되는 겁니다.

 

server.management.model.ServerManagementController

위 클래스에서 여러 방식으로 공지사항을 구현해 보겠습니다.

 

여기서 이 클래스의 역할은 스레드에 일을 등록하는 역할입니다.

(위의 Runnable 구현체를 등록하는 일)

 

방식 1. 버킷 기본 Executor Service인 Scheduler를 사용하여 동기 작업

그림 6. runTask 메소드 예시

 

서버 인스턴스를 가져와 getScheduler를 통해 작업을 등록할 수 있습니다.

 

Timer가 붙은 메소드는 주기적으로 일을 등록하는 것이고

 

Later는 일정 시간 뒤에 일을 실행하는 것입니다.

(시간 차 효과를 부여하는 데 사용할 수 있겠죠??)

 

공지사항을 띄우기 위해선 Timer를 사용해야 합니다.

 

PaperEdu.getServerInstance().getServer().getScheduler().runTaskTimer(PaperEdu.getServerInstance(), new AnnouncementRunnable(), 10L, 10L);

 

위와 같은 방식으로 사용할 수 있습니다.

 

여기서 첫 번째 10은 첫 작업의 시작 딜레이로 틱 단위입니다.

(참고로 10L은 10을 Long 단위로 하겠다는 말입니다)

 

그리고 두 번째 10은 첫 작업 후 재 반복할 시간을 의미하고 이 또한 틱 단위입니다.

 

10 틱은 약 0.5초라고 생각하시면 됩니다.

 

위와 같이 사용하시면 이제 메인 스레드에 등록하는 동기 작업에 해당합니다.

 

방식 2. 버킷 기본 Executor Service인 Scheduler를 사용하여 비동기 작업

 

PaperEdu.getServerInstance().getServer().getScheduler().runTaskTimerAsynchronously(PaperEdu.getServerInstance(), new AnnouncementRunnable(), 10L, 10L);

 

방식 1이랑 크게 다른 건 없고, 메소드가 달라졌습니다.

 

비동기적인 작업으로 처리하여 실질적으로 서버에 렉에 영향을 적게 줍니다.

 

월드에 영향 안주는 작업이니 위와 같이 쓰면 더 좋겠죠??

 

방식 3. Executor Service를 새로 만들어 비동기 작업

 

여기까지만 보면 방식 3이 왜 필요 있나 싶으실 겁니다.

 

방식 1,2 적절하게 번갈아가며 쓰면 되는 거 아니냐? 라는 질문에 봉착하게 되는데요.

 

이는 태생적으로 Bukkit의 Scheduler에 한계가 있기 때문에 방식 3에 대해 알고 계셔야 합니다.

 

1,2에서 눈치를 채셨을지 모르시겠지만, 우리는 사실 마크 시간에 관심이 없는 경우가 더 많습니다.

 

우리는 현재 시간으로 10분에 한 번씩, 일 년에 한 번을 원하지,

 

마크 시간으로 하루에 한 번을 원하는 경우는 적기 때문입니다.

 

예를 들어, 서버에서 출석 체크 이벤트를 만들어, 하루마다 보상을 줄려고 할 때,

 

1번과 2번 방식은 틱을 직접 계산해야하고 렉에 따른 틱의 밀림을 계산해야하기에 정말 구현하기가 까다롭습니다.

 

이 때문에 현재 시간을 뚜렷하게 나타내고 따라가는 3번 방식을 이해하시는 게 도움이 되실 겁니다.

 

그리고 이 방식이 오늘 강의 최종 목표입니다.

server.management.model.ServerManagementController

public class ServerManagementController {
    private final static ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
    public ServerManagementController() {
        registerServerSchedulerTask();
    }

    private void registerServerSchedulerTask(){
        scheduledExecutorService.scheduleAtFixedRate(new AnnouncementRunnable(), 10, 10, TimeUnit.SECONDS);
    }
}

 

 

위 코드는 1,2번의 runTaskTimer와 비슷합니다.

 

다만, 마지막 파라미터로 시간 단위를 넣음으로써 더 확실한 표현이 가능하죠.

 

첫 시작은 Runnable이 등록되고 10초 뒤이고, 

 

이후 10초마다 Runnable을 반복하는 방식입니다.

 

scheduledExecutorService.scheduleAtFixedRate(new AnnouncementRunnable(), 10, 10, TimeUnit.MINUTES);

위와 같이 바꿔주신다면 분 단위로 작동하게 됩니다.

 

아주 간단하죠?

 

이제 마지막으로 메인 클래스인 PaperEdu에 ServerManagementController 인스턴스를 만들고 끝내보도록 하겠습니다.

 

package org.blog.paperedu;

import org.blog.paperedu.server.management.controller.ServerManagementController;
import org.blog.paperedu.user.management.controller.UserManagementController;
import org.blog.paperedu.user.management.data.entity.UserData;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

public final class PaperEdu extends JavaPlugin {
    private static PaperEdu serverInstance;
    private static UserManagementController userManagement;
    private static ServerManagementController serverManagement;
    @Override
    public void onEnable() {
        getLogger().info("플러그인 시작 테스트");
        serverInstance = this;
        userManagement = new UserManagementController();
        serverManagement = new ServerManagementController();
    }

    @Override
    public void onDisable() {
        userManagement.saveUserData();
        serverInstance = null;
        userManagement = null;

        getLogger().info("플러그인 종료 테스트");
    }

    public static PaperEdu getServerInstance() {
        return serverInstance;
    }

    public static UserManagementController getUserManagement(){
        return userManagement;
    }

}

 

 

 


결과물

 

출처

댓글