GameAnvil Server API - Java doc
IDE: Intellij 2022.3.3
JDK: openjdk version "11.0.16.1" 2022-08-12 LTS
GameAnvil 1.4.1
DB
MySQL 8.0.23
Redis
Git 저장소에서 clone한 프로젝트를 IntelliJ로 실행합니다.
기본 설정은 Gradle 설정 Dependencies에 com.nhn.gameanvil:gameanvil:1.4.1-jdk11로 JDK11 버전이 사용되고 있습니다.
resources/GameAnvilConfig.json 파일에 IP가 127.0.0.1로 되어 있습니다.
샘플 서버는 기본 JDK11로 설정되어 있기 때문에 IntelliJ가 다른 버전으로 설정되어 있는 경우 설정을 JDK11로 맞추어야 빌드 시 오류가 발생하지 않습니다.
만약 IntelliJ에서 GameAnvil 라이브러리와 JDK의 버전을 맞추지 않고 실행하면 다음과 같은 오류가 발생합니다.
Exception in thread "main" java.lang.UnsupportedClassVersionError: co/paralleluniverse/fibers/SuspendExecution has been compiled by a more recent version of the Java Runtime (class file version 54.0), this version of the Java Runtime only recognizes class file versions up to 52.0
IntelliJ JDK 설정은 다음을 확인하십시오.
File > Project Structure > Project Settings > Project 메뉴에서 JDK를 확인합니다.
IntelliJ IDEA > Settings > Build, Execution, Deployment > Buil Tools > Gradle > Gradle JVM 메뉴에서 JDK를 확인 합니다.
Gradle JDK를 변경했다면 Gradle 탭의 Reload를 실행해 프로젝트에 반영합니다.
빌드 환경 설정은 아래의 내용을 순서대로 설정합니다. IntelliJ 버전에 따라 화면은 조금 다를 수 있습니다. (스크린샷은 2023.12 버전입니다.)
패스워드 없이 Redis에 연결한다면 com.nhn.gameanvil.sample.common.GameConstants
클래스의 Redis 접속 정보를 수정해 연결합니다.
// Redis 접속 정보
public static final String REDIS_URL = "연결 주소";
public static final int REDIS_PORT = 7500;
패스워드 정보가 설정된 것이라면 Redis에 연결할 때 com.nhn.gameanvil.sample.redis.RedisHelper
클래스의 주석된 부분의 패스워드 설정을 사용해 접속합니다.
/**
* Redis 연결 처리. 사용 전 최초 1회 호출해 연결 필요
*
* @param url 접속 url
* @param port 접속 port
* @throws SuspendExecution 이 메서드는 파이버를 suspend할 수 있음을 의미
*/
public void connect(String url, int port) throws SuspendExecution {
// Redis 연결 처리
RedisURI clusterURI = RedisURI.Builder.redis(url, port).build();
// 패스워드가 필요한 경우에는 패스워드 설정을 추가해서 RedisURI를 생성
// RedisURI clusterURI = RedisURI.Builder.redis(url, port).withPassword("password").build();
this.clusterClient = RedisClusterClient.create(Collections.singletonList(clusterURI));
this.clusterConnection = Lettuce.connect(GameConstants.REDIS_THREAD_POOL, clusterClient);
if (this.clusterConnection.isOpen()) {
logger.info("============= Connected to Redis using Lettuce =============");
}
this.clusterAsyncCommands = clusterConnection.async();
}
샘플 서버에서는 기본적으로 Jasync-sql을 기본으로 사용하며, StoredProcedure로 쿼리를 사용합니다.
Mybatis 연결 시 com.nhn.gameanvil.sample.common.GameConstants.USE_DB_JASYNC_SQL
값을 false로 바꾸면 DB가 mybatis로 동작합니다.
샘플에서 사용하고 있는 테이블 정보입니다.
CREATE TABLE `users` (
`uuid` varchar(40) NOT NULL,
`login_type` int(11) NOT NULL,
`app_version` varchar(45) DEFAULT NULL,
`app_store` varchar(45) DEFAULT NULL,
`device_model` varchar(45) DEFAULT NULL,
`device_country` varchar(45) DEFAULT NULL,
`device_language` varchar(45) DEFAULT NULL,
`nickname` varchar(45) DEFAULT NULL,
`heart` int(11) NOT NULL,
`coin` bigint(15) DEFAULT '0',
`ruby` bigint(15) DEFAULT '0',
`level` int(11) DEFAULT '1',
`exp` bigint(15) DEFAULT '0',
`high_score` bigint(15) DEFAULT '0',
`current_deck` varchar(45) NOT NULL,
`create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`uuid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
사용하고 있는 StoredProcedure 정보입니다.
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_users_insert $$
CREATE PROCEDURE sp_users_insert
(
pi_uuid VARCHAR(40),
pi_login_type INT,
pi_app_version VARCHAR(45),
pi_app_store VARCHAR(45),
pi_device_model VARCHAR(45),
pi_device_country VARCHAR(45),
pi_device_language VARCHAR(45),
pi_nickname VARCHAR(45),
pi_heart INT,
pi_coin BIGINT,
pi_ruby BIGINT,
pi_level INT,
pi_exp BIGINT,
pi_high_score BIGINT,
pi_current_deck VARCHAR(45)
)
BEGIN
DECLARE err INT default '0';
DECLARE continue handler for SQLEXCEPTION set err = -1;
START TRANSACTION;
INSERT INTO users (uuid, login_type, app_version, app_store, device_model, device_country, device_language, nickname, heart, coin, ruby, level, exp, high_score, current_deck, create_date, update_date)
VALUES (pi_uuid, pi_login_type, pi_app_version, pi_app_store, pi_device_model, pi_device_country, pi_device_language, pi_nickname, pi_heart, pi_coin, pi_ruby, pi_level, pi_exp, pi_high_score, pi_current_deck, NOW(), NOW());
SELECT ROW_COUNT();
IF err < 0 THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
END $$
DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_users_select_uuid $$
CREATE PROCEDURE sp_users_select_uuid
(
pi_uuid VARCHAR(40)
)
BEGIN
SELECT *
FROM users
WHERE uuid = pi_uuid;
END $$
DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_users_update_high_score $$
CREATE PROCEDURE sp_users_update_high_score
(
pi_uuid VARCHAR(40),
pi_high_score BIGINT
)
BEGIN
DECLARE err INT default '0';
DECLARE continue handler for SQLEXCEPTION set err = -1;
START TRANSACTION;
UPDATE users
SET high_score = pi_high_score, update_date = NOW()
WHERE uuid = pi_uuid;
SELECT ROW_COUNT();
IF err < 0 THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
END $$
DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_users_update_current_deck $$
CREATE PROCEDURE sp_users_update_current_deck
(
pi_uuid VARCHAR(40),
pi_current_deck VARCHAR(45)
)
BEGIN
DECLARE err INT default '0';
DECLARE continue handler for SQLEXCEPTION set err = -1;
START TRANSACTION;
UPDATE users
SET current_deck = pi_current_deck, update_date = NOW()
WHERE uuid = pi_uuid;
SELECT ROW_COUNT();
IF err < 0 THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
END $$
DELIMITER ;
DELIMITER $$
DROP PROCEDURE IF EXISTS sp_users_update_nickname $$
CREATE PROCEDURE sp_users_update_nickname
(
pi_uuid VARCHAR(40),
pi_nickname VARCHAR(45)
)
BEGIN
DECLARE err INT default '0';
DECLARE continue handler for SQLEXCEPTION set err = -1;
START TRANSACTION;
UPDATE users
SET nickname = pi_nickname, update_date = NOW()
WHERE uuid = pi_uuid;
SELECT ROW_COUNT();
IF result < 0 THEN
ROLLBACK;
ELSE
COMMIT;
END IF;
END $$
DELIMITER ;
Jasync-sql을 사용하면 com.nhn.gameanvil.sample.common.GameConstants
클래스의 DB 접속 정보를 수정해서 연결합니다.
// DB 접속 정보
public static final String DB_USERNAME = "유저명";
public static final String DB_HOST = "호스트명";
public static final int DB_PORT = 3306;
public static final String DB_PASSWORD = "패스워드";
public static final String DB_DATABASE = "데이터베이스명";
public static final int MAX_ACTIVE_CONNECTION = 30;
Mybatis 연결은 resources/mybatid-config.xml의 접속 설정을 수정해서 연결합니다.
<!-- MySQL 접속 정보를 지정한다. -->
<properties>
<property name="hostname" value="호스트명" />
<property name="portnumber" value="3306" />
<property name="database" value="데이터베이스명" />
<property name="username" value="유저명" />
<property name="password" value="패스워드" />
</properties>
Gradle 탭의 runMain 실행으로 IntelliJ에서 실행 "gameanvil.sample-game-server [runMain]"
앞서 설정해 두었던 "SampleGameServer" 구성을 이용하여 서버를 실행합니다.
서버가 정상적으로 구동되면 아래와 같이 모든 노드에 대해 onReady 로그가 출력됩니다.
http://127.0.0.1:18400/management/nodeInfoPage 페이지를 통해서 로컬에서 실행된 노드의 상태를 확인할 수 있습니다. 모든 노드가 READY가 되면 정상 실행된 것입니다.
구성은 GameNode 4, GatewayNode 4, SupportNode2, IpcNode 1, ManagementNode 1, Locationnode 2, LocationNookupNode1, MatchNode 1, GatewayNetworkNode 1, SupportNetwotNode1 총 18개 노드가 표시됩니다.
정상적으로 서버가 실행되지 않을 경우 설정을 다시 확인하거나 log의 오류 부분을 확인하여 문의하십시오.
DB나 Redis의 경우 샘플 서버에 적용된 부분은 팀 내부에서 사용하는 부분이므로 직접 구축해 지정해야 합니다.
DB나 Redis의 설정이 없다면 샘플 서버가 정상적으로 동작하지 않습니다.
GameAnvil 버전 확인
dependencies {
api 'com.nhn.gameanvil:gameanvil:1.4.1-jdk11'
}
build.gradle 설정
import java.nio.file.Paths
plugins {
id 'java'
id 'java-library'
}
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
group = 'com.nhn.gameanvil'
version = '1.4.1'
java.sourceCompatibility = JavaVersion.VERSION_11
java.targetCompatibility = JavaVersion.VERSION_11
repositories {
mavenLocal()
mavenCentral()
}
configurations {
quasar
api.setCanBeResolved(true)
all {
resolutionStrategy {
force 'com.esotericsoftware:kryo:4.0.2'
}
}
}
// standalone jar을 생성합니다.
jar {
baseName = "sample_game_server"
duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
manifest {
attributes 'Main-Class': 'com.nhn.gameanvil.sample.Main'
}
from {
configurations.compileClasspath.collect {
it.isDirectory() ? it : zipTree(it)
}
}
}
// GameAnvil 서버를 실행합니다.
task runMain(dependsOn: build, type: JavaExec) {
jvmArgs = [
"-Xms6g",
"-Xmx6g",
"-XX:+UseG1GC"]
main = 'com.nhn.gameanvil.sample.Main'
classpath = sourceSets.main.runtimeClasspath
}
compileJava {
dependsOn.processResources
doLast {
ant.taskdef(name: 'instrumentation', classname: 'co.paralleluniverse.fibers.instrument.InstrumentationTask', classpath: configurations.api.asPath)
ant.instrumentation(verbose: 'true', check: 'true', debug: 'true') {
fileset(dir: 'build/classes/') {
include(name: '**/*.class')
}
}
}
}
dependencies {
api files(Paths.get(project.projectDir.absolutePath, './src/main/resources/META-INF/quasar-core-0.8.0-jdk11.jar').toString())
api 'org.mybatis:mybatis:3.5.3'
api 'mysql:mysql-connector-java:8.0.23'
api 'com.nhn.gameanvil:gameanvil:1.4.1-jdk11'
}
Gradle 탭의 GameAnvilTutorial > Tasks > build > jar 통해 프로젝트를 빌드합니다.
정상적으로 빌드가 완료되면 프로젝트 폴더 build/libs에 빌드된 jar 파일이 생성됩니다.
Command로 서버를 구동하려면 Gradle로 빌드된 sample_game_server-1.4.1.jar를 사용합니다.
java -Xms6g -Xmx6g -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:+UseStringDeduplication -jar sample_game_server-1.4.1.jar
실행하면 IntelliJ에서 실행한 것과 같이 onReady 로그가 표시되고, http://127.0.0.1:18400/management/nodeInfoPage 페이지에 모든 노드가 READY 되면 정상 기동된 것입니다.
게임 개발에 참고할 수 있게 만든 GameAnvil 샘플 클라이언트와 연동하기 위해 제작된 프로젝트입니다.
게임 서버 접속 전 게임 세션 정보를 불러오기 위한 서비스입니다.
런칭 정보 요청 패킷 처리
http://127.0.0.1:18600/launching?platform=Editor&appStore=GOOGLE&appVersion=1.2.0&deviceId=4D34C127-9C56-5BAB-A3C2-D8F18C0B7B6E 형식으로 요청을 합니다.
전달 받은 데이터를 파싱하고 확인하고 GatewayNode 서버의 IP, PORT를 반환해 줍니다.
Authentication.proto: 인증, 로그인
GameMulti.proto: 멀티 게임
GameSingle.proto: 싱글 게임
Result.proto: 응답 코드
User.proto: 유저
플러그인이 설치되어 있다면 다음과 같이 build.bat 파일을 마우스 오른쪽 버튼으로 클릭해 다음과 같은 명령으로 intelliJ에서 바로 변환할 수 있습니다.
메인 클래스의 GameAnvilServer를 설정하고 실행합니다. 설정할 때 클라이언트와의 프로토콜을 같은 순서로 등록하고 서버에서 사용하는 스레드 풀을 생성합니다.
GameAnvilServer gameAnvilServer = GameAnvilServer.getInstance();
// 클라이언트와 전송할 프로토콜 정의 - 순서는 클라이언트와 동일해야 한다.
gameAnvilServer.addProtoBufClass(Authentication.getDescriptor());
gameAnvilServer.addProtoBufClass(GameMulti.getDescriptor());
gameAnvilServer.addProtoBufClass(GameSingle.getDescriptor());
gameAnvilServer.addProtoBufClass(Result.getDescriptor());
gameAnvilServer.addProtoBufClass(User.getDescriptor());
// 게임에서 사용하는 DB 스레드 풀 지정
gameAnvilServer.createExecutorService(GameConstants.DB_THREAD_POOL, 100);
// 게임에서 사용하는 Redis 스레드 풀 지정
gameAnvilServer.createExecutorService(GameConstants.REDIS_THREAD_POOL, 100);
// annotation 클래스 등록 처리를 위해 scan package 지정
gameAnvilServer.addPackageToScan("com.nhn.gameanvil.sample");
// 서버 실행
gameAnvilServer.run();
GameAnvil에서 사용할 수 있도록 GatewayNode, GameNode, SupportNode, MatchMaker, Room은 @annotaion으로 클래스와 이름을 지정해야 합니다. 서버가 실행될 때 선언된 클래스들은 엔진에서 자동으로 클래스 등록 처리가 이루어집니다.
BaseConnection을 상속 받아 구현한 커넥션 클래스에 선언해 줍니다.
@Connection()
BaseGatewayNode를 상속 받아 구현한 게이트웨이 노드에 선언해 줍니다.
@GatewayNode()
BaseSession을 상속 받아 구현한 세션에 선언해 줍니다.
@Session()
BaseGameNode를 상속 받아 구현한 게임 노드에 GameAnvilConfig.json에 설정한 게임 노드의 서비스 이름을 지정합니다.
@ServiceName(GameConstants.GAME_NAME)
BaseUser를 상속 받아 구현한 게임 유저에 게임의 서비스 이름과 유저 타입을 지정합니다. 유저 타입은 클라이언트에 접속해 생성되는 타입입니다.
@ServiceName(GameConstants.GAME_NAME)
@UserType(GameConstants.GAME_USER_TYPE)
BaseRoom을 상속 받아 구현한 룸에 게임 서비스 이름과 룸 타입을 지정합니다. 룸 타입은 클라이언트에서 접속할 룸 타입을 지정합니다.
// 싱글 게임
@ServiceName(GameConstants.GAME_NAME)
@RoomType(GameConstants.GAME_ROOM_TYPE_SINGLE)
// 룸 매치 게임
@ServiceName(GameConstants.GAME_NAME)
@RoomType(GameConstants.GAME_ROOM_TYPE_MULTI_ROOM_MATCH)
// 유저 매치 게임
@ServiceName(GameConstants.GAME_NAME)
@RoomType(GameConstants.GAME_ROOM_TYPE_MULTI_USER_MATCH)
BaseUserMatchMaker, BaseRoomMatchMaker를 상속 받아 구현한 매치 메이커에는 해당 룸과 같은 서비스 이름과 룸 타입을 지정합니다.
BaseSupportNode를 상속 받아 구현한 서포트 노드에 GameAnvilConfig.json에 설정한 서포트 노드의 서비스 이름을 지정합니다.
@ServiceName(GameConstants.SUPPORT_NAME_LAUNCHING)
게임 콘텐츠에서 정의해 처리하는 패킷으로 클라이언트와 주고받는 패킷을 등록합니다. 처리하는 클래스는 등록된 종류에 맞는 패킷 핸들러 인터페이스를 구현해야 합니다.
유저가 로그인 상태에서 처리하는 패킷 : com.nhn.gameanvil.sample.game.user.GameUser
static private PacketDispatcher packetDispatcher = new PacketDispatcher();
static {
packetDispatcher.registerMsg(User.ChangeNicknameReq.getDescriptor(), CmdChangeNicknameReq.class); // 닉네임 변경 프로토콜
packetDispatcher.registerMsg(User.ShuffleDeckReq.getDescriptor(), CmdShuffleDeckReq.class); // 덱 셔플 프로토콜
packetDispatcher.registerMsg(GameSingle.ScoreRankingReq.getDescriptor(), CmdSingleScoreRankingReq.class); // 싱글 점수 랭킹
}
// 처리하는 클래스는 implements IPacketHandler<GameUser> 를 구현해서 만들어야 한다.
클라이언트에서 request로 요청 온 패킷에 대해서는 클라이언트에서 응답을 대기하고 있기 때문에 서버에서 처리하고 전달 받은 유저 객체를 통해서 gameUser.reply()로 응답 처리를 해야 합니다.
룸 안에 있을 때 처리하는 패킷 : com.nhn.gameanvil.sample.game.multi.usermatch.SnakeRoom
private static RoomPacketDispatcher dispatcher = new RoomPacketDispatcher();
static {
dispatcher.registerMsg(GameMulti.SnakeUserMsg.getDescriptor(), CmdSnakeUserMsg.class); // 유저 위치 정보
dispatcher.registerMsg(GameMulti.SnakeFoodMsg.getDescriptor(), CmdSnakeRemoveFoodMsg.class); // food 삭제 정보처리
}
// 처리할 클래스는 implements IRoomPacketHandler<SnakeRoom, GameUser>를 구현해 만들어야 한다.
서버에서 클라이언트로 전달하는 패킷은 gameUser.send()로 응답 대기 없이 전송합니다.
rest 패킷 : com.nhn.gameanvil.sample.support.LaunchingSupport
private static RestPacketDispatcher restMsgHandler = new RestPacketDispatcher();
static {
// launching
restMsgHandler.registerMsg("/launching", RestObject.GET, CmdLaunching.class);
}
// 처리할 클래스는 implements IRestPacketHandler를 구현해 만들어야 한다.
rest 요청에 대해서는 전달 받은 restObject.writeString()으로 응답 메시지를 전달합니다.
com.nhn.gameanvil.sample.redis.RedisHelper
연결 처리
private RedisClusterClient clusterClient;
private StatefulRedisClusterConnection<String, String> clusterConnection;
private RedisAdvancedClusterAsyncCommands<String, String> clusterAsyncCommands;
/**
* Redis 연결, 사용 전 최초 1회 호출하여 연결해야 한다.
*
* @param url 접속 url
* @param port 접속 port
* @throws SuspendExecution
*/
public void connect(String url, int port) throws SuspendExecution { // Redis 연결 처리
RedisURI clusterURI = RedisURI.Builder.redis(url, port).build();
// 패스워드가 필요할 경우 패스워드 설정을 추가해 RedisURI를 생성한다.
// RedisURI clusterURI = RedisURI.Builder.redis(url, port).withPassword("password").build();
this.clusterClient = RedisClusterClient.create(Collections.singletonList(clusterURI));
this.clusterConnection = Lettuce.connect(GameConstants.REDIS_THREAD_POOL, clusterClient);
this.clusterAsyncCommands = clusterConnection.async();
}
종료 처리
/**
* 접속 종료 서버가 내려가기 전에 호출되어야 한다.
*/
public void shutdown() {
clusterConnection.close();
clusterClient.shutdown();
}
사용
/**
* 유저 데이터 Redis에 저장
*
* @param gameUserInfo 유저 정보
* @return 저장 성공 여부
* @throws SuspendExecution
*/
public boolean setUserData(GameUserInfo gameUserInfo) throws SuspendExecution {
String value = GameAnvilUtil.Gson().toJson(gameUserInfo);
boolean isSuccess = false;
try {
Lettuce.awaitFuture(clusterAsyncCommands.hset(REDIS_USER_DATA_KEY, gameUserInfo.getUuid(), value)); // 해당 리턴값은 최초에 set 할 때만 true이고 있는 값 갱신 시에는 false 응답
isSuccess = true;
} catch (TimeoutException e) {
logger.error("setUserData - timeout", e);
}
return isSuccess;
}
com.nhn.gameanvil.sample.db.jasyncsql.JAsyncSqlManager
연결처리
// JAsyncSQL 연결
public JAsyncSqlManager(String username, String host, int port, String password, String database, int maxActiveConnection) {
jAsyncSql = new JAsyncSql(new com.github.jasync.sql.db.Configuration(
username,
host,
port,
password,
database), maxActiveConnection);
logger.info("JAsyncSqlManager::JAsyncSql connect");
}
종료처리
// JAsyncSQL 종료
public void close() {
jAsyncSql.disconnect();
}
사용
/**
* 유저의 최고 점수 저장
*
* @param uuid 유저 유니크 식별자
* @param highScore 수정할 최고 점수
* @return 수정된 레코드 수 반환
* @throws TimeoutException 해당 호출에 대해 timeout이 발생할 수 있음을 의미
* @throws SuspendExecution 이 메서드는 파이버를 suspend할 수 있음을 의미
*/
public int updateUserHigScore(String uuid, int highScore) throws TimeoutException, SuspendExecution {
String sql = "CALL sp_users_update_high_score( '"
+ uuid + "', "
+ highScore + ")";
QueryResult queryResult = jAsyncSql.execute(sql);
return getStoredProcedureRowCount(queryResult);
}
DB 연결 정보 설정 : resources/maybatis-config.xml
<!-- MySQL 접속 정보를 지정한다. -->
<properties>
<property name="hostname" value="호스트명" />
<property name="portnumber" value="3306" />
<property name="database" value="데이터베이스명" />
<property name="username" value="유저명" />
<property name="password" value="패스워드" />
<property name="poolPingQuery" value="select 1"/>
<property name="poolPingEnabled" value="true"/>
<property name="poolPingConnectionsNotUsedFor" value="3600000"/>
</properties>
사용할 쿼리 등록 - 외부 xml을 사용하려면 아래 주석 부분을 참고하여 사용합니다.
<mappers>
<!-- 정의된 SQL 구문을 매핑해 준다. 기본적으로 리소스 안에 있는 mapper.xml을 사용할 때 -->
<mapper resource="query/UserDataMapper.xml"/>
<!-- 외부 지정된 mapper.xml 파일을 지정할 때는 전체 경로 지정을 사용한다. -->
<!--<mapper url="file:///C:/_KevinProjects/GameServerEngine/sample-game-server/target/query/UserDataMapper.xml"/>-->
</mappers>
쿼리 : resources/query/UserDataMapper.xml
<select id="selectUserByUuid" resultType="com.nhn.gameanvil.sample.mybatis.dto.UserDto">
SELECT uuid,
login_type AS loginType,
app_version AS appVersion,
app_store AS appStore,
device_model AS deviceModel,
device_country AS deviceCountry,
device_language AS deviceLanguage,
nickname,
heart,
coin,
ruby,
level,
exp,
high_score AS highScore,
current_deck AS currentDeck,
create_date AS createDate,
update_date AS updateDate
FROM users
WHERE uuid = #{uuid}
</select>
DB 연결 설정 : com.nhn.gameanvil.sample.db.mybatis.GameSqlSessionFactory
/**
* 게임에서 사용하는 DB 연결 객체
*/
public class GameSqlSessionFactory {
private static Logger logger = LoggerFactory.getLogger(GameSqlSessionFactory.class);
private static SqlSessionFactory sqlSessionFactory;
/** XML에 명시된 접속 정보를 읽어들인다. */
// 클래스 초기화 블럭 : 클래스 변수의 복잡한 초기화에 사용된다.
// 클래스가 처음 로딩될 때 한번만 수행된다.
static {
// 접속 정보를 명시하고 있는 XML의 경로 읽기
try {
// mybatis_config.xml 파일의 경로 지정
String mybatisConfigPath = System.getProperty("mybatisConfig"); // 파라미터 전달된 경우 서버(실행 시 -DmybatisConfig= 옵션으로 지정)
logger.info("mybatisConfigPath : {}", mybatisConfigPath);
if (mybatisConfigPath != null) {
logger.info("load to mybatisConfigPath : {}", mybatisConfigPath);
InputStream inputStream = new FileInputStream(mybatisConfigPath);
if (sqlSessionFactory == null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
}
} else { // 파라미터 전달이 없는 경우 내부 파일에서 설정을 얻는다.
Reader reader = Resources.getResourceAsReader("mybatis/mybatis-config.xml");
logger.info("load to resource : mybatis/mybatis-config.xml");
// sqlSessionFactory가 존재하지 않는다면 생성한다.
if (sqlSessionFactory == null) {
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 데이터베이스 접속 객체를 통해 DATABASE에 접속한 세션를 리턴한다.
*/
public static SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}
실행 시 -DmybatisConfig=를 사용하지 않는 경우, session을 만들 때 다음과 같이 빌드 시 들어간 내부 저장 환경 파일로 설정되는 로그가 기록됩니다.
[INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:30] mybatisConfigPath : null
[INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:39] load to resource : mybatis-config.xml
실행 시 -DmybatisConfig=를 사용한 경우, session을 만들 때 다음과 같이 지정된 위치 정보로 설정되는 로그가 기록됩니다.
[INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:30] mybatisConfigPath : .\src\main\resources\mybatis-config.xml
[INFO ] [GameAnvil-DB_THREAD_POOL-0] [GameSqlSessionFactory.java:32] load to mybatisConfigPath : .\src\main\resources\mybatis-config.xml
사용 : com.nhn.gameanvil.sample.db.mybatis.UserDbHelperService
/**
* 유저 정보 DB에 저장
*
* @param gameUserInfo 유저 정보 전달
* @return 저장된 레코드 수
* @throws TimeoutException
* @throws SuspendExecution
*/
public int insertUser(GameUserInfo gameUserInfo) throws TimeoutException, SuspendExecution { // Callable 형태로 Async 실행하고 결과 리턴.
Integer resultCount = Async.callBlocking(GameConstants.DB_THREAD_POOL, new Callable<Integer>() {
@Override
public Integer call() throws Exception {
SqlSession sqlSession = GameSqlSessionFactory.getSqlSession();
try {
UserDataMapper userDataMapper = sqlSession.getMapper(UserDataMapper.class);
int resultCount = userDataMapper.insertUser(gameUserInfo.toDtoUser());
if (resultCount == 1) { // 단건 저장이기에 1개면 정상으로 디비 commit
sqlSession.commit();
}
return resultCount;
} finally {
sqlSession.close();
}
}
});
return resultCount;
}
{
//-------------------------------------------------------------------------------------
// 공통 정보.
"common": {
"ip": "127.0.0.1", // 노드마다 공통으로 사용하는 IP. (머신의 IP를 지정)
"meetEndPoints": ["127.0.0.1:18000"], // 대상 노드의 common IP와 communicatePort 등록. (해당 서버 endpoint 포함 가능 , 리스트로 여러 개 가능)
"debugMode": false // 디버깅 시 각종 timeout이 발생 안 하도록 하는 옵션, 리얼에서는 반드시 false이어야 한다.
},
//-------------------------------------------------------------------------------------
// LocationNode 설정
"location": {
"clusterSize": 1, // 총 몇 개의 머신(VM)으로 구성되는가?
"replicaSize": 1, // 복제 그룹의 크기(master + slave의 개수)
"shardFactor": 2 // sharding을 위한 인수(아래의 주석 참고)
// 전체 shard의 개수 = clusterSize x replicaSize x shardFactor
// 하나의 머신(VM)에서 구동할 shard의 개수 = replicaSize x shardFactor
// 고유한 shard의 총 개수(master shard의 개수) = clusterSize x shardFactor
},
// 매치 노드 설정
"match": {
"nodeCnt": 1
},
//-------------------------------------------------------------------------------------
// 클라이언트와의 커넥션을 관리하는 노드.
"gateway": {
"nodeCnt": 4, // 노드 개수(노드 번호는 0부터 부여됨).
"ip": "127.0.0.1", // 클라이언트와 연결되는 IP.
"dns": "", // 클라이언트와 연결되는 도메인 주소.
"connectGroup": { // 커넥션 종류.
"TCP_SOCKET": {
"port": 18200, // 클라이언트와 연결되는 포트.
"idleClientTimeout": 240000 // 데이터 송수신이 없는 상태 이후의 타임아웃(0이면 사용하지 않음).
},
"WEB_SOCKET": {
"port": 18300,
"idleClientTimeout": 0
}
}
},
//-------------------------------------------------------------------------------------
// 게임 로비 역할을 하는 노드(게임 룸, 유저를 포함하고 있음).
"game": [
{
"nodeCnt": 4,
"serviceId": 1,
"serviceName": "TapTap",
"channelIDs": ["","","","",""], // 노드마다 부여할 채널 ID(유니크하지 않아도 됨. " " 문자열로 채널 구분 없이 중복 사용도 가능).
"userTimeout": 5000 // disconnect 이후의 유저 객체 제거 타임아웃.
}
],
"support": [
{
"nodeCnt": 2,
"serviceId": 2,
"serviceName": "Launching",
"restIp": "127.0.0.1",
"restPort": 18600
}
]
}
logger를 패키지 이름 단위로 구분해서 지정할 수 있습니다. 따로 지정하면 해당 패키지 이름은 지정된 레벨로 적용되고, 없으면 root로 지정된 설정으로 적용됩니다.
<logger name="com.nhn.gameanvil" level="INFO"/>
<logger name="com.nhn.gameanvil.sample" level="DEBUG"/>
<root>
<level value="WARN"/>
<appender-ref ref="ASYNC"/>
<appender-ref ref="STDOUT"/>
</root>