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

[자바로 마크 Paper 플러그인 만들기]2. 유저 데이터 관리하기(1) - 이벤트 처리

by Zepelown 2024. 1. 1.

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

 

이전 강의

[자바로 마크 Paper 플러그인 만들기]1. 기본 세팅하기 (tistory.com)

 

[자바로 마크 Paper 플러그인 만들기]1. 기본 세팅하기

안녕하세요. 이전에 만들었던 '인텔리제이로 마크 플러그인 만들기' , '코틀린으로 마크 플러그인 개발하기' 두 시리즈를 리뉴얼하여 '자바로 마크 Paper 플러그인 만들기'로 돌아왔습니다. Paper AP

zepelown.tistory.com

참고하면 좋은 강의

[인텔리제이로 마크 플러그인 개발하기] 3. 흙을 캐면 다이아가 나오게 해보자! (이벤트와 리스너에 관하여) (tistory.com)

 

[인텔리제이로 마크 플러그인 개발하기] 3. 흙을 캐면 다이아가 나오게 해보자! (이벤트와 리스너

이 강좌는 spigot 기준으로 작성되었습니다. 이전 화 https://zepelown.tistory.com/37 [인텔리제이로 플러그인 개발하기] 2. 인텔리제이 한글인코딩 약 2년 전에 멈췄던 강의를 다시 시작하고자 합니다. 공

zepelown.tistory.com


개요

 

어느 정도 규모가 있는 플러그인을 제작할 땐 

 

플레이어 데이터에 대한 관리는 필수적입니다.

 

이번 강의에선, 서버에 접속 중인 유저들을 관리할 수 있도록

 

기반을 다져보겠습니다.

 


유저에 관한 데이터가 뭐가 있을까요?

1. UUID

 Universally Unique IDentifier의 약자로 

 

자바에서 제공하는 식별자입니다.

 

마인크래프트에선 이를 이용하여 플레이어의 고유 식별자를 부여 및 관리합니다.

 

마크의 주민등록번호라고 생각하시면 됩니다.

 

2. 이름

UUID에 해당하는 마인크래프트 닉네임입니다.

 

플러그인 상에서 추가로 부여한 데이터

1. 랭크

사용자 관리에 필요한 랭크 등급을 부여해 보았습니다.

 

newbie -> user -> admin

 

현재 구상은 위와 같습니다.

2. 돈

경제 기능 구현을 위한 돈입니다.

3. 직업

직업 구현을 위한 데이터로 기본값은 jobless입니다.

4. 칭호

기본값은 [뉴비]입니다.

 

 

총 6개의 유저 데이터가 있다고 생각하고 진행해 보겠습니다.

 


미리 보는 결과물 

그림1. 인게임 내에서 명령어 "uinfo" 입력 시 모습
그림 2. 플레이어 입퇴장 시 기록 남기는 모습

 


전체 구조에 대해

그림 3. 프로젝트 전체 패키지(폴더) 구조

기본적인 패키지 구조는

 

MVC 디자인 패턴 + 도메인형 구조를 차용하였습니다.

 

/ (참고) 도메인형 구조란? /

더보기

도메인이란?

 

어느 한 기능(문제 해결)의 범위를 나타냅니다.

 

기능(문제 해결)의 범위.. 이 말 자체가 쉽게 와닿는 말은 아니죠.

 

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

 

지금 여기서 만들고자 하는 플러그인의 기능은

 

"유저에 대한 데이터 관리"입니다.

 

이게 도메인입니다.

참고 그림 1. 도메인형 구조 예시

 

만약 현재 만들고 있는 유저 관리 기능 말고 낚시에 대한 기능을 만든다고 가정해 봅시다.

 

그러면 위와 같이 fishing라는 이름으로 최상위 패키지를 만들어주는 겁니다.

 

그리고 그 안에 똑같이 controller, entity 만듭니다.

 

 이게 패키지의 도메인형 구조입니다.

 

이렇게 사용하게 되면 도메인의 흐름을 파악하기가 쉬워집니다.

 

/ (참고) MVC 패턴이란? /

 

다 필요 없고 이렇게만 아시면 됩니다.

패키지(폴더)명 부류 역할
controller controller 사용자의 입력을 받고 로직을 수행
controller.commands
entity model 데이터의 정보(상태)를 저장
service model 비즈니스 로직 및 데이터 조작을 수행

(잘 보면 view가 없습니다. 이는 차후 강의인 Kyori Adventure API에서 다루도록 하겠습니다.)

이제 각각의 클래스에 대해 살펴보겠습니다.

controller.UserManagementController

유저 관리 기능 진입점으로

 

유저 관리에 필요한 이벤트나 커맨드 등록을 하는 곳입니다.

 

차후 다른 기능에서 유저 정보 수정이 필요하다면 이곳을 거치게 됩니다.

controller.UserConnectionController

유저 입퇴장에 관한 이벤트를 처리하는 객체입니다.

controller.commands.UserInfoCommand

유저 정보를 보여줄 명령어인  "/uinfo" 로직을 가지고 있는 객체입니다.

entity.User

유저 데이터를 정의하는 객체입니다.

service.UserManager

유저 데이터를 입력, 삭제, 수정 등을 진행하는 객체입니다.

 

미리 패키지 구조를 만드시고 진행하는 걸 추천드립니다.


코딩 단계

중간중간 코드를 그대로 사용했을 때 

 

오류가 발생할 수도 있습니다.

 

다음 단계로 넘어가면 없어질 겁니다.

entity.User

public class User {
    private UUID uuid;
    private String displayName;

    private String rank;

    private Long money;

    private String job;

    private String prefix;

    public User(UUID uuid,   String displayName, String rank, Long money, String job, String prefix) {
        this.uuid = uuid;
        this.displayName = displayName;
        this.rank = rank;
        this.money = money;
        this.job = job;
        this.prefix = prefix;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public UUID getUuid() {
        return uuid;
    }

    public void setUuid(UUID uuid) {
        this.uuid = uuid;
    }

    public String getDisplayName() {
        return displayName;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public String getRank() {
        return rank;
    }

    public void setRank(String rank) {
        this.rank = rank;
    }

    public Long getMoney() {
        return money;
    }

    public void setMoney(Long money) {
        this.money = money;
    }
}

유저에 대한 정보를 정의하는 객체이므로

 

Getter/Setter만 있으면 됩니다.

(Getter는 데이터를 가져오는 메소드를, Setter는 데이터를 변경하는 메소드를 의미합니다)

 

service.UserManager

public class UserManager {
    private HashMap<Player, User> onlineUserData = new HashMap<>();

    public void addUser(Player player) {
        User newUser = new User(
                player.getUniqueId(),
                player.getDisplayName(),
                "newbie",
                1000L,
                "jobless", "[뉴비]");
        onlineUserData.put(player, newUser);
    }

    public void removeUser(Player player){
        onlineUserData.remove(player);
    }

    public User getUserData(Player player){
        return onlineUserData.get(player);
    }

}

앞서 만들었던 User 객체를 이용하여

 

현재 접속하고 있는 유저들의 정보들을 담고 있는 객체입니다.

 

HashMap이라는 자료 구조를 사용하여 서버 접속 시에 추가하고

 

퇴장 시에 삭제할 수 있고

 

명령어를 통해 사용자의 정보를 불러오기 위해

 

addUser, removeUser, getUserData 메소드를 만들었습니다.

/ (참고) HashMap 이란? /

더보기

키와 값을 쌍으로 저장하는 자료 구조입니다.

 

여기서 키는 Player 객체이고

 

값은 User 객체입니다.

 

유저가 서버에 접속 시 Player 객체를 가져올 수 있으니

 

이를 통해 저장할 User 객체를 넣고 뺴고 할 수 있게 되는 겁니다.

 

이는 각 유저에 맞게 정보를 저장하기 위함입니다.

 

다른 유저에게 자신의 정보가 가면 안되겠죠??

 

현재는 파일이나 DB를 통해 사용자의 데이터를 저장할 수 없기 때문에

 

사용자가 들어온다면 무조건 기본 값을 부여하는 방식으로 진행합니다.

(나가면 다 삭제됩니다)

 

PaperEdu (메인 클래스)

public final class PaperEdu extends JavaPlugin {
    private static PaperEdu serverInstance;
    @Override
    public void onEnable() {
        serverInstance = this;

        getLogger().info("플러그인 시작 테스트");
    }

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

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

    public static PaperEdu getServerInstance() {
        return serverInstance;
    }

}

차후 진행할 때 서버의 인스턴스가 필요한 경우가 있습니다.

 

이벤트, 명령어, 서버 콘솔창에 메시지 남기는 기능 등은 서버의 인스턴스를 요구합니다.

 

하지만 서버는 단 하나의 인스턴스로 존재해야 합니다.

(서버가 여러 개면 당연히 오류가 발생하겠죠??)

 

그렇기 때문에 메인 클래스 상에서 static으로 하나의 인스턴스로 만들어주고

 

다른 객체에서 getServerInstance() 메소드를 사용해 가져오는 방식인 Singleton 기법을 사용합니다.

/ (참고) Singleton 기법 /

 

controller.UserConnectionController

public class UserConnectionController implements Listener {

    private final UserManager userManager;

    UserConnectionController(UserManager userManager){
        this.userManager = userManager;
    }

    @EventHandler
    public void onUserJoinServer(PlayerJoinEvent event) {
        userManager.addUser(event.getPlayer());
        PaperEdu.getServerInstance().getLogger().info("플레이어 데이터 저장");
    }
    
    @EventHandler
    public void onUserQuitFromServer(PlayerQuitEvent event) {
        userManager.removeUser(event.getPlayer());
        PaperEdu.getServerInstance().getLogger().info("플레이어 데이터 삭제");
    }
}

이제 오늘 메인 주제인 유저 입퇴장에 관한 이벤트를 처리하는 곳입니다.

 

그림 4. Listener를 implements

 

버킷에서 제공하는 Listener 인터페이스를 implements(구현) 함으로써

 

이벤트에 대한 처리를 할 수 있습니다.

 

그림 5. 이벤트 예시

 

@EventHandler는 어노테이션으로 이벤트의 처리를 하는 메소드임을 명시적으로 표시합니다.

(반드시 있어야 합니다)

 

메소드의 명은 아무렇게 하셔도 상관없습니다.

 

이제 메소드의 파라미터를 보시면 PlayerJoinEvent 객체를 받는 걸 볼 수 있죠.

 

이게 바로 플레이어가 입장을 했을 때를 나타내며 이 onUserJoinServer 메소드가 실행되는 겁니다.

 

즉, PlayerJoinEvent가 이벤트인 겁니다.

 

/ (참고) 이벤트가 무슨 의미일까요? /

더보기

이벤트란?

 

게임에서 일어난 모든 어떤 사건(행동)을 말합니다.

 

예를 들어보겠습니다.

 

어느 한 플레이어가 걷고 있다가 돌을 발견했습니다. 그리고 돌을 캐서 돌 하나를 획득하였습니다.

 

위 문장에서 이벤트가 과연 어떤 걸까요?

 

답은 플레이어가 걷는 행동, 돌을 시야 내에서 발견, 돌을 , 돌 하나를 획득 이 모든 것이 이벤트입니다.

 

이 강의에선 플레이어가 입장, 퇴장한 것이 이벤트라고 볼 수 있습니다.

 

이벤트가 발생할 때 메소드가 실행된다고 생각하시면 됩니다.

생성자를 통해 UserManager를 가져와서

 

플레이어 입장 이벤트가 발생하면 유저 정보를 추가하고

 

퇴장 이벤트가 발생하면 유저 정보를 삭제합니다.

 

이로써 이벤트에 처리는 끝났으나 이벤트를 생성했으면 서버 인스턴스에 등록을 해줘야 합니다.

controller.UserManagementController

public class UserManagementController {

    private static UserManager userManager;

    private final PaperEdu serverInstance;

    private UserConnectionController userConnectionController;

    public UserManagementController() {
        this.userManager = new UserManager();
        this.serverInstance = PaperEdu.getServerInstance();

        this.userConnectionController = new UserConnectionController(userManager);
        
        registerEvents();
    }


    private void registerEvents() {
        serverInstance.getServer().getPluginManager().registerEvents(userConnectionController, serverInstance);
    }

}

 

그걸 하는 곳이 UserManagementController입니다.

 

실질적으로 유저 관리 기능의 모든 것을 총괄하는 컨트롤러로

 

private static UserManager userManager;

 

현재 접속한 유저의 정보를 가지고 있는 UserManager 인스턴스 또한 가지고 있습니다.

(싱글턴으로 생성)

 

    private void registerEvents() {
        serverInstance.getServer().getPluginManager().registerEvents(userConnectionController, serverInstance);
    }

 

서버 인스턴스에 이벤트를 등록하기 위해서 메인 클래스에서 서버 인스턴스를 가져오고

 

이를 이용해 이벤트를 등록합니다.

 

이제 메인 클래스에 UserManagementController를 생성하면 끝이 납니다.

 

PaperEdu (메인 클래스)

public final class PaperEdu extends JavaPlugin {
    private static PaperEdu serverInstance;
    private static UserManagementController userManagement;


    @Override
    public void onEnable() {
        serverInstance = this;
        userManagement = new UserManagementController();

        getLogger().info("플러그인 시작 테스트");
    }

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

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

    public static PaperEdu getServerInstance() {
        return serverInstance;
    }

    public static UserManagementController getUserManagement(){
        return userManagement;
    }
}

이벤트 등록은 끝났습니다.

 

명령어 만드는 다음 편으로 돌아오겠습니다.

 

감사합니다.

댓글